@gajae-code/coding-agent 0.11.1 → 0.11.3
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 +64 -0
- package/dist/types/cli/config-cli.d.ts +11 -1
- package/dist/types/cli/launch-disposition.d.ts +23 -0
- package/dist/types/cli/notify-cli.d.ts +1 -0
- package/dist/types/cli/session-picker.d.ts +2 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/commands/mcp-serve.d.ts +33 -0
- package/dist/types/commands/mcp.d.ts +1 -1
- package/dist/types/commands/notify.d.ts +0 -2
- package/dist/types/commands/sdk.d.ts +1 -1
- package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
- package/dist/types/config/file-lock.d.ts +14 -4
- package/dist/types/config/keybindings.d.ts +42 -1
- package/dist/types/config/model-auth.d.ts +3 -0
- package/dist/types/config/model-bindings-applier.d.ts +13 -0
- package/dist/types/config/model-discovery-manager.d.ts +49 -0
- package/dist/types/config/model-equivalence.d.ts +20 -0
- package/dist/types/config/model-registry.d.ts +16 -15
- package/dist/types/config/model-resolver.d.ts +28 -18
- package/dist/types/config/models-config-schema.d.ts +11 -0
- package/dist/types/config/settings-schema.d.ts +141 -11
- package/dist/types/config/settings.d.ts +14 -11
- package/dist/types/coordinator-mcp/policy.d.ts +5 -0
- package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
- package/dist/types/coordinator-mcp/server.d.ts +4 -0
- package/dist/types/discovery/helpers.d.ts +0 -6
- package/dist/types/edit/index.d.ts +3 -0
- package/dist/types/eval/py/env.d.ts +9 -0
- package/dist/types/extensibility/extensions/loader.d.ts +2 -1
- package/dist/types/extensibility/extensions/runner.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +34 -3
- package/dist/types/extensibility/hooks/runner.d.ts +1 -1
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
- package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
- package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
- package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
- package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
- package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
- package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
- package/dist/types/gjc-runtime/team-store.d.ts +160 -0
- package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
- package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
- package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/goals/state.d.ts +8 -0
- package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
- package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
- package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
- package/dist/types/hindsight/state.d.ts +11 -4
- package/dist/types/lsp/lspmux.d.ts +5 -3
- package/dist/types/lsp/path-trust.d.ts +1 -0
- package/dist/types/main.d.ts +4 -2
- package/dist/types/memories/storage.d.ts +1 -0
- package/dist/types/modes/action-registry.d.ts +29 -0
- package/dist/types/modes/components/chrome.d.ts +14 -0
- package/dist/types/modes/components/command-palette.d.ts +27 -0
- package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
- package/dist/types/modes/components/index.d.ts +2 -1
- package/dist/types/modes/components/pet-selector.d.ts +1 -1
- package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
- package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
- package/dist/types/modes/components/queue-pane.d.ts +12 -0
- package/dist/types/modes/components/read-tool-group.d.ts +6 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
- package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
- package/dist/types/modes/components/settings-selector.d.ts +1 -1
- package/dist/types/modes/components/show-images-selector.d.ts +1 -1
- package/dist/types/modes/components/status-line/types.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +1 -94
- package/dist/types/modes/components/tasks-pane.d.ts +15 -0
- package/dist/types/modes/components/theme-selector.d.ts +1 -1
- package/dist/types/modes/components/tool-execution.d.ts +15 -0
- package/dist/types/modes/components/tool-status-header.d.ts +111 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
- package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
- package/dist/types/modes/controllers/input-controller.d.ts +8 -1
- package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
- package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
- package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
- package/dist/types/modes/interactive-mode.d.ts +19 -17
- package/dist/types/modes/jobs-observer.d.ts +2 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +20 -2
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
- package/dist/types/modes/tasks-aggregator.d.ts +39 -0
- package/dist/types/modes/transcript-item-registry.d.ts +60 -0
- package/dist/types/modes/types.d.ts +20 -13
- package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/sdk/broker/broker.d.ts +78 -12
- package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
- package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
- package/dist/types/sdk/broker/session-index.d.ts +0 -1
- package/dist/types/sdk/broker/state-version.d.ts +2 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
- package/dist/types/sdk/bus/config-commands.d.ts +1 -1
- package/dist/types/sdk/bus/config.d.ts +11 -0
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/html-format.d.ts +3 -2
- package/dist/types/sdk/bus/index.d.ts +29 -0
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/notification-service.d.ts +20 -0
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
- package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
- package/dist/types/sdk/bus/rich-document.d.ts +7 -0
- package/dist/types/sdk/bus/rich-render.d.ts +18 -41
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +46 -3
- package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/session-directory.d.ts +56 -0
- package/dist/types/sdk/session.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +82 -47
- package/dist/types/session/agent-storage.d.ts +7 -0
- package/dist/types/session/fallback-chain-controller.d.ts +10 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
- package/dist/types/session/messages.d.ts +16 -0
- package/dist/types/session/session-manager-internal.d.ts +14 -0
- package/dist/types/session/session-manager.d.ts +82 -12
- package/dist/types/session/session-storage.d.ts +43 -3
- package/dist/types/session/volatile-context-pruning.d.ts +15 -0
- package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
- package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
- package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
- package/dist/types/system-prompt.d.ts +2 -11
- package/dist/types/task/executor.d.ts +3 -1
- package/dist/types/thinking.d.ts +7 -0
- package/dist/types/tool-discovery/tool-index.d.ts +1 -50
- package/dist/types/tools/ask.d.ts +64 -2
- package/dist/types/tools/bash.d.ts +3 -0
- package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
- package/dist/types/tools/cron.d.ts +2 -0
- package/dist/types/tools/fetch.d.ts +1 -0
- package/dist/types/tools/index.d.ts +30 -15
- package/dist/types/tools/output-meta.d.ts +1 -0
- package/dist/types/tools/read.d.ts +5 -0
- package/dist/types/tools/search-tool-bm25.d.ts +3 -4
- package/dist/types/tools/subagent.d.ts +1 -0
- package/dist/types/utils/event-bus.d.ts +1 -1
- package/dist/types/utils/file-mentions.d.ts +3 -3
- package/dist/types/web/search/index.d.ts +3 -1
- package/package.json +21 -9
- package/scripts/build-sdk-package-smoke.ts +34 -1
- package/scripts/generate-hotkeys-docs.ts +74 -0
- package/scripts/generate-sdk-operation-inventory.ts +64 -20
- package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
- package/src/async/job-manager.ts +49 -6
- package/src/cli/config-cli.ts +75 -2
- package/src/cli/launch-disposition.ts +42 -0
- package/src/cli/notify-cli.ts +92 -26
- package/src/cli/session-picker.ts +20 -3
- package/src/cli/setup-cli.ts +1 -1
- package/src/cli.ts +1 -1
- package/src/commands/config.ts +1 -1
- package/src/commands/harness.ts +34 -106
- package/src/commands/launch.ts +6 -4
- package/src/commands/mcp-serve.ts +85 -8
- package/src/commands/mcp.ts +2 -2
- package/src/commands/notify.ts +8 -4
- package/src/commands/sdk.ts +61 -40
- package/src/commands/ultragoal.ts +4 -1
- package/src/config/atomic-yaml-patch.ts +80 -13
- package/src/config/file-lock.ts +105 -43
- package/src/config/keybindings.ts +64 -20
- package/src/config/model-auth.ts +9 -0
- package/src/config/model-bindings-applier.ts +139 -0
- package/src/config/model-discovery-manager.ts +224 -0
- package/src/config/model-equivalence.ts +66 -0
- package/src/config/model-registry.ts +169 -302
- package/src/config/model-resolver.ts +361 -369
- package/src/config/models-config-schema.ts +5 -4
- package/src/config/settings-schema.ts +214 -12
- package/src/config/settings.ts +458 -482
- package/src/coordinator-mcp/policy.ts +76 -11
- package/src/coordinator-mcp/question-gate-codec.ts +274 -0
- package/src/coordinator-mcp/question-state.ts +778 -0
- package/src/coordinator-mcp/server.ts +1702 -238
- package/src/debug/index.ts +1 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
- package/src/defaults/gjc/skills/team/SKILL.md +16 -0
- package/src/discovery/helpers.ts +1 -50
- package/src/edit/index.ts +64 -0
- package/src/eval/py/env.ts +26 -0
- package/src/eval/py/kernel.ts +5 -3
- package/src/extensibility/extensions/loader.ts +9 -1
- package/src/extensibility/extensions/runner.ts +12 -3
- package/src/extensibility/extensions/types.ts +39 -1
- package/src/extensibility/gjc-plugins/installer.ts +16 -16
- package/src/extensibility/hooks/runner.ts +2 -2
- package/src/extensibility/runtime-skill-discovery.ts +33 -13
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
- package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
- package/src/gjc-runtime/deep-interview-state.ts +317 -5
- package/src/gjc-runtime/goal-mode-request.ts +55 -8
- package/src/gjc-runtime/launch-tmux.ts +2 -5
- package/src/gjc-runtime/launch-worktree.ts +5 -5
- package/src/gjc-runtime/linux-proc.ts +71 -0
- package/src/gjc-runtime/ralplan-runtime.ts +74 -90
- package/src/gjc-runtime/session-resolution.ts +18 -3
- package/src/gjc-runtime/session-state-sidecar.ts +6 -107
- package/src/gjc-runtime/state-migrations.ts +1 -1
- package/src/gjc-runtime/state-runtime.ts +327 -249
- package/src/gjc-runtime/state-writer.ts +36 -40
- package/src/gjc-runtime/team-launch.ts +319 -0
- package/src/gjc-runtime/team-notify.ts +326 -0
- package/src/gjc-runtime/team-runtime.ts +1099 -1482
- package/src/gjc-runtime/team-store.ts +828 -0
- package/src/gjc-runtime/team-workers.ts +962 -0
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
- package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
- package/src/gjc-runtime/tmux-sessions.ts +4 -21
- package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
- package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
- package/src/gjc-runtime/ultragoal-guard.ts +29 -5
- package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +84 -1311
- package/src/gjc-runtime/workflow-cli-common.ts +30 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +18 -0
- package/src/goals/continuation-timeout-guard.ts +79 -0
- package/src/goals/runtime.ts +7 -5
- package/src/goals/state.ts +18 -0
- package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
- package/src/harness-control-plane/phase-rollup.ts +4 -7
- package/src/harness-control-plane/receipt-ingest.ts +5 -0
- package/src/harness-control-plane/receipt-spool.ts +51 -7
- package/src/harness-control-plane/session-lease.ts +12 -130
- package/src/hindsight/backend.ts +26 -27
- package/src/hindsight/state.ts +101 -32
- package/src/hooks/skill-state.ts +17 -11
- package/src/internal-urls/docs-index.generated.ts +21 -18
- package/src/lsp/client.ts +1 -1
- package/src/lsp/config.ts +145 -26
- package/src/lsp/index.ts +1 -1
- package/src/lsp/lspmux.ts +39 -10
- package/src/lsp/path-trust.ts +81 -0
- package/src/main.ts +127 -30
- package/src/memories/index.ts +1 -0
- package/src/memories/storage.ts +6 -2
- package/src/modes/action-registry.ts +165 -0
- package/src/modes/components/chrome.ts +40 -0
- package/src/modes/components/command-palette.ts +168 -0
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/custom-provider-wizard.ts +51 -14
- package/src/modes/components/index.ts +2 -1
- package/src/modes/components/pet-selector.ts +11 -17
- package/src/modes/components/plan-preview-overlay.ts +295 -0
- package/src/modes/components/queue-mode-selector.ts +10 -27
- package/src/modes/components/queue-pane.ts +79 -0
- package/src/modes/components/read-tool-group.ts +13 -0
- package/src/modes/components/session-observer-overlay.ts +207 -772
- package/src/modes/components/sessions-dashboard.ts +181 -0
- package/src/modes/components/settings-selector.ts +1 -1
- package/src/modes/components/show-images-selector.ts +10 -24
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +1 -969
- package/src/modes/components/tasks-pane.ts +73 -0
- package/src/modes/components/theme-selector.ts +11 -31
- package/src/modes/components/tool-execution.ts +22 -0
- package/src/modes/components/tool-status-header.ts +1056 -0
- package/src/modes/components/transcript-viewer-overlay.ts +384 -0
- package/src/modes/controllers/btw-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +24 -1
- package/src/modes/controllers/goal-mode-controller.ts +474 -0
- package/src/modes/controllers/input-controller.ts +764 -229
- package/src/modes/controllers/mode-gate.ts +24 -0
- package/src/modes/controllers/plan-mode-controller.ts +517 -0
- package/src/modes/controllers/selector-controller.ts +225 -10
- package/src/modes/controllers/stt-controller.ts +125 -0
- package/src/modes/interactive-mode.ts +289 -1150
- package/src/modes/jobs-observer.ts +3 -0
- package/src/modes/prompt-action-autocomplete.ts +7 -0
- package/src/modes/runtime-init.ts +8 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +71 -16
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
- package/src/modes/tasks-aggregator.ts +199 -0
- package/src/modes/transcript-item-registry.ts +229 -0
- package/src/modes/types.ts +45 -18
- package/src/modes/utils/ui-helpers.ts +17 -9
- package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
- package/src/prompts/agent-fragments/restricted-bash.md +1 -0
- package/src/prompts/agents/architect.md +2 -8
- package/src/prompts/agents/critic.md +2 -8
- package/src/prompts/agents/executor.md +3 -0
- package/src/prompts/agents/planner.md +4 -12
- package/src/prompts/system/custom-system-prompt.md +1 -23
- package/src/prompts/system/plan-mode-approved.md +8 -0
- package/src/prompts/system/system-prompt.md +17 -96
- package/src/prompts/tools/bash.md +4 -4
- package/src/prompts/tools/browser.md +15 -52
- package/src/prompts/tools/find.md +2 -2
- package/src/prompts/tools/hashline.md +13 -55
- package/src/prompts/tools/read.md +3 -6
- package/src/prompts/tools/search-tool-bm25.md +0 -2
- package/src/prompts/tools/search.md +2 -4
- package/src/prompts/tools/skill-discovery.md +1 -2
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-mcp/manager.ts +1 -1
- package/src/sdk/broker/broker.ts +206 -78
- package/src/sdk/broker/discovery.ts +1 -1
- package/src/sdk/broker/ensure.ts +3 -1
- package/src/sdk/broker/lifecycle-codec.ts +11 -0
- package/src/sdk/broker/lifecycle-ledger.ts +371 -74
- package/src/sdk/broker/lifecycle.ts +1399 -142
- package/src/sdk/broker/process-incarnation.ts +5 -13
- package/src/sdk/broker/session-index.ts +145 -56
- package/src/sdk/broker/state-version.ts +21 -0
- package/src/sdk/broker/transport.ts +1 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -0
- package/src/sdk/bus/chat-daemon-control.ts +8 -28
- package/src/sdk/bus/chat-effect-journal.ts +10 -2
- package/src/sdk/bus/config-commands.ts +5 -4
- package/src/sdk/bus/config.ts +98 -0
- package/src/sdk/bus/conversation-store.ts +5 -29
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/html-format.ts +20 -3
- package/src/sdk/bus/index.ts +782 -94
- package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
- package/src/sdk/bus/natives-augment.d.ts +26 -1
- package/src/sdk/bus/notification-service.ts +168 -20
- package/src/sdk/bus/rate-limit-pool.ts +58 -14
- package/src/sdk/bus/recent-activity.ts +188 -74
- package/src/sdk/bus/rich-document.ts +258 -0
- package/src/sdk/bus/rich-render.ts +62 -70
- package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
- package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
- package/src/sdk/bus/telegram-daemon.ts +1389 -237
- package/src/sdk/bus/threaded-inbound.ts +8 -3
- package/src/sdk/bus/threaded-render.ts +57 -1
- package/src/sdk/bus/topic-registry.ts +120 -13
- package/src/sdk/host/control/dispatch.ts +14 -2
- package/src/sdk/host/control/operations.ts +6 -1
- package/src/sdk/host/host.ts +44 -2
- package/src/sdk/host/query/handlers.ts +20 -22
- package/src/sdk/host/query/revision-store.ts +80 -57
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
- package/src/sdk/session-directory.ts +141 -0
- package/src/sdk/session.ts +462 -480
- package/src/session/agent-session.ts +1700 -583
- package/src/session/agent-storage.ts +18 -0
- package/src/session/cache-economics.ts +25 -8
- package/src/session/fallback-chain-controller.ts +53 -3
- package/src/session/internal/managed-session-scope.ts +2226 -0
- package/src/session/internal/managed-session-storage.ts +453 -0
- package/src/session/messages.ts +91 -2
- package/src/session/session-manager-internal.ts +33 -0
- package/src/session/session-manager.ts +1112 -333
- package/src/session/session-storage.ts +444 -39
- package/src/session/volatile-context-pruning.ts +78 -0
- package/src/setup/credential-auto-import.ts +2 -1
- package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
- package/src/setup/provider-onboarding.ts +36 -61
- package/src/setup/provider-presets.json +5 -0
- package/src/skill-state/active-state.ts +3 -13
- package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
- package/src/skill-state/workflow-state-contract.ts +6 -41
- package/src/slash-commands/builtin-registry.ts +46 -11
- package/src/system-prompt.ts +37 -44
- package/src/task/agents.ts +19 -1
- package/src/task/executor.ts +90 -6
- package/src/task/index.ts +36 -21
- package/src/thinking.ts +21 -6
- package/src/tool-discovery/tool-index.ts +2 -134
- package/src/tools/ask.ts +157 -21
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/bash.ts +38 -0
- package/src/tools/browser/dead-tab-recovery.ts +78 -0
- package/src/tools/browser/tab-supervisor.ts +282 -36
- package/src/tools/computer.ts +10 -3
- package/src/tools/cron.ts +30 -16
- package/src/tools/eval.ts +1 -2
- package/src/tools/fetch.ts +11 -4
- package/src/tools/index.ts +76 -24
- package/src/tools/output-meta.ts +19 -7
- package/src/tools/read.ts +219 -64
- package/src/tools/resource-gc.ts +2 -2
- package/src/tools/search-tool-bm25.ts +11 -74
- package/src/tools/subagent.ts +43 -2
- package/src/utils/event-bus.ts +9 -4
- package/src/utils/file-mentions.ts +4 -4
- package/src/web/search/index.ts +3 -3
- package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
- package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
- package/src/discovery/plugin-dir-roots.ts +0 -28
- package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
1
3
|
import * as fs from "node:fs";
|
|
2
|
-
import * as fsp from "node:fs/promises";
|
|
3
4
|
import * as path from "node:path";
|
|
5
|
+
import * as native from "@gajae-code/natives";
|
|
6
|
+
|
|
4
7
|
import { isEnoent, pathIsWithin, peekFile, toError } from "@gajae-code/utils";
|
|
5
8
|
|
|
6
9
|
const utf8Decoder = new TextDecoder("utf-8");
|
|
@@ -155,10 +158,13 @@ export interface SessionStorage {
|
|
|
155
158
|
// Verified hard-delete identity + typed partial-cleanup evidence
|
|
156
159
|
// =============================================================================
|
|
157
160
|
|
|
158
|
-
/** Exact
|
|
161
|
+
/** Exact authorization evidence for a transcript or artifact path. */
|
|
159
162
|
export interface SessionStorageFileIdentity {
|
|
160
163
|
dev: bigint;
|
|
161
164
|
ino: bigint;
|
|
165
|
+
size: number;
|
|
166
|
+
mtimeNs: bigint;
|
|
167
|
+
sha256: string;
|
|
162
168
|
}
|
|
163
169
|
|
|
164
170
|
/** Kind of verification failure surfaced by {@link deleteSessionVerified}. */
|
|
@@ -209,6 +215,18 @@ export interface VerifiedSessionDeleteTarget {
|
|
|
209
215
|
* fails closed. Omit on first attempt or to accept recorded absence.
|
|
210
216
|
*/
|
|
211
217
|
expectedArtifactsIdentity?: SessionStorageFileIdentity;
|
|
218
|
+
/** Stable native recursive-tree evidence captured before artifact detachment. */
|
|
219
|
+
expectedArtifactsTree?: NativeDirectoryTreeSnapshot;
|
|
220
|
+
/** Identity-bound quarantine path retained when recursive artifact cleanup failed. */
|
|
221
|
+
detachedArtifactsPath?: string;
|
|
222
|
+
/** Identity-bound quarantine path retained when transcript unlink deferred cleanup. */
|
|
223
|
+
detachedTranscriptPath?: string;
|
|
224
|
+
/** Caller-published, no-replace quarantine pathname for the next artifact detach. */
|
|
225
|
+
plannedArtifactsPath?: string;
|
|
226
|
+
/** Caller-published, no-replace quarantine pathname for the next transcript detach. */
|
|
227
|
+
plannedTranscriptPath?: string;
|
|
228
|
+
/** Set only after a durable caller receipt records successful artifact removal. */
|
|
229
|
+
artifactsRemoved?: true;
|
|
212
230
|
}
|
|
213
231
|
|
|
214
232
|
/**
|
|
@@ -218,6 +236,7 @@ export interface VerifiedSessionDeleteTarget {
|
|
|
218
236
|
* `deleted` and never `{}`.
|
|
219
237
|
*/
|
|
220
238
|
export type VerifiedSessionDeleteResult =
|
|
239
|
+
| { kind: "artifacts_removed"; phase: "artifacts"; transcriptIdentity: SessionStorageFileIdentity }
|
|
221
240
|
| { kind: "deleted" }
|
|
222
241
|
| {
|
|
223
242
|
kind: "cleanup_pending";
|
|
@@ -225,6 +244,10 @@ export type VerifiedSessionDeleteResult =
|
|
|
225
244
|
error: Error;
|
|
226
245
|
/** Artifact directory identity at failure time; undefined when absent. */
|
|
227
246
|
artifactsIdentity: SessionStorageFileIdentity | undefined;
|
|
247
|
+
/** Identity-bound quarantine path retained when recursive cleanup failed. */
|
|
248
|
+
detachedArtifactsPath: string;
|
|
249
|
+
/** Native snapshot required for an identity-bound recursive retry. */
|
|
250
|
+
artifactsTree: NativeDirectoryTreeSnapshot;
|
|
228
251
|
/** Transcript identity (unchanged) for retry binding. */
|
|
229
252
|
transcriptIdentity: SessionStorageFileIdentity;
|
|
230
253
|
}
|
|
@@ -234,6 +257,8 @@ export type VerifiedSessionDeleteResult =
|
|
|
234
257
|
error: Error;
|
|
235
258
|
/** Transcript identity at failure time for retry binding. */
|
|
236
259
|
transcriptIdentity: SessionStorageFileIdentity;
|
|
260
|
+
/** Optional identity-bound transcript quarantine path for restart cleanup. */
|
|
261
|
+
detachedTranscriptPath?: string;
|
|
237
262
|
};
|
|
238
263
|
|
|
239
264
|
/** Default OS-close dispatcher: a direct `fs.closeSync`. */
|
|
@@ -243,6 +268,110 @@ const defaultCloseAdapter: SessionStorageWriterCloseAdapter = {
|
|
|
243
268
|
},
|
|
244
269
|
};
|
|
245
270
|
|
|
271
|
+
type NativeSecurity = { ok: true } | { ok: false; code: string };
|
|
272
|
+
|
|
273
|
+
type NativeExactUnlinkResult = { ok: true; detachedPath?: string } | { ok: false; code: string; detachedPath?: string };
|
|
274
|
+
type NativeExactUnlink = (
|
|
275
|
+
path: string,
|
|
276
|
+
identity: {
|
|
277
|
+
dev: bigint;
|
|
278
|
+
ino: bigint;
|
|
279
|
+
size: bigint;
|
|
280
|
+
mtimeNs: bigint;
|
|
281
|
+
/** Required for regular-file deletion; directories are identity-bound only. */
|
|
282
|
+
sha256?: string;
|
|
283
|
+
directory?: boolean;
|
|
284
|
+
/** Optional caller-planned no-replace quarantine destination component. */
|
|
285
|
+
quarantineName?: string;
|
|
286
|
+
},
|
|
287
|
+
) => NativeExactUnlinkResult;
|
|
288
|
+
|
|
289
|
+
function nativeExactUnlink(
|
|
290
|
+
pathname: string,
|
|
291
|
+
identity: {
|
|
292
|
+
dev: bigint;
|
|
293
|
+
ino: bigint;
|
|
294
|
+
size: bigint;
|
|
295
|
+
mtimeNs: bigint;
|
|
296
|
+
/** Required for regular-file deletion; directories are identity-bound only. */
|
|
297
|
+
sha256?: string;
|
|
298
|
+
directory?: boolean;
|
|
299
|
+
quarantineName?: string;
|
|
300
|
+
},
|
|
301
|
+
): NativeExactUnlinkResult {
|
|
302
|
+
return (native.exactUnlink as unknown as NativeExactUnlink)(pathname, identity);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
type NativeDirectoryTreeEntry = {
|
|
306
|
+
relativePath: string;
|
|
307
|
+
kind: string;
|
|
308
|
+
dev: string;
|
|
309
|
+
ino: string;
|
|
310
|
+
size: string;
|
|
311
|
+
mtimeNs: string;
|
|
312
|
+
sha256?: string;
|
|
313
|
+
};
|
|
314
|
+
export type NativeDirectoryTreeSnapshot = {
|
|
315
|
+
rootDev: string;
|
|
316
|
+
rootIno: string;
|
|
317
|
+
entries: NativeDirectoryTreeEntry[];
|
|
318
|
+
};
|
|
319
|
+
type NativeDirectoryTreeResult =
|
|
320
|
+
| { ok: true; snapshot: NativeDirectoryTreeSnapshot }
|
|
321
|
+
| { ok: false; code: string; snapshot?: undefined };
|
|
322
|
+
type NativeDirectoryTreeApi = {
|
|
323
|
+
snapshotDirectoryTree(pathname: string): NativeDirectoryTreeResult;
|
|
324
|
+
exactRemoveDirectoryTree(pathname: string, snapshot: NativeDirectoryTreeSnapshot): NativeExactUnlinkResult;
|
|
325
|
+
};
|
|
326
|
+
function nativeDirectoryTreeApi(): NativeDirectoryTreeApi {
|
|
327
|
+
return native as unknown as NativeDirectoryTreeApi;
|
|
328
|
+
}
|
|
329
|
+
function snapshotDirectoryTree(pathname: string): NativeDirectoryTreeSnapshot {
|
|
330
|
+
const result = nativeDirectoryTreeApi().snapshotDirectoryTree(pathname);
|
|
331
|
+
if (!result.ok || !result.snapshot)
|
|
332
|
+
throw new SessionDeleteVerificationError(
|
|
333
|
+
"artifacts",
|
|
334
|
+
`Native artifact snapshot rejected: ${result.ok ? "missing_snapshot" : result.code}`,
|
|
335
|
+
);
|
|
336
|
+
return result.snapshot;
|
|
337
|
+
}
|
|
338
|
+
function removeDirectoryTreeExact(pathname: string, snapshot: NativeDirectoryTreeSnapshot): NativeExactUnlinkResult {
|
|
339
|
+
return nativeDirectoryTreeApi().exactRemoveDirectoryTree(pathname, snapshot);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function exactUnlinkFailure(result: NativeExactUnlinkResult): SessionDeleteVerificationError {
|
|
343
|
+
if (result.ok) throw new Error("Expected exact unlink failure");
|
|
344
|
+
const kind: VerifiedDeleteFailureKind =
|
|
345
|
+
result.code === "reparse_point" || result.code === "not_regular_file"
|
|
346
|
+
? "symlink"
|
|
347
|
+
: result.code === "identity_mismatch"
|
|
348
|
+
? "identity"
|
|
349
|
+
: "stat";
|
|
350
|
+
return new SessionDeleteVerificationError(kind, `Exact transcript deletion rejected: ${result.code}`);
|
|
351
|
+
}
|
|
352
|
+
function secureOwnerOnlyFile(pathname: string): void {
|
|
353
|
+
const applied = native.applyOwnerOnlyPathSecurity(pathname, "file") as NativeSecurity;
|
|
354
|
+
if (!applied.ok) throw new Error(`Owner-only security rejected ${pathname}: ${applied.code}`);
|
|
355
|
+
const verified = native.verifyOwnerOnlyPathSecurity(pathname, "file") as NativeSecurity;
|
|
356
|
+
if (!verified.ok) throw new Error(`Owner-only security rejected ${pathname}: ${verified.code}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Reject a symlink/junction/reparse component before a storage path is created or opened. */
|
|
360
|
+
function assertNoReparsePath(pathname: string): void {
|
|
361
|
+
const resolved = path.resolve(pathname);
|
|
362
|
+
const parsed = path.parse(resolved);
|
|
363
|
+
let current = parsed.root;
|
|
364
|
+
for (const part of resolved.slice(parsed.root.length).split(path.sep)) {
|
|
365
|
+
if (!part) continue;
|
|
366
|
+
current = path.join(current, part);
|
|
367
|
+
try {
|
|
368
|
+
if (fs.lstatSync(current).isSymbolicLink()) throw new Error(`Unsafe reparse storage path: ${current}`);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
246
375
|
// FinalizationRegistry to clean up leaked file descriptors
|
|
247
376
|
const writerRegistry = new FinalizationRegistry<number>(fd => {
|
|
248
377
|
try {
|
|
@@ -264,14 +393,25 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
264
393
|
this.#onError = options?.onError;
|
|
265
394
|
this.#closeAdapter = options?.closeAdapter ?? defaultCloseAdapter;
|
|
266
395
|
const flags = options?.flags ?? "a";
|
|
267
|
-
// Ensure parent directory exists
|
|
268
396
|
const dir = path.dirname(fpath);
|
|
269
|
-
|
|
270
|
-
|
|
397
|
+
assertNoReparsePath(dir);
|
|
398
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
399
|
+
assertNoReparsePath(dir);
|
|
400
|
+
assertNoReparsePath(fpath);
|
|
401
|
+
// The creation mode prevents a POSIX exposure before the native verifier runs.
|
|
402
|
+
const openFlags =
|
|
403
|
+
(flags === "w"
|
|
404
|
+
? fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC
|
|
405
|
+
: fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND) | (fs.constants.O_NOFOLLOW ?? 0);
|
|
406
|
+
const fd = fs.openSync(fpath, openFlags, 0o600);
|
|
407
|
+
try {
|
|
408
|
+
// Do not rely on directory inheritance: secure and verify every file independently.
|
|
409
|
+
secureOwnerOnlyFile(fpath);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
fs.closeSync(fd);
|
|
412
|
+
throw error;
|
|
271
413
|
}
|
|
272
|
-
|
|
273
|
-
this.#fd = fs.openSync(fpath, flags === "w" ? "w" : "a");
|
|
274
|
-
// Register for cleanup if abandoned without close()
|
|
414
|
+
this.#fd = fd;
|
|
275
415
|
writerRegistry.register(this, this.#fd, this);
|
|
276
416
|
}
|
|
277
417
|
|
|
@@ -491,20 +631,18 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
491
631
|
}
|
|
492
632
|
|
|
493
633
|
/**
|
|
494
|
-
* Delete a session
|
|
495
|
-
*
|
|
634
|
+
* Delete a session and sibling artifacts in an operator-selected explicit directory.
|
|
635
|
+
* Default managed roots use deleteSessionVerified and never call this path.
|
|
496
636
|
*/
|
|
497
637
|
async deleteSessionWithArtifacts(sessionPath: string): Promise<void> {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
638
|
+
try {
|
|
639
|
+
await this.unlink(sessionPath);
|
|
640
|
+
} catch (error) {
|
|
641
|
+
if (!isEnoent(error)) throw error;
|
|
642
|
+
}
|
|
502
643
|
const artifactsDir = sessionPath.slice(0, -6);
|
|
503
|
-
|
|
504
|
-
// Delete artifacts directory if it exists. Missing directories are fine, but
|
|
505
|
-
// surface real cleanup failures because the session file is already gone.
|
|
506
644
|
try {
|
|
507
|
-
await
|
|
645
|
+
await fs.promises.rm(artifactsDir, { recursive: true, force: true });
|
|
508
646
|
} catch (err) {
|
|
509
647
|
const error = toError(err);
|
|
510
648
|
throw new Error(
|
|
@@ -521,68 +659,314 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
521
659
|
* evidence; identity/symlink/containment/header/cwd mismatch throws.
|
|
522
660
|
*/
|
|
523
661
|
async deleteSessionVerified(target: VerifiedSessionDeleteTarget): Promise<VerifiedSessionDeleteResult> {
|
|
524
|
-
const {
|
|
662
|
+
const {
|
|
663
|
+
sessionsRoot,
|
|
664
|
+
transcriptPath,
|
|
665
|
+
sessionId,
|
|
666
|
+
cwd,
|
|
667
|
+
transcriptIdentity,
|
|
668
|
+
expectedArtifactsIdentity,
|
|
669
|
+
expectedArtifactsTree,
|
|
670
|
+
detachedArtifactsPath,
|
|
671
|
+
detachedTranscriptPath,
|
|
672
|
+
plannedArtifactsPath,
|
|
673
|
+
plannedTranscriptPath,
|
|
674
|
+
artifactsRemoved,
|
|
675
|
+
} = target;
|
|
676
|
+
try {
|
|
677
|
+
assertNoReparsePath(sessionsRoot);
|
|
678
|
+
assertNoReparsePath(transcriptPath);
|
|
679
|
+
} catch (err) {
|
|
680
|
+
throw new SessionDeleteVerificationError("symlink", "Sessions root or transcript path is a symlink", {
|
|
681
|
+
cause: toError(err),
|
|
682
|
+
});
|
|
683
|
+
}
|
|
525
684
|
if (!transcriptPath.endsWith(".jsonl")) {
|
|
526
685
|
throw new SessionDeleteVerificationError("containment", "Transcript path is not a .jsonl file");
|
|
527
686
|
}
|
|
528
687
|
if (!pathIsWithin(sessionsRoot, transcriptPath)) {
|
|
529
688
|
throw new SessionDeleteVerificationError("containment", "Transcript is outside the sessions root");
|
|
530
689
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
690
|
+
if (
|
|
691
|
+
!plannedArtifactsPath ||
|
|
692
|
+
!plannedTranscriptPath ||
|
|
693
|
+
path.dirname(plannedArtifactsPath) !== path.dirname(transcriptPath) ||
|
|
694
|
+
path.dirname(plannedTranscriptPath) !== path.dirname(transcriptPath) ||
|
|
695
|
+
!path.basename(plannedArtifactsPath).startsWith(".gjc-delete-") ||
|
|
696
|
+
!path.basename(plannedTranscriptPath).startsWith(".gjc-delete-") ||
|
|
697
|
+
plannedArtifactsPath === plannedTranscriptPath
|
|
698
|
+
) {
|
|
699
|
+
throw new SessionDeleteVerificationError(
|
|
700
|
+
"artifacts",
|
|
701
|
+
"Verified deletion requires caller-persisted quarantine paths",
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
const cleanupTranscriptPath = detachedTranscriptPath ?? transcriptPath;
|
|
705
|
+
const hasDetachedTranscript = detachedTranscriptPath !== undefined;
|
|
706
|
+
if (
|
|
707
|
+
detachedTranscriptPath &&
|
|
708
|
+
(path.dirname(detachedTranscriptPath) !== path.dirname(transcriptPath) ||
|
|
709
|
+
!path.basename(detachedTranscriptPath).startsWith(".gjc-delete-") ||
|
|
710
|
+
detachedTranscriptPath === plannedTranscriptPath)
|
|
711
|
+
) {
|
|
712
|
+
throw new SessionDeleteVerificationError(
|
|
713
|
+
"identity",
|
|
714
|
+
"Detached transcript retry requires a fresh quarantine destination",
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
if (hasDetachedTranscript && fs.existsSync(transcriptPath)) {
|
|
718
|
+
throw new SessionDeleteVerificationError(
|
|
719
|
+
"identity",
|
|
720
|
+
"Original transcript pathname became occupied during detached cleanup replay",
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
const artifactRemovalRoot = `${plannedArtifactsPath}.removing`;
|
|
724
|
+
if (detachedArtifactsPath === plannedArtifactsPath) {
|
|
725
|
+
throw new SessionDeleteVerificationError(
|
|
726
|
+
"artifacts",
|
|
727
|
+
"Detached artifact retry requires a fresh quarantine destination",
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
const retainedArtifactRoot = (input: string): string =>
|
|
731
|
+
input.endsWith(".removing") ? input : `${input}.removing`;
|
|
732
|
+
|
|
733
|
+
const initial = hasDetachedTranscript ? undefined : this.#verifiedReadAndHeader(transcriptPath, sessionId, cwd);
|
|
734
|
+
const initialStat = initial?.snapshot.stat;
|
|
735
|
+
const initialDigest = initial ? createHash("sha256").update(initial.snapshot.bytes).digest("hex") : undefined;
|
|
736
|
+
if (
|
|
737
|
+
initialStat &&
|
|
738
|
+
(initialStat.dev !== transcriptIdentity.dev ||
|
|
739
|
+
initialStat.ino !== transcriptIdentity.ino ||
|
|
740
|
+
initialStat.size !== transcriptIdentity.size ||
|
|
741
|
+
initialStat.mtimeNs !== transcriptIdentity.mtimeNs ||
|
|
742
|
+
initialDigest !== transcriptIdentity.sha256)
|
|
743
|
+
) {
|
|
534
744
|
throw new SessionDeleteVerificationError("identity", "Transcript identity does not match authorization");
|
|
535
745
|
}
|
|
746
|
+
|
|
536
747
|
const parentIdentity = this.#directoryIdentity(path.dirname(transcriptPath));
|
|
748
|
+
if (detachedArtifactsPath) {
|
|
749
|
+
if (
|
|
750
|
+
!expectedArtifactsIdentity ||
|
|
751
|
+
path.dirname(detachedArtifactsPath) !== path.dirname(transcriptPath) ||
|
|
752
|
+
!path.basename(detachedArtifactsPath).startsWith(".gjc-delete-")
|
|
753
|
+
) {
|
|
754
|
+
throw new SessionDeleteVerificationError("artifacts", "Detached artifact cleanup evidence is invalid");
|
|
755
|
+
}
|
|
756
|
+
const detachedIdentity = this.#optionalDirectoryIdentity(detachedArtifactsPath);
|
|
757
|
+
if (
|
|
758
|
+
!detachedIdentity ||
|
|
759
|
+
detachedIdentity.dev !== expectedArtifactsIdentity.dev ||
|
|
760
|
+
detachedIdentity.ino !== expectedArtifactsIdentity.ino
|
|
761
|
+
) {
|
|
762
|
+
throw new SessionDeleteVerificationError("artifacts", "Detached artifact identity changed before retry");
|
|
763
|
+
}
|
|
764
|
+
if (!expectedArtifactsTree)
|
|
765
|
+
throw new SessionDeleteVerificationError(
|
|
766
|
+
"artifacts",
|
|
767
|
+
"Detached artifact cleanup requires a persisted tree snapshot",
|
|
768
|
+
);
|
|
769
|
+
const removal = removeDirectoryTreeExact(detachedArtifactsPath, expectedArtifactsTree);
|
|
770
|
+
if (!removal.ok) {
|
|
771
|
+
const retainedRoot = removal.detachedPath ?? detachedArtifactsPath;
|
|
772
|
+
if (retainedRoot !== detachedArtifactsPath && retainedRoot !== retainedArtifactRoot(detachedArtifactsPath))
|
|
773
|
+
throw new SessionDeleteVerificationError(
|
|
774
|
+
"artifacts",
|
|
775
|
+
"Native artifact removal returned an unauthorized root",
|
|
776
|
+
);
|
|
777
|
+
return {
|
|
778
|
+
kind: "cleanup_pending",
|
|
779
|
+
phase: "artifacts",
|
|
780
|
+
error: new SessionDeleteVerificationError(
|
|
781
|
+
"artifacts",
|
|
782
|
+
`Exact detached artifact removal rejected: ${removal.code}`,
|
|
783
|
+
),
|
|
784
|
+
artifactsIdentity: expectedArtifactsIdentity,
|
|
785
|
+
detachedArtifactsPath: retainedRoot,
|
|
786
|
+
artifactsTree: expectedArtifactsTree,
|
|
787
|
+
transcriptIdentity,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
}
|
|
537
791
|
|
|
792
|
+
if (artifactsRemoved && (detachedArtifactsPath || expectedArtifactsIdentity)) {
|
|
793
|
+
throw new SessionDeleteVerificationError(
|
|
794
|
+
"artifacts",
|
|
795
|
+
"Artifact phase receipt conflicts with pending artifact cleanup",
|
|
796
|
+
);
|
|
797
|
+
}
|
|
538
798
|
const artifactsDir = transcriptPath.slice(0, -6);
|
|
539
799
|
const artifactsIdentity = this.#optionalDirectoryIdentity(artifactsDir);
|
|
540
|
-
if (artifactsIdentity) {
|
|
800
|
+
if (artifactsRemoved && artifactsIdentity) {
|
|
801
|
+
throw new SessionDeleteVerificationError(
|
|
802
|
+
"artifacts",
|
|
803
|
+
"Artifact path reappeared after durable artifact-phase completion",
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
if (!artifactsIdentity && expectedArtifactsIdentity && !detachedArtifactsPath && !artifactsRemoved) {
|
|
807
|
+
// Absence at the original path alone is not completion: native recursive removal
|
|
808
|
+
// may retain the planned root or its deterministic `.removing` final-stage root.
|
|
809
|
+
if (fs.existsSync(plannedArtifactsPath) || fs.existsSync(artifactRemovalRoot))
|
|
810
|
+
throw new SessionDeleteVerificationError(
|
|
811
|
+
"artifacts",
|
|
812
|
+
"Authorized artifact removal root remains after restart",
|
|
813
|
+
);
|
|
814
|
+
return { kind: "artifacts_removed", phase: "artifacts", transcriptIdentity };
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (artifactsIdentity && !artifactsRemoved) {
|
|
541
818
|
if (
|
|
542
819
|
expectedArtifactsIdentity &&
|
|
543
820
|
(artifactsIdentity.dev !== expectedArtifactsIdentity.dev ||
|
|
544
|
-
artifactsIdentity.ino !== expectedArtifactsIdentity.ino
|
|
821
|
+
artifactsIdentity.ino !== expectedArtifactsIdentity.ino ||
|
|
822
|
+
artifactsIdentity.size !== expectedArtifactsIdentity.size ||
|
|
823
|
+
artifactsIdentity.mtimeNs !== expectedArtifactsIdentity.mtimeNs ||
|
|
824
|
+
artifactsIdentity.sha256 !== expectedArtifactsIdentity.sha256)
|
|
545
825
|
) {
|
|
546
826
|
throw new SessionDeleteVerificationError(
|
|
547
827
|
"artifacts",
|
|
548
828
|
"Artifact directory identity does not match recorded cleanup evidence",
|
|
549
829
|
);
|
|
550
830
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
831
|
+
const artifactStat = fs.lstatSync(artifactsDir, { bigint: true });
|
|
832
|
+
if (
|
|
833
|
+
artifactStat.isSymbolicLink() ||
|
|
834
|
+
!artifactStat.isDirectory() ||
|
|
835
|
+
artifactStat.dev !== artifactsIdentity.dev ||
|
|
836
|
+
artifactStat.ino !== artifactsIdentity.ino
|
|
837
|
+
) {
|
|
838
|
+
throw new SessionDeleteVerificationError("artifacts", "Artifact directory changed before deletion");
|
|
839
|
+
}
|
|
840
|
+
const observedArtifactsTree = snapshotDirectoryTree(artifactsDir);
|
|
841
|
+
if (expectedArtifactsTree && JSON.stringify(observedArtifactsTree) !== JSON.stringify(expectedArtifactsTree))
|
|
842
|
+
throw new SessionDeleteVerificationError("artifacts", "Artifact tree changed before root detach");
|
|
843
|
+
const artifactsTree = expectedArtifactsTree ?? observedArtifactsTree;
|
|
844
|
+
const detach = nativeExactUnlink(artifactsDir, {
|
|
845
|
+
dev: artifactStat.dev,
|
|
846
|
+
ino: artifactStat.ino,
|
|
847
|
+
size: artifactStat.size,
|
|
848
|
+
mtimeNs: artifactStat.mtimeNs,
|
|
849
|
+
directory: true,
|
|
850
|
+
quarantineName: path.basename(plannedArtifactsPath),
|
|
851
|
+
});
|
|
852
|
+
if (!detach.ok || !detach.detachedPath) {
|
|
853
|
+
throw new SessionDeleteVerificationError(
|
|
854
|
+
"artifacts",
|
|
855
|
+
`Exact artifact detach rejected: ${detach.ok ? "missing_path" : detach.code}`,
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
const removal = removeDirectoryTreeExact(detach.detachedPath, artifactsTree);
|
|
859
|
+
if (!removal.ok) {
|
|
860
|
+
const retainedRoot = removal.detachedPath ?? detach.detachedPath;
|
|
861
|
+
if (retainedRoot !== detach.detachedPath && retainedRoot !== retainedArtifactRoot(detach.detachedPath))
|
|
862
|
+
throw new SessionDeleteVerificationError(
|
|
863
|
+
"artifacts",
|
|
864
|
+
"Native artifact removal returned an unauthorized root",
|
|
865
|
+
);
|
|
554
866
|
return {
|
|
555
867
|
kind: "cleanup_pending",
|
|
556
868
|
phase: "artifacts",
|
|
557
|
-
error:
|
|
869
|
+
error: new SessionDeleteVerificationError(
|
|
870
|
+
"artifacts",
|
|
871
|
+
`Exact detached artifact removal rejected: ${removal.code}`,
|
|
872
|
+
),
|
|
558
873
|
artifactsIdentity,
|
|
559
|
-
|
|
874
|
+
detachedArtifactsPath: retainedRoot,
|
|
875
|
+
artifactsTree,
|
|
876
|
+
transcriptIdentity,
|
|
560
877
|
};
|
|
561
878
|
}
|
|
562
879
|
}
|
|
563
|
-
|
|
880
|
+
if (!artifactsRemoved) {
|
|
881
|
+
if (process.platform !== "win32") {
|
|
882
|
+
let descriptor: number | undefined;
|
|
883
|
+
try {
|
|
884
|
+
descriptor = fs.openSync(
|
|
885
|
+
path.dirname(transcriptPath),
|
|
886
|
+
fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW,
|
|
887
|
+
);
|
|
888
|
+
const durableParent = fs.fstatSync(descriptor, { bigint: true });
|
|
889
|
+
if (
|
|
890
|
+
!durableParent.isDirectory() ||
|
|
891
|
+
durableParent.dev !== parentIdentity.dev ||
|
|
892
|
+
durableParent.ino !== parentIdentity.ino
|
|
893
|
+
)
|
|
894
|
+
throw new Error("parent_changed");
|
|
895
|
+
fs.fsyncSync(descriptor);
|
|
896
|
+
} catch (error) {
|
|
897
|
+
throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
|
|
898
|
+
} finally {
|
|
899
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return { kind: "artifacts_removed", phase: "artifacts", transcriptIdentity };
|
|
903
|
+
}
|
|
904
|
+
if (hasDetachedTranscript) {
|
|
905
|
+
const deletion = nativeExactUnlink(cleanupTranscriptPath, {
|
|
906
|
+
dev: transcriptIdentity.dev,
|
|
907
|
+
ino: transcriptIdentity.ino,
|
|
908
|
+
size: BigInt(transcriptIdentity.size),
|
|
909
|
+
mtimeNs: transcriptIdentity.mtimeNs,
|
|
910
|
+
sha256: transcriptIdentity.sha256,
|
|
911
|
+
quarantineName: path.basename(plannedTranscriptPath),
|
|
912
|
+
});
|
|
913
|
+
if (!deletion.ok) {
|
|
914
|
+
const error = exactUnlinkFailure(deletion);
|
|
915
|
+
if (error.kind === "identity" || error.kind === "symlink") throw error;
|
|
916
|
+
return {
|
|
917
|
+
kind: "cleanup_pending",
|
|
918
|
+
phase: "transcript",
|
|
919
|
+
error,
|
|
920
|
+
transcriptIdentity,
|
|
921
|
+
detachedTranscriptPath: deletion.detachedPath ?? detachedTranscriptPath,
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
return { kind: "deleted" };
|
|
925
|
+
}
|
|
926
|
+
if (!initialStat || !initialDigest)
|
|
927
|
+
throw new SessionDeleteVerificationError("stat", "Transcript cleanup state is invalid");
|
|
564
928
|
const revalidate = this.#verifiedReadAndHeader(transcriptPath, sessionId, cwd);
|
|
565
929
|
const revalidateStat = revalidate.snapshot.stat;
|
|
566
|
-
|
|
930
|
+
const revalidateDigest = createHash("sha256").update(revalidate.snapshot.bytes).digest("hex");
|
|
931
|
+
if (
|
|
932
|
+
revalidateStat.dev !== initialStat.dev ||
|
|
933
|
+
revalidateStat.ino !== initialStat.ino ||
|
|
934
|
+
revalidateStat.size !== initialStat.size ||
|
|
935
|
+
revalidateStat.mtimeNs !== initialStat.mtimeNs ||
|
|
936
|
+
revalidateDigest !== initialDigest
|
|
937
|
+
) {
|
|
567
938
|
throw new SessionDeleteVerificationError(
|
|
568
939
|
"identity",
|
|
569
940
|
"Transcript identity changed after artifact removal (replacement detected)",
|
|
570
941
|
);
|
|
571
942
|
}
|
|
943
|
+
|
|
572
944
|
const parentIdentityNow = this.#directoryIdentity(path.dirname(transcriptPath));
|
|
573
945
|
if (parentIdentityNow.dev !== parentIdentity.dev || parentIdentityNow.ino !== parentIdentity.ino) {
|
|
574
946
|
throw new SessionDeleteVerificationError("identity", "Parent directory identity changed during deletion");
|
|
575
947
|
}
|
|
576
948
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
949
|
+
this.#assertPathMatchesSnapshot(transcriptPath, revalidate.snapshot);
|
|
950
|
+
if (!Number.isSafeInteger(revalidateStat.size) || revalidateStat.size < 0) {
|
|
951
|
+
throw new SessionDeleteVerificationError("identity", "Transcript size cannot be bound exactly for deletion");
|
|
952
|
+
}
|
|
953
|
+
const deletion = nativeExactUnlink(transcriptPath, {
|
|
954
|
+
dev: initialStat.dev,
|
|
955
|
+
ino: initialStat.ino,
|
|
956
|
+
size: BigInt(initialStat.size),
|
|
957
|
+
mtimeNs: initialStat.mtimeNs,
|
|
958
|
+
sha256: initialDigest,
|
|
959
|
+
quarantineName: path.basename(plannedTranscriptPath),
|
|
960
|
+
});
|
|
961
|
+
if (!deletion.ok) {
|
|
962
|
+
const error = exactUnlinkFailure(deletion);
|
|
963
|
+
if (error.kind === "identity" || error.kind === "symlink") throw error;
|
|
581
964
|
return {
|
|
582
965
|
kind: "cleanup_pending",
|
|
583
966
|
phase: "transcript",
|
|
584
|
-
error
|
|
585
|
-
transcriptIdentity
|
|
967
|
+
error,
|
|
968
|
+
transcriptIdentity,
|
|
969
|
+
detachedTranscriptPath: deletion.detachedPath,
|
|
586
970
|
};
|
|
587
971
|
}
|
|
588
972
|
return { kind: "deleted" };
|
|
@@ -627,6 +1011,27 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
627
1011
|
return { snapshot };
|
|
628
1012
|
}
|
|
629
1013
|
|
|
1014
|
+
#assertPathMatchesSnapshot(transcriptPath: string, snapshot: SessionStorageSnapshot): void {
|
|
1015
|
+
let named: fs.BigIntStats;
|
|
1016
|
+
try {
|
|
1017
|
+
named = fs.lstatSync(transcriptPath, { bigint: true });
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
throw new SessionDeleteVerificationError("stat", "Transcript path could not be revalidated", {
|
|
1020
|
+
cause: toError(err),
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
if (
|
|
1024
|
+
named.isSymbolicLink() ||
|
|
1025
|
+
!named.isFile() ||
|
|
1026
|
+
named.dev !== snapshot.stat.dev ||
|
|
1027
|
+
named.ino !== snapshot.stat.ino ||
|
|
1028
|
+
Number(named.size) !== snapshot.stat.size ||
|
|
1029
|
+
named.mtimeNs !== snapshot.stat.mtimeNs
|
|
1030
|
+
) {
|
|
1031
|
+
throw new SessionDeleteVerificationError("identity", "Transcript path changed before deletion");
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
630
1035
|
#directoryIdentity(dirPath: string): SessionStorageFileIdentity {
|
|
631
1036
|
let stat: fs.BigIntStats;
|
|
632
1037
|
try {
|
|
@@ -637,7 +1042,7 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
637
1042
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
638
1043
|
throw new SessionDeleteVerificationError("symlink", "Directory is a symlink or not a directory");
|
|
639
1044
|
}
|
|
640
|
-
return { dev: stat.dev, ino: stat.ino };
|
|
1045
|
+
return { dev: stat.dev, ino: stat.ino, size: Number(stat.size), mtimeNs: stat.mtimeNs, sha256: "" };
|
|
641
1046
|
}
|
|
642
1047
|
|
|
643
1048
|
#optionalDirectoryIdentity(dirPath: string): SessionStorageFileIdentity | undefined {
|
|
@@ -660,7 +1065,7 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
660
1065
|
// Fail closed before any mutation.
|
|
661
1066
|
throw new SessionDeleteVerificationError("artifacts", "Artifact path exists but is not a directory");
|
|
662
1067
|
}
|
|
663
|
-
return { dev: stat.dev, ino: stat.ino };
|
|
1068
|
+
return { dev: stat.dev, ino: stat.ino, size: Number(stat.size), mtimeNs: stat.mtimeNs, sha256: "" };
|
|
664
1069
|
}
|
|
665
1070
|
}
|
|
666
1071
|
|