@tablation/crew 0.54.3 → 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 +98 -38
  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";
@@ -10693,7 +10727,21 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
10693
10727
  const sha = headSha(o.cwd);
10694
10728
  const count = o.state?.noteTestGateFailed?.(sha) ?? 1;
10695
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;
10696
- 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 };
10697
10745
  }
10698
10746
  o.state?.clearTestGateFailed?.();
10699
10747
  }
@@ -10865,9 +10913,15 @@ async function applyFailureAlert(writer, n, route, openTickets, memberId, log, d
10865
10913
  return { kind: "commented", issueId: label };
10866
10914
  }
10867
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
+ }
10868
10922
  await writer.postEvent(existing.id, `${FAILURE_ALERT_MARKER}
10869
10923
 
10870
- Still failing: ${n.detail ?? n.headline}`, memberId, "failure_alert");
10924
+ Still failing: ${body}`, memberId, "failure_alert");
10871
10925
  log.warn(`release failure recurring \u2014 commented on ${label}`, { step: "release" });
10872
10926
  return { kind: "commented", issueId: label };
10873
10927
  } catch (e) {
@@ -10889,6 +10943,12 @@ Still failing: ${n.detail ?? n.headline}`, memberId, "failure_alert");
10889
10943
  ...placement.project_id ? { project_id: placement.project_id } : {}
10890
10944
  }, memberId);
10891
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
+ }
10892
10952
  log.warn(`release failure \u2014 filed ${label}`, { step: "release" });
10893
10953
  return { kind: "filed", issueId: label };
10894
10954
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.54.3",
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",