spectoflow 0.20.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/lib/adapters.js +29 -6
- package/package.json +1 -1
- package/templates/dashboard/public/app.js +19 -9
- package/templates/dashboard/public/index.html +12 -6
- package/templates/dashboard/public/styles.css +5 -4
- package/templates/dashboard/runner.js +14 -2
- package/templates/dashboard/server.js +5 -2
- package/templates/dashboard/summarize.js +2 -1
- package/templates/lib/agents-registry.js +12 -7
package/README.md
CHANGED
|
@@ -21,10 +21,12 @@ You speak in plain language; the framework classifies your intent and runs the r
|
|
|
21
21
|
ceremonial command to start.
|
|
22
22
|
|
|
23
23
|
**Works with whichever coding agent you have** — Claude Code, Codex, Cursor, Gemini CLI, OpenCode,
|
|
24
|
-
Kiro CLI, and
|
|
25
|
-
**active agent**, front and center, with a switcher: pick another and it's verified as genuinely
|
|
24
|
+
Kiro CLI, Antigravity, and Kimi CLI are auto-detected at `init`. The dashboard's topbar always shows
|
|
25
|
+
the **active agent**, front and center, with a switcher: pick another and it's verified as genuinely
|
|
26
26
|
installed before activating (a red **"No agent found"** if none is) — it never silently activates
|
|
27
|
-
something that isn't there.
|
|
27
|
+
something that isn't there. (Kimi CLI doesn't yet ship a non-interactive run mode, so spectoflow can
|
|
28
|
+
set it as active but won't try to spawn it — the agent pickers that launch a run grey it out rather
|
|
29
|
+
than hide it.)
|
|
28
30
|
|
|
29
31
|
## Install
|
|
30
32
|
|
package/lib/adapters.js
CHANGED
|
@@ -68,11 +68,20 @@ Argument: \`$ARGUMENTS\`
|
|
|
68
68
|
|
|
69
69
|
// Priority order = which agent becomes the default when several are detected.
|
|
70
70
|
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
71
|
+
// `headless: false` marks an agent spectoflow can DETECT and let you set as active, but never spawn
|
|
72
|
+
// itself — no confirmed non-interactive one-shot mode (prompt as trailing arg → stdout → exit) as of
|
|
73
|
+
// this research pass, so `runner` is null on purpose (a fabricated command would just fail silently
|
|
74
|
+
// for real users). The dashboard still lists it and shows why Run/Orchestrate/Summarize are disabled
|
|
75
|
+
// for it, rather than hiding it — you can drive it yourself in its own terminal meanwhile, and
|
|
76
|
+
// `config.json → runners` is still yours to hand-wire a command into if one ships later.
|
|
77
|
+
//
|
|
78
|
+
// Left out of KNOWN_AGENTS entirely (not even headless:false) — researched, not re-added on a whim:
|
|
79
|
+
// DeepSeek Harness (deepseek-ai/deepseek-harness). It has no single installable binary to detect —
|
|
80
|
+
// it's invoked as `npx @deepseek-ai/dsh <subcommand>`, is explicitly a local WEB-APP framework (not a
|
|
81
|
+
// terminal chat/prompt tool like the others), and ships as an August-2026 "developer preview" whose
|
|
82
|
+
// APIs the project itself says will change. Detecting "is npx present" would not mean deepseek-harness
|
|
83
|
+
// is; it doesn't fit this registry's detection model. Revisit once it (or Kimi) publishes a real
|
|
84
|
+
// one-shot flag, or wire a custom command into config.json → runners by hand in the meantime.
|
|
76
85
|
const REGISTRY = [
|
|
77
86
|
{
|
|
78
87
|
id: 'claude',
|
|
@@ -81,44 +90,58 @@ const REGISTRY = [
|
|
|
81
90
|
{ path: '.claude/commands/spectoflow.md', content: SLASH_CMD },
|
|
82
91
|
],
|
|
83
92
|
runner: 'claude -p --permission-mode acceptEdits',
|
|
93
|
+
headless: true,
|
|
84
94
|
detect: { bin: 'claude', dirs: ['.claude'] },
|
|
85
95
|
},
|
|
86
96
|
{
|
|
87
97
|
id: 'codex',
|
|
88
98
|
entries: [{ path: 'AGENTS.md', content: ROOT_AGENTS_MD }],
|
|
89
99
|
runner: 'codex exec',
|
|
100
|
+
headless: true,
|
|
90
101
|
detect: { bin: 'codex', dirs: ['.codex'] },
|
|
91
102
|
},
|
|
92
103
|
{
|
|
93
104
|
id: 'cursor',
|
|
94
105
|
entries: [{ path: 'AGENTS.md', content: ROOT_AGENTS_MD }],
|
|
95
106
|
runner: 'cursor-agent -p',
|
|
107
|
+
headless: true,
|
|
96
108
|
detect: { bin: 'cursor-agent', dirs: ['.cursor'] },
|
|
97
109
|
},
|
|
98
110
|
{
|
|
99
111
|
id: 'gemini',
|
|
100
112
|
entries: [{ path: 'GEMINI.md', content: GEMINI_MD }],
|
|
101
113
|
runner: 'gemini -p',
|
|
114
|
+
headless: true,
|
|
102
115
|
detect: { bin: 'gemini', dirs: ['.gemini'] },
|
|
103
116
|
},
|
|
104
117
|
{
|
|
105
118
|
id: 'opencode',
|
|
106
119
|
entries: [{ path: 'AGENTS.md', content: ROOT_AGENTS_MD }],
|
|
107
120
|
runner: 'opencode run --quiet',
|
|
121
|
+
headless: true,
|
|
108
122
|
detect: { bin: 'opencode', dirs: ['.opencode'] },
|
|
109
123
|
},
|
|
110
124
|
{
|
|
111
125
|
id: 'kiro',
|
|
112
126
|
entries: [{ path: 'AGENTS.md', content: ROOT_AGENTS_MD }],
|
|
113
127
|
runner: 'kiro-cli chat --no-interactive --trust-all-tools',
|
|
128
|
+
headless: true,
|
|
114
129
|
detect: { bin: 'kiro-cli', dirs: ['.kiro'] },
|
|
115
130
|
},
|
|
116
131
|
{
|
|
117
132
|
id: 'antigravity',
|
|
118
133
|
entries: [{ path: 'AGENTS.md', content: ROOT_AGENTS_MD }],
|
|
119
134
|
runner: 'agy -p',
|
|
135
|
+
headless: true,
|
|
120
136
|
detect: { bin: 'agy' },
|
|
121
137
|
},
|
|
138
|
+
{
|
|
139
|
+
id: 'kimi',
|
|
140
|
+
entries: [{ path: 'AGENTS.md', content: ROOT_AGENTS_MD }],
|
|
141
|
+
runner: null, // no confirmed non-interactive one-shot mode — see the note above REGISTRY
|
|
142
|
+
headless: false,
|
|
143
|
+
detect: { bin: 'kimi', dirs: [] },
|
|
144
|
+
},
|
|
122
145
|
];
|
|
123
146
|
|
|
124
147
|
const byId = (id) => REGISTRY.find((a) => a.id === id);
|
|
@@ -148,7 +171,7 @@ function defaultRunners(agents) {
|
|
|
148
171
|
const out = {};
|
|
149
172
|
for (const id of agents) {
|
|
150
173
|
const a = byId(id);
|
|
151
|
-
if (a) out[id] = a.runner;
|
|
174
|
+
if (a && a.runner) out[id] = a.runner; // headless:false agents (e.g. kimi) have no runner to seed
|
|
152
175
|
}
|
|
153
176
|
return out;
|
|
154
177
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spectoflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"description": "Agent-agnostic spec-driven development framework + real-time local control plane. Markdown artifacts, intent router, workflow-by-scope.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"spec-driven-development",
|
|
@@ -95,7 +95,8 @@ function setChat(open){
|
|
|
95
95
|
$('#chat').setAttribute('aria-hidden', open?'false':'true');
|
|
96
96
|
$('#chatFab').classList.toggle('is-open',open);
|
|
97
97
|
try{ localStorage.setItem('spf-chat', open?'1':'0'); }catch{}
|
|
98
|
-
|
|
98
|
+
// Land where you can actually type, not wherever the log happened to be scrolled last.
|
|
99
|
+
if(open) setTimeout(()=>{ scrollChat($('#chatLog')); $('#runPrompt').focus(); },60);
|
|
99
100
|
}
|
|
100
101
|
// doRun/doOrchestrate default to the floating widget's textarea/select; the Chat tab has its own
|
|
101
102
|
// #tabRunPrompt/#tabRunAgent (an id can't be shared by two elements) and passes them explicitly —
|
|
@@ -139,11 +140,10 @@ function render(){
|
|
|
139
140
|
// in renderSettings() below (called on every render tick) and saved via saveSettings().
|
|
140
141
|
// agent select — widget (#runAgent) and Chat tab (#tabRunAgent) each get their own populated
|
|
141
142
|
// <select>, since an id can't be shared by two elements; same option list, same source of truth.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
});
|
|
143
|
+
// Shows every known agent (not just ones already in config.json → runners — a freshly-installed
|
|
144
|
+
// agent works the first time, runner.js falls back to the registry default), disabling any that
|
|
145
|
+
// isn't genuinely installed or can't run headless (e.g. kimi).
|
|
146
|
+
[$('#runAgent'),$('#tabRunAgent')].forEach(sel=>fillAgentSelect(sel,P.knownAgents||[],runnableAgentIds(),c.agent));
|
|
147
147
|
const s=SpectoStats.stats(P);
|
|
148
148
|
const meterFill=$('#globalMeterFill');
|
|
149
149
|
if(meterFill) meterFill.style.width=(s.pct||0)+'%';
|
|
@@ -717,6 +717,12 @@ function setModeSelects(mode){ [$('#setMode'),$('#topMode')].forEach(s=>{ if(s)
|
|
|
717
717
|
// the user activate an agent that isn't actually installed — options for a not-installed known agent
|
|
718
718
|
// are present but disabled, and P.installedAgents itself comes from the server testing each agent's
|
|
719
719
|
// real CLI (bin on PATH, or the project's own config dir), not a guess.
|
|
720
|
+
// ids that can actually be SPAWNED right now: installed AND headless-capable (a known agent like
|
|
721
|
+
// kimi can be "installed" and even the active agent, but nothing can run it non-interactively).
|
|
722
|
+
function runnableAgentIds(){
|
|
723
|
+
const installed=P.installedAgents||[];
|
|
724
|
+
return (P.knownAgents||[]).filter(a=>a.headless&&installed.includes(a.id)).map(a=>a.id);
|
|
725
|
+
}
|
|
720
726
|
function fillAgentSelect(sel,known,installed,active){
|
|
721
727
|
if(!sel) return;
|
|
722
728
|
const empty=!installed.length;
|
|
@@ -936,9 +942,7 @@ function renderCustomize(){
|
|
|
936
942
|
const form=el('div','cz-form');
|
|
937
943
|
const ta=el('textarea','chat-ta'); ta.placeholder=t('customize.describePh');
|
|
938
944
|
const sel=el('select','chat-agent');
|
|
939
|
-
|
|
940
|
-
runners.forEach((k)=>{ const o=document.createElement('option'); o.value=k; o.textContent=k; sel.append(o); });
|
|
941
|
-
if(P.config&&P.config.agent) sel.value=P.config.agent;
|
|
945
|
+
fillAgentSelect(sel,P.knownAgents||[],runnableAgentIds(),P.config&&P.config.agent);
|
|
942
946
|
const actions=el('div','cz-form-actions');
|
|
943
947
|
const autoBtn=el('button','btn',t('customize.auto'));
|
|
944
948
|
const goBtn=el('button','btn primary',t('customize.generate'));
|
|
@@ -978,6 +982,10 @@ function navigateTab(tabId,push){
|
|
|
978
982
|
}
|
|
979
983
|
applyActiveTab();
|
|
980
984
|
closeNav(); // a tab pick closes the mobile menu
|
|
985
|
+
// Landing on Chat: go straight to where you can read the latest and type, not wherever the log
|
|
986
|
+
// was scrolled to last (only on an actual switch INTO the tab — applyActiveTab() alone runs on
|
|
987
|
+
// every SSE render tick too, and re-scrolling/re-focusing there would fight the user's typing).
|
|
988
|
+
if(tabId==='chat') setTimeout(()=>{ scrollChat($('#chatTabLog')); $('#tabRunPrompt').focus(); },60);
|
|
981
989
|
}
|
|
982
990
|
function closeNav(){ document.body.classList.remove('nav-open'); const nt=$('#navToggle'); if(nt) nt.setAttribute('aria-expanded','false'); }
|
|
983
991
|
|
|
@@ -1297,6 +1305,8 @@ $('#chatClose').addEventListener('click',()=> setChat(false));
|
|
|
1297
1305
|
try{ if(localStorage.getItem('spf-chat')==='1') setChat(true); }catch{}
|
|
1298
1306
|
$('#runBtn').addEventListener('click',()=>doRun());
|
|
1299
1307
|
$('#orchBtn').addEventListener('click',()=>doOrchestrate());
|
|
1308
|
+
$('#widgetSummarizeBtn').addEventListener('click',()=>summarizeChat($('#runAgent')));
|
|
1309
|
+
$('#widgetClearBtn').addEventListener('click',clearChat);
|
|
1300
1310
|
$('#runPrompt').addEventListener('keydown',e=>{ if((e.metaKey||e.ctrlKey)&&e.key==='Enter')doRun(); });
|
|
1301
1311
|
// Chat tab — same doRun/doOrchestrate/approve, its own textarea/select (#tabRunPrompt/#tabRunAgent)
|
|
1302
1312
|
$('#tabRunBtn').addEventListener('click',()=>doRun($('#tabRunPrompt'),$('#tabRunAgent')));
|
|
@@ -201,16 +201,18 @@
|
|
|
201
201
|
<h2 class="panel-title" data-i18n="nav.chat">Chat</h2>
|
|
202
202
|
<p class="panel-sub" data-i18n="chat.tabSub">Full conversation with the runner — the same run as the widget, more room to read it.</p>
|
|
203
203
|
</div>
|
|
204
|
-
<div class="chat-tab-head-actions">
|
|
205
|
-
<button id="tabSummarizeBtn" class="mini-btn" data-i18n-title="chat.summarizeTitle" data-i18n="action.summarize" title="Condense the recent activity into a summary">Summarize</button>
|
|
206
|
-
<button id="tabClearBtn" class="mini-btn" data-i18n-title="chat.clearTitle" data-i18n="action.clear" title="Clear the chat log">Clear</button>
|
|
207
|
-
<select id="tabRunAgent" class="chat-agent" title="Agent"></select>
|
|
208
|
-
</div>
|
|
209
204
|
</div>
|
|
210
205
|
<div class="chat-tab-log" id="chatTabLog">
|
|
211
206
|
<div class="chat-idle" data-i18n-html="chat.idle">Type a request — the agent runs headless in this project with full memory
|
|
212
207
|
(<code>CLAUDE.md → AGENTS.md</code>) and updates the board live.</div>
|
|
213
208
|
</div>
|
|
209
|
+
<!-- Toolbar right above the input, not up in the header — this is where you're looking
|
|
210
|
+
when you reach for Summarize/Clear or switch which agent the next message runs on. -->
|
|
211
|
+
<div class="chat-tab-toolbar">
|
|
212
|
+
<button id="tabSummarizeBtn" class="mini-btn" data-i18n-title="chat.summarizeTitle" data-i18n="action.summarize" title="Condense the recent activity into a summary">Summarize</button>
|
|
213
|
+
<button id="tabClearBtn" class="mini-btn" data-i18n-title="chat.clearTitle" data-i18n="action.clear" title="Clear the chat log">Clear</button>
|
|
214
|
+
<select id="tabRunAgent" class="chat-agent" title="Agent"></select>
|
|
215
|
+
</div>
|
|
214
216
|
<div class="chat-tab-input">
|
|
215
217
|
<textarea id="tabRunPrompt" class="chat-ta" data-i18n-ph="chat.inputPlaceholder" placeholder="e.g. Add a login feature with email + password"></textarea>
|
|
216
218
|
<div class="chat-tab-actions">
|
|
@@ -306,13 +308,17 @@
|
|
|
306
308
|
<span class="chat-title" data-i18n="chat.widgetTitle">Run an agent</span>
|
|
307
309
|
<span class="chat-sub" data-i18n="chat.widgetSub">Quick access · full view in the Chat tab</span>
|
|
308
310
|
</div>
|
|
309
|
-
<select id="runAgent" class="chat-agent" title="Agent"></select>
|
|
310
311
|
<button class="chat-close" id="chatClose" aria-label="Close">×</button>
|
|
311
312
|
</div>
|
|
312
313
|
<div class="chat-log" id="chatLog">
|
|
313
314
|
<div class="chat-idle" data-i18n-html="chat.idle">Type a request — the agent runs headless in this project with full memory
|
|
314
315
|
(<code>CLAUDE.md → AGENTS.md</code>) and updates the board live.</div>
|
|
315
316
|
</div>
|
|
317
|
+
<div class="chat-toolbar">
|
|
318
|
+
<button id="widgetSummarizeBtn" class="mini-btn" data-i18n-title="chat.summarizeTitle" data-i18n="action.summarize" title="Condense the recent activity into a summary">Summarize</button>
|
|
319
|
+
<button id="widgetClearBtn" class="mini-btn" data-i18n-title="chat.clearTitle" data-i18n="action.clear" title="Clear the chat log">Clear</button>
|
|
320
|
+
<select id="runAgent" class="chat-agent" title="Agent"></select>
|
|
321
|
+
</div>
|
|
316
322
|
<div class="chat-input">
|
|
317
323
|
<textarea id="runPrompt" class="chat-ta" data-i18n-ph="chat.inputPlaceholder" placeholder="e.g. Add a login feature with email + password"></textarea>
|
|
318
324
|
<button id="runBtn" class="btn primary chat-send" data-i18n="action.send">Send</button>
|
|
@@ -378,7 +378,8 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
378
378
|
.chat-title { font-weight:700; font-size:15px; }
|
|
379
379
|
.chat-sub { font-size:10px; color:var(--faint); text-transform:uppercase; letter-spacing:.04em; }
|
|
380
380
|
.chat-agent { margin-left:auto; flex-shrink:0; font:inherit; font-size:12px; background:var(--surface); color:var(--ink); border:1px solid var(--line); border-radius:7px; padding:5px 8px; }
|
|
381
|
-
.chat-
|
|
381
|
+
.chat-toolbar { display:flex; align-items:center; gap:8px; padding:8px 14px; border-top:1px solid var(--line); flex-wrap:wrap; }
|
|
382
|
+
.chat-close { width:28px; height:28px; flex-shrink:0; margin-left:auto; border:1px solid var(--line); background:var(--surface-2); color:var(--muted); border-radius:8px; font-size:17px; cursor:pointer; transition:border-color .15s,color .15s; }
|
|
382
383
|
.chat-close:hover { border-color:color-mix(in srgb,var(--s-blocked) 45%,var(--line)); color:var(--ink); }
|
|
383
384
|
.chat-log { flex:1; overflow-y:auto; padding:16px; display:flex; flex-direction:column; gap:11px; }
|
|
384
385
|
.chat-idle { color:var(--faint); font-style:italic; font-size:12.5px; }
|
|
@@ -414,10 +415,10 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
414
415
|
.chat-tab-head { display:flex; align-items:flex-start; gap:14px; margin-bottom:14px; }
|
|
415
416
|
.chat-tab-head .panel-title { margin:0; }
|
|
416
417
|
.chat-tab-head .panel-sub { margin:2px 0 0; }
|
|
417
|
-
.chat-tab-head-actions { display:flex; align-items:center; gap:8px; margin-left:auto; flex-shrink:0; flex-wrap:wrap; }
|
|
418
|
-
.chat-tab-head .chat-agent { flex-shrink:0; }
|
|
419
418
|
.chat-tab-log { flex:1; min-height:220px; overflow-y:auto; display:flex; flex-direction:column; gap:12px; background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:20px; box-shadow:var(--shadow); }
|
|
420
|
-
.chat-tab-
|
|
419
|
+
.chat-tab-toolbar { display:flex; align-items:center; gap:8px; margin-top:10px; flex-wrap:wrap; }
|
|
420
|
+
.chat-tab-toolbar .chat-agent { margin-left:auto; flex-shrink:0; }
|
|
421
|
+
.chat-tab-input { display:flex; gap:10px; margin-top:10px; }
|
|
421
422
|
.chat-tab-input .chat-ta { flex:1; min-height:56px; max-height:180px; }
|
|
422
423
|
.chat-tab-actions { display:flex; flex-direction:column; gap:8px; flex-shrink:0; }
|
|
423
424
|
.chat-tab-wrap .chat-warn { margin:8px 0 0; }
|
|
@@ -9,6 +9,18 @@
|
|
|
9
9
|
*/
|
|
10
10
|
const { spawn } = require('child_process');
|
|
11
11
|
const store = require('../lib/store');
|
|
12
|
+
const agentsRegistry = require('../lib/agents-registry');
|
|
13
|
+
|
|
14
|
+
// The command to run `which`: config.json's own runners map first (an explicit user choice always
|
|
15
|
+
// wins), falling back to the registry's default for a known, headless-capable, genuinely-installed
|
|
16
|
+
// agent — so picking one from a per-message agent list works the first time, before it's ever been
|
|
17
|
+
// the project's "active agent" and had a runner seeded into config.json for it.
|
|
18
|
+
function resolveRunnerCommand(root, cfg, which, opts) {
|
|
19
|
+
if (cfg.runners && cfg.runners[which]) return cfg.runners[which];
|
|
20
|
+
const known = agentsRegistry.KNOWN_AGENTS.find((a) => a.id === which);
|
|
21
|
+
if (known && known.runner && agentsRegistry.isAgentInstalled(which, root, opts)) return known.runner;
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
12
24
|
|
|
13
25
|
function runStart(root, run) {
|
|
14
26
|
const rt = store.readRuntime(root); rt.agents = rt.agents || []; rt.agents.push(run); store.writeRuntime(root, rt);
|
|
@@ -50,7 +62,7 @@ function pushAttention(root, text, by) {
|
|
|
50
62
|
function startRun(root, { prompt, agent, logPrompt = true }, emit) {
|
|
51
63
|
const cfg = store.readConfig(root);
|
|
52
64
|
const which = agent || cfg.agent || 'claude';
|
|
53
|
-
const cmdStr = cfg
|
|
65
|
+
const cmdStr = resolveRunnerCommand(root, cfg, which);
|
|
54
66
|
if (!cmdStr) return { error: `No runner configured for "${which}".` };
|
|
55
67
|
const parts = cmdStr.split(/\s+/).filter(Boolean);
|
|
56
68
|
const runId = 'r' + Date.now().toString(36);
|
|
@@ -97,4 +109,4 @@ function startRun(root, { prompt, agent, logPrompt = true }, emit) {
|
|
|
97
109
|
return { runId, child };
|
|
98
110
|
}
|
|
99
111
|
|
|
100
|
-
module.exports = { startRun };
|
|
112
|
+
module.exports = { startRun, resolveRunnerCommand };
|
|
@@ -35,7 +35,7 @@ function project(){
|
|
|
35
35
|
// Known vs. actually-installed agents — the topbar switcher needs both: the full list to offer,
|
|
36
36
|
// and which ones are real (bin on PATH, or the project already has that agent's config dir) so it
|
|
37
37
|
// can refuse to activate one that isn't there.
|
|
38
|
-
p.knownAgents = agentsRegistry.KNOWN_AGENTS.map((a) => ({ id: a.id, label: a.label }));
|
|
38
|
+
p.knownAgents = agentsRegistry.KNOWN_AGENTS.map((a) => ({ id: a.id, label: a.label, headless: a.headless }));
|
|
39
39
|
p.installedAgents = agentsRegistry.installedAgents(ROOT);
|
|
40
40
|
return p;
|
|
41
41
|
}
|
|
@@ -62,8 +62,11 @@ function writeConfig(patch){
|
|
|
62
62
|
}
|
|
63
63
|
cfg.agent = id;
|
|
64
64
|
// Seed a default runner if this agent was never configured (e.g. installed after init/update).
|
|
65
|
+
// A headless:false agent (e.g. kimi) has no runner to seed — it can still be the active agent,
|
|
66
|
+
// it just can't be spawned by Run/Orchestrate/Summarize (disabled client-side; runner.js and
|
|
67
|
+
// summarize.js also refuse server-side either way).
|
|
65
68
|
const known = agentsRegistry.KNOWN_AGENTS.find((a) => a.id === id);
|
|
66
|
-
if (known) { cfg.runners = cfg.runners || {}; if (!cfg.runners[id]) cfg.runners[id] = known.runner; }
|
|
69
|
+
if (known && known.runner) { cfg.runners = cfg.runners || {}; if (!cfg.runners[id]) cfg.runners[id] = known.runner; }
|
|
67
70
|
}
|
|
68
71
|
fs.writeFileSync(cp, JSON.stringify(cfg, null, 2) + '\n');
|
|
69
72
|
return cfg;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
const { spawn } = require('child_process');
|
|
8
8
|
const store = require('../lib/store');
|
|
9
|
+
const { resolveRunnerCommand } = require('./runner');
|
|
9
10
|
|
|
10
11
|
const DEFAULT_LIMIT = 40;
|
|
11
12
|
|
|
@@ -20,7 +21,7 @@ function formatLog(messages, limit = DEFAULT_LIMIT) {
|
|
|
20
21
|
function runSummarize(root, { agent } = {}, emit) {
|
|
21
22
|
const cfg = store.readConfig(root);
|
|
22
23
|
const which = agent || cfg.agent || 'claude';
|
|
23
|
-
const cmdStr = cfg
|
|
24
|
+
const cmdStr = resolveRunnerCommand(root, cfg, which);
|
|
24
25
|
if (!cmdStr) return { error: `No runner configured for "${which}".` };
|
|
25
26
|
|
|
26
27
|
const rt = store.readRuntime(root);
|
|
@@ -9,14 +9,19 @@
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
12
|
+
// headless:false = detectable and selectable as the active agent, but spectoflow never spawns it
|
|
13
|
+
// itself (no confirmed non-interactive one-shot mode) — Run/Orchestrate/Summarize stay disabled for
|
|
14
|
+
// it client-side; runner/summarize.js also refuse server-side (defense in depth). See the longer
|
|
15
|
+
// rationale above lib/adapters.js's REGISTRY, the richer install-time twin of this list.
|
|
12
16
|
const KNOWN_AGENTS = [
|
|
13
|
-
{ id: 'claude', label: 'Claude Code', bin: 'claude', dirs: ['.claude'], runner: 'claude -p --permission-mode acceptEdits' },
|
|
14
|
-
{ id: 'codex', label: 'Codex', bin: 'codex', dirs: ['.codex'], runner: 'codex exec' },
|
|
15
|
-
{ id: 'cursor', label: 'Cursor', bin: 'cursor-agent', dirs: ['.cursor'], runner: 'cursor-agent -p' },
|
|
16
|
-
{ id: 'gemini', label: 'Gemini CLI', bin: 'gemini', dirs: ['.gemini'], runner: 'gemini -p' },
|
|
17
|
-
{ id: 'opencode', label: 'OpenCode', bin: 'opencode', dirs: ['.opencode'], runner: 'opencode run --quiet' },
|
|
18
|
-
{ id: 'kiro', label: 'Kiro CLI', bin: 'kiro-cli', dirs: ['.kiro'], runner: 'kiro-cli chat --no-interactive --trust-all-tools' },
|
|
19
|
-
{ id: 'antigravity', label: 'Antigravity', bin: 'agy', dirs: [], runner: 'agy -p' },
|
|
17
|
+
{ id: 'claude', label: 'Claude Code', bin: 'claude', dirs: ['.claude'], runner: 'claude -p --permission-mode acceptEdits', headless: true },
|
|
18
|
+
{ id: 'codex', label: 'Codex', bin: 'codex', dirs: ['.codex'], runner: 'codex exec', headless: true },
|
|
19
|
+
{ id: 'cursor', label: 'Cursor', bin: 'cursor-agent', dirs: ['.cursor'], runner: 'cursor-agent -p', headless: true },
|
|
20
|
+
{ id: 'gemini', label: 'Gemini CLI', bin: 'gemini', dirs: ['.gemini'], runner: 'gemini -p', headless: true },
|
|
21
|
+
{ id: 'opencode', label: 'OpenCode', bin: 'opencode', dirs: ['.opencode'], runner: 'opencode run --quiet', headless: true },
|
|
22
|
+
{ id: 'kiro', label: 'Kiro CLI', bin: 'kiro-cli', dirs: ['.kiro'], runner: 'kiro-cli chat --no-interactive --trust-all-tools', headless: true },
|
|
23
|
+
{ id: 'antigravity', label: 'Antigravity', bin: 'agy', dirs: [], runner: 'agy -p', headless: true },
|
|
24
|
+
{ id: 'kimi', label: 'Kimi CLI', bin: 'kimi', dirs: [], runner: null, headless: false },
|
|
20
25
|
];
|
|
21
26
|
|
|
22
27
|
// Is `bin` an executable resolvable on PATH? On win32, an extension from PATHEXT is required, so we
|