@tablation/crew 0.45.0 → 0.45.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 +61 -24
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -11158,6 +11158,13 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
11158
11158
  }
11159
11159
  function planRemoteBranchCleanup(cwd, remote, base, tickets, contract) {
11160
11160
  if (!remoteConfigured(cwd, remote)) return [];
11161
+ const stems = deployedBranchStems(tickets, contract);
11162
+ if (stems.size === 0) return [];
11163
+ const prefix = `refs/remotes/${remote}/`;
11164
+ const refs = (gitOk(cwd, ["for-each-ref", "--format=%(refname)", prefix]) ?? "").split("\n").filter(Boolean);
11165
+ return refs.map((ref) => ref.slice(prefix.length)).filter((name) => isTicketBranch(name, base, stems));
11166
+ }
11167
+ function deployedBranchStems(tickets, contract) {
11161
11168
  const stems = /* @__PURE__ */ new Set();
11162
11169
  for (const t of tickets) {
11163
11170
  if (t.status !== contract.statuses.deployed) continue;
@@ -11169,17 +11176,40 @@ function planRemoteBranchCleanup(cwd, remote, base, tickets, contract) {
11169
11176
  stems.add(`${p}-${unpadded}`);
11170
11177
  }
11171
11178
  }
11179
+ return stems;
11180
+ }
11181
+ function isTicketBranch(name, base, stems) {
11182
+ if (name === "HEAD" || name === base) return false;
11183
+ const m = /^(.+?-\d+)(?:-.*)?$/.exec(name);
11184
+ return !!m && stems.has(m[1]);
11185
+ }
11186
+ function sweepCandidatesExist(cwd, base) {
11187
+ const linked = worktrees(cwd).slice(1);
11188
+ if (linked.some((w) => /(?:^|-)\d+$/.test(basename8(w.path)))) return true;
11189
+ return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
11190
+ }
11191
+ function localBranches(cwd) {
11192
+ return (gitOk(cwd, ["branch", "--list", "--format=%(refname:short)"]) ?? "").split("\n").filter(Boolean);
11193
+ }
11194
+ function planLocalBranchCleanup(cwd, base, tickets, contract) {
11195
+ const stems = deployedBranchStems(tickets, contract);
11172
11196
  if (stems.size === 0) return [];
11173
- const prefix = `refs/remotes/${remote}/`;
11174
- const refs = (gitOk(cwd, ["for-each-ref", "--format=%(refname)", prefix]) ?? "").split("\n").filter(Boolean);
11175
- const out = [];
11176
- for (const ref of refs) {
11177
- const name = ref.slice(prefix.length);
11178
- if (name === "HEAD" || name === base) continue;
11179
- const m = /^(.+?-\d+)(?:-.*)?$/.exec(name);
11180
- if (m && stems.has(m[1])) out.push(name);
11197
+ const checkedOut = new Set(worktrees(cwd).map((w) => w.branch).filter((b) => !!b));
11198
+ return localBranches(cwd).filter((b) => !checkedOut.has(b) && isTicketBranch(b, base, stems));
11199
+ }
11200
+ function applyLocalBranchCleanup(cwd, branches2, dryRun, log) {
11201
+ let deleted = 0;
11202
+ for (const b of branches2) {
11203
+ if (dryRun) {
11204
+ log.emit(`would delete local branch ${b} \u2014 its ticket is deployed`, { step: "worktree" });
11205
+ continue;
11206
+ }
11207
+ if (gitOk(cwd, ["branch", "-D", b]) !== null) {
11208
+ deleted++;
11209
+ log.emit(`deleted local branch ${b} \u2014 its ticket is deployed`, { step: "worktree" });
11210
+ } else log.warn(`could not delete local branch ${b} \u2014 leaving it for the next sweep`, { step: "worktree" });
11181
11211
  }
11182
- return out;
11212
+ return deleted;
11183
11213
  }
11184
11214
  function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
11185
11215
  let deleted = 0;
@@ -12984,6 +13014,26 @@ ${changed2.map((c) => ` - ${c}
12984
13014
  }
12985
13015
  return `${who2}; ${lock2}`;
12986
13016
  }
13017
+ async function sweepClosedTickets(c, target, branch, tracker, dryRun2, remit, idle = false) {
13018
+ try {
13019
+ if (idle && !sweepCandidatesExist(target.dir, branch.base)) return;
13020
+ const terminal = await tracker.terminalTickets();
13021
+ const verifiedNow = (await tracker.openTickets()).filter((t) => t.status === tracker.contract.statuses.verified);
13022
+ const scoped = ticketsByRepo(c, terminal).byRepo.get(target.name) ?? [];
13023
+ const actions = planWorktreeSweep(target, [...scoped, ...ticketsByRepo(c, verifiedNow).byRepo.get(target.name) ?? []], tracker.contract);
13024
+ if (actions.length) {
13025
+ remit.enter("worktree");
13026
+ const r = await applyWorktreeSweep(target.dir, actions, dryRun2, remit, branch.remote);
13027
+ remit.emit(`swept ${r.removed} worktree(s), kept ${r.keptBranches} branch(es)`, { step: "worktree" });
13028
+ }
13029
+ const orphaned = planLocalBranchCleanup(target.dir, branch.base, scoped, tracker.contract);
13030
+ if (orphaned.length) applyLocalBranchCleanup(target.dir, orphaned, dryRun2, remit);
13031
+ const stale = planRemoteBranchCleanup(target.dir, branch.remote, branch.base, scoped, tracker.contract);
13032
+ if (stale.length) applyRemoteBranchCleanup(target.dir, branch.remote, stale, dryRun2, remit);
13033
+ } catch (e) {
13034
+ remit.warn(`worktree sweep failed: ${e.message}`, { step: "worktree" });
13035
+ }
13036
+ }
12987
13037
  const precheckTickets = /* @__PURE__ */ new Map();
12988
13038
  async function releasePhase(c, target, opts = {}) {
12989
13039
  const scope = releaseLockScope(c.route, target.name);
@@ -13053,6 +13103,7 @@ ${changed2.map((c) => ` - ${c}
13053
13103
  unstamped
13054
13104
  })) {
13055
13105
  remit.emit(`${target.name}: nothing verified \u2014 not locking`, { step: "release" });
13106
+ await sweepClosedTickets(c, target, repo.branch, tracker, dryRun, remit, true);
13056
13107
  return { scope, tests: "skipped", outcome: "skipped", detail: "nothing verified \u2014 not locking" };
13057
13108
  }
13058
13109
  }
@@ -13257,21 +13308,7 @@ ${changed2.map((c) => ` - ${c}
13257
13308
  }
13258
13309
  }
13259
13310
  }
13260
- try {
13261
- const terminal = await tracker.terminalTickets();
13262
- const verifiedNow = (await tracker.openTickets()).filter((t) => t.status === tracker.contract.statuses.verified);
13263
- const scoped = ticketsByRepo(c, terminal).byRepo.get(target.name) ?? [];
13264
- const actions = planWorktreeSweep(target, [...scoped, ...ticketsByRepo(c, verifiedNow).byRepo.get(target.name) ?? []], tracker.contract);
13265
- if (actions.length) {
13266
- remit.enter("worktree");
13267
- const r = await applyWorktreeSweep(target.dir, actions, dryRun, remit, repo.branch.remote);
13268
- remit.emit(`swept ${r.removed} worktree(s), kept ${r.keptBranches} branch(es)`, { step: "worktree" });
13269
- }
13270
- const stale = planRemoteBranchCleanup(target.dir, repo.branch.remote, repo.branch.base, scoped, tracker.contract);
13271
- if (stale.length) applyRemoteBranchCleanup(target.dir, repo.branch.remote, stale, dryRun, remit);
13272
- } catch (e) {
13273
- remit.warn(`worktree sweep failed: ${e.message}`, { step: "worktree" });
13274
- }
13311
+ await sweepClosedTickets(c, target, repo.branch, tracker, dryRun, remit);
13275
13312
  return summary;
13276
13313
  } catch (e) {
13277
13314
  remit.warn(`release phase failed: ${e.message}`, { step: "release" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.45.0",
3
+ "version": "0.45.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",