@tablation/crew 0.79.10 → 0.80.1

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.
Files changed (2) hide show
  1. package/dist/cli.js +207 -25
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2981,9 +2981,11 @@ function describeRelease(o, route) {
2981
2981
  level: "fail",
2982
2982
  headline: `${route}: release blocked \u2014 tests failed`,
2983
2983
  detail: withHookTail(
2984
- o.composition ? `the target stays on the previous release
2984
+ (o.composition ? `the target stays on the previous release
2985
2985
 
2986
- ${describeComposition(o.composition)}` : "the target stays on the previous release",
2986
+ ${describeComposition(o.composition)}` : "the target stays on the previous release") + (o.isolation ? `
2987
+
2988
+ ${o.isolation}` : ""),
2987
2989
  o.hookFailure
2988
2990
  ),
2989
2991
  ...o.composition ? { head: o.composition.head } : {}
@@ -3013,7 +3015,9 @@ ${describeComposition(o.composition)}` : "the target stays on the previous relea
3013
3015
  return {
3014
3016
  level: "fail",
3015
3017
  headline: `${route}: build FAILED`,
3016
- detail: withHookTail("the target stays on the previous release \u2014 nothing was deployed", o.hookFailure)
3018
+ detail: withHookTail(`the target stays on the previous release \u2014 nothing was deployed${o.isolation ? `
3019
+
3020
+ ${o.isolation}` : ""}`, o.hookFailure)
3017
3021
  };
3018
3022
  }
3019
3023
  if (o.blockedCycles && o.blockedCycles >= 2) {
@@ -11311,7 +11315,7 @@ function planBuildFailureBounce(cwd, c, base, remote) {
11311
11315
  return { ...c, base, remote, worktree: worktreeForBranch(cwd, c.branch) };
11312
11316
  }
11313
11317
  function buildFailureComment(b) {
11314
- const how = b.how === "files" ? `The failing output names files that only \`${b.branch}\` changed among the branches in that release.` : `Built alone on the previous \`${b.base}\`, \`${b.branch}\` fails the same hook.`;
11318
+ const how = b.how === "pair" && b.sibling ? `Each branch passes the hook alone; \`${b.branch}\` together with ${b.sibling.key} (\`${b.sibling.branch}\`) fails it. ${b.sibling.key} was verified first, so the fix belongs on this branch: \`git merge ${b.remote}/${b.sibling.branch}\` first, then fix what disagrees. Fix it on this branch, never a fresh one.` : b.how === "files" ? `The failing output names files that only \`${b.branch}\` changed among the branches in that release.` : `Built alone on the previous \`${b.base}\`, \`${b.branch}\` fails the same hook.`;
11315
11319
  return `${BUILD_FAILURE_MARKER}
11316
11320
 
11317
11321
  **Back with the dev lane \u2014 this branch builds on its own but not on current \`${b.base}\`.**
@@ -11326,8 +11330,46 @@ ${b.tail.trimEnd()}
11326
11330
 
11327
11331
  Nothing was written to \`${b.base}\` for this ticket; the rest of the release shipped without it.`;
11328
11332
  }
11329
- async function applyBuildFailureBounce(writer, b, contract, memberId, log, dryRun) {
11333
+ function priorBuildFailures(ticket, events) {
11334
+ const stamp = (e) => e.occurred_at ?? e.created_at ?? "";
11335
+ const mine = events.filter((e) => e.record_id === ticket.id);
11336
+ const pushes = mine.filter((e) => e.event_type === "pushed").map(stamp);
11337
+ return mine.filter((e) => e.event_type === "build_failed" && pushes.some((p) => p > stamp(e))).length;
11338
+ }
11339
+ function buildFailureEscalation(b) {
11340
+ return `${BUILD_FAILURE_MARKER}
11341
+
11342
+ **Failed the release gate again after a hand-back \u2014 stopping rather than looping.**
11343
+
11344
+ \`${b.branch}\` was handed back for breaking the \`${b.hook}\` hook, pushed, verified again, and breaks it once more. That is a call for a person, not another lap through the lanes.
11345
+
11346
+ \`\`\`
11347
+ ${b.tail.trimEnd()}
11348
+ \`\`\``;
11349
+ }
11350
+ async function applyBuildFailureBounce(writer, b, contract, memberId, log, dryRun, prior = { count: 0 }) {
11330
11351
  const t = { ticket: b.ticket.issue_id, step: "release" };
11352
+ if (prior.count > 0) {
11353
+ if (dryRun) {
11354
+ log.emit(`would escalate to ${contract.statuses.needsHuman} \u2014 handed back for the ${b.hook} hook before`, t);
11355
+ return { kind: "escalated" };
11356
+ }
11357
+ try {
11358
+ await writer.updateTicket(
11359
+ b.ticket.id,
11360
+ { status: contract.statuses.needsHuman, ...prior.operatorId ? { assignee_id: prior.operatorId } : {} },
11361
+ b.ticket.updated_at
11362
+ );
11363
+ await writer.postEvent(b.ticket.id, buildFailureEscalation(b), memberId, "build_failed_escalated").catch(() => {
11364
+ log.warn("escalated, but the note failed to post", t);
11365
+ });
11366
+ log.warn(`breaks the ${b.hook} hook again after a hand-back \u2014 escalated to ${contract.statuses.needsHuman}`, t);
11367
+ return { kind: "escalated" };
11368
+ } catch (e) {
11369
+ if (e instanceof StaleWriteError8) return { kind: "stale" };
11370
+ return { kind: "failed", why: e.message };
11371
+ }
11372
+ }
11331
11373
  if (dryRun) {
11332
11374
  log.emit(`would hand ${b.ticket.issue_id} back to ${contract.statuses.building} \u2014 its branch breaks the ${b.hook} hook`, t);
11333
11375
  return { kind: "handed-back" };
@@ -11341,6 +11383,13 @@ async function applyBuildFailureBounce(writer, b, contract, memberId, log, dryRu
11341
11383
  await writer.postEvent(b.ticket.id, buildFailureComment(b), memberId, "build_failed").catch(() => {
11342
11384
  log.warn("handed back, but the note failed to post", t);
11343
11385
  });
11386
+ await writer.postEvent(
11387
+ b.ticket.id,
11388
+ `${b.ticket.issue_tag ?? b.ticket.issue_id} left out of the release: ${b.hook} hook, found by ${b.how}${b.sibling ? `, together with ${b.sibling.key} (${b.sibling.branch})` : ""}`,
11389
+ memberId,
11390
+ "release_culprit"
11391
+ ).catch(() => {
11392
+ });
11344
11393
  log.emit(`handed back to ${contract.statuses.building} \u2014 breaks the ${b.hook} hook once merged with the rest`, t);
11345
11394
  return { kind: "handed-back" };
11346
11395
  } catch (e) {
@@ -11926,6 +11975,27 @@ function planStamp(cwd, tickets, contract, fromSha, toSha, merged = /* @__PURE__
11926
11975
  function mergedAtFor(cwd, sha, fallback) {
11927
11976
  return cwd && commitDate(cwd, sha) || fallback;
11928
11977
  }
11978
+ async function applyMergeStamp(tracker, merged, tickets, emit, dryRun, cwd) {
11979
+ let stamped = 0;
11980
+ const at = (/* @__PURE__ */ new Date()).toISOString();
11981
+ for (const ticket of tickets) {
11982
+ const sha = merged.get(ticket.issue_id);
11983
+ if (dryRun) {
11984
+ emit.emit(`would stamp commit_sha${sha ? ` (${sha.slice(0, 8)})` : ""}`, { ticket: ticket.issue_id });
11985
+ stamped++;
11986
+ continue;
11987
+ }
11988
+ if (!sha || ticket.commit_sha === sha) continue;
11989
+ try {
11990
+ await tracker.updateTicket(ticket.id, { commit_sha: sha, merged_at: mergedAtFor(cwd, sha, at) });
11991
+ emit.emit(`stamped commit_sha (${sha.slice(0, 8)})`, { ticket: ticket.issue_id });
11992
+ stamped++;
11993
+ } catch (e) {
11994
+ emit.warn(`could not stamp commit_sha: ${e.message}`, { ticket: ticket.issue_id });
11995
+ }
11996
+ }
11997
+ return stamped;
11998
+ }
11929
11999
  async function applyStamp(tracker, plan, version, contract, emit, dryRun, cwd) {
11930
12000
  let stamped = 0;
11931
12001
  const at = (/* @__PURE__ */ new Date()).toISOString();
@@ -11938,6 +12008,7 @@ async function applyStamp(tracker, plan, version, contract, emit, dryRun, cwd) {
11938
12008
  stamped++;
11939
12009
  continue;
11940
12010
  }
12011
+ const recordMerge = !!sha && !ticket.commit_sha;
11941
12012
  try {
11942
12013
  await tracker.updateTicket(ticket.id, {
11943
12014
  status: contract.statuses.deployed,
@@ -11948,7 +12019,7 @@ async function applyStamp(tracker, plan, version, contract, emit, dryRun, cwd) {
11948
12019
  // ticket's record should mean the same thing regardless of which
11949
12020
  // release mode closed it (ISSUE-218). `merged_at` is the commit's own
11950
12021
  // date, not the stamp time (which trails the deploy in ci_* modes).
11951
- ...sha ? { commit_sha: sha, merged_at: mergedAtFor(cwd, sha, at) } : {}
12022
+ ...recordMerge && sha ? { commit_sha: sha, merged_at: mergedAtFor(cwd, sha, at) } : {}
11952
12023
  });
11953
12024
  emit.emit(`stamped ${contract.statuses.deployed}${version ? ` (${version})` : ""}${shaNote}`, {
11954
12025
  ticket: ticket.issue_id
@@ -12672,6 +12743,25 @@ async function runReleaseCycle(o, movedPast2, localRefSkips) {
12672
12743
  }
12673
12744
  }
12674
12745
  }
12746
+ var MAX_ISOLATE_PAIRS = 10;
12747
+ function pairOrder(n, max = MAX_ISOLATE_PAIRS) {
12748
+ const pairs = [];
12749
+ let truncated = false;
12750
+ outer: for (let j = 1; j < n; j++) {
12751
+ for (let i = 0; i < j; i++) {
12752
+ if (pairs.length >= max) {
12753
+ truncated = true;
12754
+ break outer;
12755
+ }
12756
+ pairs.push([i, j]);
12757
+ }
12758
+ }
12759
+ return { pairs, truncated };
12760
+ }
12761
+ function selfRepairLine(maxCandidates) {
12762
+ const cap = maxCandidates ?? DEFAULT_ISOLATE_MAX_CANDIDATES;
12763
+ return cap === 0 ? "culprit isolation is off (release.isolateFailures.maxCandidates: 0); a failed gate files an alert" : `culprit isolation up to ${cap} merged branches, ${MAX_ISOLATE_PAIRS} pairs before bisecting; a ticket handed back twice after a push escalates with an alert`;
12764
+ }
12675
12765
  var isolateCap = (o) => o.repo.release.isolateFailures?.maxCandidates ?? DEFAULT_ISOLATE_MAX_CANDIDATES;
12676
12766
  async function isolateAndRetry(o, name, output, merged, preMerge, decision, fresh, releaseBranch) {
12677
12767
  const applied = merged.filter((c) => c.sha && c.branch);
@@ -12696,13 +12786,13 @@ async function isolateAndRetry(o, name, output, merged, preMerge, decision, fres
12696
12786
  if (baseRun && baseRun.code !== 0) {
12697
12787
  o.emit.warn(`${name} fails on the base without ${only.branch} \u2014 not blaming the branch`, { ticket: only.ticket.issue_id });
12698
12788
  gitOk(o.cwd, ["reset", "--hard", head]);
12699
- return null;
12789
+ return { tried: `Isolation found no culprit: the ${name} hook also fails on the base alone, without ${only.branch}, so there is no branch to hand back.` };
12700
12790
  }
12701
12791
  o.emit.warn(`${name} failed with only ${only.branch} merged and passes without it \u2014 treating it as the culprit`, { ticket: only.ticket.issue_id });
12702
12792
  culprits.push({ c: only, how: "solo", tail: lastLines(output, HOOK_FAILURE_TAIL_LINES) });
12703
12793
  } else if (applied.length > cap) {
12704
12794
  o.emit.warn(`${name} failed with ${applied.length} branches merged, over release.isolateFailures.maxCandidates (${cap}) \u2014 not isolating`);
12705
- return null;
12795
+ return { tried: `Isolation was skipped: ${applied.length} branches were merged, over release.isolateFailures.maxCandidates (${cap}).` };
12706
12796
  } else {
12707
12797
  o.emit.warn(`${name} failed with ${applied.length} branches merged \u2014 building each alone to find the culprit`);
12708
12798
  for (const c of applied) {
@@ -12715,12 +12805,51 @@ async function isolateAndRetry(o, name, output, merged, preMerge, decision, fres
12715
12805
  culprits.push({ c, how: "solo", tail: lastLines(r.output, HOOK_FAILURE_TAIL_LINES) });
12716
12806
  }
12717
12807
  }
12808
+ if (culprits.length === 0) {
12809
+ const { pairs, truncated } = pairOrder(applied.length);
12810
+ for (const [i, j] of pairs) {
12811
+ gitOk(o.cwd, ["reset", "--hard", preMerge]);
12812
+ const a = mergeOne(o, applied[i]);
12813
+ const b = typeof a === "object" && "conflict" in a ? a : mergeOne(o, applied[j]);
12814
+ if (typeof b === "object" && "conflict" in b) continue;
12815
+ const r = await hook(o, name);
12816
+ if (r && r.code !== 0) {
12817
+ o.emit.warn(`${applied[j].branch} fails the ${name} hook together with ${applied[i].branch}`, { ticket: applied[j].ticket.issue_id });
12818
+ culprits.push({ c: applied[j], how: "pair", sibling: applied[i], tail: lastLines(r.output, HOOK_FAILURE_TAIL_LINES) });
12819
+ break;
12820
+ }
12821
+ }
12822
+ if (culprits.length === 0 && truncated) {
12823
+ let lo = 2, hi = applied.length;
12824
+ let tail = lastLines(output, HOOK_FAILURE_TAIL_LINES);
12825
+ while (lo < hi) {
12826
+ const mid = Math.floor((lo + hi) / 2);
12827
+ gitOk(o.cwd, ["reset", "--hard", preMerge]);
12828
+ let clean = true;
12829
+ for (const c of applied.slice(0, mid)) {
12830
+ const m = mergeOne(o, c);
12831
+ if (typeof m === "object" && "conflict" in m) {
12832
+ clean = false;
12833
+ break;
12834
+ }
12835
+ }
12836
+ const r = clean ? await hook(o, name) : null;
12837
+ if (r && r.code !== 0) {
12838
+ hi = mid;
12839
+ tail = lastLines(r.output, HOOK_FAILURE_TAIL_LINES);
12840
+ } else lo = mid + 1;
12841
+ }
12842
+ const owner = applied[hi - 1];
12843
+ o.emit.warn(`${owner.branch} is the first branch whose prefix fails the ${name} hook (bisected over ${applied.length})`, { ticket: owner.ticket.issue_id });
12844
+ culprits.push({ c: owner, how: "pair", sibling: applied[hi - 2], tail });
12845
+ }
12846
+ }
12718
12847
  }
12719
12848
  gitOk(o.cwd, ["reset", "--hard", preMerge]);
12720
12849
  if (culprits.length === 0) {
12721
12850
  o.emit.warn("no single branch could be blamed \u2014 failing the release as a whole");
12722
12851
  gitOk(o.cwd, ["reset", "--hard", head]);
12723
- return null;
12852
+ return { tried: `Isolation found no culprit: each of the ${applied.length} merged branches passes the ${name} hook alone and no pair among them fails it.` };
12724
12853
  }
12725
12854
  const out = new Set(culprits.map((x) => x.c.ticket.id));
12726
12855
  const rest = decision.merges.filter((c) => !out.has(c.ticket.id));
@@ -12732,7 +12861,8 @@ async function isolateAndRetry(o, name, output, merged, preMerge, decision, fres
12732
12861
  branch: x.c.branch,
12733
12862
  hook: name,
12734
12863
  tail: x.tail,
12735
- how: x.how
12864
+ how: x.how,
12865
+ ...x.sibling ? { sibling: { key: x.sibling.ticket.issue_tag ?? x.sibling.ticket.issue_id, branch: x.sibling.branch } } : {}
12736
12866
  }))
12737
12867
  };
12738
12868
  }
@@ -12743,6 +12873,7 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
12743
12873
  const conflicts = [];
12744
12874
  const unbuildable = [];
12745
12875
  let applied = 0;
12876
+ let isolation;
12746
12877
  for (const c of decision.merges) {
12747
12878
  if (!c.branch || c.mergedSha) {
12748
12879
  if (c.skipReason === "already-released") {
@@ -12855,7 +12986,8 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
12855
12986
  }
12856
12987
  if (r && r.code !== 0 && !isolated) {
12857
12988
  const out = await isolateAndRetry(o, "test", r.output, merged, preMerge, decision, fresh, releaseBranch);
12858
- if (out) return out;
12989
+ if (out && "tried" in out) isolation = out.tried;
12990
+ else if (out) return out;
12859
12991
  }
12860
12992
  if (r && r.code !== 0) {
12861
12993
  o.emit.error(`test gate FAILED \u2014 not deploying; the target stays on the previous release`);
@@ -12876,7 +13008,7 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
12876
13008
  label: hookLabel(o.repo, "test"),
12877
13009
  tail: failureSummary(r.output, HOOK_FAILURE_TAIL_LINES)
12878
13010
  };
12879
- return { merged, conflicts, unbuildable, deployed: false, stopped: "tests failed", decision, gateRed, hookFailure: hookFailure2, composition };
13011
+ return { merged, conflicts, unbuildable, deployed: false, stopped: "tests failed", decision, gateRed, hookFailure: hookFailure2, composition, ...isolation ? { isolation } : {} };
12880
13012
  }
12881
13013
  o.state?.clearTestGateFailed?.();
12882
13014
  }
@@ -12915,7 +13047,8 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
12915
13047
  }
12916
13048
  if (r && r.code !== 0 && !isolated) {
12917
13049
  const out = await isolateAndRetry(o, "build", r.output, merged, preMerge, decision, fresh, releaseBranch);
12918
- if (out) return out;
13050
+ if (out && "tried" in out) isolation = out.tried;
13051
+ else if (out) return out;
12919
13052
  }
12920
13053
  if (r && r.code !== 0) {
12921
13054
  o.emit.error("build failed \u2014 not deploying");
@@ -12923,7 +13056,7 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
12923
13056
  git(o.cwd, ["reset", "--hard", preBump]);
12924
13057
  o.emit.warn(`dropped the unreleased version commit for ${version} (reset ${o.repo.branch.base} to ${preBump.slice(0, 8)}) so a retry starts clean`);
12925
13058
  }
12926
- return { merged, conflicts, unbuildable, version, deployed: false, stopped: "build failed", decision, hookFailure: hookFailure(o, "build", r.output) };
13059
+ return { merged, conflicts, unbuildable, version, deployed: false, stopped: "build failed", decision, hookFailure: hookFailure(o, "build", r.output), ...isolation ? { isolation } : {} };
12927
13060
  }
12928
13061
  }
12929
13062
  }
@@ -12961,6 +13094,13 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
12961
13094
  }
12962
13095
  }
12963
13096
  }
13097
+ if (o.repo.release.mode === "ci_auto" || o.repo.release.mode === "ci_manual") {
13098
+ try {
13099
+ await o.onBasePushed?.(merged);
13100
+ } catch (e) {
13101
+ o.emit.warn(`could not record merged tickets: ${e.message}`);
13102
+ }
13103
+ }
12964
13104
  let deployed = false;
12965
13105
  let integrated = false;
12966
13106
  if (o.repo.release.mode === "integrate") {
@@ -18181,6 +18321,19 @@ ${changed2.map((c) => ` - ${c}
18181
18321
  skipTests: flag("skip-tests"),
18182
18322
  shell: cfg.ship.shell,
18183
18323
  mergeOnly: command === "merge",
18324
+ // ci_* modes: record the merge on the board when it lands on base,
18325
+ // not only once CI confirms the release live (CREW-2820).
18326
+ onBasePushed: async (merged) => {
18327
+ remit.enter("reconcile");
18328
+ await applyMergeStamp(
18329
+ tracker,
18330
+ new Map(merged.map((m) => [m.ticket.issue_id, m.sha])),
18331
+ merged.map((m) => m.ticket),
18332
+ remit,
18333
+ dryRun,
18334
+ target.dir
18335
+ );
18336
+ },
18184
18337
  // `deploy` is the "go now" button: it exists for a commit a previous
18185
18338
  // deploy failed on, where nothing new will merge but the work is
18186
18339
  // genuinely unreleased.
@@ -18294,21 +18447,25 @@ ${changed2.map((c) => ` - ${c}
18294
18447
  dryRun
18295
18448
  );
18296
18449
  }
18450
+ const alertPlacement = async () => {
18451
+ const placement = {};
18452
+ try {
18453
+ const repoId = repoIdForName(c, target.name);
18454
+ if (repoId) {
18455
+ placement.repo_id = repoId;
18456
+ const projectId = (await tracker.repoRow(repoId))?.project_id;
18457
+ if (projectId) placement.project_id = projectId;
18458
+ }
18459
+ } catch {
18460
+ }
18461
+ return placement;
18462
+ };
18297
18463
  const news = describeRelease(outcome, scope);
18298
18464
  if (news) {
18299
18465
  const notified = await notify(c, cfg.ship, news, remit, dryRun);
18300
18466
  if (!notified && news.level === "fail") {
18301
18467
  const memberId = releaseSeat(c);
18302
- const placement = {};
18303
- try {
18304
- const repoId = repoIdForName(c, target.name);
18305
- if (repoId) {
18306
- placement.repo_id = repoId;
18307
- const projectId = (await tracker.repoRow(repoId))?.project_id;
18308
- if (projectId) placement.project_id = projectId;
18309
- }
18310
- } catch {
18311
- }
18468
+ const placement = await alertPlacement();
18312
18469
  await applyFailureAlert(tracker, news, scope, all, memberId, remit, dryRun, placement).catch((e) => {
18313
18470
  remit.warn(`could not file/update a failure ticket: ${e.message}`, { step: "release" });
18314
18471
  });
@@ -18332,13 +18489,36 @@ ${changed2.map((c) => ` - ${c}
18332
18489
  }
18333
18490
  if (outcome.buildCulprits?.length) {
18334
18491
  const seat = releaseSeat(c);
18492
+ const prior = [...await tracker.events("build_failed"), ...await tracker.events("pushed")];
18335
18493
  for (const cu of outcome.buildCulprits) {
18336
18494
  try {
18337
18495
  const bounce = planBuildFailureBounce(target.dir, cu, repo.branch.base, repo.branch.remote);
18338
- const r = await applyBuildFailureBounce(tracker, bounce, tracker.contract, seat, remit, dryRun);
18496
+ const r = await applyBuildFailureBounce(
18497
+ tracker,
18498
+ bounce,
18499
+ tracker.contract,
18500
+ seat,
18501
+ remit,
18502
+ dryRun,
18503
+ { count: priorBuildFailures(cu.ticket, prior), operatorId: c.resolved?.operator }
18504
+ );
18339
18505
  if (r.kind === "failed") {
18340
18506
  remit.warn(`could not hand back ${cu.ticket.issue_id}: ${r.why}`, { step: "release" });
18341
18507
  }
18508
+ if (r.kind === "escalated" && !dryRun) {
18509
+ const key = cu.ticket.issue_tag ?? cu.ticket.issue_id;
18510
+ await applyFailureAlert(tracker, {
18511
+ level: "fail",
18512
+ headline: `${scope}: release blocked \u2014 ${key} broke the ${cu.hook} gate twice`,
18513
+ detail: `${key} (\`${cu.branch}\`) was handed back after a failed ${cu.hook} gate, pushed and re-verified, and failed it again. Isolation found it both times (${cu.how}), so it is escalated to the operator instead of another lap.
18514
+
18515
+ \`\`\`
18516
+ ${cu.tail.trimEnd()}
18517
+ \`\`\``
18518
+ }, scope, all, seat, remit, dryRun, await alertPlacement()).catch((e) => {
18519
+ remit.warn(`could not file/update a failure ticket: ${e.message}`, { step: "release" });
18520
+ });
18521
+ }
18342
18522
  } catch (e) {
18343
18523
  remit.warn(`could not hand back ${cu.ticket.issue_id}: ${e.message}`, { step: "release" });
18344
18524
  }
@@ -21546,6 +21726,8 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
21546
21726
  process.stdout.write(` - release readiness: ${p2}
21547
21727
  `);
21548
21728
  }
21729
+ process.stdout.write(` release self-repair: ${selfRepairLine(config.release.isolateFailures?.maxCandidates)}
21730
+ `);
21549
21731
  const dh = doctorBaseReport(r, config.branch.base, baseHealth);
21550
21732
  process.stdout.write(dh.text);
21551
21733
  if (dh.failed) process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.79.10",
3
+ "version": "0.80.1",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",