spectoflow 0.17.5 → 0.19.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
@@ -158,6 +158,27 @@ Either surface can also **Orchestrate** the enabled workflow: each step runs its
158
158
  /api/agentfile?path=` (scoped to `.spectoflow/agents/**` + `.spectoflow/skills/**`,
159
159
  path-traversal-safe) — the framework's only other server surface is unchanged.
160
160
 
161
+ **Customize.** Settings → **Customize** lets you extend the project's own spectoflow install: add a
162
+ dashboard, a skill, or an agent by describing what you want (or hit **Auto** to have the agent survey
163
+ the project and propose candidates), and it clarifies first if the ask is ambiguous. Dashboards are
164
+ never raw HTML — they're a small **declarative block spec** (`markdown`, `kpi-row`, `chart-bars`,
165
+ `chart-donut`, `table`, `list`, `stat-tile-row`) rendered by the same components the built-in Board
166
+ uses, so a generated dashboard automatically matches whatever design is active, in both themes, and
167
+ keeps matching if you switch designs later. Blocks can bind live to project stats (`bind:
168
+ "phases.0.pct"`) or hold a static value. Generated skills and agents follow the same gold-standard
169
+ shape as the shipped ones, cite real domain standards (OWASP, WCAG, C4/ADR, …) instead of generic
170
+ advice, and are marked `origin: user-generated` so they're easy to tell apart in the UI. The same
171
+ generators are available from the terminal:
172
+
173
+ ```bash
174
+ spectoflow skill create "reviews PRs for accessibility" # or: --auto to propose candidates
175
+ spectoflow agent create "owns accessibility review" # or: --auto
176
+ spectoflow dashboard create "a KPI overview for support" # or: --auto
177
+ ```
178
+
179
+ Each streams the agent's run live and exits with its status — the same pipeline the dashboard's
180
+ Generate/Auto buttons use, just from a shell.
181
+
161
182
  ## Agents vs skills
162
183
 
163
184
  Agents (`.spectoflow/agents/`) are **stable team personas** (Product Manager, Developer, QA Engineer…).
package/bin/spectoflow.js CHANGED
@@ -10,6 +10,8 @@ const detect = require('../lib/detect');
10
10
  const ownership = require('../lib/ownership');
11
11
  const manifest = require('../lib/manifest');
12
12
  const mcp = require('../lib/mcp');
13
+ const { startRun } = require('../templates/dashboard/runner');
14
+ const { buildCustomizePrompt } = require('../templates/lib/customize-prompts');
13
15
 
14
16
  const KIT = path.resolve(__dirname, '..');
15
17
  const TPL = path.join(KIT, 'templates');
@@ -257,9 +259,62 @@ async function dashboard() {
257
259
  if (sub === 'stop') return stopDashboard();
258
260
  if (sub === 'status') return dashboardStatus();
259
261
  if (sub === 'restart') return restartDashboard();
262
+ if (sub === 'create') return runCustomize('dashboard');
260
263
  return startDashboard();
261
264
  }
262
265
 
266
+ // ---- Customize: `spectoflow skill/agent/dashboard create` — the CLI mirror of the dashboard's
267
+ // Settings → Customize UI. Both surfaces build the same natural-language prompt (customize-prompts.js)
268
+ // and post it through the same pipeline (runner.js's startRun — the function /api/run itself calls),
269
+ // so a generation triggered from the terminal behaves identically to one triggered from a click.
270
+ function requireProjectRoot() {
271
+ const root = process.cwd();
272
+ if (!fs.existsSync(path.join(root, '.spectoflow'))) {
273
+ console.log('No spectoflow project here. Run: spectoflow init');
274
+ return null;
275
+ }
276
+ return root;
277
+ }
278
+ // "create <description words…> [--auto] [--agent=name]" → { description, auto, agentOverride }.
279
+ // Words are re-joined with spaces so an unquoted multi-word description works the same as a quoted one.
280
+ function parseCreateArgs(args) {
281
+ return {
282
+ auto: args.includes('--auto'),
283
+ agentOverride: (args.find((a) => a.startsWith('--agent=')) || '').split('=')[1] || undefined,
284
+ description: args.filter((a) => !a.startsWith('--')).join(' ').trim(),
285
+ };
286
+ }
287
+ function printCreateUsage(kind) {
288
+ console.log(`Usage: spectoflow ${kind} create "<description>" ${c.dim('[--agent=name]')}`);
289
+ console.log(` or: spectoflow ${kind} create --auto ${c.dim('[--agent=name]')}`);
290
+ }
291
+ // Streams the same events the dashboard's SSE feed would show: raw output lines as-is, and
292
+ // structured ::spectoflow sentinel messages as "[role] text" (skip the echoed user prompt — printed
293
+ // separately, up front, so it isn't shown twice).
294
+ function cliEmit(evt) {
295
+ if (evt.type === 'run-line') process.stdout.write(evt.chunk);
296
+ else if (evt.type === 'message' && evt.message && evt.message.role !== 'user') {
297
+ console.log(`${c.cy('[' + evt.message.role + ']')} ${evt.message.text}`);
298
+ }
299
+ }
300
+ async function runCustomize(kind) {
301
+ const root = requireProjectRoot();
302
+ if (!root) return;
303
+ if (argv[1] !== 'create') return printCreateUsage(kind);
304
+ const { auto, agentOverride, description } = parseCreateArgs(argv.slice(2));
305
+ let prompt;
306
+ try { prompt = buildCustomizePrompt(kind, { auto, description }); }
307
+ catch (e) { console.log(c.y(e.message)); console.log(''); return printCreateUsage(kind); }
308
+ console.log(c.dim(`→ ${prompt}`));
309
+ const code = await new Promise((resolve) => {
310
+ const r = startRun(root, { prompt, agent: agentOverride }, cliEmit);
311
+ if (r.error) { console.log(c.y(r.error)); return resolve(1); }
312
+ if (!r.child) return resolve(1); // spawn failed — cliEmit already printed the error
313
+ r.child.on('close', (exitCode) => resolve(exitCode == null ? 1 : exitCode));
314
+ });
315
+ process.exitCode = code;
316
+ }
317
+
263
318
  // Start in the background and return control. Probes first so a second start just reports the running
264
319
  // one instead of spawning a duplicate (and never crashes on EADDRINUSE).
265
320
  async function startDashboard() {
@@ -396,6 +451,11 @@ ${c.bold('Dashboard')}
396
451
  ${c.g('dashboard stop')} stop it ${c.dim('(alias: stop)')}
397
452
  ${c.g('dashboard restart')} stop then start
398
453
 
454
+ ${c.bold('Customize')} ${c.dim('— same as Settings → Customize, from the terminal')}
455
+ ${c.g('skill create')} ${c.dim('"<description>" | --auto')} generate a project skill
456
+ ${c.g('agent create')} ${c.dim('"<description>" | --auto')} generate a project agent
457
+ ${c.g('dashboard create')} ${c.dim('"<description>" | --auto')} generate a custom dashboard
458
+
399
459
  ${c.bold('Explore')}
400
460
  ${c.g('list')} agents, skills and the workflow at a glance
401
461
  ${c.g('agents')} list the team personas
@@ -421,12 +481,22 @@ const HELP = {
421
481
  to this CLI's version, ${c.bold('preserving your work')}: config.json, workflow.md, specs/, plans/
422
482
  and any agent/skill you edited are never overwritten (an edited file's new version lands as
423
483
  ${c.dim('*.new')} for you to merge). ${c.g('--dry-run')} previews without writing.`,
424
- dashboard: `${c.bold('spectoflow dashboard')} ${c.dim('[--port=NNNN] [status|stop|restart]')}\n
484
+ dashboard: `${c.bold('spectoflow dashboard')} ${c.dim('[--port=NNNN] [status|stop|restart|create]')}\n
425
485
  Start the local control plane in the ${c.bold('background')} (default ${c.dim('4319')} or
426
486
  ${c.dim('$SPECTOFLOW_PORT')}) and hand the prompt back. Subcommands:
427
487
  ${c.g('status')} is it running? (url + pid)
428
488
  ${c.g('stop')} stop it ${c.dim('(alias: spectoflow stop)')}
429
- ${c.g('restart')} stop then start`,
489
+ ${c.g('restart')} stop then start
490
+ ${c.g('create')} generate a custom dashboard, e.g. ${c.dim('spectoflow dashboard create "..." --auto')}`,
491
+ skill: `${c.bold('spectoflow skill create')} ${c.dim('"<description>" [--agent=name]')}\n${c.bold('spectoflow skill create')} ${c.dim('--auto [--agent=name]')}\n
492
+ Generate a project-specific skill — the CLI mirror of Settings → Customize → ${c.bold('Skills')} →
493
+ ${c.bold('Add skill')} in the dashboard. Describe what it should do, or pass ${c.g('--auto')} to have
494
+ the agent survey the project and propose candidates instead. Runs the configured agent headless
495
+ (${c.dim('config.json → agent')}, or override with ${c.g('--agent=')}), streaming its output live;
496
+ it clarifies first if the ask is ambiguous, and marks what it writes ${c.dim('origin: user-generated')}.`,
497
+ agent: `${c.bold('spectoflow agent create')} ${c.dim('"<description>" [--agent=name]')}\n${c.bold('spectoflow agent create')} ${c.dim('--auto [--agent=name]')}\n
498
+ Generate a project-specific agent — the CLI mirror of Settings → Customize → ${c.bold('Agents')} →
499
+ ${c.bold('Add agent')}. Same behaviour as ${c.g('spectoflow skill create')}, for an agent persona instead.`,
430
500
  status: `${c.bold('spectoflow status')}\n
431
501
  Print project progress from ${c.dim('plans/*.md')} (tasks done, specs, agents, skills, in-progress
432
502
  items) and whether the dashboard is currently running.`,
@@ -446,6 +516,8 @@ const fns = {
446
516
  agents: () => { console.log(wordmark()); printAgents(false); },
447
517
  skills: () => { console.log(wordmark()); printSkills(false); },
448
518
  workflow: () => { console.log(wordmark()); printWorkflow(false); },
519
+ skill: () => runCustomize('skill'),
520
+ agent: () => runCustomize('agent'),
449
521
  };
450
522
  const wantsHelp = argv.slice(1).some((a) => a === '-h' || a === '--help');
451
523
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectoflow",
3
- "version": "0.17.5",
3
+ "version": "0.19.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",
@@ -45,9 +45,11 @@ whole file. This lets the dashboard and you co-edit without clobbering. Reflect
45
45
 
46
46
  ## The Router (run internally on every request)
47
47
 
48
- 1. **Intake** — known task ("develop T-012") → load it from `plans/*.md`. New request or tweak →
49
- clarify (step 2) then classify. Explicit override ("just do it quick" / "full change") forced
50
- level, **policy still applies**.
48
+ 1. **Intake** — known task ("develop T-012") → load it from `plans/*.md`. A request to **extend
49
+ spectoflow itself** add a custom dashboard, a new skill, or a new agent routes to
50
+ **Customize** (see below), not the normal delivery pipeline. New request or tweak → clarify
51
+ (step 2) then classify. Explicit override ("just do it quick" / "full change") → forced level,
52
+ **policy still applies**.
51
53
  2. **Clarify (before classifying)** — if the request is ambiguous or under-specified (a vague symptom
52
54
  like "login doesn't work" or "displays badly", missing acceptance, several plausible readings,
53
55
  unclear scope/users), **do not guess and do not start**. Reflect it back in one sentence, then **ask
@@ -77,6 +79,30 @@ don't leave the user unsure what to do.** Keep it to a few lines:
77
79
  1. Say what you want to build (plain language — no ceremonial command needed).
78
80
  2. The dashboard: tell them it's at its URL (see Dashboard below), and whether it's already running.
79
81
 
82
+ ## Customize spectoflow itself — dashboards, skills, agents
83
+
84
+ The dashboard's **Settings → Customize** page (or a direct request in the same shape) lets the user
85
+ extend the framework for *this* project: a purpose-built dashboard page, a new skill, or a new agent.
86
+ Recognize this as its own request shape — distinct from a normal feature/bug request — whenever it
87
+ asks to **add/create a dashboard, a skill, or an agent** for the project (e.g. "add a dashboard that
88
+ shows my architecture", "create a skill for security review grounded in OWASP", "propose dashboards
89
+ worth building" for the Auto mode). Hand it to the `framework-curator` agent (capability
90
+ `customization`, see `.spectoflow/capabilities.md`), which runs one of:
91
+
92
+ - **`generate-dashboard`** — a declarative block-spec page (never raw HTML/CSS/JS — see
93
+ `.spectoflow/skills/generate-dashboard` for why), written to
94
+ `.spectoflow/dashboard/custom/<id>.json`.
95
+ - **`generate-skill`** — a new `.spectoflow/skills/<slug>/SKILL.md`, grounded in real, cited domain
96
+ standards, following the gold-standard shape.
97
+ - **`generate-agent`** — a new `.spectoflow/agents/<slug>.md` persona, same shape discipline.
98
+ - **`propose-customizations`** — the "Auto" mode: analyzes the project and proposes a short list of
99
+ candidates (with a one-line rationale each) instead of generating from a description.
100
+
101
+ **Still clarify first** (step 2 above) when the ask is vague — this is exactly the kind of request
102
+ `clarify` exists for. **Still gated by mode and policy** like any other change; no special-casing.
103
+ Report progress through the group chat as usual, so the requester watches it happen and answers any
104
+ clarifying question there.
105
+
80
106
  ## Workflow, capabilities, agents, skills
81
107
 
82
108
  - The **active workflow** is `.spectoflow/workflow.md` — a checklist of enabled steps, editable (also
@@ -30,6 +30,12 @@ sit at the project root and just point back here.
30
30
  agents` / `spectoflow skills` / `spectoflow workflow`. Append `-h` to any command for its help.
31
31
  - **Change how it runs** in the dashboard's **Settings** tab (autonomy mode, output language, and the
32
32
  dashboard **design**), or by editing `config.json`.
33
+ - **Extend spectoflow itself** from Settings → **Customize**: describe a project-specific dashboard,
34
+ skill, or agent (or hit **Auto** to have it propose candidates from your project), and it's generated
35
+ for you — a dashboard appears in the nav immediately, a skill/agent follows the same gold-standard
36
+ shape as the shipped ones and is marked `origin: user-generated`. Same thing from the terminal:
37
+ `spectoflow skill create "<description>"` / `agent create` / `dashboard create` (each also takes
38
+ `--auto`).
33
39
  - **Update the framework** to a newer kit: `spectoflow update` (preserves your edits; a file you
34
40
  changed is kept and its new version is written next to it as `*.new`).
35
41
 
@@ -53,7 +59,7 @@ Your **artifacts are markdown, and they live at the project root, not in here**:
53
59
  | `config.json` | Your settings: `mode`, `language`, active `agent`, `runners`, `design`, plans/specs dir. **Yours to edit** — `update` never overwrites it. |
54
60
  | `agents/` | **Stable team personas** (product-manager, developer, qa-engineer, code-reviewer, spec-source-guardian…) — the *who*. |
55
61
  | `skills/` | **Evolving procedures** (clarify, brainstorm, write-spec, write-plan, implement, write-e2e-tests, code-review, audit-source…) — the *how*. A workflow step → a capability → its agent → runs a skill. |
56
- | `dashboard/` | The zero-dependency control plane: `server.js` (SSE + file-watch), `runner.js`, `orchestrator.js`, and `public/` (the UI, charts, designs, fonts). |
62
+ | `dashboard/` | The zero-dependency control plane: `server.js` (SSE + file-watch), `runner.js`, `orchestrator.js`, `public/` (the UI, charts, designs, fonts), and `custom/` (your generated dashboards, one JSON spec per file). |
57
63
  | `lib/` | The markdown storage engine (`store.js`) and helpers (e.g. `spec-drift.js` for the spec-source-guardian). |
58
64
  | `hooks/` | Optional Claude Code hooks you can wire in yourself (e.g. `spec-drift.js`, a `Stop` hook that surfaces source-of-truth drift to the Attention tab). |
59
65
  | `runtime.json` | **Volatile execution state** (running agents, orchestration, group-chat messages, attention items, history). Gitignored — safe to delete; it's rebuilt. |
@@ -0,0 +1,94 @@
1
+ ---
2
+ name: framework-curator
3
+ title: Framework Curator
4
+ capability: customization
5
+ uses: [generate-dashboard, generate-skill, generate-agent, propose-customizations]
6
+ description: Extends spectoflow itself for this project — custom dashboards, skills and agents, generated from a description or proposed automatically.
7
+ standards: [gold-standard agents & skills shape, declarative UI generation]
8
+ ---
9
+ # Framework Curator
10
+
11
+ Stable team persona (the "who") for the `customization` capability — the only capability that
12
+ extends **the framework itself**, not the product being delivered. The *how* lives in four skills
13
+ (see `uses`): `generate-dashboard`, `generate-skill`, `generate-agent` turn a description (or an
14
+ auto-analysis) into a real, working extension; `propose-customizations` is the "Auto" mode that
15
+ suggests candidates instead of requiring a description. Delegate here whenever the request is to add
16
+ a dashboard page, a skill, or an agent to *this* project's copy of spectoflow — from the dashboard's
17
+ Settings → Customize page, or asked directly in chat.
18
+
19
+ ## Mandate
20
+
21
+ Grow spectoflow to fit the project it's installed in, without ever degrading what's already there.
22
+ Every dashboard this role generates must look and behave as if the framework's own authors built it —
23
+ same design-token discipline, same responsiveness, same restraint. Every skill or agent it generates
24
+ must earn its place next to the shipped roster: grounded in a real, named standard, not generic
25
+ advice dressed up as a procedure. This role does not build product features; it builds the tools the
26
+ project's own team will use to build product features.
27
+
28
+ ## Operating standards
29
+
30
+ - **Declarative dashboards, never raw markup (see `generate-dashboard`).** A custom dashboard is
31
+ produced as a block spec chosen from the framework's fixed vocabulary
32
+ (`.spectoflow/lib/custom-dashboard.js`), rendered by the exact same token-driven components the
33
+ built-in Board uses. Why: this is what guarantees a generated dashboard matches the *active* design
34
+ and every future one the user switches to, with zero page-specific CSS to keep in sync, and no
35
+ arbitrary generated code ever executing in the dashboard.
36
+ - **Gold-standard shape for skills and agents (`docs/agents-skills-standard.md`).** A generated
37
+ `SKILL.md` or agent `.md` follows the exact same front-matter and heading structure as every
38
+ shipped one — `## When to use` / `## Method` / `## Output contract` / `## Quality bar` /
39
+ `## References` for a skill; `## Mandate` / `## Operating standards` / `## Definition of done` /
40
+ `## Handoff` / `## Guardrails` / `## References` for an agent. Why: a skill or agent that doesn't
41
+ match the shape the dashboard's Agents & Skills tab and the rest of the framework expect degrades
42
+ the whole system's consistency, not just its own file.
43
+ - **Ground every generated skill in a real, current, cited standard for its domain** — a security
44
+ skill cites OWASP (ASVS/Top 10) or an equivalent named authority, an architecture skill cites C4/ADR
45
+ or an equivalent, and so on (see `generate-skill`'s Method for how to identify and verify the right
46
+ one). Why: the whole point of a skill is to encode a domain's actual best practice, not the model's
47
+ unaided guess at what "good" looks like — the same discipline the framework's own shipped skills
48
+ already follow (see any of them for a worked example).
49
+ - **Clarify before generating, using the existing reflex.** A vague ask ("add a dashboard for my
50
+ project") is exactly what `.spectoflow/skills/clarify` exists for — reflect it back, ask one
51
+ targeted question at a time with a recommendation, converge, then generate. Never guess a
52
+ dashboard's content or a skill's domain from a one-line request.
53
+ - **Offer Auto when the user doesn't know what they want yet.** `propose-customizations` reads the
54
+ project (specs, plans, existing agents/skills/dashboards, project type) and proposes a short,
55
+ concrete, justified list — not a generic menu — so a user who doesn't know exactly what to ask for
56
+ still gets somewhere useful in one step.
57
+
58
+ ## Definition of done
59
+
60
+ A generated dashboard renders correctly in every shipped design (light and dark) without a single
61
+ hardcoded color or manual style — verified by construction, since only the declarative block
62
+ vocabulary was used. A generated skill or agent passes the same quality bar the framework's own
63
+ shipped files are held to: real citations in `## References`, a checkable `## Quality bar` /
64
+ `## Definition of done`, and front-matter that `templates/lib/store.js`'s flat parser can read
65
+ unchanged. The new dashboard tab, skill, or agent is visible in the dashboard (Board's nav / Agents &
66
+ Skills tab) on the very next SSE tick — no manual refresh, no extra registration step.
67
+
68
+ ## Handoff
69
+
70
+ Writes the generated file(s) directly (`.spectoflow/dashboard/custom/<id>.json`,
71
+ `.spectoflow/skills/<slug>/SKILL.md`, or `.spectoflow/agents/<slug>.md`) and reports through the
72
+ `::spectoflow` sentinel (see each skill's Output contract for its exact syntax) so the requester sees
73
+ it land in the group chat and the dashboard picks it up live. A dashboard spec that fails
74
+ `.spectoflow/lib/custom-dashboard.js`'s validation, or a skill/agent file whose front-matter the flat
75
+ parser can't read, is not done — fix it before reporting completion, never leave a broken file for the
76
+ dashboard to silently skip.
77
+
78
+ ## Guardrails
79
+
80
+ - Never generates a dashboard block that isn't in the vocabulary `generate-dashboard` documents — an
81
+ unrecognized block type is invisible to the renderer, not a graceful degrade.
82
+ - Never removes or renames `name`, `capability`, `uses`, `description` (agents) or `name`,
83
+ `description` (skills) — only adds keys, per `docs/agents-skills-standard.md`'s front-matter rules.
84
+ - Never invents a "standard" to cite — if no real, verifiable authority exists for the requested
85
+ domain, say so and generate the skill's method from first principles instead, flagged as such,
86
+ rather than fabricating a citation.
87
+ - Never overwrites an existing custom dashboard/skill/agent silently on a regeneration — confirm with
88
+ the requester first (per mode gating) when a chosen id/slug already exists.
89
+
90
+ ## References
91
+
92
+ - `docs/agents-skills-standard.md` — the gold-standard shape this role's output must match.
93
+ - `.spectoflow/lib/custom-dashboard.js` — the declarative block vocabulary and its validator.
94
+ - `.spectoflow/skills/clarify` — the reflex this role leans on before generating from an ambiguous ask.
@@ -3,7 +3,7 @@
3
3
  A capability is a role; an agent implements it; a skill is the procedure it runs. Workflows ask for a
4
4
  capability, never a named agent — this keeps spectoflow agent-agnostic.
5
5
 
6
- Palette: intake · research · analysis · architecture · planning · testing · implementation · security · quality · design · operations · governance.
6
+ Palette: intake · research · analysis · architecture · planning · testing · implementation · security · quality · design · operations · governance · customization.
7
7
 
8
8
  `governance` is the odd one out: it is **advisory, not a workflow step**. The `spec-source-guardian`
9
9
  (skill `audit-source`) watches that the spec (intent) and the code/tests (reality) stay coherent, and
@@ -14,6 +14,15 @@ agent reflects it back and asks **one targeted question at a time** (each with a
14
14
  the need is crisp, then proceeds — it feeds the workflow, never replaces it. See `skills/clarify` and
15
15
  the Clarify step in `AGENTS.md`.
16
16
 
17
+ `customization` is also **not a workflow step** — it is triggered explicitly, either from the
18
+ dashboard's Settings → Customize page or by a direct request ("add a dashboard for…", "create a skill
19
+ for…", "create an agent for…"). The `framework-curator` agent owns it, running one of four skills:
20
+ `generate-dashboard` (a declarative block-spec page — see `templates/lib/custom-dashboard.js`),
21
+ `generate-skill`, `generate-agent` (both follow `docs/agents-skills-standard.md`'s gold-standard
22
+ shape, grounded in real, cited domain standards), and `propose-customizations` (the "Auto" mode:
23
+ analyzes the project and proposes candidates instead of taking a description). Still gated by mode
24
+ and policy like any other change — no special-casing.
25
+
17
26
  | Project type | Active capabilities |
18
27
  |---|---|
19
28
  | app / web / API | all |
@@ -0,0 +1,3 @@
1
+ # This directory holds user-generated custom dashboard specs (created via the Customize page in
2
+ # Settings, or written directly by the `generate-dashboard` skill). Empty on a fresh install.
3
+ # Schema: templates/lib/custom-dashboard.js
@@ -141,6 +141,7 @@ function render(){
141
141
  renderOverview(); renderBoard(); renderBacklog(); renderWorkflow(); renderTeam();
142
142
  renderChatLog($('#chatLog')); renderChatLog($('#chatTabLog'));
143
143
  renderSidebar(); renderRequests(); renderAttention(); renderInfo(); renderSettings();
144
+ renderCustomDashboards(); // adds/removes nav tabs + panels before applyActiveTab() below reads them
144
145
  applyActiveTab(); // re-apply the current tab so an SSE-driven re-render never resets to Board
145
146
  applyI18nStatic(); // re-translate the static markup (nav, headers, placeholders…) for this tick's language
146
147
  }
@@ -729,6 +730,7 @@ function renderSettings(){
729
730
  rows.forEach(([k,v])=>{ const r=el('div','settings-ro-row'); r.append(el('span','settings-ro-k',k), el('span','settings-ro-v',String(v))); box.append(r); });
730
731
  }
731
732
  const fv=$('#footerVer'); if(fv) fv.textContent = (P&&P.version) ? ('v'+P.version) : '';
733
+ renderCustomize();
732
734
  }
733
735
  async function saveSettings(){
734
736
  flash();
@@ -737,13 +739,190 @@ async function saveSettings(){
737
739
  const s=$('#settingsSaved'); if(s){ s.hidden=false; setTimeout(()=>{ s.hidden=true; },1500); }
738
740
  }
739
741
 
742
+ // ---- Custom dashboards (Customize page → generate-dashboard skill) ---------------------------
743
+ // A custom dashboard is a DECLARATIVE block spec (.spectoflow/dashboard/custom/<id>.json, embedded
744
+ // in P.customDashboards by the server) — never raw HTML/CSS/JS. Every block below reuses the exact
745
+ // same components the built-in Board renders with (kpiCard/ocard/bars/donut/statTile/mdLite/el), so a
746
+ // generated dashboard automatically matches the active design — and any design switched to later —
747
+ // with zero page-specific styling. Mirrors the schema in .spectoflow/lib/custom-dashboard.js (the
748
+ // Node-side validator); this is the independent browser-side reader for the same shape.
749
+ function resolveBind(s,bindPath,fallback){
750
+ if(bindPath==null) return fallback;
751
+ let v=s;
752
+ for(const p of String(bindPath).split('.')){ if(v==null) return fallback; v=v[p]; }
753
+ return v==null?fallback:v;
754
+ }
755
+ function renderCustomBlock(b,s){
756
+ switch(b.type){
757
+ case 'markdown': return htmlBlock('cd-markdown', mdLite(b.content||''));
758
+ case 'kpi-row': {
759
+ const row=el('div','kpi-row');
760
+ (b.items||[]).forEach(it=>{
761
+ const val=it.bind!=null?resolveBind(s,it.bind,it.value):it.value;
762
+ row.append(kpiCard(it.label||'', numBlock(val==null?'—':val,it.color||'var(--signal)'), it.sub||'', cssv(it.colorVar||'--signal')));
763
+ });
764
+ return row;
765
+ }
766
+ case 'chart-bars': {
767
+ const rows=(b.rows||[]).map(r=>({label:r.label||'', pct:r.bind!=null?(resolveBind(s,r.bind,r.pct||0)):(r.pct||0), sub:r.sub||''}));
768
+ return ocard(b.title||'', bars(rows));
769
+ }
770
+ case 'chart-donut': {
771
+ const segs=(b.segments||[]).map(g=>({key:g.key||'', value:g.bind!=null?(resolveBind(s,g.bind,g.value||0)):(g.value||0), color:cssv(g.colorVar||'--muted')}));
772
+ const total=segs.reduce((a,x)=>a+(Number(x.value)||0),0);
773
+ const d=donut(segs,140,{center:String(total),sub:t('chart.tasksSub')});
774
+ const legend=el('div','legend');
775
+ segs.forEach(seg=>{ const item=el('div','legend-item'); const sw=el('span','legend-swatch'); sw.style.background=seg.color; item.append(sw, el('span','legend-label',seg.key), el('span','legend-count',String(seg.value))); legend.append(item); });
776
+ const row=el('div','donut-row'); row.append(d.wrap,legend);
777
+ return ocard(b.title||'', row);
778
+ }
779
+ case 'table': {
780
+ const wrap=el('div','table-scroll'); const tbl=el('table','backlog-table');
781
+ const thead=el('thead'); const htr=el('tr'); (b.columns||[]).forEach(c=>htr.append(el('th',null,String(c)))); thead.append(htr); tbl.append(thead);
782
+ const tbody=el('tbody'); (b.rows||[]).forEach(r=>{ const tr=el('tr'); (r||[]).forEach(c=>tr.append(el('td',null,String(c)))); tbody.append(tr); }); tbl.append(tbody);
783
+ wrap.append(tbl);
784
+ return b.title? ocard(b.title,wrap) : wrap;
785
+ }
786
+ case 'list': {
787
+ const ul=el('ul','flatlist'); (b.items||[]).forEach(x=>ul.append(li(null,String(x))));
788
+ return b.title? ocard(b.title,ul) : ul;
789
+ }
790
+ case 'stat-tile-row': {
791
+ const row=el('div','stat-tiles');
792
+ (b.items||[]).forEach(it=>{ const val=it.bind!=null?resolveBind(s,it.bind,it.value):it.value; row.append(statTile(val==null?'—':String(val), it.label||'', it.sub||'')); });
793
+ return row;
794
+ }
795
+ default: return el('div','empty','Unknown block type: '+b.type);
796
+ }
797
+ }
798
+ function customDashboardTabs(){ return $$('#tabs .tab[data-tab^="custom:"]'); }
799
+ // Adds/removes nav tabs + panels to match P.customDashboards, and refreshes an existing tab's label
800
+ // if the dashboard was regenerated with a new title. Console's rail and Orbit's radial menu both read
801
+ // #tabs live, so a custom tab appears in either design's navigation with no design-specific change.
802
+ function syncCustomTabs(list){
803
+ const tabsNav=$('#tabs'); if(!tabsNav) return;
804
+ const wanted=new Set(list.map(spec=>'custom:'+spec.id));
805
+ customDashboardTabs().forEach(btn=>{
806
+ const id=btn.dataset.tab;
807
+ if(!wanted.has(id)){ const panel=$('.panel[data-panel="'+id+'"]'); if(panel) panel.remove(); btn.remove(); }
808
+ });
809
+ list.forEach(spec=>{
810
+ const id='custom:'+spec.id;
811
+ let btn=$('#tabs .tab[data-tab="'+id+'"]');
812
+ if(!btn){
813
+ btn=el('button','tab'); btn.dataset.tab=id;
814
+ const icoKey=spec.icon||'info';
815
+ const ico=el('span','tab-ico'); ico.dataset.icon=icoKey; if(typeof ICON!=='undefined'&&ICON[icoKey]) ico.innerHTML=ICON[icoKey];
816
+ btn.append(ico, el('span','tab-label',spec.title||spec.id));
817
+ btn.addEventListener('click',()=>navigateTab(id));
818
+ tabsNav.append(btn);
819
+ const panel=el('section','panel'); panel.dataset.panel=id;
820
+ const wrap=el('div','custom-dash-wrap');
821
+ wrap.append(el('h2','panel-title',spec.title||spec.id));
822
+ wrap.append(el('div','custom-dash-body'));
823
+ panel.append(wrap);
824
+ $('.stage').append(panel);
825
+ } else {
826
+ const lbl=btn.querySelector('.tab-label'); if(lbl) lbl.textContent=spec.title||spec.id;
827
+ const title=$('.panel[data-panel="'+id+'"] .panel-title'); if(title) title.textContent=spec.title||spec.id;
828
+ }
829
+ });
830
+ }
831
+ function renderCustomDashboards(){
832
+ const list=P.customDashboards||[];
833
+ syncCustomTabs(list);
834
+ const s=SpectoStats.stats(P);
835
+ list.forEach(spec=>{
836
+ const box=$('.panel[data-panel="custom:'+spec.id+'"] .custom-dash-body'); if(!box) return;
837
+ box.innerHTML='';
838
+ (spec.blocks||[]).forEach(b=> box.append(renderCustomBlock(b,s)));
839
+ });
840
+ }
841
+
842
+ // ---- Settings → Customize: add dashboards/skills/agents by description, or "Auto" -------------
843
+ // Generation itself is real agent work (research, clarify, write a file) — this UI never does it
844
+ // client-side. It just constructs a plain-language prompt (recognized by AGENTS.md's Router, which
845
+ // hands it to framework-curator) and sends it through the SAME /api/run + group-chat pipeline every
846
+ // other "Run" already uses, then jumps to Chat so the requester watches it happen and can answer any
847
+ // clarifying question there — no separate conversational UI to build or keep in sync.
848
+ const CZ_KINDS=[
849
+ { kind:'dashboard', promptAdd:(d)=>'Add a custom dashboard: '+d, promptAuto:'Propose dashboard candidates for this project (Auto customize)' },
850
+ { kind:'skill', promptAdd:(d)=>'Create a new skill: '+d, promptAuto:'Propose skill candidates for this project (Auto customize)' },
851
+ { kind:'agent', promptAdd:(d)=>'Create a new agent: '+d, promptAuto:'Propose agent candidates for this project (Auto customize)' },
852
+ ];
853
+ function czItemsFor(kind){
854
+ if(kind==='dashboard') return (P.customDashboards||[]).map(spec=>({ title:spec.title||spec.id, sub:(spec.blocks||[]).length+' '+t('customize.blocksSub'), open:()=>navigateTab('custom:'+spec.id) }));
855
+ if(kind==='skill') return (P.skills||[]).filter(s=>s.custom).map(s=>({ title:s.name, sub:s.description||'', open:()=>openFileDrawer('skill',s) }));
856
+ return (P.agents||[]).filter(a=>a.custom).map(a=>({ title:a.title||a.name, sub:a.description||'', open:()=>openFileDrawer('agent',a) }));
857
+ }
858
+ function renderCustomize(){
859
+ const root=$('#czRoot'); if(!root) return;
860
+ const openKind=root.dataset.open||'';
861
+ root.innerHTML='';
862
+ CZ_KINDS.forEach(({kind})=>{
863
+ const items=czItemsFor(kind);
864
+ const block=el('div','cz-block');
865
+ const head=el('div','cz-head');
866
+ head.append(el('h3',null,t('customize.'+kind+'s')+' ('+items.length+')'));
867
+ const addBtn=el('button','btn cz-add',t('customize.add.'+kind));
868
+ addBtn.setAttribute('aria-expanded',String(openKind===kind));
869
+ addBtn.addEventListener('click',()=>{ root.dataset.open=(openKind===kind)?'':kind; renderCustomize(); });
870
+ head.append(addBtn); block.append(head);
871
+ const list=el('div','cz-list');
872
+ if(!items.length) list.append(el('div','empty',t('customize.empty.'+kind)));
873
+ items.forEach(it=>{
874
+ const row=el('div','cz-item'); row.tabIndex=0;
875
+ row.append(el('span','cz-item-title',it.title));
876
+ if(it.sub) row.append(el('span','cz-item-sub',it.sub));
877
+ row.addEventListener('click',it.open);
878
+ row.addEventListener('keydown',(e)=>{ if(e.key==='Enter') it.open(); });
879
+ list.append(row);
880
+ });
881
+ block.append(list);
882
+ if(openKind===kind){
883
+ const form=el('div','cz-form');
884
+ const ta=el('textarea','chat-ta'); ta.placeholder=t('customize.describePh');
885
+ const sel=el('select','chat-agent');
886
+ const runners=Object.keys((P.config&&P.config.runners)||{claude:1});
887
+ runners.forEach((k)=>{ const o=document.createElement('option'); o.value=k; o.textContent=k; sel.append(o); });
888
+ if(P.config&&P.config.agent) sel.value=P.config.agent;
889
+ const actions=el('div','cz-form-actions');
890
+ const autoBtn=el('button','btn',t('customize.auto'));
891
+ const goBtn=el('button','btn primary',t('customize.generate'));
892
+ autoBtn.addEventListener('click',()=>czSubmit(kind,null,sel.value));
893
+ goBtn.addEventListener('click',()=>{ const v=ta.value.trim(); if(v) czSubmit(kind,v,sel.value); });
894
+ actions.append(sel,autoBtn,goBtn);
895
+ form.append(ta,actions);
896
+ block.append(form);
897
+ }
898
+ root.append(block);
899
+ });
900
+ }
901
+ async function czSubmit(kind,description,agent){
902
+ const cfg=CZ_KINDS.find((c)=>c.kind===kind);
903
+ const prompt=description?cfg.promptAdd(description):cfg.promptAuto;
904
+ await fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({prompt,agent})});
905
+ const root=$('#czRoot'); if(root) root.dataset.open='';
906
+ navigateTab('chat');
907
+ }
908
+
740
909
  // ---- client-side routing: /<tab>[/<taskId>] via the History API ------------
910
+ // A custom dashboard (Customize page) gets its own tab id "custom:<id>" and its own URL shape
911
+ // /custom/<id> — kept out of ROUTES (a fixed list) since the set of custom ids is dynamic; recognized
912
+ // by a dedicated branch in tabFromPath()/navigateTab() instead.
741
913
  const ROUTES=['board','requests','attention','backlog','workflow','team','chat','info','settings'];
742
- function tabFromPath(){ const s=location.pathname.split('/').filter(Boolean); return ROUTES.includes(s[0])?s[0]:null; }
914
+ function tabFromPath(){
915
+ const s=location.pathname.split('/').filter(Boolean);
916
+ if(s[0]==='custom'&&s[1]) return 'custom:'+decodeURIComponent(s[1]);
917
+ return ROUTES.includes(s[0])?s[0]:null;
918
+ }
743
919
  function taskFromPath(){ const s=location.pathname.split('/').filter(Boolean); return (ROUTES.includes(s[0])&&s[1])?decodeURIComponent(s[1]):null; }
744
- function navigateTab(t,push){
745
- activeTab=t; try{ localStorage.setItem('spf-tab',t); }catch{}
746
- if(push!==false) history.pushState(null,'','/'+t);
920
+ function navigateTab(tabId,push){
921
+ activeTab=tabId; try{ localStorage.setItem('spf-tab',tabId); }catch{}
922
+ if(push!==false){
923
+ const isCustom=tabId.indexOf('custom:')===0;
924
+ history.pushState(null,'', isCustom ? '/custom/'+encodeURIComponent(tabId.slice(7)) : '/'+tabId);
925
+ }
747
926
  applyActiveTab();
748
927
  closeNav(); // a tab pick closes the mobile menu
749
928
  }