@rasensio/aidlc 1.63.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.
@@ -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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"}
@@ -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);
@@ -142,7 +173,7 @@ function launchAgent(ctx, item, prompt) {
142
173
  item.pid_boot = id.boot;
143
174
  item.pid_ns = id.ns;
144
175
  }
145
- ctx.say(`${item.name}: agent started (pid ${item.pid})`);
176
+ tell(ctx, item, `agent started (pid ${item.pid})`);
146
177
  }
147
178
  function drop(state, item, budget) {
148
179
  state.items = state.items.filter((i) => i !== item);
@@ -152,18 +183,23 @@ function drop(state, item, budget) {
152
183
  function park(ctx, io, state, item, reason, budget) {
153
184
  const r = io.cli(['roadmap', 'park', item.name, '--reason', reason]);
154
185
  if (r.status !== 0) {
155
- failItem(item, `park failed: ${fail(r)}`);
156
- 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}`);
157
188
  return;
158
189
  }
159
190
  io.cli(['release', item.name]);
160
- ctx.say(`${item.name}: parked — ${reason}`);
191
+ tell(ctx, item, `parked — ${reason}`);
192
+ notice(ctx, 'item parked', `${item.name}: ${reason}`);
161
193
  drop(state, item, budget);
162
194
  }
163
- function currentPhase(ctx, item) {
164
- 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`) ?? '';
165
198
  return /^current_phase:\s*(\S+)/m.exec(yaml)?.[1] ?? null;
166
199
  }
200
+ function currentPhase(ctx, item) {
201
+ return instancePhase(ctx.fs.read, item);
202
+ }
167
203
  /** A phase file's own top-level status, not an artifact's indented one. */
168
204
  const COMPLETE_RE = /^status:\s*complete\s*$/m;
169
205
  function phaseComplete(ctx, item, phase) {
@@ -181,7 +217,8 @@ function awaitIfPaused(ctx, item) {
181
217
  item.step = 'awaiting-review';
182
218
  item.review = phase;
183
219
  item.pid = null;
184
- 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}`);
185
222
  return true;
186
223
  }
187
224
  function advanceBuilding(ctx, io, state, item, budget) {
@@ -205,7 +242,8 @@ function advanceBuilding(ctx, io, state, item, budget) {
205
242
  return;
206
243
  }
207
244
  if (ctx.fs.exists(`${ctx.repo}/.aidlc/roadmap/hold/${item.file}`)) {
208
- 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`);
209
247
  drop(state, item, budget);
210
248
  return;
211
249
  }
@@ -216,7 +254,7 @@ function advanceBuilding(ctx, io, state, item, budget) {
216
254
  park(ctx, io, state, item, `agent exited before the release boundary (phase ${phase ?? 'unknown'})`, budget);
217
255
  return;
218
256
  }
219
- openPullRequest(ctx, io, item);
257
+ openPullRequest(ctx, io, state, item);
220
258
  }
221
259
  /**
222
260
  * The pass after `aidlc autopilot approve`: wait again at the next completed pause,
@@ -228,7 +266,7 @@ function advanceApproved(ctx, io, state, item) {
228
266
  return;
229
267
  if (currentPhase(ctx, item) === 'deployment') {
230
268
  item.step = 'building';
231
- openPullRequest(ctx, io, item);
269
+ openPullRequest(ctx, io, state, item);
232
270
  return;
233
271
  }
234
272
  // A relaunch is a build like any other: it waits for a slot, ahead of new items,
@@ -236,26 +274,26 @@ function advanceApproved(ctx, io, state, item) {
236
274
  if (building(state) >= buildSlots(ctx))
237
275
  return;
238
276
  item.step = 'building';
239
- ctx.say(`${item.name}: approved — relaunching the agent to carry on`);
277
+ tell(ctx, item, 'approved — relaunching the agent to carry on');
240
278
  launchAgent(ctx, item, agentPrompt(item, itemPauses(item)));
241
279
  }
242
- function openPullRequest(ctx, io, item) {
280
+ function openPullRequest(ctx, io, state, item) {
243
281
  const push = io.call('git', ['push', '-u', 'origin', item.branch], { cwd: item.worktree });
244
282
  if (push.status !== 0) {
245
- failItem(item, `push failed: ${fail(push)}`, 'building');
283
+ failItem(ctx, state, item, `push failed: ${fail(push)}`, 'building');
246
284
  return;
247
285
  }
248
286
  if (!item.pr) {
249
287
  const pr = io.call('gh', ['pr', 'create', '--base', 'main', '--head', item.branch, '--fill'], { cwd: item.worktree });
250
288
  const n = /\/pull\/(\d+)/.exec(pr.stdout)?.[1];
251
289
  if (pr.status !== 0 || (!n && !pr.dry)) {
252
- failItem(item, `pr create failed: ${fail(pr)}`, 'building');
290
+ failItem(ctx, state, item, `pr create failed: ${fail(pr)}`, 'building');
253
291
  return;
254
292
  }
255
293
  item.pr = n ? Number(n) : null;
256
294
  }
257
295
  item.step = 'pr-open';
258
- ctx.say(`${item.name}: pull request #${item.pr} open`);
296
+ tell(ctx, item, `pull request #${item.pr} open`);
259
297
  }
260
298
  /** `gh pr view`, or null when it cannot be read. Read-only, so a dry run makes it too. */
261
299
  function prView(ctx, io, item) {
@@ -277,7 +315,7 @@ function prView(ctx, io, item) {
277
315
  function droppedIfMerged(ctx, state, item, view, budget) {
278
316
  if (view?.state !== 'MERGED')
279
317
  return false;
280
- 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`);
281
319
  drop(state, item, budget);
282
320
  return true;
283
321
  }
@@ -292,7 +330,7 @@ function answeredByPerson(ctx, io, state, item, view, budget) {
292
330
  return true;
293
331
  }
294
332
  if (view?.state === 'CLOSED') {
295
- 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`);
296
334
  drop(state, item, budget);
297
335
  return true;
298
336
  }
@@ -307,31 +345,35 @@ function closeOut(ctx, io, state, item, budget) {
307
345
  if (branch.stdout.trim() !== 'main' && !ctx.dryRun) {
308
346
  // The merge is done; trying again next pass loses nothing, and an `error` here
309
347
  // 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()})`);
348
+ tell(ctx, item, `merged — closing it out waits for the primary checkout to be on main (it is on ${branch.stdout.trim()})`);
311
349
  return;
312
350
  }
313
351
  const pull = io.call('git', ['pull', '--rebase', '--autostash'], { cwd: ctx.repo });
314
352
  if (pull.status !== 0) {
315
- failItem(item, `pull failed: ${fail(pull)}`);
316
- ctx.say(`${item.name}: ${item.error}`);
353
+ const error = `pull failed: ${fail(pull)}`;
354
+ failItem(ctx, state, item, error);
355
+ tell(ctx, item, error);
317
356
  return;
318
357
  }
319
358
  // Closed by hand already: nothing to transition, and a second close-out commit
320
359
  // would fail on an empty index (autopilot-stop-controls/AC-22).
321
360
  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`);
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`);
323
363
  drop(state, item, budget);
324
364
  return;
325
365
  }
326
366
  for (const [label, step] of closeSteps(ctx, io, item)) {
327
367
  const r = step();
328
368
  if (r.status !== 0) {
329
- failItem(item, `${label} failed: ${fail(r)}`);
330
- ctx.say(`${item.name}: ${item.error}`);
369
+ const error = `${label} failed: ${fail(r)}`;
370
+ failItem(ctx, state, item, error);
371
+ tell(ctx, item, error);
331
372
  return;
332
373
  }
333
374
  }
334
- 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`);
335
377
  drop(state, item, budget);
336
378
  }
337
379
  /** Close the instance on main. Shared by both close-outs so they cannot drift. */
@@ -368,7 +410,7 @@ function handleConflict(ctx, io, state, item, budget) {
368
410
  item.conflict_fixes = tries + 1;
369
411
  dequeue(state, item);
370
412
  item.step = 'building';
371
- 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})`);
372
414
  launchAgent(ctx, item, conflictPrompt(item));
373
415
  }
374
416
  /**
@@ -385,14 +427,14 @@ function upToDateWithMain(ctx, io, state, item, resume, budget) {
385
427
  const wt = { cwd: item.worktree };
386
428
  const fetch = io.call('git', ['fetch', 'origin', 'main'], wt);
387
429
  if (fetch.status !== 0) {
388
- failItem(item, `update from main failed: ${fail(fetch)}`, resume);
430
+ failItem(ctx, state, item, `update from main failed: ${fail(fetch)}`, resume);
389
431
  return false;
390
432
  }
391
433
  const contains = io.call('git', ['merge-base', '--is-ancestor', 'origin/main', 'HEAD'], wt);
392
434
  if (contains.status === 0)
393
435
  return true;
394
436
  if (contains.status !== 1) {
395
- failItem(item, `update from main failed: ${fail(contains)}`, resume);
437
+ failItem(ctx, state, item, `update from main failed: ${fail(contains)}`, resume);
396
438
  return false;
397
439
  }
398
440
  const merge = io.call('git', ['merge', '--no-edit', 'origin/main'], wt);
@@ -405,11 +447,11 @@ function upToDateWithMain(ctx, io, state, item, resume, budget) {
405
447
  const push = io.call('git', ['push', 'origin', item.branch], wt);
406
448
  if (push.status !== 0) {
407
449
  // The merge commit is local; the building step pushes it again on resume.
408
- failItem(item, `push failed: ${fail(push)}`, 'building');
450
+ failItem(ctx, state, item, `push failed: ${fail(push)}`, 'building');
409
451
  return false;
410
452
  }
411
453
  item.step = 'pr-open';
412
- 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');
413
455
  return false;
414
456
  }
415
457
  function advancePr(ctx, io, state, item, budget) {
@@ -427,37 +469,43 @@ function advancePr(ctx, io, state, item, budget) {
427
469
  return;
428
470
  }
429
471
  if (checks.length === 0) {
472
+ item.ci = 'waiting';
430
473
  // GitHub runs no pull_request checks on a PR that conflicts, so "no checks
431
474
  // yet" would otherwise be waited on forever (AC-12).
432
475
  if (view?.mergeable === 'CONFLICTING') {
433
- 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`);
434
477
  upToDateWithMain(ctx, io, state, item, 'pr-open', budget);
435
478
  return;
436
479
  }
437
- 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`);
438
481
  return;
439
482
  }
440
- if (checks.some((c) => c.bucket === 'pending'))
483
+ if (checks.some((c) => c.bucket === 'pending')) {
484
+ item.ci = 'pending';
441
485
  return;
486
+ }
442
487
  const failing = checks.filter((c) => c.bucket === 'fail' || c.bucket === 'cancel').map((c) => c.name);
443
488
  if (failing.length === 0) {
489
+ item.ci = 'passed';
444
490
  if (atPr) {
445
491
  item.step = 'awaiting-merge';
446
- 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`);
447
494
  return;
448
495
  }
449
496
  item.step = 'queued';
450
497
  if (!state.queue.includes(item.name))
451
498
  state.queue.push(item.name);
452
- ctx.say(`${item.name}: CI green, queued for merge`);
499
+ tell(ctx, item, 'CI green, queued for merge');
453
500
  return;
454
501
  }
502
+ item.ci = 'failed';
455
503
  item.ci_failures = (item.ci_failures ?? 0) + 1;
456
504
  if (item.ci_failures >= CI_FAILURE_LIMIT) {
457
505
  park(ctx, io, state, item, `CI failed twice: ${failing.join(', ')}`, budget);
458
506
  return;
459
507
  }
460
- ctx.say(`${item.name}: CI failed (${failing.join(', ')}), relaunching the agent once`);
508
+ tell(ctx, item, `CI failed (${failing.join(', ')}), relaunching the agent once`);
461
509
  item.step = 'building';
462
510
  launchAgent(ctx, item, fixPrompt(item, failing));
463
511
  }
@@ -490,6 +538,7 @@ function serveQueue(ctx, io, state, budget) {
490
538
  return;
491
539
  }
492
540
  if (item.step === 'error') {
541
+ // An observation repeated every pass: the failure that halted the queue already notified.
493
542
  ctx.say(`queue halted: ${item.name} — ${item.error}`);
494
543
  return;
495
544
  }
@@ -499,7 +548,7 @@ function serveQueue(ctx, io, state, budget) {
499
548
  try {
500
549
  const status = JSON.parse(runs.stdout || '[]')[0]?.status;
501
550
  if (status === 'queued' || status === 'in_progress') {
502
- ctx.say(`${item.name}: waiting — a release is already in flight`);
551
+ tell(ctx, item, 'waiting — a release is already in flight');
503
552
  return;
504
553
  }
505
554
  }
@@ -508,7 +557,7 @@ function serveQueue(ctx, io, state, budget) {
508
557
  }
509
558
  const branch = io.call('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: ctx.repo });
510
559
  if (branch.stdout.trim() !== 'main' && !ctx.dryRun) {
511
- 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');
512
561
  return;
513
562
  }
514
563
  if (!upToDateWithMain(ctx, io, state, item, 'queued', budget))
@@ -517,7 +566,7 @@ function serveQueue(ctx, io, state, budget) {
517
566
  const head = io.call('git', ['rev-parse', 'HEAD'], { cwd: item.worktree });
518
567
  const sha = head.stdout.trim();
519
568
  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');
569
+ failItem(ctx, state, item, `merge failed: cannot read the branch head (${fail(head)})`, 'queued');
521
570
  return;
522
571
  }
523
572
  const release = ctx.config?.release_command;
@@ -540,12 +589,14 @@ function serveQueue(ctx, io, state, budget) {
540
589
  if (r.status !== 0) {
541
590
  // Halt. Only a failed merge resumes on the next `autopilot run`; from the
542
591
  // 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`);
592
+ const error = `${label} failed: ${fail(r)}`;
593
+ failItem(ctx, state, item, error, label === 'merge' ? 'queued' : undefined);
594
+ tell(ctx, item, `${error} — queue halted`);
545
595
  return;
546
596
  }
547
597
  }
548
- 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`);
549
600
  drop(state, item, budget);
550
601
  }
551
602
  /**
@@ -563,6 +614,7 @@ function parkUnstartable(ctx, io, file, failure) {
563
614
  return false;
564
615
  }
565
616
  ctx.say(`could not start ${file}: ${error} — parked`);
617
+ notice(ctx, 'item parked', `${file}: could not start: ${error}`);
566
618
  return true;
567
619
  }
568
620
  function startBuilds(ctx, io, state, budget) {
@@ -700,6 +752,7 @@ export function tick(ctx, state, budget) {
700
752
  return state;
701
753
  }
702
754
  const io = makeIO(ctx);
755
+ const before = stepsOf(state);
703
756
  try {
704
757
  for (const item of [...state.items]) {
705
758
  // `awaiting-review` is passed over: nothing to read until a person approves.
@@ -724,9 +777,26 @@ export function tick(ctx, state, budget) {
724
777
  state.backoff_until = new Date(ctx.now() + state.backoff_ms).toISOString();
725
778
  ctx.say(`rate limited (${err.message}) — backing off until ${state.backoff_until}`);
726
779
  }
780
+ markStepChanges(state, before, ctx.now());
727
781
  state.last_tick = new Date(ctx.now()).toISOString();
728
782
  return state;
729
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
+ }
730
800
  /** What a person is being waited on for, in words (autopilot-stop-controls/AC-14, AC-20). */
731
801
  function waitingFor(i) {
732
802
  if (i.step === 'awaiting-review') {
@@ -738,22 +808,33 @@ function waitingFor(i) {
738
808
  return ` — waiting for you to merge pull request #${i.pr}`;
739
809
  return '';
740
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
+ }
741
827
  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'}`);
828
+ const lines = [driverLine(ctx.fs.exists(ctx.paths.stop), state, ctx.now())];
749
829
  if (state.items.length === 0)
750
830
  lines.push('no items in flight');
751
831
  for (const i of state.items) {
752
832
  lines.push(`${i.name}: ${i.step}${i.pr ? ` PR #${i.pr}` : ''} — CI failures ${i.ci_failures ?? 0}` +
753
833
  `${waitingFor(i)}${i.error ? ` — last error: ${i.error}` : ''}`);
754
834
  }
755
- if (state.queue.length > 0)
756
- lines.push(`merge queue: ${state.queue.join(' → ')}`);
835
+ const queue = queueLine(state);
836
+ if (queue !== null)
837
+ lines.push(queue);
757
838
  return lines;
758
839
  }
759
840
  //# sourceMappingURL=driver.js.map