okstra 0.128.0 → 0.129.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 (50) hide show
  1. package/docs/architecture/storage-model.md +15 -2
  2. package/docs/architecture.md +21 -5
  3. package/docs/cli.md +13 -2
  4. package/docs/project-structure-overview.md +22 -7
  5. package/package.json +1 -1
  6. package/runtime/BUILD.json +2 -2
  7. package/runtime/agents/workers/antigravity-worker.md +3 -3
  8. package/runtime/agents/workers/claude-worker.md +4 -4
  9. package/runtime/agents/workers/codex-worker.md +3 -3
  10. package/runtime/agents/workers/report-writer-worker.md +6 -6
  11. package/runtime/prompts/lead/adapters/claude-code.md +2 -1
  12. package/runtime/prompts/lead/adapters/codex.md +1 -0
  13. package/runtime/prompts/lead/adapters/external.md +1 -0
  14. package/runtime/prompts/lead/convergence.md +42 -84
  15. package/runtime/prompts/lead/okstra-lead-contract.md +10 -8
  16. package/runtime/prompts/lead/report-writer.md +22 -28
  17. package/runtime/prompts/lead/team-contract.md +28 -11
  18. package/runtime/prompts/profiles/_common-contract.md +1 -1
  19. package/runtime/prompts/profiles/error-analysis.md +1 -1
  20. package/runtime/prompts/profiles/implementation-planning.md +5 -0
  21. package/runtime/prompts/profiles/improvement-discovery.md +11 -1
  22. package/runtime/prompts/profiles/requirements-discovery.md +8 -1
  23. package/runtime/python/okstra_ctl/analysis_packet.py +7 -13
  24. package/runtime/python/okstra_ctl/codex_dispatch.py +70 -319
  25. package/runtime/python/okstra_ctl/context_cost.py +82 -7
  26. package/runtime/python/okstra_ctl/convergence.py +223 -0
  27. package/runtime/python/okstra_ctl/convergence_engine.py +1152 -0
  28. package/runtime/python/okstra_ctl/convergence_migration.py +184 -0
  29. package/runtime/python/okstra_ctl/convergence_store.py +85 -0
  30. package/runtime/python/okstra_ctl/dispatch_core.py +53 -14
  31. package/runtime/python/okstra_ctl/improvement_assignment.py +61 -0
  32. package/runtime/python/okstra_ctl/path_hints.py +23 -1
  33. package/runtime/python/okstra_ctl/paths.py +10 -1
  34. package/runtime/python/okstra_ctl/render.py +56 -11
  35. package/runtime/python/okstra_ctl/report_finalize.py +394 -0
  36. package/runtime/python/okstra_ctl/worker_liveness.py +6 -2
  37. package/runtime/python/okstra_ctl/worker_prompt_body.py +107 -0
  38. package/runtime/python/okstra_ctl/worker_prompt_contract.py +102 -9
  39. package/runtime/python/okstra_ctl/worker_prompt_headers.py +79 -9
  40. package/runtime/python/okstra_ctl/worker_prompt_policy.py +158 -0
  41. package/runtime/templates/implementation-worker-preamble.md +51 -0
  42. package/runtime/templates/operating-standard.md +4 -4
  43. package/runtime/templates/report-writer-prompt-preamble.md +37 -0
  44. package/runtime/templates/worker-error-contract.md +46 -0
  45. package/runtime/templates/worker-prompt-preamble.md +28 -234
  46. package/runtime/validators/lib/fixtures.sh +27 -12
  47. package/runtime/validators/validate-run.py +201 -72
  48. package/src/cli-registry.mjs +18 -0
  49. package/src/commands/execute/convergence.mjs +34 -0
  50. package/src/commands/report/finalize.mjs +65 -0
@@ -46,6 +46,9 @@ from okstra_ctl.md_table import ( # noqa: E402
46
46
  split_pipe_row as _split_pipe_row,
47
47
  )
48
48
  from okstra_ctl.final_report_paths import final_report_data_path as _data_path_for # noqa: E402
49
+ from okstra_ctl.improvement_assignment import ( # noqa: E402
50
+ validate_primary_lens_assignments,
51
+ )
49
52
  from okstra_ctl.design_prep import ( # noqa: E402
50
53
  DesignPrepError,
51
54
  _planning_seq as _design_prep_planning_seq,
@@ -59,8 +62,10 @@ from okstra_ctl.design_surfaces import ( # noqa: E402
59
62
  expected_prep_plan_item_id,
60
63
  )
61
64
  from okstra_ctl.worker_prompt_contract import ( # noqa: E402
62
- validate_final_verification_prompt_paths,
65
+ PromptRecord,
66
+ validate_initial_prompt_records,
63
67
  )
68
+ from okstra_ctl.convergence_engine import validate_final_state # noqa: E402
64
69
 
65
70
  TERMINAL_STATUSES = {"completed", "timeout", "error", "not-run"}
66
71
  ATTEMPTED_STATUSES = {"completed", "timeout", "error"}
@@ -370,45 +375,198 @@ def effective_run_task_type(run_manifest: dict, task_manifest: dict) -> str:
370
375
  ).strip()
371
376
 
372
377
 
373
- def _validate_final_verification_initial_prompts(
378
+ def _resolve_prompt_record_path(project_root: Path, prompt_value: str) -> Path:
379
+ prompt_path = Path(prompt_value)
380
+ return prompt_path if prompt_path.is_absolute() else project_root / prompt_path
381
+
382
+
383
+ def _fallback_dispatch_kind(prompt_value: str) -> str:
384
+ match = re.search(r"-reverify-(r\d+)", Path(prompt_value).name)
385
+ return f"reverify-{match.group(1)}" if match else "initial"
386
+
387
+
388
+ def _persisted_prompt_records(
389
+ *,
390
+ project_root: Path,
391
+ selected_ids: set[str],
392
+ team_state: dict,
393
+ ) -> list[PromptRecord]:
394
+ dispatches = team_state.get("workerDispatches")
395
+ source = dispatches if isinstance(dispatches, list) and dispatches else (
396
+ team_state.get("workers") or []
397
+ )
398
+ records: list[PromptRecord] = []
399
+ seen: set[tuple[str, str, str]] = set()
400
+ for worker in source:
401
+ if not isinstance(worker, dict):
402
+ continue
403
+ worker_id = str(worker.get("workerId") or "").strip()
404
+ prompt_value = str(worker.get("promptPath") or "").strip()
405
+ if worker_id not in selected_ids or not prompt_value:
406
+ continue
407
+ dispatch_kind = str(worker.get("kind") or "").strip()
408
+ if not dispatch_kind:
409
+ dispatch_kind = _fallback_dispatch_kind(prompt_value)
410
+ identity = (worker_id, dispatch_kind, prompt_value)
411
+ if identity in seen:
412
+ continue
413
+ seen.add(identity)
414
+ records.append(
415
+ PromptRecord(
416
+ worker_id=worker_id,
417
+ dispatch_kind=dispatch_kind,
418
+ path=_resolve_prompt_record_path(project_root, prompt_value),
419
+ )
420
+ )
421
+ return records
422
+
423
+
424
+ def _markdown_section(text: str, heading: str) -> str:
425
+ pattern = rf"(?ms)^##\s+{re.escape(heading)}\s*$\n(.*?)(?=^##\s|\Z)"
426
+ match = re.search(pattern, text)
427
+ return match.group(1) if match else ""
428
+
429
+
430
+ def _parse_resolved_lenses(grilling_text: str) -> list[str]:
431
+ section = _markdown_section(grilling_text, "Resolved lenses")
432
+ return [
433
+ match.group(1).strip().strip("`")
434
+ for line in section.splitlines()
435
+ if (match := re.match(r"^\s*-\s+(.+?)\s*$", line))
436
+ ]
437
+
438
+
439
+ def _parse_primary_assignments(
440
+ grilling_text: str,
441
+ ) -> tuple[dict[str, str], list[str]]:
442
+ section = _markdown_section(grilling_text, "Primary Pass Assignments")
443
+ rows = [
444
+ (line.strip(), _split_pipe_row(line.strip()))
445
+ for line in section.splitlines()
446
+ if line.strip().startswith("|") and line.strip().endswith("|")
447
+ ]
448
+ errors: list[str] = []
449
+ if not rows or rows[0][1] != ["Worker ID", "Primary lens"]:
450
+ return {}, [
451
+ "Primary Pass Assignments table must start with "
452
+ "`| Worker ID | Primary lens |`"
453
+ ]
454
+ assignments: dict[str, str] = {}
455
+ for raw_line, row in rows[1:]:
456
+ if _is_markdown_separator(raw_line):
457
+ continue
458
+ if len(row) != 2 or not all(cell.strip() for cell in row):
459
+ errors.append("Primary Pass Assignments rows must contain two values")
460
+ continue
461
+ worker_id, lens = (cell.strip().strip("`") for cell in row)
462
+ if worker_id in assignments:
463
+ errors.append(f"duplicate worker assignment row: {worker_id}")
464
+ continue
465
+ assignments[worker_id] = lens
466
+ return assignments, errors
467
+
468
+
469
+ def _grilling_log_path_from_prompts(
470
+ *,
471
+ project_root: Path,
472
+ worker_ids: list[str],
473
+ records: list[PromptRecord],
474
+ ) -> tuple[Path | None, list[str]]:
475
+ header = "**Phase 1.5 Grilling Log:**"
476
+ values: set[str] = set()
477
+ errors: list[str] = []
478
+ for record in records:
479
+ if record.worker_id not in worker_ids or record.dispatch_kind != "initial":
480
+ continue
481
+ try:
482
+ text = record.path.read_text(encoding="utf-8")
483
+ except OSError:
484
+ continue
485
+ matches = [
486
+ line.strip()[len(header):].strip().strip("`")
487
+ for line in text.splitlines()
488
+ if line.strip().startswith(header)
489
+ ]
490
+ if len(matches) == 1 and matches[0]:
491
+ values.add(matches[0])
492
+ if not values:
493
+ return None, ["initial analyser prompts have no grilling-log path"]
494
+ if len(values) != 1:
495
+ return None, ["initial analyser prompts reference different grilling logs"]
496
+ return _resolve_prompt_record_path(project_root, values.pop()), errors
497
+
498
+
499
+ def _validate_improvement_primary_assignments(
500
+ *,
501
+ project_root: Path,
502
+ worker_ids: list[str],
503
+ records: list[PromptRecord],
504
+ ) -> list[str]:
505
+ grilling_path, errors = _grilling_log_path_from_prompts(
506
+ project_root=project_root,
507
+ worker_ids=worker_ids,
508
+ records=records,
509
+ )
510
+ if grilling_path is None:
511
+ return errors
512
+ try:
513
+ grilling_text = grilling_path.read_text(encoding="utf-8")
514
+ except OSError as exc:
515
+ return [*errors, f"cannot read grilling log {grilling_path}: {exc}"]
516
+ lenses = _parse_resolved_lenses(grilling_text)
517
+ assignments, parse_errors = _parse_primary_assignments(grilling_text)
518
+ return [
519
+ *errors,
520
+ *parse_errors,
521
+ *validate_primary_lens_assignments(assignments, worker_ids, lenses),
522
+ ]
523
+
524
+
525
+ def _validate_initial_analysis_prompts(
374
526
  data: dict,
375
527
  failures: list[str],
376
528
  ) -> None:
377
- """Validate the selected initial analysis prompts persisted for the run."""
378
- if data.get("taskType") != "final-verification":
529
+ """Validate persisted prompts using their functional audience contract."""
530
+ task_type = str(data.get("taskType") or "").strip()
531
+ if not task_type:
379
532
  return
380
-
381
533
  run_manifest = data.get("runManifest") or {}
382
534
  team_contract = run_manifest.get("teamContract") or {}
383
535
  selected_workers = team_contract.get("requiredWorkerRoles") or []
384
- selected_ids = {
536
+ selected_worker_ids = [
385
537
  str(worker.get("workerId") or "").strip()
386
538
  for worker in selected_workers
387
539
  if isinstance(worker, dict)
388
- }
540
+ and str(worker.get("workerId") or "").strip()
541
+ ]
542
+ selected_ids = set(selected_worker_ids)
389
543
  project_root = Path(data["projectRoot"])
390
544
  team_state = data.get("teamState") or {}
391
- prompt_paths: dict[str, Path] = {}
392
- for worker in team_state.get("workers") or []:
393
- if not isinstance(worker, dict):
394
- continue
395
- worker_id = str(worker.get("workerId") or "").strip()
396
- prompt_value = str(worker.get("promptPath") or "").strip()
397
- if (
398
- worker_id not in selected_ids
399
- or worker_id == "report-writer"
400
- or not prompt_value
401
- or "-reverify-r" in Path(prompt_value).name
402
- ):
403
- continue
404
- prompt_path = Path(prompt_value)
405
- prompt_paths[worker_id] = (
406
- prompt_path if prompt_path.is_absolute() else project_root / prompt_path
545
+ manifest = dict(run_manifest)
546
+ manifest["taskType"] = task_type
547
+ records = _persisted_prompt_records(
548
+ project_root=project_root,
549
+ selected_ids=selected_ids,
550
+ team_state=team_state,
551
+ )
552
+ errors = validate_initial_prompt_records(
553
+ manifest=manifest,
554
+ records=records,
555
+ )
556
+ if task_type == "improvement-discovery":
557
+ errors.extend(
558
+ _validate_improvement_primary_assignments(
559
+ project_root=project_root,
560
+ worker_ids=[
561
+ worker_id
562
+ for worker_id in selected_worker_ids
563
+ if worker_id != "report-writer"
564
+ ],
565
+ records=records,
566
+ )
407
567
  )
408
-
409
- errors = validate_final_verification_prompt_paths(prompt_paths)
410
568
  failures.extend(
411
- f"final-verification prompt contract: {error}" for error in errors
569
+ f"{task_type} prompt contract: {error}" for error in errors
412
570
  )
413
571
 
414
572
 
@@ -3135,63 +3293,34 @@ def _validate_forbidden_actions(
3135
3293
  failures.extend(f"forbidden-action: {v}" for v in violations)
3136
3294
 
3137
3295
 
3138
- CLASSIFICATION_COUNT_KEYS = {
3139
- "full-consensus": "fullConsensus",
3140
- "partial-consensus": "partialConsensus",
3141
- "contested": "contested",
3142
- "worker-unique": "workerUnique",
3143
- }
3144
-
3145
-
3146
- def classification_counts_from_findings(findings) -> dict:
3147
- """Recompute `finalClassificationCounts` from `findings[].classification`.
3148
-
3149
- Shared with `tests/contract/test_convergence_state_contract.py` so the
3150
- invariant has one definition — it used to live only in that test, which
3151
- meant it was checked against four hand-written fixtures and never against
3152
- the state a lead actually writes.
3153
- """
3154
- counts = {value: 0 for value in CLASSIFICATION_COUNT_KEYS.values()}
3155
- for finding in findings or []:
3156
- if not isinstance(finding, dict):
3157
- continue
3158
- key = CLASSIFICATION_COUNT_KEYS.get(finding.get("classification"))
3159
- if key:
3160
- counts[key] += 1
3161
- return counts
3162
-
3296
+ _CONVERGENCE_INTERMEDIATE_PREFIXES = (
3297
+ "convergence-groups-",
3298
+ "convergence-work-",
3299
+ "convergence-round-",
3300
+ )
3163
3301
 
3164
- def _validate_convergence_state_counts(run_dir, failures) -> None:
3165
- """A run's convergence state must agree with its own findings list.
3166
3302
 
3167
- `finalClassificationCounts` is a lead-authored summary of
3168
- `findings[].classification`. Nothing recomputed it against the real
3169
- artifact, so a miscount survived into the report as two different truths
3170
- (observed on dev-6901 planning 009: declared partial 6 / worker-unique 4
3171
- against an actual 5 / 5). Downstream readers pick one arbitrarily.
3172
- """
3303
+ def _validate_convergence_states(run_dir, failures) -> None:
3304
+ """Replay shared engine invariants for every public final state artifact."""
3173
3305
  from pathlib import Path as _Path
3174
3306
 
3175
3307
  state_dir = _Path(run_dir) / "state"
3176
3308
  if not state_dir.is_dir():
3177
3309
  return
3178
3310
  for state_path in sorted(state_dir.glob("convergence-*.json")):
3311
+ if state_path.name.startswith(_CONVERGENCE_INTERMEDIATE_PREFIXES):
3312
+ continue
3179
3313
  try:
3180
3314
  state = json.loads(state_path.read_text(encoding="utf-8"))
3181
3315
  except (OSError, json.JSONDecodeError) as exc:
3182
- failures.append(f"convergence state {state_path.name} unreadable: {exc}")
3183
- continue
3184
- declared = state.get("finalClassificationCounts")
3185
- if not isinstance(declared, dict):
3186
- continue
3187
- actual = classification_counts_from_findings(state.get("findings"))
3188
- if declared != actual:
3189
3316
  failures.append(
3190
- f"convergence state {state_path.name}: finalClassificationCounts "
3191
- f"{declared} does not match findings[].classification {actual}. "
3192
- "The counts are a summary of the findings list — a mismatch means "
3193
- "the run reports two different classifications for the same work."
3317
+ f"convergence state {state_path.name}: unreadable JSON: {exc}"
3194
3318
  )
3319
+ continue
3320
+ failures.extend(
3321
+ f"convergence state {state_path.name}: {error}"
3322
+ for error in validate_final_state(state)
3323
+ )
3195
3324
 
3196
3325
 
3197
3326
  def _validate_requirements_discovery_fanout(run_dir, failures) -> None:
@@ -3569,7 +3698,7 @@ def main() -> int:
3569
3698
  # contain every required section. Substring checks below are a
3570
3699
  # safety net for hand-edited or pre-v1.0 reports.
3571
3700
  task_type = effective_run_task_type(run_manifest, task_manifest)
3572
- _validate_final_verification_initial_prompts(
3701
+ _validate_initial_analysis_prompts(
3573
3702
  {
3574
3703
  "taskType": task_type,
3575
3704
  "projectRoot": project_root,
@@ -3641,7 +3770,7 @@ def main() -> int:
3641
3770
  run_dir = report_path.parent.parent
3642
3771
  _validate_requirements_discovery_fanout(run_dir, failures)
3643
3772
  # Phase-agnostic: convergence runs in every finding-producing phase.
3644
- _validate_convergence_state_counts(report_path.parent.parent, failures)
3773
+ _validate_convergence_states(report_path.parent.parent, failures)
3645
3774
  validate_report_views(report_path, failures)
3646
3775
 
3647
3776
  validation_status = "passed" if not failures else "failed"
@@ -66,6 +66,13 @@ export const COMMAND_REGISTRY = [
66
66
  category: "admin",
67
67
  summary: ["Move legacy .project-docs/okstra/ to .okstra/ (one-shot)"],
68
68
  },
69
+ {
70
+ name: "convergence",
71
+ module: "./commands/execute/convergence.mjs",
72
+ export: "run",
73
+ category: "admin",
74
+ summary: ["Advance and validate deterministic re-verification state"],
75
+ },
69
76
  {
70
77
  name: "git-reconcile",
71
78
  module: "./commands/execute/git-reconcile.mjs",
@@ -312,6 +319,17 @@ export const COMMAND_REGISTRY = [
312
319
  category: "introspection",
313
320
  summary: ["Render slim AI + self-contained HTML views of a final report"],
314
321
  },
322
+ {
323
+ name: "report-finalize",
324
+ module: "./commands/report/finalize.mjs",
325
+ export: "run",
326
+ category: "introspection",
327
+ summary: [
328
+ "Run the Phase 7 post-report sequence (token-usage,",
329
+ "render-views, spawn-followups, validate-run) in its",
330
+ "contractual order against one final report.",
331
+ ],
332
+ },
315
333
  {
316
334
  name: "render-final-report",
317
335
  module: "./commands/report/render-final-report.mjs",
@@ -0,0 +1,34 @@
1
+ import { runPythonModule } from "../../lib/python-helper.mjs";
2
+
3
+ const USAGE = `okstra convergence — advance deterministic re-verification state
4
+
5
+ Usage:
6
+ okstra convergence seed --groups <path> --work-state <path> \\
7
+ --final-state <path> --migration-dir <path> [--restart-from-round0]
8
+ okstra convergence plan-round --work-state <path> --plan <path>
9
+ okstra convergence apply-round --work-state <path> --plan <path> \\
10
+ --results <path>
11
+ okstra convergence finalize --work-state <path> --output <path>
12
+ okstra convergence validate --state <path> --kind <working|final>
13
+
14
+ This internal admin command is consumed by the Phase 5.5 lead contract. It
15
+ does not install or revive the obsolete okstra-convergence skill.
16
+ `;
17
+
18
+ export async function run(args) {
19
+ if (args.includes("--help") || args.includes("-h")) {
20
+ process.stdout.write(USAGE);
21
+ return 0;
22
+ }
23
+ if (args.length === 0) {
24
+ process.stdout.write(USAGE);
25
+ return 2;
26
+ }
27
+ const result = await runPythonModule({
28
+ module: "okstra_ctl.convergence",
29
+ args,
30
+ stdio: "inherit-stdout",
31
+ });
32
+ return result.code;
33
+ }
34
+
@@ -0,0 +1,65 @@
1
+ import { runPythonModule } from "../../lib/python-helper.mjs";
2
+ import { resolvePaths } from "../../lib/paths.mjs";
3
+
4
+ const USAGE = `okstra report-finalize — run the Phase 7 post-report sequence
5
+
6
+ Usage:
7
+ okstra report-finalize --project-root <dir> --run-manifest <path> \\
8
+ --report <final-report-<task-type>-<seq>.md> [--team-state <path>]
9
+
10
+ Runs the four Phase 7 steps in their contractual order against one final-report:
11
+
12
+ 1. token-usage substitute real token/cost numbers into the data.json
13
+ 2. render-views write the self-contained *.html sibling (skipped when the
14
+ report has no C-* clarification rows and is not a plan
15
+ approval target)
16
+ 3. spawn-followups turn section 4 rows into task stubs
17
+ 4. validate-run validate the finished run artifacts
18
+
19
+ Every step is idempotent, so re-running after a fixed failure is safe. The
20
+ sequence stops at the first non-zero exit and reports which step failed.
21
+
22
+ This is the same code path the Codex lead adapter runs automatically after its
23
+ report-writer completes, so a Claude-led and a Codex-led run finalize
24
+ identically.
25
+
26
+ --workspace-root is owned by this command.
27
+ `;
28
+
29
+ const OWNED_FLAGS = new Set(["--workspace-root"]);
30
+
31
+ function isOwnedFlag(arg) {
32
+ if (OWNED_FLAGS.has(arg)) return true;
33
+ return [...OWNED_FLAGS].some((flag) => arg.startsWith(`${flag}=`));
34
+ }
35
+
36
+ export function buildReportFinalizeArgs(args, paths) {
37
+ return ["--workspace-root", paths.workspace, ...args];
38
+ }
39
+
40
+ export async function run(args) {
41
+ if (args.includes("--help") || args.includes("-h")) {
42
+ process.stdout.write(USAGE);
43
+ return 0;
44
+ }
45
+ if (args.length === 0) {
46
+ process.stdout.write(USAGE);
47
+ return 2;
48
+ }
49
+
50
+ const forbidden = args.find(isOwnedFlag);
51
+ if (forbidden) {
52
+ process.stderr.write(
53
+ `error: ${forbidden} is set by 'okstra report-finalize' itself — remove it from your args\n`,
54
+ );
55
+ return 2;
56
+ }
57
+
58
+ const paths = await resolvePaths();
59
+ const result = await runPythonModule({
60
+ module: "okstra_ctl.report_finalize",
61
+ args: buildReportFinalizeArgs(args, paths),
62
+ stdio: "inherit-stdout",
63
+ });
64
+ return result.code;
65
+ }