@tablation/crew 0.47.2 → 0.47.3

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 +53 -10
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -4117,15 +4117,36 @@ function remoteBranchForIssue(cwd, remote, issueKey, templates = { name: "issue-
4117
4117
  }
4118
4118
  return null;
4119
4119
  }
4120
- function materializeRemoteBranch(cwd, remote, branch) {
4121
- if (!remoteBranchExists(cwd, remote, branch)) {
4122
- return gitOk(cwd, ["rev-parse", "-q", "--verify", `refs/heads/${branch}`]) !== null;
4120
+ function levelRemoteBranch(cwd, remote, branch) {
4121
+ const local = gitOk(cwd, ["rev-parse", "-q", "--verify", `refs/heads/${branch}`]);
4122
+ if (!remoteBranchExists(cwd, remote, branch)) return { exists: local !== null };
4123
+ if (local === null) {
4124
+ return { exists: gitOk(cwd, ["branch", "--track", branch, `${remote}/${branch}`]) !== null };
4125
+ }
4126
+ const remoteTip2 = gitOk(cwd, ["rev-parse", "-q", "--verify", `refs/remotes/${remote}/${branch}`]);
4127
+ if (!remoteTip2 || remoteTip2 === local) return { exists: true };
4128
+ if (!isAncestor(cwd, local, remoteTip2)) return { exists: true };
4129
+ const holder = worktrees(cwd).find((w) => w.branch === branch && !w.prunable);
4130
+ if (holder) {
4131
+ if (gitOk(holder.path, ["status", "--porcelain"])) {
4132
+ return {
4133
+ exists: true,
4134
+ problem: `${branch} is behind ${remote}/${branch} and the worktree ${holder.path} that holds it has uncommitted or mid-merge changes`
4135
+ };
4136
+ }
4137
+ try {
4138
+ git(holder.path, ["merge", "--ff-only", "--quiet", `${remote}/${branch}`]);
4139
+ } catch (e) {
4140
+ return { exists: true, problem: `could not fast-forward ${branch} in ${holder.path}: ${e.message}` };
4141
+ }
4142
+ return { exists: true };
4123
4143
  }
4124
- if (gitOk(cwd, ["rev-parse", "-q", "--verify", `refs/heads/${branch}`]) === null) {
4125
- return gitOk(cwd, ["branch", "--track", branch, `${remote}/${branch}`]) !== null;
4144
+ try {
4145
+ git(cwd, ["fetch", "--quiet", remote, `${branch}:${branch}`]);
4146
+ } catch (e) {
4147
+ return { exists: true, problem: `could not move ${branch} to ${remote}/${branch}: ${e.message}` };
4126
4148
  }
4127
- gitOk(cwd, ["fetch", "--quiet", remote, `${branch}:${branch}`]);
4128
- return true;
4149
+ return { exists: true };
4129
4150
  }
4130
4151
  function deleteRemoteBranch(cwd, remote, branch) {
4131
4152
  if (!remoteBranchExists(cwd, remote, branch)) return true;
@@ -6285,10 +6306,11 @@ function locateBranchForTicket(dir, cfg, t, role, fetchedDirs) {
6285
6306
  const found = remoteBranchForIssue(dir, remote, t.issue_id, branchTemplates(cfg, t), branchRenderer(t, role));
6286
6307
  return found ? { branch: found, where: "remote" } : null;
6287
6308
  }
6288
- function materializeBranchForTicket(dir, cfg, t, role) {
6309
+ function materializeBranchForTicket(dir, cfg, t, role, onProblem) {
6289
6310
  const loc = locateBranchForTicket(dir, cfg, t, role);
6290
6311
  if (!loc) return null;
6291
- materializeRemoteBranch(dir, cfg.branch.remote, loc.branch);
6312
+ const r = levelRemoteBranch(dir, cfg.branch.remote, loc.branch);
6313
+ if (r.problem) onProblem?.(r.problem);
6292
6314
  return loc.branch;
6293
6315
  }
6294
6316
 
@@ -6746,6 +6768,11 @@ function planConflictBounce(cwd, ticket, branch, base, paths, events) {
6746
6768
  function refreshBranch(worktree, base) {
6747
6769
  const dirty = gitOk(worktree, ["status", "--porcelain"]);
6748
6770
  if (dirty) return { clean: false, paths: [], why: "the worktree has uncommitted changes" };
6771
+ const upstream = gitOk(worktree, ["rev-parse", "-q", "--verify", "@{upstream}"]);
6772
+ const head = gitOk(worktree, ["rev-parse", "HEAD"]);
6773
+ if (upstream && head && upstream !== head && isAncestor(worktree, head, upstream)) {
6774
+ return { clean: false, paths: [], behind: true, why: "the branch is behind its remote; fast-forward it before merging the base in" };
6775
+ }
6749
6776
  try {
6750
6777
  git(worktree, ["merge", "--no-edit", base]);
6751
6778
  return { clean: true, paths: [] };
@@ -6813,6 +6840,10 @@ async function applyConflictBounce(writer, b, contract, memberId, log, dryRun) {
6813
6840
  log.emit(`merged ${b.base} into ${b.branch} cleanly \u2014 it was stale, not conflicting`, t);
6814
6841
  return { kind: "refreshed" };
6815
6842
  }
6843
+ if (r.behind) {
6844
+ log.warn(`not handing ${b.ticket.issue_id} back: ${r.why}`, t);
6845
+ return { kind: "failed", why: r.why ?? "branch behind its remote" };
6846
+ }
6816
6847
  merged = r.paths.length > 0;
6817
6848
  if (r.why) log.warn(`could not merge ${b.base} into ${b.branch}: ${r.why}`, t);
6818
6849
  if (r.paths.length) b.paths = r.paths;
@@ -9664,7 +9695,19 @@ async function runRelease(o) {
9664
9695
  const tagPattern = o.repo.release.tagPattern ?? "v*";
9665
9696
  o.emit.enter("release");
9666
9697
  const fresh = refreshBase(o);
9667
- const decision = decideRelease(o.cwd, o.tickets, o.contract, {
9698
+ let releasable = o.tickets;
9699
+ if (!o.dryRun) {
9700
+ const skipped = /* @__PURE__ */ new Set();
9701
+ for (const t of o.tickets) {
9702
+ if (t.status !== o.contract.statuses.verified) continue;
9703
+ materializeBranchForTicket(o.cwd, o.repo, t, void 0, (problem) => {
9704
+ skipped.add(t.id);
9705
+ o.emit.error(`not releasing ${t.issue_id} this cycle: ${problem}`, { ticket: t.issue_id });
9706
+ });
9707
+ }
9708
+ if (skipped.size) releasable = o.tickets.filter((t) => !skipped.has(t.id));
9709
+ }
9710
+ const decision = decideRelease(o.cwd, releasable, o.contract, {
9668
9711
  tagPattern,
9669
9712
  base: o.repo.branch.base,
9670
9713
  blockerInfo: o.blockerInfo,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.47.2",
3
+ "version": "0.47.3",
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",