@mutmutco/cli 3.36.1 → 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.
Files changed (2) hide show
  1. package/dist/main.cjs +170 -200
  2. 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, { project: opts.project });
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, opts.project) };
4134
+ return { plan, applied: applyScratchGc(plan, repoRoot2, now) };
4162
4135
  }
4163
- function buildPrMergeScratchHousekeeping(repoRoot2, deps = {}, project2) {
4136
+ function buildPrMergeScratchHousekeeping(repoRoot2, deps = {}) {
4164
4137
  const runScratchGc = deps.runScratchGc ?? executeScratchGc;
4165
4138
  try {
4166
- const scratchApply = runScratchGc(repoRoot2, { apply: true, project: project2 });
4167
- const scratchAfter = runScratchGc(repoRoot2, { apply: false, project: project2 });
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
- if (snap.syncQueueSlugs === null) {
4232
- for (const f of snap.planMdFiles) {
4233
- if (now - f.mtimeMs <= PLAN_ADVISORY_AGE_MS) continue;
4234
- candidates.push({
4235
- path: f.path,
4236
- family: "plan",
4237
- kind: "file",
4238
- tier: "advisory",
4239
- reason: "North Star sync queue unreadable; kept",
4240
- bytes: f.bytes
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(), project2) {
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
- if (c.family === "plan") {
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
- const raw = readFile6(full, "utf8");
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
- let planMeta = {};
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 S3-synced
9444
- // via `mmi-cli northstar push` / auto-save on write; never git-tracked (AGENTS.md "Repo cleanliness", #1550, #1842).
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 ceilingLines = [...body.matchAll(/^\s*max-seconds:\s*(.+?)\s*$/gm)].map((m) => m[1].replace(/\s+#.*$/, ""));
9559
- if (ceilingLines.length === 0) {
9560
- failures.push(`${path2}: run-with-budget has no max-seconds input \u2014 the budget step guards nothing`);
9561
- continue;
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 raw of ceilingLines) {
9564
- const literal = /^['"]?(\d+)['"]?$/.exec(raw);
9565
- const seconds = literal ? Number(literal[1]) : NaN;
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
  }
@@ -17453,8 +17423,8 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
17453
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.`);
17454
17424
  }
17455
17425
  const target = header.target;
17456
- if (target !== void 0 && !/^[A-Za-z0-9_-]{1,140}$/.test(target)) {
17457
- throw new Error(`schedules lift: ${expectedId} header \`target: ${target}\` is not a bare lambda function name (allowed: A-Z a-z 0-9 _ -; max 140).`);
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.`);
17458
17428
  }
17459
17429
  return {
17460
17430
  id: expectedId,
@@ -23068,7 +23038,7 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
23068
23038
  process.exit(process.exitCode ?? 0);
23069
23039
  });
23070
23040
  });
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 and plans whose current hash is confirmed synced to North Star (#1864) instead of git branches/refs").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to inspect per state", "200").action(async (o) => {
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) => {
23072
23042
  if (o.apply && o.dryRun) return fail("worktree gc: choose either --dry-run or --apply");
23073
23043
  if (o.scratch) {
23074
23044
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.36.1",
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",