jeo-code 0.4.5 → 0.4.7
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 +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/package.json +1 -1
- package/src/agent/dev/evolution-bridge.ts +36 -3
- package/src/agent/dev/self-analysis.ts +6 -1
- package/src/agent/engine.ts +76 -71
- package/src/agent/loop.ts +2 -0
- package/src/agent/step-budget.ts +10 -0
- package/src/agent/subagent-registry.ts +131 -0
- package/src/agent/subagent-tool.ts +89 -0
- package/src/agent/subagents.ts +22 -3
- package/src/agent/task-tool.ts +123 -19
- package/src/agent/tool-output.ts +115 -0
- package/src/agent/tools.ts +42 -8
- package/src/ai/model-manager.ts +9 -14
- package/src/ai/model-registry.ts +8 -3
- package/src/ai/providers/antigravity.ts +11 -2
- package/src/ai/providers/gemini.ts +12 -2
- package/src/ai/register-providers.ts +21 -0
- package/src/ai/types.ts +4 -0
- package/src/cli/runner.ts +0 -9
- package/src/commands/launch.ts +157 -52
- package/src/commands/team.ts +13 -6
- package/src/skills/catalog.ts +0 -2
- package/src/tui/app.ts +131 -20
- package/src/tui/components/forge.ts +25 -7
- package/src/tui/components/input-box.ts +8 -3
- package/src/tui/components/markdown-text.ts +10 -1
- package/src/tui/components/themes.ts +57 -1
- package/src/tui/components/todo-card.ts +44 -13
- package/src/tui/monitoring/hud-view.ts +53 -30
- package/src/util/update-check.ts +53 -0
- package/src/commands/gjc.ts +0 -52
- package/src/prompts/skills/gjc/AGENTS.md +0 -31
- package/src/prompts/skills/gjc/SKILL.md +0 -15
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in provider registration (the registry bootstrap).
|
|
3
|
+
*
|
|
4
|
+
* Importing this module for its side effect registers every bundled LLM adapter
|
|
5
|
+
* into the shared `providerRegistry`. `model-manager` then resolves adapters
|
|
6
|
+
* through the registry alone — it no longer imports, or even names, concrete
|
|
7
|
+
* providers. To add a new built-in provider, register it HERE only; nothing in
|
|
8
|
+
* `model-manager` changes.
|
|
9
|
+
*/
|
|
10
|
+
import { providerRegistry } from "./provider-registry";
|
|
11
|
+
import { anthropicAdapter } from "./providers/anthropic";
|
|
12
|
+
import { openaiAdapter } from "./providers/openai";
|
|
13
|
+
import { geminiAdapter } from "./providers/gemini";
|
|
14
|
+
import { ollamaAdapter } from "./providers/ollama";
|
|
15
|
+
import { antigravityAdapter } from "./providers/antigravity";
|
|
16
|
+
|
|
17
|
+
providerRegistry.register("anthropic", anthropicAdapter);
|
|
18
|
+
providerRegistry.register("openai", openaiAdapter);
|
|
19
|
+
providerRegistry.register("gemini", geminiAdapter);
|
|
20
|
+
providerRegistry.register("antigravity", antigravityAdapter);
|
|
21
|
+
providerRegistry.register("ollama", ollamaAdapter);
|
package/src/ai/types.ts
CHANGED
|
@@ -43,6 +43,10 @@ export interface CallOptions {
|
|
|
43
43
|
/** Notified before each auto-retry backoff wait (rate limits / transient errors).
|
|
44
44
|
* NOT forwarded to provider adapters — consumed by the manager's retry layer. */
|
|
45
45
|
onRetry?: (attempt: number, err: unknown, delayMs: number) => void;
|
|
46
|
+
/** Streaming sink for native model reasoning/thinking text deltas (separate from the
|
|
47
|
+
* answer text). Surfaced as a transient dimmed view; absent for models that emit no
|
|
48
|
+
* thought text. */
|
|
49
|
+
onReasoning?: (delta: string) => void;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
export interface ProviderAdapter {
|
package/src/cli/runner.ts
CHANGED
|
@@ -172,15 +172,6 @@ export const COMMANDS: readonly CommandSpec[] = [
|
|
|
172
172
|
return args => m.runUpdateCommand(args);
|
|
173
173
|
},
|
|
174
174
|
},
|
|
175
|
-
{
|
|
176
|
-
name: "gjc",
|
|
177
|
-
summary: "Run the gjc workflow skill as an autonomous build loop (plan → implement → verify).",
|
|
178
|
-
usage: "gjc \"<intent>\"",
|
|
179
|
-
loader: async () => {
|
|
180
|
-
const m = await import("../commands/gjc");
|
|
181
|
-
return args => m.runGjcCommand(args);
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
175
|
{
|
|
185
176
|
name: "ooo-seed",
|
|
186
177
|
summary: "Generate an immutable ooo seed from a specification (spec-first automation).",
|
package/src/commands/launch.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { runAgentLoop, executorSystemPrompt, DEFAULT_TOOLS, TOOL_PROTOCOL, WORKI
|
|
|
3
3
|
import { initialDynamicStepLimit } from "../agent/step-budget";
|
|
4
4
|
import { memoryPromptSection, spawnDetachedDistill } from "../agent/memory";
|
|
5
5
|
import { createTaskTool, taskToolProtocolLine, type TaskSubEvent } from "../agent/task-tool";
|
|
6
|
+
import { createSubagentTool, SUBAGENT_TOOL_PROTOCOL_LINE } from "../agent/subagent-tool";
|
|
7
|
+
import { SubagentRegistry } from "../agent/subagent-registry";
|
|
6
8
|
import { createTodoTool, TODO_TOOL_PROTOCOL_LINE } from "../agent/todo-tool";
|
|
7
9
|
import { LaunchTui } from "../tui/app";
|
|
8
10
|
import { runDeepInterviewEngine } from "./deep-interview";
|
|
@@ -19,7 +21,7 @@ import { staticCompletionContext, readlineCompleter, formatCompletionPreview, to
|
|
|
19
21
|
import { EVOLUTION_STAGES, animateAsciiArt } from "../tui/components/ascii-art";
|
|
20
22
|
import { getEvolutionTip } from "../tui/components/evolution";
|
|
21
23
|
import { renderWelcome, playWelcomeSweep } from "../tui/components/welcome";
|
|
22
|
-
import { checkForUpdate } from "../util/update-check";
|
|
24
|
+
import { checkForUpdate, readUpdateCache, writeUpdateCache } from "../util/update-check";
|
|
23
25
|
import { jeoEnv } from "../util/env";
|
|
24
26
|
import { renderUpdateBox } from "../tui/components/update-box";
|
|
25
27
|
import { supportsUnicode } from "../tui/components/capability";
|
|
@@ -379,7 +381,8 @@ function streamResultSuffix(tool: string, ok: boolean, output: string | undefine
|
|
|
379
381
|
|
|
380
382
|
export function formatTaskSubEvent(e: TaskSubEvent): string {
|
|
381
383
|
const role = e.role || "subagent";
|
|
382
|
-
const roleLabel = role.toUpperCase();
|
|
384
|
+
const roleLabel = e.index && e.total ? `${role.toUpperCase()}[${e.index}/${e.total}]` : role.toUpperCase();
|
|
385
|
+
const tokTag = e.tokens ? ` (${e.tokens.input + e.tokens.output} tok)` : "";
|
|
383
386
|
const detail = firstOutputLine(e.detail);
|
|
384
387
|
const summary = e.summary ? ` — ${e.summary}` : "";
|
|
385
388
|
// No ` step N/M` marker — step counters carry no meaning under the dynamic
|
|
@@ -390,7 +393,7 @@ export function formatTaskSubEvent(e: TaskSubEvent): string {
|
|
|
390
393
|
if (e.kind === "step") return ` ${badge} ${chalk.cyan(`├─ ${roleLabel}`)} · ${detail || "working"}`;
|
|
391
394
|
if (e.kind === "tool") return ` ${badge} ${e.success === false ? chalk.red("├─") : chalk.green("├─")} ${roleLabel} ${e.success === false ? chalk.red("✗") : chalk.green("✓")} ${detail || "tool"}${summary}`;
|
|
392
395
|
if (e.kind === "error") return ` ${badge} ${chalk.red("├─")} ${roleLabel} ${chalk.red("✗")} ${detail || "error"}`;
|
|
393
|
-
return `${badge} ${e.success === false ? chalk.red("└─") : chalk.green("└─")} ${roleLabel} done${e.success === false ? " (incomplete)" : ""}${detail ? `: ${detail}` : ""}`;
|
|
396
|
+
return `${badge} ${e.success === false ? chalk.red("└─") : chalk.green("└─")} ${roleLabel} done${tokTag}${e.success === false ? " (incomplete)" : ""}${detail ? `: ${detail}` : ""}`;
|
|
394
397
|
}
|
|
395
398
|
|
|
396
399
|
function logTaskSubEvent(e: TaskSubEvent, log: (line: string) => void = (s: string) => console.log(s)): void {
|
|
@@ -1141,7 +1144,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1141
1144
|
// pi-style: load project context (JEO.md / AGENTS.md / .jeo/context.md / CLAUDE.md) into the prompt.
|
|
1142
1145
|
const contextFiles = await loadProjectContext(cwd);
|
|
1143
1146
|
|
|
1144
|
-
const KNOWN_TOOLS = new Set(["read", "write", "edit", "bash", "find", "search", "ls", "task", "todo"]);
|
|
1147
|
+
const KNOWN_TOOLS = new Set(["read", "write", "edit", "bash", "find", "search", "ls", "task", "todo", "subagent"]);
|
|
1145
1148
|
let allowedTools = new Set(KNOWN_TOOLS);
|
|
1146
1149
|
|
|
1147
1150
|
if (flags.noTools) {
|
|
@@ -1196,6 +1199,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1196
1199
|
(allowedTools.has("task") ? "\n\nDelegation: " + taskToolProtocolLine(cfg) +
|
|
1197
1200
|
" Call task with {\"role\": <one of the advertised roles>, \"task\": <assignment>, \"context\": <optional>} to hand a focused slice to a subagent." : "") +
|
|
1198
1201
|
(allowedTools.has("todo") ? "\n\nPlanning: " + TODO_TOOL_PROTOCOL_LINE : "") +
|
|
1202
|
+
(allowedTools.has("subagent") ? "\n\nDetached subagents: " + SUBAGENT_TOOL_PROTOCOL_LINE +
|
|
1203
|
+
" Launch background work with task {\"detached\": true, \"role\": <role>, \"task\": <assignment>}; it returns a subagent id immediately so you can keep working and collect the result later." : "") +
|
|
1199
1204
|
(effectiveNoSkills ? "" :
|
|
1200
1205
|
"\n\nJEO workflow routing:\n" +
|
|
1201
1206
|
"- Answer the user's request DIRECTLY. Never reply with a catalog, list, or summary of skills unless the user explicitly asks what skills exist.\n" +
|
|
@@ -1297,6 +1302,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1297
1302
|
// box during a running turn so typed text stays in the same query surface
|
|
1298
1303
|
// instead of a separate queued row.
|
|
1299
1304
|
let queueBusySnapshot: (() => { text: string }) | undefined;
|
|
1305
|
+
// Clears the live next-prompt draft — used after a mid-turn Enter is lifted into
|
|
1306
|
+
// the steering inbox so the consumed line does not also become the next prompt.
|
|
1307
|
+
let queueBusyClear: (() => void) | undefined;
|
|
1300
1308
|
let interactiveTurnActive = false;
|
|
1301
1309
|
|
|
1302
1310
|
|
|
@@ -1311,42 +1319,61 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1311
1319
|
const activeModel = sessionModel || turnConfig.defaultModel;
|
|
1312
1320
|
const contextTokens = catalogMetadata(activeModel)?.contextTokens;
|
|
1313
1321
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
});
|
|
1318
|
-
|
|
1319
|
-
if (compRes.error) {
|
|
1320
|
-
throw new Error(compRes.error);
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
if (compRes.compacted && sessionId && compRes.replacesThrough !== undefined) {
|
|
1324
|
-
const touchedNote = compRes.touchedFiles?.length ? ` Files touched: ${compRes.touchedFiles.join(", ")}.` : "";
|
|
1325
|
-
const summaryText = compRes.summary ?? `[Earlier conversation omitted: ${compRes.removed} messages — summary unavailable.${touchedNote}]`;
|
|
1326
|
-
await appendCompaction(sessionId, ++compactionSeq, summaryText, compRes.replacesThrough, cwd);
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
const beforeLen = history.length;
|
|
1330
|
-
if (images?.length && catalogMetadata(activeModel)?.images === false) {
|
|
1331
|
-
console.log(`! ${activeModel} does not advertise image input — sending the attachment anyway.`);
|
|
1332
|
-
}
|
|
1333
|
-
history.push(images?.length ? { role: "user", content: userInput, images } : { role: "user", content: userInput });
|
|
1334
|
-
|
|
1335
|
-
// `turnConfig` was read before compaction so both the compactor and delegated
|
|
1336
|
-
// task tool see mid-session config changes (e.g. `/agents <role> <model>`).
|
|
1337
|
-
const { provider: activeProvider } = await describeModel(activeModel);
|
|
1338
|
-
// Dirty count is recomputed at each turn start (gjc parity P1.B5: per-turn, not
|
|
1339
|
-
// per-render) so `?N` grows as the agent edits files; one spawn/turn, not per frame.
|
|
1322
|
+
// Resolve provider + dirty count up front — both are cheap and feed the live
|
|
1323
|
+
// frame's footer. `turnConfig` is reused so describeModel does NOT re-read the
|
|
1324
|
+
// config file. (gjc parity P1.B5: dirty count per-turn, not per-render.)
|
|
1325
|
+
const { provider: activeProvider } = await describeModel(activeModel, turnConfig);
|
|
1340
1326
|
const turnDirtyCount = branch ? gitDirtyCount(cwd) : undefined;
|
|
1341
1327
|
const tui = useTui ? new LaunchTui({ model: activeModel, provider: activeProvider, sessionId, maxSteps: initialStepLimit, cwd, branch, dirtyCount: turnDirtyCount, thinking: sessionThinking }) : null;
|
|
1342
|
-
tui?.setContextUsage(historyTokens(history), contextTokens);
|
|
1343
1328
|
tui?.setTurnTitle(userInput); // gjc-parity turn title → HUD + tmux pane title (no LLM call)
|
|
1329
|
+
// `beforeLen` marks where this turn's appended messages start; it is re-read
|
|
1330
|
+
// AFTER compaction (which mutates history) and consumed by the post-turn
|
|
1331
|
+
// persistence block below.
|
|
1332
|
+
let beforeLen = history.length;
|
|
1344
1333
|
let result;
|
|
1345
1334
|
try {
|
|
1335
|
+
// Paint the live frame + spinner the INSTANT the turn is accepted, BEFORE the
|
|
1336
|
+
// potentially slow / LLM-driven compaction below. Otherwise the gap between the
|
|
1337
|
+
// submitted prompt and the first feedback reads as a dead "no response" window
|
|
1338
|
+
// (the reported symptom). All remaining preflight runs UNDER the spinner now.
|
|
1346
1339
|
if (tui) {
|
|
1347
1340
|
interactiveTurnActive = true;
|
|
1348
1341
|
tui.start();
|
|
1349
1342
|
}
|
|
1343
|
+
const compRes = await maybeCompact(history, {
|
|
1344
|
+
model: sessionModel,
|
|
1345
|
+
contextTokens,
|
|
1346
|
+
});
|
|
1347
|
+
if (compRes.error) {
|
|
1348
|
+
throw new Error(compRes.error);
|
|
1349
|
+
}
|
|
1350
|
+
if (compRes.compacted && sessionId && compRes.replacesThrough !== undefined) {
|
|
1351
|
+
const touchedNote = compRes.touchedFiles?.length ? ` Files touched: ${compRes.touchedFiles.join(", ")}.` : "";
|
|
1352
|
+
const summaryText = compRes.summary ?? `[Earlier conversation omitted: ${compRes.removed} messages — summary unavailable.${touchedNote}]`;
|
|
1353
|
+
await appendCompaction(sessionId, ++compactionSeq, summaryText, compRes.replacesThrough, cwd);
|
|
1354
|
+
tui?.events().onNotice?.(`(compacted ${compRes.removed} older message${compRes.removed === 1 ? "" : "s"})`);
|
|
1355
|
+
}
|
|
1356
|
+
beforeLen = history.length;
|
|
1357
|
+
if (images?.length && catalogMetadata(activeModel)?.images === false) {
|
|
1358
|
+
const warn = `! ${activeModel} does not advertise image input — sending the attachment anyway.`;
|
|
1359
|
+
if (tui) tui.events().onNotice?.(warn);
|
|
1360
|
+
else console.log(warn);
|
|
1361
|
+
}
|
|
1362
|
+
history.push(images?.length ? { role: "user", content: userInput, images } : { role: "user", content: userInput });
|
|
1363
|
+
// Keep the submitted query in scrollback: the prompt that STARTS a turn shows
|
|
1364
|
+
// only as the transient HUD turn-title otherwise, which vanishes when the live
|
|
1365
|
+
// frame clears at turn-end — so the conversation transcript lost every user
|
|
1366
|
+
// prompt. Flush a `user` card (same surface as a mid-turn steer) so it persists.
|
|
1367
|
+
if (tui && userInput.trim()) tui.flushUserCard(userInput);
|
|
1368
|
+
tui?.setContextUsage(historyTokens(history), contextTokens);
|
|
1369
|
+
|
|
1370
|
+
// Per-turn steering inbox (gjc parity): additional queries typed mid-turn land
|
|
1371
|
+
// here and the engine drains them at each step boundary; createTaskTool forwards
|
|
1372
|
+
// the same drain so a single running subagent receives them live. Unconsumed
|
|
1373
|
+
// messages are folded into the next prompt in the finally block (race safety).
|
|
1374
|
+
const steerInbox: string[] = [];
|
|
1375
|
+
const steeringEnabled = !!tui && jeoEnv("NO_STEER") !== "1";
|
|
1376
|
+
const drainSteer = () => steerInbox.splice(0, steerInbox.length);
|
|
1350
1377
|
const harness = createInFlightAbortHarness({
|
|
1351
1378
|
captureEsc: !!tui,
|
|
1352
1379
|
onNoise: () => tui?.repaint(),
|
|
@@ -1368,11 +1395,38 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1368
1395
|
},
|
|
1369
1396
|
onBufferedInput: chunk => {
|
|
1370
1397
|
if (!tui) return;
|
|
1398
|
+
// gjc-style mid-turn steering: a typed Enter (outside a bracketed paste)
|
|
1399
|
+
// lifts the current draft into the steering inbox so the RUNNING turn picks
|
|
1400
|
+
// it up at the next step, instead of only becoming the next prompt.
|
|
1401
|
+
// JEO_NO_STEER=1 restores the legacy draft-only behavior.
|
|
1402
|
+
const typedEnter =
|
|
1403
|
+
steeringEnabled &&
|
|
1404
|
+
!(queueBusyPasteActive?.() ?? false) &&
|
|
1405
|
+
/[\r\n]/.test(chunk) &&
|
|
1406
|
+
!chunk.includes(PASTE_START) &&
|
|
1407
|
+
!chunk.includes(PASTE_END);
|
|
1371
1408
|
const captured = queueBusyInput?.(chunk) ?? false;
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1409
|
+
if (typedEnter) {
|
|
1410
|
+
const line = (queueBusySnapshot?.().text ?? "").trim();
|
|
1411
|
+
if (line) {
|
|
1412
|
+
steerInbox.push(line);
|
|
1413
|
+
queueBusyClear?.();
|
|
1414
|
+
tui.setLivePromptInput("");
|
|
1415
|
+
// Surface the steered query as a `user` card in scrollback so it reads
|
|
1416
|
+
// as an accepted input that started work — not just a transient notice.
|
|
1417
|
+
tui.flushSteerCard(line);
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
// Mid-turn additional input is committed (and shown) ONLY on Enter (above):
|
|
1422
|
+
// the running turn does NOT echo half-typed text per keystroke. Captured
|
|
1423
|
+
// printable input accumulates silently in the draft buffer and surfaces as a
|
|
1424
|
+
// `user` card the moment Enter lifts it into the steering inbox (or folds into
|
|
1425
|
+
// the next prompt if the turn ends first). JEO_LIVE_DRAFT=1 restores the
|
|
1426
|
+
// legacy live per-keystroke echo in the input box.
|
|
1427
|
+
if (captured && jeoEnv("LIVE_DRAFT") === "1") {
|
|
1428
|
+
tui.setLivePromptInput(queueBusySnapshot?.().text ?? "");
|
|
1429
|
+
}
|
|
1376
1430
|
},
|
|
1377
1431
|
onAbortNotice: msg => {
|
|
1378
1432
|
if (tui) tui.events().onNotice?.(msg);
|
|
@@ -1384,6 +1438,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1384
1438
|
},
|
|
1385
1439
|
});
|
|
1386
1440
|
const ac = harness.controller;
|
|
1441
|
+
// #9: per-turn registry for DETACHED subagents (task{detached:true}); the
|
|
1442
|
+
// `subagent` tool controls them and cancelAll() in finally prevents orphans.
|
|
1443
|
+
const subagentRegistry = new SubagentRegistry();
|
|
1387
1444
|
try {
|
|
1388
1445
|
// Per-turn todo snapshot: drives the done-time reconciliation gate (the
|
|
1389
1446
|
// Todos checklist used to end a finished turn stuck at "✓0 ◐1 ·4 / 5"
|
|
@@ -1403,11 +1460,14 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1403
1460
|
task: createTaskTool({
|
|
1404
1461
|
config: { ...turnConfig, defaultModel: activeModel },
|
|
1405
1462
|
signal: ac.signal,
|
|
1463
|
+
steer: drainSteer,
|
|
1464
|
+
registry: subagentRegistry,
|
|
1406
1465
|
onEvent: useTui
|
|
1407
1466
|
? (e => tui?.onSubagentEvent(e))
|
|
1408
1467
|
: (e => logTaskSubEvent(e)),
|
|
1409
1468
|
}),
|
|
1410
1469
|
todo: createTodoTool({ onChange: items => { turnTodos = items; tui?.setTodos(items); } }),
|
|
1470
|
+
subagent: createSubagentTool(subagentRegistry),
|
|
1411
1471
|
};
|
|
1412
1472
|
const tools = filterToolMap(fullTools, Array.from(allowedTools));
|
|
1413
1473
|
result = await runAgentLoop(history, {
|
|
@@ -1417,6 +1477,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1417
1477
|
model: sessionModel,
|
|
1418
1478
|
maxTokens: sessionThinking ? thinkingMaxTokens(sessionThinking) : undefined,
|
|
1419
1479
|
signal: ac.signal,
|
|
1480
|
+
steer: drainSteer,
|
|
1420
1481
|
events: { ...withToolDetailCapture(tui ? tui.events() : streamEvents), onBeforeDone },
|
|
1421
1482
|
});
|
|
1422
1483
|
if (result.done && looksLikeSkillEcho(result.doneReason ?? "", resolvedSkills)) {
|
|
@@ -1434,6 +1495,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1434
1495
|
model: sessionModel,
|
|
1435
1496
|
maxTokens: sessionThinking ? thinkingMaxTokens(sessionThinking) : undefined,
|
|
1436
1497
|
signal: ac.signal,
|
|
1498
|
+
steer: drainSteer,
|
|
1437
1499
|
events: withToolDetailCapture(tui ? tui.events() : streamEvents),
|
|
1438
1500
|
});
|
|
1439
1501
|
const usage =
|
|
@@ -1447,6 +1509,14 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1447
1509
|
}
|
|
1448
1510
|
} finally {
|
|
1449
1511
|
harness.dispose();
|
|
1512
|
+
subagentRegistry.cancelAll(); // #9: no detached run leaks past the turn
|
|
1513
|
+
// Steering typed but never drained (e.g. entered just after the final step)
|
|
1514
|
+
// must not be lost — fold it into the next prompt draft so it runs next.
|
|
1515
|
+
const leftover = steerInbox.splice(0, steerInbox.length).map(s => s.trim()).filter(Boolean);
|
|
1516
|
+
if (leftover.length) {
|
|
1517
|
+
const merged = [queuedPromptInput.partial, ...leftover].filter(Boolean).join(" ");
|
|
1518
|
+
queuedPromptInput.partial = merged;
|
|
1519
|
+
}
|
|
1450
1520
|
}
|
|
1451
1521
|
} catch (err) {
|
|
1452
1522
|
if (tui) {
|
|
@@ -1612,6 +1682,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1612
1682
|
|
|
1613
1683
|
// INTERACTIVE mode
|
|
1614
1684
|
const updatePromise = checkForUpdate({ timeoutMs: 2500 });
|
|
1685
|
+
// Refresh the on-disk update cache for the NEXT launch regardless of whether
|
|
1686
|
+
// this launch's bounded wait below catches the result. Screen-safe: writes
|
|
1687
|
+
// only, never renders (rendering after the prompt is armed would corrupt the
|
|
1688
|
+
// boxed input footer).
|
|
1689
|
+
void updatePromise.then(u => { if (u) void writeUpdateCache(u.latest); }).catch(() => {});
|
|
1615
1690
|
// Terminal hygiene BEFORE anything renders: a previous program (or stale tmux
|
|
1616
1691
|
// pane) can leave xterm mouse-tracking ON, so the terminal reports clicks and
|
|
1617
1692
|
// motion as escape sequences from the very first prompt — the "starts out
|
|
@@ -1647,8 +1722,18 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1647
1722
|
if (sweepable) await playWelcomeSweep(welcomeData, { cycles: sweepCycles });
|
|
1648
1723
|
else console.log(renderWelcome(welcomeData).join("\n"));
|
|
1649
1724
|
|
|
1650
|
-
|
|
1651
|
-
|
|
1725
|
+
// Surface the "New version" banner reliably: render ONCE from the on-disk cache
|
|
1726
|
+
// instantly (no network wait, works offline — the common path after the first
|
|
1727
|
+
// successful check), and ALSO from a bounded live check so a first run / version
|
|
1728
|
+
// bump still shows it this launch. Both must run BEFORE the prompt is armed.
|
|
1729
|
+
let updateBannerShown = false;
|
|
1730
|
+
const showUpdateBanner = (u: { current: string; latest: string; updateAvailable: boolean } | null): void => {
|
|
1731
|
+
if (updateBannerShown || !u?.updateAvailable) return;
|
|
1732
|
+
updateBannerShown = true;
|
|
1733
|
+
console.log(renderUpdateBox(u.current, u.latest).join("\n"));
|
|
1734
|
+
};
|
|
1735
|
+
showUpdateBanner(await readUpdateCache(pkg.version));
|
|
1736
|
+
showUpdateBanner(await Promise.race([updatePromise, new Promise<null>(r => setTimeout(() => r(null), 1200))]));
|
|
1652
1737
|
if (!LaunchTui.usable(flags.noTui)) console.log("(plain output)");
|
|
1653
1738
|
|
|
1654
1739
|
const useTui = LaunchTui.usable(flags.noTui);
|
|
@@ -1800,6 +1885,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1800
1885
|
};
|
|
1801
1886
|
};
|
|
1802
1887
|
let previewArmed = false;
|
|
1888
|
+
// True while a prompt line is being awaited. Folded into the readline output
|
|
1889
|
+
// gate so native echo is suppressed for the WHOLE await window — including the
|
|
1890
|
+
// brief gap between turn-end and armPreview() — so no keystroke can leak into
|
|
1891
|
+
// scrollback before the boxed footer takes over.
|
|
1892
|
+
let promptActive = false;
|
|
1803
1893
|
let pickerActive = false;
|
|
1804
1894
|
const rl = createInterface({
|
|
1805
1895
|
input: process.stdin,
|
|
@@ -1810,7 +1900,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1810
1900
|
// previously OPENED the gate and let readline echo typed filter characters
|
|
1811
1901
|
// (CJK wide chars especially) straight onto the picker frame — the
|
|
1812
1902
|
// "stacked input-box borders" corruption.
|
|
1813
|
-
output: gatedStdout(process.stdout, () => previewArmed || pickerActive || interactiveTurnActive),
|
|
1903
|
+
output: gatedStdout(process.stdout, () => previewArmed || promptActive || pickerActive || interactiveTurnActive),
|
|
1814
1904
|
completer: (line: string) => readlineCompleter(line, completionContext()),
|
|
1815
1905
|
});
|
|
1816
1906
|
const promptStdin = process.stdin as typeof process.stdin & { isRaw?: boolean; setRawMode?(raw: boolean): void };
|
|
@@ -1839,6 +1929,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1839
1929
|
queueBusySnapshot = () => ({
|
|
1840
1930
|
text: queuedPromptInput.partial,
|
|
1841
1931
|
});
|
|
1932
|
+
queueBusyClear = () => { queuedPromptInput.partial = ""; };
|
|
1842
1933
|
// Bracketed-paste line routing at the PROMPT: readline strips the 2004 markers
|
|
1843
1934
|
// and replays pasted lines as synthetic keypresses, emitting paste-start /
|
|
1844
1935
|
// paste-end around them. Lines submitted INSIDE that window are intentional
|
|
@@ -1897,6 +1988,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1897
1988
|
if (hardExitOnLoopEnd || process.stdin.isTTY || process.stdout.isTTY) forceExitFromCtrlC();
|
|
1898
1989
|
return "/exit";
|
|
1899
1990
|
}
|
|
1991
|
+
promptActive = true;
|
|
1900
1992
|
try {
|
|
1901
1993
|
return await Promise.race([
|
|
1902
1994
|
rl.question(prompt),
|
|
@@ -1908,6 +2000,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1908
2000
|
}),
|
|
1909
2001
|
]);
|
|
1910
2002
|
} finally {
|
|
2003
|
+
promptActive = false;
|
|
1911
2004
|
notifyStdinClosed = undefined;
|
|
1912
2005
|
}
|
|
1913
2006
|
};
|
|
@@ -1955,10 +2048,16 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1955
2048
|
const MAX_PREVIEW_ROWS = 12;
|
|
1956
2049
|
const MIN_PREVIEW_ROWS = 7; // status bar (1) + spacer (1) + input box (3 rows) + 2 preview rows
|
|
1957
2050
|
const previewRowsFor = (rows: number): number => Math.max(MIN_PREVIEW_ROWS, Math.min(MAX_PREVIEW_ROWS, rows - 6));
|
|
2051
|
+
// Enable the boxed input footer for ANY interactive TTY. It paints inside a
|
|
2052
|
+
// reserved bottom region (never scrollback) and only commits the line on
|
|
2053
|
+
// Enter, so typed characters never leak into the conversation history while
|
|
2054
|
+
// typing. previewRowsFor() clamps the reservation height for short terminals,
|
|
2055
|
+
// so even small panes get the box instead of the raw `jeo>` echo fallback
|
|
2056
|
+
// (which echoes every keystroke straight into scrollback). The raw fallback is
|
|
2057
|
+
// now reserved for non-TTY/piped input, where live history echo is moot.
|
|
1958
2058
|
const previewEnabled =
|
|
1959
2059
|
process.stdin.isTTY &&
|
|
1960
|
-
jeoEnv("NO_SLASH_PREVIEW") !== "1"
|
|
1961
|
-
(process.stdout.rows ?? 24) >= MIN_PREVIEW_ROWS + 6; // box + ≥6 scrollable content rows
|
|
2060
|
+
jeoEnv("NO_SLASH_PREVIEW") !== "1";
|
|
1962
2061
|
// Footer height reserved by the CURRENTLY armed region; disarm/draw must use the
|
|
1963
2062
|
// same value the arm computed, even if the terminal was resized in between.
|
|
1964
2063
|
let footerRows = MAX_PREVIEW_ROWS;
|
|
@@ -2112,7 +2211,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2112
2211
|
};
|
|
2113
2212
|
const historyPreviewLines = (detail: string[]): string[] => {
|
|
2114
2213
|
const cols = Math.max(24, (process.stdout.columns ?? 80) - 1);
|
|
2115
|
-
const title = `${
|
|
2214
|
+
const title = `${uiAccent("history")} ${chalk.dim("· Ctrl+O closes")}`;
|
|
2116
2215
|
const budget = Math.max(0, footerRows - 2);
|
|
2117
2216
|
const physical = detail.flatMap(line => line.split("\n")).map(line => truncateAnsi(line, cols));
|
|
2118
2217
|
let body = physical;
|
|
@@ -2833,29 +2932,34 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2833
2932
|
console.log(chalk.dim(`(restored ${folded} queued input line${folded > 1 ? "s" : ""} into the prompt — Enter to run, Esc to discard)`));
|
|
2834
2933
|
}
|
|
2835
2934
|
}
|
|
2836
|
-
// Refresh the status bar's dirty flag once per prompt (one git spawn, not per frame).
|
|
2837
|
-
idleDirtyCount = branch ? gitDirtyCount(cwd) : undefined;
|
|
2838
2935
|
const prefilledLine = queuedPromptInput.partial;
|
|
2839
2936
|
queuedPromptInput.partial = "";
|
|
2840
|
-
|
|
2841
|
-
//
|
|
2842
|
-
// readline's own
|
|
2843
|
-
//
|
|
2844
|
-
// box instead of dropping it as "noise". A pasted batch's TRAILING partial
|
|
2845
|
-
// (no final newline) survives in readline's own buffer — adopt it as the
|
|
2846
|
-
// visible typed line so the box never hides editable input.
|
|
2937
|
+
// Resolve the visible input text FIRST (cheap, no I/O): the queued prefill, else
|
|
2938
|
+
// any partial the user typed while the previous live turn/subagent was running
|
|
2939
|
+
// (that text survives in readline's own buffer — adopt it so the box never hides
|
|
2940
|
+
// editable input). A pasted batch's TRAILING partial is recovered the same way.
|
|
2847
2941
|
const rli = rl as unknown as { line?: string; cursor?: number; _refreshLine?: () => void };
|
|
2848
2942
|
const residualPartial = !prefilledLine && typeof rli.line === "string" && rli.line.length > 0 && !/\x1b/.test(rli.line)
|
|
2849
2943
|
? rli.line
|
|
2850
2944
|
: "";
|
|
2851
2945
|
typedLine = prefilledLine || residualPartial;
|
|
2946
|
+
navMatches = [];
|
|
2947
|
+
navIdx = -1;
|
|
2948
|
+
// Reserve + paint the boxed input IMMEDIATELY — WITH its real text — so the
|
|
2949
|
+
// prompt is visible the instant the turn ends, BEFORE the git spawn below.
|
|
2950
|
+
// Otherwise the dirty-flag spawn (slow on a large / just-edited repo) runs in a
|
|
2951
|
+
// window where the box is gone and keystrokes echo nowhere (the "no response
|
|
2952
|
+
// after the result" gap). readline's own echo stays gated while armed.
|
|
2953
|
+
armPreview();
|
|
2852
2954
|
if (prefilledLine) {
|
|
2853
2955
|
rli.line = prefilledLine;
|
|
2854
2956
|
rli.cursor = prefilledLine.length;
|
|
2855
2957
|
rli._refreshLine?.();
|
|
2856
2958
|
}
|
|
2857
|
-
|
|
2858
|
-
|
|
2959
|
+
drawFooter(previewLines(typedLine));
|
|
2960
|
+
// Refresh the status bar's dirty flag once per prompt (one git spawn, not per
|
|
2961
|
+
// frame); the second drawFooter repaints only if the count actually changed.
|
|
2962
|
+
idleDirtyCount = branch ? gitDirtyCount(cwd) : undefined;
|
|
2859
2963
|
drawFooter(previewLines(typedLine));
|
|
2860
2964
|
// Box mode: NO raw `jeo>` prompt at all — the boxed footer IS the input UI
|
|
2861
2965
|
// (gating already suppresses readline echo, the empty prompt guarantees no
|
|
@@ -3171,6 +3275,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3171
3275
|
for (const line of TOOL_PROTOCOL.split("\n")) console.log(` ${line}`);
|
|
3172
3276
|
console.log(` ${taskToolProtocolLine(await readGlobalConfig())}`);
|
|
3173
3277
|
console.log(` ${TODO_TOOL_PROTOCOL_LINE}`);
|
|
3278
|
+
console.log(` ${SUBAGENT_TOOL_PROTOCOL_LINE}`);
|
|
3174
3279
|
continue;
|
|
3175
3280
|
}
|
|
3176
3281
|
if (input === "/hotkeys") {
|
package/src/commands/team.ts
CHANGED
|
@@ -403,7 +403,8 @@ async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: str
|
|
|
403
403
|
// LLM summary failure does not halt team
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
-
let
|
|
406
|
+
let fileMutations = 0; // round-8 parent audit: successful write/edit/mkdir/delete
|
|
407
|
+
let bashRuns = 0; // bash counted apart so read-only bash isn't edit evidence
|
|
407
408
|
const result = await runAgentLoop(history, {
|
|
408
409
|
cwd: ctx.cwd,
|
|
409
410
|
model,
|
|
@@ -429,7 +430,10 @@ async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: str
|
|
|
429
430
|
}
|
|
430
431
|
},
|
|
431
432
|
onToolResult: (tool, ok) => {
|
|
432
|
-
if (ok
|
|
433
|
+
if (ok) {
|
|
434
|
+
if (tool === "write" || tool === "edit" || tool === "mkdir" || tool === "delete") fileMutations++;
|
|
435
|
+
else if (tool === "bash") bashRuns++;
|
|
436
|
+
}
|
|
433
437
|
console.log(formatRalphStreamEvent(ok ? "complete" : "error", `tool ${tool}`, renderOpts));
|
|
434
438
|
},
|
|
435
439
|
onNotice: msg => console.log(formatRalphStreamEvent("step", msg, renderOpts)),
|
|
@@ -454,11 +458,14 @@ async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: str
|
|
|
454
458
|
return false;
|
|
455
459
|
}
|
|
456
460
|
|
|
457
|
-
if (!role.readOnly &&
|
|
458
|
-
// Round-8: a mutating role finished without
|
|
461
|
+
if (!role.readOnly && fileMutations === 0) {
|
|
462
|
+
// Round-8: a mutating role finished without a successful file mutation — the
|
|
459
463
|
// task may be legitimately read-only, but its "Changed Files:" claim is
|
|
460
|
-
// unverified
|
|
461
|
-
|
|
464
|
+
// unverified. bash is tracked apart: an only-bash run MIGHT have mutated.
|
|
465
|
+
const msg = bashRuns === 0
|
|
466
|
+
? `${role.title} completed WITHOUT any successful write/edit/bash — treat its changed-files claim as unverified.`
|
|
467
|
+
: `${role.title} completed with only bash (no write/edit) — verify its changed-files claim independently.`;
|
|
468
|
+
console.log(formatRalphStreamEvent("error", msg, renderOpts));
|
|
462
469
|
}
|
|
463
470
|
console.log(formatRalphStreamEvent("complete", `${role.title} finished task`, renderOpts));
|
|
464
471
|
return true;
|
package/src/skills/catalog.ts
CHANGED
|
@@ -3,7 +3,6 @@ import deepDiveSkillRaw from "../prompts/skills/deep-dive/SKILL.md" with { type:
|
|
|
3
3
|
import ralplanSkillRaw from "../prompts/skills/ralplan/SKILL.md" with { type: "text" };
|
|
4
4
|
import teamSkillRaw from "../prompts/skills/team/SKILL.md" with { type: "text" };
|
|
5
5
|
import ultragoalSkillRaw from "../prompts/skills/ultragoal/SKILL.md" with { type: "text" };
|
|
6
|
-
import gjcSkillRaw from "../prompts/skills/gjc/SKILL.md" with { type: "text" };
|
|
7
6
|
|
|
8
7
|
const MAX_SKILL_SUMMARY_CHARS = 180;
|
|
9
8
|
const MAX_SKILL_DETAILS_CHARS = 8_000;
|
|
@@ -29,7 +28,6 @@ export const SKILLS: SkillDoc[] = [
|
|
|
29
28
|
parseSkillMarkdown("ralplan", ralplanSkillRaw),
|
|
30
29
|
parseSkillMarkdown("team", teamSkillRaw),
|
|
31
30
|
parseSkillMarkdown("ultragoal", ultragoalSkillRaw),
|
|
32
|
-
parseSkillMarkdown("gjc", gjcSkillRaw),
|
|
33
31
|
];
|
|
34
32
|
export const BUILTIN_SKILL_NAMES = SKILLS.map(s => s.name.toLowerCase());
|
|
35
33
|
|