@ikunin/sprintpilot 2.2.28 → 2.2.30
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.
|
@@ -1380,18 +1380,66 @@ function cmdStart(opts) {
|
|
|
1380
1380
|
}
|
|
1381
1381
|
|
|
1382
1382
|
// Resume detection: if a prior session left a fingerprint, diff.
|
|
1383
|
+
// Two escape hatches let cmdStart proceed despite a divergent fingerprint:
|
|
1384
|
+
//
|
|
1385
|
+
// 1. External completion (auto): if the last halt's `current_story`
|
|
1386
|
+
// is now marked `done` in sprint-status, the divergence is the
|
|
1387
|
+
// EXPECTED result of completing that story outside the autopilot
|
|
1388
|
+
// (manual merge, hot-fix, PR landed via the UI). Clear the
|
|
1389
|
+
// stale story identity from persisted state and proceed —
|
|
1390
|
+
// composeRuntimeState's resolver will pick the next pending story.
|
|
1391
|
+
//
|
|
1392
|
+
// 2. Explicit --accept-divergence flag: catch-all for cases (1) doesn't
|
|
1393
|
+
// cover (multiple stories completed, branch heads moved, etc.). The
|
|
1394
|
+
// flag is logged into the ledger so the audit trail records that
|
|
1395
|
+
// the user opted in to bypass.
|
|
1383
1396
|
const lastHalt = ledger.last({ projectRoot }, 'halt');
|
|
1384
1397
|
if (lastHalt && lastHalt.fingerprint) {
|
|
1385
1398
|
const d = divergence.detect({ projectRoot }, lastHalt.fingerprint);
|
|
1386
1399
|
if (!d.identical) {
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1400
|
+
let autoAck = null;
|
|
1401
|
+
const persistedStory = persisted.current_story || null;
|
|
1402
|
+
if (persistedStory) {
|
|
1403
|
+
const stories = readSprintStatuses(projectRoot);
|
|
1404
|
+
const status = stories && stories[persistedStory]
|
|
1405
|
+
? String(stories[persistedStory].status || '').trim().toLowerCase()
|
|
1406
|
+
: null;
|
|
1407
|
+
if (status === 'done') {
|
|
1408
|
+
autoAck = { reason: 'external_completion', story: persistedStory };
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
const accepted = autoAck || (opts['accept-divergence'] ? { reason: 'explicit_accept' } : null);
|
|
1412
|
+
if (!accepted) {
|
|
1413
|
+
const result = {
|
|
1414
|
+
kind: 'resume_divergence',
|
|
1415
|
+
differences: d.differences,
|
|
1416
|
+
last_phase: persisted.current_bmad_step || null,
|
|
1417
|
+
hint:
|
|
1418
|
+
'Pass --accept-divergence to proceed despite the diff, or finish externally-merged stories so sprint-status reflects reality before resuming.',
|
|
1419
|
+
};
|
|
1420
|
+
ledger.append({ kind: 'resume', divergence: result }, { projectRoot });
|
|
1421
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
1422
|
+
return 0;
|
|
1423
|
+
}
|
|
1424
|
+
// Clear the stale story identity before composeRuntimeState runs.
|
|
1425
|
+
// Without this the runtime would re-enter the same story (which is
|
|
1426
|
+
// now done) and verifyStoryDone would loop on "already complete".
|
|
1427
|
+
persisted.current_story = null;
|
|
1428
|
+
persisted.story_file_path = null;
|
|
1429
|
+
persisted.current_epic = null;
|
|
1430
|
+
persisted.current_bmad_step = null;
|
|
1431
|
+
ledger.append(
|
|
1432
|
+
{
|
|
1433
|
+
kind: 'resume',
|
|
1434
|
+
divergence: {
|
|
1435
|
+
kind: 'divergence_accepted',
|
|
1436
|
+
...accepted,
|
|
1437
|
+
differences: d.differences,
|
|
1438
|
+
last_phase: lastHalt.phase || null,
|
|
1439
|
+
},
|
|
1440
|
+
},
|
|
1441
|
+
{ projectRoot },
|
|
1442
|
+
);
|
|
1395
1443
|
}
|
|
1396
1444
|
}
|
|
1397
1445
|
|
|
@@ -96,30 +96,101 @@ function readStateFile(fs, filePath) {
|
|
|
96
96
|
// Narrow YAML parser sufficient for our write shape (the same shape we
|
|
97
97
|
// produce via dumpYaml above). We deliberately avoid js-yaml so we don't
|
|
98
98
|
// pull a runtime dep into the install-time script bundle.
|
|
99
|
+
//
|
|
100
|
+
// Supports:
|
|
101
|
+
// key: scalar (inline scalar)
|
|
102
|
+
// key: [a, b] (inline JSON array via parseScalar)
|
|
103
|
+
// key: (nested object — children at deeper indent)
|
|
104
|
+
// subkey: value
|
|
105
|
+
// key: (nested array — `- item` lines at deeper indent)
|
|
106
|
+
// - item-scalar
|
|
107
|
+
// - item-key: item-value
|
|
108
|
+
//
|
|
109
|
+
// The block-form array path was added in v2.2.29 — pre-2.2.29 the
|
|
110
|
+
// parser unconditionally `continue`d on any line without `:`, silently
|
|
111
|
+
// dropping every `- item` entry. Hand-edited state files (or any
|
|
112
|
+
// roundtrip through a tool that emits block-form YAML) lost their
|
|
113
|
+
// `story_queue`, leaving the autopilot's queue mysteriously empty.
|
|
99
114
|
function parseYamlNarrow(text) {
|
|
100
115
|
if (!text) return {};
|
|
101
116
|
const lines = text.split(/\r?\n/);
|
|
102
117
|
const root = {};
|
|
103
|
-
|
|
118
|
+
// Stack frame:
|
|
119
|
+
// indent — indent of the KEY that opened this container (its
|
|
120
|
+
// children live at indent > frame.indent)
|
|
121
|
+
// container — the object or array we're populating
|
|
122
|
+
// isArray — true once we've promoted container from {} to []
|
|
123
|
+
// parentObj — owner of container (used to swap {} → [] when the
|
|
124
|
+
// first child is a `- ` line)
|
|
125
|
+
// parentKey — slot on parentObj that holds container
|
|
126
|
+
const stack = [{ indent: -1, container: root, isArray: false, parentObj: null, parentKey: null }];
|
|
104
127
|
for (const raw of lines) {
|
|
105
128
|
const hashIdx = raw.indexOf('#');
|
|
106
129
|
const line = hashIdx === -1 ? raw : raw.slice(0, hashIdx);
|
|
107
130
|
if (!line.trim()) continue;
|
|
108
131
|
const indent = line.match(/^( *)/)[1].length;
|
|
109
132
|
const content = line.slice(indent).trimEnd();
|
|
133
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) stack.pop();
|
|
134
|
+
const top = stack[stack.length - 1];
|
|
135
|
+
|
|
136
|
+
// List item shape: `- ` or bare `-`.
|
|
137
|
+
if (content === '-' || content.startsWith('- ')) {
|
|
138
|
+
// Promote container to array if this is the first list item seen
|
|
139
|
+
// for the current key. Root-level lists aren't supported (state
|
|
140
|
+
// files always have an object root) — skip cleanly.
|
|
141
|
+
if (!top.isArray) {
|
|
142
|
+
if (!top.parentObj || top.parentKey == null) continue;
|
|
143
|
+
const arr = [];
|
|
144
|
+
top.parentObj[top.parentKey] = arr;
|
|
145
|
+
top.container = arr;
|
|
146
|
+
top.isArray = true;
|
|
147
|
+
}
|
|
148
|
+
const rest = content === '-' ? '' : content.slice(2).trim();
|
|
149
|
+
if (rest === '') {
|
|
150
|
+
// Bare `-` with children below — append a fresh object and let
|
|
151
|
+
// subsequent deeper-indent lines populate it.
|
|
152
|
+
const child = {};
|
|
153
|
+
top.container.push(child);
|
|
154
|
+
stack.push({ indent, container: child, isArray: false, parentObj: null, parentKey: null });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const colon = rest.indexOf(':');
|
|
158
|
+
if (colon === -1) {
|
|
159
|
+
// Plain scalar list item.
|
|
160
|
+
top.container.push(parseScalar(rest));
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
// `- key: value` or `- key:` (object item).
|
|
164
|
+
const k = rest.slice(0, colon).trim();
|
|
165
|
+
const v = rest.slice(colon + 1).trim();
|
|
166
|
+
if (v === '') {
|
|
167
|
+
const child = {};
|
|
168
|
+
const wrapper = { [k]: child };
|
|
169
|
+
top.container.push(wrapper);
|
|
170
|
+
stack.push({ indent, container: child, isArray: false, parentObj: wrapper, parentKey: k });
|
|
171
|
+
} else {
|
|
172
|
+
top.container.push({ [k]: parseScalar(v) });
|
|
173
|
+
}
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Object key: value
|
|
110
178
|
const colon = content.indexOf(':');
|
|
111
179
|
if (colon === -1) continue;
|
|
112
180
|
const key = content.slice(0, colon).trim();
|
|
113
181
|
const rest = content.slice(colon + 1).trim();
|
|
114
|
-
|
|
115
|
-
|
|
182
|
+
if (top.isArray) {
|
|
183
|
+
// Defensive: a stray `key:` inside an array context is malformed.
|
|
184
|
+
// Skip rather than corrupt the array.
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
116
187
|
if (rest === '') {
|
|
117
188
|
const child = {};
|
|
118
|
-
|
|
119
|
-
stack.push({ indent,
|
|
189
|
+
top.container[key] = child;
|
|
190
|
+
stack.push({ indent, container: child, isArray: false, parentObj: top.container, parentKey: key });
|
|
120
191
|
continue;
|
|
121
192
|
}
|
|
122
|
-
|
|
193
|
+
top.container[key] = parseScalar(rest);
|
|
123
194
|
}
|
|
124
195
|
return root;
|
|
125
196
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikunin/sprintpilot",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.30",
|
|
4
4
|
"description": "Sprintpilot — autopilot and multi-agent addon for BMad Method v6: git workflow, parallel agents, autonomous story execution",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|