Skip to content

Add the cascade cross-repository wave machinery - #3

Merged
alexander-yevsyukov merged 45 commits into
masterfrom
cascade-machinery
Sep 3, 2026
Merged

alexander-yevsyukov merged 45 commits into
masterfrom
cascade-machinery

Conversation

@alexander-yevsyukov

Copy link
Copy Markdown
Contributor

Introduces cascade, the machinery for propagating a change across the Spine
SDK repositories in dependency order, and the documentation around it. Proven
end to end: the cascade-refresh-2cd0e3b wave ran on it, publishing 15
repositories and closing with config#759.

What lands

cascade ~1600-line driver: graph, plan, status, next, prep, build, ship, refresh, park, resume, await, close
docs/dependency-graph.md The canonical build order as a Mermaid graph plus per-repo metadata, generated and re-verified by cascade graph
docs/rollout/rebuild.md Operator playbook
docs/rollout/rebuild-pr-body.md PR body applied to each wave PR
.agents/memory/, .agents/tasks/ Team memory and the wave's task record
submodules BuildSpeed, delivery-server, documentation, dokka-tools added; all pointers moved to the wave's merged masters

Design

Deterministic mechanics live in the script; judgement steps — build breakage,
pre-PR review, version edge cases — are handed back via exit code 3 with a
resume command. State is derived, never remembered: status re-proves every
fact from the registry, gh, and the git tree, so a recorded built is
demoted the moment a later commit lands. Interrupt anything and re-run it.

Two rules the wave exists to enforce:

  • PR creation is gated on the registry, not on merge. A downstream PR opens
    only once every version it pins is provably published — CI resolves from the
    registry, not from Maven Local.
  • Published-floor constants advance only at close. versionForBuildScript,
    dogfoodingVersion and the fallback* pair feed buildscript and
    pluginManagement classpaths that cannot see Maven Local, so during a wave
    they must keep pointing at the previous published generation.

Review fixes in 7c01d61

Pin-file handling. Three defects were found in cmd_close, then the same
shape was found still live in cmd_prep and derive_state. In cmd_prep it
was worse: the match was unanchored, so on ToolBase.kt a head -1 could take
the nested JavadocFilter.version — a different artifact,
spine-javadoc-filter — and the verification counted matches anywhere in the
file, so a run that clobbered the wrong constant still reported success. All
three now share anchored helpers accepting const val, private const val and
override val.

Silent aborts. A grep matching nothing exits 1; under pipefail that
killed the caller before its own diagnostic could run. cmd_prep died without
printing "constant not found", and version_of before die "cannot read".

Permissions. allow carried Bash(git push:*), which matches any
arguments, while deny named no force variant — so git push --force was
reachable. safety-rules.md requires force-push, git tag, git rebase,
gh release create and gh pr merge to stay per-action even under a grant.
Force-push is now denied, git tag asks; the other three were already covered.

For the reviewer

  • cascade never force-pushes, never touches master directly, and contains no
    gh pr merge, git rebase, or history rewriting. Branch names are always
    cascade-{refresh,ripple,retarget}-*.
  • cmd_build and cmd_ship deliberately re-implement the pre-PR and
    version-bump gates internally. Bash(./cascade:*) is one allow-listed
    invocation, so the PreToolUse hooks — which pattern-match the literal command
    text — never see the gh pr create and ./gradlew calls made inside. The
    secret-scan protection is unaffected, being a real git pre-commit hook.
  • Every sed edit to a pin file is anchored, then verified, and reverts the
    file on mismatch.
  • Not addressed, deliberately: some Markdown table rows exceed the 100-column
    limit because of cell-alignment padding. Cosmetic, and de-padding would churn
    every row; happy to do it if preferred.

Verification

  • The wave itself: 15 repositories published, closing vector merged as
    config#759.
  • bash -n clean; ./cascade status and ./cascade close re-run green and
    idempotent after the refactor.
  • Pin helpers unit-tested against the real files: ToolBase.version reads
    .422 and not the nested .75; private const val and override val both
    read; a missing constant returns empty without aborting; a write touches only
    the intended line.
  • review-docs — APPROVE WITH CHANGES (accuracy items applied).
  • spine-code-review — REQUEST CHANGES; all three Must-fix items applied above.

🤖 Generated with Claude Code

alexander-yevsyukov and others added 30 commits August 28, 2026 23:15
…ules

These three repositories consume modules already assembled here as upstream
dependencies, so cross-repository rollouts should reach them:

* `delivery-server` tracks nearly the whole SDK — `base-libraries`, `core-jvm`,
  `compiler`, `validation`, and nine more — at versions close to the tips;
* `BuildSpeed` exercises the compiler line (`compiler`, `core-jvm-compiler`,
  `core-jvm`, `validation`) to measure build-tool performance;
* `documentation` builds the spine.io site and its code samples.

All three are pinned to fixed commits and declare no tracked `branch`, so
`init-submodules` treats them as consumer-owned and leaves them alone, exactly
as it does the SDK submodules already listed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Advances 14 submodules to the tip of their `master` branch, refreshing the
snapshot of the SDK this superproject records. The remaining pins were already
at their tips.

`.agents/shared` is deliberately left unstaged: it declares a tracked `branch`
and `ignore = all`, so it floats on its own and never shows up as churn here.
Nested mounts are synced to the commits their own superprojects pin, not
floated — those pointers belong to the repositories that record them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the deterministic driver for propagating changes across the SDK
repositories in dependency order (`refresh` after a `config` merge, `ripple`
from one upstream, `retarget` for coordinated version assignment such as
`2.0.0-M1`):

* `cascade` — wave subcommands (`graph`/`plan`/`status`/`next`/`await`/`close`)
  and per-repo steps (`prep`/`build`/`ship`/`refresh`/`park`/`resume`), with
  typed exit codes: 3 = a named skill must act, 4 = waiting on an external
  event, 5 = registry-throttling halt. State is derived from git, GitHub, the
  registry, and `~/.m2` — never remembered.
* `docs/dependency-graph.md` — the standalone build-order contract: canonical
  order (audited: one-way dependencies constrain it absolutely), reduced
  production diagram, test-only and mutual-pair sections, and the complete
  machine-readable list of declared dependencies.
* `docs/rollout/rebuild.md` + `rebuild-pr-body.md` — the playbook pairing the
  script with the agent-driven judgement steps, per the proofread precedent.
* `README.md`/`docs/project.md` — links to the new contract and rollout.
* `.agents/tasks/cascade.md` — the reviewed design record with decision log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drops `Bash(git commit:*)` from `permissions.ask` — an ask rule outranks every
allow and makes unattended cascade sessions impossible, which
`safety-rules.md` → *Commits and history-writing* explicitly warns against.
Protection stays with the authorization protocol (skill-scoped commit
sections, session grants), the secret-scan `pre-commit` hook, and PR review.

Adds `allow` entries for the cascade tooling: `git commit` (summit is where
waves execute; repo-rooted sessions elsewhere keep the org default),
`./cascade`, and read-only `gh pr view`/`gh pr list`/`gh run list`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A jenv/profile JAVA_HOME may point at an older JDK (observed: corretto 11
while the SDK builds on 17, which Gradle refuses outright). The guard now
checks the resolved JVM's major version and falls back to
`/usr/libexec/java_home -v 17` instead of trusting the environment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
….bat`

The submodule now points past SpineEventEngine/documentation#87, whose
line-ending renormalization ends the perpetual phantom modification every
checkout of `gradlew.bat` used to show.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The upstream-published gate read `pin_vector`, which records only forward
(wave-ordering) upstreams. Published-floor pins point backwards in the
canonical order and never appear there, so a floor raised past the registry
was invisible and `ship` would open a PR whose CI cannot resolve it.

Derive every pinned version from the pin files by content instead, and
separate the two outcomes: an unpublished in-wave version is a wait (exit 4),
anything else is a bad pin needing a decision (exit 3).

Also correct `testlib`'s registry marker: the repo publishes `base-testlib`,
while `spine-testlib` is a separate artifact stalled at an older version, so
every probe of a current testlib version answered wrongly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Record the verified builds for logging, base-types, and change so the
derivation reaches `pr-open` instead of stalling at `bumped`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A repo opted into a wave after planning carries no `target_version`, so the
status table printed a dash for it even though the branch plainly declares a
version. Fall back to the derived value, and record the target for `money`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`prep` ran `./config/pull` unconditionally, so a repo that does not consume
`config` died on a missing script instead of taking part in the wave. Skip the
pull step when there is no `config/pull` to run.

BuildSpeed then reaches its real wall and is parked: its SDK versions arrive as
CI environment variables substituted into `settings.gradle.kts.template`, so it
has no file pins to apply, no version to bump, and nothing to publish.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The derived-version fallback referenced an out-of-scope `branch`, which
tripped `set -u` on every `status` run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`money` applies the CoreJvm Compiler plugin to generate its own
`money.proto` (`money/build.gradle.kts` applies `coreJvmCompiler.pluginId`,
and the root buildscript puts `CoreJvmCompiler.gradlePlugin` on the
classpath), while nothing depends on `money` at all. The pair is therefore
not mutual, so the audit rule applies without exception: a repository is
never placed before one it depends on.

`money` is a leaf, so moving it one place later costs nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The raw dependency relation is cyclic, so any order satisfies one direction
of each cycle from an already-published artifact. Which direction lags is a
convention. It now runs tools-first: the code-generation tools are built
against the previous generation of the libraries, and the libraries against
the current generation of the tools. A published library therefore names
tools of its own generation, and the lag moves into the tools, whose
artifacts are build-time rather than runtime.

Deriving the edges honestly first was a prerequisite. The usage pattern
rejected a preceding dot, so a fully-qualified reference such as
`classpath(io.spine.dependency.local.CoreJvmCompiler.gradlePlugin)` produced
no edge -- and that is the house style inside `buildscript {}`, where
file-level imports do not apply. Accepting the qualified form adds eight
edges (92 -> 100) and removes none:

    base-types -> compiler          money -> compiler
    change -> compiler              money -> core-jvm-compiler
    change -> core-jvm-compiler     time -> core-jvm-compiler
    delivery-server -> compiler     gcloud-jvm -> compiler

Every one of them is a library depending on a tool -- precisely the edges
the new convention promotes to build-order constraints.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It now ships as its own PR, so the closing vector PR must not cherry-pick it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Once a wave branch merges, `origin/master..<branch>` is empty, so the state
derivation read the repository as `pending` -- as though prep had never run --
and the wave asked to re-prep and rebuild repositories it had already shipped.

Check the registry for the recorded target version first: if it is served, the
repository is `published-remote` and done, whatever the branch looks like.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Owner feedback after `config#754`: a deprecated, removal-scheduled Gradle
syntax survived a rewrite of the very line carrying it, and the reviewer had
to catch it. Records the rule and the two mechanics that hid it -- `-q`
suppressing deprecation output, and a failed build aborting before the
summary prints.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alexander-yevsyukov and others added 12 commits September 2, 2026 16:08
The repository publishes `io.spine.tools:dokka-extensions`, the Dokka plugin
every other repository loads to hide `@Internal` declarations, so it belongs
in the graph and in the canonical order. It is registered as a submodule here
for the same reason as its siblings.

One edge is derived, `dokka-tools -> base-libraries`, from the plugin's use of
`io.spine.annotation.Internal`. `Logging` appears in its build only inside a
`force(...)` list, which is version management rather than a dependency, and
correctly yields no edge.

The reverse direction cannot be derived at all: every repository takes the
plugin through `buildSrc/src/main/kotlin/DokkaExts.kt`, which `config`
distributes identically, so no repository-owned build file declares it. A new
section records that, rather than leaving the graph to imply the plugin has no
consumers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both warnings that Spine builds emit come from plugins we apply, not from
our own scripts. The detekt one is easy to misread as a `config` defect
because its stack frame names `detekt-code-analysis.gradle.kts`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three defects in the pin-advance loop, all latent until now — `close`
had never run.

1. It matched `val version` anywhere in a pin file, including nested
   objects that declare *different* artifacts on independent version
   lines. `ToolBase.JavadocFilter.version` is `spine-javadoc-filter`,
   published at `2.0.0-SNAPSHOT.75`; closing this wave would have
   rewritten it to `tool-base`'s `2.0.0-SNAPSHOT.422`, which returns
   404. Every consumer would have taken that pin on the next
   `./config/pull`.

2. Restricting the match to the pin object's own constants must still
   accept every declaration form in use. `Time` is a `Dependency()`
   subclass and writes `override val version`, while `Compiler` writes
   `private const val fallbackVersion`.

3. A `grep` that matches nothing exits 1, and under `pipefail` that
   aborted the whole run part-way — leaving some pins advanced, none
   committed, and no error printed. It now warns and continues, so a
   pin file whose shape changed is loud rather than silently skipped.

The object's own constants sit at indent 4 and nested ones deeper, so
anchoring both the scan and the substitution there selects exactly the
intended set. Verified against the current pin files: the only indent-8
match in any of them is the `JavadocFilter` line that must be skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `cascade-refresh-2cd0e3b` wave is complete: every repository published,
and `config` now carries the version vector recorded by config#759.

Each submodule moves from its wave branch to the master that now contains
it. `dokka-tools` moves off `fix-javadoc-publication`, merged during the
same stretch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
**Pin-file handling, factored into one place.** `cmd_close` had three
defects fixed earlier in this branch; review found the same shape still
live in `cmd_prep` and `derive_state`, which run far more often. In
`cmd_prep` it was worse than at `close`: the match was unanchored, so on
`ToolBase.kt` a `head -1` could take the nested
`JavadocFilter.version` — a different artifact — and the verification
step counted matches anywhere in the file, so a run that clobbered the
wrong constant still reported success.

All three now share `read_pin_constant`, `read_pin_constant_at` and
`write_pin_constant`, anchored to the pin object's own indent-4
constants and accepting every declaration form in use: `const val`,
`private const val`, and the `override val` of a `Dependency()`
subclass.

**Silent aborts.** A `grep` matching nothing exits 1, and under
`pipefail` that killed the caller before its own diagnostic could run —
so `cmd_prep` died without printing "constant not found", and
`version_of` died before `die "cannot read $prop"`. Both now absorb the
no-match and let the caller report.

**Permissions.** `allow` carried `Bash(git push:*)`, which matches any
arguments, while `deny` named no force variant — so `git push --force`
was reachable. `safety-rules.md` is explicit that force-push, `git tag`,
`git rebase`, `gh release create` and `gh pr merge` stay per-action even
under a grant. Force-push is now denied and `git tag` asks; the other
three were already covered.

**Documentation accuracy.** Only `ship` performs the upstream drift
check, not `status`. Not every exit-3 message names a skill. The
prerequisites omitted the `read:packages` scope, without which `ship`
and `close` cannot verify `delivery-server`, which publishes to GitHub
Packages — a gap that blocked this very wave. One prose example used
Mermaid's invalid single-dash arrow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 3, 2026 16:54
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-03T16:59:35.796660Z 7c01d61 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There is at least one confirmed functional issue in cascade (ripple slug/version handling) plus smaller correctness/clarity issues that should be addressed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds the new cascade driver to orchestrate deterministic cross-repository “waves” across Spine SDK submodules (plan/status/next/prep/build/ship/close), alongside documentation that defines and explains the canonical dependency/build order contract used to sequence those waves.

Changes:

  • Introduce cascade (bash) with wave planning, state derivation, gating, and PR automation.
  • Add/extend documentation for the dependency graph contract and the operator playbook for rebuild waves.
  • Update repo layout docs and submodule configuration to reflect new participating repositories/tooling.
File summaries
File Description
README.md Links repository layout to the canonical dependency/build-order contract and updates submodule tables.
docs/rollout/rebuild.md Operator playbook describing how to run rebuild waves using cascade.
docs/rollout/rebuild-pr-body.md Standard PR body template used when cascade ship opens per-repo PRs.
docs/project.md Documents rebuild waves as a cross-repository workflow and links to cascade + the dependency graph.
docs/dependency-graph.md Canonical, reviewed build order + dependency diagrams and per-repo metadata consumed by cascade.
cascade New wave orchestration script implementing graph verification, planning, state derivation, and per-repo steps.
.gitmodules Adds new submodules (e.g., delivery-server, BuildSpeed, documentation, dokka-tools).
.claude/settings.json Updates tool/skill permission configuration for running cascade (skipped per repo guidelines).
.agents/tasks/cascade.md Wave/cascade design and task record (skipped per repo guidelines).
.agents/tasks/cascade-refresh-2cd0e3b.md Recorded wave task state (skipped per repo guidelines).
.agents/tasks/cascade-refresh-2cd0e3b.json Machine manifest for a wave (skipped per repo guidelines).
.agents/memory/MEMORY.md Team memory index (skipped per repo guidelines).
.agents/memory/fix-deprecations-in-touched-code.md Team guidance on deprecation cleanup (skipped per repo guidelines).
Review details

Suppressed comments (1)

cascade:853

  • plan --kind ripple --seed <repo> allows omitting @<version>, but the slug is still built from seed_ver (cascade-ripple-$seed-${seed_ver##*.}), producing a trailing - and not freezing the seed version at plan time. This can lead to ambiguous/non-deterministic wave identifiers.
    case "$kind" in refresh|ripple|retarget) ;; *) usage_plan ;; esac
    [ "$kind" = ripple ] && [ -z "$seed" ] && usage_plan
    [ "$kind" = retarget ] && [ -z "$target" ] && usage_plan

    "$0" graph --check

    # Trigger + slug.
    local cfg_sha slug
    git -C "$SUMMIT/config" fetch --quiet origin "$BASE" 2>/dev/null || true
    cfg_sha="$(git -C "$SUMMIT/config" rev-parse "origin/$BASE" 2>/dev/null \
               || git -C "$SUMMIT/config" rev-parse HEAD)"
    case "$kind" in
        refresh)  slug="cascade-refresh-$(echo "$cfg_sha" | cut -c1-7)" ;;
        ripple)   slug="cascade-ripple-$seed-${seed_ver##*.}" ;;
        retarget) slug="cascade-retarget-$target" ;;
    esac
  • Files reviewed: 33/33 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread cascade
Comment thread docs/rollout/rebuild.md Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7c01d61c4a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cascade
Comment thread cascade Outdated
Comment thread cascade Outdated
Comment thread cascade Outdated
Comment thread cascade
@alexander-yevsyukov alexander-yevsyukov self-assigned this Sep 3, 2026
@alexander-yevsyukov alexander-yevsyukov moved this to 🏗 In progress in v2.0 Sep 3, 2026
alexander-yevsyukov and others added 2 commits September 3, 2026 18:03
**A park did not hold the whole downstream.** `next` only looked at
direct upstreams whose *stored* state was literally `parked`. With
`A -> B -> C` and `A` parked, `B` was blocked but `C` was not, so `next`
could schedule `prep` for `C` against an unbuilt `B`. Blocking is now
computed in a pass over `.order`, which is topological, so it
propagates.

**A parked repo held the wave open forever.** `done_all` required
`published-remote` from every repo, and a park is terminal — `close`
skips parked repos. `BuildSpeed` is parked on every refresh, so `CLOSE`
could never be emitted and `next` returned `WAIT` indefinitely. With
this change `next` reports `DONE` for the completed wave, where before
it reported `WAIT`.

**Rebuilding at an unchanged version left stale artifacts.**
`publishToMavenLocal` ran only when the marker directory was absent.
`refresh` rebuilds a repo at the same version by design, so the
directory existed and publication was skipped — downstream builds then
resolved the previous HEAD's artifacts while the manifest recorded the
new HEAD as built. Publication now follows every successful build.

**A published ripple seed was scheduled for a version bump.** `plan`
initialized every scoped repo as `pending`, including a seed named as
`repo@version`, which by definition is already published. `next` then
scheduled `prep` for it and bumped it a second time. Such a seed now
starts terminal at the supplied version; downstream repos still read
that version through `.seed.version`.

**`close --ship` skipped the review gate.** It pushed and opened the
config PR without checking the sentinel. The script's own `git push`
and `gh pr create` never reach the PreToolUse hooks, so the documented
close -> pre-pr -> close --ship step rested on operator memory. It now
applies the same HEAD-bound gate as `ship`.

Also: `floor_edges()` was defined twice with identical bodies; the
duplicate is gone. And the `JAVA_HOME` prerequisite gave a macOS-only
incantation without saying so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The rule keyed on the lines being edited, which misses the case that
actually recurs: a wave propagates a new dependency version, that version
deprecates API our untouched code uses, and nothing in the diff points at
the warning. By the old wording nobody owned those lines, so the warning
could ride through every repo the wave touches and surface only at the
removal, far from the change that caused it.

Restated around the warnings a change introduces, by either route — the
line was edited, or the baseline moved under it.

Bounded with the split `cascade-adapt` already uses for build breakage:
a deprecation with a documented, behaviour-preserving successor is fixed
in the same PR; one whose replacement changes behaviour, or whose surface
needs design judgement, parks the repo with a diagnosis instead of being
guessed at to keep a wave moving.

Renamed accordingly; the third-party attribution notes are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@alexander-yevsyukov alexander-yevsyukov moved this from 🏗 In progress to In Review in v2.0 Sep 3, 2026
The `cascade-refresh-2cd0e3b` wave is finished — every repository
published and the closing vector merged — so its manifest and task file
move to `.agents/tasks/archive/`. `active_wave` globs
`$TASKS_DIR/cascade-*.json` without recursing, so an archived wave stops
being auto-selected, which is what completion should mean.

The move surfaced two defects in the script:

`cmd_status` declared and assigned in one statement:

    local wave="${1:-$(active_wave)}"

`local` returns its own exit status, so a failing substitution was
masked, `errexit` never fired, and `active_wave`'s `die` did not stop the
script — it carried on with an empty wave and printed a second,
contradictory error. Splitting the declaration lets the failure surface
once. This was unreachable while a manifest always existed.

`manifest_path` looked only in the top level, so archiving a wave made it
unreadable by the tool that wrote it, even when named. It now falls back
to `archive/`, keeping a completed wave inspectable via
`CASCADE_WAVE=<slug> ./cascade status` while staying out of the glob.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@alexander-yevsyukov
alexander-yevsyukov merged commit 77b39bb into master Sep 3, 2026
1 check passed
@alexander-yevsyukov
alexander-yevsyukov deleted the cascade-machinery branch September 3, 2026 17:44
@github-project-automation github-project-automation Bot moved this from In Review to ✅ Done in v2.0 Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

3 participants