@gajae-code/coding-agent 0.7.8 → 0.7.10

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 (92) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +8 -0
  3. package/dist/types/config/keybindings.d.ts +1 -1
  4. package/dist/types/config/model-profile-activation.d.ts +19 -2
  5. package/dist/types/config/model-profiles.d.ts +1 -0
  6. package/dist/types/config/model-registry.d.ts +4 -0
  7. package/dist/types/config/settings-schema.d.ts +12 -3
  8. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  9. package/dist/types/gjc-runtime/launch-tmux.d.ts +19 -3
  10. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -1
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -4
  12. package/dist/types/modes/components/hook-input.d.ts +1 -0
  13. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  14. package/dist/types/modes/components/model-selector.d.ts +9 -0
  15. package/dist/types/modes/components/status-line.d.ts +3 -0
  16. package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -1
  17. package/dist/types/modes/interactive-mode.d.ts +3 -1
  18. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +41 -7
  19. package/dist/types/modes/types.d.ts +3 -1
  20. package/dist/types/notifications/daemon-paths.d.ts +9 -0
  21. package/dist/types/notifications/html-format.d.ts +2 -0
  22. package/dist/types/notifications/recent-activity.d.ts +5 -1
  23. package/dist/types/notifications/telegram-daemon-cli.d.ts +8 -2
  24. package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
  25. package/dist/types/notifications/telegram-daemon.d.ts +1 -10
  26. package/dist/types/notifications/telegram-reference.d.ts +4 -0
  27. package/dist/types/notifications/threaded-render.d.ts +1 -0
  28. package/dist/types/tools/ask.d.ts +26 -0
  29. package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
  30. package/dist/types/utils/cmux-workspace.d.ts +23 -0
  31. package/dist/types/web/search/index.d.ts +1 -1
  32. package/dist/types/web/search/provider.d.ts +5 -0
  33. package/dist/types/web/search/render.d.ts +1 -0
  34. package/package.json +7 -7
  35. package/src/cli/notify-cli.ts +1 -1
  36. package/src/cli/web-search-cli.ts +3 -2
  37. package/src/cli.ts +1 -0
  38. package/src/config/keybindings.ts +1 -1
  39. package/src/config/model-profile-activation.ts +92 -11
  40. package/src/config/model-profiles.ts +15 -2
  41. package/src/config/model-registry.ts +81 -5
  42. package/src/config/settings-schema.ts +14 -3
  43. package/src/coordinator-mcp/server.ts +4 -2
  44. package/src/deep-interview/render-middleware.ts +1 -0
  45. package/src/defaults/gjc/skills/deep-interview/SKILL.md +40 -8
  46. package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -7
  47. package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -112
  48. package/src/extensibility/extensions/types.ts +11 -0
  49. package/src/gjc-runtime/deep-interview-runtime.ts +1 -1
  50. package/src/gjc-runtime/launch-tmux.ts +366 -48
  51. package/src/gjc-runtime/team-runtime.ts +69 -4
  52. package/src/gjc-runtime/ultragoal-guard.ts +150 -24
  53. package/src/gjc-runtime/ultragoal-runtime.ts +16 -124
  54. package/src/gjc-runtime/workflow-manifest.generated.json +0 -8
  55. package/src/gjc-runtime/workflow-manifest.ts +0 -1
  56. package/src/hooks/skill-state.ts +33 -73
  57. package/src/internal-urls/docs-index.generated.ts +8 -7
  58. package/src/modes/components/custom-model-preset-wizard.ts +2 -4
  59. package/src/modes/components/hook-editor.ts +7 -4
  60. package/src/modes/components/hook-input.ts +4 -0
  61. package/src/modes/components/hook-selector.ts +21 -3
  62. package/src/modes/components/model-selector.ts +84 -13
  63. package/src/modes/components/settings-selector.ts +83 -24
  64. package/src/modes/components/status-line/presets.ts +11 -0
  65. package/src/modes/components/status-line/segments.ts +10 -2
  66. package/src/modes/components/status-line.ts +1 -0
  67. package/src/modes/controllers/extension-ui-controller.ts +20 -2
  68. package/src/modes/controllers/selector-controller.ts +130 -7
  69. package/src/modes/interactive-mode.ts +7 -2
  70. package/src/modes/shared/agent-wire/deep-interview-gate.ts +64 -13
  71. package/src/modes/types.ts +6 -1
  72. package/src/notifications/daemon-paths.ts +22 -0
  73. package/src/notifications/html-format.ts +75 -1
  74. package/src/notifications/index.ts +12 -1
  75. package/src/notifications/recent-activity.ts +32 -8
  76. package/src/notifications/telegram-daemon-cli.ts +13 -4
  77. package/src/notifications/telegram-daemon-control.ts +9 -0
  78. package/src/notifications/telegram-daemon.ts +69 -45
  79. package/src/notifications/telegram-reference.ts +36 -14
  80. package/src/notifications/threaded-render.ts +14 -6
  81. package/src/prompts/system/system-prompt.md +10 -1
  82. package/src/sdk.ts +13 -6
  83. package/src/session/agent-session.ts +10 -1
  84. package/src/tools/ask.ts +147 -17
  85. package/src/tools/eval.ts +2 -2
  86. package/src/tools/render-utils.ts +2 -0
  87. package/src/tools/ultragoal-ask-guard.ts +39 -5
  88. package/src/utils/cmux-workspace.ts +106 -0
  89. package/src/utils/title-generator.ts +2 -0
  90. package/src/web/search/index.ts +22 -10
  91. package/src/web/search/provider.ts +15 -0
  92. package/src/web/search/render.ts +3 -0
package/src/tools/ask.ts CHANGED
@@ -62,6 +62,11 @@ const DeepInterviewMeta = z.object({
62
62
  ambiguity: z.number().min(0).max(1).describe("ambiguity at ask time (0..1)"),
63
63
  });
64
64
 
65
+ const WorkflowGateMeta = z.object({
66
+ stage: z.enum(["deep-interview", "ralplan", "ultragoal"]).describe("workflow gate stage"),
67
+ kind: z.enum(["question", "approval", "execution"]).describe("workflow gate kind"),
68
+ });
69
+
65
70
  const QuestionItem = z.object({
66
71
  id: z.string().describe("question id"),
67
72
  question: z.string().describe("question text"),
@@ -69,6 +74,7 @@ const QuestionItem = z.object({
69
74
  multi: z.boolean().describe("allow multiple selections").optional(),
70
75
  recommended: z.number().describe("recommended option index").optional(),
71
76
  deepInterview: DeepInterviewMeta.describe("optional deep-interview round metadata").optional(),
77
+ workflowGate: WorkflowGateMeta.describe("optional workflow gate stage/kind override").optional(),
72
78
  });
73
79
 
74
80
  export const askSchema = z.object({
@@ -85,6 +91,7 @@ export interface QuestionResult {
85
91
  multi: boolean;
86
92
  selectedOptions: string[];
87
93
  customInput?: string;
94
+ clarificationQuestion?: string;
88
95
  }
89
96
 
90
97
  export interface AskToolDetails {
@@ -93,6 +100,7 @@ export interface AskToolDetails {
93
100
  multi?: boolean;
94
101
  selectedOptions?: string[];
95
102
  customInput?: string;
103
+ clarificationQuestion?: string;
96
104
  /** Multi-part question mode */
97
105
  results?: QuestionResult[];
98
106
  }
@@ -102,6 +110,7 @@ export interface AskToolDetails {
102
110
  // =============================================================================
103
111
 
104
112
  const OTHER_OPTION = "Other (type your own)";
113
+ const ASK_CLARIFICATION_OPTION = "Ask about these choices";
105
114
  const RECOMMENDED_SUFFIX = " (Recommended)";
106
115
  const DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS = Number.MAX_SAFE_INTEGER;
107
116
  const DEEP_INTERVIEW_RECORDER_AWAIT_TIMEOUT_MS = 250;
@@ -177,6 +186,7 @@ function numberOptionLabels(labels: string[]): string[] {
177
186
  interface SelectionResult {
178
187
  selectedOptions: string[];
179
188
  customInput?: string;
189
+ clarificationQuestion?: string;
180
190
  timedOut: boolean;
181
191
  navigation?: "back" | "forward";
182
192
  cancelled?: boolean;
@@ -195,6 +205,7 @@ interface AskSingleQuestionOptions {
195
205
  navigation?: NavigationControls;
196
206
  scrollTitleRows?: number;
197
207
  otherOptionLabel?: string;
208
+ clarificationOptionLabel?: string;
198
209
  }
199
210
 
200
211
  interface UIContext {
@@ -213,6 +224,7 @@ interface UIContext {
213
224
  onRight?: () => void;
214
225
  helpText?: string;
215
226
  customInput?: { optionLabel: string; onSubmit: (text: string) => void };
227
+ clarificationInput?: { optionLabel: string; onSubmit: (text: string) => void; allowEmpty?: boolean };
216
228
  },
217
229
  ): Promise<string | undefined>;
218
230
  editor(
@@ -233,6 +245,7 @@ async function askSingleQuestion(
233
245
  const { recommended, timeout, signal, initialSelection, navigation, scrollTitleRows } = options;
234
246
  const doneLabel = getDoneOptionLabel();
235
247
  const otherOptionLabel = options.otherOptionLabel ?? OTHER_OPTION;
248
+ const clarificationOptionLabel = options.clarificationOptionLabel;
236
249
  let selectedOptions = [...(initialSelection?.selectedOptions ?? [])];
237
250
  let customInput = initialSelection?.customInput;
238
251
  let timedOut = false;
@@ -246,15 +259,17 @@ async function askSingleQuestion(
246
259
  timedOut: boolean;
247
260
  navigation?: "back" | "forward";
248
261
  inlineInput?: string;
262
+ inlineClarification?: string;
249
263
  }> => {
250
264
  let timeoutTriggered = false;
251
265
  const onTimeout = () => {
252
266
  timeoutTriggered = true;
253
267
  };
254
- // Inline custom input: the TUI selector keeps the question and option
255
- // list on screen and collects the "Other" text below the list, instead
256
- // of swapping to a separate editor screen that hides the question.
268
+ // Inline custom/clarification input: the TUI selector keeps the question
269
+ // and option list on screen and collects text below the list, instead of
270
+ // swapping to a separate editor screen that hides the question.
257
271
  let inlineInput: string | undefined;
272
+ let inlineClarification: string | undefined;
258
273
  let navigationAction: "back" | "forward" | undefined;
259
274
  const baseHelpText = navigation
260
275
  ? "up/down navigate enter select ←/→ question esc cancel"
@@ -280,6 +295,15 @@ async function askSingleQuestion(
280
295
  inlineInput = text;
281
296
  },
282
297
  },
298
+ clarificationInput: clarificationOptionLabel
299
+ ? {
300
+ optionLabel: clarificationOptionLabel,
301
+ allowEmpty: false,
302
+ onSubmit: (text: string) => {
303
+ inlineClarification = text;
304
+ },
305
+ }
306
+ : undefined,
283
307
  onLeft: navigation?.allowBack
284
308
  ? () => {
285
309
  navigationAction = "back";
@@ -298,7 +322,7 @@ async function askSingleQuestion(
298
322
  if (!timeoutTriggered && choice === undefined && typeof timeout === "number") {
299
323
  timeoutTriggered = Date.now() - startMs >= timeout;
300
324
  }
301
- return { choice, timedOut: timeoutTriggered, navigation: navigationAction, inlineInput };
325
+ return { choice, timedOut: timeoutTriggered, navigation: navigationAction, inlineInput, inlineClarification };
302
326
  };
303
327
 
304
328
  // Fallback for UI contexts that don't support inline custom input (they
@@ -309,6 +333,13 @@ async function askSingleQuestion(
309
333
  const input = signal ? await untilAborted(signal, showCustomInput) : await showCustomInput();
310
334
  return { input };
311
335
  };
336
+ const promptForClarificationInput = async (): Promise<{ input: string | undefined }> => {
337
+ const dialogOptions = signal ? { signal } : undefined;
338
+ const showClarificationInput = () =>
339
+ ui.editor("Ask a clarification question:", undefined, dialogOptions, { promptStyle: true });
340
+ const input = signal ? await untilAborted(signal, showClarificationInput) : await showClarificationInput();
341
+ return { input: input !== undefined && input.trim() === "" ? undefined : input };
342
+ };
312
343
 
313
344
  const promptWithProgress = navigation?.progressText ? `${question} (${navigation.progressText})` : question;
314
345
  if (multi) {
@@ -331,6 +362,9 @@ async function askSingleQuestion(
331
362
  opts.push(doneLabel);
332
363
  }
333
364
  opts.push(otherOptionLabel);
365
+ if (clarificationOptionLabel) {
366
+ opts.push(clarificationOptionLabel);
367
+ }
334
368
 
335
369
  const prefix = selected.size > 0 ? `(${selected.size} selected) ` : "";
336
370
  const {
@@ -338,6 +372,7 @@ async function askSingleQuestion(
338
372
  timedOut: selectTimedOut,
339
373
  navigation: arrowNavigation,
340
374
  inlineInput,
375
+ inlineClarification,
341
376
  } = await selectOption(`${prefix}${promptWithProgress}`, opts, cursorIndex);
342
377
 
343
378
  if (arrowNavigation) {
@@ -364,6 +399,18 @@ async function askSingleQuestion(
364
399
  customInput = input;
365
400
  break;
366
401
  }
402
+ if (clarificationOptionLabel && choice === clarificationOptionLabel) {
403
+ if (selectTimedOut) {
404
+ timedOut = true;
405
+ break;
406
+ }
407
+ const input =
408
+ inlineClarification !== undefined ? inlineClarification : (await promptForClarificationInput()).input;
409
+ if (input === undefined) {
410
+ break;
411
+ }
412
+ return { selectedOptions: [], clarificationQuestion: input, timedOut };
413
+ }
367
414
 
368
415
  const selectedIdx = opts.indexOf(choice);
369
416
  if (selectedIdx >= 0) {
@@ -394,7 +441,9 @@ async function askSingleQuestion(
394
441
  selectedOptions = Array.from(selected);
395
442
  } else {
396
443
  const displayLabels = addRecommendedSuffix(optionLabels, recommended);
397
- const optionsWithNavigation = [...displayLabels, otherOptionLabel];
444
+ const optionsWithNavigation = clarificationOptionLabel
445
+ ? [...displayLabels, otherOptionLabel, clarificationOptionLabel]
446
+ : [...displayLabels, otherOptionLabel];
398
447
 
399
448
  let initialIndex = recommended;
400
449
  const previouslySelected = selectedOptions[0];
@@ -414,6 +463,7 @@ async function askSingleQuestion(
414
463
  timedOut: selectTimedOut,
415
464
  navigation: arrowNavigation,
416
465
  inlineInput,
466
+ inlineClarification,
417
467
  } = await selectOption(promptWithProgress, optionsWithNavigation, initialIndex);
418
468
  timedOut = selectTimedOut;
419
469
 
@@ -433,6 +483,15 @@ async function askSingleQuestion(
433
483
  }
434
484
  // If input was dismissed (undefined), keep prior selectedOptions/customInput intact
435
485
  }
486
+ } else if (clarificationOptionLabel && choice === clarificationOptionLabel) {
487
+ if (!selectTimedOut) {
488
+ const input =
489
+ inlineClarification !== undefined ? inlineClarification : (await promptForClarificationInput()).input;
490
+ if (input !== undefined) {
491
+ return { selectedOptions: [], clarificationQuestion: input, timedOut };
492
+ }
493
+ // If input was dismissed (undefined), keep prior selectedOptions/customInput intact
494
+ }
436
495
  } else {
437
496
  const stripped = stripRecommendedSuffix(choice);
438
497
  if (optionLabels.includes(stripped)) {
@@ -460,6 +519,9 @@ async function askSingleQuestion(
460
519
  }
461
520
 
462
521
  function formatQuestionResult(result: QuestionResult): string {
522
+ if (result.clarificationQuestion !== undefined) {
523
+ return `${result.id}: clarification requested: ${result.clarificationQuestion}`;
524
+ }
463
525
  if (result.customInput !== undefined) {
464
526
  return `${result.id}: "${result.customInput}"`;
465
527
  }
@@ -551,7 +613,10 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
551
613
  _onUpdate?: AgentToolUpdateCallback<AskToolDetails>,
552
614
  context?: AgentToolContext,
553
615
  ): Promise<AgentToolResult<AskToolDetails>> {
554
- await assertUltragoalAskAllowed(this.session.cwd);
616
+ await assertUltragoalAskAllowed(this.session.cwd, {
617
+ activeSkillState: this.session.getActiveSkillState?.(),
618
+ sessionId: this.session.getSessionId?.() ?? null,
619
+ });
555
620
  const gateEmitter = this.session.getWorkflowGateEmitter?.();
556
621
  const canUseWorkflowGate = gateEmitter?.isUnattended() === true;
557
622
 
@@ -665,6 +730,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
665
730
  multi: q.multi,
666
731
  recommended: q.recommended,
667
732
  deepInterview: q.deepInterview,
733
+ workflowGate: q.workflowGate,
668
734
  };
669
735
  const answer = await gateEmitter.emitGate(questionToGate(gateQuestion));
670
736
  const decoded = gateAnswerToResult(gateQuestion, answer);
@@ -672,6 +738,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
672
738
  optionLabels: rawOptionLabels,
673
739
  selectedOptions: decoded.selectedOptions,
674
740
  customInput: decoded.customInput,
741
+ clarificationQuestion: decoded.clarificationQuestion,
675
742
  navigation: undefined as NavigationControls | undefined,
676
743
  cancelled: false,
677
744
  timedOut: false,
@@ -683,6 +750,9 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
683
750
  const displayQuestion = deepInterviewPrompt ?? q.question;
684
751
  const shouldNumberOptions = isDeepInterviewQuestion || isDeepInterviewAskQuestion(q.question);
685
752
  const optionLabels = shouldNumberOptions ? numberOptionLabels(rawOptionLabels) : rawOptionLabels;
753
+ const clarificationOptionLabel = shouldNumberOptions
754
+ ? formatNumberedOptionLabel(ASK_CLARIFICATION_OPTION, optionLabels.length + 1)
755
+ : undefined;
686
756
  const initialSelection =
687
757
  shouldNumberOptions && options?.previous
688
758
  ? {
@@ -696,6 +766,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
696
766
  const {
697
767
  selectedOptions: displaySelectedOptions,
698
768
  customInput,
769
+ clarificationQuestion,
699
770
  navigation,
700
771
  cancelled,
701
772
  timedOut,
@@ -709,6 +780,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
709
780
  otherOptionLabel: shouldNumberOptions
710
781
  ? formatNumberedOptionLabel(OTHER_OPTION, optionLabels.length)
711
782
  : undefined,
783
+ clarificationOptionLabel,
712
784
  });
713
785
  const selectedOptions = shouldNumberOptions
714
786
  ? displaySelectedOptions.map(selected => {
@@ -716,7 +788,15 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
716
788
  return displayIndex >= 0 ? (rawOptionLabels[displayIndex] ?? selected) : selected;
717
789
  })
718
790
  : displaySelectedOptions;
719
- return { optionLabels: rawOptionLabels, selectedOptions, customInput, navigation, cancelled, timedOut };
791
+ return {
792
+ optionLabels: rawOptionLabels,
793
+ selectedOptions,
794
+ customInput,
795
+ clarificationQuestion,
796
+ navigation,
797
+ cancelled,
798
+ timedOut,
799
+ };
720
800
  } catch (error) {
721
801
  if (error instanceof Error && error.name === "AbortError") {
722
802
  throw new ToolAbortError("Ask input was cancelled");
@@ -727,22 +807,40 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
727
807
 
728
808
  if (params.questions.length === 1) {
729
809
  const [q] = params.questions;
730
- const { optionLabels, selectedOptions, customInput, cancelled, timedOut } = await askQuestion(q);
731
-
732
- if (!timedOut && (cancelled || (selectedOptions.length === 0 && customInput === undefined))) {
810
+ const { optionLabels, selectedOptions, customInput, clarificationQuestion, cancelled, timedOut } =
811
+ await askQuestion(q);
812
+
813
+ if (
814
+ !timedOut &&
815
+ (cancelled ||
816
+ (selectedOptions.length === 0 && customInput === undefined && clarificationQuestion === undefined))
817
+ ) {
733
818
  context?.abort();
734
819
  throw new ToolAbortError("Ask tool was cancelled by the user");
735
820
  }
736
- await this.#recordDeepInterviewRound(q, selectedOptions, customInput);
821
+ if (clarificationQuestion === undefined) {
822
+ await this.#recordDeepInterviewRound(q, selectedOptions, customInput);
823
+ }
737
824
  const details: AskToolDetails = {
738
825
  question: q.question,
739
826
  options: optionLabels,
740
827
  multi: q.multi ?? false,
741
828
  selectedOptions,
742
829
  customInput,
830
+ clarificationQuestion,
743
831
  };
744
832
 
745
833
  const responseParts: string[] = [];
834
+ if (clarificationQuestion !== undefined) {
835
+ responseParts.push(
836
+ clarificationQuestion.includes("\n")
837
+ ? `User asked a clarification question about the choices:\n${clarificationQuestion
838
+ .split("\n")
839
+ .map(line => ` ${line}`)
840
+ .join("\n")}`
841
+ : `User asked a clarification question about the choices: ${clarificationQuestion}`,
842
+ );
843
+ }
746
844
  if (selectedOptions.length > 0) {
747
845
  responseParts.push(
748
846
  q.multi ? `User selected: ${selectedOptions.join(", ")}` : `User selected: ${selectedOptions[0]}`,
@@ -777,6 +875,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
777
875
  optionLabels,
778
876
  selectedOptions,
779
877
  customInput,
878
+ clarificationQuestion,
780
879
  navigation: navAction,
781
880
  cancelled,
782
881
  timedOut,
@@ -794,9 +893,12 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
794
893
  multi: q.multi ?? false,
795
894
  selectedOptions,
796
895
  customInput,
896
+ clarificationQuestion,
797
897
  };
798
898
 
799
- await this.#recordDeepInterviewRound(q, selectedOptions, customInput);
899
+ if (clarificationQuestion === undefined) {
900
+ await this.#recordDeepInterviewRound(q, selectedOptions, customInput);
901
+ }
800
902
 
801
903
  if (navAction === "back") {
802
904
  questionIndex = Math.max(0, questionIndex - 1);
@@ -985,7 +1087,10 @@ export const askToolRenderer = {
985
1087
  // Multi-part results
986
1088
  if (details.results && details.results.length > 0) {
987
1089
  const hasAnySelection = details.results.some(
988
- r => r.customInput !== undefined || (r.selectedOptions && r.selectedOptions.length > 0),
1090
+ r =>
1091
+ r.clarificationQuestion !== undefined ||
1092
+ r.customInput !== undefined ||
1093
+ (r.selectedOptions && r.selectedOptions.length > 0),
989
1094
  );
990
1095
  const header = renderStatusLine(
991
1096
  {
@@ -1003,7 +1108,8 @@ export const askToolRenderer = {
1003
1108
  const isLastQuestion = i === details.results.length - 1;
1004
1109
  const branch = isLastQuestion ? uiTheme.tree.last : uiTheme.tree.branch;
1005
1110
  const continuation = isLastQuestion ? " " : `${uiTheme.fg("dim", uiTheme.tree.vertical)} `;
1006
- const hasSelection = r.customInput !== undefined || r.selectedOptions.length > 0;
1111
+ const hasSelection =
1112
+ r.clarificationQuestion !== undefined || r.customInput !== undefined || r.selectedOptions.length > 0;
1007
1113
  const statusIcon = hasSelection
1008
1114
  ? uiTheme.styledSymbol("status.success", "success")
1009
1115
  : uiTheme.styledSymbol("status.warning", "warning");
@@ -1018,7 +1124,10 @@ export const askToolRenderer = {
1018
1124
 
1019
1125
  const answerLines: string[] = [];
1020
1126
  for (let j = 0; j < r.selectedOptions.length; j++) {
1021
- const isLast = j === r.selectedOptions.length - 1 && r.customInput === undefined;
1127
+ const isLast =
1128
+ j === r.selectedOptions.length - 1 &&
1129
+ r.customInput === undefined &&
1130
+ r.clarificationQuestion === undefined;
1022
1131
  const optBranch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
1023
1132
  const selectedLabel = renderInlineMarkdown(r.selectedOptions[j], mdTheme, t =>
1024
1133
  uiTheme.fg("toolOutput", t),
@@ -1032,6 +1141,14 @@ export const askToolRenderer = {
1032
1141
  }
1033
1142
  if (r.customInput !== undefined) {
1034
1143
  container.addChild(new Text(renderCustomInput(uiTheme, continuation, r.customInput, true, false), 0, 0));
1144
+ } else if (r.clarificationQuestion !== undefined) {
1145
+ container.addChild(
1146
+ new Text(
1147
+ renderCustomInput(uiTheme, continuation, `Clarification: ${r.clarificationQuestion}`, true, false),
1148
+ 0,
1149
+ 0,
1150
+ ),
1151
+ );
1035
1152
  } else if (r.selectedOptions.length === 0) {
1036
1153
  container.addChild(
1037
1154
  new Text(
@@ -1053,7 +1170,9 @@ export const askToolRenderer = {
1053
1170
  }
1054
1171
 
1055
1172
  const hasSelection =
1056
- details.customInput !== undefined || (details.selectedOptions && details.selectedOptions.length > 0);
1173
+ details.clarificationQuestion !== undefined ||
1174
+ details.customInput !== undefined ||
1175
+ (details.selectedOptions && details.selectedOptions.length > 0);
1057
1176
  const header = renderStatusLine({ icon: hasSelection ? "success" : "warning", title: "Ask" }, uiTheme);
1058
1177
  const container = new Container();
1059
1178
  container.addChild(new Text(header, 0, 0));
@@ -1065,7 +1184,10 @@ export const askToolRenderer = {
1065
1184
  const answerLines: string[] = [];
1066
1185
  if (details.selectedOptions && details.selectedOptions.length > 0) {
1067
1186
  for (let i = 0; i < details.selectedOptions.length; i++) {
1068
- const isLast = i === details.selectedOptions.length - 1 && details.customInput === undefined;
1187
+ const isLast =
1188
+ i === details.selectedOptions.length - 1 &&
1189
+ details.customInput === undefined &&
1190
+ details.clarificationQuestion === undefined;
1069
1191
  const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
1070
1192
  const selectedLabel = renderInlineMarkdown(details.selectedOptions[i], mdTheme, t =>
1071
1193
  uiTheme.fg("toolOutput", t),
@@ -1080,6 +1202,14 @@ export const askToolRenderer = {
1080
1202
  }
1081
1203
  if (details.customInput !== undefined) {
1082
1204
  container.addChild(new Text(renderCustomInput(uiTheme, " ", details.customInput, true, false), 0, 0));
1205
+ } else if (details.clarificationQuestion !== undefined) {
1206
+ container.addChild(
1207
+ new Text(
1208
+ renderCustomInput(uiTheme, " ", `Clarification: ${details.clarificationQuestion}`, true, false),
1209
+ 0,
1210
+ 0,
1211
+ ),
1212
+ );
1083
1213
  } else if (!details.selectedOptions || details.selectedOptions.length === 0) {
1084
1214
  container.addChild(
1085
1215
  new Text(
package/src/tools/eval.ts CHANGED
@@ -754,8 +754,8 @@ function formatStatusEventExpanded(event: EvalStatusEvent, theme: Theme): string
754
754
  case "git_log":
755
755
  if (data.entries) {
756
756
  addItems(data.entries as unknown[], e => {
757
- const entry = e as { sha: string; subject: string };
758
- return `${entry.sha} ${truncateToWidth(entry.subject, 50)}`;
757
+ const entry = e as { sha?: string; subject?: string };
758
+ return `${String(entry.sha ?? "")} ${truncateToWidth(String(entry.subject ?? ""), 50)}`;
759
759
  });
760
760
  }
761
761
  break;
@@ -86,6 +86,7 @@ export const EXPAND_HINT = "(Ctrl+O for more)";
86
86
  * Get first N lines of text as preview, with each line truncated.
87
87
  */
88
88
  export function getPreviewLines(text: string, maxLines: number, maxLineLen: number, ellipsis?: Ellipsis): string[] {
89
+ if (typeof text !== "string") return [];
89
90
  const lines = text.split("\n").filter(l => l.trim());
90
91
  return lines.slice(0, maxLines).map(l => truncateToWidth(l.trim(), maxLineLen, ellipsis));
91
92
  }
@@ -568,6 +569,7 @@ export function truncateDiffByHunk(
568
569
  // =============================================================================
569
570
 
570
571
  export function shortenPath(filePath: string, homeDir?: string): string {
572
+ if (typeof filePath !== "string") return "";
571
573
  const home = homeDir ?? os.homedir();
572
574
  if (home && filePath.startsWith(home)) {
573
575
  return `~${filePath.slice(home.length)}`;
@@ -10,6 +10,29 @@ const ULTRAGOAL_ASK_GUARD = Symbol.for("gajae-code.ultragoalAskGuard");
10
10
 
11
11
  type GuardedTool = AgentTool & { [ULTRAGOAL_ASK_GUARD]?: true };
12
12
 
13
+ export interface UltragoalAskGuardContext {
14
+ activeSkillState?: { skill?: string; session_id?: string } | null;
15
+ sessionId?: string | null;
16
+ }
17
+
18
+ const UPSTREAM_PLANNING_ASK_SKILLS = new Set(["deep-interview", "ralplan"]);
19
+
20
+ function normalizedActiveSkill(context?: UltragoalAskGuardContext): string | undefined {
21
+ const skill = context?.activeSkillState?.skill?.trim();
22
+ return skill || undefined;
23
+ }
24
+
25
+ function sessionScopedAskGuardId(
26
+ context: UltragoalAskGuardContext,
27
+ activeSkill: string | undefined,
28
+ ): string | undefined {
29
+ if (activeSkill !== "ultragoal" && !UPSTREAM_PLANNING_ASK_SKILLS.has(activeSkill ?? "")) return undefined;
30
+ const activeSessionId = context.activeSkillState?.session_id?.trim();
31
+ if (activeSessionId) return activeSessionId;
32
+ const sessionId = context.sessionId?.trim();
33
+ return sessionId || undefined;
34
+ }
35
+
13
36
  export function formatUltragoalAskBlockMessage(diagnostic: UltragoalAskBlockDiagnostic): string {
14
37
  return [
15
38
  diagnostic.message,
@@ -18,15 +41,26 @@ export function formatUltragoalAskBlockMessage(diagnostic: UltragoalAskBlockDiag
18
41
  ].join("\n");
19
42
  }
20
43
 
21
- export async function assertUltragoalAskAllowed(cwd: string): Promise<void> {
22
- const diagnostic = await isUltragoalAskBlocked(cwd);
44
+ export async function assertUltragoalAskAllowed(cwd: string, context: UltragoalAskGuardContext = {}): Promise<void> {
45
+ const activeSkill = normalizedActiveSkill(context);
46
+ // Deep-interview and ralplan are upstream planning workflows whose core gates
47
+ // are `ask` calls. Scope their Ultragoal check to the current session so stale
48
+ // or ambiguous Ultragoal durable state from another session cannot hijack those
49
+ // prompts; same-session active Ultragoal state still falls through to the
50
+ // blocker/nudge checks below.
51
+ const sessionId = sessionScopedAskGuardId(context, activeSkill);
52
+ const diagnostic = await isUltragoalAskBlocked(cwd, { sessionId });
23
53
  if (!diagnostic.active) return;
24
- const nudge = await consumeUltragoalAskNudge(cwd);
54
+ const nudge = await consumeUltragoalAskNudge(cwd, sessionId);
25
55
  if (nudge.nudged) throw new ToolError(nudge.message);
26
56
  throw new ToolError(formatUltragoalAskBlockMessage(diagnostic));
27
57
  }
28
58
 
29
- export function guardToolForUltragoalAsk<T extends AgentTool>(tool: T, getCwd: () => string): T {
59
+ export function guardToolForUltragoalAsk<T extends AgentTool>(
60
+ tool: T,
61
+ getCwd: () => string,
62
+ getContext: () => UltragoalAskGuardContext = () => ({}),
63
+ ): T {
30
64
  if (tool.name !== "ask") return tool;
31
65
  const candidate = tool as GuardedTool;
32
66
  if (candidate[ULTRAGOAL_ASK_GUARD]) return tool;
@@ -35,7 +69,7 @@ export function guardToolForUltragoalAsk<T extends AgentTool>(tool: T, getCwd: (
35
69
  if (prop === ULTRAGOAL_ASK_GUARD) return true;
36
70
  if (prop !== "execute") return Reflect.get(target, prop, receiver);
37
71
  return async (...args: unknown[]): Promise<unknown> => {
38
- await assertUltragoalAskAllowed(getCwd());
72
+ await assertUltragoalAskAllowed(getCwd(), getContext());
39
73
  return Reflect.apply(target.execute, target, args);
40
74
  };
41
75
  },
@@ -0,0 +1,106 @@
1
+ import { logger } from "@gajae-code/utils";
2
+
3
+ const CMUX_COMMAND = "cmux";
4
+ const CMUX_WORKSPACE_ID_ENV = "CMUX_WORKSPACE_ID";
5
+ const CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
6
+ const CMUX_WORKSPACE_RENAME_TIMEOUT_MS = 1500;
7
+
8
+ export interface CmuxWorkspaceRenameCommand {
9
+ command: string;
10
+ args: string[];
11
+ }
12
+
13
+ export interface CmuxWorkspaceRenameProcess {
14
+ exited: Promise<number>;
15
+ kill(): void;
16
+ unref(): void;
17
+ }
18
+
19
+ export interface CmuxWorkspaceTitleSyncOptions {
20
+ env?: NodeJS.ProcessEnv;
21
+ isTty?: boolean;
22
+ which?: (command: string) => string | null;
23
+ spawn?: (
24
+ command: string[],
25
+ options: { env: NodeJS.ProcessEnv; stdin: "ignore"; stdout: "ignore"; stderr: "ignore" },
26
+ ) => CmuxWorkspaceRenameProcess;
27
+ }
28
+
29
+ function defaultSpawn(
30
+ command: string[],
31
+ options: { env: NodeJS.ProcessEnv; stdin: "ignore"; stdout: "ignore"; stderr: "ignore" },
32
+ ): CmuxWorkspaceRenameProcess {
33
+ return Bun.spawn(command, options);
34
+ }
35
+
36
+ export function sanitizeCmuxWorkspaceTitle(title: string | undefined): string | undefined {
37
+ if (!title) return undefined;
38
+ const sanitized = title.replace(CONTROL_CHARS, " ").replace(/\s+/g, " ").trim();
39
+ return sanitized || undefined;
40
+ }
41
+
42
+ export function buildCmuxWorkspaceRenameCommand(
43
+ sessionName: string | undefined,
44
+ env: NodeJS.ProcessEnv = process.env,
45
+ ): CmuxWorkspaceRenameCommand | null {
46
+ const workspaceId = env[CMUX_WORKSPACE_ID_ENV]?.trim();
47
+ if (!workspaceId) return null;
48
+
49
+ const title = sanitizeCmuxWorkspaceTitle(sessionName);
50
+ if (!title) return null;
51
+
52
+ return {
53
+ command: CMUX_COMMAND,
54
+ args: ["workspace", "rename", workspaceId, "--title", title],
55
+ };
56
+ }
57
+
58
+ export function syncCmuxWorkspaceTitle(
59
+ sessionName: string | undefined,
60
+ options: CmuxWorkspaceTitleSyncOptions = {},
61
+ ): void {
62
+ const isTty = options.isTty ?? process.stdout.isTTY === true;
63
+ if (!isTty) return;
64
+
65
+ const env = options.env ?? process.env;
66
+ const plan = buildCmuxWorkspaceRenameCommand(sessionName, env);
67
+ if (!plan) return;
68
+
69
+ const which = options.which ?? Bun.which;
70
+ let resolvedCommand: string | null;
71
+ try {
72
+ resolvedCommand = which(plan.command);
73
+ } catch (error) {
74
+ logger.debug("cmux workspace rename command lookup failed", { error: String(error) });
75
+ return;
76
+ }
77
+ if (!resolvedCommand) return;
78
+
79
+ const spawn = options.spawn ?? defaultSpawn;
80
+ try {
81
+ const proc = spawn([resolvedCommand, ...plan.args], {
82
+ env,
83
+ stdin: "ignore",
84
+ stdout: "ignore",
85
+ stderr: "ignore",
86
+ });
87
+ proc.unref();
88
+ const timer = setTimeout(() => {
89
+ try {
90
+ proc.kill();
91
+ } catch {}
92
+ }, CMUX_WORKSPACE_RENAME_TIMEOUT_MS);
93
+ timer.unref?.();
94
+ void proc.exited
95
+ .then(exitCode => {
96
+ clearTimeout(timer);
97
+ if (exitCode !== 0) logger.debug("cmux workspace rename exited non-zero", { exitCode });
98
+ })
99
+ .catch(error => {
100
+ clearTimeout(timer);
101
+ logger.debug("cmux workspace rename failed", { error: String(error) });
102
+ });
103
+ } catch (error) {
104
+ logger.debug("cmux workspace rename failed to start", { error: String(error) });
105
+ }
106
+ }
@@ -9,6 +9,7 @@ import type { ModelRegistry } from "../config/model-registry";
9
9
  import { resolveRoleSelection } from "../config/model-resolver";
10
10
  import type { Settings } from "../config/settings";
11
11
  import titleSystemPrompt from "../prompts/system/title-system.md" with { type: "text" };
12
+ import { syncCmuxWorkspaceTitle } from "./cmux-workspace";
12
13
 
13
14
  const TITLE_SYSTEM_PROMPT = prompt.render(titleSystemPrompt);
14
15
 
@@ -218,6 +219,7 @@ export function setTerminalTitle(title: string): void {
218
219
 
219
220
  export function setSessionTerminalTitle(sessionName: string | undefined, cwd?: string): void {
220
221
  setTerminalTitle(formatSessionTerminalTitle(sessionName, cwd));
222
+ syncCmuxWorkspaceTitle(sessionName);
221
223
  }
222
224
 
223
225
  /**