@mutmutco/cli 3.36.0 → 3.37.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 +171 -200
- 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
|
+
}
|
|
9516
|
+
}
|
|
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
|
+
}
|
|
9562
9535
|
}
|
|
9563
|
-
for (const
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
if (!literal || seconds < 1) {
|
|
9567
|
-
failures.push(`${path2}: max-seconds '${raw}' is not a literal positive integer`);
|
|
9568
|
-
} else if (seconds > MAX_SECONDS_CEILING) {
|
|
9569
|
-
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`);
|
|
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
|
}
|
|
@@ -17150,6 +17120,7 @@ function strayCronDrifts(workflows) {
|
|
|
17150
17120
|
function unlauncheredLlmDrift(entry) {
|
|
17151
17121
|
if (entry.llm !== "yes") return null;
|
|
17152
17122
|
if (isHarbourLlmLauncher(entry.executor)) return null;
|
|
17123
|
+
if (entry.executor.startsWith("aws-scheduler") && entry.executor.includes("mmi-hub-schedule-dispatcher")) return null;
|
|
17153
17124
|
const allowed = [...HARBOUR_LLM_LAUNCHERS].join(", ");
|
|
17154
17125
|
return {
|
|
17155
17126
|
class: "unlaunchered-llm",
|
|
@@ -17452,8 +17423,8 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
|
|
|
17452
17423
|
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.`);
|
|
17453
17424
|
}
|
|
17454
17425
|
const target = header.target;
|
|
17455
|
-
if (target !== void 0 && !/^[A-Za-z0-9_-]{1,
|
|
17456
|
-
throw new Error(`schedules lift: ${expectedId} header \`target: ${target}\`
|
|
17426
|
+
if (target !== void 0 && !/^mmi-fleet-[A-Za-z0-9_-]{1,130}$/.test(target)) {
|
|
17427
|
+
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.`);
|
|
17457
17428
|
}
|
|
17458
17429
|
return {
|
|
17459
17430
|
id: expectedId,
|
|
@@ -23067,7 +23038,7 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
|
|
|
23067
23038
|
process.exit(process.exitCode ?? 0);
|
|
23068
23039
|
});
|
|
23069
23040
|
});
|
|
23070
|
-
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
|
|
23041
|
+
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) => {
|
|
23071
23042
|
if (o.apply && o.dryRun) return fail("worktree gc: choose either --dry-run or --apply");
|
|
23072
23043
|
if (o.scratch) {
|
|
23073
23044
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.37.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",
|