@ikunin/sprintpilot 1.0.5 → 2.0.5

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 (35) hide show
  1. package/README.md +48 -1
  2. package/_Sprintpilot/Sprintpilot.md +14 -1
  3. package/_Sprintpilot/manifest.yaml +1 -1
  4. package/_Sprintpilot/modules/autopilot/config.yaml +22 -0
  5. package/_Sprintpilot/modules/autopilot/profiles/_base.yaml +45 -0
  6. package/_Sprintpilot/modules/autopilot/profiles/large.yaml +22 -0
  7. package/_Sprintpilot/modules/autopilot/profiles/legacy.yaml +35 -0
  8. package/_Sprintpilot/modules/autopilot/profiles/medium.yaml +5 -0
  9. package/_Sprintpilot/modules/autopilot/profiles/nano.yaml +35 -0
  10. package/_Sprintpilot/modules/autopilot/profiles/small.yaml +5 -0
  11. package/_Sprintpilot/modules/git/config.yaml +8 -0
  12. package/_Sprintpilot/modules/ma/config.yaml +42 -0
  13. package/_Sprintpilot/scripts/agent-adapter.js +247 -0
  14. package/_Sprintpilot/scripts/cached-read.js +238 -0
  15. package/_Sprintpilot/scripts/check-prereqs.js +139 -0
  16. package/_Sprintpilot/scripts/dispatch-layer.js +192 -0
  17. package/_Sprintpilot/scripts/git-portable.js +219 -0
  18. package/_Sprintpilot/scripts/infer-dependencies.js +594 -0
  19. package/_Sprintpilot/scripts/inject-tasks-section.js +279 -0
  20. package/_Sprintpilot/scripts/list-remaining-stories.js +295 -0
  21. package/_Sprintpilot/scripts/log-timing.js +425 -0
  22. package/_Sprintpilot/scripts/mark-done-stories-tasks.js +254 -0
  23. package/_Sprintpilot/scripts/merge-shards.js +339 -0
  24. package/_Sprintpilot/scripts/preflight-merge.js +235 -0
  25. package/_Sprintpilot/scripts/resolve-dag.js +559 -0
  26. package/_Sprintpilot/scripts/resolve-profile.js +355 -0
  27. package/_Sprintpilot/scripts/state-shard.js +602 -0
  28. package/_Sprintpilot/scripts/submodule-lock.js +130 -0
  29. package/_Sprintpilot/scripts/summarize-timings.js +362 -0
  30. package/_Sprintpilot/scripts/sync-status.js +13 -0
  31. package/_Sprintpilot/scripts/with-retry.js +145 -0
  32. package/_Sprintpilot/skills/sprint-autopilot-on/workflow.md +572 -42
  33. package/bin/sprintpilot.js +4 -0
  34. package/lib/commands/install.js +157 -1
  35. package/package.json +1 -1
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+
3
+ // cached-read.js — TTL + mtime-aware file cache for the autopilot loop.
4
+ //
5
+ // Usage:
6
+ // cached-read.js read --file <path> [--ttl <ms>] [--cache-root <path>]
7
+ // cached-read.js invalidate --file <path> [--cache-root <path>]
8
+ // cached-read.js clear [--cache-root <path>]
9
+ // cached-read.js stats [--cache-root <path>]
10
+ //
11
+ // Rationale (PR 8 / M5):
12
+ // workflow.md re-reads sprint-status.yaml, git-status.yaml, and
13
+ // decision-log.yaml at many step boundaries. A single loop iteration
14
+ // can read each one 5+ times. This helper memoizes the reads to a
15
+ // per-project cache directory, respecting TTL AND source-file mtime
16
+ // so a write always invalidates the cache even if the caller forgets
17
+ // to call `invalidate` explicitly.
18
+ //
19
+ // Cache layout:
20
+ // <cache-root>/.cache/cached-reads/<sha256(file)>.json
21
+ // { source, mtime_ms, cached_at, body }
22
+ //
23
+ // Consumer gate:
24
+ // Callers should gate use of this script on `autopilot.cache_shared_reads`
25
+ // via resolve-profile.js. When the flag is false, read the file directly.
26
+
27
+ const fs = require('node:fs');
28
+ const path = require('node:path');
29
+ const crypto = require('node:crypto');
30
+
31
+ const { parseArgs } = require('../lib/runtime/args');
32
+ const log = require('../lib/runtime/log');
33
+
34
+ const DEFAULT_TTL_MS = 60_000;
35
+ const VALID_ACTIONS = ['read', 'invalidate', 'clear', 'stats'];
36
+
37
+ function help() {
38
+ log.out(
39
+ [
40
+ 'Usage:',
41
+ ' cached-read.js read --file <path> [--ttl <ms>] [--cache-root <path>]',
42
+ ' cached-read.js invalidate --file <path> [--cache-root <path>]',
43
+ ' cached-read.js clear [--cache-root <path>]',
44
+ ' cached-read.js stats [--cache-root <path>]',
45
+ '',
46
+ `Default TTL: ${DEFAULT_TTL_MS}ms. Source-file mtime always invalidates.`,
47
+ ].join('\n'),
48
+ );
49
+ }
50
+
51
+ function cacheDir(cacheRoot) {
52
+ return path.join(cacheRoot, '.cache', 'cached-reads');
53
+ }
54
+
55
+ function keyFor(filePath) {
56
+ return crypto.createHash('sha256').update(path.resolve(filePath)).digest('hex').slice(0, 32);
57
+ }
58
+
59
+ function cacheEntryPath(cacheRoot, filePath) {
60
+ return path.join(cacheDir(cacheRoot), `${keyFor(filePath)}.json`);
61
+ }
62
+
63
+ function readFileStat(filePath) {
64
+ try {
65
+ const stat = fs.statSync(filePath);
66
+ return { exists: true, mtime_ms: stat.mtimeMs };
67
+ } catch {
68
+ return { exists: false, mtime_ms: 0 };
69
+ }
70
+ }
71
+
72
+ function readFromCache(cacheRoot, filePath, ttlMs) {
73
+ const entryFile = cacheEntryPath(cacheRoot, filePath);
74
+ if (!fs.existsSync(entryFile)) return { hit: false, reason: 'miss' };
75
+ let entry;
76
+ try {
77
+ entry = JSON.parse(fs.readFileSync(entryFile, 'utf8'));
78
+ } catch {
79
+ return { hit: false, reason: 'corrupt' };
80
+ }
81
+ const now = Date.now();
82
+ // ttlMs=0 means "always miss" (bypass); >= (not >) ensures that.
83
+ if (now - entry.cached_at >= ttlMs) return { hit: false, reason: 'ttl-expired' };
84
+ const srcStat = readFileStat(filePath);
85
+ if (!srcStat.exists) return { hit: false, reason: 'source-gone' };
86
+ if (srcStat.mtime_ms > entry.mtime_ms) return { hit: false, reason: 'source-newer' };
87
+ return { hit: true, body: entry.body, entry };
88
+ }
89
+
90
+ function writeToCache(cacheRoot, filePath, body) {
91
+ const dir = cacheDir(cacheRoot);
92
+ fs.mkdirSync(dir, { recursive: true });
93
+ const srcStat = readFileStat(filePath);
94
+ const entry = {
95
+ source: path.resolve(filePath),
96
+ mtime_ms: srcStat.mtime_ms,
97
+ cached_at: Date.now(),
98
+ body,
99
+ };
100
+ const file = cacheEntryPath(cacheRoot, filePath);
101
+ const tmp = `${file}.tmp.${process.pid}.${process.hrtime.bigint().toString(36)}`;
102
+ fs.writeFileSync(tmp, JSON.stringify(entry));
103
+ fs.renameSync(tmp, file);
104
+ return file;
105
+ }
106
+
107
+ function readThrough(cacheRoot, filePath, ttlMs) {
108
+ const hit = readFromCache(cacheRoot, filePath, ttlMs);
109
+ if (hit.hit) return { body: hit.body, hit: true, source: filePath };
110
+ // Cache miss — read through.
111
+ if (!fs.existsSync(filePath)) {
112
+ return { body: null, hit: false, source: filePath, reason: hit.reason || 'missing' };
113
+ }
114
+ const body = fs.readFileSync(filePath, 'utf8');
115
+ writeToCache(cacheRoot, filePath, body);
116
+ return { body, hit: false, source: filePath, reason: hit.reason };
117
+ }
118
+
119
+ function invalidate(cacheRoot, filePath) {
120
+ const file = cacheEntryPath(cacheRoot, filePath);
121
+ try {
122
+ fs.unlinkSync(file);
123
+ return { cleared: true };
124
+ } catch {
125
+ return { cleared: false };
126
+ }
127
+ }
128
+
129
+ function clearAll(cacheRoot) {
130
+ const dir = cacheDir(cacheRoot);
131
+ if (!fs.existsSync(dir)) return { cleared: 0 };
132
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
133
+ let cleared = 0;
134
+ for (const f of files) {
135
+ try {
136
+ fs.unlinkSync(path.join(dir, f));
137
+ cleared++;
138
+ } catch {
139
+ /* best effort */
140
+ }
141
+ }
142
+ return { cleared };
143
+ }
144
+
145
+ function stats(cacheRoot) {
146
+ const dir = cacheDir(cacheRoot);
147
+ if (!fs.existsSync(dir)) return { entries: 0, oldest_age_ms: null, newest_age_ms: null };
148
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
149
+ const now = Date.now();
150
+ let oldest = Infinity;
151
+ let newest = 0;
152
+ for (const f of files) {
153
+ try {
154
+ const entry = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
155
+ const age = now - entry.cached_at;
156
+ if (age < oldest) oldest = age;
157
+ if (age > newest) newest = age;
158
+ } catch {
159
+ /* skip corrupt */
160
+ }
161
+ }
162
+ return {
163
+ entries: files.length,
164
+ oldest_age_ms: files.length ? oldest : null,
165
+ newest_age_ms: files.length ? newest : null,
166
+ };
167
+ }
168
+
169
+ function main() {
170
+ const { opts, positional } = parseArgs(process.argv.slice(2));
171
+ if (opts.help || positional.length === 0) {
172
+ help();
173
+ process.exit(opts.help ? 0 : 1);
174
+ }
175
+ const action = positional[0];
176
+ if (!VALID_ACTIONS.includes(action)) {
177
+ log.error(`unknown action '${action}'. Valid: ${VALID_ACTIONS.join(', ')}`);
178
+ process.exit(1);
179
+ }
180
+ const cacheRoot = opts['cache-root'] || process.cwd();
181
+ const filePath = opts.file;
182
+ const ttlMs = opts.ttl !== undefined ? Number.parseInt(String(opts.ttl), 10) : DEFAULT_TTL_MS;
183
+ if (Number.isNaN(ttlMs) || ttlMs < 0) {
184
+ log.error(`invalid --ttl '${opts.ttl}': must be a non-negative integer (ms)`);
185
+ process.exit(1);
186
+ }
187
+
188
+ if (action === 'read') {
189
+ if (!filePath) {
190
+ log.error('--file is required for read');
191
+ process.exit(1);
192
+ }
193
+ const out = readThrough(cacheRoot, filePath, ttlMs);
194
+ if (out.body === null) {
195
+ log.error(`source missing: ${filePath}`);
196
+ process.exit(2);
197
+ }
198
+ process.stdout.write(out.body);
199
+ return;
200
+ }
201
+ if (action === 'invalidate') {
202
+ if (!filePath) {
203
+ log.error('--file is required for invalidate');
204
+ process.exit(1);
205
+ }
206
+ const r = invalidate(cacheRoot, filePath);
207
+ process.stdout.write(`${JSON.stringify(r)}\n`);
208
+ return;
209
+ }
210
+ if (action === 'clear') {
211
+ const r = clearAll(cacheRoot);
212
+ process.stdout.write(`${JSON.stringify(r)}\n`);
213
+ return;
214
+ }
215
+ if (action === 'stats') {
216
+ const r = stats(cacheRoot);
217
+ process.stdout.write(`${JSON.stringify(r)}\n`);
218
+ }
219
+ }
220
+
221
+ module.exports = {
222
+ DEFAULT_TTL_MS,
223
+ VALID_ACTIONS,
224
+ cacheDir,
225
+ keyFor,
226
+ cacheEntryPath,
227
+ readFileStat,
228
+ readFromCache,
229
+ writeToCache,
230
+ readThrough,
231
+ invalidate,
232
+ clearAll,
233
+ stats,
234
+ };
235
+
236
+ if (require.main === module) {
237
+ main();
238
+ }
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+
3
+ // check-prereqs.js — verify Sprintpilot v2's environment prerequisites.
4
+ //
5
+ // Exit codes:
6
+ // 0 — all prereqs met (may include a warning for degraded mode)
7
+ // 1 — hard failure (user must resolve before continuing)
8
+ //
9
+ // Checks:
10
+ // - Node >= 18 (package.json engines)
11
+ // - Git >= 2.18 required for submodule --jobs / --reference (PR 10).
12
+ // Git 2.5.0–2.17 works in degraded mode (no submodule speedup) —
13
+ // emits a warning on stderr but exits 0.
14
+
15
+ const { execFileSync } = require('node:child_process');
16
+ const { parseArgs } = require('../lib/runtime/args');
17
+ const log = require('../lib/runtime/log');
18
+
19
+ const MIN_NODE = [18, 0, 0];
20
+ const MIN_GIT_STRICT = [2, 18, 0]; // submodule --jobs / --reference
21
+ const MIN_GIT_SOFT = [2, 5, 0]; // worktree basics
22
+
23
+ function help() {
24
+ log.out('Usage: check-prereqs.js [--min-git <semver>]');
25
+ }
26
+
27
+ function parseSemver(str) {
28
+ // Accept "2.18.0", "2.18", "git version 2.39.3 (Apple Git-145)", etc.
29
+ const m = String(str).match(/(\d+)\.(\d+)(?:\.(\d+))?/);
30
+ if (!m) return null;
31
+ return [
32
+ Number.parseInt(m[1], 10),
33
+ Number.parseInt(m[2], 10),
34
+ m[3] ? Number.parseInt(m[3], 10) : 0,
35
+ ];
36
+ }
37
+
38
+ function cmp(a, b) {
39
+ for (let i = 0; i < 3; i++) {
40
+ if (a[i] > b[i]) return 1;
41
+ if (a[i] < b[i]) return -1;
42
+ }
43
+ return 0;
44
+ }
45
+
46
+ function fmt(v) {
47
+ return `${v[0]}.${v[1]}.${v[2]}`;
48
+ }
49
+
50
+ function checkNode() {
51
+ const v = parseSemver(process.version);
52
+ if (!v) {
53
+ log.error(`unable to parse node version '${process.version}'`);
54
+ return { ok: false };
55
+ }
56
+ if (cmp(v, MIN_NODE) < 0) {
57
+ log.error(`node ${fmt(v)} is too old; need >= ${fmt(MIN_NODE)}. Upgrade node.`);
58
+ return { ok: false };
59
+ }
60
+ return { ok: true, version: fmt(v) };
61
+ }
62
+
63
+ function readGitVersion() {
64
+ // execFileSync (not exec) — no shell, no injection surface.
65
+ try {
66
+ const out = execFileSync('git', ['--version'], {
67
+ stdio: ['ignore', 'pipe', 'pipe'],
68
+ encoding: 'utf8',
69
+ }).trim();
70
+ return out;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ function checkGit(minStrictArg) {
77
+ const raw = readGitVersion();
78
+ if (!raw) {
79
+ log.error('git not found on PATH. Install git >= 2.18 (or 2.5 for degraded mode).');
80
+ return { ok: false };
81
+ }
82
+ const v = parseSemver(raw);
83
+ if (!v) {
84
+ log.warn(`unable to parse git version string: ${raw}`);
85
+ return { ok: true, version: raw, degraded: true };
86
+ }
87
+ const minStrict = minStrictArg ? parseSemver(minStrictArg) : MIN_GIT_STRICT;
88
+ if (cmp(v, minStrict) >= 0) {
89
+ return { ok: true, version: fmt(v), degraded: false };
90
+ }
91
+ if (cmp(v, MIN_GIT_SOFT) >= 0) {
92
+ log.warn(
93
+ `git ${fmt(v)} is below recommended ${fmt(minStrict)}. Degraded mode: submodule speedups disabled (PR 10 features).`,
94
+ );
95
+ return { ok: true, version: fmt(v), degraded: true };
96
+ }
97
+ log.error(
98
+ `git ${fmt(v)} is too old; need >= ${fmt(MIN_GIT_SOFT)} minimum (${fmt(minStrict)} recommended).`,
99
+ );
100
+ return { ok: false, version: fmt(v) };
101
+ }
102
+
103
+ function main() {
104
+ const { opts } = parseArgs(process.argv.slice(2));
105
+ if (opts.help) {
106
+ help();
107
+ process.exit(0);
108
+ }
109
+
110
+ const node = checkNode();
111
+ if (!node.ok) process.exit(1);
112
+
113
+ const git = checkGit(opts['min-git']);
114
+ if (!git.ok) process.exit(1);
115
+
116
+ // Summary on stdout for scripting consumers.
117
+ const summary = {
118
+ node: node.version,
119
+ git: git.version,
120
+ git_degraded: git.degraded || false,
121
+ };
122
+ process.stdout.write(JSON.stringify(summary) + '\n');
123
+ process.exit(0);
124
+ }
125
+
126
+ module.exports = {
127
+ checkNode,
128
+ checkGit,
129
+ parseSemver,
130
+ cmp,
131
+ fmt,
132
+ MIN_NODE,
133
+ MIN_GIT_STRICT,
134
+ MIN_GIT_SOFT,
135
+ };
136
+
137
+ if (require.main === module) {
138
+ main();
139
+ }
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env node
2
+
3
+ // dispatch-layer.js — orchestrator for parallel intra-epic story execution.
4
+ //
5
+ // Usage:
6
+ // dispatch-layer.js --layer <key,key,...> [--max-parallel <n>]
7
+ // [--project-root <path>] [--branch-prefix <str>]
8
+ // [--base-branch <br>] [--dry-run]
9
+ //
10
+ // Responsibilities:
11
+ // 1. For each story in --layer (respecting --max-parallel concurrency),
12
+ // create the story's worktree and branch. Worktree creation itself
13
+ // happens synchronously (cheap after PR 10); actual sub-agent spawn
14
+ // is delegated to the host agent via a plan file the host reads.
15
+ // 2. Emit a plan.json to the project at
16
+ // _bmad-output/implementation-artifacts/.layer-plan.json
17
+ // that the host workflow then consumes — invoking N sub-agents, one
18
+ // per story, pointing each at its worktree + branch.
19
+ // 3. When the host reports back (all sub-agents complete), the top-level
20
+ // workflow invokes `merge-shards.js --archive --layer <id>` to merge
21
+ // each story's state shard into the authoritative project YAML.
22
+ //
23
+ // This script itself does NOT call an LLM. Host-specific multi-agent
24
+ // dispatch is up to workflow.md (gated on agent-adapter.js's confidence).
25
+
26
+ const fs = require('node:fs');
27
+ const path = require('node:path');
28
+ const { spawnSync } = require('node:child_process');
29
+
30
+ const { parseArgs } = require('../lib/runtime/args');
31
+ const log = require('../lib/runtime/log');
32
+
33
+ const STORY_RE = /^[a-z0-9][a-z0-9-]*$/;
34
+ const PLAN_FILENAME = '.layer-plan.json';
35
+
36
+ function help() {
37
+ log.out(
38
+ [
39
+ 'Usage:',
40
+ ' dispatch-layer.js --layer <key,key,...> [options]',
41
+ '',
42
+ 'Options:',
43
+ ' --max-parallel N Upper bound on concurrent sub-agents (default 2).',
44
+ ' --project-root P Defaults to cwd.',
45
+ ' --branch-prefix S Branch name prefix (default story/).',
46
+ ' --base-branch B Branch point (default main).',
47
+ ' --dry-run Compute the plan but do not create worktrees.',
48
+ ].join('\n'),
49
+ );
50
+ }
51
+
52
+ function parseLayer(raw) {
53
+ if (!raw) return { ok: false, error: '--layer is required' };
54
+ const keys = String(raw).split(',').map((s) => s.trim()).filter(Boolean);
55
+ for (const k of keys) {
56
+ if (!STORY_RE.test(k)) {
57
+ return { ok: false, error: `invalid story key '${k}': must match ${STORY_RE}` };
58
+ }
59
+ }
60
+ if (keys.length === 0) {
61
+ return { ok: false, error: '--layer must contain at least one story key' };
62
+ }
63
+ return { ok: true, value: keys };
64
+ }
65
+
66
+ function planLayer({ keys, maxParallel, projectRoot, branchPrefix, baseBranch }) {
67
+ const effectiveParallel = Math.max(1, Math.min(maxParallel | 0, keys.length));
68
+ const worktrees = keys.map((key) => ({
69
+ story: key,
70
+ worktree: path.join(projectRoot, '.worktrees', key),
71
+ branch: `${branchPrefix}${key}`,
72
+ base_branch: baseBranch,
73
+ }));
74
+ return {
75
+ version: 1,
76
+ created_at: new Date().toISOString(),
77
+ effective_parallel: effectiveParallel,
78
+ max_parallel: maxParallel,
79
+ stories: worktrees,
80
+ };
81
+ }
82
+
83
+ function writePlan(projectRoot, plan) {
84
+ const dir = path.join(projectRoot, '_bmad-output', 'implementation-artifacts');
85
+ fs.mkdirSync(dir, { recursive: true });
86
+ const file = path.join(dir, PLAN_FILENAME);
87
+ const tmp = `${file}.tmp.${process.pid}.${process.hrtime.bigint().toString(36)}`;
88
+ fs.writeFileSync(tmp, JSON.stringify(plan, null, 2));
89
+ fs.renameSync(tmp, file);
90
+ return file;
91
+ }
92
+
93
+ function createWorktree({ projectRoot, worktree, branch, baseBranch }) {
94
+ // Try -b first, fall back to checkout-existing-branch if already present.
95
+ const args = ['worktree', 'add', worktree, '-b', branch];
96
+ if (baseBranch) args.push(baseBranch);
97
+ const first = spawnSync('git', ['-C', projectRoot, ...args], {
98
+ encoding: 'utf8',
99
+ stdio: ['ignore', 'pipe', 'pipe'],
100
+ });
101
+ if (first.status === 0) return { created: true, retried: false, stderr: first.stderr || '' };
102
+ // Retry without -b (branch exists).
103
+ const second = spawnSync(
104
+ 'git',
105
+ ['-C', projectRoot, 'worktree', 'add', worktree, branch],
106
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
107
+ );
108
+ return {
109
+ created: second.status === 0,
110
+ retried: true,
111
+ stderr: (first.stderr || '') + (second.stderr || ''),
112
+ };
113
+ }
114
+
115
+ function dispatch({ keys, maxParallel, projectRoot, branchPrefix, baseBranch, dryRun }) {
116
+ const plan = planLayer({ keys, maxParallel, projectRoot, branchPrefix, baseBranch });
117
+ const results = {
118
+ plan_file: null,
119
+ effective_parallel: plan.effective_parallel,
120
+ stories: [],
121
+ dry_run: !!dryRun,
122
+ };
123
+ if (!dryRun) {
124
+ for (const entry of plan.stories) {
125
+ const out = createWorktree({
126
+ projectRoot,
127
+ worktree: entry.worktree,
128
+ branch: entry.branch,
129
+ baseBranch: entry.base_branch,
130
+ });
131
+ results.stories.push({ story: entry.story, worktree: entry.worktree, branch: entry.branch, ...out });
132
+ }
133
+ results.plan_file = writePlan(projectRoot, plan);
134
+ } else {
135
+ results.stories = plan.stories.map((e) => ({
136
+ story: e.story,
137
+ worktree: e.worktree,
138
+ branch: e.branch,
139
+ created: false,
140
+ retried: false,
141
+ stderr: '(dry-run)',
142
+ }));
143
+ }
144
+ return results;
145
+ }
146
+
147
+ function main() {
148
+ const { opts } = parseArgs(process.argv.slice(2), { booleanFlags: ['dry-run'] });
149
+ if (opts.help) {
150
+ help();
151
+ process.exit(0);
152
+ }
153
+ const layer = parseLayer(opts.layer);
154
+ if (!layer.ok) {
155
+ log.error(layer.error);
156
+ process.exit(1);
157
+ }
158
+ const maxParallel = opts['max-parallel'] !== undefined ? Number.parseInt(String(opts['max-parallel']), 10) : 2;
159
+ if (Number.isNaN(maxParallel) || maxParallel < 1) {
160
+ log.error(`invalid --max-parallel '${opts['max-parallel']}': must be a positive integer`);
161
+ process.exit(1);
162
+ }
163
+ const projectRoot = opts['project-root'] || process.cwd();
164
+ const branchPrefix = opts['branch-prefix'] !== undefined ? String(opts['branch-prefix']) : 'story/';
165
+ const baseBranch = opts['base-branch'] !== undefined ? String(opts['base-branch']) : 'main';
166
+ const dryRun = opts['dry-run'] === true;
167
+
168
+ const result = dispatch({
169
+ keys: layer.value,
170
+ maxParallel,
171
+ projectRoot,
172
+ branchPrefix,
173
+ baseBranch,
174
+ dryRun,
175
+ });
176
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
177
+ const allCreated = dryRun || result.stories.every((s) => s.created);
178
+ process.exit(allCreated ? 0 : 1);
179
+ }
180
+
181
+ module.exports = {
182
+ STORY_RE,
183
+ PLAN_FILENAME,
184
+ parseLayer,
185
+ planLayer,
186
+ writePlan,
187
+ dispatch,
188
+ };
189
+
190
+ if (require.main === module) {
191
+ main();
192
+ }