@mutmutco/cli 2.26.0 → 2.28.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/README.md +1 -0
- package/dist/main.cjs +190 -13
- package/dist/saga.cjs +32 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,6 +44,7 @@ mmi-cli doctor --json
|
|
|
44
44
|
- `mmi-cli oauth plan|verify` prints a repo's canonical Google OAuth URI set when the registry declares an `oauth` block and verifies the client is port-agnostic.
|
|
45
45
|
- `mmi-cli issue create` creates typed, prioritized GitHub issues (priority sets the board field, not a label) and queues related-issue discovery. `--parent <ref>` files the new issue as a native GitHub sub-issue of a parent (works cross-repo); `mmi-cli issue link-child <parent> <child>` links two existing issues the same way.
|
|
46
46
|
- `mmi-cli report` files a friction report on the Hub board with your GitHub identity, deduping against the open `report`-labeled issues (a confident duplicate becomes a +1 comment, not a new issue). No repo-local `.env`, no API key, no copied report script.
|
|
47
|
+
- `mmi-cli skill-lesson --skill <name>` files a **skill-lesson** on the Hub board when a skill's own instructions misfire — the cross-skill generalization of grind's Retro. GitHub identity, its own `skill-lesson` label + dedup pool (a confident duplicate becomes a +1 comment), and a footer pinning the source checkout + plugin SHA. Advisory: the fix lands via a reviewed PR to the skill in MMI-Hub, never a live edit.
|
|
47
48
|
- `mmi-cli pr create` and `pr merge` create PRs and land them with branch/worktree cleanup; `mmi-cli gc` dry-runs cleanup of merged/closed PR branches + stale tracking refs.
|
|
48
49
|
- `mmi-cli board read|claim|show|move|done|backfill-priority` reads and moves GitHub Project work.
|
|
49
50
|
- `mmi-cli tenant control <owner/repo> <stage> <status|start|stop|restart>` runs bounded dev/rc box control for project-admins through the Hub API; main remains master-only.
|
package/dist/main.cjs
CHANGED
|
@@ -4235,6 +4235,7 @@ function parseHookInput(stdin) {
|
|
|
4235
4235
|
}
|
|
4236
4236
|
|
|
4237
4237
|
// src/saga-health.ts
|
|
4238
|
+
var MEMORY_STALE_DAYS = 14;
|
|
4238
4239
|
function buildHealth(i) {
|
|
4239
4240
|
const problems = [];
|
|
4240
4241
|
if (!i.sagaApiUrl) problems.push("Hub API URL not configured");
|
|
@@ -4245,6 +4246,10 @@ function buildHealth(i) {
|
|
|
4245
4246
|
}
|
|
4246
4247
|
if (i.reachable && i.authorized === false) problems.push("saga backend rejected authenticated state access");
|
|
4247
4248
|
if (!i.key.sessionId || i.key.sessionId === "-") problems.push("unsafe session id");
|
|
4249
|
+
const warnings = [];
|
|
4250
|
+
if (i.memoryAgeDays !== void 0 && i.memoryAgeDays > MEMORY_STALE_DAYS) {
|
|
4251
|
+
warnings.push(`PROJECT MEMORY is ${Math.round(i.memoryAgeDays)}d stale \u2014 the saga-keeper may have stalled`);
|
|
4252
|
+
}
|
|
4248
4253
|
const safeToWrite = problems.length === 0;
|
|
4249
4254
|
return {
|
|
4250
4255
|
ok: safeToWrite,
|
|
@@ -4258,14 +4263,19 @@ function buildHealth(i) {
|
|
|
4258
4263
|
pendingNotes: i.pendingNotes ?? 0,
|
|
4259
4264
|
key: i.key,
|
|
4260
4265
|
source: i.source,
|
|
4261
|
-
problems
|
|
4266
|
+
problems,
|
|
4267
|
+
warnings,
|
|
4268
|
+
memoryAgeDays: i.memoryAgeDays
|
|
4262
4269
|
};
|
|
4263
4270
|
}
|
|
4264
4271
|
function healthBanner(report) {
|
|
4265
|
-
if (report.
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4272
|
+
if (report.problems.length) {
|
|
4273
|
+
const summary = report.problems.slice(0, 2).join("; ") || "unknown saga health gap";
|
|
4274
|
+
const suffix = report.problems.length > 2 ? ` (+${report.problems.length - 2} more)` : "";
|
|
4275
|
+
return `saga health: CHECK - ${summary}${suffix}`;
|
|
4276
|
+
}
|
|
4277
|
+
if (report.warnings.length) return `saga health: NOTE - ${report.warnings.join("; ")}`;
|
|
4278
|
+
return null;
|
|
4269
4279
|
}
|
|
4270
4280
|
function resumeCue() {
|
|
4271
4281
|
return '> STATUS/RESUME CUE \u2014 For any status, resume, or "where do I stand" report: read THIS saga HEAD first (`mmi-cli saga show`), then reconcile its NEXT / LAST 5 / DECISIONS against the live board + git/gh before reporting. Do not rebuild the picture from board/issues/memory while skipping the HEAD. PRECEDENCE: the HEAD is prior-session belief and MAY BE SUPERSEDED \u2014 the current live user/master instruction WINS over any conflicting HEAD anchor, NEXT, or checklist; follow the live instruction and treat the stale HEAD item as superseded.';
|
|
@@ -4384,6 +4394,19 @@ async function probeSagaAccess(url, key) {
|
|
|
4384
4394
|
return false;
|
|
4385
4395
|
}
|
|
4386
4396
|
}
|
|
4397
|
+
async function fetchMemoryAge(url, project2) {
|
|
4398
|
+
try {
|
|
4399
|
+
const qs = new URLSearchParams({ project: project2 }).toString();
|
|
4400
|
+
const res = await fetch(`${url}/saga/memory-age?${qs}`, { headers: await hubHeaders(), signal: AbortSignal.timeout(8e3) });
|
|
4401
|
+
if (!res.ok) return void 0;
|
|
4402
|
+
const body = await res.json();
|
|
4403
|
+
if (!body.updatedAt) return void 0;
|
|
4404
|
+
const ms = Date.now() - Date.parse(body.updatedAt);
|
|
4405
|
+
return Number.isFinite(ms) && ms >= 0 ? ms / 864e5 : void 0;
|
|
4406
|
+
} catch {
|
|
4407
|
+
return void 0;
|
|
4408
|
+
}
|
|
4409
|
+
}
|
|
4387
4410
|
async function runSagaHealth(o, io = consoleIo) {
|
|
4388
4411
|
const cfg = await loadConfig();
|
|
4389
4412
|
const session = resolveSessionId();
|
|
@@ -4394,6 +4417,7 @@ async function runSagaHealth(o, io = consoleIo) {
|
|
|
4394
4417
|
cfg.sagaApiUrl ? probeBackend(cfg.sagaApiUrl) : Promise.resolve({ reachable: false })
|
|
4395
4418
|
]);
|
|
4396
4419
|
const authorized = cfg.sagaApiUrl && liveness.reachable ? await probeSagaAccess(cfg.sagaApiUrl, key) : void 0;
|
|
4420
|
+
const memoryAgeDays = cfg.sagaApiUrl && liveness.reachable ? await fetchMemoryAge(cfg.sagaApiUrl, key.project) : void 0;
|
|
4397
4421
|
const report = buildHealth({
|
|
4398
4422
|
key,
|
|
4399
4423
|
source,
|
|
@@ -4403,7 +4427,8 @@ async function runSagaHealth(o, io = consoleIo) {
|
|
|
4403
4427
|
livenessMessage: liveness.message,
|
|
4404
4428
|
authorized,
|
|
4405
4429
|
sagaApiUrl: cfg.sagaApiUrl,
|
|
4406
|
-
pendingNotes: readPending().length
|
|
4430
|
+
pendingNotes: readPending().length,
|
|
4431
|
+
memoryAgeDays
|
|
4407
4432
|
});
|
|
4408
4433
|
if (o.json) return io.log(JSON.stringify(report));
|
|
4409
4434
|
if (o.banner) {
|
|
@@ -4414,6 +4439,7 @@ async function runSagaHealth(o, io = consoleIo) {
|
|
|
4414
4439
|
if (o.quiet) return;
|
|
4415
4440
|
io.log(`saga health: ${report.ok ? "OK" : "NOT OK"}`);
|
|
4416
4441
|
if (report.problems.length) io.log(report.problems.map((p) => ` - ${p}`).join("\n"));
|
|
4442
|
+
if (report.warnings.length) io.log(report.warnings.map((w) => ` - ${w}`).join("\n"));
|
|
4417
4443
|
if (report.pendingNotes > 0) io.log(` - ${report.pendingNotes} note(s) queued locally \u2014 \`mmi-cli saga flush\` to roll forward`);
|
|
4418
4444
|
}
|
|
4419
4445
|
function registerSagaCommands(program3) {
|
|
@@ -5012,6 +5038,38 @@ function buildDupComment(duplicateNumber, body, sourceRepo) {
|
|
|
5012
5038
|
${buildReportBody(body, sourceRepo)}`;
|
|
5013
5039
|
}
|
|
5014
5040
|
|
|
5041
|
+
// src/skill-lesson.ts
|
|
5042
|
+
var SKILL_LESSON_LABEL = "skill-lesson";
|
|
5043
|
+
var SKILL_NAMES = ["bootstrap", "grind", "hotfix", "mmi", "rcand", "release", "secrets", "stage"];
|
|
5044
|
+
function assertSkillName(name) {
|
|
5045
|
+
const match = SKILL_NAMES.find((skill) => skill === name);
|
|
5046
|
+
if (!match) throw new Error(`unknown skill "${name}" \u2014 expected one of: ${SKILL_NAMES.join(", ")}`);
|
|
5047
|
+
return match;
|
|
5048
|
+
}
|
|
5049
|
+
function buildSkillLessonTitle(skill, what) {
|
|
5050
|
+
return `skill-lesson(${skill}): ${what.trim()}`;
|
|
5051
|
+
}
|
|
5052
|
+
function buildSkillLessonBody(body, sourceRepo, pluginSha) {
|
|
5053
|
+
const where = sourceRepo ?? "an unidentified checkout";
|
|
5054
|
+
const at = pluginSha ? ` @ ${pluginSha}` : "";
|
|
5055
|
+
return `${body.trimEnd()}
|
|
5056
|
+
|
|
5057
|
+
---
|
|
5058
|
+
Filed via \`mmi-cli skill-lesson\` from ${where}${at}.`;
|
|
5059
|
+
}
|
|
5060
|
+
function skillLessonDupMarker(duplicateNumber) {
|
|
5061
|
+
return `<!-- mmi-skill-lesson-dup:${duplicateNumber} -->`;
|
|
5062
|
+
}
|
|
5063
|
+
function buildSkillLessonDupComment(duplicateNumber, body, sourceRepo, pluginSha) {
|
|
5064
|
+
return `${skillLessonDupMarker(duplicateNumber)}
|
|
5065
|
+
+1 \u2014 same lesson surfaced again:
|
|
5066
|
+
|
|
5067
|
+
${buildSkillLessonBody(body, sourceRepo, pluginSha)}`;
|
|
5068
|
+
}
|
|
5069
|
+
function findDuplicateLesson(source, openLessons) {
|
|
5070
|
+
return findDuplicateReport(source, openLessons);
|
|
5071
|
+
}
|
|
5072
|
+
|
|
5015
5073
|
// src/board.ts
|
|
5016
5074
|
var import_node_child_process5 = require("node:child_process");
|
|
5017
5075
|
var import_node_util5 = require("node:util");
|
|
@@ -6685,6 +6743,28 @@ function buildMmiPluginCacheCleanupCheck(input) {
|
|
|
6685
6743
|
}))
|
|
6686
6744
|
};
|
|
6687
6745
|
}
|
|
6746
|
+
var NESTED_PLUGIN_TREE_LABEL = "self-nested MMI plugin cache tree (#1126)";
|
|
6747
|
+
var NESTED_PLUGIN_TREE_FIX = "run `mmi-cli doctor` to surface the MAX_PATH-safe robocopy cleanup for a self-nested MMI plugin cache tree (#1126)";
|
|
6748
|
+
function nestedPluginTreeCleanupCommand(paths, isWindows) {
|
|
6749
|
+
if (paths.length === 0) return "";
|
|
6750
|
+
if (isWindows) {
|
|
6751
|
+
const clears = paths.map((p) => `robocopy $e "${p}" /MIR /NJH /NJS /NFL /NDL | Out-Null; Remove-Item -LiteralPath "${p}" -Recurse -Force`).join("; ");
|
|
6752
|
+
return `$e=Join-Path $env:TEMP 'mmi-empty'; New-Item -ItemType Directory -Force $e | Out-Null; ${clears}; Remove-Item -LiteralPath $e -Recurse -Force; ${CLAUDE_PLUGIN_RECOVERY}`;
|
|
6753
|
+
}
|
|
6754
|
+
return `${paths.map((p) => `rm -rf "${p}"`).join(" && ")} && ${CLAUDE_PLUGIN_RECOVERY}`;
|
|
6755
|
+
}
|
|
6756
|
+
function buildNestedPluginTreeCheck(input) {
|
|
6757
|
+
const base = { ok: true, label: NESTED_PLUGIN_TREE_LABEL, fix: NESTED_PLUGIN_TREE_FIX };
|
|
6758
|
+
if (!input.isOrgRepo) return base;
|
|
6759
|
+
const nested = input.entries.filter((e) => e.nested).map((e) => ({ surface: e.surface, path: e.path }));
|
|
6760
|
+
if (nested.length === 0) return base;
|
|
6761
|
+
return {
|
|
6762
|
+
...base,
|
|
6763
|
+
ok: false,
|
|
6764
|
+
nested,
|
|
6765
|
+
fix: `${nested.length} self-nested MMI plugin cache tree(s) (#1126) exceed MAX_PATH and can't self-clean \u2014 run: ${nestedPluginTreeCleanupCommand(nested.map((n) => n.path), input.isWindows)}`
|
|
6766
|
+
};
|
|
6767
|
+
}
|
|
6688
6768
|
function detectSurface(env) {
|
|
6689
6769
|
const has = (k) => Boolean(env[k]?.trim());
|
|
6690
6770
|
if (env.MMI_AGENT_SURFACE === "codex" || has("CODEX_HOME") || (env.CLAUDE_PLUGIN_ROOT ?? "").includes(".codex")) {
|
|
@@ -6713,8 +6793,18 @@ function reloadAction(surface) {
|
|
|
6713
6793
|
return "restart Claude Code (or run /reload-plugins)";
|
|
6714
6794
|
}
|
|
6715
6795
|
}
|
|
6796
|
+
var CLAUDE_PLUGIN_RECOVERY = "claude plugin marketplace remove mmi && claude plugin marketplace add mutmutco/MMI-Hub && claude plugin install mmi@mmi";
|
|
6797
|
+
var CLAUDE_PLUGIN_HEAL_STEPS = [
|
|
6798
|
+
{ args: ["plugin", "marketplace", "remove", "mmi"], gated: false },
|
|
6799
|
+
{ args: ["plugin", "marketplace", "add", "mutmutco/MMI-Hub"], gated: true },
|
|
6800
|
+
{ args: ["plugin", "install", "mmi@mmi"], gated: true },
|
|
6801
|
+
{ args: ["plugin", "enable", "mmi@mmi"], gated: false }
|
|
6802
|
+
];
|
|
6803
|
+
function healStepAborts(step, ok) {
|
|
6804
|
+
return !ok && step.gated;
|
|
6805
|
+
}
|
|
6716
6806
|
function pluginRecoveryFix(surface) {
|
|
6717
|
-
const claude =
|
|
6807
|
+
const claude = CLAUDE_PLUGIN_RECOVERY;
|
|
6718
6808
|
switch (surface) {
|
|
6719
6809
|
case "claude-vscode":
|
|
6720
6810
|
case "claude-cli":
|
|
@@ -6729,7 +6819,7 @@ function pluginRecoveryFix(surface) {
|
|
|
6729
6819
|
}
|
|
6730
6820
|
}
|
|
6731
6821
|
var PLUGIN_UPDATE_RECIPES = {
|
|
6732
|
-
claude: [
|
|
6822
|
+
claude: [CLAUDE_PLUGIN_RECOVERY],
|
|
6733
6823
|
codex: ["codex plugin marketplace upgrade mmi", "codex plugin list # verify mmi@mmi shows the new version"],
|
|
6734
6824
|
cli: ["npm install -g @mutmutco/cli@latest"]
|
|
6735
6825
|
};
|
|
@@ -6784,7 +6874,7 @@ function isSemverVersion(v) {
|
|
|
6784
6874
|
return typeof v === "string" && /^v?\d+\.\d+\.\d+/.test(v.trim());
|
|
6785
6875
|
}
|
|
6786
6876
|
function staleRecordCommand(surface) {
|
|
6787
|
-
return surface === "codex" ? "codex plugin marketplace upgrade mmi && codex plugin add mmi@mmi" :
|
|
6877
|
+
return surface === "codex" ? "codex plugin marketplace upgrade mmi && codex plugin add mmi@mmi" : CLAUDE_PLUGIN_RECOVERY;
|
|
6788
6878
|
}
|
|
6789
6879
|
function staleSurfacesFix(stale, releasedVersion) {
|
|
6790
6880
|
const parts = stale.map((s) => {
|
|
@@ -11609,10 +11699,10 @@ async function runClaudePlugin(args) {
|
|
|
11609
11699
|
}
|
|
11610
11700
|
async function applyClaudePluginHeal(surface, log) {
|
|
11611
11701
|
if (surface !== "claude-cli" && surface !== "claude-vscode") return false;
|
|
11612
|
-
log(" \u21BB
|
|
11613
|
-
|
|
11614
|
-
|
|
11615
|
-
|
|
11702
|
+
log(" \u21BB reinstalling the MMI plugin via `claude plugin` (marketplace remove \u2192 add \u2192 install)\u2026");
|
|
11703
|
+
for (const step of CLAUDE_PLUGIN_HEAL_STEPS) {
|
|
11704
|
+
if (healStepAborts(step, await runClaudePlugin([...step.args]))) return false;
|
|
11705
|
+
}
|
|
11616
11706
|
return true;
|
|
11617
11707
|
}
|
|
11618
11708
|
var program2 = new Command();
|
|
@@ -12558,6 +12648,74 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
12558
12648
|
const projectItemId = await attachToProject(created.number, targetRepo2, priority);
|
|
12559
12649
|
console.log(JSON.stringify({ ...created, deduped: false, label: REPORT_LABEL, priority, projectItemId }));
|
|
12560
12650
|
});
|
|
12651
|
+
async function resolvePluginSha() {
|
|
12652
|
+
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
|
12653
|
+
if (!root) return void 0;
|
|
12654
|
+
try {
|
|
12655
|
+
const { stdout } = await execFileP2("git", ["-C", root, "rev-parse", "--short", "HEAD"], { timeout: 5e3 });
|
|
12656
|
+
return stdout.trim() || void 0;
|
|
12657
|
+
} catch {
|
|
12658
|
+
return void 0;
|
|
12659
|
+
}
|
|
12660
|
+
}
|
|
12661
|
+
program2.command("skill-lesson").description("file a skill-lesson on the Hub board (GitHub auth, dedups open lessons) and print {number,url} JSON").requiredOption("--skill <name>", `which skill misfired (${SKILL_NAMES.join(" | ")})`).requiredOption("--title <title>", "one-line summary of what misfired").option("--body <body>", "lesson body: what misfired, the evidence, and the proposed amendment (markdown)").option("--body-file <path|->", "read the lesson body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (board Priority field when configured)", "medium").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO})`).option("--force", "file a new issue even when an open lesson looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 skill-lesson always prints JSON)").action(async (o) => {
|
|
12662
|
+
const targetRepo2 = o.repo ?? HUB_REPO;
|
|
12663
|
+
const sourceRepo = await resolveRepo(void 0);
|
|
12664
|
+
const pluginSha = await resolvePluginSha();
|
|
12665
|
+
let skill;
|
|
12666
|
+
let rawBody;
|
|
12667
|
+
let title;
|
|
12668
|
+
let priority;
|
|
12669
|
+
let body;
|
|
12670
|
+
let args;
|
|
12671
|
+
try {
|
|
12672
|
+
skill = assertSkillName(o.skill);
|
|
12673
|
+
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
|
|
12674
|
+
title = buildSkillLessonTitle(skill, o.title);
|
|
12675
|
+
priority = normalizePriority(o.priority);
|
|
12676
|
+
body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
|
|
12677
|
+
args = buildIssueArgs({ type: "task", title, body, priority, repo: targetRepo2, labels: [SKILL_LESSON_LABEL] });
|
|
12678
|
+
} catch (e) {
|
|
12679
|
+
return fail(`skill-lesson: ${e.message}`);
|
|
12680
|
+
}
|
|
12681
|
+
if (!o.force) {
|
|
12682
|
+
let openLessons = [];
|
|
12683
|
+
try {
|
|
12684
|
+
openLessons = await ghJson([
|
|
12685
|
+
"issue",
|
|
12686
|
+
"list",
|
|
12687
|
+
"--repo",
|
|
12688
|
+
targetRepo2,
|
|
12689
|
+
"--state",
|
|
12690
|
+
"open",
|
|
12691
|
+
"--label",
|
|
12692
|
+
SKILL_LESSON_LABEL,
|
|
12693
|
+
"--limit",
|
|
12694
|
+
"100",
|
|
12695
|
+
"--json",
|
|
12696
|
+
"number,title,body,url"
|
|
12697
|
+
]);
|
|
12698
|
+
} catch {
|
|
12699
|
+
}
|
|
12700
|
+
const dup = findDuplicateLesson({ title, body: rawBody }, openLessons);
|
|
12701
|
+
if (dup) {
|
|
12702
|
+
try {
|
|
12703
|
+
await execFileP2("gh", ["issue", "comment", String(dup.number), "--repo", targetRepo2, "--body", buildSkillLessonDupComment(dup.number, rawBody, sourceRepo, pluginSha)], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
12704
|
+
} catch (e) {
|
|
12705
|
+
const err = e;
|
|
12706
|
+
return fail(`skill-lesson: duplicate of #${dup.number} (${dup.url}) but the +1 comment failed: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
12707
|
+
}
|
|
12708
|
+
return console.log(JSON.stringify({ deduped: true, number: dup.number, url: dup.url, score: dup.score }));
|
|
12709
|
+
}
|
|
12710
|
+
}
|
|
12711
|
+
try {
|
|
12712
|
+
await execFileP2("gh", ["label", "create", SKILL_LESSON_LABEL, "--color", "c2e0c6", "--repo", targetRepo2], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
12713
|
+
} catch {
|
|
12714
|
+
}
|
|
12715
|
+
const created = await ghCreate(args);
|
|
12716
|
+
const projectItemId = await attachToProject(created.number, targetRepo2, priority);
|
|
12717
|
+
console.log(JSON.stringify({ ...created, deduped: false, label: SKILL_LESSON_LABEL, skill, priority, projectItemId }));
|
|
12718
|
+
});
|
|
12561
12719
|
var pr = program2.command("pr").description("pull requests \u2014 reliable create with structured output");
|
|
12562
12720
|
pr.command("create").description("create a PR and print {number,url} JSON").requiredOption("--title <title>", "PR title").option("--body <body>", "PR body (markdown)").option("--body-file <path|->", "read PR body from a UTF-8 file, or from stdin with -").option("--base <branch>", "base branch (defaults to the repo default)").option("--head <branch>", "head branch (defaults to the current branch)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (o) => {
|
|
12563
12721
|
let body;
|
|
@@ -13550,6 +13708,18 @@ function mmiPluginCacheRootSnapshots() {
|
|
|
13550
13708
|
}
|
|
13551
13709
|
});
|
|
13552
13710
|
}
|
|
13711
|
+
function hasNestedMmiChild(versionDir) {
|
|
13712
|
+
try {
|
|
13713
|
+
return (0, import_node_fs11.statSync)((0, import_node_path10.join)(versionDir, "mmi")).isDirectory();
|
|
13714
|
+
} catch {
|
|
13715
|
+
return false;
|
|
13716
|
+
}
|
|
13717
|
+
}
|
|
13718
|
+
function nestedPluginTreeSnapshot() {
|
|
13719
|
+
return mmiPluginCacheRootSnapshots().filter((root) => root.surface === "claude").flatMap(
|
|
13720
|
+
(root) => root.entries.filter((e) => e.isDirectory && /^v?\d+\.\d+\.\d+/.test(e.name)).map((e) => ({ surface: "claude", path: e.path, nested: hasNestedMmiChild(e.path) }))
|
|
13721
|
+
);
|
|
13722
|
+
}
|
|
13553
13723
|
function uniqueQuarantineTarget(path2) {
|
|
13554
13724
|
if (!(0, import_node_fs11.existsSync)(path2)) return path2;
|
|
13555
13725
|
for (let i = 1; i < 100; i += 1) {
|
|
@@ -13742,6 +13912,13 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
13742
13912
|
};
|
|
13743
13913
|
}
|
|
13744
13914
|
checks.push(cacheCleanupCheck);
|
|
13915
|
+
checks.push(
|
|
13916
|
+
buildNestedPluginTreeCheck({
|
|
13917
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
13918
|
+
isWindows: isWin,
|
|
13919
|
+
entries: nestedPluginTreeSnapshot()
|
|
13920
|
+
})
|
|
13921
|
+
);
|
|
13745
13922
|
const cursorCacheRoot = cursorPluginCacheRoot();
|
|
13746
13923
|
checks.push(
|
|
13747
13924
|
buildCursorPluginInstallCheck({
|
package/dist/saga.cjs
CHANGED
|
@@ -3565,6 +3565,7 @@ async function runHeadEngine(prompt, timeoutMs = HEAD_ENGINE_TIMEOUT_MS) {
|
|
|
3565
3565
|
}
|
|
3566
3566
|
|
|
3567
3567
|
// src/saga-health.ts
|
|
3568
|
+
var MEMORY_STALE_DAYS = 14;
|
|
3568
3569
|
function buildHealth(i) {
|
|
3569
3570
|
const problems = [];
|
|
3570
3571
|
if (!i.sagaApiUrl) problems.push("Hub API URL not configured");
|
|
@@ -3575,6 +3576,10 @@ function buildHealth(i) {
|
|
|
3575
3576
|
}
|
|
3576
3577
|
if (i.reachable && i.authorized === false) problems.push("saga backend rejected authenticated state access");
|
|
3577
3578
|
if (!i.key.sessionId || i.key.sessionId === "-") problems.push("unsafe session id");
|
|
3579
|
+
const warnings = [];
|
|
3580
|
+
if (i.memoryAgeDays !== void 0 && i.memoryAgeDays > MEMORY_STALE_DAYS) {
|
|
3581
|
+
warnings.push(`PROJECT MEMORY is ${Math.round(i.memoryAgeDays)}d stale \u2014 the saga-keeper may have stalled`);
|
|
3582
|
+
}
|
|
3578
3583
|
const safeToWrite = problems.length === 0;
|
|
3579
3584
|
return {
|
|
3580
3585
|
ok: safeToWrite,
|
|
@@ -3588,14 +3593,19 @@ function buildHealth(i) {
|
|
|
3588
3593
|
pendingNotes: i.pendingNotes ?? 0,
|
|
3589
3594
|
key: i.key,
|
|
3590
3595
|
source: i.source,
|
|
3591
|
-
problems
|
|
3596
|
+
problems,
|
|
3597
|
+
warnings,
|
|
3598
|
+
memoryAgeDays: i.memoryAgeDays
|
|
3592
3599
|
};
|
|
3593
3600
|
}
|
|
3594
3601
|
function healthBanner(report) {
|
|
3595
|
-
if (report.
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3602
|
+
if (report.problems.length) {
|
|
3603
|
+
const summary = report.problems.slice(0, 2).join("; ") || "unknown saga health gap";
|
|
3604
|
+
const suffix = report.problems.length > 2 ? ` (+${report.problems.length - 2} more)` : "";
|
|
3605
|
+
return `saga health: CHECK - ${summary}${suffix}`;
|
|
3606
|
+
}
|
|
3607
|
+
if (report.warnings.length) return `saga health: NOTE - ${report.warnings.join("; ")}`;
|
|
3608
|
+
return null;
|
|
3599
3609
|
}
|
|
3600
3610
|
function resumeCue() {
|
|
3601
3611
|
return '> STATUS/RESUME CUE \u2014 For any status, resume, or "where do I stand" report: read THIS saga HEAD first (`mmi-cli saga show`), then reconcile its NEXT / LAST 5 / DECISIONS against the live board + git/gh before reporting. Do not rebuild the picture from board/issues/memory while skipping the HEAD. PRECEDENCE: the HEAD is prior-session belief and MAY BE SUPERSEDED \u2014 the current live user/master instruction WINS over any conflicting HEAD anchor, NEXT, or checklist; follow the live instruction and treat the stale HEAD item as superseded.';
|
|
@@ -4205,6 +4215,19 @@ async function probeSagaAccess(url, key) {
|
|
|
4205
4215
|
return false;
|
|
4206
4216
|
}
|
|
4207
4217
|
}
|
|
4218
|
+
async function fetchMemoryAge(url, project) {
|
|
4219
|
+
try {
|
|
4220
|
+
const qs = new URLSearchParams({ project }).toString();
|
|
4221
|
+
const res = await fetch(`${url}/saga/memory-age?${qs}`, { headers: await hubHeaders(), signal: AbortSignal.timeout(8e3) });
|
|
4222
|
+
if (!res.ok) return void 0;
|
|
4223
|
+
const body = await res.json();
|
|
4224
|
+
if (!body.updatedAt) return void 0;
|
|
4225
|
+
const ms = Date.now() - Date.parse(body.updatedAt);
|
|
4226
|
+
return Number.isFinite(ms) && ms >= 0 ? ms / 864e5 : void 0;
|
|
4227
|
+
} catch {
|
|
4228
|
+
return void 0;
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4208
4231
|
async function runSagaHealth(o, io = consoleIo) {
|
|
4209
4232
|
const cfg = await loadConfig();
|
|
4210
4233
|
const session = resolveSessionId();
|
|
@@ -4215,6 +4238,7 @@ async function runSagaHealth(o, io = consoleIo) {
|
|
|
4215
4238
|
cfg.sagaApiUrl ? probeBackend(cfg.sagaApiUrl) : Promise.resolve({ reachable: false })
|
|
4216
4239
|
]);
|
|
4217
4240
|
const authorized = cfg.sagaApiUrl && liveness.reachable ? await probeSagaAccess(cfg.sagaApiUrl, key) : void 0;
|
|
4241
|
+
const memoryAgeDays = cfg.sagaApiUrl && liveness.reachable ? await fetchMemoryAge(cfg.sagaApiUrl, key.project) : void 0;
|
|
4218
4242
|
const report = buildHealth({
|
|
4219
4243
|
key,
|
|
4220
4244
|
source,
|
|
@@ -4224,7 +4248,8 @@ async function runSagaHealth(o, io = consoleIo) {
|
|
|
4224
4248
|
livenessMessage: liveness.message,
|
|
4225
4249
|
authorized,
|
|
4226
4250
|
sagaApiUrl: cfg.sagaApiUrl,
|
|
4227
|
-
pendingNotes: readPending().length
|
|
4251
|
+
pendingNotes: readPending().length,
|
|
4252
|
+
memoryAgeDays
|
|
4228
4253
|
});
|
|
4229
4254
|
if (o.json) return io.log(JSON.stringify(report));
|
|
4230
4255
|
if (o.banner) {
|
|
@@ -4235,6 +4260,7 @@ async function runSagaHealth(o, io = consoleIo) {
|
|
|
4235
4260
|
if (o.quiet) return;
|
|
4236
4261
|
io.log(`saga health: ${report.ok ? "OK" : "NOT OK"}`);
|
|
4237
4262
|
if (report.problems.length) io.log(report.problems.map((p) => ` - ${p}`).join("\n"));
|
|
4263
|
+
if (report.warnings.length) io.log(report.warnings.map((w) => ` - ${w}`).join("\n"));
|
|
4238
4264
|
if (report.pendingNotes > 0) io.log(` - ${report.pendingNotes} note(s) queued locally \u2014 \`mmi-cli saga flush\` to roll forward`);
|
|
4239
4265
|
}
|
|
4240
4266
|
function registerSagaCommands(program2) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.28.0",
|
|
4
4
|
"description": "MMI Future CLI — delivers the org rules (whole-file), plus saga and KB access. The cross-IDE engine the plugin's SessionStart hook drives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|