@ikunin/sprintpilot 1.0.4 → 2.0.4

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 (37) hide show
  1. package/_Sprintpilot/Sprintpilot.md +14 -1
  2. package/_Sprintpilot/manifest.yaml +1 -1
  3. package/_Sprintpilot/modules/autopilot/config.yaml +22 -0
  4. package/_Sprintpilot/modules/autopilot/profiles/_base.yaml +45 -0
  5. package/_Sprintpilot/modules/autopilot/profiles/large.yaml +22 -0
  6. package/_Sprintpilot/modules/autopilot/profiles/legacy.yaml +35 -0
  7. package/_Sprintpilot/modules/autopilot/profiles/medium.yaml +5 -0
  8. package/_Sprintpilot/modules/autopilot/profiles/nano.yaml +35 -0
  9. package/_Sprintpilot/modules/autopilot/profiles/small.yaml +5 -0
  10. package/_Sprintpilot/modules/git/config.yaml +8 -0
  11. package/_Sprintpilot/modules/ma/config.yaml +42 -0
  12. package/_Sprintpilot/scripts/agent-adapter.js +247 -0
  13. package/_Sprintpilot/scripts/cached-read.js +238 -0
  14. package/_Sprintpilot/scripts/check-prereqs.js +139 -0
  15. package/_Sprintpilot/scripts/dispatch-layer.js +192 -0
  16. package/_Sprintpilot/scripts/git-portable.js +219 -0
  17. package/_Sprintpilot/scripts/infer-dependencies.js +594 -0
  18. package/_Sprintpilot/scripts/inject-tasks-section.js +279 -0
  19. package/_Sprintpilot/scripts/list-remaining-stories.js +295 -0
  20. package/_Sprintpilot/scripts/log-timing.js +360 -0
  21. package/_Sprintpilot/scripts/mark-done-stories-tasks.js +254 -0
  22. package/_Sprintpilot/scripts/merge-shards.js +339 -0
  23. package/_Sprintpilot/scripts/preflight-merge.js +235 -0
  24. package/_Sprintpilot/scripts/resolve-dag.js +559 -0
  25. package/_Sprintpilot/scripts/resolve-profile.js +355 -0
  26. package/_Sprintpilot/scripts/state-shard.js +602 -0
  27. package/_Sprintpilot/scripts/submodule-lock.js +130 -0
  28. package/_Sprintpilot/scripts/summarize-timings.js +362 -0
  29. package/_Sprintpilot/scripts/sync-status.js +13 -0
  30. package/_Sprintpilot/scripts/with-retry.js +145 -0
  31. package/_Sprintpilot/skills/sprint-autopilot-on/workflow.md +673 -540
  32. package/_Sprintpilot/skills/sprintpilot-update/workflow.md +2 -1
  33. package/_Sprintpilot/templates/epic-retrospective.md +24 -0
  34. package/_Sprintpilot/templates/sprint-report.txt +60 -0
  35. package/bin/sprintpilot.js +4 -0
  36. package/lib/commands/install.js +157 -1
  37. package/package.json +1 -1
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env node
2
+
3
+ // inject-tasks-section.js — deterministic fallback for bmad-create-story
4
+ // when it emits a story file without a `## Tasks` / `## Subtasks` section.
5
+ //
6
+ // Usage:
7
+ // inject-tasks-section.js --story-file <path> [--ac-section "Acceptance Criteria"]
8
+ //
9
+ // Behavior (idempotent):
10
+ // 1. If the story file already has a `## Tasks` or `## Subtasks` section
11
+ // with at least one `- [ ]` checkbox, do nothing. Exit 0.
12
+ // 2. Otherwise, locate the `## Acceptance Criteria` section. Extract
13
+ // every AC entry — numbered list item (`1. foo`), bullet (`- foo`),
14
+ // or `**AC-N:** foo` — ONLY within that section (bounded by the next
15
+ // `##` heading or EOF).
16
+ // 3. Append a new `## Tasks / Subtasks` section to the story file with
17
+ // one `- [ ] <AC summary>` bullet per AC entry.
18
+ //
19
+ // Exits:
20
+ // 0 — section already present or appended successfully
21
+ // 1 — story file missing or AC section cannot be located (nothing to do)
22
+ //
23
+ // Why this exists: the previous LLM-prose instruction in workflow.md was
24
+ // under-specified — it told the agent to "read every `N. ...` line" with
25
+ // no section boundary, which regularly scraped lines from Dev Notes or
26
+ // body prose. This script pins the scan exactly to the AC section.
27
+
28
+ const fs = require('node:fs');
29
+ const path = require('node:path');
30
+
31
+ const { parseArgs } = require('../lib/runtime/args');
32
+ const log = require('../lib/runtime/log');
33
+ const { atomicWrite } = require('./mark-done-stories-tasks.js');
34
+ const timing = require('./log-timing.js');
35
+
36
+ // Derive a STORY_RE-compatible story key from a story-file path. BMad
37
+ // emits files like `story-1-1-foo.md` or `1-1-foo.md`; either form
38
+ // reduces to the key `1-1-foo`. Returns null if no key is recoverable.
39
+ function storyKeyFromFile(storyFile) {
40
+ const base = path.basename(storyFile).replace(/\.md$/i, '');
41
+ const stripped = base.replace(/^story-/i, '').toLowerCase();
42
+ if (/^[a-z0-9][a-z0-9-]*$/.test(stripped)) return stripped;
43
+ return null;
44
+ }
45
+
46
+ function emitTimingEvent(projectRoot, story, phase, meta) {
47
+ try {
48
+ if (!story) return;
49
+ if (!timing.isEnabled(projectRoot)) return;
50
+ timing.appendLine(projectRoot, story, timing.buildEntry('once', story, phase, meta));
51
+ } catch {
52
+ /* ignore — timing is best-effort */
53
+ }
54
+ }
55
+
56
+ function help() {
57
+ log.out(
58
+ [
59
+ 'Usage:',
60
+ ' inject-tasks-section.js --story-file <path> [--ac-section "Acceptance Criteria"]',
61
+ '',
62
+ 'Ensures the story file has a `## Tasks / Subtasks` section with at',
63
+ 'least one `- [ ]` checkbox. If the section is missing, appends one',
64
+ 'derived 1:1 from the `## Acceptance Criteria` section. Idempotent.',
65
+ ].join('\n'),
66
+ );
67
+ }
68
+
69
+ // Locate `## Tasks` or `## Subtasks` (case-insensitive, any depth >= 2)
70
+ // and check whether the section contains at least one `- [ ]` checkbox.
71
+ // Returns { found: boolean, hasCheckbox: boolean }.
72
+ //
73
+ // Fence-aware: lines inside ``` or ~~~ fenced blocks are treated as
74
+ // content (not headers, not checkboxes). Without this, an example code
75
+ // block elsewhere in the file containing `## Tasks` or `- [ ]` would
76
+ // give a false positive — same fix mark-done-stories-tasks.js makes.
77
+ function inspectTasksSection(body) {
78
+ const lines = body.split('\n');
79
+ const headerRe = /^(#{2,})\s+(tasks|subtasks)(\s*\/\s*(tasks|subtasks))?\s*$/i;
80
+ const fenceRe = /^\s*(`{3,}|~{3,})/;
81
+ let inSection = false;
82
+ let headerLevel = 0;
83
+ let found = false;
84
+ let hasCheckbox = false;
85
+ let inFence = false;
86
+ let fenceChar = null;
87
+
88
+ for (const line of lines) {
89
+ const fence = line.match(fenceRe);
90
+ if (fence) {
91
+ const ch = fence[1][0];
92
+ if (!inFence) {
93
+ inFence = true;
94
+ fenceChar = ch;
95
+ } else if (fenceChar === ch) {
96
+ inFence = false;
97
+ fenceChar = null;
98
+ }
99
+ continue;
100
+ }
101
+ if (inFence) continue;
102
+
103
+ const h = line.match(/^(#{1,6})\s+.*$/);
104
+ if (h) {
105
+ if (inSection && h[1].length <= headerLevel) {
106
+ // left the section
107
+ break;
108
+ }
109
+ const match = line.match(headerRe);
110
+ if (match) {
111
+ inSection = true;
112
+ found = true;
113
+ headerLevel = match[1].length;
114
+ continue;
115
+ }
116
+ }
117
+ if (inSection && /^\s*[-*]\s*\[ \]/.test(line)) {
118
+ hasCheckbox = true;
119
+ }
120
+ }
121
+
122
+ return { found, hasCheckbox };
123
+ }
124
+
125
+ // Extract AC entries from the `## Acceptance Criteria` section.
126
+ // An entry is one of:
127
+ // 1. <text>
128
+ // - <text>
129
+ // * <text>
130
+ // **AC-N:** <text>
131
+ // Scanning is strictly bounded to within the AC section.
132
+ function extractAcceptanceCriteria(body, sectionName) {
133
+ const lines = body.split('\n');
134
+ const headerRe = new RegExp(
135
+ `^(#{2,})\\s+${sectionName.replace(/[.*+?^${}()|[\\\]]/g, '\\$&')}\\s*$`,
136
+ 'i',
137
+ );
138
+ const fenceRe = /^\s*(`{3,}|~{3,})/;
139
+ let inSection = false;
140
+ let sectionHeaderLevel = 0;
141
+ let inFence = false;
142
+ let fenceChar = null;
143
+ const entries = [];
144
+
145
+ const acEntryRes = [
146
+ /^\s*\d+\.\s+(.+?)\s*$/, // 1. foo
147
+ /^\s*[-*]\s+(?:\*\*AC-\d+:?\*\*\s*)?(.+?)\s*$/, // - foo or - **AC-1:** foo
148
+ /^\s*\*\*AC-\d+:?\*\*\s*(.+?)\s*$/, // **AC-1:** foo
149
+ ];
150
+
151
+ for (const line of lines) {
152
+ const fence = line.match(fenceRe);
153
+ if (fence) {
154
+ const ch = fence[1][0];
155
+ if (!inFence) {
156
+ inFence = true;
157
+ fenceChar = ch;
158
+ } else if (fenceChar === ch) {
159
+ inFence = false;
160
+ fenceChar = null;
161
+ }
162
+ continue;
163
+ }
164
+ if (inFence) continue; // example code can include AC-like lines verbatim
165
+
166
+ const h = line.match(/^(#{1,6})\s+.*$/);
167
+ if (h) {
168
+ if (inSection && h[1].length <= sectionHeaderLevel) {
169
+ inSection = false;
170
+ break;
171
+ }
172
+ const match = line.match(headerRe);
173
+ if (match) {
174
+ inSection = true;
175
+ sectionHeaderLevel = match[1].length;
176
+ continue;
177
+ }
178
+ }
179
+ if (!inSection) continue;
180
+ if (/^\s*$/.test(line)) continue;
181
+ for (const re of acEntryRes) {
182
+ const m = line.match(re);
183
+ if (m && m[1]) {
184
+ const text = m[1].replace(/\s+/g, ' ').trim();
185
+ if (text) entries.push(text);
186
+ break;
187
+ }
188
+ }
189
+ }
190
+
191
+ return entries;
192
+ }
193
+
194
+ function buildTasksSection(entries) {
195
+ const lines = ['', '## Tasks / Subtasks', ''];
196
+ if (entries.length === 0) {
197
+ lines.push('- [ ] Implement story per Acceptance Criteria');
198
+ } else {
199
+ for (const e of entries) {
200
+ lines.push(`- [ ] ${e}`);
201
+ }
202
+ }
203
+ lines.push('');
204
+ return lines.join('\n');
205
+ }
206
+
207
+ function main() {
208
+ const { opts } = parseArgs(process.argv.slice(2));
209
+ if (opts.help) {
210
+ help();
211
+ process.exit(0);
212
+ }
213
+ const storyFile = opts['story-file'];
214
+ if (!storyFile) {
215
+ log.error('--story-file is required');
216
+ process.exit(1);
217
+ }
218
+ if (!fs.existsSync(storyFile)) {
219
+ log.error(`story file missing: ${storyFile}`);
220
+ process.exit(1);
221
+ }
222
+ const acSectionName = opts['ac-section'] || 'Acceptance Criteria';
223
+ const projectRoot = opts['project-root'] || process.cwd();
224
+ const storyKey = storyKeyFromFile(storyFile);
225
+
226
+ const body = fs.readFileSync(storyFile, 'utf8');
227
+ const info = inspectTasksSection(body);
228
+ if (info.found && info.hasCheckbox) {
229
+ emitTimingEvent(projectRoot, storyKey, 'story.inject-tasks', { action: 'skip' });
230
+ process.stdout.write(
231
+ `${JSON.stringify({ action: 'skip', reason: 'tasks-section-present' })}\n`,
232
+ );
233
+ process.exit(0);
234
+ }
235
+
236
+ const entries = extractAcceptanceCriteria(body, acSectionName);
237
+ const section = buildTasksSection(entries);
238
+
239
+ let newBody;
240
+ if (info.found && !info.hasCheckbox) {
241
+ // Section exists but has no checkboxes — append checkboxes inside it
242
+ // by inserting after the header line.
243
+ const lines = body.split('\n');
244
+ const headerRe = /^(#{2,})\s+(tasks|subtasks)(\s*\/\s*(tasks|subtasks))?\s*$/i;
245
+ for (let i = 0; i < lines.length; i++) {
246
+ if (headerRe.test(lines[i])) {
247
+ const injection = entries.length === 0
248
+ ? ['', '- [ ] Implement story per Acceptance Criteria', '']
249
+ : ['', ...entries.map((e) => `- [ ] ${e}`), ''];
250
+ lines.splice(i + 1, 0, ...injection);
251
+ break;
252
+ }
253
+ }
254
+ newBody = lines.join('\n');
255
+ } else {
256
+ newBody = body.replace(/\s*$/, '') + '\n' + section;
257
+ }
258
+
259
+ atomicWrite(path.resolve(storyFile), newBody);
260
+ const action = info.found ? 'checkboxes-added' : 'section-appended';
261
+ emitTimingEvent(projectRoot, storyKey, 'story.inject-tasks', { action, entries: entries.length });
262
+ process.stdout.write(
263
+ `${JSON.stringify({
264
+ action,
265
+ entries: entries.length,
266
+ })}\n`,
267
+ );
268
+ }
269
+
270
+ module.exports = {
271
+ inspectTasksSection,
272
+ extractAcceptanceCriteria,
273
+ buildTasksSection,
274
+ storyKeyFromFile,
275
+ };
276
+
277
+ if (require.main === module) {
278
+ main();
279
+ }
@@ -0,0 +1,295 @@
1
+ #!/usr/bin/env node
2
+
3
+ // list-remaining-stories.js — deterministic compute of stories_remaining.
4
+ //
5
+ // Usage:
6
+ // list-remaining-stories.js --status-file <path> [--format json|csv|lines|envelope]
7
+ //
8
+ // Reads BMad's sprint-status.yaml and returns the set of story keys whose
9
+ // status is NOT "done" (case-insensitive). Handles every BMad-observed
10
+ // shape: dict form, inline form, block form, list form, quoted keys,
11
+ // 2/4/tab indent.
12
+ //
13
+ // Formats:
14
+ // json (default) — `["1-1-a","1-2-b"]`. Legacy form; always a bare array.
15
+ // csv — `1-1-a,1-2-b`
16
+ // lines — `1-1-a\n1-2-b`
17
+ // envelope — `{"remaining":[...],"state":"sprint-in-progress"}`
18
+ // state is one of: pre-planning | sprint-in-progress |
19
+ // sprint-complete | parse-error. This is the shape the
20
+ // autopilot workflow consumes because exit codes alone
21
+ // cannot disambiguate "all done" vs "not planned yet".
22
+ //
23
+ // Exit codes (still emitted for backward compatibility with shell callers):
24
+ // 0 — sprint-in-progress or sprint-complete
25
+ // 1 — parse error (invalid YAML-like content)
26
+ // 2 — pre-planning (status file missing)
27
+ //
28
+ // On every exit path — including errors — a well-formed value is printed
29
+ // on stdout in the requested format. Callers never need to consult stderr
30
+ // or exit-code shell probes to get a usable value.
31
+
32
+ const fs = require('node:fs');
33
+
34
+ const { parseArgs } = require('../lib/runtime/args');
35
+ const log = require('../lib/runtime/log');
36
+
37
+ const VALID_FORMATS = ['json', 'csv', 'lines', 'envelope'];
38
+
39
+ function help() {
40
+ log.out(
41
+ [
42
+ 'Usage:',
43
+ ' list-remaining-stories.js --status-file <path> [--format json|csv|lines|envelope]',
44
+ '',
45
+ 'Emits story keys whose status is NOT "done" (case-insensitive).',
46
+ 'Any status other than the literal token `done` — including null,',
47
+ 'backlog, ready-for-dev, in-progress, review, draft, or typos — is',
48
+ 'treated as "not done".',
49
+ '',
50
+ 'Exits: 0 on success (sprint-in-progress or sprint-complete); 1 on',
51
+ 'parse failure; 2 when the status file is missing. On every exit',
52
+ 'path a well-formed value is still written to stdout.',
53
+ ].join('\n'),
54
+ );
55
+ }
56
+
57
+ // Normalize a scalar value: strip surrounding quotes, trim inner whitespace.
58
+ function stripQuotes(v) {
59
+ if (v == null) return v;
60
+ let s = String(v).trim();
61
+ if (s.length >= 2) {
62
+ if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
63
+ s = s.slice(1, -1);
64
+ }
65
+ }
66
+ return s.trim();
67
+ }
68
+
69
+ // Status comparison is case-insensitive and whitespace-insensitive so
70
+ // `Done`, `"done "`, and `done` all collapse to the same class. Anything
71
+ // other than the literal token `done` is classified as not-done.
72
+ function isDone(status) {
73
+ if (status == null) return false;
74
+ return String(status).trim().toLowerCase() === 'done';
75
+ }
76
+
77
+ function leadingIndent(line) {
78
+ const m = line.match(/^[ \t]*/);
79
+ return m ? m[0].length : 0;
80
+ }
81
+
82
+ // Parse a `key: value` line where key may be quoted. Returns
83
+ // {key, value} or null. Handles trailing `# comment`.
84
+ function parseKV(content) {
85
+ const m = content.match(
86
+ /^["']?([A-Za-z0-9][A-Za-z0-9_.\-]*)["']?\s*:\s*(\S[^#]*?)?(?:\s*#.*)?\s*$/,
87
+ );
88
+ if (!m) return null;
89
+ return { key: m[1], value: m[2] ? stripQuotes(m[2].trim()) : '' };
90
+ }
91
+
92
+ // parseStatuses — indent-agnostic, shape-tolerant scanner for BMad's
93
+ // sprint-status.yaml.
94
+ //
95
+ // Recognized block headers at any indent:
96
+ // development_status:
97
+ // stories:
98
+ //
99
+ // Inside a block, children are detected by indent strictly greater than
100
+ // the block header's indent. The indent of the first child fixes the
101
+ // "item indent" for the block; deeper-indented lines are treated as
102
+ // fields of the current story.
103
+ //
104
+ // Supported per-item shapes:
105
+ // <key>: <status> — dict inline
106
+ // "<key>": <status> — dict inline (quoted)
107
+ // <key>: — dict block (status: on next line)
108
+ // status: <value>
109
+ // - <key>: <status> — list inline
110
+ // - id: <key> — list block (id|key|name as the story id field)
111
+ // status: <value>
112
+ //
113
+ // Returns {storyKey: {status: string|null}}.
114
+ function parseStatuses(raw) {
115
+ const lines = String(raw || '').split(/\r?\n/);
116
+ const stories = {};
117
+
118
+ let inBlock = false;
119
+ let blockIndent = -1;
120
+ let itemIndent = -1;
121
+ let isListBlock = false;
122
+ let currentKey = null;
123
+ let currentKeyIndent = -1;
124
+
125
+ for (let i = 0; i < lines.length; i++) {
126
+ const rawLine = lines[i].replace(/\s+$/, '');
127
+ if (!rawLine.trim()) continue;
128
+ if (/^\s*#/.test(rawLine)) continue;
129
+
130
+ const indent = leadingIndent(rawLine);
131
+ const content = rawLine.slice(indent);
132
+
133
+ // Exit the stories block when we return to its indent or shallower.
134
+ if (inBlock && indent <= blockIndent) {
135
+ inBlock = false;
136
+ itemIndent = -1;
137
+ isListBlock = false;
138
+ currentKey = null;
139
+ currentKeyIndent = -1;
140
+ }
141
+
142
+ if (!inBlock) {
143
+ const h = content.match(/^(development_status|stories)\s*:\s*$/);
144
+ if (h) {
145
+ inBlock = true;
146
+ blockIndent = indent;
147
+ itemIndent = -1;
148
+ isListBlock = false;
149
+ currentKey = null;
150
+ currentKeyIndent = -1;
151
+ }
152
+ continue;
153
+ }
154
+
155
+ // Lazily fix the direct-child indent and the block flavor on the first
156
+ // non-blank child line.
157
+ if (itemIndent < 0) {
158
+ itemIndent = indent;
159
+ isListBlock = content.startsWith('- ') || content === '-';
160
+ }
161
+
162
+ if (indent === itemIndent) {
163
+ if (isListBlock) {
164
+ // `- key: value` | `- id: key` | `- key: value # comment`
165
+ const listM = content.match(/^-\s+(.+)$/);
166
+ if (!listM) {
167
+ // bare `-` with children below; rare but tolerate.
168
+ currentKey = null;
169
+ currentKeyIndent = indent;
170
+ continue;
171
+ }
172
+ const kv = parseKV(listM[1]);
173
+ if (!kv) continue;
174
+ // If the first field inside the list item is an id-like field, the
175
+ // story key is its value; otherwise the field name IS the story key
176
+ // and the field value is its inline status.
177
+ if (kv.key === 'id' || kv.key === 'key' || kv.key === 'name') {
178
+ currentKey = kv.value || null;
179
+ if (currentKey) stories[currentKey] = stories[currentKey] ?? { status: null };
180
+ } else {
181
+ currentKey = kv.key;
182
+ stories[currentKey] = { status: kv.value || null };
183
+ }
184
+ currentKeyIndent = indent;
185
+ } else {
186
+ const kv = parseKV(content);
187
+ if (!kv) continue;
188
+ currentKey = kv.key;
189
+ stories[currentKey] = { status: kv.value || null };
190
+ currentKeyIndent = indent;
191
+ }
192
+ continue;
193
+ }
194
+
195
+ // Deeper than itemIndent → a field of the current list/dict item.
196
+ if (currentKey != null && indent > currentKeyIndent) {
197
+ const kv = parseKV(content);
198
+ if (kv && kv.key === 'status') {
199
+ stories[currentKey].status = kv.value || null;
200
+ }
201
+ }
202
+ }
203
+
204
+ return stories;
205
+ }
206
+
207
+ function remainingFrom(stories) {
208
+ const out = [];
209
+ for (const key of Object.keys(stories)) {
210
+ if (isDone(stories[key].status)) continue;
211
+ out.push(key);
212
+ }
213
+ return out;
214
+ }
215
+
216
+ function emit(list, format, state) {
217
+ if (format === 'envelope') {
218
+ process.stdout.write(`${JSON.stringify({ remaining: list, state })}\n`);
219
+ return;
220
+ }
221
+ if (format === 'csv') {
222
+ process.stdout.write(list.join(',') + (list.length ? '\n' : ''));
223
+ return;
224
+ }
225
+ if (format === 'lines') {
226
+ if (list.length === 0) return;
227
+ process.stdout.write(list.join('\n') + '\n');
228
+ return;
229
+ }
230
+ // json (default)
231
+ process.stdout.write(`${JSON.stringify(list)}\n`);
232
+ }
233
+
234
+ function main() {
235
+ const { opts } = parseArgs(process.argv.slice(2));
236
+ if (opts.help) {
237
+ help();
238
+ process.exit(0);
239
+ }
240
+ const file = opts['status-file'];
241
+ if (!file) {
242
+ log.error('--status-file is required');
243
+ emit([], opts.format || 'json', 'parse-error');
244
+ process.exit(1);
245
+ }
246
+ const format = opts.format || 'json';
247
+ if (!VALID_FORMATS.includes(format)) {
248
+ log.error(`invalid --format '${format}'. Valid: ${VALID_FORMATS.join(', ')}`);
249
+ process.exit(1);
250
+ }
251
+ if (!fs.existsSync(file)) {
252
+ // Pre-planning — status file not yet created.
253
+ emit([], format, 'pre-planning');
254
+ process.exit(2);
255
+ }
256
+ let raw;
257
+ try {
258
+ raw = fs.readFileSync(file, 'utf8');
259
+ } catch (e) {
260
+ log.error(`failed to read ${file}: ${e.message}`);
261
+ emit([], format, 'parse-error');
262
+ process.exit(1);
263
+ }
264
+ let stories;
265
+ try {
266
+ stories = parseStatuses(raw);
267
+ } catch (e) {
268
+ log.error(`failed to parse ${file}: ${e.message}`);
269
+ emit([], format, 'parse-error');
270
+ process.exit(1);
271
+ }
272
+ const remaining = remainingFrom(stories);
273
+ const storyCount = Object.keys(stories).length;
274
+ let state;
275
+ if (storyCount === 0) {
276
+ state = 'pre-planning';
277
+ } else if (remaining.length === 0) {
278
+ state = 'sprint-complete';
279
+ } else {
280
+ state = 'sprint-in-progress';
281
+ }
282
+ emit(remaining, format, state);
283
+ }
284
+
285
+ module.exports = {
286
+ VALID_FORMATS,
287
+ parseStatuses,
288
+ remainingFrom,
289
+ stripQuotes,
290
+ isDone,
291
+ };
292
+
293
+ if (require.main === module) {
294
+ main();
295
+ }