@gajae-code/coding-agent 0.11.4 → 0.11.6

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 (154) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/types/cli/notify-cli.d.ts +5 -1
  3. package/dist/types/config/settings-schema.d.ts +31 -1
  4. package/dist/types/export/html/template.generated.d.ts +1 -1
  5. package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
  6. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
  7. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
  8. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
  9. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
  10. package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  12. package/dist/types/harness-control-plane/owner.d.ts +3 -1
  13. package/dist/types/internal-urls/local-protocol.d.ts +18 -0
  14. package/dist/types/main.d.ts +2 -2
  15. package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
  16. package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
  17. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
  18. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  19. package/dist/types/modes/components/tool-transcript-format.d.ts +58 -0
  20. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
  21. package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
  22. package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
  23. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
  24. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
  25. package/dist/types/plan-mode/approved-plan.d.ts +1 -0
  26. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  27. package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
  28. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
  29. package/dist/types/sdk/bus/chat-daemon-control.d.ts +42 -4
  30. package/dist/types/sdk/bus/config-commands.d.ts +8 -0
  31. package/dist/types/sdk/bus/config.d.ts +22 -0
  32. package/dist/types/sdk/bus/index.d.ts +162 -2
  33. package/dist/types/sdk/bus/notification-service.d.ts +71 -8
  34. package/dist/types/sdk/bus/session-control.d.ts +11 -8
  35. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
  36. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -3
  37. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
  38. package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
  39. package/dist/types/sdk/session.d.ts +16 -1
  40. package/dist/types/secrets/index.d.ts +4 -2
  41. package/dist/types/secrets/obfuscator.d.ts +1 -1
  42. package/dist/types/session/agent-session.d.ts +31 -2
  43. package/dist/types/session/artifacts.d.ts +30 -8
  44. package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
  45. package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
  46. package/dist/types/session/session-manager.d.ts +98 -18
  47. package/dist/types/session/session-storage.d.ts +27 -0
  48. package/dist/types/session/streaming-output.d.ts +4 -0
  49. package/dist/types/session/yield-queue.d.ts +8 -0
  50. package/dist/types/task/executor.d.ts +9 -0
  51. package/dist/types/tool-discovery/tool-index.d.ts +2 -0
  52. package/dist/types/tools/ask.d.ts +48 -0
  53. package/dist/types/tools/bash-skill-urls.d.ts +2 -2
  54. package/dist/types/tools/index.d.ts +4 -0
  55. package/package.json +9 -9
  56. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  57. package/src/cli/notify-cli.ts +40 -18
  58. package/src/commands/sdk.ts +7 -2
  59. package/src/config/settings-schema.ts +31 -1
  60. package/src/daemon/runtime.ts +2 -2
  61. package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
  62. package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
  63. package/src/defaults/gjc/skills/team/SKILL.md +7 -9
  64. package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
  65. package/src/eval/py/index.ts +2 -2
  66. package/src/export/html/template.generated.ts +1 -1
  67. package/src/extensibility/plugins/marketplace/manager.ts +94 -10
  68. package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
  69. package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
  70. package/src/gjc-runtime/deep-interview-state.ts +127 -3
  71. package/src/gjc-runtime/launch-tmux.ts +61 -6
  72. package/src/gjc-runtime/managed-owner-admission.ts +352 -0
  73. package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
  74. package/src/gjc-runtime/state-runtime.ts +38 -12
  75. package/src/gjc-runtime/team-runtime.ts +1 -1
  76. package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
  77. package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
  78. package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
  79. package/src/harness-control-plane/owner.ts +59 -3
  80. package/src/hindsight/client.ts +65 -2
  81. package/src/internal-urls/agent-protocol.ts +76 -5
  82. package/src/internal-urls/docs-index.generated.ts +8 -7
  83. package/src/internal-urls/local-protocol.ts +416 -18
  84. package/src/lsp/index.ts +4 -88
  85. package/src/main.ts +36 -12
  86. package/src/modes/acp/acp-agent.ts +8 -0
  87. package/src/modes/components/ansi-display-validator.ts +54 -0
  88. package/src/modes/components/notifications-settings-editor.ts +26 -2
  89. package/src/modes/components/session-observer-overlay.ts +40 -24
  90. package/src/modes/components/settings-selector.ts +3 -1
  91. package/src/modes/components/tool-transcript-format.ts +321 -0
  92. package/src/modes/components/transcript-viewer-overlay.ts +39 -17
  93. package/src/modes/controllers/command-controller.ts +15 -0
  94. package/src/modes/controllers/extension-ui-controller.ts +14 -2
  95. package/src/modes/controllers/goal-mode-controller.ts +20 -1
  96. package/src/modes/controllers/plan-mode-controller.ts +2 -0
  97. package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
  98. package/src/modes/controllers/selector-controller.ts +70 -8
  99. package/src/modes/interactive-mode.ts +1 -0
  100. package/src/modes/runtime-init.ts +16 -2
  101. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
  102. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
  103. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
  104. package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
  105. package/src/plan-mode/approved-plan.ts +3 -1
  106. package/src/prompts/agents/architect.md +1 -0
  107. package/src/prompts/agents/critic.md +1 -0
  108. package/src/prompts/system/custom-system-prompt.md +1 -1
  109. package/src/prompts/system/system-prompt.md +2 -1
  110. package/src/prompts/tools/lsp.md +3 -3
  111. package/src/runtime-mcp/manager.ts +16 -1
  112. package/src/runtime-mcp/oauth-discovery.ts +116 -26
  113. package/src/sdk/broker/session-index.ts +122 -64
  114. package/src/sdk/bus/chat-daemon-cli.ts +72 -10
  115. package/src/sdk/bus/chat-daemon-control.ts +538 -130
  116. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  117. package/src/sdk/bus/config-commands.ts +22 -0
  118. package/src/sdk/bus/config.ts +37 -0
  119. package/src/sdk/bus/index.ts +388 -94
  120. package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
  121. package/src/sdk/bus/notification-service.ts +284 -62
  122. package/src/sdk/bus/recent-activity.ts +14 -5
  123. package/src/sdk/bus/session-control.ts +150 -44
  124. package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
  125. package/src/sdk/bus/telegram-daemon-contract.ts +11 -3
  126. package/src/sdk/bus/telegram-daemon-control.ts +312 -76
  127. package/src/sdk/bus/telegram-daemon.ts +1916 -285
  128. package/src/sdk/host/control/dispatch.ts +14 -5
  129. package/src/sdk/protocol/operation-inventory.generated.json +29 -0
  130. package/src/sdk/session.ts +155 -27
  131. package/src/secrets/index.ts +53 -10
  132. package/src/secrets/obfuscator.ts +21 -18
  133. package/src/secrets/regex.ts +2 -0
  134. package/src/session/agent-session.ts +1447 -852
  135. package/src/session/artifacts.ts +188 -23
  136. package/src/session/context-estimation.ts +7 -2
  137. package/src/session/internal/managed-session-scope.ts +200 -38
  138. package/src/session/internal/managed-session-storage.ts +1152 -46
  139. package/src/session/session-manager.ts +1121 -148
  140. package/src/session/session-storage.ts +109 -11
  141. package/src/session/streaming-output.ts +6 -0
  142. package/src/session/yield-queue.ts +20 -0
  143. package/src/slash-commands/builtin-registry.ts +36 -0
  144. package/src/task/executor.ts +71 -26
  145. package/src/task/index.ts +63 -20
  146. package/src/tool-discovery/tool-index.ts +18 -0
  147. package/src/tools/ask-answer-registry.ts +15 -1
  148. package/src/tools/ask.ts +129 -32
  149. package/src/tools/bash-skill-urls.ts +14 -3
  150. package/src/tools/bash.ts +2 -2
  151. package/src/tools/index.ts +4 -0
  152. package/src/tools/plan-mode-guard.ts +1 -0
  153. package/src/tools/sqlite-reader.ts +3 -2
  154. package/src/tools/subagent.ts +2 -0
@@ -65,6 +65,24 @@ export function isMCPToolName(name: string): boolean {
65
65
  return name.startsWith("mcp__");
66
66
  }
67
67
 
68
+ /** Select persisted built-in discoveries that remain eligible in the current registry, allowlist, and baseline policy. */
69
+ export function selectRestorableDiscoveredBuiltinToolNames(
70
+ toolNames: Iterable<string>,
71
+ toolRegistry: ReadonlyMap<string, AgentTool>,
72
+ allowedNames?: ReadonlySet<string>,
73
+ excludedNames?: ReadonlySet<string>,
74
+ ): string[] {
75
+ const selected: string[] = [];
76
+ for (const name of toolNames) {
77
+ const tool = toolRegistry.get(name);
78
+ if (tool?.loadMode !== "discoverable") continue;
79
+ if (allowedNames && !allowedNames.has(name)) continue;
80
+ if (excludedNames?.has(name)) continue;
81
+ if (!selected.includes(name)) selected.push(name);
82
+ }
83
+ return selected;
84
+ }
85
+
68
86
  export function isMCPBridgeTool(tool: { name: string; mcpServerName?: unknown; mcpToolName?: unknown }): boolean {
69
87
  return (
70
88
  isMCPToolName(tool.name) &&
@@ -7,6 +7,7 @@
7
7
  * source; registering returns a disposer.
8
8
  */
9
9
 
10
+ import { logger } from "@gajae-code/utils";
10
11
  import type { WorkflowGateEmitter } from "../modes/shared/agent-wire/workflow-gate-broker";
11
12
  import type { AskAnswerSource } from "./index";
12
13
 
@@ -31,7 +32,20 @@ export function getAskAnswerSource(sessionId: string): AskAnswerSource | undefin
31
32
  export function notifyWorkflowGateEmitterChanged(sessionId: string, emitter: WorkflowGateEmitter | undefined): void {
32
33
  if (emitter) workflowGateEmitters.set(sessionId, emitter);
33
34
  else workflowGateEmitters.delete(sessionId);
34
- for (const listener of workflowGateListeners.get(sessionId) ?? []) listener(emitter);
35
+ for (const listener of workflowGateListeners.get(sessionId) ?? []) {
36
+ // Isolate each listener: a throwing observer must never escape into an
37
+ // emitter suspend/bind/restore step, which runs inside session-transition
38
+ // transactions (e.g. handoff) whose rollback/commit correctness depends on
39
+ // this notification being no-throw.
40
+ try {
41
+ listener(emitter);
42
+ } catch (error) {
43
+ logger.warn("Workflow-gate emitter listener threw during notification", {
44
+ sessionId,
45
+ error: error instanceof Error ? error.message : String(error),
46
+ });
47
+ }
48
+ }
35
49
  }
36
50
 
37
51
  /** Observe workflow-gate emitter installation even when it occurs after session_start. */
package/src/tools/ask.ts CHANGED
@@ -37,11 +37,18 @@ import {
37
37
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
38
38
  import { appendOrMergeDeepInterviewRound, syncDeepInterviewRecorderHud } from "../gjc-runtime/deep-interview-recorder";
39
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";
40
46
  import {
41
47
  type AskGateQuestion,
42
48
  gateAnswerToResult,
43
49
  questionToGate,
44
50
  } from "../modes/shared/agent-wire/deep-interview-gate";
51
+
45
52
  import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
46
53
  import askDescription from "../prompts/tools/ask.md" with { type: "text" };
47
54
  import { renderStatusLine } from "../tui";
@@ -63,15 +70,35 @@ import { assertUltragoalAskAllowed } from "./ultragoal-ask-guard";
63
70
  // Types
64
71
  // =============================================================================
65
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
+
66
86
  const OptionItem = z.object({
67
87
  label: z.string().describe("display label"),
68
88
  });
69
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
+
70
97
  const DeepInterviewIntentItem = z
71
98
  .object({
72
- 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),
73
100
  category: z.enum(["artifact", "surface", "integration", "constraint"]),
74
- statement: z.string().min(1).max(1_000),
101
+ statement: deepInterviewBoundedString(1_000).min(1),
75
102
  })
76
103
  .strict()
77
104
  .superRefine((value, context) => {
@@ -82,7 +109,7 @@ const DeepInterviewIntentItem = z
82
109
  const DeepInterviewIntentContract = z
83
110
  .object({
84
111
  items: z.array(DeepInterviewIntentItem).min(1).max(64),
85
- 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),
86
113
  })
87
114
  .strict();
88
115
 
@@ -93,43 +120,57 @@ const DeepInterviewIntentReview = z
93
120
  .array(
94
121
  z
95
122
  .object({
96
- removed_id: z
97
- .string()
98
- .regex(/^(artifact|surface|integration|constraint):[a-z0-9][a-z0-9._/-]{0,127}$/),
99
- replacement_ids: z
100
- .array(z.string().regex(/^(artifact|surface|integration|constraint):[a-z0-9][a-z0-9._/-]{0,127}$/))
101
- .min(1)
102
- .max(64),
103
- 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),
104
126
  })
105
127
  .strict(),
106
128
  )
107
129
  .max(64),
108
- 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),
109
131
  })
110
132
  .strict();
111
133
 
112
134
  /** Optional structured deep-interview round metadata; when present the round is recorded automatically. */
113
135
  const DeepInterviewMetadata = z.object({
114
- round_id: z.string().max(128).describe("stable optional round identity").optional(),
136
+ round_id: deepInterviewBoundedString(128).describe("stable optional round identity").optional(),
115
137
  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"),
138
+ component: deepInterviewBoundedString(128).min(1).describe("targeted topology component"),
139
+ dimension: deepInterviewBoundedString(128).min(1).describe("targeted clarity dimension"),
118
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(),
157
+ )
158
+ .max(32)
159
+ .describe("inert reference context for contrast questions only; url/excerpt are never auto-fetched")
160
+ .optional(),
119
161
  });
120
162
 
121
- /** Provider-visible branches keep ordinary, contract, and review metadata mutually exclusive. */
122
163
  const DeepInterviewMeta = z.union([
123
164
  DeepInterviewMetadata.strict(),
124
165
  DeepInterviewMetadata.extend({
125
- round: z.literal(0).describe("round number"),
126
- component: z.literal("review-topology").describe("targeted topology component"),
127
- dimension: z.literal("topology").describe("targeted clarity dimension"),
128
- intent_contract: DeepInterviewIntentContract.describe("Round-0 locked intent contract"),
166
+ round: z.literal(0),
167
+ component: z.literal("review-topology"),
168
+ dimension: z.literal("topology"),
169
+ intent_contract: DeepInterviewIntentContract,
129
170
  }).strict(),
130
171
  DeepInterviewMetadata.extend({
131
- round: z.number().int().positive().describe("round number"),
132
- intent_review: DeepInterviewIntentReview.describe("Locked-intent reduction review"),
172
+ round: z.number().int().positive(),
173
+ intent_review: DeepInterviewIntentReview,
133
174
  }).strict(),
134
175
  ]);
135
176
 
@@ -164,6 +205,29 @@ const QuestionItem = z
164
205
  const labels = new Set(value.options.map(option => option.label));
165
206
  const contract = intentContract(value.deepInterview);
166
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
+ });
167
231
  if ((contract || review) && value.multi === true)
168
232
  context.addIssue({ code: "custom", message: "intent gates must be single-select", path: ["multi"] });
169
233
  const confirmationOptions = contract?.confirmation_options ?? [];
@@ -211,7 +275,11 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
211
275
  }
212
276
 
213
277
  function isOnlyPlainData(value: unknown): boolean {
214
- if (Array.isArray(value)) return value.every(isOnlyPlainData);
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
+ );
215
283
  if (typeof value !== "object" || value === null) return true;
216
284
  return isPlainRecord(value) && Object.values(value).every(isOnlyPlainData);
217
285
  }
@@ -270,11 +338,27 @@ function normalizeRoundZeroOptionalNulls(arguments_: Record<string, unknown>): R
270
338
  }
271
339
  }
272
340
  const normalizedDeepInterview = { ...question.deepInterview };
273
- if (Object.hasOwn(normalizedDeepInterview, "round_id") && normalizedDeepInterview.round_id === null) {
274
- delete normalizedDeepInterview.round_id;
275
- normalizedQuestion.deepInterview = normalizedDeepInterview;
276
- changed = true;
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
+ }
277
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;
278
362
  return changed ? { ...arguments_, questions: [normalizedQuestion] } : arguments_;
279
363
  }
280
364
  function recoverRoundZeroIntentContract(arguments_: Record<string, unknown>): RawArgumentValidationResult {
@@ -332,6 +416,8 @@ function recoverRoundZeroIntentContract(arguments_: Record<string, unknown>): Ra
332
416
  "component",
333
417
  "dimension",
334
418
  "ambiguity",
419
+ "confused_terms",
420
+ "references",
335
421
  "intent_contract",
336
422
  "intent_review",
337
423
  ];
@@ -986,7 +1072,9 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
986
1072
  }
987
1073
 
988
1074
  static createIf(session: ToolSession): AskTool | null {
989
- return session.hasUI || session.getWorkflowGateEmitter?.() ? new AskTool(session) : null;
1075
+ return session.hasUI || session.workflowGateEligible || session.getWorkflowGateEmitter?.()
1076
+ ? new AskTool(session)
1077
+ : null;
990
1078
  }
991
1079
 
992
1080
  /** Send terminal notification when ask tool is waiting for input */
@@ -1007,6 +1095,8 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
1007
1095
  customInput: string | undefined,
1008
1096
  ): Promise<void> {
1009
1097
  const meta = q.deepInterview;
1098
+ if (customInput !== undefined && (meta || isDeepInterviewAskQuestion(q.question)))
1099
+ assertDeepInterviewInputWithinLimit(customInput, MAX_USER_RESPONSE_LENGTH, "user_response");
1010
1100
  if (!meta) return;
1011
1101
  const cwd = this.session.cwd;
1012
1102
  const sessionId = this.session.getSessionId?.() ?? undefined;
@@ -1047,6 +1137,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
1047
1137
  activeSkillState: this.session.getActiveSkillState?.(),
1048
1138
  sessionId: this.session.getSessionId?.() ?? null,
1049
1139
  });
1140
+ assertDeepInterviewStructuredResponseWithinLimit(params);
1050
1141
  let activeRemoteReceipt: AskRemoteReceipt | undefined;
1051
1142
  let activeRemoteRequest: AskAnswerRequest | undefined;
1052
1143
  let remoteGeneration = 0;
@@ -1391,6 +1482,8 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
1391
1482
  return displayIndex >= 0 ? (rawOptionLabels[displayIndex] ?? selected) : selected;
1392
1483
  })
1393
1484
  : displaySelectedOptions;
1485
+ if ((isDeepInterviewQuestion || isDeepInterviewAskQuestion(q.question)) && customInput !== undefined)
1486
+ assertDeepInterviewInputWithinLimit(customInput, MAX_USER_RESPONSE_LENGTH, "user_response");
1394
1487
  if (activeRemoteReceipt) {
1395
1488
  const settlement: AskSettlement =
1396
1489
  clarificationQuestion !== undefined
@@ -1425,10 +1518,14 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
1425
1518
  timedOut,
1426
1519
  };
1427
1520
  } catch (error) {
1428
- await settleActiveRemote({
1429
- kind: "resolve_without_commit",
1430
- reason: error instanceof Error && error.name === "AbortError" ? "aborted" : "exception",
1431
- });
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
+ );
1432
1529
  activeRemoteRequest = undefined;
1433
1530
  if (error instanceof Error && error.name === "AbortError") {
1434
1531
  throw new ToolAbortError("Ask input was cancelled");
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import type { Skill } from "../extensibility/skills";
4
- import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
4
+ import { initializeLocalRoot, type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
5
5
  import { validateRelativePath } from "../internal-urls/skill-protocol";
6
6
  import type { InternalResource } from "../internal-urls/types";
7
7
  import { normalizeLocalScheme } from "./path-utils";
@@ -162,12 +162,23 @@ async function resolveInternalUrlToPath(
162
162
  return resolveSkillUrlToPath(url, skills);
163
163
  }
164
164
 
165
+ // Managed artifact and agent resources carry session authority. Expanding
166
+ // either into a subprocess command would let that subprocess reopen, replace,
167
+ // or unlink the managed object by pathname. Keep those resources behind the
168
+ // internal reader boundary.
169
+ if (scheme === "artifact" || scheme === "agent") {
170
+ throw new ToolError(
171
+ `${scheme}:// URLs cannot be expanded in bash commands; read them through the internal reader instead.`,
172
+ );
173
+ }
174
+
165
175
  if (scheme === "local") {
166
176
  if (!localOptions) {
167
177
  throw new ToolError(
168
178
  "Cannot resolve local:// URL in bash command: local protocol options are unavailable for this session.",
169
179
  );
170
180
  }
181
+ await initializeLocalRoot(localOptions);
171
182
  const resolvedLocalPath = resolveLocalUrlToPath(url, localOptions);
172
183
  if (ensureLocalParentDirs) {
173
184
  await fs.mkdir(path.dirname(resolvedLocalPath), { recursive: true });
@@ -215,8 +226,8 @@ export function expandSkillUrls(command: string, skills: readonly Skill[]): stri
215
226
  }
216
227
 
217
228
  /**
218
- * Expand supported internal URLs in a bash command string to shell-escaped absolute paths.
219
- * Supported schemes: skill://, agent://, artifact://, memory://, rule://, local://
229
+ * Expand filesystem-safe internal URLs in a bash command string. `agent://` and
230
+ * `artifact://` remain internal-reader-only and are rejected before resolution.
220
231
  */
221
232
  export async function expandInternalUrls(command: string, options: InternalUrlExpansionOptions): Promise<string> {
222
233
  if (!command.includes("://") && !command.includes("local:/")) return command;
package/src/tools/bash.ts CHANGED
@@ -636,6 +636,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
636
636
  internalRouter: InternalUrlRouter.instance(),
637
637
  localOptions: {
638
638
  getArtifactsDir: this.session.getArtifactsDir,
639
+ isManagedDestination: this.session.isManagedSessionDestination,
639
640
  getSessionId: this.session.getSessionId,
640
641
  },
641
642
  };
@@ -643,7 +644,6 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
643
644
  ...internalUrlOptions,
644
645
  ensureLocalParentDirs: this.session.bashRestrictionProfile !== "read-only",
645
646
  });
646
- const sessionFile = this.session.getSessionFile?.() ?? null;
647
647
  const expandedEnv = env
648
648
  ? Object.fromEntries(
649
649
  await Promise.all(
@@ -662,7 +662,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
662
662
  : undefined;
663
663
  const resolvedEnv = {
664
664
  ...buildGjcRuntimeSessionEnv({
665
- sessionFile,
665
+ sessionFile: null,
666
666
  sessionId: this.session.getSessionId?.(),
667
667
  cwd: this.session.cwd,
668
668
  }),
@@ -216,6 +216,8 @@ export interface ToolSession {
216
216
  cwd: string;
217
217
  /** Whether UI is available */
218
218
  hasUI: boolean;
219
+ /** Whether this session will bind a workflow-gate emitter after tool construction. */
220
+ workflowGateEligible?: boolean;
219
221
  /** Skip Python kernel availability check and warmup */
220
222
  skipPythonPreflight?: boolean;
221
223
  /** Pre-loaded context files (AGENTS.md, etc) */
@@ -262,6 +264,8 @@ export interface ToolSession {
262
264
  requestForegroundBashBackground?: () => boolean;
263
265
  /** Get session ID */
264
266
  getSessionId?: () => string | null;
267
+ /** Whether local:// must use external managed scratch instead of artifacts/local. */
268
+ isManagedSessionDestination?: () => boolean;
265
269
  /** Get Hindsight runtime state for this agent session. */
266
270
  getHindsightSessionState?: () => HindsightSessionState | undefined;
267
271
  /** Agent identity used for IRC routing. Returns the registry id (e.g. "0-Main", "0-AuthLoader"). */
@@ -11,6 +11,7 @@ function resolveRawPath(session: ToolSession, targetPath: string): string {
11
11
  if (normalized.startsWith(LOCAL_SCHEME_PREFIX)) {
12
12
  return resolveLocalUrlToPath(normalized, {
13
13
  getArtifactsDir: session.getArtifactsDir,
14
+ isManagedDestination: session.isManagedSessionDestination,
14
15
  getSessionId: session.getSessionId,
15
16
  });
16
17
  }
@@ -12,6 +12,7 @@ const MAX_QUERY_LIMIT = 500;
12
12
  const MAX_RENDER_WIDTH = 120;
13
13
  const MAX_COLUMN_WIDTH = 40;
14
14
  const MIN_COLUMN_WIDTH = 1;
15
+ const SQLITE_USER_TABLE_PREDICATE = "name NOT LIKE 'sqlite!_%' ESCAPE '!'";
15
16
 
16
17
  type SqliteBinding = Exclude<SQLQueryBindings, Record<string, unknown>>;
17
18
 
@@ -182,7 +183,7 @@ function getTableMasterRow(db: Database, table: string): SqliteMasterRow {
182
183
  const row =
183
184
  db
184
185
  .prepare<SqliteMasterRow, [string]>(
185
- "SELECT name, sql FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name = ?",
186
+ `SELECT name, sql FROM sqlite_master WHERE type = 'table' AND ${SQLITE_USER_TABLE_PREDICATE} AND name = ?`,
186
187
  )
187
188
  .get(table) ?? null;
188
189
  if (!row) {
@@ -502,7 +503,7 @@ export function parseSqliteSelector(subPath: string, queryString: string): Sqlit
502
503
  export function listTables(db: Database): { name: string; rowCount: number }[] {
503
504
  const names = db
504
505
  .prepare<Pick<SqliteMasterRow, "name">, []>(
505
- "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name COLLATE NOCASE",
506
+ `SELECT name FROM sqlite_master WHERE type = 'table' AND ${SQLITE_USER_TABLE_PREDICATE} ORDER BY name COLLATE NOCASE`,
506
507
  )
507
508
  .all();
508
509
 
@@ -227,6 +227,8 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
227
227
  if (!result.ok && result.reason === "context_unavailable") throw new ToolError("context unavailable");
228
228
  if (!result.ok && result.reason === "not_found") {
229
229
  missing.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
230
+ } else if (!result.ok) {
231
+ throw new ToolError(`Failed to resume subagent ${record.subagentId}: ${result.reason ?? "unknown"}.`);
230
232
  } else {
231
233
  records.push(manager.getSubagentRecord(record.subagentId, ownerFilter) ?? record);
232
234
  }