spectoflow 0.31.0 → 0.32.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/README.md +16 -0
- package/bin/spectoflow.js +31 -0
- package/lib/dashboard/files.js +14 -3
- package/lib/dashboard/handlers.js +1 -0
- package/lib/dashboard/hub-server.js +1 -1
- package/lib/dashboard/meeting.js +5 -1
- package/lib/dashboard/ops.js +24 -3
- package/lib/dashboard/orchestrator.js +1 -1
- package/lib/dashboard/public/app.js +79 -6
- package/lib/dashboard/public/i18n.js +24 -0
- package/lib/dashboard/public/index.html +4 -2
- package/lib/dashboard/public/styles.css +14 -0
- package/lib/dashboard/routes.js +4 -0
- package/lib/dashboard/runner.js +38 -6
- package/lib/dashboard/summarize.js +5 -1
- package/lib/runner-trust.js +48 -0
- package/lib/store.js +6 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,6 +24,17 @@ An **agent-agnostic** spec-driven development framework with a **real-time local
|
|
|
24
24
|
You speak in plain language; the framework classifies your intent and runs the right workflow. No
|
|
25
25
|
ceremonial command to start.
|
|
26
26
|
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g spectoflow
|
|
31
|
+
cd my-project && spectoflow init # fits the workflow to your project, detects your agents
|
|
32
|
+
spectoflow dashboard # → http://localhost:4319
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then just tell your coding agent what you want to build. Optional, once per machine:
|
|
36
|
+
`spectoflow brain setup` so your agents remember you across projects.
|
|
37
|
+
|
|
27
38
|
**Works with whichever coding agent you have.** `init` auto-detects what's installed; the dashboard's
|
|
28
39
|
topbar always shows the **active agent**, front and center, with a switcher — pick another and it's
|
|
29
40
|
verified as genuinely installed before activating (a red **"No agent found"** if none is), never
|
|
@@ -205,6 +216,11 @@ that another website sent — a malicious page, a DNS-rebinding trick, or a tunn
|
|
|
205
216
|
machine can't drive it. Clicking a link to it from another site still opens it. To reach your projects from
|
|
206
217
|
another device, use the online dashboard ([below](#going-online-optional-local-hub-vs-relay-server)).
|
|
207
218
|
|
|
219
|
+
**A custom agent command needs your OK.** `config.json → runners` sets the command that launches each agent,
|
|
220
|
+
and that file is committed — it can come from a cloned repository or a teammate's change. A command other than
|
|
221
|
+
the agent's default only runs once you allow it on this machine: Personalize → *Agent & automation* → **Allow
|
|
222
|
+
on this machine**, or `spectoflow runners allow <agent>`. Change the command and it asks again.
|
|
223
|
+
|
|
208
224
|
### One hub, every project
|
|
209
225
|
|
|
210
226
|
There is only ever **one dashboard process on your machine**, no matter how many projects you have.
|
package/bin/spectoflow.js
CHANGED
|
@@ -394,6 +394,30 @@ function workflowCmd() {
|
|
|
394
394
|
console.log(`\n ${c.g('✓')} ${changed.length} step(s) updated in .spectoflow/workflow.md\n`);
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
+
// ---- runners: the commands that launch agents, and which custom ones are allowed on this machine (D76) ----
|
|
398
|
+
function runnersCmd() {
|
|
399
|
+
const root = process.cwd();
|
|
400
|
+
if (!fs.existsSync(path.join(root, '.spectoflow', 'config.json'))) { console.log('No spectoflow project here. Run: spectoflow init'); process.exitCode = 1; return; }
|
|
401
|
+
const trust = require('../lib/runner-trust');
|
|
402
|
+
const runners = store.readConfig(root).runners || {};
|
|
403
|
+
if (argv[1] === 'allow') {
|
|
404
|
+
const which = argv[2];
|
|
405
|
+
if (!which || typeof runners[which] !== 'string') { console.log(`${c.y('!')} Usage: spectoflow runners allow <agent> ${c.dim('— one of: ' + (Object.keys(runners).join(', ') || 'none'))}`); process.exitCode = 1; return; }
|
|
406
|
+
if (trust.isDefault(which, runners[which])) { console.log(`${c.dim('·')} ${which} uses the default command — nothing to allow.`); return; }
|
|
407
|
+
trust.trust(root, which, runners[which]);
|
|
408
|
+
console.log(`${c.g('✓')} allowed on this machine for this project: ${c.bold(which)} → ${runners[which]}`);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
console.log(wordmark());
|
|
412
|
+
const w = Math.max(4, ...Object.keys(runners).map((k) => k.length));
|
|
413
|
+
for (const [which, cmd] of Object.entries(runners)) {
|
|
414
|
+
const state = trust.isDefault(which, cmd) ? c.dim('default') : trust.isTrusted(root, which, cmd) ? c.g('allowed') : c.y('needs your OK');
|
|
415
|
+
console.log(` ${which.padEnd(w)} ${state.padEnd(24)} ${cmd}`);
|
|
416
|
+
}
|
|
417
|
+
if (!Object.keys(runners).length) console.log(c.dim(' (no runners in config.json)'));
|
|
418
|
+
if (trust.untrusted(root, { runners }).length) console.log(`\n ${c.dim('a custom command only runs once allowed:')} ${c.g('spectoflow runners allow <agent>')}\n`);
|
|
419
|
+
}
|
|
420
|
+
|
|
397
421
|
// ---- brain: the user's second brain (~/.spectoflow/brain.md), shared by every project ----
|
|
398
422
|
function brainCmd() {
|
|
399
423
|
const brain = require('../lib/brain');
|
|
@@ -493,6 +517,11 @@ async function startDashboard() {
|
|
|
493
517
|
const boardUrl = (p) => (entry ? `http://localhost:${p}/p/${entry.id}/board` : `http://localhost:${p}/`);
|
|
494
518
|
const info = workspace.readLock();
|
|
495
519
|
if (info && info.port && await probeDashboard(info.port)) {
|
|
520
|
+
// A hub started by an older spectoflow keeps running that old code: replace it (D77).
|
|
521
|
+
if (info.version !== VERSION) {
|
|
522
|
+
console.log(`${c.cy('↻')} the running hub is ${info.version ? 'spectoflow v' + info.version : 'an older spectoflow'} — restarting it on v${VERSION}`);
|
|
523
|
+
return restartDashboard();
|
|
524
|
+
}
|
|
496
525
|
console.log(`${c.g('●')} hub already running → ${c.bold(boardUrl(info.port))}`);
|
|
497
526
|
await printOnlineLine(info.port, true);
|
|
498
527
|
return printDashboardCommands();
|
|
@@ -728,6 +757,7 @@ const HELP = {
|
|
|
728
757
|
${c.g('brain setup')} register the MCP server in each installed agent's USER-level config (once
|
|
729
758
|
per machine; never touches an existing entry; Goose gets a snippet to paste)
|
|
730
759
|
Learned facts are added directly by default: ${c.g('spectoflow config set brain.autoAdd false')} to confirm them first.`,
|
|
760
|
+
runners: `${c.bold('spectoflow runners')} ${c.dim('[allow <agent>]')}\n\n The commands that launch each agent (${c.dim('.spectoflow/config.json → runners')}). A command other than the\n agent's default only runs once you allow it on this machine — config.json is committed, so it can come\n from a cloned repository, a teammate, or an agent's edit.\n ${c.g('runners')} list them: default · allowed · needs your OK\n ${c.g('runners allow <agent>')} allow that agent's current command, for this project, on this machine`,
|
|
731
761
|
stop: `${c.bold('spectoflow stop')}\n Stop the running dashboard (alias for ${c.g('spectoflow dashboard stop')}).`,
|
|
732
762
|
config: `${c.bold('spectoflow config')} ${c.dim('[get <key> | set <key> <value>]')}\n
|
|
733
763
|
Global settings that apply to every project on this machine, stored in ${c.dim('~/.spectoflow/config.json')}:
|
|
@@ -745,6 +775,7 @@ const fns = {
|
|
|
745
775
|
config: configCmd,
|
|
746
776
|
mcp: () => require('../lib/mcp-server').serve({ version: VERSION }),
|
|
747
777
|
brain: brainCmd,
|
|
778
|
+
runners: runnersCmd,
|
|
748
779
|
agents: () => { console.log(wordmark()); printAgents(false); },
|
|
749
780
|
skills: () => { console.log(wordmark()); printSkills(false); },
|
|
750
781
|
workflow: workflowCmd,
|
package/lib/dashboard/files.js
CHANGED
|
@@ -38,6 +38,15 @@ function realUnderRoot(root, abs) {
|
|
|
38
38
|
return real;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// Files that make tools run commands on their own — the agent launcher (config.json runners), hooks,
|
|
42
|
+
// MCP server definitions, editor tasks. Someone editing through the online dashboard must never be able
|
|
43
|
+
// to change them: that would turn a project write into running anything on the owner's machine (D76).
|
|
44
|
+
const EXEC_CONFIG = ['.git', '.spectoflow/config.json', '.spectoflow/hooks', '.spectoflow/lib', '.claude', '.mcp.json', '.cursor', '.codex', '.gemini', '.kiro', '.vscode', '.idea', '.husky'];
|
|
45
|
+
function isExecConfig(rel) {
|
|
46
|
+
const n = String(rel || '').replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+$/, '').toLowerCase();
|
|
47
|
+
return EXEC_CONFIG.some((p) => n === p || n.startsWith(p + '/'));
|
|
48
|
+
}
|
|
49
|
+
|
|
41
50
|
function isUnderGit(rel) {
|
|
42
51
|
const n = String(rel || '').replace(/\\/g, '/').replace(/^\/+/, '');
|
|
43
52
|
return n === '.git' || n.startsWith('.git/');
|
|
@@ -83,9 +92,10 @@ function readFile(root, rel) {
|
|
|
83
92
|
return { content: buf.toString('utf8') };
|
|
84
93
|
}
|
|
85
94
|
|
|
86
|
-
function writeFile(root, rel, content) {
|
|
95
|
+
function writeFile(root, rel, content, { remote = false } = {}) {
|
|
87
96
|
if (typeof content !== 'string') return { error: 'Missing content.' };
|
|
88
97
|
if (isUnderGit(rel)) return { error: 'Writes under .git are blocked.' };
|
|
98
|
+
if (remote && isExecConfig(rel)) return { error: 'This file controls commands run on the owner\'s machine: it can only be edited locally.' };
|
|
89
99
|
const abs = safePath(root, rel);
|
|
90
100
|
if (!abs) return { error: 'Invalid path.' };
|
|
91
101
|
const parentDir = path.dirname(abs);
|
|
@@ -95,8 +105,9 @@ function writeFile(root, rel, content) {
|
|
|
95
105
|
return { ok: true };
|
|
96
106
|
}
|
|
97
107
|
|
|
98
|
-
function mkdir(root, rel) {
|
|
108
|
+
function mkdir(root, rel, { remote = false } = {}) {
|
|
99
109
|
if (isUnderGit(rel)) return { error: 'Cannot create folders under .git.' };
|
|
110
|
+
if (remote && isExecConfig(rel)) return { error: 'This folder controls commands run on the owner\'s machine: it can only be changed locally.' };
|
|
100
111
|
const abs = safePath(root, rel);
|
|
101
112
|
if (!abs) return { error: 'Invalid path.' };
|
|
102
113
|
const parentDir = path.dirname(abs);
|
|
@@ -105,4 +116,4 @@ function mkdir(root, rel) {
|
|
|
105
116
|
return { ok: true };
|
|
106
117
|
}
|
|
107
118
|
|
|
108
|
-
module.exports = { tree, readFile, writeFile, mkdir };
|
|
119
|
+
module.exports = { tree, readFile, writeFile, mkdir, isExecConfig };
|
|
@@ -68,6 +68,7 @@ function createHandlers(root) {
|
|
|
68
68
|
// A process restart loses any in-flight orchestration; clear a stale 'running'/'awaiting_approval'
|
|
69
69
|
// so the 409 guard in orchestrate.start can't wedge forever. Not a resume — just un-wedging.
|
|
70
70
|
try { orchestrator.reconcileOnBoot(root); } catch (_) {}
|
|
71
|
+
try { require('./runner').reconcileRunsOnBoot(root); } catch (_) {}
|
|
71
72
|
}
|
|
72
73
|
return {
|
|
73
74
|
handleApi,
|
|
@@ -278,7 +278,7 @@ function serveStatic(reqPath, req, res, root) {
|
|
|
278
278
|
const PROJECT_PREFIX = /^\/p\/([0-9a-f]{6})(\/.*)?$/;
|
|
279
279
|
|
|
280
280
|
const LOCK = workspace.lockPath();
|
|
281
|
-
function writeLock(){ try{ fs.mkdirSync(path.dirname(LOCK),{recursive:true}); fs.writeFileSync(LOCK, JSON.stringify({ pid:process.pid, port:PORT, url:`http://localhost:${PORT}`, startedAt:new Date().toISOString() })+'\n'); }catch{} }
|
|
281
|
+
function writeLock(){ try{ fs.mkdirSync(path.dirname(LOCK),{recursive:true}); fs.writeFileSync(LOCK, JSON.stringify({ pid:process.pid, port:PORT, url:`http://localhost:${PORT}`, version:VERSION, startedAt:new Date().toISOString() })+'\n'); }catch{} }
|
|
282
282
|
function clearLock(){ try{ const l=JSON.parse(fs.readFileSync(LOCK,'utf8')); if(l.pid===process.pid) fs.unlinkSync(LOCK); }catch{} }
|
|
283
283
|
process.on('exit', clearLock);
|
|
284
284
|
['SIGINT','SIGTERM'].forEach((s)=> process.on(s, ()=>{ if (connector) connector.stop(); clearLock(); process.exit(0); }));
|
package/lib/dashboard/meeting.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
const { spawn } = require('child_process');
|
|
11
11
|
const store = require('../store');
|
|
12
12
|
const files = require('./files');
|
|
13
|
-
const { resolveRunnerCommand } = require('./runner');
|
|
13
|
+
const { resolveRunnerCommand, trackChild } = require('./runner');
|
|
14
|
+
const runnerTrust = require('../runner-trust');
|
|
14
15
|
const { formatLog } = require('./summarize'); // reused as-is rather than reimplemented — see report
|
|
15
16
|
|
|
16
17
|
const TASK_LIMIT = 20; // mirrors summarize.js's own DEFAULT_LIMIT philosophy (cap, most-recent-last)
|
|
@@ -82,6 +83,8 @@ function runMeetingGenerate(root, { agent, date } = {}, emit) {
|
|
|
82
83
|
const which = agent || cfg.agent || 'claude';
|
|
83
84
|
const cmdStr = resolveRunnerCommand(root, cfg, which);
|
|
84
85
|
if (!cmdStr) return { error: `No runner configured for "${which}".` };
|
|
86
|
+
const blocked = runnerTrust.blockedMessage(root, which, cmdStr);
|
|
87
|
+
if (blocked) return { error: blocked, untrustedRunner: { agent: which, command: cmdStr } };
|
|
85
88
|
|
|
86
89
|
const day = date || todayLocal();
|
|
87
90
|
if (!isValidDate(day)) return { error: 'Invalid date.' };
|
|
@@ -103,6 +106,7 @@ function runMeetingGenerate(root, { agent, date } = {}, emit) {
|
|
|
103
106
|
let out = '';
|
|
104
107
|
child.stdout && child.stdout.on('data', (d) => { out += d.toString(); });
|
|
105
108
|
child.stderr && child.stderr.on('data', (d) => { out += d.toString(); });
|
|
109
|
+
trackChild(root, child);
|
|
106
110
|
child.on('close', (code) => {
|
|
107
111
|
const text = out.trim() || (code === 0 ? '(no output)' : `meeting generate failed (exit ${code})`);
|
|
108
112
|
files.writeFile(root, meetingPath(day), text);
|
package/lib/dashboard/ops.js
CHANGED
|
@@ -11,7 +11,7 @@ const fs = require('fs');
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const store = require('../store');
|
|
13
13
|
const files = require('./files');
|
|
14
|
-
const { startRun } = require('./runner');
|
|
14
|
+
const { startRun, stopRuns } = require('./runner');
|
|
15
15
|
const { runSummarize } = require('./summarize');
|
|
16
16
|
const { runMeetingGenerate, todayLocal } = require('./meeting');
|
|
17
17
|
const orchestrator = require('./orchestrator');
|
|
@@ -21,6 +21,7 @@ const brain = require('../brain');
|
|
|
21
21
|
const brainSetup = require('../brain-setup');
|
|
22
22
|
const globalConfig = require('../global-config');
|
|
23
23
|
const workflowDetect = require('../workflow-detect');
|
|
24
|
+
const runnerTrust = require('../runner-trust');
|
|
24
25
|
|
|
25
26
|
const PKG_VERSION = require('../../package.json').version;
|
|
26
27
|
|
|
@@ -163,14 +164,26 @@ const ops = {
|
|
|
163
164
|
// todayLocal() header comment for why this, not the browser's date, is the one source of truth
|
|
164
165
|
// for which .spectoflow/meetings/<date>.md "today" resolves to.
|
|
165
166
|
p.todayDate = todayLocal();
|
|
167
|
+
p.untrustedRunners = runnerTrust.untrusted(root, p.config);
|
|
168
|
+
p.kitVersion = PKG_VERSION;
|
|
166
169
|
return p;
|
|
167
170
|
},
|
|
168
171
|
'agentfile.read': async (root, { path: rel }) => readAgentFile(root, rel),
|
|
169
172
|
|
|
170
173
|
'files.tree': async (root) => ({ tree: files.tree(root) }),
|
|
171
174
|
'files.read': async (root, { path: rel }) => filesResult(files.readFile(root, rel || '')),
|
|
172
|
-
'files.write': async (root, { path: rel, content }, ctx) => changed(ctx, filesResult(files.writeFile(root, rel, content))),
|
|
173
|
-
'files.mkdir': async (root, { path: rel }, ctx) => changed(ctx, filesResult(files.mkdir(root, rel))),
|
|
175
|
+
'files.write': async (root, { path: rel, content }, ctx) => changed(ctx, filesResult(files.writeFile(root, rel, content, { remote: !!ctx.remote }))),
|
|
176
|
+
'files.mkdir': async (root, { path: rel }, ctx) => changed(ctx, filesResult(files.mkdir(root, rel, { remote: !!ctx.remote }))),
|
|
177
|
+
|
|
178
|
+
// Allow, on this machine, the custom command config.json sets for one agent (D76). Local only: the relay
|
|
179
|
+
// doesn't list this op, and ctx.remote is refused here too.
|
|
180
|
+
'runners.trust': async (root, { agent }, ctx) => {
|
|
181
|
+
if (ctx && ctx.remote) throw new OpError(404, 'Unknown operation.');
|
|
182
|
+
const cmd = ((store.readConfig(root).runners) || {})[agent];
|
|
183
|
+
if (typeof cmd !== 'string' || !cmd.trim()) notFound(`No custom command set for "${agent}".`);
|
|
184
|
+
runnerTrust.trust(root, agent, cmd);
|
|
185
|
+
return changed(ctx, { ok: true, agent, command: cmd });
|
|
186
|
+
},
|
|
174
187
|
|
|
175
188
|
'task.add': async (root, { title, phase, file, owner, level }, ctx) => {
|
|
176
189
|
const t = store.addTask(root, { title: text(title, 'A title is required.'), phase, file, owner, level });
|
|
@@ -222,6 +235,14 @@ const ops = {
|
|
|
222
235
|
if (r.error) bad(r.error);
|
|
223
236
|
return { runId: r.runId };
|
|
224
237
|
},
|
|
238
|
+
// Bring the project's framework files up to the installed spectoflow (same as `spectoflow update`). Local
|
|
239
|
+
// only: it writes framework files on the owner's machine (D77).
|
|
240
|
+
'project.update': async (root, _args, ctx) => {
|
|
241
|
+
if (ctx && ctx.remote) throw new OpError(404, 'Unknown operation.');
|
|
242
|
+
const r = require('../update').runUpdate({ projectRoot: root, templatesDir: path.join(__dirname, '..', '..', 'templates'), version: PKG_VERSION });
|
|
243
|
+
return changed(ctx, { fromVersion: r.fromVersion, toVersion: r.toVersion, refreshed: r.refreshed.length + r.created.length + r.forced.length + r.removed.length, review: r.newSidecar });
|
|
244
|
+
},
|
|
245
|
+
'run.stop': async (root, _args, ctx) => changed(ctx, { stopped: stopRuns(root) }),
|
|
225
246
|
'chat.summarize': async (root, { agent }, ctx) => {
|
|
226
247
|
const r = runSummarize(root, { agent }, ctx.emit);
|
|
227
248
|
if (r.error) bad(r.error);
|
|
@@ -90,7 +90,7 @@ function defaultRunStep({ root, step, agent, skill, request, learn = true }, emi
|
|
|
90
90
|
// logPrompt:false — the orchestrator already posts a clean "→ step (agent)" line; the raw
|
|
91
91
|
// priming prompt would otherwise show as a noisy user bubble.
|
|
92
92
|
const r = startRun(root, { prompt, agent: tool, logPrompt: false, learn }, (e) => { emit(e); if (e.type === 'run-end') resolve(e.code); });
|
|
93
|
-
if (r.error) {
|
|
93
|
+
if (r.error) { post(root, 'orchestrator', 'status', r.error, emit); resolve(1); }
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -300,7 +300,7 @@ async function load(){
|
|
|
300
300
|
const r = await fetch(withProject('/api/project')); P = await r.json();
|
|
301
301
|
syncSettingsFromServer();
|
|
302
302
|
REMOTE=typeof P.online==='boolean'; // known before the first paint: the brain tab is hidden online
|
|
303
|
-
render(); setOffline(P);
|
|
303
|
+
render(); setOffline(P); renderUpdateBar(); notifyOrchestration();
|
|
304
304
|
if(!REMOTE && !brainData) loadBrain();
|
|
305
305
|
if(openTaskId) openDrawer(openTaskId,true);
|
|
306
306
|
}
|
|
@@ -315,7 +315,7 @@ function connect(){
|
|
|
315
315
|
let m; try{ m=JSON.parse(ev.data); }catch{ return; }
|
|
316
316
|
if(m.type==='change'||m.type==='message') return scheduleLoad(); // messages live from runtime.messages
|
|
317
317
|
if(m.type==='brain') return loadBrain(); // ~/.spectoflow/brain.md changed (page, MCP, run line)
|
|
318
|
-
if(m.type==='run-start'||m.type==='run-end') { chatState.forEach(st=>{ st.rawBlock=null; }); sseBusy=(m.type==='run-start'); updateChatBusyUI(); return; }
|
|
318
|
+
if(m.type==='run-start'||m.type==='run-end') { chatState.forEach(st=>{ st.rawBlock=null; }); sseBusy=(m.type==='run-start'); updateChatBusyUI(); if(m.type==='run-end') notify(t(m.code===0?'notify.done':'notify.failed',{project:P&&P.projectName||'spectoflow'}), ''); return; }
|
|
319
319
|
if(m.type==='run-line') return appendRaw(m.chunk); // raw output is ephemeral (not logged)
|
|
320
320
|
};
|
|
321
321
|
es.onerror = ()=>{ $('#sync').classList.add('offline'); $('#syncLabel').textContent='offline'; };
|
|
@@ -376,6 +376,40 @@ function renderApproval(container){
|
|
|
376
376
|
const acts=el('div','c-actions'); acts.append(a,c); row.append(acts);
|
|
377
377
|
container.append(row); scrollChat(container);
|
|
378
378
|
}
|
|
379
|
+
// A launch refused before any agent ran (no runner, a custom command not allowed yet…) has no run-start/
|
|
380
|
+
// run-end to show for it: say so in the chat, without persisting anything.
|
|
381
|
+
function showChatError(message){
|
|
382
|
+
chatContainers().forEach(container=>{
|
|
383
|
+
clearIdle(container);
|
|
384
|
+
const st=stateFor(container); st.rawBlock=null;
|
|
385
|
+
container.append(el('div','msg chat-error',message));
|
|
386
|
+
scrollChat(container);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
// Native browser notifications when an agent finishes or a step waits for approval — only while this tab
|
|
390
|
+
// isn't in front (D77). Permission is asked on the user's own click that starts agent work, never unprompted.
|
|
391
|
+
function askNotifyPermission(){ try{ if('Notification' in window && Notification.permission==='default') Notification.requestPermission(); }catch(_){} }
|
|
392
|
+
function notify(title,body){
|
|
393
|
+
try{
|
|
394
|
+
if(!document.hidden || !('Notification' in window) || Notification.permission!=='granted') return;
|
|
395
|
+
const n=new Notification(title,{body,tag:'spectoflow-'+(P&&P.projectName||'')});
|
|
396
|
+
n.onclick=()=>{ window.focus(); n.close(); };
|
|
397
|
+
}catch(_){}
|
|
398
|
+
}
|
|
399
|
+
let notifiedApproval='';
|
|
400
|
+
function notifyOrchestration(){
|
|
401
|
+
const o=P&&P.runtime&&P.runtime.orchestration; if(!o) return;
|
|
402
|
+
const step=o.steps&&o.steps[o.currentStep];
|
|
403
|
+
const key=o.status==='awaiting_approval'?`${o.id}:${o.currentStep}`:'';
|
|
404
|
+
if(key && key!==notifiedApproval){ notify(t('notify.approval',{project:P.projectName||'spectoflow'}), step?step.name:''); }
|
|
405
|
+
notifiedApproval=key;
|
|
406
|
+
}
|
|
407
|
+
async function postRunRequest(url,body){
|
|
408
|
+
askNotifyPermission();
|
|
409
|
+
const r=await fetch(withProject(url),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
|
410
|
+
if(!r.ok){ const d=await r.json().catch(()=>({})); showChatError(d.error||t('files.saveError')); if(/needs your OK/.test(d.error||'')) scheduleLoad(); }
|
|
411
|
+
return r.ok;
|
|
412
|
+
}
|
|
379
413
|
function appendRaw(chunk){
|
|
380
414
|
chatContainers().forEach(container=>{
|
|
381
415
|
const st=stateFor(container);
|
|
@@ -413,7 +447,7 @@ async function doRun(promptEl,agentEl){
|
|
|
413
447
|
const agent=agentEl.value;
|
|
414
448
|
const ex=expandForSend(raw);
|
|
415
449
|
const body=ex ? {prompt:ex.prompt, display:ex.display, agent} : {prompt:raw, agent};
|
|
416
|
-
await
|
|
450
|
+
if(!(await postRunRequest('/api/run',body))) return;
|
|
417
451
|
promptEl.value=''; hideCmdMenu(); // the prompt renders as a bubble from the message log
|
|
418
452
|
}
|
|
419
453
|
async function doOrchestrate(promptEl){
|
|
@@ -421,7 +455,7 @@ async function doOrchestrate(promptEl){
|
|
|
421
455
|
promptEl=promptEl||$('#runPrompt');
|
|
422
456
|
const raw=promptEl.value.trim(); if(!raw) return;
|
|
423
457
|
const ex=expandForSend(raw);
|
|
424
|
-
await
|
|
458
|
+
if(!(await postRunRequest('/api/orchestrate',{request: ex?ex.prompt:raw}))) return;
|
|
425
459
|
promptEl.value=''; hideCmdMenu();
|
|
426
460
|
}
|
|
427
461
|
// ---- slash-command autocomplete: a single reused #cmdMenu popover, anchored above whichever chat
|
|
@@ -488,7 +522,7 @@ async function summarizeChat(agentEl){
|
|
|
488
522
|
if(isChatBusy()) return;
|
|
489
523
|
const agent=(agentEl||$('#tabRunAgent'))?.value;
|
|
490
524
|
flash();
|
|
491
|
-
await
|
|
525
|
+
await postRunRequest('/api/chat/summarize',{agent});
|
|
492
526
|
}
|
|
493
527
|
async function clearChat(){
|
|
494
528
|
flash();
|
|
@@ -1060,6 +1094,42 @@ function renderWfSuggest(){
|
|
|
1060
1094
|
const cancel=el('button','btn',t('workflow.dismiss')); cancel.type='button'; cancel.addEventListener('click',closeWfSuggest);
|
|
1061
1095
|
acts.append(applyBtn,cancel); box.append(acts);
|
|
1062
1096
|
}
|
|
1097
|
+
// Personalize → a custom agent command set by config.json, not yet allowed on this machine (D76).
|
|
1098
|
+
function renderUntrustedRunners(){
|
|
1099
|
+
const box=$('#setRunnersTrust'); if(!box) return;
|
|
1100
|
+
const list=(!REMOTE && P && P.untrustedRunners) || [];
|
|
1101
|
+
box.innerHTML=''; box.hidden=!list.length;
|
|
1102
|
+
if(!list.length) return;
|
|
1103
|
+
box.append(el('div','runner-trust-title',t('runners.title')), el('div','runner-trust-hint',t('runners.hint')));
|
|
1104
|
+
list.forEach(r=>{
|
|
1105
|
+
const row=el('div','runner-trust-row');
|
|
1106
|
+
row.append(el('span','runner-trust-agent',r.agent), el('code','runner-trust-cmd',r.command));
|
|
1107
|
+
const b=el('button','btn',t('runners.allow')); b.type='button';
|
|
1108
|
+
b.addEventListener('click',async()=>{ b.disabled=true; flash(); await fetch(withProject('/api/runners/trust'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({agent:r.agent})}); scheduleLoad(); });
|
|
1109
|
+
row.append(b); box.append(row);
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
// The project's framework files are older than the installed spectoflow (D77). Local only.
|
|
1113
|
+
const semverLess=(a,b)=>{ const x=String(a||'').split('.').map(Number), y=String(b||'').split('.').map(Number); for(let i=0;i<3;i++){ if((x[i]||0)!==(y[i]||0)) return (x[i]||0)<(y[i]||0); } return false; };
|
|
1114
|
+
let updateNote='';
|
|
1115
|
+
function renderUpdateBar(){
|
|
1116
|
+
const bar=$('#updateBar'); if(!bar) return;
|
|
1117
|
+
const stale=!REMOTE && P && P.version && P.kitVersion && semverLess(P.version,P.kitVersion);
|
|
1118
|
+
bar.hidden=!stale && !updateNote;
|
|
1119
|
+
$('#updateText').textContent=updateNote || (stale ? t('update.banner',{from:'v'+P.version,to:'v'+P.kitVersion}) : '');
|
|
1120
|
+
$('#updateBtn').hidden=!stale;
|
|
1121
|
+
}
|
|
1122
|
+
async function updateProject(){
|
|
1123
|
+
const b=$('#updateBtn'); b.disabled=true; flash();
|
|
1124
|
+
try{
|
|
1125
|
+
const r=await fetch(withProject('/api/project/update'),{method:'POST'});
|
|
1126
|
+
const d=await r.json().catch(()=>({}));
|
|
1127
|
+
if(!r.ok) throw new Error(d.error||'error');
|
|
1128
|
+
updateNote=t('update.done',{n:d.refreshed})+(d.review&&d.review.length?' '+t('update.review',{n:d.review.length}):'');
|
|
1129
|
+
}catch(e){ updateNote=t('update.error'); }
|
|
1130
|
+
b.disabled=false; scheduleLoad();
|
|
1131
|
+
setTimeout(()=>{ updateNote=''; renderUpdateBar(); },8000);
|
|
1132
|
+
}
|
|
1063
1133
|
function wfDetailRow(k,v){ const r=el('div','wf-detail-row'); r.append(el('span','wf-detail-k',k), el('span','wf-detail-v',v)); return r; }
|
|
1064
1134
|
function wfPopFill(pop, s, idx){
|
|
1065
1135
|
const skill=(P.skills||[]).find(x=>x.name===s.skill);
|
|
@@ -1281,6 +1351,7 @@ function renderSettings(){
|
|
|
1281
1351
|
setLangSelect(c.language||'en');
|
|
1282
1352
|
setAgentSelects();
|
|
1283
1353
|
const wfAuto=$('#setWorkflowAuto'); if(wfAuto) wfAuto.checked=!!c.workflowAutoEnable;
|
|
1354
|
+
renderUntrustedRunners();
|
|
1284
1355
|
// design switcher — options from the DESIGNS registry (designs.js)
|
|
1285
1356
|
const dsel=$('#setDesign');
|
|
1286
1357
|
if(dsel){
|
|
@@ -1475,7 +1546,7 @@ function renderCustomize(){
|
|
|
1475
1546
|
async function czSubmit(kind,description,agent){
|
|
1476
1547
|
const cfg=CZ_KINDS.find((c)=>c.kind===kind);
|
|
1477
1548
|
const prompt=description?cfg.promptAdd(description):cfg.promptAuto;
|
|
1478
|
-
await
|
|
1549
|
+
await postRunRequest('/api/run',{prompt,agent});
|
|
1479
1550
|
const root=$('#czRoot'); if(root) root.dataset.open='';
|
|
1480
1551
|
navigateTab('chat');
|
|
1481
1552
|
}
|
|
@@ -2610,7 +2681,9 @@ $('#blAddCancel').addEventListener('click', closeBacklogAddForm);
|
|
|
2610
2681
|
$('#blAddSubmit').addEventListener('click', submitBacklogAdd);
|
|
2611
2682
|
$('#blAddTitle').addEventListener('keydown', e=>{ if(e.key==='Enter') submitBacklogAdd(); });
|
|
2612
2683
|
// attention tab: add a note + filter chips
|
|
2684
|
+
$$('.chat-stop').forEach(b=>b.addEventListener('click',async()=>{ b.disabled=true; flash(); try{ await fetch(withProject('/api/run/stop'),{method:'POST'}); }finally{ setTimeout(()=>{ b.disabled=false; },800); } }));
|
|
2613
2685
|
$('#wfAnalyzeBtn').addEventListener('click',analyzeWorkflow);
|
|
2686
|
+
$('#updateBtn').addEventListener('click',updateProject);
|
|
2614
2687
|
$('#setWorkflowAuto').addEventListener('change',(e)=>{ if(P&&P.config) P.config.workflowAutoEnable=e.target.checked; saveSetting({workflowAutoEnable:e.target.checked}); });
|
|
2615
2688
|
$('#brainAutoAdd').addEventListener('change',(e)=>brainAct(()=>brainCall('POST','/api/brain/settings',{autoAdd:e.target.checked})));
|
|
2616
2689
|
$('#attnAddBtn').addEventListener('click',()=>{ const t=$('#attnInput'); const v=t.value.trim(); if(v){ addAttn(v); t.value=''; } });
|
|
@@ -90,6 +90,10 @@ en: {
|
|
|
90
90
|
'drawer.agent':'Agent','drawer.skill':'Skill','drawer.file':'File · {rel}',
|
|
91
91
|
'drawer.loading':'Loading…','drawer.loadError':'Could not load this file.','files.title':'Files','files.sub':'Browse the project\'s files — view Markdown & HTML, edit any text file, create new ones.','files.newFile':'+ File','files.newFolder':'+ Folder','files.pickFile':'Select a file to view it.','files.empty':'No files yet.','files.edit':'Edit','files.preview':'Preview','files.save':'Save','files.saved':'✓ saved','files.saveError':'Could not save this file.','files.loadError':'Could not load this file.','files.binary':'This file can\'t be previewed here (not text).','files.discardConfirm':'Discard unsaved changes?','files.newFilePrompt':'New file name (e.g. todo.md):','files.newFolderPrompt':'New folder name:','files.projectRoot':'project root','files.creatingIn':'Creating in: {path}','files.refresh':'Refresh','files.discard':'Discard','files.create':'Create',
|
|
92
92
|
'notes.sub':'A freeform scratchpad for this project — Markdown, autosaved as you type. Only you (and whoever else opens this dashboard) can see it.','notes.saving':'Saving…',
|
|
93
|
+
'notify.done':'{project}: the agent finished','notify.failed':'{project}: the agent stopped with an error','notify.approval':'{project}: a step is waiting for your approval',
|
|
94
|
+
'update.banner':'This project uses spectoflow {from}; {to} is installed.','update.button':'Update the project','update.done':'Project updated: {n} framework file(s) refreshed.','update.review':'{n} file(s) you had edited: the new version is saved next to each as .new.','update.error':'Could not update the project.',
|
|
95
|
+
'chat.stop':'Stop',
|
|
96
|
+
'runners.title':'A custom command needs your OK','runners.hint':'This project’s config.json launches an agent with a command other than its default. It only runs once you allow it on this machine.','runners.allow':'Allow on this machine',
|
|
93
97
|
'workflow.analyze':'Analyze the project','workflow.analyzeTitle':'Look at the project and suggest which steps fit it now','workflow.detected':'Detected: {type} · {phase}','workflow.type.app':'application','workflow.type.infra':'infrastructure','workflow.type.data':'data','workflow.phase.design':'design phase, no code yet','workflow.phase.build':'has code','workflow.matches':'Your workflow already matches the project.','workflow.apply':'Apply','workflow.dismiss':'Close','workflow.applied':'{n} step(s) updated.','workflow.analyzeError':'Could not analyze the project.','workflow.on':'on','workflow.off':'off','workflow.reason.always':'always useful','workflow.reason.design-no-code':'no code yet','workflow.reason.has-code':'the project has code','workflow.reason.has-tests':'the project has tests','workflow.reason.infra-no-tests':'infrastructure project with no tests','workflow.reason.data-quality':'data project — data quality tests','workflow.reason.has-integration-tests':'integration tests exist','workflow.reason.no-integration-tests':'no integration tests yet','workflow.reason.has-e2e-setup':'an end-to-end test setup exists','workflow.reason.no-e2e-setup':'no end-to-end test setup','settings.workflowAuto':'Let the agent enable workflow steps when needed','settings.workflowAutoHint':'Off: it asks you first. It never disables a step on its own.',
|
|
94
98
|
'brain.sub':'What spectoflow has learned about you, shared by all your projects and given to your agent in every session. Add or fix anything.','brain.autoAdd':'Add what the agent learns directly','brain.autoAddOn':'New facts are added right away — you can fix or delete them here.','brain.autoAddOff':'New facts wait in “To confirm” until you accept them.','brain.agents':'Reachable by:','brain.agentWired':'Connected: this agent reads and grows your second brain','brain.agentNotWired':'Not connected yet','brain.setupHint':'Run {cmd} to connect the others.','brain.noAgents':'No coding agent found on this machine.','brain.tooMany':'{n} entries — all of it is given to your agent in every session. Consider removing what is no longer true.','brain.empty':'Nothing yet. As you work, your agent notes durable things about you here — your role, your preferences, how you like to work, what to avoid. You can also add them yourself below.','brain.toConfirm':'To confirm','brain.confirm':'Confirm','brain.confirmAll':'Confirm all','brain.reject':'Reject','brain.cat.profile':'Profile','brain.cat.preferences':'Preferences','brain.cat.workflow':'Working style','brain.cat.avoid':'Avoid','brain.catHint.profile':'Who you are: role, skills, context.','brain.catHint.preferences':'Tools, languages, code style, formats you prefer.','brain.catHint.workflow':'How you like the agent to work with you.','brain.catHint.avoid':'What the agent should never do.','brain.catEmpty':'Nothing here yet.','brain.addPlaceholder':'Add a fact…','brain.duplicate':'Already in your second brain.','brain.byAgent':'learned by the agent','brain.byYou':'added by you','brain.path':'Stored in {path} — never inside a project.','brain.error':'Could not reach your second brain.',
|
|
95
99
|
'meeting.sub':'One dated note per day for this project — write it yourself, or have the active agent draft it from recent tasks and chat activity.','meeting.history':'History','meeting.today':'today','meeting.generate':'Generate','meeting.generateTitle':'Generate today\'s note from recent activity','meeting.overwriteWarn':'This will overwrite today\'s note.','meeting.generateAnyway':'Generate anyway',
|
|
@@ -211,6 +215,10 @@ fr: {
|
|
|
211
215
|
'drawer.agent':'Agent','drawer.skill':'Compétence','drawer.file':'Fichier · {rel}',
|
|
212
216
|
'drawer.loading':'Chargement…','drawer.loadError':'Impossible de charger ce fichier.','files.title':'Fichiers','files.sub':'Parcourez les fichiers du projet — visualisez Markdown et HTML, modifiez tout fichier texte, créez-en de nouveaux.','files.newFile':'+ Fichier','files.newFolder':'+ Dossier','files.pickFile':'Sélectionnez un fichier pour l’afficher.','files.empty':'Aucun fichier pour l’instant.','files.edit':'Modifier','files.preview':'Aperçu','files.save':'Enregistrer','files.saved':'✓ enregistré','files.saveError':'Impossible d’enregistrer ce fichier.','files.loadError':'Impossible de charger ce fichier.','files.binary':'Ce fichier ne peut pas être prévisualisé ici (non textuel).','files.discardConfirm':'Abandonner les modifications non enregistrées ?','files.newFilePrompt':'Nom du nouveau fichier (ex. todo.md) :','files.newFolderPrompt':'Nom du nouveau dossier :','files.projectRoot':'racine du projet','files.creatingIn':'Création dans : {path}','files.refresh':'Actualiser','files.discard':'Annuler','files.create':'Créer',
|
|
213
217
|
'notes.sub':'Un bloc-note libre pour ce projet — Markdown, enregistré automatiquement au fil de la frappe. Visible uniquement par vous (et quiconque ouvre ce tableau de bord).','notes.saving':'Enregistrement…',
|
|
218
|
+
'notify.done':'{project} : l’agent a terminé','notify.failed':'{project} : l’agent s’est arrêté sur une erreur','notify.approval':'{project} : une étape attend votre approbation',
|
|
219
|
+
'update.banner':'Ce projet utilise spectoflow {from} ; {to} est installé.','update.button':'Mettre à jour le projet','update.done':'Projet mis à jour : {n} fichier(s) du framework rafraîchi(s).','update.review':'{n} fichier(s) que vous aviez modifié(s) : la nouvelle version est enregistrée à côté en .new.','update.error':'Impossible de mettre à jour le projet.',
|
|
220
|
+
'chat.stop':'Arrêter',
|
|
221
|
+
'runners.title':'Une commande personnalisée attend votre accord','runners.hint':'Le config.json de ce projet lance un agent avec une commande différente de celle par défaut. Elle ne s’exécute qu’une fois autorisée sur cette machine.','runners.allow':'Autoriser sur cette machine',
|
|
214
222
|
'workflow.analyze':'Analyser le projet','workflow.analyzeTitle':'Examiner le projet et proposer les étapes qui lui conviennent maintenant','workflow.detected':'Détecté : {type} · {phase}','workflow.type.app':'application','workflow.type.infra':'infrastructure','workflow.type.data':'données','workflow.phase.design':'phase de conception, pas encore de code','workflow.phase.build':'contient du code','workflow.matches':'Votre workflow correspond déjà au projet.','workflow.apply':'Appliquer','workflow.dismiss':'Fermer','workflow.applied':'{n} étape(s) mise(s) à jour.','workflow.analyzeError':'Impossible d’analyser le projet.','workflow.on':'activée','workflow.off':'désactivée','workflow.reason.always':'toujours utile','workflow.reason.design-no-code':'pas encore de code','workflow.reason.has-code':'le projet contient du code','workflow.reason.has-tests':'le projet a des tests','workflow.reason.infra-no-tests':'projet d’infrastructure sans tests','workflow.reason.data-quality':'projet de données — tests de qualité des données','workflow.reason.has-integration-tests':'des tests d’intégration existent','workflow.reason.no-integration-tests':'pas encore de tests d’intégration','workflow.reason.has-e2e-setup':'une configuration de tests de bout en bout existe','workflow.reason.no-e2e-setup':'pas de configuration de tests de bout en bout','settings.workflowAuto':'Laisser l’agent activer les étapes du workflow quand il faut','settings.workflowAutoHint':'Désactivé : il vous demande d’abord. Il ne désactive jamais une étape de lui-même.',
|
|
215
223
|
'brain.sub':'Ce que spectoflow a appris sur vous, partagé par tous vos projets et donné à votre agent à chaque session. Ajoutez ou corrigez ce que vous voulez.','brain.autoAdd':'Ajouter directement ce que l’agent apprend','brain.autoAddOn':'Les nouveaux faits sont ajoutés tout de suite — vous pouvez les corriger ou les supprimer ici.','brain.autoAddOff':'Les nouveaux faits attendent dans « À confirmer » jusqu’à votre validation.','brain.agents':'Accessible par :','brain.agentWired':'Connecté : cet agent lit et enrichit votre second cerveau','brain.agentNotWired':'Pas encore connecté','brain.setupHint':'Lancez {cmd} pour connecter les autres.','brain.noAgents':'Aucun agent de code trouvé sur cette machine.','brain.tooMany':'{n} entrées — tout est donné à votre agent à chaque session. Pensez à retirer ce qui n’est plus vrai.','brain.empty':'Rien pour l’instant. Au fil de votre travail, votre agent note ici des choses durables sur vous — votre rôle, vos préférences, votre façon de travailler, ce qu’il faut éviter. Vous pouvez aussi les ajouter vous-même ci-dessous.','brain.toConfirm':'À confirmer','brain.confirm':'Confirmer','brain.confirmAll':'Tout confirmer','brain.reject':'Rejeter','brain.cat.profile':'Profil','brain.cat.preferences':'Préférences','brain.cat.workflow':'Façon de travailler','brain.cat.avoid':'À éviter','brain.catHint.profile':'Qui vous êtes : rôle, compétences, contexte.','brain.catHint.preferences':'Outils, langues, style de code, formats que vous préférez.','brain.catHint.workflow':'Comment vous aimez que l’agent travaille avec vous.','brain.catHint.avoid':'Ce que l’agent ne doit jamais faire.','brain.catEmpty':'Rien ici pour l’instant.','brain.addPlaceholder':'Ajouter un fait…','brain.duplicate':'Déjà dans votre second cerveau.','brain.byAgent':'appris par l’agent','brain.byYou':'ajouté par vous','brain.path':'Stocké dans {path} — jamais dans un projet.','brain.error':'Impossible d’accéder à votre second cerveau.',
|
|
216
224
|
'meeting.sub':'Une note datée par jour pour ce projet — rédigez-la vous-même, ou laissez l’agent actif la rédiger à partir des tâches récentes et du chat.','meeting.history':'Historique','meeting.today':'aujourd’hui','meeting.generate':'Générer','meeting.generateTitle':'Générer la note du jour à partir de l’activité récente','meeting.overwriteWarn':'Cela va écraser la note d’aujourd’hui.','meeting.generateAnyway':'Générer quand même',
|
|
@@ -332,6 +340,10 @@ es: {
|
|
|
332
340
|
'drawer.agent':'Agente','drawer.skill':'Habilidad','drawer.file':'Archivo · {rel}',
|
|
333
341
|
'drawer.loading':'Cargando…','drawer.loadError':'No se pudo cargar este archivo.','files.title':'Archivos','files.sub':'Explora los archivos del proyecto — visualiza Markdown y HTML, edita cualquier archivo de texto, crea otros nuevos.','files.newFile':'+ Archivo','files.newFolder':'+ Carpeta','files.pickFile':'Selecciona un archivo para verlo.','files.empty':'Aún no hay archivos.','files.edit':'Editar','files.preview':'Vista previa','files.save':'Guardar','files.saved':'✓ guardado','files.saveError':'No se pudo guardar este archivo.','files.loadError':'No se pudo cargar este archivo.','files.binary':'Este archivo no se puede previsualizar aquí (no es texto).','files.discardConfirm':'¿Descartar los cambios sin guardar?','files.newFilePrompt':'Nombre del nuevo archivo (p. ej. todo.md):','files.newFolderPrompt':'Nombre de la nueva carpeta:','files.projectRoot':'raíz del proyecto','files.creatingIn':'Creando en: {path}','files.refresh':'Actualizar','files.discard':'Descartar','files.create':'Crear',
|
|
334
342
|
'notes.sub':'Un bloc de notas libre para este proyecto — Markdown, guardado automáticamente mientras escribes. Solo tú (y quien más abra este panel) puedes verlo.','notes.saving':'Guardando…',
|
|
343
|
+
'notify.done':'{project}: el agente ha terminado','notify.failed':'{project}: el agente se detuvo con un error','notify.approval':'{project}: un paso espera tu aprobación',
|
|
344
|
+
'update.banner':'Este proyecto usa spectoflow {from}; está instalado {to}.','update.button':'Actualizar el proyecto','update.done':'Proyecto actualizado: {n} archivo(s) del framework renovado(s).','update.review':'{n} archivo(s) que habías editado: la nueva versión se guarda al lado como .new.','update.error':'No se pudo actualizar el proyecto.',
|
|
345
|
+
'chat.stop':'Detener',
|
|
346
|
+
'runners.title':'Un comando personalizado espera tu aprobación','runners.hint':'El config.json de este proyecto lanza un agente con un comando distinto del predeterminado. Solo se ejecuta cuando lo autorizas en esta máquina.','runners.allow':'Autorizar en esta máquina',
|
|
335
347
|
'workflow.analyze':'Analizar el proyecto','workflow.analyzeTitle':'Examinar el proyecto y proponer los pasos que le convienen ahora','workflow.detected':'Detectado: {type} · {phase}','workflow.type.app':'aplicación','workflow.type.infra':'infraestructura','workflow.type.data':'datos','workflow.phase.design':'fase de diseño, aún sin código','workflow.phase.build':'tiene código','workflow.matches':'Tu workflow ya corresponde al proyecto.','workflow.apply':'Aplicar','workflow.dismiss':'Cerrar','workflow.applied':'{n} paso(s) actualizado(s).','workflow.analyzeError':'No se pudo analizar el proyecto.','workflow.on':'activado','workflow.off':'desactivado','workflow.reason.always':'siempre útil','workflow.reason.design-no-code':'aún no hay código','workflow.reason.has-code':'el proyecto tiene código','workflow.reason.has-tests':'el proyecto tiene tests','workflow.reason.infra-no-tests':'proyecto de infraestructura sin tests','workflow.reason.data-quality':'proyecto de datos — tests de calidad de datos','workflow.reason.has-integration-tests':'existen tests de integración','workflow.reason.no-integration-tests':'aún no hay tests de integración','workflow.reason.has-e2e-setup':'existe una configuración de tests end-to-end','workflow.reason.no-e2e-setup':'no hay configuración de tests end-to-end','settings.workflowAuto':'Dejar que el agente active pasos del workflow cuando haga falta','settings.workflowAutoHint':'Desactivado: te pregunta antes. Nunca desactiva un paso por su cuenta.',
|
|
336
348
|
'brain.sub':'Lo que spectoflow ha aprendido sobre ti, compartido por todos tus proyectos y dado a tu agente en cada sesión. Añade o corrige lo que quieras.','brain.autoAdd':'Añadir directamente lo que aprende el agente','brain.autoAddOn':'Los nuevos datos se añaden al instante — puedes corregirlos o borrarlos aquí.','brain.autoAddOff':'Los nuevos datos esperan en «Por confirmar» hasta que los aceptes.','brain.agents':'Accesible por:','brain.agentWired':'Conectado: este agente lee y amplía tu segundo cerebro','brain.agentNotWired':'Aún no conectado','brain.setupHint':'Ejecuta {cmd} para conectar los demás.','brain.noAgents':'No se encontró ningún agente de código en esta máquina.','brain.tooMany':'{n} entradas — todo se da a tu agente en cada sesión. Plantéate quitar lo que ya no sea cierto.','brain.empty':'Nada todavía. Mientras trabajas, tu agente anota aquí cosas duraderas sobre ti — tu rol, tus preferencias, cómo te gusta trabajar, qué evitar. También puedes añadirlas tú abajo.','brain.toConfirm':'Por confirmar','brain.confirm':'Confirmar','brain.confirmAll':'Confirmar todo','brain.reject':'Rechazar','brain.cat.profile':'Perfil','brain.cat.preferences':'Preferencias','brain.cat.workflow':'Forma de trabajar','brain.cat.avoid':'Evitar','brain.catHint.profile':'Quién eres: rol, habilidades, contexto.','brain.catHint.preferences':'Herramientas, idiomas, estilo de código, formatos que prefieres.','brain.catHint.workflow':'Cómo te gusta que el agente trabaje contigo.','brain.catHint.avoid':'Lo que el agente nunca debe hacer.','brain.catEmpty':'Nada aquí todavía.','brain.addPlaceholder':'Añadir un dato…','brain.duplicate':'Ya está en tu segundo cerebro.','brain.byAgent':'aprendido por el agente','brain.byYou':'añadido por ti','brain.path':'Guardado en {path} — nunca dentro de un proyecto.','brain.error':'No se pudo acceder a tu segundo cerebro.',
|
|
337
349
|
'meeting.sub':'Una nota fechada por día para este proyecto — escríbela tú mismo, o deja que el agente activo la redacte a partir de las tareas y el chat recientes.','meeting.history':'Historial','meeting.today':'hoy','meeting.generate':'Generar','meeting.generateTitle':'Generar la nota de hoy a partir de la actividad reciente','meeting.overwriteWarn':'Esto sobrescribirá la nota de hoy.','meeting.generateAnyway':'Generar de todos modos',
|
|
@@ -453,6 +465,10 @@ de: {
|
|
|
453
465
|
'drawer.agent':'Agent','drawer.skill':'Skill','drawer.file':'Datei · {rel}',
|
|
454
466
|
'drawer.loading':'Lädt…','drawer.loadError':'Diese Datei konnte nicht geladen werden.','files.title':'Dateien','files.sub':'Durchsuche die Projektdateien — Markdown & HTML ansehen, jede Textdatei bearbeiten, neue erstellen.','files.newFile':'+ Datei','files.newFolder':'+ Ordner','files.pickFile':'Wähle eine Datei aus, um sie anzuzeigen.','files.empty':'Noch keine Dateien.','files.edit':'Bearbeiten','files.preview':'Vorschau','files.save':'Speichern','files.saved':'✓ gespeichert','files.saveError':'Diese Datei konnte nicht gespeichert werden.','files.loadError':'Diese Datei konnte nicht geladen werden.','files.binary':'Diese Datei kann hier nicht angezeigt werden (kein Text).','files.discardConfirm':'Nicht gespeicherte Änderungen verwerfen?','files.newFilePrompt':'Name der neuen Datei (z. B. todo.md):','files.newFolderPrompt':'Name des neuen Ordners:','files.projectRoot':'Projektstamm','files.creatingIn':'Erstellen in: {path}','files.refresh':'Aktualisieren','files.discard':'Verwerfen','files.create':'Erstellen',
|
|
455
467
|
'notes.sub':'Ein freier Notizblock für dieses Projekt — Markdown, automatisch gespeichert während der Eingabe. Nur du (und wer sonst dieses Dashboard öffnet) siehst ihn.','notes.saving':'Speichert…',
|
|
468
|
+
'notify.done':'{project}: der Agent ist fertig','notify.failed':'{project}: der Agent hat mit einem Fehler aufgehört','notify.approval':'{project}: ein Schritt wartet auf deine Freigabe',
|
|
469
|
+
'update.banner':'Dieses Projekt nutzt spectoflow {from}; installiert ist {to}.','update.button':'Projekt aktualisieren','update.done':'Projekt aktualisiert: {n} Framework-Datei(en) erneuert.','update.review':'{n} von dir bearbeitete Datei(en): die neue Version liegt jeweils daneben als .new.','update.error':'Das Projekt konnte nicht aktualisiert werden.',
|
|
470
|
+
'chat.stop':'Stoppen',
|
|
471
|
+
'runners.title':'Ein eigener Befehl braucht deine Zustimmung','runners.hint':'Die config.json dieses Projekts startet einen Agenten mit einem anderen als dem Standardbefehl. Er läuft erst, wenn du ihn auf diesem Rechner erlaubst.','runners.allow':'Auf diesem Rechner erlauben',
|
|
456
472
|
'workflow.analyze':'Projekt analysieren','workflow.analyzeTitle':'Das Projekt ansehen und vorschlagen, welche Schritte jetzt passen','workflow.detected':'Erkannt: {type} · {phase}','workflow.type.app':'Anwendung','workflow.type.infra':'Infrastruktur','workflow.type.data':'Daten','workflow.phase.design':'Entwurfsphase, noch kein Code','workflow.phase.build':'enthält Code','workflow.matches':'Dein Workflow passt bereits zum Projekt.','workflow.apply':'Anwenden','workflow.dismiss':'Schließen','workflow.applied':'{n} Schritt(e) aktualisiert.','workflow.analyzeError':'Das Projekt konnte nicht analysiert werden.','workflow.on':'an','workflow.off':'aus','workflow.reason.always':'immer nützlich','workflow.reason.design-no-code':'noch kein Code','workflow.reason.has-code':'das Projekt enthält Code','workflow.reason.has-tests':'das Projekt hat Tests','workflow.reason.infra-no-tests':'Infrastrukturprojekt ohne Tests','workflow.reason.data-quality':'Datenprojekt — Datenqualitätstests','workflow.reason.has-integration-tests':'Integrationstests vorhanden','workflow.reason.no-integration-tests':'noch keine Integrationstests','workflow.reason.has-e2e-setup':'ein End-to-End-Test-Setup ist vorhanden','workflow.reason.no-e2e-setup':'kein End-to-End-Test-Setup','settings.workflowAuto':'Den Agenten Workflow-Schritte bei Bedarf aktivieren lassen','settings.workflowAutoHint':'Aus: er fragt dich vorher. Er deaktiviert nie selbst einen Schritt.',
|
|
457
473
|
'brain.sub':'Was spectoflow über dich gelernt hat — geteilt von all deinen Projekten und deinem Agenten in jeder Sitzung mitgegeben. Ergänze oder korrigiere, was du willst.','brain.autoAdd':'Was der Agent lernt, direkt hinzufügen','brain.autoAddOn':'Neue Fakten werden sofort hinzugefügt — du kannst sie hier korrigieren oder löschen.','brain.autoAddOff':'Neue Fakten warten unter „Zu bestätigen“, bis du sie annimmst.','brain.agents':'Erreichbar für:','brain.agentWired':'Verbunden: dieser Agent liest und erweitert dein zweites Gehirn','brain.agentNotWired':'Noch nicht verbunden','brain.setupHint':'Führe {cmd} aus, um die anderen zu verbinden.','brain.noAgents':'Kein Coding-Agent auf diesem Rechner gefunden.','brain.tooMany':'{n} Einträge — alles wird deinem Agenten in jeder Sitzung mitgegeben. Entferne, was nicht mehr stimmt.','brain.empty':'Noch nichts. Während du arbeitest, notiert dein Agent hier Dauerhaftes über dich — deine Rolle, deine Vorlieben, wie du gern arbeitest, was zu vermeiden ist. Du kannst sie auch unten selbst hinzufügen.','brain.toConfirm':'Zu bestätigen','brain.confirm':'Bestätigen','brain.confirmAll':'Alle bestätigen','brain.reject':'Ablehnen','brain.cat.profile':'Profil','brain.cat.preferences':'Vorlieben','brain.cat.workflow':'Arbeitsweise','brain.cat.avoid':'Vermeiden','brain.catHint.profile':'Wer du bist: Rolle, Fähigkeiten, Kontext.','brain.catHint.preferences':'Werkzeuge, Sprachen, Code-Stil, Formate, die du bevorzugst.','brain.catHint.workflow':'Wie der Agent mit dir arbeiten soll.','brain.catHint.avoid':'Was der Agent nie tun soll.','brain.catEmpty':'Hier ist noch nichts.','brain.addPlaceholder':'Fakt hinzufügen…','brain.duplicate':'Schon in deinem zweiten Gehirn.','brain.byAgent':'vom Agenten gelernt','brain.byYou':'von dir hinzugefügt','brain.path':'Gespeichert in {path} — nie in einem Projekt.','brain.error':'Dein zweites Gehirn ist nicht erreichbar.',
|
|
458
474
|
'meeting.sub':'Eine datierte Notiz pro Tag für dieses Projekt — schreibe sie selbst, oder lass sie vom aktiven Agenten aus den letzten Aufgaben und dem Chat entwerfen.','meeting.history':'Verlauf','meeting.today':'heute','meeting.generate':'Generieren','meeting.generateTitle':'Die heutige Notiz aus der letzten Aktivität generieren','meeting.overwriteWarn':'Dies überschreibt die heutige Notiz.','meeting.generateAnyway':'Trotzdem generieren',
|
|
@@ -574,6 +590,10 @@ pt: {
|
|
|
574
590
|
'drawer.agent':'Agente','drawer.skill':'Habilidade','drawer.file':'Ficheiro · {rel}',
|
|
575
591
|
'drawer.loading':'A carregar…','drawer.loadError':'Não foi possível carregar este ficheiro.','files.title':'Ficheiros','files.sub':'Percorra os ficheiros do projeto — veja Markdown e HTML, edite qualquer ficheiro de texto, crie novos.','files.newFile':'+ Ficheiro','files.newFolder':'+ Pasta','files.pickFile':'Selecione um ficheiro para o ver.','files.empty':'Ainda não há ficheiros.','files.edit':'Editar','files.preview':'Pré-visualizar','files.save':'Guardar','files.saved':'✓ guardado','files.saveError':'Não foi possível guardar este ficheiro.','files.loadError':'Não foi possível carregar este ficheiro.','files.binary':'Este ficheiro não pode ser pré-visualizado aqui (não é texto).','files.discardConfirm':'Descartar alterações não guardadas?','files.newFilePrompt':'Nome do novo ficheiro (ex. todo.md):','files.newFolderPrompt':'Nome da nova pasta:','files.projectRoot':'raiz do projeto','files.creatingIn':'A criar em: {path}','files.refresh':'Atualizar','files.discard':'Descartar','files.create':'Criar',
|
|
576
592
|
'notes.sub':'Um bloco de notas livre para este projeto — Markdown, guardado automaticamente enquanto escreve. Só você (e quem mais abrir este painel) o vê.','notes.saving':'A guardar…',
|
|
593
|
+
'notify.done':'{project}: o agente terminou','notify.failed':'{project}: o agente parou com um erro','notify.approval':'{project}: um passo aguarda a sua aprovação',
|
|
594
|
+
'update.banner':'Este projeto usa spectoflow {from}; está instalado {to}.','update.button':'Atualizar o projeto','update.done':'Projeto atualizado: {n} ficheiro(s) do framework renovado(s).','update.review':'{n} ficheiro(s) que tinha editado: a nova versão fica ao lado como .new.','update.error':'Não foi possível atualizar o projeto.',
|
|
595
|
+
'chat.stop':'Parar',
|
|
596
|
+
'runners.title':'Um comando personalizado aguarda a sua aprovação','runners.hint':'O config.json deste projeto inicia um agente com um comando diferente do predefinido. Só é executado depois de o autorizar nesta máquina.','runners.allow':'Autorizar nesta máquina',
|
|
577
597
|
'workflow.analyze':'Analisar o projeto','workflow.analyzeTitle':'Examinar o projeto e propor os passos que lhe convêm agora','workflow.detected':'Detetado: {type} · {phase}','workflow.type.app':'aplicação','workflow.type.infra':'infraestrutura','workflow.type.data':'dados','workflow.phase.design':'fase de conceção, ainda sem código','workflow.phase.build':'tem código','workflow.matches':'O seu workflow já corresponde ao projeto.','workflow.apply':'Aplicar','workflow.dismiss':'Fechar','workflow.applied':'{n} passo(s) atualizado(s).','workflow.analyzeError':'Não foi possível analisar o projeto.','workflow.on':'ativado','workflow.off':'desativado','workflow.reason.always':'sempre útil','workflow.reason.design-no-code':'ainda sem código','workflow.reason.has-code':'o projeto tem código','workflow.reason.has-tests':'o projeto tem testes','workflow.reason.infra-no-tests':'projeto de infraestrutura sem testes','workflow.reason.data-quality':'projeto de dados — testes de qualidade de dados','workflow.reason.has-integration-tests':'existem testes de integração','workflow.reason.no-integration-tests':'ainda sem testes de integração','workflow.reason.has-e2e-setup':'existe uma configuração de testes ponta a ponta','workflow.reason.no-e2e-setup':'sem configuração de testes ponta a ponta','settings.workflowAuto':'Deixar o agente ativar passos do workflow quando necessário','settings.workflowAutoHint':'Desativado: pergunta-lhe primeiro. Nunca desativa um passo por conta própria.',
|
|
578
598
|
'brain.sub':'O que o spectoflow aprendeu sobre si, partilhado por todos os seus projetos e dado ao seu agente em cada sessão. Acrescente ou corrija o que quiser.','brain.autoAdd':'Adicionar diretamente o que o agente aprende','brain.autoAddOn':'Os novos factos são adicionados de imediato — pode corrigi-los ou apagá-los aqui.','brain.autoAddOff':'Os novos factos esperam em «A confirmar» até os aceitar.','brain.agents':'Acessível por:','brain.agentWired':'Ligado: este agente lê e enriquece o seu segundo cérebro','brain.agentNotWired':'Ainda não ligado','brain.setupHint':'Execute {cmd} para ligar os outros.','brain.noAgents':'Nenhum agente de código encontrado nesta máquina.','brain.tooMany':'{n} entradas — tudo é dado ao seu agente em cada sessão. Pense em retirar o que já não é verdade.','brain.empty':'Ainda nada. À medida que trabalha, o seu agente anota aqui coisas duradouras sobre si — o seu papel, as suas preferências, como gosta de trabalhar, o que evitar. Também as pode adicionar abaixo.','brain.toConfirm':'A confirmar','brain.confirm':'Confirmar','brain.confirmAll':'Confirmar tudo','brain.reject':'Rejeitar','brain.cat.profile':'Perfil','brain.cat.preferences':'Preferências','brain.cat.workflow':'Forma de trabalhar','brain.cat.avoid':'A evitar','brain.catHint.profile':'Quem é: papel, competências, contexto.','brain.catHint.preferences':'Ferramentas, línguas, estilo de código, formatos que prefere.','brain.catHint.workflow':'Como gosta que o agente trabalhe consigo.','brain.catHint.avoid':'O que o agente nunca deve fazer.','brain.catEmpty':'Ainda nada aqui.','brain.addPlaceholder':'Adicionar um facto…','brain.duplicate':'Já está no seu segundo cérebro.','brain.byAgent':'aprendido pelo agente','brain.byYou':'adicionado por si','brain.path':'Guardado em {path} — nunca dentro de um projeto.','brain.error':'Não foi possível aceder ao seu segundo cérebro.',
|
|
579
599
|
'meeting.sub':'Uma nota datada por dia para este projeto — escreva-a você mesmo, ou deixe o agente ativo redigi-la a partir das tarefas e do chat recentes.','meeting.history':'Histórico','meeting.today':'hoje','meeting.generate':'Gerar','meeting.generateTitle':'Gerar a nota de hoje a partir da atividade recente','meeting.overwriteWarn':'Isto vai substituir a nota de hoje.','meeting.generateAnyway':'Gerar mesmo assim',
|
|
@@ -695,6 +715,10 @@ it: {
|
|
|
695
715
|
'drawer.agent':'Agente','drawer.skill':'Skill','drawer.file':'File · {rel}',
|
|
696
716
|
'drawer.loading':'Caricamento…','drawer.loadError':'Impossibile caricare questo file.','files.title':'File','files.sub':'Sfoglia i file del progetto — visualizza Markdown e HTML, modifica qualsiasi file di testo, creane di nuovi.','files.newFile':'+ File','files.newFolder':'+ Cartella','files.pickFile':'Seleziona un file per visualizzarlo.','files.empty':'Nessun file ancora.','files.edit':'Modifica','files.preview':'Anteprima','files.save':'Salva','files.saved':'✓ salvato','files.saveError':'Impossibile salvare questo file.','files.loadError':'Impossibile caricare questo file.','files.binary':'Questo file non può essere visualizzato qui (non è testo).','files.discardConfirm':'Scartare le modifiche non salvate?','files.newFilePrompt':'Nome del nuovo file (es. todo.md):','files.newFolderPrompt':'Nome della nuova cartella:','files.projectRoot':'radice del progetto','files.creatingIn':'Creazione in: {path}','files.refresh':'Aggiorna','files.discard':'Scarta','files.create':'Crea',
|
|
697
717
|
'notes.sub':'Un blocco note libero per questo progetto — Markdown, salvato automaticamente mentre scrivi. Solo tu (e chiunque altro apra questa dashboard) puoi vederlo.','notes.saving':'Salvataggio…',
|
|
718
|
+
'notify.done':'{project}: l’agente ha finito','notify.failed':'{project}: l’agente si è fermato con un errore','notify.approval':'{project}: un passo attende la tua approvazione',
|
|
719
|
+
'update.banner':'Questo progetto usa spectoflow {from}; è installato {to}.','update.button':'Aggiorna il progetto','update.done':'Progetto aggiornato: {n} file del framework rinnovati.','update.review':'{n} file che avevi modificato: la nuova versione è salvata accanto come .new.','update.error':'Impossibile aggiornare il progetto.',
|
|
720
|
+
'chat.stop':'Ferma',
|
|
721
|
+
'runners.title':'Un comando personalizzato attende la tua approvazione','runners.hint':'Il config.json di questo progetto avvia un agente con un comando diverso da quello predefinito. Viene eseguito solo dopo che lo autorizzi su questa macchina.','runners.allow':'Autorizza su questa macchina',
|
|
698
722
|
'workflow.analyze':'Analizza il progetto','workflow.analyzeTitle':'Esaminare il progetto e proporre i passi adatti ora','workflow.detected':'Rilevato: {type} · {phase}','workflow.type.app':'applicazione','workflow.type.infra':'infrastruttura','workflow.type.data':'dati','workflow.phase.design':'fase di progettazione, ancora nessun codice','workflow.phase.build':'contiene codice','workflow.matches':'Il tuo workflow corrisponde già al progetto.','workflow.apply':'Applica','workflow.dismiss':'Chiudi','workflow.applied':'{n} passo/i aggiornato/i.','workflow.analyzeError':'Impossibile analizzare il progetto.','workflow.on':'attivo','workflow.off':'disattivo','workflow.reason.always':'sempre utile','workflow.reason.design-no-code':'ancora nessun codice','workflow.reason.has-code':'il progetto contiene codice','workflow.reason.has-tests':'il progetto ha dei test','workflow.reason.infra-no-tests':'progetto di infrastruttura senza test','workflow.reason.data-quality':'progetto di dati — test di qualità dei dati','workflow.reason.has-integration-tests':'esistono test di integrazione','workflow.reason.no-integration-tests':'ancora nessun test di integrazione','workflow.reason.has-e2e-setup':'esiste una configurazione di test end-to-end','workflow.reason.no-e2e-setup':'nessuna configurazione di test end-to-end','settings.workflowAuto':'Lascia che l’agente attivi i passi del workflow quando serve','settings.workflowAutoHint':'Disattivato: ti chiede prima. Non disattiva mai un passo da solo.',
|
|
699
723
|
'brain.sub':'Ciò che spectoflow ha imparato su di te, condiviso da tutti i tuoi progetti e dato al tuo agente in ogni sessione. Aggiungi o correggi ciò che vuoi.','brain.autoAdd':'Aggiungi direttamente ciò che l’agente impara','brain.autoAddOn':'I nuovi fatti vengono aggiunti subito — puoi correggerli o eliminarli qui.','brain.autoAddOff':'I nuovi fatti attendono in «Da confermare» finché non li accetti.','brain.agents':'Raggiungibile da:','brain.agentWired':'Collegato: questo agente legge e arricchisce il tuo secondo cervello','brain.agentNotWired':'Non ancora collegato','brain.setupHint':'Esegui {cmd} per collegare gli altri.','brain.noAgents':'Nessun agente di programmazione trovato su questa macchina.','brain.tooMany':'{n} voci — tutto viene dato al tuo agente in ogni sessione. Valuta di rimuovere ciò che non è più vero.','brain.empty':'Ancora niente. Mentre lavori, il tuo agente annota qui cose durature su di te — il tuo ruolo, le tue preferenze, come ti piace lavorare, cosa evitare. Puoi anche aggiungerle tu qui sotto.','brain.toConfirm':'Da confermare','brain.confirm':'Conferma','brain.confirmAll':'Conferma tutto','brain.reject':'Rifiuta','brain.cat.profile':'Profilo','brain.cat.preferences':'Preferenze','brain.cat.workflow':'Modo di lavorare','brain.cat.avoid':'Da evitare','brain.catHint.profile':'Chi sei: ruolo, competenze, contesto.','brain.catHint.preferences':'Strumenti, lingue, stile di codice, formati che preferisci.','brain.catHint.workflow':'Come ti piace che l’agente lavori con te.','brain.catHint.avoid':'Ciò che l’agente non deve mai fare.','brain.catEmpty':'Ancora niente qui.','brain.addPlaceholder':'Aggiungi un fatto…','brain.duplicate':'Già nel tuo secondo cervello.','brain.byAgent':'imparato dall’agente','brain.byYou':'aggiunto da te','brain.path':'Salvato in {path} — mai dentro un progetto.','brain.error':'Impossibile raggiungere il tuo secondo cervello.',
|
|
700
724
|
'meeting.sub':'Una nota datata al giorno per questo progetto — scrivila tu stesso, oppure lascia che l’agente attivo la scriva a partire dalle attività e dalla chat recenti.','meeting.history':'Cronologia','meeting.today':'oggi','meeting.generate':'Genera','meeting.generateTitle':'Genera la nota di oggi dall’attività recente','meeting.overwriteWarn':'Questo sovrascriverà la nota di oggi.','meeting.generateAnyway':'Genera comunque',
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
</header>
|
|
80
80
|
|
|
81
81
|
<div class="offline-bar" id="offlineBar" hidden><span class="offline-dot"></span><span id="offlineText"></span></div>
|
|
82
|
+
<div class="update-bar" id="updateBar" hidden><span id="updateText"></span> <button class="btn btn-xs" id="updateBtn" type="button" data-i18n="update.button">Update the project</button></div>
|
|
82
83
|
|
|
83
84
|
<main class="stage">
|
|
84
85
|
<!-- BOARD -->
|
|
@@ -358,7 +359,7 @@
|
|
|
358
359
|
</div>
|
|
359
360
|
</div>
|
|
360
361
|
<p class="chat-warn" data-i18n-html="chat.warn">⚠ Launches a real agent (<code>config.json → runners</code>) that can modify files & run commands.</p>
|
|
361
|
-
<p class="chat-status" id="tabChatStatus" hidden><span class="spinner" aria-hidden="true"></span><span data-i18n="chat.running">Agent running…</span></p>
|
|
362
|
+
<p class="chat-status" id="tabChatStatus" hidden><span class="spinner" aria-hidden="true"></span><span data-i18n="chat.running">Agent running…</span> <button class="btn btn-xs chat-stop" type="button" data-i18n="chat.stop">Stop</button></p>
|
|
362
363
|
</div>
|
|
363
364
|
</section>
|
|
364
365
|
|
|
@@ -393,6 +394,7 @@
|
|
|
393
394
|
<select id="setAgent" class="settings-select"></select>
|
|
394
395
|
<span class="settings-field-hint is-empty" id="setAgentHint" data-i18n="topbar.agent.none" hidden>No agent found</span>
|
|
395
396
|
</label>
|
|
397
|
+
<div class="runner-trust" id="setRunnersTrust" hidden></div>
|
|
396
398
|
<label class="settings-field">
|
|
397
399
|
<span class="settings-field-label" data-i18n="field.mode">Autonomy mode</span>
|
|
398
400
|
<select id="setMode" class="settings-select">
|
|
@@ -498,7 +500,7 @@
|
|
|
498
500
|
<button id="orchBtn" class="btn chat-send" data-i18n-title="chat.orchestrateTitle" data-i18n="action.orchestrate" title="Walk the enabled workflow">Orchestrate</button>
|
|
499
501
|
</div>
|
|
500
502
|
<p class="chat-warn" data-i18n-html="chat.warn">⚠ Launches a real agent (<code>config.json → runners</code>) that can modify files & run commands.</p>
|
|
501
|
-
<p class="chat-status" id="widgetChatStatus" hidden><span class="spinner" aria-hidden="true"></span><span data-i18n="chat.running">Agent running…</span></p>
|
|
503
|
+
<p class="chat-status" id="widgetChatStatus" hidden><span class="spinner" aria-hidden="true"></span><span data-i18n="chat.running">Agent running…</span> <button class="btn btn-xs chat-stop" type="button" data-i18n="chat.stop">Stop</button></p>
|
|
502
504
|
</div>
|
|
503
505
|
|
|
504
506
|
<!-- Slash-command autocomplete popover, reused by both #runPrompt and #tabRunPrompt (app.js) -->
|
|
@@ -659,6 +659,20 @@ body.booting .ring-svg circle:last-of-type { transform-origin:center; animation:
|
|
|
659
659
|
.attn-edit { width:100%; min-height:64px; font-family:inherit; font-size:14px; padding:8px; border:1px solid var(--cool); border-radius:8px; background:var(--surface-2); color:var(--ink); box-sizing:border-box; }
|
|
660
660
|
.attn-actions { display:flex; flex-wrap:wrap; gap:8px; margin-top:11px; }
|
|
661
661
|
|
|
662
|
+
/* ---- Project framework older than the installed spectoflow (D77) ---- */
|
|
663
|
+
.update-bar { display:flex; flex-wrap:wrap; align-items:center; gap:6px 10px; padding:7px 22px; font-size:12.5px; background:color-mix(in srgb,var(--signal) 12%,var(--surface)); border-bottom:1px solid var(--line); color:var(--ink); }
|
|
664
|
+
.update-bar[hidden] { display:none; }
|
|
665
|
+
|
|
666
|
+
/* ---- Custom agent commands awaiting the user's OK (D76) ---- */
|
|
667
|
+
.runner-trust { border:1px solid color-mix(in srgb,var(--s-blocked) 45%,var(--line)); border-radius:var(--radius); padding:10px 12px; display:flex; flex-direction:column; gap:8px; }
|
|
668
|
+
.runner-trust[hidden] { display:none; }
|
|
669
|
+
.runner-trust-title { font-size:13px; font-weight:600; color:var(--s-blocked); }
|
|
670
|
+
.runner-trust-hint { font-size:12px; color:var(--muted); }
|
|
671
|
+
.runner-trust-row { display:flex; flex-wrap:wrap; align-items:center; gap:6px 10px; }
|
|
672
|
+
.runner-trust-agent { font-weight:600; font-size:13px; }
|
|
673
|
+
.runner-trust-cmd { font-family:var(--mono); font-size:11.5px; background:var(--surface-2); border:1px solid var(--line); border-radius:5px; padding:2px 6px; overflow-wrap:anywhere; flex:1 1 180px; }
|
|
674
|
+
.msg.chat-error { color:var(--s-blocked); border:1px solid color-mix(in srgb,var(--s-blocked) 40%,var(--line)); border-radius:8px; padding:8px 10px; font-size:12.5px; white-space:pre-wrap; }
|
|
675
|
+
|
|
662
676
|
/* ---- Workflow → Analyze the project ---- */
|
|
663
677
|
.wf-analyze { display:flex; flex-direction:column; align-items:flex-start; gap:10px; margin:2px 0 16px; }
|
|
664
678
|
.wf-suggest { align-self:stretch; border:1px solid var(--line); border-left:3px solid var(--signal); border-radius:var(--radius); background:var(--surface); padding:12px 14px; display:flex; flex-direction:column; gap:10px; max-width:820px; }
|
package/lib/dashboard/routes.js
CHANGED
|
@@ -23,12 +23,16 @@ const ROUTES = [
|
|
|
23
23
|
['GET', '/api/workflow/suggest', 'workflow.suggest', () => ({})],
|
|
24
24
|
['POST', '/api/workflow/apply', 'workflow.apply', (_u, b) => b],
|
|
25
25
|
['POST', '/api/run', 'run.start', (_u, b) => b],
|
|
26
|
+
['POST', '/api/run/stop', 'run.stop', () => ({})],
|
|
26
27
|
['POST', '/api/chat/summarize', 'chat.summarize', (_u, b) => b],
|
|
27
28
|
['POST', '/api/meeting/generate', 'meeting.generate', (_u, b) => b],
|
|
28
29
|
['POST', '/api/chat/clear', 'chat.clear', () => ({})],
|
|
29
30
|
['POST', '/api/orchestrate', 'orchestrate.start', (_u, b) => b],
|
|
30
31
|
['POST', '/api/orchestrate/approve', 'orchestrate.approve', (_u, b) => b],
|
|
31
32
|
['POST', '/api/settings', 'settings.save', (_u, b) => b],
|
|
33
|
+
// Local only — deliberately absent from server/src/relay.js's OP_PERMISSIONS (D76, D77).
|
|
34
|
+
['POST', '/api/project/update', 'project.update', () => ({})],
|
|
35
|
+
['POST', '/api/runners/trust', 'runners.trust', (_u, b) => b],
|
|
32
36
|
['POST', '/api/attention', 'attention.add', (_u, b) => b],
|
|
33
37
|
['POST', /^\/api\/attention\/[^/]+\/promote$/, 'attention.promote', (_u, _b, p) => ({ id: seg(p, 3) })],
|
|
34
38
|
['PATCH', /^\/api\/attention\/[^/]+$/, 'attention.update', (_u, b, p) => ({ id: seg(p, 3), patch: b })],
|
package/lib/dashboard/runner.js
CHANGED
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Kept separate from the HTTP layer so the pipeline is unit-testable without a server.
|
|
9
9
|
*/
|
|
10
|
+
const path = require('path');
|
|
10
11
|
const { spawn } = require('child_process');
|
|
11
12
|
const store = require('../store');
|
|
12
13
|
const adapters = require('../adapters');
|
|
13
14
|
const detect = require('../detect');
|
|
14
15
|
const brain = require('../brain');
|
|
16
|
+
const runnerTrust = require('../runner-trust');
|
|
15
17
|
|
|
16
18
|
// The command to run `which`: config.json's own runners map first (an explicit user choice always
|
|
17
19
|
// wins), falling back to the registry's default for a known, headless-capable, genuinely-installed
|
|
@@ -24,12 +26,39 @@ function resolveRunnerCommand(root, cfg, which, opts) {
|
|
|
24
26
|
return null;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
// Agent processes in flight, per project — so a stuck one can be stopped from the dashboard. Runs, summaries
|
|
30
|
+
// and meeting notes all register here.
|
|
31
|
+
const inFlight = new Map();
|
|
32
|
+
function trackChild(root, child) {
|
|
33
|
+
const key = path.resolve(root);
|
|
34
|
+
if (!inFlight.has(key)) inFlight.set(key, new Set());
|
|
35
|
+
inFlight.get(key).add(child);
|
|
36
|
+
child.on('close', () => { const set = inFlight.get(key); if (set) set.delete(child); });
|
|
37
|
+
}
|
|
38
|
+
// Stop every agent process running for this project → how many were signalled.
|
|
39
|
+
function stopRuns(root) {
|
|
40
|
+
const set = inFlight.get(path.resolve(root));
|
|
41
|
+
if (!set || !set.size) return 0;
|
|
42
|
+
for (const child of set) { try { child.kill(); } catch (_) {} }
|
|
43
|
+
return set.size;
|
|
44
|
+
}
|
|
45
|
+
// After a crash or restart, runs recorded as running can't be: mark them interrupted.
|
|
46
|
+
function reconcileRunsOnBoot(root) {
|
|
47
|
+
const rt = store.readRuntime(root);
|
|
48
|
+
const stale = (rt.agents || []).filter((a) => a.status === 'running');
|
|
49
|
+
if (!stale.length) return 0;
|
|
50
|
+
const now = new Date().toISOString();
|
|
51
|
+
stale.forEach((a) => { a.status = 'interrupted'; a.endedAt = a.endedAt || now; });
|
|
52
|
+
store.writeRuntime(root, rt);
|
|
53
|
+
return stale.length;
|
|
54
|
+
}
|
|
55
|
+
|
|
27
56
|
function runStart(root, run) {
|
|
28
57
|
const rt = store.readRuntime(root); rt.agents = rt.agents || []; rt.agents.push(run); store.writeRuntime(root, rt);
|
|
29
58
|
}
|
|
30
|
-
function runEnd(root, id, code) {
|
|
59
|
+
function runEnd(root, id, code, signal) {
|
|
31
60
|
const rt = store.readRuntime(root); const a = (rt.agents || []).find((x) => x.id === id);
|
|
32
|
-
if (a) { a.status = code === 0 ? 'done' : 'failed'; a.endedAt = new Date().toISOString(); }
|
|
61
|
+
if (a) { a.status = signal ? 'stopped' : code === 0 ? 'done' : 'failed'; a.endedAt = new Date().toISOString(); }
|
|
33
62
|
store.writeRuntime(root, rt);
|
|
34
63
|
}
|
|
35
64
|
// Buffer a stream into whole lines; flush() emits any trailing partial line at close.
|
|
@@ -80,6 +109,8 @@ function startRun(root, { prompt, agent, logPrompt = true, display, learn = true
|
|
|
80
109
|
const which = agent || cfg.agent || 'claude';
|
|
81
110
|
const cmdStr = resolveRunnerCommand(root, cfg, which);
|
|
82
111
|
if (!cmdStr) return { error: `No runner configured for "${which}".` };
|
|
112
|
+
const blocked = runnerTrust.blockedMessage(root, which, cmdStr);
|
|
113
|
+
if (blocked) return { error: blocked, untrustedRunner: { agent: which, command: cmdStr } };
|
|
83
114
|
const parts = cmdStr.split(/\s+/).filter(Boolean);
|
|
84
115
|
const runId = 'r' + Date.now().toString(36);
|
|
85
116
|
const p = String(prompt).trim();
|
|
@@ -110,6 +141,7 @@ function startRun(root, { prompt, agent, logPrompt = true, display, learn = true
|
|
|
110
141
|
// End the child's stdin immediately: a child that reads stdin (or a Windows pipe that
|
|
111
142
|
// otherwise keeps 'close' from firing) can't stall the run waiting on input that never comes.
|
|
112
143
|
try { child.stdin && child.stdin.end(); } catch {}
|
|
144
|
+
trackChild(root, child);
|
|
113
145
|
|
|
114
146
|
const onLine = (line) => {
|
|
115
147
|
// A learn line is swallowed either way. When recorded, it ALWAYS waits in "To confirm", whatever
|
|
@@ -132,14 +164,14 @@ function startRun(root, { prompt, agent, logPrompt = true, display, learn = true
|
|
|
132
164
|
child.stdout && child.stdout.on('data', (d) => out.feed(d));
|
|
133
165
|
child.stderr && child.stderr.on('data', (d) => err.feed(d));
|
|
134
166
|
child.on('error', (e) => emit({ type: 'run-line', runId, chunk: 'error: ' + e.message + '\n' }));
|
|
135
|
-
child.on('close', (code) => {
|
|
167
|
+
child.on('close', (code, signal) => {
|
|
136
168
|
out.flush(); err.flush();
|
|
137
|
-
runEnd(root, runId, code);
|
|
138
|
-
const sm = store.appendMessage(root, { role: which, kind: 'status', text: `finished (exit ${code})`, agent: which, runId });
|
|
169
|
+
runEnd(root, runId, code, signal);
|
|
170
|
+
const sm = store.appendMessage(root, { role: which, kind: 'status', text: signal ? 'stopped' : `finished (exit ${code})`, agent: which, runId });
|
|
139
171
|
emit({ type: 'message', message: sm });
|
|
140
172
|
emit({ type: 'run-end', runId, code }); emit({ type: 'change' });
|
|
141
173
|
});
|
|
142
174
|
return { runId, child };
|
|
143
175
|
}
|
|
144
176
|
|
|
145
|
-
module.exports = { startRun, resolveRunnerCommand, parseLearnLine };
|
|
177
|
+
module.exports = { startRun, resolveRunnerCommand, parseLearnLine, trackChild, stopRuns, reconcileRunsOnBoot };
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
const { spawn } = require('child_process');
|
|
10
10
|
const store = require('../store');
|
|
11
|
-
const { resolveRunnerCommand } = require('./runner');
|
|
11
|
+
const { resolveRunnerCommand, trackChild } = require('./runner');
|
|
12
|
+
const runnerTrust = require('../runner-trust');
|
|
12
13
|
|
|
13
14
|
const DEFAULT_LIMIT = 40;
|
|
14
15
|
|
|
@@ -26,6 +27,8 @@ function runSummarize(root, { agent } = {}, emit) {
|
|
|
26
27
|
const which = agent || cfg.agent || 'claude';
|
|
27
28
|
const cmdStr = resolveRunnerCommand(root, cfg, which);
|
|
28
29
|
if (!cmdStr) return { error: `No runner configured for "${which}".` };
|
|
30
|
+
const blocked = runnerTrust.blockedMessage(root, which, cmdStr);
|
|
31
|
+
if (blocked) return { error: blocked, untrustedRunner: { agent: which, command: cmdStr } };
|
|
29
32
|
|
|
30
33
|
const rt = store.readRuntime(root);
|
|
31
34
|
const messages = (rt.messages || []).filter((m) => m.kind !== 'summary');
|
|
@@ -52,6 +55,7 @@ function runSummarize(root, { agent } = {}, emit) {
|
|
|
52
55
|
let out = '';
|
|
53
56
|
child.stdout && child.stdout.on('data', (d) => { out += d.toString(); });
|
|
54
57
|
child.stderr && child.stderr.on('data', (d) => { out += d.toString(); });
|
|
58
|
+
trackChild(root, child);
|
|
55
59
|
child.on('close', (code) => {
|
|
56
60
|
const text = out.trim() || (code === 0 ? '(no output)' : `summarize failed (exit ${code})`);
|
|
57
61
|
const summary = {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* A project's config.json can set the command that launches an agent (`runners`). That file is committed
|
|
4
|
+
* and can be written by others — a cloned repository, a teammate's change, a member of the online
|
|
5
|
+
* dashboard, or an agent run told to edit it — so a command that isn't the registry's default for that
|
|
6
|
+
* agent only runs once the user has allowed it on THIS machine. Trust lives outside every project
|
|
7
|
+
* (~/.spectoflow/trusted-runners.json) and is bound to the exact command: change it, and it must be
|
|
8
|
+
* allowed again. Default commands never ask. (D76)
|
|
9
|
+
*/
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const globalConfig = require('./global-config');
|
|
13
|
+
const adapters = require('./adapters');
|
|
14
|
+
|
|
15
|
+
function storePath() { return path.join(globalConfig.homeDir(), 'trusted-runners.json'); }
|
|
16
|
+
function readStore() { try { return JSON.parse(fs.readFileSync(storePath(), 'utf8')) || {}; } catch { return {}; } }
|
|
17
|
+
function projectKey(root) { try { return fs.realpathSync(root); } catch { return path.resolve(root); } }
|
|
18
|
+
|
|
19
|
+
const defaultRunner = (which) => { const a = adapters.knownAgents().find((x) => x.id === which); return a ? a.runner : null; };
|
|
20
|
+
const isDefault = (which, cmd) => !!cmd && cmd === defaultRunner(which);
|
|
21
|
+
|
|
22
|
+
function isTrusted(root, which, cmd) {
|
|
23
|
+
if (!cmd || isDefault(which, cmd)) return true;
|
|
24
|
+
const allowed = readStore()[projectKey(root)];
|
|
25
|
+
return !!(allowed && allowed[which] === cmd);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function trust(root, which, cmd) {
|
|
29
|
+
const store = readStore(), key = projectKey(root);
|
|
30
|
+
store[key] = { ...(store[key] || {}), [which]: cmd };
|
|
31
|
+
fs.mkdirSync(path.dirname(storePath()), { recursive: true });
|
|
32
|
+
fs.writeFileSync(storePath(), JSON.stringify(store, null, 2) + '\n', { mode: 0o600 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The project's runners that would be refused right now → [{ agent, command }].
|
|
36
|
+
function untrusted(root, cfg) {
|
|
37
|
+
return Object.entries((cfg && cfg.runners) || {})
|
|
38
|
+
.filter(([which, cmd]) => typeof cmd === 'string' && !isTrusted(root, which, cmd))
|
|
39
|
+
.map(([agent, command]) => ({ agent, command }));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// null when `cmd` may run; otherwise the message explaining why not and how to allow it.
|
|
43
|
+
function blockedMessage(root, which, cmd) {
|
|
44
|
+
if (isTrusted(root, which, cmd)) return null;
|
|
45
|
+
return `For your safety, this project's custom command for "${which}" needs your OK once on this machine before it runs: ${cmd} — allow it in Personalize → Agent & automation, or run: spectoflow runners allow ${which}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { isTrusted, isDefault, trust, untrusted, blockedMessage, storePath };
|
package/lib/store.js
CHANGED
|
@@ -182,7 +182,8 @@ function addTask(projectRoot, { file, phase, title, owner, level, status } = {})
|
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
function writeAtomic(fp, content) {
|
|
185
|
-
|
|
185
|
+
// Unique temp name: the hub and a CLI command can write the same file at the same moment.
|
|
186
|
+
const tmp = `${fp}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
186
187
|
fs.writeFileSync(tmp, content, 'utf8');
|
|
187
188
|
fs.renameSync(tmp, fp);
|
|
188
189
|
}
|
|
@@ -193,7 +194,10 @@ function readRuntime(projectRoot) {
|
|
|
193
194
|
try { return JSON.parse(fs.readFileSync(runtimePath(projectRoot), 'utf8')); }
|
|
194
195
|
catch { return { agents: [], tests: {}, messages: [], updatedAt: null }; }
|
|
195
196
|
}
|
|
197
|
+
// runtime.json is volatile and re-read on every dashboard refresh: keep it bounded. The newest entries win.
|
|
198
|
+
const RUNTIME_LIMITS = { messages: 500, agents: 100 };
|
|
196
199
|
function writeRuntime(projectRoot, rt) {
|
|
200
|
+
for (const [key, max] of Object.entries(RUNTIME_LIMITS)) if (Array.isArray(rt[key]) && rt[key].length > max) rt[key] = rt[key].slice(-max);
|
|
197
201
|
rt.updatedAt = new Date().toISOString();
|
|
198
202
|
writeAtomic(runtimePath(projectRoot), JSON.stringify(rt, null, 2) + '\n');
|
|
199
203
|
return rt;
|
|
@@ -378,6 +382,6 @@ function readSkills(projectRoot) {
|
|
|
378
382
|
module.exports = {
|
|
379
383
|
parseTaskLine, buildTaskLine, parsePlan, readPlans, readSpecs, updateTaskLine, addTaskComment,
|
|
380
384
|
nextTaskId, addTask,
|
|
381
|
-
readRuntime, writeRuntime, parseAgentLine, appendMessage, readConfig, readWorkflow, readProject,
|
|
385
|
+
readRuntime, writeRuntime, RUNTIME_LIMITS, parseAgentLine, appendMessage, readConfig, readWorkflow, readProject,
|
|
382
386
|
readAgents, readSkills, readCustomDashboards, recordSnapshot, resolvePlansDir, resolveSpecsDir,
|
|
383
387
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spectoflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
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",
|