okstra 0.202.0 → 0.205.0

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 (263) hide show
  1. package/README.md +7 -6
  2. package/dist/cli-registry.mjs +7 -7
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/dist/commands/lifecycle/install.mjs +50 -124
  5. package/dist/commands/lifecycle/install.mjs.map +1 -1
  6. package/dist/commands/memory/memory.mjs +41 -8
  7. package/dist/commands/memory/memory.mjs.map +1 -1
  8. package/dist/lib/install-assets.mjs +3 -0
  9. package/dist/lib/install-assets.mjs.map +1 -1
  10. package/dist/lib/runtime-manifest.mjs +2 -1
  11. package/dist/lib/runtime-manifest.mjs.map +1 -1
  12. package/dist/lib/types.d.mts +2 -1
  13. package/docs/architecture/storage-model.md +14 -11
  14. package/docs/architecture.md +26 -20
  15. package/docs/cli.md +15 -12
  16. package/docs/contributor-change-matrix.md +3 -2
  17. package/docs/performance-improvement-plan-v2.md +3 -9
  18. package/docs/project-structure-overview.md +39 -11
  19. package/docs/task-process/README.md +1 -1
  20. package/docs/task-process/common-flow.md +1 -1
  21. package/docs/task-process/final-verification.md +3 -1
  22. package/docs/task-process/implementation-option-selection.md +1 -1
  23. package/docs/task-process/implementation.md +1 -1
  24. package/docs/task-process/release-handoff.md +36 -39
  25. package/package.json +1 -2
  26. package/runtime/BUILD.json +2 -2
  27. package/runtime/agents/common.json +28 -0
  28. package/runtime/agents/operations/code-review.json +6 -0
  29. package/runtime/agents/operations/report-translation.json +6 -0
  30. package/runtime/agents/operations/schedule-verification.json +6 -0
  31. package/runtime/agents/roles/analyser.json +18 -0
  32. package/runtime/agents/roles/critic.json +18 -0
  33. package/runtime/agents/roles/designer.json +18 -0
  34. package/runtime/agents/roles/implementer.json +20 -0
  35. package/runtime/agents/roles/leader.json +20 -0
  36. package/runtime/agents/roles/planner.json +18 -0
  37. package/runtime/agents/roles/report-writer.json +19 -0
  38. package/runtime/agents/roles/translator.json +19 -0
  39. package/runtime/agents/roles/verifier.json +18 -0
  40. package/runtime/bin/lib/okstra/usage.sh +5 -5
  41. package/runtime/prompts/duties/acceptance-critic.json +32 -0
  42. package/runtime/prompts/duties/acceptance-verifier.json +32 -0
  43. package/runtime/prompts/duties/analysis-worker.json +32 -0
  44. package/runtime/prompts/duties/code-reviewer.json +32 -0
  45. package/runtime/prompts/duties/diagnosis-worker.json +32 -0
  46. package/runtime/prompts/duties/direction-selection-worker.json +32 -0
  47. package/runtime/prompts/duties/discovery-worker.json +32 -0
  48. package/runtime/prompts/duties/implementation-executor.json +32 -0
  49. package/runtime/prompts/duties/implementation-verifier.json +32 -0
  50. package/runtime/prompts/duties/lead.json +32 -0
  51. package/runtime/prompts/duties/planning-worker.json +36 -0
  52. package/runtime/prompts/duties/report-writer.json +32 -0
  53. package/runtime/prompts/duties/reverification-worker.json +32 -0
  54. package/runtime/prompts/duties/schedule-verifier.json +32 -0
  55. package/runtime/prompts/duties/scope-critic.json +32 -0
  56. package/runtime/prompts/duties/technical-verification-worker.json +32 -0
  57. package/runtime/prompts/duties/translator.json +32 -0
  58. package/runtime/prompts/launch.template.md +2 -1
  59. package/runtime/prompts/lead/adapters/cmux.md +1 -1
  60. package/runtime/prompts/lead/convergence.md +4 -4
  61. package/runtime/prompts/lead/okstra-lead-contract.md +115 -6
  62. package/runtime/prompts/lead/plan-body-verification.md +6 -6
  63. package/runtime/prompts/lead/report-writer.md +3 -3
  64. package/runtime/prompts/profiles/_common-contract.md +2 -2
  65. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  66. package/runtime/prompts/profiles/_implementation-executor.md +4 -1
  67. package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
  68. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  69. package/runtime/prompts/profiles/change-impact-analysis.json +31 -0
  70. package/runtime/prompts/profiles/change-impact-analysis.md +0 -20
  71. package/runtime/prompts/profiles/error-analysis.json +39 -0
  72. package/runtime/prompts/profiles/error-analysis.md +0 -25
  73. package/runtime/prompts/profiles/feature-analysis.json +31 -0
  74. package/runtime/prompts/profiles/feature-analysis.md +0 -20
  75. package/runtime/prompts/profiles/final-verification.json +30 -0
  76. package/runtime/prompts/profiles/final-verification.md +4 -23
  77. package/runtime/prompts/profiles/forbidden-actions.json +4 -3
  78. package/runtime/prompts/profiles/implementation-option-selection.json +31 -0
  79. package/runtime/prompts/profiles/implementation-option-selection.md +0 -20
  80. package/runtime/prompts/profiles/implementation-planning.json +40 -0
  81. package/runtime/prompts/profiles/implementation-planning.md +4 -29
  82. package/runtime/prompts/profiles/implementation.json +30 -0
  83. package/runtime/prompts/profiles/implementation.md +1 -20
  84. package/runtime/prompts/profiles/improvement-discovery.json +31 -0
  85. package/runtime/prompts/profiles/improvement-discovery.md +0 -20
  86. package/runtime/prompts/profiles/project-analysis.json +31 -0
  87. package/runtime/prompts/profiles/project-analysis.md +0 -20
  88. package/runtime/prompts/profiles/release-handoff.json +5 -0
  89. package/runtime/prompts/profiles/release-handoff.md +74 -74
  90. package/runtime/prompts/profiles/requirements-discovery.json +39 -0
  91. package/runtime/prompts/profiles/requirements-discovery.md +0 -25
  92. package/runtime/prompts/profiles/technical-verification.json +39 -0
  93. package/runtime/prompts/profiles/technical-verification.md +0 -25
  94. package/runtime/prompts/wizard/prompts.ko.json +14 -18
  95. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -0
  96. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +3 -0
  97. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -1
  98. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -3
  99. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/worker-session.md +108 -0
  100. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -0
  101. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -0
  102. package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -0
  103. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +8 -1
  104. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +8 -0
  105. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +23 -6
  106. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +6 -2
  107. package/runtime/python/okstra_ctl/agent/invocation.py +168 -113
  108. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +120 -0
  109. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +107 -2
  110. package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +0 -49
  111. package/runtime/python/okstra_ctl/analysis_packet.py +4 -1
  112. package/runtime/python/okstra_ctl/application/open_worker.py +6 -1
  113. package/runtime/python/okstra_ctl/assignment_resolver.py +16 -5
  114. package/runtime/python/okstra_ctl/cmux.py +69 -20
  115. package/runtime/python/okstra_ctl/code_review_target.py +16 -8
  116. package/runtime/python/okstra_ctl/conformance.py +43 -0
  117. package/runtime/python/okstra_ctl/consumers.py +23 -8
  118. package/runtime/python/okstra_ctl/container.py +31 -8
  119. package/runtime/python/okstra_ctl/context_cost.py +11 -15
  120. package/runtime/python/okstra_ctl/contract_refreeze.py +156 -0
  121. package/runtime/python/okstra_ctl/convergence_engine.py +38 -0
  122. package/runtime/python/okstra_ctl/convergence_provenance.py +7 -1
  123. package/runtime/python/okstra_ctl/design_prep.py +34 -1
  124. package/runtime/python/okstra_ctl/dispatch_core.py +53 -27
  125. package/runtime/python/okstra_ctl/domain/host.py +5 -0
  126. package/runtime/python/okstra_ctl/domain/worker_runtime.py +10 -0
  127. package/runtime/python/okstra_ctl/error_report.py +4 -3
  128. package/runtime/python/okstra_ctl/execution_manifest.py +71 -18
  129. package/runtime/python/okstra_ctl/handoff.py +384 -286
  130. package/runtime/python/okstra_ctl/handoff_verification.py +25 -6
  131. package/runtime/python/okstra_ctl/implementation_stage.py +9 -0
  132. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +113 -0
  133. package/runtime/python/okstra_ctl/lead_progress.py +1 -1
  134. package/runtime/python/okstra_ctl/legacy_model_selection.py +2 -2
  135. package/runtime/python/okstra_ctl/manager_cli.py +92 -4
  136. package/runtime/python/okstra_ctl/manager_launch.py +1 -1
  137. package/runtime/python/okstra_ctl/manager_paths.py +14 -3
  138. package/runtime/python/okstra_ctl/manager_store.py +210 -3
  139. package/runtime/python/okstra_ctl/manager_sync.py +4 -1
  140. package/runtime/python/okstra_ctl/manager_view.py +2 -1
  141. package/runtime/python/okstra_ctl/model_discovery.py +30 -0
  142. package/runtime/python/okstra_ctl/model_io/lines.py +14 -1
  143. package/runtime/python/okstra_ctl/model_io/renderers.py +4 -3
  144. package/runtime/python/okstra_ctl/models.py +1 -1
  145. package/runtime/python/okstra_ctl/next_phase.py +16 -6
  146. package/runtime/python/okstra_ctl/operation_invocation.py +86 -0
  147. package/runtime/python/okstra_ctl/option_comparison.py +168 -0
  148. package/runtime/python/okstra_ctl/path_hints.py +9 -0
  149. package/runtime/python/okstra_ctl/paths.py +3 -0
  150. package/runtime/python/okstra_ctl/profile_show.py +42 -1
  151. package/runtime/python/okstra_ctl/registry/host_discovery.py +20 -12
  152. package/runtime/python/okstra_ctl/registry/host_registry.py +11 -0
  153. package/runtime/python/okstra_ctl/render.py +79 -0
  154. package/runtime/python/okstra_ctl/report_contract.py +1 -1
  155. package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +21 -3
  156. package/runtime/python/okstra_ctl/report_synthesis_packet.py +177 -17
  157. package/runtime/python/okstra_ctl/report_translation.py +2 -1
  158. package/runtime/python/okstra_ctl/report_translation_dispatch.py +69 -9
  159. package/runtime/python/okstra_ctl/role_requirements.py +142 -129
  160. package/runtime/python/okstra_ctl/rollup.py +3 -1
  161. package/runtime/python/okstra_ctl/run.py +76 -29
  162. package/runtime/python/okstra_ctl/schedule_semantics.py +17 -6
  163. package/runtime/python/okstra_ctl/stage_fix_carry.py +23 -4
  164. package/runtime/python/okstra_ctl/stage_integrate.py +178 -18
  165. package/runtime/python/okstra_ctl/stage_map.py +16 -2
  166. package/runtime/python/okstra_ctl/stage_targets.py +209 -43
  167. package/runtime/python/okstra_ctl/team.py +22 -13
  168. package/runtime/python/okstra_ctl/time_report.py +2 -1
  169. package/runtime/python/okstra_ctl/usage_report.py +3 -1
  170. package/runtime/python/okstra_ctl/wizard/confirmation.py +3 -9
  171. package/runtime/python/okstra_ctl/wizard/ids.py +1 -1
  172. package/runtime/python/okstra_ctl/wizard/registry.py +1 -1
  173. package/runtime/python/okstra_ctl/wizard/state.py +3 -5
  174. package/runtime/python/okstra_ctl/wizard/steps_plan.py +12 -21
  175. package/runtime/python/okstra_ctl/worker_prompt_contract.py +5 -1
  176. package/runtime/python/okstra_ctl/worker_prompt_headers.py +35 -7
  177. package/runtime/python/okstra_ctl/worker_prompt_policy.py +66 -48
  178. package/runtime/python/okstra_ctl/workflow.py +1 -1
  179. package/runtime/python/okstra_ctl/worktree/__init__.py +3 -1
  180. package/runtime/python/okstra_ctl/worktree/naming.py +9 -0
  181. package/runtime/python/okstra_ctl/worktree_registry.py +38 -9
  182. package/runtime/python/okstra_token_usage/pricing.py +6 -4
  183. package/runtime/schemas/agent-common-v1.schema.json +34 -0
  184. package/runtime/schemas/agent-duty-v1.schema.json +38 -0
  185. package/runtime/schemas/agent-operation-v1.schema.json +11 -0
  186. package/runtime/schemas/agent-profile-v1.schema.json +46 -0
  187. package/runtime/schemas/agent-role-v1.schema.json +29 -0
  188. package/runtime/schemas/final-report-v2.0.schema.json +118 -97
  189. package/runtime/schemas/final-report-v3.0.schema.json +118 -97
  190. package/runtime/skills/okstra-brief-gen/SKILL.md +84 -4
  191. package/runtime/skills/okstra-chat/SKILL.md +2 -2
  192. package/runtime/skills/okstra-code-review/SKILL.md +23 -9
  193. package/runtime/skills/okstra-container-build/SKILL.md +10 -10
  194. package/runtime/skills/okstra-inspect/SKILL.md +1 -1
  195. package/runtime/skills/okstra-inspect/facets/cost.md +1 -1
  196. package/runtime/skills/okstra-inspect/facets/error-zip.md +9 -9
  197. package/runtime/skills/okstra-inspect/facets/errors.md +16 -16
  198. package/runtime/skills/okstra-inspect/facets/logs.md +7 -7
  199. package/runtime/skills/okstra-inspect/facets/recap.md +2 -2
  200. package/runtime/skills/okstra-inspect/facets/report.md +1 -1
  201. package/runtime/skills/okstra-inspect/facets/status.md +4 -3
  202. package/runtime/skills/okstra-inspect/facets/time.md +11 -10
  203. package/runtime/skills/okstra-manager/SKILL.md +18 -2
  204. package/runtime/skills/okstra-pr-gen/SKILL.md +6 -5
  205. package/runtime/skills/okstra-rollup/SKILL.md +5 -5
  206. package/runtime/skills/okstra-run/SKILL.md +31 -12
  207. package/runtime/skills/okstra-schedule-gen/SKILL.md +19 -14
  208. package/runtime/skills/okstra-setup/SKILL.md +12 -10
  209. package/runtime/skills/okstra-setup/references/project-config.md +7 -6
  210. package/runtime/skills/okstra-usage/SKILL.md +1 -1
  211. package/runtime/skills/okstra-user-response/SKILL.md +1 -1
  212. package/runtime/templates/manager/view.template.html +1 -0
  213. package/runtime/templates/report-writer-prompt-preamble.md +8 -0
  214. package/runtime/templates/reports/brief.template.md +14 -4
  215. package/runtime/templates/reports/html/i18n/en.json +5 -4
  216. package/runtime/templates/reports/html/i18n/ko.json +5 -4
  217. package/runtime/templates/reports/html/tasks/release-handoff.template.html +8 -5
  218. package/runtime/templates/reports/i18n/en.json +1 -1
  219. package/runtime/templates/reports/md/tasks/release-handoff.template.md +1 -1
  220. package/runtime/templates/reports/release-handoff-input.template.md +6 -4
  221. package/runtime/templates/translator-prompt-preamble.md +36 -0
  222. package/runtime/validators/checks/validate-assets-01.py +7 -8
  223. package/runtime/validators/validate-brief.py +70 -0
  224. package/runtime/validators/validate-implementation-plan-stages.py +2 -1
  225. package/runtime/validators/validate-run.py +72 -15
  226. package/runtime/validators/validate-schedule.py +9 -0
  227. package/docs/for-ai/README.md +0 -68
  228. package/docs/for-ai/skills/okstra-brief-gen.md +0 -262
  229. package/docs/for-ai/skills/okstra-chat.md +0 -34
  230. package/docs/for-ai/skills/okstra-code-review.md +0 -57
  231. package/docs/for-ai/skills/okstra-container-build.md +0 -129
  232. package/docs/for-ai/skills/okstra-inspect.md +0 -262
  233. package/docs/for-ai/skills/okstra-manager.md +0 -86
  234. package/docs/for-ai/skills/okstra-memory.md +0 -126
  235. package/docs/for-ai/skills/okstra-pr-gen.md +0 -49
  236. package/docs/for-ai/skills/okstra-rollup.md +0 -114
  237. package/docs/for-ai/skills/okstra-run.md +0 -250
  238. package/docs/for-ai/skills/okstra-schedule-gen.md +0 -240
  239. package/docs/for-ai/skills/okstra-setup.md +0 -167
  240. package/docs/for-ai/skills/okstra-usage.md +0 -29
  241. package/docs/for-ai/skills/okstra-user-response.md +0 -72
  242. package/runtime/agents/workers/claude-worker.md +0 -128
  243. package/runtime/agents/workers/report-writer-worker.md +0 -37
  244. package/runtime/agents/workers/translator-worker.md +0 -63
  245. package/runtime/prompts/duties/acceptance-critic.md +0 -44
  246. package/runtime/prompts/duties/acceptance-verifier.md +0 -44
  247. package/runtime/prompts/duties/analysis-worker.md +0 -44
  248. package/runtime/prompts/duties/code-reviewer.md +0 -44
  249. package/runtime/prompts/duties/common.md +0 -39
  250. package/runtime/prompts/duties/diagnosis-worker.md +0 -44
  251. package/runtime/prompts/duties/direction-selection-worker.md +0 -44
  252. package/runtime/prompts/duties/discovery-worker.md +0 -44
  253. package/runtime/prompts/duties/implementation-executor.md +0 -44
  254. package/runtime/prompts/duties/implementation-verifier.md +0 -44
  255. package/runtime/prompts/duties/lead.md +0 -44
  256. package/runtime/prompts/duties/planning-worker.md +0 -52
  257. package/runtime/prompts/duties/report-writer.md +0 -44
  258. package/runtime/prompts/duties/reverification-worker.md +0 -44
  259. package/runtime/prompts/duties/schedule-verifier.md +0 -44
  260. package/runtime/prompts/duties/scope-critic.md +0 -44
  261. package/runtime/prompts/duties/technical-verification-worker.md +0 -44
  262. package/runtime/prompts/duties/translator.md +0 -44
  263. package/runtime/python/okstra_ctl/pane_title.py +0 -154
@@ -219,6 +219,49 @@ def exempt_stage_surface_conflicts(
219
219
  return conflicts
220
220
 
221
221
 
222
+ def declared_stage_surface_gaps(
223
+ data: object, patterns: object = None,
224
+ ) -> list[dict[str, object]]:
225
+ """`Conformance tests:` 의 `requires` 가 계획된 경로의 표면을 다 덮지 못한 stage.
226
+
227
+ 구현 게이트는 두 가지를 동시에 요구한다 — diff 가 건드린 표면을 `requires`
228
+ 가 덮을 것(`_validate_conformance_surfaces`), 그리고 선언된 stage 의
229
+ `requires` 가 계획 선언과 같을 것(`_declared_conformance_errors`). 계획이
230
+ `requires` 를 좁게 적었는데 그 stage 가 계획한 경로가 다른 표면을 건드리면
231
+ 구현은 둘 중 하나에 반드시 걸린다. 실측(2026-09-22, fontradar-v2-api dev-10860
232
+ Stage 1~3): `requires=[io]` 로 선언하고 `catalog.controller.ts`(http)를
233
+ 계획해, 구현 run 이 "undeclared surface" 와 "requires mismatch" 를 번갈아 냈다.
234
+ 면제 stage 와 같은 판별을 선언 stage 에도 승인 전에 한다.
235
+
236
+ 반환 행: `{"stage": <int>, "surfaces": [빠진 표면..], "paths": [..],
237
+ "requires": [선언된 것..]}`.
238
+ """
239
+ planning = data.get("implementationPlanning") if isinstance(data, dict) else None
240
+ stages = planning.get("stages") if isinstance(planning, dict) else None
241
+ gaps: list[dict[str, object]] = []
242
+ for stage in stages if isinstance(stages, list) else []:
243
+ if not isinstance(stage, dict):
244
+ continue
245
+ parsed = parse_conformance_tests(stage.get("conformanceTests"))
246
+ number = stage.get("stage")
247
+ if parsed is None or not isinstance(number, int) or isinstance(number, bool):
248
+ continue
249
+ _script, requires = parsed
250
+ touching = sorted(
251
+ path for path in _stage_planned_paths(stage)
252
+ if detect_surfaces([path], patterns) - requires
253
+ )
254
+ missing = detect_surfaces(touching, patterns) - requires
255
+ if missing:
256
+ gaps.append({
257
+ "stage": number,
258
+ "surfaces": sorted(missing),
259
+ "paths": touching,
260
+ "requires": sorted(requires),
261
+ })
262
+ return gaps
263
+
264
+
222
265
  def _stage_planned_paths(stage: dict) -> list[str]:
223
266
  """stage 의 step 들이 계획한 경로 — 경로 모양인 문자열만.
224
267
 
@@ -218,9 +218,15 @@ def _append_row(plan_run_root: Path, record: Dict[str, Any]) -> None:
218
218
 
219
219
  def append_verified(plan_run_root: Path, *, impl_task_key: str, stage: int,
220
220
  verdict: str, report_path: str, head_commit: str = "",
221
- final_verdict: dict | None = None) -> None:
222
- """단독-stage final-verification 결과 기록. 같은 report_path 재기록은 멱등,
223
- 다른 report_path 는 재검증으로 append 한다 (읽기는 last-wins)."""
221
+ final_verdict: dict | None = None,
222
+ verification_scope: str = "",
223
+ verified_head_commit: str = "") -> None:
224
+ """final-verification 결과를 stage 별로 기록. 같은 report_path 재기록은 멱등,
225
+ 다른 report_path 는 재검증으로 append 한다 (읽기는 last-wins).
226
+
227
+ `head_commit` 은 언제나 이 stage 가 완료로 기록한 커밋이다. 전체 task 검증이
228
+ 근거인 행은 검증된 head 가 그 커밋이 아니므로, 그 head 를
229
+ `verified_head_commit` 으로 함께 남긴다 — 자격 판정이 둘을 각각 대조한다."""
224
230
  with consumers_mutex(plan_run_root):
225
231
  rows = read_consumers(plan_run_root)
226
232
  done = latest_done_by_stage(rows).get(stage, {})
@@ -234,25 +240,34 @@ def append_verified(plan_run_root: Path, *, impl_task_key: str, stage: int,
234
240
  and row.get("status") == "verified"
235
241
  and row.get("report_path") == report_path):
236
242
  return
237
- _append_row(plan_run_root, {
243
+ row: Dict[str, Any] = {
238
244
  "impl_task_key": impl_task_key, "stage": stage,
239
245
  "status": "verified", "verdict": verdict,
240
246
  "report_path": report_path,
241
247
  "head_commit": head_commit,
242
248
  "final_verdict": final_verdict or {},
243
- })
249
+ }
250
+ # 단독-stage 행에는 싣지 않는다 — 그 행의 검증된 head 는 `head_commit`
251
+ # 자신이고, 빈 필드를 더하면 옛 행과 새 행의 모양이 갈라진다.
252
+ if verification_scope:
253
+ row["verification_scope"] = verification_scope
254
+ row["verified_head_commit"] = verified_head_commit
255
+ _append_row(plan_run_root, row)
244
256
 
245
257
 
246
258
  def append_pr(plan_run_root: Path, *, impl_task_key: str, stages: List[int],
247
- branch: str, url: str) -> None:
248
- """handoff 의 PR 생성/재사용 기록. 같은 branch 의 pr 행이 이미 있으면 멱등."""
259
+ branch: str, url: str, base: str = "") -> None:
260
+ """handoff 의 PR 생성/재사용 기록. 같은 branch 의 pr 행이 이미 있으면 멱등.
261
+
262
+ `base` 는 그 PR 이 올라앉은 브랜치다. stage PR 은 스택이라 base 가 릴리스
263
+ 브랜치가 아닐 수 있고, 어느 순서로 머지해야 하는지는 이 값에만 남는다."""
249
264
  with consumers_mutex(plan_run_root):
250
265
  for row in read_consumers(plan_run_root):
251
266
  if row.get("status") == "pr" and row.get("branch") == branch:
252
267
  return
253
268
  _append_row(plan_run_root, {
254
269
  "impl_task_key": impl_task_key, "stages": sorted(stages),
255
- "status": "pr", "branch": branch, "url": url,
270
+ "status": "pr", "branch": branch, "base": base, "url": url,
256
271
  })
257
272
 
258
273
 
@@ -213,8 +213,18 @@ def _compose_ps(project_name: str, cwd: str, include_stopped: bool = False) -> l
213
213
  return rows
214
214
 
215
215
 
216
- def _docker_run(argv: list[str], cwd: str | None = None) -> str:
216
+ def _docker_run(argv: list[str], cwd: str | None = None, *, check: bool = False) -> str:
217
+ """docker 명령을 돌려 stdout 을 돌려준다.
218
+
219
+ `check=True` 면 0 이 아닌 종료를 PrepareError 로 올린다. status 가 빈 목록을,
220
+ down 이 성공을 보고하는데 실제로는 docker 가 실패한 경우를 막는다. config 계열
221
+ 조회는 실패해도 진행하도록 설계돼 있어 기본값은 그대로 둔다."""
217
222
  result = subprocess.run(argv, capture_output=True, text=True, check=False, cwd=cwd)
223
+ if check and result.returncode != 0:
224
+ detail = (result.stderr or result.stdout or "").strip().splitlines()
225
+ raise PrepareError(
226
+ f"`{' '.join(argv[:4])}` 가 exit {result.returncode} 로 실패했습니다: "
227
+ f"{detail[-1] if detail else '출력 없음'}")
218
228
  return result.stdout or ""
219
229
 
220
230
 
@@ -222,7 +232,7 @@ def query_containers_by_label(project_name: str) -> list[dict]:
222
232
  """라벨 SSOT(spec §11): `okstra.project-name` 으로 컨테이너를 질의한다.
223
233
 
224
234
  registry 가 아니라 docker 라벨이 컨테이너 존재의 기준값이다."""
225
- out = _docker_run([
235
+ out = _docker_run(check=True, argv=[
226
236
  "docker", "ps", "-a", "--filter",
227
237
  f"label=okstra.project-name={project_name}",
228
238
  "--format", (
@@ -431,7 +441,7 @@ def provision_container_group(
431
441
  raise PrepareError(
432
442
  f"container up: `up -d` 후 기동된 컨테이너가 없습니다(`docker compose -p "
433
443
  f"{project_name} ps -a` 로 확인). compose 정의·활성 profile 을 점검한 뒤 "
434
- f"`okstra container down --task-key {task_key}` 로 정리하고 다시 시도하세요.")
444
+ f"`{_down_hint(project_root, task_key)}` 로 정리하고 다시 시도하세요.")
435
445
  failed = poll_healthcheck(
436
446
  project_name=project_name, cwd=str(worktree), services=started,
437
447
  healthcheck_timeout_seconds=healthcheck_timeout_seconds,
@@ -445,8 +455,8 @@ def provision_container_group(
445
455
  raise PrepareError(
446
456
  f"container up: 다음 서비스가 시간 내 기동하지 못했습니다: {', '.join(failed)} "
447
457
  f"(timeout={healthcheck_timeout_seconds}s). `docker compose -p "
448
- f"{project_name} logs` 로 원인을 확인한 뒤 `okstra container down "
449
- f"--task-key {task_key}` 로 정리하세요.")
458
+ f"{project_name} logs` 로 원인을 확인한 뒤 "
459
+ f"`{_down_hint(project_root, task_key)}` 로 정리하세요.")
450
460
  # deploy-state 는 실제 기동된 started 만 기록한다 — full services 를 쓰면
451
461
  # profiles 로 안 뜬 서비스가 `down --all` 스윕의 대상으로 남는다.
452
462
  _write_deploy_state(cp, project_name=project_name, task_key=task_key,
@@ -528,12 +538,18 @@ def status_container_group(*, project_root, task_key: str) -> dict:
528
538
  "containers": query_containers_by_label(project_name)}
529
539
 
530
540
 
541
+ def _down_hint(project_root, task_key: str) -> str:
542
+ """실패 메시지가 안내하는 정리 명령. 파서가 요구하는 인자를 모두 싣는다."""
543
+ return f"okstra container down --project-root {project_root} --task-key {task_key}"
544
+
545
+
531
546
  def down_container_group(*, project_root, task_key: str = "", all_groups: bool = False) -> dict:
532
- """라벨 쿼리로 컨테이너를 down 한다."""
547
+ """compose 프로젝트 이름으로 task 의 컨테이너 그룹을 down 한다."""
533
548
  from .ids import compose_project_name
534
549
 
535
550
  project_root = Path(project_root)
536
551
  results = []
552
+ failures: list[str] = []
537
553
  for project_id, group, task_id in _down_targets(project_root, task_key, all_groups):
538
554
  project_name = compose_project_name(project_id, group, task_id)
539
555
  # `docker rm -f` 는 컨테이너만 지우고 compose 가 만든 네트워크를 누수시킨다.
@@ -541,9 +557,16 @@ def down_container_group(*, project_root, task_key: str = "", all_groups: bool =
541
557
  # 단위 컨테이너+네트워크를 회수한다. `-v` 는 의도적으로 빼서 compose 에
542
558
  # 선언된 named volume(예: DB 데이터)을 보존한다 — down 한 번에 사용자
543
559
  # 영속 데이터를 파괴하지 않는다.
544
- _docker_run(["docker", "compose", "-p", project_name, "down",
545
- "--remove-orphans"])
560
+ try:
561
+ _docker_run(["docker", "compose", "-p", project_name, "down",
562
+ "--remove-orphans"], check=True)
563
+ except PrepareError as exc:
564
+ failures.append(f"{project_name}: {exc}")
565
+ continue
546
566
  results.append({"projectName": project_name})
567
+ if failures:
568
+ # 나머지 대상은 끝까지 내리고, 실패한 것만 모아 알린다.
569
+ raise PrepareError("container down 실패 — " + "; ".join(failures))
547
570
  return {"downed": results}
548
571
 
549
572
 
@@ -2,7 +2,8 @@
2
2
 
3
3
  The estimator answers a narrow operational question: how much file/context
4
4
  surface does the current task bundle ask the lead, analysis workers, and
5
- report-writer to absorb? It does not mutate task artifacts.
5
+ report-writer to absorb? It writes no report or run artifact; resolving the task
6
+ may record a finished implementation outcome (see `find_task_root`).
6
7
  """
7
8
  from __future__ import annotations
8
9
 
@@ -38,11 +39,9 @@ HOT_PATH_LEAD_RESOURCES = (
38
39
  "convergence",
39
40
  "report-writer",
40
41
  )
41
- WORKER_AGENT_FILES = (
42
- "claude-worker.md",
43
- "report-writer-worker.md",
44
- "translator-worker.md",
45
- )
42
+ # 모든 디스패치 프롬프트 앞머리에 실리는 계약. 종전에는 `~/.claude/agents` 의
43
+ # 에이전트 정의를 셌는데, 그 정의는 더 이상 설치되지 않는다(ADR-0017).
44
+ HOT_PATH_AGENT_CONTRACTS = ("agents/common.json",)
46
45
  TIMESTAMPED_ARTIFACT_RE = re.compile(r"\d{4}-\d{2}-\d{2}[_T]\d{2}-\d{2}-\d{2}")
47
46
 
48
47
 
@@ -136,7 +135,7 @@ def _duty_contract_files(
136
135
  if not isinstance(duty_root_value, str) or not duty_root_value:
137
136
  return []
138
137
  duty_root = project_root / duty_root_value
139
- files = [duty_root / "common.md", duty_root / f"{audience}.md"]
138
+ files = [duty_root / "common.json", duty_root / f"{audience}.json"]
140
139
  return [path for path in files if path.is_file()]
141
140
 
142
141
 
@@ -239,19 +238,15 @@ def _skill_assets_metric() -> dict:
239
238
  instruction footprint and are the prompt-diet ranking input."""
240
239
  entries = []
241
240
  okstra_home = _okstra_asset_home()
242
- claude_home = Path.home() / ".claude"
243
241
  for name in HOT_PATH_LEAD_RESOURCES:
244
242
  path = _installed_or_dev(
245
243
  okstra_home / "prompts" / "lead" / f"{name}.md",
246
244
  f"prompts/lead/{name}.md",
247
245
  )
248
246
  entries.append((f"resource:{name}", path))
249
- for fname in WORKER_AGENT_FILES:
250
- path = _installed_or_dev(
251
- claude_home / "agents" / fname,
252
- f"runtime/agents/workers/{fname}",
253
- )
254
- entries.append((f"agent:{fname}", path))
247
+ for relative in HOT_PATH_AGENT_CONTRACTS:
248
+ path = _installed_or_dev(okstra_home / relative, relative)
249
+ entries.append((f"contract:{relative}", path))
255
250
 
256
251
  files = []
257
252
  for label, path in entries:
@@ -512,7 +507,8 @@ _CLI_EPILOG = r"""Usage:
512
507
 
513
508
  Output: JSON with task file counts, current-run counts, lead Phase 1 read
514
509
  surface, analysis-worker initial read surface, and report-writer synthesis
515
- surface. This is read-only; it never mutates task artifacts.
510
+ surface. Resolving a task may record a finished implementation outcome in its task
511
+ manifest, the task catalog, and consumers.jsonl; nothing else is written.
516
512
  """
517
513
 
518
514
 
@@ -0,0 +1,156 @@
1
+ """진행 중인 run 의 동결 계약을 현재 형식으로 다시 동결한다.
2
+
3
+ run 은 시작할 때 직무·역할·공통 계약을 자기 폴더로 동결하고, 그 다이제스트를
4
+ 매니페스트에 적는다. 계약 형식이 바뀐 릴리스를 설치하면 그 run 의 다이제스트는
5
+ 새 규칙으로 계산한 값과 영영 다르고, 프롬프트를 하나도 더 만들 수 없다 — 실측
6
+ 2026-09-23(fontradar-v2-api dev-10860 implementation-planning-002): 같은 run 에서
7
+ 8회 성공한 뒤 설치가 한 번 일어나자 그다음 materialize 가
8
+ `run duty snapshot catalog digest does not match` 로 멈췄다.
9
+
10
+ 이 모듈은 그 run 을 현재 형식으로 다시 동결하고 매니페스트의 다이제스트와 형식
11
+ 표식을 갱신한다. 계약 본문은 런타임이 소유하므로 새로 쓰는 것이 원래 값이고,
12
+ run 의 증거(프롬프트·결과·원장)는 건드리지 않는다. 사용자가 명시적으로 부를 때만
13
+ 도는 복구 경로다.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ import shutil
19
+ import tempfile
20
+ from dataclasses import dataclass
21
+ from pathlib import Path
22
+ from typing import Any, Mapping
23
+
24
+ from .agent.invocation import digest_duty_catalog
25
+ from .json_boundary import (
26
+ JsonBoundaryError,
27
+ load_owned_object,
28
+ write_owned_object_atomic,
29
+ )
30
+ from .run import (
31
+ CONTRACT_FORMAT_VERSION,
32
+ PrepareError,
33
+ _common_contract_source,
34
+ _duty_catalog_source,
35
+ )
36
+
37
+
38
+ class ContractRefreezeError(RuntimeError):
39
+ """다시 동결할 수 없는 run. 메시지가 그 자리를 이름한다."""
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class RefreezeResult:
44
+ duty_root: str
45
+ previous_digest: str
46
+ digest: str
47
+ previous_format_version: int
48
+ format_version: int
49
+ changed: bool
50
+
51
+
52
+ def frozen_format_version(contract: Mapping[str, Any]) -> int:
53
+ """이 run 이 동결한 계약 형식. 표식이 없는 run 은 1(Markdown 직무)이다."""
54
+ value = contract.get("contractFormatVersion")
55
+ return value if isinstance(value, int) and not isinstance(value, bool) else 1
56
+
57
+
58
+ def refreeze_message(duty_root: str) -> str:
59
+ """형식이 어긋난 run 이 받아야 할 문장 — 원인과 다음 명령."""
60
+ return (
61
+ "run froze an older duty contract format and this install cannot read "
62
+ f"it: {duty_root}. Re-freeze this run's contracts with "
63
+ "`okstra agent-prompt refreeze-contracts --project-root <dir> "
64
+ "--run-manifest <path>`, then repeat the command that failed."
65
+ )
66
+
67
+
68
+ def refreeze_run_contracts(
69
+ *, project_root: Path, run_manifest_path: Path, workspace_root: Path,
70
+ ) -> RefreezeResult:
71
+ """이 run 의 계약 폴더를 현재 런타임 카탈로그로 갈아 끼운다."""
72
+ root = project_root.resolve()
73
+ manifest_path = _resolve(root, run_manifest_path)
74
+ manifest = _load(manifest_path, "run manifest")
75
+ contract = manifest.get("agentContract")
76
+ if not isinstance(contract, dict):
77
+ raise ContractRefreezeError(f"run manifest has no agentContract: {manifest_path}")
78
+ duty_root_rel = str(contract.get("dutyRootPath") or "")
79
+ if not duty_root_rel:
80
+ raise ContractRefreezeError(f"agentContract has no dutyRootPath: {manifest_path}")
81
+ duty_root = _resolve(root, Path(duty_root_rel))
82
+ previous_digest = str(contract.get("catalogDigest") or "")
83
+ previous_format = frozen_format_version(contract)
84
+ try:
85
+ source = _duty_catalog_source(workspace_root)
86
+ common_source = _common_contract_source(workspace_root)
87
+ except PrepareError as exc:
88
+ raise ContractRefreezeError(str(exc)) from exc
89
+ digest = _replace_snapshot(duty_root, source, common_source)
90
+ changed = (
91
+ digest != previous_digest or previous_format != CONTRACT_FORMAT_VERSION
92
+ )
93
+ if changed:
94
+ contract["catalogDigest"] = digest
95
+ contract["contractFormatVersion"] = CONTRACT_FORMAT_VERSION
96
+ _write_manifest(manifest_path, manifest)
97
+ return RefreezeResult(
98
+ duty_root=str(duty_root),
99
+ previous_digest=previous_digest,
100
+ digest=digest,
101
+ previous_format_version=previous_format,
102
+ format_version=CONTRACT_FORMAT_VERSION,
103
+ changed=changed,
104
+ )
105
+
106
+
107
+ def _replace_snapshot(duty_root: Path, source: Path, common_source: Path) -> str:
108
+ """계약 폴더를 통째로 새로 쓴다. 옛 형식 파일은 남기지 않는다.
109
+
110
+ 옛 파일을 남기면 두 형식이 한 폴더에 섞이고, 다이제스트가 세는 대상과 로더가
111
+ 읽는 대상이 다시 갈린다 — 이 결함이 처음 생긴 자리가 그 갈림이다.
112
+ """
113
+ duty_root.parent.mkdir(parents=True, exist_ok=True)
114
+ staged = Path(tempfile.mkdtemp(
115
+ dir=duty_root.parent, prefix=f".{duty_root.name}.", suffix=".refreeze",
116
+ ))
117
+ retired = duty_root.with_name(f".{duty_root.name}.retired")
118
+ try:
119
+ shutil.copytree(source, staged, dirs_exist_ok=True)
120
+ shutil.copyfile(common_source, staged / "common.json")
121
+ shutil.copytree(
122
+ common_source.parent / "roles", staged / "roles", dirs_exist_ok=True
123
+ )
124
+ digest = digest_duty_catalog(staged)
125
+ if duty_root.exists():
126
+ shutil.rmtree(retired, ignore_errors=True)
127
+ os.rename(duty_root, retired)
128
+ os.rename(staged, duty_root)
129
+ except OSError as exc:
130
+ raise ContractRefreezeError(f"cannot re-freeze duty contracts: {exc}") from exc
131
+ finally:
132
+ shutil.rmtree(staged, ignore_errors=True)
133
+ shutil.rmtree(retired, ignore_errors=True)
134
+ return digest
135
+
136
+
137
+ def _resolve(project_root: Path, value: Path) -> Path:
138
+ path = Path(value).expanduser()
139
+ return path if path.is_absolute() else project_root / path
140
+
141
+
142
+ def _load(path: Path, label: str) -> dict[str, Any]:
143
+ try:
144
+ payload = load_owned_object(path, artifact=label)
145
+ except (OSError, JsonBoundaryError) as exc:
146
+ raise ContractRefreezeError(f"cannot read {label} {path}: {exc}") from exc
147
+ if not isinstance(payload, dict):
148
+ raise ContractRefreezeError(f"{label} is not an object: {path}")
149
+ return payload
150
+
151
+
152
+ def _write_manifest(path: Path, manifest: Mapping[str, Any]) -> None:
153
+ try:
154
+ write_owned_object_atomic(path, manifest, artifact="run manifest")
155
+ except JsonBoundaryError as exc:
156
+ raise ContractRefreezeError(f"cannot write run manifest {path}: {exc}") from exc
@@ -849,6 +849,44 @@ def validate_working_state(state: Mapping[str, Any]) -> list[str]:
849
849
  return errors
850
850
 
851
851
 
852
+ def lead_only_final_state(task_key: str) -> dict[str, Any]:
853
+ """워커를 띄우지 않는 phase 의 수렴 최종 상태.
854
+
855
+ 리포트 계약 3.0 은 모든 리포트에 수렴 입력을 요구한다(ADR-0020: 사실마다
856
+ 소유자가 하나). release-handoff 처럼 프로필이 "convergence 를 돌지 않는다"
857
+ 고 선언한 phase 에는 그 파일을 만드는 주체가 없었고, report-finalize 가
858
+ `required input is missing` 으로 멈췄다. 리드는 매 run 마다 seed →
859
+ plan-round → finalize 를 손으로 돌려 빈 상태를 만들어 왔다(실측: dev-10860
860
+ release-handoff 001·002 둘 다).
861
+
862
+ "수렴이 돌지 않았다" 는 부재가 아니라 사실이므로 파일로 남긴다 — 분석자가
863
+ 둘 미만이어서 자동 비활성이라는 뜻이고, 그게 이 어휘의 `auto-disabled` 다.
864
+ """
865
+ state = {
866
+ "schemaVersion": FINAL_SCHEMA_VERSION,
867
+ "taskKey": task_key,
868
+ "config": {
869
+ "enabled": False,
870
+ "adversarial": False,
871
+ "maxRounds": 1,
872
+ "effectiveMaxRounds": 1,
873
+ "verificationMode": "lightweight",
874
+ "autoDisabled": "fewer-than-two-analysers",
875
+ },
876
+ "findings": [],
877
+ "roundHistory": [],
878
+ "round2SkippedReason": "auto-disabled",
879
+ "finalState": "converged",
880
+ "totalRounds": 0,
881
+ "finalClassificationCounts": classification_counts_from_findings([]),
882
+ }
883
+ errors = validate_final_state(state)
884
+ if errors:
885
+ raise ConvergenceContractError(
886
+ "lead-only convergence state is invalid: " + "; ".join(errors))
887
+ return state
888
+
889
+
852
890
  def validate_final_state(state: Mapping[str, Any]) -> list[str]:
853
891
  """Validate historical v1.0-v1.2 and strict current v1.3 artifacts."""
854
892
  errors: list[str] = []
@@ -154,7 +154,13 @@ def worker_result_paths(
154
154
  if candidate.resolve().parent != results_dir.resolve():
155
155
  raise ValueError(f"accepted resultPath is outside worker-results: {candidate}")
156
156
  if not candidate.is_file():
157
- raise ValueError(f"accepted resultPath is missing: {candidate}")
157
+ # 이 모듈의 계약은 "없는 워커 결과는 조용히 건너뛴다" 이고, 그 판정은
158
+ # 아래 `read_canonical_worker_result` 가 None 을 돌려주는 것으로
159
+ # 이루어진다. 여기서 던지면 판정이 아니라 run 이 죽는다 — 실측
160
+ # 2026-09-23(jobs dev-10860 stage 1): 옛 run 의 원장이 ok 로 적은
161
+ # 결과 파일이 디스크에 없어 `report-finalize --only validate-run` 이
162
+ # 예외로 멈췄다. 파일명 규칙 경로를 그대로 두고 넘어간다.
163
+ continue
158
164
  paths[row["workerId"]] = candidate
159
165
  return paths
160
166
 
@@ -20,6 +20,7 @@ from fnmatch import fnmatchcase
20
20
  from pathlib import Path
21
21
  from typing import Any, Iterator
22
22
 
23
+ from .consumers import read_stage_consumer_state
23
24
  from .final_report_schema import load_schema, validate as validate_schema
24
25
  from .json_boundary import JsonBoundaryError, load_owned_object
25
26
  from okstra_project import ResolverError, resolve_project_root
@@ -466,6 +467,7 @@ def materialize_design_prep_requests(plan_data_path: Path) -> list[Path]:
466
467
  # 충돌은 한 항목씩 알리지 않는다. 요청 12개가 모두 옛 리포트의 파일과 부딪히면
467
468
  # 첫 건에서 멈추는 구현은 사용자에게 12회 왕복을 시키고, 그때마다 이미 쓴
468
469
  # 앞쪽 파일이 남아 상태가 절반만 진행된다. 전부 렌더해 전부 검사한 뒤 쓴다.
470
+ done = _done_stages(data_path)
469
471
  rendered = [
470
472
  _render_request(
471
473
  data_path=data_path,
@@ -474,7 +476,7 @@ def materialize_design_prep_requests(plan_data_path: Path) -> list[Path]:
474
476
  item=item,
475
477
  )
476
478
  for item in items
477
- if item.get("status") in _REQUEST_STATUSES
479
+ if item.get("status") in _REQUEST_STATUSES and not _is_implemented(item, done)
478
480
  ]
479
481
  conflicts = [
480
482
  reason
@@ -494,6 +496,37 @@ def materialize_design_prep_requests(plan_data_path: Path) -> list[Path]:
494
496
  return written
495
497
 
496
498
 
499
+ def _done_stages(data_path: Path) -> frozenset[int]:
500
+ """이 태스크에서 이미 구현이 끝난 stage.
501
+
502
+ 원장을 못 읽으면 빈 집합이다 — 그때의 동작은 이 판정이 없던 때와 같고,
503
+ "모르니까 건너뛴다" 는 반대 방향의 실패보다 안전하다.
504
+ """
505
+ try:
506
+ state = read_stage_consumer_state(data_path.parent.parent)
507
+ except (OSError, UnicodeError, ValueError):
508
+ return frozenset()
509
+ return frozenset(state.done_stages)
510
+
511
+
512
+ def _is_implemented(item: Mapping[str, Any], done: frozenset[int]) -> bool:
513
+ """이 항목이 가리키는 stage 가 전부 구현 완료인가.
514
+
515
+ 구현이 끝난 stage 의 계획은 역사이지 계획이 아니다. 그런데 탐지기는 매
516
+ 계획 run 마다 이월된 stage 본문을 다시 훑고, 그 사이 선택 방향의
517
+ `fileStructure` 가 바뀌면 같은 PREP 항목이 다른 증거로 렌더된다. 요청
518
+ 파일은 태스크 단위 이름이라 불변 파일과 충돌하고, report-finalize 는
519
+ 빠져나갈 길 없이 멈춘다(2026-09-24, dev-10860: 9건). 이미 지어진 것에
520
+ 설계 준비를 다시 요구하는 것이므로, 답도 쓸모가 없다.
521
+ """
522
+ stages = [
523
+ stage
524
+ for stage in item.get("stageRefs") or ()
525
+ if isinstance(stage, int) and not isinstance(stage, bool)
526
+ ]
527
+ return bool(stages) and all(stage in done for stage in stages)
528
+
529
+
497
530
  def load_design_prep_items(report_path: Path) -> tuple[dict[str, Any], ...]:
498
531
  """Return validated snapshot items without exposing mutable report data."""
499
532
  data_path = _canonical_data_path(report_path)