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
package/scripts/watch-report.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
import { normalizeFindings } from "./review-feedback.mjs";
|
|
5
|
+
|
|
4
6
|
// Mission 094 Phase 1 task 1.1 — the core report model.
|
|
5
7
|
//
|
|
6
8
|
// `buildReportModel` normalizes the three live data sources of a watch report
|
|
@@ -65,16 +67,20 @@ function attemptNumberOf(run) {
|
|
|
65
67
|
function readRunExtras(dir) {
|
|
66
68
|
const extras = {
|
|
67
69
|
findings: [],
|
|
70
|
+
notes: [],
|
|
68
71
|
carriedForwardTasks: null,
|
|
69
72
|
sessionNumber: null,
|
|
70
73
|
configuredModel: null,
|
|
71
74
|
resolvedModel: null,
|
|
72
75
|
modelMismatch: null,
|
|
76
|
+
reasoning: null,
|
|
77
|
+
variant: null,
|
|
73
78
|
snapshotBeforeStartedAt: null,
|
|
74
79
|
snapshotBeforeAt: null,
|
|
75
80
|
snapshotAfterStartedAt: null,
|
|
76
81
|
snapshotAfterAt: null,
|
|
77
82
|
planRevision: null,
|
|
83
|
+
planningStage: null,
|
|
78
84
|
note: null,
|
|
79
85
|
policyViolation: null,
|
|
80
86
|
failureClass: null,
|
|
@@ -90,12 +96,15 @@ function readRunExtras(dir) {
|
|
|
90
96
|
if (st.carriedForwardTasks !== undefined) extras.carriedForwardTasks = st.carriedForwardTasks;
|
|
91
97
|
if (st.configuredModel !== undefined) extras.configuredModel = st.configuredModel;
|
|
92
98
|
if (st.resolvedModel !== undefined) extras.resolvedModel = st.resolvedModel;
|
|
99
|
+
if (typeof st.reasoning === "string" && st.reasoning) extras.reasoning = st.reasoning;
|
|
100
|
+
if (typeof st.variant === "string" && st.variant) extras.variant = st.variant;
|
|
93
101
|
if (st.modelMismatch !== undefined) extras.modelMismatch = st.modelMismatch;
|
|
94
102
|
if (st.snapshotBeforeStartedAt !== undefined) extras.snapshotBeforeStartedAt = st.snapshotBeforeStartedAt;
|
|
95
103
|
if (st.snapshotBeforeAt !== undefined) extras.snapshotBeforeAt = st.snapshotBeforeAt;
|
|
96
104
|
if (st.snapshotAfterStartedAt !== undefined) extras.snapshotAfterStartedAt = st.snapshotAfterStartedAt;
|
|
97
105
|
if (st.snapshotAfterAt !== undefined) extras.snapshotAfterAt = st.snapshotAfterAt;
|
|
98
106
|
if (st.planRevision !== undefined) extras.planRevision = st.planRevision;
|
|
107
|
+
if (st.planningStage !== undefined) extras.planningStage = st.planningStage;
|
|
99
108
|
if (st.note !== undefined) extras.note = st.note;
|
|
100
109
|
if (st.interrupted_at !== undefined) extras.interrupted_at = st.interrupted_at;
|
|
101
110
|
if (st.interruption_reason !== undefined) extras.interruption_reason = st.interruption_reason;
|
|
@@ -104,10 +113,20 @@ function readRunExtras(dir) {
|
|
|
104
113
|
try {
|
|
105
114
|
const rs = JSON.parse(fs.readFileSync(path.join(dir, "result.json"), "utf8"));
|
|
106
115
|
if (rs && typeof rs === "object") {
|
|
107
|
-
if (
|
|
108
|
-
|
|
116
|
+
if (extras.planningStage == null && rs.planningStage !== undefined) extras.planningStage = rs.planningStage;
|
|
117
|
+
if (Array.isArray(rs.review?.findings) && rs.review.findings.length) extras.findings = rs.review.findings.map(String);
|
|
118
|
+
else if (Array.isArray(rs.findings) && rs.findings.length) extras.findings = rs.findings.map(String);
|
|
119
|
+
// A plan-review FAIL can record its findings only on verdict.findings
|
|
120
|
+
// (review.findings is null/empty); the watch summary must still show
|
|
121
|
+
// the reason, so fall back to the verdict array.
|
|
122
|
+
else if (Array.isArray(rs.verdict?.findings) && rs.verdict.findings.length) extras.findings = normalizeFindings(rs.verdict.findings);
|
|
123
|
+
// Backlog #8: minor notes never fail a plan review; show their count.
|
|
124
|
+
if (Array.isArray(rs.review?.notes) && rs.review.notes.length) extras.notes = rs.review.notes.map(String);
|
|
125
|
+
else if (Array.isArray(rs.verdict?.notes) && rs.verdict.notes.length) extras.notes = rs.verdict.notes.map(String);
|
|
109
126
|
if (extras.carriedForwardTasks == null && Array.isArray(rs.carriedForwardTasks)) extras.carriedForwardTasks = rs.carriedForwardTasks.slice();
|
|
110
127
|
if (extras.resolvedModel == null && typeof rs.resolvedModel === "string") extras.resolvedModel = rs.resolvedModel;
|
|
128
|
+
if (extras.reasoning == null && typeof rs.reasoning === "string" && rs.reasoning) extras.reasoning = rs.reasoning;
|
|
129
|
+
if (extras.variant == null && typeof rs.variant === "string" && rs.variant) extras.variant = rs.variant;
|
|
111
130
|
extras.policyViolation = (typeof rs.policyViolation === "string" && rs.policyViolation)
|
|
112
131
|
|| (typeof rs.policy?.violation === "string" && rs.policy.violation)
|
|
113
132
|
|| null;
|
|
@@ -126,7 +145,7 @@ function readRunExtras(dir) {
|
|
|
126
145
|
// (e.g. `resolvedModel`) silently dropped those extras (backlog #4).
|
|
127
146
|
const RUN_EXTRA_KEYS = [
|
|
128
147
|
"carriedForwardTasks", "sessionNumber", "resolvedModel", "configuredModel",
|
|
129
|
-
"modelMismatch", "snapshotBeforeStartedAt", "snapshotBeforeAt", "snapshotAfterStartedAt", "snapshotAfterAt", "planRevision", "note",
|
|
148
|
+
"modelMismatch", "reasoning", "variant", "snapshotBeforeStartedAt", "snapshotBeforeAt", "snapshotAfterStartedAt", "snapshotAfterAt", "planRevision", "planningStage", "note", "notes",
|
|
130
149
|
"policyViolation", "failureClass", "escalated", "interrupted_at", "interruption_reason",
|
|
131
150
|
];
|
|
132
151
|
function enrichRun(run) {
|
|
@@ -459,6 +478,32 @@ function buildPlanningRuns(runs, { status: missionStatus } = {}) {
|
|
|
459
478
|
};
|
|
460
479
|
}
|
|
461
480
|
|
|
481
|
+
// The revision counter the planning header shows. Standalone keeps the
|
|
482
|
+
// planner-run `planRevision` high-water mark; SpecKit and GSD gate loops keep
|
|
483
|
+
// their own counters in `state.planReviewRevisions` (keys such as
|
|
484
|
+
// `speckit-stage:specify`, or `gsd-*`), so read those too. GSD also records
|
|
485
|
+
// the latest stage revision in `state.gsdPlanningReview.revision`. Each gate
|
|
486
|
+
// has its own budget, so the header reports the highest counter used, never a
|
|
487
|
+
// sum that could exceed `max_plan_revisions`.
|
|
488
|
+
function statePlanningRevision(state, framework) {
|
|
489
|
+
if (framework !== "speckit" && framework !== "gsd") return null;
|
|
490
|
+
const values = [];
|
|
491
|
+
const revisions = state?.planReviewRevisions;
|
|
492
|
+
if (revisions && typeof revisions === "object") {
|
|
493
|
+
for (const [key, value] of Object.entries(revisions)) {
|
|
494
|
+
if (!Number.isInteger(value) || value < 0) continue;
|
|
495
|
+
if (framework === "speckit" && !key.startsWith("speckit-")) continue;
|
|
496
|
+
if (framework === "gsd" && !key.startsWith("gsd")) continue;
|
|
497
|
+
values.push(value);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (framework === "gsd") {
|
|
501
|
+
const latest = state?.gsdPlanningReview?.revision;
|
|
502
|
+
if (Number.isInteger(latest) && latest >= 0) values.push(latest);
|
|
503
|
+
}
|
|
504
|
+
return values.length ? Math.max(...values) : null;
|
|
505
|
+
}
|
|
506
|
+
|
|
462
507
|
// Final review rounds: each code_reviewer/verifier opens a round; fix-pass
|
|
463
508
|
// implementer runs between reviews belong to the preceding round.
|
|
464
509
|
function buildFinalReview(runs, { status: missionStatus, skipped } = {}) {
|
|
@@ -502,19 +547,32 @@ function isFinalReviewRun(run, phaseCount) {
|
|
|
502
547
|
return true;
|
|
503
548
|
}
|
|
504
549
|
|
|
505
|
-
function classifyStage(state, planning, phases, finalReview) {
|
|
550
|
+
function classifyStage(state, planning, phases, finalReview, runs = []) {
|
|
506
551
|
const statePhase = typeof state?.phase === "string" ? state.phase : "";
|
|
552
|
+
const stateStep = typeof state?.step === "string" ? state.step : "";
|
|
507
553
|
// The recorded state wins when it names a stage: a report rendered after a
|
|
508
554
|
// final review run finished must still read IMPLEMENTATION while the mission
|
|
509
|
-
// is still implementing.
|
|
555
|
+
// is still implementing. A plan-phase step (standalone-plan, plan_review, …)
|
|
556
|
+
// is a real stage signal even before state.phase has been written.
|
|
510
557
|
if (statePhase === "final_review") return "FINAL REVIEW";
|
|
511
|
-
if (statePhase === "planning" || statePhase === "plan_approval" || /
|
|
558
|
+
if (statePhase === "planning" || statePhase === "plan_approval" || /plan/i.test(stateStep)) return "PLANNING";
|
|
512
559
|
if (statePhase === "implementing" || statePhase === "verifying" || statePhase === "tasks_ready") return "IMPLEMENTATION";
|
|
513
560
|
if (finalReview.status === "skipped") return "FINAL REVIEW";
|
|
514
561
|
if (finalReview.runs.length > 0 && (finalReview.status === "in_progress" || finalReview.status === "passed")) return "FINAL REVIEW";
|
|
515
562
|
const anyImpl = phases.some((p) => p.runs.length > 0);
|
|
516
|
-
if (
|
|
517
|
-
return "
|
|
563
|
+
if (anyImpl) return "IMPLEMENTATION";
|
|
564
|
+
if (planning.runs.length > 0) return "PLANNING";
|
|
565
|
+
// Last resort before STARTING: the newest run's role says what the mission
|
|
566
|
+
// is doing even when state.json has no phase/step yet (the first seconds of
|
|
567
|
+
// a run). An impl/verify/review role is known work, not an unknown state.
|
|
568
|
+
const latest = [...(Array.isArray(runs) ? runs : [])]
|
|
569
|
+
.sort((a, b) => (Date.parse(a?.started_at ?? a?.startedAt) || 0) - (Date.parse(b?.started_at ?? b?.startedAt) || 0))
|
|
570
|
+
.pop();
|
|
571
|
+
const role = roleOf(latest);
|
|
572
|
+
if (PLANNING_ROLES.has(role)) return "PLANNING";
|
|
573
|
+
if (IMPLEMENTATION_ROLES.has(role) || REVIEW_ROLES.has(role)) return "IMPLEMENTATION";
|
|
574
|
+
// Nothing has started yet: never pretend the mission is implementing.
|
|
575
|
+
return "STARTING";
|
|
518
576
|
}
|
|
519
577
|
|
|
520
578
|
// ── Mission 094 task 1.2 — enrichment helpers ───────────────────────────────
|
|
@@ -589,6 +647,7 @@ export function modelDisplay(run) {
|
|
|
589
647
|
export function providerTroubleLabel(text) {
|
|
590
648
|
const s = String(text ?? "");
|
|
591
649
|
if (/ProviderHeaderTimeout/.test(s)) return "ProviderHeaderTimeout";
|
|
650
|
+
if (/free usage limit reached|usage limit reached/i.test(s) || /insufficient[_\s-]?quota|quota[_\s-]?(?:exceeded|exhausted)/i.test(s)) return "Free quota used up";
|
|
592
651
|
if (/Rate limit exceeded/i.test(s) || /rate_limit_exceeded/.test(s)) return "Rate limit exceeded";
|
|
593
652
|
const silent = s.match(/silent\s+(\d+)\s*m[,\s]+no child process/i);
|
|
594
653
|
if (silent) return `silent ${silent[1]}m, no child process`;
|
|
@@ -883,6 +942,10 @@ function decodeProgressEntry(raw) {
|
|
|
883
942
|
const text = String(m[2] ?? "").split("\n")[0].trim();
|
|
884
943
|
return { at, kind, text: text || m[1] };
|
|
885
944
|
}
|
|
945
|
+
if (/^PREPARE \|/.test(rest)) {
|
|
946
|
+
const text = rest.replace(/^PREPARE \|\s*/, "").trim();
|
|
947
|
+
return { at, kind: "PREP", text: text || "preparing" };
|
|
948
|
+
}
|
|
886
949
|
const prep = rest.match(/^PREP\b\s*(.*)$/);
|
|
887
950
|
if (prep) return { at, kind: "PREP", text: prep[1] || "pre-run snapshot started" };
|
|
888
951
|
const warn = rest.match(/^WARN\b\s*(.*)$/);
|
|
@@ -992,6 +1055,8 @@ export function detectCurrent({ runs = [], gates = [], status = "RUNNING", gateL
|
|
|
992
1055
|
attempt: attemptNumberOf(r),
|
|
993
1056
|
status: "running",
|
|
994
1057
|
model: r.displayModel ?? modelDisplay(r),
|
|
1058
|
+
reasoning: typeof r.reasoning === "string" && r.reasoning ? r.reasoning : null,
|
|
1059
|
+
variant: typeof r.variant === "string" && r.variant ? r.variant : null,
|
|
995
1060
|
sessionNumber: r.sessionNumber ?? null,
|
|
996
1061
|
session: r.sessionLabel ?? null,
|
|
997
1062
|
pid: r.pid ?? null,
|
|
@@ -1001,6 +1066,7 @@ export function detectCurrent({ runs = [], gates = [], status = "RUNNING", gateL
|
|
|
1001
1066
|
snapshotAfterAt: r.snapshotAfterAt ?? null,
|
|
1002
1067
|
usage: r.usage ?? null,
|
|
1003
1068
|
note: r.note ?? null,
|
|
1069
|
+
lastAction: r.last_action ?? null,
|
|
1004
1070
|
};
|
|
1005
1071
|
}
|
|
1006
1072
|
|
|
@@ -1145,7 +1211,7 @@ function missionStartedAt(state, runs) {
|
|
|
1145
1211
|
return earliest;
|
|
1146
1212
|
}
|
|
1147
1213
|
|
|
1148
|
-
export function buildReportModel({ state = null, runs = [], events = [], phases = [], policy = {}, layout = null, progress = [], gateLog = null, now = null, skill = null, missionFallback = null, ownerAlive } = {}) {
|
|
1214
|
+
export function buildReportModel({ state = null, runs = [], events = [], phases = [], policy = {}, layout = null, progress = [], gateLog = null, now = null, skill = null, command = null, missionFallback = null, ownerAlive } = {}) {
|
|
1149
1215
|
const eventList = Array.isArray(events) ? events : [];
|
|
1150
1216
|
const progressList = Array.isArray(progress) ? progress : [];
|
|
1151
1217
|
const phaseDefs = normalizePhases(phases);
|
|
@@ -1264,6 +1330,11 @@ export function buildReportModel({ state = null, runs = [], events = [], phases
|
|
|
1264
1330
|
|
|
1265
1331
|
const mission = typeof state?.mission === "string" && state.mission ? state.mission : null;
|
|
1266
1332
|
const framework = state?.planningDetection?.framework ?? null;
|
|
1333
|
+
const storedRevision = statePlanningRevision(state, framework);
|
|
1334
|
+
if (storedRevision != null) {
|
|
1335
|
+
const runRevision = typeof planning.revision === "number" ? planning.revision : 0;
|
|
1336
|
+
planning.revision = Math.max(runRevision, storedRevision);
|
|
1337
|
+
}
|
|
1267
1338
|
|
|
1268
1339
|
// For ended missions, compute the end time from statusInfo.at (cancel/stop/escalation)
|
|
1269
1340
|
// or from the last run's end time for COMPLETED missions.
|
|
@@ -1308,7 +1379,7 @@ export function buildReportModel({ state = null, runs = [], events = [], phases
|
|
|
1308
1379
|
status: statusInfo.status,
|
|
1309
1380
|
statusInfo,
|
|
1310
1381
|
stopLine: statusInfo.stopLine,
|
|
1311
|
-
stage: classifyStage(state, planning, phasesOut, finalReview),
|
|
1382
|
+
stage: classifyStage(state, planning, phasesOut, finalReview, enrichedRuns),
|
|
1312
1383
|
phaseIndex,
|
|
1313
1384
|
phaseNumber: phaseIndex != null ? phaseIndex + 1 : null,
|
|
1314
1385
|
phaseName: phaseIndex != null ? (phasesOut[phaseIndex]?.name ?? null) : null,
|
|
@@ -1321,7 +1392,8 @@ export function buildReportModel({ state = null, runs = [], events = [], phases
|
|
|
1321
1392
|
missionStartedAt: missionStartedAt(state, enrichedRuns),
|
|
1322
1393
|
missionEndedAt,
|
|
1323
1394
|
missionFallback,
|
|
1324
|
-
skill: skill && typeof skill === "object" ? skill : null,
|
|
1395
|
+
skill: skill && typeof skill === "object" ? skill : (state?.skill && typeof state.skill === "object" ? state.skill : null),
|
|
1396
|
+
command: command ?? state?.command ?? null,
|
|
1325
1397
|
continuationAllowance: allowance,
|
|
1326
1398
|
gateCommand,
|
|
1327
1399
|
gates,
|
|
@@ -1598,6 +1670,8 @@ function planningNote(run, model) {
|
|
|
1598
1670
|
} else if (roleOf(run) === "plan_reviewer") {
|
|
1599
1671
|
const n = Array.isArray(run.findings) ? run.findings.length : 0;
|
|
1600
1672
|
parts.push(`${n} finding${n === 1 ? "" : "s"}`);
|
|
1673
|
+
const notes = Array.isArray(run.notes) ? run.notes.length : 0;
|
|
1674
|
+
if (notes) parts.push(`notes: ${notes} minor`);
|
|
1601
1675
|
}
|
|
1602
1676
|
const failureReason = failureReasonText(run, model?.runs);
|
|
1603
1677
|
if (failureReason) parts.push(failureReason);
|
|
@@ -1665,14 +1739,40 @@ function renderRows(rows, { indent = " ", widths: sharedWidths = null, collec
|
|
|
1665
1739
|
return lines;
|
|
1666
1740
|
}
|
|
1667
1741
|
|
|
1668
|
-
//
|
|
1669
|
-
//
|
|
1742
|
+
// The planning TASK cell. Standalone planning keeps the historical
|
|
1743
|
+
// `plan/tasks` label (its planner writes and its reviewer reads both files).
|
|
1744
|
+
// SpecKit rows name the stage the run worked on (`spec`/`plan`/`tasks`) and
|
|
1745
|
+
// GSD rows their stage (`discuss`/`research`/`plan`/`plan-check`), both from
|
|
1746
|
+
// the stage recorded on the run at launch. A legacy run without that field
|
|
1747
|
+
// keeps the old `plan/tasks` label rather than inventing a stage.
|
|
1748
|
+
const PLANNING_STAGE_LABELS = {
|
|
1749
|
+
specify: "spec", spec: "spec", "review-spec": "spec", "spec-review": "spec",
|
|
1750
|
+
plan: "plan", "review-plan": "plan", "plan-review": "plan",
|
|
1751
|
+
tasks: "tasks", "review-tasks": "tasks", "tasks-review": "tasks",
|
|
1752
|
+
discuss: "discuss", research: "research",
|
|
1753
|
+
"plan-check": "plan-check", "review-plan-check": "plan-check",
|
|
1754
|
+
};
|
|
1755
|
+
function planningStageLabel(run) {
|
|
1756
|
+
const raw = run?.planningStage ?? run?.stage ?? run?.planningStep ?? null;
|
|
1757
|
+
if (typeof raw !== "string" || !raw.trim()) return null;
|
|
1758
|
+
const key = raw.trim().toLowerCase()
|
|
1759
|
+
.replace(/^(?:speckit|gsd)-/, "")
|
|
1760
|
+
.replace(/-revision-\d+$/, "")
|
|
1761
|
+
.replace(/-review-failed$/, "");
|
|
1762
|
+
return PLANNING_STAGE_LABELS[key] ?? key;
|
|
1763
|
+
}
|
|
1764
|
+
function planningTaskLabel(run) {
|
|
1765
|
+
return planningStageLabel(run) ?? "plan/tasks";
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// A planning run as a shared-table row: standalone keeps `plan/tasks`, while
|
|
1769
|
+
// SpecKit/GSD rows show the stage the run actually worked on.
|
|
1670
1770
|
function planningRow(run, model) {
|
|
1671
1771
|
return {
|
|
1672
1772
|
running: isRunning(run),
|
|
1673
1773
|
role: run?.dirName || run?.id || "-",
|
|
1674
1774
|
model: run?.displayModel ?? modelDisplay(run),
|
|
1675
|
-
task:
|
|
1775
|
+
task: planningTaskLabel(run),
|
|
1676
1776
|
status: runStatusCell(run),
|
|
1677
1777
|
duration: durationCell(run, model?.now ?? null),
|
|
1678
1778
|
snapshot: snapshotCell(run),
|
|
@@ -1836,6 +1936,7 @@ export function stageText(model) {
|
|
|
1836
1936
|
const status = String(model?.status ?? "").toUpperCase();
|
|
1837
1937
|
if (status === "COMPLETED" || status === "CANCELLED") return "done";
|
|
1838
1938
|
const stage = model?.stage;
|
|
1939
|
+
if (stage === "STARTING") return "STARTING";
|
|
1839
1940
|
if (stage === "PLANNING") return "PLANNING";
|
|
1840
1941
|
if (stage === "FINAL REVIEW") {
|
|
1841
1942
|
const limit = Number(model?.policy?.final_review_fix_rounds);
|
|
@@ -1845,6 +1946,7 @@ export function stageText(model) {
|
|
|
1845
1946
|
}
|
|
1846
1947
|
return "FINAL REVIEW";
|
|
1847
1948
|
}
|
|
1949
|
+
if (stage !== "IMPLEMENTATION") return "STARTING";
|
|
1848
1950
|
const phase = model?.phaseNumber;
|
|
1849
1951
|
const total = Array.isArray(model?.phases) ? model.phases.length : 0;
|
|
1850
1952
|
const name = stripPhasePrefix(model?.phaseName);
|
|
@@ -2038,29 +2140,42 @@ export function capsText(model) {
|
|
|
2038
2140
|
return result;
|
|
2039
2141
|
}
|
|
2040
2142
|
|
|
2041
|
-
// `Skill:` names the skill copy the orchestrator runs.
|
|
2042
|
-
//
|
|
2043
|
-
//
|
|
2143
|
+
// `Skill:` names the skill copy the orchestrator runs. autopilot records it on
|
|
2144
|
+
// the mission state (`skill-copy.mjs`) at run start as {version, mode, path};
|
|
2145
|
+
// older runs have no record and keep the explicit not-recorded label.
|
|
2044
2146
|
export function skillText(model) {
|
|
2045
2147
|
const skill = model?.skill;
|
|
2046
|
-
if (skill && (skill.mode || skill.path)) {
|
|
2148
|
+
if (skill && (skill.version || skill.mode || skill.path)) {
|
|
2047
2149
|
const mode = skill.mode || "production";
|
|
2048
2150
|
const at = skill.syncedAt ? formatStamp(skill.syncedAt).slice(0, 16) : null;
|
|
2049
|
-
|
|
2151
|
+
const head = [skill.version, mode, skill.path || "-"].filter((value) => value != null && value !== "").join(" · ");
|
|
2152
|
+
return `${head}${at ? ` (synced ${at})` : ""}`;
|
|
2050
2153
|
}
|
|
2051
2154
|
return "unknown · skill copy location not recorded";
|
|
2052
2155
|
}
|
|
2053
2156
|
|
|
2157
|
+
// `Command:` names the entry command that started the run. autopilot records
|
|
2158
|
+
// it on the mission state (`skill-copy.mjs` recordCommand) at run start as
|
|
2159
|
+
// {name, stopAfter}; older runs have no record and print nothing.
|
|
2160
|
+
export function commandText(model) {
|
|
2161
|
+
const name = model?.command?.name;
|
|
2162
|
+
if (!name) return null;
|
|
2163
|
+
const stopAfter = model.command.stopAfter;
|
|
2164
|
+
return `ap ${name} (${stopAfter ? `stops after ${stopAfter}` : "full loop"})`;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2054
2167
|
// `=== [<stamp> — ]PIPELINE STATE ===` block (watcher.md Sections): mission,
|
|
2055
|
-
// status, stage, budgets used/limit, retries, totals, caps, skill copy and the Events
|
|
2168
|
+
// status, command, stage, budgets used/limit, retries, totals, caps, skill copy and the Events
|
|
2056
2169
|
// line. No current-run detail leaks in here.
|
|
2057
2170
|
export function renderPipelineState(model, { stamp = null } = {}) {
|
|
2058
2171
|
const head = stamp ? `=== ${stamp} — PIPELINE STATE ===` : "=== PIPELINE STATE ===";
|
|
2059
2172
|
const missionName = model?.mission ?? model?.missionFallback ?? "-";
|
|
2173
|
+
const commandLine = commandText(model);
|
|
2060
2174
|
const lines = [
|
|
2061
2175
|
head,
|
|
2062
2176
|
`Mission: ${missionName}`,
|
|
2063
2177
|
label("Status") + statusText(model),
|
|
2178
|
+
...(commandLine ? [label("Command") + commandLine] : []),
|
|
2064
2179
|
label("Stage") + stageText(model),
|
|
2065
2180
|
label("Budgets") + budgetsText(model),
|
|
2066
2181
|
label("Retries") + retriesText(model),
|
|
@@ -2107,6 +2222,7 @@ const FAILURE_CLASS_LABELS = {
|
|
|
2107
2222
|
COMMAND_FAILED: "command failed",
|
|
2108
2223
|
SERVER_UNAVAILABLE: "server unavailable",
|
|
2109
2224
|
RATE_LIMITED: "rate limited",
|
|
2225
|
+
AUTH_REQUIRED: "authentication required (log in)",
|
|
2110
2226
|
PROVIDER_ERROR: "provider error",
|
|
2111
2227
|
IDLE_HANG: "idle hang",
|
|
2112
2228
|
IMPLEMENTATION_REJECTED: "implementation rejected",
|
|
@@ -2624,6 +2740,7 @@ function capForRole(model, role) {
|
|
|
2624
2740
|
// those). FINAL REVIEW may add its fix round; PLANNING is bare.
|
|
2625
2741
|
export function currentStageText(model) {
|
|
2626
2742
|
const stage = model?.stage;
|
|
2743
|
+
if (stage === "STARTING") return "STARTING";
|
|
2627
2744
|
if (stage === "PLANNING") return "PLANNING";
|
|
2628
2745
|
if (stage === "FINAL REVIEW") {
|
|
2629
2746
|
const limit = Number(model?.policy?.final_review_fix_rounds);
|
|
@@ -2703,6 +2820,10 @@ export function renderCurrentRun(model) {
|
|
|
2703
2820
|
if (current.role) roleParts.push(current.role);
|
|
2704
2821
|
if (current.taskRef) roleParts.push(`task ${current.taskRef}`);
|
|
2705
2822
|
if (current.model && current.model !== "-") roleParts.push(`model ${current.model}`);
|
|
2823
|
+
if (current.model && current.model !== "-") {
|
|
2824
|
+
if (current.reasoning) roleParts.push(`reasoning ${current.reasoning}`);
|
|
2825
|
+
if (current.variant && current.variant !== current.reasoning) roleParts.push(`variant ${current.variant}`);
|
|
2826
|
+
}
|
|
2706
2827
|
if (current.session) roleParts.push(current.session);
|
|
2707
2828
|
if (roleParts.length) out.push(label("role") + roleParts.join(" · "));
|
|
2708
2829
|
|
|
@@ -2710,8 +2831,12 @@ export function renderCurrentRun(model) {
|
|
|
2710
2831
|
if (current.pid != null) {
|
|
2711
2832
|
status += ` · pid ${current.pid} (alive)`;
|
|
2712
2833
|
} else {
|
|
2834
|
+
const preparing = typeof current.lastAction === "string" && /^preparing:\s*/.test(current.lastAction)
|
|
2835
|
+
? current.lastAction.replace(/^preparing:\s*/, "")
|
|
2836
|
+
: null;
|
|
2837
|
+
if (preparing) status += ` · PREPARING ${preparing} (no CLI process yet)`;
|
|
2713
2838
|
const silenceMs = current.updatedAt && now ? Date.parse(now) - Date.parse(current.updatedAt) : null;
|
|
2714
|
-
if (Number.isFinite(silenceMs) && silenceMs >= 10 * 60 * 1000) {
|
|
2839
|
+
if (!preparing && Number.isFinite(silenceMs) && silenceMs >= 10 * 60 * 1000) {
|
|
2715
2840
|
status += ` · ⚠ silent ${Math.floor(silenceMs / 60000)}m, no child process`;
|
|
2716
2841
|
}
|
|
2717
2842
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
readWatchEvents,
|
|
8
8
|
readRunMeta,
|
|
9
9
|
} from "./run-history.mjs";
|
|
10
|
+
import { normalizeFindings } from "./review-feedback.mjs";
|
|
10
11
|
|
|
11
12
|
// Mission 094 Phase 3 task 3.1 — the compact watch-summary cache.
|
|
12
13
|
//
|
|
@@ -30,7 +31,7 @@ import {
|
|
|
30
31
|
// unchanged, so a run that finishes (its status.json is rewritten) or a new
|
|
31
32
|
// event forces a rebuild.
|
|
32
33
|
|
|
33
|
-
export const SUMMARY_VERSION =
|
|
34
|
+
export const SUMMARY_VERSION = 7;
|
|
34
35
|
|
|
35
36
|
// Mission-scoped cache path, with the flat legacy fallback used elsewhere in
|
|
36
37
|
// run-history.mjs (watchRunsDir).
|
|
@@ -108,16 +109,20 @@ function stampMapFresh(stored, current, skipName, skipEvents) {
|
|
|
108
109
|
function readRunExtras(dir) {
|
|
109
110
|
const extras = {
|
|
110
111
|
findings: [],
|
|
112
|
+
notes: [],
|
|
111
113
|
carriedForwardTasks: null,
|
|
112
114
|
sessionNumber: null,
|
|
113
115
|
resolvedModel: null,
|
|
114
116
|
configuredModel: null,
|
|
115
117
|
modelMismatch: null,
|
|
118
|
+
reasoning: null,
|
|
119
|
+
variant: null,
|
|
116
120
|
snapshotBeforeStartedAt: null,
|
|
117
121
|
snapshotBeforeAt: null,
|
|
118
122
|
snapshotAfterStartedAt: null,
|
|
119
123
|
snapshotAfterAt: null,
|
|
120
124
|
planRevision: null,
|
|
125
|
+
planningStage: null,
|
|
121
126
|
note: null,
|
|
122
127
|
policyViolation: null,
|
|
123
128
|
failureClass: null,
|
|
@@ -133,12 +138,15 @@ function readRunExtras(dir) {
|
|
|
133
138
|
if (st.carriedForwardTasks !== undefined && st.carriedForwardTasks !== null) extras.carriedForwardTasks = st.carriedForwardTasks;
|
|
134
139
|
if (st.configuredModel !== undefined && st.configuredModel !== null) extras.configuredModel = st.configuredModel;
|
|
135
140
|
if (st.resolvedModel !== undefined && st.resolvedModel !== null) extras.resolvedModel = st.resolvedModel;
|
|
141
|
+
if (typeof st.reasoning === "string" && st.reasoning) extras.reasoning = st.reasoning;
|
|
142
|
+
if (typeof st.variant === "string" && st.variant) extras.variant = st.variant;
|
|
136
143
|
if (st.modelMismatch !== undefined && st.modelMismatch !== null) extras.modelMismatch = st.modelMismatch;
|
|
137
144
|
if (st.snapshotBeforeStartedAt !== undefined) extras.snapshotBeforeStartedAt = st.snapshotBeforeStartedAt;
|
|
138
145
|
if (st.snapshotBeforeAt !== undefined) extras.snapshotBeforeAt = st.snapshotBeforeAt;
|
|
139
146
|
if (st.snapshotAfterStartedAt !== undefined) extras.snapshotAfterStartedAt = st.snapshotAfterStartedAt;
|
|
140
147
|
if (st.snapshotAfterAt !== undefined) extras.snapshotAfterAt = st.snapshotAfterAt;
|
|
141
148
|
if (st.planRevision !== undefined) extras.planRevision = st.planRevision;
|
|
149
|
+
if (st.planningStage !== undefined) extras.planningStage = st.planningStage;
|
|
142
150
|
if (st.note !== undefined) extras.note = st.note;
|
|
143
151
|
if (st.interrupted_at !== undefined) extras.interrupted_at = st.interrupted_at;
|
|
144
152
|
if (st.interruption_reason !== undefined) extras.interruption_reason = st.interruption_reason;
|
|
@@ -147,10 +155,17 @@ function readRunExtras(dir) {
|
|
|
147
155
|
try {
|
|
148
156
|
const rs = JSON.parse(fs.readFileSync(path.join(dir, "result.json"), "utf8"));
|
|
149
157
|
if (rs && typeof rs === "object") {
|
|
150
|
-
if (
|
|
151
|
-
|
|
158
|
+
if (extras.planningStage == null && rs.planningStage !== undefined) extras.planningStage = rs.planningStage;
|
|
159
|
+
if (Array.isArray(rs.review?.findings) && rs.review.findings.length) extras.findings = rs.review.findings.map(String);
|
|
160
|
+
else if (Array.isArray(rs.findings) && rs.findings.length) extras.findings = rs.findings.map(String);
|
|
161
|
+
// Plan-review FAILs can carry their findings only on verdict.findings.
|
|
162
|
+
else if (Array.isArray(rs.verdict?.findings) && rs.verdict.findings.length) extras.findings = normalizeFindings(rs.verdict.findings);
|
|
163
|
+
if (Array.isArray(rs.review?.notes) && rs.review.notes.length) extras.notes = rs.review.notes.map(String);
|
|
164
|
+
else if (Array.isArray(rs.verdict?.notes) && rs.verdict.notes.length) extras.notes = rs.verdict.notes.map(String);
|
|
152
165
|
if (extras.carriedForwardTasks == null && Array.isArray(rs.carriedForwardTasks)) extras.carriedForwardTasks = rs.carriedForwardTasks.slice();
|
|
153
166
|
if (extras.resolvedModel == null && typeof rs.resolvedModel === "string") extras.resolvedModel = rs.resolvedModel;
|
|
167
|
+
if (extras.reasoning == null && typeof rs.reasoning === "string" && rs.reasoning) extras.reasoning = rs.reasoning;
|
|
168
|
+
if (extras.variant == null && typeof rs.variant === "string" && rs.variant) extras.variant = rs.variant;
|
|
154
169
|
extras.policyViolation = (typeof rs.policyViolation === "string" && rs.policyViolation)
|
|
155
170
|
|| (typeof rs.policy?.violation === "string" && rs.policy.violation)
|
|
156
171
|
|| null;
|
|
@@ -170,16 +185,20 @@ function compactRun(run) {
|
|
|
170
185
|
return {
|
|
171
186
|
...base,
|
|
172
187
|
findings: Array.isArray(base.findings) ? base.findings.map(String) : extras.findings,
|
|
188
|
+
notes: Array.isArray(base.notes) ? base.notes.map(String) : extras.notes,
|
|
173
189
|
carriedForwardTasks: base.carriedForwardTasks !== undefined ? base.carriedForwardTasks : extras.carriedForwardTasks,
|
|
174
190
|
sessionNumber: base.sessionNumber !== undefined ? base.sessionNumber : extras.sessionNumber,
|
|
175
191
|
resolvedModel: base.resolvedModel !== undefined ? base.resolvedModel : extras.resolvedModel,
|
|
176
192
|
configuredModel: base.configuredModel !== undefined ? base.configuredModel : extras.configuredModel,
|
|
177
193
|
modelMismatch: base.modelMismatch !== undefined ? base.modelMismatch : extras.modelMismatch,
|
|
194
|
+
reasoning: base.reasoning !== undefined ? base.reasoning : extras.reasoning,
|
|
195
|
+
variant: base.variant !== undefined ? base.variant : extras.variant,
|
|
178
196
|
snapshotBeforeStartedAt: base.snapshotBeforeStartedAt !== undefined ? base.snapshotBeforeStartedAt : extras.snapshotBeforeStartedAt,
|
|
179
197
|
snapshotBeforeAt: base.snapshotBeforeAt !== undefined ? base.snapshotBeforeAt : extras.snapshotBeforeAt,
|
|
180
198
|
snapshotAfterStartedAt: base.snapshotAfterStartedAt !== undefined ? base.snapshotAfterStartedAt : extras.snapshotAfterStartedAt,
|
|
181
199
|
snapshotAfterAt: base.snapshotAfterAt !== undefined ? base.snapshotAfterAt : extras.snapshotAfterAt,
|
|
182
200
|
planRevision: base.planRevision !== undefined ? base.planRevision : extras.planRevision,
|
|
201
|
+
planningStage: base.planningStage !== undefined ? base.planningStage : extras.planningStage,
|
|
183
202
|
note: base.note !== undefined ? base.note : extras.note,
|
|
184
203
|
policyViolation: base.policyViolation !== undefined ? base.policyViolation : extras.policyViolation,
|
|
185
204
|
failureClass: base.failureClass !== undefined ? base.failureClass : extras.failureClass,
|
|
@@ -192,16 +211,20 @@ function compactRun(run) {
|
|
|
192
211
|
function normalizeRecord(record) {
|
|
193
212
|
const r = compactRun(record);
|
|
194
213
|
if (!Array.isArray(r.findings)) r.findings = [];
|
|
214
|
+
if (!Array.isArray(r.notes)) r.notes = [];
|
|
195
215
|
if (r.carriedForwardTasks === undefined) r.carriedForwardTasks = null;
|
|
196
216
|
if (r.sessionNumber === undefined) r.sessionNumber = null;
|
|
197
217
|
if (r.resolvedModel === undefined) r.resolvedModel = null;
|
|
198
218
|
if (r.configuredModel === undefined) r.configuredModel = null;
|
|
199
219
|
if (r.modelMismatch === undefined) r.modelMismatch = null;
|
|
220
|
+
if (r.reasoning === undefined) r.reasoning = null;
|
|
221
|
+
if (r.variant === undefined) r.variant = null;
|
|
200
222
|
if (r.snapshotBeforeStartedAt === undefined) r.snapshotBeforeStartedAt = null;
|
|
201
223
|
if (r.snapshotBeforeAt === undefined) r.snapshotBeforeAt = null;
|
|
202
224
|
if (r.snapshotAfterStartedAt === undefined) r.snapshotAfterStartedAt = null;
|
|
203
225
|
if (r.snapshotAfterAt === undefined) r.snapshotAfterAt = null;
|
|
204
226
|
if (r.planRevision === undefined) r.planRevision = null;
|
|
227
|
+
if (r.planningStage === undefined) r.planningStage = null;
|
|
205
228
|
if (r.note === undefined) r.note = null;
|
|
206
229
|
if (r.policyViolation === undefined) r.policyViolation = null;
|
|
207
230
|
if (r.failureClass === undefined) r.failureClass = null;
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
// Backlog #108: Claude Code runs read-only (--permission-mode plan) sessions on
|
|
2
|
-
// Sonnet when the session model is Haiku. Pure helpers, warn-only.
|
|
3
|
-
export const READ_ONLY_ROLES = ["plan_reviewer", "code_reviewer", "verifier"];
|
|
4
|
-
|
|
5
|
-
export function isHaikuOnReadOnlyClaudeRole(role, adapter, model) {
|
|
6
|
-
if (!READ_ONLY_ROLES.includes(role)) return false;
|
|
7
|
-
if (adapter !== "claude") return false;
|
|
8
|
-
const m = String(model || "").trim().toLowerCase();
|
|
9
|
-
return m === "haiku" || /^claude-haiku-/.test(m) || /^anthropic\/claude-haiku-/.test(m);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function planModeModelNote(role) {
|
|
13
|
-
return `Note: Claude Code runs read-only (plan-mode) sessions on Sonnet when Haiku is selected — role ${role} will actually run as Sonnet and use Sonnet usage.`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// One line per read-only Claude role (primary and each fallback) using Haiku.
|
|
17
|
-
export function planModeModelWarnings(roles) {
|
|
18
|
-
const out = [];
|
|
19
|
-
for (const [name, r] of Object.entries(roles || {})) {
|
|
20
|
-
if (!r) continue;
|
|
21
|
-
if (isHaikuOnReadOnlyClaudeRole(name, r.adapter, r.model)) out.push(planModeModelNote(name));
|
|
22
|
-
for (const fb of Array.isArray(r.fallbacks) ? r.fallbacks : []) {
|
|
23
|
-
if (isHaikuOnReadOnlyClaudeRole(name, fb?.adapter, fb?.model)) out.push(planModeModelNote(name));
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return out;
|
|
27
|
-
}
|