okstra 0.220.1 → 0.222.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/storage-model.md +1 -1
- package/docs/architecture.md +6 -4
- package/docs/cli.md +13 -9
- package/docs/project-structure-overview.md +5 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/launch.template.md +1 -1
- package/runtime/prompts/lead/convergence.md +3 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +10 -6
- package/runtime/prompts/lead/phase-routing.md +1 -1
- 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/business_flow/engine.py +1 -0
- package/runtime/python/okstra_ctl/convergence.py +155 -19
- package/runtime/python/okstra_ctl/convergence_critic_prompt.py +3 -2
- package/runtime/python/okstra_ctl/convergence_migration.py +10 -1
- package/runtime/python/okstra_ctl/convergence_provenance.py +69 -24
- package/runtime/python/okstra_ctl/convergence_reverify_prompt.py +4 -1
- package/runtime/python/okstra_ctl/correction_cli.py +50 -11
- package/runtime/python/okstra_ctl/correction_round.py +80 -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/fix_cycles.py +14 -1
- package/runtime/python/okstra_ctl/git_reconcile.py +62 -8
- 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/next_phase.py +22 -9
- package/runtime/python/okstra_ctl/phases/final_verification/target.py +4 -2
- package/runtime/python/okstra_ctl/phases/implementation/entry.py +6 -2
- package/runtime/python/okstra_ctl/phases/implementation/host-rules.md +1 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +5 -4
- 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/process_group.py +4 -0
- package/runtime/python/okstra_ctl/render.py +7 -48
- package/runtime/python/okstra_ctl/report_assembly.py +50 -13
- package/runtime/python/okstra_ctl/report_html/common.py +3 -1
- package/runtime/python/okstra_ctl/report_language.py +4 -5
- package/runtime/python/okstra_ctl/report_projections.py +45 -4
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +22 -1
- package/runtime/python/okstra_ctl/run.py +36 -5
- package/runtime/python/okstra_ctl/stage_done.py +37 -3
- package/runtime/python/okstra_ctl/stage_reconcile.py +49 -1
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +3 -3
- package/runtime/schemas/final-report-v3.0.schema.json +36 -0
- 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 +4 -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/html/base.template.html +6 -1
- package/runtime/templates/reports/html/i18n/en.json +1 -0
- package/runtime/templates/reports/html/i18n/ko.json +1 -0
- package/runtime/templates/reports/task-brief.template.md +3 -3
- package/runtime/validators/validate-run.py +18 -2
- package/runtime/validators/validate_session_conformance.py +19 -1
|
@@ -17,7 +17,7 @@ from typing import Any
|
|
|
17
17
|
from okstra_ctl import stage_targets
|
|
18
18
|
from okstra_ctl.design_prep import DesignPrepDecision, DesignPrepError, resolve_design_prep
|
|
19
19
|
from okstra_ctl.final_report_paths import final_report_data_path
|
|
20
|
-
from okstra_ctl.stage_reconcile import
|
|
20
|
+
from okstra_ctl.stage_reconcile import StaleStageRecordsError, require_reconciled_stages
|
|
21
21
|
from okstra_ctl.worker_prompt_policy import IMPLEMENTATION_STAGE_HEADER
|
|
22
22
|
|
|
23
23
|
|
|
@@ -94,7 +94,11 @@ def claim_implementation_stage_run(
|
|
|
94
94
|
|
|
95
95
|
plan_run_root = Path(inp.approved_plan_path).resolve().parents[1]
|
|
96
96
|
backfill_done_from_carry(plan_run_root)
|
|
97
|
-
|
|
97
|
+
# stage 재배치·base 계산보다 먼저 거부해야 거부된 prepare 가 git 을 건드리지 않는다.
|
|
98
|
+
try:
|
|
99
|
+
require_reconciled_stages(inp, plan_run_root)
|
|
100
|
+
except StaleStageRecordsError as exc:
|
|
101
|
+
raise ImplementationStageError(str(exc)) from exc
|
|
98
102
|
from okstra_ctl.stage_rebase import synchronize_stage_stack
|
|
99
103
|
|
|
100
104
|
try:
|
|
@@ -47,6 +47,7 @@ If `render-bundle` fails with a `PrepareError` containing `Recorded stage SHAs n
|
|
|
47
47
|
- **Enter a different ref directly** — the user names a commit/branch/tag.
|
|
48
48
|
- **Abort** — stop the run without recovering.
|
|
49
49
|
3. Run `okstra git-reconcile … --apply --stage <N> --use-ref <ref>` with the chosen ref, then retry the failed `render-bundle` with the same arguments.
|
|
50
|
+
If that output carries a `Warning` naming the implementation anchor, the chosen head no longer contains the anchor: show the warning to the user and run its printed `--reset-anchor` command only after they confirm the new base, before the retry.
|
|
50
51
|
|
|
51
52
|
If the anchor (`implementation_base_commit`) is reported unresolvable, run the same command's `--reset-anchor <ref>` after user confirmation. Correcting a confirm item without the picker is forbidden — the runtime also rejects a confirm correction without `--use-ref`.
|
|
52
53
|
|
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
|
-
4. Converge the re-verification results
|
|
91
|
+
4. Converge the re-verification results in round `<N>`'s own namespace, leaving the initial convergence as written. Run Phase 5.5 again with `correction-r<N>` before `<task-type>` in every state name: `okstra convergence prepare-groups --correction-round <N>` writes `convergence-groups-correction-r<N>-<task-type>-<seq>.json`; seed it with `--work-state …/convergence-work-correction-r<N>-<task-type>-<seq>.json` and `--final-state …/convergence-correction-r<N>-<task-type>-<seq>.json`; name round plans and results `convergence-round-<k>-plan-correction-r<N>-<task-type>-<seq>.json`. A reverify round in it passes `--correction-round <N>` to `reverify-prompt` and `collect-results`, materializes with `--invocation-id` ending `-correction-r<N>-reverify-r<k>-<worker id>`, and names its result `<worker id>-worker-correction-r<N>-reverify-r<k>-<task-type>-<seq>.md`. Never reseed the initial paths: a terminal final there is reused, and `--restart-from-round0` refuses it. Round `<N+1>`'s `okstra correction prompt` reads its findings from this round's final, and the report writer's synthesis packet carries it as `Convergence state (correction-r<N>)` beside the initial `Convergence state` (`report_synthesis_packet._source_specs`); report assembly projects it into `crossVerification.correctionRounds[]` and appends the evidence it promotes to `evidence.primary` with source `convergence correction-r<N>` (`report_assembly._correction_round_convergence`).
|
|
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:** `okstra convergence seed` (`scripts/okstra_ctl/convergence.py` `_seed`) accepts state paths only in the initial or a `correction-r<N>` namespace, refuses a round grouping before every grouped verifier returned an `ok` round `<N>` result (`convergence_provenance.missing_scope_results`), and checks its cited items against that round's results (`discovery_attempts` per namespace); `convergence_migration.decide_seed_action` refuses `--restart-from-round0` without an invalid working state; `plan-round` refuses to overwrite a round plan whose namespace already dispatched that round (`_refuse_dispatched_plan_overwrite`); `collect-results` reads only the namespace's reverify invocations (`correction_round.reverify_invocation_in_scope`). `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
|
|
@@ -84,6 +84,10 @@ def kill_group(pgid: int) -> bool:
|
|
|
84
84
|
os.killpg(pgid, signal.SIGKILL)
|
|
85
85
|
except ProcessLookupError:
|
|
86
86
|
return False
|
|
87
|
+
except PermissionError:
|
|
88
|
+
# expected-miss: macOS 는 남은 구성원이 아직 거둬지지 않은 좀비뿐인 그룹에 EPERM 을
|
|
89
|
+
# 준다. 죽일 살아 있는 프로세스가 없다는 뜻이라, 끝난 워커의 결과를 버릴 이유가 아니다.
|
|
90
|
+
return False
|
|
87
91
|
return True
|
|
88
92
|
|
|
89
93
|
|
|
@@ -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
|
# --------------------------------------------------------------------------- #
|
|
@@ -18,6 +18,7 @@ from .clarification_items import (
|
|
|
18
18
|
)
|
|
19
19
|
from .final_report_schema import load_schema_version, validate
|
|
20
20
|
from .report_inputs import ReportInputPath, report_input_paths, uses_report_contract_v3
|
|
21
|
+
from .correction_round import correction_convergence_states, correction_round
|
|
21
22
|
from .json_boundary import JsonBoundaryError, load_owned_object, serialize_owned_object
|
|
22
23
|
from .report_narrative import parse_narrative, task_narrative_errors
|
|
23
24
|
from .report_synthesis_packet import (
|
|
@@ -98,6 +99,32 @@ def _read_json(row: ReportInputPath) -> dict[str, Any]:
|
|
|
98
99
|
return value
|
|
99
100
|
|
|
100
101
|
|
|
102
|
+
def _correction_round_convergence(
|
|
103
|
+
initial: ReportInputPath, cross: Mapping[str, Any],
|
|
104
|
+
) -> tuple[list[dict[str, Any]], list[tuple[str, Mapping[str, Any]]]]:
|
|
105
|
+
"""Each correction round's re-convergence, projected like the initial one, and the
|
|
106
|
+
evidence it promotes labelled `convergence <scope>`."""
|
|
107
|
+
rows: list[dict[str, Any]] = []
|
|
108
|
+
promoted: list[tuple[str, Mapping[str, Any]]] = []
|
|
109
|
+
consensus = len(cross["consensus"])
|
|
110
|
+
differences = len(cross["differences"])
|
|
111
|
+
for scope, path in correction_convergence_states(initial.path):
|
|
112
|
+
row = ReportInputPath(initial.key, initial.owner, path)
|
|
113
|
+
try:
|
|
114
|
+
result = project_convergence(
|
|
115
|
+
_read_json(row),
|
|
116
|
+
first_consensus=consensus + 1, first_difference=differences + 1,
|
|
117
|
+
)
|
|
118
|
+
except ReportProjectionError as exc:
|
|
119
|
+
_fail(row.owner, path, "$", exc)
|
|
120
|
+
projected = result["crossVerification"]
|
|
121
|
+
consensus += len(projected["consensus"])
|
|
122
|
+
differences += len(projected["differences"])
|
|
123
|
+
rows.append({"round": correction_round(scope), **projected})
|
|
124
|
+
promoted.extend((f"convergence {scope}", item) for item in result["promotedEvidence"])
|
|
125
|
+
return rows, promoted
|
|
126
|
+
|
|
127
|
+
|
|
101
128
|
def _input_map(project_root: Path, manifest: Mapping[str, Any]) -> dict[str, ReportInputPath]:
|
|
102
129
|
try:
|
|
103
130
|
return {row.key: row for row in report_input_paths(project_root, manifest)}
|
|
@@ -488,6 +515,11 @@ def _compose(
|
|
|
488
515
|
data = {"schemaVersion": "3.0", **narrative, **execution}
|
|
489
516
|
data["tokenUsage"] = usage
|
|
490
517
|
data["crossVerification"] = convergence_data["crossVerification"]
|
|
518
|
+
rounds, round_evidence = _correction_round_convergence(
|
|
519
|
+
inputs["convergence"], data["crossVerification"]
|
|
520
|
+
)
|
|
521
|
+
if rounds:
|
|
522
|
+
data["crossVerification"]["correctionRounds"] = rounds
|
|
491
523
|
data["clarificationItems"] = _clarifications(ledger, activities, inputs["approval-decisions"].path)
|
|
492
524
|
# 작성자의 사실 분류와 리드 원장의 질문 연결은 양쪽 입력을 합친 뒤 검사한다.
|
|
493
525
|
for reason in validate_blocked_answer_channel(data):
|
|
@@ -504,8 +536,13 @@ def _compose(
|
|
|
504
536
|
if scope:
|
|
505
537
|
data["verificationScope"] = scope
|
|
506
538
|
data["agentActivity"] = activities
|
|
507
|
-
data["evidence"] = {"primary": _promoted_evidence(
|
|
508
|
-
|
|
539
|
+
data["evidence"] = {"primary": _promoted_evidence([
|
|
540
|
+
*(("convergence", item) for item in convergence_data["promotedEvidence"]),
|
|
541
|
+
*round_evidence,
|
|
542
|
+
])}
|
|
543
|
+
data["missingInformation"] = _missing_information(
|
|
544
|
+
project_root, manifest, convergence_data["criticUnverified"],
|
|
545
|
+
)
|
|
509
546
|
# 스냅샷 복사가 _attach_metadata 보다 먼저다 — frontmatter 가
|
|
510
547
|
# selectedDirectionRef.snapshotPath 를 읽는다.
|
|
511
548
|
_apply_selected_direction_snapshot(data, project_root, manifest)
|
|
@@ -529,16 +566,16 @@ def _attach_discussion(data: dict[str, Any], project_root: Path, manifest: Mappi
|
|
|
529
566
|
_fail("discussion-record", store.records_path, "discussion", exc)
|
|
530
567
|
|
|
531
568
|
|
|
532
|
-
def
|
|
533
|
-
project_root: Path, manifest: Mapping[str, Any],
|
|
569
|
+
def _missing_information(
|
|
570
|
+
project_root: Path, manifest: Mapping[str, Any], critic_unverified: list[dict[str, Any]],
|
|
534
571
|
) -> list[dict[str, Any]]:
|
|
535
|
-
"""Coverage-census warnings are the only rows this
|
|
536
|
-
v3 contract, so they number from `R-001`."""
|
|
537
|
-
state = read_census_state(project_root, manifest)
|
|
538
|
-
if state is None:
|
|
539
|
-
return []
|
|
572
|
+
"""Coverage-census warnings and unjudged critic gaps are the only rows this
|
|
573
|
+
field carries under the v3 contract, so they number from `R-001`."""
|
|
540
574
|
_project_id, _task_group, task_id = _identity(manifest)
|
|
541
|
-
|
|
575
|
+
ticket_id = task_id or "unknown"
|
|
576
|
+
state = read_census_state(project_root, manifest)
|
|
577
|
+
rows = [] if state is None else census_warning_rows(state, ticket_id=ticket_id)
|
|
578
|
+
rows += [{**row, "ticketId": row["ticketId"] or ticket_id} for row in critic_unverified]
|
|
542
579
|
return [{"id": f"R-{index:03d}", **row} for index, row in enumerate(rows, start=1)]
|
|
543
580
|
|
|
544
581
|
|
|
@@ -717,16 +754,16 @@ def _attach_metadata(data: dict[str, Any], manifest: Mapping[str, Any]) -> None:
|
|
|
717
754
|
}
|
|
718
755
|
|
|
719
756
|
|
|
720
|
-
def _promoted_evidence(
|
|
757
|
+
def _promoted_evidence(rows: list[tuple[str, Mapping[str, Any]]]) -> list[dict[str, Any]]:
|
|
721
758
|
return [
|
|
722
759
|
{
|
|
723
760
|
"id": f"E-{index:03d}",
|
|
724
761
|
"ticketId": row["ticketId"],
|
|
725
762
|
"evidence": row["evidence"],
|
|
726
763
|
"sourceItems": row["sourceItems"],
|
|
727
|
-
"source":
|
|
764
|
+
"source": source,
|
|
728
765
|
}
|
|
729
|
-
for index, row in enumerate(
|
|
766
|
+
for index, (source, row) in enumerate(rows, start=1)
|
|
730
767
|
]
|
|
731
768
|
|
|
732
769
|
|
|
@@ -403,8 +403,10 @@ def anchor_index(
|
|
|
403
403
|
for row_id in sorted(found)
|
|
404
404
|
if isinstance(row_id, str) and _anchorable(row_id)
|
|
405
405
|
}
|
|
406
|
+
cross = data.get("crossVerification") if isinstance(data.get("crossVerification"), dict) else {}
|
|
407
|
+
rounds = [cross, *(r for r in cross.get("correctionRounds") or [] if isinstance(r, dict))]
|
|
406
408
|
for block in ("consensus", "differences"):
|
|
407
|
-
for row in
|
|
409
|
+
for row in (row for scope in rounds for row in scope.get(block) or []):
|
|
408
410
|
row_id = row.get("id") if isinstance(row, dict) else None
|
|
409
411
|
if isinstance(row_id, str) and _anchorable(row_id) and row_id not in index:
|
|
410
412
|
index[row_id] = f"id-xv-{row_id}"
|
|
@@ -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
|
|
@@ -261,8 +261,14 @@ def _source_items(finding: Mapping[str, Any]) -> list[str]:
|
|
|
261
261
|
return [f"{_text(worker, 'worker').removesuffix('-worker')}:{finding_id}" for worker in workers]
|
|
262
262
|
|
|
263
263
|
|
|
264
|
-
def project_convergence(
|
|
265
|
-
|
|
264
|
+
def project_convergence(
|
|
265
|
+
state: Mapping[str, Any], *, first_consensus: int = 1, first_difference: int = 1,
|
|
266
|
+
) -> dict[str, Any]:
|
|
267
|
+
"""수렴 상태에서 독자용 합의·이견과 공개 근거만 승격한다.
|
|
268
|
+
|
|
269
|
+
정정 라운드 재수렴은 초기 행 다음 번호부터 매긴다 — 행 id 가 리포트 앵커라 한 리포트
|
|
270
|
+
안에서 겹치면 안 된다.
|
|
271
|
+
"""
|
|
266
272
|
consensus = []
|
|
267
273
|
differences = []
|
|
268
274
|
promoted = []
|
|
@@ -274,7 +280,7 @@ def project_convergence(state: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
274
280
|
ticket_id = _text(ticket_ids[0] if ticket_ids else "unknown", "unknown")
|
|
275
281
|
if finding.get("classification") in {"full-consensus", "partial-consensus"}:
|
|
276
282
|
row = {
|
|
277
|
-
"id": f"CV-{len(consensus) +
|
|
283
|
+
"id": f"CV-{len(consensus) + first_consensus:03d}",
|
|
278
284
|
"ticketId": ticket_id,
|
|
279
285
|
"statement": _text(finding.get("summary"), "Converged finding"),
|
|
280
286
|
"sourceItems": _source_items(finding),
|
|
@@ -298,7 +304,7 @@ def project_convergence(state: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
298
304
|
"position": "origin" if worker == finding.get("originWorker") else "dissent",
|
|
299
305
|
})
|
|
300
306
|
differences.append({
|
|
301
|
-
"id": f"D-{len(differences) +
|
|
307
|
+
"id": f"D-{len(differences) + first_difference:03d}",
|
|
302
308
|
"ticketId": ticket_id,
|
|
303
309
|
"disagreement": _text(finding.get("summary"), "Unresolved difference"),
|
|
304
310
|
"workersPosition": positions or [{"worker": "unknown", "itemId": "", "position": "unresolved"}],
|
|
@@ -311,5 +317,40 @@ def project_convergence(state: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
311
317
|
"differences": differences,
|
|
312
318
|
},
|
|
313
319
|
"promotedEvidence": promoted,
|
|
320
|
+
"criticUnverified": _critic_unverified(state),
|
|
314
321
|
}
|
|
322
|
+
critic = (state.get("config") or {}).get("critic")
|
|
323
|
+
if isinstance(critic, Mapping):
|
|
324
|
+
result["crossVerification"]["criticGaps"] = {
|
|
325
|
+
"proposed": critic.get("gapsProposed", 0),
|
|
326
|
+
"merged": critic.get("gapsMerged", 0),
|
|
327
|
+
"unverified": critic.get("gapsUnverified", 0),
|
|
328
|
+
}
|
|
315
329
|
return result
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _critic_unverified(state: Mapping[str, Any]) -> list[dict[str, Any]]:
|
|
333
|
+
"""미판정 gap 하나당 Missing Information 행 하나 — id 와 기본 ticket 은 조립이 채운다."""
|
|
334
|
+
ledger = (state.get("criticVerification") or {}).get("gaps") or []
|
|
335
|
+
tickets = {
|
|
336
|
+
gap.get("gapId"): (gap.get("ticketIds") or [None])[0]
|
|
337
|
+
for gap in ledger
|
|
338
|
+
if isinstance(gap, Mapping)
|
|
339
|
+
}
|
|
340
|
+
rows = []
|
|
341
|
+
for gap in state.get("unverifiedGaps") or []:
|
|
342
|
+
if not isinstance(gap, Mapping):
|
|
343
|
+
continue
|
|
344
|
+
gap_id = _text(gap.get("gapId"), "unknown")
|
|
345
|
+
rows.append({
|
|
346
|
+
"ticketId": tickets.get(gap_id),
|
|
347
|
+
"item": f"Coverage critic gap `{gap_id}` was never verified: "
|
|
348
|
+
f"{_text(gap.get('summary'), 'no summary')}",
|
|
349
|
+
"risk": (
|
|
350
|
+
f"Verification did not complete ({_text(gap.get('reason'), 'unknown reason')}), "
|
|
351
|
+
"so no analyser has judged this gap; it is neither a finding nor a rejection."
|
|
352
|
+
),
|
|
353
|
+
"owner": "Okstra lead",
|
|
354
|
+
"source": "critic-unverified",
|
|
355
|
+
})
|
|
356
|
+
return rows
|