jeo-code 0.1.0 → 0.4.5
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/README.ja.md +160 -0
- package/README.ko.md +160 -0
- package/README.md +115 -297
- package/README.zh.md +160 -0
- package/package.json +11 -6
- package/scripts/install.sh +28 -28
- package/scripts/uninstall.sh +17 -15
- package/src/AGENTS.md +50 -0
- package/src/agent/AGENTS.md +49 -0
- package/src/agent/bash-fixups.ts +103 -0
- package/src/agent/compaction.ts +410 -19
- package/src/agent/config-schema.ts +119 -5
- package/src/agent/context-files.ts +314 -17
- package/src/agent/dev/AGENTS.md +36 -0
- package/src/agent/dev/advanced-analyzer.ts +12 -0
- package/src/agent/dev/evolution-bridge.ts +82 -0
- package/src/agent/dev/evolution-logger.ts +41 -0
- package/src/agent/dev/self-analysis.ts +64 -0
- package/src/agent/dev/self-improve.ts +24 -0
- package/src/agent/dev/spec-automation.ts +49 -0
- package/src/agent/engine.ts +808 -54
- package/src/agent/hooks.ts +273 -0
- package/src/agent/loop.ts +21 -1
- package/src/agent/memory.ts +201 -0
- package/src/agent/model-recency.ts +32 -0
- package/src/agent/output-minimizer.ts +108 -0
- package/src/agent/output-util.ts +64 -0
- package/src/agent/plan.ts +187 -0
- package/src/agent/seed.ts +52 -0
- package/src/agent/session.ts +235 -21
- package/src/agent/state.ts +286 -39
- package/src/agent/step-budget.ts +232 -0
- package/src/agent/subagents.ts +223 -26
- package/src/agent/task-tool.ts +272 -0
- package/src/agent/todo-tool.ts +87 -0
- package/src/agent/tokenizer.ts +117 -0
- package/src/agent/tool-registry.ts +54 -0
- package/src/agent/tools.ts +624 -103
- package/src/agent/web-search.ts +538 -0
- package/src/ai/AGENTS.md +44 -0
- package/src/ai/index.ts +1 -0
- package/src/ai/model-catalog-compat.ts +3 -1
- package/src/ai/model-catalog.ts +74 -9
- package/src/ai/model-discovery.ts +215 -17
- package/src/ai/model-manager.ts +346 -32
- package/src/ai/model-picker.ts +1 -1
- package/src/ai/model-registry.ts +4 -2
- package/src/ai/pricing.ts +84 -0
- package/src/ai/provider-registry.ts +23 -0
- package/src/ai/provider-status.ts +60 -16
- package/src/ai/providers/AGENTS.md +42 -0
- package/src/ai/providers/anthropic.ts +250 -31
- package/src/ai/providers/antigravity.ts +219 -0
- package/src/ai/providers/errors.ts +15 -1
- package/src/ai/providers/gemini.ts +196 -13
- package/src/ai/providers/ollama.ts +37 -7
- package/src/ai/providers/openai-responses.ts +173 -0
- package/src/ai/providers/openai.ts +64 -12
- package/src/ai/sse.ts +4 -1
- package/src/ai/types.ts +18 -1
- package/src/auth/AGENTS.md +41 -0
- package/src/auth/callback-server.ts +6 -1
- package/src/auth/flows/AGENTS.md +32 -0
- package/src/auth/flows/antigravity.ts +151 -0
- package/src/auth/flows/google-project.ts +190 -0
- package/src/auth/flows/google.ts +39 -18
- package/src/auth/flows/index.ts +15 -5
- package/src/auth/flows/openai.ts +2 -2
- package/src/auth/oauth.ts +8 -0
- package/src/auth/refresh.ts +44 -27
- package/src/auth/storage.ts +149 -26
- package/src/auth/types.ts +1 -1
- package/src/autopilot.ts +362 -0
- package/src/bun-imports.d.ts +4 -0
- package/src/cli/AGENTS.md +39 -0
- package/src/cli/runner.ts +148 -14
- package/src/cli.ts +13 -4
- package/src/commands/AGENTS.md +40 -0
- package/src/commands/approve.ts +62 -3
- package/src/commands/auth.ts +167 -25
- package/src/commands/chat.ts +37 -8
- package/src/commands/deep-interview.ts +633 -175
- package/src/commands/doctor.ts +84 -37
- package/src/commands/evolve-core.ts +18 -0
- package/src/commands/evolve.ts +2 -1
- package/src/commands/export.ts +176 -0
- package/src/commands/gjc.ts +52 -0
- package/src/commands/launch.ts +3549 -240
- package/src/commands/mcp.ts +3 -3
- package/src/commands/ooo-seed.ts +19 -0
- package/src/commands/ralplan.ts +253 -35
- package/src/commands/resume.ts +1 -1
- package/src/commands/session.ts +183 -0
- package/src/commands/setup-helpers.ts +10 -3
- package/src/commands/setup.ts +57 -16
- package/src/commands/skills.ts +78 -18
- package/src/commands/state.ts +198 -0
- package/src/commands/status.ts +84 -0
- package/src/commands/team.ts +340 -212
- package/src/commands/ultragoal.ts +122 -61
- package/src/commands/update.ts +244 -0
- package/src/ledger.ts +270 -0
- package/src/mcp/AGENTS.md +38 -0
- package/src/mcp/server.ts +115 -14
- package/src/mcp/tools.ts +42 -22
- package/src/md-modules.d.ts +4 -0
- package/src/prompts/AGENTS.md +41 -0
- package/src/prompts/agents/AGENTS.md +35 -0
- package/src/prompts/agents/architect.md +35 -0
- package/src/prompts/agents/critic.md +37 -0
- package/src/prompts/agents/executor.md +36 -0
- package/src/prompts/agents/planner.md +37 -0
- package/src/prompts/skills/AGENTS.md +36 -0
- package/src/prompts/skills/deep-dive/AGENTS.md +31 -0
- package/src/prompts/skills/deep-dive/SKILL.md +13 -0
- package/src/prompts/skills/deep-interview/AGENTS.md +31 -0
- package/src/prompts/skills/deep-interview/SKILL.md +12 -0
- package/src/prompts/skills/gjc/AGENTS.md +31 -0
- package/src/prompts/skills/gjc/SKILL.md +15 -0
- package/src/prompts/skills/ralplan/AGENTS.md +31 -0
- package/src/prompts/skills/ralplan/SKILL.md +11 -0
- package/src/prompts/skills/team/AGENTS.md +31 -0
- package/src/prompts/skills/team/SKILL.md +11 -0
- package/src/prompts/skills/ultragoal/AGENTS.md +31 -0
- package/src/prompts/skills/ultragoal/SKILL.md +11 -0
- package/src/skills/AGENTS.md +38 -0
- package/src/skills/catalog.ts +565 -31
- package/src/tui/AGENTS.md +43 -0
- package/src/tui/app.ts +1181 -92
- package/src/tui/components/AGENTS.md +42 -0
- package/src/tui/components/ascii-art.ts +257 -15
- package/src/tui/components/autocomplete.ts +98 -16
- package/src/tui/components/autopilot-status.ts +65 -0
- package/src/tui/components/category-index.ts +49 -0
- package/src/tui/components/code-view.ts +54 -11
- package/src/tui/components/color.ts +171 -2
- package/src/tui/components/config-panel.ts +82 -15
- package/src/tui/components/duration.ts +38 -0
- package/src/tui/components/evolution.ts +3 -3
- package/src/tui/components/footer.ts +91 -42
- package/src/tui/components/forge.ts +426 -31
- package/src/tui/components/hints.ts +54 -0
- package/src/tui/components/hud.ts +73 -0
- package/src/tui/components/index.ts +4 -0
- package/src/tui/components/input-box.ts +150 -0
- package/src/tui/components/layout.ts +11 -3
- package/src/tui/components/live-model-picker.ts +108 -0
- package/src/tui/components/markdown-table.ts +140 -0
- package/src/tui/components/markdown-text.ts +97 -0
- package/src/tui/components/meter.ts +4 -1
- package/src/tui/components/model-picker.ts +3 -2
- package/src/tui/components/provider-picker.ts +3 -2
- package/src/tui/components/section.ts +70 -0
- package/src/tui/components/select-list.ts +40 -10
- package/src/tui/components/skill-picker.ts +25 -0
- package/src/tui/components/slash.ts +244 -21
- package/src/tui/components/status.ts +272 -11
- package/src/tui/components/step-timeline.ts +218 -0
- package/src/tui/components/stream.ts +26 -9
- package/src/tui/components/themes.ts +212 -6
- package/src/tui/components/todo-card.ts +47 -0
- package/src/tui/components/tool-list.ts +58 -12
- package/src/tui/components/transcript.ts +120 -0
- package/src/tui/components/update-box.ts +31 -0
- package/src/tui/components/welcome.ts +162 -0
- package/src/tui/components/width.ts +163 -0
- package/src/tui/monitoring/AGENTS.md +31 -0
- package/src/tui/monitoring/hud-view.ts +55 -0
- package/src/tui/renderer.ts +112 -3
- package/src/tui/terminal.ts +40 -33
- package/src/util/AGENTS.md +39 -0
- package/src/util/clipboard-image.ts +118 -0
- package/src/util/env.ts +12 -0
- package/src/util/provider-error.ts +78 -0
- package/src/util/retry.ts +91 -6
- package/src/util/update-check.ts +64 -0
- package/src/commands/models.ts +0 -104
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StepBudget — gjc-style flexible step budgeting ("retry flow") for the agent loop.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the bare `step <= maxSteps` counter with a budget that EXTENDS itself
|
|
5
|
+
* while the turn is demonstrably making progress (recent tool calls succeeding on
|
|
6
|
+
* distinct targets) and fails fast into consolidation when it is stalled. Mirrors
|
|
7
|
+
* gjc's provider retry budgets: a base budget, a bounded number of extensions, an
|
|
8
|
+
* absolute hard cap, and explicit fail-fast classes (a stalled window — too many
|
|
9
|
+
* failures or no distinct targets — never earns an extension).
|
|
10
|
+
*
|
|
11
|
+
* The existing engine guards (MAX_REPEAT identical calls, MAX_FAILURES consecutive
|
|
12
|
+
* failures, parse-bounce salvage) remain the early fail-fast layer; this module
|
|
13
|
+
* only governs what happens when the step counter reaches the current limit.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface StepBudgetConfig {
|
|
17
|
+
/** Initial step budget (the caller's `maxSteps`). */
|
|
18
|
+
baseSteps: number;
|
|
19
|
+
/** Steps granted per extension. */
|
|
20
|
+
extensionSteps: number;
|
|
21
|
+
/** How many times the budget may extend (0 = legacy fixed counter). */
|
|
22
|
+
maxExtensions: number;
|
|
23
|
+
/** Absolute ceiling no extension may cross. */
|
|
24
|
+
hardCap: number;
|
|
25
|
+
/** Recent tool-call window scored for progress. */
|
|
26
|
+
windowSize: number;
|
|
27
|
+
/** Required ok-ratio in the window to earn an extension. */
|
|
28
|
+
minProgressRatio: number;
|
|
29
|
+
/** Required distinct call signatures in the window (anti-spin). */
|
|
30
|
+
minDistinct: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ExtensionDecision {
|
|
34
|
+
extend: boolean;
|
|
35
|
+
/** Human-readable reason — surfaced as an onNotice/onBudget line. */
|
|
36
|
+
reason: string;
|
|
37
|
+
/** The (possibly new) current limit after the decision. */
|
|
38
|
+
limit: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type EnvLike = Record<string, string | undefined>;
|
|
42
|
+
|
|
43
|
+
function envNum(env: EnvLike, key: string, dflt: number, min: number, max: number): number {
|
|
44
|
+
// `key` is the legacy JEO_* name; the JEO_* spelling is preferred when both are set.
|
|
45
|
+
const raw = env[key.replace(/^JEO_/, "JEO_")] ?? env[key];
|
|
46
|
+
if (raw === undefined || raw === "") return dflt;
|
|
47
|
+
const n = Number(raw);
|
|
48
|
+
if (!Number.isFinite(n)) return dflt;
|
|
49
|
+
return Math.max(min, Math.min(max, Math.trunc(n)));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the effective budget config: defaults ← env (`JEO_STEP_*`) ← caller overrides.
|
|
54
|
+
* Defaults keep the flow ON for the main agent (2 extensions, half-budget each, 3× cap);
|
|
55
|
+
* bounded delegation (task/team subagents) passes `{ maxExtensions: 0 }` so a parent's
|
|
56
|
+
* step contract stays exact.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveStepBudgetConfig(
|
|
59
|
+
baseSteps: number,
|
|
60
|
+
env: EnvLike = process.env,
|
|
61
|
+
overrides?: Partial<StepBudgetConfig>,
|
|
62
|
+
): StepBudgetConfig {
|
|
63
|
+
const base = Math.max(1, Math.trunc(baseSteps));
|
|
64
|
+
const cfg: StepBudgetConfig = {
|
|
65
|
+
baseSteps: base,
|
|
66
|
+
extensionSteps: envNum(env, "JEO_STEP_EXTENSION_SIZE", Math.max(4, Math.ceil(base / 2)), 1, 100),
|
|
67
|
+
maxExtensions: envNum(env, "JEO_STEP_EXTENSIONS", 2, 0, 8),
|
|
68
|
+
hardCap: envNum(env, "JEO_STEP_HARD_CAP", base * 3, base, base * 10),
|
|
69
|
+
windowSize: envNum(env, "JEO_STEP_WINDOW", 8, 2, 32),
|
|
70
|
+
minProgressRatio: 0.5,
|
|
71
|
+
minDistinct: 2,
|
|
72
|
+
};
|
|
73
|
+
const merged = { ...cfg, ...overrides };
|
|
74
|
+
// Sanity: the cap can never undercut the base budget.
|
|
75
|
+
merged.baseSteps = Math.max(1, Math.trunc(merged.baseSteps));
|
|
76
|
+
merged.hardCap = Math.max(merged.baseSteps, Math.trunc(merged.hardCap));
|
|
77
|
+
merged.maxExtensions = Math.max(0, Math.trunc(merged.maxExtensions));
|
|
78
|
+
merged.extensionSteps = Math.max(1, Math.trunc(merged.extensionSteps));
|
|
79
|
+
merged.windowSize = Math.max(2, Math.trunc(merged.windowSize));
|
|
80
|
+
return merged;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** True when `key` carries a non-empty value in `env`. */
|
|
84
|
+
function envSet(env: EnvLike, key: string): boolean {
|
|
85
|
+
const raw = env[key.replace(/^JEO_/, "JEO_")] ?? env[key];
|
|
86
|
+
return raw !== undefined && raw !== "";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Dynamic (process-driven) budget — the default when the caller passes no explicit
|
|
91
|
+
* `--max-steps`: there is no SMALL hardcoded step ceiling. The budget starts at a
|
|
92
|
+
* rolling base (`JEO_STEP_BASE`, default 24) and keeps extending itself for as long
|
|
93
|
+
* as the recent tool window shows real progress; only a stalled window declines the
|
|
94
|
+
* extension, at which point the loop dynamically CONSOLIDATES a final wrap-up
|
|
95
|
+
* instead of dying at a fixed count.
|
|
96
|
+
*
|
|
97
|
+
* Termination is still GUARANTEED: extensions are unlimited in count, but the
|
|
98
|
+
* absolute ceiling defaults to `DYNAMIC_HARD_CAP` (600 steps) instead of Infinity.
|
|
99
|
+
* An unbounded ceiling turned every hole in the progress heuristic into a literal
|
|
100
|
+
* infinite loop (e.g. a model cycling successful reads forever); a large finite cap
|
|
101
|
+
* keeps long autonomous runs alive while converting a pathological spin into a
|
|
102
|
+
* consolidation wrap-up. Setting `JEO_STEP_EXTENSIONS` / `JEO_STEP_HARD_CAP`
|
|
103
|
+
* restores a fully bounded budget; caller overrides win over both.
|
|
104
|
+
*/
|
|
105
|
+
export const DYNAMIC_HARD_CAP = 600;
|
|
106
|
+
|
|
107
|
+
export function dynamicStepBudgetConfig(
|
|
108
|
+
env: EnvLike = process.env,
|
|
109
|
+
overrides?: Partial<StepBudgetConfig>,
|
|
110
|
+
): StepBudgetConfig {
|
|
111
|
+
const base = envNum(env, "JEO_STEP_BASE", 24, 1, 10_000);
|
|
112
|
+
const dynamic: Partial<StepBudgetConfig> = {};
|
|
113
|
+
if (!envSet(env, "JEO_STEP_EXTENSIONS")) dynamic.maxExtensions = Number.POSITIVE_INFINITY;
|
|
114
|
+
if (!envSet(env, "JEO_STEP_HARD_CAP")) dynamic.hardCap = Math.max(base, DYNAMIC_HARD_CAP);
|
|
115
|
+
return resolveStepBudgetConfig(base, env, { ...dynamic, ...overrides });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The step limit a dynamic turn starts from — seeds the `step N/M` display before
|
|
119
|
+
* the engine's onBudget extensions grow the denominator. */
|
|
120
|
+
export function initialDynamicStepLimit(env: EnvLike = process.env): number {
|
|
121
|
+
return dynamicStepBudgetConfig(env).baseSteps;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Fixed-size hash of a tool-call signature (two interleaved FNV-1a streams).
|
|
126
|
+
* Signature strings embed the full JSON arguments — a `write` call carries the
|
|
127
|
+
* whole file body — so per-turn bookkeeping (the novelty `seen` set, the scoring
|
|
128
|
+
* window, the engine's repeat/cycle guards) stores this digest instead. Bounds a
|
|
129
|
+
* long turn's signature memory at O(steps × ~14 bytes) without changing any
|
|
130
|
+
* equality semantics the guards rely on.
|
|
131
|
+
*/
|
|
132
|
+
export function hashSignature(s: string): string {
|
|
133
|
+
let h1 = 0x811c9dc5 | 0;
|
|
134
|
+
let h2 = 0x811c9dc5 | 0;
|
|
135
|
+
for (let i = 0; i < s.length; i++) {
|
|
136
|
+
const c = s.charCodeAt(i);
|
|
137
|
+
h1 = Math.imul(h1 ^ c, 0x01000193);
|
|
138
|
+
h2 = Math.imul(h2 ^ (c + i), 0x01000193);
|
|
139
|
+
}
|
|
140
|
+
return (h1 >>> 0).toString(36) + "." + (h2 >>> 0).toString(36);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export class StepBudget {
|
|
144
|
+
private readonly cfg: StepBudgetConfig;
|
|
145
|
+
private readonly window: { signature: string; success: boolean }[] = [];
|
|
146
|
+
private extensions = 0;
|
|
147
|
+
private currentLimit: number;
|
|
148
|
+
/** Every signature executed this turn — basis of the novelty rule. */
|
|
149
|
+
private readonly seen = new Set<string>();
|
|
150
|
+
/** Never-seen-before signatures recorded since the last granted extension.
|
|
151
|
+
* An extension requires ≥ 1: a window that merely CYCLES through previously
|
|
152
|
+
* executed calls (read A, read B, read A, …) is a spin, not progress, even
|
|
153
|
+
* when every call succeeds and the distinct-count check passes. */
|
|
154
|
+
private novelSinceExtension = 0;
|
|
155
|
+
|
|
156
|
+
constructor(cfg: StepBudgetConfig) {
|
|
157
|
+
this.cfg = cfg;
|
|
158
|
+
this.currentLimit = Math.min(cfg.baseSteps, cfg.hardCap);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The current step limit (base + granted extensions). */
|
|
162
|
+
limit(): number {
|
|
163
|
+
return this.currentLimit;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Extensions granted so far. */
|
|
167
|
+
extensionsUsed(): number {
|
|
168
|
+
return this.extensions;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Record an executed tool call (ring-buffered to the scoring window).
|
|
172
|
+
* Stored as a fixed-size digest — see `hashSignature` (memory bound). */
|
|
173
|
+
record(signature: string, success: boolean): void {
|
|
174
|
+
const sig = hashSignature(signature);
|
|
175
|
+
if (!this.seen.has(sig)) {
|
|
176
|
+
this.seen.add(sig);
|
|
177
|
+
this.novelSinceExtension++;
|
|
178
|
+
}
|
|
179
|
+
this.window.push({ signature: sig, success });
|
|
180
|
+
if (this.window.length > this.cfg.windowSize) this.window.shift();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Progress over the recent window: ok count, total, distinct signatures. */
|
|
184
|
+
progress(): { ok: number; total: number; distinct: number } {
|
|
185
|
+
const ok = this.window.filter(r => r.success).length;
|
|
186
|
+
const distinct = new Set(this.window.map(r => r.signature)).size;
|
|
187
|
+
return { ok, total: this.window.length, distinct };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Called when the step counter reaches the current limit. Grants a bounded
|
|
192
|
+
* extension when the recent window shows real progress; otherwise declines
|
|
193
|
+
* with an explicit fail-fast reason (the loop then consolidates).
|
|
194
|
+
*/
|
|
195
|
+
tryExtend(): ExtensionDecision {
|
|
196
|
+
const decline = (why: string): ExtensionDecision => ({
|
|
197
|
+
extend: false,
|
|
198
|
+
reason: why,
|
|
199
|
+
limit: this.currentLimit,
|
|
200
|
+
});
|
|
201
|
+
if (this.cfg.maxExtensions <= 0) return decline("step extensions disabled");
|
|
202
|
+
if (this.extensions >= this.cfg.maxExtensions) {
|
|
203
|
+
return decline(`extension budget exhausted (${this.extensions}/${this.cfg.maxExtensions})`);
|
|
204
|
+
}
|
|
205
|
+
if (this.currentLimit >= this.cfg.hardCap) {
|
|
206
|
+
return decline(`hard step cap ${this.cfg.hardCap} reached`);
|
|
207
|
+
}
|
|
208
|
+
const p = this.progress();
|
|
209
|
+
if (p.total < 2) return decline(`not enough recent tool activity (${p.total} call(s))`);
|
|
210
|
+
const ratio = p.ok / p.total;
|
|
211
|
+
if (ratio < this.cfg.minProgressRatio || p.distinct < this.cfg.minDistinct) {
|
|
212
|
+
return decline(
|
|
213
|
+
`no recent progress (${p.ok}/${p.total} ok, ${p.distinct} distinct target(s))`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (this.novelSinceExtension < 1) {
|
|
217
|
+
return decline(
|
|
218
|
+
`no novel tool calls since the last extension (cycling through ${p.distinct} repeated target(s))`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
this.extensions++;
|
|
222
|
+
this.novelSinceExtension = 0;
|
|
223
|
+
this.currentLimit = Math.min(this.currentLimit + this.cfg.extensionSteps, this.cfg.hardCap);
|
|
224
|
+
return {
|
|
225
|
+
extend: true,
|
|
226
|
+
reason:
|
|
227
|
+
`progress detected (${p.ok}/${p.total} recent tools ok, ${p.distinct} targets) — ` +
|
|
228
|
+
`step budget extended to ${this.currentLimit} (extension ${this.extensions}/${this.cfg.maxExtensions})`,
|
|
229
|
+
limit: this.currentLimit,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
package/src/agent/subagents.ts
CHANGED
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
* Subagent role registry (gjc role-agent parity: executor / planner / architect /
|
|
3
3
|
* critic). A "subagent" is the executor tool-loop driven with a role-specific
|
|
4
4
|
* system prompt, model, step budget, and toolset. The registry is pure data so
|
|
5
|
-
* it can be listed in the TUI (`/agents`) and consumed by `
|
|
5
|
+
* it can be listed in the TUI (`/agents`) and consumed by `jeo team` without
|
|
6
6
|
* importing any provider or I/O code.
|
|
7
7
|
*
|
|
8
8
|
* Read-only roles (planner/architect/critic) get a mutation-free toolset so a
|
|
9
9
|
* review/plan lane physically cannot edit the repo, mirroring gjc's read-only
|
|
10
10
|
* role agents.
|
|
11
11
|
*/
|
|
12
|
-
import { DEFAULT_TOOLS,
|
|
12
|
+
import { DEFAULT_TOOLS, TOOL_PROTOCOL, READONLY_TOOL_PROTOCOL, WORKING_DISCIPLINE, type ToolHandler } from "./engine";
|
|
13
13
|
import type { Config } from "./state";
|
|
14
|
+
import architectPrompt from "../prompts/agents/architect.md" with { type: "text" };
|
|
15
|
+
import criticPrompt from "../prompts/agents/critic.md" with { type: "text" };
|
|
16
|
+
import executorPrompt from "../prompts/agents/executor.md" with { type: "text" };
|
|
17
|
+
import plannerPrompt from "../prompts/agents/planner.md" with { type: "text" };
|
|
14
18
|
|
|
15
19
|
export interface SubagentRole {
|
|
16
20
|
/** Stable id used in config + `/agents <id>`. */
|
|
@@ -23,6 +27,16 @@ export interface SubagentRole {
|
|
|
23
27
|
readOnly: boolean;
|
|
24
28
|
/** Default tool-loop step budget for this role. */
|
|
25
29
|
defaultMaxSteps: number;
|
|
30
|
+
/** Role-specific prompt template. */
|
|
31
|
+
prompt: string;
|
|
32
|
+
/** Required markers that must appear in done.reason. */
|
|
33
|
+
requiredDoneMarkers?: string[];
|
|
34
|
+
/** When set on a MUTATING role, bash is wrapped so EVERY shell segment must
|
|
35
|
+
* start with one of these prefixes (after stripping leading env-assignments /
|
|
36
|
+
* `sudo`); anything else is rejected. The registry definition IS the runtime
|
|
37
|
+
* constraint — no config plumbing. Undefined = unconstrained bash. Read-only
|
|
38
|
+
* roles drop bash entirely regardless. (plan/gjc-inheritance.md cycle 10, B5) */
|
|
39
|
+
bashAllowedPrefixes?: string[];
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
/** The four bundled subagent roles. `executor` is the only mutating role. */
|
|
@@ -33,6 +47,8 @@ export const SUBAGENT_ROLES: readonly SubagentRole[] = [
|
|
|
33
47
|
description: "Bounded implementation, refactors, fixes, and verification-ready edits.",
|
|
34
48
|
readOnly: false,
|
|
35
49
|
defaultMaxSteps: 15,
|
|
50
|
+
prompt: executorPrompt,
|
|
51
|
+
requiredDoneMarkers: ["Summary:", "Changed Files:", "Verification:"],
|
|
36
52
|
},
|
|
37
53
|
{
|
|
38
54
|
id: "planner",
|
|
@@ -40,6 +56,17 @@ export const SUBAGENT_ROLES: readonly SubagentRole[] = [
|
|
|
40
56
|
description: "Read-only sequencing, acceptance criteria, risks, and handoff shape.",
|
|
41
57
|
readOnly: true,
|
|
42
58
|
defaultMaxSteps: 10,
|
|
59
|
+
prompt: plannerPrompt,
|
|
60
|
+
requiredDoneMarkers: [
|
|
61
|
+
"Summary:",
|
|
62
|
+
"In Scope:",
|
|
63
|
+
"Out of Scope:",
|
|
64
|
+
"File-level Changes:",
|
|
65
|
+
"Sequencing:",
|
|
66
|
+
"Acceptance Criteria:",
|
|
67
|
+
"Verification:",
|
|
68
|
+
"Risks:",
|
|
69
|
+
],
|
|
43
70
|
},
|
|
44
71
|
{
|
|
45
72
|
id: "architect",
|
|
@@ -47,6 +74,14 @@ export const SUBAGENT_ROLES: readonly SubagentRole[] = [
|
|
|
47
74
|
description: "Read-only architecture and code review with severity-rated findings.",
|
|
48
75
|
readOnly: true,
|
|
49
76
|
defaultMaxSteps: 10,
|
|
77
|
+
prompt: architectPrompt,
|
|
78
|
+
requiredDoneMarkers: [
|
|
79
|
+
"Summary:",
|
|
80
|
+
"Findings:",
|
|
81
|
+
"Recommendations:",
|
|
82
|
+
"Architectural Status:",
|
|
83
|
+
"Code Review Recommendation:",
|
|
84
|
+
],
|
|
50
85
|
},
|
|
51
86
|
{
|
|
52
87
|
id: "critic",
|
|
@@ -54,20 +89,64 @@ export const SUBAGENT_ROLES: readonly SubagentRole[] = [
|
|
|
54
89
|
description: "Read-only plan critic; approves only actionable, verifiable plans.",
|
|
55
90
|
readOnly: true,
|
|
56
91
|
defaultMaxSteps: 8,
|
|
92
|
+
prompt: criticPrompt,
|
|
93
|
+
requiredDoneMarkers: ["Justification:"],
|
|
57
94
|
},
|
|
58
95
|
];
|
|
59
96
|
|
|
60
97
|
const DEFAULT_ROLE_ID = "executor";
|
|
61
98
|
|
|
99
|
+
/** Generic prompt template for CONFIG-DECLARED custom roles (no per-role .md
|
|
100
|
+
* bundled). Uses the same {{…}} variables as the bundled templates, so the
|
|
101
|
+
* whole role pipeline stays template-driven rather than hardcoded. */
|
|
102
|
+
const CUSTOM_ROLE_PROMPT = `You are the {{ROLE_TITLE}} subagent: {{ROLE_DESCRIPTION}}
|
|
103
|
+
|
|
104
|
+
{{TOOL_PROTOCOL}}
|
|
105
|
+
|
|
106
|
+
Work strictly within your assignment. When finished, reply
|
|
107
|
+
{"tool":"done","arguments":{"reason":"Summary: <what you found/did>"}}.`;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* SYSTEM-driven role registry: config.subagents entries that DECLARE a role
|
|
111
|
+
* identity (a \`prompt\`, \`title\`, or \`description\`) under an id that is not
|
|
112
|
+
* bundled become first-class roles — no code change needed to add one. Bare
|
|
113
|
+
* model/steps pins on unknown ids are NOT roles (typo safety). Safety default:
|
|
114
|
+
* a custom role is READ-ONLY unless it explicitly sets \`readOnly: false\`.
|
|
115
|
+
*/
|
|
116
|
+
export function rolesFromConfig(config: Pick<Config, "subagents">): SubagentRole[] {
|
|
117
|
+
const custom: SubagentRole[] = [];
|
|
118
|
+
for (const [rawId, entry] of Object.entries(config.subagents ?? {})) {
|
|
119
|
+
const id = normalizeRoleId(rawId);
|
|
120
|
+
if (!id || SUBAGENT_ROLES.some(r => r.id === id)) continue;
|
|
121
|
+
if (!entry || (entry.prompt === undefined && entry.title === undefined && entry.description === undefined)) continue;
|
|
122
|
+
const title = entry.title ?? id.charAt(0).toUpperCase() + id.slice(1);
|
|
123
|
+
custom.push({
|
|
124
|
+
id,
|
|
125
|
+
title,
|
|
126
|
+
description: entry.description ?? `Custom role "${id}" (declared in config.subagents).`,
|
|
127
|
+
readOnly: entry.readOnly ?? true,
|
|
128
|
+
defaultMaxSteps: typeof entry.maxSteps === "number" && entry.maxSteps > 0 ? entry.maxSteps : 12,
|
|
129
|
+
prompt: (entry.prompt ?? CUSTOM_ROLE_PROMPT).replaceAll("{{ROLE_DESCRIPTION}}", entry.description ?? title),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return custom;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Bundled roles + config-declared custom roles (the full live registry). */
|
|
136
|
+
export function allSubagentRoles(config?: Pick<Config, "subagents">): SubagentRole[] {
|
|
137
|
+
return config ? [...SUBAGENT_ROLES, ...rolesFromConfig(config)] : [...SUBAGENT_ROLES];
|
|
138
|
+
}
|
|
139
|
+
|
|
62
140
|
/** Normalize loosely-typed role input (case-insensitive, trimmed). */
|
|
63
141
|
export function normalizeRoleId(input: string | undefined | null): string {
|
|
64
142
|
return (input ?? "").trim().toLowerCase();
|
|
65
143
|
}
|
|
66
144
|
|
|
67
|
-
/** Look up a role by id (case-insensitive)
|
|
68
|
-
|
|
145
|
+
/** Look up a role by id (case-insensitive) across the bundled registry and —
|
|
146
|
+
* when a config is supplied — config-declared custom roles. */
|
|
147
|
+
export function getSubagentRole(id: string | undefined | null, config?: Pick<Config, "subagents">): SubagentRole | undefined {
|
|
69
148
|
const want = normalizeRoleId(id);
|
|
70
|
-
return
|
|
149
|
+
return allSubagentRoles(config).find(r => r.id === want);
|
|
71
150
|
}
|
|
72
151
|
|
|
73
152
|
/** The default role (`executor`) used when none is specified. */
|
|
@@ -81,41 +160,112 @@ export type SubagentConfig = NonNullable<Config["subagents"]>;
|
|
|
81
160
|
/** Per-role model override → falls back to the global default model. */
|
|
82
161
|
export function resolveSubagentModel(roleId: string, config: Pick<Config, "defaultModel" | "subagents">): string {
|
|
83
162
|
const entry = config.subagents?.[normalizeRoleId(roleId)];
|
|
84
|
-
return entry?.model
|
|
163
|
+
return entry?.model || config.defaultModel;
|
|
85
164
|
}
|
|
86
165
|
|
|
87
166
|
/** Per-role step budget → config override, else the role default, else 15. */
|
|
88
167
|
export function resolveSubagentMaxSteps(roleId: string, config: Pick<Config, "subagents">): number {
|
|
89
168
|
const entry = config.subagents?.[normalizeRoleId(roleId)];
|
|
90
169
|
if (typeof entry?.maxSteps === "number" && entry.maxSteps > 0) return entry.maxSteps;
|
|
91
|
-
return getSubagentRole(roleId)?.defaultMaxSteps ?? 15;
|
|
170
|
+
return getSubagentRole(roleId, config)?.defaultMaxSteps ?? 15;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function renderRolePrompt(template: string, role: SubagentRole): string {
|
|
174
|
+
return template
|
|
175
|
+
.replaceAll("{{TOOL_PROTOCOL}}", `${TOOL_PROTOCOL}\n\n${WORKING_DISCIPLINE}`)
|
|
176
|
+
.replaceAll("{{READONLY_TOOL_PROTOCOL}}", READONLY_TOOL_PROTOCOL)
|
|
177
|
+
.replaceAll("{{ROLE_TITLE}}", role.title)
|
|
178
|
+
.trim();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function validateSubagentDoneReason(role: SubagentRole, reason: string | undefined): { ok: boolean; missing?: string[] } {
|
|
182
|
+
const trimmed = (reason ?? "").trim();
|
|
183
|
+
if (!trimmed) return { ok: false, missing: ["done.reason"] };
|
|
184
|
+
if (role.id === "critic") {
|
|
185
|
+
const verdicts = ["[OKAY]", "[ITERATE]", "[REJECT]"];
|
|
186
|
+
const hasVerdict = verdicts.some(marker => trimmed.startsWith(marker));
|
|
187
|
+
const missing = [
|
|
188
|
+
...(hasVerdict ? [] : ["[OKAY]|[ITERATE]|[REJECT]"]),
|
|
189
|
+
...((role.requiredDoneMarkers ?? []).filter(marker => !trimmed.includes(marker))),
|
|
190
|
+
];
|
|
191
|
+
return { ok: missing.length === 0, missing };
|
|
192
|
+
}
|
|
193
|
+
const missing = (role.requiredDoneMarkers ?? []).filter(marker => !trimmed.includes(marker));
|
|
194
|
+
return { ok: missing.length === 0, missing };
|
|
92
195
|
}
|
|
93
196
|
|
|
94
|
-
/** Build a role-specific
|
|
197
|
+
/** Build a role-specific system prompt from its dedicated template. */
|
|
95
198
|
export function subagentSystemPrompt(role: SubagentRole): string {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
199
|
+
return renderRolePrompt(role.prompt, role);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* True when EVERY shell segment of `command` starts with one of `prefixes`
|
|
204
|
+
* (after stripping leading env-assignments and a leading `sudo`). Splitting on
|
|
205
|
+
* `; && || |` means a chained `… && rm -rf` cannot smuggle an un-vetted command
|
|
206
|
+
* past a single allowed head. Command substitution (`$(…)`, backticks) can't be
|
|
207
|
+
* statically vetted, so it is rejected outright. Empty allowlist = unconstrained.
|
|
208
|
+
*/
|
|
209
|
+
export function bashCommandAllowed(command: string, prefixes: string[]): boolean {
|
|
210
|
+
if (prefixes.length === 0) return true;
|
|
211
|
+
if (/[`]|\$\(/.test(command)) return false; // un-vettable command substitution
|
|
212
|
+
const matches = (seg: string, p: string) =>
|
|
213
|
+
seg === p || seg.startsWith(p + " ") || seg.startsWith(p + "\t");
|
|
214
|
+
for (const raw of command.split(/(?:&&|\|\||[;|])/)) {
|
|
215
|
+
let seg = raw.trim();
|
|
216
|
+
if (seg === "") continue;
|
|
217
|
+
seg = seg
|
|
218
|
+
.replace(/^(?:\w+=(?:"[^"]*"|'[^']*'|\S+)\s+)+/, "") // FOO=bar BAZ=qux cmd
|
|
219
|
+
.replace(/^sudo\s+/, "")
|
|
220
|
+
.trim();
|
|
221
|
+
if (seg === "" || !prefixes.some(p => matches(seg, p))) return false;
|
|
222
|
+
}
|
|
223
|
+
return true;
|
|
103
224
|
}
|
|
104
225
|
|
|
105
|
-
/**
|
|
226
|
+
/**
|
|
227
|
+
* Toolset for a role:
|
|
228
|
+
* - Read-only roles drop ALL mutating tools (write/edit AND bash) so a
|
|
229
|
+
* review/plan lane physically cannot change the repo.
|
|
230
|
+
* - A mutating role with `bashAllowedPrefixes` gets bash replaced by a
|
|
231
|
+
* prefix-checking wrapper (registry definition = runtime constraint).
|
|
232
|
+
* - Otherwise the full default toolset (unconstrained bash) is returned.
|
|
233
|
+
*/
|
|
106
234
|
export function subagentToolset(role: SubagentRole): Record<string, ToolHandler> {
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
235
|
+
if (role.readOnly) {
|
|
236
|
+
const MUTATING = new Set(["write", "edit", "bash", "mkdir", "delete"]);
|
|
237
|
+
const ro: Record<string, ToolHandler> = {};
|
|
238
|
+
for (const [name, handler] of Object.entries(DEFAULT_TOOLS)) {
|
|
239
|
+
if (MUTATING.has(name)) continue;
|
|
240
|
+
ro[name] = handler;
|
|
241
|
+
}
|
|
242
|
+
return ro;
|
|
112
243
|
}
|
|
113
|
-
|
|
244
|
+
const prefixes = role.bashAllowedPrefixes;
|
|
245
|
+
if (prefixes && prefixes.length > 0) {
|
|
246
|
+
const inner = DEFAULT_TOOLS.bash;
|
|
247
|
+
const guarded: Record<string, ToolHandler> = { ...DEFAULT_TOOLS };
|
|
248
|
+
guarded.bash = async (a, cwd) => {
|
|
249
|
+
const command = String(a.command ?? a.cmd ?? "");
|
|
250
|
+
if (!bashCommandAllowed(command, prefixes)) {
|
|
251
|
+
return {
|
|
252
|
+
success: false,
|
|
253
|
+
output: "",
|
|
254
|
+
error:
|
|
255
|
+
`bash rejected for role '${role.id}': every command segment must start with one of [${prefixes.join(", ")}]. ` +
|
|
256
|
+
`Received: ${command.trim().slice(0, 120)}`,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return inner(a, cwd);
|
|
260
|
+
};
|
|
261
|
+
return guarded;
|
|
262
|
+
}
|
|
263
|
+
return DEFAULT_TOOLS;
|
|
114
264
|
}
|
|
115
265
|
|
|
116
|
-
/** All role ids (for
|
|
117
|
-
export function subagentRoleIds(): string[] {
|
|
118
|
-
return
|
|
266
|
+
/** All role ids — bundled + config-declared (for autocomplete + validation). */
|
|
267
|
+
export function subagentRoleIds(config?: Pick<Config, "subagents">): string[] {
|
|
268
|
+
return allSubagentRoles(config).map(r => r.id);
|
|
119
269
|
}
|
|
120
270
|
|
|
121
271
|
/** Parse a `/agents <role> maxSteps <n>` value → positive int, else undefined. */
|
|
@@ -129,10 +279,21 @@ export function parseMaxSteps(input: string | undefined): number | undefined {
|
|
|
129
279
|
* maxSteps). Pure — does not mutate `config`. Unknown roles are rejected by the
|
|
130
280
|
* caller via `getSubagentRole`; this helper trusts the id it is given.
|
|
131
281
|
*/
|
|
282
|
+
export type ThinkLevelValue = "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
283
|
+
|
|
284
|
+
/** Per-role reasoning level → explicit role override, else undefined (= INHERIT
|
|
285
|
+
* the global thinkingLevel at call time — gjc's "(inherit)" semantics). */
|
|
286
|
+
export function resolveSubagentThinking(
|
|
287
|
+
roleId: string,
|
|
288
|
+
config: Pick<Config, "subagents">,
|
|
289
|
+
): ThinkLevelValue | undefined {
|
|
290
|
+
return config.subagents?.[normalizeRoleId(roleId)]?.thinking;
|
|
291
|
+
}
|
|
292
|
+
|
|
132
293
|
export function withSubagentSetting(
|
|
133
294
|
config: Pick<Config, "subagents">,
|
|
134
295
|
roleId: string,
|
|
135
|
-
patch: { model?: string; maxSteps?: number },
|
|
296
|
+
patch: { model?: string; maxSteps?: number; thinking?: ThinkLevelValue },
|
|
136
297
|
): SubagentConfig {
|
|
137
298
|
const id = normalizeRoleId(roleId);
|
|
138
299
|
const subs: SubagentConfig = { ...(config.subagents ?? {}) };
|
|
@@ -147,3 +308,39 @@ export function clearSubagentSetting(config: Pick<Config, "subagents">, roleId:
|
|
|
147
308
|
delete subs[id];
|
|
148
309
|
return subs;
|
|
149
310
|
}
|
|
311
|
+
|
|
312
|
+
/** One pickable apply-target: the global default or a subagent role. */
|
|
313
|
+
export interface ApplyTargetChoice {
|
|
314
|
+
/** "default" or a subagent role id. */
|
|
315
|
+
value: string;
|
|
316
|
+
label: string;
|
|
317
|
+
/** Right-aligned hint: the target's CURRENT model (so the picker doubles as a viewer). */
|
|
318
|
+
hint: string;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Choices for the "apply picked model to…" picker shown after an interactive
|
|
323
|
+
* model selection (gjc parity: picking a model also lets you pick WHO uses it).
|
|
324
|
+
* The hint shows each target's current model, so the same picker doubles as a
|
|
325
|
+
* read-and-change panel for existing role assignments. Pure — testable.
|
|
326
|
+
*/
|
|
327
|
+
export function applyTargetChoices(
|
|
328
|
+
config: Pick<Config, "defaultModel" | "subagents" | "thinkingLevel">,
|
|
329
|
+
): ApplyTargetChoice[] {
|
|
330
|
+
const roleThink = (id: string): string => {
|
|
331
|
+
const t = resolveSubagentThinking(id, config);
|
|
332
|
+
return t ? ` (${t})` : " (inherit)";
|
|
333
|
+
};
|
|
334
|
+
return [
|
|
335
|
+
{
|
|
336
|
+
value: "default",
|
|
337
|
+
label: "default — every session",
|
|
338
|
+
hint: `${config.defaultModel} (${config.thinkingLevel ?? "medium"})`,
|
|
339
|
+
},
|
|
340
|
+
...allSubagentRoles(config).map(role => ({
|
|
341
|
+
value: role.id,
|
|
342
|
+
label: `subagent ${role.id} — ${role.title}`,
|
|
343
|
+
hint: resolveSubagentModel(role.id, config) + (config.subagents?.[role.id]?.model ? "" : " (default)") + roleThink(role.id),
|
|
344
|
+
})),
|
|
345
|
+
];
|
|
346
|
+
}
|