okstra 0.151.0 → 0.152.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 (31) hide show
  1. package/docs/cli.md +1 -1
  2. package/docs/project-structure-overview.md +1 -1
  3. package/package.json +1 -1
  4. package/runtime/BUILD.json +2 -2
  5. package/runtime/agents/workers/antigravity-worker.md +2 -1
  6. package/runtime/agents/workers/claude-worker.md +2 -1
  7. package/runtime/agents/workers/codex-worker.md +2 -1
  8. package/runtime/agents/workers/grok-worker.md +2 -1
  9. package/runtime/agents/workers/kimi-worker.md +2 -1
  10. package/runtime/agents/workers/report-writer-worker.md +1 -1
  11. package/runtime/bin/okstra-report-translate.py +32 -11
  12. package/runtime/prompts/launch.template.md +1 -1
  13. package/runtime/prompts/lead/convergence.md +16 -4
  14. package/runtime/prompts/lead/report-writer.md +26 -14
  15. package/runtime/prompts/lead/team-contract.md +2 -1
  16. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  17. package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
  18. package/runtime/prompts/profiles/final-verification.md +1 -1
  19. package/runtime/prompts/profiles/implementation-planning.md +12 -7
  20. package/runtime/python/okstra_ctl/analysis_packet.py +1 -0
  21. package/runtime/python/okstra_ctl/dispatch_state.py +5 -1
  22. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +7 -0
  23. package/runtime/python/okstra_ctl/report_finalize.py +9 -4
  24. package/runtime/python/okstra_ctl/worker_prompt_body.py +4 -2
  25. package/runtime/python/okstra_ctl/worker_prompt_contract.py +41 -1
  26. package/runtime/python/okstra_ctl/worker_prompt_headers.py +3 -0
  27. package/runtime/templates/implementation-worker-preamble.md +12 -3
  28. package/runtime/templates/report-writer-prompt-preamble.md +5 -1
  29. package/runtime/templates/worker-prompt-preamble.md +12 -3
  30. package/runtime/validators/validate-implementation-plan-stages.py +170 -28
  31. package/runtime/validators/validate-run.py +137 -0
@@ -42,7 +42,7 @@
42
42
  - **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.
43
43
  - **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.
44
44
  - **YAGNI ruthlessly**: drop features, abstractions, and configuration knobs that do not serve the stated requirement.
45
- - **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.
45
+ - **Project review-rule preflight**: apply a project review rule pack only when the task brief's `Source Material` or `Reporter Confirmations` cites its exact `SKILL.md` path. Read only that cited file and the `references/*.md` files it directly names. Do not search parent directories or host skill catalogs. 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.
46
46
  - Expected output emphasis:
47
47
  - feasible plan options
48
48
  - dependency and risk visibility
@@ -80,11 +80,16 @@
80
80
  - admissible — the answer selects between behaviours the code must implement, fixes a requirement the plan would otherwise satisfy incorrectly, or resolves a safety/data-integrity question.
81
81
  - NOT admissible → use `Blocks=none` — QA-harness or tooling scope, report notation and wording, numbering or citation-range cleanup, anything inside an option the plan does not recommend, and anything the codebase answers (which the codebase-first rule already forbids raising at all). These belong in `## 5. Missing Information and Risks` or a Working Assumption; they are recorded, not gating.
82
82
  - A row you would answer with "the plan would still produce the same code either way" is by construction `Blocks=none`.
83
- - Section heading contract (BLOCKING — validator scans for these literal English substrings):
84
- - The final report MUST include section headings containing each of the following exact strings — this list mirrors `PLANNING_REQUIRED_SECTIONS` in `validators/validate-run.py`, which is the SSOT: `Option Candidates`, `Trade-off`, `Recommended Option`, `Stage Map`, `Stepwise Execution Order`, `Dependency`, `Validation Checklist`, `Rollback`, `Requirement Coverage`, `Plan Body Verification`, `Cross-Project Dependencies`, `Decision Drafts`. (Approval is no longer a body section it is the YAML frontmatter `approved` field.) Three further headings are enforced elsewhere, not by that scan: `Stage Exit Contract` and `Stage Validation` are per-stage subsections checked by `validators/validate-implementation-plan-stages.py`, and `Implementation Design Preparation` is enforced and rendered by `schemas/final-report-v1.0.schema.json` plus `templates/reports/final-report.template.md` from its required data block.
85
- - Korean translations are allowed in parentheses (e.g. `### Recommended Option (Korean gloss)`), but the English keyword must be present verbatim in the heading line.
86
- - The shape and ordering follow `final-report-template.md` sections 5.4 (`Implementation Plan Deliverables`) + 5.5 (`Stage Map`). `validators/validate-run.py` substring-matches the raw report text, so a Korean-only heading fails the gate — the cause of repeated observed failures.
87
- - Beyond substring matching, when the Plan Body Verification gate result is `passed` / `passed-with-dissent`, `validators/validate-run.py` runs the **structural** Stage Map validator (`validators/validate-implementation-plan-stages.py`) at the planning boundary not deferred to the `implementation` entry gate. It enforces: the exact `## 5.5 Stage Map` heading, each `## 5.5.<i> Stage <i>:` section with its four required subsections, the per-stage effective step count (≤8), the `depends-on` DAG, and the per-stage vertical-slice contract (S10). S10 scans for the literal in-section strings `Slice value:`, `Acceptance:`, the three `Test case (success):` / `Test case (boundary):` / `Test case (failure):` lines (S10d), and the Stepwise `action`-cell prefixes `RED:` / `GREEN:` (or a `TDD exemption:` line, which waives both the test-case lines and the RED/GREEN check) — keep these tokens verbatim for the same reason as the heading keywords above.
83
+ - Deliverable completeness contract (BLOCKING — the schema checks data keys, not heading strings):
84
+ - The plan lives in `data.json` under `implementationPlanning`, and `schemas/final-report-v2.0.schema.json` requires every one of these keys: `optionCandidates`, `tradeoffMatrix`, `recommendedOption`, `stageMap`, `stages`, `dependencyMigrationRisk`, `validationChecklist`, `rollbackStrategy`, `requirementCoverage`, `planBodyVerification`, `crossProjectDependencies`, `decisionDrafts`, `skippedAdrCandidates`, `variationPointAnalysis`, `userNarrative`. A missing block fails schema validation; there is nothing to satisfy by naming a heading. (Approval is not a body section it is the YAML frontmatter `approved` field.)
85
+ - Each `stages[]` entry requires `stage`, `title`, `sliceValue`, `acceptance`, `carryIn`, `stepwiseExecution` (1–6 rows), `exitContract`, and `stageValidation`. Each `stageMap[]` row requires `stage`, `title`, `dependsOn`, `stepCount`, `exitContractSummary`.
86
+ - Beyond the schema, `validators/validate-run.py` reads the same data.json for `_validate_planning_conformance_declared`, `_validate_end_state_coverage`, `_validate_requirement_provenance`, `_validate_stage_has_requirement`, and `_validate_plan_body_state_file`. These run for every planning report regardless of schema version.
87
+ - **Do not chase English heading substrings.** `PLANNING_REQUIRED_SECTIONS` and the Markdown scan in `collect_validation_errors` live inside `validate_phase_boundary`, which returns immediately when `schemaVersion == "2.0"` they gate historical v1 Markdown only. The v2 AI-handoff template renders nine headings and serialises the plan as JSON beneath them, so those substrings cannot appear, and a report is not defective for lacking them.
88
+ - Per-stage vertical slice and TDD contract (BLOCKING — enforced on the data, not on heading tokens):
89
+ - Every stage declares `sliceValue`, `acceptance`, and the three cases `testCaseSuccess` / `testCaseBoundary` / `testCaseFailure` — happy path, edge/boundary input, failure input. **Enforced:** the v2 schema's `if not tddExemption then require` conditional on `ImplementationPlanStage`.
90
+ - The first `stepwiseExecution` row's `action` starts with `RED:` and its `expected` reads FAIL; some later row's `action` starts with `GREEN:` and its `expected` reads PASS. **Enforced (S10c):** `collect_data_validation_errors` in `validators/validate-implementation-plan-stages.py`, run from `validate-run.py` `_append_stage_data_failures`.
91
+ - `tddExemption` waives both rules above, and only for `doc-only`, `config-only`, or `pure-rename` work. An empty or arbitrary reason waives nothing. **Enforced (S10e):** same function — the schema alone cannot reject it, because it types the field as a plain string and keys its conditional on the property merely being present.
92
+ - `stageMap[].dependsOn` must form a DAG (no self-dependency, no unknown stage, no cycle), each row's `stepCount` must equal its stage's actual `stepwiseExecution` row count, and two `(none)`-dependency stages must not name the same file in their `exitContract` — they run as concurrent implementation runs in separate worktrees. **Enforced (S8/S4/S9):** same function.
88
93
  - Required deliverable shape (final report, in addition to the standard sections):
89
94
  - at least two implementation options. **Each option must include**:
90
95
  - **File Structure**: an explicit list of files to create / modify / delete with each file's responsibility (one-line each). Use the form `Create: path — responsibility` / `Modify: path:line-range — change summary` / `Delete: path — reason`. Write every `path` in full and `<PROJECT_ROOT>`-relative — never ellipsis-abbreviated (`…` / `...` / a trailing `/…`); an abbreviated path does not resolve and is rejected by plan-body verification as a kind-b path mismatch.
@@ -188,7 +193,7 @@
188
193
  3. **Internal consistency** — option file lists, trade-off matrix, and recommended step list must agree on file paths, names, and signatures. A symbol called `clearLayers()` in the matrix and `clearFullLayers()` in the steps is a bug.
189
194
  4. **Ambiguity check** — any requirement that could be read two ways must be made explicit or moved to the `## 1. Clarification Items` table as a `Blocks=approval` row.
190
195
  5. **Scope check** — if the recommended plan now spans multiple independent subsystems, recommend splitting into separate planning runs rather than shipping an oversized plan. Then walk the plan in the expansion direction: for every stage, name the Requirement Coverage row that demanded it, and for every requirement row, read its `Source` cell as a skeptic — does the cited brief heading actually exist, and does a `derived:` rationale state a real technical consequence rather than a preference? Move anything that fails to a `Blocks=approval` clarification row.
191
- 6. **Review-rule preflight check** — if a project review rule pack exists, map each relevant rule to the recommended option. Reject the draft if it knowingly creates a violation that the later PR reviewer would flag, unless the plan records a specific rationale and follow-up. In particular, scan for repeated helper stacks across planned files, tests that assert delegation to the same calculator/helper they exercise, public names that hide side effects, domain rules placed in repositories/adapters, and APIs made dead by this change.
196
+ 6. **Review-rule preflight check** — if the task brief cites a project review rule pack under `Source Material` or `Reporter Confirmations`, map each relevant rule to the recommended option. Reject the draft if it knowingly creates a violation that the later PR reviewer would flag, unless the plan records a specific rationale and follow-up. In particular, scan for repeated helper stacks across planned files, tests that assert delegation to the same calculator/helper they exercise, public names that hide side effects, domain rules placed in repositories/adapters, and APIs made dead by this change.
192
197
  7. **Plan-body verification reconciliation (BLOCKING for implementation-planning).** For every §5.5.9 `planItems[]` entry whose verdicts make it `majority-disagree`, set that item's `clarificationId` to a `C-<N>` row that MUST exist in `## 1. Clarification Items` with `Kind` chosen per the standard policy and `Blocks=approval`. **Enforced:** `validators/validate-run.py` `_validate_plan_body_clarification_matching` recomputes each item's class and fails when a majority-disagree item has no `clarificationId`, or its `clarificationId` is dangling / points at a non-`approval` row. For `partial-consensus` and `dissent-isolated` plan-items, the dissenting opinion lives in §5.5.9 `Dissent log` and is NOT promoted to §5.
193
198
  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.
194
199
  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`.
@@ -53,6 +53,7 @@ WORKER_PROFILE_SECTIONS_BY_TASK_TYPE = {
53
53
  ),
54
54
  "implementation-planning": (
55
55
  "Worker planning procedure",
56
+ "Pre-planning context exploration",
56
57
  "Design principles applied when scoring options",
57
58
  ),
58
59
  "final-verification": (
@@ -275,7 +275,11 @@ def validate_initial_prompts(
275
275
  PromptRecord(job.worker_id, job.dispatch_kind, job.prompt_path)
276
276
  for job in jobs
277
277
  ]
278
- errors = validate_initial_prompt_records(manifest=manifest, records=records)
278
+ errors = validate_initial_prompt_records(
279
+ manifest=manifest,
280
+ records=records,
281
+ require_evidence_ledger=True,
282
+ )
279
283
  if errors:
280
284
  task_type = require_string(manifest, "taskType")
281
285
  raise DispatchError(f"{task_type} prompt contract: " + "; ".join(errors))
@@ -521,6 +521,7 @@ def _validate_prepublication_set(
521
521
  errors = validate_initial_prompt_records(
522
522
  manifest=context.manifest,
523
523
  records=[record],
524
+ require_evidence_ledger=True,
524
525
  )
525
526
  if errors:
526
527
  raise InitialPromptMaterializationError(
@@ -530,6 +531,7 @@ def _validate_prepublication_set(
530
531
  errors = validate_initial_prompt_records(
531
532
  manifest=context.manifest,
532
533
  records=records,
534
+ require_evidence_ledger=True,
533
535
  )
534
536
  if errors:
535
537
  raise InitialPromptMaterializationError(
@@ -577,6 +579,7 @@ def _validate_existing_prompt(
577
579
  basic_errors = validate_initial_prompt_records(
578
580
  manifest=context.manifest,
579
581
  records=[basic_record],
582
+ require_evidence_ledger=True,
580
583
  )
581
584
  if basic_errors:
582
585
  raise InitialPromptMaterializationError(
@@ -593,6 +596,7 @@ def _validate_existing_prompt(
593
596
  expected_errors = validate_initial_prompt_records(
594
597
  manifest=context.manifest,
595
598
  records=[expected_record],
599
+ require_evidence_ledger=True,
596
600
  )
597
601
  if expected_errors:
598
602
  reason = _existing_metadata_reason(expected_errors)
@@ -765,6 +769,7 @@ def _validate_published_set(
765
769
  errors = validate_initial_prompt_records(
766
770
  manifest=context.manifest,
767
771
  records=[record],
772
+ require_evidence_ledger=True,
768
773
  )
769
774
  if errors:
770
775
  raise InitialPromptMaterializationError(
@@ -774,6 +779,7 @@ def _validate_published_set(
774
779
  errors = validate_initial_prompt_records(
775
780
  manifest=context.manifest,
776
781
  records=records,
782
+ require_evidence_ledger=True,
777
783
  )
778
784
  if errors:
779
785
  raise InitialPromptMaterializationError(
@@ -983,6 +989,7 @@ def _validate_existing_record_subset(
983
989
  errors = validate_initial_prompt_records(
984
990
  manifest=manifest,
985
991
  records=records,
992
+ require_evidence_ledger=True,
986
993
  )
987
994
  if errors:
988
995
  raise InitialPromptMaterializationError(
@@ -1,10 +1,15 @@
1
1
  """Phase 7 report post-processing — the single reference point.
2
2
 
3
3
  Phase 7 turns a Phase 6 final-report data.json into shippable artifacts through
4
- four ordered steps: usage substitution, html view rendering, follow-up task
5
- spawning, and run validation. The order is load-bearing — rendering before
6
- substitution ships `--` token cells, and validating before rendering trips the
7
- report-views contract.
4
+ five ordered steps: English-SSOT verification, usage substitution, html view
5
+ rendering, follow-up task spawning, and run validation. The order is
6
+ load-bearing — rendering before substitution ships `--` token cells, and
7
+ validating before rendering trips the report-views contract.
8
+
9
+ The translation sidecar is NOT one of these steps. `render-views` overlays it,
10
+ so a non-English run dispatches the translator before this sequence starts —
11
+ after verifying the data.json is English, which is why `check-source` is also
12
+ available as a standalone command.
8
13
 
9
14
  Every lead adapter drives Phase 7 through this module: the Codex adapter calls
10
15
  it in-process (``codex_dispatch``), and a Claude-led run reaches the same code
@@ -128,6 +128,7 @@ def report_writer_input_lines(
128
128
  team_state: Mapping[str, Any],
129
129
  ) -> list[str]:
130
130
  inputs = [
131
+ ("Analysis packet", instruction_path(manifest, active_context, "analysisPacketPath")),
131
132
  ("Task brief", instruction_path(manifest, active_context, "taskBriefPath")),
132
133
  ("Analysis profile", instruction_path(manifest, active_context, "analysisProfilePath")),
133
134
  ("Analysis material", instruction_path(manifest, active_context, "analysisMaterialPath")),
@@ -193,8 +194,9 @@ def run_path(
193
194
 
194
195
  def mcp_pointer_line() -> str:
195
196
  return (
196
- '**MCP servers:** follow the task brief\'s "## Available MCP Servers" '
197
- "section (already in your Required reading)."
197
+ '**MCP servers:** follow the analysis packet\'s "Available MCP Servers" '
198
+ "section. If the section is absent or says none, treat MCP as unavailable "
199
+ "for this run; never infer tools from host configuration."
198
200
  )
199
201
 
200
202
 
@@ -11,6 +11,7 @@ from .worker_prompt_policy import (
11
11
  PromptPlan,
12
12
  resolve_prompt_plan_for_manifest,
13
13
  )
14
+ from .worker_prompt_headers import EVIDENCE_LEDGER_HEADER
14
15
 
15
16
 
16
17
  MAX_FINAL_VERIFICATION_DIRECTIVE_LINES = 40
@@ -24,6 +25,7 @@ FORBIDDEN_ACTIONS_HEADER = "**Forbidden actions:**"
24
25
 
25
26
  _DIRECTIVE_HEADING = "## Run-specific directive"
26
27
  _WORKER_ERROR_CONTRACT_HEADER = "**Worker Error Contract Path:**"
28
+ _EVIDENCE_LEDGER_HEADER_PREFIX = "**Evidence ledger:**"
27
29
  _PRIMARY_PACKET_RE = re.compile(
28
30
  r"(?im)^-\s+Primary analysis packet:\s+`[^`\n]*analysis-packet\.md`\s*$"
29
31
  )
@@ -45,6 +47,7 @@ _NON_BODY_PREFIXES = (
45
47
  "**Audit sidecar path:**",
46
48
  "Assigned worker prompt history path:",
47
49
  "**Worker Preamble Path:**",
50
+ _EVIDENCE_LEDGER_HEADER_PREFIX,
48
51
  *ERRORS_PATH_HEADERS,
49
52
  "**Read scope:**",
50
53
  "**File write mode:**",
@@ -239,8 +242,13 @@ def validate_initial_prompt_records(
239
242
  *,
240
243
  manifest: Mapping[str, Any],
241
244
  records: Sequence[PromptRecord],
245
+ require_evidence_ledger: bool = False,
242
246
  ) -> list[str]:
243
- """Validate prompt audiences and compare their normalized equality groups."""
247
+ """Validate prompt audiences and compare their normalized equality groups.
248
+
249
+ Newly published prompts opt into the evidence-ledger requirement. Persisted
250
+ historical prompts still validate under the contract they were written with.
251
+ """
244
252
  errors: list[str] = []
245
253
  equality_groups: dict[str, dict[str, str]] = {}
246
254
  for record in records:
@@ -262,6 +270,15 @@ def validate_initial_prompt_records(
262
270
  f"{record.worker_id}: {error}"
263
271
  for error in _validate_record_metadata(text, record)
264
272
  )
273
+ errors.extend(
274
+ f"{record.worker_id}: {error}"
275
+ for error in _validate_evidence_ledger_header(
276
+ text,
277
+ plan,
278
+ record.dispatch_kind,
279
+ required=require_evidence_ledger,
280
+ )
281
+ )
265
282
  if plan.equality_group:
266
283
  group = equality_groups.setdefault(plan.equality_group, {})
267
284
  group[record.worker_id] = text
@@ -289,6 +306,29 @@ def _validate_record_metadata(text: str, record: PromptRecord) -> list[str]:
289
306
  return errors
290
307
 
291
308
 
309
+ def _validate_evidence_ledger_header(
310
+ text: str,
311
+ plan: PromptPlan,
312
+ dispatch_kind: str,
313
+ *,
314
+ required: bool,
315
+ ) -> list[str]:
316
+ values = _header_values(text, _EVIDENCE_LEDGER_HEADER_PREFIX)
317
+ if dispatch_kind != "initial":
318
+ return []
319
+ if plan.audience == "report-writer":
320
+ if values:
321
+ return ["Evidence ledger header is forbidden for report-writer"]
322
+ return []
323
+ if not values:
324
+ if required:
325
+ return [f"exactly one `{EVIDENCE_LEDGER_HEADER}` header is required"]
326
+ return []
327
+ if values != ["required-v1"]:
328
+ return [f"exactly one `{EVIDENCE_LEDGER_HEADER}` header is required"]
329
+ return []
330
+
331
+
292
332
  def _validate_delivery_mode(
293
333
  values: list[str],
294
334
  expected: str | None,
@@ -36,6 +36,7 @@ READ_SCOPE_HEADER = (
36
36
  "un-enumerated file seems essential, record it under *Missing Information "
37
37
  "or Assumptions* instead of reading it."
38
38
  )
39
+ EVIDENCE_LEDGER_HEADER = "**Evidence ledger:** required-v1"
39
40
 
40
41
  # `agy`'s write tool validates the target against the Gemini artifact store
41
42
  # whenever the model attaches ArtifactMetadata, and rejects every path outside
@@ -101,6 +102,8 @@ def worker_prompt_headers(
101
102
  headers.append(
102
103
  f"**Coding preflight pack:** {_coding_preflight_pack_path(active_context)}"
103
104
  )
105
+ if dispatch_kind == "initial" and plan.audience != "report-writer":
106
+ headers.append(EVIDENCE_LEDGER_HEADER)
104
107
  headers.extend([
105
108
  f"**Errors log path:** {errors_log_path}",
106
109
  f"**Errors sidecar path:** {errors_sidecar_path}",
@@ -25,6 +25,14 @@ Work like a senior engineer who owns this result, not a commentator on it.
25
25
  - Only the executor may mutate source files. Verifiers remain read-only except for okstra result/audit artifacts and project-declared QA commands.
26
26
  - Execute the role sidecar's pre-write, post-write, QA, and return gates without substituting analysis Sections 1–6 or report-authoring instructions.
27
27
 
28
+ ## Evidence read ledger
29
+
30
+ When `**Evidence ledger:** required-v1` is present, append one canonical row to the audit sidecar immediately after opening every file used as claim evidence:
31
+
32
+ - Evidence read: `<project-relative path without a line suffix>`
33
+
34
+ Every file citation in the result MUST use backticks and a line suffix, for example `src/config/env.ts:1-22`. A cited path without a matching ledger row fails Phase 7 in `validators/validate-run.py` `validate_worker_results_audit()`. Do not add a row for a file you did not open.
35
+
28
36
  ## Anchor headers
29
37
 
30
38
  Every initial implementation prompt begins with these generated common anchors in this exact order, before its implementation-specific body:
@@ -36,9 +44,10 @@ Every initial implementation prompt begins with these generated common anchors i
36
44
  5. `**Worker Preamble Path:** <absolute-path>`
37
45
  6. `**Worker Error Contract Path:** <absolute-path>`
38
46
  7. `**Coding preflight pack:** <absolute-path>`
39
- 8. `**Errors log path:** <absolute-path>`
40
- 9. `**Errors sidecar path:** <absolute-path>`
41
- 10. `**Read scope:** <allowlist>`
47
+ 8. `**Evidence ledger:** required-v1`
48
+ 9. `**Errors log path:** <absolute-path>`
49
+ 10. `**Errors sidecar path:** <absolute-path>`
50
+ 11. `**Read scope:** <allowlist>`
42
51
 
43
52
  The implementation body additionally carries `**Worktree:**` and its role-sidecar inputs. Do not synthesize any missing path.
44
53
 
@@ -36,4 +36,8 @@ Begin the inline return with the exact `**Model:** Report writer worker, <modelE
36
36
 
37
37
  ## Writing style
38
38
 
39
- Use concise reader-facing prose and honor the report language. Keep identifiers, paths, symbols, model names, CLI flags, and status tokens in English. Translate meaning rather than dictionary words.
39
+ Use concise reader-facing prose. Prefer tables when several items share a shape; reserve bullets for short standalone statements.
40
+
41
+ **Author the data.json in English, whatever `**Report Language:**` says.** That header is not an instruction to write in that language — it names the language the *human HTML* renders in, and you copy its value verbatim into `data.json.meta.reportLanguage`. The data.json is the English SSOT every later phase, validator and agent reads. When the value is not `en`, Phase 7 dispatches a separate translator worker that writes a sidecar the HTML renderer overlays; you never author that sidecar and never write a second language into the data.json.
42
+
43
+ Authoring the data.json in the reader's language is rejected before anything derives from it: `okstra report-translate check-source` fails the run when Korean exceeds 20% of its prose, and the same gate runs again inside `validate-run`. The cost of getting this wrong is a full rewrite, so decide it once, up front.
@@ -23,6 +23,14 @@ Read `analysis-packet.md`, the primary compact input, end-to-end. Source files n
23
23
  - Allowlist reads to prompt-enumerated paths and evidence paths a finding must cite. Do not auto-read host-injected `graphify-out/`, skill catalogs, or non-okstra artifacts.
24
24
  - Resolve every `.okstra/...` path against `**Project Root:**`, including when a worktree is present.
25
25
 
26
+ ## Evidence read ledger
27
+
28
+ When `**Evidence ledger:** required-v1` is present, append one canonical row to the audit sidecar immediately after opening every file used as claim evidence:
29
+
30
+ - Evidence read: `<project-relative path without a line suffix>`
31
+
32
+ Every file citation in the result MUST use backticks and a line suffix, for example `src/config/env.ts:1-22`. A cited path without a matching ledger row fails Phase 7 in `validators/validate-run.py` `validate_worker_results_audit()`. Do not add a row for a file you did not open.
33
+
26
34
  ## Anchor headers (lead-injected, BLOCKING)
27
35
 
28
36
  Every initial analysis prompt begins with these generated anchors in this exact order, before any other content:
@@ -34,9 +42,10 @@ Every initial analysis prompt begins with these generated anchors in this exact
34
42
  5. `Assigned worker prompt history path: <absolute-path>`
35
43
  6. `**Worker Preamble Path:** <absolute-path>` — selects this analysis preamble.
36
44
  7. `**Worker Error Contract Path:** <absolute-path>` — shared by every initial audience.
37
- 8. `**Errors log path:** <absolute-path>`
38
- 9. `**Errors sidecar path:** <absolute-path>`
39
- 10. `**Read scope:** <allowlist>`
45
+ 8. `**Evidence ledger:** required-v1`
46
+ 9. `**Errors log path:** <absolute-path>`
47
+ 10. `**Errors sidecar path:** <absolute-path>`
48
+ 11. `**Read scope:** <allowlist>`
40
49
 
41
50
  `final-verification` additionally carries its six verification-target anchors. `improvement-discovery` carries `**Phase 1.5 Grilling Log:**`. Reverify prompts are lightweight and do not use this preamble.
42
51
 
@@ -59,6 +59,30 @@ def _check_stage_map_present(text: str) -> List[ValidationError]:
59
59
  return []
60
60
 
61
61
 
62
+ def _parse_depends_on_cell(raw: str) -> List[int] | None:
63
+ """Stage numbers in a `depends-on` cell; None when the cell is unparseable.
64
+
65
+ Schema v2 keeps the same literal cell text in `stageMap[].dependsOn`, so
66
+ both the Markdown scan and the data scan read it through here.
67
+ """
68
+ value = raw.strip()
69
+ if value in ("(none)", ""):
70
+ return []
71
+ try:
72
+ return [int(x.strip()) for x in value.split(",") if x.strip()]
73
+ except ValueError:
74
+ return None
75
+
76
+
77
+ def _stage_numbers_monotonic(stages: List[StageMeta]) -> List[ValidationError]:
78
+ return [
79
+ ValidationError("S2", r.stage_number,
80
+ f"stage numbers must be 1..N monotonic, got {r.stage_number} at row {i}")
81
+ for i, r in enumerate(stages, start=1)
82
+ if r.stage_number != i
83
+ ]
84
+
85
+
62
86
  def _parse_stage_map(text: str) -> Tuple[List[StageMeta], List[ValidationError]]:
63
87
  m = STAGE_MAP_HEADING.search(text)
64
88
  if not m:
@@ -80,29 +104,19 @@ def _parse_stage_map(text: str) -> Tuple[List[StageMeta], List[ValidationError]]
80
104
  n = int(cells[0])
81
105
  except ValueError:
82
106
  continue
83
- depends_raw = cells[2].strip()
84
- if depends_raw in ("(none)", ""):
85
- depends = []
86
- else:
87
- try:
88
- depends = [int(x.strip()) for x in depends_raw.split(",") if x.strip()]
89
- except ValueError:
90
- # 비정수 depends_on 셀 → 행 skip (비정수 stage_number 와 동일). raw
91
- # ValueError 를 흘리면 _stage_map_reject_detail·handoff 의 except
92
- # PrepareError 를 우회해 traceback 으로 죽으므로, 누락된 행을 하류
93
- # S2(비단조) 검사가 잡게 한다.
94
- continue
107
+ depends = _parse_depends_on_cell(cells[2])
108
+ if depends is None:
109
+ # 비정수 depends_on 셀 → 행 skip (비정수 stage_number 와 동일). raw
110
+ # ValueError 를 흘리면 _stage_map_reject_detail·handoff 의 except
111
+ # PrepareError 를 우회해 traceback 으로 죽으므로, 누락된 행을 하류
112
+ # S2(비단조) 검사가 잡게 한다.
113
+ continue
95
114
  try:
96
115
  step_count = int(cells[3])
97
116
  except ValueError:
98
117
  step_count = -1
99
118
  rows.append(StageMeta(n, cells[1], depends, step_count, cells[4]))
100
- errors: List[ValidationError] = []
101
- for i, r in enumerate(rows, start=1):
102
- if r.stage_number != i:
103
- errors.append(ValidationError("S2", r.stage_number,
104
- f"stage numbers must be 1..N monotonic, got {r.stage_number} at row {i}"))
105
- return rows, errors
119
+ return rows, _stage_numbers_monotonic(rows)
106
120
 
107
121
 
108
122
  def _slice_stage_section(text: str, stage_number: int) -> str:
@@ -358,16 +372,8 @@ def _extract_exit_contract_files(section: str) -> set:
358
372
  return set(PATH_TOKEN.findall(body))
359
373
 
360
374
 
361
- def _check_parallel_safety(text: str, stages: List[StageMeta]) -> List[ValidationError]:
362
- """S9: two `depends-on (none)` stages must not predict the same file —
363
- otherwise two parallel implementation runs would edit it concurrently."""
364
- files = {
365
- s.stage_number: _extract_exit_contract_files(
366
- _slice_stage_section(text, s.stage_number)
367
- )
368
- for s in stages
369
- if not s.depends_on
370
- }
375
+ def _report_shared_parallel_files(files: dict) -> List[ValidationError]:
376
+ """S9 over an already-extracted {stage_number: {path}} map."""
371
377
  errs: List[ValidationError] = []
372
378
  nums = sorted(files)
373
379
  for i in range(len(nums)):
@@ -381,6 +387,18 @@ def _check_parallel_safety(text: str, stages: List[StageMeta]) -> List[Validatio
381
387
  return errs
382
388
 
383
389
 
390
+ def _check_parallel_safety(text: str, stages: List[StageMeta]) -> List[ValidationError]:
391
+ """S9: two `depends-on (none)` stages must not predict the same file —
392
+ otherwise two parallel implementation runs would edit it concurrently."""
393
+ return _report_shared_parallel_files({
394
+ s.stage_number: _extract_exit_contract_files(
395
+ _slice_stage_section(text, s.stage_number)
396
+ )
397
+ for s in stages
398
+ if not s.depends_on
399
+ })
400
+
401
+
384
402
  def collect_validation_errors(text: str) -> List[ValidationError]:
385
403
  """All S1–S11 checks against the report text; empty list means valid.
386
404
 
@@ -404,6 +422,130 @@ def collect_validation_errors(text: str) -> List[ValidationError]:
404
422
  return errors
405
423
 
406
424
 
425
+ def _data_stage_metas(
426
+ stage_map: List[dict],
427
+ ) -> Tuple[List[StageMeta], List[ValidationError]]:
428
+ rows = []
429
+ for row in stage_map:
430
+ if not isinstance(row, dict) or not isinstance(row.get("stage"), int):
431
+ continue
432
+ depends = _parse_depends_on_cell(str(row.get("dependsOn") or ""))
433
+ if depends is None:
434
+ continue
435
+ rows.append(StageMeta(
436
+ row["stage"],
437
+ str(row.get("title") or ""),
438
+ depends,
439
+ row.get("stepCount") if isinstance(row.get("stepCount"), int) else -1,
440
+ str(row.get("exitContractSummary") or ""),
441
+ ))
442
+ return rows, _stage_numbers_monotonic(rows)
443
+
444
+
445
+ def _check_data_slice_tdd(stage: dict) -> List[ValidationError]:
446
+ """S10c / S10e over one schema-v2 `stages[]` entry.
447
+
448
+ The schema already requires `sliceValue`, `acceptance`, and — through its
449
+ `if not tddExemption then testCase*` conditional — the three test cases, so
450
+ S10a/S10b/S10d are covered declaratively. Two rules a JSON Schema cannot
451
+ state are left: the RED→GREEN ordering across `stepwiseExecution` rows, and
452
+ that a `tddExemption` naming no allowed category cannot waive them. The
453
+ schema types `tddExemption` as a plain string, so `""` currently satisfies
454
+ the conditional and drops all three test cases with no reason given.
455
+ """
456
+ number = stage.get("stage") if isinstance(stage.get("stage"), int) else 0
457
+ if "tddExemption" in stage:
458
+ reason = str(stage.get("tddExemption") or "").lower()
459
+ if not any(cat in reason for cat in TDD_EXEMPTION_ALLOWED):
460
+ return [ValidationError("S10", number,
461
+ "S10e: 'tddExemption' reason must be one of "
462
+ + " / ".join(TDD_EXEMPTION_ALLOWED)
463
+ + " — an empty or arbitrary reason cannot waive RED/GREEN "
464
+ "and the three test cases")]
465
+ return []
466
+
467
+ steps = [s for s in (stage.get("stepwiseExecution") or []) if isinstance(s, dict)]
468
+ actions = [str(s.get("action") or "") for s in steps]
469
+ if not (actions and actions[0].startswith("RED:")
470
+ and any(a.startswith("GREEN:") for a in actions)):
471
+ return [ValidationError("S10", number,
472
+ "S10c: first stepwiseExecution action must start with 'RED:' and "
473
+ "some action with 'GREEN:', or declare a 'tddExemption'")]
474
+
475
+ errs: List[ValidationError] = []
476
+ for step in steps:
477
+ action = str(step.get("action") or "")
478
+ expected = str(step.get("expected") or "")
479
+ if action.startswith("RED:") and "FAIL" not in expected.upper():
480
+ errs.append(ValidationError("S10", number,
481
+ f"S10c: 'RED:' step's expected must read FAIL, got '{expected}'"))
482
+ elif action.startswith("GREEN:") and "PASS" not in expected.upper():
483
+ errs.append(ValidationError("S10", number,
484
+ f"S10c: 'GREEN:' step's expected must read PASS, got '{expected}'"))
485
+ return errs
486
+
487
+
488
+ def _check_data_step_counts(
489
+ stage_map: List[StageMeta], stages: List[dict]
490
+ ) -> List[ValidationError]:
491
+ """The Stage Map row is the plan's own index of its stage body. A row
492
+ claiming a step count the body does not have makes the map unusable for
493
+ sizing a stage, which is the only reason the cell exists."""
494
+ by_number = {
495
+ s.get("stage"): s for s in stages
496
+ if isinstance(s, dict) and isinstance(s.get("stage"), int)
497
+ }
498
+ errs: List[ValidationError] = []
499
+ for meta in stage_map:
500
+ stage = by_number.get(meta.stage_number)
501
+ if stage is None:
502
+ errs.append(ValidationError("S3", meta.stage_number,
503
+ "stageMap row has no matching stages[] entry"))
504
+ continue
505
+ actual = len([
506
+ s for s in (stage.get("stepwiseExecution") or []) if isinstance(s, dict)
507
+ ])
508
+ if meta.step_count != actual:
509
+ errs.append(ValidationError("S4", meta.stage_number,
510
+ f"stageMap stepCount {meta.step_count} != "
511
+ f"{actual} stepwiseExecution rows"))
512
+ for number in sorted(n for n in by_number if n not in {m.stage_number for m in stage_map}):
513
+ errs.append(ValidationError("S3", number,
514
+ "stages[] entry has no matching stageMap row"))
515
+ return errs
516
+
517
+
518
+ def collect_data_validation_errors(planning: dict) -> List[ValidationError]:
519
+ """The S-checks that schema v2 cannot express, over `implementationPlanning`.
520
+
521
+ `collect_validation_errors` scans rendered v1 Markdown, and
522
+ `validate_phase_boundary` returns before calling it for a v2 report — so on
523
+ the current schema nothing enforced the depends-on DAG, parallel-stage file
524
+ safety, RED→GREEN ordering, or the TDD-exemption vocabulary. The schema
525
+ covers presence and cardinality; this covers the relationships between
526
+ fields, which is what a JSON Schema has no way to say.
527
+ """
528
+ stage_map, errors = _data_stage_metas(planning.get("stageMap") or [])
529
+ stages = [s for s in (planning.get("stages") or []) if isinstance(s, dict)]
530
+ if not stage_map and not stages:
531
+ return errors
532
+
533
+ errors.extend(_check_data_step_counts(stage_map, stages))
534
+ errors.extend(_check_depends_on(stage_map))
535
+ errors.extend(_report_shared_parallel_files({
536
+ meta.stage_number: set(PATH_TOKEN.findall(
537
+ str((next(
538
+ (s for s in stages if s.get("stage") == meta.stage_number), {}
539
+ )).get("exitContract") or "")
540
+ ))
541
+ for meta in stage_map
542
+ if not meta.depends_on
543
+ }))
544
+ for stage in stages:
545
+ errors.extend(_check_data_slice_tdd(stage))
546
+ return errors
547
+
548
+
407
549
  def main(argv: List[str]) -> int:
408
550
  p = argparse.ArgumentParser()
409
551
  p.add_argument("--plan", required=True)