okstra 0.142.0 → 0.144.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/project-structure-overview.md +3 -0
- package/docs/task-process/error-analysis.md +9 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +2 -1
- package/runtime/prompts/coding-preflight/overview.md +1 -1
- package/runtime/prompts/lead/context-loader.md +2 -2
- package/runtime/prompts/lead/convergence.md +5 -2
- package/runtime/prompts/lead/report-writer.md +4 -3
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +3 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +48 -2
- package/runtime/prompts/profiles/error-analysis.md +5 -1
- package/runtime/python/okstra_ctl/analysis_packet.py +28 -3
- package/runtime/python/okstra_ctl/brief_frontmatter.py +56 -0
- package/runtime/python/okstra_ctl/convergence_engine.py +66 -15
- package/runtime/python/okstra_ctl/mutation_probe.py +1263 -0
- package/runtime/python/okstra_ctl/run.py +81 -33
- package/runtime/python/okstra_ctl/schema_excerpt.py +5 -3
- package/runtime/python/okstra_ctl/self_mock_signals.py +183 -0
- package/runtime/python/okstra_ctl/wizard.py +2 -44
- package/runtime/schemas/final-report-v1.0.schema.json +99 -0
- package/runtime/templates/reports/final-report.template.md +26 -0
- package/runtime/templates/reports/i18n/en.json +20 -2
- package/runtime/templates/reports/i18n/ko.json +20 -2
- package/runtime/validators/detect_self_mock.py +220 -0
- package/runtime/validators/validate-brief.py +5 -1
- package/runtime/validators/validate-run.py +763 -36
|
@@ -243,6 +243,8 @@ Important modules:
|
|
|
243
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
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
245
|
| `stage_citations.py` | shared grammar SSOT for reading the Stage Map stage numbers a prose cell cites (`Stages 1, 2, and 3`, ranges, etc.). One definition serves two readers that must not drift — the coverage check in `validators/validate-run.py` proving every stage traces to a requirement, and `incremental_scope.py`'s back-trace resolving which stages an answered clarification touches |
|
|
246
|
+
| `self_mock_signals.py` | self-mock signal SSOT — language-keyed regexes (`SIGNALS`), the `EXT_TO_LANG` extension map, and the waiver-matching mechanics both gates share — `selfmock_path_key` (the one path-normalization), `waiver_entry_key` (the `(file, line, <discriminator>)` triple, with the hand-typed line coerced to `int`) and `partition_waived_entries` (the split into still-failing vs waived). Gate A passes the discriminator `signal`, gate B `mutant`; one definition means the two cannot disagree about whether a waiver matches a finding. The signals are each ported from a `prompts/coding-preflight/languages/<lang>.md` "Self-mock signals to refuse" bullet with the source `doc_keyword` retained so a drift guard fails when doc and module diverge. Patterns stay deliberately narrow (only the "stub the subject's own method, then assert the stub" shape and reaching into the subject's privates; subject identity is never inferred beyond the literal `sut` token). The static detector `validators/detect_self_mock.py`, the drift guard and `mutation_probe.py` MUST import from here; four documented shapes needing subject identity no regex has are left to the mutation gate (`mutation_probe.py`) |
|
|
247
|
+
| `mutation_probe.py` | gate B of the self-mock gate — the tool-agnostic mutation probe. `ADAPTERS` maps an `EXT_TO_LANG` language key to an adapter (`ts_js` → Stryker, `rust` → cargo-mutants, `java`/`kotlin` → PIT, which reports `unsupported` because its SCM scoping is a Maven-only goal and the report↔path mapping is unverified). `run_probe` owns everything that must not differ between tools: production-source selection, the refusal to run on an empty target set, the requirement that the diff name EVERY changed source, the adapter result-shape check and the user-acknowledged waiver application; adapters only parse. `evaluate` counts a mutant only when it covers a line the diff added or modified, and records the pre-cap `survivedTotal` so a trimmed report cannot be fully waived to PASS. Anything that stops a real inspection — no adapter, tool not installed, unreadable report, unknown outcome word, no conclusive trial, a diff that misses a changed source — answers `unsupported(<reason>)`, never `PASS`. `classify_reason` is the 3-class SSOT (capability-gap / nothing-to-verify / integrity-inspection, unknown → integrity) read by BOTH the cross-language merge here and the blocking decision in `validators/validate-run.py` |
|
|
246
248
|
| `run_context.py` | Per-task mutex, run context and run-input persistence; `consumers_mutex` helper for atomic `consumers.jsonl` writes |
|
|
247
249
|
| `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 |
|
|
248
250
|
| `consumers.py` | Append-only `consumers.jsonl` writer + reader — records which `implementation` runs consumed which `implementation-planning` stage |
|
|
@@ -378,6 +380,7 @@ Optional (v1.0 backward-compatible) top-level keys:
|
|
|
378
380
|
| `validate-schedule.py` | Schedule section/order/code validation |
|
|
379
381
|
| `validate-implementation-plan-stages.py` | enforces the Stage Map structure — checks the S1–S8 rules (`## 5.5 Stage Map` + `## 5.5.<i> Stage <i>` sections, ≤ 8 steps per stage, etc.) |
|
|
380
382
|
| `validate_improvement_report.py` | enforces the 11-item contract of the improvement-discovery final-report. Automatically invoked by `validate-run.py` when `task_type == "improvement-discovery"` |
|
|
383
|
+
| `detect_self_mock.py` | self-mock detector — runs BOTH gates and writes the run's sidecar. Gate A (static) scans the changed TEST files for SUT-stub signals (patterns imported from the SSOT `scripts/okstra_ctl/self_mock_signals.py`, never redefined here), matching each file as one whole-file string so multi-line signals are caught. Writes a `qa/self-mock[-stage-<N>].json` sidecar and prints `QA-RESULT: PASS|FAIL` as its last line (exit 0 = no hits, exit 1 = at least one hit). The sidecar records `scannedFiles`/`skippedFiles` so the gate can prove every changed test file was actually scanned (a run that skips them cannot pass on empty input). An optional `--waivers <path>` moves hits matching `(file,line,signal)` from `staticDetect.hits` to `staticDetect.waived` (each carrying the user's `reason`/`acknowledgedBy`) and records the file as `waiverSource`. Gate B (mutation) runs in the same call: `--changed-file` takes the stage's WHOLE changed set (each adapter selects its own production sources out of it), `--diff` and `--worktree` scope it, and `scripts/okstra_ctl/mutation_probe.py` writes the result into the sidecar's `mutation` block; the received set is recorded as `changedFiles` so the gate can prove gate B was not handed an empty input. `overall` and the exit code follow BOTH gates — a mutation FAIL with a clean static scan still exits 1. The same `--waivers` file feeds both (gate A reads its `signal` entries, gate B its `mutant` ones). Its verdict feeds the fail-closed `_validate_selfmock` gate in `validate-run.py` (implementation / final-verification): a diff that touches test files with no readable PASS sidecar blocks the run; a `waived` entry missing `reason`/`acknowledgedBy`, or a `waiverSource` that is not the task's own `qa/self-mock-waivers.json`, also blocks |
|
|
381
384
|
| `validate-workflow.sh` | End-to-end fixture workflow validation |
|
|
382
385
|
| `lib/*.sh` | Shared shell validator helpers and fixtures |
|
|
383
386
|
|
|
@@ -47,14 +47,17 @@ sequenceDiagram
|
|
|
47
47
|
Skill->>Wizard: task-type error-analysis selected
|
|
48
48
|
Wizard-->>Skill: workers/base-ref/model args
|
|
49
49
|
Skill->>Run: render-bundle --render-only
|
|
50
|
+
Run->>Run: canonical brief preflight
|
|
50
51
|
Run->>Run: validate brief/profile
|
|
51
52
|
Run->>Run: resolve worker roster
|
|
52
53
|
Run->>WT: provision/reuse worktree
|
|
53
54
|
Run->>Art: analysis-profile.md includes common contract
|
|
54
|
-
Run->>Art: task-manifest workflow next=
|
|
55
|
+
Run->>Art: task-manifest workflow next=validated report route
|
|
55
56
|
```
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
For canonical briefs, preflight runs before worker resolution, worktree provisioning, or report creation. A brief whose `reporter-confirmations` status is `pending` stops at this point; legacy briefs keep the compatibility path.
|
|
59
|
+
|
|
60
|
+
The final report records its next phase in `errorAnalysis.routing.nextTaskType`. After report validation passes, workflow metadata persists that route as `nextRecommendedPhase`. The static `error-analysis` → `implementation-planning` mapping is a fallback only when report data is missing, legacy, or not an error-analysis report.
|
|
58
61
|
|
|
59
62
|
## 4. lead execution flow
|
|
60
63
|
|
|
@@ -69,7 +72,7 @@ flowchart TD
|
|
|
69
72
|
Report --> Persist[Phase 7 persist + validate]
|
|
70
73
|
```
|
|
71
74
|
|
|
72
|
-
The workers analyze the symptom and evidence independently. The report-writer does not analyze during Phase 4/5 but writes the final report in Phase 6.
|
|
75
|
+
The workers analyze the symptom and evidence independently. In adversarial mode, even a finding reported by multiple workers enters the verification queue instead of receiving automatic consensus. Evidence-backed counter-evidence remains in the finding's round history, so later agreement cannot turn it into full consensus. The report-writer does not analyze during Phase 4/5 but writes the final report in Phase 6.
|
|
73
76
|
|
|
74
77
|
## 5. Deliverables and prohibitions
|
|
75
78
|
|
|
@@ -82,13 +85,15 @@ flowchart LR
|
|
|
82
85
|
Hyp -. forbidden .-> Fix[Code fix in this run]
|
|
83
86
|
```
|
|
84
87
|
|
|
85
|
-
The final
|
|
88
|
+
The expected final-report content is:
|
|
86
89
|
|
|
87
90
|
- evidence-backed cause analysis
|
|
88
91
|
- uncertainty boundary
|
|
89
92
|
- practical next diagnostic steps
|
|
90
93
|
- if there is blocking uncertainty, `## 1. Clarification Items`, usually `Blocks=next-phase`
|
|
91
94
|
|
|
95
|
+
For `error-analysis`, the structured `errorAnalysis` object is the source of truth for the verbatim symptom, reproduction status, `EA-NNN` cause candidates and their counter-evidence, the next diagnostic, and routing. Its shape is enforced by `schemas/final-report-v1.0.schema.json` `$defs.ErrorAnalysis`; `validators/validate-run.py::_validate_error_analysis_consistency` enforces the cross-field semantics. A route to `implementation-planning` needs a credible referenced leading cause and `begin-planning`. A route back to `error-analysis` needs the sharp next diagnostic and `continue-investigation`.
|
|
96
|
+
|
|
92
97
|
What is prohibited is source edit, refactor, fix attempt, implementation design artifact, and running build/migration/deploy. Deferring ambiguity that could be answered from code or logs to a user question is also a defect per the profile.
|
|
93
98
|
|
|
94
99
|
## 6. Code reviewed
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -95,6 +95,7 @@ Rules (the schema enforces most of these — they are listed here so you know *w
|
|
|
95
95
|
- `header.reportAuthor` is `"Report writer worker"`; `header.reportOwner` is `"Claude lead"`. Set author to `"Claude lead"` only for `release-handoff` runs (single-lead by design) or a recorded report-writer dispatch failure fallback.
|
|
96
96
|
- **Source items (worker:item) preservation.** Every `consensus[].sourceItems`, `differences[].workersPosition[].itemId`, and `evidence.primary[].sourceItems` entry MUST carry the worker:item-id pair (e.g. `claude:F-001`, `codex:1.1`, `antigravity:F-3`, or `lead:mcp-1` for lead-only evidence). The schema enforces this via the `SourceItem` regex; bare worker-name lists no longer parse.
|
|
97
97
|
- **Verdict Card consistency.** `verdictCard.verdictToken` and `verdictCard.direction` MUST byte-match `finalVerdict.verdictToken` / `.direction`; `validators/validate-run.py` diffs both and fails the run on divergence. `verdictCard.nextStep` names the same action as `finalVerdict.nextStep` and `recommendedNextSteps[0].text` but is written as the actionable command the reader runs (e.g. `/okstra-run task-key=… task-type=release-handoff`) where the other two are prose — it is deliberately not a byte copy. Duplicating the compared values across `verdictCard` and `finalVerdict` is intentional so the validator can diff them.
|
|
98
|
+
- **Error-analysis diagnosis and routing.** When `header.taskType` is `error-analysis`, populate the required `errorAnalysis` object. Copy `errorAnalysis.symptomVerbatim` byte-for-byte from the symptom stated in the brief's `Source Material`; do not paraphrase it. Every `causeCandidates[]` row includes the full `supportingEvidence`, `falsifyingEvidenceChecked`, `confidence`, and `disproveWith` fields. Route `errorAnalysis.routing.nextTaskType=implementation-planning` with `direction=begin-planning`, or route `errorAnalysis.routing.nextTaskType=error-analysis` with `direction=continue-investigation`; no other pairing is valid. `verdictCard.nextStep`, `finalVerdict.nextStep`, the first `recommendedNextSteps` action and command, and the unique `followUpTasks` row whose `origin` is `phase-continuation` MUST all point to the same `errorAnalysis.routing.nextTaskType` target. The schema enforces only the presence of a `phase-continuation` row. Phase validation MUST enforce exact target agreement and uniqueness through `validators/validate-run.py::_validate_error_analysis_consistency`; until that check is implemented and executed, those semantics are contract requirements rather than enforced guarantees.
|
|
98
99
|
- **Reader Summary.** Populate `readerSummary` when the schema excerpt exposes it. It is the human-first entrypoint for both Markdown and HTML: one sentence for the decision, one for the human action required, one for blockers, one for audit sections safe to skip on first read, and one runnable recommended command. Do not duplicate raw evidence tables here.
|
|
99
100
|
- **External QA advisory.** A Tier 3 entry requiring `db`, `http`, or
|
|
100
101
|
`external` may be non-PASS without changing approval or final verdict. Render
|
|
@@ -103,7 +104,7 @@ Rules (the schema enforces most of these — they are listed here so you know *w
|
|
|
103
104
|
and add the exact rerun command to `recommendedNextSteps`. Never turn this
|
|
104
105
|
advisory alone into a clarification, Acceptance Blocker, conditional
|
|
105
106
|
acceptance condition, or blocked routing.
|
|
106
|
-
- **§7 phase-continuation row (mandatory for non-terminal task-types).** When `header.taskType` is one of `requirements-discovery` / `implementation-planning` / `error-analysis` / `implementation` / `final-verification`, `followUpTasks` MUST contain at least one row whose `origin` is `phase-continuation`, `
|
|
107
|
+
- **§7 phase-continuation row (mandatory for non-terminal task-types).** When `header.taskType` is one of `requirements-discovery` / `implementation-planning` / `error-analysis` / `implementation` / `final-verification`, `followUpTasks` MUST contain at least one row whose `origin` is `phase-continuation`, `newTaskId` reuses the current task-id, `autoSpawn` is `"no"`, and `priority` is `"P0"`. For `release-handoff` runs, omit the phase-continuation row. The schema `allOf` / `contains` clause enforces row presence, not exact route-target agreement or uniqueness; phase validation must enforce those error-analysis semantics as specified above.
|
|
107
108
|
- **No deprecated sections.** The schema has no `4.5.8 User Approval Request` body field, no `4.5.9 Open Questions`, no `5.1 Additional Material Request`, no `5.2 User Confirmation Questions` — clarifications go under the unified `clarificationItems[]` array.
|
|
108
109
|
- **Optional Section 0.** Include `clarificationCarryIn` ONLY when the lead's prompt provides a non-empty carry-in path. Omit the key entirely otherwise (do NOT set it to `null` or an empty object).
|
|
109
110
|
- **Reading Confirmation** goes at `**Audit sidecar path:**` per the selected report-writer preamble's `Required reading` section — never in the data.json or the main worker-results file.
|
|
@@ -63,7 +63,7 @@ Per-file checks miss cross-cutting issues; each commit can be individually clean
|
|
|
63
63
|
|
|
64
64
|
- [ ] **Domain-literal sweep:** `grep -rn` every domain enum value / predicate you added or touched in WHERE clauses, filters, or branches. The same literal at 2+ I/O sites is a *candidate* scattered decision — ask: would these sites change together when the business rule changes? Same decision → consolidate into one named constant or query builder in the domain layer and make every site reference it. Different decisions that merely share a value → leave them separate; coupling incidental duplication is worse than the repetition. (The identifier grep above does NOT catch this — sweep *values*, not just names.)
|
|
65
65
|
- [ ] **Stand-alone name test for exports:** for each exported identifier, look at its siblings — can a caller pick the right one from the names alone? If a comment must explain which to use, the name fails; encode the distinguishing fact in it (e.g., the input shape: `parseRows` vs `parseRowsFromFlatItems`).
|
|
66
|
-
- [ ] **Self-mock sweep:** for every test file you added or edited, `grep` it for the SUT-stubbing patterns of this language (full list in `languages/*.md` → "Self-mock signals to refuse") — e.g. `spyOn(sut`, `sut.<method> = jest.fn`, `spyk(sut`, `Mockito.spy(`, `@Spy` paired with `@InjectMocks`, `patch(`-ing the class under test, `mockall::mock!` of the unit itself, plus private-reach hacks (`(sut as any).`, `ReflectionTestUtils.invokeMethod(sut`). Any hit where the stubbed/replaced target **is the unit under test** — not an injected collaborator — is a refused self-mock: delete the stub and exercise the real method, or the test only proves its own wiring and survives even if the real implementation is deleted. Mocking injected collaborators at the boundary stays fine; this sweep targets only stubs on the SUT itself. If a method on the SUT feels too painful to leave real, that's a design signal (extract it to a collaborator), not a license to stub it.
|
|
66
|
+
- [ ] **Self-mock sweep:** for every test file you added or edited, `grep` it for the SUT-stubbing patterns of this language (full list in `languages/*.md` → "Self-mock signals to refuse") — e.g. `spyOn(sut`, `sut.<method> = jest.fn`, `spyk(sut`, `Mockito.spy(`, `@Spy` paired with `@InjectMocks`, `patch(`-ing the class under test, `mockall::mock!` of the unit itself, plus private-reach hacks (`(sut as any).`, `ReflectionTestUtils.invokeMethod(sut`). Any hit where the stubbed/replaced target **is the unit under test** — not an injected collaborator — is a refused self-mock: delete the stub and exercise the real method, or the test only proves its own wiring and survives even if the real implementation is deleted. Mocking injected collaborators at the boundary stays fine; this sweep targets only stubs on the SUT itself. If a method on the SUT feels too painful to leave real, that's a design signal (extract it to a collaborator), not a license to stub it. Enforced by `validators/detect_self_mock.py` (static); absent `qa/self-mock-*.json` sidecar BLOCKS at `validate-run.py`.
|
|
67
67
|
- [ ] **No documented forks:** two deliberate variants of one capability must not survive as parallel implementations with a comment explaining the delta. Re-read both bodies and check the deltas are genuinely parametric: if they reduce to a few orthogonal options, collapse into one implementation taking explicit option parameters that encode them. If encoding the delta would take more than ~3 options, or add more branching than the duplication it removes, they are two capabilities — keep two implementations with distinct honest names and delete the "variant of" framing. Either way the comment-documented fork dies. "The divergence is documented" stays a refused rationalization, and a two-capabilities verdict must come from reading the bodies, not from reluctance to refactor.
|
|
68
68
|
|
|
69
69
|
## Boundaries
|
|
@@ -126,9 +126,9 @@ Read source files lazily:
|
|
|
126
126
|
|
|
127
127
|
### Brief Reporter-Confirmation Precondition (BLOCKING)
|
|
128
128
|
|
|
129
|
-
After reading `task-brief.md`, extract the frontmatter `reporter-confirmations` field (`complete | partial | pending | skipped`) and apply the shared handling matrix in `prompts/profiles/_common-contract.md` "Brief handoff contract" → "Reporter confirmation precondition" — that block is the single authority on per-value semantics; do not re-derive them here.
|
|
129
|
+
After reading `task-brief.md`, extract the frontmatter `reporter-confirmations` field (`complete | partial | pending | skipped`) when present and apply the shared handling matrix in `prompts/profiles/_common-contract.md` "Brief handoff contract" → "Reporter confirmation precondition" — that block is the single authority on per-value semantics; do not re-derive them here.
|
|
130
130
|
|
|
131
|
-
Loader-level flow control only: on `pending
|
|
131
|
+
Loader-level defensive flow control only: on `pending`, emit `REPORTER_CONFIRMATION_PENDING` and STOP — do not invoke `team-contract` or any analyser, and do not write a final report. Regenerate the brief with `okstra-brief-gen` Step 6.5 and prepare a fresh run. A missing field is a legacy brief, not `pending`, and proceeds to Step 5. Current-format missing or invalid values are rejected during preparation, before a fresh run can reach this loader. Every other value proceeds to Step 5 with the matrix's flags carried forward for the phase profile.
|
|
132
132
|
|
|
133
133
|
## Step 5: Read Run Manifest and Team State
|
|
134
134
|
|
|
@@ -85,7 +85,10 @@ Read the worker result files generated in Phase 4/5 and extract individual findi
|
|
|
85
85
|
- Only one worker confirms a finding → one single-source group.
|
|
86
86
|
4. When grouping is ambiguous, prefer splitting over merging (avoid over-merging). Semantic matching, ticket-set equality, and evidence interpretation remain lead judgments; the engine does not perform fuzzy matching or decide whether evidence is credible.
|
|
87
87
|
5. Write `runs/<task-type>/state/convergence-groups-<task-type>-<seq>.json`. Each group carries its `ticketIds`, `originWorker`, `originEvidence`, `discoveredBy`, and every `<worker>:<item-id>` source in `sourceItems`. When a live command or external read produced reproducible evidence, also include `evidenceArtifacts[]` with its `.okstra/` path, SHA-256 digest, command, and environment. The field is optional because historical or inaccessible evidence may not have a captured artifact. The lead and verifier MUST NOT infer live or external evidence from wording or keyword matching; they use the finding's explicit claim, provenance, and supplied artifacts. Include the resolved worker roster in order with functional `audience` values; do not derive scope from provider or model identity. The `audience` enum is a convergence role, not a phase label: every finding-producing worker uses `analysis` — an `implementation` run's verifiers included — and only the report author uses `report-writer`. There is no `implementation-verifier` audience here; map the verifier roster to `analysis`.
|
|
88
|
-
6. Do not write a queue or classification in this grouped-input artifact. `okstra convergence seed`
|
|
88
|
+
6. Do not write a queue or classification in this grouped-input artifact. `okstra convergence seed` classifies Round 0 by mode:
|
|
89
|
+
- Collaborative mode: multi-source groups become `full-consensus` immediately; only single-source groups enter the working queue.
|
|
90
|
+
- Adversarial mode: every finding enters the working queue regardless of source count. Semantic grouping merges provenance only; it does not decide a finding is reliable.
|
|
91
|
+
Section 6 never enters the grouped input.
|
|
89
92
|
|
|
90
93
|
### Round 1-N: Re-verification Loop (queue-pruned)
|
|
91
94
|
|
|
@@ -208,7 +211,7 @@ ELSE:
|
|
|
208
211
|
|
|
209
212
|
`contested` remains a **final classification only** (per §"Scope and Terminology"): a disputed finding is carried forward through intermediate rounds and labelled `contested` only at the last executed round. For `requirements-discovery` (`effectiveMaxRounds = 1`) the single round IS the last round, so a split-with-hard-refute finding is labelled `contested` in that one round. The final-classifier block of §"Convergence Algorithm" honours this: its first branch classifies an adversarially carried-forward finding `contested` regardless of the AGREE tally, so the two sections cannot assign the same finding different labels.
|
|
210
213
|
|
|
211
|
-
Design intent: one `counter-evidence` refute denies a claim consensus (it cannot rise above `contested` however many others AGREE); a lone `burden-not-met` doubt does not sink an otherwise-surviving claim — only a majority of them does. When every non-discoverer refutes (all_others_disagree) the finding is worker-unique regardless of refute basis — only the discoverer still holds it. A SUPPLEMENT/caveat with zero disagrees lands partial-consensus, not full-consensus, because a caveat means the claim does not pass cleanly (unlike the collaborative classifier, where SUPPLEMENT counts as full agreement).
|
|
214
|
+
Design intent: one `counter-evidence` refute denies a claim consensus (it cannot rise above `contested` however many others AGREE); later-round agreement does not erase that refutation history. The only resolution that overrides prior `counter-evidence` is a later round where every non-discoverer non-error worker disagrees, producing `worker-unique`. A lone `burden-not-met` doubt does not sink an otherwise-surviving claim — only a majority of them does. When every non-discoverer refutes (all_others_disagree) the finding is worker-unique regardless of refute basis — only the discoverer still holds it. A SUPPLEMENT/caveat with zero disagrees lands partial-consensus, not full-consensus, because a caveat means the claim does not pass cleanly (unlike the collaborative classifier, where SUPPLEMENT counts as full agreement).
|
|
212
215
|
|
|
213
216
|
## Re-verification Dispatch
|
|
214
217
|
|
|
@@ -291,7 +291,7 @@ Skipping this file because "the real report is in `reports/`" is wrong. Both fil
|
|
|
291
291
|
|
|
292
292
|
Section numbering follows `templates/reports/final-report.template.md` exactly — that file is the documentation SSOT for section names and ordering. For full body structure at authoring time, consult your run's **phase-stripped** `final-report-template.md` (the per-task-type instruction-set copy defined in Phase 6 dispatch item 10); the "copy that block verbatim" references below mean the §-block as it appears in that stripped copy, not a re-read of the full source.
|
|
293
293
|
|
|
294
|
-
**Verdict Card (top-of-report, mandatory).** Render `## Verdict Card` between the report header and the (conditional) Approval block. Its `Verdict Token`
|
|
294
|
+
**Verdict Card (top-of-report, mandatory).** Render `## Verdict Card` between the report header and the (conditional) Approval block. Its `Verdict Token` and `Direction` cells MUST byte-match the corresponding cells in `## 7. Final Verdict`. Its `Next Step`, the final-verdict next step, and the first item of `## 3. Recommended Next Steps` MUST name the same route target, though the actionable command and prose need not be byte-identical. Divergence is `contract-violated`.
|
|
295
295
|
|
|
296
296
|
**Background and Rationale (top-of-report, mandatory — every task-type).** Fill the data.json `rationale` object (rendered as `## Background and Rationale`, right after the Verdict Card). It is the reviewer-facing narrative that answers four questions, in order — write each as **prose**, not a table:
|
|
297
297
|
- `motivation` — why we are doing this work (goal / context).
|
|
@@ -306,18 +306,19 @@ Every field MUST anchor its claim with at least one evidence reference — a `pa
|
|
|
306
306
|
0. **Clarification Response Carried In** — render this `## 0.` heading ONLY when `{{CLARIFICATION_RESPONSE_RELATIVE_PATH}}` is non-empty. Walk every `C-*` row of the prior report's `## 1. Clarification Items` table, reconcile against new evidence, and record the outcome (`resolved` / `obsolete`) with citation before drafting the verdict. When no carry-in path was provided, OMIT the `## 0.` heading entirely — the validator fails an empty Section 0 stub. The lead calls `okstra incremental-scope` exactly once, combining answered-clarification stage impacts (`--impacted`) and changed PREP IDs (`--prep-items`); selected-option, Stage Map, or recommended-approach changes pass both CSVs empty to force full mode. Record that single decision JSON verbatim into `implementationPlanning.incrementalDecision` (`mode`, `reverifyStages`, `carryStages`, `reason`); the renderer emits the `### 0.1 Incremental Re-Verification Scope` audit block from it, and the validator fails an `incremental`-mode run whose Section 0 omits that block. In `incremental` mode this run's `planItems` MUST carry every plan-item id from the re-verified stages forward with its updated verdict; if re-verification concludes a plan item should be REMOVED, that is a signal the answer's blast radius is not local — do not drop it here, tell the lead to abandon incremental and re-route to a FULL re-verification, because the carry merge only adds prior items and would resurrect the removed item's stale verdict. After authoring the current data.json, call `okstra incremental-carry`, passing the decision's `carryStages` CSV to `--carry-stages` and its `reverifyStages` CSV to `--reverify-stages`. A `CarryError` means the stage/PREP ownership contract is unsafe: discard the partial merged output and route the run through full re-verification; never publish a partially merged report.
|
|
307
307
|
1. **Clarification Items** — single unified `C-*` table; column schema (4 columns with the short fields stacked in one record-meta cell), ID convention, and rerun behaviour are owned by `_common-contract.md §Clarification request policy` (SSOT). The deprecated `5.5.9 Open Questions` / `1.1 Additional Material Request` / `1.2 User Confirmation Questions` sub-sections are removed; the validator fails reports that reintroduce them.
|
|
308
308
|
2. **Evidence and Detailed Analysis** — primary evidence rows (file path, line, snippet); secondary evidence / alternate interpretations. If `reference-expectations.md` lists explicit expected values, record match/gap per row.
|
|
309
|
+
- **Error-analysis diagnosis and routing.** When `header.taskType` is `error-analysis`, populate the required `errorAnalysis` object. Copy `errorAnalysis.symptomVerbatim` byte-for-byte from the symptom stated in the brief's `Source Material`; do not paraphrase it. Every `causeCandidates[]` row includes the full `supportingEvidence`, `falsifyingEvidenceChecked`, `confidence`, and `disproveWith` fields. Route `errorAnalysis.routing.nextTaskType=implementation-planning` with `direction=begin-planning`, or route `errorAnalysis.routing.nextTaskType=error-analysis` with `direction=continue-investigation`; no other pairing is valid. `verdictCard.nextStep`, `finalVerdict.nextStep`, the first `recommendedNextSteps` action and command, and the unique `followUpTasks` row whose `origin` is `phase-continuation` MUST all point to the same `errorAnalysis.routing.nextTaskType` target. The schema enforces only the presence of a `phase-continuation` row. Phase validation MUST enforce exact target agreement and uniqueness through `validators/validate-run.py::_validate_error_analysis_consistency`; until that check is implemented and executed, those semantics are contract requirements rather than enforced guarantees.
|
|
309
310
|
3. **Recommended Next Steps** — prioritized actions. After Phase 7's follow-up spawner runs, append a row per newly created task-key (see "Phase 6 → Phase 7 execution sequence" above). **Approval-gate consistency:** when §1 carries any `Blocks: approval` row with `Status` ∈ {open, answered}, the Verdict Card `Next Step` and the first recommended step MUST point to the clarification rerun (`resume-clarification` of the SAME task-type) — never to "flip frontmatter `approved: true` → jump straight to `implementation`". Run-prep enforces this gate (`run.py _validate_approved_plan` fail-closes on those rows and on a blocking data.json `gateResult`), so a direct-implementation next-step is an instruction the reader cannot actually follow. **Cross-project pointer rule:** for cross-project dependencies (another repo / a different top-level deployment module / a published package), `crossProjectDependencies` (§5.4 Cross-Project Dependencies) is authoritative — do NOT duplicate that substance (prerequisite work / verification signals / handoff) into `recommendedNextSteps`; put only a one-line pointer to that section (no double-recording).
|
|
310
311
|
4. **Follow-up Tasks** — auto-spawn-eligible table. Each row drives `okstra-spawn-followups.py`; see template §4 for the row schema.
|
|
311
312
|
5. **Missing Information and Risks** — uncertain / "I don't know" items. `implementation-planning` adds §5.5 (see heading contract below); `release-handoff` adds §5.6.
|
|
312
313
|
6. **Cross Verification Results** — 4 categories (Full / Partial / Contested / Worker-Unique) when convergence is enabled, per `convergence`. Prepend the Round History sub-table (columns: `Round | inputQueueSize | resolvedCount | carriedForwardCount | dispatches | skippedWorkers`) plus a `round2SkippedReason: <value>` note, pulled verbatim from `convergence-<task-type>-<seq>.json`. Empty contested list renders as `- No items lacking consensus.`. Convergence-disabled runs use the legacy Consensus/Differences format and omit the round table.
|
|
313
|
-
7. **Final Verdict** — `Direction` ∈ `continue-investigation` / `begin-implementation` / `approve` / `reject` / `hold`. **Verdict Token** is `not-applicable` for every task-type except `final-verification` — see "Final-verification verdict token contract" below for that case.
|
|
314
|
+
7. **Final Verdict** — `Direction` ∈ `continue-investigation` / `begin-planning` / `begin-implementation` / `approve` / `reject` / `hold`. **Verdict Token** is `not-applicable` for every task-type except `final-verification` — see "Final-verification verdict token contract" below for that case.
|
|
314
315
|
|
|
315
316
|
**§5.10 Fix History (data-presence gated).** When the run-manifest carries a `fixCycleId`, fill the data.json `fixCycle` block (`cycle` / `targetReport` / `symptom` / `runs`). Read the values from the task root's `history/fix-cycles.jsonl`: `cycle` MUST equal `fixCycleId`, `targetReport` / `symptom` come from that cycle's `opened` row, and `runs` lists its attached `run` rows (`taskType` / `runSeq` / `runManifest`). The validator (`validators/validate-run.py` → `_validate_fix_cycle`) fails the run when the block is missing or `fixCycle.cycle` does not match `fixCycleId`. When the run-manifest has no `fixCycleId`, OMIT the `fixCycle` block entirely — the renderer omits §5.10.
|
|
316
317
|
|
|
317
318
|
### Writing Guidelines
|
|
318
319
|
|
|
319
320
|
- Write in Markdown. **Prefer tables over prose bullet lists** for any section that enumerates multiple items with the same shape (evidence rows, risks, options, dependencies, rollback steps, follow-ups, open questions). Bullets are reserved for short, single-line standalone statements (e.g., "- No additional information requested."). When the template provides a table form, do NOT degrade it back to bullets in the rendered report. **Exception — `## Background and Rationale` (`rationale`) is deliberately prose**: it is connected narrative explaining the *why*, not a same-shape enumeration, so write full sentences there rather than forcing it into a table.
|
|
320
|
-
- **Do not restate the same conclusion verbatim across sections.** The Verdict Card and Reader Summary are *digests*: give the outcome in one or two sentences and point to `## 7. Final Verdict` / `## 5.8.8 Routing Recommendation` for the full reasoning, rather than copying their multi-clause conclusion word-for-word. Only the `Verdict Token`
|
|
321
|
+
- **Do not restate the same conclusion verbatim across sections.** The Verdict Card and Reader Summary are *digests*: give the outcome in one or two sentences and point to `## 7. Final Verdict` / `## 5.8.8 Routing Recommendation` for the full reasoning, rather than copying their multi-clause conclusion word-for-word. Only the `Verdict Token` and `Direction` cells must byte-match §7 (per the Verdict Card contract above). `Next Step` must point to the same routing target as §7, but its actionable command and prose need not be byte-identical. The prose conclusion must not be a duplicate.
|
|
321
322
|
- **Keep each sentence to one main idea.** A single sentence that stacks four or five clauses with em-dashes and nested parentheticals (300+ characters) is hard to read, and the renderer can only line-break at sentence ends — so break such reasoning into separate sentences. Facts, evidence, and IDs still live in the tables; prose carries only the connective *why*.
|
|
322
323
|
- Write the final report body in the language passed in **Report Language**
|
|
323
324
|
above (`en` or `ko`). The template's fixed labels (section asides,
|
|
@@ -18,5 +18,5 @@ Load the applicable coding conventions for every language the diff will touch, t
|
|
|
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
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`.
|
|
20
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.
|
|
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
|
+
- **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. Self-mocking (1) — Enforced by `validators/detect_self_mock.py` (static); absent `qa/self-mock-*.json` sidecar BLOCKS at `validate-run.py`.
|
|
22
22
|
- **Graceful degradation (codex / antigravity executor runtimes, or any runtime where the resolved coding-preflight pack files are absent or unreadable):** do NOT skip the gate — apply the agnostic principles above plus the project's own `CLAUDE.md` / `CONTRIBUTING` / formatter+lint config, and record `coding-conventions: resource-unavailable → applied <project rules + agnostic principles>` in the final report. Never claim a resource read that did not happen.
|
|
@@ -32,7 +32,9 @@ profile document.
|
|
|
32
32
|
- `complete` → proceed normally.
|
|
33
33
|
- `partial` → proceed; treat still-unmarked `intent-check:` / `conversion-block:` rows as the `skipped` branch.
|
|
34
34
|
- `skipped` → do NOT silently infer the missing answers. Promote each unmarked `intent-check:` / `conversion-block:` row into this run's `## 1. Clarification Items` as `Kind=decision`. Use `Blocks=approval` in `implementation-planning`, where the row gates the `approved:` frontmatter flip; otherwise use `Blocks=next-phase`. The recommended answer is drawn from the brief's matching content and clearly labelled `reporter direct-confirmation recommended`.
|
|
35
|
-
- `pending
|
|
35
|
+
- Current-format `pending`, missing, or invalid values are rejected during task-bundle preparation, so they never reach a new run. A brief is current-format only when its frontmatter contains the `reporter-confirmations` key; its value, `type`, and `generator` are then validated together.
|
|
36
|
+
- Defensive upgrade path: if an already-prepared bundle contains `pending`, the context loader emits `REPORTER_CONFIRMATION_PENDING` and stops before worker or report-writer dispatch. It writes no final report. Regenerate the brief with `okstra-brief-gen` Step 6.5 and prepare a fresh run.
|
|
37
|
+
- A legacy brief with no `reporter-confirmations` field is not reinterpreted as `pending`; it keeps the compatibility path and proceeds without this reporter-confirmation gate.
|
|
36
38
|
`[CONFIRMED <YYYY-MM-DD> → RC-N]` markers on `Open Questions` rows are the per-row signal that the reporter has answered; their answers live verbatim under `## Reporter Confirmations` in the brief.
|
|
37
39
|
- `Source Material` is reporter-verbatim. Do NOT paraphrase, summarize, reorder, or restructure it. Quote it directly when needed.
|
|
38
40
|
- `Related Task Graph` is the structured task-topology handoff. If the section is present and not `_(none)_`, read it before classification, diagnosis, candidate discovery, fan-out, or next-step routing. Preserve the edge direction exactly as written: `From` → `To` is load-bearing for `depends-on`, `blocks`, parent/child, follow-up, and split relations.
|
|
@@ -40,7 +40,7 @@ Verifier obtains the QA command set from exactly two declared sources, in order
|
|
|
40
40
|
|
|
41
41
|
### Execution rule
|
|
42
42
|
|
|
43
|
-
Tier 1 commands run verbatim first. Then every Tier 2 entry runs once. Then the Tier 3 stage conformance script (below) runs once. Each command runs in the worktree cwd, and is recorded in the worker result with its exact command line, exit code, and the tail of stdout/stderr. Substituting or paraphrasing a Tier 1 command is forbidden (see Verifier-specific forbidden actions below).
|
|
43
|
+
Tier 1 commands run verbatim first. Then every Tier 2 entry runs once. Then the Tier 3 stage conformance script (below) runs once. Then the self-mock detector (below) runs once whenever the diff changed a test file. Each command runs in the worktree cwd, and is recorded in the worker result with its exact command line, exit code, and the tail of stdout/stderr. Substituting or paraphrasing a Tier 1 command is forbidden (see Verifier-specific forbidden actions below).
|
|
44
44
|
|
|
45
45
|
### Tier 3 — stage conformance scripts
|
|
46
46
|
|
|
@@ -84,6 +84,52 @@ also remain contract violations.
|
|
|
84
84
|
- **Read-only command log.** Record the `runCommand` exact line + its exit code in the Read-only command log. Tier 3 external non-PASS evidence MUST remain visible with status `ADVISORY`. Unlike Tiers 1·2, a conformance script MAY mutate the **replica datastore** (exercising integrated state is its whole purpose) — but only the `qaEnv` replica target, never a shared/staging/prod store. The `runCommand` itself is still subject to the same source/lockfile mutation deny-list as Tier 2 (`--fix`, `npm install` without `ci`, etc.); a denied token aborts with `contract-violated`.
|
|
85
85
|
- **No manifest / no entry for this stage.** If the manifest file is absent, or it has no entry whose `stageKey` matches this run's stageKey, the verifier records `conformance: no manifest entry for <stageKey>` and proceeds (forcing the *declaration* of conformance entries is the job of planning Step 11 + the `validate-run.py` diff-surface cross-check, not the verifier).
|
|
86
86
|
|
|
87
|
+
### Self-mock detection (changed test files)
|
|
88
|
+
|
|
89
|
+
A green suite does not prove a test exercises the unit it names — a test that stubs its own SUT passes forever, including after the real implementation is deleted. The static detector is the machine half of the **Self-mocking** blocking check below, and running it is the verifier's own duty: it is never delegated to the executor and never inferred from the executor's evidence.
|
|
90
|
+
|
|
91
|
+
- **Trigger.** This run's diff changed at least one **test** file. Enumerate the changed files with `git diff --name-only <base>...HEAD` from the worktree cwd — the same enumeration the static review's Scope rule uses — then keep only the paths the gate itself treats as tests: `*.spec.*`, `*.test.*`, a `test_`-prefixed basename, a `_test.` suffixed basename, or any path segment `test/` or `tests/`. Pass nothing else; non-test files are excluded. Exclude `tests/fixtures/self_mock/**` as well — those are the detector's own deliberately self-mocked fixtures, which `validate-run.py` also excludes from the trigger, so feeding them in would manufacture a `FAIL` the gate then blocks on. No changed test file → no run and no sidecar; the gate is vacuous by design.
|
|
92
|
+
- **Run the detector once, in the worktree cwd**, one `--test-file` per changed test file:
|
|
93
|
+
```bash
|
|
94
|
+
python3 ~/.okstra/lib/validators/detect_self_mock.py \
|
|
95
|
+
--test-file <changed test file> [--test-file <changed test file> ...] \
|
|
96
|
+
--changed-file <changed file> [--changed-file <changed file> ...] \
|
|
97
|
+
--sidecar <task_root>/qa/self-mock-<stage-name>.json \
|
|
98
|
+
--stage-name <stage-name> \
|
|
99
|
+
--waivers <task_root>/qa/self-mock-waivers.json \
|
|
100
|
+
--diff <task_root>/qa/self-mock-<stage-name>.diff \
|
|
101
|
+
--worktree <this stage's worktree root>
|
|
102
|
+
```
|
|
103
|
+
`--changed-file` / `--diff` / `--worktree` feed **gate B** (the mutation probe) and are separate from `--test-file`, which feeds gate A. `--changed-file` takes **every** path in the stage's diff — production sources included, not only the test files — because each mutation adapter selects its own production sources out of that set; hand it only the test files and every adapter finds nothing to mutate, which records a vacuous PASS while gate B is silently dead. Write `--diff` first with `git diff <base>...HEAD > <task_root>/qa/self-mock-<stage-name>.diff` — the same `<base>` and the same range every other `git diff` in this file uses: it is what scopes surviving mutants to the lines this stage added or modified, and without it gate B reports `unsupported(diff-unavailable)` rather than guessing.
|
|
104
|
+
`<stage-name>` is literally `stage-<N>` for this run's injected Stage number (`stage-3` — not the bare number, not the stageKey), and `<task_root>/qa` is the `TASK_QA_PATH` token, the same directory Tier 3's manifest and `result-*.json` live in. A whole-task run with no stage writes `<task_root>/qa/self-mock.json` and omits `--stage-name`. Any other filename or directory is invisible to the gate and reads exactly like "the detector never ran". Pass `--waivers` **unconditionally**: an absent waiver file is the normal case and the detector treats it as "no waivers", so there is no branch to decide and no file for you to create.
|
|
105
|
+
- **Write the result sidecar (BLOCKING deliverable).** The detector writes `<task_root>/qa/self-mock-<stage-name>.json` itself:
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"stageName": "stage-<N>",
|
|
109
|
+
"overall": "PASS",
|
|
110
|
+
"ranAt": "<UTC ISO8601>",
|
|
111
|
+
"scannedFiles": ["<test file the detector read>"],
|
|
112
|
+
"skippedFiles": ["<test file it received but could not read>"],
|
|
113
|
+
"changedFiles": ["<every --changed-file path you passed>"],
|
|
114
|
+
"staticDetect": { "status": "PASS", "hits": [], "waived": [], "waiverSource": "<the --waivers path, or null>" },
|
|
115
|
+
"mutation": { "status": "unsupported(stryker:tool-not-declared)", "tool": "stryker", "survived": [], "survivedTotal": 0, "waived": [], "waiverSource": "<the --waivers path, or null>" }
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
`overall` is exactly one of `PASS` / `FAIL`. `scannedFiles` + `skippedFiles` together are the detector's own record of **every** `--test-file` path it received: it read the first list, and could not read the second (extension with no signal set, or no file on disk). Which list a path lands in is the **detector's** decision, never yours — a Go/Ruby/C# test, a JSON fixture under `tests/`, and a test file this stage deleted are all legitimate `skippedFiles` entries and none of them is a defect. The verifier MUST NOT hand-write, edit, or "correct" this file — the detector's own output is the evidence, and a hand-authored sidecar is a `contract-violated` outcome. Its absence is not a passive skip: **Enforced:** `validators/validate-run.py` `_validate_selfmock` fails any report whose §5.7.3 diff summary lists a changed test file while this sidecar is absent, unreadable, malformed, or carries `overall != PASS`.
|
|
119
|
+
**Enforced (coverage, gate B):** the same gate fails the report when a file from §5.7.3 is missing from `changedFiles`, or when that field is absent — that is how "gate B saw this stage" stays distinguishable from "gate B was handed nothing". Pass **every** path in the diff summary to `--changed-file`, production sources included.
|
|
120
|
+
**Enforced (coverage, gate A):** the same gate fails the report when a changed test file from §5.7.3 appears in **neither** `scannedFiles` nor `skippedFiles` — that means you never passed it, and a PASS over a narrower input says nothing about the file left out. So pass **every** file the trigger enumeration kept, in the same repo-relative spelling the diff summary uses, and let the detector sort them. Pre-filtering by language, or dropping a path because the stage deleted the file, is the one way to trip this check.
|
|
121
|
+
- **Suspected false positive → report it, never waive it (self-check safety).** The signal set is regex-based, so it will occasionally accuse a test that is not self-mocked. The escape hatch is `<task_root>/qa/self-mock-waivers.json`, a JSON array of `{"file": "<repo-relative path, as in --test-file>", "line": <hit line>, "signal": "<signal name>", "reason": "<why this hit is not a self-mock>", "acknowledgedBy": "<the user who accepted it>"}`. A waived hit moves out of `staticDetect.hits` into `staticDetect.waived` and stops counting toward the verdict, so it is the one input that can talk the gate out of a finding — and the finding is about **the code this run is verifying**, which is why the acknowledgement must come from outside the run.
|
|
122
|
+
**The verifier MUST NOT create, edit, extend, or re-order that file.** It is the user's acknowledgement channel, not yours; writing an entry into it is self-certification of your own finding and is a `contract-violated` outcome exactly like hand-editing the sidecar. You also MUST NOT point `--waivers` at any other file, and MUST NOT copy a waiver entry into the sidecar by hand. **Enforced (source):** the detector records the `--waivers` argument verbatim as `staticDetect.waiverSource`, and `_validate_selfmock` fails any report whose sidecar carries a non-empty `waived` read from anywhere other than this task's `<task_root>/qa/self-mock-waivers.json` — so redirecting the flag at a file you wrote yourself blocks the run instead of clearing it, and every applied waiver is left in the task bundle to review.
|
|
123
|
+
What you do instead: keep the verdict `FAIL`, and record the suspected false positive in your worker result under the hit's citation — the `path:line`, the signal name, why you believe it is not a self-mock, and the exact JSON object the user would add. The user reviews it, adds the entry with their own `acknowledgedBy`, and the next detector run picks it up through `--waivers`. **Enforced:** `validators/validate-run.py` `_validate_selfmock` fails the report when any `staticDetect.waived` entry is missing a non-empty `reason` or a non-empty `acknowledgedBy` — so an unacknowledged or unexplained waiver blocks the run instead of clearing it, and a matching-but-unacknowledged waiver reaches that gate rather than being silently dropped by the detector.
|
|
124
|
+
- **Read-only command log.** Record the exact command line, its exit code (`0` = PASS, `1` = FAIL), and the detector's last stdout line `QA-RESULT: PASS|FAIL`, together with every `SELF-MOCK <file>:<line> <signal>` line it printed. When the sidecar's `staticDetect.waived` is non-empty, list each waived hit with its `reason` and `acknowledgedBy` so the report shows what the run was excused from and on whose authority. A `FAIL` sets the verifier verdict to `FAIL` with each hit cited `path:line` + signal name and the recommended fix recorded (delete the stub and exercise the real method, or stub injected collaborators only) — the same verdict machinery as the **Self-mocking** blocking check below, which the detector cites for but does not replace: a self-mock the detector's signal set does not cover is still the verifier's finding to make by reading the diff.
|
|
125
|
+
- **Gate B (mutation) runs inside the same detector call.** The detector invokes the mutation probe itself over `--changed-file` and writes the `mutation` block (`status` / `tool` / `survived` / `waived`); your duty is to pass the three flags above, never to author or edit that block by hand. Gate B is a **real gate now** — the `mutation` block is no longer a `pending-phase-2` placeholder, and running the probe over the supported languages in this diff is MANDATORY, which is what the `--changed-file` / `--diff` / `--worktree` flags above accomplish. `status` is `PASS`, `FAIL`, or `unsupported(<reason>)`, and the reason's **class** decides what happens:
|
|
126
|
+
- **Capability gap** (`no-adapter:<lang>`, `tool-not-declared`, `diff-scope-unavailable`, `no-production-sources`, `no-changed-sources`) or **nothing to verify** (`no-mutants-generated`, `diff-adds-no-line`) — gate B legitimately had no tool or nothing to check. Non-blocking; this is the normal case in a repo without mutation tooling.
|
|
127
|
+
- **Integrity / inspection failure** (`diff-incomplete`, `diff-unavailable`, `report-unavailable`, `report-unparsed`, `adapter-malformed-status`, `no-conclusive-mutants`, or any reason not listed above) — **this BLOCKS the run.** It means the stage was never actually inspected: most often a `--diff` you built from a different `<base>` than the `--changed-file` list, or one written before your last edit, so the diff does not cover the changed sources. Rebuild the diff from the same `<base>` and re-run the detector; do not treat it as a skip.
|
|
128
|
+
**Enforced:** `validators/validate-run.py` `_validate_selfmock` blocks on `mutation.status == "FAIL"`, on a missing or malformed `mutation` block, and on any `unsupported(...)` in the integrity/inspection class; the other classes stay excluded from the verdict and are kept in the sidecar for audit.
|
|
129
|
+
- **Suspected false-positive MUTANT → report it, never waive it (self-check safety).** A surviving mutant can be a false accusation too — an unreachable branch, a mutation with no observable behaviour. The escape hatch is the SAME `<task_root>/qa/self-mock-waivers.json` gate A uses, so the user manages one file: a static entry is keyed `{"file", "line", "signal", ...}` and a mutation entry `{"file": "<repo-relative path>", "line": <survivor line>, "mutant": "<mutator name as the detector printed it>", "reason": "<why this mutant is not a real gap>", "acknowledgedBy": "<the user who accepted it>"}`. The `mutant` field is what marks it as gate B's; entries without it are gate A's and never clear a mutant. A waived mutant moves out of `mutation.survived` into `mutation.waived`, and once every survivor on a changed line is waived the mutation verdict is `PASS`.
|
|
130
|
+
**The verifier MUST NOT create, edit, extend, or re-order that file** — the same rule as gate A, for the same reason: it is the user's acknowledgement channel, and writing an entry into it is self-certification of your own finding, a `contract-violated` outcome exactly like hand-editing the sidecar. You also MUST NOT point `--waivers` anywhere else. **Enforced (fields + source):** the probe only MATCHES waivers and carries an unacknowledged one straight through, so `_validate_selfmock` fails any report whose `mutation.waived` entry is missing a non-empty `reason` or `acknowledgedBy`, or whose `mutation.waiverSource` is not this task's own `qa/self-mock-waivers.json`.
|
|
131
|
+
What you do instead: keep the verdict `FAIL`, and record the suspected false positive in your worker result under the mutant's citation — the `path:line`, the mutator name, its `Survived`/`NoCoverage` status, why you believe it is not a real coverage gap, and the exact JSON object the user would add. The user reviews it, adds the entry with their own `acknowledgedBy`, and the next detector run picks it up through the same `--waivers` flag. When it is `FAIL`, cite each `MUTANT-SURVIVED <file>:<line> <mutator> (<status>)` line the detector printed: `Survived` means the test ran that line and asserted nothing about it, `NoCoverage` means no test reached it at all — which is what a stubbed subject looks like from the outside.
|
|
132
|
+
|
|
87
133
|
### Missing-tier handling
|
|
88
134
|
|
|
89
135
|
If a tier is empty or absent, verifier records the single line `qa-command not configured: <category>` per missing category (`lint` / `format` / `typecheck` / `test`; and `db-test` **only when the diff touches DB/IO/SQL**, where a missing `db-test` is escalated to a blocking finding per the DB real-execution gate below) in the worker result and proceeds — silent omission is a contract violation. **Enforced:** `validators/validate-run.py` `_validate_missing_qa_categories_recorded` for the four unconditional categories; `db-test` is left to the DB gate below because its requirement depends on whether the diff touches DB/IO/SQL. Without the note, "the category passed" and "the category never ran" read identically in the report. Verifier MUST NOT auto-detect or invent a command in this case; the user/operator must declare it in `project.json.qaCommands` or in the plan.
|
|
@@ -98,7 +144,7 @@ Tier 3 external-advisory discrepancies are excluded from this promotion: preserv
|
|
|
98
144
|
|
|
99
145
|
### Read-only command log (per verifier)
|
|
100
146
|
|
|
101
|
-
The worker result MUST contain a `Read-only command log` block listing every command executed during the verifier run with its exact invocation and exit code, in execution order — including the Tier 3 conformance `runCommand` (or the exemption/waiver skip note when no script ran). No source-mutating command may appear in this block; the only permitted
|
|
147
|
+
The worker result MUST contain a `Read-only command log` block listing every command executed during the verifier run with its exact invocation and exit code, in execution order — including the Tier 3 conformance `runCommand` (or the exemption/waiver skip note when no script ran). No source-mutating command may appear in this block; the only permitted mutations are a Tier 3 conformance script writing to its `qaEnv` replica datastore and the self-mock detector writing its own `<task_root>/qa/self-mock-*.json` sidecar — both are artifact-directory writes, both are logged like any other command, and neither touches the worktree source, so the verifier runs them without hesitation. This log is copied into the final report's verifier result section verbatim.
|
|
102
148
|
|
|
103
149
|
### Verifier evidence is independent of executor evidence
|
|
104
150
|
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
- **Graph-aware scope:** a graph edge can explain ordering or duplication, but it is not proof of cause by itself. Cite code/log evidence before claiming an upstream related task caused the current symptom.
|
|
23
23
|
- **Sharp next diagnostic:** end with the single highest-value diagnostic command, log capture, or file inspection that should happen next, plus the expected signal that would confirm or reject the leading cause.
|
|
24
24
|
- **Fix-design boundary:** do not design the implementation fix beyond what is necessary to validate the cause. If the cause is credible, route to `implementation-planning` with the verified evidence; if the cause is still unclear, route to another `error-analysis` run with the next diagnostic.
|
|
25
|
+
- Structured diagnosis and routing contract:
|
|
26
|
+
- `errorAnalysis` is the source of truth for reproduction status, `EA-NNN` cause candidates, the sharp next diagnostic, and the next route.
|
|
27
|
+
- A route to `implementation-planning` requires a credible leading cause referenced by `routing.leadingCauseId` and `begin-planning` as the direction. A route back to `error-analysis` requires the sharp next diagnostic and `continue-investigation` as the direction.
|
|
28
|
+
- Structure is enforced by `schemas/final-report-v1.0.schema.json` `$defs.ErrorAnalysis`. Cross-field diagnosis and route semantics are enforced by `validators/validate-run.py::_validate_error_analysis_consistency`.
|
|
25
29
|
- Primary focus areas:
|
|
26
30
|
- symptom and trigger clarification
|
|
27
31
|
- root-cause candidates
|
|
@@ -40,7 +44,7 @@
|
|
|
40
44
|
- **Codebase-first ambiguity resolution (defect rule)**: any ambiguity about repro, file behavior, or symbol semantics that can be answered by `Read` / `Grep` / log inspection MUST be resolved that way and recorded with file:line (or log-line) evidence. Writing a clarification row for something the codebase or shipped logs already answer is a defect of this phase.
|
|
41
45
|
- **Evidence note required inside `Statement`**: every clarification row includes `Evidence checked: <path:line>` or `Evidence checked: none — <reporter-only reason>` in the `Statement` cell. `none` is allowed ONLY when the row's nature is "only the reporter can answer this" (reporter-side data, business priority, environment they observed). A row with `none` that *could* have been answered by code or logs is a defect.
|
|
42
46
|
- Cross-verification mode:
|
|
43
|
-
- Phase 5.5 convergence runs in **adversarial mode** for this phase (`convergence.adversarial=true`). Verifiers actively try to refute each root-cause / reproduction claim by directly re-inspecting the cited code, logs, or config; the burden of proof sits on the claim. See `prompts/lead/convergence.md` §"Adversarial Verification Mode". A single evidence-backed refutation prevents a finding from reaching consensus.
|
|
47
|
+
- Phase 5.5 convergence runs in **adversarial mode** for this phase (`convergence.adversarial=true`). Verifiers actively try to refute each root-cause / reproduction claim by directly re-inspecting the cited code, logs, or config; the burden of proof sits on the claim. See `prompts/lead/convergence.md` §"Adversarial Verification Mode". Multi-source findings enter the adversarial queue rather than becoming automatic consensus. A single evidence-backed refutation prevents a finding from reaching consensus, remains in the round history, and cannot be erased into full consensus by later agreement.
|
|
44
48
|
{{INCLUDE:_coverage-critic.md}}
|
|
45
49
|
- Non-goals:
|
|
46
50
|
- implementation details unless they are necessary to validate the cause
|
|
@@ -20,6 +20,27 @@ BRIEF_SECTIONS = (
|
|
|
20
20
|
"Task Continuity Notes",
|
|
21
21
|
"Available MCP Servers",
|
|
22
22
|
)
|
|
23
|
+
ERROR_ANALYSIS_BRIEF_SECTIONS = (
|
|
24
|
+
"Source Material",
|
|
25
|
+
"Context",
|
|
26
|
+
"Problem / Symptom",
|
|
27
|
+
"Desired Outcome",
|
|
28
|
+
"Expected Behavior",
|
|
29
|
+
"Preserved Behavior",
|
|
30
|
+
"Expected Outcome",
|
|
31
|
+
"External Gates",
|
|
32
|
+
"Constraints",
|
|
33
|
+
"Scan Scope",
|
|
34
|
+
"Priority Lenses",
|
|
35
|
+
"Related Artifacts",
|
|
36
|
+
"Related Task Graph",
|
|
37
|
+
"Open Questions",
|
|
38
|
+
"Reporter Confirmations",
|
|
39
|
+
"Augmentation",
|
|
40
|
+
) + BRIEF_SECTIONS
|
|
41
|
+
BRIEF_SECTIONS_BY_TASK_TYPE = {
|
|
42
|
+
"error-analysis": ERROR_ANALYSIS_BRIEF_SECTIONS,
|
|
43
|
+
}
|
|
23
44
|
PROFILE_SECTIONS = (
|
|
24
45
|
"Primary focus areas",
|
|
25
46
|
"Expected output emphasis",
|
|
@@ -30,6 +51,7 @@ WORKER_PROFILE_SECTIONS_BY_TASK_TYPE = {
|
|
|
30
51
|
"Worker discovery procedure",
|
|
31
52
|
),
|
|
32
53
|
"error-analysis": (
|
|
54
|
+
"Brief consumption",
|
|
33
55
|
"Worker diagnosis procedure",
|
|
34
56
|
),
|
|
35
57
|
"implementation-planning": (
|
|
@@ -79,7 +101,7 @@ def build_analysis_packet(
|
|
|
79
101
|
bool(clarification_response_path),
|
|
80
102
|
)
|
|
81
103
|
)
|
|
82
|
-
parts.extend(_brief_block(brief_text))
|
|
104
|
+
parts.extend(_brief_block(task_type, brief_text))
|
|
83
105
|
parts.extend(_profile_block(task_type, profile_text))
|
|
84
106
|
parts.extend(_reference_block(reference_text))
|
|
85
107
|
parts.extend(_fix_history_block(fix_history_text))
|
|
@@ -138,12 +160,15 @@ def _intro_block(
|
|
|
138
160
|
return lines
|
|
139
161
|
|
|
140
162
|
|
|
141
|
-
def _brief_block(brief_text: str) -> list[str]:
|
|
163
|
+
def _brief_block(task_type: str, brief_text: str) -> list[str]:
|
|
142
164
|
return [
|
|
143
165
|
"",
|
|
144
166
|
"## Task-Specific Brief Extract",
|
|
145
167
|
"",
|
|
146
|
-
_extract_sections(
|
|
168
|
+
_extract_sections(
|
|
169
|
+
brief_text,
|
|
170
|
+
BRIEF_SECTIONS_BY_TASK_TYPE.get(task_type, BRIEF_SECTIONS),
|
|
171
|
+
),
|
|
147
172
|
]
|
|
148
173
|
|
|
149
174
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Shared lightweight parser for brief markdown frontmatter."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Mapping
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_BRIEF_FRONTMATTER_LINE_RE = re.compile(r"^([a-zA-Z0-9_\-]+)\s*:\s*(.*)$")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def read_brief_frontmatter(path: Path) -> dict[str, str]:
|
|
13
|
+
"""Read a brief's YAML-style frontmatter into a flat key-value map.
|
|
14
|
+
|
|
15
|
+
Returns ``{}`` if the file is unreadable, has no frontmatter, or the
|
|
16
|
+
frontmatter is malformed. Comments and quoted values are stripped.
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
text = path.read_text(encoding="utf-8")
|
|
20
|
+
except OSError:
|
|
21
|
+
return {}
|
|
22
|
+
if not text.startswith("---"):
|
|
23
|
+
return {}
|
|
24
|
+
lines = text.splitlines()
|
|
25
|
+
if not lines or lines[0].strip() != "---":
|
|
26
|
+
return {}
|
|
27
|
+
out: dict[str, str] = {}
|
|
28
|
+
for line in lines[1:]:
|
|
29
|
+
if line.strip() == "---":
|
|
30
|
+
break
|
|
31
|
+
comment_idx = line.find("#")
|
|
32
|
+
if comment_idx >= 0:
|
|
33
|
+
line = line[:comment_idx]
|
|
34
|
+
match = _BRIEF_FRONTMATTER_LINE_RE.match(line.strip())
|
|
35
|
+
if not match:
|
|
36
|
+
continue
|
|
37
|
+
key, value = match.group(1), match.group(2).strip()
|
|
38
|
+
if (
|
|
39
|
+
len(value) >= 2
|
|
40
|
+
and value[0] == value[-1]
|
|
41
|
+
and value[0] in ("'", '"')
|
|
42
|
+
):
|
|
43
|
+
value = value[1:-1]
|
|
44
|
+
out[key] = value
|
|
45
|
+
return out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_canonical_generated_brief(frontmatter: Mapping[str, str]) -> bool:
|
|
49
|
+
return (
|
|
50
|
+
frontmatter.get("type") == "brief"
|
|
51
|
+
and frontmatter.get("generator") == "okstra-brief-gen"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def has_reporter_confirmation_contract(frontmatter: Mapping[str, str]) -> bool:
|
|
56
|
+
return "reporter-confirmations" in frontmatter
|