catalyst-os 3.1.0 → 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.
- package/.catalyst/bin/install.js +9 -0
- package/.catalyst/main/temp/temp-architecture.md +188 -0
- package/.catalyst/main/temp/temp-concerns.md +225 -0
- package/.catalyst/main/temp/temp-conventions.md +321 -0
- package/.catalyst/main/temp/temp-dashboard.html +380 -0
- package/.catalyst/main/temp/temp-mission.md +65 -0
- package/.catalyst/main/temp/temp-roadmap.md +166 -0
- package/.catalyst/main/temp/temp-tech-stack.md +165 -0
- package/package.json +2 -1
|
@@ -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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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]*
|