@ikunin/sprintpilot 2.2.30 → 2.3.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/README.md +232 -413
- package/_Sprintpilot/Sprintpilot.md +76 -6
- package/_Sprintpilot/bin/autopilot.js +752 -66
- package/_Sprintpilot/lib/orchestrator/action-ledger.js +208 -0
- package/_Sprintpilot/lib/orchestrator/adapt.js +93 -15
- package/_Sprintpilot/lib/orchestrator/profile-rules.js +7 -16
- package/_Sprintpilot/lib/orchestrator/sprint-plan.js +488 -0
- package/_Sprintpilot/lib/orchestrator/state-store.js +9 -5
- package/_Sprintpilot/lib/orchestrator/user-command-applier.js +107 -0
- package/_Sprintpilot/lib/orchestrator/user-commands.js +124 -1
- package/_Sprintpilot/lib/orchestrator/verify.js +10 -17
- package/_Sprintpilot/manifest.yaml +4 -1
- package/_Sprintpilot/modules/autopilot/profiles/_base.yaml +18 -4
- package/_Sprintpilot/modules/git/config.yaml +15 -9
- package/_Sprintpilot/modules/ma/config.yaml +29 -27
- package/_Sprintpilot/scripts/dispatch-layer.js +12 -15
- package/_Sprintpilot/scripts/infer-dependencies.js +706 -254
- package/_Sprintpilot/scripts/log-timing.js +6 -10
- package/_Sprintpilot/scripts/merge-shards.js +21 -23
- package/_Sprintpilot/scripts/post-green-gates.js +3 -1
- package/_Sprintpilot/scripts/resolve-dag.js +452 -280
- package/_Sprintpilot/scripts/sprint-plan.js +1068 -0
- package/_Sprintpilot/scripts/state-shard.js +13 -5
- package/_Sprintpilot/scripts/summarize-timings.js +2 -3
- package/_Sprintpilot/skills/sprint-autopilot-on/SKILL.md +30 -2
- package/_Sprintpilot/skills/sprint-autopilot-on/workflow.orchestrator.md +36 -10
- package/_Sprintpilot/skills/sprintpilot-dependency-graph/SKILL.md +63 -0
- package/_Sprintpilot/skills/sprintpilot-dependency-graph/workflow.md +227 -0
- package/_Sprintpilot/skills/sprintpilot-plan-sprint/SKILL.md +67 -0
- package/_Sprintpilot/skills/sprintpilot-plan-sprint/workflow.md +435 -0
- package/_Sprintpilot/skills/sprintpilot-sprint-progress/SKILL.md +53 -0
- package/_Sprintpilot/skills/sprintpilot-sprint-progress/workflow.md +169 -0
- package/lib/commands/install.js +186 -10
- package/package.json +1 -1
|
@@ -44,6 +44,40 @@ const VALID_KINDS = [
|
|
|
44
44
|
// includes `summary` (counts) or `reason` ('disabled' / 'no_worktrees_dir'
|
|
45
45
|
// / 'script_missing' / 'health_check_error' / 'worktrees_disabled').
|
|
46
46
|
'worktree_health_check',
|
|
47
|
+
// v2.3.0 — sprint-plan.yaml lifecycle + queue events. Emitted from
|
|
48
|
+
// cmdStart (migration trigger, refresh, queue hydration, auto-derive
|
|
49
|
+
// gate, exhaustion) and cmdRecord (story-done sync to plan).
|
|
50
|
+
'plan_migrated',
|
|
51
|
+
'plan_migration_failed',
|
|
52
|
+
'plan_refreshed',
|
|
53
|
+
'plan_refresh_failed',
|
|
54
|
+
'plan_queue_loaded',
|
|
55
|
+
'plan_queue_failed',
|
|
56
|
+
'plan_exhausted',
|
|
57
|
+
'plan_archive_failed',
|
|
58
|
+
'auto_derive_emitted',
|
|
59
|
+
'plan_story_done',
|
|
60
|
+
'plan_story_done_failed',
|
|
61
|
+
'replan_requested_consumed',
|
|
62
|
+
// v2.3.0 — mid-flight plan mutations applied via applySideEffects.
|
|
63
|
+
'plan_reordered',
|
|
64
|
+
'plan_reorder_rejected',
|
|
65
|
+
'plan_reorder_failed',
|
|
66
|
+
'plan_stories_added',
|
|
67
|
+
'plan_add_stories_failed',
|
|
68
|
+
'plan_stories_removed',
|
|
69
|
+
'plan_remove_stories_failed',
|
|
70
|
+
// v2.3.0 — planning skill outcomes (emitted by /sprintpilot-plan-sprint
|
|
71
|
+
// via the orchestrator after the skill completes).
|
|
72
|
+
'plan_built',
|
|
73
|
+
'cross_epic_edge_rejected',
|
|
74
|
+
'issue_id_set',
|
|
75
|
+
'dag_rendered',
|
|
76
|
+
// v2.3.0 — streaming progress (Phase 4.5). Sub-step granularity within
|
|
77
|
+
// a single story so `autopilot progress` can render live status.
|
|
78
|
+
'story_step_started',
|
|
79
|
+
'story_step_progress',
|
|
80
|
+
'story_step_completed',
|
|
47
81
|
];
|
|
48
82
|
|
|
49
83
|
function isPlainObject(v) {
|
|
@@ -147,11 +181,185 @@ function nextSeq(fs, filePath) {
|
|
|
147
181
|
return 1;
|
|
148
182
|
}
|
|
149
183
|
|
|
184
|
+
// readSince — return entries with seq strictly greater than `afterSeq`.
|
|
185
|
+
// Used by the tail iterator and one-shot consumers that want incremental
|
|
186
|
+
// reads without re-parsing the whole file.
|
|
187
|
+
function readSince(context, afterSeq) {
|
|
188
|
+
const entries = read(context);
|
|
189
|
+
if (typeof afterSeq !== 'number') return entries;
|
|
190
|
+
return entries.filter((e) => typeof e.seq === 'number' && e.seq > afterSeq);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// tail — async iterator yielding ledger entries as they're appended.
|
|
194
|
+
// Polls every `pollIntervalMs` (default 250ms). Terminates when
|
|
195
|
+
// `signal.aborted` is true OR when `maxIdleMs` elapses without new events
|
|
196
|
+
// (default Infinity).
|
|
197
|
+
//
|
|
198
|
+
// Usage:
|
|
199
|
+
// const ctrl = new AbortController();
|
|
200
|
+
// for await (const event of tail({ projectRoot, signal: ctrl.signal })) {
|
|
201
|
+
// console.log(event.kind, event.seq);
|
|
202
|
+
// if (event.kind === 'halt') ctrl.abort();
|
|
203
|
+
// }
|
|
204
|
+
//
|
|
205
|
+
// CI-safe: no fs.watch (some filesystems don't support it; CI logs can
|
|
206
|
+
// be replayed via the underlying file). Pure polling with offset tracking
|
|
207
|
+
// for cheap incremental reads.
|
|
208
|
+
async function* tail(context, options) {
|
|
209
|
+
if (!context || !context.projectRoot) throw new Error('tail: context.projectRoot required');
|
|
210
|
+
const opts = options || {};
|
|
211
|
+
const pollIntervalMs = typeof opts.pollIntervalMs === 'number' ? opts.pollIntervalMs : 250;
|
|
212
|
+
const maxIdleMs = typeof opts.maxIdleMs === 'number' ? opts.maxIdleMs : Number.POSITIVE_INFINITY;
|
|
213
|
+
const signal = opts.signal;
|
|
214
|
+
let lastSeq = typeof opts.afterSeq === 'number' ? opts.afterSeq : 0;
|
|
215
|
+
|
|
216
|
+
// v2.3.0 — track the ledger file's inode so we detect rotation /
|
|
217
|
+
// truncation. If `> ledger.jsonl` or `mv ledger.jsonl ledger.jsonl.1`
|
|
218
|
+
// happens, the inode changes (or stat throws) and we reset lastSeq
|
|
219
|
+
// to 0 so the next poll picks up entries from the start of the new
|
|
220
|
+
// file. Without this, tail() silently misses every event after a
|
|
221
|
+
// rotation.
|
|
222
|
+
const filePath = resolveLedgerPath(context.projectRoot);
|
|
223
|
+
let lastInode = null;
|
|
224
|
+
let lastSize = 0;
|
|
225
|
+
const captureFileIdentity = () => {
|
|
226
|
+
try {
|
|
227
|
+
const st = nodeFs.lstatSync(filePath);
|
|
228
|
+
lastInode = st.ino;
|
|
229
|
+
lastSize = st.size;
|
|
230
|
+
} catch {
|
|
231
|
+
// File doesn't exist yet — that's fine; on first poll we'll
|
|
232
|
+
// capture the identity when it appears.
|
|
233
|
+
lastInode = null;
|
|
234
|
+
lastSize = 0;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
captureFileIdentity();
|
|
238
|
+
|
|
239
|
+
// If afterSeq isn't supplied, start from the current tail so we don't
|
|
240
|
+
// dump the whole history on every call. Pass afterSeq=0 explicitly to
|
|
241
|
+
// get everything.
|
|
242
|
+
if (typeof opts.afterSeq !== 'number') {
|
|
243
|
+
const existing = read(context);
|
|
244
|
+
if (existing.length > 0) {
|
|
245
|
+
const tailEntry = existing[existing.length - 1];
|
|
246
|
+
if (typeof tailEntry.seq === 'number') lastSeq = tailEntry.seq;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
251
|
+
if (!signal) {
|
|
252
|
+
setTimeout(resolve, ms);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const t = setTimeout(resolve, ms);
|
|
256
|
+
if (signal.aborted) {
|
|
257
|
+
clearTimeout(t);
|
|
258
|
+
resolve();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
signal.addEventListener('abort', () => {
|
|
262
|
+
clearTimeout(t);
|
|
263
|
+
resolve();
|
|
264
|
+
}, { once: true });
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
let idleAccumulatedMs = 0;
|
|
268
|
+
while (!(signal && signal.aborted)) {
|
|
269
|
+
// Rotation / truncation check before each poll. Three cases:
|
|
270
|
+
// - File didn't exist before, now does → capture identity, treat
|
|
271
|
+
// as fresh start; do NOT reset lastSeq (afterSeq semantics still
|
|
272
|
+
// apply).
|
|
273
|
+
// - File existed before, now doesn't → it was deleted; reset
|
|
274
|
+
// identity tracking, on next iteration we'll re-capture.
|
|
275
|
+
// - File exists with a different inode OR smaller size than last
|
|
276
|
+
// time → rotated/truncated; reset lastSeq=0 so we yield from
|
|
277
|
+
// the start of the new file.
|
|
278
|
+
let currentInode = null;
|
|
279
|
+
let currentSize = 0;
|
|
280
|
+
try {
|
|
281
|
+
const st = nodeFs.lstatSync(filePath);
|
|
282
|
+
currentInode = st.ino;
|
|
283
|
+
currentSize = st.size;
|
|
284
|
+
} catch {
|
|
285
|
+
// File missing — wait for it to appear.
|
|
286
|
+
}
|
|
287
|
+
if (lastInode !== null && currentInode !== null) {
|
|
288
|
+
const inodeChanged = currentInode !== lastInode;
|
|
289
|
+
const truncated = currentSize < lastSize;
|
|
290
|
+
if (inodeChanged || truncated) {
|
|
291
|
+
lastSeq = 0; // re-yield from the new file's start
|
|
292
|
+
lastInode = currentInode;
|
|
293
|
+
lastSize = currentSize;
|
|
294
|
+
}
|
|
295
|
+
} else if (currentInode !== null) {
|
|
296
|
+
// File appeared (was missing, now exists).
|
|
297
|
+
lastInode = currentInode;
|
|
298
|
+
lastSize = currentSize;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const fresh = readSince(context, lastSeq);
|
|
302
|
+
// v2.3.0 Round 2 — re-check inode AFTER readSince. The file could
|
|
303
|
+
// rotate during the read; without this we'd yield entries from the
|
|
304
|
+
// NEW file as if they were continuations of the old one (or skip
|
|
305
|
+
// them if their seq < lastSeq from the rotated file).
|
|
306
|
+
let postReadInode = null;
|
|
307
|
+
let postReadSize = 0;
|
|
308
|
+
try {
|
|
309
|
+
const st = nodeFs.lstatSync(filePath);
|
|
310
|
+
postReadInode = st.ino;
|
|
311
|
+
postReadSize = st.size;
|
|
312
|
+
} catch {
|
|
313
|
+
/* file gone — handled next iteration */
|
|
314
|
+
}
|
|
315
|
+
if (
|
|
316
|
+
lastInode !== null &&
|
|
317
|
+
postReadInode !== null &&
|
|
318
|
+
(postReadInode !== lastInode || postReadSize < lastSize)
|
|
319
|
+
) {
|
|
320
|
+
// Rotation/truncation happened during the read. Discard the
|
|
321
|
+
// fresh batch (might be from the OLD inode), reset lastSeq to 0,
|
|
322
|
+
// and let the next iteration re-yield from the new file's start.
|
|
323
|
+
lastSeq = 0;
|
|
324
|
+
lastInode = postReadInode;
|
|
325
|
+
lastSize = postReadSize;
|
|
326
|
+
// Don't yield any of `fresh` since we can't trust which file
|
|
327
|
+
// they came from after the rotation; the next iteration's
|
|
328
|
+
// readSince(0) will pick up the new file's entries.
|
|
329
|
+
await sleep(pollIntervalMs);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (fresh.length > 0) {
|
|
333
|
+
idleAccumulatedMs = 0;
|
|
334
|
+
for (const event of fresh) {
|
|
335
|
+
if (signal && signal.aborted) return;
|
|
336
|
+
if (typeof event.seq === 'number' && event.seq > lastSeq) {
|
|
337
|
+
lastSeq = event.seq;
|
|
338
|
+
}
|
|
339
|
+
yield event;
|
|
340
|
+
}
|
|
341
|
+
// Refresh size after yielding so the next iteration's truncation
|
|
342
|
+
// check uses the right baseline.
|
|
343
|
+
try {
|
|
344
|
+
lastSize = nodeFs.lstatSync(filePath).size;
|
|
345
|
+
} catch {
|
|
346
|
+
/* file disappeared between yield and stat — handle next loop */
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
idleAccumulatedMs += pollIntervalMs;
|
|
350
|
+
if (idleAccumulatedMs >= maxIdleMs) return;
|
|
351
|
+
}
|
|
352
|
+
await sleep(pollIntervalMs);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
150
356
|
module.exports = {
|
|
151
357
|
VALID_KINDS,
|
|
152
358
|
LEDGER_FILENAME,
|
|
153
359
|
append,
|
|
154
360
|
read,
|
|
361
|
+
readSince,
|
|
155
362
|
last,
|
|
363
|
+
tail,
|
|
156
364
|
resolveLedgerPath,
|
|
157
365
|
};
|
|
@@ -18,6 +18,34 @@ const userCommandApplier = require('./user-command-applier');
|
|
|
18
18
|
// Threshold for `consecutive_test_failures` — workflow.md:81 says 3.
|
|
19
19
|
const CONSECUTIVE_TEST_FAILURE_THRESHOLD = 3;
|
|
20
20
|
|
|
21
|
+
// Threshold for the verify-loop diagnostic: when the SAME verify issues
|
|
22
|
+
// repeat this many times in a row, the budget-exhausted halt prompt
|
|
23
|
+
// enriches itself with a loop-detection hint (vs. a generic "rejected N
|
|
24
|
+
// times" message). 3 matches verify_reject_budget for medium/large/legacy
|
|
25
|
+
// profiles, so by the time the budget halts, the diagnostic is guaranteed
|
|
26
|
+
// to fire if and only if the rejections were genuinely identical.
|
|
27
|
+
const VERIFY_LOOP_THRESHOLD = 3;
|
|
28
|
+
|
|
29
|
+
// Stable, order-independent signature of a verify issues array.
|
|
30
|
+
// We compare via sorted JSON so two arrays with the same strings in
|
|
31
|
+
// different order hash to the same signature (the verifier may reorder
|
|
32
|
+
// internally across runs). Returns null for empty or non-array input.
|
|
33
|
+
function verifyIssuesSignature(issues) {
|
|
34
|
+
if (!Array.isArray(issues) || issues.length === 0) return null;
|
|
35
|
+
// Coerce to strings, trim whitespace, then sort. The trim guards
|
|
36
|
+
// against the verifier accidentally producing trailing whitespace
|
|
37
|
+
// on one run but not another — without it, "branch required" and
|
|
38
|
+
// "branch required " would hash differently and silently break the
|
|
39
|
+
// loop detection. Trim is safe: leading/trailing whitespace in a
|
|
40
|
+
// verify-issue string is never load-bearing.
|
|
41
|
+
const strs = issues
|
|
42
|
+
.map((i) => (typeof i === 'string' ? i : JSON.stringify(i)))
|
|
43
|
+
.map((s) => s.trim())
|
|
44
|
+
.slice()
|
|
45
|
+
.sort();
|
|
46
|
+
return JSON.stringify(strs);
|
|
47
|
+
}
|
|
48
|
+
|
|
21
49
|
// Valid signal statuses.
|
|
22
50
|
const SIGNAL_STATUSES = [
|
|
23
51
|
'success',
|
|
@@ -73,28 +101,69 @@ function handleSuccess(state, signal, profile, verifyResult, sideEffects) {
|
|
|
73
101
|
// Trust boundary: verify.js may reject what the LLM claims as success.
|
|
74
102
|
if (verifyResult && verifyResult.ok === false) {
|
|
75
103
|
const rejectCount = (state.verify_reject_count || 0) + 1;
|
|
104
|
+
|
|
105
|
+
// Loop detection: compare the current issues signature against the
|
|
106
|
+
// last one. Identical sets in a row → the LLM is retrying with the
|
|
107
|
+
// same broken signal. This drives the enriched halt prompt below.
|
|
108
|
+
const currentSig = verifyIssuesSignature(verifyResult.issues || []);
|
|
109
|
+
const lastSig = state.last_verify_issues_signature || null;
|
|
110
|
+
const identicalCount =
|
|
111
|
+
currentSig !== null && currentSig === lastSig
|
|
112
|
+
? (state.consecutive_identical_rejections || 0) + 1
|
|
113
|
+
: 1;
|
|
114
|
+
|
|
76
115
|
sideEffects.push({
|
|
77
116
|
kind: 'log_verify_rejection',
|
|
78
117
|
phase: state.phase,
|
|
79
118
|
issues: verifyResult.issues || [],
|
|
80
119
|
consecutive: rejectCount,
|
|
120
|
+
consecutive_identical: identicalCount,
|
|
81
121
|
});
|
|
122
|
+
|
|
123
|
+
const stateWithLoopTrackers = {
|
|
124
|
+
...state,
|
|
125
|
+
last_verify_issues_signature: currentSig,
|
|
126
|
+
consecutive_identical_rejections: identicalCount,
|
|
127
|
+
};
|
|
128
|
+
|
|
82
129
|
if (rejectCount >= profile.verify_reject_budget) {
|
|
130
|
+
// Enriched diagnostic when the same issues recurred. Picks 2 as
|
|
131
|
+
// the threshold for the hint (vs. 3 for a "strong loop") because
|
|
132
|
+
// at budget exhaustion the minimum interesting case is 2 identical
|
|
133
|
+
// rejections in a row; we want the hint to fire whenever the LLM
|
|
134
|
+
// demonstrably wasn't iterating its signal between attempts.
|
|
135
|
+
const issueCount = verifyResult.issues?.length || 0;
|
|
136
|
+
const issuePlural = issueCount === 1 ? 'issue' : 'issues';
|
|
137
|
+
const timePlural = identicalCount === 1 ? 'time' : 'times';
|
|
138
|
+
const loopHint =
|
|
139
|
+
identicalCount >= 2
|
|
140
|
+
? `\n\n⚠ Verify rejected the SAME ${issueCount} ${issuePlural} ${identicalCount} ${timePlural} in a row — this is a loop, not random noise. ` +
|
|
141
|
+
`The LLM is re-sending an identical broken signal each retry. ` +
|
|
142
|
+
`Action: read each issue text below and fix the underlying cause (e.g., if "git_steps_completed must be true — skipping git push is the most common cause", verify your git_op action actually ran \`git push\` to exit 0); don't just retry the same signal.`
|
|
143
|
+
: '';
|
|
83
144
|
return {
|
|
84
|
-
newState: {
|
|
145
|
+
newState: {
|
|
146
|
+
...stateWithLoopTrackers,
|
|
147
|
+
verify_reject_count: 0,
|
|
148
|
+
last_verify_issues_signature: null,
|
|
149
|
+
consecutive_identical_rejections: 0,
|
|
150
|
+
},
|
|
85
151
|
newProfile: profile,
|
|
86
152
|
nextAction: {
|
|
87
153
|
type: 'user_prompt',
|
|
88
154
|
phase: state.phase,
|
|
89
155
|
reason: 'verify_reject_budget_exceeded',
|
|
90
|
-
prompt:
|
|
156
|
+
prompt:
|
|
157
|
+
`verify.js rejected ${rejectCount} consecutive success signals on ${state.phase}. ` +
|
|
158
|
+
`Last issues: ${JSON.stringify(verifyResult.issues || [])}${loopHint}`,
|
|
159
|
+
consecutive_identical: identicalCount,
|
|
91
160
|
},
|
|
92
161
|
sideEffects,
|
|
93
162
|
verdict: 'prompted',
|
|
94
163
|
};
|
|
95
164
|
}
|
|
96
165
|
return {
|
|
97
|
-
newState: { ...
|
|
166
|
+
newState: { ...stateWithLoopTrackers, verify_reject_count: rejectCount },
|
|
98
167
|
newProfile: profile,
|
|
99
168
|
// Retry the same phase. adapt's caller will re-run nextAction(state, profile).
|
|
100
169
|
nextAction: nextAction(state, profile),
|
|
@@ -275,9 +344,8 @@ function handleBlocked(state, signal, profile, sideEffects) {
|
|
|
275
344
|
case 'missing_dependency':
|
|
276
345
|
// Emit an abstract install action. The CLI edge (autopilot.js
|
|
277
346
|
// decorateRunScript) detects the project's language(s) from
|
|
278
|
-
// manifest files
|
|
279
|
-
//
|
|
280
|
-
// (Python, Rust, Go, Ruby, etc.).
|
|
347
|
+
// manifest files (package.json, pyproject.toml, Cargo.toml, etc.)
|
|
348
|
+
// and inlines the concrete `command` per language.
|
|
281
349
|
return {
|
|
282
350
|
newState: state,
|
|
283
351
|
newProfile: profile,
|
|
@@ -413,14 +481,12 @@ function handleUserInput(state, signal, profile, sideEffects) {
|
|
|
413
481
|
// checks all reference the wrong story.
|
|
414
482
|
//
|
|
415
483
|
// Phase advance: when the alternative carries `phase` and it's a
|
|
416
|
-
// valid STATES value, also advance state.phase.
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
// (e.g. verify may reject the new phase if its preconditions aren't
|
|
423
|
-
// met). Without this, accept_alternative is useless for cycle skips.
|
|
484
|
+
// valid STATES value, also advance state.phase. The user explicitly
|
|
485
|
+
// proposes the alternative including a target phase; they accept the
|
|
486
|
+
// consequences (e.g. verify may reject the new phase if its
|
|
487
|
+
// preconditions aren't met). This enables cycle skips like "jump to
|
|
488
|
+
// STORY_DONE because the work is already on the branch from a prior
|
|
489
|
+
// session."
|
|
424
490
|
const dispatch = applied.sideEffects.find((e) => e && e.kind === 'dispatch_action');
|
|
425
491
|
if (dispatch && dispatch.action) {
|
|
426
492
|
const a = dispatch.action;
|
|
@@ -499,7 +565,17 @@ function handleVerifyOverride(state, signal, profile, verifyResult, sideEffects)
|
|
|
499
565
|
// clears patch_findings when leaving step 6; resets per-story counters when
|
|
500
566
|
// starting a new story.
|
|
501
567
|
function advanceState(state, profile, newPhase, signal) {
|
|
502
|
-
const next = {
|
|
568
|
+
const next = {
|
|
569
|
+
...state,
|
|
570
|
+
phase: newPhase,
|
|
571
|
+
retry_count_this_phase: 0,
|
|
572
|
+
verify_reject_count: 0,
|
|
573
|
+
// v2.3.0 — phase transition clears verify-loop trackers so the next
|
|
574
|
+
// phase starts fresh. Without this a stale signature from the prior
|
|
575
|
+
// phase could artificially inflate identicalCount on the next reject.
|
|
576
|
+
last_verify_issues_signature: null,
|
|
577
|
+
consecutive_identical_rejections: 0,
|
|
578
|
+
};
|
|
503
579
|
// Advancing forward clears the prior diagnosis (the LLM resolved it).
|
|
504
580
|
next.prior_diagnosis = null;
|
|
505
581
|
|
|
@@ -626,5 +702,7 @@ module.exports = {
|
|
|
626
702
|
interpretSignal,
|
|
627
703
|
advanceState,
|
|
628
704
|
CONSECUTIVE_TEST_FAILURE_THRESHOLD,
|
|
705
|
+
VERIFY_LOOP_THRESHOLD,
|
|
629
706
|
SIGNAL_STATUSES,
|
|
707
|
+
verifyIssuesSignature,
|
|
630
708
|
};
|
|
@@ -98,9 +98,6 @@ function flatToProfile(resolved, profileName) {
|
|
|
98
98
|
// and the .timings/<story>.jsonl shards stop receiving events. Set
|
|
99
99
|
// false on the `legacy` profile (no parallel coordination, no need
|
|
100
100
|
// for granular timing). Default true on every other profile.
|
|
101
|
-
// Pre-2.2.26: flatToProfile didn't include this field, so
|
|
102
|
-
// `profile.phase_timings === false` was always false (undefined !==
|
|
103
|
-
// false), meaning the legacy override never took effect.
|
|
104
101
|
phase_timings: coerceBool(get(resolved, 'autopilot.phase_timings'), true),
|
|
105
102
|
granularity: coerceEnum(get(resolved, 'git.granularity'), VALID_GRANULARITIES, 'story'),
|
|
106
103
|
worktree_enabled: coerceBool(get(resolved, 'git.worktree.enabled'), true),
|
|
@@ -180,22 +177,16 @@ function flatToProfile(resolved, profileName) {
|
|
|
180
177
|
// --stale-minutes. 0 disables the auto-takeover entirely (locks are
|
|
181
178
|
// never considered stale; manual `autopilot off` required).
|
|
182
179
|
lock_stale_timeout_minutes: coerceInt(get(resolved, 'git.lock.stale_timeout_minutes'), 30),
|
|
183
|
-
// git.lint.* —
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
// experimental warning when lint_enabled=true (mirroring
|
|
188
|
-
// parallel_stories handling). Full state-machine integration is
|
|
189
|
-
// tracked for v2.3.0+.
|
|
180
|
+
// git.lint.* — post-DEV_GREEN lint gate (scripts/post-green-gates.js).
|
|
181
|
+
// verifyDevGreen invokes it when lint_enabled=true; lint_blocking
|
|
182
|
+
// governs whether a failed gate rejects verify or just records.
|
|
183
|
+
// lint_output_limit caps lines of lint output per gate.
|
|
190
184
|
lint_enabled: coerceBool(get(resolved, 'git.lint.enabled'), false),
|
|
191
185
|
lint_blocking: coerceBool(get(resolved, 'git.lint.blocking'), false),
|
|
192
186
|
lint_output_limit: coerceInt(get(resolved, 'git.lint.output_limit'), 100),
|
|
193
|
-
// git.lint.linters — per-language preference map.
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
// priority order in lint-changed.js matches the documented config
|
|
197
|
-
// defaults, so most users see no behavior change. Setting an empty
|
|
198
|
-
// array for a language disables linting for that language entirely.
|
|
187
|
+
// git.lint.linters — per-language preference map. Forwarded to
|
|
188
|
+
// lint-changed.js as --linters-json. Empty list disables a language.
|
|
189
|
+
// javascript + typescript merge into js-ts (shared eslint/biome tooling).
|
|
199
190
|
lint_linters: (() => {
|
|
200
191
|
const v = get(resolved, 'git.lint.linters');
|
|
201
192
|
return v && typeof v === 'object' && !Array.isArray(v) ? v : null;
|