@indigoai-us/hq-cli 5.345.117 → 5.345.119

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 (40) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/assets/scaffold/core/scripts/pipeline-conductor.sh +2671 -0
  3. package/assets/scaffold/core/scripts/pipeline-driver.sh +820 -0
  4. package/assets/scaffold/core/scripts/pipeline-envelope.sh +211 -0
  5. package/dist/command-catalog.generated.d.ts +34 -0
  6. package/dist/command-catalog.generated.js +40 -0
  7. package/dist/commands/core-checkpoint-native.d.ts +17 -3
  8. package/dist/commands/core-checkpoint-native.js +48 -8
  9. package/dist/commands/core-derive-trigger-facts-native.js +15 -7
  10. package/dist/commands/core-migrate-policy-triggers-native.js +3 -1
  11. package/dist/commands/core-pipeline-conductor-command.d.ts +5 -0
  12. package/dist/commands/core-pipeline-conductor-command.js +138 -0
  13. package/dist/commands/core-pipeline-conductor-native.d.ts +7 -0
  14. package/dist/commands/core-pipeline-conductor-native.js +2950 -0
  15. package/dist/commands/core-pipeline-driver-native.d.ts +24 -0
  16. package/dist/commands/core-pipeline-driver-native.js +843 -0
  17. package/dist/commands/core-pipeline-driver-process.d.ts +4 -0
  18. package/dist/commands/core-pipeline-driver-process.js +68 -0
  19. package/dist/commands/core-pipeline-envelope-native.d.ts +4 -0
  20. package/dist/commands/core-pipeline-envelope-native.js +87 -0
  21. package/dist/commands/core-policy.js +3 -0
  22. package/dist/commands/core-worker-lint-native.d.ts +0 -1
  23. package/dist/commands/core-worker-lint-native.js +0 -1
  24. package/dist/commands/core-worker-share-native.js +2 -2
  25. package/dist/commands/core-worker-skills-command.d.ts +1 -1
  26. package/dist/commands/core-worker-skills-command.js +3 -20
  27. package/dist/commands/core.d.ts +5 -1
  28. package/dist/commands/core.js +8 -9
  29. package/dist/commands/integrations-core.d.ts +7 -1
  30. package/dist/commands/integrations-core.js +17 -4
  31. package/dist/commands/integrations.js +91 -22
  32. package/dist/commands/storage-gate.d.ts +4 -3
  33. package/dist/commands/storage-gate.js +4 -3
  34. package/dist/fast-scaffold.js +0 -2
  35. package/dist/lib/storage-feature-flag.d.ts +7 -7
  36. package/package.json +1 -1
  37. package/assets/scaffold/core/scripts/lint-shared-worker-skills.sh +0 -197
  38. package/assets/scaffold/core/scripts/share-worker-skill.sh +0 -178
  39. package/dist/commands/core-native-worker-skills-flag.d.ts +0 -4
  40. package/dist/commands/core-native-worker-skills-flag.js +0 -7
@@ -0,0 +1,843 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { inflateRawSync } from "node:zlib";
5
+ import { isPipelineDriverProcess } from "./core-pipeline-driver-process.js";
6
+ /* eslint-disable @typescript-eslint/no-explicit-any */
7
+ export function scanDriverState(stateDir, maxFails, hqBin, run = spawnSync) {
8
+ const S = stateDir, MAX = maxFails, HQ = hqBin;
9
+ const TERMINAL = ['passed', 'failed', 'blocked'];
10
+ const EARLY = 'engine_exited_early';
11
+ const isObj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
12
+ const isNum = (v) => typeof v === 'number';
13
+ const truthy = (v) => !(v === null || v === undefined || v === false || v === 0 || v === '' ||
14
+ (Array.isArray(v) && !v.length) || (isObj(v) && !Object.keys(v).length));
15
+ const str = (v) => v === undefined || v === null ? 'None' : v === true ? 'True' : v === false ? 'False'
16
+ : typeof v === 'string' ? v : JSON.stringify(v);
17
+ const get = (o, k, d = undefined) => (isObj(o) && Object.prototype.hasOwnProperty.call(o, k) ? o[k] : d);
18
+ const byteSort = (a) => a.sort((x, y) => Buffer.compare(Buffer.from(x), Buffer.from(y)));
19
+ function load(p) {
20
+ try {
21
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
22
+ }
23
+ catch (e) {
24
+ return null;
25
+ }
26
+ }
27
+ // whole seconds since the epoch of "YYYY-MM-DDTHH:MM:SS" (first 19 chars); null when unparseable
28
+ function epoch(ts) {
29
+ const m = /^(\d{4})-(\d{1,2})-(\d{1,2})T(\d{1,2}):(\d{1,2}):(\d{1,2})$/.exec(String(ts).slice(0, 19));
30
+ if (!m)
31
+ return null;
32
+ const [y, mo, d, h, mi, s] = m.slice(1).map(Number);
33
+ if (mo < 1 || mo > 12 || h > 23 || mi > 59 || s > 61)
34
+ return null;
35
+ const ms = Date.UTC(y, mo - 1, d, h, mi, s);
36
+ if (d < 1 || new Date(Date.UTC(y, mo - 1, d)).getUTCDate() !== d)
37
+ return null;
38
+ return Math.floor(ms / 1000);
39
+ }
40
+ const now = () => Date.now() / 1000;
41
+ const isoNow = () => new Date().toISOString().slice(0, 19) + 'Z';
42
+ const collapse = (s) => Array.from(s.split(/\s+/).filter(Boolean).join(' ')).slice(0, 300).join('');
43
+ let lanes = null;
44
+ function laneRows() {
45
+ if (lanes === null) {
46
+ lanes = new Map();
47
+ try {
48
+ const owned = load(path.join(S, 'lanes.json')) || {};
49
+ const r = run(HQ, ['lanes', 'list', '--json'], { encoding: 'utf8', timeout: 30000 });
50
+ if (!r.error) {
51
+ const text = String(r.stdout || ''), starts = [text.indexOf('['), text.indexOf('{')].filter((n) => n >= 0);
52
+ const parsed = JSON.parse(starts.length ? text.slice(Math.min(...starts)) : text);
53
+ const rows = Array.isArray(parsed) ? parsed : (isObj(parsed) && Array.isArray(parsed.lanes) ? parsed.lanes : []);
54
+ for (const x of rows)
55
+ if (isObj(x) && Object.values(owned).includes(get(x, 'lane_id', null)))
56
+ lanes.set(get(x, 'lane_id', null), x);
57
+ }
58
+ }
59
+ catch (e) { /* no lanes */ }
60
+ }
61
+ return lanes;
62
+ }
63
+ function laneRow(worker) {
64
+ const owned = load(path.join(S, 'lanes.json')) || {};
65
+ return laneRows().get(owned[worker]) || null;
66
+ }
67
+ function laneStopped(worker, since) {
68
+ const row = laneRow(worker);
69
+ return !!row && get(get(row, 'loop', {}), 'state') === 'stopped';
70
+ }
71
+ function phaseExit(worker, sid, ph, since) {
72
+ // the lane's last phase-exit event for this phase written after it was routed
73
+ const row = laneRow(worker);
74
+ if (!row)
75
+ return null;
76
+ const state = get(get(row, 'loop', {}), 'state');
77
+ if (state === 'parked')
78
+ return { reason: 'loop_stalled', phase: ph, story_id: sid };
79
+ return state === 'failed' ? { reason: state, phase: ph, story_id: sid } : null;
80
+ }
81
+ function mtime(p) {
82
+ try {
83
+ return fs.statSync(p).mtimeMs / 1000;
84
+ }
85
+ catch (e) {
86
+ return null;
87
+ }
88
+ }
89
+ const out = [];
90
+ let active = 0;
91
+ const sd = path.join(S, 'stories');
92
+ for (const n of byteSort(fs.readdirSync(sd))) {
93
+ if (!n.endsWith('.json'))
94
+ continue;
95
+ const st = load(path.join(sd, n));
96
+ if (!isObj(st) || !('id' in st))
97
+ continue;
98
+ const sid = st.id, state = get(st, 'state', null);
99
+ if (['queued', 'in_flight', 'held', 'awaiting_recheck', 'interrupted'].includes(state))
100
+ active++;
101
+ const it = get(st, 'interrupted', null);
102
+ if (isObj(it) && truthy(it.phase) && !truthy(it.routed))
103
+ out.push(['INTERRUPTED', sid, str(it.phase), str(get(it, 'at', ''))]);
104
+ const phases = truthy(st.phases) ? st.phases : [];
105
+ const cur = get(st, 'current', 0);
106
+ if (!(Number.isInteger(cur) && cur >= 0 && cur < phases.length))
107
+ continue;
108
+ const ph = phases[cur].phase;
109
+ const worker = phases[cur].worker;
110
+ const h = path.join(S, 'handoffs', `${sid}-${ph}.json`);
111
+ if (state === 'queued' || state === 'held') {
112
+ if (fs.existsSync(h))
113
+ out.push(['ARCHIVE', sid, ph, h]);
114
+ // count attempts whose handoff said failed; blocked ones are the owner's, not a retry
115
+ const hd = path.join(S, 'handoffs');
116
+ // a retry by the owner starts the count over: archives up to fail_ack are acknowledged
117
+ const fa = get(st, 'fail_ack', null);
118
+ const ack = isObj(fa) ? Math.trunc(Number(get(fa, ph, 0) || 0)) : 0;
119
+ const pre = `${sid}-${ph}.failed.`;
120
+ const tried = fs.readdirSync(hd).filter((f) => {
121
+ if (!f.startsWith(pre) || !f.endsWith('.json'))
122
+ return false;
123
+ const mid = f.slice(pre.length, -5);
124
+ return /^\d+$/.test(mid) && parseInt(mid, 10) > ack;
125
+ }).map((f) => path.join(hd, f));
126
+ if (fs.existsSync(h))
127
+ tried.push(h);
128
+ const hs = tried.map((f) => { const x = load(f); return isObj(x) ? x : {}; });
129
+ const fails = hs.filter((x) => x.status === 'failed').length;
130
+ const early = hs.filter((x) => x.status === 'failed' && x.exit_reason === EARLY).length;
131
+ if (early >= 2)
132
+ out.push(['FAILCAP', sid, ph, String(early), EARLY]);
133
+ else if (fails >= MAX)
134
+ out.push(['FAILCAP', sid, ph, String(fails), 'failed']);
135
+ if (state === 'held') {
136
+ const dp = path.join(S, 'decisions', `${sid}-${get(st, 'held_for', worker)}.md`);
137
+ out.push(['HELD', sid, dp]);
138
+ }
139
+ }
140
+ else if (state === 'in_flight') {
141
+ const hd = load(h);
142
+ let since = get(st, 'routed_at', null);
143
+ if (!isNum(since))
144
+ since = mtime(path.join(S, 'envelopes', `${sid}-${ph}.json`));
145
+ const mine = isObj(hd) && hd.phase === ph;
146
+ const hstat = mine ? get(hd, 'status', null) : null;
147
+ let ev = null;
148
+ if (mine && TERMINAL.includes(hstat) && get(hd, 'exit_reason', null) !== EARLY) {
149
+ out.push(['ACCEPT', sid, ph, h]);
150
+ }
151
+ else if (mine && get(hd, 'exit_reason', null) === EARLY) {
152
+ let secs = get(hd, 'elapsed_s', null);
153
+ if (!isNum(secs))
154
+ secs = since !== null ? Math.trunc(now() - since) : 0;
155
+ const why = collapse(str(truthy(hd.notes) ? hd.notes : truthy(hd.summary) ? hd.summary : 'no reason given'));
156
+ out.push(['EARLY', sid, ph, String(Math.trunc(secs)), why]);
157
+ }
158
+ else if ((!mine || !TERMINAL.includes(hstat)) && (ev = phaseExit(worker, sid, ph, since)) !== null) {
159
+ let secs = get(ev, 'elapsed_s', null);
160
+ if (!isNum(secs))
161
+ secs = since !== null ? Math.trunc(now() - since) : 0;
162
+ const why = collapse(str(truthy(ev.reason) ? ev.reason : 'no reason given'));
163
+ if (get(ev, 'reason', null) === 'loop_stalled')
164
+ out.push(['LANE_STALLED', sid, ph, worker]);
165
+ else
166
+ out.push(['EARLY', sid, ph, String(Math.trunc(secs)), why]);
167
+ }
168
+ else if (laneStopped(worker, since)) {
169
+ out.push(['LANE_STOPPED', sid, ph]);
170
+ }
171
+ else {
172
+ const env = load(path.join(S, 'envelopes', `${sid}-${ph}.json`));
173
+ const dl = get(env, 'deadline', null);
174
+ if (typeof dl === 'string' && isoNow() > dl)
175
+ out.push(['DEADLINE', sid, ph, dl]);
176
+ if (since !== null)
177
+ out.push(['INFLIGHT', sid, ph, String(Math.max(0, Math.trunc(now() - since)))]);
178
+ }
179
+ }
180
+ else if (state === 'awaiting_go') {
181
+ out.push(['AWAITING_GO', sid, path.join(S, 'decisions', `${sid}-go.md`)]);
182
+ }
183
+ else if (state === 'blocked_needs_owner') {
184
+ out.push(['BLOCKED', sid, ph, path.join(S, 'decisions', `${sid}-blocked-${ph}.md`)]);
185
+ }
186
+ else if (state === 'awaiting_recheck') {
187
+ out.push(['RECHECK', sid]);
188
+ }
189
+ }
190
+ out.push(['ACTIVE', String(active)]);
191
+ return out.map((r) => r.join('\t') + '\n').join('');
192
+ }
193
+ export function checkDriverStall(stateDir, stallWindow, hqBin, run = spawnSync) {
194
+ const S = stateDir, W = stallWindow, HQ = hqBin;
195
+ const SF = path.join(S, 'driver', 'stall.json');
196
+ const isObj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
197
+ const truthy = (v) => !(v === null || v === undefined || v === false || v === 0 || v === '' ||
198
+ (Array.isArray(v) && !v.length) || (isObj(v) && !Object.keys(v).length));
199
+ const get = (o, k, d = undefined) => (isObj(o) && Object.prototype.hasOwnProperty.call(o, k) ? o[k] : d);
200
+ const byteSort = (a) => a.sort((x, y) => Buffer.compare(Buffer.from(x), Buffer.from(y)));
201
+ const cmp = (x, y) => (x < y ? -1 : x > y ? 1 : 0);
202
+ function load(p) {
203
+ try {
204
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
205
+ }
206
+ catch (e) {
207
+ return null;
208
+ }
209
+ }
210
+ function epoch(iso) {
211
+ const m = /^(\d{4})-(\d{1,2})-(\d{1,2})T(\d{1,2}):(\d{1,2}):(\d{1,2})$/.exec(String(iso).slice(0, 19));
212
+ if (!m)
213
+ return null;
214
+ const [y, mo, d, h, mi, s] = m.slice(1).map(Number);
215
+ if (mo < 1 || mo > 12 || h > 23 || mi > 59 || s > 61)
216
+ return null;
217
+ if (d < 1 || new Date(Date.UTC(y, mo - 1, d)).getUTCDate() !== d)
218
+ return null;
219
+ return Math.floor(Date.UTC(y, mo - 1, d, h, mi, s) / 1000);
220
+ }
221
+ const now = () => Date.now() / 1000;
222
+ const iso = (secs) => new Date(Math.floor(secs) * 1000).toISOString().slice(0, 19) + 'Z';
223
+ // Python json.dumps default output: ", " / ": " separators, non-ASCII escaped
224
+ function dumps(v) {
225
+ if (v === null || v === undefined)
226
+ return 'null';
227
+ if (v === true)
228
+ return 'true';
229
+ if (v === false)
230
+ return 'false';
231
+ if (typeof v === 'number') {
232
+ let s = String(v);
233
+ const m = /^(-?)(\d)(?:\.(\d+))?e([+-])(\d+)$/.exec(s);
234
+ if (m)
235
+ s = m[1] + m[2] + (m[3] ? '.' + m[3] : '') + 'e' + m[4] + m[5].padStart(2, '0');
236
+ return s;
237
+ }
238
+ if (typeof v === 'string')
239
+ return JSON.stringify(v).replace(/[\u0080-￿]/g, (c) => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'));
240
+ if (Array.isArray(v))
241
+ return '[' + v.map(dumps).join(', ') + ']';
242
+ return '{' + Object.keys(v).map((k) => dumps(k) + ': ' + dumps(v[k])).join(', ') + '}';
243
+ }
244
+ let rows = [];
245
+ try {
246
+ const owned = load(path.join(S, 'lanes.json')) || {};
247
+ const r = run(HQ, ['lanes', 'list', '--json'], { encoding: 'utf8', timeout: 30000 });
248
+ const text = String(r.stdout || ''), starts = [text.indexOf('['), text.indexOf('{')].filter((n) => n >= 0);
249
+ const parsed = JSON.parse(starts.length ? text.slice(Math.min(...starts)) : text);
250
+ const listed = Array.isArray(parsed) ? parsed : (isObj(parsed) && Array.isArray(parsed.lanes) ? parsed.lanes : []);
251
+ rows = listed.filter((row) => isObj(row) && Object.values(owned).includes(get(row, 'lane_id', null)));
252
+ }
253
+ catch (e) {
254
+ return '';
255
+ }
256
+ const inflight = [];
257
+ const sd = path.join(S, 'stories');
258
+ for (const n of byteSort(fs.readdirSync(sd))) {
259
+ const st = n.endsWith('.json') ? load(path.join(sd, n)) : null;
260
+ if (!isObj(st) || st.state !== 'in_flight')
261
+ continue;
262
+ const phases = truthy(st.phases) ? st.phases : [];
263
+ const cur = get(st, 'current', 0);
264
+ if (!(Number.isInteger(cur) && cur >= 0 && cur < phases.length))
265
+ continue;
266
+ let since = get(st, 'routed_at', null);
267
+ if (typeof since !== 'number') {
268
+ try {
269
+ since = fs.statSync(path.join(S, 'envelopes', `${st.id}-${phases[cur].phase}.json`)).mtimeMs / 1000;
270
+ }
271
+ catch (e) {
272
+ since = now();
273
+ }
274
+ }
275
+ inflight.push([st.id, phases[cur].worker, since]);
276
+ }
277
+ const live = rows.filter((r) => isObj(r) && isObj(r.loop));
278
+ let cond = false, stories = [], lanes = [], since = null;
279
+ if (inflight.length && live.length && live.every((r) => r.loop.state === 'waiting' && r.loop.queue_depth === 0)) {
280
+ cond = true;
281
+ stories = inflight.map((x) => x[0]);
282
+ lanes = live.map((r) => get(r, 'lane_id', null)).sort(cmp);
283
+ const changed = live.filter((r) => truthy(r.loop.updated_at)).map((r) => epoch(r.loop.updated_at));
284
+ since = Math.max(Math.min(...inflight.map((x) => x[2])), ...changed.filter((c) => c !== null));
285
+ }
286
+ else {
287
+ // a routed phase sits in its lane queue and the lane has nothing active
288
+ const owned = load(path.join(S, 'lanes.json')) || {};
289
+ const byWorker = new Map();
290
+ for (const r of live)
291
+ for (const [worker, lane] of Object.entries(owned))
292
+ if (lane === get(r, 'lane_id', null))
293
+ byWorker.set(worker, r);
294
+ for (const [sid, worker, s] of inflight) {
295
+ const r = byWorker.get(worker);
296
+ if (r && get(r.loop, 'queue_depth', 0) > 0 && !truthy(get(r.loop, 'active_envelope_id', null))) {
297
+ cond = true;
298
+ stories.push(sid);
299
+ lanes.push(get(r, 'lane_id', worker));
300
+ since = since === null ? s : Math.min(since, s);
301
+ }
302
+ }
303
+ }
304
+ const prev = load(SF);
305
+ if (!cond) {
306
+ if (isObj(prev) && !truthy(prev.fired))
307
+ fs.unlinkSync(SF);
308
+ return '';
309
+ }
310
+ if (isObj(prev)) {
311
+ if (truthy(prev.fired))
312
+ return '';
313
+ since = get(prev, 'since_epoch', since);
314
+ }
315
+ const sinceEpoch = Number(since);
316
+ const sinceIso = iso(sinceEpoch);
317
+ const rec = { since_epoch: since, since: sinceIso, stories, lanes: [...new Set(lanes)].sort(cmp), fired: false };
318
+ if (now() - sinceEpoch >= W) {
319
+ rec.fired = true;
320
+ fs.appendFileSync(path.join(S, 'events.jsonl'), dumps({ event: 'stall', stories, lanes: rec.lanes, since: sinceIso,
321
+ at: iso(now()) }) + '\n');
322
+ rec.fireLine = 'FIRE ' + sinceIso + ' stories=' + stories.join(',') + ' lanes=' + rec.lanes.join(',');
323
+ }
324
+ const fireLine = rec.fireLine || '';
325
+ delete rec.fireLine;
326
+ fs.writeFileSync(SF + '.tmp', dumps(rec));
327
+ fs.renameSync(SF + '.tmp', SF);
328
+ return fireLine;
329
+ }
330
+ export function acquirePipelineDriverLock(driverDir, pid) {
331
+ const lock = path.join(driverDir, "lock");
332
+ try {
333
+ fs.mkdirSync(lock);
334
+ fs.writeFileSync(path.join(lock, "pid"), `${pid}\n`);
335
+ return { acquired: true };
336
+ }
337
+ catch {
338
+ let holder = "";
339
+ try {
340
+ holder = fs.readFileSync(path.join(lock, "pid"), "utf8").trim();
341
+ }
342
+ catch { /* stale or incomplete lock */ }
343
+ const holderPid = Number.parseInt(holder, 10);
344
+ if (isPipelineDriverProcess(holderPid))
345
+ return { acquired: false };
346
+ fs.rmSync(lock, { recursive: true, force: true });
347
+ try {
348
+ fs.mkdirSync(lock);
349
+ fs.writeFileSync(path.join(lock, "pid"), `${pid}\n`);
350
+ return { acquired: true, stalePid: holder || "none" };
351
+ }
352
+ catch {
353
+ return { acquired: false };
354
+ }
355
+ }
356
+ }
357
+ export function releasePipelineDriverLock(driverDir) {
358
+ fs.rmSync(path.join(driverDir, "lock"), { recursive: true, force: true });
359
+ }
360
+ // Exact `usage()` output from the pinned pipeline-driver.sh companion.
361
+ const DRIVER_USAGE = inflateRawSync(Buffer.from("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", "base64")).toString("utf8");
362
+ function parseDriverArgs(args) {
363
+ const values = { prd: "", state: "", worktrees: [], storyBranches: false, allowReposWorktree: false,
364
+ table: "", interval: "15", maxFails: "2", stallWindow: process.env.PIPELINE_DRIVER_STALL_SECS || "600" };
365
+ for (let index = 0; index < args.length;) {
366
+ const arg = args[index] ?? "";
367
+ if (["--prd", "--state", "--worktree", "--interval", "--max-phase-fails", "--stall-window", "--table"].includes(arg)) {
368
+ if (index + 1 >= args.length)
369
+ return { error: `pipeline-driver: ${arg} needs a value` };
370
+ const value = args[index + 1] ?? "";
371
+ if (arg === "--prd")
372
+ values.prd = value;
373
+ else if (arg === "--state")
374
+ values.state = value;
375
+ else if (arg === "--worktree")
376
+ values.worktrees.push(value);
377
+ else if (arg === "--interval")
378
+ values.interval = value;
379
+ else if (arg === "--max-phase-fails")
380
+ values.maxFails = value;
381
+ else if (arg === "--stall-window")
382
+ values.stallWindow = value;
383
+ else
384
+ values.table = value;
385
+ index += 2;
386
+ continue;
387
+ }
388
+ if (arg === "--story-branches")
389
+ values.storyBranches = true;
390
+ else if (arg === "--allow-repos-worktree")
391
+ values.allowReposWorktree = true;
392
+ else if (arg === "-h" || arg === "--help")
393
+ return { usage: true };
394
+ else
395
+ return { error: `pipeline-driver: unknown argument: ${arg}` };
396
+ index += 1;
397
+ }
398
+ if (!values.prd || !values.state)
399
+ return { usage: true };
400
+ if (!/^(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)$/.test(values.interval) || values.interval === ".")
401
+ return { error: "pipeline-driver: --interval must be a number of seconds" };
402
+ if (!/^[0-9]+$/.test(values.maxFails) || values.maxFails === "0")
403
+ return { error: "pipeline-driver: --max-phase-fails must be a positive integer" };
404
+ if (!/^[0-9]+$/.test(values.stallWindow) || values.stallWindow === "0")
405
+ return { error: "pipeline-driver: --stall-window must be a positive number of seconds" };
406
+ if (!fs.existsSync(values.prd) || !fs.statSync(values.prd).isFile())
407
+ return { error: `pipeline-driver: no such prd: ${values.prd}` };
408
+ return { options: { ...values, interval: Number(values.interval), maxFails: Number.parseInt(values.maxFails, 10), stallWindow: Number.parseInt(values.stallWindow, 10) } };
409
+ }
410
+ let driverHqRoot = "";
411
+ export function pipelineDriverChildEnvironment(base, hqRoot = driverHqRoot) {
412
+ return hqRoot ? { ...base, PC_HQ_ROOT: hqRoot } : base;
413
+ }
414
+ function driverRun(command, args, timeout = 30_000) {
415
+ const env = pipelineDriverChildEnvironment(process.env);
416
+ const result = spawnSync(command, args, { encoding: "utf8", timeout, stdio: ["ignore", "pipe", "pipe"], maxBuffer: 1 << 30, env });
417
+ return {
418
+ code: result.signal ? 128 + ({ SIGINT: 2, SIGTERM: 15 }[result.signal] ?? 0) : result.status ?? 1,
419
+ stdout: String(result.stdout ?? ""), stderr: String(result.stderr ?? result.error?.message ?? ""),
420
+ };
421
+ }
422
+ function readJson(file) {
423
+ try {
424
+ return JSON.parse(fs.readFileSync(file, "utf8"));
425
+ }
426
+ catch {
427
+ return null;
428
+ }
429
+ }
430
+ function parseJsonOutput(text) {
431
+ const starts = [text.indexOf("["), text.indexOf("{")].filter((n) => n >= 0);
432
+ return JSON.parse(starts.length ? text.slice(Math.min(...starts)) : text);
433
+ }
434
+ function writeDriverLog(file, message) {
435
+ fs.appendFileSync(file, `${new Date().toISOString().replace(/\.\d{3}Z$/, "Z")} ${message}\n`);
436
+ }
437
+ function readDriverLines(state, maxFails, hq) {
438
+ return scanDriverState(state, maxFails, hq).split("\n").filter(Boolean);
439
+ }
440
+ function parseScanLine(line) { return line.split("\t"); }
441
+ export function failcapExplanation(story, phase, attempts, maxFails, earlyExit, reason, decision) {
442
+ const context = earlyExit
443
+ ? `phase ${story}/${phase}: engine_exited_early in ${attempts} of ${attempts} attempt(s); the lane was restarted once in place and the engine exited early again`
444
+ : `phase ${story}/${phase} returned handoff status failed in ${attempts} of ${attempts} attempt(s) (limit ${maxFails})`;
445
+ return `${context}; reason: ${reason}; held for the owner (decision: ${decision}); the parent decides whether to retry, change the story, or stop`;
446
+ }
447
+ export function stalledLaneContext(state, worker) {
448
+ const run = readJson(path.join(state, "run.json")) || {};
449
+ const laneMap = readJson(path.join(state, "lanes.json")) || {};
450
+ return {
451
+ lane: typeof laneMap[worker] === "string" ? laneMap[worker] : "",
452
+ company: typeof run.company_slug === "string" ? run.company_slug : "",
453
+ };
454
+ }
455
+ export function pendingLaneQuestion(text, lane) {
456
+ const parsed = parseJsonOutput(text);
457
+ const rows = Array.isArray(parsed) ? parsed : parsed?.questions;
458
+ const pending = Array.isArray(rows) ? rows.find((item) => item?.lane_id === lane && item?.status === "pending") : undefined;
459
+ return pending ? JSON.stringify(pending) : "";
460
+ }
461
+ function nextArchiveName(state, story, phase) {
462
+ let count = 1;
463
+ while (fs.existsSync(path.join(state, "handoffs", `${story}-${phase}.failed.${count}.json`)))
464
+ count += 1;
465
+ return path.join(state, "handoffs", `${story}-${phase}.failed.${count}.json`);
466
+ }
467
+ export async function sleep(ms, signal) {
468
+ await new Promise((resolve) => {
469
+ if (signal?.aborted) {
470
+ resolve();
471
+ return;
472
+ }
473
+ let timer;
474
+ const finish = () => {
475
+ clearTimeout(timer);
476
+ signal?.removeEventListener("abort", finish);
477
+ resolve();
478
+ };
479
+ timer = setTimeout(finish, ms);
480
+ signal?.addEventListener("abort", finish, { once: true });
481
+ });
482
+ }
483
+ function writeEarlyHandoff(state, storyId, phase, seconds, reason) {
484
+ const story = readJson(path.join(state, "stories", `${storyId}.json`));
485
+ const storyRecord = story && typeof story === "object" ? story : {};
486
+ const current = Number.isInteger(storyRecord.current) ? storyRecord.current : 0;
487
+ const worker = storyRecord.phases?.[current]?.worker ?? "None";
488
+ const handoffPath = path.join(state, "handoffs", `${storyId}-${phase}.json`);
489
+ const loaded = readJson(handoffPath);
490
+ const handoff = loaded && typeof loaded === "object" && loaded.phase === phase ? loaded : {};
491
+ const elapsed = Number(seconds);
492
+ const record = {
493
+ ...handoff,
494
+ schema: "hq-phase-handoff/v1",
495
+ story_id: storyId,
496
+ phase,
497
+ worker_id: handoff.worker_id || worker,
498
+ status: "failed",
499
+ exit_reason: "engine_exited_early",
500
+ elapsed_s: elapsed < 0 ? Math.ceil(elapsed) : Math.floor(elapsed),
501
+ };
502
+ const summary = record.summary === undefined || record.summary === null ? "None" : String(record.summary);
503
+ if (!summary.startsWith("engine_exited_early"))
504
+ record.summary = `engine_exited_early after ${seconds}s: ${reason}`;
505
+ if (!Object.hasOwn(record, "files_changed"))
506
+ record.files_changed = [];
507
+ if (!Object.hasOwn(record, "commits"))
508
+ record.commits = [];
509
+ if (!Object.hasOwn(record, "back_pressure"))
510
+ record.back_pressure = { tests: "skip", lint: "skip", typecheck: "skip", build: "skip" };
511
+ if (!Object.hasOwn(record, "context_for_next"))
512
+ record.context_for_next = "";
513
+ if (!Object.hasOwn(record, "notes"))
514
+ record.notes = reason;
515
+ fs.writeFileSync(handoffPath, `${JSON.stringify(record)}\n`);
516
+ return handoffPath;
517
+ }
518
+ function driverTimestamp() {
519
+ return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
520
+ }
521
+ function laneRowsFromHq(hq) {
522
+ const result = driverRun(hq, ["lanes", "list", "--json"]);
523
+ if (result.code !== 0)
524
+ throw new Error(`hq lanes list --json exited ${result.code}: ${result.stderr.trim()}`);
525
+ return extractLaneRows(result.stdout);
526
+ }
527
+ function extractLaneRows(text) {
528
+ const parsed = parseJsonOutput(text);
529
+ if (Array.isArray(parsed))
530
+ return parsed;
531
+ if (parsed && Array.isArray(parsed.lanes))
532
+ return parsed.lanes;
533
+ throw new Error("hq lanes list --json returned an unexpected JSON shape");
534
+ }
535
+ export async function stopDriverLanes(state, hq, logFile, options = {}) {
536
+ const laneFile = path.join(state, "lanes.json");
537
+ if (!fs.existsSync(laneFile))
538
+ return true;
539
+ const owned = readJson(laneFile) || {};
540
+ const laneIds = Object.values(owned).filter((lane) => typeof lane === "string" && lane.length > 0);
541
+ for (const lane of laneIds)
542
+ (options.stop ?? ((id) => { driverRun(hq, ["lanes", "stop", id]); }))(lane);
543
+ const deadline = Date.now() + (options.timeoutMs ?? 60_000);
544
+ while (Date.now() < deadline) {
545
+ let rows;
546
+ try {
547
+ rows = options.query ? options.query() : laneRowsFromHq(hq);
548
+ }
549
+ catch (error) {
550
+ writeDriverLog(logFile, `LANE_LIST_FAILED ${error instanceof Error ? error.message : String(error)}; retaining lane mappings`);
551
+ await sleep(options.pollMs ?? 2000);
552
+ continue;
553
+ }
554
+ const allStopped = laneIds.every((lane) => {
555
+ const row = rows.find((item) => item?.lane_id === lane);
556
+ return !row || row.loop?.state === "stopped";
557
+ });
558
+ if (allStopped) {
559
+ fs.writeFileSync(laneFile, "{}\n");
560
+ return true;
561
+ }
562
+ await sleep(options.pollMs ?? 2000);
563
+ }
564
+ writeDriverLog(logFile, "LANE_STOP_TIMEOUT unable to confirm all mapped lanes stopped or absent within 60s");
565
+ return false;
566
+ }
567
+ export async function runPipelineDriverNative(args, hqRoot = "") {
568
+ const parsed = parseDriverArgs(args);
569
+ if (parsed.usage) {
570
+ process.stderr.write(DRIVER_USAGE);
571
+ return 2;
572
+ }
573
+ if (parsed.error) {
574
+ process.stderr.write(`${parsed.error}\n`);
575
+ return 2;
576
+ }
577
+ const opts = parsed.options;
578
+ driverHqRoot = hqRoot;
579
+ const hq = process.env.PC_HQ || "hq";
580
+ const stateInput = path.resolve(opts.state);
581
+ fs.mkdirSync(stateInput, { recursive: true });
582
+ for (const name of ["stories", "handoffs", "envelopes", "driver"])
583
+ fs.mkdirSync(path.join(stateInput, name), { recursive: true });
584
+ const state = fs.realpathSync(stateInput);
585
+ const driverDir = path.join(state, "driver");
586
+ const logFile = path.join(driverDir, "driver.log");
587
+ process.title = "pipeline-driver";
588
+ const lockResult = acquirePipelineDriverLock(driverDir, process.pid);
589
+ if (!lockResult.acquired) {
590
+ let holder = "";
591
+ try {
592
+ holder = fs.readFileSync(path.join(driverDir, "lock", "pid"), "utf8").trim();
593
+ }
594
+ catch { /* report empty holder */ }
595
+ process.stderr.write(`pipeline-driver: another driver (pid ${holder}) runs against ${state}\n`);
596
+ writeDriverLog(logFile, `REFUSED second copy pid=${process.pid} holder=${holder}`);
597
+ return 24;
598
+ }
599
+ if (lockResult.stalePid !== undefined)
600
+ writeDriverLog(logFile, `STALE_LOCK pid=${lockResult.stalePid}; taking it over`);
601
+ fs.writeFileSync(path.join(driverDir, "driver.pid"), `${process.pid}\n`);
602
+ fs.rmSync(path.join(driverDir, "exit"), { force: true });
603
+ const finish = (code, reason) => {
604
+ fs.writeFileSync(path.join(driverDir, "exit"), `${code} ${reason}\n`);
605
+ writeDriverLog(logFile, `EXIT ${code} ${reason}`);
606
+ releasePipelineDriverLock(driverDir);
607
+ fs.rmSync(path.join(driverDir, "driver.pid"), { force: true });
608
+ return code;
609
+ };
610
+ const runPc = (pcArgs) => driverRun(hq, ["core", "pipeline", "conductor", ...pcArgs]);
611
+ const runTick = () => {
612
+ const pcArgs = ["tick", "--prd", opts.prd, "--state", state];
613
+ if (opts.storyBranches)
614
+ pcArgs.push("--story-branches");
615
+ if (opts.allowReposWorktree)
616
+ pcArgs.push("--allow-repos-worktree");
617
+ if (opts.table)
618
+ pcArgs.push("--table", opts.table);
619
+ for (const worktree of opts.worktrees)
620
+ pcArgs.push("--worktree", worktree);
621
+ return runPc(pcArgs);
622
+ };
623
+ let stopSignal = "";
624
+ const stopWait = new AbortController();
625
+ const onTerm = () => { stopSignal = "terminated"; stopWait.abort(); };
626
+ const onInt = () => { stopSignal = "interrupted"; stopWait.abort(); };
627
+ process.on("SIGTERM", onTerm);
628
+ process.on("SIGINT", onInt);
629
+ const removeHandlers = () => { process.off("SIGTERM", onTerm); process.off("SIGINT", onInt); };
630
+ const stop = async (code, reason) => {
631
+ const result = runPc(["interrupt", "--state", state, "--note", reason]);
632
+ writeDriverLog(logFile, `INTERRUPT rc=${result.code}: ${result.stdout.replace(/[\r\n]+/g, " ")}`);
633
+ await stopDriverLanes(state, hq, logFile);
634
+ fs.rmSync(path.join(driverDir, "stop.json"), { force: true });
635
+ removeHandlers();
636
+ return finish(code, reason);
637
+ };
638
+ writeDriverLog(logFile, `START pid=${process.pid} prd=${opts.prd} interval=${opts.interval}s max_phase_fails=${opts.maxFails} stall_window=${opts.stallWindow}s`);
639
+ const stopFile = path.join(driverDir, "stop.json");
640
+ if (fs.existsSync(stopFile)) {
641
+ writeDriverLog(logFile, "STALE_STOP removed driver/stop.json left from an earlier stop");
642
+ fs.rmSync(stopFile, { force: true });
643
+ }
644
+ try {
645
+ let goLast = "";
646
+ for (;;) {
647
+ if (stopSignal) {
648
+ const signal = stopSignal;
649
+ return await stop(signal === "terminated" ? 143 : 130, signal);
650
+ }
651
+ if (fs.existsSync(stopFile)) {
652
+ const stopText = fs.readFileSync(stopFile, "utf8").replace(/[\r\n]+/g, " ");
653
+ return await stop(29, `stopped on request: ${stopText}`);
654
+ }
655
+ let acted = 0;
656
+ let active = "";
657
+ let held = "";
658
+ let gateDue = false;
659
+ let blockedItems = "";
660
+ let goItems = "";
661
+ let interruptedItems = "";
662
+ for (let scanCount = 0; scanCount < 100; scanCount += 1) {
663
+ acted = 0;
664
+ active = "";
665
+ for (const line of readDriverLines(state, opts.maxFails, hq)) {
666
+ const [action, a = "", b = "", c = "", d = "", e = ""] = parseScanLine(line);
667
+ if (action === "ACTIVE")
668
+ active = a;
669
+ else if (action === "ARCHIVE") {
670
+ const target = nextArchiveName(state, a, b);
671
+ fs.renameSync(c, target);
672
+ writeDriverLog(logFile, `ARCHIVE ${a} ${b} -> ${path.basename(target)}`);
673
+ acted = 1;
674
+ }
675
+ else if (action === "FAILCAP") {
676
+ const result = runPc(["failcap", "--state", state, "--story", a]);
677
+ writeDriverLog(logFile, `FAILCAP_HOLD ${a} ${b} rc=${result.code}: ${result.stdout.trim()}`);
678
+ if (!(result.code === 10 && (/^PHASE_FAILCAP/.test(result.stdout) || /ALREADY/.test(result.stdout)))) {
679
+ return finish(23, `failcap ${a} ${b}: unexpected (rc=${result.code}): ${result.stdout} ${result.stderr}`);
680
+ }
681
+ const story = readJson(path.join(state, "stories", `${a}.json`));
682
+ const reason = String(story?.blocked?.text ?? "").replace(/[\r\n\t]+/g, " ");
683
+ return finish(21, failcapExplanation(a, b, c, opts.maxFails, e === "engine_exited_early", reason, result.stdout.split(/\s+/).at(-1) ?? ""));
684
+ }
685
+ else if (action === "EARLY") {
686
+ writeDriverLog(logFile, `TICK: EARLY_EXIT ${a}/${b} after ${c}s: engine_exited_early (${d})`);
687
+ const handoff = writeEarlyHandoff(state, a, b, c, d);
688
+ fs.rmSync(path.join(driverDir, "stall.json"), { force: true });
689
+ const result = runPc(["accept", "--state", state, "--story", a, "--handoff", handoff]);
690
+ writeDriverLog(logFile, `ACCEPT ${a} ${b} rc=${result.code}: ${result.stdout.trim()}`);
691
+ if (!(result.code === 1 && result.stdout.startsWith("PHASE_FAILED")))
692
+ return finish(23, `accept early exit ${a} ${b}: unexpected (rc=${result.code}): ${result.stdout} ${result.stderr}`);
693
+ acted = 1;
694
+ }
695
+ else if (action === "ACCEPT") {
696
+ fs.rmSync(path.join(driverDir, "stall.json"), { force: true });
697
+ writeDriverLog(logFile, `ACCEPTING ${a} ${b} ${c}`);
698
+ const result = runPc(["accept", "--state", state, "--story", a, "--handoff", c]);
699
+ writeDriverLog(logFile, `ACCEPT ${a} ${b} rc=${result.code}: ${result.stdout.trim()}`);
700
+ if (!((result.code === 0 && (/^(NEXT|RECHECK)/.test(result.stdout))) || (result.code === 1 && /^PHASE_/.test(result.stdout)) || (result.code === 10 && /^PHASE_BLOCKED/.test(result.stdout)))) {
701
+ return finish(23, `accept ${a} ${b}: unexpected (rc=${result.code}): ${result.stdout} ${result.stderr}`);
702
+ }
703
+ acted = 1;
704
+ }
705
+ else if (action === "RECHECK") {
706
+ writeDriverLog(logFile, `RECHECKING ${a}`);
707
+ const result = runPc(["recheck", "--prd", opts.prd, "--state", state, "--story", a]);
708
+ writeDriverLog(logFile, `RECHECK ${a} rc=${result.code}: ${result.stdout.replace(/[\r\n]+/g, " ")}`);
709
+ if (result.code === 0 && result.stdout.startsWith("VERIFIED")) {
710
+ acted = 1;
711
+ gateDue ||= result.stdout.includes("RUN_GATE");
712
+ }
713
+ else if (result.code === 1 && (/^ROUTED_BACK/.test(result.stdout) || /^FAILED/.test(result.stdout)))
714
+ acted = 1;
715
+ else
716
+ return finish(23, `recheck ${a}: unexpected (rc=${result.code}): ${result.stdout} ${result.stderr}`);
717
+ }
718
+ else if (action === "DEADLINE")
719
+ return finish(22, `phase ${a}/${b} passed its deadline ${c} with no handoff`);
720
+ else if (action === "HELD")
721
+ held = `${a} ${b}`;
722
+ else if (action === "BLOCKED")
723
+ blockedItems += `${blockedItems ? "; " : ""}${a}/${b} (decision: ${c})`;
724
+ else if (action === "AWAITING_GO")
725
+ goItems += `${goItems ? ", " : ""}${a} (decision: ${b})`;
726
+ else if (action === "INTERRUPTED")
727
+ interruptedItems += `${interruptedItems ? ", " : ""}${a} at ${b} (${c})`;
728
+ else if (action === "LANE_STOPPED") {
729
+ const result = runPc(["interrupt", "--state", state, "--story", a, "--note", `the ${b} lane exited on a stop envelope`]);
730
+ writeDriverLog(logFile, `LANE_STOPPED ${a} ${b} rc=${result.code}: ${result.stdout.trim()}`);
731
+ acted = 1;
732
+ }
733
+ else if (action === "INFLIGHT") { /* wait for this phase */ }
734
+ else if (action === "LANE_STALLED") {
735
+ const worker = c;
736
+ const { lane, company } = stalledLaneContext(state, worker);
737
+ const listed = lane && company
738
+ ? driverRun(hq, ["lanes", "questions", "list", "--company", company, "--json"])
739
+ : { code: 1, stdout: "", stderr: "" };
740
+ let question = "";
741
+ if (listed.code === 0) {
742
+ try {
743
+ question = pendingLaneQuestion(listed.stdout, lane);
744
+ }
745
+ catch { /* The shell reports no pending question for invalid JSON too. */ }
746
+ }
747
+ const detail = question || "no pending question returned";
748
+ writeDriverLog(logFile, `LANE_QUESTION story=${a} phase=${b} lane=${lane} question=${detail}`);
749
+ return finish(28, `loop lane ${lane} parked after repeated stalls for ${a}/${b}; answer its pending question with hq lanes questions answer, then restart the driver: ${detail}`);
750
+ }
751
+ else
752
+ return finish(23, `scan printed an unknown line: ${action}`);
753
+ }
754
+ if (!active)
755
+ return finish(23, `could not read the state dir ${state}`);
756
+ if (gateDue)
757
+ return finish(20, "regression gate due");
758
+ if (!acted)
759
+ break;
760
+ if (scanCount === 99)
761
+ return finish(23, "state did not settle after 100 scans");
762
+ }
763
+ if (held)
764
+ return finish(21, `story held for approval: ${held}`);
765
+ const fire = checkDriverStall(state, opts.stallWindow, hq);
766
+ if (fire) {
767
+ const parts = fire.split(" ");
768
+ writeDriverLog(logFile, `STALL since ${parts[1]} ${parts[2]} ${parts[3]}: no phase accepted for the whole ${opts.stallWindow}s stall window; event in ${path.join(state, "events.jsonl")}`);
769
+ return finish(28, `stall: since ${parts[1]}, ${parts[2]} ${parts[3]}, no phase accepted for ${opts.stallWindow}s and no lane is working on them (event: ${path.join(state, "events.jsonl")}); check the lanes, then restart the driver`);
770
+ }
771
+ if (fs.existsSync(stopFile))
772
+ return await stop(29, `stopped on request: ${fs.readFileSync(stopFile, "utf8").replace(/[\r\n]+/g, " ")}`);
773
+ const tick = runTick();
774
+ const goNow = readDriverLines(state, opts.maxFails, hq).filter((line) => line.startsWith("AWAITING_GO\t")).map((line) => parseScanLine(line)[1]).join(" ");
775
+ if (tick.stdout || goNow !== goLast || interruptedItems)
776
+ writeDriverLog(logFile, `TICK: ${tick.stdout.replace(/[\r\n]+/g, " ")}${goNow ? `| awaiting go: ${goNow}` : ""}${interruptedItems ? `| interrupted, routing first: ${interruptedItems}` : ""}`);
777
+ goLast = goNow;
778
+ if (![0, 3, 11, 12].includes(tick.code))
779
+ return finish(23, `tick failed (rc=${tick.code}): ${tick.stderr}`);
780
+ let gate = "";
781
+ let laneDown = "";
782
+ let noLane = "";
783
+ held = "";
784
+ for (const line of tick.stdout.split(/\r?\n/).filter(Boolean)) {
785
+ if (["", "RUN_GATE", "GATE_FAILED"].includes(line)) {
786
+ if (line)
787
+ gate = line;
788
+ continue;
789
+ }
790
+ if (/^(ROUTED|RETRY|PARKED_DEPENDENCY|RELEASED_DEPENDENCY|CUT|REUSED|EXISTS|MAX_STORIES|BLOCKED_OWNER|AWAITING_GO)\b/.test(line))
791
+ continue;
792
+ if (line.startsWith("HELD ")) {
793
+ held ||= line.slice(5);
794
+ continue;
795
+ }
796
+ if (line.startsWith("LANE_DOWN ")) {
797
+ laneDown ||= line.slice(10);
798
+ continue;
799
+ }
800
+ if (line.startsWith("NO_LANE ")) {
801
+ noLane ||= line.slice(8);
802
+ continue;
803
+ }
804
+ return finish(23, `tick printed an unexpected line: ${line}`);
805
+ }
806
+ if (laneDown)
807
+ return finish(26, `lane down: ${laneDown}; relaunch the named loop lane, then restart the driver`);
808
+ if (noLane)
809
+ return finish(27, `no lane: ${noLane}; add the row, launch the named loop lane, then restart the driver`);
810
+ if (gate === "RUN_GATE")
811
+ return finish(20, "regression gate due");
812
+ if (gate === "GATE_FAILED")
813
+ return finish(21, `regression gate failed; record a failed gate result for the story, then restart the driver`);
814
+ if (held)
815
+ return finish(21, `story held for approval: ${held}`);
816
+ if (!tick.stdout) {
817
+ const afterTick = readDriverLines(state, opts.maxFails, hq);
818
+ const currentActive = afterTick.map(parseScanLine).find((row) => row[0] === "ACTIVE")?.[1];
819
+ if (currentActive === "0") {
820
+ const next = runPc(["next", "--prd", opts.prd, "--state", state]);
821
+ if (next.code === 0 && !next.stdout) {
822
+ const report = runPc(["report", "final", "--state", state]);
823
+ writeDriverLog(logFile, `REPORT ${report.stdout.trim()}`);
824
+ if (blockedItems || goItems)
825
+ return finish(21, `decision needed, nothing else can move:${blockedItems ? ` blocked by its worker: ${blockedItems}` : ""}${goItems ? `; awaiting go: ${goItems}; release with pipeline-conductor.sh go --state ${state} --story <id>` : ""}`);
826
+ if (!await stopDriverLanes(state, hq, logFile))
827
+ return finish(23, "could not confirm every loop lane stopped; see LANE_STOP_TIMEOUT");
828
+ return finish(0, `all stories finished: ${report.stdout.trim()}`);
829
+ }
830
+ }
831
+ }
832
+ await sleep(opts.interval * 1000, stopWait.signal);
833
+ }
834
+ }
835
+ catch (error) {
836
+ return finish(23, `driver failed: ${error instanceof Error ? error.message : String(error)}`);
837
+ }
838
+ finally {
839
+ removeHandlers();
840
+ driverHqRoot = "";
841
+ }
842
+ }
843
+ //# sourceMappingURL=core-pipeline-driver-native.js.map