catalyst-os 3.0.2 → 3.1.1

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.
@@ -0,0 +1,380 @@
1
+ <!DOCTYPE html>
2
+ <!--
3
+ Catalyst OS — Project Dashboard (whole .catalyst/main)
4
+ Self-contained, offline. Double-click to open in a browser.
5
+ Regenerated on every spec lifecycle transition and by
6
+ /plan-project, /sync-project, /catalyze-project. See the
7
+ `spec-lifecycle-board` skill for the canonical generation procedure.
8
+
9
+ HOW COMMANDS UPDATE THIS FILE:
10
+ Replace ONLY the block between "DATA:START" and "DATA:END" with the
11
+ current project state read from the .catalyst/main docs
12
+ (roadmap.md, mission.md, tech-stack.md, architecture.md, concerns.md).
13
+ Never edit the CSS/JS below the data block — layout is fixed on purpose.
14
+ Any section with empty/omitted data is auto-hidden; only fill what exists.
15
+
16
+ LIFECYCLE: Backlog → Planned → In Progress → Audited → Done (sealed).
17
+ The Kanban board shows the 4 active states; Done is the timeline below it.
18
+ -->
19
+ <html lang="en">
20
+ <head>
21
+ <meta charset="utf-8">
22
+ <meta name="viewport" content="width=device-width, initial-scale=1">
23
+ <title>Project Dashboard</title>
24
+ <style>
25
+ :root {
26
+ --bg: #0d1117; --bg-elev: #161b22; --bg-elev2: #1c2230;
27
+ --border: #2a3140; --text: #e6edf3; --text-dim: #8b949e; --text-faint: #6e7681;
28
+ --accent: #f0b429;
29
+ --backlog: #7d8590; --planned: #a371f7; --inprogress: #4493f8; --audited: #39c5cf; --done: #3fb950;
30
+ --crit: #f85149; --high: #f0883e; --med: #d29922; --low: #3fb950;
31
+ --radius: 12px; --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
32
+ --sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
33
+ }
34
+ @media (prefers-color-scheme: light) {
35
+ :root { --bg: #f6f8fa; --bg-elev: #fff; --bg-elev2: #f0f3f6; --border: #d0d7de; --text: #1f2328; --text-dim: #59636e; --text-faint: #818b98; }
36
+ }
37
+ * { box-sizing: border-box; }
38
+ body { margin: 0; background: var(--bg); color: var(--text); font-family: var(--sans); line-height: 1.5; -webkit-font-smoothing: antialiased; }
39
+ .wrap { max-width: 1200px; margin: 0 auto; padding: 28px 24px 80px; }
40
+ a { color: var(--inprogress); text-decoration: none; } a:hover { text-decoration: underline; }
41
+ h3.section { font-size: 13px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-dim); margin: 24px 0 12px; }
42
+
43
+ header { margin-bottom: 20px; }
44
+ header h1 { margin: 0 0 4px; font-size: 26px; letter-spacing: -0.02em; }
45
+ header .tagline { color: var(--text-dim); font-size: 15px; max-width: 70ch; }
46
+ header .meta { color: var(--text-faint); font-size: 12px; margin-top: 6px; }
47
+ header .meta code { font-family: var(--mono); color: var(--accent); }
48
+
49
+ /* Stats — 5 lifecycle states */
50
+ .stats { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; margin: 18px 0 8px; }
51
+ .stat { background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 14px; }
52
+ .stat .n { font-size: 23px; font-weight: 700; letter-spacing: -0.02em; }
53
+ .stat .l { font-size: 11px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.04em; }
54
+ .stat.backlog .n { color: var(--backlog); } .stat.planned .n { color: var(--planned); }
55
+ .stat.inprogress .n { color: var(--inprogress); } .stat.audited .n { color: var(--audited); } .stat.done .n { color: var(--done); }
56
+ .progress { height: 7px; background: var(--bg-elev2); border-radius: 99px; overflow: hidden; margin: 8px 0 4px; border: 1px solid var(--border); }
57
+ .progress > span { display: block; height: 100%; background: var(--done); }
58
+ .progress-l { font-size: 11px; color: var(--text-faint); }
59
+
60
+ nav.tabs { display: flex; gap: 4px; flex-wrap: wrap; border-bottom: 1px solid var(--border); margin: 22px 0 8px; position: sticky; top: 0; background: var(--bg); z-index: 5; padding-top: 6px; }
61
+ nav.tabs button { background: transparent; border: none; border-bottom: 2px solid transparent; color: var(--text-dim); font-size: 14px; font-weight: 500; padding: 10px 14px; cursor: pointer; font-family: var(--sans); }
62
+ nav.tabs button:hover { color: var(--text); }
63
+ nav.tabs button.active { color: var(--text); border-bottom-color: var(--accent); }
64
+ .panel { display: none; } .panel.active { display: block; }
65
+
66
+ .nextup { background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 14%, var(--bg-elev)), var(--bg-elev)); border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--border)); border-radius: var(--radius); padding: 20px 22px; margin: 8px 0 28px; }
67
+ .nextup .tag { color: var(--accent); font-weight: 700; font-size: 13px; text-transform: uppercase; letter-spacing: 0.08em; }
68
+ .nextup h2 { margin: 6px 0; font-size: 22px; letter-spacing: -0.02em; }
69
+ .nextup .why { color: var(--text-dim); margin-bottom: 12px; }
70
+
71
+ /* Board — 4 collapsible columns */
72
+ .board { display: flex; gap: 14px; align-items: flex-start; }
73
+ .board > details { flex: 1 1 0; min-width: 0; background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius); }
74
+ .board > details:not([open]) { flex: 0 0 auto; }
75
+ .board > details > summary { list-style: none; cursor: pointer; padding: 12px 13px; display: flex; align-items: center; gap: 7px; font-weight: 600; font-size: 13.5px; user-select: none; }
76
+ .board > details > summary::-webkit-details-marker { display: none; }
77
+ .board > details > summary .chev { color: var(--text-faint); font-size: 10px; transition: transform .15s; margin-left: auto; }
78
+ .board > details[open] > summary .chev { transform: rotate(90deg); }
79
+ .dot { width: 9px; height: 9px; border-radius: 99px; display: inline-block; flex: none; }
80
+ .dot.backlog { background: var(--backlog); } .dot.planned { background: var(--planned); } .dot.inprogress { background: var(--inprogress); } .dot.audited { background: var(--audited); }
81
+ .count { color: var(--text-faint); font-weight: 500; font-size: 13px; }
82
+ .cards { display: flex; flex-direction: column; gap: 10px; padding: 0 11px 13px; }
83
+
84
+ .card { background: var(--bg-elev2); border: 1px solid var(--border); border-radius: 10px; padding: 12px 13px; position: relative; }
85
+ .card.backlog { border-left: 3px solid var(--backlog); } .card.planned { border-left: 3px solid var(--planned); }
86
+ .card.inprogress { border-left: 3px solid var(--inprogress); } .card.audited { border-left: 3px solid var(--audited); }
87
+ .card .rank { position: absolute; top: 11px; right: 12px; color: var(--text-faint); font-family: var(--mono); font-size: 12px; }
88
+ .card h4 { margin: 0 0 5px; font-size: 14.5px; letter-spacing: -0.01em; padding-right: 22px; }
89
+ .card .slug { font-family: var(--mono); font-size: 11px; color: var(--text-faint); }
90
+ .card .scope { font-size: 13px; color: var(--text-dim); margin: 8px 0; }
91
+ .card .done-line { font-size: 12px; color: var(--text-faint); margin: 6px 0; }
92
+ .card .done-line b { color: var(--text-dim); font-weight: 600; }
93
+ .card .row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 8px; }
94
+ .pill { font-size: 11px; padding: 2px 8px; border-radius: 99px; border: 1px solid var(--border); color: var(--text-dim); }
95
+ .pill.dep { font-family: var(--mono); }
96
+ .prio { font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 99px; }
97
+ .prio.crit { background: color-mix(in srgb, var(--crit) 18%, transparent); color: var(--crit); }
98
+ .prio.high { background: color-mix(in srgb, var(--high) 18%, transparent); color: var(--high); }
99
+ .prio.med { background: color-mix(in srgb, var(--med) 18%, transparent); color: var(--med); }
100
+ .prio.low { background: color-mix(in srgb, var(--low) 18%, transparent); color: var(--low); }
101
+ .empty { color: var(--text-faint); font-size: 13px; font-style: italic; padding: 4px 0; }
102
+
103
+ .cmd { display: flex; align-items: center; gap: 8px; margin-top: 9px; background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 7px 9px; font-family: var(--mono); font-size: 12px; }
104
+ .cmd code { flex: 1; overflow-x: auto; white-space: nowrap; color: var(--text); }
105
+ .cmd button { background: transparent; border: 1px solid var(--border); color: var(--text-dim); border-radius: 6px; padding: 3px 9px; font-size: 11px; cursor: pointer; font-family: var(--sans); white-space: nowrap; }
106
+ .cmd button:hover { color: var(--text); border-color: var(--text-dim); }
107
+ .cmd button.copied { color: var(--done); border-color: var(--done); }
108
+ .nextup .cmd { background: color-mix(in srgb, var(--accent) 10%, var(--bg-elev2)); font-size: 13px; }
109
+
110
+ /* Done timeline */
111
+ .done-list { display: flex; flex-direction: column; gap: 1px; margin-top: 10px; }
112
+ .done-item { display: flex; align-items: center; gap: 12px; padding: 8px 12px; border-radius: 8px; }
113
+ .done-item:hover { background: var(--bg-elev); }
114
+ .done-item .check { color: var(--done); font-weight: 700; }
115
+ .done-item .name { flex: 1; } .done-item .sslug { font-family: var(--mono); font-size: 11px; color: var(--text-faint); }
116
+ .done-item .date { font-family: var(--mono); font-size: 12px; color: var(--text-dim); }
117
+
118
+ details.sec { background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 12px; }
119
+ details.sec > summary { list-style: none; cursor: pointer; padding: 14px 18px; font-weight: 600; font-size: 15px; display: flex; align-items: center; gap: 8px; user-select: none; }
120
+ details.sec > summary::-webkit-details-marker { display: none; }
121
+ details.sec > summary .chev { color: var(--text-faint); font-size: 11px; margin-left: auto; transition: transform .15s; }
122
+ details.sec[open] > summary .chev { transform: rotate(90deg); }
123
+ details.sec .body { padding: 0 18px 18px; }
124
+ .callout { background: var(--bg-elev); border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: var(--radius); padding: 16px 18px; margin-bottom: 16px; font-size: 15px; }
125
+ ul.clean { margin: 0; padding-left: 18px; } ul.clean li { margin-bottom: 6px; color: var(--text-dim); font-size: 14px; }
126
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
127
+ th, td { text-align: left; padding: 7px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
128
+ th { color: var(--text-dim); font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; }
129
+ td code, .mono { font-family: var(--mono); font-size: 12px; }
130
+ .badge { font-size: 11px; padding: 1px 7px; border-radius: 99px; border: 1px solid var(--border); }
131
+
132
+ .sev { display: grid; grid-template-columns: repeat(4,1fr); gap: 12px; margin-bottom: 16px; }
133
+ .sevtile { border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 16px; background: var(--bg-elev); }
134
+ .sevtile .n { font-size: 24px; font-weight: 700; }
135
+ .sevtile.crit { border-left: 3px solid var(--crit); } .sevtile.crit .n { color: var(--crit); }
136
+ .sevtile.high { border-left: 3px solid var(--high); } .sevtile.high .n { color: var(--high); }
137
+ .sevtile.med { border-left: 3px solid var(--med); } .sevtile.med .n { color: var(--med); }
138
+ .sevtile.low { border-left: 3px solid var(--low); } .sevtile.low .n { color: var(--low); }
139
+ .sevtile .l { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-dim); }
140
+ .issue { border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; margin-bottom: 10px; background: var(--bg-elev); }
141
+ .issue .top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
142
+ .issue .id { font-family: var(--mono); font-size: 12px; color: var(--text-faint); }
143
+ .issue h4 { margin: 0; font-size: 14px; }
144
+ .issue .loc { font-family: var(--mono); font-size: 11px; color: var(--text-faint); margin-top: 4px; }
145
+ .issue .desc { font-size: 13px; color: var(--text-dim); margin-top: 6px; }
146
+
147
+ footer { margin-top: 48px; text-align: center; color: var(--text-faint); font-size: 12px; }
148
+
149
+ @media (max-width: 900px) {
150
+ .stats { grid-template-columns: repeat(3, 1fr); } .sev { grid-template-columns: repeat(2, 1fr); }
151
+ .board { flex-direction: column; } .board > details, .board > details:not([open]) { flex: 1 1 auto; width: 100%; }
152
+ }
153
+ </style>
154
+ </head>
155
+ <body>
156
+ <div class="wrap" id="app"></div>
157
+
158
+ <script>
159
+ // === DATA:START (commands replace only this object) ===
160
+ const DATA = {
161
+ project: "Project Name",
162
+ tagline: "One-line mission vision — what this project changes.",
163
+ generated: "2026-07-02",
164
+ generatedBy: "/plan-project",
165
+
166
+ roadmap: {
167
+ nextUp: { name: "example-feature", why: "The single most important thing next.", command: '!/catalyze-spec "seed prompt for the next feature"' },
168
+ backlog: [
169
+ { rank: 1, name: "Two-Factor Auth", slug: "two-factor-auth", scope: "TOTP-based 2FA on login.", doneWhen: "User enrolls + verifies TOTP.", dependsOn: "user-auth", priority: "high", command: '!/catalyze-spec "TOTP two-factor authentication on login"' },
170
+ { rank: 2, name: "Audit Log", slug: "audit-log", scope: "Record security-sensitive actions.", doneWhen: "Logins + role changes logged.", dependsOn: "#1", priority: "medium", command: '!/catalyze-spec "security audit log for sensitive actions"' }
171
+ ],
172
+ planned: [
173
+ { name: "Password Reset", slug: "2026-06-28-password-reset", scope: "Email-based reset with expiring tokens.", doneWhen: "User resets password via emailed link.", command: "!/forge-spec @2026-06-28-password-reset" }
174
+ ],
175
+ inProgress: [
176
+ { name: "User Authentication", slug: "2026-06-20-user-auth", scope: "Login, session, protected routes.", doneWhen: "Login + session pass E2E.", command: "!/audit-spec @2026-06-20-user-auth" }
177
+ ],
178
+ audited: [
179
+ { name: "Onboarding Flow", slug: "2026-06-18-onboarding", scope: "3-step guided onboarding.", doneWhen: "New users complete setup.", command: "!/seal-spec @2026-06-18-onboarding" }
180
+ ],
181
+ done: [
182
+ { name: "Project Setup", slug: "2026-06-10-project-setup", date: "2026-06-15" }
183
+ ],
184
+ vision: [ { goal: "SSO", description: "Enterprise single sign-on", prereq: "user-auth, audit-log" } ]
185
+ },
186
+
187
+ mission: {
188
+ vision: "Make X effortless for Y.",
189
+ pain: ["Users waste hours on manual step Z."],
190
+ whoSuffers: ["Small teams without dedicated ops."],
191
+ coreValue: ["Automates Z end to end."],
192
+ differentiators: ["Spec-driven", "Offline-first", "No lock-in"],
193
+ targetUsers: [ { persona: "Indie founder", description: "Builds solo", need: "Ship fast without process overhead" } ],
194
+ metrics: [ { metric: "Time to first spec", target: "< 10 min", timeline: "Q3" } ],
195
+ qualitativeGoals: ["Feels calm, not noisy."],
196
+ nonGoals: ["Not a full project-management suite."]
197
+ },
198
+
199
+ techStack: {
200
+ overview: "Next.js + FastAPI + Supabase, deployed on Vercel + DO.",
201
+ groups: [
202
+ { category: "Frontend", items: [ { name: "Next.js", version: "14.x", purpose: "App framework", docs: "" }, { name: "Tailwind", version: "3.x", purpose: "Styling", docs: "" } ] },
203
+ { category: "Backend", items: [ { name: "FastAPI", version: "0.11x", purpose: "API", docs: "" } ] },
204
+ { category: "Data", items: [ { name: "PostgreSQL", version: "15", purpose: "Primary DB (Supabase)", docs: "" } ] }
205
+ ],
206
+ decisions: [ { id: "ADR-001", title: "Celery over DB triggers", context: "Fan-out was fragile in triggers.", decision: "Move to Celery pollers.", consequences: "Simpler ops, explicit retries." } ]
207
+ },
208
+
209
+ architecture: {
210
+ overview: "Layered: presentation → application → domain → infrastructure.",
211
+ layers: [
212
+ { layer: "Presentation", location: "src/app/", responsibility: "HTTP handlers, UI" },
213
+ { layer: "Application", location: "src/services/", responsibility: "Use-case orchestration" },
214
+ { layer: "Domain", location: "src/domain/", responsibility: "Business rules" },
215
+ { layer: "Infrastructure", location: "src/lib/", responsibility: "External integrations" }
216
+ ],
217
+ patterns: [ { name: "Repository", purpose: "Abstract DB access", location: "src/db/repositories/" } ],
218
+ entryPoints: [ { name: "REST API", location: "src/app/api/", purpose: "Main routes" } ],
219
+ keyFiles: [ { purpose: "DB client", file: "src/lib/db.ts" }, { purpose: "Auth middleware", file: "src/middleware/auth.ts" } ]
220
+ },
221
+
222
+ concerns: {
223
+ summary: { critical: 0, high: 1, medium: 1, low: 0 },
224
+ issues: [
225
+ { id: "HIGH-001", title: "No rate limiting on auth", severity: "high", type: "Security", location: "src/api/auth/*", description: "Public auth endpoints unthrottled.", fix: "Add rate limiter middleware." },
226
+ { id: "MED-001", title: "N+1 in user list", severity: "medium", type: "Performance", location: "src/api/users/route.ts:34", description: "Related data loaded in a loop.", fix: "Use eager include / join." }
227
+ ],
228
+ techDebt: [ { id: "TD-001", location: "src/lib/legacy.ts", description: "Old auth path still present", effort: "High" } ],
229
+ security: [ { area: "Rate limiting", status: "Missing", notes: "Needed on public endpoints" }, { area: "Auth", status: "Good", notes: "JWT + refresh" } ],
230
+ knownBugs: [ { id: "BUG-001", description: "Login redirect fails on Safari", location: "src/lib/auth.ts", priority: "Medium" } ]
231
+ }
232
+ };
233
+ // === DATA:END ===
234
+
235
+ /* ---------- renderer (do not edit when regenerating) ---------- */
236
+ const h = (s) => String(s == null ? "" : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
237
+ const has = (a) => Array.isArray(a) && a.length > 0;
238
+ const prioClass = (p) => ({critical:'crit',high:'high',medium:'med',low:'low'}[(p||'').toLowerCase()] || 'med');
239
+ const chev = '<span class="chev">▶</span>';
240
+
241
+ function cmdBlock(cmd) {
242
+ if (!cmd) return "";
243
+ return `<div class="cmd"><code>${h(cmd)}</code><button onclick="copyCmd(this)" data-cmd="${h(cmd)}">Copy</button></div>`;
244
+ }
245
+ function copyCmd(btn) {
246
+ const t = btn.getAttribute('data-cmd');
247
+ navigator.clipboard?.writeText(t).then(() => {
248
+ btn.textContent = 'Copied'; btn.classList.add('copied');
249
+ setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1400);
250
+ }).catch(() => { btn.textContent = 'Copy failed'; });
251
+ }
252
+
253
+ function card(item, kind) {
254
+ const rank = item.rank ? `<span class="rank">#${h(item.rank)}</span>` : "";
255
+ const scope = item.scope ? `<div class="scope">${h(item.scope)}</div>` : "";
256
+ const done = item.doneWhen ? `<div class="done-line"><b>Done when:</b> ${h(item.doneWhen)}</div>` : "";
257
+ const prio = item.priority ? `<span class="prio ${prioClass(item.priority)}">${h(item.priority)}</span>` : "";
258
+ const dep = item.dependsOn ? `<span class="pill dep">↳ ${h(item.dependsOn)}</span>` : "";
259
+ const row = (prio || dep) ? `<div class="row">${prio}${dep}</div>` : "";
260
+ return `<div class="card ${kind}">${rank}<h4>${h(item.name)}</h4><div class="slug">${h(item.slug)}</div>${scope}${done}${row}${cmdBlock(item.command)}</div>`;
261
+ }
262
+ function column(title, kind, items, key) {
263
+ const body = has(items) ? items.map(i => card(i, kind)).join("") : `<div class="empty">Nothing here.</div>`;
264
+ return `<details class="dcol" data-k="${key}" open><summary><span class="dot ${kind}"></span>${h(title)} <span class="count">${(items||[]).length}</span>${chev}</summary><div class="cards">${body}</div></details>`;
265
+ }
266
+ function renderRoadmap(r) {
267
+ if (!r) return "";
268
+ const nu = r.nextUp;
269
+ const nextUp = nu && nu.name ? `<div class="nextup"><div class="tag">▶ Next Up</div><h2>${h(nu.name)}</h2><div class="why">${h(nu.why)}</div>${cmdBlock(nu.command)}</div>` : "";
270
+ const board = `<div class="board">
271
+ ${column("Backlog", "backlog", r.backlog, "backlog")}
272
+ ${column("Planned", "planned", r.planned, "planned")}
273
+ ${column("In Progress", "inprogress", r.inProgress, "inprogress")}
274
+ ${column("Audited", "audited", r.audited, "audited")}
275
+ </div>`;
276
+ const done = has(r.done) ? `<h3 class="section">✅ Done — sealed</h3><div class="done-list">${r.done.map(s => `<div class="done-item"><span class="check">✓</span><span class="name">${h(s.name)}</span><span class="sslug">${h(s.slug)}</span><span class="date">${h(s.date||"")}</span></div>`).join("")}</div>` : "";
277
+ const vision = has(r.vision) ? `<h3 class="section">Vision / Someday</h3><table><thead><tr><th>Goal</th><th>Description</th><th>Prerequisite Specs</th></tr></thead><tbody>${r.vision.map(v => `<tr><td>${h(v.goal)}</td><td>${h(v.description)}</td><td class="mono">${h(v.prereq)}</td></tr>`).join("")}</tbody></table>` : "";
278
+ return nextUp + board + done + vision;
279
+ }
280
+
281
+ function bulletCard(title, items) {
282
+ if (!has(items)) return "";
283
+ return `<details class="sec" open><summary>${h(title)}${chev}</summary><div class="body"><ul class="clean">${items.map(i=>`<li>${h(i)}</li>`).join("")}</ul></div></details>`;
284
+ }
285
+ function renderMission(m) {
286
+ if (!m) return "";
287
+ const vision = m.vision ? `<div class="callout">${h(m.vision)}</div>` : "";
288
+ const users = has(m.targetUsers) ? `<details class="sec" open><summary>Target Users${chev}</summary><div class="body"><table><thead><tr><th>Persona</th><th>Description</th><th>Primary Need</th></tr></thead><tbody>${m.targetUsers.map(u=>`<tr><td>${h(u.persona)}</td><td>${h(u.description)}</td><td>${h(u.need)}</td></tr>`).join("")}</tbody></table></div></details>` : "";
289
+ const metrics = has(m.metrics) ? `<details class="sec" open><summary>Success Metrics${chev}</summary><div class="body"><table><thead><tr><th>Metric</th><th>Target</th><th>Timeline</th></tr></thead><tbody>${m.metrics.map(x=>`<tr><td>${h(x.metric)}</td><td>${h(x.target)}</td><td>${h(x.timeline)}</td></tr>`).join("")}</tbody></table></div></details>` : "";
290
+ return vision + bulletCard("The Pain", m.pain) + bulletCard("Who Suffers", m.whoSuffers) + bulletCard("Core Value", m.coreValue) + bulletCard("Differentiators", m.differentiators) + users + metrics + bulletCard("Qualitative Goals", m.qualitativeGoals) + bulletCard("Non-Goals", m.nonGoals);
291
+ }
292
+
293
+ function renderTech(t) {
294
+ if (!t) return "";
295
+ const overview = t.overview ? `<div class="callout">${h(t.overview)}</div>` : "";
296
+ const groups = has(t.groups) ? t.groups.map(g => `<details class="sec" open><summary>${h(g.category)}${chev}</summary><div class="body"><table><thead><tr><th>Technology</th><th>Version</th><th>Purpose</th><th>Docs</th></tr></thead><tbody>${(g.items||[]).map(it=>`<tr><td>${h(it.name)}</td><td class="mono">${h(it.version)}</td><td>${h(it.purpose)}</td><td>${it.docs?`<span class="mono">${h(it.docs)}</span>`:''}</td></tr>`).join("")}</tbody></table></div></details>`).join("") : "";
297
+ const adr = has(t.decisions) ? `<h3 class="section">Architecture Decisions</h3>` + t.decisions.map(d=>`<details class="sec"><summary>${h(d.id)} — ${h(d.title)}${chev}</summary><div class="body"><p><b>Context:</b> ${h(d.context)}</p><p><b>Decision:</b> ${h(d.decision)}</p><p><b>Consequences:</b> ${h(d.consequences)}</p></div></details>`).join("") : "";
298
+ return overview + groups + adr;
299
+ }
300
+
301
+ function renderArch(a) {
302
+ if (!a) return "";
303
+ const overview = a.overview ? `<div class="callout">${h(a.overview)}</div>` : "";
304
+ const layers = has(a.layers) ? `<details class="sec" open><summary>Layers${chev}</summary><div class="body"><table><thead><tr><th>Layer</th><th>Location</th><th>Responsibility</th></tr></thead><tbody>${a.layers.map(l=>`<tr><td>${h(l.layer)}</td><td class="mono">${h(l.location)}</td><td>${h(l.responsibility)}</td></tr>`).join("")}</tbody></table></div></details>` : "";
305
+ const patterns = has(a.patterns) ? `<details class="sec"><summary>Patterns${chev}</summary><div class="body"><table><thead><tr><th>Pattern</th><th>Purpose</th><th>Location</th></tr></thead><tbody>${a.patterns.map(p=>`<tr><td>${h(p.name)}</td><td>${h(p.purpose)}</td><td class="mono">${h(p.location)}</td></tr>`).join("")}</tbody></table></div></details>` : "";
306
+ const entries = has(a.entryPoints) ? `<details class="sec"><summary>Entry Points${chev}</summary><div class="body"><table><thead><tr><th>Entry Point</th><th>Location</th><th>Purpose</th></tr></thead><tbody>${a.entryPoints.map(e=>`<tr><td>${h(e.name)}</td><td class="mono">${h(e.location)}</td><td>${h(e.purpose)}</td></tr>`).join("")}</tbody></table></div></details>` : "";
307
+ const files = has(a.keyFiles) ? `<details class="sec"><summary>Key Files${chev}</summary><div class="body"><table><thead><tr><th>Purpose</th><th>File</th></tr></thead><tbody>${a.keyFiles.map(f=>`<tr><td>${h(f.purpose)}</td><td class="mono">${h(f.file)}</td></tr>`).join("")}</tbody></table></div></details>` : "";
308
+ return overview + layers + patterns + entries + files;
309
+ }
310
+
311
+ function renderConcerns(c) {
312
+ if (!c) return "";
313
+ const s = c.summary || {};
314
+ const tiles = `<div class="sev">
315
+ <div class="sevtile crit"><div class="n">${s.critical||0}</div><div class="l">Critical</div></div>
316
+ <div class="sevtile high"><div class="n">${s.high||0}</div><div class="l">High</div></div>
317
+ <div class="sevtile med"><div class="n">${s.medium||0}</div><div class="l">Medium</div></div>
318
+ <div class="sevtile low"><div class="n">${s.low||0}</div><div class="l">Low</div></div>
319
+ </div>`;
320
+ const issues = has(c.issues) ? `<h3 class="section">Issues</h3>` + c.issues.map(i=>`<div class="issue"><div class="top"><span class="prio ${prioClass(i.severity)}">${h(i.severity)}</span><span class="id">${h(i.id)}</span>${i.type?`<span class="badge">${h(i.type)}</span>`:''}<h4>${h(i.title)}</h4></div>${i.location?`<div class="loc">${h(i.location)}</div>`:''}${i.description?`<div class="desc">${h(i.description)}</div>`:''}${i.fix?`<div class="desc"><b>Fix:</b> ${h(i.fix)}</div>`:''}</div>`).join("") : "";
321
+ const debt = has(c.techDebt) ? `<details class="sec"><summary>Tech Debt${chev}</summary><div class="body"><table><thead><tr><th>ID</th><th>Location</th><th>Description</th><th>Effort</th></tr></thead><tbody>${c.techDebt.map(d=>`<tr><td class="mono">${h(d.id)}</td><td class="mono">${h(d.location)}</td><td>${h(d.description)}</td><td>${h(d.effort)}</td></tr>`).join("")}</tbody></table></div></details>` : "";
322
+ const sec = has(c.security) ? `<details class="sec"><summary>Security Notes${chev}</summary><div class="body"><table><thead><tr><th>Area</th><th>Status</th><th>Notes</th></tr></thead><tbody>${c.security.map(x=>`<tr><td>${h(x.area)}</td><td><span class="badge">${h(x.status)}</span></td><td>${h(x.notes)}</td></tr>`).join("")}</tbody></table></div></details>` : "";
323
+ const bugs = has(c.knownBugs) ? `<details class="sec"><summary>Known Bugs${chev}</summary><div class="body"><table><thead><tr><th>ID</th><th>Description</th><th>Location</th><th>Priority</th></tr></thead><tbody>${c.knownBugs.map(b=>`<tr><td class="mono">${h(b.id)}</td><td>${h(b.description)}</td><td class="mono">${h(b.location)}</td><td>${h(b.priority)}</td></tr>`).join("")}</tbody></table></div></details>` : "";
324
+ return tiles + issues + debt + sec + bugs;
325
+ }
326
+
327
+ function render() {
328
+ const d = DATA, r = d.roadmap || {};
329
+ const nBack=(r.backlog||[]).length, nPlan=(r.planned||[]).length, nProg=(r.inProgress||[]).length, nAud=(r.audited||[]).length, nDone=(r.done||[]).length;
330
+ const total = nBack+nPlan+nProg+nAud+nDone, pct = total ? Math.round((nDone/total)*100) : 0;
331
+
332
+ const tabDefs = [
333
+ { id:"roadmap", label:"Roadmap", html: renderRoadmap(d.roadmap), show: !!d.roadmap },
334
+ { id:"mission", label:"Mission", html: renderMission(d.mission), show: !!(d.mission && (d.mission.vision || has(d.mission.pain) || has(d.mission.targetUsers))) },
335
+ { id:"tech", label:"Tech Stack", html: renderTech(d.techStack), show: !!(d.techStack && (d.techStack.overview || has(d.techStack.groups))) },
336
+ { id:"arch", label:"Architecture", html: renderArch(d.architecture), show: !!(d.architecture && (d.architecture.overview || has(d.architecture.layers))) },
337
+ { id:"concerns", label:"Concerns", html: renderConcerns(d.concerns), show: !!(d.concerns) }
338
+ ].filter(t => t.show);
339
+
340
+ const saved = localStorage.getItem('catalyst-dash-tab');
341
+ const activeId = tabDefs.some(t=>t.id===saved) ? saved : (tabDefs[0] && tabDefs[0].id);
342
+ const nav = tabDefs.map(t => `<button data-tab="${t.id}" class="${t.id===activeId?'active':''}">${h(t.label)}</button>`).join("");
343
+ const panels = tabDefs.map(t => `<section class="panel ${t.id===activeId?'active':''}" id="tab-${t.id}">${t.html}</section>`).join("");
344
+
345
+ document.getElementById('app').innerHTML = `
346
+ <header>
347
+ <h1>${h(d.project)}</h1>
348
+ ${d.tagline?`<div class="tagline">${h(d.tagline)}</div>`:''}
349
+ <div class="meta">Updated ${h(d.generated)} by <code>${h(d.generatedBy)}</code></div>
350
+ </header>
351
+ <div class="stats">
352
+ <div class="stat backlog"><div class="n">${nBack}</div><div class="l">Backlog</div></div>
353
+ <div class="stat planned"><div class="n">${nPlan}</div><div class="l">Planned</div></div>
354
+ <div class="stat inprogress"><div class="n">${nProg}</div><div class="l">In Progress</div></div>
355
+ <div class="stat audited"><div class="n">${nAud}</div><div class="l">Audited</div></div>
356
+ <div class="stat done"><div class="n">${nDone}</div><div class="l">Done</div></div>
357
+ </div>
358
+ <div class="progress" title="${pct}% done"><span style="width:${pct}%"></span></div>
359
+ <div class="progress-l">${pct}% of ${total} known specs sealed</div>
360
+ <nav class="tabs">${nav}</nav>
361
+ ${panels}
362
+ <footer>Catalyst OS dashboard — updates on every spec transition, or re-run <code>/plan-project</code> / <code>/sync-project</code>.</footer>
363
+ `;
364
+
365
+ document.querySelectorAll('nav.tabs button').forEach(btn => btn.addEventListener('click', () => {
366
+ const id = btn.getAttribute('data-tab');
367
+ document.querySelectorAll('nav.tabs button').forEach(b=>b.classList.toggle('active', b===btn));
368
+ document.querySelectorAll('.panel').forEach(p=>p.classList.toggle('active', p.id===`tab-${id}`));
369
+ localStorage.setItem('catalyst-dash-tab', id);
370
+ }));
371
+ document.querySelectorAll('details.dcol').forEach(det => {
372
+ const key = 'catalyst-dash-col-' + det.getAttribute('data-k');
373
+ if (localStorage.getItem(key) === 'closed') det.removeAttribute('open');
374
+ det.addEventListener('toggle', () => localStorage.setItem(key, det.open ? 'open' : 'closed'));
375
+ });
376
+ }
377
+ render();
378
+ </script>
379
+ </body>
380
+ </html>
@@ -0,0 +1,65 @@
1
+ # Mission Template
2
+
3
+ ## Vision
4
+
5
+ <!-- The big picture - what do you want to change? -->
6
+
7
+ > [One-liner vision statement]
8
+
9
+ ## Problem Statement
10
+
11
+ <!-- What problem are you solving? Who suffers from this problem? -->
12
+
13
+ ### The Pain
14
+
15
+ -
16
+
17
+ ### Who Suffers
18
+
19
+ -
20
+
21
+ ## Value Proposition
22
+
23
+ <!-- Why does this project exist? What differentiates it from alternatives? -->
24
+
25
+ ### Core Value
26
+
27
+ -
28
+
29
+ ### Differentiators
30
+
31
+ 1.
32
+ 2.
33
+ 3.
34
+
35
+ ## Target Users
36
+
37
+ <!-- Who are you building for? -->
38
+
39
+ | Persona | Description | Primary Need |
40
+ |---------|-------------|--------------|
41
+ | | | |
42
+
43
+ ## Success Criteria
44
+
45
+ <!-- How will you measure success? -->
46
+
47
+ ### Metrics
48
+
49
+ | Metric | Target | Timeline |
50
+ |--------|--------|----------|
51
+ | | | |
52
+
53
+ ### Qualitative Goals
54
+
55
+ -
56
+
57
+ ## Non-Goals
58
+
59
+ <!-- What is this project NOT? What's out of scope? -->
60
+
61
+ -
62
+
63
+ ---
64
+
65
+ *Generated by `/catalyze-project` on [DATE]*
@@ -0,0 +1,166 @@
1
+ # Project Roadmap
2
+
3
+ > **The roadmap is a queue of specs, not a checklist of tasks.**
4
+ > One entry = one spec = one forge cycle (one PR-sized unit of work).
5
+ > Tasks live in each spec's `tasks.md`, never here.
6
+ > Sections are **lifecycle states** — a spec moves across the board as it progresses:
7
+ > `Backlog → Planned → In Progress → Audited → Done`.
8
+ > The board shows the 4 active states; **Done** is the sealed history below.
9
+
10
+ ---
11
+
12
+ ## ▶ Next Up
13
+
14
+ <!--
15
+ The single most important thing to do next. Exactly ONE spec.
16
+ Resolution rule (closest to Done first):
17
+ IF anything Audited → that spec (seal it)
18
+ ELSE IF In Progress → that spec (finish it)
19
+ ELSE IF Planned → highest-priority Planned spec (forge it)
20
+ ELSE → Backlog #1 (catalyze it)
21
+ -->
22
+
23
+ **[spec-name]** — [one-line why this is next]
24
+
25
+ ```
26
+ !/[resolved command for this spec]
27
+ ```
28
+
29
+ ---
30
+
31
+ ## 🗂️ Backlog
32
+
33
+ <!--
34
+ Ideas we WILL build, not yet catalyzed. This is an ORDERED queue —
35
+ the number IS the priority. #1 is what becomes "Next Up" once the board clears.
36
+ Each entry must be ONE spec. If it needs more than ~5-8 tasks or spans
37
+ multiple subsystems, split it into multiple backlog entries.
38
+ -->
39
+
40
+ ### 1. [Feature Name]
41
+ **Catalyze as:** `feature-slug`
42
+ **Scope:** [one-line scope — one spec's worth of work]
43
+ **Done when:** [acceptance criteria]
44
+ **Depends on:** [prior entry, or "nothing"]
45
+ **Priority:** 🔴 Critical / 🟠 High / 🟡 Medium / 🟢 Low
46
+
47
+ **Seed prompt:**
48
+ ```
49
+ [prompt to hand /catalyze-spec]
50
+ ```
51
+
52
+ **References:**
53
+ - 📁 `./designs/feature.png`
54
+ - 🔗 https://docs.example.com
55
+
56
+ ```
57
+ !/catalyze-spec "[seed prompt]"
58
+ ```
59
+
60
+ ---
61
+
62
+ ### 2. [Feature Name]
63
+ **Catalyze as:** `another-feature-slug`
64
+ **Scope:** [one-line scope]
65
+ **Done when:** [acceptance criteria]
66
+ **Depends on:** #1
67
+ **Priority:** 🟠 High
68
+
69
+ **Seed prompt:**
70
+ ```
71
+ [prompt to hand /catalyze-spec]
72
+ ```
73
+
74
+ ```
75
+ !/catalyze-spec "[seed prompt]"
76
+ ```
77
+
78
+ ---
79
+
80
+ ## 📋 Planned
81
+
82
+ <!-- spec.md exists (catalyzed), no tasks.md yet. Ready to forge. -->
83
+
84
+ ### [Feature Name]
85
+ **Slug:** `2026-02-15-feature-slug`
86
+ **Scope:** [one-line scope — what this spec covers]
87
+ **Done when:** [acceptance criteria]
88
+
89
+ ```
90
+ !/forge-spec @2026-02-15-feature-slug
91
+ ```
92
+ > Or `!/forge-spec-worktree @2026-02-15-feature-slug` if another spec is in progress.
93
+
94
+ ---
95
+
96
+ ## 🔨 In Progress
97
+
98
+ <!-- tasks.md exists, build ongoing. Keep this to 0-1 specs (WIP limit). -->
99
+
100
+ ### [Feature Name]
101
+ **Slug:** `2026-01-11-feature-slug`
102
+ **Done when:** [acceptance criteria — how we know this spec is complete]
103
+
104
+ ```
105
+ !/audit-spec @2026-01-11-feature-slug
106
+ ```
107
+
108
+ ---
109
+
110
+ ## 🔬 Audited
111
+
112
+ <!-- validation.md exists and audit PASSED, not yet sealed. Ready to seal. -->
113
+
114
+ ### [Feature Name]
115
+ **Slug:** `2026-01-08-feature-slug`
116
+ **Done when:** [acceptance criteria]
117
+
118
+ ```
119
+ !/seal-spec @2026-01-08-feature-slug
120
+ ```
121
+
122
+ ---
123
+
124
+ ## ✅ Done
125
+
126
+ <!-- Sealed/archived specs. Gives closure and history. Newest first. -->
127
+
128
+ | Spec | Slug | Sealed |
129
+ |------|------|--------|
130
+ | [Feature Name] | `2026-01-05-feature-slug` | 2026-01-20 |
131
+
132
+ ---
133
+
134
+ ## Vision / Someday
135
+
136
+ <!-- Strategic goals not yet ready to be specs. NOT part of the ordered backlog. -->
137
+
138
+ | Goal | Description | Prerequisite Specs |
139
+ |------|-------------|--------------------|
140
+ | | | |
141
+
142
+ ---
143
+
144
+ ## Current State
145
+
146
+ ### Existing Features
147
+
148
+ | Feature | Status | Maturity |
149
+ |---------|--------|----------|
150
+ | | ✅ Complete / 🚧 In Progress / 📋 Planned | MVP / Beta / Stable |
151
+
152
+ ### Technical Debt
153
+
154
+ -
155
+
156
+ ---
157
+
158
+ ## Changelog
159
+
160
+ | Date | Change | Author |
161
+ |------|--------|--------|
162
+ | [DATE] | Initial roadmap created | `/catalyze-project` |
163
+
164
+ ---
165
+
166
+ *Generated by `/catalyze-project` on [DATE]*