okstra 0.178.0 → 0.179.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 (110) hide show
  1. package/README.md +2 -2
  2. package/dist/commands/execute/plan-verify.mjs +1 -1
  3. package/dist/commands/execute/worktree-status.mjs +8 -2
  4. package/dist/commands/execute/worktree-status.mjs.map +1 -1
  5. package/dist/commands/lifecycle/install.mjs +1 -1
  6. package/dist/commands/lifecycle/install.mjs.map +1 -1
  7. package/dist/commands/report/render-final-report.mjs +3 -3
  8. package/docs/architecture/storage-model.md +3 -3
  9. package/docs/architecture.md +10 -9
  10. package/docs/cli.md +11 -13
  11. package/docs/for-ai/skills/okstra-inspect.md +3 -3
  12. package/docs/for-ai/skills/okstra-schedule-gen.md +2 -2
  13. package/docs/for-ai/skills/okstra-user-response.md +2 -2
  14. package/docs/project-structure-overview.md +10 -11
  15. package/docs/task-process/implementation-planning.md +1 -1
  16. package/docs/task-process/implementation.md +1 -1
  17. package/package.json +1 -1
  18. package/runtime/BUILD.json +2 -2
  19. package/runtime/agents/workers/report-writer-worker.md +11 -12
  20. package/runtime/bin/lib/okstra/globals.sh +2 -2
  21. package/runtime/bin/lib/okstra/interactive.sh +1 -1
  22. package/runtime/bin/lib/okstra/usage.sh +11 -9
  23. package/runtime/bin/lib/okstra-ctl/cmd-rerun.sh +1 -1
  24. package/runtime/bin/okstra-central.sh +2 -2
  25. package/runtime/bin/okstra-render-final-report.py +1 -1
  26. package/runtime/bin/okstra-token-usage.py +1 -1
  27. package/runtime/prompts/launch.template.md +1 -1
  28. package/runtime/prompts/lead/adapters/cmux.md +6 -1
  29. package/runtime/prompts/lead/context-loader.md +3 -2
  30. package/runtime/prompts/lead/convergence.md +1 -1
  31. package/runtime/prompts/lead/okstra-lead-contract.md +4 -4
  32. package/runtime/prompts/lead/plan-body-verification.md +3 -3
  33. package/runtime/prompts/lead/report-writer.md +21 -20
  34. package/runtime/prompts/lead/team-contract.md +1 -1
  35. package/runtime/prompts/profiles/_common-contract.md +5 -4
  36. package/runtime/prompts/profiles/_implementation-deliverable.md +1 -0
  37. package/runtime/prompts/profiles/_implementation-executor.md +2 -1
  38. package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
  39. package/runtime/prompts/profiles/implementation-planning.md +9 -5
  40. package/runtime/prompts/profiles/implementation.md +4 -4
  41. package/runtime/prompts/profiles/improvement-discovery.md +2 -2
  42. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +5 -4
  43. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +5 -4
  44. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +2 -2
  45. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +71 -9
  46. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +5 -4
  47. package/runtime/python/okstra_ctl/agent_prompt_cli.py +55 -3
  48. package/runtime/python/okstra_ctl/analysis_inputs.py +5 -3
  49. package/runtime/python/okstra_ctl/analysis_packet.py +21 -0
  50. package/runtime/python/okstra_ctl/backfill.py +12 -5
  51. package/runtime/python/okstra_ctl/consumers.py +70 -3
  52. package/runtime/python/okstra_ctl/convergence_engine.py +43 -17
  53. package/runtime/python/okstra_ctl/dispatch_core.py +47 -11
  54. package/runtime/python/okstra_ctl/dispatch_state.py +20 -19
  55. package/runtime/python/okstra_ctl/domain/worker_exec.py +13 -34
  56. package/runtime/python/okstra_ctl/domain/worker_presentation.py +128 -0
  57. package/runtime/python/okstra_ctl/execution_mutation_audit.py +5 -0
  58. package/runtime/python/okstra_ctl/final_report_paths.py +77 -1
  59. package/runtime/python/okstra_ctl/handoff.py +1 -2
  60. package/runtime/python/okstra_ctl/implementation_outcome.py +1 -1
  61. package/runtime/python/okstra_ctl/index.py +4 -4
  62. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +26 -12
  63. package/runtime/python/okstra_ctl/listing.py +4 -2
  64. package/runtime/python/okstra_ctl/manager_launch.py +1 -1
  65. package/runtime/python/okstra_ctl/manager_sync.py +1 -1
  66. package/runtime/python/okstra_ctl/path_hints.py +2 -2
  67. package/runtime/python/okstra_ctl/paths.py +13 -10
  68. package/runtime/python/okstra_ctl/plan_run_root.py +9 -5
  69. package/runtime/python/okstra_ctl/recap.py +3 -2
  70. package/runtime/python/okstra_ctl/reconcile.py +3 -1
  71. package/runtime/python/okstra_ctl/render.py +22 -22
  72. package/runtime/python/okstra_ctl/report_finalize.py +4 -4
  73. package/runtime/python/okstra_ctl/rollup.py +1 -1
  74. package/runtime/python/okstra_ctl/run.py +139 -284
  75. package/runtime/python/okstra_ctl/run_audit.py +5 -5
  76. package/runtime/python/okstra_ctl/run_index_row.py +2 -2
  77. package/runtime/python/okstra_ctl/session_transcript.py +89 -0
  78. package/runtime/python/okstra_ctl/stage_ledger.py +72 -0
  79. package/runtime/python/okstra_ctl/stage_map.py +28 -29
  80. package/runtime/python/okstra_ctl/stage_targets.py +61 -0
  81. package/runtime/python/okstra_ctl/user_response.py +97 -12
  82. package/runtime/python/okstra_ctl/wizard.py +43 -65
  83. package/runtime/python/okstra_ctl/worker_prompt_body.py +6 -7
  84. package/runtime/python/okstra_ctl/worker_runner.py +76 -213
  85. package/runtime/python/okstra_ctl/workflow.py +1 -1
  86. package/runtime/python/okstra_ctl/wrapper_status.py +23 -0
  87. package/runtime/python/okstra_ctl/write_policy.py +45 -6
  88. package/runtime/python/okstra_project/state.py +2 -2
  89. package/runtime/python/okstra_token_usage/__init__.py +1 -1
  90. package/runtime/python/okstra_token_usage/cli.py +3 -3
  91. package/runtime/python/okstra_token_usage/report.py +7 -24
  92. package/runtime/schemas/convergence-groups-v1.0.schema.json +1 -1
  93. package/runtime/schemas/convergence-groups-v2.0.schema.json +1 -1
  94. package/runtime/schemas/final-report-v2.0.schema.json +11 -1
  95. package/runtime/skills/okstra-inspect/facets/history.md +3 -3
  96. package/runtime/skills/okstra-inspect/facets/recap.md +1 -1
  97. package/runtime/skills/okstra-inspect/facets/report.md +5 -5
  98. package/runtime/skills/okstra-inspect/facets/status.md +2 -2
  99. package/runtime/skills/okstra-pr-gen/SKILL.md +1 -1
  100. package/runtime/skills/okstra-run/SKILL.md +1 -1
  101. package/runtime/skills/okstra-schedule-gen/SKILL.md +1 -1
  102. package/runtime/skills/okstra-user-response/SKILL.md +3 -3
  103. package/runtime/templates/project-docs/task-index.template.md +1 -1
  104. package/runtime/templates/report-writer-prompt-preamble.md +1 -1
  105. package/runtime/validators/forbidden_actions.py +76 -5
  106. package/runtime/validators/lib/fixtures.sh +14 -10
  107. package/runtime/validators/lib/runners.sh +1 -1
  108. package/runtime/validators/validate-implementation-plan-stages.py +3 -0
  109. package/runtime/validators/validate-report-views.py +1 -1
  110. package/runtime/validators/validate-run.py +95 -37
@@ -93,6 +93,7 @@ from okstra_ctl.md_table import ( # noqa: E402
93
93
  split_pipe_row as _split_pipe_row,
94
94
  )
95
95
  from okstra_ctl.final_report_paths import final_report_data_path as _data_path_for # noqa: E402
96
+ from okstra_token_usage.report import _match_worker_index # noqa: E402
96
97
  from okstra_ctl.improvement_assignment import ( # noqa: E402
97
98
  validate_primary_lens_assignments,
98
99
  )
@@ -743,8 +744,8 @@ def update_workflow_metadata(
743
744
  "latestTeamStatePath": run_manifest.get(
744
745
  "teamStatePath", ""
745
746
  ),
746
- "latestReportPath": task_manifest.get(
747
- "latestReportPath", ""
747
+ "latestReportRecordPath": task_manifest.get(
748
+ "latestReportRecordPath", ""
748
749
  ),
749
750
  "latestResumeCommandPath": task_manifest.get(
750
751
  "latestResumeCommandPath", ""
@@ -1660,39 +1661,51 @@ def _scan_token_usage_summary(
1660
1661
  # a section heading line (not as inline text inside a paragraph or table).
1661
1662
  _VERDICT_CARD_HEADING_RE = re.compile(r"^##[ \t]+Verdict Card\b", re.MULTILINE)
1662
1663
 
1664
+ def _team_state_worker_label(worker: Mapping[str, Any]) -> str:
1665
+ role = str(worker.get("role") or "").strip()
1666
+ worker_id = str(worker.get("workerId") or "").strip()
1667
+ if role and worker_id:
1668
+ return f"{role} (`{worker_id}`)"
1669
+ return role or worker_id or "<unnamed>"
1670
+
1671
+
1672
+ def _unmatched_team_state_workers(
1673
+ report_data: Mapping[str, Any],
1674
+ team_state: Mapping[str, Any],
1675
+ ) -> list[dict]:
1676
+ raw = team_state.get("workers")
1677
+ workers = [row for row in raw if isinstance(row, dict)] if isinstance(raw, list) else []
1678
+ used: set[int] = set()
1679
+ for row in report_data.get("executionStatus") or []:
1680
+ if not isinstance(row, dict):
1681
+ continue
1682
+ if "lead" in str(row.get("role") or "").lower():
1683
+ continue
1684
+ index = _match_worker_index(row, workers, used)
1685
+ if index is not None:
1686
+ used.add(index)
1687
+ return [worker for index, worker in enumerate(workers) if index not in used]
1688
+
1689
+
1663
1690
  def _validate_v2_report(
1664
1691
  report_data: Mapping[str, Any],
1665
- required_agent_status_entries: list[str],
1666
1692
  failures: list[str],
1693
+ *,
1694
+ team_state: Mapping[str, Any] | None = None,
1667
1695
  ) -> None:
1668
1696
  """Contract checks for a schema-v2 report, read from its data.json.
1669
1697
 
1670
- The AI-handoff markdown is a deterministic rendering of this record, so the
1671
- checks that used to scan it — heading count and order, human-only fields
1672
- leaking into the AI artifact, a Reading Confirmation heading — were asking
1673
- whether the renderer had done its job, not whether the report was sound.
1674
- Schema enforcement in `render_final_report._enforce_schema` already answers
1675
- the second question, and the first is settled by the template. What is left
1676
- is the pair of facts the markdown could only ever carry second-hand: which
1677
- agents the run must account for, and whether the token cells were filled.
1678
-
1679
- Both are scanned over the serialized record, as text, because that is what
1680
- the markdown scan was. Requiring each label to equal an
1681
- `executionStatus[].role` would be a stricter rule than the contract states
1682
- anywhere: the schema types `role` as a free string, no prompt or worker spec
1683
- fixes its vocabulary, and `okstra_token_usage.report._match_worker_index`
1684
- treats function-role spellings ("Analysis verifier", "Acceptance critic") as
1685
- a shape reports do take — matching them by containment, never equality.
1686
- Tightening this belongs with an authoring rule that says what to write, not
1687
- on its own. Nothing is lost by reading the record instead of its rendering:
1688
- the labels never come from the template or the i18n dictionaries.
1698
+ Agent completeness is the team-state worker set versus `executionStatus`
1699
+ rows, matched by `_match_worker_index` (role containment, then a unique
1700
+ provider+model fallback). Token placeholders stay a serialized-record
1701
+ scan. The full reading copy is not read.
1689
1702
  """
1703
+ for worker in _unmatched_team_state_workers(report_data, team_state or {}):
1704
+ failures.append(
1705
+ "executionStatus is missing team-state worker "
1706
+ f"{_team_state_worker_label(worker)}"
1707
+ )
1690
1708
  serialized = json.dumps(report_data, ensure_ascii=False)
1691
- for label in required_agent_status_entries:
1692
- if label not in serialized:
1693
- failures.append(
1694
- f"final report does not include required agent status entry: {label}"
1695
- )
1696
1709
  for placeholder in TOKEN_PLACEHOLDERS:
1697
1710
  if placeholder in serialized:
1698
1711
  failures.append(
@@ -2875,13 +2888,14 @@ def validate_report(
2875
2888
  allow_unavailable_token_usage: bool = False,
2876
2889
  unavailable_ok_labels: frozenset[str] = frozenset(),
2877
2890
  report_data: Mapping[str, Any] | None = None,
2891
+ team_state: Mapping[str, Any] | None = None,
2878
2892
  ) -> None:
2879
- if not report_path.exists():
2880
- failures.append(f"final report is missing: {report_path}")
2893
+ if (report_data or {}).get("schemaVersion") == "2.0":
2894
+ _validate_v2_report(report_data or {}, failures, team_state=team_state)
2881
2895
  return
2882
2896
 
2883
- if (report_data or {}).get("schemaVersion") == "2.0":
2884
- _validate_v2_report(report_data or {}, required_agent_status_entries, failures)
2897
+ if not report_path.exists():
2898
+ failures.append(f"final report is missing: {report_path}")
2885
2899
  return
2886
2900
 
2887
2901
  content = report_path.read_text()
@@ -3475,6 +3489,7 @@ def validate_final_report_data(
3475
3489
  # Called here rather than from a task-type branch: four profiles raise
3476
3490
  # clarification rows, and the gate scopes itself by task type internally.
3477
3491
  _validate_clarification_options(data, failures)
3492
+ _validate_clarification_record_coordinates(data, failures)
3478
3493
  _validate_open_approval_blocker_provenance(data, failures)
3479
3494
 
3480
3495
  task_type = (data.get("header") or {}).get("taskType")
@@ -5413,6 +5428,48 @@ _REACH_TOKENS = frozenset({"in-repo", "cross-repo"})
5413
5428
  _LEGACY_EXPECTED_FORM_RE = re.compile(r"\b(?:Recommended|Alternatives):")
5414
5429
 
5415
5430
 
5431
+ _CLARIFICATION_SECTION_CITE_RE = re.compile(r"§\d")
5432
+
5433
+
5434
+ def _validate_clarification_record_coordinates(
5435
+ data: dict, failures: list[str]
5436
+ ) -> None:
5437
+ """A clarification row may cite a record coordinate, not a section number.
5438
+
5439
+ `§4.7` exists only on one full reading copy. The report record identifies
5440
+ a row by `id` (`RB-002`). schema-v1 has no record and is exempt.
5441
+ """
5442
+ if data.get("schemaVersion") != "2.0":
5443
+ return
5444
+ texts: list[tuple[str, str]] = []
5445
+ for row in data.get("clarificationItems") or []:
5446
+ if not isinstance(row, dict):
5447
+ continue
5448
+ row_id = str(row.get("id") or "<unknown>")
5449
+ for field in ("statement", "expectedForm"):
5450
+ texts.append((f"`{row_id}` {field}", str(row.get(field) or "")))
5451
+ options = row.get("options") or []
5452
+ if isinstance(options, list):
5453
+ for index, option in enumerate(options):
5454
+ if not isinstance(option, dict):
5455
+ continue
5456
+ for field in ("answer", "rationale"):
5457
+ texts.append(
5458
+ (
5459
+ f"`{row_id}` options[{index}].{field}",
5460
+ str(option.get(field) or ""),
5461
+ )
5462
+ )
5463
+ for label, text in texts:
5464
+ if _CLARIFICATION_SECTION_CITE_RE.search(text):
5465
+ failures.append(
5466
+ f"final-report data.json: clarification {label} cites a "
5467
+ "section number (`§…`). Cite a record coordinate (`RB-002`) "
5468
+ "instead — a section number exists only on one full reading "
5469
+ "copy."
5470
+ )
5471
+
5472
+
5416
5473
  def _validate_clarification_options(data: dict, failures: list[str]) -> None:
5417
5474
  """A `decision` row must carry its choices as data, not as prose.
5418
5475
 
@@ -8922,8 +8979,8 @@ def _import_token_usage():
8922
8979
  sys.path.insert(0, str(candidate))
8923
8980
  break
8924
8981
  from okstra_token_usage.collect import collect # noqa: E402
8925
- from okstra_token_usage.report import populate_and_render # noqa: E402
8926
- return collect, populate_and_render
8982
+ from okstra_token_usage.report import populate_token_cells # noqa: E402
8983
+ return collect, populate_token_cells
8927
8984
 
8928
8985
 
8929
8986
  def _needs_token_autofix(team_state: dict, report_path: Path) -> bool:
@@ -9018,7 +9075,7 @@ def attempt_token_usage_autofix(
9018
9075
  if not _needs_token_autofix(team_state, report_path):
9019
9076
  return "skipped", []
9020
9077
  try:
9021
- collect, populate_and_render = _import_token_usage()
9078
+ collect, populate_token_cells = _import_token_usage()
9022
9079
  except Exception as exc: # noqa: BLE001
9023
9080
  return "import-failed", [f"okstra_token_usage import failed: {exc}"]
9024
9081
  try:
@@ -9041,7 +9098,7 @@ def attempt_token_usage_autofix(
9041
9098
  )
9042
9099
  data_path = _data_path_for(report_path)
9043
9100
  try:
9044
- replaced, _bytes = populate_and_render(data_path, updated)
9101
+ replaced = populate_token_cells(data_path, updated)
9045
9102
  except Exception as exc: # noqa: BLE001
9046
9103
  # `SubstituteRefusedError` (or any unexpected substitution
9047
9104
  # failure) — report it as an accuracy failure so the validator
@@ -9052,7 +9109,7 @@ def attempt_token_usage_autofix(
9052
9109
  ]
9053
9110
 
9054
9111
  # Phase 7 step 1.5 is BLOCKING and the autofix just mutated the
9055
- # source MD — any pre-existing html sibling is now stale by
9112
+ # report record — any pre-existing html sibling is now stale by
9056
9113
  # construction. Re-render the html view in lock-step so the
9057
9114
  # downstream report-views validator does not trip over the
9058
9115
  # autofix's own side effect.
@@ -9203,7 +9260,7 @@ def main() -> int:
9203
9260
  parser.add_argument(
9204
9261
  "--report",
9205
9262
  required=True,
9206
- help="Project-relative or absolute path to the final report Markdown file.",
9263
+ help="Project-relative or absolute path to the report record (.data.json). Schema-v1 reports still use the Markdown file.",
9207
9264
  )
9208
9265
  parser.add_argument(
9209
9266
  "--run-manifest",
@@ -9344,6 +9401,7 @@ def main() -> int:
9344
9401
  allow_unavailable_token_usage=_session_accounting(team_state) == "artifact-only",
9345
9402
  unavailable_ok_labels=_unavailable_usage_worker_labels(team_state),
9346
9403
  report_data=validation_data,
9404
+ team_state=team_state,
9347
9405
  )
9348
9406
  validate_team_state_usage(team_state, failures)
9349
9407