@mutmutco/cli 2.11.0 → 2.12.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/index.cjs +404 -130
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -3392,9 +3392,9 @@ function useColor() {
|
|
|
3392
3392
|
var program = new Command();
|
|
3393
3393
|
|
|
3394
3394
|
// src/index.ts
|
|
3395
|
-
var
|
|
3395
|
+
var import_promises2 = require("node:fs/promises");
|
|
3396
3396
|
var import_node_fs5 = require("node:fs");
|
|
3397
|
-
var
|
|
3397
|
+
var import_node_crypto2 = require("node:crypto");
|
|
3398
3398
|
|
|
3399
3399
|
// src/rules-sync.ts
|
|
3400
3400
|
function normalizeEol(s) {
|
|
@@ -3468,6 +3468,25 @@ async function runSessionStart(parallel, sequential, io) {
|
|
|
3468
3468
|
for (const lines of buffered) flush(lines, io);
|
|
3469
3469
|
for (const step of sequential) flush(await runBufferedStep(step), io);
|
|
3470
3470
|
}
|
|
3471
|
+
function buildSessionStartPlan(verbs) {
|
|
3472
|
+
return {
|
|
3473
|
+
parallel: [
|
|
3474
|
+
{ name: "rules sync", run: verbs.rulesSync },
|
|
3475
|
+
{ name: "saga show", run: verbs.sagaShow },
|
|
3476
|
+
{ name: "saga health", run: verbs.sagaHealth }
|
|
3477
|
+
],
|
|
3478
|
+
sequential: [{ name: "doctor", run: verbs.doctor }]
|
|
3479
|
+
};
|
|
3480
|
+
}
|
|
3481
|
+
function spawnDetachedSelf(args, deps) {
|
|
3482
|
+
try {
|
|
3483
|
+
deps.spawn(deps.execPath, [deps.scriptPath, ...args], { detached: true, stdio: "ignore", windowsHide: true }).unref();
|
|
3484
|
+
} catch {
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
function northstarPointer() {
|
|
3488
|
+
return "North Stars: run `mmi-cli northstar relevant` to load plans relevant to your task (`northstar list` for all).";
|
|
3489
|
+
}
|
|
3471
3490
|
|
|
3472
3491
|
// src/saga-capture.ts
|
|
3473
3492
|
function parseHookInput(stdin) {
|
|
@@ -3482,8 +3501,8 @@ function parseHookInput(stdin) {
|
|
|
3482
3501
|
// src/index.ts
|
|
3483
3502
|
var import_node_child_process6 = require("node:child_process");
|
|
3484
3503
|
var import_node_util6 = require("node:util");
|
|
3485
|
-
var
|
|
3486
|
-
var
|
|
3504
|
+
var import_node_path6 = require("node:path");
|
|
3505
|
+
var import_node_os2 = require("node:os");
|
|
3487
3506
|
|
|
3488
3507
|
// src/saga-head-maintainer.ts
|
|
3489
3508
|
var import_node_child_process2 = require("node:child_process");
|
|
@@ -3608,6 +3627,12 @@ async function runHeadEngine(prompt, timeoutMs = HEAD_ENGINE_TIMEOUT_MS) {
|
|
|
3608
3627
|
});
|
|
3609
3628
|
}
|
|
3610
3629
|
|
|
3630
|
+
// src/gh-create.ts
|
|
3631
|
+
var import_promises = require("node:fs/promises");
|
|
3632
|
+
var import_node_os = require("node:os");
|
|
3633
|
+
var import_node_path3 = require("node:path");
|
|
3634
|
+
var import_node_crypto = require("node:crypto");
|
|
3635
|
+
|
|
3611
3636
|
// src/board-priority.ts
|
|
3612
3637
|
var BOARD_PRIORITY_NAMES = ["Urgent", "High", "Medium", "Low"];
|
|
3613
3638
|
var CLI_PRIORITIES = ["urgent", "high", "medium", "low"];
|
|
@@ -3646,6 +3671,11 @@ function recoverPriorityFromEvents(events) {
|
|
|
3646
3671
|
|
|
3647
3672
|
// src/gh-create.ts
|
|
3648
3673
|
var ISSUE_TYPES = ["bug", "feature", "task"];
|
|
3674
|
+
var GH_MUTATION_TIMEOUT_MS = 12e4;
|
|
3675
|
+
function timeoutKillNote(err, timeoutMs) {
|
|
3676
|
+
if (typeof err !== "object" || err === null || !err.killed) return void 0;
|
|
3677
|
+
return `killed at the ${timeoutMs}ms timeout \u2014 the write may have completed server-side; verify before retrying`;
|
|
3678
|
+
}
|
|
3649
3679
|
function normalizePriority(priority) {
|
|
3650
3680
|
const p = priority.trim().toLowerCase();
|
|
3651
3681
|
if (!CLI_PRIORITIES.includes(p)) {
|
|
@@ -3673,6 +3703,24 @@ function buildIssueArgs({ type, title, body, priority, repo, labels }) {
|
|
|
3673
3703
|
for (const label of labels ?? []) args.push("--label", label);
|
|
3674
3704
|
return args;
|
|
3675
3705
|
}
|
|
3706
|
+
async function bodyArgsViaFile(args, deps = {}) {
|
|
3707
|
+
const i = args.indexOf("--body");
|
|
3708
|
+
if (i === -1 || i + 1 >= args.length) return { args, cleanup: async () => {
|
|
3709
|
+
} };
|
|
3710
|
+
const write = deps.write ?? import_promises.writeFile;
|
|
3711
|
+
const remove = deps.remove ?? import_promises.unlink;
|
|
3712
|
+
const file = (0, import_node_path3.join)(deps.dir ?? (0, import_node_os.tmpdir)(), `mmi-gh-body-${process.pid}-${(0, import_node_crypto.randomBytes)(4).toString("hex")}.md`);
|
|
3713
|
+
await write(file, args[i + 1], "utf8");
|
|
3714
|
+
return {
|
|
3715
|
+
args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
|
|
3716
|
+
cleanup: async () => {
|
|
3717
|
+
try {
|
|
3718
|
+
await remove(file);
|
|
3719
|
+
} catch {
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
};
|
|
3723
|
+
}
|
|
3676
3724
|
function buildAddToProjectArgs(projectId, contentId) {
|
|
3677
3725
|
if (!projectId) throw new Error("addToProject: projectId is required");
|
|
3678
3726
|
if (!contentId) throw new Error("addToProject: contentId is required");
|
|
@@ -3811,6 +3859,32 @@ function resumeCue() {
|
|
|
3811
3859
|
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.';
|
|
3812
3860
|
}
|
|
3813
3861
|
|
|
3862
|
+
// src/fetch-retry.ts
|
|
3863
|
+
async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
|
|
3864
|
+
const attempts = opts.attempts ?? 3;
|
|
3865
|
+
const baseDelayMs = opts.baseDelayMs ?? 250;
|
|
3866
|
+
const retryOn = opts.retryOn ?? ((res) => res.status >= 500);
|
|
3867
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
3868
|
+
let lastErr;
|
|
3869
|
+
for (let i = 0; i < attempts; i++) {
|
|
3870
|
+
const isLast = i === attempts - 1;
|
|
3871
|
+
const attemptInit = opts.timeoutMs ? { ...init, signal: AbortSignal.timeout(opts.timeoutMs) } : init;
|
|
3872
|
+
try {
|
|
3873
|
+
const res = await fetchImpl(url, attemptInit);
|
|
3874
|
+
if (!isLast && retryOn(res)) {
|
|
3875
|
+
await sleep(baseDelayMs * 2 ** i);
|
|
3876
|
+
continue;
|
|
3877
|
+
}
|
|
3878
|
+
return res;
|
|
3879
|
+
} catch (e) {
|
|
3880
|
+
lastErr = e;
|
|
3881
|
+
if (isLast) throw e;
|
|
3882
|
+
await sleep(baseDelayMs * 2 ** i);
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
throw lastErr;
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3814
3888
|
// src/saga-note.ts
|
|
3815
3889
|
var AGENT_SURFACE_TOKENS = ["claude", "codex", "cursor", "gemini"];
|
|
3816
3890
|
var ROUTE_LEVEL_403 = "saga API route-level 403 from GitHubAuthorizer cache/policy";
|
|
@@ -5401,12 +5475,12 @@ function buildInstalledPluginVersionCheck(input) {
|
|
|
5401
5475
|
// src/stage-runner.ts
|
|
5402
5476
|
var import_node_child_process5 = require("node:child_process");
|
|
5403
5477
|
var import_node_fs3 = require("node:fs");
|
|
5404
|
-
var
|
|
5478
|
+
var import_node_path4 = require("node:path");
|
|
5405
5479
|
var import_node_net = require("node:net");
|
|
5406
5480
|
var import_node_util5 = require("node:util");
|
|
5407
5481
|
var execFileP3 = (0, import_node_util5.promisify)(import_node_child_process5.execFile);
|
|
5408
5482
|
function stageStatePath(cwd = process.cwd()) {
|
|
5409
|
-
return (0,
|
|
5483
|
+
return (0, import_node_path4.join)(cwd, "tmp", "stage", "state.json");
|
|
5410
5484
|
}
|
|
5411
5485
|
function validateStageConfig(config = {}, action) {
|
|
5412
5486
|
const problems = [];
|
|
@@ -6476,16 +6550,21 @@ var PROJECTS_ENVELOPE_KEY = "projects";
|
|
|
6476
6550
|
|
|
6477
6551
|
// src/registry-client.ts
|
|
6478
6552
|
var DEFAULT_TIMEOUT_MS2 = 8e3;
|
|
6553
|
+
var RETRY_ATTEMPTS = 3;
|
|
6554
|
+
function retriedFetch(deps, url, init) {
|
|
6555
|
+
return fetchWithRetry(deps.fetch ?? fetch, url, init, {
|
|
6556
|
+
attempts: RETRY_ATTEMPTS,
|
|
6557
|
+
timeoutMs: deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2
|
|
6558
|
+
});
|
|
6559
|
+
}
|
|
6479
6560
|
async function fetchTrainAuthority(repo, deps) {
|
|
6480
6561
|
if (!deps.baseUrl) return { ok: false, error: "Hub API URL not configured" };
|
|
6481
6562
|
const token = await deps.token();
|
|
6482
6563
|
if (!token) return { ok: false, error: "no GitHub token (gh auth login)" };
|
|
6483
|
-
const doFetch = deps.fetch ?? fetch;
|
|
6484
6564
|
try {
|
|
6485
|
-
const res = await
|
|
6565
|
+
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/train-authority?repo=${encodeURIComponent(repo)}`, {
|
|
6486
6566
|
method: "GET",
|
|
6487
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
6488
|
-
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
6567
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
6489
6568
|
});
|
|
6490
6569
|
if (!res.ok) return { ok: false, error: `train-authority HTTP ${res.status}` };
|
|
6491
6570
|
const body = await res.json();
|
|
@@ -6499,12 +6578,10 @@ async function fetchProjectsList(deps) {
|
|
|
6499
6578
|
if (!deps.baseUrl) return null;
|
|
6500
6579
|
const token = await deps.token();
|
|
6501
6580
|
if (!token) return null;
|
|
6502
|
-
const doFetch = deps.fetch ?? fetch;
|
|
6503
6581
|
try {
|
|
6504
|
-
const res = await
|
|
6582
|
+
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}${PROJECTS_LIST_PATH}`, {
|
|
6505
6583
|
method: "GET",
|
|
6506
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
6507
|
-
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
6584
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
6508
6585
|
});
|
|
6509
6586
|
if (!res.ok) return null;
|
|
6510
6587
|
const body = await res.json();
|
|
@@ -6523,12 +6600,10 @@ async function fetchProjectBySlugChecked(slug, deps) {
|
|
|
6523
6600
|
if (!slug) return { ok: false, error: "no slug" };
|
|
6524
6601
|
const token = await deps.token();
|
|
6525
6602
|
if (!token) return { ok: false, error: "no GitHub token (run `gh auth login`)" };
|
|
6526
|
-
const doFetch = deps.fetch ?? fetch;
|
|
6527
6603
|
try {
|
|
6528
|
-
const res = await
|
|
6604
|
+
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}`, {
|
|
6529
6605
|
method: "GET",
|
|
6530
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
6531
|
-
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
6606
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
6532
6607
|
});
|
|
6533
6608
|
if (res.status === 404) return { ok: true, project: null };
|
|
6534
6609
|
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
|
|
@@ -6545,12 +6620,10 @@ async function fetchDeployStatusBySlug(slug, deps) {
|
|
|
6545
6620
|
if (!deps.baseUrl || !slug) return null;
|
|
6546
6621
|
const token = await deps.token();
|
|
6547
6622
|
if (!token) return null;
|
|
6548
|
-
const doFetch = deps.fetch ?? fetch;
|
|
6549
6623
|
try {
|
|
6550
|
-
const res = await
|
|
6624
|
+
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}/deploy-status`, {
|
|
6551
6625
|
method: "GET",
|
|
6552
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
6553
|
-
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
6626
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
6554
6627
|
});
|
|
6555
6628
|
if (!res.ok) return null;
|
|
6556
6629
|
const body = await res.json();
|
|
@@ -6564,12 +6637,10 @@ async function fetchOrgConfig(deps) {
|
|
|
6564
6637
|
if (!deps.baseUrl) return null;
|
|
6565
6638
|
const token = await deps.token();
|
|
6566
6639
|
if (!token) return null;
|
|
6567
|
-
const doFetch = deps.fetch ?? fetch;
|
|
6568
6640
|
try {
|
|
6569
|
-
const res = await
|
|
6641
|
+
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}${ORG_CONFIG_PATH}`, {
|
|
6570
6642
|
method: "GET",
|
|
6571
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
6572
|
-
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
6643
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
6573
6644
|
});
|
|
6574
6645
|
if (!res.ok) return null;
|
|
6575
6646
|
return await res.json();
|
|
@@ -6581,13 +6652,11 @@ async function postJson(pathSuffix, payload, deps, method = "POST") {
|
|
|
6581
6652
|
if (!deps.baseUrl) return { ok: false, status: 0, body: null, error: "no Hub API URL (set MMI_HUB_URL or use a current MMI CLI/plugin build)" };
|
|
6582
6653
|
const token = await deps.token();
|
|
6583
6654
|
if (!token) return { ok: false, status: 0, body: null, error: "no GitHub token (run `gh auth login`)" };
|
|
6584
|
-
const doFetch = deps.fetch ?? fetch;
|
|
6585
6655
|
try {
|
|
6586
|
-
const res = await
|
|
6656
|
+
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}${pathSuffix}`, {
|
|
6587
6657
|
method,
|
|
6588
6658
|
headers: { Authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
6589
|
-
body: JSON.stringify(payload)
|
|
6590
|
-
signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
6659
|
+
body: JSON.stringify(payload)
|
|
6591
6660
|
});
|
|
6592
6661
|
let body = null;
|
|
6593
6662
|
try {
|
|
@@ -6658,9 +6727,12 @@ function appAttestationOf(meta) {
|
|
|
6658
6727
|
function attestedLine(att) {
|
|
6659
6728
|
return `App-owned readiness attested by @${att.by} on ${att.at.slice(0, 10)} \u2014 the static checklist is cleared (the doctor reads no product repo files); re-run \`mmi-cli project attest\` after app-owned structural changes.`;
|
|
6660
6729
|
}
|
|
6730
|
+
var CONTRACT_UNDECLARED_LINE = "No runtime secrets declared \u2014 declare requiredRuntimeSecrets (a per-stage name map) in the registry META, or attest the app needs none with an explicit empty stage map ({ dev: [], rc: [], main: [] }).";
|
|
6661
6731
|
function appGapsFor(meta, model, slug, projectType) {
|
|
6662
6732
|
const attested = appAttestationOf(meta);
|
|
6663
|
-
|
|
6733
|
+
const isTenantWeb = !(projectType === "content" || model === "content") && projectType !== "desktop-game" && !(projectType === "non-deployable" || model === "none") && model !== "hub-serverless" && model !== "serverless";
|
|
6734
|
+
const contractUndeclared = isTenantWeb && Boolean(meta) && !hasRuntimeSecretContract(meta?.requiredRuntimeSecrets);
|
|
6735
|
+
if (attested) return contractUndeclared ? [attestedLine(attested), CONTRACT_UNDECLARED_LINE] : [attestedLine(attested)];
|
|
6664
6736
|
if (projectType === "content" || model === "content") return ["Content/KB repo: keep app-owned work to docs/content changes; release train does not apply."];
|
|
6665
6737
|
if (projectType === "desktop-game") {
|
|
6666
6738
|
return [
|
|
@@ -6692,8 +6764,8 @@ function appGapsFor(meta, model, slug, projectType) {
|
|
|
6692
6764
|
"Make app config fail clearly for missing required env in prod/rc instead of relying on hidden defaults.",
|
|
6693
6765
|
"Keep app-owned README.md and architecture.md aligned with v2 central deploy/secrets reality."
|
|
6694
6766
|
];
|
|
6695
|
-
if (
|
|
6696
|
-
gaps.unshift(
|
|
6767
|
+
if (contractUndeclared) {
|
|
6768
|
+
gaps.unshift(CONTRACT_UNDECLARED_LINE);
|
|
6697
6769
|
}
|
|
6698
6770
|
if (slug === "mmi-katip") {
|
|
6699
6771
|
gaps.push("Katip-specific app plan: declare Google Workspace service-account requirements, use the service account numeric OAuth2 client ID for DWD, remove prod-hidden impersonation defaults, and make non-critical Google Workspace failures degrade instead of crash-looping.");
|
|
@@ -6773,7 +6845,29 @@ async function runV2Heal(repoOrSlug, opts, deps) {
|
|
|
6773
6845
|
async function buildV2Doctor(repoOrSlug, deps) {
|
|
6774
6846
|
const slug = slugOfRepo(repoOrSlug);
|
|
6775
6847
|
const repo = repoFrom(repoOrSlug, slug);
|
|
6776
|
-
const
|
|
6848
|
+
const read = await deps.getProject(slug);
|
|
6849
|
+
if (!read.ok) {
|
|
6850
|
+
const degradedSecrets = {
|
|
6851
|
+
dev: { required: [], present: [], missing: [] },
|
|
6852
|
+
rc: { required: [], present: [], missing: [] },
|
|
6853
|
+
main: { required: [], present: [], missing: [] }
|
|
6854
|
+
};
|
|
6855
|
+
const degradedStage = {
|
|
6856
|
+
dev: { ok: false, required: false },
|
|
6857
|
+
rc: { ok: false, required: false },
|
|
6858
|
+
main: { ok: false, required: false }
|
|
6859
|
+
};
|
|
6860
|
+
return {
|
|
6861
|
+
ok: false,
|
|
6862
|
+
repo,
|
|
6863
|
+
slug,
|
|
6864
|
+
registryError: read.error,
|
|
6865
|
+
hubOwned: { meta: { ok: false, missing: [] }, deployCoords: degradedStage, deployState: degradedStage, secrets: degradedSecrets },
|
|
6866
|
+
autoHealAvailable: [],
|
|
6867
|
+
appOwnedGaps: [`Hub registry read failed (${read.error}) \u2014 diagnosis degraded; nothing is known about META, coords, or gaps. Likely transient (cold start, network, or auth blip): retry \`mmi-cli project doctor\` shortly.`]
|
|
6868
|
+
};
|
|
6869
|
+
}
|
|
6870
|
+
const meta = read.project;
|
|
6777
6871
|
const projectType = resolveProjectType(meta, repo);
|
|
6778
6872
|
const model = resolveDeployModel(meta, repo);
|
|
6779
6873
|
const autoHeal = buildV2HealPatch(repo, meta);
|
|
@@ -6879,6 +6973,30 @@ ${section}`.trim();
|
|
|
6879
6973
|
// src/project-set.ts
|
|
6880
6974
|
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired"];
|
|
6881
6975
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
6976
|
+
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
6977
|
+
function parseRuntimeSecretsVar(raw) {
|
|
6978
|
+
let parsed;
|
|
6979
|
+
try {
|
|
6980
|
+
parsed = JSON.parse(raw);
|
|
6981
|
+
} catch {
|
|
6982
|
+
throw new Error('project set: requiredRuntimeSecrets must be JSON, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}');
|
|
6983
|
+
}
|
|
6984
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
6985
|
+
throw new Error("project set: requiredRuntimeSecrets must be a stage map (a flat array is not box-loadable)");
|
|
6986
|
+
}
|
|
6987
|
+
const map = parsed;
|
|
6988
|
+
const out = {};
|
|
6989
|
+
for (const [stage2, names] of Object.entries(map)) {
|
|
6990
|
+
if (!RUNTIME_SECRET_STAGES.includes(stage2)) {
|
|
6991
|
+
throw new Error(`project set: requiredRuntimeSecrets stage "${stage2}" \u2014 expected only ${RUNTIME_SECRET_STAGES.join("/")}`);
|
|
6992
|
+
}
|
|
6993
|
+
if (!Array.isArray(names) || names.some((n) => typeof n !== "string" || !n.trim())) {
|
|
6994
|
+
throw new Error(`project set: requiredRuntimeSecrets.${stage2} must be an array of non-empty secret names`);
|
|
6995
|
+
}
|
|
6996
|
+
out[stage2] = names;
|
|
6997
|
+
}
|
|
6998
|
+
return out;
|
|
6999
|
+
}
|
|
6882
7000
|
function buildProjectSetPatch(input) {
|
|
6883
7001
|
const patch = {};
|
|
6884
7002
|
if (input.class) {
|
|
@@ -6908,6 +7026,8 @@ function buildProjectSetPatch(input) {
|
|
|
6908
7026
|
const n = Number(raw);
|
|
6909
7027
|
if (!Number.isFinite(n)) throw new Error("project set: projectNumber must be numeric");
|
|
6910
7028
|
patch[key] = n;
|
|
7029
|
+
} else if (key === "requiredRuntimeSecrets") {
|
|
7030
|
+
patch[key] = parseRuntimeSecretsVar(raw);
|
|
6911
7031
|
} else {
|
|
6912
7032
|
patch[key] = raw;
|
|
6913
7033
|
}
|
|
@@ -6967,10 +7087,91 @@ function parseKbTree(stdout, prefix) {
|
|
|
6967
7087
|
}
|
|
6968
7088
|
|
|
6969
7089
|
// src/plan.ts
|
|
6970
|
-
var
|
|
7090
|
+
var import_node_path5 = require("node:path");
|
|
7091
|
+
|
|
7092
|
+
// src/frontmatter.ts
|
|
7093
|
+
function splitFrontmatter(content) {
|
|
7094
|
+
const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(content);
|
|
7095
|
+
if (!match) return { entries: [], body: content };
|
|
7096
|
+
return { entries: match[1].split(/\r?\n/).filter((line) => line.trim()), body: content.slice(match[0].length) };
|
|
7097
|
+
}
|
|
7098
|
+
function entryKeyValue(line) {
|
|
7099
|
+
const i = line.indexOf(":");
|
|
7100
|
+
if (i < 0) return null;
|
|
7101
|
+
return { key: line.slice(0, i).trim().toLowerCase(), value: line.slice(i + 1).trim() };
|
|
7102
|
+
}
|
|
7103
|
+
function frontmatterValue(content, key) {
|
|
7104
|
+
const want = key.trim().toLowerCase();
|
|
7105
|
+
for (const line of splitFrontmatter(content).entries) {
|
|
7106
|
+
const kv = entryKeyValue(line);
|
|
7107
|
+
if (kv && kv.key === want) return kv.value || void 0;
|
|
7108
|
+
}
|
|
7109
|
+
return void 0;
|
|
7110
|
+
}
|
|
7111
|
+
function frontmatterList(content, key) {
|
|
7112
|
+
const raw = frontmatterValue(content, key);
|
|
7113
|
+
if (!raw) return [];
|
|
7114
|
+
return raw.replace(/^\[/, "").replace(/\]$/, "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
7115
|
+
}
|
|
7116
|
+
function extractPlanMeta(content) {
|
|
7117
|
+
const meta = {};
|
|
7118
|
+
const status = frontmatterValue(content, "status");
|
|
7119
|
+
if (status) meta.status = status;
|
|
7120
|
+
const tags = frontmatterList(content, "topic-tags");
|
|
7121
|
+
if (tags.length) meta.topicTags = tags;
|
|
7122
|
+
const title = frontmatterValue(content, "title");
|
|
7123
|
+
if (title) meta.title = title;
|
|
7124
|
+
const supersedes = frontmatterValue(content, "supersedes");
|
|
7125
|
+
if (supersedes) meta.supersedes = supersedes;
|
|
7126
|
+
return meta;
|
|
7127
|
+
}
|
|
7128
|
+
|
|
7129
|
+
// src/plan-relevance.ts
|
|
7130
|
+
var STOP = /* @__PURE__ */ new Set(["the", "and", "for", "with", "plan", "issue", "fix", "feat", "add", "wip", "src", "tsx", "mjs"]);
|
|
7131
|
+
function tokenize(s) {
|
|
7132
|
+
return (s ?? "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3 && !STOP.has(t));
|
|
7133
|
+
}
|
|
7134
|
+
var SUPPRESSED = /* @__PURE__ */ new Set(["superseded", "graduated"]);
|
|
7135
|
+
function signalTokens(s) {
|
|
7136
|
+
const all = [
|
|
7137
|
+
...tokenize(s.branch),
|
|
7138
|
+
...tokenize(s.issueTitle),
|
|
7139
|
+
...(s.issueLabels ?? []).flatMap(tokenize),
|
|
7140
|
+
...(s.changedFiles ?? []).flatMap(tokenize)
|
|
7141
|
+
];
|
|
7142
|
+
return new Set(all);
|
|
7143
|
+
}
|
|
7144
|
+
function rankPlansByRelevance(plans, signals, opts = {}) {
|
|
7145
|
+
const wanted = signalTokens(signals);
|
|
7146
|
+
const eligible = opts.includeAll ? plans : plans.filter((p) => !p.status || !SUPPRESSED.has(p.status.toLowerCase()));
|
|
7147
|
+
const ranked = eligible.map((plan2) => {
|
|
7148
|
+
const tagTokens = new Set((plan2.topicTags ?? []).flatMap(tokenize));
|
|
7149
|
+
const titleTokens = new Set(tokenize(plan2.title));
|
|
7150
|
+
const slugTokens = new Set(tokenize(plan2.slug));
|
|
7151
|
+
const matched = /* @__PURE__ */ new Set();
|
|
7152
|
+
let score = 0;
|
|
7153
|
+
for (const t of wanted) {
|
|
7154
|
+
if (tagTokens.has(t)) {
|
|
7155
|
+
score += 3;
|
|
7156
|
+
matched.add(t);
|
|
7157
|
+
} else if (titleTokens.has(t)) {
|
|
7158
|
+
score += 2;
|
|
7159
|
+
matched.add(t);
|
|
7160
|
+
} else if (slugTokens.has(t)) {
|
|
7161
|
+
score += 1;
|
|
7162
|
+
matched.add(t);
|
|
7163
|
+
}
|
|
7164
|
+
}
|
|
7165
|
+
return { plan: plan2, score, matched: [...matched] };
|
|
7166
|
+
});
|
|
7167
|
+
ranked.sort((a, b) => b.score - a.score || (b.plan.updatedAt ?? "").localeCompare(a.plan.updatedAt ?? ""));
|
|
7168
|
+
return ranked;
|
|
7169
|
+
}
|
|
7170
|
+
|
|
7171
|
+
// src/plan.ts
|
|
6971
7172
|
var PLANS_DIR = "plans";
|
|
6972
|
-
var META_FILE = (0,
|
|
6973
|
-
var planPath = (slug) => (0,
|
|
7173
|
+
var META_FILE = (0, import_node_path5.join)(PLANS_DIR, ".plan-meta.json");
|
|
7174
|
+
var planPath = (slug) => (0, import_node_path5.join)(PLANS_DIR, `${slug}.md`);
|
|
6974
7175
|
var metaKey = (project2, slug) => `${project2}/${slug}`;
|
|
6975
7176
|
function parseMeta(raw) {
|
|
6976
7177
|
if (!raw) return {};
|
|
@@ -6999,12 +7200,7 @@ function formatPlanList(plans) {
|
|
|
6999
7200
|
return plans.map((p) => `${p.slug} \xB7 ${p.updatedAt ?? "-"} \xB7 ${p.project}`).join("\n");
|
|
7000
7201
|
}
|
|
7001
7202
|
var TIMEOUT_MS = 8e3;
|
|
7002
|
-
var GRADUATION_KEYS = /* @__PURE__ */ new Set(["northstar-graduation", "privacy", "merged-pr"]);
|
|
7003
|
-
function splitFrontmatter(content) {
|
|
7004
|
-
const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(content);
|
|
7005
|
-
if (!match) return { entries: [], body: content };
|
|
7006
|
-
return { entries: match[1].split(/\r?\n/).filter((line) => line.trim()), body: content.slice(match[0].length) };
|
|
7007
|
-
}
|
|
7203
|
+
var GRADUATION_KEYS = /* @__PURE__ */ new Set(["northstar-graduation", "privacy", "merged-pr", "status"]);
|
|
7008
7204
|
function markPlanGraduated(content, opts) {
|
|
7009
7205
|
const { entries, body } = splitFrontmatter(normalizeEol(content));
|
|
7010
7206
|
const preserved = entries.filter((line) => {
|
|
@@ -7014,6 +7210,7 @@ function markPlanGraduated(content, opts) {
|
|
|
7014
7210
|
const next = [
|
|
7015
7211
|
...preserved,
|
|
7016
7212
|
"northstar-graduation: built-and-merged",
|
|
7213
|
+
"status: graduated",
|
|
7017
7214
|
"privacy: org",
|
|
7018
7215
|
`merged-pr: ${opts.mergedPr}`
|
|
7019
7216
|
];
|
|
@@ -7033,6 +7230,8 @@ async function planPush(deps, slug, opts = {}) {
|
|
|
7033
7230
|
const meta = parseMeta(deps.readMetaRaw());
|
|
7034
7231
|
const entry = meta[metaKey(project2, slug)];
|
|
7035
7232
|
const body = { project: project2, slug, content };
|
|
7233
|
+
const frontmatterMeta = extractPlanMeta(content);
|
|
7234
|
+
if (Object.keys(frontmatterMeta).length) body.meta = frontmatterMeta;
|
|
7036
7235
|
if (opts.force) body.force = true;
|
|
7037
7236
|
else if (entry?.etag) body.baseEtag = entry.etag;
|
|
7038
7237
|
const res = await deps.fetch(`${deps.apiUrl}/plan/put`, {
|
|
@@ -7086,35 +7285,59 @@ async function planPull(deps, slug, opts = {}) {
|
|
|
7086
7285
|
deps.log(`pulled ${slug} \u2192 ${planPath(slug)}`);
|
|
7087
7286
|
return true;
|
|
7088
7287
|
}
|
|
7288
|
+
async function fetchPlanList(deps, project2) {
|
|
7289
|
+
const qs = project2 ? `?${new URLSearchParams({ project: project2 }).toString()}` : "";
|
|
7290
|
+
const res = await deps.fetch(`${deps.apiUrl}/plan/list${qs}`, {
|
|
7291
|
+
method: "GET",
|
|
7292
|
+
headers: await deps.headers(),
|
|
7293
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
7294
|
+
});
|
|
7295
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
7296
|
+
const { plans } = await res.json();
|
|
7297
|
+
return plans ?? [];
|
|
7298
|
+
}
|
|
7089
7299
|
async function planList(deps, opts = {}) {
|
|
7090
7300
|
const project2 = opts.project ?? (opts.quiet ? await deps.project() : void 0);
|
|
7091
|
-
|
|
7092
|
-
let res;
|
|
7301
|
+
let plans;
|
|
7093
7302
|
try {
|
|
7094
|
-
|
|
7095
|
-
method: "GET",
|
|
7096
|
-
headers: await deps.headers(),
|
|
7097
|
-
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
7098
|
-
});
|
|
7303
|
+
plans = await fetchPlanList(deps, project2);
|
|
7099
7304
|
} catch (e) {
|
|
7100
7305
|
if (!opts.quiet) deps.err(`plan list: ${e.message}`);
|
|
7101
7306
|
return;
|
|
7102
7307
|
}
|
|
7103
|
-
if (
|
|
7104
|
-
if (!opts.quiet) deps.err(`plan list failed: HTTP ${res.status}`);
|
|
7105
|
-
return;
|
|
7106
|
-
}
|
|
7107
|
-
const { plans } = await res.json();
|
|
7108
|
-
if (opts.json) {
|
|
7109
|
-
deps.log(JSON.stringify(plans));
|
|
7110
|
-
return;
|
|
7111
|
-
}
|
|
7308
|
+
if (opts.json) return deps.log(JSON.stringify(plans));
|
|
7112
7309
|
if (!plans.length) {
|
|
7113
7310
|
if (!opts.quiet) deps.log("no plans");
|
|
7114
7311
|
return;
|
|
7115
7312
|
}
|
|
7116
7313
|
deps.log(formatPlanList(plans));
|
|
7117
7314
|
}
|
|
7315
|
+
function formatRelevant(ranked) {
|
|
7316
|
+
return ranked.map((r) => {
|
|
7317
|
+
const why = r.matched.length ? `matches ${r.matched.join(", ")}` : "recent";
|
|
7318
|
+
return `${r.plan.slug} \xB7 ${why} \xB7 mmi-cli northstar pull ${r.plan.slug}`;
|
|
7319
|
+
}).join("\n");
|
|
7320
|
+
}
|
|
7321
|
+
async function relevantPlans(deps, signals, opts = {}) {
|
|
7322
|
+
const project2 = opts.project ?? await deps.project();
|
|
7323
|
+
let plans;
|
|
7324
|
+
try {
|
|
7325
|
+
const scoped = await fetchPlanList(deps, project2);
|
|
7326
|
+
const unprojected = project2 === "-" ? [] : await fetchPlanList(deps, "-").catch(() => []);
|
|
7327
|
+
const seen = new Set(scoped.map((p) => `${p.project}/${p.slug}`));
|
|
7328
|
+
plans = [...scoped, ...unprojected.filter((p) => !seen.has(`${p.project}/${p.slug}`))];
|
|
7329
|
+
} catch (e) {
|
|
7330
|
+
deps.err(`northstar relevant: ${e.message}`);
|
|
7331
|
+
return;
|
|
7332
|
+
}
|
|
7333
|
+
if (!plans.length) return deps.log("no North Stars for this repo yet");
|
|
7334
|
+
const ranked = rankPlansByRelevance(plans, signals, { includeAll: opts.includeAll });
|
|
7335
|
+
const top = (opts.includeAll ? ranked : ranked.filter((r) => r.score > 0)).slice(0, opts.limit ?? 5);
|
|
7336
|
+
if (!top.length) {
|
|
7337
|
+
return deps.log(`no task-relevant North Stars among ${plans.length} for this repo \u2014 \`mmi-cli northstar relevant --all\` lists recent ones`);
|
|
7338
|
+
}
|
|
7339
|
+
deps.log(formatRelevant(top));
|
|
7340
|
+
}
|
|
7118
7341
|
async function planDelete(deps, slug, opts = {}) {
|
|
7119
7342
|
const project2 = opts.project ?? await deps.project();
|
|
7120
7343
|
const res = await deps.fetch(`${deps.apiUrl}/plan/delete`, {
|
|
@@ -7571,7 +7794,7 @@ async function sagaHeaders(extra = {}) {
|
|
|
7571
7794
|
async function loadConfig() {
|
|
7572
7795
|
let file = {};
|
|
7573
7796
|
try {
|
|
7574
|
-
file = JSON.parse(await (0,
|
|
7797
|
+
file = JSON.parse(await (0, import_promises2.readFile)(".mmi/config.json", "utf8"));
|
|
7575
7798
|
} catch {
|
|
7576
7799
|
file = {};
|
|
7577
7800
|
}
|
|
@@ -7579,12 +7802,16 @@ async function loadConfig() {
|
|
|
7579
7802
|
return file;
|
|
7580
7803
|
}
|
|
7581
7804
|
var discoveredConfig = null;
|
|
7805
|
+
function registryDegradeError(error) {
|
|
7806
|
+
return new Error(`Hub registry read failed (${error}) \u2014 board coords could not be discovered; likely transient (cold start, network, or auth blip) \u2014 retry shortly`);
|
|
7807
|
+
}
|
|
7582
7808
|
async function loadConfigOrDiscover() {
|
|
7583
7809
|
if (discoveredConfig) return discoveredConfig;
|
|
7584
7810
|
const floor = await loadConfig();
|
|
7585
7811
|
if (!floor.sagaApiUrl) return stripMutableBoardConfig(floor);
|
|
7586
|
-
const
|
|
7587
|
-
|
|
7812
|
+
const read = await fetchProjectBySlugChecked(await repoSlug(), registryClientDeps(floor));
|
|
7813
|
+
if (!read.ok) throw registryDegradeError(read.error);
|
|
7814
|
+
discoveredConfig = read.project ? boardConfigFromProject(read.project, floor) : stripMutableBoardConfig(floor);
|
|
7588
7815
|
return discoveredConfig;
|
|
7589
7816
|
}
|
|
7590
7817
|
async function loadConfigForRepo(targetRepo2) {
|
|
@@ -7592,9 +7819,10 @@ async function loadConfigForRepo(targetRepo2) {
|
|
|
7592
7819
|
const cwdRepo = await resolveRepo();
|
|
7593
7820
|
if (cwdRepo && targetRepo2.toLowerCase() === cwdRepo.toLowerCase()) return loadConfigOrDiscover();
|
|
7594
7821
|
const floor = await loadConfig();
|
|
7595
|
-
const
|
|
7596
|
-
if (!
|
|
7597
|
-
return
|
|
7822
|
+
const read = await fetchProjectBySlugChecked(slugOf(targetRepo2), registryClientDeps(floor));
|
|
7823
|
+
if (!read.ok) throw registryDegradeError(read.error);
|
|
7824
|
+
if (!read.project) return stripMutableBoardConfig(floor);
|
|
7825
|
+
return boardConfigFromProject(read.project, floor);
|
|
7598
7826
|
}
|
|
7599
7827
|
function repoFromSelector(selector) {
|
|
7600
7828
|
const trimmed = selector.trim();
|
|
@@ -7631,7 +7859,7 @@ function sessionDeps() {
|
|
|
7631
7859
|
},
|
|
7632
7860
|
writePersisted: (id) => persistSession(id),
|
|
7633
7861
|
now: () => /* @__PURE__ */ new Date(),
|
|
7634
|
-
rand: () => (0,
|
|
7862
|
+
rand: () => (0, import_node_crypto2.randomBytes)(4).toString("hex")
|
|
7635
7863
|
};
|
|
7636
7864
|
}
|
|
7637
7865
|
var resolveSessionId = () => resolveSession(sessionDeps());
|
|
@@ -7654,16 +7882,11 @@ async function postCapture(capture, quiet = false) {
|
|
|
7654
7882
|
if (!quiet) console.error("mmi-cli saga: Hub API URL not configured");
|
|
7655
7883
|
return;
|
|
7656
7884
|
}
|
|
7657
|
-
const res = await fetch
|
|
7885
|
+
const res = await fetchWithRetry(fetch, `${cfg.sagaApiUrl}/saga/capture`, {
|
|
7658
7886
|
method: "POST",
|
|
7659
7887
|
headers: await sagaHeaders({ "content-type": "application/json" }),
|
|
7660
|
-
body: JSON.stringify({ ...capture, ...await sagaKey(cfg) })
|
|
7661
|
-
|
|
7662
|
-
// head-write timeout (20s) so a continuity note isn't lost to a slow/cold backend. No client retry:
|
|
7663
|
-
// the capture isn't guaranteed idempotent, so a retry after a server-side-completed write could
|
|
7664
|
-
// duplicate the note. Backend capture-latency root cause tracked in #255.
|
|
7665
|
-
signal: AbortSignal.timeout(2e4)
|
|
7666
|
-
});
|
|
7888
|
+
body: JSON.stringify({ ...capture, ...await sagaKey(cfg) })
|
|
7889
|
+
}, { attempts: 2, timeoutMs: 2e4, retryOn: () => false });
|
|
7667
7890
|
let message = "";
|
|
7668
7891
|
if (!res.ok) {
|
|
7669
7892
|
try {
|
|
@@ -7764,11 +7987,11 @@ async function applyGcPlan(plan2, remote) {
|
|
|
7764
7987
|
}
|
|
7765
7988
|
function resolveVersion() {
|
|
7766
7989
|
try {
|
|
7767
|
-
const manifest = (0,
|
|
7990
|
+
const manifest = (0, import_node_path6.join)(__dirname, "..", "..", ".claude-plugin", "plugin.json");
|
|
7768
7991
|
return JSON.parse((0, import_node_fs5.readFileSync)(manifest, "utf8")).version || "0.0.0";
|
|
7769
7992
|
} catch {
|
|
7770
7993
|
try {
|
|
7771
|
-
const pkg = (0,
|
|
7994
|
+
const pkg = (0, import_node_path6.join)(__dirname, "..", "package.json");
|
|
7772
7995
|
return JSON.parse((0, import_node_fs5.readFileSync)(pkg, "utf8")).version || "0.0.0";
|
|
7773
7996
|
} catch {
|
|
7774
7997
|
return "0.0.0";
|
|
@@ -7777,7 +8000,7 @@ function resolveVersion() {
|
|
|
7777
8000
|
}
|
|
7778
8001
|
function readRepoVersion() {
|
|
7779
8002
|
try {
|
|
7780
|
-
return JSON.parse((0, import_node_fs5.readFileSync)((0,
|
|
8003
|
+
return JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path6.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
7781
8004
|
} catch {
|
|
7782
8005
|
return void 0;
|
|
7783
8006
|
}
|
|
@@ -7874,11 +8097,11 @@ async function runRulesSync(opts, io = consoleIo) {
|
|
|
7874
8097
|
for (const entry of fetched) {
|
|
7875
8098
|
if ("error" in entry) continue;
|
|
7876
8099
|
const { file, source } = entry;
|
|
7877
|
-
const current = (0, import_node_fs5.existsSync)(file) ? await (0,
|
|
8100
|
+
const current = (0, import_node_fs5.existsSync)(file) ? await (0, import_promises2.readFile)(file, "utf8") : null;
|
|
7878
8101
|
if (needsUpdate(source, current)) {
|
|
7879
8102
|
const slash = file.lastIndexOf("/");
|
|
7880
8103
|
if (slash > 0) (0, import_node_fs5.mkdirSync)(file.slice(0, slash), { recursive: true });
|
|
7881
|
-
await (0,
|
|
8104
|
+
await (0, import_promises2.writeFile)(file, normalizeEol(source), "utf8");
|
|
7882
8105
|
changed++;
|
|
7883
8106
|
if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
|
|
7884
8107
|
}
|
|
@@ -7903,9 +8126,9 @@ async function runDocsSync(opts, io = consoleIo) {
|
|
|
7903
8126
|
return null;
|
|
7904
8127
|
}
|
|
7905
8128
|
},
|
|
7906
|
-
localContent: async (f) => (0, import_node_fs5.existsSync)(f) ? await (0,
|
|
8129
|
+
localContent: async (f) => (0, import_node_fs5.existsSync)(f) ? await (0, import_promises2.readFile)(f, "utf8") : null,
|
|
7907
8130
|
writeDoc: async (f, c) => {
|
|
7908
|
-
await (0,
|
|
8131
|
+
await (0, import_promises2.writeFile)(f, c, "utf8");
|
|
7909
8132
|
}
|
|
7910
8133
|
});
|
|
7911
8134
|
for (const f of result.updated) io.log(`mmi-cli docs: updated ${f} (from origin/${def})`);
|
|
@@ -7917,7 +8140,7 @@ docs.command("sync").option("--quiet", "stay silent unless something changed or
|
|
|
7917
8140
|
var saga = program2.command("saga").description("per-session continuity");
|
|
7918
8141
|
async function runNote(summary, o) {
|
|
7919
8142
|
const [sha, key] = await Promise.all([gitOut(["rev-parse", "--short", "HEAD"]), sagaKey(await loadConfig())]);
|
|
7920
|
-
const capture = buildNoteCapture(summary, o, (0,
|
|
8143
|
+
const capture = buildNoteCapture(summary, o, (0, import_node_crypto2.randomUUID)(), { sha: sha || void 0, branch: key.branch });
|
|
7921
8144
|
await postCapture(capture);
|
|
7922
8145
|
}
|
|
7923
8146
|
saga.command("note <summary>").description("record a one-line structured note into your saga (the per-turn capture)").option("--next <text>", 'set "where I left off" (NEXT)').option("--decision <text>", "append a verbatim decision").option("--queue-add <text>", "add a worklist item").option("--queue-done <n>", "mark worklist item N done").option("--verified", "mark this claim as checked against source (state: verified, else asserted)").option("--diagnostic", "isolate a probe write (state: diagnostic, source: probe) \u2014 never resume/LAST 5").option("--supersedes <key>", "retire prior decisions matching an evidence key (pr:N | file:path)").option("--anchor <intent>", "set the sprint North-Star (write-protected; needs --anchor-force to change)").option("--anchor-force", "overwrite an existing anchor").action((summary, o) => runNote(summary, o));
|
|
@@ -7931,7 +8154,7 @@ async function runSagaShow(opts, io = consoleIo) {
|
|
|
7931
8154
|
try {
|
|
7932
8155
|
const key = await sagaKey(cfg);
|
|
7933
8156
|
const qs = opts.latestAnywhere ? "scope=anywhere" : new URLSearchParams({ project: key.project, branch: key.branch }).toString();
|
|
7934
|
-
const res = await fetch
|
|
8157
|
+
const res = await fetchWithRetry(fetch, `${cfg.sagaApiUrl}/saga/head?${qs}`, { headers: await sagaHeaders() }, { attempts: 2, timeoutMs: 3e3 });
|
|
7935
8158
|
if (res.ok) {
|
|
7936
8159
|
io.log(resumeCue());
|
|
7937
8160
|
return io.log(await res.text());
|
|
@@ -7939,7 +8162,7 @@ async function runSagaShow(opts, io = consoleIo) {
|
|
|
7939
8162
|
if (!opts.quiet) io.log(`saga show failed: HTTP ${res.status}`);
|
|
7940
8163
|
} catch (e) {
|
|
7941
8164
|
if (!opts.quiet) {
|
|
7942
|
-
const reason = e.name === "TimeoutError" ? "backend unreachable (timed out after
|
|
8165
|
+
const reason = e.name === "TimeoutError" ? "backend unreachable (timed out after 2 attempts)" : e.message;
|
|
7943
8166
|
io.err(`saga show: ${reason} \u2014 continuing without saga; diagnose with \`mmi-cli saga health --json\``);
|
|
7944
8167
|
}
|
|
7945
8168
|
}
|
|
@@ -7948,7 +8171,7 @@ saga.command("show").option("--quiet", "no-op silently when unconfigured/unreach
|
|
|
7948
8171
|
saga.command("capture").option("--quiet", "capture silently (for the Stop hook)").description("per-turn deterministic capture (Stop hook): turn boundary + current sha").action(async (opts) => {
|
|
7949
8172
|
const hook = parseHookInput(await readStdin());
|
|
7950
8173
|
if (hook.session_id) persistSession(hook.session_id);
|
|
7951
|
-
await postCapture({ event: "stop", id: (0,
|
|
8174
|
+
await postCapture({ event: "stop", id: (0, import_node_crypto2.randomUUID)(), source: "hook", sha: await gitOut(["rev-parse", "--short", "HEAD"]), surface: agentSurface() }, opts.quiet ?? false);
|
|
7952
8175
|
});
|
|
7953
8176
|
saga.command("session").option("--quiet", "silent (for the SessionStart hook)").description("persist the harness session id for this repo (SessionStart hook)").action(async () => {
|
|
7954
8177
|
const hook = parseHookInput(await readStdin());
|
|
@@ -8000,7 +8223,7 @@ saga.command("key").option("--json", "machine-readable output").description("pri
|
|
|
8000
8223
|
});
|
|
8001
8224
|
async function probeBackend(url) {
|
|
8002
8225
|
try {
|
|
8003
|
-
const res = await fetch
|
|
8226
|
+
const res = await fetchWithRetry(fetch, `${url}/saga/head`, { headers: await sagaHeaders() }, { attempts: 3, timeoutMs: 4e3 });
|
|
8004
8227
|
let message = "";
|
|
8005
8228
|
try {
|
|
8006
8229
|
const body = await res.clone().json();
|
|
@@ -8098,12 +8321,17 @@ kb.command("list [prefix]").description("list KB document paths (optionally unde
|
|
|
8098
8321
|
}
|
|
8099
8322
|
});
|
|
8100
8323
|
async function ghCreate(args) {
|
|
8324
|
+
const swapped = await bodyArgsViaFile(args);
|
|
8101
8325
|
try {
|
|
8102
|
-
const { stdout } = await execFileP4("gh", args);
|
|
8326
|
+
const { stdout } = await execFileP4("gh", swapped.args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
8103
8327
|
return parseCreatedUrl(stdout);
|
|
8104
8328
|
} catch (e) {
|
|
8329
|
+
await swapped.cleanup();
|
|
8105
8330
|
const err = e;
|
|
8106
|
-
|
|
8331
|
+
const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
|
|
8332
|
+
fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
8333
|
+
} finally {
|
|
8334
|
+
await swapped.cleanup();
|
|
8107
8335
|
}
|
|
8108
8336
|
}
|
|
8109
8337
|
async function ghJson(args, timeout = 1e4) {
|
|
@@ -8123,7 +8351,13 @@ async function resolveRepo(repo) {
|
|
|
8123
8351
|
}
|
|
8124
8352
|
async function attachToProject(issueNumber, repo, priority) {
|
|
8125
8353
|
const targetRepo2 = await resolveRepo(repo);
|
|
8126
|
-
|
|
8354
|
+
let cfg;
|
|
8355
|
+
try {
|
|
8356
|
+
cfg = await loadConfigForRepo(targetRepo2);
|
|
8357
|
+
} catch (e) {
|
|
8358
|
+
console.error(`issue create: board attach skipped \u2014 ${e.message}`);
|
|
8359
|
+
return void 0;
|
|
8360
|
+
}
|
|
8127
8361
|
if (!cfg.projectId) {
|
|
8128
8362
|
console.error(`issue create: board attach skipped \u2014 no Hub registry board META for ${targetRepo2 ?? "current repo"}; run \`mmi-cli project get ${targetRepo2 ?? "<owner/repo>"}\` and backfill board coords`);
|
|
8129
8363
|
return void 0;
|
|
@@ -8134,7 +8368,7 @@ async function attachToProject(issueNumber, repo, priority) {
|
|
|
8134
8368
|
const { stdout: idOut } = await execFileP4("gh", viewArgs, { timeout: 1e4 });
|
|
8135
8369
|
const contentId = idOut.trim();
|
|
8136
8370
|
if (!contentId) throw new Error("could not resolve issue node id");
|
|
8137
|
-
const { stdout } = await execFileP4("gh", buildAddToProjectArgs(cfg.projectId, contentId), { timeout:
|
|
8371
|
+
const { stdout } = await execFileP4("gh", buildAddToProjectArgs(cfg.projectId, contentId), { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
8138
8372
|
const projectItemId = parseAddedItemId(stdout);
|
|
8139
8373
|
if (projectItemId && priority) {
|
|
8140
8374
|
try {
|
|
@@ -8226,6 +8460,26 @@ async function withPlan(quiet, run, io = consoleIo) {
|
|
|
8226
8460
|
}
|
|
8227
8461
|
await run(makePlanDeps(cfg, io));
|
|
8228
8462
|
}
|
|
8463
|
+
async function gatherRelevanceSignals() {
|
|
8464
|
+
const branch = await gitOut(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "") || void 0;
|
|
8465
|
+
const changed = (await gitOut(["diff", "--name-only", "HEAD"]).catch(() => "")).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
8466
|
+
const signals = { branch, changedFiles: changed.length ? changed : void 0 };
|
|
8467
|
+
const issueNum = branch?.match(/\b(\d{2,})\b/)?.[1];
|
|
8468
|
+
if (issueNum) {
|
|
8469
|
+
try {
|
|
8470
|
+
const { stdout } = await execFileP4(
|
|
8471
|
+
"gh",
|
|
8472
|
+
["issue", "view", issueNum, "--json", "title,labels", "--jq", "{title:.title,labels:[.labels[].name]}"],
|
|
8473
|
+
{ timeout: 1e4 }
|
|
8474
|
+
);
|
|
8475
|
+
const j = JSON.parse(stdout);
|
|
8476
|
+
if (j.title) signals.issueTitle = j.title;
|
|
8477
|
+
if (j.labels?.length) signals.issueLabels = j.labels;
|
|
8478
|
+
} catch {
|
|
8479
|
+
}
|
|
8480
|
+
}
|
|
8481
|
+
return signals;
|
|
8482
|
+
}
|
|
8229
8483
|
function registerNorthStarCommands(cmd) {
|
|
8230
8484
|
cmd.command("push <slug>").description("push a local North Star plan (plans/<slug>.md) to the server").option("--project <name>", "override the project key").option("--force", "overwrite the remote even if it changed since your last sync").action((slug, o) => withPlan(false, async (d) => {
|
|
8231
8485
|
const ok = await planPush(d, slug, o);
|
|
@@ -8236,6 +8490,10 @@ function registerNorthStarCommands(cmd) {
|
|
|
8236
8490
|
if (!ok) process.exitCode = 1;
|
|
8237
8491
|
}));
|
|
8238
8492
|
cmd.command("list").description("list your North Star plans (cross-device)").option("--project <name>", "filter by project").option("--json", "machine-readable output").option("--quiet", "silent when unconfigured/empty/unreachable (SessionStart hook)").action((o) => withPlan(o.quiet ?? false, (d) => planList(d, o)));
|
|
8493
|
+
cmd.command("relevant").description("list North Stars relevant to your current task (branch + claimed issue + changed files)").option("--project <name>", "override the project key").option("--all", "include superseded/graduated and recent non-matching plans").option("--limit <n>", "max results (default 5)").action((o) => withPlan(false, async (d) => {
|
|
8494
|
+
const signals = await gatherRelevanceSignals();
|
|
8495
|
+
await relevantPlans(d, signals, { project: o.project, includeAll: o.all, limit: o.limit ? Number(o.limit) : void 0 });
|
|
8496
|
+
}));
|
|
8239
8497
|
cmd.command("open <slug>").description("pull if needed, then open plans/<slug>.md in $EDITOR").option("--project <name>", "override the project key").action(
|
|
8240
8498
|
(slug, o) => withPlan(false, async (d) => {
|
|
8241
8499
|
const ok = await planPull(d, slug, { project: o.project });
|
|
@@ -8346,7 +8604,8 @@ tenant.command("control <owner/repo> <stage> <action>").description("run bounded
|
|
|
8346
8604
|
async function v2ReadinessDeps(cfg) {
|
|
8347
8605
|
const reg = registryClientDeps(cfg);
|
|
8348
8606
|
return {
|
|
8349
|
-
|
|
8607
|
+
// Checked read (#727/#733): the doctor distinguishes a FAILED read (degraded report) from a 404.
|
|
8608
|
+
getProject: (slug) => fetchProjectBySlugChecked(slug, reg),
|
|
8350
8609
|
hasDeployCoords: async (slug, stage2) => {
|
|
8351
8610
|
const status = await fetchDeployStatusBySlug(slug, reg);
|
|
8352
8611
|
return Boolean(status?.stages[stage2]);
|
|
@@ -8359,11 +8618,10 @@ async function v2ReadinessDeps(cfg) {
|
|
|
8359
8618
|
const apiUrl = cfg.sagaApiUrl;
|
|
8360
8619
|
if (!apiUrl) throw new Error("Hub API URL not configured \u2014 cannot verify secret names (set sagaApiUrl)");
|
|
8361
8620
|
const qs = new URLSearchParams({ repo: targetRepo2 }).toString();
|
|
8362
|
-
const res = await fetch
|
|
8621
|
+
const res = await fetchWithRetry(fetch, `${apiUrl.replace(/\/$/, "")}/secrets/list?${qs}`, {
|
|
8363
8622
|
method: "GET",
|
|
8364
|
-
headers: await sagaHeaders()
|
|
8365
|
-
|
|
8366
|
-
});
|
|
8623
|
+
headers: await sagaHeaders()
|
|
8624
|
+
}, { attempts: 2, timeoutMs: 5e3 });
|
|
8367
8625
|
if (!res.ok) throw new Error(`secrets list failed for ${targetRepo2}: HTTP ${res.status}`);
|
|
8368
8626
|
const body = await res.json();
|
|
8369
8627
|
return (body.secrets ?? []).map((s) => s.key).filter(Boolean);
|
|
@@ -8377,15 +8635,25 @@ async function updateV2ReadinessIssue(repo, report, healed) {
|
|
|
8377
8635
|
const issues = JSON.parse(list.stdout || "[]");
|
|
8378
8636
|
const existing = issues.find((i) => i.title.toLowerCase().includes("v2 readiness"));
|
|
8379
8637
|
if (!existing) {
|
|
8380
|
-
const
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8638
|
+
const create = await bodyArgsViaFile(["issue", "create", "--repo", repo, "--title", title, "--body", freshBody, "--label", "feature"]);
|
|
8639
|
+
try {
|
|
8640
|
+
const created = await execFileP4("gh", create.args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
8641
|
+
const url = created.stdout.trim();
|
|
8642
|
+
const number = Number(url.match(/\/issues\/(\d+)$/)?.[1] ?? 0);
|
|
8643
|
+
return { number, url, action: "created" };
|
|
8644
|
+
} finally {
|
|
8645
|
+
await create.cleanup();
|
|
8646
|
+
}
|
|
8384
8647
|
}
|
|
8385
8648
|
const view = await execFileP4("gh", ["issue", "view", String(existing.number), "--repo", repo, "--json", "body,url", "--jq", "{body:.body,url:.url}"], { timeout: 2e4 });
|
|
8386
8649
|
const current = JSON.parse(view.stdout || "{}");
|
|
8387
8650
|
const nextBody = renderReadinessIssueBody(current.body ?? "", report, { healed });
|
|
8388
|
-
await
|
|
8651
|
+
const edit = await bodyArgsViaFile(["issue", "edit", String(existing.number), "--repo", repo, "--body", nextBody]);
|
|
8652
|
+
try {
|
|
8653
|
+
await execFileP4("gh", edit.args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
8654
|
+
} finally {
|
|
8655
|
+
await edit.cleanup();
|
|
8656
|
+
}
|
|
8389
8657
|
return { number: existing.number, url: current.url ?? `https://github.com/${repo}/issues/${existing.number}`, action: "updated" };
|
|
8390
8658
|
}
|
|
8391
8659
|
var project = program2.command("project").description("the DDB org registry \u2014 list/get projects (any member); attest is project-admin; set is master-only");
|
|
@@ -8487,7 +8755,7 @@ project.command("attest [owner/repo]").description("attest this repo's app-owned
|
|
|
8487
8755
|
const res = await attestAppGaps(slugOf(target), repo, registryClientDeps(cfg));
|
|
8488
8756
|
reportWrite("project attest", res);
|
|
8489
8757
|
});
|
|
8490
|
-
project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project META (idempotent merge; defaults to the current repo; no clobber of unspecified fields)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--var <KEY=VALUE...>",
|
|
8758
|
+
project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project META (idempotent merge; defaults to the current repo; no clobber of unspecified fields)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--var <KEY=VALUE...>", 'META field to set (repeatable): name, division, projectId, branch, wikiRepo, vaultPath, kbPointer, requiredRuntimeSecrets (JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]})').option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
8491
8759
|
const cfg = await loadConfig();
|
|
8492
8760
|
let target;
|
|
8493
8761
|
try {
|
|
@@ -8591,7 +8859,7 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
|
|
|
8591
8859
|
let priority;
|
|
8592
8860
|
let body;
|
|
8593
8861
|
try {
|
|
8594
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
8862
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises2.readFile, readStdin });
|
|
8595
8863
|
priority = normalizePriority(o.priority);
|
|
8596
8864
|
args = buildIssueArgs({ type: o.type, title: o.title, body, priority, repo: o.repo, labels: o.label });
|
|
8597
8865
|
} catch (e) {
|
|
@@ -8601,7 +8869,7 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
|
|
|
8601
8869
|
const la = ["label", "create", label, "--color", "ededed"];
|
|
8602
8870
|
if (o.repo) la.push("--repo", o.repo);
|
|
8603
8871
|
try {
|
|
8604
|
-
await execFileP4("gh", la, { timeout:
|
|
8872
|
+
await execFileP4("gh", la, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
8605
8873
|
} catch {
|
|
8606
8874
|
}
|
|
8607
8875
|
}
|
|
@@ -8641,7 +8909,7 @@ issue.command("discover-related").description("find related issues for an existi
|
|
|
8641
8909
|
"comments"
|
|
8642
8910
|
]);
|
|
8643
8911
|
if (viewed.comments.some((comment) => comment.body.includes(relatedMarker(number)))) return;
|
|
8644
|
-
await execFileP4("gh", ["issue", "comment", String(number), "--repo", repo, "--body", buildRelatedComment(number, candidates)], { timeout:
|
|
8912
|
+
await execFileP4("gh", ["issue", "comment", String(number), "--repo", repo, "--body", buildRelatedComment(number, candidates)], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
8645
8913
|
} catch {
|
|
8646
8914
|
}
|
|
8647
8915
|
});
|
|
@@ -8652,7 +8920,7 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
8652
8920
|
const targetRepo2 = o.repo ?? HUB_REPO;
|
|
8653
8921
|
const sourceRepo = await resolveRepo(void 0);
|
|
8654
8922
|
try {
|
|
8655
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
8923
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises2.readFile, readStdin });
|
|
8656
8924
|
priority = normalizePriority(o.priority);
|
|
8657
8925
|
args = buildIssueArgs({
|
|
8658
8926
|
type: o.type,
|
|
@@ -8687,7 +8955,7 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
8687
8955
|
const dup = findDuplicateReport({ title: o.title, body }, openReports);
|
|
8688
8956
|
if (dup) {
|
|
8689
8957
|
try {
|
|
8690
|
-
await execFileP4("gh", ["issue", "comment", String(dup.number), "--repo", targetRepo2, "--body", buildDupComment(dup.number, body, sourceRepo)], { timeout:
|
|
8958
|
+
await execFileP4("gh", ["issue", "comment", String(dup.number), "--repo", targetRepo2, "--body", buildDupComment(dup.number, body, sourceRepo)], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
8691
8959
|
} catch (e) {
|
|
8692
8960
|
const err = e;
|
|
8693
8961
|
return fail(`report: duplicate of #${dup.number} (${dup.url}) but the +1 comment failed: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
@@ -8696,7 +8964,7 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
8696
8964
|
}
|
|
8697
8965
|
}
|
|
8698
8966
|
try {
|
|
8699
|
-
await execFileP4("gh", ["label", "create", REPORT_LABEL, "--color", "ededed", "--repo", targetRepo2], { timeout:
|
|
8967
|
+
await execFileP4("gh", ["label", "create", REPORT_LABEL, "--color", "ededed", "--repo", targetRepo2], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
8700
8968
|
} catch {
|
|
8701
8969
|
}
|
|
8702
8970
|
const created = await ghCreate(args);
|
|
@@ -8704,8 +8972,14 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
8704
8972
|
console.log(JSON.stringify({ ...created, deduped: false, label: REPORT_LABEL, priority, projectItemId }));
|
|
8705
8973
|
});
|
|
8706
8974
|
var pr = program2.command("pr").description("pull requests \u2014 reliable create with structured output");
|
|
8707
|
-
pr.command("create").description("create a PR and print {number,url} JSON").requiredOption("--title <title>", "PR title").
|
|
8708
|
-
|
|
8975
|
+
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) => {
|
|
8976
|
+
let body;
|
|
8977
|
+
try {
|
|
8978
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises2.readFile, readStdin });
|
|
8979
|
+
} catch (e) {
|
|
8980
|
+
return fail(`pr create: ${e.message}`);
|
|
8981
|
+
}
|
|
8982
|
+
const created = await ghCreate(buildPrArgs({ title: o.title, body, base: o.base, head: o.head, repo: o.repo }));
|
|
8709
8983
|
console.log(JSON.stringify(created));
|
|
8710
8984
|
});
|
|
8711
8985
|
async function remoteBranchExists(branch) {
|
|
@@ -8728,12 +9002,14 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
8728
9002
|
const remoteBefore = repoArgs.length ? void 0 : await remoteBranchExists(headRef);
|
|
8729
9003
|
let remoteDeleteAttempted = false;
|
|
8730
9004
|
let remoteNotAttemptedReason = repoArgs.length ? "repo-option" : void 0;
|
|
8731
|
-
await execFileP4("gh", ["pr", "merge", number, ...repoArgs, method, "--delete-branch"], { timeout:
|
|
9005
|
+
await execFileP4("gh", ["pr", "merge", number, ...repoArgs, method, "--delete-branch"], { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e) => {
|
|
8732
9006
|
const message = String(e.message || "");
|
|
8733
9007
|
if (/already been merged/i.test(message)) {
|
|
8734
9008
|
remoteNotAttemptedReason = "pr-already-merged";
|
|
8735
9009
|
return;
|
|
8736
9010
|
}
|
|
9011
|
+
const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
|
|
9012
|
+
if (note) throw new Error(`gh pr merge ${number}: ${note}`);
|
|
8737
9013
|
if (!/used by worktree|cannot delete branch/i.test(message)) throw e;
|
|
8738
9014
|
});
|
|
8739
9015
|
if (!remoteNotAttemptedReason) remoteDeleteAttempted = true;
|
|
@@ -8874,9 +9150,9 @@ function stageKeepAlive() {
|
|
|
8874
9150
|
return setTimeout(() => void 0, 5 * 60 * 1e3);
|
|
8875
9151
|
}
|
|
8876
9152
|
program2.command("port-range <repo>").description("assign (idempotently) + print the repo's local stage port block via the atomic ORG#config.portCursor allocator (committed-file fallback)").option("--json", "machine-readable output").action(async (repo, o) => {
|
|
8877
|
-
const path2 = (0,
|
|
9153
|
+
const path2 = (0, import_node_path6.join)(process.cwd(), "infra", "port-ranges.json");
|
|
8878
9154
|
const allocate = async (seed) => {
|
|
8879
|
-
const { stdout } = await execFileP4("node", [(0,
|
|
9155
|
+
const { stdout } = await execFileP4("node", [(0, import_node_path6.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
|
|
8880
9156
|
const parsed = JSON.parse(stdout);
|
|
8881
9157
|
if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
|
|
8882
9158
|
return parsed.range;
|
|
@@ -9204,7 +9480,7 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
9204
9480
|
var isWin = process.platform === "win32";
|
|
9205
9481
|
var installedPluginsPath = (surface = detectSurface(process.env)) => {
|
|
9206
9482
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
9207
|
-
return (0,
|
|
9483
|
+
return (0, import_node_path6.join)((0, import_node_os2.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
9208
9484
|
};
|
|
9209
9485
|
function readInstalledPlugins() {
|
|
9210
9486
|
try {
|
|
@@ -9215,7 +9491,7 @@ function readInstalledPlugins() {
|
|
|
9215
9491
|
}
|
|
9216
9492
|
function readClaudeSettings() {
|
|
9217
9493
|
try {
|
|
9218
|
-
return JSON.parse((0, import_node_fs5.readFileSync)((0,
|
|
9494
|
+
return JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path6.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
|
|
9219
9495
|
} catch {
|
|
9220
9496
|
return null;
|
|
9221
9497
|
}
|
|
@@ -9261,14 +9537,14 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
9261
9537
|
}
|
|
9262
9538
|
function mmiPluginCacheRootSnapshots() {
|
|
9263
9539
|
const roots = [
|
|
9264
|
-
{ surface: "claude", root: (0,
|
|
9265
|
-
{ surface: "codex", root: (0,
|
|
9540
|
+
{ surface: "claude", root: (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".claude", "plugins", "cache", "mmi", "mmi") },
|
|
9541
|
+
{ surface: "codex", root: (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".codex", "plugins", "cache", "mmi", "mmi") }
|
|
9266
9542
|
];
|
|
9267
9543
|
return roots.flatMap(({ surface, root }) => {
|
|
9268
9544
|
try {
|
|
9269
9545
|
const entries = (0, import_node_fs5.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
|
|
9270
9546
|
name: entry.name,
|
|
9271
|
-
path: (0,
|
|
9547
|
+
path: (0, import_node_path6.join)(root, entry.name),
|
|
9272
9548
|
isDirectory: entry.isDirectory()
|
|
9273
9549
|
}));
|
|
9274
9550
|
return [{ surface, root, entries }];
|
|
@@ -9291,7 +9567,7 @@ function quarantinePluginCacheDirs(plan2) {
|
|
|
9291
9567
|
try {
|
|
9292
9568
|
if (!(0, import_node_fs5.existsSync)(move.from)) continue;
|
|
9293
9569
|
const target = uniqueQuarantineTarget(move.to);
|
|
9294
|
-
(0, import_node_fs5.mkdirSync)((0,
|
|
9570
|
+
(0, import_node_fs5.mkdirSync)((0, import_node_path6.dirname)(target), { recursive: true });
|
|
9295
9571
|
(0, import_node_fs5.renameSync)(move.from, target);
|
|
9296
9572
|
moved += 1;
|
|
9297
9573
|
} catch {
|
|
@@ -9299,7 +9575,7 @@ function quarantinePluginCacheDirs(plan2) {
|
|
|
9299
9575
|
}
|
|
9300
9576
|
return moved;
|
|
9301
9577
|
}
|
|
9302
|
-
var gitignorePath = () => (0,
|
|
9578
|
+
var gitignorePath = () => (0, import_node_path6.join)(process.cwd(), ".gitignore");
|
|
9303
9579
|
function readGitignore() {
|
|
9304
9580
|
try {
|
|
9305
9581
|
return (0, import_node_fs5.readFileSync)(gitignorePath(), "utf8");
|
|
@@ -9462,24 +9738,22 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
9462
9738
|
${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
|
|
9463
9739
|
}
|
|
9464
9740
|
program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, Hub API default, plugin git clone, plugin install record, .gitignore managed block, plugin config drift, installed plugin version) and print fixes").option("--banner", "one-line resume summary; silent when all gates pass").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output").action((opts) => runDoctor(opts));
|
|
9465
|
-
program2.command("session-start").description("run the SessionStart verbs (rules
|
|
9741
|
+
program2.command("session-start").description("run the SessionStart verbs (rules sync, saga session+show, saga health, doctor) in one process; docs sync runs detached").action(async () => {
|
|
9466
9742
|
try {
|
|
9467
9743
|
const hook = parseHookInput(await readStdin());
|
|
9468
9744
|
if (hook.session_id) persistSession(hook.session_id);
|
|
9469
9745
|
} catch (e) {
|
|
9470
9746
|
console.error(`[mmi-hook] saga session failed: ${e.message}`);
|
|
9471
9747
|
}
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
9479
|
-
const sequential = [
|
|
9480
|
-
{ name: "doctor", run: (io) => runDoctor({ banner: true }, io) }
|
|
9481
|
-
];
|
|
9748
|
+
spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process6.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
9749
|
+
const { parallel, sequential } = buildSessionStartPlan({
|
|
9750
|
+
rulesSync: (io) => runRulesSync({ quiet: true }, io),
|
|
9751
|
+
sagaShow: (io) => runSagaShow({ quiet: true }, io),
|
|
9752
|
+
sagaHealth: (io) => runSagaHealth({ banner: true, quiet: true }, io),
|
|
9753
|
+
doctor: (io) => runDoctor({ banner: true }, io)
|
|
9754
|
+
});
|
|
9482
9755
|
await runSessionStart(parallel, sequential, consoleIo);
|
|
9756
|
+
consoleIo.log(northstarPointer());
|
|
9483
9757
|
});
|
|
9484
9758
|
function fail(msg) {
|
|
9485
9759
|
console.error(`mmi-cli ${msg}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.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",
|