@gajae-code/coding-agent 0.11.3 → 0.11.5

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 (213) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/types/cli/notify-cli.d.ts +5 -1
  3. package/dist/types/cli/worktree-cli.d.ts +8 -25
  4. package/dist/types/commands/gc.d.ts +4 -0
  5. package/dist/types/commands/worktree.d.ts +7 -32
  6. package/dist/types/config/settings-schema.d.ts +20 -0
  7. package/dist/types/export/html/template.generated.d.ts +1 -1
  8. package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
  9. package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
  10. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
  11. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
  12. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
  13. package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
  14. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
  15. package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
  16. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  17. package/dist/types/harness-control-plane/owner.d.ts +3 -1
  18. package/dist/types/internal-urls/local-protocol.d.ts +18 -0
  19. package/dist/types/main.d.ts +2 -2
  20. package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
  21. package/dist/types/modes/components/custom-editor.d.ts +8 -3
  22. package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
  23. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
  24. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  25. package/dist/types/modes/components/tool-transcript-format.d.ts +77 -0
  26. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
  27. package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
  28. package/dist/types/modes/controllers/input-controller.d.ts +8 -2
  29. package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
  30. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
  31. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
  32. package/dist/types/plan-mode/approved-plan.d.ts +1 -0
  33. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  34. package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
  35. package/dist/types/sdk/broker/broker.d.ts +1 -0
  36. package/dist/types/sdk/broker/discovery.d.ts +10 -0
  37. package/dist/types/sdk/broker/ensure.d.ts +24 -1
  38. package/dist/types/sdk/broker/internal.d.ts +3 -0
  39. package/dist/types/sdk/broker/session-index.d.ts +12 -0
  40. package/dist/types/sdk/broker/state-version.d.ts +2 -1
  41. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +39 -4
  43. package/dist/types/sdk/bus/config-commands.d.ts +8 -0
  44. package/dist/types/sdk/bus/config.d.ts +22 -0
  45. package/dist/types/sdk/bus/index.d.ts +162 -2
  46. package/dist/types/sdk/bus/notification-service.d.ts +71 -8
  47. package/dist/types/sdk/bus/session-control.d.ts +11 -8
  48. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
  49. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -3
  50. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
  51. package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
  52. package/dist/types/sdk/session.d.ts +16 -1
  53. package/dist/types/secrets/index.d.ts +4 -2
  54. package/dist/types/secrets/obfuscator.d.ts +1 -1
  55. package/dist/types/session/agent-session.d.ts +31 -2
  56. package/dist/types/session/artifacts.d.ts +30 -8
  57. package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
  58. package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
  59. package/dist/types/session/session-manager.d.ts +88 -18
  60. package/dist/types/session/session-storage.d.ts +27 -0
  61. package/dist/types/session/streaming-output.d.ts +4 -0
  62. package/dist/types/task/executor.d.ts +9 -0
  63. package/dist/types/tool-discovery/tool-index.d.ts +2 -0
  64. package/dist/types/tools/ask.d.ts +87 -12
  65. package/dist/types/tools/bash-skill-urls.d.ts +2 -2
  66. package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
  67. package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
  68. package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
  69. package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
  70. package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
  71. package/dist/types/tools/index.d.ts +4 -0
  72. package/dist/types/tools/path-utils.d.ts +9 -14
  73. package/dist/types/tools/render-utils.d.ts +1 -8
  74. package/dist/types/tools/resource-gc.d.ts +14 -0
  75. package/dist/types/utils/image-loading.d.ts +23 -0
  76. package/dist/types/utils/image-resize.d.ts +2 -0
  77. package/dist/types/utils/pasted-image-loading.d.ts +39 -0
  78. package/dist/types/utils/pasted-image-path.d.ts +21 -19
  79. package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
  80. package/dist/types/web/scrapers/types.d.ts +1 -5
  81. package/package.json +25 -9
  82. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  83. package/scripts/run-threat-model-check.ts +60 -0
  84. package/src/cli/notify-cli.ts +40 -18
  85. package/src/cli/worktree-cli.ts +9 -287
  86. package/src/cli.ts +10 -9
  87. package/src/commands/gc.ts +11 -1
  88. package/src/commands/sdk.ts +10 -4
  89. package/src/commands/worktree.ts +10 -53
  90. package/src/config/model-profiles.ts +15 -15
  91. package/src/config/settings-schema.ts +17 -0
  92. package/src/daemon/runtime.ts +2 -2
  93. package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
  94. package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
  95. package/src/defaults/gjc/skills/team/SKILL.md +7 -9
  96. package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
  97. package/src/eval/js/shared/runtime.ts +1 -1
  98. package/src/eval/py/index.ts +2 -2
  99. package/src/export/html/template.generated.ts +1 -1
  100. package/src/extensibility/plugins/marketplace/manager.ts +94 -10
  101. package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
  102. package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
  103. package/src/gjc-runtime/deep-interview-state.ts +127 -3
  104. package/src/gjc-runtime/gc-render.ts +19 -1
  105. package/src/gjc-runtime/gc-runtime.ts +68 -3
  106. package/src/gjc-runtime/launch-tmux.ts +61 -6
  107. package/src/gjc-runtime/managed-owner-admission.ts +352 -0
  108. package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
  109. package/src/gjc-runtime/state-argv.ts +292 -0
  110. package/src/gjc-runtime/state-runtime.ts +80 -222
  111. package/src/gjc-runtime/team-runtime.ts +1 -1
  112. package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
  113. package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
  114. package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
  115. package/src/harness-control-plane/owner.ts +59 -3
  116. package/src/hindsight/client.ts +65 -2
  117. package/src/internal-urls/agent-protocol.ts +76 -5
  118. package/src/internal-urls/docs-index.generated.ts +7 -6
  119. package/src/internal-urls/local-protocol.ts +416 -18
  120. package/src/lsp/index.ts +4 -88
  121. package/src/main.ts +37 -13
  122. package/src/modes/acp/acp-agent.ts +8 -0
  123. package/src/modes/components/ansi-display-validator.ts +54 -0
  124. package/src/modes/components/custom-editor.ts +103 -32
  125. package/src/modes/components/custom-provider-wizard.ts +26 -1
  126. package/src/modes/components/notifications-settings-editor.ts +26 -2
  127. package/src/modes/components/session-observer-overlay.ts +40 -37
  128. package/src/modes/components/settings-selector.ts +3 -1
  129. package/src/modes/components/tool-transcript-format.ts +393 -0
  130. package/src/modes/components/transcript-viewer-overlay.ts +63 -1
  131. package/src/modes/controllers/goal-mode-controller.ts +20 -1
  132. package/src/modes/controllers/input-controller.ts +98 -37
  133. package/src/modes/controllers/plan-mode-controller.ts +2 -0
  134. package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
  135. package/src/modes/controllers/selector-controller.ts +71 -9
  136. package/src/modes/interactive-mode.ts +14 -2
  137. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
  138. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
  139. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
  140. package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
  141. package/src/plan-mode/approved-plan.ts +3 -1
  142. package/src/prompts/agents/architect.md +1 -0
  143. package/src/prompts/agents/critic.md +1 -0
  144. package/src/prompts/system/custom-system-prompt.md +1 -1
  145. package/src/prompts/system/system-prompt.md +2 -1
  146. package/src/prompts/tools/lsp.md +3 -3
  147. package/src/runtime-mcp/manager.ts +16 -1
  148. package/src/runtime-mcp/oauth-discovery.ts +116 -26
  149. package/src/sdk/broker/broker.ts +139 -32
  150. package/src/sdk/broker/discovery.ts +125 -0
  151. package/src/sdk/broker/ensure.ts +103 -19
  152. package/src/sdk/broker/internal.ts +10 -0
  153. package/src/sdk/broker/session-index.ts +309 -38
  154. package/src/sdk/broker/state-version.ts +5 -2
  155. package/src/sdk/bus/chat-daemon-cli.ts +72 -10
  156. package/src/sdk/bus/chat-daemon-control.ts +526 -130
  157. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  158. package/src/sdk/bus/config-commands.ts +22 -0
  159. package/src/sdk/bus/config.ts +37 -0
  160. package/src/sdk/bus/index.ts +384 -92
  161. package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
  162. package/src/sdk/bus/notification-service.ts +284 -62
  163. package/src/sdk/bus/recent-activity.ts +14 -5
  164. package/src/sdk/bus/session-control.ts +150 -44
  165. package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
  166. package/src/sdk/bus/telegram-daemon-contract.ts +6 -3
  167. package/src/sdk/bus/telegram-daemon-control.ts +294 -76
  168. package/src/sdk/bus/telegram-daemon.ts +1842 -281
  169. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  170. package/src/sdk/session.ts +155 -27
  171. package/src/secrets/index.ts +53 -10
  172. package/src/secrets/obfuscator.ts +21 -18
  173. package/src/secrets/regex.ts +2 -0
  174. package/src/session/agent-session.ts +495 -253
  175. package/src/session/artifacts.ts +188 -23
  176. package/src/session/context-estimation.ts +7 -2
  177. package/src/session/internal/managed-session-scope.ts +215 -39
  178. package/src/session/internal/managed-session-storage.ts +1152 -46
  179. package/src/session/session-manager.ts +1055 -148
  180. package/src/session/session-storage.ts +109 -11
  181. package/src/session/streaming-output.ts +6 -0
  182. package/src/task/executor.ts +71 -26
  183. package/src/task/index.ts +63 -20
  184. package/src/tool-discovery/tool-index.ts +18 -0
  185. package/src/tools/ask.ts +329 -57
  186. package/src/tools/bash-allowed-prefixes.ts +79 -29
  187. package/src/tools/bash-skill-urls.ts +14 -3
  188. package/src/tools/bash.ts +2 -2
  189. package/src/tools/browser/launch.ts +3 -1
  190. package/src/tools/browser/readable.ts +1 -1
  191. package/src/tools/browser/screenshot-format.ts +34 -0
  192. package/src/tools/browser/tab-protocol.ts +3 -0
  193. package/src/tools/browser/tab-supervisor.ts +125 -122
  194. package/src/tools/browser/tab-worker-entry.ts +33 -14
  195. package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
  196. package/src/tools/browser/tab-worker-smoke.ts +87 -0
  197. package/src/tools/browser/tab-worker.ts +7 -5
  198. package/src/tools/index.ts +4 -0
  199. package/src/tools/path-utils.ts +42 -35
  200. package/src/tools/plan-mode-guard.ts +1 -0
  201. package/src/tools/read.ts +7 -1
  202. package/src/tools/render-utils.ts +1 -27
  203. package/src/tools/resource-gc.ts +123 -34
  204. package/src/tools/sqlite-reader.ts +3 -2
  205. package/src/tools/subagent.ts +2 -0
  206. package/src/utils/image-loading.ts +85 -34
  207. package/src/utils/image-resize.ts +16 -9
  208. package/src/utils/pasted-image-loading.ts +514 -0
  209. package/src/utils/pasted-image-path.ts +223 -98
  210. package/src/web/scrapers/html-to-markdown.ts +68 -0
  211. package/src/web/scrapers/readthedocs.ts +1 -1
  212. package/src/web/scrapers/types.ts +1 -68
  213. package/src/web/search/providers/openai-compatible.ts +76 -23
package/src/tools/ask.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
19
+ import type { RawArgumentValidationResult } from "@gajae-code/ai/types";
19
20
  import {
20
21
  type Component,
21
22
  Container,
@@ -36,11 +37,18 @@ import {
36
37
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
37
38
  import { appendOrMergeDeepInterviewRound, syncDeepInterviewRecorderHud } from "../gjc-runtime/deep-interview-recorder";
38
39
  import { deepInterviewStatePath } from "../gjc-runtime/deep-interview-runtime";
40
+ import {
41
+ assertDeepInterviewInputWithinLimit,
42
+ assertDeepInterviewStructuredResponseWithinLimit,
43
+ deepInterviewCharacterCount,
44
+ MAX_USER_RESPONSE_LENGTH,
45
+ } from "../gjc-runtime/deep-interview-state";
39
46
  import {
40
47
  type AskGateQuestion,
41
48
  gateAnswerToResult,
42
49
  questionToGate,
43
50
  } from "../modes/shared/agent-wire/deep-interview-gate";
51
+
44
52
  import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
45
53
  import askDescription from "../prompts/tools/ask.md" with { type: "text" };
46
54
  import { renderStatusLine } from "../tui";
@@ -62,15 +70,35 @@ import { assertUltragoalAskAllowed } from "./ultragoal-ask-guard";
62
70
  // Types
63
71
  // =============================================================================
64
72
 
73
+ function deepInterviewBoundedString(maximum: number) {
74
+ return z.string().superRefine((value, context) => {
75
+ if (deepInterviewCharacterCount(value) > maximum)
76
+ context.addIssue({
77
+ code: "too_big",
78
+ maximum,
79
+ inclusive: true,
80
+ origin: "string",
81
+ message: `Too big: expected string to have <=${maximum} characters`,
82
+ });
83
+ });
84
+ }
85
+
65
86
  const OptionItem = z.object({
66
87
  label: z.string().describe("display label"),
67
88
  });
68
89
 
90
+ const DEEP_INTERVIEW_INTENT_ID_PATTERN = /^(artifact|surface|integration|constraint):[a-z0-9][a-z0-9._/-]{0,127}$/;
91
+
92
+ const DeepInterviewReferenceId = z.string().superRefine((value, context) => {
93
+ if (!DEEP_INTERVIEW_INTENT_ID_PATTERN.test(value))
94
+ context.addIssue({ code: "custom", message: "invalid deep-interview intent ID" });
95
+ });
96
+
69
97
  const DeepInterviewIntentItem = z
70
98
  .object({
71
- id: z.string().regex(/^(artifact|surface|integration|constraint):[a-z0-9][a-z0-9._/-]{0,127}$/),
99
+ id: z.string().regex(DEEP_INTERVIEW_INTENT_ID_PATTERN),
72
100
  category: z.enum(["artifact", "surface", "integration", "constraint"]),
73
- statement: z.string().min(1).max(1_000),
101
+ statement: deepInterviewBoundedString(1_000).min(1),
74
102
  })
75
103
  .strict()
76
104
  .superRefine((value, context) => {
@@ -81,7 +109,7 @@ const DeepInterviewIntentItem = z
81
109
  const DeepInterviewIntentContract = z
82
110
  .object({
83
111
  items: z.array(DeepInterviewIntentItem).min(1).max(64),
84
- confirmation_options: z.array(z.string().min(1).max(200)).min(1).max(5),
112
+ confirmation_options: z.array(deepInterviewBoundedString(200).min(1)).min(1).max(5),
85
113
  })
86
114
  .strict();
87
115
 
@@ -92,53 +120,71 @@ const DeepInterviewIntentReview = z
92
120
  .array(
93
121
  z
94
122
  .object({
95
- removed_id: z
96
- .string()
97
- .regex(/^(artifact|surface|integration|constraint):[a-z0-9][a-z0-9._/-]{0,127}$/),
98
- replacement_ids: z
99
- .array(z.string().regex(/^(artifact|surface|integration|constraint):[a-z0-9][a-z0-9._/-]{0,127}$/))
100
- .min(1)
101
- .max(64),
102
- rationale: z.string().min(1).max(500),
123
+ removed_id: DeepInterviewReferenceId,
124
+ replacement_ids: z.array(DeepInterviewReferenceId).min(1).max(64),
125
+ rationale: deepInterviewBoundedString(500).min(1),
103
126
  })
104
127
  .strict(),
105
128
  )
106
129
  .max(64),
107
- approval_options: z.array(z.string().min(1).max(200)).min(1).max(5),
130
+ approval_options: z.array(deepInterviewBoundedString(200).min(1)).min(1).max(5),
108
131
  })
109
132
  .strict();
110
133
 
111
134
  /** Optional structured deep-interview round metadata; when present the round is recorded automatically. */
112
- const DeepInterviewMeta = z
113
- .object({
114
- round_id: z.string().max(128).describe("stable optional round identity").optional(),
115
- round: z.number().int().nonnegative().describe("round number"),
116
- component: z.string().min(1).max(128).describe("targeted topology component"),
117
- dimension: z.string().min(1).max(128).describe("targeted clarity dimension"),
118
- ambiguity: z.number().min(0).max(1).describe("ambiguity at ask time (0..1)"),
119
- intent_contract: DeepInterviewIntentContract.describe("Round-0 locked intent contract").optional(),
120
- intent_review: DeepInterviewIntentReview.describe("Locked-intent reduction review").optional(),
121
- })
122
- .strict()
123
- .superRefine((value, context) => {
124
- if (
125
- value.intent_contract &&
126
- (value.round !== 0 || value.component !== "review-topology" || value.dimension !== "topology")
135
+ const DeepInterviewMetadata = z.object({
136
+ round_id: deepInterviewBoundedString(128).describe("stable optional round identity").optional(),
137
+ round: z.number().int().nonnegative().describe("round number"),
138
+ component: deepInterviewBoundedString(128).min(1).describe("targeted topology component"),
139
+ dimension: deepInterviewBoundedString(128).min(1).describe("targeted clarity dimension"),
140
+ ambiguity: z.number().min(0).max(1).describe("ambiguity at ask time (0..1)"),
141
+ confused_terms: z
142
+ .array(deepInterviewBoundedString(256).min(1))
143
+ .max(32)
144
+ .describe("explicit terms the user does not understand; glossary help only, never inferred")
145
+ .optional(),
146
+ references: z
147
+ .array(
148
+ z
149
+ .object({
150
+ reference_id: deepInterviewBoundedString(256).min(1),
151
+ label: deepInterviewBoundedString(256).min(1),
152
+ origin: deepInterviewBoundedString(256).min(1),
153
+ url: deepInterviewBoundedString(2048).min(1).optional(),
154
+ excerpt: deepInterviewBoundedString(2048).min(1).optional(),
155
+ })
156
+ .strict(),
127
157
  )
128
- context.addIssue({
129
- code: "custom",
130
- message: "intent_contract requires Round 0 review-topology metadata",
131
- path: ["intent_contract"],
132
- });
133
- if (value.intent_review && value.round === 0)
134
- context.addIssue({
135
- code: "custom",
136
- message: "intent_review requires a post-Round-0 answer",
137
- path: ["intent_review"],
138
- });
139
- if (value.intent_contract && value.intent_review)
140
- context.addIssue({ code: "custom", message: "intent_contract and intent_review are mutually exclusive" });
141
- });
158
+ .max(32)
159
+ .describe("inert reference context for contrast questions only; url/excerpt are never auto-fetched")
160
+ .optional(),
161
+ });
162
+
163
+ const DeepInterviewMeta = z.union([
164
+ DeepInterviewMetadata.strict(),
165
+ DeepInterviewMetadata.extend({
166
+ round: z.literal(0),
167
+ component: z.literal("review-topology"),
168
+ dimension: z.literal("topology"),
169
+ intent_contract: DeepInterviewIntentContract,
170
+ }).strict(),
171
+ DeepInterviewMetadata.extend({
172
+ round: z.number().int().positive(),
173
+ intent_review: DeepInterviewIntentReview,
174
+ }).strict(),
175
+ ]);
176
+
177
+ type DeepInterviewMeta = z.infer<typeof DeepInterviewMeta>;
178
+
179
+ function intentContract(
180
+ metadata: DeepInterviewMeta | undefined,
181
+ ): z.infer<typeof DeepInterviewIntentContract> | undefined {
182
+ return metadata && "intent_contract" in metadata ? metadata.intent_contract : undefined;
183
+ }
184
+
185
+ function intentReview(metadata: DeepInterviewMeta | undefined): z.infer<typeof DeepInterviewIntentReview> | undefined {
186
+ return metadata && "intent_review" in metadata ? metadata.intent_review : undefined;
187
+ }
142
188
 
143
189
  const WorkflowGateMeta = z.object({
144
190
  stage: z.enum(["deep-interview", "ralplan", "ultragoal"]).describe("workflow gate stage"),
@@ -157,23 +203,48 @@ const QuestionItem = z
157
203
  })
158
204
  .superRefine((value, context) => {
159
205
  const labels = new Set(value.options.map(option => option.label));
160
- if ((value.deepInterview?.intent_contract || value.deepInterview?.intent_review) && value.multi === true)
206
+ const contract = intentContract(value.deepInterview);
207
+ const review = intentReview(value.deepInterview);
208
+ if (contract && review)
209
+ context.addIssue({
210
+ code: "custom",
211
+ message: "intent contract and review are mutually exclusive",
212
+ path: ["deepInterview"],
213
+ });
214
+ if (
215
+ contract &&
216
+ (value.deepInterview?.round !== 0 ||
217
+ value.deepInterview.component !== "review-topology" ||
218
+ value.deepInterview.dimension !== "topology")
219
+ )
220
+ context.addIssue({
221
+ code: "custom",
222
+ message: "intent contract requires round-0 review topology metadata",
223
+ path: ["deepInterview"],
224
+ });
225
+ if (review && (value.deepInterview?.round ?? 0) <= 0)
226
+ context.addIssue({
227
+ code: "custom",
228
+ message: "intent review requires a positive round",
229
+ path: ["deepInterview", "round"],
230
+ });
231
+ if ((contract || review) && value.multi === true)
161
232
  context.addIssue({ code: "custom", message: "intent gates must be single-select", path: ["multi"] });
162
- const confirmationOptions = value.deepInterview?.intent_contract?.confirmation_options ?? [];
233
+ const confirmationOptions = contract?.confirmation_options ?? [];
163
234
  if (new Set(confirmationOptions).size !== confirmationOptions.length)
164
235
  context.addIssue({
165
236
  code: "custom",
166
237
  message: "intent confirmation options must be unique",
167
238
  path: ["deepInterview", "intent_contract"],
168
239
  });
169
- const approvalOptions = value.deepInterview?.intent_review?.approval_options ?? [];
240
+ const approvalOptions = review?.approval_options ?? [];
170
241
  if (new Set(approvalOptions).size !== approvalOptions.length)
171
242
  context.addIssue({
172
243
  code: "custom",
173
244
  message: "intent approval options must be unique",
174
245
  path: ["deepInterview", "intent_review"],
175
246
  });
176
- for (const label of value.deepInterview?.intent_contract?.confirmation_options ?? []) {
247
+ for (const label of confirmationOptions) {
177
248
  if (!labels.has(label))
178
249
  context.addIssue({
179
250
  code: "custom",
@@ -181,7 +252,7 @@ const QuestionItem = z
181
252
  path: ["deepInterview", "intent_contract"],
182
253
  });
183
254
  }
184
- for (const label of value.deepInterview?.intent_review?.approval_options ?? []) {
255
+ for (const label of approvalOptions) {
185
256
  if (!labels.has(label))
186
257
  context.addIssue({
187
258
  code: "custom",
@@ -197,6 +268,195 @@ export const askSchema = z.object({
197
268
 
198
269
  export type AskToolInput = z.infer<typeof askSchema>;
199
270
 
271
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
272
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
273
+ const prototype = Object.getPrototypeOf(value);
274
+ return prototype === Object.prototype || prototype === null;
275
+ }
276
+
277
+ function isOnlyPlainData(value: unknown): boolean {
278
+ if (Array.isArray(value))
279
+ return (
280
+ Reflect.ownKeys(value).length === value.length + 1 &&
281
+ value.every((item, index) => Object.hasOwn(value, index) && isOnlyPlainData(item))
282
+ );
283
+ if (typeof value !== "object" || value === null) return true;
284
+ return isPlainRecord(value) && Object.values(value).every(isOnlyPlainData);
285
+ }
286
+
287
+ function hasExactOwnKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
288
+ const keys = Reflect.ownKeys(value);
289
+ return keys.length === allowed.length && keys.every(key => typeof key === "string" && allowed.includes(key));
290
+ }
291
+
292
+ function hasOnlyAllowedOwnKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
293
+ return Reflect.ownKeys(value).every(key => typeof key === "string" && allowed.includes(key));
294
+ }
295
+
296
+ function hasUniqueDisplayedLabels(labels: readonly string[], optionLabels: ReadonlySet<string>): boolean {
297
+ return new Set(labels).size === labels.length && labels.every(label => optionLabels.has(label));
298
+ }
299
+
300
+ /** Parse only to recognize a retired recovery shape; parsed values are never eligible for recovery. */
301
+ function parseEncodedContainer(value: unknown): unknown {
302
+ if (typeof value !== "string") return value;
303
+ try {
304
+ return JSON.parse(value);
305
+ } catch {
306
+ return value;
307
+ }
308
+ }
309
+
310
+ /** Whether malformed input is close enough to the retired pair shape to require a terminal rejection. */
311
+ function isRoundZeroRecoveryCandidate(value: unknown): boolean {
312
+ const root = parseEncodedContainer(value);
313
+ if (typeof root !== "object" || root === null || !Object.hasOwn(root, "questions")) return false;
314
+ const questionsValue = parseEncodedContainer((root as Record<string, unknown>).questions);
315
+ if (!Array.isArray(questionsValue)) return questionsValue === null;
316
+ return questionsValue.some(rawQuestion => {
317
+ const question = parseEncodedContainer(rawQuestion);
318
+ if (typeof question !== "object" || question === null || !Object.hasOwn(question, "deepInterview")) return false;
319
+ const deepInterview = parseEncodedContainer((question as Record<string, unknown>).deepInterview);
320
+ if (typeof deepInterview !== "object" || deepInterview === null) return deepInterview === null;
321
+ const metadata = deepInterview as Record<string, unknown>;
322
+ return Object.hasOwn(metadata, "intent_contract") || Object.hasOwn(metadata, "intent_review");
323
+ });
324
+ }
325
+
326
+ /** Remove only strict-provider null placeholders for fields optional in the canonical Ask contract. */
327
+ function normalizeRoundZeroOptionalNulls(arguments_: Record<string, unknown>): Record<string, unknown> {
328
+ if (!isPlainRecord(arguments_) || !Array.isArray(arguments_.questions) || arguments_.questions.length !== 1)
329
+ return arguments_;
330
+ const question = arguments_.questions[0];
331
+ if (!isPlainRecord(question) || !isPlainRecord(question.deepInterview)) return arguments_;
332
+ const normalizedQuestion = { ...question };
333
+ let changed = false;
334
+ for (const key of ["multi", "recommended", "workflowGate"] as const) {
335
+ if (Object.hasOwn(normalizedQuestion, key) && normalizedQuestion[key] === null) {
336
+ delete normalizedQuestion[key];
337
+ changed = true;
338
+ }
339
+ }
340
+ const normalizedDeepInterview = { ...question.deepInterview };
341
+ for (const key of ["round_id", "confused_terms", "references"] as const) {
342
+ if (Object.hasOwn(normalizedDeepInterview, key) && normalizedDeepInterview[key] === null) {
343
+ delete normalizedDeepInterview[key];
344
+ changed = true;
345
+ }
346
+ }
347
+ if (Array.isArray(normalizedDeepInterview.references)) {
348
+ const references = normalizedDeepInterview.references.map(reference => {
349
+ if (!isPlainRecord(reference)) return reference;
350
+ const normalizedReference = { ...reference };
351
+ for (const key of ["url", "excerpt"] as const) {
352
+ if (Object.hasOwn(normalizedReference, key) && normalizedReference[key] === null) {
353
+ delete normalizedReference[key];
354
+ changed = true;
355
+ }
356
+ }
357
+ return normalizedReference;
358
+ });
359
+ normalizedDeepInterview.references = references;
360
+ }
361
+ if (changed) normalizedQuestion.deepInterview = normalizedDeepInterview;
362
+ return changed ? { ...arguments_, questions: [normalizedQuestion] } : arguments_;
363
+ }
364
+ function recoverRoundZeroIntentContract(arguments_: Record<string, unknown>): RawArgumentValidationResult {
365
+ if (!isRoundZeroRecoveryCandidate(arguments_)) return { outcome: "passthrough" };
366
+ const normalizedArguments = normalizeRoundZeroOptionalNulls(arguments_);
367
+ if (!isOnlyPlainData(normalizedArguments) || !isPlainRecord(normalizedArguments)) return { outcome: "reject" };
368
+ if (
369
+ !hasExactOwnKeys(normalizedArguments, ["questions"]) ||
370
+ !Array.isArray(normalizedArguments.questions) ||
371
+ normalizedArguments.questions.length !== 1
372
+ )
373
+ return { outcome: "reject" };
374
+
375
+ const question = normalizedArguments.questions[0];
376
+ if (!isPlainRecord(question)) return { outcome: "reject" };
377
+ const questionKeys = ["id", "question", "options", "multi", "recommended", "deepInterview", "workflowGate"];
378
+ if (!hasOnlyAllowedOwnKeys(question, questionKeys)) return { outcome: "reject" };
379
+ if (
380
+ typeof question.id !== "string" ||
381
+ typeof question.question !== "string" ||
382
+ !Array.isArray(question.options) ||
383
+ !Object.hasOwn(question, "deepInterview") ||
384
+ !isPlainRecord(question.deepInterview) ||
385
+ (Object.hasOwn(question, "multi") && question.multi !== false) ||
386
+ (Object.hasOwn(question, "recommended") && typeof question.recommended !== "number")
387
+ )
388
+ return { outcome: "reject" };
389
+ const deepInterview = question.deepInterview;
390
+ const hasIntentContract = Object.hasOwn(deepInterview, "intent_contract");
391
+ const hasIntentReview = Object.hasOwn(deepInterview, "intent_review");
392
+ if (hasIntentContract !== hasIntentReview && askSchema.safeParse(normalizedArguments).success)
393
+ return { outcome: "passthrough" };
394
+
395
+ if (
396
+ Object.hasOwn(question, "workflowGate") &&
397
+ (!isPlainRecord(question.workflowGate) ||
398
+ !hasExactOwnKeys(question.workflowGate, ["stage", "kind"]) ||
399
+ question.workflowGate.stage !== "deep-interview" ||
400
+ question.workflowGate.kind !== "question")
401
+ )
402
+ return { outcome: "reject" };
403
+
404
+ if (
405
+ !question.options.every(
406
+ option => isPlainRecord(option) && hasExactOwnKeys(option, ["label"]) && typeof option.label === "string",
407
+ )
408
+ )
409
+ return { outcome: "reject" };
410
+ const optionLabels = question.options.map(option => (option as { label: string }).label);
411
+ if (new Set(optionLabels).size !== optionLabels.length) return { outcome: "reject" };
412
+
413
+ const deepInterviewKeys = [
414
+ "round_id",
415
+ "round",
416
+ "component",
417
+ "dimension",
418
+ "ambiguity",
419
+ "confused_terms",
420
+ "references",
421
+ "intent_contract",
422
+ "intent_review",
423
+ ];
424
+ if (
425
+ !hasOnlyAllowedOwnKeys(deepInterview, deepInterviewKeys) ||
426
+ !Object.hasOwn(deepInterview, "intent_contract") ||
427
+ !Object.hasOwn(deepInterview, "intent_review") ||
428
+ deepInterview.round !== 0 ||
429
+ typeof deepInterview.component !== "string" ||
430
+ deepInterview.component !== "review-topology" ||
431
+ typeof deepInterview.dimension !== "string" ||
432
+ deepInterview.dimension !== "topology" ||
433
+ typeof deepInterview.ambiguity !== "number" ||
434
+ (Object.hasOwn(deepInterview, "round_id") && typeof deepInterview.round_id !== "string")
435
+ )
436
+ return { outcome: "reject" };
437
+
438
+ const contract = DeepInterviewIntentContract.safeParse(deepInterview.intent_contract);
439
+ const review = DeepInterviewIntentReview.safeParse(deepInterview.intent_review);
440
+ if (!contract.success || !review.success) return { outcome: "reject" };
441
+ const displayedLabels = new Set(optionLabels);
442
+ if (
443
+ !hasUniqueDisplayedLabels(contract.data.confirmation_options, displayedLabels) ||
444
+ !hasUniqueDisplayedLabels(review.data.approval_options, displayedLabels)
445
+ )
446
+ return { outcome: "reject" };
447
+
448
+ const { intent_review: _intentReview, ...recoveredDeepInterview } = deepInterview;
449
+ const recovered = {
450
+ questions: [
451
+ {
452
+ ...question,
453
+ deepInterview: { ...recoveredDeepInterview, intent_contract: contract.data },
454
+ },
455
+ ],
456
+ };
457
+ return askSchema.safeParse(recovered).success ? { outcome: "accept", arguments: recovered } : { outcome: "reject" };
458
+ }
459
+
200
460
  /** Result for a single question */
201
461
  export interface QuestionResult {
202
462
  id: string;
@@ -803,6 +1063,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
803
1063
  readonly summary = "Ask the user a clarifying question";
804
1064
  readonly description: string;
805
1065
  readonly parameters = askSchema;
1066
+ readonly rawArgumentValidation = recoverRoundZeroIntentContract;
806
1067
  readonly strict = true;
807
1068
  readonly loadMode = "discoverable";
808
1069
 
@@ -811,7 +1072,9 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
811
1072
  }
812
1073
 
813
1074
  static createIf(session: ToolSession): AskTool | null {
814
- return session.hasUI || session.getWorkflowGateEmitter?.() ? new AskTool(session) : null;
1075
+ return session.hasUI || session.workflowGateEligible || session.getWorkflowGateEmitter?.()
1076
+ ? new AskTool(session)
1077
+ : null;
815
1078
  }
816
1079
 
817
1080
  /** Send terminal notification when ask tool is waiting for input */
@@ -832,6 +1095,8 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
832
1095
  customInput: string | undefined,
833
1096
  ): Promise<void> {
834
1097
  const meta = q.deepInterview;
1098
+ if (customInput !== undefined && (meta || isDeepInterviewAskQuestion(q.question)))
1099
+ assertDeepInterviewInputWithinLimit(customInput, MAX_USER_RESPONSE_LENGTH, "user_response");
835
1100
  if (!meta) return;
836
1101
  const cwd = this.session.cwd;
837
1102
  const sessionId = this.session.getSessionId?.() ?? undefined;
@@ -850,14 +1115,14 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
850
1115
  ambiguity: meta.ambiguity,
851
1116
  selectedOptions,
852
1117
  customInput,
853
- intent_contract: meta.intent_contract,
854
- intent_review: meta.intent_review,
1118
+ intent_contract: intentContract(meta),
1119
+ intent_review: intentReview(meta),
855
1120
  },
856
1121
  { sessionId },
857
1122
  ).then(async () => {
858
1123
  await syncDeepInterviewRecorderHud(cwd, statePath, sessionId);
859
1124
  }),
860
- meta.intent_contract !== undefined || meta.intent_review !== undefined,
1125
+ intentContract(meta) !== undefined || intentReview(meta) !== undefined,
861
1126
  );
862
1127
  }
863
1128
 
@@ -872,6 +1137,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
872
1137
  activeSkillState: this.session.getActiveSkillState?.(),
873
1138
  sessionId: this.session.getSessionId?.() ?? null,
874
1139
  });
1140
+ assertDeepInterviewStructuredResponseWithinLimit(params);
875
1141
  let activeRemoteReceipt: AskRemoteReceipt | undefined;
876
1142
  let activeRemoteRequest: AskAnswerRequest | undefined;
877
1143
  let remoteGeneration = 0;
@@ -1190,7 +1456,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
1190
1456
  navigation: options?.navigation,
1191
1457
  scrollTitleRows: isDeepInterviewQuestion ? DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS : undefined,
1192
1458
  otherOptionLabel,
1193
- autoSelectOnTimeout: !q.deepInterview?.intent_contract && !q.deepInterview?.intent_review,
1459
+ autoSelectOnTimeout: !intentContract(q.deepInterview) && !intentReview(q.deepInterview),
1194
1460
  clarificationOptionLabel,
1195
1461
  onRemoteState: state => {
1196
1462
  activeRemoteRequest = {
@@ -1216,6 +1482,8 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
1216
1482
  return displayIndex >= 0 ? (rawOptionLabels[displayIndex] ?? selected) : selected;
1217
1483
  })
1218
1484
  : displaySelectedOptions;
1485
+ if ((isDeepInterviewQuestion || isDeepInterviewAskQuestion(q.question)) && customInput !== undefined)
1486
+ assertDeepInterviewInputWithinLimit(customInput, MAX_USER_RESPONSE_LENGTH, "user_response");
1219
1487
  if (activeRemoteReceipt) {
1220
1488
  const settlement: AskSettlement =
1221
1489
  clarificationQuestion !== undefined
@@ -1250,10 +1518,14 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
1250
1518
  timedOut,
1251
1519
  };
1252
1520
  } catch (error) {
1253
- await settleActiveRemote({
1254
- kind: "resolve_without_commit",
1255
- reason: error instanceof Error && error.name === "AbortError" ? "aborted" : "exception",
1256
- });
1521
+ await settleActiveRemote(
1522
+ error instanceof Error && error.message.includes("exceeds max length")
1523
+ ? { kind: "invalid", reason: "invalid_structured_answer" }
1524
+ : {
1525
+ kind: "resolve_without_commit",
1526
+ reason: error instanceof Error && error.name === "AbortError" ? "aborted" : "exception",
1527
+ },
1528
+ );
1257
1529
  activeRemoteRequest = undefined;
1258
1530
  if (error instanceof Error && error.name === "AbortError") {
1259
1531
  throw new ToolAbortError("Ask input was cancelled");
@@ -1277,7 +1549,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
1277
1549
  }
1278
1550
  if (
1279
1551
  clarificationQuestion === undefined &&
1280
- !(timedOut && (q.deepInterview?.intent_contract || q.deepInterview?.intent_review))
1552
+ !(timedOut && (intentContract(q.deepInterview) || intentReview(q.deepInterview)))
1281
1553
  ) {
1282
1554
  await this.#recordDeepInterviewRound(q, selectedOptions, customInput);
1283
1555
  }
@@ -1358,7 +1630,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
1358
1630
 
1359
1631
  if (
1360
1632
  clarificationQuestion === undefined &&
1361
- !(timedOut && (q.deepInterview?.intent_contract || q.deepInterview?.intent_review))
1633
+ !(timedOut && (intentContract(q.deepInterview) || intentReview(q.deepInterview)))
1362
1634
  ) {
1363
1635
  await this.#recordDeepInterviewRound(q, selectedOptions, customInput);
1364
1636
  }