okstra 0.141.3 → 0.142.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 (39) 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 +4 -0
  5. package/package.json +1 -1
  6. package/runtime/BUILD.json +2 -2
  7. package/runtime/agents/workers/report-writer-worker.md +2 -1
  8. package/runtime/prompts/coding-preflight/architectures/hexagonal.md +3 -3
  9. package/runtime/prompts/coding-preflight/overview.md +1 -1
  10. package/runtime/prompts/lead/adapters/claude-code.md +2 -2
  11. package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
  12. package/runtime/prompts/lead/plan-body-verification.md +20 -9
  13. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -0
  14. package/runtime/prompts/profiles/_implementation-deliverable.md +3 -3
  15. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  16. package/runtime/prompts/profiles/_implementation-verifier.md +2 -1
  17. package/runtime/prompts/profiles/forbidden-actions.json +0 -1
  18. package/runtime/prompts/profiles/implementation-planning.md +7 -2
  19. package/runtime/prompts/profiles/requirements-discovery.md +7 -0
  20. package/runtime/python/okstra_ctl/clarification_items.py +99 -5
  21. package/runtime/python/okstra_ctl/paths.py +34 -7
  22. package/runtime/python/okstra_ctl/phase_cleanup.py +235 -0
  23. package/runtime/python/okstra_ctl/plan_items.py +38 -0
  24. package/runtime/python/okstra_ctl/wizard.py +16 -0
  25. package/runtime/python/okstra_ctl/worker_heartbeat.py +15 -5
  26. package/runtime/python/okstra_ctl/workflow.py +1 -1
  27. package/runtime/python/okstra_project/resolver.py +25 -0
  28. package/runtime/schemas/final-report-v1.0.schema.json +63 -4
  29. package/runtime/skills/okstra-run/SKILL.md +3 -1
  30. package/runtime/skills/okstra-setup/SKILL.md +3 -0
  31. package/runtime/skills/okstra-setup/references/project-config.md +47 -0
  32. package/runtime/templates/reports/final-report.template.md +25 -0
  33. package/runtime/templates/reports/i18n/en.json +12 -1
  34. package/runtime/templates/reports/i18n/ko.json +12 -1
  35. package/runtime/templates/reports/implementation-input.template.md +1 -2
  36. package/runtime/templates/reports/task-brief.template.md +1 -1
  37. package/runtime/validators/validate-run.py +143 -12
  38. package/src/cli-registry.mjs +10 -0
  39. 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,8 @@ 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 |
243
246
  | `run_context.py` | Per-task mutex, run context and run-input persistence; `consumers_mutex` helper for atomic `consumers.jsonl` writes |
244
247
  | `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
248
  | `consumers.py` | Append-only `consumers.jsonl` writer + reader — records which `implementation` runs consumed which `implementation-planning` stage |
@@ -264,6 +267,7 @@ Important modules:
264
267
  | `run_index_row.py` | single reference point for creating / slimming / hydrating a `~/.okstra` run-index row — runId SSOT, preserves projectId raw |
265
268
  | `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
269
  | `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) |
270
+ | `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
271
  | `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
272
  | `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
273
  | `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 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.141.3",
3
+ "version": "0.142.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.142.0",
3
+ "builtAt": "2026-07-31T06:10:23.526Z",
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
 
@@ -119,6 +119,7 @@ Rules (the schema enforces most of these — they are listed here so you know *w
119
119
  - 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
120
  - 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
121
  - 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).
122
+ - 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
123
  - 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
124
 
124
125
  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.
@@ -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.
@@ -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.
@@ -26,10 +26,10 @@ are collected and convergence finished. Phase 1-5 do not need it.
26
26
  - **style / lint / type-check results** — each check-only tool the verifier ran, its exit code, and the count of new findings attributable to lines this run introduced. When no tool is configured for a touched language, record the single line `no lint/style tool configured for <language>`,
27
27
  - any fix recommendations the verifier declined to apply.
28
28
  `Claude lead` synthesises a unified verdict but MUST preserve dissent — do not collapse opinions into one paragraph. External Tier 3 advisory results are excluded from this aggregate promotion and remain user-owned follow-up evidence. If any other verifier issued `FAIL` on a `Discrepancy` line, the synthesised verdict MUST be `FAIL` unless lead cites a concrete reproduction-time reason (committed flaky-test record, documented environment delta) for overriding.
29
- - **Rollback verification**: confirmation that the plan's rollback path is still valid after the changes. Strength of verification depends on the change category:
29
+ - **Rollback verification** (advisory never blocks): a human-facing record of whether the plan's rollback path is still valid after the changes. A rollback is executed by a human, not by an okstra worker/verifier, so nothing here blocks the run, forces a `contract-violated` outcome, or routes back to planning. Each `rollbackVerification` row's `result` is `ok` (verified), `not-applicable` (nothing to roll back), or `advisory — human-run` (could not verify here; a human owns it). Strength of the record depends on the change category:
30
30
  - **Pure code changes** (no persisted state, no infra mutation): a reachable revert SHA is sufficient. Record the exact `git revert <SHA>` command that would undo the change, and confirm `git rev-parse <SHA>` resolves.
31
- - **Feature-flag-gated changes**: confirm the off-switch path was exercised in this run's validation evidence (i.e. one of the validation commands ran with the flag off and succeeded). A plan that ships a flag without exercising the off-path does NOT satisfy this requirement.
32
- - **Schema migrations, config-format changes, or any change with persisted state**: a **dry-run of the rollback step is mandatory**, not preferred. Record the exact rollback command and its captured exit code / stdout. If the migration tool offers no dry-run mode (`--dry-run`, `--plan`, equivalent), the executor MUST refuse to claim rollback verification and instead end the run with a routing recommendation back to `implementation-planning` for a safer rollback strategy. Skipping this step on a stateful change is treated as a `contract-violated` outcome by `final-verification`.
31
+ - **Feature-flag-gated changes**: prefer confirming the off-switch path was exercised in this run's validation evidence (i.e. one of the validation commands ran with the flag off and succeeded). If the off-path was not exercised here, record the row as `advisory — human-run` rather than treating it as a blocking requirement.
32
+ - **Schema migrations, config-format changes, or any change with persisted state**: record the exact rollback command and, when the migration tool offers a dry-run mode (`--dry-run`, `--plan`, equivalent), its captured exit code / stdout. The rollback itself is a **human-run operation**, not an okstra worker or verifier step — so an unavailable dry-run is documented as such (advisory) and does NOT block the run, does NOT produce a `contract-violated` outcome, and does NOT route back to `implementation-planning`. The deliverable here is recording the revert path clearly enough that a human can act on it.
33
33
  - **Manual user test draft**: when this run produces a user-observable change (UI / API / CLI / artifact), write `target / environment / steps / expected result` per change into §5.7.9 (data field `implementation.manualUserTest`, `applicable=true` with `items`). Treat effective `manual-user-test` PREP content only as a seed: reconcile it with the approved plan's `Acceptance:` and this run's actual diff before writing the final steps. Environment line: if the project has a `docker-compose.yml`, use `/okstra-container-build` (which runs `okstra container up <task-id>`) then connect to the published port; otherwise the project's run command (e.g. `npm start`). When there is no user-observable change, set `applicable=false` and give a one-line `exemptionReason` instead of items. These are the steps a human (or `final-verification`) re-runs by hand, NOT the automated validation commands in `Validation evidence`; planning PREP is never a second final-verification manual-test source.
34
34
  - **Design preparation handoff**: `implementation.manualUserTest` remains the only manual-test handoff to final-verification. For each effective non-`manual-user-test` item whose `reviewAt.phase` is `final-verification` and whose question remains unresolved after inspecting the actual diff, add one common `missingInformation` row. Set `source` to `design-prep:<PREP-ID>:<assessment-fingerprint>`, put `ifStillOpen` and the unresolved question in `item`, and put the guardrails plus risk in `risk`. Do not introduce an implementation-only PREP schema.
35
35
  - **External QA advisory handoff:** for every external Tier 3 result that did
@@ -31,7 +31,7 @@ Do not scan holistically and stop when it "looks fine". Work the matrix exhausti
31
31
  - a file that wraps a third-party / library call → `clean-code.md` "Wrapping a third-party call": the wrapper adds behaviour the library does not already provide (read the installed library source before keeping a recovery branch — a `catch` repeating the library's own retry recovers nothing), no comment names a condition the call site does not establish, and a rethrow keeps the original error as `cause`.
32
32
  - a file that decides, mutates, or persists state → `clean-code.md` "Mutation and state boundaries": decide on the direct identifier rather than a status/flag proxy, capture before-state in one snapshot ahead of the mutating boundary, update only this work's owned fields on an existing row, re-read state before calling a zero-affected-rows write success or failure, put priority-between-inputs in a named domain function, and keep error messages to what was actually observed. Also check that no state union/enum was re-declared beside an authoritative one the domain or a dependency exports.
33
33
  - test file (`*.spec.*` / `*.test.*` / `test_*.py` / `*_test.go` …) → `clean-code.md` "Testing discipline": no self-mocking of the SUT, behavioral (outcome) assertions not interaction-only, no tautological delegation assertion, no effect claimed under its own mock, shared-fixture defaults left on the ordinary path, setup values that actually separate the scenarios, every new test helper/mock used by a test in this same diff, no positional mock-argument access (`rg 'mock\.calls'`), every branch this diff adds covered by a test that fails when the branch body is deleted, assertions on the last write to a record rather than an intermediate one, and test titles naming their unit plus the single condition each case isolates.
34
- - port / adapter / domain file, when the hexagonal overlay is loaded → `architectures/hexagonal.md`: no business logic in a port body, adapter methods are I/O only (no post-fetch filtering on domain state, no `findValid*`/`findActive*` names hiding a rule), domain objects declared under the domain boundary, no changed domain file importing an ORM / framework / adapter / service (read the import list — mechanical), and a service dependency you add or modify goes through a port rather than a concrete adapter (advisory: record it with the port sketch; the codebase already injecting concrete classes is the debt this pays down, not a reason to skip it).
34
+ - port / adapter / domain file, when the hexagonal overlay is loaded → `architectures/hexagonal.md`: no business logic in a port body, adapter methods are I/O only (no post-fetch filtering on domain state, no `findValid*`/`findActive*` names hiding a rule), domain objects declared under the domain boundary, no changed domain file importing an ORM / framework / adapter / service (read the import list — mechanical), and a service dependency you add or modify goes through a port rather than a concrete adapter (advisory only while the project has not declared `architecture.style = hexagonal` in `.okstra/project.json` — record it with the port sketch; blocking under that declaration, so fix it in place before the commit rather than record-and-pass, exactly as `_implementation-verifier.md` re-grades this same diff. Either way, the codebase already injecting concrete classes is the debt this pays down, not a reason to skip it).
35
35
  A file can hold several roles — apply every rule set that fits it.
36
36
  3. **Decide clean-or-finding for each cell.** Read the full file when a rule needs context (never judge a port/adapter/domain or a naming rule from the hunk alone).
37
37
  4. **Fix each finding in place** before the commit. When a readability finding is real, the fix is a named helper or named intermediate value — sketch the cleaner shape (a few lines) in your audit note, then apply it. When the fix is genuinely out of this stage's scope, record it as an `Out-of-plan` note instead of silently leaving it.
@@ -111,6 +111,7 @@ Re-running commands proves the diff *builds and passes*; it does NOT prove the d
111
111
  - **Scope (no silent sampling).** Enumerate every changed source/test file via `git diff --name-only <base>...HEAD` and review each one. Skipping a changed file silently is a `contract-violated` outcome. If a file's language has no reference and is not covered by the agnostic checks below, record `design-review skipped: <file> (language=<x> no reference)` — never pass it silently.
112
112
  - **Load the same conventions the executor used via the routed pack.** Use this worker prompt's `**Coding preflight pack:**` anchor header as the absolute path to the installed routed pack. Read `overview.md` first, then `clean-code.md`, then apply the router's three ordered stages: language, framework, architecture. In each stage, iterate every rule, treat a rule as matched when any listed condition is true, and accumulate every matching resource — including `frameworks/node-server.md` for server-side Node work and `architectures/hexagonal.md` for ports-and-adapters / NestJS-hex layouts. Degrade to the agnostic checks below when the resolved pack is unreadable, and record either `coding-conventions: resources=<...>` or `coding-conventions: resource-unavailable → applied <project rules + agnostic principles>`. The verifier does NOT inline language rules — it loads the same situation-specific resources as the executor preflight.
113
113
  - **Load project review rule packs when present.** Search the project root, `.claude/skills`, and up to two parent `skills/` directories for `*review*/SKILL.md` rule packs. Read their referenced `references/*.md` files and apply them as an overlay on this static review. If a premium review skill exists, use its coverage philosophy (recall-first enumeration followed by verify-only confirmation) as the verifier's mental model, but do NOT dispatch extra reviewer agents unless the task explicitly configured them. Record `project-review-rules: <paths read>` or `project-review-rules: none found` in the worker result.
114
+ - **Declared architecture style promotes the placement overlay from advisory to binding.** Read `<PROJECT_ROOT>/.okstra/project.json` — the same file Tier 2's `qaCommands` comes from — take `architecture.style`, and record `architecture-style: <hexagonal|layered|none>` in the worker result next to the `coding-conventions:` line. A declared `hexagonal` counts the overlay as loaded even when none of the router's Stage 3 layout signals matched, so the **Hexagonal** blocking check below applies in full, and the concrete-adapter injection listed under Advisory findings is promoted to a blocking finding → verdict `FAIL`, not a `should-fix`. A declared `layered` has no pack resource; its binding invariant is direction — an upper layer may import a lower one, never the reverse — so a changed file whose import list reaches back up a layer, or around a layer boundary, is a blocking placement violation cited `path:line` from that import list. The `layered` half is worker judgement: no machine check reads layer names, so a missed reverse dependency is a missed finding, not a validator failure. A `none` style, an absent field, or an unreadable `project.json` leaves this section exactly as it is today — Stage 3 stays detection-driven and the placement items stay advisory. **Enforced:** `scripts/okstra_project/resolver.py` `resolve_architecture` reads this same field for the planning-side rule in `validators/validate-run.py` `_validate_variation_point_analysis`, and `_validate_verifier_fail_blocks_verdict` (cited under the DB gate below) keeps the resulting `FAIL` from being dropped during synthesis.
114
115
  - **Blocking checks (any hit → verdict `FAIL`, cited `path:line` + rule name, recommended fix recorded — the verifier does NOT apply it):**
115
116
  - **New duplication / DRY:** two or more newly added or meaningfully modified blocks implement the same helper stack, transform, or domain rule. Literal copy-paste is always blocking; semantically equivalent transforms across services are blocking unless the approved plan explicitly justified keeping them separate. Recommend the shared module location.
116
117
  - **Self-mocking:** a test for `Foo` stubs/spies a method on the `Foo` instance under test (`jest.spyOn(sut, ...)`, `spyOn(FooService.prototype, ...)` in `foo.*.spec.*`, `vi.mocked(sut)` + stub). Mocking injected collaborators is fine.
@@ -129,7 +130,7 @@ Re-running commands proves the diff *builds and passes*; it does NOT prove the d
129
130
  - **Positional mock-argument access:** `mock.calls[<n>][<m>]` (or the framework equivalent) used to inspect arguments instead of an intent-revealing `toHaveBeenCalledWith` / explicit-absence assertion. Detect with `rg 'mock\.calls'` over the changed test files.
130
131
  - **Non-separating test data:** two scenarios whose setup values and assertions are identical, so a wrong implementation passes both; or new test tooling added in this diff (mock, state setter, repository branch) that no test calls.
131
132
  - **Effect claimed under its own mock:** a test presented as covering an effect whose producing path is replaced by a mock inside that same test. The mock's presence in the harness is not evidence the branch behind it works.
132
- - **Advisory findings (recorded as recommendations; verdict MAY still PASS):** function >50 effective lines, a single body mixing read+write stages, weak readability, a missing-but-non-critical outcome assertion, newly orphaned private/public code that is safe to remove but not on a critical path, weak-but-not-misleading names, priority-between-inputs policy inlined in a service condition instead of a named domain function, an error message asserting a cause the code never observed, a memory / concurrency / batching change with no test pinning the bound it claims, or (hexagonal overlay only) a service dependency this diff adds or modifies that injects a concrete adapter instead of a port — record it with the port sketch; an existing convention of concrete injections does not convert this one to `clean`, it is the debt the rule pays down. These land in the verifier result as `should-fix` / `nit` recommendations, not as a `FAIL`.
133
+ - **Advisory findings (recorded as recommendations; verdict MAY still PASS):** function >50 effective lines, a single body mixing read+write stages, weak readability, a missing-but-non-critical outcome assertion, newly orphaned private/public code that is safe to remove but not on a critical path, weak-but-not-misleading names, priority-between-inputs policy inlined in a service condition instead of a named domain function, an error message asserting a cause the code never observed, a memory / concurrency / batching change with no test pinning the bound it claims, or (hexagonal overlay only) a service dependency this diff adds or modifies that injects a concrete adapter instead of a port — record it with the port sketch; advisory only while the project has not declared `architecture.style = hexagonal`, since that declaration — and only that one — promotes exactly this item to blocking per the declared-style bullet above, while a declared `layered` binds dependency direction instead and leaves this item advisory; an existing convention of concrete injections does not convert this one to `clean`, it is the debt the rule pays down. These land in the verifier result as `should-fix` / `nit` recommendations, not as a `FAIL`.
133
134
  - **Output.** Every finding — blocking or advisory — is a structured item in the verifier's worker result (`path:line`, rule, severity, suggested fix) so it carries into Phase 5.5 convergence and the final report. A blocking hit sets the verifier verdict to `FAIL` with the rule cited, using the same verdict machinery as the Discrepancy rule above. `Claude lead` MUST NOT silently downgrade a cited blocking finding to advisory during synthesis; an override requires a concrete cited reason, exactly as for the Discrepancy rule.
134
135
 
135
136
  ### Fix-run incremental scope (applies when the profile carries a "Fix-Run Carry" block)
@@ -40,7 +40,6 @@
40
40
  "silent scope expansion: every file edited outside the approved plan list MUST appear in the `Out-of-plan edits` block with rationale",
41
41
  "leaving placeholders such as TBD / TODO / \"implement later\" / \"handle edge cases\" in newly-added lines of this run (check via `git diff <base>..HEAD | grep -E '^\\+[^+].*\\b(TBD|TODO|FIXME|XXX|implement later|handle edge cases|similar to|placeholder)\\b'`; pre-existing strings in untouched regions are out of scope)",
42
42
  "lead substituting its own verdict when every verifier present in the resolved roster (`Claude verifier`, `Codex verifier`, plus `Antigravity verifier` when opted in) returned a non-result terminal status (`timeout`/`error`/`not-run`); in that case the run MUST end as `blocked` with routing recommendation back to `error-analysis`, never with a lead-only verdict",
43
- "claiming rollback verification on a schema migration, config-format change, or any persisted-state mutation without a recorded dry-run of the rollback step and its captured exit code",
44
43
  "declaring overall task acceptance — that is `final-verification` ownership; this phase reports only \"ready for final-verification\" or \"needs new planning loop\"",
45
44
  "delegating the self-review pass to a generic subagent — `Claude lead` must run it"
46
45
  ],
@@ -22,6 +22,7 @@
22
22
  - read the task brief, related-task briefs, and any cited spec / design doc end-to-end
23
23
  - inspect the current state of every file the task names (or the closest matching files if names are stale) — record current responsibilities, public interfaces, and known coupling points
24
24
  - skim recent commits touching those files (`git log -- <path>`) to surface in-flight work or contested areas
25
+ - **sibling exploration (variation-point evidence)**: read the `Related Task Graph` sibling / `related-to` tasks' done artifacts *and the code they actually landed* — a done report is a claim, the diff is the fact. When a sibling already implements the same behavior for another resource, that is a variation point: register the existing implementation in `variationPointAnalysis.evidence` and weigh an option that refactors it behind a shared interface instead of adding a second parallel implementation alongside it. Absent an explicit graph edge, still surface a same-behavior implementation you saw in the files or `git log` output already inspected above — an unrecorded edge does not make the duplication less real.
25
26
  - **codebase-first ambiguity resolution**: any ambiguity that can be answered by `Read` / `Grep` MUST be resolved that way and recorded with file:line evidence. Only ambiguities that genuinely require a human decision are escalated as `Clarification Items` rows. Writing a clarification row for something the code already answers is a defect of this phase.
26
27
  - flag any requirement that is ambiguous, contradictory, or missing success criteria — register each one as a row in the report's `## 1. Clarification Items` table with `Blocks=approval` instead of guessing
27
28
  - read `<PROJECT_ROOT>/.okstra/glossary.md` and `<PROJECT_ROOT>/.okstra/decisions/` titles if present. Absent okstra memory files are the normal state — do not error. Treat the brief's `terminology:*` resolutions from `requirements-discovery` (if any) as authoritative; if missing, resolve any remaining fuzzy term as a `Blocks=approval` clarification row.
@@ -37,6 +38,7 @@
37
38
  - **Isolation & single responsibility**: each unit touched should have one clear purpose, well-defined interface, and be independently testable. Penalize options that widen a unit's responsibility.
38
39
  - **Files that change together live together**: split by responsibility, not by technical layer. Penalize options that scatter one logical change across unrelated layers.
39
40
  - **Follow established patterns**: in existing codebases, conform to current conventions. Targeted cleanup of a file you are already modifying is acceptable; unrelated refactors are not.
41
+ - **Variation-point extraction (OCP)**: when the same behavior is served by two or more resources / implementations — stated in the brief, or foreseeable from a sibling task or the code you inspected — the plan MUST record it in `variationPointAnalysis` and include an option that extracts the variation point behind an interface (a port, or a strategy the next implementation plugs into), scored against the non-extracted option in the trade-off matrix. Penalize an option that branches on resource identity inside a service (one `if` / `switch` arm per implementation): adding the next implementation then means editing that same call site again, which is the closed-for-extension shape this principle exists to catch. This does not contradict YAGNI below: YAGNI drops *speculative* variation (a second implementation nobody named), while a behavior with two implementations already on the table is a present fact, not a forecast. **Enforced:** the `variationPointAnalysis` bullet under `Required deliverable shape` names the schema / validator / `P-Var-*` enforcement points.
40
42
  - **YAGNI ruthlessly**: drop features, abstractions, and configuration knobs that do not serve the stated requirement.
41
43
  - **Project review-rule preflight**: before choosing the recommended option, look for project-local review rule packs such as `<PROJECT_ROOT>/skills/*review*`, `<PROJECT_ROOT>/.claude/skills/*review*`, and up to two parent directories' `skills/*review*/SKILL.md`. If present, read the relevant `SKILL.md` plus referenced `references/*.md` files and treat their rules as planning constraints. Do not run the PR-review workflow here; extract only the rules. For Fonts Ninja-style TS/NestJS review packs, this means planning away known review findings before code exists: shared transforms instead of duplicate helper stacks, behavioral tests instead of collaborator-tautology assertions, domain rules in domain modules rather than repositories/adapters, domain objects under `domain/`, plain-English functions, truthful/specific names, and no dead APIs introduced by the plan.
42
44
  - Expected output emphasis:
@@ -57,7 +59,7 @@
57
59
  - The YAML frontmatter `approved: true|false` field is the only authorised approval gate. report-writer always emits `approved: false`. The user clears it either by (a) editing the frontmatter line to `approved: true` directly, or (b) invoking the next phase with `--approve` so the CLI flips the frontmatter on the user's behalf. `okstra_ctl.run._validate_approved_plan` reads this field and refuses entry until it is `true`.
58
60
  - Cross-verification mode:
59
61
  - Phase 5.5 finding convergence runs in **adversarial mode** for this phase (`convergence.adversarial=true`). Verifiers actively try to refute each worker finding (requirement gap / risk / option) by re-inspecting its cited evidence; the burden of proof sits on the claim. See `prompts/lead/convergence.md` §"Adversarial Verification Mode".
60
- - §5.5.9 plan-body verification runs with an **adversarial posture** (`prompts/lead/plan-body-verification.md` §"Adversarial plan-body posture"): verifiers open and confirm every cited path / command and put the burden of proof on the plan. The gate threshold is majority-based for kinds `b`/`c`/`e`, but a single `DISAGREE` blocks on its own for the concrete, safety-critical kinds `a` (path/symbol mismatch) / `d` (rollback order) — and `f` on `P-Req-*` items. A majority also needs ≥2 participating votes, so a lone dissent whose peer returned a non-result does not block on a majority-gated kind (see that contract's §"Adversarial plan-body posture").
62
+ - §5.5.9 plan-body verification runs with an **adversarial posture** (`prompts/lead/plan-body-verification.md` §"Adversarial plan-body posture"): verifiers open and confirm every cited path / command and put the burden of proof on the plan. The gate threshold is majority-based for kinds `b`/`c`/`e`, but a single `DISAGREE` blocks on its own for the concrete, safety-critical kind `a` (path/symbol mismatch) — and `f` on `P-Req-*` items. `P-Var-*` items are excepted from the kind-`a` exception: a variation-point defect takes a majority. Rollback ordering (`d`) is advisory and never blocks the gate — a rollback is executed by a human, not by okstra's workers or verifiers. A majority also needs ≥2 participating votes, so a lone dissent whose peer returned a non-result does not block on a majority-gated kind (see that contract's §"Adversarial plan-body posture").
61
63
  - **Incremental re-verification scope (clarification re-runs):** when the lead's `okstra incremental-scope` decision is `mode == "incremental"` (procedure in `prompts/launch.template.md` §"Clarification Response Carried In"), workers re-analyze ONLY the stages listed in `reverify_stages` (the downstream closure of the impacted stages). Workers MUST NOT re-open, re-score, or re-judge any stage in `carry_stages` — those stages' prior plan-item verdicts are carried forward verbatim, and a worker never overwrites a carried verdict with its own judgement. When the decision is `mode == "full"` (the default), every stage is re-analyzed as usual.
62
64
  - **Single incremental-scope decision:** the lead calls `okstra incremental-scope` exactly once for the re-run, passing the answered `C-NNN` ids through `--answered-clarifications`, changed design-preparation IDs through `--prep-items`, and any lead-resolved stage numbers through `--impacted`; the CLI unions all three before applying the existing dependency closure and cutoff. The clarification ids are resolved to stages by the CLI from the prior report's own `planItems[].clarificationId` and `blocked C-NNN` coverage links — the lead does not map answers to stage numbers. An answer that changes the selected option, Stage Map, or recommended approach is not a local impact: pass every CSV empty so the same call returns `mode == "full"`. A clarification id that traces to no stage, unknown PREP IDs, or invalid `stageRefs` also return an explicit full decision instead of being guessed.
63
65
  - **Stage-aware carry:** for an incremental decision, pass its `carry_stages` and `reverify_stages` CSVs unchanged to `okstra incremental-carry`. The helper carries the prior whole stage rows and their owned PREP / `P-Prep-*` artifacts; overlap, cross-scope ownership, scope leaks, or canonical conflicts return `CarryError`. On that error, discard the partial merge and run full re-verification.
@@ -93,6 +95,8 @@
93
95
  - estimated blast radius (units, configs, deployment manifests, data migrations)
94
96
  - trade-off matrix across options (rows = options, columns at minimum: complexity, risk, reversibility, test coverage cost, rollout cost)
95
97
  - recommended option with rationale tied to the design principles above
98
+ - **Variation-point analysis (`variationPointAnalysis`, mandatory — every plan emits the block, rendered as §5.5.11):** declare `hasMultipleImplementations`, and when it is `true`, 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 it), and an `extractionDecision` of `extract` / `interfaceKind` / `coveredBy` (the Stage Map stage that builds the interface) / `rationale`. **A `false` declaration is not an omission — it is a claim**, so it carries a written `noVariationRationale` and an empty `points` array; the two are mutually exclusive, because declared points would be silently dropped from verification under a `false` header. A project whose `.okstra/project.json` sets `architecture.style: hexagonal` extracts a point as a port (`interfaceKind: "port"`), never as a shared helper. **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.VariationPointAnalysis` / `$defs.VariationPoint` make the block required and pin the row shape; `validators/validate-run.py` `_validate_variation_point_analysis` rejects a `false` declaration with no rationale, a `false` declaration carrying points, a `true` declaration with no point, an `extract: true` decision naming no `interfaceKind` or no `coveredBy`, and a hexagonal project extracting as anything but a port; and every point becomes a `P-Var-<N>` plan item judged in §5.5.9 (`prompts/lead/plan-body-verification.md`) — a plan declaring no variation point is still verified, through the lone `P-Var-0`.
99
+ - **Test seams (`recommendedOption.testSeams`, mandatory array):** one row per boundary a test injects at and replaces — `boundary` (what the seam sits on), `injectedAs` (the concrete construction / wiring point a test substitutes at), `replacedInTest` (what the test puts there instead). An **empty array is legal but is itself a claim**: "this plan needs no seam." Its absence is what forces self-mocks — with no declared injection point the implementation ends up mocking the unit against its own re-implementation, and those tests pass regardless of whether the behavior is right. A row whose `injectedAs` names no construction or wiring point a test can actually replace is a seam on paper only. **Enforced:** `schemas/final-report-v1.0.schema.json` makes `testSeams` required on `recommendedOption` and requires all three row fields; the §5.5.9 `P-Var-*` round DISAGREEs when a declared seam (or an `extractionDecision.coveredBy`) is not actually injectable.
96
100
  - **Working Assumptions (non-blocking)**: inside the `Recommended Option` section, an `Assumptions:` labelled list recording each assumption the plan proceeds on **without** user confirmation but that does NOT block approval — a sensible default the reviewer can still veto (e.g. "assuming the existing retry policy stays; not re-tuning it here"). This is distinct from `## 1. Clarification Items`, which gates approval: anything that must be answered before coding is a clarification row, never an assumption. Omit the list only when there are genuinely none. (No new scanned heading — it lives under the existing `Recommended Option`. The `Authority & permissions` class from the shared contract is explicitly NOT recorded here — that class is suppressed, not surfaced.)
97
101
  - **Stage Map (mandatory — always emitted, even when N=1):** a table of all stages with `stage | title | depends-on | step-count | exit-contract-summary`. `depends-on` is `(none)` or a comma-separated stage number list. Stages with `depends-on (none)` can be implemented in parallel by two simultaneous `implementation` runs.
98
102
  - **Keep the table at exactly 5 columns** — do NOT add a column. `validators/validate-implementation-plan-stages.py` parses `stage | title | depends-on | step-count | exit-contract-summary` and silently skips any row that is not exactly 5 cells, so a 6th column would drop every stage and bypass S2–S11.
@@ -163,7 +167,7 @@
163
167
  ```
164
168
 
165
169
  An `AGREE` note records the counterexample considered and its exclusion reason. If the judgement needs unavailable external material, record `verification-error`, not `DISAGREE`. **Enforced:** `validators/validate-run.py` `_validate_plan_item_extraction_completeness` compares the exact deterministic set, independently rejecting missing, unexpected, and duplicate plan-item IDs, including `P-Prep-*`.
166
- - **§5.5.9 Plan Body Verification (BLOCKING).** After report-writer finishes the draft, the lead MUST run a worker peer-review round on the consolidated plan body (Option Candidates / Trade-off Matrix / Recommended Option / Stage Map and per-stage sections / Dependency / Validation Checklist / Rollback / Requirement Coverage) and populate `### 5.5.9 Plan Body Verification` in the final report. The round protocol, plan-item ID scheme (`P-Opt-*` / `P-Step-*` / `P-Dep-*` / `P-Val-*` / `P-Rb-*`), verdict semantics, gate-result classification, and dissent log format are defined in `prompts/lead/plan-body-verification.md`. The four gate-result values are `passed`, `passed-with-dissent`, `blocked-by-disagreement`, `aborted-non-result`. When the gate would have been `blocked-by-disagreement` or `aborted-non-result`, the lead MUST NOT silently flip it to one of the passing values to "unblock" the run — that is a contract violation. **Enforced:** `validators/validate-run.py` `_validate_plan_body_gate_recompute` re-derives the gate from `planItems[].verdicts` and fails when the declared `gateResult` claims a healthier outcome than the recorded votes support; `_validate_plan_item_extraction_completeness` fails when any plan-body deliverable category is under-extracted into `planItems`, so a dropped item can no longer dodge the gate. When `convergence.adversarial=true` (the default for this phase), this round uses the adversarial posture — verifiers confirm cited paths/commands and the burden of proof is on the plan — but the gate threshold stays `majority-disagree` (see that skill's §"Adversarial plan-body posture"). Among the majority-disagree items, those that are majority-`planner-fixable` go through report-writer self-fix rounds (`prompts/lead/plan-body-verification.md` "Self-fix round"). A `planner-fixable` item that survives the budget is **not** promoted to the user — it becomes a Working Assumption in `## 5. Missing Information and Risks` and folds into `passed-with-dissent`, because a defect the planner could have fixed is not a user decision. Only majority-`needs-user-input` items, and correctness-critical defects (`DISAGREE` kinds `a` / `d`, or `f` on `P-Req-*`) regardless of fixability, become `Blocks=approval` clarifications. `validators/validate-run.py` `_validate_self_fix_before_clarification` fails a planner-fixable majority item promoted without a self-fix as `contract-violated`.
170
+ - **§5.5.9 Plan Body Verification (BLOCKING).** After report-writer finishes the draft, the lead MUST run a worker peer-review round on the consolidated plan body (Option Candidates / Trade-off Matrix / Recommended Option / Stage Map and per-stage sections / Dependency / Validation Checklist / Rollback / Requirement Coverage) and populate `### 5.5.9 Plan Body Verification` in the final report. The round protocol, plan-item ID scheme (`P-Opt-*` / `P-Step-*` / `P-Dep-*` / `P-Val-*` / `P-Rb-*`), verdict semantics, gate-result classification, and dissent log format are defined in `prompts/lead/plan-body-verification.md`. The four gate-result values are `passed`, `passed-with-dissent`, `blocked-by-disagreement`, `aborted-non-result`. When the gate would have been `blocked-by-disagreement` or `aborted-non-result`, the lead MUST NOT silently flip it to one of the passing values to "unblock" the run — that is a contract violation. **Enforced:** `validators/validate-run.py` `_validate_plan_body_gate_recompute` re-derives the gate from `planItems[].verdicts` and fails when the declared `gateResult` claims a healthier outcome than the recorded votes support; `_validate_plan_item_extraction_completeness` fails when any plan-body deliverable category is under-extracted into `planItems`, so a dropped item can no longer dodge the gate. When `convergence.adversarial=true` (the default for this phase), this round uses the adversarial posture — verifiers confirm cited paths/commands and the burden of proof is on the plan — but the gate threshold stays `majority-disagree` (see that skill's §"Adversarial plan-body posture"). Among the majority-disagree items, those that are majority-`planner-fixable` go through report-writer self-fix rounds (`prompts/lead/plan-body-verification.md` "Self-fix round"). A `planner-fixable` item that survives the budget is **not** promoted to the user — it becomes a Working Assumption in `## 5. Missing Information and Risks` and folds into `passed-with-dissent`, because a defect the planner could have fixed is not a user decision. Only majority-`needs-user-input` items, and correctness-critical defects (`DISAGREE` kind `a`, or `f` on `P-Req-*`) regardless of fixability, become `Blocks=approval` clarifications. Rollback ordering (`d`) is advisory and never blocks approval — a rollback is executed by a human. `validators/validate-run.py` `_validate_self_fix_before_clarification` fails a planner-fixable majority item promoted without a self-fix as `contract-violated`.
167
171
  - **Decision-record evaluation (sole owner)**: this phase is the **single owner** of decision-record evaluation in the okstra lifecycle. The brief never evaluates or drafts decision records — it only forwards `adr-candidate:*` signals. Every `adr-candidate:*` entry inherited from the brief's `Open Questions` is a mandatory evaluation target. In addition, evaluate every decision the recommended option introduces against the three criteria:
168
172
  1. **Hard to reverse** — would changing the decision later cost meaningfully more than deciding now?
169
173
  2. **Surprising without context** — would a future reader, seeing only the code, wonder "why was it built this way?"?
@@ -187,3 +191,4 @@
187
191
  8. **Stage Map self-check** — for every stage, count the effective rows of its `Stepwise Execution Order` table by hand; reject the draft if any stage exceeds 8. Confirm each stage declares a non-empty `Slice value:` and `Acceptance:` line, the three `Test case (success|boundary|failure):` lines (or carries a `TDD exemption:` line), and that its first step `action` starts with `RED:` with a later `GREEN:` — this is what validator S10 enforces, including S10d on the test-case lines. Read each stage's three test-case lines as a reviewer: reject any that restates the happy path in all three slots, leaves `boundary` blank, or writes `N/A` where a real edge input exists. Walk the `depends-on` graph and confirm it is a DAG (no cycle, no self-reference). For each `depends-on` link, confirm it encodes a real data/contract dependency — do NOT add links to serialise unrelated work, and do NOT split a stage merely to create more parallel stages. **Parallel-safety:** for every pair of `depends-on (none)` stages, confirm their `Stage Exit Contract` predicted file sets are disjoint; if they share a file, merge them or add a `depends-on` link (validator S9 rejects overlap). **Project-boundary:** confirm no stage mixes edits from two projects (different repo/`PROJECT_ROOT` or different top-level deployable module); if any stage does, split it per project. For multi-project plans, confirm each stage's `title` carries its `[<project>]` tag and the `Cross-project parallelism:` line under the table records the parallel-vs-sequenced determination (with the forcing dependency) for every project pair; for cross-repo work, confirm it is split into separate per-repo runs (required — one run structurally cannot touch another repo) rather than crammed into one task's stages.
188
192
  9. **Cross-project dependency check** — confirm you have not missed a dependency on another repo / another top-level deployable module / a published package. If `dependencyMigrationRisk` has a `kind: cross-project` row, confirm a matching `direction: upstream-precondition` `XP-NNN` row exists in `crossProjectDependencies`, and re-read as a reviewer whether its `requiredWork` is the concrete work the other side must actually build rather than an abstract phrase ("other side's work done") — validator S only checks existence, so concreteness is the self-review's responsibility. Confirm cross-repo work is split into a separate run + XP row instead of being crammed into one task's stages, and that the cross-project substance is not duplicated in `§3 Recommended Next Steps` but lives only in `§5.4 Cross-Project Dependencies`.
189
193
  10. **Decision-draft materialization check** — when `decisionDrafts` is non-empty, confirm as a reviewer which stage's stepwise order contains the matching materialization step (creating `.okstra/decisions/<NNNN>-<slug>.md`) and that the number of drafts corresponds 1:1 with the materialization steps. The validator only checks the *existence* of the step, so the `<NNNN>-<slug>` correctness and count correspondence are the self-review's responsibility.
194
+ 11. **Variation-point & seam check** — read `variationPointAnalysis` as a skeptic. Is `hasMultipleImplementations` honest against the brief and the sibling code you inspected during pre-planning, or was `false` chosen because it is the cheaper field to fill? For every point with `extract: true`, confirm the `extractionDecision` names a real interface (a `port` for a hexagonal project, not a shared helper) and a `coveredBy` stage that exists in the Stage Map — an interface no stage builds is a decision nobody executes. Then read the recommended option's `testSeams`: each `injectedAs` must name a construction or wiring point a test can actually substitute at, not a symbol the test would have to re-implement — a seam nothing can be injected into leaves the executor writing self-mocks. An empty `testSeams` array is only acceptable when you can defend it in one sentence; the validator accepts it either way, so this is the check that catches an unfilled field posing as a decision.
@@ -43,6 +43,13 @@
43
43
  - an ordering edge seeded from the brief's `Related Task Graph` is preserved in each packet's `depends-on`
44
44
  and in the final report's routing rationale. A `duplicates` / `related-to` edge is
45
45
  not interpreted as fan-out ordering; use it only as a reference for avoiding duplicate work / scope alignment.
46
+ One exception to "only a reference": when such an edge — `related-to` / `duplicates` / `split-from`, or a
47
+ shared `parent-of` parent — names a sibling task that performs the same behaviour on a different resource
48
+ (same action, different noun), record that commonality in the final report's routing rationale as a
49
+ variation-point input: a named candidate for shared-interface extraction that the next
50
+ `implementation-planning` run reads when it fills `variationPointAnalysis`. This does not fan out and adds
51
+ no `depends-on` edge — it stays a routing-rationale note. Without it the planner meets the task alone and
52
+ scores its options against a single implementation.
46
53
  - in the final report, do not duplicate the decomposition result; keep only the one-line "fan-out: N packets → fan-out/index.md"
47
54
  pointer. Packet execution is separate: the user starts each unit as a new task-key via
48
55
  `okstra-run --task-brief <packet path>` (this phase does not directly start any downstream run).