@mutmutco/claude-plugin 4.5.173 → 4.5.175
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/.claude-plugin/plugin.json +1 -1
- package/cli/dist/main.cjs +481 -268
- package/cli/package.json +1 -1
- package/package.json +1 -1
- package/skills/board/SKILL.md +36 -105
- package/skills/board/reference.md +66 -0
- package/skills/bootstrap/SKILL.md +34 -120
- package/skills/bootstrap/reference.md +65 -0
- package/skills/epic/SKILL.md +33 -78
- package/skills/hotfix/SKILL.md +22 -42
- package/skills/hotfix/recovery.md +10 -0
- package/skills/rcand/SKILL.md +18 -24
- package/skills/rcand/recovery.md +8 -0
- package/skills/release/SKILL.md +11 -73
- package/skills/release/verification.md +71 -0
- package/skills/secrets/SKILL.md +18 -80
- package/skills/secrets/reference.md +41 -0
- package/skills/stage/SKILL.md +20 -74
- package/skills/stage/reference.md +49 -0
package/cli/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.175",
|
|
19197
|
+
tag: "v4.5.175",
|
|
19198
|
+
commit: "8efcf884c10f",
|
|
19199
|
+
npm: "@mutmutco/cli@4.5.175"
|
|
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.175"
|
|
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.175 and redeploy the Hub Lambda from tag v4.5.175 (8efcf884c10f); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
19223
|
+
v3Target: "v4.5.175 (@mutmutco/cli@4.5.175, tag commit 8efcf884c10f \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,7 @@ init_hub_auth();
|
|
|
33210
33381
|
init_github_client();
|
|
33211
33382
|
|
|
33212
33383
|
// src/project-set.ts
|
|
33213
|
-
var UNSET_KEYS = ["projectAdmins", "installer", "oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "releaseChannel", "releaseLanguage", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary", "notifyOnDeploy", "vercelVersionEnv", "stageAllowlist", "suiteHold", "internalPeers"];
|
|
33384
|
+
var UNSET_KEYS = ["projectAdmins", "installer", "oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "releaseChannel", "releaseLanguage", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary", "notifyOnDeploy", "vercelVersionEnv", "stageAllowlist", "suiteHold", "internalPeers", "previews"];
|
|
33214
33385
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
33215
33386
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
33216
33387
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
@@ -33600,6 +33771,24 @@ function parseStageAllowlistVar(raw) {
|
|
|
33600
33771
|
}
|
|
33601
33772
|
return { key: row.key, stages: row.stages };
|
|
33602
33773
|
}
|
|
33774
|
+
function parsePreviewsVar(raw) {
|
|
33775
|
+
let parsed;
|
|
33776
|
+
try {
|
|
33777
|
+
parsed = JSON.parse(raw);
|
|
33778
|
+
} catch {
|
|
33779
|
+
throw new Error('org project set: previews must be JSON, e.g. {"seed":"deploy/preview-seed.sh"}');
|
|
33780
|
+
}
|
|
33781
|
+
const shape = "org project set: previews must be { seed, secrets? } \u2014 seed a repo-relative script path (no leading / or ..) and secrets a non-empty array (max 40) of env key names";
|
|
33782
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(shape);
|
|
33783
|
+
const row = parsed;
|
|
33784
|
+
const unknown = Object.keys(row).filter((key) => key !== "seed" && key !== "secrets");
|
|
33785
|
+
if (unknown.length > 0) throw new Error(`org project set: previews has unknown field(s): ${unknown.join(", ")}`);
|
|
33786
|
+
const { seed, secrets } = row;
|
|
33787
|
+
if (typeof seed !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._/-]{0,199}$/.test(seed) || seed.includes("..")) throw new Error(shape);
|
|
33788
|
+
if (secrets === void 0) return { seed };
|
|
33789
|
+
if (!Array.isArray(secrets) || secrets.length === 0 || secrets.length > 40 || !secrets.every((k) => typeof k === "string" && /^[A-Z_][A-Z0-9_]*$/.test(k))) throw new Error(shape);
|
|
33790
|
+
return { seed, secrets };
|
|
33791
|
+
}
|
|
33603
33792
|
function parseCiExemptReasonVar(raw) {
|
|
33604
33793
|
const trimmed = raw.trim();
|
|
33605
33794
|
if (!trimmed) throw new Error("org project set: ciExemptReason must be a non-empty reason (or use --unset ciExemptReason to clear it)");
|
|
@@ -33754,7 +33943,8 @@ var SETTABLE_VAR_KEYS = [
|
|
|
33754
33943
|
"installer",
|
|
33755
33944
|
"notifyOnDeploy",
|
|
33756
33945
|
"vercelVersionEnv",
|
|
33757
|
-
"stageAllowlist"
|
|
33946
|
+
"stageAllowlist",
|
|
33947
|
+
"previews"
|
|
33758
33948
|
];
|
|
33759
33949
|
var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
|
|
33760
33950
|
var SETTABLE_VAR_HINTS = {
|
|
@@ -33786,6 +33976,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
33786
33976
|
notifyOnDeploy: 'JSON stage array, e.g. ["dev"] \u2014 successful tenant-deploy sends <stage>-deployed to the project repo (#7358)',
|
|
33787
33977
|
vercelVersionEnv: "JSON {project,key,team?} \u2014 train sets this Vercel env to the release tag before the main/rc push (#7362)",
|
|
33788
33978
|
stageAllowlist: "JSON {key,stages[]} \u2014 `org access sync` writes the developer/admin company-email allowlist to <stage>/<key> for each listed stage (#7678)",
|
|
33979
|
+
previews: "JSON {seed,secrets?} \u2014 per-PR preview contract; absent means previews off; clear with --unset previews (#8405)",
|
|
33789
33980
|
ciExemptReason: "non-empty string \u2014 declares this deployable repo has no CI surface (#4265)",
|
|
33790
33981
|
gate: "JSON {runtime,cmd,affectedCmd,workdir,cacheDepPath,pyVersion}"
|
|
33791
33982
|
};
|
|
@@ -33981,6 +34172,8 @@ function buildProjectSetPatch(input) {
|
|
|
33981
34172
|
patch[key] = parseVercelVersionEnvVar(raw);
|
|
33982
34173
|
} else if (key === "stageAllowlist") {
|
|
33983
34174
|
patch[key] = parseStageAllowlistVar(raw);
|
|
34175
|
+
} else if (key === "previews") {
|
|
34176
|
+
patch[key] = parsePreviewsVar(raw);
|
|
33984
34177
|
} else if (key === "ciExemptReason") {
|
|
33985
34178
|
patch[key] = parseCiExemptReasonVar(raw);
|
|
33986
34179
|
} else if (key === "gate") {
|
|
@@ -47620,7 +47813,8 @@ function registerCollaborationCommands(program3) {
|
|
|
47620
47813
|
if (o.comments || o.context) {
|
|
47621
47814
|
const boardProjectId = o.context ? await resolveBoardProjectId(o.repo) : void 0;
|
|
47622
47815
|
const data2 = await runIssueViewContext(queryDeps(), { number: targetNumber, repo: targetRepo2, comments: Boolean(o.comments), context: Boolean(o.context), jsonFields: gh2.ghFields, boardProjectId });
|
|
47623
|
-
|
|
47816
|
+
const decisions = parseDecisionMarkers(data2.comments);
|
|
47817
|
+
return { data: { ...data2, decisions }, parent: await readParentField(targetNumber, targetRepo2) };
|
|
47624
47818
|
}
|
|
47625
47819
|
const data = await ghJson(["issue", "view", String(targetNumber), "--repo", targetRepo2, "--json", gh2.ghFields]);
|
|
47626
47820
|
return { data, parent: await readParentField(targetNumber, targetRepo2) };
|
|
@@ -47733,6 +47927,25 @@ function registerCollaborationCommands(program3) {
|
|
|
47733
47927
|
return failGraceful(`issue comment: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
47734
47928
|
}
|
|
47735
47929
|
});
|
|
47930
|
+
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) => {
|
|
47931
|
+
let marker;
|
|
47932
|
+
try {
|
|
47933
|
+
parseIssueRef(ref, o.repo);
|
|
47934
|
+
marker = buildDecisionMarker(o);
|
|
47935
|
+
} catch (e) {
|
|
47936
|
+
return fail(`issue decision: ${e.message}`);
|
|
47937
|
+
}
|
|
47938
|
+
const repo = await resolveRepo(parseIssueRef(ref, o.repo).repo ?? o.repo);
|
|
47939
|
+
if (!repo) return fail("issue decision: could not resolve repo \u2014 pass --repo owner/repo");
|
|
47940
|
+
try {
|
|
47941
|
+
console.log(JSON.stringify(await postIssueComment(defaultGitHubClient(), { ref, defaultRepo: repo, body: renderDecisionComment(marker) })));
|
|
47942
|
+
for (const line of decisionReminders(marker)) process.stderr.write(`${line}
|
|
47943
|
+
`);
|
|
47944
|
+
} catch (e) {
|
|
47945
|
+
const err = e;
|
|
47946
|
+
return failGraceful(`issue decision: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
47947
|
+
}
|
|
47948
|
+
});
|
|
47736
47949
|
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) => {
|
|
47737
47950
|
let parsed;
|
|
47738
47951
|
try {
|