project-autopilot 12.0.0 → 12.0.1
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/README.md +10 -10
- package/SKILL.md +1 -3
- package/adapters/agents.json +11 -1
- package/config.example.json +5 -3
- package/config.schema.json +28 -9
- package/docs/configuration.md +5 -3
- package/package.json +1 -1
- package/scripts/adapter-drivers.mjs +209 -22
- package/scripts/agent-contract.mjs +24 -2
- package/scripts/agent-invocation.mjs +32 -5
- package/scripts/autopilot.mjs +787 -222
- package/scripts/config-layout.mjs +6 -0
- package/scripts/config-migration.mjs +3 -3
- package/scripts/config-validation.mjs +12 -4
- package/scripts/e2e-detect.mjs +30 -0
- package/scripts/escalation-naming.mjs +19 -0
- package/scripts/failure-contract.mjs +25 -9
- package/scripts/gen-config-schema.mjs +7 -0
- package/scripts/idle-hang-watchdog.mjs +34 -7
- package/scripts/implementation-baseline.mjs +95 -4
- package/scripts/model-discovery.mjs +176 -38
- package/scripts/opencode-generation.mjs +116 -0
- package/scripts/per-task-budget.mjs +11 -7
- package/scripts/plan-review-rounds.mjs +164 -9
- package/scripts/planning-boundary.mjs +16 -6
- package/scripts/planning-continuation.mjs +43 -23
- package/scripts/planning-review-decision.mjs +3 -0
- package/scripts/planning-review-wiring.mjs +12 -8
- package/scripts/relay-guards.mjs +133 -11
- package/scripts/review-canary.mjs +5 -2
- package/scripts/role-fallback.mjs +23 -8
- package/scripts/run-history.mjs +32 -1
- package/scripts/run-observability.mjs +46 -2
- package/scripts/setup-discovery.mjs +2 -1
- package/scripts/setup.mjs +183 -81
- package/scripts/skill-copy.mjs +49 -0
- package/scripts/task-requirements.mjs +24 -0
- package/scripts/watch-report.mjs +146 -21
- package/scripts/watch-summary.mjs +26 -3
- package/scripts/plan-mode-model-note.mjs +0 -27
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
export const RATE_LIMITED_FAILURE_CLASS = "RATE_LIMITED";
|
|
23
23
|
export const IDLE_HANG_FAILURE_CLASS = "IDLE_HANG";
|
|
24
24
|
export const PROVIDER_ERROR_FAILURE_CLASS = "PROVIDER_ERROR";
|
|
25
|
+
export const AUTH_REQUIRED_FAILURE_CLASS = "AUTH_REQUIRED";
|
|
25
26
|
|
|
26
27
|
function isRecord(value) {
|
|
27
28
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -175,8 +176,13 @@ export function skipExhaustedRoleModel({role, roleConfig, activeModel = null, ex
|
|
|
175
176
|
// fallback left: run the same run at once on `next` (fresh copy).
|
|
176
177
|
// - `{action: "escalate", reason}` — RATE_LIMITED with no fallback left
|
|
177
178
|
// (or no fallbacks configured): keep today's escalate-and-stop.
|
|
178
|
-
// - `{action: "none", reason}` — any
|
|
179
|
-
//
|
|
179
|
+
// - `{action: "none", reason}` — any other class: no switch, caller
|
|
180
|
+
// behavior unchanged.
|
|
181
|
+
//
|
|
182
|
+
// AUTH_REQUIRED (the CLI is logged out) switches only to a fallback on a
|
|
183
|
+
// DIFFERENT adapter: another model on the same adapter shares the same dead
|
|
184
|
+
// credentials, so it is never retried. With no other-adapter fallback it
|
|
185
|
+
// escalates at once with the log-in instruction.
|
|
180
186
|
//
|
|
181
187
|
// IDLE_HANG with `noOutput: true` (the run produced 0 bytes for its whole
|
|
182
188
|
// life) is treated like RATE_LIMITED for the switch (sticky, next untried
|
|
@@ -187,21 +193,24 @@ export function decideRoleFallback({role, roleConfig, activeModel = null, failur
|
|
|
187
193
|
const name = typeof role === "string" && role ? role : "unknown";
|
|
188
194
|
const silentHang = failureClass === IDLE_HANG_FAILURE_CLASS && noOutput === true;
|
|
189
195
|
const providerError = failureClass === PROVIDER_ERROR_FAILURE_CLASS;
|
|
190
|
-
|
|
196
|
+
const authRequired = failureClass === AUTH_REQUIRED_FAILURE_CLASS;
|
|
197
|
+
if (failureClass !== RATE_LIMITED_FAILURE_CLASS && !silentHang && !providerError && !authRequired) {
|
|
191
198
|
return {
|
|
192
199
|
action: "none",
|
|
193
200
|
reason: `${name}: failure class ${String(failureClass)} is not RATE_LIMITED; no fallback switch`,
|
|
194
201
|
};
|
|
195
202
|
}
|
|
196
203
|
const fallbacks = fallbackListOf(roleConfig);
|
|
197
|
-
const label = silentHang ? "IDLE_HANG (no output)" : providerError ? "PROVIDER_ERROR" : "RATE_LIMITED";
|
|
204
|
+
const label = silentHang ? "IDLE_HANG (no output)" : authRequired ? "AUTH_REQUIRED" : providerError ? "PROVIDER_ERROR" : "RATE_LIMITED";
|
|
198
205
|
if (fallbacks.length === 0) {
|
|
199
206
|
if (silentHang) {
|
|
200
207
|
return {action: "none", reason: `${name}: ${label} with no fallbacks configured; normal retry`};
|
|
201
208
|
}
|
|
202
209
|
return {
|
|
203
210
|
action: "escalate",
|
|
204
|
-
reason:
|
|
211
|
+
reason: authRequired
|
|
212
|
+
? `${name}: ${label} with no fallback on another adapter; log in required`
|
|
213
|
+
: `${name}: ${label} with no fallbacks configured; human intervention required`,
|
|
205
214
|
};
|
|
206
215
|
}
|
|
207
216
|
// Sticky order: the current position is the active entry when it matches
|
|
@@ -218,17 +227,23 @@ export function decideRoleFallback({role, roleConfig, activeModel = null, failur
|
|
|
218
227
|
currentIndex = found === -1 ? 0 : found + 1;
|
|
219
228
|
}
|
|
220
229
|
}
|
|
221
|
-
const
|
|
230
|
+
const from = currentIndex === 0 ? primary : fallbacks[currentIndex - 1];
|
|
231
|
+
// AUTH_REQUIRED is scoped to the CLI's own credentials, so a fallback model
|
|
232
|
+
// on the SAME adapter cannot help — only a different adapter (another CLI)
|
|
233
|
+
// can. Never retry the logged-out CLI, even through a configured sibling.
|
|
234
|
+
const next = fallbacks.slice(currentIndex).find((entry) =>
|
|
235
|
+
(!authRequired || entry.adapter !== from?.adapter) && !isExhausted(exhausted, entry, policy, nowMs)) ?? null;
|
|
222
236
|
if (!next) {
|
|
223
237
|
if (silentHang) {
|
|
224
238
|
return {action: "none", reason: `${name}: ${label} with all ${fallbacks.length} fallback(s) exhausted; normal retry`};
|
|
225
239
|
}
|
|
226
240
|
return {
|
|
227
241
|
action: "escalate",
|
|
228
|
-
reason:
|
|
242
|
+
reason: authRequired
|
|
243
|
+
? `${name}: ${label} with no untried fallback on another adapter; log in required`
|
|
244
|
+
: `${name}: ${label} with all ${fallbacks.length} fallback(s) exhausted; human intervention required`,
|
|
229
245
|
};
|
|
230
246
|
}
|
|
231
|
-
const from = currentIndex === 0 ? primary : fallbacks[currentIndex - 1];
|
|
232
247
|
return {
|
|
233
248
|
action: "switch",
|
|
234
249
|
next: copyModelEntry(next),
|
package/scripts/run-history.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { defaultIsPidAlive } from "./run-recovery.mjs";
|
|
|
8
8
|
import { growingTerm } from "./per-task-budget.mjs";
|
|
9
9
|
import { readOwnerPid, ownerPidAlive } from "./owner-pid.mjs";
|
|
10
10
|
import { buildReportModel, renderReport } from "./watch-report.mjs";
|
|
11
|
+
import { detectUiProject } from "./e2e-detect.mjs";
|
|
11
12
|
import { phases as standalonePhases } from "../adapters/standalone.mjs";
|
|
12
13
|
|
|
13
14
|
// true = alive, false = provably gone, null = could not tell (no/invalid pid, or
|
|
@@ -196,6 +197,9 @@ export function readRunMeta(dir, id) {
|
|
|
196
197
|
// only and may be stale or null (see observabilityMeta in
|
|
197
198
|
// autopilot.mjs). result.json first (#110), status.json fallback for old runs.
|
|
198
199
|
taskRef: result?.taskRef ?? status?.taskRef ?? null,
|
|
200
|
+
// The SpecKit/GSD planning stage this run worked on, recorded at launch.
|
|
201
|
+
// Null for non-planning runs and for standalone planning (`plan/tasks`).
|
|
202
|
+
planningStage: status?.planningStage ?? result?.planningStage ?? null,
|
|
199
203
|
attempt: status?.attempt ?? result?.attempt ?? null,
|
|
200
204
|
// Backlog #26 Step B: wrap-up marker (status.json only). Renders as a
|
|
201
205
|
// NOTE suffix on the run line; runs without it read null and render
|
|
@@ -537,6 +541,30 @@ function sameResetEvent(a, b) {
|
|
|
537
541
|
// same displayStatusLabel the old layout used (a run left "running" by a dead
|
|
538
542
|
// orchestrator reads as the stale label, never RUNNING), and a cancelled
|
|
539
543
|
// mission has no current run. All optional inputs are injectable for tests.
|
|
544
|
+
// Final-report note: shown only when tests.e2e is none/unset and a UI was detected.
|
|
545
|
+
export const NO_E2E_REPORT_NOTE = "no E2E — UI checked by component tests only";
|
|
546
|
+
export function noE2eReportNote(repo, layout) {
|
|
547
|
+
let raw = layout;
|
|
548
|
+
if (raw === undefined) {
|
|
549
|
+
try { raw = JSON.parse(fs.readFileSync(path.join(repo, ".autopilot", "config.json"), "utf8")); } catch { raw = null; }
|
|
550
|
+
}
|
|
551
|
+
const e2e = raw?.tests?.e2e;
|
|
552
|
+
if (e2e && e2e !== "none") return null;
|
|
553
|
+
try { return detectUiProject(repo) ? NO_E2E_REPORT_NOTE : null; } catch { return null; }
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Append the no-E2E note to a mission REPORT.md once (idempotent); returns true when written.
|
|
557
|
+
export function appendNoE2eNoteToReport(repo, file) {
|
|
558
|
+
try {
|
|
559
|
+
const note = noE2eReportNote(repo);
|
|
560
|
+
if (!note || !fs.existsSync(file)) return false;
|
|
561
|
+
const cur = fs.readFileSync(file, "utf8");
|
|
562
|
+
if (cur.includes(note)) return false;
|
|
563
|
+
fs.writeFileSync(file, `${cur.replace(/\n+$/, "")}\n\nNote: ${note}\n`);
|
|
564
|
+
return true;
|
|
565
|
+
} catch { return false; }
|
|
566
|
+
}
|
|
567
|
+
|
|
540
568
|
export function renderWatchReport(repo, mission, { tail = 40, isOwnerAlive, isAlive, phases, policy, events, now, skill, gateLog } = {}) {
|
|
541
569
|
const d = new Date();
|
|
542
570
|
const p = (n) => String(n).padStart(2, "0");
|
|
@@ -623,10 +651,13 @@ export function renderWatchReport(repo, mission, { tail = 40, isOwnerAlive, isAl
|
|
|
623
651
|
gateLog: gateLogText,
|
|
624
652
|
now: now ?? d.toISOString(),
|
|
625
653
|
skill: skill ?? null,
|
|
654
|
+
command: state?.command ?? null,
|
|
626
655
|
missionFallback: mission ?? null,
|
|
627
656
|
ownerAlive,
|
|
628
657
|
});
|
|
629
|
-
|
|
658
|
+
const e2eNote = noE2eReportNote(repo, configResult?.layout ?? null);
|
|
659
|
+
const out = renderReport(model, { stamp });
|
|
660
|
+
return e2eNote ? `${out.replace(/\n+$/, "")}\nNote: ${e2eNote}\n` : out;
|
|
630
661
|
}
|
|
631
662
|
|
|
632
663
|
// Direct-write the watch report file (same principle as progress.log: the
|
|
@@ -169,6 +169,10 @@ export function initRun(dir, meta = {}) {
|
|
|
169
169
|
// This is the field to read; `taskGroup` above is grouping-only (see
|
|
170
170
|
// observabilityMeta in autopilot.mjs) and may be stale or null.
|
|
171
171
|
taskRef: meta.taskRef ?? null,
|
|
172
|
+
// SpecKit/GSD planning stage this run worked on (`spec`/`plan`/`tasks` or
|
|
173
|
+
// `discuss`/`research`/`plan`/`plan-check`). Null for every other run and
|
|
174
|
+
// for standalone planning, whose report label stays `plan/tasks`.
|
|
175
|
+
planningStage: meta.planningStage ?? null,
|
|
172
176
|
attempt: meta.attempt ?? null,
|
|
173
177
|
// Backlog #26 Step B: wrap-up marker (`note: "wrap-up"` on the forced
|
|
174
178
|
// hand-off run). Shown in the watch report so the extra run after a cap
|
|
@@ -182,6 +186,11 @@ export function initRun(dir, meta = {}) {
|
|
|
182
186
|
configuredModel: meta.configuredModel ?? null,
|
|
183
187
|
resolvedModel: meta.resolvedModel ?? null,
|
|
184
188
|
modelMismatch: meta.modelMismatch ?? null,
|
|
189
|
+
// Backlog #7: reasoning level / opencode variant of the model actually
|
|
190
|
+
// launched (the active fallback/escalation entry's own values, never the
|
|
191
|
+
// primary's). Null when the CLI default applies. Additive.
|
|
192
|
+
reasoning: meta.reasoning ?? null,
|
|
193
|
+
variant: meta.variant ?? null,
|
|
185
194
|
// Mission 093 Part 1 task 2.3: run-scoped layout fields. snapshotBeforeAt
|
|
186
195
|
// and snapshotAfterAt are the end wall-clock times of the relay's safety
|
|
187
196
|
// snapshots; the matching *StartedAt fields let the report print the
|
|
@@ -207,10 +216,20 @@ export function initRun(dir, meta = {}) {
|
|
|
207
216
|
// list; a non-list or empty list degrades to null here. Additive — every
|
|
208
217
|
// reader above is untouched.
|
|
209
218
|
carriedForwardTasks: (Array.isArray(meta.carriedForwardTasks) && meta.carriedForwardTasks.length > 0 ? meta.carriedForwardTasks : null),
|
|
210
|
-
|
|
219
|
+
// The pre-run seed call defers the START line until the child process
|
|
220
|
+
// actually exists (onStart re-inits with the real pid). Until then the
|
|
221
|
+
// run is visibly PREPARING, never a fake START with PID=-.
|
|
222
|
+
last_action: meta.last_action ?? (meta.deferStart === true ? "preparing" : "started"),
|
|
211
223
|
};
|
|
212
224
|
fs.writeFileSync(path.join(dir, "status.json"), JSON.stringify(status, null, 2));
|
|
213
|
-
|
|
225
|
+
if (meta.deferStart !== true) {
|
|
226
|
+
appendProgress(dir, `START | ROLE=${meta.role ?? "-"} | PHASE=${meta.phase ?? "-"} | TASK=${meta.taskRef ?? "-"} | ATTEMPT=${meta.attempt ?? "-"} | PID=${meta.pid ?? "-"}`);
|
|
227
|
+
} else {
|
|
228
|
+
// Seed an empty progress.log so readers can tail it while the pre-run
|
|
229
|
+
// steps write their PREPARE lines; no fake START with PID=-.
|
|
230
|
+
const progressFile = path.join(dir, "progress.log");
|
|
231
|
+
if (!fs.existsSync(progressFile)) fs.writeFileSync(progressFile, "");
|
|
232
|
+
}
|
|
214
233
|
// Mission 094 Phase 3 task 3.2: keep the compact watch-summary cache current
|
|
215
234
|
// at the run-write points so a later render never re-reads every run dir.
|
|
216
235
|
// Best-effort (a failed cache write must never break a run); a dir that is
|
|
@@ -219,6 +238,31 @@ export function initRun(dir, meta = {}) {
|
|
|
219
238
|
return status;
|
|
220
239
|
}
|
|
221
240
|
|
|
241
|
+
// Mark a run as PREPARING (before its CLI exists) so status.json, and therefore
|
|
242
|
+
// the watch report, names the setup step that is running instead of showing a
|
|
243
|
+
// running run with pid null. Best-effort like every observability write.
|
|
244
|
+
export function setRunPreparing(dir, action) {
|
|
245
|
+
const name = String(action || "").trim();
|
|
246
|
+
if (!name) return false;
|
|
247
|
+
updateStatus(dir, { updated_at: localIso(), last_action: `preparing: ${name}` });
|
|
248
|
+
try { updateSummaryForRunDir(dir); } catch { /* ignore */ }
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Append one `PREPARE | <action>[ (<detail>)] | DURATION=<h:mm:ss>` line for a
|
|
253
|
+
// completed pre-run step (git guard, snapshot-before, …). The START line stays
|
|
254
|
+
// the only line that names the real PID, and keeps its existing format.
|
|
255
|
+
export function prepareRun(dir, { action, detail = null, durationMs = null } = {}) {
|
|
256
|
+
const name = String(action || "").trim();
|
|
257
|
+
if (!name) return false;
|
|
258
|
+
const suffix = detail ? ` (${detail})` : "";
|
|
259
|
+
const duration = Number.isFinite(Number(durationMs)) && Number(durationMs) >= 0 ? ` | DURATION=${formatDuration(Number(durationMs))}` : "";
|
|
260
|
+
appendProgress(dir, `PREPARE | ${name}${suffix}${duration}`);
|
|
261
|
+
updateStatus(dir, { updated_at: localIso(), last_action: `preparing: ${name}` });
|
|
262
|
+
try { updateSummaryForRunDir(dir); } catch { /* ignore */ }
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
222
266
|
export function observe(dir, { stream = "stdout", bytes = 0, minIntervalMs = 1000, last } = {}) {
|
|
223
267
|
// Backlog #6a: a chunk carrying a real tool action writes its OWN progress
|
|
224
268
|
// line (tool + command/file, extracted before the ACTIVITY snippet is
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { discoverAgentContract } from './agent-contract.mjs';
|
|
2
2
|
import { discoverCapabilities } from './model-discovery.mjs';
|
|
3
3
|
|
|
4
|
-
export async function buildDiscoveryCaches(availableIds, agentsMap, discoverFn = discoverAgentContract) {
|
|
4
|
+
export async function buildDiscoveryCaches(availableIds, agentsMap, discoverFn = discoverAgentContract, {onStart = null} = {}) {
|
|
5
5
|
const capabilityCache = new Map();
|
|
6
6
|
const contractCache = new Map();
|
|
7
7
|
for (const id of availableIds) {
|
|
8
8
|
try {
|
|
9
|
+
if (typeof onStart === "function") onStart(id, agentsMap[id]);
|
|
9
10
|
const contract = await discoverFn(id, agentsMap[id]);
|
|
10
11
|
contractCache.set(id, contract);
|
|
11
12
|
capabilityCache.set(id, contract?.capabilities || await discoverCapabilities(agentsMap[id]));
|