@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/env/render.js
CHANGED
|
@@ -3,24 +3,15 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Pure renderers for per-spec isolation artifacts.
|
|
5
5
|
*
|
|
6
|
-
* `renderEnvFile` produces the worktree's `.env` body
|
|
7
|
-
* writes
|
|
8
|
-
* `open.command` template. No side effects — unit-testable in isolation.
|
|
6
|
+
* `renderEnvFile` produces the worktree's `.env` body — the only file the engine
|
|
7
|
+
* writes. No side effects — unit-testable in isolation.
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
|
-
const { expandTokens } = require('./resolve.js')
|
|
12
|
-
|
|
13
10
|
// The worktree `.env`: COMPOSE_PROJECT_NAME namespaces the Docker stack and its
|
|
14
11
|
// named volumes; PORT_OFFSET shifts the spec's reserved port block.
|
|
15
12
|
function renderEnvFile({ projectName, portOffset }) {
|
|
16
13
|
return `COMPOSE_PROJECT_NAME=${projectName}\nPORT_OFFSET=${portOffset}\n`
|
|
17
14
|
}
|
|
18
15
|
|
|
19
|
-
// Expand the opener template with the provided tokens. An empty/whitespace-only
|
|
20
|
-
// template means "no auto-open" → returns null.
|
|
21
|
-
function expandOpenCommand(template, tokens) {
|
|
22
|
-
if (typeof template !== 'string' || !template.trim()) return null
|
|
23
|
-
return expandTokens(template, tokens)
|
|
24
|
-
}
|
|
25
16
|
|
|
26
|
-
module.exports = { renderEnvFile
|
|
17
|
+
module.exports = { renderEnvFile }
|
package/src/env/resolve.js
CHANGED
|
@@ -20,12 +20,19 @@ const BUCKETS = ['backlog', 'in-progress', 'complete', 'cancelled']
|
|
|
20
20
|
|
|
21
21
|
// Find the spec folder under specs/<bucket>/<name>. `specArg` may be a bare
|
|
22
22
|
// folder name or a path — only its basename is matched against the buckets.
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
23
|
+
//
|
|
24
|
+
// Search order is `preferDirs`, then `dir`, then `extraDirs`:
|
|
25
|
+
// - `preferDirs` is the checkout the CALLER is standing in. A spec's bucket
|
|
26
|
+
// and its `Stack:` / `Base version:` headers are properties of the branch,
|
|
27
|
+
// not of the repo — `/spec-start` moves a spec to `in-progress` on the
|
|
28
|
+
// spec's own branch, so the primary checkout keeps showing `backlog`.
|
|
29
|
+
// - `dir` is the primary checkout, and stays the fallback (and the sole
|
|
30
|
+
// source of repo identity — see `resolveSpec`).
|
|
31
|
+
// - `extraDirs` lets a caller (e.g. `spec-env integrate`) reach a spec that
|
|
32
|
+
// was authored on a branch and never committed to the primary checkout.
|
|
33
|
+
function findSpecFolder(specArg, dir, extraDirs = [], preferDirs = []) {
|
|
27
34
|
const name = path.basename(specArg)
|
|
28
|
-
for (const root of [dir, ...extraDirs]) {
|
|
35
|
+
for (const root of [...preferDirs, dir, ...extraDirs]) {
|
|
29
36
|
for (const bucket of BUCKETS) {
|
|
30
37
|
const abs = path.join(root, 'specs', bucket, name)
|
|
31
38
|
if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) {
|
|
@@ -36,6 +43,209 @@ function findSpecFolder(specArg, dir, extraDirs = []) {
|
|
|
36
43
|
return null
|
|
37
44
|
}
|
|
38
45
|
|
|
46
|
+
/**
|
|
47
|
+
* A spec's phases, as the review page needs them. Never throws.
|
|
48
|
+
*
|
|
49
|
+
* WHAT IT ANSWERS: is there a phase left for `/spec-next` to build? That is the
|
|
50
|
+
* question a `Commit & Continue` button needs, and it is simply `done < total` —
|
|
51
|
+
* `/spec-next` acts on the FIRST UNFINISHED phase, so a spec sitting mid-way
|
|
52
|
+
* through its last phase still has one to build, while a spec whose phases are
|
|
53
|
+
* all done has none whether it was finished a minute ago or a month ago.
|
|
54
|
+
*
|
|
55
|
+
* THREE OUTCOMES. `null` means cannot tell — a legacy bare `<name>.md`, an
|
|
56
|
+
* overview carrying its phases inline, a folder this cannot see. The caller
|
|
57
|
+
* must route that to leaving things as they are: an absence is not evidence
|
|
58
|
+
* that a spec has no phases (`.claude/rules/negative-checks.md` rules 1 and 4).
|
|
59
|
+
*
|
|
60
|
+
* WHAT WOULD FOOL THIS: a spec whose phase files exist but whose statuses were
|
|
61
|
+
* never updated reads as unfinished, so the button stays offered — the harmless
|
|
62
|
+
* direction. The opposite error, reading a live spec as finished, would take a
|
|
63
|
+
* file claiming Done that is not, which is a lie in the repo rather than a gap
|
|
64
|
+
* in this reader.
|
|
65
|
+
*/
|
|
66
|
+
function readPhases(specDir, { overviewFile = '00-overview.md' } = {}) {
|
|
67
|
+
let entries
|
|
68
|
+
try {
|
|
69
|
+
entries = fs.readdirSync(specDir, { withFileTypes: true })
|
|
70
|
+
} catch {
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
const files = entries
|
|
74
|
+
.filter((e) => e.isFile() && /^\d\d-.+\.md$/.test(e.name) && e.name !== overviewFile)
|
|
75
|
+
.map((e) => e.name)
|
|
76
|
+
.sort()
|
|
77
|
+
// No phase files is not "no phases" — it is a legacy layout whose phases live
|
|
78
|
+
// inline in the overview, and this reader cannot see them.
|
|
79
|
+
if (!files.length) return null
|
|
80
|
+
|
|
81
|
+
let done = 0
|
|
82
|
+
for (const name of files) {
|
|
83
|
+
let text
|
|
84
|
+
try {
|
|
85
|
+
text = fs.readFileSync(path.join(specDir, name), 'utf8')
|
|
86
|
+
} catch {
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
if (phaseIsDone(text)) done++
|
|
90
|
+
}
|
|
91
|
+
return { total: files.length, done, hasNextPhase: done < files.length, live: livePhase(specDir, files) }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The phase this diff is about — its number, title, goal and tasks.
|
|
96
|
+
*
|
|
97
|
+
* WHICH PHASE. The one in progress, else the last one done. A goal shown beside
|
|
98
|
+
* a diff that predates it is worse than no goal at all, and those two are the
|
|
99
|
+
* only phases a diff can plausibly be about: work in flight, or work just
|
|
100
|
+
* finished and not yet committed.
|
|
101
|
+
*
|
|
102
|
+
* `null` throughout rather than a half-filled object — a phase file this cannot
|
|
103
|
+
* parse is a phase with nothing to say, and the page omits the section rather
|
|
104
|
+
* than rendering an empty one.
|
|
105
|
+
*/
|
|
106
|
+
function livePhase(specDir, files) {
|
|
107
|
+
let inProgress = null
|
|
108
|
+
let lastDone = null
|
|
109
|
+
for (const name of files) {
|
|
110
|
+
let text
|
|
111
|
+
try {
|
|
112
|
+
text = fs.readFileSync(path.join(specDir, name), 'utf8')
|
|
113
|
+
} catch {
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
const parsed = parsePhase(text, name)
|
|
117
|
+
if (!parsed) continue
|
|
118
|
+
if (phaseIsDone(text)) lastDone = parsed
|
|
119
|
+
else if (phaseIsStarted(text)) inProgress = inProgress || parsed
|
|
120
|
+
}
|
|
121
|
+
return inProgress || lastDone
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Is this phase under way? The status line wins, for `phaseIsDone`'s reason. */
|
|
125
|
+
function phaseIsStarted(text) {
|
|
126
|
+
const status = /^>.*\*\*Status:\*\*\s*(.+)$/m.exec(text)
|
|
127
|
+
if (status) return /^in progress\b/i.test(status[1].trim())
|
|
128
|
+
return /^#\s.*🔄\s*$/m.test(text)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* One phase file's readable parts. Pure, and tolerant: every field but `n` is
|
|
133
|
+
* optional, because a phase file someone wrote by hand is still a phase file.
|
|
134
|
+
*/
|
|
135
|
+
function parsePhase(text, name) {
|
|
136
|
+
const num = /^(\d\d)-/.exec(name)
|
|
137
|
+
if (!num) return null
|
|
138
|
+
const heading = /^#\s+(.+?)\s*$/m.exec(text)
|
|
139
|
+
// `# Phase 1 — The engine reads the phase index ✅` → the title between the
|
|
140
|
+
// dash and the status emoji, which is the half a reader wants.
|
|
141
|
+
let title = heading ? heading[1] : ''
|
|
142
|
+
title = title.replace(/^Phase\s+\d+\s*[—–-]\s*/i, '').replace(/\s*[⬜🔄✅]\s*$/u, '').trim()
|
|
143
|
+
const goal = /^\*\*Goal:\*\*\s*([\s\S]*?)(?:\n\n|\n##)/m.exec(text)
|
|
144
|
+
// LINE BY LINE, not one regex. A task wraps across lines with the
|
|
145
|
+
// continuation indented, and an `$` under `/m` matches at every line end — so
|
|
146
|
+
// the obvious regex silently truncates every wrapped task at its first line.
|
|
147
|
+
// It looked right on the short ones, which is why this is done the long way.
|
|
148
|
+
const tasks = []
|
|
149
|
+
for (const line of text.split('\n')) {
|
|
150
|
+
const start = /^- \[([ xX])\]\s*(.*)$/.exec(line)
|
|
151
|
+
if (start) {
|
|
152
|
+
tasks.push({ done: start[1].toLowerCase() === 'x', text: start[2].trim() })
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
// An indented non-empty line continues the task above it. Anything else —
|
|
156
|
+
// a blank line, a heading, an unindented paragraph — ends the list.
|
|
157
|
+
if (!tasks.length) continue
|
|
158
|
+
if (/^\s+\S/.test(line)) tasks[tasks.length - 1].text += ' ' + line.trim()
|
|
159
|
+
else if (line.trim()) break
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
n: Number(num[1]),
|
|
163
|
+
title,
|
|
164
|
+
goal: goal ? goal[1].replace(/\s+/g, ' ').trim() : null,
|
|
165
|
+
tasks,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The spec's own `## Problem` and `## Impact`, for the page's header.
|
|
171
|
+
*
|
|
172
|
+
* WHAT WOULD FOOL THIS: a spec that renames those headings, or a legacy bare
|
|
173
|
+
* `<name>.md` with no overview at all. Both yield `null`, which the caller
|
|
174
|
+
* routes to omitting the header — the page rendered without one yesterday and
|
|
175
|
+
* still does (`.claude/rules/negative-checks.md` rule 4).
|
|
176
|
+
*/
|
|
177
|
+
function readOverview(specDir, { overviewFile = '00-overview.md' } = {}) {
|
|
178
|
+
let text
|
|
179
|
+
try {
|
|
180
|
+
text = fs.readFileSync(path.join(specDir, overviewFile), 'utf8')
|
|
181
|
+
} catch {
|
|
182
|
+
return null
|
|
183
|
+
}
|
|
184
|
+
const problem = sectionOf(text, 'Problem') || sectionOf(text, 'Symptom')
|
|
185
|
+
const impact = impactRows(sectionOf(text, 'Impact'))
|
|
186
|
+
if (!problem && !impact) return null
|
|
187
|
+
return {
|
|
188
|
+
...(problem ? { problem } : {}),
|
|
189
|
+
...(impact ? { impact } : {}),
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The body under one `## Heading`, up to the next one. Trimmed, or null.
|
|
195
|
+
*
|
|
196
|
+
* SLICED, not lookahead-matched. The obvious regex ends with
|
|
197
|
+
* `(?=^##\s|\Z)` — and JS has no `\Z`, so that alternative is a literal
|
|
198
|
+
* `Z` and every section at the END of a file returns nothing. It passed on the
|
|
199
|
+
* spec used to write it, whose Problem happened to be followed by another
|
|
200
|
+
* heading, and failed on a bug spec's Symptom and on any Impact table written
|
|
201
|
+
* last.
|
|
202
|
+
*/
|
|
203
|
+
function sectionOf(text, heading) {
|
|
204
|
+
const open = new RegExp(`^##\\s+${heading}\\s*$`, 'm').exec(text)
|
|
205
|
+
if (!open) return null
|
|
206
|
+
const from = open.index + open[0].length
|
|
207
|
+
const next = /^##\s/m.exec(text.slice(from))
|
|
208
|
+
const body = (next ? text.slice(from, from + next.index) : text.slice(from)).trim()
|
|
209
|
+
return body || null
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The Impact table's rows. The heading is always present in a spec, and the
|
|
214
|
+
* table is not — a spec touching no external surface writes a one-line sentence
|
|
215
|
+
* instead, which is a real answer and is returned as prose.
|
|
216
|
+
*/
|
|
217
|
+
function impactRows(section) {
|
|
218
|
+
if (!section) return null
|
|
219
|
+
const rows = []
|
|
220
|
+
for (const line of section.split('\n')) {
|
|
221
|
+
if (!line.trim().startsWith('|')) continue
|
|
222
|
+
const cells = line.split('|').slice(1, -1).map((c) => c.trim())
|
|
223
|
+
if (cells.length < 3) continue
|
|
224
|
+
// The header and its `|---|` separator, dropped by shape rather than by
|
|
225
|
+
// position — a spec that omits either still yields its rows.
|
|
226
|
+
if (/^-{2,}$/.test(cells[0].replace(/:/g, ''))) continue
|
|
227
|
+
if (/^surface$/i.test(cells[0]) && /^change$/i.test(cells[1])) continue
|
|
228
|
+
rows.push({ surface: cells[0], change: cells[1], detail: cells[2] })
|
|
229
|
+
}
|
|
230
|
+
if (rows.length) return { rows }
|
|
231
|
+
const prose = section.split('\n').map((l) => l.trim()).filter(Boolean).join(' ').replace(/^_|_$/g, '')
|
|
232
|
+
return prose ? { prose } : null
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Is one phase file finished? Pure.
|
|
237
|
+
*
|
|
238
|
+
* THE STATUS LINE WINS over the heading's emoji. Both are written by the
|
|
239
|
+
* lifecycle skills and both are kept in step, but a hand edit that fixes one
|
|
240
|
+
* and forgets the other far more often leaves a stale emoji than stale prose —
|
|
241
|
+
* and the emoji is the half a reader's eye skips.
|
|
242
|
+
*/
|
|
243
|
+
function phaseIsDone(text) {
|
|
244
|
+
const status = /^>.*\*\*Status:\*\*\s*(.+)$/m.exec(text)
|
|
245
|
+
if (status) return /^done\b/i.test(status[1].trim())
|
|
246
|
+
return /^#\s.*✅\s*$/m.test(text)
|
|
247
|
+
}
|
|
248
|
+
|
|
39
249
|
// Split a `feat-`/`bug-`/`hotfix-` prefix. Unknown prefix → type defaults to
|
|
40
250
|
// `feat` and the whole folder name is the slug.
|
|
41
251
|
function splitPrefix(folder) {
|
|
@@ -212,18 +422,28 @@ function assertPrimaryOnMain(config, git) {
|
|
|
212
422
|
* Resolve a spec argument to its identity + isolation coordinates.
|
|
213
423
|
* Throws a clear Error when the spec folder can't be found.
|
|
214
424
|
*
|
|
425
|
+
* `opts.preferDirs` are checkout roots searched BEFORE `dir` — the checkout the
|
|
426
|
+
* caller is standing in, so the spec's bucket and headers come from the branch
|
|
427
|
+
* they are on rather than from the base branch's stale copy.
|
|
215
428
|
* `opts.searchDirs` adds fallback checkout roots to look under (after `dir`) when
|
|
216
|
-
* locating the spec folder
|
|
217
|
-
*
|
|
429
|
+
* locating the spec folder.
|
|
430
|
+
*
|
|
431
|
+
* **Identity/coordinate tokens still expand against `dir`** (the primary
|
|
432
|
+
* checkout) in every case — `{repo}`, the worktree path, the docker project name
|
|
433
|
+
* and the registry are repo-level facts that must be identical from anywhere.
|
|
434
|
+
* That is `bug-spec-env-cwd-anchor`'s fix and it is deliberately untouched here;
|
|
435
|
+
* only which *file* is read moves.
|
|
218
436
|
*/
|
|
219
437
|
function resolveSpec(specArg, dir, config, opts = {}) {
|
|
220
438
|
const searchDirs = opts.searchDirs || []
|
|
221
|
-
const
|
|
439
|
+
const preferDirs = opts.preferDirs || []
|
|
440
|
+
const found = findSpecFolder(specArg, dir, searchDirs, preferDirs)
|
|
222
441
|
if (!found) {
|
|
223
442
|
// Name the roots we looked under: the usual cause is a spec that only exists
|
|
224
443
|
// on its own branch, and the message should say where we didn't find it.
|
|
225
444
|
throw new Error(
|
|
226
|
-
`spec not found under specs/**: ${specArg}
|
|
445
|
+
`spec not found under specs/**: ${specArg} ` +
|
|
446
|
+
`(searched: ${[...preferDirs, dir, ...searchDirs].join(', ')})`,
|
|
227
447
|
)
|
|
228
448
|
}
|
|
229
449
|
|
|
@@ -255,9 +475,70 @@ function resolveSpec(specArg, dir, config, opts = {}) {
|
|
|
255
475
|
}
|
|
256
476
|
}
|
|
257
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Every worktree git knows about, as absolute paths — the primary checkout
|
|
480
|
+
* included, since `git worktree list` reports it as one.
|
|
481
|
+
*
|
|
482
|
+
* `git` is an injected reader, as everywhere else in this file: the caller owns
|
|
483
|
+
* the child-process boundary, which is what keeps this module testable without
|
|
484
|
+
* one.
|
|
485
|
+
*/
|
|
486
|
+
function liveWorktreePaths(git) {
|
|
487
|
+
const out = git(['worktree', 'list', '--porcelain'])
|
|
488
|
+
const paths = new Set()
|
|
489
|
+
if (out == null) return paths
|
|
490
|
+
for (const line of out.split('\n')) {
|
|
491
|
+
if (line.startsWith('worktree ')) {
|
|
492
|
+
paths.add(path.resolve(line.slice('worktree '.length).trim()))
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return paths
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// An in-progress spec lives on its *worktree branch*, not the primary checkout,
|
|
499
|
+
// so we must scan the worktrees too — otherwise a live spec's DB looks orphaned.
|
|
500
|
+
function collectSpecFolders(roots) {
|
|
501
|
+
const folders = new Set()
|
|
502
|
+
for (const root of roots) {
|
|
503
|
+
for (const bucket of BUCKETS) {
|
|
504
|
+
let entries
|
|
505
|
+
try {
|
|
506
|
+
entries = fs.readdirSync(path.join(root, 'specs', bucket), { withFileTypes: true })
|
|
507
|
+
} catch {
|
|
508
|
+
continue
|
|
509
|
+
}
|
|
510
|
+
for (const entry of entries) if (entry.isDirectory()) folders.add(entry.name)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return folders
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Resolve every spec folder (found in the primary checkout OR any worktree) to
|
|
518
|
+
* `{ folder, slug, worktreePath }`. `searchDirs` lets `resolveSpec` locate a
|
|
519
|
+
* spec that was authored on its branch and never committed to the primary
|
|
520
|
+
* checkout.
|
|
521
|
+
*/
|
|
522
|
+
function allSpecs(dir, config, worktreePaths) {
|
|
523
|
+
const searchDirs = [...worktreePaths]
|
|
524
|
+
const specs = []
|
|
525
|
+
for (const folder of collectSpecFolders([dir, ...searchDirs])) {
|
|
526
|
+
try {
|
|
527
|
+
const spec = resolveSpec(folder, dir, config, { searchDirs })
|
|
528
|
+
specs.push({ folder: spec.folder, slug: spec.slug, worktreePath: spec.worktreePath })
|
|
529
|
+
} catch {
|
|
530
|
+
// Unresolvable folder (not a real spec) — skip.
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return specs
|
|
534
|
+
}
|
|
535
|
+
|
|
258
536
|
module.exports = {
|
|
259
537
|
BUCKETS,
|
|
260
538
|
resolveSpec,
|
|
539
|
+
liveWorktreePaths,
|
|
540
|
+
collectSpecFolders,
|
|
541
|
+
allSpecs,
|
|
261
542
|
resolveBaseBranch,
|
|
262
543
|
resolvePrimaryCheckout,
|
|
263
544
|
currentBranch,
|
|
@@ -267,6 +548,12 @@ module.exports = {
|
|
|
267
548
|
repoInfo,
|
|
268
549
|
expandTokens,
|
|
269
550
|
findSpecFolder,
|
|
551
|
+
readPhases,
|
|
552
|
+
phaseIsDone,
|
|
553
|
+
phaseIsStarted,
|
|
554
|
+
parsePhase,
|
|
555
|
+
readOverview,
|
|
556
|
+
readFrontmatterField,
|
|
270
557
|
readStackField,
|
|
271
558
|
readBaseVersionField,
|
|
272
559
|
}
|