@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
|
@@ -4,19 +4,40 @@ import * as path from "node:path";
|
|
|
4
4
|
import { YAML } from "bun";
|
|
5
5
|
import { withFileLock } from "./file-lock";
|
|
6
6
|
|
|
7
|
+
export interface AtomicYamlExpectedPrecondition {
|
|
8
|
+
path: string;
|
|
9
|
+
hash: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
7
12
|
export interface AtomicYamlSetPatch {
|
|
8
13
|
path: string;
|
|
9
14
|
op: "set";
|
|
10
15
|
value: unknown;
|
|
16
|
+
expected?: AtomicYamlExpectedPrecondition;
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
export interface AtomicYamlUnsetPatch {
|
|
14
20
|
path: string;
|
|
15
21
|
op: "unset";
|
|
22
|
+
expected?: AtomicYamlExpectedPrecondition;
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
export type AtomicYamlPatch = AtomicYamlSetPatch | AtomicYamlUnsetPatch;
|
|
19
26
|
|
|
27
|
+
/** Raised when a compare-and-swap precondition no longer matches durable YAML. */
|
|
28
|
+
export class AtomicYamlConflictError extends Error {
|
|
29
|
+
readonly code = "ATOMIC_YAML_CONFLICT";
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
readonly path: string,
|
|
33
|
+
readonly expectedHash: string,
|
|
34
|
+
readonly actualHash: string,
|
|
35
|
+
) {
|
|
36
|
+
super(`Atomic YAML precondition failed for ${path}.`);
|
|
37
|
+
this.name = "AtomicYamlConflictError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
20
41
|
export interface AtomicYamlPatchRevision {
|
|
21
42
|
path: string;
|
|
22
43
|
beforeHash: string;
|
|
@@ -53,6 +74,8 @@ export interface AtomicYamlPatchOptions {
|
|
|
53
74
|
sleep?: (ms: number) => Promise<void>;
|
|
54
75
|
/** Test seam for Windows rename retry behavior. */
|
|
55
76
|
platform?: NodeJS.Platform;
|
|
77
|
+
/** Called under the config lock before patches are applied. */
|
|
78
|
+
validateRoot?: (root: unknown, patches: readonly AtomicYamlPatch[]) => void | Promise<void>;
|
|
56
79
|
/** Called under the config lock after a successful CAS restore. */
|
|
57
80
|
onRestored?: (patches: readonly AtomicYamlPatch[]) => void | Promise<void>;
|
|
58
81
|
}
|
|
@@ -98,6 +121,16 @@ function assertPatch(patch: AtomicYamlPatch): void {
|
|
|
98
121
|
) {
|
|
99
122
|
throw new Error("Atomic YAML patches require a non-empty dotted path.");
|
|
100
123
|
}
|
|
124
|
+
if (
|
|
125
|
+
patch.expected &&
|
|
126
|
+
(typeof patch.expected.path !== "string" ||
|
|
127
|
+
patch.expected.path.length === 0 ||
|
|
128
|
+
patch.expected.path.split(".").some(part => !part) ||
|
|
129
|
+
typeof patch.expected.hash !== "string" ||
|
|
130
|
+
patch.expected.hash.length === 0)
|
|
131
|
+
) {
|
|
132
|
+
throw new Error("Atomic YAML patch preconditions require a non-empty dotted path and hash.");
|
|
133
|
+
}
|
|
101
134
|
if (patch.op === "set") {
|
|
102
135
|
if (patch.value === undefined) {
|
|
103
136
|
throw new TypeError(`Atomic YAML set patch for ${patch.path} cannot carry undefined; use unset instead.`);
|
|
@@ -148,6 +181,12 @@ export function deleteByPath(value: Record<string, unknown>, segments: readonly
|
|
|
148
181
|
}
|
|
149
182
|
|
|
150
183
|
function stableValue(value: unknown): string {
|
|
184
|
+
if (typeof value === "number") {
|
|
185
|
+
if (Number.isNaN(value)) return "NaN";
|
|
186
|
+
if (value === Infinity) return "Infinity";
|
|
187
|
+
if (value === -Infinity) return "-Infinity";
|
|
188
|
+
if (Object.is(value, -0)) return "-0";
|
|
189
|
+
}
|
|
151
190
|
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "undefined";
|
|
152
191
|
if (Array.isArray(value)) return `[${value.map(stableValue).join(",")}]`;
|
|
153
192
|
const object = value as Record<string, unknown>;
|
|
@@ -167,12 +206,22 @@ function cloneState(state: PathState): PathState {
|
|
|
167
206
|
return state.exists ? { exists: true, value: structuredClone(state.value) } : state;
|
|
168
207
|
}
|
|
169
208
|
|
|
170
|
-
|
|
209
|
+
/** Hash a dotted YAML path state for an expected-hash patch precondition. */
|
|
210
|
+
export function atomicYamlPathHash(value: Record<string, unknown>, path: string): string {
|
|
211
|
+
return stateHash(stateAtPath(value, path.split(".")));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
type YamlReadResult = {
|
|
215
|
+
current: Record<string, unknown>;
|
|
216
|
+
root: unknown;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
async function readYaml(configPath: string): Promise<YamlReadResult> {
|
|
171
220
|
try {
|
|
172
|
-
const
|
|
173
|
-
return record(
|
|
221
|
+
const root = YAML.parse(await fs.readFile(configPath, "utf8"));
|
|
222
|
+
return { current: record(root) ?? {}, root };
|
|
174
223
|
} catch (error) {
|
|
175
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
|
|
224
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { current: {}, root: undefined };
|
|
176
225
|
throw error;
|
|
177
226
|
}
|
|
178
227
|
}
|
|
@@ -254,8 +303,9 @@ function createReceipt(
|
|
|
254
303
|
async restore(): Promise<CasRestoreResult> {
|
|
255
304
|
if (discarded) return { status: "discarded" };
|
|
256
305
|
return await enqueueAtomicYamlOperation(configPath, async canonicalPath => {
|
|
306
|
+
if (discarded) return { status: "discarded" };
|
|
257
307
|
return await withFileLock(canonicalPath, async () => {
|
|
258
|
-
const current = await readYaml(canonicalPath);
|
|
308
|
+
const { current, root } = await readYaml(canonicalPath);
|
|
259
309
|
const conflicts = changes
|
|
260
310
|
.filter(
|
|
261
311
|
change =>
|
|
@@ -269,6 +319,7 @@ function createReceipt(
|
|
|
269
319
|
? { path: change.path, op: "set", value: structuredClone(change.before.value) }
|
|
270
320
|
: { path: change.path, op: "unset" },
|
|
271
321
|
);
|
|
322
|
+
await options.validateRoot?.(root, restorePatches);
|
|
272
323
|
const receipt = await applyPatchesUnderLock(canonicalPath, current, restorePatches, options);
|
|
273
324
|
await options.onRestored?.(restorePatches);
|
|
274
325
|
return { status: "restored", receipt };
|
|
@@ -286,6 +337,14 @@ async function applyPatchesUnderLock(
|
|
|
286
337
|
): Promise<CasReceipt> {
|
|
287
338
|
if (patches.length === 0) return createReceipt(configPath, [], options);
|
|
288
339
|
|
|
340
|
+
for (const patch of patches) {
|
|
341
|
+
if (!patch.expected) continue;
|
|
342
|
+
const actualHash = stateHash(stateAtPath(current, patch.expected.path.split(".")));
|
|
343
|
+
if (actualHash !== patch.expected.hash) {
|
|
344
|
+
throw new AtomicYamlConflictError(patch.expected.path, patch.expected.hash, actualHash);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
289
348
|
const changesByPath = new Map<string, ReceiptChange>();
|
|
290
349
|
for (const patch of patches) {
|
|
291
350
|
const segments = patch.path.split(".");
|
|
@@ -329,9 +388,10 @@ export function applyAtomicYamlPatchesWithCurrent(
|
|
|
329
388
|
return enqueueAtomicYamlOperation(configPath, async canonicalPath => {
|
|
330
389
|
await fs.mkdir(path.dirname(canonicalPath), { recursive: true, mode: 0o700 });
|
|
331
390
|
return await withFileLock(canonicalPath, async () => {
|
|
332
|
-
const current = await readYaml(canonicalPath);
|
|
391
|
+
const { current, root } = await readYaml(canonicalPath);
|
|
333
392
|
const patches = await buildPatches(current);
|
|
334
393
|
for (const patch of patches) assertPatch(patch);
|
|
394
|
+
await options.validateRoot?.(root, patches);
|
|
335
395
|
return await applyPatchesUnderLock(canonicalPath, current, patches, options);
|
|
336
396
|
});
|
|
337
397
|
});
|
|
@@ -374,7 +434,8 @@ export function reserveAtomicYamlPatchSlot(
|
|
|
374
434
|
for (const patch of nextPatches) assertPatch(patch);
|
|
375
435
|
await fs.mkdir(path.dirname(canonicalPath), { recursive: true, mode: 0o700 });
|
|
376
436
|
return await withFileLock(canonicalPath, async () => {
|
|
377
|
-
const current = await readYaml(canonicalPath);
|
|
437
|
+
const { current, root } = await readYaml(canonicalPath);
|
|
438
|
+
await options.validateRoot?.(root, nextPatches);
|
|
378
439
|
return await applyPatchesUnderLock(canonicalPath, current, nextPatches, options);
|
|
379
440
|
});
|
|
380
441
|
});
|
|
@@ -393,7 +454,7 @@ export function reserveAtomicYamlUpdateSlot<T>(
|
|
|
393
454
|
const atomicUpdate = await update();
|
|
394
455
|
await fs.mkdir(path.dirname(canonicalPath), { recursive: true, mode: 0o700 });
|
|
395
456
|
return await withFileLock(canonicalPath, async () => {
|
|
396
|
-
const current = await readYaml(canonicalPath);
|
|
457
|
+
const { current } = await readYaml(canonicalPath);
|
|
397
458
|
const result = await atomicUpdate.apply(current);
|
|
398
459
|
if (atomicUpdate.shouldWrite?.(result) !== false) {
|
|
399
460
|
await writeAtomicYaml(canonicalPath, current, options);
|
|
@@ -414,10 +475,16 @@ export function applyAtomicYamlPatches(
|
|
|
414
475
|
options: AtomicYamlPatchOptions = {},
|
|
415
476
|
): Promise<CasReceipt> {
|
|
416
477
|
for (const patch of patches) assertPatch(patch);
|
|
417
|
-
const immutablePatches = patches.map(patch =>
|
|
418
|
-
patch.
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
478
|
+
const immutablePatches = patches.map(patch => {
|
|
479
|
+
const expected = patch.expected ? { ...patch.expected } : undefined;
|
|
480
|
+
return patch.op === "set"
|
|
481
|
+
? ({
|
|
482
|
+
path: patch.path,
|
|
483
|
+
op: "set",
|
|
484
|
+
value: structuredClone(patch.value),
|
|
485
|
+
...(expected ? { expected } : {}),
|
|
486
|
+
} as const)
|
|
487
|
+
: ({ path: patch.path, op: "unset", ...(expected ? { expected } : {}) } as const);
|
|
488
|
+
});
|
|
422
489
|
return reserveAtomicYamlPatchSlot(configPath, () => immutablePatches, options);
|
|
423
490
|
}
|
package/src/config/file-lock.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Stats } from "node:fs";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
3
4
|
import { isEnoent } from "@gajae-code/utils/fs-error";
|
|
4
5
|
|
|
5
6
|
export interface FileLockOptions {
|
|
@@ -14,45 +15,93 @@ const DEFAULT_OPTIONS: Required<FileLockOptions> = {
|
|
|
14
15
|
retryDelayMs: 100,
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
type LockInfo = FileLockOwnerToken;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns the OS-provided process start timestamp for PID-reuse detection.
|
|
22
|
+
* `ps` is available on the supported Unix hosts (macOS and Linux), unlike
|
|
23
|
+
* Linux's `/proc/<pid>/stat` pseudo-file.
|
|
24
|
+
*/
|
|
25
|
+
export function processStartTime(pid: number): string | null {
|
|
26
|
+
try {
|
|
27
|
+
const result = Bun.spawnSync(["ps", "-o", "lstart=", "-p", String(pid)], { stdout: "pipe", stderr: "ignore" });
|
|
28
|
+
if (result.exitCode !== 0) return null;
|
|
29
|
+
const startTime = new TextDecoder().decode(result.stdout).trim();
|
|
30
|
+
return startTime || null;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
20
34
|
}
|
|
21
35
|
|
|
22
|
-
|
|
23
|
-
|
|
36
|
+
let ownProcessStartTime: string | undefined;
|
|
37
|
+
|
|
38
|
+
function currentProcessStartTime(): string {
|
|
39
|
+
if (ownProcessStartTime === undefined) ownProcessStartTime = processStartTime(process.pid) ?? "unknown";
|
|
40
|
+
return ownProcessStartTime;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cachedProcessStartTime(owner: FileLockOwnerToken, cache?: Map<string, string | null>): string | null {
|
|
44
|
+
if (!cache) return processStartTime(owner.pid);
|
|
45
|
+
const key = `${owner.pid}:${owner.start_time ?? ""}`;
|
|
46
|
+
const cached = cache.get(key);
|
|
47
|
+
if (cached !== undefined || cache.has(key)) return cached ?? null;
|
|
48
|
+
const startTime = processStartTime(owner.pid);
|
|
49
|
+
cache.set(key, startTime);
|
|
50
|
+
return startTime;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ownerIsAlive(owner: FileLockOwnerToken, startTimeCache?: Map<string, string | null>): boolean {
|
|
54
|
+
if (ownerLiveness(owner.pid) !== "alive") return false;
|
|
55
|
+
if (!owner.start_time) return true;
|
|
56
|
+
const currentStartTime = cachedProcessStartTime(owner, startTimeCache);
|
|
57
|
+
return currentStartTime === null || currentStartTime === owner.start_time;
|
|
24
58
|
}
|
|
25
59
|
|
|
26
|
-
|
|
27
|
-
const info: LockInfo = { pid: process.pid, timestamp: Date.now() };
|
|
28
|
-
|
|
29
|
-
return info;
|
|
60
|
+
function writeLockInfo(lockPath: string): Promise<LockInfo> {
|
|
61
|
+
const info: LockInfo = { pid: process.pid, start_time: currentProcessStartTime(), timestamp: Date.now() };
|
|
62
|
+
return Bun.write(`${lockPath}/info`, JSON.stringify(info)).then(() => info);
|
|
30
63
|
}
|
|
31
64
|
|
|
32
65
|
async function readLockInfo(lockPath: string): Promise<LockInfo | null> {
|
|
66
|
+
let parsed: unknown;
|
|
33
67
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
68
|
+
parsed = JSON.parse(await fs.readFile(`${lockPath}/info`, "utf-8"));
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (isEnoent(error) || error instanceof SyntaxError) return null;
|
|
71
|
+
throw error;
|
|
38
72
|
}
|
|
73
|
+
|
|
74
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
75
|
+
const { pid, start_time, timestamp } = parsed as Partial<LockInfo>;
|
|
76
|
+
if (
|
|
77
|
+
typeof pid !== "number" ||
|
|
78
|
+
!Number.isInteger(pid) ||
|
|
79
|
+
pid <= 0 ||
|
|
80
|
+
typeof timestamp !== "number" ||
|
|
81
|
+
!Number.isFinite(timestamp) ||
|
|
82
|
+
(start_time !== undefined && (typeof start_time !== "string" || !start_time))
|
|
83
|
+
)
|
|
84
|
+
return null;
|
|
85
|
+
return { pid, start_time, timestamp };
|
|
39
86
|
}
|
|
40
87
|
|
|
41
88
|
/** @internal */
|
|
42
|
-
export async function readFileLockInfoForGc(lockDir: string): Promise<
|
|
43
|
-
|
|
44
|
-
if (!info) return null;
|
|
45
|
-
if (!Number.isFinite(info.pid) || info.pid <= 0) return null;
|
|
46
|
-
if (!Number.isFinite(info.timestamp)) return null;
|
|
47
|
-
return info;
|
|
89
|
+
export async function readFileLockInfoForGc(lockDir: string): Promise<FileLockOwnerToken | null> {
|
|
90
|
+
return await readLockInfo(lockDir);
|
|
48
91
|
}
|
|
49
92
|
|
|
50
93
|
/** Owner identity stamped into a `<file>.lock/info` record. */
|
|
51
94
|
export interface FileLockOwnerToken {
|
|
52
95
|
pid: number;
|
|
96
|
+
start_time?: string;
|
|
97
|
+
|
|
53
98
|
timestamp: number;
|
|
54
99
|
}
|
|
55
100
|
|
|
101
|
+
function getLockPath(filePath: string): string {
|
|
102
|
+
return `${filePath}.lock`;
|
|
103
|
+
}
|
|
104
|
+
|
|
56
105
|
/** Outcome of a guarded lock-dir removal attempt (`removeFileLockDirForGc`). */
|
|
57
106
|
export type FileLockGcRemoval = "removed" | "owner_changed" | "missing";
|
|
58
107
|
|
|
@@ -91,7 +140,11 @@ export async function removeFileLockDirForGc(
|
|
|
91
140
|
): Promise<FileLockGcRemoval> {
|
|
92
141
|
const current = await readLockInfo(lockDir);
|
|
93
142
|
if (!current) return "missing";
|
|
94
|
-
if (
|
|
143
|
+
if (
|
|
144
|
+
current.pid !== expected.pid ||
|
|
145
|
+
(expected.start_time !== undefined && current.start_time !== expected.start_time) ||
|
|
146
|
+
current.timestamp !== expected.timestamp
|
|
147
|
+
) {
|
|
95
148
|
return "owner_changed";
|
|
96
149
|
}
|
|
97
150
|
await fs.rm(lockDir, { recursive: true, force: true });
|
|
@@ -127,7 +180,11 @@ function sameStatToken(a: LockDirStatToken, b: LockDirStatToken): boolean {
|
|
|
127
180
|
return a.dev === b.dev && a.ino === b.ino && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
|
|
128
181
|
}
|
|
129
182
|
|
|
130
|
-
async function staleLockSnapshot(
|
|
183
|
+
async function staleLockSnapshot(
|
|
184
|
+
lockPath: string,
|
|
185
|
+
staleMs: number,
|
|
186
|
+
startTimeCache?: Map<string, string | null>,
|
|
187
|
+
): Promise<LockStaleSnapshot> {
|
|
131
188
|
const info = await readLockInfo(lockPath);
|
|
132
189
|
if (!info) {
|
|
133
190
|
try {
|
|
@@ -143,10 +200,9 @@ async function staleLockSnapshot(lockPath: string, staleMs: number): Promise<Loc
|
|
|
143
200
|
// Never reap a live owner by elapsed time: a long legitimate critical section must
|
|
144
201
|
// not have its lock stolen (#652). Reclaim a dead owner immediately. Only when owner
|
|
145
202
|
// liveness is indeterminate do we fall back to the staleMs elapsed-time heuristic.
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
return { stale: true, owner: { pid: info.pid, timestamp: info.timestamp } };
|
|
203
|
+
if (ownerIsAlive(info, startTimeCache)) return { stale: false };
|
|
204
|
+
if (ownerLiveness(info.pid) === "dead" || Date.now() - info.timestamp > staleMs) {
|
|
205
|
+
return { stale: true, owner: info };
|
|
150
206
|
}
|
|
151
207
|
return { stale: false };
|
|
152
208
|
}
|
|
@@ -171,6 +227,7 @@ async function removeStaleLockForAcquire(lockPath: string, snapshot: LockStaleSn
|
|
|
171
227
|
}
|
|
172
228
|
|
|
173
229
|
async function tryAcquireLock(lockPath: string): Promise<LockInfo | null> {
|
|
230
|
+
await fs.mkdir(path.dirname(lockPath), { recursive: true });
|
|
174
231
|
try {
|
|
175
232
|
await fs.mkdir(lockPath);
|
|
176
233
|
return await writeLockInfo(lockPath);
|
|
@@ -183,42 +240,47 @@ async function tryAcquireLock(lockPath: string): Promise<LockInfo | null> {
|
|
|
183
240
|
}
|
|
184
241
|
|
|
185
242
|
async function releaseLock(lockPath: string, owner: FileLockOwnerToken): Promise<void> {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
} catch {
|
|
189
|
-
// Ignore errors on release
|
|
190
|
-
}
|
|
243
|
+
const outcome = await removeFileLockDirForGc(lockPath, owner);
|
|
244
|
+
if (outcome !== "removed") throw new Error(`Failed to release file lock: ${outcome}.`);
|
|
191
245
|
}
|
|
192
246
|
async function acquireLock(filePath: string, options: FileLockOptions = {}): Promise<() => Promise<void>> {
|
|
193
247
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
194
248
|
const lockPath = getLockPath(filePath);
|
|
195
|
-
|
|
249
|
+
const contentionStartTimes = new Map<string, string | null>();
|
|
196
250
|
for (let attempt = 0; attempt < opts.retries; attempt++) {
|
|
197
251
|
const owner = await tryAcquireLock(lockPath);
|
|
198
|
-
if (owner)
|
|
199
|
-
return () => releaseLock(lockPath, owner);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const stale = await staleLockSnapshot(lockPath, opts.staleMs);
|
|
203
|
-
if (await removeStaleLockForAcquire(lockPath, stale)) {
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
252
|
+
if (owner) return () => releaseLock(lockPath, owner);
|
|
206
253
|
|
|
254
|
+
const stale = await staleLockSnapshot(lockPath, opts.staleMs, contentionStartTimes);
|
|
255
|
+
if (await removeStaleLockForAcquire(lockPath, stale)) continue;
|
|
207
256
|
await Bun.sleep(opts.retryDelayMs);
|
|
208
257
|
}
|
|
209
|
-
|
|
210
258
|
throw new Error(`Failed to acquire lock for ${filePath} after ${opts.retries} attempts`);
|
|
211
259
|
}
|
|
212
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Serializes all contenders, including callers in the same process. Because this
|
|
263
|
+
* API exposes no ownership token, recursive acquisition is indistinguishable
|
|
264
|
+
* from independent async contention; code that already holds the lock must pass
|
|
265
|
+
* that fact through its own `lockHeld` path instead of acquiring it again.
|
|
266
|
+
*/
|
|
213
267
|
export async function withFileLock<T>(
|
|
214
268
|
filePath: string,
|
|
215
269
|
fn: () => Promise<T>,
|
|
216
270
|
options: FileLockOptions = {},
|
|
217
271
|
): Promise<T> {
|
|
218
272
|
const release = await acquireLock(filePath, options);
|
|
273
|
+
let result: T;
|
|
219
274
|
try {
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
|
|
275
|
+
result = await fn();
|
|
276
|
+
} catch (operationError) {
|
|
277
|
+
try {
|
|
278
|
+
await release();
|
|
279
|
+
} catch (releaseError) {
|
|
280
|
+
throw new AggregateError([operationError, releaseError], "File lock operation and release both failed.");
|
|
281
|
+
}
|
|
282
|
+
throw operationError;
|
|
223
283
|
}
|
|
284
|
+
await release();
|
|
285
|
+
return result;
|
|
224
286
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { copyFileSync, existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import {
|
|
4
4
|
type Keybinding,
|
|
@@ -41,6 +41,7 @@ interface AppKeybindings {
|
|
|
41
41
|
"app.session.fork": true;
|
|
42
42
|
"app.session.resume": true;
|
|
43
43
|
"app.session.observe": true;
|
|
44
|
+
"app.session.dashboard": true;
|
|
44
45
|
"app.jobs.open": true;
|
|
45
46
|
"app.session.togglePath": true;
|
|
46
47
|
"app.session.toggleSort": true;
|
|
@@ -53,6 +54,13 @@ interface AppKeybindings {
|
|
|
53
54
|
"app.history.search": true;
|
|
54
55
|
"app.stt.toggle": true;
|
|
55
56
|
"app.irc.sidebar.toggle": true;
|
|
57
|
+
"app.transcript.browse": true;
|
|
58
|
+
"app.transcript.prevTurn": true;
|
|
59
|
+
"app.transcript.nextTurn": true;
|
|
60
|
+
"app.mode.cycle": true;
|
|
61
|
+
"app.tasks.toggle": true;
|
|
62
|
+
"app.queue.togglePane": true;
|
|
63
|
+
"app.message.sendNow": true;
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
export type AppKeybinding = keyof AppKeybindings;
|
|
@@ -65,6 +73,10 @@ export function defaultMessageQueueKeysForPlatform(platform: NodeJS.Platform = p
|
|
|
65
73
|
return platform === "win32" || platform === "darwin" ? "alt+q" : "alt+enter";
|
|
66
74
|
}
|
|
67
75
|
|
|
76
|
+
export function defaultClipboardPasteImageKeysForPlatform(platform: NodeJS.Platform = process.platform): KeyId {
|
|
77
|
+
return platform === "win32" ? "alt+v" : "ctrl+v";
|
|
78
|
+
}
|
|
79
|
+
|
|
68
80
|
/**
|
|
69
81
|
* All keybindings definitions: TUI + app-specific.
|
|
70
82
|
*/
|
|
@@ -139,7 +151,7 @@ export const KEYBINDINGS = {
|
|
|
139
151
|
description: "Select queued message to edit",
|
|
140
152
|
},
|
|
141
153
|
"app.clipboard.pasteImage": {
|
|
142
|
-
defaultKeys:
|
|
154
|
+
defaultKeys: defaultClipboardPasteImageKeysForPlatform(),
|
|
143
155
|
description: "Paste image from clipboard",
|
|
144
156
|
},
|
|
145
157
|
"app.clipboard.copyLine": {
|
|
@@ -170,6 +182,10 @@ export const KEYBINDINGS = {
|
|
|
170
182
|
defaultKeys: "ctrl+s",
|
|
171
183
|
description: "Observe subagent sessions",
|
|
172
184
|
},
|
|
185
|
+
"app.session.dashboard": {
|
|
186
|
+
defaultKeys: [],
|
|
187
|
+
description: "Show all persisted sessions dashboard",
|
|
188
|
+
},
|
|
173
189
|
|
|
174
190
|
"app.jobs.open": {
|
|
175
191
|
defaultKeys: "alt+j",
|
|
@@ -219,6 +235,34 @@ export const KEYBINDINGS = {
|
|
|
219
235
|
defaultKeys: "alt+i",
|
|
220
236
|
description: "Toggle IRC sidebar",
|
|
221
237
|
},
|
|
238
|
+
"app.transcript.browse": {
|
|
239
|
+
defaultKeys: [],
|
|
240
|
+
description: "Browse the transcript",
|
|
241
|
+
},
|
|
242
|
+
"app.transcript.prevTurn": {
|
|
243
|
+
defaultKeys: [],
|
|
244
|
+
description: "Jump to previous user turn",
|
|
245
|
+
},
|
|
246
|
+
"app.transcript.nextTurn": {
|
|
247
|
+
defaultKeys: [],
|
|
248
|
+
description: "Jump to next user turn",
|
|
249
|
+
},
|
|
250
|
+
"app.mode.cycle": {
|
|
251
|
+
defaultKeys: [],
|
|
252
|
+
description: "Cycle interaction mode",
|
|
253
|
+
},
|
|
254
|
+
"app.tasks.toggle": {
|
|
255
|
+
defaultKeys: "alt+t",
|
|
256
|
+
description: "Toggle tasks pane",
|
|
257
|
+
},
|
|
258
|
+
"app.queue.togglePane": {
|
|
259
|
+
defaultKeys: [],
|
|
260
|
+
description: "Toggle message queue pane",
|
|
261
|
+
},
|
|
262
|
+
"app.message.sendNow": {
|
|
263
|
+
defaultKeys: [],
|
|
264
|
+
description: "Send message immediately",
|
|
265
|
+
},
|
|
222
266
|
} as const satisfies KeybindingDefinitions;
|
|
223
267
|
|
|
224
268
|
/**
|
|
@@ -304,13 +348,14 @@ function toKeybindingsConfig(value: unknown): KeybindingsConfig {
|
|
|
304
348
|
|
|
305
349
|
const config: KeybindingsConfig = {};
|
|
306
350
|
for (const [key, val] of Object.entries(value)) {
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
config[key] = val as KeyId;
|
|
311
|
-
} else if (Array.isArray(val) && val.every(v => typeof v === "string")) {
|
|
312
|
-
config[key] = val as KeyId[];
|
|
351
|
+
if (!(key in KEYBINDINGS) && !isLegacyKeybindingName(key)) {
|
|
352
|
+
logger.info("Ignoring unknown keybinding entry", { key });
|
|
353
|
+
continue;
|
|
313
354
|
}
|
|
355
|
+
if (val === undefined) config[key] = undefined;
|
|
356
|
+
else if (typeof val === "string") config[key] = val as KeyId;
|
|
357
|
+
else if (Array.isArray(val) && val.every(v => typeof v === "string")) config[key] = val as KeyId[];
|
|
358
|
+
else logger.info("Ignoring malformed keybinding entry", { key });
|
|
314
359
|
}
|
|
315
360
|
return config;
|
|
316
361
|
}
|
|
@@ -381,28 +426,27 @@ function loadRawConfig(filePath: string): unknown {
|
|
|
381
426
|
}
|
|
382
427
|
}
|
|
383
428
|
|
|
384
|
-
/**
|
|
385
|
-
* Migrate keybindings config file from old format to new.
|
|
386
|
-
* Reads from agentDir/keybindings.json, migrates old names, and writes back.
|
|
387
|
-
*/
|
|
429
|
+
/** Migrate legacy keybindings once, preserving the original file as .bak. */
|
|
388
430
|
function loadKeybindingsConfig(filePath: string, writeBack: boolean): KeybindingsConfig {
|
|
389
431
|
const rawConfig = loadRawConfig(filePath);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const { config: migratedConfig, migrated } = migrateKeybindingNames(rawConfig);
|
|
432
|
+
if (rawConfig === null) return {};
|
|
433
|
+
const markerPath = `${filePath}.migration-v1`;
|
|
434
|
+
const { config: migratedConfig, migrated } = existsSync(markerPath)
|
|
435
|
+
? { config: toKeybindingsConfig(rawConfig), migrated: false }
|
|
436
|
+
: migrateKeybindingNames(rawConfig);
|
|
396
437
|
if (writeBack && migrated) {
|
|
397
438
|
const ordered = orderKeybindingsConfig(migratedConfig);
|
|
439
|
+
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
398
440
|
try {
|
|
399
|
-
|
|
441
|
+
copyFileSync(filePath, `${filePath}.bak`);
|
|
442
|
+
writeFileSync(tempPath, `${JSON.stringify(ordered, null, 2)}\n`, "utf-8");
|
|
443
|
+
renameSync(tempPath, filePath);
|
|
444
|
+
writeFileSync(markerPath, "v1\n", "utf-8");
|
|
400
445
|
logger.debug("Migrated keybindings config", { path: filePath });
|
|
401
446
|
} catch (error) {
|
|
402
447
|
logger.warn("Failed to write migrated keybindings config", { path: filePath, error: String(error) });
|
|
403
448
|
}
|
|
404
449
|
}
|
|
405
|
-
|
|
406
450
|
return migratedConfig;
|
|
407
451
|
}
|
|
408
452
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const kNoAuth = "N/A";
|
|
2
|
+
|
|
3
|
+
export function isAuthenticated(apiKey: string | undefined | null): apiKey is string {
|
|
4
|
+
return Boolean(apiKey) && apiKey !== kNoAuth;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isAuthenticatedOrKeyless(apiKey: string | undefined | null): boolean {
|
|
8
|
+
return apiKey === kNoAuth || isAuthenticated(apiKey);
|
|
9
|
+
}
|