@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,863 @@
1
+ /**
2
+ * Notebook file I/O.
3
+ *
4
+ * The notebook is plain user/agent-curated markdown. This module provides
5
+ * file-system helpers and string-level utilities for the one structured
6
+ * thing inside a notebook: `loom-invocation` fenced YAML blocks that the
7
+ * Galaxy invocation polling tools read and write.
8
+ */
9
+
10
+ import * as path from "path";
11
+ import {
12
+ NotebookChangedError,
13
+ recordStore,
14
+ withRecordCas,
15
+ withRecordLock,
16
+ type RecordVersion,
17
+ } from "./record-store";
18
+ import {
19
+ isNotebookFenceOpen,
20
+ notebookFenceOpen,
21
+ replaceNotebookBlocks,
22
+ } from "../../shared/notebook-fences.js";
23
+ import {
24
+ appendIndexOutsideOpenFence,
25
+ blockLine,
26
+ isFoldableDuplicate,
27
+ scanFencedBlocks,
28
+ mergeHarnessFields,
29
+ parseHarnessFields,
30
+ renderHarnessFieldLines,
31
+ stripHarnessFields,
32
+ type HarnessBlockFields,
33
+ } from "./harness-block-fields";
34
+ import { parseProposalFields, type Proposal, type ProposalSighting } from "./registry-proposal";
35
+
36
+ /**
37
+ * Generate slug from title for default filename.
38
+ */
39
+ export function slugify(title: string): string {
40
+ return title
41
+ .toLowerCase()
42
+ .replace(/[^a-z0-9]+/g, "-")
43
+ .replace(/^-|-$/g, "")
44
+ .slice(0, 40);
45
+ }
46
+
47
+ /**
48
+ * The I/O below is a thin delegate onto the record store (record-store.ts).
49
+ * These names stay because sixteen modules and their tests import them; the
50
+ * lock, the compare-and-swap loop and the atomic file write live in the store.
51
+ */
52
+
53
+ /** Opaque record version, the expected-value half of a compare-and-swap write. */
54
+ export type NotebookStamp = RecordVersion;
55
+
56
+ export { NotebookChangedError };
57
+
58
+ /** Per-key mutex chain; see withRecordLock. */
59
+ export const withNotebookLock = withRecordLock;
60
+
61
+ /** Version the record, or null if it isn't there. */
62
+ export const statNotebook = (filePath: string) => recordStore(filePath).stat();
63
+
64
+ /** Write the record; with `expected` it is a compare-and-swap (see RecordStore.write). */
65
+ export async function writeNotebook(
66
+ filePath: string,
67
+ content: string,
68
+ expected?: NotebookStamp,
69
+ ): Promise<void> {
70
+ await recordStore(filePath).write(content, expected);
71
+ }
72
+
73
+ export async function readNotebook(filePath: string): Promise<string> {
74
+ return (await recordStore(filePath).read()).content;
75
+ }
76
+
77
+ /** Read -> apply -> write under compare-and-swap; call with the lock held. See withRecordCas. */
78
+ export const withNotebookCas = <T>(
79
+ filePath: string,
80
+ apply: (content: string) => { content: string; result: T },
81
+ ): Promise<T> => withRecordCas(recordStore(filePath), apply);
82
+
83
+ /**
84
+ * Default notebook path for a session directory. `title` is kept in the
85
+ * signature for API stability but is no longer used — every session dir
86
+ * stores its notebook as `notebook.md`.
87
+ */
88
+ export function getDefaultNotebookPath(_title: string, directory: string): string {
89
+ return path.join(directory, "notebook.md");
90
+ }
91
+
92
+ // ─────────────────────────────────────────────────────────────────────────────
93
+ // Invocation YAML blocks
94
+ // ─────────────────────────────────────────────────────────────────────────────
95
+
96
+ /**
97
+ * Structured Galaxy invocation record embedded in the notebook as a
98
+ * `loom-invocation` fenced YAML block. The block format is intentionally
99
+ * line-oriented and grep-friendly:
100
+ *
101
+ * ```loom-invocation
102
+ * invocation_id: abc123
103
+ * galaxy_server_url: https://usegalaxy.org
104
+ * notebook_anchor: plan-1-step-3
105
+ * label: BWA alignment
106
+ * submitted_at: 2026-04-25T15:30:00Z
107
+ * status: in_progress
108
+ * summary: ""
109
+ * ```
110
+ *
111
+ * Status transitions (`in_progress` → `completed`/`failed`) are written by
112
+ * the invocation polling tools (see tools.ts). The block is the source of
113
+ * truth — there's no in-memory cache.
114
+ */
115
+ export interface InvocationYaml extends HarnessBlockFields {
116
+ invocationId: string;
117
+ galaxyServerUrl: string;
118
+ notebookAnchor: string;
119
+ label: string;
120
+ submittedAt: string;
121
+ status: "in_progress" | "completed" | "failed";
122
+ summary?: string;
123
+ /**
124
+ * Whether Galaxy confirmed this invocation id exists at record time.
125
+ *
126
+ * `false` means the record tool asked and didn't get an answer (Galaxy
127
+ * unreachable, credentials gone) — the run is written down anyway, because
128
+ * losing a real submission to a transient network is worse than an
129
+ * unconfirmed line, and the first successful poll upgrades it. Absent means
130
+ * the block predates the field and nothing has claimed either way.
131
+ */
132
+ serverVerified?: boolean;
133
+ // Progress counters — populated by galaxy_invocation_check_*. Persisted
134
+ // back to the YAML so the Orbit renderer can draw a live progress bar
135
+ // without each side polling Galaxy independently. Optional so older
136
+ // blocks (and the initial record-time write) round-trip cleanly.
137
+ totalSteps?: number;
138
+ completedSteps?: number;
139
+ totalJobs?: number;
140
+ completedJobs?: number;
141
+ failedJobs?: number;
142
+ lastPolledAt?: string;
143
+ }
144
+
145
+ const INVOCATION_FENCE_OPEN = notebookFenceOpen("invocation");
146
+ const INVOCATION_FENCE_CLOSE = "```";
147
+
148
+ /**
149
+ * Render an invocation as a `loom-invocation` fenced block. The trailing
150
+ * newline is intentional so blocks can be appended cleanly.
151
+ */
152
+ export function renderInvocationYaml(inv: InvocationYaml): string {
153
+ const lines: string[] = [
154
+ INVOCATION_FENCE_OPEN,
155
+ blockLine("invocation_id", inv.invocationId, "invocationId"),
156
+ blockLine("galaxy_server_url", inv.galaxyServerUrl),
157
+ blockLine("notebook_anchor", inv.notebookAnchor, "notebookAnchor"),
158
+ `label: ${escapeYaml(inv.label)}`,
159
+ blockLine("submitted_at", inv.submittedAt),
160
+ `status: ${inv.status}`,
161
+ `summary: ${escapeYaml(inv.summary ?? "")}`,
162
+ ];
163
+ if (inv.serverVerified !== undefined) lines.push(`server_verified: ${inv.serverVerified}`);
164
+ if (inv.totalSteps !== undefined) lines.push(`total_steps: ${inv.totalSteps}`);
165
+ if (inv.completedSteps !== undefined) lines.push(`completed_steps: ${inv.completedSteps}`);
166
+ if (inv.totalJobs !== undefined) lines.push(`total_jobs: ${inv.totalJobs}`);
167
+ if (inv.completedJobs !== undefined) lines.push(`completed_jobs: ${inv.completedJobs}`);
168
+ if (inv.failedJobs !== undefined) lines.push(`failed_jobs: ${inv.failedJobs}`);
169
+ if (inv.lastPolledAt) lines.push(blockLine("last_polled_at", inv.lastPolledAt));
170
+ lines.push(...renderHarnessFieldLines(inv));
171
+ lines.push(INVOCATION_FENCE_CLOSE);
172
+ return lines.join("\n") + "\n";
173
+ }
174
+
175
+ /**
176
+ * Find every `loom-invocation` block in the notebook content and parse
177
+ * each into an InvocationYaml. Skips blocks that fail validation.
178
+ */
179
+ export function findInvocationBlocks(content: string): InvocationYaml[] {
180
+ const lines = content.split("\n");
181
+ const result: InvocationYaml[] = [];
182
+ for (const range of scanFencedBlocks(lines, "invocation")) {
183
+ const parsed = parseInvocationBlock(lines.slice(range.start + 1, range.end));
184
+ if (parsed) result.push(parsed);
185
+ }
186
+ return result;
187
+ }
188
+
189
+ /**
190
+ * Find the first `loom-invocation` block physically carrying this id, and what
191
+ * it parses to.
192
+ *
193
+ * Two lookups used to answer "is there a block for this id": the range finder,
194
+ * which matches an `invocation_id:` line inside a fence, and
195
+ * `findInvocationBlocks`, which additionally requires the block to be
196
+ * well-formed. They can disagree -- a block with an empty `label` is found by
197
+ * the first and rejected by the second -- and every caller that asked one
198
+ * question of one and one of the other was reading a different block than the
199
+ * one it was about to overwrite. This answers both from the same physical
200
+ * block, so they cannot drift apart. `present` with a null `record` means the
201
+ * block is there but unreadable, which is a refusal, not a licence to
202
+ * overwrite it.
203
+ */
204
+ export function locateInvocationBlock(
205
+ content: string,
206
+ invocationId: string,
207
+ ): { present: boolean; record: InvocationYaml | null } {
208
+ const lines = content.split("\n");
209
+ const range = findInvocationBlockRanges(content).find((b) => b.invocationId === invocationId);
210
+ if (!range) return { present: false, record: null };
211
+ return { present: true, record: parseInvocationBlock(lines.slice(range.start + 1, range.end)) };
212
+ }
213
+
214
+ /**
215
+ * Upsert a `loom-invocation` block in the notebook content keyed by
216
+ * `invocation_id`. If a block with the same id exists, replace it in
217
+ * place (preserving surrounding whitespace). Otherwise append at the
218
+ * end of the file with a leading blank line for readability.
219
+ *
220
+ * Harness-only fields on `inv` are always discarded. They come either from
221
+ * the block already on disk (so an agent or poller write carries them
222
+ * forward untouched) or from the explicit `harness` argument, which only the
223
+ * auto-registration and enrichment paths pass. A caller cannot set
224
+ * `submitted_by: harness` by spreading tool arguments into the record, which
225
+ * is the whole point of that field. `server_verified` is not one of them --
226
+ * see harness-block-fields.ts -- so the record tools' and the poller's writes
227
+ * of it pass straight through.
228
+ */
229
+ export function upsertInvocationBlock(
230
+ content: string,
231
+ inv: InvocationYaml,
232
+ harness?: HarnessBlockFields,
233
+ ): string {
234
+ const blocks = findInvocationBlockRanges(content);
235
+ const lines = content.split("\n");
236
+
237
+ // More than one can match when an older writer used the other fence prefix;
238
+ // the first is rewritten and foldable duplicates collapse into it.
239
+ const matching = blocks.filter(
240
+ (b) => b.invocationId === inv.invocationId && isUnambiguousRange(lines, b.start),
241
+ );
242
+ const existing = matching[0];
243
+ // Provenance comes off the block this write is about to replace, read from
244
+ // its raw lines rather than from a second scan of the file. A scan can land
245
+ // on a different block with the same id, and it drops a block the strict
246
+ // parser rejects -- either way the carry-forward would be sourced from
247
+ // somewhere other than the bytes being overwritten.
248
+ const onDiskRaw = existing
249
+ ? rawInvocationFields(lines.slice(existing.start + 1, existing.end))
250
+ : {};
251
+ const onDisk = parseHarnessFields((key) => onDiskRaw[key]);
252
+ const merged: InvocationYaml = {
253
+ ...stripHarnessFields(inv),
254
+ ...mergeHarnessFields(onDisk, harness ?? {}),
255
+ };
256
+ const newBlock = renderInvocationYaml(merged).trimEnd().split("\n");
257
+
258
+ if (existing) {
259
+ const replaced = matching.filter(
260
+ (b) =>
261
+ b === existing ||
262
+ isFoldableDuplicate(
263
+ onDiskRaw.attempt_id,
264
+ rawInvocationFields(lines.slice(b.start + 1, b.end)).attempt_id,
265
+ ),
266
+ );
267
+ return replaceNotebookBlocks(content, replaced, newBlock);
268
+ }
269
+
270
+ return appendBlock(content, newBlock);
271
+ }
272
+
273
+ /**
274
+ * Whether a block starting here can be replaced in place.
275
+ *
276
+ * Only when no Orbit fence before it is still open. Past an unclosed opener the
277
+ * scanners cannot tell whose closing fence is whose: a bare ``` after an
278
+ * unterminated `loom-job` and a `loom-invocation` opener could end either one,
279
+ * and reading it as the invocation's means its "body" runs through whatever
280
+ * the user wrote in between. Field-shaped prose (`note: keep this`) sits
281
+ * inside that body and a replacement would take it with it.
282
+ *
283
+ * Refusing to replace is the answer rather than a stricter body rule, because
284
+ * "every line is a field this version knows" is not a property we can require:
285
+ * a notebook written by a newer Orbit carries fields this one has never heard
286
+ * of, and the parsers ignore unknown keys on purpose. So a block in the
287
+ * ambiguous region is left exactly as it is and a clean one is written above
288
+ * the orphan. That costs one duplicate in an already-broken notebook, and the
289
+ * duplicate is unambiguous, so it is the one every write after this finds.
290
+ */
291
+ export function isUnambiguousRange(lines: readonly string[], start: number): boolean {
292
+ return start < appendIndexOutsideOpenFence(lines);
293
+ }
294
+
295
+ /**
296
+ * Append a rendered block, or slot it in ahead of an unclosed fence so its own
297
+ * closing line cannot become that fence's. See `appendIndexOutsideOpenFence`.
298
+ */
299
+ export function appendBlock(content: string, newBlock: string[]): string {
300
+ const lines = content.split("\n");
301
+ const at = appendIndexOutsideOpenFence(lines);
302
+ if (at < lines.length) {
303
+ const before = lines.slice(0, at).join("\n").replace(/\s+$/, "");
304
+ const sep = before.length > 0 ? "\n\n" : "";
305
+ return `${before}${sep}${newBlock.join("\n")}\n\n${lines.slice(at).join("\n")}`;
306
+ }
307
+ const trimmed = content.replace(/\s+$/, "");
308
+ const sep = trimmed.length > 0 ? "\n\n" : "";
309
+ return trimmed + sep + newBlock.join("\n") + "\n";
310
+ }
311
+
312
+ /**
313
+ * What one Galaxy poll learned about an invocation — the fields the poller
314
+ * owns, and nothing else.
315
+ *
316
+ * Deliberately not a whole `InvocationYaml`: `label`, `notebook_anchor`,
317
+ * `submitted_at` and friends belong to whoever recorded the invocation, and
318
+ * writing back a copy captured before the Galaxy round trip would clobber an
319
+ * edit the agent made in the meantime — #391 again, one block down.
320
+ */
321
+ export interface InvocationPollUpdate {
322
+ invocationId: string;
323
+ totalSteps: number;
324
+ completedSteps: number;
325
+ totalJobs: number;
326
+ completedJobs: number;
327
+ failedJobs: number;
328
+ lastPolledAt: string;
329
+ /**
330
+ * True when this update came from a Galaxy round trip that answered — which
331
+ * is proof the id exists, and the only thing that clears a block recorded
332
+ * `server_verified: false`.
333
+ */
334
+ serverVerified?: boolean;
335
+ /** Present only when this poll decided the invocation reached a terminal state. */
336
+ transition?: { status: InvocationYaml["status"]; summary: string };
337
+ }
338
+
339
+ /**
340
+ * Fold a batch of freshly-polled invocations into notebook content that was
341
+ * read *after* the poll, so the result is built on current bytes rather than a
342
+ * pre-poll snapshot (#391). Each update is merged onto the block as it exists
343
+ * in `content`, so fields the poller doesn't own survive.
344
+ *
345
+ * Three updates are dropped rather than applied:
346
+ * - the block is gone from `content` — someone deleted it while we were
347
+ * talking to Galaxy, and `upsertInvocationBlock` would resurrect it at the
348
+ * end of the file;
349
+ * - the block on disk carries a newer `last_polled_at` than ours — a second
350
+ * poller (another Orbit process, or the agent calling check_all while the
351
+ * background timer is mid-tick) already recorded a later reading, and our
352
+ * counters would walk it backwards;
353
+ * - the update would reopen a block that is already terminal on disk. A poll
354
+ * that saw a job erroring while others ran writes `in_progress` on purpose,
355
+ * and a slow round trip can deliver that verdict *after* a faster checker
356
+ * recorded the run's real end — our timestamp is later, our snapshot isn't.
357
+ * Nothing ever moves a terminal block back to running, so the reopening is
358
+ * always the stale one.
359
+ *
360
+ * A transition always lands, including one terminal state correcting another:
361
+ * completion is inferred from the jobs Galaxy has materialized so far, so a
362
+ * poll that catches a workflow mid-schedule can call it complete and a later
363
+ * one has to be able to say otherwise. Refusing that would pin `completed` next
364
+ * to a nonzero `failed_jobs` — the counters and the status must agree.
365
+ *
366
+ * Returns the ids written, and separately the ids whose status this batch
367
+ * actually changed. The caller needs the second set to know which transitions
368
+ * are its to announce: re-writing `completed` over `completed` is a refresh,
369
+ * not news, and shouldn't produce a second "your workflow finished" toast.
370
+ */
371
+ export function applyInvocationUpdates(
372
+ content: string,
373
+ updates: InvocationPollUpdate[],
374
+ ): { content: string; applied: string[]; transitioned: string[] } {
375
+ let next = content;
376
+ const applied: string[] = [];
377
+ const transitioned: string[] = [];
378
+ for (const update of updates) {
379
+ const current = findInvocationBlocks(next).find((b) => b.invocationId === update.invocationId);
380
+ if (!current) continue;
381
+ if (isNewerPoll(current.lastPolledAt, update.lastPolledAt)) continue;
382
+ if (update.transition?.status === "in_progress" && current.status !== "in_progress") continue;
383
+ const merged: InvocationYaml = {
384
+ ...current,
385
+ totalSteps: update.totalSteps,
386
+ completedSteps: update.completedSteps,
387
+ totalJobs: update.totalJobs,
388
+ completedJobs: update.completedJobs,
389
+ failedJobs: update.failedJobs,
390
+ lastPolledAt: update.lastPolledAt,
391
+ // A poll Galaxy answered is proof the id exists, so it clears a block the
392
+ // record tool could only write unverified. A block with no flag at all
393
+ // predates the field; leave it alone rather than churn every old block in
394
+ // the notebook on the next tick.
395
+ ...(update.serverVerified && current.serverVerified === false
396
+ ? { serverVerified: true }
397
+ : {}),
398
+ ...(update.transition ?? {}),
399
+ };
400
+ next = upsertInvocationBlock(next, merged);
401
+ applied.push(update.invocationId);
402
+ if (merged.status !== current.status) transitioned.push(update.invocationId);
403
+ }
404
+ return { content: next, applied, transitioned };
405
+ }
406
+
407
+ /**
408
+ * How far ahead of now an on-disk `last_polled_at` may sit and still be read as
409
+ * a real reading. A competing poller stamps its update within milliseconds of
410
+ * ours -- the window only has to cover clock differences between two writers of
411
+ * the same file, which is seconds at worst.
412
+ */
413
+ const MAX_POLL_CLOCK_SKEW_MS = 60_000;
414
+
415
+ /**
416
+ * True when `onDisk` is a strictly later poll timestamp than `ours`.
417
+ *
418
+ * A timestamp further ahead than the skew window did not come from a poll: it
419
+ * came from someone editing the block, and honouring it silences the poller for
420
+ * that invocation permanently -- every later update looks stale forever. Nobody
421
+ * can outrun the clock, so an implausible future reading is ignored rather than
422
+ * obeyed.
423
+ */
424
+ function isNewerPoll(onDisk: string | undefined, ours: string | undefined): boolean {
425
+ if (!onDisk || !ours) return false;
426
+ const a = Date.parse(onDisk);
427
+ const b = Date.parse(ours);
428
+ if (Number.isNaN(a) || Number.isNaN(b)) return false;
429
+ if (a > Date.now() + MAX_POLL_CLOCK_SKEW_MS) return false;
430
+ return a > b;
431
+ }
432
+
433
+ interface InvocationBlockRange {
434
+ invocationId: string;
435
+ start: number;
436
+ end: number;
437
+ }
438
+
439
+ function findInvocationBlockRanges(content: string): InvocationBlockRange[] {
440
+ const lines = content.split("\n");
441
+ const result: InvocationBlockRange[] = [];
442
+ for (const range of scanFencedBlocks(lines, "invocation")) {
443
+ let invocationId: string | null = null;
444
+ for (let i = range.start + 1; i < range.end; i++) {
445
+ const m = lines[i].match(/^invocation_id:\s*(.+)$/);
446
+ if (m) invocationId = m[1].trim();
447
+ }
448
+ if (invocationId) result.push({ invocationId, start: range.start, end: range.end });
449
+ }
450
+ return result;
451
+ }
452
+
453
+ /**
454
+ * A YAML boolean, or undefined for anything else — including a hand-edited
455
+ * value we can't read. Absent and unreadable both mean "nobody has claimed
456
+ * this", which is the honest default for a flag about a server round trip.
457
+ */
458
+ function parseBooleanField(raw: string | undefined): boolean | undefined {
459
+ if (raw === "true") return true;
460
+ if (raw === "false") return false;
461
+ return undefined;
462
+ }
463
+
464
+ /**
465
+ * The block's `key: value` lines, untouched.
466
+ *
467
+ * Harness fields are bare tokens or single-line JSON, so they are read from
468
+ * the raw text: running `unescapeYaml` over a JSON array would strip nothing
469
+ * today but would silently mangle the first value that starts and ends with a
470
+ * quote. The upsert reads carry-forward provenance through here too, which is
471
+ * why it is separate from `parseInvocationBlock` -- provenance has to survive
472
+ * a block the strict parser rejects.
473
+ */
474
+ function rawInvocationFields(blockLines: string[]): Record<string, string> {
475
+ const rawFields: Record<string, string> = {};
476
+ for (const line of blockLines) {
477
+ const m = line.match(/^([a-z_]+):\s*(.*)$/);
478
+ if (m) rawFields[m[1]] = m[2].trim();
479
+ }
480
+ return rawFields;
481
+ }
482
+
483
+ function parseInvocationBlock(blockLines: string[]): InvocationYaml | null {
484
+ const rawFields = rawInvocationFields(blockLines);
485
+ const fields: Record<string, string> = {};
486
+ for (const [key, value] of Object.entries(rawFields)) fields[key] = unescapeYaml(value);
487
+ const status = fields.status as InvocationYaml["status"];
488
+ // `galaxy_server_url` is metadata, not identity, and it is deliberately not
489
+ // required: it comes from GALAXY_URL, which can be absent (or arrive later
490
+ // via /connect) while the submission itself is perfectly real. Requiring it
491
+ // meant a block written without it parsed back as null, so the poller never
492
+ // saw the run and the notebook looked like it had a record when nothing was
493
+ // watching. The job block has always tolerated an empty one; these now agree.
494
+ if (
495
+ !fields.invocation_id ||
496
+ !fields.notebook_anchor ||
497
+ !fields.label ||
498
+ !fields.submitted_at ||
499
+ (status !== "in_progress" && status !== "completed" && status !== "failed")
500
+ ) {
501
+ return null;
502
+ }
503
+ const numField = (key: string): number | undefined => {
504
+ const raw = fields[key];
505
+ if (raw === undefined || raw === "") return undefined;
506
+ const n = Number(raw);
507
+ return Number.isFinite(n) ? n : undefined;
508
+ };
509
+ return {
510
+ invocationId: fields.invocation_id,
511
+ galaxyServerUrl: fields.galaxy_server_url ?? "",
512
+ notebookAnchor: fields.notebook_anchor,
513
+ label: fields.label,
514
+ submittedAt: fields.submitted_at,
515
+ status,
516
+ summary: fields.summary || undefined,
517
+ serverVerified: parseBooleanField(fields.server_verified),
518
+ totalSteps: numField("total_steps"),
519
+ completedSteps: numField("completed_steps"),
520
+ totalJobs: numField("total_jobs"),
521
+ completedJobs: numField("completed_jobs"),
522
+ failedJobs: numField("failed_jobs"),
523
+ lastPolledAt: fields.last_polled_at || undefined,
524
+ ...parseHarnessFields((key) => rawFields[key]),
525
+ };
526
+ }
527
+
528
+ // ─────────────────────────────────────────────────────────────────────────────
529
+ // Proposal blocks
530
+ // ─────────────────────────────────────────────────────────────────────────────
531
+
532
+ /**
533
+ * `loom-proposal` block: a concrete run the model proposes and the user can
534
+ * approve (registry design v3 §5). Same line grammar as every other block --
535
+ * one `key: value` per line, the structured fields as single-line JSON --
536
+ * and the same forward compatibility: unknown keys are ignored on read.
537
+ *
538
+ * ```loom-proposal
539
+ * proposal_id: prop-7k2m9x
540
+ * step_anchor: plan-a-step-2
541
+ * label: Trim reads
542
+ * target: {"kind":"tool","tool_id":"fastp","version":"unpinned"}
543
+ * server_url: https://usegalaxy.org
544
+ * history_id: df8fe5ddadbf3ab1
545
+ * inputs: [{"slot":"reads","src":"hda","id":"4b6e2f1a9c3d5e70"}]
546
+ * overrides: [{"param":"threads","value":4,"rationale":"four cores"}]
547
+ * predicate: {"kind":"manual"}
548
+ * assertions: []
549
+ * template_digest: <sha256 of the template it was validated against>
550
+ * created_at: 2026-10-07T12:00:00.000Z
551
+ * spec_revision: <written by the harness after /approve>
552
+ * ```
553
+ *
554
+ * Nothing in the block is trusted. `spec_revision` is written by the harness
555
+ * after an approval so a reader can see one happened, but it is display only:
556
+ * the parser does not return it, and whether a proposal is approved is a
557
+ * question for the registry.
558
+ */
559
+ export interface ProposalBlock {
560
+ /** The id the block claims, even when the rest of it doesn't parse. */
561
+ proposalId: string | null;
562
+ proposal: Proposal | null;
563
+ errors: string[];
564
+ start: number;
565
+ end: number;
566
+ }
567
+
568
+ const PROPOSAL_FENCE_OPEN = notebookFenceOpen("proposal");
569
+
570
+ export function renderProposalBlock(p: Proposal, harness: { specRevision?: string } = {}): string {
571
+ const lines: string[] = [
572
+ PROPOSAL_FENCE_OPEN,
573
+ blockLine("proposal_id", p.proposalId),
574
+ blockLine("step_anchor", p.stepAnchor),
575
+ ];
576
+ if (p.label) lines.push(`label: ${escapeYaml(p.label)}`);
577
+ lines.push(
578
+ `target: ${JSON.stringify(p.target)}`,
579
+ blockLine("server_url", p.serverUrl),
580
+ blockLine("history_id", p.historyId),
581
+ `inputs: ${JSON.stringify(p.inputs)}`,
582
+ `overrides: ${JSON.stringify(p.overrides)}`,
583
+ `predicate: ${JSON.stringify(p.predicate)}`,
584
+ `assertions: ${JSON.stringify(p.assertions)}`,
585
+ );
586
+ if (p.templateDigest) lines.push(blockLine("template_digest", p.templateDigest));
587
+ if (p.createdAt) lines.push(blockLine("created_at", p.createdAt));
588
+ if (harness.specRevision && /^[0-9a-f]{64}$/.test(harness.specRevision)) {
589
+ lines.push(`spec_revision: ${harness.specRevision}`);
590
+ }
591
+ lines.push("```");
592
+ return lines.join("\n") + "\n";
593
+ }
594
+
595
+ /** Every `loom-proposal` block, readable or not, in notebook order. */
596
+ export function findProposalBlocks(content: string): ProposalBlock[] {
597
+ const lines = content.split("\n");
598
+ const out: ProposalBlock[] = [];
599
+ for (const range of scanFencedBlocks(lines, "proposal")) {
600
+ const raw = rawInvocationFields(lines.slice(range.start + 1, range.end));
601
+ const get = (key: string): string | undefined =>
602
+ raw[key] === undefined ? undefined : key === "label" ? unescapeYaml(raw[key]) : raw[key];
603
+ const { proposal, errors } = parseProposalFields(get);
604
+ out.push({
605
+ proposalId: raw.proposal_id || null,
606
+ proposal,
607
+ errors,
608
+ start: range.start,
609
+ end: range.end,
610
+ });
611
+ }
612
+ return out;
613
+ }
614
+
615
+ /** What the notebook says about each proposal id, for the registry's checks. */
616
+ export function proposalSightings(content: string): ProposalSighting[] {
617
+ return findProposalBlocks(content)
618
+ .filter((b): b is ProposalBlock & { proposalId: string } => b.proposalId !== null)
619
+ .map((b) => ({ proposalId: b.proposalId, proposal: b.proposal }));
620
+ }
621
+
622
+ /**
623
+ * Append a new proposal block. Refuses an id the notebook already has: a
624
+ * proposal is written once, and a changed one is a new proposal.
625
+ */
626
+ export function appendProposalBlock(content: string, p: Proposal): string {
627
+ if (findProposalBlocks(content).some((b) => b.proposalId === p.proposalId)) {
628
+ throw new Error(`proposal ${p.proposalId} is already in the notebook`);
629
+ }
630
+ return appendBlock(content, renderProposalBlock(p).trimEnd().split("\n"));
631
+ }
632
+
633
+ /**
634
+ * Write `spec_revision` into the one block carrying this id, or take it out
635
+ * (`null`, after a revoke), touching no other line of it. Returns the content
636
+ * unchanged when there isn't exactly one unambiguous block -- the registry
637
+ * already holds the truth, and this is only the echo.
638
+ */
639
+ export function setProposalSpecRevision(
640
+ content: string,
641
+ proposalId: string,
642
+ specRevision: string | null,
643
+ ): string {
644
+ if (specRevision !== null && !/^[0-9a-f]{64}$/.test(specRevision)) return content;
645
+ const lines = content.split("\n");
646
+ const blocks = findProposalBlocks(content).filter((b) => b.proposalId === proposalId);
647
+ if (blocks.length !== 1 || !isUnambiguousRange(lines, blocks[0].start)) return content;
648
+ const { start, end } = blocks[0];
649
+ const body = lines.slice(start + 1, end).filter((l) => !/^spec_revision:/.test(l));
650
+ if (specRevision !== null) body.push(`spec_revision: ${specRevision}`);
651
+ return [...lines.slice(0, start + 1), ...body, ...lines.slice(end)].join("\n");
652
+ }
653
+
654
+ // ─────────────────────────────────────────────────────────────────────────────
655
+ // Session summary YAML blocks
656
+ // ─────────────────────────────────────────────────────────────────────────────
657
+
658
+ /**
659
+ * `loom-session` block, appended on `session_shutdown`. Pairs with the
660
+ * notebook's role as the durable record: when a Pi session dies mid-poll,
661
+ * a fresh session can read the most recent `loom-session` block to learn
662
+ * what was in flight and re-orient. `orphaned_active_steps` is 0 today
663
+ * (typed plan-step blocks don't exist yet); the field is here so the
664
+ * schema stays stable when that follow-up lands.
665
+ */
666
+ export interface SessionSummaryYaml {
667
+ id: string;
668
+ startedAt: string;
669
+ endedAt: string;
670
+ notebook: string;
671
+ orphanedActiveSteps: number;
672
+ }
673
+
674
+ const SESSION_FENCE_OPEN = notebookFenceOpen("session");
675
+ const SESSION_FENCE_CLOSE = "```";
676
+
677
+ export function renderSessionSummaryYaml(s: SessionSummaryYaml): string {
678
+ const lines: string[] = [
679
+ SESSION_FENCE_OPEN,
680
+ `id: ${s.id}`,
681
+ `started_at: ${s.startedAt}`,
682
+ `ended_at: ${s.endedAt}`,
683
+ `notebook: ${s.notebook}`,
684
+ `orphaned_active_steps: ${s.orphanedActiveSteps}`,
685
+ SESSION_FENCE_CLOSE,
686
+ ];
687
+ return lines.join("\n") + "\n";
688
+ }
689
+
690
+ /**
691
+ * Append a session summary block at the end of the notebook content. This is
692
+ * the low-level primitive used for a session id never seen before; callers
693
+ * that finalize a session should use `upsertSessionSummaryBlock`, which keys
694
+ * on `id` and routes here only for first-seen ids.
695
+ */
696
+ export function appendSessionSummaryBlock(content: string, s: SessionSummaryYaml): string {
697
+ const block = renderSessionSummaryYaml(s).trimEnd();
698
+ const trimmed = content.replace(/\s+$/, "");
699
+ const sep = trimmed.length > 0 ? "\n\n" : "";
700
+ return trimmed + sep + block + "\n";
701
+ }
702
+
703
+ /**
704
+ * Upsert a `loom-session` block keyed by `id`. A first-seen id appends a new
705
+ * block (chronological log of distinct sessions); a seen id collapses every
706
+ * block sharing that id, plus the new finalize, into a single merged block
707
+ * that spans the session id's full lifetime.
708
+ *
709
+ * Why upsert and not append (#260): Pi can hand back the *same* session id
710
+ * when an idle session is resumed, so a blind append wrote two blocks under
711
+ * one id and broke the id's role as a unique key. Collapsing keeps exactly
712
+ * one block per id -- and self-heals a notebook the old append path already
713
+ * left with duplicates.
714
+ *
715
+ * The merged block keeps the position of the id's first block, so blocks stay
716
+ * in first-seen order. That order can diverge from strict `ended_at` order if
717
+ * a non-latest session is resumed; no consumer relies on positional recency
718
+ * today, and keeping the slot avoids reshuffling the user's notebook.
719
+ */
720
+ export function upsertSessionSummaryBlock(content: string, s: SessionSummaryYaml): string {
721
+ const matching = findSessionSummaryBlockRanges(content).filter((r) => r.summary.id === s.id);
722
+ if (matching.length === 0) {
723
+ return appendSessionSummaryBlock(content, s);
724
+ }
725
+ const merged = matching.reduce((acc, r) => mergeSessionSummary(acc, r.summary), s);
726
+ const newBlock = renderSessionSummaryYaml(merged).trimEnd().split("\n");
727
+ return replaceNotebookBlocks(content, matching, newBlock);
728
+ }
729
+
730
+ /**
731
+ * Merge finalizes of the same session id into one record. Keep the earliest
732
+ * start and the latest end so the block spans the whole lifetime across
733
+ * resumes; carry the orphan count from whichever finalize ended later (the
734
+ * authoritative end state).
735
+ */
736
+ function mergeSessionSummary(
737
+ prev: SessionSummaryYaml,
738
+ next: SessionSummaryYaml,
739
+ ): SessionSummaryYaml {
740
+ const nextEndsLater = compareTimestamps(next.endedAt, prev.endedAt) >= 0;
741
+ return {
742
+ id: next.id,
743
+ startedAt:
744
+ compareTimestamps(next.startedAt, prev.startedAt) < 0 ? next.startedAt : prev.startedAt,
745
+ endedAt: nextEndsLater ? next.endedAt : prev.endedAt,
746
+ notebook: next.notebook,
747
+ orphanedActiveSteps: nextEndsLater ? next.orphanedActiveSteps : prev.orphanedActiveSteps,
748
+ };
749
+ }
750
+
751
+ // Order two timestamps. The shutdown writer always emits valid ISO-8601 UTC,
752
+ // which Date.parse compares correctly (including across offsets). Fall back to
753
+ // a lexical compare only if a hand-edited value won't parse, so the result is
754
+ // still deterministic rather than NaN-poisoned.
755
+ function compareTimestamps(a: string, b: string): number {
756
+ const na = Date.parse(a);
757
+ const nb = Date.parse(b);
758
+ if (!Number.isNaN(na) && !Number.isNaN(nb)) return na - nb;
759
+ return a < b ? -1 : a > b ? 1 : 0;
760
+ }
761
+
762
+ interface SessionSummaryBlockRange {
763
+ summary: SessionSummaryYaml;
764
+ start: number;
765
+ end: number;
766
+ }
767
+
768
+ function findSessionSummaryBlockRanges(content: string): SessionSummaryBlockRange[] {
769
+ const result: SessionSummaryBlockRange[] = [];
770
+ const lines = content.split("\n");
771
+ let i = 0;
772
+ while (i < lines.length) {
773
+ if (isNotebookFenceOpen(lines[i], "session")) {
774
+ const start = i;
775
+ let end = start + 1;
776
+ while (end < lines.length && lines[end].trim() !== SESSION_FENCE_CLOSE) {
777
+ end++;
778
+ }
779
+ const summary = parseSessionSummaryBlock(lines.slice(start + 1, end));
780
+ if (summary) result.push({ summary, start, end });
781
+ i = end + 1;
782
+ } else {
783
+ i++;
784
+ }
785
+ }
786
+ return result;
787
+ }
788
+
789
+ /**
790
+ * Find every `loom-session` block in the notebook content and parse each.
791
+ * Skips blocks that fail validation. Used by `session_start` to surface
792
+ * any orphaned-active state from the previous session.
793
+ */
794
+ export function findSessionSummaryBlocks(content: string): SessionSummaryYaml[] {
795
+ const result: SessionSummaryYaml[] = [];
796
+ const lines = content.split("\n");
797
+ let i = 0;
798
+ while (i < lines.length) {
799
+ if (isNotebookFenceOpen(lines[i], "session")) {
800
+ const start = i + 1;
801
+ let end = start;
802
+ while (end < lines.length && lines[end].trim() !== SESSION_FENCE_CLOSE) {
803
+ end++;
804
+ }
805
+ const parsed = parseSessionSummaryBlock(lines.slice(start, end));
806
+ if (parsed) result.push(parsed);
807
+ i = end + 1;
808
+ } else {
809
+ i++;
810
+ }
811
+ }
812
+ return result;
813
+ }
814
+
815
+ function parseSessionSummaryBlock(blockLines: string[]): SessionSummaryYaml | null {
816
+ const fields: Record<string, string> = {};
817
+ for (const line of blockLines) {
818
+ const m = line.match(/^([a-z_]+):\s*(.*)$/);
819
+ if (m) fields[m[1]] = unescapeYaml(m[2].trim());
820
+ }
821
+ if (!fields.id || !fields.started_at || !fields.ended_at || !fields.notebook) {
822
+ return null;
823
+ }
824
+ const orphaned = Number(fields.orphaned_active_steps);
825
+ return {
826
+ id: fields.id,
827
+ startedAt: fields.started_at,
828
+ endedAt: fields.ended_at,
829
+ notebook: fields.notebook,
830
+ orphanedActiveSteps: Number.isFinite(orphaned) ? orphaned : 0,
831
+ };
832
+ }
833
+
834
+ /**
835
+ * Quote a free-text value so it stays on one line.
836
+ *
837
+ * `JSON.stringify` rather than hand-rolled quoting: the old version escaped
838
+ * quotes but passed a newline straight through, so a label spanning two lines
839
+ * rendered as two block fields and the second one was read back as a field of
840
+ * the caller's choosing. A label is agent-supplied on both record tools, which
841
+ * made that a way to write provenance the writers refuse.
842
+ */
843
+ function escapeYaml(value: string): string {
844
+ // Quote if contains characters that would confuse the line parser.
845
+ if (/[:#"\\\u0000-\u001f\u007f-\u009f]/.test(value)) {
846
+ return JSON.stringify(value);
847
+ }
848
+ return value;
849
+ }
850
+
851
+ function unescapeYaml(value: string): string {
852
+ if (value.startsWith('"') && value.endsWith('"')) {
853
+ try {
854
+ return JSON.parse(value) as string;
855
+ } catch {
856
+ // Blocks written before escapeYaml used JSON quoting: quotes were
857
+ // escaped, a lone backslash was not, so JSON.parse can fail on a value
858
+ // that was perfectly readable under the old rule.
859
+ return value.slice(1, -1).replace(/\\"/g, '"');
860
+ }
861
+ }
862
+ return value;
863
+ }