spectoflow 0.27.2 → 0.29.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.
@@ -13,14 +13,14 @@ let filter = { status: 'all', q: '' }; // board filter state — client-side onl
13
13
  // old localStorage-absent fallback used, then is reconciled from P.config the first time it loads
14
14
  // (see syncSettingsFromServer(), called once from load()) — after that they're local state mutated
15
15
  // only by user actions, exactly like the old localStorage-backed vars were.
16
- // The 13 native tab ids, mirroring ops.js's NATIVE_TABS exactly — the client's own source of truth
16
+ // The 14 native tab ids, mirroring ops.js's NATIVE_TABS exactly — the client's own source of truth
17
17
  // for "what's a real native tab id" (used to validate P.config.navTabs before trusting it, same
18
18
  // defensive stance as ROUTES further down). Task 1 shipped the original 11, all default-enabled; Task
19
19
  // 2 (Sous-projet C) registered 'notes' — the Bloc note scratchpad — as the first tab that must start
20
20
  // OFF by default (an opt-in feature, per the user's explicit request); Task 3 registers 'meeting' —
21
21
  // the Daily meeting tab — the same way, so DEFAULT_OFF_TABS exists rather than a blanket "every
22
22
  // native tab defaults to enabled" the way Task 1 assumed.
23
- const NATIVE_TABS = ['board', 'chat', 'requests', 'attention', 'backlog', 'workflow', 'team', 'files', 'notes', 'meeting', 'info', 'docs', 'personalize'];
23
+ const NATIVE_TABS = ['board', 'chat', 'requests', 'attention', 'backlog', 'workflow', 'team', 'files', 'notes', 'meeting', 'info', 'docs', 'brain', 'personalize'];
24
24
  const DEFAULT_OFF_TABS = new Set(['notes', 'meeting']);
25
25
  function defaultNavTabs() { return NATIVE_TABS.map((id) => ({ id, enabled: !DEFAULT_OFF_TABS.has(id) })); }
26
26
  let boardView = 'list'; // 'list' | 'kanban'
@@ -72,7 +72,11 @@ window.fetch=(url,opts)=>{
72
72
  if(OFFLINE&&method!=='GET'&&String(url).startsWith('/api/')) return Promise.resolve(new Response(JSON.stringify({error:t('offline.readonly')}),{status:503,headers:{'Content-Type':'application/json'}}));
73
73
  return _fetch(url,opts);
74
74
  };
75
+ // Served by the online relay? Its project.read always carries `online`; the local hub's never does.
76
+ // Online, the Second brain tab is hidden: the brain is the machine owner's, never a project's.
77
+ let REMOTE=false;
75
78
  function setOffline(p){
79
+ REMOTE=typeof p.online==='boolean';
76
80
  OFFLINE=p.online===false;
77
81
  document.body.classList.toggle('is-offline',OFFLINE);
78
82
  const bar=$('#offlineBar'); if(!bar) return;
@@ -157,14 +161,14 @@ function applyNavTabs() {
157
161
  list.forEach((entry) => {
158
162
  const btn = nav.querySelector('.tab[data-tab="' + entry.id + '"]');
159
163
  if (!btn) return;
160
- btn.hidden = !entry.enabled;
164
+ btn.hidden = !entry.enabled || (entry.id === 'brain' && REMOTE);
161
165
  nav.insertBefore(btn, anchor);
162
166
  });
163
167
  // If the currently active NATIVE tab just became disabled (e.g. the user disabled the tab they're
164
168
  // viewing, or another viewer did and this tab just reloaded), navigate to a sensible fallback
165
169
  // instead of leaving a hidden/disabled panel showing.
166
170
  const activeEntry = list.find((e) => e.id === activeTab);
167
- if (activeEntry && !activeEntry.enabled) {
171
+ if (activeEntry && (!activeEntry.enabled || (activeEntry.id === 'brain' && REMOTE))) {
168
172
  const fallback = list.find((e) => e.id === 'board' && e.enabled) || list.find((e) => e.enabled);
169
173
  if (fallback) navigateTab(fallback.id);
170
174
  }
@@ -295,7 +299,9 @@ const runtimeTests=(id)=> (P.runtime&&P.runtime.tests&&P.runtime.tests[id])||nul
295
299
  async function load(){
296
300
  const r = await fetch(withProject('/api/project')); P = await r.json();
297
301
  syncSettingsFromServer();
302
+ REMOTE=typeof P.online==='boolean'; // known before the first paint: the brain tab is hidden online
298
303
  render(); setOffline(P);
304
+ if(!REMOTE && !brainData) loadBrain();
299
305
  if(openTaskId) openDrawer(openTaskId,true);
300
306
  }
301
307
  // Coalesce bursts of SSE 'change'/'message' events into one reload so the board doesn't
@@ -308,6 +314,7 @@ function connect(){
308
314
  es.onmessage = (ev)=>{
309
315
  let m; try{ m=JSON.parse(ev.data); }catch{ return; }
310
316
  if(m.type==='change'||m.type==='message') return scheduleLoad(); // messages live from runtime.messages
317
+ if(m.type==='brain') return loadBrain(); // ~/.spectoflow/brain.md changed (page, MCP, run line)
311
318
  if(m.type==='run-start'||m.type==='run-end') { chatState.forEach(st=>{ st.rawBlock=null; }); sseBusy=(m.type==='run-start'); updateChatBusyUI(); return; }
312
319
  if(m.type==='run-line') return appendRaw(m.chunk); // raw output is ephemeral (not logged)
313
320
  };
@@ -534,7 +541,7 @@ function render(){
534
541
  kanbanExpanded.clear(); // a full SSE-driven reload always collapses any "show more" a viewer had open
535
542
  renderOverview(); renderBoard(); renderBacklog(); renderWorkflow(); renderTeam();
536
543
  renderChatLog($('#chatLog')); renderChatLog($('#chatTabLog'));
537
- renderSidebar(); renderRequests(); renderAttention(); renderInfo(); renderDocs(); renderSettings(); renderFiles(); renderNotes(); renderMeeting(); applySideHidden(); updateChatBusyUI();
544
+ renderSidebar(); renderRequests(); renderAttention(); renderInfo(); renderDocs(); renderSettings(); renderFiles(); renderNotes(); renderMeeting(); renderBrain(); applySideHidden(); updateChatBusyUI();
538
545
  renderCustomDashboards(); // adds/removes nav tabs + panels before applyActiveTab() below reads them
539
546
  applyNavTabs(); // hide/reorder native tabs per config.navTabs (may itself navigate away from a just-disabled active tab)
540
547
  applyActiveTab(); // re-apply the current tab so an SSE-driven re-render never resets to Board
@@ -1354,7 +1361,7 @@ function renderCustomDashboards(){
1354
1361
 
1355
1362
  // ---- Settings → Customize: add dashboards/skills/agents by description, or "Auto" -------------
1356
1363
  // Generation itself is real agent work (research, clarify, write a file) — this UI never does it
1357
- // client-side. It just constructs a plain-language prompt (recognized by AGENTS.md's Router, which
1364
+ // client-side. It just constructs a plain-language prompt (recognized by SPECTOFLOW.md's Router, which
1358
1365
  // hands it to framework-curator) and sends it through the SAME /api/run + group-chat pipeline every
1359
1366
  // other "Run" already uses, then jumps to Chat so the requester watches it happen and can answer any
1360
1367
  // clarifying question there — no separate conversational UI to build or keep in sync.
@@ -1425,7 +1432,7 @@ async function czSubmit(kind,description,agent){
1425
1432
  // A custom dashboard (Customize page) gets its own tab id "custom:<id>" and its own URL shape
1426
1433
  // /custom/<id> — kept out of ROUTES (a fixed list) since the set of custom ids is dynamic; recognized
1427
1434
  // by a dedicated branch in tabFromPath()/navigateTab() instead.
1428
- const ROUTES=['board','requests','attention','backlog','workflow','team','files','notes','meeting','chat','info','docs','personalize'];
1435
+ const ROUTES=['board','requests','attention','backlog','workflow','team','files','notes','meeting','chat','info','docs','brain','personalize'];
1429
1436
  // the tab used to be named/routed "settings" — old bookmarks and any localStorage value saved
1430
1437
  // under that name still land on the Personalize tab instead of a blank panel.
1431
1438
  function normalizeTab(t){ return t==='settings'?'personalize':t; }
@@ -1451,6 +1458,7 @@ function navigateTab(tabId,push){
1451
1458
  // was scrolled to last (only on an actual switch INTO the tab — applyActiveTab() alone runs on
1452
1459
  // every SSE render tick too, and re-scrolling/re-focusing there would fight the user's typing).
1453
1460
  if(tabId==='chat') setTimeout(()=>{ scrollChat($('#chatTabLog')); $('#tabRunPrompt').focus(); },60);
1461
+ if(tabId==='brain'){ renderBrain(); if(!brainData) loadBrain(); }
1454
1462
  if(tabId==='files') renderFiles(); // the tree is fetched lazily — only load it on an actual switch in
1455
1463
  }
1456
1464
  function closeNav(){ document.body.classList.remove('nav-open'); const nt=$('#navToggle'); if(nt) nt.setAttribute('aria-expanded','false'); }
@@ -2027,6 +2035,159 @@ function notesSetStatus(state){
2027
2035
  } else { tip.textContent=t('files.saveError'); tip.className='note-status is-error'; }
2028
2036
  }
2029
2037
 
2038
+ // ---- Second brain ------------------------------------------------------------------------------
2039
+ // What spectoflow has learned about the user: ~/.spectoflow/brain.md, shared by all their projects —
2040
+ // NOT this project's, so it is fetched on its own (/api/brain), never part of /api/project. The hub
2041
+ // pushes a 'brain' SSE event whenever the file changes, whoever wrote it (this page, an agent through
2042
+ // `spectoflow mcp`, or a `::spectoflow learn` run line).
2043
+ const BRAIN_CATEGORIES=['profile','preferences','workflow','avoid'];
2044
+ const BRAIN_SOFT_LIMIT=60;
2045
+ let brainData=null, brainError=null, brainActionError=null;
2046
+ async function loadBrain(){
2047
+ if(REMOTE) return;
2048
+ try{
2049
+ const r=await fetch(withProject('/api/brain'));
2050
+ const d=await r.json().catch(()=>({}));
2051
+ if(!r.ok) throw new Error(d.error||'error');
2052
+ brainData=d; brainError=null;
2053
+ }catch(err){ brainError=err.message||'error'; }
2054
+ renderBrain();
2055
+ }
2056
+ async function brainCall(method,url,body){
2057
+ flash();
2058
+ const r=await fetch(withProject(url),{method,headers:{'Content-Type':'application/json'},body:body===undefined?undefined:JSON.stringify(body)});
2059
+ const d=await r.json().catch(()=>({}));
2060
+ if(!r.ok) throw new Error(d.error||t('brain.error'));
2061
+ return d;
2062
+ }
2063
+ // A re-render (SSE tick, another tab's write) must never wipe what the user is typing.
2064
+ function brainIsEditing(){
2065
+ const a=document.activeElement;
2066
+ return !!(a && (a.tagName==='INPUT'||a.tagName==='TEXTAREA') && a.closest('#brainGrid, #brainPending'));
2067
+ }
2068
+ function renderBrain(){
2069
+ const badge=$('#brainBadge');
2070
+ const pendingN=brainData ? brainData.pending.length : 0;
2071
+ if(badge){ badge.textContent=pendingN; badge.hidden=pendingN===0; }
2072
+ if(activeTab!=='brain' || brainIsEditing()) return;
2073
+ const grid=$('#brainGrid'); if(!grid) return;
2074
+ if(brainError && !brainData){ grid.innerHTML=''; grid.append(el('div','empty',t('brain.error'))); return; }
2075
+ if(!brainData){ grid.innerHTML=''; grid.append(el('div','empty',t('drawer.loading'))); return; }
2076
+ const d=brainData;
2077
+ $('#brainCount').textContent=d.entries.length;
2078
+ const auto=$('#brainAutoAdd'); auto.checked=!!d.autoAdd;
2079
+ $('#brainAutoHint').textContent=d.autoAdd ? t('brain.autoAddOn') : t('brain.autoAddOff');
2080
+ renderBrainAgents(d.agents||[]);
2081
+ const notice=$('#brainNotice');
2082
+ const noticeText=brainActionError || (d.entries.length>BRAIN_SOFT_LIMIT ? t('brain.tooMany',{n:d.entries.length}) : '');
2083
+ notice.hidden=!noticeText; notice.textContent=noticeText;
2084
+ notice.classList.toggle('is-error',!!brainActionError);
2085
+ renderBrainPending(d.pending);
2086
+ grid.innerHTML='';
2087
+ if(!d.entries.length && !d.pending.length) grid.append(el('div','brain-empty',t('brain.empty')));
2088
+ BRAIN_CATEGORIES.forEach(cat=> grid.append(brainCard(cat, d.entries.filter(e=>e.category===cat))));
2089
+ $('#brainPath').textContent=t('brain.path',{path:d.path||'~/.spectoflow/brain.md'});
2090
+ }
2091
+ function renderBrainAgents(agents){
2092
+ const box=$('#brainAgents'); box.innerHTML='';
2093
+ if(!agents.length){ box.append(el('span','brain-agents-none',t('brain.noAgents'))); return; }
2094
+ box.append(el('span','brain-agents-label',t('brain.agents')));
2095
+ agents.forEach(a=>{
2096
+ const chip=el('span','brain-agent'+(a.wired?' is-wired':''), (a.wired?'● ':'○ ')+a.label);
2097
+ chip.title=a.wired?t('brain.agentWired'):t('brain.agentNotWired');
2098
+ box.append(chip);
2099
+ });
2100
+ if(agents.some(a=>!a.wired)){
2101
+ const hint=el('span','brain-agents-hint'); const parts=t('brain.setupHint').split('{cmd}');
2102
+ hint.append(parts[0]||'', el('code',null,'spectoflow brain setup'), parts[1]||'');
2103
+ box.append(hint);
2104
+ }
2105
+ }
2106
+ function brainMeta(e){
2107
+ const who=e.by==='agent' ? t('brain.byAgent') : t('brain.byYou');
2108
+ return e.at ? `${who} · ${e.at}` : who;
2109
+ }
2110
+ function renderBrainPending(pending){
2111
+ const box=$('#brainPending'); box.innerHTML='';
2112
+ box.hidden=!pending.length; if(!pending.length) return;
2113
+ const head=el('div','brain-pending-head');
2114
+ head.append(el('h3','brain-pending-title',t('brain.toConfirm')));
2115
+ const all=el('button','btn primary',t('brain.confirmAll'));
2116
+ all.addEventListener('click',async()=>{ all.disabled=true; try{ for(const e of pending) await brainCall('POST',`/api/brain/${encodeURIComponent(e.id)}/confirm`,{}); }catch(err){} loadBrain(); });
2117
+ head.append(all); box.append(head);
2118
+ pending.forEach(e=>{
2119
+ const row=el('div','brain-row is-pending');
2120
+ row.append(el('span','brain-cat-chip',t('brain.cat.'+e.category)));
2121
+ const txt=el('div','brain-text',e.text); row.append(txt);
2122
+ row.append(el('div','brain-meta',brainMeta(e)));
2123
+ const acts=el('div','brain-actions');
2124
+ const ok=el('button','btn primary',t('brain.confirm')); ok.addEventListener('click',()=>brainAct(()=>brainCall('POST',`/api/brain/${encodeURIComponent(e.id)}/confirm`,{})));
2125
+ const ed=el('button','btn',t('action.edit')); ed.addEventListener('click',()=>brainEdit(e,txt));
2126
+ const no=el('button','btn danger',t('brain.reject')); no.addEventListener('click',()=>brainAct(()=>brainCall('DELETE',`/api/brain/${encodeURIComponent(e.id)}`)));
2127
+ acts.append(ok,ed,no); row.append(acts);
2128
+ box.append(row);
2129
+ });
2130
+ }
2131
+ function brainCard(cat, entries){
2132
+ const card=el('div','brain-card');
2133
+ const head=el('div','brain-card-head');
2134
+ head.append(el('h3','brain-card-title',t('brain.cat.'+cat)), el('span','count',String(entries.length)));
2135
+ card.append(head, el('p','brain-card-hint',t('brain.catHint.'+cat)));
2136
+ const list=el('div','brain-list');
2137
+ if(!entries.length) list.append(el('div','empty brain-card-empty',t('brain.catEmpty')));
2138
+ entries.forEach(e=>{
2139
+ const row=el('div','brain-row'+(e.by==='agent'?' from-agent':''));
2140
+ const txt=el('div','brain-text',e.text); row.append(txt);
2141
+ const foot=el('div','brain-row-foot');
2142
+ foot.append(el('span','brain-meta',brainMeta(e)));
2143
+ const acts=el('span','brain-actions');
2144
+ const ed=el('button','btn btn-xs',t('action.edit')); ed.addEventListener('click',()=>brainEdit(e,txt));
2145
+ const del=el('button','btn btn-xs danger',t('action.delete')); del.addEventListener('click',()=>brainAct(()=>brainCall('DELETE',`/api/brain/${encodeURIComponent(e.id)}`)));
2146
+ acts.append(ed,del); foot.append(acts); row.append(foot);
2147
+ list.append(row);
2148
+ });
2149
+ card.append(list);
2150
+ const add=el('form','brain-add');
2151
+ const input=el('input','brain-add-input'); input.type='text'; input.maxLength=500; input.placeholder=t('brain.addPlaceholder');
2152
+ const btn=el('button','btn',t('action.add')); btn.type='submit';
2153
+ const err=el('div','brain-add-error'); err.hidden=true;
2154
+ add.append(input,btn,err);
2155
+ add.addEventListener('submit',async(ev)=>{
2156
+ ev.preventDefault();
2157
+ const text=input.value.trim(); if(!text){ input.focus(); return; }
2158
+ btn.disabled=true; err.hidden=true;
2159
+ try{
2160
+ const r=await brainCall('POST','/api/brain',{category:cat,text});
2161
+ input.value=''; input.blur();
2162
+ if(r.duplicate){ err.textContent=t('brain.duplicate'); err.hidden=false; }
2163
+ }catch(e){ err.textContent=e.message; err.hidden=false; }
2164
+ btn.disabled=false; loadBrain();
2165
+ });
2166
+ card.append(add);
2167
+ return card;
2168
+ }
2169
+ async function brainAct(fn){
2170
+ try{ await fn(); brainActionError=null; }catch(err){ brainActionError=err.message; }
2171
+ loadBrain();
2172
+ }
2173
+ // Inline edit, same interaction as the Attention tab: blur or Ctrl/Cmd+Enter saves, Escape cancels.
2174
+ function brainEdit(e, txtNode){
2175
+ const ta=el('textarea','brain-edit'); ta.value=e.text; ta.maxLength=500; txtNode.replaceWith(ta); ta.focus();
2176
+ let done=false;
2177
+ const finish=async(save)=>{
2178
+ if(done) return; done=true;
2179
+ const v=ta.value.trim();
2180
+ ta.blur();
2181
+ if(save && v && v!==e.text) await brainAct(()=>brainCall('PATCH',`/api/brain/${encodeURIComponent(e.id)}`,{text:v}));
2182
+ else renderBrain();
2183
+ };
2184
+ ta.addEventListener('blur',()=>finish(true));
2185
+ ta.addEventListener('keydown',ev=>{
2186
+ if((ev.metaKey||ev.ctrlKey)&&ev.key==='Enter'){ ev.preventDefault(); finish(true); }
2187
+ if(ev.key==='Escape'){ ev.preventDefault(); finish(false); }
2188
+ });
2189
+ }
2190
+
2030
2191
  // ---- Daily meeting (Sous-projet C, Task 3) ------------------------------------------------------
2031
2192
  // One dated Markdown note per day, .spectoflow/meetings/<date>.md. MANUAL editing reuses the exact
2032
2193
  // same files.read/files.write ops + filesCodeEditor() component as Bloc note above (700ms debounced
@@ -2397,6 +2558,7 @@ $('#blAddCancel').addEventListener('click', closeBacklogAddForm);
2397
2558
  $('#blAddSubmit').addEventListener('click', submitBacklogAdd);
2398
2559
  $('#blAddTitle').addEventListener('keydown', e=>{ if(e.key==='Enter') submitBacklogAdd(); });
2399
2560
  // attention tab: add a note + filter chips
2561
+ $('#brainAutoAdd').addEventListener('change',(e)=>brainAct(()=>brainCall('POST','/api/brain/settings',{autoAdd:e.target.checked})));
2400
2562
  $('#attnAddBtn').addEventListener('click',()=>{ const t=$('#attnInput'); const v=t.value.trim(); if(v){ addAttn(v); t.value=''; } });
2401
2563
  $('#attnInput').addEventListener('keydown',e=>{ if((e.metaKey||e.ctrlKey)&&e.key==='Enter'){ const v=e.target.value.trim(); if(v){ addAttn(v); e.target.value=''; } } });
2402
2564
  $$('.attn-filters .fchip').forEach(b=> b.addEventListener('click',()=>{ attnFilter=b.dataset.attn; renderAttention(); }));
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
  /*
3
3
  * Dashboard UI translations. `config.json`'s `language` governs BOTH the agent's own output (specs,
4
- * plans, code comments — per AGENTS.md) AND, from here, this dashboard's chrome — so picking a
4
+ * plans, code comments — per SPECTOFLOW.md) AND, from here, this dashboard's chrome — so picking a
5
5
  * language actually changes what you see, not just what the agent writes.
6
6
  *
7
7
  * t(key, vars?) looks up I18N[currentLang][key], falls back to I18N.en[key], then to the key itself
@@ -20,7 +20,7 @@
20
20
  const I18N = {
21
21
  en: {
22
22
  'nav.board':'Board','nav.requests':'Requests','nav.attention':'Attention','nav.backlog':'Backlog',
23
- 'nav.workflow':'Workflow','nav.team':'Agents & Skills','nav.files':'Files','nav.notes':'Notes','nav.meeting':'Daily meeting','nav.chat':'Chat','nav.info':'Info','nav.docs':'Documentation','nav.settings':'Personalize',
23
+ 'nav.workflow':'Workflow','nav.team':'Agents & Skills','nav.files':'Files','nav.notes':'Notes','nav.meeting':'Daily meeting','nav.chat':'Chat','nav.info':'Info','nav.docs':'Documentation','nav.brain':'Second brain','nav.settings':'Personalize',
24
24
  'status.todo':'To do','status.in_progress':'In progress','status.to_validate':'To validate',
25
25
  'status.to_analyze':'To analyze','status.done':'Done','status.blocked':'Blocked',
26
26
  'filter.all':'All','filter.open':'Open','filter.resolved':'Resolved',
@@ -90,10 +90,11 @@ 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
+ '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.',
93
94
  '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',
94
95
  'chat.widgetTitle':'Run an agent','chat.widgetSub':'Quick access · full view in the Chat tab',
95
96
  'chat.tabSub':'Full conversation with the runner — the same run as the widget, more room to read it.',
96
- 'chat.idle':'Type a request — the agent runs headless in this project with full memory (<code>CLAUDE.md → AGENTS.md</code>) and updates the board live.',
97
+ 'chat.idle':'Type a request — the agent runs headless in this project with full memory (<code>CLAUDE.md / AGENTS.md → .spectoflow/SPECTOFLOW.md</code>) and updates the board live.',
97
98
  'chat.inputPlaceholder':'e.g. Add a login feature with email + password',
98
99
  'chat.orchestrateTitle':'Walk the enabled workflow','chat.summarizeTitle':'Condense the recent activity into a summary','chat.clearTitle':'Clear the chat log',
99
100
  'chat.warn':'⚠ Launches a real agent (<code>config.json → runners</code>) that can modify files &amp; run commands.','chat.running':'Agent running…',
@@ -139,7 +140,7 @@ en: {
139
140
  },
140
141
  fr: {
141
142
  'nav.board':'Tableau','nav.requests':'Demandes','nav.attention':'Attention','nav.backlog':'Backlog',
142
- 'nav.workflow':'Workflow','nav.team':'Agents et compétences','nav.files':'Fichiers','nav.notes':'Notes','nav.meeting':'Réunion quotidienne','nav.chat':'Chat','nav.info':'Infos','nav.docs':'Documentation','nav.settings':'Personnalisation',
143
+ 'nav.workflow':'Workflow','nav.team':'Agents et compétences','nav.files':'Fichiers','nav.notes':'Notes','nav.meeting':'Réunion quotidienne','nav.chat':'Chat','nav.info':'Infos','nav.docs':'Documentation','nav.brain':'Second cerveau','nav.settings':'Personnalisation',
143
144
  'status.todo':'À faire','status.in_progress':'En cours','status.to_validate':'À valider',
144
145
  'status.to_analyze':'À analyser','status.done':'Terminé','status.blocked':'Bloqué',
145
146
  'filter.all':'Tous','filter.open':'Ouverts','filter.resolved':'Résolus',
@@ -209,10 +210,11 @@ fr: {
209
210
  'drawer.agent':'Agent','drawer.skill':'Compétence','drawer.file':'Fichier · {rel}',
210
211
  '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',
211
212
  '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…',
213
+ '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.',
212
214
  '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',
213
215
  'chat.widgetTitle':'Lancer un agent','chat.widgetSub':'Accès rapide · vue complète dans l’onglet Chat',
214
216
  'chat.tabSub':'Conversation complète avec l’exécuteur — la même exécution que le widget, avec plus de place pour la lire.',
215
- 'chat.idle':'Tapez une demande — l’agent s’exécute sans supervision dans ce projet avec toute sa mémoire (<code>CLAUDE.md → AGENTS.md</code>) et met le tableau à jour en direct.',
217
+ 'chat.idle':'Tapez une demande — l’agent s’exécute sans supervision dans ce projet avec toute sa mémoire (<code>CLAUDE.md / AGENTS.md → .spectoflow/SPECTOFLOW.md</code>) et met le tableau à jour en direct.',
216
218
  'chat.inputPlaceholder':'ex. Ajouter une fonctionnalité de connexion par email + mot de passe',
217
219
  'chat.orchestrateTitle':'Parcourir le workflow activé','chat.summarizeTitle':'Condenser l’activité récente en un résumé','chat.clearTitle':'Effacer le journal de discussion',
218
220
  'chat.warn':'⚠ Lance un agent réel (<code>config.json → runners</code>) qui peut modifier des fichiers et exécuter des commandes.','chat.running':'Agent en cours d’exécution…',
@@ -258,7 +260,7 @@ fr: {
258
260
  },
259
261
  es: {
260
262
  'nav.board':'Tablero','nav.requests':'Solicitudes','nav.attention':'Atención','nav.backlog':'Backlog',
261
- 'nav.workflow':'Workflow','nav.team':'Agentes y habilidades','nav.files':'Archivos','nav.notes':'Notas','nav.meeting':'Reunión diaria','nav.chat':'Chat','nav.info':'Info','nav.docs':'Documentación','nav.settings':'Personalizar',
263
+ 'nav.workflow':'Workflow','nav.team':'Agentes y habilidades','nav.files':'Archivos','nav.notes':'Notas','nav.meeting':'Reunión diaria','nav.chat':'Chat','nav.info':'Info','nav.docs':'Documentación','nav.brain':'Segundo cerebro','nav.settings':'Personalizar',
262
264
  'status.todo':'Por hacer','status.in_progress':'En curso','status.to_validate':'Por validar',
263
265
  'status.to_analyze':'Por analizar','status.done':'Hecho','status.blocked':'Bloqueado',
264
266
  'filter.all':'Todos','filter.open':'Abiertos','filter.resolved':'Resueltos',
@@ -328,10 +330,11 @@ es: {
328
330
  'drawer.agent':'Agente','drawer.skill':'Habilidad','drawer.file':'Archivo · {rel}',
329
331
  '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',
330
332
  '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…',
333
+ '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.',
331
334
  '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',
332
335
  'chat.widgetTitle':'Ejecutar un agente','chat.widgetSub':'Acceso rápido · vista completa en la pestaña Chat',
333
336
  'chat.tabSub':'Conversación completa con el ejecutor — la misma ejecución que el widget, con más espacio para leerla.',
334
- 'chat.idle':'Escribe una solicitud — el agente se ejecuta sin supervisión en este proyecto con toda su memoria (<code>CLAUDE.md → AGENTS.md</code>) y actualiza el tablero en vivo.',
337
+ 'chat.idle':'Escribe una solicitud — el agente se ejecuta sin supervisión en este proyecto con toda su memoria (<code>CLAUDE.md / AGENTS.md → .spectoflow/SPECTOFLOW.md</code>) y actualiza el tablero en vivo.',
335
338
  'chat.inputPlaceholder':'p. ej. Añadir un inicio de sesión con email + contraseña',
336
339
  'chat.orchestrateTitle':'Recorrer el workflow activado','chat.summarizeTitle':'Condensar la actividad reciente en un resumen','chat.clearTitle':'Borrar el registro del chat',
337
340
  'chat.warn':'⚠ Lanza un agente real (<code>config.json → runners</code>) que puede modificar archivos y ejecutar comandos.','chat.running':'Agente en ejecución…',
@@ -377,7 +380,7 @@ es: {
377
380
  },
378
381
  de: {
379
382
  'nav.board':'Board','nav.requests':'Anfragen','nav.attention':'Hinweise','nav.backlog':'Backlog',
380
- 'nav.workflow':'Workflow','nav.team':'Agenten & Skills','nav.files':'Dateien','nav.notes':'Notizen','nav.meeting':'Tagesbesprechung','nav.chat':'Chat','nav.info':'Info','nav.docs':'Dokumentation','nav.settings':'Personalisieren',
383
+ 'nav.workflow':'Workflow','nav.team':'Agenten & Skills','nav.files':'Dateien','nav.notes':'Notizen','nav.meeting':'Tagesbesprechung','nav.chat':'Chat','nav.info':'Info','nav.docs':'Dokumentation','nav.brain':'Zweites Gehirn','nav.settings':'Personalisieren',
381
384
  'status.todo':'Offen','status.in_progress':'In Arbeit','status.to_validate':'Zu prüfen',
382
385
  'status.to_analyze':'Zu analysieren','status.done':'Erledigt','status.blocked':'Blockiert',
383
386
  'filter.all':'Alle','filter.open':'Offen','filter.resolved':'Erledigt',
@@ -447,10 +450,11 @@ de: {
447
450
  'drawer.agent':'Agent','drawer.skill':'Skill','drawer.file':'Datei · {rel}',
448
451
  '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',
449
452
  '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…',
453
+ '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.',
450
454
  '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',
451
455
  'chat.widgetTitle':'Agenten ausführen','chat.widgetSub':'Schnellzugriff · vollständige Ansicht im Chat-Tab',
452
456
  'chat.tabSub':'Vollständiges Gespräch mit dem Runner — derselbe Lauf wie im Widget, mit mehr Platz zum Lesen.',
453
- 'chat.idle':'Geben Sie eine Anfrage ein — der Agent läuft eigenständig in diesem Projekt mit vollem Gedächtnis (<code>CLAUDE.md → AGENTS.md</code>) und aktualisiert das Board live.',
457
+ 'chat.idle':'Geben Sie eine Anfrage ein — der Agent läuft eigenständig in diesem Projekt mit vollem Gedächtnis (<code>CLAUDE.md / AGENTS.md → .spectoflow/SPECTOFLOW.md</code>) und aktualisiert das Board live.',
454
458
  'chat.inputPlaceholder':'z. B. Login mit E-Mail + Passwort hinzufügen',
455
459
  'chat.orchestrateTitle':'Den aktivierten Workflow durchlaufen','chat.summarizeTitle':'Die letzten Aktivitäten zu einer Zusammenfassung verdichten','chat.clearTitle':'Chat-Verlauf löschen',
456
460
  'chat.warn':'⚠ Startet einen echten Agenten (<code>config.json → runners</code>), der Dateien ändern und Befehle ausführen kann.','chat.running':'Agent läuft…',
@@ -496,7 +500,7 @@ de: {
496
500
  },
497
501
  pt: {
498
502
  'nav.board':'Painel','nav.requests':'Pedidos','nav.attention':'Atenção','nav.backlog':'Backlog',
499
- 'nav.workflow':'Workflow','nav.team':'Agentes e habilidades','nav.files':'Ficheiros','nav.notes':'Notas','nav.meeting':'Reunião diária','nav.chat':'Chat','nav.info':'Info','nav.docs':'Documentação','nav.settings':'Personalizar',
503
+ 'nav.workflow':'Workflow','nav.team':'Agentes e habilidades','nav.files':'Ficheiros','nav.notes':'Notas','nav.meeting':'Reunião diária','nav.chat':'Chat','nav.info':'Info','nav.docs':'Documentação','nav.brain':'Segundo cérebro','nav.settings':'Personalizar',
500
504
  'status.todo':'A fazer','status.in_progress':'Em curso','status.to_validate':'Por validar',
501
505
  'status.to_analyze':'Por analisar','status.done':'Concluído','status.blocked':'Bloqueado',
502
506
  'filter.all':'Todos','filter.open':'Abertos','filter.resolved':'Resolvidos',
@@ -566,10 +570,11 @@ pt: {
566
570
  'drawer.agent':'Agente','drawer.skill':'Habilidade','drawer.file':'Ficheiro · {rel}',
567
571
  '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',
568
572
  '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…',
573
+ '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.',
569
574
  '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',
570
575
  'chat.widgetTitle':'Executar um agente','chat.widgetSub':'Acesso rápido · vista completa no separador Chat',
571
576
  'chat.tabSub':'Conversa completa com o executor — a mesma execução do widget, com mais espaço para ler.',
572
- 'chat.idle':'Escreva um pedido — o agente corre sem supervisão neste projeto com toda a sua memória (<code>CLAUDE.md → AGENTS.md</code>) e atualiza o painel em direto.',
577
+ 'chat.idle':'Escreva um pedido — o agente corre sem supervisão neste projeto com toda a sua memória (<code>CLAUDE.md / AGENTS.md → .spectoflow/SPECTOFLOW.md</code>) e atualiza o painel em direto.',
573
578
  'chat.inputPlaceholder':'ex. Adicionar login com email + palavra-passe',
574
579
  'chat.orchestrateTitle':'Percorrer o workflow ativado','chat.summarizeTitle':'Condensar a atividade recente num resumo','chat.clearTitle':'Limpar o registo do chat',
575
580
  'chat.warn':'⚠ Inicia um agente real (<code>config.json → runners</code>) que pode alterar ficheiros e executar comandos.','chat.running':'Agente em execução…',
@@ -615,7 +620,7 @@ pt: {
615
620
  },
616
621
  it: {
617
622
  'nav.board':'Bacheca','nav.requests':'Richieste','nav.attention':'Attenzione','nav.backlog':'Backlog',
618
- 'nav.workflow':'Workflow','nav.team':'Agenti e competenze','nav.files':'File','nav.notes':'Note','nav.meeting':'Riunione quotidiana','nav.chat':'Chat','nav.info':'Info','nav.docs':'Documentazione','nav.settings':'Personalizza',
623
+ 'nav.workflow':'Workflow','nav.team':'Agenti e competenze','nav.files':'File','nav.notes':'Note','nav.meeting':'Riunione quotidiana','nav.chat':'Chat','nav.info':'Info','nav.docs':'Documentazione','nav.brain':'Secondo cervello','nav.settings':'Personalizza',
619
624
  'status.todo':'Da fare','status.in_progress':'In corso','status.to_validate':'Da convalidare',
620
625
  'status.to_analyze':'Da analizzare','status.done':'Fatto','status.blocked':'Bloccato',
621
626
  'filter.all':'Tutti','filter.open':'Aperti','filter.resolved':'Risolti',
@@ -685,10 +690,11 @@ it: {
685
690
  'drawer.agent':'Agente','drawer.skill':'Skill','drawer.file':'File · {rel}',
686
691
  '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',
687
692
  '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…',
693
+ '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.',
688
694
  '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',
689
695
  'chat.widgetTitle':'Avvia un agente','chat.widgetSub':'Accesso rapido · vista completa nella scheda Chat',
690
696
  'chat.tabSub':'Conversazione completa con l’esecutore — la stessa esecuzione del widget, con più spazio per leggerla.',
691
- 'chat.idle':'Digita una richiesta — l’agente viene eseguito senza supervisione in questo progetto con tutta la sua memoria (<code>CLAUDE.md → AGENTS.md</code>) e aggiorna la bacheca in diretta.',
697
+ 'chat.idle':'Digita una richiesta — l’agente viene eseguito senza supervisione in questo progetto con tutta la sua memoria (<code>CLAUDE.md / AGENTS.md → .spectoflow/SPECTOFLOW.md</code>) e aggiorna la bacheca in diretta.',
692
698
  'chat.inputPlaceholder':'es. Aggiungi un login con email + password',
693
699
  'chat.orchestrateTitle':'Percorri il workflow attivato','chat.summarizeTitle':'Condensa l’attività recente in un riassunto','chat.clearTitle':'Cancella il registro della chat',
694
700
  'chat.warn':'⚠ Avvia un agente reale (<code>config.json → runners</code>) che può modificare file ed eseguire comandi.','chat.running':'Agente in esecuzione…',
@@ -25,6 +25,8 @@
25
25
  attention: wrap('<line x1="4.5" y1="2.4" x2="4.5" y2="15.6"/><path d="M4.5 3.4h8.2l-1.7 2.6 1.7 2.6H4.5z"/>'),
26
26
  // preference sliders — personalize (each track's handle sits at its own setting, not a generic gear)
27
27
  settings: wrap('<line x1="3" y1="5" x2="15" y2="5"/><circle cx="7" cy="5" r="1.6" fill="currentColor" stroke="none"/><line x1="3" y1="9" x2="15" y2="9"/><circle cx="11.5" cy="9" r="1.6" fill="currentColor" stroke="none"/><line x1="3" y1="13" x2="15" y2="13"/><circle cx="6" cy="13" r="1.6" fill="currentColor" stroke="none"/>'),
28
+ // two brain hemispheres — the second brain (what spectoflow learned about you)
29
+ brain: wrap('<path d="M8.4 3.4c-1.2-.9-3.1-.4-3.4 1.2-1.4.2-2.3 1.7-1.7 3-1 .7-1 2.2-.2 3-.4 1.4.7 2.7 2.1 2.7.5 1.2 2 1.7 3.2 1z"/><path d="M9.6 3.4c1.2-.9 3.1-.4 3.4 1.2 1.4.2 2.3 1.7 1.7 3 1 .7 1 2.2.2 3 .4 1.4-.7 2.7-2.1 2.7-.5 1.2-2 1.7-3.2 1z"/>'),
28
30
  // open book — documentation
29
31
  docs: wrap('<path d="M9 5.4C7.4 4.2 5.2 3.7 2.8 4v9c2.4-.3 4.6.2 6.2 1.4 1.6-1.2 3.8-1.7 6.2-1.4V4c-2.4-.3-4.6.2-6.2 1.4z"/><line x1="9" y1="5.4" x2="9" y2="14.4"/>'),
30
32
  // folder — the project's file tree
@@ -65,6 +65,7 @@
65
65
  <button class="tab" data-tab="meeting" data-i18n-title="nav.meeting" title="Daily meeting"><span class="tab-ico" data-icon="meeting"></span><span class="tab-label" data-i18n="nav.meeting">Daily meeting</span></button>
66
66
  <button class="tab" data-tab="info" data-i18n-title="nav.info" title="Info"><span class="tab-ico" data-icon="info"></span><span class="tab-label" data-i18n="nav.info">Info</span></button>
67
67
  <button class="tab" data-tab="docs" data-i18n-title="nav.docs" title="Documentation"><span class="tab-ico" data-icon="docs"></span><span class="tab-label" data-i18n="nav.docs">Documentation</span></button>
68
+ <button class="tab" data-tab="brain" data-i18n-title="nav.brain" title="Second brain"><span class="tab-ico" data-icon="brain"></span><span class="tab-label" data-i18n="nav.brain">Second brain</span><span class="tab-badge" id="brainBadge" hidden>0</span></button>
68
69
  <button class="tab" data-tab="personalize" data-i18n-title="nav.settings" title="Personalize"><span class="tab-ico" data-icon="settings"></span><span class="tab-label" data-i18n="nav.settings">Personalize</span></button>
69
70
  </nav>
70
71
  <div class="top-right">
@@ -277,6 +278,24 @@
277
278
  </div>
278
279
  </section>
279
280
 
281
+ <!-- SECOND BRAIN — what spectoflow has learned about the user, shared by all their projects.
282
+ Lives in ~/.spectoflow/brain.md (never in a project); agents reach it through `spectoflow mcp`. -->
283
+ <section class="panel" data-panel="brain">
284
+ <div class="brain-wrap">
285
+ <h2 class="panel-title"><span data-i18n="nav.brain">Second brain</span> <span class="count" id="brainCount">0</span></h2>
286
+ <p class="panel-sub" data-i18n="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.</p>
287
+ <div class="brain-bar">
288
+ <label class="brain-auto"><input type="checkbox" id="brainAutoAdd"> <span data-i18n="brain.autoAdd">Add what the agent learns directly</span></label>
289
+ <span class="brain-auto-hint" id="brainAutoHint"></span>
290
+ </div>
291
+ <div class="brain-agents" id="brainAgents"></div>
292
+ <div class="brain-notice" id="brainNotice" hidden></div>
293
+ <div class="brain-pending" id="brainPending" hidden></div>
294
+ <div class="brain-grid" id="brainGrid"></div>
295
+ <p class="brain-path" id="brainPath"></p>
296
+ </div>
297
+ </section>
298
+
280
299
  <!-- DAILY MEETING (Sous-projet C, Task 3) — one dated Markdown note per day,
281
300
  .spectoflow/meetings/<date>.md, manually editable and agent-generatable. A journal/report
282
301
  panel, styled like the rest of the dashboard — deliberately NOT the Notes tab's post-it look. -->
@@ -318,7 +337,7 @@
318
337
  </div>
319
338
  <div class="chat-tab-log" id="chatTabLog">
320
339
  <div class="chat-idle" data-i18n-html="chat.idle">Type a request — the agent runs headless in this project with full memory
321
- (<code>CLAUDE.md → AGENTS.md</code>) and updates the board live.</div>
340
+ (<code>CLAUDE.md / AGENTS.md → .spectoflow/SPECTOFLOW.md</code>) and updates the board live.</div>
322
341
  </div>
323
342
  <!-- Toolbar right above the input, not up in the header — this is where you're looking
324
343
  when you reach for Summarize/Clear or switch which agent the next message runs on. -->
@@ -455,7 +474,7 @@
455
474
  </div>
456
475
  <div class="chat-log" id="chatLog">
457
476
  <div class="chat-idle" data-i18n-html="chat.idle">Type a request — the agent runs headless in this project with full memory
458
- (<code>CLAUDE.md → AGENTS.md</code>) and updates the board live.</div>
477
+ (<code>CLAUDE.md / AGENTS.md → .spectoflow/SPECTOFLOW.md</code>) and updates the board live.</div>
459
478
  </div>
460
479
  <div class="chat-toolbar">
461
480
  <button id="widgetSummarizeBtn" class="mini-btn" data-i18n-title="chat.summarizeTitle" data-i18n="action.summarize" title="Condense the recent activity into a summary">Summarize</button>
@@ -658,6 +658,52 @@ body.booting .ring-svg circle:last-of-type { transform-origin:center; animation:
658
658
  .attn-text { font-size:14px; line-height:1.5; white-space:pre-wrap; }
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
+
662
+ /* ---- Second brain ---- */
663
+ .brain-wrap { max-width:1080px; margin:0 auto; padding:20px 22px 32px; }
664
+ .brain-bar { display:flex; flex-wrap:wrap; align-items:center; gap:6px 14px; margin:4px 0 10px; }
665
+ .brain-auto { display:inline-flex; align-items:center; gap:8px; font-size:13.5px; font-weight:600; cursor:pointer; }
666
+ .brain-auto input { accent-color:var(--signal); width:15px; height:15px; margin:0; }
667
+ .brain-auto-hint { color:var(--muted); font-size:12.5px; }
668
+ .brain-agents { display:flex; flex-wrap:wrap; align-items:center; gap:6px 8px; font-size:12px; margin-bottom:16px; }
669
+ .brain-agents-label, .brain-agents-none, .brain-agents-hint { color:var(--muted); }
670
+ .brain-agents-hint code { font-family:var(--mono); font-size:11.5px; color:var(--ink); background:var(--surface-2); border:1px solid var(--line); border-radius:5px; padding:1px 5px; }
671
+ .brain-agent { font-family:var(--mono); font-size:11px; color:var(--faint); border:1px solid var(--line); border-radius:999px; padding:2px 8px; }
672
+ .brain-agent.is-wired { color:var(--ink); border-color:color-mix(in srgb,var(--signal) 45%,var(--line)); }
673
+ .brain-notice { font-size:12.5px; color:var(--muted); border:1px dashed var(--line); border-radius:var(--radius); padding:8px 12px; margin-bottom:14px; }
674
+ .brain-notice.is-error { color:var(--s-blocked); border-color:color-mix(in srgb,var(--s-blocked) 45%,var(--line)); border-style:solid; }
675
+ .brain-empty { grid-column:1 / -1; color:var(--muted); font-size:13px; line-height:1.55; border:1px dashed var(--line); border-radius:var(--radius); padding:14px 16px; }
676
+ .brain-pending { border:1px solid var(--line); border-left:3px solid var(--signal); border-radius:var(--radius); background:var(--surface); padding:12px 14px; margin-bottom:16px; display:flex; flex-direction:column; gap:10px; }
677
+ .brain-pending-head { display:flex; align-items:center; justify-content:space-between; gap:10px; }
678
+ .brain-pending-title { font-size:14px; margin:0; }
679
+ .brain-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:14px; align-items:start; }
680
+ .brain-card { border:1px solid var(--line); border-radius:var(--radius); background:var(--surface); padding:14px 14px 12px; display:flex; flex-direction:column; gap:8px; min-width:0; }
681
+ .brain-card-head { display:flex; align-items:center; gap:8px; }
682
+ .brain-card-title { font-size:14px; margin:0; }
683
+ .brain-card-hint { color:var(--muted); font-size:12px; margin:0; }
684
+ .brain-list { display:flex; flex-direction:column; gap:6px; }
685
+ .brain-card-empty { padding:4px 0; }
686
+ .brain-row { border:1px solid var(--line); border-left:3px solid var(--cool); border-radius:8px; background:var(--surface-2); padding:8px 10px; min-width:0; }
687
+ .brain-row.from-agent { border-left-color:var(--signal); }
688
+ .brain-row.is-pending { background:var(--surface-2); display:flex; flex-wrap:wrap; align-items:center; gap:6px 10px; }
689
+ .brain-row.is-pending .brain-text { flex:1 1 260px; }
690
+ .brain-text { font-size:13.5px; line-height:1.45; overflow-wrap:anywhere; }
691
+ .brain-row-foot { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-top:5px; }
692
+ .brain-meta { font-family:var(--mono); font-size:10.5px; color:var(--faint); }
693
+ .brain-actions { display:inline-flex; flex-wrap:wrap; gap:6px; }
694
+ .brain-row:not(.is-pending) .brain-actions { opacity:.55; transition:opacity .15s; }
695
+ .brain-row:hover .brain-actions, .brain-row:focus-within .brain-actions { opacity:1; }
696
+ .btn.btn-xs { padding:2px 8px; font-size:11px; }
697
+ .brain-cat-chip { font-family:var(--mono); font-size:10.5px; text-transform:uppercase; letter-spacing:.05em; color:var(--signal); }
698
+ .brain-edit { width:100%; min-height:56px; font-family:inherit; font-size:13.5px; padding:6px 8px; border:1px solid var(--cool); border-radius:6px; background:var(--surface); color:var(--ink); box-sizing:border-box; resize:vertical; }
699
+ .brain-add { display:flex; flex-wrap:wrap; gap:6px; margin-top:2px; }
700
+ .brain-add-input { flex:1 1 160px; min-width:0; font:inherit; font-size:13px; padding:6px 9px; border:1px solid var(--line); border-radius:8px; background:var(--surface-2); color:var(--ink); }
701
+ .brain-add-input:focus { outline:none; border-color:var(--cool); }
702
+ .brain-add-error { flex-basis:100%; font-size:12px; color:var(--s-blocked); }
703
+ .brain-add-error[hidden], .brain-pending[hidden], .brain-notice[hidden] { display:none; }
704
+ .brain-path { font-family:var(--mono); font-size:11px; color:var(--faint); margin:18px 0 0; }
705
+ @media (max-width:820px){ .brain-grid { grid-template-columns:1fr; } .brain-wrap { padding:16px 14px 24px; } }
706
+ @media (hover:none){ .brain-row .brain-actions { opacity:1; } }
661
707
  .btn.danger { color:var(--s-blocked); border-color:color-mix(in srgb,var(--s-blocked) 40%,var(--line)); }
662
708
  .btn.danger:hover { border-color:var(--s-blocked); }
663
709
 
@@ -31,6 +31,13 @@ const ROUTES = [
31
31
  ['POST', /^\/api\/attention\/[^/]+\/promote$/, 'attention.promote', (_u, _b, p) => ({ id: seg(p, 3) })],
32
32
  ['PATCH', /^\/api\/attention\/[^/]+$/, 'attention.update', (_u, b, p) => ({ id: seg(p, 3), patch: b })],
33
33
  ['DELETE', /^\/api\/attention\/[^/]+$/, 'attention.remove', (_u, _b, p) => ({ id: seg(p, 3) })],
34
+ // Second brain — deliberately absent from server/src/relay.js's OP_PERMISSIONS: local only.
35
+ ['GET', '/api/brain', 'brain.read', () => ({})],
36
+ ['POST', '/api/brain', 'brain.add', (_u, b) => b],
37
+ ['POST', '/api/brain/settings', 'brain.settings', (_u, b) => b],
38
+ ['POST', /^\/api\/brain\/[^/]+\/confirm$/, 'brain.confirm', (_u, _b, p) => ({ id: seg(p, 3) })],
39
+ ['PATCH', /^\/api\/brain\/[^/]+$/, 'brain.update', (_u, b, p) => ({ id: seg(p, 3), patch: b })],
40
+ ['DELETE', /^\/api\/brain\/[^/]+$/, 'brain.remove', (_u, _b, p) => ({ id: seg(p, 3) })],
34
41
  ];
35
42
  const matches = (m, p) => (typeof m === 'string' ? m === p : m.test(p));
36
43
  const findRoute = (method, pathname) => ROUTES.find(([m, matcher]) => m === method && matches(matcher, pathname));