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
@@ -0,0 +1,39 @@
1
+ """모델용 고정 텍스트 투영의 scalar 줄 형식을 공유한다."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+
6
+
7
+ def scalar(value: object) -> str:
8
+ """복합 값과 Markdown 줄 삽입을 노출하지 않는 scalar 문자열이다."""
9
+ if value is None or value == "" or not isinstance(value, (str, int, float, bool)):
10
+ return "-"
11
+ text = str(value).replace("`", "\\`").replace("\n", " ").replace("\r", " ")
12
+ return re.sub(
13
+ r"[\x00-\x09\x0b\x0c\x0e-\x1f\x7f-\x9f\u2028\u2029]",
14
+ lambda match: f"\\u{ord(match.group(0)):04X}",
15
+ text,
16
+ )
17
+
18
+
19
+ def line(label: str, value: object) -> str:
20
+ """한 scalar를 Markdown 목록 한 줄로 렌더한다."""
21
+ if not re.fullmatch(r"[A-Za-z][A-Za-z0-9 ()/.-]*", label):
22
+ raise ValueError("fixed text label must be a static single-line label")
23
+ return f"- {label}: `{scalar(value)}`\n"
24
+
25
+
26
+ def value_lines(label: str, value: object) -> list[str]:
27
+ """JSON 모양의 복합 값을 이름/순서/값 고정 줄로 손실 없이 펼친다."""
28
+ if isinstance(value, dict):
29
+ rows = [line(f"{label} field count", len(value))]
30
+ for index, (name, child) in enumerate(value.items(), 1):
31
+ rows.append(line(f"{label} field {index} name", name))
32
+ rows.extend(value_lines(f"{label} field {index} value", child))
33
+ return rows
34
+ if isinstance(value, list):
35
+ rows = [line(f"{label} item count", len(value))]
36
+ for index, child in enumerate(value, 1):
37
+ rows.extend(value_lines(f"{label} item {index}", child))
38
+ return rows
39
+ return [line(label, value)]
@@ -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")
@@ -7,9 +7,16 @@ import json
7
7
  import re
8
8
  import sys
9
9
  from pathlib import Path
10
+ from typing import Any, Mapping
11
+
12
+ from .convergence_store import write_json_atomic
13
+ from .final_report_schema import load_schema_version
14
+ from .report_narrative import parse_narrative, writer_owned_data
15
+ from .json_boundary import load_owned_object
10
16
 
11
17
 
12
18
  PREP_VERDICT_RE = re.compile(r"^P-Prep-S([1-9][0-9]*)-")
19
+ STEP_VERDICT_RE = re.compile(r"^P-Step-([1-9][0-9]*)\.")
13
20
 
14
21
 
15
22
  class CarryError(Exception):
@@ -290,6 +297,78 @@ def merge_carried_forward(
290
297
  return cur
291
298
 
292
299
 
300
+ def _plan_item_stage(item: Mapping[str, Any]) -> int | None:
301
+ item_id = str(item.get("id") or "")
302
+ for pattern in (PREP_VERDICT_RE, STEP_VERDICT_RE):
303
+ match = pattern.match(item_id)
304
+ if match:
305
+ return int(match.group(1))
306
+ return None
307
+
308
+
309
+ def _writer_stage_rows(data: Mapping[str, Any], *, snapshot: str) -> dict[int, dict]:
310
+ planning = _planning(dict(writer_owned_data(data)))
311
+ return _stage_rows(planning, snapshot=snapshot)
312
+
313
+
314
+ def merge_v3_plan_state(
315
+ prev: dict[str, Any],
316
+ narrative: dict[str, Any],
317
+ state: dict[str, Any],
318
+ *,
319
+ prev_seq: str,
320
+ carry_stages: set[int],
321
+ reverify_stages: set[int],
322
+ ) -> dict[str, Any]:
323
+ """이전 판정 중 이월 스테이지 소유분만 v3 수렴 상태에 복사한다."""
324
+ prev_stages = _writer_stage_rows(prev, snapshot="prior")
325
+ cur_stages = _stage_rows(_planning(narrative), snapshot="current")
326
+ _validate_stage_sets(prev_stages, cur_stages, carry_stages, reverify_stages)
327
+ for stage in sorted(carry_stages):
328
+ current = cur_stages.get(stage)
329
+ if current is None:
330
+ raise CarryError(f"carry stage {stage} is missing from current narrative")
331
+ if _canonical(prev_stages[stage]) != _canonical(current):
332
+ raise CarryError(f"carry stage {stage} changed in current narrative")
333
+
334
+ prev_items = _indexed_rows(
335
+ _planning(prev).get("planBodyVerification", {}).get("planItems", []),
336
+ key="id",
337
+ label="prior planItems",
338
+ )
339
+ state_items = _indexed_rows(
340
+ state.get("planBodyVerification", {}).get("planItems", []),
341
+ key="id",
342
+ label="current state planItems",
343
+ )
344
+ for item_id, current in state_items.items():
345
+ stage = _plan_item_stage(current)
346
+ if stage not in carry_stages:
347
+ continue
348
+ previous = prev_items.get(item_id)
349
+ if previous is None:
350
+ raise CarryError(f"carry-owned plan item {item_id} is missing from prior report")
351
+ for field in ("subject", "sourceSection"):
352
+ if current.get(field) != previous.get(field):
353
+ raise CarryError(f"carry-owned plan item {item_id} changed {field}")
354
+ carried = copy.deepcopy(previous)
355
+ carried["carriedForwardFromSeq"] = str(prev_seq)
356
+ state_items[item_id] = carried
357
+
358
+ prior_carried_ids = {
359
+ item_id
360
+ for item_id, item in prev_items.items()
361
+ if _plan_item_stage(item) in carry_stages
362
+ }
363
+ missing = sorted(prior_carried_ids - set(state_items))
364
+ if missing:
365
+ raise CarryError(f"current state omitted carry-owned plan items: {missing}")
366
+ state["planBodyVerification"]["planItems"] = [
367
+ state_items[item_id] for item_id in sorted(state_items)
368
+ ]
369
+ return state
370
+
371
+
293
372
  def _parse_stage_csv(value: str | None, *, option: str) -> set[int] | None:
294
373
  if value is None:
295
374
  return None
@@ -306,7 +385,7 @@ def _load_data(path: str, *, option: str) -> dict:
306
385
  the documented `carry refused:` refusal it knows how to fall back from.
307
386
  """
308
387
  try:
309
- data = json.loads(Path(path).read_text(encoding="utf-8"))
388
+ data = load_owned_object(Path(path), artifact="incremental carry report")
310
389
  except (OSError, ValueError) as exc:
311
390
  raise CarryError(f"{option} could not be read: {exc}") from exc
312
391
  if not isinstance(data, dict):
@@ -317,33 +396,67 @@ def _load_data(path: str, *, option: str) -> dict:
317
396
  def main(argv: list[str]) -> int:
318
397
  ap = argparse.ArgumentParser(prog="okstra incremental-carry")
319
398
  ap.add_argument("--prev-data", required=True, help="prior run final-report data.json")
320
- ap.add_argument("--cur-data", required=True, help="current incremental re-run data.json")
399
+ current = ap.add_mutually_exclusive_group(required=True)
400
+ current.add_argument("--cur-data", help="historical v2 current data.json")
401
+ current.add_argument("--cur-narrative", help="contract v3 current narrative Markdown")
402
+ ap.add_argument("--state", help="contract v3 convergence-owned plan state")
321
403
  ap.add_argument("--prev-seq", required=True, help="prior run seq, tagged onto carried items")
322
404
  ap.add_argument("--carry-stages", default=None, help="comma-separated carried stage numbers")
323
405
  ap.add_argument("--reverify-stages", default=None, help="comma-separated reverified stage numbers")
324
- ap.add_argument("--out", required=True, help="path to write the merged data.json")
406
+ ap.add_argument("--out", help="historical v2 merged data.json path")
407
+ ap.add_argument("--out-state", help="contract v3 merged plan state path")
325
408
  args = ap.parse_args(argv)
326
409
 
327
410
  try:
328
411
  prev = _load_data(args.prev_data, option="--prev-data")
329
- cur = _load_data(args.cur_data, option="--cur-data")
330
- merged = merge_carried_forward(
331
- prev,
332
- cur,
333
- prev_seq=args.prev_seq,
334
- carry_stages=_parse_stage_csv(args.carry_stages, option="--carry-stages"),
335
- reverify_stages=_parse_stage_csv(
336
- args.reverify_stages, option="--reverify-stages",
337
- ),
412
+ carry_stages = _parse_stage_csv(args.carry_stages, option="--carry-stages")
413
+ reverify_stages = _parse_stage_csv(
414
+ args.reverify_stages, option="--reverify-stages",
338
415
  )
339
- except CarryError as exc:
416
+ if args.cur_narrative:
417
+ if not args.state or not args.out_state or args.out:
418
+ raise CarryError(
419
+ "--cur-narrative requires --state and --out-state, and forbids --out"
420
+ )
421
+ if carry_stages is None or reverify_stages is None:
422
+ raise CarryError("contract v3 carry requires both stage sets")
423
+ try:
424
+ narrative = parse_narrative(
425
+ Path(args.cur_narrative).read_text(encoding="utf-8"),
426
+ load_schema_version("3.0"),
427
+ )
428
+ except (OSError, UnicodeError, ValueError) as exc:
429
+ raise CarryError(f"--cur-narrative could not be read: {exc}") from exc
430
+ state = _load_data(args.state, option="--state")
431
+ merged = merge_v3_plan_state(
432
+ prev,
433
+ narrative,
434
+ state,
435
+ prev_seq=args.prev_seq,
436
+ carry_stages=carry_stages,
437
+ reverify_stages=reverify_stages,
438
+ )
439
+ output = Path(args.out_state)
440
+ else:
441
+ if not args.out or args.out_state or args.state:
442
+ raise CarryError(
443
+ "--cur-data requires --out, and forbids --state and --out-state"
444
+ )
445
+ cur = _load_data(args.cur_data, option="--cur-data")
446
+ merged = merge_carried_forward(
447
+ prev,
448
+ cur,
449
+ prev_seq=args.prev_seq,
450
+ carry_stages=carry_stages,
451
+ reverify_stages=reverify_stages,
452
+ )
453
+ output = Path(args.out)
454
+ except (CarryError, KeyError, TypeError) as exc:
340
455
  print(f"carry refused: {exc}", file=sys.stderr)
341
456
  return 1
342
457
 
343
458
  try:
344
- Path(args.out).write_text(
345
- json.dumps(merged, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
346
- )
459
+ write_json_atomic(output, merged)
347
460
  except OSError as exc:
348
461
  print(f"carry refused: --out could not be written: {exc}", file=sys.stderr)
349
462
  return 1
@@ -354,7 +467,7 @@ def main(argv: list[str]) -> int:
354
467
  .get("planItems", [])
355
468
  if item.get("carriedForwardFromSeq") == str(args.prev_seq)
356
469
  )
357
- print(f"merged {carried} carried-forward plan item(s) from seq {args.prev_seq} -> {args.out}")
470
+ print(f"merged {carried} carried-forward plan item(s) from seq {args.prev_seq} -> {output}")
358
471
  return 0
359
472
 
360
473
 
@@ -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
 
@@ -207,7 +208,10 @@ def stages_for_clarification(data: dict, clarification_id: str) -> set[int]:
207
208
  )
208
209
  stages = _stages_blocked_on(planning.get("requirementCoverage"), clarification_id)
209
210
  for item in plan_items if isinstance(plan_items, list) else []:
210
- if isinstance(item, dict) and item.get("clarificationId") == clarification_id:
211
+ if isinstance(item, dict) and (
212
+ item.get("clarificationId") == clarification_id
213
+ or clarification_id in (item.get("clarificationRefs") or [])
214
+ ):
211
215
  stages |= _plan_item_stages(item)
212
216
  return stages
213
217
 
@@ -272,7 +276,7 @@ def preview_link_availability_for_report(
272
276
  "reason": f"prior report has no data sibling at {data_path.name}",
273
277
  }
274
278
  try:
275
- data = json.loads(data_path.read_text(encoding="utf-8"))
279
+ data = load_owned_object(data_path, artifact="incremental scope report")
276
280
  except (OSError, ValueError) as exc:
277
281
  return {
278
282
  "wouldForceFull": True,
@@ -329,7 +333,9 @@ def _preview_result(args) -> dict:
329
333
  answer when the input cannot be read.
330
334
  """
331
335
  try:
332
- 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
+ )
333
339
  answered = {
334
340
  token.strip()
335
341
  for token in args.answered_clarifications.split(",")
@@ -389,7 +395,9 @@ def main(argv: list[str]) -> int:
389
395
  return 0
390
396
 
391
397
  try:
392
- 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
+ )
393
401
  stages = parse_stage_graph(data)
394
402
  impacted = {int(t.strip()) for t in args.impacted.split(",") if t.strip()}
395
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")