agent-dealer 1.2.7 → 1.2.9
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/bundle/server/dist/adapters/agent-deck-bind.js +16 -6
- package/bundle/server/dist/adapters/agent-deck-bind.test.js +74 -0
- package/bundle/server/dist/adapters/agent-health.js +14 -3
- package/bundle/server/dist/adapters/github.js +84 -5
- package/bundle/server/dist/adapters/github.test.js +83 -1
- package/bundle/server/dist/adapters/muse-capability.js +443 -24
- package/bundle/server/dist/adapters/muse-capability.test.js +469 -25
- package/bundle/server/dist/adapters/muse-visual-qa.js +114 -0
- package/bundle/server/dist/adapters/muse-visual-qa.test.js +68 -0
- package/bundle/server/dist/adapters/worktree-deps.js +96 -0
- package/bundle/server/dist/adapters/worktree-deps.test.js +144 -0
- package/bundle/server/dist/capacity/muse-probe.js +56 -9
- package/bundle/server/dist/capacity/muse-probe.test.js +188 -1
- package/bundle/server/dist/coordinator/admission.js +14 -1
- package/bundle/server/dist/coordinator/admission.test.js +197 -4
- package/bundle/server/dist/coordinator/auto-merge.integration.test.js +276 -0
- package/bundle/server/dist/coordinator/auto-merge.js +244 -4
- package/bundle/server/dist/coordinator/checks-wait-config.js +19 -0
- package/bundle/server/dist/coordinator/commands.js +284 -23
- package/bundle/server/dist/coordinator/commands.test.js +47 -0
- package/bundle/server/dist/coordinator/deck-outage.integration.test.js +4 -2
- package/bundle/server/dist/coordinator/developer-effect.js +224 -4
- package/bundle/server/dist/coordinator/developer-effect.test.js +288 -3
- package/bundle/server/dist/coordinator/execution-report.js +6 -0
- package/bundle/server/dist/coordinator/execution-report.test.js +7 -0
- package/bundle/server/dist/coordinator/failure-cause.js +64 -1
- package/bundle/server/dist/coordinator/failure-cause.test.js +92 -1
- package/bundle/server/dist/coordinator/failure-reason.js +7 -0
- package/bundle/server/dist/coordinator/failure-reason.test.js +14 -0
- package/bundle/server/dist/coordinator/human-resolution.js +36 -1
- package/bundle/server/dist/coordinator/human-resolution.test.js +33 -0
- package/bundle/server/dist/coordinator/merge-conflict-sync.js +647 -0
- package/bundle/server/dist/coordinator/merge-conflict-sync.test.js +121 -0
- package/bundle/server/dist/coordinator/muse-developer.integration.test.js +9 -4
- package/bundle/server/dist/coordinator/muse-spawn.js +122 -29
- package/bundle/server/dist/coordinator/muse-spawn.test.js +210 -0
- package/bundle/server/dist/coordinator/operator-criteria.js +168 -0
- package/bundle/server/dist/coordinator/operator-criteria.test.js +78 -0
- package/bundle/server/dist/coordinator/operator-verification.integration.test.js +347 -0
- package/bundle/server/dist/coordinator/playbook-feedback.js +693 -0
- package/bundle/server/dist/coordinator/playbook-feedback.test.js +702 -0
- package/bundle/server/dist/coordinator/projection.js +10 -4
- package/bundle/server/dist/coordinator/projection.test.js +13 -0
- package/bundle/server/dist/coordinator/prompts-execution-contract.test.js +107 -0
- package/bundle/server/dist/coordinator/prompts.js +211 -2
- package/bundle/server/dist/coordinator/prompts.test.js +231 -0
- package/bundle/server/dist/coordinator/recovery.js +57 -3
- package/bundle/server/dist/coordinator/recovery.test.js +67 -8
- package/bundle/server/dist/coordinator/reflect-trigger.js +33 -169
- package/bundle/server/dist/coordinator/reflect-trigger.test.js +149 -200
- package/bundle/server/dist/coordinator/repair-cycle.integration.test.js +1 -0
- package/bundle/server/dist/coordinator/reviewer-effect.js +16 -1
- package/bundle/server/dist/coordinator/reviewer-result.js +6 -0
- package/bundle/server/dist/coordinator/routing.js +45 -6
- package/bundle/server/dist/coordinator/routing.test.js +76 -15
- package/bundle/server/dist/coordinator/session-timeouts.js +30 -0
- package/bundle/server/dist/coordinator/usage-cap-defer.js +61 -0
- package/bundle/server/dist/coordinator/usage-cap.integration.test.js +1 -1
- package/bundle/server/dist/coordinator/worker-loop.js +18 -4
- package/bundle/server/dist/db/ci-attempts-migration.test.js +70 -0
- package/bundle/server/dist/db/index.js +18 -0
- package/bundle/server/dist/db/schema.sql +5 -0
- package/bundle/server/dist/docs-execution-analysis.test.js +1 -0
- package/bundle/server/dist/repository/artifacts-for-issue.js +3 -3
- package/bundle/server/dist/repository/human-actions.js +14 -0
- package/bundle/server/dist/repository/issues.js +67 -6
- package/bundle/server/dist/repository/work-items.js +7 -0
- package/bundle/server/dist/repository/worker-sessions.js +51 -2
- package/bundle/server/dist/routes/human-actions.js +10 -8
- package/bundle/server/dist/routes/issues-execution-contract.test.js +321 -0
- package/bundle/server/dist/routes/issues.js +19 -2
- package/bundle/server/dist/runners/muse-code-args.js +25 -0
- package/bundle/server/dist/runners/muse-code-args.test.js +30 -0
- package/bundle/server/dist/runners/muse-code-jsonl.js +106 -11
- package/bundle/server/dist/runners/muse-config-core.js +18 -1
- package/bundle/server/dist/runners/muse-config.test.js +37 -0
- package/bundle/server/dist/runners/muse-serve-session.js +5 -0
- package/bundle/server/dist/runners/spawn-cli.js +116 -0
- package/bundle/server/dist/runners/spawn-cli.test.js +124 -0
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/index-CU6fNntg.js +64 -0
- package/bundle/server/static-ui/assets/{index-DyAJNyfV.css → index-K_YcYkQU.css} +1 -1
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/execution-analysis.d.ts +22 -22
- package/bundle/shared/dist/execution-contract.d.ts +117 -0
- package/bundle/shared/dist/execution-contract.js +307 -0
- package/bundle/shared/dist/execution-contract.test.d.ts +2 -0
- package/bundle/shared/dist/execution-contract.test.js +499 -0
- package/bundle/shared/dist/execution-report.d.ts +9 -9
- package/bundle/shared/dist/execution-report.js +2 -0
- package/bundle/shared/dist/failure-cause.d.ts +4 -4
- package/bundle/shared/dist/failure-cause.js +2 -0
- package/bundle/shared/dist/human-actions.d.ts +4 -4
- package/bundle/shared/dist/human-actions.js +17 -0
- package/bundle/shared/dist/index.d.ts +85 -84
- package/bundle/shared/dist/index.js +3 -0
- package/bundle/shared/dist/issues.d.ts +176 -16
- package/bundle/shared/dist/issues.js +22 -0
- package/bundle/shared/dist/issues.test.js +3 -0
- package/bundle/shared/dist/outbound-draft.d.ts +12 -12
- package/bundle/shared/dist/worker-sessions.d.ts +10 -0
- package/bundle/shared/dist/worker-sessions.js +8 -0
- package/bundle/shared/dist/worker-sessions.test.js +1 -0
- package/bundle/shared/dist/workflow.d.ts +4 -4
- package/bundle/shared/dist/workflow.js +16 -0
- package/bundle/shared/dist/workflow.test.js +3 -0
- package/bundle/shared/package.json +1 -1
- package/dist/action.js +4 -1
- package/dist/action.test.js +42 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-yLyxRd-7.js +0 -64
|
@@ -4,13 +4,21 @@
|
|
|
4
4
|
// granting developer sessions the shell/write tool, and every developer round after it surfaced
|
|
5
5
|
// only as a generic `dirty_worktree` escalation. So whenever `muse --version` reports a version not
|
|
6
6
|
// yet checked, one real developer-posture session is run whose only path to success is a shell
|
|
7
|
-
// call; the result is cached per version (and persisted, so a restart does not re-bill it)
|
|
7
|
+
// call; the result is cached per version (and persisted, so a restart does not re-bill it).
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
9
|
+
// NOT-308: that gate stalled execution ~15 minutes on an inconclusive (timed-out) probe —
|
|
10
|
+
// "we don't know yet" blocked exactly like a proven regression, retried forever on a flat
|
|
11
|
+
// timer, and never asked anyone to decide. New rule: an update is never assumed guilty.
|
|
12
|
+
// While the current version is unchecked (in flight) or its last result is inconclusive
|
|
13
|
+
// (error), admission proceeds on the last confirmed baseline and verification continues
|
|
14
|
+
// in the background. Only a *confirmed* capability loss (`missing`) blocks admission,
|
|
15
|
+
// and it escalates to exactly one human action per version (acknowledge/override, or pin
|
|
16
|
+
// /roll back outside Dealer). Inconclusive results retry with backoff (1/2/4 min), at most
|
|
17
|
+
// 3 attempts per version; then they escalate once as "could not verify" and stop probing
|
|
18
|
+
// (while a confirmed baseline exists to work on). A fresh install with no confirmed
|
|
19
|
+
// baseline at all keeps fail-closed behavior — and because nothing could ever unblock
|
|
20
|
+
// it otherwise, its inconclusive checks keep retrying at the maximum backoff past
|
|
21
|
+
// exhaustion until a version is confirmed.
|
|
14
22
|
//
|
|
15
23
|
// The probe is a fresh `muse exec` of the on-disk binary (never the long-lived serve host, which can
|
|
16
24
|
// still be the pre-update build). Checks are serialized: a version reported mid-probe is checked
|
|
@@ -26,8 +34,16 @@ import path from "node:path";
|
|
|
26
34
|
import { DEVELOPER_ROLE_CEILING } from "@agent-dealer/shared";
|
|
27
35
|
import { MUSE_CLI_ENV, resolveMuseBin } from "../cli-env.js";
|
|
28
36
|
import { getDataDir } from "../db/index.js";
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
import { createHumanAction, listHumanActionsByRequestId } from "../repository/human-actions.js";
|
|
38
|
+
/**
|
|
39
|
+
* NOT-308: an inconclusive check retries with backoff, not a flat timer — the delay after
|
|
40
|
+
* the Nth consecutive error is ERROR_BACKOFF_MS[N-1]. After MAX_ERROR_ATTEMPTS errors the
|
|
41
|
+
* version is exhausted: it escalates once as "could not verify" and is never re-probed
|
|
42
|
+
* automatically (the 4-minute entry is the delay a fourth attempt would have waited —
|
|
43
|
+
* asserting it never fires is the exhaustion proof).
|
|
44
|
+
*/
|
|
45
|
+
const ERROR_BACKOFF_MS = [60_000, 2 * 60_000, 4 * 60_000];
|
|
46
|
+
const MAX_ERROR_ATTEMPTS = 3;
|
|
31
47
|
const PROBE_TIMEOUT_MS = 5 * 60_000;
|
|
32
48
|
const PROBE_MAX_MODEL_STEPS = 20;
|
|
33
49
|
const STATE_FILE = "muse-capability.json";
|
|
@@ -45,23 +61,47 @@ let settledHook = () => { };
|
|
|
45
61
|
function statePath() {
|
|
46
62
|
return path.join(getDataDir(), STATE_FILE);
|
|
47
63
|
}
|
|
64
|
+
function normalizeChecked(raw, currentVersion) {
|
|
65
|
+
if (!raw || typeof raw !== "object")
|
|
66
|
+
return null;
|
|
67
|
+
const c = raw;
|
|
68
|
+
if (typeof c.version !== "string" || c.version !== currentVersion)
|
|
69
|
+
return null;
|
|
70
|
+
if (c.status !== "capable" && c.status !== "missing" && c.status !== "error")
|
|
71
|
+
return null;
|
|
72
|
+
if (typeof c.checkedAt !== "number")
|
|
73
|
+
return null;
|
|
74
|
+
const status = c.status;
|
|
75
|
+
const base = {
|
|
76
|
+
version: c.version,
|
|
77
|
+
checkedAt: c.checkedAt,
|
|
78
|
+
// Pre-NOT-308 files have no attempts counter: a recorded error counts as its first.
|
|
79
|
+
attempts: typeof c.attempts === "number" ? c.attempts : status === "error" ? 1 : 0,
|
|
80
|
+
durationMs: typeof c.durationMs === "number" ? c.durationMs : null,
|
|
81
|
+
};
|
|
82
|
+
if (status === "capable")
|
|
83
|
+
return { ...base, status };
|
|
84
|
+
return { ...base, status, detail: typeof c.detail === "string" ? c.detail : "" };
|
|
85
|
+
}
|
|
48
86
|
function loadState() {
|
|
49
87
|
if (state)
|
|
50
88
|
return state;
|
|
51
89
|
try {
|
|
52
90
|
const parsed = JSON.parse(fs.readFileSync(statePath(), "utf8"));
|
|
53
|
-
const lastChecked = parsed.lastChecked && typeof parsed.lastChecked.version === "string" ? parsed.lastChecked : null;
|
|
54
91
|
const current = parsed.current && typeof parsed.current.version === "string"
|
|
55
92
|
? { version: parsed.current.version, from: typeof parsed.current.from === "string" ? parsed.current.from : null }
|
|
56
93
|
: null;
|
|
57
94
|
state = {
|
|
58
95
|
confirmedVersion: typeof parsed.confirmedVersion === "string" ? parsed.confirmedVersion : null,
|
|
59
96
|
current,
|
|
60
|
-
lastChecked: lastChecked
|
|
97
|
+
lastChecked: normalizeChecked(parsed.lastChecked, current?.version ?? null),
|
|
98
|
+
overriddenVersions: Array.isArray(parsed.overriddenVersions)
|
|
99
|
+
? parsed.overriddenVersions.filter((v) => typeof v === "string")
|
|
100
|
+
: [],
|
|
61
101
|
};
|
|
62
102
|
}
|
|
63
103
|
catch {
|
|
64
|
-
state = { confirmedVersion: null, current: null, lastChecked: null };
|
|
104
|
+
state = { confirmedVersion: null, current: null, lastChecked: null, overriddenVersions: [] };
|
|
65
105
|
}
|
|
66
106
|
return state;
|
|
67
107
|
}
|
|
@@ -75,12 +115,36 @@ function saveState(next) {
|
|
|
75
115
|
console.warn(`[muse-capability] could not persist ${statePath()}: ${String(err)}`);
|
|
76
116
|
}
|
|
77
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* NOT-308: the safety net (a dirty, shell-less developer session ending on an unconfirmed
|
|
120
|
+
* version) cannot await the paid probe, so it registers here: when the check for `version`
|
|
121
|
+
* settles conclusively bad (`missing`) or inconclusively exhausted, the watcher issue gets
|
|
122
|
+
* its escalation then. Watchers are best-effort and capped — admission-time `ensure`
|
|
123
|
+
* covers every verdict anyway, with dedupe.
|
|
124
|
+
*/
|
|
125
|
+
let missWatchers = [];
|
|
126
|
+
function addMissWatcher(version, issueId) {
|
|
127
|
+
if (missWatchers.some((w) => w.version === version && w.issueId === issueId))
|
|
128
|
+
return;
|
|
129
|
+
missWatchers.push({ version, issueId });
|
|
130
|
+
if (missWatchers.length > 50)
|
|
131
|
+
missWatchers = missWatchers.slice(-50);
|
|
132
|
+
}
|
|
133
|
+
/** Drop watchers for every version but `keep` (a version change orphaned them). */
|
|
134
|
+
function dropMissWatchersExcept(keep) {
|
|
135
|
+
if (keep === null)
|
|
136
|
+
missWatchers = [];
|
|
137
|
+
else if (missWatchers.some((w) => w.version !== keep)) {
|
|
138
|
+
missWatchers = missWatchers.filter((w) => w.version === keep);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
78
141
|
/** Record `version` as the one Muse now reports; a change remembers what it changed from. */
|
|
79
142
|
function observe(version) {
|
|
80
143
|
const s = loadState();
|
|
81
144
|
if (s.current?.version === version)
|
|
82
145
|
return s;
|
|
83
146
|
const from = s.current?.version ?? s.confirmedVersion;
|
|
147
|
+
dropMissWatchersExcept(version);
|
|
84
148
|
saveState({ ...s, current: { version, from: from !== version ? from : null }, lastChecked: null });
|
|
85
149
|
return state;
|
|
86
150
|
}
|
|
@@ -95,7 +159,15 @@ export function parseMuseVersion(output) {
|
|
|
95
159
|
function transition(from, to) {
|
|
96
160
|
return from && from !== to ? `Muse Code updated ${from} → ${to}` : `Muse Code ${to}`;
|
|
97
161
|
}
|
|
98
|
-
function
|
|
162
|
+
function formatDuration(ms) {
|
|
163
|
+
if (ms === null || !Number.isFinite(ms) || ms < 0)
|
|
164
|
+
return "unknown time";
|
|
165
|
+
const s = Math.round(ms / 1000);
|
|
166
|
+
if (s < 60)
|
|
167
|
+
return `${s}s`;
|
|
168
|
+
return `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
169
|
+
}
|
|
170
|
+
function issueFor(checked, from, exhausted = false) {
|
|
99
171
|
if (checked.status === "capable")
|
|
100
172
|
return [];
|
|
101
173
|
const head = transition(from, checked.version);
|
|
@@ -108,12 +180,18 @@ function issueFor(checked, from) {
|
|
|
108
180
|
},
|
|
109
181
|
];
|
|
110
182
|
}
|
|
183
|
+
// Reachable only with no confirmed baseline (fresh install): with a baseline, an
|
|
184
|
+
// inconclusive result never blocks. The check always retries (checkDue keeps an
|
|
185
|
+
// unconfirmed version on the maximum backoff), so there is no "will not retry" text.
|
|
111
186
|
return [
|
|
112
187
|
{
|
|
113
188
|
code: "runtime_capability",
|
|
114
189
|
message: `Could not verify Muse Code developer ${CAPABILITY} after version change ` +
|
|
115
190
|
`(${from && from !== checked.version ? `${from} → ` : ""}${checked.version}): ` +
|
|
116
|
-
`${checked.detail} — developer admission blocked
|
|
191
|
+
`${checked.detail} — developer admission blocked` +
|
|
192
|
+
(exhausted
|
|
193
|
+
? `; escalated for a human decision (no further automatic checks for this version)`
|
|
194
|
+
: `; the check retries automatically`),
|
|
117
195
|
},
|
|
118
196
|
];
|
|
119
197
|
}
|
|
@@ -126,17 +204,34 @@ function verifyingIssue(version, from) {
|
|
|
126
204
|
},
|
|
127
205
|
];
|
|
128
206
|
}
|
|
129
|
-
/**
|
|
207
|
+
/** True once an inconclusive version has spent its whole retry budget. */
|
|
208
|
+
function isExhausted(checked) {
|
|
209
|
+
return checked.status === "error" && checked.attempts >= MAX_ERROR_ATTEMPTS;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Current version is unchecked, or its could-not-verify result is due its next backoff
|
|
213
|
+
* retry. An exhausted version stops probing — unless there is no confirmed baseline at
|
|
214
|
+
* all (fresh install): then stopping would block admission permanently with no path to
|
|
215
|
+
* unblock (no session can be admitted, so the safety net can never fire), so retries
|
|
216
|
+
* continue at the maximum backoff until some version is confirmed.
|
|
217
|
+
*/
|
|
130
218
|
function checkDue(s) {
|
|
131
219
|
if (!s.current)
|
|
132
220
|
return false;
|
|
133
221
|
const checked = s.lastChecked;
|
|
134
222
|
if (!checked)
|
|
135
223
|
return true;
|
|
136
|
-
|
|
224
|
+
if (checked.status !== "error")
|
|
225
|
+
return false;
|
|
226
|
+
if (!isExhausted(checked)) {
|
|
227
|
+
return Date.now() - checked.checkedAt >= ERROR_BACKOFF_MS[checked.attempts - 1];
|
|
228
|
+
}
|
|
229
|
+
return (s.confirmedVersion === null &&
|
|
230
|
+
Date.now() - checked.checkedAt >= ERROR_BACKOFF_MS[ERROR_BACKOFF_MS.length - 1]);
|
|
137
231
|
}
|
|
138
232
|
function startCheck(version) {
|
|
139
233
|
const gen = generation;
|
|
234
|
+
const startedAt = Date.now();
|
|
140
235
|
const promise = (async () => {
|
|
141
236
|
let result;
|
|
142
237
|
try {
|
|
@@ -152,7 +247,18 @@ function startCheck(version) {
|
|
|
152
247
|
// must not overwrite state for the newer one (which is checked next, below).
|
|
153
248
|
if (prev.current?.version !== version)
|
|
154
249
|
return;
|
|
155
|
-
|
|
250
|
+
// Only consecutive errors accumulate: a conclusive verdict resets the budget, and a
|
|
251
|
+
// safety-net forced probe past exhaustion just keeps the version exhausted.
|
|
252
|
+
const prevAttempts = prev.lastChecked?.version === version && prev.lastChecked.status === "error"
|
|
253
|
+
? prev.lastChecked.attempts
|
|
254
|
+
: 0;
|
|
255
|
+
const checked = {
|
|
256
|
+
version,
|
|
257
|
+
checkedAt: Date.now(),
|
|
258
|
+
attempts: result.status === "error" ? prevAttempts + 1 : 0,
|
|
259
|
+
durationMs: Date.now() - startedAt,
|
|
260
|
+
...result,
|
|
261
|
+
};
|
|
156
262
|
saveState({
|
|
157
263
|
...prev,
|
|
158
264
|
confirmedVersion: result.status === "capable" ? version : prev.confirmedVersion,
|
|
@@ -161,9 +267,30 @@ function startCheck(version) {
|
|
|
161
267
|
const from = prev.current.from;
|
|
162
268
|
if (result.status === "capable") {
|
|
163
269
|
console.log(`[muse-capability] ${transition(from, version)}: developer ${CAPABILITY} confirmed`);
|
|
270
|
+
dropMissWatchersExcept(null);
|
|
164
271
|
}
|
|
165
|
-
else {
|
|
272
|
+
else if (result.status === "missing") {
|
|
166
273
|
console.warn(`[muse-capability] ${issueFor(checked, from)[0].message}`);
|
|
274
|
+
escalateWatchedVersion(version);
|
|
275
|
+
}
|
|
276
|
+
else if (isExhausted(checked)) {
|
|
277
|
+
// With a baseline the version is done (escalated once, admission on the baseline);
|
|
278
|
+
// with none the checks continue at the maximum backoff — and admission stays
|
|
279
|
+
// blocked — until some version is confirmed.
|
|
280
|
+
console.warn(`[muse-capability] Could not verify Muse Code developer ${CAPABILITY} for ${version} ` +
|
|
281
|
+
`after ${checked.attempts} inconclusive checks (last probe ran ${formatDuration(checked.durationMs)}): ` +
|
|
282
|
+
(prev.confirmedVersion
|
|
283
|
+
? `escalating once, no further automatic checks; admission continues on baseline ${prev.confirmedVersion}`
|
|
284
|
+
: `escalated for a human decision; automatic checks continue at the maximum backoff; admission stays blocked (no confirmed baseline yet)`));
|
|
285
|
+
escalateWatchedVersion(version);
|
|
286
|
+
}
|
|
287
|
+
else if (checked.status === "error") {
|
|
288
|
+
console.warn(`[muse-capability] Could not verify Muse Code developer ${CAPABILITY} for ${version} ` +
|
|
289
|
+
`(attempt ${checked.attempts}/${MAX_ERROR_ATTEMPTS}, last probe ran ${formatDuration(checked.durationMs)}): ` +
|
|
290
|
+
`${checked.detail} — retrying in ${formatDuration(ERROR_BACKOFF_MS[Math.min(checked.attempts, ERROR_BACKOFF_MS.length) - 1])}; ` +
|
|
291
|
+
(prev.confirmedVersion
|
|
292
|
+
? `admission continues on baseline ${prev.confirmedVersion}`
|
|
293
|
+
: `admission stays blocked (no confirmed baseline yet)`));
|
|
167
294
|
}
|
|
168
295
|
})().finally(() => {
|
|
169
296
|
if (inFlight?.promise === promise)
|
|
@@ -179,10 +306,17 @@ function startCheck(version) {
|
|
|
179
306
|
inFlight = { version, promise };
|
|
180
307
|
}
|
|
181
308
|
/**
|
|
182
|
-
* Capability issues for the currently reported Muse version. Never awaits the probe
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
309
|
+
* Capability issues for the currently reported Muse version. Never awaits the probe.
|
|
310
|
+
*
|
|
311
|
+
* NOT-308: only evidence of a real capability loss (`missing`, and not overridden by the
|
|
312
|
+
* operator) blocks admission. An unchecked version or an inconclusive (`error`) result
|
|
313
|
+
* keeps verifying in the background while admission proceeds on the last confirmed
|
|
314
|
+
* baseline. With no confirmed baseline at all (fresh install), every unknown blocks —
|
|
315
|
+
* fail closed until the first version is confirmed.
|
|
316
|
+
*
|
|
317
|
+
* Checks are serialized — a version observed while another is being probed waits for
|
|
318
|
+
* that probe, then is checked once. `onSettled` lets the caller drop its health cache
|
|
319
|
+
* so admission sees the result as soon as it lands.
|
|
186
320
|
*/
|
|
187
321
|
export function museCapabilityIssues(version, onSettled = () => { }) {
|
|
188
322
|
settledHook = onSettled;
|
|
@@ -190,9 +324,293 @@ export function museCapabilityIssues(version, onSettled = () => { }) {
|
|
|
190
324
|
const from = s.current.from;
|
|
191
325
|
if (checkDue(s) && !inFlight)
|
|
192
326
|
startCheck(version);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
327
|
+
const checked = s.lastChecked;
|
|
328
|
+
// Unchecked (including a retry already running): a baseline means "verify in the
|
|
329
|
+
// background, keep working"; without one the unknown blocks.
|
|
330
|
+
if (!checked)
|
|
331
|
+
return s.confirmedVersion ? [] : verifyingIssue(version, from);
|
|
332
|
+
if (checked.status === "capable")
|
|
333
|
+
return [];
|
|
334
|
+
if (checked.status === "missing") {
|
|
335
|
+
return s.overriddenVersions.includes(checked.version) ? [] : issueFor(checked, from);
|
|
336
|
+
}
|
|
337
|
+
// Inconclusive: never a block while any baseline exists to work on. With none, the
|
|
338
|
+
// block stands but the check keeps retrying at the maximum backoff (checkDue), so the
|
|
339
|
+
// message always says "retries" — never "no further automatic checks".
|
|
340
|
+
return s.confirmedVersion ? [] : issueFor(checked, from, false);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Dedupe key for the one human action per Muse version *and verdict kind* (NOT-308).
|
|
344
|
+
* Stored as the action's request_id so same-issue races collapse on the existing partial
|
|
345
|
+
* unique index, and so cross-issue lookups (`listHumanActionsByRequestId`) find it.
|
|
346
|
+
*
|
|
347
|
+
* The kind is part of the key because one version can produce both verdicts in sequence:
|
|
348
|
+
* an exhausted `error` escalates as "unverified", and a later safety-net probe of the
|
|
349
|
+
* same version can still return `missing`. Sharing one key would let the still-open
|
|
350
|
+
* "unverified" action swallow the "missing" escalation (wrong text, and dismissing it
|
|
351
|
+
* would record an override for a verdict the operator never saw). The unkinded form is
|
|
352
|
+
* only for reading actions written before the key carried a kind.
|
|
353
|
+
*/
|
|
354
|
+
export function museCapabilityRequestId(version, kind) {
|
|
355
|
+
return kind ? `muse-capability:${kind}:${version}` : `muse-capability:${version}`;
|
|
356
|
+
}
|
|
357
|
+
/** Which verdict kind a prior escalation action belongs to. Legacy rows (unkinded
|
|
358
|
+
* request_id) are classified by their evidence; a row with no parseable evidence is
|
|
359
|
+
* conservatively treated as the kind being raised (dedupe, never spam). */
|
|
360
|
+
function actionKindOf(action, fallback) {
|
|
361
|
+
try {
|
|
362
|
+
const evidence = action.evidenceJson
|
|
363
|
+
? JSON.parse(action.evidenceJson)
|
|
364
|
+
: null;
|
|
365
|
+
if (evidence?.kind === "missing" || evidence?.kind === "unverified")
|
|
366
|
+
return evidence.kind;
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
// Fall through to the request_id prefix below.
|
|
370
|
+
}
|
|
371
|
+
const prefixed = /^muse-capability:(missing|unverified):/.exec(action.requestId ?? "");
|
|
372
|
+
if (prefixed)
|
|
373
|
+
return prefixed[1];
|
|
374
|
+
return fallback;
|
|
375
|
+
}
|
|
376
|
+
/** The `choice` a resolved escalation action was resolved with, if parseable. The
|
|
377
|
+
* dedicated `muse_capability` resolve path always records `{choice: "acknowledge"}`;
|
|
378
|
+
* abort/close lifecycle resolutions record a `reason` instead and never a choice. */
|
|
379
|
+
function resolutionChoiceOf(action) {
|
|
380
|
+
try {
|
|
381
|
+
const resolution = action.resolutionJson ? JSON.parse(action.resolutionJson) : null;
|
|
382
|
+
return resolution && typeof resolution === "object" && "choice" in resolution &&
|
|
383
|
+
typeof resolution.choice === "string"
|
|
384
|
+
? resolution.choice
|
|
385
|
+
: null;
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function escalationFor(checked, s) {
|
|
392
|
+
const from = s.current?.version === checked.version ? s.current.from : s.confirmedVersion;
|
|
393
|
+
const baseline = s.confirmedVersion ?? "none";
|
|
394
|
+
if (checked.status === "missing" && !s.overriddenVersions.includes(checked.version)) {
|
|
395
|
+
const rollback = from
|
|
396
|
+
? `, or pin/roll back Muse to ${from} outside Dealer (the next version change is checked automatically)?`
|
|
397
|
+
: `. Pin/roll back Muse outside Dealer to a working build if needed (the next version change is checked automatically)?`;
|
|
398
|
+
return {
|
|
399
|
+
kind: "missing",
|
|
400
|
+
reason: `${transition(from, checked.version)}: developer sessions no longer get ${CAPABILITY} ` +
|
|
401
|
+
`(${checked.detail}). Last probe ran ${formatDuration(checked.durationMs)} ` +
|
|
402
|
+
`(probe timeout ${formatDuration(PROBE_TIMEOUT_MS)}). ` +
|
|
403
|
+
`Developer admission is blocked for ${checked.version}; last confirmed baseline is ${baseline}.`,
|
|
404
|
+
question: `Muse Code ${checked.version} lost developer ${CAPABILITY}. Acknowledge to admit developers ` +
|
|
405
|
+
`on ${checked.version} anyway${rollback}`,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
if (isExhausted(checked) && checked.status === "error") {
|
|
409
|
+
const detail = checked.detail;
|
|
410
|
+
// With a baseline, admission proceeds on it and probing stops; with none (fresh
|
|
411
|
+
// install), admission is blocked and the checks continue at the maximum backoff —
|
|
412
|
+
// the text must say which, never "continues on baseline none".
|
|
413
|
+
const standing = s.confirmedVersion
|
|
414
|
+
? `Developer admission continues on the last confirmed baseline ${baseline}; ` +
|
|
415
|
+
`no further automatic checks will run for ${checked.version}.`
|
|
416
|
+
: `Developer admission is blocked (no confirmed baseline yet); ` +
|
|
417
|
+
`automatic checks continue at the maximum backoff until a version is confirmed.`;
|
|
418
|
+
return {
|
|
419
|
+
kind: "unverified",
|
|
420
|
+
reason: `Could not verify Muse Code developer ${CAPABILITY} for ${checked.version} after ` +
|
|
421
|
+
`${checked.attempts} inconclusive checks (last: ${detail}; last probe ran ` +
|
|
422
|
+
`${formatDuration(checked.durationMs)}, probe timeout ${formatDuration(PROBE_TIMEOUT_MS)}). ` +
|
|
423
|
+
standing,
|
|
424
|
+
question: `Muse Code ${checked.version} could not be verified. Acknowledge to dismiss, ` +
|
|
425
|
+
`or investigate the probe failures` +
|
|
426
|
+
(s.confirmedVersion
|
|
427
|
+
? ` (roll back to ${baseline} outside Dealer if the new build is suspect)?`
|
|
428
|
+
: `?`),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* NOT-308: raise the human action for a version the gate cannot clear by itself —
|
|
435
|
+
* exactly one per version and verdict kind, on whichever issue first observes the
|
|
436
|
+
* verdict. Later polls (and other issues' admissions) find the open action and skip
|
|
437
|
+
* creation; a resolved one of the same kind whose resolution chose `acknowledge`
|
|
438
|
+
* means the operator already decided, so the override is recorded (if missing) and
|
|
439
|
+
* nothing is re-raised. An action of the *other*
|
|
440
|
+
* kind neither suppresses this escalation nor counts as a decision on it: dismissing
|
|
441
|
+
* "could not verify" must never read as overriding a later confirmed "missing".
|
|
442
|
+
* Returns the open action, or null when nothing was (re)raised. Never blocks admission
|
|
443
|
+
* itself — the gate reads file state, not this.
|
|
444
|
+
*
|
|
445
|
+
* Repair round 3: only a resolved action whose resolution chose `acknowledge` (the
|
|
446
|
+
* dedicated resolve path's choice) counts as the operator's decision on the verdict.
|
|
447
|
+
* Abort/close resolve every open action on the issue with `{reason:
|
|
448
|
+
* "aborted_by_user" | "closed_by_operator"}` — issue lifecycle, not a capability
|
|
449
|
+
* decision — so such a resolution re-raises a fresh action instead of recording an
|
|
450
|
+
* override that would silently lift the block.
|
|
451
|
+
*/
|
|
452
|
+
export function ensureMuseCapabilityEscalation(issueId) {
|
|
453
|
+
const s = loadState();
|
|
454
|
+
const checked = s.lastChecked;
|
|
455
|
+
if (!s.current || !checked || checked.version !== s.current.version)
|
|
456
|
+
return null;
|
|
457
|
+
const escalation = escalationFor(checked, s);
|
|
458
|
+
if (!escalation || checked.status === "capable")
|
|
459
|
+
return null;
|
|
460
|
+
const detail = checked.detail;
|
|
461
|
+
const requestId = museCapabilityRequestId(checked.version, escalation.kind);
|
|
462
|
+
// Same-kind rows under both the current key and the pre-kind legacy key.
|
|
463
|
+
const seen = new Map();
|
|
464
|
+
for (const key of [requestId, museCapabilityRequestId(checked.version)]) {
|
|
465
|
+
for (const action of listHumanActionsByRequestId("muse_capability", key)) {
|
|
466
|
+
if (!seen.has(action.id))
|
|
467
|
+
seen.set(action.id, action);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const sameKind = [...seen.values()].filter((a) => actionKindOf(a, escalation.kind) === escalation.kind);
|
|
471
|
+
const open = sameKind.find((a) => a.status === "open");
|
|
472
|
+
if (open)
|
|
473
|
+
return open;
|
|
474
|
+
// Only an explicit `acknowledge` resolution is the operator's decision on this
|
|
475
|
+
// verdict — anything else (abort/close lifecycle reasons, legacy rows with no
|
|
476
|
+
// parseable choice) falls through and re-raises a fresh action below.
|
|
477
|
+
const acknowledged = sameKind.some((a) => a.status === "resolved" && resolutionChoiceOf(a) === "acknowledge");
|
|
478
|
+
if (acknowledged) {
|
|
479
|
+
if (escalation.kind === "missing")
|
|
480
|
+
recordMuseCapabilityOverride(checked.version);
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
return createHumanAction({
|
|
484
|
+
issueId,
|
|
485
|
+
actionType: "muse_capability",
|
|
486
|
+
reason: escalation.reason,
|
|
487
|
+
question: escalation.question,
|
|
488
|
+
evidence: {
|
|
489
|
+
kind: escalation.kind,
|
|
490
|
+
version: checked.version,
|
|
491
|
+
from: s.current.from,
|
|
492
|
+
detail,
|
|
493
|
+
durationMs: checked.durationMs,
|
|
494
|
+
capability: CAPABILITY,
|
|
495
|
+
},
|
|
496
|
+
responseOptions: [{ choice: "acknowledge", label: `Acknowledge — ${escalation.kind === "missing" ? `admit on ${checked.version}` : "keep working on baseline"}` }],
|
|
497
|
+
requestId,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
/** Escalate a safety-net-watched version whose probe just settled badly (best-effort). */
|
|
501
|
+
function escalateWatchedVersion(version) {
|
|
502
|
+
const owed = missWatchers.filter((w) => w.version === version);
|
|
503
|
+
missWatchers = missWatchers.filter((w) => w.version !== version);
|
|
504
|
+
for (const w of owed) {
|
|
505
|
+
try {
|
|
506
|
+
ensureMuseCapabilityEscalation(w.issueId);
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
console.warn(`[muse-capability] could not escalate ${version} on ${w.issueId}: ${String(err)}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Record the operator's acknowledge/override for a `missing` version (called by the
|
|
515
|
+
* `muse_capability` resolve path). The gate stops blocking that version; later versions
|
|
516
|
+
* are unaffected.
|
|
517
|
+
*/
|
|
518
|
+
export function recordMuseCapabilityOverride(version) {
|
|
519
|
+
const s = loadState();
|
|
520
|
+
if (s.overriddenVersions.includes(version))
|
|
521
|
+
return;
|
|
522
|
+
saveState({ ...s, overriddenVersions: [...s.overriddenVersions, version].slice(-50) });
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Shell-ish tool names in Muse session logs. `bash` is the observed shell tool
|
|
526
|
+
* (fixtures + recorded runs); anything containing `shell` is counted defensively —
|
|
527
|
+
* no known non-shell Muse tool contains it. Unknown future shell names miss the count
|
|
528
|
+
* and cause one extra verification probe, never a missed regression.
|
|
529
|
+
*/
|
|
530
|
+
const MUSE_SHELL_TOOL_RE = /^(bash|shell)$|^bash[\s_-]|shell/i;
|
|
531
|
+
/**
|
|
532
|
+
* Count shell tool calls in a Muse session log. Counts both the normalized
|
|
533
|
+
* `{type:"tool_call", name}` events the spawn writes to logPath and raw
|
|
534
|
+
* `tool:<name>` envelope operations, whichever the file holds. Returns null when
|
|
535
|
+
* the log is unreadable or holds no parseable lines — "unknown", never zero, so an
|
|
536
|
+
* unreadable log alone never fires the safety net.
|
|
537
|
+
*/
|
|
538
|
+
export function countMuseShellToolCalls(logPath) {
|
|
539
|
+
let raw;
|
|
540
|
+
try {
|
|
541
|
+
raw = fs.readFileSync(logPath, "utf8");
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
let parsed = 0;
|
|
547
|
+
let shells = 0;
|
|
548
|
+
for (const line of raw.split("\n")) {
|
|
549
|
+
const t = line.trim();
|
|
550
|
+
if (!t)
|
|
551
|
+
continue;
|
|
552
|
+
let event;
|
|
553
|
+
try {
|
|
554
|
+
event = JSON.parse(t);
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (!event || typeof event !== "object" || Array.isArray(event))
|
|
560
|
+
continue;
|
|
561
|
+
parsed += 1;
|
|
562
|
+
const rec = event;
|
|
563
|
+
if (rec.type === "tool_call" && typeof rec.name === "string" && MUSE_SHELL_TOOL_RE.test(rec.name)) {
|
|
564
|
+
shells += 1;
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
const operation = rec.payload?.event;
|
|
568
|
+
const op = operation?.operation;
|
|
569
|
+
if (typeof op === "string" && op.startsWith("tool:") && MUSE_SHELL_TOOL_RE.test(op.slice("tool:".length))) {
|
|
570
|
+
shells += 1;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return parsed === 0 ? null : shells;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* NOT-308 safety net: a developer session ending with a dirty worktree but zero shell
|
|
577
|
+
* tool calls is exactly what a shell-less build looks like from the outside, so when
|
|
578
|
+
* the running Muse version is not yet the confirmed baseline, verify it immediately —
|
|
579
|
+
* bypassing the error backoff — and escalate a `missing` verdict on this issue.
|
|
580
|
+
* Fire-and-forget (the probe settles in the background and the watcher escalates);
|
|
581
|
+
* never throws and never changes the session outcome.
|
|
582
|
+
*/
|
|
583
|
+
export function museCapabilitySafetyNetAfterSession(input) {
|
|
584
|
+
try {
|
|
585
|
+
if (input.runtime !== "muse_code" || !input.dirty)
|
|
586
|
+
return "skipped";
|
|
587
|
+
const shells = countMuseShellToolCalls(input.logPath);
|
|
588
|
+
if (shells === null || shells > 0)
|
|
589
|
+
return "skipped";
|
|
590
|
+
const version = reportedMuseVersion();
|
|
591
|
+
if (!version)
|
|
592
|
+
return "skipped";
|
|
593
|
+
const s = observe(version);
|
|
594
|
+
if (s.confirmedVersion === version)
|
|
595
|
+
return "skipped";
|
|
596
|
+
const checked = s.lastChecked;
|
|
597
|
+
if (checked && checked.version === version && checked.status === "missing") {
|
|
598
|
+
ensureMuseCapabilityEscalation(input.issueId);
|
|
599
|
+
return "escalated";
|
|
600
|
+
}
|
|
601
|
+
addMissWatcher(version, input.issueId);
|
|
602
|
+
if (inFlight)
|
|
603
|
+
return "watching";
|
|
604
|
+
// The suspicion (dirty + shell-less) outranks the error backoff timer: probe now
|
|
605
|
+
// instead of waiting it out. Exhaustion does not stop this — the session is fresh
|
|
606
|
+
// evidence, and a `missing` verdict escalates while an `error` just stays exhausted.
|
|
607
|
+
startCheck(version);
|
|
608
|
+
return "probed";
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
console.warn(`[muse-capability] safety net skipped: ${String(err)}`);
|
|
612
|
+
return "skipped";
|
|
613
|
+
}
|
|
196
614
|
}
|
|
197
615
|
/** True while a capability check is running (callers use a short health-cache TTL meanwhile). */
|
|
198
616
|
export function museCapabilityCheckInFlight() {
|
|
@@ -220,6 +638,7 @@ export function resetMuseCapabilityStateForTests() {
|
|
|
220
638
|
inFlight = null;
|
|
221
639
|
state = null;
|
|
222
640
|
settledHook = () => { };
|
|
641
|
+
missWatchers = [];
|
|
223
642
|
fs.rmSync(statePath(), { force: true });
|
|
224
643
|
}
|
|
225
644
|
/** Tests: backdate the last check so the error-retry path can be exercised without waiting. */
|