@mutmutco/cli 4.5.174 → 4.5.176
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.cjs +489 -268
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -13366,6 +13366,234 @@ function findDuplicateLesson(source, openLessons) {
|
|
|
13366
13366
|
// src/board-doctor-config.ts
|
|
13367
13367
|
init_github_client();
|
|
13368
13368
|
|
|
13369
|
+
// src/sub-issue.ts
|
|
13370
|
+
init_error_codes();
|
|
13371
|
+
init_github_quota();
|
|
13372
|
+
function buildResolveIdArgs(ref) {
|
|
13373
|
+
const args = ["issue", "view", String(ref.number), "--json", "id", "--jq", ".id"];
|
|
13374
|
+
if (ref.repo) args.push("--repo", ref.repo);
|
|
13375
|
+
return args;
|
|
13376
|
+
}
|
|
13377
|
+
function buildAddSubIssueArgs(parentId, subIssueId) {
|
|
13378
|
+
if (!parentId) throw new Error("addSubIssue: parentId is required");
|
|
13379
|
+
if (!subIssueId) throw new Error("addSubIssue: subIssueId is required");
|
|
13380
|
+
return [
|
|
13381
|
+
"api",
|
|
13382
|
+
"graphql",
|
|
13383
|
+
"-f",
|
|
13384
|
+
"query=mutation($p:ID!,$c:ID!){addSubIssue(input:{issueId:$p,subIssueId:$c}){issue{number subIssues{totalCount}} subIssue{number}}}",
|
|
13385
|
+
"-f",
|
|
13386
|
+
`p=${parentId}`,
|
|
13387
|
+
"-f",
|
|
13388
|
+
`c=${subIssueId}`
|
|
13389
|
+
];
|
|
13390
|
+
}
|
|
13391
|
+
function parseAddSubIssueResult(stdout) {
|
|
13392
|
+
try {
|
|
13393
|
+
const issue = JSON.parse(stdout)?.data?.addSubIssue;
|
|
13394
|
+
const parentNumber = issue?.issue?.number;
|
|
13395
|
+
const subIssueNumber = issue?.subIssue?.number;
|
|
13396
|
+
const totalCount = issue?.issue?.subIssues?.totalCount;
|
|
13397
|
+
if (typeof parentNumber !== "number" || typeof subIssueNumber !== "number") return void 0;
|
|
13398
|
+
return { parentNumber, subIssueNumber, totalCount: typeof totalCount === "number" ? totalCount : 0 };
|
|
13399
|
+
} catch {
|
|
13400
|
+
return void 0;
|
|
13401
|
+
}
|
|
13402
|
+
}
|
|
13403
|
+
var RESOLVE_ID_TIMEOUT_MS = 1e4;
|
|
13404
|
+
async function resolveIssueNodeId(runGh, ref, fallbackRepo) {
|
|
13405
|
+
const resolved = ref.repo ? ref : { ...ref, repo: fallbackRepo };
|
|
13406
|
+
const id = (await runGh(buildResolveIdArgs(resolved), RESOLVE_ID_TIMEOUT_MS)).trim();
|
|
13407
|
+
if (!id) throw new Error(`could not resolve node id for issue #${ref.number}${resolved.repo ? ` in ${resolved.repo}` : ""}`);
|
|
13408
|
+
return id;
|
|
13409
|
+
}
|
|
13410
|
+
var SubIssueRestFallbackError = class extends Error {
|
|
13411
|
+
constructor(message2, reason) {
|
|
13412
|
+
super(message2);
|
|
13413
|
+
this.reason = reason;
|
|
13414
|
+
this.name = "SubIssueRestFallbackError";
|
|
13415
|
+
}
|
|
13416
|
+
reason;
|
|
13417
|
+
};
|
|
13418
|
+
async function linkSubIssue(runGh, parentRef, childRef, defaultRepo) {
|
|
13419
|
+
const parent = parseIssueRef(parentRef);
|
|
13420
|
+
const child2 = parseIssueRef(childRef);
|
|
13421
|
+
try {
|
|
13422
|
+
const parentId = await resolveIssueNodeId(runGh, parent, defaultRepo);
|
|
13423
|
+
const subIssueId = await resolveIssueNodeId(runGh, child2, defaultRepo);
|
|
13424
|
+
const stdout = await runGh(buildAddSubIssueArgs(parentId, subIssueId), GH_MUTATION_TIMEOUT_MS);
|
|
13425
|
+
const result = parseAddSubIssueResult(stdout);
|
|
13426
|
+
if (!result) throw new Error(`addSubIssue returned an unexpected response:
|
|
13427
|
+
${stdout.trim() || "(empty)"}`);
|
|
13428
|
+
return result;
|
|
13429
|
+
} catch (graphqlError) {
|
|
13430
|
+
if (!isRateLimitRefusal(graphqlError)) throw graphqlError;
|
|
13431
|
+
const parentRepo = parent.repo ?? defaultRepo;
|
|
13432
|
+
const childRepo = child2.repo ?? defaultRepo;
|
|
13433
|
+
if (!parentRepo || !childRepo) throw graphqlError;
|
|
13434
|
+
try {
|
|
13435
|
+
const childPath = `repos/${childRepo}/issues/${child2.number}`;
|
|
13436
|
+
const childIssue = JSON.parse(await runGh(["api", childPath], RESOLVE_ID_TIMEOUT_MS));
|
|
13437
|
+
if (!Number.isSafeInteger(childIssue.id) || !childIssue.id) throw new Error(`REST issue ${childRepo}#${child2.number} has no numeric id`);
|
|
13438
|
+
await runGh([
|
|
13439
|
+
"api",
|
|
13440
|
+
"-X",
|
|
13441
|
+
"POST",
|
|
13442
|
+
`repos/${parentRepo}/issues/${parent.number}/sub_issues`,
|
|
13443
|
+
"-F",
|
|
13444
|
+
`sub_issue_id=${childIssue.id}`
|
|
13445
|
+
], GH_MUTATION_TIMEOUT_MS);
|
|
13446
|
+
const confirmed = JSON.parse(await runGh(["api", childPath], RESOLVE_ID_TIMEOUT_MS));
|
|
13447
|
+
const actual = resolveParentField(confirmed);
|
|
13448
|
+
if (!("parent" in actual) || !actual.parent || actual.parent.repo.toLowerCase() !== parentRepo.toLowerCase() || actual.parent.number !== parent.number) {
|
|
13449
|
+
throw new Error(`REST link not verified for ${childRepo}#${child2.number}: ${"parentReadError" in actual ? actual.parentReadError : "parent differs or is absent"}`);
|
|
13450
|
+
}
|
|
13451
|
+
let totalCount = 0;
|
|
13452
|
+
try {
|
|
13453
|
+
const parentIssue = JSON.parse(await runGh(["api", `repos/${parentRepo}/issues/${parent.number}`], RESOLVE_ID_TIMEOUT_MS));
|
|
13454
|
+
if (typeof parentIssue.sub_issues_summary?.total === "number") totalCount = parentIssue.sub_issues_summary.total;
|
|
13455
|
+
} catch {
|
|
13456
|
+
}
|
|
13457
|
+
return { parentNumber: parent.number, subIssueNumber: child2.number, totalCount };
|
|
13458
|
+
} catch (restError) {
|
|
13459
|
+
const err = restError;
|
|
13460
|
+
const detail = (err.stderr || err.message || String(restError)).trim();
|
|
13461
|
+
throw new SubIssueRestFallbackError(
|
|
13462
|
+
`GraphQL link rate-limited; REST link not verified: ${detail}`,
|
|
13463
|
+
isRateLimitRefusal(restError) ? "rate_limited" : "failed"
|
|
13464
|
+
);
|
|
13465
|
+
}
|
|
13466
|
+
}
|
|
13467
|
+
}
|
|
13468
|
+
function buildRemoveSubIssueArgs(parentId, subIssueId) {
|
|
13469
|
+
if (!parentId) throw new Error("removeSubIssue: parentId is required");
|
|
13470
|
+
if (!subIssueId) throw new Error("removeSubIssue: subIssueId is required");
|
|
13471
|
+
return [
|
|
13472
|
+
"api",
|
|
13473
|
+
"graphql",
|
|
13474
|
+
"-f",
|
|
13475
|
+
"query=mutation($p:ID!,$c:ID!){removeSubIssue(input:{issueId:$p,subIssueId:$c}){issue{number subIssues{totalCount}} subIssue{number}}}",
|
|
13476
|
+
"-f",
|
|
13477
|
+
`p=${parentId}`,
|
|
13478
|
+
"-f",
|
|
13479
|
+
`c=${subIssueId}`
|
|
13480
|
+
];
|
|
13481
|
+
}
|
|
13482
|
+
function parseRemoveSubIssueResult(stdout) {
|
|
13483
|
+
try {
|
|
13484
|
+
const issue = JSON.parse(stdout)?.data?.removeSubIssue;
|
|
13485
|
+
const parentNumber = issue?.issue?.number;
|
|
13486
|
+
const subIssueNumber = issue?.subIssue?.number;
|
|
13487
|
+
const totalCount = issue?.issue?.subIssues?.totalCount;
|
|
13488
|
+
if (typeof parentNumber !== "number" || typeof subIssueNumber !== "number") return void 0;
|
|
13489
|
+
return { parentNumber, subIssueNumber, totalCount: typeof totalCount === "number" ? totalCount : 0 };
|
|
13490
|
+
} catch {
|
|
13491
|
+
return void 0;
|
|
13492
|
+
}
|
|
13493
|
+
}
|
|
13494
|
+
async function unlinkSubIssue(runGh, parentRef, childRef, defaultRepo) {
|
|
13495
|
+
const parent = parseIssueRef(parentRef);
|
|
13496
|
+
const child2 = parseIssueRef(childRef);
|
|
13497
|
+
const parentId = await resolveIssueNodeId(runGh, parent, defaultRepo);
|
|
13498
|
+
const subIssueId = await resolveIssueNodeId(runGh, child2, defaultRepo);
|
|
13499
|
+
const stdout = await runGh(buildRemoveSubIssueArgs(parentId, subIssueId), GH_MUTATION_TIMEOUT_MS);
|
|
13500
|
+
const result = parseRemoveSubIssueResult(stdout);
|
|
13501
|
+
if (!result) throw new Error(`removeSubIssue returned an unexpected response:
|
|
13502
|
+
${stdout.trim() || "(empty)"}`);
|
|
13503
|
+
return result;
|
|
13504
|
+
}
|
|
13505
|
+
function parseParentIssueUrl(apiUrl) {
|
|
13506
|
+
if (typeof apiUrl !== "string" || !apiUrl.trim()) return null;
|
|
13507
|
+
const match = /\/repos\/([^/]+\/[^/]+)\/issues\/(\d+)\/?$/.exec(apiUrl.trim());
|
|
13508
|
+
if (!match) return null;
|
|
13509
|
+
const [, repo, number] = match;
|
|
13510
|
+
return { repo, number: Number(number), url: `https://github.com/${repo}/issues/${number}` };
|
|
13511
|
+
}
|
|
13512
|
+
function resolveParentField(payload) {
|
|
13513
|
+
if (!payload || typeof payload !== "object") {
|
|
13514
|
+
return { parentReadError: "issue payload was empty or not an object" };
|
|
13515
|
+
}
|
|
13516
|
+
if (!("sub_issues_summary" in payload)) {
|
|
13517
|
+
return { parentReadError: "issue payload carries no sub-issue fields \u2014 parent state is unknown, not absent" };
|
|
13518
|
+
}
|
|
13519
|
+
const raw = payload.parent_issue_url;
|
|
13520
|
+
if (raw === void 0 || raw === null) return { parent: null };
|
|
13521
|
+
const parsed = typeof raw === "string" ? parseParentIssueUrl(raw) : null;
|
|
13522
|
+
return parsed ? { parent: parsed } : { parentReadError: `unrecognised parent_issue_url: ${JSON.stringify(raw)}` };
|
|
13523
|
+
}
|
|
13524
|
+
async function readIssueParent(runGh, repo, number) {
|
|
13525
|
+
try {
|
|
13526
|
+
const stdout = await runGh(["api", `repos/${repo}/issues/${number}`], RESOLVE_ID_TIMEOUT_MS);
|
|
13527
|
+
const resolved = resolveParentField(JSON.parse(stdout));
|
|
13528
|
+
return "parent" in resolved ? resolved.parent : void 0;
|
|
13529
|
+
} catch {
|
|
13530
|
+
return void 0;
|
|
13531
|
+
}
|
|
13532
|
+
}
|
|
13533
|
+
function isParentConflictMessage(text) {
|
|
13534
|
+
return typeof text === "string" && /sub[- ]?issue may only have one parent/i.test(text);
|
|
13535
|
+
}
|
|
13536
|
+
function buildReparentConflictPayload(input) {
|
|
13537
|
+
const { childRepo, childNumber, requestedParent, currentParent, offendingFlag } = input;
|
|
13538
|
+
const child2 = `${childRepo}#${childNumber}`;
|
|
13539
|
+
const payload = { code: ERROR_CODES.ERR_STATE_CONFLICT };
|
|
13540
|
+
if (offendingFlag) payload.offending_flag = offendingFlag;
|
|
13541
|
+
if (!currentParent) {
|
|
13542
|
+
return {
|
|
13543
|
+
message: currentParent === null ? `${child2} cannot be reparented under ${requestedParent} \u2014 GitHub refused it under the one-parent rule, but reading the issue back showed no parent at all. Something changed it concurrently; re-read with \`mmi-cli oracle issue view ${childNumber} --repo ${childRepo}\` and retry` : `${child2} cannot be reparented under ${requestedParent} \u2014 GitHub allows a sub-issue exactly one parent and it already has one. Its current parent could not be read; check with \`mmi-cli oracle issue view ${childNumber} --repo ${childRepo}\``,
|
|
13544
|
+
payload
|
|
13545
|
+
};
|
|
13546
|
+
}
|
|
13547
|
+
const current = `${currentParent.repo}#${currentParent.number}`;
|
|
13548
|
+
const sameRepo2 = currentParent.repo.toLowerCase() === childRepo.toLowerCase();
|
|
13549
|
+
const repoFlag = ` --repo ${childRepo}`;
|
|
13550
|
+
payload.current_parent = current;
|
|
13551
|
+
const requested = parseIssueRef(requestedParent);
|
|
13552
|
+
const alreadyThere = currentParent.number === requested.number && (requested.repo ?? childRepo).toLowerCase() === currentParent.repo.toLowerCase();
|
|
13553
|
+
if (!alreadyThere) {
|
|
13554
|
+
payload.corrected_command = `mmi-cli oracle issue unlink-child ${sameRepo2 ? String(currentParent.number) : current} ${childNumber}${repoFlag} && mmi-cli oracle issue edit ${childNumber}${repoFlag} --parent ${requestedParent}`;
|
|
13555
|
+
}
|
|
13556
|
+
return {
|
|
13557
|
+
message: alreadyThere ? `${child2} is already a sub-issue of ${current} \u2014 GitHub allows a sub-issue exactly one parent, so this reparent is a no-op it refuses rather than performs; nothing to do` : `${child2} already has a parent (${current}) \u2014 GitHub allows a sub-issue exactly one parent. Reparenting it under ${requestedParent} would REMOVE it from ${current}; unlink it there first if that is what you want`,
|
|
13558
|
+
payload
|
|
13559
|
+
};
|
|
13560
|
+
}
|
|
13561
|
+
async function classifyReparentFailure(e, runGh, childRepo, childNumber, requestedParent, offendingFlag) {
|
|
13562
|
+
const err = e;
|
|
13563
|
+
if (!isParentConflictMessage((err.stderr || err.message || String(e)).trim())) return void 0;
|
|
13564
|
+
const currentParent = await readIssueParent(runGh, childRepo, childNumber);
|
|
13565
|
+
return buildReparentConflictPayload({ childRepo, childNumber, requestedParent, currentParent, offendingFlag });
|
|
13566
|
+
}
|
|
13567
|
+
function parentLinkFields(result, error, parentRef, childRef, reason = "failed") {
|
|
13568
|
+
if (result) return { parent: result, parentLink: "linked" };
|
|
13569
|
+
if (parentRef !== void 0 && childRef) return {
|
|
13570
|
+
parentLink: reason,
|
|
13571
|
+
parentLinkError: error ?? "parent link was not verified",
|
|
13572
|
+
parentLinkRecovery: `mmi-cli oracle issue link-child ${parentRef} ${childRef}`
|
|
13573
|
+
};
|
|
13574
|
+
return {};
|
|
13575
|
+
}
|
|
13576
|
+
|
|
13577
|
+
// src/issue-comment.ts
|
|
13578
|
+
async function postIssueComment(client, input) {
|
|
13579
|
+
const parsed = parseIssueRef(input.ref);
|
|
13580
|
+
const repo = parsed.repo ?? input.defaultRepo;
|
|
13581
|
+
if (!repo) throw new Error("could not resolve repo \u2014 pass --repo owner/repo");
|
|
13582
|
+
if (input.body.trim().length === 0) throw new Error("comment body is empty");
|
|
13583
|
+
const comment = await client.rest(
|
|
13584
|
+
"POST",
|
|
13585
|
+
`repos/${repo}/issues/${parsed.number}/comments`,
|
|
13586
|
+
{ body: { body: input.body } }
|
|
13587
|
+
);
|
|
13588
|
+
if (!comment?.html_url) throw new Error("GitHub did not return a comment URL");
|
|
13589
|
+
return {
|
|
13590
|
+
number: parsed.number,
|
|
13591
|
+
repo,
|
|
13592
|
+
url: `https://github.com/${repo}/issues/${parsed.number}`,
|
|
13593
|
+
commentUrl: comment.html_url
|
|
13594
|
+
};
|
|
13595
|
+
}
|
|
13596
|
+
|
|
13369
13597
|
// ../infra/board-vocab.mjs
|
|
13370
13598
|
var BOARD_STATUSES = ["Todo", "In Progress", "In Review", "Done"];
|
|
13371
13599
|
|
|
@@ -13468,17 +13696,55 @@ async function boardDoctor(options, deps = {}) {
|
|
|
13468
13696
|
if (claimedItems.length > 0) {
|
|
13469
13697
|
let next = 0;
|
|
13470
13698
|
const fetchOpenPulls = openPullsFetcher(client);
|
|
13699
|
+
const actor = deps.session ?? describeSessionIdentity();
|
|
13471
13700
|
const livenessWorker = async () => {
|
|
13472
13701
|
while (next < claimedItems.length) {
|
|
13473
13702
|
const item = claimedItems[next++];
|
|
13474
13703
|
const evidence = await gatherClaimLiveness(client, item.repository, item.number, fetchOpenPulls);
|
|
13704
|
+
const holderProvenDead = Boolean(
|
|
13705
|
+
evidence.marker && evidence.sessionState === "dead" && isHostSessionId(evidence.marker.session) && !evidence.openPr && !evidence.branch
|
|
13706
|
+
);
|
|
13475
13707
|
const freshMarker = evidence.sessionState === "live" || evidence.sessionState !== "dead" && evidence.markerAgeMs !== void 0 && evidence.markerAgeMs < CLAIM_MARKER_LIVE_MS;
|
|
13476
13708
|
const bits = [
|
|
13477
13709
|
evidence.marker ? `claim marker ${describeClaimMarker(evidence.marker)} ${formatClaimAge(evidence.markerAgeMs)} old` + (evidence.sessionState ? `; local session ${evidence.sessionState}` : "") : evidence.failed.includes("comments") ? "claim marker unreadable" : "no claim marker",
|
|
13478
13710
|
evidence.openPr,
|
|
13479
13711
|
evidence.branch ? `branch ${evidence.branch}` : void 0
|
|
13480
13712
|
].filter((bit) => Boolean(bit)).join("; ");
|
|
13481
|
-
if (
|
|
13713
|
+
if (holderProvenDead && evidence.marker) {
|
|
13714
|
+
const holder = describeClaimMarker(evidence.marker);
|
|
13715
|
+
try {
|
|
13716
|
+
await client.rest("DELETE", `repos/${item.repository}/issues/${item.number}/assignees`, {
|
|
13717
|
+
body: { assignees: item.assignees }
|
|
13718
|
+
});
|
|
13719
|
+
try {
|
|
13720
|
+
const release = {
|
|
13721
|
+
v: 1,
|
|
13722
|
+
session: actor.session,
|
|
13723
|
+
surface: actor.surface,
|
|
13724
|
+
host: actor.host,
|
|
13725
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
13726
|
+
};
|
|
13727
|
+
await postIssueComment(client, { ref: item.ref, body: renderClaimReleaseBody(release) });
|
|
13728
|
+
} catch (e) {
|
|
13729
|
+
process.stderr.write(`warning: ${item.ref} was unclaimed by board doctor, but recording the release marker comment failed: ${ghError(e)}
|
|
13730
|
+
`);
|
|
13731
|
+
}
|
|
13732
|
+
await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, cfg.statusOptions["Todo"]);
|
|
13733
|
+
append(
|
|
13734
|
+
"possibly-abandoned-claim",
|
|
13735
|
+
{ ...item, assignees: [], status: "Todo", statusOptionId: cfg.statusOptions["Todo"] },
|
|
13736
|
+
`${item.ref}: auto-released proven-dead claim holder ${holder} -> Todo (${bits})`
|
|
13737
|
+
);
|
|
13738
|
+
result.fixed += 1;
|
|
13739
|
+
} catch (e) {
|
|
13740
|
+
append(
|
|
13741
|
+
"possibly-abandoned-claim",
|
|
13742
|
+
item,
|
|
13743
|
+
`${item.ref}: proven-dead claim holder ${holder} auto-release failed (${ghError(e)}); ${bits}`
|
|
13744
|
+
);
|
|
13745
|
+
result.failed += 1;
|
|
13746
|
+
}
|
|
13747
|
+
} else if (freshMarker || evidence.openPr || evidence.branch) {
|
|
13482
13748
|
liveClaims.push({ type: "live-claim", item, detail: `${item.ref}: live \u2014 ${bits}` });
|
|
13483
13749
|
} else if (evidence.failed.length > 0) {
|
|
13484
13750
|
liveClaims.push({
|
|
@@ -14161,7 +14427,11 @@ async function readBoard(options, deps = {}) {
|
|
|
14161
14427
|
}
|
|
14162
14428
|
const readComments = claimMarkerCommentsReader(client);
|
|
14163
14429
|
for (const scope of ["primary", "secondary"]) {
|
|
14164
|
-
await markStaleClaimStamps(report[scope], readComments, {
|
|
14430
|
+
await markStaleClaimStamps(report[scope], readComments, {
|
|
14431
|
+
viewerLogin: report.viewer,
|
|
14432
|
+
pullRequests,
|
|
14433
|
+
writable: writableOrUnknown(writable)
|
|
14434
|
+
});
|
|
14165
14435
|
}
|
|
14166
14436
|
for (const scope of ["primary", "secondary"]) {
|
|
14167
14437
|
const filtered = await filterDependencyBlockedClaimables(report[scope].claimable, client, {
|
|
@@ -14171,7 +14441,10 @@ async function readBoard(options, deps = {}) {
|
|
|
14171
14441
|
report.warnings.push(...filtered.warnings);
|
|
14172
14442
|
}
|
|
14173
14443
|
for (const scope of ["primary", "secondary"]) {
|
|
14174
|
-
const demoted = await demoteLiveClaimedClaimables(report[scope], client, void 0, {
|
|
14444
|
+
const demoted = await demoteLiveClaimedClaimables(report[scope], client, void 0, {
|
|
14445
|
+
fetchComments: readComments,
|
|
14446
|
+
viewerLogin: report.viewer
|
|
14447
|
+
});
|
|
14175
14448
|
report.warnings.push(...demoted.warnings);
|
|
14176
14449
|
report.partial = report.partial || demoted.partial;
|
|
14177
14450
|
}
|
|
@@ -14695,11 +14968,22 @@ function claimFromMarker(item, marker, linked = {}, now = Date.now()) {
|
|
|
14695
14968
|
...claimStampStaleness(marker, { activityAt: [...linked.activityAt ?? []], linkedWork: linked.linkedWork }, now)
|
|
14696
14969
|
};
|
|
14697
14970
|
}
|
|
14698
|
-
function
|
|
14971
|
+
function latestClaimMarkerAuthor(comments) {
|
|
14972
|
+
if (!latestClaimMarker(comments)) return void 0;
|
|
14973
|
+
for (let i = comments.length - 1; i >= 0; i -= 1) {
|
|
14974
|
+
if (latestClaimMarker([comments[i]])) {
|
|
14975
|
+
return comments[i].author || comments[i].login || comments[i].user?.login || void 0;
|
|
14976
|
+
}
|
|
14977
|
+
}
|
|
14978
|
+
return void 0;
|
|
14979
|
+
}
|
|
14980
|
+
function classifyClaimableMarker(comments, actor, now = Date.now(), activityAt = [], viewerLogin) {
|
|
14699
14981
|
const marker = comments ? latestClaimMarker(comments) : void 0;
|
|
14700
14982
|
if (!marker) return { verdict: "claimable" };
|
|
14701
|
-
const
|
|
14702
|
-
if (
|
|
14983
|
+
const markerBy = comments ? latestClaimMarkerAuthor(comments) : void 0;
|
|
14984
|
+
if (isSeatClaimMarker(marker, actor, { viewerLogin: viewerLogin ?? markerBy, markerBy, now })) {
|
|
14985
|
+
return { verdict: "owned", holder: describeClaimMarker(marker) };
|
|
14986
|
+
}
|
|
14703
14987
|
if (!claimStampStaleness(marker, { activityAt }, now).stale) {
|
|
14704
14988
|
return { verdict: "taken", holder: describeClaimMarker(marker) };
|
|
14705
14989
|
}
|
|
@@ -14714,7 +14998,10 @@ function claimMarkerCommentsReader(client) {
|
|
|
14714
14998
|
if (item.detailsUnavailable) return void 0;
|
|
14715
14999
|
if (item.details && !item.details.commentsUnread && !item.details.omittedComments) return item.details.comments;
|
|
14716
15000
|
const comments = await client.restPaginate(`repos/${item.repository}/issues/${item.number}/comments`);
|
|
14717
|
-
return comments.map((comment) => ({
|
|
15001
|
+
return comments.map((comment) => ({
|
|
15002
|
+
body: comment.body ?? "",
|
|
15003
|
+
...comment.user?.login ? { author: comment.user.login } : {}
|
|
15004
|
+
}));
|
|
14718
15005
|
})();
|
|
14719
15006
|
cache.set(item, read);
|
|
14720
15007
|
}
|
|
@@ -14725,6 +15012,7 @@ async function markStaleClaimStamps(scope, readComments, options = {}) {
|
|
|
14725
15012
|
const actor = options.actor ?? describeSessionIdentity();
|
|
14726
15013
|
const now = options.now ?? Date.now();
|
|
14727
15014
|
const promoted = [];
|
|
15015
|
+
const adoptedOwned = [];
|
|
14728
15016
|
const buckets = ["userOwned", "claimable", "unownedInFlight", "taken"];
|
|
14729
15017
|
const issues = buckets.flatMap((bucket) => scope[bucket]).filter((item) => item.contentType === "Issue");
|
|
14730
15018
|
const read = /* @__PURE__ */ new Map();
|
|
@@ -14744,19 +15032,35 @@ async function markStaleClaimStamps(scope, readComments, options = {}) {
|
|
|
14744
15032
|
const comments = read.get(item);
|
|
14745
15033
|
const marker = comments ? latestClaimMarker(comments) : void 0;
|
|
14746
15034
|
if (!marker) continue;
|
|
15035
|
+
const markerBy = comments ? latestClaimMarkerAuthor(comments) : void 0;
|
|
15036
|
+
const effectiveViewer = options.viewerLogin ?? (bucket === "userOwned" ? item.assignees[0] : markerBy);
|
|
15037
|
+
const ownSeat = isSeatClaimMarker(marker, actor, {
|
|
15038
|
+
viewerLogin: effectiveViewer,
|
|
15039
|
+
markerBy: markerBy ?? (bucket === "userOwned" ? item.assignees[0] : void 0),
|
|
15040
|
+
now
|
|
15041
|
+
});
|
|
14747
15042
|
const prs = (options.pullRequests ?? []).filter((pr) => pr.closesIssues.some((ref) => ref.toLowerCase() === item.ref.toLowerCase()));
|
|
14748
15043
|
item.claim = claimFromMarker(item, marker, {
|
|
14749
15044
|
activityAt: prs.map((pr) => pr.updatedAt),
|
|
14750
15045
|
linkedWork: prs.some((pr) => !pr.updatedAt)
|
|
14751
15046
|
}, now);
|
|
15047
|
+
if (ownSeat && bucket !== "userOwned" && bucket !== "claimable") {
|
|
15048
|
+
kept.pop();
|
|
15049
|
+
adoptedOwned.push(item);
|
|
15050
|
+
continue;
|
|
15051
|
+
}
|
|
14752
15052
|
const movable = bucket === "userOwned" || bucket === "unownedInFlight";
|
|
14753
|
-
if (movable && item.claim.stale &&
|
|
15053
|
+
if (movable && item.claim.stale && !ownSeat && (!options.writable || options.writable.has(item.repository.toLowerCase()))) {
|
|
14754
15054
|
kept.pop();
|
|
14755
15055
|
promoted.push(item);
|
|
14756
15056
|
}
|
|
14757
15057
|
}
|
|
14758
15058
|
scope[bucket] = kept;
|
|
14759
15059
|
}
|
|
15060
|
+
if (adoptedOwned.length) {
|
|
15061
|
+
scope.userOwned.push(...adoptedOwned);
|
|
15062
|
+
scope.userOwned.sort(compareItems);
|
|
15063
|
+
}
|
|
14760
15064
|
if (promoted.length) {
|
|
14761
15065
|
scope.claimable.push(...promoted);
|
|
14762
15066
|
scope.claimable.sort(compareItems);
|
|
@@ -14785,7 +15089,7 @@ async function demoteLiveClaimedClaimables(scope, client, actor = describeSessio
|
|
|
14785
15089
|
warnings.push(`${item.ref} left claimable: claim-marker evidence could not be read`);
|
|
14786
15090
|
continue;
|
|
14787
15091
|
}
|
|
14788
|
-
const verdict = classifyClaimableMarker(comments, actor, deps.now);
|
|
15092
|
+
const verdict = classifyClaimableMarker(comments, actor, deps.now, [], deps.viewerLogin);
|
|
14789
15093
|
if (verdict.verdict === "claimable") {
|
|
14790
15094
|
stillClaimable.push(item);
|
|
14791
15095
|
} else if (verdict.verdict === "owned") {
|
|
@@ -14811,7 +15115,7 @@ var CLAIM_SESSION_ACTIVITY_MS = 30 * 6e4;
|
|
|
14811
15115
|
var CLAIM_SESSION_PROBE_CACHE_MS = 6e4;
|
|
14812
15116
|
var claimSessionProbeCache = /* @__PURE__ */ new Map();
|
|
14813
15117
|
function probeLocalClaimSession(marker, now = Date.now()) {
|
|
14814
|
-
if (!marker.session || !marker.host || marker.host.toLowerCase() !== (0, import_node_os11.hostname)().toLowerCase()) return void 0;
|
|
15118
|
+
if (!isHostSessionId(marker.session) || !marker.host || marker.host.toLowerCase() !== (0, import_node_os11.hostname)().toLowerCase()) return void 0;
|
|
14815
15119
|
if (!marker.surface?.toLowerCase().startsWith("claude")) return void 0;
|
|
14816
15120
|
const cacheKey = `${marker.host.toLowerCase()}/${marker.session}`;
|
|
14817
15121
|
const cached = claimSessionProbeCache.get(cacheKey);
|
|
@@ -14989,8 +15293,19 @@ function laneOwnership(marker, current) {
|
|
|
14989
15293
|
}
|
|
14990
15294
|
return "unknown";
|
|
14991
15295
|
}
|
|
15296
|
+
function isSeatClaimMarker(marker, current, context = {}) {
|
|
15297
|
+
if (!marker) return false;
|
|
15298
|
+
if (laneOwnership(marker, current) === "mine") return true;
|
|
15299
|
+
if (!context.viewerLogin || !context.markerBy) return false;
|
|
15300
|
+
if (context.markerBy.toLowerCase() !== context.viewerLogin.toLowerCase()) return false;
|
|
15301
|
+
if (!sameClaimHost(marker.host, current.host)) return false;
|
|
15302
|
+
if (!marker.surface || !current.surface || marker.surface !== current.surface) return false;
|
|
15303
|
+
if (!isHostSessionId(marker.session)) return false;
|
|
15304
|
+
const state = context.sessionState ?? probeLocalClaimSession(marker, context.now);
|
|
15305
|
+
return state === "dead";
|
|
15306
|
+
}
|
|
14992
15307
|
function laneResumeEvidence(evidence) {
|
|
14993
|
-
if (!evidence.marker || evidence.sessionState !== "dead"
|
|
15308
|
+
if (!evidence.marker || evidence.sessionState !== "dead") return void 0;
|
|
14994
15309
|
if (!isHostSessionId(evidence.marker.session)) return void 0;
|
|
14995
15310
|
const artifacts = [evidence.openPr, evidence.branch ? `live branch ${evidence.branch}` : void 0].filter(Boolean);
|
|
14996
15311
|
return `prior claim by lane ${describeClaimMarker(evidence.marker)} (@${evidence.markerBy}, ${formatClaimAge(evidence.markerAgeMs)} old) is verifiably not running on this host \u2014 its local session transcript is gone or stale${artifacts.length ? `; resuming over ${artifacts.join("; ")}` : ""}`;
|
|
@@ -15002,13 +15317,17 @@ async function checkLaneContest(client, item, actor = describeSessionIdentity(),
|
|
|
15002
15317
|
evidence = await gatherClaimLiveness(livenessClient, item.repository, item.number, openPullsFetcher(livenessClient));
|
|
15003
15318
|
}
|
|
15004
15319
|
const ownership = laneOwnership(evidence.marker, actor);
|
|
15005
|
-
const
|
|
15006
|
-
|
|
15007
|
-
|
|
15008
|
-
|
|
15009
|
-
|
|
15320
|
+
const ownSeat = isSeatClaimMarker(evidence.marker, actor, {
|
|
15321
|
+
viewerLogin,
|
|
15322
|
+
markerBy: evidence.markerBy,
|
|
15323
|
+
sessionState: evidence.sessionState
|
|
15324
|
+
});
|
|
15325
|
+
const resume = ownership !== "mine" && ownSeat ? laneResumeEvidence(evidence) : void 0;
|
|
15326
|
+
const live = ownSeat ? [] : liveEvidenceLines(evidence, item.repository);
|
|
15327
|
+
const unverifiable = ownSeat ? [] : evidence.failed;
|
|
15328
|
+
const contested = !ownSeat && !resume && (live.length > 0 || unverifiable.length > 0);
|
|
15010
15329
|
const carry = staleWorkLines(evidence);
|
|
15011
|
-
const staleStamp = !contested && !resume &&
|
|
15330
|
+
const staleStamp = !contested && !resume && !ownSeat && evidence.marker ? `stale claim stamp ${describeClaimMarker(evidence.marker)} (idle ${formatClaimAge(claimStampStaleness(evidence.marker, { activityAt: [evidence.lastCommentAt] }).idleMs)})${carry.length ? `; continue its ${carry.join(" and ")}` : ""}` : void 0;
|
|
15012
15331
|
return {
|
|
15013
15332
|
contested,
|
|
15014
15333
|
live,
|
|
@@ -15043,236 +15362,6 @@ function ghError(e) {
|
|
|
15043
15362
|
// src/board-claim-move.ts
|
|
15044
15363
|
init_github_client();
|
|
15045
15364
|
init_github_quota();
|
|
15046
|
-
|
|
15047
|
-
// src/sub-issue.ts
|
|
15048
|
-
init_error_codes();
|
|
15049
|
-
init_github_quota();
|
|
15050
|
-
function buildResolveIdArgs(ref) {
|
|
15051
|
-
const args = ["issue", "view", String(ref.number), "--json", "id", "--jq", ".id"];
|
|
15052
|
-
if (ref.repo) args.push("--repo", ref.repo);
|
|
15053
|
-
return args;
|
|
15054
|
-
}
|
|
15055
|
-
function buildAddSubIssueArgs(parentId, subIssueId) {
|
|
15056
|
-
if (!parentId) throw new Error("addSubIssue: parentId is required");
|
|
15057
|
-
if (!subIssueId) throw new Error("addSubIssue: subIssueId is required");
|
|
15058
|
-
return [
|
|
15059
|
-
"api",
|
|
15060
|
-
"graphql",
|
|
15061
|
-
"-f",
|
|
15062
|
-
"query=mutation($p:ID!,$c:ID!){addSubIssue(input:{issueId:$p,subIssueId:$c}){issue{number subIssues{totalCount}} subIssue{number}}}",
|
|
15063
|
-
"-f",
|
|
15064
|
-
`p=${parentId}`,
|
|
15065
|
-
"-f",
|
|
15066
|
-
`c=${subIssueId}`
|
|
15067
|
-
];
|
|
15068
|
-
}
|
|
15069
|
-
function parseAddSubIssueResult(stdout) {
|
|
15070
|
-
try {
|
|
15071
|
-
const issue = JSON.parse(stdout)?.data?.addSubIssue;
|
|
15072
|
-
const parentNumber = issue?.issue?.number;
|
|
15073
|
-
const subIssueNumber = issue?.subIssue?.number;
|
|
15074
|
-
const totalCount = issue?.issue?.subIssues?.totalCount;
|
|
15075
|
-
if (typeof parentNumber !== "number" || typeof subIssueNumber !== "number") return void 0;
|
|
15076
|
-
return { parentNumber, subIssueNumber, totalCount: typeof totalCount === "number" ? totalCount : 0 };
|
|
15077
|
-
} catch {
|
|
15078
|
-
return void 0;
|
|
15079
|
-
}
|
|
15080
|
-
}
|
|
15081
|
-
var RESOLVE_ID_TIMEOUT_MS = 1e4;
|
|
15082
|
-
async function resolveIssueNodeId(runGh, ref, fallbackRepo) {
|
|
15083
|
-
const resolved = ref.repo ? ref : { ...ref, repo: fallbackRepo };
|
|
15084
|
-
const id = (await runGh(buildResolveIdArgs(resolved), RESOLVE_ID_TIMEOUT_MS)).trim();
|
|
15085
|
-
if (!id) throw new Error(`could not resolve node id for issue #${ref.number}${resolved.repo ? ` in ${resolved.repo}` : ""}`);
|
|
15086
|
-
return id;
|
|
15087
|
-
}
|
|
15088
|
-
var SubIssueRestFallbackError = class extends Error {
|
|
15089
|
-
constructor(message2, reason) {
|
|
15090
|
-
super(message2);
|
|
15091
|
-
this.reason = reason;
|
|
15092
|
-
this.name = "SubIssueRestFallbackError";
|
|
15093
|
-
}
|
|
15094
|
-
reason;
|
|
15095
|
-
};
|
|
15096
|
-
async function linkSubIssue(runGh, parentRef, childRef, defaultRepo) {
|
|
15097
|
-
const parent = parseIssueRef(parentRef);
|
|
15098
|
-
const child2 = parseIssueRef(childRef);
|
|
15099
|
-
try {
|
|
15100
|
-
const parentId = await resolveIssueNodeId(runGh, parent, defaultRepo);
|
|
15101
|
-
const subIssueId = await resolveIssueNodeId(runGh, child2, defaultRepo);
|
|
15102
|
-
const stdout = await runGh(buildAddSubIssueArgs(parentId, subIssueId), GH_MUTATION_TIMEOUT_MS);
|
|
15103
|
-
const result = parseAddSubIssueResult(stdout);
|
|
15104
|
-
if (!result) throw new Error(`addSubIssue returned an unexpected response:
|
|
15105
|
-
${stdout.trim() || "(empty)"}`);
|
|
15106
|
-
return result;
|
|
15107
|
-
} catch (graphqlError) {
|
|
15108
|
-
if (!isRateLimitRefusal(graphqlError)) throw graphqlError;
|
|
15109
|
-
const parentRepo = parent.repo ?? defaultRepo;
|
|
15110
|
-
const childRepo = child2.repo ?? defaultRepo;
|
|
15111
|
-
if (!parentRepo || !childRepo) throw graphqlError;
|
|
15112
|
-
try {
|
|
15113
|
-
const childPath = `repos/${childRepo}/issues/${child2.number}`;
|
|
15114
|
-
const childIssue = JSON.parse(await runGh(["api", childPath], RESOLVE_ID_TIMEOUT_MS));
|
|
15115
|
-
if (!Number.isSafeInteger(childIssue.id) || !childIssue.id) throw new Error(`REST issue ${childRepo}#${child2.number} has no numeric id`);
|
|
15116
|
-
await runGh([
|
|
15117
|
-
"api",
|
|
15118
|
-
"-X",
|
|
15119
|
-
"POST",
|
|
15120
|
-
`repos/${parentRepo}/issues/${parent.number}/sub_issues`,
|
|
15121
|
-
"-F",
|
|
15122
|
-
`sub_issue_id=${childIssue.id}`
|
|
15123
|
-
], GH_MUTATION_TIMEOUT_MS);
|
|
15124
|
-
const confirmed = JSON.parse(await runGh(["api", childPath], RESOLVE_ID_TIMEOUT_MS));
|
|
15125
|
-
const actual = resolveParentField(confirmed);
|
|
15126
|
-
if (!("parent" in actual) || !actual.parent || actual.parent.repo.toLowerCase() !== parentRepo.toLowerCase() || actual.parent.number !== parent.number) {
|
|
15127
|
-
throw new Error(`REST link not verified for ${childRepo}#${child2.number}: ${"parentReadError" in actual ? actual.parentReadError : "parent differs or is absent"}`);
|
|
15128
|
-
}
|
|
15129
|
-
let totalCount = 0;
|
|
15130
|
-
try {
|
|
15131
|
-
const parentIssue = JSON.parse(await runGh(["api", `repos/${parentRepo}/issues/${parent.number}`], RESOLVE_ID_TIMEOUT_MS));
|
|
15132
|
-
if (typeof parentIssue.sub_issues_summary?.total === "number") totalCount = parentIssue.sub_issues_summary.total;
|
|
15133
|
-
} catch {
|
|
15134
|
-
}
|
|
15135
|
-
return { parentNumber: parent.number, subIssueNumber: child2.number, totalCount };
|
|
15136
|
-
} catch (restError) {
|
|
15137
|
-
const err = restError;
|
|
15138
|
-
const detail = (err.stderr || err.message || String(restError)).trim();
|
|
15139
|
-
throw new SubIssueRestFallbackError(
|
|
15140
|
-
`GraphQL link rate-limited; REST link not verified: ${detail}`,
|
|
15141
|
-
isRateLimitRefusal(restError) ? "rate_limited" : "failed"
|
|
15142
|
-
);
|
|
15143
|
-
}
|
|
15144
|
-
}
|
|
15145
|
-
}
|
|
15146
|
-
function buildRemoveSubIssueArgs(parentId, subIssueId) {
|
|
15147
|
-
if (!parentId) throw new Error("removeSubIssue: parentId is required");
|
|
15148
|
-
if (!subIssueId) throw new Error("removeSubIssue: subIssueId is required");
|
|
15149
|
-
return [
|
|
15150
|
-
"api",
|
|
15151
|
-
"graphql",
|
|
15152
|
-
"-f",
|
|
15153
|
-
"query=mutation($p:ID!,$c:ID!){removeSubIssue(input:{issueId:$p,subIssueId:$c}){issue{number subIssues{totalCount}} subIssue{number}}}",
|
|
15154
|
-
"-f",
|
|
15155
|
-
`p=${parentId}`,
|
|
15156
|
-
"-f",
|
|
15157
|
-
`c=${subIssueId}`
|
|
15158
|
-
];
|
|
15159
|
-
}
|
|
15160
|
-
function parseRemoveSubIssueResult(stdout) {
|
|
15161
|
-
try {
|
|
15162
|
-
const issue = JSON.parse(stdout)?.data?.removeSubIssue;
|
|
15163
|
-
const parentNumber = issue?.issue?.number;
|
|
15164
|
-
const subIssueNumber = issue?.subIssue?.number;
|
|
15165
|
-
const totalCount = issue?.issue?.subIssues?.totalCount;
|
|
15166
|
-
if (typeof parentNumber !== "number" || typeof subIssueNumber !== "number") return void 0;
|
|
15167
|
-
return { parentNumber, subIssueNumber, totalCount: typeof totalCount === "number" ? totalCount : 0 };
|
|
15168
|
-
} catch {
|
|
15169
|
-
return void 0;
|
|
15170
|
-
}
|
|
15171
|
-
}
|
|
15172
|
-
async function unlinkSubIssue(runGh, parentRef, childRef, defaultRepo) {
|
|
15173
|
-
const parent = parseIssueRef(parentRef);
|
|
15174
|
-
const child2 = parseIssueRef(childRef);
|
|
15175
|
-
const parentId = await resolveIssueNodeId(runGh, parent, defaultRepo);
|
|
15176
|
-
const subIssueId = await resolveIssueNodeId(runGh, child2, defaultRepo);
|
|
15177
|
-
const stdout = await runGh(buildRemoveSubIssueArgs(parentId, subIssueId), GH_MUTATION_TIMEOUT_MS);
|
|
15178
|
-
const result = parseRemoveSubIssueResult(stdout);
|
|
15179
|
-
if (!result) throw new Error(`removeSubIssue returned an unexpected response:
|
|
15180
|
-
${stdout.trim() || "(empty)"}`);
|
|
15181
|
-
return result;
|
|
15182
|
-
}
|
|
15183
|
-
function parseParentIssueUrl(apiUrl) {
|
|
15184
|
-
if (typeof apiUrl !== "string" || !apiUrl.trim()) return null;
|
|
15185
|
-
const match = /\/repos\/([^/]+\/[^/]+)\/issues\/(\d+)\/?$/.exec(apiUrl.trim());
|
|
15186
|
-
if (!match) return null;
|
|
15187
|
-
const [, repo, number] = match;
|
|
15188
|
-
return { repo, number: Number(number), url: `https://github.com/${repo}/issues/${number}` };
|
|
15189
|
-
}
|
|
15190
|
-
function resolveParentField(payload) {
|
|
15191
|
-
if (!payload || typeof payload !== "object") {
|
|
15192
|
-
return { parentReadError: "issue payload was empty or not an object" };
|
|
15193
|
-
}
|
|
15194
|
-
if (!("sub_issues_summary" in payload)) {
|
|
15195
|
-
return { parentReadError: "issue payload carries no sub-issue fields \u2014 parent state is unknown, not absent" };
|
|
15196
|
-
}
|
|
15197
|
-
const raw = payload.parent_issue_url;
|
|
15198
|
-
if (raw === void 0 || raw === null) return { parent: null };
|
|
15199
|
-
const parsed = typeof raw === "string" ? parseParentIssueUrl(raw) : null;
|
|
15200
|
-
return parsed ? { parent: parsed } : { parentReadError: `unrecognised parent_issue_url: ${JSON.stringify(raw)}` };
|
|
15201
|
-
}
|
|
15202
|
-
async function readIssueParent(runGh, repo, number) {
|
|
15203
|
-
try {
|
|
15204
|
-
const stdout = await runGh(["api", `repos/${repo}/issues/${number}`], RESOLVE_ID_TIMEOUT_MS);
|
|
15205
|
-
const resolved = resolveParentField(JSON.parse(stdout));
|
|
15206
|
-
return "parent" in resolved ? resolved.parent : void 0;
|
|
15207
|
-
} catch {
|
|
15208
|
-
return void 0;
|
|
15209
|
-
}
|
|
15210
|
-
}
|
|
15211
|
-
function isParentConflictMessage(text) {
|
|
15212
|
-
return typeof text === "string" && /sub[- ]?issue may only have one parent/i.test(text);
|
|
15213
|
-
}
|
|
15214
|
-
function buildReparentConflictPayload(input) {
|
|
15215
|
-
const { childRepo, childNumber, requestedParent, currentParent, offendingFlag } = input;
|
|
15216
|
-
const child2 = `${childRepo}#${childNumber}`;
|
|
15217
|
-
const payload = { code: ERROR_CODES.ERR_STATE_CONFLICT };
|
|
15218
|
-
if (offendingFlag) payload.offending_flag = offendingFlag;
|
|
15219
|
-
if (!currentParent) {
|
|
15220
|
-
return {
|
|
15221
|
-
message: currentParent === null ? `${child2} cannot be reparented under ${requestedParent} \u2014 GitHub refused it under the one-parent rule, but reading the issue back showed no parent at all. Something changed it concurrently; re-read with \`mmi-cli oracle issue view ${childNumber} --repo ${childRepo}\` and retry` : `${child2} cannot be reparented under ${requestedParent} \u2014 GitHub allows a sub-issue exactly one parent and it already has one. Its current parent could not be read; check with \`mmi-cli oracle issue view ${childNumber} --repo ${childRepo}\``,
|
|
15222
|
-
payload
|
|
15223
|
-
};
|
|
15224
|
-
}
|
|
15225
|
-
const current = `${currentParent.repo}#${currentParent.number}`;
|
|
15226
|
-
const sameRepo2 = currentParent.repo.toLowerCase() === childRepo.toLowerCase();
|
|
15227
|
-
const repoFlag = ` --repo ${childRepo}`;
|
|
15228
|
-
payload.current_parent = current;
|
|
15229
|
-
const requested = parseIssueRef(requestedParent);
|
|
15230
|
-
const alreadyThere = currentParent.number === requested.number && (requested.repo ?? childRepo).toLowerCase() === currentParent.repo.toLowerCase();
|
|
15231
|
-
if (!alreadyThere) {
|
|
15232
|
-
payload.corrected_command = `mmi-cli oracle issue unlink-child ${sameRepo2 ? String(currentParent.number) : current} ${childNumber}${repoFlag} && mmi-cli oracle issue edit ${childNumber}${repoFlag} --parent ${requestedParent}`;
|
|
15233
|
-
}
|
|
15234
|
-
return {
|
|
15235
|
-
message: alreadyThere ? `${child2} is already a sub-issue of ${current} \u2014 GitHub allows a sub-issue exactly one parent, so this reparent is a no-op it refuses rather than performs; nothing to do` : `${child2} already has a parent (${current}) \u2014 GitHub allows a sub-issue exactly one parent. Reparenting it under ${requestedParent} would REMOVE it from ${current}; unlink it there first if that is what you want`,
|
|
15236
|
-
payload
|
|
15237
|
-
};
|
|
15238
|
-
}
|
|
15239
|
-
async function classifyReparentFailure(e, runGh, childRepo, childNumber, requestedParent, offendingFlag) {
|
|
15240
|
-
const err = e;
|
|
15241
|
-
if (!isParentConflictMessage((err.stderr || err.message || String(e)).trim())) return void 0;
|
|
15242
|
-
const currentParent = await readIssueParent(runGh, childRepo, childNumber);
|
|
15243
|
-
return buildReparentConflictPayload({ childRepo, childNumber, requestedParent, currentParent, offendingFlag });
|
|
15244
|
-
}
|
|
15245
|
-
function parentLinkFields(result, error, parentRef, childRef, reason = "failed") {
|
|
15246
|
-
if (result) return { parent: result, parentLink: "linked" };
|
|
15247
|
-
if (parentRef !== void 0 && childRef) return {
|
|
15248
|
-
parentLink: reason,
|
|
15249
|
-
parentLinkError: error ?? "parent link was not verified",
|
|
15250
|
-
parentLinkRecovery: `mmi-cli oracle issue link-child ${parentRef} ${childRef}`
|
|
15251
|
-
};
|
|
15252
|
-
return {};
|
|
15253
|
-
}
|
|
15254
|
-
|
|
15255
|
-
// src/issue-comment.ts
|
|
15256
|
-
async function postIssueComment(client, input) {
|
|
15257
|
-
const parsed = parseIssueRef(input.ref);
|
|
15258
|
-
const repo = parsed.repo ?? input.defaultRepo;
|
|
15259
|
-
if (!repo) throw new Error("could not resolve repo \u2014 pass --repo owner/repo");
|
|
15260
|
-
if (input.body.trim().length === 0) throw new Error("comment body is empty");
|
|
15261
|
-
const comment = await client.rest(
|
|
15262
|
-
"POST",
|
|
15263
|
-
`repos/${repo}/issues/${parsed.number}/comments`,
|
|
15264
|
-
{ body: { body: input.body } }
|
|
15265
|
-
);
|
|
15266
|
-
if (!comment?.html_url) throw new Error("GitHub did not return a comment URL");
|
|
15267
|
-
return {
|
|
15268
|
-
number: parsed.number,
|
|
15269
|
-
repo,
|
|
15270
|
-
url: `https://github.com/${repo}/issues/${parsed.number}`,
|
|
15271
|
-
commentUrl: comment.html_url
|
|
15272
|
-
};
|
|
15273
|
-
}
|
|
15274
|
-
|
|
15275
|
-
// src/board-claim-move.ts
|
|
15276
15365
|
var CLAIM_CONCURRENCY = 5;
|
|
15277
15366
|
function withGatedWriteChainHint(message2) {
|
|
15278
15367
|
return `${message2}
|
|
@@ -15694,7 +15783,7 @@ async function unclaimBoardIssue(options, deps = {}) {
|
|
|
15694
15783
|
}
|
|
15695
15784
|
const toStatus = options.toStatus ?? "Todo";
|
|
15696
15785
|
if (item.contentType === "Issue") {
|
|
15697
|
-
const contest = await checkLaneContest(client, item);
|
|
15786
|
+
const contest = await checkLaneContest(client, item, deps.session ?? describeSessionIdentity(), viewer);
|
|
15698
15787
|
if (contest.contested) {
|
|
15699
15788
|
if (!options.force) {
|
|
15700
15789
|
throw new Error(withGatedWriteChainHint(laneContestMessage(item.ref, contest, "unclaim")));
|
|
@@ -19104,10 +19193,10 @@ var rollout_plan_default = {
|
|
|
19104
19193
|
note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
|
|
19105
19194
|
},
|
|
19106
19195
|
baseline: {
|
|
19107
|
-
version: "4.5.
|
|
19108
|
-
tag: "v4.5.
|
|
19109
|
-
commit: "
|
|
19110
|
-
npm: "@mutmutco/cli@4.5.
|
|
19196
|
+
version: "4.5.176",
|
|
19197
|
+
tag: "v4.5.176",
|
|
19198
|
+
commit: "d72dd40c950d",
|
|
19199
|
+
npm: "@mutmutco/cli@4.5.176"
|
|
19111
19200
|
},
|
|
19112
19201
|
exitCriterion: "fleet-n-of-n",
|
|
19113
19202
|
hubOnlyShortcut: "forbidden",
|
|
@@ -19124,14 +19213,14 @@ var rollout_plan_default = {
|
|
|
19124
19213
|
repo: "mutmutco/mmi-hub",
|
|
19125
19214
|
role: "canary",
|
|
19126
19215
|
schedule: "train",
|
|
19127
|
-
v3Target: "v4.5.
|
|
19216
|
+
v3Target: "v4.5.176"
|
|
19128
19217
|
}
|
|
19129
19218
|
],
|
|
19130
19219
|
rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
|
|
19131
19220
|
rollback: {
|
|
19132
19221
|
independent: true,
|
|
19133
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
19134
|
-
v3Target: "v4.5.
|
|
19222
|
+
mechanism: "npm dist-tag latest -> 4.5.176 and redeploy the Hub Lambda from tag v4.5.176 (d72dd40c950d); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
19223
|
+
v3Target: "v4.5.176 (@mutmutco/cli@4.5.176, tag commit d72dd40c950d \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
19135
19224
|
}
|
|
19136
19225
|
},
|
|
19137
19226
|
{
|
|
@@ -32417,6 +32506,76 @@ async function waitBoardEvents(since, waitS, deps) {
|
|
|
32417
32506
|
|
|
32418
32507
|
// src/board-commands.ts
|
|
32419
32508
|
init_github_quota();
|
|
32509
|
+
init_github_client();
|
|
32510
|
+
|
|
32511
|
+
// src/issue-decision.ts
|
|
32512
|
+
var DECISION_KINDS = ["decision", "correction", "approval"];
|
|
32513
|
+
var DECISION_SOURCES = ["typed", "ask-user", "relay"];
|
|
32514
|
+
var MARKER_PATTERN = /<!--\s*mmi-decision:\s*(\{[\s\S]*?\})\s*-->/g;
|
|
32515
|
+
var JSON_GT_ESCAPE = "\\u003e";
|
|
32516
|
+
function decisionReminders(m) {
|
|
32517
|
+
const out = [];
|
|
32518
|
+
if (m.via === "relay" && !m.by) out.push("reminder: name the relaying session with --by <endpoint> so readers can trace the relay");
|
|
32519
|
+
if (m.kind === "correction" && !m.learning) out.push("reminder: a correction also gets its learning note (jerv_learning_note); pass it as --learning when you have it");
|
|
32520
|
+
return out;
|
|
32521
|
+
}
|
|
32522
|
+
function buildDecisionMarker(input) {
|
|
32523
|
+
const kind = input.kind;
|
|
32524
|
+
if (!DECISION_KINDS.includes(kind)) throw new Error(`--kind must be one of: ${DECISION_KINDS.join(", ")}`);
|
|
32525
|
+
const via = input.via;
|
|
32526
|
+
if (!DECISION_SOURCES.includes(via)) throw new Error(`--via must be one of: ${DECISION_SOURCES.join(", ")}`);
|
|
32527
|
+
const quote = (input.quote ?? "").trim();
|
|
32528
|
+
if (!quote) throw new Error("--quote is required: the owner's words verbatim, never a paraphrase");
|
|
32529
|
+
const at = input.at ?? "";
|
|
32530
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(at) || Number.isNaN(Date.parse(at))) throw new Error("--at must be an ISO time, e.g. 2026-10-17T10:29:00Z");
|
|
32531
|
+
return {
|
|
32532
|
+
v: 1,
|
|
32533
|
+
kind,
|
|
32534
|
+
quote,
|
|
32535
|
+
at: new Date(at).toISOString(),
|
|
32536
|
+
via,
|
|
32537
|
+
...input.by?.trim() ? { by: input.by.trim() } : {},
|
|
32538
|
+
...input.learning?.trim() ? { learning: input.learning.trim() } : {}
|
|
32539
|
+
};
|
|
32540
|
+
}
|
|
32541
|
+
function renderDecisionComment(m) {
|
|
32542
|
+
const source = m.via === "relay" ? `relayed by ${m.by}` : m.via;
|
|
32543
|
+
const quoted = m.quote.split("\n").map((l) => `> ${l}`).join("\n");
|
|
32544
|
+
const json = JSON.stringify(m).replace(/-->/g, `--${JSON_GT_ESCAPE}`);
|
|
32545
|
+
return `**Owner ${m.kind}** (${source}, ${m.at})
|
|
32546
|
+
|
|
32547
|
+
${quoted}${m.learning ? `
|
|
32548
|
+
|
|
32549
|
+
Learning record: ${m.learning}` : ""}
|
|
32550
|
+
|
|
32551
|
+
<!-- mmi-decision: ${json} -->`;
|
|
32552
|
+
}
|
|
32553
|
+
function parseDecisionMarkers(comments) {
|
|
32554
|
+
const out = [];
|
|
32555
|
+
for (const comment of comments ?? []) {
|
|
32556
|
+
for (const match of (comment.body ?? "").matchAll(MARKER_PATTERN)) {
|
|
32557
|
+
try {
|
|
32558
|
+
const m = JSON.parse(match[1]);
|
|
32559
|
+
if (m.v === 1 && DECISION_KINDS.includes(m.kind) && DECISION_SOURCES.includes(m.via) && typeof m.quote === "string" && typeof m.at === "string") out.push(m);
|
|
32560
|
+
} catch {
|
|
32561
|
+
}
|
|
32562
|
+
}
|
|
32563
|
+
}
|
|
32564
|
+
return out;
|
|
32565
|
+
}
|
|
32566
|
+
function formatDecisionLines(ref, markers) {
|
|
32567
|
+
if (!markers.length) return [];
|
|
32568
|
+
return [
|
|
32569
|
+
`Owner rulings on ${ref} (${markers.length}) \u2014 read before acting:`,
|
|
32570
|
+
...markers.map((m) => ` [${m.kind}] ${m.at} ${m.via === "relay" ? `relay ${m.by}` : m.via}: "${m.quote.replace(/\s+/g, " ")}"${m.learning ? ` (learning ${m.learning})` : ""}`)
|
|
32571
|
+
];
|
|
32572
|
+
}
|
|
32573
|
+
async function readIssueDecisions(client, repo, number) {
|
|
32574
|
+
const comments = await client.restPaginate(`repos/${repo}/issues/${number}/comments`);
|
|
32575
|
+
return parseDecisionMarkers(comments);
|
|
32576
|
+
}
|
|
32577
|
+
|
|
32578
|
+
// src/board-commands.ts
|
|
32420
32579
|
function refuseRateLimited(e, json) {
|
|
32421
32580
|
if (!isRateLimitedError(e)) return false;
|
|
32422
32581
|
if (json) console.log(JSON.stringify(e.receipt));
|
|
@@ -32486,6 +32645,13 @@ function registerBoardCommands(program3) {
|
|
|
32486
32645
|
else console.log(`Warning: ${warning}`);
|
|
32487
32646
|
}
|
|
32488
32647
|
}
|
|
32648
|
+
async function claimDecisions(item) {
|
|
32649
|
+
try {
|
|
32650
|
+
return { decisions: await readIssueDecisions(defaultGitHubClient(), item.repository, item.number) };
|
|
32651
|
+
} catch (e) {
|
|
32652
|
+
return { warning: `owner rulings on ${item.repository}#${item.number} unread (${e.message}) \u2014 run \`mmi-cli oracle issue view ${item.number} --repo ${item.repository} --comments\` before acting` };
|
|
32653
|
+
}
|
|
32654
|
+
}
|
|
32489
32655
|
function claimVerdict(ref, result) {
|
|
32490
32656
|
const holder = formatClaimHolder(result.holder);
|
|
32491
32657
|
const previousHolder = result.staleStamp ?? (result.previousHolder ? formatClaimHolder(result.previousHolder) : "another lane");
|
|
@@ -32541,9 +32707,11 @@ function registerBoardCommands(program3) {
|
|
|
32541
32707
|
check: o.check,
|
|
32542
32708
|
allowPartial: o.allowPartial
|
|
32543
32709
|
}, { snapshot: registryClientDeps(config) });
|
|
32544
|
-
|
|
32710
|
+
const ruled = await claimDecisions(result.item);
|
|
32711
|
+
if (o.json) return console.log(JSON.stringify({ ...result, ...ruled.decisions ? { decisions: ruled.decisions } : {}, ...ruled.warning ? { warnings: [...result.warnings ?? [], ruled.warning] } : {} }));
|
|
32545
32712
|
console.log(claimVerdict(result.item.ref, result));
|
|
32546
|
-
|
|
32713
|
+
for (const line of formatDecisionLines(result.item.ref, ruled.decisions ?? [])) console.log(line);
|
|
32714
|
+
printClaimWarnings([...result.warnings ?? [], ...ruled.warning ? [ruled.warning] : []]);
|
|
32547
32715
|
} catch (e) {
|
|
32548
32716
|
if (refuseRateLimited(e, o.json)) return;
|
|
32549
32717
|
if (refuseNotClaimable(e, o.json)) return;
|
|
@@ -32562,14 +32730,17 @@ function registerBoardCommands(program3) {
|
|
|
32562
32730
|
check: o.check,
|
|
32563
32731
|
allowPartial: o.allowPartial
|
|
32564
32732
|
}, { snapshot: registryClientDeps(config) });
|
|
32733
|
+
const ruled = await Promise.all(bulk.results.map((r) => r.claimed ? claimDecisions(r.item) : Promise.resolve({})));
|
|
32734
|
+
const rulingWarnings = ruled.flatMap((r) => r.warning ? [r.warning] : []);
|
|
32565
32735
|
if (o.json) {
|
|
32566
|
-
console.log(JSON.stringify(bulk.results));
|
|
32567
|
-
printClaimWarnings(bulk.warnings, true);
|
|
32736
|
+
console.log(JSON.stringify(bulk.results.map((r, i) => ruled[i].decisions ? { ...r, decisions: ruled[i].decisions } : r)));
|
|
32737
|
+
printClaimWarnings([...bulk.warnings ?? [], ...rulingWarnings], true);
|
|
32568
32738
|
} else {
|
|
32569
|
-
|
|
32739
|
+
bulk.results.forEach((result, i) => {
|
|
32570
32740
|
console.log(result.claimed ? claimVerdict(result.ref, result) : `Skipped ${result.ref}: ${result.reason}`);
|
|
32571
|
-
|
|
32572
|
-
|
|
32741
|
+
if (result.claimed) for (const line of formatDecisionLines(result.ref, ruled[i].decisions ?? [])) console.log(line);
|
|
32742
|
+
});
|
|
32743
|
+
printClaimWarnings([...bulk.warnings ?? [], ...rulingWarnings]);
|
|
32573
32744
|
}
|
|
32574
32745
|
if (bulk.failed > 0) process.exitCode = 1;
|
|
32575
32746
|
} catch (e) {
|
|
@@ -32733,18 +32904,18 @@ function registerBoardCommands(program3) {
|
|
|
32733
32904
|
concurrency: Number(o.concurrency) || void 0
|
|
32734
32905
|
});
|
|
32735
32906
|
if (o.json) return console.log(JSON.stringify(result));
|
|
32736
|
-
console.log(`board doctor: scanned ${result.scanned}, findings ${result.findings}${o.fix ? `, fixed ${result.fixed}, failed ${result.failed}` : ""}`);
|
|
32907
|
+
console.log(`board doctor: scanned ${result.scanned}, findings ${result.findings}${o.fix || result.fixed || result.failed ? `, fixed ${result.fixed}, failed ${result.failed}` : ""}`);
|
|
32737
32908
|
for (const finding of result.items) {
|
|
32738
32909
|
console.log(` [${finding.type}] ${finding.detail}`);
|
|
32739
32910
|
}
|
|
32740
32911
|
for (const claim of result.liveClaims) {
|
|
32741
32912
|
console.log(` [${claim.type}] ${claim.detail}`);
|
|
32742
32913
|
}
|
|
32743
|
-
if (result.findings >
|
|
32914
|
+
if (result.findings > result.fixed && !o.fix) {
|
|
32744
32915
|
console.log("Run `mmi-cli oracle board doctor --fix` to apply automatic fixes.");
|
|
32745
32916
|
process.exitCode = 1;
|
|
32746
32917
|
}
|
|
32747
|
-
if (o.fix && result.failed) process.exitCode = 1;
|
|
32918
|
+
if ((o.fix || result.failed) && result.failed) process.exitCode = 1;
|
|
32748
32919
|
} catch (e) {
|
|
32749
32920
|
return failGraceful(`board doctor failed: ${e.message}`);
|
|
32750
32921
|
}
|
|
@@ -33210,7 +33381,20 @@ init_hub_auth();
|
|
|
33210
33381
|
init_github_client();
|
|
33211
33382
|
|
|
33212
33383
|
// src/project-set.ts
|
|
33213
|
-
|
|
33384
|
+
function staticAssetsAreValid(value) {
|
|
33385
|
+
if (value === void 0 || value === null) return true;
|
|
33386
|
+
if (!Array.isArray(value) || value.length > 4) return false;
|
|
33387
|
+
const prefixes = /* @__PURE__ */ new Set();
|
|
33388
|
+
return value.every((entry) => {
|
|
33389
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
|
|
33390
|
+
const e = entry;
|
|
33391
|
+
if (Object.keys(e).some((k) => !["urlPrefix", "containerDir", "service"].includes(k))) return false;
|
|
33392
|
+
if (typeof e.urlPrefix !== "string" || !/^\/[A-Za-z0-9._/-]+\/$/.test(e.urlPrefix) || e.urlPrefix.includes("..") || e.urlPrefix.includes("//") || prefixes.has(e.urlPrefix)) return false;
|
|
33393
|
+
prefixes.add(e.urlPrefix);
|
|
33394
|
+
return typeof e.containerDir === "string" && /^\/[A-Za-z0-9._/-]+$/.test(e.containerDir) && !e.containerDir.includes("..") && !e.containerDir.includes("//") && (e.service === void 0 || typeof e.service === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(e.service));
|
|
33395
|
+
});
|
|
33396
|
+
}
|
|
33397
|
+
var UNSET_KEYS = ["projectAdmins", "installer", "oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "staticAssets", "requiredGcpApis", "publishRequired", "publishDir", "releaseChannel", "releaseLanguage", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary", "notifyOnDeploy", "vercelVersionEnv", "stageAllowlist", "suiteHold", "internalPeers", "previews"];
|
|
33214
33398
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
33215
33399
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
33216
33400
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
@@ -33757,6 +33941,7 @@ var SETTABLE_VAR_KEYS = [
|
|
|
33757
33941
|
"tenantTasks",
|
|
33758
33942
|
"internalPeers",
|
|
33759
33943
|
"edgeDomains",
|
|
33944
|
+
"staticAssets",
|
|
33760
33945
|
"statusFieldId",
|
|
33761
33946
|
"statusOptions",
|
|
33762
33947
|
"priorityFieldId",
|
|
@@ -33795,6 +33980,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
33795
33980
|
tenantTasks: 'JSON map {name:{service,command[],stages[],timeoutSeconds?,artifact?:"required",output?:"file",secretOutput?:{path,vault:{repo,key},overwrite?}}} \u2014 merged per entry, at most 20 stored in total; retire ONE task with --unset-task NAME; use {artifact} in argv when required; a secretOutput is master-only',
|
|
33796
33981
|
internalPeers: "JSON {owner/repo:[dev|rc|main]} \u2014 same-box suite peers via the box Caddy; clear with --unset internalPeers (#7886)",
|
|
33797
33982
|
secrets: "JSON catalog map keyed by KEY {key,purpose,group,owner,stages[],consumers[],branchReadable?} \u2014 merged per entry; drop ONE entry with --unset-secret KEY; clear ALL with --unset secrets --confirm-clear-secrets; prefer --secrets-file",
|
|
33983
|
+
staticAssets: 'JSON array, e.g. [{"urlPrefix":"/assets/","containerDir":"/app/dist/assets"}] \u2014 immutable edge skew protection',
|
|
33798
33984
|
edgeDomains: "JSON {dev,rc,main} domain map",
|
|
33799
33985
|
statusOptions: "JSON name\u2192id map",
|
|
33800
33986
|
priorityOptions: "JSON {Urgent,High,Medium,Low}\u2192id map",
|
|
@@ -33995,6 +34181,15 @@ function buildProjectSetPatch(input) {
|
|
|
33995
34181
|
patch[key] = parseRequiredChecksVar(raw);
|
|
33996
34182
|
} else if (key === "requiredCheckBranches") {
|
|
33997
34183
|
patch[key] = parseRequiredCheckBranchesVar(raw);
|
|
34184
|
+
} else if (key === "staticAssets") {
|
|
34185
|
+
let value2;
|
|
34186
|
+
try {
|
|
34187
|
+
value2 = JSON.parse(raw);
|
|
34188
|
+
} catch {
|
|
34189
|
+
throw new Error("staticAssets must be JSON");
|
|
34190
|
+
}
|
|
34191
|
+
if (value2 === null || !staticAssetsAreValid(value2)) throw new Error("staticAssets must be an array of up to 4 {urlPrefix, containerDir, service?} safe absolute paths with unique prefixes");
|
|
34192
|
+
patch[key] = value2;
|
|
33998
34193
|
} else if (key === "notifyOnDeploy") {
|
|
33999
34194
|
patch[key] = parseNotifyOnDeployVar(raw);
|
|
34000
34195
|
} else if (key === "vercelVersionEnv") {
|
|
@@ -38335,10 +38530,11 @@ function bundleTag(url) {
|
|
|
38335
38530
|
}
|
|
38336
38531
|
}
|
|
38337
38532
|
async function stageStatus(member, stage, deps) {
|
|
38338
|
-
const branch = stage === "dev" ? "development" : stage;
|
|
38339
38533
|
if (member.repo === SUITE_REPO && stage === "rc") return { stage, head: "n/a", served: "n/a", source: "n/a", lag: null };
|
|
38340
38534
|
let head;
|
|
38341
38535
|
try {
|
|
38536
|
+
const track = stage === "main" ? await deps.releaseTrack?.(member.repo) : void 0;
|
|
38537
|
+
const branch = stage === "dev" || stage === "main" && track === "single" ? "development" : stage;
|
|
38342
38538
|
const observed = await deps.head(member.repo, branch);
|
|
38343
38539
|
if (observed === null) return { stage, head: "n/a", served: "n/a", source: "n/a", lag: null };
|
|
38344
38540
|
head = observed || "unknown";
|
|
@@ -47642,7 +47838,8 @@ function registerCollaborationCommands(program3) {
|
|
|
47642
47838
|
if (o.comments || o.context) {
|
|
47643
47839
|
const boardProjectId = o.context ? await resolveBoardProjectId(o.repo) : void 0;
|
|
47644
47840
|
const data2 = await runIssueViewContext(queryDeps(), { number: targetNumber, repo: targetRepo2, comments: Boolean(o.comments), context: Boolean(o.context), jsonFields: gh2.ghFields, boardProjectId });
|
|
47645
|
-
|
|
47841
|
+
const decisions = parseDecisionMarkers(data2.comments);
|
|
47842
|
+
return { data: { ...data2, decisions }, parent: await readParentField(targetNumber, targetRepo2) };
|
|
47646
47843
|
}
|
|
47647
47844
|
const data = await ghJson(["issue", "view", String(targetNumber), "--repo", targetRepo2, "--json", gh2.ghFields]);
|
|
47648
47845
|
return { data, parent: await readParentField(targetNumber, targetRepo2) };
|
|
@@ -47755,6 +47952,25 @@ function registerCollaborationCommands(program3) {
|
|
|
47755
47952
|
return failGraceful(`issue comment: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
47756
47953
|
}
|
|
47757
47954
|
});
|
|
47955
|
+
jsonParity(issue.command("decision <ref>").description("record an owner decision, correction or approval VERBATIM on the issue it governs (machine-readable mmi-decision marker; board claim and issue view --comments show every one) and print {number,repo,url,commentUrl} JSON").requiredOption("--kind <kind>", `one of: ${DECISION_KINDS.join(", ")}`).requiredOption("--quote <text>", "the owner's words verbatim (never a paraphrase)").requiredOption("--at <iso>", "when the owner said it, ISO time (e.g. 2026-10-17T10:29:00Z)").requiredOption("--via <source>", `one of: ${DECISION_SOURCES.join(", ")}`).option("--by <endpoint>", "relaying session, for --via relay").option("--learning <ref>", "learning record id or issue ref, for --kind correction").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)")).action(async (ref, o) => {
|
|
47956
|
+
let marker;
|
|
47957
|
+
try {
|
|
47958
|
+
parseIssueRef(ref, o.repo);
|
|
47959
|
+
marker = buildDecisionMarker(o);
|
|
47960
|
+
} catch (e) {
|
|
47961
|
+
return fail(`issue decision: ${e.message}`);
|
|
47962
|
+
}
|
|
47963
|
+
const repo = await resolveRepo(parseIssueRef(ref, o.repo).repo ?? o.repo);
|
|
47964
|
+
if (!repo) return fail("issue decision: could not resolve repo \u2014 pass --repo owner/repo");
|
|
47965
|
+
try {
|
|
47966
|
+
console.log(JSON.stringify(await postIssueComment(defaultGitHubClient(), { ref, defaultRepo: repo, body: renderDecisionComment(marker) })));
|
|
47967
|
+
for (const line of decisionReminders(marker)) process.stderr.write(`${line}
|
|
47968
|
+
`);
|
|
47969
|
+
} catch (e) {
|
|
47970
|
+
const err = e;
|
|
47971
|
+
return failGraceful(`issue decision: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
47972
|
+
}
|
|
47973
|
+
});
|
|
47758
47974
|
jsonParity(issue.command("check <ref>").description("tick (or with --off untick) a task-list checkbox in an issue/epic body by its item text and print {number,repo,item,checked,changed} JSON").requiredOption("--item <text>", "the checklist item to match \u2014 exact item text, else a unique substring").option("--off", "untick the item ([x] \u2014 [ ]) instead of ticking it").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)")).action(async (ref, o) => {
|
|
47759
47975
|
let parsed;
|
|
47760
47976
|
try {
|
|
@@ -50748,6 +50964,11 @@ function suiteStatusDeps(github, registry2) {
|
|
|
50748
50964
|
if (!file?.content) throw new Error(`MM-Work seed/modules.json at ${ref} is unavailable`);
|
|
50749
50965
|
return JSON.parse(decodeGitHubContents(file.content));
|
|
50750
50966
|
},
|
|
50967
|
+
releaseTrack: async (repo) => {
|
|
50968
|
+
const meta = await fetchProjectBySlugChecked(repo.split("/")[1].toLowerCase(), registry2);
|
|
50969
|
+
if (!meta.ok || !meta.project) throw new Error(`${repo} META unreadable${meta.ok ? "" : `: ${meta.error}`}`);
|
|
50970
|
+
return resolveReleaseTrack(meta.project, void 0, repo);
|
|
50971
|
+
},
|
|
50751
50972
|
deploy: async (repo, stage) => {
|
|
50752
50973
|
const slug = repo.split("/")[1].toLowerCase();
|
|
50753
50974
|
const facts = await fetchDeployFactsBySlug(slug, registry2);
|
package/package.json
CHANGED