@gajae-code/coding-agent 0.18.7 → 0.18.9
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 +377 -1
- package/dist/types/async/endpoint-id.d.ts +16 -0
- package/dist/types/async/job-manager.d.ts +1 -1
- package/dist/types/async/support.d.ts +0 -14
- package/dist/types/capability/mcp.d.ts +2 -0
- package/dist/types/capability/skill.d.ts +2 -0
- package/dist/types/capability/types.d.ts +2 -0
- package/dist/types/cli/public-command-errors.d.ts +3 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/commands/mcp-serve.d.ts +4 -0
- package/dist/types/config/autorouting-tier-map.d.ts +348 -9
- package/dist/types/config/file-lock.d.ts +1 -0
- package/dist/types/config/model-bindings-applier.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -3
- package/dist/types/config/model-registry.d.ts +4 -9
- package/dist/types/config/skill-settings-defaults.d.ts +2 -0
- package/dist/types/cursor-delete-path.d.ts +1 -0
- package/dist/types/cursor-mcp-name.d.ts +8 -0
- package/dist/types/customization/inventory.d.ts +2 -0
- package/dist/types/debug/report-bundle-parent.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
- package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
- package/dist/types/discovery/helpers.d.ts +6 -1
- package/dist/types/discovery/mcp-json.d.ts +1 -0
- package/dist/types/eval/py/kernel.d.ts +18 -0
- package/dist/types/export/html/css-color.d.ts +2 -0
- package/dist/types/export/html/html-export-destination.d.ts +2 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +5 -0
- package/dist/types/extensibility/extensions/types.d.ts +7 -0
- package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
- package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
- package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
- package/dist/types/extensibility/skill-management.d.ts +4 -1
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -3
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
- package/dist/types/gjc-runtime/sidecar-bootstrap-env.d.ts +6 -0
- package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
- package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +8 -5
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
- package/dist/types/internal-urls/skill-path.d.ts +1 -0
- package/dist/types/lsp/config.d.ts +8 -5
- package/dist/types/lsp/workspace-path.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +7 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -0
- package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
- package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
- package/dist/types/modes/controllers/input-controller.d.ts +9 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
- package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
- package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
- package/dist/types/progress/collect-project-progress.d.ts +38 -0
- package/dist/types/progress/progress-contract.d.ts +154 -0
- package/dist/types/progress/progress-snapshot.d.ts +11 -0
- package/dist/types/progress/project-progress.d.ts +141 -0
- package/dist/types/progress/render-progress.d.ts +28 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -1
- package/dist/types/runtime-mcp/mcp-redirect.d.ts +9 -0
- package/dist/types/runtime-mcp/oauth-public-url.d.ts +3 -0
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
- package/dist/types/runtime-mcp/project-host-values.d.ts +27 -0
- package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
- package/dist/types/runtime-mcp/types.d.ts +6 -0
- package/dist/types/sdk/acp/adapter.d.ts +1 -1
- package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
- package/dist/types/sdk/broker/broker.d.ts +24 -2
- package/dist/types/sdk/broker/doctor-restart.d.ts +5 -0
- package/dist/types/sdk/broker/ensure.d.ts +14 -1
- package/dist/types/sdk/broker/session-index.d.ts +12 -6
- package/dist/types/sdk/bus/index.d.ts +12 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
- package/dist/types/sdk/client/client.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +3 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session-list.d.ts +1 -1
- package/dist/types/sdk/session.d.ts +8 -0
- package/dist/types/session/agent-session.d.ts +38 -9
- package/dist/types/session/artifacts.d.ts +2 -2
- package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
- package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
- package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
- package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +31 -5
- package/dist/types/session/session-retirement.d.ts +31 -7
- package/dist/types/session/session-storage.d.ts +69 -2
- package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
- package/dist/types/setup/credential-auto-import.d.ts +1 -1
- package/dist/types/setup/credential-import.d.ts +6 -4
- package/dist/types/skill-state/active-state.d.ts +38 -0
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +3 -0
- package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/isolation-dir.d.ts +26 -0
- package/dist/types/tools/ast-edit-write-path.d.ts +13 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -1
- package/dist/types/tools/browser/cdp-url.d.ts +2 -0
- package/dist/types/tools/github-side-effect.d.ts +22 -0
- package/dist/types/tools/index.d.ts +3 -0
- package/dist/types/tools/monitor-mutation.d.ts +9 -0
- package/dist/types/tools/python.d.ts +9 -1
- package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
- package/dist/types/tools/restricted-cwd.d.ts +1 -0
- package/dist/types/tools/todo-write.d.ts +6 -0
- package/dist/types/utils/tools-checksum.d.ts +7 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +21 -0
- package/scripts/verify-managed-empty-stop.ts +28 -0
- package/src/async/endpoint-id.ts +26 -0
- package/src/async/job-manager.ts +247 -132
- package/src/async/support.ts +0 -25
- package/src/capability/index.ts +15 -0
- package/src/capability/mcp.ts +2 -0
- package/src/capability/skill.ts +2 -0
- package/src/capability/types.ts +2 -0
- package/src/cli/auth-broker-cli.ts +1 -1
- package/src/cli/customize-doctor.ts +15 -2
- package/src/cli/doctor/repairs.ts +4 -1
- package/src/cli/doctor-supervisor.ts +3 -0
- package/src/cli/public-command-errors.ts +21 -1
- package/src/cli/public-command-registry.ts +28 -6
- package/src/cli/setup-cli.ts +4 -4
- package/src/cli/skills-cli.ts +5 -0
- package/src/commands/deep-interview.ts +47 -8
- package/src/commands/mcp-serve.ts +7 -0
- package/src/commands/sdk.ts +455 -397
- package/src/config/autorouting-tier-map.ts +117 -7
- package/src/config/file-lock.ts +316 -79
- package/src/config/mcp-schema.json +4 -0
- package/src/config/model-bindings-applier.ts +20 -0
- package/src/config/model-profile-activation.ts +240 -46
- package/src/config/model-registry.ts +47 -153
- package/src/config/skill-settings-defaults.ts +7 -0
- package/src/coordinator-mcp/policy.ts +3 -2
- package/src/coordinator-mcp/server.ts +2 -0
- package/src/cursor-delete-path.ts +10 -0
- package/src/cursor-mcp-name.ts +15 -0
- package/src/cursor.ts +13 -2
- package/src/customization/inventory.ts +4 -13
- package/src/dap/config.ts +2 -2
- package/src/debug/report-bundle-parent.ts +22 -0
- package/src/debug/report-bundle.ts +3 -0
- package/src/debug/system-info.ts +13 -1
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +10 -6
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
- package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
- package/src/defaults/gjc-defaults.test.ts +18 -1
- package/src/defaults/gjc-defaults.ts +3 -0
- package/src/defaults/gjc-skills.generated.ts +58 -36
- package/src/discovery/agents.ts +6 -1
- package/src/discovery/builtin.ts +13 -1
- package/src/discovery/helpers.ts +171 -29
- package/src/discovery/mcp-json.ts +26 -12
- package/src/discovery/ssh.ts +1 -1
- package/src/discovery/substitute-plugin-root.ts +3 -1
- package/src/edit/file-read-cache.ts +1 -1
- package/src/eval/py/executor.ts +796 -154
- package/src/eval/py/kernel.ts +79 -13
- package/src/eval/py/tool-bridge.ts +4 -0
- package/src/export/html/css-color.ts +9 -0
- package/src/export/html/html-export-destination.ts +16 -0
- package/src/export/html/index.ts +7 -10
- package/src/export/html/template.generated.ts +1 -1
- package/src/extensibility/extensions/function-hooks-internal.ts +27 -0
- package/src/extensibility/extensions/runner.ts +15 -1
- package/src/extensibility/extensions/types.ts +7 -0
- package/src/extensibility/gjc-plugins/mcp-policy.ts +2 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +85 -8
- package/src/extensibility/plugins/manager.ts +3 -2
- package/src/extensibility/plugins/marketplace/cache.ts +15 -0
- package/src/extensibility/plugins/marketplace/manager.ts +11 -3
- package/src/extensibility/plugins/plugin-link-path.ts +12 -0
- package/src/extensibility/runtime-skill-discovery.ts +20 -2
- package/src/extensibility/skill-management.ts +9 -2
- package/src/extensibility/skills.ts +6 -1
- package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
- package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
- package/src/gjc-runtime/deep-interview-stage.ts +98 -21
- package/src/gjc-runtime/gc-runtime.ts +206 -9
- package/src/gjc-runtime/launch-worktree.ts +70 -9
- package/src/gjc-runtime/psmux-detect.ts +28 -4
- package/src/gjc-runtime/ralplan-runtime.ts +447 -52
- package/src/gjc-runtime/session-state-sidecar.ts +3 -2
- package/src/gjc-runtime/sidecar-bootstrap-env.ts +19 -0
- package/src/gjc-runtime/state-argv.ts +2 -0
- package/src/gjc-runtime/state-migrations.ts +91 -29
- package/src/gjc-runtime/state-runtime.ts +3716 -135
- package/src/gjc-runtime/state-writer.ts +105 -16
- package/src/gjc-runtime/tmux-common.ts +8 -5
- package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
- package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
- package/src/gjc-runtime/workflow-manifest.ts +54 -21
- package/src/harness-control-plane/operate.ts +12 -4
- package/src/harness-control-plane/owner.ts +5 -1
- package/src/hashline/parser.ts +48 -9
- package/src/hashline/recovery.ts +13 -4
- package/src/internal-urls/docs-index.generated.ts +20 -19
- package/src/internal-urls/skill-path.ts +19 -0
- package/src/internal-urls/skill-protocol.ts +2 -0
- package/src/lsp/config.ts +12 -63
- package/src/lsp/edits.ts +6 -0
- package/src/lsp/workspace-path.ts +51 -0
- package/src/main.ts +195 -31
- package/src/memories/index.ts +88 -7
- package/src/modes/acp/acp-agent.ts +88 -4
- package/src/modes/components/custom-editor.ts +14 -0
- package/src/modes/components/customization/customization-dashboard.ts +12 -0
- package/src/modes/components/extensions/extension-dashboard.ts +2 -2
- package/src/modes/components/extensions/state-manager.ts +15 -5
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/hook-selector.ts +1 -1
- package/src/modes/components/model-selector.ts +1 -4
- package/src/modes/components/provider-onboarding-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +4 -0
- package/src/modes/controllers/event-controller.ts +11 -4
- package/src/modes/controllers/input-controller.ts +76 -31
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -16
- package/src/modes/controllers/selector-controller.ts +24 -15
- package/src/modes/controllers/todo-command-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +5 -1
- package/src/modes/runtime-init.ts +4 -0
- package/src/modes/utils/abort-message.ts +3 -1
- package/src/modes/utils/interrupt-ownership.ts +12 -0
- package/src/modes/utils/keybinding-matchers.ts +4 -0
- package/src/modes/utils/macos-option-forwarding.ts +80 -0
- package/src/progress/collect-project-progress.ts +153 -0
- package/src/progress/progress-contract.ts +172 -0
- package/src/progress/progress-snapshot.ts +140 -0
- package/src/progress/project-progress.ts +446 -0
- package/src/progress/render-progress.ts +209 -0
- package/src/prompts/system/master-mode.md +1 -0
- package/src/prompts/tools/skill-discovery.md +1 -1
- package/src/runtime-mcp/client.ts +4 -2
- package/src/runtime-mcp/config-writer.ts +10 -6
- package/src/runtime-mcp/config.ts +1 -0
- package/src/runtime-mcp/json-rpc.ts +2 -1
- package/src/runtime-mcp/manager.ts +29 -11
- package/src/runtime-mcp/mcp-redirect.ts +73 -0
- package/src/runtime-mcp/oauth-flow.ts +14 -2
- package/src/runtime-mcp/oauth-public-url.ts +14 -0
- package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
- package/src/runtime-mcp/pool.ts +6 -0
- package/src/runtime-mcp/project-host-values.ts +46 -0
- package/src/runtime-mcp/smithery-registry.ts +22 -3
- package/src/runtime-mcp/transports/http.ts +4 -3
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk/acp/adapter.ts +95 -8
- package/src/sdk/broker/broker-exit.ts +52 -23
- package/src/sdk/broker/broker.ts +81 -6
- package/src/sdk/broker/doctor-restart.ts +4 -0
- package/src/sdk/broker/ensure.ts +31 -2
- package/src/sdk/broker/lifecycle.ts +711 -66
- package/src/sdk/broker/session-index.ts +191 -81
- package/src/sdk/bus/chat-daemon-cli.ts +38 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +238 -43
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
- package/src/sdk/cli/session-cli.ts +17 -3
- package/src/sdk/client/client.ts +28 -8
- package/src/sdk/host/query/handlers.ts +9 -1
- package/src/sdk/host/session-runtime.test.ts +2705 -303
- package/src/sdk/host/session-runtime.ts +490 -143
- package/src/sdk/host/surface-policy.ts +1 -0
- package/src/sdk/index.ts +8 -0
- package/src/sdk/lifecycle-session.ts +3 -1
- package/src/sdk/prompt-deadline-manager.ts +15 -0
- package/src/sdk/protocol/operation-inventory.generated.json +146 -0
- package/src/sdk/protocol/operation-registry.ts +9 -2
- package/src/sdk/router/session-router.ts +82 -18
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +69 -45
- package/src/session/agent-session.ts +679 -238
- package/src/session/artifacts.ts +176 -71
- package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
- package/src/session/internal/managed-session-scope.ts +3779 -462
- package/src/session/internal/managed-session-storage.ts +501 -78
- package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
- package/src/session/internal/task-artifact-owner-access.ts +19 -3
- package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
- package/src/session/session-manager.ts +951 -133
- package/src/session/session-retirement.ts +573 -14
- package/src/session/session-storage.ts +447 -15
- package/src/session/task-artifact-owner-codec.ts +71 -8
- package/src/session/task-artifact-owner-retirement.ts +21 -19
- package/src/setup/credential-auto-import.ts +12 -3
- package/src/setup/credential-import.ts +176 -5
- package/src/setup/paseo/announce.ts +6 -1
- package/src/skill-state/active-state.ts +145 -2
- package/src/skill-state/workflow-mutation-guard.ts +102 -10
- package/src/slash-commands/builtin-registry.ts +39 -9
- package/src/slash-commands/helpers/mcp.ts +8 -4
- package/src/slash-commands/helpers/progress-report.ts +23 -0
- package/src/stt/setup.ts +3 -5
- package/src/task/commands.ts +3 -1
- package/src/task/executor.ts +4 -0
- package/src/task/index.ts +6 -0
- package/src/task/isolation-dir.ts +143 -0
- package/src/task/worktree.ts +16 -7
- package/src/tools/ask.ts +205 -6
- package/src/tools/ast-edit-write-path.ts +68 -0
- package/src/tools/ast-edit.ts +10 -1
- package/src/tools/bash-allowed-prefixes.ts +15 -0
- package/src/tools/bash-skill-urls.ts +56 -3
- package/src/tools/bash.ts +48 -79
- package/src/tools/browser/cdp-url.ts +18 -0
- package/src/tools/browser.ts +8 -1
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/eval.ts +56 -36
- package/src/tools/gh.ts +10 -0
- package/src/tools/github-side-effect.ts +112 -0
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +3 -0
- package/src/tools/monitor-mutation.ts +27 -0
- package/src/tools/monitor.ts +7 -0
- package/src/tools/python.ts +188 -66
- package/src/tools/recipe/index.ts +9 -0
- package/src/tools/recipe/recipe-guard.ts +19 -0
- package/src/tools/restricted-cwd.ts +10 -0
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +28 -1
- package/src/tools/todo-write.ts +10 -1
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/tools/write.ts +1 -0
- package/src/utils/external-editor.ts +3 -2
- package/src/utils/tools-checksum.ts +34 -0
- package/src/utils/tools-manager.ts +24 -6
- package/src/web/insane/url-guard.ts +2 -1
- package/src/web/search/providers/codex.ts +3 -2
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
const ALLOCATION_ATTEMPTS = 8;
|
|
6
|
+
|
|
7
|
+
type PathApi = Pick<typeof path, "dirname" | "isAbsolute" | "relative" | "resolve" | "sep">;
|
|
8
|
+
|
|
9
|
+
function isErrno(error: unknown, code: string): boolean {
|
|
10
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function defaultToken(): string {
|
|
14
|
+
return randomBytes(9).toString("hex");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parent directory of `mergedDir`, but only when it is a single entry inside
|
|
19
|
+
* `worktreeRoot`. Root, empty, nested, and escaped paths are refused so
|
|
20
|
+
* cleanup cannot delete the worktree root or another session's tree by
|
|
21
|
+
* walking up.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveIsolationRemovalTarget(
|
|
24
|
+
mergedDir: string,
|
|
25
|
+
worktreeRoot: string,
|
|
26
|
+
pathApi: PathApi = path,
|
|
27
|
+
): string {
|
|
28
|
+
if (mergedDir.length === 0) {
|
|
29
|
+
throw new Error("Refusing to remove an isolation directory for an empty merged path.");
|
|
30
|
+
}
|
|
31
|
+
if (worktreeRoot.length === 0) {
|
|
32
|
+
throw new Error("Refusing to remove an isolation directory without a worktree root.");
|
|
33
|
+
}
|
|
34
|
+
const baseDir = pathApi.resolve(pathApi.dirname(mergedDir));
|
|
35
|
+
const root = pathApi.resolve(worktreeRoot);
|
|
36
|
+
const relative = pathApi.relative(root, baseDir);
|
|
37
|
+
if (
|
|
38
|
+
relative.length === 0 ||
|
|
39
|
+
relative === "." ||
|
|
40
|
+
relative.startsWith("..") ||
|
|
41
|
+
pathApi.isAbsolute(relative) ||
|
|
42
|
+
relative.includes(pathApi.sep) ||
|
|
43
|
+
relative.includes("/") ||
|
|
44
|
+
relative.includes("\\")
|
|
45
|
+
) {
|
|
46
|
+
throw new Error("Refusing to remove an isolation directory that is not a single worktree entry.");
|
|
47
|
+
}
|
|
48
|
+
return baseDir;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create an exclusive directory next to `canonicalBaseDir`. Never removes
|
|
53
|
+
* `canonicalBaseDir` or any path that already exists.
|
|
54
|
+
*/
|
|
55
|
+
export async function allocateDisjointIsolationDir(
|
|
56
|
+
canonicalBaseDir: string,
|
|
57
|
+
tokenFactory: () => string = defaultToken,
|
|
58
|
+
): Promise<string> {
|
|
59
|
+
const parent = path.dirname(canonicalBaseDir);
|
|
60
|
+
const stem = path.basename(canonicalBaseDir);
|
|
61
|
+
if (stem.length === 0 || stem === "." || stem === ".." || stem.includes("/") || stem.includes("\\")) {
|
|
62
|
+
throw new Error("Isolation directory name is not a single path segment.");
|
|
63
|
+
}
|
|
64
|
+
await fs.mkdir(parent, { recursive: true, mode: 0o700 });
|
|
65
|
+
|
|
66
|
+
for (let attempt = 0; attempt < ALLOCATION_ATTEMPTS; attempt++) {
|
|
67
|
+
const token = tokenFactory();
|
|
68
|
+
if (!/^[0-9a-f]+$/i.test(token)) {
|
|
69
|
+
throw new Error("Isolation directory token is not hexadecimal.");
|
|
70
|
+
}
|
|
71
|
+
const name = `${stem}-${token}`;
|
|
72
|
+
const baseDir = path.join(parent, name);
|
|
73
|
+
const relative = path.relative(parent, baseDir);
|
|
74
|
+
if (relative !== name || path.isAbsolute(relative)) {
|
|
75
|
+
throw new Error("Isolation directory escaped its parent.");
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
await fs.mkdir(baseDir, { mode: 0o700 });
|
|
79
|
+
return baseDir;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (isErrno(error, "EEXIST")) continue;
|
|
82
|
+
if (isErrno(error, "EPERM")) {
|
|
83
|
+
try {
|
|
84
|
+
await fs.lstat(baseDir);
|
|
85
|
+
continue;
|
|
86
|
+
} catch (statError) {
|
|
87
|
+
if (!isErrno(statError, "ENOENT")) throw statError;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
throw new Error("Failed to allocate a disjoint task isolation directory.");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Confirm native teardown may see `mergedDir`.
|
|
99
|
+
*
|
|
100
|
+
* `isoStop` deletes that path. The parent entry and `merged` itself must
|
|
101
|
+
* already be real directories, because native removal follows symlinks.
|
|
102
|
+
* A missing parent means there is nothing to stop. This does not close a
|
|
103
|
+
* replacement that happens after the lstat returns.
|
|
104
|
+
*/
|
|
105
|
+
export async function assertIsolationTeardownTarget(mergedDir: string, worktreeRoot: string): Promise<"stop" | "skip"> {
|
|
106
|
+
const baseDir = resolveIsolationRemovalTarget(mergedDir, worktreeRoot);
|
|
107
|
+
let baseInfo: { isSymbolicLink(): boolean };
|
|
108
|
+
try {
|
|
109
|
+
baseInfo = await fs.lstat(baseDir);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (isErrno(error, "ENOENT")) return "skip";
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
if (baseInfo.isSymbolicLink()) {
|
|
115
|
+
throw new Error("Refusing to remove a symlinked isolation directory.");
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const mergedInfo = await fs.lstat(mergedDir);
|
|
119
|
+
if (mergedInfo.isSymbolicLink()) {
|
|
120
|
+
throw new Error("Refusing to tear down a symlinked isolation merged directory.");
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (isErrno(error, "ENOENT")) return "stop";
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
return "stop";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Remove the isolation directory that owns `mergedDir`. Missing directories are ignored. */
|
|
130
|
+
export async function removeIsolationDirectory(mergedDir: string, worktreeRoot: string): Promise<void> {
|
|
131
|
+
const baseDir = resolveIsolationRemovalTarget(mergedDir, worktreeRoot);
|
|
132
|
+
let info: { isSymbolicLink(): boolean };
|
|
133
|
+
try {
|
|
134
|
+
info = await fs.lstat(baseDir);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (isErrno(error, "ENOENT")) return;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
if (info.isSymbolicLink()) {
|
|
140
|
+
throw new Error("Refusing to remove a symlinked isolation directory.");
|
|
141
|
+
}
|
|
142
|
+
await fs.rm(baseDir, { recursive: true, force: true });
|
|
143
|
+
}
|
package/src/task/worktree.ts
CHANGED
|
@@ -5,8 +5,9 @@ import * as path from "node:path";
|
|
|
5
5
|
|
|
6
6
|
import type * as natives from "@gajae-code/natives";
|
|
7
7
|
|
|
8
|
-
import { getWorktreeDir, hashPath, logger, Snowflake } from "@gajae-code/utils";
|
|
8
|
+
import { getWorktreeDir, getWorktreesDir, hashPath, logger, Snowflake } from "@gajae-code/utils";
|
|
9
9
|
import * as git from "../utils/git";
|
|
10
|
+
import { allocateDisjointIsolationDir, assertIsolationTeardownTarget, removeIsolationDirectory } from "./isolation-dir";
|
|
10
11
|
|
|
11
12
|
let nativeWorktreeBindings: typeof import("@gajae-code/natives") | undefined;
|
|
12
13
|
|
|
@@ -401,8 +402,13 @@ export async function ensureIsolation(
|
|
|
401
402
|
preferred?: IsoBackendKind,
|
|
402
403
|
): Promise<IsolationHandle> {
|
|
403
404
|
const repoRoot = await getRepoRoot(baseCwd);
|
|
404
|
-
|
|
405
|
+
// Task ids are allocated inside one session, so `<id>-<repo hash>` is shared
|
|
406
|
+
// by every fresh session's first task. Never remove that canonical directory.
|
|
407
|
+
// Each attempt gets an exclusive sibling and only that sibling is removed.
|
|
408
|
+
const canonicalBaseDir = getWorktreeDir(`${id}-${hashPath(repoRoot)}`);
|
|
409
|
+
const baseDir = await allocateDisjointIsolationDir(canonicalBaseDir);
|
|
405
410
|
const mergedDir = path.join(baseDir, "merged");
|
|
411
|
+
const worktreeRoot = getWorktreesDir();
|
|
406
412
|
const natives = nativeWorktree();
|
|
407
413
|
|
|
408
414
|
const resolution = natives.isoResolve(preferred ?? null);
|
|
@@ -410,7 +416,7 @@ export async function ensureIsolation(
|
|
|
410
416
|
let fallbackReason = resolution.reason ?? null;
|
|
411
417
|
|
|
412
418
|
for (const candidate of candidates) {
|
|
413
|
-
await
|
|
419
|
+
await removeIsolationDirectory(mergedDir, worktreeRoot);
|
|
414
420
|
try {
|
|
415
421
|
await natives.isoStart(candidate, repoRoot, mergedDir);
|
|
416
422
|
return {
|
|
@@ -420,7 +426,7 @@ export async function ensureIsolation(
|
|
|
420
426
|
fallbackReason,
|
|
421
427
|
};
|
|
422
428
|
} catch (err) {
|
|
423
|
-
await
|
|
429
|
+
await removeIsolationDirectory(mergedDir, worktreeRoot);
|
|
424
430
|
const message = errorMessage(err);
|
|
425
431
|
if (!natives.isoIsUnavailableError(message)) {
|
|
426
432
|
throw err;
|
|
@@ -434,6 +440,11 @@ export async function ensureIsolation(
|
|
|
434
440
|
|
|
435
441
|
/** Tear down a handle returned by {@link ensureIsolation}. */
|
|
436
442
|
export async function cleanupIsolation(handle: IsolationHandle): Promise<void> {
|
|
443
|
+
const worktreeRoot = getWorktreesDir();
|
|
444
|
+
// Native isoStop removes `mergedDir` and follows symlinks. Refuse a
|
|
445
|
+
// symlink that is already present before that call.
|
|
446
|
+
const teardown = await assertIsolationTeardownTarget(handle.mergedDir, worktreeRoot);
|
|
447
|
+
if (teardown === "skip") return;
|
|
437
448
|
const natives = nativeWorktree();
|
|
438
449
|
try {
|
|
439
450
|
try {
|
|
@@ -446,9 +457,7 @@ export async function cleanupIsolation(handle: IsolationHandle): Promise<void> {
|
|
|
446
457
|
});
|
|
447
458
|
}
|
|
448
459
|
} finally {
|
|
449
|
-
|
|
450
|
-
const baseDir = path.dirname(handle.mergedDir);
|
|
451
|
-
await fs.rm(baseDir, { recursive: true, force: true });
|
|
460
|
+
await removeIsolationDirectory(handle.mergedDir, worktreeRoot);
|
|
452
461
|
}
|
|
453
462
|
}
|
|
454
463
|
|
package/src/tools/ask.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* - Questions may time out and auto-select the recommended option (configurable, disabled in plan mode)
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { randomUUID } from "node:crypto";
|
|
18
19
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
|
|
19
20
|
import type { RawArgumentValidationResult } from "@gajae-code/ai/types";
|
|
20
21
|
import { validateToolArguments } from "@gajae-code/ai/utils/validation";
|
|
@@ -36,12 +37,25 @@ import {
|
|
|
36
37
|
} from "../deep-interview/render-middleware";
|
|
37
38
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
38
39
|
import { appendOrMergeDeepInterviewRound, syncDeepInterviewRecorderHud } from "../gjc-runtime/deep-interview-recorder";
|
|
39
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
assertDeepInterviewCrystalCoversLiveTranscript,
|
|
42
|
+
authoritativeConversationSnapshot,
|
|
43
|
+
deepInterviewStatePath,
|
|
44
|
+
} from "../gjc-runtime/deep-interview-runtime";
|
|
40
45
|
import {
|
|
41
46
|
assertDeepInterviewInputWithinLimit,
|
|
42
47
|
assertDeepInterviewStructuredResponseWithinLimit,
|
|
43
48
|
MAX_USER_RESPONSE_LENGTH,
|
|
44
49
|
} from "../gjc-runtime/deep-interview-state";
|
|
50
|
+
import type { ExecutionApprovalPresentation } from "../gjc-runtime/state-runtime";
|
|
51
|
+
import {
|
|
52
|
+
captureExecutionApprovalPresentation,
|
|
53
|
+
executionApprovalLineage,
|
|
54
|
+
recordDeepInterviewExecutionApproval,
|
|
55
|
+
recordNonCrystalExecutionApproval,
|
|
56
|
+
revokeDeepInterviewExecutionApproval,
|
|
57
|
+
revokeNonCrystalExecutionApproval,
|
|
58
|
+
} from "../gjc-runtime/state-runtime";
|
|
45
59
|
import {
|
|
46
60
|
type AskGateQuestion,
|
|
47
61
|
gateAnswerToResult,
|
|
@@ -75,7 +89,7 @@ import {
|
|
|
75
89
|
export { askSchema } from "./ask-contract";
|
|
76
90
|
|
|
77
91
|
import { formatErrorMessage, formatMeta, formatTitle } from "./render-utils";
|
|
78
|
-
import { ToolAbortError } from "./tool-errors";
|
|
92
|
+
import { ToolAbortError, ToolError } from "./tool-errors";
|
|
79
93
|
import { assertUltragoalAskAllowed } from "./ultragoal-ask-guard";
|
|
80
94
|
|
|
81
95
|
// =============================================================================
|
|
@@ -158,6 +172,23 @@ function nonEmptyCustomInput(value: string | undefined): string | undefined {
|
|
|
158
172
|
return value !== undefined && value.trim().length > 0 ? value : undefined;
|
|
159
173
|
}
|
|
160
174
|
|
|
175
|
+
function deepInterviewExecutionTarget(selectedOptions: readonly string[]): "ultragoal" | undefined {
|
|
176
|
+
if (selectedOptions.length !== 1) return undefined;
|
|
177
|
+
const normalized = selectedOptions[0]
|
|
178
|
+
?.trim()
|
|
179
|
+
.replace(/\s*\(Recommended\)\s*$/i, "")
|
|
180
|
+
.toLowerCase();
|
|
181
|
+
return normalized === "approve execution via ultragoal" ||
|
|
182
|
+
/^execute with ultragoal(?: \(only when spec is already implementation-ready and really simple\))?$/.test(
|
|
183
|
+
normalized ?? "",
|
|
184
|
+
) ||
|
|
185
|
+
normalized === "ultragoal" ||
|
|
186
|
+
normalized === "/skill:ultragoal" ||
|
|
187
|
+
normalized === "gjc ultragoal"
|
|
188
|
+
? "ultragoal"
|
|
189
|
+
: undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
161
192
|
function isAskTimeoutError(error: unknown): boolean {
|
|
162
193
|
return typeof error === "object" && error !== null && (error as { code?: unknown }).code === GJC_ASK_TIMEOUT_CODE;
|
|
163
194
|
}
|
|
@@ -320,6 +351,8 @@ interface SelectionResult {
|
|
|
320
351
|
selectedOptions: string[];
|
|
321
352
|
customInput?: string;
|
|
322
353
|
clarificationQuestion?: string;
|
|
354
|
+
executionGateId?: string;
|
|
355
|
+
executionPresentation?: ExecutionApprovalPresentation;
|
|
323
356
|
timedOut: boolean;
|
|
324
357
|
navigation?: "back" | "forward";
|
|
325
358
|
cancelled?: boolean;
|
|
@@ -340,6 +373,7 @@ interface AskSingleQuestionOptions {
|
|
|
340
373
|
otherOptionLabel?: string;
|
|
341
374
|
clarificationOptionLabel?: string;
|
|
342
375
|
autoSelectOnTimeout?: boolean;
|
|
376
|
+
executionPresentation?: ExecutionApprovalPresentation;
|
|
343
377
|
onRemoteState?: (state: {
|
|
344
378
|
interaction: "selector" | "custom_editor" | "clarification_editor";
|
|
345
379
|
selectedCount: number;
|
|
@@ -849,6 +883,65 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
849
883
|
);
|
|
850
884
|
}
|
|
851
885
|
|
|
886
|
+
/**
|
|
887
|
+
* The Deep Interview execution choice is the only ask answer that can mint
|
|
888
|
+
* execution authority. The runtime records only the redacted answer hash;
|
|
889
|
+
* the subsequent CLI action must consume that record before it can approve
|
|
890
|
+
* the canonical Crystal.
|
|
891
|
+
*/
|
|
892
|
+
async #recordDeepInterviewExecutionApproval(
|
|
893
|
+
q: AskParams["questions"][number],
|
|
894
|
+
selectedOptions: string[],
|
|
895
|
+
customInput: string | undefined,
|
|
896
|
+
toolCallId: string,
|
|
897
|
+
executionGateId?: string,
|
|
898
|
+
executionPresentation?: ExecutionApprovalPresentation,
|
|
899
|
+
): Promise<void> {
|
|
900
|
+
const deepInterviewExecution = q.workflowGate?.stage === "deep-interview" && q.workflowGate.kind === "execution";
|
|
901
|
+
const ralplanApproval = q.workflowGate?.stage === "ralplan" && q.workflowGate.kind === "approval";
|
|
902
|
+
if (!deepInterviewExecution && !ralplanApproval) return;
|
|
903
|
+
const target = deepInterviewExecutionTarget(selectedOptions);
|
|
904
|
+
const sessionId = this.session.getSessionId?.();
|
|
905
|
+
if (!target || customInput !== undefined) {
|
|
906
|
+
if (sessionId) {
|
|
907
|
+
await revokeDeepInterviewExecutionApproval(this.session.cwd, sessionId);
|
|
908
|
+
await revokeNonCrystalExecutionApproval(
|
|
909
|
+
this.session.cwd,
|
|
910
|
+
sessionId,
|
|
911
|
+
ralplanApproval ? "ralplan" : "deep-interview",
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
if (!sessionId) throw new ToolAbortError("Deep Interview execution approval requires a session");
|
|
917
|
+
if (!executionPresentation)
|
|
918
|
+
throw new ToolAbortError("Execution approval publication could not be authenticated before consent");
|
|
919
|
+
const approvalStage = ralplanApproval ? "ralplan" : "deep-interview";
|
|
920
|
+
const lineage = await executionApprovalLineage(this.session.cwd, sessionId, approvalStage);
|
|
921
|
+
const transcriptEvidence =
|
|
922
|
+
lineage === "crystal"
|
|
923
|
+
? await assertDeepInterviewCrystalCoversLiveTranscript(this.session.cwd, sessionId, deepInterviewExecution)
|
|
924
|
+
: await authoritativeConversationSnapshot(this.session.cwd, sessionId);
|
|
925
|
+
const recordApproval =
|
|
926
|
+
lineage === "crystal" ? recordDeepInterviewExecutionApproval : recordNonCrystalExecutionApproval;
|
|
927
|
+
await recordApproval({
|
|
928
|
+
cwd: this.session.cwd,
|
|
929
|
+
sessionId,
|
|
930
|
+
questionId: q.id,
|
|
931
|
+
gateId: executionGateId ?? q.id,
|
|
932
|
+
toolCallId,
|
|
933
|
+
target,
|
|
934
|
+
selectedOptions,
|
|
935
|
+
transcriptPath: transcriptEvidence.transcriptPath,
|
|
936
|
+
transcriptSha256: transcriptEvidence.transcriptSha256,
|
|
937
|
+
approvalStage,
|
|
938
|
+
presentation: executionPresentation,
|
|
939
|
+
});
|
|
940
|
+
// Ralplan approval is consumed by the subsequent handoff, after the agent
|
|
941
|
+
// persists this Ask tool result. Consuming here would validate the boundary
|
|
942
|
+
// before the matching provider toolResult exists in the transcript.
|
|
943
|
+
}
|
|
944
|
+
|
|
852
945
|
async execute(
|
|
853
946
|
_toolCallId: string,
|
|
854
947
|
params: AskParams,
|
|
@@ -1200,6 +1293,14 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
1200
1293
|
// Send notification if waiting and not suppressed
|
|
1201
1294
|
this.#sendAskNotification();
|
|
1202
1295
|
|
|
1296
|
+
if (
|
|
1297
|
+
params.questions.filter(
|
|
1298
|
+
question =>
|
|
1299
|
+
(question.workflowGate?.stage === "deep-interview" && question.workflowGate.kind === "execution") ||
|
|
1300
|
+
(question.workflowGate?.stage === "ralplan" && question.workflowGate.kind === "approval"),
|
|
1301
|
+
).length > 1
|
|
1302
|
+
)
|
|
1303
|
+
throw new ToolError("Ask accepts at most one execution-authorizing workflow gate per invocation");
|
|
1203
1304
|
const askQuestion = async (
|
|
1204
1305
|
q: AskParams["questions"][number],
|
|
1205
1306
|
options?: {
|
|
@@ -1212,6 +1313,9 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
1212
1313
|
const recommended = validRecommendedIndex(q.recommended, rawOptionLabels.length);
|
|
1213
1314
|
const questionIndex = params.questions.indexOf(q);
|
|
1214
1315
|
const emptyCustomAttempt = options?.emptyCustomAttempt ?? 0;
|
|
1316
|
+
const isExecutionApprovalQuestion =
|
|
1317
|
+
(q.workflowGate?.stage === "deep-interview" && q.workflowGate.kind === "execution") ||
|
|
1318
|
+
(q.workflowGate?.stage === "ralplan" && q.workflowGate.kind === "approval");
|
|
1215
1319
|
// Route headless asks through the SDK workflow-gate emitter; a connected
|
|
1216
1320
|
// SDK responder supplies the durable answer instead of an interactive UI.
|
|
1217
1321
|
if (gateEmitter && canUseWorkflowGate) {
|
|
@@ -1226,19 +1330,79 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
1226
1330
|
allowEmpty: q.multi === true && params.questions.length > 1,
|
|
1227
1331
|
navigationLabel: questionIndex === params.questions.length - 1 ? "Done" : "Next",
|
|
1228
1332
|
};
|
|
1229
|
-
|
|
1333
|
+
let executionGateId: string | undefined = isExecutionApprovalQuestion
|
|
1334
|
+
? `interactive-${randomUUID()}`
|
|
1335
|
+
: undefined;
|
|
1336
|
+
let executionPresentation: ExecutionApprovalPresentation | undefined;
|
|
1337
|
+
if (isExecutionApprovalQuestion) {
|
|
1338
|
+
const sessionId = this.session.getSessionId?.();
|
|
1339
|
+
if (!sessionId) throw new ToolAbortError("Execution approval requires a session");
|
|
1340
|
+
try {
|
|
1341
|
+
executionPresentation = await captureExecutionApprovalPresentation(
|
|
1342
|
+
this.session.cwd,
|
|
1343
|
+
sessionId,
|
|
1344
|
+
q.workflowGate?.stage === "ralplan" ? "ralplan" : "deep-interview",
|
|
1345
|
+
);
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
throw new ToolAbortError(
|
|
1348
|
+
error instanceof Error
|
|
1349
|
+
? `Execution approval publication could not be authenticated: ${error.message}`
|
|
1350
|
+
: "Execution approval publication could not be authenticated",
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
const stopGateObservation = gateEmitter.onGateEmitted?.(gate => {
|
|
1355
|
+
const stageState = gate.context?.stage_state;
|
|
1356
|
+
if (
|
|
1357
|
+
gate.stage === q.workflowGate?.stage &&
|
|
1358
|
+
gate.kind === q.workflowGate?.kind &&
|
|
1359
|
+
((gate.stage === "deep-interview" && gate.kind === "execution") ||
|
|
1360
|
+
(gate.stage === "ralplan" && gate.kind === "approval")) &&
|
|
1361
|
+
stageState?.question_id === q.id
|
|
1362
|
+
)
|
|
1363
|
+
executionGateId = gate.gate_id;
|
|
1364
|
+
});
|
|
1365
|
+
let answer: unknown;
|
|
1366
|
+
try {
|
|
1367
|
+
answer = await gateEmitter.emitGate(questionToGate(gateQuestion));
|
|
1368
|
+
} finally {
|
|
1369
|
+
stopGateObservation?.();
|
|
1370
|
+
}
|
|
1230
1371
|
const decoded = gateAnswerToResult(gateQuestion, answer);
|
|
1231
1372
|
return {
|
|
1232
1373
|
optionLabels: rawOptionLabels,
|
|
1233
1374
|
selectedOptions: decoded.selectedOptions,
|
|
1234
1375
|
customInput: decoded.customInput,
|
|
1235
1376
|
clarificationQuestion: decoded.clarificationQuestion,
|
|
1377
|
+
executionGateId,
|
|
1378
|
+
executionPresentation,
|
|
1236
1379
|
navigation: undefined as NavigationControls | undefined,
|
|
1237
1380
|
cancelled: false,
|
|
1238
1381
|
timedOut: false,
|
|
1239
1382
|
};
|
|
1240
1383
|
}
|
|
1241
1384
|
try {
|
|
1385
|
+
let executionPresentation: ExecutionApprovalPresentation | undefined;
|
|
1386
|
+
if (isExecutionApprovalQuestion) {
|
|
1387
|
+
const sessionId = this.session.getSessionId?.();
|
|
1388
|
+
if (!sessionId) throw new ToolAbortError("Execution approval requires a session");
|
|
1389
|
+
try {
|
|
1390
|
+
executionPresentation = await captureExecutionApprovalPresentation(
|
|
1391
|
+
this.session.cwd,
|
|
1392
|
+
sessionId,
|
|
1393
|
+
q.workflowGate?.stage === "ralplan" ? "ralplan" : "deep-interview",
|
|
1394
|
+
);
|
|
1395
|
+
} catch (error) {
|
|
1396
|
+
throw new ToolAbortError(
|
|
1397
|
+
error instanceof Error
|
|
1398
|
+
? `Execution approval publication could not be authenticated: ${error.message}`
|
|
1399
|
+
: "Execution approval publication could not be authenticated",
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
const executionGateId: string | undefined = isExecutionApprovalQuestion
|
|
1404
|
+
? `interactive-${randomUUID()}`
|
|
1405
|
+
: undefined;
|
|
1242
1406
|
const deepInterviewPrompt = formatDeepInterviewSelectorPrompt(q.question);
|
|
1243
1407
|
const isDeepInterviewQuestion = deepInterviewPrompt !== null || q.deepInterview !== undefined;
|
|
1244
1408
|
const baseDisplayQuestion = deepInterviewPrompt ?? q.question;
|
|
@@ -1307,6 +1471,7 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
1307
1471
|
!intentReview(q.deepInterview) &&
|
|
1308
1472
|
(q.workflowGate === undefined || q.workflowGate.kind === "question"),
|
|
1309
1473
|
clarificationOptionLabel,
|
|
1474
|
+
executionPresentation,
|
|
1310
1475
|
onRemoteState: state => {
|
|
1311
1476
|
activeRemoteRequest = {
|
|
1312
1477
|
question: displayQuestion,
|
|
@@ -1395,6 +1560,8 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
1395
1560
|
selectedOptions,
|
|
1396
1561
|
customInput,
|
|
1397
1562
|
clarificationQuestion,
|
|
1563
|
+
executionGateId,
|
|
1564
|
+
executionPresentation,
|
|
1398
1565
|
navigation,
|
|
1399
1566
|
cancelled,
|
|
1400
1567
|
timedOut,
|
|
@@ -1418,8 +1585,16 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
1418
1585
|
|
|
1419
1586
|
if (params.questions.length === 1) {
|
|
1420
1587
|
const [q] = params.questions;
|
|
1421
|
-
const {
|
|
1422
|
-
|
|
1588
|
+
const {
|
|
1589
|
+
optionLabels,
|
|
1590
|
+
selectedOptions,
|
|
1591
|
+
customInput,
|
|
1592
|
+
clarificationQuestion,
|
|
1593
|
+
executionGateId,
|
|
1594
|
+
executionPresentation,
|
|
1595
|
+
cancelled,
|
|
1596
|
+
timedOut,
|
|
1597
|
+
} = await askQuestion(q);
|
|
1423
1598
|
|
|
1424
1599
|
if (
|
|
1425
1600
|
!timedOut &&
|
|
@@ -1435,6 +1610,14 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
1435
1610
|
) {
|
|
1436
1611
|
await this.#recordDeepInterviewRound(q, selectedOptions, customInput);
|
|
1437
1612
|
}
|
|
1613
|
+
await this.#recordDeepInterviewExecutionApproval(
|
|
1614
|
+
q,
|
|
1615
|
+
selectedOptions,
|
|
1616
|
+
customInput,
|
|
1617
|
+
_toolCallId,
|
|
1618
|
+
executionGateId,
|
|
1619
|
+
executionPresentation,
|
|
1620
|
+
);
|
|
1438
1621
|
const details: AskToolDetails = {
|
|
1439
1622
|
question: q.question,
|
|
1440
1623
|
options: optionLabels,
|
|
@@ -1476,6 +1659,10 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
1476
1659
|
}
|
|
1477
1660
|
|
|
1478
1661
|
const resultsByIndex: Array<QuestionResult | undefined> = Array.from({ length: params.questions.length });
|
|
1662
|
+
const executionGateIdsByIndex: Array<string | undefined> = Array.from({ length: params.questions.length });
|
|
1663
|
+
const executionPresentationsByIndex: Array<ExecutionApprovalPresentation | undefined> = Array.from({
|
|
1664
|
+
length: params.questions.length,
|
|
1665
|
+
});
|
|
1479
1666
|
let questionIndex = 0;
|
|
1480
1667
|
while (questionIndex < params.questions.length) {
|
|
1481
1668
|
const q = params.questions[questionIndex]!;
|
|
@@ -1490,6 +1677,8 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
1490
1677
|
selectedOptions,
|
|
1491
1678
|
customInput,
|
|
1492
1679
|
clarificationQuestion,
|
|
1680
|
+
executionGateId,
|
|
1681
|
+
executionPresentation,
|
|
1493
1682
|
navigation: navAction,
|
|
1494
1683
|
cancelled,
|
|
1495
1684
|
timedOut,
|
|
@@ -1509,6 +1698,8 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
1509
1698
|
customInput,
|
|
1510
1699
|
clarificationQuestion,
|
|
1511
1700
|
};
|
|
1701
|
+
executionGateIdsByIndex[questionIndex] = executionGateId;
|
|
1702
|
+
executionPresentationsByIndex[questionIndex] = executionPresentation;
|
|
1512
1703
|
|
|
1513
1704
|
if (
|
|
1514
1705
|
clarificationQuestion === undefined &&
|
|
@@ -1521,7 +1712,6 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
1521
1712
|
questionIndex = Math.max(0, questionIndex - 1);
|
|
1522
1713
|
continue;
|
|
1523
1714
|
}
|
|
1524
|
-
|
|
1525
1715
|
questionIndex += 1;
|
|
1526
1716
|
}
|
|
1527
1717
|
|
|
@@ -1538,6 +1728,15 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
1538
1728
|
});
|
|
1539
1729
|
|
|
1540
1730
|
const details: AskToolDetails = { results };
|
|
1731
|
+
for (const [index, result] of results.entries())
|
|
1732
|
+
await this.#recordDeepInterviewExecutionApproval(
|
|
1733
|
+
params.questions[index]!,
|
|
1734
|
+
result.selectedOptions,
|
|
1735
|
+
result.customInput,
|
|
1736
|
+
_toolCallId,
|
|
1737
|
+
executionGateIdsByIndex[index],
|
|
1738
|
+
executionPresentationsByIndex[index],
|
|
1739
|
+
);
|
|
1541
1740
|
const responseLines = results.map(formatQuestionResult);
|
|
1542
1741
|
const responseText = `User answers:\n${responseLines.join("\n")}`;
|
|
1543
1742
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
const MAX_ANCESTOR_WALK = 64;
|
|
5
|
+
|
|
6
|
+
function isInside(root: string, target: string): boolean {
|
|
7
|
+
const relative = path.relative(root, target);
|
|
8
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Path the native writer will mutate: realpath of an existing file, or realpath
|
|
13
|
+
* of the nearest existing ancestor plus the missing tail.
|
|
14
|
+
*/
|
|
15
|
+
export async function canonicalizeAstEditWritePath(absolutePath: string): Promise<string> {
|
|
16
|
+
const suffix: string[] = [];
|
|
17
|
+
let current = path.resolve(absolutePath);
|
|
18
|
+
for (let depth = 0; depth < MAX_ANCESTOR_WALK; depth++) {
|
|
19
|
+
try {
|
|
20
|
+
const real = await fs.realpath(current);
|
|
21
|
+
return suffix.length === 0 ? real : path.join(real, ...suffix.toReversed());
|
|
22
|
+
} catch {
|
|
23
|
+
const parent = path.dirname(current);
|
|
24
|
+
if (parent === current) break;
|
|
25
|
+
suffix.push(path.basename(current));
|
|
26
|
+
current = parent;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return path.resolve(absolutePath);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Preview paths as the workflow guard must see them.
|
|
34
|
+
*
|
|
35
|
+
* The guard compares lexically against the session cwd. A raw realpath misses
|
|
36
|
+
* `.gjc/**` when that cwd is not already canonical (`/var` vs `/private/var`,
|
|
37
|
+
* or a symlinked workspace), so an in-workspace real path is reattached to `cwd`.
|
|
38
|
+
*/
|
|
39
|
+
export async function resolveAstEditPreviewWritePaths(
|
|
40
|
+
cwd: string,
|
|
41
|
+
previewedPaths: readonly string[],
|
|
42
|
+
): Promise<string[]> {
|
|
43
|
+
const resolvedCwd = path.resolve(cwd);
|
|
44
|
+
let realCwd = resolvedCwd;
|
|
45
|
+
try {
|
|
46
|
+
realCwd = await fs.realpath(resolvedCwd);
|
|
47
|
+
} catch {
|
|
48
|
+
// Missing cwd has no canonical spelling; keep the lexical root.
|
|
49
|
+
}
|
|
50
|
+
const resolved: string[] = [];
|
|
51
|
+
for (const previewed of previewedPaths) {
|
|
52
|
+
const lexical = path.resolve(resolvedCwd, previewed);
|
|
53
|
+
const real = await canonicalizeAstEditWritePath(lexical);
|
|
54
|
+
const relative = path.relative(realCwd, real);
|
|
55
|
+
const canonical =
|
|
56
|
+
isInside(realCwd, real) && relative !== ""
|
|
57
|
+
? path.resolve(resolvedCwd, relative)
|
|
58
|
+
: isInside(realCwd, real)
|
|
59
|
+
? resolvedCwd
|
|
60
|
+
: real;
|
|
61
|
+
// The guard matches `.gjc` lexically. Keep the preview spelling so a
|
|
62
|
+
// symlinked `.gjc` directory is still recognized, and add the canonical
|
|
63
|
+
// spelling so `src` → `.gjc` is recognized too.
|
|
64
|
+
resolved.push(previewed);
|
|
65
|
+
if (path.resolve(canonical) !== lexical) resolved.push(canonical);
|
|
66
|
+
}
|
|
67
|
+
return resolved;
|
|
68
|
+
}
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -13,10 +13,13 @@ import { assertWorkflowMutationRawPathsAllowed } from "../skill-state/workflow-m
|
|
|
13
13
|
import { Ellipsis, fileHyperlink, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
14
14
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
15
15
|
import type { ToolSession } from ".";
|
|
16
|
+
import { resolveAstEditPreviewWritePaths } from "./ast-edit-write-path";
|
|
17
|
+
import { assertEditableFile } from "./auto-generated-guard";
|
|
16
18
|
import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
17
19
|
import { formatGroupedFiles } from "./grouped-file-output";
|
|
18
20
|
import type { OutputMeta } from "./output-meta";
|
|
19
21
|
import { resolveToolSearchScope } from "./path-utils";
|
|
22
|
+
import { enforcePlanModeWrite, resolvePlanPath } from "./plan-mode-guard";
|
|
20
23
|
import {
|
|
21
24
|
appendParseErrorsBulletList,
|
|
22
25
|
capParseErrors,
|
|
@@ -337,11 +340,17 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
337
340
|
label: `AST Edit: ${result.totalReplacements} replacement${previewReplacementPlural} in ${result.filesTouched} file${previewFilePlural}`,
|
|
338
341
|
sourceToolName: this.name,
|
|
339
342
|
apply: async (_reason: string) => {
|
|
343
|
+
for (const filePath of previewedFiles) {
|
|
344
|
+
enforcePlanModeWrite(this.session, filePath, { op: "update" });
|
|
345
|
+
}
|
|
340
346
|
await assertWorkflowMutationRawPathsAllowed({
|
|
341
347
|
cwd: this.session.cwd,
|
|
342
348
|
sessionId: this.session.getSessionId?.() ?? undefined,
|
|
343
|
-
rawPaths: previewedFiles,
|
|
349
|
+
rawPaths: await resolveAstEditPreviewWritePaths(this.session.cwd, previewedFiles),
|
|
344
350
|
});
|
|
351
|
+
for (const filePath of previewedFiles) {
|
|
352
|
+
await assertEditableFile(resolvePlanPath(this.session, filePath), filePath, this.session.settings);
|
|
353
|
+
}
|
|
345
354
|
const applyResult = await runAstEditOnce(multiTargets, resolvedSearchPath, globFilter, {
|
|
346
355
|
rewrites: normalizedRewrites,
|
|
347
356
|
dryRun: false,
|