@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.
- package/CHANGELOG.md +40 -0
- package/README.md +8 -0
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/model-profile-activation.d.ts +19 -2
- package/dist/types/config/model-profiles.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +12 -3
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +19 -3
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -4
- package/dist/types/modes/components/hook-input.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- package/dist/types/modes/components/model-selector.d.ts +9 -0
- package/dist/types/modes/components/status-line.d.ts +3 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +3 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +41 -7
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/notifications/daemon-paths.d.ts +9 -0
- package/dist/types/notifications/html-format.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +5 -1
- package/dist/types/notifications/telegram-daemon-cli.d.ts +8 -2
- package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
- package/dist/types/notifications/telegram-daemon.d.ts +1 -10
- package/dist/types/notifications/telegram-reference.d.ts +4 -0
- package/dist/types/notifications/threaded-render.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +26 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
- package/dist/types/utils/cmux-workspace.d.ts +23 -0
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +5 -0
- package/dist/types/web/search/render.d.ts +1 -0
- package/package.json +7 -7
- package/src/cli/notify-cli.ts +1 -1
- package/src/cli/web-search-cli.ts +3 -2
- package/src/cli.ts +1 -0
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-profile-activation.ts +92 -11
- package/src/config/model-profiles.ts +15 -2
- package/src/config/model-registry.ts +81 -5
- package/src/config/settings-schema.ts +14 -3
- package/src/coordinator-mcp/server.ts +4 -2
- package/src/deep-interview/render-middleware.ts +1 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +40 -8
- package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -112
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +1 -1
- package/src/gjc-runtime/launch-tmux.ts +366 -48
- package/src/gjc-runtime/team-runtime.ts +69 -4
- package/src/gjc-runtime/ultragoal-guard.ts +150 -24
- package/src/gjc-runtime/ultragoal-runtime.ts +16 -124
- package/src/gjc-runtime/workflow-manifest.generated.json +0 -8
- package/src/gjc-runtime/workflow-manifest.ts +0 -1
- package/src/hooks/skill-state.ts +33 -73
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/modes/components/custom-model-preset-wizard.ts +2 -4
- package/src/modes/components/hook-editor.ts +7 -4
- package/src/modes/components/hook-input.ts +4 -0
- package/src/modes/components/hook-selector.ts +21 -3
- package/src/modes/components/model-selector.ts +84 -13
- package/src/modes/components/settings-selector.ts +83 -24
- package/src/modes/components/status-line/presets.ts +11 -0
- package/src/modes/components/status-line/segments.ts +10 -2
- package/src/modes/components/status-line.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +20 -2
- package/src/modes/controllers/selector-controller.ts +130 -7
- package/src/modes/interactive-mode.ts +7 -2
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +64 -13
- package/src/modes/types.ts +6 -1
- package/src/notifications/daemon-paths.ts +22 -0
- package/src/notifications/html-format.ts +75 -1
- package/src/notifications/index.ts +12 -1
- package/src/notifications/recent-activity.ts +32 -8
- package/src/notifications/telegram-daemon-cli.ts +13 -4
- package/src/notifications/telegram-daemon-control.ts +9 -0
- package/src/notifications/telegram-daemon.ts +69 -45
- package/src/notifications/telegram-reference.ts +36 -14
- package/src/notifications/threaded-render.ts +14 -6
- package/src/prompts/system/system-prompt.md +10 -1
- package/src/sdk.ts +13 -6
- package/src/session/agent-session.ts +10 -1
- package/src/tools/ask.ts +147 -17
- package/src/tools/eval.ts +2 -2
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/ultragoal-ask-guard.ts +39 -5
- package/src/utils/cmux-workspace.ts +106 -0
- package/src/utils/title-generator.ts +2 -0
- package/src/web/search/index.ts +22 -10
- package/src/web/search/provider.ts +15 -0
- 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
|
|
255
|
-
// list on screen and collects
|
|
256
|
-
//
|
|
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 =
|
|
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 {
|
|
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 } =
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =>
|
|
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 =
|
|
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 =
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
|
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>(
|
|
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
|
/**
|