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
@@ -1,13 +1,13 @@
1
1
  """list/format/show."""
2
2
  from __future__ import annotations
3
3
 
4
- import json
5
4
  import re as _re
6
5
  from datetime import datetime, timedelta, timezone
7
6
  from pathlib import Path
8
7
  from typing import List, Optional
9
8
 
10
9
  from .paths import resolve_under_root
10
+ from .json_boundary import JsonBoundaryError, load_owned_object
11
11
  from .reconcile import _parse_iso
12
12
  from .run_index_row import read_run_index
13
13
 
@@ -24,8 +24,8 @@ def list_projects(home: Path, *, only_active: bool = False,
24
24
  if not meta_file.is_file():
25
25
  continue
26
26
  try:
27
- meta = json.loads(meta_file.read_text())
28
- except (OSError, json.JSONDecodeError):
27
+ meta = load_owned_object(meta_file, artifact="project metadata")
28
+ except JsonBoundaryError:
29
29
  # 동시 쓰기/이전 크래시로 깨진 meta.json 하나가 전체 listing 을
30
30
  # 죽이지 않도록 해당 프로젝트만 건너뛴다(다른 JSON 리더와 동일 정책).
31
31
  continue
@@ -13,12 +13,18 @@ import json
13
13
  import sys
14
14
  from pathlib import Path
15
15
 
16
+ from .fixed_text import line
17
+
16
18
  from okstra_project import resolve_project_root
19
+ from okstra_ctl.json_boundary import load_owned_object
17
20
 
18
21
 
19
22
  def _project_id(project_root: Path) -> str:
20
23
  try:
21
- return json.loads((project_root / ".okstra" / "project.json").read_text(encoding="utf-8")).get("projectId", "")
24
+ return load_owned_object(
25
+ project_root / ".okstra" / "project.json",
26
+ artifact="project configuration",
27
+ ).get("projectId", "")
22
28
  except (OSError, ValueError):
23
29
  return ""
24
30
 
@@ -120,6 +126,20 @@ def scan_logs(logs_root: Path, project_root: Path, top: int = 20) -> dict:
120
126
  }
121
127
 
122
128
 
129
+ def render_result_text(result: dict) -> str:
130
+ parts = ["# Okstra Log Inventory\n\n", line("Project root", result.get("projectRoot")), line("Logs root", result.get("logsRoot"))]
131
+ totals = result.get("totals") if isinstance(result.get("totals"), dict) else {}
132
+ for key, label in (("fileCount", "File count"), ("totalBytes", "Total bytes"), ("taskCount", "Task count"), ("promptBytes", "Prompt bytes"), ("transcriptBytes", "Transcript bytes"), ("pairedFileCount", "Paired file count")):
133
+ parts.append(line(label, totals.get(key)))
134
+ for row in result.get("topLargest", []):
135
+ if isinstance(row, dict):
136
+ parts.extend(("\n## Log\n\n", line("Task key", row.get("taskKey")), line("Phase", row.get("phase")), line("Worker", row.get("worker")), line("Sequence", row.get("seq")), line("Size bytes", row.get("sizeBytes")), line("Modified epoch", row.get("mtimeEpoch")), line("Path", row.get("path"))))
137
+ for row in result.get("perTask", []):
138
+ if isinstance(row, dict):
139
+ parts.extend(("\n## Task total\n\n", line("Task key", row.get("taskKey")), line("File count", row.get("fileCount")), line("Total bytes", row.get("totalBytes")), line("Oldest epoch", row.get("oldestEpoch")), line("Newest epoch", row.get("newestEpoch"))))
140
+ return "".join(parts)
141
+
142
+
123
143
  def main(argv: list[str] | None = None) -> int:
124
144
  parser = argparse.ArgumentParser(
125
145
  prog="okstra log-report",
@@ -128,12 +148,14 @@ def main(argv: list[str] | None = None) -> int:
128
148
  parser.add_argument("--cwd", default=".")
129
149
  parser.add_argument("--top", type=int, default=20, help="topLargest entry count")
130
150
  parser.add_argument("--json", action="store_true", help="emit JSON (always on)")
151
+ parser.add_argument("--text", action="store_true", help="emit fixed text fields")
131
152
  args = parser.parse_args(argv)
132
153
 
133
154
  project_root = resolve_project_root(explicit_root=args.project_root, cwd=args.cwd)
134
155
  result = {"ok": True, "projectRoot": str(project_root)}
135
156
  result.update(scan_logs(project_root / ".okstra" / "tasks", project_root, top=args.top))
136
- print(json.dumps(result, ensure_ascii=False, indent=2))
157
+ output = render_result_text(result) if args.text else json.dumps(result, ensure_ascii=False, indent=2)
158
+ print(output, end="" if args.text else "\n")
137
159
  return 0
138
160
 
139
161
 
@@ -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: