@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,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
|
+
}
|