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
@@ -14,6 +14,7 @@ from dataclasses import dataclass
14
14
  from pathlib import Path
15
15
  from typing import Dict, List, Optional, Tuple
16
16
 
17
+ from .fixed_text import line
17
18
  from .worktree import _resolve_commit_sha
18
19
 
19
20
 
@@ -202,7 +203,7 @@ def guidance(*, plan_run_root: Path, project_id: str, task_group: str,
202
203
  f"--task-id {task_id} --work-category {work_category}")
203
204
  return ("Recorded stage SHAs no longer match the git history "
204
205
  "(external rebase/squash/amend?). Inspect and reconcile:\n"
205
- f" {base} --check --json\n"
206
+ f" {base} --check --text\n"
206
207
  f" {base} --apply --stage <N> --use-ref <branch|sha>")
207
208
 
208
209
 
@@ -272,9 +273,8 @@ def _items_as_json(items: List[StaleItem]) -> list:
272
273
  "suggested_commit": i.suggested_commit} for i in items]
273
274
 
274
275
 
275
- def main(argv: Optional[list] = None) -> int:
276
+ def _parse_args(argv: Optional[list]):
276
277
  import argparse
277
- import json as _json
278
278
 
279
279
  p = argparse.ArgumentParser(
280
280
  prog="okstra git-reconcile",
@@ -294,11 +294,44 @@ def main(argv: Optional[list] = None) -> int:
294
294
  p.add_argument("--use-ref", default="")
295
295
  p.add_argument("--reset-anchor", default="")
296
296
  p.add_argument("--json", action="store_true")
297
+ p.add_argument("--text", action="store_true")
297
298
  a = p.parse_args(argv)
298
299
  if a.check and a.apply:
299
300
  p.error("--check and --apply are mutually exclusive")
300
301
  if a.apply and a.stage is not None and not a.use_ref:
301
302
  p.error("--stage requires --use-ref (confirm 항목은 명시 ref 로만 보정된다)")
303
+ return a
304
+
305
+
306
+ def _emit_result(payload: dict, *, as_json: bool, as_text: bool) -> None:
307
+ import json as _json
308
+
309
+ print(
310
+ render_git_reconcile_text(payload)
311
+ if as_text
312
+ else _json.dumps(payload, ensure_ascii=False, indent=None if as_json else 2)
313
+ )
314
+
315
+
316
+ def render_git_reconcile_text(payload: dict) -> str:
317
+ """reconcile 결과의 승인된 분류만 고정 순서로 투영한다."""
318
+ rows = ["Okstra git reconcile\n"]
319
+ rows.append(line("Status", "ready" if payload.get("ok", True) else "error"))
320
+ items = payload.get("items") if isinstance(payload.get("items"), list) else []
321
+ rows.append(line("Item count", len(items)))
322
+ for index, item in enumerate(items, 1):
323
+ if not isinstance(item, dict):
324
+ continue
325
+ rows.append(line(f"Item {index} kind", item.get("kind")))
326
+ rows.append(line(f"Item {index} classification", item.get("classification")))
327
+ rows.append(line(f"Item {index} reason", item.get("reason")))
328
+ if "error" in payload:
329
+ rows.append(line("Failure reason", payload.get("error")))
330
+ return "".join(rows)
331
+
332
+
333
+ def main(argv: Optional[list] = None) -> int:
334
+ a = _parse_args(argv)
302
335
 
303
336
  kw = dict(project_root=Path(a.project_root).resolve(),
304
337
  plan_run_root=Path(a.plan_run_root).resolve(),
@@ -310,17 +343,16 @@ def main(argv: Optional[list] = None) -> int:
310
343
  reset_anchor=a.reset_anchor)
311
344
  out = {"applied": result["applied"],
312
345
  "remaining_confirm": _items_as_json(result["remaining_confirm"])}
313
- print(_json.dumps(out, ensure_ascii=False,
314
- indent=None if a.json else 2))
346
+ _emit_result(out, as_json=a.json, as_text=a.text)
315
347
  return 2 if result["remaining_confirm"] else 0
316
348
  items = classify_task(**kw)
317
349
  stale = [i for i in items if i.classification != "ok"]
318
- print(_json.dumps({"items": _items_as_json(stale)}, ensure_ascii=False,
319
- indent=None if a.json else 2))
350
+ out = {"items": _items_as_json(stale)}
351
+ _emit_result(out, as_json=a.json, as_text=a.text)
320
352
  return 2 if any(i.classification == "confirm" for i in stale) else 0
321
353
  except ReconcileError as exc:
322
- print(_json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False,
323
- indent=None if a.json else 2))
354
+ out = {"ok": False, "error": str(exc)}
355
+ _emit_result(out, as_json=a.json, as_text=a.text)
324
356
  return 1
325
357
 
326
358
 
@@ -15,6 +15,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
15
15
  from . import consumers, stage_targets, worktree_registry
16
16
  from .final_report_paths import final_report_markdown_path
17
17
  from .paths import RunRef
18
+ from .json_boundary import JsonBoundaryError, load_owned_object
18
19
  from .release_gate import (
19
20
  blocking_condition_ids,
20
21
  release_handoff_allowed,
@@ -75,8 +76,8 @@ def latest_whole_task_fv_release_ready(project_root, project_id: str,
75
76
  for dj in sorted(reports_dir.glob("final-report-*.data.json"),
76
77
  reverse=True):
77
78
  try:
78
- data = json.loads(dj.read_text(encoding="utf-8"))
79
- except (OSError, json.JSONDecodeError):
79
+ data = load_owned_object(dj, artifact="final verification report")
80
+ except JsonBoundaryError:
80
81
  continue
81
82
  if ((data.get("header") or {}).get("taskType") == "final-verification"
82
83
  and data.get("verificationScope") == "whole-task"
@@ -372,8 +373,8 @@ def record_verified(*, plan_run_root, stage: int, report_path: str,
372
373
  막는다. 통과 조건은 whole-task 와 같고(`release_gate`), 기록되는 verdict 는
373
374
  보고서가 실제로 실은 토큰이다."""
374
375
  try:
375
- data = json.loads(Path(data_json).read_text(encoding="utf-8"))
376
- except (OSError, json.JSONDecodeError) as exc:
376
+ data = load_owned_object(Path(data_json), artifact="final verification report")
377
+ except JsonBoundaryError as exc:
377
378
  raise HandoffError(f"cannot read final-report data.json: {exc}")
378
379
  if (data.get("header") or {}).get("taskType") != "final-verification":
379
380
  raise HandoffError("data.json is not a final-verification report")
@@ -11,6 +11,8 @@ import os
11
11
  from pathlib import Path
12
12
  from typing import Any, Callable
13
13
 
14
+ from .json_boundary import JsonBoundaryError, load_owned_object
15
+
14
16
  SUPPORTED_LANGS = ("en", "ko")
15
17
  DICTIONARY_REL = ("templates", "reports", "i18n")
16
18
  # The human HTML has its own fixed strings — table headings, empty states,
@@ -48,8 +50,8 @@ def load_dictionary(lang: str, rel: tuple[str, ...] = DICTIONARY_REL) -> dict[st
48
50
  )
49
51
  path = _i18n_dir(rel) / f"{lang}.json"
50
52
  try:
51
- return json.loads(path.read_text(encoding="utf-8"))
52
- except (OSError, json.JSONDecodeError) as exc:
53
+ return load_owned_object(path, artifact="i18n dictionary")
54
+ except JsonBoundaryError as exc:
53
55
  raise I18nError(f"failed to load {path}: {exc}") from exc
54
56
 
55
57
 
@@ -20,6 +20,11 @@ from .final_report_schema import (
20
20
  load_schema_for_data,
21
21
  validate as validate_schema,
22
22
  )
23
+ from .json_boundary import (
24
+ JsonBoundaryError,
25
+ load_owned_object_snapshot,
26
+ write_owned_object_atomic,
27
+ )
23
28
  from .report_views import normalize_direction_selection_identity
24
29
  from .scope_provenance import brief_end_state_id_sequence
25
30
  from .user_response import UserResponseError, parse_direction_selection
@@ -139,14 +144,14 @@ def _sibling_data_path(report: Path) -> Path:
139
144
  return data_path
140
145
 
141
146
 
142
- def _parse_data(data_path: Path, data_bytes: bytes) -> dict[str, Any]:
147
+ def _parse_data(data_path: Path) -> tuple[dict[str, Any], bytes]:
143
148
  try:
144
- data = json.loads(data_bytes)
145
- except (UnicodeError, json.JSONDecodeError) as exc:
149
+ snapshot = load_owned_object_snapshot(
150
+ data_path, artifact="implementation option data"
151
+ )
152
+ except JsonBoundaryError as exc:
146
153
  raise DirectionSelectionError(f"data JSON is invalid: {data_path}") from exc
147
- if not isinstance(data, dict):
148
- raise DirectionSelectionError("data JSON must contain an object")
149
- return data
154
+ return snapshot.value, snapshot.raw_bytes
150
155
 
151
156
 
152
157
  def _validate_report_identity(data: Mapping[str, Any], expected_task_key: str) -> None:
@@ -342,8 +347,7 @@ def resolve_selected_direction(
342
347
  task_root, seq = _selection_layout(report)
343
348
  report = validate_task_artifact_path(report, task_root, "report")
344
349
  data_path = validate_task_artifact_path(data_path, task_root, "data")
345
- data_bytes = data_path.read_bytes()
346
- data = _parse_data(data_path, data_bytes)
350
+ data, data_bytes = _parse_data(data_path)
347
351
  _validate_report_identity(data, expected_task_key)
348
352
  _validate_schema(data)
349
353
  selection = data.get("implementationOptionSelection")
@@ -406,9 +410,10 @@ def write_selected_direction_snapshot(
406
410
  "requirementCoverage": list(direction.requirement_coverage),
407
411
  "planningInvariants": list(direction.planning_invariants),
408
412
  }
409
- destination.write_text(
410
- json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
411
- encoding="utf-8",
413
+ write_owned_object_atomic(
414
+ destination,
415
+ payload,
416
+ artifact="selected implementation direction",
412
417
  )
413
418
  return destination
414
419
 
@@ -447,13 +452,16 @@ def _load_selected_direction_snapshot(
447
452
  task_root = path.parent.parent
448
453
  try:
449
454
  path = validate_task_artifact_path(path, task_root, "selected direction snapshot")
450
- content = path.read_bytes()
451
- data = json.loads(content)
455
+ loaded = load_owned_object_snapshot(
456
+ path, artifact="selected direction snapshot"
457
+ )
458
+ content = loaded.raw_bytes
459
+ data = loaded.value
452
460
  except DirectionSelectionError as exc:
453
461
  return None, [str(exc)]
454
462
  except OSError as exc:
455
463
  return None, [f"selected direction snapshot is unreadable: {exc}"]
456
- except (UnicodeError, json.JSONDecodeError) as exc:
464
+ except (UnicodeError, json.JSONDecodeError, JsonBoundaryError) as exc:
457
465
  return None, [f"selected direction snapshot JSON is invalid: {exc}"]
458
466
  if not isinstance(data, Mapping):
459
467
  return None, ["selected direction snapshot JSON must contain an object"]
@@ -15,6 +15,7 @@ from . import next_phase
15
15
  from .paths import RunRef, task_manifest_file
16
16
  from .stage_map import StageMapError, load_task_stage_map
17
17
  from .workflow import PHASE_SEQUENCE
18
+ from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
18
19
 
19
20
 
20
21
  @dataclass(frozen=True)
@@ -114,8 +115,8 @@ def _load_json(path: Path) -> dict[str, Any]:
114
115
  if not path.is_file():
115
116
  return {}
116
117
  try:
117
- data = json.loads(path.read_text(encoding="utf-8"))
118
- except json.JSONDecodeError:
118
+ data = load_owned_object(path, artifact="implementation outcome")
119
+ except JsonBoundaryError:
119
120
  return {}
120
121
  return data if isinstance(data, dict) else {}
121
122
 
@@ -224,8 +225,4 @@ def _update_task_catalog(project_root: Path, manifest: dict[str, Any]) -> bool:
224
225
 
225
226
 
226
227
  def _write_json(path: Path, payload: dict[str, Any]) -> None:
227
- path.parent.mkdir(parents=True, exist_ok=True)
228
- path.write_text(
229
- json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
230
- encoding="utf-8",
231
- )
228
+ write_owned_object_atomic(path, payload, artifact="implementation outcome")
@@ -12,6 +12,7 @@ from typing import Any, Mapping
12
12
  from .convergence_store import write_json_atomic
13
13
  from .final_report_schema import load_schema_version
14
14
  from .report_narrative import parse_narrative, writer_owned_data
15
+ from .json_boundary import load_owned_object
15
16
 
16
17
 
17
18
  PREP_VERDICT_RE = re.compile(r"^P-Prep-S([1-9][0-9]*)-")
@@ -384,7 +385,7 @@ def _load_data(path: str, *, option: str) -> dict:
384
385
  the documented `carry refused:` refusal it knows how to fall back from.
385
386
  """
386
387
  try:
387
- data = json.loads(Path(path).read_text(encoding="utf-8"))
388
+ data = load_owned_object(Path(path), artifact="incremental carry report")
388
389
  except (OSError, ValueError) as exc:
389
390
  raise CarryError(f"{option} could not be read: {exc}") from exc
390
391
  if not isinstance(data, dict):
@@ -18,6 +18,7 @@ from pathlib import Path
18
18
  from okstra_ctl.final_report_paths import final_report_data_path
19
19
  from okstra_ctl.stage_citations import cited_stage_numbers
20
20
  from okstra_ctl.stage_targets import downstream_stage_closure
21
+ from okstra_ctl.json_boundary import load_owned_object
21
22
 
22
23
  CUTOFF_RATIO = 0.5
23
24
 
@@ -275,7 +276,7 @@ def preview_link_availability_for_report(
275
276
  "reason": f"prior report has no data sibling at {data_path.name}",
276
277
  }
277
278
  try:
278
- data = json.loads(data_path.read_text(encoding="utf-8"))
279
+ data = load_owned_object(data_path, artifact="incremental scope report")
279
280
  except (OSError, ValueError) as exc:
280
281
  return {
281
282
  "wouldForceFull": True,
@@ -332,7 +333,9 @@ def _preview_result(args) -> dict:
332
333
  answer when the input cannot be read.
333
334
  """
334
335
  try:
335
- data = json.loads(Path(args.prev_data).read_text(encoding="utf-8"))
336
+ data = load_owned_object(
337
+ Path(args.prev_data), artifact="incremental scope report"
338
+ )
336
339
  answered = {
337
340
  token.strip()
338
341
  for token in args.answered_clarifications.split(",")
@@ -392,7 +395,9 @@ def main(argv: list[str]) -> int:
392
395
  return 0
393
396
 
394
397
  try:
395
- data = json.loads(Path(args.prev_data).read_text(encoding="utf-8"))
398
+ data = load_owned_object(
399
+ Path(args.prev_data), artifact="incremental scope report"
400
+ )
396
401
  stages = parse_stage_graph(data)
397
402
  impacted = {int(t.strip()) for t in args.impacted.split(",") if t.strip()}
398
403
  prep_ids = {t.strip() for t in args.prep_items.split(",") if t.strip()}
@@ -1,14 +1,17 @@
1
1
  """active/projects 인덱스 행 치환·기록."""
2
2
  from __future__ import annotations
3
3
 
4
- import json
5
4
  import os
6
5
  from pathlib import Path
7
6
 
8
7
  from .ids import build_run_id
9
8
  from .invocation import save_invocation
10
- from .jsonl import append_jsonl, read_jsonl, remove_jsonl_row, rotate_recent_if_needed
9
+ from .jsonl import (
10
+ append_jsonl, read_jsonl, remove_jsonl_row, rewrite_jsonl,
11
+ rotate_recent_if_needed,
12
+ )
11
13
  from .project_meta import upsert_project_meta
14
+ from .json_boundary import load_owned_object, write_owned_object_atomic
12
15
  from .run_index_row import build_run_index_row, slim_run_row
13
16
 
14
17
 
@@ -21,11 +24,7 @@ def _replace_or_append_row(path: Path, run_id: str, row: dict) -> None:
21
24
  if r.get("runId") == run_id:
22
25
  rows[i] = slim
23
26
  path.parent.mkdir(parents=True, exist_ok=True)
24
- tmp = path.with_suffix(path.suffix + ".tmp")
25
- with tmp.open("w") as f:
26
- for rr in rows:
27
- f.write(json.dumps(rr, separators=(",", ":")) + "\n")
28
- os.replace(tmp, path)
27
+ rewrite_jsonl(path, rows)
29
28
  return
30
29
  append_jsonl(path, slim)
31
30
 
@@ -156,9 +155,7 @@ def remove_reservation(home: Path, *, project_id: str, task_group: str,
156
155
  target = home / "projects" / project_id / "meta.json"
157
156
  if not target.is_file():
158
157
  return
159
- meta = json.loads(target.read_text())
158
+ meta = load_owned_object(target, artifact="project metadata")
160
159
  meta["runCount"] = max(0, int(meta.get("runCount", 0)) - 1)
161
160
  meta["activeCount"] = max(0, int(meta.get("activeCount", 0)) - 1)
162
- tmp = target.with_suffix(".json.tmp")
163
- tmp.write_text(json.dumps(meta, indent=2) + "\n")
164
- os.replace(tmp, target)
161
+ write_owned_object_atomic(target, meta, artifact="project metadata")
@@ -24,6 +24,13 @@ from .agent_invocation import (
24
24
  )
25
25
  from .path_hints import hydrate_active_run_context
26
26
  from .report_language import resolve_report_language
27
+ from .report_inputs import report_narrative_path, uses_report_contract_v3
28
+ from .report_synthesis_packet import (
29
+ ReportSynthesisPacketError,
30
+ materialize_report_synthesis_packet,
31
+ report_synthesis_packet_paths,
32
+ )
33
+ from .json_boundary import JsonBoundaryError, load_owned_object
27
34
  from .worker_prompt_body import (
28
35
  REPORT_WRITER_WORKER_ID,
29
36
  analysis_prompt_body,
@@ -444,7 +451,17 @@ def _render_prompt_parts(
444
451
  f"**Prompt Delivery Mode:** {context.request.delivery_mode.value}"
445
452
  )
446
453
  anchors.extend(_implementation_anchor_lines(context, item.plan))
447
- body = _prompt_body(context, state, worker, item.plan)
454
+ synthesis_packet_path = _materialize_report_writer_packet(
455
+ context,
456
+ item.plan,
457
+ )
458
+ body = _prompt_body(
459
+ context,
460
+ state,
461
+ worker,
462
+ item.plan,
463
+ synthesis_packet_path,
464
+ )
448
465
  body.extend(_resource_lines(context, item))
449
466
  except InitialPromptMaterializationError:
450
467
  raise
@@ -503,7 +520,7 @@ def _agent_invocation_request(
503
520
  f"requested model does not match {assignment_ref}",
504
521
  )
505
522
  anchors, body = _render_prompt_parts(context, item)
506
- source_paths = (
523
+ source_paths = [
507
524
  AgentInstructionSource(
508
525
  "project",
509
526
  _project_relative(context.project_root, context.request.run_manifest_path),
@@ -514,7 +531,10 @@ def _agent_invocation_request(
514
531
  AgentInstructionSource(
515
532
  "project", _required_string(context.manifest, "teamStatePath")
516
533
  ),
517
- )
534
+ ]
535
+ packet_source = _report_writer_packet_source(context, item.plan)
536
+ if packet_source is not None:
537
+ source_paths.append(packet_source)
518
538
  invocation_id = _worker_invocation_id(context.manifest, item.worker.worker_id)
519
539
  return AgentInvocationRequest(
520
540
  invocation_id=invocation_id,
@@ -526,7 +546,7 @@ def _agent_invocation_request(
526
546
  instruction=AgentInstruction(
527
547
  anchor_lines=tuple(anchors),
528
548
  body="\n".join(body).rstrip() + "\n",
529
- source_paths=source_paths,
549
+ source_paths=tuple(source_paths),
530
550
  ),
531
551
  project_root=context.project_root,
532
552
  run_manifest_path=context.request.run_manifest_path,
@@ -549,6 +569,7 @@ def _prompt_body(
549
569
  state: Mapping[str, Any],
550
570
  worker: InitialPromptWorkerRequest,
551
571
  plan: PromptPlan,
572
+ synthesis_packet_path: str = "",
552
573
  ) -> list[str]:
553
574
  if plan.audience == "report-writer":
554
575
  return report_writer_prompt_body(
@@ -561,6 +582,7 @@ def _prompt_body(
561
582
  context.manifest,
562
583
  context.active_context,
563
584
  ),
585
+ synthesis_packet_path,
564
586
  )
565
587
  return analysis_prompt_body(
566
588
  context.manifest,
@@ -571,6 +593,56 @@ def _prompt_body(
571
593
  )
572
594
 
573
595
 
596
+ def _report_writer_narrative_path(context: _MaterializationContext) -> Path:
597
+ return report_narrative_path(context.project_root, context.manifest)
598
+
599
+
600
+ def _report_writer_packet_source(
601
+ context: _MaterializationContext,
602
+ plan: PromptPlan,
603
+ ) -> AgentInstructionSource | None:
604
+ if (
605
+ plan.audience != "report-writer"
606
+ or not uses_report_contract_v3(context.manifest)
607
+ ):
608
+ return None
609
+ _, markdown_path = report_synthesis_packet_paths(
610
+ _report_writer_narrative_path(context)
611
+ )
612
+ return AgentInstructionSource(
613
+ "project",
614
+ _project_relative(context.project_root, markdown_path),
615
+ )
616
+
617
+
618
+ def _materialize_report_writer_packet(
619
+ context: _MaterializationContext,
620
+ plan: PromptPlan,
621
+ ) -> str:
622
+ packet_source = _report_writer_packet_source(context, plan)
623
+ if packet_source is None:
624
+ return ""
625
+ try:
626
+ _, markdown_path = materialize_report_synthesis_packet(
627
+ project_root=context.project_root,
628
+ manifest=context.manifest,
629
+ active_context=context.active_context,
630
+ team_state=context.team_state,
631
+ narrative_path=_report_writer_narrative_path(context),
632
+ )
633
+ except ReportSynthesisPacketError as exc:
634
+ defects = "; ".join(
635
+ f"owner={issue.owner} source={issue.label} path={issue.path} "
636
+ f"reason={issue.reason}"
637
+ for issue in exc.issues
638
+ )
639
+ raise InitialPromptMaterializationError(
640
+ "required_input_missing",
641
+ f"report synthesis packet contract defects: {defects}",
642
+ ) from exc
643
+ return _project_relative(context.project_root, markdown_path)
644
+
645
+
574
646
  def _resource_lines(
575
647
  context: _MaterializationContext,
576
648
  item: _PromptItem,
@@ -1325,12 +1397,12 @@ def _load_referenced_json(
1325
1397
 
1326
1398
  def _load_json_object(path: Path, label: str) -> Mapping[str, Any]:
1327
1399
  try:
1328
- payload = json.loads(path.read_text(encoding="utf-8"))
1329
- except (OSError, UnicodeError, json.JSONDecodeError) as exc:
1400
+ payload = load_owned_object(path, artifact=label)
1401
+ except JsonBoundaryError as exc:
1330
1402
  raise InitialPromptMaterializationError(
1331
1403
  "required_input_missing",
1332
1404
  f"cannot load {label}: {path}: {exc}",
1333
- ) from exc
1405
+ ) from (exc.__cause__ or exc)
1334
1406
  if not isinstance(payload, dict):
1335
1407
  raise InitialPromptMaterializationError(
1336
1408
  "required_input_missing",
@@ -7,6 +7,7 @@ from pathlib import Path
7
7
  from typing import Optional
8
8
 
9
9
  from .ids import _safe_fs_segment
10
+ from .json_boundary import load_owned_object, write_owned_object_atomic
10
11
 
11
12
 
12
13
  def invocation_path(home: Path, project_id: str,
@@ -32,11 +33,7 @@ def save_invocation(home: Path, project_id: str,
32
33
  run_seq: int, payload: dict) -> None:
33
34
  """invocation 을 원자적으로 저장(임시파일 + os.replace)."""
34
35
  target = invocation_path(home, project_id, task_group, task_id, task_type, run_seq)
35
- target.parent.mkdir(parents=True, exist_ok=True)
36
- tmp = target.with_suffix(".json.tmp")
37
- tmp.write_text(json.dumps(payload, indent=2) + "\n")
38
- os.chmod(tmp, 0o600)
39
- os.replace(tmp, target)
36
+ write_owned_object_atomic(target, payload, artifact="invocation metadata")
40
37
 
41
38
 
42
39
  def load_invocation(home: Path, project_id: str,
@@ -46,4 +43,4 @@ def load_invocation(home: Path, project_id: str,
46
43
  target = invocation_path(home, project_id, task_group, task_id, task_type, run_seq)
47
44
  if not target.is_file():
48
45
  return None
49
- return json.loads(target.read_text())
46
+ return load_owned_object(target, artifact="invocation metadata")