okstra 0.179.2 → 0.183.2

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 (258) hide show
  1. package/README.md +1 -1
  2. package/dist/cli-registry.mjs +30 -2
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/dist/commands/execute/incremental-carry.mjs +9 -8
  5. package/dist/commands/execute/incremental-carry.mjs.map +1 -1
  6. package/dist/commands/execute/plan-verify.mjs +3 -1
  7. package/dist/commands/execute/plan-verify.mjs.map +1 -1
  8. package/dist/commands/execute/render-bundle.d.mts +4 -2
  9. package/dist/commands/execute/render-bundle.mjs +46 -5
  10. package/dist/commands/execute/render-bundle.mjs.map +1 -1
  11. package/dist/commands/execute/run.mjs +11 -3
  12. package/dist/commands/execute/run.mjs.map +1 -1
  13. package/dist/commands/inspect/model-io.d.mts +1 -0
  14. package/dist/commands/inspect/model-io.mjs +25 -0
  15. package/dist/commands/inspect/model-io.mjs.map +1 -0
  16. package/dist/commands/inspect/stage-map.mjs +29 -8
  17. package/dist/commands/inspect/stage-map.mjs.map +1 -1
  18. package/dist/commands/inspect/task-list.mjs +52 -6
  19. package/dist/commands/inspect/task-list.mjs.map +1 -1
  20. package/dist/commands/inspect/user-response.mjs +14 -4
  21. package/dist/commands/inspect/user-response.mjs.map +1 -1
  22. package/dist/commands/lifecycle/check-project.d.mts +1 -0
  23. package/dist/commands/lifecycle/check-project.mjs +69 -50
  24. package/dist/commands/lifecycle/check-project.mjs.map +1 -1
  25. package/dist/commands/lifecycle/contract-check.d.mts +1 -0
  26. package/dist/commands/lifecycle/contract-check.mjs +18 -0
  27. package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
  28. package/dist/commands/lifecycle/preflight.mjs +154 -51
  29. package/dist/commands/lifecycle/preflight.mjs.map +1 -1
  30. package/dist/commands/pr/pr.d.mts +1 -0
  31. package/dist/commands/pr/pr.mjs +19 -1
  32. package/dist/commands/pr/pr.mjs.map +1 -1
  33. package/dist/commands/report/agent-activity.mjs +2 -2
  34. package/dist/commands/report/approval-decision.d.mts +1 -0
  35. package/dist/commands/report/approval-decision.mjs +21 -0
  36. package/dist/commands/report/approval-decision.mjs.map +1 -0
  37. package/dist/commands/report/design-snapshot.d.mts +1 -0
  38. package/dist/commands/report/design-snapshot.mjs +19 -0
  39. package/dist/commands/report/design-snapshot.mjs.map +1 -0
  40. package/dist/commands/report/translate.mjs +3 -0
  41. package/dist/commands/report/translate.mjs.map +1 -1
  42. package/dist/lib/host-registry-client.mjs +13 -9
  43. package/dist/lib/host-registry-client.mjs.map +1 -1
  44. package/docs/architecture/storage-model.md +1 -1
  45. package/docs/architecture.md +21 -10
  46. package/docs/cli.md +36 -20
  47. package/docs/container.md +6 -4
  48. package/docs/contributor-change-matrix.md +1 -1
  49. package/docs/for-ai/README.md +2 -2
  50. package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
  51. package/docs/for-ai/skills/okstra-code-review.md +4 -4
  52. package/docs/for-ai/skills/okstra-container-build.md +20 -17
  53. package/docs/for-ai/skills/okstra-inspect.md +20 -23
  54. package/docs/for-ai/skills/okstra-manager.md +19 -18
  55. package/docs/for-ai/skills/okstra-memory.md +2 -2
  56. package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
  57. package/docs/for-ai/skills/okstra-rollup.md +14 -13
  58. package/docs/for-ai/skills/okstra-run.md +7 -3
  59. package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
  60. package/docs/for-ai/skills/okstra-setup.md +7 -7
  61. package/docs/for-ai/skills/okstra-usage.md +5 -4
  62. package/docs/for-ai/skills/okstra-user-response.md +50 -32
  63. package/docs/project-structure-overview.md +43 -31
  64. package/docs/task-process/implementation-planning.md +2 -2
  65. package/docs/task-process/implementation.md +7 -4
  66. package/package.json +1 -1
  67. package/runtime/BUILD.json +2 -2
  68. package/runtime/agents/workers/claude-worker.md +4 -4
  69. package/runtime/agents/workers/report-writer-worker.md +15 -164
  70. package/runtime/agents/workers/translator-worker.md +5 -13
  71. package/runtime/bin/okstra-error-log.py +51 -11
  72. package/runtime/bin/okstra-report-translate.py +210 -23
  73. package/runtime/prompts/host-orchestration/implementation.md +1 -1
  74. package/runtime/prompts/launch.template.md +9 -12
  75. package/runtime/prompts/lead/adapters/cmux.md +1 -1
  76. package/runtime/prompts/lead/context-loader.md +41 -141
  77. package/runtime/prompts/lead/convergence.md +10 -8
  78. package/runtime/prompts/lead/okstra-lead-contract.md +40 -49
  79. package/runtime/prompts/lead/plan-body-verification.md +217 -23
  80. package/runtime/prompts/lead/report-writer.md +81 -421
  81. package/runtime/prompts/lead/team-contract.md +9 -54
  82. package/runtime/prompts/profiles/_clarification-recommendation.md +5 -4
  83. package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
  84. package/runtime/prompts/profiles/_common-contract.md +4 -4
  85. package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
  86. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  87. package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
  88. package/runtime/prompts/profiles/change-impact-analysis.md +1 -1
  89. package/runtime/prompts/profiles/error-analysis.md +1 -1
  90. package/runtime/prompts/profiles/feature-analysis.md +1 -1
  91. package/runtime/prompts/profiles/final-verification.md +1 -1
  92. package/runtime/prompts/profiles/implementation-planning.md +20 -12
  93. package/runtime/prompts/profiles/improvement-discovery.md +1 -1
  94. package/runtime/prompts/profiles/project-analysis.md +1 -1
  95. package/runtime/prompts/profiles/release-handoff.md +0 -1
  96. package/runtime/prompts/profiles/requirements-discovery.md +1 -1
  97. package/runtime/prompts/wizard/prompts.ko.json +0 -1
  98. package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
  99. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
  100. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +8 -2
  101. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
  102. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
  103. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
  104. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
  105. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
  106. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
  107. package/runtime/python/okstra_ctl/agent_activity.py +141 -38
  108. package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
  109. package/runtime/python/okstra_ctl/agent_prompt_cli.py +70 -23
  110. package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
  111. package/runtime/python/okstra_ctl/analysis_packet.py +124 -3
  112. package/runtime/python/okstra_ctl/approval_decisions.py +328 -0
  113. package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
  114. package/runtime/python/okstra_ctl/backfill.py +13 -10
  115. package/runtime/python/okstra_ctl/batch.py +2 -4
  116. package/runtime/python/okstra_ctl/build_tools.py +6 -3
  117. package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
  118. package/runtime/python/okstra_ctl/clarification_items.py +27 -13
  119. package/runtime/python/okstra_ctl/cmux.py +130 -52
  120. package/runtime/python/okstra_ctl/code_review_target.py +34 -8
  121. package/runtime/python/okstra_ctl/consumers.py +5 -4
  122. package/runtime/python/okstra_ctl/container.py +103 -8
  123. package/runtime/python/okstra_ctl/context_cost.py +2 -1
  124. package/runtime/python/okstra_ctl/contract_graph.py +497 -0
  125. package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
  126. package/runtime/python/okstra_ctl/convergence.py +338 -17
  127. package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
  128. package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
  129. package/runtime/python/okstra_ctl/convergence_store.py +55 -34
  130. package/runtime/python/okstra_ctl/design_prep.py +7 -4
  131. package/runtime/python/okstra_ctl/design_snapshot.py +134 -0
  132. package/runtime/python/okstra_ctl/dispatch_core.py +97 -69
  133. package/runtime/python/okstra_ctl/dispatch_state.py +163 -63
  134. package/runtime/python/okstra_ctl/doctor.py +6 -3
  135. package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
  136. package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
  137. package/runtime/python/okstra_ctl/error_log_write.py +35 -30
  138. package/runtime/python/okstra_ctl/error_report.py +26 -1
  139. package/runtime/python/okstra_ctl/error_zip.py +27 -5
  140. package/runtime/python/okstra_ctl/execution_identity.py +3 -2
  141. package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
  142. package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -2
  143. package/runtime/python/okstra_ctl/final_report_schema.py +26 -17
  144. package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
  145. package/runtime/python/okstra_ctl/fixed_text.py +39 -0
  146. package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
  147. package/runtime/python/okstra_ctl/handoff.py +5 -4
  148. package/runtime/python/okstra_ctl/i18n.py +4 -2
  149. package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
  150. package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
  151. package/runtime/python/okstra_ctl/incremental_carry.py +130 -17
  152. package/runtime/python/okstra_ctl/incremental_scope.py +12 -4
  153. package/runtime/python/okstra_ctl/index.py +8 -11
  154. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
  155. package/runtime/python/okstra_ctl/invocation.py +3 -6
  156. package/runtime/python/okstra_ctl/json_boundary.py +366 -0
  157. package/runtime/python/okstra_ctl/json_registry.py +10 -12
  158. package/runtime/python/okstra_ctl/jsonl.py +19 -2
  159. package/runtime/python/okstra_ctl/lead_events.py +33 -1
  160. package/runtime/python/okstra_ctl/listing.py +3 -3
  161. package/runtime/python/okstra_ctl/log_report.py +24 -2
  162. package/runtime/python/okstra_ctl/manager_cli.py +92 -7
  163. package/runtime/python/okstra_ctl/manager_store.py +12 -10
  164. package/runtime/python/okstra_ctl/material.py +5 -1
  165. package/runtime/python/okstra_ctl/migrate.py +29 -25
  166. package/runtime/python/okstra_ctl/model_cli.py +3 -15
  167. package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
  168. package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
  169. package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
  170. package/runtime/python/okstra_ctl/path_hints.py +12 -0
  171. package/runtime/python/okstra_ctl/paths.py +21 -0
  172. package/runtime/python/okstra_ctl/plan_items.py +115 -4
  173. package/runtime/python/okstra_ctl/plan_items_cli.py +626 -34
  174. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +2 -1
  175. package/runtime/python/okstra_ctl/pr_template.py +3 -2
  176. package/runtime/python/okstra_ctl/project_meta.py +5 -7
  177. package/runtime/python/okstra_ctl/recap.py +5 -4
  178. package/runtime/python/okstra_ctl/reconcile.py +21 -27
  179. package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
  180. package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
  181. package/runtime/python/okstra_ctl/render.py +74 -16
  182. package/runtime/python/okstra_ctl/render_final_report.py +10 -8
  183. package/runtime/python/okstra_ctl/report_assembly.py +509 -0
  184. package/runtime/python/okstra_ctl/report_contract.py +2 -1
  185. package/runtime/python/okstra_ctl/report_finalize.py +67 -32
  186. package/runtime/python/okstra_ctl/report_html/render.py +3 -2
  187. package/runtime/python/okstra_ctl/report_inputs.py +72 -0
  188. package/runtime/python/okstra_ctl/report_language.py +3 -2
  189. package/runtime/python/okstra_ctl/report_markdown.py +81 -8
  190. package/runtime/python/okstra_ctl/report_narrative.py +351 -0
  191. package/runtime/python/okstra_ctl/report_projections.py +265 -0
  192. package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
  193. package/runtime/python/okstra_ctl/report_views.py +3 -2
  194. package/runtime/python/okstra_ctl/rollup.py +65 -4
  195. package/runtime/python/okstra_ctl/run.py +184 -65
  196. package/runtime/python/okstra_ctl/run_audit.py +3 -2
  197. package/runtime/python/okstra_ctl/run_context.py +6 -9
  198. package/runtime/python/okstra_ctl/run_index_row.py +2 -8
  199. package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
  200. package/runtime/python/okstra_ctl/schema_excerpt.py +15 -8
  201. package/runtime/python/okstra_ctl/session_transcript.py +27 -1
  202. package/runtime/python/okstra_ctl/set_work_status.py +64 -38
  203. package/runtime/python/okstra_ctl/stage_fix_carry.py +8 -6
  204. package/runtime/python/okstra_ctl/stage_ledger.py +132 -18
  205. package/runtime/python/okstra_ctl/stage_map.py +96 -28
  206. package/runtime/python/okstra_ctl/stage_targets.py +3 -4
  207. package/runtime/python/okstra_ctl/team.py +3 -2
  208. package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
  209. package/runtime/python/okstra_ctl/time_report.py +51 -4
  210. package/runtime/python/okstra_ctl/usage_identity.py +2 -1
  211. package/runtime/python/okstra_ctl/usage_report.py +58 -4
  212. package/runtime/python/okstra_ctl/user_response.py +1431 -66
  213. package/runtime/python/okstra_ctl/wizard.py +11 -44
  214. package/runtime/python/okstra_ctl/work_categories.py +3 -2
  215. package/runtime/python/okstra_ctl/worker_dispatch.py +5 -2
  216. package/runtime/python/okstra_ctl/worker_prompt_body.py +49 -3
  217. package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
  218. package/runtime/python/okstra_ctl/worker_prompt_policy.py +31 -3
  219. package/runtime/python/okstra_ctl/worker_runner.py +14 -12
  220. package/runtime/python/okstra_ctl/workflow.py +2 -1
  221. package/runtime/python/okstra_ctl/worktree.py +3 -2
  222. package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
  223. package/runtime/python/okstra_ctl/write_policy.py +4 -2
  224. package/runtime/python/okstra_token_usage/antigravity.py +39 -12
  225. package/runtime/python/okstra_token_usage/collect.py +90 -38
  226. package/runtime/python/okstra_token_usage/grok.py +127 -0
  227. package/runtime/schemas/final-report-v2.0.schema.json +9 -0
  228. package/runtime/schemas/final-report-v3.0.schema.json +10219 -0
  229. package/runtime/schemas/report-narrative-v3.0.schema.json +30 -0
  230. package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
  231. package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
  232. package/runtime/skills/okstra-code-review/SKILL.md +21 -11
  233. package/runtime/skills/okstra-container-build/SKILL.md +18 -18
  234. package/runtime/skills/okstra-inspect/SKILL.md +12 -11
  235. package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
  236. package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
  237. package/runtime/skills/okstra-inspect/facets/history.md +9 -14
  238. package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
  239. package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
  240. package/runtime/skills/okstra-inspect/facets/report.md +6 -10
  241. package/runtime/skills/okstra-inspect/facets/status.md +9 -8
  242. package/runtime/skills/okstra-inspect/facets/time.md +3 -3
  243. package/runtime/skills/okstra-manager/SKILL.md +16 -14
  244. package/runtime/skills/okstra-memory/SKILL.md +3 -3
  245. package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
  246. package/runtime/skills/okstra-rollup/SKILL.md +6 -16
  247. package/runtime/skills/okstra-run/SKILL.md +7 -7
  248. package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
  249. package/runtime/skills/okstra-setup/SKILL.md +21 -13
  250. package/runtime/skills/okstra-setup/references/project-config.md +2 -2
  251. package/runtime/skills/okstra-usage/SKILL.md +10 -10
  252. package/runtime/skills/okstra-user-response/SKILL.md +78 -107
  253. package/runtime/templates/report-writer-prompt-preamble.md +29 -19
  254. package/runtime/templates/reports/html/macros/forms.html +6 -4
  255. package/runtime/templates/reports/schedule.template.md +4 -4
  256. package/runtime/templates/worker-error-contract.md +17 -29
  257. package/runtime/validators/validate-run.py +636 -72
  258. package/runtime/validators/validate_session_conformance.py +43 -7
@@ -94,6 +94,7 @@ from okstra_ctl.clarification_items import (
94
94
  user_response_sidecars,
95
95
  )
96
96
  from okstra_ctl.convergence_store import write_json_atomic
97
+ from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
97
98
  from okstra_ctl.incremental_scope import (
98
99
  parse_stage_graph,
99
100
  preview_link_availability_for_report,
@@ -869,13 +870,13 @@ def _validate_sidecar_option(plan_path: Path, option_name: str, errors_t: dict)
869
870
  candidates: list[str] = []
870
871
  if data_path.is_file():
871
872
  try:
872
- data = json.loads(data_path.read_text(encoding="utf-8"))
873
+ data = load_owned_object(data_path, artifact="planning final report")
873
874
  planning = data.get("implementationPlanning") or {}
874
875
  candidates = [
875
876
  c.get("name", "") for c in planning.get("optionCandidates") or []
876
877
  if isinstance(c, dict) and c.get("name")
877
878
  ]
878
- except (OSError, json.JSONDecodeError):
879
+ except (OSError, JsonBoundaryError):
879
880
  candidates = []
880
881
  if option_name not in candidates:
881
882
  raise WizardError(
@@ -2002,8 +2003,8 @@ def _load_wizard_root(workspace_root: str) -> dict:
2002
2003
  "Re-run `okstra install` or check the workspace_root."
2003
2004
  )
2004
2005
  try:
2005
- raw = json.loads(path.read_text(encoding="utf-8"))
2006
- except json.JSONDecodeError as exc:
2006
+ raw = load_owned_object(path, artifact="wizard prompts")
2007
+ except JsonBoundaryError as exc:
2007
2008
  raise WizardError(f"wizard prompt JSON malformed: {path}: {exc}") from exc
2008
2009
  if not isinstance(raw, dict):
2009
2010
  raise WizardError(f"wizard prompt JSON root must be an object: {path}")
@@ -4217,7 +4218,7 @@ def _prior_stage_numbers(state: WizardState) -> set[int]:
4217
4218
  )
4218
4219
  data_path = final_report_data_path(report)
4219
4220
  try:
4220
- data = json.loads(data_path.read_text(encoding="utf-8"))
4221
+ data = load_owned_object(data_path, artifact="planning final report")
4221
4222
  stages = {num for num, _ in parse_stage_graph(data)}
4222
4223
  except (OSError, ValueError, KeyError, TypeError) as exc:
4223
4224
  raise WizardError(
@@ -4300,8 +4301,8 @@ def _suggest_project_pr_template(state: WizardState) -> str:
4300
4301
  if not project_json.is_file():
4301
4302
  return ""
4302
4303
  try:
4303
- data = json.loads(project_json.read_text(encoding="utf-8"))
4304
- except (OSError, json.JSONDecodeError):
4304
+ data = load_owned_object(project_json, artifact="project config")
4305
+ except (OSError, JsonBoundaryError):
4305
4306
  return ""
4306
4307
  val = data.get("prTemplatePath") or ""
4307
4308
  return val if isinstance(val, str) else ""
@@ -5138,39 +5139,6 @@ def _worktree_preview_line_impl(state: WizardState) -> str:
5138
5139
  stage=stage, path=str(decision.path), branch=decision.branch)
5139
5140
 
5140
5141
 
5141
- def _critic_collision_lines(state: WizardState) -> list[str]:
5142
- """critic provider 가 분석 로스터와 겹칠 때 그 대가를 확인 화면에서 말한다.
5143
-
5144
- gap 검증 투표자는 `convergence_engine._is_critic_worker` 가 critic **provider
5145
- 이름**으로 걸러낸다 — critic 워커 id 가 아니라 provider 다. 그래서 같은
5146
- provider 의 분석 워커가 투표에서 빠지고, 분석 워커가 둘뿐인 로스터에서는
5147
- 정족수가 1로 준다. 그 사실이 선택 시점에는 어디에도 드러나지 않아, critic 도
5148
- 돌고 gap 검증 라운드도 완료된 뒤 `apply-critic-gaps` 가 거부할 때에야 알려졌다
5149
- — 이미 워커 실행 비용을 쓴 다음이다.
5150
-
5151
- 막지는 않는다. 겹치는 provider 를 고르는 것이 잘못은 아니고, 그 대가를 알고
5152
- 고르는 것과 모르고 고르는 것의 차이만 없앤다.
5153
- """
5154
- if not state.critic or state.critic == "off":
5155
- return []
5156
- analysers = [
5157
- worker for worker in _resolved_roster(state) if worker != "report-writer"
5158
- ]
5159
- excluded = [
5160
- worker for worker in analysers
5161
- if worker in {state.critic, f"{state.critic}-worker"}
5162
- ]
5163
- if not excluded:
5164
- return []
5165
- return [
5166
- _msg(
5167
- state.workspace_root, "confirmation", "critic_shares_analyser_provider",
5168
- worker=excluded[0],
5169
- remaining=len(analysers) - len(excluded),
5170
- )
5171
- ]
5172
-
5173
-
5174
5142
  def _build_confirm(state: WizardState) -> Prompt:
5175
5143
  t = _p(state.workspace_root, "confirm")
5176
5144
  return Prompt(
@@ -6496,7 +6464,6 @@ def confirmation_block(state: WizardState) -> str:
6496
6464
  if (not role_selection
6497
6465
  and state.task_type in ("requirements-discovery", "error-analysis", "implementation-planning", "final-verification")):
6498
6466
  lines.append(f" critic : {state.critic or '(off)'}")
6499
- lines.extend(_critic_collision_lines(state))
6500
6467
  if state.task_type in _STAGE_SCOPED_TASK_TYPES:
6501
6468
  lines.append(f" approved-plan : {state.approved_plan_path}")
6502
6469
  stage = wizard_stage_confirmation_label(
@@ -6631,8 +6598,8 @@ def _matching_v2_companion(
6631
6598
  source_metadata: dict[str, str],
6632
6599
  ) -> dict[str, Any] | None:
6633
6600
  try:
6634
- payload = json.loads(path.read_text(encoding="utf-8"))
6635
- except (OSError, UnicodeError, json.JSONDecodeError):
6601
+ payload = load_owned_object(path, artifact="wizard state")
6602
+ except (OSError, UnicodeError, JsonBoundaryError):
6636
6603
  return None
6637
6604
  if not isinstance(payload, dict):
6638
6605
  return None
@@ -6661,7 +6628,7 @@ def _load_matching_v2_state(
6661
6628
  def load_state_file(path: Path) -> WizardState:
6662
6629
  source_path = Path(path)
6663
6630
  source = source_path.read_bytes()
6664
- data = json.loads(source)
6631
+ data = load_owned_object(source_path, artifact="wizard state")
6665
6632
  source_version = data.get("executionIdentityVersion", 1)
6666
6633
  source_metadata = _legacy_source_metadata(source_path, source)
6667
6634
  source_digest = source_metadata["sha256"]
@@ -12,6 +12,7 @@ import json
12
12
  from pathlib import Path
13
13
 
14
14
  from .paths import task_dir, task_manifest_file
15
+ from .json_boundary import JsonBoundaryError, load_owned_object
15
16
 
16
17
  WORK_CATEGORIES: tuple[str, ...] = (
17
18
  "bugfix",
@@ -32,8 +33,8 @@ def recorded_work_category(project_root: Path, task_group: str, task_id: str) ->
32
33
  """task-manifest.json 에 이미 기록된 work-category. 없거나 enum 밖이면 ""."""
33
34
  manifest = task_manifest_file(task_dir(project_root, task_group, task_id))
34
35
  try:
35
- data = json.loads(manifest.read_text(encoding="utf-8"))
36
- except (OSError, json.JSONDecodeError):
36
+ data = load_owned_object(manifest, artifact="task manifest")
37
+ except JsonBoundaryError:
37
38
  return ""
38
39
  value = str(data.get("workCategory") or "").strip()
39
40
  return value if is_valid_category(value) else ""
@@ -76,7 +76,7 @@ def _parser() -> argparse.ArgumentParser:
76
76
  parser.add_argument("--okstra-bin", default="")
77
77
  parser.add_argument("--workers", default="")
78
78
  parser.add_argument("--dry-run", action="store_true")
79
- parser.add_argument("--idle-timeout-seconds", default="600")
79
+ parser.add_argument("--idle-timeout-seconds", default="")
80
80
  parser.add_argument("--dispatch-kind", default="initial")
81
81
  parser.add_argument("--jobs-file", default="")
82
82
  return parser
@@ -86,7 +86,10 @@ def _parse_workers(raw: str) -> list[str]:
86
86
  return [item.strip() for item in raw.split(",") if item.strip()]
87
87
 
88
88
 
89
- def _parse_idle_timeout(raw: str) -> int:
89
+ def _parse_idle_timeout(raw: str) -> int | None:
90
+ """명시된 예산, 또는 역할이 정하게 두는 ``None``."""
91
+ if not raw:
92
+ return None
90
93
  try:
91
94
  value = int(raw)
92
95
  except ValueError as exc:
@@ -89,8 +89,18 @@ def report_writer_prompt_body(
89
89
  team_state: Mapping[str, Any],
90
90
  model: str,
91
91
  report_language: str,
92
+ synthesis_packet_path: str = "",
92
93
  ) -> list[str]:
93
94
  """Render the provider-neutral body for the report-writer audience."""
95
+ if str(manifest.get("reportContractVersion") or "") == "3.0":
96
+ return _report_writer_v3_body(
97
+ manifest,
98
+ active_context,
99
+ team_state,
100
+ model,
101
+ report_language,
102
+ synthesis_packet_path,
103
+ )
94
104
  return [
95
105
  f"**Model:** Report writer worker, {model}",
96
106
  f"**Report Language:** {report_language}",
@@ -102,9 +112,11 @@ def report_writer_prompt_body(
102
112
  f"- Task type: `{_require_string(manifest, 'taskType')}`",
103
113
  "",
104
114
  "## Inputs",
105
- *report_writer_input_lines(manifest, active_context, team_state),
106
- "",
107
- mcp_pointer_line(),
115
+ *(
116
+ [f"- Report synthesis packet: `{synthesis_packet_path}`"]
117
+ if synthesis_packet_path
118
+ else report_writer_input_lines(manifest, active_context, team_state)
119
+ ),
108
120
  "",
109
121
  "## Output Contract",
110
122
  "You are the author of TWO files:",
@@ -124,6 +136,40 @@ def report_writer_prompt_body(
124
136
  ]
125
137
 
126
138
 
139
+ def _report_writer_v3_body(
140
+ manifest: Mapping[str, Any],
141
+ active_context: Mapping[str, Any],
142
+ team_state: Mapping[str, Any],
143
+ model: str,
144
+ report_language: str,
145
+ synthesis_packet_path: str,
146
+ ) -> list[str]:
147
+ return [
148
+ f"**Model:** Report writer worker, {model}",
149
+ f"**Report Language:** {report_language}",
150
+ "",
151
+ "# Report Writer Worker Dispatch",
152
+ "",
153
+ "## Task",
154
+ f"- Task key: `{_require_string(manifest, 'taskKey')}`",
155
+ f"- Task type: `{_require_string(manifest, 'taskType')}`",
156
+ "",
157
+ "## Inputs",
158
+ *(
159
+ [f"- Report synthesis packet: `{synthesis_packet_path}`"]
160
+ if synthesis_packet_path
161
+ else report_writer_input_lines(manifest, active_context, team_state)
162
+ ),
163
+ "",
164
+ "## Output Contract",
165
+ "You are the author of the report narrative Markdown at Result Path.",
166
+ "Write the worker-result pointer at Worker Result Path.",
167
+ "Maintain the separate audit sidecar at Audit sidecar path.",
168
+ "Do not write the final report record or another owner's input ledger.",
169
+ "Do not return the report inline.",
170
+ ]
171
+
172
+
127
173
  def report_writer_input_lines(
128
174
  manifest: Mapping[str, Any],
129
175
  active_context: Mapping[str, Any],
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
  from typing import Any, Iterable, Mapping, Sequence
9
9
 
10
10
  from .worker_prompt_body import analysis_worker_label
11
+ from .json_boundary import load_owned_object
11
12
  from .worker_prompt_policy import (
12
13
  ERRORS_PATH_HEADERS,
13
14
  IMPLEMENTATION_HEADERS,
@@ -389,8 +390,8 @@ def _validate_record_metadata(text: str, record: PromptRecord) -> list[str]:
389
390
  if record.metadata_path is None:
390
391
  return [*errors, "agent invocation metadata path is required"]
391
392
  try:
392
- metadata = json.loads(
393
- record.metadata_path.read_text(encoding="utf-8")
393
+ metadata = load_owned_object(
394
+ record.metadata_path, artifact="agent invocation metadata"
394
395
  )
395
396
  except (OSError, UnicodeError, ValueError):
396
397
  return [*errors, "agent invocation metadata is invalid"]
@@ -120,9 +120,7 @@ def resolve_prompt_plan(
120
120
  # that are *supposed* to differ and fail every critic-enabled run.
121
121
  analysis_equality_group = None if dispatch_kind == "critic" else "analysis-core"
122
122
  analysis_duty: AgentAudience = (
123
- "acceptance-critic"
124
- if dispatch_kind == "critic" and task_type == "final-verification"
125
- else "scope-critic"
123
+ critic_duty_for_task_type(task_type)
126
124
  if dispatch_kind == "critic"
127
125
  else "acceptance-verifier"
128
126
  if task_type == "final-verification"
@@ -232,6 +230,36 @@ def _worker_facing_headers(
232
230
  return (*ERRORS_PATH_HEADERS, *required_headers)
233
231
 
234
232
 
233
+ # 하나의 run 이 실제로 디스패치할 수 있는 critic 은 한 종류다. 판정은 task type
234
+ # 만의 함수이므로 여기 한 번만 적고, 로스터 등록과 프롬프트 정체성 검사가 같은
235
+ # 값을 읽는다 — 로스터가 실행될 수 없는 critic 행을 하나 더 들고 있으면 provider
236
+ # 로만 만든 role 라벨이 겹쳐 validate-run 의 중복 역할 검사에 걸린다.
237
+ CRITIC_DUTY_BY_ASSIGNMENT_SEGMENT: dict[str, AgentAudience] = {
238
+ "scope": "scope-critic",
239
+ "acceptance": "acceptance-critic",
240
+ }
241
+
242
+
243
+ def critic_duty_for_task_type(task_type: str) -> AgentAudience:
244
+ """이 task type 의 critic 직무."""
245
+ return (
246
+ "acceptance-critic"
247
+ if task_type == "final-verification"
248
+ else "scope-critic"
249
+ )
250
+
251
+
252
+ def critic_assignment_ref(task_type: str) -> str:
253
+ """이 task type 이 등록할 유일한 critic 배정 참조."""
254
+ duty = critic_duty_for_task_type(task_type)
255
+ segment = next(
256
+ key
257
+ for key, value in CRITIC_DUTY_BY_ASSIGNMENT_SEGMENT.items()
258
+ if value == duty
259
+ )
260
+ return f"critic/{segment}"
261
+
262
+
235
263
  def _executor_worker_id(manifest: Mapping[str, Any]) -> str | None:
236
264
  team_contract = manifest.get("teamContract")
237
265
  if not isinstance(team_contract, Mapping):
@@ -28,6 +28,7 @@ from .domain.worker_exec import (
28
28
  )
29
29
  from .domain.worker_presentation import JsonEvents, Presentation
30
30
  from .session_transcript import SessionTranscript
31
+ from .json_boundary import JsonBoundaryError, write_owned_object_atomic
31
32
 
32
33
  LIVE = "live"
33
34
  QUIET = "quiet"
@@ -225,7 +226,11 @@ def _with_served_model_observation(
225
226
  observation.record(observed)
226
227
  return observed
227
228
 
228
- return JsonEvents(normalise=presentation.normalise, observe=observe)
229
+ return JsonEvents(
230
+ normalise=presentation.normalise,
231
+ observe=observe,
232
+ join_adjacent_text=presentation.join_adjacent_text,
233
+ )
229
234
 
230
235
 
231
236
  class _ServedModelObservation:
@@ -350,9 +355,7 @@ def _pump(
350
355
  stdin_text: str | None = None,
351
356
  ) -> tuple[int, bool, int]:
352
357
  selector = selectors.DefaultSelector()
353
- readers = _register_streams(
354
- selector, process, strategy, transcript, presentation == LIVE
355
- )
358
+ readers = _register_streams(selector, process, strategy, transcript)
356
359
  outgoing = _register_prompt(selector, process, stdin_text)
357
360
  closing_text: str | None = None
358
361
  last_output = time.monotonic()
@@ -387,6 +390,7 @@ def _pump(
387
390
 
388
391
  for reader in readers.values():
389
392
  closing_text = reader.flush() or closing_text
393
+ strategy.flush(transcript)
390
394
 
391
395
  exit_code = process.wait()
392
396
  if closing_text is None and isinstance(strategy, JsonEvents):
@@ -441,13 +445,12 @@ def _register_streams(
441
445
  process: subprocess.Popen[bytes],
442
446
  presentation: Presentation,
443
447
  transcript: SessionTranscript,
444
- live: bool,
445
448
  ) -> dict[int, _LineReader]:
446
449
  """이 자식이 말하는 스트림마다 전략이 지정한 싱크를 붙인다."""
447
450
  assert process.stdout is not None
448
451
  streams = {"stdout": process.stdout, "stderr": process.stderr}
449
452
  readers: dict[int, _LineReader] = {}
450
- for channel, sink in presentation.sinks(transcript, live):
453
+ for channel, sink in presentation.sinks(transcript):
451
454
  stream = streams[channel]
452
455
  assert stream is not None, channel
453
456
  selector.register(stream, selectors.EVENT_READ)
@@ -599,12 +602,11 @@ def _write_status(path: Path, status: Mapping[str, Any]) -> None:
599
602
  Replaced whole rather than rewritten in place: readers poll this file while
600
603
  the worker is still running and must never see a half-written document.
601
604
  """
602
- temporary = Path(f"{path}.tmp")
603
605
  try:
604
- path.parent.mkdir(parents=True, exist_ok=True)
605
- temporary.write_text(
606
- json.dumps(status, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
606
+ write_owned_object_atomic(
607
+ path,
608
+ status,
609
+ artifact="worker wrapper status",
607
610
  )
608
- os.replace(temporary, path)
609
- except OSError:
611
+ except (OSError, JsonBoundaryError):
610
612
  return
@@ -14,6 +14,7 @@ from __future__ import annotations
14
14
 
15
15
  import json
16
16
  from pathlib import Path
17
+ from .json_boundary import load_owned_object
17
18
 
18
19
  PHASE_SEQUENCE = [
19
20
  "requirements-discovery",
@@ -160,7 +161,7 @@ def load_phase_forbidden(workspace_root: Path) -> dict[str, str]:
160
161
  `prompts/profiles/forbidden-actions.json`.
161
162
  """
162
163
  path = Path(workspace_root) / "prompts" / "profiles" / "forbidden-actions.json"
163
- data = json.loads(path.read_text(encoding="utf-8"))
164
+ data = load_owned_object(path, artifact="forbidden actions profile")
164
165
  if "unknown" not in data:
165
166
  # compute_workflow_state falls back to the "unknown" entry for any
166
167
  # unmapped task_type; without it a missing/renamed key would surface as
@@ -44,6 +44,7 @@ from typing import Optional
44
44
  from okstra_project.dirs import okstra_home, project_json_path
45
45
 
46
46
  from .ids import _safe_fs_segment
47
+ from .json_boundary import JsonBoundaryError, load_owned_object
47
48
  from . import worktree_registry
48
49
  from .seeding import (
49
50
  SettingsLinkError,
@@ -415,8 +416,8 @@ def _read_project_json_field(project_root: Path, field: str) -> Optional[tuple[s
415
416
  if not target.is_file():
416
417
  return None
417
418
  try:
418
- data = json.loads(target.read_text(encoding="utf-8"))
419
- except (OSError, json.JSONDecodeError):
419
+ data = load_owned_object(target, artifact="project config")
420
+ except (OSError, JsonBoundaryError):
420
421
  return None
421
422
  if not isinstance(data, dict):
422
423
  return None
@@ -6,6 +6,8 @@ from dataclasses import dataclass
6
6
  from pathlib import Path
7
7
  from typing import Any, Mapping
8
8
 
9
+ from .json_boundary import JsonBoundaryError, load_owned_object
10
+
9
11
 
10
12
  @dataclass(frozen=True)
11
13
  class WrapperStatus:
@@ -65,8 +67,8 @@ def prompt_derived_paths(prompt_path: Path) -> tuple[Path, ...]:
65
67
 
66
68
  def read_wrapper_status(path: Path) -> WrapperStatus | None:
67
69
  try:
68
- raw = json.loads(path.read_text(encoding="utf-8"))
69
- except (OSError, json.JSONDecodeError):
70
+ raw = load_owned_object(path, artifact="worker wrapper status")
71
+ except (OSError, JsonBoundaryError):
70
72
  return None
71
73
  if not isinstance(raw, dict):
72
74
  return None
@@ -9,6 +9,8 @@ from dataclasses import dataclass
9
9
  from pathlib import Path, PurePosixPath
10
10
  from typing import Any, Literal
11
11
 
12
+ from .json_boundary import JsonBoundaryError, load_owned_object
13
+
12
14
  BoundaryPrecision = Literal["exact-path", "directory-boundary", "none"]
13
15
 
14
16
  _PRECISION_RANK = {"none": 0, "directory-boundary": 1, "exact-path": 2}
@@ -538,8 +540,8 @@ def _rooted(project_root: Path, value: str) -> Path:
538
540
 
539
541
  def _read_json(path: Path, label: str) -> Mapping[str, Any]:
540
542
  try:
541
- value = json.loads(path.read_text(encoding="utf-8"))
542
- except (OSError, UnicodeError, json.JSONDecodeError) as exc:
543
+ value = load_owned_object(path, artifact=label)
544
+ except (OSError, UnicodeError, JsonBoundaryError) as exc:
543
545
  raise WritePolicyError(f"{label} is invalid: {path}") from exc
544
546
  if not isinstance(value, Mapping):
545
547
  raise WritePolicyError(f"{label} must be an object: {path}")
@@ -1,29 +1,56 @@
1
1
  """Antigravity (`agy`) CLI usage collector.
2
2
 
3
- okstra dispatches the antigravity-worker via `agy --print` (see
4
- `okstra-antigravity-exec.sh`). That non-interactive print mode does not persist
5
- a parseable transcript with per-call token counts to any known location, so
6
- per-run token/cost usage cannot be attributed to the antigravity worker. These
7
- collectors therefore report "unavailable" rather than fabricating numbers — the
8
- worker's usage block degrades to `na` and `/okstra-inspect context-cost` shows
9
- N/A for antigravity. If a future `agy` build exposes a transcript, wire its
10
- parser here (model ids stay `gemini-*`, so `antigravity_cost_usd` already prices it).
3
+ 공식 headless 문서는 ``--output-format json`` / ``stream-json`` 의 최종
4
+ ``usage`` 객체에 토큰을 둔다. okstra 이미 ``stream-json`` 으로 호출하고
5
+ 줄을 워커 로그에 남긴다. 정본은 마지막 ``result.usage`` 이다.
11
6
  """
12
7
  from __future__ import annotations
13
8
 
14
9
  from pathlib import Path
15
10
 
16
-
17
- PRINT_MODE_USAGE_NOTE = "N/A — print mode exposes no transcript"
11
+ from .jsonl_io import iter_jsonl
18
12
 
19
13
 
20
14
  def antigravity_session_total(json_path: Path) -> dict:
21
- return {"totalTokens": 0, "available": False}
15
+ """stream-json 로그에서 마지막 result.usage 를 읽는다."""
16
+ result_usage: dict | None = None
17
+ step_usage: dict | None = None
18
+ model: str | None = None
19
+ for record in iter_jsonl(json_path):
20
+ kind = record.get("event")
21
+ if kind == "init":
22
+ init = record.get("init")
23
+ if isinstance(init, dict) and isinstance(init.get("model"), str):
24
+ model = init["model"]
25
+ continue
26
+ if kind == "result":
27
+ result = record.get("result")
28
+ if isinstance(result, dict) and isinstance(result.get("usage"), dict):
29
+ result_usage = result["usage"]
30
+ continue
31
+ if kind == "step_update":
32
+ step = record.get("step_update")
33
+ if isinstance(step, dict) and isinstance(step.get("usage"), dict):
34
+ step_usage = step["usage"]
35
+ usage = result_usage or step_usage
36
+ if usage is None:
37
+ return {"totalTokens": 0, "available": False}
38
+ return {
39
+ "totalTokens": usage.get("total_tokens", 0) or 0,
40
+ "inputTokens": usage.get("input_tokens", 0) or 0,
41
+ "outputTokens": usage.get("output_tokens", 0) or 0,
42
+ "thoughtsTokens": usage.get("thinking_tokens", 0) or 0,
43
+ "cacheReadTokens": usage.get("cache_read_tokens", 0) or 0,
44
+ "model": model,
45
+ "available": True,
46
+ }
22
47
 
23
48
 
24
49
  def find_antigravity_session(project_root: Path, started_at: str, ended_at: str) -> Path | None:
25
- return None
50
+ sessions = find_antigravity_sessions(project_root, started_at, ended_at)
51
+ return sessions[-1] if sessions else None
26
52
 
27
53
 
28
54
  def find_antigravity_sessions(project_root: Path, started_at: str, ended_at: str) -> list[Path]:
55
+ # 세션 파일은 홈 디렉터리가 아니라 워커 로그다. 경로는 호출부가 prompt 에서 붙인다.
29
56
  return []