@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.
- package/CHANGELOG.md +123 -0
- package/LICENSE +21 -0
- package/README.md +760 -5
- package/bin/legacy-mcp-config.js +363 -0
- package/bin/orbit.js +810 -0
- package/bin/pi-extension-path.d.ts +4 -0
- package/bin/pi-extension-path.js +44 -0
- package/bin/provider-auth.d.ts +22 -0
- package/bin/provider-auth.js +77 -0
- package/bin/thinking-pref.js +38 -0
- package/bin/update-check.d.ts +15 -0
- package/bin/update-check.js +137 -0
- package/extensions/cli-update/index.ts +32 -0
- package/extensions/orbit/activity-hooks.ts +141 -0
- package/extensions/orbit/activity.ts +116 -0
- package/extensions/orbit/agent-dir.ts +11 -0
- package/extensions/orbit/auto-resume.ts +352 -0
- package/extensions/orbit/config.ts +3 -0
- package/extensions/orbit/confusables.ts +1 -0
- package/extensions/orbit/context.ts +957 -0
- package/extensions/orbit/dashboard-commands.ts +191 -0
- package/extensions/orbit/dashboard-store.ts +473 -0
- package/extensions/orbit/dashboard-tools.ts +1185 -0
- package/extensions/orbit/empty-completion.ts +99 -0
- package/extensions/orbit/evidence-gate.ts +1276 -0
- package/extensions/orbit/evidence-override-command.ts +294 -0
- package/extensions/orbit/exec-guard/bash-risk.ts +1408 -0
- package/extensions/orbit/exec-guard/gate.ts +181 -0
- package/extensions/orbit/exec-guard/guardian-config.ts +54 -0
- package/extensions/orbit/exec-guard/index.ts +1 -0
- package/extensions/orbit/exec-guard/model-tier.ts +36 -0
- package/extensions/orbit/exec-guard/path-jail.ts +54 -0
- package/extensions/orbit/exec-guard/policy.ts +342 -0
- package/extensions/orbit/exec-guard/runtime-state.ts +15 -0
- package/extensions/orbit/exec-guard/sensitive-read.ts +525 -0
- package/extensions/orbit/exec-guard/types.ts +42 -0
- package/extensions/orbit/execution-commands.ts +134 -0
- package/extensions/orbit/feedback-command.ts +81 -0
- package/extensions/orbit/feedback.ts +96 -0
- package/extensions/orbit/galaxy-api.test.ts +122 -0
- package/extensions/orbit/galaxy-api.ts +606 -0
- package/extensions/orbit/galaxy-artifact-link-guidance.ts +5 -0
- package/extensions/orbit/galaxy-destructive-gate.ts +113 -0
- package/extensions/orbit/galaxy-enrich.ts +1108 -0
- package/extensions/orbit/galaxy-failure.ts +101 -0
- package/extensions/orbit/galaxy-fixture.ts +126 -0
- package/extensions/orbit/galaxy-history-guard.ts +233 -0
- package/extensions/orbit/galaxy-job-block.ts +347 -0
- package/extensions/orbit/galaxy-job-ops.ts +288 -0
- package/extensions/orbit/galaxy-live-source.ts +929 -0
- package/extensions/orbit/galaxy-markdown-adapter.ts +248 -0
- package/extensions/orbit/galaxy-op-policy.ts +332 -0
- package/extensions/orbit/galaxy-ops-tools.ts +326 -0
- package/extensions/orbit/galaxy-output-store.ts +80 -0
- package/extensions/orbit/galaxy-output.ts +346 -0
- package/extensions/orbit/galaxy-page-binding.ts +222 -0
- package/extensions/orbit/galaxy-page-markdown-guidance.ts +15 -0
- package/extensions/orbit/galaxy-page-ops.ts +159 -0
- package/extensions/orbit/galaxy-page-sync.test.ts +417 -0
- package/extensions/orbit/galaxy-page-sync.ts +318 -0
- package/extensions/orbit/galaxy-pages-api.ts +203 -0
- package/extensions/orbit/galaxy-pages-sync.ts +409 -0
- package/extensions/orbit/galaxy-poll-guard.ts +189 -0
- package/extensions/orbit/galaxy-poller.ts +706 -0
- package/extensions/orbit/galaxy-provenance.ts +623 -0
- package/extensions/orbit/galaxy-reconcile.ts +956 -0
- package/extensions/orbit/galaxy-recovery.ts +176 -0
- package/extensions/orbit/galaxy-repeat-guard.ts +55 -0
- package/extensions/orbit/galaxy-submission-capture.ts +707 -0
- package/extensions/orbit/galaxy-submission.ts +502 -0
- package/extensions/orbit/galaxy-tool-source.ts +78 -0
- package/extensions/orbit/galaxy-tool-wrap.ts +234 -0
- package/extensions/orbit/galaxy-udt-block.ts +127 -0
- package/extensions/orbit/galaxy-upload-tus.ts +334 -0
- package/extensions/orbit/galaxy-upload.ts +300 -0
- package/extensions/orbit/galaxy-version-capture.ts +65 -0
- package/extensions/orbit/git.ts +173 -0
- package/extensions/orbit/harness-block-fields.ts +597 -0
- package/extensions/orbit/index.ts +593 -0
- package/extensions/orbit/init-gate.ts +284 -0
- package/extensions/orbit/instructions-command.ts +136 -0
- package/extensions/orbit/invocation-failure-hint.ts +57 -0
- package/extensions/orbit/iwc-plan-check.ts +94 -0
- package/extensions/orbit/lesson-command.ts +303 -0
- package/extensions/orbit/lesson-hint.ts +245 -0
- package/extensions/orbit/lesson-nudge.ts +106 -0
- package/extensions/orbit/lessons/compose.ts +177 -0
- package/extensions/orbit/lessons/counters.ts +120 -0
- package/extensions/orbit/lessons/enabled.ts +116 -0
- package/extensions/orbit/lessons/matcher.ts +346 -0
- package/extensions/orbit/lessons/paths.ts +286 -0
- package/extensions/orbit/lessons/pi-event-contract.ts +105 -0
- package/extensions/orbit/lessons/propose-replay.ts +103 -0
- package/extensions/orbit/lessons/propose-tool.ts +222 -0
- package/extensions/orbit/lessons/propose.ts +502 -0
- package/extensions/orbit/lessons/replay.ts +89 -0
- package/extensions/orbit/lessons/reproduction-index.ts +83 -0
- package/extensions/orbit/lessons/search-tool.ts +99 -0
- package/extensions/orbit/lessons/search.ts +136 -0
- package/extensions/orbit/lessons/store.ts +415 -0
- package/extensions/orbit/lessons/text.ts +36 -0
- package/extensions/orbit/lessons/types.ts +69 -0
- package/extensions/orbit/lessons/user-dir.ts +24 -0
- package/extensions/orbit/lessons/user-lesson.ts +88 -0
- package/extensions/orbit/lessons/wrapper.ts +61 -0
- package/extensions/orbit/lessons-command.ts +147 -0
- package/extensions/orbit/local-exec.ts +39 -0
- package/extensions/orbit/mcp-output.ts +179 -0
- package/extensions/orbit/mcp-servers.test.ts +24 -0
- package/extensions/orbit/mcp-servers.ts +25 -0
- package/extensions/orbit/model-script.ts +351 -0
- package/extensions/orbit/notebook-anchors.ts +342 -0
- package/extensions/orbit/notebook-writer.ts +863 -0
- package/extensions/orbit/observation-allowlists.ts +1109 -0
- package/extensions/orbit/observation-replay.ts +148 -0
- package/extensions/orbit/observation-triggers.ts +563 -0
- package/extensions/orbit/observation-ui.ts +175 -0
- package/extensions/orbit/observations-command.ts +421 -0
- package/extensions/orbit/observations-config.ts +200 -0
- package/extensions/orbit/observations.ts +1381 -0
- package/extensions/orbit/profiles.ts +270 -0
- package/extensions/orbit/progress-updates.ts +95 -0
- package/extensions/orbit/proposal-commands.ts +882 -0
- package/extensions/orbit/provenance-fs-file.ts +59 -0
- package/extensions/orbit/provenance-fs.ts +32 -0
- package/extensions/orbit/provenance-memory-fs.ts +59 -0
- package/extensions/orbit/record-store-file.ts +177 -0
- package/extensions/orbit/record-store-page.ts +77 -0
- package/extensions/orbit/record-store-seam.ts +47 -0
- package/extensions/orbit/record-store.ts +149 -0
- package/extensions/orbit/registry-carrier.ts +107 -0
- package/extensions/orbit/registry-eligibility.ts +95 -0
- package/extensions/orbit/registry-evaluation.ts +403 -0
- package/extensions/orbit/registry-evaluator.ts +519 -0
- package/extensions/orbit/registry-import.ts +91 -0
- package/extensions/orbit/registry-lock.ts +221 -0
- package/extensions/orbit/registry-memory-fs.ts +42 -0
- package/extensions/orbit/registry-page-carrier.ts +103 -0
- package/extensions/orbit/registry-proposal.ts +1348 -0
- package/extensions/orbit/registry-runtime.ts +234 -0
- package/extensions/orbit/registry-schema.ts +735 -0
- package/extensions/orbit/registry-submitter.ts +59 -0
- package/extensions/orbit/registry-templates.ts +346 -0
- package/extensions/orbit/registry.ts +476 -0
- package/extensions/orbit/sandbox/index.ts +120 -0
- package/extensions/orbit/sandbox/sandbox-bash.ts +93 -0
- package/extensions/orbit/sandbox/sandbox-config.ts +87 -0
- package/extensions/orbit/sandbox/sandbox-init-message.ts +28 -0
- package/extensions/orbit/secret-redaction.ts +247 -0
- package/extensions/orbit/session-index/cwd.ts +18 -0
- package/extensions/orbit/session-index/db.ts +56 -0
- package/extensions/orbit/session-index/indexer.ts +154 -0
- package/extensions/orbit/session-index/is-enabled.ts +17 -0
- package/extensions/orbit/session-index/parse.ts +254 -0
- package/extensions/orbit/session-index/query.ts +209 -0
- package/extensions/orbit/session-index/schema.ts +72 -0
- package/extensions/orbit/session-index/tools.ts +152 -0
- package/extensions/orbit/session-lifecycle.ts +321 -0
- package/extensions/orbit/skill-triggers.ts +159 -0
- package/extensions/orbit/skills-command.ts +48 -0
- package/extensions/orbit/skills-discovery.ts +435 -0
- package/extensions/orbit/skills.ts +84 -0
- package/extensions/orbit/sra-import-gate.ts +77 -0
- package/extensions/orbit/state.ts +265 -0
- package/extensions/orbit/submission-replay.ts +147 -0
- package/extensions/orbit/sync-command.ts +157 -0
- package/extensions/orbit/teams/critic-parser.ts +93 -0
- package/extensions/orbit/teams/dispatcher.ts +210 -0
- package/extensions/orbit/teams/is-enabled.ts +22 -0
- package/extensions/orbit/teams/tool.ts +187 -0
- package/extensions/orbit/teams/types.ts +57 -0
- package/extensions/orbit/teams/validate.ts +42 -0
- package/extensions/orbit/tester-id-command.ts +133 -0
- package/extensions/orbit/tools-sync.ts +279 -0
- package/extensions/orbit/tools.ts +1300 -0
- package/extensions/orbit/types.ts +41 -0
- package/extensions/orbit/ui-bridge.ts +55 -0
- package/extensions/orbit/ulid.ts +60 -0
- package/extensions/orbit/user-instructions.ts +388 -0
- package/extensions/orbit/vendor/skills/_catalog.json +68 -0
- package/extensions/orbit/vendor/skills/_manifest.json +1106 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/SKILL.md +112 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_provenance.json +218 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_required_tools.json +33 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/add.json +50 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-extract.json +51 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-next-step.json +42 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-validate.json +140 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/list.json +32 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/notes/galaxy-tool-job-failure-reference.md +137 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/notes/open-requirements-ledger.md +173 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/schemas/galaxy-tool-summary.schema.json +175 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/schemas/galaxy-workflow-draft.schema.json +2727 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/SKILL.md +82 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_provenance.json +133 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_required_tools.json +16 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/cli/draft-validate.json +140 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/notes/galaxy-workflow-draft-format.md +135 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/notes/open-requirements-ledger.md +173 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/schemas/galaxy-workflow-draft.schema.json +2727 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/SKILL.md +84 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/_provenance.json +166 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/_verify.json +29 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/galaxy-workflow-testability-design.md +136 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/iwc-test-data-conventions.md +308 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/planemo-asserts-idioms.md +246 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/planemo-workflow-test-architecture.md +151 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/galaxy-workflow-test-plan.schema.json +552 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/summary-galaxy-workflow.schema.json +687 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/tests-format.schema.json +7328 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/SKILL.md +127 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_provenance.json +198 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_required_tools.json +16 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/cli/convert.json +123 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/galaxy-data-flow-draft-contract.md +129 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/iwc-test-data-conventions.md +308 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/open-requirements-ledger.md +173 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/SKILL.md +73 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/_provenance.json +160 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-collection-semantics.md +43 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-collection-semantics.yml +1250 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-tool-job-failure-reference.md +137 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/nextflow-operators-to-galaxy-collection-recipes.md +98 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/planemo-asserts-idioms.md +246 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/planemo-workflow-test-architecture.md +151 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/SKILL.md +174 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/_provenance.json +130 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/_required_tools.json +18 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/_verify.json +17 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-revisions.json +48 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-search.json +95 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-versions.json +39 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/references/notes/component-tool-shed-search.md +230 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/references/schemas/galaxy-tool-discovery.schema.json +231 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/SKILL.md +93 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_provenance.json +201 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_required_tools.json +28 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_verify.json +17 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/cli/planemo.md +44 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/cli/validate-tests.json +40 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/galaxy-workflow-testability-design.md +136 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/iwc-test-data-conventions.md +308 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/planemo-asserts-idioms.md +246 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/planemo-workflow-test-architecture.md +151 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/schemas/galaxy-workflow-test-plan.schema.json +552 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/schemas/tests-format.schema.json +7328 -0
- package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/SKILL.md +81 -0
- package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/_provenance.json +93 -0
- package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/_verify.json +17 -0
- package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/references/notes/open-requirements-ledger.md +173 -0
- package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/references/schemas/summary-galaxy-workflow.schema.json +687 -0
- package/extensions/orbit/vendor/skills/pipeline-update-interview-to-galaxy/SKILL.md +81 -0
- package/extensions/orbit/vendor/skills/pipeline-update-interview-to-galaxy/_assembly.json +23 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/SKILL.md +79 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/_provenance.json +125 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/_required_tools.json +28 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/cli/planemo.md +44 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/cli/validate-tests.json +40 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/planemo-asserts-idioms.md +246 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/planemo-workflow-test-architecture.md +151 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/schemas/tests-format.schema.json +7328 -0
- package/extensions/orbit/vendor/skills/skills/LICENSE +21 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/README.md +39 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/SKILL.md +892 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/api-patterns.md +927 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/apply-rules.md +1482 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/test-patterns.md +996 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/tools.md +609 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-data-libraries/SKILL.md +170 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/README.md +162 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/SKILL.md +99 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/README.md +119 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/tool-checking.md +419 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/workflow-testing.md +617 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/galaxy-integration.md +614 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/SKILL.md +144 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/average_col3.ipynb +68 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/extract_sample_metadata.ipynb +190 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/variant_annotation.ipynb +816 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/vcp_variant_map.ipynb +443 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/scripts/README.md +165 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/scripts/galaxy_tool_checker.py +521 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/SKILL.md +128 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/gotchas.md +135 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/history-access.md +132 -0
- package/extensions/orbit/vendor/skills/skills/reproduciblify/SKILL.md +146 -0
- package/extensions/orbit/vendor/skills/skills/reproduciblify/references/directives.yml +496 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/SKILL.md +245 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/01-minimal-echo.yml +27 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/02-single-data-in-out.yml +27 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/03-multiple-data-js.yml +28 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/04-select-integer-boolean.yml +41 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/05-resource-requirements.yml +25 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/06-biocontainer-tool.yml +38 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/07-inline-python-script.yml +56 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/08-configfile-script.yml +74 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/references/common-mistakes.md +76 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/references/schema-reference.md +155 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/references/templating.md +196 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/scripts/validate.py +109 -0
- package/extensions/orbit/vendor/skills/skills/workflow-reports/README.md +71 -0
- package/extensions/orbit/vendor/skills/skills/workflow-reports/SKILL.md +190 -0
- package/extensions/orbit/vendor/skills/skills/workflow-reports/examples/histology-staining.md +145 -0
- package/extensions/orbit/vendor/skills/skills/workflow-reports/examples/tissue-microarray-analysis.md +176 -0
- package/extensions/orbit/vendor/skills/skills/workflow-reports/references/directives.md +415 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/SKILL.md +98 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_provenance.json +93 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_required_tools.json +28 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_verify.json +17 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/convert.json +123 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/foundry.md +25 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/validate.json +142 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/schemas/summary-galaxy-workflow.schema.json +687 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/SKILL.md +74 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_provenance.json +71 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_required_tools.json +16 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/cli/validate.json +142 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/notes/galaxy-workflow-testability-design.md +136 -0
- package/extensions/orbit/vendor-skills.ts +283 -0
- package/extensions/orbit/workspace-state-dir.ts +46 -0
- package/extensions/orbit-handoff/index.ts +73 -0
- package/extensions/orbit-handoff/orbit-launcher.ts +142 -0
- package/extensions/whats-new/index.ts +98 -0
- package/lessons/LICENSE +17 -0
- package/lessons/README.md +117 -0
- package/lessons/SCHEMA.md +187 -0
- package/lessons/build-snapshot.mjs +311 -0
- package/lessons/data/downloaded-file-is-not-what-its-extension-says.md +61 -0
- package/lessons/galaxy-api/403-history-is-a-hard-stop.md +57 -0
- package/lessons/galaxy-api/collection-into-single-dataset-input.md +61 -0
- package/lessons/galaxy-api/connectedvalue-in-command-line.md +62 -0
- package/lessons/galaxy-api/hid-is-not-an-id.md +59 -0
- package/lessons/galaxy-api/invoke-workflow-inputs-not-params.md +60 -0
- package/lessons/galaxy-api/repeat-param-pipe-keys.md +63 -0
- package/lessons/galaxy-api/run-tool-returns-on-submit.md +61 -0
- package/lessons/galaxy-tools/reference-index-not-on-server.md +62 -0
- package/lessons/log.md +42 -0
- package/lessons/reproduction/input-population-mismatch.md +59 -0
- package/lessons/reproduction/methods-text-vs-executed-parameters.md +62 -0
- package/lessons/snapshot.json +1004 -0
- package/lessons/stats/de-contrast-direction-and-sample-labels.md +63 -0
- package/lessons/stats/na-coerced-to-zero-in-filters.md +59 -0
- package/lessons/validate.mjs +1219 -0
- package/package.json +103 -5
- package/shared/agent-prompt.d.ts +8 -0
- package/shared/agent-prompt.js +15 -0
- package/shared/approval-prompt.d.ts +4 -0
- package/shared/approval-prompt.js +61 -0
- package/shared/brain-env.d.ts +14 -0
- package/shared/brain-env.js +108 -0
- package/shared/brain-exit.d.ts +20 -0
- package/shared/brain-exit.js +149 -0
- package/shared/capability-profile.d.ts +16 -0
- package/shared/capability-profile.js +75 -0
- package/shared/config-source.d.ts +13 -0
- package/shared/config-source.js +17 -0
- package/shared/confusables.d.ts +3 -0
- package/shared/confusables.js +93 -0
- package/shared/custom-provider.d.ts +45 -0
- package/shared/custom-provider.js +169 -0
- package/shared/dashboard-contract.d.ts +123 -0
- package/shared/dashboard-contract.js +560 -0
- package/shared/dashboard-layout-store.d.ts +44 -0
- package/shared/dashboard-layout-store.js +281 -0
- package/shared/feedback-contract.d.ts +61 -0
- package/shared/feedback-contract.js +146 -0
- package/shared/galaxy-artifact-links.d.ts +30 -0
- package/shared/galaxy-artifact-links.js +166 -0
- package/shared/galaxy-credentials.d.ts +26 -0
- package/shared/galaxy-credentials.js +71 -0
- package/shared/galaxy-destructive.d.ts +21 -0
- package/shared/galaxy-destructive.js +269 -0
- package/shared/galaxy-guidance.d.ts +2 -0
- package/shared/galaxy-guidance.js +53 -0
- package/shared/galaxy-live-contract.d.ts +92 -0
- package/shared/galaxy-live-contract.js +180 -0
- package/shared/galaxy-page-cells.d.ts +2 -0
- package/shared/galaxy-page-cells.js +22 -0
- package/shared/galaxy-tool-family.d.ts +5 -0
- package/shared/galaxy-tool-family.js +27 -0
- package/shared/galaxy-url.d.ts +1 -0
- package/shared/galaxy-url.js +48 -0
- package/shared/gtn.d.ts +31 -0
- package/shared/gtn.js +253 -0
- package/shared/handled-prompt.d.ts +3 -0
- package/shared/handled-prompt.js +26 -0
- package/shared/known-provider.d.ts +11 -0
- package/shared/known-provider.js +89 -0
- package/shared/lesson-hint-marker.d.ts +7 -0
- package/shared/lesson-hint-marker.js +32 -0
- package/shared/lesson-rules.d.ts +150 -0
- package/shared/lesson-rules.js +203 -0
- package/shared/malformed-object-ids.d.ts +1 -0
- package/shared/malformed-object-ids.js +37 -0
- package/shared/markdown-sections.d.ts +10 -0
- package/shared/markdown-sections.js +118 -0
- package/shared/notebook-fences.d.ts +12 -0
- package/shared/notebook-fences.js +53 -0
- package/shared/observation-contract.d.ts +91 -0
- package/shared/observation-contract.js +544 -0
- package/shared/orbit-config.d.ts +226 -0
- package/shared/orbit-config.js +189 -0
- package/shared/orbit-env.d.ts +13 -0
- package/shared/orbit-env.js +124 -0
- package/shared/orbit-shell-contract.d.ts +74 -0
- package/shared/orbit-shell-contract.js +30 -0
- package/shared/page-content-hash.d.ts +1 -0
- package/shared/page-content-hash.js +13 -0
- package/shared/pi-default-model.d.ts +7 -0
- package/shared/pi-default-model.js +41 -0
- package/shared/prompt-blocks.d.ts +87 -0
- package/shared/prompt-blocks.js +898 -0
- package/shared/provider-auth-caps.d.ts +22 -0
- package/shared/provider-auth-caps.js +57 -0
- package/shared/random.d.ts +2 -0
- package/shared/random.js +15 -0
- package/shared/record-excerpt.d.ts +4 -0
- package/shared/record-excerpt.js +16 -0
- package/shared/redirect-guard.d.ts +36 -0
- package/shared/redirect-guard.js +235 -0
- package/shared/skills-pin.d.ts +8 -0
- package/shared/skills-pin.js +9 -0
- package/shared/skills-router.d.ts +17 -0
- package/shared/skills-router.js +63 -0
- package/shared/sra-import.d.ts +34 -0
- package/shared/sra-import.js +206 -0
- package/shared/state-dir.d.ts +21 -0
- package/shared/state-dir.js +240 -0
- package/shared/team-dispatch-contract.d.ts +28 -0
- package/shared/team-dispatch-contract.js +1 -0
- package/shared/truncate-middle.d.ts +11 -0
- package/shared/truncate-middle.js +68 -0
- package/shared/version-compare.d.ts +10 -0
- package/shared/version-compare.js +74 -0
- package/shared/whats-new.d.ts +31 -0
- package/shared/whats-new.js +83 -0
- package/shared/wsl.d.ts +10 -0
- 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
|
+
}
|