@jhizzard/termdeck 0.14.0 → 0.16.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/docs/multi-agent-substrate/boot-prompts/boot-prompt-claude.md +14 -0
- package/docs/multi-agent-substrate/boot-prompts/boot-prompt-codex.md +14 -0
- package/docs/multi-agent-substrate/boot-prompts/boot-prompt-gemini.md +14 -0
- package/docs/multi-agent-substrate/boot-prompts/boot-prompt-grok.md +14 -0
- package/package.json +2 -1
- package/packages/client/public/app.js +34 -56
- package/packages/client/public/graph.js +23 -1
- package/packages/client/public/index.html +1 -0
- package/packages/client/public/launcher-resolver.js +68 -0
- package/packages/server/src/agent-adapters/claude.js +4 -0
- package/packages/server/src/agent-adapters/codex.js +3 -0
- package/packages/server/src/agent-adapters/gemini.js +3 -0
- package/packages/server/src/agent-adapters/grok.js +16 -4
- package/packages/server/src/boot-prompt-resolver.js +81 -0
- package/packages/server/src/sprint-frontmatter.js +151 -0
- package/packages/server/src/sprint-inject.js +150 -31
- package/packages/server/src/status-merger.js +141 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
You are {{lane.tag}} in TermDeck Sprint {{sprint.n}} ({{sprint.name}}). Joshua may be orchestrating from his phone via Telegram (the orchestrator session runs with the @JoshTermDeckBot listener active via claude-tg).
|
|
2
|
+
|
|
3
|
+
Boot sequence:
|
|
4
|
+
|
|
5
|
+
1. Run `date` to time-stamp.
|
|
6
|
+
2. memory_recall(project="{{lane.project}}", query="{{lane.topic}}")
|
|
7
|
+
3. memory_recall(query="recent decisions and bugs across projects")
|
|
8
|
+
4. Read /Users/joshuaizzard/.claude/CLAUDE.md (global rules — two-stage submit, never copy-paste, session-end email mandate)
|
|
9
|
+
5. Read /Users/joshuaizzard/Documents/Graciella/ChopinNashville/SideHustles/TermDeck/termdeck/CLAUDE.md (project router)
|
|
10
|
+
6. Read {{sprint.docPath}}/PLANNING.md
|
|
11
|
+
7. Read {{sprint.docPath}}/STATUS.md
|
|
12
|
+
8. Read {{sprint.docPath}}/{{lane.briefing}} (your full briefing — authoritative)
|
|
13
|
+
|
|
14
|
+
Then begin. Stay in your lane. Post FINDING / FIX-PROPOSED / DONE in {{sprint.docPath}}/STATUS.md (append-only, with timestamps). Don't bump versions, don't touch CHANGELOG, don't commit. Orchestrator handles all close-out.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
You are {{lane.tag}} in TermDeck Sprint {{sprint.n}} ({{sprint.name}}), running on the Codex CLI. Joshua may be orchestrating from his phone via Telegram (the orchestrator session runs with the @JoshTermDeckBot listener active via claude-tg).
|
|
2
|
+
|
|
3
|
+
Boot sequence:
|
|
4
|
+
|
|
5
|
+
1. Run `date` to time-stamp.
|
|
6
|
+
2. memory_recall(project="{{lane.project}}", query="{{lane.topic}}")
|
|
7
|
+
(Mnestra MCP — wired into Codex via ~/.codex/config.toml [mcp_servers.mnestra]. If memory_recall is unavailable, fall back to: cat /Users/joshuaizzard/.claude/projects/-Users-joshuaizzard-Documents-Graciella-ChopinNashville-SideHustles-TermDeck-termdeck/memory/MEMORY.md and grep the relevant terms.)
|
|
8
|
+
3. memory_recall(query="recent decisions and bugs across projects")
|
|
9
|
+
4. Read /Users/joshuaizzard/Documents/Graciella/ChopinNashville/SideHustles/TermDeck/termdeck/AGENTS.md (project router — auto-generated mirror of CLAUDE.md via scripts/sync-agent-instructions.js; canonical content lives in CLAUDE.md but AGENTS.md is what Codex reads natively)
|
|
10
|
+
5. Read {{sprint.docPath}}/PLANNING.md
|
|
11
|
+
6. Read {{sprint.docPath}}/STATUS.md
|
|
12
|
+
7. Read {{sprint.docPath}}/{{lane.briefing}} (your full briefing — authoritative)
|
|
13
|
+
|
|
14
|
+
Then begin. Stay in your lane. Post FINDING / FIX-PROPOSED / DONE in {{sprint.docPath}}/STATUS.md (append-only, with timestamps). Use the canonical "Tn: <FINDING|FIX-PROPOSED|DONE> — <one-line summary> — <timestamp>" shape; do not prefix with emoji or wrap in code-fence — the cross-agent STATUS merger normalizes alternate shapes but the canonical form skips that pass. Don't bump versions, don't touch CHANGELOG, don't commit. Orchestrator handles all close-out.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
You are {{lane.tag}} in TermDeck Sprint {{sprint.n}} ({{sprint.name}}), running on the Gemini CLI. Joshua may be orchestrating from his phone via Telegram (the orchestrator session runs with the @JoshTermDeckBot listener active via claude-tg).
|
|
2
|
+
|
|
3
|
+
Boot sequence:
|
|
4
|
+
|
|
5
|
+
1. Run `date` to time-stamp.
|
|
6
|
+
2. memory_recall(project="{{lane.project}}", query="{{lane.topic}}")
|
|
7
|
+
(Mnestra MCP — wired into Gemini via ~/.gemini/settings.json mcpServers.mnestra. If memory_recall is unavailable, fall back to: read /Users/joshuaizzard/.claude/projects/-Users-joshuaizzard-Documents-Graciella-ChopinNashville-SideHustles-TermDeck-termdeck/memory/MEMORY.md and grep the relevant terms.)
|
|
8
|
+
3. memory_recall(query="recent decisions and bugs across projects")
|
|
9
|
+
4. Read /Users/joshuaizzard/Documents/Graciella/ChopinNashville/SideHustles/TermDeck/termdeck/GEMINI.md (project router — auto-generated mirror of CLAUDE.md via scripts/sync-agent-instructions.js; canonical content lives in CLAUDE.md but GEMINI.md is what Gemini reads natively)
|
|
10
|
+
5. Read {{sprint.docPath}}/PLANNING.md
|
|
11
|
+
6. Read {{sprint.docPath}}/STATUS.md
|
|
12
|
+
7. Read {{sprint.docPath}}/{{lane.briefing}} (your full briefing — authoritative)
|
|
13
|
+
|
|
14
|
+
Then begin. Stay in your lane. Post FINDING / FIX-PROPOSED / DONE in {{sprint.docPath}}/STATUS.md (append-only, with timestamps). Use the canonical "Tn: <FINDING|FIX-PROPOSED|DONE> — <one-line summary> — <timestamp>" shape; avoid bullet lists or prose wrapping — the cross-agent STATUS merger normalizes alternate shapes but the canonical form skips that pass. Don't bump versions, don't touch CHANGELOG, don't commit. Orchestrator handles all close-out.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
You are {{lane.tag}} in TermDeck Sprint {{sprint.n}} ({{sprint.name}}), running on the Grok CLI (SuperGrok Heavy). Joshua may be orchestrating from his phone via Telegram (the orchestrator session runs with the @JoshTermDeckBot listener active via claude-tg).
|
|
2
|
+
|
|
3
|
+
Boot sequence:
|
|
4
|
+
|
|
5
|
+
1. Run `date` to time-stamp.
|
|
6
|
+
2. memory_recall(project="{{lane.project}}", query="{{lane.topic}}")
|
|
7
|
+
(Mnestra MCP — wired into Grok via ~/.grok/user-settings.json mcpServers.mnestra. If memory_recall is unavailable, fall back to: read /Users/joshuaizzard/.claude/projects/-Users-joshuaizzard-Documents-Graciella-ChopinNashville-SideHustles-TermDeck-termdeck/memory/MEMORY.md and grep the relevant terms.)
|
|
8
|
+
3. memory_recall(query="recent decisions and bugs across projects")
|
|
9
|
+
4. Read /Users/joshuaizzard/Documents/Graciella/ChopinNashville/SideHustles/TermDeck/termdeck/AGENTS.md (project router — auto-generated mirror of CLAUDE.md via scripts/sync-agent-instructions.js; canonical content lives in CLAUDE.md but AGENTS.md is what Grok reads natively, shared with Codex)
|
|
10
|
+
5. Read {{sprint.docPath}}/PLANNING.md
|
|
11
|
+
6. Read {{sprint.docPath}}/STATUS.md
|
|
12
|
+
7. Read {{sprint.docPath}}/{{lane.briefing}} (your full briefing — authoritative)
|
|
13
|
+
|
|
14
|
+
Then begin. Stay in your lane. Post FINDING / FIX-PROPOSED / DONE in {{sprint.docPath}}/STATUS.md (append-only, with timestamps). Use the canonical "Tn: <FINDING|FIX-PROPOSED|DONE> — <one-line summary> — <timestamp>" shape; avoid free-form prose narration — the cross-agent STATUS merger normalizes alternate shapes but the canonical form skips that pass. Don't bump versions, don't touch CHANGELOG, don't commit. Orchestrator handles all close-out.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhizzard/termdeck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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"
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"config/secrets.env.example",
|
|
16
16
|
"config/transcript-migration.sql",
|
|
17
17
|
"docs/orchestrator-guide.md",
|
|
18
|
+
"docs/multi-agent-substrate/boot-prompts/*.md",
|
|
18
19
|
"LICENSE",
|
|
19
20
|
"README.md"
|
|
20
21
|
],
|
|
@@ -2479,56 +2479,20 @@
|
|
|
2479
2479
|
return;
|
|
2480
2480
|
}
|
|
2481
2481
|
|
|
2482
|
-
// Sprint 45 T4:
|
|
2483
|
-
//
|
|
2484
|
-
//
|
|
2485
|
-
// /
|
|
2486
|
-
// the python-server
|
|
2487
|
-
//
|
|
2488
|
-
//
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
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
|
-
}
|
|
2524
|
-
}
|
|
2525
|
-
resolvedCommand = adapter.binary;
|
|
2526
|
-
}
|
|
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.
|
|
2530
|
-
resolvedType = 'python-server';
|
|
2531
|
-
}
|
|
2482
|
+
// Sprint 45 T4 + Sprint 46 T4: resolver extracted to
|
|
2483
|
+
// packages/client/public/launcher-resolver.js so the same routing
|
|
2484
|
+
// logic runs in the browser AND under `node --test` (see
|
|
2485
|
+
// tests/launcher-resolver.test.js for the contract pin). Sprint 46
|
|
2486
|
+
// T4 also extended the python-server preemptive regex to recognize
|
|
2487
|
+
// `http.server` so the python topbar quick-launch button is typed
|
|
2488
|
+
// correctly from the first frame.
|
|
2489
|
+
const { resolvedCommand, resolvedType, resolvedCwd, resolvedProject } =
|
|
2490
|
+
LauncherResolver.resolve(
|
|
2491
|
+
command,
|
|
2492
|
+
project,
|
|
2493
|
+
state.agentAdapters,
|
|
2494
|
+
state.config.projects
|
|
2495
|
+
);
|
|
2532
2496
|
|
|
2533
2497
|
const session = await api('POST', '/api/sessions', {
|
|
2534
2498
|
command: resolvedCommand,
|
|
@@ -4493,18 +4457,28 @@
|
|
|
4493
4457
|
for (const sess of data.sessions) {
|
|
4494
4458
|
const id = sess.sessionId || sess.session_id || 'unknown';
|
|
4495
4459
|
const shortId = id.slice(0, 8);
|
|
4496
|
-
|
|
4460
|
+
// Server (/api/transcripts/recent) returns { sessions: [{ session_id, chunks: [...] }] }
|
|
4461
|
+
// with chunks already grouped per session in DESC created_at order. Type/project
|
|
4462
|
+
// metadata isn't on the transcripts table — fall back to optional fields if any
|
|
4463
|
+
// future server enrichment ships them.
|
|
4464
|
+
const chunks = Array.isArray(sess.chunks) ? sess.chunks : [];
|
|
4465
|
+
const type = sess.type || (chunks.length ? 'session' : 'shell');
|
|
4497
4466
|
const project = sess.project || '';
|
|
4498
|
-
const
|
|
4499
|
-
|
|
4467
|
+
const totalChunks = sess.totalLines || chunks.length;
|
|
4468
|
+
// Build preview from the most-recent chunks. Server returns DESC order, so
|
|
4469
|
+
// the first 6 entries are the newest — reverse for natural top-down reading.
|
|
4470
|
+
const previewChunks = chunks.slice(0, 6).reverse();
|
|
4471
|
+
const previewText = sess.preview
|
|
4472
|
+
? (Array.isArray(sess.preview) ? sess.preview.join('\n') : String(sess.preview))
|
|
4473
|
+
: previewChunks.map(c => (c && typeof c.content === 'string') ? c.content : '').join('');
|
|
4500
4474
|
html += `<div class="transcript-session" data-session-id="${escapeHtml(id)}">
|
|
4501
4475
|
<div class="ts-header">
|
|
4502
4476
|
<span class="ts-id">${escapeHtml(shortId)}</span>
|
|
4503
4477
|
<span class="ts-type">${escapeHtml(type)}</span>
|
|
4504
4478
|
${project ? `<span class="ts-project">${escapeHtml(project)}</span>` : ''}
|
|
4505
|
-
<span class="ts-lines">${
|
|
4479
|
+
<span class="ts-lines">${totalChunks} chunks</span>
|
|
4506
4480
|
</div>
|
|
4507
|
-
<pre class="ts-preview">${escapeHtml(
|
|
4481
|
+
<pre class="ts-preview">${escapeHtml(previewText)}</pre>
|
|
4508
4482
|
</div>`;
|
|
4509
4483
|
}
|
|
4510
4484
|
body.innerHTML = html;
|
|
@@ -4544,7 +4518,11 @@
|
|
|
4544
4518
|
const id = result.sessionId || result.session_id || 'unknown';
|
|
4545
4519
|
const shortId = id.slice(0, 8);
|
|
4546
4520
|
const line = result.line || result.content || '';
|
|
4547
|
-
|
|
4521
|
+
// Server (/api/transcripts/search) sends `created_at`; legacy `timestamp` kept
|
|
4522
|
+
// as a fallback in case a future enrichment swaps the field name.
|
|
4523
|
+
const tsSource = result.timestamp || result.created_at || '';
|
|
4524
|
+
const tsDate = tsSource ? new Date(tsSource) : null;
|
|
4525
|
+
const ts = (tsDate && !isNaN(tsDate.getTime())) ? tsDate.toLocaleTimeString() : '';
|
|
4548
4526
|
html += `<div class="transcript-result" data-session-id="${escapeHtml(id)}">
|
|
4549
4527
|
<div class="tr-meta">
|
|
4550
4528
|
<span class="tr-session">${escapeHtml(shortId)}</span>
|
|
@@ -552,7 +552,7 @@
|
|
|
552
552
|
.attr('stroke-width', 1.2)
|
|
553
553
|
.attr('filter', 'url(#nodeGlow)')
|
|
554
554
|
.style('cursor', 'pointer')
|
|
555
|
-
.on('mouseenter', (event, d) => onNodeHover(d.id))
|
|
555
|
+
.on('mouseenter', (event, d) => { onNodeHover(d.id); showNodeTooltip(event, d); })
|
|
556
556
|
.on('mouseleave', () => onNodeHover(null))
|
|
557
557
|
.on('click', (event, d) => onNodeClick(d))
|
|
558
558
|
.call(window.d3.drag()
|
|
@@ -738,6 +738,28 @@
|
|
|
738
738
|
tip.hidden = false;
|
|
739
739
|
moveTooltip(event);
|
|
740
740
|
}
|
|
741
|
+
|
|
742
|
+
// Sprint 46 T1 — node hover tooltip. Shows project (color-coded) + a short
|
|
743
|
+
// content snippet so the user can scan the graph without having to open the
|
|
744
|
+
// drawer for every node. Click still opens the full detail drawer.
|
|
745
|
+
function showNodeTooltip(event, node) {
|
|
746
|
+
const tip = $('graphTooltip');
|
|
747
|
+
if (!tip || !node) return;
|
|
748
|
+
const proj = node.project || 'global';
|
|
749
|
+
const text = escapeHtml(truncate(node.label || node.snippet || '(no content)', 80));
|
|
750
|
+
const meta = node.source_type ? `<span style="opacity:0.7">${escapeHtml(node.source_type)}</span>` : '';
|
|
751
|
+
tip.innerHTML = `<strong style="color:${hashHue(proj)}">${escapeHtml(proj)}</strong> ${meta} · ${text}`;
|
|
752
|
+
tip.hidden = false;
|
|
753
|
+
moveTooltip(event);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function escapeHtml(s) {
|
|
757
|
+
return String(s == null ? '' : s)
|
|
758
|
+
.replace(/&/g, '&')
|
|
759
|
+
.replace(/</g, '<')
|
|
760
|
+
.replace(/>/g, '>')
|
|
761
|
+
.replace(/"/g, '"');
|
|
762
|
+
}
|
|
741
763
|
function moveTooltip(event) {
|
|
742
764
|
const tip = $('graphTooltip');
|
|
743
765
|
if (tip.hidden) return;
|
|
@@ -372,6 +372,7 @@
|
|
|
372
372
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
373
373
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
374
374
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
|
|
375
|
+
<script src="launcher-resolver.js" defer></script>
|
|
375
376
|
<script src="app.js" defer></script>
|
|
376
377
|
</body>
|
|
377
378
|
</html>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// TermDeck launcher resolver — extracted Sprint 46 T4
|
|
2
|
+
//
|
|
3
|
+
// Pure function: given (command, project, agentAdapters, projects),
|
|
4
|
+
// returns the resolved spawn parameters the launcher POSTs to /api/sessions.
|
|
5
|
+
// Lives in its own file so the same code runs in the browser (via
|
|
6
|
+
// <script src="launcher-resolver.js">) AND under `node --test` (via
|
|
7
|
+
// `require('.../launcher-resolver')`). Sprint 46 T4 added this extraction
|
|
8
|
+
// to close a zero-coverage gap on the client-side routing logic — see
|
|
9
|
+
// tests/launcher-resolver.test.js for the contract pin.
|
|
10
|
+
//
|
|
11
|
+
// Sprint 45 T4 refactor lives here too: registry-driven shorthand
|
|
12
|
+
// resolution. Pre-Sprint-45 had hardcoded claude/cc/gemini/python branches;
|
|
13
|
+
// now the type detection consults `agentAdapters` (loaded from
|
|
14
|
+
// /api/agent-adapters at init), and only the Claude `cc` alias and the
|
|
15
|
+
// python-server detection (no adapter exists) stay as special-cases.
|
|
16
|
+
// Adapter matching uses an anchored prefix on the adapter's binary name
|
|
17
|
+
// (`^binary\b`, case-insensitive) which fits all four Sprint-45 adapters
|
|
18
|
+
// (claude / codex / gemini / grok) since each binary is uniquely named.
|
|
19
|
+
|
|
20
|
+
(function (root, factory) {
|
|
21
|
+
if (typeof module === 'object' && module.exports) {
|
|
22
|
+
module.exports = factory();
|
|
23
|
+
} else {
|
|
24
|
+
root.LauncherResolver = factory();
|
|
25
|
+
}
|
|
26
|
+
})(typeof self !== 'undefined' ? self : this, function () {
|
|
27
|
+
function resolve(command, project, agentAdapters, projects) {
|
|
28
|
+
let resolvedCommand = command;
|
|
29
|
+
let resolvedType = 'shell';
|
|
30
|
+
let resolvedCwd;
|
|
31
|
+
let resolvedProject = project || undefined;
|
|
32
|
+
|
|
33
|
+
let canonical = command;
|
|
34
|
+
if (/^cc\b/i.test(canonical)) {
|
|
35
|
+
canonical = canonical.replace(/^cc\b/i, 'claude');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const adapter = (agentAdapters || []).find((a) =>
|
|
39
|
+
a && a.binary && new RegExp(`^${a.binary}\\b`, 'i').test(canonical)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (adapter) {
|
|
43
|
+
resolvedType = adapter.sessionType;
|
|
44
|
+
if (adapter.name === 'claude') {
|
|
45
|
+
const argMatch = canonical.match(/^claude\s+(?:code\s+)?(.+)/i);
|
|
46
|
+
if (argMatch) {
|
|
47
|
+
const arg = argMatch[1].trim();
|
|
48
|
+
if (projects && projects[arg]) {
|
|
49
|
+
resolvedProject = arg;
|
|
50
|
+
} else {
|
|
51
|
+
resolvedCwd = arg;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
resolvedCommand = adapter.binary;
|
|
55
|
+
}
|
|
56
|
+
} else if (/^python3?\b.*(?:runserver|uvicorn|flask|gunicorn|http\.server)/i.test(canonical)) {
|
|
57
|
+
// Sprint 46 T4: extended `http\.server` so the python topbar
|
|
58
|
+
// quick-launch button is preemptively typed correctly. Without
|
|
59
|
+
// this, the badge flickers through `shell` for ~1s before
|
|
60
|
+
// session.js's runtime detection (`/Serving HTTP on/`) catches up.
|
|
61
|
+
resolvedType = 'python-server';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { resolvedCommand, resolvedType, resolvedCwd, resolvedProject };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { resolve };
|
|
68
|
+
});
|
|
@@ -153,6 +153,10 @@ const claudeAdapter = {
|
|
|
153
153
|
parseTranscript,
|
|
154
154
|
bootPromptTemplate,
|
|
155
155
|
costBand: 'pay-per-token',
|
|
156
|
+
// Sprint 47 T3 — Claude Code's input box accepts bracketed-paste cleanly.
|
|
157
|
+
// The two-stage submit pattern (paste then \r alone) is the canonical inject
|
|
158
|
+
// shape for this adapter; chunked-fallback is unnecessary.
|
|
159
|
+
acceptsPaste: true,
|
|
156
160
|
};
|
|
157
161
|
|
|
158
162
|
module.exports = claudeAdapter;
|
|
@@ -194,6 +194,9 @@ const codexAdapter = {
|
|
|
194
194
|
parseTranscript,
|
|
195
195
|
bootPromptTemplate,
|
|
196
196
|
costBand: 'pay-per-token',
|
|
197
|
+
// Sprint 47 T3 — Codex's Ratatui TUI accepts bracketed-paste per the
|
|
198
|
+
// Sprint 45 T1 audit; safe to use the two-stage submit pattern unchanged.
|
|
199
|
+
acceptsPaste: true,
|
|
197
200
|
};
|
|
198
201
|
|
|
199
202
|
module.exports = codexAdapter;
|
|
@@ -153,6 +153,9 @@ const geminiAdapter = {
|
|
|
153
153
|
parseTranscript,
|
|
154
154
|
bootPromptTemplate,
|
|
155
155
|
costBand: 'pay-per-token',
|
|
156
|
+
// Sprint 47 T3 — Gemini's CLI is paste-friendly per the single-JSON-object
|
|
157
|
+
// session shape captured in Sprint 45 T2; bracketed-paste injects cleanly.
|
|
158
|
+
acceptsPaste: true,
|
|
156
159
|
};
|
|
157
160
|
|
|
158
161
|
module.exports = geminiAdapter;
|
|
@@ -53,10 +53,16 @@ const { chooseModel } = require('./grok-models');
|
|
|
53
53
|
// than false positives (badge flapping or spurious 'errored' status).
|
|
54
54
|
// ──────────────────────────────────────────────────────────────────────────
|
|
55
55
|
|
|
56
|
-
// Prompt indicator — the
|
|
57
|
-
// only stable string
|
|
58
|
-
//
|
|
59
|
-
|
|
56
|
+
// Prompt indicator — Sprint 45 T3 anchored on the empty-state placeholder
|
|
57
|
+
// "Message Grok…" assuming it was the only stable string in TUI output.
|
|
58
|
+
// That assumption was wrong: the TUI rotates placeholders ("What are we
|
|
59
|
+
// building?", "Bring me a problem", etc.). Sprint 47 orchestrator side-task
|
|
60
|
+
// extends to also match the model-mode footer line ("Grok 4.20 Reasoning",
|
|
61
|
+
// "Grok 4.20 Heavy", "Grok 4.20 Code", "Grok 4.20 Auto", "Grok 4.20
|
|
62
|
+
// Planning") which renders on every frame regardless of which placeholder
|
|
63
|
+
// the TUI surfaced. Version number digits stay open-ended so future Grok
|
|
64
|
+
// versions don't regress detection.
|
|
65
|
+
const PROMPT = /Message Grok[….]|Grok\s+\d+(?:\.\d+)?\s+(?:Reasoning|Heavy|Code|Auto|Planning)/;
|
|
60
66
|
|
|
61
67
|
// Thinking — Grok's three known "isProcessing" shimmer states. Hits any of
|
|
62
68
|
// the literal labels. The trailing variants on "Generating" / "Answering"
|
|
@@ -248,6 +254,12 @@ const grokAdapter = {
|
|
|
248
254
|
parseTranscript,
|
|
249
255
|
bootPromptTemplate,
|
|
250
256
|
costBand: 'subscription',
|
|
257
|
+
// Sprint 47 T3 — Grok's Bun+OpenTUI input box hasn't been empirically
|
|
258
|
+
// pasted-against yet (Sprint 45 T3 prep notes flagged this for verification).
|
|
259
|
+
// Default to true so the helper uses the bracketed-paste fast path; if a
|
|
260
|
+
// lane-time test shows the OpenTUI input handler eats the paste markers,
|
|
261
|
+
// flip this to false and the inject helper falls back to chunked stdin.
|
|
262
|
+
acceptsPaste: true,
|
|
251
263
|
};
|
|
252
264
|
|
|
253
265
|
module.exports = grokAdapter;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Per-agent boot-prompt resolver — Sprint 47 T2.
|
|
4
|
+
//
|
|
5
|
+
// Reads a Mustache-style template from
|
|
6
|
+
// docs/multi-agent-substrate/boot-prompts/boot-prompt-<agent>.md, interpolates
|
|
7
|
+
// {{var.path}} placeholders against the provided `vars` object, and returns
|
|
8
|
+
// the final paste-ready string for the 4+1 inject script.
|
|
9
|
+
//
|
|
10
|
+
// Sprint 47 T3 wires the inject helper to read `lane.agent` from the lane
|
|
11
|
+
// definition and call resolveBootPrompt(agent, vars) per lane — that's how
|
|
12
|
+
// mixed 4+1 (Sprint 48+) gets agent-correct boot prompts when a sprint
|
|
13
|
+
// declares e.g. T1=codex / T2=gemini / T3=grok / T4=claude.
|
|
14
|
+
//
|
|
15
|
+
// Contract:
|
|
16
|
+
// resolveBootPrompt(agentName, vars) -> string
|
|
17
|
+
// agentName ∈ {claude, codex, gemini, grok}
|
|
18
|
+
// vars : { lane: {tag, project, topic, briefing}, sprint: {n, name, docPath} }
|
|
19
|
+
// Throws on unknown agent (with the four valid options listed) or any
|
|
20
|
+
// missing placeholder variable (with the dotted path reported, e.g.
|
|
21
|
+
// "Missing variable: lane.tag"). No template-engine dependency — hand-rolled
|
|
22
|
+
// regex interpolation, ~10 LOC. Project is no-build vanilla JS.
|
|
23
|
+
//
|
|
24
|
+
// Pure read-only: re-reads the template file on each call so authors can edit
|
|
25
|
+
// templates without restarting the server. The Sprint-47 templates are <1KB
|
|
26
|
+
// each so the disk hit is negligible.
|
|
27
|
+
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
|
|
31
|
+
const VALID_AGENTS = ['claude', 'codex', 'gemini', 'grok'];
|
|
32
|
+
|
|
33
|
+
// Resolve template directory relative to this file. __dirname is
|
|
34
|
+
// packages/server/src; the templates live at <repo-root>/docs/multi-agent-substrate/boot-prompts.
|
|
35
|
+
const DEFAULT_TEMPLATE_DIR = path.join(
|
|
36
|
+
__dirname, '..', '..', '..',
|
|
37
|
+
'docs', 'multi-agent-substrate', 'boot-prompts'
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
function _resolveDotted(vars, dotPath) {
|
|
41
|
+
const parts = dotPath.split('.');
|
|
42
|
+
let cur = vars;
|
|
43
|
+
for (const p of parts) {
|
|
44
|
+
if (cur == null || typeof cur !== 'object' || !Object.prototype.hasOwnProperty.call(cur, p)) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
cur = cur[p];
|
|
48
|
+
}
|
|
49
|
+
return cur;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _interpolate(template, vars) {
|
|
53
|
+
return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_match, dotPath) => {
|
|
54
|
+
const value = _resolveDotted(vars, dotPath);
|
|
55
|
+
if (value === undefined || value === null) {
|
|
56
|
+
throw new Error(`Missing variable: ${dotPath}`);
|
|
57
|
+
}
|
|
58
|
+
return String(value);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveBootPrompt(agentName, vars, options) {
|
|
63
|
+
if (!VALID_AGENTS.includes(agentName)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Unknown agent: ${agentName}. Valid agents are: ${VALID_AGENTS.join(', ')}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const dir = (options && options.templateDir) || DEFAULT_TEMPLATE_DIR;
|
|
69
|
+
const filePath = path.join(dir, `boot-prompt-${agentName}.md`);
|
|
70
|
+
const template = fs.readFileSync(filePath, 'utf8');
|
|
71
|
+
return _interpolate(template, vars || {});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
resolveBootPrompt,
|
|
76
|
+
VALID_AGENTS,
|
|
77
|
+
DEFAULT_TEMPLATE_DIR,
|
|
78
|
+
// Exported for unit tests; not part of the public contract.
|
|
79
|
+
_interpolate,
|
|
80
|
+
_resolveDotted,
|
|
81
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Sprint 47 T1 — YAML-subset frontmatter parser for sprint PLANNING.md and
|
|
2
|
+
// per-lane briefs, plus a lane→adapter resolver.
|
|
3
|
+
//
|
|
4
|
+
// Convention this lane establishes (Sprint 48+ uses it):
|
|
5
|
+
// ---
|
|
6
|
+
// sprint: 48
|
|
7
|
+
// lanes:
|
|
8
|
+
// - tag: T1
|
|
9
|
+
// agent: codex
|
|
10
|
+
// - tag: T2
|
|
11
|
+
// agent: gemini
|
|
12
|
+
// ---
|
|
13
|
+
//
|
|
14
|
+
// Sprint 45/46/47 docs have no frontmatter — `parseFrontmatter` returns `{}`
|
|
15
|
+
// and `getLaneAgent` falls back to the Claude adapter. Forward-only, no
|
|
16
|
+
// rewriting of historical PLANNING.md files. No third-party YAML dependency:
|
|
17
|
+
// hand-rolled subset (top-level scalars + a sequence of string-scalar
|
|
18
|
+
// mappings) keeps the no-build vanilla-JS architecture intact.
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const { AGENT_ADAPTERS } = require('./agent-adapters');
|
|
22
|
+
|
|
23
|
+
const VALID_AGENTS = Object.keys(AGENT_ADAPTERS);
|
|
24
|
+
|
|
25
|
+
function parseFrontmatter(filePath) {
|
|
26
|
+
const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
|
|
27
|
+
if (!lines.length || lines[0].trim() !== '---') return {};
|
|
28
|
+
let end = -1;
|
|
29
|
+
for (let i = 1; i < lines.length; i++) {
|
|
30
|
+
if (lines[i].trim() === '---') { end = i; break; }
|
|
31
|
+
}
|
|
32
|
+
if (end === -1) {
|
|
33
|
+
throw new Error(`${filePath}: unclosed frontmatter block (no trailing '---')`);
|
|
34
|
+
}
|
|
35
|
+
const result = {};
|
|
36
|
+
let i = 1;
|
|
37
|
+
while (i < end) {
|
|
38
|
+
const line = lines[i];
|
|
39
|
+
if (line.trim() === '' || line.trim().startsWith('#')) { i++; continue; }
|
|
40
|
+
if (_indent(line, filePath, i) !== 0) {
|
|
41
|
+
throw new Error(`${filePath}:${i + 1}: top-level key must start at column 0`);
|
|
42
|
+
}
|
|
43
|
+
const colon = line.indexOf(':');
|
|
44
|
+
if (colon === -1) {
|
|
45
|
+
throw new Error(`${filePath}:${i + 1}: expected 'key: value' or 'key:'`);
|
|
46
|
+
}
|
|
47
|
+
const key = line.slice(0, colon).trim();
|
|
48
|
+
const rest = line.slice(colon + 1).trim();
|
|
49
|
+
if (rest !== '') {
|
|
50
|
+
result[key] = _scalar(rest, filePath, i);
|
|
51
|
+
i++;
|
|
52
|
+
} else {
|
|
53
|
+
const [items, consumed] = _sequence(lines, i + 1, end, filePath);
|
|
54
|
+
result[key] = items;
|
|
55
|
+
i = i + 1 + consumed;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(result.lanes)) {
|
|
59
|
+
for (const lane of result.lanes) {
|
|
60
|
+
if (lane.agent !== undefined && !VALID_AGENTS.includes(lane.agent)) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`${filePath}: invalid agent '${lane.agent}' on lane ${lane.tag || '(unknown)'}. ` +
|
|
63
|
+
`Valid: ${VALID_AGENTS.join(', ')}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function _sequence(lines, start, end, filePath) {
|
|
72
|
+
const items = [];
|
|
73
|
+
let baseIndent = null;
|
|
74
|
+
let mapIndent = null;
|
|
75
|
+
let current = null;
|
|
76
|
+
let i = start;
|
|
77
|
+
while (i < end) {
|
|
78
|
+
const line = lines[i];
|
|
79
|
+
if (line.trim() === '' || line.trim().startsWith('#')) { i++; continue; }
|
|
80
|
+
const ind = _indent(line, filePath, i);
|
|
81
|
+
if (baseIndent === null) baseIndent = ind;
|
|
82
|
+
if (ind < baseIndent) break;
|
|
83
|
+
const trimmed = line.slice(ind);
|
|
84
|
+
if (ind === baseIndent && trimmed.startsWith('- ')) {
|
|
85
|
+
if (current) items.push(current);
|
|
86
|
+
current = {};
|
|
87
|
+
mapIndent = ind + 2;
|
|
88
|
+
const after = trimmed.slice(2);
|
|
89
|
+
const c = after.indexOf(':');
|
|
90
|
+
if (c === -1) {
|
|
91
|
+
throw new Error(`${filePath}:${i + 1}: sequence item must start with 'key: value'`);
|
|
92
|
+
}
|
|
93
|
+
current[after.slice(0, c).trim()] = _scalar(after.slice(c + 1).trim(), filePath, i);
|
|
94
|
+
} else if (ind === mapIndent && current) {
|
|
95
|
+
const c = line.indexOf(':');
|
|
96
|
+
if (c === -1) {
|
|
97
|
+
throw new Error(`${filePath}:${i + 1}: mapping continuation must be 'key: value'`);
|
|
98
|
+
}
|
|
99
|
+
current[line.slice(0, c).trim()] = _scalar(line.slice(c + 1).trim(), filePath, i);
|
|
100
|
+
} else {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`${filePath}:${i + 1}: unexpected indentation (got ${ind} spaces; ` +
|
|
103
|
+
`expected ${baseIndent} for new item or ${mapIndent} for continuation)`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
i++;
|
|
107
|
+
}
|
|
108
|
+
if (current) items.push(current);
|
|
109
|
+
return [items, i - start];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function _scalar(s, filePath, idx) {
|
|
113
|
+
if (s === '') return null;
|
|
114
|
+
const q = s[0];
|
|
115
|
+
if (q === '"' || q === "'") {
|
|
116
|
+
if (s.length < 2 || s[s.length - 1] !== q) {
|
|
117
|
+
throw new Error(`${filePath}:${idx + 1}: unclosed quote`);
|
|
118
|
+
}
|
|
119
|
+
return s.slice(1, -1);
|
|
120
|
+
}
|
|
121
|
+
if (/^-?\d+$/.test(s)) return parseInt(s, 10);
|
|
122
|
+
if (s === 'true') return true;
|
|
123
|
+
if (s === 'false') return false;
|
|
124
|
+
return s;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function _indent(line, filePath, idx) {
|
|
128
|
+
let n = 0;
|
|
129
|
+
while (n < line.length && line[n] === ' ') n++;
|
|
130
|
+
if (line[n] === '\t') {
|
|
131
|
+
throw new Error(`${filePath}:${idx + 1}: tab indentation not supported (use spaces)`);
|
|
132
|
+
}
|
|
133
|
+
return n;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getLaneAgent(briefPath, laneTag) {
|
|
137
|
+
const fm = parseFrontmatter(briefPath);
|
|
138
|
+
if (!Array.isArray(fm.lanes)) return AGENT_ADAPTERS.claude;
|
|
139
|
+
const lane = fm.lanes.find((l) => l.tag === laneTag);
|
|
140
|
+
if (!lane || !lane.agent) return AGENT_ADAPTERS.claude;
|
|
141
|
+
const adapter = AGENT_ADAPTERS[lane.agent];
|
|
142
|
+
if (!adapter) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`sprint-frontmatter: unknown agent '${lane.agent}' for lane ${laneTag} ` +
|
|
145
|
+
`(valid: ${VALID_AGENTS.join(', ')})`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return adapter;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { parseFrontmatter, getLaneAgent };
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Two-stage submit pattern for the in-dashboard 4+1 sprint runner
|
|
3
|
+
// Two-stage submit pattern for the in-dashboard 4+1 sprint runner.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
5
|
+
// Sprint 37 T4 baseline: Claude-only, bracketed-paste + lone-CR.
|
|
6
|
+
// Sprint 47 T3 extension: per-lane agent dispatch via the adapter registry.
|
|
7
|
+
// Each lane may declare an `agent` name (claude/codex/gemini/grok). The
|
|
8
|
+
// helper looks up the adapter and selects the inject shape:
|
|
9
|
+
// • acceptsPaste: true → bracketed-paste payload (`\x1b[200~…\x1b[201~`)
|
|
10
|
+
// followed by a lone `\r` after the settle window.
|
|
11
|
+
// • acceptsPaste: false → chunked stdin fallback (line + `\r` per chunk
|
|
12
|
+
// with `chunkedDelayMs` between). Chunked lanes
|
|
13
|
+
// self-submit on their last line; the stage-2
|
|
14
|
+
// `\r` is skipped for them so we don't fire a
|
|
15
|
+
// duplicate empty submit.
|
|
16
|
+
//
|
|
17
|
+
// The cardinal rule from the global 4+1 inject mandate (paste path):
|
|
6
18
|
//
|
|
7
19
|
// Stage 1: write `\x1b[200~<prompt>\x1b[201~` to each session in turn,
|
|
8
20
|
// with a small inter-session gap. NO trailing CR.
|
|
@@ -23,24 +35,65 @@
|
|
|
23
35
|
// Pure logic — caller injects writeBytes/getStatus/sleep so tests don't need
|
|
24
36
|
// a live PTY. Wired in by sprint-routes.js.
|
|
25
37
|
|
|
38
|
+
const { AGENT_ADAPTERS } = require('./agent-adapters');
|
|
39
|
+
|
|
26
40
|
const DEFAULTS = {
|
|
27
41
|
gapMs: 250,
|
|
28
42
|
settleMs: 400,
|
|
29
43
|
verifyTimeoutMs: 8000,
|
|
30
44
|
verifyPollMs: 500,
|
|
31
45
|
postPokeWaitMs: 500,
|
|
46
|
+
chunkedDelayMs: 20,
|
|
32
47
|
};
|
|
33
48
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
// Resolve the per-lane inject shape from the adapter registry.
|
|
50
|
+
// Returns a discriminated payload:
|
|
51
|
+
// { kind: 'paste', bytes: string }
|
|
52
|
+
// { kind: 'chunked', lines: string[] }
|
|
53
|
+
// When `agent` is null/undefined or the adapter is absent from the registry,
|
|
54
|
+
// defaults to bracketed-paste (the Sprint 37 baseline). Unknown agent names
|
|
55
|
+
// are not an error here — they just fall through to the bracketed-paste path
|
|
56
|
+
// so a typo in a lane brief degrades to "works for Claude-shaped TUIs"
|
|
57
|
+
// rather than throwing mid-inject. Validation belongs at lane-frontmatter
|
|
58
|
+
// parse time (Sprint 47 T1).
|
|
59
|
+
function buildPayload(prompt, agent, adapters) {
|
|
60
|
+
const registry = adapters || AGENT_ADAPTERS;
|
|
61
|
+
const adapter = agent ? registry[agent] : null;
|
|
62
|
+
const acceptsPaste = adapter ? adapter.acceptsPaste !== false : true;
|
|
63
|
+
if (acceptsPaste) {
|
|
64
|
+
return { kind: 'paste', bytes: `\x1b[200~${prompt}\x1b[201~` };
|
|
65
|
+
}
|
|
66
|
+
return { kind: 'chunked', lines: prompt.split('\n') };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Normalize the three accepted input shapes into a single internal lane list.
|
|
70
|
+
// 1. { sessionIds, prompts } — Sprint 37 baseline (claude only)
|
|
71
|
+
// 2. { sessionIds, prompts, agents } — parallel agents array
|
|
72
|
+
// 3. { lanes: [{ sessionId, prompt, agent }] } — Sprint 47 lanes shape
|
|
73
|
+
function normalizeLanes({ sessionIds, prompts, agents, lanes }) {
|
|
74
|
+
if (Array.isArray(lanes)) {
|
|
75
|
+
if (lanes.length === 0) {
|
|
76
|
+
throw new Error('at least one lane required');
|
|
77
|
+
}
|
|
78
|
+
return lanes.map((l, i) => {
|
|
79
|
+
if (!l || typeof l !== 'object') {
|
|
80
|
+
throw new Error(`lanes[${i}] must be an object`);
|
|
81
|
+
}
|
|
82
|
+
if (typeof l.sessionId !== 'string' || !l.sessionId) {
|
|
83
|
+
throw new Error(`lanes[${i}].sessionId must be a non-empty string`);
|
|
84
|
+
}
|
|
85
|
+
if (typeof l.prompt !== 'string') {
|
|
86
|
+
throw new Error(`lanes[${i}].prompt must be a string`);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
sessionId: l.sessionId,
|
|
90
|
+
prompt: l.prompt,
|
|
91
|
+
agent: l.agent || null,
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
}
|
|
42
95
|
if (!Array.isArray(sessionIds) || !Array.isArray(prompts)) {
|
|
43
|
-
throw new Error('sessionIds and prompts must be arrays');
|
|
96
|
+
throw new Error('sessionIds and prompts must be arrays (or pass lanes[])');
|
|
44
97
|
}
|
|
45
98
|
if (sessionIds.length !== prompts.length) {
|
|
46
99
|
throw new Error('sessionIds and prompts must be the same length');
|
|
@@ -48,6 +101,28 @@ async function injectSprintPrompts({
|
|
|
48
101
|
if (sessionIds.length === 0) {
|
|
49
102
|
throw new Error('at least one session required');
|
|
50
103
|
}
|
|
104
|
+
if (agents !== undefined && agents !== null) {
|
|
105
|
+
if (!Array.isArray(agents) || agents.length !== sessionIds.length) {
|
|
106
|
+
throw new Error('agents must be an array of the same length as sessionIds');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return sessionIds.map((sessionId, i) => ({
|
|
110
|
+
sessionId,
|
|
111
|
+
prompt: prompts[i],
|
|
112
|
+
agent: agents ? agents[i] || null : null,
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function injectSprintPrompts({
|
|
117
|
+
sessionIds,
|
|
118
|
+
prompts,
|
|
119
|
+
agents,
|
|
120
|
+
lanes,
|
|
121
|
+
writeBytes,
|
|
122
|
+
getStatus,
|
|
123
|
+
sleep,
|
|
124
|
+
options,
|
|
125
|
+
}) {
|
|
51
126
|
if (typeof writeBytes !== 'function') {
|
|
52
127
|
throw new Error('writeBytes(sessionId, bytes) callback required');
|
|
53
128
|
}
|
|
@@ -56,10 +131,15 @@ async function injectSprintPrompts({
|
|
|
56
131
|
}
|
|
57
132
|
|
|
58
133
|
const opts = { ...DEFAULTS, ...(options || {}) };
|
|
134
|
+
const registry = (options && options.adapters) || AGENT_ADAPTERS;
|
|
59
135
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
136
|
+
const internal = normalizeLanes({ sessionIds, prompts, agents, lanes });
|
|
137
|
+
|
|
138
|
+
const enriched = internal.map((l) => ({
|
|
139
|
+
sessionId: l.sessionId,
|
|
140
|
+
prompt: l.prompt,
|
|
141
|
+
agent: l.agent,
|
|
142
|
+
dispatch: buildPayload(l.prompt, l.agent, registry),
|
|
63
143
|
paste: null,
|
|
64
144
|
submit: null,
|
|
65
145
|
verified: false,
|
|
@@ -67,17 +147,48 @@ async function injectSprintPrompts({
|
|
|
67
147
|
finalStatus: null,
|
|
68
148
|
}));
|
|
69
149
|
|
|
70
|
-
// Stage 1:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
150
|
+
// Stage 1: per-lane payload. Paste lanes get one PTY write; chunked lanes
|
|
151
|
+
// get N writes (one per line) with `chunkedDelayMs` between. `gapMs`
|
|
152
|
+
// separates lanes from each other so stages stay deterministically ordered.
|
|
153
|
+
for (let i = 0; i < enriched.length; i++) {
|
|
154
|
+
const lane = enriched[i];
|
|
155
|
+
if (lane.dispatch.kind === 'paste') {
|
|
156
|
+
try {
|
|
157
|
+
const r = await writeBytes(lane.sessionId, lane.dispatch.bytes);
|
|
158
|
+
lane.paste = {
|
|
159
|
+
ok: true,
|
|
160
|
+
bytes: (r && r.bytes) || lane.dispatch.bytes.length,
|
|
161
|
+
mode: 'paste',
|
|
162
|
+
};
|
|
163
|
+
} catch (err) {
|
|
164
|
+
lane.paste = {
|
|
165
|
+
ok: false,
|
|
166
|
+
error: err && err.message ? err.message : String(err),
|
|
167
|
+
mode: 'paste',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
// chunked
|
|
172
|
+
let totalBytes = 0;
|
|
173
|
+
let firstError = null;
|
|
174
|
+
for (let j = 0; j < lane.dispatch.lines.length; j++) {
|
|
175
|
+
const chunk = lane.dispatch.lines[j] + '\r';
|
|
176
|
+
try {
|
|
177
|
+
const r = await writeBytes(lane.sessionId, chunk);
|
|
178
|
+
totalBytes += (r && r.bytes) || chunk.length;
|
|
179
|
+
} catch (err) {
|
|
180
|
+
firstError = err && err.message ? err.message : String(err);
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
if (j < lane.dispatch.lines.length - 1) {
|
|
184
|
+
await sleep(opts.chunkedDelayMs);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
lane.paste = firstError
|
|
188
|
+
? { ok: false, error: firstError, mode: 'chunked' }
|
|
189
|
+
: { ok: true, bytes: totalBytes, mode: 'chunked' };
|
|
79
190
|
}
|
|
80
|
-
if (i <
|
|
191
|
+
if (i < enriched.length - 1) await sleep(opts.gapMs);
|
|
81
192
|
}
|
|
82
193
|
|
|
83
194
|
// Settle window — long enough for the PTY to flush each paste to the TUI's
|
|
@@ -85,19 +196,25 @@ async function injectSprintPrompts({
|
|
|
85
196
|
await sleep(opts.settleMs);
|
|
86
197
|
|
|
87
198
|
// Stage 2: submit-only (\r alone, guaranteed its own PTY write).
|
|
88
|
-
|
|
89
|
-
|
|
199
|
+
// Chunked-mode lanes already self-submitted on their last line — skip.
|
|
200
|
+
for (let i = 0; i < enriched.length; i++) {
|
|
201
|
+
const lane = enriched[i];
|
|
90
202
|
if (!lane.paste || !lane.paste.ok) {
|
|
91
203
|
lane.submit = { ok: false, skipped: 'paste-failed' };
|
|
92
204
|
continue;
|
|
93
205
|
}
|
|
206
|
+
if (lane.paste.mode === 'chunked') {
|
|
207
|
+
lane.submit = { ok: true, bytes: 0, skipped: 'chunked-already-submitted' };
|
|
208
|
+
if (i < enriched.length - 1) await sleep(opts.gapMs);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
94
211
|
try {
|
|
95
212
|
const r = await writeBytes(lane.sessionId, '\r');
|
|
96
213
|
lane.submit = { ok: true, bytes: (r && r.bytes) || 1 };
|
|
97
214
|
} catch (err) {
|
|
98
215
|
lane.submit = { ok: false, error: err && err.message ? err.message : String(err) };
|
|
99
216
|
}
|
|
100
|
-
if (i <
|
|
217
|
+
if (i < enriched.length - 1) await sleep(opts.gapMs);
|
|
101
218
|
}
|
|
102
219
|
|
|
103
220
|
// Verify: poll each lane's status until it reads `thinking` or we hit the
|
|
@@ -106,7 +223,7 @@ async function injectSprintPrompts({
|
|
|
106
223
|
const deadline = Date.now() + opts.verifyTimeoutMs;
|
|
107
224
|
while (Date.now() < deadline) {
|
|
108
225
|
let anyPending = false;
|
|
109
|
-
for (const lane of
|
|
226
|
+
for (const lane of enriched) {
|
|
110
227
|
if (lane.verified) continue;
|
|
111
228
|
try {
|
|
112
229
|
const s = await getStatus(lane.sessionId);
|
|
@@ -126,7 +243,7 @@ async function injectSprintPrompts({
|
|
|
126
243
|
|
|
127
244
|
// Auto-poke (cr-flood) any lane that didn't reach `thinking`. Best-effort —
|
|
128
245
|
// never page the user; the orchestrator dashboard surfaces the result.
|
|
129
|
-
for (const lane of
|
|
246
|
+
for (const lane of enriched) {
|
|
130
247
|
if (lane.verified) continue;
|
|
131
248
|
try {
|
|
132
249
|
await writeBytes(lane.sessionId, '\r\r\r');
|
|
@@ -146,11 +263,13 @@ async function injectSprintPrompts({
|
|
|
146
263
|
}
|
|
147
264
|
}
|
|
148
265
|
|
|
149
|
-
const ok =
|
|
150
|
-
return { ok, lanes };
|
|
266
|
+
const ok = enriched.every((l) => l.paste && l.paste.ok && l.submit && l.submit.ok);
|
|
267
|
+
return { ok, lanes: enriched };
|
|
151
268
|
}
|
|
152
269
|
|
|
153
270
|
module.exports = {
|
|
154
271
|
injectSprintPrompts,
|
|
272
|
+
buildPayload,
|
|
273
|
+
normalizeLanes,
|
|
155
274
|
DEFAULTS,
|
|
156
275
|
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Sprint 47 T4 — Cross-agent STATUS.md merger.
|
|
4
|
+
//
|
|
5
|
+
// Each lane agent (Claude / Codex / Gemini / Grok) posts FINDING /
|
|
6
|
+
// FIX-PROPOSED / DONE differently. Claude has the canonical shape nailed:
|
|
7
|
+
// `- Tn: STAGE — one-line summary — YYYY-MM-DD HH:MM ET`
|
|
8
|
+
// The others may emit emoji prefixes, generic bullet lists, or free-form
|
|
9
|
+
// prose. mergeStatusLine() takes a raw line in any of those shapes and
|
|
10
|
+
// returns the canonical form, so the dashboard's STATUS.md regex parser and
|
|
11
|
+
// human readers see one consistent shape regardless of CLI.
|
|
12
|
+
//
|
|
13
|
+
// Sprint 47 ships this as infrastructure. Sprint 48 (or whenever the mixed
|
|
14
|
+
// 4+1 dogfood lands) is when it actually runs over real cross-agent posts.
|
|
15
|
+
|
|
16
|
+
const STAGES = new Set(['FINDING', 'FIX-PROPOSED', 'DONE']);
|
|
17
|
+
|
|
18
|
+
// Codex idiom: emoji prefix, optionally followed by `Found:`/`Fixed:`/etc.
|
|
19
|
+
// 🛠️ (with VS-16) and 🛠 (without) both round to FIX-PROPOSED — terminals
|
|
20
|
+
// disagree about the variation selector and we don't care which one came in.
|
|
21
|
+
const EMOJI_STAGES = [
|
|
22
|
+
['🛠️', 'FIX-PROPOSED'],
|
|
23
|
+
['🛠', 'FIX-PROPOSED'],
|
|
24
|
+
['🔍', 'FINDING'],
|
|
25
|
+
['✅', 'DONE'],
|
|
26
|
+
['🔧', 'FIX-PROPOSED'],
|
|
27
|
+
];
|
|
28
|
+
const EMOJI_LEADIN_RE = /^(?:Found|Fixed|Proposed|Proposing|Note|Status)\s*[:—\-]?\s*/i;
|
|
29
|
+
|
|
30
|
+
// Gemini idiom: bullet-pointed list with stage keyword.
|
|
31
|
+
const BULLET_FINDING_RE = /^[-*]\s+(?:found(?:\s+that)?|finding|noticed|observation)\s*[:—\-]?\s*(.+)$/i;
|
|
32
|
+
const BULLET_FIX_RE = /^[-*]\s+(?:propos(?:ing|ed)\s+fix(?:ing)?|fix[\s-]proposed|proposed\s+fix)\s*[:—\-]?\s*(.+)$/i;
|
|
33
|
+
const BULLET_DONE_RE = /^[-*]\s+(?:done|completed|finished|shipped)\s*[:—\-]?\s*(.+)$/i;
|
|
34
|
+
|
|
35
|
+
// Grok idiom: free-form first-person prose.
|
|
36
|
+
const PROSE_DONE_RE = /^(?:Done|Completed|Finished|Shipped)\s*[:—\-]?\s*(.+)$/i;
|
|
37
|
+
const PROSE_FIX_RE = /^(?:I'?ll\s+fix|I\s+will\s+fix|I'?m\s+fixing|Fixing|Proposing\s+(?:a\s+)?fix|Proposed\s+fix)\b\s*(?:this\s+by\s+|by\s+|[:—\-]\s*)?(.+)$/i;
|
|
38
|
+
const PROSE_FINDING_RE = /^(?:I\s+noticed|I\s+observed|I\s+found|I\s+saw|Noticed|Observed)\b\s*(?:that\s+)?(.+)$/i;
|
|
39
|
+
|
|
40
|
+
// Canonical Claude — timestamped. Greedy backtracking: we anchor the
|
|
41
|
+
// timestamp to a YYYY-MM-DD prefix so bodies that contain stray ` — ` (very
|
|
42
|
+
// common in real Sprint 46 lines) split at the right boundary.
|
|
43
|
+
const CANONICAL_TS_RE =
|
|
44
|
+
/^[-*]?\s*(T\d+):\s*(FINDING|FIX-PROPOSED|DONE)\s+[—\-]\s+(.+?)\s+[—\-]\s+(\d{4}-\d{2}-\d{2}\b.*)$/;
|
|
45
|
+
// Canonical Claude without a trailing timestamp — we'll add one.
|
|
46
|
+
const CANONICAL_NO_TS_RE =
|
|
47
|
+
/^[-*]?\s*(T\d+):\s*(FINDING|FIX-PROPOSED|DONE)\s+[—\-]\s+(.+)$/;
|
|
48
|
+
|
|
49
|
+
// Markdown section header — never a status line.
|
|
50
|
+
const HEADER_RE = /^#{1,6}\s/;
|
|
51
|
+
// Bare bracket-like meta lines: `_(no entries yet)_`, `> note`, etc.
|
|
52
|
+
const META_RE = /^[_>(]/;
|
|
53
|
+
|
|
54
|
+
const SUMMARY_MAX = 120;
|
|
55
|
+
|
|
56
|
+
function trimSummary(s) {
|
|
57
|
+
s = String(s).trim().replace(/\s+/g, ' ');
|
|
58
|
+
if (s.length > SUMMARY_MAX) {
|
|
59
|
+
s = s.slice(0, SUMMARY_MAX - 1).replace(/\s+\S*$/, '') + '…';
|
|
60
|
+
}
|
|
61
|
+
return s;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function pad2(n) {
|
|
65
|
+
return String(n).padStart(2, '0');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Matches the real STATUS.md convention `YYYY-MM-DD HH:MM ET`. The ET tag is
|
|
69
|
+
// decorative (Joshua's tz); we don't try to convert from UTC because the
|
|
70
|
+
// surrounding harness already runs in his local clock.
|
|
71
|
+
function formatTimestamp(date) {
|
|
72
|
+
return (
|
|
73
|
+
`${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}` +
|
|
74
|
+
` ${pad2(date.getHours())}:${pad2(date.getMinutes())} ET`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function detectEmojiStage(text) {
|
|
79
|
+
for (const [emoji, stage] of EMOJI_STAGES) {
|
|
80
|
+
if (text.startsWith(emoji)) {
|
|
81
|
+
const after = text.slice(emoji.length).replace(/^[\s️]+/, '');
|
|
82
|
+
return { stage, summary: after.replace(EMOJI_LEADIN_RE, '') };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function detectBulletStage(text) {
|
|
89
|
+
let m;
|
|
90
|
+
if ((m = BULLET_FINDING_RE.exec(text))) return { stage: 'FINDING', summary: m[1] };
|
|
91
|
+
if ((m = BULLET_FIX_RE.exec(text))) return { stage: 'FIX-PROPOSED', summary: m[1] };
|
|
92
|
+
if ((m = BULLET_DONE_RE.exec(text))) return { stage: 'DONE', summary: m[1] };
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function detectProseStage(text) {
|
|
97
|
+
let m;
|
|
98
|
+
// Order matters: PROSE_DONE before PROSE_FIX before PROSE_FINDING because
|
|
99
|
+
// "Done: foo" would otherwise also match nothing later, and "I'll fix" is
|
|
100
|
+
// distinct from "I noticed" so order between them is safe.
|
|
101
|
+
if ((m = PROSE_DONE_RE.exec(text))) return { stage: 'DONE', summary: m[1] };
|
|
102
|
+
if ((m = PROSE_FIX_RE.exec(text))) return { stage: 'FIX-PROPOSED', summary: m[1] };
|
|
103
|
+
if ((m = PROSE_FINDING_RE.exec(text))) return { stage: 'FINDING', summary: m[1] };
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function mergeStatusLine(rawLine, opts = {}) {
|
|
108
|
+
if (typeof rawLine !== 'string') return null;
|
|
109
|
+
const line = rawLine.replace(/\r?\n$/, '').trim();
|
|
110
|
+
if (!line) return null;
|
|
111
|
+
if (HEADER_RE.test(line)) return null;
|
|
112
|
+
if (META_RE.test(line)) return null;
|
|
113
|
+
|
|
114
|
+
// 1. Canonical with timestamp — pass through unchanged, normalize only the
|
|
115
|
+
// leading bullet. The body is never trimmed here: real Sprint 46 lines are
|
|
116
|
+
// routinely well over 120 chars and the brief mandates "same line out".
|
|
117
|
+
let m = CANONICAL_TS_RE.exec(line);
|
|
118
|
+
if (m) {
|
|
119
|
+
const [, tag, stage, summary, ts] = m;
|
|
120
|
+
return `- ${tag}: ${stage} — ${summary.trim()} — ${ts.trim()}`;
|
|
121
|
+
}
|
|
122
|
+
// 2. Canonical without timestamp — synthesize one. Body still untrimmed:
|
|
123
|
+
// the author wrote a canonical-shape line, we trust its length.
|
|
124
|
+
m = CANONICAL_NO_TS_RE.exec(line);
|
|
125
|
+
if (m) {
|
|
126
|
+
const [, tag, stage, summary] = m;
|
|
127
|
+
const ts = formatTimestamp(opts.now || new Date());
|
|
128
|
+
return `- ${tag}: ${stage} — ${summary.trim()} — ${ts}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 3. Variant idioms — emoji, bullet, prose.
|
|
132
|
+
const detected =
|
|
133
|
+
detectEmojiStage(line) || detectBulletStage(line) || detectProseStage(line);
|
|
134
|
+
if (!detected || !STAGES.has(detected.stage)) return null;
|
|
135
|
+
|
|
136
|
+
const tag = opts.laneTag || 'T?';
|
|
137
|
+
const ts = formatTimestamp(opts.now || new Date());
|
|
138
|
+
return `- ${tag}: ${detected.stage} — ${trimSummary(detected.summary)} — ${ts}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { mergeStatusLine };
|