@gajae-code/coding-agent 0.11.7 → 0.11.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 (180) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/types/cli/args.d.ts +0 -2
  3. package/dist/types/config/keybindings.d.ts +33 -3
  4. package/dist/types/config/model-profile-activation.d.ts +4 -3
  5. package/dist/types/config/model-profile-contract.d.ts +38 -0
  6. package/dist/types/config/settings-schema.d.ts +15 -2
  7. package/dist/types/edit/modes/patch.d.ts +13 -0
  8. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  9. package/dist/types/eval/py/executor.d.ts +1 -1
  10. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  11. package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
  12. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +1 -0
  13. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
  14. package/dist/types/main.d.ts +8 -0
  15. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  16. package/dist/types/modes/components/tool-status-header.d.ts +3 -2
  17. package/dist/types/modes/components/welcome.d.ts +2 -0
  18. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  19. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  20. package/dist/types/modes/interactive-mode.d.ts +5 -2
  21. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  22. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  23. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  24. package/dist/types/modes/types.d.ts +3 -0
  25. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  26. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  27. package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
  28. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
  29. package/dist/types/sdk/broker/broker.d.ts +2 -0
  30. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  31. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
  32. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  33. package/dist/types/sdk/bus/config.d.ts +2 -0
  34. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  35. package/dist/types/sdk/bus/index.d.ts +25 -0
  36. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  37. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
  38. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -3
  39. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
  41. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  42. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  43. package/dist/types/sdk/host/host.d.ts +1 -1
  44. package/dist/types/sdk/host/query/handlers.d.ts +9 -0
  45. package/dist/types/sdk/index.d.ts +2 -0
  46. package/dist/types/sdk/prompt-status.d.ts +88 -0
  47. package/dist/types/sdk/session-directory.d.ts +1 -1
  48. package/dist/types/sdk/startup-capability.d.ts +3 -0
  49. package/dist/types/session/agent-session.d.ts +2 -0
  50. package/dist/types/session/internal/managed-session-scope.d.ts +11 -2
  51. package/dist/types/session/internal/managed-session-storage.d.ts +17 -2
  52. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  53. package/dist/types/session/session-storage.d.ts +18 -0
  54. package/dist/types/task/index.d.ts +3 -1
  55. package/dist/types/task/receipt.d.ts +2 -0
  56. package/dist/types/task/types.d.ts +94 -0
  57. package/dist/types/tools/ask.d.ts +454 -174
  58. package/dist/types/tools/image-gen.d.ts +47 -1
  59. package/dist/types/tools/index.d.ts +4 -0
  60. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  61. package/package.json +8 -8
  62. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  63. package/scripts/dogfood-repository-binding.ts +209 -0
  64. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  65. package/scripts/run-test-manifest.ts +4 -4
  66. package/src/cli/args.ts +2 -92
  67. package/src/cli/notify-cli.ts +5 -38
  68. package/src/cli.ts +18 -0
  69. package/src/commands/sdk.ts +2 -2
  70. package/src/config/keybindings.ts +157 -24
  71. package/src/config/model-profile-activation.ts +7 -18
  72. package/src/config/model-profile-contract.ts +161 -0
  73. package/src/config/model-registry.ts +11 -1
  74. package/src/config/settings-schema.ts +17 -2
  75. package/src/coordinator-mcp/model-preset.ts +21 -74
  76. package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
  77. package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
  78. package/src/edit/index.ts +3 -6
  79. package/src/edit/modes/patch.ts +38 -1
  80. package/src/edit/modes/replace.ts +42 -0
  81. package/src/edit/path-mutation-lock.ts +112 -0
  82. package/src/eval/py/executor.ts +30 -11
  83. package/src/eval/py/prelude.py +2 -2
  84. package/src/eval/py/tool-bridge.ts +34 -18
  85. package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
  86. package/src/gjc-runtime/launch-tmux.ts +1 -1
  87. package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
  88. package/src/gjc-runtime/ralplan-runtime.ts +75 -3
  89. package/src/gjc-runtime/repository-binding.ts +267 -0
  90. package/src/gjc-runtime/session-state-sidecar.ts +25 -9
  91. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
  92. package/src/gjc-runtime/tmux-sessions.ts +45 -6
  93. package/src/gjc-runtime/ultragoal-guard.ts +5 -5
  94. package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
  95. package/src/internal-urls/docs-index.generated.ts +6 -5
  96. package/src/internal-urls/local-protocol.ts +33 -16
  97. package/src/internal-urls/mcp-protocol.ts +73 -11
  98. package/src/main.ts +25 -10
  99. package/src/modes/DESIGN.md +5 -0
  100. package/src/modes/acp/acp-agent.ts +50 -16
  101. package/src/modes/components/model-selector.ts +42 -8
  102. package/src/modes/components/plan-preview-overlay.ts +1 -1
  103. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  104. package/src/modes/components/tool-status-header.ts +20 -4
  105. package/src/modes/components/welcome.ts +22 -18
  106. package/src/modes/controllers/command-controller.ts +97 -54
  107. package/src/modes/controllers/event-controller.ts +10 -0
  108. package/src/modes/controllers/input-controller.ts +7 -2
  109. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  110. package/src/modes/controllers/selector-controller.ts +46 -54
  111. package/src/modes/interactive-mode.ts +41 -27
  112. package/src/modes/prompt-action-autocomplete.ts +32 -13
  113. package/src/modes/prompt-suggestion-controller.ts +96 -0
  114. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  115. package/src/modes/types.ts +3 -0
  116. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  117. package/src/modes/utils/ui-helpers.ts +5 -3
  118. package/src/prompts/agents/architect.md +0 -1
  119. package/src/prompts/agents/critic.md +0 -1
  120. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  121. package/src/prompts/system/system-prompt.md +6 -4
  122. package/src/prompts/tools/cron.md +3 -1
  123. package/src/runtime-mcp/manager.ts +51 -53
  124. package/src/runtime-mcp/oauth-flow.ts +0 -72
  125. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  126. package/src/runtime-mcp/transports/http.ts +13 -6
  127. package/src/sdk/broker/broker.ts +34 -20
  128. package/src/sdk/broker/lifecycle.ts +185 -36
  129. package/src/sdk/bus/chat-daemon-control.ts +18 -5
  130. package/src/sdk/bus/config-commands.ts +2 -2
  131. package/src/sdk/bus/config.ts +9 -1
  132. package/src/sdk/bus/conversation-store.ts +40 -6
  133. package/src/sdk/bus/index.ts +240 -65
  134. package/src/sdk/bus/lifecycle-commands.ts +47 -3
  135. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  136. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  137. package/src/sdk/bus/notification-orchestration.ts +12 -0
  138. package/src/sdk/bus/notification-service.ts +27 -2
  139. package/src/sdk/bus/prompt-reconciliation.ts +239 -0
  140. package/src/sdk/bus/recent-activity.ts +90 -26
  141. package/src/sdk/bus/telegram-daemon-contract.ts +13 -3
  142. package/src/sdk/bus/telegram-daemon-control.ts +145 -33
  143. package/src/sdk/bus/telegram-daemon.ts +835 -110
  144. package/src/sdk/bus/telegram-reference.ts +20 -2
  145. package/src/sdk/cli/session-cli.ts +15 -2
  146. package/src/sdk/host/control/dispatch.ts +1 -1
  147. package/src/sdk/host/control/operations.ts +1 -1
  148. package/src/sdk/host/host.ts +2 -6
  149. package/src/sdk/host/query/handlers.ts +70 -5
  150. package/src/sdk/index.ts +12 -0
  151. package/src/sdk/prompt-status.ts +97 -0
  152. package/src/sdk/protocol/operation-inventory.generated.json +47 -0
  153. package/src/sdk/protocol/operation-registry.ts +18 -4
  154. package/src/sdk/session-directory.ts +5 -1
  155. package/src/sdk/session.ts +2 -0
  156. package/src/sdk/startup-capability.ts +9 -1
  157. package/src/session/agent-session.ts +52 -5
  158. package/src/session/blob-store.ts +7 -2
  159. package/src/session/internal/managed-session-scope.ts +854 -84
  160. package/src/session/internal/managed-session-storage.ts +251 -199
  161. package/src/session/internal/native-publish-outcome.ts +266 -0
  162. package/src/session/session-manager.ts +310 -327
  163. package/src/session/session-storage.ts +128 -11
  164. package/src/task/gjc-command.ts +2 -2
  165. package/src/task/index.ts +121 -15
  166. package/src/task/receipt.ts +3 -0
  167. package/src/task/types.ts +34 -3
  168. package/src/tools/ask.ts +198 -88
  169. package/src/tools/ast-edit.ts +2 -2
  170. package/src/tools/bash-pty-selection.ts +2 -2
  171. package/src/tools/browser/screenshot-format.ts +6 -1
  172. package/src/tools/cron.ts +1 -1
  173. package/src/tools/image-gen.ts +178 -6
  174. package/src/tools/index.ts +4 -0
  175. package/src/tools/render-utils.ts +7 -1
  176. package/src/utils/edit-mode.ts +2 -2
  177. package/src/utils/prompt-suggestion.ts +265 -0
  178. package/src/utils/sixel.ts +3 -3
  179. package/src/web/insane/url-guard.ts +3 -1
  180. package/src/web/scrapers/docs-rs.ts +30 -5
package/src/tools/ask.ts CHANGED
@@ -160,20 +160,23 @@ const DeepInterviewMetadata = z.object({
160
160
  .optional(),
161
161
  });
162
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
-
163
+ const DeepInterviewTopologyMeta = DeepInterviewMetadata.extend({
164
+ round: z.literal(0).describe("Round 0 topology confirmation"),
165
+ component: z.literal("review-topology"),
166
+ dimension: z.literal("topology"),
167
+ intent_contract: DeepInterviewIntentContract.describe("required Round 0 locked-intent contract"),
168
+ }).strict();
169
+
170
+ const DeepInterviewRoundMeta = DeepInterviewMetadata.extend({
171
+ round: z.number().int().positive().describe("positive interview round number"),
172
+ }).strict();
173
+
174
+ const DeepInterviewReviewMeta = DeepInterviewMetadata.extend({
175
+ round: z.number().int().positive().describe("positive post-Round-0 review number"),
176
+ intent_review: DeepInterviewIntentReview.describe("post-Round-0 locked-intent reduction review"),
177
+ }).strict();
178
+
179
+ const DeepInterviewMeta = z.union([DeepInterviewTopologyMeta, DeepInterviewRoundMeta, DeepInterviewReviewMeta]);
177
180
  type DeepInterviewMeta = z.infer<typeof DeepInterviewMeta>;
178
181
 
179
182
  function intentContract(
@@ -191,81 +194,118 @@ const WorkflowGateMeta = z.object({
191
194
  kind: z.enum(["question", "approval", "execution"]).describe("workflow gate kind"),
192
195
  });
193
196
 
194
- const QuestionItem = z
195
- .object({
196
- id: z.string().describe("question id"),
197
- question: z.string().describe("question text"),
198
- options: z.array(OptionItem).describe("available options"),
199
- multi: z.boolean().describe("allow multiple selections").optional(),
200
- recommended: z.number().describe("recommended option index").optional(),
201
- deepInterview: DeepInterviewMeta.describe("optional deep-interview round metadata").optional(),
202
- workflowGate: WorkflowGateMeta.describe("optional workflow gate stage/kind override").optional(),
203
- })
204
- .superRefine((value, context) => {
205
- const labels = new Set(value.options.map(option => option.label));
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)
232
- context.addIssue({ code: "custom", message: "intent gates must be single-select", path: ["multi"] });
233
- const confirmationOptions = contract?.confirmation_options ?? [];
234
- if (new Set(confirmationOptions).size !== confirmationOptions.length)
235
- context.addIssue({
236
- code: "custom",
237
- message: "intent confirmation options must be unique",
238
- path: ["deepInterview", "intent_contract"],
239
- });
240
- const approvalOptions = review?.approval_options ?? [];
241
- if (new Set(approvalOptions).size !== approvalOptions.length)
242
- context.addIssue({
243
- code: "custom",
244
- message: "intent approval options must be unique",
245
- path: ["deepInterview", "intent_review"],
246
- });
247
- for (const label of confirmationOptions) {
248
- if (!labels.has(label))
197
+ function createQuestionItemSchema(deepInterviewSchema: z.ZodType<DeepInterviewMeta>) {
198
+ return z
199
+ .object({
200
+ id: z.string().describe("question id"),
201
+ question: z.string().describe("question text"),
202
+ options: z.array(OptionItem).describe("available options"),
203
+ multi: z.boolean().describe("allow multiple selections").optional(),
204
+ recommended: z.number().describe("recommended option index").optional(),
205
+ deepInterview: deepInterviewSchema.describe("optional deep-interview round metadata").optional(),
206
+ workflowGate: WorkflowGateMeta.describe("optional workflow gate stage/kind override").optional(),
207
+ })
208
+ .superRefine((value, context) => {
209
+ const labels = new Set(value.options.map(option => option.label));
210
+ const contract = intentContract(value.deepInterview);
211
+ const review = intentReview(value.deepInterview);
212
+ if (
213
+ value.deepInterview &&
214
+ value.workflowGate &&
215
+ (value.workflowGate.stage !== "deep-interview" || value.workflowGate.kind !== "question")
216
+ )
217
+ context.addIssue({
218
+ code: "custom",
219
+ message: "deep-interview metadata requires a deep-interview question workflow gate",
220
+ path: ["workflowGate"],
221
+ });
222
+ if (contract && review)
223
+ context.addIssue({
224
+ code: "custom",
225
+ message: "intent contract and review are mutually exclusive",
226
+ path: ["deepInterview"],
227
+ });
228
+ if (
229
+ contract &&
230
+ (value.deepInterview?.round !== 0 ||
231
+ value.deepInterview.component !== "review-topology" ||
232
+ value.deepInterview.dimension !== "topology")
233
+ )
234
+ context.addIssue({
235
+ code: "custom",
236
+ message: "intent contract requires round-0 review topology metadata",
237
+ path: ["deepInterview"],
238
+ });
239
+ if (review && (value.deepInterview?.round ?? 0) <= 0)
249
240
  context.addIssue({
250
241
  code: "custom",
251
- message: "intent confirmation option must be displayed",
242
+ message: "intent review requires a positive round",
243
+ path: ["deepInterview", "round"],
244
+ });
245
+ if ((contract || review) && value.multi === true)
246
+ context.addIssue({ code: "custom", message: "intent gates must be single-select", path: ["multi"] });
247
+ const confirmationOptions = contract?.confirmation_options ?? [];
248
+ if (new Set(confirmationOptions).size !== confirmationOptions.length)
249
+ context.addIssue({
250
+ code: "custom",
251
+ message: "intent confirmation options must be unique",
252
252
  path: ["deepInterview", "intent_contract"],
253
253
  });
254
- }
255
- for (const label of approvalOptions) {
256
- if (!labels.has(label))
254
+ const approvalOptions = review?.approval_options ?? [];
255
+ if (new Set(approvalOptions).size !== approvalOptions.length)
257
256
  context.addIssue({
258
257
  code: "custom",
259
- message: "intent approval option must be displayed",
258
+ message: "intent approval options must be unique",
260
259
  path: ["deepInterview", "intent_review"],
261
260
  });
262
- }
263
- });
261
+ for (const label of confirmationOptions) {
262
+ if (!labels.has(label))
263
+ context.addIssue({
264
+ code: "custom",
265
+ message: "intent confirmation option must be displayed",
266
+ path: ["deepInterview", "intent_contract"],
267
+ });
268
+ }
269
+ for (const label of approvalOptions) {
270
+ if (!labels.has(label))
271
+ context.addIssue({
272
+ code: "custom",
273
+ message: "intent approval option must be displayed",
274
+ path: ["deepInterview", "intent_review"],
275
+ });
276
+ }
277
+ });
278
+ }
279
+
280
+ const QuestionItem = createQuestionItemSchema(DeepInterviewMeta);
281
+ const TopologyQuestionItem = createQuestionItemSchema(DeepInterviewTopologyMeta);
282
+ const PostTopologyQuestionItem = createQuestionItemSchema(z.union([DeepInterviewRoundMeta, DeepInterviewReviewMeta]));
283
+
284
+ const OrdinaryQuestionItem = z.object({
285
+ id: z.string().describe("question id"),
286
+ question: z.string().describe("question text"),
287
+ options: z.array(OptionItem).describe("available options"),
288
+ multi: z.boolean().describe("allow multiple selections").optional(),
289
+ recommended: z.number().describe("recommended option index").optional(),
290
+ workflowGate: WorkflowGateMeta.describe("optional workflow gate stage/kind override").optional(),
291
+ });
264
292
 
265
293
  export const askSchema = z.object({
266
294
  questions: z.array(QuestionItem).min(1).describe("questions to ask"),
267
295
  });
268
296
 
297
+ const topologyAskSchema = z.object({
298
+ questions: z.array(TopologyQuestionItem).min(1).describe("questions to ask"),
299
+ });
300
+
301
+ const postTopologyAskSchema = z.object({
302
+ questions: z.array(PostTopologyQuestionItem).min(1).describe("questions to ask"),
303
+ });
304
+
305
+ const ordinaryAskSchema = z.object({
306
+ questions: z.array(OrdinaryQuestionItem).min(1).describe("questions to ask"),
307
+ });
308
+
269
309
  export type AskToolInput = z.infer<typeof askSchema>;
270
310
 
271
311
  function isPlainRecord(value: unknown): value is Record<string, unknown> {
@@ -361,9 +401,41 @@ function normalizeRoundZeroOptionalNulls(arguments_: Record<string, unknown>): R
361
401
  if (changed) normalizedQuestion.deepInterview = normalizedDeepInterview;
362
402
  return changed ? { ...arguments_, questions: [normalizedQuestion] } : arguments_;
363
403
  }
364
- function recoverRoundZeroIntentContract(arguments_: Record<string, unknown>): RawArgumentValidationResult {
404
+
405
+ function knownIntentRejection(arguments_: Record<string, unknown>): RawArgumentValidationResult | undefined {
406
+ if (!isPlainRecord(arguments_) || !Array.isArray(arguments_.questions) || arguments_.questions.length !== 1)
407
+ return undefined;
408
+ const question = arguments_.questions[0];
409
+ if (!isPlainRecord(question) || !isPlainRecord(question.deepInterview)) return undefined;
410
+ const metadata = question.deepInterview;
411
+ const hasContract = Object.hasOwn(metadata, "intent_contract");
412
+ const hasReview = Object.hasOwn(metadata, "intent_review");
413
+ const workflowGate = question.workflowGate;
414
+ if (
415
+ Object.hasOwn(question, "workflowGate") &&
416
+ (!isPlainRecord(workflowGate) || workflowGate.stage !== "deep-interview" || workflowGate.kind !== "question")
417
+ )
418
+ return { outcome: "reject", code: "ask-deep-interview-metadata-requires-deep-interview-gate" };
419
+ if (hasReview && !hasContract && metadata.round === 0) {
420
+ return { outcome: "reject", code: "ask-intent-review-requires-positive-round" };
421
+ }
422
+ if (!hasContract || !isPlainRecord(metadata.intent_contract)) return undefined;
423
+ const contract = metadata.intent_contract;
424
+ if (
425
+ (Array.isArray(contract.items) && contract.items.length === 0) ||
426
+ (Array.isArray(contract.confirmation_options) && contract.confirmation_options.length === 0)
427
+ )
428
+ return { outcome: "reject", code: "ask-intent-contract-requires-non-empty-authority" };
429
+ return undefined;
430
+ }
431
+ function recoverRoundZeroIntentContract(
432
+ arguments_: Record<string, unknown>,
433
+ stage?: "topology" | "post-topology",
434
+ ): RawArgumentValidationResult {
365
435
  if (!isRoundZeroRecoveryCandidate(arguments_)) return { outcome: "passthrough" };
366
436
  const normalizedArguments = normalizeRoundZeroOptionalNulls(arguments_);
437
+ const knownRejection = knownIntentRejection(normalizedArguments);
438
+ if (knownRejection) return knownRejection;
367
439
  if (!isOnlyPlainData(normalizedArguments) || !isPlainRecord(normalizedArguments)) return { outcome: "reject" };
368
440
  if (
369
441
  !hasExactOwnKeys(normalizedArguments, ["questions"]) ||
@@ -421,10 +493,19 @@ function recoverRoundZeroIntentContract(arguments_: Record<string, unknown>): Ra
421
493
  "intent_contract",
422
494
  "intent_review",
423
495
  ];
496
+ if (!hasOnlyAllowedOwnKeys(deepInterview, deepInterviewKeys)) return { outcome: "reject" };
497
+ if (
498
+ stage === "post-topology" &&
499
+ !hasIntentContract &&
500
+ hasIntentReview &&
501
+ typeof deepInterview.round === "number" &&
502
+ Number.isInteger(deepInterview.round) &&
503
+ deepInterview.round > 0
504
+ )
505
+ return { outcome: "passthrough" };
424
506
  if (
425
- !hasOnlyAllowedOwnKeys(deepInterview, deepInterviewKeys) ||
426
- !Object.hasOwn(deepInterview, "intent_contract") ||
427
- !Object.hasOwn(deepInterview, "intent_review") ||
507
+ !hasIntentContract ||
508
+ !hasIntentReview ||
428
509
  deepInterview.round !== 0 ||
429
510
  typeof deepInterview.component !== "string" ||
430
511
  deepInterview.component !== "review-topology" ||
@@ -522,6 +603,16 @@ function getDoneOptionLabel(): string {
522
603
  return `${theme.status.success} Done selecting`;
523
604
  }
524
605
 
606
+ function validRecommendedIndex(recommended: number | undefined, optionCount: number): number | undefined {
607
+ return typeof recommended === "number" &&
608
+ Number.isFinite(recommended) &&
609
+ Number.isInteger(recommended) &&
610
+ recommended >= 0 &&
611
+ recommended < optionCount
612
+ ? recommended
613
+ : undefined;
614
+ }
615
+
525
616
  /** Add "(Recommended)" suffix to the option at the given index if not already present */
526
617
  function addRecommendedSuffix(labels: string[], recommendedIndex?: number): string[] {
527
618
  if (recommendedIndex === undefined || recommendedIndex < 0 || recommendedIndex >= labels.length) {
@@ -1050,6 +1141,11 @@ function formatQuestionResult(result: QuestionResult): string {
1050
1141
  // =============================================================================
1051
1142
 
1052
1143
  type AskParams = AskToolInput;
1144
+ type AskParametersSchema =
1145
+ | typeof ordinaryAskSchema
1146
+ | typeof askSchema
1147
+ | typeof topologyAskSchema
1148
+ | typeof postTopologyAskSchema;
1053
1149
 
1054
1150
  /**
1055
1151
  * Ask tool for interactive user prompting during execution.
@@ -1057,13 +1153,19 @@ type AskParams = AskToolInput;
1057
1153
  * Allows gathering user preferences, clarifying instructions, and getting decisions
1058
1154
  * on implementation choices as the agent works.
1059
1155
  */
1060
- export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
1156
+ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
1061
1157
  readonly name = "ask";
1062
1158
  readonly label = "Ask";
1063
1159
  readonly summary = "Ask the user a clarifying question";
1064
1160
  readonly description: string;
1065
- readonly parameters = askSchema;
1066
- readonly rawArgumentValidation = recoverRoundZeroIntentContract;
1161
+ get parameters(): AskParametersSchema {
1162
+ const stage = this.session.getDeepInterviewAskStage?.();
1163
+ if (stage === "topology") return topologyAskSchema;
1164
+ if (stage === "post-topology") return postTopologyAskSchema;
1165
+ return ordinaryAskSchema;
1166
+ }
1167
+ readonly rawArgumentValidation = (arguments_: Record<string, unknown>): RawArgumentValidationResult =>
1168
+ recoverRoundZeroIntentContract(arguments_, this.session.getDeepInterviewAskStage?.());
1067
1169
  readonly strict = true;
1068
1170
  readonly loadMode = "discoverable";
1069
1171
 
@@ -1098,6 +1200,8 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
1098
1200
  if (customInput !== undefined && (meta || isDeepInterviewAskQuestion(q.question)))
1099
1201
  assertDeepInterviewInputWithinLimit(customInput, MAX_USER_RESPONSE_LENGTH, "user_response");
1100
1202
  if (!meta) return;
1203
+ if (this.session.getDeepInterviewAskStage?.() === undefined) return;
1204
+ if (q.workflowGate && (q.workflowGate.stage !== "deep-interview" || q.workflowGate.kind !== "question")) return;
1101
1205
  const cwd = this.session.cwd;
1102
1206
  const sessionId = this.session.getSessionId?.() ?? undefined;
1103
1207
  const statePath = deepInterviewStatePath(cwd, sessionId);
@@ -1154,12 +1258,13 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
1154
1258
  };
1155
1259
  const gateEmitter = this.session.getWorkflowGateEmitter?.();
1156
1260
  // A durable workflow-gate emitter now exists for every session, and its
1157
- // isUnattended() is always true, so it can no longer signal "no local UI".
1158
- // The workflow gate is only the headless (non-TUI) answer path: when a real
1159
- // interactive UI is present, prefer it — otherwise attended TUI asks would
1160
- // route to emitGate() and hang forever waiting on a remote responder.
1261
+ // supportsRemoteGateAnswers() is always true, so it can no longer signal
1262
+ // "no local UI". The workflow gate is only the headless (non-TUI) answer
1263
+ // path: when a real interactive UI is present, prefer it — otherwise
1264
+ // attended TUI asks would route to emitGate() and hang forever waiting on
1265
+ // a remote responder.
1161
1266
  const hasInteractiveUi = context?.hasUI === true && !!context.ui;
1162
- const canUseWorkflowGate = !hasInteractiveUi && gateEmitter?.isUnattended() === true;
1267
+ const canUseWorkflowGate = !hasInteractiveUi && gateEmitter?.supportsRemoteGateAnswers() === true;
1163
1268
  // Headless fallback: SDK workflow gates are the non-TUI answer path.
1164
1269
  if (!canUseWorkflowGate && (!context?.hasUI || !context.ui)) {
1165
1270
  context?.abort();
@@ -1428,10 +1533,12 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
1428
1533
  }),
1429
1534
  }
1430
1535
  : options?.previous;
1536
+ const recommendedIndex = validRecommendedIndex(q.recommended, rawOptionLabels.length);
1431
1537
  activeRemoteRequest = {
1432
1538
  question: displayQuestion,
1433
1539
  options: remoteSelectorOptions,
1434
1540
  interaction: "selector",
1541
+ ...(recommendedIndex === undefined ? {} : { recommendedIndex }),
1435
1542
  controls: askRemoteControls({
1436
1543
  multi: q.multi === true,
1437
1544
  questionIndex,
@@ -1463,6 +1570,9 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
1463
1570
  question: displayQuestion,
1464
1571
  options: state.interaction === "selector" ? remoteSelectorOptions : [],
1465
1572
  interaction: state.interaction,
1573
+ ...(state.interaction === "selector" && recommendedIndex !== undefined
1574
+ ? { recommendedIndex }
1575
+ : {}),
1466
1576
  controls:
1467
1577
  state.interaction === "selector"
1468
1578
  ? askRemoteControls({
@@ -3,7 +3,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
3
3
  import { type AstReplaceChange, type AstReplaceFileChange, astEdit } from "@gajae-code/natives";
4
4
  import type { Component } from "@gajae-code/tui";
5
5
  import { Text } from "@gajae-code/tui";
6
- import { $envpos, prompt, untilAborted } from "@gajae-code/utils";
6
+ import { $pickenvpos, prompt, untilAborted } from "@gajae-code/utils";
7
7
  import * as z from "zod/v4";
8
8
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
9
9
  import { computeLineHash, HL_BODY_SEP } from "../hashline/hash";
@@ -199,7 +199,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
199
199
  seenPatterns.add(pat);
200
200
  }
201
201
  const normalizedRewrites = Object.fromEntries(ops);
202
- const maxFiles = $envpos("PI_MAX_AST_FILES", 1000);
202
+ const maxFiles = $pickenvpos(["GJC_MAX_AST_FILES", "PI_MAX_AST_FILES"], 1000);
203
203
 
204
204
  const scope = await resolveToolSearchScope({
205
205
  rawPaths: params.paths,
@@ -1,4 +1,4 @@
1
- import { $env } from "@gajae-code/utils/env";
1
+ import { $pickenv } from "@gajae-code/utils/env";
2
2
 
3
3
  /** Minimal UI-capability fields needed to decide whether bash can use the local PTY overlay. */
4
4
  export interface BashPtyContext {
@@ -9,6 +9,6 @@ export interface BashPtyContext {
9
9
  /** Return whether a bash tool call should use the local interactive PTY overlay. */
10
10
  export function canUseInteractiveBashPty(pty: boolean, ctx: BashPtyContext | undefined): boolean {
11
11
  if (!pty) return false;
12
- if ($env.PI_NO_PTY === "1") return false;
12
+ if ($pickenv("GJC_NO_PTY", "PI_NO_PTY") === "1") return false;
13
13
  return ctx?.hasUI === true && ctx.ui !== undefined;
14
14
  }
@@ -1,10 +1,15 @@
1
1
  import * as os from "node:os";
2
+ import * as path from "node:path";
2
3
 
3
4
  import { formatDimensionNote, type ResizedImage } from "../../utils/image-resize";
4
5
 
5
6
  function shortenPath(filePath: string): string {
6
7
  const home = os.homedir();
7
- return home && filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath;
8
+ if (!home) return filePath;
9
+ // Require a separator boundary so a sibling that shares the home prefix
10
+ // (e.g. "/home/woodyx/x" for home "/home/woody") is not corrupted into "~x/x".
11
+ const boundary = home.endsWith(path.sep) ? home : home + path.sep;
12
+ return filePath === home || filePath.startsWith(boundary) ? `~${filePath.slice(home.length)}` : filePath;
8
13
  }
9
14
 
10
15
  export function formatScreenshot(opts: {
package/src/tools/cron.ts CHANGED
@@ -36,7 +36,7 @@ const cronSchema = z
36
36
  .string()
37
37
  .optional()
38
38
  .describe(
39
- "(op=create, required) Prompt to inject between turns when the cron fires. May reference slash commands (e.g. '/review-pr 1234') or natural-language instructions.",
39
+ "(op=create, required) Prompt to inject between turns when the cron fires. Every firing starts a normal agent turn whose response may be visible; use a persistent monitor, not cron, for ongoing polling that should emit only on state changes.",
40
40
  ),
41
41
  recurring: z
42
42
  .boolean()