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.
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
- package/runtime/prompts/profiles/_implementation-deliverable.md +3 -2
- package/runtime/prompts/profiles/_implementation-diff-review.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +22 -15
- package/runtime/prompts/profiles/_implementation-verifier.md +4 -4
- package/runtime/prompts/profiles/_stage-discipline.md +4 -3
- 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/domain/wizard/interaction.py +17 -5
- package/runtime/python/okstra_ctl/implementation_stage.py +2 -1
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +104 -12
- package/runtime/python/okstra_ctl/path_hints.py +10 -2
- package/runtime/python/okstra_ctl/render.py +5 -0
- package/runtime/python/okstra_ctl/stage_map.py +10 -4
- package/runtime/python/okstra_ctl/team.py +30 -6
- package/runtime/python/okstra_ctl/wizard.py +19 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +24 -4
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -1
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +18 -1
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +13 -2
- 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/templates/implementation-worker-preamble.md +10 -3
- package/runtime/templates/reports/final-report.template.md +3 -0
- package/runtime/templates/worker-prompt-preamble.md +4 -3
- package/runtime/validators/validate-implementation-plan-stages.py +19 -2
- package/runtime/validators/validate-run.py +7 -0
- package/src/commands/execute/wizard.mjs +20 -8
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -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.
|
|
@@ -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
|
-
|
|
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 —
|
|
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
|
-
- **
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
-
|
|
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
|
|
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
|
-
##
|
|
237
|
+
## Executor completion self-check (not this role's gate)
|
|
238
238
|
|
|
239
|
-
-
|
|
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).
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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:
|
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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"-
|
|
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."
|