@tablation/crew 0.49.3 → 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
|
@@ -4971,6 +4971,19 @@ var Tracker = class {
|
|
|
4971
4971
|
limit: 500
|
|
4972
4972
|
});
|
|
4973
4973
|
}
|
|
4974
|
+
/**
|
|
4975
|
+
* Tickets at the hand-off status whose row changed at or after `since`,
|
|
4976
|
+
* other than `exceptId` (CREW-1759: a run's stray hand-offs).
|
|
4977
|
+
*/
|
|
4978
|
+
async handedOffSince(since2, exceptId) {
|
|
4979
|
+
const c = this.contract;
|
|
4980
|
+
const rows = await this.client.records.list(this.models.issues, {
|
|
4981
|
+
filters: encodeFilters([{ columnName: c.columns.status, operator: "EQ", value: c.statuses.handoff }]),
|
|
4982
|
+
limit: 100
|
|
4983
|
+
});
|
|
4984
|
+
const from = Date.parse(since2);
|
|
4985
|
+
return rows.filter((t) => t.id !== exceptId && Date.parse(t.updated_at) >= from);
|
|
4986
|
+
}
|
|
4974
4987
|
/**
|
|
4975
4988
|
* The tickets behind a set of ticket numbers — the cleanup sweeps' input
|
|
4976
4989
|
* (CREW-1593).
|
|
@@ -6099,6 +6112,7 @@ function operatorTodo(tickets, contract, operatorId) {
|
|
|
6099
6112
|
}
|
|
6100
6113
|
|
|
6101
6114
|
// src/digest.ts
|
|
6115
|
+
var DIRECTION_EVENT_TYPES = ["conflict_handed_back", "requeued", "bounced", "conflict_escalated"];
|
|
6102
6116
|
function displayKey2(t) {
|
|
6103
6117
|
return t.issue_tag ?? t.issue_id ?? "?";
|
|
6104
6118
|
}
|
|
@@ -6133,10 +6147,24 @@ function lastComment(i, ticketId) {
|
|
|
6133
6147
|
if (!last) return "\u2014";
|
|
6134
6148
|
return `${last.created_at.slice(0, 16)}Z ${author(last, i)}`;
|
|
6135
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
|
+
}
|
|
6136
6162
|
function newFromOthers(i, ticketId) {
|
|
6137
|
-
|
|
6163
|
+
const comments = i.comments.filter(
|
|
6138
6164
|
(c) => c.ticket_id === ticketId && c.team_member_id !== i.me && c.created_at > i.watermark
|
|
6139
6165
|
).length;
|
|
6166
|
+
const d = pendingDirection(i, ticketId);
|
|
6167
|
+
return comments + (d && eventAt(d) > i.watermark ? 1 : 0);
|
|
6140
6168
|
}
|
|
6141
6169
|
function who(t, i) {
|
|
6142
6170
|
return whoAssigned(t, i) + heldBySuffix(t, i);
|
|
@@ -6175,14 +6203,14 @@ ${sorted(ts).map(row).join("\n")}
|
|
|
6175
6203
|
`;
|
|
6176
6204
|
}
|
|
6177
6205
|
function buildingDigest(i) {
|
|
6178
|
-
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|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|";
|
|
6179
6207
|
const row = (t) => {
|
|
6180
6208
|
const n = newFromOthers(i, t.id);
|
|
6181
6209
|
const branch = i.branchFor ? i.branchFor(t) : "";
|
|
6182
6210
|
const worktree = i.worktreeFor ? i.worktreeFor(t) : "";
|
|
6183
6211
|
const dir = i.dirFor ? i.dirFor(t) : null;
|
|
6184
6212
|
const repo = i.dirFor ? dir ?? "**NO CHECKOUT**" : "";
|
|
6185
|
-
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"} |`;
|
|
6186
6214
|
};
|
|
6187
6215
|
const blockedRow = (t) => `| ${displayKey2(t)} | ${t.status} | ${who(t, i)} | p${effectivePriority(t)} | ${blockers(t, i)} |`;
|
|
6188
6216
|
const blockedTable = (ts) => ts.length === 0 ? "\n_None._\n" : `
|
|
@@ -6213,6 +6241,7 @@ ${sorted(held).map((t) => `| ${displayKey2(t)} | ${stopOf(t)} |`).join("\n")}
|
|
|
6213
6241
|
"## Current queue \u2014 built for you by the poll\n",
|
|
6214
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",
|
|
6215
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',
|
|
6216
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",
|
|
6217
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"] : [],
|
|
6218
6247
|
"\n### Step 1 \u2014 open tickets that may be yours to act on\n",
|
|
@@ -6436,6 +6465,7 @@ async function decideCycle(o) {
|
|
|
6436
6465
|
tracker.projectWorkModes(),
|
|
6437
6466
|
tracker.events("stalled")
|
|
6438
6467
|
]);
|
|
6468
|
+
const directionEvents = (await Promise.all([...DIRECTION_EVENT_TYPES, "pushed"].map((t) => tracker.events(t)))).flat();
|
|
6439
6469
|
emit.emit(`${tickets.length} open ticket(s), ${comments.length} comment(s)`, {
|
|
6440
6470
|
data: { tickets: tickets.length, comments: comments.length }
|
|
6441
6471
|
});
|
|
@@ -6581,6 +6611,7 @@ async function decideCycle(o) {
|
|
|
6581
6611
|
selection,
|
|
6582
6612
|
actionable,
|
|
6583
6613
|
watermark,
|
|
6614
|
+
directionEvents,
|
|
6584
6615
|
selectionInput,
|
|
6585
6616
|
attention: attentionCurrent
|
|
6586
6617
|
};
|
|
@@ -6660,6 +6691,7 @@ function writeDigest(o, d, role, stateDir) {
|
|
|
6660
6691
|
ships: d.ships,
|
|
6661
6692
|
myShipId: d.ships.find((s) => (s.name ?? "").trim() === o.ship.name.trim())?.id ?? null,
|
|
6662
6693
|
watermark: d.watermark,
|
|
6694
|
+
events: d.directionEvents,
|
|
6663
6695
|
blocked: d.blocked,
|
|
6664
6696
|
blockerInfo: d.info
|
|
6665
6697
|
};
|
|
@@ -6764,7 +6796,14 @@ function worktreeForBranch(cwd, branch) {
|
|
|
6764
6796
|
}
|
|
6765
6797
|
return null;
|
|
6766
6798
|
}
|
|
6767
|
-
var
|
|
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
|
+
};
|
|
6768
6807
|
function planConflictBounce(cwd, ticket, branch, base, paths, events) {
|
|
6769
6808
|
return {
|
|
6770
6809
|
ticket,
|
|
@@ -8738,21 +8777,15 @@ async function enforceHandoff(i) {
|
|
|
8738
8777
|
return { kind: "failed", why: e.message };
|
|
8739
8778
|
}
|
|
8740
8779
|
}
|
|
8741
|
-
async function guardRunHandoff(route, ship, tracker, role, workingId, log) {
|
|
8780
|
+
async function guardRunHandoff(route, ship, tracker, role, workingId, log, since2) {
|
|
8742
8781
|
if (!workingId || role !== "dev" && role !== "design" && role !== "qa") return null;
|
|
8743
8782
|
const seatId = route.resolved?.seats[role];
|
|
8744
8783
|
if (!seatId) return null;
|
|
8745
8784
|
try {
|
|
8746
8785
|
const t = await tracker.ticket(workingId);
|
|
8747
|
-
const
|
|
8748
|
-
if (!
|
|
8749
|
-
const
|
|
8750
|
-
const cfg = resolveRepoConfig(loadRepoConfig(target.dir), {
|
|
8751
|
-
hooks: { ...route.hooks, ...o?.hooks },
|
|
8752
|
-
labels: { ...route.labels, ...o?.labels },
|
|
8753
|
-
branch: { ...route.branch, ...o?.branch },
|
|
8754
|
-
worktrees: { nameOverride: o?.worktreeName }
|
|
8755
|
-
}, target.dir);
|
|
8786
|
+
const resolved = repoConfigFor(route, t);
|
|
8787
|
+
if (!resolved) return null;
|
|
8788
|
+
const { target, cfg } = resolved;
|
|
8756
8789
|
if (role === "qa") {
|
|
8757
8790
|
await retireRunWorktree(tracker, target.dir, cfg, t, role, seatId, ship.name, log);
|
|
8758
8791
|
return null;
|
|
@@ -8771,6 +8804,7 @@ async function guardRunHandoff(route, ship, tracker, role, workingId, log) {
|
|
|
8771
8804
|
log
|
|
8772
8805
|
});
|
|
8773
8806
|
if (outcome.kind === "pushed") await retireRunWorktree(tracker, target.dir, cfg, t, role, seatId, ship.name, log);
|
|
8807
|
+
if (since2) await guardRunStrays(route, ship, tracker, role, workingId, since2, log);
|
|
8774
8808
|
return outcome;
|
|
8775
8809
|
} catch (e) {
|
|
8776
8810
|
log.warn(`could not check the hand-off: ${e.message}`, { step: "handoff" });
|
|
@@ -8793,6 +8827,88 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
|
|
|
8793
8827
|
log.warn(`could not retire the run's worktree: ${e.message}`, { step: "handoff" });
|
|
8794
8828
|
}
|
|
8795
8829
|
}
|
|
8830
|
+
function repoConfigFor(route, t) {
|
|
8831
|
+
const target = repoTargetFor(route, t.repo_id);
|
|
8832
|
+
if (!target) return null;
|
|
8833
|
+
const o = route.repoOverrides[target.name];
|
|
8834
|
+
const cfg = resolveRepoConfig(loadRepoConfig(target.dir), {
|
|
8835
|
+
hooks: { ...route.hooks, ...o?.hooks },
|
|
8836
|
+
labels: { ...route.labels, ...o?.labels },
|
|
8837
|
+
branch: { ...route.branch, ...o?.branch },
|
|
8838
|
+
worktrees: { nameOverride: o?.worktreeName }
|
|
8839
|
+
}, target.dir);
|
|
8840
|
+
return { target, cfg };
|
|
8841
|
+
}
|
|
8842
|
+
var STRAY_HANDOFF_MARKER = "<!-- crew:stray-handoff -->";
|
|
8843
|
+
async function guardStrayHandoffs(i) {
|
|
8844
|
+
const st = i.contract.statuses;
|
|
8845
|
+
if (i.role === "qa") return;
|
|
8846
|
+
for (const t of i.candidates) {
|
|
8847
|
+
const at = { ticket: t.issue_id, step: "handoff" };
|
|
8848
|
+
try {
|
|
8849
|
+
const holder = i.myShipId ? ticketHolderShipId(t, i.contract) : null;
|
|
8850
|
+
if (holder && holder !== i.myShipId) {
|
|
8851
|
+
await i.writer.updateTicket(t.id, { status: st.building });
|
|
8852
|
+
await i.writer.postEvent(
|
|
8853
|
+
t.id,
|
|
8854
|
+
`${STRAY_HANDOFF_MARKER}
|
|
8855
|
+
**Hand-off bounced: this ticket is held by another ship.**
|
|
8856
|
+
|
|
8857
|
+
${i.shipName ? `${i.shipName}'s ` : "A "}${i.role} run set it to \`${st.handoff}\` without having claimed it. Set back to \`${st.building}\`; the holding ship's own hand-off is the one that counts.`,
|
|
8858
|
+
i.seatId,
|
|
8859
|
+
"bounced"
|
|
8860
|
+
).catch(() => i.log.warn("bounced, but the note failed to post", at));
|
|
8861
|
+
i.log.warn(`${t.issue_id} was handed off by a run that did not claim it, while held by another ship \u2014 bounced to ${st.building}`, at);
|
|
8862
|
+
continue;
|
|
8863
|
+
}
|
|
8864
|
+
const co = i.checkout(t);
|
|
8865
|
+
if (!co || !existingBranchForTicket(co.dir, co.cfg, t, i.role)) {
|
|
8866
|
+
i.log.warn(`${t.issue_id} moved to ${st.handoff} during a run that did not claim it; no local branch to verify, left alone`, at);
|
|
8867
|
+
continue;
|
|
8868
|
+
}
|
|
8869
|
+
const v = inspectHandoff(co.dir, co.cfg, t, i.role);
|
|
8870
|
+
i.log.warn(`${t.issue_id} was handed off by a run that did not claim it`, at);
|
|
8871
|
+
if (v.kind === "pushed") {
|
|
8872
|
+
await i.writer.postEvent(t.id, pushedEventBody(v.sha, v.remote, v.branch), i.seatId, "pushed");
|
|
8873
|
+
} else if (v.kind === "bounce") {
|
|
8874
|
+
if (v.origin && v.local && !isAncestor(co.dir, v.origin, v.local)) {
|
|
8875
|
+
i.log.warn(`${t.issue_id}: \`${v.remote}/${v.branch}\` is at ${v.origin.slice(0, 7)}, not a tip this checkout built \u2014 left alone`, at);
|
|
8876
|
+
continue;
|
|
8877
|
+
}
|
|
8878
|
+
await i.writer.updateTicket(t.id, { status: st.building, ...t.assignee_id ? {} : { assignee_id: i.seatId } });
|
|
8879
|
+
await i.writer.postEvent(t.id, bounceComment(v, i.shipName), i.seatId, "bounced").catch(() => {
|
|
8880
|
+
});
|
|
8881
|
+
}
|
|
8882
|
+
} catch (e) {
|
|
8883
|
+
i.log.warn(`could not check the stray hand-off: ${e.message}`, at);
|
|
8884
|
+
}
|
|
8885
|
+
}
|
|
8886
|
+
}
|
|
8887
|
+
async function guardRunStrays(route, ship, tracker, role, workingId, since2, log) {
|
|
8888
|
+
const seatId = route.resolved?.seats[role];
|
|
8889
|
+
if (!seatId) return;
|
|
8890
|
+
try {
|
|
8891
|
+
const affinity = await tracker.claimAffinity();
|
|
8892
|
+
const candidates = await tracker.handedOffSince(since2, workingId);
|
|
8893
|
+
if (candidates.length === 0) return;
|
|
8894
|
+
await guardStrayHandoffs({
|
|
8895
|
+
writer: tracker,
|
|
8896
|
+
contract: tracker.contract,
|
|
8897
|
+
candidates,
|
|
8898
|
+
seatId,
|
|
8899
|
+
role,
|
|
8900
|
+
myShipId: affinity?.shipId ?? null,
|
|
8901
|
+
shipName: ship.name,
|
|
8902
|
+
checkout: (t) => {
|
|
8903
|
+
const r = repoConfigFor(route, t);
|
|
8904
|
+
return r ? { dir: r.target.dir, cfg: r.cfg } : null;
|
|
8905
|
+
},
|
|
8906
|
+
log
|
|
8907
|
+
});
|
|
8908
|
+
} catch (e) {
|
|
8909
|
+
log.warn(`could not check for stray hand-offs: ${e.message}`, { step: "handoff" });
|
|
8910
|
+
}
|
|
8911
|
+
}
|
|
8796
8912
|
|
|
8797
8913
|
// src/connect-wizard.ts
|
|
8798
8914
|
import { delimiter, dirname as dirname8, join as join17 } from "path";
|
|
@@ -12743,8 +12859,9 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
12743
12859
|
emit.warn(`could not beat ship engaged: ${e.message}`, { step: "agent" });
|
|
12744
12860
|
}
|
|
12745
12861
|
try {
|
|
12862
|
+
const runStart = (/* @__PURE__ */ new Date()).toISOString();
|
|
12746
12863
|
await spawnAgent(plan, emit);
|
|
12747
|
-
await guardRunHandoff(route, ship, tracker, role, workingId, emit);
|
|
12864
|
+
await guardRunHandoff(route, ship, tracker, role, workingId, emit, runStart);
|
|
12748
12865
|
} finally {
|
|
12749
12866
|
if (memberId) {
|
|
12750
12867
|
try {
|
|
@@ -13731,7 +13848,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13731
13848
|
}
|
|
13732
13849
|
}
|
|
13733
13850
|
if (outcome.conflicts?.length || outcome.unbuildable?.length) {
|
|
13734
|
-
const handedBack = await tracker.events("conflict_handed_back");
|
|
13851
|
+
const handedBack = [...await tracker.events("conflict_handed_back"), ...await tracker.events("pushed")];
|
|
13735
13852
|
const flagged = await tracker.events("stranded_flagged");
|
|
13736
13853
|
const seat = releaseSeat(c);
|
|
13737
13854
|
for (const f of outcome.conflicts ?? []) {
|
|
@@ -14019,8 +14136,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14019
14136
|
wemit.warn(`could not beat ship engaged: ${e.message}`, { step: "agent" });
|
|
14020
14137
|
}
|
|
14021
14138
|
try {
|
|
14139
|
+
const runStart = (/* @__PURE__ */ new Date()).toISOString();
|
|
14022
14140
|
await spawnAgent(fleetPlan, wemit);
|
|
14023
|
-
await guardRunHandoff(w.route, cfg.ship, fleetTracker, w.role, fleetWorkingId, wemit);
|
|
14141
|
+
await guardRunHandoff(w.route, cfg.ship, fleetTracker, w.role, fleetWorkingId, wemit, runStart);
|
|
14024
14142
|
} finally {
|
|
14025
14143
|
dropLock();
|
|
14026
14144
|
if (fleetMemberId) {
|
|
@@ -14234,8 +14352,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14234
14352
|
emit.warn(`could not beat ship engaged: ${e.message}`, { step: "agent" });
|
|
14235
14353
|
}
|
|
14236
14354
|
try {
|
|
14355
|
+
const runStart = (/* @__PURE__ */ new Date()).toISOString();
|
|
14237
14356
|
await spawnAgent(plan, emit);
|
|
14238
|
-
await guardRunHandoff(route, cfg.ship, tracker2, current, workingId, emit);
|
|
14357
|
+
await guardRunHandoff(route, cfg.ship, tracker2, current, workingId, emit, runStart);
|
|
14239
14358
|
} finally {
|
|
14240
14359
|
dropLock();
|
|
14241
14360
|
if (memberId) {
|
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".
|
|
@@ -478,6 +486,12 @@ existing worktree and note that the status change was not needed.
|
|
|
478
486
|
|
|
479
487
|
- One ticket at a time. Never mix two tickets' changes in one branch,
|
|
480
488
|
worktree or commit.
|
|
489
|
+
- **A ticket that appears mid-run is not yours this run.** A bounce, a
|
|
490
|
+
re-accept or a hand-back that lands while you work another ticket is left for
|
|
491
|
+
a later run, which claims it with `assignee_id` and the ship hold like any
|
|
492
|
+
other. Finish the ticket you claimed. Before touching any worktree, re-fetch
|
|
493
|
+
its record and stop if the ship hold names another ship. Setting `fixed` on a
|
|
494
|
+
ticket you did not claim is bounced by the runner.
|
|
481
495
|
- **Stay in your lane.** Status decides QA's slice; `needs_design` decides
|
|
482
496
|
dev versus design. Both are checked before assignee and before whether a
|
|
483
497
|
worktree exists.
|
|
@@ -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".
|
|
@@ -478,6 +486,12 @@ existing worktree and note that the status change was not needed.
|
|
|
478
486
|
|
|
479
487
|
- One ticket at a time. Never mix two tickets' changes in one branch,
|
|
480
488
|
worktree or commit.
|
|
489
|
+
- **A ticket that appears mid-run is not yours this run.** A bounce, a
|
|
490
|
+
re-accept or a hand-back that lands while you work another ticket is left for
|
|
491
|
+
a later run, which claims it with `assignee_id` and the ship hold like any
|
|
492
|
+
other. Finish the ticket you claimed. Before touching any worktree, re-fetch
|
|
493
|
+
its record and stop if the ship hold names another ship. Setting `fixed` on a
|
|
494
|
+
ticket you did not claim is bounced by the runner.
|
|
481
495
|
- **Stay in your lane.** Status decides QA's slice (`fixed` and `qa`);
|
|
482
496
|
everything else is dev's. Checked before assignee and before whether a
|
|
483
497
|
worktree exists.
|