@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,1300 @@
1
+ /**
2
+ * Orbit-extension-specific tool registrations.
3
+ *
4
+ * Plans, steps, and decisions live as markdown sections inside the project
5
+ * notebook (`notebook.md`) — the agent maintains them via the generic
6
+ * Edit/Write tools. The only tools registered here are:
7
+ * - GTN tutorial discovery / fetch
8
+ * - Galaxy invocation tracking (record + poll status from the notebook)
9
+ * - Galaxy skills fetch (operational know-how from galaxyproject/galaxy-skills)
10
+ */
11
+
12
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
+ import { Text } from "@earendil-works/pi-tui";
14
+ import { Type } from "@sinclair/typebox";
15
+ import { getNotebookPath } from "./state";
16
+ import { defaultDeps as proposalDeps, noteProposalBinding } from "./proposal-commands";
17
+ import {
18
+ readNotebook,
19
+ writeNotebook,
20
+ withNotebookLock,
21
+ withNotebookCas,
22
+ findInvocationBlocks,
23
+ locateInvocationBlock,
24
+ upsertInvocationBlock,
25
+ applyInvocationUpdates,
26
+ statNotebook,
27
+ NotebookChangedError,
28
+ type InvocationYaml,
29
+ type InvocationPollUpdate,
30
+ } from "./notebook-writer";
31
+ import {
32
+ isTerminalJobState,
33
+ locateJobBlock,
34
+ upsertJobBlock,
35
+ type JobYaml,
36
+ } from "./galaxy-job-block";
37
+ import {
38
+ ambiguousAnchorMessage,
39
+ listNotebookAnchors,
40
+ resolveNotebookAnchor,
41
+ unknownAnchorMessage,
42
+ UnknownAnchorError,
43
+ } from "./notebook-anchors";
44
+ import {
45
+ INVOCATION_SCHEDULED_OK,
46
+ INVOCATION_SCHEDULING_DONE,
47
+ getGalaxyConfig,
48
+ galaxyGetInvocation,
49
+ sameGalaxyServer,
50
+ verifyGalaxyRun,
51
+ type InvocationDetail,
52
+ } from "./galaxy-api";
53
+ import { listEnabledSkillRepos, findSkillRepo } from "./skills";
54
+ import { fetchSkillFile, githubRawBase } from "./skills-discovery";
55
+ import {
56
+ STAGING_REPO_NAME,
57
+ VENDOR_REPO_NAME,
58
+ hasBundledContent,
59
+ isBundledRepo,
60
+ isReservedRepo,
61
+ readBundledRepoFile,
62
+ } from "./vendor-skills";
63
+ import { GTN_API, gtnPageText, gtnTopics, gtnTutorials, gtnUrl } from "../../shared/gtn.js";
64
+ import type { GalaxyToolFamily } from "../../shared/galaxy-tool-family.js";
65
+
66
+ /**
67
+ * A record call's label has to say something.
68
+ *
69
+ * Not politeness: `parseInvocationBlock` requires a label, so a block written
70
+ * with an empty one stops parsing, and then the next call to the same tool
71
+ * cannot find it and treats the run as unrecorded. Refusing here is what keeps
72
+ * the block readable by the same parser that has to find it again.
73
+ */
74
+ function requireLabel(label: string): string {
75
+ const trimmed = label.trim();
76
+ if (!trimmed) {
77
+ throw new Error(
78
+ "label cannot be empty -- it is what the Activity panel and the notebook " +
79
+ "show for this run. Give it something like 'BWA alignment'.",
80
+ );
81
+ }
82
+ return trimmed;
83
+ }
84
+
85
+ /**
86
+ * Reduce a GTN page to the agent's text. The reduction is shared with the browser shell
87
+ * (shared/gtn.js): it drops chrome, prefers the most specific content region, and degrades
88
+ * on malformed HTML rather than leaking an unterminated script body.
89
+ */
90
+ function stripGtnHtml(html: string): string {
91
+ return gtnPageText(html);
92
+ }
93
+
94
+ /**
95
+ * Resolve a caller-supplied anchor against the notebook, or refuse.
96
+ *
97
+ * The record tools wrote whatever they were handed, so an anchor the notebook
98
+ * has never heard of produced a block bound to nothing: the run still shows up
99
+ * and still polls, but every consumer that looks a step up by anchor -- the
100
+ * evidence gate first among them -- reads "no opinion" instead of "broken".
101
+ * Rejecting costs the model one retry and hands it the list to retry with.
102
+ */
103
+ function requireAnchor(content: string, input: string): string {
104
+ const resolved = resolveNotebookAnchor(content, input);
105
+ if (resolved.kind === "resolved") return resolved.anchor;
106
+ throw new UnknownAnchorError(
107
+ resolved.kind === "ambiguous"
108
+ ? ambiguousAnchorMessage(input, resolved.candidates)
109
+ : unknownAnchorMessage(input, listNotebookAnchors(content)),
110
+ );
111
+ }
112
+
113
+ /** The refusal both record tools hand back: nothing written, reason named. */
114
+ function recordFailure(message: string): {
115
+ content: { type: "text"; text: string }[];
116
+ details: Record<string, unknown>;
117
+ } {
118
+ return {
119
+ content: [{ type: "text" as const, text: JSON.stringify({ success: false, error: message }) }],
120
+ details: { error: true } as Record<string, unknown>,
121
+ };
122
+ }
123
+
124
+ export function registerPlanTools(pi: ExtensionAPI, family: GalaxyToolFamily): void {
125
+ // ─────────────────────────────────────────────────────────────────────────────
126
+ // Tool: Search/browse GTN topics and tutorials
127
+ // ─────────────────────────────────────────────────────────────────────────────
128
+ pi.registerTool({
129
+ name: "gtn_search",
130
+ label: "Search GTN Tutorials",
131
+ description: `Browse GTN topics and discover tutorials. Call with no arguments to list all
132
+ topics. Provide a topic ID to list its tutorials. Use query to filter tutorials by keyword
133
+ in their title or objectives. Use this to find tutorial URLs before fetching with gtn_fetch.`,
134
+ parameters: Type.Object({
135
+ topic: Type.Optional(
136
+ Type.String({
137
+ description: "Topic ID to list tutorials for (e.g., 'transcriptomics', 'introduction')",
138
+ }),
139
+ ),
140
+ query: Type.Optional(
141
+ Type.String({
142
+ description: "Keyword to filter tutorials by title or objectives (case-insensitive)",
143
+ }),
144
+ ),
145
+ }),
146
+ async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
147
+ try {
148
+ if (!params.topic) {
149
+ const resp = await fetch(`${GTN_API}/topics.json`, { signal });
150
+ if (!resp.ok) {
151
+ return {
152
+ content: [{ type: "text", text: `Error: GTN API returned HTTP ${resp.status}` }],
153
+ details: { error: true },
154
+ };
155
+ }
156
+
157
+ const listing = gtnTopics(await resp.json());
158
+ if (!listing) {
159
+ return {
160
+ content: [{ type: "text", text: "Error: GTN returned an unexpected topics payload" }],
161
+ details: { error: true },
162
+ };
163
+ }
164
+ return {
165
+ content: [{ type: "text", text: JSON.stringify(listing, null, 2) }],
166
+ details: { count: listing.count },
167
+ };
168
+ }
169
+
170
+ const resp = await fetch(`${GTN_API}/topics/${params.topic}.json`, { signal });
171
+ if (!resp.ok) {
172
+ return {
173
+ content: [
174
+ {
175
+ type: "text",
176
+ text: `Error: Topic "${params.topic}" not found (HTTP ${resp.status}). Use gtn_search with no arguments to list available topics.`,
177
+ },
178
+ ],
179
+ details: { error: true },
180
+ };
181
+ }
182
+
183
+ const listing = gtnTutorials(await resp.json(), params.query);
184
+ if (!listing) {
185
+ return {
186
+ content: [
187
+ {
188
+ type: "text",
189
+ text: `Error: Topic "${params.topic}" returned an unexpected payload.`,
190
+ },
191
+ ],
192
+ details: { error: true },
193
+ };
194
+ }
195
+ return {
196
+ content: [{ type: "text", text: JSON.stringify(listing, null, 2) }],
197
+ details: { topic: params.topic, count: listing.count },
198
+ };
199
+ } catch (error) {
200
+ const msg = error instanceof Error ? error.message : String(error);
201
+ return {
202
+ content: [{ type: "text", text: `Error searching GTN: ${msg}` }],
203
+ details: { error: true },
204
+ };
205
+ }
206
+ },
207
+ renderResult: (result) => {
208
+ const d = result.details as { count?: number; topic?: string; error?: boolean } | undefined;
209
+ if (d?.error) {
210
+ return new Text("❌ GTN search failed");
211
+ }
212
+ if (d?.topic) {
213
+ return new Text(`📚 Found ${d.count || 0} tutorials in "${d.topic}"`);
214
+ }
215
+ return new Text(`📚 Found ${d?.count || 0} GTN topics`);
216
+ },
217
+ });
218
+
219
+ // ─────────────────────────────────────────────────────────────────────────────
220
+ // Tool: Fetch GTN tutorial content
221
+ // ─────────────────────────────────────────────────────────────────────────────
222
+ pi.registerTool({
223
+ name: "gtn_fetch",
224
+ label: "Fetch GTN Tutorial",
225
+ description: `Fetch a Galaxy Training Network (GTN) tutorial page and return its content as
226
+ readable text. Only URLs on training.galaxyproject.org are allowed. Use gtn_search first to
227
+ discover valid tutorial URLs — do not guess or construct URLs. Use this to read tutorial
228
+ instructions, tool names, parameters, and workflow steps so you can follow along and reproduce
229
+ analyses in Galaxy.`,
230
+ parameters: Type.Object({
231
+ url: Type.String({
232
+ description: "URL of the GTN tutorial page (must be on training.galaxyproject.org)",
233
+ }),
234
+ }),
235
+ async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
236
+ const checked = gtnUrl(params.url);
237
+ if (checked.problem !== undefined) {
238
+ return {
239
+ content: [{ type: "text", text: `Error: ${checked.problem}` }],
240
+ details: { error: true },
241
+ };
242
+ }
243
+
244
+ try {
245
+ const response = await fetch(params.url, { signal });
246
+
247
+ if (!response.ok) {
248
+ return {
249
+ content: [
250
+ {
251
+ type: "text",
252
+ text: `Error: Failed to fetch tutorial (HTTP ${response.status})`,
253
+ },
254
+ ],
255
+ details: { error: true },
256
+ };
257
+ }
258
+
259
+ const html = await response.text();
260
+ const text = stripGtnHtml(html);
261
+
262
+ return {
263
+ content: [
264
+ {
265
+ type: "text",
266
+ text,
267
+ },
268
+ ],
269
+ details: { url: params.url, length: text.length },
270
+ };
271
+ } catch (error) {
272
+ const msg = error instanceof Error ? error.message : String(error);
273
+ return {
274
+ content: [{ type: "text", text: `Error fetching tutorial: ${msg}` }],
275
+ details: { error: true },
276
+ };
277
+ }
278
+ },
279
+ renderResult: (result) => {
280
+ const d = result.details as { url?: string; length?: number; error?: boolean } | undefined;
281
+ if (d?.error) {
282
+ return new Text("❌ GTN fetch failed");
283
+ }
284
+ return new Text(`📖 Fetched GTN tutorial (${d?.length || 0} chars)`);
285
+ },
286
+ });
287
+
288
+ // ─────────────────────────────────────────────────────────────────────────────
289
+ // Tool: Fetch a SKILL.md or reference doc from a configured skills repo
290
+ // ─────────────────────────────────────────────────────────────────────────────
291
+ pi.registerTool({
292
+ name: "skills_fetch",
293
+ label: "Fetch Skill",
294
+ description: `Load operational know-how from a skills repo. The system
295
+ prompt's "Skills repositories" section lists the repos and the canonical paths
296
+ inside each; a repo marked bundled there is read from the package, and any other
297
+ is fetched and cached locally for 24h. If \`repo\` is omitted, the first enabled
298
+ repo is used (typically \`galaxy-skills\`).
299
+
300
+ \`repo: "${STAGING_REPO_NAME}"\` reads Orbit's staging skills (ones that have not
301
+ moved to a permanent repo yet), listed in the router under that name. \`repo: "${VENDOR_REPO_NAME}"\` reads Galaxy workflow
302
+ reference material bundled with Orbit. Both are always offline. The
303
+ ${VENDOR_REPO_NAME} material is deliberately absent from the router, so its
304
+ paths are not listed there: a hint names the exact file when it becomes
305
+ relevant, and a wrong path answers with the entry points that do exist.`,
306
+ parameters: Type.Object({
307
+ repo: Type.Optional(
308
+ Type.String({
309
+ description:
310
+ "Name of the skills repo to fetch from (e.g. 'galaxy-skills'), " +
311
+ `'${STAGING_REPO_NAME}' for Orbit's staging skills, or ` +
312
+ `'${VENDOR_REPO_NAME}' for bundled reference material. ` +
313
+ "Omit to use the default (first enabled repo).",
314
+ }),
315
+ ),
316
+ path: Type.String({
317
+ description:
318
+ "Relative path inside the repo. For a repo the router lists, use the " +
319
+ "path it prints, e.g. 'skills/collection-manipulation/SKILL.md'; a " +
320
+ "skill's own reference docs sit beside it, e.g. " +
321
+ `'skills/galaxy-mcp-reference/gotchas.md'. For '${VENDOR_REPO_NAME}', ` +
322
+ "use the path a hint gave you.",
323
+ }),
324
+ }),
325
+ async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
326
+ // Normalised once, up front, so every branch below reasons about the same
327
+ // string. The reserved-name branch used to take the model's raw input.
328
+ const cleanPath = params.path.replace(/^\/+/, "").replace(/\\/g, "/");
329
+ if (cleanPath.includes("..") || cleanPath === "") {
330
+ return {
331
+ content: [{ type: "text", text: `Error: Invalid skill path "${params.path}"` }],
332
+ details: { error: true },
333
+ };
334
+ }
335
+
336
+ // A reserved name reads from the package, but only when the user has not
337
+ // claimed it: configuring a repo called "foundry" or "orbit" has to win, or
338
+ // an explicitly chosen branch is silently answered from the package.
339
+ // Checked before the read, not after a miss, because the bundled tree
340
+ // holds a whole skills mirror and would answer for paths that belong to it.
341
+ const reserved = params.repo;
342
+ if (isReservedRepo(reserved) && !findSkillRepo(reserved)) {
343
+ if (!hasBundledContent(reserved)) {
344
+ return {
345
+ content: [
346
+ {
347
+ type: "text",
348
+ text: `Error: this build of Orbit ships nothing under "${reserved}".`,
349
+ },
350
+ ],
351
+ details: { error: true, repo: reserved, path: cleanPath },
352
+ };
353
+ }
354
+ const res = readBundledRepoFile({ name: reserved }, cleanPath);
355
+ if (res.ok) {
356
+ return {
357
+ content: [{ type: "text", text: res.text }],
358
+ details: {
359
+ repo: reserved,
360
+ path: cleanPath,
361
+ length: res.text.length,
362
+ bundled: true,
363
+ },
364
+ };
365
+ }
366
+ return {
367
+ content: [
368
+ {
369
+ type: "text",
370
+ text: `Error: ${res.error}. Available: ${res.available.join(", ") || "(none)"}.`,
371
+ },
372
+ ],
373
+ details: { error: true, repo: reserved, path: cleanPath },
374
+ };
375
+ }
376
+
377
+ const repo = findSkillRepo(params.repo);
378
+ if (!repo) {
379
+ const enabled =
380
+ listEnabledSkillRepos()
381
+ .map((r) => r.name)
382
+ .join(", ") || "(none)";
383
+ return {
384
+ content: [
385
+ {
386
+ type: "text",
387
+ text: params.repo
388
+ ? `Error: Skills repo "${params.repo}" is not configured or is disabled. Enabled: ${enabled}.`
389
+ : `Error: No skills repos are enabled. Configure one in Preferences → Skills.`,
390
+ },
391
+ ],
392
+ details: { error: true },
393
+ };
394
+ }
395
+
396
+ // A repo still on its shipped URL and branch is served from the package:
397
+ // the content was reviewed at a pinned commit and works with no network.
398
+ // Point it at another branch and it goes back to being fetched, which is
399
+ // how a skill author evaluates a change before it is merged.
400
+ //
401
+ // Gated on the content actually being there. If the vendor tree is missing
402
+ // -- a packaging regression, a half-finished install -- falling through to
403
+ // the network is a worse day than usual, not a session with no skills.
404
+ if (isBundledRepo(repo) && hasBundledContent(repo.name)) {
405
+ const res = readBundledRepoFile(repo, cleanPath);
406
+ if (res.ok) {
407
+ return {
408
+ content: [{ type: "text", text: res.text }],
409
+ details: { repo: repo.name, path: cleanPath, length: res.text.length, bundled: true },
410
+ };
411
+ }
412
+ return {
413
+ content: [
414
+ {
415
+ type: "text",
416
+ text: `Error: ${res.error}. Available: ${res.available.join(", ") || "(none)"}.`,
417
+ },
418
+ ],
419
+ details: { error: true, repo: repo.name, path: cleanPath },
420
+ };
421
+ }
422
+
423
+ const rawBase = githubRawBase(repo.url, repo.branch);
424
+ if (!rawBase) {
425
+ return {
426
+ content: [
427
+ {
428
+ type: "text",
429
+ text: `Error: Repo URL "${repo.url}" must be a GitHub repo (https://github.com/<owner>/<repo>).`,
430
+ },
431
+ ],
432
+ details: { error: true },
433
+ };
434
+ }
435
+
436
+ const res = await fetchSkillFile(repo, cleanPath, signal);
437
+ if (!res.ok) {
438
+ if (res.status) {
439
+ return {
440
+ content: [
441
+ {
442
+ type: "text",
443
+ text:
444
+ `Error: Failed to fetch "${cleanPath}" from ${repo.name} (HTTP ${res.status}). ` +
445
+ `Check the path against the skills router in the system prompt.`,
446
+ },
447
+ ],
448
+ details: { error: true, repo: repo.name, path: cleanPath },
449
+ };
450
+ }
451
+ return {
452
+ content: [{ type: "text", text: `Error fetching skill: ${res.error}` }],
453
+ details: { error: true, repo: repo.name, path: cleanPath },
454
+ };
455
+ }
456
+ return {
457
+ content: [{ type: "text", text: res.text }],
458
+ details: { repo: repo.name, path: cleanPath, length: res.text.length, cached: res.cached },
459
+ };
460
+ },
461
+ renderResult: (result) => {
462
+ const d = result.details as
463
+ | {
464
+ repo?: string;
465
+ path?: string;
466
+ length?: number;
467
+ cached?: boolean;
468
+ bundled?: boolean;
469
+ error?: boolean;
470
+ }
471
+ | undefined;
472
+ if (d?.error) return new Text("❌ Skill fetch failed");
473
+ const tag = d?.bundled ? "(bundled)" : d?.cached ? "(cached)" : "(fetched)";
474
+ return new Text(`📘 ${d?.repo}/${d?.path} ${tag} (${d?.length || 0} chars)`);
475
+ },
476
+ });
477
+
478
+ // ─────────────────────────────────────────────────────────────────────────────
479
+ // Tool: Record a Galaxy invocation in the notebook
480
+ // ─────────────────────────────────────────────────────────────────────────────
481
+ pi.registerTool({
482
+ name: "galaxy_invocation_record",
483
+ label: "Record Galaxy Invocation",
484
+ description: `Name the plan step a Galaxy workflow invocation belongs to. The harness
485
+ records the run; you name the step it belongs to. Orbit already wrote a \`loom-invocation\`
486
+ block the moment ${family.tool("invoke_workflow")} returned, so this normally just sets \`label\` and
487
+ \`notebook_anchor\` on the block already carrying that id -- the run is pollable either way.
488
+ If no block carries the id (a submission Orbit could not see, such as one made from inside
489
+ code mode), this creates one. Both arguments are checked before anything is written: the
490
+ anchor must resolve in notebook.md, and the invocation id must exist on the Galaxy server.`,
491
+ parameters: Type.Object({
492
+ invocationId: Type.String({
493
+ description: `Galaxy invocation ID returned from ${family.tool("invoke_workflow")}`,
494
+ }),
495
+ notebookAnchor: Type.String({
496
+ description:
497
+ "Anchor of the plan step this run belongs to, e.g. 'plan-a-step-3'. It must " +
498
+ "already exist in notebook.md, either as a {#anchor} on the step or as a " +
499
+ "heading; an anchor nothing resolves to is rejected.",
500
+ }),
501
+ label: Type.String({
502
+ description: "Human-readable description for status display, e.g. 'BWA alignment'",
503
+ }),
504
+ proposalId: Type.Optional(
505
+ Type.String({
506
+ description:
507
+ "The approved proposal this run carries out (the id from loom_propose that the " +
508
+ "user approved). Recorded alongside the run; it doesn't approve anything.",
509
+ }),
510
+ ),
511
+ }),
512
+ async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
513
+ const notebookPath = getNotebookPath();
514
+ if (!notebookPath) return recordFailure("No notebook open.");
515
+
516
+ const cfg = getGalaxyConfig();
517
+ const galaxyServerUrl = cfg?.url || "";
518
+
519
+ try {
520
+ // Ask Galaxy before writing anything down. An id Galaxy has never heard
521
+ // of records nothing: the poller 404s on it every tick, the Activity
522
+ // panel shows a run that doesn't exist, and whatever the model really
523
+ // submitted stays untracked.
524
+ const check = await verifyGalaxyRun("invocation", params.invocationId, signal);
525
+ if (check.outcome === "absent") {
526
+ return recordFailure(
527
+ `Galaxy has no invocation "${params.invocationId}" (${check.detail}). ` +
528
+ `Nothing was recorded -- re-read the id from the ${family.tool("invoke_workflow")} result.`,
529
+ );
530
+ }
531
+ // A failed check plus an aborted call is the user cancelling, not Galaxy
532
+ // being unreachable. Recording an unverified block for a turn they
533
+ // stopped would leave them a record of something nobody confirmed.
534
+ if (check.outcome === "unreachable" && signal?.aborted) {
535
+ return recordFailure("Cancelled before Galaxy could confirm the invocation.");
536
+ }
537
+ const serverVerified = check.outcome === "found";
538
+
539
+ const submittedAt = new Date().toISOString();
540
+ // Guarded write: the poller and the agent both write this file while we
541
+ // are talking to Galaxy, and an unguarded whole-file rewrite would drop
542
+ // whatever landed in between. Resolving the anchor inside means it is
543
+ // checked against the bytes we're about to rewrite rather than a copy
544
+ // read earlier -- a block bound to a step that was renamed away is
545
+ // exactly the silent-nothing the check exists to stop.
546
+ const { inv, annotated } = await withNotebookLock(notebookPath, () =>
547
+ withNotebookCas(notebookPath, (content) => {
548
+ const notebookAnchor = requireAnchor(content, params.notebookAnchor);
549
+ const label = requireLabel(params.label);
550
+ // The harness writes a block the moment a submission answers, so
551
+ // the usual case here is a block that already exists. Annotating
552
+ // it means label and anchor only: the rest -- status, the poller's
553
+ // counters, the server it was submitted to, and its
554
+ // `server_verified` verdict -- belongs to whoever wrote it, and a
555
+ // fresh record object would reset a running invocation to
556
+ // `in_progress` and throw away the progress already polled.
557
+ //
558
+ // Presence and contents come from one physical block, so this
559
+ // cannot decide "nothing carries this id" about a block the upsert
560
+ // is nonetheless going to overwrite.
561
+ const found = locateInvocationBlock(content, params.invocationId);
562
+ if (found.present && !found.record) {
563
+ throw new Error(
564
+ `A loom-invocation block for ${params.invocationId} is already in the notebook ` +
565
+ `but cannot be read -- it is missing a required field or has been hand-edited. ` +
566
+ `Nothing was changed; fix or remove that block first.`,
567
+ );
568
+ }
569
+ const record: InvocationYaml = found.record
570
+ ? { ...found.record, notebookAnchor, label }
571
+ : {
572
+ invocationId: params.invocationId,
573
+ galaxyServerUrl,
574
+ notebookAnchor,
575
+ label,
576
+ submittedAt,
577
+ status: "in_progress",
578
+ serverVerified,
579
+ };
580
+ // Only a block this call creates carries provenance, and it is the
581
+ // agent's: the harness did not see this submission happen.
582
+ return {
583
+ content: upsertInvocationBlock(
584
+ content,
585
+ record,
586
+ found.present ? undefined : { submittedBy: "agent" },
587
+ ),
588
+ result: { inv: record, annotated: found.present },
589
+ };
590
+ }),
591
+ );
592
+
593
+ const where = `${inv.invocationId} (${inv.label}) at ${inv.notebookAnchor}`;
594
+ const proposal = params.proposalId
595
+ ? await noteProposalBinding(proposalDeps, {
596
+ proposalId: params.proposalId,
597
+ notebookAnchor: inv.notebookAnchor,
598
+ run: { kind: "invocation", id: inv.invocationId },
599
+ })
600
+ : undefined;
601
+ return {
602
+ content: [
603
+ {
604
+ type: "text",
605
+ text: JSON.stringify(
606
+ {
607
+ success: true,
608
+ invocationId: inv.invocationId,
609
+ notebookAnchor: inv.notebookAnchor,
610
+ label: inv.label,
611
+ status: inv.status,
612
+ annotated,
613
+ serverVerified: inv.serverVerified,
614
+ ...(proposal ? { proposal } : {}),
615
+ message: annotated
616
+ ? `Bound invocation ${where}. Orbit had already recorded the run; this ` +
617
+ `named the step it belongs to.`
618
+ : serverVerified
619
+ ? `Recorded invocation ${where}.`
620
+ : `Recorded invocation ${where}, but Galaxy could not confirm it ` +
621
+ `(${check.detail}). The block says server_verified: false; the poller ` +
622
+ `clears that on its first successful poll.`,
623
+ },
624
+ null,
625
+ 2,
626
+ ),
627
+ },
628
+ ],
629
+ details: {
630
+ invocationId: inv.invocationId,
631
+ notebookAnchor: inv.notebookAnchor,
632
+ serverVerified: inv.serverVerified,
633
+ annotated,
634
+ } as Record<string, unknown>,
635
+ };
636
+ } catch (error) {
637
+ return recordFailure(error instanceof Error ? error.message : String(error));
638
+ }
639
+ },
640
+ renderResult: (result) => {
641
+ const d = result.details as
642
+ | {
643
+ invocationId?: string;
644
+ notebookAnchor?: string;
645
+ serverVerified?: boolean;
646
+ annotated?: boolean;
647
+ error?: boolean;
648
+ }
649
+ | undefined;
650
+ if (d?.error) return new Text("❌ Failed to record invocation");
651
+ const unconfirmed = d?.serverVerified === false ? " (unconfirmed)" : "";
652
+ const verb = d?.annotated ? "Bound" : "Recorded";
653
+ return new Text(
654
+ `🔗 ${verb} invocation ${d?.invocationId} → ${d?.notebookAnchor}${unconfirmed}`,
655
+ );
656
+ },
657
+ });
658
+
659
+ // ─────────────────────────────────────────────────────────────────────────────
660
+ // Tool: Record a Galaxy tool run (job) in the notebook
661
+ // ─────────────────────────────────────────────────────────────────────────────
662
+ pi.registerTool({
663
+ name: "galaxy_job_record",
664
+ label: "Record Galaxy Job",
665
+ description: `Name the plan step a Galaxy TOOL run belongs to, the same way
666
+ galaxy_invocation_record does for a workflow. The harness records the run; you name the step
667
+ it belongs to. Orbit already wrote a \`loom-job\` block the moment ${family.tool("run_tool")} returned, so
668
+ this normally just sets \`label\` and \`notebook_anchor\` on the block already carrying that
669
+ job id -- the poller is watching it either way. If no block carries the id, this creates one.
670
+ Both arguments are checked before anything is written: the anchor must resolve in notebook.md,
671
+ and the job id must exist on the Galaxy server. \`toolId\` is used only when creating.`,
672
+ parameters: Type.Object({
673
+ jobId: Type.String({ description: `Galaxy job ID returned from ${family.tool("run_tool")}` }),
674
+ notebookAnchor: Type.String({
675
+ description:
676
+ "Anchor of the plan step this run belongs to, e.g. 'plan-a-step-3'. It must " +
677
+ "already exist in notebook.md, either as a {#anchor} on the step or as a " +
678
+ "heading; an anchor nothing resolves to is rejected.",
679
+ }),
680
+ label: Type.String({
681
+ description: "Human-readable description for status display, e.g. 'BWA alignment'",
682
+ }),
683
+ toolId: Type.Optional(
684
+ Type.String({
685
+ description:
686
+ "Galaxy tool id, e.g. 'bwa_mem' — shown if no label fits. Only used when this " +
687
+ "call creates the block; an existing block keeps the tool id it was recorded with.",
688
+ }),
689
+ ),
690
+ proposalId: Type.Optional(
691
+ Type.String({
692
+ description:
693
+ "The approved proposal this run carries out (the id from loom_propose that the " +
694
+ "user approved). Recorded alongside the run; it doesn't approve anything.",
695
+ }),
696
+ ),
697
+ }),
698
+ async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
699
+ const notebookPath = getNotebookPath();
700
+ if (!notebookPath) return recordFailure("No notebook open.");
701
+
702
+ const cfg = getGalaxyConfig();
703
+ try {
704
+ const check = await verifyGalaxyRun("job", params.jobId, signal);
705
+ if (check.outcome === "absent") {
706
+ return recordFailure(
707
+ `Galaxy has no job "${params.jobId}" (${check.detail}). Nothing was recorded -- ` +
708
+ `re-read the id from the ${family.tool("run_tool")} result. A tool run returns a job id, ` +
709
+ `not a dataset id.`,
710
+ );
711
+ }
712
+ if (check.outcome === "unreachable" && signal?.aborted) {
713
+ return recordFailure("Cancelled before Galaxy could confirm the job.");
714
+ }
715
+ const serverVerified = check.outcome === "found";
716
+
717
+ const submittedAt = new Date().toISOString();
718
+ const { job, annotated } = await withNotebookLock(notebookPath, () =>
719
+ withNotebookCas(notebookPath, (content) => {
720
+ const notebookAnchor = requireAnchor(content, params.notebookAnchor);
721
+ const label = requireLabel(params.label);
722
+ // Same rules as the invocation side: annotate means label and
723
+ // anchor only, and presence and contents come from one physical
724
+ // block. See the comments there.
725
+ const found = locateJobBlock(content, params.jobId);
726
+ if (found.present && !found.record) {
727
+ throw new Error(
728
+ `A loom-job block for ${params.jobId} is already in the notebook but cannot be ` +
729
+ `read. Nothing was changed; fix or remove that block first.`,
730
+ );
731
+ }
732
+ const record: JobYaml = found.record
733
+ ? { ...found.record, notebookAnchor, label }
734
+ : {
735
+ jobId: params.jobId,
736
+ galaxyServerUrl: cfg?.url || "",
737
+ notebookAnchor,
738
+ label,
739
+ toolId: params.toolId,
740
+ submittedAt,
741
+ status: "in_progress",
742
+ serverVerified,
743
+ };
744
+ return {
745
+ content: upsertJobBlock(
746
+ content,
747
+ record,
748
+ found.present ? undefined : { submittedBy: "agent" },
749
+ ),
750
+ result: { job: record, annotated: found.present },
751
+ };
752
+ }),
753
+ );
754
+
755
+ const where = `${job.jobId} (${job.label}) at ${job.notebookAnchor}`;
756
+ const proposal = params.proposalId
757
+ ? await noteProposalBinding(proposalDeps, {
758
+ proposalId: params.proposalId,
759
+ notebookAnchor: job.notebookAnchor,
760
+ run: { kind: "job", id: job.jobId },
761
+ })
762
+ : undefined;
763
+ return {
764
+ content: [
765
+ {
766
+ type: "text",
767
+ text: JSON.stringify(
768
+ {
769
+ success: true,
770
+ jobId: job.jobId,
771
+ notebookAnchor: job.notebookAnchor,
772
+ label: job.label,
773
+ status: job.status,
774
+ annotated,
775
+ serverVerified: job.serverVerified,
776
+ ...(proposal ? { proposal } : {}),
777
+ message: annotated
778
+ ? `Bound job ${where}. Orbit had already recorded the run; this named the ` +
779
+ `step it belongs to.`
780
+ : serverVerified
781
+ ? `Recorded job ${where}. The background poller will advance it and notify on completion.`
782
+ : `Recorded job ${where}, but Galaxy could not confirm it (${check.detail}). ` +
783
+ `The block says server_verified: false; the poller clears that on its ` +
784
+ `first successful poll.`,
785
+ },
786
+ null,
787
+ 2,
788
+ ),
789
+ },
790
+ ],
791
+ details: {
792
+ jobId: job.jobId,
793
+ notebookAnchor: job.notebookAnchor,
794
+ serverVerified: job.serverVerified,
795
+ annotated,
796
+ } as Record<string, unknown>,
797
+ };
798
+ } catch (error) {
799
+ return recordFailure(error instanceof Error ? error.message : String(error));
800
+ }
801
+ },
802
+ renderResult: (result) => {
803
+ const d = result.details as
804
+ | {
805
+ jobId?: string;
806
+ notebookAnchor?: string;
807
+ serverVerified?: boolean;
808
+ annotated?: boolean;
809
+ error?: boolean;
810
+ }
811
+ | undefined;
812
+ if (d?.error) return new Text("❌ Failed to record job");
813
+ const unconfirmed = d?.serverVerified === false ? " (unconfirmed)" : "";
814
+ const verb = d?.annotated ? "Bound" : "Recorded";
815
+ return new Text(`🔗 ${verb} job ${d?.jobId} → ${d?.notebookAnchor}${unconfirmed}`);
816
+ },
817
+ });
818
+
819
+ // ─────────────────────────────────────────────────────────────────────────────
820
+ // Tool: Poll all in-flight invocations and update notebook YAML
821
+ // ─────────────────────────────────────────────────────────────────────────────
822
+ pi.registerTool({
823
+ name: "galaxy_invocation_check_all",
824
+ label: "Check All Galaxy Invocations",
825
+ description: `Scan the notebook for in-flight loom-invocation blocks, poll Galaxy for each,
826
+ and apply deterministic state transitions: an invocation Galaxy has finished scheduling whose
827
+ jobs have all stopped becomes completed (or failed, if any errored); anything still scheduling,
828
+ running, queued or paused stays in_progress. Updates the YAML blocks in place. Returns a summary
829
+ list.`,
830
+ parameters: Type.Object({}),
831
+ async execute(_toolCallId, _params, signal, _onUpdate, _ctx) {
832
+ return await checkInvocations(undefined, signal);
833
+ },
834
+ renderResult: (result) => {
835
+ const d = result.details as { checked?: number; error?: boolean } | undefined;
836
+ if (d?.error) return new Text("❌ Invocation check failed");
837
+ return new Text(`🔍 Checked ${d?.checked || 0} invocation(s)`);
838
+ },
839
+ });
840
+
841
+ // ─────────────────────────────────────────────────────────────────────────────
842
+ // Tool: Poll one invocation by id
843
+ // ─────────────────────────────────────────────────────────────────────────────
844
+ pi.registerTool({
845
+ name: "galaxy_invocation_check_one",
846
+ label: "Check Galaxy Invocation",
847
+ description: `Poll a single Galaxy invocation by id. Same auto-transition rules as
848
+ galaxy_invocation_check_all. Errors if the invocation isn't recorded in the notebook.`,
849
+ parameters: Type.Object({
850
+ invocationId: Type.String({ description: "Galaxy invocation ID to check" }),
851
+ }),
852
+ async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
853
+ return await checkInvocations(params.invocationId, signal);
854
+ },
855
+ renderResult: (result) => {
856
+ const d = result.details as { checked?: number; error?: boolean } | undefined;
857
+ if (d?.error) return new Text("❌ Invocation check failed");
858
+ return new Text(`🔍 Checked ${d?.checked || 0} invocation(s)`);
859
+ },
860
+ });
861
+ }
862
+
863
+ // ─────────────────────────────────────────────────────────────────────────────
864
+ // Internal: shared poll loop for the two check tools
865
+ // ─────────────────────────────────────────────────────────────────────────────
866
+
867
+ interface CheckResultEntry {
868
+ invocationId: string;
869
+ notebookAnchor: string;
870
+ label: string;
871
+ invocationState: string;
872
+ /** The block's status before this poll, so a transition can name both ends. */
873
+ priorStatus: InvocationYaml["status"];
874
+ jobSummary: { ok: number; running: number; queued: number; error: number; other: number };
875
+ /** The raw Galaxy states behind `jobSummary.other`, counted, so they can be named. */
876
+ otherStates: Record<string, number>;
877
+ /** Jobs Galaxy could still advance: running + queued + the non-terminal half of `other`. */
878
+ activeJobs: number;
879
+ /** The status this poll wrote, when it wrote one. Absent on a no-op poll. */
880
+ newStatus?: InvocationYaml["status"];
881
+ lastPolledAt?: string;
882
+ autoAction?: string;
883
+ }
884
+
885
+ const TERMINAL_INVOCATION_STATES = INVOCATION_SCHEDULING_DONE;
886
+
887
+ /** Galaxy job states that mean "this job failed", as opposed to ended some other way. */
888
+ const FAILED_JOB_STATES: ReadonlySet<string> = new Set(["error", "failed", "deleted"]);
889
+
890
+ /** "1 paused, 2 skipped" — for naming the states hiding behind `other`. */
891
+ function describeStates(counts: Record<string, number>): string {
892
+ return Object.entries(counts)
893
+ .map(([state, n]) => `${n} ${state}`)
894
+ .join(", ");
895
+ }
896
+
897
+ /** What one invocation's jobs add up to. */
898
+ export interface InvocationJobRollup {
899
+ summary: { ok: number; running: number; queued: number; error: number; other: number };
900
+ /** The raw Galaxy states behind `summary.other`, counted. */
901
+ otherStates: Record<string, number>;
902
+ /** Jobs Galaxy could still advance: running + queued + the non-terminal half of `other`. */
903
+ activeJobs: number;
904
+ totalJobs: number;
905
+ completedSteps: number;
906
+ }
907
+
908
+ /** Count an invocation's jobs by state, keeping what `other` is actually made of. */
909
+ export function rollUpInvocationJobs(inv: InvocationDetail): InvocationJobRollup {
910
+ const summary = { ok: 0, running: 0, queued: 0, error: 0, other: 0 };
911
+ // What is actually behind `other`, counted by state. The rollup can't tell a
912
+ // paused job (Galaxy will run it) from a skipped one (a conditional step that
913
+ // never will), and both used to be ignored outright.
914
+ const otherStates: Record<string, number> = {};
915
+ let activeOther = 0;
916
+ let totalJobs = 0;
917
+ let completedSteps = 0;
918
+ for (const invStep of inv.steps) {
919
+ let stepJobs = 0;
920
+ let stepOk = 0;
921
+ for (const job of invStep.jobs) {
922
+ stepJobs++;
923
+ totalJobs++;
924
+ if (job.state === "ok") {
925
+ summary.ok++;
926
+ stepOk++;
927
+ } else if (job.state === "running") summary.running++;
928
+ else if (job.state === "queued" || job.state === "new" || job.state === "waiting")
929
+ summary.queued++;
930
+ else if (FAILED_JOB_STATES.has(job.state)) summary.error++;
931
+ else {
932
+ summary.other++;
933
+ const state = job.state || "unknown";
934
+ otherStates[state] = (otherStates[state] ?? 0) + 1;
935
+ // `skipped` and `stopped` are over; `paused`, `upload`,
936
+ // `setting_metadata`, `deleting` and friends are not.
937
+ if (!isTerminalJobState(job.state)) activeOther++;
938
+ }
939
+ }
940
+ // A step that runs no job (a workflow input, a step mapped over an empty
941
+ // collection) is done once Galaxy has scheduled it. A subworkflow step also
942
+ // has no job of its own, but its work is still going until the subworkflow
943
+ // finishes, so it isn't counted that way.
944
+ const doneWithoutJobs =
945
+ stepJobs === 0 && invStep.state === "scheduled" && !invStep.subworkflow_invocation_id;
946
+ if ((stepJobs > 0 && stepJobs === stepOk) || doneWithoutJobs) completedSteps++;
947
+ }
948
+ return {
949
+ summary,
950
+ otherStates,
951
+ activeJobs: summary.running + summary.queued + activeOther,
952
+ totalJobs,
953
+ completedSteps,
954
+ };
955
+ }
956
+
957
+ /**
958
+ * True while Galaxy could still advance this invocation — either it hasn't
959
+ * finished scheduling, or a job it already scheduled is still moving.
960
+ *
961
+ * The poller asks this about an invocation whose notebook block has vanished:
962
+ * a live run nobody is watching is worth saying out loud, a finished one isn't.
963
+ */
964
+ export function isInvocationLive(inv: InvocationDetail): boolean {
965
+ if (!TERMINAL_INVOCATION_STATES.has(inv.state)) return true;
966
+ return rollUpInvocationJobs(inv).activeJobs > 0;
967
+ }
968
+
969
+ interface CheckInvocationsResult {
970
+ content: { type: "text"; text: string }[];
971
+ details: Record<string, unknown>;
972
+ }
973
+
974
+ export async function checkInvocations(
975
+ specificId: string | undefined,
976
+ signal?: AbortSignal,
977
+ ): Promise<CheckInvocationsResult> {
978
+ const notebookPath = getNotebookPath();
979
+ if (!notebookPath) {
980
+ return {
981
+ content: [
982
+ {
983
+ type: "text" as const,
984
+ text: JSON.stringify({ success: false, error: "No notebook open." }),
985
+ },
986
+ ],
987
+ details: { error: true } as Record<string, unknown>,
988
+ };
989
+ }
990
+
991
+ if (!getGalaxyConfig()) {
992
+ return {
993
+ content: [
994
+ {
995
+ type: "text" as const,
996
+ text: JSON.stringify({ success: false, error: "Galaxy credentials not configured." }),
997
+ },
998
+ ],
999
+ details: { error: true } as Record<string, unknown>,
1000
+ };
1001
+ }
1002
+
1003
+ // Phase 1 — decide what to poll. Read under the lock so we see any Orbit
1004
+ // write (a just-finished invocation_record) whole, but hold it only for the
1005
+ // read: there is no network I/O in here.
1006
+ const blocks = await withNotebookLock(notebookPath, async () =>
1007
+ findInvocationBlocks(await readNotebook(notebookPath)),
1008
+ );
1009
+
1010
+ let toCheck: InvocationYaml[];
1011
+ if (specificId) {
1012
+ const found = blocks.find((b) => b.invocationId === specificId);
1013
+ if (!found) {
1014
+ return {
1015
+ content: [
1016
+ {
1017
+ type: "text" as const,
1018
+ text: JSON.stringify({
1019
+ success: false,
1020
+ error: `Invocation ${specificId} not found in notebook.`,
1021
+ }),
1022
+ },
1023
+ ],
1024
+ details: { error: true } as Record<string, unknown>,
1025
+ };
1026
+ }
1027
+ toCheck = [found];
1028
+ } else {
1029
+ toCheck = blocks.filter((b) => b.status === "in_progress");
1030
+ }
1031
+
1032
+ if (toCheck.length === 0) {
1033
+ return {
1034
+ content: [
1035
+ {
1036
+ type: "text" as const,
1037
+ text: JSON.stringify({
1038
+ success: true,
1039
+ results: [],
1040
+ message: "No in-progress invocations.",
1041
+ }),
1042
+ },
1043
+ ],
1044
+ details: { checked: 0 } as Record<string, unknown>,
1045
+ };
1046
+ }
1047
+
1048
+ // Phase 2 — talk to Galaxy with no lock held and nothing pinned in memory
1049
+ // about the notebook. These GETs are the slow part (one round trip per
1050
+ // in-flight invocation); before #391 they sat between the read and the write,
1051
+ // so every notebook write the agent made meanwhile was overwritten by a
1052
+ // seconds-stale snapshot. Errors stay per-block: a 502 on one invocation must
1053
+ // not cost the others their update.
1054
+ const results: CheckResultEntry[] = [];
1055
+ const updates: InvocationPollUpdate[] = [];
1056
+
1057
+ for (const block of toCheck) {
1058
+ try {
1059
+ // galaxyGetInvocation asks for step details, which the counters below
1060
+ // depend on: without them every step's `jobs` list is empty, neither the
1061
+ // completed nor the failed branch can fire, and the block sits at
1062
+ // in_progress forever with no toast and no transition.
1063
+ const inv = await galaxyGetInvocation(block.invocationId, signal);
1064
+
1065
+ const { summary, otherStates, activeJobs, totalJobs, completedSteps } =
1066
+ rollUpInvocationJobs(inv);
1067
+
1068
+ let autoAction: string | undefined;
1069
+ let transition: InvocationPollUpdate["transition"];
1070
+
1071
+ // Two questions, both of which the old predicate skipped: is Galaxy done
1072
+ // handing out jobs, and is any job it already handed out still moving? A
1073
+ // workflow whose first two steps are ok while the third is still being
1074
+ // scheduled is not finished, and neither is one holding a paused job.
1075
+ const schedulingDone = TERMINAL_INVOCATION_STATES.has(inv.state);
1076
+
1077
+ if (schedulingDone && activeJobs === 0) {
1078
+ const ended = describeStates(otherStates);
1079
+ if (inv.state === "cancelled") {
1080
+ // A cancel is not a failure and not a completion, and the block has
1081
+ // no third word for it -- so it lands terminal (nothing is coming
1082
+ // that would move it again) and the summary says what happened.
1083
+ const unfinished = summary.error > 0 ? `, ${summary.error} did not` : "";
1084
+ transition = {
1085
+ status: "failed",
1086
+ summary: `Workflow cancelled: ${summary.ok} job(s) finished before it stopped${unfinished}`,
1087
+ };
1088
+ autoAction = "cancelled";
1089
+ } else if (summary.error > 0) {
1090
+ transition = {
1091
+ status: "failed",
1092
+ summary: `Workflow failed: ${summary.error} job(s) errored, ${summary.ok} succeeded`,
1093
+ };
1094
+ autoAction = "failed";
1095
+ } else if (inv.state === "failed") {
1096
+ // Galaxy failed to schedule the workflow. No job carries the failure,
1097
+ // so without this the block sits at in_progress with nothing left to
1098
+ // poll it into a terminal state.
1099
+ transition = {
1100
+ status: "failed",
1101
+ summary: `Workflow failed: Galaxy reported invocation state "failed"`,
1102
+ };
1103
+ autoAction = "failed";
1104
+ } else if (summary.ok > 0 && INVOCATION_SCHEDULED_OK.has(inv.state)) {
1105
+ transition = {
1106
+ status: "completed",
1107
+ summary:
1108
+ `Workflow completed: ${summary.ok} jobs succeeded` + (ended ? ` (${ended})` : ""),
1109
+ };
1110
+ autoAction = "completed";
1111
+ }
1112
+ } else {
1113
+ // A cancel is not instant: Galaxy moves the invocation to `cancelled`
1114
+ // and then deletes its jobs one at a time. Naming it is what lets the
1115
+ // renderer tell a deliberate stop from a failure -- it has no
1116
+ // invocation state to look at, only this sentence -- and without it the
1117
+ // loudest surface in the product raises a red alarm about something the
1118
+ // user asked for.
1119
+ const stopping = inv.state === "cancelled" || inv.state === "cancelling";
1120
+ const tail =
1121
+ activeJobs > 0
1122
+ ? `${activeJobs} still running`
1123
+ : stopping
1124
+ ? `waiting for Galaxy to finish the cancel (state: ${inv.state})`
1125
+ : `invocation still scheduling (state: ${inv.state})`;
1126
+ if (stopping) {
1127
+ // Deliberately not gated on the failed counter. Right after a cancel
1128
+ // every job is still running, so nothing has landed in it yet -- and
1129
+ // that is exactly the window where the panel would otherwise say the
1130
+ // run is going along fine.
1131
+ //
1132
+ // "did not finish" rather than "failed" because the counter cannot
1133
+ // tell the two apart: rollUpInvocationJobs scores a `deleted` job
1134
+ // beside a genuinely errored one, so a run that broke and was then
1135
+ // cancelled has both in the same number. It is the word the panel
1136
+ // itself uses for a stopping row.
1137
+ transition = {
1138
+ status: "in_progress",
1139
+ summary: `Workflow cancelling: ${summary.error} job(s) did not finish, ${tail}`,
1140
+ };
1141
+ autoAction = "cancelling";
1142
+ } else if (summary.error > 0) {
1143
+ // A failure with work still in flight. Keep the block in_progress so
1144
+ // the rest stays under observation -- terminal blocks are never polled
1145
+ // again -- but say so in the summary and let the poller raise it once.
1146
+ transition = {
1147
+ status: "in_progress",
1148
+ summary: `Workflow in progress: ${summary.error} job(s) failed, ${tail}`,
1149
+ };
1150
+ autoAction = "failing";
1151
+ }
1152
+ }
1153
+
1154
+ // Always update the block — even if the rolled-up status didn't
1155
+ // change, the per-poll counters (and last_polled_at) did, and the
1156
+ // renderer wants those for the live progress bar. Status and summary
1157
+ // ride along only on a transition, so a no-op poll can't overwrite an
1158
+ // edit the agent made to the block while we were talking to Galaxy.
1159
+ const lastPolledAt = new Date().toISOString();
1160
+ updates.push({
1161
+ invocationId: block.invocationId,
1162
+ totalSteps: inv.steps.length,
1163
+ completedSteps,
1164
+ totalJobs,
1165
+ completedJobs: summary.ok,
1166
+ failedJobs: summary.error,
1167
+ lastPolledAt,
1168
+ // We just got an answer out of Galaxy for this id, which is the proof a
1169
+ // block recorded `server_verified: false` is waiting for -- but only if
1170
+ // the answer came from the server the block names. A profile switch
1171
+ // must not let another server's `inv-1` certify this one.
1172
+ serverVerified: sameGalaxyServer(block.galaxyServerUrl, getGalaxyConfig()?.url),
1173
+ transition,
1174
+ });
1175
+
1176
+ results.push({
1177
+ invocationId: block.invocationId,
1178
+ notebookAnchor: block.notebookAnchor,
1179
+ label: block.label,
1180
+ invocationState: inv.state,
1181
+ priorStatus: block.status,
1182
+ jobSummary: summary,
1183
+ otherStates,
1184
+ activeJobs,
1185
+ newStatus: transition?.status,
1186
+ lastPolledAt,
1187
+ autoAction,
1188
+ });
1189
+ } catch (error) {
1190
+ const msg = error instanceof Error ? error.message : String(error);
1191
+ results.push({
1192
+ invocationId: block.invocationId,
1193
+ notebookAnchor: block.notebookAnchor,
1194
+ label: block.label,
1195
+ invocationState: "error_checking",
1196
+ priorStatus: block.status,
1197
+ jobSummary: { ok: 0, running: 0, queued: 0, error: 0, other: 0 },
1198
+ otherStates: {},
1199
+ activeJobs: 0,
1200
+ autoAction: `check_error: ${msg}`,
1201
+ });
1202
+ }
1203
+ }
1204
+
1205
+ // Phase 3 — persist. The lock still keeps two Orbit writers from losing each
1206
+ // other's updates, but the guarantee now comes from re-reading inside it
1207
+ // rather than from having held it across the poll: whoever gets the lock
1208
+ // second folds their blocks into content that already contains the first
1209
+ // writer's. A write that loses to an outside writer between our read and our
1210
+ // rename fails the stamp check and is retried against fresh content rather
1211
+ // than silently overwriting it.
1212
+ if (updates.length > 0) {
1213
+ const { applied, transitioned } = await withNotebookLock(notebookPath, () =>
1214
+ persistInvocationUpdates(notebookPath, updates),
1215
+ );
1216
+ // A transition we didn't actually record isn't news. The poller turns
1217
+ // "completed"/"failed" straight into a user-facing toast, so leaving the
1218
+ // flag on a block that was deleted mid-poll — or that another poller had
1219
+ // already advanced — would announce a state change nothing wrote.
1220
+ //
1221
+ // "failing" and "cancelling" never change the status, so they can't be
1222
+ // checked against the transitions; they're news as long as the counters
1223
+ // and summary carrying them landed somewhere.
1224
+ for (const entry of results) {
1225
+ const announced =
1226
+ entry.autoAction === "completed" ||
1227
+ entry.autoAction === "failed" ||
1228
+ entry.autoAction === "cancelled";
1229
+ if (announced && !transitioned.has(entry.invocationId)) entry.autoAction = undefined;
1230
+ const midFlight = entry.autoAction === "failing" || entry.autoAction === "cancelling";
1231
+ if (midFlight && !applied.has(entry.invocationId)) {
1232
+ entry.autoAction = undefined;
1233
+ }
1234
+ }
1235
+ }
1236
+
1237
+ return {
1238
+ content: [
1239
+ {
1240
+ type: "text" as const,
1241
+ text: JSON.stringify({ success: true, checked: results.length, results }, null, 2),
1242
+ },
1243
+ ],
1244
+ // Expose the per-invocation results so the background poller can fire a
1245
+ // completion notification for any invocation that just reached a terminal
1246
+ // state (autoAction "completed"/"failed"). The agent-facing summary already
1247
+ // travels in `content`; this is for the headless poller path.
1248
+ details: { checked: results.length, results } as Record<string, unknown>,
1249
+ };
1250
+ }
1251
+
1252
+ // A write only loses the stamp check when someone else wrote in the microseconds
1253
+ // between our read and our rename; three attempts is far more than convergence
1254
+ // needs, and bounding it keeps a pathological writer from spinning us.
1255
+ const MAX_PERSIST_ATTEMPTS = 3;
1256
+
1257
+ /**
1258
+ * Read -> fold in the polled blocks -> write, retrying against fresh content if
1259
+ * the notebook changed under us. Call with the notebook lock held. Returns the
1260
+ * invocation ids this call wrote, and separately those whose status it changed.
1261
+ *
1262
+ * The stamp is taken *before* the read on purpose. Stamping afterwards would
1263
+ * let a write that landed between the read and the stat look unchanged — the
1264
+ * exact clobber this is here to stop — whereas stamping first can only ever
1265
+ * cost us a spurious retry.
1266
+ */
1267
+ async function persistInvocationUpdates(
1268
+ notebookPath: string,
1269
+ updates: InvocationPollUpdate[],
1270
+ ): Promise<{ applied: Set<string>; transitioned: Set<string> }> {
1271
+ let lastError: unknown;
1272
+ for (let attempt = 0; attempt < MAX_PERSIST_ATTEMPTS; attempt++) {
1273
+ const stamp = await statNotebook(notebookPath);
1274
+ // Read regardless of the stat, so a notebook that's actually gone or
1275
+ // unreadable fails with its own ENOENT/EACCES instead of being dressed up
1276
+ // as a race.
1277
+ const fresh = await readNotebook(notebookPath);
1278
+ // Readable but unstattable: without a stamp there's no compare-and-swap,
1279
+ // and an unguarded whole-file write is the thing #391 is about. Treat it as
1280
+ // a lost race rather than quietly downgrading to one.
1281
+ if (!stamp) {
1282
+ lastError = new NotebookChangedError(notebookPath);
1283
+ continue;
1284
+ }
1285
+ const { content, applied, transitioned } = applyInvocationUpdates(fresh, updates);
1286
+ // Every block was deleted or already has a newer poll on disk — nothing to
1287
+ // write, so don't rewrite the file (and don't risk a race) for no change.
1288
+ if (applied.length === 0) return { applied: new Set(), transitioned: new Set() };
1289
+ try {
1290
+ await writeNotebook(notebookPath, content, stamp);
1291
+ return { applied: new Set(applied), transitioned: new Set(transitioned) };
1292
+ } catch (error) {
1293
+ if (!(error instanceof NotebookChangedError)) throw error;
1294
+ lastError = error;
1295
+ }
1296
+ }
1297
+ // Surface it: the poller's catch drops the tick (so no completion toast fires
1298
+ // for a transition we failed to record) and the next tick re-polls.
1299
+ throw lastError;
1300
+ }