okstra 0.141.3 → 0.143.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/docs/architecture.md +11 -2
  2. package/docs/cli.md +15 -0
  3. package/docs/for-ai/skills/okstra-setup.md +8 -0
  4. package/docs/project-structure-overview.md +6 -0
  5. package/docs/task-process/error-analysis.md +9 -4
  6. package/package.json +1 -1
  7. package/runtime/BUILD.json +2 -2
  8. package/runtime/agents/workers/report-writer-worker.md +4 -2
  9. package/runtime/prompts/coding-preflight/architectures/hexagonal.md +3 -3
  10. package/runtime/prompts/coding-preflight/overview.md +1 -1
  11. package/runtime/prompts/lead/adapters/claude-code.md +2 -2
  12. package/runtime/prompts/lead/context-loader.md +2 -2
  13. package/runtime/prompts/lead/convergence.md +5 -2
  14. package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
  15. package/runtime/prompts/lead/plan-body-verification.md +20 -9
  16. package/runtime/prompts/lead/report-writer.md +4 -3
  17. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -0
  18. package/runtime/prompts/profiles/_common-contract.md +3 -1
  19. package/runtime/prompts/profiles/_implementation-deliverable.md +3 -3
  20. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  21. package/runtime/prompts/profiles/_implementation-verifier.md +2 -1
  22. package/runtime/prompts/profiles/error-analysis.md +5 -1
  23. package/runtime/prompts/profiles/forbidden-actions.json +0 -1
  24. package/runtime/prompts/profiles/implementation-planning.md +7 -2
  25. package/runtime/prompts/profiles/requirements-discovery.md +7 -0
  26. package/runtime/python/okstra_ctl/analysis_packet.py +28 -3
  27. package/runtime/python/okstra_ctl/brief_frontmatter.py +56 -0
  28. package/runtime/python/okstra_ctl/clarification_items.py +99 -5
  29. package/runtime/python/okstra_ctl/convergence_engine.py +66 -15
  30. package/runtime/python/okstra_ctl/paths.py +34 -7
  31. package/runtime/python/okstra_ctl/phase_cleanup.py +235 -0
  32. package/runtime/python/okstra_ctl/plan_items.py +38 -0
  33. package/runtime/python/okstra_ctl/run.py +81 -33
  34. package/runtime/python/okstra_ctl/schema_excerpt.py +5 -3
  35. package/runtime/python/okstra_ctl/wizard.py +18 -44
  36. package/runtime/python/okstra_ctl/worker_heartbeat.py +15 -5
  37. package/runtime/python/okstra_ctl/workflow.py +1 -1
  38. package/runtime/python/okstra_project/resolver.py +25 -0
  39. package/runtime/schemas/final-report-v1.0.schema.json +162 -4
  40. package/runtime/skills/okstra-run/SKILL.md +3 -1
  41. package/runtime/skills/okstra-setup/SKILL.md +3 -0
  42. package/runtime/skills/okstra-setup/references/project-config.md +47 -0
  43. package/runtime/templates/reports/final-report.template.md +51 -0
  44. package/runtime/templates/reports/i18n/en.json +32 -3
  45. package/runtime/templates/reports/i18n/ko.json +32 -3
  46. package/runtime/templates/reports/implementation-input.template.md +1 -2
  47. package/runtime/templates/reports/task-brief.template.md +1 -1
  48. package/runtime/validators/validate-brief.py +5 -1
  49. package/runtime/validators/validate-run.py +430 -48
  50. package/src/cli-registry.mjs +10 -0
  51. package/src/commands/execute/phase-cleanup.mjs +38 -0
@@ -189,7 +189,7 @@ Task identity, paths, and workflow state are not stored in per-process environme
189
189
 
190
190
  | Data | Authoritative file |
191
191
  |---|---|
192
- | projectId, projectRoot | `<PROJECT_ROOT>/.okstra/project.json` |
192
+ | projectId, projectRoot, declared `architecture.style` | `<PROJECT_ROOT>/.okstra/project.json` |
193
193
  | task identity / workflow | `<task-root>/task-manifest.json` |
194
194
  | task candidate list | `<PROJECT_ROOT>/.okstra/discovery/task-catalog.json` |
195
195
  | latest task pointer | `<PROJECT_ROOT>/.okstra/discovery/latest-task.json` |
@@ -373,7 +373,8 @@ If all three fail, `okstra.sh` exits immediately with an error (there is no auto
373
373
  "projectRoot": "/Volumes/Workspaces/workspace/projects/sample-project",
374
374
  "createdAt": "2026-05-10T00:00:00Z",
375
375
  "updatedAt": "2026-05-10T00:00:00Z",
376
- "worktreeSyncDirs": [".project-docs", ".scratch", "graphify-out", ".claude"]
376
+ "worktreeSyncDirs": [".project-docs", ".scratch", "graphify-out", ".claude"],
377
+ "architecture": { "style": "hexagonal" }
377
378
  }
378
379
  ```
379
380
 
@@ -381,6 +382,14 @@ On the first run, it writes the four fields `projectId`, `projectRoot`, `created
381
382
 
382
383
  `worktreeSyncDirs` (optional) overrides, per project, the list of project-root-relative directories to symlink into task worktrees. Resolution order is the `OKSTRA_WORKTREE_SYNC_DIRS` environment variable → `project.json` → built-in default (`.project-docs`, `.scratch`, `graphify-out`, `.claude`). An empty array disables syncing entirely. Sync exists only for filesystem continuity; the okstra context/write boundary remains `<PROJECT_ROOT>/.okstra/**`.
383
384
 
385
+ `architecture.style` (optional, one of `hexagonal` / `layered` / `none`, default `none`) declares the project's architecture. It is read by [`scripts/okstra_project/resolver.py`](../scripts/okstra_project/resolver.py) `resolve_architecture`, which falls back to `none` on an absent field, an unrecognized value, or an unreadable `project.json`. It is the switch for the second of two enforcement layers.
386
+
387
+ **Layer 1 — always on, style-agnostic.** Independent of any declaration, every `implementation-planning` plan emits `variationPointAnalysis`: whether the same behavior is served by two or more implementations, and for each such point the interface it is extracted behind (`extractionDecision`) plus the Stage Map stage that builds it. `hasMultipleImplementations: false` is a claim rather than an omission, so it requires a written `noVariationRationale` and an empty `points` array. The recommended option carries `testSeams` — one row per boundary a test injects at and replaces — and each point becomes a `P-Var-<N>` item judged in the plan-body verification gate (§5.5.9), which DISAGREEs when an extraction branches on resource identity instead of extracting the interface the next implementation plugs into (the open/closed shape). **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.VariationPointAnalysis` pins the shape, [`validators/validate-run.py`](../validators/validate-run.py) `_validate_variation_point_analysis` rejects a rationale-less or points-carrying `false`, a point-less `true`, and an `extract: true` decision with an empty `interfaceKind` or `coveredBy`, and `okstra_ctl.plan_items` emits the `P-Var-*` items.
388
+
389
+ **Layer 2 — only when a style is declared.** A declared style promotes the placement rules from advisory to binding at three sites. `hexagonal`: extraction itself is not made mandatory, but when a point *is* extracted the interface has to be a port — an `extractionDecision` carrying `extract: true` with any `interfaceKind` other than `"port"` is a validator failure in the same `_validate_variation_point_analysis`, while `extract: false` remains a legal decision the style does not reject; the executor loads `prompts/coding-preflight/architectures/hexagonal.md` even when none of the router's Stage 3 layout signals matched, so the declaration, not the directory shape, decides; and the verifier promotes an added or modified service dependency injecting a concrete adapter instead of a port from an advisory recommendation to a blocking finding → verdict `FAIL` ([`prompts/profiles/_coding-conventions-preflight.md`](../prompts/profiles/_coding-conventions-preflight.md), [`_implementation-verifier.md`](../prompts/profiles/_implementation-verifier.md)). `layered` has no pack resource; its binding invariant is dependency direction — an upper layer may import a lower one, never the reverse — and it is worker judgement, because no machine check reads layer names.
390
+
391
+ `none` (or an absent field) keeps layer 1 only: the placement overlay stays advisory and Stage 3 stays detection-driven, so an already-configured project's behavior does not change until it opts in.
392
+
384
393
  The authoritative source for `okstra-ctl` reindex/backfill also changed under the new model. Previously it sourced `examples/projects/*.conf.sh`; it now scans `~/.okstra/projects/<projectId>/meta.json` (the mirror of the project.json information produced by record_start) to restore (projectId, projectRoot) mappings. The `OKSTRA_PROJECT_DEFINITION_DIR_OVERRIDE` environment variable has also been retired.
385
394
 
386
395
  ## Artifact-home rule
package/docs/cli.md CHANGED
@@ -267,6 +267,20 @@ On the first run from the resolved PROJECT_ROOT, `<PROJECT_ROOT>/.okstra/project
267
267
 
268
268
  If the file already exists, okstra checks that `projectId` matches and updates only `projectRoot` and `updatedAt`. A mismatched `projectId` exits immediately, preventing two IDs from being used in the same directory.
269
269
 
270
+ User-added fields are preserved across that upsert, so optional settings can be hand-edited into the file. One of them is `architecture.style` — the project's declared architecture, one of `hexagonal`, `layered`, or `none` (default `none` when the field is absent, unrecognized, or unreadable):
271
+
272
+ ```json
273
+ {
274
+ "projectId": "<--project-id argument value>",
275
+ "projectRoot": "<resolved absolute path>",
276
+ "createdAt": "<ISO8601 UTC>",
277
+ "updatedAt": "<ISO8601 UTC>",
278
+ "architecture": { "style": "hexagonal" }
279
+ }
280
+ ```
281
+
282
+ Declaring a style promotes that architecture's placement rules from advisory to binding. Under `hexagonal`: a variation point that `implementation-planning` decides to extract has to be extracted behind a port (`interfaceKind: "port"` — validator-enforced; deciding *not* to extract a point stays legal, the style does not force extraction), the implementation executor loads `architectures/hexagonal.md` even when directory-shape detection did not match it, and the verifier grades a placement violation as a blocking `FAIL` rather than a recommendation. `layered` has no preflight pack resource; its binding invariant is dependency direction — an upper layer may import a lower one, never the reverse — and a reverse import is a blocking placement violation found by worker judgement, since no machine check reads layer names. Leaving the field out changes nothing — the style-agnostic planning rules (variation-point analysis and test seams) apply either way. See [`architecture.md`](architecture.md) § Project self-registration for the full two-layer model.
283
+
270
284
  Example:
271
285
 
272
286
  ```bash
@@ -645,6 +659,7 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
645
659
  | `okstra convergence example --kind <groups\|round-results\|critic-results>` | Print one deterministic valid input example as JSON |
646
660
  | `okstra plan-items extract --data <data.json> --output <items.json>` | Deterministically extract the complete implementation-planning `P-*` queue from report-writer data.json |
647
661
  | `okstra plan-items validate --data <data.json> --items <items.json>` | Require the persisted `P-*` queue to match a fresh deterministic extraction exactly |
662
+ | `okstra phase-cleanup --project-root <dir> [--task-key <k>] [--run-dir <dir>] [--fallback-team <label>] [--json]` | Reclaim the resources the previous phase or worker batch finished with, so the next phase does not inherit them. It is tmux-aware: inside a tmux pane it reclaims the prior run's **completed** worker panes through `okstra-trace-cleanup.sh --reclaim-completed`, and outside tmux there are no panes, so it skips pane reclaim entirely. Either way it reconciles teammates through `okstra-team-reconcile.sh` and prints the dismissible teammate names for the lead to shut down — it names them, it never dismisses them itself. Only completed resources are touched: the lead pane and any in-flight worker are preserved. `--project-root` is required and is enough on its own for the teammate half. `--fallback-team <label>` passes the live team label (`session-<lead-session-prefix>`) that the reconcile falls back to when the live session directory is gone: Claude Code re-issues the session id on resume or compaction, and without the label the roster resolves to nothing and the dismissible-teammate list comes back empty — so every caller inside a run should pass it. The prior run is located by `--run-dir` when given, otherwise auto-discovered from `--task-key` — auto-discovery walks both flat `runs/<type>/reports/` and staged `runs/<type>/stage-N/reports/` (`implementation` / `final-verification`), so a staged prior run is found without `--run-dir`; pass `--run-dir` only to override the discovery with a specific run. Output is the `mode` / `panes-reclaimed` / `dismissible-teammates` triple, or the same values as JSON under `--json`. A cleanup failure never blocks the next phase: a missing script, a failed helper, or an undiscoverable prior run still exits 0 (only a malformed invocation exits non-zero) |
648
663
  | `okstra config <get\|set\|unset\|show> [key] [value] [--scope project\|global\|all]` | Manage persistent settings such as `pr-template-path` with atomic JSON writes |
649
664
  | `okstra memory <add\|list\|search\|show\|archive>` | Manage global conversation memory in `~/.okstra/memory-book`, a user-home store separate from project `.okstra/` and the CLI basis of the `save this in okstra` natural-language skill |
650
665
  | `okstra manager <init\|discover-projects\|new\|task>` | Public CLI for grouping cross-project okstra tasks into manager-owned context. `new project`, `new task-group`, and `new task` create manager plans; `task assign`, `task note`, `task sync`, `task status`, and `task run` manage per-project assignments and snapshots. `new project --project-root` accepts only existing directories and performs setup-equivalent registration only if `.okstra/project.json` is absent. Public documentation uses the full `project-id:task-group:task-id` child task key; when child task IDs differ within the same manager task, select the exact child with `--child-task-id`. `task run` does not execute the child lead directly; it returns `prepared` launch metadata/event and a child launch-context packet as JSON |
@@ -105,6 +105,14 @@ Optional settings:
105
105
  non-PASS.
106
106
  - PR body template: `okstra config set pr-template-path "<path>" --scope project|global`
107
107
  - final report language: `okstra config set report-language <en|ko|auto> --scope project`
108
+ - `architecture.style`: the project's declared architecture — `hexagonal`,
109
+ `layered`, or `none` (default `none` when absent, unrecognized, or
110
+ unreadable). `okstra setup` never writes it; hand-add it to `project.json`
111
+ and the upsert preserves it. Declaring a style promotes that architecture's
112
+ placement rules from advisory to a binding planning + verification
113
+ constraint — under `hexagonal` an extracted variation point must be a port,
114
+ under `layered` the dependency direction is worker-judged with no machine
115
+ check. See section F of `references/project-config.md`.
108
116
 
109
117
  If `qaCommands.cmd` contains a token implying mutation, the verifier refuses it. The actual authority for the deny-list is `scripts/okstra_ctl/qa_commands.py`.
110
118
 
@@ -171,6 +171,7 @@ Runtime/install asset changes follow this checklist:
171
171
  | `git-reconcile` | `src/commands/execute/git-reconcile.mjs` | Reconcile stale stage SHAs after external git history changes |
172
172
  | `handoff` | `src/commands/execute/handoff.mjs` | Stage-group release-handoff eligibility / assemble / record helpers |
173
173
  | `integrate-stages` | `src/commands/execute/integrate-stages.mjs` | Merge verified stages into the task worktree and clean stage worktrees |
174
+ | `phase-cleanup` | `src/commands/execute/phase-cleanup.mjs` | Reclaim the prior phase/batch's completed panes and teammates before the next phase starts; tmux-aware, and it preserves the lead pane and in-flight workers (Python: `okstra_ctl.phase_cleanup`) |
174
175
  | `task-list`, `task-show` | `src/commands/inspect/task-list.mjs`, `src/commands/inspect/task-show.mjs` | Task/run introspection for skills; `task-show` consumes the Python task read-side snapshot |
175
176
  | `resolve-task-key` | `src/commands/inspect/resolve-task-key.mjs` | Resolve a bare task-id to candidate task-keys from the project catalog |
176
177
  | `set-work-status` | `src/commands/inspect/set-work-status.mjs` | Set a task's user-managed `workStatus` in task-manifest.json (Python: `okstra_ctl.set_work_status`) |
@@ -240,6 +241,9 @@ Important modules:
240
241
  | `design_prep.py` | fingerprint / materialize / resolve backend for design-preparation requests (CLI: `okstra design-prep <list\|show\|write>`) — computes an assessment fingerprint from the approved planning snapshot's `ASSESSMENT_FIELDS`, idempotently writes an Okstra-owned request under `design-prep-requests/`, and resolves the highest-revision append-only user response under `design-prep-inputs/` whose fingerprint matches as the effective response. Keeps the three authorities (report snapshot / Okstra request / user input) separate and never modifies the report or existing revisions. Sidecar I/O is protected by a directory-fd anchor + flock |
241
242
  | `incremental_scope.py` | incremental re-verification decision for an `implementation-planning` clarification re-run (deterministic pure function) — reads the dependency graph from the previous run data.json's `implementationPlanning.stageMap` and returns `mode="incremental"` only when the base-ref SHA is unchanged and the affected stages' `downstream_stage_closure` is at most half of all stages. CLI: `okstra incremental-scope` |
242
243
  | `incremental_carry.py` | carry merge for an incremental re-run — merges the previous run's plan-item verdicts that this run does not re-verify into the current data.json with a `carriedForwardFromSeq` tag. On `schemaVersion` drift it exits non-zero with `CarryError` to force a full fallback. CLI: `okstra incremental-carry` |
244
+ | `build_tools.py` | allowlist SSOT for deciding whether a plan's command cell invokes the project build toolchain (`npm`/`pytest`/`cargo`/`gradle`/… behind transparent leaders like `sudo`/`env`). The planning worktree has no dependencies installed, so `validators/validate-run.py` uses this to warn (advisory) when a toolchain stage declares no install precondition. Intentionally an allowlist, not a denylist, so unknown tokens go undetected rather than firing on `grep`/`sed` in every plan |
245
+ | `stage_citations.py` | shared grammar SSOT for reading the Stage Map stage numbers a prose cell cites (`Stages 1, 2, and 3`, ranges, etc.). One definition serves two readers that must not drift — the coverage check in `validators/validate-run.py` proving every stage traces to a requirement, and `incremental_scope.py`'s back-trace resolving which stages an answered clarification touches |
246
+ | `self_mock_signals.py` | self-mock signal SSOT — language-keyed regexes (`SIGNALS`), the `EXT_TO_LANG` extension map, and `selfmock_path_key` (the one path-normalization the coverage check and the waiver matcher both share). The signals are each ported from a `prompts/coding-preflight/languages/<lang>.md` "Self-mock signals to refuse" bullet with the source `doc_keyword` retained so a drift guard fails when doc and module diverge. Patterns stay deliberately narrow (only the "stub the subject's own method, then assert the stub" shape and reaching into the subject's privates; subject identity is never inferred beyond the literal `sut` token). Both the static detector `validators/detect_self_mock.py` and the drift guard MUST import from here; four documented shapes needing subject identity no regex has are left to the mutation gate |
243
247
  | `run_context.py` | Per-task mutex, run context and run-input persistence; `consumers_mutex` helper for atomic `consumers.jsonl` writes |
244
248
  | `path_hints.py` | Compact path-hint persistence + legacy context hydration — stores `run-context` / `active-run-context` in the schemaVersion `2.0` `identity` + `pathHints` compact schema, and hydrates the legacy flat path keys (`RUN_MANIFEST_RELATIVE_PATH`, `TEAM_STATE_PATH`, etc.) in memory the moment the host-side reader reads them |
245
249
  | `consumers.py` | Append-only `consumers.jsonl` writer + reader — records which `implementation` runs consumed which `implementation-planning` stage |
@@ -264,6 +268,7 @@ Important modules:
264
268
  | `run_index_row.py` | single reference point for creating / slimming / hydrating a `~/.okstra` run-index row — runId SSOT, preserves projectId raw |
265
269
  | `error_report.py`, `error_log_core.py`, `error_zip.py` | backend for the okstra-inspect errors/error-zip facets — `error_log_core` is the read-only core that globs/parses/aggregates `errors-*.jsonl`, `error_report` renders the errors facet, and `error_zip` collects cross-project run directories, allowlist-anonymizes, aggregates clusters, and produces a zip |
266
270
  | `worker_heartbeat.py`, `worker_liveness.py` | `worker_heartbeat` is the single definition of the `- PROGRESS:` heartbeat line shape and its 5-minute (+60s grace) cadence budget, shared by the Phase 7 audit (`validators/validate_session_conformance.py`) and the live probe; `worker_liveness` backs `okstra worker-liveness`, reporting a pending worker as `stalled` (heartbeat past the budget) or `did-not-launch` (no wrapper `.log`/`.status.json` past the launch grace) |
271
+ | `phase_cleanup.py` | backs `okstra phase-cleanup` — decides tmux vs in-process mode, resolves the prior run dir (explicit `--run-dir`, else the newest FLAT run for a `--task-key`), and sequences the existing `okstra-trace-cleanup.sh --reclaim-completed` and `okstra-team-reconcile.sh` primitives. It never re-implements pane kill or completion detection, and it degrades to "nothing to report" instead of propagating a helper failure, so cleanup cannot block the next phase |
267
272
  | `log_report.py`, `time_report.py` | read-side backend for the okstra-inspect logs/time facets (`okstra log-report` pairs each wrapper transcript `.log` with its sibling prompt `.md` and reports both byte counts without changing legacy transcript-size fields; `okstra time-report` is per-task time aggregation) |
268
273
  | `rollup.py` | read-side backend for the okstra-rollup skill — fans the catalog out per task-group (or the whole project) and deterministically aggregates each task's run count, elapsed time (raw ms), error count, and latest report path, plus group-level totals/status, category, and phase distribution. Reuses the `time_report`/`error_log_core` functions and delegates report-body synthesis to the skill |
269
274
  | `usage_report.py` | Read-only okstra-usage backend — scans the whole current project's recent run timelines, defaults to 30 days, and returns task-type coverage, raw/billable tokens, known USD cost, CPU-sum and wall-clock milliseconds, unavailable reason counts, and unmatched pricing models |
@@ -374,6 +379,7 @@ Optional (v1.0 backward-compatible) top-level keys:
374
379
  | `validate-schedule.py` | Schedule section/order/code validation |
375
380
  | `validate-implementation-plan-stages.py` | enforces the Stage Map structure — checks the S1–S8 rules (`## 5.5 Stage Map` + `## 5.5.<i> Stage <i>` sections, ≤ 8 steps per stage, etc.) |
376
381
  | `validate_improvement_report.py` | enforces the 11-item contract of the improvement-discovery final-report. Automatically invoked by `validate-run.py` when `task_type == "improvement-discovery"` |
382
+ | `detect_self_mock.py` | static self-mock detector — scans the changed TEST files for SUT-stub signals (patterns imported from the SSOT `scripts/okstra_ctl/self_mock_signals.py`, never redefined here), matching each file as one whole-file string so multi-line signals are caught. Writes a `qa/self-mock[-stage-<N>].json` sidecar and prints `QA-RESULT: PASS|FAIL` as its last line (exit 0 = no hits, exit 1 = at least one hit). The sidecar records `scannedFiles`/`skippedFiles` so the gate can prove every changed test file was actually scanned (a run that skips them cannot pass on empty input). An optional `--waivers <path>` moves hits matching `(file,line,signal)` from `staticDetect.hits` to `staticDetect.waived` (each carrying the user's `reason`/`acknowledgedBy`) and records the file as `waiverSource`. Its verdict feeds the fail-closed `_validate_selfmock` gate in `validate-run.py` (implementation / final-verification): a diff that touches test files with no readable PASS sidecar blocks the run; a `waived` entry missing `reason`/`acknowledgedBy`, or a `waiverSource` that is not the task's own `qa/self-mock-waivers.json`, also blocks |
377
383
  | `validate-workflow.sh` | End-to-end fixture workflow validation |
378
384
  | `lib/*.sh` | Shared shell validator helpers and fixtures |
379
385
 
@@ -47,14 +47,17 @@ sequenceDiagram
47
47
  Skill->>Wizard: task-type error-analysis selected
48
48
  Wizard-->>Skill: workers/base-ref/model args
49
49
  Skill->>Run: render-bundle --render-only
50
+ Run->>Run: canonical brief preflight
50
51
  Run->>Run: validate brief/profile
51
52
  Run->>Run: resolve worker roster
52
53
  Run->>WT: provision/reuse worktree
53
54
  Run->>Art: analysis-profile.md includes common contract
54
- Run->>Art: task-manifest workflow next=implementation-planning
55
+ Run->>Art: task-manifest workflow next=validated report route
55
56
  ```
56
57
 
57
- The static next phase in `workflow.py` is `implementation-planning`. The actual final report may say more investigation is needed, but in the lifecycle the fix does not go straight ahead; it moves on to planning.
58
+ For canonical briefs, preflight runs before worker resolution, worktree provisioning, or report creation. A brief whose `reporter-confirmations` status is `pending` stops at this point; legacy briefs keep the compatibility path.
59
+
60
+ The final report records its next phase in `errorAnalysis.routing.nextTaskType`. After report validation passes, workflow metadata persists that route as `nextRecommendedPhase`. The static `error-analysis` → `implementation-planning` mapping is a fallback only when report data is missing, legacy, or not an error-analysis report.
58
61
 
59
62
  ## 4. lead execution flow
60
63
 
@@ -69,7 +72,7 @@ flowchart TD
69
72
  Report --> Persist[Phase 7 persist + validate]
70
73
  ```
71
74
 
72
- The workers analyze the symptom and evidence independently. The report-writer does not analyze during Phase 4/5 but writes the final report in Phase 6.
75
+ The workers analyze the symptom and evidence independently. In adversarial mode, even a finding reported by multiple workers enters the verification queue instead of receiving automatic consensus. Evidence-backed counter-evidence remains in the finding's round history, so later agreement cannot turn it into full consensus. The report-writer does not analyze during Phase 4/5 but writes the final report in Phase 6.
73
76
 
74
77
  ## 5. Deliverables and prohibitions
75
78
 
@@ -82,13 +85,15 @@ flowchart LR
82
85
  Hyp -. forbidden .-> Fix[Code fix in this run]
83
86
  ```
84
87
 
85
- The final report must contain the following.
88
+ The expected final-report content is:
86
89
 
87
90
  - evidence-backed cause analysis
88
91
  - uncertainty boundary
89
92
  - practical next diagnostic steps
90
93
  - if there is blocking uncertainty, `## 1. Clarification Items`, usually `Blocks=next-phase`
91
94
 
95
+ For `error-analysis`, the structured `errorAnalysis` object is the source of truth for the verbatim symptom, reproduction status, `EA-NNN` cause candidates and their counter-evidence, the next diagnostic, and routing. Its shape is enforced by `schemas/final-report-v1.0.schema.json` `$defs.ErrorAnalysis`; `validators/validate-run.py::_validate_error_analysis_consistency` enforces the cross-field semantics. A route to `implementation-planning` needs a credible referenced leading cause and `begin-planning`. A route back to `error-analysis` needs the sharp next diagnostic and `continue-investigation`.
96
+
92
97
  What is prohibited is source edit, refactor, fix attempt, implementation design artifact, and running build/migration/deploy. Deferring ambiguity that could be answered from code or logs to a user question is also a defect per the profile.
93
98
 
94
99
  ## 6. Code reviewed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.141.3",
3
+ "version": "0.143.0",
4
4
  "description": "Multi-agent cross-verification orchestrator runtime + Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.141.3",
3
- "builtAt": "2026-07-30T12:10:17.183Z",
2
+ "package": "0.143.0",
3
+ "builtAt": "2026-08-02T11:16:25.811Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -43,7 +43,7 @@ Do NOT duplicate the data.json contents here — the data.json is the canonical
43
43
 
44
44
  Write the audit sidecar at `**Audit sidecar path:**` before required reading, then append `- PROGRESS: <stage> <ISO-8601-UTC>` at a cadence no longer than five minutes. Use only: `started`, `required-reading-complete`, `synthesis-start`, `data-json-write-start`, `render-start`, and `write-result-start`. When a stage runs longer than that, append `- PROGRESS: in-stage:<stage> <ISO-8601-UTC>` with the same stage names. The Phase 7 validator enforces the first stage, timestamps, cadence, and this allowlist.
45
45
 
46
- `data-json-write-start`, `render-start` and `write-result-start` open a stretch whose work is one uninterruptible tool call, so no `in-stage:` line can be appended while it runs. Those three carry a 20-minute budget instead of five; every other stage keeps the five-minute cadence.
46
+ `data-json-write-start`, `render-start` and `write-result-start` open a stretch whose work is one uninterruptible tool call, so no `in-stage:` line can be appended while it runs. Those three carry a 20-minute budget instead of five. `required-reading-complete` and `synthesis-start` open the tool-free reading and synthesis stretches — the worker cannot append an `in-stage:` line mid-synthesis either — and carry a 15-minute budget. Every other stage keeps the five-minute cadence.
47
47
 
48
48
  ## Execution Rules
49
49
 
@@ -95,6 +95,7 @@ Rules (the schema enforces most of these — they are listed here so you know *w
95
95
  - `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.
96
96
  - **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.
97
97
  - **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.
98
+ - **Error-analysis diagnosis and routing.** When `header.taskType` is `error-analysis`, populate the required `errorAnalysis` object. Copy `errorAnalysis.symptomVerbatim` byte-for-byte from the symptom stated in the brief's `Source Material`; do not paraphrase it. Every `causeCandidates[]` row includes the full `supportingEvidence`, `falsifyingEvidenceChecked`, `confidence`, and `disproveWith` fields. Route `errorAnalysis.routing.nextTaskType=implementation-planning` with `direction=begin-planning`, or route `errorAnalysis.routing.nextTaskType=error-analysis` with `direction=continue-investigation`; no other pairing is valid. `verdictCard.nextStep`, `finalVerdict.nextStep`, the first `recommendedNextSteps` action and command, and the unique `followUpTasks` row whose `origin` is `phase-continuation` MUST all point to the same `errorAnalysis.routing.nextTaskType` target. The schema enforces only the presence of a `phase-continuation` row. Phase validation MUST enforce exact target agreement and uniqueness through `validators/validate-run.py::_validate_error_analysis_consistency`; until that check is implemented and executed, those semantics are contract requirements rather than enforced guarantees.
98
99
  - **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.
99
100
  - **External QA advisory.** A Tier 3 entry requiring `db`, `http`, or
100
101
  `external` may be non-PASS without changing approval or final verdict. Render
@@ -103,7 +104,7 @@ Rules (the schema enforces most of these — they are listed here so you know *w
103
104
  and add the exact rerun command to `recommendedNextSteps`. Never turn this
104
105
  advisory alone into a clarification, Acceptance Blocker, conditional
105
106
  acceptance condition, or blocked routing.
106
- - **§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`.
107
+ - **§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`, `newTaskId` reuses the current task-id, `autoSpawn` is `"no"`, and `priority` is `"P0"`. For `release-handoff` runs, omit the phase-continuation row. The schema `allOf` / `contains` clause enforces row presence, not exact route-target agreement or uniqueness; phase validation must enforce those error-analysis semantics as specified above.
107
108
  - **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.
108
109
  - **Optional Section 0.** Include `clarificationCarryIn` ONLY when the lead's prompt provides a non-empty carry-in path. Omit the key entirely otherwise (do NOT set it to `null` or an empty object).
109
110
  - **Reading Confirmation** goes at `**Audit sidecar path:**` per the selected report-writer preamble's `Required reading` section — never in the data.json or the main worker-results file.
@@ -119,6 +120,7 @@ Rules (the schema enforces most of these — they are listed here so you know *w
119
120
  - For `implementation-planning`, populate `implementationPlanning.requirementCoverage` with one row per concrete requirement from the brief / packet, using IDs `R-001`, `R-002`, ... in source order. A `covered` row's `coveredBy` MUST name the specific Option Candidate plus Stage/Step that satisfies the requirement. Use `status: "covered"` only when the report's plan actually covers it; use `documented-deviation` only when `coveredBy` states the concrete alternative and the row records non-empty unique `decisionRefs` plus `approvalDisposition`. Each `C-NNN` ref must name a clarification in this report; each `D-NNNN` ref must name a `decisionDrafts[].number`. `approvalDisposition: "accepted"` requires a referenced clarification with `status: answered|resolved` and non-empty `userInput`; `approvalDisposition: "blocked C-NNN"` requires that same-report clarification to be `status: open, blocks: approval`. Otherwise use `gap` or `blocked C-NNN` and ensure the corresponding `Clarification Items` row blocks approval. Do not collapse this into `ticketCoverage`; ticket coverage is not requirement coverage. **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.ImplementationRequirementCoverageRow` and `validators/validate-run.py` `_validate_requirement_deviations`.
120
121
  - For `implementation-planning`, each `requirementCoverage` row's `source` is a graded cell, not prose — free text like `"carry-in from requirements-discovery C-001"` is rejected. Write exactly one of: `brief:EB-001` / `brief:PB-001` / `brief:EO-001`, an end-state id the brief declares — when the brief pins ids, citing a heading instead is rejected, because every brief carries the same generic headings and a heading cannot say WHICH reporter line the requirement came from (only a brief authored before the end-state sections existed still takes the older `brief:<heading>` form, and there the heading must literally exist in it); `derived:R-NNN — <one-line reason>`, whose chain must terminate at a `brief:` or `contract:` row of the same table without cycling; or `contract:<rule>`, for artifacts okstra's own phase contract mandates, whose allowlist is exactly the two tokens `decision-record-step` (the §5.4 Decision Drafts materialization step) and `glossary-step` (the glossary proposal step) — any other rule name is rejected, so never invent one. (Maintainer SSOT for that allowlist: `scripts/okstra_ctl/scope_provenance.py` in the okstra repo.) A requirement you cannot source this way does not belong in the table: put it in `clarificationItems[]` with `Blocks=approval`. **Enforced:** `validators/validate-run.py` `_validate_requirement_provenance`. In the same table, anchor every stage number in `coveredBy` to a `Stage` / `Stages` word (`Stage 2`, `Stages 1-3`) — `_validate_stage_has_requirement` reads that cell as prose and fails the plan when a Stage Map stage is cited by no row.
121
122
  - For `implementation-planning`, also populate `implementationPlanning.decisionDrafts` (one row per decision meeting all three decision-record criteria; `[]` otherwise) and `implementationPlanning.skippedAdrCandidates` (evaluated-but-dropped adr-candidates; `[]` otherwise). The schema excerpt enumerates the row shape; the renderer emits §5.4 `### Decision Drafts`. When `decisionDrafts` is non-empty, the plan's stages MUST carry a stepwise step that creates `.okstra/decisions/<NNNN>-<slug>.md` (validate-run gates this).
123
+ - For `implementation-planning`, populate `implementationPlanning.variationPointAnalysis` — a `hasMultipleImplementations` judgement synthesized from the analysis workers' output, not a field filled in last. When it is `true`, write one `points[]` row per varying behavior carrying `behavior`, the two or more `implementations` that serve it, `evidence` (a `path:line`, or the sibling task / stage that already implements that behavior), and an `extractionDecision` of `extract` / `interfaceKind` / `coveredBy` (the Stage Map stage that builds the interface) / `rationale`; when it is `false`, write a non-empty `noVariationRationale` and leave `points` empty (the two branches are mutually exclusive). Do NOT pass a boilerplate rationale — `false` is the cheaper field to fill, and a `false` declaration the brief or the sibling code in the workers' evidence contradicts is a `P-Var` DISAGREE, not a saving. Also populate `implementationPlanning.recommendedOption.testSeams`: one row per boundary a test injects at and replaces, each carrying `boundary` / `injectedAs` / `replacedInTest`. An empty list is a conscious "no seam needed" claim, never a default for a field nobody filled. The schema excerpt enumerates both row shapes — author against it. (Maintainer SSOT for these two rules: the `Required deliverable shape` bullet in `prompts/profiles/implementation-planning.md` in the okstra repo; that path is not resolvable here, so it is provenance, not a file to open.) **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.VariationPointAnalysis` / `$defs.VariationPoint` (the block is in `implementationPlanning.required`) plus `testSeams` in `$defs.RecommendedOption`'s `required`; `validators/validate-run.py` `_validate_variation_point_analysis` rejects a rationale-less `false`, a `false` carrying points, a `true` with no point, an `extract: true` decision leaving `interfaceKind` or `coveredBy` empty, and a hexagonal project extracting as anything but a port; and every point becomes a `P-Var-*` plan item judged in §5.5.9.
122
124
  - When the `Task Type` is `improvement-discovery`, populate `## 5.9 Improvement Candidates` with the 11-column schema enforced by `validators/validate_improvement_report.py`. The `Expected behavior after` cell states in one observable sentence what becomes different once the candidate is applied — it seeds the downstream brief's `EB-NNN` / `EO-NNN`, and an empty cell fails the run. Source the row IDs (`I-NNN`), lens whitelist, and Source workers patterns from `scripts/okstra_ctl/improvement_lenses.py` — do NOT introduce new lens names or worker prefixes. `improvement-discovery` is NOT in the data.json schema enum, so author its markdown directly (not via `okstra-render-final-report.py`). Immediately after writing the markdown, run (`Bash`): `okstra inject-report-index <markdown path> --report-language <en|ko>`. That adds the top-of-report Index plus `I-NNN` / `C-NNN` scroll anchors; the run validator fails the report when the Index anchor is absent.
123
125
 
124
126
  Write the data.json (and the audit sidecar `.md`) with your `Write` tool — that is the canonical authoring path, and okstra ships no hook that blocks `.md` writes (its only settings hook is the `SessionEnd` trace-cleanup; the coding-preflight hook emits reminders but never blocks). A Bash heredoc is acceptable ONLY when a specific `Write` call is genuinely rejected by the host environment, and it MUST produce byte-identical content — do not reach for it pre-emptively. Then invoke the renderer (`Bash`): `okstra render-final-report <data.json path>`. Confirm both files exist and respond with a short status line prefixed by your model identity, per the preamble §"Return message to the lead":
@@ -133,9 +133,9 @@ When a change adds or modifies a service's dependency on a concrete adapter —
133
133
 
134
134
  The verdict is mechanical: this change adds or modifies such a dependency → finding; the injection sits entirely on lines this change did not touch → clean.
135
135
 
136
- **An existing convention does not clear this one.** A codebase that injects concrete `*Repository` classes everywhere is precisely the debt this rule pays down, one touched injection at a time — matching the surrounding style is the condition being flagged, not a defence against it. Record it, say so in the note (*"matches existing convention — advisory"*), and propose the port: its name and the two or three method signatures it would declare. This is the single rule in this overlay where a project-local convention does not override the pack; every other conflict still resolves in the project's favour.
136
+ **An existing convention does not clear this one.** A codebase that injects concrete `*Repository` classes everywhere is precisely the debt this rule pays down, one touched injection at a time — matching the surrounding style is the condition being flagged, not a defence against it. Record it, say so in the note (*"matches existing convention — advisory"*), and propose the port: its name and the two or three method signatures it would declare — unless the project declares `architecture.style = hexagonal` in `.okstra/project.json`, where this item is blocking and that same port sketch is what you write rather than what you record: fix the injection before the write, never record-and-pass. This is the single rule in this overlay where a project-local convention does not override the pack; every other conflict still resolves in the project's favour.
137
137
 
138
- Severity: advisory. It is a direction-of-travel rule, not a correctness gate it never blocks on its own.
138
+ Severity: advisory in a project that has not declared `architecture.style = hexagonal` a direction-of-travel rule there, not a correctness gate, so it never blocks on its own. Under that declaration it is blocking: the injection is fixed before the write.
139
139
 
140
140
  ---
141
141
 
@@ -147,4 +147,4 @@ Severity: advisory. It is a direction-of-travel rule, not a correctness gate —
147
147
  - [ ] Every entity, value object, domain enum, and domain error is declared under `domain/`.
148
148
  - [ ] Port files only declare shape and `import` domain types — they don't *define* them.
149
149
  - [ ] No changed domain file imports an ORM, a framework, or anything under adapters / infrastructure / services.
150
- - [ ] A service dependency this change adds or modifies goes through a port — or the advisory is recorded with the port sketch.
150
+ - [ ] A service dependency this change adds or modifies goes through a port — or, unless the project declares `architecture.style = hexagonal` in `.okstra/project.json` (there this item is blocking: fix before write, never record-and-pass), the advisory is recorded with the port sketch.
@@ -53,7 +53,7 @@ If no Stage 1 language rule matches (an unlisted language), stop and ask the use
53
53
  - [ ] Tests planned: which test(s) cover this change. New behaviour without a test is **incomplete** unless the user has explicitly opted out for this change.
54
54
  - [ ] **Testing discipline:** the test does not stub/spy methods on the SUT itself (collaborators are fine), and assertions are on outcomes (return values, state, events, boundary calls) — not on which internal helper was called. Each branch this change adds (`catch`, guard, early return, `else`) has a test that fails when the branch body is deleted; assertions land on the last write to a record, not an intermediate one; each test title names its unit and the single condition it isolates; no effect is claimed under its own mock; shared fixtures keep their ordinary defaults; the scenarios' setup values actually differ; every new test helper/mock is used by a test in this same change; no positional mock-argument access (`rg 'mock\.calls'`).
55
55
  - [ ] **Third-party wrapper (when this change wraps a library call):** the wrapper adds behaviour the library does not already provide — read the installed library source before keeping a recovery branch — no comment names a condition the call site does not establish, and a rethrow keeps the original error as `cause`.
56
- - [ ] **Hexagonal overlay (if loaded):** no business logic inside any port body, adapter methods are I/O only (no post-fetch JS filtering on domain state, no `findValid*`/`findActive*` adapter names hiding rules), all domain objects declared under `domain/`, no changed domain file importing outward (ORM / framework / adapters / services), and a service dependency you add or modify goes through a port rather than a concrete adapter (advisory — record it with the port sketch).
56
+ - [ ] **Hexagonal overlay (if loaded):** no business logic inside any port body, adapter methods are I/O only (no post-fetch JS filtering on domain state, no `findValid*`/`findActive*` adapter names hiding rules), all domain objects declared under `domain/`, no changed domain file importing outward (ORM / framework / adapters / services), and a service dependency you add or modify goes through a port rather than a concrete adapter (advisory — record it with the port sketch; blocking when the project declares `architecture.style = hexagonal` in `.okstra/project.json` — fix before write, never record-and-pass).
57
57
  - [ ] Existing code searched: `grep` for the symbol / file / identifier you are about to add. Do not duplicate.
58
58
  - [ ] Project conventions checked: `.editorconfig`, `CONTRIBUTING.md`, formatter config (`.prettierrc`, `rustfmt.toml`, `ktlint`, `google-java-format`, etc.). **Project rules override this resource pack on conflict.**
59
59
 
@@ -88,7 +88,7 @@ This adapter maps the neutral Okstra lead operations to Claude Code host primiti
88
88
  - At run start, record `teamName` as the audit label and `teamCreate: { attempted: false, status: "implicit", splitPane: <bool> }` in team-state. A concurrent run records `status: "skipped", reason: "concurrent-run"`. Populate `lead.sessionId`; the session transcript lives under `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl`.
89
89
  - Record the lead pane once with `mkdir -p "<RUN_DIR>/state" && { . "$HOME/.okstra/bin/lib/okstra/tmux-pane.sh" 2>/dev/null && okstra_resolve_caller_pane; } > "<RUN_DIR>/state/lead-pane.id" 2>/dev/null || true`. This is silent setup and must not gate cleanup; the cleanup script protects the lead pane itself.
90
90
  - Collect and persist token usage before any live-roster cleanup, including cleanup between batches and the run-end shutdown sequence.
91
- - Before each new worker batch, run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<RUN_DIR>"`, then send `SendMessage(to: <name>, message: { type: "shutdown_request" })` only to confirmed-complete teammates from the prior batch. Never target the lead or an incomplete critic/reverify worker.
91
+ - Before each new worker batch (and before the next phase's render-bundle), the batch/phase-boundary reclaim is `okstra phase-cleanup --run-dir "<RUN_DIR>" --project-root "<PROJECT_ROOT>" --fallback-team "session-<lead.sessionId-prefix>"` (tmux-aware: it skips pane reclaim in a non-tmux session). Pass `--fallback-team` every time: after a resume or compaction the session id is re-issued, and without the label the reconcile finds no live roster and prints nothing to dismiss. It reclaims only completed panes and prints `dismissible-teammates`; send `SendMessage(to: <name>, message: { type: "shutdown_request" })` only to those confirmed-complete teammates. Never target the lead or an incomplete critic/reverify worker. (This is the batch/phase boundary reclaim; the run-end keep/clean sequence below is a separate, final step for when no next phase follows.)
92
92
  - After batch cleanup, record the current live session generation with `okstra token-usage "<TEAM_STATE_PATH>" --record-observed-session --project-root "<PROJECT_ROOT>"`. This protects usage accounting when Claude Code re-issues the session id after resume or compaction.
93
93
  - Claude Code cannot delete the implicit team or surgically remove an idle roster entry. Explain that teammates may remain visible until session end and, when needed, give the manual action `Delete team <teamName> in Teams/FleetView`.
94
94
  - The `SessionEnd` hook runs `$HOME/.okstra/bin/okstra-team-reconcile.sh --session-end` as the safety net for the current live session.
@@ -102,6 +102,6 @@ This adapter maps the neutral Okstra lead operations to Claude Code host primiti
102
102
  > This phase is ending. The following Okstra panes and worker teammates remain — close and clean them up?
103
103
  > <quoted `--list` output>
104
104
  > (Yes) Close everything and clean up teammates / (No) Keep everything
105
- 5. On `keep`, preserve every residual resource and show `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<RUN_DIR>"` plus the manual Teams/FleetView action.
105
+ 5. On `keep`, preserve every residual resource and show `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<RUN_DIR>"` plus the manual Teams/FleetView action. Tell the user that `keep` holds only until the next boundary: if this session goes on to another phase/batch, that transition's `okstra phase-cleanup` reclaims the kept **completed** panes unattended (in-flight resources and the lead pane are never touched).
106
106
  6. On approved `clean`, emit the teardown checkpoint, run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<RUN_DIR>"`, then run `$HOME/.okstra/bin/okstra-team-reconcile.sh --project-root "<PROJECT_ROOT>" --fallback-team "session-<lead.sessionId-prefix>"` exactly once. The resolver reads the current live session's `~/.claude/teams/session-<live>/config.json`, falling back to the snapshot directory only when the live directory is absent, and prints `dismissible-member: <name>` records.
107
107
  7. Send `SendMessage(to: <name>, message: { type: "shutdown_request" })` to each printed, confirmed-complete non-lead member. The `message` MUST be the object literal shown, NEVER a JSON string in a text field. Never target the lead or use `TaskStop`; teammates are not background tasks.
@@ -126,9 +126,9 @@ Read source files lazily:
126
126
 
127
127
  ### Brief Reporter-Confirmation Precondition (BLOCKING)
128
128
 
129
- After reading `task-brief.md`, extract the frontmatter `reporter-confirmations` field (`complete | partial | pending | skipped`) and apply the shared handling matrix in `prompts/profiles/_common-contract.md` "Brief handoff contract" → "Reporter confirmation precondition" — that block is the single authority on per-value semantics; do not re-derive them here.
129
+ After reading `task-brief.md`, extract the frontmatter `reporter-confirmations` field (`complete | partial | pending | skipped`) when present and apply the shared handling matrix in `prompts/profiles/_common-contract.md` "Brief handoff contract" → "Reporter confirmation precondition" — that block is the single authority on per-value semantics; do not re-derive them here.
130
130
 
131
- Loader-level flow control only: on `pending` (or field missing), emit `REPORTER_CONFIRMATION_PENDING` and STOP — do not invoke `team-contract` or any analyser; the operator must rerun `okstra-brief-gen` Step 6.5 before Phase 2 can start. Every other value proceeds to Step 5 (with the matrix's flags carried forward for the phase profile).
131
+ Loader-level defensive flow control only: on `pending`, emit `REPORTER_CONFIRMATION_PENDING` and STOP — do not invoke `team-contract` or any analyser, and do not write a final report. Regenerate the brief with `okstra-brief-gen` Step 6.5 and prepare a fresh run. A missing field is a legacy brief, not `pending`, and proceeds to Step 5. Current-format missing or invalid values are rejected during preparation, before a fresh run can reach this loader. Every other value proceeds to Step 5 with the matrix's flags carried forward for the phase profile.
132
132
 
133
133
  ## Step 5: Read Run Manifest and Team State
134
134
 
@@ -85,7 +85,10 @@ Read the worker result files generated in Phase 4/5 and extract individual findi
85
85
  - Only one worker confirms a finding → one single-source group.
86
86
  4. When grouping is ambiguous, prefer splitting over merging (avoid over-merging). Semantic matching, ticket-set equality, and evidence interpretation remain lead judgments; the engine does not perform fuzzy matching or decide whether evidence is credible.
87
87
  5. Write `runs/<task-type>/state/convergence-groups-<task-type>-<seq>.json`. Each group carries its `ticketIds`, `originWorker`, `originEvidence`, `discoveredBy`, and every `<worker>:<item-id>` source in `sourceItems`. When a live command or external read produced reproducible evidence, also include `evidenceArtifacts[]` with its `.okstra/` path, SHA-256 digest, command, and environment. The field is optional because historical or inaccessible evidence may not have a captured artifact. The lead and verifier MUST NOT infer live or external evidence from wording or keyword matching; they use the finding's explicit claim, provenance, and supplied artifacts. Include the resolved worker roster in order with functional `audience` values; do not derive scope from provider or model identity. The `audience` enum is a convergence role, not a phase label: every finding-producing worker uses `analysis` — an `implementation` run's verifiers included — and only the report author uses `report-writer`. There is no `implementation-verifier` audience here; map the verifier roster to `analysis`.
88
- 6. Do not write a queue or classification in this grouped-input artifact. `okstra convergence seed` deterministically marks multi-source groups `full-consensus` and puts only single-source groups in the working queue. Section 6 never enters the grouped input.
88
+ 6. Do not write a queue or classification in this grouped-input artifact. `okstra convergence seed` classifies Round 0 by mode:
89
+ - Collaborative mode: multi-source groups become `full-consensus` immediately; only single-source groups enter the working queue.
90
+ - Adversarial mode: every finding enters the working queue regardless of source count. Semantic grouping merges provenance only; it does not decide a finding is reliable.
91
+ Section 6 never enters the grouped input.
89
92
 
90
93
  ### Round 1-N: Re-verification Loop (queue-pruned)
91
94
 
@@ -208,7 +211,7 @@ ELSE:
208
211
 
209
212
  `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.
210
213
 
211
- 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).
214
+ Design intent: one `counter-evidence` refute denies a claim consensus (it cannot rise above `contested` however many others AGREE); later-round agreement does not erase that refutation history. The only resolution that overrides prior `counter-evidence` is a later round where every non-discoverer non-error worker disagrees, producing `worker-unique`. 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).
212
215
 
213
216
  ## Re-verification Dispatch
214
217
 
@@ -391,7 +391,7 @@ After persistence, reply briefly in the resolved Report Language with: completio
391
391
  ## Run-scoped worker-resource lifecycle
392
392
 
393
393
  - At run start, call the selected adapter's setup required to distinguish lead-owned resources from worker-owned resources.
394
- - Before every new worker batch, clean only confirmed-complete resources from the prior batch, call `record_lead_event` for the batch-cleanup checkpoint, and never terminate the lead or an incomplete worker.
394
+ - Before every new worker batch, clean only confirmed-complete resources from the prior batch, call `record_lead_event` for the batch-cleanup checkpoint, and never terminate the lead or an incomplete worker; the batch-reclaim primitive is the selected adapter's.
395
395
  - After Phase 7 persistence and `collect_usage`, enumerate residual adapter-owned resources. If none remain, skip the question.
396
396
  - If resources remain, call `prompt_user` once with a binary keep-or-clean choice. The answer controls the entire residual set; do not ask a second backend-specific cleanup question.
397
397
  - On keep, preserve all resources and provide the selected adapter's manual-cleanup instruction.
@@ -78,6 +78,7 @@ The deterministic extractor assigns the following prefixes:
78
78
  | `P-Rb-<N>` | `4.5.7 Rollback Strategy` | one rollback path |
79
79
  | `P-Req-<N>` | `4.5.8 Requirement Coverage` | one requirement coverage row |
80
80
  | `P-Prep-S<stage>-<kind>` | Stage `designSurfaceCoverage` + `5.5.10 Implementation Design Preparation` | exactly one detector-produced `(stage, kind)` |
81
+ | `P-Var-<N>` | `5.5.11 Variation-Point Analysis` | one variation point (its `behavior` + `extractionDecision`), or a lone `P-Var-0` when the plan declares no variation point |
81
82
 
82
83
  `4.5.2 Trade-off Matrix` and `4.5.3 Recommended Option` are NOT extracted as standalone plan items — the trade-off matrix is evaluated implicitly through each option's `P-Opt-*` verification, and the recommended option is one of those `P-Opt-*` rows.
83
84
 
@@ -93,10 +94,10 @@ The verdict tokens `AGREE` / `DISAGREE` / `SUPPLEMENT` are reused, but their mea
93
94
 
94
95
  - **AGREE**: the item is executable as written *and* internally consistent with other items in the plan.
95
96
  - **DISAGREE(<kind>)**: the item is broken. `<kind>` MUST be one of:
96
- - `a` — a concrete referenced file path / symbol **contradicts** a different concrete path / symbol for the same artifact in another step or option's File Structure list (a genuine mismatch between two spelled-out references). An abbreviated / ellipsis (`…`) / under-specified path is NOT kind `a` — it contradicts nothing, it is merely imprecise notation; classify it as `b`.
97
+ - `a` — a concrete referenced file path / symbol **contradicts** a different concrete path / symbol for the same artifact in another step or option's File Structure list (a genuine mismatch between two spelled-out references). An abbreviated / ellipsis (`…`) / under-specified path is NOT kind `a` — it contradicts nothing, it is merely imprecise notation; classify it as `b`. On a `P-Var-*` item this kind never blocks on one vote — a variation-point defect is a design judgement and takes a majority exactly like `b` / `e` (see the `P-Var-*` paragraph below).
97
98
  - `b` — a command **or a referenced path** is not executable or is ambiguous — including an abbreviated, ellipsis, or under-specified path that does not resolve as written. A command that fails **only** because the planning-time worktree lacks build/test dependencies is NOT kind `b` — see §"Planning-time environment gap" below.
98
99
  - `c` — validation signal is not observable
99
- - `d` — rollback violates commit / dependency order
100
+ - `d` — rollback violates commit / dependency order (advisory — a rollback is human-run, so this never blocks the gate; likewise any `P-Rb-*` rollback item is advisory regardless of kind)
100
101
  - `e` — item contradicts the trade-off matrix
101
102
  - `f` — requirement coverage row cites no concrete option / stage / step, cites a non-existent option / stage / step, or marks a requirement `covered` while the cited plan item does not satisfy the row's stated requirement. A row that cites an existing option / stage / step is concrete for this purpose even if that option's File Structure paths are abbreviated — path imprecision inside the cited option is kind `b` on that option's own item, not `f` on the coverage row.
102
103
  - **SUPPLEMENT**: the item is sound but is missing a dependency / edge case / precondition.
@@ -112,6 +113,16 @@ The verdict tokens `AGREE` / `DISAGREE` / `SUPPLEMENT` are reused, but their mea
112
113
  - `not-applicable`: AGREE only when the rationale is consistent with the stage action; otherwise DISAGREE with `fixability` (`planner-fixable` when the plan can supply the missing contract, `needs-user-input` only for genuinely external facts).
113
114
  - A declared `blocked` item is not itself a plan-body failure. Missing or duplicate coverage, an empty proposal, a mismatched reference, or an unjustified disposition is a failure and receives `DISAGREE(<kind>)` with `fixability`.
114
115
 
116
+ `P-Var-<N>` applies the same verdict tokens to the variation-point analysis and is **majority-gated**: exactly like the `b` / `c` / `e` kinds, only a `majority-disagree` blocks the gate, and a single `DISAGREE` does not block on its own. Whether a behavior has two implementations, and whether the plan extracted the right interface for it, is a judgement about the design — it lacks the concrete certainty of kind `a`, where the verifier can point at two spelled-out references that contradict each other. So a P-Var defect is raised under a majority-gated kind (`b` when the extraction decision or its seam is not implementable as written, `b` likewise when a declared "no variation point" is contradicted by evidence the plan itself carries, `e` when it contradicts the recommended option) and never as kind `a`, which would single-vote-block on a judgement call. **Enforced:** `validators/validate-run.py` `_is_variation_point_item` excludes `P-Var-*` from both kind-`a` gating paths (`_classify_plan_item_gate`, `_is_correctness_critical`), so a mis-tagged `DISAGREE(a)` on one still needs a majority to block.
117
+
118
+ DISAGREE on a `P-Var-*` item means one of:
119
+
120
+ - **the declared "no variation point" is false** — `P-Var-0` claims `hasMultipleImplementations: false`, but the same behavior already has two or more implementations in a sibling task's stage or in the brief's own material, so the declaration is contradicted by evidence the plan itself carries;
121
+ - **the extraction decision violates OCP** — the plan branches on resource identity (one `if` / `switch` arm per implementation) instead of extracting the interface the second implementation plugs into, so adding the next implementation means editing the same call site again;
122
+ - **the declared test seam is not actually injectable** — `extractionDecision.coveredBy` or the recommended option's `testSeams[].injectedAs` names no construction or wiring point a test can replace, so the seam exists on paper but nothing can be substituted at it.
123
+
124
+ The hexagonal rule that an extracted point must declare `interfaceKind: "port"` is already machine-checked by `validators/validate-run.py` `_validate_variation_point_analysis` (it fires only for a project whose `architecture.style` is `hexagonal`). Do not re-run that mechanical check as a verdict; spend the judgement on placement and semantics instead — a point extracted as a port whose domain rule leaked into the adapter passes the validator and is still wrong.
125
+
115
126
  The semantic checks above are the plan-body enforcement layer for the schema-valid structures; `validators/validate-run.py` enforces detector coverage and references, while this worker verdict decides whether the content is implementable.
116
127
 
117
128
  Worker non-result handling (`timeout`, `error`, no result file, wrapper `cli-failure`) is identical to finding convergence: do NOT aggregate as DISAGREE, record `contract-violation`, and apply the round-level abort rule below.
@@ -156,9 +167,9 @@ When `config.adversarial == true` (the default for `implementation-planning`; se
156
167
  - The burden of proof sits on the plan: an item earns `AGREE` only if the verifier actively tried to break it and could not.
157
168
  - The verifier MUST open the file paths / symbols / commands the item cites and confirm they exist and are **defined** 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". The widening stops at *definition*: a build/test command's **execution success** is out of scope here, because the planning worktree has no dependencies installed (§"Planning-time environment gap"). Confirm the script is declared; do not treat its failure to run as evidence against the plan.
158
169
  - If a cited path / command / validation signal cannot be confirmed, the verifier responds `DISAGREE(<kind>)` with the applicable breakage kind (a–f); uncertainty resolves toward DISAGREE, not AGREE.
159
- - **Single-vote-blocking kinds.** A single `DISAGREE` is approval-blocking on its own — no majority needed — when the breakage kind is `a` (cited path/symbol mismatch) or `d` (rollback violates commit/dependency order) on *any* plan item, or `f` (requirement-coverage mismatch) on a `P-Req-*` item. These defects are concrete, safety-critical, and adversarially verifiable (the verifier confirmed the cited path / order / requirement), so one correct dissent must not be outvoted. Each creates a `majority-disagree` classification and MUST become a `Blocks=approval` clarification row. Kinds `b` / `c` / `e` still need a majority — `b` especially is prone to planning-vs-implementation environment false positives. Because `a` is reserved for a concrete contradiction between two spelled-out references (see §"Plan-body verdict semantics"), an abbreviated / ellipsis / under-specified path is raised as `b` (majority-gated), never `a` — a lone "this path is abbreviated" dissent must not single-vote-block on notation alone, and it is especially not blocking on a *rejected* option that will never be implemented. **Enforced:** `validators/validate-run.py` `_classify_plan_item_gate` (`_SINGLE_VOTE_BLOCKING_KINDS = {a, d}` + the P-Req `f` rule).
170
+ - **Single-vote-blocking kinds.** A single `DISAGREE` is approval-blocking on its own — no majority needed — when the breakage kind is `a` (cited path/symbol mismatch) on any plan item other than a `P-Var-*` one, or `f` (requirement-coverage mismatch) on a `P-Req-*` item. On a `P-Var-*` item kind `a` never blocks on one vote — a variation-point defect is a design judgement and takes a majority exactly like `b` / `e` (see §"Plan-body verdict semantics"). These defects are concrete, safety-critical, and adversarially verifiable (the verifier confirmed the cited path / requirement), so one correct dissent must not be outvoted. Each creates a `majority-disagree` classification and MUST become a `Blocks=approval` clarification row. Kinds `b` / `c` / `e` still need a majority — `b` especially is prone to planning-vs-implementation environment false positives. **Rollback ordering (`d`) never blocks the gate at all** — a rollback is executed by a human, not by okstra's workers or verifiers, so a `DISAGREE(d)` is recorded as dissent and dropped from every gate tally; it can only ever fold an item into `passed-with-dissent`. Because `a` is reserved for a concrete contradiction between two spelled-out references (see §"Plan-body verdict semantics"), an abbreviated / ellipsis / under-specified path is raised as `b` (majority-gated), never `a` — a lone "this path is abbreviated" dissent must not single-vote-block on notation alone, and it is especially not blocking on a *rejected* option that will never be implemented. **Enforced:** `validators/validate-run.py` `_classify_plan_item_gate` (`_SINGLE_VOTE_BLOCKING_KINDS = {a}`, `_ADVISORY_ONLY_KINDS = {d}` + the P-Req `f` rule).
160
171
 
161
- Plan-body verification stays **lightweight** even under this posture — the `verificationMode = "full-reanalysis"` forcing in [convergence](./convergence.md) §"Adversarial Verification Mode" applies to finding convergence only (see §"Mode constraint"); the adversarial posture here only changes verifier behaviour, not the mode. This raises verification *quality* (active refutation, plan-side burden). The gate *threshold* stays majority-based for the majority-gated kinds (`b`/`c`/`e`), with the single-vote-blocking exception above for the concrete, safety-critical kinds (`a`/`d`, and `f` on P-Req). A majority requires at least two participating (non-error) votes, so a lone surviving `DISAGREE` whose peer returned a non-result does NOT block on a majority-gated kind — a worker failure must not make the gate stricter than a healthy roster would.
172
+ Plan-body verification stays **lightweight** even under this posture — the `verificationMode = "full-reanalysis"` forcing in [convergence](./convergence.md) §"Adversarial Verification Mode" applies to finding convergence only (see §"Mode constraint"); the adversarial posture here only changes verifier behaviour, not the mode. This raises verification *quality* (active refutation, plan-side burden). The gate *threshold* stays majority-based for the majority-gated kinds (`b`/`c`/`e`), with the single-vote-blocking exception above for the concrete, safety-critical kinds (`a`, and `f` on P-Req); rollback ordering (`d`) is advisory and never blocks. A majority requires at least two participating (non-error) votes, so a lone surviving `DISAGREE` whose peer returned a non-result does NOT block on a majority-gated kind — a worker failure must not make the gate stricter than a healthy roster would.
162
173
 
163
174
  ## Round protocol (single round at default `maxRounds=1`)
164
175
 
@@ -171,8 +182,8 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
171
182
  - `full-consensus` — all participating analysers `AGREE` (SUPPLEMENT counts as agree on the item itself).
172
183
  - `partial-consensus` — majority `AGREE`, dissenting `DISAGREE` recorded.
173
184
  - `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.)
174
- - `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**.
175
- - `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`.
185
+ - `majority-disagree` — a *majority* of analysers `DISAGREE` (majority needs ≥2 participating non-error votes; rollback-ordering `DISAGREE(d)` votes are advisory and excluded from the tally), OR any single-vote-blocking kind fires: one `DISAGREE(a)` on any item other than a `P-Var-*` one — where kind `a` never blocks on one vote and takes a majority like `b` / `e` — or one `DISAGREE(f)` on a `P-Req-*` item (see §"Single-vote-blocking kinds"). This classification **blocks approval**.
186
+ - `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, and on a `P-Var-*` item none fires at all — its kind `a` never blocks on one vote and takes a majority like `b` / `e`. 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`.
176
187
  - `contested` only meaningful when `maxRounds > 1`; at default `maxRounds=1`, fold any unresolved item into `partial-consensus`.
177
188
  5. Gate result resolution:
178
189
  - any `majority-disagree` item present AND `gating=true` → `blocked-by-disagreement`
@@ -207,7 +218,7 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
207
218
  - `Blocks=approval`
208
219
  - 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.
209
220
  - **A `planner-fixable` item that survives the self-fix loop is NOT promoted to the user by default.** A defect the *planner* could have fixed but did not is still a planner defect; promoting it asks the user to proofread the plan. Once the self-fix budget is exhausted, such an item is recorded as a Working Assumption in `## 5. Missing Information and Risks` — naming the defect, the assumption the implementation will proceed under, and the stop reason — and it stops blocking the gate (it folds into `passed-with-dissent`). It gets **no** `Blocks=approval` row and **no** `clarificationId`.
210
- - **Exception — correctness-critical defects still block.** An item whose `DISAGREE` kinds include `a` (cited path/symbol mismatch) or `d` (rollback violates commit/dependency order), or `f` on a `P-Req-*` item, is promoted per the rules above regardless of `fixability`. These are the defects that make `implementation` produce wrong or unsafe code; `b`/`c`/`e` degrade the plan document, not the resulting code. **Enforced:** `validators/validate-run.py` `_is_dissent_downgraded` (fold) + `_is_correctness_critical` (the exception).
221
+ - **Exception — correctness-critical defects still block.** An item whose `DISAGREE` kinds include `a` (cited path/symbol mismatch), or `f` on a `P-Req-*` item, is promoted per the rules above regardless of `fixability`. These are the defects that make `implementation` produce wrong or unsafe code; `b`/`c`/`e` degrade the plan document, not the resulting code. Rollback ordering (`d`) is advisory — a human runs the rollback — so it is never correctness-critical and never blocks. **Enforced:** `validators/validate-run.py` `_is_dissent_downgraded` (fold) + `_is_correctness_critical` (the exception).
211
222
  - When a correctness-critical `planner-fixable` item is promoted, 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.
212
223
  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.
213
224
 
@@ -319,10 +330,10 @@ verdict:
319
330
  - **AGREE**: The item is executable as written and internally consistent with
320
331
  other items in the plan.
321
332
  - **DISAGREE(<kind>)**: The item is broken. Cite which kind:
322
- (a) a concrete referenced file path / symbol contradicts a different concrete path / symbol for the same artifact elsewhere — a genuine mismatch; an abbreviated / ellipsis path is NOT (a), use (b),
333
+ (a) a concrete referenced file path / symbol contradicts a different concrete path / symbol for the same artifact elsewhere — a genuine mismatch; an abbreviated / ellipsis path is NOT (a), use (b); on a `P-Var-*` item, (a) takes a majority to block like (b)/(e), so raise a variation-point defect as (b) or (e),
323
334
  (b) command or referenced path is not executable or is ambiguous — including an abbreviated / ellipsis / under-specified path that does not resolve as written. A command that IS declared but cannot run here because build/test dependencies are not installed is NOT (b) — answer UNVERIFIABLE,
324
335
  (c) validation signal is not observable,
325
- (d) rollback violates commit / dependency order,
336
+ (d) rollback violates commit / dependency order — advisory only: a rollback is run by a human, so a DISAGREE(d), and any DISAGREE on a `P-Rb-*` rollback item, is recorded as dissent but never blocks approval,
326
337
  (e) item contradicts the trade-off matrix,
327
338
  (f) requirement coverage row does not map the stated requirement to a concrete satisfying option / stage / step — citing an existing option counts as concrete even if that option's paths are abbreviated (that is (b) on the option's item, not (f)).
328
339
  When you give a DISAGREE, also answer **Fixability** — `planner-fixable` if this defect can be fixed using only the code + this plan draft + the brief, `needs-user-input` if an open user clarification / external information is required.
@@ -291,7 +291,7 @@ Skipping this file because "the real report is in `reports/`" is wrong. Both fil
291
291
 
292
292
  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 10); 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.
293
293
 
294
- **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`.
294
+ **Verdict Card (top-of-report, mandatory).** Render `## Verdict Card` between the report header and the (conditional) Approval block. Its `Verdict Token` and `Direction` cells MUST byte-match the corresponding cells in `## 7. Final Verdict`. Its `Next Step`, the final-verdict next step, and the first item of `## 3. Recommended Next Steps` MUST name the same route target, though the actionable command and prose need not be byte-identical. Divergence is `contract-violated`.
295
295
 
296
296
  **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:
297
297
  - `motivation` — why we are doing this work (goal / context).
@@ -306,18 +306,19 @@ Every field MUST anchor its claim with at least one evidence reference — a `pa
306
306
  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.
307
307
  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.
308
308
  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.
309
+ - **Error-analysis diagnosis and routing.** When `header.taskType` is `error-analysis`, populate the required `errorAnalysis` object. Copy `errorAnalysis.symptomVerbatim` byte-for-byte from the symptom stated in the brief's `Source Material`; do not paraphrase it. Every `causeCandidates[]` row includes the full `supportingEvidence`, `falsifyingEvidenceChecked`, `confidence`, and `disproveWith` fields. Route `errorAnalysis.routing.nextTaskType=implementation-planning` with `direction=begin-planning`, or route `errorAnalysis.routing.nextTaskType=error-analysis` with `direction=continue-investigation`; no other pairing is valid. `verdictCard.nextStep`, `finalVerdict.nextStep`, the first `recommendedNextSteps` action and command, and the unique `followUpTasks` row whose `origin` is `phase-continuation` MUST all point to the same `errorAnalysis.routing.nextTaskType` target. The schema enforces only the presence of a `phase-continuation` row. Phase validation MUST enforce exact target agreement and uniqueness through `validators/validate-run.py::_validate_error_analysis_consistency`; until that check is implemented and executed, those semantics are contract requirements rather than enforced guarantees.
309
310
  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).
310
311
  4. **Follow-up Tasks** — auto-spawn-eligible table. Each row drives `okstra-spawn-followups.py`; see template §4 for the row schema.
311
312
  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.
312
313
  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.
313
- 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.
314
+ 7. **Final Verdict** — `Direction` ∈ `continue-investigation` / `begin-planning` / `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.
314
315
 
315
316
  **§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.
316
317
 
317
318
  ### Writing Guidelines
318
319
 
319
320
  - Write in Markdown. **Prefer tables over prose bullet lists** for any section that enumerates multiple items with the same shape (evidence rows, risks, options, dependencies, rollback steps, follow-ups, open questions). Bullets are reserved for short, single-line standalone statements (e.g., "- No additional information requested."). When the template provides a table form, do NOT degrade it back to bullets in the rendered report. **Exception — `## Background and Rationale` (`rationale`) is deliberately prose**: it is connected narrative explaining the *why*, not a same-shape enumeration, so write full sentences there rather than forcing it into a table.
320
- - **Do not restate the same conclusion verbatim across sections.** The Verdict Card and Reader Summary are *digests*: give the outcome in one or two sentences and point to `## 7. Final Verdict` / `## 5.8.8 Routing Recommendation` for the full reasoning, rather than copying their multi-clause conclusion word-for-word. Only the `Verdict Token` / `Direction` / `Next Step` cells must byte-match §7 (per the Verdict Card contract above); the prose conclusion must not be a duplicate.
321
+ - **Do not restate the same conclusion verbatim across sections.** The Verdict Card and Reader Summary are *digests*: give the outcome in one or two sentences and point to `## 7. Final Verdict` / `## 5.8.8 Routing Recommendation` for the full reasoning, rather than copying their multi-clause conclusion word-for-word. Only the `Verdict Token` and `Direction` cells must byte-match §7 (per the Verdict Card contract above). `Next Step` must point to the same routing target as §7, but its actionable command and prose need not be byte-identical. The prose conclusion must not be a duplicate.
321
322
  - **Keep each sentence to one main idea.** A single sentence that stacks four or five clauses with em-dashes and nested parentheticals (300+ characters) is hard to read, and the renderer can only line-break at sentence ends — so break such reasoning into separate sentences. Facts, evidence, and IDs still live in the tables; prose carries only the connective *why*.
322
323
  - Write the final report body in the language passed in **Report Language**
323
324
  above (`en` or `ko`). The template's fixed labels (section asides,
@@ -16,6 +16,7 @@ prompt (agents/workers/_cli-wrapper-template.md → Prompt Composition).
16
16
  Load the applicable coding conventions for every language the diff will touch, then state in ONE line which conventions apply (e.g. `Applying TS + hexagonal overlay; domain at src/domains/*/domain/`). Lint/test green is necessary but NOT sufficient — self-mocked tests, interaction-only assertions, and untruthful names all pass a green pipeline; this gate is what keeps them out of the diff.
17
17
 
18
18
  - **Resource selection — read the routed pack, never inline it here.** Use this worker prompt's `**Coding preflight pack:**` anchor header as the absolute path to the installed routed pack. Detect each touched file's language and framework from its extension or project manifest (`package.json`, `Cargo.toml`, `pyproject.toml`, `pom.xml`, `build.gradle*`, `prisma/schema.prisma`), then read that pack's resources via the Read tool by absolute path. Always read `overview.md` (the router) + `clean-code.md`, then select per the router's three ordered stages — Stage 1 language → `languages/<lang>.md`, Stage 2 framework → `frameworks/<fw>.md` (e.g. `frameworks/node-server.md` for server-side Node), Stage 3 architecture → `architectures/<arch>.md` (e.g. `architectures/hexagonal.md` for ports-and-adapters / NestJS-hex). Each stage is a list of rules; include EVERY matching resource (a change set can touch multiple languages/frameworks/architectures) — do not stop at the first match. These files are runtime resources, not Skill-tool skills, so always read them by path.
19
+ - **Declared architecture style — an authoritative Stage 3 input, and it binds.** Before selecting resources, read `<PROJECT_ROOT>/.okstra/project.json` and take `architecture.style`. A declared `hexagonal` selects `architectures/hexagonal.md` even when none of Stage 3's layout signals matched, so the declaration — not the directory shape — decides. A declared `layered` has no pack resource; its invariant applies from this line: dependencies run one direction only — an upper layer may import a lower one, never the reverse — and a variation point is extracted onto a layer boundary. A declared style makes this overlay binding rather than advisory, and which rule binds follows the style: under `hexagonal` the overlay's otherwise-advisory concrete-adapter item is blocking, so a service dependency you add or modify goes through a port instead of a concrete implementation and that placement violation is fixed before the write rather than recorded as a note; under `layered` what binds is the direction invariant just stated — your own judgement over the import list of every file the diff touches, plus extracting a variation point onto a layer boundary — while the concrete-adapter item stays advisory, since `layered` has no ports to route it through. An absent field, a `none` style, or an unreadable `project.json` changes nothing — Stage 3 stays detection-driven and its overlay stays advisory, leaving the language-agnostic principles below as the only always-binding layer. The verifier re-grades the same diff under the same declaration (`_implementation-verifier.md` → Static design & test-quality review), so a placement violation missed here returns as a verdict `FAIL`.
19
20
  - **Project review rule packs:** also look for project-local review skills in `<PROJECT_ROOT>/skills/*review*`, `<PROJECT_ROOT>/.claude/skills/*review*`, and up to two parent directories' `skills/*review*/SKILL.md`. Read the relevant `SKILL.md` plus referenced `references/*.md` files and apply their rules during implementation. This is a prevention pass, not a PR-comment generation workflow: do not dispatch reviewer subagents from the executor. For Fonts Ninja-style PR review packs, the executor must avoid newly introduced duplicate helper stacks, tautological tests that merely re-call the delegated helper, self-mocking, domain rules in adapters/ports, domain objects outside `domain/`, dead APIs, weak public names, and functions that fail the plain-English read.
20
21
  - **Language-agnostic principles that ALWAYS bind (the TDD loop MUST satisfy them):** (1) no self-mocking of the SUT — stub/spy only injected collaborators, never the subject's own methods; (2) behavioral assertions on outcomes (return value, state, persisted rows, events, boundary calls) — never `toHaveBeenCalled*` on an internal helper as the only/primary assertion; (3) truthful names — a `get*` / `find*` that writes/inserts, or a name encoding the caller's use-case (`*ForInit`) or hiding a domain rule (`findValid*`), is a defect; (4) single-purpose functions ≤50 effective lines, plain-English readability.
21
22
  - **Graceful degradation (codex / antigravity executor runtimes, or any runtime where the resolved coding-preflight pack files are absent or unreadable):** do NOT skip the gate — apply the agnostic principles above plus the project's own `CLAUDE.md` / `CONTRIBUTING` / formatter+lint config, and record `coding-conventions: resource-unavailable → applied <project rules + agnostic principles>` in the final report. Never claim a resource read that did not happen.
@@ -32,7 +32,9 @@ profile document.
32
32
  - `complete` → proceed normally.
33
33
  - `partial` → proceed; treat still-unmarked `intent-check:` / `conversion-block:` rows as the `skipped` branch.
34
34
  - `skipped` → do NOT silently infer the missing answers. Promote each unmarked `intent-check:` / `conversion-block:` row into this run's `## 1. Clarification Items` as `Kind=decision`. Use `Blocks=approval` in `implementation-planning`, where the row gates the `approved:` frontmatter flip; otherwise use `Blocks=next-phase`. The recommended answer is drawn from the brief's matching content and clearly labelled `reporter direct-confirmation recommended`.
35
- - `pending` (or field missing) ABORT analysis; render the Verdict Card with `Verdict Token = blocked` + `Direction = hold` and write a single `## Reporter Confirmation Required` block (no leading number) summarising which rows are pending. The `## 1. Clarification Items` table carries one row per pending item with `Blocks=approval` in `implementation-planning`, otherwise `Blocks=next-phase`. The operator must rerun `okstra-brief-gen` Step 6.5. Do NOT emit `## 0.` for this case — Section 0 is reserved for clarification-response carry-in only.
35
+ - Current-format `pending`, missing, or invalid values are rejected during task-bundle preparation, so they never reach a new run. A brief is current-format only when its frontmatter contains the `reporter-confirmations` key; its value, `type`, and `generator` are then validated together.
36
+ - Defensive upgrade path: if an already-prepared bundle contains `pending`, the context loader emits `REPORTER_CONFIRMATION_PENDING` and stops before worker or report-writer dispatch. It writes no final report. Regenerate the brief with `okstra-brief-gen` Step 6.5 and prepare a fresh run.
37
+ - A legacy brief with no `reporter-confirmations` field is not reinterpreted as `pending`; it keeps the compatibility path and proceeds without this reporter-confirmation gate.
36
38
  `[CONFIRMED <YYYY-MM-DD> → RC-N]` markers on `Open Questions` rows are the per-row signal that the reporter has answered; their answers live verbatim under `## Reporter Confirmations` in the brief.
37
39
  - `Source Material` is reporter-verbatim. Do NOT paraphrase, summarize, reorder, or restructure it. Quote it directly when needed.
38
40
  - `Related Task Graph` is the structured task-topology handoff. If the section is present and not `_(none)_`, read it before classification, diagnosis, candidate discovery, fan-out, or next-step routing. Preserve the edge direction exactly as written: `From` → `To` is load-bearing for `depends-on`, `blocks`, parent/child, follow-up, and split relations.