@mutmutco/codex-plugin 4.5.87 → 4.5.88

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mmi",
3
- "version": "4.5.87",
3
+ "version": "4.5.88",
4
4
  "mmiCompat": "4.x",
5
5
  "description": "MMI workflow skills and org gates delivery.",
6
6
  "author": {
package/cli/dist/main.cjs CHANGED
@@ -7632,21 +7632,15 @@ function isHarbourUnmanaged(yamlText) {
7632
7632
  function strayCronDrifts(workflows) {
7633
7633
  return workflows.filter((w) => !isHarbourUnmanaged(w.yamlText)).filter((w) => workflowTriggersSchedule(w.yamlText)).map((w) => strayCronDrift(w.name)).sort((a, b) => a.name.localeCompare(b.name));
7634
7634
  }
7635
- var HARBOUR_LLM_LAUNCHERS = /* @__PURE__ */ new Set(["codex-agent"]);
7636
- function isHarbourLlmLauncher(executor) {
7637
- return HARBOUR_LLM_LAUNCHERS.has(executor);
7638
- }
7639
7635
  function unlauncheredLlmDrift(entry) {
7640
7636
  if (entry.llm !== "yes") return null;
7641
- if (isHarbourLlmLauncher(entry.executor)) return null;
7642
7637
  if (entry.executor === "aws-scheduler \u2192 mmi-hub-schedule-dispatcher") return null;
7643
- const allowed = [...HARBOUR_LLM_LAUNCHERS].join(", ");
7644
7638
  return {
7645
7639
  class: "unlaunchered-llm",
7646
7640
  name: entry.name,
7647
7641
  executor: entry.executor,
7648
- detail: `llm: yes with executor outside the harbour launcher set (${allowed})`,
7649
- remedy: `move the lane to executor: ${allowed} and run it through the Hub harbour (docs/schedules.md rule 3)`
7642
+ detail: "llm: yes but no scheduled-LLM executor is supported (Cursor retired, #7779; Codex executor retired, #7821)",
7643
+ remedy: "retire the lane or set it to llm: no until the harbour redesign (#7821; docs/schedules.md rule 3)"
7650
7644
  };
7651
7645
  }
7652
7646
  function unlauncheredLlmDrifts(entries) {
@@ -7718,8 +7712,6 @@ var SCHEDULE_HEADER_FIELDS = ["schedule", "what", "owner", "cadence", "executor"
7718
7712
  var SCHEDULE_EXECUTORS = /* @__PURE__ */ new Set([
7719
7713
  "github-actions",
7720
7714
  // deterministic workflow — clocked by the harbour, fired via workflow_dispatch
7721
- "codex-agent",
7722
- // LLM lane — the harbour's ONLY scheduled-LLM launcher (docs/schedules.md rule 3, #7780)
7723
7715
  "lambda",
7724
7716
  // fleet function row — requires the `# target: mmi-fleet-*` header (#3205/#3246)
7725
7717
  "box systemd",
@@ -7800,8 +7792,8 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
7800
7792
  if (llmFromHeader(yamlText) === "unknown") {
7801
7793
  throw new Error(`schedules register: ${expectedId} header \`llm: ${h.llm}\` does not classify (yes / no / embeddings) \u2014 an unreadable declaration would register a row whose LLM column lies.`);
7802
7794
  }
7803
- if (llmFromHeader(yamlText) === "yes" && h.executor !== "codex-agent") {
7804
- throw new Error(`schedules register: ${expectedId} header \`llm: yes\` requires the harbour launcher \`executor: codex-agent\` (declared: \`${h.executor}\`) \u2014 scheduled LLM work runs only through the Codex executor (docs/schedules.md rule 3, #7780); change the executor or the llm value.`);
7795
+ if (llmFromHeader(yamlText) === "yes") {
7796
+ throw new Error(`schedules register: ${expectedId} header \`llm: yes\` is refused \u2014 no scheduled-LLM executor is supported: Cursor (\`cursor-agent\`) was retired (#7779) and the Codex executor was retired (#7821). Set \`llm: no\` or retire the lane until the harbour redesign.`);
7805
7797
  }
7806
7798
  const target = header.target;
7807
7799
  if (target !== void 0 && h.executor !== "lambda") {
@@ -7811,7 +7803,7 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
7811
7803
  throw new Error(`schedules register: ${expectedId} header \`target: ${target}\` must be a bare lambda function name in the reserved fleet namespace \`mmi-fleet-*\` (allowed after the prefix: A-Z a-z 0-9 _ -; total max 140). The dispatcher can only invoke mmi-fleet-* functions.`);
7812
7804
  }
7813
7805
  if (header.model !== void 0) {
7814
- throw new Error(`schedules register: ${expectedId} header \`model: ${header.model}\` is refused \u2014 the Codex executor pins its model (#7780), so no row declares one. Remove the line.`);
7806
+ throw new Error(`schedules register: ${expectedId} header \`model: ${header.model}\` is refused \u2014 a model role key is valid only on an LLM lane, and no scheduled-LLM executor is supported (Cursor retired #7779; Codex executor retired #7821). Remove the line.`);
7815
7807
  }
7816
7808
  return {
7817
7809
  id: expectedId,
@@ -12396,6 +12388,7 @@ function renderBoardItem(item) {
12396
12388
  if (claim) {
12397
12389
  const who = `${claim.holder}, ${formatClaimAge(claim.ageMs)} ago`;
12398
12390
  lines2.push(claim.currentlyClaimed ? `Claimed by: ${who}` : `Last claimed by: ${who} (not currently claimed)`);
12391
+ if (claim.stale) lines2.push(`Claim stamp: STALE, idle ${formatClaimAge(claim.idleMs)}; any live session may claim it`);
12399
12392
  }
12400
12393
  lines2.push(
12401
12394
  `Type: ${item.type ?? "item"}${item.labels.length ? ` \xB7 ${item.labels.join(", ")}` : ""}`,
@@ -12696,6 +12689,10 @@ async function readBoard(options, deps = {}) {
12696
12689
  if (options.includeBundleDetails || options.includeAllBodies) {
12697
12690
  await attachBundleDetails(report, client, options.allowPartial ?? false, { all: options.includeAllBodies });
12698
12691
  }
12692
+ const readComments = claimMarkerCommentsReader(client);
12693
+ for (const scope of ["primary", "secondary"]) {
12694
+ await markStaleClaimStamps(report[scope], readComments, { pullRequests, writable: writableOrUnknown(writable) });
12695
+ }
12699
12696
  for (const scope of ["primary", "secondary"]) {
12700
12697
  const filtered = await filterDependencyBlockedClaimables(report[scope].claimable, client, {
12701
12698
  requireBundledBody: Boolean(options.includeBundleDetails)
@@ -12704,7 +12701,7 @@ async function readBoard(options, deps = {}) {
12704
12701
  report.warnings.push(...filtered.warnings);
12705
12702
  }
12706
12703
  for (const scope of ["primary", "secondary"]) {
12707
- const demoted = await demoteLiveClaimedClaimables(report[scope], client);
12704
+ const demoted = await demoteLiveClaimedClaimables(report[scope], client, void 0, { fetchComments: readComments });
12708
12705
  report.warnings.push(...demoted.warnings);
12709
12706
  report.partial = report.partial || demoted.partial;
12710
12707
  }
@@ -12994,7 +12991,7 @@ function renderClaimable(lines2, items) {
12994
12991
  function renderTaken(lines2, items) {
12995
12992
  if (!items.length) return;
12996
12993
  lines2.push("Taken");
12997
- for (const item of items) lines2.push(` ${item.ref} \xB7 ${item.status} \xB7 @${item.assignees.join(", @")}`);
12994
+ for (const item of items) lines2.push(` ${item.ref} \xB7 ${item.status} \xB7 @${item.assignees.join(", @")}${renderClaimLane(item)}`);
12998
12995
  }
12999
12996
  function renderUnowned(lines2, items) {
13000
12997
  if (!items.length) return;
@@ -13005,7 +13002,11 @@ function renderTitledItem(item) {
13005
13002
  const pri = item.priority ? ` \xB7 ${item.priority}` : "";
13006
13003
  const loop = item.loopState ? ` \xB7 loop:${item.loopState.state}` : "";
13007
13004
  const stale = item.loopStaleness?.stale ? ` \xB7 stale:${formatClaimAge(item.loopStaleness.idleMs)}` : "";
13008
- return `${item.ref} - [${item.type ?? "item"}]${pri}${loop}${stale} ${item.title}`;
13005
+ return `${item.ref} - [${item.type ?? "item"}]${pri}${loop}${stale}${renderClaimLane(item)} ${item.title}`;
13006
+ }
13007
+ function renderClaimLane(item) {
13008
+ if (!item.claim) return "";
13009
+ return item.claim.stale ? ` \xB7 stale:${formatClaimAge(item.claim.idleMs)} (${item.claim.holder})` : ` \xB7 lane ${item.claim.holder}`;
13009
13010
  }
13010
13011
  function hasItems(buckets) {
13011
13012
  return buckets.userOwned.length > 0 || buckets.claimable.length > 0 || buckets.taken.length > 0 || buckets.unownedInFlight.length > 0;
@@ -13130,6 +13131,7 @@ function deriveLoopStaleness(item, loopState, now = Date.now()) {
13130
13131
  }
13131
13132
  var CLAIM_MARKER_PATTERN = /<!--\s*mmi-claim:\s*(\{[\s\S]*?\})\s*-->/g;
13132
13133
  var CLAIM_MARKER_LIVE_MS = 24 * 60 * 6e4;
13134
+ var CLAIM_STAMP_IDLE_MS = 4 * 60 * 6e4;
13133
13135
  function isValidClaimMarker(parsed) {
13134
13136
  if (!parsed || typeof parsed !== "object") return false;
13135
13137
  const marker = parsed;
@@ -13166,34 +13168,86 @@ function describeClaimMarker(marker) {
13166
13168
  }
13167
13169
  function boardItemClaim(item) {
13168
13170
  const marker = item.details ? latestClaimMarker(item.details.comments) : void 0;
13169
- if (!marker) return void 0;
13171
+ return marker ? claimFromMarker(item, marker) : void 0;
13172
+ }
13173
+ function claimStampStaleness(marker, evidence = {}, now = Date.now()) {
13174
+ const times = [marker.ts, ...evidence.activityAt ?? []].map((t) => t ? Date.parse(t) : Number.NaN);
13175
+ const idleMs = now - Math.max(...times.filter(Number.isFinite));
13176
+ return { stale: !evidence.linkedWork && idleMs >= CLAIM_STAMP_IDLE_MS, idleMs };
13177
+ }
13178
+ function workIsLive(at, now = Date.now()) {
13179
+ const idle = now - Date.parse(at ?? "");
13180
+ return !Number.isFinite(idle) || idle < CLAIM_STAMP_IDLE_MS;
13181
+ }
13182
+ function claimFromMarker(item, marker, linked = {}, now = Date.now()) {
13170
13183
  return {
13171
13184
  marker,
13172
13185
  holder: describeClaimMarker(marker),
13173
- ageMs: Date.now() - Date.parse(marker.ts),
13174
- currentlyClaimed: item.assignees.length > 0 && item.status === "In Progress"
13186
+ ageMs: now - Date.parse(marker.ts),
13187
+ currentlyClaimed: item.assignees.length > 0 && item.status === "In Progress",
13188
+ ...claimStampStaleness(marker, { activityAt: [item.updatedAt, ...linked.activityAt ?? []], linkedWork: linked.linkedWork }, now)
13175
13189
  };
13176
13190
  }
13177
- function classifyClaimableMarker(comments, actor, now = Date.now()) {
13191
+ function classifyClaimableMarker(comments, actor, now = Date.now(), activityAt = []) {
13178
13192
  const marker = comments ? latestClaimMarker(comments) : void 0;
13179
13193
  if (!marker) return { verdict: "claimable" };
13180
13194
  const ownership = laneOwnership(marker, actor);
13181
13195
  if (ownership === "mine") return { verdict: "owned", holder: describeClaimMarker(marker) };
13182
- const age = now - Date.parse(marker.ts);
13183
- if (Number.isFinite(age) && age >= 0 && age < CLAIM_MARKER_LIVE_MS) {
13196
+ if (!claimStampStaleness(marker, { activityAt }, now).stale) {
13184
13197
  return { verdict: "taken", holder: describeClaimMarker(marker) };
13185
13198
  }
13186
13199
  return { verdict: "claimable" };
13187
13200
  }
13201
+ function claimMarkerCommentsReader(client) {
13202
+ const cache = /* @__PURE__ */ new Map();
13203
+ return (item) => {
13204
+ let read = cache.get(item);
13205
+ if (!read) {
13206
+ read = (async () => {
13207
+ if (item.detailsUnavailable) return void 0;
13208
+ if (item.details && !item.details.commentsUnread && !item.details.omittedComments) return item.details.comments;
13209
+ const comments = await client.restPaginate(`repos/${item.repository}/issues/${item.number}/comments`);
13210
+ return comments.map((comment) => ({ body: comment.body ?? "" }));
13211
+ })();
13212
+ cache.set(item, read);
13213
+ }
13214
+ return read;
13215
+ };
13216
+ }
13217
+ async function markStaleClaimStamps(scope, readComments, options = {}) {
13218
+ const actor = options.actor ?? describeSessionIdentity();
13219
+ const now = options.now ?? Date.now();
13220
+ const promoted = [];
13221
+ for (const bucket of ["userOwned", "claimable", "unownedInFlight", "taken"]) {
13222
+ const kept = [];
13223
+ for (const item of scope[bucket]) {
13224
+ kept.push(item);
13225
+ if (item.contentType !== "Issue") continue;
13226
+ const comments = await readComments(item).catch(() => void 0);
13227
+ const marker = comments ? latestClaimMarker(comments) : void 0;
13228
+ if (!marker) continue;
13229
+ const prs = (options.pullRequests ?? []).filter((pr) => pr.closesIssues.some((ref) => ref.toLowerCase() === item.ref.toLowerCase()));
13230
+ item.claim = claimFromMarker(item, marker, {
13231
+ activityAt: prs.map((pr) => pr.updatedAt),
13232
+ linkedWork: prs.some((pr) => !pr.updatedAt)
13233
+ }, now);
13234
+ const movable = bucket === "userOwned" || bucket === "unownedInFlight";
13235
+ if (movable && item.claim.stale && laneOwnership(marker, actor) !== "mine" && (!options.writable || options.writable.has(item.repository.toLowerCase()))) {
13236
+ kept.pop();
13237
+ promoted.push(item);
13238
+ }
13239
+ }
13240
+ scope[bucket] = kept;
13241
+ }
13242
+ if (promoted.length) {
13243
+ scope.claimable.push(...promoted);
13244
+ scope.claimable.sort(compareItems);
13245
+ }
13246
+ }
13188
13247
  async function demoteLiveClaimedClaimables(scope, client, actor = describeSessionIdentity(), deps = {}) {
13189
13248
  const warnings = [];
13190
13249
  let partial = false;
13191
- const fetchComments = deps.fetchComments ?? (async (item) => {
13192
- if (item.detailsUnavailable) return void 0;
13193
- if (item.details && !item.details.commentsUnread && !item.details.omittedComments) return item.details.comments;
13194
- const comments = await client.restPaginate(`repos/${item.repository}/issues/${item.number}/comments`);
13195
- return comments.map((comment) => ({ body: comment.body ?? "" }));
13196
- });
13250
+ const fetchComments = deps.fetchComments ?? claimMarkerCommentsReader(client);
13197
13251
  const stillClaimable = [];
13198
13252
  const movedTaken = [];
13199
13253
  const movedOwned = [];
@@ -13213,7 +13267,7 @@ async function demoteLiveClaimedClaimables(scope, client, actor = describeSessio
13213
13267
  warnings.push(`${item.ref} left claimable: claim-marker evidence could not be read`);
13214
13268
  continue;
13215
13269
  }
13216
- const verdict = classifyClaimableMarker(comments, actor, deps.now);
13270
+ const verdict = classifyClaimableMarker(comments, actor, deps.now, [item.updatedAt]);
13217
13271
  if (verdict.verdict === "claimable") {
13218
13272
  stillClaimable.push(item);
13219
13273
  } else if (verdict.verdict === "owned") {
@@ -13308,6 +13362,12 @@ async function gatherClaimLiveness(client, repo, number, fetchOpenPulls) {
13308
13362
  const comments = await client.restPaginate(`repos/${repo}/issues/${number}/comments`);
13309
13363
  const rows = comments.map((comment) => ({ body: comment.body ?? "", login: comment.user?.login ?? void 0 }));
13310
13364
  out.marker = latestClaimMarker(rows);
13365
+ for (let i = comments.length - 1; i >= 0; i -= 1) {
13366
+ if (comments[i].created_at && !latestClaimMarker([rows[i]])) {
13367
+ out.lastCommentAt = comments[i].created_at;
13368
+ break;
13369
+ }
13370
+ }
13311
13371
  if (out.marker) {
13312
13372
  for (let i = rows.length - 1; i >= 0; i -= 1) {
13313
13373
  if (latestClaimMarker([rows[i]])) {
@@ -13334,7 +13394,10 @@ async function gatherClaimLiveness(client, repo, number, fetchOpenPulls) {
13334
13394
  if (from && from.toLowerCase() !== repo.toLowerCase()) return false;
13335
13395
  return pullClosesIssue(source, number);
13336
13396
  });
13337
- if (pr?.source?.issue?.number) out.openPr = `open PR #${pr.source.issue.number} would close it on merge`;
13397
+ if (pr?.source?.issue?.number) {
13398
+ out.openPr = `open PR #${pr.source.issue.number} would close it on merge`;
13399
+ out.openPrAt = pr.source.issue.updated_at ?? void 0;
13400
+ }
13338
13401
  } catch {
13339
13402
  out.failed.push("timeline");
13340
13403
  }
@@ -13345,11 +13408,24 @@ async function gatherClaimLiveness(client, repo, number, fetchOpenPulls) {
13345
13408
  } catch {
13346
13409
  out.failed.push("branches");
13347
13410
  }
13411
+ if (out.branch) {
13412
+ try {
13413
+ const head = await client.rest(
13414
+ "GET",
13415
+ `repos/${repo}/branches/${encodeURIComponent(out.branch)}`
13416
+ );
13417
+ out.branchAt = head?.commit?.commit?.committer?.date ?? void 0;
13418
+ } catch {
13419
+ }
13420
+ }
13348
13421
  if (!out.branch) {
13349
13422
  try {
13350
13423
  const pulls = await fetchOpenPulls(repo);
13351
13424
  const hit = pulls.find((pr) => pullClosesIssue(pr, number));
13352
- if (hit?.head?.ref) out.branch = `${hit.head.ref} (head of open PR #${hit.number} closing #${number})`;
13425
+ if (hit?.head?.ref) {
13426
+ out.branch = `${hit.head.ref} (head of open PR #${hit.number} closing #${number})`;
13427
+ out.branchAt = hit.updated_at ?? void 0;
13428
+ }
13353
13429
  } catch {
13354
13430
  out.failed.push("pulls");
13355
13431
  }
@@ -13360,13 +13436,24 @@ function liveEvidenceLines(evidence, repo) {
13360
13436
  const live = [];
13361
13437
  if (evidence.marker && evidence.sessionState === "live") {
13362
13438
  live.push(`claim session ${describeClaimMarker(evidence.marker)} has recent local transcript activity`);
13363
- } else if (evidence.marker && evidence.sessionState !== "dead" && evidence.markerAgeMs !== void 0 && evidence.markerAgeMs < CLAIM_MARKER_LIVE_MS) {
13364
- live.push(`claim marker from ${describeClaimMarker(evidence.marker)} is only ${formatClaimAge(evidence.markerAgeMs)} old (session not locally probeable)`);
13439
+ } else if (evidence.marker && evidence.sessionState !== "dead") {
13440
+ const { stale, idleMs } = claimStampStaleness(evidence.marker, { activityAt: [evidence.lastCommentAt] });
13441
+ const marker = describeClaimMarker(evidence.marker);
13442
+ if (!stale) {
13443
+ live.push(evidence.lastCommentAt && Date.parse(evidence.lastCommentAt) > Date.parse(evidence.marker.ts) ? `claim marker from ${marker} is ${formatClaimAge(evidence.markerAgeMs)} old but the issue was commented ${formatClaimAge(idleMs)} ago (session not locally probeable)` : `claim marker from ${marker} is only ${formatClaimAge(evidence.markerAgeMs)} old (session not locally probeable)`);
13444
+ }
13365
13445
  }
13366
- if (evidence.openPr) live.push(evidence.openPr);
13367
- if (evidence.branch) live.push(`live branch ${evidence.branch} on ${repo}`);
13446
+ if (evidence.openPr && workIsLive(evidence.openPrAt)) live.push(evidence.openPr);
13447
+ if (evidence.branch && workIsLive(evidence.branchAt)) live.push(`live branch ${evidence.branch} on ${repo}`);
13368
13448
  return live;
13369
13449
  }
13450
+ function staleWorkLines(evidence) {
13451
+ const age = (at) => formatClaimAge(Date.now() - Date.parse(at ?? ""));
13452
+ return [
13453
+ evidence.branch && !workIsLive(evidence.branchAt) ? `branch ${evidence.branch} (last commit ${age(evidence.branchAt)} ago)` : void 0,
13454
+ evidence.openPr && !workIsLive(evidence.openPrAt) ? `${evidence.openPr.replace(/ would close it on merge$/, "")} (updated ${age(evidence.openPrAt)} ago)` : void 0
13455
+ ].filter((line) => Boolean(line));
13456
+ }
13370
13457
  function sameClaimHost(a, b) {
13371
13458
  return Boolean(a && b && a.toLowerCase() === b.toLowerCase());
13372
13459
  }
@@ -13399,13 +13486,17 @@ async function checkLaneContest(client, item, actor = describeSessionIdentity(),
13399
13486
  const resume = ownership !== "mine" && sameOwner ? laneResumeEvidence(evidence) : void 0;
13400
13487
  const live = ownership === "mine" ? [] : liveEvidenceLines(evidence, item.repository);
13401
13488
  const unverifiable = ownership === "mine" ? [] : evidence.failed;
13489
+ const contested = ownership !== "mine" && !resume && (live.length > 0 || unverifiable.length > 0);
13490
+ const carry = staleWorkLines(evidence);
13491
+ const staleStamp = !contested && !resume && ownership !== "mine" && 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;
13402
13492
  return {
13403
- contested: ownership !== "mine" && !resume && (live.length > 0 || unverifiable.length > 0),
13493
+ contested,
13404
13494
  live,
13405
13495
  unverifiable,
13406
13496
  ownership,
13407
13497
  marker: evidence.marker,
13408
- resume
13498
+ resume,
13499
+ ...staleStamp ? { staleStamp } : {}
13409
13500
  };
13410
13501
  }
13411
13502
  function laneContestParts(contest) {
@@ -13849,11 +13940,13 @@ async function claimOneBoardItem(ctx, selector, options) {
13849
13940
  };
13850
13941
  let previousHolder;
13851
13942
  let resumeEvidence;
13943
+ let staleStamp;
13944
+ const tookOver = () => ({ outcome: "took-over", holder, previousHolder, ...staleStamp ? { staleStamp } : {} });
13852
13945
  const claimedReceipt = () => ({
13853
13946
  ...item.status === "In Review" ? { reclaimedFrom: "In Review" } : {},
13854
- ...previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : { outcome: "took-over", holder, previousHolder } : { outcome: "claimed", holder }
13947
+ ...previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : tookOver() : { outcome: "claimed", holder }
13855
13948
  });
13856
- const heldReceipt = () => previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : { outcome: "took-over", holder, previousHolder } : { outcome: "held", holder };
13949
+ const heldReceipt = () => previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : tookOver() : { outcome: "held", holder };
13857
13950
  if (flatItem.contentType !== "Issue") throw new Error(`${flatItem.ref} is not an issue`);
13858
13951
  const pre = evaluateClaim(flatItem, assignedLogin);
13859
13952
  if (!pre.ok) throw new NotClaimableError(pre.reason, { ref: flatItem.ref, status: flatItem.status });
@@ -13866,6 +13959,7 @@ async function claimOneBoardItem(ctx, selector, options) {
13866
13959
  const refuseIfContested = async () => {
13867
13960
  const contest = await checkLaneContest(client, item, ctx.session, report.viewer);
13868
13961
  resumeEvidence = void 0;
13962
+ staleStamp = void 0;
13869
13963
  const displaced = () => ({
13870
13964
  login: assignedLogin,
13871
13965
  ...contest.marker ? {
@@ -13878,6 +13972,9 @@ async function claimOneBoardItem(ctx, selector, options) {
13878
13972
  if (contest.resume && contest.marker) {
13879
13973
  resumeEvidence = contest.resume;
13880
13974
  previousHolder = displaced();
13975
+ } else if (contest.staleStamp) {
13976
+ staleStamp = contest.staleStamp;
13977
+ previousHolder = displaced();
13881
13978
  }
13882
13979
  return;
13883
13980
  }
@@ -13892,7 +13989,7 @@ async function claimOneBoardItem(ctx, selector, options) {
13892
13989
  if (options.check) {
13893
13990
  return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, ...heldReceipt(), alreadyClaimed: true, checked: true };
13894
13991
  }
13895
- const renewed = await postClaimMarkerComment(client, item, ctx.session);
13992
+ const renewed = await postClaimMarkerComment(client, item, ctx.session, staleStamp);
13896
13993
  return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, ...heldReceipt(), alreadyClaimed: true, renewed };
13897
13994
  }
13898
13995
  if (options.check) {
@@ -13904,7 +14001,7 @@ async function claimOneBoardItem(ctx, selector, options) {
13904
14001
  } catch (e) {
13905
14002
  throw new Error(`claim failed before board status changed: ${ghError(e)}`);
13906
14003
  }
13907
- await postClaimMarkerComment(client, item, ctx.session);
14004
+ await postClaimMarkerComment(client, item, ctx.session, staleStamp);
13908
14005
  let credential = "user";
13909
14006
  try {
13910
14007
  ({ credential } = await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, cfg.statusOptions["In Progress"]));
@@ -13958,7 +14055,7 @@ async function claimBoardIssues(options, deps = {}) {
13958
14055
  const ref = `${selector.repo}#${selector.number}`;
13959
14056
  try {
13960
14057
  const result = await claimOneBoardItem(ctx, selector, { ...options, bulk: true });
13961
- results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, resumeEvidence: result.resumeEvidence, reclaimedFrom: result.reclaimedFrom, alreadyClaimed: result.alreadyClaimed, renewed: result.renewed, checked: result.checked, credential: result.credential };
14058
+ results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, resumeEvidence: result.resumeEvidence, staleStamp: result.staleStamp, reclaimedFrom: result.reclaimedFrom, alreadyClaimed: result.alreadyClaimed, renewed: result.renewed, checked: result.checked, credential: result.credential };
13962
14059
  } catch (e) {
13963
14060
  results[index] = { ref, claimed: false, reason: e.message };
13964
14061
  }
@@ -14146,7 +14243,7 @@ async function setBoardPriorityForSelector(options, deps = {}) {
14146
14243
  }
14147
14244
  return { ref: `${options.selector.repo}#${options.selector.number}`, itemId, priority };
14148
14245
  }
14149
- async function postClaimMarkerComment(client, item, actor = describeSessionIdentity()) {
14246
+ async function postClaimMarkerComment(client, item, actor = describeSessionIdentity(), staleStamp) {
14150
14247
  try {
14151
14248
  const marker = {
14152
14249
  v: 1,
@@ -14157,7 +14254,7 @@ async function postClaimMarkerComment(client, item, actor = describeSessionIdent
14157
14254
  };
14158
14255
  await postIssueComment(client, {
14159
14256
  ref: item.ref,
14160
- body: `Claimed by \`${describeClaimMarker(marker)}\` at ${marker.ts}.
14257
+ body: `Claimed by \`${describeClaimMarker(marker)}\` at ${marker.ts}.${staleStamp ? ` Took over from ${staleStamp}.` : ""}
14161
14258
 
14162
14259
  <!-- mmi-claim: ${JSON.stringify(marker)} -->`
14163
14260
  });
@@ -17128,10 +17225,10 @@ var rollout_plan_default = {
17128
17225
  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)."
17129
17226
  },
17130
17227
  baseline: {
17131
- version: "4.5.87",
17132
- tag: "v4.5.87",
17133
- commit: "c8eaaec639a7",
17134
- npm: "@mutmutco/cli@4.5.87"
17228
+ version: "4.5.88",
17229
+ tag: "v4.5.88",
17230
+ commit: "6e198cd64755",
17231
+ npm: "@mutmutco/cli@4.5.88"
17135
17232
  },
17136
17233
  exitCriterion: "fleet-n-of-n",
17137
17234
  hubOnlyShortcut: "forbidden",
@@ -17148,14 +17245,14 @@ var rollout_plan_default = {
17148
17245
  repo: "mutmutco/mmi-hub",
17149
17246
  role: "canary",
17150
17247
  schedule: "train",
17151
- v3Target: "v4.5.87"
17248
+ v3Target: "v4.5.88"
17152
17249
  }
17153
17250
  ],
17154
17251
  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.",
17155
17252
  rollback: {
17156
17253
  independent: true,
17157
- mechanism: "npm dist-tag latest -> 4.5.87 and redeploy the Hub Lambda from tag v4.5.87 (c8eaaec639a7); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17158
- v3Target: "v4.5.87 (@mutmutco/cli@4.5.87, tag commit c8eaaec639a7 \u2014 last known-good release carrying the repo-index v4-only contract)"
17254
+ mechanism: "npm dist-tag latest -> 4.5.88 and redeploy the Hub Lambda from tag v4.5.88 (6e198cd64755); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17255
+ v3Target: "v4.5.88 (@mutmutco/cli@4.5.88, tag commit 6e198cd64755 \u2014 last known-good release carrying the repo-index v4-only contract)"
17159
17256
  }
17160
17257
  },
17161
17258
  {
@@ -23182,7 +23279,12 @@ var TRANSIENT_TRANSPORT_MARKERS = [
23182
23279
  ];
23183
23280
  function classifyTransientTransportFailure(evidence) {
23184
23281
  if (!evidence) return false;
23185
- return TRANSIENT_TRANSPORT_MARKERS.some((marker) => marker.test(evidence));
23282
+ return TRANSIENT_TRANSPORT_MARKERS.some((marker) => marker.test(evidence)) || isSetupBunSpawnEtxtbsy(evidence);
23283
+ }
23284
+ function isSetupBunSpawnEtxtbsy(evidence) {
23285
+ if (!/\bspawn ETXTBSY\b/.test(evidence)) return false;
23286
+ const steps = [...evidence.matchAll(/"steps":\[([^\]]*)\]/g)].flatMap((m) => [...m[1].matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((s) => s[1]));
23287
+ return steps.length > 0 && steps.every((name) => /\bsetup[- ]bun\b|oven-sh\/setup-bun/i.test(name));
23186
23288
  }
23187
23289
  var TRANSIENT_EVIDENCE_MAX_CHARS = 16e3;
23188
23290
  var AUTO_RERUN_MAX_RETRIES = 1;
@@ -23229,8 +23331,16 @@ async function transientEvidenceForRun(deps, repo, runId) {
23229
23331
  parts.push(await deps.run("gh", ["run", "view", String(runId), "--repo", repo, "--log-failed"]));
23230
23332
  } catch {
23231
23333
  }
23232
- const evidence = parts.filter(Boolean).join("\n");
23233
- return evidence.length > TRANSIENT_EVIDENCE_MAX_CHARS ? evidence.slice(-TRANSIENT_EVIDENCE_MAX_CHARS) : evidence;
23334
+ const [head, ...rest] = parts.filter(Boolean);
23335
+ if (rest.length === 0 || !head?.startsWith("[")) {
23336
+ const evidence = parts.filter(Boolean).join("\n");
23337
+ return evidence.length > TRANSIENT_EVIDENCE_MAX_CHARS ? evidence.slice(-TRANSIENT_EVIDENCE_MAX_CHARS) : evidence;
23338
+ }
23339
+ const header = head.slice(0, TRANSIENT_EVIDENCE_MAX_CHARS / 4);
23340
+ const log = rest.join("\n");
23341
+ const room = TRANSIENT_EVIDENCE_MAX_CHARS - header.length - 1;
23342
+ return `${header}
23343
+ ${log.length > room ? log.slice(-room) : log}`;
23234
23344
  }
23235
23345
  function extractFailedRequiredContexts(failure) {
23236
23346
  const match = /required train check failed:\s*([^\n(]+)/.exec(failure);
@@ -30012,11 +30122,13 @@ function registerBoardCommands(program3) {
30012
30122
  }
30013
30123
  function claimVerdict(ref, result) {
30014
30124
  const holder = formatClaimHolder(result.holder);
30015
- const previousHolder = result.previousHolder ? formatClaimHolder(result.previousHolder) : "another lane";
30125
+ const previousHolder = result.staleStamp ?? (result.previousHolder ? formatClaimHolder(result.previousHolder) : "another lane");
30016
30126
  const reclaimed = result.reclaimedFrom ? ` (reclaimed from ${result.reclaimedFrom})` : "";
30017
30127
  if (result.checked) {
30018
30128
  if (result.outcome === "held") return `Check ${ref}: held by ${holder} - claim would renew the lease (nothing written)`;
30019
- if (result.outcome === "took-over") return `Check ${ref}: held by ${previousHolder} - --force claim would take it over for ${holder} (nothing written)`;
30129
+ if (result.outcome === "took-over") {
30130
+ return result.staleStamp ? `Check ${ref}: ${previousHolder} - claim would take it over for ${holder} (nothing written)` : `Check ${ref}: held by ${previousHolder} - --force claim would take it over for ${holder} (nothing written)`;
30131
+ }
30020
30132
  if (result.outcome === "resumed") return `Check ${ref}: prior lane ${previousHolder} is verifiably dead - claim would resume its work for ${holder} (nothing written; ${result.resumeEvidence})`;
30021
30133
  return `Check ${ref}: free - claim would proceed for ${holder}${reclaimed} (nothing written)`;
30022
30134
  }
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.87",
3
+ "version": "4.5.88",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/codex-plugin",
3
- "version": "4.5.87",
3
+ "version": "4.5.88",
4
4
  "description": "MMI workflow skills and org gates delivery.",
5
5
  "author": {
6
6
  "name": "MMI Future",
@@ -27,9 +27,16 @@ Project. Partial reads exit nonzero; pass `--allow-partial` only when the dev ac
27
27
  Partition, as the CLI computes it:
28
28
 
29
29
  - **Yours** — assignee includes the viewer, status in `Todo · In Progress · In Review`.
30
- - **Free to claim** — status `Todo`, unassigned, and the viewer has push on the repo.
30
+ - **Free to claim** — status `Todo`, unassigned, and the viewer has push on the repo. Also any item in
31
+ any status whose claim stamp is stale: its session has done no work on it for 4 hours. A stale stamp
32
+ holds nothing; `board claim` takes it over without `--force` and names the old stamp.
31
33
  - **Taken** — someone else's, any active status.
32
34
 
35
+ Every stamped row names its session (`lane surface/session@host`), or `stale:<age>` when the stamp is
36
+ stale. Work means the stamp, an issue comment, a commit on the issue's branch, or an update to an open
37
+ PR that closes it. Any of these in the last 4 hours keeps the hold; a date that cannot be read counts as
38
+ work. A takeover keeps the old branch and PR and names them, so the new session continues them.
39
+
33
40
  ## Step 2 — render it
34
41
 
35
42
  Plain markdown, never a fenced block. Flat `-` lists under bold heads, never `##` headings, never tables.
@@ -100,8 +107,9 @@ Claim a whole batch in one call — it shares setup cost and returns per-item re
100
107
  mmi-cli oracle board claim <ref> <ref> <ref> --json
101
108
  ```
102
109
 
103
- Claiming validates `Todo` + unassigned, assigns the viewer, moves Status to `In Progress`, and stamps a
104
- lane-identity comment so other agents can attribute the hold. Any per-item failure exits nonzero — read
110
+ Claiming takes a card that is unassigned or assigned to the viewer, in `Todo`, `In Progress` or
111
+ `In Review`, unless another session still shows work on it in the last 4 hours (then it refuses). It assigns the viewer, moves Status to
112
+ `In Progress`, and stamps a lane-identity comment so other agents can attribute the hold. Any per-item failure exits nonzero — read
105
113
  the per-item results rather than assuming the batch took. **Route is `oracle board claim` only**; there is
106
114
  no `oracle issue claim` and no flat `board claim` — both refuse. Guessing either burns a call.
107
115