spectoflow 0.31.1 → 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 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
package/bin/spectoflow.js CHANGED
@@ -517,6 +517,11 @@ async function startDashboard() {
517
517
  const boardUrl = (p) => (entry ? `http://localhost:${p}/p/${entry.id}/board` : `http://localhost:${p}/`);
518
518
  const info = workspace.readLock();
519
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
+ }
520
525
  console.log(`${c.g('●')} hub already running → ${c.bold(boardUrl(info.port))}`);
521
526
  await printOnlineLine(info.port, true);
522
527
  return printDashboardCommands();
@@ -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); }));
@@ -10,7 +10,7 @@
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
14
  const runnerTrust = require('../runner-trust');
15
15
  const { formatLog } = require('./summarize'); // reused as-is rather than reimplemented — see report
16
16
 
@@ -106,6 +106,7 @@ function runMeetingGenerate(root, { agent, date } = {}, emit) {
106
106
  let out = '';
107
107
  child.stdout && child.stdout.on('data', (d) => { out += d.toString(); });
108
108
  child.stderr && child.stderr.on('data', (d) => { out += d.toString(); });
109
+ trackChild(root, child);
109
110
  child.on('close', (code) => {
110
111
  const text = out.trim() || (code === 0 ? '(no output)' : `meeting generate failed (exit ${code})`);
111
112
  files.writeFile(root, meetingPath(day), text);
@@ -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');
@@ -165,6 +165,7 @@ const ops = {
165
165
  // for which .spectoflow/meetings/<date>.md "today" resolves to.
166
166
  p.todayDate = todayLocal();
167
167
  p.untrustedRunners = runnerTrust.untrusted(root, p.config);
168
+ p.kitVersion = PKG_VERSION;
168
169
  return p;
169
170
  },
170
171
  'agentfile.read': async (root, { path: rel }) => readAgentFile(root, rel),
@@ -234,6 +235,14 @@ const ops = {
234
235
  if (r.error) bad(r.error);
235
236
  return { runId: r.runId };
236
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) }),
237
246
  'chat.summarize': async (root, { agent }, ctx) => {
238
247
  const r = runSummarize(root, { agent }, ctx.emit);
239
248
  if (r.error) bad(r.error);
@@ -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'; };
@@ -386,7 +386,26 @@ function showChatError(message){
386
386
  scrollChat(container);
387
387
  });
388
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
+ }
389
407
  async function postRunRequest(url,body){
408
+ askNotifyPermission();
390
409
  const r=await fetch(withProject(url),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
391
410
  if(!r.ok){ const d=await r.json().catch(()=>({})); showChatError(d.error||t('files.saveError')); if(/needs your OK/.test(d.error||'')) scheduleLoad(); }
392
411
  return r.ok;
@@ -1090,6 +1109,27 @@ function renderUntrustedRunners(){
1090
1109
  row.append(b); box.append(row);
1091
1110
  });
1092
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
+ }
1093
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; }
1094
1134
  function wfPopFill(pop, s, idx){
1095
1135
  const skill=(P.skills||[]).find(x=>x.name===s.skill);
@@ -2641,7 +2681,9 @@ $('#blAddCancel').addEventListener('click', closeBacklogAddForm);
2641
2681
  $('#blAddSubmit').addEventListener('click', submitBacklogAdd);
2642
2682
  $('#blAddTitle').addEventListener('keydown', e=>{ if(e.key==='Enter') submitBacklogAdd(); });
2643
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); } }));
2644
2685
  $('#wfAnalyzeBtn').addEventListener('click',analyzeWorkflow);
2686
+ $('#updateBtn').addEventListener('click',updateProject);
2645
2687
  $('#setWorkflowAuto').addEventListener('change',(e)=>{ if(P&&P.config) P.config.workflowAutoEnable=e.target.checked; saveSetting({workflowAutoEnable:e.target.checked}); });
2646
2688
  $('#brainAutoAdd').addEventListener('change',(e)=>brainAct(()=>brainCall('POST','/api/brain/settings',{autoAdd:e.target.checked})));
2647
2689
  $('#attnAddBtn').addEventListener('click',()=>{ const t=$('#attnInput'); const v=t.value.trim(); if(v){ addAttn(v); t.value=''; } });
@@ -90,6 +90,9 @@ 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',
93
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',
94
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.',
95
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.',
@@ -212,6 +215,9 @@ fr: {
212
215
  'drawer.agent':'Agent','drawer.skill':'Compétence','drawer.file':'Fichier · {rel}',
213
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',
214
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',
215
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',
216
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.',
217
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.',
@@ -334,6 +340,9 @@ es: {
334
340
  'drawer.agent':'Agente','drawer.skill':'Habilidad','drawer.file':'Archivo · {rel}',
335
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',
336
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',
337
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',
338
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.',
339
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.',
@@ -456,6 +465,9 @@ de: {
456
465
  'drawer.agent':'Agent','drawer.skill':'Skill','drawer.file':'Datei · {rel}',
457
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',
458
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',
459
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',
460
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.',
461
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.',
@@ -578,6 +590,9 @@ pt: {
578
590
  'drawer.agent':'Agente','drawer.skill':'Habilidade','drawer.file':'Ficheiro · {rel}',
579
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',
580
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',
581
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',
582
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.',
583
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.',
@@ -700,6 +715,9 @@ it: {
700
715
  'drawer.agent':'Agente','drawer.skill':'Skill','drawer.file':'File · {rel}',
701
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',
702
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',
703
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',
704
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.',
705
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.',
@@ -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 &amp; 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
 
@@ -499,7 +500,7 @@
499
500
  <button id="orchBtn" class="btn chat-send" data-i18n-title="chat.orchestrateTitle" data-i18n="action.orchestrate" title="Walk the enabled workflow">Orchestrate</button>
500
501
  </div>
501
502
  <p class="chat-warn" data-i18n-html="chat.warn">⚠ Launches a real agent (<code>config.json → runners</code>) that can modify files &amp; run commands.</p>
502
- <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>
503
504
  </div>
504
505
 
505
506
  <!-- Slash-command autocomplete popover, reused by both #runPrompt and #tabRunPrompt (app.js) -->
@@ -659,6 +659,10 @@ 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
+
662
666
  /* ---- Custom agent commands awaiting the user's OK (D76) ---- */
663
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; }
664
668
  .runner-trust[hidden] { display:none; }
@@ -23,13 +23,15 @@ 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],
32
- // Local only — deliberately absent from server/src/relay.js's OP_PERMISSIONS (D76).
33
+ // Local only — deliberately absent from server/src/relay.js's OP_PERMISSIONS (D76, D77).
34
+ ['POST', '/api/project/update', 'project.update', () => ({})],
33
35
  ['POST', '/api/runners/trust', 'runners.trust', (_u, b) => b],
34
36
  ['POST', '/api/attention', 'attention.add', (_u, b) => b],
35
37
  ['POST', /^\/api\/attention\/[^/]+\/promote$/, 'attention.promote', (_u, _b, p) => ({ id: seg(p, 3) })],
@@ -7,6 +7,7 @@
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');
@@ -25,12 +26,39 @@ function resolveRunnerCommand(root, cfg, which, opts) {
25
26
  return null;
26
27
  }
27
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
+
28
56
  function runStart(root, run) {
29
57
  const rt = store.readRuntime(root); rt.agents = rt.agents || []; rt.agents.push(run); store.writeRuntime(root, rt);
30
58
  }
31
- function runEnd(root, id, code) {
59
+ function runEnd(root, id, code, signal) {
32
60
  const rt = store.readRuntime(root); const a = (rt.agents || []).find((x) => x.id === id);
33
- 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(); }
34
62
  store.writeRuntime(root, rt);
35
63
  }
36
64
  // Buffer a stream into whole lines; flush() emits any trailing partial line at close.
@@ -113,6 +141,7 @@ function startRun(root, { prompt, agent, logPrompt = true, display, learn = true
113
141
  // End the child's stdin immediately: a child that reads stdin (or a Windows pipe that
114
142
  // otherwise keeps 'close' from firing) can't stall the run waiting on input that never comes.
115
143
  try { child.stdin && child.stdin.end(); } catch {}
144
+ trackChild(root, child);
116
145
 
117
146
  const onLine = (line) => {
118
147
  // A learn line is swallowed either way. When recorded, it ALWAYS waits in "To confirm", whatever
@@ -135,14 +164,14 @@ function startRun(root, { prompt, agent, logPrompt = true, display, learn = true
135
164
  child.stdout && child.stdout.on('data', (d) => out.feed(d));
136
165
  child.stderr && child.stderr.on('data', (d) => err.feed(d));
137
166
  child.on('error', (e) => emit({ type: 'run-line', runId, chunk: 'error: ' + e.message + '\n' }));
138
- child.on('close', (code) => {
167
+ child.on('close', (code, signal) => {
139
168
  out.flush(); err.flush();
140
- runEnd(root, runId, code);
141
- 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 });
142
171
  emit({ type: 'message', message: sm });
143
172
  emit({ type: 'run-end', runId, code }); emit({ type: 'change' });
144
173
  });
145
174
  return { runId, child };
146
175
  }
147
176
 
148
- module.exports = { startRun, resolveRunnerCommand, parseLearnLine };
177
+ module.exports = { startRun, resolveRunnerCommand, parseLearnLine, trackChild, stopRuns, reconcileRunsOnBoot };
@@ -8,7 +8,7 @@
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
12
  const runnerTrust = require('../runner-trust');
13
13
 
14
14
  const DEFAULT_LIMIT = 40;
@@ -55,6 +55,7 @@ function runSummarize(root, { agent } = {}, emit) {
55
55
  let out = '';
56
56
  child.stdout && child.stdout.on('data', (d) => { out += d.toString(); });
57
57
  child.stderr && child.stderr.on('data', (d) => { out += d.toString(); });
58
+ trackChild(root, child);
58
59
  child.on('close', (code) => {
59
60
  const text = out.trim() || (code === 0 ? '(no output)' : `summarize failed (exit ${code})`);
60
61
  const summary = {
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
- const tmp = fp + '.tmp';
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.31.1",
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",