@tablation/crew 0.19.5 → 0.20.1

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
@@ -1792,6 +1792,7 @@ var DEFAULT_CONTRACT = {
1792
1792
  blockedBy: "blocked_by",
1793
1793
  updatedAt: "updated_at",
1794
1794
  needsDesign: "needs_design",
1795
+ workMode: "work_mode",
1795
1796
  reportType: "report_type",
1796
1797
  epic: "epic_id",
1797
1798
  author: "filed_by_id",
@@ -1840,6 +1841,7 @@ var DEFAULT_CONTRACT = {
1840
1841
  severityOrder: ["s1", "s2", "s3", "s4"],
1841
1842
  unknownPriorityRank: 2,
1842
1843
  coordinatingValue: "coordinating",
1844
+ manualWorkMode: "manual",
1843
1845
  repoExemptReportTypes: ["coordinating", "question", "investigation"]
1844
1846
  };
1845
1847
  function closedStatuses(c) {
@@ -1860,6 +1862,7 @@ function resolveContract(override) {
1860
1862
  severityOrder: override.severityOrder ?? DEFAULT_CONTRACT.severityOrder,
1861
1863
  unknownPriorityRank: override.unknownPriorityRank ?? DEFAULT_CONTRACT.unknownPriorityRank,
1862
1864
  coordinatingValue: override.coordinatingValue ?? DEFAULT_CONTRACT.coordinatingValue,
1865
+ manualWorkMode: override.manualWorkMode ?? DEFAULT_CONTRACT.manualWorkMode,
1863
1866
  // A workspace overriding `coordinatingValue` alone (renaming the rollup
1864
1867
  // marker) still gets that renamed value exempted here by default,
1865
1868
  // without also having to repeat it — only an explicit
@@ -3239,6 +3242,17 @@ var SHELL_WORDS = /* @__PURE__ */ new Set([
3239
3242
  "ok"
3240
3243
  ]);
3241
3244
 
3245
+ // src/work-mode.ts
3246
+ function manualProjectIds(rows, contract) {
3247
+ const out = /* @__PURE__ */ new Set();
3248
+ for (const r of rows) if (r[contract.columns.workMode] === contract.manualWorkMode) out.add(r.id);
3249
+ return out;
3250
+ }
3251
+ function withinWorkMode(tickets, me, manual) {
3252
+ if (!manual || manual.size === 0) return tickets;
3253
+ return tickets.filter((t) => !t.project_id || !manual.has(t.project_id) || !!me && t.assignee_id === me);
3254
+ }
3255
+
3242
3256
  // src/board-lock.ts
3243
3257
  import { StaleWriteError } from "@tablation/client";
3244
3258
  function releaseLockScope(route, repo) {
@@ -3460,6 +3474,22 @@ var Tracker = class {
3460
3474
  return void 0;
3461
3475
  }
3462
3476
  }
3477
+ /**
3478
+ * Ids of the Projects (area) rows in `manual` work mode (CREW-1445). One
3479
+ * extra read per cycle. A workspace with no Projects table, no `work_mode`
3480
+ * column, a null value or an unreadable table yields the empty set: every
3481
+ * project stays automatic, exactly as before the column existed.
3482
+ */
3483
+ async manualProjectIds() {
3484
+ const model = this.route.resolved?.areaModelId;
3485
+ if (!model) return /* @__PURE__ */ new Set();
3486
+ try {
3487
+ const rows = await this.client.records.list(model, { limit: 200 });
3488
+ return manualProjectIds(rows, this.contract);
3489
+ } catch {
3490
+ return /* @__PURE__ */ new Set();
3491
+ }
3492
+ }
3463
3493
  /** Every `Projects` (area) row — what `crew repos add` offers when a new Repos row needs one. */
3464
3494
  async projectRows() {
3465
3495
  const model = this.route.resolved?.areaModelId;
@@ -4154,7 +4184,7 @@ function notHeldByOtherShip(slice, i) {
4154
4184
  function buildingRoleHasWork(role, i) {
4155
4185
  const me = i.seats[role];
4156
4186
  if (!me) return { hasWork: false, reason: "this ship does not crew that role" };
4157
- const mine = notInStoppedRepo(notHeldByOtherShip(sliceFor(i.tickets, role), i), i);
4187
+ const mine = withinWorkMode(notInStoppedRepo(notHeldByOtherShip(sliceFor(i.tickets, role), i), i), me, i.manualProjects);
4158
4188
  const startable = mine.filter(
4159
4189
  (t) => (t.status === "accepted" || t.status === "blocked") && !i.blocked.has(t.id) && !isHeld(t, i.holds)
4160
4190
  );
@@ -4204,7 +4234,7 @@ function roleCandidates(role, i) {
4204
4234
  if (role === "pair") return [];
4205
4235
  const me = i.seats[role];
4206
4236
  if (!me) return [];
4207
- return notInStoppedRepo(notHeldByOtherShip(sliceFor(i.tickets, role), i), i).filter(
4237
+ return withinWorkMode(notInStoppedRepo(notHeldByOtherShip(sliceFor(i.tickets, role), i), i), me, i.manualProjects).filter(
4208
4238
  (t) => ((t.status === "accepted" || t.status === "blocked") && !i.blocked.has(t.id) || t.status === "in_progress" && (t.assignee_id === me || !t.assignee_id)) && !isHeld(t, i.holds)
4209
4239
  );
4210
4240
  }
@@ -4499,6 +4529,7 @@ ${sorted(held).map((t) => `| ${displayKey2(t)} | ${stopOf(t)} |`).join("\n")}
4499
4529
  "\nAlready filtered to your lane, and already ordered by the Step 2 rule.\nTicket bodies are deliberately omitted: fetch the full record of only the\nticket you actually pick up. **Do not re-fetch the whole tracker.** This\ndigest comes from the same API call the poll just made, moments ago.\n",
4500
4530
  '\n"new since last poll" counts comments from someone other than you since\nthe poll watermark \u2014 the same signal that woke this run.\n',
4501
4531
  "\n**`repo` is the checkout a ticket's work happens in** \u2014 an area spans\nseveral repositories, so cut the worktree beside THAT directory, not beside\nwhichever one you started in. A ticket marked **NO CHECKOUT** is not yours:\nthis ship has no clone of its repository, and another ship may serve it.\n\n**Use the `branch` and `worktree` columns verbatim.** They are that\nrepository's own naming convention, rendered for THIS ticket \u2014 cut the\nworktree at `<repo>/../<worktree>`, on branch `<branch>`. Do not derive\neither yourself, and prefer these over the Environment section's own worked\nexample if the two ever disagree \u2014 that example is illustrative only,\nrendered before any ticket is picked.\n",
4532
+ ...i.manualProjects && i.manualProjects.size > 0 ? ["\n**Manual projects.** Some projects on this board are in manual work mode: their tickets appear below only when a person has assigned them to YOU. An unassigned `accepted` ticket in such a project is not yours to claim and is deliberately absent \u2014 do not go looking for it.\n"] : [],
4502
4533
  "\n### Step 1 \u2014 open tickets that may be yours to act on\n",
4503
4534
  "\n`fixed` tickets are deliberately absent: they belong to the QA lane.\n",
4504
4535
  table(step1, i, header, row),
@@ -4686,12 +4717,13 @@ async function decideCycle(o) {
4686
4717
  const { route, ship, state, emit } = o;
4687
4718
  const tracker = new Tracker(route, ship);
4688
4719
  emit.enter("poll");
4689
- const [tickets, comments, crewRows, epicRows, ships] = await Promise.all([
4720
+ const [tickets, comments, crewRows, epicRows, ships, manualProjects] = await Promise.all([
4690
4721
  tracker.openTickets(),
4691
4722
  tracker.comments(200),
4692
4723
  tracker.crewRows(),
4693
4724
  tracker.epicRows(),
4694
- tracker.shipRows()
4725
+ tracker.shipRows(),
4726
+ tracker.manualProjectIds()
4695
4727
  ]);
4696
4728
  emit.emit(`${tickets.length} open ticket(s), ${comments.length} comment(s)`, {
4697
4729
  data: { tickets: tickets.length, comments: comments.length }
@@ -4799,6 +4831,7 @@ async function decideCycle(o) {
4799
4831
  await checkRepoDisks({ state, emit, route, ship });
4800
4832
  const selectionInput = {
4801
4833
  repoStop: repoStopFor(openShipAttention(state), route),
4834
+ manualProjects,
4802
4835
  tickets,
4803
4836
  comments,
4804
4837
  watermark,
@@ -4893,7 +4926,8 @@ function writeDigest(o, d, role, stateDir) {
4893
4926
  }
4894
4927
  return existingBranchForTicket(dir, repoFor(dir), t, role);
4895
4928
  },
4896
- tickets: role === "triage" ? d.tickets.filter((t) => t.assignee_id === o.route.resolved?.seats.triage) : sliceFor(d.tickets, role),
4929
+ tickets: role === "triage" ? d.tickets.filter((t) => t.assignee_id === o.route.resolved?.seats.triage) : role === "dev" || role === "design" ? withinWorkMode(sliceFor(d.tickets, role), me, d.selectionInput.manualProjects) : sliceFor(d.tickets, role),
4930
+ manualProjects: d.selectionInput.manualProjects,
4897
4931
  comments: d.comments,
4898
4932
  me,
4899
4933
  roster: d.roster,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.19.5",
3
+ "version": "0.20.1",
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",
@@ -305,6 +305,11 @@ Never start work on a ticket without first setting `assignee_id` to your
305
305
  own id (Step 3.3 already does this) — that claim is itself part of what
306
306
  keeps a concurrent interactive session from re-entering the same ticket.
307
307
 
308
+ A project in **manual work mode** (its Projects row says so) is never consumed
309
+ automatically: the digest lists its tickets only when a person has assigned them to
310
+ your seat, and status alone never makes one yours. The digest says so when it
311
+ applies; do not go looking for the missing tickets. QA is unaffected.
312
+
308
313
  Never touch a ticket still at `new` — only `accepted` tickets are yours to
309
314
  pick up; triage (a separate process) is what promotes `new` → `accepted`/
310
315
  `needs_info`.