@tablation/crew 0.54.2 → 0.54.4

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 +111 -40
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -3404,6 +3404,69 @@ function validateContract(c) {
3404
3404
  }
3405
3405
  var EVENT_LOG_MISSING = "the Issues table has no event log \u2014 crew writes its audit trail there (CREW-1574). In the app, open the Issues table's Data & lifecycle tab and turn on Event log, then rerun.";
3406
3406
 
3407
+ // src/callout.ts
3408
+ var calloutHeader = (kind, label) => `> [!${kind}]
3409
+ > **Crew** \u2014 ${label}`;
3410
+ var calloutLabelLine = (label) => `> **Crew** \u2014 ${label}`;
3411
+
3412
+ // src/gate-red.ts
3413
+ var GATE_RED_MARKER = calloutHeader("CAUTION", "Test Gate Red");
3414
+ var shaLine = (g) => `Head \`${g.sha.slice(0, 8)}\`.`;
3415
+ var GATE_RED_AFTER = 3;
3416
+ var lastLines = (output, n) => output.replace(/\u001b\[[0-9;]*m/g, "").trimEnd().split("\n").slice(-n).join("\n");
3417
+ function gateRedTickets(merged, merges) {
3418
+ const seen = /* @__PURE__ */ new Set();
3419
+ const out = [];
3420
+ for (const c of [...merged, ...merges.filter((m) => m.skipReason === "already-merged")]) {
3421
+ if (seen.has(c.ticket.id)) continue;
3422
+ seen.add(c.ticket.id);
3423
+ out.push(c.ticket);
3424
+ }
3425
+ return out;
3426
+ }
3427
+ function gateRedComment(g) {
3428
+ return `${GATE_RED_MARKER}
3429
+ > ${shaLine(g)}
3430
+
3431
+ **This ticket is merged on the base, but the release test gate is red \u2014 nothing has shipped.**
3432
+
3433
+ The \`test\` hook (\`${g.hook}\`) has failed ${g.count} cycles running on the same head (\`${g.sha.slice(0, 8)}\`). The ticket's commit is on the base and will ship once the gate passes; it is not stranded, and re-setting it to \`verified\` will not help. Last lines of the hook's output:
3434
+
3435
+ \`\`\`
3436
+ ${g.tail}
3437
+ \`\`\`
3438
+ `;
3439
+ }
3440
+ var alreadyReported = (ticket, g, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "gate_red" && (e.message ?? "").includes(shaLine(g)));
3441
+
3442
+ // src/test-evidence.ts
3443
+ var MAX_FAIL_LINES = 20;
3444
+ var ANSI = /\u001b\[[0-9;]*m/g;
3445
+ var FAIL_LINE = /^\s*(FAIL\b|●\s|not ok\b)/;
3446
+ var ERROR_LINE = /\b(?:[A-Z]\w*)?Error\b[:\s]/;
3447
+ function failureSummary(output, tailLines = 15) {
3448
+ const lines = output.replace(ANSI, "").trimEnd().split("\n");
3449
+ const names = [];
3450
+ for (const l of lines) {
3451
+ if (FAIL_LINE.test(l) && !names.includes(l.trim())) names.push(l.trim());
3452
+ if (names.length >= MAX_FAIL_LINES) break;
3453
+ }
3454
+ const at = lines.findIndex((l) => ERROR_LINE.test(l));
3455
+ if (names.length === 0 && at < 0) return lastLines(output, tailLines);
3456
+ const parts = [...names];
3457
+ if (at >= 0) parts.push("", ...lines.slice(at, at + 4).map((l) => l.trimEnd()));
3458
+ return parts.join("\n").trim();
3459
+ }
3460
+ var headLine = (head) => `Candidate head \`${head.slice(0, 8)}\`.`;
3461
+ function describeComposition(c) {
3462
+ const rows = c.branches.length ? c.branches.map((b) => `- ${b.key} \`${b.branch}\` @ \`${b.tip.slice(0, 8)}\``).join("\n") : "- (none merged this cycle)";
3463
+ return `${headLine(c.head)} Base before merging \`${c.base.slice(0, 8)}\`. Merged branches:
3464
+
3465
+ ${rows}
3466
+
3467
+ The gate runs on the base plus every verified branch above, not on the base alone: reproduce on that composition.`;
3468
+ }
3469
+
3407
3470
  // src/shell.ts
3408
3471
  import { spawn } from "child_process";
3409
3472
  import { platform } from "process";
@@ -3515,7 +3578,13 @@ function describeRelease(o, route) {
3515
3578
  return {
3516
3579
  level: "fail",
3517
3580
  headline: `${route}: release blocked \u2014 tests failed`,
3518
- detail: "the target stays on the previous release"
3581
+ detail: withHookTail(
3582
+ o.composition ? `the target stays on the previous release
3583
+
3584
+ ${describeComposition(o.composition)}` : "the target stays on the previous release",
3585
+ o.hookFailure
3586
+ ),
3587
+ ...o.composition ? { head: o.composition.head } : {}
3519
3588
  };
3520
3589
  }
3521
3590
  if (o.stopped === "setup failed") {
@@ -6497,11 +6566,6 @@ function materializeBranchForTicket(dir, cfg, t, role, onProblem) {
6497
6566
  import { writeFileSync as writeFileSync7 } from "fs";
6498
6567
  import { join as join11 } from "path";
6499
6568
 
6500
- // src/callout.ts
6501
- var calloutHeader = (kind, label) => `> [!${kind}]
6502
- > **Crew** \u2014 ${label}`;
6503
- var calloutLabelLine = (label) => `> **Crew** \u2014 ${label}`;
6504
-
6505
6569
  // src/stalled.ts
6506
6570
  var STALLED_AFTER_CYCLES = 30;
6507
6571
  var CYCLE_MS = 120 * 1e3;
@@ -7229,36 +7293,6 @@ async function applyStrandedVerified(writer, s, contract, operatorId, memberId,
7229
7293
  }
7230
7294
  }
7231
7295
 
7232
- // src/gate-red.ts
7233
- var GATE_RED_MARKER = calloutHeader("CAUTION", "Test Gate Red");
7234
- var shaLine = (g) => `Head \`${g.sha.slice(0, 8)}\`.`;
7235
- var GATE_RED_AFTER = 3;
7236
- var lastLines = (output, n) => output.replace(/\u001b\[[0-9;]*m/g, "").trimEnd().split("\n").slice(-n).join("\n");
7237
- function gateRedTickets(merged, merges) {
7238
- const seen = /* @__PURE__ */ new Set();
7239
- const out = [];
7240
- for (const c of [...merged, ...merges.filter((m) => m.skipReason === "already-merged")]) {
7241
- if (seen.has(c.ticket.id)) continue;
7242
- seen.add(c.ticket.id);
7243
- out.push(c.ticket);
7244
- }
7245
- return out;
7246
- }
7247
- function gateRedComment(g) {
7248
- return `${GATE_RED_MARKER}
7249
- > ${shaLine(g)}
7250
-
7251
- **This ticket is merged on the base, but the release test gate is red \u2014 nothing has shipped.**
7252
-
7253
- The \`test\` hook (\`${g.hook}\`) has failed ${g.count} cycles running on the same head (\`${g.sha.slice(0, 8)}\`). The ticket's commit is on the base and will ship once the gate passes; it is not stranded, and re-setting it to \`verified\` will not help. Last lines of the hook's output:
7254
-
7255
- \`\`\`
7256
- ${g.tail}
7257
- \`\`\`
7258
- `;
7259
- }
7260
- var alreadyReported = (ticket, g, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "gate_red" && (e.message ?? "").includes(shaLine(g)));
7261
-
7262
7296
  // src/agent.ts
7263
7297
  import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
7264
7298
  import { readFileSync as readFileSync12, existsSync as existsSync14, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
@@ -10507,7 +10541,7 @@ var isolateCap = (o) => o.repo.release.isolateFailures?.maxCandidates ?? DEFAULT
10507
10541
  async function isolateAndRetry(o, name, output, merged, preMerge, decision, fresh, releaseBranch) {
10508
10542
  const applied = merged.filter((c) => c.sha && c.branch);
10509
10543
  const cap = isolateCap(o);
10510
- if (o.dryRun || !preMerge || applied.length < 2 || cap === 0) return null;
10544
+ if (o.dryRun || !preMerge || applied.length < 1 || cap === 0) return null;
10511
10545
  const head = headSha(o.cwd);
10512
10546
  const withFiles = applied.map((c) => ({
10513
10547
  c,
@@ -10520,6 +10554,17 @@ async function isolateAndRetry(o, name, output, merged, preMerge, decision, fres
10520
10554
  if (byFile) {
10521
10555
  o.emit.warn(`${name} failed; its output names files only ${byFile.c.branch} changed \u2014 treating it as the culprit`, { ticket: byFile.c.ticket.issue_id });
10522
10556
  culprits.push({ c: byFile.c, how: "files", tail: lastLines(output, HOOK_FAILURE_TAIL_LINES) });
10557
+ } else if (applied.length === 1) {
10558
+ const only = applied[0];
10559
+ gitOk(o.cwd, ["reset", "--hard", preMerge]);
10560
+ const baseRun = await hook(o, name);
10561
+ if (baseRun && baseRun.code !== 0) {
10562
+ o.emit.warn(`${name} fails on the base without ${only.branch} \u2014 not blaming the branch`, { ticket: only.ticket.issue_id });
10563
+ gitOk(o.cwd, ["reset", "--hard", head]);
10564
+ return null;
10565
+ }
10566
+ 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 });
10567
+ culprits.push({ c: only, how: "solo", tail: lastLines(output, HOOK_FAILURE_TAIL_LINES) });
10523
10568
  } else if (applied.length > cap) {
10524
10569
  o.emit.warn(`${name} failed with ${applied.length} branches merged, over release.isolateFailures.maxCandidates (${cap}) \u2014 not isolating`);
10525
10570
  return null;
@@ -10682,7 +10727,21 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
10682
10727
  const sha = headSha(o.cwd);
10683
10728
  const count = o.state?.noteTestGateFailed?.(sha) ?? 1;
10684
10729
  const gateRed = count >= GATE_RED_AFTER && !o.state?.testGateReported?.(sha) ? { sha, count, hook: hookLabel(o.repo, "test"), tail: lastLines(r.output, 15) } : void 0;
10685
- return { merged, conflicts, unbuildable, deployed: false, stopped: "tests failed", decision, gateRed };
10730
+ const composition = {
10731
+ head: sha,
10732
+ base: preMerge,
10733
+ branches: merged.filter((c) => c.branch).map((c) => ({
10734
+ key: c.ticket.issue_tag ?? c.ticket.issue_id,
10735
+ branch: c.branch,
10736
+ tip: resolve2(o.cwd, c.branch) ?? ""
10737
+ }))
10738
+ };
10739
+ const hookFailure2 = {
10740
+ hook: "test",
10741
+ label: hookLabel(o.repo, "test"),
10742
+ tail: failureSummary(r.output, HOOK_FAILURE_TAIL_LINES)
10743
+ };
10744
+ return { merged, conflicts, unbuildable, deployed: false, stopped: "tests failed", decision, gateRed, hookFailure: hookFailure2, composition };
10686
10745
  }
10687
10746
  o.state?.clearTestGateFailed?.();
10688
10747
  }
@@ -10708,7 +10767,7 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
10708
10767
  if (o.repo.hooks.build) {
10709
10768
  if (o.dryRun) {
10710
10769
  o.emit.emit(`would run: ${hookLabel(o.repo, "build")}`);
10711
- if (applied > 1) {
10770
+ if (applied >= 1) {
10712
10771
  o.emit.emit(
10713
10772
  `if it fails, would find the one of ${applied} merged branches that broke it (by the files it names, else by building each alone, up to ${isolateCap(o)}), hand that back to the dev lane and ship the rest`
10714
10773
  );
@@ -10854,9 +10913,15 @@ async function applyFailureAlert(writer, n, route, openTickets, memberId, log, d
10854
10913
  return { kind: "commented", issueId: label };
10855
10914
  }
10856
10915
  try {
10916
+ let body = n.detail ?? n.headline;
10917
+ if (n.head && writer.events) {
10918
+ const line = headLine(n.head);
10919
+ const shown = (await writer.events("failure_alert")).some((e) => e.record_id === existing.id && (e.message ?? "").includes(line));
10920
+ if (shown) body = `${n.headline} \u2014 ${line}`;
10921
+ }
10857
10922
  await writer.postEvent(existing.id, `${FAILURE_ALERT_MARKER}
10858
10923
 
10859
- Still failing: ${n.detail ?? n.headline}`, memberId, "failure_alert");
10924
+ Still failing: ${body}`, memberId, "failure_alert");
10860
10925
  log.warn(`release failure recurring \u2014 commented on ${label}`, { step: "release" });
10861
10926
  return { kind: "commented", issueId: label };
10862
10927
  } catch (e) {
@@ -10878,6 +10943,12 @@ Still failing: ${n.detail ?? n.headline}`, memberId, "failure_alert");
10878
10943
  ...placement.project_id ? { project_id: placement.project_id } : {}
10879
10944
  }, memberId);
10880
10945
  const label = created.issue_id ? displayKey(created) : created.id;
10946
+ if (n.head) {
10947
+ await writer.postEvent(created.id, `${FAILURE_ALERT_MARKER}
10948
+
10949
+ Filed with the evidence above. ${headLine(n.head)}`, memberId, "failure_alert").catch(() => {
10950
+ });
10951
+ }
10881
10952
  log.warn(`release failure \u2014 filed ${label}`, { step: "release" });
10882
10953
  return { kind: "filed", issueId: label };
10883
10954
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.54.2",
3
+ "version": "0.54.4",
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",