@gajae-code/coding-agent 0.1.1 → 0.1.2

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.
@@ -12,9 +12,9 @@ import {
12
12
  Text,
13
13
  type TUI,
14
14
  } from "@gajae-code/tui";
15
- import type { ModelRegistry } from "../../config/model-registry";
16
- import { getRoleInfo } from "../../config/model-registry";
17
- import { resolveModelRoleValue } from "../../config/model-resolver";
15
+ import type { GjcModelAssignmentTargetId, ModelRegistry } from "../../config/model-registry";
16
+ import { GJC_MODEL_ASSIGNMENT_TARGET_IDS, GJC_MODEL_ASSIGNMENT_TARGETS } from "../../config/model-registry";
17
+ import { formatModelSelectorValue, resolveModelRoleValue } from "../../config/model-resolver";
18
18
  import type { Settings } from "../../config/settings";
19
19
  import { type ThemeColor, theme } from "../../modes/theme/theme";
20
20
  import { formatModelOnboardingInlineHint } from "../../setup/model-onboarding-guidance";
@@ -53,6 +53,7 @@ interface ModelItem {
53
53
  id: string;
54
54
  model: Model;
55
55
  selector: string;
56
+ thinkingLevel?: ThinkingLevel;
56
57
  }
57
58
 
58
59
  interface CanonicalModelItem {
@@ -64,11 +65,12 @@ interface CanonicalModelItem {
64
65
  searchText: string;
65
66
  normalizedSearchText: string;
66
67
  compactSearchText: string;
68
+ thinkingLevel?: ThinkingLevel;
67
69
  }
68
70
 
69
71
  interface ScopedModelItem {
70
72
  model: Model;
71
- thinkingLevel?: string;
73
+ thinkingLevel?: ThinkingLevel;
72
74
  }
73
75
 
74
76
  interface RoleAssignment {
@@ -78,7 +80,7 @@ interface RoleAssignment {
78
80
 
79
81
  type RoleSelectCallback = (
80
82
  model: Model,
81
- role: "default" | null,
83
+ role: GjcModelAssignmentTargetId | null,
82
84
  thinkingLevel?: ThinkingLevel,
83
85
  selector?: string,
84
86
  ) => void;
@@ -108,7 +110,7 @@ function createProviderTab(providerId: string): ProviderTabState {
108
110
  * Component that renders a canonical model selector with provider tabs.
109
111
  * - Tab/Arrow Left/Right: Switch between provider tabs
110
112
  * - Arrow Up/Down: Navigate model list
111
- * - Enter: Select the highlighted model as the canonical/default model
113
+ * - Enter: Open assignment actions for default plus GJC role-agent models
112
114
  * - Escape: Close selector
113
115
  */
114
116
  export class ModelSelectorComponent extends Container {
@@ -130,6 +132,8 @@ export class ModelSelectorComponent extends Container {
130
132
  #tui: TUI;
131
133
  #scopedModels: ReadonlyArray<ScopedModelItem>;
132
134
  #temporaryOnly: boolean;
135
+ #pendingActionItem?: ModelItem | CanonicalModelItem;
136
+ #selectedActionIndex: number = 0;
133
137
 
134
138
  // Tab state
135
139
  #providers: ProviderTabState[] = STATIC_PROVIDER_TABS;
@@ -141,7 +145,12 @@ export class ModelSelectorComponent extends Container {
141
145
  settings: Settings,
142
146
  modelRegistry: ModelRegistry,
143
147
  scopedModels: ReadonlyArray<ScopedModelItem>,
144
- onSelect: (model: Model, role: "default" | null, thinkingLevel?: ThinkingLevel, selector?: string) => void,
148
+ onSelect: (
149
+ model: Model,
150
+ role: GjcModelAssignmentTargetId | null,
151
+ thinkingLevel?: ThinkingLevel,
152
+ selector?: string,
153
+ ) => void,
145
154
  onCancel: () => void,
146
155
  options?: { temporaryOnly?: boolean; initialSearchInput?: string },
147
156
  ) {
@@ -183,7 +192,7 @@ export class ModelSelectorComponent extends Container {
183
192
  this.#searchInput.onSubmit = () => {
184
193
  const selectedItem = this.#getSelectedItem();
185
194
  if (selectedItem) {
186
- this.#handleSelect(selectedItem, this.#temporaryOnly ? null : "default");
195
+ this.#beginActionMenuOrSelect(selectedItem);
187
196
  }
188
197
  };
189
198
  this.addChild(this.#searchInput);
@@ -219,8 +228,11 @@ export class ModelSelectorComponent extends Container {
219
228
  #loadRoleModels(): void {
220
229
  const allModels = this.#modelRegistry.getAll();
221
230
  const matchPreferences = { usageOrder: this.#settings.getStorage()?.getModelUsageOrder() };
222
- for (const role of ["default"]) {
223
- const roleValue = this.#settings.getModelRole(role);
231
+ const agentModelOverrides = this.#settings.get("task.agentModelOverrides");
232
+ for (const role of GJC_MODEL_ASSIGNMENT_TARGET_IDS) {
233
+ const target = GJC_MODEL_ASSIGNMENT_TARGETS[role];
234
+ const roleValue =
235
+ target.settingsPath === "modelRoles" ? this.#settings.getModelRole(role) : agentModelOverrides[role];
224
236
  if (!roleValue) continue;
225
237
 
226
238
  const resolved = resolveModelRoleValue(roleValue, allModels, {
@@ -336,6 +348,7 @@ export class ModelSelectorComponent extends Container {
336
348
  id: scoped.model.id,
337
349
  model: scoped.model,
338
350
  selector: `${scoped.model.provider}/${scoped.model.id}`,
351
+ thinkingLevel: scoped.thinkingLevel,
339
352
  }));
340
353
  } else {
341
354
  // Reload config and cached discovery state without blocking on live provider refresh
@@ -369,17 +382,20 @@ export class ModelSelectorComponent extends Container {
369
382
  }
370
383
  }
371
384
 
385
+ const candidateModels = models.map(item => item.model);
372
386
  const canonicalRecords = this.#modelRegistry.getCanonicalModels({
373
387
  availableOnly: this.#scopedModels.length === 0,
374
- candidates: models.map(item => item.model),
388
+ candidates: candidateModels,
375
389
  });
390
+ const scopedThinkingBySelector = new Map(models.map(item => [item.selector, item.thinkingLevel]));
376
391
  const canonicalModels = canonicalRecords
377
- .map(record => {
392
+ .map((record): CanonicalModelItem | undefined => {
378
393
  const selectedModel = this.#modelRegistry.resolveCanonicalModel(record.id, {
379
394
  availableOnly: this.#scopedModels.length === 0,
380
- candidates: models.map(item => item.model),
395
+ candidates: candidateModels,
381
396
  });
382
397
  if (!selectedModel) return undefined;
398
+ const selectedSelector = `${selectedModel.provider}/${selectedModel.id}`;
383
399
  const searchText = [
384
400
  record.id,
385
401
  record.name,
@@ -388,8 +404,8 @@ export class ModelSelectorComponent extends Container {
388
404
  selectedModel.name,
389
405
  ...record.variants.flatMap(variant => [variant.selector, variant.model.name]),
390
406
  ].join(" ");
391
- return {
392
- kind: "canonical" as const,
407
+ const item: CanonicalModelItem = {
408
+ kind: "canonical",
393
409
  id: record.id,
394
410
  model: selectedModel,
395
411
  selector: record.id,
@@ -398,6 +414,11 @@ export class ModelSelectorComponent extends Container {
398
414
  normalizedSearchText: normalizeSearchText(searchText),
399
415
  compactSearchText: compactSearchText(searchText),
400
416
  };
417
+ const scopedThinkingLevel = scopedThinkingBySelector.get(selectedSelector);
418
+ if (scopedThinkingLevel !== undefined) {
419
+ item.thinkingLevel = scopedThinkingLevel;
420
+ }
421
+ return item;
401
422
  })
402
423
  .filter((item): item is CanonicalModelItem => item !== undefined);
403
424
 
@@ -627,12 +648,14 @@ export class ModelSelectorComponent extends Container {
627
648
 
628
649
  // Build role badges (inverted: color as background, black text)
629
650
  const roleBadgeTokens: string[] = [];
630
- const defaultRoleInfo = getRoleInfo("default", this.#settings);
631
- const defaultAssigned = this.#roles.default;
632
- if (defaultRoleInfo.tag && defaultAssigned && modelsAreEqual(defaultAssigned.model, item.model)) {
633
- const badge = makeInvertedBadge(defaultRoleInfo.tag, defaultRoleInfo.color ?? "success");
634
- const thinkingLabel = getThinkingLevelMetadata(defaultAssigned.thinkingLevel).label;
635
- roleBadgeTokens.push(`${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`);
651
+ for (const role of GJC_MODEL_ASSIGNMENT_TARGET_IDS) {
652
+ const roleInfo = GJC_MODEL_ASSIGNMENT_TARGETS[role];
653
+ const assigned = this.#roles[role];
654
+ if (roleInfo.tag && assigned && modelsAreEqual(assigned.model, item.model)) {
655
+ const badge = makeInvertedBadge(roleInfo.tag, roleInfo.color ?? "muted");
656
+ const thinkingLabel = getThinkingLevelMetadata(assigned.thinkingLevel).label;
657
+ roleBadgeTokens.push(`${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`);
658
+ }
636
659
  }
637
660
  const badgeText = roleBadgeTokens.length > 0 ? ` ${roleBadgeTokens.join(" ")}` : "";
638
661
 
@@ -699,8 +722,26 @@ export class ModelSelectorComponent extends Container {
699
722
  this.#listContainer.addChild(
700
723
  new Text(theme.fg("muted", ` Model Name: ${selected.model.name}${suffix}`), 0, 0),
701
724
  );
725
+ if (this.#pendingActionItem) {
726
+ this.#renderActionMenu(this.#pendingActionItem);
727
+ }
702
728
  }
703
729
  }
730
+ #renderActionMenu(item: ModelItem | CanonicalModelItem): void {
731
+ this.#listContainer.addChild(new Spacer(1));
732
+ this.#listContainer.addChild(new Text(theme.fg("muted", ` Action for: ${item.model.id}`), 0, 0));
733
+ this.#listContainer.addChild(new Spacer(1));
734
+ for (let i = 0; i < GJC_MODEL_ASSIGNMENT_TARGET_IDS.length; i++) {
735
+ const role = GJC_MODEL_ASSIGNMENT_TARGET_IDS[i];
736
+ const target = GJC_MODEL_ASSIGNMENT_TARGETS[role];
737
+ const prefix = i === this.#selectedActionIndex ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
738
+ const label = `Set as ${target.tag ?? role.toUpperCase()} (${target.name})`;
739
+ this.#listContainer.addChild(
740
+ new Text(`${prefix}${i === this.#selectedActionIndex ? theme.fg("accent", label) : label}`, 0, 0),
741
+ );
742
+ }
743
+ }
744
+
704
745
  #getCurrentRoleThinkingLevel(role: string): ThinkingLevel {
705
746
  return this.#roles[role]?.thinkingLevel ?? ThinkingLevel.Inherit;
706
747
  }
@@ -712,6 +753,11 @@ export class ModelSelectorComponent extends Container {
712
753
  }
713
754
 
714
755
  handleInput(keyData: string): void {
756
+ if (this.#pendingActionItem) {
757
+ this.#handleActionMenuInput(keyData);
758
+ return;
759
+ }
760
+
715
761
  // Tab bar navigation
716
762
  if (this.#tabBar?.handleInput(keyData)) {
717
763
  return;
@@ -735,12 +781,12 @@ export class ModelSelectorComponent extends Container {
735
781
  return;
736
782
  }
737
783
 
738
- // Enter - select highlighted model directly. Canonical setup exposes one default model,
739
- // while temporary-only mode keeps the existing non-persistent quick-switch behavior.
784
+ // Enter opens the persistent assignment menu. Temporary-only mode keeps the
785
+ // existing non-persistent quick-switch behavior.
740
786
  if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
741
787
  const selectedItem = this.#getSelectedItem();
742
788
  if (selectedItem) {
743
- this.#handleSelect(selectedItem, this.#temporaryOnly ? null : "default");
789
+ this.#beginActionMenuOrSelect(selectedItem);
744
790
  }
745
791
  return;
746
792
  }
@@ -755,20 +801,66 @@ export class ModelSelectorComponent extends Container {
755
801
  this.#searchInput.handleInput(keyData);
756
802
  this.#filterModels(this.#searchInput.getValue());
757
803
  }
758
- #handleSelect(item: ModelItem | CanonicalModelItem, role: "default" | null, thinkingLevel?: ThinkingLevel): void {
804
+ #beginActionMenuOrSelect(item: ModelItem | CanonicalModelItem): void {
805
+ if (this.#temporaryOnly) {
806
+ this.#handleSelect(item, null);
807
+ return;
808
+ }
809
+ this.#pendingActionItem = item;
810
+ this.#selectedActionIndex = 0;
811
+ this.#updateList();
812
+ }
813
+
814
+ #handleActionMenuInput(keyData: string): void {
815
+ if (matchesKey(keyData, "up")) {
816
+ this.#selectedActionIndex =
817
+ this.#selectedActionIndex === 0
818
+ ? GJC_MODEL_ASSIGNMENT_TARGET_IDS.length - 1
819
+ : this.#selectedActionIndex - 1;
820
+ this.#updateList();
821
+ return;
822
+ }
823
+ if (matchesKey(keyData, "down")) {
824
+ this.#selectedActionIndex = (this.#selectedActionIndex + 1) % GJC_MODEL_ASSIGNMENT_TARGET_IDS.length;
825
+ this.#updateList();
826
+ return;
827
+ }
828
+ if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
829
+ const item = this.#pendingActionItem;
830
+ if (!item) return;
831
+ const role = GJC_MODEL_ASSIGNMENT_TARGET_IDS[this.#selectedActionIndex];
832
+ this.#pendingActionItem = undefined;
833
+ this.#handleSelect(item, role);
834
+ return;
835
+ }
836
+ if (getKeybindings().matches(keyData, "tui.select.cancel")) {
837
+ this.#pendingActionItem = undefined;
838
+ this.#updateList();
839
+ }
840
+ }
841
+
842
+ #handleSelect(
843
+ item: ModelItem | CanonicalModelItem,
844
+ role: GjcModelAssignmentTargetId | null,
845
+ thinkingLevel?: ThinkingLevel,
846
+ ): void {
847
+ const itemThinkingLevel = thinkingLevel ?? item.thinkingLevel;
848
+
759
849
  // For temporary role, don't save to settings - just notify caller
760
850
  if (role === null) {
761
- this.#onSelectCallback(item.model, null, undefined, item.selector);
851
+ this.#onSelectCallback(item.model, null, itemThinkingLevel, item.selector);
762
852
  return;
763
853
  }
764
854
 
765
- const selectedThinkingLevel = thinkingLevel ?? this.#getCurrentRoleThinkingLevel(role);
855
+ const selectedThinkingLevel = itemThinkingLevel ?? this.#getCurrentRoleThinkingLevel(role);
856
+ const selectorValue =
857
+ role === "default" ? item.selector : formatModelSelectorValue(item.selector, selectedThinkingLevel);
766
858
 
767
859
  // Update local state for UI
768
860
  this.#roles[role] = { model: item.model, thinkingLevel: selectedThinkingLevel };
769
861
 
770
862
  // Notify caller (for updating agent state if needed)
771
- this.#onSelectCallback(item.model, role, selectedThinkingLevel, item.selector);
863
+ this.#onSelectCallback(item.model, role, selectedThinkingLevel, selectorValue);
772
864
 
773
865
  // Update list to show new badges
774
866
  this.#updateList();
@@ -435,14 +435,14 @@ export class SelectorController {
435
435
  try {
436
436
  if (role === null) {
437
437
  // Temporary: update agent state but don't persist to settings
438
- await this.ctx.session.setModelTemporary(model);
438
+ await this.ctx.session.setModelTemporary(model, thinkingLevel);
439
439
  this.ctx.statusLine.invalidate();
440
440
  this.ctx.updateEditorBorderColor();
441
441
  this.ctx.showStatus(`Temporary model: ${selector ?? model.id}`);
442
442
  done();
443
443
  this.ctx.ui.requestRender();
444
- } else {
445
- // Default: update agent state and persist
444
+ } else if (role === "default") {
445
+ // Default: update agent state and persist as the active default model.
446
446
  await this.ctx.session.setModel(model, role, {
447
447
  selector,
448
448
  thinkingLevel,
@@ -455,6 +455,19 @@ export class SelectorController {
455
455
  this.ctx.showStatus(`Default model: ${selector ?? model.id}`);
456
456
  done();
457
457
  this.ctx.ui.requestRender();
458
+ } else {
459
+ // Role-agent assignments configure Task dispatch and must not switch the active chat model.
460
+ const apiKey = await this.ctx.session.modelRegistry.getApiKey(model, this.ctx.session.sessionId);
461
+ if (!apiKey) {
462
+ throw new Error(`No API key for ${model.provider}/${model.id}`);
463
+ }
464
+ const overrides = this.ctx.settings.get("task.agentModelOverrides");
465
+ const value = selector ?? `${model.provider}/${model.id}`;
466
+ this.ctx.settings.set("task.agentModelOverrides", { ...overrides, [role]: value });
467
+ this.ctx.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
468
+ this.ctx.showStatus(`${role} agent model: ${value}`);
469
+ done();
470
+ this.ctx.ui.requestRender();
458
471
  }
459
472
  } catch (error) {
460
473
  this.ctx.showError(error instanceof Error ? error.message : String(error));
@@ -84,6 +84,34 @@ function isSkillCommandAutocompleteItem(item: AutocompleteItem): item is SkillCo
84
84
  return "normalizedSkillCommand" in item && item.normalizedSkillCommand === true;
85
85
  }
86
86
 
87
+ function mergeAutocompleteSuggestions(
88
+ primary: { items: AutocompleteItem[]; prefix: string } | null,
89
+ secondary: { items: AutocompleteItem[]; prefix: string } | null,
90
+ ): { items: AutocompleteItem[]; prefix: string } | null {
91
+ if (!primary) return secondary;
92
+ if (!secondary) return primary;
93
+ if (primary.prefix !== secondary.prefix) return primary;
94
+
95
+ const seen = new Set<string>();
96
+ const items: AutocompleteItem[] = [];
97
+ for (const item of [...primary.items, ...secondary.items]) {
98
+ const key = `${item.value}\0${item.label}`;
99
+ if (seen.has(key)) continue;
100
+ seen.add(key);
101
+ items.push(item);
102
+ }
103
+
104
+ return { items, prefix: primary.prefix };
105
+ }
106
+
107
+ function withoutSkillCommandSuggestions(
108
+ suggestions: { items: AutocompleteItem[]; prefix: string } | null,
109
+ ): { items: AutocompleteItem[]; prefix: string } | null {
110
+ if (!suggestions) return null;
111
+ const items = suggestions.items.filter(item => !item.value.startsWith("skill:"));
112
+ return items.length > 0 ? { ...suggestions, items } : null;
113
+ }
114
+
87
115
  function getPromptActionPrefix(textBeforeCursor: string): string | null {
88
116
  const hashIndex = textBeforeCursor.lastIndexOf("#");
89
117
  if (hashIndex === -1) return null;
@@ -148,8 +176,14 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
148
176
  }
149
177
  }
150
178
 
151
- const skillCommandSuggestions = this.#getSkillCommandSuggestions(textBeforeCursor);
152
- if (skillCommandSuggestions) return skillCommandSuggestions;
179
+ const slashPrefix = getSlashTokenPrefix(textBeforeCursor);
180
+ if (slashPrefix) {
181
+ const baseSuggestions = withoutSkillCommandSuggestions(
182
+ await this.#baseProvider.getSuggestions(lines, cursorLine, cursorCol),
183
+ );
184
+ const skillCommandSuggestions = this.#getSkillCommandSuggestions(textBeforeCursor);
185
+ return mergeAutocompleteSuggestions(baseSuggestions, skillCommandSuggestions);
186
+ }
153
187
 
154
188
  if (!isSettingsInitialized() || settings.get("emojiAutocomplete")) {
155
189
  const emojiSuggestions = getEmojiSuggestions(textBeforeCursor);
@@ -215,9 +249,11 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
215
249
  return this.#baseProvider.getInlineHint?.(lines, cursorLine, cursorCol) ?? null;
216
250
  }
217
251
  trySyncSlashCompletion(textBeforeCursor: string): { items: AutocompleteItem[]; prefix: string } | null {
252
+ const baseSuggestions = withoutSkillCommandSuggestions(
253
+ this.#baseProvider.trySyncSlashCompletion?.(textBeforeCursor) ?? null,
254
+ );
218
255
  const skillCommandSuggestions = this.#getSkillCommandSuggestions(textBeforeCursor);
219
- if (skillCommandSuggestions) return skillCommandSuggestions;
220
- return this.#baseProvider.trySyncSlashCompletion?.(textBeforeCursor) ?? null;
256
+ return mergeAutocompleteSuggestions(baseSuggestions, skillCommandSuggestions);
221
257
  }
222
258
  trySyncInlineReplace(textBeforeCursor: string): { replaceLen: number; insert: string } | null {
223
259
  if (isSettingsInitialized() && !settings.get("emojiAutocomplete")) return null;
@@ -233,17 +269,6 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
233
269
  const exactNonSkillCommand = this.#commands.some(
234
270
  command => command.name === query && !command.name.startsWith("skill:"),
235
271
  );
236
- if (exactNonSkillCommand) {
237
- const command = this.#commands.find(
238
- candidate => candidate.name === query && !candidate.name.startsWith("skill:"),
239
- );
240
- if (command) {
241
- return {
242
- items: [{ value: command.name, label: command.name, description: command.description }],
243
- prefix,
244
- };
245
- }
246
- }
247
272
  const items = this.#commands
248
273
  .filter(command => command.name.startsWith("skill:"))
249
274
  .map(command => {
@@ -167,6 +167,7 @@ import {
167
167
  import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
168
168
  import { formatNoCredentialOnboardingError, formatNoModelOnboardingError } from "../setup/model-onboarding-guidance";
169
169
  import { isCanonicalGjcWorkflowSkill, syncSkillActiveState } from "../skill-state/active-state";
170
+ import { assertDeepInterviewMutationAllowed } from "../skill-state/deep-interview-mutation-guard";
170
171
  import { invalidateHostMetadata } from "../ssh/connection-manager";
171
172
  import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
172
173
  import {
@@ -3273,6 +3274,35 @@ export class AgentSession {
3273
3274
  }) as T;
3274
3275
  }
3275
3276
 
3277
+ /**
3278
+ * Wrap a tool with the deep-interview mutation guard. This guard is intentionally
3279
+ * outermost so active interviews reject product-code mutation before ACP permission
3280
+ * prompts or tool execution can run.
3281
+ */
3282
+ #wrapToolForDeepInterviewMutationGuard<T extends AgentTool>(tool: T): T {
3283
+ if (!["edit", "write", "ast_edit"].includes(tool.name)) return tool;
3284
+ return new Proxy(tool, {
3285
+ get: (target, prop) => {
3286
+ if (prop !== "execute") return Reflect.get(target, prop, target);
3287
+ return async (
3288
+ toolCallId: string,
3289
+ args: unknown,
3290
+ signal: AbortSignal | undefined,
3291
+ onUpdate: never,
3292
+ ctx: never,
3293
+ ) => {
3294
+ await assertDeepInterviewMutationAllowed({
3295
+ cwd: this.sessionManager.getCwd(),
3296
+ sessionId: this.sessionManager.getSessionId(),
3297
+ tool: target,
3298
+ args,
3299
+ });
3300
+ return await target.execute(toolCallId, args as never, signal, onUpdate, ctx);
3301
+ };
3302
+ },
3303
+ }) as T;
3304
+ }
3305
+
3276
3306
  async #applyActiveToolsByName(
3277
3307
  toolNames: string[],
3278
3308
  options?: { persistMCPSelection?: boolean; previousSelectedMCPToolNames?: string[] },
@@ -3284,7 +3314,7 @@ export class AgentSession {
3284
3314
  for (const name of toolNames) {
3285
3315
  const tool = this.#toolRegistry.get(name);
3286
3316
  if (tool) {
3287
- tools.push(this.#wrapToolForAcpPermission(tool));
3317
+ tools.push(this.#wrapToolForDeepInterviewMutationGuard(this.#wrapToolForAcpPermission(tool)));
3288
3318
  validToolNames.push(name);
3289
3319
  }
3290
3320
  }
@@ -7,14 +7,16 @@ export const MODEL_ONBOARDING_OAUTH_COMMAND = "/provider login [provider-id] or
7
7
  export function formatModelOnboardingGuidance(): string {
8
8
  return [
9
9
  "Model selection only shows configured providers.",
10
+ "Assignment targets are DEFAULT plus the GJC role agents: EXECUTOR, ARCHITECT, PLANNER, and CRITIC.",
11
+ "Legacy model-role aliases are compatibility-only and are not shown as assignment targets.",
10
12
  `API-compatible providers: ${MODEL_ONBOARDING_API_PROVIDER_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND}).`,
11
13
  `OAuth/subscription providers: ${MODEL_ONBOARDING_OAUTH_COMMAND}.`,
12
- "Then run /model to select a configured model.",
14
+ "Then run /model to select a configured model or assign it to a target.",
13
15
  ].join("\n");
14
16
  }
15
17
 
16
18
  export function formatModelOnboardingInlineHint(): string {
17
- return `Add API-compatible providers with ${MODEL_ONBOARDING_API_PROVIDER_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND}); OAuth/subscription with ${MODEL_ONBOARDING_OAUTH_COMMAND}; then run /model.`;
19
+ return `Add API-compatible providers with ${MODEL_ONBOARDING_API_PROVIDER_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND}); OAuth/subscription with ${MODEL_ONBOARDING_OAUTH_COMMAND}; then run /model for DEFAULT, EXECUTOR, ARCHITECT, PLANNER, and CRITIC.`;
18
20
  }
19
21
 
20
22
  export function formatNoModelOnboardingError(): string {
@@ -27,7 +29,7 @@ export function formatNoCredentialOnboardingError(providerId: string): string {
27
29
  "",
28
30
  `For API-compatible providers, configure credentials with ${MODEL_ONBOARDING_API_PROVIDER_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND}).`,
29
31
  `For OAuth/subscription providers, use ${MODEL_ONBOARDING_OAUTH_COMMAND}.`,
30
- "Then run /model to select a configured model.",
32
+ "Then run /model to select a configured model or assign it to DEFAULT, EXECUTOR, ARCHITECT, PLANNER, or CRITIC.",
31
33
  ].join("\n");
32
34
  }
33
35