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
@@ -103,6 +103,7 @@ from okstra_ctl.worker_prompt_contract import ( # noqa: E402
103
103
  PromptRecord,
104
104
  validate_initial_prompt_records,
105
105
  )
106
+ from okstra_ctl.worker_prompt_headers import EVIDENCE_LEDGER_HEADER # noqa: E402
106
107
  from validate_analysis_report import validate_analysis_report # noqa: E402
107
108
  from okstra_ctl.convergence_engine import ( # noqa: E402
108
109
  grouped_input_digest,
@@ -2378,6 +2379,16 @@ def validate_report(
2378
2379
  _WORKER_RESULT_BASENAME_RE = re.compile(
2379
2380
  r"^(?P<worker>[a-z][a-z0-9-]*-worker)-(?P<task_type>[a-z][a-z-]*?)-(?P<seq>\d{3})\.md$"
2380
2381
  )
2382
+ _EVIDENCE_READ_RE = re.compile(
2383
+ r"^- Evidence read: `(?P<path>[^`\n]+)`\s*$",
2384
+ re.MULTILINE,
2385
+ )
2386
+ _FILE_LINE_CITATION_RE = re.compile(
2387
+ r"`(?P<path>(?!https?://)[^`\n]+?):(?P<line>\d+(?:-\d+)?)`"
2388
+ )
2389
+ _EXTENSIONLESS_SOURCE_FILENAMES = frozenset(
2390
+ {"Dockerfile", "Justfile", "Makefile", "Procfile", "Rakefile"}
2391
+ )
2381
2392
 
2382
2393
  _REPORT_BASENAME_SEQ_RE = re.compile(r"-(?P<seq>\d{3})(?:\.data)?\.(?:md|json)$")
2383
2394
 
@@ -2390,6 +2401,90 @@ def _report_run_seq(report_path: Path) -> str | None:
2390
2401
  return match.group("seq") if match else None
2391
2402
 
2392
2403
 
2404
+ def _cited_file_paths(content: str) -> set[str]:
2405
+ paths: set[str] = set()
2406
+ for match in _FILE_LINE_CITATION_RE.finditer(content):
2407
+ path = match.group("path")
2408
+ if _looks_like_file_path(path):
2409
+ paths.add(path)
2410
+ return paths
2411
+
2412
+
2413
+ def _looks_like_file_path(path: str) -> bool:
2414
+ if (
2415
+ not path
2416
+ or path.startswith(("-", "$"))
2417
+ or any(char.isspace() for char in path)
2418
+ ):
2419
+ return False
2420
+ if re.fullmatch(r"[0-9a-fA-F]{7,64}", path):
2421
+ return False
2422
+ return (
2423
+ "/" in path
2424
+ or "." in Path(path).name
2425
+ or Path(path).name in _EXTENSIONLESS_SOURCE_FILENAMES
2426
+ )
2427
+
2428
+
2429
+ def _audit_evidence_read_paths(content: str) -> set[str]:
2430
+ return {
2431
+ match.group("path")
2432
+ for match in _EVIDENCE_READ_RE.finditer(content)
2433
+ }
2434
+
2435
+
2436
+ def _worker_prompt_path(
2437
+ report_path: Path,
2438
+ worker_role: str,
2439
+ task_type: str,
2440
+ seq: str,
2441
+ ) -> Path:
2442
+ return (
2443
+ report_path.parent.parent
2444
+ / "prompts"
2445
+ / f"{worker_role}-prompt-{task_type}-{seq}.md"
2446
+ )
2447
+
2448
+
2449
+ def _validate_worker_evidence_read_ledger(
2450
+ *,
2451
+ report_path: Path,
2452
+ worker_role: str,
2453
+ task_type: str,
2454
+ seq: str,
2455
+ result_name: str,
2456
+ result_content: str,
2457
+ audit_path: Path,
2458
+ failures: list[str],
2459
+ ) -> None:
2460
+ if worker_role == "report-writer-worker":
2461
+ return
2462
+ prompt_path = _worker_prompt_path(report_path, worker_role, task_type, seq)
2463
+ try:
2464
+ prompt_content = prompt_path.read_text(encoding="utf-8")
2465
+ except OSError:
2466
+ return
2467
+ if EVIDENCE_LEDGER_HEADER not in prompt_content.splitlines():
2468
+ return
2469
+ try:
2470
+ audit_content = audit_path.read_text(encoding="utf-8")
2471
+ except OSError as exc:
2472
+ failures.append(
2473
+ f"worker audit sidecar unreadable: {audit_path.name} ({exc})"
2474
+ )
2475
+ return
2476
+
2477
+ missing_paths = sorted(
2478
+ _cited_file_paths(result_content) - _audit_evidence_read_paths(audit_content)
2479
+ )
2480
+ for missing_path in missing_paths:
2481
+ failures.append(
2482
+ f"worker `{worker_role}` result `{result_name}` cites "
2483
+ f"`{missing_path}:line` without an Evidence read row for "
2484
+ f"`{missing_path}` in `{audit_path.name}`"
2485
+ )
2486
+
2487
+
2393
2488
  def validate_worker_results_audit(
2394
2489
  report_path: Path, task_type: str, failures: list[str]
2395
2490
  ) -> None:
@@ -2404,6 +2499,10 @@ def validate_worker_results_audit(
2404
2499
  2. The matching audit sidecar exists at
2405
2500
  `<worker>-audit-<task-type>-<seq>.md`. Missing sidecar means the
2406
2501
  worker silently skipped the reading-confirmation step.
2502
+ 3. For new prompts carrying the required-v1 marker, every canonical
2503
+ backticked `path:line` citation has a matching Evidence read row in
2504
+ that audit sidecar. Historical prompts without the marker retain the
2505
+ existence-only contract.
2407
2506
 
2408
2507
  Scoped to this run's seq. `worker-results/` accumulates every run's
2409
2508
  artifacts, so scanning the whole directory judged a run by files it did
@@ -2465,6 +2564,18 @@ def validate_worker_results_audit(
2465
2564
  f"Confirmation block (one short line per input file). Workers "
2466
2565
  f"write this in the same step as the main worker-results file."
2467
2566
  )
2567
+ continue
2568
+
2569
+ _validate_worker_evidence_read_ledger(
2570
+ report_path=report_path,
2571
+ worker_role=worker_role,
2572
+ task_type=task_type,
2573
+ seq=seq,
2574
+ result_name=rel,
2575
+ result_content=content,
2576
+ audit_path=audit_path,
2577
+ failures=failures,
2578
+ )
2468
2579
 
2469
2580
 
2470
2581
  def validate_team_state_usage(team_state: dict, failures: list[str]) -> None:
@@ -6066,6 +6177,31 @@ def _load_stage_validator():
6066
6177
  return mod
6067
6178
 
6068
6179
 
6180
+ def _append_stage_data_failures(data: Mapping[str, Any], failures: list[str]) -> None:
6181
+ """Run the stage relationship checks that schema v2 cannot express.
6182
+
6183
+ `_append_stage_structure_failures` scans rendered Markdown and sits after
6184
+ the v2 early return in `validate_phase_boundary`, so for a v2 report the
6185
+ depends-on DAG, parallel-stage file safety, RED→GREEN ordering, and the
6186
+ TDD-exemption vocabulary had nothing enforcing them. The same validator
6187
+ owns both modes so the rule vocabulary stays defined once.
6188
+ """
6189
+ if (data or {}).get("schemaVersion") != "2.0":
6190
+ return # v1 reports are covered by the Markdown scan.
6191
+ planning = (data or {}).get("implementationPlanning")
6192
+ if not isinstance(planning, Mapping):
6193
+ return # Schema validation already reported the missing block.
6194
+ mod = _load_stage_validator()
6195
+ if mod is None: # pragma: no cover — repo/runtime always ship the file
6196
+ failures.append(f"cannot load Stage Map validator at {_STAGE_VALIDATOR_PATH}")
6197
+ return
6198
+ for e in mod.collect_data_validation_errors(dict(planning)):
6199
+ failures.append(
6200
+ f"implementation-planning stage contract invalid "
6201
+ f"[{e.code} stage={e.stage}]: {e.message}"
6202
+ )
6203
+
6204
+
6069
6205
  def _append_stage_structure_failures(content: str, failures: list[str]) -> None:
6070
6206
  """Enforce the Stage Map structural contract at the implementation-planning
6071
6207
  boundary. Without this, a plan missing `## 5.5 Stage Map` passes the
@@ -7309,6 +7445,7 @@ def main() -> int:
7309
7445
  validation_data, brief_path, failures
7310
7446
  )
7311
7447
  _validate_stage_has_requirement(validation_data, failures)
7448
+ _append_stage_data_failures(validation_data, failures)
7312
7449
  if task_type == "improvement-discovery":
7313
7450
  run_dir = report_path.parent.parent
7314
7451
  _validate_improvement_discovery(report_path, run_dir, brief_path, failures)