@mutmutco/cli 3.36.1 → 3.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.cjs +233 -212
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -4107,33 +4107,6 @@ var import_node_path6 = require("node:path");
|
|
|
4107
4107
|
var import_node_child_process4 = require("node:child_process");
|
|
4108
4108
|
var import_node_fs6 = require("node:fs");
|
|
4109
4109
|
var import_node_path4 = require("node:path");
|
|
4110
|
-
|
|
4111
|
-
// src/plan-scratch.ts
|
|
4112
|
-
var metaKey = (project2, slug) => `${project2}/${slug}`;
|
|
4113
|
-
function parseMeta(raw) {
|
|
4114
|
-
if (!raw) return {};
|
|
4115
|
-
try {
|
|
4116
|
-
const o = JSON.parse(raw);
|
|
4117
|
-
return o && typeof o === "object" ? o : {};
|
|
4118
|
-
} catch {
|
|
4119
|
-
return {};
|
|
4120
|
-
}
|
|
4121
|
-
}
|
|
4122
|
-
function hashContent(s) {
|
|
4123
|
-
let h = 2166136261;
|
|
4124
|
-
for (let i = 0; i < s.length; i++) {
|
|
4125
|
-
h ^= s.charCodeAt(i);
|
|
4126
|
-
h = Math.imul(h, 16777619);
|
|
4127
|
-
}
|
|
4128
|
-
return (h >>> 0).toString(16);
|
|
4129
|
-
}
|
|
4130
|
-
|
|
4131
|
-
// src/rules-sync.ts
|
|
4132
|
-
function normalizeEol(s) {
|
|
4133
|
-
return s.replace(/\r\n/g, "\n");
|
|
4134
|
-
}
|
|
4135
|
-
|
|
4136
|
-
// src/scratch-gc.ts
|
|
4137
4110
|
var PLAN_ADVISORY_AGE_MS = 30 * 24 * 36e5;
|
|
4138
4111
|
var ROOT_SCRATCH_STALE_MS = 24 * 36e5;
|
|
4139
4112
|
var SCRATCH_GC_THROTTLE_MS = 24 * 36e5;
|
|
@@ -4155,16 +4128,16 @@ function markScratchGcRun(stampPath, now = Date.now()) {
|
|
|
4155
4128
|
}
|
|
4156
4129
|
}
|
|
4157
4130
|
function executeScratchGc(repoRoot2, opts, now = Date.now()) {
|
|
4158
|
-
const snap = collectScratchSnapshot(repoRoot2
|
|
4131
|
+
const snap = collectScratchSnapshot(repoRoot2);
|
|
4159
4132
|
const plan = planScratchGc(snap, now);
|
|
4160
4133
|
if (!opts.apply) return { plan };
|
|
4161
|
-
return { plan, applied: applyScratchGc(plan, repoRoot2, now
|
|
4134
|
+
return { plan, applied: applyScratchGc(plan, repoRoot2, now) };
|
|
4162
4135
|
}
|
|
4163
|
-
function buildPrMergeScratchHousekeeping(repoRoot2, deps = {}
|
|
4136
|
+
function buildPrMergeScratchHousekeeping(repoRoot2, deps = {}) {
|
|
4164
4137
|
const runScratchGc = deps.runScratchGc ?? executeScratchGc;
|
|
4165
4138
|
try {
|
|
4166
|
-
const scratchApply = runScratchGc(repoRoot2, { apply: true
|
|
4167
|
-
const scratchAfter = runScratchGc(repoRoot2, { apply: false
|
|
4139
|
+
const scratchApply = runScratchGc(repoRoot2, { apply: true });
|
|
4140
|
+
const scratchAfter = runScratchGc(repoRoot2, { apply: false });
|
|
4168
4141
|
const applied = scratchApply.applied;
|
|
4169
4142
|
const pruned = applied?.pruned.length ?? 0;
|
|
4170
4143
|
const skipped = applied?.skipped ?? 0;
|
|
@@ -4228,58 +4201,16 @@ function planScratchGc(snap, now = Date.now()) {
|
|
|
4228
4201
|
});
|
|
4229
4202
|
}
|
|
4230
4203
|
}
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
});
|
|
4242
|
-
}
|
|
4243
|
-
} else {
|
|
4244
|
-
for (const f of snap.planMdFiles) {
|
|
4245
|
-
if (now - f.mtimeMs <= PLAN_ADVISORY_AGE_MS) continue;
|
|
4246
|
-
const slug = f.name.replace(/\.md$/, "");
|
|
4247
|
-
if (snap.syncQueueSlugs.has(slug)) {
|
|
4248
|
-
candidates.push({
|
|
4249
|
-
path: f.path,
|
|
4250
|
-
family: "plan",
|
|
4251
|
-
kind: "file",
|
|
4252
|
-
tier: "advisory",
|
|
4253
|
-
reason: "pending North Star sync; kept",
|
|
4254
|
-
bytes: f.bytes
|
|
4255
|
-
});
|
|
4256
|
-
continue;
|
|
4257
|
-
}
|
|
4258
|
-
const hash = f.hash;
|
|
4259
|
-
const meta = snap.planMeta;
|
|
4260
|
-
const entry = hash && meta ? syncedPlanMetaEntry(meta, snap.project, slug, hash) : void 0;
|
|
4261
|
-
if (entry) {
|
|
4262
|
-
candidates.push({
|
|
4263
|
-
path: f.path,
|
|
4264
|
-
family: "plan",
|
|
4265
|
-
kind: "file",
|
|
4266
|
-
tier: "safe-auto",
|
|
4267
|
-
reason: "North Star synced hash confirmed",
|
|
4268
|
-
bytes: f.bytes,
|
|
4269
|
-
hash
|
|
4270
|
-
});
|
|
4271
|
-
} else {
|
|
4272
|
-
candidates.push({
|
|
4273
|
-
path: f.path,
|
|
4274
|
-
family: "plan",
|
|
4275
|
-
kind: "file",
|
|
4276
|
-
tier: "advisory",
|
|
4277
|
-
reason: "local plan content is not confirmed synced; kept",
|
|
4278
|
-
bytes: f.bytes,
|
|
4279
|
-
...hash ? { hash } : {}
|
|
4280
|
-
});
|
|
4281
|
-
}
|
|
4282
|
-
}
|
|
4204
|
+
for (const f of snap.planMdFiles) {
|
|
4205
|
+
if (now - f.mtimeMs <= PLAN_ADVISORY_AGE_MS) continue;
|
|
4206
|
+
candidates.push({
|
|
4207
|
+
path: f.path,
|
|
4208
|
+
family: "plan",
|
|
4209
|
+
kind: "file",
|
|
4210
|
+
tier: "advisory",
|
|
4211
|
+
reason: "plan older than the advisory floor; review and prune manually",
|
|
4212
|
+
bytes: f.bytes
|
|
4213
|
+
});
|
|
4283
4214
|
}
|
|
4284
4215
|
return {
|
|
4285
4216
|
candidates,
|
|
@@ -4287,47 +4218,6 @@ function planScratchGc(snap, now = Date.now()) {
|
|
|
4287
4218
|
advisory: candidates.filter((c) => c.tier === "advisory")
|
|
4288
4219
|
};
|
|
4289
4220
|
}
|
|
4290
|
-
function syncedPlanMetaEntry(meta, project2, slug, hash) {
|
|
4291
|
-
if (!project2) return false;
|
|
4292
|
-
const entry = meta[metaKey(project2, slug)];
|
|
4293
|
-
return Boolean(entry?.hash === hash && entry.syncedAt);
|
|
4294
|
-
}
|
|
4295
|
-
function readPlanMeta(plansRoot) {
|
|
4296
|
-
try {
|
|
4297
|
-
return parseMeta((0, import_node_fs6.readFileSync)((0, import_node_path4.join)(plansRoot, ".plan-meta.json"), "utf8"));
|
|
4298
|
-
} catch {
|
|
4299
|
-
return null;
|
|
4300
|
-
}
|
|
4301
|
-
}
|
|
4302
|
-
function readSyncQueueSlugs(plansRoot) {
|
|
4303
|
-
let queueRaw;
|
|
4304
|
-
try {
|
|
4305
|
-
queueRaw = (0, import_node_fs6.readFileSync)((0, import_node_path4.join)(plansRoot, ".sync-queue.json"), "utf8");
|
|
4306
|
-
} catch (e) {
|
|
4307
|
-
const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
|
|
4308
|
-
return code === "ENOENT" || code === "ENOTDIR" ? /* @__PURE__ */ new Set() : null;
|
|
4309
|
-
}
|
|
4310
|
-
try {
|
|
4311
|
-
const parsed = JSON.parse(queueRaw);
|
|
4312
|
-
const entries = Array.isArray(parsed) ? parsed : parsed.entries ?? [];
|
|
4313
|
-
return new Set(entries.map((e) => e.slug).filter((s) => typeof s === "string"));
|
|
4314
|
-
} catch {
|
|
4315
|
-
return null;
|
|
4316
|
-
}
|
|
4317
|
-
}
|
|
4318
|
-
function physicalPlanCandidateStillAllowed(candidatePath, repoAnchor, lstat = import_node_fs6.lstatSync) {
|
|
4319
|
-
const path2 = candidatePath.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
4320
|
-
const parent = (0, import_node_path4.dirname)(path2).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
4321
|
-
const name = (0, import_node_path4.basename)(path2);
|
|
4322
|
-
const plansDir = `${repoAnchor}/plans`;
|
|
4323
|
-
if (parent !== plansDir || !name.endsWith(".md")) return false;
|
|
4324
|
-
try {
|
|
4325
|
-
const st = lstat(plansDir);
|
|
4326
|
-
return st.isDirectory() && !st.isSymbolicLink();
|
|
4327
|
-
} catch {
|
|
4328
|
-
return false;
|
|
4329
|
-
}
|
|
4330
|
-
}
|
|
4331
4221
|
function formatScratchGcPlan(plan, applied, applyResult) {
|
|
4332
4222
|
if (plan.candidates.length === 0) return "scratch GC: nothing to prune \u2014 local scratch is clean.";
|
|
4333
4223
|
const lines = [];
|
|
@@ -4373,7 +4263,7 @@ function treeOlderThan(root, now, floor) {
|
|
|
4373
4263
|
}
|
|
4374
4264
|
return true;
|
|
4375
4265
|
}
|
|
4376
|
-
function applyScratchGc(plan, repoRoot2, now = Date.now()
|
|
4266
|
+
function applyScratchGc(plan, repoRoot2, now = Date.now()) {
|
|
4377
4267
|
const result = { pruned: [], skipped: 0, bytes: 0 };
|
|
4378
4268
|
let repoAnchor;
|
|
4379
4269
|
try {
|
|
@@ -4402,30 +4292,7 @@ function applyScratchGc(plan, repoRoot2, now = Date.now(), project2) {
|
|
|
4402
4292
|
result.skipped += 1;
|
|
4403
4293
|
continue;
|
|
4404
4294
|
}
|
|
4405
|
-
|
|
4406
|
-
if (!c.hash || !physicalPlanCandidateStillAllowed(real, repoAnchor)) {
|
|
4407
|
-
result.skipped += 1;
|
|
4408
|
-
continue;
|
|
4409
|
-
}
|
|
4410
|
-
const plansRoot = `${repoAnchor}/plans`;
|
|
4411
|
-
const slug = (0, import_node_path4.basename)(c.path).replace(/\.md$/, "");
|
|
4412
|
-
const pending = readSyncQueueSlugs(plansRoot);
|
|
4413
|
-
if (pending === null || pending.has(slug)) {
|
|
4414
|
-
result.skipped += 1;
|
|
4415
|
-
continue;
|
|
4416
|
-
}
|
|
4417
|
-
const currentHash = hashContent(normalizeEol((0, import_node_fs6.readFileSync)(c.path, "utf8")));
|
|
4418
|
-
if (currentHash !== c.hash) {
|
|
4419
|
-
result.skipped += 1;
|
|
4420
|
-
continue;
|
|
4421
|
-
}
|
|
4422
|
-
const meta = readPlanMeta(plansRoot);
|
|
4423
|
-
if (!meta || !syncedPlanMetaEntry(meta, project2, slug, currentHash)) {
|
|
4424
|
-
result.skipped += 1;
|
|
4425
|
-
continue;
|
|
4426
|
-
}
|
|
4427
|
-
}
|
|
4428
|
-
const floor = c.family === "plan" ? PLAN_ADVISORY_AGE_MS : c.family === "scratch-dir" || c.family === "scratch-file" ? ROOT_SCRATCH_STALE_MS : 0;
|
|
4295
|
+
const floor = c.family === "scratch-dir" || c.family === "scratch-file" ? ROOT_SCRATCH_STALE_MS : 0;
|
|
4429
4296
|
if (floor > 0 && now - st.mtimeMs <= floor) {
|
|
4430
4297
|
result.skipped += 1;
|
|
4431
4298
|
continue;
|
|
@@ -4453,7 +4320,6 @@ function rootCandidateStillAllowed(c, realPath, repoAnchor) {
|
|
|
4453
4320
|
const name = (0, import_node_path4.basename)(path2);
|
|
4454
4321
|
if (c.family === "scratch-dir") return c.kind === "dir" && parent === repoAnchor && ROOT_SCRATCH_DIRS.has(name);
|
|
4455
4322
|
if (c.family === "scratch-file") return (c.kind ?? "file") === "file" && parent === repoAnchor && ROOT_SCRATCH_FILE_PREFIXES.some((prefix) => name.startsWith(prefix));
|
|
4456
|
-
if (c.family === "plan") return physicalPlanCandidateStillAllowed(path2, repoAnchor);
|
|
4457
4323
|
return false;
|
|
4458
4324
|
}
|
|
4459
4325
|
function trackedPathStatus(c, realPath, repoAnchor) {
|
|
@@ -4509,7 +4375,6 @@ function rootScratchDirSnapshot(root, readdir2, stat2) {
|
|
|
4509
4375
|
function collectScratchSnapshot(repoRoot2, deps = {}) {
|
|
4510
4376
|
const readdir2 = deps.readdir ?? import_node_fs6.readdirSync;
|
|
4511
4377
|
const stat2 = deps.stat ?? import_node_fs6.statSync;
|
|
4512
|
-
const readFile6 = deps.readFile ?? import_node_fs6.readFileSync;
|
|
4513
4378
|
const plansRoot = (0, import_node_path4.join)(repoRoot2, "plans");
|
|
4514
4379
|
const rootScratchFiles = [];
|
|
4515
4380
|
try {
|
|
@@ -4540,37 +4405,13 @@ function collectScratchSnapshot(repoRoot2, deps = {}) {
|
|
|
4540
4405
|
const full = (0, import_node_path4.join)(plansRoot, ent.name);
|
|
4541
4406
|
try {
|
|
4542
4407
|
const st = stat2(full);
|
|
4543
|
-
|
|
4544
|
-
planMdFiles.push({ path: full, dir: plansRoot, name: ent.name, mtimeMs: st.mtimeMs, bytes: st.size, kind: "file", hash: hashContent(normalizeEol(raw)) });
|
|
4408
|
+
planMdFiles.push({ path: full, dir: plansRoot, name: ent.name, mtimeMs: st.mtimeMs, bytes: st.size, kind: "file" });
|
|
4545
4409
|
} catch {
|
|
4546
4410
|
}
|
|
4547
4411
|
}
|
|
4548
4412
|
} catch {
|
|
4549
4413
|
}
|
|
4550
|
-
|
|
4551
|
-
try {
|
|
4552
|
-
planMeta = parseMeta(readFile6((0, import_node_path4.join)(plansRoot, ".plan-meta.json"), "utf8"));
|
|
4553
|
-
} catch {
|
|
4554
|
-
planMeta = {};
|
|
4555
|
-
}
|
|
4556
|
-
const project2 = deps.project?.trim() || void 0;
|
|
4557
|
-
const syncQueueSlugs = (() => {
|
|
4558
|
-
let queueRaw;
|
|
4559
|
-
try {
|
|
4560
|
-
queueRaw = readFile6((0, import_node_path4.join)(plansRoot, ".sync-queue.json"), "utf8");
|
|
4561
|
-
} catch (e) {
|
|
4562
|
-
const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
|
|
4563
|
-
return code === "ENOENT" || code === "ENOTDIR" ? /* @__PURE__ */ new Set() : null;
|
|
4564
|
-
}
|
|
4565
|
-
try {
|
|
4566
|
-
const parsed = JSON.parse(queueRaw);
|
|
4567
|
-
const entries = Array.isArray(parsed) ? parsed : parsed.entries ?? [];
|
|
4568
|
-
return new Set(entries.map((e) => e.slug).filter((s) => typeof s === "string"));
|
|
4569
|
-
} catch {
|
|
4570
|
-
return null;
|
|
4571
|
-
}
|
|
4572
|
-
})();
|
|
4573
|
-
return { repoRoot: repoRoot2, rootScratchFiles, planMdFiles, planMeta, project: project2, syncQueueSlugs };
|
|
4414
|
+
return { repoRoot: repoRoot2, rootScratchFiles, planMdFiles };
|
|
4574
4415
|
}
|
|
4575
4416
|
|
|
4576
4417
|
// src/repo-runtime-state.ts
|
|
@@ -9440,8 +9281,8 @@ var MANAGED_GITIGNORE_LINES = [
|
|
|
9440
9281
|
"/tmp/",
|
|
9441
9282
|
// Ad-hoc agent scratch at repo root (e.g. tmp_diff.txt) — same class as /tmp/ (#1676).
|
|
9442
9283
|
"/tmp_*",
|
|
9443
|
-
// Plan scratch at ANY depth (root plans/, cli/plans/, .cursor/plans/) — AI planning docs are
|
|
9444
|
-
//
|
|
9284
|
+
// Plan scratch at ANY depth (root plans/, cli/plans/, .cursor/plans/) — AI planning docs are managed by
|
|
9285
|
+
// the personal `jerv` CLI's plan store; never git-tracked (AGENTS.md "Repo cleanliness", #1550, #1842).
|
|
9445
9286
|
"**/plans/",
|
|
9446
9287
|
// Superpowers plan/spec output is scratch too; authoritative docs live directly under docs/.
|
|
9447
9288
|
"docs/superpowers/",
|
|
@@ -9526,6 +9367,117 @@ function isGateWorkflowPath(path2) {
|
|
|
9526
9367
|
const base = path2.split(/[/\\]/).pop() ?? path2;
|
|
9527
9368
|
return /^(?:.+[-_.])?gate\.ya?ml$/i.test(base);
|
|
9528
9369
|
}
|
|
9370
|
+
function unquoteInline(value) {
|
|
9371
|
+
const v = value.trim();
|
|
9372
|
+
if (v.length >= 2 && v.startsWith('"') && v.endsWith('"')) return v.slice(1, -1).replace(/\\(["\\])/g, "$1");
|
|
9373
|
+
if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) return v.slice(1, -1).replace(/''/g, "'");
|
|
9374
|
+
return v;
|
|
9375
|
+
}
|
|
9376
|
+
var BLOCK_SCALAR = /^[|>][+-]?\s*$/;
|
|
9377
|
+
function readScalar(lines, from, inlineValue, keyIndent) {
|
|
9378
|
+
if (!BLOCK_SCALAR.test(inlineValue)) return unquoteInline(inlineValue);
|
|
9379
|
+
const body = [];
|
|
9380
|
+
for (let k = from + 1; k < lines.length; k++) {
|
|
9381
|
+
if (lines[k].indent <= keyIndent) break;
|
|
9382
|
+
body.push(lines[k].text);
|
|
9383
|
+
}
|
|
9384
|
+
return body.join("\n");
|
|
9385
|
+
}
|
|
9386
|
+
function readWith(lines, from, inlineValue, withIndent) {
|
|
9387
|
+
if (inlineValue.startsWith("{")) {
|
|
9388
|
+
const cmd = inlineValue.match(/command:\s*("[^"]*"|'[^']*'|[^,}]+)/);
|
|
9389
|
+
const ms = inlineValue.match(/max-seconds:\s*("[^"]*"|'[^']*'|[^,}]+)/);
|
|
9390
|
+
return {
|
|
9391
|
+
command: cmd ? unquoteInline(cmd[1].trim()) : void 0,
|
|
9392
|
+
maxSeconds: ms ? unquoteInline(ms[1].trim()) : void 0
|
|
9393
|
+
};
|
|
9394
|
+
}
|
|
9395
|
+
const out = {};
|
|
9396
|
+
for (let j = from + 1; j < lines.length; j++) {
|
|
9397
|
+
if (lines[j].indent <= withIndent) break;
|
|
9398
|
+
const cm = lines[j].text.match(/^command:\s?(.*)$/);
|
|
9399
|
+
if (cm && out.command === void 0) {
|
|
9400
|
+
out.command = readScalar(lines, j, cm[1].trim(), lines[j].indent);
|
|
9401
|
+
continue;
|
|
9402
|
+
}
|
|
9403
|
+
const mm = lines[j].text.match(/^max-seconds:\s?(.*)$/);
|
|
9404
|
+
if (mm && out.maxSeconds === void 0) {
|
|
9405
|
+
out.maxSeconds = readScalar(lines, j, mm[1].trim(), lines[j].indent);
|
|
9406
|
+
continue;
|
|
9407
|
+
}
|
|
9408
|
+
}
|
|
9409
|
+
return out;
|
|
9410
|
+
}
|
|
9411
|
+
function parseStepLines(lines) {
|
|
9412
|
+
const step = {};
|
|
9413
|
+
const base = Math.min(...lines.map((l) => l.indent));
|
|
9414
|
+
for (let i = 0; i < lines.length; i++) {
|
|
9415
|
+
const { indent, text } = lines[i];
|
|
9416
|
+
if (indent !== base) continue;
|
|
9417
|
+
const m = text.match(/^([A-Za-z][\w-]*):\s?(.*)$/);
|
|
9418
|
+
if (!m) continue;
|
|
9419
|
+
const key = m[1];
|
|
9420
|
+
const val = m[2].trim();
|
|
9421
|
+
if (key === "uses") step.uses = unquoteInline(val);
|
|
9422
|
+
else if (key === "run") step.run = readScalar(lines, i, val, base);
|
|
9423
|
+
else if (key === "with") {
|
|
9424
|
+
const w = readWith(lines, i, val, base);
|
|
9425
|
+
step.withCommand = w.command;
|
|
9426
|
+
step.withMaxSeconds = w.maxSeconds;
|
|
9427
|
+
}
|
|
9428
|
+
}
|
|
9429
|
+
return step;
|
|
9430
|
+
}
|
|
9431
|
+
function parseGateSteps(body) {
|
|
9432
|
+
const lines = body.split(/\r?\n/);
|
|
9433
|
+
const steps = [];
|
|
9434
|
+
let stepsIndent = -1;
|
|
9435
|
+
let itemIndent = -1;
|
|
9436
|
+
let current = null;
|
|
9437
|
+
const flush2 = () => {
|
|
9438
|
+
if (current && current.length) steps.push(parseStepLines(current));
|
|
9439
|
+
current = null;
|
|
9440
|
+
};
|
|
9441
|
+
for (const raw of lines) {
|
|
9442
|
+
const trimmed = raw.trim();
|
|
9443
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
9444
|
+
const indent = raw.match(/^(\s*)/)[1].length;
|
|
9445
|
+
if (stepsIndent === -1) {
|
|
9446
|
+
if (/^steps:\s*$/.test(trimmed)) stepsIndent = indent;
|
|
9447
|
+
continue;
|
|
9448
|
+
}
|
|
9449
|
+
if (indent <= stepsIndent) {
|
|
9450
|
+
flush2();
|
|
9451
|
+
itemIndent = -1;
|
|
9452
|
+
stepsIndent = /^steps:\s*$/.test(trimmed) ? indent : -1;
|
|
9453
|
+
continue;
|
|
9454
|
+
}
|
|
9455
|
+
if (/^-\s?/.test(trimmed) && (itemIndent === -1 || indent === itemIndent)) {
|
|
9456
|
+
itemIndent = indent;
|
|
9457
|
+
flush2();
|
|
9458
|
+
current = [{ indent: indent + 2, text: trimmed.replace(/^-\s?/, "") }];
|
|
9459
|
+
continue;
|
|
9460
|
+
}
|
|
9461
|
+
if (current) current.push({ indent, text: trimmed });
|
|
9462
|
+
}
|
|
9463
|
+
flush2();
|
|
9464
|
+
return steps;
|
|
9465
|
+
}
|
|
9466
|
+
function isNoOpCommand(command) {
|
|
9467
|
+
const parts = command.split(/&&|\|\||;|\r?\n/).map((p) => p.trim()).filter(Boolean);
|
|
9468
|
+
if (parts.length === 0) return true;
|
|
9469
|
+
return parts.every((p) => /^(?::$|true$|false$|exit(?:\s|$)|echo(?:\s|$)|printf(?:\s|$)|#)/.test(p));
|
|
9470
|
+
}
|
|
9471
|
+
var TEST_RUNNER_AT_START = new RegExp(
|
|
9472
|
+
"^(?:(?:npm|yarn|pnpm|bun)\\s+(?:run\\s+)?(?:test|t)(?:[:\\s]|$)|(?:npx|bunx|pnpm\\s+dlx)\\s+(?:vitest|jest|mocha|playwright|cypress|ava|tap)\\b|(?:vitest|jest|mocha|pytest|py\\.test|phpunit|rspec)\\b|python[0-9.]*\\s+-m\\s+(?:pytest|unittest)\\b|(?:poetry|pipenv)\\s+run\\s+(?:pytest|py\\.test|python[0-9.]*\\s+-m\\s+(?:pytest|unittest))\\b|(?:bundle\\s+exec\\s+)?rspec\\b|(?:go|cargo|dotnet|mvn|gradle|swift)\\s+test\\b|(?:rails|php\\s+artisan)\\s+test\\b|playwright\\s+test\\b)",
|
|
9473
|
+
"i"
|
|
9474
|
+
);
|
|
9475
|
+
function runsTestRunner(command) {
|
|
9476
|
+
return command.split(/&&|\|\||;|\||\r?\n/).map((sub) => sub.trim().replace(/^[!(]+\s*/, "")).some((sub) => TEST_RUNNER_AT_START.test(sub));
|
|
9477
|
+
}
|
|
9478
|
+
function firstLine(command) {
|
|
9479
|
+
return command.split(/\r?\n/).map((l) => l.trim()).find(Boolean) ?? command.trim();
|
|
9480
|
+
}
|
|
9529
9481
|
function checkGateBudget(files, opts = {}) {
|
|
9530
9482
|
const failures = [];
|
|
9531
9483
|
const notes = [];
|
|
@@ -9555,18 +9507,35 @@ function checkGateBudget(files, opts = {}) {
|
|
|
9555
9507
|
notes.push(`${path2}: run-with-budget pinned to ${remote[1].slice(0, 12)} (blessed ${BLESSED_RUN_WITH_BUDGET_SHA.slice(0, 12)}) \u2014 valid but outdated`);
|
|
9556
9508
|
}
|
|
9557
9509
|
}
|
|
9558
|
-
const
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
9510
|
+
const steps = parseGateSteps(body);
|
|
9511
|
+
const budgetSteps = steps.filter((step) => step.uses?.includes("run-with-budget"));
|
|
9512
|
+
if (budgetSteps.length === 0) {
|
|
9513
|
+
if (![...body.matchAll(/^\s*max-seconds:\s*(.+?)\s*$/gm)].some((m) => /^['"]?\d+['"]?$/.test(m[1].replace(/\s+#.*$/, "").trim()))) {
|
|
9514
|
+
failures.push(`${path2}: run-with-budget has no max-seconds input \u2014 the budget step guards nothing`);
|
|
9515
|
+
}
|
|
9562
9516
|
}
|
|
9563
|
-
for (const
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
if (
|
|
9567
|
-
failures.push(`${path2}:
|
|
9568
|
-
}
|
|
9569
|
-
|
|
9517
|
+
for (const step of budgetSteps) {
|
|
9518
|
+
if (!step.withCommand) {
|
|
9519
|
+
failures.push(`${path2}: run-with-budget step has no with.command \u2014 the budget wraps nothing; put the gate's check command in it`);
|
|
9520
|
+
} else if (isNoOpCommand(step.withCommand)) {
|
|
9521
|
+
failures.push(`${path2}: run-with-budget step wraps a no-op command '${firstLine(step.withCommand)}' \u2014 a budgeted decoy leaves the real gate check unbounded (#3247)`);
|
|
9522
|
+
}
|
|
9523
|
+
const rawMs = step.withMaxSeconds?.replace(/\s+#.*$/, "").trim();
|
|
9524
|
+
if (!rawMs) {
|
|
9525
|
+
failures.push(`${path2}: run-with-budget step has no max-seconds input \u2014 the budget step guards nothing`);
|
|
9526
|
+
} else {
|
|
9527
|
+
const literal = /^['"]?(\d+)['"]?$/.exec(rawMs);
|
|
9528
|
+
const seconds = literal ? Number(literal[1]) : NaN;
|
|
9529
|
+
if (!literal || seconds < 1) {
|
|
9530
|
+
failures.push(`${path2}: max-seconds '${rawMs}' is not a literal positive integer`);
|
|
9531
|
+
} else if (seconds > MAX_SECONDS_CEILING) {
|
|
9532
|
+
failures.push(`${path2}: max-seconds ${seconds} exceeds the org ceiling ${MAX_SECONDS_CEILING} \u2014 a budget that generous guards nothing; measure the gate and set a real ceiling`);
|
|
9533
|
+
}
|
|
9534
|
+
}
|
|
9535
|
+
}
|
|
9536
|
+
for (const step of steps) {
|
|
9537
|
+
if (step.run && runsTestRunner(step.run)) {
|
|
9538
|
+
failures.push(`${path2}: the gate's check command runs in an unbudgeted 'run:' step ('${firstLine(step.run)}') \u2014 wrap it in the run-with-budget step so it has a wall-clock ceiling (#3247)`);
|
|
9570
9539
|
}
|
|
9571
9540
|
}
|
|
9572
9541
|
}
|
|
@@ -10490,6 +10459,13 @@ var import_node_fs14 = require("node:fs");
|
|
|
10490
10459
|
var import_node_path12 = require("node:path");
|
|
10491
10460
|
var import_node_net = require("node:net");
|
|
10492
10461
|
var import_node_util6 = require("node:util");
|
|
10462
|
+
|
|
10463
|
+
// src/rules-sync.ts
|
|
10464
|
+
function normalizeEol(s) {
|
|
10465
|
+
return s.replace(/\r\n/g, "\n");
|
|
10466
|
+
}
|
|
10467
|
+
|
|
10468
|
+
// src/stage-runner.ts
|
|
10493
10469
|
var execFileP4 = (0, import_node_util6.promisify)(import_node_child_process7.execFile);
|
|
10494
10470
|
var DOCKER_TIMEOUT_MS = 15e3;
|
|
10495
10471
|
var EARLY_EXIT_GRACE_MS = 2e3;
|
|
@@ -11100,10 +11076,6 @@ async function runStage(config = {}, opts = {}) {
|
|
|
11100
11076
|
}
|
|
11101
11077
|
|
|
11102
11078
|
// src/wave-status.ts
|
|
11103
|
-
function parseNorthStarSlugFromHead(headText) {
|
|
11104
|
-
const m = /northstar:`([^`]+)`/.exec(headText);
|
|
11105
|
-
return m?.[1];
|
|
11106
|
-
}
|
|
11107
11079
|
function parseNextFromHead(headText) {
|
|
11108
11080
|
const m = /\*\*NEXT:\*\*\s*(.+)/.exec(headText);
|
|
11109
11081
|
return m?.[1]?.trim();
|
|
@@ -11136,7 +11108,6 @@ function formatWaveStatus(rows) {
|
|
|
11136
11108
|
lines.push(` ${tags.join(" \xB7 ")}`);
|
|
11137
11109
|
if (row.pr) lines.push(` PR #${row.pr.number} (${row.pr.state})${row.pr.url ? ` ${row.pr.url}` : ""}`);
|
|
11138
11110
|
if (row.stage) lines.push(` stage: port ${row.stage.port}${row.stage.url ? ` ${row.stage.url}` : ""}`);
|
|
11139
|
-
if (row.northStarSlug) lines.push(` compass: northstar:${row.northStarSlug}`);
|
|
11140
11111
|
if (row.next) lines.push(` next: ${row.next}`);
|
|
11141
11112
|
}
|
|
11142
11113
|
return lines.join("\n");
|
|
@@ -11162,7 +11133,6 @@ async function collectWaveStatus(deps) {
|
|
|
11162
11133
|
primary: wt.path === primaryPath,
|
|
11163
11134
|
dirty,
|
|
11164
11135
|
stage: readStageSummary(wt.path),
|
|
11165
|
-
northStarSlug: headText ? parseNorthStarSlugFromHead(headText) : void 0,
|
|
11166
11136
|
next: headText ? parseNextFromHead(headText) : void 0
|
|
11167
11137
|
});
|
|
11168
11138
|
}
|
|
@@ -11266,6 +11236,17 @@ function buildIssueArgs({ type, title, body, priority, repo, labels }) {
|
|
|
11266
11236
|
for (const label of labels ?? []) args.push("--label", label);
|
|
11267
11237
|
return args;
|
|
11268
11238
|
}
|
|
11239
|
+
async function ensureLabelsExist(labels, repo, deps = {}) {
|
|
11240
|
+
const run = deps.run ?? execFileP2;
|
|
11241
|
+
for (const label of new Set(labels)) {
|
|
11242
|
+
const args = ["label", "create", label, "--color", "ededed"];
|
|
11243
|
+
if (repo) args.push("--repo", repo);
|
|
11244
|
+
try {
|
|
11245
|
+
await run("gh", args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
11246
|
+
} catch {
|
|
11247
|
+
}
|
|
11248
|
+
}
|
|
11249
|
+
}
|
|
11269
11250
|
async function bodyArgsViaFile(args, deps = {}) {
|
|
11270
11251
|
const i = args.indexOf("--body");
|
|
11271
11252
|
if (i === -1 || i + 1 >= args.length) return { args, cleanup: async () => {
|
|
@@ -11991,6 +11972,10 @@ function parseNpmVersion(stdout) {
|
|
|
11991
11972
|
function staleTrainCliMessage(report, commandName) {
|
|
11992
11973
|
return `running mmi-cli ${report.currentVersion} is stale against released ${report.releasedVersion}; this is recoverable \u2014 run \`npm install -g @mutmutco/cli@latest\` to update to ${report.releasedVersion} (a standalone CLI shadows a plugin-provided mmi-cli on PATH and needs no Claude restart), then rerun ${commandName} so it uses the current train path. This gate runs before any train mutation, so this invocation changed nothing; any partial train state (local merge, pushed tag) is from an earlier run and the rerun resumes it safely`;
|
|
11993
11974
|
}
|
|
11975
|
+
function versionAutoUpdateAction(report, releasedSource) {
|
|
11976
|
+
if (report.ok || report.staleAgainst !== "released") return "none";
|
|
11977
|
+
return releasedSource === "gh" ? "npm-unreachable" : "npm";
|
|
11978
|
+
}
|
|
11994
11979
|
|
|
11995
11980
|
// src/plugin-cache-prune.ts
|
|
11996
11981
|
var PLUGIN_CACHE_KEEP = 2;
|
|
@@ -17453,8 +17438,8 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
|
|
|
17453
17438
|
throw new Error(`schedules lift: ${expectedId} header \`llm: ${h.llm}\` does not classify (yes / no / embeddings) \u2014 an unreadable declaration would register a row whose LLM column lies.`);
|
|
17454
17439
|
}
|
|
17455
17440
|
const target = header.target;
|
|
17456
|
-
if (target !== void 0 && !/^[A-Za-z0-9_-]{1,
|
|
17457
|
-
throw new Error(`schedules lift: ${expectedId} header \`target: ${target}\`
|
|
17441
|
+
if (target !== void 0 && !/^mmi-fleet-[A-Za-z0-9_-]{1,130}$/.test(target)) {
|
|
17442
|
+
throw new Error(`schedules lift: ${expectedId} header \`target: ${target}\` must be a bare lambda function name in the reserved fleet namespace \`mmi-fleet-*\` (allowed after the prefix: A-Z a-z 0-9 _ -; total max 140). The dispatcher can only invoke mmi-fleet-* functions.`);
|
|
17458
17443
|
}
|
|
17459
17444
|
return {
|
|
17460
17445
|
id: expectedId,
|
|
@@ -20751,6 +20736,7 @@ function validateBatchSpecs(specs) {
|
|
|
20751
20736
|
return { ok: errors.length === 0, errors, validated };
|
|
20752
20737
|
}
|
|
20753
20738
|
async function createIssuesBatch(specs, options, deps = {}) {
|
|
20739
|
+
const ensureLabels = deps.ensureLabels ?? ensureLabelsExist;
|
|
20754
20740
|
const client = deps.client ?? defaultGitHubClient();
|
|
20755
20741
|
const validation = validateBatchSpecs(specs);
|
|
20756
20742
|
if (!validation.ok) {
|
|
@@ -20763,6 +20749,14 @@ ${lines}`);
|
|
|
20763
20749
|
throw new Error("could not resolve repo \u2014 pass --repo owner/repo or set repo per row");
|
|
20764
20750
|
}
|
|
20765
20751
|
const rowRepo = (spec) => spec.repo ?? defaultRepo;
|
|
20752
|
+
const labelsByRepo = /* @__PURE__ */ new Map();
|
|
20753
|
+
for (const { spec } of validation.validated) {
|
|
20754
|
+
if (!spec.labels?.length) continue;
|
|
20755
|
+
const bucket = labelsByRepo.get(rowRepo(spec)) ?? /* @__PURE__ */ new Set();
|
|
20756
|
+
for (const label of spec.labels) bucket.add(label);
|
|
20757
|
+
labelsByRepo.set(rowRepo(spec), bucket);
|
|
20758
|
+
}
|
|
20759
|
+
for (const [repo, labels] of labelsByRepo) await ensureLabels([...labels], repo);
|
|
20766
20760
|
const created = [];
|
|
20767
20761
|
const failures = [];
|
|
20768
20762
|
for (const entry of validation.validated) {
|
|
@@ -21170,6 +21164,15 @@ async function fetchNpmReleasedVersion() {
|
|
|
21170
21164
|
return void 0;
|
|
21171
21165
|
}
|
|
21172
21166
|
}
|
|
21167
|
+
var NPM_INSTALL_TIMEOUT_MS = 12e4;
|
|
21168
|
+
async function npmSelfUpdateCli() {
|
|
21169
|
+
try {
|
|
21170
|
+
await runHostBin("npm", ["install", "-g", "@mutmutco/cli@latest"], { timeout: NPM_INSTALL_TIMEOUT_MS });
|
|
21171
|
+
return { ok: true, detail: "npm install -g @mutmutco/cli@latest exited 0" };
|
|
21172
|
+
} catch (e) {
|
|
21173
|
+
return { ok: false, detail: e.message.trim().slice(0, 200).replace(/\s+/g, " ") };
|
|
21174
|
+
}
|
|
21175
|
+
}
|
|
21173
21176
|
function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRepo = false) {
|
|
21174
21177
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
21175
21178
|
const installed = readInstalledPlugins(surface);
|
|
@@ -22597,7 +22600,25 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
22597
22600
|
const plugin = checkClaudePlugin({ installed, released, guardState: deps.pluginGuardState(isOrgRepo) });
|
|
22598
22601
|
checks.push(plugin);
|
|
22599
22602
|
if (!plugin.ok) restartPending = true;
|
|
22600
|
-
|
|
22603
|
+
const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
|
|
22604
|
+
const cliReport = buildVersionLagReport(cliInput);
|
|
22605
|
+
if (apply && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm") {
|
|
22606
|
+
const heal = await deps.updateCli();
|
|
22607
|
+
const healEvidence = [`running: ${cliReport.currentVersion}`, `published: ${cliReport.releasedVersion}`, `heal: ${heal.detail}`];
|
|
22608
|
+
checks.push(heal.ok ? {
|
|
22609
|
+
ok: true,
|
|
22610
|
+
label: `mmi-cli ${cliReport.currentVersion} \u2192 ${cliReport.releasedVersion}`,
|
|
22611
|
+
detail: "self-updated via npm install -g; the next mmi-cli invocation runs the new version",
|
|
22612
|
+
verbose: healEvidence
|
|
22613
|
+
} : {
|
|
22614
|
+
ok: false,
|
|
22615
|
+
label: `mmi-cli ${cliReport.currentVersion} \u2192 ${cliReport.releasedVersion}`,
|
|
22616
|
+
fix: `self-update failed (${heal.detail}) \u2014 run \`npm install -g @mutmutco/cli@latest\``,
|
|
22617
|
+
verbose: healEvidence
|
|
22618
|
+
});
|
|
22619
|
+
} else {
|
|
22620
|
+
checks.push(checkCliVersion(cliInput));
|
|
22621
|
+
}
|
|
22601
22622
|
checks.push(checkPluginCache(deps.pluginCache()));
|
|
22602
22623
|
if (!opts.fast && !opts.banner && !opts.preflight) {
|
|
22603
22624
|
const probe = await deps.schedulesNotebook().catch((e) => ({
|
|
@@ -22803,6 +22824,8 @@ function mmiDoctorDeps() {
|
|
|
22803
22824
|
installedPluginVersion: installedClaudePluginVersion,
|
|
22804
22825
|
pluginGuardState: claudePluginGuardState,
|
|
22805
22826
|
releasedVersion: fetchNpmReleasedVersion,
|
|
22827
|
+
// #3272: the --apply self-heal for a stale running CLI — npm global, shadows any plugin shim (#2879).
|
|
22828
|
+
updateCli: npmSelfUpdateCli,
|
|
22806
22829
|
currentCliVersion: resolveClientVersion,
|
|
22807
22830
|
readGitignore,
|
|
22808
22831
|
writeGitignore,
|
|
@@ -23068,7 +23091,7 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
|
|
|
23068
23091
|
process.exit(process.exitCode ?? 0);
|
|
23069
23092
|
});
|
|
23070
23093
|
});
|
|
23071
|
-
gcCmd.option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR local+remote branches, linked worktrees, and stale tracking refs").option("--json", "machine-readable output").option("--scratch", "prune safe local scratch
|
|
23094
|
+
gcCmd.option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR local+remote branches, linked worktrees, and stale tracking refs").option("--json", "machine-readable output").option("--scratch", "prune safe local scratch (#1864) instead of git branches/refs; local plans are surfaced advisory-only, never auto-pruned").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to inspect per state", "200").action(async (o) => {
|
|
23072
23095
|
if (o.apply && o.dryRun) return fail("worktree gc: choose either --dry-run or --apply");
|
|
23073
23096
|
if (o.scratch) {
|
|
23074
23097
|
try {
|
|
@@ -23888,6 +23911,7 @@ withExamples(mutating(
|
|
|
23888
23911
|
let title;
|
|
23889
23912
|
let issueType;
|
|
23890
23913
|
let extraLabels = [];
|
|
23914
|
+
let targetRepo2;
|
|
23891
23915
|
try {
|
|
23892
23916
|
issueType = resolveCreateType(o.type, "issue create");
|
|
23893
23917
|
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
|
|
@@ -23895,33 +23919,30 @@ withExamples(mutating(
|
|
|
23895
23919
|
if (o.idempotencyKey) body = appendIdempotencyMarker(body, o.idempotencyKey);
|
|
23896
23920
|
priority = resolveCreatePriority(o.priority, "issue create");
|
|
23897
23921
|
extraLabels = [...o.label ?? []];
|
|
23922
|
+
targetRepo2 = await resolveRepo(o.repo);
|
|
23923
|
+
if (!targetRepo2) {
|
|
23924
|
+
return fail("issue create: could not resolve the target repo \u2014 run inside a git checkout or pass --repo <owner/repo>");
|
|
23925
|
+
}
|
|
23898
23926
|
args = buildIssueArgs({
|
|
23899
23927
|
type: issueType,
|
|
23900
23928
|
title,
|
|
23901
23929
|
body,
|
|
23902
23930
|
priority,
|
|
23903
|
-
repo:
|
|
23931
|
+
repo: targetRepo2,
|
|
23904
23932
|
labels: extraLabels.length ? extraLabels : void 0
|
|
23905
23933
|
});
|
|
23906
23934
|
if (o.parent !== void 0) parseIssueRef(o.parent);
|
|
23907
23935
|
} catch (e) {
|
|
23908
23936
|
return fail(`issue create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
|
|
23909
23937
|
}
|
|
23910
|
-
|
|
23911
|
-
const la = ["label", "create", label, "--color", "ededed"];
|
|
23912
|
-
if (o.repo) la.push("--repo", o.repo);
|
|
23913
|
-
try {
|
|
23914
|
-
await execFileP2("gh", la, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
23915
|
-
} catch {
|
|
23916
|
-
}
|
|
23917
|
-
}
|
|
23938
|
+
await ensureLabelsExist(extraLabels, targetRepo2);
|
|
23918
23939
|
const created = await ghCreate(args);
|
|
23919
|
-
const { projectItemId, onBoard } = await attachToProject(created.number,
|
|
23940
|
+
const { projectItemId, onBoard } = await attachToProject(created.number, targetRepo2, priority);
|
|
23920
23941
|
let parent;
|
|
23921
23942
|
let parentLinkError;
|
|
23922
23943
|
if (o.parent !== void 0) {
|
|
23923
23944
|
try {
|
|
23924
|
-
parent = await linkSubIssue(ghRunner2, o.parent, created.url,
|
|
23945
|
+
parent = await linkSubIssue(ghRunner2, o.parent, created.url, targetRepo2);
|
|
23925
23946
|
} catch (e) {
|
|
23926
23947
|
const err = e;
|
|
23927
23948
|
parentLinkError = (err.stderr || err.message || String(e)).trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.38.0",
|
|
4
4
|
"description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the plugin's session-start hook drives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|