okstra 0.180.0 → 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 (223) hide show
  1. package/dist/cli-registry.mjs +16 -2
  2. package/dist/cli-registry.mjs.map +1 -1
  3. package/dist/commands/execute/render-bundle.d.mts +4 -2
  4. package/dist/commands/execute/render-bundle.mjs +46 -5
  5. package/dist/commands/execute/render-bundle.mjs.map +1 -1
  6. package/dist/commands/execute/run.mjs +11 -3
  7. package/dist/commands/execute/run.mjs.map +1 -1
  8. package/dist/commands/inspect/model-io.d.mts +1 -0
  9. package/dist/commands/inspect/model-io.mjs +25 -0
  10. package/dist/commands/inspect/model-io.mjs.map +1 -0
  11. package/dist/commands/inspect/stage-map.mjs +29 -8
  12. package/dist/commands/inspect/stage-map.mjs.map +1 -1
  13. package/dist/commands/inspect/task-list.mjs +52 -6
  14. package/dist/commands/inspect/task-list.mjs.map +1 -1
  15. package/dist/commands/inspect/user-response.mjs +14 -4
  16. package/dist/commands/inspect/user-response.mjs.map +1 -1
  17. package/dist/commands/lifecycle/check-project.d.mts +1 -0
  18. package/dist/commands/lifecycle/check-project.mjs +69 -50
  19. package/dist/commands/lifecycle/check-project.mjs.map +1 -1
  20. package/dist/commands/lifecycle/contract-check.d.mts +1 -0
  21. package/dist/commands/lifecycle/contract-check.mjs +18 -0
  22. package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
  23. package/dist/commands/lifecycle/preflight.mjs +154 -51
  24. package/dist/commands/lifecycle/preflight.mjs.map +1 -1
  25. package/dist/commands/pr/pr.d.mts +1 -0
  26. package/dist/commands/pr/pr.mjs +19 -1
  27. package/dist/commands/pr/pr.mjs.map +1 -1
  28. package/dist/commands/report/agent-activity.mjs +2 -2
  29. package/dist/commands/report/translate.mjs +3 -0
  30. package/dist/commands/report/translate.mjs.map +1 -1
  31. package/dist/lib/host-registry-client.mjs +13 -9
  32. package/dist/lib/host-registry-client.mjs.map +1 -1
  33. package/docs/architecture.md +11 -0
  34. package/docs/cli.md +26 -13
  35. package/docs/container.md +6 -4
  36. package/docs/contributor-change-matrix.md +1 -1
  37. package/docs/for-ai/README.md +2 -2
  38. package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
  39. package/docs/for-ai/skills/okstra-code-review.md +4 -4
  40. package/docs/for-ai/skills/okstra-container-build.md +20 -17
  41. package/docs/for-ai/skills/okstra-inspect.md +20 -23
  42. package/docs/for-ai/skills/okstra-manager.md +19 -18
  43. package/docs/for-ai/skills/okstra-memory.md +2 -2
  44. package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
  45. package/docs/for-ai/skills/okstra-rollup.md +14 -13
  46. package/docs/for-ai/skills/okstra-run.md +7 -3
  47. package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
  48. package/docs/for-ai/skills/okstra-setup.md +7 -7
  49. package/docs/for-ai/skills/okstra-usage.md +5 -4
  50. package/docs/for-ai/skills/okstra-user-response.md +50 -32
  51. package/docs/project-structure-overview.md +29 -26
  52. package/docs/task-process/implementation.md +7 -4
  53. package/package.json +1 -1
  54. package/runtime/BUILD.json +2 -2
  55. package/runtime/agents/workers/claude-worker.md +4 -4
  56. package/runtime/agents/workers/report-writer-worker.md +3 -3
  57. package/runtime/agents/workers/translator-worker.md +5 -13
  58. package/runtime/bin/okstra-error-log.py +51 -11
  59. package/runtime/bin/okstra-report-translate.py +210 -23
  60. package/runtime/prompts/host-orchestration/implementation.md +1 -1
  61. package/runtime/prompts/launch.template.md +3 -7
  62. package/runtime/prompts/lead/context-loader.md +41 -141
  63. package/runtime/prompts/lead/convergence.md +8 -6
  64. package/runtime/prompts/lead/okstra-lead-contract.md +25 -35
  65. package/runtime/prompts/lead/plan-body-verification.md +187 -14
  66. package/runtime/prompts/lead/report-writer.md +20 -1
  67. package/runtime/prompts/lead/team-contract.md +8 -53
  68. package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
  69. package/runtime/prompts/profiles/_common-contract.md +1 -1
  70. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  71. package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
  72. package/runtime/prompts/profiles/final-verification.md +1 -1
  73. package/runtime/prompts/profiles/implementation-planning.md +8 -2
  74. package/runtime/prompts/profiles/release-handoff.md +0 -1
  75. package/runtime/prompts/wizard/prompts.ko.json +0 -1
  76. package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
  77. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
  78. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +6 -1
  79. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
  80. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
  81. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
  82. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
  83. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
  84. package/runtime/python/okstra_ctl/agent_activity.py +118 -35
  85. package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
  86. package/runtime/python/okstra_ctl/agent_prompt_cli.py +65 -18
  87. package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
  88. package/runtime/python/okstra_ctl/analysis_packet.py +81 -1
  89. package/runtime/python/okstra_ctl/approval_decisions.py +3 -2
  90. package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
  91. package/runtime/python/okstra_ctl/backfill.py +13 -10
  92. package/runtime/python/okstra_ctl/batch.py +2 -4
  93. package/runtime/python/okstra_ctl/build_tools.py +6 -3
  94. package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
  95. package/runtime/python/okstra_ctl/clarification_items.py +27 -13
  96. package/runtime/python/okstra_ctl/cmux.py +130 -52
  97. package/runtime/python/okstra_ctl/code_review_target.py +34 -8
  98. package/runtime/python/okstra_ctl/consumers.py +5 -4
  99. package/runtime/python/okstra_ctl/container.py +103 -8
  100. package/runtime/python/okstra_ctl/context_cost.py +2 -1
  101. package/runtime/python/okstra_ctl/contract_graph.py +497 -0
  102. package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
  103. package/runtime/python/okstra_ctl/convergence.py +338 -17
  104. package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
  105. package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
  106. package/runtime/python/okstra_ctl/convergence_store.py +55 -34
  107. package/runtime/python/okstra_ctl/design_prep.py +7 -4
  108. package/runtime/python/okstra_ctl/dispatch_core.py +35 -65
  109. package/runtime/python/okstra_ctl/dispatch_state.py +134 -59
  110. package/runtime/python/okstra_ctl/doctor.py +6 -3
  111. package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
  112. package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
  113. package/runtime/python/okstra_ctl/error_log_write.py +35 -30
  114. package/runtime/python/okstra_ctl/error_report.py +26 -1
  115. package/runtime/python/okstra_ctl/error_zip.py +27 -5
  116. package/runtime/python/okstra_ctl/execution_identity.py +3 -2
  117. package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
  118. package/runtime/python/okstra_ctl/final_report_schema.py +2 -2
  119. package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
  120. package/runtime/python/okstra_ctl/fixed_text.py +39 -0
  121. package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
  122. package/runtime/python/okstra_ctl/handoff.py +5 -4
  123. package/runtime/python/okstra_ctl/i18n.py +4 -2
  124. package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
  125. package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
  126. package/runtime/python/okstra_ctl/incremental_carry.py +2 -1
  127. package/runtime/python/okstra_ctl/incremental_scope.py +8 -3
  128. package/runtime/python/okstra_ctl/index.py +8 -11
  129. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
  130. package/runtime/python/okstra_ctl/invocation.py +3 -6
  131. package/runtime/python/okstra_ctl/json_boundary.py +366 -0
  132. package/runtime/python/okstra_ctl/json_registry.py +10 -12
  133. package/runtime/python/okstra_ctl/jsonl.py +19 -2
  134. package/runtime/python/okstra_ctl/lead_events.py +33 -1
  135. package/runtime/python/okstra_ctl/listing.py +3 -3
  136. package/runtime/python/okstra_ctl/log_report.py +24 -2
  137. package/runtime/python/okstra_ctl/manager_cli.py +92 -7
  138. package/runtime/python/okstra_ctl/manager_store.py +12 -10
  139. package/runtime/python/okstra_ctl/material.py +5 -1
  140. package/runtime/python/okstra_ctl/migrate.py +29 -25
  141. package/runtime/python/okstra_ctl/model_cli.py +3 -15
  142. package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
  143. package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
  144. package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
  145. package/runtime/python/okstra_ctl/paths.py +9 -0
  146. package/runtime/python/okstra_ctl/plan_items.py +115 -4
  147. package/runtime/python/okstra_ctl/plan_items_cli.py +513 -18
  148. package/runtime/python/okstra_ctl/pr_template.py +3 -2
  149. package/runtime/python/okstra_ctl/project_meta.py +5 -7
  150. package/runtime/python/okstra_ctl/recap.py +5 -4
  151. package/runtime/python/okstra_ctl/reconcile.py +21 -27
  152. package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
  153. package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
  154. package/runtime/python/okstra_ctl/render.py +26 -15
  155. package/runtime/python/okstra_ctl/render_final_report.py +3 -2
  156. package/runtime/python/okstra_ctl/report_assembly.py +172 -17
  157. package/runtime/python/okstra_ctl/report_finalize.py +7 -10
  158. package/runtime/python/okstra_ctl/report_html/render.py +3 -2
  159. package/runtime/python/okstra_ctl/report_language.py +3 -2
  160. package/runtime/python/okstra_ctl/report_markdown.py +13 -1
  161. package/runtime/python/okstra_ctl/report_narrative.py +40 -8
  162. package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
  163. package/runtime/python/okstra_ctl/report_views.py +3 -2
  164. package/runtime/python/okstra_ctl/rollup.py +65 -4
  165. package/runtime/python/okstra_ctl/run.py +159 -56
  166. package/runtime/python/okstra_ctl/run_audit.py +3 -2
  167. package/runtime/python/okstra_ctl/run_context.py +6 -9
  168. package/runtime/python/okstra_ctl/run_index_row.py +2 -8
  169. package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
  170. package/runtime/python/okstra_ctl/schema_excerpt.py +4 -2
  171. package/runtime/python/okstra_ctl/session_transcript.py +27 -1
  172. package/runtime/python/okstra_ctl/set_work_status.py +64 -38
  173. package/runtime/python/okstra_ctl/stage_fix_carry.py +4 -2
  174. package/runtime/python/okstra_ctl/stage_map.py +26 -6
  175. package/runtime/python/okstra_ctl/stage_targets.py +3 -4
  176. package/runtime/python/okstra_ctl/team.py +2 -1
  177. package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
  178. package/runtime/python/okstra_ctl/time_report.py +51 -4
  179. package/runtime/python/okstra_ctl/usage_identity.py +2 -1
  180. package/runtime/python/okstra_ctl/usage_report.py +58 -4
  181. package/runtime/python/okstra_ctl/user_response.py +1431 -66
  182. package/runtime/python/okstra_ctl/wizard.py +11 -44
  183. package/runtime/python/okstra_ctl/work_categories.py +3 -2
  184. package/runtime/python/okstra_ctl/worker_prompt_body.py +18 -7
  185. package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
  186. package/runtime/python/okstra_ctl/worker_runner.py +14 -12
  187. package/runtime/python/okstra_ctl/workflow.py +2 -1
  188. package/runtime/python/okstra_ctl/worktree.py +3 -2
  189. package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
  190. package/runtime/python/okstra_ctl/write_policy.py +4 -2
  191. package/runtime/python/okstra_token_usage/antigravity.py +39 -12
  192. package/runtime/python/okstra_token_usage/collect.py +90 -38
  193. package/runtime/python/okstra_token_usage/grok.py +127 -0
  194. package/runtime/schemas/final-report-v2.0.schema.json +9 -0
  195. package/runtime/schemas/final-report-v3.0.schema.json +9 -0
  196. package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
  197. package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
  198. package/runtime/skills/okstra-code-review/SKILL.md +21 -11
  199. package/runtime/skills/okstra-container-build/SKILL.md +18 -18
  200. package/runtime/skills/okstra-inspect/SKILL.md +12 -11
  201. package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
  202. package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
  203. package/runtime/skills/okstra-inspect/facets/history.md +9 -14
  204. package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
  205. package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
  206. package/runtime/skills/okstra-inspect/facets/report.md +6 -10
  207. package/runtime/skills/okstra-inspect/facets/status.md +9 -8
  208. package/runtime/skills/okstra-inspect/facets/time.md +3 -3
  209. package/runtime/skills/okstra-manager/SKILL.md +16 -14
  210. package/runtime/skills/okstra-memory/SKILL.md +3 -3
  211. package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
  212. package/runtime/skills/okstra-rollup/SKILL.md +6 -16
  213. package/runtime/skills/okstra-run/SKILL.md +7 -7
  214. package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
  215. package/runtime/skills/okstra-setup/SKILL.md +21 -13
  216. package/runtime/skills/okstra-setup/references/project-config.md +2 -2
  217. package/runtime/skills/okstra-usage/SKILL.md +10 -10
  218. package/runtime/skills/okstra-user-response/SKILL.md +78 -107
  219. package/runtime/templates/report-writer-prompt-preamble.md +17 -1
  220. package/runtime/templates/reports/schedule.template.md +4 -4
  221. package/runtime/templates/worker-error-contract.md +17 -29
  222. package/runtime/validators/validate-run.py +379 -63
  223. package/runtime/validators/validate_session_conformance.py +43 -7
@@ -16,6 +16,7 @@ state passing, and are read once at the start.
16
16
  from __future__ import annotations
17
17
 
18
18
  import hashlib
19
+ from argparse import Action, ArgumentError, ArgumentParser, Namespace
19
20
  import json
20
21
  import os
21
22
  import re
@@ -144,6 +145,11 @@ from .agent_invocation import (
144
145
  verify_agent_invocation,
145
146
  )
146
147
  from .report_contract import CURRENT_REPORT_SCHEMA_VERSION
148
+ from .json_boundary import (
149
+ JsonBoundaryError,
150
+ load_owned_object,
151
+ write_owned_object_atomic,
152
+ )
147
153
  from .path_resolve import relative_to_project_root, resolve_user_file
148
154
  from .render import (
149
155
  apply_lead_prompt_defaults,
@@ -257,8 +263,8 @@ def _load_final_report_data_if_present(report_path: Path) -> tuple[Path, dict] |
257
263
  if not data_path.is_file():
258
264
  return None
259
265
  try:
260
- data = json.loads(data_path.read_text(encoding="utf-8"))
261
- except json.JSONDecodeError as exc:
266
+ data = load_owned_object(data_path, artifact="approved plan final report")
267
+ except JsonBoundaryError as exc:
262
268
  raise PrepareError(
263
269
  f"approved plan data.json is invalid JSON: {data_path}: {exc}"
264
270
  ) from exc
@@ -671,7 +677,6 @@ def _record_start(
671
677
  """record_start hook 호출. okstra-central.sh 의 bash wrapper 와 같은 동작
672
678
  이지만 python 직접 호출이라 환경 변수 의존 없음.
673
679
  """
674
- import json as _json
675
680
  from datetime import datetime, timezone
676
681
  from okstra_ctl import record_start
677
682
  from .locks import central_lock
@@ -686,11 +691,15 @@ def _record_start(
686
691
  (home / f).touch(exist_ok=True)
687
692
  state_file = home / "state.json"
688
693
  if not state_file.exists():
689
- state_file.write_text(_json.dumps({
690
- "schemaVersion": "1",
691
- "createdAt": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
692
- "backfilledAt": None,
693
- }, indent=2) + "\n", encoding="utf-8")
694
+ write_owned_object_atomic(
695
+ state_file,
696
+ {
697
+ "schemaVersion": "1",
698
+ "createdAt": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
699
+ "backfilledAt": None,
700
+ },
701
+ artifact="global state",
702
+ )
694
703
  os.chmod(state_file, 0o600)
695
704
  # 중앙 인덱스 락은 reconcile.py 와 공유하는 central_lock 단일 기준점을 쓴다.
696
705
  # 직접 flock 을 손으로 깔면 락 파일/모드가 두 경로에서 갈라질 수 있다.
@@ -1533,8 +1542,8 @@ def validate_project_qa_commands(task_type: str, project_root: Path) -> None:
1533
1542
  if not project_json.is_file():
1534
1543
  return
1535
1544
  try:
1536
- project_meta = json.loads(project_json.read_text())
1537
- except (OSError, json.JSONDecodeError) as exc:
1545
+ project_meta = load_owned_object(project_json, artifact="project config")
1546
+ except (OSError, JsonBoundaryError) as exc:
1538
1547
  raise PrepareError(
1539
1548
  f"project.json read failed at {project_json}: {exc}"
1540
1549
  ) from exc
@@ -1569,11 +1578,13 @@ def _apply_qa_waiver_if_requested(inp: "PrepareInputs", project_root: Path) -> N
1569
1578
  ):
1570
1579
  if not manifest_path.is_file():
1571
1580
  raise PrepareError(f"--qa-waiver: conformance manifest not found at {manifest_path}")
1572
- manifest = json.loads(manifest_path.read_text())
1581
+ manifest = load_owned_object(manifest_path, artifact="conformance manifest")
1573
1582
  when = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
1574
1583
  if not apply_qa_waiver(manifest, stage_key, reason, at=when):
1575
1584
  raise PrepareError(f"--qa-waiver: stageKey {stage_key!r} not in manifest {manifest_path}")
1576
- manifest_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n")
1585
+ write_owned_object_atomic(
1586
+ manifest_path, manifest, artifact="conformance manifest"
1587
+ )
1577
1588
 
1578
1589
 
1579
1590
  def _clear_stale_stage_waiver(inp: "PrepareInputs", project_root: Path, stage: int) -> None:
@@ -1591,7 +1602,7 @@ def _clear_stale_stage_waiver(inp: "PrepareInputs", project_root: Path, stage: i
1591
1602
  )
1592
1603
  if not manifest_path.is_file():
1593
1604
  return
1594
- manifest = json.loads(manifest_path.read_text())
1605
+ manifest = load_owned_object(manifest_path, artifact="conformance manifest")
1595
1606
  entries = manifest.get("entries") if isinstance(manifest, dict) else None
1596
1607
  if not isinstance(entries, list):
1597
1608
  return
@@ -1611,8 +1622,8 @@ def _clear_stale_stage_waiver(inp: "PrepareInputs", project_root: Path, stage: i
1611
1622
  if parsed is not None and parsed[0] == stage_key:
1612
1623
  return # user intentionally waived this stage for this run
1613
1624
  if clear_qa_waiver(manifest, stage_key):
1614
- manifest_path.write_text(
1615
- json.dumps(manifest, indent=2, ensure_ascii=False) + "\n"
1625
+ write_owned_object_atomic(
1626
+ manifest_path, manifest, artifact="conformance manifest"
1616
1627
  )
1617
1628
 
1618
1629
 
@@ -2124,8 +2135,8 @@ def _read_model_defaults(path: Path) -> dict[str, tuple[str, ...]]:
2124
2135
  if not path.is_file():
2125
2136
  return {}
2126
2137
  try:
2127
- payload = json.loads(path.read_text(encoding="utf-8"))
2128
- except (OSError, json.JSONDecodeError) as exc:
2138
+ payload = load_owned_object(path, artifact="model defaults config")
2139
+ except (OSError, JsonBoundaryError) as exc:
2129
2140
  raise PrepareError(f"model defaults file is invalid: {path}: {exc}") from exc
2130
2141
  raw_defaults = payload.get("modelDefaults", {}) if isinstance(payload, dict) else {}
2131
2142
  if not isinstance(raw_defaults, dict):
@@ -3327,9 +3338,10 @@ def _render_lead_prompt_and_snapshot(
3327
3338
  inp.task_type,
3328
3339
  installed_version(),
3329
3340
  )
3330
- Path(ctx["FINAL_REPORT_SCHEMA_PATH"]).write_text(
3331
- json.dumps(_excerpt, indent=2, ensure_ascii=False) + "\n",
3332
- encoding="utf-8",
3341
+ write_owned_object_atomic(
3342
+ Path(ctx["FINAL_REPORT_SCHEMA_PATH"]),
3343
+ _excerpt,
3344
+ artifact="final report schema excerpt",
3333
3345
  )
3334
3346
  except Exception: # noqa: BLE001 — advisory artifact; never fail prep over it
3335
3347
  pass
@@ -3339,7 +3351,9 @@ def _render_lead_prompt_and_snapshot(
3339
3351
  json.loads(ctx["INVOCATION_ASSIGNMENTS_JSON"])["lead"]
3340
3352
  )
3341
3353
  identity = invocation_execution_identity_from_manifest(
3342
- json.loads(Path(ctx["RUN_MANIFEST_PATH"]).read_text(encoding="utf-8")),
3354
+ load_owned_object(
3355
+ Path(ctx["RUN_MANIFEST_PATH"]), artifact="run manifest"
3356
+ ),
3343
3357
  assignment=assignment,
3344
3358
  assignment_ref="lead",
3345
3359
  duty_id="lead",
@@ -3492,8 +3506,8 @@ def _read_existing_manifest(manifest_path: Path) -> dict:
3492
3506
  if not manifest_path.exists():
3493
3507
  return {}
3494
3508
  try:
3495
- return json.loads(manifest_path.read_text(encoding="utf-8"))
3496
- except (OSError, json.JSONDecodeError):
3509
+ return load_owned_object(manifest_path, artifact="run manifest")
3510
+ except (OSError, JsonBoundaryError):
3497
3511
  return {}
3498
3512
 
3499
3513
 
@@ -3987,7 +4001,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
3987
4001
  try:
3988
4002
  json.loads(runtime_resolution_json or "{}")
3989
4003
  except json.JSONDecodeError as exc:
3990
- raise PrepareError(f"invalid --runtime-resolution-json: {exc}") from exc
4004
+ raise PrepareError(f"invalid runtime resolution data: {exc}") from exc
3991
4005
  # Probed once here and reused below, so the gate and the manifest cannot
3992
4006
  # disagree about which backend this run is on.
3993
4007
  terminal_backend = detect_terminal_backend()
@@ -4302,11 +4316,77 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
4302
4316
  )
4303
4317
 
4304
4318
 
4319
+ class _StoreRuntimeSingleton(Action):
4320
+ def __call__(
4321
+ self,
4322
+ parser: ArgumentParser,
4323
+ namespace: Namespace,
4324
+ values: object,
4325
+ option_string: str | None = None,
4326
+ ) -> None:
4327
+ seen = getattr(namespace, "_runtime_singletons_seen", frozenset())
4328
+ if self.dest in seen:
4329
+ raise ArgumentError(
4330
+ self, f"{option_string} may be supplied only once"
4331
+ )
4332
+ setattr(namespace, self.dest, True if self.nargs == 0 else values)
4333
+ setattr(namespace, "_runtime_singletons_seen", seen | {self.dest})
4334
+
4335
+
4336
+ def _add_runtime_resolution_arguments(parser: ArgumentParser) -> None:
4337
+ runtime_ids = (*default_host_registry().ids(), "all")
4338
+ parser.add_argument(
4339
+ "--lead-runtime",
4340
+ action=_StoreRuntimeSingleton,
4341
+ default="claude-code",
4342
+ choices=runtime_ids,
4343
+ dest="lead_runtime",
4344
+ help=(
4345
+ "Lead runtime adapter. Default: claude-code. `codex` renders "
4346
+ "Codex CLI artifact bundles; `antigravity` renders Antigravity CLI "
4347
+ "artifact bundles; `external` renders neutral bootstrap bundles for "
4348
+ "manifest inspection until a dispatch backend is enabled."
4349
+ ),
4350
+ )
4351
+ parser.add_argument(
4352
+ "--lead-runtime-request", action=_StoreRuntimeSingleton, default=""
4353
+ )
4354
+ parser.add_argument(
4355
+ "--runtime-resolution-launch-mode",
4356
+ action=_StoreRuntimeSingleton,
4357
+ choices=("lead", "team", "catalog"),
4358
+ default="",
4359
+ )
4360
+ parser.add_argument(
4361
+ "--runtime-resolution-install-target", action="append", default=[]
4362
+ )
4363
+ for flag in (
4364
+ "--runtime-resolution-host",
4365
+ "--runtime-resolution-reason",
4366
+ "--runtime-resolution-fallback-from",
4367
+ ):
4368
+ parser.add_argument(flag, action=_StoreRuntimeSingleton, default="")
4369
+ parser.add_argument(
4370
+ "--runtime-resolution-confidence",
4371
+ action=_StoreRuntimeSingleton,
4372
+ choices=("none", "low", "medium", "high"),
4373
+ default="",
4374
+ )
4375
+ parser.add_argument("--runtime-resolution-warning", action="append", default=[])
4376
+ parser.add_argument(
4377
+ "--runtime-resolution-catalog-hosts-present",
4378
+ action=_StoreRuntimeSingleton,
4379
+ nargs=0,
4380
+ default=False,
4381
+ )
4382
+ parser.add_argument(
4383
+ "--runtime-resolution-catalog-host-id", action="append", default=[]
4384
+ )
4385
+
4386
+
4305
4387
  def build_prepare_argument_parser():
4306
4388
  """Build the shared flat-argument parser used by public prepare shims."""
4307
- import argparse
4308
-
4309
- p = argparse.ArgumentParser()
4389
+ p = ArgumentParser()
4310
4390
  p.add_argument("--workspace-root", required=True)
4311
4391
  p.add_argument("--project-root", required=True)
4312
4392
  p.add_argument("--project-id", required=True)
@@ -4348,30 +4428,7 @@ def build_prepare_argument_parser():
4348
4428
  p.add_argument("--worker-model", default="", dest="worker_models_raw")
4349
4429
  p.add_argument("--report-writer-provider", default="")
4350
4430
  p.add_argument("--report-writer-model", default="")
4351
- p.add_argument(
4352
- "--lead-runtime",
4353
- default="claude-code",
4354
- choices=list(default_host_registry().ids()),
4355
- dest="lead_runtime",
4356
- help=(
4357
- "Lead runtime adapter. Default: claude-code. `codex` renders "
4358
- "Codex CLI artifact bundles; `antigravity` renders Antigravity CLI "
4359
- "artifact bundles; `external` renders neutral bootstrap bundles for "
4360
- "manifest inspection until a dispatch backend is enabled."
4361
- ),
4362
- )
4363
- p.add_argument(
4364
- "--lead-runtime-request",
4365
- default="",
4366
- dest="lead_runtime_request",
4367
- help="Original lead runtime request before Node-side resolution, e.g. auto.",
4368
- )
4369
- p.add_argument(
4370
- "--runtime-resolution-json",
4371
- default="",
4372
- dest="runtime_resolution_json",
4373
- help="Node-side runtime resolution payload serialized as JSON.",
4374
- )
4431
+ _add_runtime_resolution_arguments(p)
4375
4432
  p.add_argument("--executor", default="")
4376
4433
  p.add_argument("--critic", default="")
4377
4434
  p.add_argument("--related-tasks", default="", dest="related_tasks_raw")
@@ -4486,6 +4543,51 @@ def build_prepare_argument_parser():
4486
4543
  return p
4487
4544
 
4488
4545
 
4546
+ def _runtime_resolution_json_from_args(args: Namespace) -> str:
4547
+ values = {
4548
+ "launch mode": args.runtime_resolution_launch_mode,
4549
+ "host": args.runtime_resolution_host,
4550
+ "confidence": args.runtime_resolution_confidence,
4551
+ "reason": args.runtime_resolution_reason,
4552
+ }
4553
+ optional_values = (
4554
+ args.runtime_resolution_install_target,
4555
+ args.runtime_resolution_fallback_from,
4556
+ args.runtime_resolution_warning,
4557
+ args.runtime_resolution_catalog_hosts_present,
4558
+ args.runtime_resolution_catalog_host_id,
4559
+ )
4560
+ if not any(values.values()) and not any(optional_values):
4561
+ return "{}"
4562
+ missing = [name for name, value in values.items() if not value]
4563
+ if missing:
4564
+ raise PrepareError(
4565
+ "incomplete runtime resolution arguments: " + ", ".join(missing)
4566
+ )
4567
+ if (
4568
+ args.runtime_resolution_catalog_host_id
4569
+ and not args.runtime_resolution_catalog_hosts_present
4570
+ ):
4571
+ raise PrepareError(
4572
+ "runtime resolution catalog host IDs require the presence marker"
4573
+ )
4574
+ payload: dict[str, object] = {
4575
+ "ok": True,
4576
+ "requestedRuntime": args.lead_runtime_request or args.lead_runtime,
4577
+ "resolvedRuntime": args.lead_runtime,
4578
+ "launchMode": args.runtime_resolution_launch_mode,
4579
+ "installTargets": args.runtime_resolution_install_target,
4580
+ "host": args.runtime_resolution_host,
4581
+ "confidence": args.runtime_resolution_confidence,
4582
+ "reason": args.runtime_resolution_reason,
4583
+ "fallbackFrom": args.runtime_resolution_fallback_from or None,
4584
+ "warnings": args.runtime_resolution_warning,
4585
+ }
4586
+ if args.runtime_resolution_catalog_hosts_present:
4587
+ payload["catalogHostIds"] = args.runtime_resolution_catalog_host_id
4588
+ return json.dumps(payload, ensure_ascii=False)
4589
+
4590
+
4489
4591
  def main(argv: list[str]) -> int:
4490
4592
  """Parse one public prepare invocation and emit its rendered bundle."""
4491
4593
  args = build_prepare_argument_parser().parse_args(argv)
@@ -4536,7 +4638,8 @@ def main(argv: list[str]) -> int:
4536
4638
  host_session_context = deserialize_host_session_context(
4537
4639
  args.host_session_context_json
4538
4640
  )
4539
- except ModelSelectionInputError as exc:
4641
+ runtime_resolution_json = _runtime_resolution_json_from_args(args)
4642
+ except (ModelSelectionInputError, PrepareError) as exc:
4540
4643
  print(str(exc), file=__import__("sys").stderr)
4541
4644
  return 1
4542
4645
  if not args.host_session_context_json:
@@ -4576,7 +4679,7 @@ def main(argv: list[str]) -> int:
4576
4679
  report_writer_model=args.report_writer_model,
4577
4680
  lead_runtime=args.lead_runtime,
4578
4681
  lead_runtime_request=args.lead_runtime_request,
4579
- runtime_resolution_json=args.runtime_resolution_json,
4682
+ runtime_resolution_json=runtime_resolution_json,
4580
4683
  executor=args.executor,
4581
4684
  critic=args.critic,
4582
4685
  related_tasks_raw=args.related_tasks_raw,
@@ -4644,17 +4747,17 @@ def _lead_launch_payload(ctx: dict[str, object]) -> dict[str, object]:
4644
4747
  project_root = Path(str(ctx["PROJECT_ROOT"]))
4645
4748
  run_manifest_path = Path(str(ctx["RUN_MANIFEST_PATH"]))
4646
4749
  try:
4647
- manifest = json.loads(run_manifest_path.read_text(encoding="utf-8"))
4750
+ manifest = load_owned_object(run_manifest_path, artifact="run manifest")
4648
4751
  resources = manifest["resources"]
4649
4752
  assignment = agent_model_assignment_from_payload(
4650
4753
  manifest["invocationAssignments"]["lead"]
4651
4754
  )
4652
4755
  prompt_file = project_root / resources["leadExecutionPromptPath"]
4653
4756
  metadata_path = project_root / resources["leadPromptMetadataPath"]
4654
- metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
4757
+ metadata = load_owned_object(metadata_path, artifact="lead prompt metadata")
4655
4758
  if metadata["prompt"]["path"] != resources["leadExecutionPromptPath"]:
4656
4759
  raise KeyError("lead prompt resource does not match metadata")
4657
- except (OSError, json.JSONDecodeError, KeyError, TypeError, AgentInvocationError) as exc:
4760
+ except (OSError, JsonBoundaryError, KeyError, TypeError, AgentInvocationError) as exc:
4658
4761
  raise PrepareError("lead invocation authority is incomplete") from exc
4659
4762
  errors = verify_agent_invocation(
4660
4763
  metadata_path,
@@ -19,6 +19,7 @@ from okstra_ctl.paths import okstra_home, resolve_under_root
19
19
  from okstra_ctl.reconcile import NON_TERMINAL_RECENT_STATUSES
20
20
  from okstra_ctl.task_target import project_rel
21
21
  from okstra_ctl.workflow import PHASE_SEQUENCE
22
+ from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
22
23
 
23
24
  # 불변식 이름은 여기서만 정의한다. `INVARIANTS` 와 방출부가 같은 상수를 봐야,
24
25
  # 이름을 하나 더할 때 한쪽만 고치고도 스위트가 green 인 상태가 생기지 않는다 —
@@ -74,8 +75,8 @@ _RECORDED_WORKER_STATUSES = frozenset(
74
75
 
75
76
  def _load_json(path: Path) -> dict:
76
77
  try:
77
- loaded = json.loads(path.read_text(encoding="utf-8"))
78
- except (OSError, json.JSONDecodeError):
78
+ loaded = load_owned_object(path, artifact="run audit input")
79
+ except JsonBoundaryError:
79
80
  return {}
80
81
  return loaded if isinstance(loaded, dict) else {}
81
82
 
@@ -26,6 +26,7 @@ from typing import Iterator, Optional
26
26
  from okstra_project.dirs import okstra_home
27
27
 
28
28
  from .path_hints import compact_run_context, hydrate_run_context
29
+ from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
29
30
  from .paths import compute_run_paths, task_runs_dir
30
31
 
31
32
 
@@ -66,8 +67,8 @@ def latest_run_inputs(
66
67
  return {}
67
68
  candidates.sort(key=lambda x: -x[0])
68
69
  try:
69
- data = json.loads(candidates[0][1].read_text(encoding="utf-8"))
70
- except (OSError, json.JSONDecodeError):
70
+ data = load_owned_object(candidates[0][1], artifact="run context")
71
+ except JsonBoundaryError:
71
72
  return {}
72
73
  inputs = data.get("inputs")
73
74
  return inputs if isinstance(inputs, dict) else {}
@@ -129,11 +130,7 @@ def consumers_mutex(plan_run_root: Path) -> Iterator[None]:
129
130
 
130
131
 
131
132
  def _atomic_write_json(path: Path, payload: dict) -> None:
132
- path.parent.mkdir(parents=True, exist_ok=True)
133
- tmp = path.with_suffix(path.suffix + ".tmp")
134
- tmp.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n",
135
- encoding="utf-8")
136
- os.replace(tmp, path)
133
+ write_owned_object_atomic(path, payload, artifact="run context")
137
134
 
138
135
 
139
136
  def _run_context_filename(task_type_segment: str, seq: str) -> str:
@@ -240,7 +237,7 @@ def read_run_context(run_manifests_dir: Path, task_type_segment: str,
240
237
  path = Path(run_manifests_dir) / _run_context_filename(task_type_segment, seq)
241
238
  if not path.is_file():
242
239
  return None
243
- payload = json.loads(path.read_text(encoding="utf-8"))
240
+ payload = load_owned_object(path, artifact="run context")
244
241
  context = hydrate_run_context(payload)
245
242
  analysis = payload.get("analysis")
246
243
  if isinstance(analysis, dict):
@@ -268,4 +265,4 @@ def read_run_inputs(run_manifests_dir: Path, task_type_segment: str,
268
265
  path = Path(run_manifests_dir) / _run_inputs_filename(task_type_segment, seq)
269
266
  if not path.is_file():
270
267
  return None
271
- return json.loads(path.read_text(encoding="utf-8"))
268
+ return load_owned_object(path, artifact="run inputs")
@@ -8,14 +8,12 @@ slim row 에 raw 로 보존한다 — taskGroup/taskId 와 동일한 이유다.
8
8
  """
9
9
  from __future__ import annotations
10
10
 
11
- import json
12
- import os
13
11
  from pathlib import Path
14
12
  from typing import Optional
15
13
 
16
14
  from .ids import build_run_id, parse_run_id
17
15
  from .invocation import invocation_path
18
- from .jsonl import read_jsonl
16
+ from .jsonl import read_jsonl, rewrite_jsonl
19
17
 
20
18
  DERIVED_FIELDS = ("taskType", "runSeq", "taskKey", "invocationFile")
21
19
 
@@ -103,11 +101,7 @@ def _rewrite_slim(path: Path) -> int:
103
101
  rows = read_jsonl(path)
104
102
  if not rows:
105
103
  return 0
106
- tmp = path.with_suffix(path.suffix + ".tmp")
107
- with tmp.open("w") as f:
108
- for r in rows:
109
- f.write(json.dumps(slim_run_row(r), separators=(",", ":")) + "\n")
110
- os.replace(tmp, path)
104
+ rewrite_jsonl(path, [slim_run_row(row) for row in rows])
111
105
  return len(rows)
112
106
 
113
107
 
@@ -9,6 +9,7 @@ from pathlib import Path
9
9
  from typing import Any
10
10
 
11
11
  from .md_table import is_separator_row, split_pipe_row
12
+ from .json_boundary import external_user_json_source, load_external_json
12
13
 
13
14
 
14
15
  _ROOT_FIELDS = {"schemaVersion", "tasks"}
@@ -404,8 +405,10 @@ def _strict_json_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
404
405
 
405
406
 
406
407
  def load_schedule_selection(path: Path) -> tuple[SelectionTask, ...]:
407
- payload = json.loads(
408
- path.read_text(encoding="utf-8"),
408
+ # 외부 입력: okstra-schedule 사용자가 선택한 작업 집합 문서다.
409
+ payload = load_external_json(
410
+ external_user_json_source(path),
411
+ artifact="schedule selection",
409
412
  object_pairs_hook=_strict_json_object,
410
413
  )
411
414
  root = _require_object(payload, "selection")
@@ -27,6 +27,8 @@ from collections.abc import Sequence
27
27
  from dataclasses import dataclass
28
28
  from pathlib import Path
29
29
 
30
+ from .json_boundary import JsonBoundaryError, load_owned_object
31
+
30
32
  from .final_report_schema import load_schema_version
31
33
  from .report_contract import TASK_TYPE_DATA_PROPERTY
32
34
 
@@ -160,8 +162,8 @@ def excerpt_contract_skew(
160
162
  if not installed:
161
163
  return None
162
164
  try:
163
- excerpt = json.loads(excerpt_path.read_text(encoding="utf-8"))
164
- except (OSError, json.JSONDecodeError):
165
+ excerpt = load_owned_object(excerpt_path, artifact="schema excerpt")
166
+ except JsonBoundaryError:
165
167
  return None
166
168
  if not isinstance(excerpt, dict):
167
169
  return None
@@ -50,13 +50,39 @@ class SessionTranscript:
50
50
  결론과 okstra 자신의 기록이 그렇다. 잘린 도구 에코는 세부를 잃지만
51
51
  잘린 결론은 사후 분석 전체를 잃는다.
52
52
  """
53
+ row = self._row(speaker, line)
54
+ self._show(row)
55
+ self._keep(row, capped=capped)
56
+
57
+ def write_event(
58
+ self, speaker: str, *, screen: list[str], archive: list[str]
59
+ ) -> None:
60
+ """한 이벤트의 두 투영을 각자의 목적지로 보낸다.
61
+
62
+ 화면은 요약을 받고 기록은 본문까지 받는다. 둘 중 하나를 골라 양쪽에
63
+ 쓰던 동안에는 pane 이 붙은 run 의 기록이 화면 투영으로 대체됐다 —
64
+ 도구 결과가 `← done (23 bytes)` 한 줄로만 남고 그 23 바이트가 무엇이
65
+ 었는지는 파일 어디에도 없었다. antigravity `view_file` 은 파일 내용이
66
+ 아니라 `721 lines, 114306 bytes` 같은 요약을 돌려주므로, 본문이 빠진
67
+ 기록에서는 46KB 를 읽은 호출과 잘린 호출이 같은 줄로 보인다.
68
+ """
69
+ for line in screen:
70
+ self._show(self._row(speaker, line))
71
+ for line in archive:
72
+ self._keep(self._row(speaker, line), capped=True)
73
+
74
+ def _row(self, speaker: str, line: str) -> str:
53
75
  # 패딩이 본문 앞 공백을 겸한다. 뒤에 공백을 한 칸 더 넣으면
54
76
  # `[worker:grok]`(13칸) 줄이 두 칸이 되고 `[okstra]` 정렬이 깨진다.
55
77
  label = f"[{speaker}]".ljust(_SPEAKER_WIDTH)
56
- row = f"{self._clock()} {label}{line}".rstrip()
78
+ return f"{self._clock()} {label}{line}".rstrip()
79
+
80
+ def _show(self, row: str) -> None:
57
81
  # 화면은 상한과 무관하다. 사람이 보고 있는 것을 잘라 낼 이유가 없다.
58
82
  if self._live:
59
83
  print(row, flush=True)
84
+
85
+ def _keep(self, row: str, *, capped: bool) -> None:
60
86
  if not capped:
61
87
  self._append(row)
62
88
  return