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
@@ -3,10 +3,8 @@ from __future__ import annotations
3
3
 
4
4
  import argparse
5
5
  import json
6
- import os
7
6
  import re
8
7
  import sys
9
- import tempfile
10
8
  from collections.abc import Mapping, Sequence
11
9
  from datetime import datetime, timezone
12
10
  from pathlib import Path
@@ -22,6 +20,11 @@ from okstra_ctl.worker_audit_ledger import (
22
20
  parse_evidence_commands,
23
21
  read_evidence_commands,
24
22
  )
23
+ from okstra_ctl.json_boundary import (
24
+ JsonBoundaryError,
25
+ load_owned_object,
26
+ mutate_owned_object_atomic,
27
+ )
25
28
 
26
29
 
27
30
  ACTIVITY_KINDS = frozenset({
@@ -59,12 +62,9 @@ class ActivityProjectionError(ValueError):
59
62
 
60
63
  def _read_json_object(path: Path) -> dict[str, Any]:
61
64
  try:
62
- payload = json.loads(path.read_text(encoding="utf-8"))
63
- except (OSError, json.JSONDecodeError) as exc:
64
- raise ActivityProjectionError(f"cannot read JSON object {path}: {exc}") from exc
65
- if not isinstance(payload, dict):
66
- raise ActivityProjectionError(f"JSON value must be an object: {path}")
67
- return payload
65
+ return load_owned_object(path, artifact="agent activity artifact")
66
+ except JsonBoundaryError as exc:
67
+ raise ActivityProjectionError(str(exc)) from exc
68
68
 
69
69
 
70
70
  def _resolve_project_path(project_root: Path, value: str | Path) -> Path:
@@ -88,6 +88,50 @@ def _manifest_run_seq(manifest: Mapping[str, Any]) -> str:
88
88
  return _require_string(sequences, "manifests")
89
89
 
90
90
 
91
+ def _validate_activity_ownership(
92
+ project_root: Path,
93
+ manifest: Mapping[str, Any],
94
+ details: Mapping[str, Any],
95
+ ) -> None:
96
+ executions = manifest.get("roleExecutions")
97
+ assignments = manifest.get("workerAssignments")
98
+ if isinstance(assignments, list):
99
+ assigned = {
100
+ row["workerId"] for row in assignments
101
+ if isinstance(row, Mapping) and isinstance(row.get("workerId"), str)
102
+ }
103
+ elif isinstance(executions, list):
104
+ assigned = {
105
+ f"{row['provider']}-worker"
106
+ for row in executions
107
+ if isinstance(row, Mapping) and isinstance(row.get("provider"), str)
108
+ }
109
+ else:
110
+ assigned = set()
111
+ if isinstance(assignments, list) or isinstance(executions, list):
112
+ assigned.update({"okstra-lead", f"{manifest.get('leadRuntime', '')}-lead"})
113
+ agent = _require_string(details, "agent")
114
+ if agent not in assigned:
115
+ raise ActivityProjectionError(
116
+ f"agent `{agent}` is not assigned to this run"
117
+ )
118
+ state_value = manifest.get("planBodyVerificationPath")
119
+ if not isinstance(state_value, str) or not details.get("planItemIds"):
120
+ return
121
+ state = _read_json_object(_resolve_project_path(project_root, state_value))
122
+ verification = state.get("planBodyVerification")
123
+ items = verification.get("planItems") if isinstance(verification, Mapping) else []
124
+ known = {
125
+ row.get("id") for row in items
126
+ if isinstance(row, Mapping) and isinstance(row.get("id"), str)
127
+ }
128
+ unknown = sorted(set(details["planItemIds"]) - known)
129
+ if unknown:
130
+ raise ActivityProjectionError(
131
+ "plan item(s) not in the current plan state: " + ", ".join(unknown)
132
+ )
133
+
134
+
91
135
  def _utc_now() -> str:
92
136
  return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
93
137
 
@@ -116,6 +160,14 @@ def _activity_row(event: LeadEvent) -> dict[str, Any]:
116
160
  from okstra_ctl.execution_identity import stored_identity
117
161
 
118
162
  row = {key: details[key] for key in ACTIVITY_FIELDS}
163
+ if "clarificationRefs" in details:
164
+ refs = details["clarificationRefs"]
165
+ if not isinstance(refs, list) or any(
166
+ not isinstance(ref, str) or re.fullmatch(r"C-\d{3,}", ref) is None
167
+ for ref in refs
168
+ ):
169
+ raise ActivityProjectionError("invalid clarificationRefs")
170
+ row["clarificationRefs"] = list(refs)
119
171
  row.update(stored_identity(details))
120
172
  return row
121
173
 
@@ -131,27 +183,6 @@ def _validate_activity_order(rows: Sequence[Mapping[str, Any]]) -> None:
131
183
  )
132
184
 
133
185
 
134
- def _atomic_write_json(path: Path, payload: Mapping[str, Any]) -> None:
135
- path.parent.mkdir(parents=True, exist_ok=True)
136
- temporary_path: Path | None = None
137
- try:
138
- with tempfile.NamedTemporaryFile(
139
- mode="w",
140
- encoding="utf-8",
141
- dir=path.parent,
142
- prefix=f".{path.name}.",
143
- suffix=".tmp",
144
- delete=False,
145
- ) as handle:
146
- temporary_path = Path(handle.name)
147
- json.dump(payload, handle, ensure_ascii=False, indent=2)
148
- handle.write("\n")
149
- os.replace(temporary_path, path)
150
- finally:
151
- if temporary_path is not None and temporary_path.exists():
152
- temporary_path.unlink()
153
-
154
-
155
186
  def record_activity(
156
187
  project_root: Path,
157
188
  run_manifest_path: Path,
@@ -164,6 +195,7 @@ def record_activity(
164
195
  raise ActivityProjectionError(
165
196
  "run manifest does not declare activityContractVersion 1"
166
197
  )
198
+ _validate_activity_ownership(project_root, manifest, details)
167
199
  event = LeadEvent(
168
200
  event_type="activity",
169
201
  lead_runtime=_require_string(manifest, "leadRuntime"),
@@ -176,15 +208,17 @@ def record_activity(
176
208
  events_path = _resolve_project_path(
177
209
  project_root, _require_string(manifest, "leadEventsPath")
178
210
  )
179
- return append_activity_event(events_path, event)
211
+ try:
212
+ return append_activity_event(events_path, event)
213
+ except ValueError as exc:
214
+ raise ActivityProjectionError(str(exc)) from exc
180
215
 
181
216
 
182
- def project_agent_activity(
217
+ def agent_activity_rows(
183
218
  project_root: Path,
184
219
  run_manifest_path: Path,
185
- data_path: Path,
186
220
  ) -> tuple[dict[str, Any], ...]:
187
- """Replace only ``agentActivity`` with this run's canonical events."""
221
+ """현재 실행의 정본 활동을 파일 변경 없이 반환한다."""
188
222
  manifest = _read_json_object(run_manifest_path)
189
223
  if manifest.get("activityContractVersion") != 1:
190
224
  return ()
@@ -204,9 +238,27 @@ def project_agent_activity(
204
238
  )
205
239
  rows = tuple(_activity_row(event) for event in events)
206
240
  _validate_activity_order(rows)
207
- data = _read_json_object(data_path)
208
- data["agentActivity"] = list(rows)
209
- _atomic_write_json(data_path, data)
241
+ return rows
242
+
243
+
244
+ def project_agent_activity(
245
+ project_root: Path,
246
+ run_manifest_path: Path,
247
+ data_path: Path,
248
+ ) -> tuple[dict[str, Any], ...]:
249
+ """2.0 호환 경로에서 ``agentActivity``만 교체한다."""
250
+ rows = agent_activity_rows(project_root, run_manifest_path)
251
+ manifest = _read_json_object(run_manifest_path)
252
+ if manifest.get("activityContractVersion") != 1:
253
+ return ()
254
+ try:
255
+ mutate_owned_object_atomic(
256
+ data_path,
257
+ lambda data: {**data, "agentActivity": list(rows)},
258
+ artifact="final report record",
259
+ )
260
+ except JsonBoundaryError as exc:
261
+ raise ActivityProjectionError(str(exc)) from exc
210
262
  return rows
211
263
 
212
264
 
@@ -222,6 +274,9 @@ def _parse_command_records(raw_records: Sequence[str]) -> tuple[EvidenceCommand,
222
274
 
223
275
  def _activity_commands(args: argparse.Namespace) -> tuple[EvidenceCommand, ...]:
224
276
  commands = list(_parse_command_records(args.command_record))
277
+ typed = _typed_command(args)
278
+ if typed is not None:
279
+ commands.append(typed)
225
280
  if args.audit_sidecar is not None:
226
281
  audit_commands, failures = read_evidence_commands(args.audit_sidecar)
227
282
  if failures:
@@ -230,6 +285,45 @@ def _activity_commands(args: argparse.Namespace) -> tuple[EvidenceCommand, ...]:
230
285
  return tuple(commands)
231
286
 
232
287
 
288
+ def _typed_command(args: argparse.Namespace) -> EvidenceCommand | None:
289
+ values = (
290
+ args.evidence_command,
291
+ args.command_cwd,
292
+ args.command_exit_code,
293
+ args.command_output_file,
294
+ )
295
+ if not any(value is not None for value in values):
296
+ return None
297
+ if any(value is None for value in values):
298
+ raise ActivityProjectionError(
299
+ "--command requires --command-cwd, --command-exit-code, and "
300
+ "--command-output-file"
301
+ )
302
+ try:
303
+ output_summary = args.command_output_file.read_text(encoding="utf-8")
304
+ except (OSError, UnicodeError) as exc:
305
+ raise ActivityProjectionError(
306
+ f"cannot read command output {args.command_output_file}: {exc}"
307
+ ) from exc
308
+ return EvidenceCommand(
309
+ command=args.evidence_command,
310
+ cwd=args.command_cwd,
311
+ exit_code=args.command_exit_code,
312
+ output_summary=output_summary,
313
+ )
314
+
315
+
316
+ def _activity_summary(args: argparse.Namespace) -> str:
317
+ if args.summary is not None:
318
+ return args.summary
319
+ try:
320
+ return args.summary_file.read_text(encoding="utf-8").strip()
321
+ except (OSError, UnicodeError) as exc:
322
+ raise ActivityProjectionError(
323
+ f"cannot read activity summary {args.summary_file}: {exc}"
324
+ ) from exc
325
+
326
+
233
327
  def _conversation_activity_line(details: Mapping[str, Any]) -> str:
234
328
  plan_items = ",".join(details["planItemIds"]) or "<none>"
235
329
  result_path = str(details["resultPath"] or "<none>")
@@ -246,13 +340,15 @@ def _append(args: argparse.Namespace) -> int:
246
340
  details = {
247
341
  "kind": args.kind,
248
342
  "agent": args.agent,
249
- "summary": args.summary,
343
+ "summary": _activity_summary(args),
250
344
  "planItemIds": args.plan_item_id,
251
345
  "resultPath": args.result_path,
252
346
  "commands": [command.to_record() for command in commands],
253
347
  "evidenceRefs": args.evidence_ref,
254
348
  "outcome": args.outcome,
255
349
  }
350
+ if args.request_ref is not None:
351
+ details["activityRequestRef"] = args.request_ref
256
352
  event = record_activity(args.project_root, args.run_manifest, details)
257
353
  payload = dict(event.details)
258
354
  payload["ok"] = True
@@ -281,15 +377,22 @@ def _parser() -> argparse.ArgumentParser:
281
377
  "--kind", choices=sorted(ACTIVITY_KINDS), required=True
282
378
  )
283
379
  append_parser.add_argument("--agent", required=True)
284
- append_parser.add_argument("--summary", required=True)
380
+ summary = append_parser.add_mutually_exclusive_group(required=True)
381
+ summary.add_argument("--summary")
382
+ summary.add_argument("--summary-file", type=Path)
285
383
  append_parser.add_argument(
286
384
  "--outcome", choices=sorted(ACTIVITY_OUTCOMES), required=True
287
385
  )
288
386
  append_parser.add_argument("--plan-item-id", action="append", default=[])
289
387
  append_parser.add_argument("--evidence-ref", action="append", default=[])
290
388
  append_parser.add_argument("--command-record", action="append", default=[])
389
+ append_parser.add_argument("--command", dest="evidence_command")
390
+ append_parser.add_argument("--command-cwd")
391
+ append_parser.add_argument("--command-exit-code", type=int)
392
+ append_parser.add_argument("--command-output-file", type=Path)
291
393
  append_parser.add_argument("--result-path", default="")
292
394
  append_parser.add_argument("--audit-sidecar", type=Path)
395
+ append_parser.add_argument("--request-ref")
293
396
  project_parser = subparsers.add_parser("project")
294
397
  project_parser.add_argument("--project-root", type=Path, required=True)
295
398
  project_parser.add_argument("--run-manifest", type=Path, required=True)
@@ -15,6 +15,12 @@ import tempfile
15
15
  from typing import Iterator, Literal, Mapping, get_args
16
16
 
17
17
  from .domain.role import RoleCatalogError, role_for_duty
18
+ from .json_boundary import (
19
+ JsonBoundaryError,
20
+ external_invocation_json_source,
21
+ load_external_json,
22
+ load_owned_object,
23
+ )
18
24
 
19
25
 
20
26
  AgentAudience = Literal[
@@ -935,8 +941,15 @@ def _verified_result_envelope(
935
941
  ) -> tuple[bytes, str]:
936
942
  try:
937
943
  body = result_path.read_bytes()
938
- envelope = json.loads(body.decode("utf-8"))
939
- except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
944
+ envelope = load_external_json(
945
+ external_invocation_json_source(
946
+ result_path,
947
+ project_root=project_root,
948
+ purpose=purpose,
949
+ ),
950
+ artifact="standalone worker result",
951
+ )
952
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError, JsonBoundaryError) as exc:
940
953
  raise AgentInvocationError(
941
954
  f"standalone result envelope is missing or invalid: {result_path}"
942
955
  ) from exc
@@ -1494,8 +1507,8 @@ def _metadata_difference(
1494
1507
  so they are reported by name too.
1495
1508
  """
1496
1509
  try:
1497
- existing = json.loads(metadata.read_text(encoding="utf-8"))
1498
- except (OSError, json.JSONDecodeError) as exc:
1510
+ existing = load_owned_object(metadata, artifact="agent invocation metadata")
1511
+ except JsonBoundaryError as exc:
1499
1512
  return f"existing metadata is unreadable: {exc}"
1500
1513
  composed = materialized.metadata
1501
1514
  if not isinstance(existing, dict):
@@ -2240,8 +2253,8 @@ def _load_json_object(path: Path, label: str) -> dict[str, object]:
2240
2253
 
2241
2254
  def _read_json_if_object(path: Path) -> dict[str, object] | None:
2242
2255
  try:
2243
- value = json.loads(path.read_text(encoding="utf-8"))
2244
- except (OSError, json.JSONDecodeError):
2256
+ value = load_owned_object(path, artifact="agent invocation metadata")
2257
+ except JsonBoundaryError:
2245
2258
  return None
2246
2259
  return value if isinstance(value, dict) else None
2247
2260
 
@@ -34,6 +34,8 @@ from .agent_invocation import (
34
34
  verify_agent_invocation,
35
35
  v2_role_assignment_authority_errors,
36
36
  )
37
+ from .fixed_text import line
38
+ from .json_boundary import JsonBoundaryError, load_owned_object
37
39
  from .convergence_store import (
38
40
  DYNAMIC_VERIFIER_SOURCE_ROLES,
39
41
  reserve_dynamic_verifier,
@@ -48,7 +50,11 @@ from .wrapper_status import (
48
50
  prompt_derived_paths,
49
51
  status_path_for_prompt,
50
52
  )
51
- from .worker_prompt_policy import resolve_prompt_plan_for_manifest
53
+ from .report_inputs import report_narrative_path, uses_report_contract_v3
54
+ from .worker_prompt_policy import (
55
+ CRITIC_DUTY_BY_ASSIGNMENT_SEGMENT,
56
+ resolve_prompt_plan_for_manifest,
57
+ )
52
58
  from .dispatch_state import (
53
59
  BACKEND_CLI_WRAPPER,
54
60
  BACKEND_CMUX_PANE,
@@ -70,6 +76,13 @@ class AgentPromptCliError(RuntimeError):
70
76
  def _parser() -> argparse.ArgumentParser:
71
77
  parser = argparse.ArgumentParser(prog="okstra agent-prompt")
72
78
  commands = parser.add_subparsers(dest="command", required=True)
79
+ _add_materialize_parser(commands)
80
+ _add_completion_parsers(commands)
81
+ _add_dispatch_parsers(commands)
82
+ return parser
83
+
84
+
85
+ def _add_materialize_parser(commands: argparse._SubParsersAction) -> None:
73
86
  materialize = commands.add_parser("materialize")
74
87
  _common_paths(materialize)
75
88
  materialize.add_argument("--invocation-id", required=True)
@@ -110,7 +123,10 @@ def _parser() -> argparse.ArgumentParser:
110
123
  verify.add_argument("--run-manifest")
111
124
  verify.add_argument("--metadata", required=True)
112
125
  verify.add_argument("--json", action="store_true")
126
+ verify.add_argument("--text", action="store_true")
113
127
 
128
+
129
+ def _add_completion_parsers(commands: argparse._SubParsersAction) -> None:
114
130
  materialize_result = commands.add_parser("materialize-result")
115
131
  _standalone_completion_args(materialize_result)
116
132
  materialize_result.add_argument("--returned-body-file", required=True)
@@ -124,6 +140,8 @@ def _parser() -> argparse.ArgumentParser:
124
140
  verify_completion.add_argument("--completion", required=True)
125
141
  verify_completion.add_argument("--json", action="store_true")
126
142
 
143
+
144
+ def _add_dispatch_parsers(commands: argparse._SubParsersAction) -> None:
127
145
  record_dispatch = commands.add_parser("record-dispatch")
128
146
  _common_paths(record_dispatch)
129
147
  record_dispatch.add_argument("--run-manifest", required=True)
@@ -153,7 +171,6 @@ def _parser() -> argparse.ArgumentParser:
153
171
  link_result.add_argument("--dispatch-id", required=True)
154
172
  link_result.add_argument("--result", required=True)
155
173
  link_result.add_argument("--json", action="store_true")
156
- return parser
157
174
 
158
175
 
159
176
  def _common_paths(parser: argparse.ArgumentParser) -> None:
@@ -179,6 +196,7 @@ def main(argv: list[str] | None = None) -> int:
179
196
  _emit(
180
197
  {"ok": True, "metadataPath": str(Path(args.metadata).resolve())},
181
198
  args.json,
199
+ args.text,
182
200
  )
183
201
  return 0
184
202
  if args.command == "materialize-result":
@@ -314,16 +332,32 @@ def _materialize_run(
314
332
  )
315
333
  if args.audience == "report-writer" and not args.audit_source:
316
334
  # The report writer is the one audience whose result path is not its own
317
- # worker result: it writes the report `data.json`, while the audit
318
- # sidecar is derived from its `.md`. With both collapsed into one value
319
- # the prompt loses its `**Worker Result Path:**` anchor and the writer
320
- # puts the report where the audit file belongs — silently, because every
321
- # header is still present and well-formed. The roster path derives both
322
- # from the manifest; a dynamic call has to name them.
335
+ # worker result: it writes the report body, while the audit sidecar is
336
+ # derived from its `.md`. With both collapsed into one value the prompt
337
+ # loses its `**Worker Result Path:**` anchor and the writer puts the
338
+ # report where the audit file belongs — silently, because every header
339
+ # is still present and well-formed. The roster path derives both from
340
+ # the manifest; a dynamic call has to name them.
341
+ #
342
+ # Which artifact `--result` names depends on the report contract, and
343
+ # `dispatch_state.dispatch_result_path` is what decides it. Naming only
344
+ # the v2 answer here sent v3 runs to the data.json, so the narrative
345
+ # `report-finalize` assembles from was never written and the phase
346
+ # failed later, at a place that does not point back here.
347
+ if uses_report_contract_v3(manifest):
348
+ expected = report_narrative_path(project_root, manifest)
349
+ raise AgentPromptCliError(
350
+ "report-writer materialization requires --audit-source: under "
351
+ f"report contract 3.0 --result is the narrative ({expected}), "
352
+ "which `report-finalize` assembles the report data.json from, "
353
+ "and --audit-source the worker-result .md the audit sidecar is "
354
+ "derived from"
355
+ )
323
356
  raise AgentPromptCliError(
324
- "report-writer materialization requires --audit-source: --result is "
325
- "the report data.json, --audit-source the worker-result .md the "
326
- "audit sidecar is derived from"
357
+ "report-writer materialization requires --audit-source: under "
358
+ "report contract 2.0 --result is the report data.json, and "
359
+ "--audit-source the worker-result .md the audit sidecar is "
360
+ "derived from"
327
361
  )
328
362
  audit_source_path = (
329
363
  _authorized_path(
@@ -630,17 +664,15 @@ def _validate_run_identity(
630
664
  raise AgentPromptCliError("translator assignment requires worker ID 'translator'")
631
665
  elif assignment_ref.startswith("critic/"):
632
666
  scope = assignment_ref.split("/", 1)[1]
633
- expected = {"scope": "scope-critic", "acceptance": "acceptance-critic"}.get(
634
- scope,
635
- "",
636
- )
667
+ expected = CRITIC_DUTY_BY_ASSIGNMENT_SEGMENT.get(scope, "")
637
668
  if not expected or dispatch_kind != "critic":
638
669
  raise AgentPromptCliError("critic assignment identity is invalid")
639
- convergence = manifest.get("convergence")
640
- critic = convergence.get("critic") if isinstance(convergence, Mapping) else None
641
- provider = critic.get("provider") if isinstance(critic, Mapping) else None
642
- if provider and provider != worker_id:
643
- raise AgentPromptCliError("critic worker does not match manifest provider")
670
+ # provider 대조는 여기 두지 않는다. critic 의 `worker_id` 는 배정 참조의
671
+ # 마지막 마디(`scope` / `acceptance`)이고 provider 이름이 아니다
672
+ # (`render.py` 의 로스터 행이 `assignment_ref.rsplit("/", 1)[-1]` 만든다).
673
+ # 종전에 있던 `provider != worker_id` 검사는 두 어휘를 비교해서 늘 참이 되는
674
+ # 형태였고, 설정을 run manifest 에서 찾다 못 찾아 조용히 건너뛰었기에
675
+ # 드러나지 않았다. 정본에서 읽게 고치면 모든 critic 디스패치가 거부된다.
644
676
  else:
645
677
  try:
646
678
  expected = resolve_prompt_plan_for_manifest(
@@ -1039,8 +1071,8 @@ def _relative(project_root: Path, path: Path) -> str:
1039
1071
 
1040
1072
  def _read_json_object(path: Path, label: str) -> dict[str, Any]:
1041
1073
  try:
1042
- value = json.loads(path.read_text(encoding="utf-8"))
1043
- except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
1074
+ value = load_owned_object(path, artifact="agent prompt metadata")
1075
+ except JsonBoundaryError as exc:
1044
1076
  raise AgentPromptCliError(f"{label} is invalid: {path}") from exc
1045
1077
  if not isinstance(value, dict):
1046
1078
  raise AgentPromptCliError(f"{label} must be a JSON object: {path}")
@@ -1098,12 +1130,27 @@ def _prepared_payload(
1098
1130
  }
1099
1131
 
1100
1132
 
1101
- def _emit(payload: Mapping[str, object], as_json: bool) -> None:
1133
+ def _emit(
1134
+ payload: Mapping[str, object], as_json: bool, as_text: bool = False
1135
+ ) -> None:
1102
1136
  if as_json:
1103
1137
  print(json.dumps(payload, ensure_ascii=False, indent=2))
1138
+ elif as_text:
1139
+ print(render_agent_prompt_text(payload), end="")
1104
1140
  else:
1105
1141
  print(payload.get("promptPath") or payload.get("metadataPath") or "ok")
1106
1142
 
1107
1143
 
1144
+ def render_agent_prompt_text(payload: Mapping[str, object]) -> str:
1145
+ """agent prompt 검증 결과의 승인된 경로만 투영한다."""
1146
+ rows = ["Okstra agent prompt\n"]
1147
+ rows.append(line("Status", "ready" if payload.get("ok", True) else "error"))
1148
+ for label, key in (("Prompt path", "promptPath"), ("Metadata path", "metadataPath"),
1149
+ ("Assignment ID", "assignmentId"), ("Worker role", "workerRole")):
1150
+ if key in payload:
1151
+ rows.append(line(label, payload.get(key)))
1152
+ return "".join(rows)
1153
+
1154
+
1108
1155
  if __name__ == "__main__":
1109
1156
  raise SystemExit(main())
@@ -11,6 +11,7 @@ from typing import Mapping, Sequence
11
11
 
12
12
  from .final_report_paths import final_report_data_path
13
13
  from .paths import task_manifest_file
14
+ from .json_boundary import JsonBoundaryError, load_owned_object
14
15
  from .user_response import (
15
16
  UserResponseError,
16
17
  load_authoritative_analysis_review,
@@ -196,8 +197,8 @@ def _task_key_from_manifest(task_root: Path, okstra_root: Path) -> str:
196
197
  task_manifest_file(task_root), okstra_root, "task manifest path"
197
198
  )
198
199
  try:
199
- manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
200
- except (OSError, json.JSONDecodeError) as exc:
200
+ manifest = load_owned_object(manifest_path, artifact="task manifest")
201
+ except JsonBoundaryError as exc:
201
202
  raise AnalysisInputError(f"cannot read task manifest: {manifest_path}") from exc
202
203
  if not isinstance(manifest, dict):
203
204
  raise AnalysisInputError("task manifest must contain an object")
@@ -219,8 +220,8 @@ def load_analysis_report_candidate(
219
220
  raise AnalysisInputError("report filename is not a final report")
220
221
  data_path = _resolved_within(final_report_data_path(report), okstra_root, "data.json path")
221
222
  try:
222
- data = json.loads(data_path.read_text(encoding="utf-8"))
223
- except (OSError, json.JSONDecodeError) as exc:
223
+ data = load_owned_object(data_path, artifact="analysis report record")
224
+ except JsonBoundaryError as exc:
224
225
  raise AnalysisInputError(f"cannot read data.json: {data_path}") from exc
225
226
  if not isinstance(data, dict):
226
227
  raise AnalysisInputError("data.json must contain an object")