@mutmutco/cli 0.9.0 → 0.10.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 +392 -43
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -3052,6 +3052,18 @@ function needsUpdate(source, current) {
|
|
|
3052
3052
|
function isRulesSource(orgRulesSource) {
|
|
3053
3053
|
return orgRulesSource === "self";
|
|
3054
3054
|
}
|
|
3055
|
+
function rulesSourceAuthHeaders(sourceUrl, token) {
|
|
3056
|
+
if (!token) return void 0;
|
|
3057
|
+
try {
|
|
3058
|
+
const host = new URL(sourceUrl).hostname.toLowerCase();
|
|
3059
|
+
if (host === "raw.githubusercontent.com" || host === "api.github.com") {
|
|
3060
|
+
return { Authorization: `Bearer ${token}` };
|
|
3061
|
+
}
|
|
3062
|
+
} catch {
|
|
3063
|
+
return void 0;
|
|
3064
|
+
}
|
|
3065
|
+
return void 0;
|
|
3066
|
+
}
|
|
3055
3067
|
|
|
3056
3068
|
// src/saga-capture.ts
|
|
3057
3069
|
function parseHookInput(stdin) {
|
|
@@ -3066,7 +3078,7 @@ function parseHookInput(stdin) {
|
|
|
3066
3078
|
// src/index.ts
|
|
3067
3079
|
var import_node_child_process4 = require("node:child_process");
|
|
3068
3080
|
var import_node_util3 = require("node:util");
|
|
3069
|
-
var
|
|
3081
|
+
var import_node_path4 = require("node:path");
|
|
3070
3082
|
|
|
3071
3083
|
// src/saga-head-maintainer.ts
|
|
3072
3084
|
var import_node_child_process = require("node:child_process");
|
|
@@ -3099,13 +3111,18 @@ function markHeadRun(path, now = Date.now()) {
|
|
|
3099
3111
|
}
|
|
3100
3112
|
function headPrompt(state) {
|
|
3101
3113
|
return [
|
|
3102
|
-
"You maintain
|
|
3103
|
-
"
|
|
3104
|
-
"
|
|
3105
|
-
"
|
|
3106
|
-
"
|
|
3114
|
+
"You maintain ONE durable slot of a work-session: PINNED (things worth remembering). Given the CURRENT",
|
|
3115
|
+
"HEAD and the recent TRANSCRIPT + DECISIONS, return an updated PINNED only. Keep it tight and concrete;",
|
|
3116
|
+
"keep anything the user pinned; never invent; preserve Turkish characters (\xE7 \u011F \u0131 \u0130 \xF6 \u015F \xFC) exactly. Do",
|
|
3117
|
+
"NOT manage next or the checklist \u2014 the note path owns those. The ANCHOR is the read-only North-Star \u2014",
|
|
3118
|
+
"NEVER change it. Never restate an unverified artifact-claim (a named file, PR, flag, or board state)",
|
|
3107
3119
|
"as settled fact \u2014 keep it as the belief it was recorded as.",
|
|
3108
|
-
|
|
3120
|
+
"You MAY also propose supersessions: each DECISION is shown with a stable 0-based index. Propose a",
|
|
3121
|
+
"supersession ONLY for a NEWER decision that directly contradicts/replaces an OLDER one where neither",
|
|
3122
|
+
"already carries a supersededBy. HIGH PRECISION \u2014 propose ONLY when you are confident the older claim",
|
|
3123
|
+
"is now false or obsolete; the newer decision's timestamp MUST be later than the older's (newer-supersedes-",
|
|
3124
|
+
"older only, never the reverse). When unsure, propose nothing. NEVER touch the anchor, next, or checklist.",
|
|
3125
|
+
'Output ONLY a JSON object: {"pinned":[string],"supersede":[{"older":int,"newer":int,"reason":string}]}.',
|
|
3109
3126
|
"",
|
|
3110
3127
|
"CURRENT HEAD:",
|
|
3111
3128
|
JSON.stringify(state.head ?? {}, null, 2),
|
|
@@ -3114,7 +3131,7 @@ function headPrompt(state) {
|
|
|
3114
3131
|
JSON.stringify(state.actionLog ?? []),
|
|
3115
3132
|
"",
|
|
3116
3133
|
"DECISIONS:",
|
|
3117
|
-
(state.decisions ?? []).map((d) =>
|
|
3134
|
+
(state.decisions ?? []).map((d, i) => `[${i}] ${d}`).join("\n") || "(none)"
|
|
3118
3135
|
].join("\n");
|
|
3119
3136
|
}
|
|
3120
3137
|
function parseHeadUpdate(raw) {
|
|
@@ -3128,8 +3145,15 @@ function parseHeadUpdate(raw) {
|
|
|
3128
3145
|
}
|
|
3129
3146
|
if (!obj || typeof obj !== "object") return null;
|
|
3130
3147
|
const u = {};
|
|
3131
|
-
if (typeof obj.goal === "string") u.goal = obj.goal;
|
|
3132
3148
|
if (Array.isArray(obj.pinned)) u.pinned = obj.pinned.filter((x) => typeof x === "string");
|
|
3149
|
+
if (Array.isArray(obj.supersede)) {
|
|
3150
|
+
const supersede = obj.supersede.filter((e) => {
|
|
3151
|
+
if (!e || typeof e !== "object") return false;
|
|
3152
|
+
const { older, newer, reason } = e;
|
|
3153
|
+
return Number.isInteger(older) && older >= 0 && Number.isInteger(newer) && newer >= 0 && older !== newer && typeof reason === "string" && reason.length > 0;
|
|
3154
|
+
}).map((e) => ({ older: e.older, newer: e.newer, reason: e.reason }));
|
|
3155
|
+
if (supersede.length) u.supersede = supersede;
|
|
3156
|
+
}
|
|
3133
3157
|
return Object.keys(u).length ? u : null;
|
|
3134
3158
|
}
|
|
3135
3159
|
async function runHeadEngine(prompt, timeoutMs = HEAD_ENGINE_TIMEOUT_MS) {
|
|
@@ -3246,11 +3270,12 @@ function buildNoteCapture(summary, o, id, evidence) {
|
|
|
3246
3270
|
const queueOp = o.queueAdd ? { op: "add", text: o.queueAdd } : o.queueDone != null ? { op: "done", index: Number(o.queueDone) } : void 0;
|
|
3247
3271
|
const state = o.diagnostic ? "diagnostic" : o.verified ? "verified" : "asserted";
|
|
3248
3272
|
const source = o.diagnostic ? "probe" : "note";
|
|
3249
|
-
const
|
|
3250
|
-
if (evidence.sha)
|
|
3251
|
-
if (evidence.branch)
|
|
3252
|
-
if (evidence.pr)
|
|
3253
|
-
if (evidence.file)
|
|
3273
|
+
const ev = {};
|
|
3274
|
+
if (evidence.sha) ev.sha = evidence.sha;
|
|
3275
|
+
if (evidence.branch) ev.branch = evidence.branch;
|
|
3276
|
+
if (evidence.pr) ev.pr = evidence.pr;
|
|
3277
|
+
if (evidence.file) ev.file = evidence.file;
|
|
3278
|
+
const anchor = o.anchor ? { intent: o.anchor, setAt: (/* @__PURE__ */ new Date()).toISOString() } : void 0;
|
|
3254
3279
|
return {
|
|
3255
3280
|
event: "note",
|
|
3256
3281
|
id,
|
|
@@ -3260,14 +3285,17 @@ function buildNoteCapture(summary, o, id, evidence) {
|
|
|
3260
3285
|
queueOp,
|
|
3261
3286
|
state,
|
|
3262
3287
|
source,
|
|
3263
|
-
evidence: Object.keys(
|
|
3264
|
-
surface: process.env.MMI_AGENT_SURFACE || "claude"
|
|
3288
|
+
evidence: Object.keys(ev).length ? ev : void 0,
|
|
3289
|
+
surface: process.env.MMI_AGENT_SURFACE || "claude",
|
|
3290
|
+
supersedes: o.supersedes,
|
|
3291
|
+
anchor,
|
|
3292
|
+
anchorForce: o.anchorForce || void 0
|
|
3265
3293
|
};
|
|
3266
3294
|
}
|
|
3267
3295
|
|
|
3268
3296
|
// src/version-lag.ts
|
|
3269
3297
|
var VERSION_LABEL = "installed plugin/adapter cache freshness";
|
|
3270
|
-
var VERSION_FIX = "
|
|
3298
|
+
var VERSION_FIX = "update the MMI plugin via /plugin; standalone npm CLI: npm install -g @mutmutco/cli@latest";
|
|
3271
3299
|
function parseVersion(v) {
|
|
3272
3300
|
return v.replace(/^v/, "").split(/[.-]/).slice(0, 3).map((part) => {
|
|
3273
3301
|
const n = Number.parseInt(part, 10);
|
|
@@ -3824,31 +3852,31 @@ function parseWorktreePorcelain(stdout) {
|
|
|
3824
3852
|
}
|
|
3825
3853
|
return out;
|
|
3826
3854
|
}
|
|
3827
|
-
function formatGcPlan(
|
|
3855
|
+
function formatGcPlan(plan2, apply) {
|
|
3828
3856
|
const lines = [`mmi-cli gc: ${apply ? "apply" : "dry-run"}`];
|
|
3829
|
-
if (!
|
|
3830
|
-
if (
|
|
3857
|
+
if (!plan2.branches.length && !plan2.trackingRefs.length) lines.push("nothing to clean");
|
|
3858
|
+
if (plan2.branches.length) {
|
|
3831
3859
|
lines.push("local branches:");
|
|
3832
|
-
for (const b of
|
|
3860
|
+
for (const b of plan2.branches) {
|
|
3833
3861
|
const prs = b.prNumbers.length ? ` #${b.prNumbers.join(",#")}` : "";
|
|
3834
3862
|
const wt = b.worktreePath ? ` (worktree: ${b.worktreePath})` : "";
|
|
3835
3863
|
lines.push(` - ${b.branch} (${b.prState}${prs})${wt}`);
|
|
3836
3864
|
}
|
|
3837
3865
|
}
|
|
3838
|
-
if (
|
|
3866
|
+
if (plan2.trackingRefs.length) {
|
|
3839
3867
|
lines.push("stale tracking refs:");
|
|
3840
|
-
for (const r of
|
|
3868
|
+
for (const r of plan2.trackingRefs) {
|
|
3841
3869
|
const prs = r.prNumbers.length ? ` #${r.prNumbers.join(",#")}` : "";
|
|
3842
3870
|
lines.push(` - ${r.ref} (${r.prState}${prs})`);
|
|
3843
3871
|
}
|
|
3844
3872
|
}
|
|
3845
|
-
if (
|
|
3873
|
+
if (plan2.skipped.length) {
|
|
3846
3874
|
lines.push("skipped:");
|
|
3847
|
-
for (const s of
|
|
3875
|
+
for (const s of plan2.skipped) {
|
|
3848
3876
|
lines.push(` - ${s.branch}: ${s.reason}${s.detail ? ` (${s.detail})` : ""}`);
|
|
3849
3877
|
}
|
|
3850
3878
|
}
|
|
3851
|
-
if (!apply && (
|
|
3879
|
+
if (!apply && (plan2.branches.length || plan2.trackingRefs.length)) lines.push("rerun with --apply to delete only the listed items");
|
|
3852
3880
|
return lines.join("\n");
|
|
3853
3881
|
}
|
|
3854
3882
|
|
|
@@ -4037,7 +4065,25 @@ async function runStage(config = {}, opts = {}) {
|
|
|
4037
4065
|
|
|
4038
4066
|
// src/bootstrap-verify.ts
|
|
4039
4067
|
var requiredDocs = ["README.md", "architecture.md", "AGENTS.md", "CLAUDE.md", ".claude/settings.json", ".mmi/config.json"];
|
|
4040
|
-
var
|
|
4068
|
+
var requiredIssueTemplates = [
|
|
4069
|
+
".github/ISSUE_TEMPLATE/bug.yml",
|
|
4070
|
+
".github/ISSUE_TEMPLATE/feature.yml",
|
|
4071
|
+
".github/ISSUE_TEMPLATE/task.yml",
|
|
4072
|
+
".github/ISSUE_TEMPLATE/config.yml"
|
|
4073
|
+
];
|
|
4074
|
+
var requiredWorkflows = [".github/workflows/pr-to-board.yml"];
|
|
4075
|
+
var requiredLabels = ["bug", "feature", "task", "priority:high", "priority:medium", "priority:low"];
|
|
4076
|
+
var requiredStatusOptions = ["Todo", "In Progress", "In Review", "Done"];
|
|
4077
|
+
var requiredProjectWorkflows = [
|
|
4078
|
+
"Auto-add sub-issues to project",
|
|
4079
|
+
"Auto-close issue",
|
|
4080
|
+
"Item added to project",
|
|
4081
|
+
"Item closed",
|
|
4082
|
+
"Pull request linked to issue",
|
|
4083
|
+
"Pull request merged"
|
|
4084
|
+
];
|
|
4085
|
+
var requiredActionsVariables = ["MMI_APP_ID"];
|
|
4086
|
+
var requiredActionsSecrets = ["MMI_APP_PRIVATE_KEY"];
|
|
4041
4087
|
function expectedBranches(repoClass) {
|
|
4042
4088
|
return repoClass === "content" ? ["main"] : ["development", "rc", "main"];
|
|
4043
4089
|
}
|
|
@@ -4064,6 +4110,20 @@ async function contentExists(deps, repo, branch, path) {
|
|
|
4064
4110
|
return false;
|
|
4065
4111
|
}
|
|
4066
4112
|
}
|
|
4113
|
+
async function contentText(deps, repo, branch, path) {
|
|
4114
|
+
try {
|
|
4115
|
+
const encodedPath = path.split("/").map(encodeURIComponent).join("/");
|
|
4116
|
+
const response = safeJson(
|
|
4117
|
+
(await deps.gh(["api", `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`])).stdout,
|
|
4118
|
+
{}
|
|
4119
|
+
);
|
|
4120
|
+
if (response.content == null) return null;
|
|
4121
|
+
if (response.encoding != null && response.encoding !== "base64") return null;
|
|
4122
|
+
return Buffer.from(response.content.replace(/\s/g, ""), "base64").toString("utf8");
|
|
4123
|
+
} catch {
|
|
4124
|
+
return null;
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4067
4127
|
async function protectedBranch(deps, repo, branch) {
|
|
4068
4128
|
try {
|
|
4069
4129
|
await deps.gh(["api", `repos/${repo}/branches/${branch}/protection`]);
|
|
@@ -4072,6 +4132,9 @@ async function protectedBranch(deps, repo, branch) {
|
|
|
4072
4132
|
return false;
|
|
4073
4133
|
}
|
|
4074
4134
|
}
|
|
4135
|
+
function optionDetail(missing) {
|
|
4136
|
+
return missing.length === 0 ? void 0 : `missing: ${missing.join(", ")}`;
|
|
4137
|
+
}
|
|
4075
4138
|
function localRegistryCheck(deps, path, predicate) {
|
|
4076
4139
|
const text = deps.readLocalFile?.(path);
|
|
4077
4140
|
if (text == null) return null;
|
|
@@ -4094,14 +4157,86 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
4094
4157
|
for (const path of requiredDocs) {
|
|
4095
4158
|
checks.push({ ok: await contentExists(deps, repo, baseBranch, path), label: `bootstrap artifact exists: ${path}` });
|
|
4096
4159
|
}
|
|
4160
|
+
for (const path of requiredIssueTemplates) {
|
|
4161
|
+
checks.push({ ok: await contentExists(deps, repo, baseBranch, path), label: `issue template exists: ${path}` });
|
|
4162
|
+
}
|
|
4163
|
+
for (const path of requiredWorkflows) {
|
|
4164
|
+
checks.push({ ok: await contentExists(deps, repo, baseBranch, path), label: `automation workflow exists: ${path}` });
|
|
4165
|
+
}
|
|
4097
4166
|
checks.push({ ok: await contentExists(deps, repo, baseBranch, ".cursor/environment.json"), label: "Cursor environment committed" });
|
|
4098
|
-
const labels = await ghJson(deps, ["label", "list", "--repo", repo, "--json", "name"], []);
|
|
4167
|
+
const labels = await ghJson(deps, ["label", "list", "--repo", repo, "--limit", "200", "--json", "name"], []);
|
|
4099
4168
|
const labelNames = new Set(labels.map((l) => l.name));
|
|
4100
4169
|
for (const label of requiredLabels) {
|
|
4101
4170
|
checks.push({ ok: labelNames.has(label), label: `label exists: ${label}` });
|
|
4102
4171
|
}
|
|
4103
4172
|
const actions = await ghJson(deps, ["api", `repos/${repo}/actions/permissions`], {});
|
|
4104
4173
|
checks.push({ ok: actions.enabled === true, label: "GitHub Actions enabled" });
|
|
4174
|
+
const variables = await ghJson(deps, ["variable", "list", "--repo", repo, "--json", "name"], []);
|
|
4175
|
+
const variableNames = new Set(variables.map((v) => v.name));
|
|
4176
|
+
for (const variable of requiredActionsVariables) {
|
|
4177
|
+
checks.push({ ok: variableNames.has(variable), label: `Actions variable exists: ${variable}` });
|
|
4178
|
+
}
|
|
4179
|
+
const secrets = await ghJson(deps, ["secret", "list", "--repo", repo, "--json", "name"], []);
|
|
4180
|
+
const secretNames = new Set(secrets.map((s) => s.name));
|
|
4181
|
+
for (const secret of requiredActionsSecrets) {
|
|
4182
|
+
checks.push({ ok: secretNames.has(secret), label: `Actions secret exists: ${secret}` });
|
|
4183
|
+
}
|
|
4184
|
+
const config = safeJson(await contentText(deps, repo, baseBranch, ".mmi/config.json") || "", null);
|
|
4185
|
+
checks.push({
|
|
4186
|
+
ok: Boolean(config?.projectOwner && config?.projectNumber),
|
|
4187
|
+
label: ".mmi project board config exists"
|
|
4188
|
+
});
|
|
4189
|
+
if (config?.projectOwner && config.projectNumber != null) {
|
|
4190
|
+
const project = await ghJson(
|
|
4191
|
+
deps,
|
|
4192
|
+
["project", "field-list", String(config.projectNumber), "--owner", config.projectOwner, "--format", "json"],
|
|
4193
|
+
{}
|
|
4194
|
+
);
|
|
4195
|
+
const fields = project.fields || [];
|
|
4196
|
+
const statusField = fields.find((field) => field.name === "Status");
|
|
4197
|
+
const labelField = fields.find((field) => field.name === "Labels");
|
|
4198
|
+
checks.push({
|
|
4199
|
+
ok: Boolean(statusField),
|
|
4200
|
+
label: `Project Status field exists: ${config.projectOwner}#${config.projectNumber}`
|
|
4201
|
+
});
|
|
4202
|
+
checks.push({
|
|
4203
|
+
ok: Boolean(labelField),
|
|
4204
|
+
label: `Project Labels field exists: ${config.projectOwner}#${config.projectNumber}`
|
|
4205
|
+
});
|
|
4206
|
+
if (statusField != null) {
|
|
4207
|
+
const optionNames = new Set((statusField.options || []).map((option) => option.name));
|
|
4208
|
+
const missingOptions = requiredStatusOptions.filter((option) => !optionNames.has(option));
|
|
4209
|
+
checks.push({
|
|
4210
|
+
ok: missingOptions.length === 0,
|
|
4211
|
+
label: "Project Status lanes configured",
|
|
4212
|
+
detail: optionDetail(missingOptions)
|
|
4213
|
+
});
|
|
4214
|
+
checks.push({
|
|
4215
|
+
ok: config.statusFieldId === statusField.id,
|
|
4216
|
+
label: ".mmi statusFieldId matches project"
|
|
4217
|
+
});
|
|
4218
|
+
for (const optionName of requiredStatusOptions) {
|
|
4219
|
+
const projectOption = statusField.options?.find((option) => option.name === optionName);
|
|
4220
|
+
checks.push({
|
|
4221
|
+
ok: Boolean(projectOption?.id && config.statusOptions?.[optionName] === projectOption.id),
|
|
4222
|
+
label: `.mmi status option matches project: ${optionName}`
|
|
4223
|
+
});
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
const workflowQuery = "query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { workflows(first: 30) { nodes { name enabled } } } } }";
|
|
4227
|
+
const workflowResponse = await ghJson(
|
|
4228
|
+
deps,
|
|
4229
|
+
["api", "graphql", "-f", `query=${workflowQuery}`, "-f", `login=${config.projectOwner}`, "-F", `number=${config.projectNumber}`],
|
|
4230
|
+
{}
|
|
4231
|
+
);
|
|
4232
|
+
const workflows = workflowResponse.data?.organization?.projectV2?.workflows?.nodes || [];
|
|
4233
|
+
for (const workflowName of requiredProjectWorkflows) {
|
|
4234
|
+
checks.push({
|
|
4235
|
+
ok: workflows.some((workflow) => workflow.name === workflowName && workflow.enabled === true),
|
|
4236
|
+
label: `Project workflow enabled: ${workflowName}`
|
|
4237
|
+
});
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4105
4240
|
const fanout = repo === "mutmutco/MMI-Hub" ? true : localRegistryCheck(deps, ".github/fanout-targets.json", (json) => Array.isArray(json?.repos) && json.repos.some((r) => r.repo === repo.split("/")[1] && r.branch === baseBranch));
|
|
4106
4241
|
if (fanout != null) checks.push({ ok: fanout, label: `fanout target registered on ${baseBranch}` });
|
|
4107
4242
|
const projectRegistry = localRegistryCheck(deps, "projects.json", (json) => Array.isArray(json?.projects) && json.projects.some((p) => (p.repos || []).includes(repo)));
|
|
@@ -4111,11 +4246,151 @@ async function verifyBootstrap(repo, repoClass, deps) {
|
|
|
4111
4246
|
function renderBootstrapVerifyReport(report) {
|
|
4112
4247
|
const lines = [`mmi-cli bootstrap verify: ${report.ok ? "OK" : "CHECK"} ${report.repo} (${report.class}, ${report.baseBranch})`];
|
|
4113
4248
|
for (const check of report.checks) {
|
|
4114
|
-
lines.push(`${check.ok ? "
|
|
4249
|
+
lines.push(`${check.ok ? "OK" : "FAIL"} ${check.label}${check.detail ? ` - ${check.detail}` : ""}`);
|
|
4115
4250
|
}
|
|
4116
4251
|
return lines.join("\n");
|
|
4117
4252
|
}
|
|
4118
4253
|
|
|
4254
|
+
// src/plan.ts
|
|
4255
|
+
var import_node_path3 = require("node:path");
|
|
4256
|
+
var PLANS_DIR = "plans";
|
|
4257
|
+
var META_FILE = (0, import_node_path3.join)(PLANS_DIR, ".plan-meta.json");
|
|
4258
|
+
var planPath = (slug) => (0, import_node_path3.join)(PLANS_DIR, `${slug}.md`);
|
|
4259
|
+
var metaKey = (project, slug) => `${project}/${slug}`;
|
|
4260
|
+
function parseMeta(raw) {
|
|
4261
|
+
if (!raw) return {};
|
|
4262
|
+
try {
|
|
4263
|
+
const o = JSON.parse(raw);
|
|
4264
|
+
return o && typeof o === "object" ? o : {};
|
|
4265
|
+
} catch {
|
|
4266
|
+
return {};
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
function serializeMeta(meta) {
|
|
4270
|
+
return JSON.stringify(meta, null, 2) + "\n";
|
|
4271
|
+
}
|
|
4272
|
+
function hashContent(s) {
|
|
4273
|
+
let h = 2166136261;
|
|
4274
|
+
for (let i = 0; i < s.length; i++) {
|
|
4275
|
+
h ^= s.charCodeAt(i);
|
|
4276
|
+
h = Math.imul(h, 16777619);
|
|
4277
|
+
}
|
|
4278
|
+
return (h >>> 0).toString(16);
|
|
4279
|
+
}
|
|
4280
|
+
function staleHint(slug) {
|
|
4281
|
+
return `remote "${slug}" is newer \u2014 run \`mmi-cli plan pull ${slug}\` first (your local is based on an older version), or re-push with \`--force\` to overwrite`;
|
|
4282
|
+
}
|
|
4283
|
+
function formatPlanList(plans) {
|
|
4284
|
+
return plans.map((p) => `${p.slug} \xB7 ${p.updatedAt ?? "-"} \xB7 ${p.project}`).join("\n");
|
|
4285
|
+
}
|
|
4286
|
+
var TIMEOUT_MS = 8e3;
|
|
4287
|
+
async function planPush(deps, slug, opts = {}) {
|
|
4288
|
+
const raw = deps.readLocal(slug);
|
|
4289
|
+
if (raw == null) {
|
|
4290
|
+
deps.err(`no local ${planPath(slug)} to push`);
|
|
4291
|
+
return;
|
|
4292
|
+
}
|
|
4293
|
+
const content = normalizeEol(raw);
|
|
4294
|
+
const project = opts.project ?? await deps.project();
|
|
4295
|
+
const meta = parseMeta(deps.readMetaRaw());
|
|
4296
|
+
const entry = meta[metaKey(project, slug)];
|
|
4297
|
+
const body = { project, slug, content };
|
|
4298
|
+
if (opts.force) body.force = true;
|
|
4299
|
+
else if (entry?.etag) body.baseEtag = entry.etag;
|
|
4300
|
+
const res = await deps.fetch(`${deps.apiUrl}/plan/put`, {
|
|
4301
|
+
method: "POST",
|
|
4302
|
+
headers: await deps.headers({ "content-type": "application/json" }),
|
|
4303
|
+
body: JSON.stringify(body),
|
|
4304
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
4305
|
+
});
|
|
4306
|
+
if (res.ok) {
|
|
4307
|
+
const out = await res.json();
|
|
4308
|
+
meta[metaKey(project, slug)] = { etag: out.etag, hash: hashContent(content), syncedAt: deps.now() };
|
|
4309
|
+
deps.writeMetaRaw(serializeMeta(meta));
|
|
4310
|
+
deps.log(`pushed ${slug}`);
|
|
4311
|
+
} else if (res.status === 409) {
|
|
4312
|
+
deps.err(staleHint(slug));
|
|
4313
|
+
} else {
|
|
4314
|
+
deps.err(`plan push failed: HTTP ${res.status}`);
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
async function planPull(deps, slug, opts = {}) {
|
|
4318
|
+
const project = opts.project ?? await deps.project();
|
|
4319
|
+
const meta = parseMeta(deps.readMetaRaw());
|
|
4320
|
+
const entry = meta[metaKey(project, slug)];
|
|
4321
|
+
const local = deps.readLocal(slug);
|
|
4322
|
+
if (local != null && entry && !opts.force && hashContent(normalizeEol(local)) !== entry.hash) {
|
|
4323
|
+
deps.err(`local ${planPath(slug)} has unpushed edits \u2014 push it, or pull with --force to overwrite`);
|
|
4324
|
+
return;
|
|
4325
|
+
}
|
|
4326
|
+
const qs = new URLSearchParams({ project, slug }).toString();
|
|
4327
|
+
const res = await deps.fetch(`${deps.apiUrl}/plan/get?${qs}`, {
|
|
4328
|
+
method: "GET",
|
|
4329
|
+
headers: await deps.headers(),
|
|
4330
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
4331
|
+
});
|
|
4332
|
+
if (res.status === 404) {
|
|
4333
|
+
deps.err(`no plan "${slug}" found on the server`);
|
|
4334
|
+
return;
|
|
4335
|
+
}
|
|
4336
|
+
if (!res.ok) {
|
|
4337
|
+
deps.err(`plan pull failed: HTTP ${res.status}`);
|
|
4338
|
+
return;
|
|
4339
|
+
}
|
|
4340
|
+
const doc = await res.json();
|
|
4341
|
+
const content = normalizeEol(doc.content ?? "");
|
|
4342
|
+
deps.writeLocal(slug, content);
|
|
4343
|
+
meta[metaKey(project, slug)] = { etag: doc.etag, hash: hashContent(content), syncedAt: deps.now() };
|
|
4344
|
+
deps.writeMetaRaw(serializeMeta(meta));
|
|
4345
|
+
deps.log(`pulled ${slug} \u2192 ${planPath(slug)}`);
|
|
4346
|
+
}
|
|
4347
|
+
async function planList(deps, opts = {}) {
|
|
4348
|
+
const qs = opts.project ? `?${new URLSearchParams({ project: opts.project }).toString()}` : "";
|
|
4349
|
+
let res;
|
|
4350
|
+
try {
|
|
4351
|
+
res = await deps.fetch(`${deps.apiUrl}/plan/list${qs}`, {
|
|
4352
|
+
method: "GET",
|
|
4353
|
+
headers: await deps.headers(),
|
|
4354
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
4355
|
+
});
|
|
4356
|
+
} catch (e) {
|
|
4357
|
+
if (!opts.quiet) deps.err(`plan list: ${e.message}`);
|
|
4358
|
+
return;
|
|
4359
|
+
}
|
|
4360
|
+
if (!res.ok) {
|
|
4361
|
+
if (!opts.quiet) deps.err(`plan list failed: HTTP ${res.status}`);
|
|
4362
|
+
return;
|
|
4363
|
+
}
|
|
4364
|
+
const { plans } = await res.json();
|
|
4365
|
+
if (opts.json) {
|
|
4366
|
+
deps.log(JSON.stringify(plans));
|
|
4367
|
+
return;
|
|
4368
|
+
}
|
|
4369
|
+
if (!plans.length) {
|
|
4370
|
+
if (!opts.quiet) deps.log("no plans");
|
|
4371
|
+
return;
|
|
4372
|
+
}
|
|
4373
|
+
deps.log(formatPlanList(plans));
|
|
4374
|
+
}
|
|
4375
|
+
async function planDelete(deps, slug, opts = {}) {
|
|
4376
|
+
const project = opts.project ?? await deps.project();
|
|
4377
|
+
const res = await deps.fetch(`${deps.apiUrl}/plan/delete`, {
|
|
4378
|
+
method: "POST",
|
|
4379
|
+
headers: await deps.headers({ "content-type": "application/json" }),
|
|
4380
|
+
body: JSON.stringify({ project, slug }),
|
|
4381
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
4382
|
+
});
|
|
4383
|
+
if (!res.ok) {
|
|
4384
|
+
deps.err(`plan delete failed: HTTP ${res.status}`);
|
|
4385
|
+
return;
|
|
4386
|
+
}
|
|
4387
|
+
deps.removeLocal(slug);
|
|
4388
|
+
const meta = parseMeta(deps.readMetaRaw());
|
|
4389
|
+
delete meta[metaKey(project, slug)];
|
|
4390
|
+
deps.writeMetaRaw(serializeMeta(meta));
|
|
4391
|
+
deps.log(`deleted ${slug}`);
|
|
4392
|
+
}
|
|
4393
|
+
|
|
4119
4394
|
// src/index.ts
|
|
4120
4395
|
var execFileP3 = (0, import_node_util3.promisify)(import_node_child_process4.execFile);
|
|
4121
4396
|
var GIT_TIMEOUT_MS = 1e4;
|
|
@@ -4250,25 +4525,25 @@ async function gcPlan(remote, limit) {
|
|
|
4250
4525
|
remote
|
|
4251
4526
|
});
|
|
4252
4527
|
}
|
|
4253
|
-
async function applyGcPlan(
|
|
4254
|
-
for (const branch of
|
|
4528
|
+
async function applyGcPlan(plan2, remote) {
|
|
4529
|
+
for (const branch of plan2.branches) {
|
|
4255
4530
|
if (branch.worktreePath) await execFileP3("git", ["worktree", "remove", branch.worktreePath], { timeout: GIT_TIMEOUT_MS });
|
|
4256
4531
|
await execFileP3("git", ["branch", "-D", branch.branch], { timeout: GIT_TIMEOUT_MS });
|
|
4257
4532
|
}
|
|
4258
|
-
for (const ref of
|
|
4533
|
+
for (const ref of plan2.trackingRefs) {
|
|
4259
4534
|
await execFileP3("git", ["update-ref", "-d", `refs/remotes/${remote}/${ref.branch}`], { timeout: GIT_TIMEOUT_MS });
|
|
4260
4535
|
}
|
|
4261
|
-
if (
|
|
4536
|
+
if (plan2.branches.some((b) => b.worktreePath)) {
|
|
4262
4537
|
await execFileP3("git", ["worktree", "prune"], { timeout: GIT_TIMEOUT_MS });
|
|
4263
4538
|
}
|
|
4264
4539
|
}
|
|
4265
4540
|
function resolveVersion() {
|
|
4266
4541
|
try {
|
|
4267
|
-
const manifest = (0,
|
|
4542
|
+
const manifest = (0, import_node_path4.join)(__dirname, "..", "..", ".claude-plugin", "plugin.json");
|
|
4268
4543
|
return JSON.parse((0, import_node_fs3.readFileSync)(manifest, "utf8")).version || "0.0.0";
|
|
4269
4544
|
} catch {
|
|
4270
4545
|
try {
|
|
4271
|
-
const pkg = (0,
|
|
4546
|
+
const pkg = (0, import_node_path4.join)(__dirname, "..", "package.json");
|
|
4272
4547
|
return JSON.parse((0, import_node_fs3.readFileSync)(pkg, "utf8")).version || "0.0.0";
|
|
4273
4548
|
} catch {
|
|
4274
4549
|
return "0.0.0";
|
|
@@ -4277,7 +4552,7 @@ function resolveVersion() {
|
|
|
4277
4552
|
}
|
|
4278
4553
|
function readRepoVersion() {
|
|
4279
4554
|
try {
|
|
4280
|
-
return JSON.parse((0, import_node_fs3.readFileSync)((0,
|
|
4555
|
+
return JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path4.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
4281
4556
|
} catch {
|
|
4282
4557
|
return void 0;
|
|
4283
4558
|
}
|
|
@@ -4303,11 +4578,13 @@ rules.command("sync").option("--quiet", "stay silent unless something changed or
|
|
|
4303
4578
|
return;
|
|
4304
4579
|
}
|
|
4305
4580
|
const base = (cfg.orgRulesSource ?? DEFAULT_RULES_SOURCE).replace(/\/$/, "");
|
|
4581
|
+
const token = await githubToken();
|
|
4306
4582
|
let changed = 0;
|
|
4307
4583
|
for (const file of ["AGENTS.md", "CLAUDE.md", ".claude/settings.json"]) {
|
|
4308
4584
|
let source;
|
|
4309
4585
|
try {
|
|
4310
|
-
const
|
|
4586
|
+
const url = `${base}/${file}`;
|
|
4587
|
+
const res = await fetch(url, { headers: rulesSourceAuthHeaders(url, token) });
|
|
4311
4588
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
4312
4589
|
source = await res.text();
|
|
4313
4590
|
} catch (e) {
|
|
@@ -4331,7 +4608,7 @@ async function runNote(summary, o) {
|
|
|
4331
4608
|
const capture = buildNoteCapture(summary, o, (0, import_node_crypto.randomUUID)(), { sha: sha || void 0, branch: key.branch });
|
|
4332
4609
|
await postCapture(capture);
|
|
4333
4610
|
}
|
|
4334
|
-
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").action((summary, o) => runNote(summary, o));
|
|
4611
|
+
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));
|
|
4335
4612
|
saga.command("probe <summary>").description("record a diagnostic probe note (alias for `saga note --diagnostic`)").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").action((summary, o) => runNote(summary, { ...o, diagnostic: true }));
|
|
4336
4613
|
saga.command("show").option("--quiet", "no-op silently when unconfigured/unreachable (SessionStart hook)").option("--latest-anywhere", "resume the newest saga across all repos (default: current repo)").description("print your resume block \u2014 current repo HEAD + project memory (where you left off)").action(async (opts) => {
|
|
4337
4614
|
const cfg = await loadConfig();
|
|
@@ -4383,7 +4660,8 @@ saga.command("head-update").option("--run", "detached worker: fetch state, run t
|
|
|
4383
4660
|
if (!res.ok) return;
|
|
4384
4661
|
const state = await res.json();
|
|
4385
4662
|
if (!state.actionLog?.length) return;
|
|
4386
|
-
const
|
|
4663
|
+
const decisionTexts = (state.decisions ?? []).map((d) => typeof d === "string" ? d : d.text);
|
|
4664
|
+
const update = parseHeadUpdate(await runHeadEngine(headPrompt({ ...state, decisions: decisionTexts })));
|
|
4387
4665
|
if (!update) return;
|
|
4388
4666
|
await fetch(`${cfg.sagaApiUrl}/saga/head`, {
|
|
4389
4667
|
method: "POST",
|
|
@@ -4433,10 +4711,10 @@ program2.command("gc").description("dry-run cleanup for merged/closed PR branche
|
|
|
4433
4711
|
const limit = Number.parseInt(o.limit, 10);
|
|
4434
4712
|
if (!Number.isFinite(limit) || limit < 1) return fail("gc: --limit must be a positive integer");
|
|
4435
4713
|
try {
|
|
4436
|
-
const
|
|
4437
|
-
if (o.apply) await applyGcPlan(
|
|
4438
|
-
if (o.json) return console.log(JSON.stringify({ dryRun: !o.apply, remote: o.remote, plan }, null, 2));
|
|
4439
|
-
console.log(formatGcPlan(
|
|
4714
|
+
const plan2 = await gcPlan(o.remote, limit);
|
|
4715
|
+
if (o.apply) await applyGcPlan(plan2, o.remote);
|
|
4716
|
+
if (o.json) return console.log(JSON.stringify({ dryRun: !o.apply, remote: o.remote, plan: plan2 }, null, 2));
|
|
4717
|
+
console.log(formatGcPlan(plan2, Boolean(o.apply)));
|
|
4440
4718
|
} catch (e) {
|
|
4441
4719
|
fail(`gc: ${e.message}`);
|
|
4442
4720
|
}
|
|
@@ -4482,6 +4760,77 @@ function scheduleRelatedDiscovery(o) {
|
|
|
4482
4760
|
} catch {
|
|
4483
4761
|
}
|
|
4484
4762
|
}
|
|
4763
|
+
function makePlanDeps(cfg) {
|
|
4764
|
+
const ensureDir = () => (0, import_node_fs3.mkdirSync)(PLANS_DIR, { recursive: true });
|
|
4765
|
+
return {
|
|
4766
|
+
apiUrl: cfg.sagaApiUrl,
|
|
4767
|
+
fetch: (url, init) => fetch(url, init),
|
|
4768
|
+
headers: (extra) => sagaHeaders(extra),
|
|
4769
|
+
project: async () => (await sagaKey(cfg)).project,
|
|
4770
|
+
readLocal: (slug) => {
|
|
4771
|
+
try {
|
|
4772
|
+
return (0, import_node_fs3.readFileSync)(planPath(slug), "utf8");
|
|
4773
|
+
} catch {
|
|
4774
|
+
return null;
|
|
4775
|
+
}
|
|
4776
|
+
},
|
|
4777
|
+
writeLocal: (slug, content) => {
|
|
4778
|
+
ensureDir();
|
|
4779
|
+
(0, import_node_fs3.writeFileSync)(planPath(slug), content, "utf8");
|
|
4780
|
+
},
|
|
4781
|
+
removeLocal: (slug) => {
|
|
4782
|
+
try {
|
|
4783
|
+
(0, import_node_fs3.rmSync)(planPath(slug));
|
|
4784
|
+
} catch {
|
|
4785
|
+
}
|
|
4786
|
+
},
|
|
4787
|
+
readMetaRaw: () => {
|
|
4788
|
+
try {
|
|
4789
|
+
return (0, import_node_fs3.readFileSync)(META_FILE, "utf8");
|
|
4790
|
+
} catch {
|
|
4791
|
+
return null;
|
|
4792
|
+
}
|
|
4793
|
+
},
|
|
4794
|
+
writeMetaRaw: (raw) => {
|
|
4795
|
+
ensureDir();
|
|
4796
|
+
(0, import_node_fs3.writeFileSync)(META_FILE, raw, "utf8");
|
|
4797
|
+
},
|
|
4798
|
+
log: (m) => console.log(m),
|
|
4799
|
+
err: (m) => console.error(m),
|
|
4800
|
+
now: () => (/* @__PURE__ */ new Date()).toISOString()
|
|
4801
|
+
};
|
|
4802
|
+
}
|
|
4803
|
+
function openInEditor(path) {
|
|
4804
|
+
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
4805
|
+
if (!editor) {
|
|
4806
|
+
console.log(`plan at ${path} (set $EDITOR to open it automatically)`);
|
|
4807
|
+
return;
|
|
4808
|
+
}
|
|
4809
|
+
try {
|
|
4810
|
+
(0, import_node_child_process4.spawn)(editor, [path], { stdio: "inherit" });
|
|
4811
|
+
} catch {
|
|
4812
|
+
console.log(`open ${path} manually`);
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
async function withPlan(quiet, run) {
|
|
4816
|
+
const cfg = await loadConfig();
|
|
4817
|
+
if (!cfg.sagaApiUrl) {
|
|
4818
|
+
if (!quiet) fail("plan: sagaApiUrl not configured in .mmi/config.json");
|
|
4819
|
+
return;
|
|
4820
|
+
}
|
|
4821
|
+
await run(makePlanDeps(cfg));
|
|
4822
|
+
}
|
|
4823
|
+
var plan = program2.command("plan").description("your cross-device plans/SSOTs (S3-backed, git-clean)");
|
|
4824
|
+
plan.command("push <slug>").description("push a local 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, (d) => planPush(d, slug, o)));
|
|
4825
|
+
plan.command("pull <slug>").description("pull a plan from the server into plans/<slug>.md").option("--project <name>", "override the project key").option("--force", "overwrite local even if it has unpushed edits").action((slug, o) => withPlan(false, (d) => planPull(d, slug, o)));
|
|
4826
|
+
plan.command("list").description("list your 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)));
|
|
4827
|
+
plan.command("open <slug>").description("pull if needed, then open plans/<slug>.md in $EDITOR").option("--project <name>", "override the project key").action(
|
|
4828
|
+
(slug, o) => withPlan(false, async (d) => {
|
|
4829
|
+
await planPull(d, slug, { project: o.project });
|
|
4830
|
+
openInEditor(planPath(slug));
|
|
4831
|
+
})
|
|
4832
|
+
);
|
|
4833
|
+
plan.command("delete <slug>").description("delete a plan from the server and the local copy").option("--project <name>", "override the project key").action((slug, o) => withPlan(false, (d) => planDelete(d, slug, o)));
|
|
4485
4834
|
var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
|
|
4486
4835
|
issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").requiredOption("--type <type>", "bug | feature | task (sets the matching label)").requiredOption("--title <title>", "issue title").requiredOption("--body <body>", "issue body (markdown)").requiredOption("--priority <priority>", "high | medium | low (adds a priority:<p> label)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (o) => {
|
|
4487
4836
|
let args;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|