spectoflow 0.31.0 → 0.31.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 CHANGED
@@ -205,6 +205,11 @@ that another website sent — a malicious page, a DNS-rebinding trick, or a tunn
205
205
  machine can't drive it. Clicking a link to it from another site still opens it. To reach your projects from
206
206
  another device, use the online dashboard ([below](#going-online-optional-local-hub-vs-relay-server)).
207
207
 
208
+ **A custom agent command needs your OK.** `config.json → runners` sets the command that launches each agent,
209
+ and that file is committed — it can come from a cloned repository or a teammate's change. A command other than
210
+ the agent's default only runs once you allow it on this machine: Personalize → *Agent & automation* → **Allow
211
+ on this machine**, or `spectoflow runners allow <agent>`. Change the command and it asks again.
212
+
208
213
  ### One hub, every project
209
214
 
210
215
  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');
@@ -728,6 +752,7 @@ const HELP = {
728
752
  ${c.g('brain setup')} register the MCP server in each installed agent's USER-level config (once
729
753
  per machine; never touches an existing entry; Goose gets a snippet to paste)
730
754
  Learned facts are added directly by default: ${c.g('spectoflow config set brain.autoAdd false')} to confirm them first.`,
755
+ 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
756
  stop: `${c.bold('spectoflow stop')}\n Stop the running dashboard (alias for ${c.g('spectoflow dashboard stop')}).`,
732
757
  config: `${c.bold('spectoflow config')} ${c.dim('[get <key> | set <key> <value>]')}\n
733
758
  Global settings that apply to every project on this machine, stored in ${c.dim('~/.spectoflow/config.json')}:
@@ -745,6 +770,7 @@ const fns = {
745
770
  config: configCmd,
746
771
  mcp: () => require('../lib/mcp-server').serve({ version: VERSION }),
747
772
  brain: brainCmd,
773
+ runners: runnersCmd,
748
774
  agents: () => { console.log(wordmark()); printAgents(false); },
749
775
  skills: () => { console.log(wordmark()); printSkills(false); },
750
776
  workflow: workflowCmd,
@@ -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 };
@@ -11,6 +11,7 @@ const { spawn } = require('child_process');
11
11
  const store = require('../store');
12
12
  const files = require('./files');
13
13
  const { resolveRunnerCommand } = 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.' };
@@ -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,25 @@ 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);
166
168
  return p;
167
169
  },
168
170
  'agentfile.read': async (root, { path: rel }) => readAgentFile(root, rel),
169
171
 
170
172
  'files.tree': async (root) => ({ tree: files.tree(root) }),
171
173
  '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))),
174
+ 'files.write': async (root, { path: rel, content }, ctx) => changed(ctx, filesResult(files.writeFile(root, rel, content, { remote: !!ctx.remote }))),
175
+ 'files.mkdir': async (root, { path: rel }, ctx) => changed(ctx, filesResult(files.mkdir(root, rel, { remote: !!ctx.remote }))),
176
+
177
+ // Allow, on this machine, the custom command config.json sets for one agent (D76). Local only: the relay
178
+ // doesn't list this op, and ctx.remote is refused here too.
179
+ 'runners.trust': async (root, { agent }, ctx) => {
180
+ if (ctx && ctx.remote) throw new OpError(404, 'Unknown operation.');
181
+ const cmd = ((store.readConfig(root).runners) || {})[agent];
182
+ if (typeof cmd !== 'string' || !cmd.trim()) notFound(`No custom command set for "${agent}".`);
183
+ runnerTrust.trust(root, agent, cmd);
184
+ return changed(ctx, { ok: true, agent, command: cmd });
185
+ },
174
186
 
175
187
  'task.add': async (root, { title, phase, file, owner, level }, ctx) => {
176
188
  const t = store.addTask(root, { title: text(title, 'A title is required.'), phase, file, owner, level });
@@ -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) { emit({ type: 'message', message: { role: 'orchestrator', kind: 'status', text: r.error } }); resolve(1); }
93
+ if (r.error) { post(root, 'orchestrator', 'status', r.error, emit); resolve(1); }
94
94
  });
95
95
  }
96
96
 
@@ -376,6 +376,21 @@ 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
+ async function postRunRequest(url,body){
390
+ const r=await fetch(withProject(url),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
391
+ if(!r.ok){ const d=await r.json().catch(()=>({})); showChatError(d.error||t('files.saveError')); if(/needs your OK/.test(d.error||'')) scheduleLoad(); }
392
+ return r.ok;
393
+ }
379
394
  function appendRaw(chunk){
380
395
  chatContainers().forEach(container=>{
381
396
  const st=stateFor(container);
@@ -413,7 +428,7 @@ async function doRun(promptEl,agentEl){
413
428
  const agent=agentEl.value;
414
429
  const ex=expandForSend(raw);
415
430
  const body=ex ? {prompt:ex.prompt, display:ex.display, agent} : {prompt:raw, agent};
416
- await fetch(withProject('/api/run'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
431
+ if(!(await postRunRequest('/api/run',body))) return;
417
432
  promptEl.value=''; hideCmdMenu(); // the prompt renders as a bubble from the message log
418
433
  }
419
434
  async function doOrchestrate(promptEl){
@@ -421,7 +436,7 @@ async function doOrchestrate(promptEl){
421
436
  promptEl=promptEl||$('#runPrompt');
422
437
  const raw=promptEl.value.trim(); if(!raw) return;
423
438
  const ex=expandForSend(raw);
424
- await fetch(withProject('/api/orchestrate'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({request: ex?ex.prompt:raw})});
439
+ if(!(await postRunRequest('/api/orchestrate',{request: ex?ex.prompt:raw}))) return;
425
440
  promptEl.value=''; hideCmdMenu();
426
441
  }
427
442
  // ---- slash-command autocomplete: a single reused #cmdMenu popover, anchored above whichever chat
@@ -488,7 +503,7 @@ async function summarizeChat(agentEl){
488
503
  if(isChatBusy()) return;
489
504
  const agent=(agentEl||$('#tabRunAgent'))?.value;
490
505
  flash();
491
- await fetch(withProject('/api/chat/summarize'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({agent})});
506
+ await postRunRequest('/api/chat/summarize',{agent});
492
507
  }
493
508
  async function clearChat(){
494
509
  flash();
@@ -1060,6 +1075,21 @@ function renderWfSuggest(){
1060
1075
  const cancel=el('button','btn',t('workflow.dismiss')); cancel.type='button'; cancel.addEventListener('click',closeWfSuggest);
1061
1076
  acts.append(applyBtn,cancel); box.append(acts);
1062
1077
  }
1078
+ // Personalize → a custom agent command set by config.json, not yet allowed on this machine (D76).
1079
+ function renderUntrustedRunners(){
1080
+ const box=$('#setRunnersTrust'); if(!box) return;
1081
+ const list=(!REMOTE && P && P.untrustedRunners) || [];
1082
+ box.innerHTML=''; box.hidden=!list.length;
1083
+ if(!list.length) return;
1084
+ box.append(el('div','runner-trust-title',t('runners.title')), el('div','runner-trust-hint',t('runners.hint')));
1085
+ list.forEach(r=>{
1086
+ const row=el('div','runner-trust-row');
1087
+ row.append(el('span','runner-trust-agent',r.agent), el('code','runner-trust-cmd',r.command));
1088
+ const b=el('button','btn',t('runners.allow')); b.type='button';
1089
+ 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(); });
1090
+ row.append(b); box.append(row);
1091
+ });
1092
+ }
1063
1093
  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
1094
  function wfPopFill(pop, s, idx){
1065
1095
  const skill=(P.skills||[]).find(x=>x.name===s.skill);
@@ -1281,6 +1311,7 @@ function renderSettings(){
1281
1311
  setLangSelect(c.language||'en');
1282
1312
  setAgentSelects();
1283
1313
  const wfAuto=$('#setWorkflowAuto'); if(wfAuto) wfAuto.checked=!!c.workflowAutoEnable;
1314
+ renderUntrustedRunners();
1284
1315
  // design switcher — options from the DESIGNS registry (designs.js)
1285
1316
  const dsel=$('#setDesign');
1286
1317
  if(dsel){
@@ -1475,7 +1506,7 @@ function renderCustomize(){
1475
1506
  async function czSubmit(kind,description,agent){
1476
1507
  const cfg=CZ_KINDS.find((c)=>c.kind===kind);
1477
1508
  const prompt=description?cfg.promptAdd(description):cfg.promptAuto;
1478
- await fetch(withProject('/api/run'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({prompt,agent})});
1509
+ await postRunRequest('/api/run',{prompt,agent});
1479
1510
  const root=$('#czRoot'); if(root) root.dataset.open='';
1480
1511
  navigateTab('chat');
1481
1512
  }
@@ -90,6 +90,7 @@ 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
+ '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
94
  '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
95
  '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
96
  '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 +212,7 @@ fr: {
211
212
  'drawer.agent':'Agent','drawer.skill':'Compétence','drawer.file':'Fichier · {rel}',
212
213
  '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
214
  '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…',
215
+ '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
216
  '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
217
  '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
218
  '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 +334,7 @@ es: {
332
334
  'drawer.agent':'Agente','drawer.skill':'Habilidad','drawer.file':'Archivo · {rel}',
333
335
  '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
336
  '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…',
337
+ '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
338
  '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
339
  '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
340
  '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 +456,7 @@ de: {
453
456
  'drawer.agent':'Agent','drawer.skill':'Skill','drawer.file':'Datei · {rel}',
454
457
  '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
458
  '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…',
459
+ '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
460
  '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
461
  '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
462
  '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 +578,7 @@ pt: {
574
578
  'drawer.agent':'Agente','drawer.skill':'Habilidade','drawer.file':'Ficheiro · {rel}',
575
579
  '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
580
  '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…',
581
+ '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
582
  '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
583
  '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
584
  '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 +700,7 @@ it: {
695
700
  'drawer.agent':'Agente','drawer.skill':'Skill','drawer.file':'File · {rel}',
696
701
  '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
702
  '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…',
703
+ '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
704
  '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
705
  '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
706
  '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',
@@ -393,6 +393,7 @@
393
393
  <select id="setAgent" class="settings-select"></select>
394
394
  <span class="settings-field-hint is-empty" id="setAgentHint" data-i18n="topbar.agent.none" hidden>No agent found</span>
395
395
  </label>
396
+ <div class="runner-trust" id="setRunnersTrust" hidden></div>
396
397
  <label class="settings-field">
397
398
  <span class="settings-field-label" data-i18n="field.mode">Autonomy mode</span>
398
399
  <select id="setMode" class="settings-select">
@@ -659,6 +659,16 @@ 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
+ /* ---- Custom agent commands awaiting the user's OK (D76) ---- */
663
+ .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; }
664
+ .runner-trust[hidden] { display:none; }
665
+ .runner-trust-title { font-size:13px; font-weight:600; color:var(--s-blocked); }
666
+ .runner-trust-hint { font-size:12px; color:var(--muted); }
667
+ .runner-trust-row { display:flex; flex-wrap:wrap; align-items:center; gap:6px 10px; }
668
+ .runner-trust-agent { font-weight:600; font-size:13px; }
669
+ .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; }
670
+ .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; }
671
+
662
672
  /* ---- Workflow → Analyze the project ---- */
663
673
  .wf-analyze { display:flex; flex-direction:column; align-items:flex-start; gap:10px; margin:2px 0 16px; }
664
674
  .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; }
@@ -29,6 +29,8 @@ const ROUTES = [
29
29
  ['POST', '/api/orchestrate', 'orchestrate.start', (_u, b) => b],
30
30
  ['POST', '/api/orchestrate/approve', 'orchestrate.approve', (_u, b) => b],
31
31
  ['POST', '/api/settings', 'settings.save', (_u, b) => b],
32
+ // Local only — deliberately absent from server/src/relay.js's OP_PERMISSIONS (D76).
33
+ ['POST', '/api/runners/trust', 'runners.trust', (_u, b) => b],
32
34
  ['POST', '/api/attention', 'attention.add', (_u, b) => b],
33
35
  ['POST', /^\/api\/attention\/[^/]+\/promote$/, 'attention.promote', (_u, _b, p) => ({ id: seg(p, 3) })],
34
36
  ['PATCH', /^\/api\/attention\/[^/]+$/, 'attention.update', (_u, b, p) => ({ id: seg(p, 3), patch: b })],
@@ -12,6 +12,7 @@ const store = require('../store');
12
12
  const adapters = require('../adapters');
13
13
  const detect = require('../detect');
14
14
  const brain = require('../brain');
15
+ const runnerTrust = require('../runner-trust');
15
16
 
16
17
  // The command to run `which`: config.json's own runners map first (an explicit user choice always
17
18
  // wins), falling back to the registry's default for a known, headless-capable, genuinely-installed
@@ -80,6 +81,8 @@ function startRun(root, { prompt, agent, logPrompt = true, display, learn = true
80
81
  const which = agent || cfg.agent || 'claude';
81
82
  const cmdStr = resolveRunnerCommand(root, cfg, which);
82
83
  if (!cmdStr) return { error: `No runner configured for "${which}".` };
84
+ const blocked = runnerTrust.blockedMessage(root, which, cmdStr);
85
+ if (blocked) return { error: blocked, untrustedRunner: { agent: which, command: cmdStr } };
83
86
  const parts = cmdStr.split(/\s+/).filter(Boolean);
84
87
  const runId = 'r' + Date.now().toString(36);
85
88
  const p = String(prompt).trim();
@@ -9,6 +9,7 @@
9
9
  const { spawn } = require('child_process');
10
10
  const store = require('../store');
11
11
  const { resolveRunnerCommand } = 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');
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectoflow",
3
- "version": "0.31.0",
3
+ "version": "0.31.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",