@gajae-code/coding-agent 0.11.7 → 0.11.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/CHANGELOG.md +73 -1
  2. package/dist/types/cli/args.d.ts +0 -2
  3. package/dist/types/cli.d.ts +1 -0
  4. package/dist/types/config/keybindings.d.ts +33 -3
  5. package/dist/types/config/model-profile-activation.d.ts +4 -3
  6. package/dist/types/config/model-profile-contract.d.ts +38 -0
  7. package/dist/types/config/model-profiles.d.ts +2 -3
  8. package/dist/types/config/settings-schema.d.ts +15 -2
  9. package/dist/types/edit/modes/patch.d.ts +13 -0
  10. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  11. package/dist/types/eval/py/executor.d.ts +1 -1
  12. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  13. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +12 -5
  14. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +8 -0
  15. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +41 -0
  16. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +32 -17
  17. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +17 -0
  18. package/dist/types/gjc-runtime/deep-interview-state.d.ts +135 -4
  19. package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
  20. package/dist/types/gjc-runtime/state-writer.d.ts +44 -0
  21. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +1 -0
  22. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
  23. package/dist/types/gjc-runtime/workflow-manifest.d.ts +5 -3
  24. package/dist/types/main.d.ts +8 -0
  25. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  26. package/dist/types/modes/components/tool-status-header.d.ts +3 -2
  27. package/dist/types/modes/components/welcome.d.ts +4 -0
  28. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  29. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  30. package/dist/types/modes/interactive-mode.d.ts +5 -2
  31. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  32. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  33. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  34. package/dist/types/modes/types.d.ts +3 -0
  35. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  36. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  37. package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
  38. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
  39. package/dist/types/runtime-mcp/redaction.d.ts +2 -0
  40. package/dist/types/sdk/broker/broker.d.ts +2 -0
  41. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +7 -1
  43. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  44. package/dist/types/sdk/bus/config.d.ts +2 -0
  45. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  46. package/dist/types/sdk/bus/index.d.ts +25 -0
  47. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  48. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
  49. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +15 -3
  50. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  51. package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
  52. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  53. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  54. package/dist/types/sdk/host/host.d.ts +1 -1
  55. package/dist/types/sdk/host/query/handlers.d.ts +9 -0
  56. package/dist/types/sdk/index.d.ts +2 -0
  57. package/dist/types/sdk/prompt-status.d.ts +88 -0
  58. package/dist/types/sdk/session-directory.d.ts +1 -1
  59. package/dist/types/sdk/startup-capability.d.ts +3 -0
  60. package/dist/types/session/agent-session.d.ts +2 -0
  61. package/dist/types/session/internal/managed-session-scope.d.ts +22 -3
  62. package/dist/types/session/internal/managed-session-storage.d.ts +24 -4
  63. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  64. package/dist/types/session/session-manager.d.ts +5 -1
  65. package/dist/types/session/session-storage.d.ts +31 -0
  66. package/dist/types/skill-state/active-state.d.ts +4 -0
  67. package/dist/types/task/index.d.ts +3 -1
  68. package/dist/types/task/receipt.d.ts +2 -0
  69. package/dist/types/task/types.d.ts +94 -0
  70. package/dist/types/tools/ask.d.ts +454 -174
  71. package/dist/types/tools/image-gen.d.ts +47 -1
  72. package/dist/types/tools/index.d.ts +4 -0
  73. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  74. package/package.json +8 -8
  75. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  76. package/scripts/dogfood-repository-binding.ts +209 -0
  77. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  78. package/scripts/run-test-manifest.ts +4 -4
  79. package/src/cli/args.ts +2 -92
  80. package/src/cli/mcp-cli.ts +4 -21
  81. package/src/cli/notify-cli.ts +5 -38
  82. package/src/cli.ts +50 -0
  83. package/src/commands/deep-interview.ts +9 -4
  84. package/src/commands/sdk.ts +2 -2
  85. package/src/commit/agentic/index.ts +3 -3
  86. package/src/commit/map-reduce/index.ts +2 -2
  87. package/src/config/keybindings.ts +157 -24
  88. package/src/config/model-profile-activation.ts +7 -18
  89. package/src/config/model-profile-contract.ts +161 -0
  90. package/src/config/model-profiles.ts +2 -3
  91. package/src/config/model-registry.ts +11 -1
  92. package/src/config/settings-schema.ts +17 -2
  93. package/src/coordinator-mcp/model-preset.ts +21 -74
  94. package/src/defaults/gjc/skills/deep-interview/SKILL.md +80 -165
  95. package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
  96. package/src/edit/index.ts +3 -6
  97. package/src/edit/modes/patch.ts +38 -1
  98. package/src/edit/modes/replace.ts +42 -0
  99. package/src/edit/path-mutation-lock.ts +112 -0
  100. package/src/eval/py/executor.ts +30 -11
  101. package/src/eval/py/prelude.py +2 -2
  102. package/src/eval/py/tool-bridge.ts +34 -18
  103. package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
  104. package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
  105. package/src/gjc-runtime/deep-interview-draft.ts +842 -0
  106. package/src/gjc-runtime/deep-interview-payload.ts +262 -0
  107. package/src/gjc-runtime/deep-interview-recorder.ts +294 -226
  108. package/src/gjc-runtime/deep-interview-repair.ts +1674 -0
  109. package/src/gjc-runtime/deep-interview-runtime.ts +264 -103
  110. package/src/gjc-runtime/deep-interview-state.ts +916 -4
  111. package/src/gjc-runtime/launch-tmux.ts +1 -1
  112. package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
  113. package/src/gjc-runtime/ralplan-runtime.ts +78 -4
  114. package/src/gjc-runtime/repository-binding.ts +267 -0
  115. package/src/gjc-runtime/session-state-sidecar.ts +25 -9
  116. package/src/gjc-runtime/state-writer.ts +166 -2
  117. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
  118. package/src/gjc-runtime/tmux-sessions.ts +45 -6
  119. package/src/gjc-runtime/ultragoal-guard.ts +5 -5
  120. package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
  121. package/src/gjc-runtime/workflow-manifest.generated.json +386 -1
  122. package/src/gjc-runtime/workflow-manifest.ts +191 -5
  123. package/src/internal-urls/docs-index.generated.ts +8 -6
  124. package/src/internal-urls/local-protocol.ts +33 -16
  125. package/src/internal-urls/mcp-protocol.ts +73 -11
  126. package/src/main.ts +25 -10
  127. package/src/modes/DESIGN.md +5 -0
  128. package/src/modes/acp/acp-agent.ts +50 -16
  129. package/src/modes/components/model-selector.ts +42 -8
  130. package/src/modes/components/plan-preview-overlay.ts +1 -1
  131. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  132. package/src/modes/components/tool-status-header.ts +20 -4
  133. package/src/modes/components/welcome.ts +32 -20
  134. package/src/modes/controllers/command-controller.ts +97 -54
  135. package/src/modes/controllers/event-controller.ts +10 -0
  136. package/src/modes/controllers/input-controller.ts +7 -2
  137. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  138. package/src/modes/controllers/selector-controller.ts +46 -54
  139. package/src/modes/interactive-mode.ts +70 -36
  140. package/src/modes/prompt-action-autocomplete.ts +32 -13
  141. package/src/modes/prompt-suggestion-controller.ts +96 -0
  142. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  143. package/src/modes/types.ts +3 -0
  144. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  145. package/src/modes/utils/ui-helpers.ts +5 -3
  146. package/src/prompts/agents/architect.md +0 -1
  147. package/src/prompts/agents/critic.md +0 -1
  148. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  149. package/src/prompts/system/system-prompt.md +7 -4
  150. package/src/prompts/tools/cron.md +3 -1
  151. package/src/runtime-mcp/json-rpc.ts +7 -4
  152. package/src/runtime-mcp/manager.ts +51 -53
  153. package/src/runtime-mcp/oauth-flow.ts +0 -72
  154. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  155. package/src/runtime-mcp/redaction.ts +30 -0
  156. package/src/runtime-mcp/transports/http.ts +13 -6
  157. package/src/sdk/broker/broker.ts +34 -20
  158. package/src/sdk/broker/lifecycle.ts +185 -36
  159. package/src/sdk/broker/session-index.ts +11 -1
  160. package/src/sdk/bus/chat-daemon-control.ts +20 -5
  161. package/src/sdk/bus/config-commands.ts +2 -2
  162. package/src/sdk/bus/config.ts +9 -1
  163. package/src/sdk/bus/conversation-store.ts +40 -6
  164. package/src/sdk/bus/index.ts +240 -65
  165. package/src/sdk/bus/lifecycle-commands.ts +47 -3
  166. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  167. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  168. package/src/sdk/bus/notification-orchestration.ts +12 -0
  169. package/src/sdk/bus/notification-service.ts +27 -2
  170. package/src/sdk/bus/prompt-reconciliation.ts +239 -0
  171. package/src/sdk/bus/recent-activity.ts +90 -26
  172. package/src/sdk/bus/telegram-daemon-contract.ts +15 -3
  173. package/src/sdk/bus/telegram-daemon-control.ts +145 -33
  174. package/src/sdk/bus/telegram-daemon.ts +853 -112
  175. package/src/sdk/bus/telegram-reference.ts +20 -2
  176. package/src/sdk/cli/session-cli.ts +15 -2
  177. package/src/sdk/host/control/dispatch.ts +1 -1
  178. package/src/sdk/host/control/operations.ts +1 -1
  179. package/src/sdk/host/host.ts +2 -6
  180. package/src/sdk/host/query/handlers.ts +70 -5
  181. package/src/sdk/index.ts +12 -0
  182. package/src/sdk/prompt-status.ts +97 -0
  183. package/src/sdk/protocol/operation-inventory.generated.json +47 -0
  184. package/src/sdk/protocol/operation-registry.ts +18 -4
  185. package/src/sdk/session-directory.ts +5 -1
  186. package/src/sdk/session.ts +2 -0
  187. package/src/sdk/startup-capability.ts +9 -1
  188. package/src/session/agent-session.ts +138 -5
  189. package/src/session/blob-store.ts +7 -2
  190. package/src/session/internal/managed-session-scope.ts +998 -133
  191. package/src/session/internal/managed-session-storage.ts +268 -203
  192. package/src/session/internal/native-publish-outcome.ts +266 -0
  193. package/src/session/session-manager.ts +406 -360
  194. package/src/session/session-storage.ts +151 -11
  195. package/src/skill-state/active-state.ts +10 -0
  196. package/src/task/executor.ts +13 -5
  197. package/src/task/gjc-command.ts +2 -2
  198. package/src/task/index.ts +121 -15
  199. package/src/task/receipt.ts +3 -0
  200. package/src/task/types.ts +34 -3
  201. package/src/tools/ask.ts +199 -88
  202. package/src/tools/ast-edit.ts +2 -2
  203. package/src/tools/bash-pty-selection.ts +2 -2
  204. package/src/tools/browser/screenshot-format.ts +6 -1
  205. package/src/tools/cron.ts +1 -1
  206. package/src/tools/image-gen.ts +178 -6
  207. package/src/tools/index.ts +4 -0
  208. package/src/tools/render-utils.ts +7 -1
  209. package/src/utils/edit-mode.ts +2 -2
  210. package/src/utils/prompt-suggestion.ts +265 -0
  211. package/src/utils/sixel.ts +3 -3
  212. package/src/web/insane/url-guard.ts +3 -1
  213. package/src/web/scrapers/docs-rs.ts +30 -5
package/src/task/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ThinkingLevel } from "@gajae-code/agent-core";
2
2
  import type { Usage } from "@gajae-code/ai";
3
- import { $env } from "@gajae-code/utils";
3
+ import { $pickenv } from "@gajae-code/utils";
4
4
  import * as z from "zod/v4";
5
5
  import { isValidTaskId, TASK_ID_DESCRIPTION } from "./id";
6
6
  import type { TaskResultReceipt } from "./receipt";
@@ -27,10 +27,10 @@ const parseNumber = (value: string | undefined, defaultValue: number): number =>
27
27
  };
28
28
 
29
29
  /** Maximum output bytes per agent */
30
- export const MAX_OUTPUT_BYTES = parseNumber($env.PI_TASK_MAX_OUTPUT_BYTES, 500_000);
30
+ export const MAX_OUTPUT_BYTES = parseNumber($pickenv("GJC_TASK_MAX_OUTPUT_BYTES", "PI_TASK_MAX_OUTPUT_BYTES"), 500_000);
31
31
 
32
32
  /** Maximum output lines per agent */
33
- export const MAX_OUTPUT_LINES = parseNumber($env.PI_TASK_MAX_OUTPUT_LINES, 5000);
33
+ export const MAX_OUTPUT_LINES = parseNumber($pickenv("GJC_TASK_MAX_OUTPUT_LINES", "PI_TASK_MAX_OUTPUT_LINES"), 5000);
34
34
 
35
35
  /** EventBus channel for raw subagent events */
36
36
  export const TASK_SUBAGENT_EVENT_CHANNEL = "task:subagent:event";
@@ -74,6 +74,19 @@ const spawnPlanSchema = z
74
74
  })
75
75
  .describe("justification required before spawning more than four tasks");
76
76
 
77
+ const repositoryBindingSchema = z
78
+ .object({
79
+ schema: z.literal("gjc.repository_binding.v1"),
80
+ worktreeRoot: z.string().min(1).describe("canonical git worktree root"),
81
+ commonDir: z.string().min(1).nullable().describe("git common dir, or null outside a git checkout"),
82
+ relativeSubdir: z.string().min(1).optional().describe("optional repo-relative subdirectory; not an absolute cwd"),
83
+ displayPath: z.string().min(1).optional().describe("human-facing path; never used for authority"),
84
+ head: z.string().min(1).optional(),
85
+ branch: z.string().min(1).optional(),
86
+ })
87
+ .strict()
88
+ .describe("authoritative repository identity for multi-repo fail-closed spawn");
89
+
77
90
  const createTaskItemSchema = (_contextEnabled: boolean) =>
78
91
  z.object({
79
92
  id: z.string().max(48).refine(isValidTaskId, TASK_ID_DESCRIPTION).describe("filesystem-safe task identifier"),
@@ -85,6 +98,11 @@ const createTaskItemSchema = (_contextEnabled: boolean) =>
85
98
  .describe(
86
99
  "fork-context mode: none/omitted copies no parent context; receipt copies a minimal receipt-sized snapshot; last-turn copies only the latest exchange; bounded copies the bounded default snapshot; full copies a larger sanitized snapshot up to the configured/model token cap",
87
100
  ),
101
+ repositoryBinding: repositoryBindingSchema
102
+ .optional()
103
+ .describe(
104
+ "authoritative repository identity; omitted items are stamped from session cwd before discovery/spawn and still fail closed on sibling drift",
105
+ ),
88
106
  });
89
107
 
90
108
  /** Single task item for parallel execution (default shape with context enabled). */
@@ -353,6 +371,19 @@ export interface SingleResult {
353
371
  * never changes the actual mode selection).
354
372
  */
355
373
  forkContextAdvisory?: { recommendedMode: ForkContextMode; reasons: string[] };
374
+ /**
375
+ * Resolved repository identity used for this task after pre-discovery stamping
376
+ * and fail-closed validation (#2901).
377
+ */
378
+ repositoryBinding?: {
379
+ schema: "gjc.repository_binding.v1";
380
+ worktreeRoot: string;
381
+ commonDir: string | null;
382
+ relativeSubdir?: string;
383
+ displayPath?: string;
384
+ head?: string;
385
+ branch?: string;
386
+ };
356
387
  }
357
388
 
358
389
  /** True only for complete, factual five-bucket cost accounting. */
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"]) ||
@@ -389,6 +461,7 @@ function recoverRoundZeroIntentContract(arguments_: Record<string, unknown>): Ra
389
461
  const deepInterview = question.deepInterview;
390
462
  const hasIntentContract = Object.hasOwn(deepInterview, "intent_contract");
391
463
  const hasIntentReview = Object.hasOwn(deepInterview, "intent_review");
464
+ if (hasIntentContract && hasIntentReview && stage !== "topology") return { outcome: "reject" };
392
465
  if (hasIntentContract !== hasIntentReview && askSchema.safeParse(normalizedArguments).success)
393
466
  return { outcome: "passthrough" };
394
467
 
@@ -421,10 +494,19 @@ function recoverRoundZeroIntentContract(arguments_: Record<string, unknown>): Ra
421
494
  "intent_contract",
422
495
  "intent_review",
423
496
  ];
497
+ if (!hasOnlyAllowedOwnKeys(deepInterview, deepInterviewKeys)) return { outcome: "reject" };
498
+ if (
499
+ stage === "post-topology" &&
500
+ !hasIntentContract &&
501
+ hasIntentReview &&
502
+ typeof deepInterview.round === "number" &&
503
+ Number.isInteger(deepInterview.round) &&
504
+ deepInterview.round > 0
505
+ )
506
+ return { outcome: "passthrough" };
424
507
  if (
425
- !hasOnlyAllowedOwnKeys(deepInterview, deepInterviewKeys) ||
426
- !Object.hasOwn(deepInterview, "intent_contract") ||
427
- !Object.hasOwn(deepInterview, "intent_review") ||
508
+ !hasIntentContract ||
509
+ !hasIntentReview ||
428
510
  deepInterview.round !== 0 ||
429
511
  typeof deepInterview.component !== "string" ||
430
512
  deepInterview.component !== "review-topology" ||
@@ -522,6 +604,16 @@ function getDoneOptionLabel(): string {
522
604
  return `${theme.status.success} Done selecting`;
523
605
  }
524
606
 
607
+ function validRecommendedIndex(recommended: number | undefined, optionCount: number): number | undefined {
608
+ return typeof recommended === "number" &&
609
+ Number.isFinite(recommended) &&
610
+ Number.isInteger(recommended) &&
611
+ recommended >= 0 &&
612
+ recommended < optionCount
613
+ ? recommended
614
+ : undefined;
615
+ }
616
+
525
617
  /** Add "(Recommended)" suffix to the option at the given index if not already present */
526
618
  function addRecommendedSuffix(labels: string[], recommendedIndex?: number): string[] {
527
619
  if (recommendedIndex === undefined || recommendedIndex < 0 || recommendedIndex >= labels.length) {
@@ -1050,6 +1142,11 @@ function formatQuestionResult(result: QuestionResult): string {
1050
1142
  // =============================================================================
1051
1143
 
1052
1144
  type AskParams = AskToolInput;
1145
+ type AskParametersSchema =
1146
+ | typeof ordinaryAskSchema
1147
+ | typeof askSchema
1148
+ | typeof topologyAskSchema
1149
+ | typeof postTopologyAskSchema;
1053
1150
 
1054
1151
  /**
1055
1152
  * Ask tool for interactive user prompting during execution.
@@ -1057,13 +1154,19 @@ type AskParams = AskToolInput;
1057
1154
  * Allows gathering user preferences, clarifying instructions, and getting decisions
1058
1155
  * on implementation choices as the agent works.
1059
1156
  */
1060
- export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
1157
+ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
1061
1158
  readonly name = "ask";
1062
1159
  readonly label = "Ask";
1063
1160
  readonly summary = "Ask the user a clarifying question";
1064
1161
  readonly description: string;
1065
- readonly parameters = askSchema;
1066
- readonly rawArgumentValidation = recoverRoundZeroIntentContract;
1162
+ get parameters(): AskParametersSchema {
1163
+ const stage = this.session.getDeepInterviewAskStage?.();
1164
+ if (stage === "topology") return topologyAskSchema;
1165
+ if (stage === "post-topology") return postTopologyAskSchema;
1166
+ return ordinaryAskSchema;
1167
+ }
1168
+ readonly rawArgumentValidation = (arguments_: Record<string, unknown>): RawArgumentValidationResult =>
1169
+ recoverRoundZeroIntentContract(arguments_, this.session.getDeepInterviewAskStage?.());
1067
1170
  readonly strict = true;
1068
1171
  readonly loadMode = "discoverable";
1069
1172
 
@@ -1098,6 +1201,8 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
1098
1201
  if (customInput !== undefined && (meta || isDeepInterviewAskQuestion(q.question)))
1099
1202
  assertDeepInterviewInputWithinLimit(customInput, MAX_USER_RESPONSE_LENGTH, "user_response");
1100
1203
  if (!meta) return;
1204
+ if (this.session.getDeepInterviewAskStage?.() === undefined) return;
1205
+ if (q.workflowGate && (q.workflowGate.stage !== "deep-interview" || q.workflowGate.kind !== "question")) return;
1101
1206
  const cwd = this.session.cwd;
1102
1207
  const sessionId = this.session.getSessionId?.() ?? undefined;
1103
1208
  const statePath = deepInterviewStatePath(cwd, sessionId);
@@ -1154,12 +1259,13 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
1154
1259
  };
1155
1260
  const gateEmitter = this.session.getWorkflowGateEmitter?.();
1156
1261
  // 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.
1262
+ // supportsRemoteGateAnswers() is always true, so it can no longer signal
1263
+ // "no local UI". The workflow gate is only the headless (non-TUI) answer
1264
+ // path: when a real interactive UI is present, prefer it — otherwise
1265
+ // attended TUI asks would route to emitGate() and hang forever waiting on
1266
+ // a remote responder.
1161
1267
  const hasInteractiveUi = context?.hasUI === true && !!context.ui;
1162
- const canUseWorkflowGate = !hasInteractiveUi && gateEmitter?.isUnattended() === true;
1268
+ const canUseWorkflowGate = !hasInteractiveUi && gateEmitter?.supportsRemoteGateAnswers() === true;
1163
1269
  // Headless fallback: SDK workflow gates are the non-TUI answer path.
1164
1270
  if (!canUseWorkflowGate && (!context?.hasUI || !context.ui)) {
1165
1271
  context?.abort();
@@ -1428,10 +1534,12 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
1428
1534
  }),
1429
1535
  }
1430
1536
  : options?.previous;
1537
+ const recommendedIndex = validRecommendedIndex(q.recommended, rawOptionLabels.length);
1431
1538
  activeRemoteRequest = {
1432
1539
  question: displayQuestion,
1433
1540
  options: remoteSelectorOptions,
1434
1541
  interaction: "selector",
1542
+ ...(recommendedIndex === undefined ? {} : { recommendedIndex }),
1435
1543
  controls: askRemoteControls({
1436
1544
  multi: q.multi === true,
1437
1545
  questionIndex,
@@ -1463,6 +1571,9 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
1463
1571
  question: displayQuestion,
1464
1572
  options: state.interaction === "selector" ? remoteSelectorOptions : [],
1465
1573
  interaction: state.interaction,
1574
+ ...(state.interaction === "selector" && recommendedIndex !== undefined
1575
+ ? { recommendedIndex }
1576
+ : {}),
1466
1577
  controls:
1467
1578
  state.interaction === "selector"
1468
1579
  ? 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()