@tablation/crew 0.73.0 → 0.73.2

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 +80 -10
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -3563,7 +3563,7 @@ function remoteBranchForIssue(cwd, remote, issueKey, templates = { name: "issue-
3563
3563
  }
3564
3564
  return null;
3565
3565
  }
3566
- function levelRemoteBranch(cwd, remote, branch) {
3566
+ function levelRemoteBranch(cwd, remote, branch, baseRef) {
3567
3567
  const local = gitOk(cwd, ["rev-parse", "-q", "--verify", `refs/heads/${branch}`]);
3568
3568
  if (!remoteBranchExists(cwd, remote, branch)) return { exists: local !== null };
3569
3569
  if (local === null) {
@@ -3572,6 +3572,20 @@ function levelRemoteBranch(cwd, remote, branch) {
3572
3572
  const remoteTip2 = gitOk(cwd, ["rev-parse", "-q", "--verify", `refs/remotes/${remote}/${branch}`]);
3573
3573
  if (!remoteTip2 || remoteTip2 === local) return { exists: true };
3574
3574
  if (!isAncestor(cwd, local, remoteTip2)) {
3575
+ if (baseRef && isAncestor(cwd, local, baseRef)) {
3576
+ const holder3 = worktrees(cwd).find((w) => w.branch === branch && !w.prunable);
3577
+ if (holder3) {
3578
+ if (gitOk(holder3.path, ["status", "--porcelain"])) {
3579
+ return { exists: true, problem: `${branch} holds only commits already on ${baseRef}, but the worktree ${holder3.path} that holds it has uncommitted changes` };
3580
+ }
3581
+ if (gitOk(holder3.path, ["reset", "--hard", "--quiet", remoteTip2]) === null) {
3582
+ return { exists: true, problem: `could not reset ${branch} to ${remote}/${branch} in ${holder3.path}` };
3583
+ }
3584
+ } else if (gitOk(cwd, ["branch", "-f", branch, remoteTip2]) === null) {
3585
+ return { exists: true, problem: `could not reset ${branch} to ${remote}/${branch}` };
3586
+ }
3587
+ return { exists: true, reset: `${branch} held only commits already on ${baseRef}; reset ${local.slice(0, 8)} -> ${remote}/${branch} at ${remoteTip2.slice(0, 8)}` };
3588
+ }
3575
3589
  const diverged = !isAncestor(cwd, remoteTip2, local);
3576
3590
  const holder2 = diverged ? worktrees(cwd).find((w) => w.branch === branch && !w.prunable) : void 0;
3577
3591
  if (holder2) {
@@ -6483,11 +6497,12 @@ function locateBranchForTicket(dir, cfg, t, role, fetchedDirs) {
6483
6497
  const found = remoteBranchForIssue(dir, remote, t.issue_id, branchTemplates(cfg, t), branchRenderer(t, role, cfg.branch.lowercase));
6484
6498
  return found ? { branch: found, where: "remote" } : null;
6485
6499
  }
6486
- function materializeBranchForTicket(dir, cfg, t, role, onProblem) {
6500
+ function materializeBranchForTicket(dir, cfg, t, role, onProblem, onReset) {
6487
6501
  const loc = locateBranchForTicket(dir, cfg, t, role);
6488
6502
  if (!loc) return null;
6489
- const r = levelRemoteBranch(dir, cfg.branch.remote, loc.branch);
6503
+ const r = levelRemoteBranch(dir, cfg.branch.remote, loc.branch, `${cfg.branch.remote}/${cfg.branch.base}`);
6490
6504
  if (r.problem) onProblem?.(r.problem);
6505
+ if (r.reset) onReset?.(r.reset);
6491
6506
  return loc.branch;
6492
6507
  }
6493
6508
 
@@ -6774,10 +6789,10 @@ function applyRunWorktree(dir, cfg, plan) {
6774
6789
  case "recut":
6775
6790
  git(dir, ["worktree", "remove", "--force", plan.path]);
6776
6791
  gitOk(dir, ["branch", "-D", plan.branch]);
6777
- git(dir, ["worktree", "add", "-b", plan.branch, plan.path, plan.base]);
6792
+ git(dir, ["worktree", "add", "--no-track", "-b", plan.branch, plan.path, plan.base]);
6778
6793
  return plan.path;
6779
6794
  case "cut-fresh":
6780
- git(dir, ["worktree", "add", "-b", plan.branch, plan.path, plan.base]);
6795
+ git(dir, ["worktree", "add", "--no-track", "-b", plan.branch, plan.path, plan.base]);
6781
6796
  return plan.path;
6782
6797
  }
6783
6798
  }
@@ -6785,6 +6800,19 @@ function worktreeHoldsUnpushed(path, remote) {
6785
6800
  const out = gitOk(path, ["for-each-ref", "--contains", "HEAD", "--count=1", "--format=%(refname)", `refs/remotes/${remote}/`]);
6786
6801
  return out === null || out.trim() === "";
6787
6802
  }
6803
+ function dropEmptyRunBranch(dir, cfg, branch) {
6804
+ const { remote, base } = cfg.branch;
6805
+ const baseRef = remoteConfigured(dir, remote) ? `${remote}/${base}` : base;
6806
+ if (gitOk(dir, ["rev-parse", "-q", "--verify", `refs/heads/${branch}`]) === null) return false;
6807
+ if (remoteConfigured(dir, remote) && remoteBranchExists(dir, remote, branch)) return false;
6808
+ if (countCommits(dir, `${baseRef}..${branch}`) !== 0) return false;
6809
+ const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
6810
+ if (wt && existsSync14(wt.path)) {
6811
+ if (status(wt.path).length > 0) return false;
6812
+ if (gitOk(dir, ["worktree", "remove", wt.path]) === null) return false;
6813
+ }
6814
+ return gitOk(dir, ["branch", "-D", branch]) !== null;
6815
+ }
6788
6816
  async function finishRunWorktree(dir, cfg, branch) {
6789
6817
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
6790
6818
  if (!wt || !existsSync14(wt.path)) return { kind: "none" };
@@ -6953,6 +6981,12 @@ async function guardRunHandoff(route, ship, tracker, role, workingId, log, since
6953
6981
  log
6954
6982
  });
6955
6983
  if (outcome.kind === "pushed") await retireRunWorktree(tracker, target.dir, cfg, t, role, seatId, ship.name, log);
6984
+ else {
6985
+ const branch = existingBranchForTicket(target.dir, cfg, t, role);
6986
+ if (branch && dropEmptyRunBranch(target.dir, cfg, branch)) {
6987
+ log.emit(`${t.issue_id}: dropped ${branch} \u2014 the run made no commits beyond ${cfg.branch.base}`, { ticket: t.issue_id, step: "handoff" });
6988
+ }
6989
+ }
6956
6990
  if (since2) await guardRunStrays(route, ship, tracker, role, workingId, since2, log);
6957
6991
  return outcome;
6958
6992
  } catch (e) {
@@ -10584,6 +10618,19 @@ async function applyStrandedVerified(writer, s, contract, operatorId, memberId,
10584
10618
  }
10585
10619
  }
10586
10620
 
10621
+ // src/local-ref-skip.ts
10622
+ var LOCAL_REF_SKIP_MARKER = calloutHeader("WARNING", "Release Skipped");
10623
+ function localRefSkipComment(shipName2, problem) {
10624
+ return `${LOCAL_REF_SKIP_MARKER}
10625
+ > ${problem}
10626
+
10627
+ **This ticket is verified but was not released: the release ship's local branch disagrees with the remote.**
10628
+
10629
+ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody verified and retries every cycle. If the local branch holds only commits already on the base, run \`git branch -f <branch> <remote>/<branch>\` in the release checkout (the crew now does this itself when it can); otherwise a person needs to reconcile the branch.
10630
+ `;
10631
+ }
10632
+ var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
10633
+
10587
10634
  // src/connect-wizard.ts
10588
10635
  import { delimiter, dirname as dirname10, join as join23 } from "path";
10589
10636
  import { accessSync as accessSync2, constants as constants2, existsSync as existsSync21, mkdirSync as mkdirSync13, writeFileSync as writeFileSync15 } from "fs";
@@ -11641,7 +11688,7 @@ function openReleaseCheckout(o) {
11641
11688
  o.emit.emit(`release checkout: ${dir}`, { step: "release" });
11642
11689
  const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
11643
11690
  try {
11644
- git(o.cwd, ["worktree", "add", "-q", "-b", branch, dir, `${remote}/${base}`]);
11691
+ git(o.cwd, ["worktree", "add", "-q", "--no-track", "-b", branch, dir, `${remote}/${base}`]);
11645
11692
  } catch (e) {
11646
11693
  close();
11647
11694
  throw e;
@@ -11667,10 +11714,15 @@ function reportHeld(o, held) {
11667
11714
  }
11668
11715
  async function runRelease(o) {
11669
11716
  const movedPast2 = [];
11670
- const out = await runReleaseCycle(o, movedPast2);
11671
- return movedPast2.length ? { ...out, movedPast: movedPast2 } : out;
11717
+ const localRefSkips = [];
11718
+ const out = await runReleaseCycle(o, movedPast2, localRefSkips);
11719
+ return {
11720
+ ...out,
11721
+ ...movedPast2.length ? { movedPast: movedPast2 } : {},
11722
+ ...localRefSkips.length ? { localRefSkips } : {}
11723
+ };
11672
11724
  }
11673
- async function runReleaseCycle(o, movedPast2) {
11725
+ async function runReleaseCycle(o, movedPast2, localRefSkips) {
11674
11726
  const scoped = (msg) => o.scope ? `${o.scope}: ${msg}` : msg;
11675
11727
  const tagPattern = o.repo.release.tagPattern ?? "v*";
11676
11728
  o.emit.enter("release");
@@ -11682,8 +11734,9 @@ async function runReleaseCycle(o, movedPast2) {
11682
11734
  if (t.status !== o.contract.statuses.verified) continue;
11683
11735
  materializeBranchForTicket(o.cwd, o.repo, t, void 0, (problem) => {
11684
11736
  skipped.add(t.id);
11737
+ localRefSkips.push({ ticket: t, problem });
11685
11738
  o.emit.error(`not releasing ${t.issue_id} this cycle: ${problem}`, { ticket: t.issue_id });
11686
- });
11739
+ }, (note) => o.emit.emit(`${t.issue_id}: ${note}`, { ticket: t.issue_id }));
11687
11740
  }
11688
11741
  if (skipped.size) releasable = o.tickets.filter((t) => !skipped.has(t.id));
11689
11742
  }
@@ -11702,6 +11755,10 @@ async function runReleaseCycle(o, movedPast2) {
11702
11755
  `not releasing ${t.issue_id}: local ${branch} at ${moved.tip.slice(0, 8)} is not on ${o.repo.branch.remote}/${branch} (${remoteTip2.slice(0, 8)}); not requeuing it`,
11703
11756
  { ticket: t.issue_id }
11704
11757
  );
11758
+ localRefSkips.push({
11759
+ ticket: t,
11760
+ problem: `local ${branch} at ${moved.tip.slice(0, 8)} is not on ${o.repo.branch.remote}/${branch} (${remoteTip2.slice(0, 8)})`
11761
+ });
11705
11762
  continue;
11706
11763
  }
11707
11764
  movedPast2.push({ ticket: t, verified: moved.verified, head: moved.tip, branch: pushedBranchForTicket(o.repo, t) });
@@ -17401,6 +17458,18 @@ ${changed2.map((c) => ` - ${c}
17401
17458
  remit.warn(`could not note the red test gate on the board: ${e.message}`, { step: "merge" });
17402
17459
  }
17403
17460
  }
17461
+ if (outcome.localRefSkips?.length && !dryRun) {
17462
+ try {
17463
+ const reported = await tracker.events("local_ref_skip");
17464
+ const seat = releaseSeat(c);
17465
+ for (const s of outcome.localRefSkips) {
17466
+ if (localRefSkipReported(s.ticket, s.problem, reported)) continue;
17467
+ await tracker.postEvent(s.ticket.id, localRefSkipComment(cfg.ship.name, s.problem), seat, "local_ref_skip");
17468
+ }
17469
+ } catch (e) {
17470
+ remit.warn(`could not note the skipped release on the board: ${e.message}`, { step: "merge" });
17471
+ }
17472
+ }
17404
17473
  if (outcome.externalClosures?.length) {
17405
17474
  await applyExternalClosures(tracker, outcome.externalClosures, tracker.contract, remit, dryRun, target.dir);
17406
17475
  }
@@ -19835,6 +19904,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19835
19904
  continue;
19836
19905
  }
19837
19906
  if (!s2.upstream) continue;
19907
+ if (w.branch !== r.config.branch.base && s2.upstream === `${r.config.branch.remote}/${r.config.branch.base}`) continue;
19838
19908
  tracked++;
19839
19909
  if (s2.canFastForward) {
19840
19910
  acted++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.73.0",
3
+ "version": "0.73.2",
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",