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.
- package/docs/cli.md +1 -1
- package/docs/project-structure-overview.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +2 -1
- package/runtime/agents/workers/claude-worker.md +2 -1
- package/runtime/agents/workers/codex-worker.md +2 -1
- package/runtime/agents/workers/grok-worker.md +2 -1
- package/runtime/agents/workers/kimi-worker.md +2 -1
- package/runtime/agents/workers/report-writer-worker.md +1 -1
- package/runtime/bin/okstra-report-translate.py +32 -11
- package/runtime/prompts/launch.template.md +1 -1
- package/runtime/prompts/lead/convergence.md +16 -4
- package/runtime/prompts/lead/report-writer.md +26 -14
- package/runtime/prompts/lead/team-contract.md +2 -1
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +12 -7
- package/runtime/python/okstra_ctl/analysis_packet.py +1 -0
- package/runtime/python/okstra_ctl/dispatch_state.py +5 -1
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +7 -0
- package/runtime/python/okstra_ctl/report_finalize.py +9 -4
- package/runtime/python/okstra_ctl/worker_prompt_body.py +4 -2
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +41 -1
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +3 -0
- package/runtime/templates/implementation-worker-preamble.md +12 -3
- package/runtime/templates/report-writer-prompt-preamble.md +5 -1
- package/runtime/templates/worker-prompt-preamble.md +12 -3
- package/runtime/validators/validate-implementation-plan-stages.py +170 -28
- 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**:
|
|
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
|
-
-
|
|
84
|
-
- The
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
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
|
|
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`.
|
|
@@ -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(
|
|
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
|
-
|
|
5
|
-
spawning, and run validation. The order is
|
|
6
|
-
substitution ships `--` token cells, and
|
|
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
|
|
197
|
-
"section
|
|
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. `**
|
|
40
|
-
9. `**Errors
|
|
41
|
-
10. `**
|
|
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
|
|
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. `**
|
|
38
|
-
9. `**Errors
|
|
39
|
-
10. `**
|
|
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
|
-
|
|
84
|
-
if
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
|
362
|
-
"""S9
|
|
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)
|