okstra 0.165.2 → 0.165.4

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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/runtime/BUILD.json +2 -2
  3. package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
  4. package/runtime/prompts/profiles/_implementation-deliverable.md +3 -2
  5. package/runtime/prompts/profiles/_implementation-diff-review.md +2 -2
  6. package/runtime/prompts/profiles/_implementation-executor.md +22 -15
  7. package/runtime/prompts/profiles/_implementation-verifier.md +4 -4
  8. package/runtime/prompts/profiles/_stage-discipline.md +4 -3
  9. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +1 -1
  10. package/runtime/python/okstra_ctl/cmux.py +9 -7
  11. package/runtime/python/okstra_ctl/consumers.py +73 -24
  12. package/runtime/python/okstra_ctl/dispatch_core.py +3 -3
  13. package/runtime/python/okstra_ctl/dispatch_state.py +21 -1
  14. package/runtime/python/okstra_ctl/domain/wizard/interaction.py +17 -5
  15. package/runtime/python/okstra_ctl/implementation_stage.py +2 -1
  16. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +104 -12
  17. package/runtime/python/okstra_ctl/path_hints.py +10 -2
  18. package/runtime/python/okstra_ctl/render.py +5 -0
  19. package/runtime/python/okstra_ctl/stage_map.py +10 -4
  20. package/runtime/python/okstra_ctl/team.py +30 -6
  21. package/runtime/python/okstra_ctl/wizard.py +19 -2
  22. package/runtime/python/okstra_ctl/worker_prompt_body.py +24 -4
  23. package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -1
  24. package/runtime/python/okstra_ctl/worker_prompt_headers.py +18 -1
  25. package/runtime/python/okstra_ctl/worker_prompt_policy.py +13 -2
  26. package/runtime/python/okstra_ctl/worktree.py +17 -1
  27. package/runtime/schemas/final-report-v1.0.schema.json +4 -0
  28. package/runtime/schemas/final-report-v2.0.schema.json +4 -0
  29. package/runtime/templates/implementation-worker-preamble.md +10 -3
  30. package/runtime/templates/reports/final-report.template.md +3 -0
  31. package/runtime/templates/worker-prompt-preamble.md +4 -3
  32. package/runtime/validators/validate-implementation-plan-stages.py +19 -2
  33. package/runtime/validators/validate-run.py +7 -0
  34. package/src/commands/execute/wizard.mjs +20 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.165.2",
3
+ "version": "0.165.4",
4
4
  "description": "Host-aware multi-provider cross-verification orchestrator runtime and agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.165.2",
3
- "builtAt": "2026-08-11T05:13:20.960Z",
2
+ "package": "0.165.4",
3
+ "builtAt": "2026-08-11T10:11:57.736Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -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
- - 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; re-runs are out of scope for this version).
63
- - 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.
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.
@@ -19,7 +19,7 @@ prompt (agents/workers/_cli-wrapper-template.md → Prompt Composition).
19
19
 
20
20
  # Pre-commit diff review sweep (BLOCKING — before the executor's final commit)
21
21
 
22
- Lint/test green is necessary but NOT sufficient self-mocked tests, interaction-only assertions, untruthful names, and unreadable functions all pass a green pipeline. This sweep is what keeps them out of the diff. Run it once, after the last `Edit` / `Write` of the stage, before the final commit. Fix every finding in place (you are the executor — you may edit); this is a prevention pass, not a report you hand off.
22
+ This is where the preflight's conventions get enforced against the code you actually wrote. Run it once, after the last `Edit` / `Write` of the stage, before the final commit. Fix every finding in place (you are the executor — you may edit); this is a prevention pass, not a report you hand off.
23
23
 
24
24
  Do not scan holistically and stop when it "looks fine". Work the matrix exhaustively — the failure mode this gate exists to prevent is a real defect surviving because you eyeballed the diff instead of enumerating it.
25
25
 
@@ -48,4 +48,4 @@ End your audit-sidecar entry for this sweep with a one-line `Coverage:` footer n
48
48
 
49
49
  ## Graceful degradation
50
50
 
51
- When the routed coding-preflight pack is unreadable (codex / antigravity runtime, or the files are absent), do NOT skip the sweep — apply the language-agnostic principles the preflight already listed (no self-mocking, behavioral assertions, truthful/standalone names, single-purpose ≤50-line functions) plus the project's `CLAUDE.md` / `CONTRIBUTING` / lint config, and record `diff-review: resource-unavailable → applied <agnostic principles + project rules>` with the Coverage footer. Never claim a resource read that did not happen.
51
+ When the routed coding-preflight pack is unreadable (codex / antigravity runtime, or the files are absent), do NOT skip the sweep — fall back to the always-binding principles the preflight enumerates plus the project's `CLAUDE.md` / `CONTRIBUTING` / lint config, and record `diff-review: resource-unavailable → applied <agnostic principles + project rules>` with the Coverage footer.
@@ -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.
@@ -19,17 +19,25 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
19
19
 
20
20
  ## Pre-implementation context exploration (executor before first edit)
21
21
 
22
- - **Coding-conventions preflight (BLOCKING runs before the first `Edit` / `Write`, and binds the TDD loop below).** The gate body is a single source at `prompts/profiles/_coding-conventions-preflight.md` (sibling of this sidecar). Do NOT re-type that content from memory deliver it by file so it cannot drift or be dropped:
23
- - **Native-session executor:** Read `_coding-conventions-preflight.md` end-to-end before the first edit, then state in ONE line which conventions apply (e.g. `Applying TS + hexagonal overlay; domain at src/domains/*/domain/`).
24
- - **CLI-wrapper executor (BLOCKING):** the executor process does NOT share the lead's context, and it cannot read this sidecar's directory — that path sits outside the CLI sandbox and the CLI only sees its prompt, so a file reference never reaches it. The lead MUST physically append the **body** of `_coding-conventions-preflight.md` into the persisted executor prompt at dispatch time. Never hand-retype it. Enforcement: CLI wrapper agents refuse an implementation-Executor dispatch whose persisted prompt lacks the literal heading `Coding-conventions preflight`, returning `<SENTINEL_PREFIX>_PREFLIGHT_MISSING` (see `agents/workers/_cli-wrapper-template.md` → Prompt Composition).
25
- - **Pre-commit diff review sweep (BLOCKING — runs AFTER the last `Edit` / `Write`, BEFORE the final commit).** The gate body is a single source at `prompts/profiles/_implementation-diff-review.md` (sibling of this sidecar): an exhaustive file×rule sweep of the actual diff against the conventions the preflight loaded, with fix-in-place and a `Coverage:` footer. Do NOT re-type it from memory deliver it by file so it cannot drift.
26
- - **Native-session executor:** Read `_implementation-diff-review.md` end-to-end after the stage's last edit, run the sweep over `git diff <stage-base>..HEAD`, fix findings in place, and write its `Coverage:` footer to your audit sidecar before committing.
27
- - **CLI-wrapper executor:** the CLI process cannot read this path outside its sandbox. The lead appends this file's body into the persisted executor prompt at dispatch time, between the preflight body and the self-check body (see `okstra_ctl.initial_prompt_materialization.materialize_initial_prompts()`). The head-less executor honours all three gates from the single prompt.
28
- - **Completion self-check (BLOCKING runs BEFORE you claim the stage done).** The gate body is a single source at `prompts/profiles/_implementation-self-check.md` (sibling of this sidecar): the completion gate (diff-review Coverage footer present, functions ≤50 lines, conventions applied, truthful names & why-comments, real build/test run, cleanup). Do NOT re-type it from memory — deliver it by file so it cannot drift.
29
- - **Native-session executor:** Read `_implementation-self-check.md` end-to-end before appending the `status:"done"` row, then write the confirming evidence per item to your audit sidecar.
30
- - **CLI-wrapper executor:** the CLI process cannot read this path outside its sandbox. The lead appends this file's body into the persisted executor prompt at dispatch time, immediately after the diff-review body (see `okstra_ctl.initial_prompt_materialization.materialize_initial_prompts()`). The head-less executor honours all three gates from the single prompt.
31
- - **Stage discipline transcription (when a preceding stage is `done`):** the lead MUST transcribe the `Stage discipline` rule (from this run's rendered profile — the INCLUDEd `_stage-discipline.md` body) verbatim into every dispatched CLI-wrapper executor prompt so it honors the prior-stage behavior-freeze. Declaration-level — no wrapper sentinel.
32
- - **Non-interactive auto-execution (BLOCKING for `runner=cli-wrapper`).** A CLI-wrapper executor runs head-less — there is no human at the keyboard. Skills loaded during the run (tdd, coding-preflight, and others) contain "get user approval", "state your plan to the user and wait", or "ask before proceeding" gates written for interactive sessions; in this run those gates are **already satisfied** by the upstream `implementation-planning` approval (the plan this stage executes was human-approved). The executor MUST NOT stop to request approval, MUST NOT end its turn after only producing a plan, and MUST carry the stage through end-to-end — RED → GREEN → refactor → per-cycle commit → `### Stage Carry Evidence`. The ONLY skill step to skip is the interactive user-approval prompt itself; every other skill rule (TDD discipline, conventions, real-IO isolation) still binds. The lead MUST transcribe this bullet verbatim into the dispatched CLI-wrapper executor prompt (same reason as the preflight transcription rule above — the CLI process does not share lead context). Stopping early for approval in a head-less run is the observed empty-exit failure (exit 0, no diff): treat it as `contract-violated`.
22
+ - **Three BLOCKING gates bind this run, and their bodies travel with this prompt** inlined under their own headings, or named by path under `## Required prompt resources`. Follow the delivered body verbatim; a gate re-typed from memory is a skipped gate. Each gate's own body owns its rule list, so nothing here restates it:
23
+ - `Coding-conventions preflight` (`prompts/profiles/_coding-conventions-preflight.md`) before the first `Edit` / `Write`. It loads the conventions and binds the TDD loop below; close it by stating in ONE line which conventions apply (e.g. `Applying TS + hexagonal overlay; domain at src/domains/*/domain/`).
24
+ - `Pre-commit diff review sweep` (`prompts/profiles/_implementation-diff-review.md`) after the stage's last `Edit` / `Write`, before the final commit. Sweep `git diff <stage-base>..HEAD` against the conventions the preflight loaded, fix findings in place while they are inside this stage's scope, and write its `Coverage:` footer to your audit sidecar.
25
+ - `Implementation self-check` (`prompts/profiles/_implementation-self-check.md`) before you append the `status:"done"` row. Write the confirming evidence per item to your audit sidecar.
26
+
27
+ <!--
28
+ Gate delivery (lead / maintainer stripped before this body reaches a worker).
29
+ `okstra_ctl.initial_prompt_materialization` appends all three bodies to the
30
+ persisted executor prompt in that order under EAGER_INCLUDE, and lists their
31
+ paths under `## Required prompt resources` under LAZY_PATH_REFERENCE; a CLI
32
+ executor cannot read the profiles directory, so the file reference alone never
33
+ reaches it. Enforcement: the CLI wrapper refuses an Executor dispatch whose
34
+ persisted prompt lacks the heading `Coding-conventions preflight`
35
+ (`<SENTINEL_PREFIX>_PREFLIGHT_MISSING`) or either post-write heading
36
+ (`<SENTINEL_PREFIX>_POSTWRITE_GATE_MISSING`) — see
37
+ `agents/workers/_cli-wrapper-template.md` → Prompt Composition.
38
+ -->
39
+ - **Stage discipline (when a preceding stage is `done`):** its code is behavior-frozen — you may call, extend, or compose with it, never change what it already does. The rule body travels with this prompt the same way the gates do (`prompts/profiles/_stage-discipline.md`); only its `implementation` bullet binds you, the `implementation-planning` one binds the planner. Declaration-level — no wrapper sentinel.
40
+ - **Non-interactive auto-execution (BLOCKING for `runner=cli-wrapper`).** A CLI-wrapper executor runs head-less — there is no human at the keyboard. Skills loaded during the run (tdd, coding-preflight, and others) contain "get user approval", "state your plan to the user and wait", or "ask before proceeding" gates written for interactive sessions; in this run those gates are **already satisfied** by the upstream `implementation-planning` approval (the plan this stage executes was human-approved). The executor MUST NOT stop to request approval, MUST NOT end its turn after only producing a plan, and MUST carry the stage through end-to-end — RED → GREEN → refactor → per-cycle commit → `### Stage Carry Evidence`. The ONLY skill step to skip is the interactive user-approval prompt itself; every other skill rule (TDD discipline, conventions, real-IO isolation) still binds. Stopping early for approval in a head-less run is the observed empty-exit failure (exit 0, no diff): treat it as `contract-violated`.
33
41
  - **Mandatory TDD loop**: BEFORE the first `Edit` or `Write` call, the executor MUST apply a red-green-refactor loop for every code change in this run. This is required; skipping it is a `contract-violated` outcome. This governs HOW each step is executed (failing test first → minimal implementation → refactor); it does not override the approved plan's WHAT/file scope.
34
42
  - Order of operations per plan step: (1) write/extend the test that captures the step's acceptance criterion and confirm it fails for the right reason, (2) implement the minimum change to make it pass, (3) commit the test and its implementation together in a single commit (`feat|fix(<scope>): ...`) — do NOT commit the failing test separately, (4) refactor without changing behaviour and commit separately if any cleanup is made (`refactor(<scope>): ...`). The failing-then-passing transition is preserved as `TDD evidence` in the final report (failing output captured before the merged commit, passing output after), not as two separate commits.
35
43
  - Doc-only / config-only / pure-rename steps that have no observable runtime behaviour are exempt from the failing-test requirement, but the executor MUST cite the exemption per step in the final report (`TDD exemption: <reason>`).
@@ -37,10 +45,10 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
37
45
  - **DB / IO / SQL changes require real execution — mock-only is NOT validation evidence:** when this run's diff touches DB/IO/SQL (ORM / query-builder code — sequelize / typeorm / prisma / knex / raw SQL — `*.repository.*`, model/entity files, `migrations/**`, `*.sql`, or any changed query string), a mocked unit test cannot observe the SQL the query builder actually emits (observed failure class: `_implementation-verifier.md` §"DB / IO / SQL change — real-execution gate"). The executor MUST run the change against a real (or faithful-replica) datastore — the `db-test` validation step (plan `validation` db step, else `project.json.qaCommands.db-test`), targeting a **local / replica** DB — and cite its exact command + exit code in the final report's `Validation evidence`. If no real DB / `db-test` command is reachable, do NOT claim the change verified: label the DB portion `static-analysis only …, unverified (not executed)` in the report, surface it in the routing recommendation, and never downplay the real run as "too heavy". `git push` stays forbidden (universal list); the unverified DB state is carried forward so `final-verification` cannot accept it and `release-handoff` cannot push.
38
46
  - **External-source adapters — structure AND fixture both derive from a captured real sample; a self-authored fixture is NOT reality evidence:** when this run's diff builds or changes an `external-interface` or `transformation-mapping` surface (an HTTP / network client, or a parser / mapper of a third-party payload — HTML / JSON / XML / CSV originating outside this repo), the adapter's structural assumptions (selectors, field paths, expected response shape) AND the static fixture / golden that tests them MUST BOTH derive from a **captured real sample** of that payload — the capture cited in the stage's `external-interface` / `transformation-mapping` design-prep item, or one captured this run and recorded with its `source` + capture time. The captured sample is a static fixture (no live socket), so a parser test against it stays in source like any unit test — the Real-IO isolation rule below governs *live* calls, not the captured bytes. Do NOT hand-invent the shape and then hand-write a fixture that agrees with it: the passing test then only proves the code matches your assumption, never that the assumption matches reality (self-confirming oracle — the observed failure was a parser whose selectors existed in its synthetic fixture and in zero real pages: hundreds of green units over a fiction, and the whole structure built on the wrong shape). When no real sample is reachable (no network this run, or the brief supplied none), do NOT synthesize a stand-in and present its green tests as correctness: mark the adapter's shape `reality-unverified (no captured sample)` in `Validation evidence`, keep any placeholder fixture explicitly labelled an assumption (never validation evidence), and surface an explicit **user-owned** item in the routing recommendation to confirm against real data. Unlike the DB gate above this does NOT itself block acceptance — live external verification stays a user-owned item per `final-verification`'s External QA advisory policy — but a synthetic external fixture presented as reality-verified is exactly the mock-only external evidence the `final-verification` test-correctness pass is meant to reject.
39
47
  - **Real-IO test isolation (BLOCKING).** A test that exercises a **real** datastore, HTTP endpoint, external service, message queue, or filesystem — a live DB connection / DSN, a real `fetch` / `axios` / `http` request, an actual S3 / queue client, anything the project's normal CI test suite cannot run because that backend is absent — MUST be written under the task's qa scripts directory `<task_root>/qa/scripts/` (`<TASK_QA_PATH>/scripts`; the `qa/` root itself holds only data sidecars — the Tier 3 conformance manifest and `result-*.json`). It MUST NOT be written into the project source test tree — `src/**`, `test/**`, `tests/**`, `**/__test__/**`, `**/__tests__/**`, `*.spec.*`, `*.test.*`, or anywhere the project's lint/test globs collect. Two reasons: (a) the project's CI / normal suite has no real DB or network, so a real-IO test placed in source silently breaks the pipeline; (b) it is an okstra verification artifact, and the artifact-home rule confines okstra outputs to `.okstra/`. **The dividing line is the IO, not the intent:** a unit test that stubs/spies only *injected collaborators* (mock — no real socket, no real DB handle) is a TDD red-green artifact and stays in source; the moment a test opens a real connection or makes a real network call it belongs in qa. A stage's real-IO requirement check is a Tier 3 conformance script under `<task_root>/qa/scripts/` (declared via the implementation-planning conformance entry) — never smuggle real IO into a `*.spec.*` in source to make it run "as a unit test". The `db-test` real-execution gate above is satisfied by the conformance/db-test path against the replica, NOT by adding a live-DB `*.spec.*` to the project suite. **Author qa specs with the project's own test framework — never hand-roll `describe`/`it`/`expect`.** When the project ships a test runner as a devDependency (jest / vitest / pytest …), the qa spec uses it, invoked with the project config plus a discovery override pointing at the qa scripts dir (jest: `npx jest --config <project jest config> --roots <task_root>/qa/scripts --runInBand <spec-name>`) — the project config keeps module aliases resolving while the default sweep never collects the file; never widen the project's own test config to include qa paths. For TypeScript qa specs also write `<task_root>/qa/scripts/tsconfig.json` (`extends` the project tsconfig, adds the runner's `types` entry, `"include": ["**/*.ts"]`) so editors resolve path aliases and test globals — it is a qa artifact like the rest (untracked). **These qa artifacts stay untracked — never commit them.** `.okstra/**` is gitignored (the artifact-home rule); conformance scripts and their results are *executed* and recorded in the carry sidecar / verifier result, never written into git history. A committed `.okstra/qa` file is a stage-branch defect that leaks okstra internals into the eventual PR (see the `git add` rules below).
40
- - re-read the approved plan end-to-end and parse the `## 5.5 Stage Map`. Read the **Stage** injected in the launch prompt (`Stage for this implementation run`): the single stage number this run owns. The runtime already selected and reserved this stage (one run = one stage) — do NOT recompute the start stage from `consumers.jsonl`.
48
+ - read the approved plan at this prompt's `**Approved plan:**` anchor end-to-end and parse the `## 5.5 Stage Map`. Read this prompt's `**Stage for this implementation run:**` anchor: the single stage number this run owns. The runtime already selected and reserved this stage (one run = one stage) — do NOT recompute the start stage from `consumers.jsonl`. Both anchors are generated headers; when either is missing, stop and report `contract-violated` rather than inferring the value.
41
49
  - load every `runs/<plan-key>/carry/stage-<i>.json` for `i ∈ depends-on(this stage)` and inject them into the executor's working context as "runtime carry-in". For a `depends-on (none)` stage, no sidecar load — task-brief only.
42
50
  - this stage's `depends-on` are all already `status:done`. Its file list, step order, Stage Validation commands, Stage Exit Contract, and rollback path are the authoritative scope.
43
- - **Clarification answers carried in (read before the first edit; inlined for CLI executors):** when `instruction-set/clarification-response.md` exists, it carries the user's answers to the approved plan's `## 1. Clarification Items` rows (the planning HTML form's `# Attached User Responses`). Treat each answer as an authoritative refinement of the plan's scope for the matching row; an answer that contradicts or expands the approved scope beyond the plan is a re-plan trigger (route to a new `implementation-planning` run), not a silent in-run change. **CLI executor (codex/antigravity):** that file sits outside the CLI sandbox, so the lead MUST transcribe its body into the dispatched executor prompt at dispatch time (same rule as the preflight / stage-discipline transcription above) — a path reference never reaches the CLI process.
51
+ - **Clarification answers carried in (read before the first edit):** when the user answered the approved plan's `## 1. Clarification Items` rows, those answers arrive with this prompt under the heading `# Clarification answers carried in (authoritative)`, or as the `Clarification response` path under `## Inputs`. Treat each answer as an authoritative refinement of the plan's scope for the matching row; an answer that contradicts or expands the approved scope beyond the plan is a re-plan trigger (route to a new `implementation-planning` run), not a silent in-run change.
44
52
  - **Effective design preparation (runtime-resolved after stage selection):**
45
53
 
46
54
  {{DESIGN_PREP_CONTEXT}}
@@ -52,7 +60,6 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
52
60
  - **drift rule** (this section): if a file *named in the plan* has materially drifted, refuse to edit and route back to planning. This protects trust in the approved scope.
53
61
  - **out-of-plan rule** (Allowed actions section below): if a step *requires touching a file NOT in the plan list*, that is permitted with `Out-of-plan edits` justification. This handles honest scope discovery during execution.
54
62
  - confirm the test/build commands referenced in the plan still exist and run from a clean state
55
- - **Pre-commit diff review sweep (BLOCKING before the executor's final commit):** run the sweep whose body is delivered via `_implementation-diff-review.md` (see the gate-delivery bullet above) over the run diff (`git diff <stage-base>..HEAD`), and fix findings in place before handing to verifiers when the issue is inside this stage's scope. That sidecar is the single source for the sweep's file×rule matrix (DRY / self-mock / behavioral-test / hexagonal / truthful-name / plain-English rules) and its `Coverage:` footer — do not maintain a second copy of the rule list here.
56
63
 
57
64
  ## Stage execution contract (this run owns one stage)
58
65
 
@@ -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`).
@@ -23,7 +23,7 @@ Every verifier acts as a QA gate, not just a diff reviewer. Trusting the executo
23
23
 
24
24
  Verifier obtains the QA command set from exactly two declared sources, in order — there is **no fallback to guessing tools from manifest files**.
25
25
 
26
- 1. **Tier 1 — plan validation set (task-specific):** every command listed under the approved plan's `validation` block (pre / mid / post).
26
+ 1. **Tier 1 — plan validation set (task-specific):** every command listed under the approved plan's `validation` block (pre / mid / post). The plan is the file at this prompt's `**Approved plan:**` anchor, scoped to the stage its `**Stage for this implementation run:**` anchor names; both are generated headers, so a missing one is `contract-violated`, never a value to infer.
27
27
  2. **Tier 2 — project baseline (`project.json.qaCommands`):** the project's standing QA baseline declared in `<PROJECT_ROOT>/.okstra/project.json` under the `qaCommands` key. Schema (each category is an array of `{ "label", "cmd", "language"? }` objects):
28
28
  ```json
29
29
  {
@@ -234,6 +234,6 @@ If every verifier present in the resolved roster ends with a non-result terminal
234
234
  - running integration / end-to-end tests that produce non-local side effects (DB writes against a non-local datastore, external API writes, docker compose against a non-isolated environment) unless that exact command is listed in the approved plan's validation set
235
235
  - redirecting tool caches or output to paths outside the worktree — e.g. setting `CARGO_TARGET_DIR`, `PYTEST_CACHE_DIR`, `NODE_OPTIONS=--require=<external>`, or any env var that causes the verifier's command to write outside the worktree's normal build artifact paths
236
236
 
237
- ## Completion self-check (declaration)
237
+ ## Executor completion self-check (not this role's gate)
238
238
 
239
- - **Completion self-check (declaration).** If this verifier edits project code (not just inspects it), the same completion gate at `prompts/profiles/_implementation-self-check.md` binds before claiming done same four items, recorded in the audit sidecar.
239
+ - The executor's `Implementation self-check` gate (`prompts/profiles/_implementation-self-check.md`) belongs to the worker that owns the diff, and its body is deliberately not delivered here: it asks for in-place fixes and break-then-restore mutation checks, every one of which this verifier is forbidden to perform. Do not re-derive its items or claim to have run it. What grades the same defects from this side is the blocking taxonomy above, applied to the diff you re-read yourself. When the executor's `Coverage:` / `Self-check coverage:` lines are among the inputs this prompt enumerates, a missing line or one whose file list does not reconcile with the diff is a blocking finding — the gate was skipped or partially run.
@@ -1,8 +1,9 @@
1
1
  <!--
2
2
  Shared stage-discipline rule. INCLUDEd by implementation-planning.md (binds the
3
- planner) and implementation.md (binds the lead). The implementation executor
4
- sidecar instructs the lead to transcribe this rule into a CLI executor prompt,
5
- since codex/antigravity executors do not share lead context.
3
+ planner) and implementation.md (binds the lead). `okstra_ctl.initial_prompt_-
4
+ materialization` also delivers this body to every implementation executor
5
+ prompt as a required resource, because a codex/antigravity executor shares no
6
+ lead context and cannot read the rendered profile.
6
7
 
7
8
  Do NOT write the literal include directive token in this file's body — the
8
9
  resolver matches it anywhere and would recurse on this file itself.
@@ -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: { attempted: false, status: "implicit", splitPane: <bool> }` in team-state. A concurrent run records `status: "skipped", reason: "concurrent-run"`. Populate `lead.sessionId`; the session transcript lives under `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl`.
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 = _surface_ids(workspace)
476
+ before = open_surface_ids(workspace)
477
477
  _create_surface(workspace, placement, target)
478
- new_ids = _surface_ids(workspace) - before
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 _surface_ids(workspace: str) -> set[str]:
487
- """Every surface UUID in the workspace.
486
+ def open_surface_ids(workspace: str) -> set[str]:
487
+ """Every surface UUID the workspace currently holds.
488
488
 
489
- The new surface is identified by diffing this set rather than by translating
490
- the `OK surface:N` echo, because `list-pane-surfaces` reports only the
491
- focused pane unless given a `--pane`, and the new pane is not focused.
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 row's identity for idempotency is the tuple
4
- (impl_task_key, stage, status)
5
- so the same (started / done) record is never duplicated.
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
- started = {
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 ("started", "done"):
96
- raise ValueError(f"status must be 'started' or 'done', got: {status!r}")
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
- # done 점유 해제 이벤트이기도 하다 — 중복 append(no-op)에서도 풀어야
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
- for row in read_consumers(plan_run_root):
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 _release_stage_reservation(impl_task_key: str, stage: Any) -> None:
129
- """done 기록된 stage 의 worktree-registry 점유(stage-key)를 해제한다.
130
-
131
- release 점유 표시만 푼다 worktree 디렉토리·브랜치는 보존된다.
132
- registry 좌표는 TASK_KEY(`project:group:task`) segment
133
- safe-segment 같다(stage 예약이 그렇게 만들어진다). 형식이 다르면
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
- _safe_fs_segment(parts[0]), _safe_fs_segment(parts[1]),
142
- _safe_fs_segment(parts[2]), stage_number=stage,
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
- set_dispatch_mode as _set_dispatch_mode,
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
- _set_dispatch_mode(plan.team_state_path, _dispatch_mode(plan.jobs))
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
- _set_dispatch_mode(plan.team_state_path, _mode_from_handles(handles))
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 set_dispatch_mode(team_state_path: Path, dispatch_mode: str) -> None:
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:
@@ -49,18 +49,30 @@ class WizardAnswerError(ValueError):
49
49
  """Raised when an answer violates the interaction plan's protocol."""
50
50
 
51
51
 
52
+ def _exact_value_match(prompt: WizardPrompt, candidate: str) -> str | None:
53
+ for option in prompt.options:
54
+ if option.value == candidate:
55
+ return option.value
56
+ return None
57
+
58
+
52
59
  def _normalize_numbered_item(prompt: WizardPrompt, answer: str) -> str:
53
60
  candidate = (answer or "").strip()
54
- exact_values = tuple(
55
- option.value for option in prompt.options if option.value == candidate
56
- )
57
- if exact_values:
58
- return exact_values[0]
61
+ # A bare number is the position in the list the user was shown. Matching
62
+ # option values first breaks every picker whose values are themselves
63
+ # numbers — a stage picker resolves "1" to stage 1 while the user was
64
+ # pointing at line 1, which is a different stage (or "every stage").
59
65
  if candidate.isdecimal():
60
66
  number = int(candidate)
61
67
  if 1 <= number <= len(prompt.options):
62
68
  return prompt.options[number - 1].value
69
+ out_of_list = _exact_value_match(prompt, candidate)
70
+ if out_of_list is not None:
71
+ return out_of_list
63
72
  raise WizardAnswerError(f"numbered-text answer is out of range: {candidate}")
73
+ exact_value = _exact_value_match(prompt, candidate)
74
+ if exact_value is not None:
75
+ return exact_value
64
76
  label_values = tuple(
65
77
  option.value for option in prompt.options if option.label == candidate
66
78
  )
@@ -17,6 +17,7 @@ from . import stage_targets
17
17
  from .design_prep import DesignPrepDecision, DesignPrepError, resolve_design_prep
18
18
  from .final_report_paths import final_report_data_path
19
19
  from .stage_reconcile import auto_reconcile_best_effort
20
+ from .worker_prompt_policy import IMPLEMENTATION_STAGE_HEADER
20
21
 
21
22
 
22
23
  class ImplementationStageError(Exception):
@@ -250,7 +251,7 @@ def publish_stage_run_claim(
250
251
  ctx["EFFECTIVE_STAGES"] = csv
251
252
  ctx["CONCURRENT_RUN_STAGES"] = ",".join(str(s) for s in claim.concurrent_stages)
252
253
  ctx["STAGE_BATCH_DIRECTIVE"] = (
253
- f"- **Stage for this implementation run:** `{csv}`. "
254
+ f"- {IMPLEMENTATION_STAGE_HEADER} `{csv}`. "
254
255
  "Execute exactly this Stage Map stage — this is the authoritative scope. "
255
256
  "Do NOT recompute from `consumers.jsonl`; the runtime already selected "
256
257
  "and reserved this stage."