spectoflow 0.12.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/LICENSE +21 -0
- package/README.md +133 -0
- package/bin/spectoflow.js +170 -0
- package/lib/adapters.js +120 -0
- package/lib/detect.js +34 -0
- package/lib/manifest.js +36 -0
- package/lib/ownership.js +34 -0
- package/lib/update.js +78 -0
- package/package.json +30 -0
- package/templates/AGENTS.md +66 -0
- package/templates/agents/architect.md +54 -0
- package/templates/agents/business-analyst.md +53 -0
- package/templates/agents/code-reviewer.md +58 -0
- package/templates/agents/developer.md +73 -0
- package/templates/agents/devops.md +59 -0
- package/templates/agents/product-manager.md +53 -0
- package/templates/agents/qa-engineer.md +63 -0
- package/templates/agents/security-engineer.md +59 -0
- package/templates/agents/tech-lead.md +52 -0
- package/templates/agents/ux-designer.md +50 -0
- package/templates/capabilities.md +15 -0
- package/templates/config.json +10 -0
- package/templates/dashboard/orchestrator.js +117 -0
- package/templates/dashboard/public/app.js +747 -0
- package/templates/dashboard/public/charts.js +192 -0
- package/templates/dashboard/public/icons.js +28 -0
- package/templates/dashboard/public/index.html +226 -0
- package/templates/dashboard/public/stats.js +32 -0
- package/templates/dashboard/public/styles.css +426 -0
- package/templates/dashboard/runner.js +77 -0
- package/templates/dashboard/server.js +115 -0
- package/templates/lib/store.js +289 -0
- package/templates/policy.md +11 -0
- package/templates/skills/analyze-requirements/SKILL.md +67 -0
- package/templates/skills/brainstorm/SKILL.md +58 -0
- package/templates/skills/code-review/SKILL.md +67 -0
- package/templates/skills/implement/SKILL.md +80 -0
- package/templates/skills/security-review/SKILL.md +70 -0
- package/templates/skills/write-adr/SKILL.md +61 -0
- package/templates/skills/write-e2e-tests/SKILL.md +99 -0
- package/templates/skills/write-plan/SKILL.md +66 -0
- package/templates/skills/write-spec/SKILL.md +66 -0
- package/templates/skills/write-tests/SKILL.md +80 -0
- package/templates/workflow.md +15 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 G. MOMO
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/georgesmomo/spectoflow/main/logo-spectoflow-white.png">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/georgesmomo/spectoflow/main/logo-spectoflow.png" alt="spectoflow" width="112">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<h1 align="center">spectoflow</h1>
|
|
9
|
+
|
|
10
|
+
<p align="center"><em>Agent-agnostic spec-driven development with a real-time local control plane.</em></p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/spectoflow"><img src="https://img.shields.io/npm/v/spectoflow.svg?color=e6a54b" alt="npm version"></a>
|
|
14
|
+
<img src="https://img.shields.io/badge/node-%3E%3D18-5fb2cc" alt="node >= 18">
|
|
15
|
+
<img src="https://img.shields.io/badge/dependencies-0-4caf72" alt="zero dependencies">
|
|
16
|
+
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
An **agent-agnostic** spec-driven development framework with a **real-time local control plane**.
|
|
20
|
+
You speak in plain language; the framework classifies your intent and runs the right workflow. No
|
|
21
|
+
ceremonial command to start.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install -g spectoflow # (once published)
|
|
27
|
+
spectoflow init /path/to/project # or: node bin/spectoflow.js init .
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`init` scaffolds:
|
|
31
|
+
- `.spectoflow/` — the framework (brain, `workflow.md`, `agents/`, `skills/`, `policy.md`, `config.json`, dashboard, engine).
|
|
32
|
+
- `specs/` and `plans/` — **markdown artifacts**, your versioned source of truth.
|
|
33
|
+
- per-agent shims: `CLAUDE.md` (Claude Code), `AGENTS.md` (Codex/Cursor), `GEMINI.md` (Gemini),
|
|
34
|
+
`.claude/commands/spectoflow.md`.
|
|
35
|
+
- `.spectoflow/runtime.json` is gitignored (volatile execution state).
|
|
36
|
+
|
|
37
|
+
`init` **auto-detects your installed agent(s)** (probes PATH for `claude`, `codex`, `cursor-agent`,
|
|
38
|
+
`gemini`, and existing `.claude`/`.codex`/… dirs): it writes shims for each, sets the active agent in
|
|
39
|
+
`config.json`, and seeds the runner commands. Override with `--agent=claude,codex`; if nothing is
|
|
40
|
+
detected it falls back to claude + codex.
|
|
41
|
+
|
|
42
|
+
**Empty project** → your agent asks what to build and runs Intake (brainstorm → analysis → spec → plan).
|
|
43
|
+
**Existing project** → an existing `CLAUDE.md` is preserved as `CLAUDE.md.tomerge` (merged on first run);
|
|
44
|
+
existing `plans/*.md` tasks are given stable ids.
|
|
45
|
+
|
|
46
|
+
## Update
|
|
47
|
+
|
|
48
|
+
`init` is idempotent (it never overwrites), so it can't refresh an installed project. `spectoflow
|
|
49
|
+
update` refreshes **framework-owned** files (engine, dashboard, `AGENTS.md`, `capabilities.md`,
|
|
50
|
+
`policy.md`, default agents & skills) to the CLI's version, while **preserving your work** —
|
|
51
|
+
`config.json`, `workflow.md`, `specs/`, `plans/`, and any agent/skill you created or edited are never
|
|
52
|
+
touched. A file you edited is preserved and its new version is written next to it as `<file>.new`
|
|
53
|
+
for you to merge by hand. Add `--dry-run` to preview.
|
|
54
|
+
|
|
55
|
+
First refresh the kit, then run update from your project — the flow is the same whichever way you
|
|
56
|
+
installed:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# npm global
|
|
60
|
+
npm update -g spectoflow && spectoflow update
|
|
61
|
+
|
|
62
|
+
# manual (cloned repo)
|
|
63
|
+
git -C /path/to/spectoflow pull && node /path/to/spectoflow/bin/spectoflow.js update
|
|
64
|
+
|
|
65
|
+
# npx (no install)
|
|
66
|
+
npx spectoflow@latest update
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`update` resolves the framework version from the CLI you run and the project from the current
|
|
70
|
+
directory, so it behaves identically across all three. `init` records a hashed baseline in
|
|
71
|
+
`.spectoflow/.manifest.json` (committed) that lets update tell an untouched framework file from one
|
|
72
|
+
you've edited; installs made before this file existed degrade safely (a matching file is adopted, a
|
|
73
|
+
divergent one gets a `.new`).
|
|
74
|
+
|
|
75
|
+
## Storage is markdown
|
|
76
|
+
|
|
77
|
+
Plans are plain markdown with checkbox tasks — human-readable, git-diffable, standard:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
## Phase 1 — Login
|
|
81
|
+
- [ ] T-001 Add login form @dev ~standard %in_progress
|
|
82
|
+
- note: waiting on design tokens
|
|
83
|
+
- [x] T-002 Set up auth routes @dev
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`[x]` done · `~level` quick|standard|major · `%status` in_progress|to_validate|to_analyze|blocked ·
|
|
87
|
+
`@owner`. The dashboard parses these and writes back **one line at a time** (granular), so it and your
|
|
88
|
+
agent never clobber each other.
|
|
89
|
+
|
|
90
|
+
## Dashboard (real-time)
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
node .spectoflow/dashboard/server.js # → http://localhost:4319
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Zero dependencies, updates live via SSE + file watching. Seven tabs behind a dense icon-tab header
|
|
97
|
+
(brand · subtitle · a slim global-progress meter · a sync dot · a **Run** quick-action): **Board**
|
|
98
|
+
(the control-room Overview — KPI cards, a status donut, a **scope-vs-delivered area curve**, a
|
|
99
|
+
workflow-at-a-glance strip, per-phase progress bars, filter chips + search — plus the phase board),
|
|
100
|
+
**Requests** (tasks awaiting you — `to_validate`/`to_analyze`), **Backlog** (a flat sortable/filterable
|
|
101
|
+
table of every task across every plan), **Workflow** (the pipeline as a diagram — click a step to
|
|
102
|
+
enable/disable it, which edits `workflow.md`), **Agents & Skills** (enriched cards — capability,
|
|
103
|
+
standards, uses/inputs/outputs — that open a full-body markdown drawer), **Chat** (a full-height
|
|
104
|
+
group-chat panel), and **Info** (a project-at-a-glance summary). Charts are zero-dep, hand-rolled SVG
|
|
105
|
+
in `dashboard/public/charts.js` (donut/area/bars/ring, animated, `prefers-reduced-motion`-aware), and
|
|
106
|
+
every aggregate is computed client-side.
|
|
107
|
+
|
|
108
|
+
A floating **💬 chat widget** (bottom-right, redesigned) and the **Chat** tab render the same
|
|
109
|
+
`runtime.messages` log via a shared `renderChatLog()`, so they never drift. A running agent identifies
|
|
110
|
+
itself by printing `::spectoflow role=… kind=… msg=…` sentinels, which become labelled messages
|
|
111
|
+
(analyst / developer / qa …); other output streams raw. The board refreshes live as it edits plans.
|
|
112
|
+
Either surface can also **Orchestrate** the enabled workflow: each step runs its agent, gated by mode
|
|
113
|
+
+ policy. The Agents & Skills drawer is served by the one read-only endpoint, `GET
|
|
114
|
+
/api/agentfile?path=` (scoped to `.spectoflow/agents/**` + `.spectoflow/skills/**`,
|
|
115
|
+
path-traversal-safe) — the framework's only other server surface is unchanged.
|
|
116
|
+
|
|
117
|
+
## Agents vs skills
|
|
118
|
+
|
|
119
|
+
Agents (`.spectoflow/agents/`) are **stable team personas** (Product Manager, Developer, QA Engineer…).
|
|
120
|
+
Skills (`.spectoflow/skills/`) are **evolving procedures**. A workflow step → a capability → its agent →
|
|
121
|
+
runs a skill. Improve a skill without touching the agent.
|
|
122
|
+
|
|
123
|
+
Agents and skills follow real domain standards, cited in-file — TDD, OWASP ASVS/Top 10, C4/ADR,
|
|
124
|
+
INVEST, Playwright E2E, Conventional Commits, and more — not generic one-liners.
|
|
125
|
+
|
|
126
|
+
## Language
|
|
127
|
+
|
|
128
|
+
`.spectoflow/config.json` → `language` (default `en`, incl. code comments). Switchable.
|
|
129
|
+
|
|
130
|
+
## Studied, not copied
|
|
131
|
+
|
|
132
|
+
Structure informed by spec-kit, OpenSpec (markdown + per-agent adapters), and BMAD (agent-personas).
|
|
133
|
+
spectoflow keeps the good ideas, removes the ceremony, and adds a real-time control plane. MIT.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
const store = require('../templates/lib/store');
|
|
7
|
+
const adapters = require('../lib/adapters');
|
|
8
|
+
const detect = require('../lib/detect');
|
|
9
|
+
const ownership = require('../lib/ownership');
|
|
10
|
+
const manifest = require('../lib/manifest');
|
|
11
|
+
|
|
12
|
+
const KIT = path.resolve(__dirname, '..');
|
|
13
|
+
const TPL = path.join(KIT, 'templates');
|
|
14
|
+
const VERSION = require('../package.json').version;
|
|
15
|
+
const argv = process.argv.slice(2);
|
|
16
|
+
const cmd = argv[0] || 'help';
|
|
17
|
+
|
|
18
|
+
function copyDir(src, dst) {
|
|
19
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
20
|
+
for (const e of fs.readdirSync(src, { withFileTypes: true })) {
|
|
21
|
+
const s = path.join(src, e.name), d = path.join(dst, e.name);
|
|
22
|
+
if (e.isDirectory()) copyDir(s, d);
|
|
23
|
+
else if (!fs.existsSync(d)) fs.copyFileSync(s, d);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Existing project: give id-less checkbox tasks a stable id, in place.
|
|
28
|
+
const ID_RE = /^[A-Za-z]{1,5}-?\d+[A-Za-z]?$/;
|
|
29
|
+
function normalizePlans(root) {
|
|
30
|
+
const dir = path.join(root, 'plans');
|
|
31
|
+
if (!fs.existsSync(dir)) return 0;
|
|
32
|
+
let added = 0, seq = 1;
|
|
33
|
+
for (const f of fs.readdirSync(dir).filter((x) => x.endsWith('.md'))) {
|
|
34
|
+
const fp = path.join(dir, f);
|
|
35
|
+
const lines = fs.readFileSync(fp, 'utf8').split('\n');
|
|
36
|
+
let touched = false;
|
|
37
|
+
for (let i = 0; i < lines.length; i++) {
|
|
38
|
+
const m = lines[i].match(/^(\s*- \[[ xX]\]\s+)(\S+)(\s.*)?$/);
|
|
39
|
+
if (m && !ID_RE.test(m[2])) {
|
|
40
|
+
const id = 'T-' + String(seq++).padStart(3, '0');
|
|
41
|
+
lines[i] = `${m[1]}${id} ${m[2]}${m[3] || ''}`;
|
|
42
|
+
touched = true; added++;
|
|
43
|
+
} else if (m) { seq++; }
|
|
44
|
+
}
|
|
45
|
+
if (touched) fs.writeFileSync(fp, lines.join('\n'));
|
|
46
|
+
}
|
|
47
|
+
return added;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function init() {
|
|
51
|
+
const target = path.resolve(argv[1] && !argv[1].startsWith('--') ? argv[1] : '.');
|
|
52
|
+
const agentsArg = (argv.find((a) => a.startsWith('--agent=')) || '').split('=')[1];
|
|
53
|
+
fs.mkdirSync(target, { recursive: true });
|
|
54
|
+
const notes = [];
|
|
55
|
+
|
|
56
|
+
// explicit --agent wins; otherwise detect installed agents; otherwise fall back to claude + codex
|
|
57
|
+
let agents, detected = [];
|
|
58
|
+
if (agentsArg) {
|
|
59
|
+
agents = agentsArg.split(',');
|
|
60
|
+
} else {
|
|
61
|
+
detected = detect.detectAgents(target);
|
|
62
|
+
agents = detected.length ? detected : ['claude', 'codex'];
|
|
63
|
+
notes.push(detected.length
|
|
64
|
+
? `Detected agent(s): ${detected.join(', ')} — active: ${agents[0]}.`
|
|
65
|
+
: 'No agent CLI detected — defaulted to claude + codex.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// preserve an existing CLAUDE.md
|
|
69
|
+
const claude = path.join(target, 'CLAUDE.md');
|
|
70
|
+
if (fs.existsSync(claude) && !fs.existsSync(claude + '.tomerge')) {
|
|
71
|
+
fs.renameSync(claude, claude + '.tomerge');
|
|
72
|
+
notes.push('Existing CLAUDE.md preserved as CLAUDE.md.tomerge — your agent merges it on first run.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// canonical framework → .spectoflow/
|
|
76
|
+
const spectoflowDir = path.join(target, '.spectoflow');
|
|
77
|
+
copyDir(TPL, spectoflowDir);
|
|
78
|
+
|
|
79
|
+
// record the install baseline so `update` can tell untouched framework files from user edits
|
|
80
|
+
const frameworkFiles = ownership.listFrameworkFiles(TPL);
|
|
81
|
+
manifest.writeManifest(spectoflowDir, {
|
|
82
|
+
version: VERSION,
|
|
83
|
+
files: manifest.hashFileMap(spectoflowDir, frameworkFiles),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// set the active agent and seed runner commands from the selected/detected agents
|
|
87
|
+
const cfgPath = path.join(spectoflowDir, 'config.json');
|
|
88
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
89
|
+
cfg.agent = agents[0];
|
|
90
|
+
cfg.runners = { ...cfg.runners, ...adapters.defaultRunners(agents) };
|
|
91
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
92
|
+
|
|
93
|
+
// artifact folders
|
|
94
|
+
fs.mkdirSync(path.join(target, 'specs'), { recursive: true });
|
|
95
|
+
fs.mkdirSync(path.join(target, 'plans'), { recursive: true });
|
|
96
|
+
|
|
97
|
+
// existing project: id-normalize any plans already there
|
|
98
|
+
const added = normalizePlans(target);
|
|
99
|
+
if (added) notes.push(`Normalized ${added} existing task(s) with stable ids.`);
|
|
100
|
+
|
|
101
|
+
// per-agent shims
|
|
102
|
+
const written = adapters.generate(target, agents);
|
|
103
|
+
|
|
104
|
+
// gitignore the volatile runtime
|
|
105
|
+
const gi = path.join(target, '.gitignore');
|
|
106
|
+
const line = '.spectoflow/runtime.json';
|
|
107
|
+
if (!fs.existsSync(gi) || !fs.readFileSync(gi, 'utf8').includes(line)) {
|
|
108
|
+
fs.appendFileSync(gi, (fs.existsSync(gi) ? '\n' : '') + line + '\n');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log('spectoflow installed in', target);
|
|
112
|
+
console.log(' .spectoflow/ framework (brain, workflow, agents, skills, policy, dashboard, config)');
|
|
113
|
+
console.log(' specs/ plans/ markdown artifacts (your source of truth)');
|
|
114
|
+
written.forEach((w) => console.log(' + ' + w));
|
|
115
|
+
notes.forEach((n) => console.log(' ! ' + n));
|
|
116
|
+
console.log('\nNext:');
|
|
117
|
+
console.log(' 1) Open your agent here (Claude Code loads CLAUDE.md; Codex loads AGENTS.md).');
|
|
118
|
+
console.log(' 2) Run /spectoflow init — or just say what you want to build.');
|
|
119
|
+
console.log(' 3) Watch: node .spectoflow/dashboard/server.js → http://localhost:4319');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function update() {
|
|
123
|
+
const root = process.cwd();
|
|
124
|
+
if (!fs.existsSync(path.join(root, '.spectoflow'))) {
|
|
125
|
+
return console.log('No spectoflow project here. Run: spectoflow init');
|
|
126
|
+
}
|
|
127
|
+
const dryRun = argv.includes('--dry-run');
|
|
128
|
+
const r = require('../lib/update').runUpdate({ projectRoot: root, templatesDir: TPL, version: VERSION, dryRun });
|
|
129
|
+
|
|
130
|
+
const from = r.fromVersion || 'unknown';
|
|
131
|
+
console.log(`spectoflow update — ${from} → ${r.toVersion}${dryRun ? ' (dry-run)' : ''}`);
|
|
132
|
+
const line = (label, list) => list.length && console.log(` ${label.padEnd(10)} ${String(list.length).padStart(2)} ${list.join(', ')}`);
|
|
133
|
+
line('refreshed', r.refreshed);
|
|
134
|
+
line('created', r.created);
|
|
135
|
+
line('adopted', r.adopted);
|
|
136
|
+
line('.new', r.newSidecar);
|
|
137
|
+
if (r.unchanged.length) console.log(` ${'unchanged'.padEnd(10)} ${String(r.unchanged.length).padStart(2)}`);
|
|
138
|
+
console.log(` ${'preserved'.padEnd(10)} config.json, workflow.md, specs/, plans/, your custom agents & skills`);
|
|
139
|
+
if (r.newSidecar.length) {
|
|
140
|
+
console.log(`\n${r.newSidecar.length} file(s) you edited have a new version alongside as *.new — compare and merge by hand.`);
|
|
141
|
+
}
|
|
142
|
+
if (dryRun) console.log('\n(dry-run — nothing was written)');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function dashboard() {
|
|
146
|
+
const local = path.resolve('.spectoflow', 'dashboard', 'server.js');
|
|
147
|
+
const bundled = path.join(TPL, 'dashboard', 'server.js');
|
|
148
|
+
spawn('node', [fs.existsSync(local) ? local : bundled], { stdio: 'inherit' });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function status() {
|
|
152
|
+
const root = process.cwd();
|
|
153
|
+
if (!fs.existsSync(path.join(root, 'plans')) && !fs.existsSync(path.join(root, '.spectoflow'))) {
|
|
154
|
+
return console.log('No spectoflow project here. Run: spectoflow init');
|
|
155
|
+
}
|
|
156
|
+
const p = store.readProject(root);
|
|
157
|
+
const tasks = p.plans.flatMap((pl) => pl.phases.flatMap((ph) => ph.tasks));
|
|
158
|
+
const done = tasks.filter((t) => t.status === 'done').length;
|
|
159
|
+
console.log(`${(p.config && p.config.projectType) || 'project'} — mode ${p.config.mode} · lang ${p.config.language}`);
|
|
160
|
+
console.log(`${done}/${tasks.length} tasks done · ${p.specs.length} spec(s) · ${p.agents.length} agents · ${p.skills.length} skills`);
|
|
161
|
+
tasks.filter((t) => t.status === 'in_progress').forEach((t) => console.log(` > in progress: ${t.id} ${t.title}`));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const help = () => console.log(`spectoflow — commands:
|
|
165
|
+
init [dir] [--agent=claude,codex] install into a project
|
|
166
|
+
update [--dry-run] refresh framework files to this kit version
|
|
167
|
+
dashboard run the local control plane
|
|
168
|
+
status print progress`);
|
|
169
|
+
|
|
170
|
+
({ init, update, dashboard, status, help })[cmd] ? ({ init, update, dashboard, status, help })[cmd]() : help();
|
package/lib/adapters.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* Per-agent adapters. The canonical brain is .spectoflow/AGENTS.md. Each agent needs a thin entry
|
|
4
|
+
* file in the place IT reads, pointing back to the canonical brain — so the framework stays in
|
|
5
|
+
* .spectoflow/ (swappable, no pollution) while every agent still finds its native entry point.
|
|
6
|
+
* (Same pattern OpenSpec uses to support claude/codex/cursor/gemini/…)
|
|
7
|
+
*
|
|
8
|
+
* The REGISTRY is the single source: for each agent, the native entry file(s) to write (shims), the
|
|
9
|
+
* default headless runner command (fills config.runners; user-adjustable), and how to detect it
|
|
10
|
+
* (a PATH binary and/or existing agent dir). Order = default-agent priority when several are found.
|
|
11
|
+
*/
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const CLAUDE_MD = `# CLAUDE.md — spectoflow
|
|
16
|
+
|
|
17
|
+
This project uses **spectoflow**. **Read \`.spectoflow/AGENTS.md\` and follow it** as your operating
|
|
18
|
+
instructions (intent router, workflow, standing rules).
|
|
19
|
+
|
|
20
|
+
> If \`CLAUDE.md.tomerge\` exists next to this file, it is your previous CLAUDE.md, preserved on
|
|
21
|
+
> install. Merge its project-specific content into this file, then delete \`CLAUDE.md.tomerge\`,
|
|
22
|
+
> before anything else.
|
|
23
|
+
|
|
24
|
+
- Command: \`/spectoflow\` (\`init\` / \`status\` / or just a request).
|
|
25
|
+
- Dashboard: \`node .spectoflow/dashboard/server.js\` → http://localhost:4319
|
|
26
|
+
- Artifacts are markdown in \`specs/\` and \`plans/\`; volatile state in \`.spectoflow/runtime.json\`.
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const ROOT_AGENTS_MD = `# AGENTS.md — spectoflow
|
|
30
|
+
|
|
31
|
+
This project uses **spectoflow**. **Read \`.spectoflow/AGENTS.md\` and follow it** as your operating
|
|
32
|
+
instructions. Artifacts are markdown in \`specs/\` and \`plans/\`; the workflow is \`.spectoflow/workflow.md\`.
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
const GEMINI_MD = `# GEMINI.md — spectoflow
|
|
36
|
+
|
|
37
|
+
This project uses **spectoflow**. **Read \`.spectoflow/AGENTS.md\` and follow it** as your operating
|
|
38
|
+
instructions (intent router, workflow, standing rules). Artifacts are markdown in \`specs/\` and
|
|
39
|
+
\`plans/\`; the workflow is \`.spectoflow/workflow.md\`.
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const SLASH_CMD = `---
|
|
43
|
+
description: spectoflow — spec-driven control (init / status / or just a request)
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
Read \`.spectoflow/AGENTS.md\` and \`.spectoflow/config.json\` first.
|
|
47
|
+
|
|
48
|
+
Argument: \`$ARGUMENTS\`
|
|
49
|
+
|
|
50
|
+
- \`init\`: verify setup. If \`CLAUDE.md.tomerge\` exists, merge it into \`CLAUDE.md\` and delete it. If
|
|
51
|
+
\`specs/\` and \`plans/\` are empty, greet me, state the mode, and start Intake (brainstorm → analysis
|
|
52
|
+
→ spec → plan) by asking what I want to build.
|
|
53
|
+
- \`status\`: summarize progress from \`plans/*.md\` and \`.spectoflow/runtime.json\`.
|
|
54
|
+
- otherwise: treat \`$ARGUMENTS\` as a request and run the Router in \`.spectoflow/AGENTS.md\`.
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
// Priority order = which agent becomes the default when several are detected.
|
|
58
|
+
const REGISTRY = [
|
|
59
|
+
{
|
|
60
|
+
id: 'claude',
|
|
61
|
+
entries: [
|
|
62
|
+
{ path: 'CLAUDE.md', content: CLAUDE_MD },
|
|
63
|
+
{ path: '.claude/commands/spectoflow.md', content: SLASH_CMD },
|
|
64
|
+
],
|
|
65
|
+
runner: 'claude -p --permission-mode acceptEdits',
|
|
66
|
+
detect: { bin: 'claude', dirs: ['.claude'] },
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'codex',
|
|
70
|
+
entries: [{ path: 'AGENTS.md', content: ROOT_AGENTS_MD }],
|
|
71
|
+
runner: 'codex exec',
|
|
72
|
+
detect: { bin: 'codex', dirs: ['.codex'] },
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'cursor',
|
|
76
|
+
entries: [{ path: 'AGENTS.md', content: ROOT_AGENTS_MD }],
|
|
77
|
+
runner: 'cursor-agent -p',
|
|
78
|
+
detect: { bin: 'cursor-agent', dirs: ['.cursor'] },
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'gemini',
|
|
82
|
+
entries: [{ path: 'GEMINI.md', content: GEMINI_MD }],
|
|
83
|
+
runner: 'gemini -p',
|
|
84
|
+
detect: { bin: 'gemini', dirs: ['.gemini'] },
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const byId = (id) => REGISTRY.find((a) => a.id === id);
|
|
89
|
+
|
|
90
|
+
function writeIfAbsent(fp, content) {
|
|
91
|
+
if (fs.existsSync(fp)) return false;
|
|
92
|
+
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
93
|
+
fs.writeFileSync(fp, content);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Write the native entry-file shims for each selected agent. Shared files (AGENTS.md across
|
|
98
|
+
// codex/cursor) are written once — writeIfAbsent dedupes. Returns the relative paths written.
|
|
99
|
+
function generate(projectRoot, agents) {
|
|
100
|
+
const list = (agents && agents.length ? agents : ['claude', 'codex']).map(byId).filter(Boolean);
|
|
101
|
+
const written = [];
|
|
102
|
+
for (const a of list) {
|
|
103
|
+
for (const e of a.entries) {
|
|
104
|
+
if (writeIfAbsent(path.join(projectRoot, e.path), e.content)) written.push(e.path);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return written;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// { id: runner } defaults for the given agents — used to seed config.runners at init.
|
|
111
|
+
function defaultRunners(agents) {
|
|
112
|
+
const out = {};
|
|
113
|
+
for (const id of agents) {
|
|
114
|
+
const a = byId(id);
|
|
115
|
+
if (a) out[id] = a.runner;
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { generate, defaultRunners, REGISTRY };
|
package/lib/detect.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* Agent auto-detection. A detected agent means: its CLI is on PATH (so its runner will actually
|
|
4
|
+
* work) OR the project already has that agent's config dir. `init` uses this to pick sensible
|
|
5
|
+
* defaults (which shims to write, which agent is active) instead of asking the user to specify.
|
|
6
|
+
*/
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { REGISTRY } = require('./adapters');
|
|
10
|
+
|
|
11
|
+
// Is `bin` an executable resolvable on PATH? On win32, an extension from PATHEXT is required, so we
|
|
12
|
+
// try each; we also try the bare name (covers test fixtures and extensionless shims).
|
|
13
|
+
function binOnPath(bin, { env = process.env, platform = process.platform } = {}) {
|
|
14
|
+
const raw = env.PATH || env.Path || '';
|
|
15
|
+
const dirs = raw.split(path.delimiter).filter(Boolean);
|
|
16
|
+
const exts =
|
|
17
|
+
platform === 'win32' ? ['', ...(env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)] : [''];
|
|
18
|
+
for (const d of dirs) {
|
|
19
|
+
for (const e of exts) {
|
|
20
|
+
if (fs.existsSync(path.join(d, bin + e))) return true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Agent ids detected for `projectRoot`, in REGISTRY (priority) order.
|
|
27
|
+
function detectAgents(projectRoot, opts = {}) {
|
|
28
|
+
return REGISTRY.filter((a) => {
|
|
29
|
+
if (a.detect.bin && binOnPath(a.detect.bin, opts)) return true;
|
|
30
|
+
return (a.detect.dirs || []).some((d) => fs.existsSync(path.join(projectRoot, d)));
|
|
31
|
+
}).map((a) => a.id);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { binOnPath, detectAgents };
|
package/lib/manifest.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* The install manifest — `.spectoflow/.manifest.json`. Records the kit version and a sha256 of every
|
|
4
|
+
* framework-owned file as it was installed. `update` compares the on-disk hash against this baseline
|
|
5
|
+
* to tell "untouched framework file" (safe to refresh) from "user-edited" (preserve, drop a .new).
|
|
6
|
+
*/
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
|
|
11
|
+
const MANIFEST_NAME = '.manifest.json';
|
|
12
|
+
|
|
13
|
+
function sha256(buf) {
|
|
14
|
+
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// { relPath: sha256 } for each file, read relative to baseDir.
|
|
18
|
+
function hashFileMap(baseDir, relFiles) {
|
|
19
|
+
const map = {};
|
|
20
|
+
for (const rel of relFiles) {
|
|
21
|
+
map[rel] = sha256(fs.readFileSync(path.join(baseDir, rel.split('/').join(path.sep))));
|
|
22
|
+
}
|
|
23
|
+
return map;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function writeManifest(spectoflowDir, data) {
|
|
27
|
+
fs.writeFileSync(path.join(spectoflowDir, MANIFEST_NAME), JSON.stringify(data, null, 2) + '\n');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readManifest(spectoflowDir) {
|
|
31
|
+
const fp = path.join(spectoflowDir, MANIFEST_NAME);
|
|
32
|
+
if (!fs.existsSync(fp)) return null;
|
|
33
|
+
return JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { sha256, hashFileMap, writeManifest, readManifest, MANIFEST_NAME };
|
package/lib/ownership.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* Ownership model for `spectoflow update`.
|
|
4
|
+
*
|
|
5
|
+
* Framework-owned files (refreshed on update) are DERIVED from the kit's templates/ — every file the
|
|
6
|
+
* kit ships EXCEPT the ones that belong to the user. Deriving the set (rather than hard-coding it)
|
|
7
|
+
* keeps it correct when new default agents/skills are added to the kit.
|
|
8
|
+
*
|
|
9
|
+
* User-owned (never touched): config.json (settings) and workflow.md (edited from the dashboard).
|
|
10
|
+
* specs/, plans/, runtime.json and .manifest.json are not in templates/, so they never appear here.
|
|
11
|
+
*/
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
// Files the kit ships but the user owns — never refreshed by update.
|
|
16
|
+
const USER_OWNED = new Set(['config.json', 'workflow.md']);
|
|
17
|
+
|
|
18
|
+
function walk(dir, base, out) {
|
|
19
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
20
|
+
const abs = path.join(dir, e.name);
|
|
21
|
+
const rel = path.relative(base, abs).split(path.sep).join('/');
|
|
22
|
+
if (e.isDirectory()) walk(abs, base, out);
|
|
23
|
+
else out.push(rel);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Relative POSIX paths of every framework-owned file shipped in `templatesDir`, sorted, deduped.
|
|
28
|
+
function listFrameworkFiles(templatesDir) {
|
|
29
|
+
const out = [];
|
|
30
|
+
walk(templatesDir, templatesDir, out);
|
|
31
|
+
return [...new Set(out.filter((f) => !USER_OWNED.has(f)))].sort();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { listFrameworkFiles, USER_OWNED };
|
package/lib/update.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* `spectoflow update` — refresh framework-owned files in an installed project to the current kit,
|
|
4
|
+
* without ever touching the user's work. Ownership is decided per file against the install manifest:
|
|
5
|
+
*
|
|
6
|
+
* file absent on disk .......................... create (new file shipped by this version)
|
|
7
|
+
* disk == new template ......................... already current (unchanged, or adopted if legacy)
|
|
8
|
+
* disk == manifest baseline (untouched) ........ refresh to the new template
|
|
9
|
+
* otherwise (user-edited, or legacy-divergent) . preserve; write <file>.new for manual merge
|
|
10
|
+
*
|
|
11
|
+
* User-owned files (config.json, workflow.md) are not in the framework set, so they are never read
|
|
12
|
+
* for writing. No 3-way auto-merge and no deletions in v1 — refresh the safe, offer the rest as .new.
|
|
13
|
+
*/
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const ownership = require('./ownership');
|
|
17
|
+
const manifest = require('./manifest');
|
|
18
|
+
|
|
19
|
+
function toDisk(sf, rel) {
|
|
20
|
+
return path.join(sf, rel.split('/').join(path.sep));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function runUpdate({ projectRoot, templatesDir, version, dryRun = false }) {
|
|
24
|
+
const sf = path.join(projectRoot, '.spectoflow');
|
|
25
|
+
const prev = manifest.readManifest(sf);
|
|
26
|
+
const report = {
|
|
27
|
+
fromVersion: prev ? prev.version : null,
|
|
28
|
+
toVersion: version,
|
|
29
|
+
dryRun,
|
|
30
|
+
created: [],
|
|
31
|
+
refreshed: [],
|
|
32
|
+
newSidecar: [],
|
|
33
|
+
adopted: [],
|
|
34
|
+
unchanged: [],
|
|
35
|
+
};
|
|
36
|
+
const baseline = (prev && prev.files) || {};
|
|
37
|
+
const nextFiles = {}; // manifest to write after this run
|
|
38
|
+
|
|
39
|
+
const write = (fp, buf) => {
|
|
40
|
+
if (dryRun) return;
|
|
41
|
+
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
42
|
+
fs.writeFileSync(fp, buf);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
for (const rel of ownership.listFrameworkFiles(templatesDir)) {
|
|
46
|
+
const newBuf = fs.readFileSync(toDisk(templatesDir, rel));
|
|
47
|
+
const newHash = manifest.sha256(newBuf);
|
|
48
|
+
const diskPath = toDisk(sf, rel);
|
|
49
|
+
const base = baseline[rel]; // undefined for legacy / brand-new files
|
|
50
|
+
|
|
51
|
+
if (!fs.existsSync(diskPath)) {
|
|
52
|
+
write(diskPath, newBuf);
|
|
53
|
+
report.created.push(rel);
|
|
54
|
+
nextFiles[rel] = newHash;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const diskHash = manifest.sha256(fs.readFileSync(diskPath));
|
|
59
|
+
|
|
60
|
+
if (diskHash === newHash) {
|
|
61
|
+
(base === undefined ? report.adopted : report.unchanged).push(rel);
|
|
62
|
+
nextFiles[rel] = newHash; // bring legacy files under tracking; keep tracked ones current
|
|
63
|
+
} else if (base !== undefined && diskHash === base) {
|
|
64
|
+
write(diskPath, newBuf); // untouched framework file → safe to refresh
|
|
65
|
+
report.refreshed.push(rel);
|
|
66
|
+
nextFiles[rel] = newHash;
|
|
67
|
+
} else {
|
|
68
|
+
write(diskPath + '.new', newBuf); // user-edited or legacy-divergent → offer, never overwrite
|
|
69
|
+
report.newSidecar.push(rel);
|
|
70
|
+
if (base !== undefined) nextFiles[rel] = base; // keep flagged as diverged next time
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!dryRun) manifest.writeManifest(sf, { version, files: nextFiles });
|
|
75
|
+
return report;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { runUpdate };
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "spectoflow",
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"description": "Agent-agnostic spec-driven development framework + real-time local control plane. Markdown artifacts, intent router, workflow-by-scope.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"spec-driven-development",
|
|
7
|
+
"sdd",
|
|
8
|
+
"ai-agents",
|
|
9
|
+
"coding-agents",
|
|
10
|
+
"agent-agnostic",
|
|
11
|
+
"workflow",
|
|
12
|
+
"orchestration",
|
|
13
|
+
"dashboard",
|
|
14
|
+
"control-plane",
|
|
15
|
+
"markdown",
|
|
16
|
+
"claude",
|
|
17
|
+
"codex",
|
|
18
|
+
"cursor",
|
|
19
|
+
"gemini"
|
|
20
|
+
],
|
|
21
|
+
"homepage": "https://github.com/georgesmomo/spectoflow#readme",
|
|
22
|
+
"bugs": { "url": "https://github.com/georgesmomo/spectoflow/issues" },
|
|
23
|
+
"repository": { "type": "git", "url": "git+https://github.com/georgesmomo/spectoflow.git" },
|
|
24
|
+
"author": "Georges MOMO <georges.momo@gmail.com>",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"bin": { "spectoflow": "bin/spectoflow.js" },
|
|
27
|
+
"files": ["bin", "lib", "templates", "README.md", "LICENSE"],
|
|
28
|
+
"scripts": { "status": "node bin/spectoflow.js status", "test": "node --test" },
|
|
29
|
+
"engines": { "node": ">=18" }
|
|
30
|
+
}
|