spectoflow 0.24.0 → 0.27.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.
Files changed (53) hide show
  1. package/README.md +28 -6
  2. package/bin/spectoflow.js +88 -6
  3. package/lib/dashboard/connector.js +193 -0
  4. package/lib/dashboard/handlers.js +2 -27
  5. package/lib/dashboard/hub-server.js +103 -11
  6. package/lib/dashboard/inject-design.js +41 -0
  7. package/lib/dashboard/meeting.js +116 -0
  8. package/lib/dashboard/ops.js +83 -2
  9. package/lib/dashboard/public/app.js +738 -118
  10. package/lib/dashboard/public/charts.js +3 -3
  11. package/lib/dashboard/public/commands.js +77 -0
  12. package/lib/dashboard/public/designs/console.css +7 -19
  13. package/lib/dashboard/public/designs/orbit.css +3 -4
  14. package/lib/dashboard/public/designs.js +2 -2
  15. package/lib/dashboard/public/fonts/bricolage-grotesque-400.woff2 +0 -0
  16. package/lib/dashboard/public/fonts/bricolage-grotesque-600.woff2 +0 -0
  17. package/lib/dashboard/public/fonts/bricolage-grotesque-700.woff2 +0 -0
  18. package/lib/dashboard/public/hub.html +1 -1
  19. package/lib/dashboard/public/hub.js +27 -4
  20. package/lib/dashboard/public/i18n.js +60 -18
  21. package/lib/dashboard/public/icons.js +2 -0
  22. package/lib/dashboard/public/index.html +106 -13
  23. package/lib/dashboard/public/styles.css +161 -26
  24. package/lib/dashboard/public/vendor/prism/prism-bash.min.js +1 -0
  25. package/lib/dashboard/public/vendor/prism/prism-c.min.js +1 -0
  26. package/lib/dashboard/public/vendor/prism/prism-clike.min.js +1 -0
  27. package/lib/dashboard/public/vendor/prism/prism-core.min.js +1 -0
  28. package/lib/dashboard/public/vendor/prism/prism-cpp.min.js +1 -0
  29. package/lib/dashboard/public/vendor/prism/prism-csharp.min.js +1 -0
  30. package/lib/dashboard/public/vendor/prism/prism-css.min.js +1 -0
  31. package/lib/dashboard/public/vendor/prism/prism-docker.min.js +1 -0
  32. package/lib/dashboard/public/vendor/prism/prism-go.min.js +1 -0
  33. package/lib/dashboard/public/vendor/prism/prism-java.min.js +1 -0
  34. package/lib/dashboard/public/vendor/prism/prism-json.min.js +1 -0
  35. package/lib/dashboard/public/vendor/prism/prism-kotlin.min.js +1 -0
  36. package/lib/dashboard/public/vendor/prism/prism-markdown.min.js +1 -0
  37. package/lib/dashboard/public/vendor/prism/prism-markup-templating.min.js +1 -0
  38. package/lib/dashboard/public/vendor/prism/prism-markup.min.js +1 -0
  39. package/lib/dashboard/public/vendor/prism/prism-php.min.js +1 -0
  40. package/lib/dashboard/public/vendor/prism/prism-python.min.js +1 -0
  41. package/lib/dashboard/public/vendor/prism/prism-ruby.min.js +1 -0
  42. package/lib/dashboard/public/vendor/prism/prism-rust.min.js +1 -0
  43. package/lib/dashboard/public/vendor/prism/prism-sql.min.js +1 -0
  44. package/lib/dashboard/public/vendor/prism/prism-swift.min.js +1 -0
  45. package/lib/dashboard/public/vendor/prism/prism-yaml.min.js +1 -0
  46. package/lib/dashboard/routes.js +38 -0
  47. package/lib/dashboard/runner.js +7 -2
  48. package/lib/workspace.js +36 -1
  49. package/package.json +3 -3
  50. package/templates/config.json +23 -0
  51. package/lib/dashboard/public/fonts/space-grotesk-400.woff2 +0 -0
  52. package/lib/dashboard/public/fonts/space-grotesk-500.woff2 +0 -0
  53. package/lib/dashboard/public/fonts/space-grotesk-700.woff2 +0 -0
@@ -0,0 +1 @@
1
+ !function(e){var n=/[*&][^\s[\]{},]+/,r=/!(?:<[\w\-%#;/?:@&=+$,.!~*'()[\]]+>|(?:[a-zA-Z\d-]*!)?[\w\-%#;/?:@&=+$.~*'()]+)?/,t="(?:"+r.source+"(?:[ \t]+"+n.source+")?|"+n.source+"(?:[ \t]+"+r.source+")?)",a="(?:[^\\s\\x00-\\x08\\x0e-\\x1f!\"#%&'*,\\-:>?@[\\]`{|}\\x7f-\\x84\\x86-\\x9f\\ud800-\\udfff\\ufffe\\uffff]|[?:-]<PLAIN>)(?:[ \t]*(?:(?![#:])<PLAIN>|:<PLAIN>))*".replace(/<PLAIN>/g,(function(){return"[^\\s\\x00-\\x08\\x0e-\\x1f,[\\]{}\\x7f-\\x84\\x86-\\x9f\\ud800-\\udfff\\ufffe\\uffff]"})),d="\"(?:[^\"\\\\\r\n]|\\\\.)*\"|'(?:[^'\\\\\r\n]|\\\\.)*'";function o(e,n){n=(n||"").replace(/m/g,"")+"m";var r="([:\\-,[{]\\s*(?:\\s<<prop>>[ \t]+)?)(?:<<value>>)(?=[ \t]*(?:$|,|\\]|\\}|(?:[\r\n]\\s*)?#))".replace(/<<prop>>/g,(function(){return t})).replace(/<<value>>/g,(function(){return e}));return RegExp(r,n)}e.languages.yaml={scalar:{pattern:RegExp("([\\-:]\\s*(?:\\s<<prop>>[ \t]+)?[|>])[ \t]*(?:((?:\r?\n|\r)[ \t]+)\\S[^\r\n]*(?:\\2[^\r\n]+)*)".replace(/<<prop>>/g,(function(){return t}))),lookbehind:!0,alias:"string"},comment:/#.*/,key:{pattern:RegExp("((?:^|[:\\-,[{\r\n?])[ \t]*(?:<<prop>>[ \t]+)?)<<key>>(?=\\s*:\\s)".replace(/<<prop>>/g,(function(){return t})).replace(/<<key>>/g,(function(){return"(?:"+a+"|"+d+")"}))),lookbehind:!0,greedy:!0,alias:"atrule"},directive:{pattern:/(^[ \t]*)%.+/m,lookbehind:!0,alias:"important"},datetime:{pattern:o("\\d{4}-\\d\\d?-\\d\\d?(?:[tT]|[ \t]+)\\d\\d?:\\d{2}:\\d{2}(?:\\.\\d*)?(?:[ \t]*(?:Z|[-+]\\d\\d?(?::\\d{2})?))?|\\d{4}-\\d{2}-\\d{2}|\\d\\d?:\\d{2}(?::\\d{2}(?:\\.\\d*)?)?"),lookbehind:!0,alias:"number"},boolean:{pattern:o("false|true","i"),lookbehind:!0,alias:"important"},null:{pattern:o("null|~","i"),lookbehind:!0,alias:"important"},string:{pattern:o(d),lookbehind:!0,greedy:!0},number:{pattern:o("[+-]?(?:0x[\\da-f]+|0o[0-7]+|(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?|\\.inf|\\.nan)","i"),lookbehind:!0},tag:r,important:n,punctuation:/---|[:[\]{}\-,|>?]|\.\.\./},e.languages.yml=e.languages.yaml}(Prism);
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+ /*
3
+ * The dashboard's HTTP route table — method + path → op name + args extraction — with NO reference
4
+ * to ops.js. Two consumers share it: handlers.js (local hub, calls ops directly) and the online
5
+ * relay server (server/src/relay.js), which turns the same rows into `op` frames sent to the machine.
6
+ * Keeping it dependency-free is what lets the server require it without loading ops.js (which
7
+ * would drag in the runner, the orchestrator, agent detection… none of which exist server-side).
8
+ */
9
+ const seg = (p, i) => decodeURIComponent(p.split('/')[i] || '');
10
+ const q = (u, k) => u.searchParams.get(k) || '';
11
+ // [method, matcher, op, args(u, body, pathname)]
12
+ const ROUTES = [
13
+ ['GET', '/api/project', 'project.read', () => ({})],
14
+ ['GET', '/api/agentfile', 'agentfile.read', (u) => ({ path: q(u, 'path') })],
15
+ ['GET', '/api/files/tree', 'files.tree', () => ({})],
16
+ ['GET', '/api/files/read', 'files.read', (u) => ({ path: q(u, 'path') })],
17
+ ['POST', '/api/files/write', 'files.write', (_u, b) => b],
18
+ ['POST', '/api/files/mkdir', 'files.mkdir', (_u, b) => b],
19
+ ['POST', '/api/task', 'task.add', (_u, b) => b],
20
+ ['PATCH', /^\/api\/task\/[^/]+$/, 'task.update', (_u, b, p) => ({ id: seg(p, 3), patch: b })],
21
+ ['POST', /^\/api\/task\/[^/]+\/comment$/, 'task.comment', (_u, b, p) => ({ id: seg(p, 3), text: b.text, action: b.action })],
22
+ ['POST', '/api/workflow/toggle', 'workflow.toggle', (_u, b) => b],
23
+ ['POST', '/api/run', 'run.start', (_u, b) => b],
24
+ ['POST', '/api/chat/summarize', 'chat.summarize', (_u, b) => b],
25
+ ['POST', '/api/meeting/generate', 'meeting.generate', (_u, b) => b],
26
+ ['POST', '/api/chat/clear', 'chat.clear', () => ({})],
27
+ ['POST', '/api/orchestrate', 'orchestrate.start', (_u, b) => b],
28
+ ['POST', '/api/orchestrate/approve', 'orchestrate.approve', (_u, b) => b],
29
+ ['POST', '/api/settings', 'settings.save', (_u, b) => b],
30
+ ['POST', '/api/attention', 'attention.add', (_u, b) => b],
31
+ ['POST', /^\/api\/attention\/[^/]+\/promote$/, 'attention.promote', (_u, _b, p) => ({ id: seg(p, 3) })],
32
+ ['PATCH', /^\/api\/attention\/[^/]+$/, 'attention.update', (_u, b, p) => ({ id: seg(p, 3), patch: b })],
33
+ ['DELETE', /^\/api\/attention\/[^/]+$/, 'attention.remove', (_u, _b, p) => ({ id: seg(p, 3) })],
34
+ ];
35
+ const matches = (m, p) => (typeof m === 'string' ? m === p : m.test(p));
36
+ const findRoute = (method, pathname) => ROUTES.find(([m, matcher]) => m === method && matches(matcher, pathname));
37
+
38
+ module.exports = { ROUTES, seg, q, matches, findRoute };
@@ -60,7 +60,8 @@ function pushAttention(root, text, by) {
60
60
  // Start an agent run. Returns { runId, child } or { error } if no runner is configured.
61
61
  // logPrompt:false suppresses echoing the prompt as a user bubble — used by the orchestrator,
62
62
  // whose priming prompt ("You are the …") is machinery the user shouldn't have to read.
63
- function startRun(root, { prompt, agent, logPrompt = true }, emit) {
63
+ // display: when a non-empty string, the chat bubble shows this while the child still receives prompt.
64
+ function startRun(root, { prompt, agent, logPrompt = true, display }, emit) {
64
65
  const cfg = store.readConfig(root);
65
66
  const which = agent || cfg.agent || 'claude';
66
67
  const cmdStr = resolveRunnerCommand(root, cfg, which);
@@ -68,9 +69,13 @@ function startRun(root, { prompt, agent, logPrompt = true }, emit) {
68
69
  const parts = cmdStr.split(/\s+/).filter(Boolean);
69
70
  const runId = 'r' + Date.now().toString(36);
70
71
  const p = String(prompt).trim();
72
+ // The chat bubble can show a shorter, friendlier text than what the agent actually receives — e.g.
73
+ // a slash-command invocation "/rapport_jour focus bugs" instead of the full expanded instruction.
74
+ // The child (line ~84) always spawns with `p`, the real prompt.
75
+ const shown = (typeof display === 'string' && display.trim()) ? display.trim() : p;
71
76
 
72
77
  if (logPrompt) {
73
- const um = store.appendMessage(root, { role: 'user', kind: 'message', text: p, agent: which, runId });
78
+ const um = store.appendMessage(root, { role: 'user', kind: 'message', text: shown, agent: which, runId });
74
79
  emit({ type: 'message', message: um });
75
80
  }
76
81
 
package/lib/workspace.js CHANGED
@@ -81,4 +81,39 @@ function readLock(baseDir) {
81
81
  return readJSON(lockPath(baseDir)) || readJSON(path.join(globalConfig.homeDir(), 'hub.lock'));
82
82
  }
83
83
 
84
- module.exports = { dir, exists, settings, settingsPath, lockPath, projectDir, init, registerProject, migrateLegacyHome, readLock };
84
+ // ---- online dashboard (sub-project C1): remote.json + the per-project `published` flag ----
85
+ // remote.json = { url, token, machineName, transport } written by `spectoflow dashboard login`; the
86
+ // hub reads it at boot to start the connector. It holds a secret, hence mode 0600 (no-op on Windows).
87
+ function remotePath(baseDir) { return path.join(dir(baseDir), 'remote.json'); }
88
+ function readRemote(baseDir) {
89
+ const r = readJSON(remotePath(baseDir));
90
+ return r && r.url && r.token ? r : null;
91
+ }
92
+ function writeRemote({ url, token, machineName, transport }, baseDir) {
93
+ const obj = { url: String(url).replace(/\/+$/, ''), token: String(token), machineName: String(machineName || ''), transport: transport === 'http' ? 'http' : 'ws' };
94
+ fs.mkdirSync(dir(baseDir), { recursive: true });
95
+ fs.writeFileSync(remotePath(baseDir), JSON.stringify(obj, null, 2) + '\n', { mode: 0o600 });
96
+ try { fs.chmodSync(remotePath(baseDir), 0o600); } catch (_) {}
97
+ return obj;
98
+ }
99
+ function clearRemote(baseDir) {
100
+ try { fs.unlinkSync(remotePath(baseDir)); return true; } catch { return false; }
101
+ }
102
+
103
+ function metaPath(id, baseDir) { return path.join(projectDir(id, baseDir), 'meta.json'); }
104
+ function readMeta(id, baseDir) { return readJSON(metaPath(id, baseDir)); }
105
+ // `published: true` means "announce this project to the online dashboard". Only registered projects
106
+ // can carry it; null tells the caller the id is unknown (a 404, not a silent write).
107
+ function setPublished(id, published, baseDir) {
108
+ const entry = registry.listProjects(baseDir).find((p) => p.id === id);
109
+ if (!entry) return null;
110
+ fs.mkdirSync(projectDir(id, baseDir), { recursive: true });
111
+ const meta = { addedAt: new Date().toISOString(), lastOpened: entry.lastOpened, kind: entry.kind || 'spectoflow', ...(readMeta(id, baseDir) || {}), published: !!published };
112
+ fs.writeFileSync(metaPath(id, baseDir), JSON.stringify(meta, null, 2) + '\n');
113
+ return meta;
114
+ }
115
+ function isPublished(id, baseDir) { const m = readMeta(id, baseDir); return !!(m && m.published); }
116
+ function listPublished(baseDir) { return registry.listProjects(baseDir).filter((p) => isPublished(p.id, baseDir)); }
117
+
118
+ module.exports = { dir, exists, settings, settingsPath, lockPath, projectDir, init, registerProject, migrateLegacyHome, readLock,
119
+ remotePath, readRemote, writeRemote, clearRemote, readMeta, setPublished, isPublished, listPublished };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectoflow",
3
- "version": "0.24.0",
3
+ "version": "0.27.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",
@@ -41,9 +41,9 @@
41
41
  "scripts": {
42
42
  "status": "node bin/spectoflow.js status",
43
43
  "postinstall": "node bin/postinstall.js",
44
- "test": "node --test"
44
+ "test": "node --test \"test/**/*.test.js\""
45
45
  },
46
46
  "engines": {
47
- "node": ">=18"
47
+ "node": ">=22"
48
48
  }
49
49
  }
@@ -7,6 +7,29 @@
7
7
  "specsDir": null,
8
8
  "dashboard": { "autostart": true },
9
9
  "design": "console",
10
+ "theme": "dark",
11
+ "boardView": "list",
12
+ "sideHidden": false,
13
+ "expandedPhases": [],
14
+ "activeTab": "board",
15
+ "chatOpen": false,
16
+ "kanbanColumns": ["todo", "in_progress", "to_validate", "to_analyze", "done", "blocked"],
17
+ "kanbanPageSize": 10,
18
+ "navTabs": [
19
+ {"id":"board","enabled":true},
20
+ {"id":"chat","enabled":true},
21
+ {"id":"requests","enabled":true},
22
+ {"id":"attention","enabled":true},
23
+ {"id":"backlog","enabled":true},
24
+ {"id":"workflow","enabled":true},
25
+ {"id":"team","enabled":true},
26
+ {"id":"files","enabled":true},
27
+ {"id":"notes","enabled":false},
28
+ {"id":"meeting","enabled":false},
29
+ {"id":"info","enabled":true},
30
+ {"id":"docs","enabled":true},
31
+ {"id":"personalize","enabled":true}
32
+ ],
10
33
  "runners": {
11
34
  "claude": "claude -p --permission-mode acceptEdits",
12
35
  "codex": "codex exec"