@gajae-code/coding-agent 0.18.11 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +760 -0
- package/dist/types/async/job-manager.d.ts +7 -3
- package/dist/types/async/result-formatting.d.ts +2 -0
- package/dist/types/autoresearch/git.d.ts +1 -1
- package/dist/types/capability/fs.d.ts +2 -2
- package/dist/types/capability/rule.d.ts +7 -2
- package/dist/types/cli/accounts-cli.d.ts +2 -2
- package/dist/types/cli/append-system-prompt.d.ts +6 -0
- package/dist/types/cli/args.d.ts +7 -2
- package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
- package/dist/types/cli/customize-doctor.d.ts +4 -3
- package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
- package/dist/types/cli/install-activation.d.ts +10 -4
- package/dist/types/cli/public-command-errors.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +0 -12
- package/dist/types/cli/update-cli.d.ts +21 -3
- package/dist/types/cli-main.d.ts +0 -6
- package/dist/types/cli-ordinary.d.ts +2 -2
- package/dist/types/commands/accounts.d.ts +2 -1
- package/dist/types/commands/launch.d.ts +0 -6
- package/dist/types/commands/migrate.d.ts +1 -1
- package/dist/types/commands/sdk.d.ts +13 -0
- package/dist/types/commit/changelog/index.d.ts +1 -0
- package/dist/types/commit/map-reduce/index.d.ts +4 -4
- package/dist/types/config/autorouting-tier-map.d.ts +53 -1
- package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
- package/dist/types/config/config-hot-reload.d.ts +44 -0
- package/dist/types/config/file-lock.d.ts +52 -3
- package/dist/types/config/model-discovery-manager.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +79 -9
- package/dist/types/config/model-registry.d.ts +72 -8
- package/dist/types/config/model-resolver.d.ts +7 -4
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/settings-schema.d.ts +57 -4
- package/dist/types/config/settings.d.ts +33 -1
- package/dist/types/coordinator-mcp/server.d.ts +4 -0
- package/dist/types/customization/import.d.ts +2 -0
- package/dist/types/customization/types.d.ts +1 -1
- package/dist/types/dap/config.d.ts +8 -4
- package/dist/types/dap/session.d.ts +11 -2
- package/dist/types/debug/system-info.d.ts +6 -1
- package/dist/types/discovery/helpers.d.ts +7 -2
- package/dist/types/discovery/mcp-compat.d.ts +3 -1
- package/dist/types/edit/index.d.ts +5 -0
- package/dist/types/edit/modes/patch.d.ts +14 -0
- package/dist/types/edit/modes/replace.d.ts +3 -0
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +12 -0
- package/dist/types/export/ttsr.d.ts +19 -0
- package/dist/types/extensibility/extensions/types.d.ts +5 -1
- package/dist/types/extensibility/slash-commands.d.ts +1 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
- package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
- package/dist/types/hindsight/content.d.ts +5 -6
- package/dist/types/internal-urls/types.d.ts +2 -0
- package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
- package/dist/types/main.d.ts +23 -18
- package/dist/types/migrate/action-planner.d.ts +10 -1
- package/dist/types/migrate/adapters/codex.d.ts +2 -1
- package/dist/types/migrate/adapters/index.d.ts +42 -1
- package/dist/types/migrate/executor.d.ts +3 -1
- package/dist/types/migrate/hook-module.d.ts +23 -0
- package/dist/types/migrate/report.d.ts +2 -1
- package/dist/types/migrate/types.d.ts +82 -3
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +43 -0
- package/dist/types/modes/components/settings-selector.d.ts +6 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
- package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/modes/utils/host-status.d.ts +15 -11
- package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
- package/dist/types/runtime-mcp/manager.d.ts +7 -0
- package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
- package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
- package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
- package/dist/types/sdk/broker/broker-exit.d.ts +3 -3
- package/dist/types/sdk/broker/broker.d.ts +29 -2
- package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
- package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
- package/dist/types/sdk/broker/ensure.d.ts +141 -9
- package/dist/types/sdk/broker/hop.d.ts +45 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
- package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
- package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
- package/dist/types/sdk/broker/session-index.d.ts +6 -1
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +44 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
- package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
- package/dist/types/sdk/cli/session-cli.d.ts +0 -1
- package/dist/types/sdk/host/control/operations.d.ts +5 -1
- package/dist/types/sdk/host/events.d.ts +4 -0
- package/dist/types/sdk/host/host.d.ts +2 -1
- package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
- package/dist/types/sdk/host/session-runtime.d.ts +40 -8
- package/dist/types/sdk/host/surface-policy.d.ts +2 -1
- package/dist/types/sdk/lifecycle/service.d.ts +12 -0
- package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
- package/dist/types/sdk/prompt-status.d.ts +11 -4
- package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
- package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
- package/dist/types/sdk/session-list.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +107 -9
- package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
- package/dist/types/session/messages.d.ts +5 -1
- package/dist/types/session/session-manager.d.ts +29 -3
- package/dist/types/session/startup-auth-config.d.ts +24 -2
- package/dist/types/session/streaming-output.d.ts +11 -0
- package/dist/types/session/terminal-abort.d.ts +3 -1
- package/dist/types/session-import/service.d.ts +1 -1
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +20 -4
- package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
- package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
- package/dist/types/ssh/ssh-executor.d.ts +8 -1
- package/dist/types/system-prompt.d.ts +3 -2
- package/dist/types/task/executor.d.ts +19 -2
- package/dist/types/task/role-body.d.ts +11 -0
- package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
- package/dist/types/tools/atomic-file-write.d.ts +9 -7
- package/dist/types/tools/awaiting-input.d.ts +28 -0
- package/dist/types/tools/bash-allowed-prefixes.d.ts +17 -0
- package/dist/types/tools/bash-skill-urls.d.ts +4 -0
- package/dist/types/tools/bash.d.ts +7 -0
- package/dist/types/tools/descriptors.d.ts +15 -2
- package/dist/types/tools/fetch.d.ts +7 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/irc.d.ts +5 -3
- package/dist/types/tools/python.d.ts +1 -1
- package/dist/types/tools/report-issue.d.ts +41 -0
- package/dist/types/tools/subagent.d.ts +23 -0
- package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/file-mentions.d.ts +5 -0
- package/dist/types/utils/image-limits.d.ts +7 -0
- package/dist/types/utils/image-loading.d.ts +0 -1
- package/dist/types/utils/open.d.ts +0 -1
- package/dist/types/utils/pasted-image-loading.d.ts +1 -5
- package/dist/types/utils/prompt-suggestion.d.ts +2 -2
- package/dist/types/utils/shell-snapshot.d.ts +2 -0
- package/dist/types/utils/title-generator.d.ts +13 -3
- package/dist/types/web/scrapers/types.d.ts +8 -1
- package/dist/types/web/search/providers/codex.d.ts +6 -7
- package/dist/types/web/search/providers/insane.d.ts +7 -1
- package/dist/types/web/search/providers/searxng.d.ts +5 -0
- package/package.json +10 -9
- package/scripts/acp-conformance-agent.ts +12 -2
- package/scripts/build-binary.ts +2 -1
- package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
- package/scripts/generate-docs-index.ts +8 -1
- package/scripts/generate-sdk-operation-inventory.ts +38 -4
- package/src/async/job-manager.ts +356 -56
- package/src/async/result-formatting.ts +40 -0
- package/src/autoresearch/git.ts +9 -2
- package/src/autoresearch/prompts/prompt-setup.md +1 -1
- package/src/capability/fs.ts +58 -22
- package/src/capability/index.ts +2 -2
- package/src/capability/rule.ts +11 -2
- package/src/cli/accounts-cli.ts +176 -8
- package/src/cli/append-system-prompt.ts +12 -0
- package/src/cli/args.ts +11 -7
- package/src/cli/auth-broker-cli.ts +21 -11
- package/src/cli/customize-doctor-precedence.ts +31 -0
- package/src/cli/customize-doctor.ts +49 -39
- package/src/cli/doctor/install-repairs.ts +35 -5
- package/src/cli/fast-help.ts +1 -3
- package/src/cli/install-activation.ts +47 -4
- package/src/cli/migrate-cli.ts +19 -6
- package/src/cli/public-command-errors.ts +6 -0
- package/src/cli/root-flags.ts +2 -4
- package/src/cli/stats-cli.ts +9 -5
- package/src/cli/update-cli.ts +471 -13
- package/src/cli-ordinary.ts +7 -2
- package/src/commands/accounts.ts +10 -5
- package/src/commands/harness.ts +25 -15
- package/src/commands/migrate.ts +3 -3
- package/src/commands/sdk.ts +277 -50
- package/src/commands/ultragoal.ts +16 -8
- package/src/commit/agentic/agent.ts +6 -6
- package/src/commit/analysis/conventional.ts +14 -5
- package/src/commit/changelog/index.ts +39 -1
- package/src/commit/map-reduce/index.ts +7 -7
- package/src/commit/message.ts +10 -3
- package/src/commit/pipeline.ts +12 -12
- package/src/config/autorouting-tier-map.ts +50 -1
- package/src/config/config-file.ts +106 -3
- package/src/config/config-hot-reload-trust.ts +148 -0
- package/src/config/config-hot-reload.ts +742 -0
- package/src/config/file-lock.ts +1615 -328
- package/src/config/keybindings.ts +154 -7
- package/src/config/model-bindings-applier.ts +85 -47
- package/src/config/model-discovery-manager.ts +10 -0
- package/src/config/model-profile-activation.ts +889 -148
- package/src/config/model-registry.ts +1706 -205
- package/src/config/model-resolver.ts +52 -22
- package/src/config/models-config-schema.ts +6 -0
- package/src/config/settings-schema.ts +73 -4
- package/src/config/settings.ts +460 -24
- package/src/coordinator-mcp/policy.ts +26 -1
- package/src/coordinator-mcp/server.ts +37 -2
- package/src/crash/sanitize.ts +7 -1
- package/src/customization/import.ts +279 -8
- package/src/customization/inventory.ts +24 -8
- package/src/customization/types.ts +1 -0
- package/src/dap/client.ts +3 -2
- package/src/dap/config.ts +31 -11
- package/src/dap/defaults.json +1 -1
- package/src/dap/session.ts +161 -36
- package/src/debug/crash-diagnostics.ts +89 -6
- package/src/debug/report-bundle.ts +90 -2
- package/src/debug/system-info.ts +8 -3
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
- package/src/discovery/agents-md.ts +1 -1
- package/src/discovery/builtin.ts +4 -3
- package/src/discovery/helpers.ts +25 -16
- package/src/discovery/mcp-compat.ts +120 -17
- package/src/edit/apply-patch/index.ts +12 -1
- package/src/edit/index.ts +204 -22
- package/src/edit/modes/patch.ts +214 -8
- package/src/edit/modes/replace.ts +136 -0
- package/src/eval/js/context-manager.ts +29 -3
- package/src/eval/js/worker-entry.ts +51 -18
- package/src/eval/py/executor.ts +14 -4
- package/src/eval/py/runner.py +144 -5
- package/src/eval/py/runtime.ts +44 -4
- package/src/exec/bash-executor.ts +81 -4
- package/src/exec/bash-shell-guardian.ts +4 -1
- package/src/exec/bash-shell-supervisor.ts +8 -4
- package/src/exec/isolated-shell.ts +5 -1
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/export/html/index.ts +86 -6
- package/src/export/ttsr.ts +176 -1
- package/src/extensibility/custom-tools/wrapper.ts +3 -1
- package/src/extensibility/extensions/loader.ts +10 -1
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
- package/src/extensibility/extensions/types.ts +5 -1
- package/src/extensibility/hooks/loader.ts +10 -2
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
- package/src/extensibility/plugins/marketplace/manager.ts +36 -10
- package/src/extensibility/slash-commands.ts +5 -0
- package/src/gjc-runtime/gc-runtime.ts +37 -7
- package/src/gjc-runtime/launch-tmux.ts +30 -15
- package/src/gjc-runtime/launch-worktree.test.ts +148 -0
- package/src/gjc-runtime/launch-worktree.ts +101 -14
- package/src/gjc-runtime/managed-owner-admission.ts +46 -2
- package/src/gjc-runtime/ralplan-current-final.ts +294 -0
- package/src/gjc-runtime/ralplan-runtime.ts +95 -1
- package/src/gjc-runtime/session-resolution.ts +38 -6
- package/src/gjc-runtime/session-state-lock.ts +59 -14
- package/src/gjc-runtime/state-runtime.ts +23 -19
- package/src/gjc-runtime/state-writer.ts +109 -5
- package/src/gjc-runtime/tmux-sessions.ts +93 -13
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
- package/src/gjc-runtime/ultragoal-guard.ts +9 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
- package/src/harness-control-plane/finalize.ts +11 -0
- package/src/harness-control-plane/receipt-spool.ts +105 -28
- package/src/harness-control-plane/storage.ts +124 -6
- package/src/hindsight/content.ts +116 -16
- package/src/hindsight/mental-models.ts +22 -10
- package/src/hindsight/state.ts +7 -4
- package/src/hooks/native-skill-hook.ts +17 -12
- package/src/internal-urls/artifact-protocol.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +23 -22
- package/src/internal-urls/local-protocol.ts +167 -8
- package/src/internal-urls/types.ts +2 -0
- package/src/lsp/client.ts +2 -1
- package/src/lsp/clients/biome-client.ts +2 -0
- package/src/lsp/clients/swiftlint-client.ts +2 -0
- package/src/lsp/index.ts +29 -0
- package/src/lsp/workspace-path.ts +23 -0
- package/src/main.ts +330 -92
- package/src/memories/index.ts +25 -2
- package/src/migrate/action-planner.ts +574 -2
- package/src/migrate/adapters/claude-code.ts +162 -5
- package/src/migrate/adapters/codex.ts +77 -7
- package/src/migrate/adapters/index.ts +258 -5
- package/src/migrate/adapters/opencode.ts +10 -1
- package/src/migrate/executor.ts +222 -7
- package/src/migrate/hook-module.ts +193 -0
- package/src/migrate/report.ts +63 -2
- package/src/migrate/types.ts +84 -4
- package/src/modes/DESIGN.md +51 -0
- package/src/modes/acp/acp-agent.ts +10 -1
- package/src/modes/acp/permission-mode.ts +18 -1
- package/src/modes/components/agent-dashboard.ts +20 -4
- package/src/modes/components/assistant-message.ts +55 -1
- package/src/modes/components/customization/customization-dashboard.ts +7 -0
- package/src/modes/components/customization/import-wizard.ts +29 -3
- package/src/modes/components/model-selector.ts +157 -103
- package/src/modes/components/settings-selector.ts +103 -37
- package/src/modes/components/welcome.ts +3 -1
- package/src/modes/controllers/command-controller.ts +300 -9
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/controllers/extension-ui-controller.ts +31 -1
- package/src/modes/controllers/goal-mode-controller.ts +79 -10
- package/src/modes/controllers/input-controller.ts +2 -0
- package/src/modes/controllers/plan-mode-controller.ts +66 -11
- package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
- package/src/modes/controllers/selector-controller.ts +76 -7
- package/src/modes/interactive-mode.ts +123 -4
- package/src/modes/prompt-suggestion-controller.ts +1 -1
- package/src/modes/runtime-init.ts +5 -1
- package/src/modes/theme/theme.ts +15 -0
- package/src/modes/utils/host-status.ts +21 -10
- package/src/prompts/agent-fragments/browser-aside.md +2 -2
- package/src/prompts/system/irc-incoming.md +3 -1
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/report-issue.md +13 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-credential-selector.ts +2 -2
- package/src/runtime-mcp/content-limits.ts +7 -0
- package/src/runtime-mcp/manager-pool.test.ts +5 -4
- package/src/runtime-mcp/manager.ts +40 -3
- package/src/runtime-mcp/oauth-flow.ts +32 -6
- package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
- package/src/runtime-mcp/project-host-values.ts +12 -8
- package/src/runtime-mcp/render.ts +16 -1
- package/src/runtime-mcp/tool-bridge.ts +194 -21
- package/src/runtime-mcp/transports/stdio.ts +2 -1
- package/src/sdk/broker/broker-exit.ts +10 -5
- package/src/sdk/broker/broker.ts +484 -85
- package/src/sdk/broker/daemon-entry.ts +338 -116
- package/src/sdk/broker/doctor-restart.ts +2 -1
- package/src/sdk/broker/ensure.ts +1214 -268
- package/src/sdk/broker/hop.ts +286 -0
- package/src/sdk/broker/lifecycle-ledger.ts +38 -11
- package/src/sdk/broker/lifecycle.ts +102 -6
- package/src/sdk/broker/lock-host-identity.ts +29 -0
- package/src/sdk/broker/session-index.ts +91 -16
- package/src/sdk/broker/transport.ts +17 -1
- package/src/sdk/bus/chat-command-policy.ts +16 -3
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +460 -35
- package/src/sdk/bus/reconciliation-store.ts +201 -8
- package/src/sdk/bus/session-control.ts +2 -0
- package/src/sdk/bus/slack-daemon.ts +105 -9
- package/src/sdk/bus/telegram-daemon-contract.ts +9 -1
- package/src/sdk/bus/telegram-daemon.ts +10 -2
- package/src/sdk/bus/threaded-render.ts +20 -1
- package/src/sdk/cli/session-cli.ts +23 -6
- package/src/sdk/host/control/dispatch.ts +13 -1
- package/src/sdk/host/control/operations.ts +10 -1
- package/src/sdk/host/events.ts +67 -4
- package/src/sdk/host/host.ts +8 -2
- package/src/sdk/host/prompt-image-upload.ts +438 -0
- package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
- package/src/sdk/host/session-runtime.test.ts +1737 -129
- package/src/sdk/host/session-runtime.ts +684 -83
- package/src/sdk/host/surface-policy.ts +4 -0
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
- package/src/sdk/host/websocket-transport.ts +19 -7
- package/src/sdk/lifecycle/service.ts +15 -1
- package/src/sdk/prompt-deadline-flush.ts +78 -5
- package/src/sdk/prompt-deadline-manager.ts +68 -14
- package/src/sdk/prompt-status.ts +6 -4
- package/src/sdk/protocol/adapter-validation.ts +81 -12
- package/src/sdk/protocol/operation-inventory.generated.json +261 -2
- package/src/sdk/protocol/operation-registry.ts +33 -3
- package/src/sdk/reconciliation-extensions.ts +1 -0
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +237 -97
- package/src/session/agent-session.ts +2726 -479
- package/src/session/history-storage.ts +182 -12
- package/src/session/internal/managed-session-scope.ts +173 -57
- package/src/session/internal/managed-session-storage.ts +85 -12
- package/src/session/internal/native-publish-outcome.ts +5 -1
- package/src/session/messages.ts +47 -6
- package/src/session/session-manager.ts +326 -19
- package/src/session/startup-auth-config.ts +116 -8
- package/src/session/streaming-output.ts +262 -4
- package/src/session/terminal-abort.ts +14 -2
- package/src/session-import/redact.ts +53 -2
- package/src/session-import/service.ts +1 -0
- package/src/skill-state/workflow-mutation-guard.ts +2145 -206
- package/src/slash-commands/builtin-registry.ts +21 -6
- package/src/slash-commands/helpers/credential-switch.ts +10 -5
- package/src/slash-commands/helpers/mcp.ts +11 -3
- package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
- package/src/ssh/ssh-executor.ts +9 -2
- package/src/stt/recorder.ts +26 -9
- package/src/system-prompt.ts +61 -16
- package/src/task/executor.ts +115 -20
- package/src/task/index.ts +389 -175
- package/src/task/role-body.ts +84 -0
- package/src/task/token-log-mode.test.ts +162 -0
- package/src/task/token-log.ts +91 -2
- package/src/tools/archive-reader.ts +650 -37
- package/src/tools/ask.ts +1 -0
- package/src/tools/ast-edit-write-path.ts +64 -0
- package/src/tools/ast-edit.ts +18 -1
- package/src/tools/ast-grep.ts +7 -2
- package/src/tools/atomic-file-write.ts +46 -15
- package/src/tools/auto-generated-guard.ts +15 -3
- package/src/tools/awaiting-input.ts +114 -0
- package/src/tools/bash-allowed-prefixes.ts +214 -6
- package/src/tools/bash-interactive.ts +14 -2
- package/src/tools/bash-skill-urls.ts +58 -5
- package/src/tools/bash.ts +113 -26
- package/src/tools/browser/tab-worker.ts +39 -13
- package/src/tools/browser.ts +17 -7
- package/src/tools/cron.ts +10 -1
- package/src/tools/debug.ts +16 -8
- package/src/tools/descriptors.test.ts +44 -0
- package/src/tools/descriptors.ts +50 -6
- package/src/tools/eval.ts +15 -0
- package/src/tools/fetch.ts +93 -4
- package/src/tools/gh.ts +27 -12
- package/src/tools/image-gen.ts +20 -6
- package/src/tools/index.ts +2 -0
- package/src/tools/irc.ts +5 -3
- package/src/tools/monitor.ts +9 -1
- package/src/tools/output-meta.ts +74 -5
- package/src/tools/python.ts +1 -1
- package/src/tools/read.ts +26 -37
- package/src/tools/recipe/runner.ts +23 -12
- package/src/tools/render-utils.ts +2 -2
- package/src/tools/report-issue.ts +184 -0
- package/src/tools/search.ts +3 -2
- package/src/tools/ssh.ts +274 -8
- package/src/tools/subagent-render.ts +114 -12
- package/src/tools/subagent.ts +118 -5
- package/src/tools/tmux-self-injection-guard.ts +3301 -122
- package/src/tools/todo-write.ts +41 -3
- package/src/tools/tool-catalog.generated.ts +32 -4
- package/src/tools/ultragoal-ask-guard.ts +13 -1
- package/src/tools/write.ts +42 -3
- package/src/tui/tree-list.ts +15 -1
- package/src/utils/commit-message-generator.ts +12 -4
- package/src/utils/external-editor.ts +21 -7
- package/src/utils/file-mentions.ts +75 -7
- package/src/utils/git.ts +38 -7
- package/src/utils/image-limits.ts +8 -0
- package/src/utils/image-loading.ts +1 -1
- package/src/utils/open.ts +73 -4
- package/src/utils/pasted-image-loading.ts +9 -8
- package/src/utils/prompt-suggestion.ts +5 -4
- package/src/utils/shell-snapshot.ts +9 -1
- package/src/utils/title-generator.ts +72 -22
- package/src/web/scrapers/index.ts +3 -1
- package/src/web/scrapers/types.ts +8 -0
- package/src/web/scrapers/youtube.ts +3 -2
- package/src/web/search/index.ts +2 -1
- package/src/web/search/providers/codex.ts +29 -32
- package/src/web/search/providers/insane.ts +91 -28
- package/src/web/search/providers/searxng.ts +62 -10
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate the exact current ralplan final that an execution approval may target.
|
|
3
|
+
*
|
|
4
|
+
* The Ultragoal ask guard blocks every `ask` while a goal is incomplete or
|
|
5
|
+
* review-blocked, which is correct for ordinary questions but also deadlocks the
|
|
6
|
+
* one question that can legitimately unblock the work: the execution approval for
|
|
7
|
+
* an already-reviewed ralplan final (issue #6578). Granting that route needs proof
|
|
8
|
+
* that the final being approved is the current one and that its reviewers really
|
|
9
|
+
* passed it — never a caller-declared tag.
|
|
10
|
+
*
|
|
11
|
+
* The proof is reconstructed from what ralplan already persists:
|
|
12
|
+
*
|
|
13
|
+
* - `<session plans>/ralplan/<run-id>/index.jsonl` rows `{stage, stage_n, path, sha256}`
|
|
14
|
+
* - the artifact files those rows name, re-hashed here
|
|
15
|
+
* - the run state (`<session state>/ralplan-state.json`) for run id and phase
|
|
16
|
+
* - the resolved `autoHandoff` target, which must be `off`
|
|
17
|
+
*
|
|
18
|
+
* Anything missing, stale, rewritten, out of order, or outside this session's run
|
|
19
|
+
* directory is a typed refusal. This module only reads and hashes; it never
|
|
20
|
+
* mutates state and never grants anything by itself.
|
|
21
|
+
*/
|
|
22
|
+
import * as fs from "node:fs/promises";
|
|
23
|
+
import * as path from "node:path";
|
|
24
|
+
import { isEnoent } from "@gajae-code/utils";
|
|
25
|
+
import { resolveRalplanAutoHandoff } from "./ralplan-runtime";
|
|
26
|
+
import { modeStatePath, sessionPlansDir } from "./session-layout";
|
|
27
|
+
|
|
28
|
+
/** Stages that open a consensus iteration; a final is only valid after one. */
|
|
29
|
+
const ITERATION_OPENER_STAGES: ReadonlySet<string> = new Set(["planner", "revision"]);
|
|
30
|
+
|
|
31
|
+
/** Reviewer stages whose verdicts must join the same iteration as the final. */
|
|
32
|
+
const REVIEW_LANE_STAGES: readonly string[] = ["architect", "critic"];
|
|
33
|
+
|
|
34
|
+
export interface RalplanIndexRow {
|
|
35
|
+
stage: string;
|
|
36
|
+
stageN: number;
|
|
37
|
+
path: string;
|
|
38
|
+
sha256: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RalplanCurrentFinalDescriptor {
|
|
42
|
+
/** Run id the final belongs to. */
|
|
43
|
+
runId: string;
|
|
44
|
+
/** Absolute path of the final artifact. */
|
|
45
|
+
finalPath: string;
|
|
46
|
+
/** sha256 recorded for the final and re-verified against its bytes. */
|
|
47
|
+
finalSha256: string;
|
|
48
|
+
/** sha256 of the iteration-opening planner (or revision) artifact. */
|
|
49
|
+
plannerSha256: string;
|
|
50
|
+
/** Reviewer verdict hashes that joined this iteration. */
|
|
51
|
+
reviewerSha256: Record<string, string>;
|
|
52
|
+
/** Session that owns the run; a foreign session can never be granted. */
|
|
53
|
+
sessionId: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type RalplanCurrentFinalRefusalCode =
|
|
57
|
+
| "missing_state"
|
|
58
|
+
| "run_not_final"
|
|
59
|
+
| "auto_handoff_enabled"
|
|
60
|
+
| "missing_index"
|
|
61
|
+
| "malformed_index"
|
|
62
|
+
| "no_iteration_opener"
|
|
63
|
+
| "final_not_last"
|
|
64
|
+
| "missing_reviewers"
|
|
65
|
+
| "artifact_missing"
|
|
66
|
+
| "artifact_outside_run"
|
|
67
|
+
| "hash_mismatch";
|
|
68
|
+
|
|
69
|
+
export interface RalplanCurrentFinalRefusal {
|
|
70
|
+
code: RalplanCurrentFinalRefusalCode;
|
|
71
|
+
/** Human-readable reason suitable for a guard error message. */
|
|
72
|
+
reason: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type RalplanCurrentFinalResolution =
|
|
76
|
+
| { ok: true; descriptor: RalplanCurrentFinalDescriptor }
|
|
77
|
+
| { ok: false; refusal: RalplanCurrentFinalRefusal };
|
|
78
|
+
|
|
79
|
+
export interface RalplanCurrentFinalInput {
|
|
80
|
+
cwd: string;
|
|
81
|
+
/** Caller session id; the run must belong to it. */
|
|
82
|
+
sessionId: string;
|
|
83
|
+
/** Optional agent dir for settings resolution. */
|
|
84
|
+
agentDir?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Whether the pending `ask` payload selects the ralplan execution-approval gate.
|
|
89
|
+
*
|
|
90
|
+
* This is a selector, not authority: it only decides which validation runs. The
|
|
91
|
+
* descriptor it leads to is the proof.
|
|
92
|
+
*/
|
|
93
|
+
export function selectsRalplanApprovalGate(pendingQuestions: unknown): boolean {
|
|
94
|
+
const questions = (pendingQuestions as { questions?: unknown } | undefined)?.questions;
|
|
95
|
+
if (!Array.isArray(questions)) return false;
|
|
96
|
+
return questions.some(question => {
|
|
97
|
+
if (!question || typeof question !== "object" || Array.isArray(question)) return false;
|
|
98
|
+
const gate = (question as { workflowGate?: unknown }).workflowGate;
|
|
99
|
+
if (!gate || typeof gate !== "object" || Array.isArray(gate)) return false;
|
|
100
|
+
const record = gate as { stage?: unknown; kind?: unknown };
|
|
101
|
+
return record.stage === "ralplan" && record.kind === "approval";
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function refusal(code: RalplanCurrentFinalRefusalCode, reason: string): RalplanCurrentFinalRefusal {
|
|
106
|
+
return { code, reason };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parseIndexRow(line: string): RalplanIndexRow | undefined {
|
|
110
|
+
const trimmed = line.trim();
|
|
111
|
+
if (!trimmed) return undefined;
|
|
112
|
+
let parsed: unknown;
|
|
113
|
+
try {
|
|
114
|
+
parsed = JSON.parse(trimmed);
|
|
115
|
+
} catch {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
119
|
+
const record = parsed as Record<string, unknown>;
|
|
120
|
+
const { stage, path: artifactPath, sha256 } = record;
|
|
121
|
+
const stageN = record.stage_n;
|
|
122
|
+
if (typeof stage !== "string" || !stage.trim()) return undefined;
|
|
123
|
+
if (typeof stageN !== "number" || !Number.isInteger(stageN) || stageN <= 0) return undefined;
|
|
124
|
+
if (typeof artifactPath !== "string" || !artifactPath.trim()) return undefined;
|
|
125
|
+
if (typeof sha256 !== "string" || !/^[0-9a-f]{64}$/i.test(sha256)) return undefined;
|
|
126
|
+
return { stage: stage.trim(), stageN, path: artifactPath.trim(), sha256: sha256.toLowerCase() };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function readRunState(statePath: string): Promise<{ runId: string; phase: string } | RalplanCurrentFinalRefusal> {
|
|
130
|
+
let text: string;
|
|
131
|
+
try {
|
|
132
|
+
text = await fs.readFile(statePath, "utf-8");
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (isEnoent(error)) return refusal("missing_state", `no ralplan run state at ${statePath}`);
|
|
135
|
+
return refusal("missing_state", `failed to read ralplan run state at ${statePath}`);
|
|
136
|
+
}
|
|
137
|
+
let parsed: unknown;
|
|
138
|
+
try {
|
|
139
|
+
parsed = JSON.parse(text);
|
|
140
|
+
} catch {
|
|
141
|
+
return refusal("malformed_index", `ralplan run state at ${statePath} is not valid JSON`);
|
|
142
|
+
}
|
|
143
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
144
|
+
return refusal("malformed_index", `ralplan run state at ${statePath} is not an object`);
|
|
145
|
+
}
|
|
146
|
+
const record = parsed as Record<string, unknown>;
|
|
147
|
+
const runId = typeof record.run_id === "string" ? record.run_id.trim() : "";
|
|
148
|
+
const phase = typeof record.current_phase === "string" ? record.current_phase.trim().toLowerCase() : "";
|
|
149
|
+
if (!runId) return refusal("malformed_index", `ralplan run state at ${statePath} has no run_id`);
|
|
150
|
+
return { runId, phase };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Resolve and verify the current ralplan final for `sessionId`.
|
|
155
|
+
*
|
|
156
|
+
* Fails closed: every check returns a refusal instead of degrading to a weaker
|
|
157
|
+
* proof, because the caller uses a success to lift the Ultragoal ask guard.
|
|
158
|
+
*/
|
|
159
|
+
export async function resolveRalplanCurrentFinal(
|
|
160
|
+
input: RalplanCurrentFinalInput,
|
|
161
|
+
): Promise<RalplanCurrentFinalResolution> {
|
|
162
|
+
const sessionId = input.sessionId.trim();
|
|
163
|
+
if (!sessionId) return { ok: false, refusal: refusal("missing_state", "no caller session id") };
|
|
164
|
+
|
|
165
|
+
const state = await readRunState(modeStatePath(input.cwd, sessionId, "ralplan"));
|
|
166
|
+
if (!("runId" in state)) return { ok: false, refusal: state };
|
|
167
|
+
if (state.phase !== "final" && state.phase !== "handoff") {
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
refusal: refusal("run_not_final", `ralplan run ${state.runId} is in phase "${state.phase}", not final`),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// A final that would hand off automatically is not waiting for approval.
|
|
175
|
+
const autoHandoff = await resolveRalplanAutoHandoff(input.cwd, { agentDir: input.agentDir });
|
|
176
|
+
if (autoHandoff.effectiveTarget !== "off") {
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
refusal: refusal(
|
|
180
|
+
"auto_handoff_enabled",
|
|
181
|
+
`ralplan autoHandoff is "${autoHandoff.effectiveTarget}"; a current final is only approvable while handoff is off`,
|
|
182
|
+
),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const runDir = path.join(sessionPlansDir(input.cwd, sessionId), "ralplan", state.runId);
|
|
187
|
+
const indexPath = path.join(runDir, "index.jsonl");
|
|
188
|
+
let indexText: string;
|
|
189
|
+
try {
|
|
190
|
+
indexText = await fs.readFile(indexPath, "utf-8");
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (isEnoent(error)) return { ok: false, refusal: refusal("missing_index", `no ralplan index at ${indexPath}`) };
|
|
193
|
+
return { ok: false, refusal: refusal("missing_index", `failed to read ralplan index at ${indexPath}`) };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const rows: RalplanIndexRow[] = [];
|
|
197
|
+
for (const line of indexText.split("\n")) {
|
|
198
|
+
if (!line.trim()) continue;
|
|
199
|
+
const row = parseIndexRow(line);
|
|
200
|
+
if (!row) {
|
|
201
|
+
return { ok: false, refusal: refusal("malformed_index", `malformed ralplan index row in ${indexPath}`) };
|
|
202
|
+
}
|
|
203
|
+
rows.push(row);
|
|
204
|
+
}
|
|
205
|
+
if (rows.length === 0)
|
|
206
|
+
return { ok: false, refusal: refusal("missing_index", `ralplan index ${indexPath} is empty`) };
|
|
207
|
+
|
|
208
|
+
const finalRow = rows.reduce((latest, row) => (row.stageN > latest.stageN ? row : latest));
|
|
209
|
+
if (finalRow.stage !== "final") {
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
refusal: refusal("final_not_last", `the newest ralplan stage is "${finalRow.stage}", not "final"`),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const earlierRows = rows.filter(row => row.stageN < finalRow.stageN);
|
|
217
|
+
const opener = earlierRows
|
|
218
|
+
.filter(row => ITERATION_OPENER_STAGES.has(row.stage))
|
|
219
|
+
.reduce<RalplanIndexRow | undefined>(
|
|
220
|
+
(latest, row) => (!latest || row.stageN > latest.stageN ? row : latest),
|
|
221
|
+
undefined,
|
|
222
|
+
);
|
|
223
|
+
if (!opener) {
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
refusal: refusal("no_iteration_opener", "the final has no planner or revision stage before it"),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const reviewers: Record<string, string> = {};
|
|
231
|
+
for (const lane of REVIEW_LANE_STAGES) {
|
|
232
|
+
const laneRow = earlierRows
|
|
233
|
+
.filter(row => row.stage === lane && row.stageN > opener.stageN)
|
|
234
|
+
.reduce<RalplanIndexRow | undefined>(
|
|
235
|
+
(latest, row) => (!latest || row.stageN > latest.stageN ? row : latest),
|
|
236
|
+
undefined,
|
|
237
|
+
);
|
|
238
|
+
if (!laneRow) {
|
|
239
|
+
return {
|
|
240
|
+
ok: false,
|
|
241
|
+
refusal: refusal(
|
|
242
|
+
"missing_reviewers",
|
|
243
|
+
`no ${lane} verdict between the ${opener.stage} stage and the final; the final is not reviewed`,
|
|
244
|
+
),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
reviewers[lane] = laneRow.sha256;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Hash every referenced artifact and require it to live inside this run.
|
|
251
|
+
for (const row of rows) {
|
|
252
|
+
const resolved = path.resolve(row.path);
|
|
253
|
+
const relative = path.relative(runDir, resolved);
|
|
254
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
255
|
+
return {
|
|
256
|
+
ok: false,
|
|
257
|
+
refusal: refusal("artifact_outside_run", `ralplan stage artifact ${row.path} is outside ${runDir}`),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
let bytes: Buffer;
|
|
261
|
+
try {
|
|
262
|
+
bytes = await Bun.file(resolved)
|
|
263
|
+
.arrayBuffer()
|
|
264
|
+
.then(buffer => Buffer.from(buffer));
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (isEnoent(error)) {
|
|
267
|
+
return { ok: false, refusal: refusal("artifact_missing", `ralplan stage artifact ${row.path} is missing`) };
|
|
268
|
+
}
|
|
269
|
+
return { ok: false, refusal: refusal("artifact_missing", `cannot read ralplan stage artifact ${row.path}`) };
|
|
270
|
+
}
|
|
271
|
+
const actual = new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
|
|
272
|
+
if (actual !== row.sha256) {
|
|
273
|
+
return {
|
|
274
|
+
ok: false,
|
|
275
|
+
refusal: refusal(
|
|
276
|
+
"hash_mismatch",
|
|
277
|
+
`ralplan stage artifact ${path.basename(row.path)} does not match its recorded hash`,
|
|
278
|
+
),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
ok: true,
|
|
285
|
+
descriptor: {
|
|
286
|
+
runId: state.runId,
|
|
287
|
+
finalPath: path.resolve(finalRow.path),
|
|
288
|
+
finalSha256: finalRow.sha256,
|
|
289
|
+
plannerSha256: opener.sha256,
|
|
290
|
+
reviewerSha256: reviewers,
|
|
291
|
+
sessionId,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
@@ -4,6 +4,7 @@ import * as fssync from "node:fs";
|
|
|
4
4
|
import * as fs from "node:fs/promises";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { isSettingsInitialized, Settings } from "../config/settings";
|
|
7
|
+
import { sanitizeImportedString } from "../session-import/redact";
|
|
7
8
|
import { syncSkillActiveState } from "../skill-state/active-state";
|
|
8
9
|
import { buildRalplanHudSummary } from "../skill-state/workflow-hud";
|
|
9
10
|
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
@@ -2319,6 +2320,97 @@ function buildIndexedReviewArtifacts(indexText: string | undefined): Map<string,
|
|
|
2319
2320
|
return map;
|
|
2320
2321
|
}
|
|
2321
2322
|
|
|
2323
|
+
function scrubImported(value: string): string {
|
|
2324
|
+
return sanitizeImportedString(value);
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
/** Scrub, then trim the same way the disposition parser does. An empty result is not an id. */
|
|
2328
|
+
function canonicalDispositionId(value: string): string {
|
|
2329
|
+
const canonical = scrubImported(value).trim();
|
|
2330
|
+
if (canonical === "") {
|
|
2331
|
+
throw new RalplanCommandError(
|
|
2332
|
+
2,
|
|
2333
|
+
"refusing to persist ralplan disposition: credential scrub removed an identifier",
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
return canonical;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
/**
|
|
2340
|
+
* Scrub a validated disposition, then rebuild conflict ids from the scrubbed
|
|
2341
|
+
* finding and target ids. Only `sourceReceipt.path` and `sourceReceipt.sha256`
|
|
2342
|
+
* stay literal. A result that no longer parses is refused instead of stored.
|
|
2343
|
+
*/
|
|
2344
|
+
function scrubDispositionDocument(normalized: string): string {
|
|
2345
|
+
const doc = parseReviewConflictDocument(normalized);
|
|
2346
|
+
const findingIds = new Map(
|
|
2347
|
+
doc.findings.map(finding => [finding.findingId, canonicalDispositionId(finding.findingId)]),
|
|
2348
|
+
);
|
|
2349
|
+
const targetIds = new Map<string, string>();
|
|
2350
|
+
const originalsByScrubbedTarget = new Map<string, string>();
|
|
2351
|
+
for (const finding of doc.findings) {
|
|
2352
|
+
const canonical = canonicalDispositionId(finding.targetId);
|
|
2353
|
+
const prior = originalsByScrubbedTarget.get(canonical);
|
|
2354
|
+
if (prior !== undefined && prior !== finding.targetId) {
|
|
2355
|
+
throw new RalplanCommandError(
|
|
2356
|
+
2,
|
|
2357
|
+
"refusing to persist ralplan disposition: distinct target ids collapse after credential scrub",
|
|
2358
|
+
);
|
|
2359
|
+
}
|
|
2360
|
+
originalsByScrubbedTarget.set(canonical, finding.targetId);
|
|
2361
|
+
targetIds.set(finding.targetId, canonical);
|
|
2362
|
+
}
|
|
2363
|
+
const priorConflicts = doc.conflicts.map(conflict => ({
|
|
2364
|
+
conflictId: conflict.conflictId,
|
|
2365
|
+
targetId: conflict.targetId,
|
|
2366
|
+
findingIds: [...conflict.findingIds] as [string, string],
|
|
2367
|
+
}));
|
|
2368
|
+
for (const finding of doc.findings) {
|
|
2369
|
+
finding.findingId = findingIds.get(finding.findingId) ?? finding.findingId;
|
|
2370
|
+
finding.targetId = targetIds.get(finding.targetId) ?? finding.targetId;
|
|
2371
|
+
finding.evidence = scrubImported(finding.evidence);
|
|
2372
|
+
if (finding.proposedOwner !== undefined) finding.proposedOwner = scrubImported(finding.proposedOwner);
|
|
2373
|
+
}
|
|
2374
|
+
for (const disposition of doc.dispositions) {
|
|
2375
|
+
const prior = priorConflicts.find(conflict => conflict.conflictId === disposition.conflictId);
|
|
2376
|
+
if (!prior) {
|
|
2377
|
+
throw new RalplanCommandError(2, "refusing to persist ralplan disposition: conflict id did not survive scrub");
|
|
2378
|
+
}
|
|
2379
|
+
const targetId = targetIds.get(prior.targetId) ?? canonicalDispositionId(prior.targetId);
|
|
2380
|
+
const mapped = prior.findingIds.map(id => findingIds.get(id) ?? canonicalDispositionId(id));
|
|
2381
|
+
const [left, right] = mapped[0]! <= mapped[1]! ? [mapped[0]!, mapped[1]!] : [mapped[1]!, mapped[0]!];
|
|
2382
|
+
disposition.conflictId = `conflict:${targetId}:${left}:${right}`;
|
|
2383
|
+
disposition.rationale = scrubImported(disposition.rationale);
|
|
2384
|
+
disposition.decisionOwner = scrubImported(disposition.decisionOwner);
|
|
2385
|
+
disposition.affectedSections = disposition.affectedSections.map(section => scrubImported(section));
|
|
2386
|
+
if (disposition.dispositionedAt !== undefined)
|
|
2387
|
+
disposition.dispositionedAt = scrubImported(disposition.dispositionedAt);
|
|
2388
|
+
}
|
|
2389
|
+
const raw = JSON.parse(serializeReviewConflictDocument(doc)) as Record<string, unknown>;
|
|
2390
|
+
delete raw.conflicts;
|
|
2391
|
+
let rewritten: ReturnType<typeof parseReviewConflictDocument>;
|
|
2392
|
+
try {
|
|
2393
|
+
rewritten = parseReviewConflictDocument(`${JSON.stringify(raw, null, 2)}\n`);
|
|
2394
|
+
} catch (error) {
|
|
2395
|
+
throw new RalplanCommandError(
|
|
2396
|
+
2,
|
|
2397
|
+
`refusing to persist ralplan disposition: credential scrub broke the document (${error instanceof Error ? error.message : String(error)})`,
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
return serializeReviewConflictDocument(rewritten);
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
/**
|
|
2404
|
+
* Credential scrub for a ralplan stage body. The result is what
|
|
2405
|
+
* `gjc ralplan --write` hashes and stores.
|
|
2406
|
+
*/
|
|
2407
|
+
export function scrubRalplanPersistedBody(stage: string, content: string): string {
|
|
2408
|
+
const normalized = content.endsWith("\n") ? content : `${content}\n`;
|
|
2409
|
+
if (stage === "disposition") return scrubDispositionDocument(normalized);
|
|
2410
|
+
const scrubbed = sanitizeImportedString(normalized);
|
|
2411
|
+
return scrubbed.endsWith("\n") ? scrubbed : `${scrubbed}\n`;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2322
2414
|
async function handleArtifactWrite(
|
|
2323
2415
|
args: readonly string[],
|
|
2324
2416
|
cwd: string,
|
|
@@ -2380,7 +2472,9 @@ async function handleArtifactWrite(
|
|
|
2380
2472
|
resolved.stage === "disposition"
|
|
2381
2473
|
? normalizeDispositionArtifact(resolved.artifact, resolved.stageN, indexLoad.rawText)
|
|
2382
2474
|
: resolved.artifact;
|
|
2383
|
-
|
|
2475
|
+
// Scrub before the content hash. The ledger, dedupe, and pending-approval copy
|
|
2476
|
+
// all trust this digest, so it has to name the bytes that actually land on disk.
|
|
2477
|
+
const content = scrubRalplanPersistedBody(resolved.stage, normalizedArtifact);
|
|
2384
2478
|
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
2385
2479
|
|
|
2386
2480
|
// Duplicate-write guard: a second `--write` for the same (stage, stage_n) must not
|
|
@@ -29,6 +29,14 @@ import {
|
|
|
29
29
|
/** Window within which two activity timestamps are treated as an ambiguous tie. */
|
|
30
30
|
export const LATEST_SESSION_TIE_WINDOW_MS = 1000;
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Local writers stamp `Date.now()`. A parsed `updated_at` further ahead than this
|
|
34
|
+
* is planted marker content, not activity, and is not a candidate. Marker mtime
|
|
35
|
+
* is not used in that case: a checkout or touch refreshes mtime without rewriting
|
|
36
|
+
* a future timestamp.
|
|
37
|
+
*/
|
|
38
|
+
const ACTIVITY_FUTURE_SKEW_MS = 2 * 60 * 1000;
|
|
39
|
+
|
|
32
40
|
export interface SessionIdSources {
|
|
33
41
|
/** Raw `--session-id` value: `undefined` = flag absent; `""` = present-but-blank (invalid). */
|
|
34
42
|
flagValue?: string | undefined;
|
|
@@ -46,8 +54,12 @@ export class SessionResolutionError extends Error {
|
|
|
46
54
|
}
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
function isUnsafeResolvedSessionId(sessionId: string): boolean {
|
|
58
|
+
return sessionId === "." || sessionId === ".." || /[/\\]/.test(sessionId);
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
function assertSafeResolvedSessionId(sessionId: string): void {
|
|
50
|
-
if (
|
|
62
|
+
if (isUnsafeResolvedSessionId(sessionId)) {
|
|
51
63
|
throw new SessionResolutionError(
|
|
52
64
|
"session id must be a single path component (no separators or traversal)",
|
|
53
65
|
"unsafe_session",
|
|
@@ -135,8 +147,17 @@ interface SessionCandidate {
|
|
|
135
147
|
* or an ambiguous tie.
|
|
136
148
|
*/
|
|
137
149
|
export async function detectLatestSession(cwd: string): Promise<GjcSessionContext> {
|
|
138
|
-
const
|
|
150
|
+
const found = await collectActiveSessionCandidates(cwd);
|
|
151
|
+
const candidates = found.candidates;
|
|
139
152
|
if (candidates.length === 0) {
|
|
153
|
+
// No safe session was selectable. Keep the unsafe-id error when a crafted
|
|
154
|
+
// directory was the reason nothing could be chosen, matching explicit ids.
|
|
155
|
+
if (found.sawUnsafe) {
|
|
156
|
+
throw new SessionResolutionError(
|
|
157
|
+
"session id must be a single path component (no separators or traversal)",
|
|
158
|
+
"unsafe_session",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
140
161
|
throw new SessionResolutionError(
|
|
141
162
|
"no active GJC session found: pass --session-id or set GJC_SESSION_ID",
|
|
142
163
|
"no_session",
|
|
@@ -156,19 +177,27 @@ export async function detectLatestSession(cwd: string): Promise<GjcSessionContex
|
|
|
156
177
|
return { gjcSessionId: first.gjcSessionId, sessionRoot: first.sessionRoot, source: "latest" };
|
|
157
178
|
}
|
|
158
179
|
|
|
159
|
-
async function collectActiveSessionCandidates(
|
|
180
|
+
async function collectActiveSessionCandidates(
|
|
181
|
+
cwd: string,
|
|
182
|
+
): Promise<{ candidates: SessionCandidate[]; sawUnsafe: boolean }> {
|
|
160
183
|
const root = gjcRoot(cwd);
|
|
161
184
|
let entries: import("node:fs").Dirent[];
|
|
162
185
|
try {
|
|
163
186
|
entries = await fs.readdir(root, { withFileTypes: true });
|
|
164
187
|
} catch {
|
|
165
|
-
return [];
|
|
188
|
+
return { candidates: [], sawUnsafe: false };
|
|
166
189
|
}
|
|
167
190
|
const candidates: SessionCandidate[] = [];
|
|
191
|
+
let sawUnsafe = false;
|
|
168
192
|
for (const entry of entries) {
|
|
169
193
|
if (!entry.isDirectory()) continue;
|
|
170
194
|
const gjcSessionId = sessionIdFromDirName(entry.name);
|
|
171
195
|
if (!gjcSessionId) continue;
|
|
196
|
+
// Skip; throwing here would stop discovery for every other session.
|
|
197
|
+
if (isUnsafeResolvedSessionId(gjcSessionId)) {
|
|
198
|
+
sawUnsafe = true;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
172
201
|
assertSafeResolvedSessionId(gjcSessionId);
|
|
173
202
|
const dir = path.join(root, entry.name);
|
|
174
203
|
const activityMs = await readActivityMs(path.join(dir, GJC_SESSION_ACTIVITY_FILE));
|
|
@@ -177,7 +206,7 @@ async function collectActiveSessionCandidates(cwd: string): Promise<SessionCandi
|
|
|
177
206
|
if (activityMs === undefined) continue;
|
|
178
207
|
candidates.push({ gjcSessionId, sessionRoot: dir, activityMs });
|
|
179
208
|
}
|
|
180
|
-
return candidates;
|
|
209
|
+
return { candidates, sawUnsafe };
|
|
181
210
|
}
|
|
182
211
|
|
|
183
212
|
async function readActivityMs(markerPath: string): Promise<number | undefined> {
|
|
@@ -191,7 +220,10 @@ async function readActivityMs(markerPath: string): Promise<number | undefined> {
|
|
|
191
220
|
const parsed = JSON.parse(raw) as { updated_at?: unknown };
|
|
192
221
|
if (typeof parsed.updated_at === "string") {
|
|
193
222
|
const ms = Date.parse(parsed.updated_at);
|
|
194
|
-
if (!Number.isNaN(ms))
|
|
223
|
+
if (!Number.isNaN(ms)) {
|
|
224
|
+
if (ms > Date.now() + ACTIVITY_FUTURE_SKEW_MS) return undefined;
|
|
225
|
+
return ms;
|
|
226
|
+
}
|
|
195
227
|
}
|
|
196
228
|
} catch {
|
|
197
229
|
// fall through to mtime
|
|
@@ -300,9 +300,12 @@ async function repairPendingOwnerRecord(pending: PendingOwnerRepair): Promise<bo
|
|
|
300
300
|
const handle = pending.repairHandle;
|
|
301
301
|
if (!handle || pending.repairBytes === undefined) return true;
|
|
302
302
|
try {
|
|
303
|
-
await writeOwnerBytes(handle, Buffer.from(pending.repairBytes, "utf8"));
|
|
303
|
+
const written = await writeOwnerBytes(handle, Buffer.from(pending.repairBytes, "utf8"), undefined);
|
|
304
304
|
const stat = await handle.stat({ bigint: true });
|
|
305
|
-
|
|
305
|
+
// The rewritten record may have been padded back to its previous length, so the
|
|
306
|
+
// identity that authorizes later cleanup is the bytes that landed, not the bytes
|
|
307
|
+
// requested.
|
|
308
|
+
pending.repairSnapshot = ownerSnapshotFrom(stat, written);
|
|
306
309
|
await handle.close();
|
|
307
310
|
pending.repairHandle = undefined;
|
|
308
311
|
return true;
|
|
@@ -533,6 +536,8 @@ export const SessionStateLockTestHooks: {
|
|
|
533
536
|
afterCurrentOwnerValidation?: (file: string) => void | Promise<void>;
|
|
534
537
|
/** @internal Fault seam immediately before a held owner record rewrite mutates bytes. */
|
|
535
538
|
beforeOwnerRecordRewrite?: (file: string) => void | Promise<void>;
|
|
539
|
+
/** @internal Fault seam immediately before the pre-rewrite fstat on the owner record handle. */
|
|
540
|
+
beforeOwnerRecordPrewriteStat?: (file: string) => void | Promise<void>;
|
|
536
541
|
/** @internal Fault seam immediately before final transition-generation validation. */
|
|
537
542
|
beforeTransitionReleaseLstat?: (transitionDir: string) => void | Promise<void>;
|
|
538
543
|
/** @internal Fault seam immediately before setup generation capture. */
|
|
@@ -1491,13 +1496,21 @@ async function createOwnerLock(
|
|
|
1491
1496
|
// pair names the record this call brought into existence and nothing else.
|
|
1492
1497
|
let created: OpenOwnerIdentity | null = null;
|
|
1493
1498
|
try {
|
|
1494
|
-
await handle.chmod(0o600);
|
|
1495
1499
|
created = await openOwnerIdentity(handle);
|
|
1496
1500
|
await SessionStateLockTestHooks.ownerRecordWriteFault?.(file);
|
|
1501
|
+
// The record's bytes land before anything else is asked of this descriptor. A
|
|
1502
|
+
// freshly created record is empty until they do, and an empty record carries no
|
|
1503
|
+
// `owner_host_id`, so every syscall placed in front of this write is a window in
|
|
1504
|
+
// which a kill leaves debris no later contender may reclaim. `chmod` is the one
|
|
1505
|
+
// that can actually stall — it is a metadata round-trip that a File Provider,
|
|
1506
|
+
// NFS, or CIFS mount may hold for seconds — so it moves behind the write. The
|
|
1507
|
+
// unrestricted mode it corrected for is already excluded by the create's own
|
|
1508
|
+
// mode argument, so the descriptor is never more permissive than 0o600 here.
|
|
1497
1509
|
await handle.writeFile(bytes);
|
|
1498
1510
|
const stat = await handle.stat({ bigint: true });
|
|
1499
1511
|
if (!stat.isFile() || stat.size !== BigInt(bytes.byteLength))
|
|
1500
1512
|
throw new SessionStateLockUnavailableError(new Error("Owner record did not land as a regular file."));
|
|
1513
|
+
await handle.chmod(0o600);
|
|
1501
1514
|
return ownerSnapshotFrom(stat, bytes);
|
|
1502
1515
|
} catch (error) {
|
|
1503
1516
|
// Deliberately BEFORE any close: releasing the descriptor first would free the
|
|
@@ -1520,16 +1533,48 @@ const POSIX_OWNER_REWRITE_FLAGS = POSIX_NOFOLLOW_AVAILABLE
|
|
|
1520
1533
|
? fsSync.constants.O_RDWR | fsSync.constants.O_NONBLOCK | fsSync.constants.O_NOFOLLOW
|
|
1521
1534
|
: undefined;
|
|
1522
1535
|
|
|
1523
|
-
|
|
1524
|
-
|
|
1536
|
+
/**
|
|
1537
|
+
* The one writer of an owner record's bytes: overwrite in place, never shorter.
|
|
1538
|
+
*
|
|
1539
|
+
* `truncate(0)` followed by a write looks atomic and is not. The bytes a truncate
|
|
1540
|
+
* publishes are EMPTY, and an empty record carries no `owner_host_id` at all, so a
|
|
1541
|
+
* process that dies inside that window (a repeated Ctrl-C reaches `postmortem.quit`,
|
|
1542
|
+
* which deliberately performs no lock release) leaves a record with no attributable
|
|
1543
|
+
* owner. `malformedOwnerProvenance` classifies it `unprovenanced`, the reclaim refuses
|
|
1544
|
+
* it, and every later contender fails closed forever: `transition_claim_timeout` with
|
|
1545
|
+
* `claim_or_owner_unreadable`, and coordinator state persistence for that session is
|
|
1546
|
+
* lost for good — the failure class #5159 and #5216 both land on.
|
|
1547
|
+
*
|
|
1548
|
+
* So the replacement is written over the HEAD of the record and the previous byte
|
|
1549
|
+
* length is kept instead of discarded. A JSON document ignores trailing whitespace, so
|
|
1550
|
+
* a padded record is the same record to every reader — `JSON.parse`, `validLockOwner`,
|
|
1551
|
+
* `malformedOwnerProvenance`, and the identity-bound unlink all see exactly the
|
|
1552
|
+
* replacement — while the bytes on disk always carry the full ownership preamble. That
|
|
1553
|
+
* leaves a torn single write (which needs the kernel to short-write a ~200 byte payload)
|
|
1554
|
+
* as the only state the record cannot be attributed from, instead of the guaranteed
|
|
1555
|
+
* empty window between two syscalls.
|
|
1556
|
+
*
|
|
1557
|
+
* @returns the exact bytes now on disk. A padded record's size and SHA-256 are the
|
|
1558
|
+
* padded bytes', so this — not the requested payload — is what identifies the record
|
|
1559
|
+
* for every later capture, comparison, and replay.
|
|
1560
|
+
*/
|
|
1561
|
+
async function writeOwnerBytes(handle: fs.FileHandle, bytes: Buffer, file?: string): Promise<Buffer> {
|
|
1562
|
+
if (file) await SessionStateLockTestHooks.beforeOwnerRecordPrewriteStat?.(file);
|
|
1563
|
+
const previous = await handle.stat({ bigint: true });
|
|
1564
|
+
const previousSize = previous.isFile() === true ? Number(previous.size) : 0;
|
|
1565
|
+
const padding = Math.max(0, previousSize - bytes.byteLength);
|
|
1566
|
+
const payload = padding === 0 ? bytes : Buffer.concat([bytes, Buffer.alloc(padding, 0x20)]);
|
|
1525
1567
|
let offset = 0;
|
|
1526
|
-
while (offset <
|
|
1527
|
-
const { bytesWritten } = await handle.write(
|
|
1568
|
+
while (offset < payload.byteLength) {
|
|
1569
|
+
const { bytesWritten } = await handle.write(payload, offset, payload.byteLength - offset, offset);
|
|
1528
1570
|
if (bytesWritten <= 0) throw new Error("Owner record rewrite made no progress.");
|
|
1529
1571
|
offset += bytesWritten;
|
|
1530
1572
|
}
|
|
1531
|
-
|
|
1573
|
+
// `payload` is never shorter than the record already was, so this can only remove a
|
|
1574
|
+
// tail that outgrew the bytes just written; it can never cut the record itself.
|
|
1575
|
+
await handle.truncate(payload.byteLength);
|
|
1532
1576
|
await handle.sync();
|
|
1577
|
+
return payload;
|
|
1533
1578
|
}
|
|
1534
1579
|
|
|
1535
1580
|
async function rewriteHeldOwnerRecord(
|
|
@@ -1581,15 +1626,15 @@ async function rewriteHeldOwnerRecord(
|
|
|
1581
1626
|
mutationStarted = true;
|
|
1582
1627
|
await SessionStateLockTestHooks.beforeOwnerRecordRewrite?.(file);
|
|
1583
1628
|
rewriteHookPassed = true;
|
|
1584
|
-
await writeOwnerBytes(handle, replacementBytes);
|
|
1629
|
+
const writtenBytes = await writeOwnerBytes(handle, replacementBytes, file);
|
|
1585
1630
|
const canonical = await captureRegularLockOwner(file);
|
|
1586
|
-
// On Windows, an in-place
|
|
1631
|
+
// On Windows, an in-place rewrite can expose refreshed descriptor
|
|
1587
1632
|
// metadata even though the file object is unchanged. The descriptor opened
|
|
1588
1633
|
// before mutation is the stable identity bracket; the path capture still
|
|
1589
1634
|
// proves that the pathname resolves to that same object after the rewrite.
|
|
1590
1635
|
if (!canonical || canonical.dev !== opened.dev || canonical.ino !== opened.ino)
|
|
1591
1636
|
throw new SessionStateLockUnavailableError(new Error("Owner pathname changed after rewrite."));
|
|
1592
|
-
if (canonical.bytes !==
|
|
1637
|
+
if (canonical.bytes !== writtenBytes.toString("utf8")) {
|
|
1593
1638
|
let successor: unknown;
|
|
1594
1639
|
try {
|
|
1595
1640
|
successor = JSON.parse(canonical.bytes);
|
|
@@ -1602,7 +1647,7 @@ async function rewriteHeldOwnerRecord(
|
|
|
1602
1647
|
return canonical;
|
|
1603
1648
|
} catch (error) {
|
|
1604
1649
|
// Keep the descriptor as the repair authority until the held bytes have
|
|
1605
|
-
// been restored. A
|
|
1650
|
+
// been restored. A write/fsync fault may leave a malformed or
|
|
1606
1651
|
// partially committed record; closing first would force recovery to trust
|
|
1607
1652
|
// an unproven pathname and can wedge a successor behind the transition.
|
|
1608
1653
|
let repairNeeded = mutationStarted;
|
|
@@ -1616,9 +1661,9 @@ async function rewriteHeldOwnerRecord(
|
|
|
1616
1661
|
if (repairNeeded) {
|
|
1617
1662
|
let repaired: LockOwnerSnapshot;
|
|
1618
1663
|
try {
|
|
1619
|
-
await writeOwnerBytes(handle, Buffer.from(held.bytes, "utf8"));
|
|
1664
|
+
const restored = await writeOwnerBytes(handle, Buffer.from(held.bytes, "utf8"), file);
|
|
1620
1665
|
const repairedStat = await handle.stat({ bigint: true });
|
|
1621
|
-
repaired = ownerSnapshotFrom(repairedStat,
|
|
1666
|
+
repaired = ownerSnapshotFrom(repairedStat, restored);
|
|
1622
1667
|
} catch (repairError) {
|
|
1623
1668
|
const aggregate = new AggregateError(
|
|
1624
1669
|
[error, repairError],
|