@galaxyproject/orbit 0.0.0 → 0.9.0

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 (451) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/LICENSE +21 -0
  3. package/README.md +760 -5
  4. package/bin/legacy-mcp-config.js +363 -0
  5. package/bin/orbit.js +810 -0
  6. package/bin/pi-extension-path.d.ts +4 -0
  7. package/bin/pi-extension-path.js +44 -0
  8. package/bin/provider-auth.d.ts +22 -0
  9. package/bin/provider-auth.js +77 -0
  10. package/bin/thinking-pref.js +38 -0
  11. package/bin/update-check.d.ts +15 -0
  12. package/bin/update-check.js +137 -0
  13. package/extensions/cli-update/index.ts +32 -0
  14. package/extensions/orbit/activity-hooks.ts +141 -0
  15. package/extensions/orbit/activity.ts +116 -0
  16. package/extensions/orbit/agent-dir.ts +11 -0
  17. package/extensions/orbit/auto-resume.ts +352 -0
  18. package/extensions/orbit/config.ts +3 -0
  19. package/extensions/orbit/confusables.ts +1 -0
  20. package/extensions/orbit/context.ts +957 -0
  21. package/extensions/orbit/dashboard-commands.ts +191 -0
  22. package/extensions/orbit/dashboard-store.ts +473 -0
  23. package/extensions/orbit/dashboard-tools.ts +1185 -0
  24. package/extensions/orbit/empty-completion.ts +99 -0
  25. package/extensions/orbit/evidence-gate.ts +1276 -0
  26. package/extensions/orbit/evidence-override-command.ts +294 -0
  27. package/extensions/orbit/exec-guard/bash-risk.ts +1408 -0
  28. package/extensions/orbit/exec-guard/gate.ts +181 -0
  29. package/extensions/orbit/exec-guard/guardian-config.ts +54 -0
  30. package/extensions/orbit/exec-guard/index.ts +1 -0
  31. package/extensions/orbit/exec-guard/model-tier.ts +36 -0
  32. package/extensions/orbit/exec-guard/path-jail.ts +54 -0
  33. package/extensions/orbit/exec-guard/policy.ts +342 -0
  34. package/extensions/orbit/exec-guard/runtime-state.ts +15 -0
  35. package/extensions/orbit/exec-guard/sensitive-read.ts +525 -0
  36. package/extensions/orbit/exec-guard/types.ts +42 -0
  37. package/extensions/orbit/execution-commands.ts +134 -0
  38. package/extensions/orbit/feedback-command.ts +81 -0
  39. package/extensions/orbit/feedback.ts +96 -0
  40. package/extensions/orbit/galaxy-api.test.ts +122 -0
  41. package/extensions/orbit/galaxy-api.ts +606 -0
  42. package/extensions/orbit/galaxy-artifact-link-guidance.ts +5 -0
  43. package/extensions/orbit/galaxy-destructive-gate.ts +113 -0
  44. package/extensions/orbit/galaxy-enrich.ts +1108 -0
  45. package/extensions/orbit/galaxy-failure.ts +101 -0
  46. package/extensions/orbit/galaxy-fixture.ts +126 -0
  47. package/extensions/orbit/galaxy-history-guard.ts +233 -0
  48. package/extensions/orbit/galaxy-job-block.ts +347 -0
  49. package/extensions/orbit/galaxy-job-ops.ts +288 -0
  50. package/extensions/orbit/galaxy-live-source.ts +929 -0
  51. package/extensions/orbit/galaxy-markdown-adapter.ts +248 -0
  52. package/extensions/orbit/galaxy-op-policy.ts +332 -0
  53. package/extensions/orbit/galaxy-ops-tools.ts +326 -0
  54. package/extensions/orbit/galaxy-output-store.ts +80 -0
  55. package/extensions/orbit/galaxy-output.ts +346 -0
  56. package/extensions/orbit/galaxy-page-binding.ts +222 -0
  57. package/extensions/orbit/galaxy-page-markdown-guidance.ts +15 -0
  58. package/extensions/orbit/galaxy-page-ops.ts +159 -0
  59. package/extensions/orbit/galaxy-page-sync.test.ts +417 -0
  60. package/extensions/orbit/galaxy-page-sync.ts +318 -0
  61. package/extensions/orbit/galaxy-pages-api.ts +203 -0
  62. package/extensions/orbit/galaxy-pages-sync.ts +409 -0
  63. package/extensions/orbit/galaxy-poll-guard.ts +189 -0
  64. package/extensions/orbit/galaxy-poller.ts +706 -0
  65. package/extensions/orbit/galaxy-provenance.ts +623 -0
  66. package/extensions/orbit/galaxy-reconcile.ts +956 -0
  67. package/extensions/orbit/galaxy-recovery.ts +176 -0
  68. package/extensions/orbit/galaxy-repeat-guard.ts +55 -0
  69. package/extensions/orbit/galaxy-submission-capture.ts +707 -0
  70. package/extensions/orbit/galaxy-submission.ts +502 -0
  71. package/extensions/orbit/galaxy-tool-source.ts +78 -0
  72. package/extensions/orbit/galaxy-tool-wrap.ts +234 -0
  73. package/extensions/orbit/galaxy-udt-block.ts +127 -0
  74. package/extensions/orbit/galaxy-upload-tus.ts +334 -0
  75. package/extensions/orbit/galaxy-upload.ts +300 -0
  76. package/extensions/orbit/galaxy-version-capture.ts +65 -0
  77. package/extensions/orbit/git.ts +173 -0
  78. package/extensions/orbit/harness-block-fields.ts +597 -0
  79. package/extensions/orbit/index.ts +593 -0
  80. package/extensions/orbit/init-gate.ts +284 -0
  81. package/extensions/orbit/instructions-command.ts +136 -0
  82. package/extensions/orbit/invocation-failure-hint.ts +57 -0
  83. package/extensions/orbit/iwc-plan-check.ts +94 -0
  84. package/extensions/orbit/lesson-command.ts +303 -0
  85. package/extensions/orbit/lesson-hint.ts +245 -0
  86. package/extensions/orbit/lesson-nudge.ts +106 -0
  87. package/extensions/orbit/lessons/compose.ts +177 -0
  88. package/extensions/orbit/lessons/counters.ts +120 -0
  89. package/extensions/orbit/lessons/enabled.ts +116 -0
  90. package/extensions/orbit/lessons/matcher.ts +346 -0
  91. package/extensions/orbit/lessons/paths.ts +286 -0
  92. package/extensions/orbit/lessons/pi-event-contract.ts +105 -0
  93. package/extensions/orbit/lessons/propose-replay.ts +103 -0
  94. package/extensions/orbit/lessons/propose-tool.ts +222 -0
  95. package/extensions/orbit/lessons/propose.ts +502 -0
  96. package/extensions/orbit/lessons/replay.ts +89 -0
  97. package/extensions/orbit/lessons/reproduction-index.ts +83 -0
  98. package/extensions/orbit/lessons/search-tool.ts +99 -0
  99. package/extensions/orbit/lessons/search.ts +136 -0
  100. package/extensions/orbit/lessons/store.ts +415 -0
  101. package/extensions/orbit/lessons/text.ts +36 -0
  102. package/extensions/orbit/lessons/types.ts +69 -0
  103. package/extensions/orbit/lessons/user-dir.ts +24 -0
  104. package/extensions/orbit/lessons/user-lesson.ts +88 -0
  105. package/extensions/orbit/lessons/wrapper.ts +61 -0
  106. package/extensions/orbit/lessons-command.ts +147 -0
  107. package/extensions/orbit/local-exec.ts +39 -0
  108. package/extensions/orbit/mcp-output.ts +179 -0
  109. package/extensions/orbit/mcp-servers.test.ts +24 -0
  110. package/extensions/orbit/mcp-servers.ts +25 -0
  111. package/extensions/orbit/model-script.ts +351 -0
  112. package/extensions/orbit/notebook-anchors.ts +342 -0
  113. package/extensions/orbit/notebook-writer.ts +863 -0
  114. package/extensions/orbit/observation-allowlists.ts +1109 -0
  115. package/extensions/orbit/observation-replay.ts +148 -0
  116. package/extensions/orbit/observation-triggers.ts +563 -0
  117. package/extensions/orbit/observation-ui.ts +175 -0
  118. package/extensions/orbit/observations-command.ts +421 -0
  119. package/extensions/orbit/observations-config.ts +200 -0
  120. package/extensions/orbit/observations.ts +1381 -0
  121. package/extensions/orbit/profiles.ts +270 -0
  122. package/extensions/orbit/progress-updates.ts +95 -0
  123. package/extensions/orbit/proposal-commands.ts +882 -0
  124. package/extensions/orbit/provenance-fs-file.ts +59 -0
  125. package/extensions/orbit/provenance-fs.ts +32 -0
  126. package/extensions/orbit/provenance-memory-fs.ts +59 -0
  127. package/extensions/orbit/record-store-file.ts +177 -0
  128. package/extensions/orbit/record-store-page.ts +77 -0
  129. package/extensions/orbit/record-store-seam.ts +47 -0
  130. package/extensions/orbit/record-store.ts +149 -0
  131. package/extensions/orbit/registry-carrier.ts +107 -0
  132. package/extensions/orbit/registry-eligibility.ts +95 -0
  133. package/extensions/orbit/registry-evaluation.ts +403 -0
  134. package/extensions/orbit/registry-evaluator.ts +519 -0
  135. package/extensions/orbit/registry-import.ts +91 -0
  136. package/extensions/orbit/registry-lock.ts +221 -0
  137. package/extensions/orbit/registry-memory-fs.ts +42 -0
  138. package/extensions/orbit/registry-page-carrier.ts +103 -0
  139. package/extensions/orbit/registry-proposal.ts +1348 -0
  140. package/extensions/orbit/registry-runtime.ts +234 -0
  141. package/extensions/orbit/registry-schema.ts +735 -0
  142. package/extensions/orbit/registry-submitter.ts +59 -0
  143. package/extensions/orbit/registry-templates.ts +346 -0
  144. package/extensions/orbit/registry.ts +476 -0
  145. package/extensions/orbit/sandbox/index.ts +120 -0
  146. package/extensions/orbit/sandbox/sandbox-bash.ts +93 -0
  147. package/extensions/orbit/sandbox/sandbox-config.ts +87 -0
  148. package/extensions/orbit/sandbox/sandbox-init-message.ts +28 -0
  149. package/extensions/orbit/secret-redaction.ts +247 -0
  150. package/extensions/orbit/session-index/cwd.ts +18 -0
  151. package/extensions/orbit/session-index/db.ts +56 -0
  152. package/extensions/orbit/session-index/indexer.ts +154 -0
  153. package/extensions/orbit/session-index/is-enabled.ts +17 -0
  154. package/extensions/orbit/session-index/parse.ts +254 -0
  155. package/extensions/orbit/session-index/query.ts +209 -0
  156. package/extensions/orbit/session-index/schema.ts +72 -0
  157. package/extensions/orbit/session-index/tools.ts +152 -0
  158. package/extensions/orbit/session-lifecycle.ts +321 -0
  159. package/extensions/orbit/skill-triggers.ts +159 -0
  160. package/extensions/orbit/skills-command.ts +48 -0
  161. package/extensions/orbit/skills-discovery.ts +435 -0
  162. package/extensions/orbit/skills.ts +84 -0
  163. package/extensions/orbit/sra-import-gate.ts +77 -0
  164. package/extensions/orbit/state.ts +265 -0
  165. package/extensions/orbit/submission-replay.ts +147 -0
  166. package/extensions/orbit/sync-command.ts +157 -0
  167. package/extensions/orbit/teams/critic-parser.ts +93 -0
  168. package/extensions/orbit/teams/dispatcher.ts +210 -0
  169. package/extensions/orbit/teams/is-enabled.ts +22 -0
  170. package/extensions/orbit/teams/tool.ts +187 -0
  171. package/extensions/orbit/teams/types.ts +57 -0
  172. package/extensions/orbit/teams/validate.ts +42 -0
  173. package/extensions/orbit/tester-id-command.ts +133 -0
  174. package/extensions/orbit/tools-sync.ts +279 -0
  175. package/extensions/orbit/tools.ts +1300 -0
  176. package/extensions/orbit/types.ts +41 -0
  177. package/extensions/orbit/ui-bridge.ts +55 -0
  178. package/extensions/orbit/ulid.ts +60 -0
  179. package/extensions/orbit/user-instructions.ts +388 -0
  180. package/extensions/orbit/vendor/skills/_catalog.json +68 -0
  181. package/extensions/orbit/vendor/skills/_manifest.json +1106 -0
  182. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/SKILL.md +112 -0
  183. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_provenance.json +218 -0
  184. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_required_tools.json +33 -0
  185. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_verify.json +4 -0
  186. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/add.json +50 -0
  187. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-extract.json +51 -0
  188. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-next-step.json +42 -0
  189. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-validate.json +140 -0
  190. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/list.json +32 -0
  191. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/notes/galaxy-tool-job-failure-reference.md +137 -0
  192. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/notes/open-requirements-ledger.md +173 -0
  193. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/schemas/galaxy-tool-summary.schema.json +175 -0
  194. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/schemas/galaxy-workflow-draft.schema.json +2727 -0
  195. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/SKILL.md +82 -0
  196. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_provenance.json +133 -0
  197. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_required_tools.json +16 -0
  198. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_verify.json +4 -0
  199. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/cli/draft-validate.json +140 -0
  200. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/notes/galaxy-workflow-draft-format.md +135 -0
  201. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/notes/open-requirements-ledger.md +173 -0
  202. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/schemas/galaxy-workflow-draft.schema.json +2727 -0
  203. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/SKILL.md +84 -0
  204. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/_provenance.json +166 -0
  205. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/_verify.json +29 -0
  206. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/galaxy-workflow-testability-design.md +136 -0
  207. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
  208. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/iwc-test-data-conventions.md +308 -0
  209. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/planemo-asserts-idioms.md +246 -0
  210. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/planemo-workflow-test-architecture.md +151 -0
  211. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/galaxy-workflow-test-plan.schema.json +552 -0
  212. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/summary-galaxy-workflow.schema.json +687 -0
  213. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/tests-format.schema.json +7328 -0
  214. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/SKILL.md +127 -0
  215. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_provenance.json +198 -0
  216. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_required_tools.json +16 -0
  217. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_verify.json +4 -0
  218. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/cli/convert.json +123 -0
  219. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/galaxy-data-flow-draft-contract.md +129 -0
  220. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
  221. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/iwc-test-data-conventions.md +308 -0
  222. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/open-requirements-ledger.md +173 -0
  223. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/SKILL.md +73 -0
  224. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/_provenance.json +160 -0
  225. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/_verify.json +4 -0
  226. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-collection-semantics.md +43 -0
  227. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-collection-semantics.yml +1250 -0
  228. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-tool-job-failure-reference.md +137 -0
  229. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
  230. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
  231. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/nextflow-operators-to-galaxy-collection-recipes.md +98 -0
  232. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/planemo-asserts-idioms.md +246 -0
  233. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/planemo-workflow-test-architecture.md +151 -0
  234. package/extensions/orbit/vendor/skills/discover-shed-tool/SKILL.md +174 -0
  235. package/extensions/orbit/vendor/skills/discover-shed-tool/_provenance.json +130 -0
  236. package/extensions/orbit/vendor/skills/discover-shed-tool/_required_tools.json +18 -0
  237. package/extensions/orbit/vendor/skills/discover-shed-tool/_verify.json +17 -0
  238. package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-revisions.json +48 -0
  239. package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-search.json +95 -0
  240. package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-versions.json +39 -0
  241. package/extensions/orbit/vendor/skills/discover-shed-tool/references/notes/component-tool-shed-search.md +230 -0
  242. package/extensions/orbit/vendor/skills/discover-shed-tool/references/schemas/galaxy-tool-discovery.schema.json +231 -0
  243. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/SKILL.md +93 -0
  244. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_provenance.json +201 -0
  245. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_required_tools.json +28 -0
  246. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_verify.json +17 -0
  247. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/cli/planemo.md +44 -0
  248. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/cli/validate-tests.json +40 -0
  249. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/galaxy-workflow-testability-design.md +136 -0
  250. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
  251. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/iwc-test-data-conventions.md +308 -0
  252. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/planemo-asserts-idioms.md +246 -0
  253. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/planemo-workflow-test-architecture.md +151 -0
  254. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/schemas/galaxy-workflow-test-plan.schema.json +552 -0
  255. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/schemas/tests-format.schema.json +7328 -0
  256. package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/SKILL.md +81 -0
  257. package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/_provenance.json +93 -0
  258. package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/_verify.json +17 -0
  259. package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/references/notes/open-requirements-ledger.md +173 -0
  260. package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/references/schemas/summary-galaxy-workflow.schema.json +687 -0
  261. package/extensions/orbit/vendor/skills/pipeline-update-interview-to-galaxy/SKILL.md +81 -0
  262. package/extensions/orbit/vendor/skills/pipeline-update-interview-to-galaxy/_assembly.json +23 -0
  263. package/extensions/orbit/vendor/skills/run-workflow-test/SKILL.md +79 -0
  264. package/extensions/orbit/vendor/skills/run-workflow-test/_provenance.json +125 -0
  265. package/extensions/orbit/vendor/skills/run-workflow-test/_required_tools.json +28 -0
  266. package/extensions/orbit/vendor/skills/run-workflow-test/_verify.json +4 -0
  267. package/extensions/orbit/vendor/skills/run-workflow-test/references/cli/planemo.md +44 -0
  268. package/extensions/orbit/vendor/skills/run-workflow-test/references/cli/validate-tests.json +40 -0
  269. package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
  270. package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/planemo-asserts-idioms.md +246 -0
  271. package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/planemo-workflow-test-architecture.md +151 -0
  272. package/extensions/orbit/vendor/skills/run-workflow-test/references/schemas/tests-format.schema.json +7328 -0
  273. package/extensions/orbit/vendor/skills/skills/LICENSE +21 -0
  274. package/extensions/orbit/vendor/skills/skills/collection-manipulation/README.md +39 -0
  275. package/extensions/orbit/vendor/skills/skills/collection-manipulation/SKILL.md +892 -0
  276. package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/api-patterns.md +927 -0
  277. package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/apply-rules.md +1482 -0
  278. package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/test-patterns.md +996 -0
  279. package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/tools.md +609 -0
  280. package/extensions/orbit/vendor/skills/skills/galaxy-data-libraries/SKILL.md +170 -0
  281. package/extensions/orbit/vendor/skills/skills/galaxy-integration/README.md +162 -0
  282. package/extensions/orbit/vendor/skills/skills/galaxy-integration/SKILL.md +99 -0
  283. package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/README.md +119 -0
  284. package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/tool-checking.md +419 -0
  285. package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/workflow-testing.md +617 -0
  286. package/extensions/orbit/vendor/skills/skills/galaxy-integration/galaxy-integration.md +614 -0
  287. package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/SKILL.md +144 -0
  288. package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/average_col3.ipynb +68 -0
  289. package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/extract_sample_metadata.ipynb +190 -0
  290. package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/variant_annotation.ipynb +816 -0
  291. package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/vcp_variant_map.ipynb +443 -0
  292. package/extensions/orbit/vendor/skills/skills/galaxy-integration/scripts/README.md +165 -0
  293. package/extensions/orbit/vendor/skills/skills/galaxy-integration/scripts/galaxy_tool_checker.py +521 -0
  294. package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/SKILL.md +128 -0
  295. package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/gotchas.md +135 -0
  296. package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/history-access.md +132 -0
  297. package/extensions/orbit/vendor/skills/skills/reproduciblify/SKILL.md +146 -0
  298. package/extensions/orbit/vendor/skills/skills/reproduciblify/references/directives.yml +496 -0
  299. package/extensions/orbit/vendor/skills/skills/udt-authoring/SKILL.md +245 -0
  300. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/01-minimal-echo.yml +27 -0
  301. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/02-single-data-in-out.yml +27 -0
  302. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/03-multiple-data-js.yml +28 -0
  303. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/04-select-integer-boolean.yml +41 -0
  304. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/05-resource-requirements.yml +25 -0
  305. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/06-biocontainer-tool.yml +38 -0
  306. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/07-inline-python-script.yml +56 -0
  307. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/08-configfile-script.yml +74 -0
  308. package/extensions/orbit/vendor/skills/skills/udt-authoring/references/common-mistakes.md +76 -0
  309. package/extensions/orbit/vendor/skills/skills/udt-authoring/references/schema-reference.md +155 -0
  310. package/extensions/orbit/vendor/skills/skills/udt-authoring/references/templating.md +196 -0
  311. package/extensions/orbit/vendor/skills/skills/udt-authoring/scripts/validate.py +109 -0
  312. package/extensions/orbit/vendor/skills/skills/workflow-reports/README.md +71 -0
  313. package/extensions/orbit/vendor/skills/skills/workflow-reports/SKILL.md +190 -0
  314. package/extensions/orbit/vendor/skills/skills/workflow-reports/examples/histology-staining.md +145 -0
  315. package/extensions/orbit/vendor/skills/skills/workflow-reports/examples/tissue-microarray-analysis.md +176 -0
  316. package/extensions/orbit/vendor/skills/skills/workflow-reports/references/directives.md +415 -0
  317. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/SKILL.md +98 -0
  318. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_provenance.json +93 -0
  319. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_required_tools.json +28 -0
  320. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_verify.json +17 -0
  321. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/convert.json +123 -0
  322. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/foundry.md +25 -0
  323. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/validate.json +142 -0
  324. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/schemas/summary-galaxy-workflow.schema.json +687 -0
  325. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/SKILL.md +74 -0
  326. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_provenance.json +71 -0
  327. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_required_tools.json +16 -0
  328. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_verify.json +4 -0
  329. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/cli/validate.json +142 -0
  330. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
  331. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/notes/galaxy-workflow-testability-design.md +136 -0
  332. package/extensions/orbit/vendor-skills.ts +283 -0
  333. package/extensions/orbit/workspace-state-dir.ts +46 -0
  334. package/extensions/orbit-handoff/index.ts +73 -0
  335. package/extensions/orbit-handoff/orbit-launcher.ts +142 -0
  336. package/extensions/whats-new/index.ts +98 -0
  337. package/lessons/LICENSE +17 -0
  338. package/lessons/README.md +117 -0
  339. package/lessons/SCHEMA.md +187 -0
  340. package/lessons/build-snapshot.mjs +311 -0
  341. package/lessons/data/downloaded-file-is-not-what-its-extension-says.md +61 -0
  342. package/lessons/galaxy-api/403-history-is-a-hard-stop.md +57 -0
  343. package/lessons/galaxy-api/collection-into-single-dataset-input.md +61 -0
  344. package/lessons/galaxy-api/connectedvalue-in-command-line.md +62 -0
  345. package/lessons/galaxy-api/hid-is-not-an-id.md +59 -0
  346. package/lessons/galaxy-api/invoke-workflow-inputs-not-params.md +60 -0
  347. package/lessons/galaxy-api/repeat-param-pipe-keys.md +63 -0
  348. package/lessons/galaxy-api/run-tool-returns-on-submit.md +61 -0
  349. package/lessons/galaxy-tools/reference-index-not-on-server.md +62 -0
  350. package/lessons/log.md +42 -0
  351. package/lessons/reproduction/input-population-mismatch.md +59 -0
  352. package/lessons/reproduction/methods-text-vs-executed-parameters.md +62 -0
  353. package/lessons/snapshot.json +1004 -0
  354. package/lessons/stats/de-contrast-direction-and-sample-labels.md +63 -0
  355. package/lessons/stats/na-coerced-to-zero-in-filters.md +59 -0
  356. package/lessons/validate.mjs +1219 -0
  357. package/package.json +103 -5
  358. package/shared/agent-prompt.d.ts +8 -0
  359. package/shared/agent-prompt.js +15 -0
  360. package/shared/approval-prompt.d.ts +4 -0
  361. package/shared/approval-prompt.js +61 -0
  362. package/shared/brain-env.d.ts +14 -0
  363. package/shared/brain-env.js +108 -0
  364. package/shared/brain-exit.d.ts +20 -0
  365. package/shared/brain-exit.js +149 -0
  366. package/shared/capability-profile.d.ts +16 -0
  367. package/shared/capability-profile.js +75 -0
  368. package/shared/config-source.d.ts +13 -0
  369. package/shared/config-source.js +17 -0
  370. package/shared/confusables.d.ts +3 -0
  371. package/shared/confusables.js +93 -0
  372. package/shared/custom-provider.d.ts +45 -0
  373. package/shared/custom-provider.js +169 -0
  374. package/shared/dashboard-contract.d.ts +123 -0
  375. package/shared/dashboard-contract.js +560 -0
  376. package/shared/dashboard-layout-store.d.ts +44 -0
  377. package/shared/dashboard-layout-store.js +281 -0
  378. package/shared/feedback-contract.d.ts +61 -0
  379. package/shared/feedback-contract.js +146 -0
  380. package/shared/galaxy-artifact-links.d.ts +30 -0
  381. package/shared/galaxy-artifact-links.js +166 -0
  382. package/shared/galaxy-credentials.d.ts +26 -0
  383. package/shared/galaxy-credentials.js +71 -0
  384. package/shared/galaxy-destructive.d.ts +21 -0
  385. package/shared/galaxy-destructive.js +269 -0
  386. package/shared/galaxy-guidance.d.ts +2 -0
  387. package/shared/galaxy-guidance.js +53 -0
  388. package/shared/galaxy-live-contract.d.ts +92 -0
  389. package/shared/galaxy-live-contract.js +180 -0
  390. package/shared/galaxy-page-cells.d.ts +2 -0
  391. package/shared/galaxy-page-cells.js +22 -0
  392. package/shared/galaxy-tool-family.d.ts +5 -0
  393. package/shared/galaxy-tool-family.js +27 -0
  394. package/shared/galaxy-url.d.ts +1 -0
  395. package/shared/galaxy-url.js +48 -0
  396. package/shared/gtn.d.ts +31 -0
  397. package/shared/gtn.js +253 -0
  398. package/shared/handled-prompt.d.ts +3 -0
  399. package/shared/handled-prompt.js +26 -0
  400. package/shared/known-provider.d.ts +11 -0
  401. package/shared/known-provider.js +89 -0
  402. package/shared/lesson-hint-marker.d.ts +7 -0
  403. package/shared/lesson-hint-marker.js +32 -0
  404. package/shared/lesson-rules.d.ts +150 -0
  405. package/shared/lesson-rules.js +203 -0
  406. package/shared/malformed-object-ids.d.ts +1 -0
  407. package/shared/malformed-object-ids.js +37 -0
  408. package/shared/markdown-sections.d.ts +10 -0
  409. package/shared/markdown-sections.js +118 -0
  410. package/shared/notebook-fences.d.ts +12 -0
  411. package/shared/notebook-fences.js +53 -0
  412. package/shared/observation-contract.d.ts +91 -0
  413. package/shared/observation-contract.js +544 -0
  414. package/shared/orbit-config.d.ts +226 -0
  415. package/shared/orbit-config.js +189 -0
  416. package/shared/orbit-env.d.ts +13 -0
  417. package/shared/orbit-env.js +124 -0
  418. package/shared/orbit-shell-contract.d.ts +74 -0
  419. package/shared/orbit-shell-contract.js +30 -0
  420. package/shared/page-content-hash.d.ts +1 -0
  421. package/shared/page-content-hash.js +13 -0
  422. package/shared/pi-default-model.d.ts +7 -0
  423. package/shared/pi-default-model.js +41 -0
  424. package/shared/prompt-blocks.d.ts +87 -0
  425. package/shared/prompt-blocks.js +898 -0
  426. package/shared/provider-auth-caps.d.ts +22 -0
  427. package/shared/provider-auth-caps.js +57 -0
  428. package/shared/random.d.ts +2 -0
  429. package/shared/random.js +15 -0
  430. package/shared/record-excerpt.d.ts +4 -0
  431. package/shared/record-excerpt.js +16 -0
  432. package/shared/redirect-guard.d.ts +36 -0
  433. package/shared/redirect-guard.js +235 -0
  434. package/shared/skills-pin.d.ts +8 -0
  435. package/shared/skills-pin.js +9 -0
  436. package/shared/skills-router.d.ts +17 -0
  437. package/shared/skills-router.js +63 -0
  438. package/shared/sra-import.d.ts +34 -0
  439. package/shared/sra-import.js +206 -0
  440. package/shared/state-dir.d.ts +21 -0
  441. package/shared/state-dir.js +240 -0
  442. package/shared/team-dispatch-contract.d.ts +28 -0
  443. package/shared/team-dispatch-contract.js +1 -0
  444. package/shared/truncate-middle.d.ts +11 -0
  445. package/shared/truncate-middle.js +68 -0
  446. package/shared/version-compare.d.ts +10 -0
  447. package/shared/version-compare.js +74 -0
  448. package/shared/whats-new.d.ts +31 -0
  449. package/shared/whats-new.js +83 -0
  450. package/shared/wsl.d.ts +10 -0
  451. package/shared/wsl.js +24 -0
@@ -0,0 +1,1276 @@
1
+ /**
2
+ * Evidence gate for plan-step completion.
3
+ *
4
+ * Orbit's system prompt says "Evidence comes before assertion -- you must run an
5
+ * actual verification step before marking a notebook step complete", and
6
+ * `docs/agent/galaxy-routing.md` is sharper still: "Treat invocation YAML
7
+ * status as Galaxy job state. Treat the plan checkbox as verified-result
8
+ * state." Nothing checked either one. Orbit hard-gates what can hurt you
9
+ * (exec-guard, write-jail, destructive deletes) and asked nicely for the thing
10
+ * the product is actually for.
11
+ *
12
+ * ## Deny on contradiction, warn on absence
13
+ *
14
+ * The tempting design -- "block a checkbox flip that arrives without evidence"
15
+ * -- is wrong here, and the codebase says so. `/execute` teaches
16
+ * *evidence-first*: step 5 "Write the verification evidence into the notebook
17
+ * before changing status", step 6 "Only after verification succeeds, edit the
18
+ * markdown checkbox". So a flip-only edit is the **documented honest shape**,
19
+ * and a gate keyed on "this edit added nothing" would fire on exactly the
20
+ * behaviour Orbit teaches. Absence of evidence is not decidable from one write:
21
+ * Galaxy evidence lands in a `loom-invocation` block at end of file, prose
22
+ * evidence may sit in a results section, and the honest sequence spans two
23
+ * edits.
24
+ *
25
+ * What *is* decidable is **contradiction**: a claim in the notebook that a
26
+ * record the model can't author disagrees with. There are two such records,
27
+ * so there are two verdict paths.
28
+ *
29
+ * ## The registry path: steps with a recorded run
30
+ *
31
+ * When the approval registry (`registry.ts`) holds an attempt bound to the
32
+ * step that has a submission -- a run the registry knows about -- the verdict
33
+ * is the registry's. The step is held while none of its runs is
34
+ * `handoff_eligible` (recomputed here, so a revocation held in memory counts)
35
+ * and the user hasn't overridden any of them. The evaluation writer
36
+ * (`registry-evaluator.ts`) sets that flag from Galaxy's own answers, and the
37
+ * registry is signed by the session, so nothing in the notebook moves it.
38
+ * Only checkbox states are read from the notebook, by anchor, anywhere in the
39
+ * file:
40
+ *
41
+ * - a held step arriving at `- [x]` is denied, whatever its block's `status:`
42
+ * says (the old two-edit split);
43
+ * - a held step whose checkbox disappears -- an anchor renamed, the line
44
+ * fenced, the box broken -- is denied, which is what stops a rename from
45
+ * carrying a completion past the check in one edit or two;
46
+ * - a plan-heading rename doesn't hide a step, because anchored checkboxes are
47
+ * found outside `## Plan` sections too.
48
+ *
49
+ * Marking a held step `- [!]` is always allowed. An approval nothing has been
50
+ * submitted for yet is not a run and leaves the step to the legacy path.
51
+ *
52
+ * ## The legacy path: runs the registry doesn't know
53
+ *
54
+ * Until `loom_submit` (registry slice 3) records gated submissions, that is
55
+ * every raw submission. When a plan step flips to `- [x]` while the
56
+ * `loom-invocation` block bound to it still reads `in_progress`, the agent is
57
+ * claiming a verified result for a run Galaxy says has not finished. That
58
+ * status is written by the poller, so it can't be satisfied by writing a
59
+ * convincing sentence -- but it is notebook text, and so is the step's
60
+ * identity, which leaves the three gaps listed below.
61
+ *
62
+ * Everything else is recorded, not blocked. A flip with no invocation binding
63
+ * (a local step, say) gets no opinion at all: guessing there produces the false
64
+ * positives that get gates switched off.
65
+ *
66
+ * ## Modes
67
+ *
68
+ * `off | warn | deny`, from `LOOM_EVIDENCE_GATE` or `evidenceGate.mode` in the
69
+ * config, default `deny`. Every decision that involves a completion is
70
+ * recorded to `activity.jsonl` as `evidence.decision`, with the
71
+ * contradiction's source, so `scripts/evidence-gate-audit.mjs` can tabulate
72
+ * what deny would have done; `docs/evidence-gate-audit.md` holds the audit
73
+ * the default rests on.
74
+ *
75
+ * ## The exception is the user's, not the model's
76
+ *
77
+ * The first cut of this gate let the second attempt on a step through, which
78
+ * made the decision advisory: a model that disagrees only has to ask twice. So
79
+ * the deny persists for as long as the contradiction does, and the escape
80
+ * hatch is `/override <step-key> <reason>` (`evidence-override-command.ts`) --
81
+ * a person, a named step, a recorded reason. On the registry path it is
82
+ * written into the registry as an evidence-gate exception on the runs it held
83
+ * the step for; a later run on the step is held again. On the legacy path it
84
+ * is a one-shot token for the run in flight. It is also the way out of the
85
+ * pinned-`in_progress` case: credentials that vanish mid-session, or an
86
+ * invocation that ends with nothing ok and nothing failed.
87
+ *
88
+ * ## What remains
89
+ *
90
+ * 1. **Legacy path, two-edit split.** Rewrite `status:` in one edit, flip the
91
+ * checkbox in the next: the pre-image already says `completed`.
92
+ * 2. **Legacy path, anchor rename plus flip.** The post-image step key is not
93
+ * the pre-image's, so it does not read as a flip.
94
+ * 3. **Legacy path, plan-heading rename plus flip.** The step stops parsing.
95
+ * All three have tests pinning them, and close for raw submissions only
96
+ * when slice 3 records them in the registry.
97
+ * 4. **Bash.** `WRITE_TOOLS` is `write` and `edit`. On the desktop the
98
+ * exec-guard denies shell writes to `notebook.md`, `activity.jsonl` and the
99
+ * registry and provenance directories outright (a floor, not an ask), and
100
+ * never auto-allows a line that names them, so an interpreter one-liner
101
+ * asks the user. A script that finds the files itself still can, and is out
102
+ * of scope for a command-string classifier.
103
+ * 5. **`activity.jsonl` through the file tools.** Not floored there: the
104
+ * activity log is an audit trail, not an input to any verdict, now that the
105
+ * verdict lives in the signed registry.
106
+ * 6. **Positional anchors.** A step with no `{#id}` is addressed by position
107
+ * (`plan-a-step-2`), so inserting a step above it moves its address onto
108
+ * another line. On the registry path, different text at a held positional
109
+ * address is treated as that move and refused, which also means a held
110
+ * positional step can't be retitled until it's eligible; explicit anchors
111
+ * don't have the problem. */
112
+
113
+ import fs from "node:fs";
114
+ import os from "node:os";
115
+ import path from "node:path";
116
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
117
+ import { loadConfig } from "./config";
118
+ import { getNotebookPath } from "./state";
119
+ import { isRecordMissing, recordStore } from "./record-store";
120
+ import { appendActivityEvent } from "./activity";
121
+ import { findInvocationBlocks, type InvocationYaml } from "./notebook-writer";
122
+ import { findJobBlocks, type JobYaml } from "./galaxy-job-block";
123
+ import { attemptOwns, readAttemptRecordSync } from "./galaxy-provenance";
124
+ import { collectCheckboxSteps } from "./notebook-anchors";
125
+ import { computeHandoffEligible, type Attempt, type Registry } from "./registry";
126
+ import { NO_APPROVAL_REVISION } from "./registry-evaluation";
127
+ import { getSessionRegistry, sessionView } from "./registry-runtime";
128
+ import { readEnv } from "../../shared/orbit-env.js";
129
+ import { applySectionEdit, repeatedHeadingRefusal } from "../../shared/markdown-sections.js";
130
+ import type { GalaxyToolFamily } from "../../shared/galaxy-tool-family.js";
131
+
132
+ /** pi emits its built-in file tools lowercase; mirrors exec-guard's FILE_WRITE_TOOLS. */
133
+ const WRITE_TOOLS = new Set(["write", "edit"]);
134
+
135
+ export type EvidenceGateMode = "off" | "warn" | "deny";
136
+
137
+ export function resolveMode(): EvidenceGateMode {
138
+ const env = readEnv("EVIDENCE_GATE")?.trim().toLowerCase();
139
+ if (env === "off" || env === "warn" || env === "deny") return env;
140
+ const cfg = loadConfig() as { evidenceGate?: { mode?: string } };
141
+ const mode = cfg.evidenceGate?.mode?.trim().toLowerCase();
142
+ if (mode === "off" || mode === "warn" || mode === "deny") return mode;
143
+ return "deny";
144
+ }
145
+
146
+ /** `## Plan A: Title [routing]` -- same shape init-gate.ts parses. */
147
+ const PLAN_HEADING = /^##\s+Plan\s+([^:]+):\s*(.+?)(?:\s*\[(local|galaxy|hybrid|remote)\])?\s*$/i;
148
+ const ANY_H2 = /^##\s+/;
149
+ /** A plan step checkbox line: indent + open bracket, state char, close, rest. */
150
+ const STEP_LINE = /^(\s*-\s+\[)([ xX!])(\]\s+)(.*)$/;
151
+ const ANCHOR = /\{#([^}]+)\}/;
152
+
153
+ export interface PlanStep {
154
+ /** Stable identity across edits: the anchor when present, else a normalized title. */
155
+ key: string;
156
+ anchor?: string;
157
+ state: " " | "x" | "!";
158
+ text: string;
159
+ }
160
+
161
+ /**
162
+ * Normalize a step's text into a stable key. Strips the ordinal, bold wrapper,
163
+ * anchor and punctuation so an edit that renumbers or retitles cosmetically
164
+ * doesn't read as a different step.
165
+ */
166
+ function normalizeTitle(raw: string): string {
167
+ return raw
168
+ .replace(ANCHOR, "")
169
+ .replace(/^\d+\.\s*/, "")
170
+ .replace(/\*\*([^*]+)\*\*/, "$1")
171
+ .replace(/[—\-:|].*$/, "")
172
+ .trim()
173
+ .toLowerCase()
174
+ .replace(/\s+/g, " ");
175
+ }
176
+
177
+ /**
178
+ * Parse every checkbox line that sits inside a `## Plan X:` section.
179
+ *
180
+ * Scoped deliberately: a plain markdown to-do list elsewhere in the notebook is
181
+ * not a plan step, and gating it would be a false positive on ordinary note
182
+ * taking. Keyed rather than line-indexed, because inserting a line anywhere
183
+ * above destroys line identity while leaving the step untouched.
184
+ */
185
+ export function parsePlanSteps(content: string): Map<string, PlanStep> {
186
+ const out = new Map<string, PlanStep>();
187
+ let inPlan = false;
188
+ let inFence = false;
189
+ let planKey = "";
190
+ for (const line of content.split("\n")) {
191
+ // Fenced content is quoted, not asserted. A plan section that shows an
192
+ // example checkbox in a ``` block was otherwise parsed as a real step, and
193
+ // because the map is keyed by anchor and last-write-wins, an example
194
+ // carrying the same anchor as a real step overwrote the real step's state
195
+ // -- flip the step and paste a pending copy of it in a fence below, and the
196
+ // completion vanished from the post-image. Also the honest reading: the
197
+ // `loom-invocation` blocks are fenced too, and nothing in one is a plan
198
+ // step.
199
+ if (/^\s*(```|~~~)/.test(line)) {
200
+ inFence = !inFence;
201
+ continue;
202
+ }
203
+ if (inFence) continue;
204
+ const heading = line.match(PLAN_HEADING);
205
+ if (heading) {
206
+ inPlan = true;
207
+ planKey = heading[1].trim().toLowerCase();
208
+ continue;
209
+ }
210
+ if (ANY_H2.test(line)) {
211
+ inPlan = false;
212
+ continue;
213
+ }
214
+ if (!inPlan) continue;
215
+ const m = line.match(STEP_LINE);
216
+ if (!m) continue;
217
+ const text = m[4];
218
+ const anchor = text.match(ANCHOR)?.[1];
219
+ const key = anchor ? `#${anchor}` : `${planKey}#${normalizeTitle(text)}`;
220
+ const state = (m[2] === "X" ? "x" : m[2]) as PlanStep["state"];
221
+ out.set(key, { key, anchor, state, text: text.trim() });
222
+ }
223
+ return out;
224
+ }
225
+
226
+ /**
227
+ * Steps that arrived at `- [x]` from some other state between two versions of
228
+ * the notebook.
229
+ *
230
+ * The claim being adjudicated is "this step is verified complete", so every way
231
+ * of arriving at it counts: `- [ ]` -> `- [x]` and `- [!]` -> `- [x]` alike.
232
+ * Keying only on the pending state, as the first cut did, left a two-edit
233
+ * bypass that never has to touch the invocation block at all -- mark the step
234
+ * failed, then mark it complete, and neither edit is a flip the gate can see.
235
+ *
236
+ * Gating the failed->complete transition costs nothing on the honest path.
237
+ * Recovering from a failure means rerunning, and a rerun that finished leaves a
238
+ * `completed` block for the anchor, which `findContradictions` already fails
239
+ * open on. What is left is the flip made while the rerun is still in flight,
240
+ * which is the contradiction this gate exists for.
241
+ *
242
+ * The reverse directions stay silent. `- [ ]` -> `- [!]` records a failure,
243
+ * which is the honest behaviour the discipline asks for and must never be
244
+ * gated, and `- [x]` -> `- [ ]` is reopening. A step that is `[x]` in the new
245
+ * content with no counterpart in the old is not a flip either -- that rule is
246
+ * what keeps a first write, or a wholesale plan regeneration, from reading as a
247
+ * pile of unevidenced completions.
248
+ */
249
+ export function detectCompletions(before: string, after: string): PlanStep[] {
250
+ const pre = parsePlanSteps(before);
251
+ const post = parsePlanSteps(after);
252
+ const flips: PlanStep[] = [];
253
+ for (const [key, step] of post) {
254
+ if (step.state !== "x") continue;
255
+ const prior = pre.get(key);
256
+ if (prior && prior.state !== "x") flips.push(step);
257
+ }
258
+ return flips;
259
+ }
260
+
261
+ /** A flip the step's own `loom-invocation` block contradicts (the legacy path). */
262
+ export interface BlockContradiction {
263
+ source: "block";
264
+ step: PlanStep;
265
+ invocation: InvocationYaml;
266
+ }
267
+
268
+ /**
269
+ * A write the registry contradicts: the step has a recorded run, and the
270
+ * record says that run is not eligible to hand off. `flip` is the step arriving
271
+ * at `- [x]`; `vanished` is the write removing the step's checkbox altogether,
272
+ * which is how a rename would otherwise carry the completion past the check.
273
+ */
274
+ export interface RegistryContradiction {
275
+ source: "registry";
276
+ step: PlanStep;
277
+ kind: "flip" | "vanished";
278
+ hold: RegistryHold;
279
+ }
280
+
281
+ export type Contradiction = BlockContradiction | RegistryContradiction;
282
+
283
+ /**
284
+ * Flips that contradict machine-owned state: the step's own `loom-invocation`
285
+ * block still says the run is in progress.
286
+ *
287
+ * Three properties this depends on, all verified against the code rather than
288
+ * assumed, because each one is a way to get this badly wrong:
289
+ *
290
+ * 1. **Read the PRE-image, never the post-image.** The block is ordinary
291
+ * plaintext in the same file, so an edit that flips the checkbox *and*
292
+ * rewrites `status: in_progress` to `completed` in one hunk would clear a
293
+ * post-image check by construction -- the sole deny path defeating itself in
294
+ * one call. The pre-image is what Galaxy's poller last wrote, before the
295
+ * edit under adjudication. (A two-edit split -- rewrite status, then flip --
296
+ * still evades this; see the module note on the out-of-band record.)
297
+ *
298
+ * 2. **`in_progress` only, never `failed`.** `failed` is a rolled-up
299
+ * any-job-errored verdict, it is sticky, and `checkInvocations` polls only
300
+ * `in_progress` blocks (`tools.ts:650`) so it can never be re-polled back
301
+ * out. Denying on it would block the legitimate flip after a successful
302
+ * rerun, with no remediation the agent could actually execute.
303
+ *
304
+ * 3. **Anchors are not unique.** `upsertInvocationBlock` keys on
305
+ * `invocation_id` (`notebook-writer.ts:283`), so a rerun leaves a second
306
+ * block with the same `notebook_anchor`. If ANY block for the anchor reads
307
+ * `completed`, the step is not contradicted -- the common retry shape is a
308
+ * stale `failed`/`in_progress` block beside the fresh `completed` one, and
309
+ * failing open there is the difference between a gate people keep and a
310
+ * gate people switch off.
311
+ */
312
+ export function findContradictions(before: string, flips: PlanStep[]): BlockContradiction[] {
313
+ const blocks = findInvocationBlocks(before);
314
+ if (blocks.length === 0) return [];
315
+ const byAnchor = new Map<string, InvocationYaml[]>();
316
+ for (const b of blocks) {
317
+ if (!b.notebookAnchor) continue;
318
+ const list = byAnchor.get(b.notebookAnchor) ?? [];
319
+ list.push(b);
320
+ byAnchor.set(b.notebookAnchor, list);
321
+ }
322
+ const out: BlockContradiction[] = [];
323
+ for (const step of flips) {
324
+ if (!step.anchor) continue;
325
+ const list = byAnchor.get(step.anchor);
326
+ if (!list || list.length === 0) continue;
327
+ if (list.some((b) => b.status === "completed")) continue; // a run for this step did finish
328
+ const pending = list.find((b) => b.status === "in_progress");
329
+ if (pending) out.push({ source: "block", step, invocation: pending });
330
+ }
331
+ return out;
332
+ }
333
+
334
+ // ─────────────────────────────────────────────────────────────────────────────
335
+ // The registry path
336
+ // ─────────────────────────────────────────────────────────────────────────────
337
+
338
+ /** A step the registry holds: it has a recorded run, and nothing lets it through. */
339
+ export interface RegistryHold {
340
+ anchor: string;
341
+ /** Attempts bound to the step that have a submission. */
342
+ attemptIds: string[];
343
+ /** What the record says is missing, for the reason and the activity row. */
344
+ missing: string[];
345
+ }
346
+
347
+ /** The user's `/override`, recorded as an evidence-gate exception on the attempt. */
348
+ export function gateExcepted(attempt: Attempt, exceptions: Registry["exceptions"]): boolean {
349
+ const revision = attempt.approval?.spec_revision ?? NO_APPROVAL_REVISION;
350
+ return exceptions.some(
351
+ (x) =>
352
+ x.attempt_id === attempt.attempt_id &&
353
+ x.by === "user" &&
354
+ x.scope === "evidence_gate" &&
355
+ x.assertion_id === undefined &&
356
+ x.spec_revision === revision,
357
+ );
358
+ }
359
+
360
+ function missingFacts(a: Attempt): string[] {
361
+ const ev = a.evaluation;
362
+ if (!ev) return ["not evaluated yet"];
363
+ const out: string[] = [];
364
+ if (ev.authority !== "established" || a.provenance?.authority !== "established") {
365
+ out.push("not re-verified against Galaxy in this session");
366
+ }
367
+ if (ev.execution !== "success") out.push(`execution ${ev.execution}`);
368
+ if (ev.conformity !== "conformant" && ev.conformity !== "excepted") {
369
+ out.push(`conformity ${ev.conformity}`);
370
+ }
371
+ if (ev.predicate_result !== "pass" && ev.predicate_result !== "attested") {
372
+ out.push(`predicate ${ev.predicate_result}`);
373
+ }
374
+ if (ev.integrity !== "ok") out.push(`integrity ${ev.integrity}`);
375
+ return out.length > 0 ? out : ["the record's checks don't line up"];
376
+ }
377
+
378
+ /**
379
+ * Which steps the registry has an opinion on, and which of those it holds.
380
+ *
381
+ * A step has an opinion when an attempt bound to it has a submission -- a run
382
+ * the registry knows about. An approval nothing has been submitted for yet is
383
+ * not a run, so it leaves the step to the legacy path. A step is held when
384
+ * none of its runs is eligible and the user hasn't overridden any of them; one
385
+ * eligible run is enough, the same fail-open the legacy path gives a step with
386
+ * one completed block beside stale ones.
387
+ *
388
+ * `handoff_eligible` is recomputed here rather than read off the attempt, so a
389
+ * revocation the session holds in memory (`sessionView`) counts at once.
390
+ */
391
+ export function registryHolds(registry: Registry | null | undefined): {
392
+ bound: Set<string>;
393
+ held: Map<string, RegistryHold>;
394
+ } {
395
+ const bound = new Set<string>();
396
+ const held = new Map<string, RegistryHold>();
397
+ if (!registry) return { bound, held };
398
+ const byAnchor = new Map<string, Attempt[]>();
399
+ for (const a of Object.values(registry.attempts)) {
400
+ if (!a.submission || a.binding.step_anchor === "unattributed") continue;
401
+ const list = byAnchor.get(a.binding.step_anchor) ?? [];
402
+ list.push(a);
403
+ byAnchor.set(a.binding.step_anchor, list);
404
+ }
405
+ for (const [anchor, attempts] of byAnchor) {
406
+ bound.add(anchor);
407
+ const through = attempts.some(
408
+ (a) => computeHandoffEligible(a, registry.exceptions) || gateExcepted(a, registry.exceptions),
409
+ );
410
+ if (through) continue;
411
+ held.set(anchor, {
412
+ anchor,
413
+ attemptIds: attempts.map((a) => a.attempt_id),
414
+ missing: [...new Set(attempts.flatMap(missingFacts))],
415
+ });
416
+ }
417
+ return { bound, held };
418
+ }
419
+
420
+ /** Checkbox states by anchor; a step written twice is complete if either copy says so. */
421
+ function checkboxesByAnchor(content: string): Map<string, PlanStep & { positional?: true }> {
422
+ const out = new Map<string, PlanStep & { positional?: true }>();
423
+ for (const box of collectCheckboxSteps(content)) {
424
+ const prior = out.get(box.anchor);
425
+ if (prior?.state === "x") continue;
426
+ out.set(box.anchor, {
427
+ key: `#${box.anchor}`,
428
+ anchor: box.anchor,
429
+ state: box.state,
430
+ text: box.text,
431
+ ...(box.explicit ? {} : { positional: true as const }),
432
+ });
433
+ }
434
+ return out;
435
+ }
436
+
437
+ /** A step's text with the ordinal, the state and the spacing taken out. */
438
+ function stepIdentity(text: string): string {
439
+ return text
440
+ .replace(/^\d+\.\s*/, "")
441
+ .replace(/\s+/g, " ")
442
+ .trim()
443
+ .toLowerCase();
444
+ }
445
+
446
+ /**
447
+ * Writes the registry contradicts. Reads only checkbox states from the
448
+ * notebook -- the verdict is the registry's -- so rewriting a block's
449
+ * `status:` first changes nothing (gap 1), and because steps are found by
450
+ * anchor anywhere in the file, renaming the plan heading doesn't hide one
451
+ * (gap 3). Removing a held step's checkbox is refused outright, which is what
452
+ * keeps a rename from carrying a completion past the check in one edit or two
453
+ * (gap 2): mark it `- [!]` instead, or ask for `/override`.
454
+ */
455
+ export function findRegistryContradictions(
456
+ before: string,
457
+ after: string,
458
+ registry: Registry | null | undefined,
459
+ opts: { newStepsCount?: boolean } = {},
460
+ ): RegistryContradiction[] {
461
+ const newStepsCount = opts.newStepsCount ?? true;
462
+ const { held } = registryHolds(registry);
463
+ if (held.size === 0) return [];
464
+ const pre = checkboxesByAnchor(before);
465
+ const post = checkboxesByAnchor(after);
466
+ const out: RegistryContradiction[] = [];
467
+ for (const [anchor, hold] of held) {
468
+ const was = pre.get(anchor);
469
+ const now = post.get(anchor);
470
+ // A step the notebook never had arriving complete counts for an edit (the
471
+ // model wrote it). A pull or resume replacing the notebook wholesale --
472
+ // a fresh container resuming its Page -- brings steps in whatever state
473
+ // they were left, and judging those before reconcile has re-checked them
474
+ // would refuse the honest resume.
475
+ if (now?.state === "x" && was?.state !== "x" && (was || newStepsCount)) {
476
+ out.push({ source: "registry", step: now, kind: "flip", hold });
477
+ } else if (was && !now) {
478
+ out.push({ source: "registry", step: was, kind: "vanished", hold });
479
+ } else if (was?.positional && now && stepIdentity(was.text) !== stepIdentity(now.text)) {
480
+ // A step with no `{#id}` is addressed by position, so a line inserted
481
+ // above it moves its address onto another step -- and the original could
482
+ // then be completed under an address nothing holds. Different text at a
483
+ // held positional address is that move, and is refused like a removal.
484
+ out.push({ source: "registry", step: was, kind: "vanished", hold });
485
+ }
486
+ }
487
+ return out;
488
+ }
489
+
490
+ /** The registry as this session reads it, or null when there is none. */
491
+ export function currentRegistryView(): Registry | null {
492
+ const session = getSessionRegistry();
493
+ if (!session || session.unavailable) return null;
494
+ return sessionView(session);
495
+ }
496
+
497
+ /**
498
+ * Every plan step the gate would currently fire on, regardless of what its
499
+ * checkbox says right now. The gate itself only adjudicates flips, but the
500
+ * user asking "what is blocked, and under what key?" is asking about the
501
+ * standing state -- and after a deny the step is still `- [ ]`, so keying the
502
+ * answer off the checkbox would report nothing. Same predicate, applied to
503
+ * every step in the file.
504
+ */
505
+ export function outstandingContradictions(
506
+ content: string,
507
+ registry: Registry | null = null,
508
+ ): Contradiction[] {
509
+ const { bound, held } = registryHolds(registry);
510
+ const steps = [...parsePlanSteps(content).values()].filter(
511
+ (s) => !s.anchor || !bound.has(s.anchor),
512
+ );
513
+ const out: Contradiction[] = findContradictions(content, steps);
514
+ const boxes = checkboxesByAnchor(content);
515
+ for (const [anchor, hold] of held) {
516
+ const step = boxes.get(anchor) ?? {
517
+ key: `#${anchor}`,
518
+ anchor,
519
+ state: " " as const,
520
+ text: anchor,
521
+ };
522
+ out.push({ source: "registry", step, kind: "flip", hold });
523
+ }
524
+ return out;
525
+ }
526
+
527
+ /**
528
+ * Resolve a user-typed step key against the notebook. Accepts the gate's own
529
+ * key (`#plan-a-step-2`), the bare anchor as it appears in the markdown
530
+ * (`plan-a-step-2`), and either spelled with different case -- anchors are
531
+ * copied out of the notebook by hand, and a gate whose override is hard to
532
+ * address is a gate people work around.
533
+ */
534
+ export function resolveStepKey(
535
+ content: string,
536
+ input: string,
537
+ registry: Registry | null = null,
538
+ ): PlanStep | null {
539
+ const raw = input.trim();
540
+ if (!raw) return null;
541
+ // Plan steps first, then any anchored checkbox (a step whose heading was
542
+ // renamed is still that step to the registry), then a step the registry holds
543
+ // that the notebook no longer shows at all.
544
+ const boxes = checkboxesByAnchor(content);
545
+ const held = registryHolds(registry).held;
546
+ const sources: Map<string, PlanStep>[] = [
547
+ parsePlanSteps(content),
548
+ boxes,
549
+ new Map(
550
+ [...held.keys()]
551
+ .filter((a) => !boxes.has(a))
552
+ .map((a) => [`#${a}`, { key: `#${a}`, anchor: a, state: " " as const, text: a }]),
553
+ ),
554
+ ];
555
+ for (const steps of sources) {
556
+ for (const candidate of [raw, `#${raw}`]) {
557
+ const hit = steps.get(candidate);
558
+ if (hit) return hit;
559
+ }
560
+ const lowered = raw.toLowerCase();
561
+ for (const [key, step] of steps) {
562
+ if (key.toLowerCase() === lowered || key.toLowerCase() === `#${lowered}`) return step;
563
+ }
564
+ }
565
+ return null;
566
+ }
567
+
568
+ /**
569
+ * Mirror of pi's fuzzy-match normalization (`core/tools/edit-diff.ts`,
570
+ * `normalizeForFuzzyMatch`), reimplemented rather than imported because
571
+ * reaching into pi's `dist/core/**` from here is exactly the deep-path
572
+ * coupling that has broken loom on an upstream repackage before.
573
+ *
574
+ * It has to exist. pi's `edit` does not require `oldText` to match byte for
575
+ * byte: it tries an exact `indexOf` first and then retries in this normalized
576
+ * space, so an `oldText` whose em-dash arrived as a hyphen, or whose line lost
577
+ * its trailing space, still lands on disk. A gate that only did the exact
578
+ * match would silently abstain on those -- and models normalize punctuation
579
+ * when echoing text back all the time, so that is a hole a model falls through
580
+ * by accident as often as on purpose, under-counting the warn-mode audit as
581
+ * well as letting a deny-mode flip past.
582
+ */
583
+ function normalizeForMatch(text: string): string {
584
+ return text
585
+ .normalize("NFKC")
586
+ .split("\n")
587
+ .map((line) => line.trimEnd())
588
+ .join("\n")
589
+ .replace(/[\u2018\u2019\u201A\u201B]/g, "'")
590
+ .replace(/[\u201C\u201D\u201E\u201F]/g, '"')
591
+ .replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g, "-")
592
+ .replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " ");
593
+ }
594
+
595
+ /** pi normalizes both the file and every edit to LF before matching. */
596
+ function toLF(text: string): string {
597
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
598
+ }
599
+
600
+ interface Span {
601
+ start: number;
602
+ end: number;
603
+ newText: string;
604
+ }
605
+
606
+ /**
607
+ * Locate every edit the way pi does, or null if pi would refuse the call.
608
+ *
609
+ * Three things here are pi's behaviour rather than the obvious behaviour, and
610
+ * each of them was a way past the gate when this simulated the obvious one:
611
+ *
612
+ * 1. **Every edit matches against the ORIGINAL file, not against the running
613
+ * result.** pi resolves all matches up front and only then applies them.
614
+ * Folding them in sequence let one edit manufacture the text a later edit
615
+ * matched: insert a `[ ]` into a prose line, then "flip" that, and the
616
+ * simulator saw a checkbox appear in prose while pi -- matching the second
617
+ * edit against the original -- flipped the real plan step.
618
+ * 2. **A non-unique `oldText` is refused outright** (`getDuplicateError`), and
619
+ * so is an overlapping pair. Abstaining on those is not a gap: the call
620
+ * errors and nothing reaches the disk.
621
+ * 3. **Line endings are normalized to LF first.** A `newText` ending `\r\n`
622
+ * left a stray CR in the simulated line, which `STEP_LINE` then failed to
623
+ * parse, so the completion disappeared from the post-image while pi wrote
624
+ * it happily.
625
+ */
626
+ function locateEdits(
627
+ content: string,
628
+ edits: { oldText: string; newText: string }[],
629
+ ): Span[] | null {
630
+ const spans: Span[] = [];
631
+ for (const edit of edits) {
632
+ const oldText = toLF(edit.oldText);
633
+ const newText = toLF(edit.newText);
634
+ if (oldText.length === 0) return null; // pi throws on an empty oldText
635
+ const first = content.indexOf(oldText);
636
+ if (first === -1) return null; // caller retries in pi's fuzzy space
637
+ if (content.indexOf(oldText, first + 1) !== -1) return null; // pi: duplicate, refused
638
+ spans.push({ start: first, end: first + oldText.length, newText });
639
+ }
640
+ const ordered = [...spans].sort((a, b) => a.start - b.start);
641
+ for (let i = 1; i < ordered.length; i++) {
642
+ if (ordered[i - 1].end > ordered[i].start) return null; // pi: overlapping, refused
643
+ }
644
+ return ordered;
645
+ }
646
+
647
+ /** Apply located spans right to left so earlier offsets stay valid. */
648
+ function applySpans(content: string, spans: Span[]): string {
649
+ let out = content;
650
+ for (let i = spans.length - 1; i >= 0; i--) {
651
+ out = out.slice(0, spans[i].start) + spans[i].newText + out.slice(spans[i].end);
652
+ }
653
+ return out;
654
+ }
655
+
656
+ /**
657
+ * Reconstruct what the file will contain after this call, or null when we
658
+ * can't tell. Null always means "no opinion" -- an unreadable file or an edit
659
+ * pi itself would not land is not ours to adjudicate, and guessing would
660
+ * manufacture exactly the false positives that get gates disabled.
661
+ */
662
+ export function computeAfterContent(
663
+ before: string,
664
+ toolName: string,
665
+ input: Record<string, unknown>,
666
+ ): string | null {
667
+ if (toolName === "write") {
668
+ return typeof input.content === "string" ? input.content : null;
669
+ }
670
+ if (toolName === "update_page") return pageAfterContent(before, input);
671
+ if (toolName !== "edit") return null;
672
+ const raw = input.edits;
673
+ if (!Array.isArray(raw)) return null;
674
+ const edits: { oldText: string; newText: string }[] = [];
675
+ for (const entry of raw) {
676
+ const e = entry as { oldText?: unknown; newText?: unknown };
677
+ if (typeof e.oldText !== "string" || typeof e.newText !== "string") return null;
678
+ edits.push({ oldText: e.oldText, newText: e.newText });
679
+ }
680
+ if (edits.length === 0) return null;
681
+
682
+ const content = toLF(before);
683
+ const exact = locateEdits(content, edits);
684
+ if (exact) return applySpans(content, exact);
685
+
686
+ // Nothing matched exactly, so pi is in its fuzzy pass, where it rewrites the
687
+ // whole file in normalized space. Simulate on the same footing.
688
+ const fuzzyContent = normalizeForMatch(content);
689
+ const fuzzyEdits = edits.map((e) => ({
690
+ oldText: normalizeForMatch(toLF(e.oldText)),
691
+ newText: toLF(e.newText),
692
+ }));
693
+ const fuzzy = locateEdits(fuzzyContent, fuzzyEdits);
694
+ if (!fuzzy) return null;
695
+ return applySpans(fuzzyContent, fuzzy);
696
+ }
697
+
698
+ /**
699
+ * The record page after an update_page call, as the page bridge would write it: whole content,
700
+ * or one section rewritten. A title-only call, or a combination the bridge refuses (a repeated
701
+ * heading among them), writes no body we can judge.
702
+ */
703
+ function pageAfterContent(before: string, input: Record<string, unknown>): string | null {
704
+ const { content, section_heading: heading, section_content: section } = input;
705
+ const sectioned = heading != null || section != null;
706
+ if (typeof content === "string") return sectioned ? null : content;
707
+ if (typeof heading === "string" && typeof section === "string") {
708
+ return repeatedHeadingRefusal(before, heading)
709
+ ? null
710
+ : applySectionEdit(before, heading, section);
711
+ }
712
+ return null;
713
+ }
714
+
715
+ export function contradictionReason(c: Contradiction): string {
716
+ if (c.source === "registry") return registryReason(c);
717
+ return (
718
+ `Marking "${c.step.text}" complete contradicts its own Galaxy invocation ` +
719
+ `block, which reads \`status: ${c.invocation.status}\`. That status is written ` +
720
+ `by Orbit's poller from Galaxy job state, not by you. A \`- [x]\` means the ` +
721
+ `result was verified, so it cannot precede the run succeeding.\n` +
722
+ `If the run is still going, leave the step pending. If it failed, mark it ` +
723
+ `\`- [!]\` and record what failed. If Galaxy has actually finished and the ` +
724
+ `block is stale, call \`galaxy_invocation_check_all\` to refresh it, inspect ` +
725
+ `the outputs, record that evidence, and then flip the checkbox.\n` +
726
+ `Do not retry this write unchanged -- it will be refused again. If the run ` +
727
+ `was abandoned and the work was done another way, that block is the thing ` +
728
+ `to reconcile first: it still says a run for this step is in flight. If you ` +
729
+ `believe the gate is wrong, say so and ask the user to run ` +
730
+ `\`/override ${c.step.anchor ?? c.step.key} <reason>\`; only they can clear it, ` +
731
+ `and the reason is recorded.`
732
+ );
733
+ }
734
+
735
+ function registryReason(c: RegistryContradiction): string {
736
+ const address = c.step.anchor ?? c.step.key;
737
+ const record =
738
+ `The approval registry holds a recorded run for this step that is not eligible to ` +
739
+ `hand off (${c.hold.missing.join("; ")}). Orbit writes that record from Galaxy's own ` +
740
+ `answers; nothing in the notebook changes it.`;
741
+ const head =
742
+ c.kind === "flip"
743
+ ? `Marking "${c.step.text}" complete contradicts the registry. ${record}`
744
+ : `This edit removes the step "${c.step.text}" ({#${address}}) from the notebook while ` +
745
+ `its recorded run isn't eligible. ${record} A step with a recorded run can't be ` +
746
+ `renamed or removed until it is; if the step failed or was abandoned, mark it ` +
747
+ `\`- [!]\` and say why instead.`;
748
+ return (
749
+ `${head}\n` +
750
+ `If the run is still going, leave the step pending. If it failed, mark it \`- [!]\` and ` +
751
+ `record what failed. If Galaxy has finished, the next poll or \`/reconcile\` re-checks ` +
752
+ `it against Galaxy; inspect the outputs and record that evidence meanwhile.\n` +
753
+ `Do not retry this write unchanged -- it will be refused again. If you believe the ` +
754
+ `record is wrong, say so and ask the user to run \`/override ${address} <reason>\`; only ` +
755
+ `they can clear it, and it is recorded in the registry.`
756
+ );
757
+ }
758
+
759
+ /** The reason shown to the agent for the contradictions actually blocking it. */
760
+ export function renderBlockReason(contradictions: Contradiction[]): string {
761
+ return contradictions.map(contradictionReason).join("\n\n");
762
+ }
763
+
764
+ export interface GateDecision {
765
+ gated: boolean;
766
+ mode: EvidenceGateMode;
767
+ completions: PlanStep[];
768
+ contradictions: Contradiction[];
769
+ reason?: string;
770
+ }
771
+
772
+ /** Pure decision for one notebook write. Exported for tests. */
773
+ export function decideNotebookWrite(
774
+ before: string,
775
+ toolName: string,
776
+ input: Record<string, unknown>,
777
+ mode: EvidenceGateMode,
778
+ registry: Registry | null = null,
779
+ ): GateDecision {
780
+ const none: GateDecision = { gated: false, mode, completions: [], contradictions: [] };
781
+ if (mode === "off") return none;
782
+ const after = computeAfterContent(before, toolName, input);
783
+ if (after === null) return none;
784
+ return decideTransition(before, after, mode, registry);
785
+ }
786
+
787
+ /**
788
+ * The decision for a notebook going from `before` to `after`, whatever wrote
789
+ * it: a file tool, or a Page pull replacing the body.
790
+ */
791
+ export function decideTransition(
792
+ before: string,
793
+ after: string,
794
+ mode: EvidenceGateMode,
795
+ registry: Registry | null = null,
796
+ opts: { newStepsCount?: boolean } = {},
797
+ ): GateDecision {
798
+ const none: GateDecision = { gated: false, mode, completions: [], contradictions: [] };
799
+ if (mode === "off") return none;
800
+ const { bound } = registryHolds(registry);
801
+ const fromRegistry = findRegistryContradictions(before, after, registry, opts);
802
+ // A step the registry has an opinion on is the registry's to judge; the
803
+ // block text beside it is not consulted.
804
+ const flips = detectCompletions(before, after);
805
+ const completions = [...flips];
806
+ for (const c of fromRegistry) {
807
+ if (!completions.some((s) => s.key === c.step.key)) completions.push(c.step);
808
+ }
809
+ if (completions.length === 0) return none;
810
+ // Pre-image, deliberately: see findContradictions.
811
+ const contradictions: Contradiction[] = [
812
+ ...fromRegistry,
813
+ ...findContradictions(
814
+ before,
815
+ flips.filter((s) => !s.anchor || !bound.has(s.anchor)),
816
+ ),
817
+ ];
818
+ if (contradictions.length === 0) return { ...none, completions };
819
+ return {
820
+ gated: mode === "deny",
821
+ mode,
822
+ completions,
823
+ contradictions,
824
+ reason: renderBlockReason(contradictions),
825
+ };
826
+ }
827
+
828
+ export interface EnrichmentWarning {
829
+ step: string;
830
+ blockKind: "invocation" | "job";
831
+ id: string;
832
+ attemptId?: string;
833
+ reasons: string[];
834
+ }
835
+
836
+ /**
837
+ * Flips whose evidence run is not fully on the record yet: its enrichment is
838
+ * not `complete`, a job has no tool version, or the block says complete and
839
+ * the provenance file behind it is missing.
840
+ *
841
+ * Warn only, in every mode. The rate is the thing to learn before this can
842
+ * deny -- a built-in tool never has a version to give, a details fetch fails
843
+ * for reasons nobody can fix from a notebook -- and a gate that blocks a
844
+ * verified result over a metadata gap is the gate people switch off.
845
+ *
846
+ * Read from the pre-image, as `findContradictions` is. The provenance check is
847
+ * the one part the agent cannot write its way past: `enrichment: complete` is
848
+ * text in a block it can edit, the attempt file is not.
849
+ *
850
+ * Only completed runs are judged when the step has one: an old failed attempt
851
+ * whose details never came is not the evidence the flip rests on.
852
+ */
853
+ export function findEnrichmentWarnings(
854
+ before: string,
855
+ flips: PlanStep[],
856
+ analysisDir: string,
857
+ ): EnrichmentWarning[] {
858
+ const blocks: ({ kind: "invocation"; b: InvocationYaml } | { kind: "job"; b: JobYaml })[] = [
859
+ ...findInvocationBlocks(before).map((b) => ({ kind: "invocation" as const, b })),
860
+ ...findJobBlocks(before).map((b) => ({ kind: "job" as const, b })),
861
+ ];
862
+ const out: EnrichmentWarning[] = [];
863
+ for (const step of flips) {
864
+ if (!step.anchor) continue;
865
+ const bound = blocks.filter((x) => x.b.notebookAnchor === step.anchor);
866
+ if (bound.length === 0) continue;
867
+ const completed = bound.filter((x) => x.b.status === "completed");
868
+ for (const x of completed.length > 0 ? completed : bound) {
869
+ const id = x.kind === "invocation" ? x.b.invocationId : x.b.jobId;
870
+ const reasons: string[] = [];
871
+ if (x.b.enrichment !== "complete") reasons.push(`enrichment_${x.b.enrichment ?? "absent"}`);
872
+ // The block's word is notebook text; the protected record is what the
873
+ // check rests on whenever the block claims to be complete.
874
+ const record = x.b.attemptId ? readAttemptRecordSync(analysisDir, x.b.attemptId) : null;
875
+ const owned = record && attemptOwns(record, x.kind, id) ? record : null;
876
+ const recordJobs = owned
877
+ ? x.kind === "job"
878
+ ? [owned.jobs[id]].filter(Boolean)
879
+ : Object.values(owned.jobs)
880
+ : [];
881
+ if (x.b.enrichment === "complete") {
882
+ if (!owned) reasons.push("provenance_missing");
883
+ else {
884
+ // Made from notebook text, or from fixture answers: neither is a
885
+ // server's word about this run.
886
+ if (owned.origin === "notebook" || owned.fixture) reasons.push("provenance_untrusted");
887
+ const incomplete =
888
+ recordJobs.length === 0 ||
889
+ recordJobs.some((j) => j.unavailable) ||
890
+ (x.kind === "invocation" && owned.enrichment.state !== "complete");
891
+ if (incomplete) reasons.push("provenance_incomplete");
892
+ }
893
+ }
894
+ const versionMissing = owned
895
+ ? recordJobs.length === 0 || recordJobs.some((j) => j.tool_version === "unknown")
896
+ : x.kind === "job"
897
+ ? !(x.b.jobs ?? []).some((j) => j.jobId === id && j.toolVersion)
898
+ : !x.b.jobs || x.b.jobs.length === 0 || x.b.jobs.some((j) => !j.toolVersion);
899
+ if (versionMissing) reasons.push("tool_version_missing");
900
+ if (reasons.length > 0) {
901
+ out.push({
902
+ step: step.key,
903
+ blockKind: x.kind,
904
+ id,
905
+ ...(x.b.attemptId ? { attemptId: x.b.attemptId } : {}),
906
+ reasons,
907
+ });
908
+ }
909
+ }
910
+ }
911
+ return out;
912
+ }
913
+
914
+ /**
915
+ * Session-scoped, user-granted clearances, keyed by plan-step key.
916
+ *
917
+ * Deliberately not a "the model asked twice" escape. The gate used to keep a
918
+ * `denied` set and let the second attempt through on the theory that an
919
+ * unwinnable retry loop is worse than an unevidenced claim; that made the
920
+ * decision advisory, because a model that disagrees only has to ask again. A
921
+ * repeated model request is not an exception. An exception is a person saying
922
+ * so, with a reason, on one named step -- which is what `/override` grants and
923
+ * what lands in `activity.jsonl` as `evidence.override`.
924
+ *
925
+ * One token per step *and invocation*, consumed by the first write it actually
926
+ * clears. Keying on the step alone let a clearance outlive the thing it was
927
+ * granted for: override while run one is in flight, watch the poller fail it,
928
+ * submit run two for the same anchor, and the stale token cleared a
929
+ * contradiction the user never saw. The user approved a specific run being
930
+ * ahead of its checkbox, not the step forever. A contradiction that recurs on
931
+ * the step, from a different invocation or after the step is reopened, is
932
+ * denied again.
933
+ *
934
+ * Module-level, and cleared on `session_start`, which is the same one-session-
935
+ * per-process assumption `state.ts` already makes -- the notebook path itself
936
+ * is a module-level singleton reset in that same hook, so two concurrent
937
+ * sessions in one brain process is not a shape that exists. It does mean a
938
+ * clearance does not survive a restart: granting one in the CLI and resuming
939
+ * in Orbit means granting it again. Deliberate while the token is one-shot; a
940
+ * clearance that outlives the process is a durable record, and durable records
941
+ * are the registry's job.
942
+ */
943
+ const overrides = new Set<string>();
944
+
945
+ /** The clearance is for this run of this step, not for the step in general. */
946
+ export function overrideToken(stepKey: string, invocationId: string): string {
947
+ return `${stepKey}\u0000${invocationId}`;
948
+ }
949
+
950
+ /** Grant one clearance for `stepKey` while `invocationId` is the run in flight. */
951
+ export function grantEvidenceOverride(stepKey: string, invocationId: string): void {
952
+ overrides.add(overrideToken(stepKey, invocationId));
953
+ }
954
+
955
+ /** The standing clearances, for a write path the tool_call hook doesn't see (a Page pull). */
956
+ export function evidenceOverrides(): ReadonlySet<string> {
957
+ return overrides;
958
+ }
959
+
960
+ /** Spend the tokens a write actually used -- only when it went through. */
961
+ export function spendClearances(adjudication: GateAdjudication): void {
962
+ if (adjudication.block) return;
963
+ for (const c of adjudication.cleared) {
964
+ if (c.source === "block")
965
+ overrides.delete(overrideToken(c.step.key, c.invocation.invocationId));
966
+ }
967
+ }
968
+
969
+ /** Session boundary / test reset. */
970
+ export function resetEvidenceOverrides(): void {
971
+ overrides.clear();
972
+ }
973
+
974
+ export type GateOutcome = "recorded" | "warned" | "overridden" | "blocked";
975
+
976
+ export interface GateAdjudication {
977
+ decision: GateDecision;
978
+ /** Contradictions a standing user override would clear on this write. */
979
+ cleared: Contradiction[];
980
+ /** Contradictions with no override behind them. */
981
+ unresolved: Contradiction[];
982
+ block: boolean;
983
+ outcome: GateOutcome;
984
+ }
985
+
986
+ export interface EvidenceDecisionInfo {
987
+ outcome: GateOutcome;
988
+ toolName: string;
989
+ /** Plan-step keys the decision was about. */
990
+ steps: string[];
991
+ mode: EvidenceGateMode;
992
+ }
993
+
994
+ export type EvidenceDecisionListener = (info: EvidenceDecisionInfo) => void;
995
+
996
+ // The gate already records every decision to activity.jsonl; this is the
997
+ // in-process twin of that row, so another subsystem can react to a block
998
+ // without re-deriving an adjudication that depends on this module's private
999
+ // override set. Listeners are registered once at startup and are not cleared
1000
+ // at a session boundary -- the override set is session state, a subscription
1001
+ // is not.
1002
+ const decisionListeners = new Set<EvidenceDecisionListener>();
1003
+
1004
+ export function onEvidenceDecision(listener: EvidenceDecisionListener): () => void {
1005
+ decisionListeners.add(listener);
1006
+ return () => {
1007
+ decisionListeners.delete(listener);
1008
+ };
1009
+ }
1010
+
1011
+ /** Exported for tests; the hook below is the only production caller. */
1012
+ export function notifyEvidenceDecision(info: EvidenceDecisionInfo): void {
1013
+ for (const listener of decisionListeners) {
1014
+ try {
1015
+ listener(info);
1016
+ } catch (err) {
1017
+ // Isolate subscribers: a throwing listener must never turn the gate's
1018
+ // decision into an exception out of the tool_call hook.
1019
+ console.error("evidence decision listener failed:", err);
1020
+ }
1021
+ }
1022
+ }
1023
+
1024
+ /**
1025
+ * The full decision for one write, overrides included. Split out from the hook
1026
+ * so the interesting half is testable without a session: the hook's only job
1027
+ * on top of this is to consume the tokens, log, and return the block.
1028
+ *
1029
+ * Tokens are only spent when the write actually proceeds. A write carrying
1030
+ * contradictions on two steps where only one is overridden is still blocked,
1031
+ * and burning the granted token there would make the user grant it twice for
1032
+ * one flip. In `warn` the decision was never going to block, so nothing is
1033
+ * spent and nothing about the recorded outcome changes.
1034
+ */
1035
+ export function adjudicateNotebookWrite(
1036
+ before: string,
1037
+ toolName: string,
1038
+ input: Record<string, unknown>,
1039
+ mode: EvidenceGateMode,
1040
+ granted: ReadonlySet<string>,
1041
+ registry: Registry | null = null,
1042
+ ): GateAdjudication {
1043
+ return adjudicate(decideNotebookWrite(before, toolName, input, mode, registry), granted);
1044
+ }
1045
+
1046
+ /**
1047
+ * Apply the user's standing clearances to a decision. A block contradiction is
1048
+ * cleared by its one-shot token; a registry contradiction only by the
1049
+ * evidence-gate exception `/override` writes into the registry, which the
1050
+ * decision has already taken into account.
1051
+ */
1052
+ export function adjudicate(decision: GateDecision, granted: ReadonlySet<string>): GateAdjudication {
1053
+ const cleared = decision.gated
1054
+ ? decision.contradictions.filter(
1055
+ (c) =>
1056
+ c.source === "block" && granted.has(overrideToken(c.step.key, c.invocation.invocationId)),
1057
+ )
1058
+ : [];
1059
+ const unresolved = decision.contradictions.filter((c) => !cleared.includes(c));
1060
+ const block = decision.gated && unresolved.length > 0;
1061
+ const outcome: GateOutcome = block
1062
+ ? "blocked"
1063
+ : cleared.length > 0
1064
+ ? "overridden"
1065
+ : decision.contradictions.length > 0
1066
+ ? "warned"
1067
+ : "recorded";
1068
+ return { decision, cleared, unresolved, block, outcome };
1069
+ }
1070
+
1071
+ function sameFile(a: string, b: string): boolean {
1072
+ try {
1073
+ return fs.realpathSync(a) === fs.realpathSync(b);
1074
+ } catch {
1075
+ return path.resolve(a) === path.resolve(b);
1076
+ }
1077
+ }
1078
+
1079
+ /** Unicode spaces pi folds to a plain space before resolving (utils/paths.ts). */
1080
+ const UNICODE_SPACES = /[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g;
1081
+
1082
+ /**
1083
+ * Does this tool argument name the notebook, once pi is done with it?
1084
+ *
1085
+ * `path.resolve` is not what pi does. `resolveToCwd` runs the argument through
1086
+ * `normalizePath` with `stripAtPrefix` and `normalizeUnicodeSpaces` on and
1087
+ * tilde expansion defaulted on, so `@notebook.md`, `~/work/x/notebook.md` and
1088
+ * a path carrying a non-breaking space all land on the real file while a
1089
+ * literal `path.resolve` sends them somewhere else entirely -- and the gate
1090
+ * that resolved them literally would abstain on a write pi was about to make.
1091
+ *
1092
+ * Every plausible spelling is tested rather than one canonical form, because
1093
+ * the question here is only "is this call about the notebook". Over-including
1094
+ * costs an adjudication of a write that was never going to touch it; under-
1095
+ * including is a silent bypass.
1096
+ */
1097
+ function resolvesToNotebook(raw: string, cwd: string, nbPath: string): boolean {
1098
+ const candidates = new Set<string>();
1099
+ const add = (value: string) => {
1100
+ if (!value) return;
1101
+ candidates.add(value);
1102
+ if (value.startsWith("@")) candidates.add(value.slice(1));
1103
+ };
1104
+ add(raw);
1105
+ add(raw.replace(UNICODE_SPACES, " "));
1106
+ for (const candidate of [...candidates]) {
1107
+ if (candidate === "~") candidates.add(os.homedir());
1108
+ else if (candidate.startsWith("~/"))
1109
+ candidates.add(path.join(os.homedir(), candidate.slice(2)));
1110
+ }
1111
+ for (const candidate of candidates) {
1112
+ const abs = path.isAbsolute(candidate) ? candidate : path.resolve(cwd, candidate);
1113
+ if (sameFile(abs, nbPath)) return true;
1114
+ }
1115
+ return false;
1116
+ }
1117
+
1118
+ /** One `evidence.decision` row: what was claimed, what contradicted it, and the outcome. */
1119
+ export function recordDecision(
1120
+ analysisDir: string,
1121
+ toolName: string,
1122
+ adjudication: GateAdjudication,
1123
+ ): void {
1124
+ const { decision } = adjudication;
1125
+ appendActivityEvent(analysisDir, {
1126
+ timestamp: new Date().toISOString(),
1127
+ kind: "evidence.decision",
1128
+ source: "evidence-gate",
1129
+ payload: {
1130
+ mode: decision.mode,
1131
+ toolName,
1132
+ completions: decision.completions.map((s) => s.key),
1133
+ contradictions: decision.contradictions.map((c) =>
1134
+ c.source === "block"
1135
+ ? {
1136
+ source: "block",
1137
+ step: c.step.key,
1138
+ status: c.invocation.status,
1139
+ // The id is what makes a row adjudicable: warn mode records the
1140
+ // would-block decision, and deciding later whether it was a false
1141
+ // positive means going and looking at this invocation in Galaxy.
1142
+ invocationId: c.invocation.invocationId,
1143
+ }
1144
+ : {
1145
+ source: "registry",
1146
+ step: c.step.key,
1147
+ kind: c.kind,
1148
+ attempts: c.hold.attemptIds,
1149
+ missing: c.hold.missing,
1150
+ },
1151
+ ),
1152
+ overridden: adjudication.block ? [] : adjudication.cleared.map((c) => c.step.key),
1153
+ outcome: adjudication.outcome,
1154
+ },
1155
+ });
1156
+ }
1157
+
1158
+ export function registerEvidenceGate(pi: ExtensionAPI, family: GalaxyToolFamily): void {
1159
+ // A clearance the user granted and the gate never spent must not outlive the
1160
+ // session it was granted in. Registered here rather than in
1161
+ // session-lifecycle.ts so the gate owns the whole lifetime of its own state.
1162
+ pi.on("session_start", async () => {
1163
+ resetEvidenceOverrides();
1164
+ });
1165
+
1166
+ pi.on("tool_call", async (event, ctx) => {
1167
+ const mode = resolveMode();
1168
+ if (mode === "off") return;
1169
+ // Under a page record the model completes steps by editing the page, not a file.
1170
+ const pageWrite = family.toolName(event.toolName) === "update_page";
1171
+ if (!WRITE_TOOLS.has(event.toolName) && !pageWrite) return;
1172
+
1173
+ const nbPath = getNotebookPath();
1174
+ if (!nbPath) return;
1175
+ const input = event.input as Record<string, unknown>;
1176
+ let adjudicateAs = event.toolName;
1177
+ if (pageWrite) {
1178
+ const store = recordStore(nbPath);
1179
+ if (store.kind !== "page" || store.pageId === undefined || store.pageId !== input.page_id) {
1180
+ return;
1181
+ }
1182
+ adjudicateAs = "update_page";
1183
+ } else {
1184
+ // `file_path` is read as well as `path`, defensively. In the pinned pi it
1185
+ // is not actually an execution alias: `editSchema`/`writeSchema` require
1186
+ // `path`, `validateToolArguments` throws when it is missing, and both
1187
+ // `execute` bodies destructure `path`, so a call naming only `file_path`
1188
+ // never reaches the filesystem. Only pi's *render* helpers read the alias
1189
+ // (`getRenderablePreviewInput`, `formatEditCall`). Kept because it costs
1190
+ // one ternary and covers pi adding the alias, or another shell forwarding
1191
+ // a tool call in that shape. (exec-guard/policy.ts reads only `path` too;
1192
+ // tracked separately as P0.3.)
1193
+ // Adjudicate if ANY spelling of the target names the notebook, rather than
1194
+ // picking one and trusting the precedence to match pi's. Precedence is a
1195
+ // thing to get wrong later; "does this call mention the notebook at all"
1196
+ // is not. A call naming some other file under the other key is adversarial
1197
+ // or malformed, and pi refuses it, so there is no honest write to misjudge.
1198
+ const targets = [input.path, input.file_path].filter(
1199
+ (t): t is string => typeof t === "string" && t.length > 0,
1200
+ );
1201
+ const touchesNotebook = targets.some((t) => resolvesToNotebook(t, ctx.cwd, nbPath));
1202
+ if (!touchesNotebook) return;
1203
+ }
1204
+
1205
+ const registry = currentRegistryView();
1206
+ let before: string;
1207
+ try {
1208
+ before = (await recordStore(nbPath).read()).content;
1209
+ } catch (error) {
1210
+ // A record that can't be reached right now (a page GET that timed out or
1211
+ // got a 5xx) says nothing about which steps it holds, and judging the
1212
+ // write against "" would let it complete any of them unseen.
1213
+ if (!isRecordMissing(error)) {
1214
+ if (mode !== "deny") return;
1215
+ return {
1216
+ block: true,
1217
+ reason:
1218
+ `Could not read the notebook (${recordStore(nbPath).label}) to check this write ` +
1219
+ `against its steps: ${error instanceof Error ? error.message : String(error)}. ` +
1220
+ `Nothing was written. Try the same call again in a moment.`,
1221
+ };
1222
+ }
1223
+ // No notebook yet. Nothing to compare against -- unless the registry
1224
+ // holds a step, in which case a notebook written from scratch with that
1225
+ // step complete is still a completion.
1226
+ if (registryHolds(registry).held.size === 0) return;
1227
+ before = "";
1228
+ }
1229
+
1230
+ const adjudication = adjudicateNotebookWrite(
1231
+ before,
1232
+ adjudicateAs,
1233
+ input,
1234
+ mode,
1235
+ overrides,
1236
+ registry,
1237
+ );
1238
+ const { decision } = adjudication;
1239
+ if (decision.completions.length === 0) return;
1240
+
1241
+ spendClearances(adjudication);
1242
+
1243
+ recordDecision(path.dirname(nbPath), event.toolName, adjudication);
1244
+
1245
+ const warnings = findEnrichmentWarnings(before, decision.completions, path.dirname(nbPath));
1246
+ if (warnings.length > 0) {
1247
+ appendActivityEvent(path.dirname(nbPath), {
1248
+ timestamp: new Date().toISOString(),
1249
+ kind: "evidence.enrichment_warning",
1250
+ source: "evidence-gate",
1251
+ payload: {
1252
+ mode: decision.mode,
1253
+ toolName: event.toolName,
1254
+ decision: "warn",
1255
+ warnings: warnings.map((w) => ({
1256
+ step: w.step,
1257
+ block_kind: w.blockKind,
1258
+ id: w.id,
1259
+ attempt_id: w.attemptId ?? null,
1260
+ reasons: w.reasons,
1261
+ })),
1262
+ },
1263
+ });
1264
+ }
1265
+
1266
+ notifyEvidenceDecision({
1267
+ outcome: adjudication.outcome,
1268
+ toolName: event.toolName,
1269
+ steps: decision.completions.map((s) => s.key),
1270
+ mode: decision.mode,
1271
+ });
1272
+
1273
+ if (!adjudication.block) return;
1274
+ return { block: true, reason: renderBlockReason(adjudication.unresolved) };
1275
+ });
1276
+ }