@tablation/crew 0.73.2 → 0.73.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 +29 -11
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -4239,7 +4239,7 @@ function describeComposition(c) {
4239
4239
 
4240
4240
  ${rows}
4241
4241
 
4242
- The gate runs on the base plus every verified branch above, not on the base alone: reproduce on that composition.`;
4242
+ The gate runs on the base plus every verified branch above, not on the base alone: reproduce on that composition. A fix that compiles only on the composition belongs on one of those branches (the one owning the failing file, after merging the others into it), not on a new branch cut from the base: a separate fix branch conflicts with the branches it contains when the release squash-merges them all. Re-queue the touched ticket to QA and close this alert with a note.`;
4243
4243
  }
4244
4244
 
4245
4245
  // src/shell.ts
@@ -10331,7 +10331,8 @@ var lastActedHandBack = (ticket, events) => {
10331
10331
  return mine.filter((e) => e.event_type === "conflict_handed_back" && pushes.some((p) => p > stampOf(e))).sort((a, b) => stampOf(b).localeCompare(stampOf(a)))[0];
10332
10332
  };
10333
10333
  var MAX_ABSORBED_HANDBACKS = 2;
10334
- function planConflictBounce(cwd, ticket, branch, base, paths, events) {
10334
+ function planConflictBounce(cwd, ticket, branch, base, paths, events, merged = []) {
10335
+ const siblings = merged.filter((m) => m.branch !== branch && paths.some((p) => (gitOk(cwd, ["diff", "--name-only", `${base}...${m.branch}`, "--", p]) ?? "").trim() !== ""));
10335
10336
  return {
10336
10337
  ticket,
10337
10338
  branch,
@@ -10341,7 +10342,8 @@ function planConflictBounce(cwd, ticket, branch, base, paths, events) {
10341
10342
  priorBounces: priorBounces(ticket, events),
10342
10343
  cwd,
10343
10344
  baseSha: gitOk(cwd, ["rev-parse", "--verify", "-q", base]) || null,
10344
- priorBaseSha: baseShaOf(lastActedHandBack(ticket, events) ?? { record_id: "", event_type: "" })
10345
+ priorBaseSha: baseShaOf(lastActedHandBack(ticket, events) ?? { record_id: "", event_type: "" }),
10346
+ siblings
10345
10347
  };
10346
10348
  }
10347
10349
  function refreshBranch(worktree, base) {
@@ -10370,7 +10372,7 @@ function handBackComment(b, merged) {
10370
10372
 
10371
10373
  **Back with the dev lane \u2014 this branch no longer merges.**
10372
10374
 
10373
- \`${b.branch}\` was verified, but squash-merging it into \`${b.base}\` conflicts in ${list}. Work that landed on \`${b.base}\` after this branch was cut has changed the same lines.
10375
+ \`${b.branch}\` was verified, but squash-merging it into \`${b.base}\` conflicts in ${list}. ${b.siblings?.length ? `This conflicts with ${b.siblings.map((x) => `${x.key} (\`${x.branch}\`)`).join(", ")}, merged earlier in the same release, not with a \`${b.base}\` that moved: merging \`${b.base}\` in will change nothing. Merge ${b.siblings.length === 1 ? "that branch" : "those branches"} into this one (or move this change onto ${b.siblings.length === 1 ? "it" : "one of them"}), resolve, and push.` : `Work that landed on \`${b.base}\` after this branch was cut has changed the same lines.`}
10374
10376
 
10375
10377
  ${merged ? `\`${b.base}\` has been merged into \`${b.branch}\` in its worktree and the conflict is left in place, so \`git status\` there shows exactly what disagrees. Resolve it, commit, push as a plain fast-forward (the branch is pushed, so rebasing it is not an option) and hand back to QA as usual \u2014 \`git merge --abort\` backs the merge out if you would rather start differently.` : `This ship has no worktree for \`${b.branch}\`, so nothing has been merged for you. Merge \`${b.base}\` into the branch wherever it lives, resolve the conflict, and commit.`}
10376
10378
 
@@ -14749,7 +14751,15 @@ function planFix(f) {
14749
14751
  const actions = [];
14750
14752
  const notes = [];
14751
14753
  for (const r of f.routes) {
14752
- if (!r.enabled && sound(r)) actions.push({ kind: "enable", route: r.route });
14754
+ if (!r.enabled && sound(r)) {
14755
+ actions.push({ kind: "enable", route: r.route });
14756
+ const gaps = r.readiness ?? [];
14757
+ if (gaps.length) {
14758
+ notes.push(`${r.route}: ${gaps.length} release-readiness gap${gaps.length === 1 ? "" : "s"} (${gaps[0]}${gaps.length > 1 ? ", ..." : ""}) \u2014 the route will run, but releases stop at the first missing hook until it is added`);
14759
+ }
14760
+ } else if (!sound(r) && !r.enabled) {
14761
+ notes.push(`${r.route}: not enabled \u2014 ${r.hasRepos ? r.problems[0] : "no checkout is configured"}`);
14762
+ }
14753
14763
  }
14754
14764
  if (f.schedulerAbsent && f.routes.some(sound)) {
14755
14765
  if (f.host === "windows") notes.push("scheduler support is not built for Windows yet \u2014 nothing to install; run `crew run` by hand");
@@ -14759,12 +14769,15 @@ function planFix(f) {
14759
14769
  }
14760
14770
  async function gatherHealth(c, check) {
14761
14771
  let problems;
14772
+ let readiness = [];
14762
14773
  try {
14763
- problems = await check();
14774
+ const r = await check();
14775
+ if (Array.isArray(r)) problems = r;
14776
+ else ({ problems, readiness } = r);
14764
14777
  } catch (e) {
14765
14778
  problems = [`checks could not run: ${e instanceof Error ? e.message : String(e)}`];
14766
14779
  }
14767
- return { route: c.route, enabled: c.enabled, hasRepos: c.hasRepos, problems };
14780
+ return { route: c.route, enabled: c.enabled, hasRepos: c.hasRepos, problems, readiness };
14768
14781
  }
14769
14782
  function routesInScope(routes, named) {
14770
14783
  return named ? routes.filter((r) => r.route === named) : routes;
@@ -17096,6 +17109,7 @@ if (command === "passenger-sync-daemon") {
17096
17109
  async function routeProblems(c) {
17097
17110
  const host = hostPlatform();
17098
17111
  const problems = [];
17112
+ const readiness = [];
17099
17113
  if (c.hostPassengers) {
17100
17114
  if (!dockerAvailable()) problems.push("host passengers: docker is not available");
17101
17115
  if (!sshKeygenAvailable()) problems.push("host passengers: ssh-keygen is not available");
@@ -17105,7 +17119,7 @@ if (command === "passenger-sync-daemon") {
17105
17119
  for (const r of await resolvedRepos(c)) {
17106
17120
  if (!existsSync34(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
17107
17121
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
17108
- problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
17122
+ readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
17109
17123
  }
17110
17124
  try {
17111
17125
  const ships = await tracker.shipRows();
@@ -17117,7 +17131,7 @@ if (command === "passenger-sync-daemon") {
17117
17131
  } catch (e) {
17118
17132
  problems.push(`tracker unreachable: ${e instanceof Error ? e.message : String(e)}`);
17119
17133
  }
17120
- return problems;
17134
+ return { problems, readiness };
17121
17135
  }
17122
17136
  async function doctorFix(o) {
17123
17137
  const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
@@ -17559,7 +17573,8 @@ ${changed2.map((c) => ` - ${c}
17559
17573
  f.candidate.branch,
17560
17574
  repo.branch.base,
17561
17575
  f.paths,
17562
- handedBack
17576
+ handedBack,
17577
+ outcome.merged.filter((m) => m.branch).map((m) => ({ key: m.ticket.issue_id, branch: m.branch }))
17563
17578
  );
17564
17579
  const r = await applyConflictBounce(tracker, bounce, tracker.contract, seat, remit, dryRun);
17565
17580
  if (r.kind === "failed") {
@@ -20404,6 +20419,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20404
20419
  // Additive (CREW-1379): `unsafe` means a ship must not cut a
20405
20420
  // worktree or release from this base. `version` stays 1.
20406
20421
  baseBranch: baseHealth(r),
20422
+ // Additive (CREW-2537): gaps that only matter once a ticket reaches
20423
+ // release (no deploy hook, no CI provider...). They never stop a poll.
20424
+ releaseReadiness: validateEffective(r.config),
20407
20425
  // Additive (CREW-2051): worktrees the sweep refuses to remove.
20408
20426
  heldWorktrees: openShipAttention(state).filter((i) => i.kind === "worktree_held" && i.held?.repo === r.name).map((i) => ({
20409
20427
  path: i.held.path,
@@ -20613,7 +20631,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
20613
20631
  );
20614
20632
  }
20615
20633
  for (const p2 of validateEffective(config)) {
20616
- process.stdout.write(` - ${p2}
20634
+ process.stdout.write(` - release readiness: ${p2}
20617
20635
  `);
20618
20636
  }
20619
20637
  const dh = doctorBaseReport(r, config.branch.base, baseHealth);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.73.2",
3
+ "version": "0.73.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",