@jhizzard/termdeck 0.13.0 → 0.14.0
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/package.json +1 -1
- package/packages/client/public/app.js +60 -16
- package/packages/server/src/agent-adapters/codex.js +199 -0
- package/packages/server/src/agent-adapters/gemini.js +158 -0
- package/packages/server/src/agent-adapters/grok-models.js +115 -0
- package/packages/server/src/agent-adapters/grok.js +253 -0
- package/packages/server/src/agent-adapters/index.js +6 -0
- package/packages/server/src/index.js +45 -6
- package/packages/server/src/rumen-pool-resilience.js +111 -0
- package/packages/server/src/session.js +45 -57
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhizzard/termdeck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Browser-based terminal multiplexer with metadata overlays, panel flashback memory recall, and AI-aware session management",
|
|
5
5
|
"bin": {
|
|
6
6
|
"termdeck": "./packages/cli/src/index.js"
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
layout: '2x1',
|
|
11
11
|
themes: {},
|
|
12
12
|
config: {},
|
|
13
|
+
// Sprint 45 T4: serializable projection of the multi-agent registry
|
|
14
|
+
// (server's AGENT_ADAPTERS). Populated from GET /api/agent-adapters
|
|
15
|
+
// during init(). The launcher's command-shorthand parser reads this
|
|
16
|
+
// to detect which adapter (if any) a typed command should map to.
|
|
17
|
+
// Fallback list is the pre-Sprint-45 default so the launcher still
|
|
18
|
+
// works if the endpoint 404s on an older server during a rolling
|
|
19
|
+
// upgrade — Claude only, anchored binary match.
|
|
20
|
+
agentAdapters: [{ name: 'claude', sessionType: 'claude-code', binary: 'claude', costBand: 'pay-per-token' }],
|
|
13
21
|
focusedId: null
|
|
14
22
|
};
|
|
15
23
|
|
|
@@ -27,6 +35,17 @@
|
|
|
27
35
|
state.config = await api('GET', '/api/config');
|
|
28
36
|
updateRagIndicator();
|
|
29
37
|
|
|
38
|
+
// Sprint 45 T4: fetch the multi-agent adapter registry projection.
|
|
39
|
+
// Drives the launcher's command-shorthand → sessionType resolution
|
|
40
|
+
// below in launchTerminal(). Falls back to the bootstrap default
|
|
41
|
+
// (Claude only) if the endpoint isn't available on this server.
|
|
42
|
+
try {
|
|
43
|
+
const adapters = await api('GET', '/api/agent-adapters');
|
|
44
|
+
if (Array.isArray(adapters) && adapters.length > 0) {
|
|
45
|
+
state.agentAdapters = adapters;
|
|
46
|
+
}
|
|
47
|
+
} catch (_) { /* keep bootstrap fallback */ }
|
|
48
|
+
|
|
30
49
|
// Populate project dropdown
|
|
31
50
|
const sel = document.getElementById('promptProject');
|
|
32
51
|
for (const name of Object.keys(state.config.projects || {})) {
|
|
@@ -2460,29 +2479,54 @@
|
|
|
2460
2479
|
return;
|
|
2461
2480
|
}
|
|
2462
2481
|
|
|
2463
|
-
//
|
|
2482
|
+
// Sprint 45 T4: registry-driven shorthand resolution. Pre-Sprint-45
|
|
2483
|
+
// had hardcoded claude/cc/gemini/python branches here; now the type
|
|
2484
|
+
// detection consults state.agentAdapters (loaded from
|
|
2485
|
+
// /api/agent-adapters at init), and only the Claude `cc` alias and
|
|
2486
|
+
// the python-server detection (no adapter exists) stay as
|
|
2487
|
+
// special-cases below. Adapter matching uses an anchored prefix on
|
|
2488
|
+
// the adapter's binary name (`^binary\b`, case-insensitive) which
|
|
2489
|
+
// fits all four Sprint-45 adapters (claude / codex / gemini / grok)
|
|
2490
|
+
// since each binary is uniquely named.
|
|
2464
2491
|
let resolvedCommand = command;
|
|
2465
2492
|
let resolvedType = 'shell';
|
|
2466
2493
|
let resolvedCwd = undefined;
|
|
2467
|
-
|
|
2468
2494
|
let resolvedProject = project || undefined;
|
|
2469
2495
|
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2496
|
+
// Claude `cc` alias normalization. Documented Claude shorthand —
|
|
2497
|
+
// does not generalize to other adapters, so it stays in client UX,
|
|
2498
|
+
// not in the server-side adapter contract.
|
|
2499
|
+
let canonical = command;
|
|
2500
|
+
if (/^cc\b/i.test(canonical)) {
|
|
2501
|
+
canonical = canonical.replace(/^cc\b/i, 'claude');
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
const adapter = (state.agentAdapters || []).find((a) =>
|
|
2505
|
+
a && a.binary && new RegExp(`^${a.binary}\\b`, 'i').test(canonical)
|
|
2506
|
+
);
|
|
2507
|
+
|
|
2508
|
+
if (adapter) {
|
|
2509
|
+
resolvedType = adapter.sessionType;
|
|
2510
|
+
// Claude shorthand: `claude <project-or-cwd>` rewrites to `claude`
|
|
2511
|
+
// and routes the trailing arg into either the project dropdown
|
|
2512
|
+
// (if it's a known project name) or the cwd parameter. Other
|
|
2513
|
+
// adapters' arg-parsing — codex sub-commands, gemini -p flag,
|
|
2514
|
+
// grok --model — pass through unchanged via resolvedCommand.
|
|
2515
|
+
if (adapter.name === 'claude') {
|
|
2516
|
+
const argMatch = canonical.match(/^claude\s+(?:code\s+)?(.+)/i);
|
|
2517
|
+
if (argMatch) {
|
|
2518
|
+
const arg = argMatch[1].trim();
|
|
2519
|
+
if (state.config.projects && state.config.projects[arg]) {
|
|
2520
|
+
resolvedProject = arg;
|
|
2521
|
+
} else {
|
|
2522
|
+
resolvedCwd = arg;
|
|
2523
|
+
}
|
|
2480
2524
|
}
|
|
2525
|
+
resolvedCommand = adapter.binary;
|
|
2481
2526
|
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
} else if (/^python3?\b.*(?:runserver|uvicorn|flask|gunicorn)/i.test(command)) {
|
|
2527
|
+
} else if (/^python3?\b.*(?:runserver|uvicorn|flask|gunicorn)/i.test(canonical)) {
|
|
2528
|
+
// python-server is a server SUBTYPE for status badges, not an
|
|
2529
|
+
// agent adapter. No registry entry for it; detection stays here.
|
|
2486
2530
|
resolvedType = 'python-server';
|
|
2487
2531
|
}
|
|
2488
2532
|
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// Codex CLI adapter — Sprint 45 T1
|
|
2
|
+
//
|
|
3
|
+
// Second adapter in the AGENT_ADAPTERS registry (see ./index.js). Sprint 44 T3
|
|
4
|
+
// shipped the Claude adapter as the reference implementation; this file is the
|
|
5
|
+
// recipe in `docs/AGENT-RUNTIMES.md` § 6 turned into running code for Codex
|
|
6
|
+
// CLI (`/usr/local/bin/codex`, v0.125.0 verified 2026-05-01).
|
|
7
|
+
//
|
|
8
|
+
// This is *Codex-as-its-own-panel* — distinct from the existing
|
|
9
|
+
// `codex@openai-codex` Claude Code plugin which is a delegate-from-Claude
|
|
10
|
+
// pathway. Sprint 46 wires per-lane agent assignment; this lane just makes
|
|
11
|
+
// `codex` work end-to-end inside a TermDeck panel: type detection, status
|
|
12
|
+
// badge, transcript ingestion into Mnestra.
|
|
13
|
+
//
|
|
14
|
+
// Contract — see ./claude.js header for the full annotated shape.
|
|
15
|
+
//
|
|
16
|
+
// Pattern provenance:
|
|
17
|
+
// • Codex CLI ships a Ratatui (Rust) TUI. The TUI redraws on each turn so the
|
|
18
|
+
// raw PTY stream is heavy in ANSI escapes; session.js stripAnsi() runs
|
|
19
|
+
// *before* these regexes, so the patterns assume cleaned text.
|
|
20
|
+
// • The headless `codex exec` mode emits a documented sequence: a `--------`
|
|
21
|
+
// header block, `user` / `codex` speaker lines on their own row, function
|
|
22
|
+
// `exec_command` blocks, and a `tokens used` footer. The TUI mirrors these
|
|
23
|
+
// speaker shapes inside its rendered chat surface.
|
|
24
|
+
// • Reasoning markers come from the JSONL `response_item.payload.type=reasoning`
|
|
25
|
+
// events that the TUI renders as a "Thinking…" status line.
|
|
26
|
+
// • Apply-patch / exec markers come from `response_item.payload.type=function_call`
|
|
27
|
+
// entries with names like `apply_patch` and `exec_command`.
|
|
28
|
+
//
|
|
29
|
+
// Patterns are conservative defaults — Sprint 45 T4 / Sprint 46 will tune
|
|
30
|
+
// against captured real-world TUI output. Snapshot tests in
|
|
31
|
+
// tests/agent-adapter-codex.test.js pin the current behavior so any tuning
|
|
32
|
+
// is an explicit, reviewed change.
|
|
33
|
+
|
|
34
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
35
|
+
// Patterns
|
|
36
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
// Codex prompt detection. Three shapes accepted:
|
|
39
|
+
// 1. `codex>` literal (mirrors gemini's `gemini>` and the codex CLI's REPL
|
|
40
|
+
// prompt convention — used by `codex resume` interactive sessions).
|
|
41
|
+
// 2. A bare `codex` line (the speaker label the TUI prints above an
|
|
42
|
+
// assistant turn AND that headless `codex exec` prints before the reply).
|
|
43
|
+
// 3. The `--------` divider that wraps the codex header block in headless
|
|
44
|
+
// mode and bookends turns in the TUI.
|
|
45
|
+
const PROMPT = /^(?:codex>\s|codex\s*$|--------\s*$)/m;
|
|
46
|
+
|
|
47
|
+
// Reasoning indicator. Codex's TUI status line shows "Thinking" while the
|
|
48
|
+
// model reasons; "Reasoning" appears in some headless transcripts; "Working"
|
|
49
|
+
// is what `codex exec` prints for tool-loop progress.
|
|
50
|
+
const THINKING = /\b(Thinking|Reasoning|Working)\b/;
|
|
51
|
+
|
|
52
|
+
// File edit / patch markers. Codex applies diffs through the `apply_patch`
|
|
53
|
+
// tool which the TUI renders as `Apply patch <file>` headers. Plain
|
|
54
|
+
// Edit/Create/Update/Delete shapes are also kept so simple file ops register
|
|
55
|
+
// (mirrors the Claude adapter's editing markers for cross-adapter parity).
|
|
56
|
+
const EDITING = /^(Apply patch|Edit|Create|Update|Delete|Modified)\s/m;
|
|
57
|
+
const EDITING_DETAIL = /^(Apply patch|Edit|Create|Update|Delete|Modified)\s+(.+)$/m;
|
|
58
|
+
|
|
59
|
+
// Tool / shell-exec markers. Codex's TUI prefixes shell commands with `$`
|
|
60
|
+
// (chat-shell convention), arrow `→` for read tool calls, and bare keywords
|
|
61
|
+
// `exec` / `Running` / `Calling` for the phase between dispatch and result.
|
|
62
|
+
// `exec_command` is Codex's function-call name (verified in rollout JSONL
|
|
63
|
+
// 2026-05-01); the alternation handles both bare `exec` and the underscored
|
|
64
|
+
// `exec_command` shape (the underscore is a word character so `exec\b`
|
|
65
|
+
// alone wouldn't match `exec_command`).
|
|
66
|
+
const TOOL = /^(?:\$\s|→\s|exec(?:_command\b|\b)|Running\b|Calling\b)/m;
|
|
67
|
+
|
|
68
|
+
// Idle / waiting-for-input. The TUI returns to the bare `codex` speaker
|
|
69
|
+
// label when it's done reasoning and waiting on the user.
|
|
70
|
+
const IDLE = /^codex\s*$/m;
|
|
71
|
+
|
|
72
|
+
// Error patterns — line-anchored to avoid mid-line "error" mentions in tool
|
|
73
|
+
// output (grep results, test logs, file dumps) flagging false positives.
|
|
74
|
+
// Same shape as Claude with codex-specific OpenAI-API failure modes added
|
|
75
|
+
// (rate-limit 429, model-not-found, invalid_api_key) which surface as visible
|
|
76
|
+
// strings in Codex's error reporting and would otherwise slip through.
|
|
77
|
+
const ERROR = /^\s*(?:(?:error|Error|ERROR|exception|Exception|Traceback|fatal|Fatal|FATAL|segmentation fault|panic|EACCES|ECONNREFUSED|ENOENT|command not found|undefined reference|cannot find module|failed with exit code|No such file or directory|Permission denied|429\s+Too Many Requests|rate.?limit|invalid_api_key|model_not_found|insufficient_quota)\b|npm ERR!)/m;
|
|
78
|
+
|
|
79
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
80
|
+
// statusFor — Codex panel status. Order mirrors Claude's cascade:
|
|
81
|
+
// thinking → editing → tool → idle. First match wins.
|
|
82
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
function statusFor(data) {
|
|
85
|
+
if (THINKING.test(data)) {
|
|
86
|
+
return { status: 'thinking', statusDetail: 'Codex is reasoning...' };
|
|
87
|
+
}
|
|
88
|
+
if (EDITING.test(data)) {
|
|
89
|
+
const match = data.match(EDITING_DETAIL);
|
|
90
|
+
return {
|
|
91
|
+
status: 'editing',
|
|
92
|
+
statusDetail: match ? `${match[1]} ${match[2]}` : 'Editing files',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (TOOL.test(data)) {
|
|
96
|
+
return { status: 'active', statusDetail: 'Using tools' };
|
|
97
|
+
}
|
|
98
|
+
if (IDLE.test(data)) {
|
|
99
|
+
return { status: 'idle', statusDetail: 'Waiting for input' };
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
105
|
+
// parseTranscript — Codex JSONL format.
|
|
106
|
+
//
|
|
107
|
+
// Each line is `{ timestamp, type, payload }`. We want only:
|
|
108
|
+
// type === 'response_item' && payload.type === 'message'
|
|
109
|
+
// with payload.role in {user, assistant}. The 'developer' role carries the
|
|
110
|
+
// permissions/sandbox prelude — skip. `event_msg` lines duplicate the
|
|
111
|
+
// canonical message channel and additionally carry exec_command_end shell
|
|
112
|
+
// output blocks — skip too.
|
|
113
|
+
//
|
|
114
|
+
// content is an array of { type: 'input_text' | 'output_text', text: string }
|
|
115
|
+
// (sometimes plain `text`). Joined with spaces and truncated to 400 chars
|
|
116
|
+
// per message (same cut-off Claude uses).
|
|
117
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
function parseTranscript(raw) {
|
|
120
|
+
if (typeof raw !== 'string' || raw.length === 0) return [];
|
|
121
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
122
|
+
const messages = [];
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
let entry;
|
|
125
|
+
try { entry = JSON.parse(line); } catch (_) { continue; }
|
|
126
|
+
if (!entry || entry.type !== 'response_item') continue;
|
|
127
|
+
const p = entry.payload;
|
|
128
|
+
if (!p || p.type !== 'message') continue;
|
|
129
|
+
const role = p.role;
|
|
130
|
+
if (role !== 'user' && role !== 'assistant') continue;
|
|
131
|
+
const content = p.content;
|
|
132
|
+
let text = '';
|
|
133
|
+
if (typeof content === 'string') {
|
|
134
|
+
text = content;
|
|
135
|
+
} else if (Array.isArray(content)) {
|
|
136
|
+
text = content
|
|
137
|
+
.filter((c) => c && (c.type === 'input_text' || c.type === 'output_text' || c.type === 'text'))
|
|
138
|
+
.map((c) => c.text || '')
|
|
139
|
+
.join(' ');
|
|
140
|
+
}
|
|
141
|
+
if (text) messages.push({ role, content: text.slice(0, 400) });
|
|
142
|
+
}
|
|
143
|
+
return messages;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
147
|
+
// bootPromptTemplate — Codex variant of the Claude scaffold. Points at
|
|
148
|
+
// AGENTS.md (Codex's instructional file) instead of CLAUDE.md. Sprint 46 T2
|
|
149
|
+
// will refine per-agent prompts; this is the placeholder so the contract is
|
|
150
|
+
// uniform across all four adapters.
|
|
151
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
function bootPromptTemplate(lane = {}, sprint = {}) {
|
|
154
|
+
const tn = lane.id || 'T?';
|
|
155
|
+
const sprintNum = sprint.number || '?';
|
|
156
|
+
const sprintName = sprint.name || 'unnamed';
|
|
157
|
+
const project = (lane.project || sprint.project || 'termdeck');
|
|
158
|
+
const briefing = lane.briefingPath || `docs/sprint-${sprintNum}-${sprintName}/${tn}-<lane>.md`;
|
|
159
|
+
return [
|
|
160
|
+
`You are ${tn} in Sprint ${sprintNum} (${sprintName}). Boot sequence:`,
|
|
161
|
+
`1. memory_recall(project="${project}", query="<topic>")`,
|
|
162
|
+
`2. memory_recall(query="<broader topic>")`,
|
|
163
|
+
`3. Read ~/.claude/CLAUDE.md and ./AGENTS.md`,
|
|
164
|
+
`4. Read docs/sprint-${sprintNum}-${sprintName}/PLANNING.md`,
|
|
165
|
+
`5. Read docs/sprint-${sprintNum}-${sprintName}/STATUS.md`,
|
|
166
|
+
`6. Read ${briefing}`,
|
|
167
|
+
'',
|
|
168
|
+
'Then begin. Stay in your lane. Post FINDING / FIX-PROPOSED / DONE in STATUS.md.',
|
|
169
|
+
"Don't bump versions, don't touch CHANGELOG, don't commit.",
|
|
170
|
+
].join('\n');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const codexAdapter = {
|
|
174
|
+
name: 'codex',
|
|
175
|
+
sessionType: 'codex',
|
|
176
|
+
matches: (cmd) => typeof cmd === 'string' && /\bcodex\b/i.test(cmd),
|
|
177
|
+
spawn: {
|
|
178
|
+
binary: 'codex',
|
|
179
|
+
defaultArgs: [],
|
|
180
|
+
env: { OPENAI_API_KEY: process.env.OPENAI_API_KEY },
|
|
181
|
+
},
|
|
182
|
+
patterns: {
|
|
183
|
+
prompt: PROMPT,
|
|
184
|
+
thinking: THINKING,
|
|
185
|
+
editing: EDITING,
|
|
186
|
+
tool: TOOL,
|
|
187
|
+
idle: IDLE,
|
|
188
|
+
error: ERROR,
|
|
189
|
+
},
|
|
190
|
+
patternNames: {
|
|
191
|
+
error: 'codexErrorLineStart',
|
|
192
|
+
},
|
|
193
|
+
statusFor,
|
|
194
|
+
parseTranscript,
|
|
195
|
+
bootPromptTemplate,
|
|
196
|
+
costBand: 'pay-per-token',
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
module.exports = codexAdapter;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Gemini CLI adapter — Sprint 45 T2
|
|
2
|
+
//
|
|
3
|
+
// Lifts the previously-hardcoded gemini logic out of session.js into the
|
|
4
|
+
// AGENT_ADAPTERS registry alongside the Claude adapter shipped in Sprint 44
|
|
5
|
+
// T3. Behavior is bit-for-bit identical to the pre-Sprint-45 inline path:
|
|
6
|
+
// same `^gemini>` prompt regex, same `Generating|Working` thinking regex,
|
|
7
|
+
// same status strings ("Gemini is generating..." / "Waiting for input"),
|
|
8
|
+
// same loose `/gemini/i` command-string match. parseTranscript is the new
|
|
9
|
+
// capability — Gemini sessions previously didn't write to Mnestra because
|
|
10
|
+
// the memory hook assumed Claude JSONL.
|
|
11
|
+
//
|
|
12
|
+
// Contract — see ./claude.js header for the full 7-field shape.
|
|
13
|
+
//
|
|
14
|
+
// Patterns intentionally omit `error`. The fallback in session.js
|
|
15
|
+
// `_detectErrors` (`adapter.patterns.error || PATTERNS.error`) lets generic
|
|
16
|
+
// prose-shape error detection continue to apply to Gemini sessions, which
|
|
17
|
+
// matches the pre-Sprint-45 behavior. Sprint 46+ can layer in a Gemini-
|
|
18
|
+
// specific line-anchored error pattern once we've observed enough TUI
|
|
19
|
+
// output to know what false positives to dodge.
|
|
20
|
+
|
|
21
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
22
|
+
// Patterns — verbatim regexes lifted from session.js's PATTERNS.geminiCli
|
|
23
|
+
// (lines 47-50). Reference-equal preservation matters because session.js
|
|
24
|
+
// keeps a `PATTERNS.geminiCli` shim that points back at these regex
|
|
25
|
+
// objects, the same way `PATTERNS.claudeCode.*` shimmed Sprint 44 T3.
|
|
26
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const PROMPT = /^gemini>\s/m;
|
|
29
|
+
const THINKING = /\b(Generating|Working)\b/;
|
|
30
|
+
|
|
31
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
32
|
+
// statusFor — replaces the `case 'gemini':` block of _updateStatus. Order
|
|
33
|
+
// matches the legacy switch's `if/else if` cascade exactly: thinking wins,
|
|
34
|
+
// then prompt → idle. No editing/tool/error branches in the legacy switch,
|
|
35
|
+
// so statusFor has none either; null returns leave the status untouched
|
|
36
|
+
// just like the legacy fall-through.
|
|
37
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
function statusFor(data) {
|
|
40
|
+
if (THINKING.test(data)) {
|
|
41
|
+
return { status: 'thinking', statusDetail: 'Gemini is generating...' };
|
|
42
|
+
}
|
|
43
|
+
if (PROMPT.test(data)) {
|
|
44
|
+
return { status: 'idle', statusDetail: 'Waiting for input' };
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
50
|
+
// parseTranscript — Gemini CLI session JSON format (NOT JSONL).
|
|
51
|
+
//
|
|
52
|
+
// Captured shape (from `gemini -p "say hi"` 2026-05-01):
|
|
53
|
+
// {
|
|
54
|
+
// sessionId, projectHash, startTime, lastUpdated, kind,
|
|
55
|
+
// messages: [
|
|
56
|
+
// { id, timestamp, type: 'user', content: [{ text: '...' }] },
|
|
57
|
+
// { id, timestamp, type: 'gemini', content: '...', thoughts, tokens, model },
|
|
58
|
+
// ...
|
|
59
|
+
// ]
|
|
60
|
+
// }
|
|
61
|
+
//
|
|
62
|
+
// The user role carries a content ARRAY of `{text}` parts; the gemini
|
|
63
|
+
// (assistant) role carries a STRING. We normalize both to the Claude
|
|
64
|
+
// adapter's output shape — `{ role: 'user'|'assistant', content: string }`
|
|
65
|
+
// truncated to 400 chars — so the memory-hook summary builder doesn't have
|
|
66
|
+
// to branch on adapter type.
|
|
67
|
+
//
|
|
68
|
+
// `type: 'gemini'` maps to `role: 'assistant'` for cross-adapter parity.
|
|
69
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
function parseTranscript(raw) {
|
|
72
|
+
if (typeof raw !== 'string' || raw.length === 0) return [];
|
|
73
|
+
let session;
|
|
74
|
+
try { session = JSON.parse(raw); } catch (_) { return []; }
|
|
75
|
+
if (!session || !Array.isArray(session.messages)) return [];
|
|
76
|
+
|
|
77
|
+
const messages = [];
|
|
78
|
+
for (const msg of session.messages) {
|
|
79
|
+
if (!msg || typeof msg !== 'object') continue;
|
|
80
|
+
let role;
|
|
81
|
+
if (msg.type === 'user') role = 'user';
|
|
82
|
+
else if (msg.type === 'gemini' || msg.type === 'assistant') role = 'assistant';
|
|
83
|
+
else continue;
|
|
84
|
+
|
|
85
|
+
const content = msg.content;
|
|
86
|
+
let text = '';
|
|
87
|
+
if (typeof content === 'string') {
|
|
88
|
+
text = content;
|
|
89
|
+
} else if (Array.isArray(content)) {
|
|
90
|
+
text = content
|
|
91
|
+
.filter((c) => c && typeof c.text === 'string')
|
|
92
|
+
.map((c) => c.text)
|
|
93
|
+
.join(' ');
|
|
94
|
+
}
|
|
95
|
+
if (text) messages.push({ role, content: text.slice(0, 400) });
|
|
96
|
+
}
|
|
97
|
+
return messages;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
101
|
+
// bootPromptTemplate — placeholder mirroring the Claude adapter's shape.
|
|
102
|
+
// Points at GEMINI.md (the auto-generated mirror of CLAUDE.md per Sprint 44
|
|
103
|
+
// T2's sync-agent-instructions.js script). Sprint 46 T2 will refine the
|
|
104
|
+
// per-agent boot prompt — Gemini doesn't have Claude's `memory_recall` MCP
|
|
105
|
+
// tool out-of-the-box, so the lane brief shape may need agent-specific
|
|
106
|
+
// scaffolding. The placeholder here keeps the contract complete.
|
|
107
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
function bootPromptTemplate(lane = {}, sprint = {}) {
|
|
110
|
+
const tn = lane.id || 'T?';
|
|
111
|
+
const sprintNum = sprint.number || '?';
|
|
112
|
+
const sprintName = sprint.name || 'unnamed';
|
|
113
|
+
const project = (lane.project || sprint.project || 'termdeck');
|
|
114
|
+
const briefing = lane.briefingPath || `docs/sprint-${sprintNum}-${sprintName}/${tn}-<lane>.md`;
|
|
115
|
+
return [
|
|
116
|
+
`You are ${tn} in Sprint ${sprintNum} (${sprintName}). Boot sequence:`,
|
|
117
|
+
`1. memory_recall(project="${project}", query="<topic>")`,
|
|
118
|
+
`2. memory_recall(query="<broader topic>")`,
|
|
119
|
+
`3. Read ~/.claude/CLAUDE.md and ./GEMINI.md`,
|
|
120
|
+
`4. Read docs/sprint-${sprintNum}-${sprintName}/PLANNING.md`,
|
|
121
|
+
`5. Read docs/sprint-${sprintNum}-${sprintName}/STATUS.md`,
|
|
122
|
+
`6. Read ${briefing}`,
|
|
123
|
+
'',
|
|
124
|
+
'Then begin. Stay in your lane. Post FINDING / FIX-PROPOSED / DONE in STATUS.md.',
|
|
125
|
+
"Don't bump versions, don't touch CHANGELOG, don't commit.",
|
|
126
|
+
].join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const geminiAdapter = {
|
|
130
|
+
name: 'gemini',
|
|
131
|
+
sessionType: 'gemini',
|
|
132
|
+
matches: (cmd) => typeof cmd === 'string' && /gemini/i.test(cmd),
|
|
133
|
+
spawn: {
|
|
134
|
+
binary: 'gemini',
|
|
135
|
+
defaultArgs: [],
|
|
136
|
+
// GEMINI_API_KEY is read via `process.env` at spawn time by index.js'
|
|
137
|
+
// PTY env merge — declared here for documentation / discoverability,
|
|
138
|
+
// not for in-adapter overriding. OAuth-personal is the typical auth
|
|
139
|
+
// path (settings.json `security.auth.selectedType: 'oauth-personal'`).
|
|
140
|
+
env: {},
|
|
141
|
+
},
|
|
142
|
+
patterns: {
|
|
143
|
+
prompt: PROMPT,
|
|
144
|
+
thinking: THINKING,
|
|
145
|
+
// editing / tool / error intentionally omitted — see header comment.
|
|
146
|
+
},
|
|
147
|
+
patternNames: {
|
|
148
|
+
// No adapter-owned error pattern → session.js falls back to the
|
|
149
|
+
// generic `PATTERNS.error` and the `'error'` diag label, which is
|
|
150
|
+
// exactly what gemini-typed sessions saw pre-Sprint-45.
|
|
151
|
+
},
|
|
152
|
+
statusFor,
|
|
153
|
+
parseTranscript,
|
|
154
|
+
bootPromptTemplate,
|
|
155
|
+
costBand: 'pay-per-token',
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
module.exports = geminiAdapter;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Grok model selection — Sprint 45 T3
|
|
2
|
+
//
|
|
3
|
+
// `grok-dev` (the superagent-ai CLI) ships an 11-model lineup spanning
|
|
4
|
+
// $0.2/$0.5 cheap-fast tiers up to $3/$15 flagship. The wrong default
|
|
5
|
+
// silently 10x's a bill on routine tasks: a "look at this file and tell me
|
|
6
|
+
// what's wrong" lane on `grok-4.20-0309-reasoning` (Heavy, $2/$6) costs the
|
|
7
|
+
// same as ten lanes on `grok-4-1-fast-non-reasoning`. The orchestrator picks
|
|
8
|
+
// per-lane via `chooseModel(taskHint)` at boot-prompt construction time
|
|
9
|
+
// (see SPRINT-45-PREP-NOTES.md § "Concern 2: Model selection heuristic").
|
|
10
|
+
// The adapter's `spawn.env.GROK_MODEL` defaults to the cheap-fast model and
|
|
11
|
+
// is overridden per-lane by the launcher.
|
|
12
|
+
//
|
|
13
|
+
// Tier table (price = USD per 1M tokens, in/out):
|
|
14
|
+
//
|
|
15
|
+
// tier | model id | price | use case
|
|
16
|
+
// ───────────────────┼───────────────────────────────────┼──────────┼──────────────────────
|
|
17
|
+
// fast-non-reasoning | grok-4-1-fast-non-reasoning | $0.2/0.5 | DEFAULT — routine
|
|
18
|
+
// fast-reasoning | grok-4-1-fast-reasoning | $0.2/0.5 | light CoT under budget
|
|
19
|
+
// code | grok-code-fast-1 | $0.2/1.5 | code gen / refactor
|
|
20
|
+
// reasoning-deep | grok-4.20-0309-reasoning | $2/6 | hard problems, audit
|
|
21
|
+
// reasoning-non-cot | grok-4.20-0309-non-reasoning | $2/6 | high-quality non-CoT
|
|
22
|
+
// multi-agent | grok-4.20-multi-agent-0309 | $2/6 | parallel sub-agent fan-out
|
|
23
|
+
// flagship | grok-4-0709 | $3/15 | when Heavy isn't enough
|
|
24
|
+
// budget-compact | grok-3-mini | $0.3/0.5 | rare — usually wrong
|
|
25
|
+
//
|
|
26
|
+
// `grok-4-fast-non-reasoning`, `grok-4-fast-reasoning`, and `grok-3` are
|
|
27
|
+
// legacy aliases retained for completeness but not in the heuristic switch.
|
|
28
|
+
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
// Canonical model ids. Use the symbolic key in code; the heuristic resolves
|
|
32
|
+
// to the live id below. Keep these as data, not constants — Sprint 46+ may
|
|
33
|
+
// gain a `taskHint -> model` override file in `~/.termdeck/`.
|
|
34
|
+
const MODELS = {
|
|
35
|
+
'fast-non-reasoning': 'grok-4-1-fast-non-reasoning',
|
|
36
|
+
'fast-reasoning': 'grok-4-1-fast-reasoning',
|
|
37
|
+
'code': 'grok-code-fast-1',
|
|
38
|
+
'reasoning-deep': 'grok-4.20-0309-reasoning',
|
|
39
|
+
'reasoning-non-cot': 'grok-4.20-0309-non-reasoning',
|
|
40
|
+
'multi-agent': 'grok-4.20-multi-agent-0309',
|
|
41
|
+
'flagship': 'grok-4-0709',
|
|
42
|
+
'budget-compact': 'grok-3-mini',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Legacy aliases — accepted as input to chooseModel for back-compat with
|
|
46
|
+
// Joshua's earlier `grok models` outputs. Resolution table:
|
|
47
|
+
const LEGACY_ALIASES = {
|
|
48
|
+
'grok-4-fast-non-reasoning': MODELS['fast-non-reasoning'],
|
|
49
|
+
'grok-4-fast-reasoning': MODELS['fast-reasoning'],
|
|
50
|
+
'grok-beta': MODELS['reasoning-deep'],
|
|
51
|
+
'grok-4.20-multi-agent': MODELS['multi-agent'],
|
|
52
|
+
'grok-3': MODELS['flagship'],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// chooseModel — orchestrator-side heuristic. Pass `taskHint` from the lane
|
|
56
|
+
// brief (Sprint 46 frontmatter `model-hint: code|reasoning-deep|...`) or omit
|
|
57
|
+
// for the cheap-fast default. Unknown hints fall back to the default rather
|
|
58
|
+
// than throwing — the bill consequence of a typo silently routing to Heavy
|
|
59
|
+
// is worse than the latency hit of cheap-fast on a hard task.
|
|
60
|
+
function chooseModel(taskHint) {
|
|
61
|
+
switch (taskHint) {
|
|
62
|
+
case 'code':
|
|
63
|
+
return MODELS.code;
|
|
64
|
+
case 'multi-agent':
|
|
65
|
+
return MODELS['multi-agent'];
|
|
66
|
+
case 'reasoning-deep':
|
|
67
|
+
return MODELS['reasoning-deep'];
|
|
68
|
+
case 'reasoning-quick':
|
|
69
|
+
case 'fast-reasoning':
|
|
70
|
+
return MODELS['fast-reasoning'];
|
|
71
|
+
case 'reasoning-non-cot':
|
|
72
|
+
return MODELS['reasoning-non-cot'];
|
|
73
|
+
case 'flagship':
|
|
74
|
+
return MODELS.flagship;
|
|
75
|
+
case 'budget-compact':
|
|
76
|
+
return MODELS['budget-compact'];
|
|
77
|
+
case 'fast-non-reasoning':
|
|
78
|
+
case undefined:
|
|
79
|
+
case null:
|
|
80
|
+
case '':
|
|
81
|
+
return MODELS['fast-non-reasoning'];
|
|
82
|
+
default:
|
|
83
|
+
// Accept legacy aliases verbatim; otherwise fall back to cheap-fast.
|
|
84
|
+
if (LEGACY_ALIASES[taskHint]) return LEGACY_ALIASES[taskHint];
|
|
85
|
+
return MODELS['fast-non-reasoning'];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// getModelInfo — for the launcher / dashboard cost annotations (Sprint 46).
|
|
90
|
+
// Returns the price band so the UI can render a $-tier indicator alongside
|
|
91
|
+
// the model name without each caller knowing the table.
|
|
92
|
+
function getModelInfo(modelId) {
|
|
93
|
+
const cheap = new Set([
|
|
94
|
+
MODELS['fast-non-reasoning'],
|
|
95
|
+
MODELS['fast-reasoning'],
|
|
96
|
+
MODELS.code,
|
|
97
|
+
]);
|
|
98
|
+
const heavy = new Set([
|
|
99
|
+
MODELS['reasoning-deep'],
|
|
100
|
+
MODELS['reasoning-non-cot'],
|
|
101
|
+
MODELS['multi-agent'],
|
|
102
|
+
]);
|
|
103
|
+
if (cheap.has(modelId)) return { tier: 'cheap', priceIn: 0.2, priceOut: modelId === MODELS.code ? 1.5 : 0.5 };
|
|
104
|
+
if (heavy.has(modelId)) return { tier: 'heavy', priceIn: 2, priceOut: 6 };
|
|
105
|
+
if (modelId === MODELS.flagship) return { tier: 'flagship', priceIn: 3, priceOut: 15 };
|
|
106
|
+
if (modelId === MODELS['budget-compact']) return { tier: 'budget', priceIn: 0.3, priceOut: 0.5 };
|
|
107
|
+
return { tier: 'unknown', priceIn: null, priceOut: null };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = {
|
|
111
|
+
MODELS,
|
|
112
|
+
LEGACY_ALIASES,
|
|
113
|
+
chooseModel,
|
|
114
|
+
getModelInfo,
|
|
115
|
+
};
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// Grok adapter (superagent-ai grok-dev CLI) — Sprint 45 T3
|
|
2
|
+
//
|
|
3
|
+
// Implements the 7-field adapter contract documented in ./claude.js and
|
|
4
|
+
// docs/AGENT-RUNTIMES.md § 5. TUI mode by default — conversation persists
|
|
5
|
+
// inside the PTY process for the lifetime of the panel, matching the Claude
|
|
6
|
+
// Code pattern. Headless `grok --prompt` is reserved for orchestrator
|
|
7
|
+
// background tasks (Sprint 46+) and is NOT this adapter's spawn shape.
|
|
8
|
+
//
|
|
9
|
+
// Lane-time empirical findings (Sprint 45 T3, 2026-05-01) — see
|
|
10
|
+
// docs/multi-agent-substrate/SPRINT-45-PREP-NOTES.md and Sprint 45 STATUS.md
|
|
11
|
+
// for the full investigation:
|
|
12
|
+
//
|
|
13
|
+
// • grok-dev v1.1.5, binary `/usr/local/bin/grok` (#!/usr/bin/env bun)
|
|
14
|
+
// • Session storage: SQLite at ~/.grok/grok.db, NOT JSON files in
|
|
15
|
+
// ~/.grok/sessions/. Tables (STRICT, requires SQLite ≥3.37):
|
|
16
|
+
// sessions(id, workspace_id, title, model, mode, status, created_at, ...)
|
|
17
|
+
// messages(session_id, seq, role, message_json, created_at)
|
|
18
|
+
// tool_calls, tool_results, usage_events, compactions
|
|
19
|
+
// `messages.message_json` is a JSON blob in AI SDK provider shape:
|
|
20
|
+
// { role: 'user'|'assistant'|'tool', content: string | Array<...> }
|
|
21
|
+
// where array parts are { type: 'text', text } | { type: 'tool-call', ... }
|
|
22
|
+
// | { type: 'tool-result', ... }. Sprint 45 T4 wires the memory hook to
|
|
23
|
+
// extract from grok.db and feed parseTranscript a JSON envelope.
|
|
24
|
+
//
|
|
25
|
+
// • TUI shimmer text strings (the canonical "thinking" indicator):
|
|
26
|
+
// "Planning next moves" — default isProcessing without stream content
|
|
27
|
+
// "Generating plan..." — plan-mode label
|
|
28
|
+
// "Answering…" — /btw overlay
|
|
29
|
+
// • Tool indicators: TUI renders `→ <label>` (InlineTool component);
|
|
30
|
+
// headless mode emits `▸ <label>`. Both forms accepted.
|
|
31
|
+
// • Sub-agents: 5 built-in (general / explore / vision / verify / computer)
|
|
32
|
+
// plus up to 12 user-defined customs on grok-4.20-multi-agent-0309
|
|
33
|
+
// (16-agent ceiling). Sub-agent fan-out is internal to grok-dev — the
|
|
34
|
+
// adapter doesn't need to surface per-sub-agent status; the parent CLI
|
|
35
|
+
// emits SubagentTaskLine entries that show through as inline tool calls.
|
|
36
|
+
// • Empty-state placeholder: "Message Grok…" — used only as a weak idle
|
|
37
|
+
// hint, not a load-bearing pattern.
|
|
38
|
+
//
|
|
39
|
+
// Cost band: 'subscription'. Joshua's SuperGrok Heavy carries the rate
|
|
40
|
+
// limits; non-Heavy users supply GROK_API_KEY / XAI_API_KEY via secrets.env
|
|
41
|
+
// (which the spawn inherits from process.env automatically — no need to
|
|
42
|
+
// re-list it in spawn.env).
|
|
43
|
+
|
|
44
|
+
'use strict';
|
|
45
|
+
|
|
46
|
+
const { chooseModel } = require('./grok-models');
|
|
47
|
+
|
|
48
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
49
|
+
// Patterns — observed from grok-dev@1.1.5 source (dist/ui/app.js) plus
|
|
50
|
+
// Joshua's smoke test on 2026-05-01. TUI is OpenTUI/React-rendered with
|
|
51
|
+
// frequent redraws; patterns must survive ANSI strip and partial chunks.
|
|
52
|
+
// Conservative bias: false negatives (missed status updates) are cheaper
|
|
53
|
+
// than false positives (badge flapping or spurious 'errored' status).
|
|
54
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
// Prompt indicator — the TUI's empty-state placeholder. Weak signal but the
|
|
57
|
+
// only stable string that appears reliably in TUI output. Sprint 46 T4 may
|
|
58
|
+
// refine if a more precise marker is observed.
|
|
59
|
+
const PROMPT = /Message Grok[….]/;
|
|
60
|
+
|
|
61
|
+
// Thinking — Grok's three known "isProcessing" shimmer states. Hits any of
|
|
62
|
+
// the literal labels. The trailing variants on "Generating" / "Answering"
|
|
63
|
+
// cover both ASCII `...` and Unicode ellipsis.
|
|
64
|
+
const THINKING = /Planning next moves|Generating plan[….]|Answering[….]/;
|
|
65
|
+
|
|
66
|
+
// Tool — TUI inline-tool prefix `→ ` (in box layout) OR headless `▸ `
|
|
67
|
+
// (yellow ANSI in dist/headless/output.js:23). Anchored on the leading
|
|
68
|
+
// glyph + space to avoid mid-line `→` in prose markdown firing as a tool.
|
|
69
|
+
// Also catches the activity strings emitted by long-running tools.
|
|
70
|
+
const TOOL = /(?:^|\n)\s*[→▸]\s|Running command[….]|Starting process[….]/;
|
|
71
|
+
|
|
72
|
+
// Editing — Grok's TUI prefixes file-mutation tool calls with `Edit` /
|
|
73
|
+
// `Write` / `Read` / `Run` labels rendered through InlineTool. Match these
|
|
74
|
+
// after the tool glyph; the toolLabel function uses these verbatim.
|
|
75
|
+
const EDITING = /(?:^|\n)\s*[→▸]\s+(Edit|Write|Read|Run|Create|Update|Delete)\b/;
|
|
76
|
+
const EDITING_DETAIL = /(?:^|\n)\s*[→▸]\s+((?:Edit|Write|Read|Run|Create|Update|Delete)\b[^\n]*)/;
|
|
77
|
+
|
|
78
|
+
// Idle — empty-state shows the placeholder and the cwd footer line. Use the
|
|
79
|
+
// placeholder only — cwd shape varies by terminal width and home expansion.
|
|
80
|
+
const IDLE = /Message Grok[….]\s*$/m;
|
|
81
|
+
|
|
82
|
+
// Error — line-anchored variant matching Claude's strategy. Grok's tool
|
|
83
|
+
// output (grep, test logs, lsp diagnostics) routinely carries "Error" /
|
|
84
|
+
// "error" mid-line in a way that should NOT flip the panel to errored. Only
|
|
85
|
+
// fire on line-leading failure phrases — same conservative shape as Claude
|
|
86
|
+
// uses, plus the Grok-specific BtwOverlay error fallback "Something went
|
|
87
|
+
// wrong." literal (rendered in t.diffRemovedFg).
|
|
88
|
+
const ERROR = /(?:^|\n)\s*(?:(?:error|Error|ERROR|exception|Exception|Traceback|fatal|Fatal|FATAL|panic|EACCES|ECONNREFUSED|ENOENT|command not found|cannot find module|failed with exit code|Permission denied|Something went wrong)\b)/m;
|
|
89
|
+
|
|
90
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
91
|
+
// statusFor — replaces the absent grok branch in session.js _updateStatus.
|
|
92
|
+
// Order matches Claude's: thinking → editing → tool → idle. First match
|
|
93
|
+
// wins. Returns null on no-match so the caller leaves status untouched
|
|
94
|
+
// (preserves the "no fallthrough" semantics _updateStatus relies on).
|
|
95
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
function statusFor(data) {
|
|
98
|
+
if (typeof data !== 'string') return null;
|
|
99
|
+
if (THINKING.test(data)) {
|
|
100
|
+
return { status: 'thinking', statusDetail: 'Grok is reasoning...' };
|
|
101
|
+
}
|
|
102
|
+
if (EDITING.test(data)) {
|
|
103
|
+
const match = data.match(EDITING_DETAIL);
|
|
104
|
+
return {
|
|
105
|
+
status: 'editing',
|
|
106
|
+
statusDetail: match ? match[1].slice(0, 80) : 'Editing files',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (TOOL.test(data)) {
|
|
110
|
+
return { status: 'active', statusDetail: 'Using tools' };
|
|
111
|
+
}
|
|
112
|
+
if (IDLE.test(data)) {
|
|
113
|
+
return { status: 'idle', statusDetail: 'Waiting for input' };
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
119
|
+
// parseTranscript — Grok stores messages in SQLite (~/.grok/grok.db), not
|
|
120
|
+
// in a JSONL file. The adapter contract is `(raw: string) => Memory[]`, so
|
|
121
|
+
// the caller (the memory-session-end hook, refactored in Sprint 45 T4) is
|
|
122
|
+
// responsible for extracting `messages.message_json` rows from grok.db and
|
|
123
|
+
// passing them in as a JSON string envelope. Two accepted shapes:
|
|
124
|
+
//
|
|
125
|
+
// 1. JSON array of message objects (preferred):
|
|
126
|
+
// '[{"role":"user","content":"hi"},{"role":"assistant","content":[...]}]'
|
|
127
|
+
// 2. JSONL — one message JSON per line (back-compat with hooks that
|
|
128
|
+
// replay grok.db rows verbatim):
|
|
129
|
+
// '{"role":"user","content":"hi"}\n{"role":"assistant","content":[...]}'
|
|
130
|
+
//
|
|
131
|
+
// Both fall through to the same per-message loop. message.content matches
|
|
132
|
+
// the AI SDK provider shape: string OR array of { type: 'text', text } |
|
|
133
|
+
// { type: 'tool-call', ... } | { type: 'tool-result', ... }. We extract the
|
|
134
|
+
// text parts only — tool calls and results are surfaced via the `tool_calls`
|
|
135
|
+
// and `tool_results` tables in grok.db, which the hook layer treats
|
|
136
|
+
// separately if it wants tool-trace memories.
|
|
137
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
function _extractText(content) {
|
|
140
|
+
if (typeof content === 'string') return content;
|
|
141
|
+
if (Array.isArray(content)) {
|
|
142
|
+
return content
|
|
143
|
+
.filter((c) => c && c.type === 'text' && typeof c.text === 'string')
|
|
144
|
+
.map((c) => c.text)
|
|
145
|
+
.join(' ');
|
|
146
|
+
}
|
|
147
|
+
return '';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseTranscript(raw) {
|
|
151
|
+
if (typeof raw !== 'string' || raw.length === 0) return [];
|
|
152
|
+
|
|
153
|
+
// Try JSON-array first — the preferred envelope.
|
|
154
|
+
let messages = null;
|
|
155
|
+
try {
|
|
156
|
+
const parsed = JSON.parse(raw);
|
|
157
|
+
if (Array.isArray(parsed)) messages = parsed;
|
|
158
|
+
} catch (_) { /* fall through to JSONL */ }
|
|
159
|
+
|
|
160
|
+
// JSONL fallback — line-by-line parse, skip malformed lines (matches
|
|
161
|
+
// Claude adapter's tolerance).
|
|
162
|
+
if (!messages) {
|
|
163
|
+
messages = [];
|
|
164
|
+
for (const line of raw.split('\n')) {
|
|
165
|
+
const trimmed = line.trim();
|
|
166
|
+
if (!trimmed) continue;
|
|
167
|
+
try {
|
|
168
|
+
const obj = JSON.parse(trimmed);
|
|
169
|
+
if (obj && typeof obj === 'object') messages.push(obj);
|
|
170
|
+
} catch (_) { continue; }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const out = [];
|
|
175
|
+
for (const msg of messages) {
|
|
176
|
+
if (!msg || typeof msg !== 'object') continue;
|
|
177
|
+
const role = msg.role;
|
|
178
|
+
if (role !== 'user' && role !== 'assistant') continue;
|
|
179
|
+
const text = _extractText(msg.content);
|
|
180
|
+
if (text) out.push({ role, content: text.slice(0, 400) });
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
186
|
+
// bootPromptTemplate — Grok reads `AGENTS.md` (per docs/AGENT-RUNTIMES.md
|
|
187
|
+
// § 4: convergent file with Codex via the sync-agent-instructions.js
|
|
188
|
+
// generator). The boot block points the lane at AGENTS.md instead of
|
|
189
|
+
// CLAUDE.md and uses the same `memory_recall + read instructional file +
|
|
190
|
+
// read sprint docs` shape as Claude. Sprint 46 T2 will refine per-agent
|
|
191
|
+
// boot prompts further; this is the contract-complete placeholder.
|
|
192
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
function bootPromptTemplate(lane = {}, sprint = {}) {
|
|
195
|
+
const tn = lane.id || 'T?';
|
|
196
|
+
const sprintNum = sprint.number || '?';
|
|
197
|
+
const sprintName = sprint.name || 'unnamed';
|
|
198
|
+
const project = lane.project || sprint.project || 'termdeck';
|
|
199
|
+
const briefing = lane.briefingPath || `docs/sprint-${sprintNum}-${sprintName}/${tn}-<lane>.md`;
|
|
200
|
+
const topic = lane.topic || lane.briefingPath || sprintName;
|
|
201
|
+
return [
|
|
202
|
+
`You are ${tn} in Sprint ${sprintNum} (${sprintName}). Boot sequence:`,
|
|
203
|
+
`1. memory_recall(project="${project}", query="${topic}")`,
|
|
204
|
+
`2. memory_recall(query="recent decisions and bugs")`,
|
|
205
|
+
`3. Read ~/.claude/CLAUDE.md and ./AGENTS.md`,
|
|
206
|
+
`4. Read docs/sprint-${sprintNum}-${sprintName}/PLANNING.md`,
|
|
207
|
+
`5. Read docs/sprint-${sprintNum}-${sprintName}/STATUS.md`,
|
|
208
|
+
`6. Read ${briefing}`,
|
|
209
|
+
'',
|
|
210
|
+
'Then begin. Stay in your lane. Post FINDING / FIX-PROPOSED / DONE in STATUS.md.',
|
|
211
|
+
"Don't bump versions, don't touch CHANGELOG, don't commit.",
|
|
212
|
+
].join('\n');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
216
|
+
// Adapter export. spawn.env.GROK_MODEL defaults to the cheap-fast tier;
|
|
217
|
+
// per-lane override is the launcher's job at session-spawn time (Sprint 46
|
|
218
|
+
// reads `agent: grok` + optional `model-hint: code|reasoning-deep|...` from
|
|
219
|
+
// the lane brief frontmatter and overlays). GROK_API_KEY isn't repeated in
|
|
220
|
+
// spawn.env because the PTY inherits it from the TermDeck server's process
|
|
221
|
+
// env; the secrets.env load at server boot is the canonical path.
|
|
222
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
const grokAdapter = {
|
|
225
|
+
name: 'grok',
|
|
226
|
+
sessionType: 'grok',
|
|
227
|
+
matches: (cmd) => typeof cmd === 'string' && /(?:^|\s|\/)grok(?:\b|$)/i.test(cmd),
|
|
228
|
+
spawn: {
|
|
229
|
+
binary: 'grok',
|
|
230
|
+
defaultArgs: [],
|
|
231
|
+
env: {
|
|
232
|
+
GROK_MODEL: chooseModel(),
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
patterns: {
|
|
236
|
+
prompt: PROMPT,
|
|
237
|
+
thinking: THINKING,
|
|
238
|
+
editing: EDITING,
|
|
239
|
+
tool: TOOL,
|
|
240
|
+
idle: IDLE,
|
|
241
|
+
error: ERROR,
|
|
242
|
+
},
|
|
243
|
+
patternNames: {
|
|
244
|
+
error: 'grok-error',
|
|
245
|
+
tool: 'grok-tool',
|
|
246
|
+
},
|
|
247
|
+
statusFor,
|
|
248
|
+
parseTranscript,
|
|
249
|
+
bootPromptTemplate,
|
|
250
|
+
costBand: 'subscription',
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
module.exports = grokAdapter;
|
|
@@ -13,12 +13,18 @@
|
|
|
13
13
|
// and Sprint 45 T4 wires the launcher UI through the same registry.
|
|
14
14
|
|
|
15
15
|
const claude = require('./claude');
|
|
16
|
+
const codex = require('./codex');
|
|
17
|
+
const gemini = require('./gemini');
|
|
18
|
+
const grok = require('./grok');
|
|
16
19
|
|
|
17
20
|
// Keyed by adapter name (NOT session.meta.type — adapters expose their own
|
|
18
21
|
// `sessionType` field for that mapping). Order is iteration order for the
|
|
19
22
|
// detect loop in session.js, so list more-specific adapters before less.
|
|
20
23
|
const AGENT_ADAPTERS = {
|
|
21
24
|
claude,
|
|
25
|
+
codex,
|
|
26
|
+
gemini,
|
|
27
|
+
grok,
|
|
22
28
|
};
|
|
23
29
|
|
|
24
30
|
// Convenience accessor — returns the adapter whose `sessionType` matches the
|
|
@@ -8,7 +8,9 @@ const { WebSocketServer } = require('ws');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
10
|
const fs = require('fs');
|
|
11
|
+
const dns = require('dns');
|
|
11
12
|
const { v4: uuidv4 } = require('uuid');
|
|
13
|
+
const { createCachedLookup, createFailureLogger } = require('./rumen-pool-resilience');
|
|
12
14
|
|
|
13
15
|
// Conditional imports (graceful fallback if not installed yet)
|
|
14
16
|
let pty, Database, pg;
|
|
@@ -19,10 +21,18 @@ try { pg = require('pg'); } catch { pg = null; }
|
|
|
19
21
|
// Module-level singleton Postgres pool for rumen_insights (petvetbid DB).
|
|
20
22
|
// Lazy-initialized on first rumen endpoint hit so startup stays fast and
|
|
21
23
|
// servers without DATABASE_URL never pay the connection cost.
|
|
24
|
+
//
|
|
25
|
+
// DNS-resilience (Sprint 45 side-task): the pool is constructed with a
|
|
26
|
+
// cached `lookup` function that retries DNS failures with jittered
|
|
27
|
+
// exponential backoff and serves stale entries during transient outages.
|
|
28
|
+
// Pool errors / recoveries flow through a recency-graded logger so a
|
|
29
|
+
// flapping host doesn't flood the log.
|
|
22
30
|
let _rumenPool = null;
|
|
23
31
|
let _rumenPoolFailed = false;
|
|
24
32
|
let _rumenPoolFailedAt = 0;
|
|
25
33
|
const RUMEN_POOL_RETRY_MS = 30_000;
|
|
34
|
+
const _rumenLookup = createCachedLookup(dns);
|
|
35
|
+
const _rumenLogger = createFailureLogger(console);
|
|
26
36
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
27
37
|
function getRumenPool() {
|
|
28
38
|
if (_rumenPool) return _rumenPool;
|
|
@@ -38,14 +48,14 @@ function getRumenPool() {
|
|
|
38
48
|
connectionString: process.env.DATABASE_URL,
|
|
39
49
|
max: 4,
|
|
40
50
|
idleTimeoutMillis: 30000,
|
|
41
|
-
connectionTimeoutMillis: 5000
|
|
42
|
-
|
|
43
|
-
_rumenPool.on('error', (err) => {
|
|
44
|
-
console.warn('[rumen] pg pool error:', err.message);
|
|
51
|
+
connectionTimeoutMillis: 5000,
|
|
52
|
+
lookup: _rumenLookup,
|
|
45
53
|
});
|
|
54
|
+
_rumenPool.on('error', (err) => _rumenLogger.logFailure(`pg pool error: ${err.message}`));
|
|
55
|
+
_rumenPool.on('connect', () => _rumenLogger.logRecovery());
|
|
46
56
|
return _rumenPool;
|
|
47
57
|
} catch (err) {
|
|
48
|
-
|
|
58
|
+
_rumenLogger.logFailure(`failed to create pg pool: ${err.message}`);
|
|
49
59
|
_rumenPoolFailed = true;
|
|
50
60
|
_rumenPoolFailedAt = Date.now();
|
|
51
61
|
return null;
|
|
@@ -69,6 +79,7 @@ const { createGraphRoutes } = require('./graph-routes');
|
|
|
69
79
|
const { createProjectsRoutes } = require('./projects-routes');
|
|
70
80
|
const orchestrationPreview = require('./orchestration-preview');
|
|
71
81
|
const { createPtyReaper } = require('./pty-reaper');
|
|
82
|
+
const { AGENT_ADAPTERS } = require('./agent-adapters');
|
|
72
83
|
|
|
73
84
|
// Sprint 37 T3 — lazy resolution of T2's CLI modules. The orchestration-preview
|
|
74
85
|
// helper is decoupled from T2's templates.js / init-project.js; we resolve
|
|
@@ -1244,6 +1255,28 @@ function createServer(config) {
|
|
|
1244
1255
|
res.json(t);
|
|
1245
1256
|
});
|
|
1246
1257
|
|
|
1258
|
+
// GET /api/agent-adapters - serializable projection of the multi-agent
|
|
1259
|
+
// registry for the launcher. Sprint 45 T4: replaces the hardcoded
|
|
1260
|
+
// claude/cc/gemini/python branches in app.js with a registry-driven
|
|
1261
|
+
// detector. Each entry exposes only the fields the client needs:
|
|
1262
|
+
// • name — adapter id ("claude", "codex", "gemini", "grok")
|
|
1263
|
+
// • sessionType — meta.type the launcher should set
|
|
1264
|
+
// • binary — canonical command name; client matches `^binary\b` (i)
|
|
1265
|
+
// • costBand — 'free' | 'pay-per-token' | 'subscription' (Sprint 46
|
|
1266
|
+
// surfaces this in PLANNING.md cost annotations)
|
|
1267
|
+
// Functions / RegExps are NOT serialized — match logic lives client-side
|
|
1268
|
+
// and uses the binary as the prefix anchor. Adapter-specific shorthand
|
|
1269
|
+
// (e.g. `cc` → `claude`) is normalized in app.js before this lookup.
|
|
1270
|
+
app.get('/api/agent-adapters', (req, res) => {
|
|
1271
|
+
const list = Object.values(AGENT_ADAPTERS).map((a) => ({
|
|
1272
|
+
name: a.name,
|
|
1273
|
+
sessionType: a.sessionType,
|
|
1274
|
+
binary: a.spawn && a.spawn.binary,
|
|
1275
|
+
costBand: a.costBand,
|
|
1276
|
+
}));
|
|
1277
|
+
res.json(list);
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1247
1280
|
// Public-shape helper so GET and PATCH return the same envelope.
|
|
1248
1281
|
function publicConfigPayload() {
|
|
1249
1282
|
return {
|
|
@@ -1650,10 +1683,16 @@ function createServer(config) {
|
|
|
1650
1683
|
if (!pool) return res.json({ enabled: false });
|
|
1651
1684
|
|
|
1652
1685
|
try {
|
|
1686
|
+
// Sprint 45 side-task 2 — order by COALESCE(started_at, completed_at) so
|
|
1687
|
+
// jobs whose upstream writer (the @jhizzard/rumen createJob INSERT in the
|
|
1688
|
+
// Edge Function) leaves started_at NULL still surface as "latest" via
|
|
1689
|
+
// their populated completed_at. Pre-fix the query returned a 2026-04-16
|
|
1690
|
+
// job permanently because that was the last row to have started_at
|
|
1691
|
+
// populated — every subsequent insert lands started_at = NULL.
|
|
1653
1692
|
const jobSql =
|
|
1654
1693
|
`SELECT id, status, completed_at, sessions_processed, insights_generated
|
|
1655
1694
|
FROM rumen_jobs
|
|
1656
|
-
ORDER BY started_at DESC
|
|
1695
|
+
ORDER BY COALESCE(started_at, completed_at) DESC NULLS LAST
|
|
1657
1696
|
LIMIT 1`;
|
|
1658
1697
|
const insightSql =
|
|
1659
1698
|
`SELECT
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Sprint 45 side-task — DNS-resilience policy for the rumen pg.Pool.
|
|
2
|
+
//
|
|
3
|
+
// Two factories, both DI-friendly so tests can stub dns + console:
|
|
4
|
+
//
|
|
5
|
+
// createCachedLookup(dnsModule, opts)
|
|
6
|
+
// Returns a (hostname, options, callback) function suitable for
|
|
7
|
+
// pg.Pool's `lookup` config. Caches successful lookups for
|
|
8
|
+
// `cacheTtlMs` (default 30s). On lookup failure, retries with
|
|
9
|
+
// jittered exponential backoff up to `backoffCapsMs.length`
|
|
10
|
+
// attempts (default [100, 500, 2000, 5000]). If every retry fails
|
|
11
|
+
// and a stale cached address exists, serves stale rather than
|
|
12
|
+
// failing — DNS flickers shouldn't tear the pool down.
|
|
13
|
+
//
|
|
14
|
+
// createFailureLogger(consoleModule, opts)
|
|
15
|
+
// Returns { logFailure, logRecovery } closures owning a private
|
|
16
|
+
// failure-window state. First failure logs `warn`; consecutive
|
|
17
|
+
// failures within `windowMs` (default 60s) downgrade to `debug`;
|
|
18
|
+
// a recovery after any prior failure logs `info` once and clears
|
|
19
|
+
// the window. Idempotent recovery (no failures pending) is silent.
|
|
20
|
+
//
|
|
21
|
+
// Both factories are pure — no module-scope state, no side effects on
|
|
22
|
+
// require — so tests can construct fresh instances per case.
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const DEFAULT_BACKOFF_CAPS_MS = [100, 500, 2000, 5000];
|
|
27
|
+
const DEFAULT_DNS_CACHE_TTL_MS = 30_000;
|
|
28
|
+
const DEFAULT_FAILURE_WINDOW_MS = 60_000;
|
|
29
|
+
|
|
30
|
+
function _jitter(capMs, rng) {
|
|
31
|
+
return Math.floor(capMs * (0.5 + rng() * 0.5));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createCachedLookup(dnsModule, opts = {}) {
|
|
35
|
+
const cacheTtlMs = opts.cacheTtlMs ?? DEFAULT_DNS_CACHE_TTL_MS;
|
|
36
|
+
const backoffCapsMs = opts.backoffCapsMs ?? DEFAULT_BACKOFF_CAPS_MS;
|
|
37
|
+
const setTimeoutFn = opts.setTimeout ?? setTimeout;
|
|
38
|
+
const now = opts.now ?? Date.now;
|
|
39
|
+
const rng = opts.random ?? Math.random;
|
|
40
|
+
const cache = new Map();
|
|
41
|
+
|
|
42
|
+
return function cachedLookup(hostname, options, callback) {
|
|
43
|
+
if (typeof options === 'function') { callback = options; options = {}; }
|
|
44
|
+
const t = now();
|
|
45
|
+
const hit = cache.get(hostname);
|
|
46
|
+
if (hit && hit.expiresAt > t) {
|
|
47
|
+
return callback(null, hit.address, hit.family);
|
|
48
|
+
}
|
|
49
|
+
let attempt = 0;
|
|
50
|
+
const tryOnce = () => {
|
|
51
|
+
dnsModule.lookup(hostname, options, (err, address, family) => {
|
|
52
|
+
if (!err) {
|
|
53
|
+
cache.set(hostname, { address, family, expiresAt: now() + cacheTtlMs });
|
|
54
|
+
return callback(null, address, family);
|
|
55
|
+
}
|
|
56
|
+
if (attempt >= backoffCapsMs.length) {
|
|
57
|
+
if (hit) return callback(null, hit.address, hit.family);
|
|
58
|
+
return callback(err);
|
|
59
|
+
}
|
|
60
|
+
const delay = _jitter(backoffCapsMs[attempt++], rng);
|
|
61
|
+
setTimeoutFn(tryOnce, delay);
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
tryOnce();
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createFailureLogger(consoleModule, opts = {}) {
|
|
69
|
+
const windowMs = opts.windowMs ?? DEFAULT_FAILURE_WINDOW_MS;
|
|
70
|
+
const prefix = opts.prefix ?? '[rumen]';
|
|
71
|
+
const now = opts.now ?? Date.now;
|
|
72
|
+
let firstAt = 0;
|
|
73
|
+
let lastAt = 0;
|
|
74
|
+
let count = 0;
|
|
75
|
+
|
|
76
|
+
function logFailure(message) {
|
|
77
|
+
const t = now();
|
|
78
|
+
if (firstAt > 0 && (t - lastAt) < windowMs) {
|
|
79
|
+
count += 1;
|
|
80
|
+
lastAt = t;
|
|
81
|
+
const debug = consoleModule.debug || consoleModule.log;
|
|
82
|
+
debug(`${prefix} (debounced ${count}) ${message}`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
firstAt = t;
|
|
86
|
+
lastAt = t;
|
|
87
|
+
count = 1;
|
|
88
|
+
consoleModule.warn(`${prefix} ${message}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function logRecovery(message) {
|
|
92
|
+
if (firstAt === 0) return;
|
|
93
|
+
const info = consoleModule.info || consoleModule.log;
|
|
94
|
+
info(`${prefix} recovered after ${count} failure(s)${message ? ` — ${message}` : ''}`);
|
|
95
|
+
firstAt = 0;
|
|
96
|
+
lastAt = 0;
|
|
97
|
+
count = 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function _state() { return { firstAt, lastAt, count }; }
|
|
101
|
+
|
|
102
|
+
return { logFailure, logRecovery, _state };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
createCachedLookup,
|
|
107
|
+
createFailureLogger,
|
|
108
|
+
DEFAULT_BACKOFF_CAPS_MS,
|
|
109
|
+
DEFAULT_DNS_CACHE_TTL_MS,
|
|
110
|
+
DEFAULT_FAILURE_WINDOW_MS,
|
|
111
|
+
};
|
|
@@ -15,7 +15,7 @@ const os = require('os');
|
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const { resolveTheme } = require('./theme-resolver');
|
|
17
17
|
const flashbackDiag = require('./flashback-diag');
|
|
18
|
-
const
|
|
18
|
+
const geminiAdapter = require('./agent-adapters/gemini');
|
|
19
19
|
const { detectAdapter, getAdapterForSessionType } = require('./agent-adapters');
|
|
20
20
|
|
|
21
21
|
// Strip ANSI escape codes for pattern matching
|
|
@@ -29,24 +29,31 @@ function stripAnsi(str) {
|
|
|
29
29
|
|
|
30
30
|
// Pattern matchers for detecting terminal type and status.
|
|
31
31
|
//
|
|
32
|
-
// Sprint
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
32
|
+
// Sprint 45 T4 removed the Sprint 44 T3 Claude shim (`PATTERNS.claudeCode.*`
|
|
33
|
+
// and `PATTERNS.errorLineStart`). Claude-specific regexes now live exclusively
|
|
34
|
+
// at ./agent-adapters/claude.js — read via `claudeAdapter.patterns.*`. The
|
|
35
|
+
// `_detectErrors` and `_updateStatus` paths route through `getAdapterForSessionType`
|
|
36
|
+
// for any registered adapter.
|
|
37
|
+
//
|
|
38
|
+
// Sprint 45 T2 retains `PATTERNS.geminiCli` as a shim into the Gemini adapter
|
|
39
|
+
// for the one-release deprecation horizon — same pattern Sprint 44 T3 used.
|
|
40
|
+
// What stays in this file:
|
|
41
|
+
// • geminiCli — Sprint 45 T2 shim into ./agent-adapters/gemini.js
|
|
42
|
+
// • pythonServer — server SUBTYPE detection (no adapter; status-badge only)
|
|
43
|
+
// • shell — default fallback (no adapter)
|
|
44
|
+
// • error — cross-agent prose-shape primary error fallback (used
|
|
45
|
+
// by `_detectErrors` when the active adapter has no
|
|
46
|
+
// `patterns.error`, and exported for tests)
|
|
47
|
+
// • shellError — cross-agent Unix shell-error shapes (always tried as
|
|
48
|
+
// the secondary fallback in `_detectErrors`)
|
|
39
49
|
const PATTERNS = {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
tool: claudeAdapter.patterns.tool,
|
|
45
|
-
idle: claudeAdapter.patterns.idle
|
|
46
|
-
},
|
|
50
|
+
// Sprint 45 T2: geminiCli patterns are owned by the Gemini adapter at
|
|
51
|
+
// ./agent-adapters/gemini.js. Shim preserves the legacy
|
|
52
|
+
// `PATTERNS.geminiCli.{prompt,thinking}` shape — same regex objects, so
|
|
53
|
+
// any external reference equality holds.
|
|
47
54
|
geminiCli: {
|
|
48
|
-
prompt:
|
|
49
|
-
thinking:
|
|
55
|
+
prompt: geminiAdapter.patterns.prompt,
|
|
56
|
+
thinking: geminiAdapter.patterns.thinking,
|
|
50
57
|
},
|
|
51
58
|
pythonServer: {
|
|
52
59
|
uvicorn: /Uvicorn running on/,
|
|
@@ -90,17 +97,12 @@ const PATTERNS = {
|
|
|
90
97
|
// child-process error reporting fire without depending on the line ALSO
|
|
91
98
|
// containing the `No such file or directory` prose phrase.
|
|
92
99
|
error: /(?:^|\n)\s*(?:Error:\s+\S|error:\s+\S|ERROR:\s+\S|Traceback \(most recent call last\):|npm ERR!|error\[E\d+\]:|Uncaught Exception|Fatal:|ENOENT:\s+\S|EACCES:\s+\S|ECONNREFUSED:\s+\S)/m,
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
// Sprint 44 T3: this regex is now owned by the Claude adapter
|
|
100
|
-
// (./agent-adapters/claude.js patterns.error). The shim below preserves
|
|
101
|
-
// the legacy PATTERNS.errorLineStart export — same regex object, so any
|
|
102
|
-
// existing reference equality (e.g. `=== PATTERNS.errorLineStart`) holds.
|
|
103
|
-
errorLineStart: claudeAdapter.patterns.error,
|
|
100
|
+
// Sprint 45 T4: the Claude-specific line-anchored variant
|
|
101
|
+
// (formerly `PATTERNS.errorLineStart`) is owned by the Claude adapter at
|
|
102
|
+
// ./agent-adapters/claude.js — read via `claudeAdapter.patterns.error`.
|
|
103
|
+
// _detectErrors below routes through `getAdapterForSessionType` for
|
|
104
|
+
// claude-code sessions and falls through to PATTERNS.error / shellError
|
|
105
|
+
// for non-adapter sessions.
|
|
104
106
|
// Sprint 33: PATTERNS.error misses the most common Unix shell errors —
|
|
105
107
|
// `cat: /foo: No such file or directory`, `bash: foo: command not found`,
|
|
106
108
|
// `rm: cannot remove ...: Permission denied`. These have a colon-prefix
|
|
@@ -255,20 +257,17 @@ class Session {
|
|
|
255
257
|
}
|
|
256
258
|
|
|
257
259
|
_detectType(data) {
|
|
258
|
-
// Sprint 44 T3: registry-aware detection. detectAdapter()
|
|
259
|
-
// AGENT_ADAPTERS in declaration order and returns the first
|
|
260
|
-
// prompt regex OR command-string match.
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
// moves into gemini.js.
|
|
260
|
+
// Sprint 44 T3 + Sprint 45: registry-aware detection. detectAdapter()
|
|
261
|
+
// iterates AGENT_ADAPTERS in declaration order and returns the first
|
|
262
|
+
// hit by prompt regex OR command-string match. Claude / Codex /
|
|
263
|
+
// Gemini / Grok all live in the registry now; only python-server
|
|
264
|
+
// (a non-CLI-agent type) stays here as in-file fall-through.
|
|
264
265
|
const adapter = detectAdapter(data, this.meta.command);
|
|
265
266
|
if (adapter) {
|
|
266
267
|
this.meta.type = adapter.sessionType;
|
|
267
268
|
return;
|
|
268
269
|
}
|
|
269
|
-
if (
|
|
270
|
-
this.meta.type = 'gemini';
|
|
271
|
-
} else if (
|
|
270
|
+
if (
|
|
272
271
|
PATTERNS.pythonServer.uvicorn.test(data) ||
|
|
273
272
|
PATTERNS.pythonServer.flask.test(data) ||
|
|
274
273
|
PATTERNS.pythonServer.django.test(data) ||
|
|
@@ -282,12 +281,11 @@ class Session {
|
|
|
282
281
|
const p = PATTERNS;
|
|
283
282
|
const oldStatus = this.meta.status;
|
|
284
283
|
|
|
285
|
-
// Sprint 44 T3:
|
|
284
|
+
// Sprint 44 T3 + Sprint 45: per-agent status detection lives in each
|
|
286
285
|
// adapter's `statusFor(data)` method. Returns { status, statusDetail }
|
|
287
286
|
// on a match, null on no-change — preserves the original switch's
|
|
288
|
-
// "leave status untouched if no
|
|
289
|
-
//
|
|
290
|
-
// until Sprint 45 migrates them.
|
|
287
|
+
// "leave status untouched if no pattern fires" semantics. Only
|
|
288
|
+
// non-CLI-agent types (python-server + default shell) stay in-file.
|
|
291
289
|
const adapter = getAdapterForSessionType(this.meta.type);
|
|
292
290
|
if (adapter && typeof adapter.statusFor === 'function') {
|
|
293
291
|
const result = adapter.statusFor(data);
|
|
@@ -297,16 +295,6 @@ class Session {
|
|
|
297
295
|
}
|
|
298
296
|
} else {
|
|
299
297
|
switch (this.meta.type) {
|
|
300
|
-
case 'gemini':
|
|
301
|
-
if (p.geminiCli.thinking.test(data)) {
|
|
302
|
-
this.meta.status = 'thinking';
|
|
303
|
-
this.meta.statusDetail = 'Gemini is generating...';
|
|
304
|
-
} else if (p.geminiCli.prompt.test(data)) {
|
|
305
|
-
this.meta.status = 'idle';
|
|
306
|
-
this.meta.statusDetail = 'Waiting for input';
|
|
307
|
-
}
|
|
308
|
-
break;
|
|
309
|
-
|
|
310
298
|
case 'python-server':
|
|
311
299
|
if (p.pythonServer.request.test(data)) {
|
|
312
300
|
this.meta.status = 'active';
|
|
@@ -409,12 +397,12 @@ class Session {
|
|
|
409
397
|
// (grep matches, test results, log dumps). Use a line-anchored pattern
|
|
410
398
|
// for that session type so we don't flag content as failure.
|
|
411
399
|
//
|
|
412
|
-
// Sprint 44 T3: per-agent primary error pattern is
|
|
413
|
-
// adapter (`patterns.error` + `patternNames.error`). Falls back
|
|
414
|
-
// generic prose-shape PATTERNS.error when no adapter has claimed
|
|
415
|
-
// session type.
|
|
416
|
-
//
|
|
417
|
-
//
|
|
400
|
+
// Sprint 44 T3 / Sprint 45 T4: per-agent primary error pattern is read
|
|
401
|
+
// off the adapter (`patterns.error` + `patternNames.error`). Falls back
|
|
402
|
+
// to the generic prose-shape PATTERNS.error when no adapter has claimed
|
|
403
|
+
// the session type. (Sprint 44 retained a `PATTERNS.errorLineStart` shim
|
|
404
|
+
// that pointed at the Claude adapter's regex; Sprint 45 T4 removed the
|
|
405
|
+
// shim — read `claudeAdapter.patterns.error` directly when needed.)
|
|
418
406
|
const adapter = getAdapterForSessionType(this.meta.type);
|
|
419
407
|
const primaryPattern = adapter && adapter.patterns && adapter.patterns.error
|
|
420
408
|
? adapter.patterns.error
|