@gajae-code/coding-agent 0.18.7 → 0.18.8

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.
Files changed (279) hide show
  1. package/CHANGELOG.md +279 -1
  2. package/dist/types/async/job-manager.d.ts +1 -1
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/capability/skill.d.ts +2 -0
  5. package/dist/types/capability/types.d.ts +2 -0
  6. package/dist/types/cli/public-command-errors.d.ts +3 -1
  7. package/dist/types/cli/setup-cli.d.ts +1 -1
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  10. package/dist/types/config/file-lock.d.ts +1 -0
  11. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  12. package/dist/types/config/model-profile-activation.d.ts +11 -3
  13. package/dist/types/config/model-registry.d.ts +4 -9
  14. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  15. package/dist/types/cursor-delete-path.d.ts +1 -0
  16. package/dist/types/cursor-mcp-name.d.ts +8 -0
  17. package/dist/types/customization/inventory.d.ts +2 -0
  18. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  21. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  22. package/dist/types/discovery/helpers.d.ts +6 -1
  23. package/dist/types/export/html/css-color.d.ts +2 -0
  24. package/dist/types/export/html/template.generated.d.ts +1 -1
  25. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  26. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  27. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  28. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  29. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  30. package/dist/types/extensibility/skill-management.d.ts +3 -1
  31. package/dist/types/extensibility/skills.d.ts +2 -0
  32. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  33. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  34. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  35. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  36. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  37. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  38. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  39. package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
  40. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  41. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  42. package/dist/types/lsp/workspace-path.d.ts +1 -0
  43. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  44. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  45. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  46. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  47. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  48. package/dist/types/modes/interactive-mode.d.ts +1 -0
  49. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  50. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  51. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  52. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  53. package/dist/types/progress/progress-contract.d.ts +154 -0
  54. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  55. package/dist/types/progress/project-progress.d.ts +141 -0
  56. package/dist/types/progress/render-progress.d.ts +28 -0
  57. package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
  58. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  59. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  60. package/dist/types/runtime-mcp/types.d.ts +6 -0
  61. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  62. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  63. package/dist/types/sdk/broker/broker.d.ts +18 -1
  64. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  65. package/dist/types/sdk/bus/index.d.ts +12 -1
  66. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  67. package/dist/types/sdk/client/client.d.ts +2 -0
  68. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  69. package/dist/types/sdk/index.d.ts +1 -0
  70. package/dist/types/sdk/router/session-router.d.ts +2 -0
  71. package/dist/types/sdk/session-list.d.ts +1 -1
  72. package/dist/types/sdk/session.d.ts +8 -0
  73. package/dist/types/session/agent-session.d.ts +38 -9
  74. package/dist/types/session/artifacts.d.ts +2 -2
  75. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  76. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  77. package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
  78. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  79. package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
  80. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
  81. package/dist/types/session/session-manager.d.ts +31 -5
  82. package/dist/types/session/session-retirement.d.ts +31 -7
  83. package/dist/types/session/session-storage.d.ts +62 -2
  84. package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
  85. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  86. package/dist/types/setup/credential-import.d.ts +6 -4
  87. package/dist/types/skill-state/active-state.d.ts +38 -0
  88. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  89. package/dist/types/task/executor.d.ts +3 -0
  90. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  91. package/dist/types/tools/index.d.ts +3 -0
  92. package/dist/types/tools/python.d.ts +9 -1
  93. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  94. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  95. package/dist/types/tools/todo-write.d.ts +6 -0
  96. package/dist/types/utils/tools-checksum.d.ts +7 -0
  97. package/package.json +7 -7
  98. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  99. package/scripts/verify-managed-empty-stop.ts +28 -0
  100. package/src/async/job-manager.ts +228 -122
  101. package/src/capability/index.ts +17 -0
  102. package/src/capability/mcp.ts +2 -0
  103. package/src/capability/skill.ts +2 -0
  104. package/src/capability/types.ts +2 -0
  105. package/src/cli/auth-broker-cli.ts +1 -1
  106. package/src/cli/customize-doctor.ts +15 -2
  107. package/src/cli/public-command-errors.ts +21 -1
  108. package/src/cli/setup-cli.ts +4 -4
  109. package/src/cli/skills-cli.ts +5 -0
  110. package/src/commands/deep-interview.ts +47 -8
  111. package/src/commands/sdk.ts +28 -8
  112. package/src/config/autorouting-tier-map.ts +117 -7
  113. package/src/config/file-lock.ts +307 -68
  114. package/src/config/mcp-schema.json +4 -0
  115. package/src/config/model-bindings-applier.ts +20 -0
  116. package/src/config/model-profile-activation.ts +240 -46
  117. package/src/config/model-registry.ts +47 -153
  118. package/src/config/skill-settings-defaults.ts +7 -0
  119. package/src/coordinator-mcp/policy.ts +3 -2
  120. package/src/cursor-delete-path.ts +10 -0
  121. package/src/cursor-mcp-name.ts +15 -0
  122. package/src/cursor.ts +13 -2
  123. package/src/customization/inventory.ts +3 -0
  124. package/src/debug/report-bundle-parent.ts +22 -0
  125. package/src/debug/report-bundle.ts +3 -0
  126. package/src/debug/system-info.ts +13 -1
  127. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  128. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  129. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  130. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  131. package/src/defaults/gjc-defaults.test.ts +18 -1
  132. package/src/defaults/gjc-defaults.ts +3 -0
  133. package/src/defaults/gjc-skills.generated.ts +58 -36
  134. package/src/discovery/agents.ts +6 -1
  135. package/src/discovery/builtin.ts +10 -0
  136. package/src/discovery/helpers.ts +159 -20
  137. package/src/discovery/mcp-json.ts +6 -0
  138. package/src/discovery/ssh.ts +1 -1
  139. package/src/discovery/substitute-plugin-root.ts +3 -1
  140. package/src/edit/file-read-cache.ts +1 -1
  141. package/src/eval/py/executor.ts +380 -120
  142. package/src/export/html/css-color.ts +9 -0
  143. package/src/export/html/index.ts +5 -4
  144. package/src/export/html/template.generated.ts +1 -1
  145. package/src/extensibility/extensions/runner.ts +4 -0
  146. package/src/extensibility/extensions/types.ts +7 -0
  147. package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
  148. package/src/extensibility/plugins/manager.ts +3 -2
  149. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  150. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  151. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  152. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  153. package/src/extensibility/skill-management.ts +6 -2
  154. package/src/extensibility/skills.ts +6 -1
  155. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  156. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  157. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  158. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  159. package/src/gjc-runtime/gc-runtime.ts +206 -9
  160. package/src/gjc-runtime/launch-worktree.ts +14 -4
  161. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  162. package/src/gjc-runtime/state-argv.ts +2 -0
  163. package/src/gjc-runtime/state-migrations.ts +91 -29
  164. package/src/gjc-runtime/state-runtime.ts +3716 -135
  165. package/src/gjc-runtime/state-writer.ts +83 -8
  166. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  167. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  168. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  169. package/src/harness-control-plane/operate.ts +12 -4
  170. package/src/harness-control-plane/owner.ts +5 -1
  171. package/src/hashline/parser.ts +48 -9
  172. package/src/hashline/recovery.ts +13 -4
  173. package/src/internal-urls/docs-index.generated.ts +18 -17
  174. package/src/internal-urls/skill-path.ts +19 -0
  175. package/src/internal-urls/skill-protocol.ts +2 -0
  176. package/src/lsp/edits.ts +6 -0
  177. package/src/lsp/workspace-path.ts +51 -0
  178. package/src/main.ts +195 -31
  179. package/src/modes/acp/acp-agent.ts +88 -4
  180. package/src/modes/components/custom-editor.ts +14 -0
  181. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  182. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  183. package/src/modes/components/extensions/state-manager.ts +15 -5
  184. package/src/modes/components/model-selector.ts +1 -4
  185. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  186. package/src/modes/components/status-line/segments.ts +4 -0
  187. package/src/modes/controllers/event-controller.ts +11 -4
  188. package/src/modes/controllers/input-controller.ts +74 -29
  189. package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
  190. package/src/modes/controllers/selector-controller.ts +28 -15
  191. package/src/modes/interactive-mode.ts +4 -0
  192. package/src/modes/runtime-init.ts +4 -0
  193. package/src/modes/utils/abort-message.ts +3 -1
  194. package/src/modes/utils/interrupt-ownership.ts +12 -0
  195. package/src/modes/utils/keybinding-matchers.ts +4 -0
  196. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  197. package/src/progress/collect-project-progress.ts +153 -0
  198. package/src/progress/progress-contract.ts +172 -0
  199. package/src/progress/progress-snapshot.ts +140 -0
  200. package/src/progress/project-progress.ts +446 -0
  201. package/src/progress/render-progress.ts +209 -0
  202. package/src/prompts/system/master-mode.md +1 -0
  203. package/src/prompts/tools/skill-discovery.md +1 -1
  204. package/src/runtime-mcp/config-writer.ts +10 -6
  205. package/src/runtime-mcp/config.ts +1 -0
  206. package/src/runtime-mcp/oauth-flow.ts +5 -0
  207. package/src/runtime-mcp/oauth-public-url.ts +8 -0
  208. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  209. package/src/runtime-mcp/smithery-registry.ts +22 -3
  210. package/src/runtime-mcp/transports/http.ts +2 -2
  211. package/src/runtime-mcp/types.ts +6 -0
  212. package/src/sdk/acp/adapter.ts +93 -8
  213. package/src/sdk/broker/broker-exit.ts +52 -23
  214. package/src/sdk/broker/broker.ts +75 -6
  215. package/src/sdk/broker/ensure.ts +2 -0
  216. package/src/sdk/broker/lifecycle.ts +711 -66
  217. package/src/sdk/broker/session-index.ts +191 -81
  218. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  219. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  220. package/src/sdk/bus/index.ts +238 -43
  221. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  222. package/src/sdk/cli/session-cli.ts +17 -3
  223. package/src/sdk/client/client.ts +28 -8
  224. package/src/sdk/host/query/handlers.ts +9 -1
  225. package/src/sdk/host/session-runtime.test.ts +2647 -294
  226. package/src/sdk/host/session-runtime.ts +426 -109
  227. package/src/sdk/host/surface-policy.ts +1 -0
  228. package/src/sdk/index.ts +8 -0
  229. package/src/sdk/lifecycle-session.ts +3 -1
  230. package/src/sdk/prompt-deadline-manager.ts +15 -0
  231. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  232. package/src/sdk/protocol/operation-registry.ts +9 -2
  233. package/src/sdk/router/session-router.ts +75 -12
  234. package/src/sdk/session-list.ts +1 -0
  235. package/src/sdk/session.ts +67 -38
  236. package/src/session/agent-session.ts +623 -224
  237. package/src/session/artifacts.ts +174 -70
  238. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  239. package/src/session/internal/managed-session-scope.ts +3457 -462
  240. package/src/session/internal/managed-session-storage.ts +376 -61
  241. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  242. package/src/session/internal/task-artifact-owner-access.ts +19 -3
  243. package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
  244. package/src/session/session-manager.ts +939 -133
  245. package/src/session/session-retirement.ts +573 -14
  246. package/src/session/session-storage.ts +417 -14
  247. package/src/session/task-artifact-owner-codec.ts +71 -8
  248. package/src/session/task-artifact-owner-retirement.ts +21 -19
  249. package/src/setup/credential-auto-import.ts +12 -3
  250. package/src/setup/credential-import.ts +176 -5
  251. package/src/setup/paseo/announce.ts +6 -1
  252. package/src/skill-state/active-state.ts +132 -2
  253. package/src/slash-commands/builtin-registry.ts +39 -9
  254. package/src/slash-commands/helpers/progress-report.ts +23 -0
  255. package/src/stt/setup.ts +3 -5
  256. package/src/task/commands.ts +3 -1
  257. package/src/task/executor.ts +4 -0
  258. package/src/task/index.ts +6 -0
  259. package/src/tools/ask.ts +205 -6
  260. package/src/tools/ast-edit.ts +4 -0
  261. package/src/tools/bash-allowed-prefixes.ts +15 -0
  262. package/src/tools/bash-skill-urls.ts +56 -3
  263. package/src/tools/bash.ts +48 -79
  264. package/src/tools/descriptors.ts +5 -1
  265. package/src/tools/eval.ts +56 -36
  266. package/src/tools/image-gen.ts +1 -1
  267. package/src/tools/index.ts +3 -0
  268. package/src/tools/python.ts +187 -66
  269. package/src/tools/recipe/index.ts +9 -0
  270. package/src/tools/recipe/recipe-guard.ts +19 -0
  271. package/src/tools/restricted-cwd.ts +10 -0
  272. package/src/tools/skill-discovery.ts +5 -0
  273. package/src/tools/skill.ts +28 -1
  274. package/src/tools/todo-write.ts +10 -1
  275. package/src/tools/tool-catalog.generated.ts +1 -1
  276. package/src/tools/write.ts +1 -0
  277. package/src/utils/tools-checksum.ts +34 -0
  278. package/src/utils/tools-manager.ts +24 -6
  279. package/src/web/search/providers/codex.ts +3 -2
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 { deepInterviewStatePath } from "../gjc-runtime/deep-interview-runtime";
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
- const answer = await awaitAskAnswer(() => gateEmitter.emitGate(questionToGate(gateQuestion)));
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 { optionLabels, selectedOptions, customInput, clarificationQuestion, cancelled, timedOut } =
1422
- await askQuestion(q);
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
 
@@ -17,6 +17,7 @@ import { createFileRecorder, formatResultPath } from "./file-recorder";
17
17
  import { formatGroupedFiles } from "./grouped-file-output";
18
18
  import type { OutputMeta } from "./output-meta";
19
19
  import { resolveToolSearchScope } from "./path-utils";
20
+ import { enforcePlanModeWrite } from "./plan-mode-guard";
20
21
  import {
21
22
  appendParseErrorsBulletList,
22
23
  capParseErrors,
@@ -337,6 +338,9 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
337
338
  label: `AST Edit: ${result.totalReplacements} replacement${previewReplacementPlural} in ${result.filesTouched} file${previewFilePlural}`,
338
339
  sourceToolName: this.name,
339
340
  apply: async (_reason: string) => {
341
+ for (const filePath of previewedFiles) {
342
+ enforcePlanModeWrite(this.session, filePath, { op: "update" });
343
+ }
340
344
  await assertWorkflowMutationRawPathsAllowed({
341
345
  cwd: this.session.cwd,
342
346
  sessionId: this.session.getSessionId?.() ?? undefined,
@@ -268,10 +268,25 @@ function validateReadOnlyCommand(words: readonly string[]): BashAllowedPrefixesC
268
268
  }
269
269
  }
270
270
 
271
+ const gitWrite = rejectGitHostAccess(words);
272
+ if (gitWrite) return gitWrite;
273
+
271
274
  return { allowed: true };
272
275
  }
273
276
 
277
+ function rejectGitHostAccess(words: readonly string[]): BashAllowedPrefixesCheck | undefined {
278
+ if (words[0] !== "git") return undefined;
279
+ for (const option of optionWords(words)) {
280
+ if (isLongOption(option, "--output") || isLongOption(option, "--no-index")) {
281
+ return { allowed: false, reason: "restricted bash does not allow git --output or --no-index" };
282
+ }
283
+ }
284
+ return undefined;
285
+ }
286
+
274
287
  function validateMatchedGjcCommand(words: readonly string[]): BashAllowedPrefixesCheck {
288
+ const gitWrite = rejectGitHostAccess(words);
289
+ if (gitWrite) return gitWrite;
275
290
  if (words[0] !== "gjc") return { allowed: true };
276
291
 
277
292
  if (words[1] === "ralplan") {
@@ -1,7 +1,13 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
+ import { isEnoent } from "@gajae-code/utils";
3
4
  import type { Skill } from "../extensibility/skills";
4
- import { initializeLocalRoot, type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
5
+ import {
6
+ initializeLocalRoot,
7
+ type LocalProtocolOptions,
8
+ resolveLocalRoot,
9
+ resolveLocalUrlToPath,
10
+ } from "../internal-urls/local-protocol";
5
11
  import { validateRelativePath } from "../internal-urls/skill-protocol";
6
12
  import type { InternalResource } from "../internal-urls/types";
7
13
  import { normalizeLocalScheme } from "./path-utils";
@@ -125,6 +131,52 @@ function matchSkillName(
125
131
  return { skill: undefined, suffix: undefined };
126
132
  }
127
133
 
134
+ function localPathEscapes(realRoot: string, candidate: string): boolean {
135
+ return candidate !== realRoot && !candidate.startsWith(`${realRoot}${path.sep}`);
136
+ }
137
+
138
+ async function canonicalLocalPath(localRoot: string, lexicalPath: string): Promise<string> {
139
+ const realRoot = await fs.realpath(localRoot);
140
+ try {
141
+ const realTarget = await fs.realpath(lexicalPath);
142
+ if (localPathEscapes(realRoot, realTarget)) {
143
+ throw new ToolError("local:// path escapes the session local root");
144
+ }
145
+ return realTarget;
146
+ } catch (error) {
147
+ if (error instanceof ToolError || !isEnoent(error)) throw error;
148
+ }
149
+
150
+ const missing: string[] = [];
151
+ let cursor = lexicalPath;
152
+ for (;;) {
153
+ let stat: Awaited<ReturnType<typeof fs.lstat>> | undefined;
154
+ try {
155
+ stat = await fs.lstat(cursor);
156
+ } catch (error) {
157
+ if (!isEnoent(error)) throw error;
158
+ }
159
+ if (stat) {
160
+ let realAncestor: string;
161
+ try {
162
+ realAncestor = await fs.realpath(cursor);
163
+ } catch (error) {
164
+ if (isEnoent(error)) throw new ToolError("local:// path escapes the session local root");
165
+ throw error;
166
+ }
167
+ const canonical = path.join(realAncestor, ...missing);
168
+ if (localPathEscapes(realRoot, canonical)) {
169
+ throw new ToolError("local:// path escapes the session local root");
170
+ }
171
+ return canonical;
172
+ }
173
+ const parent = path.dirname(cursor);
174
+ if (parent === cursor) throw new ToolError("local:// path escapes the session local root");
175
+ missing.unshift(path.basename(cursor));
176
+ cursor = parent;
177
+ }
178
+ }
179
+
128
180
  function extractScheme(url: string): SupportedInternalScheme | undefined {
129
181
  const match = /^([a-z][a-z0-9+.-]*):\/\//i.exec(url);
130
182
  if (!match) return undefined;
@@ -180,10 +232,11 @@ async function resolveInternalUrlToPath(
180
232
  }
181
233
  await initializeLocalRoot(localOptions);
182
234
  const resolvedLocalPath = resolveLocalUrlToPath(url, localOptions);
235
+ const realTarget = await canonicalLocalPath(resolveLocalRoot(localOptions), resolvedLocalPath);
183
236
  if (ensureLocalParentDirs) {
184
- await fs.mkdir(path.dirname(resolvedLocalPath), { recursive: true });
237
+ await fs.mkdir(path.dirname(realTarget), { recursive: true });
185
238
  }
186
- return resolvedLocalPath;
239
+ return realTarget;
187
240
  }
188
241
 
189
242
  if (!internalRouter?.canHandle(url)) {