@skitterbyte/skitterspec 17.0.0 → 19.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.
- package/MIGRATION.md +260 -10
- package/README.md +53 -4
- package/assets/claude-md-section.md +48 -2
- package/assets/commands/spec-connect.md +2 -2
- package/assets/commands/spec-live.md +2 -2
- package/assets/core/env.config.json.example +9 -3
- package/assets/core/env.config.md +102 -30
- package/assets/core/gating.config.json.example +4 -0
- package/assets/core/gating.config.md +81 -0
- package/assets/review/page.html +1501 -0
- package/assets/rules/spec-planning.md +224 -15
- package/assets/rules/spec-reports.md +269 -0
- package/assets/skills/spec/SKILL.md +63 -12
- package/assets/skills/spec-bug/SKILL.md +134 -26
- package/assets/skills/spec-cancel/SKILL.md +85 -6
- package/assets/skills/spec-complete/SKILL.md +109 -20
- package/assets/skills/spec-diff/SKILL.md +564 -0
- package/assets/skills/spec-hotfix/SKILL.md +143 -21
- package/assets/skills/spec-init/SKILL.md +49 -9
- package/assets/skills/spec-next/SKILL.md +289 -7
- package/assets/skills/spec-review/SKILL.md +45 -9
- package/assets/skills/spec-reviewed/SKILL.md +241 -0
- package/assets/skills/spec-start/SKILL.md +323 -66
- package/assets/skills/spec-to-main/SKILL.md +42 -20
- package/package.json +11 -7
- package/src/cli.js +1710 -80
- package/src/env/building.js +143 -0
- package/src/env/classify.js +91 -0
- package/src/env/config.js +57 -9
- package/src/env/provision.js +192 -19
- package/src/env/proxy.js +34 -1
- package/src/env/render.js +3 -12
- package/src/env/resolve.js +296 -9
- package/src/env/review.js +1329 -0
- package/src/env/serve.js +549 -0
- package/src/env/teardown.js +13 -6
- package/src/gating.js +155 -0
- package/src/init.js +124 -2
- package/src/prompts.js +10 -1
- package/LICENSE +0 -21
package/src/gating.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Release gating — the loader, the header reader, and the check.
|
|
5
|
+
*
|
|
6
|
+
* The feature records ONE decision per spec: does this ship behind a feature
|
|
7
|
+
* flag, or land live? Skitterspec never learns how a project does flags; it asks
|
|
8
|
+
* the question, cites the project's own documentation, and reads back what was
|
|
9
|
+
* written. Strictly opt-in: with `specs/.core/gating.config.json` absent, every
|
|
10
|
+
* function here reports "not configured" and nothing else changes.
|
|
11
|
+
*
|
|
12
|
+
* Mirrors `src/env/config.js` (frozen defaults, merge known keys only, never
|
|
13
|
+
* throws on absence) so the two opt-in configs behave alike.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('node:fs')
|
|
17
|
+
const path = require('node:path')
|
|
18
|
+
|
|
19
|
+
const CONFIG_FILE = path.join('specs', '.core', 'gating.config.json')
|
|
20
|
+
|
|
21
|
+
// Only these two buckets are ever read.
|
|
22
|
+
//
|
|
23
|
+
// A SPEC WRITTEN BEFORE GATING WAS ADOPTED HAS NO HEADER AND IS NOT BROKEN. That
|
|
24
|
+
// is the blind spot this check would otherwise walk into: "no Gating: line" is
|
|
25
|
+
// evidence of an unanswered question only for a spec that could have been asked,
|
|
26
|
+
// and every finished or abandoned spec predates the question by definition. They
|
|
27
|
+
// are excluded STRUCTURALLY rather than by a filter someone must remember — a
|
|
28
|
+
// completed spec is not in range, so no future edit can make it fire. Pre-existing
|
|
29
|
+
// specs still in flight ARE reported, deliberately: they are live work, the
|
|
30
|
+
// question genuinely still applies, and the report never blocks anything.
|
|
31
|
+
const ACTIVE_BUCKETS = ['backlog', 'in-progress']
|
|
32
|
+
|
|
33
|
+
const DEFAULT_CONFIG = Object.freeze({
|
|
34
|
+
guidance: '',
|
|
35
|
+
default: 'none: <reason>',
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load `specs/.core/gating.config.json`. Returns `{ config, present }`;
|
|
40
|
+
* `present:false` means the project has not adopted gating, which is read as
|
|
41
|
+
* "this project does not use feature flags" — never as an error.
|
|
42
|
+
*/
|
|
43
|
+
function loadGatingConfig(dir = process.cwd()) {
|
|
44
|
+
const base = { ...DEFAULT_CONFIG }
|
|
45
|
+
let raw
|
|
46
|
+
try {
|
|
47
|
+
raw = fs.readFileSync(path.join(dir, CONFIG_FILE), 'utf-8')
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error.code === 'ENOENT') return { config: base, present: false }
|
|
50
|
+
throw error
|
|
51
|
+
}
|
|
52
|
+
let parsed
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(raw)
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw new Error(`Invalid ${CONFIG_FILE}: ${error.message}`)
|
|
57
|
+
}
|
|
58
|
+
if (parsed && typeof parsed === 'object') {
|
|
59
|
+
if (typeof parsed.guidance === 'string') base.guidance = parsed.guidance.trim()
|
|
60
|
+
if (typeof parsed.default === 'string' && parsed.default.trim()) {
|
|
61
|
+
base.default = parsed.default.trim()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { config: base, present: true }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Read a spec's `> **Gating:** …` blockquote field from `00-overview.md`.
|
|
69
|
+
*
|
|
70
|
+
* Returns `{ raw, kind }` with four kinds, because there are four states and
|
|
71
|
+
* collapsing them is what made the omission invisible in the first place:
|
|
72
|
+
*
|
|
73
|
+
* flag a flag name — ships behind it
|
|
74
|
+
* none `none: <reason>` — deliberately not flagged, and why
|
|
75
|
+
* invalid present but says nothing: empty, or a bare `none` with no reason
|
|
76
|
+
* missing no field at all
|
|
77
|
+
*
|
|
78
|
+
* `invalid` and `missing` are kept apart on purpose. A bare `none` is someone
|
|
79
|
+
* answering without deciding; a missing line is nobody having been asked. They
|
|
80
|
+
* want different words.
|
|
81
|
+
*/
|
|
82
|
+
function readGatingField(specPath) {
|
|
83
|
+
const overview = path.join(specPath, '00-overview.md')
|
|
84
|
+
let raw
|
|
85
|
+
try {
|
|
86
|
+
raw = fs.readFileSync(overview, 'utf-8')
|
|
87
|
+
} catch {
|
|
88
|
+
return { raw: null, kind: 'missing' }
|
|
89
|
+
}
|
|
90
|
+
const m = /^>\s*\*\*Gating:\*\*\s*(.*)$/m.exec(raw)
|
|
91
|
+
if (!m) return { raw: null, kind: 'missing' }
|
|
92
|
+
const value = m[1].trim().replace(/^["'`]|["'`]$/g, '')
|
|
93
|
+
if (!value) return { raw: value, kind: 'invalid' }
|
|
94
|
+
const bare = /^none\b/i.test(value)
|
|
95
|
+
if (bare) {
|
|
96
|
+
// `none` alone, or `none:` with nothing after it, is a shrug rather than a
|
|
97
|
+
// decision — the reason half is the whole point of recording it.
|
|
98
|
+
const reason = value.replace(/^none\b:?/i, '').trim()
|
|
99
|
+
return { raw: value, kind: reason ? 'none' : 'invalid' }
|
|
100
|
+
}
|
|
101
|
+
return { raw: value, kind: 'flag' }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Active specs on disk, as `{ folder, bucket, path }`. A bucket that does not
|
|
105
|
+
// exist is simply empty — git does not store empty directories, so a missing
|
|
106
|
+
// `specs/backlog/` is the ordinary state of a project with nothing queued.
|
|
107
|
+
function activeSpecs(dir) {
|
|
108
|
+
const out = []
|
|
109
|
+
for (const bucket of ACTIVE_BUCKETS) {
|
|
110
|
+
const root = path.join(dir, 'specs', bucket)
|
|
111
|
+
let entries
|
|
112
|
+
try {
|
|
113
|
+
entries = fs.readdirSync(root, { withFileTypes: true })
|
|
114
|
+
} catch {
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
for (const e of entries) {
|
|
118
|
+
if (e.isDirectory()) out.push({ folder: e.name, bucket, path: path.join(root, e.name) })
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return out
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Check specs for a recorded gating decision.
|
|
126
|
+
*
|
|
127
|
+
* Advisory by construction: it returns findings and says nothing about what the
|
|
128
|
+
* caller should do. Nothing here exits, throws on a finding, or blocks.
|
|
129
|
+
*
|
|
130
|
+
* @returns {{configured: boolean, findings: Array<{folder, bucket, kind, raw}>,
|
|
131
|
+
* checked: number, guidance: string}}
|
|
132
|
+
*/
|
|
133
|
+
function checkGating(dir, specs) {
|
|
134
|
+
const { config, present } = loadGatingConfig(dir)
|
|
135
|
+
if (!present) return { configured: false, findings: [], checked: 0, guidance: '' }
|
|
136
|
+
const targets = specs && specs.length ? specs : activeSpecs(dir)
|
|
137
|
+
const findings = []
|
|
138
|
+
for (const spec of targets) {
|
|
139
|
+
const { kind, raw } = readGatingField(spec.path)
|
|
140
|
+
if (kind === 'missing' || kind === 'invalid') {
|
|
141
|
+
findings.push({ folder: spec.folder, bucket: spec.bucket, kind, raw })
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { configured: true, findings, checked: targets.length, guidance: config.guidance }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
CONFIG_FILE,
|
|
149
|
+
DEFAULT_CONFIG,
|
|
150
|
+
ACTIVE_BUCKETS,
|
|
151
|
+
loadGatingConfig,
|
|
152
|
+
readGatingField,
|
|
153
|
+
activeSpecs,
|
|
154
|
+
checkGating,
|
|
155
|
+
}
|
package/src/init.js
CHANGED
|
@@ -153,7 +153,7 @@ function assertComposedAssets() {
|
|
|
153
153
|
const SPEC_MARKER_START = '<!-- skitterspec:start -->'
|
|
154
154
|
const SPEC_MARKER_END = '<!-- skitterspec:end -->'
|
|
155
155
|
|
|
156
|
-
const report = { created: [], updated: [], skipped: [], removed: [], customized: [], healed: [], warnings: [] }
|
|
156
|
+
const report = { created: [], updated: [], skipped: [], refused: [], removed: [], customized: [], healed: [], warnings: [] }
|
|
157
157
|
|
|
158
158
|
function resetReport() {
|
|
159
159
|
for (const k of Object.keys(report)) report[k].length = 0
|
|
@@ -293,6 +293,25 @@ function writeFile(dir, target, content, { force }) {
|
|
|
293
293
|
if (link && link.isSymbolicLink() && !fs.existsSync(target)) {
|
|
294
294
|
fs.unlinkSync(target)
|
|
295
295
|
}
|
|
296
|
+
// A LIVE symlink is the opposite case, and `--force` is what makes it
|
|
297
|
+
// dangerous: `writeFileSync` follows the link, so forcing would write composed
|
|
298
|
+
// content — seam markers resolved, provider text spliced in — straight through
|
|
299
|
+
// it and into whatever it points at. In a checkout that dogfoods its own
|
|
300
|
+
// assets that is `packages/*/assets`, i.e. the SOURCE the link exists to keep
|
|
301
|
+
// live. Refuse: the staleness `--force` was reached for is a smaller problem
|
|
302
|
+
// than corrupting the file it would overwrite.
|
|
303
|
+
//
|
|
304
|
+
// WHAT WOULD MAKE THIS LIE: a HARD link. It has no distinguishing lstat — it
|
|
305
|
+
// simply is the file — so it takes the same corrupting path and nothing here
|
|
306
|
+
// can see it. Out of scope deliberately, and said out loud rather than left to
|
|
307
|
+
// be discovered; nothing in this project's install creates one.
|
|
308
|
+
//
|
|
309
|
+
// It cannot fire in an ordinary consumer install, because nothing there is
|
|
310
|
+
// linked — `skitterspec update` writes copies by design.
|
|
311
|
+
if (force && link && link.isSymbolicLink() && fs.existsSync(target)) {
|
|
312
|
+
report.refused.push(rel(dir, target))
|
|
313
|
+
return
|
|
314
|
+
}
|
|
296
315
|
if (fs.existsSync(target)) {
|
|
297
316
|
if (!force) {
|
|
298
317
|
report.skipped.push(rel(dir, target))
|
|
@@ -444,6 +463,24 @@ function installIsolation(dir, { enabled, workspaceMode }, opts) {
|
|
|
444
463
|
trustWorktreeRoot(dir)
|
|
445
464
|
}
|
|
446
465
|
|
|
466
|
+
// Activate opt-in release gating: write specs/.core/gating.config.json from the
|
|
467
|
+
// example asset, so /spec and friends start asking whether a change ships behind
|
|
468
|
+
// a feature flag and recording the answer.
|
|
469
|
+
//
|
|
470
|
+
// Only called when the operator opts in, and NEVER on `update` — for the same
|
|
471
|
+
// reason as isolation: adopting a policy is a deliberate choice, not something a
|
|
472
|
+
// re-sync flips on. Idempotent; copyAsset never clobbers an existing config
|
|
473
|
+
// without --force, so an operator's edited `guidance` survives a re-init.
|
|
474
|
+
function installGating(dir, { enabled }, opts) {
|
|
475
|
+
if (!enabled) return
|
|
476
|
+
copyAsset(
|
|
477
|
+
dir,
|
|
478
|
+
path.join('core', 'gating.config.json.example'),
|
|
479
|
+
path.join(dir, 'specs', '.core', 'gating.config.json'),
|
|
480
|
+
opts,
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
|
|
447
484
|
// Seed the absolute worktree root into .claude/settings.local.json (gitignored)
|
|
448
485
|
// so the operator enabling isolation isn't prompted on every edit into a
|
|
449
486
|
// freshly-provisioned worktree. Best-effort: an unreadable config or malformed
|
|
@@ -481,6 +518,67 @@ function trustWorktreeRoot(dir) {
|
|
|
481
518
|
}
|
|
482
519
|
}
|
|
483
520
|
|
|
521
|
+
// Is the CLAUDE.md section this project has installed the one we ship?
|
|
522
|
+
//
|
|
523
|
+
// THREE answers, not two. `differs` deliberately does NOT mean "stale": the
|
|
524
|
+
// block is a COPY, so a difference is either an out-of-date copy or the user's
|
|
525
|
+
// own edit, and from here those read identically. Rule 4 of
|
|
526
|
+
// `.claude/rules/negative-checks.md` — route the unknown case to the harmless
|
|
527
|
+
// branch, which here means reporting a difference and naming the fix rather
|
|
528
|
+
// than accusing them of being behind.
|
|
529
|
+
//
|
|
530
|
+
// WHAT WOULD FOOL THIS: absent markers mean the section was never installed, OR
|
|
531
|
+
// that someone stripped it deliberately (`stripClaudeMdSection` exists and is
|
|
532
|
+
// reachable from `reset`). Neither is a fault, so both answer `not installed`
|
|
533
|
+
// and neither is reported as a problem.
|
|
534
|
+
function claudeMdSectionState(dir) {
|
|
535
|
+
const target = path.join(dir, 'CLAUDE.md')
|
|
536
|
+
if (!fs.existsSync(target)) return 'not installed'
|
|
537
|
+
const existing = fs.readFileSync(target, 'utf8')
|
|
538
|
+
if (!existing.includes(SPEC_MARKER_START) || !existing.includes(SPEC_MARKER_END)) {
|
|
539
|
+
return 'not installed'
|
|
540
|
+
}
|
|
541
|
+
const shipped = fs.readFileSync(path.join(ASSETS, 'claude-md-section.md'), 'utf8').trim()
|
|
542
|
+
const start = existing.indexOf(SPEC_MARKER_START) + SPEC_MARKER_START.length
|
|
543
|
+
const installed = existing.slice(start, existing.indexOf(SPEC_MARKER_END)).trim()
|
|
544
|
+
return installed === shipped ? 'fresh' : 'differs'
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// `update --check`: say what `update` would change, write nothing, exit 0.
|
|
548
|
+
// It reports; `update` without the flag stays the only thing that touches a
|
|
549
|
+
// file. This exists because the section is a copy and a copy goes quietly out
|
|
550
|
+
// of date — this repo's own was a whole spec behind the template it ships,
|
|
551
|
+
// through a spec about that template, with every test green.
|
|
552
|
+
function checkSync(dir, { claudeMd = true, log = console.log } = {}) {
|
|
553
|
+
if (!fs.existsSync(dir)) throw new Error(`target dir does not exist: ${dir}`)
|
|
554
|
+
const manifest = readManifest(dir)
|
|
555
|
+
const rows = []
|
|
556
|
+
// Mirror `resyncManagedFile`'s decision exactly rather than re-deriving it:
|
|
557
|
+
// missing → it would create; customized → it would KEEP yours and say so;
|
|
558
|
+
// pristine → it would write only when the shipped content actually differs.
|
|
559
|
+
for (const { relPath, abs, bundled } of managedTargets(dir)) {
|
|
560
|
+
const state = managedState(dir, relPath, manifest, bundled)
|
|
561
|
+
if (state === 'missing') rows.push([relPath, 'missing — would be created'])
|
|
562
|
+
else if (state === 'customized') rows.push([relPath, 'your edit — kept (--force overwrites)'])
|
|
563
|
+
else if (fs.readFileSync(abs, 'utf8') !== bundled) rows.push([relPath, 'out of date — would be updated'])
|
|
564
|
+
}
|
|
565
|
+
const section = claudeMd ? claudeMdSectionState(dir) : 'fresh'
|
|
566
|
+
// A healthy area says NOTHING. A report that lists what is already fine is a
|
|
567
|
+
// report people learn to skim, and then the one line that mattered is missed.
|
|
568
|
+
if (section === 'differs') {
|
|
569
|
+
rows.push([
|
|
570
|
+
'CLAUDE.md (spec workflow section)',
|
|
571
|
+
'differs from the shipped one — `update` would replace it (it is a copy, so this is either your edit or an out-of-date one)',
|
|
572
|
+
])
|
|
573
|
+
}
|
|
574
|
+
if (!rows.length) log('skitterspec update --check: everything is up to date.')
|
|
575
|
+
else {
|
|
576
|
+
log('skitterspec update --check: `skitterspec update` would:')
|
|
577
|
+
for (const [name, why] of rows) log(` ${name} — ${why}`)
|
|
578
|
+
}
|
|
579
|
+
return { rows, section }
|
|
580
|
+
}
|
|
581
|
+
|
|
484
582
|
function installClaudeMd(dir, { mode }) {
|
|
485
583
|
const section = fs.readFileSync(path.join(ASSETS, 'claude-md-section.md'), 'utf8').trim()
|
|
486
584
|
const block = `${SPEC_MARKER_START}\n${section}\n${SPEC_MARKER_END}\n`
|
|
@@ -663,6 +761,15 @@ function printReport(dir, mode, { diff = false } = {}) {
|
|
|
663
761
|
)
|
|
664
762
|
line('manifest repaired', report.healed)
|
|
665
763
|
line('unchanged', report.skipped)
|
|
764
|
+
if (report.refused.length) {
|
|
765
|
+
process.stdout.write('\nrefused (a symlink — writing would overwrite what it points at):\n')
|
|
766
|
+
for (const it of report.refused) process.stdout.write(` ${it}\n`)
|
|
767
|
+
process.stdout.write(
|
|
768
|
+
' These are links into the shipped assets. --force would follow them and\n' +
|
|
769
|
+
' write composed content into the source. Unlink one to take the copy\n' +
|
|
770
|
+
' (rm <path>, then re-run), or leave it linked and edit the asset.\n',
|
|
771
|
+
)
|
|
772
|
+
}
|
|
666
773
|
if (report.warnings.length) {
|
|
667
774
|
process.stdout.write('\nwarnings:\n')
|
|
668
775
|
for (const w of report.warnings) process.stdout.write(` ! ${w}\n`)
|
|
@@ -682,6 +789,13 @@ function printReport(dir, mode, { diff = false } = {}) {
|
|
|
682
789
|
' at /spec-start (Docker is a per-spec escalation — set > **Stack:** in the spec).\n'
|
|
683
790
|
: 'Per-spec isolation is opt-in: re-run with --isolation (or copy' +
|
|
684
791
|
' specs/.core/env.config.json.example → env.config.json) to enable it.\n'
|
|
792
|
+
const gatingOn = fs.existsSync(path.join(dir, 'specs', '.core', 'gating.config.json'))
|
|
793
|
+
const gatingNote = gatingOn
|
|
794
|
+
? 'Release gating is ON: /spec asks whether a change ships behind a feature' +
|
|
795
|
+
' flag and records the answer on the spec (skitterspec gating check reports' +
|
|
796
|
+
' any that have none).\n'
|
|
797
|
+
: 'Release gating is opt-in: re-run with --gating (or copy' +
|
|
798
|
+
' specs/.core/gating.config.json.example → gating.config.json) to enable it.\n'
|
|
685
799
|
// A provider superset ships its own `spec-<provider>-setup` skill; the base
|
|
686
800
|
// ships none. Discovering it from what was actually installed keeps this file
|
|
687
801
|
// tracker-free — it never has to know which tracker (if any) is in the box.
|
|
@@ -707,6 +821,7 @@ function printReport(dir, mode, { diff = false } = {}) {
|
|
|
707
821
|
'Next: tailor .claude/rules/spec-planning.md + the CLAUDE.md section to this' +
|
|
708
822
|
" project's stack, then run /spec.\n" +
|
|
709
823
|
isolationNote +
|
|
824
|
+
gatingNote +
|
|
710
825
|
trackerNote,
|
|
711
826
|
)
|
|
712
827
|
}
|
|
@@ -714,7 +829,7 @@ function printReport(dir, mode, { diff = false } = {}) {
|
|
|
714
829
|
// `mode` here is the INSTALL mode ('init' | 'update'), long-standing and
|
|
715
830
|
// unrelated to the config's own `mode` key — which arrives as `workspaceMode`
|
|
716
831
|
// precisely so the two cannot be confused at a call site.
|
|
717
|
-
async function init({ dir, force, claudeMd, mode, isolation, workspaceMode }) {
|
|
832
|
+
async function init({ dir, force, claudeMd, mode, isolation, workspaceMode, gating }) {
|
|
718
833
|
if (!fs.existsSync(dir)) throw new Error(`target dir does not exist: ${dir}`)
|
|
719
834
|
resetReport()
|
|
720
835
|
|
|
@@ -726,6 +841,7 @@ async function init({ dir, force, claudeMd, mode, isolation, workspaceMode }) {
|
|
|
726
841
|
installCore(dir, { force })
|
|
727
842
|
// Adopting isolation writes the live env.config.json — init only, never update.
|
|
728
843
|
if (mode !== 'update') installIsolation(dir, { enabled: isolation, workspaceMode }, { force })
|
|
844
|
+
if (mode !== 'update') installGating(dir, { enabled: gating }, { force })
|
|
729
845
|
if (claudeMd) installClaudeMd(dir, { mode })
|
|
730
846
|
|
|
731
847
|
// Record what we wrote (and migrate a pre-manifest repo) so a later resync can
|
|
@@ -737,6 +853,10 @@ async function init({ dir, force, claudeMd, mode, isolation, workspaceMode }) {
|
|
|
737
853
|
|
|
738
854
|
module.exports = {
|
|
739
855
|
init,
|
|
856
|
+
// A snapshot of the last run's report, for tests that need to assert on what a
|
|
857
|
+
// run DECIDED rather than only on what it left on disk. Copied, so a caller
|
|
858
|
+
// cannot mutate the live report between phases of a run.
|
|
859
|
+
lastReport: () => JSON.parse(JSON.stringify(report)),
|
|
740
860
|
SKILLS,
|
|
741
861
|
COMMANDS,
|
|
742
862
|
RULES,
|
|
@@ -747,6 +867,8 @@ module.exports = {
|
|
|
747
867
|
writeManifest,
|
|
748
868
|
managedTargets,
|
|
749
869
|
managedState,
|
|
870
|
+
claudeMdSectionState,
|
|
871
|
+
checkSync,
|
|
750
872
|
isExistingSetup,
|
|
751
873
|
resync,
|
|
752
874
|
reset,
|
package/src/prompts.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* is `'worktree'` otherwise (the value the config defaults to anyway).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
async function promptSetup({ isolationSeed = false } = {}) {
|
|
14
|
+
async function promptSetup({ isolationSeed = false, gatingSeed = false } = {}) {
|
|
15
15
|
const prompts = require('prompts')
|
|
16
16
|
|
|
17
17
|
let cancelled = false
|
|
@@ -48,6 +48,14 @@ async function promptSetup({ isolationSeed = false } = {}) {
|
|
|
48
48
|
},
|
|
49
49
|
],
|
|
50
50
|
},
|
|
51
|
+
{
|
|
52
|
+
// Orthogonal to isolation, so it is asked unconditionally rather than
|
|
53
|
+
// nested under it — a project can adopt either, both, or neither.
|
|
54
|
+
type: 'confirm',
|
|
55
|
+
name: 'gating',
|
|
56
|
+
message: 'Record a release-gating decision on each spec — flag, or land live?',
|
|
57
|
+
initial: gatingSeed,
|
|
58
|
+
},
|
|
51
59
|
]
|
|
52
60
|
|
|
53
61
|
const ans = await prompts(questions, { onCancel })
|
|
@@ -59,6 +67,7 @@ async function promptSetup({ isolationSeed = false } = {}) {
|
|
|
59
67
|
return {
|
|
60
68
|
isolation: Boolean(ans.isolation),
|
|
61
69
|
mode: ans.mode === 'checkout' ? 'checkout' : 'worktree',
|
|
70
|
+
gating: Boolean(ans.gating),
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
73
|
|
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.
|