spectoflow 0.17.5 → 0.18.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.
@@ -70,7 +70,13 @@ function promoteAttention(item){
70
70
  }
71
71
 
72
72
  function watch(dir){ try{ fs.watch(dir,{recursive:false},()=>emit({type:'change'})); }catch(_){} }
73
- ['plans','specs','.spectoflow'].forEach(d=>{ const p=path.join(ROOT,d); if(fs.existsSync(p)) watch(p); });
73
+ // Custom dashboards (Customize page) live in their own subdirectory of .spectoflow, which the
74
+ // top-level `.spectoflow` watch below does NOT cover — fs.watch here is non-recursive on purpose
75
+ // (a recursive watch on the whole .spectoflow tree would also fire on every runtime.json write).
76
+ // Ensure the directory exists before watching it: a project that hasn't used Customize yet won't
77
+ // have it on disk, and `spectoflow init` on an older install won't have created it either.
78
+ try { fs.mkdirSync(path.join(ROOT,'.spectoflow','dashboard','custom'), { recursive: true }); } catch (_) {}
79
+ ['plans','specs','.spectoflow','.spectoflow/dashboard/custom'].forEach(d=>{ const p=path.join(ROOT,d); if(fs.existsSync(p)) watch(p); });
74
80
 
75
81
  // A process restart loses any in-flight orchestration; without this, a stale 'running' or
76
82
  // 'awaiting_approval' status wedges the /api/orchestrate 409 guard forever. Not a real
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+ /*
3
+ * Pure helpers for user-generated custom dashboards (.spectoflow/dashboard/custom/<id>.json).
4
+ *
5
+ * A custom dashboard is a DECLARATIVE block spec, never raw HTML/CSS/JS: the generating agent picks
6
+ * blocks from a fixed vocabulary (BLOCK_TYPES) that the dashboard already knows how to render, using
7
+ * the exact same token-driven components (kpi cards, bars, donut, tables…) the built-in Board uses.
8
+ * That is what guarantees a custom dashboard always matches the active design — including any design
9
+ * the user switches to later — with zero per-dashboard styling to keep in sync, and no arbitrary code
10
+ * ever running in the dashboard.
11
+ *
12
+ * Zero dependency; consumed by templates/dashboard/server.js (Node) via readCustomDashboards() in
13
+ * store.js. The browser-side renderer (dashboard/public/app.js) re-implements the tiny `resolveBind`
14
+ * walk independently — sharing code across the Node/browser boundary would need a build step, which
15
+ * this project avoids on purpose (see CLAUDE.md's zero-runtime-dependency invariant).
16
+ */
17
+
18
+ // Every block a generated dashboard may use. Adding a new type here is how the vocabulary grows —
19
+ // pair it with a matching case in app.js's renderCustomBlock().
20
+ const BLOCK_TYPES = new Set([
21
+ 'markdown', // rendered prose — a spec excerpt, an explanation, a summary
22
+ 'kpi-row', // a row of big-number stat cards, each optionally live-bound
23
+ 'chart-bars', // horizontal progress/comparison bars
24
+ 'chart-donut', // a status/category breakdown donut + legend
25
+ 'table', // a simple column/row data table
26
+ 'list', // a flat bullet list
27
+ 'stat-tile-row', // a row of compact stat tiles (value/label/sub)
28
+ ]);
29
+
30
+ // A small, explicit allow-list of live data paths a block may `bind` to, resolved against the same
31
+ // stats object SpectoStats.stats(P) already computes for the built-in Board — never an arbitrary
32
+ // expression, just a dotted property walk, so there is nothing to sandbox or evaluate.
33
+ // Mirrors the exact shape SpectoStats.stats(P) returns (dashboard/public/stats.js):
34
+ // { total, done, pct, byStatus, phases, toAsk, running, statuses }.
35
+ const BIND_ROOTS = new Set(['pct', 'done', 'total', 'byStatus', 'phases', 'toAsk', 'running', 'statuses']);
36
+
37
+ const ID_RE = /^[a-z][a-z0-9-]{0,39}$/;
38
+
39
+ // A conservative, curated icon key set — the same ICON map the rest of the dashboard already uses
40
+ // (icons.js), so a custom dashboard's tab never introduces a one-off, unstyled icon.
41
+ const ICON_KEYS = new Set(['board', 'requests', 'backlog', 'workflow', 'agents', 'chat', 'info', 'attention', 'settings']);
42
+
43
+ function isPlainObject(v) { return v != null && typeof v === 'object' && !Array.isArray(v); }
44
+
45
+ // Validates one block. Returns a list of error strings (empty = valid). Deliberately permissive on
46
+ // the *content* fields (labels, values, markdown text are free text) — it only enforces the block's
47
+ // *shape* (a known type, and that any `bind` path starts from an allowed root) so a slightly unusual
48
+ // but well-typed spec still renders rather than being rejected outright.
49
+ function validateBlock(b, i) {
50
+ const errs = [];
51
+ const at = `blocks[${i}]`;
52
+ if (!isPlainObject(b)) { errs.push(`${at} is not an object`); return errs; }
53
+ if (!BLOCK_TYPES.has(b.type)) errs.push(`${at}.type "${b.type}" is not a known block type (${[...BLOCK_TYPES].join(', ')})`);
54
+ const binds = [];
55
+ if (typeof b.bind === 'string') binds.push(b.bind);
56
+ if (Array.isArray(b.items)) b.items.forEach((it) => { if (it && typeof it.bind === 'string') binds.push(it.bind); });
57
+ if (Array.isArray(b.rows)) b.rows.forEach((r) => { if (r && typeof r.bind === 'string') binds.push(r.bind); });
58
+ if (Array.isArray(b.segments)) b.segments.forEach((s) => { if (s && typeof s.bind === 'string') binds.push(s.bind); });
59
+ binds.forEach((p) => { const root = String(p).split('.')[0]; if (!BIND_ROOTS.has(root)) errs.push(`${at} has an unbound bind path "${p}" (must start with one of: ${[...BIND_ROOTS].join(', ')})`); });
60
+ return errs;
61
+ }
62
+
63
+ // Validates a whole dashboard spec as read from disk. Never throws — callers (store.js) should skip
64
+ // an invalid file rather than let one bad custom dashboard break the whole /api/project response.
65
+ function validateSpec(spec) {
66
+ const errors = [];
67
+ if (!isPlainObject(spec)) return { valid: false, errors: ['not an object'] };
68
+ if (!ID_RE.test(String(spec.id || ''))) errors.push('id must be lowercase kebab-case, starting with a letter, 1-40 chars');
69
+ if (!spec.title || typeof spec.title !== 'string') errors.push('title is required (a short display name)');
70
+ if (spec.icon != null && !ICON_KEYS.has(spec.icon)) errors.push(`icon "${spec.icon}" is not one of: ${[...ICON_KEYS].join(', ')}`);
71
+ if (!Array.isArray(spec.blocks) || !spec.blocks.length) errors.push('blocks must be a non-empty array');
72
+ else spec.blocks.forEach((b, i) => errors.push(...validateBlock(b, i)));
73
+ return { valid: errors.length === 0, errors };
74
+ }
75
+
76
+ module.exports = { BLOCK_TYPES, BIND_ROOTS, ICON_KEYS, validateSpec, validateBlock };
@@ -17,6 +17,7 @@
17
17
  */
18
18
  const fs = require('fs');
19
19
  const path = require('path');
20
+ const { validateSpec } = require('./custom-dashboard');
20
21
 
21
22
  // ---- task line parsing -------------------------------------------------------
22
23
  // - [ ] T-012 Title here @owner ~level %status
@@ -226,6 +227,23 @@ function readWorkflow(projectRoot) {
226
227
  } catch { return []; }
227
228
  }
228
229
 
230
+ // ---- user-generated custom dashboards (.spectoflow/dashboard/custom/<id>.json) --------------
231
+ // One JSON file per custom dashboard page (see lib/custom-dashboard.js for the block schema this
232
+ // validates against). A malformed file is skipped, never thrown — one bad custom dashboard must
233
+ // never take down the whole /api/project response.
234
+ function readCustomDashboards(projectRoot) {
235
+ const dir = path.join(projectRoot, '.spectoflow', 'dashboard', 'custom');
236
+ if (!fs.existsSync(dir)) return [];
237
+ const out = [];
238
+ for (const f of fs.readdirSync(dir).filter((x) => x.endsWith('.json')).sort()) {
239
+ try {
240
+ const spec = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
241
+ if (validateSpec(spec).valid) out.push(spec);
242
+ } catch { /* skip malformed */ }
243
+ }
244
+ return out;
245
+ }
246
+
229
247
  // ---- unified read for the dashboard -----------------------------------------
230
248
  function readProject(projectRoot) {
231
249
  const config = readConfig(projectRoot);
@@ -235,6 +253,7 @@ function readProject(projectRoot) {
235
253
  const specs = readSpecs(projectRoot);
236
254
  const agents = listMd(path.join(projectRoot, '.spectoflow', 'agents'));
237
255
  const skills = listSkills(path.join(projectRoot, '.spectoflow', 'skills'));
256
+ const customDashboards = readCustomDashboards(projectRoot);
238
257
 
239
258
  // Write-guarded snapshot: readProject is polled continuously by the dashboard (and reacts to
240
259
  // fs.watch on .spectoflow). Recording unconditionally on every read would rewrite runtime.json
@@ -260,7 +279,7 @@ function readProject(projectRoot) {
260
279
  runtime = writeRuntime(projectRoot, cur);
261
280
  }
262
281
 
263
- return { config, plans, specs, workflow, agents, skills, runtime };
282
+ return { config, plans, specs, workflow, agents, skills, runtime, customDashboards };
264
283
  }
265
284
  function frontmatter(text) {
266
285
  const m = String(text).replace(/\r\n?/g, '\n').match(/^---\n([\s\S]*?)\n---/);
@@ -274,12 +293,17 @@ function parseFlatList(raw) {
274
293
  if (!raw) return [];
275
294
  return String(raw).replace(/[[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean);
276
295
  }
296
+ // `origin: user-generated` in a file's front-matter (written by generate-skill/generate-agent, see
297
+ // templates/skills/generate-skill and generate-agent) marks it as created through the Customize page
298
+ // rather than shipped by the framework — the dashboard's Customize section uses this `custom` flag to
299
+ // list only the user's own additions, distinct from the framework-shipped roster.
300
+ const isCustomOrigin = (fm) => fm.origin === 'user-generated';
277
301
  function listMd(dir) {
278
302
  if (!fs.existsSync(dir)) return [];
279
303
  return fs.readdirSync(dir).filter((f) => f.endsWith('.md')).map((f) => {
280
304
  const fm = frontmatter(fs.readFileSync(path.join(dir, f), 'utf8'));
281
305
  return { file: f, name: fm.name || f.replace(/\.md$/, ''), title: fm.title || fm.name || f, capability: fm.capability || '', description: fm.description || '',
282
- standards: parseFlatList(fm.standards), uses: parseFlatList(fm.uses) };
306
+ standards: parseFlatList(fm.standards), uses: parseFlatList(fm.uses), custom: isCustomOrigin(fm) };
283
307
  });
284
308
  }
285
309
  function listSkills(dir) {
@@ -288,7 +312,7 @@ function listSkills(dir) {
288
312
  const sk = path.join(dir, e.name, 'SKILL.md');
289
313
  const fm = fs.existsSync(sk) ? frontmatter(fs.readFileSync(sk, 'utf8')) : {};
290
314
  return { name: fm.name || e.name, description: fm.description || '', capability: fm.capability || '',
291
- inputs: fm.inputs || '', outputs: fm.outputs || '', standard: fm.standard || '' };
315
+ inputs: fm.inputs || '', outputs: fm.outputs || '', standard: fm.standard || '', custom: isCustomOrigin(fm) };
292
316
  });
293
317
  }
294
318
  function readAgents(projectRoot) {
@@ -298,7 +322,7 @@ function readAgents(projectRoot) {
298
322
  const fm = frontmatter(fs.readFileSync(path.join(dir, f), 'utf8'));
299
323
  return { name: fm.name || f.replace(/\.md$/, ''), capability: fm.capability || null,
300
324
  title: fm.title || '', description: fm.description || '',
301
- standards: parseFlatList(fm.standards), uses: parseFlatList(fm.uses) };
325
+ standards: parseFlatList(fm.standards), uses: parseFlatList(fm.uses), custom: isCustomOrigin(fm) };
302
326
  });
303
327
  }
304
328
  function readSkills(projectRoot) {
@@ -308,5 +332,5 @@ function readSkills(projectRoot) {
308
332
  module.exports = {
309
333
  parseTaskLine, buildTaskLine, parsePlan, readPlans, readSpecs, updateTaskLine, addTaskComment,
310
334
  readRuntime, writeRuntime, parseAgentLine, appendMessage, readConfig, readWorkflow, readProject,
311
- readAgents, readSkills, recordSnapshot, resolvePlansDir, resolveSpecsDir,
335
+ readAgents, readSkills, readCustomDashboards, recordSnapshot, resolvePlansDir, resolveSpecsDir,
312
336
  };
@@ -0,0 +1,135 @@
1
+ ---
2
+ name: generate-agent
3
+ description: Turn a description (or an auto-analysis) into a new agent persona, grounded in real named methods and matching the framework's gold-standard shape.
4
+ capability: customization
5
+ inputs: A description of the role needed (from the Customize page or chat), or a chosen candidate from propose-customizations; the project's existing agents as worked examples.
6
+ outputs: A new .spectoflow/agents/<slug>.md matching docs/agents-skills-standard.md's shape, listed in the dashboard's Agents & Skills tab on the next tick.
7
+ standard: docs/agents-skills-standard.md gold-standard shape
8
+ ---
9
+ # Generate agent
10
+
11
+ Turn a described role into a real agent persona — a stable team member with a clear mandate, named
12
+ operating standards, and guardrails, that reads like it shipped with the framework's own roster.
13
+
14
+ ## When to use
15
+
16
+ Whenever the user asks (from the Customize page, or directly in chat) to **add an agent** — "I want a
17
+ data-migration specialist", "add an accessibility reviewer", "create an agent for API contract
18
+ reviews" — or when `propose-customizations` proposed an agent candidate the user picked.
19
+
20
+ ## Method
21
+
22
+ ### 1. Clarify before generating
23
+
24
+ A one-line ask ("add a data agent") is under-specified. Use `.spectoflow/skills/clarify`'s reflex —
25
+ one targeted question at a time, each with a recommended default — until you know:
26
+ - **What this role owns that no existing agent already owns.** Read `.spectoflow/agents/*.md` first —
27
+ a new agent for a capability an existing one already covers is redundant; either the existing agent
28
+ should gain a skill instead (see `generate-skill`), or this really is a distinct capability.
29
+ - **Which capability it serves.** Pick the closest match from `.spectoflow/capabilities.md`'s palette,
30
+ or note that this genuinely needs a new capability name (rare — most real needs fit the existing
31
+ palette; propose adding to the palette only when nothing fits).
32
+ - **What skill(s) it runs.** An agent without at least one skill in `uses` has no procedure to
33
+ execute — either an existing skill fits, or this request also needs `generate-skill` (sequence the
34
+ two: skill first, so the agent's `uses` list is accurate from the start).
35
+
36
+ ### 2. Remember the agent/skill split
37
+
38
+ Per the framework's own core invariant: **agents are stable personas (the who); skills are the
39
+ evolving procedures (the how).** This agent's file should describe *who* the role is and what it's
40
+ accountable for — the actual step-by-step method belongs in its skill(s), referenced via `uses`, not
41
+ duplicated here. An agent file heavy with procedural detail has blurred the split; move that content
42
+ into a skill instead.
43
+
44
+ ### 3. Ground the operating standards in named, real methods
45
+
46
+ Per `docs/agents-skills-standard.md`, `## Operating standards` names **cited methods**, each with a
47
+ one-line *why* — the same discipline every shipped agent already follows (open a couple as worked
48
+ examples: `qa-engineer` cites Kent Beck's TDD and Meszaros's xUnit Test Patterns; `security-engineer`
49
+ cites OWASP ASVS and the Top 10; `architect` cites C4 and ADRs). Identify the real, current, named
50
+ authority for this role's domain the same way `generate-skill`'s Method (step 2 there) describes —
51
+ verify it with your environment's research tools rather than relying purely on memory for a
52
+ fast-moving domain, and if no real standard exists for the role's specific angle, say so explicitly
53
+ and reason from first principles instead of fabricating a citation.
54
+
55
+ ### 4. Write the agent in the gold-standard shape
56
+
57
+ Follow `docs/agents-skills-standard.md`'s agent shape exactly:
58
+
59
+ ```yaml
60
+ ---
61
+ name: <slug>
62
+ title: <Team title>
63
+ capability: <the palette capability chosen in step 1>
64
+ uses: [<skill-slug>, ...]
65
+ description: <one line>
66
+ standards: [<named method or source>, ...]
67
+ ---
68
+ # <Title>
69
+ <1-2 line intro naming the persona and the capability it serves>
70
+
71
+ ## Mandate
72
+ <who/why, 1-2 lines — what this role owns>
73
+
74
+ ## Operating standards
75
+ <named, cited methods this role applies, each with a one-line why — from step 3>
76
+
77
+ ## Definition of done
78
+ <concrete, checkable exit criteria for this role's contribution>
79
+
80
+ ## Handoff
81
+ <what it produces and to whom — feeds the group-chat identity + orchestrator>
82
+
83
+ ## Guardrails
84
+ <what it must never do — ties to .spectoflow/policy.md where relevant>
85
+
86
+ ## References
87
+ <the real, verified sources from step 3, as titled links>
88
+ ```
89
+
90
+ ### 5. Mark it as user-generated
91
+
92
+ Add `origin: user-generated` to the front-matter (an extra key — never remove or rename the required
93
+ ones: `name`, `title`, `capability`, `uses`, `description`). This is how the dashboard's Customize
94
+ page distinguishes what the user added from the shipped roster; omitting it hides the agent from that
95
+ list.
96
+
97
+ ### 6. Resolve capability collisions explicitly
98
+
99
+ `.spectoflow/AGENTS.md`'s routing assumes one agent per capability unless a `priority` is set (see the
100
+ front-matter rules in `docs/agents-skills-standard.md`). If the chosen capability already has an
101
+ agent, either pick a different, more precise capability for this role, or set `priority` deliberately
102
+ and tell the user which agent now wins ties — never leave two agents silently competing for the same
103
+ capability with no way to tell which runs.
104
+
105
+ ## Output contract
106
+
107
+ - One file: `.spectoflow/agents/<slug>.md`, matching the gold-standard shape, with
108
+ `origin: user-generated` in its front-matter.
109
+ - Progress and completion reported to the orchestrator and group chat:
110
+
111
+ ```
112
+ ::spectoflow role=customization kind=progress msg=Drafting agent "<title>" (capability <capability>)
113
+ ::spectoflow role=customization kind=need msg=<what's missing, e.g. no skill yet for this agent to use>
114
+ ::spectoflow role=customization kind=done msg=Agent "<title>" added — see it in Agents & Skills
115
+ ```
116
+
117
+ ## Quality bar
118
+
119
+ - [ ] Front-matter matches the gold-standard shape exactly, plus `origin: user-generated`.
120
+ - [ ] Body has exactly the five required `##` headings, in order.
121
+ - [ ] `uses` lists at least one real, existing (or just-generated) skill — never an empty list.
122
+ - [ ] `## Operating standards` names real, verified, cited methods — or explicitly says none exist for
123
+ this angle and reasons from first principles instead. Never a fabricated citation.
124
+ - [ ] No capability collision left unresolved (step 6) — or the `priority` tie-break is explicit and
125
+ explained to the user.
126
+ - [ ] The role is genuinely distinct from every existing agent — not a duplicate the user could have
127
+ gotten by adding a skill to one that already exists.
128
+ - [ ] If the ask was ambiguous, it was clarified one question at a time before any file was written.
129
+
130
+ ## References
131
+
132
+ - `docs/agents-skills-standard.md` — the gold-standard shape this agent's output must match exactly.
133
+ - Any shipped agent under `.spectoflow/agents/` (e.g. `qa-engineer`, `security-engineer`,
134
+ `spec-source-guardian`) — worked examples of real citation density in `## Operating standards`.
135
+ - `.spectoflow/capabilities.md` — the capability palette a new agent's `capability` must fit.
@@ -0,0 +1,152 @@
1
+ ---
2
+ name: generate-dashboard
3
+ description: Turn a description (or an auto-analysis) into a new custom dashboard page, as a declarative block spec that automatically matches every design the dashboard ships.
4
+ capability: customization
5
+ inputs: A description of what the dashboard should show (from the Customize page or chat), or a chosen candidate from propose-customizations; the project's specs/plans/code as source material.
6
+ outputs: A validated block-spec JSON file at .spectoflow/dashboard/custom/<id>.json, live in the dashboard's nav on the next tick.
7
+ standard: declarative UI generation; Few's dashboard design principles
8
+ ---
9
+ # Generate dashboard
10
+
11
+ Turn a described need into a new dashboard page for *this* project — added to the dashboard's own
12
+ navigation, rendered by the dashboard's own components, so it looks and behaves like it shipped with
13
+ the framework, not like a plugin bolted on.
14
+
15
+ ## When to use
16
+
17
+ Whenever the user asks (from the Customize page, or directly in chat) to **add a dashboard** —
18
+ "I want a dashboard that shows my architecture", "add a page tracking API endpoint coverage", "show me
19
+ a dashboard of open security findings" — or when `propose-customizations` proposed a dashboard
20
+ candidate the user picked.
21
+
22
+ ## Method
23
+
24
+ ### 1. Never generate raw markup — only the declarative block vocabulary
25
+
26
+ The dashboard renders a custom page from a **JSON block spec**, using the exact same components the
27
+ built-in Board uses (`kpiCard`, `ocard`, `bars`, `donut`, a table builder, `mdLite` for markdown —
28
+ see `dashboard/public/app.js`). This is not a stylistic preference: it is the mechanism that makes the
29
+ result correct.
30
+
31
+ - Every block type is styled entirely through the active design's CSS custom properties
32
+ (`--signal`, `--surface`, `--line`, `--s-done`, …). Nothing in a block spec ever sets a literal
33
+ color, font, radius, or shadow.
34
+ - Because of that, a dashboard generated under one design (say, the default Spectral Console) renders
35
+ correctly, unmodified, under every other shipped design (Orbit, Control Room, Obsidian Ops, Neon
36
+ Command, Mission Control) — including any the user switches to **after** this dashboard was
37
+ generated. There is nothing design-specific to regenerate or maintain.
38
+ - It is also the safety boundary: a block spec is data, never executable code, so nothing this skill
39
+ writes can run arbitrary script in the user's dashboard.
40
+
41
+ **Never** write HTML, CSS, or JS for a custom dashboard, and never suggest doing so "for more
42
+ flexibility" — if the vocabulary genuinely can't express what's needed, say so explicitly (raise a
43
+ `need`) rather than stepping outside it.
44
+
45
+ ### 2. Clarify before generating
46
+
47
+ A one-line ask ("add a dashboard for my project") is under-specified — you don't yet know what it
48
+ should show. Use `.spectoflow/skills/clarify`'s reflex: reflect the ask back, then ask one targeted
49
+ question at a time, each with a recommended default, until you know:
50
+ - **What it should show** (which data/content — an architecture overview, security posture, a
51
+ specific spec's status, custom KPIs…).
52
+ - **Static or live.** Does it need to update as the project changes (task counts, phase progress), or
53
+ is a point-in-time snapshot the actual intent (e.g. "show my chosen architecture" — a design
54
+ decision doesn't change every time a task ships)?
55
+
56
+ Skip clarification only when the ask is already unambiguous, or the user picked a fully-specified
57
+ candidate from `propose-customizations`.
58
+
59
+ ### 3. Gather the source material
60
+
61
+ Read what the dashboard needs to show from the project itself — `specs/*.md`, `plans/*.md`, ADRs,
62
+ `.spectoflow/agents/`, `.spectoflow/skills/`, or the codebase, as the ask requires. For "my
63
+ architecture", that typically means reading a spec/ADR that documents it and turning its structure
64
+ into `markdown` blocks (rendered as-is) plus maybe a `list`/`table` block for components or decisions.
65
+ For "task/security/coverage tracking", that typically means **live-bound** blocks reading the same
66
+ computed stats the Board already uses (see step 5).
67
+
68
+ ### 4. Choose blocks — the vocabulary
69
+
70
+ Pick from exactly these block types (anything else is invisible to the renderer — see
71
+ `.spectoflow/lib/custom-dashboard.js` for the enforced schema):
72
+
73
+ | `type` | Shape | Use for |
74
+ |---|---|---|
75
+ | `markdown` | `{ type, content }` — content is the markdown text to render (via the dashboard's own light markdown renderer: headings, lists, `code`, **bold**) | Explaining, documenting, an architecture write-up, a decision summary |
76
+ | `kpi-row` | `{ type, items: [{ label, value?, bind?, sub?, color? }] }` | A row of big-number stat cards (mirrors the Board's own KPI row) |
77
+ | `chart-bars` | `{ type, title, rows: [{ label, pct?, bind?, sub? }] }` | Progress or comparison bars (mirrors "Phase progress") |
78
+ | `chart-donut` | `{ type, title, segments: [{ key, value?, bind?, colorVar }] }` | A status/category breakdown + legend (mirrors "Status distribution") — `colorVar` is a design token name, e.g. `--s-done`, never a literal color |
79
+ | `table` | `{ type, title, columns: [string], rows: [[cell, ...]] }` | Structured tabular data |
80
+ | `list` | `{ type, title, items: [string] }` | A flat bullet list |
81
+ | `stat-tile-row` | `{ type, items: [{ value?, bind?, label, sub? }] }` | Compact stat tiles (mirrors the Info tab's counts) |
82
+
83
+ Apply Stephen Few's information-dashboard discipline while choosing: **one dashboard, one purpose** —
84
+ don't cram unrelated concerns onto the same page just because they were mentioned in the same request
85
+ (propose two dashboards instead, or ask which matters more); prefer the plainest block that carries
86
+ the point (a `stat-tile-row` over a `chart-donut` when there's nothing to compare); keep it scannable
87
+ in one screen — 4-8 blocks is a healthy page, not 20.
88
+
89
+ ### 5. Static content vs. live bindings
90
+
91
+ - **Static**: give the block its content directly (`content`, `rows`, `items`, `value` fields) —
92
+ baked in at generation time. This is the default, and the right choice whenever the ask is about a
93
+ point-in-time view (architecture, a decision record, a written summary).
94
+ - **Live**: instead of `value`/`pct`, set `bind` to a dotted path into the same stats object the Board
95
+ already computes (`SpectoStats.stats(P)` — see `dashboard/public/stats.js`). Allowed roots only:
96
+ `pct`, `done`, `total`, `byStatus` (per-status counts, e.g. `byStatus.done`), `phases` (per-phase
97
+ `{title,done,total,pct}`), `toAsk` (tasks awaiting review), `running` (active agents/orchestration),
98
+ `statuses` (the status key list). Anything else is rejected — there is no free-form expression, only
99
+ this fixed, safe set of paths. Use `bind` whenever the ask is explicitly about *tracking* something
100
+ over time ("show my task progress", "how many findings are open").
101
+
102
+ ### 6. Pick an id, a title, an icon
103
+
104
+ - `id`: lowercase kebab-case, unique among existing custom dashboards (list
105
+ `.spectoflow/dashboard/custom/*.json` first) — this becomes the URL segment and the file name.
106
+ - `title`: short, a few words, shown as the nav tab label.
107
+ - `icon`: one of `board`, `requests`, `backlog`, `workflow`, `agents`, `chat`, `info`, `attention`,
108
+ `settings` (the same set the rest of the dashboard uses — pick the closest match; default to `info`
109
+ when nothing fits well). An icon outside this set fails validation.
110
+
111
+ ### 7. Write and verify
112
+
113
+ Write the spec to `.spectoflow/dashboard/custom/<id>.json` (pretty-printed, 2-space indent). Then
114
+ **verify it, don't assume it's valid** — run:
115
+ ```
116
+ node -e "console.log(JSON.stringify(require('./.spectoflow/lib/custom-dashboard').validateSpec(JSON.parse(require('fs').readFileSync('./.spectoflow/dashboard/custom/<id>.json','utf8')))))"
117
+ ```
118
+ If `valid` is `false`, fix the reported errors and re-run before reporting done — a spec the
119
+ dashboard's own validator rejects is never a finished deliverable, it would simply be skipped and the
120
+ user would see nothing.
121
+
122
+ ## Output contract
123
+
124
+ - One file: `.spectoflow/dashboard/custom/<id>.json`, valid against
125
+ `.spectoflow/lib/custom-dashboard.js`'s `validateSpec` (verified per step 7, not assumed).
126
+ - Progress and completion reported to the orchestrator and group chat:
127
+
128
+ ```
129
+ ::spectoflow role=customization kind=progress msg=Drafting dashboard "<title>" — <n> blocks
130
+ ::spectoflow role=customization kind=need msg=<what's missing and why generation can't proceed>
131
+ ::spectoflow role=customization kind=done msg=Dashboard "<title>" added — open it from the nav (<id>)
132
+ ```
133
+
134
+ ## Quality bar
135
+
136
+ - [ ] Every block's `type` is one of the seven documented types — nothing else.
137
+ - [ ] No block, anywhere, sets a literal color/font/size — `colorVar` values are design-token names
138
+ (`--s-*`, `--signal`, `--cool`, …), never hex/rgb literals.
139
+ - [ ] Every `bind` path's root is one of `pct`/`done`/`total`/`byStatus`/`phases`/`toAsk`/`running`/`statuses`.
140
+ - [ ] The generated file passes `validateSpec` — actually run, not assumed (step 7).
141
+ - [ ] `id` is unique, kebab-case; `icon` is one of the nine allowed keys.
142
+ - [ ] The page has a clear, single purpose — not an unrelated grab-bag of blocks.
143
+ - [ ] If the ask was ambiguous, it was clarified one question at a time before any file was written.
144
+
145
+ ## References
146
+
147
+ - Stephen Few, *Information Dashboard Design* (O'Reilly/Analytics Press) — one purpose per dashboard,
148
+ the plainest chart that carries the point, single-screen legibility.
149
+ https://www.perceptualedge.com/library.php
150
+ - `.spectoflow/lib/custom-dashboard.js` — the enforced block schema and bind allow-list (source of
151
+ truth; this document summarizes it, the code is authoritative).
152
+ - `dashboard/public/stats.js` — the exact shape of the live stats object bindable via `bind`.
@@ -0,0 +1,153 @@
1
+ ---
2
+ name: generate-skill
3
+ description: Turn a description (or an auto-analysis) into a new skill file, grounded in a real, cited domain standard and matching the framework's gold-standard shape.
4
+ capability: customization
5
+ inputs: A description of the procedure needed (from the Customize page or chat), or a chosen candidate from propose-customizations; the project's existing skills as worked examples.
6
+ outputs: A new .spectoflow/skills/<slug>/SKILL.md matching docs/agents-skills-standard.md's shape, listed in the dashboard's Agents & Skills tab on the next tick.
7
+ standard: docs/agents-skills-standard.md gold-standard shape
8
+ ---
9
+ # Generate skill
10
+
11
+ Turn a described need for a new procedure into a real `SKILL.md` — one that reads like it shipped
12
+ with the framework: grounded in a named, current, cited standard for its domain, not generic advice
13
+ dressed up as a procedure.
14
+
15
+ ## When to use
16
+
17
+ Whenever the user asks (from the Customize page, or directly in chat) to **add a skill** — "create a
18
+ skill for security review grounded in OWASP", "I want a skill for accessibility audits", "add a skill
19
+ for database migration reviews" — or when `propose-customizations` proposed a skill candidate the
20
+ user picked.
21
+
22
+ ## Method
23
+
24
+ ### 1. Clarify before generating
25
+
26
+ A one-line ask ("add a security skill") is under-specified. Use `.spectoflow/skills/clarify`'s
27
+ reflex — one targeted question at a time, each with a recommended default — until you know:
28
+ - **The exact procedure's scope.** "Security review" could mean a dozen different things (dependency
29
+ vulnerabilities? authn/authz review? secrets scanning? infra hardening?) — narrow it before writing
30
+ a method for the wrong one.
31
+ - **Which capability it belongs to** — pick the closest match from `.spectoflow/capabilities.md`'s
32
+ palette (security, quality, architecture, testing, operations, …); propose one, don't leave it open.
33
+ - **Who runs it** — an existing agent whose capability matches, or does this need a new agent too
34
+ (if so, this request also needs `generate-agent` — say so and sequence the two).
35
+
36
+ ### 2. Identify the real domain standard — don't skip this, and don't fabricate it
37
+
38
+ This is the step that separates a real skill from a plausible-sounding one. For the procedure's
39
+ domain, identify the **actual, named, current authority** practitioners in that field defer to, the
40
+ same way the framework's own shipped skills already do (open a couple as worked examples —
41
+ `write-e2e-tests` cites Playwright's own docs page-by-page; `security-review` cites OWASP ASVS and the
42
+ OWASP Top 10; `write-adr` cites the ADR/C4 literature). Concretely:
43
+
44
+ | Domain | Look for | 2026-current example anchors |
45
+ |---|---|---|
46
+ | Security (general) | OWASP's current flagship guides | OWASP Top 10, OWASP ASVS, OWASP Cheat Sheet Series |
47
+ | Web app auth/session | OWASP-specific cheat sheets | Authentication/Session-Management Cheat Sheets |
48
+ | Accessibility | The current W3C recommendation | WCAG (check the current version — 2.2 at last knowledge, verify) |
49
+ | Architecture / ADRs | The established literature | C4 model (Simon Brown), Michael Nygard's ADR format |
50
+ | API design | A widely-adopted style guide | Google API Design Guide, Microsoft REST API Guidelines |
51
+ | Testing (any level) | The tool's own official docs | e.g. Playwright's own best-practices pages, not a blog summary of them |
52
+ | Performance | Vendor/W3C measurement standards | Core Web Vitals (web.dev), the relevant runtime's own profiling docs |
53
+ | Database/migrations | The database's own official docs + a recognized migration-safety pattern | e.g. "expand/contract" schema migration pattern |
54
+ | Accessibility, i18n, privacy, or any domain not listed here | Whatever is genuinely the field's own authority — never a generic listicle | — |
55
+
56
+ **Verify the standard is real and current before citing it** — use whatever research tool your
57
+ environment provides (web search/fetch) to confirm the standard's name, current version, and a real
58
+ URL; do not rely purely on training-time memory for a fast-moving domain (security guidance, W3C
59
+ specs, and vendor docs all revise). If you cannot verify a specific standard exists for the requested
60
+ domain, **say so explicitly and generate the method from first principles instead**, clearly flagged
61
+ in the skill's own body as "no single authoritative standard identified; method reasoned from
62
+ [whatever sound engineering principles apply]" — never invent a citation to fill the References
63
+ section. A skill honestly grounded in reasoned first principles is worth more than one dressed up with
64
+ a fabricated source.
65
+
66
+ ### 3. Write the skill in the gold-standard shape
67
+
68
+ Follow `docs/agents-skills-standard.md`'s skill shape exactly:
69
+
70
+ ```yaml
71
+ ---
72
+ name: <slug>
73
+ description: <one line — reads as a trigger>
74
+ capability: <the palette capability chosen in step 1>
75
+ inputs: <what it needs>
76
+ outputs: <what it produces>
77
+ standard: <the named standard from step 2>
78
+ ---
79
+ # <Skill name>
80
+ <1-line purpose>
81
+
82
+ ## When to use
83
+ <the trigger — when does the workflow, or a direct request, reach for this>
84
+
85
+ ## Method
86
+ <opinionated, numbered, SOURCED procedure — this is where the domain standard actually lives, applied
87
+ step by step, not just name-dropped in the References section>
88
+
89
+ ## Output contract
90
+ <the exact artifact produced and where it's written; how progress is reported — see step 4>
91
+
92
+ ## Quality bar
93
+ <a checkable checklist of what "good" looks like for this skill's output>
94
+
95
+ ## References
96
+ <the real, verified sources from step 2, as titled links>
97
+ ```
98
+
99
+ Match the depth and citation density of the framework's own shipped skills — a Method section that
100
+ just says "follow best practices" has failed this step; one that names the specific technique
101
+ (e.g. "apply OWASP ASVS V2 Authentication requirements, specifically…") has succeeded.
102
+
103
+ ### 4. Own the `::spectoflow` sentinel
104
+
105
+ Per the gold standard's conventions: **the skill owns the exact reporting syntax** — write out real
106
+ `::spectoflow role=<capability> kind=progress|need|done msg=…` lines in the new skill's Output
107
+ contract, matching the pattern every other skill uses (see any shipped skill for the exact grammar).
108
+ Do not leave this section vague or reference "the standard sentinel format" without spelling it out.
109
+
110
+ ### 5. Mark it as user-generated
111
+
112
+ Add `origin: user-generated` to the front-matter (an extra key — never remove or rename the required
113
+ ones). This is how the dashboard's Customize page distinguishes what the user added from what shipped
114
+ with the framework; omitting it hides the skill from that list.
115
+
116
+ ### 6. Wire it up if it belongs to the workflow
117
+
118
+ If the new skill is meant to run as part of the delivery pipeline (not just on-demand), tell the user
119
+ it can be added as a step in `.spectoflow/workflow.md` (or from the dashboard's Workflow tab) — but do
120
+ **not** edit `workflow.md` yourself without being asked; a new skill existing is not the same decision
121
+ as it being wired into every request's pipeline.
122
+
123
+ ## Output contract
124
+
125
+ - One file: `.spectoflow/skills/<slug>/SKILL.md`, matching the gold-standard shape, with
126
+ `origin: user-generated` in its front-matter.
127
+ - Progress and completion reported to the orchestrator and group chat:
128
+
129
+ ```
130
+ ::spectoflow role=customization kind=progress msg=Researching the standard for "<domain>" — checking <source>
131
+ ::spectoflow role=customization kind=need msg=<what's missing, e.g. no authoritative standard found for X>
132
+ ::spectoflow role=customization kind=done msg=Skill "<slug>" added (capability <capability>) — grounded in <standard>
133
+ ```
134
+
135
+ ## Quality bar
136
+
137
+ - [ ] Front-matter matches the gold-standard shape exactly, plus `origin: user-generated`.
138
+ - [ ] Body has exactly the five required `##` headings, in order.
139
+ - [ ] `## Method` names and applies a real, verified, current standard — or explicitly says none was
140
+ found and reasons from first principles instead. Never a fabricated citation.
141
+ - [ ] `## References` links are real and were verified (not assumed from memory) when the domain is
142
+ fast-moving (security, web standards, vendor APIs).
143
+ - [ ] The `::spectoflow` sentinel syntax is spelled out in full in the Output contract, not just
144
+ referenced.
145
+ - [ ] If the ask was ambiguous, it was clarified one question at a time before any file was written.
146
+ - [ ] `.spectoflow/workflow.md` was left untouched unless the user explicitly asked to enable this
147
+ skill as a pipeline step.
148
+
149
+ ## References
150
+
151
+ - `docs/agents-skills-standard.md` — the gold-standard shape this skill's output must match exactly.
152
+ - Any shipped skill under `.spectoflow/skills/` (e.g. `security-review`, `write-e2e-tests`,
153
+ `write-adr`) — worked examples of real citation density and Method-section depth to match.