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
@@ -85,6 +85,7 @@ from .self_mock_signals import (
85
85
  partition_waived_entries,
86
86
  selfmock_path_key,
87
87
  )
88
+ from .json_boundary import JsonBoundaryError, external_tool_json_source, load_external_json
88
89
 
89
90
  # `survived` is read by a human in the sidecar, so it is trimmed. The verdict and
90
91
  # the logged total are taken before the trim — the cap shortens the report, never
@@ -414,8 +415,12 @@ class StrykerAdapter:
414
415
  if not path.is_file():
415
416
  return None
416
417
  try:
417
- return json.loads(path.read_text(encoding="utf-8"))
418
- except (OSError, json.JSONDecodeError):
418
+ # 외부 입력: mutation testing 도구가 생성한 실행 보고서다.
419
+ return load_external_json(
420
+ external_tool_json_source(path, Path(worktree) if worktree else Path.cwd()),
421
+ artifact="mutation test report",
422
+ )
423
+ except JsonBoundaryError:
419
424
  return None
420
425
 
421
426
 
@@ -1004,8 +1009,12 @@ class CargoMutantsAdapter:
1004
1009
  if not path.is_file():
1005
1010
  return None
1006
1011
  try:
1007
- data = json.loads(path.read_text(encoding="utf-8"))
1008
- except (OSError, json.JSONDecodeError):
1012
+ # 외부 입력: mutation testing 도구가 생성한 실행 보고서다.
1013
+ data = load_external_json(
1014
+ external_tool_json_source(path, Path(worktree) if worktree else Path.cwd()),
1015
+ artifact="mutation test report",
1016
+ )
1017
+ except JsonBoundaryError:
1009
1018
  return None
1010
1019
  return data if isinstance(data, dict) else None
1011
1020
 
@@ -18,6 +18,7 @@ import sys
18
18
  from pathlib import Path
19
19
 
20
20
  from .dispatch_state import NON_TERMINAL_WORKER_STATUSES
21
+ from .json_boundary import JsonBoundaryError, load_owned_object
21
22
 
22
23
 
23
24
  def _newest_team_state(state_dir: Path) -> Path | None:
@@ -33,8 +34,8 @@ def _newest_team_state(state_dir: Path) -> Path | None:
33
34
 
34
35
  def _has_live_dispatch(team_state_path: Path) -> bool:
35
36
  try:
36
- payload = json.loads(team_state_path.read_text(encoding="utf-8"))
37
- except (OSError, json.JSONDecodeError):
37
+ payload = load_owned_object(team_state_path, artifact="team state")
38
+ except JsonBoundaryError:
38
39
  return False
39
40
  if not isinstance(payload, dict):
40
41
  return False
@@ -439,6 +439,10 @@ def _run_files(
439
439
  "codex_worker_result": worker_results / f"codex-worker{suffixes['worker_results']}.md",
440
440
  "antigravity_worker_result": worker_results / f"antigravity-worker{suffixes['worker_results']}.md",
441
441
  "report_writer_worker_result": worker_results / f"report-writer-worker{suffixes['worker_results']}.md",
442
+ "report_writer_narrative": worker_results / f"report-writer-narrative{suffixes['worker_results']}.md",
443
+ "approval_decisions": run_state / f"approval-decisions-{task_type_segment}-{sequences['state']}.json",
444
+ "design_preparation": run_state / f"design-preparation-{task_type_segment}-{sequences['state']}.json",
445
+ "plan_body_verification": run_state / f"plan-body-verification-{task_type_segment}-{sequences['state']}.json",
442
446
  "run_errors_log": run_logs / f"errors-{task_type_segment}-{sequences['state']}.jsonl",
443
447
  "claude_worker_errors_sidecar": worker_results / f"claude-worker-errors{suffixes['worker_results']}.json",
444
448
  "codex_worker_errors_sidecar": worker_results / f"codex-worker-errors{suffixes['worker_results']}.json",
@@ -570,6 +574,10 @@ def _absolute_worker_fields(paths: Mapping[str, Any]) -> dict[str, str]:
570
574
  "CODEX_WORKER_RESULT_FILE": str(paths["codex_worker_result"]),
571
575
  "ANTIGRAVITY_WORKER_RESULT_FILE": str(paths["antigravity_worker_result"]),
572
576
  "REPORT_WRITER_WORKER_RESULT_FILE": str(paths["report_writer_worker_result"]),
577
+ "REPORT_WRITER_NARRATIVE_FILE": str(paths["report_writer_narrative"]),
578
+ "APPROVAL_DECISIONS_PATH": str(paths["approval_decisions"]),
579
+ "DESIGN_PREPARATION_PATH": str(paths["design_preparation"]),
580
+ "PLAN_BODY_VERIFICATION_STATE_PATH": str(paths["plan_body_verification"]),
573
581
  "CLAUDE_WORKER_ERRORS_SIDECAR_PATH": str(paths["claude_worker_errors_sidecar"]),
574
582
  "CODEX_WORKER_ERRORS_SIDECAR_PATH": str(paths["codex_worker_errors_sidecar"]),
575
583
  "ANTIGRAVITY_WORKER_ERRORS_SIDECAR_PATH": str(paths["antigravity_worker_errors_sidecar"]),
@@ -670,6 +678,10 @@ def _relative_file_fields(project_root: Path, paths: Mapping[str, Any]) -> dict[
670
678
  "CODEX_WORKER_RESULT_RELATIVE_PATH": _rel(project_root, paths["codex_worker_result"]),
671
679
  "ANTIGRAVITY_WORKER_RESULT_RELATIVE_PATH": _rel(project_root, paths["antigravity_worker_result"]),
672
680
  "REPORT_WRITER_WORKER_RESULT_RELATIVE_PATH": _rel(project_root, paths["report_writer_worker_result"]),
681
+ "REPORT_WRITER_NARRATIVE_RELATIVE_PATH": _rel(project_root, paths["report_writer_narrative"]),
682
+ "APPROVAL_DECISIONS_RELATIVE_PATH": _rel(project_root, paths["approval_decisions"]),
683
+ "DESIGN_PREPARATION_RELATIVE_PATH": _rel(project_root, paths["design_preparation"]),
684
+ "PLAN_BODY_VERIFICATION_STATE_RELATIVE_PATH": _rel(project_root, paths["plan_body_verification"]),
673
685
  "RUN_ERRORS_LOG_RELATIVE_PATH": _rel(project_root, paths["run_errors_log"]),
674
686
  "CLAUDE_WORKER_ERRORS_SIDECAR_RELATIVE_PATH": _rel(project_root, paths["claude_worker_errors_sidecar"]),
675
687
  "CODEX_WORKER_ERRORS_SIDECAR_RELATIVE_PATH": _rel(project_root, paths["codex_worker_errors_sidecar"]),
@@ -552,6 +552,9 @@ def compute_run_paths(
552
552
  team_state = run_state / f"team-state{suffixes['state']}.json"
553
553
  active_run_context = run_state / f"active-run-context{suffixes['state']}.json"
554
554
  lead_events = run_state / f"lead-events-{task_type_segment}-{seqs['state']}.jsonl"
555
+ convergence_state = (
556
+ run_state / f"convergence-{task_type_segment}-{seqs['state']}.json"
557
+ )
555
558
  final_report_template = instruction_set / "final-report-template.md"
556
559
  final_report_schema = instruction_set / "final-report-schema.json"
557
560
  reference_expectations = instruction_set / "reference-expectations.md"
@@ -562,6 +565,10 @@ def compute_run_paths(
562
565
  codex_worker_result = worker_results / f"codex-worker{suffixes['worker_results']}.md"
563
566
  antigravity_worker_result = worker_results / f"antigravity-worker{suffixes['worker_results']}.md"
564
567
  report_writer_worker_result = worker_results / f"report-writer-worker{suffixes['worker_results']}.md"
568
+ report_writer_narrative = worker_results / f"report-writer-narrative{suffixes['worker_results']}.md"
569
+ approval_decisions = run_state / f"approval-decisions-{task_type_segment}-{seqs['state']}.json"
570
+ design_preparation = run_state / f"design-preparation-{task_type_segment}-{seqs['state']}.json"
571
+ plan_body_verification = run_state / f"plan-body-verification-{task_type_segment}-{seqs['state']}.json"
565
572
 
566
573
  run_errors_log = run_logs / f"errors-{task_type_segment}-{seqs['state']}.jsonl"
567
574
  runtime_home = okstra_home()
@@ -667,6 +674,11 @@ def compute_run_paths(
667
674
  "CODEX_WORKER_RESULT_FILE": str(codex_worker_result),
668
675
  "ANTIGRAVITY_WORKER_RESULT_FILE": str(antigravity_worker_result),
669
676
  "REPORT_WRITER_WORKER_RESULT_FILE": str(report_writer_worker_result),
677
+ "REPORT_WRITER_NARRATIVE_FILE": str(report_writer_narrative),
678
+ "APPROVAL_DECISIONS_PATH": str(approval_decisions),
679
+ "DESIGN_PREPARATION_PATH": str(design_preparation),
680
+ "CONVERGENCE_STATE_PATH": str(convergence_state),
681
+ "PLAN_BODY_VERIFICATION_STATE_PATH": str(plan_body_verification),
670
682
  "RUN_ERRORS_LOG_PATH": str(run_errors_log),
671
683
  "CLAUDE_WORKER_ERRORS_SIDECAR_PATH": str(claude_worker_errors_sidecar),
672
684
  "CODEX_WORKER_ERRORS_SIDECAR_PATH": str(codex_worker_errors_sidecar),
@@ -724,6 +736,11 @@ def compute_run_paths(
724
736
  ("FINAL_REPORT_RECORD_RELATIVE_PATH", final_report),
725
737
  ("FINAL_STATUS_RELATIVE_PATH", final_status),
726
738
  ("TEAM_STATE_RELATIVE_PATH", team_state),
739
+ # `render.py` 가 이 키로 매니페스트의 `convergenceStatePath` 를 채운다.
740
+ # `path_hints` 만 내보내던 동안 그 필드가 빈 문자열로 렌더돼,
741
+ # `report-finalize` 가 "report contract 3.0 requires convergenceStatePath"
742
+ # 로 죽었다 — 수렴 상태 파일 자체는 정상 생성돼 있었다.
743
+ ("CONVERGENCE_STATE_RELATIVE_PATH", convergence_state),
727
744
  ("ACTIVE_RUN_CONTEXT_RELATIVE_PATH", active_run_context),
728
745
  ("LEAD_EVENTS_RELATIVE_PATH", lead_events),
729
746
  ("WORKER_RESULTS_RELATIVE_PATH", worker_results),
@@ -737,6 +754,10 @@ def compute_run_paths(
737
754
  ("CODEX_WORKER_RESULT_RELATIVE_PATH", codex_worker_result),
738
755
  ("ANTIGRAVITY_WORKER_RESULT_RELATIVE_PATH", antigravity_worker_result),
739
756
  ("REPORT_WRITER_WORKER_RESULT_RELATIVE_PATH", report_writer_worker_result),
757
+ ("REPORT_WRITER_NARRATIVE_RELATIVE_PATH", report_writer_narrative),
758
+ ("APPROVAL_DECISIONS_RELATIVE_PATH", approval_decisions),
759
+ ("DESIGN_PREPARATION_RELATIVE_PATH", design_preparation),
760
+ ("PLAN_BODY_VERIFICATION_STATE_RELATIVE_PATH", plan_body_verification),
740
761
  ("RUN_ERRORS_LOG_RELATIVE_PATH", run_errors_log),
741
762
  ("CLAUDE_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", claude_worker_errors_sidecar),
742
763
  ("CODEX_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", codex_worker_errors_sidecar),
@@ -1,6 +1,7 @@
1
1
  """Deterministic extraction of implementation-planning verification items."""
2
2
  from __future__ import annotations
3
3
 
4
+ import re
4
5
  from collections.abc import Mapping
5
6
  from copy import deepcopy
6
7
  from typing import Any
@@ -50,6 +51,87 @@ def _non_empty_string(value: object) -> str | None:
50
51
  return None
51
52
 
52
53
 
54
+ # 자연키가 있는 소스. 이 필드는 요소의 *정체*이지 내용이 아니다 — 명령이나 문구를 고쳐도
55
+ # 같은 요소로 남고, 앞의 형제가 지워져도 값이 변하지 않는다.
56
+ _NATURAL_KEY_FIELDS: dict[str, tuple[str, ...]] = {
57
+ "optionCandidates": ("name",),
58
+ "dependencyMigrationRisk": ("id",),
59
+ "validationChecklist": ("id",),
60
+ "rollbackStrategy": ("id",),
61
+ "requirementCoverage": ("id", "originalRequirementId"),
62
+ }
63
+
64
+
65
+ def _element_key(value: str) -> str:
66
+ return re.sub(r"[^a-z0-9]+", "-", value.strip().lower()).strip("-")
67
+
68
+
69
+ def _element_id(kind: str, key: str, fallback: str) -> str:
70
+ """이 요소의 안정 식별자. 자연키가 없으면 위치 id 로 떨어진다.
71
+
72
+ `P-Opt-N` / `P-Val-N` / `P-Req-N` 같은 위치 id 는 서수라, 자기수정이 요소
73
+ 하나를 지우면 뒤가 전부 밀린다 — 재작성 전의 `P-Req-16` 과 후의 `P-Req-16` 이
74
+ 같은 요소라는 보장이 없다(`prompts/lead/plan-body-verification.md` §7). 그래서
75
+ 라운드나 run 을 가로질러 판정을 이월할 수 없고, 검증 표면이 매번 전체로
76
+ 돌아간다.
77
+
78
+ `elementId` 는 그 대조를 가능하게 한다. 위치가 아니라 요소가 스스로 지닌
79
+ 이름(`R-001`, `VC-011`, 스테이지·스텝 좌표)에서 만들어지므로 형제의 삭제에
80
+ 영향받지 않는다. 표시와 인용은 계속 `id` 를 쓴다 — 이 값은 대조용이다.
81
+
82
+ 자연키가 없는 소스는 위치 id 로 떨어지고, 그 항목은 삭제를 가로질러 안정하지
83
+ 않다. 소비자는 `elementId == id` 인 항목을 그렇게 다뤄야 한다.
84
+ """
85
+ normalized = _element_key(key)
86
+ return f"{kind}:{normalized}" if normalized else fallback
87
+
88
+
89
+ # 자기 기록 블록. 계획은 실행 지시와 그 실행의 감사 기록을 한 산출물에 담는데,
90
+ # 게이트가 둘을 구분하지 않으면 기록의 부정확이 구현 착수를 막는다 — 실측 run 에서
91
+ # 남은 차단 4건이 전부 "사용자가 내린 결정을 계획이 정확히 반영했는가" 였다.
92
+ #
93
+ # 커버리지 행이 유일한 기록 항목이다. 승인 처분과 decision 참조는 그 행의 필드이고,
94
+ # 나머지 소스는 전부 무엇을 어떻게 만드는지를 말한다.
95
+ _RECORD_SOURCES = frozenset({"requirementCoverage"})
96
+
97
+
98
+ def _item_block(source: str) -> str:
99
+ return "record" if source in _RECORD_SOURCES else "execution"
100
+
101
+
102
+ def _stage_scope(row: Mapping[str, Any] | None, *coordinates: int) -> list[int] | None:
103
+ """이 항목이 걸리는 스테이지. 어디에도 안 걸리면 ``None``.
104
+
105
+ 게이트 범위를 다음 실행 스테이지로 좁히려면 항목마다 이 값이 있어야 한다.
106
+ 스텝과 design-prep 은 id 자체가 좌표를 지니지만, 커버리지 행과 검증 행은
107
+ `stageRefs` 를 실어야 알 수 있다 — 그 전에는 `coveredBy` 산문을 정규식으로
108
+ 읽는 수밖에 없었고, 그것은 게이트 판정의 근거가 되지 못한다.
109
+
110
+ ``None`` 은 "스테이지에 걸리지 않는다" 이고, 소비처는 이를 **모든 스테이지에
111
+ 적용** 으로 읽는다. 그게 안전한 쪽이고 지금 동작과 같다. `P-Opt-*` 처럼 계획
112
+ 전체의 성격을 판정하는 항목이 여기 해당하고, `stageRefs` 를 아직 안 쓰는 계획도
113
+ 같은 자리로 떨어진다.
114
+ """
115
+ if coordinates:
116
+ return [int(value) for value in coordinates]
117
+ refs = (row or {}).get("stageRefs")
118
+ if not isinstance(refs, list):
119
+ return None
120
+ stages = [
121
+ value for value in refs
122
+ if isinstance(value, int) and not isinstance(value, bool) and value >= 1
123
+ ]
124
+ return stages or None
125
+
126
+
127
+ def _natural_element_id(source: str, prefix: str, row: Mapping[str, Any], item_id: str) -> str:
128
+ for field in _NATURAL_KEY_FIELDS.get(source, ()):
129
+ value = _non_empty_string(row.get(field))
130
+ if value is not None:
131
+ return _element_id(prefix.removeprefix("P-").lower(), value, item_id)
132
+ return item_id
133
+
134
+
53
135
  def _subject(row: Mapping[str, Any], source: str) -> str:
54
136
  for field in _SUBJECT_FIELDS[source]:
55
137
  value = _non_empty_string(row.get(field))
@@ -77,6 +159,11 @@ def _row_payload(row: Mapping[str, Any], **coordinates: int) -> dict[str, Any]:
77
159
  def _add_item(items: list[dict[str, Any]], item: dict[str, Any]) -> None:
78
160
  if any(existing["id"] == item["id"] for existing in items):
79
161
  raise PlanItemContractError(f"duplicate plan item ID: {item['id']}")
162
+ element_id = item.get("elementId")
163
+ if element_id and any(existing.get("elementId") == element_id for existing in items):
164
+ # 두 요소가 같은 자연키를 쓰면 이월이 엉뚱한 항목에 붙는다. 위치 id 가
165
+ # 갈라져 있어도 대조는 elementId 로 하므로 여기서 막는다.
166
+ raise PlanItemContractError(f"duplicate plan element ID: {element_id}")
80
167
  items.append(item)
81
168
 
82
169
 
@@ -115,6 +202,9 @@ def _extract_standard_items(planning: Mapping[str, Any]) -> list[dict[str, Any]]
115
202
  items,
116
203
  {
117
204
  "id": "P-Dir-1",
205
+ "elementId": "dir:selected-direction",
206
+ "stageScope": None,
207
+ "block": "execution",
118
208
  "subject": _non_empty_string(realization.get("goal"))
119
209
  or "Selected direction realization",
120
210
  "sourceSection": "Selected Direction",
@@ -127,10 +217,14 @@ def _extract_standard_items(planning: Mapping[str, Any]) -> list[dict[str, Any]]
127
217
  _extract_step_items(planning, items, prefix, section)
128
218
  continue
129
219
  for index, row in enumerate(_rows(planning, source), start=1):
220
+ item_id = f"{prefix}-{index}"
130
221
  _add_item(
131
222
  items,
132
223
  {
133
- "id": f"{prefix}-{index}",
224
+ "id": item_id,
225
+ "elementId": _natural_element_id(source, prefix, row, item_id),
226
+ "stageScope": _stage_scope(row),
227
+ "block": _item_block(source),
134
228
  "subject": _subject(row, source),
135
229
  "sourceSection": section,
136
230
  "ticketId": _ticket_id(row),
@@ -151,8 +245,11 @@ def _extract_step_items(
151
245
  _add_item(
152
246
  items,
153
247
  {
248
+ # 평면 배열에는 좌표가 없어 위치 id 로 떨어진다.
154
249
  "id": f"{prefix}-{index}",
155
- "subject": _subject(row, "stepwiseExecution"),
250
+ "elementId": f"{prefix}-{index}",
251
+ "stageScope": _stage_scope(row),
252
+ "block": "execution", "subject": _subject(row, "stepwiseExecution"),
156
253
  "sourceSection": section,
157
254
  "ticketId": _ticket_id(row),
158
255
  "payload": _row_payload(row),
@@ -172,6 +269,9 @@ def _extract_step_items(
172
269
  items,
173
270
  {
174
271
  "id": f"{prefix}-{stage_number}.{step_number}",
272
+ "elementId": f"step:{stage_number}.{step_number}",
273
+ "stageScope": _stage_scope(None, stage_number),
274
+ "block": "execution",
175
275
  "subject": _subject(row, "stepwiseExecution"),
176
276
  "sourceSection": section,
177
277
  "ticketId": _ticket_id(row),
@@ -212,6 +312,9 @@ def _extract_prep_items(
212
312
  items,
213
313
  {
214
314
  "id": item_id,
315
+ "elementId": f"prep:{trigger.stage}:{_element_key(trigger.kind)}",
316
+ "stageScope": _stage_scope(None, trigger.stage),
317
+ "block": "execution",
215
318
  "subject": f"Stage {trigger.stage} {trigger.kind} design preparation",
216
319
  "sourceSection": "5.5.10",
217
320
  "ticketId": "",
@@ -243,7 +346,9 @@ def _extract_variation_point_items(
243
346
  items,
244
347
  {
245
348
  "id": "P-Var-0",
246
- "subject": "No variation point declared",
349
+ "elementId": "var:none",
350
+ "stageScope": None,
351
+ "block": "execution", "subject": "No variation point declared",
247
352
  "sourceSection": "5.5.11",
248
353
  "ticketId": "",
249
354
  "payload": _row_payload(dict(analysis)),
@@ -259,7 +364,13 @@ def _extract_variation_point_items(
259
364
  items,
260
365
  {
261
366
  "id": f"P-Var-{index}",
262
- "subject": _non_empty_string(point.get("behavior"))
367
+ "elementId": _element_id(
368
+ "var",
369
+ _non_empty_string(point.get("behavior")) or "",
370
+ f"P-Var-{index}",
371
+ ),
372
+ "stageScope": None,
373
+ "block": "execution", "subject": _non_empty_string(point.get("behavior"))
263
374
  or f"variation point {index}",
264
375
  "sourceSection": "5.5.11",
265
376
  "ticketId": "",