okstra 0.179.2 → 0.183.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/README.md +1 -1
  2. package/dist/cli-registry.mjs +30 -2
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/dist/commands/execute/incremental-carry.mjs +9 -8
  5. package/dist/commands/execute/incremental-carry.mjs.map +1 -1
  6. package/dist/commands/execute/plan-verify.mjs +3 -1
  7. package/dist/commands/execute/plan-verify.mjs.map +1 -1
  8. package/dist/commands/execute/render-bundle.d.mts +4 -2
  9. package/dist/commands/execute/render-bundle.mjs +46 -5
  10. package/dist/commands/execute/render-bundle.mjs.map +1 -1
  11. package/dist/commands/execute/run.mjs +11 -3
  12. package/dist/commands/execute/run.mjs.map +1 -1
  13. package/dist/commands/inspect/model-io.d.mts +1 -0
  14. package/dist/commands/inspect/model-io.mjs +25 -0
  15. package/dist/commands/inspect/model-io.mjs.map +1 -0
  16. package/dist/commands/inspect/stage-map.mjs +29 -8
  17. package/dist/commands/inspect/stage-map.mjs.map +1 -1
  18. package/dist/commands/inspect/task-list.mjs +52 -6
  19. package/dist/commands/inspect/task-list.mjs.map +1 -1
  20. package/dist/commands/inspect/user-response.mjs +14 -4
  21. package/dist/commands/inspect/user-response.mjs.map +1 -1
  22. package/dist/commands/lifecycle/check-project.d.mts +1 -0
  23. package/dist/commands/lifecycle/check-project.mjs +69 -50
  24. package/dist/commands/lifecycle/check-project.mjs.map +1 -1
  25. package/dist/commands/lifecycle/contract-check.d.mts +1 -0
  26. package/dist/commands/lifecycle/contract-check.mjs +18 -0
  27. package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
  28. package/dist/commands/lifecycle/preflight.mjs +154 -51
  29. package/dist/commands/lifecycle/preflight.mjs.map +1 -1
  30. package/dist/commands/pr/pr.d.mts +1 -0
  31. package/dist/commands/pr/pr.mjs +19 -1
  32. package/dist/commands/pr/pr.mjs.map +1 -1
  33. package/dist/commands/report/agent-activity.mjs +2 -2
  34. package/dist/commands/report/approval-decision.d.mts +1 -0
  35. package/dist/commands/report/approval-decision.mjs +21 -0
  36. package/dist/commands/report/approval-decision.mjs.map +1 -0
  37. package/dist/commands/report/design-snapshot.d.mts +1 -0
  38. package/dist/commands/report/design-snapshot.mjs +19 -0
  39. package/dist/commands/report/design-snapshot.mjs.map +1 -0
  40. package/dist/commands/report/translate.mjs +3 -0
  41. package/dist/commands/report/translate.mjs.map +1 -1
  42. package/dist/lib/host-registry-client.mjs +13 -9
  43. package/dist/lib/host-registry-client.mjs.map +1 -1
  44. package/docs/architecture/storage-model.md +1 -1
  45. package/docs/architecture.md +21 -10
  46. package/docs/cli.md +36 -20
  47. package/docs/container.md +6 -4
  48. package/docs/contributor-change-matrix.md +1 -1
  49. package/docs/for-ai/README.md +2 -2
  50. package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
  51. package/docs/for-ai/skills/okstra-code-review.md +4 -4
  52. package/docs/for-ai/skills/okstra-container-build.md +20 -17
  53. package/docs/for-ai/skills/okstra-inspect.md +20 -23
  54. package/docs/for-ai/skills/okstra-manager.md +19 -18
  55. package/docs/for-ai/skills/okstra-memory.md +2 -2
  56. package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
  57. package/docs/for-ai/skills/okstra-rollup.md +14 -13
  58. package/docs/for-ai/skills/okstra-run.md +7 -3
  59. package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
  60. package/docs/for-ai/skills/okstra-setup.md +7 -7
  61. package/docs/for-ai/skills/okstra-usage.md +5 -4
  62. package/docs/for-ai/skills/okstra-user-response.md +50 -32
  63. package/docs/project-structure-overview.md +43 -31
  64. package/docs/task-process/implementation-planning.md +2 -2
  65. package/docs/task-process/implementation.md +7 -4
  66. package/package.json +1 -1
  67. package/runtime/BUILD.json +2 -2
  68. package/runtime/agents/workers/claude-worker.md +4 -4
  69. package/runtime/agents/workers/report-writer-worker.md +15 -164
  70. package/runtime/agents/workers/translator-worker.md +5 -13
  71. package/runtime/bin/okstra-error-log.py +51 -11
  72. package/runtime/bin/okstra-report-translate.py +210 -23
  73. package/runtime/prompts/host-orchestration/implementation.md +1 -1
  74. package/runtime/prompts/launch.template.md +9 -12
  75. package/runtime/prompts/lead/adapters/cmux.md +1 -1
  76. package/runtime/prompts/lead/context-loader.md +41 -141
  77. package/runtime/prompts/lead/convergence.md +10 -8
  78. package/runtime/prompts/lead/okstra-lead-contract.md +40 -49
  79. package/runtime/prompts/lead/plan-body-verification.md +217 -23
  80. package/runtime/prompts/lead/report-writer.md +81 -421
  81. package/runtime/prompts/lead/team-contract.md +9 -54
  82. package/runtime/prompts/profiles/_clarification-recommendation.md +5 -4
  83. package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
  84. package/runtime/prompts/profiles/_common-contract.md +4 -4
  85. package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
  86. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  87. package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
  88. package/runtime/prompts/profiles/change-impact-analysis.md +1 -1
  89. package/runtime/prompts/profiles/error-analysis.md +1 -1
  90. package/runtime/prompts/profiles/feature-analysis.md +1 -1
  91. package/runtime/prompts/profiles/final-verification.md +1 -1
  92. package/runtime/prompts/profiles/implementation-planning.md +20 -12
  93. package/runtime/prompts/profiles/improvement-discovery.md +1 -1
  94. package/runtime/prompts/profiles/project-analysis.md +1 -1
  95. package/runtime/prompts/profiles/release-handoff.md +0 -1
  96. package/runtime/prompts/profiles/requirements-discovery.md +1 -1
  97. package/runtime/prompts/wizard/prompts.ko.json +0 -1
  98. package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
  99. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
  100. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +8 -2
  101. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
  102. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
  103. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
  104. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
  105. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
  106. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
  107. package/runtime/python/okstra_ctl/agent_activity.py +141 -38
  108. package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
  109. package/runtime/python/okstra_ctl/agent_prompt_cli.py +70 -23
  110. package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
  111. package/runtime/python/okstra_ctl/analysis_packet.py +124 -3
  112. package/runtime/python/okstra_ctl/approval_decisions.py +328 -0
  113. package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
  114. package/runtime/python/okstra_ctl/backfill.py +13 -10
  115. package/runtime/python/okstra_ctl/batch.py +2 -4
  116. package/runtime/python/okstra_ctl/build_tools.py +6 -3
  117. package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
  118. package/runtime/python/okstra_ctl/clarification_items.py +27 -13
  119. package/runtime/python/okstra_ctl/cmux.py +130 -52
  120. package/runtime/python/okstra_ctl/code_review_target.py +34 -8
  121. package/runtime/python/okstra_ctl/consumers.py +5 -4
  122. package/runtime/python/okstra_ctl/container.py +103 -8
  123. package/runtime/python/okstra_ctl/context_cost.py +2 -1
  124. package/runtime/python/okstra_ctl/contract_graph.py +497 -0
  125. package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
  126. package/runtime/python/okstra_ctl/convergence.py +338 -17
  127. package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
  128. package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
  129. package/runtime/python/okstra_ctl/convergence_store.py +55 -34
  130. package/runtime/python/okstra_ctl/design_prep.py +7 -4
  131. package/runtime/python/okstra_ctl/design_snapshot.py +134 -0
  132. package/runtime/python/okstra_ctl/dispatch_core.py +97 -69
  133. package/runtime/python/okstra_ctl/dispatch_state.py +163 -63
  134. package/runtime/python/okstra_ctl/doctor.py +6 -3
  135. package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
  136. package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
  137. package/runtime/python/okstra_ctl/error_log_write.py +35 -30
  138. package/runtime/python/okstra_ctl/error_report.py +26 -1
  139. package/runtime/python/okstra_ctl/error_zip.py +27 -5
  140. package/runtime/python/okstra_ctl/execution_identity.py +3 -2
  141. package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
  142. package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -2
  143. package/runtime/python/okstra_ctl/final_report_schema.py +26 -17
  144. package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
  145. package/runtime/python/okstra_ctl/fixed_text.py +39 -0
  146. package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
  147. package/runtime/python/okstra_ctl/handoff.py +5 -4
  148. package/runtime/python/okstra_ctl/i18n.py +4 -2
  149. package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
  150. package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
  151. package/runtime/python/okstra_ctl/incremental_carry.py +130 -17
  152. package/runtime/python/okstra_ctl/incremental_scope.py +12 -4
  153. package/runtime/python/okstra_ctl/index.py +8 -11
  154. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
  155. package/runtime/python/okstra_ctl/invocation.py +3 -6
  156. package/runtime/python/okstra_ctl/json_boundary.py +366 -0
  157. package/runtime/python/okstra_ctl/json_registry.py +10 -12
  158. package/runtime/python/okstra_ctl/jsonl.py +19 -2
  159. package/runtime/python/okstra_ctl/lead_events.py +33 -1
  160. package/runtime/python/okstra_ctl/listing.py +3 -3
  161. package/runtime/python/okstra_ctl/log_report.py +24 -2
  162. package/runtime/python/okstra_ctl/manager_cli.py +92 -7
  163. package/runtime/python/okstra_ctl/manager_store.py +12 -10
  164. package/runtime/python/okstra_ctl/material.py +5 -1
  165. package/runtime/python/okstra_ctl/migrate.py +29 -25
  166. package/runtime/python/okstra_ctl/model_cli.py +3 -15
  167. package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
  168. package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
  169. package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
  170. package/runtime/python/okstra_ctl/path_hints.py +12 -0
  171. package/runtime/python/okstra_ctl/paths.py +21 -0
  172. package/runtime/python/okstra_ctl/plan_items.py +115 -4
  173. package/runtime/python/okstra_ctl/plan_items_cli.py +626 -34
  174. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +2 -1
  175. package/runtime/python/okstra_ctl/pr_template.py +3 -2
  176. package/runtime/python/okstra_ctl/project_meta.py +5 -7
  177. package/runtime/python/okstra_ctl/recap.py +5 -4
  178. package/runtime/python/okstra_ctl/reconcile.py +21 -27
  179. package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
  180. package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
  181. package/runtime/python/okstra_ctl/render.py +74 -16
  182. package/runtime/python/okstra_ctl/render_final_report.py +10 -8
  183. package/runtime/python/okstra_ctl/report_assembly.py +509 -0
  184. package/runtime/python/okstra_ctl/report_contract.py +2 -1
  185. package/runtime/python/okstra_ctl/report_finalize.py +67 -32
  186. package/runtime/python/okstra_ctl/report_html/render.py +3 -2
  187. package/runtime/python/okstra_ctl/report_inputs.py +72 -0
  188. package/runtime/python/okstra_ctl/report_language.py +3 -2
  189. package/runtime/python/okstra_ctl/report_markdown.py +81 -8
  190. package/runtime/python/okstra_ctl/report_narrative.py +351 -0
  191. package/runtime/python/okstra_ctl/report_projections.py +265 -0
  192. package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
  193. package/runtime/python/okstra_ctl/report_views.py +3 -2
  194. package/runtime/python/okstra_ctl/rollup.py +65 -4
  195. package/runtime/python/okstra_ctl/run.py +184 -65
  196. package/runtime/python/okstra_ctl/run_audit.py +3 -2
  197. package/runtime/python/okstra_ctl/run_context.py +6 -9
  198. package/runtime/python/okstra_ctl/run_index_row.py +2 -8
  199. package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
  200. package/runtime/python/okstra_ctl/schema_excerpt.py +15 -8
  201. package/runtime/python/okstra_ctl/session_transcript.py +27 -1
  202. package/runtime/python/okstra_ctl/set_work_status.py +64 -38
  203. package/runtime/python/okstra_ctl/stage_fix_carry.py +8 -6
  204. package/runtime/python/okstra_ctl/stage_ledger.py +132 -18
  205. package/runtime/python/okstra_ctl/stage_map.py +96 -28
  206. package/runtime/python/okstra_ctl/stage_targets.py +3 -4
  207. package/runtime/python/okstra_ctl/team.py +3 -2
  208. package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
  209. package/runtime/python/okstra_ctl/time_report.py +51 -4
  210. package/runtime/python/okstra_ctl/usage_identity.py +2 -1
  211. package/runtime/python/okstra_ctl/usage_report.py +58 -4
  212. package/runtime/python/okstra_ctl/user_response.py +1431 -66
  213. package/runtime/python/okstra_ctl/wizard.py +11 -44
  214. package/runtime/python/okstra_ctl/work_categories.py +3 -2
  215. package/runtime/python/okstra_ctl/worker_dispatch.py +5 -2
  216. package/runtime/python/okstra_ctl/worker_prompt_body.py +49 -3
  217. package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
  218. package/runtime/python/okstra_ctl/worker_prompt_policy.py +31 -3
  219. package/runtime/python/okstra_ctl/worker_runner.py +14 -12
  220. package/runtime/python/okstra_ctl/workflow.py +2 -1
  221. package/runtime/python/okstra_ctl/worktree.py +3 -2
  222. package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
  223. package/runtime/python/okstra_ctl/write_policy.py +4 -2
  224. package/runtime/python/okstra_token_usage/antigravity.py +39 -12
  225. package/runtime/python/okstra_token_usage/collect.py +90 -38
  226. package/runtime/python/okstra_token_usage/grok.py +127 -0
  227. package/runtime/schemas/final-report-v2.0.schema.json +9 -0
  228. package/runtime/schemas/final-report-v3.0.schema.json +10219 -0
  229. package/runtime/schemas/report-narrative-v3.0.schema.json +30 -0
  230. package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
  231. package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
  232. package/runtime/skills/okstra-code-review/SKILL.md +21 -11
  233. package/runtime/skills/okstra-container-build/SKILL.md +18 -18
  234. package/runtime/skills/okstra-inspect/SKILL.md +12 -11
  235. package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
  236. package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
  237. package/runtime/skills/okstra-inspect/facets/history.md +9 -14
  238. package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
  239. package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
  240. package/runtime/skills/okstra-inspect/facets/report.md +6 -10
  241. package/runtime/skills/okstra-inspect/facets/status.md +9 -8
  242. package/runtime/skills/okstra-inspect/facets/time.md +3 -3
  243. package/runtime/skills/okstra-manager/SKILL.md +16 -14
  244. package/runtime/skills/okstra-memory/SKILL.md +3 -3
  245. package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
  246. package/runtime/skills/okstra-rollup/SKILL.md +6 -16
  247. package/runtime/skills/okstra-run/SKILL.md +7 -7
  248. package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
  249. package/runtime/skills/okstra-setup/SKILL.md +21 -13
  250. package/runtime/skills/okstra-setup/references/project-config.md +2 -2
  251. package/runtime/skills/okstra-usage/SKILL.md +10 -10
  252. package/runtime/skills/okstra-user-response/SKILL.md +78 -107
  253. package/runtime/templates/report-writer-prompt-preamble.md +29 -19
  254. package/runtime/templates/reports/html/macros/forms.html +6 -4
  255. package/runtime/templates/reports/schedule.template.md +4 -4
  256. package/runtime/templates/worker-error-contract.md +17 -29
  257. package/runtime/validators/validate-run.py +636 -72
  258. package/runtime/validators/validate_session_conformance.py +43 -7
@@ -1,10 +1,11 @@
1
1
  """Phase 7 report post-processing — the single reference point.
2
2
 
3
- Phase 7 turns a Phase 6 final-report data.json into shippable artifacts through
4
- six ordered steps: activity projection, English-SSOT verification, usage
5
- substitution, html view rendering, follow-up task spawning, and run validation.
6
- The order is load-bearing rendering before substitution ships `--` token
7
- cells, and validating before rendering trips the report-views contract.
3
+ Contract 3.0 collects usage into team state, assembles the role-owned inputs
4
+ into final-report data.json once, then verifies and renders that published
5
+ record. Contract 2.0 keeps its historical in-place projection sequence for
6
+ read-only compatibility. The order is load-bearing: rendering before assembly
7
+ or usage collection publishes stale derived views, and validating before
8
+ rendering trips the report-views contract.
8
9
 
9
10
  `token-usage` is the one step whose failure does not stop the sequence, which
10
11
  deliberately accepts that first state: its input is the lead session log, so a
@@ -39,10 +40,12 @@ from typing import Any, Callable, Mapping, Sequence
39
40
 
40
41
  from .agent_activity import ActivityProjectionError, project_agent_activity
41
42
  from .report_contract import apply_execution_roles
43
+ from .report_assembly import ReportAssemblyError, assemble_report
42
44
  from .dispatch_state import DispatchError, link_agent_dispatch_result
43
45
  from .final_report_paths import final_report_data_path, final_report_markdown_path
44
46
  from .paths import task_dir, task_manifest_file
45
47
  from .release_gate import release_handoff_allowed
48
+ from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
46
49
  from .stage_integrate import IntegrateError
47
50
  from .stage_targets import (
48
51
  StageTargetError,
@@ -74,6 +77,16 @@ STEP_ORDER = (
74
77
  STEP_TEARDOWN_STAGES,
75
78
  )
76
79
 
80
+ V3_STEP_ORDER = (
81
+ STEP_TOKEN_USAGE,
82
+ STEP_PROJECT_ACTIVITY,
83
+ STEP_CHECK_SOURCE,
84
+ STEP_RENDER_VIEWS,
85
+ STEP_SPAWN_FOLLOWUPS,
86
+ STEP_VALIDATE_RUN,
87
+ STEP_TEARDOWN_STAGES,
88
+ )
89
+
77
90
 
78
91
  class FinalizeError(Exception):
79
92
  """Raised when the Phase 7 sequence cannot be assembled."""
@@ -226,6 +239,7 @@ class FinalizeContext:
226
239
  task_id: str
227
240
  seq: str
228
241
  final_status_path: Path | None
242
+ report_contract_version: str = "2.0"
229
243
 
230
244
  @property
231
245
  def markdown_path(self) -> Path:
@@ -257,13 +271,16 @@ class FinalizeContext:
257
271
  final_status_path=resolve_optional_path(
258
272
  project_root, manifest.get("finalStatusPath")
259
273
  ),
274
+ report_contract_version=str(
275
+ manifest.get("reportContractVersion") or "2.0"
276
+ ),
260
277
  )
261
278
 
262
279
 
263
280
  def build_commands(ctx: FinalizeContext) -> list[tuple[str, list[str]]]:
264
281
  """Assemble the ordered Phase 7 argv list. Order is contractual."""
265
282
  markdown_path = ctx.markdown_path
266
- return [
283
+ commands = [
267
284
  (
268
285
  STEP_PROJECT_ACTIVITY,
269
286
  [
@@ -344,6 +361,16 @@ def build_commands(ctx: FinalizeContext) -> list[tuple[str, list[str]]]:
344
361
  ["<in-process>", "teardown-stages", str(ctx.data_path)],
345
362
  ),
346
363
  ]
364
+ if ctx.report_contract_version != "3.0":
365
+ return commands
366
+ by_name = dict(commands)
367
+ usage = by_name[STEP_TOKEN_USAGE]
368
+ marker = usage.index("--substitute-data")
369
+ by_name[STEP_TOKEN_USAGE] = usage[:marker]
370
+ by_name[STEP_PROJECT_ACTIVITY] = [
371
+ "<in-process>", "report-assembly", str(ctx.manifest_path), str(ctx.data_path)
372
+ ]
373
+ return [(name, by_name[name]) for name in V3_STEP_ORDER]
347
374
 
348
375
 
349
376
  def _teardown_stage_worktrees(
@@ -363,8 +390,8 @@ def _teardown_stage_worktrees(
363
390
  if ctx.task_type != "final-verification":
364
391
  return _done({"skipped": "not a final-verification run"})
365
392
  try:
366
- data = json.loads(Path(ctx.data_path).read_text(encoding="utf-8"))
367
- except (OSError, json.JSONDecodeError) as exc:
393
+ data = load_owned_object(Path(ctx.data_path), artifact="final report record")
394
+ except JsonBoundaryError as exc:
368
395
  return subprocess.CompletedProcess(
369
396
  command, 1, "", f"cannot read final-report data.json: {exc}")
370
397
  if data.get("verificationScope") != "whole-task":
@@ -441,14 +468,16 @@ def run_finalize(
441
468
  steps: list[dict[str, Any]] = []
442
469
  deferred = ""
443
470
  try:
444
- write_execution_roles(ctx)
471
+ if ctx.report_contract_version != "3.0":
472
+ write_execution_roles(ctx)
445
473
  commands = build_commands(ctx)
446
474
  except FinalizeError as exc:
447
475
  return {"ok": False, "reason": str(exc), "steps": steps}
448
476
 
449
477
  if only:
450
478
  selected = set(only)
451
- unknown = sorted(selected - set(STEP_ORDER))
479
+ contract_order = V3_STEP_ORDER if ctx.report_contract_version == "3.0" else STEP_ORDER
480
+ unknown = sorted(selected - set(contract_order))
452
481
  if unknown:
453
482
  return {
454
483
  "ok": False,
@@ -462,12 +491,17 @@ def run_finalize(
462
491
  before_step(name)
463
492
  if name == STEP_PROJECT_ACTIVITY:
464
493
  try:
465
- rows = project_agent_activity(
466
- ctx.project_root,
467
- ctx.manifest_path,
468
- ctx.data_path,
469
- )
470
- except ActivityProjectionError as exc:
494
+ if ctx.report_contract_version == "3.0":
495
+ assembled = assemble_report(ctx.project_root, ctx.manifest_path)
496
+ count = len(assembled.get("agentActivity") or [])
497
+ else:
498
+ rows = project_agent_activity(
499
+ ctx.project_root,
500
+ ctx.manifest_path,
501
+ ctx.data_path,
502
+ )
503
+ count = len(rows)
504
+ except (ActivityProjectionError, ReportAssemblyError) as exc:
471
505
  result = subprocess.CompletedProcess(
472
506
  command,
473
507
  1,
@@ -478,7 +512,7 @@ def run_finalize(
478
512
  result = subprocess.CompletedProcess(
479
513
  command,
480
514
  0,
481
- json.dumps({"count": len(rows)}),
515
+ json.dumps({"count": count}),
482
516
  "",
483
517
  )
484
518
  else:
@@ -530,9 +564,7 @@ def write_execution_roles(ctx: FinalizeContext) -> None:
530
564
  updated = apply_execution_roles(dict(data), dict(manifest))
531
565
  if updated == data:
532
566
  return
533
- tmp = ctx.data_path.with_suffix(ctx.data_path.suffix + ".tmp")
534
- tmp.write_text(json.dumps(updated, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
535
- tmp.replace(ctx.data_path)
567
+ write_owned_object_atomic(ctx.data_path, updated, artifact="final report record")
536
568
 
537
569
 
538
570
  def _link_lead_result_for_validation(ctx: FinalizeContext) -> None:
@@ -571,11 +603,9 @@ def _link_lead_result_for_validation(ctx: FinalizeContext) -> None:
571
603
 
572
604
  def _load_manifest(path: Path) -> Mapping[str, Any]:
573
605
  try:
574
- payload = json.loads(path.read_text(encoding="utf-8"))
575
- except FileNotFoundError as exc:
576
- raise FinalizeError(f"run manifest not found: {path}") from exc
577
- except json.JSONDecodeError as exc:
578
- raise FinalizeError(f"run manifest is not valid JSON: {path} ({exc})") from exc
606
+ payload = load_owned_object(path, artifact="run manifest")
607
+ except JsonBoundaryError as exc:
608
+ raise FinalizeError(f"run manifest is not valid: {path} ({exc})") from exc
579
609
  if not isinstance(payload, Mapping):
580
610
  raise FinalizeError(f"run manifest is not a JSON object: {path}")
581
611
  return payload
@@ -633,7 +663,9 @@ def _parser() -> argparse.ArgumentParser:
633
663
  return parser
634
664
 
635
665
 
636
- def _step_summary_lines(result: Mapping[str, Any]) -> list[str]:
666
+ def _step_summary_lines(
667
+ result: Mapping[str, Any], order: Sequence[str] = STEP_ORDER,
668
+ ) -> list[str]:
637
669
  """One human-readable line per step, so the outcome is legible without
638
670
  parsing the JSON payload."""
639
671
  lines = []
@@ -641,13 +673,15 @@ def _step_summary_lines(result: Mapping[str, Any]) -> list[str]:
641
673
  code = step.get("exitCode")
642
674
  mark = "ok " if code == 0 else "FAIL"
643
675
  lines.append(f" [{mark}] {step.get('name')} (exit {code})")
644
- for name in STEP_ORDER:
676
+ for name in order:
645
677
  if not any(s.get("name") == name for s in (result.get("steps") or [])):
646
678
  lines.append(f" [skip] {name}")
647
679
  return lines
648
680
 
649
681
 
650
- def _recovery_step_names(result: Mapping[str, Any]) -> list[str]:
682
+ def _recovery_step_names(
683
+ result: Mapping[str, Any], order: Sequence[str] = STEP_ORDER,
684
+ ) -> list[str]:
651
685
  """The steps a retry has to re-run: every step from the earliest failure on.
652
686
 
653
687
  Naming only the failed steps would prescribe half a recovery. `token-usage`
@@ -664,9 +698,9 @@ def _recovery_step_names(result: Mapping[str, Any]) -> list[str]:
664
698
  for step in (result.get("steps") or [])
665
699
  if step.get("exitCode") != 0
666
700
  }
667
- for index, name in enumerate(STEP_ORDER):
701
+ for index, name in enumerate(order):
668
702
  if name in failed:
669
- return list(STEP_ORDER[index:])
703
+ return list(order[index:])
670
704
  return []
671
705
 
672
706
 
@@ -682,13 +716,14 @@ def main(argv: Sequence[str] | None = None) -> int:
682
716
  # sequence because the token-usage step below reads `leadSessionIds`.
683
717
  observe_lead_session(ctx.project_root, ctx.team_state_path)
684
718
  result = run_finalize(ctx, only=args.only or None)
719
+ order = V3_STEP_ORDER if ctx.report_contract_version == "3.0" else STEP_ORDER
685
720
  print(json.dumps(result, indent=2, ensure_ascii=False))
686
721
  print("finalize steps:", file=sys.stderr)
687
- for line in _step_summary_lines(result):
722
+ for line in _step_summary_lines(result, order):
688
723
  print(line, file=sys.stderr)
689
724
  if not result["ok"]:
690
725
  print(f"error: {result['reason']}", file=sys.stderr)
691
- recovery = _recovery_step_names(result)
726
+ recovery = _recovery_step_names(result, order)
692
727
  if recovery:
693
728
  flags = " ".join(f"--only {name}" for name in recovery)
694
729
  print(
@@ -15,6 +15,7 @@ from ..i18n import HTML_DICTIONARY_REL, load_dictionary, make_jinja_global
15
15
  from ..report_translation import overlay
16
16
  from ..report_view_artifacts import user_responses_dir_for_report
17
17
  from ..usage_cells import format_duration_ms
18
+ from ..json_boundary import load_owned_object
18
19
  from .common import anchor_index
19
20
  from .filters import (
20
21
  code_evidence,
@@ -87,7 +88,7 @@ def _localize(data: dict, data_path: Path) -> tuple[dict, str]:
87
88
  "rendering the English source\n"
88
89
  )
89
90
  return data, lang
90
- payload = json.loads(sidecar_file.read_text(encoding="utf-8"))
91
+ payload = load_owned_object(sidecar_file, artifact="translation sidecar")
91
92
  strings = payload.get("strings")
92
93
  if not isinstance(strings, dict):
93
94
  raise HtmlRenderError(f"translation sidecar has no 'strings' object: {sidecar_file}")
@@ -120,7 +121,7 @@ def render_v2_html_view(
120
121
  no reader ever compared, which made a derived artifact a precondition for
121
122
  another derived artifact.
122
123
  """
123
- data = json.loads(data_path.read_text(encoding="utf-8"))
124
+ data = load_owned_object(data_path, artifact="final report record")
124
125
  errors = validate(data, load_schema_for_data(data))
125
126
  if errors:
126
127
  raise HtmlRenderError("invalid v2 final-report data: " + "; ".join(errors[:5]))
@@ -0,0 +1,72 @@
1
+ """리포트 계약 3.0의 역할별 입력 경로와 단일 소유자 레지스트리."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Any, Mapping
7
+
8
+
9
+ REPORT_CONTRACT_V3 = "3.0"
10
+
11
+
12
+ class ReportInputError(ValueError):
13
+ """3.0 입력 경로나 소유자가 매니페스트에서 해소되지 않았다."""
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class ReportInputPath:
18
+ key: str
19
+ owner: str
20
+ path: Path
21
+
22
+
23
+ _INPUT_FIELDS = (
24
+ ("narrative", "report-writer", "reportNarrativePath"),
25
+ ("approval-decisions", "lead", "approvalDecisionsPath"),
26
+ ("agent-activity", "activity-ledger", "leadEventsPath"),
27
+ ("execution-status", "team-state", "teamStatePath"),
28
+ ("convergence", "convergence", "convergenceStatePath"),
29
+ )
30
+
31
+ _PLANNING_INPUT_FIELDS = (
32
+ ("design-preparation", "design-surface-detector", "designPreparationPath"),
33
+ ("plan-body-verification", "convergence", "planBodyVerificationPath"),
34
+ )
35
+
36
+
37
+ def uses_report_contract_v3(manifest: Mapping[str, Any]) -> bool:
38
+ return str(manifest.get("reportContractVersion") or "").strip() == REPORT_CONTRACT_V3
39
+
40
+
41
+ def _project_path(project_root: Path, value: object, field: str) -> Path:
42
+ if not isinstance(value, str) or not value.strip():
43
+ raise ReportInputError(f"report contract 3.0 requires {field}")
44
+ path = Path(value)
45
+ return path if path.is_absolute() else project_root / path
46
+
47
+
48
+ def report_narrative_path(
49
+ project_root: Path, manifest: Mapping[str, Any],
50
+ ) -> Path:
51
+ if not uses_report_contract_v3(manifest):
52
+ raise ReportInputError("report narrative path belongs to report contract 3.0")
53
+ return _project_path(project_root, manifest.get("reportNarrativePath"), "reportNarrativePath")
54
+
55
+
56
+ def report_input_paths(
57
+ project_root: Path, manifest: Mapping[str, Any],
58
+ ) -> tuple[ReportInputPath, ...]:
59
+ if not uses_report_contract_v3(manifest):
60
+ raise ReportInputError("role-owned report inputs require report contract 3.0")
61
+ fields = _INPUT_FIELDS + (
62
+ _PLANNING_INPUT_FIELDS
63
+ if manifest.get("taskType") == "implementation-planning"
64
+ else ()
65
+ )
66
+ rows = tuple(
67
+ ReportInputPath(key, owner, _project_path(project_root, manifest.get(field), field))
68
+ for key, owner, field in fields
69
+ )
70
+ if len({row.key for row in rows}) != len(rows):
71
+ raise ReportInputError("report input keys must be unique")
72
+ return rows
@@ -12,6 +12,7 @@ from pathlib import Path
12
12
  from typing import Any, Mapping
13
13
 
14
14
  from .worker_prompt_body import instruction_path
15
+ from .json_boundary import JsonBoundaryError, load_owned_object
15
16
 
16
17
  REPORT_LANGUAGE_VALUES = {"en", "ko", "auto"}
17
18
 
@@ -50,8 +51,8 @@ def _read_report_language(path: Path, label: str) -> str:
50
51
  if not path.is_file():
51
52
  return ""
52
53
  try:
53
- payload = json.loads(path.read_text(encoding="utf-8"))
54
- except (OSError, json.JSONDecodeError) as exc:
54
+ payload = load_owned_object(path, artifact=f"{label} configuration")
55
+ except JsonBoundaryError as exc:
55
56
  raise ReportLanguageError(f"{label} config is unreadable: {path}") from exc
56
57
  if not isinstance(payload, Mapping):
57
58
  raise ReportLanguageError(f"{label} config must be a JSON object: {path}")
@@ -95,6 +95,17 @@ def humanise(key: str) -> str:
95
95
  )
96
96
 
97
97
 
98
+ def label_key(label: str) -> str:
99
+ """라벨 대조용 정규화 표기.
100
+
101
+ `humanise` 는 `_ACRONYMS` 표에 있는 낱말만 대문자로 올린다(`ticketId` ->
102
+ `Ticket ID`). 그 표는 저작자에게 전달되지 않으므로 `Ticket Id` 라고 쓰면
103
+ 글자 하나 때문에 리포트 전체가 되돌아온다. 대소문자와 낱말 구분 문자를
104
+ 지운 표기로 맞춰, 약어 표를 모르고도 쓸 수 있게 한다.
105
+ """
106
+ return re.sub(r"[^a-z0-9]", "", str(label).lower())
107
+
108
+
98
109
  class SchemaIndex:
99
110
  """Property-order lookup over the final-report schema.
100
111
 
@@ -105,6 +116,7 @@ class SchemaIndex:
105
116
 
106
117
  def __init__(self, schema: Any) -> None:
107
118
  self._defs = schema.get("$defs", {}) if isinstance(schema, dict) else {}
119
+ self._schema = schema if isinstance(schema, dict) else {}
108
120
 
109
121
  def resolve(self, node: Any) -> dict:
110
122
  seen: set[str] = set()
@@ -118,10 +130,21 @@ class SchemaIndex:
118
130
 
119
131
  def _branches(self, node: Any) -> list[dict]:
120
132
  resolved = self.resolve(node)
121
- branches = [resolved]
122
- for keyword in ("allOf", "oneOf", "anyOf"):
123
- for branch in resolved.get(keyword) or ():
124
- branches.append(self.resolve(branch))
133
+ branches: list[dict] = []
134
+ pending = [resolved]
135
+ seen: set[int] = set()
136
+ while pending:
137
+ branch = pending.pop(0)
138
+ identity = id(branch)
139
+ if identity in seen:
140
+ continue
141
+ seen.add(identity)
142
+ branches.append(branch)
143
+ for keyword in ("allOf", "oneOf", "anyOf"):
144
+ pending.extend(
145
+ self.resolve(candidate)
146
+ for candidate in branch.get(keyword) or ()
147
+ )
125
148
  return branches
126
149
 
127
150
  def key_order(self, node: Any) -> list[str]:
@@ -133,17 +156,67 @@ class SchemaIndex:
133
156
  return order
134
157
 
135
158
  def child(self, node: Any, key: str) -> dict:
159
+ candidates: list[dict] = []
136
160
  for branch in self._branches(node):
137
161
  candidate = (branch.get("properties") or {}).get(key)
138
162
  if candidate is not None:
139
- return self.resolve(candidate)
140
- return {}
163
+ candidates.append(self.resolve(candidate))
164
+ return max(candidates, key=_schema_detail, default={})
141
165
 
142
166
  def item(self, node: Any) -> dict:
167
+ candidates: list[dict] = []
143
168
  for branch in self._branches(node):
144
169
  if "items" in branch:
145
- return self.resolve(branch["items"])
146
- return {}
170
+ candidates.append(self.resolve(branch["items"]))
171
+ return max(candidates, key=_schema_detail, default={})
172
+
173
+ def keys_for_label(self, label: str) -> list[str]:
174
+ """전체 스키마에서 사람이 읽는 표기가 일치하는 고유 키 후보."""
175
+ wanted = label_key(label)
176
+ found: set[str] = set()
177
+ pending: list[Any] = [self._schema]
178
+ seen: set[int] = set()
179
+ while pending:
180
+ node = pending.pop()
181
+ if isinstance(node, list):
182
+ pending.extend(node)
183
+ continue
184
+ if not isinstance(node, dict) or id(node) in seen:
185
+ continue
186
+ seen.add(id(node))
187
+ for key in (node.get("properties") or {}):
188
+ if label_key(humanise(key)) == wanted:
189
+ found.add(key)
190
+ pending.extend(node.values())
191
+ return sorted(found)
192
+
193
+ def schema_for_key(self, key: str) -> dict:
194
+ """전체 스키마에서 이 키를 가장 구체적으로 설명하는 후보."""
195
+ found: list[dict] = []
196
+ pending: list[Any] = [self._schema]
197
+ seen: set[int] = set()
198
+ while pending:
199
+ node = pending.pop()
200
+ if isinstance(node, list):
201
+ pending.extend(node)
202
+ continue
203
+ if not isinstance(node, dict) or id(node) in seen:
204
+ continue
205
+ seen.add(id(node))
206
+ candidate = (node.get("properties") or {}).get(key)
207
+ if isinstance(candidate, dict):
208
+ found.append(self.resolve(candidate))
209
+ pending.extend(node.values())
210
+ return max(found, key=_schema_detail, default={})
211
+
212
+
213
+ def _schema_detail(node: dict) -> tuple[int, int]:
214
+ """구체적인 분기 스키마가 빈 호환 스키마보다 먼저 선택되게 한다."""
215
+ structural = sum(
216
+ 1 for key in ("properties", "items", "required", "enum", "const", "type")
217
+ if key in node
218
+ )
219
+ return structural, len(str(node))
147
220
 
148
221
 
149
222
  def _is_scalar(value: Any) -> bool: