@skitterbyte/skitterspec 1.0.0 → 2.0.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.
Files changed (42) hide show
  1. package/README.md +27 -244
  2. package/assets/claude-md-section.md +0 -6
  3. package/assets/core/env.config.json.example +5 -1
  4. package/assets/core/env.config.md +21 -5
  5. package/assets/rules/spec-planning.md +14 -10
  6. package/assets/skills/spec/SKILL.md +11 -38
  7. package/assets/skills/spec-complete/SKILL.md +31 -4
  8. package/assets/skills/spec-env/SKILL.md +6 -0
  9. package/assets/skills/spec-env-down/SKILL.md +16 -8
  10. package/assets/skills/spec-go/SKILL.md +15 -17
  11. package/package.json +6 -11
  12. package/src/cli.js +174 -318
  13. package/src/deprecate.js +138 -0
  14. package/src/env/config.js +17 -4
  15. package/src/env/integrate.js +46 -0
  16. package/src/env/resolve.js +54 -45
  17. package/src/env/teardown.js +19 -4
  18. package/src/env/trust.js +87 -0
  19. package/src/init.js +78 -170
  20. package/src/prompts.js +26 -63
  21. package/LICENSE +0 -21
  22. package/assets/core/linear.config.json.example +0 -39
  23. package/assets/core/linear.config.md +0 -121
  24. package/assets/rules/commit-messages.md +0 -85
  25. package/assets/scripts/generate-changelog.js +0 -274
  26. package/assets/scripts/generate-releases.js +0 -360
  27. package/assets/scripts/lib/config.js +0 -127
  28. package/assets/scripts/lib/git-commits.js +0 -265
  29. package/assets/skills/commit/SKILL.md +0 -28
  30. package/assets/skills/spec-pull/SKILL.md +0 -46
  31. package/assets/skills/spec-push/SKILL.md +0 -53
  32. package/assets/skills/spec-status/SKILL.md +0 -46
  33. package/src/config.js +0 -13
  34. package/src/sync/apply.js +0 -66
  35. package/src/sync/base.js +0 -83
  36. package/src/sync/compare.js +0 -99
  37. package/src/sync/config.js +0 -198
  38. package/src/sync/mcp.js +0 -112
  39. package/src/sync/normalize.js +0 -249
  40. package/src/sync/pull.js +0 -84
  41. package/src/sync/push.js +0 -106
  42. package/src/sync/write.js +0 -86
package/src/init.js CHANGED
@@ -3,48 +3,51 @@
3
3
  const fs = require('fs')
4
4
  const path = require('path')
5
5
 
6
- const { loadConfig, SCHEMA_VERSION } = require('./config.js')
6
+ const { ensureWorktreeDirTrusted } = require('./env/trust.js')
7
+ const { repoInfo, expandTokens } = require('./env/resolve.js')
7
8
 
8
9
  const ASSETS = path.join(__dirname, '..', 'assets')
9
10
 
10
- // Shipped generator scripts, copied into the consumer's scripts/ when enabled.
11
- const SHARED_LIB = [
12
- path.join('scripts', 'lib', 'git-commits.js'),
13
- path.join('scripts', 'lib', 'config.js'),
14
- ]
15
- const CHANGELOG_SCRIPT = path.join('scripts', 'generate-changelog.js')
16
- const RELEASES_SCRIPT = path.join('scripts', 'generate-releases.js')
17
- const CONFIG_FILE = 'skitterspec.config.json'
18
-
19
- const SKILLS = [
20
- 'spec',
21
- 'spec-bug',
22
- 'spec-ready',
23
- 'spec-review',
24
- 'spec-go',
25
- 'spec-complete',
26
- 'spec-cancel',
27
- 'spec-init',
28
- 'spec-env',
29
- 'spec-env-down',
30
- 'spec-status',
31
- 'spec-pull',
32
- 'spec-push',
33
- 'commit',
34
- ]
11
+ // Skills, rules, and specs/.core templates are discovered from the bundled assets
12
+ // tree rather than hardcoded, so each distribution installs exactly what it ships:
13
+ // the tracker-free base carries the neutral skill set + env.config templates; a
14
+ // provider superset (built by composing its fragments in) additionally carries its
15
+ // sync skills and provider config templates, and they install with no code change.
16
+ function listSkills() {
17
+ const dir = path.join(ASSETS, 'skills')
18
+ return fs
19
+ .readdirSync(dir, { withFileTypes: true })
20
+ .filter((e) => e.isDirectory() && fs.existsSync(path.join(dir, e.name, 'SKILL.md')))
21
+ .map((e) => e.name)
22
+ .sort()
23
+ }
24
+
25
+ function listRules() {
26
+ return fs
27
+ .readdirSync(path.join(ASSETS, 'rules'))
28
+ .filter((f) => f.endsWith('.md'))
29
+ .sort()
30
+ }
31
+
32
+ // Templates scaffolded into specs/.core/ (the *.example configs + their *.md docs).
33
+ // A consumer copies an example → live config to adopt the matching feature.
34
+ function listCoreTemplates() {
35
+ return fs
36
+ .readdirSync(path.join(ASSETS, 'core'))
37
+ .filter((f) => f.endsWith('.example') || f.endsWith('.md'))
38
+ .sort()
39
+ .map((f) => path.join('core', f))
40
+ }
41
+
42
+ const SKILLS = listSkills()
35
43
 
36
- const RULES = ['spec-planning.md', 'commit-messages.md']
44
+ const RULES = listRules()
37
45
 
38
46
  const SPEC_FOLDERS = ['.core', 'backlog', 'in-progress', 'complete', 'cancelled']
39
47
 
40
- // Opt-in per-spec isolation config, scaffolded into specs/.core/ as templates
41
- // the consumer copies (env.config.json.example env.config.json to adopt).
42
- const CORE_FILES = [
43
- path.join('core', 'env.config.json.example'),
44
- path.join('core', 'env.config.md'),
45
- path.join('core', 'linear.config.json.example'),
46
- path.join('core', 'linear.config.md'),
47
- ]
48
+ // Opt-in config templates, scaffolded into specs/.core/ (the base ships the
49
+ // env.config isolation templates; a provider superset also ships its own).
50
+ const CORE_FILES = listCoreTemplates()
48
51
 
49
52
  const SPEC_MARKER_START = '<!-- skitterspec:start -->'
50
53
  const SPEC_MARKER_END = '<!-- skitterspec:end -->'
@@ -171,6 +174,44 @@ function installIsolation(dir, { enabled }, opts) {
171
174
  path.join(dir, 'specs', '.core', 'env.config.json'),
172
175
  opts,
173
176
  )
177
+ trustWorktreeRoot(dir)
178
+ }
179
+
180
+ // Seed the absolute worktree root into .claude/settings.local.json (gitignored)
181
+ // so the operator enabling isolation isn't prompted on every edit into a
182
+ // freshly-provisioned worktree. Best-effort: an unreadable config or malformed
183
+ // settings file is reported, never fatal. `spec-env up` re-ensures this on every
184
+ // provision, so a miss here self-heals.
185
+ function trustWorktreeRoot(dir) {
186
+ let root
187
+ try {
188
+ const cfg = JSON.parse(
189
+ fs.readFileSync(path.join(dir, 'specs', '.core', 'env.config.json'), 'utf8'),
190
+ )
191
+ root = cfg && cfg.worktree && cfg.worktree.root
192
+ } catch {
193
+ /* fall through to the warning below */
194
+ }
195
+ if (!root) {
196
+ report.warnings.push('could not read worktree.root — skipped trusting the worktree dir')
197
+ return
198
+ }
199
+ const { repo, repoSlug } = repoInfo(dir)
200
+ const rootAbs = path.resolve(dir, expandTokens(root, { repo, repoSlug }))
201
+ const res = ensureWorktreeDirTrusted(dir, rootAbs)
202
+ const label = '.claude/settings.local.json (trusted worktree root)'
203
+ if (res.reason === 'malformed') {
204
+ report.warnings.push(
205
+ '.claude/settings.local.json is not valid JSON — did not trust the worktree' +
206
+ ` dir; add ${rootAbs} to permissions.additionalDirectories yourself`,
207
+ )
208
+ } else if (res.reason === 'created') {
209
+ report.created.push(label)
210
+ } else if (res.reason === 'added') {
211
+ report.updated.push(label)
212
+ } else {
213
+ report.skipped.push('.claude/settings.local.json (worktree root already trusted)')
214
+ }
174
215
  }
175
216
 
176
217
  function installClaudeMd(dir, { mode }) {
@@ -212,130 +253,6 @@ function installClaudeMd(dir, { mode }) {
212
253
  report.updated.push('CLAUDE.md (appended spec workflow section)')
213
254
  }
214
255
 
215
- // --- release tooling (changelog / release notes) ---------------------------
216
-
217
- // Build a release-config object from a loaded skitterspec.config.json.
218
- function releaseFromConfig(cfg) {
219
- return {
220
- changelog: { enabled: cfg.changelog.enabled, file: cfg.changelog.file },
221
- releases: {
222
- enabled: cfg.releases.enabled,
223
- file: cfg.releases.file,
224
- productName: cfg.releases.productName,
225
- scopeAreas: cfg.releases.scopeAreas,
226
- },
227
- versionHook: cfg.versionHook,
228
- }
229
- }
230
-
231
- function serializeConfig(release) {
232
- return (
233
- JSON.stringify(
234
- {
235
- version: SCHEMA_VERSION,
236
- changelog: release.changelog,
237
- releases: release.releases,
238
- versionHook: release.versionHook,
239
- },
240
- null,
241
- 2,
242
- ) + '\n'
243
- )
244
- }
245
-
246
- // Write the resolved config. The release object already folds in any existing
247
- // file (the CLI seeds it from loadConfig), so this is a merge, not a clobber —
248
- // safe to persist without --force. Unchanged content is left alone.
249
- function writeConfig(dir, release) {
250
- const target = path.join(dir, CONFIG_FILE)
251
- const content = serializeConfig(release)
252
- const exists = fs.existsSync(target)
253
- if (exists && fs.readFileSync(target, 'utf8') === content) {
254
- report.skipped.push(CONFIG_FILE)
255
- return
256
- }
257
- fs.writeFileSync(target, content)
258
- report[exists ? 'updated' : 'created'].push(CONFIG_FILE)
259
- }
260
-
261
- function installScripts(dir, release, opts) {
262
- if (!release.changelog.enabled && !release.releases.enabled) return
263
- for (const lib of SHARED_LIB) {
264
- copyAsset(dir, lib, path.join(dir, lib), opts)
265
- }
266
- if (release.changelog.enabled) {
267
- copyAsset(dir, CHANGELOG_SCRIPT, path.join(dir, CHANGELOG_SCRIPT), opts)
268
- }
269
- if (release.releases.enabled) {
270
- copyAsset(dir, RELEASES_SCRIPT, path.join(dir, RELEASES_SCRIPT), opts)
271
- }
272
- }
273
-
274
- // Idempotently add the npm scripts that drive generation at `npm version`.
275
- // Never overwrites a user's custom `version` script without --force.
276
- function wireVersionHook(dir, release, { force }) {
277
- const pkgPath = path.join(dir, 'package.json')
278
- if (!fs.existsSync(pkgPath)) {
279
- report.skipped.push('version hook (no package.json)')
280
- return
281
- }
282
-
283
- let pkg
284
- try {
285
- pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
286
- } catch {
287
- report.warnings.push('package.json is not valid JSON — skipped version hook wiring')
288
- return
289
- }
290
-
291
- const genCmds = []
292
- const addFiles = []
293
- if (release.changelog.enabled) {
294
- genCmds.push('node scripts/generate-changelog.js')
295
- addFiles.push(release.changelog.file)
296
- }
297
- if (release.releases.enabled) {
298
- genCmds.push('node scripts/generate-releases.js')
299
- addFiles.push(release.releases.file)
300
- }
301
- if (genCmds.length === 0) return
302
-
303
- const versionCmd = [...genCmds, `git add ${addFiles.join(' ')}`].join(' && ')
304
-
305
- const before = JSON.stringify(pkg)
306
- pkg.scripts = pkg.scripts || {}
307
-
308
- if (pkg.scripts.version && pkg.scripts.version !== versionCmd && !force) {
309
- report.warnings.push(
310
- 'Kept your existing "version" npm script. To regenerate on release, add:\n' +
311
- ` "version": "${versionCmd}" (or re-run with --force)`,
312
- )
313
- } else {
314
- pkg.scripts.version = versionCmd
315
- }
316
-
317
- const helpers = {}
318
- if (release.changelog.enabled) {
319
- helpers.changelog = 'node scripts/generate-changelog.js'
320
- helpers['changelog:retro'] = 'node scripts/generate-changelog.js --retro'
321
- }
322
- if (release.releases.enabled) {
323
- helpers.releases = 'node scripts/generate-releases.js'
324
- helpers['releases:retro'] = 'node scripts/generate-releases.js --retro'
325
- }
326
- for (const [name, cmd] of Object.entries(helpers)) {
327
- if (pkg.scripts[name] && pkg.scripts[name] !== cmd && !force) continue
328
- pkg.scripts[name] = cmd
329
- }
330
-
331
- if (JSON.stringify(pkg) !== before) {
332
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
333
- report.updated.push('package.json (version hook + scripts)')
334
- } else {
335
- report.skipped.push('package.json (version hook already present)')
336
- }
337
- }
338
-
339
256
  function printReport(dir, mode) {
340
257
  const line = (label, items) => {
341
258
  if (!items.length) return
@@ -359,15 +276,14 @@ function printReport(dir, mode) {
359
276
  ' specs/.core/env.config.json.example → env.config.json) to enable it.\n'
360
277
  process.stdout.write(
361
278
  '\nDone. Skills resolve as /spec, /spec-ready, /spec-go, /spec-complete,' +
362
- ' /spec-cancel, /spec-bug, /spec-init, /spec-env, /spec-env-down,' +
363
- ' /spec-status, /spec-pull, /spec-push, /commit.\n' +
279
+ ' /spec-cancel, /spec-bug, /spec-init, /spec-env, /spec-env-down.\n' +
364
280
  'Next: tailor .claude/rules/spec-planning.md + the CLAUDE.md section to this' +
365
281
  " project's stack, then run /spec.\n" +
366
282
  isolationNote,
367
283
  )
368
284
  }
369
285
 
370
- async function init({ dir, force, claudeMd, mode, release, isolation }) {
286
+ async function init({ dir, force, claudeMd, mode, isolation }) {
371
287
  if (!fs.existsSync(dir)) throw new Error(`target dir does not exist: ${dir}`)
372
288
  report.created.length = 0
373
289
  report.updated.length = 0
@@ -384,13 +300,6 @@ async function init({ dir, force, claudeMd, mode, release, isolation }) {
384
300
  if (mode !== 'update') installIsolation(dir, { enabled: isolation }, { force })
385
301
  if (claudeMd) installClaudeMd(dir, { mode })
386
302
 
387
- // Release tooling. The CLI resolves `release` from flags/prompts; when called
388
- // directly (e.g. tests, update) fall back to the on-disk/default config.
389
- const rel = release || releaseFromConfig(loadConfig(dir))
390
- if (mode !== 'update') writeConfig(dir, rel)
391
- installScripts(dir, rel, { force })
392
- if (mode !== 'update' && rel.versionHook) wireVersionHook(dir, rel, { force })
393
-
394
303
  printReport(dir, mode)
395
304
  }
396
305
 
@@ -399,5 +308,4 @@ module.exports = {
399
308
  SKILLS,
400
309
  RULES,
401
310
  SPEC_FOLDERS,
402
- releaseFromConfig,
403
311
  }
package/src/prompts.js CHANGED
@@ -6,12 +6,11 @@
6
6
  * non-interactive path (flags / --yes / CI) never loads this module, so the
7
7
  * test suite never imports the interactive UI.
8
8
  *
9
- * `seed` is the resolved release config (existing file merged with any flags),
10
- * used to pre-fill every answer. `isolationSeed` pre-fills the per-spec isolation
11
- * question. Returns `{ release, isolation }`.
9
+ * `isolationSeed` pre-fills the per-spec isolation question. Returns
10
+ * `{ isolation }`.
12
11
  */
13
12
 
14
- async function promptSetup({ seed, pkgExists, isolationSeed = false }) {
13
+ async function promptSetup({ isolationSeed = false } = {}) {
15
14
  const prompts = require('prompts')
16
15
 
17
16
  let cancelled = false
@@ -23,71 +22,35 @@ async function promptSetup({ seed, pkgExists, isolationSeed = false }) {
23
22
  const questions = [
24
23
  {
25
24
  type: 'confirm',
26
- name: 'changelogEnabled',
27
- message: 'Generate a dev-facing CHANGELOG from commit subjects?',
28
- initial: seed.changelog.enabled,
29
- },
30
- {
31
- type: (prev) => (prev ? 'text' : null),
32
- name: 'changelogFile',
33
- message: 'Changelog filename',
34
- initial: seed.changelog.file,
35
- },
36
- {
37
- type: 'confirm',
38
- name: 'releasesEnabled',
39
- message: 'Generate user-facing release notes from Release-Note: footers?',
40
- initial: seed.releases.enabled,
41
- },
42
- {
43
- type: (_prev, values) => (values.releasesEnabled ? 'text' : null),
44
- name: 'releasesFile',
45
- message: 'Release-notes filename',
46
- initial: seed.releases.file,
47
- },
48
- {
49
- type: (_prev, values) => (values.releasesEnabled ? 'text' : null),
50
- name: 'productName',
51
- message: 'Product name (shown in the release-notes header)',
52
- initial: seed.releases.productName,
25
+ name: 'isolation',
26
+ message: 'Enable per-spec isolation — a git worktree per spec?',
27
+ initial: isolationSeed,
53
28
  },
54
29
  ]
55
30
 
56
- if (pkgExists) {
57
- questions.push({
58
- type: 'confirm',
59
- name: 'versionHook',
60
- message: 'Wire an npm "version" hook to regenerate these on release?',
61
- initial: seed.versionHook,
62
- })
63
- }
64
-
65
- questions.push({
66
- type: 'confirm',
67
- name: 'isolation',
68
- message: 'Enable per-spec isolation — a git worktree per spec?',
69
- initial: isolationSeed,
70
- })
71
-
72
31
  const ans = await prompts(questions, { onCancel })
73
32
  if (cancelled) throw new Error('Setup cancelled')
74
33
 
75
- return {
76
- release: {
77
- changelog: {
78
- enabled: ans.changelogEnabled,
79
- file: ans.changelogFile || seed.changelog.file,
80
- },
81
- releases: {
82
- enabled: ans.releasesEnabled,
83
- file: ans.releasesFile || seed.releases.file,
84
- productName: ans.productName || seed.releases.productName,
85
- scopeAreas: seed.releases.scopeAreas,
86
- },
87
- versionHook: pkgExists ? Boolean(ans.versionHook) : seed.versionHook,
34
+ return { isolation: Boolean(ans.isolation) }
35
+ }
36
+
37
+ /**
38
+ * Interactive confirm for removing leftover release tooling on `update`. Returns
39
+ * true only on an explicit yes; a cancel (Ctrl-C / Esc) resolves to false so the
40
+ * default is always to keep the files.
41
+ */
42
+ async function confirmRemoveReleaseTooling() {
43
+ const prompts = require('prompts')
44
+ const ans = await prompts(
45
+ {
46
+ type: 'confirm',
47
+ name: 'remove',
48
+ message: 'Found release tooling (now in @skitterbyte/skittership). Remove it here?',
49
+ initial: false,
88
50
  },
89
- isolation: Boolean(ans.isolation),
90
- }
51
+ { onCancel: () => false },
52
+ )
53
+ return Boolean(ans.remove)
91
54
  }
92
55
 
93
- module.exports = { promptSetup }
56
+ module.exports = { promptSetup, confirmRemoveReleaseTooling }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Reuben Greaves
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,39 +0,0 @@
1
- {
2
- "linear": {
3
- "teamKey": "",
4
- "teamId": "",
5
- "initiativeId": ""
6
- },
7
- "mapping": {
8
- "specFolder": "project",
9
- "phases": "milestone",
10
- "tasks": "issue"
11
- },
12
- "states": {
13
- "backlog": "Backlog",
14
- "in-progress": "In Progress",
15
- "complete": "Done",
16
- "cancelled": "Cancelled"
17
- },
18
- "snapshot": {
19
- "overviewFile": "00-overview.md"
20
- },
21
- "branch": {
22
- "pattern": "{type}/{slug}"
23
- },
24
- "sync": {
25
- "baseDir": "specs/.core/linear-base",
26
- "backupDir": "specs/.core/linear-backups",
27
- "fieldOwnership": {
28
- "description": "both",
29
- "milestones": "both",
30
- "phaseBodies": "both",
31
- "acceptanceCriteria": "both",
32
- "taskBreakdown": "both",
33
- "workflowState": "pull",
34
- "priority": "pull",
35
- "labels": "pull"
36
- },
37
- "localOnlySections": ["State log", "Changelog", "Open questions"]
38
- }
39
- }
@@ -1,121 +0,0 @@
1
- # `linear.config.json` — Linear hybrid-sync config
2
-
3
- Opt-in config for the git-like Linear sync (`/spec-status`, `/spec-pull`,
4
- `/spec-push`, and the Linear-aware paths of `/spec` and `/spec-go`). Linear owns
5
- **status and discussion**; the repo stays the **co-authoring surface for spec
6
- content**. Sync is bidirectional but git-like: explicit commands, a committed
7
- **base sidecar** for three-way merge, and no blind overwrites.
8
-
9
- **Every Linear step is gated on this file.** While `specs/.core/linear.config.json`
10
- is absent the feature is simply unused — `/spec`, `/spec-go`, and the CLI's
11
- `spec-sync` subcommands behave exactly as they do today (local-only). Adopt it by
12
- copying `linear.config.json.example` → `linear.config.json` here and filling in
13
- your team / initiative IDs.
14
-
15
- The loader (`src/sync/config.js` → `loadLinearConfig`) merges your file over the
16
- frozen defaults below and returns `{ config, present }`; `present:false` means no
17
- live `linear.config.json` was found (the opt-in gate — it never throws on
18
- absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error.
19
-
20
- ## Fields
21
-
22
- ```jsonc
23
- {
24
- // Which Linear team/initiative specs sync into. IDs are read by the Phase 2
25
- // MCP adapter; leave blank until you connect the `linear` MCP server.
26
- "linear": {
27
- "teamKey": "", // human-facing key, e.g. "ENG" (optional)
28
- "teamId": "", // Linear team UUID (create target)
29
- "initiativeId": "" // optional Initiative that groups these specs
30
- },
31
-
32
- // How a spec's parts map onto Linear objects. Defaults mirror Decision 7:
33
- // spec folder → Project, phases → Milestones, tasks → Issues. `phases` may be
34
- // switched to "issue" if your workspace doesn't expose project milestones.
35
- "mapping": {
36
- "specFolder": "project",
37
- "phases": "milestone", // "milestone" | "issue"
38
- "tasks": "issue"
39
- },
40
-
41
- // Map the spec's lifecycle bucket → the Linear workflow-state name. Used when
42
- // translating workflowState across the boundary (Linear owns status → `pull`).
43
- "states": {
44
- "backlog": "Backlog",
45
- "in-progress": "In Progress",
46
- "complete": "Done",
47
- "cancelled": "Cancelled"
48
- },
49
-
50
- // The spec's entry-point file the local snapshot + frontmatter live in.
51
- "snapshot": {
52
- "overviewFile": "00-overview.md"
53
- },
54
-
55
- // Git branch name derived for a linked spec. Tokens: {type}, {slug},
56
- // {identifier} (the Linear issue/project identifier, e.g. ENG-123). Shared
57
- // with the isolation engine's branch derivation (src/env/resolve.js).
58
- "branch": {
59
- "pattern": "{type}/{slug}"
60
- },
61
-
62
- // The three-way merge engine's on-disk state.
63
- "sync": {
64
- // Committed base sidecar dir: the last-synced snapshot per spec, as
65
- // {baseDir}/{identifier}.base.json. Committed so each worktree carries its
66
- // own base and the divergence check stays accurate.
67
- "baseDir": "specs/.core/linear-base",
68
-
69
- // Backup-before-force lands the about-to-be-clobbered side here (the
70
- // reflog). --force never destroys without first writing a copy.
71
- "backupDir": "specs/.core/linear-backups",
72
-
73
- // Per-field sync direction — collapses which fields can ever conflict:
74
- // "both" — co-authored: push + pull, may conflict (both moved off base).
75
- // "pull" — Linear→local only (e.g. status/priority); a local edit never
76
- // pushes and a conflict resolves to remote-wins.
77
- // "push" — local→Linear only; a remote edit never pulls and a conflict
78
- // resolves to local-wins.
79
- // Any field key you add here joins the compared field set; a value outside
80
- // both|pull|push is rejected at load time.
81
- "fieldOwnership": {
82
- "description": "both",
83
- "milestones": "both",
84
- "phaseBodies": "both",
85
- "acceptanceCriteria": "both",
86
- "taskBreakdown": "both",
87
- "workflowState": "pull",
88
- "priority": "pull",
89
- "labels": "pull"
90
- },
91
-
92
- // Markdown sections of 00-overview.md that are local-only scaffolding and
93
- // are stripped from the pushed `description` (never sent to Linear).
94
- "localOnlySections": ["State log", "Changelog", "Open questions"]
95
- }
96
- }
97
- ```
98
-
99
- ## Field ownership & conflicts
100
-
101
- The spec is a set of structured fields, most written by only one side. Marking a
102
- field's owner collapses which fields can genuinely conflict:
103
-
104
- - A `pull` field (Linear owns it) never reports as **pushable** — a stray local
105
- edit is informational and gets reverted on the next pull.
106
- - A `push` field (the repo owns it) never reports as **pullable**.
107
- - Only a `both` field where **both** sides moved off the committed base is a real
108
- `conflict` — `/spec-push` / `/spec-pull` refuse it unless `--force` (which
109
- backs up the losing side into `sync.backupDir` first).
110
-
111
- After any successful pull/push/force the engine **rewrites the base** so the next
112
- three-way compare starts clean.
113
-
114
- ## What to commit
115
-
116
- - **`sync.baseDir`** (default `specs/.core/linear-base/`) — **commit it.** The base
117
- sidecar is the last-synced snapshot the three-way merge compares against; each
118
- worktree carries its own base, so it must travel with the branch.
119
- - **`sync.backupDir`** (default `specs/.core/linear-backups/`) — **gitignore it.**
120
- These are `--force` recovery copies (a local reflog), per-machine and not shared.
121
- Add `specs/.core/linear-backups/` to your `.gitignore`.
@@ -1,85 +0,0 @@
1
- # Commit Messages
2
-
3
- ## Format
4
-
5
- `type(scope): subject` — [Conventional Commits](https://www.conventionalcommits.org/)
6
-
7
- ## Length limits
8
-
9
- - **Subject line:** 50 characters max
10
- - **Body lines:** 72 characters max
11
-
12
- (These match the common commitlint defaults — if your project runs commitlint,
13
- they'll be enforced; otherwise treat them as the convention.)
14
-
15
- ## Template
16
-
17
- ```
18
- type(scope): subject
19
-
20
- - Bullet point 1
21
- - Bullet point 2
22
- ```
23
-
24
- ## Types
25
-
26
- `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `style`
27
-
28
- ## Rules
29
-
30
- - Start bullets with a verb (Add, Fix, Refactor, Remove, Update)
31
- - Be specific about what changed (file, module, feature)
32
- - No emojis, no trailing punctuation
33
- - No **authorship** trailers — `Co-authored-by`, `Signed-off-by`, etc.
34
- (The `Release-Note:` footers below are the one permitted exception — they
35
- carry content, not attribution.)
36
- - Use plain `git commit -m "message"` only
37
- - Output ONLY the commit message — no explanations before or after
38
-
39
- ## Release notes footer (user-facing changes)
40
-
41
- When a change is **user-visible** (a feature, fix, or improvement an end user
42
- would notice), add a `Release-Note:` footer. The terse subject feeds the
43
- dev-facing changelog (`CHANGELOG.md` by default); the footer feeds the
44
- user-facing release notes (`RELEASES.md` by default) via
45
- `scripts/generate-releases.js`, run at `npm version`. Both are generated from
46
- the same commit. Filenames, the product name, and the scope→area map are
47
- configured in `skitterspec.config.json` (this whole section applies only when
48
- the release tooling is installed — see the project README).
49
-
50
- ```
51
- feat(tasks): explicit state/created dates + sort-by
52
-
53
- - Add stateEnteredAt column, sortBy param
54
-
55
- Release-Note: You can now sort your task inbox by when an item entered its
56
- current state or when it was created, with both dates shown on every row.
57
- ```
58
-
59
- Grammar:
60
-
61
- - `Release-Note: <text>` — a plain-English, benefit-framed sentence aimed at
62
- users (not "add column X" — say what they can now do). Multi-line is fine;
63
- wrap continuation lines at 72 like the body.
64
- - `Release-Note!: <text>` — same, but also promoted into the release's
65
- **Highlights** line. Use for the headline change of a release.
66
- - `Release-Area: <name>` — optional. Overrides the scope→area mapping (from
67
- `skitterspec.config.json` → `releases.scopeAreas`) when the dev scope isn't a
68
- user area (e.g. scope `engine` but area `Platform`).
69
- - `Release-Note: none` — explicit "not user-facing" marker (same effect as
70
- omitting it; documents the decision).
71
-
72
- Rules:
73
-
74
- - **Opt-in.** Omit the footer for internal/dev-only changes (`chore`, `test`,
75
- `docs`, `style`, refactors with no user effect, plumbing). Only commits with a
76
- footer appear in `RELEASES.md`.
77
- - Put a **blank line before** the footer (so it's a proper commit footer).
78
- - `feat`→New, `fix`→Fixed, `perf`/`refactor`→Improved, breaking→Action required
79
- — the bucket is derived from the commit type, so just write the note.
80
-
81
- ## Abbreviations
82
-
83
- Common short forms are fine in subjects — e.g. `config`, `ctx`, `impl`, `util`,
84
- `id`, `repo`. List any project-specific abbreviations your team allows in your
85
- own `.claude/rules/`.