@tablation/crew 0.49.4 → 0.49.5

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
@@ -6112,6 +6112,7 @@ function operatorTodo(tickets, contract, operatorId) {
6112
6112
  }
6113
6113
 
6114
6114
  // src/digest.ts
6115
+ var DIRECTION_EVENT_TYPES = ["conflict_handed_back", "requeued", "bounced", "conflict_escalated"];
6115
6116
  function displayKey2(t) {
6116
6117
  return t.issue_tag ?? t.issue_id ?? "?";
6117
6118
  }
@@ -6146,10 +6147,24 @@ function lastComment(i, ticketId) {
6146
6147
  if (!last) return "\u2014";
6147
6148
  return `${last.created_at.slice(0, 16)}Z ${author(last, i)}`;
6148
6149
  }
6150
+ var eventAt = (e) => e.occurred_at ?? e.created_at ?? "";
6151
+ function pendingDirection(i, ticketId) {
6152
+ const mine = (i.events ?? []).filter((e) => e.record_id === ticketId);
6153
+ const pushed = mine.filter((e) => e.event_type === "pushed").map(eventAt).sort().pop() ?? "";
6154
+ return mine.filter((e) => DIRECTION_EVENT_TYPES.includes(e.event_type) && eventAt(e) > pushed).sort((a, b) => eventAt(a) < eventAt(b) ? 1 : -1)[0];
6155
+ }
6156
+ function directionCell(i, ticketId) {
6157
+ const d = pendingDirection(i, ticketId);
6158
+ if (!d) return "\u2014";
6159
+ const msg = (d.message ?? "").replace(/\s+/g, " ").trim().slice(0, 160);
6160
+ return `${d.event_type} ${eventAt(d).slice(0, 16)}Z${msg ? ` \u2014 ${msg}` : ""}`.replace(/\|/g, "/");
6161
+ }
6149
6162
  function newFromOthers(i, ticketId) {
6150
- return i.comments.filter(
6163
+ const comments = i.comments.filter(
6151
6164
  (c) => c.ticket_id === ticketId && c.team_member_id !== i.me && c.created_at > i.watermark
6152
6165
  ).length;
6166
+ const d = pendingDirection(i, ticketId);
6167
+ return comments + (d && eventAt(d) > i.watermark ? 1 : 0);
6153
6168
  }
6154
6169
  function who(t, i) {
6155
6170
  return whoAssigned(t, i) + heldBySuffix(t, i);
@@ -6188,14 +6203,14 @@ ${sorted(ts).map(row).join("\n")}
6188
6203
  `;
6189
6204
  }
6190
6205
  function buildingDigest(i) {
6191
- const header = "| ticket | repo | branch | worktree | mode | status | assignee | filed by | sev | pri | eff | updated | last comment | new since last poll |\n|---|---|---|---|---|---|---|---|---|---|---|---|---|---|";
6206
+ const header = "| ticket | repo | branch | worktree | mode | status | assignee | filed by | sev | pri | eff | updated | last comment | runner direction | new since last poll |\n|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|";
6192
6207
  const row = (t) => {
6193
6208
  const n = newFromOthers(i, t.id);
6194
6209
  const branch = i.branchFor ? i.branchFor(t) : "";
6195
6210
  const worktree = i.worktreeFor ? i.worktreeFor(t) : "";
6196
6211
  const dir = i.dirFor ? i.dirFor(t) : null;
6197
6212
  const repo = i.dirFor ? dir ?? "**NO CHECKOUT**" : "";
6198
- return `| ${displayKey2(t)} | ${repo} | ${branch} | ${worktree} | ${modeCell(t, i)} | ${t.status} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | ${effCell(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
6213
+ return `| ${displayKey2(t)} | ${repo} | ${branch} | ${worktree} | ${modeCell(t, i)} | ${t.status} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | ${effCell(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${directionCell(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
6199
6214
  };
6200
6215
  const blockedRow = (t) => `| ${displayKey2(t)} | ${t.status} | ${who(t, i)} | p${effectivePriority(t)} | ${blockers(t, i)} |`;
6201
6216
  const blockedTable = (ts) => ts.length === 0 ? "\n_None._\n" : `
@@ -6226,6 +6241,7 @@ ${sorted(held).map((t) => `| ${displayKey2(t)} | ${stopOf(t)} |`).join("\n")}
6226
6241
  "## Current queue \u2014 built for you by the poll\n",
6227
6242
  "\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",
6228
6243
  '\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',
6244
+ '\n"runner direction" is the newest instruction the crew itself posted to the\nevent log (not Comments) that no push has answered yet: `conflict_handed_back`\n(merge the base into the branch and resolve), `requeued`, `bounced`,\n`conflict_escalated`. It is direction, same as a comment from a hold: act on it.\n',
6229
6245
  "\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",
6230
6246
  ...i.workModes && [...i.workModes.modes.values()].some((m) => m !== "automatic") ? ["\n**Manual / hybrid projects.** Some projects on this board are not fully automatic. In a `manual` project no lane touches any ticket, and a ticket with no project is treated the same way, so none appear below. In a `hybrid` project a ticket appears below only when it is assigned to an agent. An `accepted` ticket that is absent for either reason is not yours to claim \u2014 do not go looking for it.\n"] : [],
6231
6247
  "\n### Step 1 \u2014 open tickets that may be yours to act on\n",
@@ -6449,6 +6465,7 @@ async function decideCycle(o) {
6449
6465
  tracker.projectWorkModes(),
6450
6466
  tracker.events("stalled")
6451
6467
  ]);
6468
+ const directionEvents = (await Promise.all([...DIRECTION_EVENT_TYPES, "pushed"].map((t) => tracker.events(t)))).flat();
6452
6469
  emit.emit(`${tickets.length} open ticket(s), ${comments.length} comment(s)`, {
6453
6470
  data: { tickets: tickets.length, comments: comments.length }
6454
6471
  });
@@ -6594,6 +6611,7 @@ async function decideCycle(o) {
6594
6611
  selection,
6595
6612
  actionable,
6596
6613
  watermark,
6614
+ directionEvents,
6597
6615
  selectionInput,
6598
6616
  attention: attentionCurrent
6599
6617
  };
@@ -6673,6 +6691,7 @@ function writeDigest(o, d, role, stateDir) {
6673
6691
  ships: d.ships,
6674
6692
  myShipId: d.ships.find((s) => (s.name ?? "").trim() === o.ship.name.trim())?.id ?? null,
6675
6693
  watermark: d.watermark,
6694
+ events: d.directionEvents,
6676
6695
  blocked: d.blocked,
6677
6696
  blockerInfo: d.info
6678
6697
  };
@@ -6777,7 +6796,14 @@ function worktreeForBranch(cwd, branch) {
6777
6796
  }
6778
6797
  return null;
6779
6798
  }
6780
- var priorBounces = (ticket, events) => events.filter((e) => e.record_id === ticket.id && e.event_type === "conflict_handed_back").length;
6799
+ var stampOf = (e) => e.occurred_at ?? e.created_at ?? "";
6800
+ var priorBounces = (ticket, events) => {
6801
+ const mine = events.filter((e) => e.record_id === ticket.id);
6802
+ const pushes = mine.filter((e) => e.event_type === "pushed").map(stampOf);
6803
+ return mine.filter(
6804
+ (e) => e.event_type === "conflict_handed_back" && pushes.some((p) => p > stampOf(e))
6805
+ ).length;
6806
+ };
6781
6807
  function planConflictBounce(cwd, ticket, branch, base, paths, events) {
6782
6808
  return {
6783
6809
  ticket,
@@ -13822,7 +13848,7 @@ ${changed2.map((c) => ` - ${c}
13822
13848
  }
13823
13849
  }
13824
13850
  if (outcome.conflicts?.length || outcome.unbuildable?.length) {
13825
- const handedBack = await tracker.events("conflict_handed_back");
13851
+ const handedBack = [...await tracker.events("conflict_handed_back"), ...await tracker.events("pushed")];
13826
13852
  const flagged = await tracker.events("stranded_flagged");
13827
13853
  const seat = releaseSeat(c);
13828
13854
  for (const f of outcome.conflicts ?? []) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.49.4",
3
+ "version": "0.49.5",
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",
@@ -141,6 +141,12 @@ Then:
141
141
  the ticket again because the status was left alone.
142
142
  - A hold gave new direction on an `in_progress` ticket → adjust the branch
143
143
  and comment on what changed.
144
+ - A ticket whose `runner direction` cell is not `—` has been handed back by
145
+ the crew itself (a conflict with the base, a requeue, a bounce). That is
146
+ direction exactly as a comment from a hold is, and "no new comment" does not
147
+ cancel it: for `conflict_handed_back`, merge `<remote>/<base>` into the
148
+ branch, resolve, run the suite, push as a fast-forward and set `fixed`
149
+ (Step 4.4). Never answer it with "nothing to build".
144
150
  - An `in_progress` ticket with no assignee and no new comment → still "up
145
151
  for grabs, more work to do": a hold clearing the assignee *is* the signal.
146
152
  - **An `in_progress` ticket assigned to you with no new comment is your own
@@ -397,8 +403,10 @@ worktree, runs the test gate and deploys. Three consequences:
397
403
  lines from Step 3.5 are the only input the release phase has; nobody
398
404
  reads the diff later to fill them in.
399
405
  3. **A branch that no longer squash-merges cleanly comes back to you** at
400
- `in_progress`, reassigned to your lane, with a comment naming the
401
- conflict. Fix it in the same worktree and take it to `fixed` again. "Never
406
+ `in_progress`, reassigned to your lane, with the hand-back
407
+ posted to the event log, which the digest's `runner direction` column
408
+ shows (a ticket there reads as new direction even with no new comment).
409
+ Fix it in the same worktree and take it to `fixed` again. "Never
402
410
  merge" above means never merge INTO the base; merging the base INTO your
403
411
  own ticket branch is the expected fix (rule 4), and it is not "by hand to
404
412
  help".
@@ -142,6 +142,12 @@ Then:
142
142
  the ticket again because the status was left alone.
143
143
  - A hold gave new direction on an `in_progress` ticket → adjust the branch
144
144
  and comment on what changed.
145
+ - A ticket whose `runner direction` cell is not `—` has been handed back by
146
+ the crew itself (a conflict with the base, a requeue, a bounce). That is
147
+ direction exactly as a comment from a hold is, and "no new comment" does not
148
+ cancel it: for `conflict_handed_back`, merge `<remote>/<base>` into the
149
+ branch, resolve, run the suite, push as a fast-forward and set `fixed`
150
+ (Step 4.4). Never answer it with "nothing to build".
145
151
  - An `in_progress` ticket with no assignee and no new comment → still "up
146
152
  for grabs, more work to do": a hold clearing the assignee *is* the signal.
147
153
  - **An `in_progress` ticket assigned to you with no new comment is your own
@@ -397,8 +403,10 @@ worktree, runs the test gate and deploys. Three consequences:
397
403
  lines from Step 3.5 are the only input the release phase has; nobody
398
404
  reads the diff later to fill them in.
399
405
  3. **A branch that no longer squash-merges cleanly comes back to you** at
400
- `in_progress`, reassigned to your lane, with a comment naming the
401
- conflict. Fix it in the same worktree and take it to `fixed` again. "Never
406
+ `in_progress`, reassigned to your lane, with the hand-back
407
+ posted to the event log, which the digest's `runner direction` column
408
+ shows (a ticket there reads as new direction even with no new comment).
409
+ Fix it in the same worktree and take it to `fixed` again. "Never
402
410
  merge" above means never merge INTO the base; merging the base INTO your
403
411
  own ticket branch is the expected fix (rule 4), and it is not "by hand to
404
412
  help".