@moda-ai/cli 1.55.44 → 1.55.46
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 +7 -8
- package/dist/{agent-5ax7fqhe.js → agent-aewgndk0.js} +16 -16
- package/dist/{cli-tztzrh6b.js → cli-4kf462jq.js} +3 -3
- package/dist/{cli-xcpwp0gb.js → cli-d6gfxx8t.js} +1 -1
- package/dist/{cli-t4xfvrry.js → cli-dxbd1xxs.js} +5 -5
- package/dist/{cli-wwbr5caw.js → cli-fqc02t6h.js} +12 -91
- package/dist/{cli-sejqvvhv.js → cli-h1z9yyqk.js} +1 -1
- package/dist/{cli-dmm9jm9m.js → cli-j08a1mam.js} +1 -1
- package/dist/{cli-8pcq2et6.js → cli-m4474bnj.js} +1 -1
- package/dist/{cli-xx0pjzkh.js → cli-s7d069g0.js} +5 -19
- package/dist/{cli-k2ka2kjb.js → cli-sqnw96gj.js} +7 -2
- package/dist/{cli-d4cdczhe.js → cli-x7rc3twr.js} +42 -42
- package/dist/{cli-5aa48ry5.js → cli-xctqdn42.js} +3 -3
- package/dist/cli.js +227 -630
- package/dist/{doctor-k4tdgvgv.js → doctor-fz5kq44d.js} +8 -8
- package/dist/{harness-avynbq4f.js → harness-cy0gpyhj.js} +10 -10
- package/dist/{harness-github-actions-gd6vyxb0.js → harness-github-actions-aac3tn96.js} +9 -9
- package/dist/{index-63k2a6gb.js → index-5eyq3ykt.js} +26 -26
- package/dist/{production-intelligence-sv3qx7f4.js → production-intelligence-8j8008d2.js} +6 -4
- package/dist/{profile-commands-mh3kfqkn.js → profile-commands-zbyn9xg2.js} +5 -5
- package/dist/{prompt-attribution-wvvnzw4m.js → prompt-attribution-g9gtv191.js} +9 -9
- package/dist/prompt-source-mcp.js +9 -9
- package/dist/{prompts-snnbyxmz.js → prompts-b6awryvv.js} +9 -13
- package/dist/{provision-t1dpzcjp.js → provision-xkhwz86g.js} +6 -6
- package/dist/{registry-8zkxefjs.js → registry-d7xdm4dw.js} +4 -4
- package/dist/{skills-d4215fa9.js → skills-zfenne8w.js} +6 -6
- package/dist/{sync-17f9yxb1.js → sync-5vd8gwte.js} +12 -12
- package/package.json +1 -1
- package/skills/integration/index.json +2 -2
- package/skills/moda-cli/SKILL.md +7 -10
- package/skills/moda-cli/references/commands.md +1 -2
- package/skills/moda-cli/references/dashboard-parity.md +2 -3
- package/skills/moda-cli/references/output-protocol.md +1 -1
- package/skills/moda-cli/references/production-intelligence.md +0 -4
- package/skills/moda-cli/references/prompts.md +1 -27
- package/skills/moda-cli/references/{replays-and-registry.md → registry.md} +15 -59
- package/dist/cli-4pc0a18x.js +0 -12
- package/dist/cli-8pksf2x7.js +0 -754
- package/dist/fix-nk5xffz8.js +0 -1440
- package/dist/prompts-ab-y7egdzg4.js +0 -64
- package/dist/prompts-propose-5nw25v9a.js +0 -256
- package/dist/{cli-e24fjyrb.js → cli-da3h2p6w.js} +28 -28
package/dist/fix-nk5xffz8.js
DELETED
|
@@ -1,1440 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_POLL_INTERVAL_MS,
|
|
3
|
-
DEFAULT_WAIT_TIMEOUT_MS,
|
|
4
|
-
fetchLatestRun
|
|
5
|
-
} from "./cli-4pc0a18x.js";
|
|
6
|
-
import {
|
|
7
|
-
assertFlags
|
|
8
|
-
} from "./cli-tztzrh6b.js";
|
|
9
|
-
import {
|
|
10
|
-
callControlAPI
|
|
11
|
-
} from "./cli-dmm9jm9m.js";
|
|
12
|
-
import {
|
|
13
|
-
EXIT_DEGRADED,
|
|
14
|
-
EXIT_ERROR,
|
|
15
|
-
EXIT_OK,
|
|
16
|
-
getModaPaths,
|
|
17
|
-
resolveApiTenantId,
|
|
18
|
-
shellArg,
|
|
19
|
-
validateConfig
|
|
20
|
-
} from "./cli-e24fjyrb.js";
|
|
21
|
-
import {
|
|
22
|
-
CliInputError
|
|
23
|
-
} from "./cli-k4tqwah4.js";
|
|
24
|
-
import"./cli-ns5fcnkz.js";
|
|
25
|
-
import"./cli-0v6na3yp.js";
|
|
26
|
-
|
|
27
|
-
// src/fix.ts
|
|
28
|
-
import {
|
|
29
|
-
closeSync,
|
|
30
|
-
constants as fsConstants,
|
|
31
|
-
existsSync,
|
|
32
|
-
fstatSync,
|
|
33
|
-
ftruncateSync,
|
|
34
|
-
lstatSync,
|
|
35
|
-
mkdirSync,
|
|
36
|
-
openSync,
|
|
37
|
-
readFileSync,
|
|
38
|
-
realpathSync,
|
|
39
|
-
renameSync,
|
|
40
|
-
writeFileSync,
|
|
41
|
-
writeSync
|
|
42
|
-
} from "node:fs";
|
|
43
|
-
import { createHash } from "node:crypto";
|
|
44
|
-
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
45
|
-
var O_NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
|
|
46
|
-
var ADVANCE_TIMEOUT_MS = 300000;
|
|
47
|
-
var STEP_DELAY_MS = 2000;
|
|
48
|
-
var MAX_WAIT_TIMEOUT_MS = 24 * 60 * 60 * 1000;
|
|
49
|
-
var MAX_POLL_INTERVAL_MS = 120000;
|
|
50
|
-
var PER_CASE_FINDINGS_CAP = 50;
|
|
51
|
-
var DRIVE_PASS_INTERVAL_MS = 20000;
|
|
52
|
-
var DRIVE_LIST_LIMIT = 100;
|
|
53
|
-
var DRIVE_LIST_MAX_PAGES = 10;
|
|
54
|
-
var DRAFT_BATCH_LIMIT_MAX = 25;
|
|
55
|
-
var DRAFT_BATCH_LIMIT_DEFAULT = 5;
|
|
56
|
-
var DRIVE_MAX_FIXES_DEFAULT = 3;
|
|
57
|
-
var DRIVE_MAX_FIXES_MAX = 10;
|
|
58
|
-
var VERIFY_COOLDOWN_MS = 10 * 60 * 1000;
|
|
59
|
-
var REGATE_COOLDOWN_STATUSES = new Set(["VERIFIED", "GATE_FAILED", "GATE_INCONCLUSIVE"]);
|
|
60
|
-
var DRAFT_BATCH_TIMEOUT_MS = 300000;
|
|
61
|
-
var ADVANCING_STATUSES = new Set(["DRAFT", "SCOPING", "PROPOSING", "PROPOSED", "GATING"]);
|
|
62
|
-
var FIX_USAGE = [
|
|
63
|
-
"Usage: moda fixes [--status=S] [--limit=N] [--cursor=TOKEN]",
|
|
64
|
-
" moda fixes draft-batch [--limit=N] (N is 1-25, default 5)",
|
|
65
|
-
" moda fixes drive [--status=S] [--max-fixes=N] [--timeout=ms] [--pass-interval=ms] (N is 1-10, default 3)",
|
|
66
|
-
" moda fix <fix_id> [--packet] [--wait]",
|
|
67
|
-
" moda fix start <problem_id> [--type=auto|prompt] [--wait] [--timeout=ms]",
|
|
68
|
-
" moda fix verify <fix_id> [--prompt-file=PATH] [--no-wait] [--timeout=ms] [--yes]",
|
|
69
|
-
" moda fix checkout <fix_id>",
|
|
70
|
-
" moda fix submit <fix_id> --pr | --local-ref=<branch>",
|
|
71
|
-
" moda fix mark-applied <fix_id> [--note=TEXT]",
|
|
72
|
-
" moda fix dismiss <fix_id> --reason=TEXT"
|
|
73
|
-
].join(`
|
|
74
|
-
`);
|
|
75
|
-
var FIXES_SUBCOMMAND_FLAGS = {
|
|
76
|
-
list: ["cursor", "limit", "status"],
|
|
77
|
-
"draft-batch": ["limit"],
|
|
78
|
-
drive: ["status", "timeout", "pass-interval", "max-fixes"]
|
|
79
|
-
};
|
|
80
|
-
var FIX_SUBCOMMAND_FLAGS = {
|
|
81
|
-
detail: ["packet", "timeout", "wait", "poll-interval"],
|
|
82
|
-
start: ["timeout", "type", "wait", "poll-interval", "problem-id"],
|
|
83
|
-
verify: ["timeout", "wait", "fix-id", "no-wait", "poll-interval", "prompt-file"],
|
|
84
|
-
checkout: ["fix-id"],
|
|
85
|
-
submit: ["pr", "fix-id", "local-ref"],
|
|
86
|
-
"mark-applied": ["note", "fix-id"],
|
|
87
|
-
dismiss: ["reason", "fix-id"]
|
|
88
|
-
};
|
|
89
|
-
function assertWaitFlags(flags) {
|
|
90
|
-
const maxByFlag = {
|
|
91
|
-
timeout: MAX_WAIT_TIMEOUT_MS,
|
|
92
|
-
"poll-interval": MAX_POLL_INTERVAL_MS,
|
|
93
|
-
"pass-interval": MAX_POLL_INTERVAL_MS
|
|
94
|
-
};
|
|
95
|
-
for (const [name, max] of Object.entries(maxByFlag)) {
|
|
96
|
-
const raw = flags[name];
|
|
97
|
-
if (raw === undefined)
|
|
98
|
-
continue;
|
|
99
|
-
const value = Number(raw);
|
|
100
|
-
if (!/^\d+$/.test(raw.trim()) || !Number.isSafeInteger(value) || value < 1 || value > max) {
|
|
101
|
-
throw new CliInputError(`--${name}=${raw} must be a whole number of milliseconds between 1 and ${max}, so nothing was sent.`, FIX_USAGE);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
async function runFixesCommand(context) {
|
|
106
|
-
const fixesSub = context.positional && FIXES_SUBCOMMAND_FLAGS[context.positional] ? context.positional : "list";
|
|
107
|
-
assertFlags(fixesSub === "list" ? "fixes" : `fixes ${fixesSub}`, context.flags, [...FIXES_SUBCOMMAND_FLAGS[fixesSub], "tenant-id"]);
|
|
108
|
-
assertWaitFlags(context.flags);
|
|
109
|
-
validateConfig();
|
|
110
|
-
const profileOptions = profileOptionsFor(context);
|
|
111
|
-
const tenantId = requireTenantId(context.flags, profileOptions);
|
|
112
|
-
switch (context.positional) {
|
|
113
|
-
case "draft-batch":
|
|
114
|
-
return runFixesDraftBatch(context, profileOptions, tenantId);
|
|
115
|
-
case "drive":
|
|
116
|
-
return runFixesDrive(context, profileOptions, tenantId);
|
|
117
|
-
case undefined:
|
|
118
|
-
break;
|
|
119
|
-
default:
|
|
120
|
-
throw new CliInputError(`Unknown fixes subcommand '${context.positional}'`, FIX_USAGE);
|
|
121
|
-
}
|
|
122
|
-
const query = new URLSearchParams;
|
|
123
|
-
const status = (context.flags.status ?? "").trim().toUpperCase();
|
|
124
|
-
if (context.flags.status !== undefined && !status) {
|
|
125
|
-
throw new CliInputError("--status must name a fix status.", FIX_USAGE);
|
|
126
|
-
}
|
|
127
|
-
if (status)
|
|
128
|
-
query.set("status", status);
|
|
129
|
-
if (context.flags.limit) {
|
|
130
|
-
const limit = Number(context.flags.limit);
|
|
131
|
-
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
132
|
-
throw new CliInputError(`--limit must be an integer between 1 and 100 (got ${context.flags.limit})`, FIX_USAGE);
|
|
133
|
-
}
|
|
134
|
-
query.set("limit", String(limit));
|
|
135
|
-
}
|
|
136
|
-
if (context.flags.cursor)
|
|
137
|
-
query.set("cursor", context.flags.cursor);
|
|
138
|
-
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
139
|
-
const data = await callControlAPI(`${fixesBase(tenantId)}${suffix}`, { method: "GET" }, profileOptions);
|
|
140
|
-
const fixes = data.fixes ?? [];
|
|
141
|
-
if (context.outputMode === "human") {
|
|
142
|
-
if (fixes.length === 0) {
|
|
143
|
-
process.stderr.write("No fixes in the queue. Draft one with `moda fix start <problem_id> --wait`.\n");
|
|
144
|
-
} else {
|
|
145
|
-
for (const fix of fixes) {
|
|
146
|
-
process.stderr.write(`MODA-FIX-${fix.shortRef} ${fix.status} ${fix.fixType} ${fix.id} ${fix.statusReason ?? ""}
|
|
147
|
-
`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
context.output.writeData(data, {
|
|
152
|
-
summary: {
|
|
153
|
-
text: `${fixes.length} fix(es)${context.flags.status ? ` with status ${context.flags.status}` : ""} in the ranked queue.`,
|
|
154
|
-
confidence: "high"
|
|
155
|
-
},
|
|
156
|
-
nextCommands: fixes.slice(0, 1).map((fix) => ({
|
|
157
|
-
command: `moda fix ${shellArg(fix.id)}`,
|
|
158
|
-
purpose: "Open the top-ranked fix (status, gate result, next actions).",
|
|
159
|
-
mutability: "read",
|
|
160
|
-
requires_approval: false
|
|
161
|
-
}))
|
|
162
|
-
});
|
|
163
|
-
return EXIT_OK;
|
|
164
|
-
}
|
|
165
|
-
async function runFixesDraftBatch(context, profileOptions, tenantId) {
|
|
166
|
-
const usage = "Usage: moda fixes draft-batch [--limit=N] (N is 1-25, default 5)";
|
|
167
|
-
let limit = DRAFT_BATCH_LIMIT_DEFAULT;
|
|
168
|
-
if (context.flags.limit) {
|
|
169
|
-
const parsed = Number(context.flags.limit);
|
|
170
|
-
if (!Number.isInteger(parsed) || parsed < 1 || parsed > DRAFT_BATCH_LIMIT_MAX) {
|
|
171
|
-
throw new CliInputError(`--limit must be an integer between 1 and ${DRAFT_BATCH_LIMIT_MAX} (got ${context.flags.limit}) — batches cap at ${DRAFT_BATCH_LIMIT_MAX} (LLM spend guard)`, usage);
|
|
172
|
-
}
|
|
173
|
-
limit = parsed;
|
|
174
|
-
}
|
|
175
|
-
const result = await callControlAPI(`${fixesBase(tenantId)}/draft-batch`, { method: "POST", body: JSON.stringify({ limit }) }, profileOptions, { timeoutMs: DRAFT_BATCH_TIMEOUT_MS, retries: 0 });
|
|
176
|
-
const drafted = result.drafted ?? [];
|
|
177
|
-
const skipped = result.skipped ?? [];
|
|
178
|
-
if (context.outputMode === "human") {
|
|
179
|
-
const w = (line) => process.stderr.write(`${line}
|
|
180
|
-
`);
|
|
181
|
-
w(`Drafted ${drafted.length} fix(es) (batch cap ${DRAFT_BATCH_LIMIT_MAX} — LLM spend guard):`);
|
|
182
|
-
for (const fix of drafted) {
|
|
183
|
-
w(` MODA-FIX-${fix.shortRef} ${fix.status} problem ${fix.problemId}`);
|
|
184
|
-
}
|
|
185
|
-
if (skipped.length > 0) {
|
|
186
|
-
w(`Skipped ${skipped.length} problem(s):`);
|
|
187
|
-
for (const skip of skipped) {
|
|
188
|
-
w(` ${skip.problemId}: ${skip.reason}`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
if (drafted.length > 0) {
|
|
192
|
-
w("Next: moda fixes drive");
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
context.output.writeData({
|
|
196
|
-
drafted: drafted.map((fix) => ({
|
|
197
|
-
fixId: fix.id,
|
|
198
|
-
shortRef: fix.shortRef,
|
|
199
|
-
problemId: fix.problemId,
|
|
200
|
-
status: fix.status,
|
|
201
|
-
problemRank: fix.problemRank ?? null
|
|
202
|
-
})),
|
|
203
|
-
skipped
|
|
204
|
-
}, {
|
|
205
|
-
summary: {
|
|
206
|
-
text: `Drafted ${drafted.length} fix(es) for the top-ranked problems; skipped ${skipped.length} (reasons in data.skipped). Batches cap at ${DRAFT_BATCH_LIMIT_MAX} — an LLM spend guard; run another batch after this one lands.`,
|
|
207
|
-
confidence: "high"
|
|
208
|
-
},
|
|
209
|
-
nextCommands: drafted.length > 0 ? [
|
|
210
|
-
{
|
|
211
|
-
command: "moda fixes drive",
|
|
212
|
-
purpose: "Round-robin advance every drafted fix (scope → propose → gate) until all rest.",
|
|
213
|
-
mutability: "write",
|
|
214
|
-
requires_approval: false
|
|
215
|
-
}
|
|
216
|
-
] : []
|
|
217
|
-
});
|
|
218
|
-
return EXIT_OK;
|
|
219
|
-
}
|
|
220
|
-
function driveStatePath(tenantId, profileOptions) {
|
|
221
|
-
const safeTenant = tenantId.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
222
|
-
return join(getModaPaths(profileOptions).dir, `fixes-drive-last.${safeTenant}.json`);
|
|
223
|
-
}
|
|
224
|
-
function lastDrivenTimes(tenantId, profileOptions) {
|
|
225
|
-
try {
|
|
226
|
-
const raw = JSON.parse(readFileSync(driveStatePath(tenantId, profileOptions), "utf8"));
|
|
227
|
-
if (Array.isArray(raw)) {
|
|
228
|
-
return new Map(raw.filter((id) => typeof id === "string").map((id) => [id, Date.now()]));
|
|
229
|
-
}
|
|
230
|
-
const out = new Map;
|
|
231
|
-
for (const [id, at] of Object.entries(raw ?? {})) {
|
|
232
|
-
if (typeof at === "number" && Number.isFinite(at))
|
|
233
|
-
out.set(id, at);
|
|
234
|
-
}
|
|
235
|
-
return out;
|
|
236
|
-
} catch {
|
|
237
|
-
return new Map;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
function recordDrivenFixIds(tenantId, ids, previous, profileOptions) {
|
|
241
|
-
try {
|
|
242
|
-
const now = Date.now();
|
|
243
|
-
const merged = new Map(previous);
|
|
244
|
-
for (const id of ids)
|
|
245
|
-
merged.set(id, now);
|
|
246
|
-
const entries = [...merged.entries()].sort((left, right) => right[1] - left[1]).slice(0, 500);
|
|
247
|
-
const path = driveStatePath(tenantId, profileOptions);
|
|
248
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
249
|
-
const tmp = `${path}.${process.pid}.${now}.tmp`;
|
|
250
|
-
writeFileSync(tmp, `${JSON.stringify(Object.fromEntries(entries))}
|
|
251
|
-
`, { mode: 384 });
|
|
252
|
-
renameSync(tmp, path);
|
|
253
|
-
} catch {}
|
|
254
|
-
}
|
|
255
|
-
async function runFixesDrive(context, profileOptions, tenantId) {
|
|
256
|
-
const timeoutMs = parsePositiveInt(context.flags.timeout, DEFAULT_WAIT_TIMEOUT_MS, MAX_WAIT_TIMEOUT_MS);
|
|
257
|
-
const passIntervalMs = parsePositiveInt(context.flags["pass-interval"], DRIVE_PASS_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
|
|
258
|
-
const deadline = Date.now() + timeoutMs;
|
|
259
|
-
let maxFixes = DRIVE_MAX_FIXES_DEFAULT;
|
|
260
|
-
if (context.flags["max-fixes"] !== undefined) {
|
|
261
|
-
const parsed = Number(context.flags["max-fixes"]);
|
|
262
|
-
if (!Number.isInteger(parsed) || parsed < 1 || parsed > DRIVE_MAX_FIXES_MAX) {
|
|
263
|
-
throw new CliInputError(`--max-fixes must be an integer between 1 and ${DRIVE_MAX_FIXES_MAX} (got ${context.flags["max-fixes"]}) — each driven fix can run hundreds of replay playouts`, FIX_USAGE);
|
|
264
|
-
}
|
|
265
|
-
maxFixes = parsed;
|
|
266
|
-
}
|
|
267
|
-
const statusFilter = (context.flags.status ?? "").trim().toUpperCase();
|
|
268
|
-
if (statusFilter && !ADVANCING_STATUSES.has(statusFilter)) {
|
|
269
|
-
throw new CliInputError(`--status must be one of the advancing statuses (${[...ADVANCING_STATUSES].join(", ")}) — a resting fix has nothing to drive (got ${context.flags.status})`, FIX_USAGE);
|
|
270
|
-
}
|
|
271
|
-
const statusParam = statusFilter ? `&status=${encodeURIComponent(statusFilter)}` : "";
|
|
272
|
-
const snapshot = [];
|
|
273
|
-
const snapshotWarnings = [];
|
|
274
|
-
let coverageTruncated = false;
|
|
275
|
-
const seenIds = new Set;
|
|
276
|
-
let reorderDetected = false;
|
|
277
|
-
let multiPage = false;
|
|
278
|
-
let scanTruncated = false;
|
|
279
|
-
let cursor = null;
|
|
280
|
-
for (let page = 0;page < DRIVE_LIST_MAX_PAGES; page++) {
|
|
281
|
-
const data = await callControlAPI(`${fixesBase(tenantId)}?limit=${DRIVE_LIST_LIMIT}${statusParam}` + (cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""), { method: "GET" }, profileOptions);
|
|
282
|
-
for (const fix of data.fixes ?? []) {
|
|
283
|
-
if (seenIds.has(fix.id)) {
|
|
284
|
-
if (!reorderDetected) {
|
|
285
|
-
reorderDetected = true;
|
|
286
|
-
coverageTruncated = true;
|
|
287
|
-
snapshotWarnings.push("The fix queue was re-ranked while paging (a page repeated a fix), so rows may have shifted past the page boundary and been skipped — coverage is not guaranteed. Re-run `moda fixes drive` to pick up anything missed.");
|
|
288
|
-
}
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
seenIds.add(fix.id);
|
|
292
|
-
snapshot.push(fix);
|
|
293
|
-
}
|
|
294
|
-
if (data.pagination?.scan_truncated) {
|
|
295
|
-
coverageTruncated = true;
|
|
296
|
-
scanTruncated = true;
|
|
297
|
-
snapshotWarnings.push(statusFilter ? `Status ${statusFilter} alone exceeds the backend list scan window — driving the newest ${snapshot.length}; older ${statusFilter} fixes are not reachable through drive, advance them directly with \`moda fix <id> --wait\`.` : "Fix queue exceeds the backend list scan window — driving the newest fixes only. Re-running unfiltered rescans the same rows; drive each advancing status separately (`moda fixes drive --status SCOPING`, `--status PROPOSED`, …) — a status filter narrows the server-side scan window and reaches older fixes.");
|
|
298
|
-
}
|
|
299
|
-
cursor = data.pagination?.has_more ? data.pagination?.next_cursor ?? null : null;
|
|
300
|
-
if (!cursor)
|
|
301
|
-
break;
|
|
302
|
-
if (!multiPage) {
|
|
303
|
-
multiPage = true;
|
|
304
|
-
coverageTruncated = true;
|
|
305
|
-
snapshotWarnings.push("The queue needed more than one page, and the backend re-ranks it per request — rows can shift across a page boundary and be skipped without any visible duplicate, so this drive cannot prove it covered every advancing fix. Re-run `moda fixes drive` until it exits 0 (single page = provably complete).");
|
|
306
|
-
}
|
|
307
|
-
if (page === DRIVE_LIST_MAX_PAGES - 1) {
|
|
308
|
-
coverageTruncated = true;
|
|
309
|
-
scanTruncated = true;
|
|
310
|
-
snapshotWarnings.push(`Fix queue exceeds ${DRIVE_LIST_LIMIT * DRIVE_LIST_MAX_PAGES} rows — driving the first ${snapshot.length}; narrow with \`moda fixes drive --status <ADVANCING_STATUS>\` to reach the rest (a plain re-run pages the same rank order).`);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
const tracked = new Map;
|
|
314
|
-
const skipped = [];
|
|
315
|
-
const advancing = snapshot.filter((fix) => ADVANCING_STATUSES.has(fix.status) && !isRestingProposed(fix));
|
|
316
|
-
const drivenAt = lastDrivenTimes(tenantId, profileOptions);
|
|
317
|
-
const rankOf = new Map(advancing.map((fix, index) => [fix.id, index]));
|
|
318
|
-
const byLeastRecent = [...advancing].sort((left, right) => {
|
|
319
|
-
const leftAt = drivenAt.get(left.id) ?? -1;
|
|
320
|
-
const rightAt = drivenAt.get(right.id) ?? -1;
|
|
321
|
-
return leftAt !== rightAt ? leftAt - rightAt : (rankOf.get(left.id) ?? 0) - (rankOf.get(right.id) ?? 0);
|
|
322
|
-
});
|
|
323
|
-
const picked = new Set(byLeastRecent.slice(0, maxFixes).map((fix) => fix.id));
|
|
324
|
-
for (const fix of advancing) {
|
|
325
|
-
if (picked.has(fix.id))
|
|
326
|
-
tracked.set(fix.id, fix);
|
|
327
|
-
else
|
|
328
|
-
skipped.push(fix);
|
|
329
|
-
}
|
|
330
|
-
recordDrivenFixIds(tenantId, [...picked], drivenAt, profileOptions);
|
|
331
|
-
const gatingCount = [...tracked.values()].filter((fix) => fix.status === "GATING").length;
|
|
332
|
-
if (skipped.length > 0) {
|
|
333
|
-
coverageTruncated = true;
|
|
334
|
-
snapshotWarnings.push(`Drove ${tracked.size} fix(es) (--max-fixes=${maxFixes}; ${gatingCount} already gating; least-recently-driven first); skipped ${skipped.length} advancing fix(es): ${skipped.map((fix) => `MODA-FIX-${fix.shortRef}`).join(", ")}. Re-run \`moda fixes drive\` to drive the next ones.`);
|
|
335
|
-
}
|
|
336
|
-
if (tracked.size === 0) {
|
|
337
|
-
context.output.writeData({
|
|
338
|
-
fixes: [],
|
|
339
|
-
passes: 0,
|
|
340
|
-
rested: 0,
|
|
341
|
-
stragglers: [],
|
|
342
|
-
timedOut: false,
|
|
343
|
-
coverageTruncated
|
|
344
|
-
}, {
|
|
345
|
-
...coverageTruncated ? { status: "degraded" } : {},
|
|
346
|
-
summary: {
|
|
347
|
-
text: coverageTruncated ? "Nothing advancing inside the scanned window, but the queue scan was truncated — fixes outside it may still be advancing (see warnings)." : "No fixes in an advancing status — nothing to drive.",
|
|
348
|
-
confidence: coverageTruncated ? "medium" : "high"
|
|
349
|
-
},
|
|
350
|
-
...snapshotWarnings.length > 0 ? { warnings: snapshotWarnings } : {},
|
|
351
|
-
nextCommands: [
|
|
352
|
-
{
|
|
353
|
-
command: "moda fixes draft-batch",
|
|
354
|
-
purpose: "Batch-draft fixes for the top-ranked problems (cap 25 — LLM spend guard).",
|
|
355
|
-
mutability: "write",
|
|
356
|
-
requires_approval: true
|
|
357
|
-
}
|
|
358
|
-
]
|
|
359
|
-
});
|
|
360
|
-
return coverageTruncated ? EXIT_DEGRADED : EXIT_OK;
|
|
361
|
-
}
|
|
362
|
-
const isRested = (fix) => !ADVANCING_STATUSES.has(fix.status) || isRestingProposed(fix);
|
|
363
|
-
const activeIds = () => [...tracked.values()].filter((fix) => !isRested(fix)).map((fix) => fix.id);
|
|
364
|
-
const lastAnnounced = new Map;
|
|
365
|
-
const announce = (row) => {
|
|
366
|
-
if (lastAnnounced.get(row.id) === row.status)
|
|
367
|
-
return;
|
|
368
|
-
lastAnnounced.set(row.id, row.status);
|
|
369
|
-
announceFixTransition(context, row);
|
|
370
|
-
};
|
|
371
|
-
for (const fix of tracked.values())
|
|
372
|
-
announce(fix);
|
|
373
|
-
const errors = new Map;
|
|
374
|
-
let errorCount = 0;
|
|
375
|
-
let passes = 0;
|
|
376
|
-
let timedOut = false;
|
|
377
|
-
while (activeIds().length > 0) {
|
|
378
|
-
if (Date.now() >= deadline) {
|
|
379
|
-
timedOut = true;
|
|
380
|
-
break;
|
|
381
|
-
}
|
|
382
|
-
passes += 1;
|
|
383
|
-
for (const fixId of activeIds()) {
|
|
384
|
-
if (Date.now() >= deadline) {
|
|
385
|
-
timedOut = true;
|
|
386
|
-
break;
|
|
387
|
-
}
|
|
388
|
-
try {
|
|
389
|
-
const next = await advanceFix(tenantId, fixId, profileOptions);
|
|
390
|
-
tracked.set(fixId, next);
|
|
391
|
-
errors.delete(fixId);
|
|
392
|
-
announce(next);
|
|
393
|
-
} catch (err) {
|
|
394
|
-
errorCount += 1;
|
|
395
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
396
|
-
errors.set(fixId, message);
|
|
397
|
-
if (context.outputMode === "human") {
|
|
398
|
-
const known = tracked.get(fixId);
|
|
399
|
-
process.stderr.write(`fix MODA-FIX-${known?.shortRef ?? fixId}: advance failed — ${message}
|
|
400
|
-
`);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
if (timedOut || activeIds().length === 0)
|
|
405
|
-
break;
|
|
406
|
-
await sleep(passIntervalMs);
|
|
407
|
-
}
|
|
408
|
-
const landings = [...tracked.values()].map((fix) => ({
|
|
409
|
-
shortRef: fix.shortRef,
|
|
410
|
-
status: fix.status,
|
|
411
|
-
fixType: fix.fixType,
|
|
412
|
-
statusReason: fix.statusReason ?? null
|
|
413
|
-
}));
|
|
414
|
-
const stragglers = [...tracked.values()].filter((fix) => !isRested(fix));
|
|
415
|
-
const skippedRefs = skipped.map((fix) => fix.shortRef);
|
|
416
|
-
const warnings = [
|
|
417
|
-
...snapshotWarnings,
|
|
418
|
-
...[...errors.entries()].map(([fixId, message]) => {
|
|
419
|
-
const fix = tracked.get(fixId);
|
|
420
|
-
return `advance failed for MODA-FIX-${fix?.shortRef ?? fixId}: ${message}`;
|
|
421
|
-
})
|
|
422
|
-
];
|
|
423
|
-
if (errorCount > errors.size) {
|
|
424
|
-
warnings.push(`${errorCount} advance error(s) total across ${passes} pass(es) — each was retried on the next pass.`);
|
|
425
|
-
}
|
|
426
|
-
const degraded = timedOut || coverageTruncated;
|
|
427
|
-
const restedCount = tracked.size - stragglers.length;
|
|
428
|
-
const coverageNote = coverageTruncated ? " Coverage was incomplete: the queue scan was truncated, so fixes outside the scanned window were never driven (see warnings)." : "";
|
|
429
|
-
if (context.outputMode === "human") {
|
|
430
|
-
process.stderr.write(timedOut ? `
|
|
431
|
-
Timed out after ${timeoutMs}ms with ${stragglers.length} of ${tracked.size} fix(es) still advancing.${coverageNote}
|
|
432
|
-
` : `
|
|
433
|
-
${coverageTruncated ? `Rested all ${tracked.size} scanned fix(es)` : `All ${tracked.size} fix(es) rested`} after ${passes} pass(es).${coverageNote}
|
|
434
|
-
`);
|
|
435
|
-
}
|
|
436
|
-
context.output.writeData({
|
|
437
|
-
fixes: landings,
|
|
438
|
-
passes,
|
|
439
|
-
rested: restedCount,
|
|
440
|
-
stragglers: stragglers.map((fix) => fix.shortRef),
|
|
441
|
-
timedOut,
|
|
442
|
-
coverageTruncated,
|
|
443
|
-
maxFixes,
|
|
444
|
-
skipped: skippedRefs
|
|
445
|
-
}, {
|
|
446
|
-
...degraded ? { status: "degraded" } : {},
|
|
447
|
-
summary: {
|
|
448
|
-
text: timedOut ? `Drive timed out after ${timeoutMs}ms: ${stragglers.length} of ${tracked.size} fix(es) still advancing (${stragglers.map((fix) => `MODA-FIX-${fix.shortRef}`).join(", ")}). Re-run \`moda fixes drive\` to resume.${coverageNote}` : coverageTruncated ? `Rested all ${tracked.size} scanned fix(es) after ${passes} pass(es), but the queue was NOT swept.${coverageNote}` : `All ${tracked.size} fix(es) rested after ${passes} pass(es).`,
|
|
449
|
-
confidence: "high"
|
|
450
|
-
},
|
|
451
|
-
...warnings.length > 0 ? { warnings } : {},
|
|
452
|
-
nextCommands: [
|
|
453
|
-
...timedOut ? [{
|
|
454
|
-
command: "moda fixes drive",
|
|
455
|
-
purpose: "Resume driving the stragglers — advance-on-poll picks up where it left off.",
|
|
456
|
-
mutability: "write",
|
|
457
|
-
requires_approval: false
|
|
458
|
-
}] : coverageTruncated ? [
|
|
459
|
-
...skipped.length > 0 || !scanTruncated ? [{
|
|
460
|
-
command: "moda fixes drive",
|
|
461
|
-
purpose: skipped.length > 0 ? `Drive the next ${Math.min(skipped.length, maxFixes)} of the ${skipped.length} skipped fix(es) (--max-fixes spend cap).` : "The queue was re-ranked mid-page — re-run to pick up any fix that shifted past the boundary.",
|
|
462
|
-
mutability: "write",
|
|
463
|
-
requires_approval: false
|
|
464
|
-
}] : [],
|
|
465
|
-
...scanTruncated ? [{
|
|
466
|
-
command: statusFilter ? "moda fix <fix_id> --wait" : "moda fixes drive --status=SCOPING",
|
|
467
|
-
purpose: statusFilter ? `Status ${statusFilter} overflows the scan window — advance the older fixes individually.` : "Drive each advancing status separately to reach fixes outside the unfiltered scan window.",
|
|
468
|
-
mutability: "write",
|
|
469
|
-
requires_approval: false
|
|
470
|
-
}] : []
|
|
471
|
-
] : [{
|
|
472
|
-
command: "moda fixes",
|
|
473
|
-
purpose: "Review where every fix landed (VERIFIED fixes are ready to deliver).",
|
|
474
|
-
mutability: "read",
|
|
475
|
-
requires_approval: false
|
|
476
|
-
}]
|
|
477
|
-
]
|
|
478
|
-
});
|
|
479
|
-
return degraded ? EXIT_DEGRADED : EXIT_OK;
|
|
480
|
-
}
|
|
481
|
-
async function runFixCommand(context) {
|
|
482
|
-
const fixSub = context.positional && FIX_SUBCOMMAND_FLAGS[context.positional] ? context.positional : "detail";
|
|
483
|
-
assertFlags(fixSub === "detail" ? "fix" : `fix ${fixSub}`, context.flags, [...FIX_SUBCOMMAND_FLAGS[fixSub], "tenant-id"]);
|
|
484
|
-
assertWaitFlags(context.flags);
|
|
485
|
-
validateConfig();
|
|
486
|
-
const profileOptions = profileOptionsFor(context);
|
|
487
|
-
const tenantId = requireTenantId(context.flags, profileOptions);
|
|
488
|
-
const sub = context.positional;
|
|
489
|
-
if (!sub) {
|
|
490
|
-
throw new CliInputError("A fix id or subcommand is required", FIX_USAGE);
|
|
491
|
-
}
|
|
492
|
-
switch (sub) {
|
|
493
|
-
case "start":
|
|
494
|
-
return runFixStart(context, profileOptions, tenantId);
|
|
495
|
-
case "verify":
|
|
496
|
-
return runFixVerify(context, profileOptions, tenantId);
|
|
497
|
-
case "checkout":
|
|
498
|
-
return runFixCheckout(context, profileOptions, tenantId);
|
|
499
|
-
case "submit":
|
|
500
|
-
return runFixSubmit(context, profileOptions, tenantId);
|
|
501
|
-
case "mark-applied":
|
|
502
|
-
return runFixMarkApplied(context, profileOptions, tenantId);
|
|
503
|
-
case "dismiss":
|
|
504
|
-
return runFixDismiss(context, profileOptions, tenantId);
|
|
505
|
-
default:
|
|
506
|
-
return runFixDetail(context, profileOptions, tenantId, sub);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
async function runFixStart(context, profileOptions, tenantId) {
|
|
510
|
-
const problemId = (context.positionals[1] || context.flags["problem-id"] || "").trim();
|
|
511
|
-
if (!problemId) {
|
|
512
|
-
throw new CliInputError("<problem_id> is required", "Usage: moda fix start <problem_id> [--type=auto|prompt] [--wait] [--timeout=ms] [--poll-interval=ms]");
|
|
513
|
-
}
|
|
514
|
-
const fixType = context.flags.type;
|
|
515
|
-
if (fixType !== undefined && fixType !== "auto" && fixType !== "prompt") {
|
|
516
|
-
throw new CliInputError(`--type must be 'auto' or 'prompt'; got '${fixType}'`);
|
|
517
|
-
}
|
|
518
|
-
const created = await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/problems/${encodeURIComponent(problemId)}/fixes`, { method: "POST", body: JSON.stringify(fixType ? { fixType } : {}) }, profileOptions, { timeoutMs: 120000, retries: 0 });
|
|
519
|
-
if (context.flags.wait !== "true") {
|
|
520
|
-
if (context.outputMode === "human") {
|
|
521
|
-
process.stderr.write(`Fix MODA-FIX-${created.shortRef} (${created.id}) drafted for problem ${created.problemId} — status ${created.status}.
|
|
522
|
-
`);
|
|
523
|
-
}
|
|
524
|
-
context.output.writeData(created, {
|
|
525
|
-
summary: {
|
|
526
|
-
text: `Fix MODA-FIX-${created.shortRef} drafted (status ${created.status}). Re-run with --wait to drive the pipeline.`,
|
|
527
|
-
confidence: "high"
|
|
528
|
-
},
|
|
529
|
-
nextCommands: [
|
|
530
|
-
{
|
|
531
|
-
command: `moda fix start ${shellArg(problemId)} --wait`,
|
|
532
|
-
purpose: "Drive the pipeline (scope → propose → gate) to a resting status.",
|
|
533
|
-
mutability: "write",
|
|
534
|
-
requires_approval: false
|
|
535
|
-
},
|
|
536
|
-
{
|
|
537
|
-
command: `moda fix ${shellArg(created.id)}`,
|
|
538
|
-
purpose: "Inspect the fix status and gate result.",
|
|
539
|
-
mutability: "read",
|
|
540
|
-
requires_approval: false
|
|
541
|
-
}
|
|
542
|
-
]
|
|
543
|
-
});
|
|
544
|
-
return EXIT_OK;
|
|
545
|
-
}
|
|
546
|
-
const timeoutMs = parsePositiveInt(context.flags.timeout, DEFAULT_WAIT_TIMEOUT_MS, MAX_WAIT_TIMEOUT_MS);
|
|
547
|
-
const pollIntervalMs = parsePositiveInt(context.flags["poll-interval"], DEFAULT_POLL_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
|
|
548
|
-
const fix = await driveAdvance({
|
|
549
|
-
tenantId,
|
|
550
|
-
fixId: created.id,
|
|
551
|
-
initial: created,
|
|
552
|
-
timeoutMs,
|
|
553
|
-
pollIntervalMs,
|
|
554
|
-
profileOptions,
|
|
555
|
-
context
|
|
556
|
-
});
|
|
557
|
-
const packet = await fetchPacketFailSoft(tenantId, created.id, profileOptions);
|
|
558
|
-
const fidelityCopy = fixVerifiedFidelityCopy(fix);
|
|
559
|
-
if (context.outputMode === "human") {
|
|
560
|
-
process.stderr.write(`
|
|
561
|
-
Fix MODA-FIX-${fix.shortRef} rested at ${fix.status}${fidelityCopy ? ` — ${fidelityCopy}` : ""}${fix.statusReason ? ` — ${fix.statusReason}` : ""}
|
|
562
|
-
`);
|
|
563
|
-
}
|
|
564
|
-
context.output.writeData({ fix, ...packet ? { packet } : {} }, {
|
|
565
|
-
summary: {
|
|
566
|
-
text: `Fix MODA-FIX-${fix.shortRef} reached ${fix.status}${fix.statusReason ? ` (${fix.statusReason})` : ""}.${fidelityCopy ? ` ${fidelityCopy}.` : ""}`,
|
|
567
|
-
confidence: "high"
|
|
568
|
-
},
|
|
569
|
-
findings: gateFindings(fix),
|
|
570
|
-
nextCommands: nextCommandsFor(fix)
|
|
571
|
-
});
|
|
572
|
-
return EXIT_OK;
|
|
573
|
-
}
|
|
574
|
-
async function runFixVerify(context, profileOptions, tenantId) {
|
|
575
|
-
const usage = "Usage: moda fix verify <fix_id> [--prompt-file=PATH] [--no-wait] [--timeout=ms] [--poll-interval=ms]";
|
|
576
|
-
const fixId = (context.positionals[1] || context.flags["fix-id"] || "").trim();
|
|
577
|
-
if (!fixId) {
|
|
578
|
-
throw new CliInputError("<fix_id> is required", usage);
|
|
579
|
-
}
|
|
580
|
-
let candidateContent;
|
|
581
|
-
const promptFile = context.flags["prompt-file"];
|
|
582
|
-
if (promptFile) {
|
|
583
|
-
const absPath = isAbsolute(promptFile) ? promptFile : resolve(process.cwd(), promptFile);
|
|
584
|
-
if (!existsSync(absPath)) {
|
|
585
|
-
throw new CliInputError(`--prompt-file not found: ${promptFile}`, usage);
|
|
586
|
-
}
|
|
587
|
-
candidateContent = readFileSync(absPath, "utf8");
|
|
588
|
-
if (candidateContent.trim().length < 40) {
|
|
589
|
-
throw new CliInputError("--prompt-file content is shorter than 40 characters — refusing to gate a truncated candidate");
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
const candidateHash = candidateContent === undefined ? undefined : sha256(candidateContent.trim());
|
|
593
|
-
if (!context.yes) {
|
|
594
|
-
await assertRegateCooldown(tenantId, fixId, context, profileOptions, candidateHash);
|
|
595
|
-
}
|
|
596
|
-
const enqueue = await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/verify`, { method: "POST", body: JSON.stringify(candidateContent ? { candidateContent } : {}) }, profileOptions, { timeoutMs: 120000, retries: 0 });
|
|
597
|
-
const noWait = context.flags.wait === "false" || context.flags["no-wait"] === "true";
|
|
598
|
-
if (noWait) {
|
|
599
|
-
context.output.writeData({ fixId, gateRunId: enqueue.gateRunId, status: "GATING" }, {
|
|
600
|
-
summary: {
|
|
601
|
-
text: `Gate + control runs enqueued (gate run ${enqueue.gateRunId}). Resume with \`moda fix ${shellArg(fixId)} --wait\`.`,
|
|
602
|
-
confidence: "high"
|
|
603
|
-
},
|
|
604
|
-
nextCommands: [
|
|
605
|
-
{
|
|
606
|
-
command: `moda fix ${shellArg(fixId)} --wait`,
|
|
607
|
-
purpose: "Drive the advance-on-poll gate to a verdict.",
|
|
608
|
-
mutability: "write",
|
|
609
|
-
requires_approval: false
|
|
610
|
-
}
|
|
611
|
-
]
|
|
612
|
-
});
|
|
613
|
-
return EXIT_OK;
|
|
614
|
-
}
|
|
615
|
-
const timeoutMs = parsePositiveInt(context.flags.timeout, DEFAULT_WAIT_TIMEOUT_MS, MAX_WAIT_TIMEOUT_MS);
|
|
616
|
-
const pollIntervalMs = parsePositiveInt(context.flags["poll-interval"], DEFAULT_POLL_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
|
|
617
|
-
const deadline = Date.now() + timeoutMs;
|
|
618
|
-
let fix = await advanceFix(tenantId, fixId, profileOptions);
|
|
619
|
-
while (fix.status === "GATING") {
|
|
620
|
-
if (Date.now() >= deadline) {
|
|
621
|
-
throw new Error(`Timed out after ${timeoutMs}ms waiting for gate run ${enqueue.gateRunId} to finish`);
|
|
622
|
-
}
|
|
623
|
-
if (context.outputMode === "human") {
|
|
624
|
-
process.stderr.write(`Waiting for gate run (${fix.statusReason || "running"})...
|
|
625
|
-
`);
|
|
626
|
-
}
|
|
627
|
-
await sleep(pollIntervalMs);
|
|
628
|
-
fix = await advanceFix(tenantId, fixId, profileOptions);
|
|
629
|
-
}
|
|
630
|
-
const warnings = [];
|
|
631
|
-
if (fix.gateRunId && fix.gateRunId !== enqueue.gateRunId) {
|
|
632
|
-
warnings.push(`The reported verdict is for gate run ${fix.gateRunId}, not the one this command enqueued (${enqueue.gateRunId}) — a concurrent verify superseded it.`);
|
|
633
|
-
}
|
|
634
|
-
const gateResult = fix.gateResult ?? null;
|
|
635
|
-
const verdict = gateResult?.verdict ?? "";
|
|
636
|
-
let exitCode;
|
|
637
|
-
let envelopeStatus;
|
|
638
|
-
if (fix.status === "VERIFIED" && verdict === "pass") {
|
|
639
|
-
exitCode = EXIT_OK;
|
|
640
|
-
envelopeStatus = "success";
|
|
641
|
-
} else if (fix.status === "GATE_FAILED" || verdict === "fail") {
|
|
642
|
-
exitCode = EXIT_ERROR;
|
|
643
|
-
envelopeStatus = "error";
|
|
644
|
-
} else {
|
|
645
|
-
exitCode = EXIT_DEGRADED;
|
|
646
|
-
envelopeStatus = "degraded";
|
|
647
|
-
}
|
|
648
|
-
let cases = [];
|
|
649
|
-
if (exitCode === EXIT_ERROR && fix.replaySetId) {
|
|
650
|
-
try {
|
|
651
|
-
const payload = await fetchLatestRun(tenantId, String(fix.replaySetId), enqueue.gateRunId, profileOptions);
|
|
652
|
-
if (payload.run?.runId === enqueue.gateRunId) {
|
|
653
|
-
cases = payload.cases;
|
|
654
|
-
}
|
|
655
|
-
} catch {
|
|
656
|
-
warnings.push("Could not load per-case gate results — see the replay run in the dashboard.");
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
if (context.outputMode === "human") {
|
|
660
|
-
printHumanGateVerdict(fix, enqueue.gateRunId, gateResult, cases);
|
|
661
|
-
}
|
|
662
|
-
const findings = gateFindings(fix);
|
|
663
|
-
for (const row of cases.slice(0, PER_CASE_FINDINGS_CAP)) {
|
|
664
|
-
const baseline = passLabel(row.prodPass);
|
|
665
|
-
const candidate = passLabel(row.proposedPass);
|
|
666
|
-
findings.push({
|
|
667
|
-
id: `case:${row.caseId}`,
|
|
668
|
-
title: `${row.caseId}: baseline=${baseline} candidate=${candidate}`,
|
|
669
|
-
status: candidate,
|
|
670
|
-
severity: baseline === "pass" && candidate === "fail" ? "error" : candidate === "fail" ? "warning" : "info"
|
|
671
|
-
});
|
|
672
|
-
}
|
|
673
|
-
const fidelityCopy = verifiedFidelityCopy(gateResult);
|
|
674
|
-
context.output.writeData({
|
|
675
|
-
fixId: fix.id,
|
|
676
|
-
shortRef: fix.shortRef,
|
|
677
|
-
status: fix.status,
|
|
678
|
-
gateRunId: enqueue.gateRunId,
|
|
679
|
-
verdict: verdict || null,
|
|
680
|
-
gateResult,
|
|
681
|
-
...gateResult?.baselineFidelity ? { baselineFidelity: gateResult.baselineFidelity } : {},
|
|
682
|
-
...gateResult?.counterfactual ? { counterfactual: gateResult.counterfactual } : {},
|
|
683
|
-
...cases.length > 0 ? { cases } : {}
|
|
684
|
-
}, {
|
|
685
|
-
status: envelopeStatus,
|
|
686
|
-
summary: {
|
|
687
|
-
text: verdict === "pass" ? `${fidelityCopy ? `${fidelityCopy}. ` : ""}Gate passed on the frozen holdout: ${gateResult?.reason ?? ""}`.trim() : verdict === "fail" ? `Gate failed: ${gateResult?.reason ?? fix.statusReason ?? "candidate did not beat baseline"}` : `Gate degraded (${fix.status}): ${gateResult?.reason ?? fix.statusReason ?? "no verdict"}`,
|
|
688
|
-
confidence: "high"
|
|
689
|
-
},
|
|
690
|
-
findings,
|
|
691
|
-
warnings,
|
|
692
|
-
nextCommands: nextCommandsFor(fix)
|
|
693
|
-
});
|
|
694
|
-
return exitCode;
|
|
695
|
-
}
|
|
696
|
-
async function runFixCheckout(context, profileOptions, tenantId) {
|
|
697
|
-
const fixId = (context.positionals[1] || context.flags["fix-id"] || "").trim();
|
|
698
|
-
if (!fixId) {
|
|
699
|
-
throw new CliInputError("<fix_id> is required", "Usage: moda fix checkout <fix_id>");
|
|
700
|
-
}
|
|
701
|
-
const fix = await getFix(tenantId, fixId, profileOptions);
|
|
702
|
-
if (isArtifactFix(fix)) {
|
|
703
|
-
throw new CliInputError(`Fix MODA-FIX-${fix.shortRef} is a ${fix.fixType} fix — its candidate is applied in your own stack, not checked out. Run \`moda fix ${shellArg(fixId)} --packet\` for the candidate (written as local artifacts), apply it, then confirm with \`moda fix mark-applied ${shellArg(fixId)}\`.`);
|
|
704
|
-
}
|
|
705
|
-
if (!fix.targetSourcePath) {
|
|
706
|
-
throw new CliInputError(`Fix MODA-FIX-${fix.shortRef} has no target source path — ${fix.fixType} fixes are packet-only. Run \`moda fix ${shellArg(fixId)} --packet\` for the diagnosis packet.`);
|
|
707
|
-
}
|
|
708
|
-
const candidateVersionId = fix.candidateRef?.promptVersionId ?? "";
|
|
709
|
-
if (!candidateVersionId) {
|
|
710
|
-
throw new CliInputError(`Fix MODA-FIX-${fix.shortRef} has no registered candidate version yet (status ${fix.status}). Run \`moda fix start ${shellArg(fix.problemId)} --wait\` to propose one, or edit the target locally and gate it with \`moda fix verify ${shellArg(fixId)} --prompt-file=<path>\`.`);
|
|
711
|
-
}
|
|
712
|
-
if (!fix.promptId) {
|
|
713
|
-
throw new CliInputError(`Fix MODA-FIX-${fix.shortRef} has no routed prompt — the candidate content cannot be resolved.`);
|
|
714
|
-
}
|
|
715
|
-
const prompt = await callControlAPI(`/prompts/${encodeURIComponent(String(fix.promptId))}`, { method: "GET" }, profileOptions);
|
|
716
|
-
const version = (prompt.versions ?? []).find((row) => row.id === candidateVersionId);
|
|
717
|
-
if (!version || typeof version.content !== "string" || !version.content) {
|
|
718
|
-
throw new Error(`Candidate prompt version ${candidateVersionId} not found in the registry`);
|
|
719
|
-
}
|
|
720
|
-
const relPath = safeTargetSourcePath(String(fix.targetSourcePath));
|
|
721
|
-
const absPath = join(process.cwd(), relPath);
|
|
722
|
-
assertRealpathContained(absPath, relPath);
|
|
723
|
-
const writeStatus = writeContainedFile(absPath, relPath, version.content);
|
|
724
|
-
const branch = `moda/fix/${String(fix.shortRef).toLowerCase()}`;
|
|
725
|
-
const magicWord = `Fixes MODA-FIX-${fix.shortRef}`;
|
|
726
|
-
if (context.outputMode === "human") {
|
|
727
|
-
process.stderr.write([
|
|
728
|
-
`${writeStatus} ${relPath} (candidate ${candidateVersionId})`,
|
|
729
|
-
"",
|
|
730
|
-
"Next steps (the CLI never runs git — branching and committing are yours):",
|
|
731
|
-
` 1. git checkout -b ${branch}`,
|
|
732
|
-
` 2. Review the diff. Re-gate any edits: moda fix verify ${shellArg(fix.id)} --prompt-file=${shellArg(relPath)}`,
|
|
733
|
-
` 3. Hand back: moda fix submit ${shellArg(fix.id)} --pr (or --local-ref=${shellArg(branch)})`,
|
|
734
|
-
` A hand-authored PR body must end with the magic word: ${magicWord}`,
|
|
735
|
-
""
|
|
736
|
-
].join(`
|
|
737
|
-
`));
|
|
738
|
-
}
|
|
739
|
-
context.output.writeData({
|
|
740
|
-
fixId: fix.id,
|
|
741
|
-
shortRef: fix.shortRef,
|
|
742
|
-
path: relPath,
|
|
743
|
-
writeStatus,
|
|
744
|
-
branch,
|
|
745
|
-
magicWord,
|
|
746
|
-
candidateVersionId
|
|
747
|
-
}, {
|
|
748
|
-
summary: {
|
|
749
|
-
text: `Candidate for MODA-FIX-${fix.shortRef} ${writeStatus === "unchanged" ? "already matches" : "written to"} ${relPath}. Create branch ${branch} yourself — the CLI never runs git.`,
|
|
750
|
-
confidence: "high"
|
|
751
|
-
},
|
|
752
|
-
artifacts: [
|
|
753
|
-
{
|
|
754
|
-
kind: "fix_candidate",
|
|
755
|
-
path: relPath,
|
|
756
|
-
description: `Candidate content for fix MODA-FIX-${fix.shortRef} (prompt version ${candidateVersionId}).`
|
|
757
|
-
}
|
|
758
|
-
],
|
|
759
|
-
nextCommands: [
|
|
760
|
-
{
|
|
761
|
-
command: `moda fix verify ${shellArg(fix.id)} --prompt-file=${shellArg(relPath)}`,
|
|
762
|
-
purpose: "Re-gate the (possibly edited) local candidate on the frozen holdout.",
|
|
763
|
-
mutability: "write",
|
|
764
|
-
requires_approval: false
|
|
765
|
-
},
|
|
766
|
-
{
|
|
767
|
-
command: `moda fix submit ${shellArg(fix.id)} --pr`,
|
|
768
|
-
purpose: `Open the draft fix PR (branch ${branch}, magic word "${magicWord}").`,
|
|
769
|
-
mutability: "write",
|
|
770
|
-
requires_approval: true
|
|
771
|
-
}
|
|
772
|
-
]
|
|
773
|
-
});
|
|
774
|
-
return EXIT_OK;
|
|
775
|
-
}
|
|
776
|
-
async function runFixSubmit(context, profileOptions, tenantId) {
|
|
777
|
-
const usage = "Usage: moda fix submit <fix_id> --pr | --local-ref=<branch>";
|
|
778
|
-
const fixId = (context.positionals[1] || context.flags["fix-id"] || "").trim();
|
|
779
|
-
if (!fixId) {
|
|
780
|
-
throw new CliInputError("<fix_id> is required", usage);
|
|
781
|
-
}
|
|
782
|
-
const wantPr = context.flags.pr === "true";
|
|
783
|
-
const localRef = (context.flags["local-ref"] ?? "").trim();
|
|
784
|
-
if (wantPr === (localRef !== "")) {
|
|
785
|
-
throw new CliInputError("Pass exactly one of --pr or --local-ref=<branch>", usage);
|
|
786
|
-
}
|
|
787
|
-
const result = await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/submit`, {
|
|
788
|
-
method: "POST",
|
|
789
|
-
body: JSON.stringify(wantPr ? { channel: "pr" } : { channel: "local_ref", localRef })
|
|
790
|
-
}, profileOptions, { timeoutMs: 120000, retries: 0 });
|
|
791
|
-
if (context.outputMode === "human") {
|
|
792
|
-
process.stderr.write(result.prUrl ? `Draft PR open: ${result.prUrl} (${result.prBranch ?? ""}) — merging it confirms the fix.
|
|
793
|
-
` : `Fix recorded as ${result.status}${result.prBranch ? ` on ${result.prBranch}` : ""}.
|
|
794
|
-
`);
|
|
795
|
-
}
|
|
796
|
-
context.output.writeData({ fixId, ...result }, {
|
|
797
|
-
summary: {
|
|
798
|
-
text: result.prUrl ? `Draft fix PR open at ${result.prUrl}. Merging with the magic word confirms the fix and starts monitoring.` : `Fix ${fixId} recorded as ${result.status}${result.prBranch ? ` on branch ${result.prBranch}` : ""}. Land the branch with the magic word in the PR body to confirm the fix.`,
|
|
799
|
-
confidence: "high"
|
|
800
|
-
},
|
|
801
|
-
nextCommands: [
|
|
802
|
-
{
|
|
803
|
-
command: `moda fix ${shellArg(fixId)}`,
|
|
804
|
-
purpose: "Track delivery and confirmation status.",
|
|
805
|
-
mutability: "read",
|
|
806
|
-
requires_approval: false
|
|
807
|
-
}
|
|
808
|
-
]
|
|
809
|
-
});
|
|
810
|
-
return EXIT_OK;
|
|
811
|
-
}
|
|
812
|
-
async function runFixMarkApplied(context, profileOptions, tenantId) {
|
|
813
|
-
const usage = "Usage: moda fix mark-applied <fix_id> [--note=TEXT]";
|
|
814
|
-
const fixId = (context.positionals[1] || context.flags["fix-id"] || "").trim();
|
|
815
|
-
if (!fixId) {
|
|
816
|
-
throw new CliInputError("<fix_id> is required", usage);
|
|
817
|
-
}
|
|
818
|
-
const note = (context.flags.note ?? "").trim();
|
|
819
|
-
if (note.length > 600) {
|
|
820
|
-
throw new CliInputError(`--note must be 600 characters or fewer (got ${note.length})`, usage);
|
|
821
|
-
}
|
|
822
|
-
const fix = await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/mark-applied`, { method: "POST", body: JSON.stringify(note ? { note } : {}) }, profileOptions, { timeoutMs: 60000, retries: 0 });
|
|
823
|
-
if (context.outputMode === "human") {
|
|
824
|
-
process.stderr.write(fix.duplicate === true ? `Fix MODA-FIX-${fix.shortRef} was already confirmed (${fix.status}) — duplicate no-op; the mark_fixed feedback was re-posted.
|
|
825
|
-
` : `Fix MODA-FIX-${fix.shortRef} marked applied (${fix.status}) — mark_fixed feedback posted, monitoring for recurrence.
|
|
826
|
-
`);
|
|
827
|
-
}
|
|
828
|
-
context.output.writeData(fix, {
|
|
829
|
-
summary: {
|
|
830
|
-
text: fix.duplicate === true ? `Fix MODA-FIX-${fix.shortRef} was already confirmed (${fix.status}); mark-applied was a duplicate no-op.` : `Fix MODA-FIX-${fix.shortRef} marked applied — status ${fix.status}; the problem enters fixed-monitoring.`,
|
|
831
|
-
confidence: "high"
|
|
832
|
-
},
|
|
833
|
-
nextCommands: [
|
|
834
|
-
{
|
|
835
|
-
command: `moda fix ${shellArg(fix.id)}`,
|
|
836
|
-
purpose: "Track confirmation monitoring (HELD after 14 clean days; REGRESSED on reopen).",
|
|
837
|
-
mutability: "read",
|
|
838
|
-
requires_approval: false
|
|
839
|
-
}
|
|
840
|
-
]
|
|
841
|
-
});
|
|
842
|
-
return EXIT_OK;
|
|
843
|
-
}
|
|
844
|
-
async function runFixDismiss(context, profileOptions, tenantId) {
|
|
845
|
-
const usage = "Usage: moda fix dismiss <fix_id> --reason=TEXT";
|
|
846
|
-
const fixId = (context.positionals[1] || context.flags["fix-id"] || "").trim();
|
|
847
|
-
if (!fixId) {
|
|
848
|
-
throw new CliInputError("<fix_id> is required", usage);
|
|
849
|
-
}
|
|
850
|
-
const reason = context.flags.reason;
|
|
851
|
-
if (!reason || !reason.trim()) {
|
|
852
|
-
throw new CliInputError("--reason=TEXT is required (it feeds problem feedback)", usage);
|
|
853
|
-
}
|
|
854
|
-
const fix = await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/dismiss`, { method: "POST", body: JSON.stringify({ reason }) }, profileOptions, { timeoutMs: 60000, retries: 0 });
|
|
855
|
-
if (context.outputMode === "human") {
|
|
856
|
-
process.stderr.write(`Fix MODA-FIX-${fix.shortRef} dismissed (${fix.status}).
|
|
857
|
-
`);
|
|
858
|
-
}
|
|
859
|
-
context.output.writeData(fix, {
|
|
860
|
-
summary: {
|
|
861
|
-
text: `Fix MODA-FIX-${fix.shortRef} dismissed — the reason was recorded as problem feedback.`,
|
|
862
|
-
confidence: "high"
|
|
863
|
-
}
|
|
864
|
-
});
|
|
865
|
-
return EXIT_OK;
|
|
866
|
-
}
|
|
867
|
-
async function runFixDetail(context, profileOptions, tenantId, fixId) {
|
|
868
|
-
if (context.flags.packet === "true") {
|
|
869
|
-
const packet = await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/packet`, { method: "GET" }, profileOptions);
|
|
870
|
-
const extras = materializePacketCandidates(packet, fixId, context.outputMode === "human");
|
|
871
|
-
context.output.writeData(packet, {
|
|
872
|
-
summary: {
|
|
873
|
-
text: `moda.fix_packet.v1 for fix ${fixId}.${extras.summarySuffix}`,
|
|
874
|
-
confidence: "high"
|
|
875
|
-
},
|
|
876
|
-
...extras.findings.length > 0 ? { findings: extras.findings } : {},
|
|
877
|
-
...extras.artifacts.length > 0 ? { artifacts: extras.artifacts } : {},
|
|
878
|
-
...extras.warnings.length > 0 ? { warnings: extras.warnings } : {},
|
|
879
|
-
...extras.nextCommands.length > 0 ? { nextCommands: extras.nextCommands } : {}
|
|
880
|
-
});
|
|
881
|
-
return EXIT_OK;
|
|
882
|
-
}
|
|
883
|
-
let fix = await getFix(tenantId, fixId, profileOptions);
|
|
884
|
-
if (context.flags.wait === "true" && ADVANCING_STATUSES.has(fix.status)) {
|
|
885
|
-
const timeoutMs = parsePositiveInt(context.flags.timeout, DEFAULT_WAIT_TIMEOUT_MS, MAX_WAIT_TIMEOUT_MS);
|
|
886
|
-
const pollIntervalMs = parsePositiveInt(context.flags["poll-interval"], DEFAULT_POLL_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
|
|
887
|
-
fix = await driveAdvance({
|
|
888
|
-
tenantId,
|
|
889
|
-
fixId,
|
|
890
|
-
initial: fix,
|
|
891
|
-
timeoutMs,
|
|
892
|
-
pollIntervalMs,
|
|
893
|
-
profileOptions,
|
|
894
|
-
context
|
|
895
|
-
});
|
|
896
|
-
}
|
|
897
|
-
const fidelityCopy = fixVerifiedFidelityCopy(fix);
|
|
898
|
-
const detailCf = counterfactualBlock(fix.gateResult ?? null);
|
|
899
|
-
if (context.outputMode === "human") {
|
|
900
|
-
process.stderr.write(`MODA-FIX-${fix.shortRef} ${fix.status} ${fix.fixType}${fidelityCopy ? ` — ${fidelityCopy}` : ""}${fix.statusReason ? ` — ${fix.statusReason}` : ""}
|
|
901
|
-
`);
|
|
902
|
-
if (detailCf) {
|
|
903
|
-
process.stderr.write(`${counterfactualHumanLine(detailCf)}
|
|
904
|
-
`);
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
context.output.writeData(fix, {
|
|
908
|
-
summary: {
|
|
909
|
-
text: `Fix MODA-FIX-${fix.shortRef} is ${fix.status}${fix.statusReason ? ` (${fix.statusReason})` : ""}.${fidelityCopy ? ` ${fidelityCopy}.` : ""}`,
|
|
910
|
-
confidence: "high"
|
|
911
|
-
},
|
|
912
|
-
findings: gateFindings(fix),
|
|
913
|
-
nextCommands: nextCommandsFor(fix)
|
|
914
|
-
});
|
|
915
|
-
return EXIT_OK;
|
|
916
|
-
}
|
|
917
|
-
async function driveAdvance(args) {
|
|
918
|
-
const deadline = Date.now() + args.timeoutMs;
|
|
919
|
-
let fix = args.initial;
|
|
920
|
-
let lastStatus = "";
|
|
921
|
-
const announce = (row) => {
|
|
922
|
-
if (row.status === lastStatus)
|
|
923
|
-
return;
|
|
924
|
-
lastStatus = row.status;
|
|
925
|
-
announceFixTransition(args.context, row);
|
|
926
|
-
};
|
|
927
|
-
announce(fix);
|
|
928
|
-
while (ADVANCING_STATUSES.has(fix.status)) {
|
|
929
|
-
if (Date.now() >= deadline) {
|
|
930
|
-
throw new Error(`Timed out after ${args.timeoutMs}ms waiting for fix ${args.fixId} (status ${fix.status})`);
|
|
931
|
-
}
|
|
932
|
-
fix = await advanceFix(args.tenantId, args.fixId, args.profileOptions);
|
|
933
|
-
announce(fix);
|
|
934
|
-
if (!ADVANCING_STATUSES.has(fix.status))
|
|
935
|
-
break;
|
|
936
|
-
if (isRestingProposed(fix)) {
|
|
937
|
-
break;
|
|
938
|
-
}
|
|
939
|
-
await sleep(fix.status === "GATING" ? args.pollIntervalMs : Math.min(STEP_DELAY_MS, args.pollIntervalMs));
|
|
940
|
-
}
|
|
941
|
-
return fix;
|
|
942
|
-
}
|
|
943
|
-
function profileOptionsFor(context) {
|
|
944
|
-
return { profile: context.profile, env: context.env };
|
|
945
|
-
}
|
|
946
|
-
function requireTenantId(flags, profileOptions) {
|
|
947
|
-
const tenantId = flags["tenant-id"] || resolveApiTenantId(profileOptions);
|
|
948
|
-
if (!tenantId) {
|
|
949
|
-
throw new CliInputError("Missing tenant id. Run `moda init` or pass --tenant-id=<id>.");
|
|
950
|
-
}
|
|
951
|
-
return tenantId;
|
|
952
|
-
}
|
|
953
|
-
function fixesBase(tenantId) {
|
|
954
|
-
return `/tenants/${encodeURIComponent(tenantId)}/fixes`;
|
|
955
|
-
}
|
|
956
|
-
async function assertRegateCooldown(tenantId, fixId, context, profileOptions, candidateHash) {
|
|
957
|
-
const current = await getFix(tenantId, fixId, profileOptions);
|
|
958
|
-
if (candidateHash && current.candidateRef?.contentSha256 !== candidateHash)
|
|
959
|
-
return;
|
|
960
|
-
const gate = current.gateResult ?? null;
|
|
961
|
-
if (!REGATE_COOLDOWN_STATUSES.has(current.status) || !gate?.verdict || gate.pending)
|
|
962
|
-
return;
|
|
963
|
-
const landedAt = typeof gate.completedAt === "string" ? Date.parse(gate.completedAt) : Number.NaN;
|
|
964
|
-
if (Number.isNaN(landedAt))
|
|
965
|
-
return;
|
|
966
|
-
const ageMs = Date.now() - landedAt;
|
|
967
|
-
if (ageMs >= VERIFY_COOLDOWN_MS)
|
|
968
|
-
return;
|
|
969
|
-
const waitMin = Math.ceil((VERIFY_COOLDOWN_MS - ageMs) / 60000);
|
|
970
|
-
const rerun = ["moda", "fix", "verify", shellArg(fixId), ...Object.entries(context.flags).filter(([key]) => key !== "yes" && key !== "fix-id").map(([key, value]) => value === "true" ? `--${key}` : `--${key}=${shellArg(value)}`), "--yes"].join(" ");
|
|
971
|
-
throw new CliInputError(`MODA-FIX-${current.shortRef} was gated ${Math.max(0, Math.floor(ageMs / 60000))} min ago (verdict ${gate.verdict}); re-gating within 10 minutes re-runs the replay. Nothing was sent.`, `Wait ~${waitMin} min, or re-run with --yes to gate now: ${rerun}`);
|
|
972
|
-
}
|
|
973
|
-
function sha256(content) {
|
|
974
|
-
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
975
|
-
}
|
|
976
|
-
async function getFix(tenantId, fixId, profileOptions) {
|
|
977
|
-
return await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}`, { method: "GET" }, profileOptions);
|
|
978
|
-
}
|
|
979
|
-
async function advanceFix(tenantId, fixId, profileOptions) {
|
|
980
|
-
return await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/advance`, { method: "POST" }, profileOptions, { timeoutMs: ADVANCE_TIMEOUT_MS, retries: 0 });
|
|
981
|
-
}
|
|
982
|
-
async function fetchPacketFailSoft(tenantId, fixId, profileOptions) {
|
|
983
|
-
try {
|
|
984
|
-
return await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/packet`, { method: "GET" }, profileOptions);
|
|
985
|
-
} catch {
|
|
986
|
-
return null;
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
function isRestingProposed(fix) {
|
|
990
|
-
return fix.status === "PROPOSED" && /gate (pending|unavailable)/i.test(fix.statusReason ?? "");
|
|
991
|
-
}
|
|
992
|
-
function announceFixTransition(context, row) {
|
|
993
|
-
const line = `fix MODA-FIX-${row.shortRef}: ${row.status}${row.statusReason ? ` — ${row.statusReason}` : ""}`;
|
|
994
|
-
if (context.outputMode === "human") {
|
|
995
|
-
process.stderr.write(`${line}
|
|
996
|
-
`);
|
|
997
|
-
} else if (context.outputMode === "agent-stream") {
|
|
998
|
-
context.output.writeEvent({
|
|
999
|
-
event: "progress",
|
|
1000
|
-
phase: "fix",
|
|
1001
|
-
message: line,
|
|
1002
|
-
elapsed_ms: Date.now() - context.startedAt
|
|
1003
|
-
});
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
function isArtifactFix(fix) {
|
|
1007
|
-
return fix.fixType === "TOOL_SCHEMA" || fix.fixType === "SKILL";
|
|
1008
|
-
}
|
|
1009
|
-
function materializePacketCandidates(packet, fixId, human) {
|
|
1010
|
-
const findings = [];
|
|
1011
|
-
const artifacts = [];
|
|
1012
|
-
const warnings = [];
|
|
1013
|
-
const nextCommands = [];
|
|
1014
|
-
const summaryParts = [];
|
|
1015
|
-
const tool = packet.candidate_tool;
|
|
1016
|
-
const skill = packet.candidate_skill;
|
|
1017
|
-
const skillEdit = packet.candidate_skill_edit;
|
|
1018
|
-
const hasTool = typeof tool?.proposed_description === "string" && tool.proposed_description !== "";
|
|
1019
|
-
const hasSkill = typeof skill?.skill_md === "string" && skill.skill_md !== "";
|
|
1020
|
-
const hasSkillEdit = typeof skillEdit?.proposed_skill_md === "string" && skillEdit.proposed_skill_md !== "";
|
|
1021
|
-
if (!hasTool && !hasSkill && !hasSkillEdit) {
|
|
1022
|
-
return { findings, artifacts, warnings, nextCommands, summarySuffix: "" };
|
|
1023
|
-
}
|
|
1024
|
-
const rawRef = typeof packet.short_ref === "string" && packet.short_ref.trim() ? packet.short_ref : fixId;
|
|
1025
|
-
const ref = rawRef.replace(/[^A-Za-z0-9._-]/g, "").replace(/^\.+/, "").slice(0, 64) || "fix";
|
|
1026
|
-
const w = (line) => process.stderr.write(`${line}
|
|
1027
|
-
`);
|
|
1028
|
-
const block = (label, text) => {
|
|
1029
|
-
w(` ${label}:`);
|
|
1030
|
-
for (const line of (text || "(none)").split(`
|
|
1031
|
-
`))
|
|
1032
|
-
w(` ${line}`);
|
|
1033
|
-
};
|
|
1034
|
-
const writeCandidateFile = (name, content, kind, description) => {
|
|
1035
|
-
const relPath = safeTargetSourcePath(`.moda/fixes/${ref}/${name}`);
|
|
1036
|
-
const absPath = join(process.cwd(), relPath);
|
|
1037
|
-
try {
|
|
1038
|
-
assertRealpathContained(absPath, relPath);
|
|
1039
|
-
writeContainedFile(absPath, relPath, content);
|
|
1040
|
-
artifacts.push({ kind, path: relPath, description });
|
|
1041
|
-
return relPath;
|
|
1042
|
-
} catch (error) {
|
|
1043
|
-
warnings.push(`Could not write ${relPath}: ${error instanceof Error ? error.message : String(error)} — the full candidate text is still in the packet payload.`);
|
|
1044
|
-
return null;
|
|
1045
|
-
}
|
|
1046
|
-
};
|
|
1047
|
-
if (hasTool && tool) {
|
|
1048
|
-
const toolName = typeof tool.tool_name === "string" && tool.tool_name ? tool.tool_name : "unknown-tool";
|
|
1049
|
-
const source = typeof tool.current_description_source === "string" ? tool.current_description_source : "";
|
|
1050
|
-
const paramNotes = (Array.isArray(tool.param_notes) ? tool.param_notes : []).filter((note) => typeof note === "string" && note !== "");
|
|
1051
|
-
const rationale = typeof tool.rationale === "string" ? tool.rationale : "";
|
|
1052
|
-
const currentPath = writeCandidateFile("tool-description.current.md", typeof tool.current_description === "string" ? tool.current_description : "", "fix_tool_description_current", `Current description of tool '${toolName}'${source ? ` (from ${source})` : ""} — the diff base.`);
|
|
1053
|
-
const proposedPath = writeCandidateFile("tool-description.proposed.md", tool.proposed_description ?? "", "fix_tool_description_proposed", `Proposed description for tool '${toolName}' — apply it to your tool definition, then \`moda fix mark-applied ${shellArg(fixId)}\`.`);
|
|
1054
|
-
findings.push({
|
|
1055
|
-
id: "fix:candidate_tool",
|
|
1056
|
-
title: `tool '${toolName}' description candidate${source ? ` (current from ${source})` : ""}`,
|
|
1057
|
-
status: "proposed",
|
|
1058
|
-
severity: "info",
|
|
1059
|
-
...rationale || paramNotes.length > 0 ? {
|
|
1060
|
-
description: [rationale, ...paramNotes.map((note) => `param note: ${note}`)].filter(Boolean).join(`
|
|
1061
|
-
`)
|
|
1062
|
-
} : {}
|
|
1063
|
-
});
|
|
1064
|
-
summaryParts.push(`Includes a tool-description candidate for '${toolName}'.`);
|
|
1065
|
-
if (human) {
|
|
1066
|
-
w("");
|
|
1067
|
-
w(`Tool-description candidate — '${toolName}'${source ? ` (current description from ${source})` : ""}`);
|
|
1068
|
-
block("current ", tool.current_description ?? "");
|
|
1069
|
-
block("proposed", tool.proposed_description ?? "");
|
|
1070
|
-
if (paramNotes.length > 0)
|
|
1071
|
-
block("param notes", paramNotes.join(`
|
|
1072
|
-
`));
|
|
1073
|
-
if (rationale)
|
|
1074
|
-
block("rationale", rationale);
|
|
1075
|
-
if (currentPath && proposedPath) {
|
|
1076
|
-
w(` diff: diff ${shellArg(currentPath)} ${shellArg(proposedPath)}`);
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
if (hasSkill && skill) {
|
|
1081
|
-
const skillName = typeof skill.skill_name === "string" && skill.skill_name ? skill.skill_name : "moda-fix-skill";
|
|
1082
|
-
const skillPath = writeCandidateFile("SKILL.md", skill.skill_md ?? "", "fix_candidate_skill", `Drafted SKILL.md for '${skillName}' — install it (for example .claude/skills/${skillName}/SKILL.md), then \`moda fix mark-applied ${shellArg(fixId)}\`.`);
|
|
1083
|
-
findings.push({
|
|
1084
|
-
id: "fix:candidate_skill",
|
|
1085
|
-
title: `skill '${skillName}' drafted (SKILL.md, ${(skill.skill_md ?? "").length} chars)`,
|
|
1086
|
-
status: "proposed",
|
|
1087
|
-
severity: "info",
|
|
1088
|
-
description: `Install the skill in your agent, then confirm with \`moda fix mark-applied ${shellArg(fixId)}\`. Gating on the replay holdout stays pending until the skill is registered with the skill harness.`
|
|
1089
|
-
});
|
|
1090
|
-
summaryParts.push(`Includes a drafted SKILL.md candidate ('${skillName}').`);
|
|
1091
|
-
if (human) {
|
|
1092
|
-
w("");
|
|
1093
|
-
w(`Skill candidate — '${skillName}'`);
|
|
1094
|
-
if (skillPath)
|
|
1095
|
-
w(` SKILL.md written to ${skillPath}`);
|
|
1096
|
-
w(` install it (for example .claude/skills/${skillName}/SKILL.md), then confirm:`);
|
|
1097
|
-
w(` moda fix mark-applied ${shellArg(fixId)} --note=${shellArg(`installed skill ${skillName}`)}`);
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
if (hasSkillEdit && skillEdit) {
|
|
1101
|
-
const skillKey = typeof skillEdit.skill_key === "string" && skillEdit.skill_key ? skillEdit.skill_key : typeof skillEdit.skill_name === "string" && skillEdit.skill_name ? skillEdit.skill_name : "moda-skill";
|
|
1102
|
-
const changeSummary = typeof skillEdit.change_summary === "string" ? skillEdit.change_summary : "";
|
|
1103
|
-
const currentPath = writeCandidateFile("SKILL.current.md", typeof skillEdit.current_skill_md === "string" ? skillEdit.current_skill_md : "", "fix_skill_edit_current", `Current SKILL.md of your synced skill '${skillKey}' — the diff base.`);
|
|
1104
|
-
const proposedPath = writeCandidateFile("SKILL.proposed.md", skillEdit.proposed_skill_md ?? "", "fix_skill_edit_proposed", `Proposed SKILL.md for '${skillKey}' (minimal edit) — apply it to the existing skill in your repo, then \`moda fix mark-applied ${shellArg(fixId)}\`.`);
|
|
1105
|
-
findings.push({
|
|
1106
|
-
id: "fix:candidate_skill_edit",
|
|
1107
|
-
title: `skill '${skillKey}' edit proposed${changeSummary ? `: ${changeSummary}` : " (minimal delta to the existing skill)"}`,
|
|
1108
|
-
status: "proposed",
|
|
1109
|
-
severity: "info",
|
|
1110
|
-
description: `This amends the EXISTING synced skill '${skillKey}' — a minimal delta, not a new skill. Apply the proposed SKILL.md in your repo, then confirm with \`moda fix mark-applied ${shellArg(fixId)}\`.`
|
|
1111
|
-
});
|
|
1112
|
-
summaryParts.push(`Includes a skill-edit candidate amending existing skill '${skillKey}'.`);
|
|
1113
|
-
if (human) {
|
|
1114
|
-
w("");
|
|
1115
|
-
w(`Skill-edit candidate — amends existing skill '${skillKey}'`);
|
|
1116
|
-
if (changeSummary)
|
|
1117
|
-
block("change summary", changeSummary);
|
|
1118
|
-
if (currentPath)
|
|
1119
|
-
w(` current SKILL.md written to ${currentPath}`);
|
|
1120
|
-
if (proposedPath)
|
|
1121
|
-
w(` proposed SKILL.md written to ${proposedPath}`);
|
|
1122
|
-
if (currentPath && proposedPath)
|
|
1123
|
-
w(` diff: diff ${shellArg(currentPath)} ${shellArg(proposedPath)}`);
|
|
1124
|
-
w(` apply the edit to the existing skill in your repo, then confirm:`);
|
|
1125
|
-
w(` moda fix mark-applied ${shellArg(fixId)} --note=${shellArg(`amended skill ${skillKey}`)}`);
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
nextCommands.push({
|
|
1129
|
-
command: `moda fix mark-applied ${shellArg(fixId)} --note="where/how you applied it"`,
|
|
1130
|
-
purpose: "Confirm you applied the candidate in your own stack (no PR) — flips the fix to SHIPPED and starts recurrence monitoring.",
|
|
1131
|
-
mutability: "write",
|
|
1132
|
-
requires_approval: true
|
|
1133
|
-
});
|
|
1134
|
-
return {
|
|
1135
|
-
findings,
|
|
1136
|
-
artifacts,
|
|
1137
|
-
warnings,
|
|
1138
|
-
nextCommands,
|
|
1139
|
-
summarySuffix: summaryParts.length > 0 ? ` ${summaryParts.join(" ")}` : ""
|
|
1140
|
-
};
|
|
1141
|
-
}
|
|
1142
|
-
function baselineFidelityDescriptor(gate) {
|
|
1143
|
-
const fidelity = gate?.baselineFidelity;
|
|
1144
|
-
const base = fidelity?.prompt === "tenant" ? "tenant prompt baseline" : fidelity?.prompt === "generic" ? "standard-agent baseline" : null;
|
|
1145
|
-
if (fidelity?.skills === "observed") {
|
|
1146
|
-
return `${base ?? "baseline"} · skill recovered from production receipts`;
|
|
1147
|
-
}
|
|
1148
|
-
return base;
|
|
1149
|
-
}
|
|
1150
|
-
function verifiedFidelityCopy(gate) {
|
|
1151
|
-
const descriptor = baselineFidelityDescriptor(gate);
|
|
1152
|
-
return descriptor ? `Verified — ${descriptor}` : null;
|
|
1153
|
-
}
|
|
1154
|
-
function counterfactualBlock(gate) {
|
|
1155
|
-
if (!gate || gate.pending === true)
|
|
1156
|
-
return null;
|
|
1157
|
-
const cf = gate.counterfactual;
|
|
1158
|
-
if (!cf || typeof cf !== "object" || typeof cf.verdict !== "string" || !cf.verdict)
|
|
1159
|
-
return null;
|
|
1160
|
-
return cf;
|
|
1161
|
-
}
|
|
1162
|
-
function counterfactualSentence(cf) {
|
|
1163
|
-
return `prevented ${cf.wins ?? 0} of ${cf.informative ?? 0} replayed production failures (control still failed in ${cf.informative ?? 0}; regressions ${cf.losses ?? 0})`;
|
|
1164
|
-
}
|
|
1165
|
-
function counterfactualHumanLine(cf) {
|
|
1166
|
-
return `Counterfactual: ${cf.verdict} — ${counterfactualSentence(cf)}; anchors ${cf.anchors ?? 0}, abstained ${cf.abstained ?? 0}`;
|
|
1167
|
-
}
|
|
1168
|
-
function fixVerifiedFidelityCopy(fix) {
|
|
1169
|
-
if (fix.status !== "VERIFIED")
|
|
1170
|
-
return null;
|
|
1171
|
-
return verifiedFidelityCopy(fix.gateResult ?? null);
|
|
1172
|
-
}
|
|
1173
|
-
function gateFindings(fix) {
|
|
1174
|
-
const gateResult = fix.gateResult ?? null;
|
|
1175
|
-
if (!gateResult || gateResult.pending === true)
|
|
1176
|
-
return [];
|
|
1177
|
-
const verdict = gateResult.verdict ?? fix.status;
|
|
1178
|
-
const fidelity = baselineFidelityDescriptor(gateResult);
|
|
1179
|
-
const cf = counterfactualBlock(gateResult);
|
|
1180
|
-
return [
|
|
1181
|
-
{
|
|
1182
|
-
id: "fix:gate",
|
|
1183
|
-
title: `gate ${verdict}: ${gateResult.reason ?? fix.statusReason ?? ""}`.trim(),
|
|
1184
|
-
status: verdict,
|
|
1185
|
-
severity: verdict === "pass" ? "info" : verdict === "fail" ? "error" : "warning",
|
|
1186
|
-
description: `${formatGateScoreboard(gateResult)}${fidelity ? `; baseline fidelity: ${fidelity}` : ""}` + `${cf ? `; counterfactual ${cf.verdict}: ${counterfactualSentence(cf)} (anchors ${cf.anchors ?? 0}, abstained ${cf.abstained ?? 0})` : ""}`
|
|
1187
|
-
}
|
|
1188
|
-
];
|
|
1189
|
-
}
|
|
1190
|
-
function formatGateScoreboard(gate) {
|
|
1191
|
-
const holdout = gate.holdout ?? {};
|
|
1192
|
-
const repair = gate.repair ?? {};
|
|
1193
|
-
const guard = gate.byScope?.regression_guard;
|
|
1194
|
-
return `${gate.byScope ? "in-problem holdout" : "holdout"}: candidate ${holdout.candidate ?? 0}/${holdout.n ?? 0} vs baseline ${holdout.baseline ?? 0}/${holdout.n ?? 0} ` + `(wins ${holdout.wins ?? 0}, losses ${holdout.losses ?? 0}); ` + (guard ? `regression guards: candidate ${guard.candidate ?? 0}/${guard.n ?? 0} vs baseline ${guard.baseline ?? 0}/${guard.n ?? 0} ` + `(losses ${guard.losses ?? 0}, noise floor ${guard.noiseFloor ?? 0}); ` : "") + `training cases (repair): candidate ${repair.candidate ?? 0}/${repair.n ?? 0} vs baseline ${repair.baseline ?? 0}/${repair.n ?? 0}; ` + `noise floor ${gate.noiseFloor ?? 0}; abstained ${gate.abstained ?? 0}`;
|
|
1195
|
-
}
|
|
1196
|
-
function printHumanGateVerdict(fix, gateRunId, gateResult, cases) {
|
|
1197
|
-
const w = (line) => process.stderr.write(`${line}
|
|
1198
|
-
`);
|
|
1199
|
-
w("");
|
|
1200
|
-
w("Fix gate verdict");
|
|
1201
|
-
w("----------------");
|
|
1202
|
-
w(`Fix: MODA-FIX-${fix.shortRef} (${fix.status})`);
|
|
1203
|
-
w(`Gate run: ${gateRunId}`);
|
|
1204
|
-
w(`Verdict: ${gateResult?.verdict ?? "none"}${gateResult?.reason ? ` — ${gateResult.reason}` : ""}`);
|
|
1205
|
-
const fidelity = baselineFidelityDescriptor(gateResult);
|
|
1206
|
-
if (fidelity) {
|
|
1207
|
-
w(`Baseline: ${fidelity}`);
|
|
1208
|
-
}
|
|
1209
|
-
if (gateResult) {
|
|
1210
|
-
w(`Score: ${formatGateScoreboard(gateResult)}`);
|
|
1211
|
-
}
|
|
1212
|
-
const cf = counterfactualBlock(gateResult);
|
|
1213
|
-
if (cf) {
|
|
1214
|
-
w(counterfactualHumanLine(cf));
|
|
1215
|
-
}
|
|
1216
|
-
if (cases.length > 0) {
|
|
1217
|
-
w("");
|
|
1218
|
-
w("Per-case (holdout is the verdict; repair rows are training cases):");
|
|
1219
|
-
for (const row of cases) {
|
|
1220
|
-
w(` ${row.caseId}: baseline=${passLabel(row.prodPass)} candidate=${passLabel(row.proposedPass)}`);
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
w("");
|
|
1224
|
-
}
|
|
1225
|
-
function passLabel(value) {
|
|
1226
|
-
if (value === null || value === undefined)
|
|
1227
|
-
return "abstained";
|
|
1228
|
-
return value ? "pass" : "fail";
|
|
1229
|
-
}
|
|
1230
|
-
function nextCommandsFor(fix) {
|
|
1231
|
-
const packetCommand = {
|
|
1232
|
-
command: `moda fix ${shellArg(fix.id)} --packet`,
|
|
1233
|
-
purpose: "Read the packet — the tool/skill candidate is written as local artifacts to apply in your own stack.",
|
|
1234
|
-
mutability: "write",
|
|
1235
|
-
requires_approval: false
|
|
1236
|
-
};
|
|
1237
|
-
const markAppliedCommand = {
|
|
1238
|
-
command: `moda fix mark-applied ${shellArg(fix.id)} --note="where/how you applied it"`,
|
|
1239
|
-
purpose: "Confirm you applied the candidate in your own stack (no PR) — flips the fix to SHIPPED and starts recurrence monitoring.",
|
|
1240
|
-
mutability: "write",
|
|
1241
|
-
requires_approval: true
|
|
1242
|
-
};
|
|
1243
|
-
const skillGatePending = fix.fixType === "SKILL" && (fix.candidateRef?.kind === "skill_edit" || fix.candidateRef?.kind === "skill" && !fix.candidateRef?.skillId);
|
|
1244
|
-
switch (fix.status) {
|
|
1245
|
-
case "VERIFIED":
|
|
1246
|
-
if (isArtifactFix(fix)) {
|
|
1247
|
-
return [packetCommand, markAppliedCommand];
|
|
1248
|
-
}
|
|
1249
|
-
return [
|
|
1250
|
-
{
|
|
1251
|
-
command: `moda fix submit ${shellArg(fix.id)} --pr`,
|
|
1252
|
-
purpose: "Open the draft fix PR (merge with the magic word to confirm).",
|
|
1253
|
-
mutability: "write",
|
|
1254
|
-
requires_approval: true
|
|
1255
|
-
},
|
|
1256
|
-
{
|
|
1257
|
-
command: `moda fix checkout ${shellArg(fix.id)}`,
|
|
1258
|
-
purpose: "Write the verified candidate locally instead of opening a PR.",
|
|
1259
|
-
mutability: "write",
|
|
1260
|
-
requires_approval: false
|
|
1261
|
-
}
|
|
1262
|
-
];
|
|
1263
|
-
case "PROPOSED":
|
|
1264
|
-
if (skillGatePending) {
|
|
1265
|
-
return [packetCommand, markAppliedCommand];
|
|
1266
|
-
}
|
|
1267
|
-
return [
|
|
1268
|
-
{
|
|
1269
|
-
command: `moda fix verify ${shellArg(fix.id)}`,
|
|
1270
|
-
purpose: "Gate the proposed candidate on the frozen holdout.",
|
|
1271
|
-
mutability: "write",
|
|
1272
|
-
requires_approval: false
|
|
1273
|
-
}
|
|
1274
|
-
];
|
|
1275
|
-
case "GATE_FAILED":
|
|
1276
|
-
if (isArtifactFix(fix)) {
|
|
1277
|
-
return [
|
|
1278
|
-
{
|
|
1279
|
-
command: `moda fix verify ${shellArg(fix.id)}`,
|
|
1280
|
-
purpose: "Re-gate the stored candidate on the frozen holdout.",
|
|
1281
|
-
mutability: "write",
|
|
1282
|
-
requires_approval: false
|
|
1283
|
-
},
|
|
1284
|
-
packetCommand
|
|
1285
|
-
];
|
|
1286
|
-
}
|
|
1287
|
-
return [
|
|
1288
|
-
{
|
|
1289
|
-
command: `moda fix checkout ${shellArg(fix.id)}`,
|
|
1290
|
-
purpose: "Check out the candidate, edit it, and re-gate (fail-to-pass loop).",
|
|
1291
|
-
mutability: "write",
|
|
1292
|
-
requires_approval: false
|
|
1293
|
-
},
|
|
1294
|
-
{
|
|
1295
|
-
command: `moda fix verify ${shellArg(fix.id)} --prompt-file=<path>`,
|
|
1296
|
-
purpose: "Re-gate an edited local candidate on the frozen holdout.",
|
|
1297
|
-
mutability: "write",
|
|
1298
|
-
requires_approval: false
|
|
1299
|
-
}
|
|
1300
|
-
];
|
|
1301
|
-
case "GATE_INCONCLUSIVE":
|
|
1302
|
-
return [
|
|
1303
|
-
{
|
|
1304
|
-
command: `moda fix verify ${shellArg(fix.id)}`,
|
|
1305
|
-
purpose: "Re-run the gate — the last run was inconclusive (never a pass).",
|
|
1306
|
-
mutability: "write",
|
|
1307
|
-
requires_approval: false
|
|
1308
|
-
}
|
|
1309
|
-
];
|
|
1310
|
-
case "UNROUTED":
|
|
1311
|
-
return [
|
|
1312
|
-
{
|
|
1313
|
-
command: `moda fix ${shellArg(fix.id)} --packet`,
|
|
1314
|
-
purpose: "Read the investigation packet — this fix is deliverable as diagnosis.",
|
|
1315
|
-
mutability: "read",
|
|
1316
|
-
requires_approval: false
|
|
1317
|
-
},
|
|
1318
|
-
{
|
|
1319
|
-
command: "moda prompts status",
|
|
1320
|
-
purpose: "If the reason says evidence carries no prompt version: check the prompt lockfile, then pass prompt: lock.prompts['<key>'] to createThinkTelemetry / createAI7Telemetry (docs.moda.dev/prompt-management/attribution).",
|
|
1321
|
-
mutability: "read",
|
|
1322
|
-
requires_approval: false
|
|
1323
|
-
}
|
|
1324
|
-
];
|
|
1325
|
-
case "NO_EVIDENCE":
|
|
1326
|
-
case "BLOCKED_COVERAGE":
|
|
1327
|
-
case "NO_HEADROOM":
|
|
1328
|
-
return [
|
|
1329
|
-
{
|
|
1330
|
-
command: `moda fix ${shellArg(fix.id)} --packet`,
|
|
1331
|
-
purpose: "Read the investigation packet — this fix is deliverable as diagnosis.",
|
|
1332
|
-
mutability: "read",
|
|
1333
|
-
requires_approval: false
|
|
1334
|
-
}
|
|
1335
|
-
];
|
|
1336
|
-
case "DRAFT":
|
|
1337
|
-
case "SCOPING":
|
|
1338
|
-
case "PROPOSING":
|
|
1339
|
-
case "GATING":
|
|
1340
|
-
return [
|
|
1341
|
-
{
|
|
1342
|
-
command: `moda fix ${shellArg(fix.id)} --wait`,
|
|
1343
|
-
purpose: "Drive the advance-on-poll pipeline to its next resting status.",
|
|
1344
|
-
mutability: "write",
|
|
1345
|
-
requires_approval: false
|
|
1346
|
-
}
|
|
1347
|
-
];
|
|
1348
|
-
default:
|
|
1349
|
-
return [
|
|
1350
|
-
{
|
|
1351
|
-
command: `moda fix ${shellArg(fix.id)}`,
|
|
1352
|
-
purpose: "Track the fix status.",
|
|
1353
|
-
mutability: "read",
|
|
1354
|
-
requires_approval: false
|
|
1355
|
-
}
|
|
1356
|
-
];
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
function assertRealpathContained(absPath, relPath) {
|
|
1360
|
-
const root = realpathSync(process.cwd());
|
|
1361
|
-
const within = (p) => p === root || p.startsWith(root + sep);
|
|
1362
|
-
const segments = absPath.slice(root.length).split(sep).filter(Boolean);
|
|
1363
|
-
let probe = root;
|
|
1364
|
-
for (const segment of segments) {
|
|
1365
|
-
probe = join(probe, segment);
|
|
1366
|
-
let stat;
|
|
1367
|
-
try {
|
|
1368
|
-
stat = lstatSync(probe);
|
|
1369
|
-
} catch {
|
|
1370
|
-
break;
|
|
1371
|
-
}
|
|
1372
|
-
if (stat.isSymbolicLink()) {
|
|
1373
|
-
throw new CliInputError(`Refusing to write through a symlink: ${relPath}`);
|
|
1374
|
-
}
|
|
1375
|
-
if (stat.isFile() && stat.nlink > 1) {
|
|
1376
|
-
throw new CliInputError(`Refusing to write through a hard link (${stat.nlink} names share this file): ${relPath} — replace it with an independent copy (cp then rm the link) and re-run.`);
|
|
1377
|
-
}
|
|
1378
|
-
if (!within(realpathSync(probe))) {
|
|
1379
|
-
throw new CliInputError(`Refusing to write outside the repo: ${relPath} resolves past the working tree`);
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
function writeContainedFile(absPath, relPath, content) {
|
|
1384
|
-
mkdirSync(dirname(absPath), { recursive: true });
|
|
1385
|
-
const existedBefore = existsSync(absPath);
|
|
1386
|
-
let fd;
|
|
1387
|
-
try {
|
|
1388
|
-
fd = openSync(absPath, fsConstants.O_RDWR | fsConstants.O_CREAT | O_NOFOLLOW, 420);
|
|
1389
|
-
} catch (error) {
|
|
1390
|
-
if (error.code === "ELOOP") {
|
|
1391
|
-
throw new CliInputError(`Refusing to write through a symlink: ${relPath}`);
|
|
1392
|
-
}
|
|
1393
|
-
throw error;
|
|
1394
|
-
}
|
|
1395
|
-
try {
|
|
1396
|
-
const stat = fstatSync(fd);
|
|
1397
|
-
if (!stat.isFile()) {
|
|
1398
|
-
throw new CliInputError(`Refusing to write ${relPath}: not a regular file`);
|
|
1399
|
-
}
|
|
1400
|
-
if (stat.nlink > 1) {
|
|
1401
|
-
throw new CliInputError(`Refusing to write through a hard link (${stat.nlink} names share this file): ${relPath} — replace it with an independent copy (cp then rm the link) and re-run.`);
|
|
1402
|
-
}
|
|
1403
|
-
if (readFileSync(fd, "utf8") === content) {
|
|
1404
|
-
return "unchanged";
|
|
1405
|
-
}
|
|
1406
|
-
ftruncateSync(fd, 0);
|
|
1407
|
-
writeSync(fd, content, 0, "utf8");
|
|
1408
|
-
return existedBefore ? "updated" : "wrote";
|
|
1409
|
-
} finally {
|
|
1410
|
-
closeSync(fd);
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
function safeTargetSourcePath(raw) {
|
|
1414
|
-
const trimmed = raw.trim();
|
|
1415
|
-
if (!trimmed || trimmed.includes("\x00")) {
|
|
1416
|
-
throw new CliInputError(`Refusing to write an invalid target source path: ${JSON.stringify(raw).slice(0, 120)}`);
|
|
1417
|
-
}
|
|
1418
|
-
if (isAbsolute(trimmed) || /^[A-Za-z]:[\\/]/.test(trimmed)) {
|
|
1419
|
-
throw new CliInputError(`Refusing to write outside the repo: target source path is absolute (${trimmed})`);
|
|
1420
|
-
}
|
|
1421
|
-
const segments = trimmed.split(/[\\/]+/).filter(Boolean);
|
|
1422
|
-
if (segments.length === 0 || segments.some((segment) => segment === "..")) {
|
|
1423
|
-
throw new CliInputError(`Refusing to write outside the repo: target source path contains '..' (${trimmed})`);
|
|
1424
|
-
}
|
|
1425
|
-
return segments.join("/");
|
|
1426
|
-
}
|
|
1427
|
-
function parsePositiveInt(raw, fallback, max) {
|
|
1428
|
-
const parsed = Number.parseInt(raw ?? "", 10);
|
|
1429
|
-
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
1430
|
-
return fallback;
|
|
1431
|
-
return Math.min(parsed, max);
|
|
1432
|
-
}
|
|
1433
|
-
function sleep(ms) {
|
|
1434
|
-
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
1435
|
-
}
|
|
1436
|
-
export {
|
|
1437
|
-
writeContainedFile,
|
|
1438
|
-
runFixesCommand,
|
|
1439
|
-
runFixCommand
|
|
1440
|
-
};
|