@skitterbyte/skitterspec 1.0.1 → 2.0.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/README.md +27 -244
- package/assets/claude-md-section.md +0 -6
- package/assets/core/env.config.json.example +5 -1
- package/assets/core/env.config.md +21 -5
- package/assets/rules/spec-planning.md +14 -10
- package/assets/skills/spec/SKILL.md +11 -38
- package/assets/skills/spec-complete/SKILL.md +31 -4
- package/assets/skills/spec-env/SKILL.md +6 -0
- package/assets/skills/spec-env-down/SKILL.md +16 -8
- package/assets/skills/spec-go/SKILL.md +15 -17
- package/package.json +6 -11
- package/src/cli.js +186 -318
- package/src/deprecate.js +138 -0
- package/src/env/config.js +17 -4
- package/src/env/integrate.js +46 -0
- package/src/env/resolve.js +70 -52
- package/src/env/teardown.js +19 -4
- package/src/env/trust.js +87 -0
- package/src/init.js +78 -170
- package/src/prompts.js +26 -63
- package/LICENSE +0 -21
- package/assets/core/linear.config.json.example +0 -39
- package/assets/core/linear.config.md +0 -121
- package/assets/rules/commit-messages.md +0 -85
- package/assets/scripts/generate-changelog.js +0 -274
- package/assets/scripts/generate-releases.js +0 -360
- package/assets/scripts/lib/config.js +0 -127
- package/assets/scripts/lib/git-commits.js +0 -265
- package/assets/skills/commit/SKILL.md +0 -28
- package/assets/skills/spec-pull/SKILL.md +0 -46
- package/assets/skills/spec-push/SKILL.md +0 -53
- package/assets/skills/spec-status/SKILL.md +0 -46
- package/src/config.js +0 -13
- package/src/sync/apply.js +0 -66
- package/src/sync/base.js +0 -83
- package/src/sync/compare.js +0 -99
- package/src/sync/config.js +0 -198
- package/src/sync/mcp.js +0 -112
- package/src/sync/normalize.js +0 -249
- package/src/sync/pull.js +0 -84
- package/src/sync/push.js +0 -106
- 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 {
|
|
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
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 =
|
|
44
|
+
const RULES = listRules()
|
|
37
45
|
|
|
38
46
|
const SPEC_FOLDERS = ['.core', 'backlog', 'in-progress', 'complete', 'cancelled']
|
|
39
47
|
|
|
40
|
-
// Opt-in
|
|
41
|
-
//
|
|
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,
|
|
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
|
-
* `
|
|
10
|
-
*
|
|
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({
|
|
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: '
|
|
27
|
-
message: '
|
|
28
|
-
initial:
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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/`.
|