@rasensio/aidlc 1.62.0 → 1.64.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.
Files changed (39) hide show
  1. package/dist/autopilot/driver.d.ts +25 -1
  2. package/dist/autopilot/driver.d.ts.map +1 -1
  3. package/dist/autopilot/driver.js +155 -52
  4. package/dist/autopilot/driver.js.map +1 -1
  5. package/dist/autopilot/notify.d.ts +33 -0
  6. package/dist/autopilot/notify.d.ts.map +1 -0
  7. package/dist/autopilot/notify.js +66 -0
  8. package/dist/autopilot/notify.js.map +1 -0
  9. package/dist/autopilot/run.d.ts +14 -1
  10. package/dist/autopilot/run.d.ts.map +1 -1
  11. package/dist/autopilot/run.js +44 -7
  12. package/dist/autopilot/run.js.map +1 -1
  13. package/dist/autopilot/state.d.ts +19 -0
  14. package/dist/autopilot/state.d.ts.map +1 -1
  15. package/dist/autopilot/state.js.map +1 -1
  16. package/dist/autopilot/team.d.ts +2 -1
  17. package/dist/autopilot/team.d.ts.map +1 -1
  18. package/dist/autopilot/team.js +6 -1
  19. package/dist/autopilot/team.js.map +1 -1
  20. package/dist/autopilot/watch.d.ts +58 -0
  21. package/dist/autopilot/watch.d.ts.map +1 -0
  22. package/dist/autopilot/watch.js +165 -0
  23. package/dist/autopilot/watch.js.map +1 -0
  24. package/dist/commands/autopilot.d.ts +10 -4
  25. package/dist/commands/autopilot.d.ts.map +1 -1
  26. package/dist/commands/autopilot.js +42 -16
  27. package/dist/commands/autopilot.js.map +1 -1
  28. package/dist/commands/spec.d.ts.map +1 -1
  29. package/dist/commands/spec.js +3 -1
  30. package/dist/commands/spec.js.map +1 -1
  31. package/dist/spec/delta.d.ts.map +1 -1
  32. package/dist/spec/delta.js +11 -2
  33. package/dist/spec/delta.js.map +1 -1
  34. package/dist/spec/fold.d.ts.map +1 -1
  35. package/dist/spec/fold.js +6 -0
  36. package/dist/spec/fold.js.map +1 -1
  37. package/dist/spec/types.d.ts +2 -0
  38. package/dist/spec/types.d.ts.map +1 -1
  39. package/package.json +2 -2
@@ -15,7 +15,8 @@
15
15
  *
16
16
  * @module
17
17
  */
18
- import type { AutopilotConfig, AutopilotItem, AutopilotPaths, AutopilotState } from './state.js';
18
+ import type { Notice } from './notify.js';
19
+ import type { AutopilotConfig, AutopilotItem, AutopilotPaths, AutopilotState, ItemStep } from './state.js';
19
20
  import { type PausePhase, type Stops } from './stops.js';
20
21
  export interface RunResult {
21
22
  status: number;
@@ -55,6 +56,11 @@ export interface DriverCtx {
55
56
  exists(path: string): boolean;
56
57
  write(path: string, text: string): void;
57
58
  };
59
+ /**
60
+ * Tell a person something needs them or is done. Absent sends nothing: only a
61
+ * background run sets it (autopilot-live-progress/AC-20, AC-23).
62
+ */
63
+ notify?(notice: Notice): void;
58
64
  }
59
65
  /** How many builds a run may still start, and what to tell it (autopilot-on-demand/AC-7). */
60
66
  export interface Budget {
@@ -85,6 +91,8 @@ export declare function conflictPrompt(item: {
85
91
  }): string;
86
92
  /** The stops an item is held to: those recorded when it started (autopilot-stop-controls/AC-6). */
87
93
  export declare function itemStops(item: AutopilotItem): Stops;
94
+ /** The instance's `current_phase` in its worktree, or null when unreadable. Shared with the view. */
95
+ export declare function instancePhase(read: (path: string) => string | null, item: Pick<AutopilotItem, 'name' | 'worktree'>): string | null;
88
96
  /**
89
97
  * Return every item whose failure is resumable to the step it failed in, and
90
98
  * clear the error. Called when a run starts: asking to run is asking to retry
@@ -112,5 +120,21 @@ export type ApproveOutcome = {
112
120
  export declare function approveItem(state: AutopilotState, name: string): ApproveOutcome;
113
121
  /** One pass. Returns the new state; the caller saves it (not in a dry run). */
114
122
  export declare function tick(ctx: DriverCtx, state: AutopilotState, budget?: Budget): AutopilotState;
123
+ /** Each item's step by name, to compare against after a change (autopilot-live-progress/AC-11). */
124
+ export declare function stepsOf(state: AutopilotState): Map<string, ItemStep>;
125
+ /**
126
+ * Stamp `step_since` on every item whose step differs from `before`; a new item has
127
+ * no entry there and is stamped too. Diffing here, rather than at each of the
128
+ * fourteen places a step is set, means a new handler cannot forget it.
129
+ */
130
+ export declare function markStepChanges(state: AutopilotState, before: Map<string, ItemStep>, now: number): void;
131
+ /**
132
+ * The driver's line, shared by `status` and the watch view.
133
+ *
134
+ * @param stopped - whether the STOP file exists.
135
+ * @param lastPass - how to show the last pass's time; `status` shows it as stored.
136
+ */
137
+ export declare function driverLine(stopped: boolean, state: AutopilotState, now: number, lastPass?: (iso: string) => string): string;
138
+ export declare function queueLine(state: AutopilotState): string | null;
115
139
  export declare function statusLines(ctx: Pick<DriverCtx, 'fs' | 'paths' | 'now'>, state: AutopilotState): string[];
116
140
  //# sourceMappingURL=driver.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/autopilot/driver.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/autopilot/driver.ts"],"names":[],"mappings":"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"}
@@ -49,6 +49,28 @@ function makeIO(ctx) {
49
49
  return { call, cli };
50
50
  }
51
51
  const fail = (r) => (r.stderr || r.stdout || `exit ${r.status}`).trim().split('\n').slice(-1)[0];
52
+ /**
53
+ * Why `aidlc transition --json` refused: each violation, where `fail` alone would keep
54
+ * the last line — a "next step" naming no cause, which is all a halted queue logged
55
+ * for 2.7 hours on 2026-10-05 (empty-spec-delta-fails-after-release/AC-7, AC-8).
56
+ * Read from the JSON payload, the machine contract, not the human block.
57
+ */
58
+ function transitionFailure(r) {
59
+ if (r.status === 0)
60
+ return r;
61
+ try {
62
+ const { violations } = JSON.parse(r.stdout);
63
+ if (Array.isArray(violations) && violations.length > 0) {
64
+ // One line: `fail` keeps only the last, and a wrapped reason would lose its start.
65
+ const text = violations.map((v) => `[${v.gate}] ${v.reason}`).join('; ');
66
+ return { ...r, stderr: text.replace(/\s*\n\s*/g, ' ') };
67
+ }
68
+ }
69
+ catch {
70
+ // Not the payload — an error before any gate ran is on stderr already.
71
+ }
72
+ return r;
73
+ }
52
74
  /**
53
75
  * @param pauses - `AIDLC_PAUSE_AFTER` as the agent will see it. Empty adds nothing,
54
76
  * so an item with no pauses gets the prompt it always did.
@@ -74,18 +96,49 @@ export function conflictPrompt(item) {
74
96
  `\`git fetch origin main\` and \`git merge origin/main\` on this branch, resolve every conflict keeping ` +
75
97
  `the intent of both sides, run the tests, commit the merge, and stop. Do not push, merge or release.`);
76
98
  }
99
+ /**
100
+ * Record `text` as the item's last event for the view. The same text again keeps
101
+ * the time it was first said: "waiting for checks to start, 12m ago"
102
+ * (autopilot-live-progress/AC-12).
103
+ */
104
+ function record(ctx, item, text) {
105
+ if (item.last_event?.text === text)
106
+ return;
107
+ item.last_event = { at: new Date(ctx.now()).toISOString(), text };
108
+ }
109
+ /** Say a line about an item — the same line as ever — and record it. */
110
+ function tell(ctx, item, text) {
111
+ ctx.say(`${item.name}: ${text}`);
112
+ record(ctx, item, text);
113
+ }
114
+ /**
115
+ * Notices go out at a transition, never from an observation a pass repeats, so a
116
+ * queue halted for five hours notifies once with no record of what was sent
117
+ * (autopilot-live-progress/AC-16).
118
+ */
119
+ function notice(ctx, title, message) {
120
+ ctx.notify?.({ title: `autopilot: ${title}`, message });
121
+ }
77
122
  /**
78
123
  * Put an item in `error`. `resume` names the step `aidlc autopilot run` returns
79
124
  * it to; leave it out for a failure at or after the merge, which stays a
80
125
  * person's call (autopilot-merge-lane-stalls-on-conflicts/AC-14, AC-16).
126
+ *
127
+ * Never reached for an item already in `error` — handlers are picked by step and
128
+ * `serveQueue` returns on an errored head — so this notifies once per failure
129
+ * (autopilot-live-progress/AC-15).
81
130
  */
82
- function failItem(item, error, resume) {
131
+ function failItem(ctx, state, item, error, resume) {
83
132
  item.step = 'error';
84
133
  item.error = error;
85
134
  if (resume)
86
135
  item.resume_step = resume;
87
136
  else
88
137
  delete item.resume_step;
138
+ // Recorded, not said: half the callers print nothing today, and what is printed must not change.
139
+ record(ctx, item, error);
140
+ const title = error.startsWith('release failed') ? 'release failed' : state.queue.includes(item.name) ? 'queue halted' : 'item stopped';
141
+ notice(ctx, title, `${item.name}: ${error}`);
89
142
  }
90
143
  function dequeue(state, item) {
91
144
  state.queue = state.queue.filter((n) => n !== item.name);
@@ -120,7 +173,7 @@ function launchAgent(ctx, item, prompt) {
120
173
  item.pid_boot = id.boot;
121
174
  item.pid_ns = id.ns;
122
175
  }
123
- ctx.say(`${item.name}: agent started (pid ${item.pid})`);
176
+ tell(ctx, item, `agent started (pid ${item.pid})`);
124
177
  }
125
178
  function drop(state, item, budget) {
126
179
  state.items = state.items.filter((i) => i !== item);
@@ -130,18 +183,23 @@ function drop(state, item, budget) {
130
183
  function park(ctx, io, state, item, reason, budget) {
131
184
  const r = io.cli(['roadmap', 'park', item.name, '--reason', reason]);
132
185
  if (r.status !== 0) {
133
- failItem(item, `park failed: ${fail(r)}`);
134
- ctx.say(`${item.name}: could not park — ${item.error}`);
186
+ failItem(ctx, state, item, `park failed: ${fail(r)}`);
187
+ tell(ctx, item, `could not park — ${item.error}`);
135
188
  return;
136
189
  }
137
190
  io.cli(['release', item.name]);
138
- ctx.say(`${item.name}: parked — ${reason}`);
191
+ tell(ctx, item, `parked — ${reason}`);
192
+ notice(ctx, 'item parked', `${item.name}: ${reason}`);
139
193
  drop(state, item, budget);
140
194
  }
141
- function currentPhase(ctx, item) {
142
- const yaml = ctx.fs.read(`${item.worktree}/.aidlc/state/${item.name}/instance.yaml`) ?? '';
195
+ /** The instance's `current_phase` in its worktree, or null when unreadable. Shared with the view. */
196
+ export function instancePhase(read, item) {
197
+ const yaml = read(`${item.worktree}/.aidlc/state/${item.name}/instance.yaml`) ?? '';
143
198
  return /^current_phase:\s*(\S+)/m.exec(yaml)?.[1] ?? null;
144
199
  }
200
+ function currentPhase(ctx, item) {
201
+ return instancePhase(ctx.fs.read, item);
202
+ }
145
203
  /** A phase file's own top-level status, not an artifact's indented one. */
146
204
  const COMPLETE_RE = /^status:\s*complete\s*$/m;
147
205
  function phaseComplete(ctx, item, phase) {
@@ -159,7 +217,8 @@ function awaitIfPaused(ctx, item) {
159
217
  item.step = 'awaiting-review';
160
218
  item.review = phase;
161
219
  item.pid = null;
162
- ctx.say(`${item.name}: paused after ${phase} for your review — \`aidlc autopilot approve ${item.name}\` carries it on`);
220
+ tell(ctx, item, `paused after ${phase} for your review — \`aidlc autopilot approve ${item.name}\` carries it on`);
221
+ notice(ctx, 'review needed', `${item.name}: paused after ${phase} — aidlc autopilot approve ${item.name}`);
163
222
  return true;
164
223
  }
165
224
  function advanceBuilding(ctx, io, state, item, budget) {
@@ -183,7 +242,8 @@ function advanceBuilding(ctx, io, state, item, budget) {
183
242
  return;
184
243
  }
185
244
  if (ctx.fs.exists(`${ctx.repo}/.aidlc/roadmap/hold/${item.file}`)) {
186
- ctx.say(`${item.name}: the agent parked it`);
245
+ tell(ctx, item, 'the agent parked it');
246
+ notice(ctx, 'item parked', `${item.name}: parked by its agent — the reason is in the item's history`);
187
247
  drop(state, item, budget);
188
248
  return;
189
249
  }
@@ -194,7 +254,7 @@ function advanceBuilding(ctx, io, state, item, budget) {
194
254
  park(ctx, io, state, item, `agent exited before the release boundary (phase ${phase ?? 'unknown'})`, budget);
195
255
  return;
196
256
  }
197
- openPullRequest(ctx, io, item);
257
+ openPullRequest(ctx, io, state, item);
198
258
  }
199
259
  /**
200
260
  * The pass after `aidlc autopilot approve`: wait again at the next completed pause,
@@ -206,7 +266,7 @@ function advanceApproved(ctx, io, state, item) {
206
266
  return;
207
267
  if (currentPhase(ctx, item) === 'deployment') {
208
268
  item.step = 'building';
209
- openPullRequest(ctx, io, item);
269
+ openPullRequest(ctx, io, state, item);
210
270
  return;
211
271
  }
212
272
  // A relaunch is a build like any other: it waits for a slot, ahead of new items,
@@ -214,26 +274,26 @@ function advanceApproved(ctx, io, state, item) {
214
274
  if (building(state) >= buildSlots(ctx))
215
275
  return;
216
276
  item.step = 'building';
217
- ctx.say(`${item.name}: approved — relaunching the agent to carry on`);
277
+ tell(ctx, item, 'approved — relaunching the agent to carry on');
218
278
  launchAgent(ctx, item, agentPrompt(item, itemPauses(item)));
219
279
  }
220
- function openPullRequest(ctx, io, item) {
280
+ function openPullRequest(ctx, io, state, item) {
221
281
  const push = io.call('git', ['push', '-u', 'origin', item.branch], { cwd: item.worktree });
222
282
  if (push.status !== 0) {
223
- failItem(item, `push failed: ${fail(push)}`, 'building');
283
+ failItem(ctx, state, item, `push failed: ${fail(push)}`, 'building');
224
284
  return;
225
285
  }
226
286
  if (!item.pr) {
227
287
  const pr = io.call('gh', ['pr', 'create', '--base', 'main', '--head', item.branch, '--fill'], { cwd: item.worktree });
228
288
  const n = /\/pull\/(\d+)/.exec(pr.stdout)?.[1];
229
289
  if (pr.status !== 0 || (!n && !pr.dry)) {
230
- failItem(item, `pr create failed: ${fail(pr)}`, 'building');
290
+ failItem(ctx, state, item, `pr create failed: ${fail(pr)}`, 'building');
231
291
  return;
232
292
  }
233
293
  item.pr = n ? Number(n) : null;
234
294
  }
235
295
  item.step = 'pr-open';
236
- ctx.say(`${item.name}: pull request #${item.pr} open`);
296
+ tell(ctx, item, `pull request #${item.pr} open`);
237
297
  }
238
298
  /** `gh pr view`, or null when it cannot be read. Read-only, so a dry run makes it too. */
239
299
  function prView(ctx, io, item) {
@@ -255,7 +315,7 @@ function prView(ctx, io, item) {
255
315
  function droppedIfMerged(ctx, state, item, view, budget) {
256
316
  if (view?.state !== 'MERGED')
257
317
  return false;
258
- ctx.say(`${item.name}: pull request #${item.pr} was merged outside autopilot — no longer tracked`);
318
+ tell(ctx, item, `pull request #${item.pr} was merged outside autopilot — no longer tracked`);
259
319
  drop(state, item, budget);
260
320
  return true;
261
321
  }
@@ -270,7 +330,7 @@ function answeredByPerson(ctx, io, state, item, view, budget) {
270
330
  return true;
271
331
  }
272
332
  if (view?.state === 'CLOSED') {
273
- ctx.say(`${item.name}: pull request #${item.pr} was closed without merging — no longer tracked`);
333
+ tell(ctx, item, `pull request #${item.pr} was closed without merging — no longer tracked`);
274
334
  drop(state, item, budget);
275
335
  return true;
276
336
  }
@@ -285,31 +345,35 @@ function closeOut(ctx, io, state, item, budget) {
285
345
  if (branch.stdout.trim() !== 'main' && !ctx.dryRun) {
286
346
  // The merge is done; trying again next pass loses nothing, and an `error` here
287
347
  // could only be cleared by `forget`, which skips the close-out altogether.
288
- ctx.say(`${item.name}: merged — closing it out waits for the primary checkout to be on main (it is on ${branch.stdout.trim()})`);
348
+ tell(ctx, item, `merged — closing it out waits for the primary checkout to be on main (it is on ${branch.stdout.trim()})`);
289
349
  return;
290
350
  }
291
351
  const pull = io.call('git', ['pull', '--rebase', '--autostash'], { cwd: ctx.repo });
292
352
  if (pull.status !== 0) {
293
- failItem(item, `pull failed: ${fail(pull)}`);
294
- ctx.say(`${item.name}: ${item.error}`);
353
+ const error = `pull failed: ${fail(pull)}`;
354
+ failItem(ctx, state, item, error);
355
+ tell(ctx, item, error);
295
356
  return;
296
357
  }
297
358
  // Closed by hand already: nothing to transition, and a second close-out commit
298
359
  // would fail on an empty index (autopilot-stop-controls/AC-22).
299
360
  if (COMPLETE_RE.test(ctx.fs.read(`${ctx.repo}/.aidlc/state/${item.name}/phase-deployment.yaml`) ?? '')) {
300
- ctx.say(`${item.name}: pull request #${item.pr} merged, and already closed on main — no longer tracked`);
361
+ tell(ctx, item, `pull request #${item.pr} merged, and already closed on main — no longer tracked`);
362
+ notice(ctx, 'item closed', `${item.name}: pull request #${item.pr} merged, and already closed on main`);
301
363
  drop(state, item, budget);
302
364
  return;
303
365
  }
304
366
  for (const [label, step] of closeSteps(ctx, io, item)) {
305
367
  const r = step();
306
368
  if (r.status !== 0) {
307
- failItem(item, `${label} failed: ${fail(r)}`);
308
- ctx.say(`${item.name}: ${item.error}`);
369
+ const error = `${label} failed: ${fail(r)}`;
370
+ failItem(ctx, state, item, error);
371
+ tell(ctx, item, error);
309
372
  return;
310
373
  }
311
374
  }
312
- ctx.say(`${item.name}: pull request #${item.pr} merged by you, and closed`);
375
+ tell(ctx, item, `pull request #${item.pr} merged by you, and closed`);
376
+ notice(ctx, 'item closed', `${item.name}: pull request #${item.pr} merged by you, and closed`);
313
377
  drop(state, item, budget);
314
378
  }
315
379
  /** Close the instance on main. Shared by both close-outs so they cannot drift. */
@@ -320,7 +384,7 @@ function closeSteps(ctx, io, item) {
320
384
  () => {
321
385
  if (!markDeploymentComplete(ctx, item.name))
322
386
  return { status: 1, stdout: '', stderr: 'no phase-deployment.yaml on main' };
323
- return io.cli(['transition', item.name]);
387
+ return transitionFailure(io.cli(['transition', item.name, '--json']));
324
388
  },
325
389
  ],
326
390
  ['stage-close', () => io.call('git', ['add', '.aidlc'], { cwd: ctx.repo })],
@@ -346,7 +410,7 @@ function handleConflict(ctx, io, state, item, budget) {
346
410
  item.conflict_fixes = tries + 1;
347
411
  dequeue(state, item);
348
412
  item.step = 'building';
349
- ctx.say(`${item.name}: conflicts with main — relaunching the agent to resolve it (attempt ${tries + 1} of ${CONFLICT_FIX_LIMIT})`);
413
+ tell(ctx, item, `conflicts with main — relaunching the agent to resolve it (attempt ${tries + 1} of ${CONFLICT_FIX_LIMIT})`);
350
414
  launchAgent(ctx, item, conflictPrompt(item));
351
415
  }
352
416
  /**
@@ -363,14 +427,14 @@ function upToDateWithMain(ctx, io, state, item, resume, budget) {
363
427
  const wt = { cwd: item.worktree };
364
428
  const fetch = io.call('git', ['fetch', 'origin', 'main'], wt);
365
429
  if (fetch.status !== 0) {
366
- failItem(item, `update from main failed: ${fail(fetch)}`, resume);
430
+ failItem(ctx, state, item, `update from main failed: ${fail(fetch)}`, resume);
367
431
  return false;
368
432
  }
369
433
  const contains = io.call('git', ['merge-base', '--is-ancestor', 'origin/main', 'HEAD'], wt);
370
434
  if (contains.status === 0)
371
435
  return true;
372
436
  if (contains.status !== 1) {
373
- failItem(item, `update from main failed: ${fail(contains)}`, resume);
437
+ failItem(ctx, state, item, `update from main failed: ${fail(contains)}`, resume);
374
438
  return false;
375
439
  }
376
440
  const merge = io.call('git', ['merge', '--no-edit', 'origin/main'], wt);
@@ -383,11 +447,11 @@ function upToDateWithMain(ctx, io, state, item, resume, budget) {
383
447
  const push = io.call('git', ['push', 'origin', item.branch], wt);
384
448
  if (push.status !== 0) {
385
449
  // The merge commit is local; the building step pushes it again on resume.
386
- failItem(item, `push failed: ${fail(push)}`, 'building');
450
+ failItem(ctx, state, item, `push failed: ${fail(push)}`, 'building');
387
451
  return false;
388
452
  }
389
453
  item.step = 'pr-open';
390
- ctx.say(`${item.name}: brought up to date with main — waiting for CI on the new head`);
454
+ tell(ctx, item, 'brought up to date with main — waiting for CI on the new head');
391
455
  return false;
392
456
  }
393
457
  function advancePr(ctx, io, state, item, budget) {
@@ -405,37 +469,43 @@ function advancePr(ctx, io, state, item, budget) {
405
469
  return;
406
470
  }
407
471
  if (checks.length === 0) {
472
+ item.ci = 'waiting';
408
473
  // GitHub runs no pull_request checks on a PR that conflicts, so "no checks
409
474
  // yet" would otherwise be waited on forever (AC-12).
410
475
  if (view?.mergeable === 'CONFLICTING') {
411
- ctx.say(`${item.name}: pull request #${item.pr} conflicts with main and has no checks`);
476
+ tell(ctx, item, `pull request #${item.pr} conflicts with main and has no checks`);
412
477
  upToDateWithMain(ctx, io, state, item, 'pr-open', budget);
413
478
  return;
414
479
  }
415
- ctx.say(`${item.name}: pull request #${item.pr} — waiting for checks to start`);
480
+ tell(ctx, item, `pull request #${item.pr} — waiting for checks to start`);
416
481
  return;
417
482
  }
418
- if (checks.some((c) => c.bucket === 'pending'))
483
+ if (checks.some((c) => c.bucket === 'pending')) {
484
+ item.ci = 'pending';
419
485
  return;
486
+ }
420
487
  const failing = checks.filter((c) => c.bucket === 'fail' || c.bucket === 'cancel').map((c) => c.name);
421
488
  if (failing.length === 0) {
489
+ item.ci = 'passed';
422
490
  if (atPr) {
423
491
  item.step = 'awaiting-merge';
424
- ctx.say(`${item.name}: CI green — pull request #${item.pr} waits for you to merge it`);
492
+ tell(ctx, item, `CI green — pull request #${item.pr} waits for you to merge it`);
493
+ notice(ctx, 'merge needed', `${item.name}: pull request #${item.pr} waits for you to merge it`);
425
494
  return;
426
495
  }
427
496
  item.step = 'queued';
428
497
  if (!state.queue.includes(item.name))
429
498
  state.queue.push(item.name);
430
- ctx.say(`${item.name}: CI green, queued for merge`);
499
+ tell(ctx, item, 'CI green, queued for merge');
431
500
  return;
432
501
  }
502
+ item.ci = 'failed';
433
503
  item.ci_failures = (item.ci_failures ?? 0) + 1;
434
504
  if (item.ci_failures >= CI_FAILURE_LIMIT) {
435
505
  park(ctx, io, state, item, `CI failed twice: ${failing.join(', ')}`, budget);
436
506
  return;
437
507
  }
438
- ctx.say(`${item.name}: CI failed (${failing.join(', ')}), relaunching the agent once`);
508
+ tell(ctx, item, `CI failed (${failing.join(', ')}), relaunching the agent once`);
439
509
  item.step = 'building';
440
510
  launchAgent(ctx, item, fixPrompt(item, failing));
441
511
  }
@@ -468,6 +538,7 @@ function serveQueue(ctx, io, state, budget) {
468
538
  return;
469
539
  }
470
540
  if (item.step === 'error') {
541
+ // An observation repeated every pass: the failure that halted the queue already notified.
471
542
  ctx.say(`queue halted: ${item.name} — ${item.error}`);
472
543
  return;
473
544
  }
@@ -477,7 +548,7 @@ function serveQueue(ctx, io, state, budget) {
477
548
  try {
478
549
  const status = JSON.parse(runs.stdout || '[]')[0]?.status;
479
550
  if (status === 'queued' || status === 'in_progress') {
480
- ctx.say(`${item.name}: waiting — a release is already in flight`);
551
+ tell(ctx, item, 'waiting — a release is already in flight');
481
552
  return;
482
553
  }
483
554
  }
@@ -486,7 +557,7 @@ function serveQueue(ctx, io, state, budget) {
486
557
  }
487
558
  const branch = io.call('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: ctx.repo });
488
559
  if (branch.stdout.trim() !== 'main' && !ctx.dryRun) {
489
- failItem(item, `primary checkout is on ${branch.stdout.trim()}, not main`, 'queued');
560
+ failItem(ctx, state, item, `primary checkout is on ${branch.stdout.trim()}, not main`, 'queued');
490
561
  return;
491
562
  }
492
563
  if (!upToDateWithMain(ctx, io, state, item, 'queued', budget))
@@ -495,7 +566,7 @@ function serveQueue(ctx, io, state, budget) {
495
566
  const head = io.call('git', ['rev-parse', 'HEAD'], { cwd: item.worktree });
496
567
  const sha = head.stdout.trim();
497
568
  if ((head.status !== 0 || !/^[0-9a-f]{7,64}$/.test(sha)) && !ctx.dryRun) {
498
- failItem(item, `merge failed: cannot read the branch head (${fail(head)})`, 'queued');
569
+ failItem(ctx, state, item, `merge failed: cannot read the branch head (${fail(head)})`, 'queued');
499
570
  return;
500
571
  }
501
572
  const release = ctx.config?.release_command;
@@ -518,12 +589,14 @@ function serveQueue(ctx, io, state, budget) {
518
589
  if (r.status !== 0) {
519
590
  // Halt. Only a failed merge resumes on the next `autopilot run`; from the
520
591
  // pull on, the merge has happened and a half-done release is for a person (AC-16).
521
- failItem(item, `${label} failed: ${fail(r)}`, label === 'merge' ? 'queued' : undefined);
522
- ctx.say(`${item.name}: ${item.error} — queue halted`);
592
+ const error = `${label} failed: ${fail(r)}`;
593
+ failItem(ctx, state, item, error, label === 'merge' ? 'queued' : undefined);
594
+ tell(ctx, item, `${error} — queue halted`);
523
595
  return;
524
596
  }
525
597
  }
526
- ctx.say(`${item.name}: merged${releases ? ', released' : ''} and closed`);
598
+ tell(ctx, item, `merged${releases ? ', released' : ''} and closed`);
599
+ notice(ctx, releases ? 'item released' : 'item merged', `${item.name}: merged${releases ? ', released' : ''} and closed`);
527
600
  drop(state, item, budget);
528
601
  }
529
602
  /**
@@ -541,6 +614,7 @@ function parkUnstartable(ctx, io, file, failure) {
541
614
  return false;
542
615
  }
543
616
  ctx.say(`could not start ${file}: ${error} — parked`);
617
+ notice(ctx, 'item parked', `${file}: could not start: ${error}`);
544
618
  return true;
545
619
  }
546
620
  function startBuilds(ctx, io, state, budget) {
@@ -678,6 +752,7 @@ export function tick(ctx, state, budget) {
678
752
  return state;
679
753
  }
680
754
  const io = makeIO(ctx);
755
+ const before = stepsOf(state);
681
756
  try {
682
757
  for (const item of [...state.items]) {
683
758
  // `awaiting-review` is passed over: nothing to read until a person approves.
@@ -702,9 +777,26 @@ export function tick(ctx, state, budget) {
702
777
  state.backoff_until = new Date(ctx.now() + state.backoff_ms).toISOString();
703
778
  ctx.say(`rate limited (${err.message}) — backing off until ${state.backoff_until}`);
704
779
  }
780
+ markStepChanges(state, before, ctx.now());
705
781
  state.last_tick = new Date(ctx.now()).toISOString();
706
782
  return state;
707
783
  }
784
+ /** Each item's step by name, to compare against after a change (autopilot-live-progress/AC-11). */
785
+ export function stepsOf(state) {
786
+ return new Map(state.items.map((i) => [i.name, i.step]));
787
+ }
788
+ /**
789
+ * Stamp `step_since` on every item whose step differs from `before`; a new item has
790
+ * no entry there and is stamped too. Diffing here, rather than at each of the
791
+ * fourteen places a step is set, means a new handler cannot forget it.
792
+ */
793
+ export function markStepChanges(state, before, now) {
794
+ const at = new Date(now).toISOString();
795
+ for (const item of state.items) {
796
+ if (before.get(item.name) !== item.step)
797
+ item.step_since = { step: item.step, at };
798
+ }
799
+ }
708
800
  /** What a person is being waited on for, in words (autopilot-stop-controls/AC-14, AC-20). */
709
801
  function waitingFor(i) {
710
802
  if (i.step === 'awaiting-review') {
@@ -716,22 +808,33 @@ function waitingFor(i) {
716
808
  return ` — waiting for you to merge pull request #${i.pr}`;
717
809
  return '';
718
810
  }
811
+ /**
812
+ * The driver's line, shared by `status` and the watch view.
813
+ *
814
+ * @param stopped - whether the STOP file exists.
815
+ * @param lastPass - how to show the last pass's time; `status` shows it as stored.
816
+ */
817
+ export function driverLine(stopped, state, now, lastPass = (iso) => iso) {
818
+ if (stopped)
819
+ return 'driver: stopped (STOP file present)';
820
+ if (state.backoff_until && now < Date.parse(state.backoff_until))
821
+ return `driver: backing off until ${state.backoff_until}`;
822
+ return `driver: last pass ${state.last_tick ? lastPass(state.last_tick) : 'never'}`;
823
+ }
824
+ export function queueLine(state) {
825
+ return state.queue.length > 0 ? `merge queue: ${state.queue.join(' → ')}` : null;
826
+ }
719
827
  export function statusLines(ctx, state) {
720
- const lines = [];
721
- if (ctx.fs.exists(ctx.paths.stop))
722
- lines.push('driver: stopped (STOP file present)');
723
- else if (state.backoff_until && ctx.now() < Date.parse(state.backoff_until))
724
- lines.push(`driver: backing off until ${state.backoff_until}`);
725
- else
726
- lines.push(`driver: last pass ${state.last_tick ?? 'never'}`);
828
+ const lines = [driverLine(ctx.fs.exists(ctx.paths.stop), state, ctx.now())];
727
829
  if (state.items.length === 0)
728
830
  lines.push('no items in flight');
729
831
  for (const i of state.items) {
730
832
  lines.push(`${i.name}: ${i.step}${i.pr ? ` PR #${i.pr}` : ''} — CI failures ${i.ci_failures ?? 0}` +
731
833
  `${waitingFor(i)}${i.error ? ` — last error: ${i.error}` : ''}`);
732
834
  }
733
- if (state.queue.length > 0)
734
- lines.push(`merge queue: ${state.queue.join(' → ')}`);
835
+ const queue = queueLine(state);
836
+ if (queue !== null)
837
+ lines.push(queue);
735
838
  return lines;
736
839
  }
737
840
  //# sourceMappingURL=driver.js.map