okstra 0.165.3 → 0.166.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/for-ai/skills/okstra-run.md +2 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/host-orchestration/implementation.md +3 -0
- package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
- package/runtime/prompts/profiles/_implementation-deliverable.md +3 -2
- package/runtime/prompts/profiles/_implementation-executor.md +3 -2
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +1 -1
- package/runtime/python/okstra_ctl/cmux.py +9 -7
- package/runtime/python/okstra_ctl/consumers.py +73 -24
- package/runtime/python/okstra_ctl/dispatch_core.py +3 -3
- package/runtime/python/okstra_ctl/dispatch_state.py +21 -1
- package/runtime/python/okstra_ctl/team.py +30 -6
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +1 -0
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +18 -1
- package/runtime/python/okstra_ctl/worktree.py +17 -1
- package/runtime/schemas/final-report-v1.0.schema.json +4 -0
- package/runtime/schemas/final-report-v2.0.schema.json +4 -0
- package/runtime/skills/okstra-run/SKILL.md +5 -2
- package/runtime/templates/implementation-worker-preamble.md +4 -3
- package/runtime/templates/reports/final-report.template.md +3 -0
- package/runtime/templates/worker-prompt-preamble.md +4 -3
- package/runtime/validators/validate-run.py +7 -0
|
@@ -225,12 +225,13 @@ When `task-type == implementation` and the render-args `chain-stages` CSV has 2
|
|
|
225
225
|
|
|
226
226
|
1. Re-call render-bundle with the same arguments but `--stage N` (the base commit is auto-computed by prepare from the predecessor's done `head_commit` — do not pass it by hand). The `io`-only conformance waiver·concurrent-run·git-reconcile gates apply identically to each stage's render-bundle.
|
|
227
227
|
2. As in Step 6, become the host-native Okstra lead and run that stage's Phase 1–7 inline. Phase 6's lead persistence appends that stage's `status:"done"` row to `runs/<plan-task-key>/consumers.jsonl`.
|
|
228
|
-
3. After confirming the `done` row was written, move to the next stage. Clean up context (leftover panes·finished teammates) at each stage boundary.
|
|
228
|
+
3. After confirming the `done` row was written, move to the next stage. Clean up context (leftover panes·finished teammates) at each stage boundary. A `status:"failed"` row in place of `done` means the stage ended `FAIL` — stop the queue per the FAIL branch below.
|
|
229
229
|
4. One-line report at each stage start/finish: `stage N/<total> start` / `stage N done → next K`.
|
|
230
230
|
|
|
231
231
|
Once the whole queue is consumed, end the chain and report completion.
|
|
232
232
|
|
|
233
233
|
- **Next stage not yet ready — normal termination:** When a stage in the queue is occupied by another implementation run as started/reserved and render-bundle is rejected with `--stage N already in progress or reserved by another run` (StageTargetError), this is not an exception — **terminate the chain normally** and report the remaining queue (e.g. `remaining queue: stage 4, 5 — resume with okstra-run after occupancy is released`).
|
|
234
|
+
- **Stage ended FAIL — stop the queue and report:** When a stage's synthesised verdict is `FAIL`, Phase 6 writes no carry sidecar and appends a `status:"failed"` row instead of `done`. **Stop the queue there** and report the failed stage, its report path, and the remaining queue. Do not continue to the next stage even when it is dependency-independent — later work must not be stacked on a confirmed regression. The `failed` row frees the occupancy, so `--stage <N>` re-enters that stage on its preserved worktree and branch.
|
|
234
235
|
- **Exception gate during chaining:** If render-bundle raises a concurrent-run conflict or git stale-SHA reconciliation, **stop the chain at that stage** and present the gate to the user per the Step 5 procedure. Once the user resolves it, resume the remaining queue in place. Data corruption·concurrent-occupancy conflicts are confirmed by a human — this is the safety boundary of unattended chaining.
|
|
235
236
|
|
|
236
237
|
## Forbidden patterns
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -53,5 +53,8 @@ If the anchor (`implementation_base_commit`) is reported unresolvable, run the s
|
|
|
53
53
|
### Next stage not yet ready — normal termination (not an exception gate)
|
|
54
54
|
Because of the dependency closure, the chain queue **may include a stage that another implementation run has occupied as started/reserved.** That stage's `render-bundle` is rejected with `--stage N already in progress or reserved by another run` (StageTargetError). This is **not** an exception gate needing human judgment but a "next stage not yet ready" situation. On this rejection, **terminate the chain normally** and report the remaining queue to the user (e.g. `remaining queue: stage 4, 5 — resume with okstra-run after occupancy is released`). This is a different branch from the exception gate below (data corruption·concurrent-occupancy conflict confirmation).
|
|
55
55
|
|
|
56
|
+
### Stage ended FAIL — stop the queue and report (not an exception gate)
|
|
57
|
+
When a stage's synthesised verdict is `FAIL`, Phase 6 writes no carry sidecar and appends a `status:"failed"` row in place of `done` (`prompts/profiles/_implementation-deliverable.md` "Lead post-stage persistence"). **Stop the queue at that stage** and report the failed stage, its report path, and the remaining queue (e.g. `stage 1 FAIL — remaining queue: stage 2, 3, 5; re-enter with okstra-run --stage 1 after the fix`). Do **not** continue to the next stage even when that stage is dependency-independent: an unattended chain that keeps building past a confirmed regression stacks later work on top of it. The `failed` row releases the stage's occupancy, so `--stage <N>` re-enters the same stage on its preserved worktree and branch — there is nothing to unblock by hand.
|
|
58
|
+
|
|
56
59
|
### Exception gate during chaining
|
|
57
60
|
If `render-bundle` raises Step 5's concurrent-run conflict detection (concurrent-run branch) or git stale-SHA reconciliation (git-reconcile branch), **stop the chain at that stage** and present the gate to the user exactly as Step 5 prescribes. Once the user resolves the gate, resume the chain in place (continue with the remaining queue). Data corruption·concurrent-occupancy conflicts are confirmed by a human — this is the safety boundary of unattended chaining. (Unlike the "not ready" rejection above, these two branches do not discard the queue; they wait for user resolution.)
|
|
@@ -99,9 +99,9 @@ Required checkpoints:
|
|
|
99
99
|
- `PROGRESS: phase-1-intake complete` — after all intake reads return.
|
|
100
100
|
- `PROGRESS: phase-2-prompts preparing <N> worker prompts` — at the start of Phase 2, before any `Write` to the assigned prompt paths.
|
|
101
101
|
- `PROGRESS: phase-3-team-create <adapter-specific-status>` — after selected-adapter setup is recorded in team-state. The stable phase id is retained for artifact compatibility.
|
|
102
|
-
- `PROGRESS: phase-4-dispatch worker=<role> model=<model>` — once per worker, immediately before `dispatch_worker`.
|
|
102
|
+
- `PROGRESS: phase-4-dispatch worker=<role> model=<model>` — once per worker, immediately before `dispatch_worker`. `<role>` is the **roster** role, exactly as team-state's `workers[].role` records it (`Claude worker`, `Codex worker`) — the checkpoint is matched against that entry, so a phase-specific functional label (`Claude verifier`, `Codex executor`) names no roster worker and fails the check. Only `claude-worker`-style hyphenation of the same roster role is also accepted.
|
|
103
103
|
- `PROGRESS: phase-5-poll pending=<n> done=<m>` — emitted on each wakeup while the pending set is non-empty.
|
|
104
|
-
- `PROGRESS: phase-5-collect worker=<role> status=<terminal-status>` — once per worker, immediately after the result file is verified.
|
|
104
|
+
- `PROGRESS: phase-5-collect worker=<role> status=<terminal-status>` — once per worker, immediately after the result file is verified. `<role>` is the roster role, same rule as `phase-4-dispatch` above.
|
|
105
105
|
- `PROGRESS: phase-5.5-convergence round=<N> queue=<count>` — at the start of each convergence round (Phase 5.5).
|
|
106
106
|
- `PROGRESS: phase-5.6-critic provider=<provider> gaps=<n>` — after the critic result is collected (Phase 5.6, opt-in; the critic dispatch itself fires concurrently with the first 5.5 reverify round). Omitted when `convergence.critic.enabled == false`.
|
|
107
107
|
- `PROGRESS: phase-batch-cleanup panes=<n>` — immediately after cleaning up the previous batch's panes, at each batch boundary (① just before the first `phase-5.5-convergence` round ② just before the `phase-6-synthesis` report-writer dispatch). `<n>` is the number of panes reclaimed at that boundary — worker-compute panes plus completed teammate panes, which are panes too — read from the cleanup's `--list` pass taken immediately before the reclaim, never estimated. Expose only the counts and NEVER expose `%NNN`/lead-pane.id/raw worker handles. Just before the first batch (analysis-worker dispatch) there is nothing to clean up, so it is a no-op and the marker is omitted.
|
|
@@ -59,7 +59,8 @@ are collected and convergence finished. Phase 1-5 do not need it.
|
|
|
59
59
|
|
|
60
60
|
- Parse the executor's `### Stage Carry Evidence` JSON block. If absent or unparsable, end with status `contract-violated` and route to a follow-up `error-analysis`.
|
|
61
61
|
- The `### Stage Carry Evidence` JSON may include `designPrepEvidence[]`. Emit a row only when this stage produced concrete evidence that refines an effective PREP item: `itemId`, the injected `assessmentFingerprint`, `resolution`, and non-empty `evidence[]` are required; `overrides` is optional and only records observed, non-authoritative refinements. Carry evidence never represents user approval. Downstream resolution accepts it only from transitive dependency stages with the matching fingerprint.
|
|
62
|
-
-
|
|
63
|
-
-
|
|
62
|
+
- **A `FAIL` synthesised verdict withholds the two writes below.** They are what marks the stage `done`, so performing them on a stage whose verifier found a blocking defect stacks the next stage on a confirmed regression. When the synthesised verdict is `FAIL`: write NO carry sidecar, and append a `status:"failed"` row in place of the `done` row — same `okstra_ctl.consumers.append_consumer` call, carrying `report_path` and the SHA of HEAD. That row is terminal *without* completion: dependent stages stay blocked because this stage is not done, while its worktree-registry occupancy is released so a fix run can re-enter the same stage number — `--stage <N>` reuses the preserved worktree and branch instead of provisioning a new one. State the reason in the report's `Stage sidecar evidence` section as `withheld`. **Enforced:** `validators/validate-run.py` `_validate_stage_carry_sidecar_exists` accepts a missing carry file only when that field is non-empty, so silently skipping the sidecar still fails the run.
|
|
63
|
+
- On a non-`FAIL` verdict, for this run's single stage: write its JSON verbatim to `runs/<impl-task-key>/carry/stage-<N>.json`. Refuse to overwrite an existing file (one stage = one sidecar; a fix run re-entering after a `failed` row writes the first one, because a withheld stage never wrote it).
|
|
64
|
+
- On a non-`FAIL` verdict, for this run's single stage: append a `status:"done"` row to `runs/<plan-task-key>/consumers.jsonl` with `completed_at`, `carry_path`, `report_path` (this run's final-report path relative to the run root), and the SHA of HEAD. Append it with `okstra_ctl.consumers.append_consumer` (NOT a raw filesystem write) — that call honours the consumers lock AND releases this stage's worktree-registry occupancy, so later runs stop seeing a finished stage as a concurrent run. `report_path` lets `final-verification` cite each stage's originating report when assembling its Source Implementation Report list.
|
|
64
65
|
- The verifier round, Phase 5.5 convergence, and this Phase 6 report run **once per run** over this stage's diff — NOT per step.
|
|
65
66
|
- Quote this stage's new contents (the sidecar JSON in full and the new consumers row by itself) in the final report's `Stage sidecar evidence` deliverable section.
|
|
@@ -11,7 +11,7 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
|
|
|
11
11
|
|
|
12
12
|
## Executor role binding (carried over from the thin core)
|
|
13
13
|
|
|
14
|
-
- **Executor dispatch labelling.** The core functional role label is `<provider>-executor` (e.g. `codex-executor`). Provider, role, and model identity are owned by `prompts/lead/okstra-lead-contract.md` "Model assignments"; the selected runtime adapter owns provider-native dispatch-label mapping (including any `name` / `**Pane role:**` fields) and token-attribution wiring under its "Semantic operation mapping".
|
|
14
|
+
- **Executor dispatch labelling.** The core functional role label is `<provider>-executor` (e.g. `codex-executor`). Provider, role, and model identity are owned by `prompts/lead/okstra-lead-contract.md` "Model assignments"; the selected runtime adapter owns provider-native dispatch-label mapping (including any `name` / `**Pane role:**` fields) and token-attribution wiring under its "Semantic operation mapping". This functional label is NOT what the run's PROGRESS checkpoints carry: `phase-4-dispatch` / `phase-5-collect` name the roster role team-state records (`Codex worker`), because that is the entry the Phase 7 conformance check matches them against.
|
|
15
15
|
- The `Executor` (bound in `implementation.md` thin core) is the **only worker permitted to mutate project files**. All other workers run read-only. A `runner=native-session` executor uses the selected host adapter's native edit and command primitives. A `runner=cli-wrapper` executor mutates files inside its provider CLI's auto-edit mode. The safety rules in this sidecar apply identically to both runners.
|
|
16
16
|
- When the thin core's Task worktree block resolves status to `created` or `reused`, the Executor MUST run every Edit / Write / build / test / commit command with the worktree path as cwd. Treat it as `project_root` for the duration of this run. Do NOT mutate the caller's original checkout. Do NOT `cd` out of the worktree to reach files. If a file outside the worktree is genuinely needed, treat it as a planning gap: record it in `Out-of-plan edits` and continue.
|
|
17
17
|
- **How to set the working directory**: every command and native edit MUST target `{{EXECUTOR_WORKTREE_PATH}}`, never the lead session's original project directory. The selected runtime adapter owns the exact native command syntax. Provider CLI wrappers inject the worktree at the CLI layer. For tools that accept an explicit working-directory flag (`git -C <path>`, `cargo --manifest-path`, `pytest --rootdir`), prefer that form.
|
|
@@ -82,7 +82,8 @@ persisted prompt lacks the heading `Coding-conventions preflight`
|
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
The file MUST NOT exist before the run starts (overwrite is refused — see `--force-stage` non-goal). **Enforced:** `validators/validate-run.py` `_validate_stage_carry_sidecar_exists` fails a run that declares `stageSidecarEvidence` without the file on disk. Transcribing the JSON into the report is not the same as writing it: `consumers` treats the carry file as the source of truth for marking the stage `done`, so a missing file leaves the stage permanently incomplete and blocks every dependent stage with a `PrepareError` — while this run reports success.
|
|
85
|
-
- **
|
|
85
|
+
- **An external Tier 3 non-PASS does NOT withhold the carry evidence.** A Tier 3 entry whose `requires` include `http`, `external`, or `db` is advisory. Its FAIL, MISSING, no result, startup failure, or credential / network / service absence gets recorded honestly — exact command, exit code, output tail, marked `ADVISORY` in `Validation evidence` — and you emit the carry evidence anyway. Only Tier 1 and Tier 2 failures withhold it. Withholding on an external result is what actually blocks the stage: the carry file is the only thing that can mark a stage `done`, the verifier re-runs that same command from the host (where a call your sandbox could not complete often passes), and a stage the verifier then PASSes can never be closed because its evidence was never written.
|
|
86
|
+
- **Reverse link (BLOCKING).** The runtime already appended a `status:"started"` row for this stage before the run began. The terminal row belongs to the lead's post-stage persistence and is verdict-gated — `status:"done"` with `carry_path` on a non-`FAIL` verdict, `status:"failed"` on `FAIL` (`_implementation-deliverable.md` §"Lead post-stage persistence").
|
|
86
87
|
- **No PR / push in this phase.** This run produces local commits, carry sidecar evidence, verifier results, and the implementation final report only. Push and PR creation belong exclusively to the later `release-handoff` phase after `final-verification` returns `accepted`.
|
|
87
88
|
|
|
88
89
|
## Allowed actions during the run
|
|
@@ -9,7 +9,7 @@ at Phase 5, BEFORE constructing the verifier worker dispatch prompts.
|
|
|
9
9
|
|
|
10
10
|
## Verifier roles (resolved at run-prep time)
|
|
11
11
|
|
|
12
|
-
- **Verifier dispatch labelling.** The core functional role label is `<provider>-verifier` (here, and identically in `final-verification`). Provider, role, and model identity are owned by `prompts/lead/okstra-lead-contract.md` "Model assignments"; the selected runtime adapter owns provider-native dispatch-label mapping (including any `name` / `**Pane role:**` fields) and token-attribution wiring under its "Semantic operation mapping".
|
|
12
|
+
- **Verifier dispatch labelling.** The core functional role label is `<provider>-verifier` (here, and identically in `final-verification`). Provider, role, and model identity are owned by `prompts/lead/okstra-lead-contract.md` "Model assignments"; the selected runtime adapter owns provider-native dispatch-label mapping (including any `name` / `**Pane role:**` fields) and token-attribution wiring under its "Semantic operation mapping". This functional label is NOT what the run's PROGRESS checkpoints carry: `phase-4-dispatch` / `phase-5-collect` name the roster role team-state records (`Claude worker`, `Codex worker`), because that is the entry the Phase 7 conformance check matches them against.
|
|
13
13
|
- The verifier slots are `Claude verifier` and `Codex verifier`, plus `Antigravity verifier` **only when `antigravity` is in the resolved `--workers` roster**. Every verifier in the resolved roster is dispatched regardless of which provider holds the executor role; the executor's own provider is run *separately* as a verifier (a fresh CLI session with no shared context) so that no verdict is produced from the same session that wrote the diff. Verifiers MUST NOT call Edit, Write, or any Bash command that mutates files outside the run's artifact directories. If a verifier wants a fix, it records the recommendation in its worker result; it does not apply the fix itself.
|
|
14
14
|
- Session isolation — not model-variant divergence — is the primary self-review safeguard: each verifier is a separate CLI invocation with its own context window, so reusing the same model variant for executor and same-provider verifier is acceptable. Different model variants (e.g. executor=opus / Claude verifier=sonnet) remain recommended when available.
|
|
15
15
|
- Phase-specific model defaults override the shared defaults: `Claude verifier`=`opus`, `Codex verifier`=`gpt-5.6-sol`, `Antigravity verifier`=`gemini-3.1-pro` (only when present in the roster). The `Executor`'s model is taken from the provider-specific worker model corresponding to `--executor`: claude→`--claude-model` (default `opus`), codex→`--codex-model` (default `gpt-5.6-sol`), antigravity→`--antigravity-model` (default `gemini-3.1-pro`).
|
|
@@ -196,7 +196,7 @@ For a `host-text` mapping, render each numbered item as its option label followe
|
|
|
196
196
|
|
|
197
197
|
## Run-scoped resource lifecycle
|
|
198
198
|
|
|
199
|
-
- At run start, record `teamName` as the audit label and `teamCreate
|
|
199
|
+
- At run start, record `teamName` as the audit label in team-state and populate `lead.sessionId`; the session transcript lives under `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl`. You do NOT write `teamCreate`: `okstra team dispatch` records the implicit-team marker (`{ attempted: false, status: "implicit" }`) itself, on every dispatch path, because v2.1.178 made that value a constant rather than a judgment. The one marker that IS yours is the concurrent-run decision — a concurrent run records `teamCreate: { attempted: false, status: "skipped", reason: "concurrent-run" }` **before** the first dispatch, and dispatch then leaves it alone.
|
|
200
200
|
- Record the lead pane once with `mkdir -p "<RUN_DIR>/state" && { . "$HOME/.okstra/bin/lib/okstra/tmux-pane.sh" 2>/dev/null && okstra_resolve_caller_pane; } > "<RUN_DIR>/state/lead-pane.id" 2>/dev/null || true`. This is silent setup and must not gate cleanup; the cleanup script protects the lead pane itself.
|
|
201
201
|
- Collect and persist token usage before any live-roster cleanup, including cleanup between batches and the run-end shutdown sequence.
|
|
202
202
|
- Before each new worker batch (and before the next phase's render-bundle), reclaim the prior round's completed teammate panes in two passes, adding `--keep report-writer-worker` to **both** passes while the report writer is in flight. First source the count: `$HOME/.okstra/bin/okstra-trace-cleanup.sh --list --run-dir "<RUN_DIR>" [--keep report-writer-worker]` never kills and prints one `<pane_id>\t<pane_title>` line per pane it would reclaim — count those lines as `<n>`. Then perform the reclaim by running the same command **without** `--list`, and emit the neutral contract's `PROGRESS: phase-batch-cleanup panes=<n>` checkpoint with that count. Call both passes after collecting that round's results and token usage and before the next dispatch, so no in-flight worker pane is caught. This `tmux kill-pane`s the harness teammate panes; `shutdown_request` only idles the agent and never frees the pane, so it stays part of the run-end sequence for roster/token hygiene. In a non-tmux session there are no panes, both passes no-op, and `<n>` is `0` — still emit the checkpoint. The lead pane (read from `<RUN_DIR>/state/lead-pane.id`) is always preserved.
|
|
@@ -473,9 +473,9 @@ def rpc(method: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
|
473
473
|
def _open_worker_surface(
|
|
474
474
|
workspace: str, placement: Placement, target: PaneGeometry
|
|
475
475
|
) -> str:
|
|
476
|
-
before =
|
|
476
|
+
before = open_surface_ids(workspace)
|
|
477
477
|
_create_surface(workspace, placement, target)
|
|
478
|
-
new_ids =
|
|
478
|
+
new_ids = open_surface_ids(workspace) - before
|
|
479
479
|
if len(new_ids) != 1:
|
|
480
480
|
raise RuntimeError(
|
|
481
481
|
f"cmux opened {len(new_ids)} surfaces where exactly one was expected"
|
|
@@ -483,12 +483,14 @@ def _open_worker_surface(
|
|
|
483
483
|
return new_ids.pop()
|
|
484
484
|
|
|
485
485
|
|
|
486
|
-
def
|
|
487
|
-
"""Every surface UUID
|
|
486
|
+
def open_surface_ids(workspace: str) -> set[str]:
|
|
487
|
+
"""Every surface UUID the workspace currently holds.
|
|
488
488
|
|
|
489
|
-
|
|
490
|
-
the `OK surface:N` echo, because `list-pane-surfaces`
|
|
491
|
-
focused pane unless given a `--pane`, and the new pane is
|
|
489
|
+
Dispatch diffs this set across a create to identify the new surface, rather
|
|
490
|
+
than translating the `OK surface:N` echo, because `list-pane-surfaces`
|
|
491
|
+
reports only the focused pane unless given a `--pane`, and the new pane is
|
|
492
|
+
not focused. Teardown intersects its recorded ids with it to tell a surface
|
|
493
|
+
that is still open from one that closed earlier in the run.
|
|
492
494
|
"""
|
|
493
495
|
return {
|
|
494
496
|
surface_id
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""Append-only writer / reader for `consumers.jsonl` under a plan run's task root.
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
A stage's lifecycle position is the LAST lifecycle row written for it. An append
|
|
4
|
+
is redundant only when it would not move that position, so the same
|
|
5
|
+
(started / done / failed) record is never duplicated, while a `started` that
|
|
6
|
+
re-enters a stage whose last row is terminal does land.
|
|
6
7
|
force_reappend=True 인 보정 append 만 같은 tuple 을 다른 head_commit 으로 재기록할 수 있다."""
|
|
7
8
|
|
|
8
9
|
from __future__ import annotations
|
|
@@ -17,6 +18,15 @@ from .run_context import consumers_mutex
|
|
|
17
18
|
|
|
18
19
|
CONSUMERS_FILENAME = "consumers.jsonl"
|
|
19
20
|
|
|
21
|
+
# The rows that move a stage through its lifecycle. `started` claims the stage;
|
|
22
|
+
# `done` and `failed` both end that claim. `failed` is terminal WITHOUT
|
|
23
|
+
# completion: dependents stay blocked because the stage is not done, but the
|
|
24
|
+
# stage-key occupancy is released so the same stage number can be re-entered by
|
|
25
|
+
# a fix run. Without it a stage whose verifier returned FAIL stays `active`
|
|
26
|
+
# forever — `done` would be the only exit, and writing it would mark a stage
|
|
27
|
+
# carrying a confirmed regression as complete.
|
|
28
|
+
STAGE_LIFECYCLE_STATUSES = ("started", "done", "failed")
|
|
29
|
+
|
|
20
30
|
|
|
21
31
|
@dataclass(frozen=True)
|
|
22
32
|
class StageConsumerState:
|
|
@@ -71,19 +81,30 @@ def read_stage_consumer_state(
|
|
|
71
81
|
return stage_consumer_state_from_rows(rows)
|
|
72
82
|
|
|
73
83
|
|
|
84
|
+
def last_lifecycle_status_by_stage(
|
|
85
|
+
rows: List[Dict[str, Any]],
|
|
86
|
+
) -> Dict[int, str]:
|
|
87
|
+
"""stage → 마지막 lifecycle row 의 status. 파일이 append-only 이므로
|
|
88
|
+
읽기 순서가 곧 시간 순서다."""
|
|
89
|
+
out: Dict[int, str] = {}
|
|
90
|
+
for r in rows:
|
|
91
|
+
stage = r.get("stage")
|
|
92
|
+
status = r.get("status")
|
|
93
|
+
if status in STAGE_LIFECYCLE_STATUSES and isinstance(stage, int):
|
|
94
|
+
out[stage] = status
|
|
95
|
+
return out
|
|
96
|
+
|
|
97
|
+
|
|
74
98
|
def stage_consumer_state_from_rows(rows: List[Dict[str, Any]]) -> StageConsumerState:
|
|
75
99
|
done_rows = [r for r in rows if r.get("status") == "done"]
|
|
76
100
|
done_by_stage = latest_done_by_stage(rows)
|
|
77
|
-
|
|
78
|
-
r["stage"] for r in rows
|
|
79
|
-
if r.get("status") == "started" and isinstance(r.get("stage"), int)
|
|
80
|
-
}
|
|
101
|
+
last_status = last_lifecycle_status_by_stage(rows)
|
|
81
102
|
return StageConsumerState(
|
|
82
103
|
rows=rows,
|
|
83
104
|
done_rows=done_rows,
|
|
84
105
|
done_by_stage=done_by_stage,
|
|
85
106
|
done_stages=set(done_by_stage.keys()),
|
|
86
|
-
started_stages=started,
|
|
107
|
+
started_stages={n for n, s in last_status.items() if s == "started"},
|
|
87
108
|
verified_accepted_stages=verified_accepted_stages(rows),
|
|
88
109
|
pr_covered_stages=pr_covered_stages(rows),
|
|
89
110
|
)
|
|
@@ -92,8 +113,9 @@ def stage_consumer_state_from_rows(rows: List[Dict[str, Any]]) -> StageConsumerS
|
|
|
92
113
|
def append_consumer(plan_run_root: Path, *, impl_task_key: str, stage: int,
|
|
93
114
|
status: str, force_reappend: bool = False,
|
|
94
115
|
**fields: Any) -> None:
|
|
95
|
-
if status not in
|
|
96
|
-
|
|
116
|
+
if status not in STAGE_LIFECYCLE_STATUSES:
|
|
117
|
+
allowed = " or ".join(repr(s) for s in STAGE_LIFECYCLE_STATUSES)
|
|
118
|
+
raise ValueError(f"status must be {allowed}, got: {status!r}")
|
|
97
119
|
with consumers_mutex(plan_run_root):
|
|
98
120
|
if not _equivalent_row_exists(plan_run_root, impl_task_key, stage,
|
|
99
121
|
status, force_reappend,
|
|
@@ -105,16 +127,23 @@ def append_consumer(plan_run_root: Path, *, impl_task_key: str, stage: int,
|
|
|
105
127
|
**fields,
|
|
106
128
|
}
|
|
107
129
|
_append_row(plan_run_root, record)
|
|
108
|
-
#
|
|
130
|
+
# 종결 status 는 점유 해제 이벤트이기도 하다 — 중복 append(no-op)에서도 풀어야
|
|
109
131
|
# release 없이 done 만 기록된 과거 run 의 잔존 점유가 다음 호출에서 치유된다.
|
|
110
132
|
if status == "done":
|
|
111
133
|
_release_stage_reservation(impl_task_key, stage)
|
|
134
|
+
elif status == "failed":
|
|
135
|
+
_release_stage_occupancy_keeping_branch(impl_task_key, stage)
|
|
112
136
|
|
|
113
137
|
|
|
114
138
|
def _equivalent_row_exists(plan_run_root: Path, impl_task_key: str, stage: int,
|
|
115
139
|
status: str, force_reappend: bool,
|
|
116
140
|
head_commit: Any) -> bool:
|
|
117
|
-
|
|
141
|
+
rows = read_consumers(plan_run_root)
|
|
142
|
+
# 같은 tuple 이 이미 있어도, 그 뒤에 다른 lifecycle row 가 왔다면 이 append 는
|
|
143
|
+
# stage 의 현재 위치를 옮기는 새 사실이다 — fix run 의 started 재기록이 그 경우다.
|
|
144
|
+
if last_lifecycle_status_by_stage(rows).get(stage) != status:
|
|
145
|
+
return False
|
|
146
|
+
for row in rows:
|
|
118
147
|
if (row.get("impl_task_key") == impl_task_key
|
|
119
148
|
and row.get("stage") == stage
|
|
120
149
|
and row.get("status") == status):
|
|
@@ -125,22 +154,42 @@ def _equivalent_row_exists(plan_run_root: Path, impl_task_key: str, stage: int,
|
|
|
125
154
|
return False
|
|
126
155
|
|
|
127
156
|
|
|
128
|
-
def
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
점유 주체가 아니므로 건너뛴다."""
|
|
157
|
+
def _stage_registry_coords(
|
|
158
|
+
impl_task_key: str, stage: Any,
|
|
159
|
+
) -> Optional[tuple[str, str, str]]:
|
|
160
|
+
"""stage 점유의 registry 좌표. TASK_KEY(`project:group:task`) 각 segment 의
|
|
161
|
+
safe-segment 와 같다(stage 예약이 그렇게 만들어진다). 형식이 다르면 점유
|
|
162
|
+
주체가 아니므로 None."""
|
|
135
163
|
parts = impl_task_key.split(":")
|
|
136
164
|
if len(parts) != 3 or not isinstance(stage, int):
|
|
137
|
-
return
|
|
165
|
+
return None
|
|
138
166
|
from .ids import _safe_fs_segment
|
|
167
|
+
return (_safe_fs_segment(parts[0]), _safe_fs_segment(parts[1]),
|
|
168
|
+
_safe_fs_segment(parts[2]))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _release_stage_reservation(impl_task_key: str, stage: Any) -> None:
|
|
172
|
+
"""done 이 기록된 stage 의 worktree-registry 점유(stage-key)를 해제하고
|
|
173
|
+
브랜치 슬롯도 반납한다. worktree 디렉토리·브랜치 자체는 보존된다."""
|
|
174
|
+
coords = _stage_registry_coords(impl_task_key, stage)
|
|
175
|
+
if coords is None:
|
|
176
|
+
return
|
|
139
177
|
from . import worktree_registry
|
|
140
|
-
worktree_registry.release(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
178
|
+
worktree_registry.release(*coords, stage_number=stage)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _release_stage_occupancy_keeping_branch(
|
|
182
|
+
impl_task_key: str, stage: Any,
|
|
183
|
+
) -> None:
|
|
184
|
+
"""failed 이 기록된 stage 의 점유 표시만 푼다 — 브랜치 슬롯은 유지한다.
|
|
185
|
+
|
|
186
|
+
fix run 은 같은 stage 워크트리·브랜치로 재진입하므로 슬롯이 계속 필요하다
|
|
187
|
+
(`worktree_registry.release_status` 의 "slot is still needed" 경우)."""
|
|
188
|
+
coords = _stage_registry_coords(impl_task_key, stage)
|
|
189
|
+
if coords is None:
|
|
190
|
+
return
|
|
191
|
+
from . import worktree_registry
|
|
192
|
+
worktree_registry.release_status(*coords, stage_number=stage)
|
|
144
193
|
|
|
145
194
|
|
|
146
195
|
def _append_row(plan_run_root: Path, record: Dict[str, Any]) -> None:
|
|
@@ -27,7 +27,7 @@ from .dispatch_state import (
|
|
|
27
27
|
require_string as _require_string,
|
|
28
28
|
resolve_project_path as _resolve_project_path,
|
|
29
29
|
resolve_required_path as _resolve_required_path,
|
|
30
|
-
|
|
30
|
+
record_dispatch_facts as _record_dispatch_facts,
|
|
31
31
|
transition_worker_status as _transition_worker_status,
|
|
32
32
|
string_list as _string_list,
|
|
33
33
|
string_value as _string_value,
|
|
@@ -213,14 +213,14 @@ def dispatch_plan(plan: DispatchPlan, *, wait: bool = True) -> int:
|
|
|
213
213
|
"instead of running them concurrently; dispatch panes with "
|
|
214
214
|
"wait=False"
|
|
215
215
|
)
|
|
216
|
-
|
|
216
|
+
_record_dispatch_facts(plan.team_state_path, _dispatch_mode(plan.jobs))
|
|
217
217
|
for job in plan.jobs:
|
|
218
218
|
result = _dispatch_job_with_retry(plan, job)
|
|
219
219
|
if result != 0:
|
|
220
220
|
return result
|
|
221
221
|
return 0
|
|
222
222
|
handles = [_spawn_job(plan, job, 1) for job in plan.jobs]
|
|
223
|
-
|
|
223
|
+
_record_dispatch_facts(plan.team_state_path, _mode_from_handles(handles))
|
|
224
224
|
return 0
|
|
225
225
|
|
|
226
226
|
|
|
@@ -275,12 +275,32 @@ def _utc_timestamp(value: str | datetime | None) -> str:
|
|
|
275
275
|
return instant.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
276
276
|
|
|
277
277
|
|
|
278
|
-
def
|
|
278
|
+
def record_dispatch_facts(team_state_path: Path, dispatch_mode: str) -> None:
|
|
279
|
+
"""Persist the team-state facts okstra owns at dispatch time.
|
|
280
|
+
|
|
281
|
+
`dispatchMode` names the backend the workers actually went out on.
|
|
282
|
+
`teamCreate` is the implicit-team audit marker: Claude Code v2.1.178 removed
|
|
283
|
+
the TeamCreate tool, so `{attempted: false, status: "implicit"}` is a
|
|
284
|
+
constant of a non-concurrent run rather than a lead judgment. validate-run
|
|
285
|
+
requires it once any worker has been dispatched, and Phase 7 token
|
|
286
|
+
attribution reads it to locate worker sessions — leaving it for the lead to
|
|
287
|
+
hand-write failed every dispatch path whose host adapter does not spell the
|
|
288
|
+
rule out. A marker the launch prompt pre-recorded (the concurrent-run
|
|
289
|
+
`skipped` decision) belongs to that run and is never overwritten.
|
|
290
|
+
"""
|
|
279
291
|
payload = load_json_object(team_state_path, "team-state")
|
|
280
292
|
payload["dispatchMode"] = dispatch_mode
|
|
293
|
+
if not _has_recorded_team_create(payload):
|
|
294
|
+
payload["teamCreate"] = {"attempted": False, "status": "implicit"}
|
|
281
295
|
write_json(team_state_path, payload)
|
|
282
296
|
|
|
283
297
|
|
|
298
|
+
def _has_recorded_team_create(team_state: Mapping[str, Any]) -> bool:
|
|
299
|
+
existing = team_state.get("teamCreate")
|
|
300
|
+
return (isinstance(existing, dict)
|
|
301
|
+
and bool(str(existing.get("status") or "").strip()))
|
|
302
|
+
|
|
303
|
+
|
|
284
304
|
# --- job facts ----------------------------------------------------------------
|
|
285
305
|
|
|
286
306
|
def dispatch_mode(jobs: Sequence[WorkerJob]) -> str:
|
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import argparse
|
|
11
11
|
import json
|
|
12
|
+
import subprocess
|
|
12
13
|
import sys
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from typing import Any, Mapping, Sequence
|
|
@@ -202,11 +203,11 @@ def _reclaimable_panes(
|
|
|
202
203
|
) -> list[dict[str, str]]:
|
|
203
204
|
"""Everything this run owns and may close.
|
|
204
205
|
|
|
205
|
-
Under cmux the recorded ids are the
|
|
206
|
-
to sweep with, and scanning by title would be worse than nothing: cmux
|
|
207
|
-
its own agent surfaces with the same glyph okstra's tmux cleanup
|
|
208
|
-
teammate marker, so a sweep could close the lead. Only surfaces
|
|
209
|
-
created are recorded, so only those can be closed.
|
|
206
|
+
Under cmux the recorded ids are the only candidates. There is no per-pane tag
|
|
207
|
+
API to sweep with, and scanning by title would be worse than nothing: cmux
|
|
208
|
+
labels its own agent surfaces with the same glyph okstra's tmux cleanup
|
|
209
|
+
treats as a teammate marker, so a sweep could close the lead. Only surfaces
|
|
210
|
+
okstra created are recorded, so only those can be closed.
|
|
210
211
|
"""
|
|
211
212
|
seen: set[str] = set()
|
|
212
213
|
panes: list[dict[str, str]] = []
|
|
@@ -214,13 +215,36 @@ def _reclaimable_panes(
|
|
|
214
215
|
if isinstance(record, dict):
|
|
215
216
|
_append_pane(panes, seen, str(record.get("paneId", "")), "worker")
|
|
216
217
|
if _is_cmux_run(manifest):
|
|
217
|
-
return panes
|
|
218
|
+
return _still_open_surfaces(panes)
|
|
218
219
|
lead_pane = tmux.resolve_caller_pane()
|
|
219
220
|
for pane in tmux.list_run_panes(run_dir, lead_pane=lead_pane):
|
|
220
221
|
_append_pane(panes, seen, pane.pane_id, pane.kind)
|
|
221
222
|
return panes
|
|
222
223
|
|
|
223
224
|
|
|
225
|
+
def _still_open_surfaces(panes: list[dict[str, str]]) -> list[dict[str, str]]:
|
|
226
|
+
"""The recorded surfaces cmux still shows.
|
|
227
|
+
|
|
228
|
+
`workerDispatches` is append-only and nothing prunes it, so a surface closed
|
|
229
|
+
at an earlier round boundary stays recorded for the rest of the run. Taking
|
|
230
|
+
the ledger as the residual set makes the run-end cleanup gate offer to close
|
|
231
|
+
panes that left the screen rounds ago, on a workspace holding none.
|
|
232
|
+
|
|
233
|
+
An unreachable cmux keeps the ledger rather than reporting an empty set: a
|
|
234
|
+
wedged app is not evidence that the surfaces are gone, closing one that
|
|
235
|
+
already went away is a silent no-op, and skipping a live one strands it on
|
|
236
|
+
the user's screen for the rest of the session.
|
|
237
|
+
"""
|
|
238
|
+
workspace = cmux.resolve_lead_workspace()
|
|
239
|
+
if not workspace:
|
|
240
|
+
return panes
|
|
241
|
+
try:
|
|
242
|
+
open_ids = cmux.open_surface_ids(workspace)
|
|
243
|
+
except (RuntimeError, OSError, subprocess.SubprocessError):
|
|
244
|
+
return panes
|
|
245
|
+
return [pane for pane in panes if pane["paneId"] in open_ids]
|
|
246
|
+
|
|
247
|
+
|
|
224
248
|
def _append_pane(panes: list[dict[str, str]], seen: set[str], pane_id: str, kind: str) -> None:
|
|
225
249
|
if pane_id and pane_id not in seen:
|
|
226
250
|
panes.append({"paneId": pane_id, "kind": kind})
|
|
@@ -38,6 +38,23 @@ READ_SCOPE_HEADER = (
|
|
|
38
38
|
)
|
|
39
39
|
EVIDENCE_LEDGER_HEADER = "**Evidence ledger:** required-v1"
|
|
40
40
|
|
|
41
|
+
# `required-v1` is a mode name; what it demands lives in the Worker Preamble's
|
|
42
|
+
# "Evidence read ledger". But the preamble reaches the worker as a *path* —
|
|
43
|
+
# eager-include inlines only the role sidecars — so a worker that never opens it
|
|
44
|
+
# gets the switch without the rule, and Phase 7 then fails its result over a
|
|
45
|
+
# citation form the prompt never stated. Restating it inline is what
|
|
46
|
+
# READ_SCOPE_HEADER does, for the same reason: the rule has to be in front of the
|
|
47
|
+
# worker before its first citation, not in a file it may never read.
|
|
48
|
+
EVIDENCE_CITATION_HEADER = (
|
|
49
|
+
"**Evidence citations:** Append one `- Evidence read: <project-relative "
|
|
50
|
+
"path, no line suffix>` row to the audit sidecar for every file you open as "
|
|
51
|
+
"claim evidence, and cite that file in your result with backticks, a line "
|
|
52
|
+
"suffix, and the identical project-relative path — `src/config/env.ts:1-22`, "
|
|
53
|
+
"never the bare filename `env.ts:1-22`. A bare filename does not match its "
|
|
54
|
+
"ledger row and fails exactly like a file you never opened, however many "
|
|
55
|
+
"times you cited the full path earlier."
|
|
56
|
+
)
|
|
57
|
+
|
|
41
58
|
# `agy`'s write tool validates the target against the Gemini artifact store
|
|
42
59
|
# whenever the model attaches ArtifactMetadata, and rejects every path outside
|
|
43
60
|
# `~/.gemini/antigravity-cli/brain/<uuid>/`. All okstra worker outputs live under
|
|
@@ -103,7 +120,7 @@ def worker_prompt_headers(
|
|
|
103
120
|
f"**Coding preflight pack:** {_coding_preflight_pack_path(active_context)}"
|
|
104
121
|
)
|
|
105
122
|
if dispatch_kind == "initial" and plan.audience != "report-writer":
|
|
106
|
-
headers
|
|
123
|
+
headers += [EVIDENCE_LEDGER_HEADER, EVIDENCE_CITATION_HEADER]
|
|
107
124
|
headers.extend([
|
|
108
125
|
f"**Errors log path:** {errors_log_path}",
|
|
109
126
|
f"**Errors sidecar path:** {errors_sidecar_path}",
|
|
@@ -219,7 +219,7 @@ def resolve_stage_worktree_decision(
|
|
|
219
219
|
safe_task = _safe_segment(task_id_segment)
|
|
220
220
|
existing = worktree_registry.lookup(
|
|
221
221
|
safe_project, safe_group, safe_task, stage_number=stage_number)
|
|
222
|
-
if existing is not None and existing
|
|
222
|
+
if existing is not None and _stage_entry_is_reusable(existing):
|
|
223
223
|
return StageWorktreeDecision(
|
|
224
224
|
status="reused",
|
|
225
225
|
path=existing.worktree_path,
|
|
@@ -237,6 +237,22 @@ def resolve_stage_worktree_decision(
|
|
|
237
237
|
)
|
|
238
238
|
|
|
239
239
|
|
|
240
|
+
def _stage_entry_is_reusable(entry: worktree_registry.WorktreeEntry) -> bool:
|
|
241
|
+
"""Whether a registered stage worktree can be entered by this run.
|
|
242
|
+
|
|
243
|
+
`active` is the live-run case. `released` with the directory still on disk is
|
|
244
|
+
the fix-run case: a stage whose verifier returned FAIL records a `failed`
|
|
245
|
+
consumers row, which frees the occupancy but deliberately keeps the worktree
|
|
246
|
+
and its branch as the reviewable stack. Re-entry MUST reuse that tree —
|
|
247
|
+
provisioning anew refuses on the existing path and branch. After whole-task
|
|
248
|
+
final-verification removes the directory the entry stops being reusable, so
|
|
249
|
+
the stage provisions from scratch.
|
|
250
|
+
"""
|
|
251
|
+
if entry.status == "active":
|
|
252
|
+
return True
|
|
253
|
+
return entry.status == "released" and Path(entry.worktree_path).is_dir()
|
|
254
|
+
|
|
255
|
+
|
|
240
256
|
def _safe_segment(value: str) -> str:
|
|
241
257
|
"""Sanitise a single path/branch segment.
|
|
242
258
|
|
|
@@ -320,6 +320,9 @@ If the anchor (`implementation_base_commit`) is reported unresolvable, run the s
|
|
|
320
320
|
### Next stage not yet ready — normal termination (not an exception gate)
|
|
321
321
|
Because of the dependency closure, the chain queue **may include a stage that another implementation run has occupied as started/reserved.** That stage's `render-bundle` is rejected with `--stage N already in progress or reserved by another run` (StageTargetError). This is **not** an exception gate needing human judgment but a "next stage not yet ready" situation. On this rejection, **terminate the chain normally** and report the remaining queue to the user (e.g. `remaining queue: stage 4, 5 — resume with okstra-run after occupancy is released`). This is a different branch from the exception gate below (data corruption·concurrent-occupancy conflict confirmation).
|
|
322
322
|
|
|
323
|
+
### Stage ended FAIL — stop the queue and report (not an exception gate)
|
|
324
|
+
When a stage's synthesised verdict is `FAIL`, Phase 6 writes no carry sidecar and appends a `status:"failed"` row in place of `done` (`prompts/profiles/_implementation-deliverable.md` "Lead post-stage persistence"). **Stop the queue at that stage** and report the failed stage, its report path, and the remaining queue (e.g. `stage 1 FAIL — remaining queue: stage 2, 3, 5; re-enter with okstra-run --stage 1 after the fix`). Do **not** continue to the next stage even when that stage is dependency-independent: an unattended chain that keeps building past a confirmed regression stacks later work on top of it. The `failed` row releases the stage's occupancy, so `--stage <N>` re-enters the same stage on its preserved worktree and branch — there is nothing to unblock by hand.
|
|
325
|
+
|
|
323
326
|
### Exception gate during chaining
|
|
324
327
|
If `render-bundle` raises Step 5's concurrent-run conflict detection (concurrent-run branch) or git stale-SHA reconciliation (git-reconcile branch), **stop the chain at that stage** and present the gate to the user exactly as Step 5 prescribes. Once the user resolves the gate, resume the chain in place (continue with the remaining queue). Data corruption·concurrent-occupancy conflicts are confirmed by a human — this is the safety boundary of unattended chaining. (Unlike the "not ready" rejection above, these two branches do not discard the queue; they wait for user resolution.)
|
|
325
328
|
<!-- END FRAGMENT: host-orchestration-implementation -->
|
|
@@ -341,12 +344,12 @@ Queue = the topologically-sorted stage list from splitting `orchestration.chainS
|
|
|
341
344
|
|
|
342
345
|
1. Call Step 5's `render-bundle` with the same arguments but `--stage N` (the base commit is auto-computed by prepare from the predecessor's done `head_commit`, so do not pass it by hand). Step 5's blocking local conformance waiver offer·concurrent-run detection·git-reconcile gates apply identically to each stage's `render-bundle`.
|
|
343
346
|
2. As in Step 6, become the host-native Okstra lead and run that stage's Phase 1–7 inline. Phase 6's lead post-stage persistence appends that stage's `status:"done"` row to `runs/<plan-task-key>/consumers.jsonl` (per the implementation profile directive).
|
|
344
|
-
3. After confirming that `done` row was written, reclaim the completed teammate panes of the stage you just finished: run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<the run dir of the stage you just completed>"` (add `--keep report-writer-worker` if the report writer is still in flight). Then move to the next stage.
|
|
347
|
+
3. After confirming that `done` row was written, reclaim the completed teammate panes of the stage you just finished: run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<the run dir of the stage you just completed>"` (add `--keep report-writer-worker` if the report writer is still in flight). Then move to the next stage. A `status:"failed"` row instead of `done` means the stage ended `FAIL` — reclaim the panes the same way, then stop the queue per "Stage ended FAIL" above.
|
|
345
348
|
4. One-line report at each stage start/finish: `stage N/<total> start` / `stage N done → next K`.
|
|
346
349
|
|
|
347
350
|
Once the whole queue is consumed, end the chain and report completion to the user.
|
|
348
351
|
|
|
349
|
-
The
|
|
352
|
+
The three branches that end or pause the queue — "Next stage not yet ready", "Stage ended FAIL", and "Exception gate during chaining" — are in the host orchestration rules block above.
|
|
350
353
|
|
|
351
354
|
## Persisting the PR template scope (release-handoff)
|
|
352
355
|
|
|
@@ -51,9 +51,10 @@ Every initial implementation prompt begins with these generated common anchors i
|
|
|
51
51
|
6. `**Worker Error Contract Path:** <absolute-path>`
|
|
52
52
|
7. `**Coding preflight pack:** <absolute-path>`
|
|
53
53
|
8. `**Evidence ledger:** required-v1`
|
|
54
|
-
9. `**
|
|
55
|
-
10. `**Errors
|
|
56
|
-
11. `**
|
|
54
|
+
9. `**Evidence citations:** <rule>` — the ledger-row and citation-path rule of §"Evidence read ledger", restated inline so it is in front of you before your first citation.
|
|
55
|
+
10. `**Errors log path:** <absolute-path>`
|
|
56
|
+
11. `**Errors sidecar path:** <absolute-path>`
|
|
57
|
+
12. `**Read scope:** <allowlist>`
|
|
57
58
|
|
|
58
59
|
The implementation body additionally carries `**Worktree:**` and its role-sidecar inputs. Do not synthesize any missing path.
|
|
59
60
|
|
|
@@ -674,6 +674,9 @@ Carried-forward plan items retain their prior verdicts verbatim; each such item
|
|
|
674
674
|
### 5.7.5 Stage Sidecar Evidence
|
|
675
675
|
|
|
676
676
|
- Stage: `{{ implementation.stageSidecarEvidence.stageNumber }}` — {{ implementation.stageSidecarEvidence.stageTitle }}
|
|
677
|
+
{% if implementation.stageSidecarEvidence.withheld -%}
|
|
678
|
+
- Carry sidecar **withheld** — not written to `carry/stage-{{ implementation.stageSidecarEvidence.stageNumber }}.json`: {{ implementation.stageSidecarEvidence.withheld }}
|
|
679
|
+
{% endif -%}
|
|
677
680
|
- Carry sidecar JSON:
|
|
678
681
|
```json
|
|
679
682
|
{{ implementation.stageSidecarEvidence.carryJson | indent(2) }}
|
|
@@ -43,9 +43,10 @@ Every initial analysis prompt begins with these generated anchors in this exact
|
|
|
43
43
|
6. `**Worker Preamble Path:** <absolute-path>` — selects this analysis preamble.
|
|
44
44
|
7. `**Worker Error Contract Path:** <absolute-path>` — shared by every initial audience.
|
|
45
45
|
8. `**Evidence ledger:** required-v1`
|
|
46
|
-
9. `**
|
|
47
|
-
10. `**Errors
|
|
48
|
-
11. `**
|
|
46
|
+
9. `**Evidence citations:** <rule>` — the ledger-row and citation-path rule of §"Evidence read ledger", restated inline so it is in front of you before your first citation.
|
|
47
|
+
10. `**Errors log path:** <absolute-path>`
|
|
48
|
+
11. `**Errors sidecar path:** <absolute-path>`
|
|
49
|
+
12. `**Read scope:** <allowlist>`
|
|
49
50
|
|
|
50
51
|
`final-verification` additionally carries its six verification-target anchors. `improvement-discovery` carries `**Phase 1.5 Grilling Log:**`. Reverify prompts are lightweight and do not use this preamble.
|
|
51
52
|
|
|
@@ -5061,6 +5061,13 @@ def _validate_stage_carry_sidecar_exists(
|
|
|
5061
5061
|
stage = evidence.get("stageNumber")
|
|
5062
5062
|
if not isinstance(stage, int):
|
|
5063
5063
|
return
|
|
5064
|
+
# A stage whose verifier returned FAIL must NOT persist its carry: the carry
|
|
5065
|
+
# file is what marks the stage `done`, and doing that would stack the next
|
|
5066
|
+
# stage on a confirmed regression. Such a run states the reason in
|
|
5067
|
+
# `withheld` and records a `failed` consumers row instead, so the absent
|
|
5068
|
+
# file is the correct outcome, not a gap.
|
|
5069
|
+
if str(evidence.get("withheld") or "").strip():
|
|
5070
|
+
return
|
|
5064
5071
|
# Carry sidecars are stage-SHARED: the next stage's carry-in and
|
|
5065
5072
|
# `consumers.backfill_done_from_carry` glob them without knowing the
|
|
5066
5073
|
# producing run's layout. `RunRef.carry()` owns that flat-vs-staged rule;
|