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
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Pure SVG chart builders — no DOM, no fetch, just strings. Node-testable and
|
|
3
|
+
// browser-usable (UMD, same pattern as stats.js). Callers own colours (design
|
|
4
|
+
// tokens) and drop the returned markup straight into innerHTML.
|
|
5
|
+
(function (root) {
|
|
6
|
+
// polar(cx,cy,r,deg) -> [x,y] on the circle, 0deg = top (12 o'clock), increasing clockwise.
|
|
7
|
+
function polar(cx, cy, r, deg) {
|
|
8
|
+
const rad = (deg * Math.PI) / 180;
|
|
9
|
+
return [cx + r * Math.sin(rad), cy - r * Math.cos(rad)];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const esc = (v) => String(v == null ? '' : v).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
13
|
+
const n2 = (v) => (Math.round(v * 100) / 100).toString();
|
|
14
|
+
|
|
15
|
+
// donut(segs, opts) — segs=[{value,color,label}]. One <path> arc per non-zero
|
|
16
|
+
// segment (zeros skipped), small gap between arcs, staggered draw via --i,
|
|
17
|
+
// optional centre value/sub label.
|
|
18
|
+
function donut(segs, opts) {
|
|
19
|
+
opts = opts || {};
|
|
20
|
+
segs = segs || [];
|
|
21
|
+
const size = opts.size || 140;
|
|
22
|
+
const stroke = opts.stroke || 16;
|
|
23
|
+
const cx = size / 2, cy = size / 2;
|
|
24
|
+
const r = (size - stroke) / 2;
|
|
25
|
+
const gap = opts.gap != null ? opts.gap : 3; // degrees
|
|
26
|
+
const total = segs.reduce((a, s) => a + (s.value || 0), 0);
|
|
27
|
+
|
|
28
|
+
let angle = 0, i = 0, arcs = '';
|
|
29
|
+
segs.forEach((s) => {
|
|
30
|
+
const v = s.value || 0;
|
|
31
|
+
if (!v) return;
|
|
32
|
+
const sweep = total ? (v / total) * 360 : 0;
|
|
33
|
+
const halfGap = sweep > gap ? gap / 2 : 0;
|
|
34
|
+
const start = angle + halfGap;
|
|
35
|
+
const end = angle + sweep - halfGap;
|
|
36
|
+
const [x0, y0] = polar(cx, cy, r, start);
|
|
37
|
+
const [x1, y1] = polar(cx, cy, r, end);
|
|
38
|
+
const large = end - start > 180 ? 1 : 0;
|
|
39
|
+
arcs += `<path d="M ${n2(x0)} ${n2(y0)} A ${n2(r)} ${n2(r)} 0 ${large} 1 ${n2(x1)} ${n2(y1)}" fill="none" stroke="${esc(s.color)}" stroke-width="${stroke}" stroke-linecap="round" class="seg-anim" style="--i:${i}"><title>${esc(s.label)}</title></path>`;
|
|
40
|
+
angle += sweep;
|
|
41
|
+
i++;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const center = opts.center != null
|
|
45
|
+
? `<text x="${cx}" y="${total ? cy - 2 : cy}" text-anchor="middle" dominant-baseline="middle" class="donut-total" style="fill:var(--ink)">${esc(opts.center)}</text>`
|
|
46
|
+
: '';
|
|
47
|
+
const sub = opts.sub
|
|
48
|
+
? `<text x="${cx}" y="${cy + 15}" text-anchor="middle" class="donut-sub" style="fill:var(--faint)">${esc(opts.sub)}</text>`
|
|
49
|
+
: '';
|
|
50
|
+
|
|
51
|
+
return `<svg viewBox="0 0 ${size} ${size}" width="${size}" height="${size}" class="donut-svg">` +
|
|
52
|
+
`<circle cx="${cx}" cy="${cy}" r="${n2(r)}" fill="none" stroke="var(--line)" stroke-width="${stroke}"/>` +
|
|
53
|
+
arcs + center + sub +
|
|
54
|
+
`</svg>`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// area(series, labels, opts) — series=[{name,color,data:[...]}]. A 4-line
|
|
58
|
+
// horizontal grid + y labels, one smoothed (Catmull-Rom) line + filled area
|
|
59
|
+
// per series, dots + value labels, transparent per-x hover hit-rects, x labels.
|
|
60
|
+
function smoothPath(pts) {
|
|
61
|
+
if (!pts.length) return '';
|
|
62
|
+
if (pts.length === 1) return `M ${n2(pts[0][0])} ${n2(pts[0][1])}`;
|
|
63
|
+
let d = `M ${n2(pts[0][0])} ${n2(pts[0][1])}`;
|
|
64
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
65
|
+
const p0 = pts[i - 1] || pts[i];
|
|
66
|
+
const p1 = pts[i];
|
|
67
|
+
const p2 = pts[i + 1];
|
|
68
|
+
const p3 = pts[i + 2] || p2;
|
|
69
|
+
const c1x = p1[0] + (p2[0] - p0[0]) / 6;
|
|
70
|
+
const c1y = p1[1] + (p2[1] - p0[1]) / 6;
|
|
71
|
+
const c2x = p2[0] - (p3[0] - p1[0]) / 6;
|
|
72
|
+
const c2y = p2[1] - (p3[1] - p1[1]) / 6;
|
|
73
|
+
d += ` C ${n2(c1x)} ${n2(c1y)}, ${n2(c2x)} ${n2(c2y)}, ${n2(p2[0])} ${n2(p2[1])}`;
|
|
74
|
+
}
|
|
75
|
+
return d;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function area(series, labels, opts) {
|
|
79
|
+
opts = opts || {};
|
|
80
|
+
series = series || [];
|
|
81
|
+
labels = labels || [];
|
|
82
|
+
const width = opts.width || 480;
|
|
83
|
+
const height = opts.height || 180;
|
|
84
|
+
const padL = opts.padLeft != null ? opts.padLeft : 32;
|
|
85
|
+
const padR = opts.padRight != null ? opts.padRight : 10;
|
|
86
|
+
const padT = opts.padTop != null ? opts.padTop : 10;
|
|
87
|
+
const padB = opts.padBottom != null ? opts.padBottom : 22;
|
|
88
|
+
const innerW = Math.max(0, width - padL - padR);
|
|
89
|
+
const innerH = Math.max(0, height - padT - padB);
|
|
90
|
+
|
|
91
|
+
const n = labels.length || series.reduce((a, s) => Math.max(a, (s.data || []).length), 0);
|
|
92
|
+
const allVals = series.reduce((a, s) => a.concat(s.data || []), []);
|
|
93
|
+
const dataMax = allVals.length ? Math.max.apply(null, allVals) : 0;
|
|
94
|
+
const max = opts.max != null ? opts.max : Math.max(1, Math.ceil(dataMax));
|
|
95
|
+
|
|
96
|
+
const x = (i) => (n > 1 ? padL + (innerW * i) / (n - 1) : padL + innerW / 2);
|
|
97
|
+
const y = (v) => padT + innerH - (innerH * v) / max;
|
|
98
|
+
const baseline = padT + innerH;
|
|
99
|
+
|
|
100
|
+
// grid: 4 horizontal lines + y labels
|
|
101
|
+
const steps = 4;
|
|
102
|
+
let grid = '';
|
|
103
|
+
for (let g = 0; g <= steps; g++) {
|
|
104
|
+
const gy = padT + (innerH * g) / steps;
|
|
105
|
+
const val = Math.round(max - (max * g) / steps);
|
|
106
|
+
grid += `<line x1="${n2(padL)}" y1="${n2(gy)}" x2="${n2(padL + innerW)}" y2="${n2(gy)}" class="area-grid" stroke="var(--line)"/>`;
|
|
107
|
+
grid += `<text x="${n2(padL - 8)}" y="${n2(gy + 3)}" text-anchor="end" class="area-ylabel" style="fill:var(--faint)">${val}</text>`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let xlabels = '';
|
|
111
|
+
labels.forEach((lb, i) => {
|
|
112
|
+
xlabels += `<text x="${n2(x(i))}" y="${n2(height - 4)}" text-anchor="middle" class="area-xlabel" style="fill:var(--faint)">${esc(lb)}</text>`;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
let seriesMarkup = '';
|
|
116
|
+
series.forEach((s, si) => {
|
|
117
|
+
const data = s.data || [];
|
|
118
|
+
const pts = data.map((v, i) => [x(i), y(v)]);
|
|
119
|
+
const linePath = smoothPath(pts);
|
|
120
|
+
let fillPath = linePath;
|
|
121
|
+
if (pts.length) {
|
|
122
|
+
fillPath += ` L ${n2(pts[pts.length - 1][0])} ${n2(baseline)} L ${n2(pts[0][0])} ${n2(baseline)} Z`;
|
|
123
|
+
}
|
|
124
|
+
seriesMarkup += `<path d="${fillPath}" fill="${esc(s.color)}" fill-opacity="0.13" stroke="none" class="area-fill"/>`;
|
|
125
|
+
seriesMarkup += `<path d="${linePath}" fill="none" stroke="${esc(s.color)}" stroke-width="2" class="area-line" pathLength="1" style="--i:${si}"/>`;
|
|
126
|
+
pts.forEach((p, i) => {
|
|
127
|
+
seriesMarkup += `<circle cx="${n2(p[0])}" cy="${n2(p[1])}" r="3" fill="${esc(s.color)}" class="area-dot"/>`;
|
|
128
|
+
seriesMarkup += `<text x="${n2(p[0])}" y="${n2(p[1] - 8)}" text-anchor="middle" class="area-value" style="fill:var(--ink)">${esc(data[i])}</text>`;
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
let tips = '';
|
|
133
|
+
for (let i = 0; i < n; i++) {
|
|
134
|
+
const cx = x(i);
|
|
135
|
+
const w = n > 1 ? innerW / (n - 1) : innerW;
|
|
136
|
+
tips += `<rect x="${n2(cx - w / 2)}" y="${n2(padT)}" width="${n2(w)}" height="${n2(innerH)}" fill="transparent" class="area-hit" data-tip="${esc(labels[i])}"/>`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return `<svg viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" class="area-svg">` +
|
|
140
|
+
`<g class="area-grid-g">${grid}</g>` +
|
|
141
|
+
seriesMarkup +
|
|
142
|
+
`<g class="area-xlabels">${xlabels}</g>` +
|
|
143
|
+
`<g class="area-tips">${tips}</g>` +
|
|
144
|
+
`</svg>`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// bars(items, opts) — items=[{label,value,sub,color,suffix}]. Rows with a
|
|
148
|
+
// track + .bar-fill (width via --w, staggered via --i) and a count-up span.
|
|
149
|
+
function bars(items, opts) {
|
|
150
|
+
opts = opts || {};
|
|
151
|
+
items = items || [];
|
|
152
|
+
if (!items.length) return `<div class="empty">${esc(opts.empty || 'No phases yet.')}</div>`;
|
|
153
|
+
let rows = '';
|
|
154
|
+
items.forEach((it, i) => {
|
|
155
|
+
const pct = Math.max(0, Math.min(100, it.value || 0));
|
|
156
|
+
const fillStyle = `width:${n2(pct)}%; --w:${n2(pct)}; --i:${i}` + (it.color ? `; background:${esc(it.color)}` : '');
|
|
157
|
+
rows += `<div class="bar-row">` +
|
|
158
|
+
`<div class="bar-head">` +
|
|
159
|
+
`<span class="bar-label">${esc(it.label)}</span>` +
|
|
160
|
+
`<span class="bar-sub">${esc(it.sub || '')}</span>` +
|
|
161
|
+
`<span class="bar-count" data-count="${it.value || 0}">0${esc(it.suffix || '')}</span>` +
|
|
162
|
+
`</div>` +
|
|
163
|
+
`<div class="bar-track"><div class="bar-fill" style="${fillStyle}"></div></div>` +
|
|
164
|
+
`</div>`;
|
|
165
|
+
});
|
|
166
|
+
return `<div class="bars">${rows}</div>`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ring(pct, opts) — a progress ring: track circle + foreground arc via
|
|
170
|
+
// stroke-dasharray/offset (stroke="url(#grad)" — caller provides the
|
|
171
|
+
// <linearGradient id="grad"> once), centre pct% text.
|
|
172
|
+
function ring(pct, opts) {
|
|
173
|
+
opts = opts || {};
|
|
174
|
+
pct = Math.max(0, Math.min(100, pct || 0));
|
|
175
|
+
const size = opts.size || 72;
|
|
176
|
+
const stroke = opts.stroke || 7;
|
|
177
|
+
const cx = size / 2, cy = size / 2;
|
|
178
|
+
const r = (size - stroke) / 2;
|
|
179
|
+
const c = 2 * Math.PI * r;
|
|
180
|
+
const offset = c * (1 - pct / 100);
|
|
181
|
+
return `<svg viewBox="0 0 ${size} ${size}" width="${size}" height="${size}" class="ring-svg">` +
|
|
182
|
+
`<circle cx="${cx}" cy="${cy}" r="${n2(r)}" fill="none" stroke="var(--line)" stroke-width="${stroke}"/>` +
|
|
183
|
+
`<circle cx="${cx}" cy="${cy}" r="${n2(r)}" fill="none" stroke="${opts.stroke2 || 'url(#grad)'}" stroke-width="${stroke}" stroke-linecap="round" ` +
|
|
184
|
+
`stroke-dasharray="${n2(c)}" stroke-dashoffset="${n2(offset)}" transform="rotate(-90 ${cx} ${cy})"/>` +
|
|
185
|
+
`<text x="${cx}" y="${cy}" text-anchor="middle" dominant-baseline="middle" class="ring-label" style="fill:var(--ink)">${pct}%</text>` +
|
|
186
|
+
`</svg>`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const api = { polar, donut, area, bars, ring };
|
|
190
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
191
|
+
else root.SpectoCharts = api;
|
|
192
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Small inline-SVG line-icon set for the header nav and quick actions.
|
|
3
|
+
// Line style, 16-18px viewBox units, stroke=currentColor so icons inherit tab/text color.
|
|
4
|
+
(function (root) {
|
|
5
|
+
const wrap = (inner, extra) => `<svg viewBox="0 0 18 18" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"${extra ? ' ' + extra : ''}>${inner}</svg>`;
|
|
6
|
+
|
|
7
|
+
const ICON = {
|
|
8
|
+
// kanban columns
|
|
9
|
+
board: wrap('<rect x="2" y="3" width="4" height="12" rx="1"/><rect x="7" y="3" width="4" height="8" rx="1"/><rect x="12" y="3" width="4" height="10" rx="1"/>'),
|
|
10
|
+
// inbox tray — tasks awaiting a decision
|
|
11
|
+
requests: wrap('<path d="M2.6 10h3.2l1.4 2h3.6l1.4-2h3.2"/><path d="M2.6 10 3.9 4.6a1 1 0 0 1 1-.8h7.2a1 1 0 0 1 1 .8L14.4 10"/><path d="M2.6 10v3a1 1 0 0 0 1 1h9.8a1 1 0 0 0 1-1v-3"/>'),
|
|
12
|
+
// stacked list
|
|
13
|
+
backlog: wrap('<line x1="3" y1="5" x2="15" y2="5"/><line x1="3" y1="9" x2="15" y2="9"/><line x1="3" y1="13" x2="10.5" y2="13"/>'),
|
|
14
|
+
// connected nodes — pipeline
|
|
15
|
+
workflow: wrap('<circle cx="3.4" cy="9" r="1.7"/><circle cx="9" cy="9" r="1.7"/><circle cx="14.6" cy="9" r="1.7"/><line x1="5.1" y1="9" x2="7.3" y2="9"/><line x1="10.7" y1="9" x2="12.9" y2="9"/>'),
|
|
16
|
+
// two people — agents & skills
|
|
17
|
+
agents: wrap('<circle cx="6.4" cy="6.1" r="2.1"/><path d="M2.6 15c0-2.5 1.7-4 3.8-4s3.8 1.5 3.8 4"/><circle cx="13.1" cy="7.1" r="1.7"/><path d="M10.9 15c.2-2 1.5-3.1 3.1-3.1 1.8 0 3.4 1.3 3.4 3.1"/>'),
|
|
18
|
+
// speech bubble
|
|
19
|
+
chat: wrap('<path d="M3 4.4h12a1 1 0 0 1 1 1V11a1 1 0 0 1-1 1H8.2l-3.3 2.6a.4.4 0 0 1-.65-.31V12H3a1 1 0 0 1-1-1V5.4a1 1 0 0 1 1-1z"/>'),
|
|
20
|
+
// info circle
|
|
21
|
+
info: wrap('<circle cx="9" cy="9" r="6.4"/><line x1="9" y1="8.3" x2="9" y2="12.2"/><circle cx="9" cy="5.7" r="0.9" fill="currentColor" stroke="none"/>'),
|
|
22
|
+
// play triangle — run
|
|
23
|
+
run: wrap('<path d="M5.4 3.6v10.8l9-5.4z" fill="currentColor" stroke="none"/>'),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = ICON;
|
|
27
|
+
else root.ICON = ICON;
|
|
28
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>spectoflow · control</title>
|
|
7
|
+
<link rel="stylesheet" href="styles.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header class="topbar">
|
|
11
|
+
<div class="brand">
|
|
12
|
+
<span class="brand-mark" aria-hidden="true"></span>
|
|
13
|
+
<div class="brand-text">
|
|
14
|
+
<div class="brand-line">
|
|
15
|
+
<span class="brand-name">spectoflow</span>
|
|
16
|
+
<span class="brand-sep">/</span>
|
|
17
|
+
<span class="brand-project" id="projectName">—</span>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="brand-sub" id="brandSub">semi · en</div>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="progress-meter" id="globalMeter" title="Global progress">
|
|
22
|
+
<div class="progress-meter-fill" id="globalMeterFill"></div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<nav class="tabs" id="tabs">
|
|
26
|
+
<button class="tab is-active" data-tab="board"><span class="tab-ico" data-icon="board"></span><span class="tab-label">Board</span></button>
|
|
27
|
+
<button class="tab" data-tab="requests"><span class="tab-ico" data-icon="requests"></span><span class="tab-label">Requests</span></button>
|
|
28
|
+
<button class="tab" data-tab="backlog"><span class="tab-ico" data-icon="backlog"></span><span class="tab-label">Backlog</span></button>
|
|
29
|
+
<button class="tab" data-tab="workflow"><span class="tab-ico" data-icon="workflow"></span><span class="tab-label">Workflow</span></button>
|
|
30
|
+
<button class="tab" data-tab="team"><span class="tab-ico" data-icon="agents"></span><span class="tab-label">Agents & Skills</span></button>
|
|
31
|
+
<button class="tab" data-tab="chat"><span class="tab-ico" data-icon="chat"></span><span class="tab-label">Chat</span></button>
|
|
32
|
+
<button class="tab" data-tab="info"><span class="tab-ico" data-icon="info"></span><span class="tab-label">Info</span></button>
|
|
33
|
+
</nav>
|
|
34
|
+
<div class="top-right">
|
|
35
|
+
<span class="meta-chip" id="agentChip" title="Active agent">claude</span>
|
|
36
|
+
<span class="meta-chip" id="langChip" title="Output language">en</span>
|
|
37
|
+
<span class="mode-chip" id="modeChip" title="Autonomy mode">semi</span>
|
|
38
|
+
<span class="sync" id="sync"><span class="sync-dot"></span><span id="syncLabel">live</span></span>
|
|
39
|
+
<button class="btn primary run-btn" id="runQuickBtn" title="Open the run chat"><span class="tab-ico" data-icon="run"></span><span>Run</span></button>
|
|
40
|
+
<button class="theme-toggle" id="themeToggle" aria-label="Toggle theme"><span class="theme-ico"></span></button>
|
|
41
|
+
</div>
|
|
42
|
+
</header>
|
|
43
|
+
|
|
44
|
+
<main class="stage">
|
|
45
|
+
<!-- BOARD -->
|
|
46
|
+
<section class="panel is-active" data-panel="board">
|
|
47
|
+
<div class="main">
|
|
48
|
+
<div class="overview" id="overview"></div>
|
|
49
|
+
<div class="filters" id="filters">
|
|
50
|
+
<div class="chip-list" id="statusChips">
|
|
51
|
+
<button class="fchip active" data-status="all">All</button>
|
|
52
|
+
<button class="fchip" data-status="todo">To do</button>
|
|
53
|
+
<button class="fchip" data-status="in_progress">In progress</button>
|
|
54
|
+
<button class="fchip" data-status="to_validate">To validate</button>
|
|
55
|
+
<button class="fchip" data-status="to_analyze">To analyze</button>
|
|
56
|
+
<button class="fchip" data-status="done">Done</button>
|
|
57
|
+
<button class="fchip" data-status="blocked">Blocked</button>
|
|
58
|
+
</div>
|
|
59
|
+
<input type="search" id="search" class="search" placeholder="Filter tasks…" autocomplete="off" />
|
|
60
|
+
</div>
|
|
61
|
+
<div class="board" id="board"></div>
|
|
62
|
+
</div>
|
|
63
|
+
<aside class="side" id="side">
|
|
64
|
+
<div class="rail-block rail-block-journal">
|
|
65
|
+
<h2 class="rail-title">Journal <span class="count" id="journalCount">0</span></h2>
|
|
66
|
+
<div class="journal" id="journal"></div>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="rail-block">
|
|
69
|
+
<h2 class="rail-title">Specs <span class="count" id="specsCount">0</span></h2>
|
|
70
|
+
<ul class="flatlist" id="specs"></ul>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="rail-block">
|
|
73
|
+
<h2 class="rail-title">Running <span class="count" id="runCount">0</span></h2>
|
|
74
|
+
<ul class="flatlist" id="running"></ul>
|
|
75
|
+
</div>
|
|
76
|
+
</aside>
|
|
77
|
+
</section>
|
|
78
|
+
|
|
79
|
+
<!-- REQUESTS (tasks awaiting review: to_validate / to_analyze) -->
|
|
80
|
+
<section class="panel" data-panel="requests">
|
|
81
|
+
<div class="requests-wrap">
|
|
82
|
+
<h2 class="panel-title">Requests <span class="count" id="requestsCount">0</span></h2>
|
|
83
|
+
<p class="panel-sub">Tasks awaiting your input — status <em>to validate</em> or <em>to analyze</em>.</p>
|
|
84
|
+
<ul class="flatlist requests-list" id="requestsList"></ul>
|
|
85
|
+
</div>
|
|
86
|
+
</section>
|
|
87
|
+
|
|
88
|
+
<!-- BACKLOG (flat, sortable, filterable table of every task) -->
|
|
89
|
+
<section class="panel" data-panel="backlog">
|
|
90
|
+
<div class="backlog-wrap">
|
|
91
|
+
<h2 class="panel-title">Backlog <span class="count" id="backlogCount">0</span></h2>
|
|
92
|
+
<p class="panel-sub">Every task, across every plan, in one flat table.</p>
|
|
93
|
+
<div class="filters" id="backlogFilters">
|
|
94
|
+
<div class="chip-list" id="backlogStatusChips">
|
|
95
|
+
<button class="fchip active" data-status="all">All</button>
|
|
96
|
+
<button class="fchip" data-status="todo">To do</button>
|
|
97
|
+
<button class="fchip" data-status="in_progress">In progress</button>
|
|
98
|
+
<button class="fchip" data-status="to_validate">To validate</button>
|
|
99
|
+
<button class="fchip" data-status="to_analyze">To analyze</button>
|
|
100
|
+
<button class="fchip" data-status="done">Done</button>
|
|
101
|
+
<button class="fchip" data-status="blocked">Blocked</button>
|
|
102
|
+
</div>
|
|
103
|
+
<input type="search" id="backlogSearch" class="search" placeholder="Filter by id or title…" autocomplete="off" />
|
|
104
|
+
</div>
|
|
105
|
+
<div class="table-scroll">
|
|
106
|
+
<table class="backlog-table" id="backlogTable">
|
|
107
|
+
<thead>
|
|
108
|
+
<tr>
|
|
109
|
+
<th data-col="id">ID</th>
|
|
110
|
+
<th data-col="title">Title</th>
|
|
111
|
+
<th data-col="phase">Phase</th>
|
|
112
|
+
<th data-col="status">Status</th>
|
|
113
|
+
<th data-col="owner">Owner</th>
|
|
114
|
+
<th data-col="level">Level</th>
|
|
115
|
+
<th data-col="comments">💬</th>
|
|
116
|
+
</tr>
|
|
117
|
+
</thead>
|
|
118
|
+
<tbody id="backlogBody"></tbody>
|
|
119
|
+
</table>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</section>
|
|
123
|
+
|
|
124
|
+
<!-- WORKFLOW -->
|
|
125
|
+
<section class="panel" data-panel="workflow">
|
|
126
|
+
<div class="wf-wrap">
|
|
127
|
+
<h2 class="panel-title">Active workflow</h2>
|
|
128
|
+
<p class="panel-sub">Click a step to enable or disable it. This edits <code>.spectoflow/workflow.md</code>.</p>
|
|
129
|
+
<div class="wf-diagram" id="wfDiagram"></div>
|
|
130
|
+
</div>
|
|
131
|
+
</section>
|
|
132
|
+
|
|
133
|
+
<!-- AGENTS & SKILLS -->
|
|
134
|
+
<section class="panel" data-panel="team">
|
|
135
|
+
<div class="team-wrap">
|
|
136
|
+
<h2 class="panel-title">Agents <span class="count" id="agentsCount">0</span></h2>
|
|
137
|
+
<p class="panel-sub">Stable team personas — the <em>who</em>.</p>
|
|
138
|
+
<div class="cards" id="agents"></div>
|
|
139
|
+
<h2 class="panel-title" style="margin-top:28px">Skills <span class="count" id="skillsCount">0</span></h2>
|
|
140
|
+
<p class="panel-sub">Evolving procedures — the <em>how</em>.</p>
|
|
141
|
+
<div class="cards" id="skills"></div>
|
|
142
|
+
</div>
|
|
143
|
+
</section>
|
|
144
|
+
|
|
145
|
+
<!-- CHAT (full-width chat tab — shares renderChatLog + doRun/doOrchestrate/approve with the floating widget) -->
|
|
146
|
+
<section class="panel" data-panel="chat">
|
|
147
|
+
<div class="chat-tab-wrap">
|
|
148
|
+
<div class="chat-tab-head">
|
|
149
|
+
<div>
|
|
150
|
+
<h2 class="panel-title">Chat</h2>
|
|
151
|
+
<p class="panel-sub">Full conversation with the runner — the same run as the widget, more room to read it.</p>
|
|
152
|
+
</div>
|
|
153
|
+
<select id="tabRunAgent" class="chat-agent" title="Agent"></select>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="chat-tab-log" id="chatTabLog">
|
|
156
|
+
<div class="chat-idle">Type a request — the agent runs headless in this project with full memory
|
|
157
|
+
(<code>CLAUDE.md → AGENTS.md</code>) and updates the board live.</div>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="chat-tab-input">
|
|
160
|
+
<textarea id="tabRunPrompt" class="chat-ta" placeholder="e.g. Add a login feature with email + password"></textarea>
|
|
161
|
+
<div class="chat-tab-actions">
|
|
162
|
+
<button id="tabRunBtn" class="btn primary chat-send">Send</button>
|
|
163
|
+
<button id="tabOrchBtn" class="btn chat-send" title="Walk the enabled workflow">Orchestrate</button>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
<p class="chat-warn">⚠ Launches a real agent (<code>config.json → runners</code>) that can modify files & run commands.</p>
|
|
167
|
+
</div>
|
|
168
|
+
</section>
|
|
169
|
+
|
|
170
|
+
<!-- INFO (project overview — filled by renderInfo() in app.js) -->
|
|
171
|
+
<section class="panel" data-panel="info">
|
|
172
|
+
<div class="info-wrap">
|
|
173
|
+
<h2 class="panel-title">Info</h2>
|
|
174
|
+
<p class="panel-sub">Project overview — configuration, counts, specs and active workflow.</p>
|
|
175
|
+
<div class="info-grid" id="infoGrid"></div>
|
|
176
|
+
</div>
|
|
177
|
+
</section>
|
|
178
|
+
</main>
|
|
179
|
+
|
|
180
|
+
<!-- CHAT WIDGET (floating agent runner; entry point to the group-chat) -->
|
|
181
|
+
<button class="chat-fab" id="chatFab" aria-label="Open run chat" title="Run an agent">💬</button>
|
|
182
|
+
<div class="chat" id="chat" aria-hidden="true" role="dialog" aria-label="Run an agent">
|
|
183
|
+
<div class="chat-head">
|
|
184
|
+
<div class="chat-head-text">
|
|
185
|
+
<span class="chat-title">Run an agent</span>
|
|
186
|
+
<span class="chat-sub">Quick access · full view in the Chat tab</span>
|
|
187
|
+
</div>
|
|
188
|
+
<select id="runAgent" class="chat-agent" title="Agent"></select>
|
|
189
|
+
<button class="chat-close" id="chatClose" aria-label="Close">×</button>
|
|
190
|
+
</div>
|
|
191
|
+
<div class="chat-log" id="chatLog">
|
|
192
|
+
<div class="chat-idle">Type a request — the agent runs headless in this project with full memory
|
|
193
|
+
(<code>CLAUDE.md → AGENTS.md</code>) and updates the board live.</div>
|
|
194
|
+
</div>
|
|
195
|
+
<div class="chat-input">
|
|
196
|
+
<textarea id="runPrompt" class="chat-ta" placeholder="e.g. Add a login feature with email + password"></textarea>
|
|
197
|
+
<button id="runBtn" class="btn primary chat-send">Send</button>
|
|
198
|
+
<button id="orchBtn" class="btn chat-send" title="Walk the enabled workflow">Orchestrate</button>
|
|
199
|
+
</div>
|
|
200
|
+
<p class="chat-warn">⚠ Launches a real agent (<code>config.json → runners</code>) that can modify files & run commands.</p>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<div class="drawer" id="drawer" aria-hidden="true">
|
|
204
|
+
<div class="drawer-scrim" id="drawerScrim"></div>
|
|
205
|
+
<div class="drawer-panel" role="dialog"><button class="drawer-close" id="drawerClose">×</button><div id="drawerBody"></div></div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<!-- floating chart tooltip layer — positioned on mousemove over [data-tip] hit targets -->
|
|
209
|
+
<div id="tooltip" class="tooltip" hidden></div>
|
|
210
|
+
|
|
211
|
+
<!-- shared gradient used by SpectoCharts.ring's foreground arc (stroke="url(#grad)") -->
|
|
212
|
+
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
|
|
213
|
+
<defs>
|
|
214
|
+
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
215
|
+
<stop offset="0%" stop-color="var(--cool)" />
|
|
216
|
+
<stop offset="100%" stop-color="var(--signal)" />
|
|
217
|
+
</linearGradient>
|
|
218
|
+
</defs>
|
|
219
|
+
</svg>
|
|
220
|
+
|
|
221
|
+
<script src="stats.js"></script>
|
|
222
|
+
<script src="charts.js"></script>
|
|
223
|
+
<script src="icons.js"></script>
|
|
224
|
+
<script src="app.js"></script>
|
|
225
|
+
</body>
|
|
226
|
+
</html>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
(function (root) {
|
|
3
|
+
const STATUSES = ['todo', 'in_progress', 'to_validate', 'to_analyze', 'done', 'blocked'];
|
|
4
|
+
const allTasks = (p) => (p.plans || []).flatMap((pl) => pl.phases.flatMap((ph) => ph.tasks.map((t) => ({ ...t, file: pl.file }))));
|
|
5
|
+
function stats(p) {
|
|
6
|
+
p = p || {};
|
|
7
|
+
const tasks = allTasks(p);
|
|
8
|
+
const total = tasks.length;
|
|
9
|
+
const byStatus = Object.fromEntries(STATUSES.map((s) => [s, 0]));
|
|
10
|
+
tasks.forEach((t) => { if (byStatus[t.status] === undefined) byStatus[t.status] = 0; byStatus[t.status]++; });
|
|
11
|
+
const done = byStatus.done || 0;
|
|
12
|
+
const pct = total ? Math.round((done / total) * 100) : 0;
|
|
13
|
+
const phases = (p.plans || []).flatMap((pl) => pl.phases.map((ph) => {
|
|
14
|
+
const d = ph.tasks.filter((t) => t.status === 'done').length, tot = ph.tasks.length;
|
|
15
|
+
return { title: ph.title, file: pl.file, done: d, total: tot, pct: tot ? Math.round((d / tot) * 100) : 0 };
|
|
16
|
+
}));
|
|
17
|
+
const toAsk = tasks.filter((t) => t.status === 'to_validate' || t.status === 'to_analyze')
|
|
18
|
+
.map((t) => ({ id: t.id, title: t.title, status: t.status, file: t.file }));
|
|
19
|
+
const rt = p.runtime || {};
|
|
20
|
+
const agents = (rt.agents || []);
|
|
21
|
+
const last = agents.length ? agents[agents.length - 1] : null;
|
|
22
|
+
const running = {
|
|
23
|
+
agents: agents.filter((a) => a.status === 'running').length,
|
|
24
|
+
lastRun: last ? { tool: last.tool, status: last.status } : null,
|
|
25
|
+
orchestration: rt.orchestration || null,
|
|
26
|
+
};
|
|
27
|
+
return { total, done, pct, byStatus, phases, toAsk, running, statuses: STATUSES };
|
|
28
|
+
}
|
|
29
|
+
const api = { stats, STATUSES };
|
|
30
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
31
|
+
else root.SpectoStats = api;
|
|
32
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|