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
@@ -24,6 +24,13 @@ from .agent_invocation import (
24
24
  )
25
25
  from .path_hints import hydrate_active_run_context
26
26
  from .report_language import resolve_report_language
27
+ from .report_inputs import report_narrative_path, uses_report_contract_v3
28
+ from .report_synthesis_packet import (
29
+ ReportSynthesisPacketError,
30
+ materialize_report_synthesis_packet,
31
+ report_synthesis_packet_paths,
32
+ )
33
+ from .json_boundary import JsonBoundaryError, load_owned_object
27
34
  from .worker_prompt_body import (
28
35
  REPORT_WRITER_WORKER_ID,
29
36
  analysis_prompt_body,
@@ -444,7 +451,17 @@ def _render_prompt_parts(
444
451
  f"**Prompt Delivery Mode:** {context.request.delivery_mode.value}"
445
452
  )
446
453
  anchors.extend(_implementation_anchor_lines(context, item.plan))
447
- body = _prompt_body(context, state, worker, item.plan)
454
+ synthesis_packet_path = _materialize_report_writer_packet(
455
+ context,
456
+ item.plan,
457
+ )
458
+ body = _prompt_body(
459
+ context,
460
+ state,
461
+ worker,
462
+ item.plan,
463
+ synthesis_packet_path,
464
+ )
448
465
  body.extend(_resource_lines(context, item))
449
466
  except InitialPromptMaterializationError:
450
467
  raise
@@ -503,7 +520,7 @@ def _agent_invocation_request(
503
520
  f"requested model does not match {assignment_ref}",
504
521
  )
505
522
  anchors, body = _render_prompt_parts(context, item)
506
- source_paths = (
523
+ source_paths = [
507
524
  AgentInstructionSource(
508
525
  "project",
509
526
  _project_relative(context.project_root, context.request.run_manifest_path),
@@ -514,7 +531,10 @@ def _agent_invocation_request(
514
531
  AgentInstructionSource(
515
532
  "project", _required_string(context.manifest, "teamStatePath")
516
533
  ),
517
- )
534
+ ]
535
+ packet_source = _report_writer_packet_source(context, item.plan)
536
+ if packet_source is not None:
537
+ source_paths.append(packet_source)
518
538
  invocation_id = _worker_invocation_id(context.manifest, item.worker.worker_id)
519
539
  return AgentInvocationRequest(
520
540
  invocation_id=invocation_id,
@@ -526,7 +546,7 @@ def _agent_invocation_request(
526
546
  instruction=AgentInstruction(
527
547
  anchor_lines=tuple(anchors),
528
548
  body="\n".join(body).rstrip() + "\n",
529
- source_paths=source_paths,
549
+ source_paths=tuple(source_paths),
530
550
  ),
531
551
  project_root=context.project_root,
532
552
  run_manifest_path=context.request.run_manifest_path,
@@ -549,6 +569,7 @@ def _prompt_body(
549
569
  state: Mapping[str, Any],
550
570
  worker: InitialPromptWorkerRequest,
551
571
  plan: PromptPlan,
572
+ synthesis_packet_path: str = "",
552
573
  ) -> list[str]:
553
574
  if plan.audience == "report-writer":
554
575
  return report_writer_prompt_body(
@@ -561,6 +582,7 @@ def _prompt_body(
561
582
  context.manifest,
562
583
  context.active_context,
563
584
  ),
585
+ synthesis_packet_path,
564
586
  )
565
587
  return analysis_prompt_body(
566
588
  context.manifest,
@@ -571,6 +593,56 @@ def _prompt_body(
571
593
  )
572
594
 
573
595
 
596
+ def _report_writer_narrative_path(context: _MaterializationContext) -> Path:
597
+ return report_narrative_path(context.project_root, context.manifest)
598
+
599
+
600
+ def _report_writer_packet_source(
601
+ context: _MaterializationContext,
602
+ plan: PromptPlan,
603
+ ) -> AgentInstructionSource | None:
604
+ if (
605
+ plan.audience != "report-writer"
606
+ or not uses_report_contract_v3(context.manifest)
607
+ ):
608
+ return None
609
+ _, markdown_path = report_synthesis_packet_paths(
610
+ _report_writer_narrative_path(context)
611
+ )
612
+ return AgentInstructionSource(
613
+ "project",
614
+ _project_relative(context.project_root, markdown_path),
615
+ )
616
+
617
+
618
+ def _materialize_report_writer_packet(
619
+ context: _MaterializationContext,
620
+ plan: PromptPlan,
621
+ ) -> str:
622
+ packet_source = _report_writer_packet_source(context, plan)
623
+ if packet_source is None:
624
+ return ""
625
+ try:
626
+ _, markdown_path = materialize_report_synthesis_packet(
627
+ project_root=context.project_root,
628
+ manifest=context.manifest,
629
+ active_context=context.active_context,
630
+ team_state=context.team_state,
631
+ narrative_path=_report_writer_narrative_path(context),
632
+ )
633
+ except ReportSynthesisPacketError as exc:
634
+ defects = "; ".join(
635
+ f"owner={issue.owner} source={issue.label} path={issue.path} "
636
+ f"reason={issue.reason}"
637
+ for issue in exc.issues
638
+ )
639
+ raise InitialPromptMaterializationError(
640
+ "required_input_missing",
641
+ f"report synthesis packet contract defects: {defects}",
642
+ ) from exc
643
+ return _project_relative(context.project_root, markdown_path)
644
+
645
+
574
646
  def _resource_lines(
575
647
  context: _MaterializationContext,
576
648
  item: _PromptItem,
@@ -1325,12 +1397,12 @@ def _load_referenced_json(
1325
1397
 
1326
1398
  def _load_json_object(path: Path, label: str) -> Mapping[str, Any]:
1327
1399
  try:
1328
- payload = json.loads(path.read_text(encoding="utf-8"))
1329
- except (OSError, UnicodeError, json.JSONDecodeError) as exc:
1400
+ payload = load_owned_object(path, artifact=label)
1401
+ except JsonBoundaryError as exc:
1330
1402
  raise InitialPromptMaterializationError(
1331
1403
  "required_input_missing",
1332
1404
  f"cannot load {label}: {path}: {exc}",
1333
- ) from exc
1405
+ ) from (exc.__cause__ or exc)
1334
1406
  if not isinstance(payload, dict):
1335
1407
  raise InitialPromptMaterializationError(
1336
1408
  "required_input_missing",
@@ -7,6 +7,7 @@ from pathlib import Path
7
7
  from typing import Optional
8
8
 
9
9
  from .ids import _safe_fs_segment
10
+ from .json_boundary import load_owned_object, write_owned_object_atomic
10
11
 
11
12
 
12
13
  def invocation_path(home: Path, project_id: str,
@@ -32,11 +33,7 @@ def save_invocation(home: Path, project_id: str,
32
33
  run_seq: int, payload: dict) -> None:
33
34
  """invocation 을 원자적으로 저장(임시파일 + os.replace)."""
34
35
  target = invocation_path(home, project_id, task_group, task_id, task_type, run_seq)
35
- target.parent.mkdir(parents=True, exist_ok=True)
36
- tmp = target.with_suffix(".json.tmp")
37
- tmp.write_text(json.dumps(payload, indent=2) + "\n")
38
- os.chmod(tmp, 0o600)
39
- os.replace(tmp, target)
36
+ write_owned_object_atomic(target, payload, artifact="invocation metadata")
40
37
 
41
38
 
42
39
  def load_invocation(home: Path, project_id: str,
@@ -46,4 +43,4 @@ def load_invocation(home: Path, project_id: str,
46
43
  target = invocation_path(home, project_id, task_group, task_id, task_type, run_seq)
47
44
  if not target.is_file():
48
45
  return None
49
- return json.loads(target.read_text())
46
+ return load_owned_object(target, artifact="invocation metadata")
@@ -0,0 +1,366 @@
1
+ """Strict JSON boundaries for okstra-owned artifacts."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ from collections.abc import Callable, Mapping
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ import tempfile
10
+ from typing import Any
11
+
12
+ from .final_report_schema import SchemaError, validate as validate_schema
13
+
14
+
15
+ class JsonBoundaryError(ValueError):
16
+ """An owned JSON artifact could not satisfy its persistence contract."""
17
+
18
+ def __init__(self, path: Path, artifact: str, reason: str) -> None:
19
+ self.path = path
20
+ self.artifact = artifact
21
+ self.reason = reason
22
+ super().__init__(f"{artifact}: {reason}: {path}")
23
+
24
+
25
+ _EXTERNAL_SOURCE_SEAL = object()
26
+
27
+
28
+ @dataclass(frozen=True, init=False)
29
+ class ExternalJsonSource:
30
+ """검증된 외부 생산자가 소유한 JSON 입력 경로다."""
31
+
32
+ path: Path
33
+ producer: str
34
+
35
+ def __init__(self, path: Path, producer: str, seal: object = None) -> None:
36
+ if seal is not _EXTERNAL_SOURCE_SEAL:
37
+ raise TypeError("ExternalJsonSource is sealed")
38
+ object.__setattr__(self, "path", path)
39
+ object.__setattr__(self, "producer", producer)
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class OwnedJsonSnapshot:
44
+ """한 번 읽은 원시 바이트와 그 바이트에서 검증한 객체다."""
45
+
46
+ value: dict[str, Any]
47
+ raw_bytes: bytes
48
+
49
+
50
+ def _issue_external_source(path: Path, producer: str) -> ExternalJsonSource:
51
+ if not isinstance(path, Path):
52
+ raise TypeError("external JSON source requires Path")
53
+ return ExternalJsonSource(path, producer, _EXTERNAL_SOURCE_SEAL)
54
+
55
+
56
+ def _is_okstra_owned_path(path: Path) -> bool:
57
+ lexical = Path(os.path.abspath(path))
58
+ resolved = path.resolve(strict=False)
59
+ return ".okstra" in lexical.parts or ".okstra" in resolved.parts
60
+
61
+
62
+ def external_user_json_source(path: Path) -> ExternalJsonSource:
63
+ """사용자 제공 JSON이 Okstra 소유 트리 밖에 있을 때만 승인한다."""
64
+ if _is_okstra_owned_path(path):
65
+ raise JsonBoundaryError(path, "external JSON", "Okstra-owned path")
66
+ return _issue_external_source(path, "user")
67
+
68
+
69
+ def external_worker_json_source(
70
+ path: Path, *, trusted_root: Path, lane_root: Path
71
+ ) -> ExternalJsonSource:
72
+ """신뢰 루트 아래의 정확한 worker 결과 lane만 승인한다."""
73
+ lexical_root = Path(os.path.abspath(trusted_root))
74
+ lexical_lane = Path(os.path.abspath(lane_root))
75
+ lexical_path = Path(os.path.abspath(path))
76
+ try:
77
+ lane_parts = lexical_lane.relative_to(lexical_root).parts
78
+ except ValueError:
79
+ lane_parts = ()
80
+ is_worker_lane = (
81
+ len(lane_parts) in {7, 8}
82
+ and lane_parts[:2] == (".okstra", "tasks")
83
+ and lane_parts[4] == "runs"
84
+ and lane_parts[-1] == "worker-results"
85
+ and (len(lane_parts) == 7 or lane_parts[6].startswith("stage-"))
86
+ )
87
+ if not is_worker_lane or lexical_path.parent != lexical_lane:
88
+ raise JsonBoundaryError(
89
+ path, "worker JSON", "outside worker result lanes"
90
+ )
91
+ try:
92
+ lexical_lane.relative_to(lexical_root)
93
+ lexical_path.relative_to(lexical_lane)
94
+ resolved_lane = lane_root.resolve(strict=False)
95
+ resolved_lane.relative_to(trusted_root.resolve(strict=False))
96
+ path.resolve(strict=False).relative_to(resolved_lane)
97
+ except ValueError as exc:
98
+ raise JsonBoundaryError(
99
+ path, "worker JSON", "outside worker result lanes"
100
+ ) from exc
101
+ if _has_symlink_component(lexical_lane, lexical_root) or _has_symlink_component(
102
+ lexical_path, lexical_lane
103
+ ):
104
+ raise JsonBoundaryError(path, "worker JSON", "outside worker result lanes")
105
+ return _issue_external_source(path, "worker")
106
+
107
+
108
+ def external_invocation_json_source(
109
+ path: Path, *, project_root: Path, purpose: str
110
+ ) -> ExternalJsonSource:
111
+ """정확한 standalone purpose lane의 직접 결과만 승인한다."""
112
+ if not purpose or any(char not in "abcdefghijklmnopqrstuvwxyz0123456789-" for char in purpose):
113
+ raise JsonBoundaryError(path, "invocation JSON", "outside invocation result lane")
114
+ lane = project_root / ".okstra" / "agent-invocations" / purpose
115
+ lexical_path = Path(os.path.abspath(path))
116
+ lexical_lane = Path(os.path.abspath(lane))
117
+ lexical_root = Path(os.path.abspath(project_root))
118
+ if lexical_path.parent != lexical_lane or path.is_symlink():
119
+ raise JsonBoundaryError(path, "invocation JSON", "outside invocation result lane")
120
+ try:
121
+ lane.resolve(strict=False).relative_to(project_root.resolve(strict=False))
122
+ path.resolve(strict=False).relative_to(lane.resolve(strict=False))
123
+ except ValueError as exc:
124
+ raise JsonBoundaryError(
125
+ path, "invocation JSON", "outside invocation result lane"
126
+ ) from exc
127
+ if _has_symlink_component(lexical_lane, lexical_root):
128
+ raise JsonBoundaryError(path, "invocation JSON", "outside invocation result lane")
129
+ return _issue_external_source(path, "standalone worker")
130
+
131
+
132
+ def _has_symlink_component(path: Path, root: Path) -> bool:
133
+ current = path
134
+ while current != root:
135
+ if current.is_symlink():
136
+ return True
137
+ if current.parent == current:
138
+ return True
139
+ current = current.parent
140
+ return root.is_symlink()
141
+
142
+
143
+ def external_claude_global_json_source(
144
+ path: Path, home_dir: Path
145
+ ) -> ExternalJsonSource:
146
+ """호스트 어댑터가 지정한 home의 전역 Claude 설정만 승인한다."""
147
+ expected = home_dir / ".claude.json"
148
+ if Path(os.path.abspath(path)) != Path(os.path.abspath(expected)) or path.is_symlink():
149
+ raise JsonBoundaryError(path, "Claude JSON", "outside fixed adapter path")
150
+ return _issue_external_source(path, "Claude Code")
151
+
152
+
153
+ def external_claude_team_json_source(path: Path) -> ExternalJsonSource:
154
+ """현재 사용자의 Claude team roster 설정만 승인한다."""
155
+ root = Path.home() / ".claude" / "teams"
156
+ try:
157
+ Path(os.path.abspath(path)).relative_to(Path(os.path.abspath(root)))
158
+ path.resolve(strict=False).relative_to(root.resolve(strict=False))
159
+ except ValueError as exc:
160
+ raise JsonBoundaryError(path, "Claude JSON", "outside Claude team root") from exc
161
+ if path.name != "config.json" or path.is_symlink():
162
+ raise JsonBoundaryError(path, "Claude JSON", "outside Claude team root")
163
+ return _issue_external_source(path, "Claude Code")
164
+
165
+
166
+ def external_tool_json_source(path: Path, workspace_root: Path) -> ExternalJsonSource:
167
+ """외부 도구가 workspace의 비-Okstra 경로에 쓴 보고서만 승인한다."""
168
+ try:
169
+ path.resolve(strict=False).relative_to(workspace_root.resolve(strict=False))
170
+ except ValueError as exc:
171
+ raise JsonBoundaryError(path, "tool JSON", "outside tool workspace") from exc
172
+ if _is_okstra_owned_path(path):
173
+ raise JsonBoundaryError(path, "tool JSON", "Okstra-owned path")
174
+ return _issue_external_source(path, "external tool")
175
+
176
+
177
+ def external_error_sidecar_source(path: Path) -> ExternalJsonSource:
178
+ """worker-results lane 또는 Okstra 밖 레거시 sidecar만 승인한다."""
179
+ if _is_okstra_owned_path(path):
180
+ raise JsonBoundaryError(
181
+ path, "worker error sidecar", "trusted worker lane is required"
182
+ )
183
+ return _issue_external_source(path, "legacy automation")
184
+
185
+
186
+ class _NonstandardJsonConstant(ValueError):
187
+ """Raised by ``json.loads`` when a non-standard numeric token appears."""
188
+
189
+
190
+ def load_owned_object(
191
+ path: Path,
192
+ *,
193
+ artifact: str,
194
+ schema: Mapping[str, Any] | None = None,
195
+ validate_cross_fields: Callable[[dict[str, Any]], object] | None = None,
196
+ ) -> dict[str, Any]:
197
+ """Load and validate one okstra-owned JSON object."""
198
+ return load_owned_object_snapshot(
199
+ path,
200
+ artifact=artifact,
201
+ schema=schema,
202
+ validate_cross_fields=validate_cross_fields,
203
+ ).value
204
+
205
+
206
+ def load_owned_object_snapshot(
207
+ path: Path,
208
+ *,
209
+ artifact: str,
210
+ schema: Mapping[str, Any] | None = None,
211
+ validate_cross_fields: Callable[[dict[str, Any]], object] | None = None,
212
+ ) -> OwnedJsonSnapshot:
213
+ """한 번 읽은 바이트에서 객체를 파싱·검증해 함께 반환한다."""
214
+ try:
215
+ raw_bytes = path.read_bytes()
216
+ value = json.loads(
217
+ raw_bytes.decode("utf-8"),
218
+ parse_constant=_reject_nonstandard_json_constant,
219
+ )
220
+ except FileNotFoundError as exc:
221
+ raise JsonBoundaryError(path, artifact, "missing") from exc
222
+ except UnicodeDecodeError as exc:
223
+ raise JsonBoundaryError(path, artifact, "invalid JSON encoding") from exc
224
+ except _NonstandardJsonConstant as exc:
225
+ raise JsonBoundaryError(path, artifact, str(exc)) from exc
226
+ except json.JSONDecodeError as exc:
227
+ raise JsonBoundaryError(path, artifact, "invalid JSON") from exc
228
+ except OSError as exc:
229
+ raise JsonBoundaryError(path, artifact, f"I/O error while reading ({exc})") from exc
230
+ return OwnedJsonSnapshot(
231
+ value=_validate_owned(
232
+ value,
233
+ path=path,
234
+ artifact=artifact,
235
+ schema=schema,
236
+ validate_cross_fields=validate_cross_fields,
237
+ ),
238
+ raw_bytes=raw_bytes,
239
+ )
240
+
241
+
242
+ def load_external_json(
243
+ source: ExternalJsonSource,
244
+ *,
245
+ artifact: str,
246
+ object_pairs_hook: Callable[[list[tuple[str, Any]]], Any] | None = None,
247
+ ) -> Any:
248
+ """Load external JSON without applying an owned-artifact contract."""
249
+ if not isinstance(source, ExternalJsonSource):
250
+ raise TypeError("load_external_json requires ExternalJsonSource")
251
+ path = source.path
252
+ try:
253
+ return json.loads(
254
+ path.read_text(encoding="utf-8"),
255
+ object_pairs_hook=object_pairs_hook,
256
+ )
257
+ except FileNotFoundError as exc:
258
+ raise JsonBoundaryError(path, artifact, "missing") from exc
259
+ except UnicodeDecodeError as exc:
260
+ raise JsonBoundaryError(path, artifact, "invalid JSON encoding") from exc
261
+ except json.JSONDecodeError as exc:
262
+ raise JsonBoundaryError(path, artifact, "invalid JSON") from exc
263
+ except OSError as exc:
264
+ raise JsonBoundaryError(path, artifact, f"I/O error while reading ({exc})") from exc
265
+
266
+
267
+ def write_owned_object_atomic(
268
+ path: Path,
269
+ payload: Mapping[str, Any],
270
+ *,
271
+ artifact: str,
272
+ schema: Mapping[str, Any] | None = None,
273
+ validate_cross_fields: Callable[[dict[str, Any]], object] | None = None,
274
+ ) -> None:
275
+ """Validate a payload before atomically replacing an owned artifact."""
276
+ serialized = serialize_owned_object(
277
+ path,
278
+ payload,
279
+ artifact=artifact,
280
+ schema=schema,
281
+ validate_cross_fields=validate_cross_fields,
282
+ )
283
+ try:
284
+ path.parent.mkdir(parents=True, exist_ok=True)
285
+ descriptor, raw_temp = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
286
+ except OSError as exc:
287
+ raise JsonBoundaryError(path, artifact, f"I/O error while preparing write ({exc})") from exc
288
+ temporary = Path(raw_temp)
289
+ try:
290
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
291
+ handle.write(serialized)
292
+ handle.flush()
293
+ os.fsync(handle.fileno())
294
+ os.replace(temporary, path)
295
+ except (TypeError, ValueError) as exc:
296
+ raise JsonBoundaryError(path, artifact, f"JSON serialization failed ({exc})") from exc
297
+ except OSError as exc:
298
+ raise JsonBoundaryError(path, artifact, f"I/O error while writing ({exc})") from exc
299
+ finally:
300
+ temporary.unlink(missing_ok=True)
301
+
302
+
303
+ def serialize_owned_object(
304
+ path: Path,
305
+ payload: Mapping[str, Any],
306
+ *,
307
+ artifact: str,
308
+ schema: Mapping[str, Any] | None = None,
309
+ validate_cross_fields: Callable[[dict[str, Any]], object] | None = None,
310
+ ) -> str:
311
+ """전용 게시 프로토콜이 사용할 검증된 canonical 객체 문자열이다."""
312
+ owned = _validate_owned(
313
+ payload, path=path, artifact=artifact, schema=schema,
314
+ validate_cross_fields=validate_cross_fields,
315
+ )
316
+ try:
317
+ return json.dumps(
318
+ owned, ensure_ascii=False, indent=2, allow_nan=False
319
+ ) + "\n"
320
+ except (TypeError, ValueError) as exc:
321
+ raise JsonBoundaryError(
322
+ path, artifact, f"JSON serialization failed ({exc})"
323
+ ) from exc
324
+
325
+
326
+ def mutate_owned_object_atomic(
327
+ path: Path,
328
+ mutation: Callable[[dict[str, Any]], dict[str, Any]],
329
+ **contract: Any,
330
+ ) -> dict[str, Any]:
331
+ """Load, mutate, validate, and atomically replace one owned object."""
332
+ payload = load_owned_object(path, **contract)
333
+ updated = mutation(dict(payload))
334
+ write_owned_object_atomic(path, updated, **contract)
335
+ return updated
336
+
337
+
338
+ def _reject_nonstandard_json_constant(constant: str) -> None:
339
+ raise _NonstandardJsonConstant(f"non-standard JSON constant: {constant}")
340
+
341
+
342
+ def _validate_owned(
343
+ value: object,
344
+ *,
345
+ path: Path,
346
+ artifact: str,
347
+ schema: Mapping[str, Any] | None,
348
+ validate_cross_fields: Callable[[dict[str, Any]], object] | None,
349
+ ) -> dict[str, Any]:
350
+ if not isinstance(value, Mapping):
351
+ raise JsonBoundaryError(path, artifact, "top-level JSON value must be an object")
352
+ owned = dict(value)
353
+ if schema is not None:
354
+ try:
355
+ schema_errors = validate_schema(owned, dict(schema))
356
+ except SchemaError as exc:
357
+ raise JsonBoundaryError(path, artifact, f"invalid schema ({exc})") from exc
358
+ if schema_errors:
359
+ raise JsonBoundaryError(path, artifact, f"schema validation failed ({schema_errors[0]})")
360
+ if validate_cross_fields is not None:
361
+ errors = validate_cross_fields(owned)
362
+ if isinstance(errors, str):
363
+ errors = [errors]
364
+ if errors:
365
+ raise JsonBoundaryError(path, artifact, f"cross-field validation failed ({next(iter(errors))})")
366
+ return owned
@@ -3,11 +3,15 @@ from __future__ import annotations
3
3
 
4
4
  import contextlib
5
5
  import fcntl
6
- import json
7
- import os
8
6
  from collections.abc import Callable, Iterator
9
7
  from pathlib import Path
10
8
 
9
+ from .json_boundary import (
10
+ JsonBoundaryError,
11
+ load_owned_object,
12
+ write_owned_object_atomic,
13
+ )
14
+
11
15
 
12
16
  @contextlib.contextmanager
13
17
  def registry_lock(lock_path: Path) -> Iterator[None]:
@@ -26,18 +30,12 @@ def registry_lock(lock_path: Path) -> Iterator[None]:
26
30
 
27
31
  def load_registry_json(path: Path, default_factory: Callable[[], dict]) -> dict:
28
32
  """Load a registry dict, or a fresh default when absent/corrupt."""
29
- if not path.exists():
30
- return default_factory()
31
33
  try:
32
- data = json.loads(path.read_text(encoding="utf-8"))
33
- except (OSError, json.JSONDecodeError):
34
+ return load_owned_object(path, artifact="registry")
35
+ except JsonBoundaryError:
34
36
  return default_factory()
35
- return data if isinstance(data, dict) else default_factory()
36
37
 
37
38
 
38
39
  def save_registry_json(path: Path, data: dict) -> None:
39
- """Persist registry JSON via temp file + ``os.replace``."""
40
- path.parent.mkdir(parents=True, exist_ok=True)
41
- tmp = path.with_suffix(".json.tmp")
42
- tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
43
- os.replace(tmp, path)
40
+ """Persist registry JSON through the owned-artifact boundary."""
41
+ write_owned_object_atomic(path, data, artifact="registry")
@@ -8,14 +8,31 @@ from pathlib import Path
8
8
  from typing import Callable, List, Optional
9
9
 
10
10
 
11
- def append_jsonl(path: Path, row: dict) -> None:
11
+ def append_jsonl(
12
+ path: Path, row: dict, *, ensure_ascii: bool = True, compact: bool = True
13
+ ) -> None:
12
14
  """jsonl 파일 끝에 한 줄 append. 파일이 없으면 생성."""
13
15
  path.parent.mkdir(parents=True, exist_ok=True)
14
- line = json.dumps(row, separators=(",", ":")) + "\n"
16
+ line = json.dumps(
17
+ row, separators=(",", ":") if compact else None,
18
+ ensure_ascii=ensure_ascii,
19
+ ) + "\n"
15
20
  with path.open("a") as f:
16
21
  f.write(line)
17
22
 
18
23
 
24
+ def rewrite_jsonl(path: Path, rows: List[dict], *, ensure_ascii: bool = True) -> None:
25
+ """JSONL 행 전체를 임시 파일에 쓴 뒤 원자 교체한다."""
26
+ path.parent.mkdir(parents=True, exist_ok=True)
27
+ temporary = path.with_suffix(path.suffix + ".tmp")
28
+ with temporary.open("w", encoding="utf-8") as handle:
29
+ for row in rows:
30
+ handle.write(json.dumps(
31
+ row, separators=(",", ":"), ensure_ascii=ensure_ascii
32
+ ) + "\n")
33
+ os.replace(temporary, path)
34
+
35
+
19
36
  def read_jsonl(path: Path) -> List[dict]:
20
37
  """jsonl 파일을 모두 읽어 dict 목록으로 반환. 파싱 실패 라인은 스킵."""
21
38
  if not path.is_file():
@@ -2,7 +2,9 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import json
5
+ import hashlib
5
6
  import re
7
+ import uuid
6
8
  from contextlib import contextmanager
7
9
  from dataclasses import dataclass, field, replace
8
10
  from pathlib import Path
@@ -107,14 +109,44 @@ def _next_activity_id(events: list[LeadEvent]) -> str:
107
109
  return f"A-{max(numbers, default=0) + 1:03d}"
108
110
 
109
111
 
112
+ def _activity_request_digest(details: Mapping[str, Any]) -> str:
113
+ payload = {
114
+ key: value for key, value in details.items()
115
+ if key not in {"activityId", "activityRequestDigest", "activityRequestRef", "activityVersion"}
116
+ }
117
+ encoded = json.dumps(_stable_json_value(payload), ensure_ascii=False, separators=(",", ":"))
118
+ return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
119
+
120
+
110
121
  def append_activity_event(path: Path, event: LeadEvent) -> LeadEvent:
111
122
  """Assign the next activity ID and append the event under one lock."""
112
123
  if event.event_type != "activity":
113
124
  raise ValueError("activity append requires eventType=activity")
114
125
  with _event_log_lock(path):
115
126
  details = dict(event.details)
127
+ request_ref = details.get("activityRequestRef")
128
+ events = read_lead_events(path)
129
+ digest = _activity_request_digest(details)
130
+ if request_ref is not None:
131
+ if not isinstance(request_ref, str) or not request_ref:
132
+ raise ValueError("activityRequestRef must be a non-empty string")
133
+ for previous in events:
134
+ if (
135
+ previous.event_type == event.event_type
136
+ and previous.lead_runtime == event.lead_runtime
137
+ and previous.task_key == event.task_key
138
+ and previous.task_type == event.task_type
139
+ and previous.run_seq == event.run_seq
140
+ and previous.details.get("activityRequestRef") == request_ref
141
+ ):
142
+ if previous.details.get("activityRequestDigest") != digest:
143
+ raise ValueError("activity request reference has different payload")
144
+ return previous
145
+ raise ValueError("unknown activity request reference")
146
+ details["activityRequestRef"] = f"activity-{uuid.uuid4().hex}"
147
+ details["activityRequestDigest"] = digest
116
148
  details["activityVersion"] = 1
117
- details["activityId"] = _next_activity_id(read_lead_events(path))
149
+ details["activityId"] = _next_activity_id(events)
118
150
  stored = replace(event, details=details)
119
151
  _append_unlocked(path, stored)
120
152
  return stored