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 +67 -4
- package/bin/spectoflow.js +4 -2
- package/lib/adapters.js +36 -9
- package/lib/init.js +2 -1
- package/lib/update.js +4 -0
- package/package.json +1 -1
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 `
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
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 (
|
|
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.
|
|
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",
|