@mutmutco/cli 2.34.1 → 2.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/index.cjs +17 -8
- package/dist/main.cjs +1942 -302
- package/dist/saga.cjs +27 -15
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -3392,7 +3392,7 @@ var program = new Command();
|
|
|
3392
3392
|
|
|
3393
3393
|
// src/index.ts
|
|
3394
3394
|
var import_promises5 = require("node:fs/promises");
|
|
3395
|
-
var
|
|
3395
|
+
var import_node_fs18 = require("node:fs");
|
|
3396
3396
|
|
|
3397
3397
|
// src/rules-sync.ts
|
|
3398
3398
|
function normalizeEol(s) {
|
|
@@ -3427,7 +3427,7 @@ var import_node_child_process10 = require("node:child_process");
|
|
|
3427
3427
|
|
|
3428
3428
|
// src/cli-shared.ts
|
|
3429
3429
|
var import_promises = require("node:fs/promises");
|
|
3430
|
-
var
|
|
3430
|
+
var import_node_fs7 = require("node:fs");
|
|
3431
3431
|
var import_node_crypto2 = require("node:crypto");
|
|
3432
3432
|
var import_node_child_process4 = require("node:child_process");
|
|
3433
3433
|
var import_node_util4 = require("node:util");
|
|
@@ -4109,10 +4109,19 @@ async function hubAuthToken(deps) {
|
|
|
4109
4109
|
}
|
|
4110
4110
|
|
|
4111
4111
|
// src/stdin-inject.ts
|
|
4112
|
+
var import_node_fs6 = require("node:fs");
|
|
4112
4113
|
var injectedStdin;
|
|
4114
|
+
function stdinHasPipedInput(statFd = () => (0, import_node_fs6.fstatSync)(0)) {
|
|
4115
|
+
try {
|
|
4116
|
+
const stat = statFd();
|
|
4117
|
+
return stat.isFIFO() || stat.isFile();
|
|
4118
|
+
} catch {
|
|
4119
|
+
return false;
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4113
4122
|
async function readStdin() {
|
|
4114
4123
|
if (injectedStdin !== void 0) return injectedStdin;
|
|
4115
|
-
if (
|
|
4124
|
+
if (!stdinHasPipedInput()) return "";
|
|
4116
4125
|
const chunks = [];
|
|
4117
4126
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
4118
4127
|
return Buffer.concat(chunks).toString("utf8");
|
|
@@ -4161,7 +4170,7 @@ function sessionDeps() {
|
|
|
4161
4170
|
env: process.env,
|
|
4162
4171
|
readPersisted: () => {
|
|
4163
4172
|
try {
|
|
4164
|
-
return (0,
|
|
4173
|
+
return (0, import_node_fs7.readFileSync)(SESSION_FILE, "utf8");
|
|
4165
4174
|
} catch {
|
|
4166
4175
|
return null;
|
|
4167
4176
|
}
|
|
@@ -4174,8 +4183,8 @@ function sessionDeps() {
|
|
|
4174
4183
|
var resolveSessionId = () => resolveSession(sessionDeps());
|
|
4175
4184
|
function persistSession(id) {
|
|
4176
4185
|
try {
|
|
4177
|
-
(0,
|
|
4178
|
-
(0,
|
|
4186
|
+
(0, import_node_fs7.mkdirSync)(".mmi", { recursive: true });
|
|
4187
|
+
(0, import_node_fs7.writeFileSync)(SESSION_FILE, id, "utf8");
|
|
4179
4188
|
} catch {
|
|
4180
4189
|
}
|
|
4181
4190
|
}
|
|
@@ -4267,7 +4276,9 @@ async function resolveTextArg(input, deps, labels) {
|
|
|
4267
4276
|
const source = input.file ?? "";
|
|
4268
4277
|
const text = source === "-" ? await deps.readStdin() : await deps.readFile(source, "utf8");
|
|
4269
4278
|
if (text.trim().length === 0) {
|
|
4270
|
-
throw new Error(
|
|
4279
|
+
throw new Error(
|
|
4280
|
+
source === "-" ? `${labels.file} - read empty stdin (nothing piped \u2014 pass a heredoc/pipe, or ${labels.file} <path>)` : `${labels.file} produced an empty ${labels.noun}`
|
|
4281
|
+
);
|
|
4271
4282
|
}
|
|
4272
4283
|
return text;
|
|
4273
4284
|
}
|
|
@@ -4287,7 +4298,7 @@ function resolveIssueTitle(input, deps) {
|
|
|
4287
4298
|
}
|
|
4288
4299
|
|
|
4289
4300
|
// src/saga-capture.ts
|
|
4290
|
-
var
|
|
4301
|
+
var import_node_fs8 = require("node:fs");
|
|
4291
4302
|
var import_node_os2 = require("node:os");
|
|
4292
4303
|
var import_node_path6 = require("node:path");
|
|
4293
4304
|
function parseHookInput(stdin) {
|
|
@@ -4314,16 +4325,16 @@ function resolveCursorTranscriptPath(hook, env = process.env) {
|
|
|
4314
4325
|
const slug = cursorProjectSlug(workspaceRoot);
|
|
4315
4326
|
const transcriptsDir = (0, import_node_path6.join)(cursorProjectsRoot(env), slug, "agent-transcripts");
|
|
4316
4327
|
const nested = (0, import_node_path6.join)(transcriptsDir, conversationId, `${conversationId}.jsonl`);
|
|
4317
|
-
if ((0,
|
|
4328
|
+
if ((0, import_node_fs8.existsSync)(nested)) return nested;
|
|
4318
4329
|
const flat = (0, import_node_path6.join)(transcriptsDir, `${conversationId}.jsonl`);
|
|
4319
|
-
if ((0,
|
|
4330
|
+
if ((0, import_node_fs8.existsSync)(flat)) return flat;
|
|
4320
4331
|
return void 0;
|
|
4321
4332
|
}
|
|
4322
4333
|
function resolveTranscriptPath(hook, env = process.env) {
|
|
4323
4334
|
const fromHook = (hook.transcript_path ?? hook.transcriptPath)?.trim();
|
|
4324
4335
|
if (fromHook) return fromHook;
|
|
4325
4336
|
const fromEnv = env.CURSOR_TRANSCRIPT_PATH?.trim();
|
|
4326
|
-
if (fromEnv && (0,
|
|
4337
|
+
if (fromEnv && (0, import_node_fs8.existsSync)(fromEnv)) return fromEnv;
|
|
4327
4338
|
const surface = env.MMI_AGENT_SURFACE?.trim();
|
|
4328
4339
|
if (surface === "cursor" || env.CURSOR_TRACE_ID || env.CURSOR_SESSION_ID) {
|
|
4329
4340
|
return resolveCursorTranscriptPath(hook, env);
|
|
@@ -4436,6 +4447,7 @@ function buildNoteCapture(summary, o, id, evidence) {
|
|
|
4436
4447
|
next: o.next,
|
|
4437
4448
|
decision: o.decision,
|
|
4438
4449
|
queueOp,
|
|
4450
|
+
handoffClose: o.handoffClose,
|
|
4439
4451
|
state,
|
|
4440
4452
|
source,
|
|
4441
4453
|
evidence: Object.keys(ev).length ? ev : void 0,
|
|
@@ -5069,27 +5081,281 @@ function registerSagaCommands(program3) {
|
|
|
5069
5081
|
});
|
|
5070
5082
|
}
|
|
5071
5083
|
|
|
5084
|
+
// src/handoff-commands.ts
|
|
5085
|
+
var import_node_crypto4 = require("node:crypto");
|
|
5086
|
+
|
|
5087
|
+
// src/handoff.ts
|
|
5088
|
+
var HANDOFF_PREFIX = "handoff:";
|
|
5089
|
+
function clean(value) {
|
|
5090
|
+
return value?.trim() ?? "";
|
|
5091
|
+
}
|
|
5092
|
+
function normalizeHandoffKey(value) {
|
|
5093
|
+
const key = clean(value);
|
|
5094
|
+
if (!key) throw new Error("handoff key is required");
|
|
5095
|
+
return key;
|
|
5096
|
+
}
|
|
5097
|
+
function serializeHandoff(record) {
|
|
5098
|
+
return `${HANDOFF_PREFIX}${JSON.stringify(record)}`;
|
|
5099
|
+
}
|
|
5100
|
+
function parseHandoffText(text) {
|
|
5101
|
+
if (!text.startsWith(HANDOFF_PREFIX)) return null;
|
|
5102
|
+
try {
|
|
5103
|
+
const raw = JSON.parse(text.slice(HANDOFF_PREFIX.length));
|
|
5104
|
+
const state = raw.state;
|
|
5105
|
+
const key = clean(raw.key);
|
|
5106
|
+
const northStarSlug = clean(raw.northStarSlug);
|
|
5107
|
+
const summary = clean(raw.summary);
|
|
5108
|
+
const sourceSessionId = clean(raw.sourceSessionId);
|
|
5109
|
+
const createdAt = clean(raw.createdAt);
|
|
5110
|
+
if (state !== "open" && state !== "claimed" && state !== "cancelled") return null;
|
|
5111
|
+
if (!key || !northStarSlug || !summary || !sourceSessionId || !createdAt) return null;
|
|
5112
|
+
return {
|
|
5113
|
+
state,
|
|
5114
|
+
key,
|
|
5115
|
+
northStarSlug,
|
|
5116
|
+
summary,
|
|
5117
|
+
sourceSessionId,
|
|
5118
|
+
createdAt,
|
|
5119
|
+
claimedBySessionId: clean(raw.claimedBySessionId) || void 0,
|
|
5120
|
+
closedAt: clean(raw.closedAt) || void 0
|
|
5121
|
+
};
|
|
5122
|
+
} catch {
|
|
5123
|
+
return null;
|
|
5124
|
+
}
|
|
5125
|
+
}
|
|
5126
|
+
function listHandoffs(head, opts = {}) {
|
|
5127
|
+
const parsed = (head.queued ?? []).map((item, index) => ({ done: item.done, index, record: parseHandoffText(item.text) })).filter((x) => x.record !== null);
|
|
5128
|
+
return parsed.filter((x) => opts.includeClosed || x.record.state === "open" && !x.done).map((x) => ({ index: x.index, done: x.done, record: x.record }));
|
|
5129
|
+
}
|
|
5130
|
+
function findOpenHandoff(head, key) {
|
|
5131
|
+
const normalized = normalizeHandoffKey(key).toLowerCase();
|
|
5132
|
+
return listHandoffs(head).find((item) => item.record.key.toLowerCase() === normalized || item.record.northStarSlug.toLowerCase() === normalized);
|
|
5133
|
+
}
|
|
5134
|
+
function planOpenHandoff(input) {
|
|
5135
|
+
const key = normalizeHandoffKey(input.key);
|
|
5136
|
+
const northStarSlug = clean(input.northStarSlug);
|
|
5137
|
+
const summary = clean(input.summary);
|
|
5138
|
+
const sourceSessionId = clean(input.sourceSessionId);
|
|
5139
|
+
if (!northStarSlug) throw new Error("north star slug is required");
|
|
5140
|
+
if (!summary) throw new Error("handoff summary is required");
|
|
5141
|
+
if (!sourceSessionId) throw new Error("source session id is required");
|
|
5142
|
+
return {
|
|
5143
|
+
state: "open",
|
|
5144
|
+
key,
|
|
5145
|
+
northStarSlug,
|
|
5146
|
+
summary,
|
|
5147
|
+
sourceSessionId,
|
|
5148
|
+
createdAt: input.createdAt
|
|
5149
|
+
};
|
|
5150
|
+
}
|
|
5151
|
+
function closeHandoff(record, state, at, claimedBySessionId) {
|
|
5152
|
+
return {
|
|
5153
|
+
...record,
|
|
5154
|
+
state,
|
|
5155
|
+
closedAt: at,
|
|
5156
|
+
claimedBySessionId: claimedBySessionId || record.claimedBySessionId
|
|
5157
|
+
};
|
|
5158
|
+
}
|
|
5159
|
+
function formatHandoffLine(item) {
|
|
5160
|
+
const r = item.record;
|
|
5161
|
+
const suffix = r.state === "claimed" && r.claimedBySessionId ? ` -> ${r.claimedBySessionId}` : "";
|
|
5162
|
+
return `${r.key} [${r.state}] northstar:${r.northStarSlug} source:${r.sourceSessionId}${suffix} - ${r.summary}`;
|
|
5163
|
+
}
|
|
5164
|
+
|
|
5165
|
+
// src/handoff-commands.ts
|
|
5166
|
+
var FOREGROUND_FETCH = { attempts: 2, timeoutMs: 8e3 };
|
|
5167
|
+
var SESSION_START_FETCH = { attempts: 1, timeoutMs: 3e3 };
|
|
5168
|
+
async function fetchState(url, qs, retry = FOREGROUND_FETCH) {
|
|
5169
|
+
const res = await fetchWithRetry(fetch, `${url}/saga/state?${qs}`, { headers: await hubHeaders() }, retry);
|
|
5170
|
+
if (!res.ok) return null;
|
|
5171
|
+
const body = await res.json();
|
|
5172
|
+
if ("state" in body) return body;
|
|
5173
|
+
return { key: null, state: { head: body.head } };
|
|
5174
|
+
}
|
|
5175
|
+
async function fetchScopedSessions(url, project2, branch, retry = FOREGROUND_FETCH) {
|
|
5176
|
+
const qs = new URLSearchParams({ project: project2, branch });
|
|
5177
|
+
const res = await fetchWithRetry(fetch, `${url}/saga/sessions?${qs}`, { headers: await hubHeaders() }, retry);
|
|
5178
|
+
if (!res.ok) return [];
|
|
5179
|
+
const body = await res.json();
|
|
5180
|
+
return body.sessions ?? [];
|
|
5181
|
+
}
|
|
5182
|
+
async function fetchSessionHead(url, key, retry = FOREGROUND_FETCH) {
|
|
5183
|
+
const qs = new URLSearchParams(key);
|
|
5184
|
+
const got = await fetchState(url, qs, retry);
|
|
5185
|
+
return got?.state?.head ?? null;
|
|
5186
|
+
}
|
|
5187
|
+
async function fetchCurrentState() {
|
|
5188
|
+
const cfg = await loadConfig();
|
|
5189
|
+
if (!cfg.sagaApiUrl) return null;
|
|
5190
|
+
const key = await sagaKey(cfg);
|
|
5191
|
+
const got = await fetchState(cfg.sagaApiUrl, new URLSearchParams(key));
|
|
5192
|
+
return got ? { key, head: got.state?.head ?? {} } : null;
|
|
5193
|
+
}
|
|
5194
|
+
async function collectScopedHandoffs(opts = {}) {
|
|
5195
|
+
const cfg = await loadConfig();
|
|
5196
|
+
if (!cfg.sagaApiUrl) return { handoffs: [], sessions: [] };
|
|
5197
|
+
const key = await sagaKey(cfg);
|
|
5198
|
+
const retry = opts.retry ?? FOREGROUND_FETCH;
|
|
5199
|
+
const sessions = await fetchScopedSessions(cfg.sagaApiUrl, key.project, key.branch, retry);
|
|
5200
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5201
|
+
const handoffs = [];
|
|
5202
|
+
for (const session of sessions) {
|
|
5203
|
+
const head = await fetchSessionHead(cfg.sagaApiUrl, session, retry);
|
|
5204
|
+
if (!head) continue;
|
|
5205
|
+
for (const item of listHandoffs(head, { includeClosed: opts.includeClosed })) {
|
|
5206
|
+
const dedupe = `${item.record.sourceSessionId}:${item.record.key}:${item.record.state}`;
|
|
5207
|
+
if (seen.has(dedupe)) continue;
|
|
5208
|
+
seen.add(dedupe);
|
|
5209
|
+
handoffs.push(item);
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
return { handoffs, sessions };
|
|
5213
|
+
}
|
|
5214
|
+
async function locateOpenHandoff(key, retry = FOREGROUND_FETCH) {
|
|
5215
|
+
const cfg = await loadConfig();
|
|
5216
|
+
if (!cfg.sagaApiUrl) return null;
|
|
5217
|
+
const scopeKey = await sagaKey(cfg);
|
|
5218
|
+
const sessions = await fetchScopedSessions(cfg.sagaApiUrl, scopeKey.project, scopeKey.branch, retry);
|
|
5219
|
+
for (const session of sessions) {
|
|
5220
|
+
const head = await fetchSessionHead(cfg.sagaApiUrl, session, retry);
|
|
5221
|
+
if (!head) continue;
|
|
5222
|
+
const item = findOpenHandoff(head, key);
|
|
5223
|
+
if (item) {
|
|
5224
|
+
return {
|
|
5225
|
+
key: { project: session.project, branch: session.branch, sessionId: session.sessionId },
|
|
5226
|
+
item
|
|
5227
|
+
};
|
|
5228
|
+
}
|
|
5229
|
+
}
|
|
5230
|
+
return null;
|
|
5231
|
+
}
|
|
5232
|
+
async function locateClaimedHandoff(key, claimedBySessionId, retry = FOREGROUND_FETCH) {
|
|
5233
|
+
const { handoffs } = await collectScopedHandoffs({ includeClosed: true, retry });
|
|
5234
|
+
const normalized = key.toLowerCase();
|
|
5235
|
+
return handoffs.find((item) => {
|
|
5236
|
+
const r = item.record;
|
|
5237
|
+
return r.state === "claimed" && r.claimedBySessionId === claimedBySessionId && (r.key.toLowerCase() === normalized || r.northStarSlug.toLowerCase() === normalized);
|
|
5238
|
+
}) ?? null;
|
|
5239
|
+
}
|
|
5240
|
+
async function postKeyedNote(key, summary, options) {
|
|
5241
|
+
const cfg = await loadConfig();
|
|
5242
|
+
if (!cfg.sagaApiUrl) fail("handoff: Hub API URL not configured");
|
|
5243
|
+
const sha = await gitOut(["rev-parse", "--short", "HEAD"]);
|
|
5244
|
+
const capture = buildNoteCapture(summary, options, (0, import_node_crypto4.randomUUID)(), { sha: sha || void 0, branch: key.branch });
|
|
5245
|
+
const result = await postCaptureOnce(cfg.sagaApiUrl, { ...capture, ...key });
|
|
5246
|
+
if (!result.ok) fail(`handoff: write failed${result.status ? ` (HTTP ${result.status})` : ""}${result.message ? `: ${result.message}` : ""}`);
|
|
5247
|
+
}
|
|
5248
|
+
function deriveOpenFields(summary, opts, head, key) {
|
|
5249
|
+
const northStarSlug = opts.northStarSlug ?? head.anchor?.slug;
|
|
5250
|
+
const handoffKey = opts.key ?? northStarSlug;
|
|
5251
|
+
const text = summary?.trim() || head.next?.trim() || head.anchor?.intent?.trim();
|
|
5252
|
+
return planOpenHandoff({
|
|
5253
|
+
key: handoffKey ?? "",
|
|
5254
|
+
northStarSlug: northStarSlug ?? "",
|
|
5255
|
+
summary: text ?? "",
|
|
5256
|
+
sourceSessionId: key.sessionId,
|
|
5257
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5258
|
+
});
|
|
5259
|
+
}
|
|
5260
|
+
async function runHandoffOpen(summary, opts, io = consoleIo) {
|
|
5261
|
+
const current = await fetchCurrentState();
|
|
5262
|
+
if (!current) return fail("handoff open: saga state unavailable");
|
|
5263
|
+
let record;
|
|
5264
|
+
try {
|
|
5265
|
+
record = deriveOpenFields(summary, opts, current.head, current.key);
|
|
5266
|
+
} catch (e) {
|
|
5267
|
+
return fail(`handoff open: ${e.message}`);
|
|
5268
|
+
}
|
|
5269
|
+
await postKeyedNote(current.key, `handoff opened ${record.key}`, { queueAdd: serializeHandoff(record), anchorSlug: record.northStarSlug });
|
|
5270
|
+
if (opts.json) return io.log(JSON.stringify({ ok: true, handoff: record }));
|
|
5271
|
+
io.log(`handoff open: ${formatHandoffLine({ index: -1, done: false, record })}`);
|
|
5272
|
+
}
|
|
5273
|
+
async function runHandoffList(opts, io = consoleIo) {
|
|
5274
|
+
const { handoffs } = await collectScopedHandoffs({ includeClosed: opts.all });
|
|
5275
|
+
if (opts.json) {
|
|
5276
|
+
io.log(JSON.stringify({ handoffs }, null, 2));
|
|
5277
|
+
} else if (!handoffs.length) {
|
|
5278
|
+
io.log("handoff list: none");
|
|
5279
|
+
} else {
|
|
5280
|
+
for (const h of handoffs) io.log(formatHandoffLine(h));
|
|
5281
|
+
}
|
|
5282
|
+
return handoffs;
|
|
5283
|
+
}
|
|
5284
|
+
async function runHandoffOffer(io = consoleIo, opts = {}) {
|
|
5285
|
+
const retry = opts.fast ? SESSION_START_FETCH : FOREGROUND_FETCH;
|
|
5286
|
+
const { handoffs } = await collectScopedHandoffs({ retry });
|
|
5287
|
+
if (!handoffs.length) return;
|
|
5288
|
+
io.log("Open handoffs:");
|
|
5289
|
+
for (const h of handoffs) io.log(`- ${formatHandoffLine(h)}`);
|
|
5290
|
+
io.log("Use `mmi-cli handoff accept <key>` to claim, `mmi-cli handoff cancel <key>` to close, or decline in chat to leave it open.");
|
|
5291
|
+
}
|
|
5292
|
+
async function closeSourceHandoff(key, state, opts = {}, io = consoleIo) {
|
|
5293
|
+
const current = await sagaKey(await loadConfig(), resolveSessionId());
|
|
5294
|
+
const located = await locateOpenHandoff(key);
|
|
5295
|
+
if (!located) {
|
|
5296
|
+
if (state === "claimed") {
|
|
5297
|
+
const prior = await locateClaimedHandoff(key, current.sessionId);
|
|
5298
|
+
if (prior) {
|
|
5299
|
+
if (opts.json) return io.log(JSON.stringify({ ok: true, handoff: prior.record, idempotent: true }));
|
|
5300
|
+
return io.log(`handoff claimed: ${formatHandoffLine(prior)} (already accepted)`);
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
return fail(`handoff ${state}: no open handoff matching ${key}`);
|
|
5304
|
+
}
|
|
5305
|
+
const closed = closeHandoff(located.item.record, state, (/* @__PURE__ */ new Date()).toISOString(), state === "claimed" ? current.sessionId : void 0);
|
|
5306
|
+
await postKeyedNote(located.key, `handoff ${state} ${located.item.record.key}`, {
|
|
5307
|
+
handoffClose: { index: located.item.index, closedText: serializeHandoff(closed) }
|
|
5308
|
+
});
|
|
5309
|
+
if (state === "claimed") {
|
|
5310
|
+
await postKeyedNote(current, `handoff accepted ${located.item.record.key}`, {
|
|
5311
|
+
anchor: located.item.record.summary,
|
|
5312
|
+
anchorSlug: located.item.record.northStarSlug,
|
|
5313
|
+
anchorForce: true,
|
|
5314
|
+
decision: `accepted handoff ${located.item.record.key} from session ${located.item.record.sourceSessionId}; northstar ${located.item.record.northStarSlug}`,
|
|
5315
|
+
verified: true
|
|
5316
|
+
});
|
|
5317
|
+
}
|
|
5318
|
+
if (opts.json) return io.log(JSON.stringify({ ok: true, handoff: closed }));
|
|
5319
|
+
io.log(`handoff ${state}: ${formatHandoffLine({ index: located.item.index, done: true, record: closed })}`);
|
|
5320
|
+
}
|
|
5321
|
+
async function runHandoffDecline(key, opts = {}, io = consoleIo) {
|
|
5322
|
+
const located = await locateOpenHandoff(key);
|
|
5323
|
+
if (!located) return fail(`handoff decline: no open handoff matching ${key}`);
|
|
5324
|
+
if (opts.json) return io.log(JSON.stringify({ ok: true, unchanged: true, handoff: located.item.record }));
|
|
5325
|
+
io.log(`handoff decline: left open for re-offer \u2014 ${formatHandoffLine(located.item)}`);
|
|
5326
|
+
}
|
|
5327
|
+
function registerHandoffCommands(program3) {
|
|
5328
|
+
const handoff = program3.command("handoff").description("explicit saga + North Star handoff lifecycle");
|
|
5329
|
+
handoff.command("open [summary]").description("open a handoff bound to the current North Star slug").option("--key <slug|#issue>", "handoff key (defaults to the current North Star slug)").option("--north-star-slug <slug>", "North Star slug to bind (defaults to current saga anchor slug)").option("--json", "machine-readable output").action((summary, opts) => runHandoffOpen(summary, opts));
|
|
5330
|
+
handoff.command("list").description("list open handoffs for the current repo/branch").option("--all", "include claimed/cancelled records").option("--json", "machine-readable output").action(async (opts) => {
|
|
5331
|
+
await runHandoffList(opts);
|
|
5332
|
+
});
|
|
5333
|
+
handoff.command("accept <key>").description("claim an open handoff and bind this session to its North Star").option("--json", "machine-readable output").action((key, opts) => closeSourceHandoff(key, "claimed", opts));
|
|
5334
|
+
handoff.command("cancel <key>").description("close an open handoff without claiming it").option("--json", "machine-readable output").action((key, opts) => closeSourceHandoff(key, "cancelled", opts));
|
|
5335
|
+
handoff.command("decline <key>").description("leave an open handoff unchanged so it is re-offered later").option("--json", "machine-readable output").action((key, opts) => runHandoffDecline(key, opts));
|
|
5336
|
+
}
|
|
5337
|
+
|
|
5072
5338
|
// src/honcho-commands.ts
|
|
5073
5339
|
var import_node_child_process5 = require("node:child_process");
|
|
5074
5340
|
var import_promises3 = require("node:fs/promises");
|
|
5075
|
-
var
|
|
5341
|
+
var import_node_fs10 = require("node:fs");
|
|
5076
5342
|
var import_node_path8 = require("node:path");
|
|
5077
5343
|
|
|
5078
5344
|
// src/honcho-ingest-skip.ts
|
|
5079
|
-
var
|
|
5345
|
+
var import_node_fs9 = require("node:fs");
|
|
5080
5346
|
var import_node_path7 = require("node:path");
|
|
5081
5347
|
var INGEST_SKIP_FILE = ".mmi/honcho/last-skip.json";
|
|
5082
5348
|
function recordIngestSkip(record) {
|
|
5083
5349
|
try {
|
|
5084
|
-
(0,
|
|
5085
|
-
(0,
|
|
5350
|
+
(0, import_node_fs9.mkdirSync)((0, import_node_path7.dirname)(INGEST_SKIP_FILE), { recursive: true });
|
|
5351
|
+
(0, import_node_fs9.writeFileSync)(INGEST_SKIP_FILE, JSON.stringify(record), "utf8");
|
|
5086
5352
|
} catch {
|
|
5087
5353
|
}
|
|
5088
5354
|
}
|
|
5089
5355
|
function readIngestSkip() {
|
|
5090
5356
|
try {
|
|
5091
|
-
if (!(0,
|
|
5092
|
-
const o = JSON.parse((0,
|
|
5357
|
+
if (!(0, import_node_fs9.existsSync)(INGEST_SKIP_FILE)) return null;
|
|
5358
|
+
const o = JSON.parse((0, import_node_fs9.readFileSync)(INGEST_SKIP_FILE, "utf8"));
|
|
5093
5359
|
return o?.reason && o?.surface && o?.ts ? o : null;
|
|
5094
5360
|
} catch {
|
|
5095
5361
|
return null;
|
|
@@ -5097,7 +5363,7 @@ function readIngestSkip() {
|
|
|
5097
5363
|
}
|
|
5098
5364
|
function clearIngestSkip() {
|
|
5099
5365
|
try {
|
|
5100
|
-
if ((0,
|
|
5366
|
+
if ((0, import_node_fs9.existsSync)(INGEST_SKIP_FILE)) (0, import_node_fs9.unlinkSync)(INGEST_SKIP_FILE);
|
|
5101
5367
|
} catch {
|
|
5102
5368
|
}
|
|
5103
5369
|
}
|
|
@@ -5159,7 +5425,8 @@ function formatVaultPointer(p) {
|
|
|
5159
5425
|
``,
|
|
5160
5426
|
`enumerate actual keys: mmi-cli secrets list`,
|
|
5161
5427
|
`read one: mmi-cli secrets get <stage>/<KEY> (e.g. main/GOOGLE_CLIENT_ID)`,
|
|
5162
|
-
`set a project-tier key: mmi-cli secrets set <KEY> (value via stdin; org tier needs a master grant)
|
|
5428
|
+
`set a project-tier key: mmi-cli secrets set <KEY> (value via stdin; org tier needs a master grant)`,
|
|
5429
|
+
`copy provider keys: mmi-cli secrets copy --from rc --to dev --keys RECALL_API_KEY,GEMINI_API_KEY`
|
|
5163
5430
|
];
|
|
5164
5431
|
return lines.join("\n");
|
|
5165
5432
|
}
|
|
@@ -5503,6 +5770,54 @@ async function secretsRevoke(deps, repo, login, key, _opts) {
|
|
|
5503
5770
|
}
|
|
5504
5771
|
deps.log(`revoked @${login}'s access to ${key} in ${repo}`);
|
|
5505
5772
|
}
|
|
5773
|
+
var SECRET_COPY_BLOCKED_RE = /(?:ENC_KEY|ENCRYPTION_KEY|SECRET_KEY_BASE)/i;
|
|
5774
|
+
function isSecretCopyBlocked(key) {
|
|
5775
|
+
const slash = key.indexOf("/");
|
|
5776
|
+
const leaf = slash === -1 ? key : key.slice(slash + 1);
|
|
5777
|
+
return SECRET_COPY_BLOCKED_RE.test(leaf);
|
|
5778
|
+
}
|
|
5779
|
+
function copyTierKey(stage2, leaf) {
|
|
5780
|
+
return `${stage2}/${leaf}`;
|
|
5781
|
+
}
|
|
5782
|
+
async function secretsCopy(deps, opts) {
|
|
5783
|
+
if (opts.from === opts.to) {
|
|
5784
|
+
deps.err("secrets copy: --from and --to must differ");
|
|
5785
|
+
return false;
|
|
5786
|
+
}
|
|
5787
|
+
const keys = [...new Set(opts.keys.map((k) => k.trim()).filter(Boolean))];
|
|
5788
|
+
if (!keys.length) {
|
|
5789
|
+
deps.err("secrets copy: --keys required (comma-separated allowlist)");
|
|
5790
|
+
return false;
|
|
5791
|
+
}
|
|
5792
|
+
for (const key of keys) {
|
|
5793
|
+
if (!isValidSecretKey(key)) {
|
|
5794
|
+
deps.err(`invalid secret key ${JSON.stringify(key)}`);
|
|
5795
|
+
return false;
|
|
5796
|
+
}
|
|
5797
|
+
if (isSecretCopyBlocked(key)) {
|
|
5798
|
+
deps.err(`secrets copy: ${key} is not copyable \u2014 generate encryption/stage-distinct keys per stage`);
|
|
5799
|
+
return false;
|
|
5800
|
+
}
|
|
5801
|
+
}
|
|
5802
|
+
for (const key of keys) {
|
|
5803
|
+
const leaf = secretLeafName(key);
|
|
5804
|
+
const srcKey = copyTierKey(opts.from, leaf);
|
|
5805
|
+
const dstKey = copyTierKey(opts.to, leaf);
|
|
5806
|
+
const value = await fetchSecretValue(deps, srcKey, opts);
|
|
5807
|
+
if (!value) {
|
|
5808
|
+
deps.err(`secrets copy: could not read source ${srcKey}`);
|
|
5809
|
+
return false;
|
|
5810
|
+
}
|
|
5811
|
+
if (opts.dryRun) {
|
|
5812
|
+
deps.log(`would copy ${srcKey} \u2192 ${dstKey}`);
|
|
5813
|
+
continue;
|
|
5814
|
+
}
|
|
5815
|
+
const ok = await putSecret(deps, dstKey, value, opts);
|
|
5816
|
+
if (!ok) return false;
|
|
5817
|
+
deps.log(`copied ${srcKey} \u2192 ${dstKey}`);
|
|
5818
|
+
}
|
|
5819
|
+
return true;
|
|
5820
|
+
}
|
|
5506
5821
|
async function secretsUse(deps, key, opts) {
|
|
5507
5822
|
const slug = await vaultSlug(deps, opts);
|
|
5508
5823
|
const tier = classifyTier(slug, key);
|
|
@@ -5580,7 +5895,7 @@ async function resolveHonchoConfig(cfg = {}, opts = {}) {
|
|
|
5580
5895
|
}
|
|
5581
5896
|
|
|
5582
5897
|
// src/honcho-ingest.ts
|
|
5583
|
-
var
|
|
5898
|
+
var import_node_crypto5 = require("node:crypto");
|
|
5584
5899
|
var DEFAULT_HONCHO_MAX_CHARS = 4e3;
|
|
5585
5900
|
var DEFAULT_HONCHO_CARD_MAX_CHARS = 1200;
|
|
5586
5901
|
var REDACTED = "[REDACTED]";
|
|
@@ -5666,7 +5981,7 @@ function buildIngestPayload(args) {
|
|
|
5666
5981
|
const maxChars = args.maxChars ?? DEFAULT_HONCHO_MAX_CHARS;
|
|
5667
5982
|
const messages = args.messages.map((m) => ({ role: m.role, content: capContent(redactSecrets(m.content), maxChars) })).filter((m) => m.content.trim().length > 0);
|
|
5668
5983
|
return {
|
|
5669
|
-
id: args.id ?? (0,
|
|
5984
|
+
id: args.id ?? (0, import_node_crypto5.randomUUID)(),
|
|
5670
5985
|
workspace: args.workspace,
|
|
5671
5986
|
peer: args.peer,
|
|
5672
5987
|
session: args.session,
|
|
@@ -5873,7 +6188,7 @@ function honchoThrottlePath(key) {
|
|
|
5873
6188
|
function honchoIngestDue(path2, intervalMs, now = Date.now()) {
|
|
5874
6189
|
if (intervalMs <= 0) return true;
|
|
5875
6190
|
try {
|
|
5876
|
-
const last = Number((0,
|
|
6191
|
+
const last = Number((0, import_node_fs10.readFileSync)(path2, "utf8").trim()) || 0;
|
|
5877
6192
|
return now - last >= intervalMs;
|
|
5878
6193
|
} catch {
|
|
5879
6194
|
return true;
|
|
@@ -5881,8 +6196,8 @@ function honchoIngestDue(path2, intervalMs, now = Date.now()) {
|
|
|
5881
6196
|
}
|
|
5882
6197
|
function markHonchoIngest(path2, now = Date.now()) {
|
|
5883
6198
|
try {
|
|
5884
|
-
(0,
|
|
5885
|
-
(0,
|
|
6199
|
+
(0, import_node_fs10.mkdirSync)((0, import_node_path8.dirname)(path2), { recursive: true });
|
|
6200
|
+
(0, import_node_fs10.writeFileSync)(path2, String(now), "utf8");
|
|
5886
6201
|
} catch {
|
|
5887
6202
|
}
|
|
5888
6203
|
}
|
|
@@ -6165,8 +6480,205 @@ async function syncDocs(deps, docs2 = SYNCED_DOCS) {
|
|
|
6165
6480
|
}
|
|
6166
6481
|
|
|
6167
6482
|
// src/session-start.ts
|
|
6168
|
-
var
|
|
6483
|
+
var import_node_fs12 = require("node:fs");
|
|
6484
|
+
var import_node_path10 = require("node:path");
|
|
6485
|
+
|
|
6486
|
+
// src/scratch-gc.ts
|
|
6487
|
+
var import_node_fs11 = require("node:fs");
|
|
6169
6488
|
var import_node_path9 = require("node:path");
|
|
6489
|
+
var TMP_STALE_MS = 6e4;
|
|
6490
|
+
var FLUSH_LOCK_GC_MS = 6e5;
|
|
6491
|
+
var HEAD_TS_STALE_MS = 48 * 36e5;
|
|
6492
|
+
var LAST_SKIP_STALE_MS = 24 * 36e5;
|
|
6493
|
+
var CONFLICT_COPY_STALE_MS = 24 * 36e5;
|
|
6494
|
+
var PLAN_ADVISORY_AGE_MS = 30 * 24 * 36e5;
|
|
6495
|
+
var SCRATCH_GC_THROTTLE_MS = 24 * 36e5;
|
|
6496
|
+
function scratchGcThrottlePath(mmiRoot) {
|
|
6497
|
+
return (0, import_node_path9.join)(mmiRoot, "head-ts", ".scratch-gc-last");
|
|
6498
|
+
}
|
|
6499
|
+
function scratchGcDue(stampPath, now = Date.now(), read = import_node_fs11.readFileSync) {
|
|
6500
|
+
try {
|
|
6501
|
+
return now - (Number(read(stampPath, "utf8").trim()) || 0) >= SCRATCH_GC_THROTTLE_MS;
|
|
6502
|
+
} catch {
|
|
6503
|
+
return true;
|
|
6504
|
+
}
|
|
6505
|
+
}
|
|
6506
|
+
function markScratchGcRun(stampPath, now = Date.now()) {
|
|
6507
|
+
try {
|
|
6508
|
+
(0, import_node_fs11.mkdirSync)((0, import_node_path9.dirname)(stampPath), { recursive: true });
|
|
6509
|
+
(0, import_node_fs11.writeFileSync)(stampPath, String(now), "utf8");
|
|
6510
|
+
} catch {
|
|
6511
|
+
}
|
|
6512
|
+
}
|
|
6513
|
+
function executeScratchGc(repoRoot, opts, now = Date.now()) {
|
|
6514
|
+
const snap = collectScratchSnapshot(repoRoot);
|
|
6515
|
+
const plan2 = planScratchGc(snap, now);
|
|
6516
|
+
if (!opts.apply) return { plan: plan2 };
|
|
6517
|
+
return { plan: plan2, applied: applyScratchGc(plan2, snap.mmiRoot, now) };
|
|
6518
|
+
}
|
|
6519
|
+
var NEVER_BASENAMES = /* @__PURE__ */ new Set([".session", "config.json", "saga-pending.jsonl"]);
|
|
6520
|
+
var TMP_SIDECAR_BASES = ["saga-pending.jsonl", "last-skip.json"];
|
|
6521
|
+
var CONFLICT_COPY_ALLOWLIST = /* @__PURE__ */ new Set(["saga-pending.jsonl", "last-skip.json"]);
|
|
6522
|
+
function conflictCopyOriginal(name) {
|
|
6523
|
+
const m = /^(.+) \d+(\.[^.]+)$/.exec(name);
|
|
6524
|
+
return m ? `${m[1]}${m[2]}` : null;
|
|
6525
|
+
}
|
|
6526
|
+
function isTmpSidecar(name) {
|
|
6527
|
+
return name.endsWith(".tmp") && TMP_SIDECAR_BASES.some((b) => name.startsWith(`${b}.`));
|
|
6528
|
+
}
|
|
6529
|
+
function planScratchGc(snap, now = Date.now()) {
|
|
6530
|
+
const candidates = [];
|
|
6531
|
+
const normalizePath = (p) => p.replace(/\\/g, "/");
|
|
6532
|
+
const mmiPaths = new Set(snap.mmiFiles.map((f) => normalizePath(f.path)));
|
|
6533
|
+
const headTsPrefix = `${snap.mmiRoot.replace(/[\\/]+$/, "")}/head-ts/`.replace(/\\/g, "/");
|
|
6534
|
+
const days = (ms) => `${Math.floor(ms / 864e5)}d`;
|
|
6535
|
+
for (const f of snap.mmiFiles) {
|
|
6536
|
+
const age = now - f.mtimeMs;
|
|
6537
|
+
const add = (family, reason) => candidates.push({ path: f.path, family, tier: "safe-auto", reason, bytes: f.bytes });
|
|
6538
|
+
if (isTmpSidecar(f.name)) {
|
|
6539
|
+
if (age > TMP_STALE_MS) add("tmp-sidecar", `crashed atomic-write sidecar (${days(age)} old)`);
|
|
6540
|
+
continue;
|
|
6541
|
+
}
|
|
6542
|
+
if (f.name === "saga-flush.lock") {
|
|
6543
|
+
if (age > FLUSH_LOCK_GC_MS) add("flush-lock", `abandoned flush lock (${days(age)} old)`);
|
|
6544
|
+
continue;
|
|
6545
|
+
}
|
|
6546
|
+
if (NEVER_BASENAMES.has(f.name)) continue;
|
|
6547
|
+
if (f.path.replace(/\\/g, "/").startsWith(headTsPrefix) && !f.name.startsWith(".")) {
|
|
6548
|
+
if (age > HEAD_TS_STALE_MS) add("head-ts", `stale throttle stamp (${days(age)} old)`);
|
|
6549
|
+
continue;
|
|
6550
|
+
}
|
|
6551
|
+
if (f.name === "last-skip.json") {
|
|
6552
|
+
if (age > LAST_SKIP_STALE_MS) add("last-skip", `stale ingest-skip hint (${days(age)} old)`);
|
|
6553
|
+
continue;
|
|
6554
|
+
}
|
|
6555
|
+
const orig = conflictCopyOriginal(f.name);
|
|
6556
|
+
if (orig && CONFLICT_COPY_ALLOWLIST.has(orig) && mmiPaths.has(normalizePath((0, import_node_path9.join)(f.dir, orig))) && age > CONFLICT_COPY_STALE_MS) {
|
|
6557
|
+
add("conflict-copy", `cloud-sync conflict copy of ${orig} (${days(age)} old)`);
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
if (snap.syncQueueSlugs) {
|
|
6561
|
+
for (const f of snap.planMdFiles) {
|
|
6562
|
+
const slug = f.name.replace(/\.md$/, "");
|
|
6563
|
+
if (snap.syncQueueSlugs.has(slug)) continue;
|
|
6564
|
+
if (now - f.mtimeMs > PLAN_ADVISORY_AGE_MS) {
|
|
6565
|
+
candidates.push({
|
|
6566
|
+
path: f.path,
|
|
6567
|
+
family: "plan",
|
|
6568
|
+
tier: "advisory",
|
|
6569
|
+
reason: `synced plan older than ${Math.floor(PLAN_ADVISORY_AGE_MS / 864e5)}d`,
|
|
6570
|
+
bytes: f.bytes
|
|
6571
|
+
});
|
|
6572
|
+
}
|
|
6573
|
+
}
|
|
6574
|
+
}
|
|
6575
|
+
return {
|
|
6576
|
+
candidates,
|
|
6577
|
+
safeAuto: candidates.filter((c) => c.tier === "safe-auto"),
|
|
6578
|
+
advisory: candidates.filter((c) => c.tier === "advisory")
|
|
6579
|
+
};
|
|
6580
|
+
}
|
|
6581
|
+
function formatScratchGcPlan(plan2, applied) {
|
|
6582
|
+
if (plan2.candidates.length === 0) return "scratch GC: nothing to prune \u2014 local saga/plan/honcho scratch is clean.";
|
|
6583
|
+
const lines = [];
|
|
6584
|
+
const bytes = plan2.safeAuto.reduce((n, c) => n + c.bytes, 0);
|
|
6585
|
+
lines.push(
|
|
6586
|
+
`scratch GC: ${applied ? "pruned" : "would prune"} ${plan2.safeAuto.length} stale file(s) (${bytes} bytes)` + (plan2.advisory.length ? `; ${plan2.advisory.length} advisory (kept):` : ":")
|
|
6587
|
+
);
|
|
6588
|
+
for (const c of plan2.safeAuto.slice(0, 40)) lines.push(` - [${c.family}] ${c.path} \u2014 ${c.reason}`);
|
|
6589
|
+
if (plan2.safeAuto.length > 40) lines.push(` ... +${plan2.safeAuto.length - 40} more`);
|
|
6590
|
+
for (const c of plan2.advisory.slice(0, 20)) lines.push(` \xB7 [advisory] ${c.path} \u2014 ${c.reason} (kept; review manually)`);
|
|
6591
|
+
return lines.join("\n");
|
|
6592
|
+
}
|
|
6593
|
+
function scratchGcBannerLine(safeAutoPruned, advisory) {
|
|
6594
|
+
const parts = [];
|
|
6595
|
+
if (safeAutoPruned > 0) parts.push(`pruned ${safeAutoPruned} stale local file(s)`);
|
|
6596
|
+
if (advisory > 0) parts.push(`${advisory} old synced plan(s) prunable \u2014 \`mmi-cli gc --scratch --dry-run\` to review`);
|
|
6597
|
+
return parts.length ? `[cleanup] ${parts.join("; ")}` : void 0;
|
|
6598
|
+
}
|
|
6599
|
+
function applyScratchGc(plan2, mmiRoot, now = Date.now()) {
|
|
6600
|
+
const result = { pruned: [], skipped: 0, bytes: 0 };
|
|
6601
|
+
let anchor;
|
|
6602
|
+
try {
|
|
6603
|
+
anchor = (0, import_node_fs11.realpathSync)(mmiRoot).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
6604
|
+
} catch {
|
|
6605
|
+
return result;
|
|
6606
|
+
}
|
|
6607
|
+
for (const c of plan2.safeAuto) {
|
|
6608
|
+
try {
|
|
6609
|
+
const real = (0, import_node_fs11.realpathSync)(c.path).replace(/\\/g, "/");
|
|
6610
|
+
if (real !== anchor && !real.startsWith(`${anchor}/`)) {
|
|
6611
|
+
result.skipped += 1;
|
|
6612
|
+
continue;
|
|
6613
|
+
}
|
|
6614
|
+
const st = (0, import_node_fs11.statSync)(c.path);
|
|
6615
|
+
const floor = c.family === "flush-lock" ? FLUSH_LOCK_GC_MS : c.family === "tmp-sidecar" ? TMP_STALE_MS : c.family === "head-ts" ? HEAD_TS_STALE_MS : c.family === "last-skip" ? LAST_SKIP_STALE_MS : CONFLICT_COPY_STALE_MS;
|
|
6616
|
+
if (now - st.mtimeMs <= floor) {
|
|
6617
|
+
result.skipped += 1;
|
|
6618
|
+
continue;
|
|
6619
|
+
}
|
|
6620
|
+
(0, import_node_fs11.unlinkSync)(c.path);
|
|
6621
|
+
result.pruned.push(c.path);
|
|
6622
|
+
result.bytes += c.bytes;
|
|
6623
|
+
} catch {
|
|
6624
|
+
result.skipped += 1;
|
|
6625
|
+
}
|
|
6626
|
+
}
|
|
6627
|
+
return result;
|
|
6628
|
+
}
|
|
6629
|
+
function collectScratchSnapshot(repoRoot, deps = {}) {
|
|
6630
|
+
const readdir = deps.readdir ?? import_node_fs11.readdirSync;
|
|
6631
|
+
const stat = deps.stat ?? import_node_fs11.statSync;
|
|
6632
|
+
const readFile5 = deps.readFile ?? import_node_fs11.readFileSync;
|
|
6633
|
+
const mmiRoot = (0, import_node_path9.join)(repoRoot, ".mmi");
|
|
6634
|
+
const plansRoot = (0, import_node_path9.join)(repoRoot, "plans");
|
|
6635
|
+
const mmiFiles = [];
|
|
6636
|
+
try {
|
|
6637
|
+
for (const ent of readdir(mmiRoot, { recursive: true, withFileTypes: true })) {
|
|
6638
|
+
if (!ent.isFile()) continue;
|
|
6639
|
+
const dir = ent.parentPath ?? ent.path ?? mmiRoot;
|
|
6640
|
+
const full = (0, import_node_path9.join)(dir, ent.name);
|
|
6641
|
+
try {
|
|
6642
|
+
const st = stat(full);
|
|
6643
|
+
mmiFiles.push({ path: full, dir, name: ent.name, mtimeMs: st.mtimeMs, bytes: st.size });
|
|
6644
|
+
} catch {
|
|
6645
|
+
}
|
|
6646
|
+
}
|
|
6647
|
+
} catch {
|
|
6648
|
+
}
|
|
6649
|
+
const planMdFiles = [];
|
|
6650
|
+
try {
|
|
6651
|
+
for (const ent of readdir(plansRoot, { withFileTypes: true })) {
|
|
6652
|
+
if (!ent.isFile() || !ent.name.endsWith(".md")) continue;
|
|
6653
|
+
const full = (0, import_node_path9.join)(plansRoot, ent.name);
|
|
6654
|
+
try {
|
|
6655
|
+
const st = stat(full);
|
|
6656
|
+
planMdFiles.push({ path: full, dir: plansRoot, name: ent.name, mtimeMs: st.mtimeMs, bytes: st.size });
|
|
6657
|
+
} catch {
|
|
6658
|
+
}
|
|
6659
|
+
}
|
|
6660
|
+
} catch {
|
|
6661
|
+
}
|
|
6662
|
+
let syncQueueSlugs = null;
|
|
6663
|
+
let queueRaw;
|
|
6664
|
+
try {
|
|
6665
|
+
queueRaw = readFile5((0, import_node_path9.join)(plansRoot, ".sync-queue.json"), "utf8");
|
|
6666
|
+
} catch {
|
|
6667
|
+
syncQueueSlugs = /* @__PURE__ */ new Set();
|
|
6668
|
+
}
|
|
6669
|
+
if (queueRaw !== void 0) {
|
|
6670
|
+
try {
|
|
6671
|
+
const parsed = JSON.parse(queueRaw);
|
|
6672
|
+
const entries = Array.isArray(parsed) ? parsed : parsed.entries ?? [];
|
|
6673
|
+
syncQueueSlugs = new Set(entries.map((e) => e.slug).filter((s) => typeof s === "string"));
|
|
6674
|
+
} catch {
|
|
6675
|
+
syncQueueSlugs = null;
|
|
6676
|
+
}
|
|
6677
|
+
}
|
|
6678
|
+
return { mmiRoot, mmiFiles, planMdFiles, syncQueueSlugs };
|
|
6679
|
+
}
|
|
6680
|
+
|
|
6681
|
+
// src/session-start.ts
|
|
6170
6682
|
async function runBufferedStep(step) {
|
|
6171
6683
|
const lines = [];
|
|
6172
6684
|
const io = {
|
|
@@ -6194,6 +6706,7 @@ function buildSessionStartPlan(verbs) {
|
|
|
6194
6706
|
parallel: [
|
|
6195
6707
|
{ name: "rules sync", run: verbs.rulesSync },
|
|
6196
6708
|
{ name: "saga show", run: verbs.sagaShow },
|
|
6709
|
+
{ name: "handoff offer", run: verbs.handoffOffer },
|
|
6197
6710
|
// honcho profile (#1162): the behavioral-memory prior, flushed right after the saga resume. A fast
|
|
6198
6711
|
// peer-card GET, fail-soft + silent when off/empty — it never hangs or noises the banner.
|
|
6199
6712
|
{ name: "honcho profile", run: verbs.honchoContext },
|
|
@@ -6217,12 +6730,12 @@ function spawnDetachedSelf(args, deps) {
|
|
|
6217
6730
|
}
|
|
6218
6731
|
function planStoreLines(cwd) {
|
|
6219
6732
|
const mdFiles = (dir, minSize = 0) => {
|
|
6220
|
-
const p = (0,
|
|
6221
|
-
if (!(0,
|
|
6733
|
+
const p = (0, import_node_path10.join)(cwd, dir);
|
|
6734
|
+
if (!(0, import_node_fs12.existsSync)(p)) return [];
|
|
6222
6735
|
try {
|
|
6223
|
-
return (0,
|
|
6736
|
+
return (0, import_node_fs12.readdirSync)(p).filter((f) => f.toLowerCase().endsWith(".md")).filter((f) => {
|
|
6224
6737
|
try {
|
|
6225
|
-
return (0,
|
|
6738
|
+
return (0, import_node_fs12.statSync)((0, import_node_path10.join)(p, f)).size >= minSize;
|
|
6226
6739
|
} catch {
|
|
6227
6740
|
return false;
|
|
6228
6741
|
}
|
|
@@ -6240,6 +6753,19 @@ function planStoreLines(cwd) {
|
|
|
6240
6753
|
out.push(`[plan-store] ${localPlans.length} local plan(s) in plans/ \u2014 ensure new/changed ones are \`mmi-cli plan push\`ed (S3-backed, not git).`);
|
|
6241
6754
|
return out;
|
|
6242
6755
|
}
|
|
6756
|
+
function scratchGcLines(cwd, env = process.env, now = Date.now()) {
|
|
6757
|
+
if (env.MMI_NO_AUTO_GC) return [];
|
|
6758
|
+
try {
|
|
6759
|
+
const stamp = scratchGcThrottlePath((0, import_node_path10.join)(cwd, ".mmi"));
|
|
6760
|
+
if (!scratchGcDue(stamp, now)) return [];
|
|
6761
|
+
const run = executeScratchGc(cwd, { apply: true }, now);
|
|
6762
|
+
markScratchGcRun(stamp, now);
|
|
6763
|
+
const line = scratchGcBannerLine(run.applied?.pruned.length ?? 0, run.plan.advisory.length);
|
|
6764
|
+
return line ? [line] : [];
|
|
6765
|
+
} catch {
|
|
6766
|
+
return [];
|
|
6767
|
+
}
|
|
6768
|
+
}
|
|
6243
6769
|
function northstarPointer(injected = false) {
|
|
6244
6770
|
if (injected) {
|
|
6245
6771
|
return "North Stars: `mmi-cli northstar relevant` for more matches; `northstar pull <slug>` for the full SSOT.";
|
|
@@ -6287,6 +6813,71 @@ function recoverPriorityFromEvents(events) {
|
|
|
6287
6813
|
return found;
|
|
6288
6814
|
}
|
|
6289
6815
|
|
|
6816
|
+
// src/board-dependency.ts
|
|
6817
|
+
var DEPENDS_ON_LINE = /^\s*[-*]?\s*\*\*Depends on:\*\*\s*(.+)$/im;
|
|
6818
|
+
var ISSUE_REF = /([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+)#(\d+)/g;
|
|
6819
|
+
function parseDependsOnRefs(body) {
|
|
6820
|
+
const match = body.match(DEPENDS_ON_LINE);
|
|
6821
|
+
if (!match) return [];
|
|
6822
|
+
const refs = [];
|
|
6823
|
+
for (const m of match[1].matchAll(ISSUE_REF)) {
|
|
6824
|
+
refs.push({ repo: m[1], number: Number(m[2]) });
|
|
6825
|
+
}
|
|
6826
|
+
return refs;
|
|
6827
|
+
}
|
|
6828
|
+
async function issueOpen(client, repo, number) {
|
|
6829
|
+
try {
|
|
6830
|
+
const data = await client.rest("GET", `repos/${repo}/issues/${number}`);
|
|
6831
|
+
return data?.state === "OPEN";
|
|
6832
|
+
} catch {
|
|
6833
|
+
return void 0;
|
|
6834
|
+
}
|
|
6835
|
+
}
|
|
6836
|
+
async function dependencyBlocksClaim(client, body) {
|
|
6837
|
+
const refs = parseDependsOnRefs(body);
|
|
6838
|
+
if (!refs.length) return { blocked: false, openDependencies: [] };
|
|
6839
|
+
const openDependencies = [];
|
|
6840
|
+
for (const ref of refs) {
|
|
6841
|
+
const open = await issueOpen(client, ref.repo, ref.number);
|
|
6842
|
+
if (open === true) openDependencies.push(`${ref.repo}#${ref.number}`);
|
|
6843
|
+
}
|
|
6844
|
+
return { blocked: openDependencies.length > 0, openDependencies };
|
|
6845
|
+
}
|
|
6846
|
+
async function filterDependencyBlockedClaimables(items, client, opts = {}) {
|
|
6847
|
+
const claimable = [];
|
|
6848
|
+
const blocked = [];
|
|
6849
|
+
const warnings = [];
|
|
6850
|
+
for (const item of items) {
|
|
6851
|
+
if (item.contentType !== "Issue") {
|
|
6852
|
+
claimable.push(item);
|
|
6853
|
+
continue;
|
|
6854
|
+
}
|
|
6855
|
+
let body = item.details?.body;
|
|
6856
|
+
if (body === void 0) {
|
|
6857
|
+
if (opts.requireBundledBody) {
|
|
6858
|
+
claimable.push(item);
|
|
6859
|
+
continue;
|
|
6860
|
+
}
|
|
6861
|
+
try {
|
|
6862
|
+
const data = await client.rest("GET", `repos/${item.repository}/issues/${item.number}`);
|
|
6863
|
+
body = data?.body ?? "";
|
|
6864
|
+
} catch (e) {
|
|
6865
|
+
warnings.push(`dependency check skipped for ${item.ref}: ${e.message}`);
|
|
6866
|
+
claimable.push(item);
|
|
6867
|
+
continue;
|
|
6868
|
+
}
|
|
6869
|
+
}
|
|
6870
|
+
const gate = await dependencyBlocksClaim(client, body);
|
|
6871
|
+
if (gate.blocked) {
|
|
6872
|
+
blocked.push(item);
|
|
6873
|
+
warnings.push(`${item.ref} blocked on open ${gate.openDependencies.join(", ")}`);
|
|
6874
|
+
} else {
|
|
6875
|
+
claimable.push(item);
|
|
6876
|
+
}
|
|
6877
|
+
}
|
|
6878
|
+
return { claimable, blocked, warnings };
|
|
6879
|
+
}
|
|
6880
|
+
|
|
6290
6881
|
// ../infra/board-vocab.mjs
|
|
6291
6882
|
var BOARD_STATUSES = ["Todo", "In Progress", "In Review", "Done"];
|
|
6292
6883
|
|
|
@@ -6610,6 +7201,13 @@ async function readBoard(options, deps = {}) {
|
|
|
6610
7201
|
if (options.includeBundleDetails) {
|
|
6611
7202
|
await attachBundleDetails(report, client, options.allowPartial ?? false);
|
|
6612
7203
|
}
|
|
7204
|
+
for (const scope of ["primary", "secondary"]) {
|
|
7205
|
+
const filtered = await filterDependencyBlockedClaimables(report[scope].claimable, client, {
|
|
7206
|
+
requireBundledBody: Boolean(options.includeBundleDetails)
|
|
7207
|
+
});
|
|
7208
|
+
report[scope].claimable = filtered.claimable;
|
|
7209
|
+
report.warnings.push(...filtered.warnings);
|
|
7210
|
+
}
|
|
6613
7211
|
return report;
|
|
6614
7212
|
}
|
|
6615
7213
|
function findBoardItem(items, selector) {
|
|
@@ -6691,6 +7289,11 @@ async function prepareClaimContext(options, selectors, deps, collected) {
|
|
|
6691
7289
|
warnings: collected.warnings,
|
|
6692
7290
|
partial: collected.partial
|
|
6693
7291
|
};
|
|
7292
|
+
for (const scope of ["primary", "secondary"]) {
|
|
7293
|
+
const filtered = await filterDependencyBlockedClaimables(report[scope].claimable, client);
|
|
7294
|
+
report[scope].claimable = filtered.claimable;
|
|
7295
|
+
report.warnings.push(...filtered.warnings);
|
|
7296
|
+
}
|
|
6694
7297
|
return { cfg, client, items: collected.items, writable: writable.repos, report };
|
|
6695
7298
|
}
|
|
6696
7299
|
async function claimOneBoardItem(ctx, selector, options) {
|
|
@@ -6699,6 +7302,21 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
6699
7302
|
if (flatItem.status === "Todo" && flatItem.assignees.length === 0 && !ctx.writable.has(flatItem.repository.toLowerCase())) {
|
|
6700
7303
|
throw new Error(`${flatItem.ref} is not claimable: viewer does not have write access to ${flatItem.repository}`);
|
|
6701
7304
|
}
|
|
7305
|
+
if (flatItem.contentType === "Issue") {
|
|
7306
|
+
let body = flatItem.details?.body;
|
|
7307
|
+
if (body === void 0) {
|
|
7308
|
+
try {
|
|
7309
|
+
const data = await client.rest("GET", `repos/${flatItem.repository}/issues/${flatItem.number}`);
|
|
7310
|
+
body = data?.body ?? "";
|
|
7311
|
+
} catch {
|
|
7312
|
+
body = "";
|
|
7313
|
+
}
|
|
7314
|
+
}
|
|
7315
|
+
const gate = await dependencyBlocksClaim(client, body);
|
|
7316
|
+
if (gate.blocked) {
|
|
7317
|
+
throw new Error(`${flatItem.ref} is not claimable: blocked on open ${gate.openDependencies.join(", ")}`);
|
|
7318
|
+
}
|
|
7319
|
+
}
|
|
6702
7320
|
let item = findClaimableItem(report, selector);
|
|
6703
7321
|
if (item.contentType !== "Issue") throw new Error(`${item.ref} is not an issue`);
|
|
6704
7322
|
const fresh = (await fetchIssueProjectItem(client, cfg, { repo: item.repository, number: item.number })).item;
|
|
@@ -6824,6 +7442,73 @@ async function backfillBoardPriorities(options, deps = {}) {
|
|
|
6824
7442
|
}
|
|
6825
7443
|
return result;
|
|
6826
7444
|
}
|
|
7445
|
+
async function prunePriorityLabels(options, deps = {}) {
|
|
7446
|
+
const cfg = resolveBoardConfig(options.config);
|
|
7447
|
+
const client = deps.client ?? defaultGitHubClient();
|
|
7448
|
+
const collected = await collectBoardItems(cfg, { repo: options.repo }, deps);
|
|
7449
|
+
const issues = collected.items.filter(
|
|
7450
|
+
(item) => item.contentType === "Issue" && item.labels.some((l) => labelToFieldPriority(l))
|
|
7451
|
+
);
|
|
7452
|
+
const concurrency = Math.max(1, options.concurrency ?? 8);
|
|
7453
|
+
const result = {
|
|
7454
|
+
scanned: issues.length,
|
|
7455
|
+
pruned: 0,
|
|
7456
|
+
removedLabels: 0,
|
|
7457
|
+
skippedNoField: 0,
|
|
7458
|
+
failed: 0,
|
|
7459
|
+
details: []
|
|
7460
|
+
};
|
|
7461
|
+
async function work(item) {
|
|
7462
|
+
const priorityLabels = item.labels.filter((l) => labelToFieldPriority(l));
|
|
7463
|
+
if (!priorityLabels.length) return;
|
|
7464
|
+
if (!item.priority) {
|
|
7465
|
+
result.skippedNoField += 1;
|
|
7466
|
+
result.details.push(
|
|
7467
|
+
`${item.ref}: Priority field unset \u2014 kept ${priorityLabels.join(", ")} (run \`mmi-cli board backfill-priority\` first)`
|
|
7468
|
+
);
|
|
7469
|
+
return;
|
|
7470
|
+
}
|
|
7471
|
+
if (options.dryRun) {
|
|
7472
|
+
result.pruned += 1;
|
|
7473
|
+
result.removedLabels += priorityLabels.length;
|
|
7474
|
+
result.details.push(`${item.ref} \u2192 remove ${priorityLabels.join(", ")} (field=${item.priority}) (dry-run)`);
|
|
7475
|
+
return;
|
|
7476
|
+
}
|
|
7477
|
+
try {
|
|
7478
|
+
const removed = [];
|
|
7479
|
+
const alreadyAbsent = [];
|
|
7480
|
+
for (const label of priorityLabels) {
|
|
7481
|
+
try {
|
|
7482
|
+
await client.rest(
|
|
7483
|
+
"DELETE",
|
|
7484
|
+
`repos/${item.repository}/issues/${item.number}/labels/${encodeURIComponent(label)}`
|
|
7485
|
+
);
|
|
7486
|
+
removed.push(label);
|
|
7487
|
+
result.removedLabels += 1;
|
|
7488
|
+
} catch (e) {
|
|
7489
|
+
if (e instanceof GitHubApiError && e.status === 404) {
|
|
7490
|
+
alreadyAbsent.push(label);
|
|
7491
|
+
continue;
|
|
7492
|
+
}
|
|
7493
|
+
throw e;
|
|
7494
|
+
}
|
|
7495
|
+
}
|
|
7496
|
+
result.pruned += 1;
|
|
7497
|
+
const parts = [
|
|
7498
|
+
...removed.length ? [`removed ${removed.join(", ")}`] : [],
|
|
7499
|
+
...alreadyAbsent.length ? [`already absent ${alreadyAbsent.join(", ")}`] : []
|
|
7500
|
+
];
|
|
7501
|
+
result.details.push(`${item.ref} \u2192 ${parts.join("; ")} (field=${item.priority})`);
|
|
7502
|
+
} catch (e) {
|
|
7503
|
+
result.failed += 1;
|
|
7504
|
+
result.details.push(`${item.ref}: ${ghError(e)}`);
|
|
7505
|
+
}
|
|
7506
|
+
}
|
|
7507
|
+
for (let i = 0; i < issues.length; i += concurrency) {
|
|
7508
|
+
await Promise.all(issues.slice(i, i + concurrency).map(work));
|
|
7509
|
+
}
|
|
7510
|
+
return result;
|
|
7511
|
+
}
|
|
6827
7512
|
async function recoverIssuePriority(client, item) {
|
|
6828
7513
|
for (const label of item.labels) {
|
|
6829
7514
|
const fromLabel = labelToFieldPriority(label);
|
|
@@ -7082,6 +7767,109 @@ async function runBoardSlice(io, deps, opts) {
|
|
|
7082
7767
|
}
|
|
7083
7768
|
}
|
|
7084
7769
|
|
|
7770
|
+
// src/worktree.ts
|
|
7771
|
+
var import_node_fs13 = require("node:fs");
|
|
7772
|
+
var import_node_path11 = require("node:path");
|
|
7773
|
+
var LOCAL_ONLY_FILES = [".claude/settings.local.json"];
|
|
7774
|
+
var PKG = "package.json";
|
|
7775
|
+
var LOCKFILE = "package-lock.json";
|
|
7776
|
+
var NODE_MODULES = "node_modules";
|
|
7777
|
+
var realFsProbe = {
|
|
7778
|
+
isDir: (p) => {
|
|
7779
|
+
try {
|
|
7780
|
+
return (0, import_node_fs13.statSync)(p).isDirectory();
|
|
7781
|
+
} catch {
|
|
7782
|
+
return false;
|
|
7783
|
+
}
|
|
7784
|
+
},
|
|
7785
|
+
isFile: (p) => {
|
|
7786
|
+
try {
|
|
7787
|
+
return (0, import_node_fs13.statSync)(p).isFile();
|
|
7788
|
+
} catch {
|
|
7789
|
+
return false;
|
|
7790
|
+
}
|
|
7791
|
+
},
|
|
7792
|
+
listDirs: (p) => {
|
|
7793
|
+
try {
|
|
7794
|
+
return (0, import_node_fs13.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
7795
|
+
} catch {
|
|
7796
|
+
return [];
|
|
7797
|
+
}
|
|
7798
|
+
}
|
|
7799
|
+
};
|
|
7800
|
+
function scanInstallDirs(root, fs2 = realFsProbe) {
|
|
7801
|
+
const factsFor = (dir) => {
|
|
7802
|
+
const abs = dir ? (0, import_node_path11.join)(root, dir) : root;
|
|
7803
|
+
return {
|
|
7804
|
+
dir,
|
|
7805
|
+
hasPackageJson: fs2.isFile((0, import_node_path11.join)(abs, PKG)),
|
|
7806
|
+
hasLockfile: fs2.isFile((0, import_node_path11.join)(abs, LOCKFILE)),
|
|
7807
|
+
hasNodeModules: fs2.isDir((0, import_node_path11.join)(abs, NODE_MODULES))
|
|
7808
|
+
};
|
|
7809
|
+
};
|
|
7810
|
+
const children = fs2.listDirs(root).filter((name) => name !== NODE_MODULES && name !== ".git");
|
|
7811
|
+
return [factsFor(""), ...children.map(factsFor).filter((f) => f.hasPackageJson)];
|
|
7812
|
+
}
|
|
7813
|
+
function npmInstallTargets(dirs) {
|
|
7814
|
+
return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install" }));
|
|
7815
|
+
}
|
|
7816
|
+
function isLinkedWorktree(root, fs2 = realFsProbe) {
|
|
7817
|
+
return fs2.isFile((0, import_node_path11.join)(root, ".git"));
|
|
7818
|
+
}
|
|
7819
|
+
function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
|
|
7820
|
+
if (!isLinkedWorktree(root, fs2)) return null;
|
|
7821
|
+
const pending = npmInstallTargets(scanInstallDirs(root, fs2));
|
|
7822
|
+
if (!pending.length) return null;
|
|
7823
|
+
const where = pending.map((t) => t.dir || ".").join(", ");
|
|
7824
|
+
return `[worktree] provisioning tooling in the background (deps in ${where} + local config) \u2014 \`mmi-cli worktree setup\` to redo`;
|
|
7825
|
+
}
|
|
7826
|
+
function defaultCopyFile(from, to) {
|
|
7827
|
+
(0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(to), { recursive: true });
|
|
7828
|
+
(0, import_node_fs13.copyFileSync)(from, to);
|
|
7829
|
+
}
|
|
7830
|
+
async function provisionWorktree(worktreeRoot, deps) {
|
|
7831
|
+
const fs2 = deps.fs ?? realFsProbe;
|
|
7832
|
+
const copyFile = deps.copyFile ?? defaultCopyFile;
|
|
7833
|
+
const log = deps.log ?? (() => {
|
|
7834
|
+
});
|
|
7835
|
+
const allDirs = scanInstallDirs(worktreeRoot, fs2);
|
|
7836
|
+
const targets = npmInstallTargets(allDirs);
|
|
7837
|
+
const skippedInstall = allDirs.filter((d) => d.hasPackageJson && d.hasNodeModules).map((d) => d.dir);
|
|
7838
|
+
const installed = [];
|
|
7839
|
+
for (const target of targets) {
|
|
7840
|
+
const cwd = target.dir ? (0, import_node_path11.join)(worktreeRoot, target.dir) : worktreeRoot;
|
|
7841
|
+
log(`installing deps: ${target.command} in ${target.dir || "."}`);
|
|
7842
|
+
await deps.runInstall(target.command, cwd);
|
|
7843
|
+
installed.push(target);
|
|
7844
|
+
}
|
|
7845
|
+
const copied = [];
|
|
7846
|
+
const copySkipped = [];
|
|
7847
|
+
const primary = await deps.primaryCheckout();
|
|
7848
|
+
for (const rel of LOCAL_ONLY_FILES) {
|
|
7849
|
+
const dest = (0, import_node_path11.join)(worktreeRoot, rel);
|
|
7850
|
+
if (fs2.isFile(dest)) {
|
|
7851
|
+
copySkipped.push({ file: rel, reason: "already-present" });
|
|
7852
|
+
continue;
|
|
7853
|
+
}
|
|
7854
|
+
if (!primary) {
|
|
7855
|
+
copySkipped.push({ file: rel, reason: "no-primary" });
|
|
7856
|
+
continue;
|
|
7857
|
+
}
|
|
7858
|
+
if (!fs2.isFile((0, import_node_path11.join)(primary, rel))) {
|
|
7859
|
+
copySkipped.push({ file: rel, reason: "absent-in-primary" });
|
|
7860
|
+
continue;
|
|
7861
|
+
}
|
|
7862
|
+
copyFile((0, import_node_path11.join)(primary, rel), dest);
|
|
7863
|
+
copied.push(rel);
|
|
7864
|
+
log(`copied local config: ${rel}`);
|
|
7865
|
+
}
|
|
7866
|
+
return { worktree: worktreeRoot, installed, skippedInstall, copied, copySkipped };
|
|
7867
|
+
}
|
|
7868
|
+
function defaultWorktreePath(repoRoot, branch) {
|
|
7869
|
+
const safe = branch.replace(/[/\\]+/g, "-");
|
|
7870
|
+
return (0, import_node_path11.join)((0, import_node_path11.dirname)(repoRoot), "mmi-worktrees", safe);
|
|
7871
|
+
}
|
|
7872
|
+
|
|
7085
7873
|
// src/frontmatter.ts
|
|
7086
7874
|
function splitFrontmatter(content) {
|
|
7087
7875
|
const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(content);
|
|
@@ -7237,54 +8025,460 @@ function buildNorthstarContextBlock(ranked, signals, readLocal, opts = {}) {
|
|
|
7237
8025
|
cards.push(card);
|
|
7238
8026
|
used += card.length;
|
|
7239
8027
|
}
|
|
7240
|
-
if (!cards.length) return null;
|
|
7241
|
-
return [NORTHSTAR_CONTEXT_FRAMING, ...cards].join("\n");
|
|
8028
|
+
if (!cards.length) return null;
|
|
8029
|
+
return [NORTHSTAR_CONTEXT_FRAMING, ...cards].join("\n");
|
|
8030
|
+
}
|
|
8031
|
+
async function withTimeout2(promise, ms) {
|
|
8032
|
+
let timer;
|
|
8033
|
+
try {
|
|
8034
|
+
return await Promise.race([
|
|
8035
|
+
promise,
|
|
8036
|
+
new Promise((_, reject) => {
|
|
8037
|
+
timer = setTimeout(() => reject(new Error("northstar context timeout")), ms);
|
|
8038
|
+
})
|
|
8039
|
+
]);
|
|
8040
|
+
} finally {
|
|
8041
|
+
if (timer) clearTimeout(timer);
|
|
8042
|
+
}
|
|
8043
|
+
}
|
|
8044
|
+
async function runNorthstarContext(io, deps) {
|
|
8045
|
+
try {
|
|
8046
|
+
const injected = await withTimeout2(
|
|
8047
|
+
(async () => {
|
|
8048
|
+
const plans = await deps.loadPlans();
|
|
8049
|
+
if (!plans.length) return false;
|
|
8050
|
+
const signals = await deps.gatherSignals();
|
|
8051
|
+
const ranked = rankPlansByRelevance(plans, signals);
|
|
8052
|
+
const block = buildNorthstarContextBlock(ranked, signals, deps.readLocal, {
|
|
8053
|
+
anchorSlug: signals.anchorSlug
|
|
8054
|
+
});
|
|
8055
|
+
if (!block) return false;
|
|
8056
|
+
io.log(block);
|
|
8057
|
+
return true;
|
|
8058
|
+
})(),
|
|
8059
|
+
deps.timeoutMs ?? SESSION_START_NORTHSTAR_TIMEOUT_MS
|
|
8060
|
+
);
|
|
8061
|
+
return injected;
|
|
8062
|
+
} catch {
|
|
8063
|
+
return false;
|
|
8064
|
+
}
|
|
8065
|
+
}
|
|
8066
|
+
|
|
8067
|
+
// src/index.ts
|
|
8068
|
+
var import_node_path16 = require("node:path");
|
|
8069
|
+
|
|
8070
|
+
// src/merge-ci-policy.ts
|
|
8071
|
+
function resolveMergeCiPolicy(input) {
|
|
8072
|
+
if (input.registryCi === "none") {
|
|
8073
|
+
return { policy: "no-ci", reason: "registry META ci:none" };
|
|
8074
|
+
}
|
|
8075
|
+
if (input.registryRequiredChecks === null) {
|
|
8076
|
+
return { policy: "no-ci", reason: "registry META requiredChecks:null" };
|
|
8077
|
+
}
|
|
8078
|
+
if (Array.isArray(input.registryRequiredChecks) && input.registryRequiredChecks.length === 0) {
|
|
8079
|
+
return { policy: "no-ci", reason: "registry META requiredChecks:[]" };
|
|
8080
|
+
}
|
|
8081
|
+
const ciWorkflows = input.workflowPaths.filter(
|
|
8082
|
+
(p) => /^\.github\/workflows\/[^/]+\.(ya?ml)$/i.test(p.replace(/\\/g, "/"))
|
|
8083
|
+
);
|
|
8084
|
+
if (!ciWorkflows.length) {
|
|
8085
|
+
return { policy: "no-ci", reason: "no .github/workflows CI" };
|
|
8086
|
+
}
|
|
8087
|
+
return { policy: "wait-for-checks", reason: ciWorkflows.join(", ") };
|
|
8088
|
+
}
|
|
8089
|
+
function parseGhPrChecksOutput(stdout) {
|
|
8090
|
+
const text = stdout.trim();
|
|
8091
|
+
if (!text) return "no-checks-reported";
|
|
8092
|
+
if (/^no checks reported/i.test(text)) return "no-checks-reported";
|
|
8093
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim());
|
|
8094
|
+
if (!lines.length) return "no-checks-reported";
|
|
8095
|
+
let anyPending = false;
|
|
8096
|
+
for (const line of lines) {
|
|
8097
|
+
const parts = line.split(/\s+/);
|
|
8098
|
+
const state = (parts[1] ?? "").toLowerCase();
|
|
8099
|
+
if (state === "fail" || state === "failure" || state === "error") return "failure";
|
|
8100
|
+
if (state === "pass" || state === "success" || state === "skipping" || state === "skipped") continue;
|
|
8101
|
+
anyPending = true;
|
|
8102
|
+
}
|
|
8103
|
+
return anyPending ? "pending" : "success";
|
|
8104
|
+
}
|
|
8105
|
+
var PR_CHECKS_POLL_MS = 3e4;
|
|
8106
|
+
var PR_CHECKS_TIMEOUT_MS = 10 * 6e4;
|
|
8107
|
+
async function waitForPrChecks(deps) {
|
|
8108
|
+
const { policy, reason } = await deps.resolvePolicy();
|
|
8109
|
+
if (policy === "no-ci") {
|
|
8110
|
+
return { policy, status: "skipped", reason };
|
|
8111
|
+
}
|
|
8112
|
+
const now = deps.now ?? (() => Date.now());
|
|
8113
|
+
const deadline = now() + PR_CHECKS_TIMEOUT_MS;
|
|
8114
|
+
let lastDetail = "pending";
|
|
8115
|
+
while (now() < deadline) {
|
|
8116
|
+
const state = await deps.pollChecks();
|
|
8117
|
+
if (state === "success") return { policy, status: "success", detail: lastDetail };
|
|
8118
|
+
if (state === "failure") return { policy, status: "failure", detail: lastDetail };
|
|
8119
|
+
if (state === "no-checks-reported") {
|
|
8120
|
+
lastDetail = "no-checks-reported (waiting for workflow to queue)";
|
|
8121
|
+
await deps.sleep(PR_CHECKS_POLL_MS);
|
|
8122
|
+
continue;
|
|
8123
|
+
}
|
|
8124
|
+
lastDetail = state;
|
|
8125
|
+
await deps.sleep(PR_CHECKS_POLL_MS);
|
|
8126
|
+
}
|
|
8127
|
+
return { policy, status: "timeout", detail: lastDetail };
|
|
8128
|
+
}
|
|
8129
|
+
|
|
8130
|
+
// src/bootstrap-ruleset.ts
|
|
8131
|
+
var PRODUCT_RULESET_NAME = "mmi-product-required-checks";
|
|
8132
|
+
var PRODUCT_GATE_CONTEXT = "gate";
|
|
8133
|
+
function stripRulesetComment(raw) {
|
|
8134
|
+
const parsed = JSON.parse(raw);
|
|
8135
|
+
delete parsed._comment;
|
|
8136
|
+
return parsed;
|
|
8137
|
+
}
|
|
8138
|
+
function rulesetHasGateContext(ruleset, context = PRODUCT_GATE_CONTEXT) {
|
|
8139
|
+
for (const rule of ruleset.rules ?? []) {
|
|
8140
|
+
if (rule.type !== "required_status_checks") continue;
|
|
8141
|
+
for (const check of rule.parameters?.required_status_checks ?? []) {
|
|
8142
|
+
if (check.context === context) return true;
|
|
8143
|
+
}
|
|
8144
|
+
}
|
|
8145
|
+
return false;
|
|
8146
|
+
}
|
|
8147
|
+
function findProductRuleset(rulesets) {
|
|
8148
|
+
return rulesets.find((r) => r.name === PRODUCT_RULESET_NAME);
|
|
8149
|
+
}
|
|
8150
|
+
async function activateProductRuleset(repo, rulesetBody, client) {
|
|
8151
|
+
const list = await client.rest("GET", `repos/${repo}/rulesets`, { timeoutMs: 2e4 });
|
|
8152
|
+
const existing = findProductRuleset(list ?? []);
|
|
8153
|
+
if (existing?.id != null) {
|
|
8154
|
+
const detail = await client.rest("GET", `repos/${repo}/rulesets/${existing.id}`, { timeoutMs: 2e4 });
|
|
8155
|
+
if (detail.enforcement === "active" && rulesetHasGateContext(detail)) {
|
|
8156
|
+
return { action: "skipped", detail: "active ruleset already requires gate" };
|
|
8157
|
+
}
|
|
8158
|
+
await client.rest("PUT", `repos/${repo}/rulesets/${existing.id}`, { body: rulesetBody, timeoutMs: 2e4 });
|
|
8159
|
+
return { action: "updated", detail: `ruleset ${existing.id}` };
|
|
8160
|
+
}
|
|
8161
|
+
await client.rest("POST", `repos/${repo}/rulesets`, { body: rulesetBody, timeoutMs: 2e4 });
|
|
8162
|
+
return { action: "created" };
|
|
8163
|
+
}
|
|
8164
|
+
|
|
8165
|
+
// src/ci-audit.ts
|
|
8166
|
+
var HUB_REPO = "mutmutco/MMI-Hub";
|
|
8167
|
+
var PRODUCT_GATE_CONTEXT2 = "gate";
|
|
8168
|
+
var HUB_GATE_CONTEXTS = ["cli", "infra", "docs"];
|
|
8169
|
+
var PRODUCT_GATE_PATH = ".github/workflows/gate.yml";
|
|
8170
|
+
var PRODUCT_RULESET_REF = ".github/rulesets/mmi-product-required-checks.json";
|
|
8171
|
+
function slugFromRepo(repo) {
|
|
8172
|
+
return (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
|
|
8173
|
+
}
|
|
8174
|
+
function classifyRepo(repo, meta) {
|
|
8175
|
+
if (repo.toLowerCase() === HUB_REPO.toLowerCase()) return "hub";
|
|
8176
|
+
if (meta?.class === "content") return "content";
|
|
8177
|
+
if (meta?.class === "deployable" || meta?.deployModel && meta.deployModel !== "content" && meta.deployModel !== "none") return "deployable";
|
|
8178
|
+
if (meta) return "deployable";
|
|
8179
|
+
return "unknown";
|
|
8180
|
+
}
|
|
8181
|
+
function rulesetStatusChecks(rulesets) {
|
|
8182
|
+
return new Set(rulesets.flatMap((ruleset) => (ruleset.rules || []).filter((rule) => rule.type === "required_status_checks").flatMap((rule) => rule.parameters?.required_status_checks || []).map((check) => check.context).filter((context) => Boolean(context))));
|
|
8183
|
+
}
|
|
8184
|
+
async function restJson(deps, path2, fallback) {
|
|
8185
|
+
try {
|
|
8186
|
+
return await deps.client.rest("GET", path2) ?? fallback;
|
|
8187
|
+
} catch {
|
|
8188
|
+
return fallback;
|
|
8189
|
+
}
|
|
8190
|
+
}
|
|
8191
|
+
async function rulesetDetails(deps, repo, list) {
|
|
8192
|
+
const details = [];
|
|
8193
|
+
for (const ruleset of list) {
|
|
8194
|
+
if (ruleset.id == null || ruleset.rules != null) {
|
|
8195
|
+
details.push(ruleset);
|
|
8196
|
+
continue;
|
|
8197
|
+
}
|
|
8198
|
+
details.push(await restJson(deps, `repos/${repo}/rulesets/${ruleset.id}`, ruleset));
|
|
8199
|
+
}
|
|
8200
|
+
return details;
|
|
8201
|
+
}
|
|
8202
|
+
async function contentExists(deps, repo, branch, path2) {
|
|
8203
|
+
try {
|
|
8204
|
+
const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
|
|
8205
|
+
await deps.client.rest("GET", `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`);
|
|
8206
|
+
return true;
|
|
8207
|
+
} catch {
|
|
8208
|
+
return false;
|
|
8209
|
+
}
|
|
8210
|
+
}
|
|
8211
|
+
function collectRegistryRepos(projects) {
|
|
8212
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8213
|
+
for (const project2 of projects) {
|
|
8214
|
+
for (const raw of project2.repos ?? []) {
|
|
8215
|
+
const repo = raw.includes("/") ? raw : `mutmutco/${raw}`;
|
|
8216
|
+
seen.add(repo);
|
|
8217
|
+
}
|
|
8218
|
+
}
|
|
8219
|
+
if (!seen.has(HUB_REPO)) seen.add(HUB_REPO);
|
|
8220
|
+
return [...seen].sort((a, b) => a.localeCompare(b));
|
|
8221
|
+
}
|
|
8222
|
+
async function resolveRepoMergeCiPolicy(repo, deps) {
|
|
8223
|
+
const meta = await deps.getProjectMeta(slugFromRepo(repo));
|
|
8224
|
+
const repoClass = classifyRepo(repo, meta);
|
|
8225
|
+
if (repoClass === "content") {
|
|
8226
|
+
return resolveMergeCiPolicy({
|
|
8227
|
+
workflowPaths: [],
|
|
8228
|
+
registryCi: meta?.ci ?? "none",
|
|
8229
|
+
registryRequiredChecks: meta?.requiredChecks ?? []
|
|
8230
|
+
});
|
|
8231
|
+
}
|
|
8232
|
+
const baseBranch = "development";
|
|
8233
|
+
const hasGate = repoClass === "hub" ? true : await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH);
|
|
8234
|
+
return resolveMergeCiPolicy({
|
|
8235
|
+
workflowPaths: hasGate ? repoClass === "hub" ? [".github/workflows/gate.yml"] : [PRODUCT_GATE_PATH] : [],
|
|
8236
|
+
registryCi: meta?.ci,
|
|
8237
|
+
registryRequiredChecks: meta?.requiredChecks
|
|
8238
|
+
});
|
|
8239
|
+
}
|
|
8240
|
+
async function auditRepoCi(repo, deps) {
|
|
8241
|
+
const meta = await deps.getProjectMeta(slugFromRepo(repo));
|
|
8242
|
+
const repoClass = classifyRepo(repo, meta);
|
|
8243
|
+
const checks = [];
|
|
8244
|
+
const info = await restJson(deps, `repos/${repo}`, {});
|
|
8245
|
+
const baseBranch = repoClass === "content" ? "main" : "development";
|
|
8246
|
+
checks.push({
|
|
8247
|
+
ok: info.allow_auto_merge === true,
|
|
8248
|
+
label: "allow_auto_merge enabled",
|
|
8249
|
+
detail: info.allow_auto_merge === true ? void 0 : "false or unavailable",
|
|
8250
|
+
remediation: `gh api -X PATCH repos/${repo} -f allow_auto_merge=true -f allow_squash_merge=true -f delete_branch_on_merge=true`
|
|
8251
|
+
});
|
|
8252
|
+
checks.push({
|
|
8253
|
+
ok: info.allow_squash_merge === true,
|
|
8254
|
+
label: "allow_squash_merge enabled",
|
|
8255
|
+
detail: info.allow_squash_merge === true ? void 0 : "false or unavailable"
|
|
8256
|
+
});
|
|
8257
|
+
checks.push({
|
|
8258
|
+
ok: info.delete_branch_on_merge === true,
|
|
8259
|
+
label: "delete_branch_on_merge enabled",
|
|
8260
|
+
detail: info.delete_branch_on_merge === true ? void 0 : "false or unavailable"
|
|
8261
|
+
});
|
|
8262
|
+
const hasGateWorkflow = repoClass === "hub" ? true : repoClass === "content" ? true : await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH);
|
|
8263
|
+
if (repoClass === "deployable") {
|
|
8264
|
+
checks.push({
|
|
8265
|
+
ok: hasGateWorkflow,
|
|
8266
|
+
label: "gate workflow committed",
|
|
8267
|
+
detail: hasGateWorkflow ? void 0 : `missing ${PRODUCT_GATE_PATH}`,
|
|
8268
|
+
remediation: `mmi-cli bootstrap apply ${repo} --class deployable --execute (seeds gate.yml)`
|
|
8269
|
+
});
|
|
8270
|
+
checks.push({
|
|
8271
|
+
ok: await contentExists(deps, repo, baseBranch, PRODUCT_RULESET_REF),
|
|
8272
|
+
label: "product ruleset reference committed",
|
|
8273
|
+
detail: `expected ${PRODUCT_RULESET_REF}`,
|
|
8274
|
+
remediation: `mmi-cli bootstrap apply ${repo} --class deployable --execute`
|
|
8275
|
+
});
|
|
8276
|
+
}
|
|
8277
|
+
const rulesetList = await restJson(deps, `repos/${repo}/rulesets?includes_parents=true`, []);
|
|
8278
|
+
const rulesets = await rulesetDetails(deps, repo, rulesetList);
|
|
8279
|
+
const activeBranchRulesets = rulesets.filter((r) => r.target === "branch" && r.enforcement === "active");
|
|
8280
|
+
const statusChecks = rulesetStatusChecks(activeBranchRulesets);
|
|
8281
|
+
if (repoClass === "hub") {
|
|
8282
|
+
const missing = HUB_GATE_CONTEXTS.filter((c) => !statusChecks.has(c));
|
|
8283
|
+
checks.push({
|
|
8284
|
+
ok: missing.length === 0,
|
|
8285
|
+
label: "Hub required status checks active",
|
|
8286
|
+
detail: missing.length ? `missing: ${missing.join(", ")}` : void 0
|
|
8287
|
+
});
|
|
8288
|
+
} else if (repoClass === "deployable") {
|
|
8289
|
+
const missing = [PRODUCT_GATE_CONTEXT2].filter((c) => !statusChecks.has(c));
|
|
8290
|
+
checks.push({
|
|
8291
|
+
ok: missing.length === 0,
|
|
8292
|
+
label: "product gate status check active",
|
|
8293
|
+
detail: missing.length ? `missing context: ${PRODUCT_GATE_CONTEXT2} \u2014 activate ${PRODUCT_RULESET_REF} as a repo ruleset` : void 0,
|
|
8294
|
+
remediation: missing.length ? `Import ${PRODUCT_RULESET_REF} as an active repository ruleset (GitHub \u2192 Settings \u2192 Rules \u2192 Rulesets) \u2014 target: bootstrap --apply automation (#1440)` : void 0
|
|
8295
|
+
});
|
|
8296
|
+
}
|
|
8297
|
+
const registryCi = meta?.ci;
|
|
8298
|
+
const registryRequiredChecks = meta?.requiredChecks;
|
|
8299
|
+
const workflowPaths = hasGateWorkflow && repoClass === "deployable" ? [PRODUCT_GATE_PATH] : [];
|
|
8300
|
+
const { policy, reason } = resolveMergeCiPolicy({
|
|
8301
|
+
workflowPaths,
|
|
8302
|
+
registryCi,
|
|
8303
|
+
registryRequiredChecks
|
|
8304
|
+
});
|
|
8305
|
+
if (repoClass === "content") {
|
|
8306
|
+
const explicitNoCi = registryCi === "none" || registryRequiredChecks === null || Array.isArray(registryRequiredChecks) && registryRequiredChecks.length === 0;
|
|
8307
|
+
checks.push({
|
|
8308
|
+
ok: explicitNoCi,
|
|
8309
|
+
label: "registry META declares intentional no-ci",
|
|
8310
|
+
detail: explicitNoCi ? void 0 : "set ci:none and requiredChecks:[] in registry META",
|
|
8311
|
+
remediation: `mmi-cli project set ${repo} --var ci=none --var requiredChecks=[]`
|
|
8312
|
+
});
|
|
8313
|
+
} else if (repoClass === "deployable") {
|
|
8314
|
+
checks.push({
|
|
8315
|
+
ok: policy === "wait-for-checks",
|
|
8316
|
+
label: "merge CI policy is wait-for-checks",
|
|
8317
|
+
detail: `${policy} (${reason})`
|
|
8318
|
+
});
|
|
8319
|
+
}
|
|
8320
|
+
const ok = checks.every((c) => c.ok);
|
|
8321
|
+
return { repo, class: repoClass, mergePolicy: policy, ok, checks };
|
|
8322
|
+
}
|
|
8323
|
+
async function auditOrgCi(deps, repoFilter) {
|
|
8324
|
+
const projects = await deps.listProjects();
|
|
8325
|
+
if (!projects) {
|
|
8326
|
+
const single = repoFilter ?? HUB_REPO;
|
|
8327
|
+
const report = await auditRepoCi(single, deps);
|
|
8328
|
+
return { ok: report.ok, repos: [report] };
|
|
8329
|
+
}
|
|
8330
|
+
const targets = repoFilter ? [repoFilter] : collectRegistryRepos(projects);
|
|
8331
|
+
const repos = [];
|
|
8332
|
+
for (const repo of targets) {
|
|
8333
|
+
repos.push(await auditRepoCi(repo, deps));
|
|
8334
|
+
}
|
|
8335
|
+
return { ok: repos.every((r) => r.ok), repos };
|
|
8336
|
+
}
|
|
8337
|
+
function renderCiAuditMarkdown(report) {
|
|
8338
|
+
const lines = [
|
|
8339
|
+
`# CI merge-readiness audit`,
|
|
8340
|
+
"",
|
|
8341
|
+
`Fleet: ${report.ok ? "OK" : "GAPS"} (${report.repos.filter((r) => r.ok).length}/${report.repos.length} repos ready)`,
|
|
8342
|
+
"",
|
|
8343
|
+
"| Repo | Class | Policy | OK | Top gap |",
|
|
8344
|
+
"|------|-------|--------|----|---------|"
|
|
8345
|
+
];
|
|
8346
|
+
for (const r of report.repos) {
|
|
8347
|
+
const gap = r.checks.find((c) => !c.ok);
|
|
8348
|
+
lines.push(`| ${r.repo} | ${r.class} | ${r.mergePolicy} | ${r.ok ? "yes" : "no"} | ${gap?.label ?? "\u2014"} |`);
|
|
8349
|
+
}
|
|
8350
|
+
return lines.join("\n");
|
|
8351
|
+
}
|
|
8352
|
+
function renderCiAuditText(report) {
|
|
8353
|
+
const lines = [`mmi-cli ci audit: ${report.ok ? "OK" : "GAPS"} (${report.repos.length} repos)`];
|
|
8354
|
+
for (const r of report.repos) {
|
|
8355
|
+
lines.push(`
|
|
8356
|
+
${r.repo} (${r.class}, policy=${r.mergePolicy}) ${r.ok ? "OK" : "GAP"}`);
|
|
8357
|
+
for (const c of r.checks) {
|
|
8358
|
+
lines.push(` ${c.ok ? "OK" : "FAIL"} ${c.label}${c.detail ? ` \u2014 ${c.detail}` : ""}`);
|
|
8359
|
+
}
|
|
8360
|
+
}
|
|
8361
|
+
return lines.join("\n");
|
|
7242
8362
|
}
|
|
7243
|
-
async function
|
|
7244
|
-
|
|
8363
|
+
async function applyCiReconcileMergeSettings(repo, deps) {
|
|
8364
|
+
const report = await auditRepoCi(repo, deps);
|
|
8365
|
+
const applied = [];
|
|
8366
|
+
const skipped = [];
|
|
8367
|
+
const errors = [];
|
|
8368
|
+
const mergeChecks = report.checks.filter((c) => c.label.startsWith("allow_") || c.label.startsWith("delete_branch"));
|
|
8369
|
+
const needsPatch = mergeChecks.some((c) => !c.ok);
|
|
8370
|
+
if (!needsPatch) {
|
|
8371
|
+
skipped.push("merge settings already canonical");
|
|
8372
|
+
return { repo, applied, skipped, errors };
|
|
8373
|
+
}
|
|
7245
8374
|
try {
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
8375
|
+
await deps.client.rest("PATCH", `repos/${repo}`, {
|
|
8376
|
+
body: {
|
|
8377
|
+
allow_auto_merge: true,
|
|
8378
|
+
allow_squash_merge: true,
|
|
8379
|
+
delete_branch_on_merge: true
|
|
8380
|
+
}
|
|
8381
|
+
});
|
|
8382
|
+
applied.push("allow_auto_merge, allow_squash_merge, delete_branch_on_merge");
|
|
8383
|
+
} catch (e) {
|
|
8384
|
+
errors.push(e.message);
|
|
7254
8385
|
}
|
|
8386
|
+
return { repo, applied, skipped, errors };
|
|
7255
8387
|
}
|
|
7256
|
-
async function
|
|
8388
|
+
async function fetchRulesetSeedBody(deps, repo) {
|
|
7257
8389
|
try {
|
|
7258
|
-
const
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
const signals = await deps.gatherSignals();
|
|
7263
|
-
const ranked = rankPlansByRelevance(plans, signals);
|
|
7264
|
-
const block = buildNorthstarContextBlock(ranked, signals, deps.readLocal, {
|
|
7265
|
-
anchorSlug: signals.anchorSlug
|
|
7266
|
-
});
|
|
7267
|
-
if (!block) return false;
|
|
7268
|
-
io.log(block);
|
|
7269
|
-
return true;
|
|
7270
|
-
})(),
|
|
7271
|
-
deps.timeoutMs ?? SESSION_START_NORTHSTAR_TIMEOUT_MS
|
|
8390
|
+
const encodedPath = PRODUCT_RULESET_REF.split("/").map(encodeURIComponent).join("/");
|
|
8391
|
+
const file = await deps.client.rest(
|
|
8392
|
+
"GET",
|
|
8393
|
+
`repos/${repo}/contents/${encodedPath}?ref=development`
|
|
7272
8394
|
);
|
|
7273
|
-
return
|
|
8395
|
+
if (file.encoding !== "base64" || typeof file.content !== "string") return null;
|
|
8396
|
+
return Buffer.from(file.content, "base64").toString("utf8");
|
|
7274
8397
|
} catch {
|
|
7275
|
-
return
|
|
8398
|
+
return null;
|
|
8399
|
+
}
|
|
8400
|
+
}
|
|
8401
|
+
async function applyCiReconcileRepo(repo, deps) {
|
|
8402
|
+
const merge = await applyCiReconcileMergeSettings(repo, deps);
|
|
8403
|
+
const report = await auditRepoCi(repo, deps);
|
|
8404
|
+
if (report.class !== "deployable") return merge;
|
|
8405
|
+
const gateCheck = report.checks.find((c) => c.label === "product gate status check active");
|
|
8406
|
+
if (gateCheck?.ok) {
|
|
8407
|
+
merge.skipped.push("product ruleset already active");
|
|
8408
|
+
return merge;
|
|
8409
|
+
}
|
|
8410
|
+
const raw = await fetchRulesetSeedBody(deps, repo);
|
|
8411
|
+
if (!raw) {
|
|
8412
|
+
merge.errors.push(`missing ${PRODUCT_RULESET_REF} on development \u2014 run bootstrap apply first`);
|
|
8413
|
+
return merge;
|
|
8414
|
+
}
|
|
8415
|
+
try {
|
|
8416
|
+
const body = stripRulesetComment(raw);
|
|
8417
|
+
const activation = await activateProductRuleset(repo, body, deps.client);
|
|
8418
|
+
if (activation.action === "skipped") merge.skipped.push(activation.detail ?? "product ruleset");
|
|
8419
|
+
else merge.applied.push(`product ruleset ${activation.action}${activation.detail ? `: ${activation.detail}` : ""}`);
|
|
8420
|
+
} catch (e) {
|
|
8421
|
+
merge.errors.push(e.message);
|
|
8422
|
+
}
|
|
8423
|
+
return merge;
|
|
8424
|
+
}
|
|
8425
|
+
|
|
8426
|
+
// src/pr-land.ts
|
|
8427
|
+
var PR_LAND_POLL_MS = 3e4;
|
|
8428
|
+
var PR_LAND_ENQUEUE_TIMEOUT_MS = 10 * 6e4;
|
|
8429
|
+
async function runPrLand(prNumber, options, deps) {
|
|
8430
|
+
const repo = await deps.resolveRepo(prNumber, options.repo);
|
|
8431
|
+
const base2 = { status: "failed", repo, pr: prNumber };
|
|
8432
|
+
if (options.requireTrain !== false) {
|
|
8433
|
+
const verdict = await deps.fetchTrainAuthority(repo);
|
|
8434
|
+
if (!verdict.ok) {
|
|
8435
|
+
return { ...base2, error: `train authority unavailable: ${verdict.error}` };
|
|
8436
|
+
}
|
|
8437
|
+
base2.train = { role: verdict.authority.role, train: verdict.authority.train };
|
|
8438
|
+
if (!verdict.authority.train) {
|
|
8439
|
+
return {
|
|
8440
|
+
...base2,
|
|
8441
|
+
error: `@${verdict.authority.login ?? "caller"} is ${verdict.authority.role} \u2014 train not authorized on ${repo}; cannot land PR`
|
|
8442
|
+
};
|
|
8443
|
+
}
|
|
8444
|
+
}
|
|
8445
|
+
const ciPolicy = await deps.resolveCiPolicy(repo);
|
|
8446
|
+
base2.ciPolicy = ciPolicy;
|
|
8447
|
+
const checksWait = await deps.waitForChecks(prNumber, repo);
|
|
8448
|
+
base2.checksWait = checksWait;
|
|
8449
|
+
if (checksWait.status === "failure" || checksWait.status === "timeout") {
|
|
8450
|
+
return {
|
|
8451
|
+
...base2,
|
|
8452
|
+
error: `checks-wait ${checksWait.status}${checksWait.detail ? `: ${checksWait.detail}` : ""}`
|
|
8453
|
+
};
|
|
8454
|
+
}
|
|
8455
|
+
const merge = await deps.mergeAuto(prNumber, repo);
|
|
8456
|
+
base2.mergeStatus = merge.mergeStatus;
|
|
8457
|
+
if (merge.mergeStatus === "failed") {
|
|
8458
|
+
return { ...base2, error: merge.error ?? "merge failed" };
|
|
8459
|
+
}
|
|
8460
|
+
if (merge.mergeStatus === "merged") {
|
|
8461
|
+
return { ...base2, status: "merged" };
|
|
8462
|
+
}
|
|
8463
|
+
const now = deps.now ?? (() => Date.now());
|
|
8464
|
+
const merged = await deps.pollMerged(prNumber, repo, now() + PR_LAND_ENQUEUE_TIMEOUT_MS);
|
|
8465
|
+
if (merged) {
|
|
8466
|
+
return { ...base2, status: "auto-merge-enqueued-then-merged", mergeStatus: "merged" };
|
|
7276
8467
|
}
|
|
8468
|
+
return {
|
|
8469
|
+
...base2,
|
|
8470
|
+
error: "auto-merge enqueued but PR did not reach MERGED within timeout \u2014 poll manually with gh pr view"
|
|
8471
|
+
};
|
|
7277
8472
|
}
|
|
7278
8473
|
|
|
7279
8474
|
// src/index.ts
|
|
7280
|
-
var import_node_path14 = require("node:path");
|
|
7281
8475
|
var import_node_os5 = require("node:os");
|
|
7282
8476
|
|
|
7283
8477
|
// src/gh-create.ts
|
|
7284
8478
|
var import_promises4 = require("node:fs/promises");
|
|
7285
8479
|
var import_node_os3 = require("node:os");
|
|
7286
|
-
var
|
|
7287
|
-
var
|
|
8480
|
+
var import_node_path12 = require("node:path");
|
|
8481
|
+
var import_node_crypto6 = require("node:crypto");
|
|
7288
8482
|
var ISSUE_TYPES = ["bug", "feature", "task"];
|
|
7289
8483
|
var GH_MUTATION_TIMEOUT_MS = 12e4;
|
|
7290
8484
|
function timeoutKillNote(err, timeoutMs) {
|
|
@@ -7324,7 +8518,7 @@ async function bodyArgsViaFile(args, deps = {}) {
|
|
|
7324
8518
|
} };
|
|
7325
8519
|
const write = deps.write ?? import_promises4.writeFile;
|
|
7326
8520
|
const remove = deps.remove ?? import_promises4.unlink;
|
|
7327
|
-
const file = (0,
|
|
8521
|
+
const file = (0, import_node_path12.join)(deps.dir ?? (0, import_node_os3.tmpdir)(), `mmi-gh-body-${process.pid}-${(0, import_node_crypto6.randomBytes)(4).toString("hex")}.md`);
|
|
7328
8522
|
await write(file, args[i + 1], "utf8");
|
|
7329
8523
|
return {
|
|
7330
8524
|
args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
|
|
@@ -7614,7 +8808,7 @@ function parentLinkFields(result, error) {
|
|
|
7614
8808
|
}
|
|
7615
8809
|
|
|
7616
8810
|
// src/report.ts
|
|
7617
|
-
var
|
|
8811
|
+
var HUB_REPO2 = "mutmutco/MMI-Hub";
|
|
7618
8812
|
var REPORT_LABEL = "report";
|
|
7619
8813
|
function findDuplicateReport(source, openReports, threshold = 0.6) {
|
|
7620
8814
|
const normalizedTitle = normalizeTitle(source.title);
|
|
@@ -7649,7 +8843,7 @@ ${buildReportBody(body, sourceRepo)}`;
|
|
|
7649
8843
|
|
|
7650
8844
|
// src/skill-lesson.ts
|
|
7651
8845
|
var SKILL_LESSON_LABEL = "skill-lesson";
|
|
7652
|
-
var SKILL_NAMES = ["bootstrap", "browser-automation", "build", "grind", "hotfix", "mmi", "rcand", "release", "secrets", "stage"];
|
|
8846
|
+
var SKILL_NAMES = ["bootstrap", "browser-automation", "build", "grind", "handoff", "hotfix", "mmi", "rcand", "release", "secrets", "stage"];
|
|
7653
8847
|
function assertSkillName(name) {
|
|
7654
8848
|
const match = SKILL_NAMES.find((skill) => skill === name);
|
|
7655
8849
|
if (!match) throw new Error(`unknown skill "${name}" \u2014 expected one of: ${SKILL_NAMES.join(", ")}`);
|
|
@@ -8149,12 +9343,12 @@ function buildGcPlan(inputs) {
|
|
|
8149
9343
|
skipped.push({ branch, reason: "current-branch" });
|
|
8150
9344
|
continue;
|
|
8151
9345
|
}
|
|
8152
|
-
const
|
|
8153
|
-
if (
|
|
8154
|
-
skipped.push({ branch, reason: "dirty-worktree", detail:
|
|
9346
|
+
const worktree2 = worktrees.get(branch);
|
|
9347
|
+
if (worktree2?.dirty) {
|
|
9348
|
+
skipped.push({ branch, reason: "dirty-worktree", detail: worktree2.path });
|
|
8155
9349
|
continue;
|
|
8156
9350
|
}
|
|
8157
|
-
branches.push({ branch, prState: state.state, prNumbers: state.numbers, worktreePath:
|
|
9351
|
+
branches.push({ branch, prState: state.state, prNumbers: state.numbers, worktreePath: worktree2?.path });
|
|
8158
9352
|
}
|
|
8159
9353
|
const trackingRefs = [...new Set(inputs.staleTrackingRefs ?? [])].map((ref) => {
|
|
8160
9354
|
const branch = branchForTrackingRef(ref, remote);
|
|
@@ -8298,6 +9492,28 @@ function selectSafeWorktreeCwd(worktrees, targetPath, options) {
|
|
|
8298
9492
|
const exists = options?.pathExists ?? (() => true);
|
|
8299
9493
|
return worktrees.find((w) => !samePath(w.path, targetPath) && exists(w.path))?.path;
|
|
8300
9494
|
}
|
|
9495
|
+
function isPathUnderDirectory(childPath, parentPath) {
|
|
9496
|
+
const child = normPath(childPath);
|
|
9497
|
+
const parent = normPath(parentPath);
|
|
9498
|
+
if (!child || !parent) return false;
|
|
9499
|
+
if (child === parent) return true;
|
|
9500
|
+
return child.startsWith(`${parent}/`);
|
|
9501
|
+
}
|
|
9502
|
+
function planReleaseCwdBeforeWorktreeRemoval(targetPath, safeCwd, currentCwd) {
|
|
9503
|
+
if (!safeCwd || !isPathUnderDirectory(currentCwd, targetPath)) return void 0;
|
|
9504
|
+
if (samePath(currentCwd, safeCwd)) return void 0;
|
|
9505
|
+
return safeCwd;
|
|
9506
|
+
}
|
|
9507
|
+
function releaseCwdIfUnderWorktree(targetPath, safeCwd, options) {
|
|
9508
|
+
const getCwd = options?.getCwd ?? (() => process.cwd());
|
|
9509
|
+
const chdir = options?.chdir ?? ((path2) => {
|
|
9510
|
+
process.chdir(path2);
|
|
9511
|
+
});
|
|
9512
|
+
const releaseTo = planReleaseCwdBeforeWorktreeRemoval(targetPath, safeCwd, getCwd());
|
|
9513
|
+
if (!releaseTo) return false;
|
|
9514
|
+
chdir(releaseTo);
|
|
9515
|
+
return true;
|
|
9516
|
+
}
|
|
8301
9517
|
function branchMissingFromList(branch, stdout) {
|
|
8302
9518
|
const names = stdout.split(/\r?\n/).map((line) => line.replace(/^\*\s*/, "").trim()).filter(Boolean);
|
|
8303
9519
|
return !names.includes(branch);
|
|
@@ -8350,6 +9566,10 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
8350
9566
|
stageTeardown = { status: "failed", error: errorMessage(e) };
|
|
8351
9567
|
}
|
|
8352
9568
|
}
|
|
9569
|
+
releaseCwdIfUnderWorktree(wtPath, safeCwd, {
|
|
9570
|
+
getCwd: options.getCwd,
|
|
9571
|
+
chdir: options.chdir
|
|
9572
|
+
});
|
|
8353
9573
|
const outcome = await removeWorktreeWithRecovery(wtPath, {
|
|
8354
9574
|
git,
|
|
8355
9575
|
sleep: options.sleep ?? defaultSleep,
|
|
@@ -8667,9 +9887,9 @@ function stalePosixFields(config, shell2) {
|
|
|
8667
9887
|
}
|
|
8668
9888
|
function sanitizeLocalStage(local, stale) {
|
|
8669
9889
|
if (!stale.length) return local;
|
|
8670
|
-
const
|
|
8671
|
-
for (const field of stale) delete
|
|
8672
|
-
return
|
|
9890
|
+
const clean4 = { ...local };
|
|
9891
|
+
for (const field of stale) delete clean4[field];
|
|
9892
|
+
return clean4;
|
|
8673
9893
|
}
|
|
8674
9894
|
function staleNote(staleFields, outcome) {
|
|
8675
9895
|
const list = staleFields.join(", ");
|
|
@@ -8715,9 +9935,9 @@ function decideStage(inputs) {
|
|
|
8715
9935
|
|
|
8716
9936
|
// src/cursor-plugin-seed.ts
|
|
8717
9937
|
var import_node_child_process7 = require("node:child_process");
|
|
8718
|
-
var
|
|
9938
|
+
var import_node_fs14 = require("node:fs");
|
|
8719
9939
|
var import_node_os4 = require("node:os");
|
|
8720
|
-
var
|
|
9940
|
+
var import_node_path13 = require("node:path");
|
|
8721
9941
|
var import_node_util6 = require("node:util");
|
|
8722
9942
|
function isSemverVersion(v) {
|
|
8723
9943
|
return typeof v === "string" && /^v?\d+\.\d+\.\d+/.test(v.trim());
|
|
@@ -8734,17 +9954,17 @@ function ghReleaseTarballApiArgs(tag) {
|
|
|
8734
9954
|
}
|
|
8735
9955
|
function cursorUserGlobalStatePath() {
|
|
8736
9956
|
if (process.platform === "win32") {
|
|
8737
|
-
const base2 = process.env.APPDATA || (0,
|
|
8738
|
-
return (0,
|
|
9957
|
+
const base2 = process.env.APPDATA || (0, import_node_path13.join)((0, import_node_os4.homedir)(), "AppData", "Roaming");
|
|
9958
|
+
return (0, import_node_path13.join)(base2, "Cursor", "User", "globalStorage", "state.vscdb");
|
|
8739
9959
|
}
|
|
8740
9960
|
if (process.platform === "darwin") {
|
|
8741
|
-
return (0,
|
|
9961
|
+
return (0, import_node_path13.join)((0, import_node_os4.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
8742
9962
|
}
|
|
8743
|
-
return (0,
|
|
9963
|
+
return (0, import_node_path13.join)((0, import_node_os4.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
8744
9964
|
}
|
|
8745
9965
|
async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
|
|
8746
9966
|
const dbPath = cursorUserGlobalStatePath();
|
|
8747
|
-
if (!(0,
|
|
9967
|
+
if (!(0, import_node_fs14.existsSync)(dbPath)) return void 0;
|
|
8748
9968
|
try {
|
|
8749
9969
|
const { stdout } = await execFileP5("sqlite3", [dbPath, `SELECT value FROM ItemTable WHERE key = '${CURSOR_THIRD_PARTY_STATE_KEY}';`], {
|
|
8750
9970
|
timeout: 5e3
|
|
@@ -8758,57 +9978,57 @@ async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
|
|
|
8758
9978
|
}
|
|
8759
9979
|
}
|
|
8760
9980
|
function syncDirContents(src, dest) {
|
|
8761
|
-
(0,
|
|
8762
|
-
for (const name of (0,
|
|
8763
|
-
(0,
|
|
9981
|
+
(0, import_node_fs14.mkdirSync)(dest, { recursive: true });
|
|
9982
|
+
for (const name of (0, import_node_fs14.readdirSync)(dest)) {
|
|
9983
|
+
(0, import_node_fs14.rmSync)((0, import_node_path13.join)(dest, name), { recursive: true, force: true });
|
|
8764
9984
|
}
|
|
8765
|
-
(0,
|
|
9985
|
+
(0, import_node_fs14.cpSync)(src, dest, { recursive: true });
|
|
8766
9986
|
}
|
|
8767
9987
|
function releaseTag(releasedVersion) {
|
|
8768
9988
|
return releasedVersion.startsWith("v") ? releasedVersion : `v${releasedVersion}`;
|
|
8769
9989
|
}
|
|
8770
9990
|
async function extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5) {
|
|
8771
|
-
const tarFile = (0,
|
|
9991
|
+
const tarFile = (0, import_node_path13.join)(tmpRoot, "archive.tar");
|
|
8772
9992
|
try {
|
|
8773
9993
|
await execFileP5("git", gitFetchReleaseTagArgs(hubCheckout, tag), { timeout: 6e4 });
|
|
8774
9994
|
await execFileP5("git", ["-C", hubCheckout, "archive", "--format=tar", `--output=${tarFile}`, tag, "plugins/mmi"], {
|
|
8775
9995
|
timeout: 6e4
|
|
8776
9996
|
});
|
|
8777
9997
|
await execFileP5("tar", ["-xf", tarFile, "-C", tmpRoot], { timeout: 6e4 });
|
|
8778
|
-
const pluginMmi = (0,
|
|
8779
|
-
return (0,
|
|
9998
|
+
const pluginMmi = (0, import_node_path13.join)(tmpRoot, "plugins", "mmi");
|
|
9999
|
+
return (0, import_node_fs14.existsSync)((0, import_node_path13.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
|
|
8780
10000
|
} catch {
|
|
8781
10001
|
return void 0;
|
|
8782
10002
|
}
|
|
8783
10003
|
}
|
|
8784
10004
|
async function downloadPluginMmiViaGh(tag, tmpRoot) {
|
|
8785
|
-
const tarPath = (0,
|
|
10005
|
+
const tarPath = (0, import_node_path13.join)(tmpRoot, "repo.tgz");
|
|
8786
10006
|
try {
|
|
8787
|
-
(0,
|
|
10007
|
+
(0, import_node_fs14.mkdirSync)(tmpRoot, { recursive: true });
|
|
8788
10008
|
const { stdout } = await execFileBuffer("gh", ghReleaseTarballApiArgs(tag), {
|
|
8789
10009
|
timeout: 12e4,
|
|
8790
10010
|
maxBuffer: 100 * 1024 * 1024,
|
|
8791
10011
|
encoding: "buffer",
|
|
8792
10012
|
windowsHide: true
|
|
8793
10013
|
});
|
|
8794
|
-
(0,
|
|
10014
|
+
(0, import_node_fs14.writeFileSync)(tarPath, stdout);
|
|
8795
10015
|
await execFileBuffer("tar", ["-xzf", tarPath, "-C", tmpRoot], { timeout: 12e4, windowsHide: true });
|
|
8796
|
-
const top = (0,
|
|
10016
|
+
const top = (0, import_node_fs14.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
|
|
8797
10017
|
if (!top) return void 0;
|
|
8798
|
-
const pluginMmi = (0,
|
|
8799
|
-
return (0,
|
|
10018
|
+
const pluginMmi = (0, import_node_path13.join)(tmpRoot, top, "plugins", "mmi");
|
|
10019
|
+
return (0, import_node_fs14.existsSync)((0, import_node_path13.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
|
|
8800
10020
|
} catch {
|
|
8801
10021
|
return void 0;
|
|
8802
10022
|
}
|
|
8803
10023
|
}
|
|
8804
10024
|
async function resolvePluginMmiSource(releasedVersion, hubCheckout, tmpRoot, execFileP5) {
|
|
8805
|
-
(0,
|
|
10025
|
+
(0, import_node_fs14.mkdirSync)(tmpRoot, { recursive: true });
|
|
8806
10026
|
const tag = releaseTag(releasedVersion);
|
|
8807
10027
|
if (hubCheckout) {
|
|
8808
10028
|
const fromHub = await extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5);
|
|
8809
10029
|
if (fromHub) return fromHub;
|
|
8810
10030
|
}
|
|
8811
|
-
return downloadPluginMmiViaGh(tag, (0,
|
|
10031
|
+
return downloadPluginMmiViaGh(tag, (0, import_node_path13.join)(tmpRoot, "gh"));
|
|
8812
10032
|
}
|
|
8813
10033
|
function cursorPluginPinsNeedingSeed(pins, releasedVersion) {
|
|
8814
10034
|
if (!isSemverVersion(releasedVersion)) return pins.filter((pin) => !pin.hasPluginJson || !pin.hasHooksJson || pin.isEmpty);
|
|
@@ -8829,7 +10049,7 @@ async function applyCursorPluginCacheSeed(input) {
|
|
|
8829
10049
|
for (const pin of pinsToSeed) {
|
|
8830
10050
|
syncDirContents(source, pin.path);
|
|
8831
10051
|
}
|
|
8832
|
-
(0,
|
|
10052
|
+
(0, import_node_fs14.rmSync)(tmpRoot, { recursive: true, force: true });
|
|
8833
10053
|
return true;
|
|
8834
10054
|
}
|
|
8835
10055
|
|
|
@@ -8877,8 +10097,12 @@ var MANAGED_GITIGNORE_LINES = [
|
|
|
8877
10097
|
".claude/worktrees/",
|
|
8878
10098
|
".mmi/.session",
|
|
8879
10099
|
".mmi/head-ts/",
|
|
8880
|
-
//
|
|
8881
|
-
//
|
|
10100
|
+
// Honcho's whole runtime dir — its ingest-throttle stamp (`last-skip.json`) is NOT a queue file, so the
|
|
10101
|
+
// recursive patterns below miss it and it dirtied the working tree (blocked a release clean-tree check,
|
|
10102
|
+
// #1472). The dir is pure scratch, so ignore it wholesale, like `.mmi/head-ts/`.
|
|
10103
|
+
".mmi/honcho/",
|
|
10104
|
+
// Recursive so any reuse of the parameterized queue engine in a new .mmi subdir is ignored too —
|
|
10105
|
+
// `**` matches zero dirs, so this still covers the root saga queue.
|
|
8882
10106
|
".mmi/**/saga-pending.jsonl*",
|
|
8883
10107
|
".mmi/**/saga-flush.lock",
|
|
8884
10108
|
".aws-sam/",
|
|
@@ -9092,6 +10316,12 @@ function cachePathJoin(root, ...parts) {
|
|
|
9092
10316
|
const sep = root.includes("\\") ? "\\" : "/";
|
|
9093
10317
|
return [root.replace(/[\\/]+$/, ""), ...parts].join(sep);
|
|
9094
10318
|
}
|
|
10319
|
+
function cacheParentDir(root) {
|
|
10320
|
+
const sep = root.includes("\\") ? "\\" : "/";
|
|
10321
|
+
const trimmed = root.replace(/[\\/]+$/, "");
|
|
10322
|
+
const idx = trimmed.lastIndexOf(sep);
|
|
10323
|
+
return idx <= 0 ? root : trimmed.slice(0, idx);
|
|
10324
|
+
}
|
|
9095
10325
|
function isProtectedCacheDir(name, protectedVersions) {
|
|
9096
10326
|
const normalized = normalizeVersion(name);
|
|
9097
10327
|
return name.startsWith(".") || normalized !== void 0 && protectedVersions.has(normalized);
|
|
@@ -9122,7 +10352,7 @@ function buildMmiPluginCacheCleanupCheck(input) {
|
|
|
9122
10352
|
plannedCount: leftovers.length,
|
|
9123
10353
|
quarantinePlan: leftovers.map((entry) => ({
|
|
9124
10354
|
from: entry.path,
|
|
9125
|
-
to: cachePathJoin(entry.root, ".mmi-quarantine", stamp, entry.name)
|
|
10355
|
+
to: cachePathJoin(cacheParentDir(entry.root), ".mmi-quarantine", stamp, entry.name)
|
|
9126
10356
|
}))
|
|
9127
10357
|
};
|
|
9128
10358
|
}
|
|
@@ -9183,6 +10413,12 @@ var CLAUDE_PLUGIN_HEAL_STEPS = [
|
|
|
9183
10413
|
{ args: ["plugin", "install", "mmi@mmi"], gated: true },
|
|
9184
10414
|
{ args: ["plugin", "enable", "mmi@mmi"], gated: false }
|
|
9185
10415
|
];
|
|
10416
|
+
var CODEX_PLUGIN_RECOVERY = "codex plugin marketplace remove mmi && codex plugin marketplace add mutmutco/MMI-Hub --ref main && codex plugin add mmi@mmi";
|
|
10417
|
+
var CODEX_PLUGIN_HEAL_STEPS = [
|
|
10418
|
+
{ args: ["plugin", "marketplace", "remove", "mmi"], gated: false },
|
|
10419
|
+
{ args: ["plugin", "marketplace", "add", "mutmutco/MMI-Hub", "--ref", "main"], gated: true },
|
|
10420
|
+
{ args: ["plugin", "add", "mmi@mmi"], gated: true }
|
|
10421
|
+
];
|
|
9186
10422
|
function healStepAborts(step, ok) {
|
|
9187
10423
|
return !ok && step.gated;
|
|
9188
10424
|
}
|
|
@@ -9193,7 +10429,7 @@ function pluginRecoveryFix(surface) {
|
|
|
9193
10429
|
case "claude-cli":
|
|
9194
10430
|
return `${claude} # then ${reloadAction(surface)} to reload MMI commands`;
|
|
9195
10431
|
case "codex":
|
|
9196
|
-
return
|
|
10432
|
+
return `${CODEX_PLUGIN_RECOVERY} # then ${reloadAction(surface)}`;
|
|
9197
10433
|
case "cursor":
|
|
9198
10434
|
return `in Cursor Dashboard \u2192 Settings \u2192 Plugins, click Update next to the MMI Team Marketplace; then ${reloadAction(surface)} to reload MMI skills + hooks`;
|
|
9199
10435
|
case "shell":
|
|
@@ -9203,7 +10439,7 @@ function pluginRecoveryFix(surface) {
|
|
|
9203
10439
|
}
|
|
9204
10440
|
var PLUGIN_UPDATE_RECIPES = {
|
|
9205
10441
|
claude: [CLAUDE_PLUGIN_RECOVERY],
|
|
9206
|
-
codex: [
|
|
10442
|
+
codex: [CODEX_PLUGIN_RECOVERY, "codex plugin list # verify mmi@mmi shows the released version"],
|
|
9207
10443
|
cli: ["npm install -g @mutmutco/cli@latest"]
|
|
9208
10444
|
};
|
|
9209
10445
|
function highestSemver(versions) {
|
|
@@ -9257,7 +10493,7 @@ function isSemverVersion2(v) {
|
|
|
9257
10493
|
return typeof v === "string" && /^v?\d+\.\d+\.\d+/.test(v.trim());
|
|
9258
10494
|
}
|
|
9259
10495
|
function staleRecordCommand(surface) {
|
|
9260
|
-
return surface === "codex" ?
|
|
10496
|
+
return surface === "codex" ? CODEX_PLUGIN_RECOVERY : CLAUDE_PLUGIN_RECOVERY;
|
|
9261
10497
|
}
|
|
9262
10498
|
function staleSurfacesFix(stale, releasedVersion) {
|
|
9263
10499
|
const parts = stale.map((s) => {
|
|
@@ -9535,8 +10771,8 @@ async function runStageLiveDown(deps, t) {
|
|
|
9535
10771
|
|
|
9536
10772
|
// src/stage-runner.ts
|
|
9537
10773
|
var import_node_child_process8 = require("node:child_process");
|
|
9538
|
-
var
|
|
9539
|
-
var
|
|
10774
|
+
var import_node_fs15 = require("node:fs");
|
|
10775
|
+
var import_node_path14 = require("node:path");
|
|
9540
10776
|
var import_node_net2 = require("node:net");
|
|
9541
10777
|
var import_node_util7 = require("node:util");
|
|
9542
10778
|
var execFileP4 = (0, import_node_util7.promisify)(import_node_child_process8.execFile);
|
|
@@ -9581,7 +10817,28 @@ function detectStaleEnvFile(exampleContent, targetContent, mtimes) {
|
|
|
9581
10817
|
return void 0;
|
|
9582
10818
|
}
|
|
9583
10819
|
function stageStatePath(cwd = process.cwd()) {
|
|
9584
|
-
return (0,
|
|
10820
|
+
return (0, import_node_path14.join)(cwd, "tmp", "stage", "state.json");
|
|
10821
|
+
}
|
|
10822
|
+
function mergeEnvSecretsIntoFile(content, secrets2) {
|
|
10823
|
+
const lines = content.split(/\r?\n/);
|
|
10824
|
+
const indexByKey = /* @__PURE__ */ new Map();
|
|
10825
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10826
|
+
const trimmed = lines[i].trim();
|
|
10827
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
10828
|
+
const eq = trimmed.indexOf("=");
|
|
10829
|
+
if (eq === -1) continue;
|
|
10830
|
+
indexByKey.set(trimmed.slice(0, eq).trim(), i);
|
|
10831
|
+
}
|
|
10832
|
+
for (const [key, value] of Object.entries(secrets2)) {
|
|
10833
|
+
const escaped = /[\s#"'\\]/.test(value) ? `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : value;
|
|
10834
|
+
const line = `${key}=${escaped}`;
|
|
10835
|
+
const idx = indexByKey.get(key);
|
|
10836
|
+
if (idx != null) lines[idx] = line;
|
|
10837
|
+
else lines.push(line);
|
|
10838
|
+
}
|
|
10839
|
+
const body = lines.join("\n");
|
|
10840
|
+
return body.endsWith("\n") ? body : `${body}
|
|
10841
|
+
`;
|
|
9585
10842
|
}
|
|
9586
10843
|
var POSIX_ONLY_VERBS = ["cp", "mv", "rm", "ln", "cat", "touch", "chmod", "export"];
|
|
9587
10844
|
function posixOnlyShellProblems(command, field, platform = process.platform) {
|
|
@@ -9643,9 +10900,9 @@ async function shell(command, cwd, timeoutMs) {
|
|
|
9643
10900
|
});
|
|
9644
10901
|
}
|
|
9645
10902
|
function readState(path2) {
|
|
9646
|
-
if (!(0,
|
|
10903
|
+
if (!(0, import_node_fs15.existsSync)(path2)) return null;
|
|
9647
10904
|
try {
|
|
9648
|
-
return JSON.parse((0,
|
|
10905
|
+
return JSON.parse((0, import_node_fs15.readFileSync)(path2, "utf8"));
|
|
9649
10906
|
} catch {
|
|
9650
10907
|
return null;
|
|
9651
10908
|
}
|
|
@@ -9697,7 +10954,7 @@ async function stopStage(opts = {}) {
|
|
|
9697
10954
|
return { ok: true, action: "stop", statePath, message: "no previous stage state found" };
|
|
9698
10955
|
}
|
|
9699
10956
|
await killTree(state.pid);
|
|
9700
|
-
(0,
|
|
10957
|
+
(0, import_node_fs15.rmSync)(statePath, { force: true });
|
|
9701
10958
|
return { ok: true, action: "stop", statePath, pid: state.pid, message: `stopped previous stage pid ${state.pid}` };
|
|
9702
10959
|
}
|
|
9703
10960
|
async function startStage(config = {}, opts = {}) {
|
|
@@ -9706,7 +10963,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
9706
10963
|
const cwd = opts.cwd ?? process.cwd();
|
|
9707
10964
|
const statePath = opts.statePath ?? stageStatePath(cwd);
|
|
9708
10965
|
const dir = statePath.slice(0, Math.max(statePath.lastIndexOf("/"), statePath.lastIndexOf("\\")));
|
|
9709
|
-
(0,
|
|
10966
|
+
(0, import_node_fs15.mkdirSync)(dir, { recursive: true });
|
|
9710
10967
|
let stagePort;
|
|
9711
10968
|
if (config.portRange) {
|
|
9712
10969
|
const [s, e] = config.portRange;
|
|
@@ -9716,14 +10973,14 @@ async function startStage(config = {}, opts = {}) {
|
|
|
9716
10973
|
}
|
|
9717
10974
|
const sub = (s) => s != null && stagePort != null ? s.replace(/\$\{?STAGE_PORT\}?/g, String(stagePort)) : s;
|
|
9718
10975
|
if (config.ensureEnv) {
|
|
9719
|
-
const target = (0,
|
|
9720
|
-
const example = (0,
|
|
9721
|
-
if (!(0,
|
|
9722
|
-
(0,
|
|
9723
|
-
} else if ((0,
|
|
9724
|
-
const stale = detectStaleEnvFile((0,
|
|
9725
|
-
exampleMtimeMs: (0,
|
|
9726
|
-
targetMtimeMs: (0,
|
|
10976
|
+
const target = (0, import_node_path14.join)(cwd, config.ensureEnv.target);
|
|
10977
|
+
const example = (0, import_node_path14.join)(cwd, config.ensureEnv.example);
|
|
10978
|
+
if (!(0, import_node_fs15.existsSync)(target) && (0, import_node_fs15.existsSync)(example)) {
|
|
10979
|
+
(0, import_node_fs15.copyFileSync)(example, target);
|
|
10980
|
+
} else if ((0, import_node_fs15.existsSync)(target) && (0, import_node_fs15.existsSync)(example)) {
|
|
10981
|
+
const stale = detectStaleEnvFile((0, import_node_fs15.readFileSync)(example, "utf8"), (0, import_node_fs15.readFileSync)(target, "utf8"), {
|
|
10982
|
+
exampleMtimeMs: (0, import_node_fs15.statSync)(example).mtimeMs,
|
|
10983
|
+
targetMtimeMs: (0, import_node_fs15.statSync)(target).mtimeMs
|
|
9727
10984
|
});
|
|
9728
10985
|
if (stale) {
|
|
9729
10986
|
const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
|
|
@@ -9731,6 +10988,9 @@ async function startStage(config = {}, opts = {}) {
|
|
|
9731
10988
|
console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
|
|
9732
10989
|
}
|
|
9733
10990
|
}
|
|
10991
|
+
if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs15.existsSync)(target)) {
|
|
10992
|
+
(0, import_node_fs15.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs15.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
|
|
10993
|
+
}
|
|
9734
10994
|
}
|
|
9735
10995
|
const extraEnv = {};
|
|
9736
10996
|
for (const [k, v] of Object.entries(config.env ?? {})) extraEnv[k] = sub(v) ?? v;
|
|
@@ -9754,13 +11014,13 @@ async function startStage(config = {}, opts = {}) {
|
|
|
9754
11014
|
healthUrl: sub(config.healthUrl?.trim()) || void 0,
|
|
9755
11015
|
port: stagePort
|
|
9756
11016
|
};
|
|
9757
|
-
(0,
|
|
11017
|
+
(0, import_node_fs15.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf8");
|
|
9758
11018
|
try {
|
|
9759
11019
|
if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
|
|
9760
11020
|
else await waitForProcessStability(child);
|
|
9761
11021
|
} catch (e) {
|
|
9762
11022
|
await killTree(state.pid);
|
|
9763
|
-
(0,
|
|
11023
|
+
(0, import_node_fs15.rmSync)(statePath, { force: true });
|
|
9764
11024
|
throw e;
|
|
9765
11025
|
}
|
|
9766
11026
|
const result = { ok: true, action: "start", statePath, pid: state.pid, port: stagePort, message: `started stage pid ${state.pid}${stagePort != null ? ` on port ${stagePort}` : ""}` };
|
|
@@ -9876,7 +11136,7 @@ function resolveDeployModel2(meta, repo) {
|
|
|
9876
11136
|
if (isDeployModel(m)) return m;
|
|
9877
11137
|
return resolveDeployModel(meta, repo);
|
|
9878
11138
|
}
|
|
9879
|
-
function
|
|
11139
|
+
function clean2(out) {
|
|
9880
11140
|
return out.trim();
|
|
9881
11141
|
}
|
|
9882
11142
|
function requireValue(value, label) {
|
|
@@ -9927,11 +11187,11 @@ async function verifyPublishedRelease(deps, repo, tag, expectedTarget, expectedS
|
|
|
9927
11187
|
if (release.targetCommitish !== expectedTarget) {
|
|
9928
11188
|
throw new Error(`Release ${tag} targetCommitish is ${String(release.targetCommitish || "(missing)")}, expected ${expectedTarget}`);
|
|
9929
11189
|
}
|
|
9930
|
-
const tagSha = requireValue(
|
|
11190
|
+
const tagSha = requireValue(clean2(await deps.run("git", ["rev-parse", `${tag}^{commit}`])), "release tag sha");
|
|
9931
11191
|
if (tagSha !== expectedSha) {
|
|
9932
11192
|
throw new Error(`Release ${tag} tag points at ${tagSha}, expected ${expectedSha}`);
|
|
9933
11193
|
}
|
|
9934
|
-
const branchOut =
|
|
11194
|
+
const branchOut = clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${expectedTarget}`]));
|
|
9935
11195
|
const branchSha = requireValue(branchOut.split(/\s+/)[0] ?? "", `origin/${expectedTarget} sha`);
|
|
9936
11196
|
if (branchSha !== expectedSha) {
|
|
9937
11197
|
throw new Error(`origin/${expectedTarget} points at ${branchSha}, expected ${expectedSha}`);
|
|
@@ -9991,7 +11251,7 @@ function ensurePositiveCount(out, emptyMessage) {
|
|
|
9991
11251
|
if (count <= 0) throw new Error(emptyMessage);
|
|
9992
11252
|
}
|
|
9993
11253
|
async function remoteBranchExists(deps, branch) {
|
|
9994
|
-
const out =
|
|
11254
|
+
const out = clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
|
|
9995
11255
|
return out.length > 0;
|
|
9996
11256
|
}
|
|
9997
11257
|
async function loadReleaseTrackBranchHints(deps) {
|
|
@@ -10003,10 +11263,10 @@ async function loadReleaseTrackBranchHints(deps) {
|
|
|
10003
11263
|
return { hasDevelopmentBranch, hasMainBranch, hasRcBranch };
|
|
10004
11264
|
}
|
|
10005
11265
|
async function buildTrainApplyContext(deps) {
|
|
10006
|
-
const repo = requireValue(
|
|
11266
|
+
const repo = requireValue(clean2(await deps.run("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])), "repo");
|
|
10007
11267
|
const [owner, name] = repo.split("/");
|
|
10008
11268
|
if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
|
|
10009
|
-
const login = requireValue(
|
|
11269
|
+
const login = requireValue(clean2(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
|
|
10010
11270
|
const verdict = await deps.trainAuthority(repo);
|
|
10011
11271
|
if (!verdict.ok) throw new Error(`${commandAuthorityLabel(owner)}: train authority could not be verified (${verdict.error})`);
|
|
10012
11272
|
if (!verdict.train) {
|
|
@@ -10027,12 +11287,12 @@ async function requireCleanTree(deps) {
|
|
|
10027
11287
|
if (status.trim()) throw new Error("working tree must be clean before train apply");
|
|
10028
11288
|
}
|
|
10029
11289
|
async function requireBranch(deps, branch) {
|
|
10030
|
-
const current =
|
|
11290
|
+
const current = clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
10031
11291
|
if (current !== branch) throw new Error(`must run from ${branch}, currently on ${current || "(unknown)"}`);
|
|
10032
11292
|
}
|
|
10033
|
-
var
|
|
11293
|
+
var HUB_REPO3 = "mutmutco/MMI-Hub";
|
|
10034
11294
|
function isHubControlRepo(repo) {
|
|
10035
|
-
return repo.toLowerCase() ===
|
|
11295
|
+
return repo.toLowerCase() === HUB_REPO3.toLowerCase();
|
|
10036
11296
|
}
|
|
10037
11297
|
function projectGetFailureText(e) {
|
|
10038
11298
|
const err = e;
|
|
@@ -10092,7 +11352,7 @@ async function correlateDispatchedRun(deps, workflow, since) {
|
|
|
10092
11352
|
"run",
|
|
10093
11353
|
"list",
|
|
10094
11354
|
"--repo",
|
|
10095
|
-
|
|
11355
|
+
HUB_REPO3,
|
|
10096
11356
|
"--workflow",
|
|
10097
11357
|
workflow,
|
|
10098
11358
|
"--limit",
|
|
@@ -10126,7 +11386,7 @@ async function correlateWorkflowRun(deps, args) {
|
|
|
10126
11386
|
"run",
|
|
10127
11387
|
"list",
|
|
10128
11388
|
"--repo",
|
|
10129
|
-
|
|
11389
|
+
HUB_REPO3,
|
|
10130
11390
|
"--workflow",
|
|
10131
11391
|
args.workflow,
|
|
10132
11392
|
"--event",
|
|
@@ -10154,7 +11414,7 @@ async function correlateWorkflowRun(deps, args) {
|
|
|
10154
11414
|
async function watchTenantRun(deps, runId) {
|
|
10155
11415
|
if (runId == null) return "pending";
|
|
10156
11416
|
try {
|
|
10157
|
-
await deps.run("gh", ["run", "watch", String(runId), "--repo",
|
|
11417
|
+
await deps.run("gh", ["run", "watch", String(runId), "--repo", HUB_REPO3, "--exit-status"]);
|
|
10158
11418
|
return "success";
|
|
10159
11419
|
} catch {
|
|
10160
11420
|
return "failure";
|
|
@@ -10196,7 +11456,7 @@ async function discoverRequiredCheckContexts(deps, ctx, branch) {
|
|
|
10196
11456
|
return [...contexts];
|
|
10197
11457
|
}
|
|
10198
11458
|
async function findOpenAlignmentPr(deps, ctx) {
|
|
10199
|
-
const out =
|
|
11459
|
+
const out = clean2(await deps.run("gh", ["pr", "list", "--repo", ctx.repo, "--base", "development", "--head", "main", "--state", "open", "--json", "number,url"]));
|
|
10200
11460
|
if (!out) return void 0;
|
|
10201
11461
|
const rows = JSON.parse(out);
|
|
10202
11462
|
const row = rows.find((r) => typeof r.number === "number" && typeof r.url === "string");
|
|
@@ -10224,14 +11484,14 @@ async function rollDevelopmentForward(deps, ctx, tag) {
|
|
|
10224
11484
|
note: `alignment PR already open: ${existing.url} \u2014 land it with \`gh pr merge ${existing.number} --merge\``
|
|
10225
11485
|
};
|
|
10226
11486
|
}
|
|
10227
|
-
const ahead =
|
|
11487
|
+
const ahead = clean2(await deps.run("git", ["rev-list", "--count", "origin/development..main"]));
|
|
10228
11488
|
if (ahead === "0") {
|
|
10229
11489
|
return { status: "aligned", note: "development already contains the released main; nothing to roll forward" };
|
|
10230
11490
|
}
|
|
10231
11491
|
const body = `Carries the ${tag} release (including the version fold) from \`main\` back to \`development\`.
|
|
10232
11492
|
|
|
10233
11493
|
\`development\` requires status checks, so the release train opens this alignment PR instead of a direct push of the un-checked merge commit (#1143). Land it with a **true merge** (\`gh pr merge --merge\`, not squash) so the merge parentage survives and the misalignment guard stays satisfied.`;
|
|
10234
|
-
const url =
|
|
11494
|
+
const url = clean2(await deps.run("gh", ["pr", "create", "--repo", ctx.repo, "--base", "development", "--head", "main", "--title", `chore(release): align development to ${tag}`, "--body", body]));
|
|
10235
11495
|
const number = parsePrNumber(url);
|
|
10236
11496
|
return {
|
|
10237
11497
|
status: "pr-pending",
|
|
@@ -10297,10 +11557,10 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
|
|
|
10297
11557
|
}
|
|
10298
11558
|
async function ensureTagPushed(deps, tag, sha) {
|
|
10299
11559
|
const remoteOut = await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]);
|
|
10300
|
-
const remoteSha =
|
|
11560
|
+
const remoteSha = clean2(remoteOut).split(/\s+/)[0] || "";
|
|
10301
11561
|
let localSha = "";
|
|
10302
11562
|
try {
|
|
10303
|
-
localSha =
|
|
11563
|
+
localSha = clean2(await deps.run("git", ["rev-parse", "--verify", `refs/tags/${tag}^{commit}`]));
|
|
10304
11564
|
} catch {
|
|
10305
11565
|
}
|
|
10306
11566
|
if (remoteSha) {
|
|
@@ -10323,7 +11583,7 @@ async function ensureTagPushed(deps, tag, sha) {
|
|
|
10323
11583
|
}
|
|
10324
11584
|
async function resolveRcResumeTag(deps, base2, sha) {
|
|
10325
11585
|
const out = await deps.run("git", ["ls-remote", "--tags", "origin", `refs/tags/${base2}-rc.*`]);
|
|
10326
|
-
const onSha =
|
|
11586
|
+
const onSha = clean2(out).split("\n").map((line) => line.trim()).filter(Boolean).map((line) => line.split(/\s+/)).filter(([refSha]) => refSha === sha).map(([, ref]) => ref.replace(/^refs\/tags\//, "").replace(/\^\{\}$/, "")).filter((ref) => new RegExp(`^${base2.replace(/\./g, "\\.")}-rc\\.\\d+$`).test(ref));
|
|
10327
11587
|
const unique = [...new Set(onSha)];
|
|
10328
11588
|
if (unique.length === 0) return { tag: null };
|
|
10329
11589
|
const rcNum = (t) => Number.parseInt(t.replace(/^.*-rc\./, ""), 10);
|
|
@@ -10375,7 +11635,7 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince
|
|
|
10375
11635
|
"run",
|
|
10376
11636
|
"tenant-publish.yml",
|
|
10377
11637
|
"--repo",
|
|
10378
|
-
|
|
11638
|
+
HUB_REPO3,
|
|
10379
11639
|
"-f",
|
|
10380
11640
|
`slug=${ctx.slug}`,
|
|
10381
11641
|
"-f",
|
|
@@ -10448,13 +11708,13 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
10448
11708
|
"nothing to promote: origin/development is not ahead of origin/rc"
|
|
10449
11709
|
);
|
|
10450
11710
|
const deployModel2 = await preflight(deps, ctx, "rc", meta);
|
|
10451
|
-
const releaseBase = requireValue(
|
|
11711
|
+
const releaseBase = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
|
|
10452
11712
|
await deps.run("git", ["checkout", "rc"]);
|
|
10453
11713
|
await ffOnlyPull(deps, "rc");
|
|
10454
11714
|
await deps.run("git", ["merge", "development", "--no-edit"]);
|
|
10455
|
-
const rcSha = requireValue(
|
|
11715
|
+
const rcSha = requireValue(clean2(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
|
|
10456
11716
|
const resume = await resolveRcResumeTag(deps, releaseBase, rcSha);
|
|
10457
|
-
const tag2 = resume.tag ?? requireValue(
|
|
11717
|
+
const tag2 = resume.tag ?? requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
|
|
10458
11718
|
const resumeNote = resume.tag ? resume.note : void 0;
|
|
10459
11719
|
await ensureTagPushed(deps, tag2, rcSha);
|
|
10460
11720
|
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "rc");
|
|
@@ -10472,7 +11732,7 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
10472
11732
|
"nothing to release: origin/development is not ahead of origin/main"
|
|
10473
11733
|
);
|
|
10474
11734
|
const deployModel2 = await preflight(deps, ctx, "main", meta);
|
|
10475
|
-
const tag2 = requireValue(
|
|
11735
|
+
const tag2 = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
|
|
10476
11736
|
const foldPaths2 = await resolveFoldPaths(deps, deployModel2);
|
|
10477
11737
|
const tolerated2 = [...foldPaths2, ...RELEASE_TOLERATED_PATHS];
|
|
10478
11738
|
const predicted2 = await predictMergeConflicts(deps, "origin/main", "origin/development");
|
|
@@ -10490,12 +11750,12 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
10490
11750
|
await mergeWithSpineResolution(deps, "development", "development -> main", "theirs", tolerated2);
|
|
10491
11751
|
}
|
|
10492
11752
|
const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
|
|
10493
|
-
const releaseSha2 = requireValue(
|
|
11753
|
+
const releaseSha2 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
10494
11754
|
await ensureTagPushed(deps, tag2, releaseSha2);
|
|
10495
11755
|
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
10496
11756
|
const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
|
|
10497
11757
|
await deps.run("git", ["push", "origin", "main"]);
|
|
10498
|
-
const releaseUrl2 =
|
|
11758
|
+
const releaseUrl2 = clean2(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
10499
11759
|
await verifyPublishedRelease(deps, ctx.repo, tag2, "main", releaseSha2);
|
|
10500
11760
|
const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
10501
11761
|
const autoRunSince2 = (deps.now ?? Date.now)();
|
|
@@ -10544,8 +11804,8 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
10544
11804
|
"nothing to release: origin/development is not ahead of origin/main"
|
|
10545
11805
|
);
|
|
10546
11806
|
const deployModel2 = await preflight(deps, ctx, "main", meta);
|
|
10547
|
-
const tag2 = requireValue(
|
|
10548
|
-
const rcShaAtRelease = hasRcBranch ?
|
|
11807
|
+
const tag2 = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
|
|
11808
|
+
const rcShaAtRelease = hasRcBranch ? clean2(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
|
|
10549
11809
|
const foldPaths2 = await resolveFoldPaths(deps, deployModel2);
|
|
10550
11810
|
const tolerated2 = [...foldPaths2, ...RELEASE_TOLERATED_PATHS];
|
|
10551
11811
|
const predicted2 = await predictMergeConflicts(deps, "origin/main", "origin/development");
|
|
@@ -10563,12 +11823,12 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
10563
11823
|
await mergeWithSpineResolution(deps, "development", "development -> main", "theirs", tolerated2);
|
|
10564
11824
|
}
|
|
10565
11825
|
const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
|
|
10566
|
-
const releaseSha2 = requireValue(
|
|
11826
|
+
const releaseSha2 = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
10567
11827
|
await ensureTagPushed(deps, tag2, releaseSha2);
|
|
10568
11828
|
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
10569
11829
|
const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
|
|
10570
11830
|
await deps.run("git", ["push", "origin", "main"]);
|
|
10571
|
-
const releaseUrl2 =
|
|
11831
|
+
const releaseUrl2 = clean2(await deps.run("gh", ["release", "create", tag2, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
10572
11832
|
await verifyPublishedRelease(deps, ctx.repo, tag2, "main", releaseSha2);
|
|
10573
11833
|
const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
10574
11834
|
const autoRunSince2 = (deps.now ?? Date.now)();
|
|
@@ -10635,7 +11895,7 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
10635
11895
|
`hotfix-coverage: ${coverage.uncovered.length} main-only commit(s) not proven in origin/rc \u2014 the candidate would revert a prod hotfix: ${list}. Re-cut /rcand from development, or have the authorized human verify the content is in the candidate and rerun release with --ack <sha>[,<sha>\u2026].`
|
|
10636
11896
|
);
|
|
10637
11897
|
}
|
|
10638
|
-
const releasedRcSha =
|
|
11898
|
+
const releasedRcSha = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
10639
11899
|
await deps.run("git", ["checkout", "main"]);
|
|
10640
11900
|
await ffOnlyPull(deps, "main");
|
|
10641
11901
|
if (predicted.length === 0) {
|
|
@@ -10643,15 +11903,15 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
10643
11903
|
} else {
|
|
10644
11904
|
await mergeWithSpineResolution(deps, "rc", "rc -> main", "theirs", tolerated);
|
|
10645
11905
|
}
|
|
10646
|
-
const tag = requireValue(
|
|
11906
|
+
const tag = requireValue(clean2(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
|
|
10647
11907
|
const versionFold = await foldReleaseVersion(deps, deployModel, tag, foldPaths);
|
|
10648
|
-
const releaseSha = requireValue(
|
|
11908
|
+
const releaseSha = requireValue(clean2(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
10649
11909
|
await ensureTagPushed(deps, tag, releaseSha);
|
|
10650
11910
|
const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
10651
11911
|
const checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
|
|
10652
11912
|
await deps.run("git", ["push", "origin", "main"]);
|
|
10653
11913
|
const autoRunSince = (deps.now ?? Date.now)();
|
|
10654
|
-
const releaseUrl =
|
|
11914
|
+
const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
10655
11915
|
await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
|
|
10656
11916
|
const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
10657
11917
|
const d = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report");
|
|
@@ -10779,7 +12039,7 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
|
10779
12039
|
}
|
|
10780
12040
|
try {
|
|
10781
12041
|
await deps.run("git", ["fetch", "origin", "rc"]);
|
|
10782
|
-
const rcNow =
|
|
12042
|
+
const rcNow = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
10783
12043
|
if (rcNow !== releasedRcSha) {
|
|
10784
12044
|
return {
|
|
10785
12045
|
status: "skipped",
|
|
@@ -10811,7 +12071,7 @@ async function runTenantRedeploy(deps, options) {
|
|
|
10811
12071
|
const repo = options.repo;
|
|
10812
12072
|
const [owner, name] = repo.split("/");
|
|
10813
12073
|
if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
|
|
10814
|
-
const login = requireValue(
|
|
12074
|
+
const login = requireValue(clean2(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
|
|
10815
12075
|
const verdict = await deps.trainAuthority(repo);
|
|
10816
12076
|
if (!verdict.ok) throw new Error(`${commandAuthorityLabel(owner)}: train authority could not be verified (${verdict.error})`);
|
|
10817
12077
|
if (!verdict.train) throw new Error(`${commandAuthorityLabel(owner)}: @${login} is ${verdict.role} \u2014 no train authority on ${repo}`);
|
|
@@ -10967,7 +12227,7 @@ var HOTFIX_RUN_FIND_ATTEMPTS = 10;
|
|
|
10967
12227
|
var HOTFIX_RUN_FIND_DELAY_MS = 15e3;
|
|
10968
12228
|
var HOTFIX_VERIFY_ATTEMPTS = 5;
|
|
10969
12229
|
var HOTFIX_VERIFY_RETRY_MS = 6e3;
|
|
10970
|
-
function
|
|
12230
|
+
function clean3(out) {
|
|
10971
12231
|
return out.trim();
|
|
10972
12232
|
}
|
|
10973
12233
|
function sleeper(deps) {
|
|
@@ -11025,7 +12285,7 @@ async function resolveHotfixSource(deps, ctx, from) {
|
|
|
11025
12285
|
if (!sha2) throw new Error(`PR #${num} has no merge commit recorded \u2014 name the commit SHA explicitly`);
|
|
11026
12286
|
return { sha: sha2, label: `PR #${num} (${sha2.slice(0, 7)})` };
|
|
11027
12287
|
}
|
|
11028
|
-
const sha =
|
|
12288
|
+
const sha = clean3(await deps.run("git", ["rev-parse", "--verify", `${from}^{commit}`]));
|
|
11029
12289
|
if (!sha) throw new Error(`could not resolve commit ${from}`);
|
|
11030
12290
|
return { sha, label: sha.slice(0, 7) };
|
|
11031
12291
|
}
|
|
@@ -11053,7 +12313,7 @@ async function runHotfixStart(deps, options) {
|
|
|
11053
12313
|
};
|
|
11054
12314
|
}
|
|
11055
12315
|
const { sha, label } = await resolveHotfixSource(deps, ctx, options.from);
|
|
11056
|
-
const remoteBranch =
|
|
12316
|
+
const remoteBranch = clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
|
|
11057
12317
|
if (remoteBranch) {
|
|
11058
12318
|
await deps.run("git", ["checkout", branch]);
|
|
11059
12319
|
const isRulesSource2 = deps.isRulesSource ? await deps.isRulesSource() : false;
|
|
@@ -11086,7 +12346,7 @@ async function runHotfixStart(deps, options) {
|
|
|
11086
12346
|
await deps.run("git", ["push", "-u", "origin", branch]);
|
|
11087
12347
|
}
|
|
11088
12348
|
const bumpNote = deployModel === "hub-serverless" ? " with the locked distribution bump" : "";
|
|
11089
|
-
const prUrl =
|
|
12349
|
+
const prUrl = clean3(await deps.run("gh", [
|
|
11090
12350
|
"pr",
|
|
11091
12351
|
"create",
|
|
11092
12352
|
"--repo",
|
|
@@ -11206,7 +12466,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
11206
12466
|
}
|
|
11207
12467
|
let verifyNote;
|
|
11208
12468
|
if (deployModel === "hub-serverless") {
|
|
11209
|
-
const previousRef =
|
|
12469
|
+
const previousRef = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
11210
12470
|
const publishSucceeded = runs.find((r) => r.workflow === "publish.yml")?.conclusion === "success";
|
|
11211
12471
|
try {
|
|
11212
12472
|
await deps.run("git", ["-c", "advice.detachedHead=false", "checkout", tag]);
|
|
@@ -11291,9 +12551,9 @@ async function runHotfixStatus(deps, versionInput) {
|
|
|
11291
12551
|
return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts) };
|
|
11292
12552
|
}
|
|
11293
12553
|
async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
11294
|
-
const branchExists = Boolean(
|
|
12554
|
+
const branchExists = Boolean(clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
|
|
11295
12555
|
const pr2 = await findHotfixPr(deps, ctx, tag);
|
|
11296
|
-
const remoteTag =
|
|
12556
|
+
const remoteTag = clean3(await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]));
|
|
11297
12557
|
const tagPushed = Boolean(remoteTag);
|
|
11298
12558
|
const tagSha = remoteTag.split(/\s+/)[0] || "";
|
|
11299
12559
|
let releaseExists = false;
|
|
@@ -11327,7 +12587,7 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
|
11327
12587
|
}
|
|
11328
12588
|
}
|
|
11329
12589
|
}
|
|
11330
|
-
const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(
|
|
12590
|
+
const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean3, () => "unknown");
|
|
11331
12591
|
return { tag, version, branchExists, pr: pr2 ? { number: pr2.number, state: pr2.state, url: pr2.url } : null, tagPushed, releaseExists, runs, npmVersion };
|
|
11332
12592
|
}
|
|
11333
12593
|
async function findInFlightHotfixVersion(deps, ctx) {
|
|
@@ -11350,7 +12610,7 @@ async function findInFlightHotfixVersion(deps, ctx) {
|
|
|
11350
12610
|
const m = typeof row.headRefName === "string" && /^hotfix\/(v\d+\.\d+\.\d+)/.exec(row.headRefName);
|
|
11351
12611
|
if (m) tags.add(m[1]);
|
|
11352
12612
|
}
|
|
11353
|
-
const branchOut =
|
|
12613
|
+
const branchOut = clean3(await deps.run("git", ["ls-remote", "origin", "refs/heads/hotfix/v*"]));
|
|
11354
12614
|
for (const line of branchOut.split("\n").filter(Boolean)) {
|
|
11355
12615
|
const ref = line.split(/\s+/)[1] ?? "";
|
|
11356
12616
|
const m = /^refs\/heads\/hotfix\/(v\d+\.\d+\.\d+)/.exec(ref);
|
|
@@ -11499,7 +12759,7 @@ async function announceRelease(deps, args) {
|
|
|
11499
12759
|
}
|
|
11500
12760
|
|
|
11501
12761
|
// src/port-registry.ts
|
|
11502
|
-
var
|
|
12762
|
+
var import_node_fs16 = require("node:fs");
|
|
11503
12763
|
|
|
11504
12764
|
// ../infra/port-geometry.mjs
|
|
11505
12765
|
var PORT_BLOCK = 100;
|
|
@@ -11513,8 +12773,8 @@ function nextPortBlock(registry2) {
|
|
|
11513
12773
|
return [base2, base2 + PORT_SPAN];
|
|
11514
12774
|
}
|
|
11515
12775
|
function loadPortRegistry(path2) {
|
|
11516
|
-
if (!(0,
|
|
11517
|
-
const raw = JSON.parse((0,
|
|
12776
|
+
if (!(0, import_node_fs16.existsSync)(path2)) return {};
|
|
12777
|
+
const raw = JSON.parse((0, import_node_fs16.readFileSync)(path2, "utf8"));
|
|
11518
12778
|
const out = {};
|
|
11519
12779
|
for (const [key, value] of Object.entries(raw)) {
|
|
11520
12780
|
if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
|
|
@@ -11528,9 +12788,9 @@ function ensurePortRange(repo, path2) {
|
|
|
11528
12788
|
const existing = registry2[repo];
|
|
11529
12789
|
if (existing) return existing;
|
|
11530
12790
|
const range = nextPortBlock(registry2);
|
|
11531
|
-
const raw = (0,
|
|
12791
|
+
const raw = (0, import_node_fs16.existsSync)(path2) ? JSON.parse((0, import_node_fs16.readFileSync)(path2, "utf8")) : {};
|
|
11532
12792
|
raw[repo] = range;
|
|
11533
|
-
(0,
|
|
12793
|
+
(0, import_node_fs16.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
|
|
11534
12794
|
return range;
|
|
11535
12795
|
}
|
|
11536
12796
|
function portCursorSeed(registry2) {
|
|
@@ -11582,7 +12842,7 @@ function safeJson(text, fallback) {
|
|
|
11582
12842
|
return fallback;
|
|
11583
12843
|
}
|
|
11584
12844
|
}
|
|
11585
|
-
async function
|
|
12845
|
+
async function restJson2(deps, path2, fallback) {
|
|
11586
12846
|
try {
|
|
11587
12847
|
return await deps.client.rest("GET", path2) ?? fallback;
|
|
11588
12848
|
} catch {
|
|
@@ -11709,7 +12969,7 @@ async function auditRepoAccess(repo, repoClass, owners, deps, projectAdmins = /*
|
|
|
11709
12969
|
return { repo, class: repoClass, ok: !findings.some((f) => f.severity === "high"), findings };
|
|
11710
12970
|
}
|
|
11711
12971
|
async function auditOrgBasePermission(deps) {
|
|
11712
|
-
const org = await
|
|
12972
|
+
const org = await restJson2(deps, `orgs/${OWNER2}`, {});
|
|
11713
12973
|
const perm = org.default_repository_permission;
|
|
11714
12974
|
if (perm && perm !== "read" && perm !== "none") {
|
|
11715
12975
|
return [{
|
|
@@ -11818,8 +13078,8 @@ var requiredIssueTemplates = [
|
|
|
11818
13078
|
var requiredWorkflows = [];
|
|
11819
13079
|
var requiredProductWorkflows = [".github/workflows/gate.yml"];
|
|
11820
13080
|
var requiredProductRulesetRef = ".github/rulesets/mmi-product-required-checks.json";
|
|
11821
|
-
var
|
|
11822
|
-
var requiredLabels = ["bug", "feature", "task"
|
|
13081
|
+
var HUB_REPO4 = "mutmutco/MMI-Hub";
|
|
13082
|
+
var requiredLabels = ["bug", "feature", "task"];
|
|
11823
13083
|
var requiredPriorityOptions = ["Urgent", "High", "Medium", "Low"];
|
|
11824
13084
|
var strayDefaultLabels = ["documentation", "duplicate", "enhancement", "good first issue", "help wanted", "invalid", "question", "wontfix"];
|
|
11825
13085
|
var requiredStatusOptions = ["Todo", "In Progress", "In Review", "Done"];
|
|
@@ -11858,7 +13118,7 @@ function safeJson2(text, fallback) {
|
|
|
11858
13118
|
return fallback;
|
|
11859
13119
|
}
|
|
11860
13120
|
}
|
|
11861
|
-
async function
|
|
13121
|
+
async function restJson3(deps, path2, fallback) {
|
|
11862
13122
|
try {
|
|
11863
13123
|
return await deps.client.rest("GET", path2) ?? fallback;
|
|
11864
13124
|
} catch {
|
|
@@ -11872,7 +13132,7 @@ async function restPagedJson2(deps, path2, fallback) {
|
|
|
11872
13132
|
return fallback;
|
|
11873
13133
|
}
|
|
11874
13134
|
}
|
|
11875
|
-
async function
|
|
13135
|
+
async function contentExists2(deps, repo, branch, path2) {
|
|
11876
13136
|
try {
|
|
11877
13137
|
const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
|
|
11878
13138
|
await deps.client.rest("GET", `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`);
|
|
@@ -11917,17 +13177,17 @@ function missingRuleTypes(ruleset, required) {
|
|
|
11917
13177
|
const types = new Set((ruleset.rules || []).map((rule) => rule.type).filter(Boolean));
|
|
11918
13178
|
return required.filter((type) => !types.has(type));
|
|
11919
13179
|
}
|
|
11920
|
-
function
|
|
13180
|
+
function rulesetStatusChecks2(rulesets) {
|
|
11921
13181
|
return new Set(rulesets.flatMap((ruleset) => (ruleset.rules || []).filter((rule) => rule.type === "required_status_checks").flatMap((rule) => rule.parameters?.required_status_checks || []).map((check) => check.context).filter((context) => Boolean(context))));
|
|
11922
13182
|
}
|
|
11923
|
-
async function
|
|
13183
|
+
async function rulesetDetails2(deps, repo, list) {
|
|
11924
13184
|
const details = [];
|
|
11925
13185
|
for (const ruleset of list) {
|
|
11926
13186
|
if (ruleset.id == null || ruleset.rules != null) {
|
|
11927
13187
|
details.push(ruleset);
|
|
11928
13188
|
continue;
|
|
11929
13189
|
}
|
|
11930
|
-
details.push(await
|
|
13190
|
+
details.push(await restJson3(deps, `repos/${repo}/rulesets/${ruleset.id}`, ruleset));
|
|
11931
13191
|
}
|
|
11932
13192
|
return details;
|
|
11933
13193
|
}
|
|
@@ -11946,7 +13206,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
11946
13206
|
const branchesWanted = expectedBranches(repoClass, releaseTrack);
|
|
11947
13207
|
const baseBranch = releaseTrack === "trunk" || repoClass === "content" ? "main" : "development";
|
|
11948
13208
|
const checks = [];
|
|
11949
|
-
const repoInfo = await
|
|
13209
|
+
const repoInfo = await restJson3(deps, `repos/${repo}`, {});
|
|
11950
13210
|
checks.push({ ok: Boolean(repoInfo.default_branch), label: "repo exists" });
|
|
11951
13211
|
checks.push({ ok: repoInfo.default_branch === baseBranch, label: `default branch is ${baseBranch}`, detail: repoInfo.default_branch || "missing" });
|
|
11952
13212
|
checks.push({ ok: repoInfo.has_wiki === true, label: "wiki enabled", detail: repoInfo.has_wiki === true ? void 0 : "has_wiki is false or unavailable" });
|
|
@@ -11976,29 +13236,29 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
11976
13236
|
detail: overgrants.length ? `over-granted: ${overgrants.map((f) => f.actor).join(", ")}` : void 0
|
|
11977
13237
|
});
|
|
11978
13238
|
for (const path2 of requiredDocs) {
|
|
11979
|
-
checks.push({ ok: await
|
|
13239
|
+
checks.push({ ok: await contentExists2(deps, repo, baseBranch, path2), label: `bootstrap artifact exists: ${path2}` });
|
|
11980
13240
|
}
|
|
11981
13241
|
for (const path2 of requiredIssueTemplates) {
|
|
11982
|
-
checks.push({ ok: await
|
|
13242
|
+
checks.push({ ok: await contentExists2(deps, repo, baseBranch, path2), label: `issue template exists: ${path2}` });
|
|
11983
13243
|
}
|
|
11984
13244
|
for (const path2 of requiredWorkflows) {
|
|
11985
|
-
checks.push({ ok: await
|
|
13245
|
+
checks.push({ ok: await contentExists2(deps, repo, baseBranch, path2), label: `automation workflow exists: ${path2}` });
|
|
11986
13246
|
}
|
|
11987
|
-
if (repo !==
|
|
13247
|
+
if (repo !== HUB_REPO4 && repoClass === "deployable") {
|
|
11988
13248
|
for (const path2 of requiredProductWorkflows) {
|
|
11989
|
-
checks.push({ ok: await
|
|
13249
|
+
checks.push({ ok: await contentExists2(deps, repo, baseBranch, path2), label: `gate workflow exists: ${path2}` });
|
|
11990
13250
|
}
|
|
11991
13251
|
checks.push({
|
|
11992
|
-
ok: await
|
|
13252
|
+
ok: await contentExists2(deps, repo, baseBranch, requiredProductRulesetRef),
|
|
11993
13253
|
label: "product required-check ruleset reference exists",
|
|
11994
13254
|
detail: `expected: ${requiredProductRulesetRef} (apply as an active repo ruleset after bootstrap)`
|
|
11995
13255
|
});
|
|
11996
13256
|
}
|
|
11997
13257
|
if (repoClass === "deployable") {
|
|
11998
13258
|
const trainScript = "scripts/next-version.mjs";
|
|
11999
|
-
checks.push({ ok: await
|
|
13259
|
+
checks.push({ ok: await contentExists2(deps, repo, baseBranch, trainScript), label: `train tooling script exists: ${trainScript}` });
|
|
12000
13260
|
}
|
|
12001
|
-
checks.push({ ok: await
|
|
13261
|
+
checks.push({ ok: await contentExists2(deps, repo, baseBranch, ".cursor/environment.json"), label: "Cursor environment committed" });
|
|
12002
13262
|
const readme = await contentText(deps, repo, baseBranch, "README.md");
|
|
12003
13263
|
checks.push({
|
|
12004
13264
|
ok: readme !== null && readme.includes("## Agent context"),
|
|
@@ -12006,7 +13266,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
12006
13266
|
detail: readme === null ? "README.md not readable via API" : void 0
|
|
12007
13267
|
});
|
|
12008
13268
|
const agentRulesPath = `.cursor/rules/${repoSlugFromFullName(repo)}.mdc`;
|
|
12009
|
-
const agentRulesOk = await
|
|
13269
|
+
const agentRulesOk = await contentExists2(deps, repo, baseBranch, agentRulesPath);
|
|
12010
13270
|
checks.push({
|
|
12011
13271
|
ok: agentRulesOk,
|
|
12012
13272
|
label: "Cursor agent rules file exists",
|
|
@@ -12019,7 +13279,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
12019
13279
|
}
|
|
12020
13280
|
const strays = strayDefaultLabels.filter((l) => labelNames.has(l));
|
|
12021
13281
|
checks.push({ ok: strays.length === 0, label: "no stray GitHub-default labels", detail: presentDetail(strays) });
|
|
12022
|
-
const actions = await
|
|
13282
|
+
const actions = await restJson3(deps, `repos/${repo}/actions/permissions`, {});
|
|
12023
13283
|
checks.push({ ok: actions.enabled === true, label: "GitHub Actions enabled" });
|
|
12024
13284
|
const config = deps.projectMeta ?? null;
|
|
12025
13285
|
checks.push({
|
|
@@ -12124,8 +13384,8 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
12124
13384
|
if (fanout != null) checks.push({ ok: fanout, label: `fanout target registered on ${baseBranch}` });
|
|
12125
13385
|
const projectRegistry = localRegistryCheck(deps, "projects.json", (json) => Array.isArray(json?.projects) && projectRegistryIncludesRepo(json.projects, repo));
|
|
12126
13386
|
if (projectRegistry != null) checks.push({ ok: projectRegistry, label: "cloud-agent project registry includes repo" });
|
|
12127
|
-
const rulesetList = await
|
|
12128
|
-
const rulesets = await
|
|
13387
|
+
const rulesetList = await restJson3(deps, `repos/${repo}/rulesets?includes_parents=true`, []);
|
|
13388
|
+
const rulesets = await rulesetDetails2(deps, repo, rulesetList);
|
|
12129
13389
|
const activeOrgRulesets = rulesets.filter(
|
|
12130
13390
|
(r) => r.source_type === "Organization" && r.target === "branch" && r.enforcement === "active"
|
|
12131
13391
|
);
|
|
@@ -12136,16 +13396,16 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
12136
13396
|
label: "covered by an active org ruleset",
|
|
12137
13397
|
detail: orgRuleset ? void 0 : activeOrgRulesets.length === 0 ? "no active Organization-sourced branch ruleset targets this repo" : `missing rule types: ${missingOrgRuleTypes.join(", ")}`
|
|
12138
13398
|
});
|
|
12139
|
-
if (repo ===
|
|
12140
|
-
const statusChecks =
|
|
13399
|
+
if (repo === HUB_REPO4) {
|
|
13400
|
+
const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
|
|
12141
13401
|
const missing = requiredHubStatusChecks.filter((check) => !statusChecks.has(check));
|
|
12142
13402
|
checks.push({
|
|
12143
13403
|
ok: missing.length === 0,
|
|
12144
13404
|
label: "Hub required status checks configured",
|
|
12145
13405
|
detail: optionDetail(missing)
|
|
12146
13406
|
});
|
|
12147
|
-
} else {
|
|
12148
|
-
const statusChecks =
|
|
13407
|
+
} else if (repoClass === "deployable") {
|
|
13408
|
+
const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
|
|
12149
13409
|
const missing = requiredProductStatusChecks.filter((check) => !statusChecks.has(check));
|
|
12150
13410
|
checks.push({
|
|
12151
13411
|
ok: missing.length === 0,
|
|
@@ -13324,8 +14584,8 @@ function resolveKbSource(rawBase) {
|
|
|
13324
14584
|
return { owner: m[1], repo: m[2], ref: m[3] };
|
|
13325
14585
|
}
|
|
13326
14586
|
function buildKbGetArgs(src, path2) {
|
|
13327
|
-
const
|
|
13328
|
-
return ["api", `repos/${src.owner}/${src.repo}/contents/${
|
|
14587
|
+
const clean4 = path2.replace(/^\/+/, "");
|
|
14588
|
+
return ["api", `repos/${src.owner}/${src.repo}/contents/${clean4}?ref=${src.ref}`, "-H", "Accept: application/vnd.github.raw"];
|
|
13329
14589
|
}
|
|
13330
14590
|
function buildKbTreeArgs(src) {
|
|
13331
14591
|
return ["api", `repos/${src.owner}/${src.repo}/git/trees/${src.ref}?recursive=1`];
|
|
@@ -13342,10 +14602,10 @@ function parseKbTree(stdout, prefix) {
|
|
|
13342
14602
|
}
|
|
13343
14603
|
|
|
13344
14604
|
// src/plan.ts
|
|
13345
|
-
var
|
|
14605
|
+
var import_node_path15 = require("node:path");
|
|
13346
14606
|
var PLANS_DIR = "plans";
|
|
13347
|
-
var META_FILE = (0,
|
|
13348
|
-
var planPath = (slug) => (0,
|
|
14607
|
+
var META_FILE = (0, import_node_path15.join)(PLANS_DIR, ".plan-meta.json");
|
|
14608
|
+
var planPath = (slug) => (0, import_node_path15.join)(PLANS_DIR, `${slug}.md`);
|
|
13349
14609
|
var metaKey = (project2, slug) => `${project2}/${slug}`;
|
|
13350
14610
|
function parseMeta(raw) {
|
|
13351
14611
|
if (!raw) return {};
|
|
@@ -13370,7 +14630,7 @@ function hashContent(s) {
|
|
|
13370
14630
|
function staleHint(slug) {
|
|
13371
14631
|
return `remote "${slug}" is newer \u2014 run \`mmi-cli northstar pull ${slug}\` first (your local is based on an older version), or re-push with \`--force\` to overwrite`;
|
|
13372
14632
|
}
|
|
13373
|
-
var INDEX_FILE = (0,
|
|
14633
|
+
var INDEX_FILE = (0, import_node_path15.join)(PLANS_DIR, ".index.json");
|
|
13374
14634
|
var INDEX_TTL_MS = 6e4;
|
|
13375
14635
|
function parseIndex(raw) {
|
|
13376
14636
|
if (!raw) return null;
|
|
@@ -13399,7 +14659,7 @@ function mergeIndex(idx, scope, plans, now) {
|
|
|
13399
14659
|
const mergedScope = idx.scope === null ? null : [.../* @__PURE__ */ new Set([...idx.scope, ...scope])];
|
|
13400
14660
|
return { fetchedAt: now, scope: mergedScope, plans: [...kept, ...plans] };
|
|
13401
14661
|
}
|
|
13402
|
-
var QUEUE_FILE = (0,
|
|
14662
|
+
var QUEUE_FILE = (0, import_node_path15.join)(PLANS_DIR, ".sync-queue.json");
|
|
13403
14663
|
var QUEUE_MAX_ATTEMPTS = 10;
|
|
13404
14664
|
function isValidQueueEntry(e) {
|
|
13405
14665
|
if (!e || typeof e !== "object") return false;
|
|
@@ -13848,11 +15108,11 @@ async function planGraduate(deps, slug, opts = {}) {
|
|
|
13848
15108
|
}
|
|
13849
15109
|
|
|
13850
15110
|
// src/atomic-write.ts
|
|
13851
|
-
var
|
|
15111
|
+
var import_node_fs17 = require("node:fs");
|
|
13852
15112
|
function atomicWriteFileSync(path2, content) {
|
|
13853
15113
|
const tmp = `${path2}.${process.pid}.tmp`;
|
|
13854
|
-
(0,
|
|
13855
|
-
(0,
|
|
15114
|
+
(0, import_node_fs17.writeFileSync)(tmp, content, "utf8");
|
|
15115
|
+
(0, import_node_fs17.renameSync)(tmp, path2);
|
|
13856
15116
|
}
|
|
13857
15117
|
|
|
13858
15118
|
// src/oauth.ts
|
|
@@ -14083,7 +15343,7 @@ async function fetchHubVersionInfo(baseUrl) {
|
|
|
14083
15343
|
}
|
|
14084
15344
|
function readRepoVersion() {
|
|
14085
15345
|
try {
|
|
14086
|
-
return JSON.parse((0,
|
|
15346
|
+
return JSON.parse((0, import_node_fs18.readFileSync)((0, import_node_path16.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
14087
15347
|
} catch {
|
|
14088
15348
|
return void 0;
|
|
14089
15349
|
}
|
|
@@ -14158,6 +15418,22 @@ async function applyClaudePluginHeal(surface, log) {
|
|
|
14158
15418
|
}
|
|
14159
15419
|
return true;
|
|
14160
15420
|
}
|
|
15421
|
+
async function runCodexPlugin(args) {
|
|
15422
|
+
try {
|
|
15423
|
+
await runHostBin("codex", args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
|
|
15424
|
+
return true;
|
|
15425
|
+
} catch {
|
|
15426
|
+
return false;
|
|
15427
|
+
}
|
|
15428
|
+
}
|
|
15429
|
+
async function applyCodexPluginHeal(surface, log) {
|
|
15430
|
+
if (surface !== "codex") return false;
|
|
15431
|
+
log(" \u21BB reinstalling the MMI plugin via `codex plugin` (marketplace remove \u2192 add --ref main \u2192 add)\u2026");
|
|
15432
|
+
for (const step of CODEX_PLUGIN_HEAL_STEPS) {
|
|
15433
|
+
if (healStepAborts(step, await runCodexPlugin([...step.args]))) return false;
|
|
15434
|
+
}
|
|
15435
|
+
return true;
|
|
15436
|
+
}
|
|
14161
15437
|
var program2 = new Command();
|
|
14162
15438
|
program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, saga, KB. The engine the plugin SessionStart hook drives.").version(resolveClientVersion());
|
|
14163
15439
|
async function runRulesSync(opts, io = consoleIo) {
|
|
@@ -14193,10 +15469,10 @@ async function runRulesSync(opts, io = consoleIo) {
|
|
|
14193
15469
|
for (const entry of fetched) {
|
|
14194
15470
|
if ("error" in entry) continue;
|
|
14195
15471
|
const { file, source } = entry;
|
|
14196
|
-
const current = (0,
|
|
15472
|
+
const current = (0, import_node_fs18.existsSync)(file) ? await (0, import_promises5.readFile)(file, "utf8") : null;
|
|
14197
15473
|
if (needsUpdate(source, current)) {
|
|
14198
15474
|
const slash = file.lastIndexOf("/");
|
|
14199
|
-
if (slash > 0) (0,
|
|
15475
|
+
if (slash > 0) (0, import_node_fs18.mkdirSync)(file.slice(0, slash), { recursive: true });
|
|
14200
15476
|
await (0, import_promises5.writeFile)(file, normalizeEol(source), "utf8");
|
|
14201
15477
|
changed++;
|
|
14202
15478
|
if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
|
|
@@ -14222,7 +15498,7 @@ async function runDocsSync(opts, io = consoleIo) {
|
|
|
14222
15498
|
return null;
|
|
14223
15499
|
}
|
|
14224
15500
|
},
|
|
14225
|
-
localContent: async (f) => (0,
|
|
15501
|
+
localContent: async (f) => (0, import_node_fs18.existsSync)(f) ? await (0, import_promises5.readFile)(f, "utf8") : null,
|
|
14226
15502
|
writeDoc: async (f, c) => {
|
|
14227
15503
|
await (0, import_promises5.writeFile)(f, c, "utf8");
|
|
14228
15504
|
}
|
|
@@ -14234,6 +15510,7 @@ async function runDocsSync(opts, io = consoleIo) {
|
|
|
14234
15510
|
var docs = program2.command("docs").description("repo-owned authoritative docs");
|
|
14235
15511
|
docs.command("sync").option("--quiet", "stay silent unless something changed or errored").description("refresh README.md / architecture.md from the repo default branch (keeper-authored); never clobbers uncommitted edits").action((opts) => runDocsSync(opts));
|
|
14236
15512
|
registerSagaCommands(program2);
|
|
15513
|
+
registerHandoffCommands(program2);
|
|
14237
15514
|
registerHonchoCommands(program2);
|
|
14238
15515
|
async function runWhoami(io = consoleIo) {
|
|
14239
15516
|
const cfg = await loadConfig();
|
|
@@ -14247,8 +15524,18 @@ async function runWhoami(io = consoleIo) {
|
|
|
14247
15524
|
program2.command("whoami").description('resolve the logged-in human: {login, source, sessionExpiresAt} JSON; source "unknown" (exit 0) when neither the Hub session nor gh can name them').option("--json", "machine-readable output (default)").action(async () => {
|
|
14248
15525
|
await runWhoami();
|
|
14249
15526
|
});
|
|
14250
|
-
program2.command("gc").description("dry-run cleanup for merged/closed PR branches and stale tracking refs").option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR branches and stale tracking refs").option("--json", "machine-readable output").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to inspect per state", "200").action(async (o) => {
|
|
15527
|
+
program2.command("gc").description("dry-run cleanup for merged/closed PR branches and stale tracking refs (--scratch: stale local saga/plan/honcho files)").option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR branches and stale tracking refs").option("--json", "machine-readable output").option("--scratch", "prune stale local saga/plan/honcho scratch already recorded remotely (#1474) instead of git branches/refs").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to inspect per state", "200").action(async (o) => {
|
|
14251
15528
|
if (o.apply && o.dryRun) return fail("gc: choose either --dry-run or --apply");
|
|
15529
|
+
if (o.scratch) {
|
|
15530
|
+
try {
|
|
15531
|
+
const root = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
15532
|
+
const run = executeScratchGc(root, { apply: Boolean(o.apply) });
|
|
15533
|
+
if (o.json) return console.log(JSON.stringify({ dryRun: !o.apply, ...run }, null, 2));
|
|
15534
|
+
return console.log(formatScratchGcPlan(run.plan, Boolean(o.apply)));
|
|
15535
|
+
} catch (e) {
|
|
15536
|
+
return fail(`gc --scratch: ${e.message}`);
|
|
15537
|
+
}
|
|
15538
|
+
}
|
|
14252
15539
|
const limit = Number.parseInt(o.limit, 10);
|
|
14253
15540
|
if (!Number.isFinite(limit) || limit < 1) return fail("gc: --limit must be a positive integer");
|
|
14254
15541
|
try {
|
|
@@ -14276,6 +15563,120 @@ ${renderGcApplyResult(applyResult)}`);
|
|
|
14276
15563
|
fail(`gc: ${e.message}`);
|
|
14277
15564
|
}
|
|
14278
15565
|
});
|
|
15566
|
+
var NPM_PROVISION_TIMEOUT_MS = 3e5;
|
|
15567
|
+
var WORKTREE_SETUP_LOCK_TTL_MS = 10 * 6e4;
|
|
15568
|
+
function runWorktreeInstall(command, cwd, quiet) {
|
|
15569
|
+
const [bin, ...args] = command.split(" ");
|
|
15570
|
+
const file = isWin ? "cmd.exe" : bin;
|
|
15571
|
+
const spawnArgs = isWin ? ["/c", bin, ...args] : args;
|
|
15572
|
+
return new Promise((resolve, reject) => {
|
|
15573
|
+
const child = (0, import_node_child_process10.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
|
|
15574
|
+
const timer = setTimeout(() => {
|
|
15575
|
+
try {
|
|
15576
|
+
child.kill();
|
|
15577
|
+
} catch {
|
|
15578
|
+
}
|
|
15579
|
+
reject(new Error(`${command} timed out after ${NPM_PROVISION_TIMEOUT_MS}ms in ${cwd}`));
|
|
15580
|
+
}, NPM_PROVISION_TIMEOUT_MS);
|
|
15581
|
+
child.on("error", (e) => {
|
|
15582
|
+
clearTimeout(timer);
|
|
15583
|
+
reject(e);
|
|
15584
|
+
});
|
|
15585
|
+
child.on("exit", (code) => {
|
|
15586
|
+
clearTimeout(timer);
|
|
15587
|
+
if (code === 0) resolve();
|
|
15588
|
+
else reject(new Error(`${command} exited ${code} in ${cwd}`));
|
|
15589
|
+
});
|
|
15590
|
+
});
|
|
15591
|
+
}
|
|
15592
|
+
async function primaryCheckoutRoot(worktreeRoot) {
|
|
15593
|
+
try {
|
|
15594
|
+
const out = (await execFileP2("git", ["-C", worktreeRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
|
|
15595
|
+
return out ? (0, import_node_path16.dirname)(out) : void 0;
|
|
15596
|
+
} catch {
|
|
15597
|
+
return void 0;
|
|
15598
|
+
}
|
|
15599
|
+
}
|
|
15600
|
+
function makeProvisionDeps(worktreeRoot, quiet, log) {
|
|
15601
|
+
return {
|
|
15602
|
+
runInstall: (command, cwd) => runWorktreeInstall(command, cwd, quiet),
|
|
15603
|
+
primaryCheckout: () => primaryCheckoutRoot(worktreeRoot),
|
|
15604
|
+
log
|
|
15605
|
+
};
|
|
15606
|
+
}
|
|
15607
|
+
function acquireWorktreeSetupLock(worktreeRoot) {
|
|
15608
|
+
const lockPath = (0, import_node_path16.join)(worktreeRoot, ".mmi", "worktree-setup.lock");
|
|
15609
|
+
const take = () => {
|
|
15610
|
+
const fd = (0, import_node_fs18.openSync)(lockPath, "wx");
|
|
15611
|
+
try {
|
|
15612
|
+
(0, import_node_fs18.writeSync)(fd, String(Date.now()));
|
|
15613
|
+
} finally {
|
|
15614
|
+
(0, import_node_fs18.closeSync)(fd);
|
|
15615
|
+
}
|
|
15616
|
+
return () => {
|
|
15617
|
+
try {
|
|
15618
|
+
(0, import_node_fs18.rmSync)(lockPath, { force: true });
|
|
15619
|
+
} catch {
|
|
15620
|
+
}
|
|
15621
|
+
};
|
|
15622
|
+
};
|
|
15623
|
+
try {
|
|
15624
|
+
(0, import_node_fs18.mkdirSync)((0, import_node_path16.dirname)(lockPath), { recursive: true });
|
|
15625
|
+
return take();
|
|
15626
|
+
} catch {
|
|
15627
|
+
try {
|
|
15628
|
+
if (Date.now() - (0, import_node_fs18.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
|
|
15629
|
+
(0, import_node_fs18.rmSync)(lockPath, { force: true });
|
|
15630
|
+
return take();
|
|
15631
|
+
}
|
|
15632
|
+
} catch {
|
|
15633
|
+
}
|
|
15634
|
+
return null;
|
|
15635
|
+
}
|
|
15636
|
+
}
|
|
15637
|
+
var worktree = program2.command("worktree").description("self-provisioning worktrees \u2014 install deps + copy local-only config");
|
|
15638
|
+
worktree.command("create <branch>").description("create a worktree from a base ref and provision it (install deps + copy local-only config)").option("--from <ref>", "base ref to branch from", "origin/development").option("--path <path>", "worktree path (default: ../mmi-worktrees/<branch>)").option("--remote <name>", "remote to fetch the base from", "origin").option("--json", "machine-readable output").action(async (branch, o) => {
|
|
15639
|
+
try {
|
|
15640
|
+
const repoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
15641
|
+
const wtPath = o.path ?? defaultWorktreePath(repoRoot, branch);
|
|
15642
|
+
const baseBranch = o.from.startsWith(`${o.remote}/`) ? o.from.slice(o.remote.length + 1) : void 0;
|
|
15643
|
+
if (baseBranch) await execFileP2("git", ["fetch", o.remote, baseBranch], { timeout: GH_MUTATION_TIMEOUT_MS }).catch(() => void 0);
|
|
15644
|
+
await execFileP2("git", ["worktree", "add", wtPath, "-b", branch, o.from], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
15645
|
+
const report = await provisionWorktree(wtPath, makeProvisionDeps(wtPath, Boolean(o.json), (m) => {
|
|
15646
|
+
if (!o.json) console.error(` ${m}`);
|
|
15647
|
+
}));
|
|
15648
|
+
if (o.json) return console.log(JSON.stringify({ branch, path: wtPath, base: o.from, ...report }, null, 2));
|
|
15649
|
+
console.log(`worktree ready: ${wtPath} (branch ${branch} from ${o.from})`);
|
|
15650
|
+
console.log(` installed: ${report.installed.map((i) => i.dir || ".").join(", ") || "none"}`);
|
|
15651
|
+
console.log(` copied: ${report.copied.join(", ") || "none"}`);
|
|
15652
|
+
} catch (e) {
|
|
15653
|
+
fail(`worktree create: ${e.message}`);
|
|
15654
|
+
}
|
|
15655
|
+
});
|
|
15656
|
+
worktree.command("setup [path]").description("provision an existing worktree (install missing deps + copy local-only config); fired automatically by SessionStart").option("--quiet", "no output on success (for the detached SessionStart auto-fire worker)").option("--json", "machine-readable output").action(async (path2, o) => {
|
|
15657
|
+
const root = path2 ?? process.cwd();
|
|
15658
|
+
const release = acquireWorktreeSetupLock(root);
|
|
15659
|
+
if (!release) {
|
|
15660
|
+
if (!o.quiet && !o.json) console.log("worktree setup: another provision is in progress \u2014 skipping");
|
|
15661
|
+
return;
|
|
15662
|
+
}
|
|
15663
|
+
try {
|
|
15664
|
+
const report = await provisionWorktree(root, makeProvisionDeps(root, Boolean(o.quiet), (m) => {
|
|
15665
|
+
if (!o.quiet && !o.json) console.error(` ${m}`);
|
|
15666
|
+
}));
|
|
15667
|
+
if (o.json) return console.log(JSON.stringify({ path: root, ...report }, null, 2));
|
|
15668
|
+
if (!o.quiet) {
|
|
15669
|
+
console.log(`worktree provisioned: ${root}`);
|
|
15670
|
+
console.log(` installed: ${report.installed.map((i) => i.dir || ".").join(", ") || "none"}`);
|
|
15671
|
+
console.log(` copied: ${report.copied.join(", ") || "none"}`);
|
|
15672
|
+
}
|
|
15673
|
+
} catch (e) {
|
|
15674
|
+
if (o.quiet) return void console.error(`[worktree-setup] ${e.message}`);
|
|
15675
|
+
fail(`worktree setup: ${e.message}`);
|
|
15676
|
+
} finally {
|
|
15677
|
+
release();
|
|
15678
|
+
}
|
|
15679
|
+
});
|
|
14279
15680
|
var kb = program2.command("kb").description("org knowledgebase (read-only)");
|
|
14280
15681
|
kb.command("get <path>").description("print a KB document by path").action(async (path2) => {
|
|
14281
15682
|
const src = resolveKbSource((await loadConfig()).kbSource);
|
|
@@ -14395,7 +15796,7 @@ function detachPlanSync() {
|
|
|
14395
15796
|
}
|
|
14396
15797
|
}
|
|
14397
15798
|
function makePlanDeps(cfg, io = consoleIo) {
|
|
14398
|
-
const ensureDir = () => (0,
|
|
15799
|
+
const ensureDir = () => (0, import_node_fs18.mkdirSync)(PLANS_DIR, { recursive: true });
|
|
14399
15800
|
return {
|
|
14400
15801
|
apiUrl: cfg.sagaApiUrl,
|
|
14401
15802
|
fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
|
|
@@ -14403,24 +15804,24 @@ function makePlanDeps(cfg, io = consoleIo) {
|
|
|
14403
15804
|
project: async () => (await sagaKey(cfg)).project,
|
|
14404
15805
|
readLocal: (slug) => {
|
|
14405
15806
|
try {
|
|
14406
|
-
return (0,
|
|
15807
|
+
return (0, import_node_fs18.readFileSync)(planPath(slug), "utf8");
|
|
14407
15808
|
} catch {
|
|
14408
15809
|
return null;
|
|
14409
15810
|
}
|
|
14410
15811
|
},
|
|
14411
15812
|
writeLocal: (slug, content) => {
|
|
14412
15813
|
ensureDir();
|
|
14413
|
-
(0,
|
|
15814
|
+
(0, import_node_fs18.writeFileSync)(planPath(slug), content, "utf8");
|
|
14414
15815
|
},
|
|
14415
15816
|
removeLocal: (slug) => {
|
|
14416
15817
|
try {
|
|
14417
|
-
(0,
|
|
15818
|
+
(0, import_node_fs18.rmSync)(planPath(slug));
|
|
14418
15819
|
} catch {
|
|
14419
15820
|
}
|
|
14420
15821
|
},
|
|
14421
15822
|
readMetaRaw: () => {
|
|
14422
15823
|
try {
|
|
14423
|
-
return (0,
|
|
15824
|
+
return (0, import_node_fs18.readFileSync)(META_FILE, "utf8");
|
|
14424
15825
|
} catch {
|
|
14425
15826
|
return null;
|
|
14426
15827
|
}
|
|
@@ -14431,7 +15832,7 @@ function makePlanDeps(cfg, io = consoleIo) {
|
|
|
14431
15832
|
},
|
|
14432
15833
|
readIndexRaw: () => {
|
|
14433
15834
|
try {
|
|
14434
|
-
return (0,
|
|
15835
|
+
return (0, import_node_fs18.readFileSync)(INDEX_FILE, "utf8");
|
|
14435
15836
|
} catch {
|
|
14436
15837
|
return null;
|
|
14437
15838
|
}
|
|
@@ -14442,7 +15843,7 @@ function makePlanDeps(cfg, io = consoleIo) {
|
|
|
14442
15843
|
},
|
|
14443
15844
|
readQueueRaw: () => {
|
|
14444
15845
|
try {
|
|
14445
|
-
return (0,
|
|
15846
|
+
return (0, import_node_fs18.readFileSync)(QUEUE_FILE, "utf8");
|
|
14446
15847
|
} catch {
|
|
14447
15848
|
return null;
|
|
14448
15849
|
}
|
|
@@ -14632,6 +16033,20 @@ secrets.command("edit <key>").description("alias for set \u2014 replace a secret
|
|
|
14632
16033
|
const ok = await secretsEdit(d, key, o);
|
|
14633
16034
|
if (!ok) process.exitCode = 1;
|
|
14634
16035
|
}));
|
|
16036
|
+
secrets.command("copy").description("copy provider keys between vault tiers (audit-logged; org-tier source is master-gated)").requiredOption("--from <stage>", "source tier: dev, rc, or main").requiredOption("--to <stage>", "destination tier: dev, rc, or main").requiredOption("--keys <names>", "comma-separated secret names (encryption keys blocked)").option("--dry-run", "report copies without writing").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets(async (d) => {
|
|
16037
|
+
const stages = ["dev", "rc", "main"];
|
|
16038
|
+
if (!stages.includes(o.from) || !stages.includes(o.to)) {
|
|
16039
|
+
return fail("secrets copy: --from and --to must be dev, rc, or main");
|
|
16040
|
+
}
|
|
16041
|
+
const ok = await secretsCopy(d, {
|
|
16042
|
+
repo: o.repo,
|
|
16043
|
+
from: o.from,
|
|
16044
|
+
to: o.to,
|
|
16045
|
+
keys: o.keys.split(","),
|
|
16046
|
+
dryRun: o.dryRun
|
|
16047
|
+
});
|
|
16048
|
+
if (!ok) process.exitCode = 1;
|
|
16049
|
+
}));
|
|
14635
16050
|
secrets.command("rm <key>").description("remove a secret (project tier self-serve; org tier needs a grant)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
|
|
14636
16051
|
secrets.command("use <key>").description("print guidance on consuming a secret without committing it (no value)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsUse(d, key, o)));
|
|
14637
16052
|
secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to a specific org-tier secret").action((repo, login, key) => withSecrets((d) => secretsGrant(d, repo, login, key, {})));
|
|
@@ -14997,7 +16412,7 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
|
|
|
14997
16412
|
if (mismatch) process.exitCode = 1;
|
|
14998
16413
|
});
|
|
14999
16414
|
var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
|
|
15000
|
-
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)").option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file, or from stdin with -").
|
|
16415
|
+
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)").option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment").action(async (o) => {
|
|
15001
16416
|
let args;
|
|
15002
16417
|
let priority;
|
|
15003
16418
|
let body;
|
|
@@ -15005,6 +16420,7 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
|
|
|
15005
16420
|
try {
|
|
15006
16421
|
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises5.readFile, readStdin });
|
|
15007
16422
|
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises5.readFile, readStdin });
|
|
16423
|
+
if (o.priority === void 0) throw new Error("missing --priority <priority> \u2014 expected one of: urgent, high, medium, low");
|
|
15008
16424
|
priority = normalizePriority(o.priority);
|
|
15009
16425
|
args = buildIssueArgs({ type: o.type, title, body, priority, repo: o.repo, labels: o.label });
|
|
15010
16426
|
if (o.parent !== void 0) parseIssueRef(o.parent);
|
|
@@ -15082,12 +16498,12 @@ issue.command("link-child <parent> <child>").description("link an existing issue
|
|
|
15082
16498
|
return fail(`issue link-child: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
15083
16499
|
}
|
|
15084
16500
|
});
|
|
15085
|
-
program2.command("report").description("file a friction report on the Hub board (GitHub auth, dedups open reports) and print {number,url} JSON").option("--title <title>", "one-line friction summary").option("--title-file <path|->", "read the friction summary from a UTF-8 file, or from stdin with -").option("--body <body>", "report body (markdown)").option("--body-file <path|->", "read report body from a UTF-8 file, or from stdin with -").option("--type <type>", "bug | feature | task (sets the matching label)", "task").option("--priority <priority>", "urgent | high | medium | low (board Priority field
|
|
16501
|
+
program2.command("report").description("file a friction report on the Hub board (GitHub auth, dedups open reports) and print {number,url} JSON").option("--title <title>", "one-line friction summary").option("--title-file <path|->", "read the friction summary from a UTF-8 file, or from stdin with -").option("--body <body>", "report body (markdown)").option("--body-file <path|->", "read report body from a UTF-8 file, or from stdin with -").option("--type <type>", "bug | feature | task (sets the matching label)", "task").option("--priority <priority>", "urgent | high | medium | low (sets the board Priority field only, #416)", "medium").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO2})`).option("--force", "file a new issue even when an open report looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 report always prints JSON; #682)").action(async (o) => {
|
|
15086
16502
|
let body;
|
|
15087
16503
|
let priority;
|
|
15088
16504
|
let args;
|
|
15089
16505
|
let title;
|
|
15090
|
-
const targetRepo2 = o.repo ??
|
|
16506
|
+
const targetRepo2 = o.repo ?? HUB_REPO2;
|
|
15091
16507
|
const sourceRepo = await resolveRepo(void 0);
|
|
15092
16508
|
try {
|
|
15093
16509
|
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises5.readFile, readStdin });
|
|
@@ -15282,8 +16698,8 @@ grindCmd.command("estimate").description("Worst-case cost proxy (agent-call unit
|
|
|
15282
16698
|
console.log(`ceiling: ${GRIND_COST_CEILING} units \u2014 ${estimate.exceedsCeiling ? "EXCEEDS \u2192 ask human (cap/stuck path)" : "within"}`);
|
|
15283
16699
|
}
|
|
15284
16700
|
});
|
|
15285
|
-
program2.command("skill-lesson").description("file a skill-lesson on the Hub board (GitHub auth, dedups open lessons) and print {number,url} JSON").requiredOption("--skill <name>", `which skill misfired (${SKILL_NAMES.join(" | ")})`).option("--title <title>", "one-line summary of what misfired").option("--title-file <path|->", "read the one-line summary from a UTF-8 file, or from stdin with -").option("--body <body>", "lesson body: what misfired, the evidence, and the proposed amendment (markdown)").option("--body-file <path|->", "read the lesson body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (board Priority field
|
|
15286
|
-
const targetRepo2 = o.repo ??
|
|
16701
|
+
program2.command("skill-lesson").description("file a skill-lesson on the Hub board (GitHub auth, dedups open lessons) and print {number,url} JSON").requiredOption("--skill <name>", `which skill misfired (${SKILL_NAMES.join(" | ")})`).option("--title <title>", "one-line summary of what misfired").option("--title-file <path|->", "read the one-line summary from a UTF-8 file, or from stdin with -").option("--body <body>", "lesson body: what misfired, the evidence, and the proposed amendment (markdown)").option("--body-file <path|->", "read the lesson body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (sets the board Priority field only, #416)", "medium").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO2})`).option("--force", "file a new issue even when an open lesson looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 skill-lesson always prints JSON)").action(async (o) => {
|
|
16702
|
+
const targetRepo2 = o.repo ?? HUB_REPO2;
|
|
15287
16703
|
const sourceRepo = await resolveRepo(void 0);
|
|
15288
16704
|
const pluginSha = await resolvePluginSha();
|
|
15289
16705
|
let skill;
|
|
@@ -15354,6 +16770,111 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
|
|
|
15354
16770
|
const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo }));
|
|
15355
16771
|
console.log(JSON.stringify(created));
|
|
15356
16772
|
});
|
|
16773
|
+
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
16774
|
+
const wfDir = (0, import_node_path16.join)(cwd, ".github", "workflows");
|
|
16775
|
+
if (!(0, import_node_fs18.existsSync)(wfDir)) return [];
|
|
16776
|
+
return (0, import_node_fs18.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
|
|
16777
|
+
}
|
|
16778
|
+
async function resolveMergeCiPolicyForCheckout(repoOpt) {
|
|
16779
|
+
const repo = repoOpt ?? await resolveRepo();
|
|
16780
|
+
if (repo) {
|
|
16781
|
+
return resolveRepoMergeCiPolicy(repo, ciAuditDeps());
|
|
16782
|
+
}
|
|
16783
|
+
const workflowPaths = await listCiWorkflowPaths();
|
|
16784
|
+
return resolveMergeCiPolicy({ workflowPaths });
|
|
16785
|
+
}
|
|
16786
|
+
function ciAuditDeps() {
|
|
16787
|
+
const cfgPromise = loadConfig();
|
|
16788
|
+
return {
|
|
16789
|
+
client: defaultGitHubClient(),
|
|
16790
|
+
listProjects: async () => fetchProjectsList(registryClientDeps(await cfgPromise)),
|
|
16791
|
+
getProjectMeta: async (slug) => fetchProjectBySlug(slug, registryClientDeps(await cfgPromise))
|
|
16792
|
+
};
|
|
16793
|
+
}
|
|
16794
|
+
pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs no-ci (for grind/build agents)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").action(async (o) => {
|
|
16795
|
+
const result = await resolveMergeCiPolicyForCheckout(o.repo);
|
|
16796
|
+
if (o.json) return printLine(JSON.stringify(result));
|
|
16797
|
+
printLine(`merge CI policy: ${result.policy} (${result.reason})`);
|
|
16798
|
+
});
|
|
16799
|
+
pr.command("checks-wait <number>").description("bounded wait for PR checks; skips immediately on no-ci repos (#1432)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (number, o) => {
|
|
16800
|
+
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
16801
|
+
const result = await waitForPrChecks({
|
|
16802
|
+
resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
|
|
16803
|
+
pollChecks: async () => {
|
|
16804
|
+
const { stdout } = await execFileP2("gh", ["pr", "checks", number, ...repoArgs], { timeout: GC_GH_TIMEOUT_MS });
|
|
16805
|
+
return parseGhPrChecksOutput(stdout);
|
|
16806
|
+
},
|
|
16807
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
16808
|
+
});
|
|
16809
|
+
if (o.json) printLine(JSON.stringify(result));
|
|
16810
|
+
else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
|
|
16811
|
+
if (result.status === "failure" || result.status === "timeout") process.exitCode = 1;
|
|
16812
|
+
});
|
|
16813
|
+
pr.command("land <number>").description("agent merge path (#1440): train probe \u2192 checks-wait \u2192 merge --auto \u2192 poll enqueued \u2014 development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").action(async (number, o) => {
|
|
16814
|
+
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
16815
|
+
const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false }, {
|
|
16816
|
+
resolveRepo: async (prNumber, repoOpt) => {
|
|
16817
|
+
if (repoOpt) return repoOpt;
|
|
16818
|
+
const viewed = (await execFileP2("gh", ["pr", "view", prNumber, ...repoArgs, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim();
|
|
16819
|
+
const [repo, base2] = viewed.split(/\s+/);
|
|
16820
|
+
if (base2 && base2 !== "development") {
|
|
16821
|
+
throw new Error(`pr land: base branch must be development (got ${base2}) \u2014 promotion merges stay human-only`);
|
|
16822
|
+
}
|
|
16823
|
+
if (!repo) throw new Error("pr land: could not resolve PR repo");
|
|
16824
|
+
return repo;
|
|
16825
|
+
},
|
|
16826
|
+
fetchTrainAuthority: async (repo) => fetchTrainAuthority(repo, registryClientDeps(await loadConfig())),
|
|
16827
|
+
resolveCiPolicy: (repo) => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
|
|
16828
|
+
waitForChecks: (prNumber, repo) => waitForPrChecks({
|
|
16829
|
+
resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
|
|
16830
|
+
pollChecks: async () => {
|
|
16831
|
+
const args = repo ? ["--repo", repo] : [];
|
|
16832
|
+
const { stdout } = await execFileP2("gh", ["pr", "checks", prNumber, ...args], { timeout: GC_GH_TIMEOUT_MS });
|
|
16833
|
+
return parseGhPrChecksOutput(stdout);
|
|
16834
|
+
},
|
|
16835
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
16836
|
+
}),
|
|
16837
|
+
mergeAuto: async (prNumber, repo) => {
|
|
16838
|
+
const args = repo ? ["--repo", repo] : [];
|
|
16839
|
+
try {
|
|
16840
|
+
await execFileP2("gh", buildPrMergeArgs({ number: prNumber, repoArgs: args, method: "--squash", auto: true }), { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
16841
|
+
} catch (e) {
|
|
16842
|
+
const message = String(e.message || "");
|
|
16843
|
+
if (/already been merged/i.test(message)) return { mergeStatus: "merged" };
|
|
16844
|
+
const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
|
|
16845
|
+
if (note) return { mergeStatus: "failed", error: note };
|
|
16846
|
+
if (!basePolicyBlocksImmediateMerge(message)) {
|
|
16847
|
+
return { mergeStatus: "failed", error: message.split("\n")[0] };
|
|
16848
|
+
}
|
|
16849
|
+
return { mergeStatus: "failed", error: `merge blocked: ${message.split("\n")[0]} \u2014 ensure checks are green` };
|
|
16850
|
+
}
|
|
16851
|
+
const state = (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
16852
|
+
return { mergeStatus: state === "MERGED" ? "merged" : "auto-merge-enqueued" };
|
|
16853
|
+
},
|
|
16854
|
+
pollMerged: async (prNumber, repo, deadlineMs) => {
|
|
16855
|
+
const args = repo ? ["--repo", repo] : [];
|
|
16856
|
+
while (Date.now() < deadlineMs) {
|
|
16857
|
+
const state = (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
16858
|
+
if (state === "MERGED") return true;
|
|
16859
|
+
await new Promise((resolve) => setTimeout(resolve, PR_LAND_POLL_MS));
|
|
16860
|
+
}
|
|
16861
|
+
return false;
|
|
16862
|
+
}
|
|
16863
|
+
});
|
|
16864
|
+
if (o.json) printLine(JSON.stringify(result));
|
|
16865
|
+
else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
|
|
16866
|
+
if (result.status === "failed") process.exitCode = 1;
|
|
16867
|
+
else {
|
|
16868
|
+
await execFileP2(process.execPath, [
|
|
16869
|
+
process.argv[1],
|
|
16870
|
+
"pr",
|
|
16871
|
+
"merge",
|
|
16872
|
+
number,
|
|
16873
|
+
...o.repo ? ["--repo", o.repo] : [],
|
|
16874
|
+
"--squash"
|
|
16875
|
+
], { timeout: GH_MUTATION_TIMEOUT_MS }).catch(() => void 0);
|
|
16876
|
+
}
|
|
16877
|
+
});
|
|
15357
16878
|
async function remoteBranchExists2(branch, options = {}) {
|
|
15358
16879
|
return checkRemoteBranchExists(branch, {
|
|
15359
16880
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout
|
|
@@ -15362,7 +16883,7 @@ async function remoteBranchExists2(branch, options = {}) {
|
|
|
15362
16883
|
var COMPOSE_TIMEOUT_MS = 12e4;
|
|
15363
16884
|
async function createDeferredWorktreeStore() {
|
|
15364
16885
|
try {
|
|
15365
|
-
const { stdout } = await execFileP2("git", ["rev-parse", "--git-dir"], { timeout: GIT_TIMEOUT_MS });
|
|
16886
|
+
const { stdout } = await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS });
|
|
15366
16887
|
const registryPath = deferredWorktreesRegistryPath(stdout.trim());
|
|
15367
16888
|
return {
|
|
15368
16889
|
read: async () => {
|
|
@@ -15374,7 +16895,7 @@ async function createDeferredWorktreeStore() {
|
|
|
15374
16895
|
},
|
|
15375
16896
|
write: async (entries) => {
|
|
15376
16897
|
try {
|
|
15377
|
-
await (0, import_promises5.mkdir)((0,
|
|
16898
|
+
await (0, import_promises5.mkdir)((0, import_node_path16.dirname)(registryPath), { recursive: true });
|
|
15378
16899
|
await (0, import_promises5.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
|
|
15379
16900
|
} catch {
|
|
15380
16901
|
}
|
|
@@ -15393,7 +16914,7 @@ function worktreeRemoveDeps(execGit) {
|
|
|
15393
16914
|
}
|
|
15394
16915
|
function teardownWorktreeStage(worktreePath) {
|
|
15395
16916
|
return runWorktreeStageTeardown(worktreePath, {
|
|
15396
|
-
hasStageState: (wt) => (0,
|
|
16917
|
+
hasStageState: (wt) => (0, import_node_fs18.existsSync)(stageStatePath(wt)),
|
|
15397
16918
|
stopRecordedStage: async (wt) => (await stopStage({ cwd: wt })).pid,
|
|
15398
16919
|
listComposeProjects: async () => {
|
|
15399
16920
|
const { stdout } = await execFileP2("docker", ["compose", "ls", "--all", "--format", "json"], { timeout: GC_GH_TIMEOUT_MS });
|
|
@@ -15404,7 +16925,7 @@ function teardownWorktreeStage(worktreePath) {
|
|
|
15404
16925
|
}
|
|
15405
16926
|
});
|
|
15406
16927
|
}
|
|
15407
|
-
pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").action(async (number, o) => {
|
|
16928
|
+
pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch; on no-ci repos run pr ci-policy / checks-wait first (#1432)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").action(async (number, o) => {
|
|
15408
16929
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
15409
16930
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
15410
16931
|
const headRef = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName", "--jq", ".headRefName"], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim();
|
|
@@ -15456,7 +16977,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
15456
16977
|
} : await cleanupPrMergeLocalBranch(headRef, {
|
|
15457
16978
|
beforeWorktrees,
|
|
15458
16979
|
startingPath,
|
|
15459
|
-
pathExists: (p) => (0,
|
|
16980
|
+
pathExists: (p) => (0, import_node_fs18.existsSync)(p),
|
|
15460
16981
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
15461
16982
|
teardownWorktreeStage,
|
|
15462
16983
|
deferredStore,
|
|
@@ -15571,6 +17092,25 @@ board.command("backfill-priority").description("set board Priority from priority
|
|
|
15571
17092
|
return failGraceful(`board backfill-priority failed: ${e.message}`);
|
|
15572
17093
|
}
|
|
15573
17094
|
});
|
|
17095
|
+
board.command("prune-priority-labels").description("remove retired priority:* labels (#416) from issues whose board Priority field is already set").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--dry-run", "report what would be removed without writing").option("--concurrency <n>", "parallel issue edits (default 8)", "8").action(async (o) => {
|
|
17096
|
+
try {
|
|
17097
|
+
const result = await prunePriorityLabels({
|
|
17098
|
+
config: await loadConfigForRepo(o.repo),
|
|
17099
|
+
repo: o.repo,
|
|
17100
|
+
dryRun: o.dryRun,
|
|
17101
|
+
concurrency: Number(o.concurrency) || 8
|
|
17102
|
+
});
|
|
17103
|
+
if (o.json) return console.log(JSON.stringify(result));
|
|
17104
|
+
console.log(
|
|
17105
|
+
`prune-priority-labels: scanned ${result.scanned}, pruned ${result.pruned} (${result.removedLabels} labels), skipped ${result.skippedNoField} (field unset), failed ${result.failed}`
|
|
17106
|
+
);
|
|
17107
|
+
for (const line of result.details.slice(0, 30)) console.log(` ${line}`);
|
|
17108
|
+
if (result.details.length > 30) console.log(` ... +${result.details.length - 30} more`);
|
|
17109
|
+
if (result.failed) process.exitCode = 1;
|
|
17110
|
+
} catch (e) {
|
|
17111
|
+
return failGraceful(`board prune-priority-labels failed: ${e.message}`);
|
|
17112
|
+
}
|
|
17113
|
+
});
|
|
15574
17114
|
board.command("done <issue>").description("set a board item's Status to Done (does not close the GitHub issue; use `gh issue close`)").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--allow-partial", "return success JSON if the item resolves but the status move fails").action(async (issueRef, o) => {
|
|
15575
17115
|
try {
|
|
15576
17116
|
const result = await moveBoardItem({ config: await loadConfigOrDiscover(), selector: issueRef, status: "Done", repo: o.repo, allowPartial: o.allowPartial });
|
|
@@ -15609,7 +17149,7 @@ function rawValues(flag) {
|
|
|
15609
17149
|
return out;
|
|
15610
17150
|
}
|
|
15611
17151
|
function printLine(value) {
|
|
15612
|
-
(0,
|
|
17152
|
+
(0, import_node_fs18.writeSync)(1, `${value}
|
|
15613
17153
|
`);
|
|
15614
17154
|
}
|
|
15615
17155
|
function stageKeepAlive() {
|
|
@@ -15626,10 +17166,26 @@ async function resolveStage() {
|
|
|
15626
17166
|
local,
|
|
15627
17167
|
shell: shellFor(),
|
|
15628
17168
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
15629
|
-
hasCompose: (0,
|
|
15630
|
-
hasEnvExample: (0,
|
|
17169
|
+
hasCompose: (0, import_node_fs18.existsSync)((0, import_node_path16.join)(process.cwd(), "docker-compose.yml")),
|
|
17170
|
+
hasEnvExample: (0, import_node_fs18.existsSync)((0, import_node_path16.join)(process.cwd(), ".env.example"))
|
|
15631
17171
|
});
|
|
15632
17172
|
}
|
|
17173
|
+
async function fetchStageVaultEnvMerge() {
|
|
17174
|
+
const cfg = await loadConfig();
|
|
17175
|
+
if (!cfg.sagaApiUrl) return void 0;
|
|
17176
|
+
const read = await fetchProjectBySlugChecked(await repoSlug(), registryClientDeps(cfg)).catch(() => null);
|
|
17177
|
+
if (!read?.ok || !read.project) return void 0;
|
|
17178
|
+
const names = requiredRuntimeSecretNames("dev", read.project.requiredRuntimeSecrets, { includeGoogleOAuth: false });
|
|
17179
|
+
if (!names.length) return void 0;
|
|
17180
|
+
const d = makeSecretsDeps(cfg);
|
|
17181
|
+
const merge = {};
|
|
17182
|
+
for (const name of names) {
|
|
17183
|
+
const key = name.includes("/") ? name : `dev/${name}`;
|
|
17184
|
+
const value = await fetchSecretValue(d, key, {});
|
|
17185
|
+
if (value != null) merge[name.includes("/") ? name.split("/").pop() : name] = value;
|
|
17186
|
+
}
|
|
17187
|
+
return Object.keys(merge).length ? merge : void 0;
|
|
17188
|
+
}
|
|
15633
17189
|
function stageStepsFor(res, stops = true) {
|
|
15634
17190
|
if (res.source === "derived" && res.derived) return derivedStagePlan(res.derived, shellFor(), stops);
|
|
15635
17191
|
if (res.source === "local") return stagePlan(res.config ?? {}, stops);
|
|
@@ -15663,9 +17219,9 @@ program2.command("port-range <repo>").description("assign (idempotently) + print
|
|
|
15663
17219
|
printLine(o.json ? JSON.stringify({ repo, portRange: [start2, end2], source: "meta" }) : `${repo}: stage.portRange [${start2}, ${end2}]`);
|
|
15664
17220
|
return;
|
|
15665
17221
|
}
|
|
15666
|
-
const path2 = (0,
|
|
17222
|
+
const path2 = (0, import_node_path16.join)(process.cwd(), "infra", "port-ranges.json");
|
|
15667
17223
|
const allocate = async (seed) => {
|
|
15668
|
-
const { stdout } = await execFileP2("node", [(0,
|
|
17224
|
+
const { stdout } = await execFileP2("node", [(0, import_node_path16.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
|
|
15669
17225
|
const parsed = JSON.parse(stdout);
|
|
15670
17226
|
if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
|
|
15671
17227
|
return parsed.range;
|
|
@@ -15761,6 +17317,7 @@ stage.command("start").description("start the configured local stage process and
|
|
|
15761
17317
|
}
|
|
15762
17318
|
if (res.source === "none") return failGraceful(`stage start: ${res.gap}`);
|
|
15763
17319
|
const cfg = res.config ?? res.derived.config;
|
|
17320
|
+
const vaultEnvMerge = res.source === "derived" ? await fetchStageVaultEnvMerge() : void 0;
|
|
15764
17321
|
try {
|
|
15765
17322
|
const hold = stageKeepAlive();
|
|
15766
17323
|
let printed = false;
|
|
@@ -15768,6 +17325,7 @@ stage.command("start").description("start the configured local stage process and
|
|
|
15768
17325
|
const result = await startStage(cfg, {
|
|
15769
17326
|
timeoutMs: Number(o.timeoutMs || 6e4),
|
|
15770
17327
|
allowStaleEnv: o.allowStaleEnv,
|
|
17328
|
+
vaultEnvMerge,
|
|
15771
17329
|
onReady: (ready) => {
|
|
15772
17330
|
printed = true;
|
|
15773
17331
|
const reportUrl = reportedStageUrl(res, ready);
|
|
@@ -15795,6 +17353,7 @@ stage.command("run").description("force-stop previous stage, build, start, and h
|
|
|
15795
17353
|
}
|
|
15796
17354
|
if (res.source === "none") return failGraceful(`stage run: ${res.gap}`);
|
|
15797
17355
|
const cfg = res.config ?? res.derived.config;
|
|
17356
|
+
const vaultEnvMerge = res.source === "derived" ? await fetchStageVaultEnvMerge() : void 0;
|
|
15798
17357
|
try {
|
|
15799
17358
|
const hold = stageKeepAlive();
|
|
15800
17359
|
let printed = false;
|
|
@@ -15802,6 +17361,7 @@ stage.command("run").description("force-stop previous stage, build, start, and h
|
|
|
15802
17361
|
const result = await runStage(cfg, {
|
|
15803
17362
|
timeoutMs: Number(o.timeoutMs || 6e4),
|
|
15804
17363
|
allowStaleEnv: o.allowStaleEnv,
|
|
17364
|
+
vaultEnvMerge,
|
|
15805
17365
|
onReady: (ready) => {
|
|
15806
17366
|
const reportUrl = reportedStageUrl(res, ready);
|
|
15807
17367
|
const url = reportUrl ? ` \u2014 ${reportUrl}` : "";
|
|
@@ -15981,6 +17541,39 @@ var hotfixCmd = program2.command("hotfix").description("stepwise hotfix orchestr
|
|
|
15981
17541
|
hotfixCmd.command("start").description("cherry-pick a merged development PR (or SHA) onto hotfix/vX.Y.Z from origin/main, bump the distribution, open the main-base PR").requiredOption("--from <pr#|sha>", "merged development PR number or commit SHA to cherry-pick").option("--json", "machine-readable output").action(async (o) => runHotfixSub("start", () => runHotfixStart(trainApplyDeps(), { from: o.from }), o.json, renderHotfixStart));
|
|
15982
17542
|
hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated summary lines for the Hub Slack announcement (#883)").action(async (version, o) => runHotfixSub("release", () => runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile }), o.json, renderHotfixRelease));
|
|
15983
17543
|
hotfixCmd.command("status [version]").description("derive the full hotfix pipeline state from live git/gh reads and name the exact next subcommand").option("--json", "machine-readable output").action(async (version, o) => runHotfixSub("status", () => runHotfixStatus(trainApplyDeps(), version), o.json, renderHotfixStatus));
|
|
17544
|
+
var ci = program2.command("ci").description("org CI + merge-readiness audit and reconcile");
|
|
17545
|
+
ci.command("audit").description("read-only fleet scan: gate workflow, ruleset contexts, auto-merge, registry META (#1440)").option("--json", "machine-readable output").option("--markdown", "fleet summary table for issue comments").option("--repo <owner/repo>", "audit one repo instead of the full registry").action(async (o) => {
|
|
17546
|
+
const report = await auditOrgCi(ciAuditDeps(), o.repo);
|
|
17547
|
+
if (o.json) console.log(JSON.stringify(report, null, 2));
|
|
17548
|
+
else if (o.markdown) console.log(renderCiAuditMarkdown(report));
|
|
17549
|
+
else console.log(renderCiAuditText(report));
|
|
17550
|
+
if (!report.ok) process.exitCode = 1;
|
|
17551
|
+
});
|
|
17552
|
+
ci.command("reconcile").description("audit + optionally apply merge settings and product ruleset activation (master-admin)").option("--json", "machine-readable output").option("--repo <owner/repo>", "reconcile one repo instead of the full registry").option("--apply", "PATCH merge settings + activate product ruleset when missing (master role required)").action(async (o) => {
|
|
17553
|
+
if (o.apply) {
|
|
17554
|
+
const verdict = await fetchTrainAuthority(HUB_REPO2, registryClientDeps(await loadConfig()));
|
|
17555
|
+
if (!verdict.ok || verdict.authority.role !== "master") {
|
|
17556
|
+
return fail("ci reconcile --apply: master-admin required");
|
|
17557
|
+
}
|
|
17558
|
+
}
|
|
17559
|
+
const deps = ciAuditDeps();
|
|
17560
|
+
const audit = await auditOrgCi(deps, o.repo);
|
|
17561
|
+
const applyResults = o.apply ? await Promise.all(audit.repos.map((r) => applyCiReconcileRepo(r.repo, deps))) : [];
|
|
17562
|
+
const payload = { audit, apply: applyResults };
|
|
17563
|
+
if (o.json) console.log(JSON.stringify(payload, null, 2));
|
|
17564
|
+
else {
|
|
17565
|
+
console.log(renderCiAuditText(audit));
|
|
17566
|
+
if (o.apply) {
|
|
17567
|
+
for (const r of applyResults) {
|
|
17568
|
+
console.log(`
|
|
17569
|
+
${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${r.errors.length ? ` errors=[${r.errors.join("; ")}]` : ""}`);
|
|
17570
|
+
}
|
|
17571
|
+
} else {
|
|
17572
|
+
console.log("\nDry-run \u2014 re-run with --apply to patch merge settings and activate product rulesets (master-admin).");
|
|
17573
|
+
}
|
|
17574
|
+
}
|
|
17575
|
+
if (!audit.ok) process.exitCode = 1;
|
|
17576
|
+
});
|
|
15984
17577
|
var bootstrap = program2.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").option("--class <class>", "deployable | content", "deployable").option("--json", "machine-readable output").option("--apply", "reserved for future bootstrap execution after explicit master-admin approval").action((o) => {
|
|
15985
17578
|
if (!o.repo) return fail("bootstrap: required option --repo <owner/repo> not specified");
|
|
15986
17579
|
if (o.apply) return fail("bootstrap: execution is not implemented yet; use the dry-run plan and the existing /bootstrap skill");
|
|
@@ -15999,7 +17592,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
15999
17592
|
const report = await verifyBootstrap(repo, o.class, {
|
|
16000
17593
|
client: defaultGitHubClient(),
|
|
16001
17594
|
projectMeta: meta,
|
|
16002
|
-
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0,
|
|
17595
|
+
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs18.existsSync)(path2) ? (0, import_node_fs18.readFileSync)(path2, "utf8") : null,
|
|
16003
17596
|
// requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
|
|
16004
17597
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
16005
17598
|
requiredGcpApis: (() => {
|
|
@@ -16042,12 +17635,12 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
16042
17635
|
return fail(`bootstrap apply: ${e.message}`);
|
|
16043
17636
|
}
|
|
16044
17637
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
16045
|
-
if (!(0,
|
|
16046
|
-
const manifest = loadBootstrapSeeds((0,
|
|
17638
|
+
if (!(0, import_node_fs18.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
|
|
17639
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs18.readFileSync)(manifestPath, "utf8"));
|
|
16047
17640
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
16048
17641
|
const slug = parsedRepo.slug;
|
|
16049
17642
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
16050
|
-
const readFile5 = (p) => (0,
|
|
17643
|
+
const readFile5 = (p) => (0, import_node_fs18.existsSync)(p) ? (0, import_node_fs18.readFileSync)(p, "utf8") : null;
|
|
16051
17644
|
const enc2 = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
16052
17645
|
const rawVars = {};
|
|
16053
17646
|
for (const value of rawValues("--var")) {
|
|
@@ -16097,6 +17690,26 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
16097
17690
|
applied.push(`${action.action} ${resolved.target}`);
|
|
16098
17691
|
}
|
|
16099
17692
|
}
|
|
17693
|
+
if (o.execute && o.class === "deployable") {
|
|
17694
|
+
try {
|
|
17695
|
+
await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]);
|
|
17696
|
+
applied.push("merge settings: allow_auto_merge, squash, delete-branch-on-merge");
|
|
17697
|
+
} catch (e) {
|
|
17698
|
+
applied.push(`merge settings (failed: ${e.message})`);
|
|
17699
|
+
}
|
|
17700
|
+
const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
|
|
17701
|
+
if (rulesetSeed) {
|
|
17702
|
+
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile5);
|
|
17703
|
+
if (rulesetContent) {
|
|
17704
|
+
try {
|
|
17705
|
+
const activation = await activateProductRuleset(repo, stripRulesetComment(rulesetContent), defaultGitHubClient());
|
|
17706
|
+
applied.push(`product ruleset: ${activation.action}${activation.detail ? ` (${activation.detail})` : ""}`);
|
|
17707
|
+
} catch (e) {
|
|
17708
|
+
return failGraceful(`bootstrap apply: product ruleset activation failed: ${e.message}`);
|
|
17709
|
+
}
|
|
17710
|
+
}
|
|
17711
|
+
}
|
|
17712
|
+
}
|
|
16100
17713
|
if (o.execute) {
|
|
16101
17714
|
for (const l of manifest.labels) {
|
|
16102
17715
|
try {
|
|
@@ -16150,10 +17763,10 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
16150
17763
|
name: vars.NAME || parsedRepo.name
|
|
16151
17764
|
};
|
|
16152
17765
|
const readHubFile = async (path2) => {
|
|
16153
|
-
const r = await gh(["api", `repos/${
|
|
17766
|
+
const r = await gh(["api", `repos/${HUB_REPO2}/contents/${enc2(path2)}?ref=development`]);
|
|
16154
17767
|
const parsed = JSON.parse(r.stdout);
|
|
16155
17768
|
if (parsed.encoding !== "base64" || typeof parsed.content !== "string" || !parsed.sha) {
|
|
16156
|
-
throw new Error(`could not read ${
|
|
17769
|
+
throw new Error(`could not read ${HUB_REPO2}/${path2}`);
|
|
16157
17770
|
}
|
|
16158
17771
|
return { content: Buffer.from(parsed.content, "base64").toString("utf8"), sha: parsed.sha };
|
|
16159
17772
|
};
|
|
@@ -16165,15 +17778,15 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
16165
17778
|
applied.push(`fanout: already registered (${parsedRepo.name})`);
|
|
16166
17779
|
} else {
|
|
16167
17780
|
const branchName = `bootstrap-register-fanout-${slug}`;
|
|
16168
|
-
const headSha = (await gh(["api", `repos/${
|
|
17781
|
+
const headSha = (await gh(["api", `repos/${HUB_REPO2}/git/ref/heads/development`, "--jq", ".object.sha"])).stdout.trim();
|
|
16169
17782
|
try {
|
|
16170
|
-
await gh(["api", "-X", "POST", `repos/${
|
|
17783
|
+
await gh(["api", "-X", "POST", `repos/${HUB_REPO2}/git/refs`, "-f", `ref=refs/heads/${branchName}`, "-f", `sha=${headSha}`]);
|
|
16171
17784
|
} catch (e) {
|
|
16172
17785
|
if (!/Reference already exists|already exists/i.test(String(e.message ?? ""))) throw e;
|
|
16173
17786
|
}
|
|
16174
17787
|
const branchFileSha = async (path2) => {
|
|
16175
17788
|
try {
|
|
16176
|
-
const r = await gh(["api", `repos/${
|
|
17789
|
+
const r = await gh(["api", `repos/${HUB_REPO2}/contents/${enc2(path2)}?ref=${branchName}`, "--jq", ".sha"]);
|
|
16177
17790
|
return r.stdout.trim() || void 0;
|
|
16178
17791
|
} catch (e) {
|
|
16179
17792
|
if (/404|Not Found/i.test(String(e.message ?? ""))) return void 0;
|
|
@@ -16182,9 +17795,9 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
16182
17795
|
};
|
|
16183
17796
|
const fanoutBranchSha = await branchFileSha(".github/fanout-targets.json");
|
|
16184
17797
|
const projectsBranchSha = await branchFileSha("projects.json");
|
|
16185
|
-
await gh(contentPutArgs(
|
|
16186
|
-
await gh(contentPutArgs(
|
|
16187
|
-
const openPrs = await gh(["pr", "list", "--repo",
|
|
17798
|
+
await gh(contentPutArgs(HUB_REPO2, ".github/fanout-targets.json", plan2.fanoutTargets, branchName, fanoutBranchSha));
|
|
17799
|
+
await gh(contentPutArgs(HUB_REPO2, "projects.json", plan2.projects, branchName, projectsBranchSha));
|
|
17800
|
+
const openPrs = await gh(["pr", "list", "--repo", HUB_REPO2, "--head", branchName, "--base", "development", "--state", "open", "--json", "number,url"]);
|
|
16188
17801
|
const prDecision = decideFanoutPrAction(JSON.parse(openPrs.stdout || "[]"));
|
|
16189
17802
|
if (prDecision.action === "reuse") {
|
|
16190
17803
|
fanoutPrUrl = prDecision.url;
|
|
@@ -16193,7 +17806,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
16193
17806
|
"pr",
|
|
16194
17807
|
"create",
|
|
16195
17808
|
"--repo",
|
|
16196
|
-
|
|
17809
|
+
HUB_REPO2,
|
|
16197
17810
|
"--base",
|
|
16198
17811
|
"development",
|
|
16199
17812
|
"--head",
|
|
@@ -16205,7 +17818,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
16205
17818
|
]);
|
|
16206
17819
|
fanoutPrUrl = created.url;
|
|
16207
17820
|
}
|
|
16208
|
-
await gh(["pr", "merge", fanoutPrUrl, "--repo",
|
|
17821
|
+
await gh(["pr", "merge", fanoutPrUrl, "--repo", HUB_REPO2, "--auto", "--squash"]).catch((e) => {
|
|
16209
17822
|
if (!/already/i.test(String(e.message ?? ""))) throw e;
|
|
16210
17823
|
});
|
|
16211
17824
|
applied.push(`fanout: PR ${fanoutPrUrl} (auto-merge enabled)`);
|
|
@@ -16256,16 +17869,16 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
16256
17869
|
if (o.class !== "deployable" && o.class !== "content") return failGraceful("access audit: --class must be deployable or content");
|
|
16257
17870
|
targets = [{ repo: o.repo, class: o.class }];
|
|
16258
17871
|
} else {
|
|
16259
|
-
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0,
|
|
17872
|
+
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs18.existsSync)("projects.json") ? (0, import_node_fs18.readFileSync)("projects.json", "utf8") : null;
|
|
16260
17873
|
if (!projectsJson) return failGraceful("access audit: no project registry \u2014 Hub API unreachable and projects.json not found; run from the MMI-Hub repo root or pass --repo <owner/repo>");
|
|
16261
|
-
const fanoutJson = (0,
|
|
17874
|
+
const fanoutJson = (0, import_node_fs18.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs18.readFileSync)(".github/fanout-targets.json", "utf8") : null;
|
|
16262
17875
|
targets = loadAccessTargets(projectsJson, fanoutJson);
|
|
16263
17876
|
}
|
|
16264
17877
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
16265
|
-
const fileMatrix = (0,
|
|
17878
|
+
const fileMatrix = (0, import_node_fs18.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs18.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
16266
17879
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
16267
17880
|
const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
16268
|
-
const fileContracts = (0,
|
|
17881
|
+
const fileContracts = (0, import_node_fs18.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs18.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
|
|
16269
17882
|
const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
|
|
16270
17883
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
|
|
16271
17884
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
@@ -16274,20 +17887,20 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
16274
17887
|
var isWin = process.platform === "win32";
|
|
16275
17888
|
var installedPluginsPath = (surface = detectSurface(process.env)) => {
|
|
16276
17889
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
16277
|
-
return (0,
|
|
17890
|
+
return (0, import_node_path16.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
16278
17891
|
};
|
|
16279
17892
|
function readInstalledPlugins() {
|
|
16280
17893
|
try {
|
|
16281
|
-
return JSON.parse((0,
|
|
17894
|
+
return JSON.parse((0, import_node_fs18.readFileSync)(installedPluginsPath(), "utf8"));
|
|
16282
17895
|
} catch {
|
|
16283
17896
|
return null;
|
|
16284
17897
|
}
|
|
16285
17898
|
}
|
|
16286
17899
|
function installedPluginSources() {
|
|
16287
17900
|
return ["claude", "codex"].map((surface) => {
|
|
16288
|
-
const recordPath = (0,
|
|
17901
|
+
const recordPath = (0, import_node_path16.join)((0, import_node_os5.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
|
|
16289
17902
|
try {
|
|
16290
|
-
return { surface, installed: JSON.parse((0,
|
|
17903
|
+
return { surface, installed: JSON.parse((0, import_node_fs18.readFileSync)(recordPath, "utf8")), recordPath };
|
|
16291
17904
|
} catch {
|
|
16292
17905
|
return { surface, installed: null, recordPath };
|
|
16293
17906
|
}
|
|
@@ -16295,7 +17908,7 @@ function installedPluginSources() {
|
|
|
16295
17908
|
}
|
|
16296
17909
|
function readClaudeSettings() {
|
|
16297
17910
|
try {
|
|
16298
|
-
return JSON.parse((0,
|
|
17911
|
+
return JSON.parse((0, import_node_fs18.readFileSync)((0, import_node_path16.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
|
|
16299
17912
|
} catch {
|
|
16300
17913
|
return null;
|
|
16301
17914
|
}
|
|
@@ -16317,7 +17930,7 @@ function writeProjectInstallRecord(record) {
|
|
|
16317
17930
|
const list = file.plugins[MMI_PLUGIN_ID] ?? [];
|
|
16318
17931
|
list.push(record);
|
|
16319
17932
|
file.plugins[MMI_PLUGIN_ID] = list;
|
|
16320
|
-
(0,
|
|
17933
|
+
(0, import_node_fs18.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
|
|
16321
17934
|
`, "utf8");
|
|
16322
17935
|
return true;
|
|
16323
17936
|
} catch {
|
|
@@ -16330,9 +17943,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
16330
17943
|
if (!file) return false;
|
|
16331
17944
|
if (!file.plugins) file.plugins = {};
|
|
16332
17945
|
const path2 = installedPluginsPath();
|
|
16333
|
-
(0,
|
|
17946
|
+
(0, import_node_fs18.copyFileSync)(path2, `${path2}.bak`);
|
|
16334
17947
|
file.plugins[pluginId] = records;
|
|
16335
|
-
(0,
|
|
17948
|
+
(0, import_node_fs18.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
|
|
16336
17949
|
`, "utf8");
|
|
16337
17950
|
return true;
|
|
16338
17951
|
} catch {
|
|
@@ -16340,35 +17953,35 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
16340
17953
|
}
|
|
16341
17954
|
}
|
|
16342
17955
|
function cursorPluginCacheRoot() {
|
|
16343
|
-
return (0,
|
|
17956
|
+
return (0, import_node_path16.join)((0, import_node_os5.homedir)(), ".cursor", "plugins", "cache", "mmi", "mmi");
|
|
16344
17957
|
}
|
|
16345
17958
|
function cursorPluginCachePinSnapshots() {
|
|
16346
17959
|
const root = cursorPluginCacheRoot();
|
|
16347
17960
|
try {
|
|
16348
|
-
return (0,
|
|
16349
|
-
const path2 = (0,
|
|
16350
|
-
const pluginJson = (0,
|
|
16351
|
-
const hooksJson = (0,
|
|
16352
|
-
const cliBundle = (0,
|
|
17961
|
+
return (0, import_node_fs18.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
|
|
17962
|
+
const path2 = (0, import_node_path16.join)(root, entry.name);
|
|
17963
|
+
const pluginJson = (0, import_node_path16.join)(path2, ".cursor-plugin", "plugin.json");
|
|
17964
|
+
const hooksJson = (0, import_node_path16.join)(path2, "hooks", "hooks.json");
|
|
17965
|
+
const cliBundle = (0, import_node_path16.join)(path2, "cli", "dist", "index.cjs");
|
|
16353
17966
|
let version;
|
|
16354
17967
|
try {
|
|
16355
|
-
const raw = JSON.parse((0,
|
|
17968
|
+
const raw = JSON.parse((0, import_node_fs18.readFileSync)(pluginJson, "utf8"));
|
|
16356
17969
|
version = typeof raw.version === "string" ? raw.version : void 0;
|
|
16357
17970
|
} catch {
|
|
16358
17971
|
version = void 0;
|
|
16359
17972
|
}
|
|
16360
17973
|
let isEmpty = true;
|
|
16361
17974
|
try {
|
|
16362
|
-
isEmpty = (0,
|
|
17975
|
+
isEmpty = (0, import_node_fs18.readdirSync)(path2).length === 0;
|
|
16363
17976
|
} catch {
|
|
16364
17977
|
isEmpty = true;
|
|
16365
17978
|
}
|
|
16366
17979
|
return {
|
|
16367
17980
|
name: entry.name,
|
|
16368
17981
|
path: path2,
|
|
16369
|
-
hasPluginJson: (0,
|
|
16370
|
-
hasHooksJson: (0,
|
|
16371
|
-
hasCliBundle: (0,
|
|
17982
|
+
hasPluginJson: (0, import_node_fs18.existsSync)(pluginJson),
|
|
17983
|
+
hasHooksJson: (0, import_node_fs18.existsSync)(hooksJson),
|
|
17984
|
+
hasCliBundle: (0, import_node_fs18.existsSync)(cliBundle),
|
|
16372
17985
|
isEmpty,
|
|
16373
17986
|
version
|
|
16374
17987
|
};
|
|
@@ -16378,19 +17991,19 @@ function cursorPluginCachePinSnapshots() {
|
|
|
16378
17991
|
}
|
|
16379
17992
|
}
|
|
16380
17993
|
function hubCheckoutForCursorSeed() {
|
|
16381
|
-
const manifest = (0,
|
|
16382
|
-
return (0,
|
|
17994
|
+
const manifest = (0, import_node_path16.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
|
|
17995
|
+
return (0, import_node_fs18.existsSync)(manifest) ? process.cwd() : void 0;
|
|
16383
17996
|
}
|
|
16384
17997
|
function mmiPluginCacheRootSnapshots() {
|
|
16385
17998
|
const roots = [
|
|
16386
|
-
{ surface: "claude", root: (0,
|
|
16387
|
-
{ surface: "codex", root: (0,
|
|
17999
|
+
{ surface: "claude", root: (0, import_node_path16.join)((0, import_node_os5.homedir)(), ".claude", "plugins", "cache", "mmi", "mmi") },
|
|
18000
|
+
{ surface: "codex", root: (0, import_node_path16.join)((0, import_node_os5.homedir)(), ".codex", "plugins", "cache", "mmi", "mmi") }
|
|
16388
18001
|
];
|
|
16389
18002
|
return roots.flatMap(({ surface, root }) => {
|
|
16390
18003
|
try {
|
|
16391
|
-
const entries = (0,
|
|
18004
|
+
const entries = (0, import_node_fs18.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
|
|
16392
18005
|
name: entry.name,
|
|
16393
|
-
path: (0,
|
|
18006
|
+
path: (0, import_node_path16.join)(root, entry.name),
|
|
16394
18007
|
isDirectory: entry.isDirectory()
|
|
16395
18008
|
}));
|
|
16396
18009
|
return [{ surface, root, entries }];
|
|
@@ -16401,7 +18014,7 @@ function mmiPluginCacheRootSnapshots() {
|
|
|
16401
18014
|
}
|
|
16402
18015
|
function hasNestedMmiChild(versionDir) {
|
|
16403
18016
|
try {
|
|
16404
|
-
return (0,
|
|
18017
|
+
return (0, import_node_fs18.statSync)((0, import_node_path16.join)(versionDir, "mmi")).isDirectory();
|
|
16405
18018
|
} catch {
|
|
16406
18019
|
return false;
|
|
16407
18020
|
}
|
|
@@ -16412,26 +18025,28 @@ function nestedPluginTreeSnapshot() {
|
|
|
16412
18025
|
);
|
|
16413
18026
|
}
|
|
16414
18027
|
function uniqueQuarantineTarget(path2) {
|
|
16415
|
-
if (!(0,
|
|
18028
|
+
if (!(0, import_node_fs18.existsSync)(path2)) return path2;
|
|
16416
18029
|
for (let i = 1; i < 100; i += 1) {
|
|
16417
18030
|
const candidate = `${path2}-${i}`;
|
|
16418
|
-
if (!(0,
|
|
18031
|
+
if (!(0, import_node_fs18.existsSync)(candidate)) return candidate;
|
|
16419
18032
|
}
|
|
16420
18033
|
return `${path2}-${Date.now()}`;
|
|
16421
18034
|
}
|
|
16422
18035
|
function quarantinePluginCacheDirs(plan2) {
|
|
16423
18036
|
let moved = 0;
|
|
18037
|
+
const failed = [];
|
|
16424
18038
|
for (const move of plan2) {
|
|
16425
18039
|
try {
|
|
16426
|
-
if (!(0,
|
|
18040
|
+
if (!(0, import_node_fs18.existsSync)(move.from)) continue;
|
|
16427
18041
|
const target = uniqueQuarantineTarget(move.to);
|
|
16428
|
-
(0,
|
|
16429
|
-
(0,
|
|
18042
|
+
(0, import_node_fs18.mkdirSync)((0, import_node_path16.dirname)(target), { recursive: true });
|
|
18043
|
+
(0, import_node_fs18.renameSync)(move.from, target);
|
|
16430
18044
|
moved += 1;
|
|
16431
18045
|
} catch {
|
|
18046
|
+
failed.push(move);
|
|
16432
18047
|
}
|
|
16433
18048
|
}
|
|
16434
|
-
return moved;
|
|
18049
|
+
return { moved, failed };
|
|
16435
18050
|
}
|
|
16436
18051
|
async function robocopyMirrorEmpty(emptyDir, target) {
|
|
16437
18052
|
try {
|
|
@@ -16444,23 +18059,23 @@ async function robocopyMirrorEmpty(emptyDir, target) {
|
|
|
16444
18059
|
}
|
|
16445
18060
|
async function clearNestedPluginTreeDir(targetPath) {
|
|
16446
18061
|
try {
|
|
16447
|
-
if (!(0,
|
|
18062
|
+
if (!(0, import_node_fs18.existsSync)(targetPath)) return true;
|
|
16448
18063
|
if (isWin) {
|
|
16449
|
-
const emptyDir = (0,
|
|
16450
|
-
(0,
|
|
18064
|
+
const emptyDir = (0, import_node_path16.join)((0, import_node_os5.tmpdir)(), `mmi-empty-${Date.now()}`);
|
|
18065
|
+
(0, import_node_fs18.mkdirSync)(emptyDir, { recursive: true });
|
|
16451
18066
|
try {
|
|
16452
18067
|
await robocopyMirrorEmpty(emptyDir, targetPath);
|
|
16453
|
-
(0,
|
|
18068
|
+
(0, import_node_fs18.rmSync)(targetPath, { recursive: true, force: true });
|
|
16454
18069
|
} finally {
|
|
16455
18070
|
try {
|
|
16456
|
-
(0,
|
|
18071
|
+
(0, import_node_fs18.rmSync)(emptyDir, { recursive: true, force: true });
|
|
16457
18072
|
} catch {
|
|
16458
18073
|
}
|
|
16459
18074
|
}
|
|
16460
|
-
return !(0,
|
|
18075
|
+
return !(0, import_node_fs18.existsSync)(targetPath);
|
|
16461
18076
|
}
|
|
16462
|
-
(0,
|
|
16463
|
-
return !(0,
|
|
18077
|
+
(0, import_node_fs18.rmSync)(targetPath, { recursive: true, force: true });
|
|
18078
|
+
return !(0, import_node_fs18.existsSync)(targetPath);
|
|
16464
18079
|
} catch {
|
|
16465
18080
|
return false;
|
|
16466
18081
|
}
|
|
@@ -16473,11 +18088,11 @@ async function applyNestedPluginTreeCleanup(paths, log) {
|
|
|
16473
18088
|
}
|
|
16474
18089
|
return true;
|
|
16475
18090
|
}
|
|
16476
|
-
var gitignorePath = () => (0,
|
|
18091
|
+
var gitignorePath = () => (0, import_node_path16.join)(process.cwd(), ".gitignore");
|
|
16477
18092
|
function readTextFile(path2) {
|
|
16478
18093
|
try {
|
|
16479
|
-
if (!(0,
|
|
16480
|
-
return (0,
|
|
18094
|
+
if (!(0, import_node_fs18.existsSync)(path2)) return null;
|
|
18095
|
+
return (0, import_node_fs18.readFileSync)(path2, "utf8");
|
|
16481
18096
|
} catch {
|
|
16482
18097
|
return null;
|
|
16483
18098
|
}
|
|
@@ -16486,9 +18101,9 @@ function playwrightMcpConfigSnapshots() {
|
|
|
16486
18101
|
const cwd = process.cwd();
|
|
16487
18102
|
const home = (0, import_node_os5.homedir)();
|
|
16488
18103
|
const candidates = [
|
|
16489
|
-
(0,
|
|
16490
|
-
(0,
|
|
16491
|
-
(0,
|
|
18104
|
+
(0, import_node_path16.join)(cwd, ".cursor", "mcp.json"),
|
|
18105
|
+
(0, import_node_path16.join)(home, ".cursor", "mcp.json"),
|
|
18106
|
+
(0, import_node_path16.join)(home, ".codex", "config.toml")
|
|
16492
18107
|
];
|
|
16493
18108
|
const out = [];
|
|
16494
18109
|
for (const path2 of candidates) {
|
|
@@ -16501,7 +18116,7 @@ function strayBrowserArtifactPaths() {
|
|
|
16501
18116
|
const cwd = process.cwd();
|
|
16502
18117
|
return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
|
|
16503
18118
|
try {
|
|
16504
|
-
return (0,
|
|
18119
|
+
return (0, import_node_fs18.existsSync)((0, import_node_path16.join)(cwd, rel));
|
|
16505
18120
|
} catch {
|
|
16506
18121
|
return false;
|
|
16507
18122
|
}
|
|
@@ -16509,14 +18124,14 @@ function strayBrowserArtifactPaths() {
|
|
|
16509
18124
|
}
|
|
16510
18125
|
function readGitignore() {
|
|
16511
18126
|
try {
|
|
16512
|
-
return (0,
|
|
18127
|
+
return (0, import_node_fs18.readFileSync)(gitignorePath(), "utf8");
|
|
16513
18128
|
} catch {
|
|
16514
18129
|
return null;
|
|
16515
18130
|
}
|
|
16516
18131
|
}
|
|
16517
18132
|
function writeGitignore(content) {
|
|
16518
18133
|
try {
|
|
16519
|
-
(0,
|
|
18134
|
+
(0, import_node_fs18.writeFileSync)(gitignorePath(), content, "utf8");
|
|
16520
18135
|
return true;
|
|
16521
18136
|
} catch {
|
|
16522
18137
|
return false;
|
|
@@ -16555,7 +18170,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
16555
18170
|
let onPath = pathProbe;
|
|
16556
18171
|
if (!onPath) {
|
|
16557
18172
|
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
|
16558
|
-
if (root && (0,
|
|
18173
|
+
if (root && (0, import_node_fs18.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
|
|
16559
18174
|
}
|
|
16560
18175
|
checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
|
|
16561
18176
|
const surface = detectSurface(process.env);
|
|
@@ -16649,6 +18264,19 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
16649
18264
|
io.err(` \u21BB updated MMI plugin \u2192 ${releasedVersion ?? "latest"} via claude plugin \u2014 ${reloadAction(surface)} to load the new commands`);
|
|
16650
18265
|
}
|
|
16651
18266
|
}
|
|
18267
|
+
const codexStale = installedVersionCheck.staleSurfaces?.some((s) => s.surface === "codex") ?? false;
|
|
18268
|
+
if (!installedVersionCheck.ok && codexStale && await applyCodexPluginHeal(surface, (m) => io.err(m))) {
|
|
18269
|
+
const healed = buildInstalledPluginVersionCheck({
|
|
18270
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
18271
|
+
sources: installedPluginSources(),
|
|
18272
|
+
releasedVersion,
|
|
18273
|
+
surface
|
|
18274
|
+
});
|
|
18275
|
+
installedVersionCheck = healed;
|
|
18276
|
+
if (healed.ok) {
|
|
18277
|
+
io.err(` \u21BB updated MMI plugin \u2192 ${releasedVersion ?? "latest"} via codex plugin \u2014 ${reloadAction(surface)} to load the new commands`);
|
|
18278
|
+
}
|
|
18279
|
+
}
|
|
16652
18280
|
}
|
|
16653
18281
|
checks.push(installedVersionCheck);
|
|
16654
18282
|
let cacheCleanupCheck = buildMmiPluginCacheCleanupCheck({
|
|
@@ -16659,7 +18287,8 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
16659
18287
|
installedVersions: installedPluginVersions(installed)
|
|
16660
18288
|
});
|
|
16661
18289
|
if (!cacheCleanupCheck.ok && cacheCleanupCheck.quarantinePlan && repairLocal) {
|
|
16662
|
-
const moved = quarantinePluginCacheDirs(cacheCleanupCheck.quarantinePlan);
|
|
18290
|
+
const { moved, failed } = quarantinePluginCacheDirs(cacheCleanupCheck.quarantinePlan);
|
|
18291
|
+
const attempted = moved + failed.length;
|
|
16663
18292
|
if (moved > 0) {
|
|
16664
18293
|
const surfaces = [...new Set(cacheCleanupCheck.leftovers?.map((entry) => entry.surface) ?? [])].join("/");
|
|
16665
18294
|
const names = cacheCleanupCheck.leftovers?.map((entry) => entry.name).join(", ");
|
|
@@ -16670,8 +18299,12 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
16670
18299
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
16671
18300
|
roots: mmiPluginCacheRootSnapshots(),
|
|
16672
18301
|
activeVersion: resolveClientVersion(),
|
|
16673
|
-
releasedVersion
|
|
18302
|
+
releasedVersion,
|
|
18303
|
+
installedVersions: installedPluginVersions(installed)
|
|
16674
18304
|
}),
|
|
18305
|
+
attemptedCount: attempted,
|
|
18306
|
+
failedCount: failed.length,
|
|
18307
|
+
...failed.length > 0 ? { failedMoves: failed } : {},
|
|
16675
18308
|
...moved > 0 ? { cleanedCount: moved } : {}
|
|
16676
18309
|
};
|
|
16677
18310
|
}
|
|
@@ -16709,7 +18342,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
16709
18342
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
16710
18343
|
surface,
|
|
16711
18344
|
cacheRoot: cursorCacheRoot,
|
|
16712
|
-
cacheRootExists: (0,
|
|
18345
|
+
cacheRootExists: (0, import_node_fs18.existsSync)(cursorCacheRoot),
|
|
16713
18346
|
pins: cursorPins,
|
|
16714
18347
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
16715
18348
|
releasedVersion
|
|
@@ -16720,7 +18353,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
16720
18353
|
releasedVersion,
|
|
16721
18354
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
16722
18355
|
execFileP: execFileP2,
|
|
16723
|
-
mkdtemp: (prefix) => (0, import_promises5.mkdtemp)((0,
|
|
18356
|
+
mkdtemp: (prefix) => (0, import_promises5.mkdtemp)((0, import_node_path16.join)((0, import_node_os5.tmpdir)(), prefix)),
|
|
16724
18357
|
log: (m) => io.err(m)
|
|
16725
18358
|
});
|
|
16726
18359
|
if (seeded) {
|
|
@@ -16729,7 +18362,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
16729
18362
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
16730
18363
|
surface,
|
|
16731
18364
|
cacheRoot: cursorCacheRoot,
|
|
16732
|
-
cacheRootExists: (0,
|
|
18365
|
+
cacheRootExists: (0, import_node_fs18.existsSync)(cursorCacheRoot),
|
|
16733
18366
|
pins: cursorPins,
|
|
16734
18367
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
16735
18368
|
releasedVersion
|
|
@@ -16812,6 +18445,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
|
|
|
16812
18445
|
const { parallel, sequential } = buildSessionStartPlan({
|
|
16813
18446
|
rulesSync: (io) => runRulesSync({ quiet: true }, io),
|
|
16814
18447
|
sagaShow: (io) => runSagaShow({ quiet: true }, io),
|
|
18448
|
+
handoffOffer: (io) => runHandoffOffer(io, { fast: true }),
|
|
16815
18449
|
// honcho profile (#1162): inject the behavioral-memory prior (peer card). Bounded + fail-soft +
|
|
16816
18450
|
// silent when off/empty, so it never delays or noises the session banner.
|
|
16817
18451
|
honchoContext: (io) => runHonchoContext({ quiet: true, banner: true }, io),
|
|
@@ -16845,6 +18479,12 @@ program2.command("session-start").description("run the SessionStart verbs (rules
|
|
|
16845
18479
|
await runSessionStart(parallel, sequential, consoleIo);
|
|
16846
18480
|
consoleIo.log(northstarPointer(northstarInjected));
|
|
16847
18481
|
for (const line of planStoreLines(process.cwd())) consoleIo.log(line);
|
|
18482
|
+
for (const line of scratchGcLines(process.cwd())) consoleIo.log(line);
|
|
18483
|
+
const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
|
|
18484
|
+
if (worktreeBanner) {
|
|
18485
|
+
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
18486
|
+
consoleIo.log(worktreeBanner);
|
|
18487
|
+
}
|
|
16848
18488
|
});
|
|
16849
18489
|
installProcessBackstop();
|
|
16850
18490
|
program2.parseAsync().catch((e) => failGraceful(e.message));
|