@tablation/crew 0.45.2 → 0.45.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.
package/dist/cli.js CHANGED
@@ -4804,7 +4804,6 @@ function referenceKeys(t) {
4804
4804
  }
4805
4805
  var RESOLVED_STATUSES = new Set(DEFAULT_CONTRACT.statuses.resolved);
4806
4806
  var encodeFilters = (f) => JSON.stringify(f);
4807
- var encodeSort = (s) => JSON.stringify(s);
4808
4807
  var Tracker = class {
4809
4808
  // NB: explicit fields, not TypeScript parameter properties — node's
4810
4809
  // --experimental-strip-types cannot transform those, and the test runner
@@ -4864,37 +4863,41 @@ var Tracker = class {
4864
4863
  });
4865
4864
  }
4866
4865
  /**
4867
- * Tickets at a terminal, no-longer-open status — the worktree sweep's
4868
- * input (ISSUE-346).
4866
+ * The tickets behind a set of ticket numbers — the cleanup sweeps' input
4867
+ * (CREW-1593).
4869
4868
  *
4870
- * The exact complement of `openTickets()`: `closed_deployed`,
4871
- * `closed_wont_fix`, `closed_duplicate`. `verified` is excluded even
4872
- * though the contract counts it as resolved for a *blocker*'s purposes —
4873
- * it is still pre-release, and its worktree is exactly what the release
4874
- * phase is about to merge.
4869
+ * The sweeps used to fetch "every closed ticket, newest `updated_at` first,
4870
+ * capped at 500" and hope the tickets they cared about were in the page. They
4871
+ * were not once the board held more than 500 closed tickets, and a bulk
4872
+ * write gave 1457 of them one identical `updated_at`, so which ones made the
4873
+ * page was arbitrary and a deployed ticket's worktree/branch was silently
4874
+ * never swept. The caller now names the numbers it actually has a worktree
4875
+ * or remote branch for, so the request is bounded by those and not by how
4876
+ * many tickets are closed.
4875
4877
  *
4876
- * Sorted most-recently-updated first: a board can hold far more than
4877
- * `limit` closed tickets (this one already has), and an unordered fetch
4878
- * has no guarantee which ones a truncated page contains. Ordering by
4879
- * `updatedAt` descending means a truncated page still holds the tickets
4880
- * most likely to need their worktree swept — the ones that closed most
4881
- * recently — rather than an arbitrary, possibly stale slice (ISSUE-403).
4878
+ * Matched on `issue_id`, in the raw, unpadded and 3-digit-padded spellings a
4879
+ * number has been stored under. `onTruncated` is told when a page came back
4880
+ * full, since anything past it would be missing without a trace.
4882
4881
  */
4883
- async terminalTickets() {
4882
+ async ticketsByNumbers(nums, onTruncated) {
4884
4883
  const c = this.contract;
4885
- const statuses = closedStatuses(c);
4886
- if (!statuses.length) return [];
4887
- const filters = [
4888
- { columnName: c.columns.status, operator: "IN", value: statuses }
4889
- ];
4890
- const areaId = this.route.resolved?.areaId;
4891
- if (areaId) filters.push({ columnName: c.columns.slice, operator: "EQ", value: areaId });
4892
- const params = {
4893
- filters: encodeFilters(filters),
4894
- limit: 500,
4895
- sort: encodeSort([{ columnName: c.columns.updatedAt, direction: "desc" }])
4896
- };
4897
- return this.client.records.list(this.models.issues, params);
4884
+ const keys = /* @__PURE__ */ new Set();
4885
+ for (const raw of nums) {
4886
+ const unpadded = String(Number.parseInt(raw, 10));
4887
+ for (const n of [raw, unpadded, unpadded.padStart(3, "0")]) keys.add(`ISSUE-${n}`);
4888
+ }
4889
+ const all = [...keys];
4890
+ const out = [];
4891
+ const limit = 500;
4892
+ for (let i = 0; i < all.length; i += 100) {
4893
+ const page = await this.client.records.list(this.models.issues, {
4894
+ filters: encodeFilters([{ columnName: c.columns.key, operator: "IN", value: all.slice(i, i + 100) }]),
4895
+ limit
4896
+ });
4897
+ if (page.length >= limit) onTruncated?.(limit);
4898
+ out.push(...page);
4899
+ }
4900
+ return out;
4898
4901
  }
4899
4902
  /**
4900
4903
  * Tickets by id, for blockers that have already closed and so are absent
@@ -11098,6 +11101,14 @@ function planWorktreeSweep(target, tickets, contract) {
11098
11101
  }
11099
11102
  return actions;
11100
11103
  }
11104
+ function worktreeTicketNumbers(target) {
11105
+ const nums = /* @__PURE__ */ new Set();
11106
+ for (const w of worktrees(target.dir)) {
11107
+ const m = /(?:^|-)(\d+)$/.exec(basename8(w.path));
11108
+ if (m) nums.add(m[1]);
11109
+ }
11110
+ return [...nums];
11111
+ }
11101
11112
  async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin") {
11102
11113
  let removed = 0;
11103
11114
  let keptBranches = 0;
@@ -11156,6 +11167,19 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
11156
11167
  }
11157
11168
  return { removed, keptBranches };
11158
11169
  }
11170
+ function remoteBranchTicketNumbers(cwd, remote, base) {
11171
+ if (!remoteConfigured(cwd, remote)) return [];
11172
+ const prefix = `refs/remotes/${remote}/`;
11173
+ const refs = (gitOk(cwd, ["for-each-ref", "--format=%(refname)", prefix]) ?? "").split("\n").filter(Boolean);
11174
+ const nums = /* @__PURE__ */ new Set();
11175
+ for (const ref of refs) {
11176
+ const name = ref.slice(prefix.length);
11177
+ if (name === "HEAD" || name === base) continue;
11178
+ const m = /^.+?-(\d+)(?:-.*)?$/.exec(name);
11179
+ if (m) nums.add(m[1]);
11180
+ }
11181
+ return [...nums];
11182
+ }
11159
11183
  function planRemoteBranchCleanup(cwd, remote, base, tickets, contract) {
11160
11184
  if (!remoteConfigured(cwd, remote)) return [];
11161
11185
  const stems = deployedBranchStems(tickets, contract);
@@ -11188,6 +11212,14 @@ function sweepCandidatesExist(cwd, base) {
11188
11212
  if (linked.some((w) => /(?:^|-)\d+$/.test(basename8(w.path)))) return true;
11189
11213
  return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
11190
11214
  }
11215
+ function localBranchTicketNumbers(cwd, base) {
11216
+ const nums = /* @__PURE__ */ new Set();
11217
+ for (const b of localBranches(cwd)) {
11218
+ const m = b === base ? null : /^.+?-(\d+)(?:-.*)?$/.exec(b);
11219
+ if (m) nums.add(m[1]);
11220
+ }
11221
+ return [...nums];
11222
+ }
11191
11223
  function localBranches(cwd) {
11192
11224
  return (gitOk(cwd, ["branch", "--list", "--format=%(refname:short)"]) ?? "").split("\n").filter(Boolean);
11193
11225
  }
@@ -13017,10 +13049,14 @@ ${changed2.map((c) => ` - ${c}
13017
13049
  async function sweepClosedTickets(c, target, branch, tracker, dryRun2, remit, idle = false) {
13018
13050
  try {
13019
13051
  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);
13052
+ const truncated = (limit) => remit.warn(`ticket lookup for the sweep hit its ${limit}-row limit; some tickets may be missed`, { step: "worktree" });
13053
+ const nums = /* @__PURE__ */ new Set([
13054
+ ...worktreeTicketNumbers(target),
13055
+ ...localBranchTicketNumbers(target.dir, branch.base),
13056
+ ...remoteBranchTicketNumbers(target.dir, branch.remote, branch.base)
13057
+ ]);
13058
+ const scoped = ticketsByRepo(c, await tracker.ticketsByNumbers([...nums], truncated)).byRepo.get(target.name) ?? [];
13059
+ const actions = planWorktreeSweep(target, scoped, tracker.contract);
13024
13060
  if (actions.length) {
13025
13061
  remit.enter("worktree");
13026
13062
  const r = await applyWorktreeSweep(target.dir, actions, dryRun2, remit, branch.remote);
@@ -14709,15 +14745,15 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14709
14745
  }
14710
14746
  if (!dryRun && orphans.length) await killGently(orphans.map((o) => o.pid));
14711
14747
  const tracker = new Tracker(route, cfg.ship);
14712
- const terminal = await tracker.terminalTickets();
14713
- const { byRepo } = ticketsByRepo(route, terminal);
14714
- const verifiedByRepo = ticketsByRepo(
14715
- route,
14716
- (await tracker.openTickets()).filter((t) => t.status === tracker.contract.statuses.verified)
14717
- ).byRepo;
14748
+ const truncated = (limit) => emit.warn(`ticket lookup for the sweep hit its ${limit}-row limit; some tickets may be missed`, { step: "worktree" });
14718
14749
  for (const r of await resolvedRepos(route)) {
14719
- const actions = planWorktreeSweep(r, [...byRepo.get(r.name) ?? [], ...verifiedByRepo.get(r.name) ?? []], tracker.contract);
14720
- const stale = planRemoteBranchCleanup(r.dir, r.config.branch.remote, r.config.branch.base, byRepo.get(r.name) ?? [], tracker.contract);
14750
+ const wtTickets = ticketsByRepo(route, await tracker.ticketsByNumbers(worktreeTicketNumbers(r), truncated)).byRepo.get(r.name) ?? [];
14751
+ const brTickets = ticketsByRepo(
14752
+ route,
14753
+ await tracker.ticketsByNumbers(remoteBranchTicketNumbers(r.dir, r.config.branch.remote, r.config.branch.base), truncated)
14754
+ ).byRepo.get(r.name) ?? [];
14755
+ const actions = planWorktreeSweep(r, wtTickets, tracker.contract);
14756
+ const stale = planRemoteBranchCleanup(r.dir, r.config.branch.remote, r.config.branch.base, brTickets, tracker.contract);
14721
14757
  if (stale.length) {
14722
14758
  anything = true;
14723
14759
  applyRemoteBranchCleanup(r.dir, r.config.branch.remote, stale, dryRun, emit);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.45.2",
3
+ "version": "0.45.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",
@@ -45,6 +45,13 @@ request — a default client user agent is rejected before it reaches the API,
45
45
  and the failure looks like a network problem. Do not guess column names; the
46
46
  Environment section lists them for this workspace.
47
47
 
48
+ A write to a record can be refused by a rule on the table. The response is a
49
+ 422 whose `message` says what is missing and whose `fieldErrors` names the
50
+ field. Do not retry the same write. If the missing value is yours to supply
51
+ (a resolution note when closing, a duplicate target), supply it and write
52
+ once more; otherwise leave the ticket where it is and say so in your
53
+ hand-off comment.
54
+
48
55
  Your identity is the Crew row named for your seat in the `## Your crew`
49
56
  roster at the top of this prompt. Use its id as the assignee and as the
50
57
  comment author. Every agent and every person who touches this tracker has
@@ -46,6 +46,13 @@ request — a default client user agent is rejected before it reaches the API,
46
46
  and the failure looks like a network problem. Do not guess column names; the
47
47
  Environment section lists them for this workspace.
48
48
 
49
+ A write to a record can be refused by a rule on the table. The response is a
50
+ 422 whose `message` says what is missing and whose `fieldErrors` names the
51
+ field. Do not retry the same write. If the missing value is yours to supply
52
+ (a resolution note when closing, a duplicate target), supply it and write
53
+ once more; otherwise leave the ticket where it is and say so in your
54
+ hand-off comment.
55
+
49
56
  Your identity is the Crew row named for your seat in the `## Your crew`
50
57
  roster at the top of this prompt. Use its id as the assignee and as the
51
58
  comment author. Every agent and every person who touches this tracker has