@rasensio/aidlc 1.63.0 → 1.65.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.
@@ -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;
@@ -23,6 +24,16 @@ export interface RunResult {
23
24
  stderr: string;
24
25
  dry?: boolean;
25
26
  }
27
+ /** How a spawned agent ended, as seen by the process that spawned it. */
28
+ export interface AgentExit {
29
+ /** null when a signal ended it. */
30
+ code: number | null;
31
+ signal: string | null;
32
+ /** From spawn to exit. */
33
+ ms: number;
34
+ /** At most 4 KB of what this launch wrote to the item's log — never an earlier launch's (codex-preset-removed-flag/AC-8). */
35
+ output: string;
36
+ }
26
37
  export interface DriverCtx {
27
38
  /** Primary checkout — where merges and releases happen. */
28
39
  repo: string;
@@ -49,12 +60,22 @@ export interface DriverCtx {
49
60
  boot: string | null;
50
61
  ns: string | null;
51
62
  } | null;
63
+ /**
64
+ * How `pid` ended, or null when this process did not spawn it or it has not ended.
65
+ * Absent is the same as null: the pass cannot tell (codex-preset-removed-flag/AC-11).
66
+ */
67
+ exited?(pid: number): AgentExit | null;
52
68
  kill(pid: number): void;
53
69
  fs: {
54
70
  read(path: string): string | null;
55
71
  exists(path: string): boolean;
56
72
  write(path: string, text: string): void;
57
73
  };
74
+ /**
75
+ * Tell a person something needs them or is done. Absent sends nothing: only a
76
+ * background run sets it (autopilot-live-progress/AC-20, AC-23).
77
+ */
78
+ notify?(notice: Notice): void;
58
79
  }
59
80
  /** How many builds a run may still start, and what to tell it (autopilot-on-demand/AC-7). */
60
81
  export interface Budget {
@@ -85,6 +106,20 @@ export declare function conflictPrompt(item: {
85
106
  }): string;
86
107
  /** The stops an item is held to: those recorded when it started (autopilot-stop-controls/AC-6). */
87
108
  export declare function itemStops(item: AutopilotItem): Stops;
109
+ /**
110
+ * A non-zero exit sooner than this after launch is a launch failure, not a failed
111
+ * build. Argument and startup errors exit in well under a second; a session that
112
+ * fails later prints a banner first, which names nothing (codex-preset-removed-flag/AC-6, AC-10).
113
+ */
114
+ export declare const LAUNCH_FAILURE_MS = 30000;
115
+ /**
116
+ * The first non-blank line of `text`, terminal escapes and control characters removed,
117
+ * at most 200 characters; null when there is none (codex-preset-removed-flag/AC-12).
118
+ * The first line, not `fail`'s last: for an argument error the last is the usage line.
119
+ */
120
+ export declare function firstOutputLine(text: string): string | null;
121
+ /** The instance's `current_phase` in its worktree, or null when unreadable. Shared with the view. */
122
+ export declare function instancePhase(read: (path: string) => string | null, item: Pick<AutopilotItem, 'name' | 'worktree'>): string | null;
88
123
  /**
89
124
  * Return every item whose failure is resumable to the step it failed in, and
90
125
  * clear the error. Called when a run starts: asking to run is asking to retry
@@ -112,5 +147,21 @@ export type ApproveOutcome = {
112
147
  export declare function approveItem(state: AutopilotState, name: string): ApproveOutcome;
113
148
  /** One pass. Returns the new state; the caller saves it (not in a dry run). */
114
149
  export declare function tick(ctx: DriverCtx, state: AutopilotState, budget?: Budget): AutopilotState;
150
+ /** Each item's step by name, to compare against after a change (autopilot-live-progress/AC-11). */
151
+ export declare function stepsOf(state: AutopilotState): Map<string, ItemStep>;
152
+ /**
153
+ * Stamp `step_since` on every item whose step differs from `before`; a new item has
154
+ * no entry there and is stamped too. Diffing here, rather than at each of the
155
+ * fourteen places a step is set, means a new handler cannot forget it.
156
+ */
157
+ export declare function markStepChanges(state: AutopilotState, before: Map<string, ItemStep>, now: number): void;
158
+ /**
159
+ * The driver's line, shared by `status` and the watch view.
160
+ *
161
+ * @param stopped - whether the STOP file exists.
162
+ * @param lastPass - how to show the last pass's time; `status` shows it as stored.
163
+ */
164
+ export declare function driverLine(stopped: boolean, state: AutopilotState, now: number, lastPass?: (iso: string) => string): string;
165
+ export declare function queueLine(state: AutopilotState): string | null;
115
166
  export declare function statusLines(ctx: Pick<DriverCtx, 'fs' | 'paths' | 'now'>, state: AutopilotState): string[];
116
167
  //# 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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"}
@@ -96,18 +96,49 @@ export function conflictPrompt(item) {
96
96
  `\`git fetch origin main\` and \`git merge origin/main\` on this branch, resolve every conflict keeping ` +
97
97
  `the intent of both sides, run the tests, commit the merge, and stop. Do not push, merge or release.`);
98
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
+ }
99
122
  /**
100
123
  * Put an item in `error`. `resume` names the step `aidlc autopilot run` returns
101
124
  * it to; leave it out for a failure at or after the merge, which stays a
102
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).
103
130
  */
104
- function failItem(item, error, resume) {
131
+ function failItem(ctx, state, item, error, resume) {
105
132
  item.step = 'error';
106
133
  item.error = error;
107
134
  if (resume)
108
135
  item.resume_step = resume;
109
136
  else
110
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}`);
111
142
  }
112
143
  function dequeue(state, item) {
113
144
  state.queue = state.queue.filter((n) => n !== item.name);
@@ -121,6 +152,37 @@ export function itemStops(item) {
121
152
  return item.stops ?? LEGACY_STOPS;
122
153
  }
123
154
  const itemPauses = (item) => pauseEnv(itemStops(item), item.approved ?? []);
155
+ /**
156
+ * A non-zero exit sooner than this after launch is a launch failure, not a failed
157
+ * build. Argument and startup errors exit in well under a second; a session that
158
+ * fails later prints a banner first, which names nothing (codex-preset-removed-flag/AC-6, AC-10).
159
+ */
160
+ export const LAUNCH_FAILURE_MS = 30_000;
161
+ const REASON_MAX = 200;
162
+ /**
163
+ * The first non-blank line of `text`, terminal escapes and control characters removed,
164
+ * at most 200 characters; null when there is none (codex-preset-removed-flag/AC-12).
165
+ * The first line, not `fail`'s last: for an argument error the last is the usage line.
166
+ */
167
+ export function firstOutputLine(text) {
168
+ const plain = text.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '').replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '');
169
+ const line = plain.split('\n').map((l) => l.trim()).find((l) => l !== '');
170
+ return line === undefined ? null : line.slice(0, REASON_MAX);
171
+ }
172
+ /**
173
+ * The park reason for an agent that failed to launch, or null when it did not or the
174
+ * pass cannot tell. Its first line, not its stderr: both streams share the log by
175
+ * design, and an argument error is printed before anything else
176
+ * (codex-preset-removed-flag/AC-6, AC-7, AC-11).
177
+ */
178
+ function launchFailure(ctx, item) {
179
+ const exit = item.pid === null ? null : ctx.exited?.(item.pid) ?? null;
180
+ if (exit === null || exit.code === 0 || exit.ms >= LAUNCH_FAILURE_MS)
181
+ return null;
182
+ const how = exit.code === null ? `signal ${exit.signal ?? 'unknown'}` : `exit ${exit.code}`;
183
+ const line = firstOutputLine(exit.output);
184
+ return line === null ? `agent failed to launch (${how}) with no output` : `agent failed to launch (${how}): ${line}`;
185
+ }
124
186
  function launchAgent(ctx, item, prompt) {
125
187
  const command = ctx.config?.agent_command ?? [];
126
188
  const argv = command.map((a) => (a === '{prompt}' ? prompt : a));
@@ -142,7 +204,7 @@ function launchAgent(ctx, item, prompt) {
142
204
  item.pid_boot = id.boot;
143
205
  item.pid_ns = id.ns;
144
206
  }
145
- ctx.say(`${item.name}: agent started (pid ${item.pid})`);
207
+ tell(ctx, item, `agent started (pid ${item.pid})`);
146
208
  }
147
209
  function drop(state, item, budget) {
148
210
  state.items = state.items.filter((i) => i !== item);
@@ -152,18 +214,23 @@ function drop(state, item, budget) {
152
214
  function park(ctx, io, state, item, reason, budget) {
153
215
  const r = io.cli(['roadmap', 'park', item.name, '--reason', reason]);
154
216
  if (r.status !== 0) {
155
- failItem(item, `park failed: ${fail(r)}`);
156
- ctx.say(`${item.name}: could not park — ${item.error}`);
217
+ failItem(ctx, state, item, `park failed: ${fail(r)}`);
218
+ tell(ctx, item, `could not park — ${item.error}`);
157
219
  return;
158
220
  }
159
221
  io.cli(['release', item.name]);
160
- ctx.say(`${item.name}: parked — ${reason}`);
222
+ tell(ctx, item, `parked — ${reason}`);
223
+ notice(ctx, 'item parked', `${item.name}: ${reason}`);
161
224
  drop(state, item, budget);
162
225
  }
163
- function currentPhase(ctx, item) {
164
- const yaml = ctx.fs.read(`${item.worktree}/.aidlc/state/${item.name}/instance.yaml`) ?? '';
226
+ /** The instance's `current_phase` in its worktree, or null when unreadable. Shared with the view. */
227
+ export function instancePhase(read, item) {
228
+ const yaml = read(`${item.worktree}/.aidlc/state/${item.name}/instance.yaml`) ?? '';
165
229
  return /^current_phase:\s*(\S+)/m.exec(yaml)?.[1] ?? null;
166
230
  }
231
+ function currentPhase(ctx, item) {
232
+ return instancePhase(ctx.fs.read, item);
233
+ }
167
234
  /** A phase file's own top-level status, not an artifact's indented one. */
168
235
  const COMPLETE_RE = /^status:\s*complete\s*$/m;
169
236
  function phaseComplete(ctx, item, phase) {
@@ -181,7 +248,8 @@ function awaitIfPaused(ctx, item) {
181
248
  item.step = 'awaiting-review';
182
249
  item.review = phase;
183
250
  item.pid = null;
184
- ctx.say(`${item.name}: paused after ${phase} for your review — \`aidlc autopilot approve ${item.name}\` carries it on`);
251
+ tell(ctx, item, `paused after ${phase} for your review — \`aidlc autopilot approve ${item.name}\` carries it on`);
252
+ notice(ctx, 'review needed', `${item.name}: paused after ${phase} — aidlc autopilot approve ${item.name}`);
185
253
  return true;
186
254
  }
187
255
  function advanceBuilding(ctx, io, state, item, budget) {
@@ -205,18 +273,26 @@ function advanceBuilding(ctx, io, state, item, budget) {
205
273
  return;
206
274
  }
207
275
  if (ctx.fs.exists(`${ctx.repo}/.aidlc/roadmap/hold/${item.file}`)) {
208
- ctx.say(`${item.name}: the agent parked it`);
276
+ tell(ctx, item, 'the agent parked it');
277
+ notice(ctx, 'item parked', `${item.name}: parked by its agent — the reason is in the item's history`);
209
278
  drop(state, item, budget);
210
279
  return;
211
280
  }
212
281
  if (awaitIfPaused(ctx, item))
213
282
  return;
283
+ // Before the phase: a relaunch that never started would otherwise push and open
284
+ // the pull request as if it had finished (codex-preset-removed-flag/AC-9).
285
+ const launch = launchFailure(ctx, item);
286
+ if (launch !== null) {
287
+ park(ctx, io, state, item, launch, budget);
288
+ return;
289
+ }
214
290
  const phase = currentPhase(ctx, item);
215
291
  if (phase !== 'deployment') {
216
292
  park(ctx, io, state, item, `agent exited before the release boundary (phase ${phase ?? 'unknown'})`, budget);
217
293
  return;
218
294
  }
219
- openPullRequest(ctx, io, item);
295
+ openPullRequest(ctx, io, state, item);
220
296
  }
221
297
  /**
222
298
  * The pass after `aidlc autopilot approve`: wait again at the next completed pause,
@@ -228,7 +304,7 @@ function advanceApproved(ctx, io, state, item) {
228
304
  return;
229
305
  if (currentPhase(ctx, item) === 'deployment') {
230
306
  item.step = 'building';
231
- openPullRequest(ctx, io, item);
307
+ openPullRequest(ctx, io, state, item);
232
308
  return;
233
309
  }
234
310
  // A relaunch is a build like any other: it waits for a slot, ahead of new items,
@@ -236,26 +312,26 @@ function advanceApproved(ctx, io, state, item) {
236
312
  if (building(state) >= buildSlots(ctx))
237
313
  return;
238
314
  item.step = 'building';
239
- ctx.say(`${item.name}: approved — relaunching the agent to carry on`);
315
+ tell(ctx, item, 'approved — relaunching the agent to carry on');
240
316
  launchAgent(ctx, item, agentPrompt(item, itemPauses(item)));
241
317
  }
242
- function openPullRequest(ctx, io, item) {
318
+ function openPullRequest(ctx, io, state, item) {
243
319
  const push = io.call('git', ['push', '-u', 'origin', item.branch], { cwd: item.worktree });
244
320
  if (push.status !== 0) {
245
- failItem(item, `push failed: ${fail(push)}`, 'building');
321
+ failItem(ctx, state, item, `push failed: ${fail(push)}`, 'building');
246
322
  return;
247
323
  }
248
324
  if (!item.pr) {
249
325
  const pr = io.call('gh', ['pr', 'create', '--base', 'main', '--head', item.branch, '--fill'], { cwd: item.worktree });
250
326
  const n = /\/pull\/(\d+)/.exec(pr.stdout)?.[1];
251
327
  if (pr.status !== 0 || (!n && !pr.dry)) {
252
- failItem(item, `pr create failed: ${fail(pr)}`, 'building');
328
+ failItem(ctx, state, item, `pr create failed: ${fail(pr)}`, 'building');
253
329
  return;
254
330
  }
255
331
  item.pr = n ? Number(n) : null;
256
332
  }
257
333
  item.step = 'pr-open';
258
- ctx.say(`${item.name}: pull request #${item.pr} open`);
334
+ tell(ctx, item, `pull request #${item.pr} open`);
259
335
  }
260
336
  /** `gh pr view`, or null when it cannot be read. Read-only, so a dry run makes it too. */
261
337
  function prView(ctx, io, item) {
@@ -277,7 +353,7 @@ function prView(ctx, io, item) {
277
353
  function droppedIfMerged(ctx, state, item, view, budget) {
278
354
  if (view?.state !== 'MERGED')
279
355
  return false;
280
- ctx.say(`${item.name}: pull request #${item.pr} was merged outside autopilot — no longer tracked`);
356
+ tell(ctx, item, `pull request #${item.pr} was merged outside autopilot — no longer tracked`);
281
357
  drop(state, item, budget);
282
358
  return true;
283
359
  }
@@ -292,7 +368,7 @@ function answeredByPerson(ctx, io, state, item, view, budget) {
292
368
  return true;
293
369
  }
294
370
  if (view?.state === 'CLOSED') {
295
- ctx.say(`${item.name}: pull request #${item.pr} was closed without merging — no longer tracked`);
371
+ tell(ctx, item, `pull request #${item.pr} was closed without merging — no longer tracked`);
296
372
  drop(state, item, budget);
297
373
  return true;
298
374
  }
@@ -307,31 +383,35 @@ function closeOut(ctx, io, state, item, budget) {
307
383
  if (branch.stdout.trim() !== 'main' && !ctx.dryRun) {
308
384
  // The merge is done; trying again next pass loses nothing, and an `error` here
309
385
  // could only be cleared by `forget`, which skips the close-out altogether.
310
- ctx.say(`${item.name}: merged — closing it out waits for the primary checkout to be on main (it is on ${branch.stdout.trim()})`);
386
+ tell(ctx, item, `merged — closing it out waits for the primary checkout to be on main (it is on ${branch.stdout.trim()})`);
311
387
  return;
312
388
  }
313
389
  const pull = io.call('git', ['pull', '--rebase', '--autostash'], { cwd: ctx.repo });
314
390
  if (pull.status !== 0) {
315
- failItem(item, `pull failed: ${fail(pull)}`);
316
- ctx.say(`${item.name}: ${item.error}`);
391
+ const error = `pull failed: ${fail(pull)}`;
392
+ failItem(ctx, state, item, error);
393
+ tell(ctx, item, error);
317
394
  return;
318
395
  }
319
396
  // Closed by hand already: nothing to transition, and a second close-out commit
320
397
  // would fail on an empty index (autopilot-stop-controls/AC-22).
321
398
  if (COMPLETE_RE.test(ctx.fs.read(`${ctx.repo}/.aidlc/state/${item.name}/phase-deployment.yaml`) ?? '')) {
322
- ctx.say(`${item.name}: pull request #${item.pr} merged, and already closed on main — no longer tracked`);
399
+ tell(ctx, item, `pull request #${item.pr} merged, and already closed on main — no longer tracked`);
400
+ notice(ctx, 'item closed', `${item.name}: pull request #${item.pr} merged, and already closed on main`);
323
401
  drop(state, item, budget);
324
402
  return;
325
403
  }
326
404
  for (const [label, step] of closeSteps(ctx, io, item)) {
327
405
  const r = step();
328
406
  if (r.status !== 0) {
329
- failItem(item, `${label} failed: ${fail(r)}`);
330
- ctx.say(`${item.name}: ${item.error}`);
407
+ const error = `${label} failed: ${fail(r)}`;
408
+ failItem(ctx, state, item, error);
409
+ tell(ctx, item, error);
331
410
  return;
332
411
  }
333
412
  }
334
- ctx.say(`${item.name}: pull request #${item.pr} merged by you, and closed`);
413
+ tell(ctx, item, `pull request #${item.pr} merged by you, and closed`);
414
+ notice(ctx, 'item closed', `${item.name}: pull request #${item.pr} merged by you, and closed`);
335
415
  drop(state, item, budget);
336
416
  }
337
417
  /** Close the instance on main. Shared by both close-outs so they cannot drift. */
@@ -368,7 +448,7 @@ function handleConflict(ctx, io, state, item, budget) {
368
448
  item.conflict_fixes = tries + 1;
369
449
  dequeue(state, item);
370
450
  item.step = 'building';
371
- ctx.say(`${item.name}: conflicts with main — relaunching the agent to resolve it (attempt ${tries + 1} of ${CONFLICT_FIX_LIMIT})`);
451
+ tell(ctx, item, `conflicts with main — relaunching the agent to resolve it (attempt ${tries + 1} of ${CONFLICT_FIX_LIMIT})`);
372
452
  launchAgent(ctx, item, conflictPrompt(item));
373
453
  }
374
454
  /**
@@ -385,14 +465,14 @@ function upToDateWithMain(ctx, io, state, item, resume, budget) {
385
465
  const wt = { cwd: item.worktree };
386
466
  const fetch = io.call('git', ['fetch', 'origin', 'main'], wt);
387
467
  if (fetch.status !== 0) {
388
- failItem(item, `update from main failed: ${fail(fetch)}`, resume);
468
+ failItem(ctx, state, item, `update from main failed: ${fail(fetch)}`, resume);
389
469
  return false;
390
470
  }
391
471
  const contains = io.call('git', ['merge-base', '--is-ancestor', 'origin/main', 'HEAD'], wt);
392
472
  if (contains.status === 0)
393
473
  return true;
394
474
  if (contains.status !== 1) {
395
- failItem(item, `update from main failed: ${fail(contains)}`, resume);
475
+ failItem(ctx, state, item, `update from main failed: ${fail(contains)}`, resume);
396
476
  return false;
397
477
  }
398
478
  const merge = io.call('git', ['merge', '--no-edit', 'origin/main'], wt);
@@ -405,11 +485,11 @@ function upToDateWithMain(ctx, io, state, item, resume, budget) {
405
485
  const push = io.call('git', ['push', 'origin', item.branch], wt);
406
486
  if (push.status !== 0) {
407
487
  // The merge commit is local; the building step pushes it again on resume.
408
- failItem(item, `push failed: ${fail(push)}`, 'building');
488
+ failItem(ctx, state, item, `push failed: ${fail(push)}`, 'building');
409
489
  return false;
410
490
  }
411
491
  item.step = 'pr-open';
412
- ctx.say(`${item.name}: brought up to date with main — waiting for CI on the new head`);
492
+ tell(ctx, item, 'brought up to date with main — waiting for CI on the new head');
413
493
  return false;
414
494
  }
415
495
  function advancePr(ctx, io, state, item, budget) {
@@ -427,37 +507,43 @@ function advancePr(ctx, io, state, item, budget) {
427
507
  return;
428
508
  }
429
509
  if (checks.length === 0) {
510
+ item.ci = 'waiting';
430
511
  // GitHub runs no pull_request checks on a PR that conflicts, so "no checks
431
512
  // yet" would otherwise be waited on forever (AC-12).
432
513
  if (view?.mergeable === 'CONFLICTING') {
433
- ctx.say(`${item.name}: pull request #${item.pr} conflicts with main and has no checks`);
514
+ tell(ctx, item, `pull request #${item.pr} conflicts with main and has no checks`);
434
515
  upToDateWithMain(ctx, io, state, item, 'pr-open', budget);
435
516
  return;
436
517
  }
437
- ctx.say(`${item.name}: pull request #${item.pr} — waiting for checks to start`);
518
+ tell(ctx, item, `pull request #${item.pr} — waiting for checks to start`);
438
519
  return;
439
520
  }
440
- if (checks.some((c) => c.bucket === 'pending'))
521
+ if (checks.some((c) => c.bucket === 'pending')) {
522
+ item.ci = 'pending';
441
523
  return;
524
+ }
442
525
  const failing = checks.filter((c) => c.bucket === 'fail' || c.bucket === 'cancel').map((c) => c.name);
443
526
  if (failing.length === 0) {
527
+ item.ci = 'passed';
444
528
  if (atPr) {
445
529
  item.step = 'awaiting-merge';
446
- ctx.say(`${item.name}: CI green — pull request #${item.pr} waits for you to merge it`);
530
+ tell(ctx, item, `CI green — pull request #${item.pr} waits for you to merge it`);
531
+ notice(ctx, 'merge needed', `${item.name}: pull request #${item.pr} waits for you to merge it`);
447
532
  return;
448
533
  }
449
534
  item.step = 'queued';
450
535
  if (!state.queue.includes(item.name))
451
536
  state.queue.push(item.name);
452
- ctx.say(`${item.name}: CI green, queued for merge`);
537
+ tell(ctx, item, 'CI green, queued for merge');
453
538
  return;
454
539
  }
540
+ item.ci = 'failed';
455
541
  item.ci_failures = (item.ci_failures ?? 0) + 1;
456
542
  if (item.ci_failures >= CI_FAILURE_LIMIT) {
457
543
  park(ctx, io, state, item, `CI failed twice: ${failing.join(', ')}`, budget);
458
544
  return;
459
545
  }
460
- ctx.say(`${item.name}: CI failed (${failing.join(', ')}), relaunching the agent once`);
546
+ tell(ctx, item, `CI failed (${failing.join(', ')}), relaunching the agent once`);
461
547
  item.step = 'building';
462
548
  launchAgent(ctx, item, fixPrompt(item, failing));
463
549
  }
@@ -490,6 +576,7 @@ function serveQueue(ctx, io, state, budget) {
490
576
  return;
491
577
  }
492
578
  if (item.step === 'error') {
579
+ // An observation repeated every pass: the failure that halted the queue already notified.
493
580
  ctx.say(`queue halted: ${item.name} — ${item.error}`);
494
581
  return;
495
582
  }
@@ -499,7 +586,7 @@ function serveQueue(ctx, io, state, budget) {
499
586
  try {
500
587
  const status = JSON.parse(runs.stdout || '[]')[0]?.status;
501
588
  if (status === 'queued' || status === 'in_progress') {
502
- ctx.say(`${item.name}: waiting — a release is already in flight`);
589
+ tell(ctx, item, 'waiting — a release is already in flight');
503
590
  return;
504
591
  }
505
592
  }
@@ -508,7 +595,7 @@ function serveQueue(ctx, io, state, budget) {
508
595
  }
509
596
  const branch = io.call('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: ctx.repo });
510
597
  if (branch.stdout.trim() !== 'main' && !ctx.dryRun) {
511
- failItem(item, `primary checkout is on ${branch.stdout.trim()}, not main`, 'queued');
598
+ failItem(ctx, state, item, `primary checkout is on ${branch.stdout.trim()}, not main`, 'queued');
512
599
  return;
513
600
  }
514
601
  if (!upToDateWithMain(ctx, io, state, item, 'queued', budget))
@@ -517,7 +604,7 @@ function serveQueue(ctx, io, state, budget) {
517
604
  const head = io.call('git', ['rev-parse', 'HEAD'], { cwd: item.worktree });
518
605
  const sha = head.stdout.trim();
519
606
  if ((head.status !== 0 || !/^[0-9a-f]{7,64}$/.test(sha)) && !ctx.dryRun) {
520
- failItem(item, `merge failed: cannot read the branch head (${fail(head)})`, 'queued');
607
+ failItem(ctx, state, item, `merge failed: cannot read the branch head (${fail(head)})`, 'queued');
521
608
  return;
522
609
  }
523
610
  const release = ctx.config?.release_command;
@@ -540,12 +627,14 @@ function serveQueue(ctx, io, state, budget) {
540
627
  if (r.status !== 0) {
541
628
  // Halt. Only a failed merge resumes on the next `autopilot run`; from the
542
629
  // pull on, the merge has happened and a half-done release is for a person (AC-16).
543
- failItem(item, `${label} failed: ${fail(r)}`, label === 'merge' ? 'queued' : undefined);
544
- ctx.say(`${item.name}: ${item.error} — queue halted`);
630
+ const error = `${label} failed: ${fail(r)}`;
631
+ failItem(ctx, state, item, error, label === 'merge' ? 'queued' : undefined);
632
+ tell(ctx, item, `${error} — queue halted`);
545
633
  return;
546
634
  }
547
635
  }
548
- ctx.say(`${item.name}: merged${releases ? ', released' : ''} and closed`);
636
+ tell(ctx, item, `merged${releases ? ', released' : ''} and closed`);
637
+ notice(ctx, releases ? 'item released' : 'item merged', `${item.name}: merged${releases ? ', released' : ''} and closed`);
549
638
  drop(state, item, budget);
550
639
  }
551
640
  /**
@@ -563,6 +652,7 @@ function parkUnstartable(ctx, io, file, failure) {
563
652
  return false;
564
653
  }
565
654
  ctx.say(`could not start ${file}: ${error} — parked`);
655
+ notice(ctx, 'item parked', `${file}: could not start: ${error}`);
566
656
  return true;
567
657
  }
568
658
  function startBuilds(ctx, io, state, budget) {
@@ -700,6 +790,7 @@ export function tick(ctx, state, budget) {
700
790
  return state;
701
791
  }
702
792
  const io = makeIO(ctx);
793
+ const before = stepsOf(state);
703
794
  try {
704
795
  for (const item of [...state.items]) {
705
796
  // `awaiting-review` is passed over: nothing to read until a person approves.
@@ -724,9 +815,26 @@ export function tick(ctx, state, budget) {
724
815
  state.backoff_until = new Date(ctx.now() + state.backoff_ms).toISOString();
725
816
  ctx.say(`rate limited (${err.message}) — backing off until ${state.backoff_until}`);
726
817
  }
818
+ markStepChanges(state, before, ctx.now());
727
819
  state.last_tick = new Date(ctx.now()).toISOString();
728
820
  return state;
729
821
  }
822
+ /** Each item's step by name, to compare against after a change (autopilot-live-progress/AC-11). */
823
+ export function stepsOf(state) {
824
+ return new Map(state.items.map((i) => [i.name, i.step]));
825
+ }
826
+ /**
827
+ * Stamp `step_since` on every item whose step differs from `before`; a new item has
828
+ * no entry there and is stamped too. Diffing here, rather than at each of the
829
+ * fourteen places a step is set, means a new handler cannot forget it.
830
+ */
831
+ export function markStepChanges(state, before, now) {
832
+ const at = new Date(now).toISOString();
833
+ for (const item of state.items) {
834
+ if (before.get(item.name) !== item.step)
835
+ item.step_since = { step: item.step, at };
836
+ }
837
+ }
730
838
  /** What a person is being waited on for, in words (autopilot-stop-controls/AC-14, AC-20). */
731
839
  function waitingFor(i) {
732
840
  if (i.step === 'awaiting-review') {
@@ -738,22 +846,33 @@ function waitingFor(i) {
738
846
  return ` — waiting for you to merge pull request #${i.pr}`;
739
847
  return '';
740
848
  }
849
+ /**
850
+ * The driver's line, shared by `status` and the watch view.
851
+ *
852
+ * @param stopped - whether the STOP file exists.
853
+ * @param lastPass - how to show the last pass's time; `status` shows it as stored.
854
+ */
855
+ export function driverLine(stopped, state, now, lastPass = (iso) => iso) {
856
+ if (stopped)
857
+ return 'driver: stopped (STOP file present)';
858
+ if (state.backoff_until && now < Date.parse(state.backoff_until))
859
+ return `driver: backing off until ${state.backoff_until}`;
860
+ return `driver: last pass ${state.last_tick ? lastPass(state.last_tick) : 'never'}`;
861
+ }
862
+ export function queueLine(state) {
863
+ return state.queue.length > 0 ? `merge queue: ${state.queue.join(' → ')}` : null;
864
+ }
741
865
  export function statusLines(ctx, state) {
742
- const lines = [];
743
- if (ctx.fs.exists(ctx.paths.stop))
744
- lines.push('driver: stopped (STOP file present)');
745
- else if (state.backoff_until && ctx.now() < Date.parse(state.backoff_until))
746
- lines.push(`driver: backing off until ${state.backoff_until}`);
747
- else
748
- lines.push(`driver: last pass ${state.last_tick ?? 'never'}`);
866
+ const lines = [driverLine(ctx.fs.exists(ctx.paths.stop), state, ctx.now())];
749
867
  if (state.items.length === 0)
750
868
  lines.push('no items in flight');
751
869
  for (const i of state.items) {
752
870
  lines.push(`${i.name}: ${i.step}${i.pr ? ` PR #${i.pr}` : ''} — CI failures ${i.ci_failures ?? 0}` +
753
871
  `${waitingFor(i)}${i.error ? ` — last error: ${i.error}` : ''}`);
754
872
  }
755
- if (state.queue.length > 0)
756
- lines.push(`merge queue: ${state.queue.join(' → ')}`);
873
+ const queue = queueLine(state);
874
+ if (queue !== null)
875
+ lines.push(queue);
757
876
  return lines;
758
877
  }
759
878
  //# sourceMappingURL=driver.js.map