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
@@ -10,6 +10,7 @@ from pathlib import Path
10
10
  from okstra_project.dirs import okstra_home
11
11
 
12
12
  from .manager_launch import build_launch_packet
13
+ from .fixed_text import line, value_lines
13
14
  from .manager_store import (
14
15
  ManagerError,
15
16
  append_directive,
@@ -23,11 +24,90 @@ from .manager_store import (
23
24
  from .manager_sync import status_task, sync_task
24
25
 
25
26
 
26
- def _emit(payload: dict | list[dict], as_json: bool) -> None:
27
+ def render_manager_text(command: str, payload: dict | list[dict]) -> str:
28
+ """manager 모델 표면에 승인된 식별자와 상태만 투영한다."""
29
+ allowed = {"init", "discover-projects", "new-project", "new-task-group", "new-task",
30
+ "task-assign", "task-note", "task-sync", "task-status", "task-run"}
31
+ if command not in allowed:
32
+ raise ValueError("unknown manager text purpose")
33
+ rows = [f"Okstra manager {command}\n", line("Status", "ready")]
34
+ if isinstance(payload, list):
35
+ rows.append(line("Result count", len(payload)))
36
+ rows.extend(_render_manager_projects(payload))
37
+ return "".join(rows)
38
+ if command == "task-status":
39
+ rows.extend(_render_manager_status(payload))
40
+ rows.extend(value_lines("Status details", payload))
41
+ return "".join(rows)
42
+ for label, key in (("Manager ID", "managerId"), ("Schema version", "schemaVersion"),
43
+ ("Task key", "taskKey"), ("Project ID", "projectId"),
44
+ ("Task group", "taskGroup"), ("Task ID", "taskId"),
45
+ ("Work status", "workStatus"), ("Run command", "command"),
46
+ ("Backend", "backend"),
47
+ ("Worker dispatch backend", "workerDispatchBackend"),
48
+ ("Project root", "projectRoot"), ("Context path", "contextPath")):
49
+ if key in payload:
50
+ rows.append(line(label, payload.get(key)))
51
+ run_args = payload.get("runArgs")
52
+ if isinstance(run_args, list):
53
+ for index, value in enumerate(run_args, 1):
54
+ rows.append(line(f"Run arg {index}", value))
55
+ structures = {
56
+ "new-project": (("Projects", "projects"),),
57
+ "new-task": (("Manifest", "manifest"), ("Children", "children")),
58
+ "task-assign": (("Children", "children"),),
59
+ "task-sync": (("Children", "children"),),
60
+ }
61
+ for label, key in structures.get(command, ()):
62
+ rows.extend(value_lines(label, payload.get(key)))
63
+ if command == "task-note":
64
+ for label, key in (("Scope", "scope"), ("Directive project ID", "projectId"),
65
+ ("Directive body", "body"), ("Created at", "createdAt"),
66
+ ("Source", "source")):
67
+ rows.append(line(label, payload.get(key)))
68
+ if command == "task-sync" and "syncedAt" in payload:
69
+ rows.append(line("Synced at", payload.get("syncedAt")))
70
+ return "".join(rows)
71
+
72
+
73
+ def _render_manager_projects(projects: list[dict]) -> list[str]:
74
+ rows: list[str] = []
75
+ for index, project in enumerate(projects, 1):
76
+ if not isinstance(project, dict):
77
+ continue
78
+ for label, key in (("ID", "projectId"), ("root", "projectRoot"),
79
+ ("run count", "runCount"), ("active count", "activeCount"),
80
+ ("last run at", "lastRunAt")):
81
+ rows.append(line(f"Project {index} {label}", project.get(key)))
82
+ return rows
83
+
84
+
85
+ def _render_manager_status(payload: dict) -> list[str]:
86
+ rows: list[str] = []
87
+ manifest = payload.get("manifest") if isinstance(payload.get("manifest"), dict) else {}
88
+ for label, key in (("Manager ID", "managerId"), ("Task group", "taskGroup"),
89
+ ("Task ID", "taskId"), ("Progress mode", "progressMode")):
90
+ rows.append(line(label, manifest.get(key)))
91
+ summary = payload.get("summary") if isinstance(payload.get("summary"), dict) else {}
92
+ for key in ("planned", "missing", "running", "done", "blocked"):
93
+ rows.append(line(f"Summary {key}", summary.get(key)))
94
+ children = payload.get("children") if isinstance(payload.get("children"), list) else []
95
+ for index, child in enumerate(children, 1):
96
+ if not isinstance(child, dict):
97
+ continue
98
+ for label, key in (("task key", "taskKey"), ("run status", "latestRunStatus"),
99
+ ("current phase", "currentPhase"), ("work status", "workStatus"),
100
+ ("report path", "latestReportRecordPath"), ("error", "error")):
101
+ if key in child:
102
+ rows.append(line(f"Child {index} {label}", child.get(key)))
103
+ return rows
104
+
105
+
106
+ def _emit(command: str, payload: dict | list[dict], as_json: bool) -> None:
27
107
  if as_json:
28
108
  print(json.dumps(payload, ensure_ascii=False, indent=2))
29
109
  return
30
- print(json.dumps(payload, ensure_ascii=False))
110
+ print(render_manager_text(command, payload), end="")
31
111
 
32
112
 
33
113
  def _normalize_global_flags(argv: list[str] | None) -> list[str]:
@@ -142,11 +222,12 @@ def main(argv: list[str] | None = None) -> int:
142
222
  home = okstra_home()
143
223
  try:
144
224
  if args.command == "init":
145
- _emit(init_manager(home, args.manager_id), args.json)
225
+ _emit("init", init_manager(home, args.manager_id), args.json)
146
226
  elif args.command == "discover-projects":
147
- _emit(discover_projects(home), args.json)
227
+ _emit("discover-projects", discover_projects(home), args.json)
148
228
  elif args.command == "new" and args.new_command == "project":
149
229
  _emit(
230
+ "new-project",
150
231
  link_project(
151
232
  home,
152
233
  args.manager_id,
@@ -158,9 +239,10 @@ def main(argv: list[str] | None = None) -> int:
158
239
  args.json,
159
240
  )
160
241
  elif args.command == "new" and args.new_command == "task-group":
161
- _emit(create_task_group(home, args.manager_id, args.task_group), args.json)
242
+ _emit("new-task-group", create_task_group(home, args.manager_id, args.task_group), args.json)
162
243
  elif args.command == "new" and args.new_command == "task":
163
244
  _emit(
245
+ "new-task",
164
246
  create_task(
165
247
  home,
166
248
  args.manager_id,
@@ -175,6 +257,7 @@ def main(argv: list[str] | None = None) -> int:
175
257
  )
176
258
  elif args.command == "task" and args.task_command == "assign":
177
259
  _emit(
260
+ "task-assign",
178
261
  assign_child(
179
262
  home,
180
263
  args.manager_id,
@@ -190,6 +273,7 @@ def main(argv: list[str] | None = None) -> int:
190
273
  )
191
274
  elif args.command == "task" and args.task_command == "note":
192
275
  _emit(
276
+ "task-note",
193
277
  append_directive(
194
278
  home,
195
279
  args.manager_id,
@@ -202,11 +286,12 @@ def main(argv: list[str] | None = None) -> int:
202
286
  args.json,
203
287
  )
204
288
  elif args.command == "task" and args.task_command == "sync":
205
- _emit(sync_task(home, args.manager_id, args.task_group, args.task_id), args.json)
289
+ _emit("task-sync", sync_task(home, args.manager_id, args.task_group, args.task_id), args.json)
206
290
  elif args.command == "task" and args.task_command == "status":
207
- _emit(status_task(home, args.manager_id, args.task_group, args.task_id), args.json)
291
+ _emit("task-status", status_task(home, args.manager_id, args.task_group, args.task_id), args.json)
208
292
  elif args.command == "task" and args.task_command == "run":
209
293
  _emit(
294
+ "task-run",
210
295
  build_launch_packet(
211
296
  home,
212
297
  args.manager_id,
@@ -9,6 +9,11 @@ from pathlib import Path
9
9
  from okstra_project import project_json_path, upsert_project_json
10
10
 
11
11
  from okstra_ctl.jsonl import append_jsonl
12
+ from okstra_ctl.json_boundary import (
13
+ JsonBoundaryError,
14
+ load_owned_object,
15
+ write_owned_object_atomic,
16
+ )
12
17
 
13
18
  from .manager_paths import (
14
19
  ManagerPathError,
@@ -32,18 +37,15 @@ def _now_iso() -> str:
32
37
 
33
38
 
34
39
  def _write_json(path: Path, payload: dict) -> None:
35
- path.parent.mkdir(parents=True, exist_ok=True)
36
- tmp = path.with_suffix(path.suffix + ".tmp")
37
- tmp.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
38
- os.replace(tmp, path)
40
+ write_owned_object_atomic(path, payload, artifact="manager state")
39
41
 
40
42
 
41
43
  def _read_json(path: Path) -> dict:
42
44
  if not path.is_file():
43
45
  raise ManagerError(f"manager file not found: {path}")
44
46
  try:
45
- data = json.loads(path.read_text(encoding="utf-8"))
46
- except json.JSONDecodeError as exc:
47
+ data = load_owned_object(path, artifact="manager state")
48
+ except JsonBoundaryError as exc:
47
49
  raise ManagerError(f"failed to parse {path}: {exc}") from exc
48
50
  if not isinstance(data, dict):
49
51
  raise ManagerError(f"expected JSON object in {path}")
@@ -101,8 +103,8 @@ def discover_projects(home: Path) -> list[dict]:
101
103
  items: list[dict] = []
102
104
  for meta in sorted(base.glob("*/meta.json")):
103
105
  try:
104
- data = json.loads(meta.read_text(encoding="utf-8"))
105
- except (OSError, json.JSONDecodeError):
106
+ data = load_owned_object(meta, artifact="project metadata")
107
+ except JsonBoundaryError:
106
108
  continue
107
109
  if isinstance(data, dict):
108
110
  items.append(
@@ -134,8 +136,8 @@ def link_project(
134
136
  project_json = project_json_path(root)
135
137
  if project_json.is_file():
136
138
  try:
137
- existing = json.loads(project_json.read_text(encoding="utf-8"))
138
- except json.JSONDecodeError as exc:
139
+ existing = load_owned_object(project_json, artifact="project metadata")
140
+ except JsonBoundaryError as exc:
139
141
  raise ManagerError(f"failed to parse {project_json}: {exc}") from exc
140
142
  existing_id = str(existing.get("projectId") or "")
141
143
  if existing_id != project_id:
@@ -11,6 +11,8 @@ import json
11
11
  from pathlib import Path
12
12
  from typing import Optional
13
13
 
14
+ from .json_boundary import load_owned_object
15
+
14
16
 
15
17
  def build_analysis_material(brief_path: Path, directive: str = "") -> str:
16
18
  """`analysis-material.md` 본문 문자열을 돌려준다."""
@@ -35,7 +37,9 @@ def resolve_related_tasks(
35
37
  existing: list[str] = []
36
38
  if task_manifest_path and Path(task_manifest_path).exists():
37
39
  try:
38
- manifest = json.loads(Path(task_manifest_path).read_text(encoding="utf-8"))
40
+ manifest = load_owned_object(
41
+ Path(task_manifest_path), artifact="task manifest"
42
+ )
39
43
  existing = manifest.get("relatedTasks") or []
40
44
  if not isinstance(existing, list):
41
45
  existing = []
@@ -24,6 +24,11 @@ from pathlib import Path
24
24
  from typing import Optional
25
25
 
26
26
  from okstra_ctl.worktree import is_git_work_tree
27
+ from okstra_ctl.json_boundary import (
28
+ JsonBoundaryError,
29
+ load_owned_object,
30
+ write_owned_object_atomic,
31
+ )
27
32
  # 모듈 import 인 이유: 이 파일의 공개 함수들이 `okstra_home` 이라는 파라미터
28
33
  # 이름을 쓰고 있어, 같은 이름의 함수를 직접 import 하면 가려진다.
29
34
  from okstra_project import dirs as project_dirs
@@ -305,9 +310,10 @@ def _scan_registries(project_root: Path, *, okstra_home: Path) -> list[dict]:
305
310
  registry = okstra_home / "worktrees" / "registry.json"
306
311
  if registry.is_file():
307
312
  try:
308
- text = registry.read_text(encoding="utf-8")
309
- except OSError:
310
- text = ""
313
+ data = load_owned_object(registry, artifact="worktree registry")
314
+ except JsonBoundaryError:
315
+ data = {}
316
+ text = json.dumps(data, ensure_ascii=False)
311
317
  if LEGACY_OKSTRA_DIR_NAME in text and _mentions_project_path(text, project_str):
312
318
  updates.append({"file": str(registry), "rowCount": text.count(LEGACY_OKSTRA_DIR_NAME), "scope": "worktrees-registry"})
313
319
  return updates
@@ -342,47 +348,45 @@ def _apply_registry_updates(plan: MigrationPlan, *, okstra_home: Path) -> int:
342
348
  registry = okstra_home / "worktrees" / "registry.json"
343
349
  if registry.is_file():
344
350
  try:
345
- text = registry.read_text(encoding="utf-8")
346
- except OSError:
347
- text = ""
351
+ data = load_owned_object(registry, artifact="worktree registry")
352
+ except JsonBoundaryError:
353
+ data = {}
348
354
  # Only mutate when this project's root appears anywhere in the file —
349
355
  # otherwise leave it alone (covers the case where registry.json
350
356
  # contains an unrelated `.project-docs/okstra` literal in someone
351
357
  # else's row).
358
+ text = json.dumps(data, ensure_ascii=False)
352
359
  if _mentions_project_path(text, project_str) and LEGACY_OKSTRA_DIR_NAME in text:
353
- new_text = _replace_in_project_rows(text, project_str)
354
- if new_text != text:
355
- registry.write_text(new_text, encoding="utf-8")
356
- total += text.count(LEGACY_OKSTRA_DIR_NAME) - new_text.count(
357
- LEGACY_OKSTRA_DIR_NAME
360
+ updated, changed = _replace_in_project_rows(data, project_str)
361
+ if changed:
362
+ write_owned_object_atomic(
363
+ registry, updated, artifact="worktree registry"
358
364
  )
365
+ total += changed
359
366
  return total
360
367
 
361
368
 
362
- def _replace_in_project_rows(text: str, project_str: str) -> str:
369
+ def _replace_in_project_rows(
370
+ data: dict[str, object], project_str: str
371
+ ) -> tuple[dict[str, object], int]:
363
372
  """For registry.json: replace `.project-docs/okstra` with `.okstra` only
364
373
  inside rows that mention `project_str`. registry.json is structured JSON
365
374
  (top-level object with task-key → row), so we operate at the JSON level
366
375
  to avoid clobbering unrelated rows.
367
376
  """
368
- try:
369
- data = json.loads(text)
370
- except json.JSONDecodeError:
371
- return text
372
- if not isinstance(data, dict):
373
- return text
374
- changed = False
377
+ updated = dict(data)
378
+ changed = 0
375
379
  for key, row in data.items():
376
- row_text = json.dumps(row)
380
+ row_text = json.dumps(row, ensure_ascii=False)
377
381
  if not _mentions_project_path(row_text, project_str) or LEGACY_OKSTRA_DIR_NAME not in row_text:
378
382
  continue
379
383
  new_row_text = row_text.replace(LEGACY_OKSTRA_DIR_NAME, OKSTRA_DIR_NAME)
380
384
  if new_row_text != row_text:
381
- data[key] = json.loads(new_row_text)
382
- changed = True
383
- if not changed:
384
- return text
385
- return json.dumps(data, indent=2, ensure_ascii=False) + "\n"
385
+ updated[key] = json.loads(new_row_text)
386
+ changed += row_text.count(LEGACY_OKSTRA_DIR_NAME) - new_row_text.count(
387
+ LEGACY_OKSTRA_DIR_NAME
388
+ )
389
+ return updated, changed
386
390
 
387
391
 
388
392
  def main(argv: Optional[list[str]] = None) -> int:
@@ -17,6 +17,7 @@ from .model_pool import ModelPool, ModelPoolValidationError
17
17
  from .ports.host_model import HostModelBindingError, HostModelBindingRequest
18
18
  from .registry.host_registry import default_host_registry
19
19
  from .registry.provider_registry import default_provider_registry
20
+ from .json_boundary import load_owned_object, write_owned_object_atomic
20
21
 
21
22
 
22
23
  class ModelCliError(ValueError):
@@ -226,24 +227,11 @@ def _config_path(scope: str, cwd: Path) -> Path:
226
227
  def _read_json(path: Path) -> dict[str, Any]:
227
228
  if not path.is_file():
228
229
  return {}
229
- payload = json.loads(path.read_text(encoding="utf-8"))
230
- return payload if isinstance(payload, dict) else {}
230
+ return load_owned_object(path, artifact="model configuration")
231
231
 
232
232
 
233
233
  def _write_json_atomic(path: Path, payload: dict[str, Any]) -> None:
234
- path.parent.mkdir(parents=True, exist_ok=True)
235
- handle = tempfile.NamedTemporaryFile(
236
- "w", encoding="utf-8", delete=False, dir=path.parent
237
- )
238
- try:
239
- json.dump(payload, handle, ensure_ascii=False, indent=2)
240
- handle.write("\n")
241
- handle.close()
242
- os.replace(handle.name, path)
243
- except Exception:
244
- handle.close()
245
- Path(handle.name).unlink(missing_ok=True)
246
- raise
234
+ write_owned_object_atomic(path, payload, artifact="model configuration")
247
235
 
248
236
 
249
237
  def main(argv: list[str] | None = None) -> int: