okstra 0.150.0 → 0.151.1

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 (33) hide show
  1. package/docs/cli.md +2 -2
  2. package/docs/project-structure-overview.md +1 -1
  3. package/package.json +3 -2
  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 +9 -1
  11. package/runtime/bin/okstra-report-translate.py +56 -2
  12. package/runtime/prompts/launch.template.md +1 -1
  13. package/runtime/prompts/lead/convergence.md +16 -4
  14. package/runtime/prompts/lead/plan-body-verification.md +1 -1
  15. package/runtime/prompts/lead/report-writer.md +28 -16
  16. package/runtime/prompts/lead/team-contract.md +2 -1
  17. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  18. package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
  19. package/runtime/prompts/profiles/final-verification.md +1 -1
  20. package/runtime/prompts/profiles/implementation-planning.md +8 -7
  21. package/runtime/python/okstra_ctl/analysis_packet.py +1 -0
  22. package/runtime/python/okstra_ctl/convergence.py +15 -1
  23. package/runtime/python/okstra_ctl/dispatch_state.py +5 -1
  24. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +7 -0
  25. package/runtime/python/okstra_ctl/report_finalize.py +27 -4
  26. package/runtime/python/okstra_ctl/report_translation.py +29 -0
  27. package/runtime/python/okstra_ctl/worker_prompt_body.py +4 -2
  28. package/runtime/python/okstra_ctl/worker_prompt_contract.py +41 -1
  29. package/runtime/python/okstra_ctl/worker_prompt_headers.py +3 -0
  30. package/runtime/templates/implementation-worker-preamble.md +12 -3
  31. package/runtime/templates/report-writer-prompt-preamble.md +5 -1
  32. package/runtime/templates/worker-prompt-preamble.md +12 -3
  33. package/runtime/validators/validate-run.py +142 -0
@@ -61,6 +61,10 @@ from okstra_ctl.mutation_probe import ( # noqa: E402
61
61
  classify_reason,
62
62
  )
63
63
  from okstra_ctl.self_mock_signals import selfmock_path_key # noqa: E402
64
+ from okstra_ctl.report_translation import ( # noqa: E402
65
+ HANGUL_PROSE_LIMIT,
66
+ hangul_share,
67
+ )
64
68
  from okstra_ctl.stage_citations import cited_stage_numbers # noqa: E402
65
69
  from okstra_ctl.workflow import DEFAULT_NEXT_PHASE, PHASE_SEQUENCE # noqa: E402
66
70
  from okstra_ctl.md_table import ( # noqa: E402
@@ -99,6 +103,7 @@ from okstra_ctl.worker_prompt_contract import ( # noqa: E402
99
103
  PromptRecord,
100
104
  validate_initial_prompt_records,
101
105
  )
106
+ from okstra_ctl.worker_prompt_headers import EVIDENCE_LEDGER_HEADER # noqa: E402
102
107
  from validate_analysis_report import validate_analysis_report # noqa: E402
103
108
  from okstra_ctl.convergence_engine import ( # noqa: E402
104
109
  grouped_input_digest,
@@ -2374,6 +2379,16 @@ def validate_report(
2374
2379
  _WORKER_RESULT_BASENAME_RE = re.compile(
2375
2380
  r"^(?P<worker>[a-z][a-z0-9-]*-worker)-(?P<task_type>[a-z][a-z-]*?)-(?P<seq>\d{3})\.md$"
2376
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
+ )
2377
2392
 
2378
2393
  _REPORT_BASENAME_SEQ_RE = re.compile(r"-(?P<seq>\d{3})(?:\.data)?\.(?:md|json)$")
2379
2394
 
@@ -2386,6 +2401,90 @@ def _report_run_seq(report_path: Path) -> str | None:
2386
2401
  return match.group("seq") if match else None
2387
2402
 
2388
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
+
2389
2488
  def validate_worker_results_audit(
2390
2489
  report_path: Path, task_type: str, failures: list[str]
2391
2490
  ) -> None:
@@ -2400,6 +2499,10 @@ def validate_worker_results_audit(
2400
2499
  2. The matching audit sidecar exists at
2401
2500
  `<worker>-audit-<task-type>-<seq>.md`. Missing sidecar means the
2402
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.
2403
2506
 
2404
2507
  Scoped to this run's seq. `worker-results/` accumulates every run's
2405
2508
  artifacts, so scanning the whole directory judged a run by files it did
@@ -2461,6 +2564,18 @@ def validate_worker_results_audit(
2461
2564
  f"Confirmation block (one short line per input file). Workers "
2462
2565
  f"write this in the same step as the main worker-results file."
2463
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
+ )
2464
2579
 
2465
2580
 
2466
2581
  def validate_team_state_usage(team_state: dict, failures: list[str]) -> None:
@@ -6316,6 +6431,32 @@ def _validate_improvement_discovery(
6316
6431
  failures.append(f"improvement-discovery: {err}")
6317
6432
 
6318
6433
 
6434
+ def _validate_ssot_is_english(data: dict, failures: list[str]) -> None:
6435
+ """Enforce: the final-report data.json is authored in English.
6436
+
6437
+ `meta.reportLanguage` names the language the human HTML renders in, and
6438
+ Phase 7's translator serves it from a sidecar. The data.json itself is the
6439
+ record every later phase, validator and agent reads, so a worker that
6440
+ authors it in the reader's language instead splits the record — and does so
6441
+ silently, because rendering, follow-up spawning and validation all succeed
6442
+ on it.
6443
+
6444
+ `report-finalize` runs the same check as its first step, before anything
6445
+ derives from the report. This is the second gate, for a report that reached
6446
+ validation by some other path.
6447
+ """
6448
+ if not data:
6449
+ return
6450
+ share, length = hangul_share(data)
6451
+ if length and share >= HANGUL_PROSE_LIMIT:
6452
+ failures.append(
6453
+ f"final-report data.json was authored in Korean ({share:.0%} of its "
6454
+ f"prose, limit {HANGUL_PROSE_LIMIT:.0%}). The data.json is the "
6455
+ "English SSOT; meta.reportLanguage selects the human HTML's "
6456
+ "language and is served by the Phase 7 translator sidecar."
6457
+ )
6458
+
6459
+
6319
6460
  def _validate_fix_cycle(run_manifest: dict, data: dict, failures: list[str]) -> None:
6320
6461
  """Enforce: when the run-manifest carries a fixCycleId, the final-report
6321
6462
  data.json MUST contain a fixCycle block whose ``cycle`` matches it.
@@ -7206,6 +7347,7 @@ def main() -> int:
7206
7347
  ),
7207
7348
  )
7208
7349
  validation_data = report_data if isinstance(report_data, Mapping) else {}
7350
+ _validate_ssot_is_english(validation_data, failures)
7209
7351
  _validate_fix_cycle(run_manifest, validation_data, failures)
7210
7352
  validate_report(
7211
7353
  report_path,