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
@@ -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
@@ -32,7 +33,11 @@ from okstra_project import project_json_path, upsert_project_json
32
33
  from okstra_project.state import slugify
33
34
  from . import fix_cycles
34
35
  from .analysis_packet import build_analysis_packet
35
- from .stage_ledger import build_stage_ledger, render_stage_ledger
36
+ from .stage_ledger import (
37
+ build_stage_ledger,
38
+ render_stage_ledger,
39
+ stage_ledger_notice,
40
+ )
36
41
  from .analysis_inputs import (
37
42
  ANALYSIS_TASK_TYPES,
38
43
  AnalysisInputError,
@@ -109,7 +114,10 @@ from .legacy_model_selection import (
109
114
  serialize_host_session_context,
110
115
  )
111
116
  from .model_defaults import ModelDefaultScopes
112
- from .worker_prompt_policy import ANALYSIS_DUTY_BY_TASK_TYPE
117
+ from .worker_prompt_policy import (
118
+ ANALYSIS_DUTY_BY_TASK_TYPE,
119
+ critic_assignment_ref,
120
+ )
113
121
  from .models import (
114
122
  UnknownProviderError,
115
123
  default_model,
@@ -137,6 +145,11 @@ from .agent_invocation import (
137
145
  verify_agent_invocation,
138
146
  )
139
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
+ )
140
153
  from .path_resolve import relative_to_project_root, resolve_user_file
141
154
  from .render import (
142
155
  apply_lead_prompt_defaults,
@@ -250,8 +263,8 @@ def _load_final_report_data_if_present(report_path: Path) -> tuple[Path, dict] |
250
263
  if not data_path.is_file():
251
264
  return None
252
265
  try:
253
- data = json.loads(data_path.read_text(encoding="utf-8"))
254
- except json.JSONDecodeError as exc:
266
+ data = load_owned_object(data_path, artifact="approved plan final report")
267
+ except JsonBoundaryError as exc:
255
268
  raise PrepareError(
256
269
  f"approved plan data.json is invalid JSON: {data_path}: {exc}"
257
270
  ) from exc
@@ -664,7 +677,6 @@ def _record_start(
664
677
  """record_start hook 호출. okstra-central.sh 의 bash wrapper 와 같은 동작
665
678
  이지만 python 직접 호출이라 환경 변수 의존 없음.
666
679
  """
667
- import json as _json
668
680
  from datetime import datetime, timezone
669
681
  from okstra_ctl import record_start
670
682
  from .locks import central_lock
@@ -679,11 +691,15 @@ def _record_start(
679
691
  (home / f).touch(exist_ok=True)
680
692
  state_file = home / "state.json"
681
693
  if not state_file.exists():
682
- state_file.write_text(_json.dumps({
683
- "schemaVersion": "1",
684
- "createdAt": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
685
- "backfilledAt": None,
686
- }, 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
+ )
687
703
  os.chmod(state_file, 0o600)
688
704
  # 중앙 인덱스 락은 reconcile.py 와 공유하는 central_lock 단일 기준점을 쓴다.
689
705
  # 직접 flock 을 손으로 깔면 락 파일/모드가 두 경로에서 갈라질 수 있다.
@@ -1526,8 +1542,8 @@ def validate_project_qa_commands(task_type: str, project_root: Path) -> None:
1526
1542
  if not project_json.is_file():
1527
1543
  return
1528
1544
  try:
1529
- project_meta = json.loads(project_json.read_text())
1530
- except (OSError, json.JSONDecodeError) as exc:
1545
+ project_meta = load_owned_object(project_json, artifact="project config")
1546
+ except (OSError, JsonBoundaryError) as exc:
1531
1547
  raise PrepareError(
1532
1548
  f"project.json read failed at {project_json}: {exc}"
1533
1549
  ) from exc
@@ -1562,11 +1578,13 @@ def _apply_qa_waiver_if_requested(inp: "PrepareInputs", project_root: Path) -> N
1562
1578
  ):
1563
1579
  if not manifest_path.is_file():
1564
1580
  raise PrepareError(f"--qa-waiver: conformance manifest not found at {manifest_path}")
1565
- manifest = json.loads(manifest_path.read_text())
1581
+ manifest = load_owned_object(manifest_path, artifact="conformance manifest")
1566
1582
  when = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
1567
1583
  if not apply_qa_waiver(manifest, stage_key, reason, at=when):
1568
1584
  raise PrepareError(f"--qa-waiver: stageKey {stage_key!r} not in manifest {manifest_path}")
1569
- 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
+ )
1570
1588
 
1571
1589
 
1572
1590
  def _clear_stale_stage_waiver(inp: "PrepareInputs", project_root: Path, stage: int) -> None:
@@ -1584,7 +1602,7 @@ def _clear_stale_stage_waiver(inp: "PrepareInputs", project_root: Path, stage: i
1584
1602
  )
1585
1603
  if not manifest_path.is_file():
1586
1604
  return
1587
- manifest = json.loads(manifest_path.read_text())
1605
+ manifest = load_owned_object(manifest_path, artifact="conformance manifest")
1588
1606
  entries = manifest.get("entries") if isinstance(manifest, dict) else None
1589
1607
  if not isinstance(entries, list):
1590
1608
  return
@@ -1604,8 +1622,8 @@ def _clear_stale_stage_waiver(inp: "PrepareInputs", project_root: Path, stage: i
1604
1622
  if parsed is not None and parsed[0] == stage_key:
1605
1623
  return # user intentionally waived this stage for this run
1606
1624
  if clear_qa_waiver(manifest, stage_key):
1607
- manifest_path.write_text(
1608
- json.dumps(manifest, indent=2, ensure_ascii=False) + "\n"
1625
+ write_owned_object_atomic(
1626
+ manifest_path, manifest, artifact="conformance manifest"
1609
1627
  )
1610
1628
 
1611
1629
 
@@ -2117,8 +2135,8 @@ def _read_model_defaults(path: Path) -> dict[str, tuple[str, ...]]:
2117
2135
  if not path.is_file():
2118
2136
  return {}
2119
2137
  try:
2120
- payload = json.loads(path.read_text(encoding="utf-8"))
2121
- except (OSError, json.JSONDecodeError) as exc:
2138
+ payload = load_owned_object(path, artifact="model defaults config")
2139
+ except (OSError, JsonBoundaryError) as exc:
2122
2140
  raise PrepareError(f"model defaults file is invalid: {path}: {exc}") from exc
2123
2141
  raw_defaults = payload.get("modelDefaults", {}) if isinstance(payload, dict) else {}
2124
2142
  if not isinstance(raw_defaults, dict):
@@ -2504,6 +2522,7 @@ def _resolve_model_bindings(
2504
2522
  worker_assignments,
2505
2523
  critic_assignment,
2506
2524
  translator_assignment,
2525
+ inp.task_type,
2507
2526
  )
2508
2527
  worker_by_provider = {
2509
2528
  row.provider: row for row in worker_assignments
@@ -2653,6 +2672,7 @@ def _project_model_bindings(
2653
2672
  workers,
2654
2673
  critic_assignment,
2655
2674
  _role_assignment(resolved=translator, role="translator"),
2675
+ inp.task_type,
2656
2676
  )
2657
2677
  worker_by_provider = {row.provider: row for row in workers}
2658
2678
  return _ModelBindings(
@@ -2892,6 +2912,7 @@ def _build_invocation_assignments(
2892
2912
  workers: tuple[RoleAssignment, ...],
2893
2913
  critic: RoleAssignment | None,
2894
2914
  translator: RoleAssignment,
2915
+ task_type: str,
2895
2916
  ) -> dict[str, dict[str, object]]:
2896
2917
  assignments = {"lead": lead.to_model_payload()}
2897
2918
  for assignment in workers:
@@ -2902,8 +2923,11 @@ def _build_invocation_assignments(
2902
2923
  assignment.to_model_payload()
2903
2924
  )
2904
2925
  if critic is not None:
2905
- assignments["critic/scope"] = critic.to_model_payload()
2906
- assignments["critic/acceptance"] = critic.to_model_payload()
2926
+ # 한 run 이 도달할 수 있는 critic 한 종류다. 둘 다 등록하면 실행될
2927
+ # 없는 로스터 행이 남고, `_optional_worker_roles` 가 provider 로만
2928
+ # role 라벨을 만들기 때문에 두 행의 이름이 겹쳐 validate-run 이
2929
+ # `duplicate worker role detected` 로 런을 실패시킨다.
2930
+ assignments[critic_assignment_ref(task_type)] = critic.to_model_payload()
2907
2931
  assignments["translator"] = translator.to_model_payload()
2908
2932
  return assignments
2909
2933
 
@@ -3218,6 +3242,12 @@ def _write_instruction_set_sources(
3218
3242
  render_reference_expectations(
3219
3243
  str(inp.brief_path), str(instruction_set / "reference-expectations.md"), ctx,
3220
3244
  )
3245
+ # 계획 phase 만 원장을 받는다. 원장은 "무엇이 이미 지어졌나" 를 계획 저작
3246
+ # 쪽에 알려 주는 것이므로, 계획을 쓰지 않는 phase 에는 실을 자리가 없다.
3247
+ stage_ledger = (
3248
+ build_stage_ledger(Path(ctx["TASK_MANIFEST_PATH"]).parent)
3249
+ if inp.task_type == "implementation-planning" else None
3250
+ )
3221
3251
  packet = build_analysis_packet(
3222
3252
  task_key=ctx["TASK_KEY"],
3223
3253
  task_type=ctx["TASK_TYPE"],
@@ -3232,11 +3262,8 @@ def _write_instruction_set_sources(
3232
3262
  instruction_set_relative_path=ctx["INSTRUCTION_SET_RELATIVE_PATH"],
3233
3263
  fix_history_text=fix_cycles.packet_summary(
3234
3264
  fix_cycles.read_rows(Path(ctx["TASK_MANIFEST_PATH"]).parent)),
3235
- stage_ledger_json=(
3236
- render_stage_ledger(
3237
- build_stage_ledger(Path(ctx["TASK_MANIFEST_PATH"]).parent))
3238
- if inp.task_type == "implementation-planning" else ""
3239
- ),
3265
+ stage_ledger_json=render_stage_ledger(stage_ledger),
3266
+ stage_ledger_notice=stage_ledger_notice(stage_ledger),
3240
3267
  )
3241
3268
  if inp.task_type in ANALYSIS_TASK_TYPES:
3242
3269
  packet += (
@@ -3311,9 +3338,10 @@ def _render_lead_prompt_and_snapshot(
3311
3338
  inp.task_type,
3312
3339
  installed_version(),
3313
3340
  )
3314
- Path(ctx["FINAL_REPORT_SCHEMA_PATH"]).write_text(
3315
- json.dumps(_excerpt, indent=2, ensure_ascii=False) + "\n",
3316
- encoding="utf-8",
3341
+ write_owned_object_atomic(
3342
+ Path(ctx["FINAL_REPORT_SCHEMA_PATH"]),
3343
+ _excerpt,
3344
+ artifact="final report schema excerpt",
3317
3345
  )
3318
3346
  except Exception: # noqa: BLE001 — advisory artifact; never fail prep over it
3319
3347
  pass
@@ -3323,7 +3351,9 @@ def _render_lead_prompt_and_snapshot(
3323
3351
  json.loads(ctx["INVOCATION_ASSIGNMENTS_JSON"])["lead"]
3324
3352
  )
3325
3353
  identity = invocation_execution_identity_from_manifest(
3326
- 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
+ ),
3327
3357
  assignment=assignment,
3328
3358
  assignment_ref="lead",
3329
3359
  duty_id="lead",
@@ -3476,8 +3506,8 @@ def _read_existing_manifest(manifest_path: Path) -> dict:
3476
3506
  if not manifest_path.exists():
3477
3507
  return {}
3478
3508
  try:
3479
- return json.loads(manifest_path.read_text(encoding="utf-8"))
3480
- except (OSError, json.JSONDecodeError):
3509
+ return load_owned_object(manifest_path, artifact="run manifest")
3510
+ except (OSError, JsonBoundaryError):
3481
3511
  return {}
3482
3512
 
3483
3513
 
@@ -3971,7 +4001,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
3971
4001
  try:
3972
4002
  json.loads(runtime_resolution_json or "{}")
3973
4003
  except json.JSONDecodeError as exc:
3974
- raise PrepareError(f"invalid --runtime-resolution-json: {exc}") from exc
4004
+ raise PrepareError(f"invalid runtime resolution data: {exc}") from exc
3975
4005
  # Probed once here and reused below, so the gate and the manifest cannot
3976
4006
  # disagree about which backend this run is on.
3977
4007
  terminal_backend = detect_terminal_backend()
@@ -4286,11 +4316,77 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
4286
4316
  )
4287
4317
 
4288
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
+
4289
4387
  def build_prepare_argument_parser():
4290
4388
  """Build the shared flat-argument parser used by public prepare shims."""
4291
- import argparse
4292
-
4293
- p = argparse.ArgumentParser()
4389
+ p = ArgumentParser()
4294
4390
  p.add_argument("--workspace-root", required=True)
4295
4391
  p.add_argument("--project-root", required=True)
4296
4392
  p.add_argument("--project-id", required=True)
@@ -4332,30 +4428,7 @@ def build_prepare_argument_parser():
4332
4428
  p.add_argument("--worker-model", default="", dest="worker_models_raw")
4333
4429
  p.add_argument("--report-writer-provider", default="")
4334
4430
  p.add_argument("--report-writer-model", default="")
4335
- p.add_argument(
4336
- "--lead-runtime",
4337
- default="claude-code",
4338
- choices=list(default_host_registry().ids()),
4339
- dest="lead_runtime",
4340
- help=(
4341
- "Lead runtime adapter. Default: claude-code. `codex` renders "
4342
- "Codex CLI artifact bundles; `antigravity` renders Antigravity CLI "
4343
- "artifact bundles; `external` renders neutral bootstrap bundles for "
4344
- "manifest inspection until a dispatch backend is enabled."
4345
- ),
4346
- )
4347
- p.add_argument(
4348
- "--lead-runtime-request",
4349
- default="",
4350
- dest="lead_runtime_request",
4351
- help="Original lead runtime request before Node-side resolution, e.g. auto.",
4352
- )
4353
- p.add_argument(
4354
- "--runtime-resolution-json",
4355
- default="",
4356
- dest="runtime_resolution_json",
4357
- help="Node-side runtime resolution payload serialized as JSON.",
4358
- )
4431
+ _add_runtime_resolution_arguments(p)
4359
4432
  p.add_argument("--executor", default="")
4360
4433
  p.add_argument("--critic", default="")
4361
4434
  p.add_argument("--related-tasks", default="", dest="related_tasks_raw")
@@ -4470,6 +4543,51 @@ def build_prepare_argument_parser():
4470
4543
  return p
4471
4544
 
4472
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
+
4473
4591
  def main(argv: list[str]) -> int:
4474
4592
  """Parse one public prepare invocation and emit its rendered bundle."""
4475
4593
  args = build_prepare_argument_parser().parse_args(argv)
@@ -4520,7 +4638,8 @@ def main(argv: list[str]) -> int:
4520
4638
  host_session_context = deserialize_host_session_context(
4521
4639
  args.host_session_context_json
4522
4640
  )
4523
- except ModelSelectionInputError as exc:
4641
+ runtime_resolution_json = _runtime_resolution_json_from_args(args)
4642
+ except (ModelSelectionInputError, PrepareError) as exc:
4524
4643
  print(str(exc), file=__import__("sys").stderr)
4525
4644
  return 1
4526
4645
  if not args.host_session_context_json:
@@ -4560,7 +4679,7 @@ def main(argv: list[str]) -> int:
4560
4679
  report_writer_model=args.report_writer_model,
4561
4680
  lead_runtime=args.lead_runtime,
4562
4681
  lead_runtime_request=args.lead_runtime_request,
4563
- runtime_resolution_json=args.runtime_resolution_json,
4682
+ runtime_resolution_json=runtime_resolution_json,
4564
4683
  executor=args.executor,
4565
4684
  critic=args.critic,
4566
4685
  related_tasks_raw=args.related_tasks_raw,
@@ -4628,17 +4747,17 @@ def _lead_launch_payload(ctx: dict[str, object]) -> dict[str, object]:
4628
4747
  project_root = Path(str(ctx["PROJECT_ROOT"]))
4629
4748
  run_manifest_path = Path(str(ctx["RUN_MANIFEST_PATH"]))
4630
4749
  try:
4631
- manifest = json.loads(run_manifest_path.read_text(encoding="utf-8"))
4750
+ manifest = load_owned_object(run_manifest_path, artifact="run manifest")
4632
4751
  resources = manifest["resources"]
4633
4752
  assignment = agent_model_assignment_from_payload(
4634
4753
  manifest["invocationAssignments"]["lead"]
4635
4754
  )
4636
4755
  prompt_file = project_root / resources["leadExecutionPromptPath"]
4637
4756
  metadata_path = project_root / resources["leadPromptMetadataPath"]
4638
- metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
4757
+ metadata = load_owned_object(metadata_path, artifact="lead prompt metadata")
4639
4758
  if metadata["prompt"]["path"] != resources["leadExecutionPromptPath"]:
4640
4759
  raise KeyError("lead prompt resource does not match metadata")
4641
- except (OSError, json.JSONDecodeError, KeyError, TypeError, AgentInvocationError) as exc:
4760
+ except (OSError, JsonBoundaryError, KeyError, TypeError, AgentInvocationError) as exc:
4642
4761
  raise PrepareError("lead invocation authority is incomplete") from exc
4643
4762
  errors = verify_agent_invocation(
4644
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")
@@ -1,11 +1,11 @@
1
- """Build a task-type-scoped excerpt of the final-report schema.
1
+ """Build a task-type-scoped excerpt of a versioned final-report schema.
2
2
 
3
- The full schema (``schemas/final-report-v2.0.schema.json``) carries the
3
+ The full schema carries the
4
4
  deliverable property blocks for ALL task-types (``errorAnalysis``, the three
5
5
  read-only analysis blocks, ``implementationPlanning``, ``releaseHandoff``,
6
6
  ``implementation``, and ``finalVerification``) plus a
7
7
  ``$defs`` library (~38% of the file) shared across them. A single run only
8
- authors ONE task-type's data.json, so the report-writer worker only needs
8
+ authors ONE task-type's narrative, so the report-writer worker only needs
9
9
  the common structure + its own task-type's block + the ``$defs`` those
10
10
  reach.
11
11
 
@@ -16,7 +16,7 @@ repo/installed schema (whose `schemas/...` path is not even resolvable
16
16
  from inside a consumer project's task bundle).
17
17
 
18
18
  The excerpt is ADVISORY — a reading aid for the author. Validation always
19
- runs against the FULL schema via ``final_report_schema.load_schema()``, so
19
+ runs against the matching FULL schema, so
20
20
  the excerpt never gates correctness; it only trims what the worker reads.
21
21
  """
22
22
  from __future__ import annotations
@@ -27,8 +27,10 @@ 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
- from .report_contract import CURRENT_REPORT_SCHEMA_VERSION, TASK_TYPE_DATA_PROPERTY
33
+ from .report_contract import TASK_TYPE_DATA_PROPERTY
32
34
 
33
35
 
34
36
  _ALL_PER_TYPE_PROPERTIES = frozenset(TASK_TYPE_DATA_PROPERTY.values())
@@ -160,17 +162,22 @@ 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
168
170
  cut_from = excerpt_cut_from_version(excerpt)
169
171
  if not cut_from or cut_from == installed:
170
172
  return None
173
+ schema_version = (
174
+ excerpt.get("properties", {})
175
+ .get("schemaVersion", {})
176
+ .get("const", "2.0")
177
+ )
171
178
  try:
172
179
  fresh = build_schema_excerpt(
173
- load_schema_version(CURRENT_REPORT_SCHEMA_VERSION), task_type, installed
180
+ load_schema_version(str(schema_version)), task_type, installed
174
181
  )
175
182
  except Exception: # noqa: BLE001 — an unloadable schema proves no drift
176
183
  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