spectoflow 0.27.1 → 0.28.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
@@ -117,12 +117,21 @@ detected it falls back to claude + codex.
117
117
 
118
118
  **Empty project** → your agent asks what to build and runs Intake (brainstorm → analysis → spec → plan).
119
119
  **Existing project** → an existing `CLAUDE.md` is preserved as `CLAUDE.md.tomerge` (merged on first run);
120
- existing `plans/*.md` tasks are given stable ids.
120
+ an existing `AGENTS.md` or `GEMINI.md` is kept as-is and gets a short, delimited spectoflow section
121
+ appended (`<!-- spectoflow:start -->` … `<!-- spectoflow:end -->`) pointing to `.spectoflow/SPECTOFLOW.md` —
122
+ `spectoflow update` adds it too to a project installed before that; existing `plans/*.md` tasks are given
123
+ stable ids.
124
+
125
+ **Entry files vs. the brain.** `CLAUDE.md`, `AGENTS.md` and `GEMINI.md` at your project root are thin
126
+ pointers, in the place each agent reads natively. The real brain (intent router, workflow, rules) is
127
+ `.spectoflow/SPECTOFLOW.md`, owned by the framework and refreshed by `spectoflow update`. (Before 0.28 it
128
+ was named `.spectoflow/AGENTS.md`, easy to confuse with the root one — `update` renames it, keeping any
129
+ edits you made, and rewrites the old path in your entry files.)
121
130
 
122
131
  ## Update
123
132
 
124
133
  `init` is idempotent (it never overwrites), so it can't refresh an installed project. `spectoflow
125
- update` refreshes **framework-owned** files (engine, `AGENTS.md`, `capabilities.md`, `policy.md`,
134
+ update` refreshes **framework-owned** files (engine, `SPECTOFLOW.md`, `capabilities.md`, `policy.md`,
126
135
  default agents & skills) to the CLI's version — retiring the project's own vendored dashboard folder
127
136
  along the way for anyone updating from before v0.24 — while **preserving your work** —
128
137
  `config.json`, `workflow.md`, `specs/`, `plans/`, and any agent/skill you created or edited are never
@@ -186,9 +195,65 @@ spectoflow dashboard # → http://localhost:4319 (or --port=
186
195
  dashboard already running on the port). `spectoflow status` tells you whether one is up. Zero
187
196
  dependencies, updates live via SSE + file watching.
188
197
 
189
- Want your own hosted, online dashboard instead of (or alongside) the local one? `server/` is a
190
- separate, self-hostable relay with a ready-made Docker deployment path — see
191
- [`server/docs/deploy-vps.md`](server/docs/deploy-vps.md).
198
+ ### One hub, every project
199
+
200
+ There is only ever **one dashboard process on your machine**, no matter how many projects you have.
201
+ `spectoflow dashboard`, run from *any* initialized project, does two things: it registers that
202
+ project, and it makes sure the hub is running — starting it if it's the first one to ask, or simply
203
+ joining an already-running hub otherwise. So the very first `spectoflow dashboard` you ever run
204
+ starts the hub; every one after that (from other projects) just adds a card to the same page.
205
+
206
+ ```
207
+ $ spectoflow dashboard (in todo-list-v2/)
208
+ no hub found → starts one → http://localhost:4319
209
+ │
210
+ └── card: "todo-list-v2"
211
+
212
+ $ spectoflow dashboard (in my-other-app/, later, or another day)
213
+ hub already running → just joins it
214
+ │
215
+ └── card: "my-other-app"
216
+
217
+ → open http://localhost:4319 in your browser: both projects, one page
218
+ ```
219
+
220
+ That's the whole local setup — no login, no token, nothing to configure. **The rest of this section
221
+ is entirely optional.**
222
+
223
+ ### Going online (optional): local hub vs. relay server
224
+
225
+ `spectoflow dashboard login/publish` are for one specific, separate need: opening a project's
226
+ dashboard from **another device**, or sharing it with someone else. They talk to `server/` — a
227
+ different application in this repo, not part of the local hub — that you (or someone) hosts
228
+ somewhere reachable (see [`server/docs/deploy-vps.md`](server/docs/deploy-vps.md), or run it
229
+ locally to try it, per [`server/README.md`](server/README.md)). Nothing about your local hub changes;
230
+ it grows one extra, optional connection outward:
231
+
232
+ ```
233
+ YOUR MACHINE THE RELAY SERVER (server/)
234
+ local hub · localhost:4319 hosted by you or someone else
235
+
236
+ [todo-list-v2] ── published ──────────────► only "published" projects
237
+ [my-other-app] ── NOT published, stays local ever show up here
238
+
239
+ ▲
240
+ │ one-time, per machine:
241
+ │ $ spectoflow dashboard login --url=<relay-url> --token=<spf_…>
242
+ │
243
+ the token is minted ON THE RELAY, not by you:
244
+ $ node server/cli.js token create (run by whoever administers it)
245
+
246
+ once logged in and published → any browser, anywhere, can open it
247
+ ```
248
+
249
+ | Command | Runs where | What it does |
250
+ | --- | --- | --- |
251
+ | `spectoflow dashboard` | your machine | start/join the local hub, register the current project |
252
+ | `node server/cli.js token create` | on the relay server | mint a login token for a machine |
253
+ | `spectoflow dashboard login --url=… --token=…` | your machine | link this machine to that relay (once) |
254
+ | `spectoflow dashboard publish` | your machine | make the *current* project visible through the relay |
255
+ | `spectoflow dashboard unpublish` | your machine | take it back offline |
256
+ | `spectoflow dashboard logout` | your machine | unlink the machine entirely |
192
257
 
193
258
  The header bar always shows the brand, the **active agent**, autonomy mode, language, a global-progress
194
259
  meter, a sync dot, and a **Run** quick-action. Thirteen tabs — and **which ones you see, and in what
@@ -290,7 +355,7 @@ INVEST, Playwright E2E, Conventional Commits, and more — not generic one-liner
290
355
  ### Clarify before acting
291
356
 
292
357
  spectoflow is an **expert analyst, not an order-taker**. When a request is vague ("login displays
293
- badly, users can't sign in"), an always-on **Clarify reflex** — in the agent's memory (`AGENTS.md`)
358
+ badly, users can't sign in"), an always-on **Clarify reflex** — in the agent's memory (`SPECTOFLOW.md`)
294
359
  and backed by the `clarify` skill — reflects it back and asks **one targeted question at a time**,
295
360
  each with a recommendation anchored in the project's goals and best practices, until the need is
296
361
  crisp; then it runs the normal workflow. It's additive: it feeds the router, never replaces it, and
package/bin/spectoflow.js CHANGED
@@ -133,7 +133,7 @@ async function update() {
133
133
  const r = require('../lib/update').runUpdate({ projectRoot: root, templatesDir: TPL, version: VERSION, dryRun, force });
134
134
 
135
135
  const from = r.fromVersion || 'unknown';
136
- const changed = r.refreshed.length + r.created.length + r.adopted.length + r.newSidecar.length + r.forced.length + r.removed.length + r.migration.movedViews.length;
136
+ const changed = r.refreshed.length + r.created.length + r.adopted.length + r.newSidecar.length + r.forced.length + r.removed.length + r.migration.movedViews.length + r.pointers.appended.length + r.pointers.repointed.length + r.migration.repointedUserFiles.length + (r.migration.renamedBrain ? 1 : 0);
137
137
  const row = (sym, label, list, painter, note) => {
138
138
  if (!list.length) return;
139
139
  const n = c.dim(String(list.length).padStart(2));
@@ -150,6 +150,9 @@ async function update() {
150
150
  row(c.y('!'), '.new', r.newSidecar, c.y, 'you edited these — new version saved as *.new, merge by hand (or re-run with --force)');
151
151
  row(c.dim('−'), 'removed', r.removed, c.dim, 'no longer part of the kit (the dashboard lives in the spectoflow package now)');
152
152
  row(c.y('!'), 'kept', r.kept, c.y, 'you modified these and they are no longer part of the kit — delete them yourself when ready');
153
+ if (r.migration.renamedBrain) console.log(` ${c.cy('→')} ${c.cy('renamed'.padEnd(9))} ${c.dim(' 1')} ${c.dim('.spectoflow/AGENTS.md → .spectoflow/SPECTOFLOW.md (the brain; your edits, if any, came along)')}`);
154
+ row(c.cy('→'), 'repointed', [...r.pointers.repointed, ...r.migration.repointedUserFiles.map((f) => '.spectoflow/' + f)], c.cy, null);
155
+ row(c.cy('+'), 'linked', r.pointers.appended, c.cy, `${r.pointers.appended.join(', ')} existed without a pointer — a spectoflow section was appended`);
153
156
  if (r.migration.movedViews.length) console.log(` ${c.cy('→')} ${c.cy('views'.padEnd(9))} ${c.dim(String(r.migration.movedViews.length).padStart(2))} ${c.dim('custom views moved to .spectoflow/dashboards/')}`);
154
157
  r.migration.conflicts.forEach((f) => console.log(` ${c.y('!')} ${c.y('conflict'.padEnd(9))} ${c.dim(`dashboards/${f} already exists — the old copy stays in dashboard/custom/ for you to merge`)}`));
155
158
  if (r.legacyLeftovers.length) console.log(` ${c.y('!')} ${c.dim('this project has no install manifest, so nothing was deleted. Safe to remove by hand: ' + r.legacyLeftovers.map((p) => '.spectoflow/' + p).join(', '))}`);
@@ -611,10 +614,11 @@ const HELP = {
611
614
  ${c.dim('copilot, amazon-q, droid, auggie, goose, kimi')}) and writes their entry shims; override
612
615
  with ${c.g('--agent=claude,codex')}. Also wires ${c.bold('Playwright MCP')} into the project's
613
616
  ${c.dim('.mcp.json')} (idempotent — never touches an existing entry).
614
- ${c.dim('An existing CLAUDE.md is preserved as CLAUDE.md.tomerge for you to merge on first run.')}
617
+ ${c.dim('An existing CLAUDE.md is preserved as CLAUDE.md.tomerge for you to merge on first run;')}
618
+ ${c.dim('an existing AGENTS.md/GEMINI.md is kept and gets a spectoflow pointer section appended.')}
615
619
  ${c.dim('Full list with docs links: the dashboard\'s Documentation tab, or the README.')}`,
616
620
  update: `${c.bold('spectoflow update')} ${c.dim('[--dry-run] [--force|-f]')}\n
617
- Refresh framework-owned files (engine, default agents & skills, AGENTS.md, policy…)
621
+ Refresh framework-owned files (engine, default agents & skills, SPECTOFLOW.md, policy…)
618
622
  to this CLI's version, ${c.bold('preserving your work')}: config.json, workflow.md, specs/, plans/
619
623
  and any agent/skill you edited are never overwritten (an edited file's new version lands as
620
624
  ${c.dim('*.new')} for you to merge). ${c.g('--dry-run')} previews without writing.
package/lib/adapters.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
  /*
3
- * Per-agent adapters. The canonical brain is .spectoflow/AGENTS.md. Each agent needs a thin entry
3
+ * Per-agent adapters. The canonical brain is .spectoflow/SPECTOFLOW.md. Each agent needs a thin entry
4
4
  * file in the place IT reads, pointing back to the canonical brain — so the framework stays in
5
5
  * .spectoflow/ (swappable, no pollution) while every agent still finds its native entry point.
6
6
  * (Same pattern OpenSpec uses to support claude/codex/cursor/gemini/…)
@@ -14,7 +14,7 @@ const path = require('path');
14
14
 
15
15
  const CLAUDE_MD = `# CLAUDE.md — spectoflow
16
16
 
17
- This project uses **spectoflow**. **Read \`.spectoflow/AGENTS.md\` and follow it** as your operating
17
+ This project uses **spectoflow**. **Read \`.spectoflow/SPECTOFLOW.md\` and follow it** as your operating
18
18
  instructions (intent router, workflow, standing rules).
19
19
 
20
20
  > If \`CLAUDE.md.tomerge\` exists next to this file, it is your previous CLAUDE.md, preserved on
@@ -23,7 +23,7 @@ instructions (intent router, workflow, standing rules).
23
23
 
24
24
  **Be an expert analyst, not an order-taker.** When a request is ambiguous, **clarify before acting**:
25
25
  reflect it back and ask **one targeted question at a time** (each with a recommendation) until the need
26
- is clear — then execute. See the Clarify reflex in \`.spectoflow/AGENTS.md\`.
26
+ is clear — then execute. See the Clarify reflex in \`.spectoflow/SPECTOFLOW.md\`.
27
27
 
28
28
  - Command: \`/spectoflow\` (\`init\` / \`status\` / or just a request).
29
29
  - Dashboard: \`spectoflow dashboard\` → http://localhost:4319
@@ -32,30 +32,30 @@ is clear — then execute. See the Clarify reflex in \`.spectoflow/AGENTS.md\`.
32
32
 
33
33
  const ROOT_AGENTS_MD = `# AGENTS.md — spectoflow
34
34
 
35
- This project uses **spectoflow**. **Read \`.spectoflow/AGENTS.md\` and follow it** as your operating
35
+ This project uses **spectoflow**. **Read \`.spectoflow/SPECTOFLOW.md\` and follow it** as your operating
36
36
  instructions. Artifacts are markdown in \`specs/\` and \`plans/\`; the workflow is \`.spectoflow/workflow.md\`.
37
37
 
38
38
  **Be an expert analyst, not an order-taker.** When a request is ambiguous, **clarify before acting**:
39
39
  reflect it back and ask **one targeted question at a time** (each with a recommendation) until the need
40
- is clear — then execute. See the Clarify reflex in \`.spectoflow/AGENTS.md\`.
40
+ is clear — then execute. See the Clarify reflex in \`.spectoflow/SPECTOFLOW.md\`.
41
41
  `;
42
42
 
43
43
  const GEMINI_MD = `# GEMINI.md — spectoflow
44
44
 
45
- This project uses **spectoflow**. **Read \`.spectoflow/AGENTS.md\` and follow it** as your operating
45
+ This project uses **spectoflow**. **Read \`.spectoflow/SPECTOFLOW.md\` and follow it** as your operating
46
46
  instructions (intent router, workflow, standing rules). Artifacts are markdown in \`specs/\` and
47
47
  \`plans/\`; the workflow is \`.spectoflow/workflow.md\`.
48
48
 
49
49
  **Be an expert analyst, not an order-taker.** When a request is ambiguous, **clarify before acting**:
50
50
  reflect it back and ask **one targeted question at a time** (each with a recommendation) until the need
51
- is clear — then execute. See the Clarify reflex in \`.spectoflow/AGENTS.md\`.
51
+ is clear — then execute. See the Clarify reflex in \`.spectoflow/SPECTOFLOW.md\`.
52
52
  `;
53
53
 
54
54
  const SLASH_CMD = `---
55
55
  description: spectoflow — spec-driven control (init / status / or just a request)
56
56
  ---
57
57
 
58
- Read \`.spectoflow/AGENTS.md\` and \`.spectoflow/config.json\` first.
58
+ Read \`.spectoflow/SPECTOFLOW.md\` and \`.spectoflow/config.json\` first.
59
59
 
60
60
  Argument: \`$ARGUMENTS\`
61
61
 
@@ -63,7 +63,7 @@ Argument: \`$ARGUMENTS\`
63
63
  \`specs/\` and \`plans/\` are empty, greet me, state the mode, and start Intake (brainstorm → analysis
64
64
  → spec → plan) by asking what I want to build.
65
65
  - \`status\`: summarize progress from \`plans/*.md\` and \`.spectoflow/runtime.json\`.
66
- - otherwise: treat \`$ARGUMENTS\` as a request and run the Router in \`.spectoflow/AGENTS.md\`.
66
+ - otherwise: treat \`$ARGUMENTS\` as a request and run the Router in \`.spectoflow/SPECTOFLOW.md\`.
67
67
  `;
68
68
 
69
69
  // Priority order = which agent becomes the default when several are detected.
@@ -224,24 +224,68 @@ const REGISTRY = [
224
224
 
225
225
  const byId = (id) => REGISTRY.find((a) => a.id === id);
226
226
 
227
- function writeIfAbsent(fp, content) {
228
- if (fs.existsSync(fp)) return false;
229
- fs.mkdirSync(path.dirname(fp), { recursive: true });
230
- fs.writeFileSync(fp, content);
231
- return true;
227
+ // Root memory files an agent loads natively. A project often already has one (AGENTS.md is shared
228
+ // by most tools and usually team-maintained), so we never replace it: we append a delimited pointer
229
+ // section instead, otherwise the agent reading it would never be told spectoflow exists.
230
+ const MEMORY_FILES = ['AGENTS.md', 'GEMINI.md', 'CLAUDE.md'];
231
+ const POINTER_START = '<!-- spectoflow:start -->';
232
+ const POINTER_END = '<!-- spectoflow:end -->';
233
+ const POINTER_BLOCK = `${POINTER_START}\n${ROOT_AGENTS_MD.replace(/^# .*\n/, '## spectoflow\n').trimEnd()}\n${POINTER_END}\n`;
234
+
235
+ // The brain was `.spectoflow/AGENTS.md` before 0.28 — renamed so it can't be mistaken for the
236
+ // project-root AGENTS.md. Pointers written back then are rewritten in place, never duplicated.
237
+ const BRAIN = '.spectoflow/SPECTOFLOW.md';
238
+ const LEGACY_BRAIN = '.spectoflow/AGENTS.md';
239
+ const ENTRY_FILES = [...MEMORY_FILES, '.claude/commands/spectoflow.md'];
240
+
241
+ // Bring one existing entry file up to date: rewrite a legacy pointer, then (memory files only)
242
+ // append the pointer section if it still has none. Returns what it did (or would do, on dryRun).
243
+ function linkEntry(fp, rel, dryRun) {
244
+ const before = fs.readFileSync(fp, 'utf8');
245
+ let text = before.split(LEGACY_BRAIN).join(BRAIN);
246
+ const repointed = text !== before;
247
+ const appended = MEMORY_FILES.includes(rel) && !text.includes(BRAIN);
248
+ if (appended) text = text.replace(/\s*$/, '') + '\n\n' + POINTER_BLOCK;
249
+ if ((repointed || appended) && !dryRun) fs.writeFileSync(fp, text);
250
+ return { repointed, appended };
232
251
  }
233
252
 
234
253
  // Write the native entry-file shims for each selected agent. Shared files (AGENTS.md across
235
- // codex/cursor) are written once — writeIfAbsent dedupes. Returns the relative paths written.
254
+ // codex/cursor) are handled once. An entry file that already exists is linked instead (linkEntry).
236
255
  function generate(projectRoot, agents) {
237
256
  const list = (agents && agents.length ? agents : ['claude', 'codex']).map(byId).filter(Boolean);
238
- const written = [];
257
+ const written = [], appended = [], repointed = [], seen = new Set();
239
258
  for (const a of list) {
240
259
  for (const e of a.entries) {
241
- if (writeIfAbsent(path.join(projectRoot, e.path), e.content)) written.push(e.path);
260
+ if (seen.has(e.path)) continue;
261
+ seen.add(e.path);
262
+ const fp = path.join(projectRoot, e.path);
263
+ if (!fs.existsSync(fp)) {
264
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
265
+ fs.writeFileSync(fp, e.content);
266
+ written.push(e.path);
267
+ continue;
268
+ }
269
+ const r = linkEntry(fp, e.path, false);
270
+ if (r.appended) appended.push(e.path);
271
+ if (r.repointed) repointed.push(e.path);
242
272
  }
243
273
  }
244
- return written;
274
+ return { written, appended, repointed };
275
+ }
276
+
277
+ // For `update`: link every entry file that exists — never creates one for an agent the project may
278
+ // not use.
279
+ function ensurePointers(projectRoot, dryRun = false) {
280
+ const out = { appended: [], repointed: [] };
281
+ for (const rel of ENTRY_FILES) {
282
+ const fp = path.join(projectRoot, rel);
283
+ if (!fs.existsSync(fp)) continue;
284
+ const r = linkEntry(fp, rel, dryRun);
285
+ if (r.appended) out.appended.push(rel);
286
+ if (r.repointed) out.repointed.push(rel);
287
+ }
288
+ return out;
245
289
  }
246
290
 
247
291
  // { id: runner } defaults for the given agents — used to seed config.runners at init.
@@ -260,4 +304,4 @@ function knownAgents() {
260
304
  return REGISTRY.map((a) => ({ id: a.id, label: a.label, bin: a.detect.bin, dirs: a.detect.dirs || [], runner: a.runner, headless: a.headless, docsUrl: a.docsUrl }));
261
305
  }
262
306
 
263
- module.exports = { generate, defaultRunners, REGISTRY, knownAgents };
307
+ module.exports = { generate, ensurePointers, defaultRunners, REGISTRY, knownAgents, BRAIN, LEGACY_BRAIN };
@@ -1354,7 +1354,7 @@ function renderCustomDashboards(){
1354
1354
 
1355
1355
  // ---- Settings → Customize: add dashboards/skills/agents by description, or "Auto" -------------
1356
1356
  // 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
1357
+ // client-side. It just constructs a plain-language prompt (recognized by SPECTOFLOW.md's Router, which
1358
1358
  // hands it to framework-curator) and sends it through the SAME /api/run + group-chat pipeline every
1359
1359
  // other "Run" already uses, then jumps to Chat so the requester watches it happen and can answer any
1360
1360
  // clarifying question there — no separate conversational UI to build or keep in sync.
@@ -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
@@ -93,7 +93,7 @@ en: {
93
93
  '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
94
  'chat.widgetTitle':'Run an agent','chat.widgetSub':'Quick access · full view in the Chat tab',
95
95
  '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.',
96
+ '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
97
  'chat.inputPlaceholder':'e.g. Add a login feature with email + password',
98
98
  'chat.orchestrateTitle':'Walk the enabled workflow','chat.summarizeTitle':'Condense the recent activity into a summary','chat.clearTitle':'Clear the chat log',
99
99
  'chat.warn':'⚠ Launches a real agent (<code>config.json → runners</code>) that can modify files &amp; run commands.','chat.running':'Agent running…',
@@ -212,7 +212,7 @@ fr: {
212
212
  '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
213
  'chat.widgetTitle':'Lancer un agent','chat.widgetSub':'Accès rapide · vue complète dans l’onglet Chat',
214
214
  '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.',
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 → .spectoflow/SPECTOFLOW.md</code>) et met le tableau à jour en direct.',
216
216
  'chat.inputPlaceholder':'ex. Ajouter une fonctionnalité de connexion par email + mot de passe',
217
217
  '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
218
  '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…',
@@ -331,7 +331,7 @@ es: {
331
331
  '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
332
  'chat.widgetTitle':'Ejecutar un agente','chat.widgetSub':'Acceso rápido · vista completa en la pestaña Chat',
333
333
  '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.',
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 → .spectoflow/SPECTOFLOW.md</code>) y actualiza el tablero en vivo.',
335
335
  'chat.inputPlaceholder':'p. ej. Añadir un inicio de sesión con email + contraseña',
336
336
  'chat.orchestrateTitle':'Recorrer el workflow activado','chat.summarizeTitle':'Condensar la actividad reciente en un resumen','chat.clearTitle':'Borrar el registro del chat',
337
337
  'chat.warn':'⚠ Lanza un agente real (<code>config.json → runners</code>) que puede modificar archivos y ejecutar comandos.','chat.running':'Agente en ejecución…',
@@ -450,7 +450,7 @@ de: {
450
450
  '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
451
  'chat.widgetTitle':'Agenten ausführen','chat.widgetSub':'Schnellzugriff · vollständige Ansicht im Chat-Tab',
452
452
  '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.',
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 → .spectoflow/SPECTOFLOW.md</code>) und aktualisiert das Board live.',
454
454
  'chat.inputPlaceholder':'z. B. Login mit E-Mail + Passwort hinzufügen',
455
455
  'chat.orchestrateTitle':'Den aktivierten Workflow durchlaufen','chat.summarizeTitle':'Die letzten Aktivitäten zu einer Zusammenfassung verdichten','chat.clearTitle':'Chat-Verlauf löschen',
456
456
  'chat.warn':'⚠ Startet einen echten Agenten (<code>config.json → runners</code>), der Dateien ändern und Befehle ausführen kann.','chat.running':'Agent läuft…',
@@ -569,7 +569,7 @@ pt: {
569
569
  '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
570
  'chat.widgetTitle':'Executar um agente','chat.widgetSub':'Acesso rápido · vista completa no separador Chat',
571
571
  '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.',
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 → .spectoflow/SPECTOFLOW.md</code>) e atualiza o painel em direto.',
573
573
  'chat.inputPlaceholder':'ex. Adicionar login com email + palavra-passe',
574
574
  'chat.orchestrateTitle':'Percorrer o workflow ativado','chat.summarizeTitle':'Condensar a atividade recente num resumo','chat.clearTitle':'Limpar o registo do chat',
575
575
  'chat.warn':'⚠ Inicia um agente real (<code>config.json → runners</code>) que pode alterar ficheiros e executar comandos.','chat.running':'Agente em execução…',
@@ -688,7 +688,7 @@ it: {
688
688
  '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
689
  'chat.widgetTitle':'Avvia un agente','chat.widgetSub':'Accesso rapido · vista completa nella scheda Chat',
690
690
  '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.',
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 → .spectoflow/SPECTOFLOW.md</code>) e aggiorna la bacheca in diretta.',
692
692
  'chat.inputPlaceholder':'es. Aggiungi un login con email + password',
693
693
  'chat.orchestrateTitle':'Percorri il workflow attivato','chat.summarizeTitle':'Condensa l’attività recente in un riassunto','chat.clearTitle':'Cancella il registro della chat',
694
694
  'chat.warn':'⚠ Avvia un agente reale (<code>config.json → runners</code>) che può modificare file ed eseguire comandi.','chat.running':'Agente in esecuzione…',
@@ -318,7 +318,7 @@
318
318
  </div>
319
319
  <div class="chat-tab-log" id="chatTabLog">
320
320
  <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>
321
+ (<code>CLAUDE.md / AGENTS.md → .spectoflow/SPECTOFLOW.md</code>) and updates the board live.</div>
322
322
  </div>
323
323
  <!-- Toolbar right above the input, not up in the header — this is where you're looking
324
324
  when you reach for Summarize/Clear or switch which agent the next message runs on. -->
@@ -455,7 +455,7 @@
455
455
  </div>
456
456
  <div class="chat-log" id="chatLog">
457
457
  <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>
458
+ (<code>CLAUDE.md / AGENTS.md → .spectoflow/SPECTOFLOW.md</code>) and updates the board live.</div>
459
459
  </div>
460
460
  <div class="chat-toolbar">
461
461
  <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>
package/lib/init.js CHANGED
@@ -99,7 +99,9 @@ function runInit({ target, templatesDir, version, agentsArg, defaults }) {
99
99
  const added = normalizePlans(target, cfg);
100
100
  if (added) notes.push(`Normalized ${added} existing task(s) with stable ids.`);
101
101
 
102
- const written = adapters.generate(target, agents);
102
+ const { written, appended, repointed } = adapters.generate(target, agents);
103
+ appended.forEach((f) => notes.push(`Existing ${f} kept — a spectoflow section was appended to it so your agent finds ${adapters.BRAIN}.`));
104
+ repointed.forEach((f) => notes.push(`${f} pointed to ${adapters.LEGACY_BRAIN} — now ${adapters.BRAIN}.`));
103
105
 
104
106
  const mcpTargets = [path.join(target, '.mcp.json')];
105
107
  if (agents.includes('cursor')) mcpTargets.push(path.join(target, '.cursor', 'mcp.json'));
package/lib/update.js CHANGED
@@ -17,6 +17,7 @@ const fs = require('fs');
17
17
  const path = require('path');
18
18
  const ownership = require('./ownership');
19
19
  const manifest = require('./manifest');
20
+ const adapters = require('./adapters');
20
21
 
21
22
  function toDisk(sf, rel) {
22
23
  return path.join(sf, rel.split('/').join(path.sep));
@@ -29,7 +30,7 @@ const LEGACY_LEFTOVERS = ['dashboard', 'lib/store.js', 'lib/agents-registry.js',
29
30
  // Data migration (0.23 → 0.24): custom views out of the old dashboard folder, the per-project lock
30
31
  // and its .gitignore line gone. Runs before any removal, is idempotent, and never overwrites.
31
32
  function migrateProjectData(projectRoot, sf, dryRun) {
32
- const r = { movedViews: [], conflicts: [], removedLock: false, gitignoreCleaned: false };
33
+ const r = { movedViews: [], conflicts: [], removedLock: false, gitignoreCleaned: false, renamedBrain: false, repointedUserFiles: [] };
33
34
  const oldDir = path.join(sf, 'dashboard', 'custom'), newDir = path.join(sf, 'dashboards');
34
35
  if (fs.existsSync(oldDir)) {
35
36
  for (const f of fs.readdirSync(oldDir).filter((x) => x.endsWith('.json')).sort()) {
@@ -53,6 +54,42 @@ function migrateProjectData(projectRoot, sf, dryRun) {
53
54
  return r;
54
55
  }
55
56
 
57
+ // 0.28: the brain `AGENTS.md` became `SPECTOFLOW.md`. Treated as a move, not "retire + create": the
58
+ // file (edits included) and its manifest baseline follow the new name, so the normal matrix below
59
+ // then refreshes it if untouched or offers a .new if edited. In dry-run nothing moves; `readFrom`
60
+ // tells the matrix to read the brain from its old place instead.
61
+ const BRAIN = 'SPECTOFLOW.md', LEGACY_BRAIN = 'AGENTS.md';
62
+ function moveBrain(sf, templatesDir, baseline, dryRun) {
63
+ const kitHasBrain = fs.existsSync(toDisk(templatesDir, BRAIN)) && !fs.existsSync(toDisk(templatesDir, LEGACY_BRAIN));
64
+ const from = toDisk(sf, LEGACY_BRAIN), to = toDisk(sf, BRAIN);
65
+ if (!kitHasBrain || !fs.existsSync(from) || fs.existsSync(to)) return null;
66
+ if (baseline[LEGACY_BRAIN] !== undefined) { baseline[BRAIN] = baseline[LEGACY_BRAIN]; delete baseline[LEGACY_BRAIN]; }
67
+ if (!dryRun) fs.renameSync(from, to);
68
+ return { readFrom: dryRun ? from : to };
69
+ }
70
+
71
+ // Agent-read markdown the user owns (generated agents/skills) may name the old brain path. Kit files
72
+ // are skipped: the matrix owns them, and rewriting one would make it look user-edited.
73
+ function repointUserFiles(sf, templatesDir, dryRun) {
74
+ const kit = new Set(ownership.listFrameworkFiles(templatesDir));
75
+ const changed = [];
76
+ const walk = (rel) => {
77
+ const abs = toDisk(sf, rel);
78
+ if (!fs.existsSync(abs)) return;
79
+ for (const e of fs.readdirSync(abs, { withFileTypes: true })) {
80
+ const child = rel + '/' + e.name;
81
+ if (e.isDirectory()) { walk(child); continue; }
82
+ if (!e.name.endsWith('.md') || kit.has(child)) continue;
83
+ const fp = toDisk(sf, child), text = fs.readFileSync(fp, 'utf8');
84
+ if (!text.includes('.spectoflow/' + LEGACY_BRAIN)) continue;
85
+ changed.push(child);
86
+ if (!dryRun) fs.writeFileSync(fp, text.split('.spectoflow/' + LEGACY_BRAIN).join('.spectoflow/' + BRAIN));
87
+ }
88
+ };
89
+ walk('agents'); walk('skills');
90
+ return changed;
91
+ }
92
+
56
93
  // Remove `fp`, then every now-empty parent up to (not including) `stop`.
57
94
  function removeAndPrune(fp, stop) {
58
95
  fs.unlinkSync(fp);
@@ -81,10 +118,13 @@ function runUpdate({ projectRoot, templatesDir, version, dryRun = false, force =
81
118
  kept: [],
82
119
  migration: null,
83
120
  legacyLeftovers: [],
121
+ pointers: [],
84
122
  };
85
- const baseline = (prev && prev.files) || {};
123
+ const baseline = { ...((prev && prev.files) || {}) };
86
124
  const nextFiles = {}; // manifest to write after this run
87
125
  report.migration = migrateProjectData(projectRoot, sf, dryRun);
126
+ const brainMove = moveBrain(sf, templatesDir, baseline, dryRun);
127
+ report.migration.renamedBrain = !!brainMove;
88
128
 
89
129
  const write = (fp, buf) => {
90
130
  if (dryRun) return;
@@ -95,7 +135,7 @@ function runUpdate({ projectRoot, templatesDir, version, dryRun = false, force =
95
135
  for (const rel of ownership.listFrameworkFiles(templatesDir)) {
96
136
  const newBuf = fs.readFileSync(toDisk(templatesDir, rel));
97
137
  const newHash = manifest.sha256(newBuf);
98
- const diskPath = toDisk(sf, rel);
138
+ const diskPath = rel === BRAIN && brainMove ? brainMove.readFrom : toDisk(sf, rel);
99
139
  const base = baseline[rel]; // undefined for legacy / brand-new files
100
140
 
101
141
  if (!fs.existsSync(diskPath)) {
@@ -145,6 +185,9 @@ function runUpdate({ projectRoot, templatesDir, version, dryRun = false, force =
145
185
  for (const rel of LEGACY_LEFTOVERS) if (fs.existsSync(toDisk(sf, rel))) report.legacyLeftovers.push(rel);
146
186
  }
147
187
 
188
+ report.pointers = adapters.ensurePointers(projectRoot, dryRun);
189
+ report.migration.repointedUserFiles = repointUserFiles(sf, templatesDir, dryRun);
190
+
148
191
  if (!dryRun) manifest.writeManifest(sf, { version, files: nextFiles });
149
192
  return report;
150
193
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectoflow",
3
- "version": "0.27.1",
3
+ "version": "0.28.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",
@@ -7,17 +7,17 @@ workflow, and tracks everything as **markdown artifacts** you can diff and own.
7
7
 
8
8
  Everything the framework needs lives here in `.spectoflow/`, so your project root stays clean and the
9
9
  framework is swappable/updatable. Your per-agent entry files (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`)
10
- sit at the project root and just point back here.
10
+ sit at the project root and just point back to `SPECTOFLOW.md`, here.
11
11
 
12
12
  ## How you use it
13
13
 
14
14
  - **Just say what you want** to your agent ("add a login feature", "fix T-042"). The router in
15
- `AGENTS.md` classifies it (quick / standard / major), gates it by your **mode** and **policy**, and
15
+ `SPECTOFLOW.md` classifies it (quick / standard / major), gates it by your **mode** and **policy**, and
16
16
  runs the matching workflow — no ceremonial command.
17
17
  - **When your ask is vague, it clarifies first.** spectoflow behaves like an expert analyst, not an
18
18
  order-taker: on an ambiguous request ("login displays badly") it reflects it back and asks **one
19
19
  targeted question at a time** (each with a recommendation) until the need is crisp, then executes
20
- (skill `clarify`, wired into the agent's memory in `AGENTS.md`).
20
+ (skill `clarify`, wired into the agent's memory in `SPECTOFLOW.md`).
21
21
  - **Watch it live** in the dashboard (it starts in the background and hands the prompt back):
22
22
  ```
23
23
  spectoflow dashboard # → http://localhost:4319
@@ -58,7 +58,7 @@ Your **artifacts are markdown, and they live at the project root, not in here**:
58
58
 
59
59
  | Path | What it is |
60
60
  |------|------------|
61
- | `AGENTS.md` | **The brain** — the intent router, the modes, and the standing rules your agent follows. |
61
+ | `SPECTOFLOW.md` | **The brain** — the intent router, the modes, and the standing rules your agent follows. |
62
62
  | `workflow.md` | The **single** workflow definition (the pipeline steps and their capability/skill). |
63
63
  | `capabilities.md` | The capability palette (intake, analysis, planning, implementation, testing, quality, security, governance…) and how it adapts to the project type. |
64
64
  | `policy.md` | **Non-negotiable gates** — actions that need explicit human approval regardless of mode (prod deploy, destructive migration, security change, spend, source-of-truth drift at done/Major). |
@@ -1,7 +1,7 @@
1
1
  # spectoflow — project brain (read fully at session start)
2
2
 
3
- > Agent-agnostic. Any agent reading this — Claude Code (`CLAUDE.md` points here), Codex/Cursor
4
- > (`AGENTS.md`), etc. — knows how to behave. Keep it lean; details live in the files it points to.
3
+ > Agent-agnostic. Any agent reading this — Claude Code (`CLAUDE.md` points here), Codex/Cursor and most
4
+ > others (the project-root `AGENTS.md` points here), etc. — knows how to behave. Keep it lean; details live in the files it points to.
5
5
 
6
6
  ## What spectoflow is
7
7
 
@@ -12,7 +12,7 @@ surfaces drift to the Attention tab; it gates only at `done`/Major (see `policy.
12
12
  `clarify` is a **reflex under `intake`, not a workflow step** either: on *any* ambiguous request the
13
13
  agent reflects it back and asks **one targeted question at a time** (each with a recommendation) until
14
14
  the need is crisp, then proceeds — it feeds the workflow, never replaces it. See `skills/clarify` and
15
- the Clarify step in `AGENTS.md`.
15
+ the Clarify step in `SPECTOFLOW.md`.
16
16
 
17
17
  `customization` is also **not a workflow step** — it is triggered explicitly, either from the
18
18
  dashboard's Settings → Customize page or by a direct request ("add a dashboard for…", "create a skill
@@ -10,7 +10,7 @@ standard: requirements elicitation
10
10
 
11
11
  Turn a vague request into a crisp, agreed need **before** classifying or acting — the way a good
12
12
  analyst does: reflect, ask the sharpest question, listen, repeat. This is a **reflex**, always in the
13
- agent's memory (see the Clarify step in `AGENTS.md`), not a workflow stage — it fires on *any*
13
+ agent's memory (see the Clarify step in `SPECTOFLOW.md`), not a workflow stage — it fires on *any*
14
14
  request, including bug reports and change requests on an existing project ("the login page doesn't
15
15
  display well, users can't sign in").
16
16
 
@@ -96,7 +96,7 @@ list.
96
96
 
97
97
  ### 6. Resolve capability collisions explicitly
98
98
 
99
- `.spectoflow/AGENTS.md`'s routing assumes one agent per capability unless a `priority` is set (see the
99
+ `.spectoflow/SPECTOFLOW.md`'s routing assumes one agent per capability unless a `priority` is set (see the
100
100
  front-matter rules in `docs/agents-skills-standard.md`). If the chosen capability already has an
101
101
  agent, either pick a different, more precise capability for this role, or set `priority` deliberately
102
102
  and tell the user which agent now wins ties — never leave two agents silently competing for the same