okstra 0.125.4 → 0.125.6
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/README.md +1 -1
- package/docs/architecture/storage-model.md +1 -1
- package/docs/architecture.md +2 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +2 -1
- package/runtime/agents/workers/claude-worker.md +1 -0
- package/runtime/agents/workers/codex-worker.md +2 -1
- package/runtime/agents/workers/report-writer-worker.md +1 -1
- package/runtime/prompts/lead/context-loader.md +1 -1
- package/runtime/prompts/lead/convergence.md +12 -6
- package/runtime/prompts/lead/okstra-lead-contract.md +5 -3
- package/runtime/prompts/lead/plan-body-verification.md +9 -9
- package/runtime/prompts/lead/report-writer.md +33 -23
- package/runtime/prompts/lead/team-contract.md +2 -2
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/forbidden-actions.json +3 -3
- package/runtime/prompts/profiles/implementation-planning.md +1 -1
- package/runtime/prompts/profiles/implementation.md +2 -2
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/python/okstra_ctl/path_hints.py +10 -1
- package/runtime/python/okstra_ctl/paths.py +12 -1
- package/runtime/python/okstra_ctl/report_views.py +2 -0
- package/runtime/python/okstra_ctl/workers.py +5 -0
- package/runtime/python/okstra_ctl/workflow.py +1 -1
- package/runtime/schemas/final-report-v1.0.schema.json +12 -1
- package/runtime/skills/okstra-brief-gen/SKILL.md +15 -7
- package/runtime/templates/reports/final-report.template.md +3 -9
- package/runtime/templates/reports/i18n/en.json +1 -1
- package/runtime/templates/reports/i18n/ko.json +1 -1
- package/runtime/templates/worker-prompt-preamble.md +2 -0
- package/runtime/validators/validate-brief.py +43 -0
- package/runtime/validators/validate-run.py +161 -1
- package/runtime/validators/validate-schedule.py +6 -6
package/README.md
CHANGED
|
@@ -216,7 +216,7 @@ Major workflow changes added to `main` after 0.8.0:
|
|
|
216
216
|
- **Brief as translation layer + Step 6.5 reporter batch confirmation** — `okstra-brief-gen` converts external input—an issue ticket, requirements document, or user message—verbatim and marks okstra-added content as labeled augmentation. Step 6.5 asks the user to confirm in one batch whether that conversion changed meaning and records the result in `Reporter Confirmations`. Every analysis profile requires this section before phase analysis begins; `validators/validate-brief.py` enforces the requirement.
|
|
217
217
|
- **Artifact-home rule (`.okstra/`)** — `<project>/.okstra/` is the only project artifact root owned by okstra. Anything outside this root is not okstra memory and may be read only when explicitly cited in Source Material or Reporter Confirmations. Writing outside the root requires the same explicit requested path. Internal equivalents are `glossary.md` for terminology and `decisions/<NNNN>-<slug>.md` for decision records, evaluated during `implementation-planning`.
|
|
218
218
|
- **Self-contained HTML final-report view** — After Phase 7 writes `final-report-<task-type>-<seq>.md`, `okstra render-views` automatically creates a sibling self-contained HTML view in the same `reports/` directory, with inline CSS/JavaScript and no external URLs. Its `Export user response` button serializes `## 1. Clarification Items` responses to `runs/<task-type>/user-responses/user-response-<task-type>-<seq>.md` for the next phase. View generation never changes the source Markdown.
|
|
219
|
-
- **`improvement-discovery` task type (sidetrack entry point)** — Within a codebase scope and priority-lens allowlist, multi-worker consensus produces N improvement candidates, with a default of eight and a hard cap of 12. This is a sidetrack entry point outside `PHASE_SEQUENCE`; the user selects candidates and starts each under a new task ID with `requirements-discovery`, `implementation-planning`, or `error-analysis`. Lens enum SSOT: [`scripts/okstra_ctl/improvement_lenses.py`](scripts/okstra_ctl/improvement_lenses.py). Output section: `##
|
|
219
|
+
- **`improvement-discovery` task type (sidetrack entry point)** — Within a codebase scope and priority-lens allowlist, multi-worker consensus produces N improvement candidates, with a default of eight and a hard cap of 12. This is a sidetrack entry point outside `PHASE_SEQUENCE`; the user selects candidates and starts each under a new task ID with `requirements-discovery`, `implementation-planning`, or `error-analysis`. Lens enum SSOT: [`scripts/okstra_ctl/improvement_lenses.py`](scripts/okstra_ctl/improvement_lenses.py). Output section: `## 5.9 Improvement Candidates` (10-column table). Validator: [`validators/validate_improvement_report.py`](validators/validate_improvement_report.py).
|
|
220
220
|
|
|
221
221
|
<a id="ops-commands"></a>
|
|
222
222
|
### 3.5 Operations commands
|
|
@@ -85,7 +85,7 @@ Unless `--render-only` is used, the handed-off Claude session typically adds the
|
|
|
85
85
|
- `user-responses/user-response-<task-type>-<seq>.md` *(A sidecar downloaded under the same name by the HTML `Export user response` button; storing it here lets `--resume-clarification` automatically attach it to `clarification-response.md` in the instruction set — `clarification_items.clarification_response_with_sidecars`)*
|
|
86
86
|
- `worker-results/<worker>-audit-<task-type>-<seq>.md` *(Per-worker Reading Confirmation sidecar; for auditing rather than report content)*
|
|
87
87
|
- `status/final-<task-type>-<seq>.status`
|
|
88
|
-
- `runs/implementation/
|
|
88
|
+
- `runs/implementation/carry/stage-<N>.json` *(implementation only: execution evidence sidecar. Stage-SHARED like `consumers.jsonl` — it lives flat under the task-type run dir, NOT under `stage-<N>/`, because the next stage's carry-in and `backfill_done_from_carry` glob it without knowing the producing run's layout)*
|
|
89
89
|
- `consumers.jsonl` *(implementation-planning only: backlinks to the impl-run that consumed each stage in this plan; append-only)*
|
|
90
90
|
|
|
91
91
|
Design-preparation storage has three separate authorities:
|
package/docs/architecture.md
CHANGED
|
@@ -404,7 +404,7 @@ Common constraints:
|
|
|
404
404
|
- After the user fills answers into the `## 1. Clarification Items` section of a final report produced by `requirements-discovery`, `error-analysis`, or `implementation-planning`, carry that file into the next run with `--clarification-response <previous-final-report.md>`.
|
|
405
405
|
- The carried-in file is copied to the current run's `instruction-set/clarification-response.md`; the lead updates each prior `Q*` row's `Status` (`resolved` / `obsolete`) in Section 0 before proceeding.
|
|
406
406
|
- To edit answers and rerun in one operation, use `--resume-clarification`. See the `### --resume-clarification` section for details.
|
|
407
|
-
- **Stage carry-in (`implementation` → next stage)**: Every `implementation` run writes a `runs/implementation/
|
|
407
|
+
- **Stage carry-in (`implementation` → next stage)**: Every `implementation` run writes a `runs/implementation/carry/stage-<N>.json` evidence sidecar (flat under the task-type run dir — stage-shared, like `consumers.jsonl`). The next stage automatically carries in this file. Reverse links identifying which `implementation` run consumed each stage accumulate in the shared coordination file `runs/implementation-planning/consumers.jsonl`.
|
|
408
408
|
|
|
409
409
|
### Fix cycle (post-release bug hotfix history)
|
|
410
410
|
|
|
@@ -770,7 +770,7 @@ Entry points that enforce whether phase artifacts may be published:
|
|
|
770
770
|
|
|
771
771
|
Each validator blocks the phase with a `contract-violated` exit code when a contract is breached. Violations are applied when the next phase executes, leaving prior artifacts unchanged.
|
|
772
772
|
|
|
773
|
-
`contractValidation.status=failed` means the run artifact contract failed; it does not necessarily mean the implementation itself is incomplete. An implementation run derives `phaseOutcome.implementation` from `runs/implementation/
|
|
773
|
+
`contractValidation.status=failed` means the run artifact contract failed; it does not necessarily mean the implementation itself is incomplete. An implementation run derives `phaseOutcome.implementation` from `runs/implementation/carry/stage-<N>.json`, `runs/implementation-planning/consumers.jsonl`, and the approved Stage Map together. If every Stage Map stage has pass-grade carry evidence, it can retain the contract failure as audit information while correcting `workflow.nextRecommendedPhase` to `final-verification`.
|
|
774
774
|
|
|
775
775
|
## Practical notes
|
|
776
776
|
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -91,7 +91,8 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
|
|
|
91
91
|
- Otherwise (mtime stale `> 90s`, OR grace already applied): call `KillShell(shell_id: <shell_id>)`, then record a `cli-failure` event with `--error-type cli-failure`, `--exit-code 124`, `--duration-ms <observed_ms>`, `--message "okstra-antigravity-exec.sh exceeded polling cap (grace=<applied|not-applied>, last_mtime_age=<n>s)"`, and return `ANTIGRAVITY_CLI_TIMEOUT: agy exec exceeded polling cap`.
|
|
92
92
|
4. Otherwise continue polling. Read `current_ts` cheaply via another `Bash` call (`date +%s`) at most once per poll iteration.
|
|
93
93
|
- Do NOT abort the loop on transient `running` status. Only `completed` or the polling cap (initially 30min, optionally extended once to 35min by mtime grace) end it.
|
|
94
|
-
- **
|
|
94
|
+
- **Wrapper-internal idle watchdog.** Independently of this polling loop, the wrapper *script* reaps a silent CLI: if the CLI writes nothing to its log for `<idle-timeout-seconds>` (6th positional arg, default 600s), the script TERM/KILLs the CLI, records a `timeout` stage in the `<prompt-path>.status.json` sidecar, and the dispatched shell exits. Your `BashOutput` then reports the exit — handle it via step 8b (`cli-failure`), citing the sidecar's `timeout` stage in the message. This is a wrapper-owned mechanism, not an external timeout.
|
|
95
|
+
- **No external timeout from Lead.** This polling loop and the script's idle watchdog are together the SINGLE timeout authority for this dispatch. Lead MUST NOT impose a separate Agent-call timeout that would terminate this subagent before those caps fire (see team-contract "No external timeout on wrapper subagents").
|
|
95
96
|
- Do NOT issue parallel `BashOutput` calls or speculate about progress between polls.
|
|
96
97
|
- **No standalone `sleep` between polls.** The harness blocks `sleep` calls of 5 seconds or longer as a circumvention vector and explicitly forbids chaining shorter sleeps to work around it. `BashOutput` itself is the wait primitive — calling it again immediately after a `running` status is correct.
|
|
97
98
|
|
|
@@ -46,6 +46,7 @@ Unlike the Codex / Antigravity workers, you are an in-process Claude subagent
|
|
|
46
46
|
4. Anchor all file operations to the absolute `Project Root` from the lead prompt. Use absolute paths — do NOT rely on inherited cwd. Never use `cd` to change directory.
|
|
47
47
|
- **Executor exception (implementation phase only):** when this worker is dispatched as the `Executor` and the lead prompt provides an `EXECUTOR_WORKTREE_PATH` that differs from the session's inherited cwd, cwd-sensitive Bash commands (`cargo *`, `npm *`, `pnpm *`, `bun *`, `pytest`, `make *`, `go *`, language-toolchain test/build commands) MUST be prefixed with `cd <EXECUTOR_WORKTREE_PATH> && ` in the same Bash invocation — e.g. `cd /Users/.../worktrees/foo && cargo test -p bar`. Do NOT wrap the whole thing in `bash -lc "..."` or `bash -c "..."`; pass the chained command directly to the Bash tool so the leading `cd` token remains visible to the permission layer. The `cd` is scoped to the single Bash subshell and does not mutate the session's shell state, so this does not conflict with the "never use cd" rule above (which prevents the worker from drifting the session cwd across calls).
|
|
48
48
|
- **Executor coding-conventions preflight (BLOCKING, before your first `Edit` / `Write`):** when dispatched as the `Executor`, you MUST run the coding-conventions preflight defined in the executor sidecar (`prompts/profiles/_implementation-executor.md` → "Pre-implementation context exploration") before writing any code. Use this worker prompt's `**Coding preflight pack:**` anchor header; read that pack's `overview.md` and `clean-code.md`, then follow the routed pack's language → framework → architecture stages, iterating every rule and loading every matching resource (for example `frameworks/node-server.md` and `architectures/hexagonal.md` when their conditions match). The preflight pack is a runtime resource, not an auto-invoked skill; read the files via the Read tool by absolute path.
|
|
49
|
+
- **Executor post-write gates (BLOCKING, before your final commit / before claiming done):** the same dispatch prompt carries two gate blocks the lead appends after the preflight — `Pre-commit diff review sweep` (`prompts/profiles/_implementation-diff-review.md`) and `Implementation self-check` (`prompts/profiles/_implementation-self-check.md`). Execute both and record their coverage lines in your worker result exactly as the blocks specify. The codex/antigravity wrappers refuse to launch when an executor prompt lacks these blocks (`*_POSTWRITE_GATE_MISSING`); this worker runs in-process with no wrapper gate, so the contract lands on you directly — if either block is missing from your dispatch prompt, record a `tool-failure` in the errors sidecar and tell the lead to re-dispatch with the blocks included instead of skipping the gates.
|
|
49
50
|
- **Verifier QA-gate exception:** verifier roles MAY use the same `cd <WORKTREE> && <cmd>` shape when executing project-declared `qaCommands` (lint / format / typecheck / test) from `project.json`, since those commands are cwd-sensitive by nature. Outside the QA gate, verifiers still read with absolute paths only — do NOT use `cd` for file inspection.
|
|
50
51
|
- **No extra chaining beyond `cd && cmd`:** the permission matcher only allows the exact two-segment shape `cd <PATH> && <single-command>`. Do NOT append additional pipes, semicolons, redirects, or `&&` chains — e.g. `cd ... && cargo test ... 2>&1 | tail -20; echo "exit:$?"` will trigger a permission prompt every dispatch because the trailing `| tail`, `; echo`, and `2>&1` tokens disqualify the prefix match against `Bash(cargo:*)`. Let Claude Code capture the full stdout/stderr and exit code natively — do not post-process with `tail`, `head`, or `echo "exit:$?"`. If output truncation is genuinely needed, run the command first and read the result in a separate tool call.
|
|
51
52
|
|
|
@@ -91,7 +91,8 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
|
|
|
91
91
|
- Otherwise (mtime stale `> 90s`, OR grace already applied): call `KillShell(shell_id: <shell_id>)`, then record a `cli-failure` event with `--error-type cli-failure`, `--exit-code 124`, `--duration-ms <observed_ms>`, `--message "okstra-codex-exec.sh exceeded polling cap (grace=<applied|not-applied>, last_mtime_age=<n>s)"`, and return `CODEX_CLI_TIMEOUT: codex exec exceeded polling cap`.
|
|
92
92
|
4. Otherwise continue polling. Read `current_ts` cheaply via another `Bash` call (`date +%s`) at most once per poll iteration.
|
|
93
93
|
- Do NOT abort the loop on transient `running` status. Only `completed` or the polling cap (initially 30min, optionally extended once to 35min by mtime grace) end it.
|
|
94
|
-
- **
|
|
94
|
+
- **Wrapper-internal idle watchdog.** Independently of this polling loop, the wrapper *script* reaps a silent CLI: if the CLI writes nothing to its log for `<idle-timeout-seconds>` (6th positional arg, default 600s), the script TERM/KILLs the CLI, records a `timeout` stage in the `<prompt-path>.status.json` sidecar, and the dispatched shell exits. Your `BashOutput` then reports the exit — handle it via step 8b (`cli-failure`), citing the sidecar's `timeout` stage in the message. This is a wrapper-owned mechanism, not an external timeout.
|
|
95
|
+
- **No external timeout from Lead.** This polling loop and the script's idle watchdog are together the SINGLE timeout authority for this dispatch. Lead MUST NOT impose a separate Agent-call timeout that would terminate this subagent before those caps fire (see team-contract "No external timeout on wrapper subagents").
|
|
95
96
|
- Do NOT issue parallel `BashOutput` calls or speculate about progress between polls.
|
|
96
97
|
- **No standalone `sleep` between polls.** The harness blocks `sleep` calls of 5 seconds or longer as a circumvention vector and explicitly forbids chaining shorter sleeps to work around it. `BashOutput` itself is the wait primitive — calling it again immediately after a `running` status is correct.
|
|
97
98
|
|
|
@@ -87,7 +87,7 @@ Rules (the schema enforces most of these — they are listed here so you know *w
|
|
|
87
87
|
|
|
88
88
|
- `header.reportAuthor` is `"Report writer worker"`; `header.reportOwner` is `"Claude lead"`. Set author to `"Claude lead"` only for `release-handoff` runs (single-lead by design) or a recorded report-writer dispatch failure fallback.
|
|
89
89
|
- **Source items (worker:item) preservation.** Every `consensus[].sourceItems`, `differences[].workersPosition[].itemId`, and `evidence.primary[].sourceItems` entry MUST carry the worker:item-id pair (e.g. `claude:F-001`, `codex:1.1`, `antigravity:F-3`, or `lead:mcp-1` for lead-only evidence). The schema enforces this via the `SourceItem` regex; bare worker-name lists no longer parse.
|
|
90
|
-
- **Verdict Card consistency.** `verdictCard.verdictToken`
|
|
90
|
+
- **Verdict Card consistency.** `verdictCard.verdictToken` and `verdictCard.direction` MUST byte-match `finalVerdict.verdictToken` / `.direction`; `validators/validate-run.py` diffs both and fails the run on divergence. `verdictCard.nextStep` names the same action as `finalVerdict.nextStep` and `recommendedNextSteps[0].text` but is written as the actionable command the reader runs (e.g. `/okstra-run task-key=… task-type=release-handoff`) where the other two are prose — it is deliberately not a byte copy. Duplicating the compared values across `verdictCard` and `finalVerdict` is intentional so the validator can diff them.
|
|
91
91
|
- **Reader Summary.** Populate `readerSummary` when the schema excerpt exposes it. It is the human-first entrypoint for both Markdown and HTML: one sentence for the decision, one for the human action required, one for blockers, one for audit sections safe to skip on first read, and one runnable recommended command. Do not duplicate raw evidence tables here.
|
|
92
92
|
- **§7 phase-continuation row (mandatory for non-terminal task-types).** When `header.taskType` is one of `requirements-discovery` / `implementation-planning` / `error-analysis` / `implementation` / `final-verification`, `followUpTasks` MUST contain at least one row whose `origin` is `phase-continuation`, `suggestedTaskType` equals the next phase (byte-identical to `finalVerdict.nextStep`'s referenced phase), `newTaskId` reuses the current task-id, `autoSpawn` is `"no"`, and `priority` is `"P0"`. For `release-handoff` runs, omit the phase-continuation row. Schema `allOf` clause enforces this via `contains`.
|
|
93
93
|
- **No deprecated sections.** The schema has no `4.5.8 User Approval Request` body field, no `4.5.9 Open Questions`, no `5.1 Additional Material Request`, no `5.2 User Confirmation Questions` — clarifications go under the unified `clarificationItems[]` array.
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
| `projectId` | Project ID |
|
|
38
38
|
| `taskGroup` | Task group |
|
|
39
39
|
| `taskId` | Task ID |
|
|
40
|
-
| `taskType` | Analysis type (requirements-discovery, error-analysis, implementation-planning, implementation, final-verification, release-handoff) |
|
|
40
|
+
| `taskType` | Analysis type (requirements-discovery, error-analysis, implementation-planning, implementation, final-verification, release-handoff, plus the sidetrack improvement-discovery) |
|
|
41
41
|
| `workCategory` | bugfix / feature / improvement / refactor / ops / unknown |
|
|
42
42
|
| `recommendedWorkers` | List of selected workers |
|
|
43
43
|
| `currentStatus` | Current task status |
|
|
@@ -44,7 +44,7 @@ Configure this in the `convergence` block of `task-manifest.json`. If the block
|
|
|
44
44
|
| `verificationMode` | `"lightweight"` | `"lightweight"` or `"full-reanalysis"` |
|
|
45
45
|
| `adversarial` | phase-aware: `true` for `requirements-discovery` / `error-analysis` / `implementation-planning`, `false` otherwise | When `true`, Phase 5.5 runs in **adversarial mode** (see §"Adversarial Verification Mode"): verifiers actively try to refute each finding, the burden of proof sits on the claim, and `verificationMode` is forced to `"full-reanalysis"` scoped to the finding's cited evidence. Resolved by `scripts/okstra_ctl/render.py` `_build_convergence_block` and recorded in `config.adversarial` of the convergence state artifact. |
|
|
46
46
|
|
|
47
|
-
**Auto-disable rule (BLOCKING).** Convergence requires ≥2 analyser workers to produce a meaningful consensus tally. When the active profile's `Required workers:` block (see `prompts/profiles/*.md`) resolves to fewer than 2 analyser workers — e.g. `release-handoff` (zero analyser workers, lead-only) — the lead MUST treat `convergence.enabled` as `false` for that run regardless of manifest configuration, skip Phases 5.5 and the plan-body verification round ([plan-body-verification](./plan-body-verification.md)), and record `finalState: "converged"` with `totalRounds: 0` and an explanatory note in `config` (e.g. `"autoDisabled": "fewer-than-two-analysers"`). The plan-body round inherits the same rule via its `gating=false` advisory path.
|
|
47
|
+
**Auto-disable rule (BLOCKING).** Convergence requires ≥2 analyser workers to produce a meaningful consensus tally. When the active profile's `Required workers:` block (see `prompts/profiles/*.md`) resolves to fewer than 2 analyser workers — e.g. `release-handoff` (zero analyser workers, lead-only) — the lead MUST treat `convergence.enabled` as `false` for that run regardless of manifest configuration, skip Phases 5.5 and the plan-body verification round ([plan-body-verification](./plan-body-verification.md)), and record `finalState: "converged"` with `totalRounds: 0`, `round2SkippedReason: "auto-disabled"`, an empty `roundHistory`, and an explanatory note in `config` (e.g. `"autoDisabled": "fewer-than-two-analysers"`). The plan-body round inherits the same rule via its `gating=false` advisory path.
|
|
48
48
|
|
|
49
49
|
## Finding Category
|
|
50
50
|
|
|
@@ -136,7 +136,11 @@ WHILE roundIndex < effectiveMaxRounds AND queue is non-empty:
|
|
|
136
136
|
|
|
137
137
|
# Final classification — runs after the WHILE loop exits (queue empty OR roundIndex == effectiveMaxRounds OR Round 2 gate closed)
|
|
138
138
|
FOR each finding F still in queue:
|
|
139
|
-
IF
|
|
139
|
+
IF config.adversarial AND F was carried forward by the adversarial round
|
|
140
|
+
resolution (a surviving `counter-evidence` refute, or a burden-not-met
|
|
141
|
+
majority — see §"Adversarial Verification Mode"):
|
|
142
|
+
F.classification = "contested" # one evidence-backed refute denies consensus, whatever the AGREE tally
|
|
143
|
+
ELIF majority AGREE-or-SUPPLEMENT across all executed rounds:
|
|
140
144
|
F.classification = "partial-consensus"
|
|
141
145
|
ELSE:
|
|
142
146
|
F.classification = "contested"
|
|
@@ -235,7 +239,7 @@ ELSE:
|
|
|
235
239
|
resolve F as "partial-consensus"
|
|
236
240
|
```
|
|
237
241
|
|
|
238
|
-
`contested` remains a **final classification only** (per §"Scope and Terminology"): a disputed finding is carried forward through intermediate rounds and labelled `contested` only at the last executed round. For `requirements-discovery` (`effectiveMaxRounds = 1`) the single round IS the last round, so a split-with-hard-refute finding is labelled `contested` in that one round. The final-classifier block of §"Convergence Algorithm"
|
|
242
|
+
`contested` remains a **final classification only** (per §"Scope and Terminology"): a disputed finding is carried forward through intermediate rounds and labelled `contested` only at the last executed round. For `requirements-discovery` (`effectiveMaxRounds = 1`) the single round IS the last round, so a split-with-hard-refute finding is labelled `contested` in that one round. The final-classifier block of §"Convergence Algorithm" honours this: its first branch classifies an adversarially carried-forward finding `contested` regardless of the AGREE tally, so the two sections cannot assign the same finding different labels.
|
|
239
243
|
|
|
240
244
|
Design intent: one `counter-evidence` refute denies a claim consensus (it cannot rise above `contested` however many others AGREE); a lone `burden-not-met` doubt does not sink an otherwise-surviving claim — only a majority of them does. When every non-discoverer refutes (all_others_disagree) the finding is worker-unique regardless of refute basis — only the discoverer still holds it. A SUPPLEMENT/caveat with zero disagrees lands partial-consensus, not full-consensus, because a caveat means the claim does not pass cleanly (unlike the collaborative classifier, where SUPPLEMENT counts as full agreement).
|
|
241
245
|
|
|
@@ -489,7 +493,7 @@ Schema rules:
|
|
|
489
493
|
- `roundHistory[].carriedForwardCount`: queue size at the END of this round — the single definition. In-round insertions into the queue are forbidden, so this always equals `inputQueueSize - resolvedCount`. The pseudocode's per-item `carriedForwardCount += 1` accumulator is a counting convenience that lands on the same value; persist the post-round queue length, not the loop accumulator, if the two ever diverge.
|
|
490
494
|
- `roundHistory[].dispatches[]`: one entry per worker that was actually dispatched in this round. Each entry is `{worker, status, durationMs}`. `status ∈ {completed, timeout, error, not-run}`. `durationMs` is integer milliseconds and is always present, even for terminal-non-result dispatches (use the elapsed time before the wrapper gave up).
|
|
491
495
|
- `roundHistory[].skippedWorkers[]`: per-worker `{worker, reason}` for workers with no items to verify OR with a non-result dispatch.
|
|
492
|
-
- `round2SkippedReason`: literal enum `queue-empty | max-rounds-1 | all-reverify-non-result | not-skipped`. Always present.
|
|
496
|
+
- `round2SkippedReason`: literal enum `queue-empty | max-rounds-1 | all-reverify-non-result | not-skipped | auto-disabled`. Always present. Precedence when more than one condition holds, highest first: `"auto-disabled"` when the auto-disable rule fired (see §"Configuration" — the loop never built a queue; `roundHistory` is empty); `"max-rounds-1"` when `effectiveMaxRounds == 1` (Round 2 was never attempted — this wins over `queue-empty` even when Round 1 drained the queue, so a clean `requirements-discovery` run records `max-rounds-1`); `"all-reverify-non-result"` when all Round 1 dispatches terminated as non-result; `"queue-empty"` when Round 1 fully drained the queue (`effectiveMaxRounds >= 2` only, by the precedence above); `"not-skipped"` when Round 2 actually ran.
|
|
493
497
|
- `finalClassificationCounts`: post-loop counts. Required field with keys `fullConsensus`, `partialConsensus`, `contested`, `workerUnique`.
|
|
494
498
|
- `finalState ∈ {converged, max-rounds-reached, aborted-non-result}`. Assigned by the lead at WHILE-loop exit: `converged` when the queue is empty at the end of any round; `max-rounds-reached` when the loop exits because `roundIndex == effectiveMaxRounds` with the queue still non-empty; `aborted-non-result` when the loop exits via the Worker-failure BREAK (per the "Worker failure handling in reverify" section, rule 4). `aborted-non-result` is the new v1.1 value.
|
|
495
499
|
- `totalRounds`: count of rounds actually executed (not `effectiveMaxRounds`). May be `0` when Round 0 produced no queue items (all findings reached consensus during grouping).
|
|
@@ -519,11 +523,13 @@ Do NOT restate an existing finding. If nothing is missing, say so explicitly.
|
|
|
519
523
|
```
|
|
520
524
|
|
|
521
525
|
### Gap verification (1 adversarial reverify round)
|
|
522
|
-
Each critic gap enters the verification queue as a finding with `originWorker = "<provider>-critic"` and `source = "critic"`. The lead runs ONE adversarial reverify round (§"Adversarial Verification Mode" classifier) with the Phase 4 analysers (excluding the critic itself) as voters. Only gaps classified `full-consensus` / `partial-consensus` merge into the final report findings; `contested` / `worker-unique` gaps are treated as hallucinations and dropped (recorded in the convergence state, not promoted).
|
|
526
|
+
Each critic gap enters the verification queue as a finding with `originWorker = "<provider>-critic"` and `source = "critic"`. The lead runs ONE adversarial reverify round (§"Adversarial Verification Mode" classifier) with the Phase 4 analysers (excluding the critic itself) as voters. Only gaps classified `full-consensus` / `partial-consensus` merge into the final report findings; `contested` / `worker-unique` gaps are treated as hallucinations and dropped (recorded in the convergence state, not promoted).
|
|
527
|
+
|
|
528
|
+
**A gap that received no verdict is NOT a rejected gap (BLOCKING).** Dropping applies only to gaps the voters actually judged. A gap can also end the round *unjudged* — the verification dispatch returned a terminal non-result (`timeout`, `error`, no result file), the returned result covered only some of the gaps, or no non-critic analyser was available to vote at all. Nobody inspected those, so classifying them as hallucinations is a fabricated verdict. Each one MUST be recorded as a `## 5. Missing Information and Risks` row (`missingInformation`, `source: "critic-unverified"`) whose `risk` names the gap and the reason verification did not complete, and counted in `config.critic.gapsUnverified`. They are **not** promoted to findings (unverified) and **not** raised as `clarification` items — an unverified gap needs an analyser to verify it on the next run, not a decision from the user. Silently losing them is a contract violation: the batch that times out is exactly the batch of gaps too expensive to check, so the highest-risk items are the ones that vanish.
|
|
523
529
|
|
|
524
530
|
### State
|
|
525
531
|
- `convergence.critic` manifest block: `{ enabled, provider, modelExecutionValue }`.
|
|
526
|
-
- Convergence state artifact: critic gaps appear in `findings[]` with `source: "critic"`. Add a `config.critic` summary `{ provider, modelExecutionValue, gapsProposed, gapsMerged }`. `source` and `config.critic` are optional v1.2 fields (readers treat absence as null); no enum changes.
|
|
532
|
+
- Convergence state artifact: critic gaps appear in `findings[]` with `source: "critic"`. Add a `config.critic` summary `{ provider, modelExecutionValue, gapsProposed, gapsMerged, gapsUnverified }`. `gapsProposed - gapsMerged - gapsUnverified` is the number the voters judged and rejected, so a gap that quietly disappeared shows up as an arithmetic mismatch rather than as nothing at all. `source` and `config.critic` are optional v1.2 fields (readers treat absence as null); no enum changes.
|
|
527
533
|
|
|
528
534
|
## Acceptance critic pass (final-verification)
|
|
529
535
|
|
|
@@ -69,9 +69,11 @@ A single okstra run executes **exactly one** lifecycle phase. The phase is given
|
|
|
69
69
|
|-----------------|-----------------|-------------------|
|
|
70
70
|
| `requirements-discovery` | classification, routing decision, missing-input list, next-phase recommendation | code edits, plan documents, build/test execution that mutates state |
|
|
71
71
|
| `error-analysis` | evidence, root-cause hypotheses, reproduction gaps, validation paths | code edits, implementation design, build/migration/deploy execution |
|
|
72
|
-
| `implementation-planning` | option matrix, trade-offs, dependencies, recommended order, validation/rollback strategy, **explicit user-approval request** | source code edits, file writes outside the run's `reports/`, `prompts/`, `state/`, `manifests/`, `worker-results/`, `status/`, `sessions/` directories, build/migration/deploy execution |
|
|
72
|
+
| `implementation-planning` | option matrix, trade-offs, dependencies, recommended order, validation/rollback strategy, Tier3 conformance scripts + manifest under the task-root `qa/` tree, **explicit user-approval request** | source code edits, file writes outside the run's `reports/`, `prompts/`, `state/`, `manifests/`, `worker-results/`, `status/`, `sessions/` directories and the task-root `qa/` tree, build/migration/deploy execution |
|
|
73
73
|
| `implementation` | code edits authorised by an approved plan, accompanying tests | starting work without an approved `implementation-planning` final report carried in via `--clarification-response` or referenced in the brief |
|
|
74
|
-
| `final-verification` | acceptance verdict, residual risk, regression notes; read-only execution of existing test/validation commands
|
|
74
|
+
| `final-verification` | acceptance verdict, residual risk, regression notes; read-only execution of existing test/validation commands, run-artifact writes (qa result sidecars, `okstra handoff record-verified` on acceptance), and qaEnv-replica-only conformance runs are permitted | source code edits, refactors, scope expansion, mutations of the project or shared environments |
|
|
75
|
+
| `release-handoff` | user-selected packaging only: PR creation / push of the implementation branch, handoff summary (lead-only phase) | local commits, history-rewriting pushes, direct pushes to a base branch, release-publishing commands, source edits, worker dispatch of any kind |
|
|
76
|
+
| `improvement-discovery` *(sidetrack — outside `PHASE_SEQUENCE`)* | lens-scoped improvement candidates (`## 5.9` report), per-candidate next-phase recommendation | code edits, planning or root-cause work, candidates outside the lens allowlist or beyond the cap, starting any lifecycle phase inside this run |
|
|
75
77
|
|
|
76
78
|
Phase-transition checklist (lead, end of run):
|
|
77
79
|
|
|
@@ -203,7 +205,7 @@ These phases are governed by [team-contract](./team-contract.md). It is the cano
|
|
|
203
205
|
- Worker prompt anchor headers and body composition rules.
|
|
204
206
|
- The `[Required reading]` clause (analysis-packet primary input for analysis workers, full source files for report-writer, scoped inputs for reverify dispatches).
|
|
205
207
|
- The `[Error reporting]` clause and the asymmetry between claude-worker and codex/antigravity-worker prompts.
|
|
206
|
-
- Worker output contract (sections
|
|
208
|
+
- Worker output contract (sections 1–5 + optional Section 6; the Reading Confirmation block lives in the audit sidecar, never in the worker-results file — the preamble "Reading rules" section is canonical and the validator rejects violations), header standard, terminal statuses, errors-sidecar schema.
|
|
207
209
|
- Token-usage tracking conventions.
|
|
208
210
|
|
|
209
211
|
`Report writer worker` is NOT an analysis worker. Do not dispatch it in Phase 4/5 alongside analysis workers. It is invoked only in Phase 6 — see [report-writer](./report-writer.md).
|
|
@@ -17,7 +17,7 @@ Phase 4 workers produce independent analyses (Findings F-001…)
|
|
|
17
17
|
→ Phase 5.5 FINDING convergence ([convergence](./convergence.md), sections "Convergence Algorithm" through "Convergence State Artifact")
|
|
18
18
|
→ Phase 6 report-writer authors final-report draft (consolidated Option Candidates / Stepwise Execution Order / Dependency / Validation Checklist / Rollback)
|
|
19
19
|
→ PLAN-BODY VERIFICATION ROUND ← this contract
|
|
20
|
-
→ User Approval gate (
|
|
20
|
+
→ User Approval gate (the frontmatter `approved:` flip is honoured by run-prep only when this round's Gate result is `passed` or `passed-with-dissent`)
|
|
21
21
|
→ implementation phase (separate run)
|
|
22
22
|
```
|
|
23
23
|
|
|
@@ -40,10 +40,10 @@ Plan-body verification is configured under `convergence.planBodyVerification` in
|
|
|
40
40
|
|
|
41
41
|
| Setting | Default | Description |
|
|
42
42
|
|---------|---------|-------------|
|
|
43
|
-
| `enabled` | `true` | If `false`, the round is skipped and the
|
|
43
|
+
| `enabled` | `true` | If `false`, the round is skipped and the approval gate is not blocked by this round (legacy behaviour). |
|
|
44
44
|
| `maxRounds` | `1` | Upper bound. Plan-body verification is consistency / completeness checking, not fact checking — additional rounds rarely help. Range 1–3. |
|
|
45
45
|
| `selfFixMaxRounds` | `3` | Upper bound on the report-writer self-fix loop (§"Round protocol" step 7). Range 1–5. The loop also stops early on no-progress, so this is a ceiling, not a target. |
|
|
46
|
-
| `gating` | `true` | If `true` (default), `majority-disagree` blocks
|
|
46
|
+
| `gating` | `true` | If `true` (default), `majority-disagree` blocks approval. If `false`, the round is advisory-only and never blocks approval. |
|
|
47
47
|
|
|
48
48
|
Default values are emitted into the manifest by `scripts/okstra_ctl/render.py` (`_build_convergence_block`). The ctx knob `OKSTRA_PLAN_VERIFICATION=false` flips `planBodyVerification.enabled` to false.
|
|
49
49
|
|
|
@@ -108,7 +108,7 @@ Exception for `P-Req-*`: verifiers still MUST NOT re-open the original task brie
|
|
|
108
108
|
|
|
109
109
|
## Adversarial plan-body posture
|
|
110
110
|
|
|
111
|
-
When `config.adversarial == true` (the default for `implementation-planning`; see [convergence](./convergence.md) §"Configuration"), the plan-body round runs with an **adversarial posture**. The classification rules and gate arithmetic in §"Round protocol" are UNCHANGED — `majority-disagree` (a *majority* of analysers DISAGREE) remains the only classification that blocks
|
|
111
|
+
When `config.adversarial == true` (the default for `implementation-planning`; see [convergence](./convergence.md) §"Configuration"), the plan-body round runs with an **adversarial posture**. The classification rules and gate arithmetic in §"Round protocol" are UNCHANGED — `majority-disagree` (a *majority* of analysers DISAGREE) remains the only classification that blocks approval, and `dissent-isolated` still passes the gate. Adversarial mode changes only *how each verifier evaluates an item*:
|
|
112
112
|
|
|
113
113
|
- The burden of proof sits on the plan: an item earns `AGREE` only if the verifier actively tried to break it and could not.
|
|
114
114
|
- The verifier MUST open the file paths / symbols / commands the item cites and confirm they exist and are executable as written. This is the one allowed widening of the lightweight "judge from internal consistency and stated commands / paths" rule — confirming the existence of cited paths is not "re-analyzing the original requirements".
|
|
@@ -126,8 +126,8 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
|
|
|
126
126
|
- `full-consensus` — all participating analysers `AGREE` (SUPPLEMENT counts as agree on the item itself).
|
|
127
127
|
- `partial-consensus` — majority `AGREE`, dissenting `DISAGREE` recorded.
|
|
128
128
|
- `dissent-isolated` — only one worker `DISAGREE`s, others `AGREE` — treat as `partial-consensus` for gate purposes; record dissent. (Distinct from finding-convergence `worker-unique`, which means the *opposite*: only one worker AGREEs. Plan-body classifications use this dedicated label to avoid the collision.)
|
|
129
|
-
- `majority-disagree` — a *majority* of analysers `DISAGREE` (majority needs ≥2 participating non-error votes), OR any single-vote-blocking kind fires: one `DISAGREE(a)` / `DISAGREE(d)` on any item, or one `DISAGREE(f)` on a `P-Req-*` item (see §"Single-vote-blocking kinds"). This classification **blocks
|
|
130
|
-
- `needs-reverify` — a single-vote-blocking kind fired but the item has **fewer than 2 participating non-error votes**, i.e. the lone dissent was never cross-verified because its peer returned `verification-error`. A single-vote-blocking kind means "one *confirmed* DISAGREE is enough"; an unconfirmed one is not. This does **not** block
|
|
129
|
+
- `majority-disagree` — a *majority* of analysers `DISAGREE` (majority needs ≥2 participating non-error votes), OR any single-vote-blocking kind fires: one `DISAGREE(a)` / `DISAGREE(d)` on any item, or one `DISAGREE(f)` on a `P-Req-*` item (see §"Single-vote-blocking kinds"). This classification **blocks approval**.
|
|
130
|
+
- `needs-reverify` — a single-vote-blocking kind fired but the item has **fewer than 2 participating non-error votes**, i.e. the lone dissent was never cross-verified because its peer returned `verification-error`. A single-vote-blocking kind means "one *confirmed* DISAGREE is enough"; an unconfirmed one is not. This does **not** block approval — blocking on it would make a worker failure produce a stricter gate than a healthy roster, the same paradox the ≥2-vote majority rule already rules out. The item is re-dispatched in the next round (step 7); if it survives the round budget it is promoted per step 8 with a Statement that says verification never completed. **Enforced:** `validators/validate-run.py` `_classify_plan_item_gate` returns `needs-reverify` for this shape and `_recompute_plan_body_gate` folds it into `passed-with-dissent`.
|
|
131
131
|
- `contested` only meaningful when `maxRounds > 1`; at default `maxRounds=1`, fold any unresolved item into `partial-consensus`.
|
|
132
132
|
5. Gate result resolution:
|
|
133
133
|
- any `majority-disagree` item present AND `gating=true` → `blocked-by-disagreement`
|
|
@@ -154,7 +154,7 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
|
|
|
154
154
|
- `Blocks=approval`
|
|
155
155
|
- the item's `planItems[].clarificationId` set to that `C-<N>` (1:1 link). `validators/validate-run.py` `_validate_plan_body_clarification_matching` recomputes each item's class and fails when a majority-disagree item's `clarificationId` is missing, dangling, or points at a non-`approval` row.
|
|
156
156
|
- a `planner-fixable` item that survives the self-fix loop is still promoted, but its `Statement` MUST state "planner self-fix attempted but unresolved" and name the stop reason. `validators/validate-run.py` `_validate_self_fix_before_clarification` fails when a planner-fixable majority item is promoted while the budget is not exhausted — it requires `selfFixRoundsApplied >= 1` **and** `selfFixStopReason` in `{no-progress, max-rounds-reached}`, so neither `all-resolved` nor `not-attempted` can excuse a promotion.
|
|
157
|
-
9.
|
|
157
|
+
9. Approval lives in the report's YAML frontmatter `approved:` field — there is no in-body marker line. The user may flip it to `true` only when the Gate result is `passed` or `passed-with-dissent`. **Enforced:** run-prep (`scripts/okstra_ctl/run.py` `_validate_approved_plan`) fail-closes an `approved: true` plan whose data.json carries a blocking `gateResult` or an open/answered `Blocks: approval` clarification row, and `validators/validate-run.py` `_validate_plan_body_gate_recompute` rejects a declared `gateResult` healthier than the recorded votes.
|
|
158
158
|
|
|
159
159
|
## `plan-body-verification-<task-type>-<seq>.json` schema
|
|
160
160
|
|
|
@@ -195,9 +195,9 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
|
|
|
195
195
|
}
|
|
196
196
|
```
|
|
197
197
|
|
|
198
|
-
`dispatches[].terminalStatus` mirrors finding convergence (`completed | timeout | error | not-run
|
|
198
|
+
`dispatches[].terminalStatus` mirrors finding convergence (`completed | timeout | error | not-run`). A wrapper-recorded `cli-failure` is a run-error-log event, not a terminal status — record that dispatch's `terminalStatus` as `error`.
|
|
199
199
|
|
|
200
|
-
`planItems[].classification` enum: `full-consensus | partial-consensus | dissent-isolated | majority-disagree | contested`. `contested` only appears when `maxRounds > 1`; at default `maxRounds=1` any otherwise-unresolved item folds into `partial-consensus` per the round protocol above.
|
|
200
|
+
`planItems[].classification` enum: `full-consensus | partial-consensus | dissent-isolated | majority-disagree | needs-reverify | contested`. `needs-reverify` is the peer-error shape from §"Round protocol" step 4 (a single-vote-blocking kind with fewer than 2 participating non-error votes) — it survives into the state file when the round budget runs out before the re-dispatch resolves it, and `_recompute_plan_body_gate` folds it into `passed-with-dissent`. `contested` only appears when `maxRounds > 1`; at default `maxRounds=1` any otherwise-unresolved item folds into `partial-consensus` per the round protocol above.
|
|
201
201
|
|
|
202
202
|
`planItems[].votes.<worker>` is the verbatim verdict token emitted by the worker — `AGREE | DISAGREE(<a|b|c|d|e|f>) | SUPPLEMENT` — or `verification-error` for terminal non-result dispatches. The `DISAGREE` token retains its `<kind>` suffix so the breakage class is recoverable from the state file alone.
|
|
203
203
|
|
|
@@ -51,7 +51,7 @@ The prompt MUST include, in this order at the top:
|
|
|
51
51
|
has been resolved by the lead from project.json / global config
|
|
52
52
|
before the dispatch is constructed. The worker copies this verbatim
|
|
53
53
|
into `data.json.meta.reportLanguage`.
|
|
54
|
-
12. For implementation-planning runs: a literal block listing the
|
|
54
|
+
12. For implementation-planning runs: a literal block listing the 12 required English section headings — `Option Candidates`, `Trade-off`, `Recommended Option`, `Stage Map`, `Stepwise Execution Order`, `Dependency`, `Validation Checklist`, `Rollback`, `Requirement Coverage`, `Plan Body Verification`, `Cross-Project Dependencies`, `Decision Drafts`. This list is `PLANNING_REQUIRED_SECTIONS` in `validators/validate-run.py`; that tuple is the SSOT and this block must match it exactly. The writer uses these exact substrings as section headings (Korean translation in parentheses is allowed), and the `Plan Body Verification` section carries its required `Gate result:` line.
|
|
55
55
|
13. An explicit instruction: `You are the author of TWO files: (a) the final-report data.json at <Result Path>, (b) the worker-results audit file at <Worker Result Path>. After writing the data.json, invoke "okstra render-final-report <Result Path>" through the available execution interface so the markdown sibling is rendered before you return. Do not return the report inline. The validator fails the run when (a)'s schema validation fails, when the rendered markdown is absent, or when (b) is missing.`
|
|
56
56
|
14. The prose budget (dedup contract): `verdictCard.finalConclusion` is the conclusion SSOT — at most 3 sentences. `rationale.*` fields stay within 2 sentences each and reference the verdict card / row IDs instead of restating their prose; `readerSummary` fields are one line each; `summary` stays at 3-5 rows unless the run covers multiple tickets. The schema field descriptions carry the same budgets (`tests/contract/test_report_prose_budget.py` guards both surfaces). Generation time scales with output volume, so exceeding the budget is a cost bug, not extra diligence.
|
|
57
57
|
|
|
@@ -153,11 +153,13 @@ Milestone 1 keeps the final-report schema unchanged. Read the exact permitted va
|
|
|
153
153
|
|
|
154
154
|
### Agent-Specific Execution Status Table
|
|
155
155
|
|
|
156
|
+
The template renders this table in the merged record-meta shape (same pattern as §1 / §2.1): one meta cell stacking the short fields, one findings cell. The writer supplies the per-agent fields in data.json; the rendered form is:
|
|
157
|
+
|
|
156
158
|
```markdown
|
|
157
|
-
|
|
|
158
|
-
|
|
159
|
-
|
|
|
160
|
-
|
|
|
159
|
+
| Record | Summary of Key Findings |
|
|
160
|
+
|--------|-------------------------|
|
|
161
|
+
| **<team-state.lead.agent>**<br>Role: <team-state.lead.role><br>Model: <model><br>Status: completed<br>Raw tokens: <tokens><br>Billable: <billable><br>Cost: <cost><br>Duration: <duration> | Final synthesis status |
|
|
162
|
+
| **<team-state.workers[].agent>**<br>Role: <team-state.workers[].role><br>Model: <model><br>Status: <status><br>Raw tokens: <tokens><br>Billable: <billable><br>Cost: <cost><br>Duration: <duration> | Key findings summary |
|
|
161
163
|
```
|
|
162
164
|
|
|
163
165
|
Table Generation Rules:
|
|
@@ -184,7 +186,7 @@ Example (English mode shown — the renderer substitutes Korean when `meta.repor
|
|
|
184
186
|
| Lead | 10,479,327 | 1,769,798 | $26.55 |
|
|
185
187
|
| Worker subtotal | 7,988,699 | 2,075,192 | $25.43 |
|
|
186
188
|
| **Grand total** | **18,468,026** | **3,844,990** | **$51.97** |
|
|
187
|
-
| Codex/Antigravity CLI add-on | | | $
|
|
189
|
+
| Codex/Antigravity CLI add-on | | | $4.12 |
|
|
188
190
|
|
|
189
191
|
> **How to read**: "Raw tokens" is the total tokens the model actually processed (input + output + cache_creation + cache_read). In long sessions cache_read can account for 95%+ making the number look large. "Billable tokens" weights cache_read at 0.1×, cache_creation at 1.25×, and output at 5× to give an input-equivalent figure closer to actual cost. Costs are estimates based on published Anthropic/OpenAI/Google pricing.
|
|
190
192
|
```
|
|
@@ -199,27 +201,35 @@ Token Summary Generation Rules:
|
|
|
199
201
|
- **Lead** row: `usageSummary.leadTotalTokens` / `usageSummary.leadBillableEquivalentTokens` / `usageSummary.estimatedCostUsd.lead`.
|
|
200
202
|
- **Worker subtotal** row: `usageSummary.workerTotalTokens` / `usageSummary.workerBillableEquivalentTokens` / `usageSummary.estimatedCostUsd.claudeWorkers`.
|
|
201
203
|
- **Grand total** row: `usageSummary.grandTotalTokens` / `usageSummary.grandBillableEquivalentTokens` / sum of `lead + claudeWorkers`.
|
|
202
|
-
- **Codex/Antigravity CLI add-on** row: `usageSummary.estimatedCostUsd.cliWorkers`.
|
|
204
|
+
- **Codex/Antigravity CLI add-on** row: `usageSummary.estimatedCostUsd.cliWorkers`. The writer only supplies the value in data.json — the renderer emits the row when the CLI cost is > 0 and omits it entirely at 0 (`final-report.template.md` gates it on `tokenUsage.cli.costUsd > 0`), so do not expect a `$0.00` row in the rendered report.
|
|
203
205
|
- Format tokens with comma separators (e.g., `32,500`); format USD with two decimals (e.g., `$1.43`).
|
|
204
206
|
- If `lead` or any `worker.usage` records unavailable evidence, show `--` for that row and append a one-line note (`reason: <note>`).
|
|
205
207
|
- If pricing for a model is unknown, the script omits `estimatedCostUsd` for that block — show `N/A` in that column and add a note like `pricing missing for model <model>`.
|
|
206
208
|
|
|
207
209
|
### Implementation-planning section heading contract (BLOCKING)
|
|
208
210
|
|
|
209
|
-
When the run's `task-type` is `implementation-planning`, the final report MUST contain section headings whose **lines include each of the
|
|
211
|
+
When the run's `task-type` is `implementation-planning`, the final report MUST contain section headings whose **lines include each of the 12 literal English substrings below**. The validator (`validators/validate-run.py`) does plain substring matching on the report text and validates the design-preparation data contract — missing headings was a real, repeatedly observed failure mode caused by translating the headings to Korean.
|
|
212
|
+
|
|
213
|
+
The rows below mirror `PLANNING_REQUIRED_SECTIONS` in `validators/validate-run.py`, which is the SSOT. **Enforced:** `tests/contract/test_planning_required_sections_ssot.py` fails when this table and that tuple diverge — a heading listed here but not required (or required but not listed) sends the writer to author a section the validator rejects, or to omit one it demands.
|
|
210
214
|
|
|
211
215
|
| # | Required substring | Recommended heading form |
|
|
212
216
|
|---|--------------------|--------------------------|
|
|
213
217
|
| 1 | `Option Candidates` | `### Option Candidates (Korean gloss)` |
|
|
214
218
|
| 2 | `Trade-off` | `### Trade-off Matrix (Korean gloss)` |
|
|
215
219
|
| 3 | `Recommended Option` | `### Recommended Option (Korean gloss)` |
|
|
216
|
-
| 4 | `
|
|
217
|
-
| 5 | `
|
|
218
|
-
| 6 | `
|
|
219
|
-
| 7 | `
|
|
220
|
-
| 8 | `
|
|
221
|
-
| 9 | `
|
|
222
|
-
| 10 | `
|
|
220
|
+
| 4 | `Stage Map` | `## 5.5 Stage Map (Korean gloss)` — the per-stage sections hang off it. |
|
|
221
|
+
| 5 | `Stepwise Execution Order` | `### Stepwise Execution Order (Korean gloss)` |
|
|
222
|
+
| 6 | `Dependency` | `### Dependency / Migration Risk (Korean gloss)` |
|
|
223
|
+
| 7 | `Validation Checklist` | `### Validation Checklist (Korean gloss)` |
|
|
224
|
+
| 8 | `Rollback` | `### Rollback Strategy (Korean gloss)` |
|
|
225
|
+
| 9 | `Requirement Coverage` | `### Requirement Coverage (Korean gloss)` |
|
|
226
|
+
| 10 | `Plan Body Verification` + `Gate result:` | `### Plan Body Verification (Korean gloss)` containing a `Gate result:` line — copy `templates/reports/final-report.template.md §5.5.9` verbatim. Validator checks both substrings. |
|
|
227
|
+
| 11 | `Cross-Project Dependencies` | `### Cross-Project Dependencies (Korean gloss)` — `_(none)_` when the plan is single-project. |
|
|
228
|
+
| 12 | `Decision Drafts` | `### Decision Drafts (Korean gloss)` |
|
|
229
|
+
|
|
230
|
+
**Never author a `User Approval Request` section.** The approval gate moved to the YAML frontmatter `approved: true|false` field, and `validators/validate-run.py` fails any report containing a `## User Approval Request` block or a `### 5.5.8 User Approval Request` stub. This paragraph replaces an earlier row that listed the heading as required — following it produced a report the validator rejected.
|
|
231
|
+
|
|
232
|
+
`Implementation Design Preparation` is NOT in the substring list: `templates/reports/final-report.template.md` §5.5.10 renders that heading from `implementationPlanning.designPreparation`, so it is enforced by the schema + renderer rather than by a heading scan.
|
|
223
233
|
|
|
224
234
|
The Korean translation in parentheses is optional but the English keyword is mandatory. The body of each section is written in the Report Language per the writing rules below. For non-`implementation-planning` runs, omit this entire block — these headings are NOT validator-checked for other task-types.
|
|
225
235
|
|
|
@@ -279,7 +289,7 @@ Skipping this file because "the real report is in `reports/`" is wrong. Both fil
|
|
|
279
289
|
|
|
280
290
|
Section numbering follows `templates/reports/final-report.template.md` exactly — that file is the documentation SSOT for section names and ordering. For full body structure at authoring time, consult your run's **phase-stripped** `final-report-template.md` (the per-task-type instruction-set copy defined in Phase 6 dispatch item 8); the "copy that block verbatim" references below mean the §-block as it appears in that stripped copy, not a re-read of the full source.
|
|
281
291
|
|
|
282
|
-
**Verdict Card (top-of-report, mandatory).** Render `## Verdict Card` between the report header and the (conditional) Approval block. Its `Verdict Token` / `Direction` / `Next Step` cells MUST byte-match the corresponding cells in `## 7. Final Verdict` and the first item of `##
|
|
292
|
+
**Verdict Card (top-of-report, mandatory).** Render `## Verdict Card` between the report header and the (conditional) Approval block. Its `Verdict Token` / `Direction` / `Next Step` cells MUST byte-match the corresponding cells in `## 7. Final Verdict` and the first item of `## 3. Recommended Next Steps`. Divergence is `contract-violated`.
|
|
283
293
|
|
|
284
294
|
**Background and Rationale (top-of-report, mandatory — every task-type).** Fill the data.json `rationale` object (rendered as `## Background and Rationale`, right after the Verdict Card). It is the reviewer-facing narrative that answers four questions, in order — write each as **prose**, not a table:
|
|
285
295
|
- `motivation` — why we are doing this work (goal / context).
|
|
@@ -292,13 +302,13 @@ Every field MUST anchor its claim with at least one evidence reference — a `pa
|
|
|
292
302
|
**Reader-facing prose MUST NOT cite a bare brief/worker-internal ID that this report never surfaces** — `RC-*` (reporter confirmations, defined in the brief), `RF-*` / `F-*` (findings, defined in worker-results) have no anchor in the final report, so a reader hits an opaque token with nothing to click. Either expand it inline (`the confirmed version target 1.27.47→1.27.48`) or, for an audit trail, namespace it (`claude:F-005`). **Enforced:** `_validate_no_opaque_id_references` fails a bare `RC-*` / `RF-*` / non-namespaced `F-*` appearing in `verdictCard` / `finalVerdict` / `rationale` / `clarificationItems[].statement`. (The renderer anchors + links in-report IDs of any digit width, so a surfaced `RC-4`-style id does resolve.) Do NOT restate the Verdict Card or the §5.4 trade-off matrix verbatim — this section is the *why*, in connected prose, that those tables compress.
|
|
293
303
|
|
|
294
304
|
0. **Clarification Response Carried In** — render this `## 0.` heading ONLY when `{{CLARIFICATION_RESPONSE_RELATIVE_PATH}}` is non-empty. Walk every `C-*` row of the prior report's `## 1. Clarification Items` table, reconcile against new evidence, and record the outcome (`resolved` / `obsolete`) with citation before drafting the verdict. When no carry-in path was provided, OMIT the `## 0.` heading entirely — the validator fails an empty Section 0 stub. The lead calls `okstra incremental-scope` exactly once, combining answered-clarification stage impacts (`--impacted`) and changed PREP IDs (`--prep-items`); selected-option, Stage Map, or recommended-approach changes pass both CSVs empty to force full mode. Record that single decision JSON verbatim into `implementationPlanning.incrementalDecision` (`mode`, `reverifyStages`, `carryStages`, `reason`); the renderer emits the `### 0.1 Incremental Re-Verification Scope` audit block from it, and the validator fails an `incremental`-mode run whose Section 0 omits that block. In `incremental` mode this run's `planItems` MUST carry every plan-item id from the re-verified stages forward with its updated verdict; if re-verification concludes a plan item should be REMOVED, that is a signal the answer's blast radius is not local — do not drop it here, tell the lead to abandon incremental and re-route to a FULL re-verification, because the carry merge only adds prior items and would resurrect the removed item's stale verdict. After authoring the current data.json, call `okstra incremental-carry`, passing the decision's `carryStages` CSV to `--carry-stages` and its `reverifyStages` CSV to `--reverify-stages`. A `CarryError` means the stage/PREP ownership contract is unsafe: discard the partial merged output and route the run through full re-verification; never publish a partially merged report.
|
|
295
|
-
1. **
|
|
296
|
-
2. **
|
|
297
|
-
3. **
|
|
298
|
-
4. **
|
|
299
|
-
5. **
|
|
300
|
-
6. **
|
|
301
|
-
7. **
|
|
305
|
+
1. **Clarification Items** — single unified `C-*` table; column schema (4 columns with the short fields stacked in one record-meta cell), ID convention, and rerun behaviour are owned by `_common-contract.md §Clarification request policy` (SSOT). The deprecated `5.5.9 Open Questions` / `1.1 Additional Material Request` / `1.2 User Confirmation Questions` sub-sections are removed; the validator fails reports that reintroduce them.
|
|
306
|
+
2. **Evidence and Detailed Analysis** — primary evidence rows (file path, line, snippet); secondary evidence / alternate interpretations. If `reference-expectations.md` lists explicit expected values, record match/gap per row.
|
|
307
|
+
3. **Recommended Next Steps** — prioritized actions. After Phase 7's follow-up spawner runs, append a row per newly created task-key (see "Phase 6 → Phase 7 execution sequence" above). **Approval-gate consistency:** when §1 carries any `Blocks: approval` row with `Status` ∈ {open, answered}, the Verdict Card `Next Step` and the first recommended step MUST point to the clarification rerun (`resume-clarification` of the SAME task-type) — never to "flip frontmatter `approved: true` → jump straight to `implementation`". Run-prep enforces this gate (`run.py _validate_approved_plan` fail-closes on those rows and on a blocking data.json `gateResult`), so a direct-implementation next-step is an instruction the reader cannot actually follow. **Cross-project pointer rule:** for cross-project dependencies (another repo / a different top-level deployment module / a published package), `crossProjectDependencies` (§5.4 Cross-Project Dependencies) is authoritative — do NOT duplicate that substance (prerequisite work / verification signals / handoff) into `recommendedNextSteps`; put only a one-line pointer to that section (no double-recording).
|
|
308
|
+
4. **Follow-up Tasks** — auto-spawn-eligible table. Each row drives `okstra-spawn-followups.py`; see template §4 for the row schema.
|
|
309
|
+
5. **Missing Information and Risks** — uncertain / "I don't know" items. `implementation-planning` adds §5.5 (see heading contract below); `release-handoff` adds §5.6.
|
|
310
|
+
6. **Cross Verification Results** — 4 categories (Full / Partial / Contested / Worker-Unique) when convergence is enabled, per `convergence`. Prepend the Round History sub-table (columns: `Round | inputQueueSize | resolvedCount | carriedForwardCount | dispatches | skippedWorkers`) plus a `round2SkippedReason: <value>` note, pulled verbatim from `convergence-<task-type>-<seq>.json`. Empty contested list renders as `- No items lacking consensus.`. Convergence-disabled runs use the legacy Consensus/Differences format and omit the round table.
|
|
311
|
+
7. **Final Verdict** — `Direction` ∈ `continue-investigation` / `begin-implementation` / `approve` / `reject` / `hold`. **Verdict Token** is `not-applicable` for every task-type except `final-verification` — see "Final-verification verdict token contract" below for that case.
|
|
302
312
|
|
|
303
313
|
**§5.10 Fix History (data-presence gated).** When the run-manifest carries a `fixCycleId`, fill the data.json `fixCycle` block (`cycle` / `targetReport` / `symptom` / `runs`). Read the values from the task root's `history/fix-cycles.jsonl`: `cycle` MUST equal `fixCycleId`, `targetReport` / `symptom` come from that cycle's `opened` row, and `runs` lists its attached `run` rows (`taskType` / `runSeq` / `runManifest`). The validator (`validators/validate-run.py` → `_validate_fix_cycle`) fails the run when the block is missing or `fixCycle.cycle` does not match `fixCycleId`. When the run-manifest has no `fixCycleId`, OMIT the `fixCycle` block entirely — the renderer omits §5.10.
|
|
304
314
|
|
|
@@ -196,13 +196,13 @@ omits either header, the worker MUST return `<WORKER>_ERRORS_PATH_MISSING`
|
|
|
196
196
|
without proceeding.
|
|
197
197
|
|
|
198
198
|
- `cli-failure` events are recorded by the wrapper subagent itself (Codex / Antigravity), but **directly to the run-level error log** via `okstra error-log append-observed --error-type cli-failure ...` — NOT via the sidecar. The sidecar is an in-process tool-failure channel only.
|
|
199
|
-
- **Wrapper invocation arity.** Both `okstra-codex-exec.sh` and `okstra-antigravity-exec.sh`
|
|
199
|
+
- **Wrapper invocation arity.** Both `okstra-codex-exec.sh` and `okstra-antigravity-exec.sh` take three required positional arguments plus three optional ones, matching their usage line verbatim: `<project-root> <model-execution-value> <prompt-path> [worktree-path] [role] [idle-timeout-seconds]`. The fourth (worktree) argument is **mandatory for implementation phase** and optional otherwise — when passed it must be an existing directory (the wrapper hard-fails otherwise). For codex it becomes `--add-dir <worktree>` (sandbox write access); for antigravity it is appended to `--include-directories`. Omitting it during implementation causes the codex sandbox to reject every Edit/Write targeting the worktree with EPERM. Workers extract the path from the `**Worktree:**` / `EXECUTOR_WORKTREE_PATH` / `cwd for every mutating command:` line in the lead prompt. The optional fifth `<role>` is folded into both the caller (worker) pane title `<cli>-<role>` and the sibling trace-pane title `<cli>-<role>-tail` (e.g. `codex-executor` ↔ `codex-executor-tail`; antigravity uses the `agy-` prefix). The selected adapter maps the functional assignment identity to this value; when the value is absent, pass the literal `worker` (the wrapper also defaults to `worker` if the argument is omitted). The optional sixth `<idle-timeout-seconds>` overrides the wrapper's stdout-idle watchdog (default 600 — see "No external timeout on wrapper subagents" below).
|
|
200
200
|
- **Background dispatch + polling contract (Codex / Antigravity wrappers).** Both wrapper subagents MUST start their CLI through the selected adapter's asynchronous execution mapping and await the same handle until it reports terminal completion, capped at 30 minutes (1800s) of wall-clock elapsed time. The adapter's await operation is the wait primitive; do not add a standalone sleep or build shorter-sleep loops to bypass a host constraint. This rule applies in **every phase**. Recording responsibilities:
|
|
201
201
|
- Successful completion: return the wrapper's accumulated stdout from the terminal await result. No log entry.
|
|
202
202
|
- Non-zero `exit_code`: record a `cli-failure` to the run-level error log with the real `exit_code` and observed `duration-ms`.
|
|
203
203
|
- Polling cap reached: perform a one-shot **mtime-grace check** on the wrapper's live log (`<prompt>.log`). If the log was written within the last 90 seconds and grace has not yet been applied, extend the cap from 1800s to 2100s and continue awaiting. Otherwise call the selected adapter's termination mapping, record `cli-failure` with `--exit-code 124 --duration-ms <observed_ms> --message "<wrapper> exceeded polling cap (grace=<applied|not-applied>, last_mtime_age=<n>s)"`, then return the language-specific `*_CLI_TIMEOUT` sentinel.
|
|
204
204
|
- The selected adapter owns runtime-session accounting for the full wrapper window; core retains only the observed start/end event boundaries.
|
|
205
|
-
- **No external timeout on wrapper subagents.** The codex/antigravity wrapper subagent's polling
|
|
205
|
+
- **No external timeout on wrapper subagents.** The codex/antigravity wrapper owns BOTH of its timeout mechanisms, and they are the only two: (1) the subagent's polling cap (30min + optional 5min mtime grace), and (2) the wrapper script's internal stdout-idle watchdog — if the CLI writes nothing to its log for `<idle-timeout-seconds>` (6th positional arg, default 600s), the script TERM/KILLs the CLI and records a `timeout` stage in the status sidecar (`okstra-codex-exec.sh` / `okstra-antigravity-exec.sh`). A long-running but *chatty* CLI is never idle-killed; a silent hang is reaped at the idle cap instead of burning the full 30 minutes. Lead MUST NOT impose a separate `dispatch_worker` timeout, an outer `Bash` wall-clock deadline, or any other mechanism that terminates the subagent before the wrapper's own caps fire. Doing so reproduces the historical failure mode that motivated this rule: Lead aborts the subagent at e.g. 18 minutes, the subagent returns nothing, and Lead classifies the role as "no response" while the underlying CLI was actively working. The caps are calibrated so that, combined with Lead's redispatch policy (see "Lead Redispatch Policy on Result-Missing"), a recoverable single-run failure costs at most ~70 minutes of wall-clock — predictable enough to plan around. If a specific run requires a tighter cap, pass a lower `<idle-timeout-seconds>` or lower the polling cap in the wrapper subagent's polling contract (single source of truth), NOT by layering Lead-side timeouts.
|
|
206
206
|
- `contract-violation` events (C) are recorded by Lead via `okstra error-log append-observed --error-type contract-violation ...` after inspecting worker outputs.
|
|
207
207
|
- Lead's responsibility regarding the sidecar is to dump it to the run-level error log via `okstra error-log append-from-worker` after each worker terminates; Lead does not write into the sidecar.
|
|
208
208
|
|
|
@@ -70,7 +70,7 @@ profile document.
|
|
|
70
70
|
- the same `final-report.md` file is the canonical artifact carried into the next run; the user appends answers inline before rerunning. The preferred turn-around is `scripts/okstra.sh --resume-clarification --task-key <project-id>:<task-group>:<task-id>` (opens the latest report in `$EDITOR`, then auto-reruns the same phase with `--clarification-response` carry-in). The lower-level form `--clarification-response <path>` remains available for scripted runs.
|
|
71
71
|
- if a clarification response was carried in for this run, render the conditional `## 0. Clarification Response Carried In From Previous Run` section (the template's `RENDER_IF` guard activates it), walk every `C-*` row of the prior report's `## 1. Clarification Items` table, reconcile each one against new evidence, and update its `Status` to `resolved` or `obsolete` before issuing the next decision/verdict. When no carry-in path was provided, omit the `## 0.` heading entirely — the validator fails reports that emit an empty Section 0 stub (e.g. "No prior clarification response was provided for this run.").
|
|
72
72
|
- Verdict Card (shared — applies to every final-report regardless of profile):
|
|
73
|
-
- The top-of-report `## Verdict Card` block is mandatory in every final-report. Its `Verdict Token
|
|
73
|
+
- The top-of-report `## Verdict Card` block is mandatory in every final-report. Its `Verdict Token` and `Direction` cells MUST byte-match the corresponding cells in `## 7. Final Verdict` — the card is a non-authoritative index and divergence makes the run `contract-violated`. **Enforced:** `validators/validate-run.py` `_validate_verdict_card_consistency` (token, on the rendered markdown) and `_validate_verdict_card_fields` (direction, on data.json). The `Next Step` cell points at the SAME action as `## 7. Final Verdict` and the first item of `## 3. Recommended Next Steps`, but is written as the actionable command the reader runs (`/okstra-run task-type=release-handoff`) where §7 states it as prose — it is not a byte copy and is not compared.
|
|
74
74
|
- Cross-worker traceability (shared — applies to every analysis worker output and to the lead's `## 6.` / `## 2.` tables in the final-report):
|
|
75
75
|
- **Worker-side item IDs (free-form but unique within the worker).** Every row item in sections 1–5 (and any optional section 6) of an analysis worker's output MUST carry an item ID that is unique within that one worker's result file. The ID convention is the worker's choice — `F-001` / `F-002` per the suggested schema, `1.1` / `1.2` / `1.3` as Codex tends to use, or any other shape — but it MUST appear as the leading column of the row (for table-form items) or as a `[<ID>]` prefix (for bullet/numbered items). Workers that emit findings without IDs make cross-worker reconciliation impossible.
|
|
76
76
|
- **Lead-side ID assignment + source preservation.** When the lead (or `report-writer-worker`) synthesises `## 6.1 Consensus` / `## 6.2 Differences` / `## 2.1 Primary Evidence` rows from worker outputs, the lead assigns a fresh `C-NNN` / `D-NNN` / `E-NNN` row ID. The `Source items` column (or, where the template still calls it `Supporting workers` / `Workers (position)` / `Source`, that same column) MUST list every contributing worker:item pair (e.g. `claude:F-001, codex:1.1, antigravity:F-3`) so a reviewer can trace the synthesised row back to each worker's original wording without re-reading every worker-results file. Bare worker names without item IDs (e.g. `claude, codex, antigravity`) are deprecated for these tables; the validator does not yet fail on them but the readability pass treats it as a contract violation.
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
],
|
|
23
23
|
"implementation-planning": [
|
|
24
24
|
"source code edits of any kind (Edit/Write on project source files is forbidden)",
|
|
25
|
-
"file writes outside the run`s artifact directories (`reports/`, `prompts/`, `state/`, `manifests/`, `worker-results/`, `status/`, `sessions/`); in particular, do not write to `docs/superpowers/specs/` or `docs/superpowers/plans/`",
|
|
25
|
+
"file writes outside the run`s artifact directories (`reports/`, `prompts/`, `state/`, `manifests/`, `worker-results/`, `status/`, `sessions/`) and the task-root qa tree (`<task_root>/qa/` — the Tier3 conformance scripts, manifest, and tsconfig this phase MUST write per the Stage Map conformance contract); in particular, do not write to `docs/superpowers/specs/` or `docs/superpowers/plans/`",
|
|
26
26
|
"executing builds, migrations, deployments, or any state-mutating command",
|
|
27
27
|
"starting `implementation` inside this run (must be a separate run authorised by an approved deliverable from this phase), even if the user says \"다음 단계 진행해\"",
|
|
28
28
|
"dispatching parallel sub-agents beyond the required worker roster (okstra owns worker fan-out)",
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
"dispatching parallel sub-agents beyond the required worker roster",
|
|
40
40
|
"silent scope expansion: every file edited outside the approved plan list MUST appear in the `Out-of-plan edits` block with rationale",
|
|
41
41
|
"leaving placeholders such as TBD / TODO / \"implement later\" / \"handle edge cases\" in newly-added lines of this run (check via `git diff <base>..HEAD | grep -E '^\\+[^+].*\\b(TBD|TODO|FIXME|XXX|implement later|handle edge cases|similar to|placeholder)\\b'`; pre-existing strings in untouched regions are out of scope)",
|
|
42
|
-
"lead substituting its own verdict when every
|
|
42
|
+
"lead substituting its own verdict when every verifier present in the resolved roster (`Claude verifier`, `Codex verifier`, plus `Antigravity verifier` when opted in) returned a non-result terminal status (`timeout`/`error`/`not-run`); in that case the run MUST end as `blocked` with routing recommendation back to `error-analysis`, never with a lead-only verdict",
|
|
43
43
|
"claiming rollback verification on a schema migration, config-format change, or any persisted-state mutation without a recorded dry-run of the rollback step and its captured exit code",
|
|
44
44
|
"declaring overall task acceptance — that is `final-verification` ownership; this phase reports only \"ready for final-verification\" or \"needs new planning loop\"",
|
|
45
45
|
"delegating the self-review pass to a generic subagent — `Claude lead` must run it"
|
|
46
46
|
],
|
|
47
47
|
"final-verification": [
|
|
48
48
|
"source code edits, follow-up bug fixes, or scope expansion",
|
|
49
|
-
"state-mutating commands; only read-only execution of pre-existing test or validation commands
|
|
49
|
+
"state-mutating commands against the project or shared environments; permitted mutations are limited to the run's own `.okstra` artifacts (reports, state, `<task_root>/qa/result-*.json` sidecars, `okstra handoff record-verified` on acceptance) and Tier3 conformance scripts that mutate only their qaEnv replica datastore — everything else is read-only execution of pre-existing test or validation commands",
|
|
50
50
|
"starting any follow-up phase inside this run; record findings and end the run"
|
|
51
51
|
],
|
|
52
52
|
"release-handoff": [
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
{{INCLUDE:_clarification-recommendation.md}}
|
|
67
67
|
- **Evidence note required inside `Statement`**: every clarification row includes `Evidence checked: <path:line>` or `Evidence checked: none — <human-only reason>` in the `Statement` cell. `none` is allowed ONLY when the row's nature is "only a human can answer this" (reporter intent, business priority, organisational decision). A row with `none` that *could* have been answered by the codebase is a defect of this phase, restated from the pre-planning rule above.
|
|
68
68
|
- Section heading contract (BLOCKING — validator scans for these literal English substrings):
|
|
69
|
-
- The final report MUST include section headings containing each of the following exact strings: `Option Candidates`, `Trade-off`, `Recommended Option`, `Stage Map`, `
|
|
69
|
+
- The final report MUST include section headings containing each of the following exact strings — this list mirrors `PLANNING_REQUIRED_SECTIONS` in `validators/validate-run.py`, which is the SSOT: `Option Candidates`, `Trade-off`, `Recommended Option`, `Stage Map`, `Stepwise Execution Order`, `Dependency`, `Validation Checklist`, `Rollback`, `Requirement Coverage`, `Plan Body Verification`, `Cross-Project Dependencies`, `Decision Drafts`. (Approval is no longer a body section — it is the YAML frontmatter `approved` field.) Three further headings are enforced elsewhere, not by that scan: `Stage Exit Contract` and `Stage Validation` are per-stage subsections checked by `validators/validate-implementation-plan-stages.py`, and `Implementation Design Preparation` is enforced and rendered by `schemas/final-report-v1.0.schema.json` plus `templates/reports/final-report.template.md` from its required data block.
|
|
70
70
|
- Korean translations are allowed in parentheses (e.g. `### Recommended Option (Korean gloss)`), but the English keyword must be present verbatim in the heading line.
|
|
71
71
|
- The shape and ordering follow `final-report-template.md` sections 5.4 (`Implementation Plan Deliverables`) + 5.5 (`Stage Map`). `validators/validate-run.py` substring-matches the raw report text, so a Korean-only heading fails the gate — the cause of repeated observed failures.
|
|
72
72
|
- Beyond substring matching, when the Plan Body Verification gate result is `passed` / `passed-with-dissent`, `validators/validate-run.py` runs the **structural** Stage Map validator (`validators/validate-implementation-plan-stages.py`) at the planning boundary — not deferred to the `implementation` entry gate. It enforces: the exact `## 5.5 Stage Map` heading, each `## 5.5.<i> Stage <i>:` section with its four required subsections, the per-stage effective step count (≤8), the `depends-on` DAG, and the per-stage vertical-slice contract (S10). S10 scans for the literal in-section strings `Slice value:`, `Acceptance:`, the three `Test case (success):` / `Test case (boundary):` / `Test case (failure):` lines (S10d), and the Stepwise `action`-cell prefixes `RED:` / `GREEN:` (or a `TDD exemption:` line, which waives both the test-case lines and the RED/GREEN check) — keep these tokens verbatim for the same reason as the heading keywords above.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
- Executor provider: `{{EXECUTOR_PROVIDER}}` (one of: `claude` | `codex` | `antigravity`; chosen via `--executor` or `OKSTRA_DEFAULT_EXECUTOR`, default `claude`)
|
|
15
15
|
- Executor subagent for dispatch: `{{EXECUTOR_WORKER_AGENT}}`
|
|
16
16
|
- Executor model: `{{EXECUTOR_MODEL_DISPLAY}}` (launch value: `{{EXECUTOR_MODEL_EXECUTION_VALUE}}`)
|
|
17
|
-
- Wherever this profile mentions the `Executor`, it refers to the role bound above.
|
|
17
|
+
- Wherever this profile mentions the `Executor`, it refers to the role bound above. **Every** analysis provider in the resolved roster is also dispatched as a verifier — including the executor's own provider, which runs *separately* as a fresh session with no shared context so no verdict comes from the session that wrote the diff (`_implementation-verifier.md` owns this rule). Verifier dispatches remain strictly read-only.
|
|
18
18
|
{{INCLUDE:_common-contract.md}}
|
|
19
19
|
{{INCLUDE:_stage-discipline.md}}
|
|
20
20
|
- Pre-implementation gate (mandatory — refuse to start if any item fails):
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
- Base ref: `{{EXECUTOR_WORKTREE_BASE_REF}}` — canonical `<base>` for every `git diff` / `git log` in this run. Independent stages start from the common anchor (the task-key worktree HEAD, fixed once at first stage entry); dependent stages start from the predecessor done commit or the verified merged task worktree head.
|
|
30
30
|
- Provisioning note: `{{EXECUTOR_WORKTREE_NOTE}}`
|
|
31
31
|
- Treat the working-tree path as `project_root` for the duration of this run. Do NOT mutate the caller's original checkout. cwd-sensitive Bash commands MUST be prefixed `cd {{EXECUTOR_WORKTREE_PATH}} && ` in the same Bash invocation (never `bash -lc "..."` wrappers — see executor sidecar for full rules).
|
|
32
|
-
- Lifecycle: kept after the run completes as this stage's evidence worktree. Later stages get their own stage worktrees. Manual cleanup: `git worktree remove <path>` → `git branch -D <branch>` → drop the stage-key registry entry (`<task-key>#stage-<N>`).
|
|
32
|
+
- Lifecycle: kept after the run completes as this stage's evidence worktree. Later stages get their own stage worktrees. Manual cleanup: `git worktree remove <path>` → `git branch -D <branch>` → drop the stage-key registry entry (`<task-key>#stage-<N>`). Exception: whole-task `final-verification` auto-merges every done stage and tears down its worktree/branch/registry key (`stage_integrate.integrate_stages`, `teardown=True`) — do not promise the user the worktree survives past that point.
|
|
33
33
|
- Approval gate (phase-specific addendum to shared authority rule):
|
|
34
34
|
- the pre-implementation gate's recorded user approval marker is the only authorised approval gate at this phase — proceed once it is satisfied without further external coordination.
|
|
35
35
|
- Forbidden actions — universal (any occurrence → terminal status `contract-violated`):
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
- antigravity
|
|
8
8
|
- report-writer
|
|
9
9
|
- Optional workers (opt-in via `--workers`):
|
|
10
|
-
- none —
|
|
10
|
+
- none — the block above is the full roster. As everywhere, `--workers` may narrow within it (`okstra_ctl.workers.validate_workers_against_profile` enforces the allowlist only, minimum 1), but narrowing this phase is strongly discouraged: cross-worker lens diversity is its load-bearing value, and a reduced roster produces candidates no second worker ever challenged.
|
|
11
11
|
{{INCLUDE:_common-contract.md}}
|
|
12
12
|
- Brief consumption (phase-specific addendum — shared rules live in `_common-contract.md` under "Brief handoff contract"):
|
|
13
13
|
- this phase REQUIRES a codebase-scan brief whose frontmatter contains `scope: codebase`. A brief without that marker is rejected before worker dispatch.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Compact path-hint persistence and legacy context hydration."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import re
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Any, Mapping
|
|
6
7
|
|
|
@@ -370,7 +371,15 @@ def _run_path_set(
|
|
|
370
371
|
"run_sessions": run_dir / "sessions",
|
|
371
372
|
"run_logs": run_dir / "logs",
|
|
372
373
|
"worker_results": worker_results,
|
|
373
|
-
|
|
374
|
+
# Carry is implementation-only and stage-shared (flat under the
|
|
375
|
+
# task-type run dir), so strip a `stage-<N>` leaf before anchoring it —
|
|
376
|
+
# mirrors paths.py `run_carry`.
|
|
377
|
+
"run_carry": (
|
|
378
|
+
(run_dir.parent if re.fullmatch(r"stage-\d+", run_dir.name) else run_dir)
|
|
379
|
+
/ "carry"
|
|
380
|
+
if task_type_segment == "implementation"
|
|
381
|
+
else run_dir / "carry"
|
|
382
|
+
),
|
|
374
383
|
**_run_files(run_dir, run_prompts, worker_results, task_type_segment, sequences, suffixes),
|
|
375
384
|
}
|
|
376
385
|
|
|
@@ -188,7 +188,18 @@ def compute_run_paths(
|
|
|
188
188
|
run_sessions = run_dir / "sessions"
|
|
189
189
|
run_logs = run_dir / "logs"
|
|
190
190
|
worker_results = run_dir / "worker-results"
|
|
191
|
-
|
|
191
|
+
# The carry sidecar ledger is implementation-only and stage-SHARED, like
|
|
192
|
+
# consumers.jsonl: every reader/writer (consumers.py,
|
|
193
|
+
# implementation_outcome.py, design_prep.py, the deliverable profile) uses
|
|
194
|
+
# the flat `runs/implementation/carry/`, because stage N+1 must find stage
|
|
195
|
+
# N's sidecar without knowing N's run layout. Never stage-scope it. Other
|
|
196
|
+
# task types keep the (unused) run-local spelling so their run dirs don't
|
|
197
|
+
# grow a stray task-type-level carry/.
|
|
198
|
+
run_carry = (
|
|
199
|
+
runs_dir / task_type_segment / "carry"
|
|
200
|
+
if task_type_segment == "implementation"
|
|
201
|
+
else run_dir / "carry"
|
|
202
|
+
)
|
|
192
203
|
|
|
193
204
|
if run_seq_override is not None:
|
|
194
205
|
seq_int = int(run_seq_override)
|
|
@@ -109,6 +109,11 @@ def validate_workers_against_profile(
|
|
|
109
109
|
"""프로파일이 `Required workers:` 로 로스터를 선언했다면, 사용자
|
|
110
110
|
override 가 (required ∪ optional) 의 부분집합인지 검증한다.
|
|
111
111
|
|
|
112
|
+
`Required workers:` 는 이 시스템에서 **allowlist + 기본 로스터**다 —
|
|
113
|
+
전원 필수 집합이 아니다. 사용자는 그 안에서 좁혀 고를 수 있고(최소 1),
|
|
114
|
+
wizard 는 분석 워커만 고르게 한 뒤 report-writer 를 뒤에서 강제
|
|
115
|
+
추가하므로 누락 검증을 여기서 하면 그 흐름이 깨진다.
|
|
116
|
+
|
|
112
117
|
`profile_workers` 가 비어 있으면(프로파일이 로스터를 선언하지 않은
|
|
113
118
|
구버전) 검증을 건너뛴다.
|
|
114
119
|
"""
|
|
@@ -83,7 +83,7 @@ PHASE_RULES: dict[str, dict[str, str]] = {
|
|
|
83
83
|
" - `Out-of-plan edits` block listing every file touched outside the approved plan with rationale (empty block preferred)\n"
|
|
84
84
|
' - validation evidence: actual stdout/stderr and exit code for every pre / mid / post command from the plan (no paraphrased "tests pass")\n'
|
|
85
85
|
" - TDD evidence for TDD-applicable steps: failing-test output before implementation commit and passing-test output after, with framing SHAs\n"
|
|
86
|
-
" - per-verifier sections (`
|
|
86
|
+
" - per-verifier sections for every verifier in the resolved roster (`Claude verifier`, `Codex verifier`, plus `Antigravity verifier` when opted in) with independent verdict (PASS / CONCERNS / FAIL) and cited diff snippets; dissent is preserved by `Claude lead`\n"
|
|
87
87
|
" - rollback verification (revert SHA reachable, feature flag toggle works, migration down step valid; dry-run preferred)\n"
|
|
88
88
|
" - routing recommendation for `final-verification` (ready / needs new error-analysis or planning loop)"
|
|
89
89
|
),
|
|
@@ -308,6 +308,17 @@
|
|
|
308
308
|
}
|
|
309
309
|
]
|
|
310
310
|
},
|
|
311
|
+
"criticGaps": {
|
|
312
|
+
"type": "object",
|
|
313
|
+
"description": "Coverage-critic gap accounting. `proposed - merged - unverified` is the count the voters judged and rejected, so a gap that vanished shows up as a mismatch instead of as nothing. Every `unverified` gap needs a matching `missingInformation` row with source `critic-unverified` (prompts/lead/convergence.md \"Gap verification\").",
|
|
314
|
+
"required": ["proposed", "merged", "unverified"],
|
|
315
|
+
"additionalProperties": false,
|
|
316
|
+
"properties": {
|
|
317
|
+
"proposed": { "type": "integer", "minimum": 0 },
|
|
318
|
+
"merged": { "type": "integer", "minimum": 0 },
|
|
319
|
+
"unverified": { "type": "integer", "minimum": 0 }
|
|
320
|
+
}
|
|
321
|
+
},
|
|
311
322
|
"consensus": {
|
|
312
323
|
"type": "array",
|
|
313
324
|
"items": { "$ref": "#/$defs/ConsensusRow" }
|
|
@@ -507,7 +518,7 @@
|
|
|
507
518
|
"required": ["h1", "h2b", "h3"],
|
|
508
519
|
"additionalProperties": false,
|
|
509
520
|
"properties": {
|
|
510
|
-
"h1": { "enum": ["local
|
|
521
|
+
"h1": { "enum": ["local checkout", "push + PR", "skip"] },
|
|
511
522
|
"h2": { "type": "string" },
|
|
512
523
|
"h2b": {
|
|
513
524
|
"enum": ["not-run", "clean", "proceed anyway", "change base branch", "cancel"]
|
|
@@ -878,9 +878,13 @@ is set (Step 6.5), and **before** printing the Step 7 hand-off message, you
|
|
|
878
878
|
MUST validate every brief produced this run. The validator enforces the
|
|
879
879
|
labelled-handoff contract — required frontmatter keys, Augmentation labels,
|
|
880
880
|
`intent-inference ↔ intent-check:` auto-mirroring, `parent-id`/`depth`
|
|
881
|
-
consistency, `reporter-confirmations` markers,
|
|
882
|
-
|
|
883
|
-
`
|
|
881
|
+
consistency, `reporter-confirmations` markers, the variant-required sections
|
|
882
|
+
from "Required sections by variant" (`Source Material` / `Problem / Symptom`
|
|
883
|
+
off-codebase, plus `Context` / `Desired Outcome` / `Acceptance Criteria` /
|
|
884
|
+
`External Gates` / `Constraints` / `Related Artifacts` / `Related Task Graph`
|
|
885
|
+
/ `Open Questions` everywhere — each present with a non-blank body, `_(none)_`
|
|
886
|
+
allowed), `Related Task Graph` table shape/relation values, and the
|
|
887
|
+
codebase-scan `scope`/`Scan Scope`/`Priority Lenses` rules.
|
|
884
888
|
|
|
885
889
|
1. Run the validator over the briefs directory:
|
|
886
890
|
|
|
@@ -898,10 +902,14 @@ table shape/relation values, and the codebase-scan `scope`/`Scan Scope`/
|
|
|
898
902
|
confirm each brief has all ten required frontmatter keys (`type`,
|
|
899
903
|
`brief-id`, `parent-id`, `ticket-id`, `source-type`, `task-group`,
|
|
900
904
|
`depth`, `created`, `generator`, `reporter-confirmations`); every
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
+
section the "Required sections by variant" table marks Required for the
|
|
906
|
+
brief's variant is present with a non-blank body (`_(none)_` allowed) —
|
|
907
|
+
in particular `Desired Outcome`, `Acceptance Criteria`, `External Gates`,
|
|
908
|
+
`Related Artifacts`, and `Related Task Graph`, the ones authors drop most;
|
|
909
|
+
every Augmentation entry carries one of the four labels; every
|
|
910
|
+
`intent-inference` has a paired `intent-check:` row; `parent-id` is `self`
|
|
911
|
+
at depth 0 and a real parent id below; and for `scope: codebase` briefs,
|
|
912
|
+
`Scan Scope` is non-empty and `Priority Lenses` lists 1–4 whitelist lenses.
|
|
905
913
|
|
|
906
914
|
## Step 7: Hand off
|
|
907
915
|
|
|
@@ -758,15 +758,9 @@ Single scan of every agreed check that this run did NOT confirm — requirement
|
|
|
758
758
|
{%- endif %}
|
|
759
759
|
|
|
760
760
|
{% endif %}
|
|
761
|
-
{
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
> Lens whitelist and column schema enforced by `validators/validate_improvement_report.py`. Row count is bounded by the brief's `candidate-cap` (default 8, absolute max 12).
|
|
765
|
-
|
|
766
|
-
| Cand ID | Lens | Title | Scope | Severity | Effort | Consensus | Source workers | Recommended next-phase | Evidence |
|
|
767
|
-
|---------|------|-------|-------|----------|--------|-----------|----------------|------------------------|----------|
|
|
768
|
-
|
|
769
|
-
{% endif %}
|
|
761
|
+
{# `improvement-discovery` is not in the data.json TaskType enum — its
|
|
762
|
+
`## 5.9 Improvement Candidates` report is authored directly by the report
|
|
763
|
+
writer (report-writer-worker.md), never rendered through this template. #}
|
|
770
764
|
{% if fixCycle and fixCycle.cycle %}
|
|
771
765
|
## 5.10 Fix History
|
|
772
766
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
},
|
|
6
6
|
"emptyState": {
|
|
7
7
|
"consensusItems": "- No consensus items.",
|
|
8
|
-
"differences": "- No significant differences.
|
|
8
|
+
"differences": "- No significant differences. 6.1 Consensus stands as-is.",
|
|
9
9
|
"primaryEvidence": "- No primary evidence.",
|
|
10
10
|
"secondaryEvidence": "- No secondary evidence or alternate interpretations.",
|
|
11
11
|
"risks": "- No missing information or risks.",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
},
|
|
6
6
|
"emptyState": {
|
|
7
7
|
"consensusItems": "- 합의 항목 없음.",
|
|
8
|
-
"differences": "- 유의미한 차이 없음.
|
|
8
|
+
"differences": "- 유의미한 차이 없음. 6.1 Consensus 가 그대로 유효합니다.",
|
|
9
9
|
"primaryEvidence": "- 주 증거 없음.",
|
|
10
10
|
"secondaryEvidence": "- 보조 증거 또는 대안 해석 없음.",
|
|
11
11
|
"risks": "- 누락된 정보·위험 없음.",
|
|
@@ -102,6 +102,8 @@ Every worker prompt MUST begin with these anchor headers, in this exact order, b
|
|
|
102
102
|
7. `**Errors log path:** <absolute-path>` — run-level JSONL (see Error reporting above).
|
|
103
103
|
8. `**Errors sidecar path:** <absolute-path>` — per-worker JSON (see Error reporting above).
|
|
104
104
|
|
|
105
|
+
**Exception — reverify dispatches.** A Phase 5.5 re-verification prompt (its `**Prompt History Path:**` carries a `-reverify-r<N>-` segment) deliberately omits anchors 5 and 6 and adds a `**Model:**` line — lightweight mode reads neither this file nor the preflight pack. The two errors-path anchors are NOT omitted; that gate applies to reverify prompts unchanged. The reverify anchor set is owned by `prompts/lead/convergence.md` "Required reverify-prompt anchor headers".
|
|
106
|
+
|
|
105
107
|
For the **implementation phase** specifically, the dispatched prompt MUST also include:
|
|
106
108
|
|
|
107
109
|
- `**Worktree:** <absolute-path>` — the task worktree path.
|
|
@@ -459,6 +459,48 @@ def check_scope_split_sections(text: str, errors: list[str]) -> None:
|
|
|
459
459
|
)
|
|
460
460
|
|
|
461
461
|
|
|
462
|
+
# The variant-independent rows of the skill's "Required sections by variant"
|
|
463
|
+
# table (okstra-brief-gen SKILL.md). Desired Outcome and the Acceptance
|
|
464
|
+
# Criteria / External Gates split have dedicated checks with richer messages;
|
|
465
|
+
# Scan Scope / Priority Lenses are codebase-only (check_codebase_scope);
|
|
466
|
+
# Source Material / Problem / Symptom are omitted by the codebase variant, so
|
|
467
|
+
# they are enforced only off it.
|
|
468
|
+
ALWAYS_REQUIRED_SECTIONS = (
|
|
469
|
+
"Context",
|
|
470
|
+
"Constraints",
|
|
471
|
+
"Related Artifacts",
|
|
472
|
+
"Related Task Graph",
|
|
473
|
+
"Open Questions",
|
|
474
|
+
)
|
|
475
|
+
NON_CODEBASE_REQUIRED_SECTIONS = ("Source Material", "Problem / Symptom")
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def check_variant_required_sections(
|
|
479
|
+
text: str, scope: str, errors: list[str]
|
|
480
|
+
) -> None:
|
|
481
|
+
"""Presence + non-blank body for the table's remaining Required rows.
|
|
482
|
+
|
|
483
|
+
Downstream phases read these sections by heading; a silently-absent
|
|
484
|
+
heading reads as "the reporter had nothing to say" when the brief was
|
|
485
|
+
simply authored off-template. `_(none)_` stays the explicit empty marker.
|
|
486
|
+
"""
|
|
487
|
+
required = ALWAYS_REQUIRED_SECTIONS
|
|
488
|
+
if scope != "codebase":
|
|
489
|
+
required = required + NON_CODEBASE_REQUIRED_SECTIONS
|
|
490
|
+
for heading in required:
|
|
491
|
+
if not has_section_heading(text, heading):
|
|
492
|
+
errors.append(
|
|
493
|
+
f"required section '## {heading}' is missing "
|
|
494
|
+
"(okstra-brief-gen SKILL.md 'Required sections by variant')"
|
|
495
|
+
)
|
|
496
|
+
continue
|
|
497
|
+
if not section_body(text, heading).strip():
|
|
498
|
+
errors.append(
|
|
499
|
+
f"required section '## {heading}' has an empty body "
|
|
500
|
+
"(use _(none)_ for a deliberately-empty section)"
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
|
|
462
504
|
def check_reporter_confirmations(
|
|
463
505
|
rc_status: str | None, reporter_rows: list[str], errors: list[str]
|
|
464
506
|
) -> None:
|
|
@@ -522,6 +564,7 @@ def validate_brief(path: Path, briefs_root: Path) -> list[str]:
|
|
|
522
564
|
|
|
523
565
|
check_requirement_section(text, errors)
|
|
524
566
|
check_scope_split_sections(text, errors)
|
|
567
|
+
check_variant_required_sections(text, scope, errors)
|
|
525
568
|
|
|
526
569
|
# 2. brief-id matches filename stem
|
|
527
570
|
stem = path.stem
|
|
@@ -1529,6 +1529,32 @@ def _validate_verdict_card_consistency(content: str, failures: list[str]) -> Non
|
|
|
1529
1529
|
)
|
|
1530
1530
|
|
|
1531
1531
|
|
|
1532
|
+
def _validate_verdict_card_fields(data: dict, failures: list[str]) -> None:
|
|
1533
|
+
"""`verdictCard.direction` must byte-match its authoritative home in §7.
|
|
1534
|
+
|
|
1535
|
+
The markdown check above compares only the Verdict Token, so a Card whose
|
|
1536
|
+
Direction contradicted §7 shipped silently. `nextStep` is deliberately NOT
|
|
1537
|
+
compared: every shipped fixture authors the Card's cell as the actionable
|
|
1538
|
+
command (`/okstra-run task-type=release-handoff`) while §7 states the same
|
|
1539
|
+
action as prose ("Proceed to release-handoff."), so a byte-match rule there
|
|
1540
|
+
would reject the reference reports.
|
|
1541
|
+
"""
|
|
1542
|
+
card = data.get("verdictCard")
|
|
1543
|
+
final = data.get("finalVerdict")
|
|
1544
|
+
if not isinstance(card, dict) or not isinstance(final, dict):
|
|
1545
|
+
return
|
|
1546
|
+
card_value = str(card.get("direction") or "").strip()
|
|
1547
|
+
final_value = str(final.get("direction") or "").strip()
|
|
1548
|
+
if not card_value or not final_value or card_value == final_value:
|
|
1549
|
+
return
|
|
1550
|
+
failures.append(
|
|
1551
|
+
f"final-report data.json: verdictCard.direction `{card_value}` does not "
|
|
1552
|
+
f"match finalVerdict.direction `{final_value}` — the Card is a "
|
|
1553
|
+
"non-authoritative index and MUST byte-match §7 "
|
|
1554
|
+
"(_common-contract.md Verdict Card)."
|
|
1555
|
+
)
|
|
1556
|
+
|
|
1557
|
+
|
|
1532
1558
|
def _load_final_report_data(report_path: Path) -> dict:
|
|
1533
1559
|
"""Best-effort parse of the final-report data.json sibling. Returns {} when
|
|
1534
1560
|
absent or unparseable — those conditions are already surfaced as failures by
|
|
@@ -1596,6 +1622,9 @@ def validate_final_report_data(
|
|
|
1596
1622
|
|
|
1597
1623
|
_validate_rationale_evidence(data, failures)
|
|
1598
1624
|
_validate_no_opaque_id_references(data, failures)
|
|
1625
|
+
_validate_verdict_card_fields(data, failures)
|
|
1626
|
+
# Phase-agnostic: the coverage critic runs in every finding-producing phase.
|
|
1627
|
+
_validate_unverified_critic_gaps_recorded(data, failures)
|
|
1599
1628
|
|
|
1600
1629
|
task_type = (data.get("header") or {}).get("taskType")
|
|
1601
1630
|
if task_type == "final-verification":
|
|
@@ -1905,6 +1934,27 @@ _PLAN_ITEM_SOURCES = (
|
|
|
1905
1934
|
)
|
|
1906
1935
|
|
|
1907
1936
|
|
|
1937
|
+
def _plan_item_source_count(planning: dict, field: str) -> int:
|
|
1938
|
+
"""Row count for one plan-body deliverable array.
|
|
1939
|
+
|
|
1940
|
+
`stepwiseExecution` has two homes: the top-level array is the legacy flat
|
|
1941
|
+
summary (schema: "Legacy flat summary kept for compatibility only. New
|
|
1942
|
+
reports use stageMap/stages") and is absent from `implementationPlanning.
|
|
1943
|
+
required`, while a current report carries the real steps per stage. Counting
|
|
1944
|
+
only the legacy field made this whole check a no-op for the largest
|
|
1945
|
+
category — a modern plan could drop every `P-Step-*` item and still pass.
|
|
1946
|
+
"""
|
|
1947
|
+
rows = [r for r in (planning.get(field) or []) if isinstance(r, dict)]
|
|
1948
|
+
if field != "stepwiseExecution":
|
|
1949
|
+
return len(rows)
|
|
1950
|
+
per_stage = sum(
|
|
1951
|
+
len([s for s in (stage.get("stepwiseExecution") or []) if isinstance(s, dict)])
|
|
1952
|
+
for stage in (planning.get("stages") or [])
|
|
1953
|
+
if isinstance(stage, dict)
|
|
1954
|
+
)
|
|
1955
|
+
return per_stage or len(rows)
|
|
1956
|
+
|
|
1957
|
+
|
|
1908
1958
|
def _validate_plan_item_extraction_completeness(
|
|
1909
1959
|
data: dict,
|
|
1910
1960
|
failures: list[str],
|
|
@@ -1932,7 +1982,7 @@ def _validate_plan_item_extraction_completeness(
|
|
|
1932
1982
|
if isinstance(it, dict)
|
|
1933
1983
|
]
|
|
1934
1984
|
for field, prefix, label in _PLAN_ITEM_SOURCES:
|
|
1935
|
-
source_count =
|
|
1985
|
+
source_count = _plan_item_source_count(ip, field)
|
|
1936
1986
|
if source_count == 0:
|
|
1937
1987
|
continue
|
|
1938
1988
|
extracted = len(
|
|
@@ -2465,6 +2515,55 @@ def _validate_self_fix_before_clarification(data: dict, failures: list[str]) ->
|
|
|
2465
2515
|
)
|
|
2466
2516
|
|
|
2467
2517
|
|
|
2518
|
+
CRITIC_UNVERIFIED_SOURCE = "critic-unverified"
|
|
2519
|
+
|
|
2520
|
+
|
|
2521
|
+
def _validate_unverified_critic_gaps_recorded(data: dict, failures: list[str]) -> None:
|
|
2522
|
+
"""A coverage gap nobody judged must survive as a Missing Information row.
|
|
2523
|
+
|
|
2524
|
+
Gap verification drops `contested` / `worker-unique` gaps as hallucinations,
|
|
2525
|
+
which is sound *because voters looked at them*. A gap left unjudged — the
|
|
2526
|
+
verification dispatch timed out, covered only part of the batch, or no
|
|
2527
|
+
eligible analyser voted — has no verdict at all, and treating it as rejected
|
|
2528
|
+
invents one. The failure mode is adversarial: the batch that times out is
|
|
2529
|
+
the batch of gaps too expensive to check, so the highest-risk items are the
|
|
2530
|
+
ones that would silently disappear (prompts/lead/convergence.md
|
|
2531
|
+
"Gap verification").
|
|
2532
|
+
"""
|
|
2533
|
+
cross = data.get("crossVerification")
|
|
2534
|
+
if not isinstance(cross, dict):
|
|
2535
|
+
return
|
|
2536
|
+
critic = cross.get("criticGaps")
|
|
2537
|
+
if not isinstance(critic, dict):
|
|
2538
|
+
return
|
|
2539
|
+
unverified = critic.get("unverified")
|
|
2540
|
+
if not isinstance(unverified, int) or unverified <= 0:
|
|
2541
|
+
return
|
|
2542
|
+
recorded = [
|
|
2543
|
+
row for row in (data.get("missingInformation") or [])
|
|
2544
|
+
if isinstance(row, dict) and row.get("source") == CRITIC_UNVERIFIED_SOURCE
|
|
2545
|
+
]
|
|
2546
|
+
if len(recorded) < unverified:
|
|
2547
|
+
failures.append(
|
|
2548
|
+
"final-report data.json: crossVerification.criticGaps.unverified is "
|
|
2549
|
+
f"{unverified} but only {len(recorded)} missingInformation row(s) carry "
|
|
2550
|
+
f"`source: \"{CRITIC_UNVERIFIED_SOURCE}\"`. A gap the voters never "
|
|
2551
|
+
"judged MUST be recorded as a Missing Information row naming the gap "
|
|
2552
|
+
"and why verification did not complete — dropping it fabricates a "
|
|
2553
|
+
"rejection (prompts/lead/convergence.md \"Gap verification\")."
|
|
2554
|
+
)
|
|
2555
|
+
|
|
2556
|
+
proposed = critic.get("proposed")
|
|
2557
|
+
merged = critic.get("merged")
|
|
2558
|
+
if isinstance(proposed, int) and isinstance(merged, int):
|
|
2559
|
+
if merged + unverified > proposed:
|
|
2560
|
+
failures.append(
|
|
2561
|
+
"final-report data.json: crossVerification.criticGaps has "
|
|
2562
|
+
f"merged({merged}) + unverified({unverified}) > proposed({proposed}) "
|
|
2563
|
+
"— the accounting does not describe a real set of gaps."
|
|
2564
|
+
)
|
|
2565
|
+
|
|
2566
|
+
|
|
2468
2567
|
# Allowed `fixability` values, mirroring the schema enum
|
|
2469
2568
|
# (schemas/final-report-v1.0.schema.json planItems[].verdicts[].fixability).
|
|
2470
2569
|
_FIXABILITY_VALUES = frozenset({"planner-fixable", "needs-user-input"})
|
|
@@ -2990,6 +3089,65 @@ def _validate_forbidden_actions(
|
|
|
2990
3089
|
failures.extend(f"forbidden-action: {v}" for v in violations)
|
|
2991
3090
|
|
|
2992
3091
|
|
|
3092
|
+
CLASSIFICATION_COUNT_KEYS = {
|
|
3093
|
+
"full-consensus": "fullConsensus",
|
|
3094
|
+
"partial-consensus": "partialConsensus",
|
|
3095
|
+
"contested": "contested",
|
|
3096
|
+
"worker-unique": "workerUnique",
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
|
|
3100
|
+
def classification_counts_from_findings(findings) -> dict:
|
|
3101
|
+
"""Recompute `finalClassificationCounts` from `findings[].classification`.
|
|
3102
|
+
|
|
3103
|
+
Shared with `tests/contract/test_convergence_state_contract.py` so the
|
|
3104
|
+
invariant has one definition — it used to live only in that test, which
|
|
3105
|
+
meant it was checked against four hand-written fixtures and never against
|
|
3106
|
+
the state a lead actually writes.
|
|
3107
|
+
"""
|
|
3108
|
+
counts = {value: 0 for value in CLASSIFICATION_COUNT_KEYS.values()}
|
|
3109
|
+
for finding in findings or []:
|
|
3110
|
+
if not isinstance(finding, dict):
|
|
3111
|
+
continue
|
|
3112
|
+
key = CLASSIFICATION_COUNT_KEYS.get(finding.get("classification"))
|
|
3113
|
+
if key:
|
|
3114
|
+
counts[key] += 1
|
|
3115
|
+
return counts
|
|
3116
|
+
|
|
3117
|
+
|
|
3118
|
+
def _validate_convergence_state_counts(run_dir, failures) -> None:
|
|
3119
|
+
"""A run's convergence state must agree with its own findings list.
|
|
3120
|
+
|
|
3121
|
+
`finalClassificationCounts` is a lead-authored summary of
|
|
3122
|
+
`findings[].classification`. Nothing recomputed it against the real
|
|
3123
|
+
artifact, so a miscount survived into the report as two different truths
|
|
3124
|
+
(observed on dev-6901 planning 009: declared partial 6 / worker-unique 4
|
|
3125
|
+
against an actual 5 / 5). Downstream readers pick one arbitrarily.
|
|
3126
|
+
"""
|
|
3127
|
+
from pathlib import Path as _Path
|
|
3128
|
+
|
|
3129
|
+
state_dir = _Path(run_dir) / "state"
|
|
3130
|
+
if not state_dir.is_dir():
|
|
3131
|
+
return
|
|
3132
|
+
for state_path in sorted(state_dir.glob("convergence-*.json")):
|
|
3133
|
+
try:
|
|
3134
|
+
state = json.loads(state_path.read_text(encoding="utf-8"))
|
|
3135
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
3136
|
+
failures.append(f"convergence state {state_path.name} unreadable: {exc}")
|
|
3137
|
+
continue
|
|
3138
|
+
declared = state.get("finalClassificationCounts")
|
|
3139
|
+
if not isinstance(declared, dict):
|
|
3140
|
+
continue
|
|
3141
|
+
actual = classification_counts_from_findings(state.get("findings"))
|
|
3142
|
+
if declared != actual:
|
|
3143
|
+
failures.append(
|
|
3144
|
+
f"convergence state {state_path.name}: finalClassificationCounts "
|
|
3145
|
+
f"{declared} does not match findings[].classification {actual}. "
|
|
3146
|
+
"The counts are a summary of the findings list — a mismatch means "
|
|
3147
|
+
"the run reports two different classifications for the same work."
|
|
3148
|
+
)
|
|
3149
|
+
|
|
3150
|
+
|
|
2993
3151
|
def _validate_requirements_discovery_fanout(run_dir, failures) -> None:
|
|
2994
3152
|
"""requirements-discovery run 에 fan-out/ 이 있으면 packet+index 를 검증해
|
|
2995
3153
|
실패를 ``requirements-discovery: `` 접두로 folding 한다. fan-out 이 없으면 no-op.
|
|
@@ -3427,6 +3585,8 @@ def main() -> int:
|
|
|
3427
3585
|
if task_type == "requirements-discovery":
|
|
3428
3586
|
run_dir = report_path.parent.parent
|
|
3429
3587
|
_validate_requirements_discovery_fanout(run_dir, failures)
|
|
3588
|
+
# Phase-agnostic: convergence runs in every finding-producing phase.
|
|
3589
|
+
_validate_convergence_state_counts(report_path.parent.parent, failures)
|
|
3430
3590
|
validate_report_views(report_path, failures)
|
|
3431
3591
|
|
|
3432
3592
|
validation_status = "passed" if not failures else "failed"
|
|
@@ -115,8 +115,8 @@ CALENDAR_DATE_RE = re.compile(
|
|
|
115
115
|
)
|
|
116
116
|
|
|
117
117
|
DAYS_TOTAL_RE = re.compile(
|
|
118
|
-
r"
|
|
119
|
-
r"\d+(?:\.\d+)?\s*~\s*\d+(?:\.\d+)?\s*days?\s*\(Effort\s
|
|
118
|
+
r"\*\*\d+\s*tasks\s*total\s*/\s*estimated\s*effort:\s*"
|
|
119
|
+
r"\d+(?:\.\d+)?\s*~\s*\d+(?:\.\d+)?\s*days?\s*\(Effort\s*sum\)\*\*"
|
|
120
120
|
)
|
|
121
121
|
|
|
122
122
|
# Opaque cross-document identifiers from internal report formats.
|
|
@@ -288,7 +288,7 @@ def validate(path: Path) -> list[str]:
|
|
|
288
288
|
"report instead"
|
|
289
289
|
)
|
|
290
290
|
|
|
291
|
-
# 10. Days total format inside At a Glance:
|
|
291
|
+
# 10. Days total format inside At a Glance: `**N tasks total / estimated effort: X.X ~ Y.Y days (Effort sum)**`
|
|
292
292
|
if "## At a Glance" in section_positions:
|
|
293
293
|
start = section_positions["## At a Glance"]
|
|
294
294
|
rest = "\n".join(lines[start + 1:])
|
|
@@ -297,7 +297,7 @@ def validate(path: Path) -> list[str]:
|
|
|
297
297
|
if not DAYS_TOTAL_RE.search(body):
|
|
298
298
|
violations.append(
|
|
299
299
|
"At a Glance must contain a totals line matching "
|
|
300
|
-
"
|
|
300
|
+
"`**N tasks total / estimated effort: X.X ~ Y.Y days (Effort sum)**`"
|
|
301
301
|
)
|
|
302
302
|
|
|
303
303
|
# 11. At a Glance row enum check (Effort / Priority / Risk / Phase)
|
|
@@ -375,7 +375,7 @@ def validate(path: Path) -> list[str]:
|
|
|
375
375
|
next_h = re.search(r"^##\s", rest, re.MULTILINE)
|
|
376
376
|
body = (rest[: next_h.start()] if next_h else rest).strip()
|
|
377
377
|
|
|
378
|
-
no_data_marker = "
|
|
378
|
+
no_data_marker = "_none_"
|
|
379
379
|
fence_match = re.search(r"^```([^\n]*)\n(.*?)^```\s*$", body, re.MULTILINE | re.DOTALL)
|
|
380
380
|
|
|
381
381
|
if not body:
|
|
@@ -466,7 +466,7 @@ def validate(path: Path) -> list[str]:
|
|
|
466
466
|
f"per-task block {heading_line!r}: sub-section {label!r} "
|
|
467
467
|
"appears out of required order "
|
|
468
468
|
"(Problem → Solution → Work Breakdown → Verification Commands "
|
|
469
|
-
"→ Rollback
|
|
469
|
+
"→ Rollback)"
|
|
470
470
|
)
|
|
471
471
|
break
|
|
472
472
|
last_pos = pos
|