@tablation/crew 0.41.0 → 0.42.0

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
@@ -3103,7 +3103,8 @@ var DEFAULT_CONTRACT = {
3103
3103
  ship: "ship_id",
3104
3104
  heldBy: "held_by_ship_id",
3105
3105
  verifiedSha: "verified_sha",
3106
- prRef: "pr_ref"
3106
+ prRef: "pr_ref",
3107
+ position: "position"
3107
3108
  },
3108
3109
  comments: {
3109
3110
  parent: "ticket_id",
@@ -5391,20 +5392,25 @@ function issueNumber(t) {
5391
5392
  function epicRank(t) {
5392
5393
  return t.epicInProgress ? 0 : 1;
5393
5394
  }
5395
+ function positionRank(t) {
5396
+ const p = t.position;
5397
+ return typeof p === "number" && Number.isFinite(p) ? p : Number.POSITIVE_INFINITY;
5398
+ }
5394
5399
  function rank(t, c = DEFAULT_CONTRACT) {
5395
- return [effectivePriority(t, c), severityRank(t, c), epicRank(t), issueNumber(t)];
5400
+ return [effectivePriority(t, c), positionRank(t), severityRank(t, c), epicRank(t), issueNumber(t)];
5396
5401
  }
5397
5402
  function compareRank(a, b, c = DEFAULT_CONTRACT) {
5398
5403
  const ra = rank(a, c);
5399
5404
  const rb = rank(b, c);
5400
5405
  for (let i = 0; i < ra.length; i++) {
5401
- const d = ra[i] - rb[i];
5402
- if (d !== 0) return d;
5406
+ const x = ra[i];
5407
+ const y = rb[i];
5408
+ if (x !== y) return x < y ? -1 : 1;
5403
5409
  }
5404
5410
  return 0;
5405
5411
  }
5406
5412
  function rankScalar(t, c = DEFAULT_CONTRACT) {
5407
- const [p, s, e, n] = rank(t, c);
5413
+ const [p, , s, e, n] = rank(t, c);
5408
5414
  return p * 1e9 + s * 1e7 + e * 1e6 + n;
5409
5415
  }
5410
5416
  var NOTHING_ACTIONABLE = 999999999999;
@@ -5611,7 +5617,7 @@ function claimStatuses(role, c) {
5611
5617
  }
5612
5618
  function rankedCandidates(role, i) {
5613
5619
  const c = i.contract ?? DEFAULT_CONTRACT;
5614
- return [...roleCandidates(role, i)].sort((a, b) => rankScalar(a, c) - rankScalar(b, c));
5620
+ return [...roleCandidates(role, i)].sort((a, b) => compareRank(a, b, c));
5615
5621
  }
5616
5622
  function roleTopRank(role, i) {
5617
5623
  if (role !== "qa" && role !== "triage" && !i.seats[role]) return NOTHING_ACTIONABLE;
@@ -5624,7 +5630,7 @@ function actionableSummary(i, pending) {
5624
5630
  const c = i.contract ?? DEFAULT_CONTRACT;
5625
5631
  const all = pending.flatMap((role) => roleCandidates(role, i));
5626
5632
  if (all.length === 0) return { count: 0 };
5627
- const top = all.reduce((best, t) => rankScalar(t, c) < rankScalar(best, c) ? t : best);
5633
+ const top = all.reduce((best, t) => compareRank(t, best, c) < 0 ? t : best);
5628
5634
  return { count: all.length, top };
5629
5635
  }
5630
5636
  var FIXED_RANK = { qa: -2, triage: -1 };
@@ -5850,6 +5856,7 @@ function blockers(t, i) {
5850
5856
  function modeCell(t, i) {
5851
5857
  return i.workModes && i.workModes.modes.size > 0 ? modeOf(t, i.workModes) : "\u2014";
5852
5858
  }
5859
+ var effCell = (t) => `p${effectivePriority(t)}${positionRank(t) === Number.POSITIVE_INFINITY ? "" : ` #${positionRank(t)}`}`;
5853
5860
  var sorted = (ts) => [...ts].sort(compareRank);
5854
5861
  var stream = (segments) => `${segments.join("\n")}
5855
5862
  `;
@@ -5868,7 +5875,7 @@ function buildingDigest(i) {
5868
5875
  const worktree = i.worktreeFor ? i.worktreeFor(t) : "";
5869
5876
  const dir = i.dirFor ? i.dirFor(t) : null;
5870
5877
  const repo = i.dirFor ? dir ?? "**NO CHECKOUT**" : "";
5871
- return `| ${displayKey2(t)} | ${repo} | ${branch} | ${worktree} | ${modeCell(t, i)} | ${t.status} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | p${effectivePriority(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
5878
+ 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"} |`;
5872
5879
  };
5873
5880
  const blockedRow = (t) => `| ${displayKey2(t)} | ${t.status} | ${who(t, i)} | p${effectivePriority(t)} | ${blockers(t, i)} |`;
5874
5881
  const blockedTable = (ts) => ts.length === 0 ? "\n_None._\n" : `
@@ -5929,7 +5936,7 @@ function qaDigest(i) {
5929
5936
  const n = newFromOthers(i, t.id);
5930
5937
  const dir = i.dirFor ? i.dirFor(t) : null;
5931
5938
  const repo = i.dirFor ? dir ?? "**NO CHECKOUT**" : "";
5932
- return `| ${displayKey2(t)} | ${repo} | ${modeCell(t, i)} | ${t.status} | ${builtBy(t)} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | p${effectivePriority(t)} | ${branchCell(t)} | ${worktreeCell(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
5939
+ return `| ${displayKey2(t)} | ${repo} | ${modeCell(t, i)} | ${t.status} | ${builtBy(t)} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | ${effCell(t)} | ${branchCell(t)} | ${worktreeCell(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
5933
5940
  };
5934
5941
  const waiting = i.tickets.filter((t) => (t.status === "qa" || t.status === "fixed") && i.blocked.has(t.id));
5935
5942
  const takeable = (status2) => i.tickets.filter((t) => t.status === status2 && !i.blocked.has(t.id));
@@ -6124,6 +6131,7 @@ async function decideCycle(o) {
6124
6131
  data: { tickets: tickets.length, comments: comments.length }
6125
6132
  });
6126
6133
  const epicColumn = tracker.contract.columns.epic;
6134
+ const positionColumn = tracker.contract.columns.position;
6127
6135
  const epicBuilding = tracker.contract.epics.statuses.building;
6128
6136
  const epicStatusColumn = tracker.contract.epics.statusColumn;
6129
6137
  const epicStatusById = new Map(epicRows.map((e) => [e.id, e[epicStatusColumn] ?? null]));
@@ -6143,6 +6151,9 @@ async function decideCycle(o) {
6143
6151
  for (const t of tickets) {
6144
6152
  const epicId = t[epicColumn];
6145
6153
  t.epicInProgress = typeof epicId === "string" && epicStatusById.get(epicId) === epicBuilding;
6154
+ const pos = t[positionColumn];
6155
+ const n = typeof pos === "string" && pos.trim() !== "" ? Number(pos) : pos;
6156
+ t.position = typeof n === "number" && Number.isFinite(n) ? n : null;
6146
6157
  }
6147
6158
  const extra = await tracker.ticketsByIds(missingBlockerIds(tickets));
6148
6159
  const info = blockerInfoMap(tickets, extra);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.41.0",
3
+ "version": "0.42.0",
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",
@@ -183,13 +183,17 @@ should is Step 1's call, not a filesystem check.
183
183
  and then it is a stop for a person (see "Stopping"), never a bare comment.
184
184
  - Otherwise pick from the digest's "Step 2" table, which is already
185
185
  `accepted`, in your lane and in order. Absent a digest, fetch those tickets
186
- and order them by **effective priority, then severity, then `issue_id`**:
186
+ and order them by **effective priority, then manual position, then severity, then `issue_id`**:
187
187
  - **Effective priority** is the stronger of the ticket's own `priority`
188
188
  and one derived from `severity` (`s1`→p0, `s2`→p1, `s3`→p2, `s4`→p3;
189
189
  neither set → p2). p0 beats p1 beats p2 beats p3, so an explicit
190
190
  priority can only move a ticket *forward*. Priority is an escalation
191
191
  lever; to rank something down, lower its severity.
192
- - Within one tier, more severe first (`s1` … `s4`, then unset).
192
+ - Within one tier, a ticket with a manual **`position`** (a lower number
193
+ is earlier) goes before one without; position only orders tickets of the
194
+ same effective priority and never lets a lower tier jump a higher one. A
195
+ workspace with no position column skips this clause.
196
+ - Then more severe first (`s1` … `s4`, then unset).
193
197
  - Within one severity, a ticket whose **epic is already in progress** goes
194
198
  first (finishing beats starting); a `planned` epic or none ranks the
195
199
  same. Only then oldest `issue_id` first.
@@ -183,13 +183,17 @@ should is Step 1's call, not a filesystem check.
183
183
  and then it is a stop for a person (see "Stopping"), never a bare comment.
184
184
  - Otherwise pick from the digest's "Step 2" table, which is already
185
185
  `accepted`, in your lane and in order. Absent a digest, fetch those tickets
186
- and order them by **effective priority, then severity, then `issue_id`**:
186
+ and order them by **effective priority, then manual position, then severity, then `issue_id`**:
187
187
  - **Effective priority** is the stronger of the ticket's own `priority`
188
188
  and one derived from `severity` (`s1`→p0, `s2`→p1, `s3`→p2, `s4`→p3;
189
189
  neither set → p2). p0 beats p1 beats p2 beats p3, so an explicit
190
190
  priority can only move a ticket *forward*. Priority is an escalation
191
191
  lever; to rank something down, lower its severity.
192
- - Within one tier, more severe first (`s1` … `s4`, then unset).
192
+ - Within one tier, a ticket with a manual **`position`** (a lower number
193
+ is earlier) goes before one without; position only orders tickets of the
194
+ same effective priority and never lets a lower tier jump a higher one. A
195
+ workspace with no position column skips this clause.
196
+ - Then more severe first (`s1` … `s4`, then unset).
193
197
  - Within one severity, a ticket whose **epic is already in progress** goes
194
198
  first (finishing beats starting); a `planned` epic or none ranks the
195
199
  same. Only then oldest `issue_id` first.