@skitterbyte/skitterspec 0.1.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.
@@ -0,0 +1,127 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Config loader for the release-artifact generators.
5
+ *
6
+ * Reads `skitterspec.config.json` from the repo root and normalises it over
7
+ * documented defaults. Shipped alongside the generators (copied into the
8
+ * consumer's `scripts/lib/`) so the consumer's scripts never depend back into
9
+ * the skitterspec package. Zero-dependency.
10
+ *
11
+ * Shape:
12
+ * {
13
+ * "version": 1,
14
+ * "changelog": { "enabled": true, "file": "CHANGELOG.md" },
15
+ * "releases": { "enabled": true, "file": "RELEASES.md",
16
+ * "productName": "<repo name>", "scopeAreas": {} },
17
+ * "versionHook": true
18
+ * }
19
+ */
20
+
21
+ const { readFileSync } = require('node:fs')
22
+ const { basename, join } = require('node:path')
23
+
24
+ const SCHEMA_VERSION = 1
25
+ const CONFIG_FILE = 'skitterspec.config.json'
26
+
27
+ // Static template (productName is derived from the repo dir when blank).
28
+ const DEFAULT_CONFIG = Object.freeze({
29
+ version: SCHEMA_VERSION,
30
+ changelog: Object.freeze({ enabled: true, file: 'CHANGELOG.md' }),
31
+ releases: Object.freeze({
32
+ enabled: true,
33
+ file: 'RELEASES.md',
34
+ productName: '',
35
+ scopeAreas: Object.freeze({}),
36
+ }),
37
+ versionHook: true,
38
+ })
39
+
40
+ function isObject(value) {
41
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
42
+ }
43
+
44
+ function defaultsFor(dir) {
45
+ return {
46
+ version: SCHEMA_VERSION,
47
+ changelog: { enabled: true, file: 'CHANGELOG.md' },
48
+ releases: {
49
+ enabled: true,
50
+ file: 'RELEASES.md',
51
+ productName: basename(dir),
52
+ scopeAreas: {},
53
+ },
54
+ versionHook: true,
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Merge a parsed config over the defaults. Only known keys are copied (unknown
60
+ * keys are ignored for forward-compat); `scopeAreas` is replaced wholesale, not
61
+ * deep-merged, since it's a complete map.
62
+ */
63
+ function mergeConfig(base, parsed) {
64
+ if (!isObject(parsed)) return base
65
+
66
+ if (typeof parsed.version === 'number') base.version = parsed.version
67
+ if (typeof parsed.versionHook === 'boolean') base.versionHook = parsed.versionHook
68
+
69
+ if (isObject(parsed.changelog)) {
70
+ if (typeof parsed.changelog.enabled === 'boolean') {
71
+ base.changelog.enabled = parsed.changelog.enabled
72
+ }
73
+ if (typeof parsed.changelog.file === 'string' && parsed.changelog.file.trim()) {
74
+ base.changelog.file = parsed.changelog.file.trim()
75
+ }
76
+ }
77
+
78
+ if (isObject(parsed.releases)) {
79
+ if (typeof parsed.releases.enabled === 'boolean') {
80
+ base.releases.enabled = parsed.releases.enabled
81
+ }
82
+ if (typeof parsed.releases.file === 'string' && parsed.releases.file.trim()) {
83
+ base.releases.file = parsed.releases.file.trim()
84
+ }
85
+ if (typeof parsed.releases.productName === 'string' && parsed.releases.productName.trim()) {
86
+ base.releases.productName = parsed.releases.productName.trim()
87
+ }
88
+ if (isObject(parsed.releases.scopeAreas)) {
89
+ base.releases.scopeAreas = { ...parsed.releases.scopeAreas }
90
+ }
91
+ }
92
+
93
+ return base
94
+ }
95
+
96
+ /**
97
+ * Load and normalise config from `dir` (default cwd). Missing file → all
98
+ * defaults. Malformed JSON → throws a clear Error (callers exit non-zero).
99
+ */
100
+ function loadConfig(dir = process.cwd()) {
101
+ const base = defaultsFor(dir)
102
+ const file = join(dir, CONFIG_FILE)
103
+
104
+ let raw
105
+ try {
106
+ raw = readFileSync(file, 'utf-8')
107
+ } catch (error) {
108
+ if (error.code === 'ENOENT') return base
109
+ throw error
110
+ }
111
+
112
+ let parsed
113
+ try {
114
+ parsed = JSON.parse(raw)
115
+ } catch (error) {
116
+ throw new Error(`Invalid ${CONFIG_FILE}: ${error.message}`)
117
+ }
118
+
119
+ return mergeConfig(base, parsed)
120
+ }
121
+
122
+ module.exports = {
123
+ loadConfig,
124
+ DEFAULT_CONFIG,
125
+ SCHEMA_VERSION,
126
+ CONFIG_FILE,
127
+ }
@@ -0,0 +1,265 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Shared git-history plumbing for the release artifact generators.
5
+ *
6
+ * Both `generate-changelog.js` (dev-facing CHANGELOG, from commit subjects) and
7
+ * `generate-releases.js` (user-facing RELEASES, from `Release-Note:` footers)
8
+ * walk the same tag ranges and parse the same conventional-commit format. This
9
+ * module is the single source of that logic so the two generators cannot drift.
10
+ *
11
+ * Commit serialisation: git log emits `hash\0subject\0body\0` per commit (NUL
12
+ * delimiters so multi-line bodies survive). `reconstructCommits` regroups the
13
+ * flat NUL-split array back into per-commit `hash\0subject\0body` strings.
14
+ *
15
+ * A parsed commit is a plain object:
16
+ * { type, scope?, message, body?, hash, breaking }
17
+ */
18
+
19
+ const { execSync } = require('node:child_process')
20
+
21
+ function getCommitsSinceLastTag(currentVersion) {
22
+ try {
23
+ // Fetch tags to ensure they're available (important in CI)
24
+ try {
25
+ execSync('git fetch --tags --force', { encoding: 'utf-8', stdio: 'pipe' })
26
+ } catch {
27
+ // If fetch fails, continue - tags might already be available
28
+ }
29
+
30
+ // Get all tags sorted by version (newest first)
31
+ const allTags = execSync('git tag --sort=-version:refname', {
32
+ encoding: 'utf-8',
33
+ stdio: 'pipe',
34
+ })
35
+ .trim()
36
+ .split('\n')
37
+ .filter((tag) => tag.trim().length > 0)
38
+
39
+ // Determine the previous tag to compare against
40
+ let previousTag = null
41
+ let currentTag = null
42
+
43
+ if (allTags.length === 0) {
44
+ // No tags exist, get all commits
45
+ const output = execSync('git log --pretty=format:"%h%x00%s%x00%b%x00" --no-merges', {
46
+ encoding: 'utf-8',
47
+ stdio: 'pipe',
48
+ }).trim()
49
+
50
+ return reconstructCommits(output)
51
+ }
52
+
53
+ // Check if HEAD is at a tag
54
+ try {
55
+ currentTag = execSync('git describe --tags --exact-match HEAD', {
56
+ encoding: 'utf-8',
57
+ stdio: 'pipe',
58
+ }).trim()
59
+ } catch {
60
+ // HEAD is not at a tag - try to get current branch/tag from environment
61
+ // In CI, Build.SourceBranchName might be available
62
+ const sourceBranch = process.env.BUILD_SOURCEBRANCHNAME || process.env.BUILD_SOURCEBRANCH
63
+ if (sourceBranch && sourceBranch.startsWith('v')) {
64
+ currentTag = sourceBranch
65
+ } else if (currentVersion) {
66
+ // Use the version parameter as fallback (e.g., "8.0.0" -> "v8.0.0")
67
+ const versionTag = `v${currentVersion}`
68
+ if (allTags.includes(versionTag)) {
69
+ currentTag = versionTag
70
+ }
71
+ }
72
+ }
73
+
74
+ if (currentTag && allTags.includes(currentTag)) {
75
+ // HEAD is at a tag - find the previous tag
76
+ const currentIndex = allTags.indexOf(currentTag)
77
+ if (currentIndex > 0) {
78
+ // There is a previous tag
79
+ previousTag = allTags[currentIndex - 1]
80
+ } else {
81
+ // This is the first tag, get all commits
82
+ const output = execSync('git log --pretty=format:"%h%x00%s%x00%b%x00" --no-merges', {
83
+ encoding: 'utf-8',
84
+ stdio: 'pipe',
85
+ }).trim()
86
+
87
+ return reconstructCommits(output)
88
+ }
89
+ } else {
90
+ // HEAD is not at a tag, use the most recent tag
91
+ previousTag = allTags[0]
92
+ }
93
+
94
+ if (!previousTag) {
95
+ // No previous tag found, get all commits
96
+ const output = execSync('git log --pretty=format:"%h%x00%s%x00%b%x00" --no-merges', {
97
+ encoding: 'utf-8',
98
+ stdio: 'pipe',
99
+ }).trim()
100
+
101
+ return reconstructCommits(output)
102
+ }
103
+
104
+ // When HEAD is at a tag, use the tag explicitly instead of HEAD
105
+ // This ensures we get commits up to and including the tag commit
106
+ const rangeEnd = currentTag || 'HEAD'
107
+
108
+ // Get commits since previous tag (inclusive of rangeEnd)
109
+ // Use null character as delimiter to handle multi-line bodies
110
+ // Format: hash\0subject\0body\0hash2\0subject2\0body2\0...
111
+ // NOTE: do NOT pass --all here — it traverses every ref (branches,
112
+ // remotes, tags) and leaks commits from unmerged branches into the
113
+ // range. Shallow-clone fallback below uses git fetch --unshallow.
114
+ const output = execSync(
115
+ `git log ${previousTag}..${rangeEnd} --pretty=format:"%h%x00%s%x00%b%x00" --no-merges`,
116
+ { encoding: 'utf-8', stdio: 'pipe' },
117
+ ).trim()
118
+
119
+ const commits = reconstructCommits(output)
120
+
121
+ // If no commits found and we're in CI, try unshallow the repo
122
+ if (commits.length === 0) {
123
+ try {
124
+ execSync('git fetch --unshallow', { encoding: 'utf-8', stdio: 'pipe' })
125
+ // Try again after unshallow
126
+ const retryOutput = execSync(
127
+ `git log ${previousTag}..${rangeEnd} --pretty=format:"%h%x00%s%x00%b%x00" --no-merges`,
128
+ { encoding: 'utf-8', stdio: 'pipe' },
129
+ ).trim()
130
+ return reconstructCommits(retryOutput)
131
+ } catch {
132
+ // Unshallow failed or not a shallow clone, return empty
133
+ }
134
+ }
135
+
136
+ return commits
137
+ } catch (error) {
138
+ // If git commands fail, try to get all commits as fallback
139
+ try {
140
+ const output = execSync('git log --pretty=format:"%h%x00%s%x00%b%x00" --no-merges', {
141
+ encoding: 'utf-8',
142
+ stdio: 'pipe',
143
+ }).trim()
144
+
145
+ return reconstructCommits(output)
146
+ } catch {
147
+ console.error('Failed to get git commits:', error)
148
+ return []
149
+ }
150
+ }
151
+ }
152
+
153
+ function reconstructCommits(output) {
154
+ if (!output.trim()) {
155
+ return []
156
+ }
157
+
158
+ // Split by null character - DO NOT filter empty parts yet
159
+ // Empty bodies are valid and needed to maintain correct grouping
160
+ const parts = output.split('\0')
161
+
162
+ const commits = []
163
+
164
+ // Group parts into commits: each commit has hash, subject, body
165
+ // Parts array: [hash1, subject1, body1, hash2, subject2, body2, ...]
166
+ // Trailing empty string from final \0 is expected and ignored
167
+ for (let i = 0; i < parts.length - 1; i += 3) {
168
+ const hash = parts[i] || ''
169
+ const subject = parts[i + 1] || ''
170
+ const body = parts[i + 2] || ''
171
+
172
+ // Only add commit if we have hash and subject (body can be empty)
173
+ if (hash.trim() && subject.trim()) {
174
+ // Reconstruct commit string with null delimiters
175
+ commits.push(`${hash}\0${subject}\0${body}`)
176
+ }
177
+ }
178
+
179
+ return commits
180
+ }
181
+
182
+ function parseCommit(commitLine) {
183
+ // Split by null character (used as delimiter in git log format)
184
+ const parts = commitLine.split('\0')
185
+
186
+ // Need at least hash and subject (body is optional)
187
+ if (parts.length < 2) {
188
+ return null // Invalid format, skip
189
+ }
190
+
191
+ const hash = parts[0].trim()
192
+ const subject = parts[1].trim()
193
+ const body = (parts[2] && parts[2].trim()) || undefined
194
+
195
+ // Parse conventional commit format: type(scope)!: description
196
+ // The optional `!` marks a breaking change per the Conventional Commits spec.
197
+ const conventionalCommitRegex = /^(\w+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/
198
+ const match = subject.match(conventionalCommitRegex)
199
+
200
+ if (!match) {
201
+ return null // Skip non-conventional commits
202
+ }
203
+
204
+ const [, type, scope, bang, message] = match
205
+
206
+ // Breaking change markers:
207
+ // 1. `!` suffix on type/scope (e.g. `feat!:` or `feat(api)!:`)
208
+ // 2. A `BREAKING CHANGE:` or `BREAKING-CHANGE:` footer in the body
209
+ const breakingFooterRegex = /(^|\n)BREAKING[- ]CHANGE:/i
210
+ const breaking = Boolean(bang) || (body ? breakingFooterRegex.test(body) : false)
211
+
212
+ return {
213
+ type: type.toLowerCase(),
214
+ scope: scope || undefined,
215
+ message: message.trim(),
216
+ body: body,
217
+ hash: hash.trim(),
218
+ breaking,
219
+ }
220
+ }
221
+
222
+ function getAllVersionTags() {
223
+ try {
224
+ execSync('git fetch --tags --force', { encoding: 'utf-8', stdio: 'pipe' })
225
+ } catch {
226
+ // fetch is best-effort
227
+ }
228
+
229
+ return execSync('git tag --sort=-version:refname', { encoding: 'utf-8', stdio: 'pipe' })
230
+ .trim()
231
+ .split('\n')
232
+ .map((t) => t.trim())
233
+ .filter((t) => /^v?\d+\.\d+\.\d+/.test(t))
234
+ }
235
+
236
+ function getCommitsBetween(fromTag, toTag) {
237
+ const range = fromTag ? `${fromTag}..${toTag}` : toTag
238
+ const output = execSync(`git log ${range} --pretty=format:"%h%x00%s%x00%b%x00" --no-merges`, {
239
+ encoding: 'utf-8',
240
+ stdio: 'pipe',
241
+ }).trim()
242
+ return reconstructCommits(output)
243
+ }
244
+
245
+ function getTagDate(tag) {
246
+ try {
247
+ return execSync(`git log -1 --format=%cs ${tag}`, { encoding: 'utf-8', stdio: 'pipe' }).trim()
248
+ } catch {
249
+ return new Date().toISOString().split('T')[0]
250
+ }
251
+ }
252
+
253
+ function escapeRegex(value) {
254
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
255
+ }
256
+
257
+ module.exports = {
258
+ getCommitsSinceLastTag,
259
+ reconstructCommits,
260
+ parseCommit,
261
+ getAllVersionTags,
262
+ getCommitsBetween,
263
+ getTagDate,
264
+ escapeRegex,
265
+ }
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: commit
3
+ description: Stage and commit the current change with a concise conventional-commit message. Stages only files related to the task, runs typecheck and the relevant tests first, and appends a Release-Note: footer when the change is user-visible (grammar in .claude/rules/commit-messages.md). Use when the user says "/commit", "commit this", or wants their working changes committed.
4
+ ---
5
+
6
+ # /commit — stage and commit the current change
7
+
8
+ A disciplined commit: stage only what belongs to the task, prove it's green,
9
+ then write a conventional-commit message — with a `Release-Note:` footer when an
10
+ end user would notice the change. Message grammar and length limits live in
11
+ `.claude/rules/commit-messages.md`.
12
+
13
+ 1. Run `git status` and `git diff --staged`.
14
+ 2. Stage ONLY files related to the current task (ignore unrelated UI/config
15
+ drift).
16
+ 3. Run typecheck and the relevant tests.
17
+ 4. Write a concise conventional commit message scoped to the change.
18
+ 5. **Decide if the change is user-visible.** If an end user would notice it
19
+ (feature, fix, improvement), append a `Release-Note:` footer in plain user
20
+ language — what they can now do, not the implementation. Use `Release-Note!:`
21
+ for a release headline, and `Release-Area:` to override the area when the
22
+ scope isn't a user area. Omit the footer for internal/dev-only changes
23
+ (`chore`, `test`, `docs`, refactors with no user effect). Put a blank line
24
+ before the footer. See `.claude/rules/commit-messages.md` → "Release notes
25
+ footer" for the grammar. When the release tooling is installed, these footers
26
+ are what the generated release notes are built from at `npm version` — so the
27
+ note is the user-facing record of the change, not just metadata.
28
+ 6. Do NOT ask about unrelated uncommitted files.
@@ -0,0 +1,176 @@
1
+ ---
2
+ name: spec
3
+ description: Create a new spec-driven-development spec. Grills the user to a clear, shared understanding of the requirement AND the proposed solution FIRST, then writes one concise, phased, test-included, change-logged spec into specs/backlog/. Use when the user wants to plan a feature, write a spec, capture a requirement, or says "/spec" or "spec this out".
4
+ ---
5
+
6
+ # /spec — author a new spec
7
+
8
+ Produce ONE concise spec in `specs/backlog/`. Do not start coding — this skill
9
+ plans only. Implementation happens later via `/spec-go`.
10
+
11
+ Lifecycle (the governing skills) — status in parentheses:
12
+ `/spec` (Draft, backlog) → `/spec-ready` (Ready, still backlog) → `/spec-go`
13
+ (In Progress, in-progress; implement phase 1) → `/spec-complete` (Complete) /
14
+ `/spec-cancel` (Cancelled). See `.claude/rules/spec-planning.md`.
15
+
16
+ ## Phase A — reach a clear shared understanding (grill first)
17
+
18
+ Interview the user until requirement AND proposed solution are unambiguous. Do
19
+ not write the spec until this is resolved.
20
+
21
+ - Break the problem into **distinctive areas** and work them in logical order,
22
+ resolving dependencies between decisions one at a time.
23
+ - Ask **one question at a time**. For each, give your **recommended answer**.
24
+ - If a question can be answered by **reading the codebase, read it** instead of
25
+ asking. Verify endpoints/models/files actually exist before relying on them.
26
+ - Cover, at minimum, the areas that apply:
27
+ 1. **Problem & why** — what's broken/missing, who feels it, why now.
28
+ 2. **Scope & non-goals** — explicit out-of-scope items.
29
+ 3. **Affected areas** — concrete files/modules/packages this touches.
30
+ 4. **Proposed solution shape** — the chosen approach and the alternatives
31
+ rejected, with the reason (this becomes "Decisions").
32
+ 5. **Data / API impact** — schema/model changes, new endpoints, and
33
+ **backward compatibility** (additive = safe; breaking = needs explicit
34
+ permission and coordination).
35
+ 6. **Security & multi-tenancy** — authz, tenant scoping, untrusted input.
36
+ 7. **Edge cases & failure modes.**
37
+ 8. **Testing approach** — what proves each phase correct.
38
+ 9. **Open questions** — anything still undecided.
39
+
40
+ Stop grilling when there are no unresolved branches that would change the spec.
41
+ Briefly play back the agreed understanding before writing.
42
+
43
+ ## Phase B — write the spec
44
+
45
+ This skill is for **features**. For bugs, use `/spec-bug` (test-first, red→green).
46
+
47
+ - **Every spec is a folder** — never a bare file, even for a one-line change:
48
+ `specs/backlog/feat-<kebab-name>/`. Create it with `mkdir -p`.
49
+ - The entry point is **always `00-overview.md`** — the index/dashboard for the
50
+ spec. It holds the header block, Problem, Decisions, Solution overview, the
51
+ **phase index** (a table linking to each phase file), Open questions, State
52
+ log, and Changelog. It does **not** hold the per-phase task lists.
53
+ - **Each phase is its own file** — `01-<phase-slug>.md`, `02-<phase-slug>.md`, …
54
+ numbered in execution order; the slug is a short kebab description of the phase
55
+ goal (e.g. `01-data-model.md`, `02-api-endpoints.md`). The phase file holds
56
+ that phase's goal, its task checkboxes (tests included), and any phase-specific
57
+ notes. **Even a single-phase spec gets `01-….md`** — never lump phase tasks
58
+ into `00-overview.md`. This keeps each phase easy to dive into on its own.
59
+ - Choose a short kebab-case name and **prefix it `feat-`** (the bug counterpart
60
+ uses `bug-`).
61
+
62
+ Use this template (keep it **as concise as possible** — no filler, no restating
63
+ the codebase, link rather than duplicate):
64
+
65
+ ```markdown
66
+ # <Feature title>
67
+
68
+ > **Type:** Feature
69
+ > **Status:** Draft — not started
70
+ > **Author:** <git user.name — `git config user.name`>
71
+ > **Developer:** —
72
+ > **Raised:** <YYYY-MM-DD (today)>
73
+ > **Area:** <comma-separated files/modules this touches>
74
+
75
+ ## Problem
76
+
77
+ <2–6 sentences: what's wrong/missing and why it matters. No fluff.>
78
+
79
+ ## Decisions
80
+
81
+ <Numbered, confirmed decisions from Phase A. Each: the choice + one-line why,
82
+ and the rejected alternative when it sharpens the choice. This is the heart of
83
+ the spec — be specific.>
84
+
85
+ ## Solution overview
86
+
87
+ <Short prose or bullets describing the chosen shape end-to-end. Optional small
88
+ schema/grammar/output snippets where they remove ambiguity.>
89
+
90
+ ## Phases
91
+
92
+ Each phase lives in its own file in this folder. Status: ⬜ not started ·
93
+ 🔄 in progress · ✅ done.
94
+
95
+ | # | Phase | Status | File |
96
+ |---|-------|--------|------|
97
+ | 1 | <goal> | ⬜ | [01-<phase-slug>.md](01-<phase-slug>.md) |
98
+ | 2 | <goal> | ⬜ | [02-<phase-slug>.md](02-<phase-slug>.md) |
99
+
100
+ ## Open questions
101
+
102
+ - [ ] <anything deferred — or "None">
103
+
104
+ ## State log
105
+
106
+ | Date | Status | Folder | By |
107
+ |------|--------|--------|----|
108
+ | <YYYY-MM-DD> | Draft | backlog | <author> |
109
+
110
+ ## Changelog
111
+
112
+ - <YYYY-MM-DD> — Spec created.
113
+ ```
114
+
115
+ Then create **one file per phase** (`01-<phase-slug>.md`, `02-…`, in execution
116
+ order). Each phase file uses this template:
117
+
118
+ ```markdown
119
+ # Phase 1 — <goal> ⬜
120
+
121
+ > Spec: [00-overview.md](00-overview.md) · **Status:** Not started
122
+
123
+ **Goal:** <one line — what this phase delivers and how it's proven>.
124
+
125
+ ## Tasks
126
+
127
+ - [ ] <clear, verb-first task>
128
+ - [ ] <clear, verb-first task>
129
+ - [ ] Add/extend tests covering this phase; run the project's typecheck and
130
+ test commands (see `.claude/rules/spec-planning.md`) — green before the
131
+ phase is done.
132
+
133
+ ## Notes
134
+
135
+ <Phase-specific decisions, gotchas, or context. Delete if empty.>
136
+ ```
137
+
138
+ Keep the `00-overview.md` phase index and the phase files in sync: the index row
139
+ is the one-line summary + status; the phase file is the detail.
140
+
141
+ The **State log** is the audit trail of folder/status transitions — every
142
+ lifecycle skill (`/spec-ready`, `/spec-go`, `/spec-complete`, `/spec-cancel`)
143
+ appends one row when it moves the spec. The **Changelog** is for decisions and
144
+ course-corrections only — keep the two separate.
145
+
146
+ Rules for the spec body:
147
+
148
+ - **Every phase is independently shippable and ends with tests.** A phase is
149
+ not "done" until its tests are written and the suite is green. Bake a test
150
+ task into each phase — never a separate "testing phase" at the end only.
151
+ - **Tasks are checkboxes** (`- [ ]`), clear, verb-first, and granular enough to
152
+ finish in one session. They live in the **phase files**, not the overview. Use
153
+ `⬜`/`🔄`/`✅` on each phase-file heading and mirror it in the `00-overview.md`
154
+ phase index.
155
+ - **Honour project conventions** when writing tasks — reference the relevant
156
+ `.claude/rules/*.md` rather than re-explaining them.
157
+ - **Changelog** is mandatory and lives in the spec. Every later decision or
158
+ course-correction gets a dated one-line entry. Convert relative dates to
159
+ absolute.
160
+ - Keep it tight. If a section adds no information, delete it.
161
+
162
+ ## Phase C — index the spec
163
+
164
+ Prepend a row to `specs/backlog/00-index.md` (newest first — directly under the table
165
+ header row, above existing rows):
166
+
167
+ ```
168
+ | <YYYY-MM-DD> | <feat-name> | Feature | Draft |
169
+ ```
170
+
171
+ This is the live view of the backlog; `/spec-go` / `/spec-cancel` remove the row
172
+ when the spec leaves. Create `00-index.md` from a header if it's somehow missing
173
+ (`/spec-init` normally ensures it).
174
+
175
+ After writing, tell the user the path and that it's a `Draft` in `backlog`. Next
176
+ step is `/spec-ready` once it's groomed, then `/spec-go` to start building.