@tablation/crew 0.49.4 → 0.49.6
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
|
@@ -4931,6 +4931,8 @@ var Tracker = class {
|
|
|
4931
4931
|
* writes pass its seat's agent key so the event log and rule context see an
|
|
4932
4932
|
* agent, not the ship's human principal.
|
|
4933
4933
|
*/
|
|
4934
|
+
/** Told once per failed best-effort read (`events`); poll.ts points it at the pass's emitter. */
|
|
4935
|
+
onReadFailure;
|
|
4934
4936
|
constructor(route, ship, opts = {}) {
|
|
4935
4937
|
this.route = route;
|
|
4936
4938
|
if (!route.resolved) {
|
|
@@ -5372,10 +5374,16 @@ var Tracker = class {
|
|
|
5372
5374
|
*/
|
|
5373
5375
|
async events(type, limit = 500) {
|
|
5374
5376
|
const filters = encodeURIComponent(JSON.stringify([{ columnName: "event_type", operator: "EQ", value: type }]));
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5377
|
+
let rows;
|
|
5378
|
+
try {
|
|
5379
|
+
const res = await this.eventsFetch("GET", `/events?limit=${limit}&filters=${filters}`);
|
|
5380
|
+
if (res.status === 404) return [];
|
|
5381
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
5382
|
+
rows = await res.json();
|
|
5383
|
+
} catch (err) {
|
|
5384
|
+
this.onReadFailure?.(`reading ${type} events failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
5385
|
+
return [];
|
|
5386
|
+
}
|
|
5379
5387
|
return rows.filter((e) => e.event_type === type).sort((a, b) => Date.parse(b.occurred_at ?? b.created_at ?? "") - Date.parse(a.occurred_at ?? a.created_at ?? ""));
|
|
5380
5388
|
}
|
|
5381
5389
|
/** The newest event of `type` on a ticket, or undefined — e.g. the `pushed` tip QA verifies against. */
|
|
@@ -6112,6 +6120,7 @@ function operatorTodo(tickets, contract, operatorId) {
|
|
|
6112
6120
|
}
|
|
6113
6121
|
|
|
6114
6122
|
// src/digest.ts
|
|
6123
|
+
var DIRECTION_EVENT_TYPES = ["conflict_handed_back", "requeued", "bounced", "conflict_escalated"];
|
|
6115
6124
|
function displayKey2(t) {
|
|
6116
6125
|
return t.issue_tag ?? t.issue_id ?? "?";
|
|
6117
6126
|
}
|
|
@@ -6146,10 +6155,24 @@ function lastComment(i, ticketId) {
|
|
|
6146
6155
|
if (!last) return "\u2014";
|
|
6147
6156
|
return `${last.created_at.slice(0, 16)}Z ${author(last, i)}`;
|
|
6148
6157
|
}
|
|
6158
|
+
var eventAt = (e) => e.occurred_at ?? e.created_at ?? "";
|
|
6159
|
+
function pendingDirection(i, ticketId) {
|
|
6160
|
+
const mine = (i.events ?? []).filter((e) => e.record_id === ticketId);
|
|
6161
|
+
const pushed = mine.filter((e) => e.event_type === "pushed").map(eventAt).sort().pop() ?? "";
|
|
6162
|
+
return mine.filter((e) => DIRECTION_EVENT_TYPES.includes(e.event_type) && eventAt(e) > pushed).sort((a, b) => eventAt(a) < eventAt(b) ? 1 : -1)[0];
|
|
6163
|
+
}
|
|
6164
|
+
function directionCell(i, ticketId) {
|
|
6165
|
+
const d = pendingDirection(i, ticketId);
|
|
6166
|
+
if (!d) return "\u2014";
|
|
6167
|
+
const msg = (d.message ?? "").replace(/\s+/g, " ").trim().slice(0, 160);
|
|
6168
|
+
return `${d.event_type} ${eventAt(d).slice(0, 16)}Z${msg ? ` \u2014 ${msg}` : ""}`.replace(/\|/g, "/");
|
|
6169
|
+
}
|
|
6149
6170
|
function newFromOthers(i, ticketId) {
|
|
6150
|
-
|
|
6171
|
+
const comments = i.comments.filter(
|
|
6151
6172
|
(c) => c.ticket_id === ticketId && c.team_member_id !== i.me && c.created_at > i.watermark
|
|
6152
6173
|
).length;
|
|
6174
|
+
const d = pendingDirection(i, ticketId);
|
|
6175
|
+
return comments + (d && eventAt(d) > i.watermark ? 1 : 0);
|
|
6153
6176
|
}
|
|
6154
6177
|
function who(t, i) {
|
|
6155
6178
|
return whoAssigned(t, i) + heldBySuffix(t, i);
|
|
@@ -6188,14 +6211,14 @@ ${sorted(ts).map(row).join("\n")}
|
|
|
6188
6211
|
`;
|
|
6189
6212
|
}
|
|
6190
6213
|
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
|
|
6214
|
+
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
6215
|
const row = (t) => {
|
|
6193
6216
|
const n = newFromOthers(i, t.id);
|
|
6194
6217
|
const branch = i.branchFor ? i.branchFor(t) : "";
|
|
6195
6218
|
const worktree = i.worktreeFor ? i.worktreeFor(t) : "";
|
|
6196
6219
|
const dir = i.dirFor ? i.dirFor(t) : null;
|
|
6197
6220
|
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"} |`;
|
|
6221
|
+
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
6222
|
};
|
|
6200
6223
|
const blockedRow = (t) => `| ${displayKey2(t)} | ${t.status} | ${who(t, i)} | p${effectivePriority(t)} | ${blockers(t, i)} |`;
|
|
6201
6224
|
const blockedTable = (ts) => ts.length === 0 ? "\n_None._\n" : `
|
|
@@ -6226,6 +6249,7 @@ ${sorted(held).map((t) => `| ${displayKey2(t)} | ${stopOf(t)} |`).join("\n")}
|
|
|
6226
6249
|
"## Current queue \u2014 built for you by the poll\n",
|
|
6227
6250
|
"\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
6251
|
'\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',
|
|
6252
|
+
'\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
6253
|
"\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
6254
|
...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
6255
|
"\n### Step 1 \u2014 open tickets that may be yours to act on\n",
|
|
@@ -6439,16 +6463,26 @@ async function runStalledSweep(o, stalled) {
|
|
|
6439
6463
|
async function decideCycle(o) {
|
|
6440
6464
|
const { route, ship, state, emit } = o;
|
|
6441
6465
|
const tracker = new Tracker(route, ship);
|
|
6466
|
+
tracker.onReadFailure = (m) => emit.warn(m);
|
|
6442
6467
|
emit.enter("poll");
|
|
6468
|
+
const optional = async (what, read, fallback) => {
|
|
6469
|
+
try {
|
|
6470
|
+
return await read;
|
|
6471
|
+
} catch (err) {
|
|
6472
|
+
emit.warn(`reading ${what} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
6473
|
+
return fallback;
|
|
6474
|
+
}
|
|
6475
|
+
};
|
|
6443
6476
|
const [tickets, comments, crewRows, epicRows, ships, projectModes, stalledEvents] = await Promise.all([
|
|
6444
6477
|
tracker.openTickets(),
|
|
6445
6478
|
tracker.comments(200),
|
|
6446
6479
|
tracker.crewRows(),
|
|
6447
|
-
tracker.epicRows(),
|
|
6448
|
-
tracker.shipRows(),
|
|
6449
|
-
tracker.projectWorkModes(),
|
|
6480
|
+
optional("epics", tracker.epicRows(), []),
|
|
6481
|
+
optional("ships", tracker.shipRows(), []),
|
|
6482
|
+
optional("project work modes", tracker.projectWorkModes(), /* @__PURE__ */ new Map()),
|
|
6450
6483
|
tracker.events("stalled")
|
|
6451
6484
|
]);
|
|
6485
|
+
const directionEvents = (await Promise.all([...DIRECTION_EVENT_TYPES, "pushed"].map((t) => tracker.events(t)))).flat();
|
|
6452
6486
|
emit.emit(`${tickets.length} open ticket(s), ${comments.length} comment(s)`, {
|
|
6453
6487
|
data: { tickets: tickets.length, comments: comments.length }
|
|
6454
6488
|
});
|
|
@@ -6594,6 +6628,7 @@ async function decideCycle(o) {
|
|
|
6594
6628
|
selection,
|
|
6595
6629
|
actionable,
|
|
6596
6630
|
watermark,
|
|
6631
|
+
directionEvents,
|
|
6597
6632
|
selectionInput,
|
|
6598
6633
|
attention: attentionCurrent
|
|
6599
6634
|
};
|
|
@@ -6673,6 +6708,7 @@ function writeDigest(o, d, role, stateDir) {
|
|
|
6673
6708
|
ships: d.ships,
|
|
6674
6709
|
myShipId: d.ships.find((s) => (s.name ?? "").trim() === o.ship.name.trim())?.id ?? null,
|
|
6675
6710
|
watermark: d.watermark,
|
|
6711
|
+
events: d.directionEvents,
|
|
6676
6712
|
blocked: d.blocked,
|
|
6677
6713
|
blockerInfo: d.info
|
|
6678
6714
|
};
|
|
@@ -6777,7 +6813,14 @@ function worktreeForBranch(cwd, branch) {
|
|
|
6777
6813
|
}
|
|
6778
6814
|
return null;
|
|
6779
6815
|
}
|
|
6780
|
-
var
|
|
6816
|
+
var stampOf = (e) => e.occurred_at ?? e.created_at ?? "";
|
|
6817
|
+
var priorBounces = (ticket, events) => {
|
|
6818
|
+
const mine = events.filter((e) => e.record_id === ticket.id);
|
|
6819
|
+
const pushes = mine.filter((e) => e.event_type === "pushed").map(stampOf);
|
|
6820
|
+
return mine.filter(
|
|
6821
|
+
(e) => e.event_type === "conflict_handed_back" && pushes.some((p) => p > stampOf(e))
|
|
6822
|
+
).length;
|
|
6823
|
+
};
|
|
6781
6824
|
function planConflictBounce(cwd, ticket, branch, base, paths, events) {
|
|
6782
6825
|
return {
|
|
6783
6826
|
ticket,
|
|
@@ -12521,7 +12564,18 @@ async function runDaemonLoop(o) {
|
|
|
12521
12564
|
let passes = 0;
|
|
12522
12565
|
for (; ; ) {
|
|
12523
12566
|
if (o.signal?.aborted) return "aborted";
|
|
12524
|
-
|
|
12567
|
+
let result;
|
|
12568
|
+
try {
|
|
12569
|
+
result = await o.runPass();
|
|
12570
|
+
} catch (err) {
|
|
12571
|
+
passes++;
|
|
12572
|
+
o.onPassError?.(err);
|
|
12573
|
+
if (o.maxPasses !== void 0 && passes >= o.maxPasses) return "maxPasses";
|
|
12574
|
+
if (o.signal?.aborted) return "aborted";
|
|
12575
|
+
await sleep(backoff);
|
|
12576
|
+
backoff = Math.min(backoff * 2, ceiling);
|
|
12577
|
+
continue;
|
|
12578
|
+
}
|
|
12525
12579
|
passes++;
|
|
12526
12580
|
if (result.started.length > 0) {
|
|
12527
12581
|
backoff = floor;
|
|
@@ -13822,7 +13876,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13822
13876
|
}
|
|
13823
13877
|
}
|
|
13824
13878
|
if (outcome.conflicts?.length || outcome.unbuildable?.length) {
|
|
13825
|
-
const handedBack = await tracker.events("conflict_handed_back");
|
|
13879
|
+
const handedBack = [...await tracker.events("conflict_handed_back"), ...await tracker.events("pushed")];
|
|
13826
13880
|
const flagged = await tracker.events("stranded_flagged");
|
|
13827
13881
|
const seat = releaseSeat(c);
|
|
13828
13882
|
for (const f of outcome.conflicts ?? []) {
|
|
@@ -14447,6 +14501,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14447
14501
|
`)
|
|
14448
14502
|
})
|
|
14449
14503
|
}),
|
|
14504
|
+
onPassError: (err) => emit.error(
|
|
14505
|
+
`daemon pass failed, continuing: ${err instanceof Error ? err.stack ?? err.message : String(err)}`
|
|
14506
|
+
),
|
|
14450
14507
|
checkStale: makeStaleChecker(watchPaths, { reloadSentinel: reloadSentinelFor(cfg.ship.stateDir) }),
|
|
14451
14508
|
onPass: (result) => {
|
|
14452
14509
|
if (result.started.length) emit.emit(`daemon pass started: ${result.started.join(", ")}`);
|
package/package.json
CHANGED
|
@@ -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
|
|
401
|
-
|
|
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
|
|
401
|
-
|
|
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".
|