okstra 0.179.2 → 0.183.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/README.md +1 -1
  2. package/dist/cli-registry.mjs +30 -2
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/dist/commands/execute/incremental-carry.mjs +9 -8
  5. package/dist/commands/execute/incremental-carry.mjs.map +1 -1
  6. package/dist/commands/execute/plan-verify.mjs +3 -1
  7. package/dist/commands/execute/plan-verify.mjs.map +1 -1
  8. package/dist/commands/execute/render-bundle.d.mts +4 -2
  9. package/dist/commands/execute/render-bundle.mjs +46 -5
  10. package/dist/commands/execute/render-bundle.mjs.map +1 -1
  11. package/dist/commands/execute/run.mjs +11 -3
  12. package/dist/commands/execute/run.mjs.map +1 -1
  13. package/dist/commands/inspect/model-io.d.mts +1 -0
  14. package/dist/commands/inspect/model-io.mjs +25 -0
  15. package/dist/commands/inspect/model-io.mjs.map +1 -0
  16. package/dist/commands/inspect/stage-map.mjs +29 -8
  17. package/dist/commands/inspect/stage-map.mjs.map +1 -1
  18. package/dist/commands/inspect/task-list.mjs +52 -6
  19. package/dist/commands/inspect/task-list.mjs.map +1 -1
  20. package/dist/commands/inspect/user-response.mjs +14 -4
  21. package/dist/commands/inspect/user-response.mjs.map +1 -1
  22. package/dist/commands/lifecycle/check-project.d.mts +1 -0
  23. package/dist/commands/lifecycle/check-project.mjs +69 -50
  24. package/dist/commands/lifecycle/check-project.mjs.map +1 -1
  25. package/dist/commands/lifecycle/contract-check.d.mts +1 -0
  26. package/dist/commands/lifecycle/contract-check.mjs +18 -0
  27. package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
  28. package/dist/commands/lifecycle/preflight.mjs +154 -51
  29. package/dist/commands/lifecycle/preflight.mjs.map +1 -1
  30. package/dist/commands/pr/pr.d.mts +1 -0
  31. package/dist/commands/pr/pr.mjs +19 -1
  32. package/dist/commands/pr/pr.mjs.map +1 -1
  33. package/dist/commands/report/agent-activity.mjs +2 -2
  34. package/dist/commands/report/approval-decision.d.mts +1 -0
  35. package/dist/commands/report/approval-decision.mjs +21 -0
  36. package/dist/commands/report/approval-decision.mjs.map +1 -0
  37. package/dist/commands/report/design-snapshot.d.mts +1 -0
  38. package/dist/commands/report/design-snapshot.mjs +19 -0
  39. package/dist/commands/report/design-snapshot.mjs.map +1 -0
  40. package/dist/commands/report/translate.mjs +3 -0
  41. package/dist/commands/report/translate.mjs.map +1 -1
  42. package/dist/lib/host-registry-client.mjs +13 -9
  43. package/dist/lib/host-registry-client.mjs.map +1 -1
  44. package/docs/architecture/storage-model.md +1 -1
  45. package/docs/architecture.md +21 -10
  46. package/docs/cli.md +36 -20
  47. package/docs/container.md +6 -4
  48. package/docs/contributor-change-matrix.md +1 -1
  49. package/docs/for-ai/README.md +2 -2
  50. package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
  51. package/docs/for-ai/skills/okstra-code-review.md +4 -4
  52. package/docs/for-ai/skills/okstra-container-build.md +20 -17
  53. package/docs/for-ai/skills/okstra-inspect.md +20 -23
  54. package/docs/for-ai/skills/okstra-manager.md +19 -18
  55. package/docs/for-ai/skills/okstra-memory.md +2 -2
  56. package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
  57. package/docs/for-ai/skills/okstra-rollup.md +14 -13
  58. package/docs/for-ai/skills/okstra-run.md +7 -3
  59. package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
  60. package/docs/for-ai/skills/okstra-setup.md +7 -7
  61. package/docs/for-ai/skills/okstra-usage.md +5 -4
  62. package/docs/for-ai/skills/okstra-user-response.md +50 -32
  63. package/docs/project-structure-overview.md +43 -31
  64. package/docs/task-process/implementation-planning.md +2 -2
  65. package/docs/task-process/implementation.md +7 -4
  66. package/package.json +1 -1
  67. package/runtime/BUILD.json +2 -2
  68. package/runtime/agents/workers/claude-worker.md +4 -4
  69. package/runtime/agents/workers/report-writer-worker.md +15 -164
  70. package/runtime/agents/workers/translator-worker.md +5 -13
  71. package/runtime/bin/okstra-error-log.py +51 -11
  72. package/runtime/bin/okstra-report-translate.py +210 -23
  73. package/runtime/prompts/host-orchestration/implementation.md +1 -1
  74. package/runtime/prompts/launch.template.md +9 -12
  75. package/runtime/prompts/lead/adapters/cmux.md +1 -1
  76. package/runtime/prompts/lead/context-loader.md +41 -141
  77. package/runtime/prompts/lead/convergence.md +10 -8
  78. package/runtime/prompts/lead/okstra-lead-contract.md +40 -49
  79. package/runtime/prompts/lead/plan-body-verification.md +217 -23
  80. package/runtime/prompts/lead/report-writer.md +81 -421
  81. package/runtime/prompts/lead/team-contract.md +9 -54
  82. package/runtime/prompts/profiles/_clarification-recommendation.md +5 -4
  83. package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
  84. package/runtime/prompts/profiles/_common-contract.md +4 -4
  85. package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
  86. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  87. package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
  88. package/runtime/prompts/profiles/change-impact-analysis.md +1 -1
  89. package/runtime/prompts/profiles/error-analysis.md +1 -1
  90. package/runtime/prompts/profiles/feature-analysis.md +1 -1
  91. package/runtime/prompts/profiles/final-verification.md +1 -1
  92. package/runtime/prompts/profiles/implementation-planning.md +20 -12
  93. package/runtime/prompts/profiles/improvement-discovery.md +1 -1
  94. package/runtime/prompts/profiles/project-analysis.md +1 -1
  95. package/runtime/prompts/profiles/release-handoff.md +0 -1
  96. package/runtime/prompts/profiles/requirements-discovery.md +1 -1
  97. package/runtime/prompts/wizard/prompts.ko.json +0 -1
  98. package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
  99. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
  100. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +8 -2
  101. package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
  102. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
  103. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
  104. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
  105. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
  106. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
  107. package/runtime/python/okstra_ctl/agent_activity.py +141 -38
  108. package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
  109. package/runtime/python/okstra_ctl/agent_prompt_cli.py +70 -23
  110. package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
  111. package/runtime/python/okstra_ctl/analysis_packet.py +124 -3
  112. package/runtime/python/okstra_ctl/approval_decisions.py +328 -0
  113. package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
  114. package/runtime/python/okstra_ctl/backfill.py +13 -10
  115. package/runtime/python/okstra_ctl/batch.py +2 -4
  116. package/runtime/python/okstra_ctl/build_tools.py +6 -3
  117. package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
  118. package/runtime/python/okstra_ctl/clarification_items.py +27 -13
  119. package/runtime/python/okstra_ctl/cmux.py +130 -52
  120. package/runtime/python/okstra_ctl/code_review_target.py +34 -8
  121. package/runtime/python/okstra_ctl/consumers.py +5 -4
  122. package/runtime/python/okstra_ctl/container.py +103 -8
  123. package/runtime/python/okstra_ctl/context_cost.py +2 -1
  124. package/runtime/python/okstra_ctl/contract_graph.py +497 -0
  125. package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
  126. package/runtime/python/okstra_ctl/convergence.py +338 -17
  127. package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
  128. package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
  129. package/runtime/python/okstra_ctl/convergence_store.py +55 -34
  130. package/runtime/python/okstra_ctl/design_prep.py +7 -4
  131. package/runtime/python/okstra_ctl/design_snapshot.py +134 -0
  132. package/runtime/python/okstra_ctl/dispatch_core.py +97 -69
  133. package/runtime/python/okstra_ctl/dispatch_state.py +163 -63
  134. package/runtime/python/okstra_ctl/doctor.py +6 -3
  135. package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
  136. package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
  137. package/runtime/python/okstra_ctl/error_log_write.py +35 -30
  138. package/runtime/python/okstra_ctl/error_report.py +26 -1
  139. package/runtime/python/okstra_ctl/error_zip.py +27 -5
  140. package/runtime/python/okstra_ctl/execution_identity.py +3 -2
  141. package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
  142. package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -2
  143. package/runtime/python/okstra_ctl/final_report_schema.py +26 -17
  144. package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
  145. package/runtime/python/okstra_ctl/fixed_text.py +39 -0
  146. package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
  147. package/runtime/python/okstra_ctl/handoff.py +5 -4
  148. package/runtime/python/okstra_ctl/i18n.py +4 -2
  149. package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
  150. package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
  151. package/runtime/python/okstra_ctl/incremental_carry.py +130 -17
  152. package/runtime/python/okstra_ctl/incremental_scope.py +12 -4
  153. package/runtime/python/okstra_ctl/index.py +8 -11
  154. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
  155. package/runtime/python/okstra_ctl/invocation.py +3 -6
  156. package/runtime/python/okstra_ctl/json_boundary.py +366 -0
  157. package/runtime/python/okstra_ctl/json_registry.py +10 -12
  158. package/runtime/python/okstra_ctl/jsonl.py +19 -2
  159. package/runtime/python/okstra_ctl/lead_events.py +33 -1
  160. package/runtime/python/okstra_ctl/listing.py +3 -3
  161. package/runtime/python/okstra_ctl/log_report.py +24 -2
  162. package/runtime/python/okstra_ctl/manager_cli.py +92 -7
  163. package/runtime/python/okstra_ctl/manager_store.py +12 -10
  164. package/runtime/python/okstra_ctl/material.py +5 -1
  165. package/runtime/python/okstra_ctl/migrate.py +29 -25
  166. package/runtime/python/okstra_ctl/model_cli.py +3 -15
  167. package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
  168. package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
  169. package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
  170. package/runtime/python/okstra_ctl/path_hints.py +12 -0
  171. package/runtime/python/okstra_ctl/paths.py +21 -0
  172. package/runtime/python/okstra_ctl/plan_items.py +115 -4
  173. package/runtime/python/okstra_ctl/plan_items_cli.py +626 -34
  174. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +2 -1
  175. package/runtime/python/okstra_ctl/pr_template.py +3 -2
  176. package/runtime/python/okstra_ctl/project_meta.py +5 -7
  177. package/runtime/python/okstra_ctl/recap.py +5 -4
  178. package/runtime/python/okstra_ctl/reconcile.py +21 -27
  179. package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
  180. package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
  181. package/runtime/python/okstra_ctl/render.py +74 -16
  182. package/runtime/python/okstra_ctl/render_final_report.py +10 -8
  183. package/runtime/python/okstra_ctl/report_assembly.py +509 -0
  184. package/runtime/python/okstra_ctl/report_contract.py +2 -1
  185. package/runtime/python/okstra_ctl/report_finalize.py +67 -32
  186. package/runtime/python/okstra_ctl/report_html/render.py +3 -2
  187. package/runtime/python/okstra_ctl/report_inputs.py +72 -0
  188. package/runtime/python/okstra_ctl/report_language.py +3 -2
  189. package/runtime/python/okstra_ctl/report_markdown.py +81 -8
  190. package/runtime/python/okstra_ctl/report_narrative.py +351 -0
  191. package/runtime/python/okstra_ctl/report_projections.py +265 -0
  192. package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
  193. package/runtime/python/okstra_ctl/report_views.py +3 -2
  194. package/runtime/python/okstra_ctl/rollup.py +65 -4
  195. package/runtime/python/okstra_ctl/run.py +184 -65
  196. package/runtime/python/okstra_ctl/run_audit.py +3 -2
  197. package/runtime/python/okstra_ctl/run_context.py +6 -9
  198. package/runtime/python/okstra_ctl/run_index_row.py +2 -8
  199. package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
  200. package/runtime/python/okstra_ctl/schema_excerpt.py +15 -8
  201. package/runtime/python/okstra_ctl/session_transcript.py +27 -1
  202. package/runtime/python/okstra_ctl/set_work_status.py +64 -38
  203. package/runtime/python/okstra_ctl/stage_fix_carry.py +8 -6
  204. package/runtime/python/okstra_ctl/stage_ledger.py +132 -18
  205. package/runtime/python/okstra_ctl/stage_map.py +96 -28
  206. package/runtime/python/okstra_ctl/stage_targets.py +3 -4
  207. package/runtime/python/okstra_ctl/team.py +3 -2
  208. package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
  209. package/runtime/python/okstra_ctl/time_report.py +51 -4
  210. package/runtime/python/okstra_ctl/usage_identity.py +2 -1
  211. package/runtime/python/okstra_ctl/usage_report.py +58 -4
  212. package/runtime/python/okstra_ctl/user_response.py +1431 -66
  213. package/runtime/python/okstra_ctl/wizard.py +11 -44
  214. package/runtime/python/okstra_ctl/work_categories.py +3 -2
  215. package/runtime/python/okstra_ctl/worker_dispatch.py +5 -2
  216. package/runtime/python/okstra_ctl/worker_prompt_body.py +49 -3
  217. package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
  218. package/runtime/python/okstra_ctl/worker_prompt_policy.py +31 -3
  219. package/runtime/python/okstra_ctl/worker_runner.py +14 -12
  220. package/runtime/python/okstra_ctl/workflow.py +2 -1
  221. package/runtime/python/okstra_ctl/worktree.py +3 -2
  222. package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
  223. package/runtime/python/okstra_ctl/write_policy.py +4 -2
  224. package/runtime/python/okstra_token_usage/antigravity.py +39 -12
  225. package/runtime/python/okstra_token_usage/collect.py +90 -38
  226. package/runtime/python/okstra_token_usage/grok.py +127 -0
  227. package/runtime/schemas/final-report-v2.0.schema.json +9 -0
  228. package/runtime/schemas/final-report-v3.0.schema.json +10219 -0
  229. package/runtime/schemas/report-narrative-v3.0.schema.json +30 -0
  230. package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
  231. package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
  232. package/runtime/skills/okstra-code-review/SKILL.md +21 -11
  233. package/runtime/skills/okstra-container-build/SKILL.md +18 -18
  234. package/runtime/skills/okstra-inspect/SKILL.md +12 -11
  235. package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
  236. package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
  237. package/runtime/skills/okstra-inspect/facets/history.md +9 -14
  238. package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
  239. package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
  240. package/runtime/skills/okstra-inspect/facets/report.md +6 -10
  241. package/runtime/skills/okstra-inspect/facets/status.md +9 -8
  242. package/runtime/skills/okstra-inspect/facets/time.md +3 -3
  243. package/runtime/skills/okstra-manager/SKILL.md +16 -14
  244. package/runtime/skills/okstra-memory/SKILL.md +3 -3
  245. package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
  246. package/runtime/skills/okstra-rollup/SKILL.md +6 -16
  247. package/runtime/skills/okstra-run/SKILL.md +7 -7
  248. package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
  249. package/runtime/skills/okstra-setup/SKILL.md +21 -13
  250. package/runtime/skills/okstra-setup/references/project-config.md +2 -2
  251. package/runtime/skills/okstra-usage/SKILL.md +10 -10
  252. package/runtime/skills/okstra-user-response/SKILL.md +78 -107
  253. package/runtime/templates/report-writer-prompt-preamble.md +29 -19
  254. package/runtime/templates/reports/html/macros/forms.html +6 -4
  255. package/runtime/templates/reports/schedule.template.md +4 -4
  256. package/runtime/templates/worker-error-contract.md +17 -29
  257. package/runtime/validators/validate-run.py +636 -72
  258. package/runtime/validators/validate_session_conformance.py +43 -7
@@ -0,0 +1,509 @@
1
+ """역할별 리포트 입력을 검증해 계약 3.0 정본을 한 번 게시한다."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import tempfile
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any, Callable, Mapping, Sequence
10
+
11
+ from .agent_activity import agent_activity_rows
12
+ from .final_report_schema import load_schema_version, validate
13
+ from .report_inputs import ReportInputPath, report_input_paths, uses_report_contract_v3
14
+ from .json_boundary import JsonBoundaryError, load_owned_object, serialize_owned_object
15
+ from .report_narrative import parse_narrative
16
+ from .report_synthesis_packet import (
17
+ report_synthesis_packet_paths,
18
+ verify_report_synthesis_packet_sources,
19
+ )
20
+ from .report_projections import (
21
+ ReportProjectionError,
22
+ project_convergence,
23
+ project_design,
24
+ project_execution,
25
+ project_token_usage,
26
+ )
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class AssemblyIssue:
31
+ owner: str
32
+ artifact_path: str
33
+ field_path: str
34
+ reason: str
35
+
36
+
37
+ class ReportAssemblyError(ValueError):
38
+ """게시 전 입력 검증 실패와 실제 소유자를 전달한다."""
39
+
40
+ def __init__(self, issues: AssemblyIssue | Sequence[AssemblyIssue]) -> None:
41
+ rows = (issues,) if isinstance(issues, AssemblyIssue) else tuple(issues)
42
+ if not rows:
43
+ raise ValueError("report assembly error requires at least one issue")
44
+ self.issues = rows
45
+ self.issue = rows[0]
46
+ super().__init__("; ".join(_issue_message(issue) for issue in rows))
47
+
48
+
49
+ def _issue_message(issue: AssemblyIssue) -> str:
50
+ return (
51
+ f"owner={issue.owner} artifactPath={issue.artifact_path} "
52
+ f"fieldPath={issue.field_path} reason={issue.reason}"
53
+ )
54
+
55
+
56
+ def _fail(owner: str, path: Path, field: str, reason: object) -> None:
57
+ raise ReportAssemblyError(AssemblyIssue(owner, str(path), field, str(reason)))
58
+
59
+
60
+ def _read_json(row: ReportInputPath) -> dict[str, Any]:
61
+ if not row.path.is_file():
62
+ _fail(row.owner, row.path, "$", "required input is missing")
63
+ try:
64
+ value = load_owned_object(row.path, artifact=f"{row.owner} report input")
65
+ except JsonBoundaryError as exc:
66
+ _fail(row.owner, row.path, "$", exc)
67
+ if not isinstance(value, dict):
68
+ _fail(row.owner, row.path, "$", "input must be a JSON object")
69
+ return value
70
+
71
+
72
+ def _input_map(project_root: Path, manifest: Mapping[str, Any]) -> dict[str, ReportInputPath]:
73
+ try:
74
+ return {row.key: row for row in report_input_paths(project_root, manifest)}
75
+ except ValueError as exc:
76
+ _fail("orchestrator", project_root, "manifest", exc)
77
+
78
+
79
+ def _input_preflight_issues(
80
+ inputs: Mapping[str, ReportInputPath],
81
+ schema: Mapping[str, Any],
82
+ ) -> tuple[AssemblyIssue, ...]:
83
+ issues: list[AssemblyIssue] = []
84
+ for row in inputs.values():
85
+ if not row.path.is_file():
86
+ issues.append(
87
+ AssemblyIssue(
88
+ row.owner,
89
+ str(row.path),
90
+ "$",
91
+ "required input is missing",
92
+ )
93
+ )
94
+ continue
95
+ try:
96
+ if row.key == "narrative":
97
+ parse_narrative(row.path.read_text(encoding="utf-8"), schema)
98
+ elif row.key == "agent-activity":
99
+ row.path.read_text(encoding="utf-8")
100
+ else:
101
+ value = load_owned_object(
102
+ row.path,
103
+ artifact=f"{row.owner} report input",
104
+ )
105
+ if not isinstance(value, dict):
106
+ raise ValueError("input must be a JSON object")
107
+ except (JsonBoundaryError, OSError, UnicodeError, ValueError) as exc:
108
+ issues.append(AssemblyIssue(row.owner, str(row.path), "$", str(exc)))
109
+ return tuple(issues)
110
+
111
+
112
+ def _narrative(row: ReportInputPath, schema: Mapping[str, Any]) -> dict[str, Any]:
113
+ if not row.path.is_file():
114
+ _fail(row.owner, row.path, "$", "required input is missing")
115
+ try:
116
+ return parse_narrative(row.path.read_text(encoding="utf-8"), schema)
117
+ except (OSError, UnicodeError, ValueError) as exc:
118
+ _fail(row.owner, row.path, "$", exc)
119
+
120
+
121
+ def _clarifications(
122
+ ledger: Mapping[str, Any], activities: list[dict[str, Any]], path: Path,
123
+ ) -> list[dict[str, Any]]:
124
+ if ledger.get("owner") != "lead":
125
+ _fail("lead", path, "owner", "approval ledger owner must be lead")
126
+ active = ledger.get("activeClarifications")
127
+ if not isinstance(active, list):
128
+ _fail("lead", path, "activeClarifications", "must be an array")
129
+ for index, row in enumerate(active):
130
+ if not isinstance(row, Mapping):
131
+ _fail("lead", path, f"activeClarifications[{index}]", "must be an object")
132
+ activity_by_id = {row.get("activityId"): row for row in activities}
133
+ return [
134
+ _clarification_row(row, activity_by_id, path)
135
+ for row in active
136
+ ]
137
+
138
+
139
+ def _project_relative(project_root: Path, path: Path) -> str:
140
+ try:
141
+ return path.relative_to(project_root).as_posix()
142
+ except ValueError:
143
+ return str(path)
144
+
145
+
146
+ def _clarification_response_value(
147
+ project_root: Path,
148
+ manifest: Mapping[str, Any],
149
+ ) -> str:
150
+ direct = manifest.get("clarificationResponsePath")
151
+ if isinstance(direct, str) and direct.strip():
152
+ return direct.strip()
153
+ context_value = manifest.get("activeRunContextPath")
154
+ if isinstance(context_value, str) and context_value.strip():
155
+ context_path = Path(context_value)
156
+ if not context_path.is_absolute():
157
+ context_path = project_root / context_path
158
+ context = _read_json(
159
+ ReportInputPath("active-run-context", "orchestrator", context_path)
160
+ )
161
+ instruction_set = context.get("instructionSet")
162
+ value = (
163
+ instruction_set.get("clarificationResponsePath")
164
+ if isinstance(instruction_set, Mapping)
165
+ else ""
166
+ )
167
+ if isinstance(value, str) and value.strip():
168
+ return value.strip()
169
+ return _latest_user_response_value(project_root, manifest)
170
+
171
+
172
+ def _latest_user_response_value(
173
+ project_root: Path,
174
+ manifest: Mapping[str, Any],
175
+ ) -> str:
176
+ record_value = manifest.get("expectedReportRecordPath")
177
+ if not isinstance(record_value, str) or not record_value.strip():
178
+ return ""
179
+ record_path = Path(record_value)
180
+ if not record_path.is_absolute():
181
+ record_path = project_root / record_path
182
+ if record_path.parent.name != "reports":
183
+ return ""
184
+ task_type = str(manifest.get("taskType") or "").strip()
185
+ if not task_type:
186
+ return ""
187
+ responses = sorted(
188
+ path
189
+ for path in (record_path.parent.parent / "user-responses").glob(
190
+ f"user-response-{task_type}-*.md"
191
+ )
192
+ if path.is_file()
193
+ )
194
+ if not responses:
195
+ return ""
196
+ return _project_relative(project_root, responses[-1])
197
+
198
+
199
+ def _clarification_carry_in(
200
+ project_root: Path,
201
+ manifest: Mapping[str, Any],
202
+ ) -> dict[str, str] | None:
203
+ value = _clarification_response_value(project_root, manifest)
204
+ if not value:
205
+ return None
206
+ path = Path(value)
207
+ if not path.is_absolute():
208
+ path = project_root / path
209
+ if not path.is_file():
210
+ _fail("user", path, "$", "attached clarification response is missing")
211
+ return {"sourceFile": _project_relative(project_root, path)}
212
+
213
+
214
+ def _clarification_row(
215
+ source: Mapping[str, Any], activities: Mapping[object, Mapping[str, Any]], path: Path,
216
+ ) -> dict[str, Any]:
217
+ row = {
218
+ key: source[key]
219
+ for key in (
220
+ "id", "ticketId", "kind", "statement", "expectedForm", "blocks",
221
+ "origin", "userConfirmation", "options",
222
+ )
223
+ if key in source
224
+ }
225
+ if "approval" in source:
226
+ row["approvalContext"] = source["approval"]
227
+ resolution = source.get("resolutionInput")
228
+ if not isinstance(resolution, Mapping):
229
+ row["status"] = "open"
230
+ return row
231
+ clarification_id = source.get("id")
232
+ check_refs = resolution.get("checkRefs")
233
+ if not isinstance(check_refs, list) or not check_refs:
234
+ _fail("lead", path, f"{clarification_id}.resolutionInput.checkRefs", "must not be empty")
235
+ for activity_id in check_refs:
236
+ activity = activities.get(activity_id)
237
+ if activity is None:
238
+ _fail("lead", path, f"{clarification_id}.resolutionInput.checkRefs", f"unknown activity {activity_id}")
239
+ if clarification_id not in (activity.get("clarificationRefs") or []):
240
+ _fail("activity-ledger", path, str(activity_id), f"missing clarificationRefs {clarification_id}")
241
+ row.update(
242
+ status="resolved",
243
+ userInput=resolution.get("userText", ""),
244
+ resolution={
245
+ "disposition": resolution.get("disposition"),
246
+ "userText": resolution.get("userText"),
247
+ "checkRefs": list(check_refs),
248
+ },
249
+ )
250
+ return row
251
+
252
+
253
+ def _attach_plan_backlinks(data: dict[str, Any], activities: list[dict[str, Any]]) -> None:
254
+ planning = data.get("implementationPlanning")
255
+ if not isinstance(planning, dict):
256
+ return
257
+ verification = planning.get("planBodyVerification")
258
+ if not isinstance(verification, dict):
259
+ return
260
+ refs_by_item: dict[str, set[str]] = {}
261
+ for activity in activities:
262
+ for item_id in activity.get("planItemIds") or []:
263
+ refs_by_item.setdefault(str(item_id), set()).update(
264
+ str(ref) for ref in activity.get("clarificationRefs") or []
265
+ )
266
+ for item in verification.get("planItems") or []:
267
+ if isinstance(item, dict):
268
+ item["clarificationRefs"] = sorted(refs_by_item.get(str(item.get("id")), set()))
269
+
270
+
271
+ def _compose(
272
+ project_root: Path,
273
+ manifest_path: Path,
274
+ manifest: Mapping[str, Any],
275
+ inputs: Mapping[str, ReportInputPath],
276
+ schema: Mapping[str, Any],
277
+ ) -> dict[str, Any]:
278
+ narrative = _narrative(inputs["narrative"], schema)
279
+ ledger = _read_json(inputs["approval-decisions"])
280
+ team_state = _read_json(inputs["execution-status"])
281
+ convergence = _read_json(inputs["convergence"])
282
+ if not inputs["agent-activity"].path.is_file():
283
+ _fail("activity-ledger", inputs["agent-activity"].path, "$", "required input is missing")
284
+ try:
285
+ activities = list(agent_activity_rows(project_root, manifest_path))
286
+ except ValueError as exc:
287
+ _fail("activity-ledger", inputs["agent-activity"].path, "$", exc)
288
+ try:
289
+ convergence_data = project_convergence(convergence)
290
+ except ReportProjectionError as exc:
291
+ _fail("convergence", inputs["convergence"].path, "$", exc)
292
+ try:
293
+ execution = project_execution(manifest, team_state)
294
+ usage = project_token_usage(team_state)
295
+ except (TypeError, ValueError) as exc:
296
+ _fail("team-state", inputs["execution-status"].path, "$", exc)
297
+ data = {"schemaVersion": "3.0", **narrative, **execution}
298
+ data["tokenUsage"] = usage
299
+ data["crossVerification"] = convergence_data["crossVerification"]
300
+ data["clarificationItems"] = _clarifications(ledger, activities, inputs["approval-decisions"].path)
301
+ carry_in = _clarification_carry_in(project_root, manifest)
302
+ if carry_in is not None:
303
+ data["clarificationCarryIn"] = carry_in
304
+ data["agentActivity"] = activities
305
+ data["evidence"] = {"primary": _promoted_evidence(convergence_data)}
306
+ data["missingInformation"] = []
307
+ _attach_metadata(data, manifest)
308
+ _attach_machine_planning(data, inputs)
309
+ _attach_plan_backlinks(data, activities)
310
+ return data
311
+
312
+
313
+ def _identity(manifest: Mapping[str, Any]) -> tuple[str, str, str]:
314
+ task_key = str(manifest.get("taskKey") or "")
315
+ parts = task_key.split(":")
316
+ if len(parts) >= 3:
317
+ return parts[0], parts[-2], parts[-1]
318
+ return (
319
+ str(manifest.get("projectId") or "unknown"),
320
+ str(manifest.get("taskGroup") or "unknown"),
321
+ str(manifest.get("taskId") or task_key or "unknown"),
322
+ )
323
+
324
+
325
+ def _attach_metadata(data: dict[str, Any], manifest: Mapping[str, Any]) -> None:
326
+ project_id, task_group, task_id = _identity(manifest)
327
+ task_type = str(manifest.get("taskType") or "")
328
+ task_key = str(manifest.get("taskKey") or "")
329
+ created = str(manifest.get("runTimestamp") or manifest.get("createdAt") or "unknown")
330
+ data["meta"] = {"reportLanguage": str(manifest.get("reportLanguage") or "en")}
331
+ clarifications = data.get("clarificationItems") or []
332
+ blocked = any(
333
+ isinstance(row, Mapping) and row.get("status") in {"open", "answered"}
334
+ for row in clarifications
335
+ )
336
+ frontmatter = {
337
+ "title": f"OKSTRA Final Report - {task_key}",
338
+ "id": task_key.replace(":", "-"),
339
+ "tags": ["final-report"],
340
+ "status": "in-progress" if blocked else "completed",
341
+ "aliases": [f"{task_id}-{task_type}"],
342
+ "date": created,
343
+ "taskId": task_id,
344
+ "taskGroup": task_group,
345
+ "projectId": project_id,
346
+ "taskType": task_type,
347
+ "workerId": "report-writer",
348
+ "approved": False,
349
+ }
350
+ planning = data.get("implementationPlanning")
351
+ if isinstance(planning, Mapping) and planning.get("planningContract") == "selected-direction":
352
+ selected = planning.get("selectedDirectionRef")
353
+ frontmatter["selectedDirectionRef"] = str(
354
+ selected.get("snapshotPath") if isinstance(selected, Mapping) else ""
355
+ )
356
+ else:
357
+ frontmatter["implementationOption"] = ""
358
+ data["frontmatter"] = frontmatter
359
+ data["header"] = {
360
+ "taskKey": task_key,
361
+ "createdAt": created,
362
+ "taskType": task_type,
363
+ "reportOwner": "Okstra lead",
364
+ "reportAuthor": "Report writer worker",
365
+ "leadModel": str(manifest.get("leadModel") or "unknown"),
366
+ "okstraVersion": str(manifest.get("okstraVersion") or "unknown"),
367
+ }
368
+
369
+
370
+ def _promoted_evidence(convergence: Mapping[str, Any]) -> list[dict[str, Any]]:
371
+ return [
372
+ {
373
+ "id": f"E-{index:03d}",
374
+ "ticketId": row["ticketId"],
375
+ "evidence": row["evidence"],
376
+ "sourceItems": row["sourceItems"],
377
+ "source": "convergence",
378
+ }
379
+ for index, row in enumerate(convergence.get("promotedEvidence") or [], start=1)
380
+ ]
381
+
382
+
383
+ def _attach_machine_planning(
384
+ data: dict[str, Any], inputs: Mapping[str, ReportInputPath],
385
+ ) -> None:
386
+ planning = data.get("implementationPlanning")
387
+ if not isinstance(planning, dict):
388
+ return
389
+ design_row = inputs.get("design-preparation")
390
+ if design_row is None:
391
+ return
392
+ try:
393
+ design = project_design(planning, _read_json(design_row))
394
+ except ReportProjectionError as exc:
395
+ _fail(design_row.owner, design_row.path, "$", exc)
396
+ planning["designPreparation"] = design["designPreparation"]
397
+ stages = {row.get("stage"): row for row in planning.get("stages") or [] if isinstance(row, dict)}
398
+ for coverage in design["stageCoverage"]:
399
+ stage = stages.get(coverage.get("stage"))
400
+ if stage is not None:
401
+ stage["designSurfaceCoverage"] = coverage.get("rows") or []
402
+ verification_row = inputs.get("plan-body-verification")
403
+ if verification_row is None:
404
+ return
405
+ verification = _read_json(verification_row)
406
+ if verification.get("owner") != "convergence":
407
+ _fail(
408
+ verification_row.owner, verification_row.path, "owner",
409
+ "plan-body verification owner must be convergence",
410
+ )
411
+ projection = verification.get("planBodyVerification")
412
+ if not isinstance(projection, Mapping):
413
+ _fail(
414
+ verification_row.owner, verification_row.path,
415
+ "planBodyVerification", "must be an object",
416
+ )
417
+ planning["planBodyVerification"] = dict(projection)
418
+
419
+
420
+ def _schema_owner(error: str) -> str:
421
+ field = error.split(" ", 1)[0]
422
+ owners = {
423
+ "executionStatus": "team-state",
424
+ "tokenUsage": "team-state",
425
+ "crossVerification": "convergence",
426
+ "clarificationItems": "lead",
427
+ "agentActivity": "activity-ledger",
428
+ "executionRoles": "run-manifest",
429
+ "frontmatter": "orchestrator",
430
+ "header": "orchestrator",
431
+ "meta": "orchestrator",
432
+ }
433
+ if "designPreparation" in error or "designSurfaceCoverage" in error:
434
+ return "design-surface-detector"
435
+ if "planBodyVerification" in error:
436
+ return "convergence"
437
+ if "selectedDirectionRef" in error:
438
+ return "report-writer"
439
+ return owners.get(field.split(".", 1)[0], "report-writer")
440
+
441
+
442
+ def _publish(
443
+ target: Path, data: Mapping[str, Any], replace: Callable[[str, str], None],
444
+ ) -> None:
445
+ target.parent.mkdir(parents=True, exist_ok=True)
446
+ serialized = serialize_owned_object(target, data, artifact="final report record")
447
+ descriptor, name = tempfile.mkstemp(prefix=f".{target.name}.", dir=target.parent)
448
+ try:
449
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
450
+ handle.write(serialized)
451
+ handle.flush()
452
+ os.fsync(handle.fileno())
453
+ replace(name, str(target))
454
+ finally:
455
+ if os.path.exists(name):
456
+ os.unlink(name)
457
+
458
+
459
+ def assemble_report(
460
+ project_root: Path, manifest_path: Path, *,
461
+ replace: Callable[[str, str], None] = os.replace,
462
+ ) -> dict[str, Any]:
463
+ """모든 입력이 유효할 때만 계약 3.0 정본을 한 번 교체한다."""
464
+ manifest_row = ReportInputPath("run-manifest", "orchestrator", manifest_path)
465
+ manifest = _read_json(manifest_row)
466
+ if not uses_report_contract_v3(manifest):
467
+ _fail("orchestrator", manifest_path, "reportContractVersion", "requires 3.0")
468
+ schema = load_schema_version("3.0")
469
+ inputs = _input_map(project_root, manifest)
470
+ input_issues = list(_input_preflight_issues(inputs, schema))
471
+ packet_data_path, _ = report_synthesis_packet_paths(inputs["narrative"].path)
472
+ if packet_data_path.is_file():
473
+ input_issues.extend(
474
+ AssemblyIssue(
475
+ issue.owner,
476
+ str(issue.path),
477
+ f"sources.{issue.label}",
478
+ issue.reason,
479
+ )
480
+ for issue in verify_report_synthesis_packet_sources(
481
+ project_root,
482
+ packet_data_path,
483
+ )
484
+ )
485
+ if input_issues:
486
+ raise ReportAssemblyError(tuple(input_issues))
487
+ data = _compose(project_root, manifest_path, manifest, inputs, schema)
488
+ errors = validate(data, schema)
489
+ if errors:
490
+ target = Path(str(manifest.get("expectedReportRecordPath") or "data.json"))
491
+ target = target if target.is_absolute() else project_root / target
492
+ raise ReportAssemblyError(
493
+ tuple(
494
+ AssemblyIssue(
495
+ _schema_owner(error),
496
+ str(target),
497
+ error.split(" ", 1)[0],
498
+ error,
499
+ )
500
+ for error in errors
501
+ )
502
+ )
503
+ value = manifest.get("expectedReportRecordPath")
504
+ if not isinstance(value, str) or not value:
505
+ _fail("orchestrator", manifest_path, "expectedReportRecordPath", "required")
506
+ target = Path(value)
507
+ target = target if target.is_absolute() else project_root / target
508
+ _publish(target, data, replace)
509
+ return data
@@ -2,7 +2,8 @@
2
2
  from __future__ import annotations
3
3
 
4
4
 
5
- CURRENT_REPORT_SCHEMA_VERSION = "2.0"
5
+ CURRENT_REPORT_SCHEMA_VERSION = "3.0"
6
+ LATEST_REPORT_SCHEMA_VERSION = "3.0"
6
7
 
7
8
  PUBLIC_REPORT_TASK_TYPES = (
8
9
  "requirements-discovery",