jeo-code 0.7.30 → 0.7.32
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 +46 -0
- package/README.ja.md +5 -3
- package/README.ko.md +5 -3
- package/README.md +5 -3
- package/README.zh.md +5 -3
- package/package.json +3 -3
- package/scripts/install.sh +19 -0
- package/scripts/uninstall.sh +2 -2
- package/src/agent/AGENTS.md +12 -0
- package/src/agent/ast-edit-tool.ts +123 -0
- package/src/agent/ast-file-scan.ts +62 -0
- package/src/agent/ast-grep-tool.ts +107 -0
- package/src/agent/ast-match.ts +241 -0
- package/src/agent/compaction.ts +6 -19
- package/src/agent/debug-session.ts +435 -0
- package/src/agent/debug-tool.ts +89 -0
- package/src/agent/engine.ts +103 -18
- package/src/agent/goal-tool.ts +57 -0
- package/src/agent/irc-tool.ts +71 -0
- package/src/agent/job-registry.ts +163 -0
- package/src/agent/job-tool.ts +106 -0
- package/src/agent/lsp-rename-tool.ts +108 -0
- package/src/agent/lsp-tool.ts +177 -0
- package/src/agent/memory-graph.ts +6 -1
- package/src/agent/memory.ts +73 -52
- package/src/agent/session.ts +27 -0
- package/src/agent/subagent-registry.ts +40 -4
- package/src/agent/subagent-tool.ts +27 -7
- package/src/agent/subagents.ts +1 -1
- package/src/agent/task-tool.ts +27 -5
- package/src/agent/tools.ts +100 -5
- package/src/agent/ts-language-service.ts +173 -0
- package/src/ai/model-manager.ts +58 -26
- package/src/ai/sse.ts +22 -7
- package/src/ai/think-tags.ts +10 -1
- package/src/commands/deep-interview.ts +49 -1
- package/src/commands/launch/agents-slash.ts +489 -0
- package/src/commands/launch/flags.ts +27 -9
- package/src/commands/launch/model-slash.ts +290 -0
- package/src/commands/launch/session-slash.ts +305 -0
- package/src/commands/launch/slash-handlers.ts +6 -0
- package/src/commands/launch/system-slash.ts +81 -0
- package/src/commands/launch/tmux.ts +32 -0
- package/src/commands/launch.ts +227 -750
- package/src/tui/app.ts +32 -12
- package/src/tui/components/evolution.ts +23 -3
- package/src/tui/components/width.ts +11 -6
|
@@ -128,6 +128,13 @@ function freezeReadiness(parsed: SocraticResponse | undefined): { ok: boolean; r
|
|
|
128
128
|
return { ok: true };
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/** Normalize a question for repeat detection: case-, whitespace-, and trailing-
|
|
132
|
+
* punctuation-insensitive so the SAME question re-asked with trivial formatting
|
|
133
|
+
* drift still registers as a repeat. */
|
|
134
|
+
function normalizeQuestion(q: string): string {
|
|
135
|
+
return q.toLowerCase().replace(/\s+/g, " ").replace(/[\s?.!…。?!]+$/g, "").trim();
|
|
136
|
+
}
|
|
137
|
+
|
|
131
138
|
function slugify(input: string): string {
|
|
132
139
|
return input
|
|
133
140
|
.toLowerCase()
|
|
@@ -499,7 +506,8 @@ export async function runDeepInterviewEngine(opts: DeepInterviewEngineOptions =
|
|
|
499
506
|
`}\n` +
|
|
500
507
|
`Ensure ambiguityScore drops dynamically as more detail is gathered. Do not report ambiguityScore <= ${threshold} unless acceptance_criteria is populated with concrete, testable checks.\n` +
|
|
501
508
|
`Do not fill an ambiguous point with your own convenient assumption to push the score down — surface that assumption as the nextQuestion instead. Never supply unstated detail that makes the request look simpler or safer than the user actually wrote it.\n` +
|
|
502
|
-
`Conversely, when the user's answers genuinely resolve a dimension, lower the score accordingly — do not keep a dimension ambiguous just to prolong the interview. If you notice yourself quietly narrowing the user's idea to make it easier to score, that narrowing is the signal the dimension is still open: ask, don't assume
|
|
509
|
+
`Conversely, when the user's answers genuinely resolve a dimension, lower the score accordingly — do not keep a dimension ambiguous just to prolong the interview. If you notice yourself quietly narrowing the user's idea to make it easier to score, that narrowing is the signal the dimension is still open: ask, don't assume.\n` +
|
|
510
|
+
`Never re-ask a question that was already asked and answered — every round must target a NEW gap or a different dimension.`
|
|
503
511
|
},
|
|
504
512
|
{
|
|
505
513
|
role: "user",
|
|
@@ -568,6 +576,12 @@ export async function runDeepInterviewEngine(opts: DeepInterviewEngineOptions =
|
|
|
568
576
|
let round = 1;
|
|
569
577
|
let ambiguity = state.current_ambiguity ?? 1.0;
|
|
570
578
|
let lastParsed: SocraticResponse | undefined;
|
|
579
|
+
// Repeat-question loop guard state (deep-interview's descendant of the agent
|
|
580
|
+
// loop's repeat_correct/repeat_stop taxonomy in src/agent/loop-guards.ts): an
|
|
581
|
+
// interviewer that re-asks the SAME question cannot converge by repetition —
|
|
582
|
+
// it just burns rounds and, in interactive mode, the user's patience.
|
|
583
|
+
let lastQuestionNorm = "";
|
|
584
|
+
let questionRepeats = 0;
|
|
571
585
|
|
|
572
586
|
const freezeSeed = async (parsed: SocraticResponse): Promise<boolean> => {
|
|
573
587
|
const readiness = freezeReadiness(parsed);
|
|
@@ -655,6 +669,40 @@ export async function runDeepInterviewEngine(opts: DeepInterviewEngineOptions =
|
|
|
655
669
|
nextQuestion = interviewLanguage.acceptanceFollowup;
|
|
656
670
|
}
|
|
657
671
|
|
|
672
|
+
// Repeat-question guard: first repeat gets ONE corrective bounce (the model
|
|
673
|
+
// is told the question was already answered and steered to a new gap, WITHOUT
|
|
674
|
+
// re-asking the user); a repeat that survives the correction stops this run
|
|
675
|
+
// instead of looping through the remaining rounds on the same question.
|
|
676
|
+
const qNorm = normalizeQuestion(nextQuestion);
|
|
677
|
+
if (qNorm && qNorm === lastQuestionNorm) {
|
|
678
|
+
questionRepeats++;
|
|
679
|
+
} else {
|
|
680
|
+
lastQuestionNorm = qNorm;
|
|
681
|
+
questionRepeats = 0;
|
|
682
|
+
}
|
|
683
|
+
if (questionRepeats === 1) {
|
|
684
|
+
log(`\n[REPEAT] The interviewer re-asked the same question — nudging it toward a new gap instead of re-asking you.`);
|
|
685
|
+
history.push({ role: "assistant", content: responseText });
|
|
686
|
+
history.push({
|
|
687
|
+
role: "user",
|
|
688
|
+
content:
|
|
689
|
+
`You already asked exactly this question and it was answered above — do not ask it again. ` +
|
|
690
|
+
`Ask a DIFFERENT question that targets the weakest unresolved dimension; or, if the prior answers genuinely resolve it, ` +
|
|
691
|
+
`lower ambiguityScore and populate goal/constraints/acceptance_criteria from what was already said. ` +
|
|
692
|
+
`Reply in the same strict JSON format.`,
|
|
693
|
+
});
|
|
694
|
+
round++;
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
if (questionRepeats >= 2) {
|
|
698
|
+
log(
|
|
699
|
+
`\n[LOOP GUARD] The interview is stuck re-asking the same question despite a corrective nudge — ` +
|
|
700
|
+
`stopping this run instead of looping. No progress is possible by repetition; ` +
|
|
701
|
+
`resume with 'jeo deep-interview' (adding more detail to the idea helps) to continue.`,
|
|
702
|
+
);
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
|
|
658
706
|
log(`\nQuestion: ${nextQuestion}`);
|
|
659
707
|
if (opts.signal?.aborted) {
|
|
660
708
|
return { ok: false, reason: "aborted" };
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/agents`, `/roles`, `/fast`, and `/thinking` slash-command handlers
|
|
3
|
+
* extracted from launch.ts.
|
|
4
|
+
*
|
|
5
|
+
* These four commands share mutable REPL state with each other and with the
|
|
6
|
+
* already-extracted `/model` (model-slash.ts): `/agents` and `/roles` read
|
|
7
|
+
* and write `lastPickIndex`; `/fast` and `/thinking` read and write
|
|
8
|
+
* `sessionThinking` (which `/model thinking <level>` also writes). The
|
|
9
|
+
* caller threads the current values in via an explicit context object and
|
|
10
|
+
* reads back a result object instead of these functions closing over REPL
|
|
11
|
+
* state directly.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readGlobalConfig, saveConfigPatch } from "../../agent/state";
|
|
15
|
+
import {
|
|
16
|
+
describeModel,
|
|
17
|
+
describeAllProviders,
|
|
18
|
+
thinkingMaxTokens,
|
|
19
|
+
flattenModels,
|
|
20
|
+
resolveSelection,
|
|
21
|
+
resolveRoleModel,
|
|
22
|
+
catalogByProvider,
|
|
23
|
+
qualifyModelId,
|
|
24
|
+
} from "../../ai";
|
|
25
|
+
import type { ProviderModelsResult, PickEntry, ProviderName, ThinkLevel } from "../../ai";
|
|
26
|
+
import {
|
|
27
|
+
allSubagentRoles,
|
|
28
|
+
getSubagentRole,
|
|
29
|
+
resolveSubagentModel,
|
|
30
|
+
resolveSubagentMaxSteps,
|
|
31
|
+
resolveSubagentThinking,
|
|
32
|
+
parseMaxSteps,
|
|
33
|
+
withSubagentSetting,
|
|
34
|
+
clearSubagentSetting,
|
|
35
|
+
} from "../../agent/subagents";
|
|
36
|
+
import {
|
|
37
|
+
formatModelLine,
|
|
38
|
+
liveModelKnown,
|
|
39
|
+
formatPickListWithCapabilities,
|
|
40
|
+
formatAgentsPanel,
|
|
41
|
+
formatAgentDetail,
|
|
42
|
+
} from "../../tui/components/config-panel";
|
|
43
|
+
import type { SelectItem } from "../../tui/components/select-list";
|
|
44
|
+
import { openaiCompatDef } from "../../ai/providers/openai-compatible-catalog";
|
|
45
|
+
import { isProviderName, fastThinkingLevelForModel } from "./flags";
|
|
46
|
+
|
|
47
|
+
// Per-provider starting model for provider pinning. Catalog OpenAI-compatible
|
|
48
|
+
// providers supply their own default; built-ins use this map.
|
|
49
|
+
// Pure data — mirrors the identically-named REPL-local const in launch.ts.
|
|
50
|
+
const STATIC_PROVIDER_DEFAULT: Partial<Record<ProviderName, string>> = { anthropic: "sonnet", openai: "gpt-5.5", gemini: "flash", antigravity: "antigravity/gemini-3-pro-high", ollama: "fast", lmstudio: "lmstudio/local-model", xai: "grok-4.3", kimi: "kimi-k2-0711-preview" };
|
|
51
|
+
|
|
52
|
+
// Pure function — mirrors the identically-named REPL-local const in launch.ts.
|
|
53
|
+
function providerDefaultModel(p: ProviderName): string {
|
|
54
|
+
return openaiCompatDef(p)?.defaultModel ?? STATIC_PROVIDER_DEFAULT[p] ?? "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Pick-list entries for ONE provider, with static fallbacks so the list is
|
|
58
|
+
// never empty. Pure function — mirrors launch.ts's exported `providerPickEntries`
|
|
59
|
+
// (duplicated here rather than imported to avoid a launch.ts <-> launch/*.ts cycle).
|
|
60
|
+
function providerPickEntries(live: ProviderModelsResult[], want: ProviderName): PickEntry[] {
|
|
61
|
+
const fromLive = flattenModels(live.filter(r => r.provider === want));
|
|
62
|
+
if (fromLive.length) return fromLive;
|
|
63
|
+
const catalog = catalogByProvider(want);
|
|
64
|
+
if (catalog.length) {
|
|
65
|
+
return catalog.map((m, i) => ({ index: i + 1, provider: want, model: qualifyModelId(m.providerModel, want) }));
|
|
66
|
+
}
|
|
67
|
+
const def = openaiCompatDef(want);
|
|
68
|
+
if (def) {
|
|
69
|
+
const ids = [def.defaultModel, ...(def.knownModels ?? [])].map(m => qualifyModelId(m, want));
|
|
70
|
+
const seen = new Set<string>();
|
|
71
|
+
const entries: PickEntry[] = [];
|
|
72
|
+
for (const model of ids) {
|
|
73
|
+
if (seen.has(model)) continue;
|
|
74
|
+
seen.add(model);
|
|
75
|
+
entries.push({ index: entries.length + 1, provider: want, model });
|
|
76
|
+
}
|
|
77
|
+
if (entries.length) return entries;
|
|
78
|
+
}
|
|
79
|
+
const fallback = providerDefaultModel(want);
|
|
80
|
+
return fallback ? [{ index: 1, provider: want, model: qualifyModelId(fallback, want) }] : [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Antigravity with ANY Google OAuth (own login or the gemini-cli fallback) stays
|
|
84
|
+
// SELECTABLE in pickers even when not call-ready: picking the model is how users
|
|
85
|
+
// reach the flow, and the auth layer gives actionable login guidance on the first
|
|
86
|
+
// call if the fallback token is rejected (403). Refusing selection was a dead end.
|
|
87
|
+
// Pure predicate — mirrors the identically-named REPL-local const in launch.ts.
|
|
88
|
+
const selectableThoughNotReady = (st?: { name: string; kind: string }): boolean =>
|
|
89
|
+
!!st && st.name === "antigravity" && st.kind === "oauth";
|
|
90
|
+
|
|
91
|
+
// Pure function — mirrors the identically-named REPL-local const in launch.ts.
|
|
92
|
+
const notReadyWarning = (st: { name: string; label: string }): string =>
|
|
93
|
+
` ! ${st.name} is not call-ready yet (${st.label}) — run /provider login antigravity before the first turn.`;
|
|
94
|
+
|
|
95
|
+
export interface AgentsSlashCtx {
|
|
96
|
+
sessionModel: string | undefined;
|
|
97
|
+
sessionThinking: ThinkLevel | undefined;
|
|
98
|
+
lastPickIndex: PickEntry[];
|
|
99
|
+
getLiveModels: (force?: boolean) => Promise<ProviderModelsResult[]>;
|
|
100
|
+
/**
|
|
101
|
+
* REPL's interactive live-model picker menu (raw-mode keypress loop +
|
|
102
|
+
* repaint). Not a pure module export — it closes over `runSelectPicker`
|
|
103
|
+
* and `modelPickerAssignments` (which itself reads `sessionModel`), so the
|
|
104
|
+
* caller threads it through like the other REPL hooks.
|
|
105
|
+
*/
|
|
106
|
+
pickLiveProviderModel: (
|
|
107
|
+
providerName: string,
|
|
108
|
+
entries: PickEntry[],
|
|
109
|
+
current?: string,
|
|
110
|
+
disabledProviders?: readonly ProviderName[],
|
|
111
|
+
) => Promise<PickEntry | undefined>;
|
|
112
|
+
/**
|
|
113
|
+
* Sets (or clears) a subagent role's thinking override, printing the
|
|
114
|
+
* confirmation itself. Closes over `readGlobalConfig`/`saveConfigPatch`
|
|
115
|
+
* REPL-local wiring, so the caller threads it through.
|
|
116
|
+
*/
|
|
117
|
+
setRoleThinking: (roleId: string, rawLevel: string | undefined) => Promise<boolean>;
|
|
118
|
+
/** Generic arrows+Enter option picker (role / action menus). REPL-local closure. */
|
|
119
|
+
pickFromOptions: (title: string, options: SelectItem<string>[]) => Promise<string | undefined>;
|
|
120
|
+
/** Arrows+Enter thinking-level picker built on `pickFromOptions`. REPL-local closure. */
|
|
121
|
+
pickThinkingLevel: (
|
|
122
|
+
title: string,
|
|
123
|
+
current: ThinkLevel | undefined,
|
|
124
|
+
inheritLabel?: string,
|
|
125
|
+
) => Promise<ThinkLevel | "inherit" | undefined>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface AgentsSlashResult {
|
|
129
|
+
/** Present only when changed. */
|
|
130
|
+
sessionThinking?: ThinkLevel;
|
|
131
|
+
/** Present only when the block builds/refreshes a pick list. */
|
|
132
|
+
lastPickIndex?: PickEntry[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Handle `/agents [edit|role] [model|#N|thinking L|maxSteps N|reset]`.
|
|
137
|
+
* Extracted verbatim from launch.ts's inline REPL branch — the caller keeps
|
|
138
|
+
* the `input === "/agents" || input.startsWith("/agents ")` guard and just
|
|
139
|
+
* calls this function.
|
|
140
|
+
*/
|
|
141
|
+
export async function runAgentsSlash(input: string, ctx: AgentsSlashCtx): Promise<AgentsSlashResult> {
|
|
142
|
+
const { getLiveModels, pickLiveProviderModel, setRoleThinking, pickFromOptions, pickThinkingLevel } = ctx;
|
|
143
|
+
let lastPickIndex = ctx.lastPickIndex;
|
|
144
|
+
let lastPickIndexChanged = false;
|
|
145
|
+
|
|
146
|
+
const result = (): AgentsSlashResult => ({
|
|
147
|
+
...(lastPickIndexChanged ? { lastPickIndex } : {}),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const agentsCommand = "/agents";
|
|
151
|
+
const tokens = input.substring(agentsCommand.length).trim().split(/\s+/).filter(Boolean);
|
|
152
|
+
const roleArg = tokens[0];
|
|
153
|
+
const modelArg = tokens[1];
|
|
154
|
+
const cfgNow = await readGlobalConfig();
|
|
155
|
+
const subcommand = roleArg?.toLowerCase();
|
|
156
|
+
const printRoster = () => {
|
|
157
|
+
console.log("Subagent roles (used by 'jeo team'):");
|
|
158
|
+
for (const line of formatAgentsPanel(allSubagentRoles(cfgNow), r => ({
|
|
159
|
+
model: resolveSubagentModel(r.id, cfgNow),
|
|
160
|
+
maxSteps: resolveSubagentMaxSteps(r.id, cfgNow),
|
|
161
|
+
thinking: resolveSubagentThinking(r.id, cfgNow),
|
|
162
|
+
}))) console.log(line);
|
|
163
|
+
console.log("Detail: /agents <role> · set model: /agents <role> <model|#N> · provider: /agents <role> provider <name> [model] · thinking: /agents <role> thinking <level|inherit> · steps: /agents <role> maxSteps <N> · picker: /agents edit");
|
|
164
|
+
console.log("Tip: primary model flow: /model → pick model → choose default or subagent role → choose thinking level");
|
|
165
|
+
console.log(`Available: ${allSubagentRoles(cfgNow).map(r => r.id).join(", ")} (declare custom roles in config.subagents)`);
|
|
166
|
+
console.log("Subcommands: edit, <role> <model|#N>, <role> thinking <level|inherit>, <role> provider <name> [model], <role> maxSteps <N>, <role> reset");
|
|
167
|
+
};
|
|
168
|
+
if (!roleArg || roleArg === "/" || roleArg === "?" || subcommand === "help") {
|
|
169
|
+
printRoster();
|
|
170
|
+
return result();
|
|
171
|
+
}
|
|
172
|
+
if (subcommand === "edit" || subcommand === "picker") {
|
|
173
|
+
printRoster();
|
|
174
|
+
// Interactive editor (TTY): role picker → action picker → live model /
|
|
175
|
+
// thinking / reset — the arrows+Enter way to CHANGE an existing setting.
|
|
176
|
+
const rolePick = await pickFromOptions(
|
|
177
|
+
"Edit a subagent role (ESC to skip)",
|
|
178
|
+
allSubagentRoles(cfgNow).map(r => ({
|
|
179
|
+
value: r.id,
|
|
180
|
+
label: `${r.id} — ${r.title}`,
|
|
181
|
+
hint: `${resolveSubagentModel(r.id, cfgNow)} · ${resolveSubagentMaxSteps(r.id, cfgNow)} steps${cfgNow.subagents?.[r.id]?.model ? "" : " (default)"}`,
|
|
182
|
+
})),
|
|
183
|
+
);
|
|
184
|
+
const editRole = rolePick ? getSubagentRole(rolePick, cfgNow) : undefined;
|
|
185
|
+
if (!editRole) return result();
|
|
186
|
+
const action = await pickFromOptions(`${editRole.title} — choose action`, [
|
|
187
|
+
{ value: "model", label: "change model", hint: resolveSubagentModel(editRole.id, cfgNow) },
|
|
188
|
+
{ value: "thinking", label: "change thinking", hint: resolveSubagentThinking(editRole.id, cfgNow) ?? `inherit (${cfgNow.thinkingLevel ?? "medium"})` },
|
|
189
|
+
{ value: "reset", label: "reset to defaults", hint: "clears model + maxSteps + thinking override" },
|
|
190
|
+
]);
|
|
191
|
+
if (action === "reset") {
|
|
192
|
+
await saveConfigPatch(raw => ({ subagents: clearSubagentSetting(raw, editRole.id) }));
|
|
193
|
+
console.log(`${editRole.title} settings reset to defaults → ~/.jeo/config.json`);
|
|
194
|
+
} else if (action === "model") {
|
|
195
|
+
const live = await getLiveModels();
|
|
196
|
+
const entries = flattenModels(live);
|
|
197
|
+
const picked = await pickLiveProviderModel(`${editRole.id}`, entries, resolveSubagentModel(editRole.id, cfgNow));
|
|
198
|
+
if (picked) {
|
|
199
|
+
const pinned = qualifyModelId(picked.model, picked.provider);
|
|
200
|
+
await saveConfigPatch(raw => ({ subagents: withSubagentSetting(raw, editRole.id, { model: pinned }) }));
|
|
201
|
+
console.log(`Subagent '${editRole.id}' model set to ${pinned} → ~/.jeo/config.json`);
|
|
202
|
+
}
|
|
203
|
+
} else if (action === "thinking") {
|
|
204
|
+
const lvl = await pickThinkingLevel(
|
|
205
|
+
`Reasoning for ${editRole.title}`,
|
|
206
|
+
cfgNow.subagents?.[editRole.id]?.thinking,
|
|
207
|
+
`inherit — follow default (${cfgNow.thinkingLevel ?? "medium"})`,
|
|
208
|
+
);
|
|
209
|
+
if (lvl) await setRoleThinking(editRole.id, lvl);
|
|
210
|
+
}
|
|
211
|
+
return result();
|
|
212
|
+
}
|
|
213
|
+
const role = getSubagentRole(roleArg, cfgNow);
|
|
214
|
+
if (!role) {
|
|
215
|
+
console.log(`Unknown role '${roleArg}'. Known: ${allSubagentRoles(cfgNow).map(r => r.id).join(", ")}.`);
|
|
216
|
+
return result();
|
|
217
|
+
}
|
|
218
|
+
if (modelArg?.toLowerCase() === "reset") {
|
|
219
|
+
await saveConfigPatch(raw => ({ subagents: clearSubagentSetting(raw, role.id) }));
|
|
220
|
+
console.log(`${role.title} settings reset to defaults → ~/.jeo/config.json`);
|
|
221
|
+
return result();
|
|
222
|
+
}
|
|
223
|
+
if (modelArg?.toLowerCase() === "maxsteps" || modelArg?.toLowerCase() === "steps") {
|
|
224
|
+
const maxSteps = parseMaxSteps(tokens[2]);
|
|
225
|
+
if (!maxSteps) {
|
|
226
|
+
console.log(`Usage: /agents ${role.id} maxSteps <positive-number>`);
|
|
227
|
+
return result();
|
|
228
|
+
}
|
|
229
|
+
await saveConfigPatch(raw => ({ subagents: withSubagentSetting(raw, role.id, { maxSteps }) }));
|
|
230
|
+
console.log(`${role.title} maxSteps set to ${maxSteps} → ~/.jeo/config.json`);
|
|
231
|
+
return result();
|
|
232
|
+
}
|
|
233
|
+
if (modelArg?.toLowerCase() === "thinking" || modelArg?.toLowerCase() === "think") {
|
|
234
|
+
await setRoleThinking(role.id, tokens[2]);
|
|
235
|
+
return result();
|
|
236
|
+
}
|
|
237
|
+
if (modelArg?.toLowerCase() === "provider") {
|
|
238
|
+
const want = (tokens[2] ?? "").toLowerCase();
|
|
239
|
+
if (!isProviderName(want)) {
|
|
240
|
+
console.log(`Usage: /agents ${role.id} provider <name> [model|#N] — e.g. anthropic, openai, gemini, groq, deepseek, openrouter (any configured provider)`);
|
|
241
|
+
return result();
|
|
242
|
+
}
|
|
243
|
+
const st = (await describeAllProviders()).find(s => s.name === want);
|
|
244
|
+
if (st && !st.ready) {
|
|
245
|
+
if (selectableThoughNotReady(st)) {
|
|
246
|
+
console.log(notReadyWarning(st));
|
|
247
|
+
} else {
|
|
248
|
+
console.log(`Cannot pin ${role.title} to ${want}: not ready (${st.label}). Set ${st.envVar ?? "the provider key"} first.`);
|
|
249
|
+
return result();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const live = await getLiveModels();
|
|
253
|
+
const forProvider = providerPickEntries(live, want);
|
|
254
|
+
const liveForProvider = live.some(r => r.ok && r.provider === want && r.models.length > 0);
|
|
255
|
+
const explicit = tokens[3];
|
|
256
|
+
let chosenModel: string;
|
|
257
|
+
if (explicit && forProvider.length) {
|
|
258
|
+
const sel = resolveSelection(forProvider, explicit);
|
|
259
|
+
if (sel.kind === "index" || sel.kind === "match") chosenModel = qualifyModelId(sel.entry.model, want);
|
|
260
|
+
else if (sel.kind === "ambiguous") {
|
|
261
|
+
console.log(`'${explicit}' matches ${sel.matches.length} ${want} models — be more specific:`);
|
|
262
|
+
for (const e of sel.matches.slice(0, 12)) console.log(` #${e.index} ${e.model}`);
|
|
263
|
+
return result();
|
|
264
|
+
} else if (sel.kind === "out-of-range") {
|
|
265
|
+
console.log(`#${explicit.slice(1)} is out of range for ${want} (1-${sel.max}).`);
|
|
266
|
+
return result();
|
|
267
|
+
} else {
|
|
268
|
+
chosenModel = qualifyModelId(explicit, want);
|
|
269
|
+
}
|
|
270
|
+
} else if (explicit) {
|
|
271
|
+
chosenModel = qualifyModelId(explicit, want);
|
|
272
|
+
} else if (forProvider.length) {
|
|
273
|
+
// No model given → the provider's first known model, provider-qualified.
|
|
274
|
+
chosenModel = qualifyModelId(forProvider[0]!.model, want);
|
|
275
|
+
} else {
|
|
276
|
+
chosenModel = providerDefaultModel(want);
|
|
277
|
+
}
|
|
278
|
+
await saveConfigPatch(raw => ({ subagents: withSubagentSetting(raw, role.id, { model: chosenModel }) }));
|
|
279
|
+
console.log(`${role.title} pinned to ${want} via model ${chosenModel} — saved to ~/.jeo/config.json`);
|
|
280
|
+
if (forProvider.length) {
|
|
281
|
+
lastPickIndex = forProvider;
|
|
282
|
+
lastPickIndexChanged = true;
|
|
283
|
+
const sourceNote = liveForProvider ? "Live" : "Catalog";
|
|
284
|
+
const tail = liveForProvider ? "" : " (log in to list live models)";
|
|
285
|
+
console.log(`${sourceNote} ${want} models — refine with /agents ${role.id} #N:${tail}`);
|
|
286
|
+
for (const line of formatPickListWithCapabilities(lastPickIndex, { current: chosenModel, cap: 12 })) console.log(line);
|
|
287
|
+
}
|
|
288
|
+
return result();
|
|
289
|
+
}
|
|
290
|
+
if (modelArg) {
|
|
291
|
+
let chosenModel = modelArg;
|
|
292
|
+
let entries = lastPickIndex;
|
|
293
|
+
if (modelArg.startsWith("#") && entries.length === 0) {
|
|
294
|
+
const live = await getLiveModels();
|
|
295
|
+
entries = flattenModels(live);
|
|
296
|
+
}
|
|
297
|
+
if (entries.length) {
|
|
298
|
+
const sel = resolveSelection(entries, modelArg);
|
|
299
|
+
if (sel.kind === "index" || sel.kind === "match") {
|
|
300
|
+
chosenModel = qualifyModelId(sel.entry.model, sel.entry.provider);
|
|
301
|
+
const bad = (await describeAllProviders()).find(s => s.name === sel.entry.provider && !s.ready);
|
|
302
|
+
if (bad) {
|
|
303
|
+
if (selectableThoughNotReady(bad)) {
|
|
304
|
+
console.log(notReadyWarning(bad));
|
|
305
|
+
} else {
|
|
306
|
+
console.log(`Cannot pin ${sel.entry.model}: ${sel.entry.provider} is not ready (${bad.label}). Set ${bad.envVar ?? "the provider key"} first.`);
|
|
307
|
+
return result();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
} else if (sel.kind === "ambiguous") {
|
|
311
|
+
console.log(`'${modelArg}' matches ${sel.matches.length} live models — be more specific:`);
|
|
312
|
+
for (const e of sel.matches.slice(0, 12)) console.log(` #${e.index} ${e.model} (${e.provider})`);
|
|
313
|
+
return result();
|
|
314
|
+
} else if (sel.kind === "out-of-range") {
|
|
315
|
+
console.log(`#${modelArg.slice(1)} is out of range (1-${sel.max}). Use /model first.`);
|
|
316
|
+
return result();
|
|
317
|
+
}
|
|
318
|
+
} else if (modelArg.startsWith("#")) {
|
|
319
|
+
console.log("Use /model first to build the numbered live model list.");
|
|
320
|
+
return result();
|
|
321
|
+
}
|
|
322
|
+
// Persist a per-role model override to ~/.jeo/config.json (consumed by 'jeo team').
|
|
323
|
+
await saveConfigPatch(raw => ({ subagents: withSubagentSetting(raw, role.id, { model: chosenModel }) }));
|
|
324
|
+
const { provider } = await describeModel(chosenModel);
|
|
325
|
+
console.log(`${role.title} model set to ${chosenModel} (${provider}) — saved to ~/.jeo/config.json`);
|
|
326
|
+
const live = await getLiveModels();
|
|
327
|
+
if (!liveModelKnown(live, chosenModel)) {
|
|
328
|
+
console.log(` (note: '${chosenModel}' is not in any live model list — verify it is valid for ${provider})`);
|
|
329
|
+
}
|
|
330
|
+
return result();
|
|
331
|
+
}
|
|
332
|
+
for (const line of formatAgentDetail(role, {
|
|
333
|
+
model: resolveSubagentModel(role.id, cfgNow),
|
|
334
|
+
maxSteps: resolveSubagentMaxSteps(role.id, cfgNow),
|
|
335
|
+
thinking: resolveSubagentThinking(role.id, cfgNow),
|
|
336
|
+
})) console.log(line);
|
|
337
|
+
const live = await getLiveModels();
|
|
338
|
+
const agentPick = flattenModels(live);
|
|
339
|
+
if (agentPick.length) {
|
|
340
|
+
lastPickIndex = agentPick;
|
|
341
|
+
lastPickIndexChanged = true;
|
|
342
|
+
console.log(`Live models for ${role.title} — pin with /agents ${role.id} #N:`);
|
|
343
|
+
for (const line of formatPickListWithCapabilities(lastPickIndex, { current: resolveSubagentModel(role.id, cfgNow), cap: 20 })) console.log(line);
|
|
344
|
+
}
|
|
345
|
+
return result();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const ROLE_TIERS = ["smol", "slow", "plan"] as const;
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Handle `/roles [tier model]`. Extracted verbatim from launch.ts's inline
|
|
352
|
+
* REPL branch — the caller keeps the `input.startsWith("/roles") &&
|
|
353
|
+
* (input === "/roles" || input[6] === " ")` guard and just calls this
|
|
354
|
+
* function.
|
|
355
|
+
*/
|
|
356
|
+
export async function runRolesSlash(input: string, ctx: AgentsSlashCtx): Promise<AgentsSlashResult> {
|
|
357
|
+
const { getLiveModels } = ctx;
|
|
358
|
+
let lastPickIndex = ctx.lastPickIndex;
|
|
359
|
+
let lastPickIndexChanged = false;
|
|
360
|
+
|
|
361
|
+
const result = (): AgentsSlashResult => ({
|
|
362
|
+
...(lastPickIndexChanged ? { lastPickIndex } : {}),
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const tokens = input.substring(6).trim().split(/\s+/).filter(Boolean);
|
|
366
|
+
const cfgNow = await readGlobalConfig();
|
|
367
|
+
if (tokens.length >= 2 && (ROLE_TIERS as readonly string[]).includes(tokens[0])) {
|
|
368
|
+
const tier = tokens[0] as (typeof ROLE_TIERS)[number];
|
|
369
|
+
let chosenModel = tokens[1]!;
|
|
370
|
+
let entries = lastPickIndex;
|
|
371
|
+
if (chosenModel.startsWith("#") && entries.length === 0) {
|
|
372
|
+
const live = await getLiveModels();
|
|
373
|
+
entries = flattenModels(live);
|
|
374
|
+
}
|
|
375
|
+
if (entries.length) {
|
|
376
|
+
const sel = resolveSelection(entries, chosenModel);
|
|
377
|
+
if (sel.kind === "index" || sel.kind === "match") {
|
|
378
|
+
chosenModel = qualifyModelId(sel.entry.model, sel.entry.provider);
|
|
379
|
+
const bad = (await describeAllProviders()).find(s => s.name === sel.entry.provider && !s.ready);
|
|
380
|
+
if (bad) {
|
|
381
|
+
console.log(`Cannot set role ${tier} to ${sel.entry.model}: ${sel.entry.provider} is not ready (${bad.label}). Set ${bad.envVar ?? "the provider key"} first.`);
|
|
382
|
+
return result();
|
|
383
|
+
}
|
|
384
|
+
} else if (sel.kind === "ambiguous") {
|
|
385
|
+
console.log(`'${chosenModel}' matches ${sel.matches.length} live models — be more specific:`);
|
|
386
|
+
for (const e of sel.matches.slice(0, 12)) console.log(` #${e.index} ${e.model} (${e.provider})`);
|
|
387
|
+
return result();
|
|
388
|
+
} else if (sel.kind === "out-of-range") {
|
|
389
|
+
console.log(`#${chosenModel.slice(1)} is out of range (1-${sel.max}). Use /model first.`);
|
|
390
|
+
return result();
|
|
391
|
+
}
|
|
392
|
+
} else if (chosenModel.startsWith("#")) {
|
|
393
|
+
console.log("Use /model first to build the numbered live model list.");
|
|
394
|
+
return result();
|
|
395
|
+
}
|
|
396
|
+
await saveConfigPatch(raw => ({ roles: { ...(raw.roles ?? {}), [tier]: chosenModel } }));
|
|
397
|
+
console.log(`Role '${tier}' model set to ${chosenModel} → ~/.jeo/config.json`);
|
|
398
|
+
return result();
|
|
399
|
+
}
|
|
400
|
+
console.log("Model role tiers (fall back to the default model):");
|
|
401
|
+
for (const tier of ROLE_TIERS) {
|
|
402
|
+
const { provider } = await describeModel(resolveRoleModel(tier, cfgNow));
|
|
403
|
+
console.log(` ${tier.padEnd(5)} ${resolveRoleModel(tier, cfgNow)} (${provider})`);
|
|
404
|
+
}
|
|
405
|
+
console.log("Set a tier: /roles <smol|slow|plan> <model>");
|
|
406
|
+
const live = await getLiveModels();
|
|
407
|
+
const rolePick = flattenModels(live);
|
|
408
|
+
if (rolePick.length) {
|
|
409
|
+
lastPickIndex = rolePick;
|
|
410
|
+
lastPickIndexChanged = true;
|
|
411
|
+
console.log("Live models for role tiers — set with /roles <tier> #N:");
|
|
412
|
+
for (const line of formatPickListWithCapabilities(lastPickIndex, { cap: 15 })) console.log(line);
|
|
413
|
+
}
|
|
414
|
+
return result();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Handle `/fast [on|off|status]`. Extracted verbatim from launch.ts's inline
|
|
419
|
+
* REPL branch — the caller keeps the `input.startsWith("/fast") && (input
|
|
420
|
+
* === "/fast" || input[5] === " ")` guard and just calls this function.
|
|
421
|
+
*/
|
|
422
|
+
export async function runFastSlash(input: string, ctx: AgentsSlashCtx): Promise<AgentsSlashResult> {
|
|
423
|
+
let sessionThinking = ctx.sessionThinking;
|
|
424
|
+
let sessionThinkingChanged = false;
|
|
425
|
+
|
|
426
|
+
const result = (): AgentsSlashResult => ({
|
|
427
|
+
...(sessionThinkingChanged ? { sessionThinking } : {}),
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const arg = input.substring(5).trim().toLowerCase() || "status";
|
|
431
|
+
const cfgNow = await readGlobalConfig();
|
|
432
|
+
const currentModel = ctx.sessionModel || cfgNow.defaultModel;
|
|
433
|
+
const { resolved, provider } = await describeModel(currentModel);
|
|
434
|
+
const fastLevel = fastThinkingLevelForModel(resolved);
|
|
435
|
+
const currentThinking = sessionThinking ?? cfgNow.thinkingLevel ?? "medium";
|
|
436
|
+
const status = fastLevel && currentThinking === fastLevel ? "on" : "off";
|
|
437
|
+
if (arg === "status") {
|
|
438
|
+
const support = fastLevel ? `supported (thinking ${fastLevel})` : "unsupported";
|
|
439
|
+
console.log(`Fast mode: ${status} · ${support} · ${formatModelLine({ label: currentModel, resolved, provider })} · current thinking ${currentThinking}`);
|
|
440
|
+
return result();
|
|
441
|
+
}
|
|
442
|
+
if (arg === "on") {
|
|
443
|
+
if (!fastLevel) {
|
|
444
|
+
console.log(`Fast mode is not advertised for ${formatModelLine({ label: currentModel, resolved, provider })}; pick a thinking-capable model with /model.`);
|
|
445
|
+
return result();
|
|
446
|
+
}
|
|
447
|
+
sessionThinking = fastLevel;
|
|
448
|
+
sessionThinkingChanged = true;
|
|
449
|
+
console.log(`Fast mode on: ${formatModelLine({ label: currentModel, resolved, provider })} · thinking ${fastLevel} (~${thinkingMaxTokens(fastLevel)} max tokens/step)`);
|
|
450
|
+
return result();
|
|
451
|
+
}
|
|
452
|
+
if (arg === "off") {
|
|
453
|
+
sessionThinking = cfgNow.thinkingLevel ?? "medium";
|
|
454
|
+
sessionThinkingChanged = true;
|
|
455
|
+
console.log(`Fast mode off: restored thinking ${sessionThinking} (~${thinkingMaxTokens(sessionThinking)} max tokens/step)`);
|
|
456
|
+
return result();
|
|
457
|
+
}
|
|
458
|
+
console.log("Usage: /fast [on|off|status]");
|
|
459
|
+
return result();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Handle `/thinking [level]`. Extracted verbatim from launch.ts's inline
|
|
464
|
+
* REPL branch — the caller keeps the `input.startsWith("/thinking") &&
|
|
465
|
+
* (input === "/thinking" || input[9] === " ")` guard and just calls this
|
|
466
|
+
* function.
|
|
467
|
+
*/
|
|
468
|
+
export async function runThinkingSlash(input: string, ctx: AgentsSlashCtx): Promise<AgentsSlashResult> {
|
|
469
|
+
let sessionThinking = ctx.sessionThinking;
|
|
470
|
+
let sessionThinkingChanged = false;
|
|
471
|
+
|
|
472
|
+
const result = (): AgentsSlashResult => ({
|
|
473
|
+
...(sessionThinkingChanged ? { sessionThinking } : {}),
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const arg = input.substring(9).trim().toLowerCase();
|
|
477
|
+
if (!arg) {
|
|
478
|
+
console.log(`Thinking level: ${sessionThinking ?? "medium"} (~${thinkingMaxTokens(sessionThinking)} max tokens/step)`);
|
|
479
|
+
return result();
|
|
480
|
+
}
|
|
481
|
+
if (arg === "minimal" || arg === "low" || arg === "medium" || arg === "high" || arg === "xhigh") {
|
|
482
|
+
sessionThinking = arg;
|
|
483
|
+
sessionThinkingChanged = true;
|
|
484
|
+
console.log(`Thinking set to ${arg} (~${thinkingMaxTokens(arg)} max tokens/step)`);
|
|
485
|
+
} else {
|
|
486
|
+
console.log(`Invalid level '${arg}'. Use: minimal | low | medium | high | xhigh.`);
|
|
487
|
+
}
|
|
488
|
+
return result();
|
|
489
|
+
}
|
|
@@ -6,6 +6,9 @@ export interface LaunchFlags {
|
|
|
6
6
|
list: boolean;
|
|
7
7
|
resume: boolean;
|
|
8
8
|
resumeId?: string;
|
|
9
|
+
/** True when `--resume`/`-r` was given with NO value at cold startup (gjc-parity):
|
|
10
|
+
* triggers the interactive session picker instead of silently resuming latest. */
|
|
11
|
+
resumeInteractive: boolean;
|
|
9
12
|
noSession: boolean;
|
|
10
13
|
noTui: boolean;
|
|
11
14
|
/** Explicit step cap from --max-steps; 0 = dynamic (process-driven budget that
|
|
@@ -63,9 +66,9 @@ export function fastThinkingLevelForModel(modelId: string): ThinkLevel | undefin
|
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
export function parseFlags(args: string[], cwd: string = process.cwd()): LaunchFlags {
|
|
66
|
-
const flags: LaunchFlags = { list: false, resume: false, noSession: false, noTui: false, maxSteps: 0, message: "", tmux: false, errors: [], print: false, noSkills: false, noTools: false };
|
|
69
|
+
const flags: LaunchFlags = { list: false, resume: false, resumeInteractive: false, noSession: false, noTui: false, maxSteps: 0, message: "", tmux: false, errors: [], print: false, noSkills: false, noTools: false };
|
|
67
70
|
const rest: string[] = [];
|
|
68
|
-
|
|
71
|
+
|
|
69
72
|
for (let i = 0; i < args.length; i++) {
|
|
70
73
|
const a = args[i];
|
|
71
74
|
if (a === "--") {
|
|
@@ -115,22 +118,37 @@ export function parseFlags(args: string[], cwd: string = process.cwd()): LaunchF
|
|
|
115
118
|
i = nextIndex;
|
|
116
119
|
} else if (a === "--smol" || a === "--slow" || a === "--plan") {
|
|
117
120
|
flags.modelRole = a.slice(2) as ModelRole;
|
|
118
|
-
} else if (a === "--
|
|
121
|
+
} else if (a === "--continue" || a === "-c") {
|
|
122
|
+
// Always silently resume the MOST RECENT session (gjc-parity: no picker for --continue/-c).
|
|
119
123
|
flags.resume = true;
|
|
124
|
+
flags.resumeInteractive = false;
|
|
120
125
|
const next = args[i + 1];
|
|
121
|
-
if (next &&
|
|
126
|
+
if (next && !next.startsWith("-")) {
|
|
122
127
|
flags.resumeId = next;
|
|
123
128
|
i++;
|
|
124
129
|
}
|
|
125
|
-
} else if (a.startsWith("--
|
|
130
|
+
} else if (a.startsWith("--continue=") || a.startsWith("-c=")) {
|
|
126
131
|
flags.resume = true;
|
|
132
|
+
flags.resumeInteractive = false;
|
|
127
133
|
const eqIdx = a.indexOf("=");
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
134
|
+
flags.resumeId = a.slice(eqIdx + 1);
|
|
135
|
+
} else if (a === "--resume" || a === "-r") {
|
|
136
|
+
// gjc-parity: value present -> resolve as id/prefix directly; no value -> interactive picker.
|
|
137
|
+
flags.resume = true;
|
|
138
|
+
const next = args[i + 1];
|
|
139
|
+
if (next && !next.startsWith("-")) {
|
|
140
|
+
flags.resumeId = next;
|
|
141
|
+
flags.resumeInteractive = false;
|
|
142
|
+
i++;
|
|
131
143
|
} else {
|
|
132
|
-
|
|
144
|
+
flags.resumeId = undefined;
|
|
145
|
+
flags.resumeInteractive = true;
|
|
133
146
|
}
|
|
147
|
+
} else if (a.startsWith("--resume=")) {
|
|
148
|
+
flags.resume = true;
|
|
149
|
+
flags.resumeInteractive = false;
|
|
150
|
+
const eqIdx = a.indexOf("=");
|
|
151
|
+
flags.resumeId = a.slice(eqIdx + 1);
|
|
134
152
|
} else if (a === "--append-system-prompt" || a.startsWith("--append-system-prompt=")) {
|
|
135
153
|
const { value, nextIndex } = takeValue(args, i, "--append-system-prompt=");
|
|
136
154
|
if (value) flags.appendSystemPromptRaw = value;
|