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
@@ -18,8 +18,12 @@ from __future__ import annotations
18
18
  import json
19
19
  import re
20
20
  from pathlib import Path
21
+ from typing import Mapping
21
22
 
22
23
  from okstra_ctl.convergence_engine import grouped_input_digest
24
+ from okstra_ctl.json_boundary import (
25
+ load_owned_object,
26
+ )
23
27
 
24
28
  GROUPS_BASENAME_RE = re.compile(
25
29
  r"^convergence-groups-(?P<suffix>[a-z][a-z-]*?-\d{3})\.json$"
@@ -84,7 +88,7 @@ def groups_digest_ok(state_dir: Path, suffix: str, document: dict) -> bool:
84
88
  """
85
89
  work_path = state_dir / f"convergence-work-{suffix}.json"
86
90
  try:
87
- work = json.loads(work_path.read_text(encoding="utf-8"))
91
+ work = load_owned_object(work_path, artifact="convergence work state")
88
92
  except (OSError, ValueError):
89
93
  return True
90
94
  recorded = work.get("groupsDigest") if isinstance(work, dict) else None
@@ -96,10 +100,18 @@ def groups_digest_ok(state_dir: Path, suffix: str, document: dict) -> bool:
96
100
  def read_canonical_worker_result(
97
101
  worker_results_dir: Path, worker: str, suffix: str
98
102
  ) -> str | None:
99
- """Return the `<worker>-<suffix>.md` text, or ``None`` when it is not a
100
- resolvable file directly inside ``worker-results/`` (missing, unreadable,
101
- or a worker slug that is not a single path component)."""
102
- candidate = worker_results_dir / f"{worker}-{suffix}.md"
103
+ """Return the `<worker>-worker-<suffix>.md` text, or ``None`` when it is not
104
+ a resolvable file directly inside ``worker-results/`` (missing, unreadable,
105
+ or a worker slug that is not a single path component).
106
+
107
+ The literal `-worker` marker comes from the writer,
108
+ `worker_artifacts.artifacts_from_context`, which names results
109
+ `f"{worker_id}-worker{result_suffix}.md"` from a bare worker id. Dropping it
110
+ here made every lookup miss once the roster moved to bare ids, and a missed
111
+ lookup is silent by design (see `provenance_errors`), so the guard stopped
112
+ firing without any signal.
113
+ """
114
+ candidate = worker_results_dir / f"{worker}-worker-{suffix}.md"
103
115
  try:
104
116
  if candidate.resolve().parent != worker_results_dir.resolve():
105
117
  return None
@@ -141,13 +153,46 @@ def provenance_errors(
141
153
  errors.append(
142
154
  f"convergence groups `{groups_label}` group {finding_id} "
143
155
  f"cites source item `{worker}:{item_id}`, but that ID does not "
144
- f"occur in the worker's result file `{worker}-{suffix}.md` — a "
156
+ f"occur in the worker's result file `{worker}-worker-{suffix}.md` — a "
145
157
  "grouping may not invent a provenance link to an item the "
146
158
  "worker never reported."
147
159
  )
148
160
  return errors
149
161
 
150
162
 
163
+ def worker_result_suffix(run_dir: Path, document: object) -> str | None:
164
+ """`<task-type>-<seq>` naming the worker results a grouping cites.
165
+
166
+ A grouping's own filename carries the **state** sequence, but worker results
167
+ are named with the **workerResults** sequence — separate counters, both
168
+ written by `render.render_run_manifest`. They happened to agree on early
169
+ runs, which is why reusing the groups suffix went unnoticed.
170
+
171
+ The link is the grouping's `runManifestPath`, required by
172
+ `convergence-groups-v2.0.schema.json`. Only its basename is used: the
173
+ grouping and its run manifest live in the same run directory, so this does
174
+ not need a project root.
175
+ """
176
+ if not isinstance(document, Mapping):
177
+ return None
178
+ recorded = document.get("runManifestPath")
179
+ if not _nonempty_string(recorded):
180
+ return None
181
+ manifest_path = Path(run_dir) / "manifests" / Path(str(recorded)).name
182
+ try:
183
+ manifest = load_owned_object(manifest_path, artifact="run manifest")
184
+ except (OSError, ValueError):
185
+ return None
186
+ if not isinstance(manifest, Mapping):
187
+ return None
188
+ task_type = manifest.get("taskType")
189
+ sequences = manifest.get("runSequencesByCategory")
190
+ seq = sequences.get("workerResults") if isinstance(sequences, Mapping) else None
191
+ if not _nonempty_string(task_type) or not _nonempty_string(seq):
192
+ return None
193
+ return f"{task_type}-{seq}"
194
+
195
+
151
196
  def run_dir_provenance_errors(run_dir: Path) -> list[str]:
152
197
  """Provenance errors across every grouping persisted under ``run_dir``.
153
198
 
@@ -167,18 +212,23 @@ def run_dir_provenance_errors(run_dir: Path) -> list[str]:
167
212
  continue
168
213
  suffix = match.group("suffix")
169
214
  try:
170
- document = json.loads(groups_path.read_text(encoding="utf-8"))
215
+ document = load_owned_object(
216
+ groups_path, artifact="convergence groups input"
217
+ )
171
218
  except (OSError, ValueError):
172
219
  continue
173
220
  if not isinstance(document, dict):
174
221
  continue
175
222
  if not groups_digest_ok(state_dir, suffix, document):
176
223
  continue
224
+ result_suffix = worker_result_suffix(Path(run_dir), document)
225
+ if result_suffix is None:
226
+ continue
177
227
  errors.extend(
178
228
  provenance_errors(
179
229
  document,
180
230
  worker_results_dir=worker_results_dir,
181
- suffix=suffix,
231
+ suffix=result_suffix,
182
232
  groups_label=groups_path.name,
183
233
  )
184
234
  )
@@ -1,10 +1,7 @@
1
1
  """Validated JSON loading and atomic persistence for convergence artifacts."""
2
2
  from __future__ import annotations
3
3
 
4
- import json
5
- import os
6
4
  from pathlib import Path
7
- import tempfile
8
5
  from typing import Any, Mapping
9
6
 
10
7
  from .convergence_engine import ConvergenceContractError
@@ -14,6 +11,8 @@ from .execution_manifest import (
14
11
  read_execution_manifest,
15
12
  reserve_derived_role_invocation,
16
13
  )
14
+ from .final_report_schema import load_named_schema
15
+ from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
17
16
  from .worker_request import verifier_extra_dirs
18
17
  from .write_policy import build_invocation_write_contract
19
18
 
@@ -25,44 +24,66 @@ DYNAMIC_VERIFIER_SOURCE_ROLES = frozenset({
25
24
  "verifier",
26
25
  })
27
26
 
27
+ _CRITIC_BATCH_SCHEMA = {
28
+ "type": "object",
29
+ "required": [
30
+ "schemaVersion", "taskKey", "mode", "provider",
31
+ "modelExecutionValue", "dispatches", "gaps",
32
+ ],
33
+ "additionalProperties": False,
34
+ "properties": {
35
+ "schemaVersion": {"const": "1.0"},
36
+ "taskKey": {"type": "string", "pattern": "\\S"},
37
+ "mode": {"const": "coverage"},
38
+ "provider": {"type": "string", "pattern": "\\S"},
39
+ "modelExecutionValue": {"type": "string", "pattern": "\\S"},
40
+ "dispatches": {"type": "array", "items": {"type": "object"}},
41
+ "gaps": {"type": "array", "items": {"type": "object"}},
42
+ },
43
+ }
44
+
45
+
46
+ def load_owned_json_object(path: Path) -> dict[str, Any]:
47
+ try:
48
+ return load_owned_object(path, artifact="convergence artifact")
49
+ except JsonBoundaryError as exc:
50
+ raise ConvergenceContractError(str(exc)) from exc
51
+
52
+
53
+ def _load_convergence_result(
54
+ path: Path, *, schema: Mapping[str, Any]
55
+ ) -> dict[str, Any]:
56
+ try:
57
+ return load_owned_object(
58
+ path, artifact="convergence result", schema=schema
59
+ )
60
+ except JsonBoundaryError as exc:
61
+ raise ConvergenceContractError(str(exc)) from exc
62
+
63
+
64
+ def load_convergence_round_results(path: Path) -> dict[str, Any]:
65
+ return _load_convergence_result(
66
+ path,
67
+ schema=load_named_schema("convergence-round-results-v1.0.schema.json"),
68
+ )
69
+
70
+
71
+ def load_convergence_critic_batch(path: Path) -> dict[str, Any]:
72
+ return _load_convergence_result(path, schema=_CRITIC_BATCH_SCHEMA)
73
+
28
74
 
29
75
  def load_json_object(path: Path) -> dict[str, Any]:
30
- value = json.loads(path.read_text(encoding="utf-8"))
31
- if not isinstance(value, dict):
32
- raise ConvergenceContractError(f"JSON input must be an object: {path}")
33
- return value
76
+ """Compatibility alias for convergence artifacts owned by okstra."""
77
+ return load_owned_json_object(path)
34
78
 
35
79
 
36
80
  def write_json_atomic(path: Path, payload: Mapping[str, Any]) -> None:
37
81
  if not isinstance(payload, Mapping):
38
82
  raise ConvergenceContractError("JSON output payload must be an object")
39
- path.parent.mkdir(parents=True, exist_ok=True)
40
- temp_path: Path | None = None
41
83
  try:
42
- with tempfile.NamedTemporaryFile(
43
- mode="w",
44
- encoding="utf-8",
45
- dir=path.parent,
46
- prefix=f".{path.name}.",
47
- suffix=".tmp",
48
- delete=False,
49
- ) as handle:
50
- temp_path = Path(handle.name)
51
- json.dump(
52
- payload,
53
- handle,
54
- ensure_ascii=False,
55
- sort_keys=True,
56
- indent=2,
57
- )
58
- handle.write("\n")
59
- handle.flush()
60
- os.fsync(handle.fileno())
61
- os.replace(temp_path, path)
62
- temp_path = None
63
- finally:
64
- if temp_path is not None:
65
- temp_path.unlink(missing_ok=True)
84
+ write_owned_object_atomic(path, payload, artifact="convergence artifact")
85
+ except JsonBoundaryError as exc:
86
+ raise ConvergenceContractError(str(exc)) from exc
66
87
 
67
88
 
68
89
  def write_final_state_atomic(
@@ -111,7 +132,7 @@ def reserve_dynamic_verifier(
111
132
  source_role_execution_ref,
112
133
  duty_id,
113
134
  )
114
- authority = load_json_object(manifest_path)
135
+ authority = load_owned_json_object(manifest_path)
115
136
  if authority.get("taskKey") not in (None, task_key):
116
137
  raise ExecutionManifestError("dynamic verifier taskKey does not match manifest")
117
138
  project_root = _manifest_project_root(authority, manifest_path)
@@ -21,6 +21,7 @@ from pathlib import Path
21
21
  from typing import Any, Iterator
22
22
 
23
23
  from .final_report_schema import load_schema, validate as validate_schema
24
+ from .json_boundary import JsonBoundaryError, load_owned_object
24
25
  from okstra_project import ResolverError, resolve_project_root
25
26
 
26
27
 
@@ -166,8 +167,10 @@ def assessment_fingerprint(item: Mapping[str, Any]) -> str:
166
167
 
167
168
  def _load_plan_data(data_path: Path) -> dict[str, Any]:
168
169
  try:
169
- data = json.loads(data_path.read_text(encoding="utf-8"))
170
- except (OSError, UnicodeError, json.JSONDecodeError) as exc:
170
+ data = load_owned_object(data_path, artifact="planning report record")
171
+ except JsonBoundaryError as exc:
172
+ if exc.reason == "top-level JSON value must be an object":
173
+ raise DesignPrepError(f"planning data must be an object: {data_path}") from exc
171
174
  raise DesignPrepError(f"cannot read planning data {data_path}: {exc}") from exc
172
175
  if not isinstance(data, dict):
173
176
  raise DesignPrepError(f"planning data must be an object: {data_path}")
@@ -1045,8 +1048,8 @@ def _read_carry(path: Path, stage: int, warnings: list[str]) -> Mapping[str, Any
1045
1048
  if not path.is_file():
1046
1049
  return None
1047
1050
  try:
1048
- payload = json.loads(path.read_text(encoding="utf-8"))
1049
- except (OSError, UnicodeError, json.JSONDecodeError) as exc:
1051
+ payload = load_owned_object(path, artifact="design preparation carry")
1052
+ except JsonBoundaryError as exc:
1050
1053
  warnings.append(f"malformed carry stage {stage}; ignored: {exc}")
1051
1054
  return None
1052
1055
  if not isinstance(payload, Mapping):
@@ -0,0 +1,134 @@
1
+ """보고서 서사에서 설계 표면 탐지기 소유 스냅샷을 생성한다."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any, Mapping
9
+
10
+ from .convergence_store import write_json_atomic
11
+ from .design_surfaces import DesignSurfaceError, detect_design_surfaces
12
+ from .final_report_schema import load_schema_version
13
+ from .report_narrative import NarrativeContractError, parse_narrative
14
+
15
+
16
+ def _evidence(trigger: Any) -> list[dict[str, Any]]:
17
+ return [
18
+ {"step": row.step, "field": row.field, "match": row.match}
19
+ for row in trigger.evidence
20
+ ]
21
+
22
+
23
+ def build_design_snapshot(planning: Mapping[str, Any]) -> dict[str, Any]:
24
+ """계획 본문에서 재현 가능한 설계 표면과 보수적 준비 항목을 만든다."""
25
+ triggers = detect_design_surfaces(planning)
26
+ by_stage: dict[int, list[dict[str, Any]]] = {}
27
+ items = []
28
+ for index, trigger in enumerate(triggers, start=1):
29
+ evidence = _evidence(trigger)
30
+ prep_id = f"PREP-{index:03d}"
31
+ evidence_refs = [
32
+ f"Stage {trigger.stage} step {row['step'] or 'n/a'} "
33
+ f"{row['field']}={row['match']}"
34
+ for row in evidence
35
+ ]
36
+ by_stage.setdefault(trigger.stage, []).append({
37
+ "kind": trigger.kind,
38
+ "triggerEvidence": evidence,
39
+ "disposition": "prep-item",
40
+ "prepItemId": prep_id,
41
+ })
42
+ items.append({
43
+ "id": prep_id,
44
+ "kind": trigger.kind,
45
+ "title": f"Stage {trigger.stage} {trigger.kind} contract",
46
+ "stageRefs": [trigger.stage],
47
+ "status": "provisional",
48
+ "need": (
49
+ f"Stage {trigger.stage} touches a {trigger.kind} surface, but the "
50
+ "detector evidence does not define its implementation contract."
51
+ ),
52
+ "knownFacts": [
53
+ {"statement": "The plan triggered this design surface.", "evidence": ref}
54
+ for ref in evidence_refs
55
+ ],
56
+ "openQuestions": [
57
+ f"What exact {trigger.kind} contract must Stage {trigger.stage} implement?"
58
+ ],
59
+ "aiProposal": {
60
+ "summary": "Preserve existing behaviour until the contract is confirmed.",
61
+ "details": [
62
+ "Record the concrete contract before an irreversible implementation step."
63
+ ],
64
+ "assumptions": [
65
+ "Detector evidence proves surface presence, not the missing contract details."
66
+ ],
67
+ "evidence": evidence_refs,
68
+ "confidence": "low",
69
+ },
70
+ "humanConfirmation": {
71
+ "required": True,
72
+ "reason": "The plan does not contain enough evidence to infer the contract.",
73
+ "suggestedAction": "confirm-or-edit",
74
+ },
75
+ "workingAssumption": "Preserve existing behaviour and avoid irreversible changes.",
76
+ "guardrails": [
77
+ "Do not present a synthetic or inferred contract as externally verified."
78
+ ],
79
+ "reviewAt": {"phase": "implementation", "stage": trigger.stage},
80
+ "ifStillOpen": "block",
81
+ "requestPath": (
82
+ f"design-prep-requests/design-prep-request-{trigger.stage}-{prep_id}.md"
83
+ ),
84
+ "replanTriggerFields": ["aiProposal", "workingAssumption"],
85
+ })
86
+ preparation = {
87
+ "mode": "assessed" if triggers else "no-design-inputs",
88
+ "reason": (
89
+ f"Detector found {len(triggers)} design surface(s)."
90
+ if triggers else "Detector found no design surfaces in the plan."
91
+ ),
92
+ "items": items,
93
+ }
94
+ return {
95
+ "schemaVersion": "1.0",
96
+ "owner": "design-surface-detector",
97
+ "designPreparation": preparation,
98
+ "stageCoverage": [
99
+ {"stage": stage, "rows": rows}
100
+ for stage, rows in sorted(by_stage.items())
101
+ ],
102
+ }
103
+
104
+
105
+ def write_snapshot(narrative_path: Path, output_path: Path) -> dict[str, Any]:
106
+ narrative = parse_narrative(
107
+ narrative_path.read_text(encoding="utf-8"), load_schema_version("3.0")
108
+ )
109
+ planning = narrative.get("implementationPlanning")
110
+ if not isinstance(planning, Mapping):
111
+ raise DesignSurfaceError("implementationPlanning is missing from narrative")
112
+ snapshot = build_design_snapshot(planning)
113
+ write_json_atomic(output_path, snapshot)
114
+ return snapshot
115
+
116
+
117
+ def main(argv: list[str] | None = None) -> int:
118
+ parser = argparse.ArgumentParser(prog="okstra design-snapshot")
119
+ parser.add_argument("--narrative", type=Path, required=True)
120
+ parser.add_argument("--output", type=Path, required=True)
121
+ args = parser.parse_args(argv)
122
+ try:
123
+ snapshot = write_snapshot(args.narrative, args.output)
124
+ except (OSError, UnicodeError, DesignSurfaceError, NarrativeContractError) as exc:
125
+ print(f"design-snapshot: {exc}", file=sys.stderr)
126
+ return 2
127
+ print(json.dumps({"ok": True, "surfaces": sum(
128
+ len(row["rows"]) for row in snapshot["stageCoverage"]
129
+ )}, ensure_ascii=False))
130
+ return 0
131
+
132
+
133
+ if __name__ == "__main__":
134
+ raise SystemExit(main())