okstra 0.220.1 → 0.221.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-registry.mjs +6 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/docs/architecture.md +2 -0
- package/docs/cli.md +7 -3
- package/docs/project-structure-overview.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/launch.template.md +1 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +9 -5
- package/runtime/prompts/lead/team-contract.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +3 -2
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +3 -3
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/worker-session.md +4 -2
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +3 -3
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +27 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +24 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +24 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +25 -0
- package/runtime/python/okstra_ctl/adapters/providers/zai/adapter.py +20 -1
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +3 -0
- package/runtime/python/okstra_ctl/analysis_packet.py +14 -0
- package/runtime/python/okstra_ctl/correction_cli.py +44 -5
- package/runtime/python/okstra_ctl/correction_round.py +40 -11
- package/runtime/python/okstra_ctl/domain/mcp.py +43 -0
- package/runtime/python/okstra_ctl/domain/provider.py +2 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +89 -8
- package/runtime/python/okstra_ctl/file_size_cap.py +142 -0
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +3 -6
- package/runtime/python/okstra_ctl/manager_cli.py +11 -1
- package/runtime/python/okstra_ctl/manager_sync.py +38 -2
- package/runtime/python/okstra_ctl/mcp_cli.py +179 -0
- package/runtime/python/okstra_ctl/mcp_servers.py +421 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +4 -3
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-executor.md +1 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-self-check.md +1 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-verifier.md +3 -2
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +19 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +14 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +2 -1
- package/runtime/python/okstra_ctl/phases/technical_verification/entry.py +37 -0
- package/runtime/python/okstra_ctl/phases/technical_verification/validation.py +3 -3
- package/runtime/python/okstra_ctl/render.py +7 -48
- package/runtime/python/okstra_ctl/report_language.py +4 -5
- package/runtime/python/okstra_ctl/run.py +29 -0
- package/runtime/python/okstra_ctl/stage_done.py +37 -3
- package/runtime/skills/_fragments/wizard-lead-takeover.md +1 -1
- package/runtime/skills/_fragments/wizard-relay-protocol.md +2 -2
- package/runtime/skills/okstra-discussion/SKILL.md +3 -3
- package/runtime/skills/okstra-manager/SKILL.md +1 -0
- package/runtime/skills/okstra-run/SKILL.md +3 -3
- package/runtime/skills/okstra-setup/SKILL.md +2 -0
- package/runtime/skills/okstra-setup/references/project-config.md +34 -0
- package/runtime/templates/report-writer-prompt-preamble.md +1 -1
- package/runtime/templates/reports/task-brief.template.md +3 -3
- package/runtime/validators/validate-run.py +13 -0
package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md
CHANGED
|
@@ -77,7 +77,7 @@ When the synthesised verdict is `FAIL` and the lead minor fix does not apply, th
|
|
|
77
77
|
**Entry.** Every blocking finding of the round meets all of these, or the run ends `FAIL` as before:
|
|
78
78
|
|
|
79
79
|
1. A verifier cited where it is (`path:line`) and what the correct result is.
|
|
80
|
-
2. The fix stays inside this stage's `plannedPaths` or files the executor already changed.
|
|
80
|
+
2. The fix stays inside this stage's `plannedPaths` or files the executor already changed. A `FILE-SIZE:` finding also allows the new files the split creates beside the oversized one, so it always meets this rule.
|
|
81
81
|
3. It needs no new plan step, dependency, schema, migration, or config of record — a finding that does is a plan change: end the run `FAIL` and route by `prompts/lead/phase-routing.md`.
|
|
82
82
|
|
|
83
83
|
Changing a test's expected value is allowed here when the finding shows the oracle is wrong, because the verifiers re-verify the fix.
|
|
@@ -85,12 +85,13 @@ Changing a test's expected value is allowed here when the finding shows the orac
|
|
|
85
85
|
**Procedure for round `<N>`.**
|
|
86
86
|
|
|
87
87
|
1. Record the stage HEAD as `<base>` (`git rev-parse HEAD` in the stage worktree).
|
|
88
|
-
2. Write the executor instruction with `okstra correction prompt --run-manifest <path> --round <N> --role executor --base <base> --finding <F-id> [--finding <F-id> ...]` into a file under the run's `state/`, then materialize it with `okstra agent-prompt materialize` — `--audience implementation-executor`, `--assignment-ref initial/<executor worker id>`, `--dispatch-kind correction-r<N>`, a new `--invocation-id` ending `-correction-r<N>-<executor worker id>`, and a result path `<executor worker id>-worker-correction-r<N>-implementation-<seq>.md`. Build a jobs file with `okstra agent-prompt jobs --dispatch-kind correction-r<N>`, dispatch it alone with `okstra team dispatch --jobs-file <path> --dispatch-kind correction-r<N>`, and settle it with `okstra team await`.
|
|
88
|
+
2. Write the executor instruction with `okstra correction prompt --run-manifest <path> --round <N> --role executor --base <base> --stage-base <stage base> --finding <F-id> [--finding <F-id> ...]` into a file under the run's `state/`, then materialize it with `okstra agent-prompt materialize` — `--audience implementation-executor`, `--assignment-ref initial/<executor worker id>`, `--dispatch-kind correction-r<N>`, a new `--invocation-id` ending `-correction-r<N>-<executor worker id>`, and a result path `<executor worker id>-worker-correction-r<N>-implementation-<seq>.md`. Build a jobs file with `okstra agent-prompt jobs --dispatch-kind correction-r<N>`, dispatch it alone with `okstra team dispatch --jobs-file <path> --dispatch-kind correction-r<N>`, and settle it with `okstra team await`.
|
|
89
|
+
`--stage-base` (the carry's `stageCommitRange.base`) adds a `FILE-SIZE:<path>` finding for each file the stage diff leaves over the 400-line limit, computed by `okstra correction file-size`; a `FILE-SIZE-ADVISORY:` line (the 300-line target) never opens or joins a round; a round may hold only those findings, and `findingIds` records them by that id. Leave out a convergence finding id that restates a `FILE-SIZE:` line, or the executor reads it twice.
|
|
89
90
|
3. Do the same for every verifier with `--role verifier`, `--audience implementation-verifier`, its own `initial/<verifier worker id>` assignment, and result `<verifier worker id>-worker-correction-r<N>-implementation-<seq>.md`; dispatch the verifiers in one batch without the executor.
|
|
90
91
|
4. Converge the re-verification results the way the initial round was converged, restarting Phase 5.5 with `okstra convergence seed ... --restart-from-round0`.
|
|
91
92
|
5. No re-verification verdict is `FAIL`: the round `result` is `pass` and the run continues to persistence with the post-fix head. Otherwise `result: fail`; start round `<N+1>` if the cap allows and the entry rules still hold, or end the run `FAIL`.
|
|
92
93
|
|
|
93
|
-
**Enforced:** `scripts/okstra_ctl/dispatch_core.py` `_validate_correction_rounds` (via `correction_round.validate_correction_dispatch`) refuses a round past the cap, a verifier dispatch before that round's executor returned an `ok` result, a second executor fix in the same round, and round `<N+1>` before round `<N>` was re-verified. `validators/validate-run.py` `_validate_correction_rounds` (blocking) fails a report whose `correctionRounds` differ from the correction rounds the run dispatched, whose rounds are not 1, 2 in order, whose `result` contradicts its verdicts, or whose `verifierResults` are not the last round's verdicts. The entry rules are instructions only.
|
|
94
|
+
**Enforced:** `scripts/okstra_ctl/dispatch_core.py` `_validate_correction_rounds` (via `correction_round.validate_correction_dispatch`) refuses a round past the cap, a verifier dispatch before that round's executor returned an `ok` result, a second executor fix in the same round, and round `<N+1>` before round `<N>` was re-verified. `validators/validate-run.py` `_validate_correction_rounds` (blocking) fails a report whose `correctionRounds` differ from the correction rounds the run dispatched, whose rounds are not 1, 2 in order, whose `result` contradicts its verdicts, or whose `verifierResults` are not the last round's verdicts. The entry rules are instructions only. `okstra stage-done` (`scripts/okstra_ctl/stage_done.py` `_check_file_size`) refuses to settle a stage whose carry range breaks the 400-line file-size limit and names the `--stage-base` to pass, so a size violation no verifier reported still reaches a correction round, or ends the run `FAIL` when no round is left.
|
|
94
95
|
|
|
95
96
|
## Self-review pass before finalising the report (the Okstra lead runs this; do not delegate it)
|
|
96
97
|
|
package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-executor.md
CHANGED
|
@@ -92,6 +92,7 @@ template's check; that template is gone.
|
|
|
92
92
|
The file MUST NOT exist before the run starts (overwrite is refused — see `--force-stage` non-goal). **Enforced:** `validators/validate-run.py` `_validate_stage_carry_sidecar_exists` fails a run that declares `stageSidecarEvidence` without the file on disk. Transcribing the JSON into the report is not the same as writing it: `consumers` treats the carry file as the source of truth for marking the stage `done`, so a missing file leaves the stage permanently incomplete and blocks every dependent stage with a `PrepareError` — while this run reports success.
|
|
93
93
|
- **Verifier gates are not yours to run (BLOCKING).** The self-mock detector (`validators/detect_self_mock.py`) belongs to the implementation verifier and is never delegated to you (`_implementation-verifier.md` §"Self-mock detection"). The coding-conventions preflight names it as the enforcement behind the no-self-mocking principle — that names who will check your diff, not a command for you to run. You MUST NOT invoke it and MUST NOT write `<task_root>/qa/self-mock-*.json`; running it early does not pre-satisfy the gate, because the verifier runs it again under its own duty. **Enforced:** that sidecar is not among the paths your attempt's `writePolicy.artifactPolicy.allowedPaths` carries, so the write audit closes the attempt as `error` with `artifact-root change exceeds batch policy union` (`scripts/okstra_ctl/execution_mutation_audit.py`) — a stage whose every gate passed still lands as a failed run. The same holds for the conformance results `<task_root>/qa/result-*.json`: you may run a conformance script to check your work — a baseline capture or `--compare` target goes under `<task_root>/qa/baseline/` and anything else it writes (build logs, measurements) under `<task_root>/qa/output/`, the only qa directories besides `qa/scripts/` your attempt may write — but the verifier records every result, including an earlier stage's result that your rewrite of its script made stale. When the approved plan tells you to write one, leave it and name it in your result as a plan step the verifier owns.
|
|
94
94
|
- **An external Tier 3 non-PASS does NOT withhold the carry evidence.** A Tier 3 entry whose `requires` include `http`, `external`, or `db` is advisory. Its FAIL, MISSING, no result, startup failure, or credential / network / service absence gets recorded honestly — exact command, exit code, output tail, marked `ADVISORY` in `Validation evidence` — and you emit the carry evidence anyway. Only Tier 1 and Tier 2 failures withhold it. Withholding on an external result is what actually blocks the stage: the carry file is the only thing that can mark a stage `done`, the verifier re-runs that same command from the host (where a call your sandbox could not complete often passes), and a stage the verifier then PASSes can never be closed because its evidence was never written.
|
|
95
|
+
- **File size: aim for 300 lines, never exceed 400 (BLOCKING at 400).** Each threshold is broken the same three ways — a new file over it, an existing file taken past it, a file already over it that grows by any net line — counted in physical lines. Breaking 400 is a blocking finding; breaking only 300 is an advisory recommendation the verifiers record. Put a new responsibility — a group of functions, a class, a `describe` block — in a new module or test file beside the existing one instead of appending, and when a file would pass 400, split it by responsibility; never compress code or drop comments to meet the number. Lockfiles, data (JSON / YAML), Markdown, fixtures, snapshots, generated and vendored files are exempt, as are languages okstra has no coding preflight for. Check with `okstra correction file-size --base <stage-base>` before you emit the carry evidence; it writes nothing, so you may run it. **Enforced:** every verifier runs the same command and reports each `FILE-SIZE:` line (400) as a blocking finding and each `FILE-SIZE-ADVISORY:` line (300) as a recommendation, and `okstra stage-done` (`scripts/okstra_ctl/stage_done.py` `_check_file_size`) refuses to settle a stage whose carry range still breaks 400, so the stage goes to a correction round.
|
|
95
96
|
- **Reverse link (BLOCKING).** The runtime already appended a `status:"started"` row for this stage before the run began. The terminal row belongs to the lead's post-stage persistence and is verdict-gated — `status:"done"` with `carry_path` on a non-`FAIL` verdict, `status:"failed"` on `FAIL` (`_implementation-deliverable.md` §"Lead post-stage persistence").
|
|
96
97
|
- **No PR / push in this phase.** This run produces local commits, carry sidecar evidence, verifier results, and the implementation final report only. Push and PR creation belong exclusively to the later `release-handoff` phase after `final-verification` returns `accepted`.
|
|
97
98
|
|
package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-self-check.md
CHANGED
|
@@ -17,6 +17,7 @@ fails, fix it or surface the violation — do not claim done on a failing item.
|
|
|
17
17
|
|
|
18
18
|
- diff-review done: the `Pre-commit diff review sweep` ran over the full diff and its `Coverage:` footer is in your audit sidecar — every changed file named, with each applied rule set carrying its outcome (`clean` or `fixed <n>`). No footer, or entries with no outcome → the sweep is unfinished; do not claim done.
|
|
19
19
|
- functions: every new/edited function ≤50 effective lines, single purpose — cite the longest function's actual line count, not "all under 50"
|
|
20
|
+
- file size: no source or test file this diff adds or grows ends over 400 physical lines, and no file already over 400 grew — paste the output of `okstra correction file-size --base <stage-base>`; it must end `QA-RESULT: PASS`. Aim for 300: for each `FILE-SIZE-ADVISORY:` line it prints, split the file or say why not. A responsibility that would push a file over goes into its own new module or test file
|
|
20
21
|
- conventions: applied the routed pack + project patterns (name which ones)
|
|
21
22
|
- names & comments: names say what, comments say why; no obvious-restatement comments; no truthful-name violations
|
|
22
23
|
- verification: ran the actual build/test — paste the exact command line and its real exit code / output tail, never a paraphrased "tests pass"
|
package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-verifier.md
CHANGED
|
@@ -214,13 +214,14 @@ Re-running commands proves the diff *builds and passes*; it does NOT prove the d
|
|
|
214
214
|
- **Non-separating test data:** two scenarios whose setup values and assertions are identical, so a wrong implementation passes both; or new test tooling added in this diff (mock, state setter, repository branch) that no test calls.
|
|
215
215
|
- **Effect claimed under its own mock:** a test presented as covering an effect whose producing path is replaced by a mock inside that same test. The mock's presence in the harness is not evidence the branch behind it works.
|
|
216
216
|
- **Single-caller abstraction (KISS):** an abstraction layer this diff introduces — a helper module, a strategy / factory / builder, an indirection or wrapper layer, an interface or abstract base — that has exactly one caller after the change, where inlining it at that call site would be simpler. The rule is `overview.md` core principle 2: name the second caller now, or inline. Four exits are legitimate and each must **cite its evidence**: the approved plan declares it (a `testSeams[]` boundary or a variation-point extraction — cite the plan step); the project declares `architecture.style = hexagonal` and this is a port at the domain boundary (a port with one adapter is the normal shape, not a violation); it breaks a real import cycle (name both modules); or its single caller is the public API of a published package. Absent one of those, cite `path:line` and recommend the inlined shape. Do NOT raise this on a function extracted purely to bring a body under the 50-line cap — that is principle 5 doing its job — nor on a pre-existing abstraction this diff merely edits.
|
|
217
|
+
- **File-size limit (every verifier, not only the stage QA owner):** run `okstra correction file-size --base <base>` from the worktree cwd and log it in the `Read-only command log`. Each `FILE-SIZE:<path>:401 <before>-><after>` line it prints is a blocking finding cited exactly as printed — a new file over 400 lines, a file the diff took past 400, or a file already over 400 the diff grew. Recommended fix: move whole responsibilities into new modules or test files beside it. The command writes nothing and its result is the same for every verifier, so the finding converges and the lead sends it to a correction round. Each `FILE-SIZE-ADVISORY:<path>:301` line (the 300-line target, same three conditions) goes under the advisory findings below, not here. Do not judge size yourself: a file it does not print is not a finding (lockfiles, data, Markdown, fixtures, snapshots, generated files and languages without a coding preflight are exempt by `scripts/okstra_ctl/file_size_cap.py` `capped_path`).
|
|
217
218
|
- **Caller-less identifier (YAGNI / orphan):** an identifier this diff leaves with zero callers — either newly added and never called (a speculative parameter, an optional config object, a "future-proof" hook, an exported helper whose only caller is hypothetical), or pre-existing code orphaned because this diff removed its last caller. Grep the identifier across the whole repository rather than the diff alone; a hit inside its own declaration, its own test, or a commented-out line is not a caller. Two exits are legitimate and must be **stated with their evidence**, never assumed: an identifier the approved plan reserves for a named later stage (cite the plan step), and a contract point something other than project code calls (a framework entrypoint, an implemented interface method, a migration hook, the public API of a published package — cite the caller or the registration). Anything else is work no requirement asked for: recommend deleting it, or folding an added parameter back into its single call site. A caller-less identifier is the one YAGNI violation that survives a green pipeline unchanged, which is why it grades here rather than as a recommendation.
|
|
218
|
-
- **Advisory findings (recorded as recommendations; verdict MAY still PASS):** function >50 effective lines, a single body mixing read+write stages, weak readability, a missing-but-non-critical outcome assertion, weak-but-not-misleading names, priority-between-inputs policy inlined in a service condition instead of a named domain function, an error message asserting a cause the code never observed, a memory / concurrency / batching change with no test pinning the bound it claims, or (hexagonal overlay only) a service dependency this diff adds or modifies that injects a concrete adapter instead of a port — record it with the port sketch; advisory only while the project has not declared `architecture.style = hexagonal`, since that declaration — and only that one — promotes exactly this item to blocking per the declared-style bullet above, while a declared `layered` binds dependency direction instead and leaves this item advisory; an existing convention of concrete injections does not convert this one to `clean`, it is the debt the rule pays down. These land in the verifier result as `should-fix` / `nit` recommendations, not as a `FAIL`.
|
|
219
|
+
- **Advisory findings (recorded as recommendations; verdict MAY still PASS):** each `FILE-SIZE-ADVISORY:` line from the file-size command (a file over the 300-line target but within 400), function >50 effective lines, a single body mixing read+write stages, weak readability, a missing-but-non-critical outcome assertion, weak-but-not-misleading names, priority-between-inputs policy inlined in a service condition instead of a named domain function, an error message asserting a cause the code never observed, a memory / concurrency / batching change with no test pinning the bound it claims, or (hexagonal overlay only) a service dependency this diff adds or modifies that injects a concrete adapter instead of a port — record it with the port sketch; advisory only while the project has not declared `architecture.style = hexagonal`, since that declaration — and only that one — promotes exactly this item to blocking per the declared-style bullet above, while a declared `layered` binds dependency direction instead and leaves this item advisory; an existing convention of concrete injections does not convert this one to `clean`, it is the debt the rule pays down. These land in the verifier result as `should-fix` / `nit` recommendations, not as a `FAIL`.
|
|
219
220
|
- **Output.** Every finding — blocking or advisory — is a structured item in the verifier's worker result (`path:line`, rule, severity, suggested fix) so it carries into Phase 5.5 convergence and the final report. A blocking hit sets the verifier verdict to `FAIL` with the rule cited, using the same verdict machinery as the Discrepancy rule above. The Okstra lead MUST NOT silently downgrade a cited blocking finding to advisory during synthesis; an override requires a concrete cited reason, exactly as for the Discrepancy rule.
|
|
220
221
|
|
|
221
222
|
### Fix-run incremental scope (applies when the profile carries a "Fix-Run Carry" block)
|
|
222
223
|
|
|
223
|
-
A correction-round re-verification (`--dispatch-kind correction-r<N>`, a "Correction Round" block in the task instructions) takes the same scope: the block's findings and the correction diff it names, with every validation command re-run.
|
|
224
|
+
A correction-round re-verification (`--dispatch-kind correction-r<N>`, a "Correction Round" block in the task instructions) takes the same scope: the block's findings and the correction diff it names, with every validation command re-run. The file-size command is the exception: run it against the stage base, never the round's base, because a file the round did not touch keeps the violation the initial diff made.
|
|
224
225
|
|
|
225
226
|
A fix run re-verifies a stage whose previous run already passed a full static
|
|
226
227
|
sweep and failed only on cited blocking findings. Re-sweeping the whole stage
|
|
@@ -39,4 +39,4 @@
|
|
|
39
39
|
|
|
40
40
|
Record candidate validity, unresolved user decisions, and explicitly classified technical facts. The lead chooses the next phase under `## implementation-option-selection` in `prompts/lead/phase-routing.md`.
|
|
41
41
|
|
|
42
|
-
When `--clarification-response` carries a technical-verification report, read its source comparison, experiment plans, results and logs. Check that the tested environment matches current code, then author fresh feasibility votes. Do not directly convert a supported result into a feasible candidate, remove unrelated uncertainties or treat a failed or unrun probe as compatibility proof. Preserve the user's pilot scope and any deferred production rollout.
|
|
42
|
+
When `--clarification-response` carries a technical-verification report, read its source comparison, experiment plans, results and logs. The analysis packet's `## Technical Verification Evidence` section names the result record and the experiment directory (rendered by `okstra_ctl.phases.technical_verification.entry.technical_evidence_packet_block`). Check that the tested environment matches current code, then author fresh feasibility votes. Do not directly convert a supported result into a feasible candidate, remove unrelated uncertainties or treat a failed or unrun probe as compatibility proof. Preserve the user's pilot scope and any deferred production rollout.
|
|
@@ -733,6 +733,23 @@ _DIR_SNAPSHOT_OWNED_NOTE = (
|
|
|
733
733
|
"planner-fixable DISAGREE that asks to edit these four fields.\n"
|
|
734
734
|
)
|
|
735
735
|
|
|
736
|
+
# 구현 executor 는 소스 트리의 실 IO 테스트를 첫 편집 전에 거부한다. 계획이 그런
|
|
737
|
+
# step 을 승인받으면 stage 가 변경 0 으로 멈춘다(2026-10-18 dev-11374 실측).
|
|
738
|
+
_REAL_IO_STEP_NOTE = (
|
|
739
|
+
"\nReal-IO placement: this step must not create, edit, or run a test that "
|
|
740
|
+
"opens a real database connection, makes a real network call, or talks to "
|
|
741
|
+
"a real external service inside the project source test tree (`src/**`, "
|
|
742
|
+
"`test/**`, `tests/**`, `**/__tests__/**`, `*.spec.*`, `*.test.*`, "
|
|
743
|
+
"`*.e2e-spec.*`), and its PASS must not depend on a reachable database or "
|
|
744
|
+
"service. The implementation executor refuses such a step before any edit. "
|
|
745
|
+
"Real-backend checks belong only in the stage's `Conformance tests:` "
|
|
746
|
+
"declaration, which the implementation run writes under "
|
|
747
|
+
"`<task_root>/qa/scripts/`, never commits, and treats as advisory. If this "
|
|
748
|
+
"step breaks the rule, vote DISAGREE(b) with Fixability planner-fixable and "
|
|
749
|
+
"name the real-IO test path or command. A test whose collaborator is "
|
|
750
|
+
"stubbed is not real IO.\n"
|
|
751
|
+
)
|
|
752
|
+
|
|
736
753
|
|
|
737
754
|
def _step_coordinate(payload: object, field: str) -> int | None:
|
|
738
755
|
if not isinstance(payload, Mapping):
|
|
@@ -1022,6 +1039,8 @@ def _prompt(args: argparse.Namespace) -> str:
|
|
|
1022
1039
|
rows.append(_render_prior_steps(item, all_items))
|
|
1023
1040
|
if item_id.startswith("P-Prep-"):
|
|
1024
1041
|
rows.append(_PREP_OWNERSHIP_NOTE)
|
|
1042
|
+
if item_id.startswith("P-Step-"):
|
|
1043
|
+
rows.append(_REAL_IO_STEP_NOTE)
|
|
1025
1044
|
if item_id == "P-Dir-1":
|
|
1026
1045
|
rows.append(_DIR_SNAPSHOT_OWNED_NOTE)
|
|
1027
1046
|
split = _render_analyser_split(
|
|
@@ -316,6 +316,20 @@ from both numerator and denominator, and never converted to `DISAGREE`. Voting `
|
|
|
316
316
|
dependency-missing command instead manufactures a defect the planner cannot fix by editing the plan,
|
|
317
317
|
which is what sends the item into a self-fix round that can only paper over it.
|
|
318
318
|
|
|
319
|
+
## Real-IO test in a committed step
|
|
320
|
+
|
|
321
|
+
A `P-Step-*` item that creates, edits, or runs a test opening a real database connection, a real
|
|
322
|
+
network call, or a real external service inside the project source test tree (`src/**`, `test/**`,
|
|
323
|
+
`tests/**`, `**/__tests__/**`, `*.spec.*`, `*.test.*`, `*.e2e-spec.*`), or whose `PASS` depends on a
|
|
324
|
+
reachable database or service, is `DISAGREE(b)` with `planner-fixable`. The implementation executor
|
|
325
|
+
refuses that step before its first edit (`_implementation-executor.md` "Real-IO test isolation"), so
|
|
326
|
+
the stage would end with zero changes. The planner's fix is a stubbed-collaborator unit test in the
|
|
327
|
+
step plus the stage's `Conformance tests:` declaration for the real-backend check. A test whose
|
|
328
|
+
collaborator is stubbed is not real IO. **Enforced:** the rule reaches each verifier on every
|
|
329
|
+
`P-Step-*` item through `okstra plan-items prompt`
|
|
330
|
+
(`okstra_ctl.phases.implementation_planning.authoring._REAL_IO_STEP_NOTE`); the vote itself is
|
|
331
|
+
verifier judgement.
|
|
332
|
+
|
|
319
333
|
## Mode constraint
|
|
320
334
|
|
|
321
335
|
Plan-body verification only supports **lightweight mode** (defined in `prompts/lead/convergence.md` §"Verification Mode"). `full-reanalysis` is not meaningful here because the "original source materials" for a plan item are the worker's own analysis plus the lead-mediated synthesis — there is no independent ground truth to re-read. The manifest's top-level `verificationMode` is ignored for this round; lightweight is always used.
|
|
@@ -147,7 +147,8 @@ Plan for the actual worktree layout before approval. Shared documentation direct
|
|
|
147
147
|
**Pick the cases by distinct outcome, not by line coverage.** When the stage writes or reconciles state, its meaningfully different outcomes are usually more than three — normal success, target already in the desired state (resume), existing data reused rather than created, a conflicting concurrent state, target absent, and mid-way failure with rollback. Enumerate the ones this stage actually implements and route them across the three lines (the `boundary` line is where resume / already-done / reuse belongs; `failure` carries conflict, absence, and rollback), naming each in the cell rather than collapsing them into "edge input". An implemented outcome with no declared case is a coverage gap the executor will not backfill.
|
|
148
148
|
- **Per-stage subsections** (`## 5.5.<i> Stage <i>: <title>` for each `i`), each containing the four required subsections:
|
|
149
149
|
- `### Carry-In` — for `depends-on (none)`: task-brief only. Otherwise: each depended-on stage's static exit contract + runtime sidecar path `runs/<impl-key>/carry/stage-<i>.json` placeholder.
|
|
150
|
-
- `### Stepwise Execution Order` — bite-sized table with `step | action | files | command | outcome | expected`. `outcome` is one word — `PASS` or `FAIL` — and `expected` is the sentence saying what that looks like here; a verdict written inside the sentence is not read as one. The `files` cell lists each touched path in full and `<PROJECT_ROOT>`-relative — never ellipsis-abbreviated (`…` / `...`), which does not resolve and is rejected by plan-body verification as a kind-b path mismatch. **The narrative row additionally carries `plannedPaths`: the paths this step may write as an array, one repository-relative path per entry, with no globs, exclusions, counts or commentary. Read-only checks use `plannedPaths: []`; their working directory belongs in the command, not the write ledger. Never declare the repository or worktree root. Planning assembly and correction preflight enforce this through `write_policy.planned_path_declaration_errors`.** `files` is the sentence a reader sees; `plannedPaths` is the ledger report assembly preserves and the implementer write policy enforces. When a step legitimately covers a set too large to enumerate, split it or name the directory the set lives under. **Effective row count ≤ 8** (excluding header / divider / blank). Each step is one cohesive, self-contained change. **TDD ordering is MUST, not a preference:** the **first** effective step's `action` cell MUST start with the literal `RED:` and describe the failing test(s) that capture this stage's `Acceptance` **and the three declared `Test case (success|boundary|failure)` lines** (`outcome` = `FAIL`); at least one later `action` cell MUST start with the literal `GREEN:` and describe the minimal implementation that makes it pass (`outcome` = `PASS`); an optional refactor step starts with `REFACTOR:`. **Exemption:** doc-only / config-only / pure-rename stages with no observable runtime behaviour may omit RED/GREEN by declaring one line `TDD exemption: <reason>` in the stage section. A stage that is truthfully none of those three declares `TDD exemption: user-bypass — <the user's words>`, which holds only while the user has granted it for that stage with `okstra prepare --tdd-bypass "<stage>:<reason>"` — never file the nearest of the three instead. Validator S10c enforces RED-first + GREEN; the `outcome` cell agreeing with its prefix is a schema conditional. S10e rejects an unsupported exemption reason and a `user-bypass` with no user grant (`validators/validate-implementation-plan-stages.py`).
|
|
150
|
+
- `### Stepwise Execution Order` — bite-sized table with `step | action | files | command | outcome | expected`. `outcome` is one word — `PASS` or `FAIL` — and `expected` is the sentence saying what that looks like here; a verdict written inside the sentence is not read as one. The `files` cell lists each touched path in full and `<PROJECT_ROOT>`-relative — never ellipsis-abbreviated (`…` / `...`), which does not resolve and is rejected by plan-body verification as a kind-b path mismatch. **The narrative row additionally carries `plannedPaths`: the paths this step may write as an array, one repository-relative path per entry, with no globs, exclusions, counts or commentary. Read-only checks use `plannedPaths: []`; their working directory belongs in the command, not the write ledger. Never declare the repository or worktree root. Planning assembly and correction preflight enforce this through `write_policy.planned_path_declaration_errors`.** **File size:** the implementation run aims for 300 physical lines per source and test file and enforces 400 — a new file over 400, an existing file taken past 400, and a file already over 400 that grows are blocking findings the executor must split in a correction round; the same conditions at 300 are advisory. When a step would add code or tests to a file near or over 300 lines (`wc -l`), plan a new module or test file for that responsibility in `plannedPaths` instead of appending to the existing one. **Enforced at implementation, not here:** `okstra correction file-size` (`scripts/okstra_ctl/file_size_cap.py`), re-run by `okstra stage-done`. `files` is the sentence a reader sees; `plannedPaths` is the ledger report assembly preserves and the implementer write policy enforces. When a step legitimately covers a set too large to enumerate, split it or name the directory the set lives under. **Effective row count ≤ 8** (excluding header / divider / blank). Each step is one cohesive, self-contained change. **TDD ordering is MUST, not a preference:** the **first** effective step's `action` cell MUST start with the literal `RED:` and describe the failing test(s) that capture this stage's `Acceptance` **and the three declared `Test case (success|boundary|failure)` lines** (`outcome` = `FAIL`); at least one later `action` cell MUST start with the literal `GREEN:` and describe the minimal implementation that makes it pass (`outcome` = `PASS`); an optional refactor step starts with `REFACTOR:`. **Exemption:** doc-only / config-only / pure-rename stages with no observable runtime behaviour may omit RED/GREEN by declaring one line `TDD exemption: <reason>` in the stage section. A stage that is truthfully none of those three declares `TDD exemption: user-bypass — <the user's words>`, which holds only while the user has granted it for that stage with `okstra prepare --tdd-bypass "<stage>:<reason>"` — never file the nearest of the three instead. Validator S10c enforces RED-first + GREEN; the `outcome` cell agreeing with its prefix is a schema conditional. S10e rejects an unsupported exemption reason and a `user-bypass` with no user grant (`validators/validate-implementation-plan-stages.py`).
|
|
151
|
+
- **Real-IO tests stay out of the plan's committed steps (BLOCKING).** A step never creates, edits, or runs a test that opens a real database connection, makes a real network call, or talks to a real external service inside the project source test tree (`src/**`, `test/**`, `tests/**`, `**/__tests__/**`, `*.spec.*`, `*.test.*`, `*.e2e-spec.*`), and no step's `PASS` depends on a reachable database or service. The implementation executor refuses such a step before its first edit (`_implementation-executor.md` "Real-IO test isolation"), so a plan that contains one stops the stage with zero changes (observed 2026-10-18, dev-11374: a live-MySQL `test/font-pairing.e2e-spec.ts` planned in steps 1, 4 and 5). The committed RED/GREEN steps test the adapter with its injected collaborator stubbed — the exact query text, its replacements, and the returned rows — and run without any backend. Real-backend verification is declared only as this stage's `Conformance tests:` line below (`requires=[db|http|external]`): the implementation run writes that script under `<task_root>/qa/scripts/`, runs it against a local or replica backend when one is reachable, never commits it, and records a non-PASS or unreachable backend as advisory. Read a brief line such as `verify: integration test against a test database` as that conformance declaration, not as a committed test. Do not plan a shared real-DB guard, a test DDL fixture, or an edit to an existing real-IO suite in source to support it. **Enforced:** every `P-Step-*` item of the §5.5.9 plan-body round carries this rule to its verifiers (`okstra_ctl.phases.implementation_planning.authoring._REAL_IO_STEP_NOTE`), which vote `DISAGREE(b)` on a step that breaks it; no path pattern tells a mocked test from a live one, so no validator checks it.
|
|
151
152
|
- **The `command` cell runs inside an okstra task worktree, not a bare checkout (BLOCKING).** okstra provisions `.okstra`, the configured sync entries (`.project-docs`, `.claude`, …), and — for `implementation` — a nested `stage-<N>/` worktree into the tree the step executes in. Two consequences bind every command you write:
|
|
152
153
|
- **Clean-tree assertions use `okstra worktree-status --check-clean`.** A bare `git status --porcelain` is never empty there, so an assertion built on one fails on okstra's scaffolding rather than on the stage's work. The okstra command asks the same question over source paths only and exits 1 when dirty, so it stands alone as a step's assertion: `okstra worktree-status --check-clean`. Validator S13 rejects the bare form. Do not add a `git tag stage-<N>-exit` to the step. Stage completion records the commit in the consumer ledger without creating or moving git tags.
|
|
153
154
|
- **Never read an `.okstra/` artifact back out of a git object.** `.okstra/**` is gitignored and never committed — the executor aborts a commit that stages an ignored path and the verifier reports a committed `.okstra` path as a branch defect — so `git cat-file -e <tag>:.okstra/…`, `git show <tag>:.okstra/…`, and every variant of that read can never resolve, at any tag, in any stage. A later stage that needs a QA artifact reads it from the working tree or receives it through the carry sidecar / verifier result; do not design a stage contract around one being reachable from a tag. Validator S12 rejects the read.
|
|
@@ -98,3 +98,40 @@ def materialize_technical_input_packet(payload_json: str, run_root: Path, seq: s
|
|
|
98
98
|
"- Read the input before planning experiments; copy each fact identity verbatim into results.\n"
|
|
99
99
|
+ "\n```json\n" + payload_json + "\n```\n"
|
|
100
100
|
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def technical_evidence_packet_block(source: Path, project_root: Path) -> str:
|
|
104
|
+
"""carry-in 이 시험 결과 기록이면 후속 비교 워커가 읽을 증거 위치를 렌더한다.
|
|
105
|
+
|
|
106
|
+
clarification carry-in 은 §1 행만 싣고 원문은 `Source report:` 한 줄로만
|
|
107
|
+
가리키므로, 이 블록이 없으면 워커는 시험 결과와 로그에 닿지 못한다.
|
|
108
|
+
"""
|
|
109
|
+
from okstra_ctl.path_resolve import relative_to_project_root
|
|
110
|
+
from okstra_ctl.phases.technical_verification.validation import REPORT_NAME_RE
|
|
111
|
+
|
|
112
|
+
match = REPORT_NAME_RE.fullmatch(source.name)
|
|
113
|
+
if match is None or not source.is_file():
|
|
114
|
+
return ""
|
|
115
|
+
data = load_owned_object_snapshot(
|
|
116
|
+
source, artifact="technical verification result"
|
|
117
|
+
).value
|
|
118
|
+
if (data.get("header") or {}).get("taskType") != "technical-verification":
|
|
119
|
+
return ""
|
|
120
|
+
experiments = source.parent.parent / "experiments" / match[1]
|
|
121
|
+
lines = [
|
|
122
|
+
"## Technical Verification Evidence",
|
|
123
|
+
"",
|
|
124
|
+
"The clarification response is a technical-verification record. Apply"
|
|
125
|
+
" `Technical evidence loop` in `## Phase Focus Extract`: read each"
|
|
126
|
+
" check's plan, commands and logs below before authoring a fresh"
|
|
127
|
+
" feasibility vote.",
|
|
128
|
+
"",
|
|
129
|
+
f"- Result record: `{relative_to_project_root(source, project_root)}`",
|
|
130
|
+
"- Experiment plans and logs:"
|
|
131
|
+
f" `{relative_to_project_root(experiments, project_root)}/`",
|
|
132
|
+
]
|
|
133
|
+
for check in (data.get("technicalVerification") or {}).get("checks") or []:
|
|
134
|
+
lines.append(
|
|
135
|
+
f"- `{check.get('id')}` on `{check.get('candidateId')}`: {check.get('status')}"
|
|
136
|
+
)
|
|
137
|
+
return "\n".join(lines)
|
|
@@ -10,6 +10,8 @@ from okstra_ctl.implementation_direction import validate_task_artifact_path
|
|
|
10
10
|
from okstra_ctl.json_boundary import load_owned_object_snapshot
|
|
11
11
|
from okstra_ctl.technical_verification_facts import TechnicalVerificationError
|
|
12
12
|
|
|
13
|
+
REPORT_NAME_RE = re.compile(r"final-report-technical-verification-(\d{3,})\.data\.json")
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
def _check_execution_evidence(
|
|
15
17
|
check: Mapping[str, Any], project_root: Path, run_root: Path, seq: str
|
|
@@ -51,9 +53,7 @@ def validate_technical_verification_report(
|
|
|
51
53
|
"""발행·최종 검증에서 같은 사실 집합과 실제 시험 산출물을 대조한다."""
|
|
52
54
|
if (data.get("header") or {}).get("taskType") != "technical-verification":
|
|
53
55
|
return []
|
|
54
|
-
match =
|
|
55
|
-
r"final-report-technical-verification-(\d{3,})\.data\.json", report_path.name
|
|
56
|
-
)
|
|
56
|
+
match = REPORT_NAME_RE.fullmatch(report_path.name)
|
|
57
57
|
if match is None:
|
|
58
58
|
return ["technical verification report must use its canonical record path"]
|
|
59
59
|
run_root = report_path.parent.parent
|
|
@@ -26,7 +26,7 @@ import sys
|
|
|
26
26
|
from collections.abc import Mapping
|
|
27
27
|
from pathlib import Path
|
|
28
28
|
|
|
29
|
-
from okstra_project.dirs import TASK_MANIFEST_FILENAME
|
|
29
|
+
from okstra_project.dirs import TASK_MANIFEST_FILENAME
|
|
30
30
|
|
|
31
31
|
# phase 시퀀스 / 기본 next-phase 매핑의 SSOT 는 workflow 모듈이다. 과거
|
|
32
32
|
# render_task_manifest 가 동일한 리스트/딕셔너리를 로컬에 중복 정의했는데,
|
|
@@ -50,6 +50,8 @@ from .registry.provider_registry import default_provider_registry
|
|
|
50
50
|
from .path_hints import compact_active_run_context, hydrate_run_context
|
|
51
51
|
from .paths import task_timeline_file
|
|
52
52
|
from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
|
|
53
|
+
from .mcp_servers import load_declarations as load_mcp_declarations
|
|
54
|
+
from .mcp_servers import render_block as render_mcp_block
|
|
53
55
|
from .report_contract import CURRENT_REPORT_SCHEMA_VERSION
|
|
54
56
|
from .report_language import resolve_report_language
|
|
55
57
|
from .validation_contract import CURRENT_VALIDATION_CONTRACT_VERSION
|
|
@@ -2509,54 +2511,11 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
|
|
|
2509
2511
|
# --------------------------------------------------------------------------- #
|
|
2510
2512
|
|
|
2511
2513
|
|
|
2512
|
-
_NO_MCP_SERVERS_LINE = (
|
|
2513
|
-
f"- No MCP servers are declared in `{OKSTRA_DIR_NAME}/project.json`'s "
|
|
2514
|
-
"`mcpServers` array. Treat MCP tools as unavailable for this run. To enable "
|
|
2515
|
-
"them, add entries shaped `{name, description, tools, notes?}` to that array "
|
|
2516
|
-
"and re-render the bundle."
|
|
2517
|
-
)
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
2514
|
def build_available_mcp_servers_block(project_root: Path) -> str:
|
|
2521
|
-
"""
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
so each user/project declares the MCP surface available to their
|
|
2526
|
-
lead+workers. Missing file or empty array yields a generic "none declared"
|
|
2527
|
-
fallback.
|
|
2528
|
-
"""
|
|
2529
|
-
config_path = project_json_path(project_root)
|
|
2530
|
-
try:
|
|
2531
|
-
raw = load_owned_object(config_path, artifact="project config")
|
|
2532
|
-
except JsonBoundaryError:
|
|
2533
|
-
return _NO_MCP_SERVERS_LINE
|
|
2534
|
-
servers = raw.get("mcpServers") if isinstance(raw, dict) else None
|
|
2535
|
-
if not isinstance(servers, list) or not servers:
|
|
2536
|
-
return _NO_MCP_SERVERS_LINE
|
|
2537
|
-
lines: list[str] = []
|
|
2538
|
-
for entry in servers:
|
|
2539
|
-
if not isinstance(entry, dict):
|
|
2540
|
-
continue
|
|
2541
|
-
name = str(entry.get("name", "")).strip()
|
|
2542
|
-
if not name:
|
|
2543
|
-
continue
|
|
2544
|
-
description = str(entry.get("description", "")).strip()
|
|
2545
|
-
tools = entry.get("tools") or []
|
|
2546
|
-
notes = str(entry.get("notes", "")).strip()
|
|
2547
|
-
parts = [f"`mcp__{name}`"]
|
|
2548
|
-
if description:
|
|
2549
|
-
parts.append(description)
|
|
2550
|
-
if isinstance(tools, list) and tools:
|
|
2551
|
-
tool_names = ", ".join(
|
|
2552
|
-
f"`{str(t).strip()}`" for t in tools if str(t).strip()
|
|
2553
|
-
)
|
|
2554
|
-
if tool_names:
|
|
2555
|
-
parts.append(f"Tools: {tool_names}")
|
|
2556
|
-
if notes:
|
|
2557
|
-
parts.append(notes)
|
|
2558
|
-
lines.append("- " + ". ".join(parts) + ".")
|
|
2559
|
-
return "\n".join(lines) if lines else _NO_MCP_SERVERS_LINE
|
|
2515
|
+
"""provider 판정 없이 렌더한다. prepare 는 판정한 블록을 ctx 로 넘기므로
|
|
2516
|
+
이 경로는 prepare 를 거치지 않는 렌더 호출자만 쓴다."""
|
|
2517
|
+
declarations, _problems = load_mcp_declarations(project_root)
|
|
2518
|
+
return render_mcp_block(declarations)
|
|
2560
2519
|
|
|
2561
2520
|
|
|
2562
2521
|
# --------------------------------------------------------------------------- #
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
"""Report-language resolution — the one place the precedence lives.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
for `ko`, get a Korean-instructed writer, and still ship an English report.
|
|
3
|
+
Prepare calls this once and freezes the answer as `reportLanguage` in the run
|
|
4
|
+
manifest. Both report-writer dispatch paths stamp that manifest value as the
|
|
5
|
+
`**Report Language:**` line, and report assembly copies it into
|
|
6
|
+
`meta.reportLanguage`, so a config edit after prepare cannot split the two.
|
|
8
7
|
|
|
9
8
|
The value is a language tag (`en`, `ko`, `fr`, `pt-BR`, …). The report writer
|
|
10
9
|
authors the report prose in that language; nothing here knows any particular
|
|
@@ -23,6 +23,7 @@ from okstra_ctl.phases.implementation_planning.validation import validate_select
|
|
|
23
23
|
from .phases.technical_verification.entry import (
|
|
24
24
|
prepare_technical_verification_input,
|
|
25
25
|
materialize_technical_input_packet,
|
|
26
|
+
technical_evidence_packet_block,
|
|
26
27
|
)
|
|
27
28
|
|
|
28
29
|
from argparse import (
|
|
@@ -55,6 +56,7 @@ from .phases.implementation_planning.entry import (
|
|
|
55
56
|
)
|
|
56
57
|
from . import fix_cycles
|
|
57
58
|
from .analysis_packet import build_analysis_packet
|
|
59
|
+
from .mcp_servers import NO_MCP_SERVERS_LINE, prepare_mcp
|
|
58
60
|
from .coverage_census import (
|
|
59
61
|
CensusError,
|
|
60
62
|
census_filename,
|
|
@@ -3051,6 +3053,16 @@ def _write_instruction_set_sources(
|
|
|
3051
3053
|
),
|
|
3052
3054
|
coverage_census_text=coverage_census_text,
|
|
3053
3055
|
has_selected_direction=(instruction_set / "selected-direction.json").is_file(),
|
|
3056
|
+
mcp_servers_text=(
|
|
3057
|
+
"" if ctx.get("AVAILABLE_MCP_SERVERS", NO_MCP_SERVERS_LINE) == NO_MCP_SERVERS_LINE
|
|
3058
|
+
else ctx["AVAILABLE_MCP_SERVERS"]
|
|
3059
|
+
),
|
|
3060
|
+
technical_evidence_text=(
|
|
3061
|
+
technical_evidence_packet_block(
|
|
3062
|
+
Path(inp.clarification_response_path), Path(ctx["PROJECT_ROOT"]),
|
|
3063
|
+
)
|
|
3064
|
+
if inp.clarification_response_path else ""
|
|
3065
|
+
),
|
|
3054
3066
|
)
|
|
3055
3067
|
if inp.task_type == "technical-verification":
|
|
3056
3068
|
packet += materialize_technical_input_packet(
|
|
@@ -4055,6 +4067,20 @@ def _prepare_user_authorization(inp: PrepareInputs) -> dict:
|
|
|
4055
4067
|
return record
|
|
4056
4068
|
|
|
4057
4069
|
|
|
4070
|
+
def _prepare_mcp_block(
|
|
4071
|
+
project_root: Path, plan: AssignmentPlan, brief_path: Path,
|
|
4072
|
+
) -> str:
|
|
4073
|
+
brief_text = brief_path.read_text(encoding="utf-8") if brief_path.is_file() else ""
|
|
4074
|
+
prepared = prepare_mcp(
|
|
4075
|
+
project_root,
|
|
4076
|
+
(assignment.provider_id for assignment in plan.assignments),
|
|
4077
|
+
brief_text,
|
|
4078
|
+
)
|
|
4079
|
+
for warning in prepared.warnings:
|
|
4080
|
+
print(f"prepare: {warning}", file=sys.stderr)
|
|
4081
|
+
return prepared.block
|
|
4082
|
+
|
|
4083
|
+
|
|
4058
4084
|
def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
4059
4085
|
"""Produce a complete okstra task bundle on disk. See module docstring."""
|
|
4060
4086
|
user_authorization = _prepare_user_authorization(inp)
|
|
@@ -4326,6 +4352,9 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
4326
4352
|
"DYNAMIC_EXECUTION_ROLES_JSON": json.dumps(
|
|
4327
4353
|
dynamic_roles, ensure_ascii=False,
|
|
4328
4354
|
),
|
|
4355
|
+
"AVAILABLE_MCP_SERVERS": _prepare_mcp_block(
|
|
4356
|
+
project_root, assignment_plan, inp.brief_path,
|
|
4357
|
+
),
|
|
4329
4358
|
})
|
|
4330
4359
|
refresh_run_context_snapshot(ctx)
|
|
4331
4360
|
if lead_runtime == "codex":
|
|
@@ -12,6 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
import argparse
|
|
13
13
|
import json
|
|
14
14
|
import re
|
|
15
|
+
import subprocess
|
|
15
16
|
from datetime import datetime, timezone
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
|
|
@@ -23,6 +24,7 @@ from .consumers import (
|
|
|
23
24
|
carry_head_commit,
|
|
24
25
|
read_consumers,
|
|
25
26
|
)
|
|
27
|
+
from .file_size_cap import file_size_violations
|
|
26
28
|
from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
|
|
27
29
|
from .paths import RunRef
|
|
28
30
|
from .stage_reopen import resolve_planned_stage
|
|
@@ -49,6 +51,34 @@ def _read_carry(carry_file: Path, stage: int) -> dict:
|
|
|
49
51
|
return carry
|
|
50
52
|
|
|
51
53
|
|
|
54
|
+
def _check_file_size(project_root: Path, carry: dict, head: str) -> str:
|
|
55
|
+
"""Refuse to settle a stage whose diff breaks the file-size limit; return what was checked.
|
|
56
|
+
|
|
57
|
+
Verifiers report the limit from `okstra correction file-size`; this re-runs it so a stage
|
|
58
|
+
a verifier did not check still reaches the correction round instead of landing. A range
|
|
59
|
+
git cannot read is reported in the output, not refused: the limit is a code-quality rule
|
|
60
|
+
and must not strand a stage whose verdict already passed.
|
|
61
|
+
"""
|
|
62
|
+
rng = carry.get("stageCommitRange")
|
|
63
|
+
base = rng.get("base") if isinstance(rng, dict) else None
|
|
64
|
+
if not base:
|
|
65
|
+
return "not checked: the carry has no stageCommitRange.base"
|
|
66
|
+
try:
|
|
67
|
+
violations = [
|
|
68
|
+
v for v in file_size_violations(project_root, str(base), head) if v.blocking
|
|
69
|
+
]
|
|
70
|
+
except subprocess.CalledProcessError as exc:
|
|
71
|
+
return f"not checked: git: {exc.stderr.decode(errors='replace').strip()}"
|
|
72
|
+
if violations:
|
|
73
|
+
raise StateError(
|
|
74
|
+
"the stage diff breaks the 400-line file-size limit:\n"
|
|
75
|
+
+ "\n".join(v.line() for v in violations)
|
|
76
|
+
+ f"\nRun a correction round (`okstra correction prompt ... --base {head} "
|
|
77
|
+
f"--stage-base {base}`) so the executor splits these files, or end the run FAIL "
|
|
78
|
+
"with `okstra stage-fail` when no round is left.", stage="file-size")
|
|
79
|
+
return f"pass: {base}..{head}"
|
|
80
|
+
|
|
81
|
+
|
|
52
82
|
def _supersede(target: Path, rows: list[dict], stage: int) -> Path:
|
|
53
83
|
"""Move the sidecar of the run that first settled a reopened stage aside, unchanged."""
|
|
54
84
|
settled = [r for r in rows if r.get("stage") == stage and r.get("status") == "done"]
|
|
@@ -76,6 +106,7 @@ def done_stage(
|
|
|
76
106
|
identity, plan_run_root = resolve_planned_stage(project_root, task_key, stage)
|
|
77
107
|
carry = _read_carry(carry_file, stage)
|
|
78
108
|
head = carry_head_commit(carry)
|
|
109
|
+
file_size_cap = _check_file_size(project_root, carry, head)
|
|
79
110
|
report = report if report.is_absolute() else project_root / report
|
|
80
111
|
try:
|
|
81
112
|
report_rel = report.relative_to(RunRef.from_report_path(report).run_dir).as_posix()
|
|
@@ -118,6 +149,7 @@ def done_stage(
|
|
|
118
149
|
"supersededCarryPath": str(superseded) if superseded else "",
|
|
119
150
|
"reportPath": report_rel,
|
|
120
151
|
"consumersPath": str(plan_run_root / "consumers.jsonl"),
|
|
152
|
+
"fileSizeCap": file_size_cap,
|
|
121
153
|
}
|
|
122
154
|
|
|
123
155
|
|
|
@@ -131,11 +163,13 @@ canonical sidecar `runs/implementation/carry/stage-<N>.json`, then appends the
|
|
|
131
163
|
`done` row with that carry path, the report path relative to its run directory,
|
|
132
164
|
the carry's head and the completion time. A reopened stage's previous sidecar
|
|
133
165
|
is moved to `carry/superseded/stage-<N>-from-<task-type>-<seq>.json` first.
|
|
134
|
-
Refuses a carry for another stage, a failed carry,
|
|
135
|
-
another head
|
|
166
|
+
Refuses a carry for another stage, a failed carry, a stage already done at
|
|
167
|
+
another head, and a carry whose stageCommitRange diff leaves a source or test
|
|
168
|
+
file over the 400-line limit (`okstra correction file-size`). Re-running with the
|
|
169
|
+
same carry writes nothing new.
|
|
136
170
|
|
|
137
171
|
Output: JSON { ok, taskKey, stage, headCommit, carryPath, supersededCarryPath,
|
|
138
|
-
reportPath, consumersPath }. Exit 1 on a refusal, 2 when
|
|
172
|
+
reportPath, consumersPath, fileSizeCap }. Exit 1 on a refusal, 2 when
|
|
139
173
|
PROJECT_ROOT cannot be resolved.
|
|
140
174
|
"""
|
|
141
175
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Step 6: Take over as Okstra lead
|
|
2
2
|
|
|
3
|
-
Read `resources.leadExecutionPromptPath` from the run manifest at `<RUN_MANIFEST_PATH>` — the same manifest Step 5 already opened for `resources.leadPromptMetadataPath` — then read that file verbatim and take over as `Okstra lead` in the current host-native session. Do not construct the path yourself: the prompt is written per run under `runs/<task-type>/prompts/lead-execution-prompt-<task-type>-<seq>.md`, not in the instruction set, and the `<seq>` is assigned at prepare time. The prompt selects exactly one runtime adapter and points to compact intake artifacts first (`active-run-context`, `analysis-profile.md`, and `analysis-packet.md`); full source files such as `analysis-material.md`, `reference-expectations.md`, and `final-report-template.md` are lazy/fallback inputs. Follow the rendered prompt order, do not preempt it.
|
|
3
|
+
Read `resources.leadExecutionPromptPath` from the run manifest at `<RUN_MANIFEST_PATH>` — the same manifest Step 5 already opened for `resources.leadPromptMetadataPath` — then read that file verbatim and take over as `Okstra lead` in the current host-native session. Do not construct the path yourself: the prompt is written per run under `runs/<task-type>/prompts/lead-execution-prompt-<task-type>-<seq>.md`, not in the instruction set, and the `<seq>` is assigned at prepare time. The prompt selects exactly one runtime adapter and points to compact intake artifacts first (`active-run-context`, `analysis-profile.md`, and `analysis-packet.md`); full source files such as `analysis-material.md`, `reference-expectations.md`, and `final-report-template.md` are lazy/fallback inputs. Follow the rendered prompt order, do not preempt it. The run manifest's `reportLanguage` sets the language of okstra report artifacts; a host, project or global instruction file (`CLAUDE.md`, `AGENTS.md`) does not override it, and the lead never tells the report writer to use another language.
|
|
4
4
|
|
|
5
5
|
Then proceed through the phases exactly as the lead prompt directs (Phase 1 context → Phase 2+ worker dispatch → final synthesis → final report).
|
|
6
6
|
|
|
@@ -20,8 +20,8 @@ On `ok: false`, re-prompt with the same `current.step` using the error message.
|
|
|
20
20
|
The wizard tells you which relay operation to use via `next.interaction.kind`. Step 1 loads the current registered host's relay contract. Use the matching entry under its `interactions` object, including the runtime-generated navigation and completion options. If the entry is absent, stop instead of inventing a host function or falling back silently. Use the explicit `Legacy text relay compatibility` mapping only when the preflight response has no `relayContract`.
|
|
21
21
|
|
|
22
22
|
- `native-single` → use the current registered host's relay contract for its native single-select function. Pass every option in its original order and submit the selected `options[].value`.
|
|
23
|
-
- `native-multi` → use the relay contract's native multi-select function. Pass every option in its original order and submit the selected
|
|
24
|
-
- `native-group` → use the relay contract's native grouped-question function once. Preserve every question and option, then submit one JSON object keyed by `questions[].step`;
|
|
23
|
+
- `native-multi` → use the relay contract's native multi-select function. Pass every option in its original order and submit the host's selected-labels string unchanged (labels may contain `, `; the wizard decodes it against the option list); submit an empty selection as `--answer ""`.
|
|
24
|
+
- `native-group` → use the relay contract's native grouped-question function once. Preserve every question and option, then submit one JSON object keyed by `questions[].step`; a single-select step value is the selected option value, and a multi-select step value is the host's selected-labels string unchanged, not a JSON array.
|
|
25
25
|
- `numbered-single` → render every option as a 1-based numbered Markdown list and submit the user's next message unchanged.
|
|
26
26
|
- `numbered-multi` → render every option as a 1-based numbered Markdown list and submit the user's comma-separated reply unchanged.
|
|
27
27
|
- `sequential-group` → render every question in order, preserve each complete option list, collect every answer, and submit one JSON object keyed by `questions[].step`.
|
|
@@ -47,8 +47,8 @@ On `ok: false`, re-prompt with the same `current.step` using the error message.
|
|
|
47
47
|
The wizard tells you which relay operation to use via `next.interaction.kind`. Step 1 loads the current registered host's relay contract. Use the matching entry under its `interactions` object, including the runtime-generated navigation and completion options. If the entry is absent, stop instead of inventing a host function or falling back silently. Use the explicit `Legacy text relay compatibility` mapping only when the preflight response has no `relayContract`.
|
|
48
48
|
|
|
49
49
|
- `native-single` → use the current registered host's relay contract for its native single-select function. Pass every option in its original order and submit the selected `options[].value`.
|
|
50
|
-
- `native-multi` → use the relay contract's native multi-select function. Pass every option in its original order and submit the selected
|
|
51
|
-
- `native-group` → use the relay contract's native grouped-question function once. Preserve every question and option, then submit one JSON object keyed by `questions[].step`;
|
|
50
|
+
- `native-multi` → use the relay contract's native multi-select function. Pass every option in its original order and submit the host's selected-labels string unchanged (labels may contain `, `; the wizard decodes it against the option list); submit an empty selection as `--answer ""`.
|
|
51
|
+
- `native-group` → use the relay contract's native grouped-question function once. Preserve every question and option, then submit one JSON object keyed by `questions[].step`; a single-select step value is the selected option value, and a multi-select step value is the host's selected-labels string unchanged, not a JSON array.
|
|
52
52
|
- `numbered-single` → render every option as a 1-based numbered Markdown list and submit the user's next message unchanged.
|
|
53
53
|
- `numbered-multi` → render every option as a 1-based numbered Markdown list and submit the user's comma-separated reply unchanged.
|
|
54
54
|
- `sequential-group` → render every question in order, preserve each complete option list, collect every answer, and submit one JSON object keyed by `questions[].step`.
|
|
@@ -290,7 +290,7 @@ You can delete the literal state-file path after this point — its job is done.
|
|
|
290
290
|
<!-- BEGIN FRAGMENT: wizard-lead-takeover -->
|
|
291
291
|
## Step 6: Take over as Okstra lead
|
|
292
292
|
|
|
293
|
-
Read `resources.leadExecutionPromptPath` from the run manifest at `<RUN_MANIFEST_PATH>` — the same manifest Step 5 already opened for `resources.leadPromptMetadataPath` — then read that file verbatim and take over as `Okstra lead` in the current host-native session. Do not construct the path yourself: the prompt is written per run under `runs/<task-type>/prompts/lead-execution-prompt-<task-type>-<seq>.md`, not in the instruction set, and the `<seq>` is assigned at prepare time. The prompt selects exactly one runtime adapter and points to compact intake artifacts first (`active-run-context`, `analysis-profile.md`, and `analysis-packet.md`); full source files such as `analysis-material.md`, `reference-expectations.md`, and `final-report-template.md` are lazy/fallback inputs. Follow the rendered prompt order, do not preempt it.
|
|
293
|
+
Read `resources.leadExecutionPromptPath` from the run manifest at `<RUN_MANIFEST_PATH>` — the same manifest Step 5 already opened for `resources.leadPromptMetadataPath` — then read that file verbatim and take over as `Okstra lead` in the current host-native session. Do not construct the path yourself: the prompt is written per run under `runs/<task-type>/prompts/lead-execution-prompt-<task-type>-<seq>.md`, not in the instruction set, and the `<seq>` is assigned at prepare time. The prompt selects exactly one runtime adapter and points to compact intake artifacts first (`active-run-context`, `analysis-profile.md`, and `analysis-packet.md`); full source files such as `analysis-material.md`, `reference-expectations.md`, and `final-report-template.md` are lazy/fallback inputs. Follow the rendered prompt order, do not preempt it. The run manifest's `reportLanguage` sets the language of okstra report artifacts; a host, project or global instruction file (`CLAUDE.md`, `AGENTS.md`) does not override it, and the lead never tells the report writer to use another language.
|
|
294
294
|
|
|
295
295
|
Then proceed through the phases exactly as the lead prompt directs (Phase 1 context → Phase 2+ worker dispatch → final synthesis → final report).
|
|
296
296
|
|
|
@@ -52,6 +52,7 @@ okstra manager view --manager-id <manager-id>
|
|
|
52
52
|
|
|
53
53
|
`remove project`, `remove task`, `remove task-group` and `task remove-child` change nothing without `--confirm`; they print what would be removed (`Removed: no — dry run`). Run the command without `--confirm` first, show the user the listed project, path, tasks or kept children, and rerun with `--confirm` only after the user approves that exact target. These commands touch manager files only; project `.okstra/` state and briefs written by `task split` stay.
|
|
54
54
|
|
|
55
|
+
- `task status` shows the snapshot as of `Synced at` with its `Sync age`; a child marked `changed since sync: yes` has a project task manifest written after that sync, so run `task sync` before reporting its state.
|
|
55
56
|
- `remove project` unregisters the project and keeps its child tasks. `task status` shows them with `project linked: no`, the view marks them `unlinked`, and `task run` refuses them until the project is registered again with `new project`.
|
|
56
57
|
- `task update` changes only the fields given and needs no confirmation. `new task` still refuses to change an existing task's objective, common brief or progress mode.
|
|
57
58
|
|
|
@@ -47,8 +47,8 @@ On `ok: false`, re-prompt with the same `current.step` using the error message.
|
|
|
47
47
|
The wizard tells you which relay operation to use via `next.interaction.kind`. Step 1 loads the current registered host's relay contract. Use the matching entry under its `interactions` object, including the runtime-generated navigation and completion options. If the entry is absent, stop instead of inventing a host function or falling back silently. Use the explicit `Legacy text relay compatibility` mapping only when the preflight response has no `relayContract`.
|
|
48
48
|
|
|
49
49
|
- `native-single` → use the current registered host's relay contract for its native single-select function. Pass every option in its original order and submit the selected `options[].value`.
|
|
50
|
-
- `native-multi` → use the relay contract's native multi-select function. Pass every option in its original order and submit the selected
|
|
51
|
-
- `native-group` → use the relay contract's native grouped-question function once. Preserve every question and option, then submit one JSON object keyed by `questions[].step`;
|
|
50
|
+
- `native-multi` → use the relay contract's native multi-select function. Pass every option in its original order and submit the host's selected-labels string unchanged (labels may contain `, `; the wizard decodes it against the option list); submit an empty selection as `--answer ""`.
|
|
51
|
+
- `native-group` → use the relay contract's native grouped-question function once. Preserve every question and option, then submit one JSON object keyed by `questions[].step`; a single-select step value is the selected option value, and a multi-select step value is the host's selected-labels string unchanged, not a JSON array.
|
|
52
52
|
- `numbered-single` → render every option as a 1-based numbered Markdown list and submit the user's next message unchanged.
|
|
53
53
|
- `numbered-multi` → render every option as a 1-based numbered Markdown list and submit the user's comma-separated reply unchanged.
|
|
54
54
|
- `sequential-group` → render every question in order, preserve each complete option list, collect every answer, and submit one JSON object keyed by `questions[].step`.
|
|
@@ -455,7 +455,7 @@ If `render-bundle` raises Step 5's concurrent-run conflict detection (concurrent
|
|
|
455
455
|
<!-- BEGIN FRAGMENT: wizard-lead-takeover -->
|
|
456
456
|
## Step 6: Take over as Okstra lead
|
|
457
457
|
|
|
458
|
-
Read `resources.leadExecutionPromptPath` from the run manifest at `<RUN_MANIFEST_PATH>` — the same manifest Step 5 already opened for `resources.leadPromptMetadataPath` — then read that file verbatim and take over as `Okstra lead` in the current host-native session. Do not construct the path yourself: the prompt is written per run under `runs/<task-type>/prompts/lead-execution-prompt-<task-type>-<seq>.md`, not in the instruction set, and the `<seq>` is assigned at prepare time. The prompt selects exactly one runtime adapter and points to compact intake artifacts first (`active-run-context`, `analysis-profile.md`, and `analysis-packet.md`); full source files such as `analysis-material.md`, `reference-expectations.md`, and `final-report-template.md` are lazy/fallback inputs. Follow the rendered prompt order, do not preempt it.
|
|
458
|
+
Read `resources.leadExecutionPromptPath` from the run manifest at `<RUN_MANIFEST_PATH>` — the same manifest Step 5 already opened for `resources.leadPromptMetadataPath` — then read that file verbatim and take over as `Okstra lead` in the current host-native session. Do not construct the path yourself: the prompt is written per run under `runs/<task-type>/prompts/lead-execution-prompt-<task-type>-<seq>.md`, not in the instruction set, and the `<seq>` is assigned at prepare time. The prompt selects exactly one runtime adapter and points to compact intake artifacts first (`active-run-context`, `analysis-profile.md`, and `analysis-packet.md`); full source files such as `analysis-material.md`, `reference-expectations.md`, and `final-report-template.md` are lazy/fallback inputs. Follow the rendered prompt order, do not preempt it. The run manifest's `reportLanguage` sets the language of okstra report artifacts; a host, project or global instruction file (`CLAUDE.md`, `AGENTS.md`) does not override it, and the lead never tells the report writer to use another language.
|
|
459
459
|
|
|
460
460
|
Then proceed through the phases exactly as the lead prompt directs (Phase 1 context → Phase 2+ worker dispatch → final synthesis → final report).
|
|
461
461
|
|
|
@@ -174,6 +174,8 @@ matching section:
|
|
|
174
174
|
- **F. declared architecture style** (`architecture.style`) — `hexagonal` /
|
|
175
175
|
`layered` / `none`; hand-added, default `none`. Declaring one makes that
|
|
176
176
|
architecture's placement rules binding in planning + verification.
|
|
177
|
+
- **H. MCP servers** (`mcpServers`) — `okstra mcp add` declares a server,
|
|
178
|
+
`okstra mcp check` shows which provider CLIs reach it.
|
|
177
179
|
|
|
178
180
|
## Step 4: Verify
|
|
179
181
|
|