@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.
- package/dist/autopilot/driver.d.ts +25 -1
- package/dist/autopilot/driver.d.ts.map +1 -1
- package/dist/autopilot/driver.js +132 -51
- package/dist/autopilot/driver.js.map +1 -1
- package/dist/autopilot/notify.d.ts +33 -0
- package/dist/autopilot/notify.d.ts.map +1 -0
- package/dist/autopilot/notify.js +66 -0
- package/dist/autopilot/notify.js.map +1 -0
- package/dist/autopilot/run.d.ts +14 -1
- package/dist/autopilot/run.d.ts.map +1 -1
- package/dist/autopilot/run.js +44 -7
- package/dist/autopilot/run.js.map +1 -1
- package/dist/autopilot/state.d.ts +19 -0
- package/dist/autopilot/state.d.ts.map +1 -1
- package/dist/autopilot/state.js.map +1 -1
- package/dist/autopilot/team.d.ts +2 -1
- package/dist/autopilot/team.d.ts.map +1 -1
- package/dist/autopilot/team.js +6 -1
- package/dist/autopilot/team.js.map +1 -1
- package/dist/autopilot/watch.d.ts +58 -0
- package/dist/autopilot/watch.d.ts.map +1 -0
- package/dist/autopilot/watch.js +165 -0
- package/dist/autopilot/watch.js.map +1 -0
- package/dist/commands/autopilot.d.ts +10 -4
- package/dist/commands/autopilot.d.ts.map +1 -1
- package/dist/commands/autopilot.js +42 -16
- package/dist/commands/autopilot.js.map +1 -1
- package/package.json +2 -2
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
*
|
|
16
16
|
* @module
|
|
17
17
|
*/
|
|
18
|
-
import type {
|
|
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":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,EAAc,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/autopilot/driver.ts"],"names":[],"mappings":"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"}
|
package/dist/autopilot/driver.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
191
|
+
tell(ctx, item, `parked — ${reason}`);
|
|
192
|
+
notice(ctx, 'item parked', `${item.name}: ${reason}`);
|
|
161
193
|
drop(state, item, budget);
|
|
162
194
|
}
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
316
|
-
ctx
|
|
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
|
|
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
|
-
|
|
330
|
-
ctx
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
544
|
-
ctx
|
|
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
|
|
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
|
-
|
|
756
|
-
|
|
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
|