@gajae-code/coding-agent 0.7.8 → 0.7.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +6 -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 +2 -2
- package/dist/types/gjc-runtime/launch-tmux.d.ts +12 -3
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +3 -1
- package/dist/types/modes/components/hook-input.d.ts +1 -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 +21 -1
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/notifications/daemon-paths.d.ts +9 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +8 -2
- package/dist/types/notifications/telegram-daemon.d.ts +1 -10
- package/dist/types/tools/ask.d.ts +24 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
- package/package.json +7 -7
- package/src/cli/notify-cli.ts +1 -1
- 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 +3 -2
- 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 +14 -6
- package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -7
- package/src/gjc-runtime/deep-interview-runtime.ts +1 -1
- package/src/gjc-runtime/launch-tmux.ts +303 -46
- package/src/gjc-runtime/team-runtime.ts +69 -4
- package/src/gjc-runtime/ultragoal-guard.ts +11 -5
- package/src/internal-urls/docs-index.generated.ts +6 -6
- 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/model-selector.ts +84 -13
- package/src/modes/components/settings-selector.ts +36 -21
- 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 +2 -0
- 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 +11 -4
- package/src/modes/types.ts +6 -1
- package/src/notifications/daemon-paths.ts +22 -0
- package/src/notifications/telegram-daemon-cli.ts +13 -4
- package/src/notifications/telegram-daemon.ts +2 -21
- package/src/prompts/system/system-prompt.md +5 -0
- package/src/sdk.ts +10 -1
- package/src/session/agent-session.ts +10 -1
- package/src/tools/ask.ts +11 -1
- package/src/tools/eval.ts +2 -2
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/ultragoal-ask-guard.ts +39 -5
|
@@ -5,6 +5,7 @@ import { formatClampedModelSelector } from "../thinking";
|
|
|
5
5
|
import {
|
|
6
6
|
aggregateModelProfileRequiredProviders,
|
|
7
7
|
formatAvailableProfileNames,
|
|
8
|
+
formatModelProfileDisplayLabel,
|
|
8
9
|
resolveProfileBindings,
|
|
9
10
|
} from "./model-profiles";
|
|
10
11
|
import {
|
|
@@ -46,7 +47,7 @@ export interface PrepareModelProfileActivationOptions {
|
|
|
46
47
|
export interface PreparedModelProfileActivation {
|
|
47
48
|
profileName: string;
|
|
48
49
|
session: ModelProfileActivationSession & { setModelTemporary: AgentSession["setModelTemporary"] };
|
|
49
|
-
settings: Pick<Settings, "get" | "override" | "set" | "flush">;
|
|
50
|
+
settings: Pick<Settings, "clearOverride" | "get" | "getGlobal" | "override" | "set" | "flush">;
|
|
50
51
|
previousModel: Model<Api> | undefined;
|
|
51
52
|
previousThinkingLevel: ThinkingLevel | undefined;
|
|
52
53
|
previousAgentModelOverrides: Record<string, string>;
|
|
@@ -108,8 +109,8 @@ export function materializeActiveModelProfileAssignment(options: MaterializeMode
|
|
|
108
109
|
return true;
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
export function formatModelProfileCredentialError(
|
|
112
|
-
return `Model profile "${
|
|
112
|
+
export function formatModelProfileCredentialError(profileLabel: string, providers: readonly string[]): string {
|
|
113
|
+
return `Model profile "${profileLabel}" requires credentials for: ${providers.join(", ")}. Run /login and configure the missing provider(s), then retry.`;
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
function resolveModelProfileName(profileName: string, profiles: ReadonlyMap<string, unknown>): string {
|
|
@@ -183,6 +184,7 @@ export async function prepareModelProfileActivation(
|
|
|
183
184
|
const available = formatAvailableProfileNames(profiles);
|
|
184
185
|
throw new Error(`Unknown model profile "${options.profileName}". Available profiles: ${available}`);
|
|
185
186
|
}
|
|
187
|
+
const profileLabel = formatModelProfileDisplayLabel(profile);
|
|
186
188
|
|
|
187
189
|
const allProviders = aggregateModelProfileRequiredProviders(profile.requiredProviders, profile);
|
|
188
190
|
const alternativeGroups = profile.alternativeProviderGroups ?? [];
|
|
@@ -202,19 +204,19 @@ export async function prepareModelProfileActivation(
|
|
|
202
204
|
// Check strict (non-alternative) providers — all must be authenticated.
|
|
203
205
|
const strictMissing = missingProviders.filter(p => !alternativeSet.has(p));
|
|
204
206
|
if (strictMissing.length > 0) {
|
|
205
|
-
throw new Error(formatModelProfileCredentialError(
|
|
207
|
+
throw new Error(formatModelProfileCredentialError(profileLabel, strictMissing));
|
|
206
208
|
}
|
|
207
209
|
|
|
208
210
|
// Check alternative groups — at least one provider per group must be authenticated.
|
|
209
211
|
for (const group of alternativeGroups) {
|
|
210
212
|
const groupAuthenticated = group.some(p => authenticatedProviders.includes(p));
|
|
211
213
|
if (!groupAuthenticated) {
|
|
212
|
-
throw new Error(formatModelProfileCredentialError(
|
|
214
|
+
throw new Error(formatModelProfileCredentialError(profileLabel, [...group]));
|
|
213
215
|
}
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
if (authenticatedProviders.length === 0) {
|
|
217
|
-
throw new Error(formatModelProfileCredentialError(
|
|
219
|
+
throw new Error(formatModelProfileCredentialError(profileLabel, missingProviders));
|
|
218
220
|
}
|
|
219
221
|
|
|
220
222
|
const availableModels = options.modelRegistry.getAll();
|
|
@@ -229,9 +231,7 @@ export async function prepareModelProfileActivation(
|
|
|
229
231
|
})
|
|
230
232
|
: undefined;
|
|
231
233
|
if (bindings.defaultSelector && !resolvedDefault?.model) {
|
|
232
|
-
throw new Error(
|
|
233
|
-
`Model profile "${options.profileName}" default selector did not resolve: ${bindings.defaultSelector}`,
|
|
234
|
-
);
|
|
234
|
+
throw new Error(`Model profile "${profileLabel}" default selector did not resolve: ${bindings.defaultSelector}`);
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
const modelRoles: Record<string, string> = {};
|
|
@@ -241,7 +241,7 @@ export async function prepareModelProfileActivation(
|
|
|
241
241
|
modelRegistry: options.modelRegistry,
|
|
242
242
|
});
|
|
243
243
|
if (!resolved.model) {
|
|
244
|
-
throw new Error(`Model profile "${
|
|
244
|
+
throw new Error(`Model profile "${profileLabel}" ${role} selector did not resolve: ${selector}`);
|
|
245
245
|
}
|
|
246
246
|
modelRoles[role] = formatClampedModelSelector(selector, resolved.model);
|
|
247
247
|
}
|
|
@@ -256,7 +256,7 @@ export async function prepareModelProfileActivation(
|
|
|
256
256
|
modelRegistry: options.modelRegistry,
|
|
257
257
|
});
|
|
258
258
|
if (!resolved.model) {
|
|
259
|
-
throw new Error(`Model profile "${
|
|
259
|
+
throw new Error(`Model profile "${profileLabel}" ${role} selector did not resolve: ${selector}`);
|
|
260
260
|
}
|
|
261
261
|
agentModelOverrides[role] = formatClampedModelSelector(selector, resolved.model);
|
|
262
262
|
}
|
|
@@ -357,6 +357,87 @@ export async function applyPreparedModelProfileActivation(
|
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
+
export interface MaterializeModelProfileForDeletionResult {
|
|
361
|
+
modelRoles: Record<string, string>;
|
|
362
|
+
agentModelOverrides: Record<string, string>;
|
|
363
|
+
previousModelRoles: Record<string, string>;
|
|
364
|
+
previousAgentModelOverrides: Record<string, string>;
|
|
365
|
+
previousDefaultProfile: string | undefined;
|
|
366
|
+
previousPersistedDefaultProfile: string | undefined;
|
|
367
|
+
previousActiveModelProfile: string | undefined;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export async function materializeModelProfileForDeletion(
|
|
371
|
+
options: PrepareModelProfileActivationOptions & {
|
|
372
|
+
settings: Pick<Settings, "clearOverride" | "flush" | "get" | "getGlobal" | "override" | "set">;
|
|
373
|
+
},
|
|
374
|
+
): Promise<MaterializeModelProfileForDeletionResult> {
|
|
375
|
+
const prepared = await prepareModelProfileActivation(options);
|
|
376
|
+
const previousDefaultProfile = prepared.settings.get("modelProfile.default");
|
|
377
|
+
const previousPersistedDefaultProfile = prepared.settings.getGlobal("modelProfile.default");
|
|
378
|
+
const nextModelRoles = {
|
|
379
|
+
...prepared.previousModelRoles,
|
|
380
|
+
...(prepared.defaultModel
|
|
381
|
+
? {
|
|
382
|
+
default: formatModelSelectorValue(
|
|
383
|
+
`${prepared.defaultModel.provider}/${prepared.defaultModel.id}`,
|
|
384
|
+
prepared.defaultThinkingLevel,
|
|
385
|
+
),
|
|
386
|
+
}
|
|
387
|
+
: {}),
|
|
388
|
+
...prepared.modelRoles,
|
|
389
|
+
};
|
|
390
|
+
const nextAgentModelOverrides = {
|
|
391
|
+
...prepared.previousAgentModelOverrides,
|
|
392
|
+
...prepared.agentModelOverrides,
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
prepared.settings.set("modelRoles", nextModelRoles);
|
|
397
|
+
prepared.settings.set("task.agentModelOverrides", nextAgentModelOverrides);
|
|
398
|
+
prepared.settings.set("modelProfile.default", undefined);
|
|
399
|
+
prepared.settings.clearOverride("modelProfile.default");
|
|
400
|
+
prepared.settings.override("modelRoles", nextModelRoles);
|
|
401
|
+
prepared.settings.override("task.agentModelOverrides", nextAgentModelOverrides);
|
|
402
|
+
prepared.session.setActiveModelProfile?.(undefined);
|
|
403
|
+
await prepared.settings.flush();
|
|
404
|
+
} catch (error) {
|
|
405
|
+
prepared.settings.set("modelRoles", prepared.previousModelRoles);
|
|
406
|
+
prepared.settings.set("task.agentModelOverrides", prepared.previousAgentModelOverrides);
|
|
407
|
+
prepared.settings.set("modelProfile.default", previousPersistedDefaultProfile);
|
|
408
|
+
prepared.settings.override("modelRoles", prepared.previousModelRoles);
|
|
409
|
+
prepared.settings.override("task.agentModelOverrides", prepared.previousAgentModelOverrides);
|
|
410
|
+
prepared.settings.override("modelProfile.default", previousDefaultProfile);
|
|
411
|
+
prepared.session.setActiveModelProfile?.(prepared.previousActiveModelProfile);
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
modelRoles: nextModelRoles,
|
|
417
|
+
agentModelOverrides: nextAgentModelOverrides,
|
|
418
|
+
previousModelRoles: prepared.previousModelRoles,
|
|
419
|
+
previousAgentModelOverrides: prepared.previousAgentModelOverrides,
|
|
420
|
+
previousDefaultProfile,
|
|
421
|
+
previousPersistedDefaultProfile,
|
|
422
|
+
previousActiveModelProfile: prepared.previousActiveModelProfile,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export async function restoreMaterializedModelProfileForDeletion(options: {
|
|
427
|
+
settings: Pick<Settings, "flush" | "override" | "set">;
|
|
428
|
+
session: Pick<ModelProfileActivationSession, "setActiveModelProfile">;
|
|
429
|
+
snapshot: MaterializeModelProfileForDeletionResult;
|
|
430
|
+
}): Promise<void> {
|
|
431
|
+
options.settings.set("modelRoles", options.snapshot.previousModelRoles);
|
|
432
|
+
options.settings.set("task.agentModelOverrides", options.snapshot.previousAgentModelOverrides);
|
|
433
|
+
options.settings.set("modelProfile.default", options.snapshot.previousPersistedDefaultProfile);
|
|
434
|
+
options.settings.override("modelRoles", options.snapshot.previousModelRoles);
|
|
435
|
+
options.settings.override("task.agentModelOverrides", options.snapshot.previousAgentModelOverrides);
|
|
436
|
+
options.settings.override("modelProfile.default", options.snapshot.previousDefaultProfile);
|
|
437
|
+
options.session.setActiveModelProfile?.(options.snapshot.previousActiveModelProfile);
|
|
438
|
+
await options.settings.flush();
|
|
439
|
+
}
|
|
440
|
+
|
|
360
441
|
export async function activateModelProfile(
|
|
361
442
|
options: PrepareModelProfileActivationOptions,
|
|
362
443
|
applyOptions: { persistDefault?: boolean } = {},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sanitizeText } from "@gajae-code/utils";
|
|
1
2
|
import type { GjcModelAssignmentTargetId } from "./model-registry";
|
|
2
3
|
import type { ModelsConfig } from "./models-config-schema";
|
|
3
4
|
|
|
@@ -98,7 +99,7 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
98
99
|
}),
|
|
99
100
|
profile("claude-opus", ["anthropic"], {
|
|
100
101
|
default: "anthropic/claude-opus-4-8:xhigh",
|
|
101
|
-
executor: "anthropic/claude-sonnet-
|
|
102
|
+
executor: "anthropic/claude-sonnet-5",
|
|
102
103
|
planner: "anthropic/claude-opus-4-8:low",
|
|
103
104
|
critic: "anthropic/claude-opus-4-8:high",
|
|
104
105
|
architect: "anthropic/claude-opus-4-8:xhigh",
|
|
@@ -267,6 +268,10 @@ export interface ModelProfilePresentation {
|
|
|
267
268
|
providerGroup: string;
|
|
268
269
|
}
|
|
269
270
|
|
|
271
|
+
function sanitizeModelProfileLabel(value: string): string {
|
|
272
|
+
return sanitizeText(value).replace(/\s+/g, " ").trim();
|
|
273
|
+
}
|
|
274
|
+
|
|
270
275
|
const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
|
|
271
276
|
"codex-eco": { displayName: "Codex Eco", providerGroup: "CODEX" },
|
|
272
277
|
"codex-medium": { displayName: "Codex Medium", providerGroup: "CODEX" },
|
|
@@ -332,7 +337,15 @@ export function getModelProfilePresentation(
|
|
|
332
337
|
const displayName = typeof profile === "string" ? undefined : profile.displayName;
|
|
333
338
|
const presentation = PROFILE_PRESENTATION[name];
|
|
334
339
|
if (presentation) return presentation;
|
|
335
|
-
return { displayName:
|
|
340
|
+
return { displayName: formatModelProfileDisplayLabel({ name, displayName }), providerGroup: "CUSTOM" };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function formatModelProfileDisplayLabel(profile: Pick<ModelProfileDefinition, "name" | "displayName">): string {
|
|
344
|
+
return (
|
|
345
|
+
sanitizeModelProfileLabel(profile.displayName ?? profile.name) ||
|
|
346
|
+
sanitizeModelProfileLabel(profile.name) ||
|
|
347
|
+
"Unnamed profile"
|
|
348
|
+
);
|
|
336
349
|
}
|
|
337
350
|
|
|
338
351
|
export function groupModelProfilesForPresetLanding(
|
|
@@ -83,7 +83,8 @@ export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
|
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
export const MODEL_ROLE_IDS: ModelRole[] = ["default"];
|
|
86
|
-
|
|
86
|
+
export const MODEL_PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
87
|
+
export const MODEL_PROFILE_NAME_PATTERN_DESCRIPTION = "lowercase letters, numbers, dots, underscores, or hyphens";
|
|
87
88
|
export type GjcModelAssignmentTargetId = "default" | "executor" | "architect" | "planner" | "critic";
|
|
88
89
|
|
|
89
90
|
export interface GjcModelAssignmentTargetInfo extends ModelRoleInfo {
|
|
@@ -1590,10 +1591,7 @@ export class ModelRegistry {
|
|
|
1590
1591
|
const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
|
|
1591
1592
|
throw new Error(`Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`);
|
|
1592
1593
|
}
|
|
1593
|
-
await
|
|
1594
|
-
await Bun.write(this.#modelsConfigFile.path(), Bun.YAML.stringify(checkedConfig.data, null, 2));
|
|
1595
|
-
this.#modelsConfigFile.invalidate();
|
|
1596
|
-
this.#reloadStaticModels();
|
|
1594
|
+
await this.#writeCheckedModelsConfig(checkedConfig.data);
|
|
1597
1595
|
const modelMapping = { ...definition.model_mapping };
|
|
1598
1596
|
const profile: ModelProfileDefinition = {
|
|
1599
1597
|
name: normalizedName,
|
|
@@ -1604,6 +1602,84 @@ export class ModelRegistry {
|
|
|
1604
1602
|
};
|
|
1605
1603
|
return profile;
|
|
1606
1604
|
}
|
|
1605
|
+
|
|
1606
|
+
async renameCustomModelProfile(name: string, displayName: string): Promise<ModelProfileDefinition> {
|
|
1607
|
+
const normalizedName = name.trim();
|
|
1608
|
+
const nextDisplayName = displayName.trim();
|
|
1609
|
+
if (!normalizedName) throw new Error("Profile name is required.");
|
|
1610
|
+
if (!nextDisplayName) throw new Error("Profile display name is required.");
|
|
1611
|
+
const { current, profile } = this.#loadCustomProfileForMutation(normalizedName, "rename");
|
|
1612
|
+
const nextProfiles = {
|
|
1613
|
+
...(current.profiles ?? {}),
|
|
1614
|
+
[normalizedName]: {
|
|
1615
|
+
...profile,
|
|
1616
|
+
display_name: nextDisplayName,
|
|
1617
|
+
},
|
|
1618
|
+
};
|
|
1619
|
+
const checkedConfig = ModelsConfigSchema.safeParse({ ...current, profiles: nextProfiles });
|
|
1620
|
+
if (!checkedConfig.success) {
|
|
1621
|
+
const first = checkedConfig.error.issues[0];
|
|
1622
|
+
const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
|
|
1623
|
+
throw new Error(`Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`);
|
|
1624
|
+
}
|
|
1625
|
+
await this.#writeCheckedModelsConfig(checkedConfig.data);
|
|
1626
|
+
const modelMapping = { ...profile.model_mapping };
|
|
1627
|
+
return {
|
|
1628
|
+
name: normalizedName,
|
|
1629
|
+
displayName: nextDisplayName,
|
|
1630
|
+
requiredProviders: aggregateModelProfileRequiredProviders(profile.required_providers, { modelMapping }),
|
|
1631
|
+
modelMapping,
|
|
1632
|
+
source: "user",
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
async deleteCustomModelProfile(name: string): Promise<ModelProfileConfig> {
|
|
1637
|
+
const normalizedName = name.trim();
|
|
1638
|
+
if (!normalizedName) throw new Error("Profile name is required.");
|
|
1639
|
+
const { current, profile } = this.#loadCustomProfileForMutation(normalizedName, "delete");
|
|
1640
|
+
const nextProfiles = { ...(current.profiles ?? {}) };
|
|
1641
|
+
delete nextProfiles[normalizedName];
|
|
1642
|
+
const checkedConfig = ModelsConfigSchema.safeParse({
|
|
1643
|
+
...current,
|
|
1644
|
+
profiles: Object.keys(nextProfiles).length > 0 ? nextProfiles : undefined,
|
|
1645
|
+
});
|
|
1646
|
+
if (!checkedConfig.success) {
|
|
1647
|
+
const first = checkedConfig.error.issues[0];
|
|
1648
|
+
const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
|
|
1649
|
+
throw new Error(`Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`);
|
|
1650
|
+
}
|
|
1651
|
+
await this.#writeCheckedModelsConfig(checkedConfig.data);
|
|
1652
|
+
return profile;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
#loadCustomProfileForMutation(
|
|
1656
|
+
normalizedName: string,
|
|
1657
|
+
action: "rename" | "delete",
|
|
1658
|
+
): { current: ModelsConfig; profile: ModelProfileConfig } {
|
|
1659
|
+
const loaded = this.#modelsConfigFile.tryLoad();
|
|
1660
|
+
if (loaded.status === "error") {
|
|
1661
|
+
throw new Error(
|
|
1662
|
+
`Cannot ${action} custom model profile because ${this.#modelsConfigFile.path()} is invalid. Fix the existing config before modifying presets.`,
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
const current = loaded.value ?? this.#modelsConfigFile.createDefault();
|
|
1666
|
+
const profile = current.profiles?.[normalizedName];
|
|
1667
|
+
if (!profile) {
|
|
1668
|
+
const existing = this.#modelProfiles.get(normalizedName);
|
|
1669
|
+
if (existing && existing.source !== "user") {
|
|
1670
|
+
throw new Error(`Cannot ${action} bundled model profile: ${normalizedName}.`);
|
|
1671
|
+
}
|
|
1672
|
+
throw new Error(`Custom model profile does not exist: ${normalizedName}.`);
|
|
1673
|
+
}
|
|
1674
|
+
return { current, profile };
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
async #writeCheckedModelsConfig(config: ModelsConfig): Promise<void> {
|
|
1678
|
+
await fs.mkdir(path.dirname(this.#modelsConfigFile.path()), { recursive: true });
|
|
1679
|
+
await Bun.write(this.#modelsConfigFile.path(), Bun.YAML.stringify(config, null, 2));
|
|
1680
|
+
this.#modelsConfigFile.invalidate();
|
|
1681
|
+
this.#reloadStaticModels();
|
|
1682
|
+
}
|
|
1607
1683
|
applyConfiguredModelBindings(targetSettings: Settings): void {
|
|
1608
1684
|
this.#modelBindingsTargetSettings = targetSettings;
|
|
1609
1685
|
this.#applyConfiguredModelBindingsToTarget();
|
|
@@ -280,7 +280,8 @@ export const SETTINGS_SCHEMA = {
|
|
|
280
280
|
ui: {
|
|
281
281
|
tab: "interaction",
|
|
282
282
|
label: "Terminal Bell",
|
|
283
|
-
description:
|
|
283
|
+
description:
|
|
284
|
+
"Emit a BEL character for local terminal notifications. Windows Terminal may keep BEL silent depending on profile/system sound settings; use completion.notifyCommand for a PowerShell Console.Beep workaround.",
|
|
284
285
|
},
|
|
285
286
|
},
|
|
286
287
|
"notifications.bellOnComplete": {
|
|
@@ -1179,7 +1180,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
1179
1180
|
tab: "interaction",
|
|
1180
1181
|
label: "Completion Notification Command",
|
|
1181
1182
|
description:
|
|
1182
|
-
"Optional user-level shell command to run when an agent turn completes; receives GJC_NOTIFICATION_* environment variables.",
|
|
1183
|
+
"Optional user-level shell command to run when an agent turn completes; receives GJC_NOTIFICATION_* environment variables. On Windows, this can call PowerShell [Console]::Beep when terminal BEL is silent.",
|
|
1183
1184
|
},
|
|
1184
1185
|
},
|
|
1185
1186
|
|
|
@@ -1273,8 +1273,10 @@ async function sendTmuxPromptKeys(
|
|
|
1273
1273
|
prompt: string,
|
|
1274
1274
|
runner: CommandRunner = runCommand,
|
|
1275
1275
|
): Promise<boolean> {
|
|
1276
|
-
const
|
|
1277
|
-
|
|
1276
|
+
const typed = await runner(["tmux", "send-keys", "-t", target, "-l", prompt]);
|
|
1277
|
+
if (typed.exitCode !== 0) return false;
|
|
1278
|
+
const submitted = await runner(["tmux", "send-keys", "-t", target, "-l", "\x1b[13;5u"]);
|
|
1279
|
+
return submitted.exitCode === 0;
|
|
1278
1280
|
}
|
|
1279
1281
|
|
|
1280
1282
|
function boundedLineCount(value: unknown): number {
|
|
@@ -49,6 +49,7 @@ interface ThresholdModel {
|
|
|
49
49
|
type DeepInterviewModel = RoundQuestionModel | TopologyQuestionModel | ProgressModel | ThresholdModel;
|
|
50
50
|
|
|
51
51
|
function normalizeText(text: string): string {
|
|
52
|
+
if (typeof text !== "string") return "";
|
|
52
53
|
return text.trim().replaceAll("\r\n", "\n");
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -51,6 +51,10 @@ Inspired by the [Ouroboros project](https://github.com/Q00/ouroboros) which demo
|
|
|
51
51
|
- Keep prompt payloads budgeted: summarize or trim oversized initial context/history before composing question, scoring, spec, or handoff prompts
|
|
52
52
|
- If the user's initial context is oversized, create a concise prompt-safe summary first and wait for that summary before ambiguity scoring, question generation, or downstream execution handoff
|
|
53
53
|
- Do not proceed to execution until ambiguity ≤ the resolved threshold for this run and the user explicitly approves a scoped execution path
|
|
54
|
+
- Treat user wording such as `implementation`, "implementation plan", Korean `구현`, or "구현 계획" as describing the eventual target, not permission to implement now.
|
|
55
|
+
- While still in deep-interview, do not implement, edit/write code, launch implementation workers, or start task/skill/ultragoal implementation; continue interviewing for scope, risks, acceptance criteria, and unknowns.
|
|
56
|
+
- When the user wants interview output for eventual implementation, say: "I can interview for an implementation plan, but I won't implement during deep-interview." Then continue clarifying scope, risks, acceptance criteria, and unknowns.
|
|
57
|
+
- Implementation requires an explicit phase transition/approval after the interview: deep-interview must first produce its spec/handoff, the workflow phase must explicitly transition out of deep-interview, and execution approval must be captured by a downstream execution path.
|
|
54
58
|
- Allow early exit with a clear warning if ambiguity is still high
|
|
55
59
|
- Persist interview state for resume across session interruptions
|
|
56
60
|
- A multi-persona lateral-review panel convenes at ambiguity-milestone transitions (and before synthesizing any agent-supplied answer) to expose blind spots from independent perspectives
|
|
@@ -74,7 +78,7 @@ Inspired by the [Ouroboros project](https://github.com/Q00/ouroboros) which demo
|
|
|
74
78
|
|
|
75
79
|
## Native Plugin Invocation Guard (Issue #3030)
|
|
76
80
|
|
|
77
|
-
If this raw bundled skill is loaded by GJC's native skill loader through `/skill:deep-interview`, do not treat that path as permission to skip rendered GJC setup. The user-facing invocation is `/skill:deep-interview`; do not recommend or advertise CLI bridge commands as the deep-interview entrypoint. Regardless of invocation path, Phase 0 below remains blocking and must resolve `gjc.deepInterview.ambiguityThreshold` from settings before any announcement, state write, question, or ambiguity score.
|
|
81
|
+
If this raw bundled skill is loaded by GJC's native skill loader through `/skill:deep-interview`, do not treat that path as permission to skip rendered GJC setup. The user-facing invocation is `/skill:deep-interview`; do not recommend or advertise CLI bridge commands as the deep-interview entrypoint. Regardless of invocation path, Phase 0 below remains blocking and must resolve `gjc.deepInterview.ambiguityThreshold` from pre-resolved native state or settings before any announcement, state write, question, or ambiguity score.
|
|
78
82
|
|
|
79
83
|
## Corrupt current-session state recovery
|
|
80
84
|
|
|
@@ -84,20 +88,24 @@ When deep-interview detects its own current-session state is corrupt, tampered,
|
|
|
84
88
|
|
|
85
89
|
Complete this phase before Phase 1, before brownfield exploration, before GJC state persistence, before Round 0, and before any ambiguity scoring. Do not continue if the resolved threshold and source are unknown.
|
|
86
90
|
|
|
87
|
-
1. **
|
|
91
|
+
1. **Prefer pre-resolved native state**:
|
|
92
|
+
- First inspect active deep-interview state with `gjc state deep-interview read --json`.
|
|
93
|
+
- If state contains a finite numeric `threshold` and a non-empty `threshold_source`, use those values, set `<resolvedThreshold>`, `<resolvedThresholdPercent>`, and `<resolvedThresholdSource>`, and skip optional settings-file reads. This is the normal `/skill:deep-interview` path because the native hook already resolved settings quietly before loading the skill.
|
|
94
|
+
2. **Only if native state lacks a resolved threshold, read threshold settings in precedence order**:
|
|
88
95
|
- User settings: `[$GJC_CONFIG_DIR|~/.gjc]/settings.json`
|
|
89
96
|
- Project settings: `./.gjc/settings.json` (overrides user settings)
|
|
90
|
-
|
|
91
|
-
-
|
|
97
|
+
- Read `gjc.deepInterview.ambiguityThreshold` only from files that are known to exist; optional settings-file absence is expected and must not be surfaced as failed `Read` calls.
|
|
98
|
+
- Do not probe arbitrary ancestor candidates such as `../../.gjc/settings.json`; use the current project `.gjc/settings.json` and user settings only.
|
|
99
|
+
3. **Resolve threshold and source**:
|
|
92
100
|
- Use the project value when valid; otherwise use the user value when valid; otherwise use the default `0.05`.
|
|
93
101
|
- Set these run variables exactly: `<resolvedThreshold>`, `<resolvedThresholdPercent>`, and `<resolvedThresholdSource>` (for example `./.gjc/settings.json`, `[$GJC_CONFIG_DIR|~/.gjc]/settings.json`, or `default`).
|
|
94
|
-
|
|
102
|
+
4. **Emit the required first line to the user before any other interview announcement**:
|
|
95
103
|
|
|
96
104
|
```
|
|
97
105
|
Deep Interview threshold: <resolvedThresholdPercent> (source: <resolvedThresholdSource>)
|
|
98
106
|
```
|
|
99
107
|
|
|
100
|
-
|
|
108
|
+
5. **Carry threshold source forward mechanically**:
|
|
101
109
|
- Substitute `<resolvedThreshold>`, `<resolvedThresholdPercent>`, and `<resolvedThresholdSource>` throughout the remaining instructions before continuing.
|
|
102
110
|
- Include `threshold_source` in the first `gjc state write` payload and preserve it on later state updates; do not edit `.gjc/_session-{sessionid}/state` files directly unless an explicit force override is active.
|
|
103
111
|
- Include both threshold and source in the final spec metadata.
|
|
@@ -19,7 +19,7 @@ Ralplan is the consensus planning workflow. It triggers iterative planning with
|
|
|
19
19
|
|
|
20
20
|
## Flags
|
|
21
21
|
|
|
22
|
-
- `--interactive`: Enables user prompts
|
|
22
|
+
- `--interactive`: Enables extra mid-loop user prompts (draft review in step 2 and one-at-a-time reconciliation in step 6c). Regardless of this flag, the workflow always finishes the post-interview gate with an `ask`-tool prompt offering Refine further / Approve ultragoal / Approve team / Stop here, and never auto-executes — execution always requires explicit approval through that prompt.
|
|
23
23
|
- `--deliberate`: Forces deliberate mode for high-risk work. Adds pre-mortem (3 scenarios) and expanded test planning (unit/integration/e2e/observability). Without this flag, deliberate mode can still auto-enable when the request explicitly signals high risk (auth/security, migrations, destructive changes, production incidents, compliance/PII, public API breakage).
|
|
24
24
|
- `--architect openai-code`: Use OpenAI code for the Architect pass when OpenAI code CLI is available. Otherwise, briefly note the fallback and keep the default GJC Architect review.
|
|
25
25
|
- `--critic openai-code`: Use OpenAI code for the Critic pass when OpenAI code CLI is available. Otherwise, briefly note the fallback and keep the default GJC Critic review.
|
|
@@ -83,13 +83,20 @@ The consensus workflow:
|
|
|
83
83
|
6. **Post-ralplan interview** (intent reconciliation gate): After Critic returns `APPROVE` and before the plan is finalized, reconcile the consensus plan against the user's actual intent. The goal is to make sure ralplan did not silently bake in assumptions that conflict with what the user wants.
|
|
84
84
|
a. **Collect open items** from the run: every assumption the Planner/Architect/Critic resolved by assumption rather than by stated fact, every ambiguity flagged during review, and every decision the loop made without explicit user input. Source these from the persisted `planner`/`architect`/`critic`/`revision` stage artifacts, not from memory.
|
|
85
85
|
b. **Cross-check prior context for conflicts**: glob `.gjc/_session-{sessionid}/specs/deep-interview-*.md` and other prior specs/plans/context relevant by topic. For each, list points where the consensus plan contradicts, weakens, or expands beyond a previously crystallized decision, constraint, or non-goal. Cite the conflicting artifact and line/section.
|
|
86
|
-
c. **Reconcile with the user**:
|
|
87
|
-
-
|
|
88
|
-
-
|
|
86
|
+
c. **Reconcile with the user via the `ask` tool (always, regardless of `--interactive`)**: Never stop idle with plain-text prose after the consensus loop. Every reconciliation question MUST go through the `ask` tool with contextual options plus free-text.
|
|
87
|
+
- If open items exist, confirm the open assumptions and conflicts **one at a time** with the `ask` tool, weakest/highest-impact first, polishing intent. If any confirmation reveals that the plan diverges from user intent, route the consolidated correction back into the re-review loop (step 5b Planner revision) and re-run Architect + Critic before returning here. Cap at the same 5-iteration ceiling.
|
|
88
|
+
- If the plan is crystal clear (no open assumptions or prior-context conflicts), skip straight to the step 8 final-options `ask` instead of inventing filler questions.
|
|
89
|
+
- For every confirmed open item, embed the resolved outcome into the final plan under an **## Intent Reconciliation** section so the `pending approval` artifact records each decision; record any item the user explicitly defers as an open confirmation under that same section.
|
|
89
90
|
d. Persist the reconciliation with `gjc ralplan --write --stage post-interview --stage_n <N> --artifact "..." --json`, then return the receipt/path plus a compact status (reconciled-clean / reconciled-with-revision / open-confirmations-pending) instead of pasting the full body.
|
|
90
|
-
7. On reconciliation completion, mark the plan `pending approval` unless explicit execution approval has already been captured, persist the ADR/final plan via `gjc ralplan --write --stage final --stage_n <N> --artifact "..."`, and do not directly edit `.gjc/_session-{sessionid}/plans`.
|
|
91
|
-
8.
|
|
92
|
-
|
|
91
|
+
7. On reconciliation completion, mark the plan `pending approval` unless explicit execution approval has already been captured, persist the ADR/final plan via `gjc ralplan --write --stage final --stage_n <N> --artifact "..."`, and do not directly edit `.gjc/_session-{sessionid}/plans`. Final plan must include ADR (Decision, Drivers, Alternatives considered, Why chosen, Consequences, Follow-ups) and, when present, the **## Intent Reconciliation** section.
|
|
92
|
+
8. **Always** present the finalized plan via the `ask` tool (regardless of `--interactive`) with `workflowGate: { stage: "ralplan", kind: "approval" }` on the final question so RPC/headless clients receive a `ralplan`/`approval` workflow gate, not a deep-interview question gate. Use these options:
|
|
93
|
+
- **Refine further** — re-run the consensus loop / request changes, then return here
|
|
94
|
+
- **Approve execution via ultragoal (Recommended)** — goal-tracked autonomous execution
|
|
95
|
+
- **Approve execution via team** — only when tmux-based interactive worker parallelization is required
|
|
96
|
+
- **Stop here** — keep the plan as `pending approval` and make no further changes
|
|
97
|
+
|
|
98
|
+
Always include a free-text option. Do not stop with plain text and no `ask`; the post-interview gate's terminal action is this `ask`.
|
|
99
|
+
9. On approval: invoke `/skill:ultragoal` for execution by default; invoke `/skill:team` only when the user explicitly needs tmux-based interactive worker parallelization. On **Refine further**, return to the step 5 re-review loop. On **Stop here**, leave the `pending approval` artifact and stop. Never implement directly.
|
|
93
100
|
|
|
94
101
|
Before invoking `/skill:team` or `/skill:ultragoal`, mark ralplan ready for handoff so the skill tool's chain guard permits the transition:
|
|
95
102
|
|
|
@@ -100,7 +100,7 @@ async function resolveSpecContent(rawSpec: string, cwd: string): Promise<string>
|
|
|
100
100
|
if (stat.isFile()) return await fs.readFile(candidate, "utf-8");
|
|
101
101
|
} catch (error) {
|
|
102
102
|
const err = error as NodeJS.ErrnoException;
|
|
103
|
-
if (err.code !== "ENOENT" && err.code !== "ENOTDIR") {
|
|
103
|
+
if (err.code !== "ENOENT" && err.code !== "ENOTDIR" && err.code !== "ENAMETOOLONG") {
|
|
104
104
|
throw new DeepInterviewCommandError(2, `failed to read --spec ${candidate}: ${err.message}`);
|
|
105
105
|
}
|
|
106
106
|
}
|