spectoflow 0.27.1 → 0.27.2

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,7 +117,14 @@ 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/AGENTS.md` —
122
+ `spectoflow update` adds it too to a project installed before that; existing `plans/*.md` tasks are given
123
+ stable ids.
124
+
125
+ **Two `AGENTS.md`, on purpose.** The one at your project root is a thin pointer, in the place each agent
126
+ reads natively. The real brain (intent router, workflow, rules) is `.spectoflow/AGENTS.md`, owned by the
127
+ framework and refreshed by `spectoflow update`.
121
128
 
122
129
  ## Update
123
130
 
@@ -186,9 +193,65 @@ spectoflow dashboard # → http://localhost:4319 (or --port=
186
193
  dashboard already running on the port). `spectoflow status` tells you whether one is up. Zero
187
194
  dependencies, updates live via SSE + file watching.
188
195
 
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).
196
+ ### One hub, every project
197
+
198
+ There is only ever **one dashboard process on your machine**, no matter how many projects you have.
199
+ `spectoflow dashboard`, run from *any* initialized project, does two things: it registers that
200
+ project, and it makes sure the hub is running — starting it if it's the first one to ask, or simply
201
+ joining an already-running hub otherwise. So the very first `spectoflow dashboard` you ever run
202
+ starts the hub; every one after that (from other projects) just adds a card to the same page.
203
+
204
+ ```
205
+ $ spectoflow dashboard (in todo-list-v2/)
206
+ no hub found → starts one → http://localhost:4319
207
+ │
208
+ └── card: "todo-list-v2"
209
+
210
+ $ spectoflow dashboard (in my-other-app/, later, or another day)
211
+ hub already running → just joins it
212
+ │
213
+ └── card: "my-other-app"
214
+
215
+ → open http://localhost:4319 in your browser: both projects, one page
216
+ ```
217
+
218
+ That's the whole local setup — no login, no token, nothing to configure. **The rest of this section
219
+ is entirely optional.**
220
+
221
+ ### Going online (optional): local hub vs. relay server
222
+
223
+ `spectoflow dashboard login/publish` are for one specific, separate need: opening a project's
224
+ dashboard from **another device**, or sharing it with someone else. They talk to `server/` — a
225
+ different application in this repo, not part of the local hub — that you (or someone) hosts
226
+ somewhere reachable (see [`server/docs/deploy-vps.md`](server/docs/deploy-vps.md), or run it
227
+ locally to try it, per [`server/README.md`](server/README.md)). Nothing about your local hub changes;
228
+ it grows one extra, optional connection outward:
229
+
230
+ ```
231
+ YOUR MACHINE THE RELAY SERVER (server/)
232
+ local hub · localhost:4319 hosted by you or someone else
233
+
234
+ [todo-list-v2] ── published ──────────────► only "published" projects
235
+ [my-other-app] ── NOT published, stays local ever show up here
236
+
237
+ ▲
238
+ │ one-time, per machine:
239
+ │ $ spectoflow dashboard login --url=<relay-url> --token=<spf_…>
240
+ │
241
+ the token is minted ON THE RELAY, not by you:
242
+ $ node server/cli.js token create (run by whoever administers it)
243
+
244
+ once logged in and published → any browser, anywhere, can open it
245
+ ```
246
+
247
+ | Command | Runs where | What it does |
248
+ | --- | --- | --- |
249
+ | `spectoflow dashboard` | your machine | start/join the local hub, register the current project |
250
+ | `node server/cli.js token create` | on the relay server | mint a login token for a machine |
251
+ | `spectoflow dashboard login --url=… --token=…` | your machine | link this machine to that relay (once) |
252
+ | `spectoflow dashboard publish` | your machine | make the *current* project visible through the relay |
253
+ | `spectoflow dashboard unpublish` | your machine | take it back offline |
254
+ | `spectoflow dashboard logout` | your machine | unlink the machine entirely |
192
255
 
193
256
  The header bar always shows the brand, the **active agent**, autonomy mode, language, a global-progress
194
257
  meter, a sync dot, and a **Run** quick-action. Thirteen tabs — and **which ones you see, and in what
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.length;
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,7 @@ 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
+ row(c.cy('+'), 'linked', r.pointers, c.cy, `${r.pointers.join(', ')} existed without a pointer — a spectoflow section was appended`);
153
154
  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
155
  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
156
  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,7 +612,8 @@ const HELP = {
611
612
  ${c.dim('copilot, amazon-q, droid, auggie, goose, kimi')}) and writes their entry shims; override
612
613
  with ${c.g('--agent=claude,codex')}. Also wires ${c.bold('Playwright MCP')} into the project's
613
614
  ${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.')}
615
+ ${c.dim('An existing CLAUDE.md is preserved as CLAUDE.md.tomerge for you to merge on first run;')}
616
+ ${c.dim('an existing AGENTS.md/GEMINI.md is kept and gets a spectoflow pointer section appended.')}
615
617
  ${c.dim('Full list with docs links: the dashboard\'s Documentation tab, or the README.')}`,
616
618
  update: `${c.bold('spectoflow update')} ${c.dim('[--dry-run] [--force|-f]')}\n
617
619
  Refresh framework-owned files (engine, default agents & skills, AGENTS.md, policy…)
package/lib/adapters.js CHANGED
@@ -224,24 +224,51 @@ 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);
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
+ const hasPointer = (text) => text.includes('.spectoflow/AGENTS.md');
236
+
237
+ function appendPointer(fp, dryRun) {
238
+ const text = fs.readFileSync(fp, 'utf8');
239
+ if (hasPointer(text)) return false;
240
+ if (!dryRun) fs.writeFileSync(fp, text.replace(/\s*$/, '') + '\n\n' + POINTER_BLOCK);
231
241
  return true;
232
242
  }
233
243
 
234
244
  // 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.
245
+ // codex/cursor) are handled once. A memory file that already exists gets the pointer appended.
236
246
  function generate(projectRoot, agents) {
237
247
  const list = (agents && agents.length ? agents : ['claude', 'codex']).map(byId).filter(Boolean);
238
- const written = [];
248
+ const written = [], appended = [];
239
249
  for (const a of list) {
240
250
  for (const e of a.entries) {
241
- if (writeIfAbsent(path.join(projectRoot, e.path), e.content)) written.push(e.path);
251
+ if (written.includes(e.path) || appended.includes(e.path)) continue;
252
+ const fp = path.join(projectRoot, e.path);
253
+ if (!fs.existsSync(fp)) {
254
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
255
+ fs.writeFileSync(fp, e.content);
256
+ written.push(e.path);
257
+ } else if (MEMORY_FILES.includes(e.path) && appendPointer(fp)) {
258
+ appended.push(e.path);
259
+ }
242
260
  }
243
261
  }
244
- return written;
262
+ return { written, appended };
263
+ }
264
+
265
+ // For `update`: an already-installed project whose existing memory files never got the pointer.
266
+ // Only repairs files that exist — never creates one for an agent the project may not use.
267
+ function ensurePointers(projectRoot, dryRun = false) {
268
+ return MEMORY_FILES.filter((rel) => {
269
+ const fp = path.join(projectRoot, rel);
270
+ return fs.existsSync(fp) && appendPointer(fp, dryRun);
271
+ });
245
272
  }
246
273
 
247
274
  // { id: runner } defaults for the given agents — used to seed config.runners at init.
@@ -260,4 +287,4 @@ function knownAgents() {
260
287
  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
288
  }
262
289
 
263
- module.exports = { generate, defaultRunners, REGISTRY, knownAgents };
290
+ module.exports = { generate, ensurePointers, defaultRunners, REGISTRY, knownAgents };
package/lib/init.js CHANGED
@@ -99,7 +99,8 @@ 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 } = adapters.generate(target, agents);
103
+ appended.forEach((f) => notes.push(`Existing ${f} kept — a spectoflow section was appended to it so your agent finds .spectoflow/AGENTS.md.`));
103
104
 
104
105
  const mcpTargets = [path.join(target, '.mcp.json')];
105
106
  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));
@@ -81,6 +82,7 @@ function runUpdate({ projectRoot, templatesDir, version, dryRun = false, force =
81
82
  kept: [],
82
83
  migration: null,
83
84
  legacyLeftovers: [],
85
+ pointers: [],
84
86
  };
85
87
  const baseline = (prev && prev.files) || {};
86
88
  const nextFiles = {}; // manifest to write after this run
@@ -145,6 +147,8 @@ function runUpdate({ projectRoot, templatesDir, version, dryRun = false, force =
145
147
  for (const rel of LEGACY_LEFTOVERS) if (fs.existsSync(toDisk(sf, rel))) report.legacyLeftovers.push(rel);
146
148
  }
147
149
 
150
+ report.pointers = adapters.ensurePointers(projectRoot, dryRun);
151
+
148
152
  if (!dryRun) manifest.writeManifest(sf, { version, files: nextFiles });
149
153
  return report;
150
154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectoflow",
3
- "version": "0.27.1",
3
+ "version": "0.27.2",
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",