@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.
- package/dist/autopilot/driver.d.ts +25 -1
- package/dist/autopilot/driver.d.ts.map +1 -1
- package/dist/autopilot/driver.js +155 -52
- 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/dist/commands/spec.d.ts.map +1 -1
- package/dist/commands/spec.js +3 -1
- package/dist/commands/spec.js.map +1 -1
- package/dist/spec/delta.d.ts.map +1 -1
- package/dist/spec/delta.js +11 -2
- package/dist/spec/delta.js.map +1 -1
- package/dist/spec/fold.d.ts.map +1 -1
- package/dist/spec/fold.js +6 -0
- package/dist/spec/fold.js.map +1 -1
- package/dist/spec/types.d.ts +2 -0
- package/dist/spec/types.d.ts.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
|
@@ -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
|
|
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
|
|
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
|
|
191
|
+
tell(ctx, item, `parked — ${reason}`);
|
|
192
|
+
notice(ctx, 'item parked', `${item.name}: ${reason}`);
|
|
139
193
|
drop(state, item, budget);
|
|
140
194
|
}
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
294
|
-
ctx
|
|
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
|
|
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
|
-
|
|
308
|
-
ctx
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
522
|
-
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`);
|
|
523
595
|
return;
|
|
524
596
|
}
|
|
525
597
|
}
|
|
526
|
-
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`);
|
|
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
|
-
|
|
734
|
-
|
|
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
|