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