@tablation/crew 0.49.3 → 0.49.4
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).
|
|
@@ -8738,21 +8751,15 @@ async function enforceHandoff(i) {
|
|
|
8738
8751
|
return { kind: "failed", why: e.message };
|
|
8739
8752
|
}
|
|
8740
8753
|
}
|
|
8741
|
-
async function guardRunHandoff(route, ship, tracker, role, workingId, log) {
|
|
8754
|
+
async function guardRunHandoff(route, ship, tracker, role, workingId, log, since2) {
|
|
8742
8755
|
if (!workingId || role !== "dev" && role !== "design" && role !== "qa") return null;
|
|
8743
8756
|
const seatId = route.resolved?.seats[role];
|
|
8744
8757
|
if (!seatId) return null;
|
|
8745
8758
|
try {
|
|
8746
8759
|
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);
|
|
8760
|
+
const resolved = repoConfigFor(route, t);
|
|
8761
|
+
if (!resolved) return null;
|
|
8762
|
+
const { target, cfg } = resolved;
|
|
8756
8763
|
if (role === "qa") {
|
|
8757
8764
|
await retireRunWorktree(tracker, target.dir, cfg, t, role, seatId, ship.name, log);
|
|
8758
8765
|
return null;
|
|
@@ -8771,6 +8778,7 @@ async function guardRunHandoff(route, ship, tracker, role, workingId, log) {
|
|
|
8771
8778
|
log
|
|
8772
8779
|
});
|
|
8773
8780
|
if (outcome.kind === "pushed") await retireRunWorktree(tracker, target.dir, cfg, t, role, seatId, ship.name, log);
|
|
8781
|
+
if (since2) await guardRunStrays(route, ship, tracker, role, workingId, since2, log);
|
|
8774
8782
|
return outcome;
|
|
8775
8783
|
} catch (e) {
|
|
8776
8784
|
log.warn(`could not check the hand-off: ${e.message}`, { step: "handoff" });
|
|
@@ -8793,6 +8801,88 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
|
|
|
8793
8801
|
log.warn(`could not retire the run's worktree: ${e.message}`, { step: "handoff" });
|
|
8794
8802
|
}
|
|
8795
8803
|
}
|
|
8804
|
+
function repoConfigFor(route, t) {
|
|
8805
|
+
const target = repoTargetFor(route, t.repo_id);
|
|
8806
|
+
if (!target) return null;
|
|
8807
|
+
const o = route.repoOverrides[target.name];
|
|
8808
|
+
const cfg = resolveRepoConfig(loadRepoConfig(target.dir), {
|
|
8809
|
+
hooks: { ...route.hooks, ...o?.hooks },
|
|
8810
|
+
labels: { ...route.labels, ...o?.labels },
|
|
8811
|
+
branch: { ...route.branch, ...o?.branch },
|
|
8812
|
+
worktrees: { nameOverride: o?.worktreeName }
|
|
8813
|
+
}, target.dir);
|
|
8814
|
+
return { target, cfg };
|
|
8815
|
+
}
|
|
8816
|
+
var STRAY_HANDOFF_MARKER = "<!-- crew:stray-handoff -->";
|
|
8817
|
+
async function guardStrayHandoffs(i) {
|
|
8818
|
+
const st = i.contract.statuses;
|
|
8819
|
+
if (i.role === "qa") return;
|
|
8820
|
+
for (const t of i.candidates) {
|
|
8821
|
+
const at = { ticket: t.issue_id, step: "handoff" };
|
|
8822
|
+
try {
|
|
8823
|
+
const holder = i.myShipId ? ticketHolderShipId(t, i.contract) : null;
|
|
8824
|
+
if (holder && holder !== i.myShipId) {
|
|
8825
|
+
await i.writer.updateTicket(t.id, { status: st.building });
|
|
8826
|
+
await i.writer.postEvent(
|
|
8827
|
+
t.id,
|
|
8828
|
+
`${STRAY_HANDOFF_MARKER}
|
|
8829
|
+
**Hand-off bounced: this ticket is held by another ship.**
|
|
8830
|
+
|
|
8831
|
+
${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.`,
|
|
8832
|
+
i.seatId,
|
|
8833
|
+
"bounced"
|
|
8834
|
+
).catch(() => i.log.warn("bounced, but the note failed to post", at));
|
|
8835
|
+
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);
|
|
8836
|
+
continue;
|
|
8837
|
+
}
|
|
8838
|
+
const co = i.checkout(t);
|
|
8839
|
+
if (!co || !existingBranchForTicket(co.dir, co.cfg, t, i.role)) {
|
|
8840
|
+
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);
|
|
8841
|
+
continue;
|
|
8842
|
+
}
|
|
8843
|
+
const v = inspectHandoff(co.dir, co.cfg, t, i.role);
|
|
8844
|
+
i.log.warn(`${t.issue_id} was handed off by a run that did not claim it`, at);
|
|
8845
|
+
if (v.kind === "pushed") {
|
|
8846
|
+
await i.writer.postEvent(t.id, pushedEventBody(v.sha, v.remote, v.branch), i.seatId, "pushed");
|
|
8847
|
+
} else if (v.kind === "bounce") {
|
|
8848
|
+
if (v.origin && v.local && !isAncestor(co.dir, v.origin, v.local)) {
|
|
8849
|
+
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);
|
|
8850
|
+
continue;
|
|
8851
|
+
}
|
|
8852
|
+
await i.writer.updateTicket(t.id, { status: st.building, ...t.assignee_id ? {} : { assignee_id: i.seatId } });
|
|
8853
|
+
await i.writer.postEvent(t.id, bounceComment(v, i.shipName), i.seatId, "bounced").catch(() => {
|
|
8854
|
+
});
|
|
8855
|
+
}
|
|
8856
|
+
} catch (e) {
|
|
8857
|
+
i.log.warn(`could not check the stray hand-off: ${e.message}`, at);
|
|
8858
|
+
}
|
|
8859
|
+
}
|
|
8860
|
+
}
|
|
8861
|
+
async function guardRunStrays(route, ship, tracker, role, workingId, since2, log) {
|
|
8862
|
+
const seatId = route.resolved?.seats[role];
|
|
8863
|
+
if (!seatId) return;
|
|
8864
|
+
try {
|
|
8865
|
+
const affinity = await tracker.claimAffinity();
|
|
8866
|
+
const candidates = await tracker.handedOffSince(since2, workingId);
|
|
8867
|
+
if (candidates.length === 0) return;
|
|
8868
|
+
await guardStrayHandoffs({
|
|
8869
|
+
writer: tracker,
|
|
8870
|
+
contract: tracker.contract,
|
|
8871
|
+
candidates,
|
|
8872
|
+
seatId,
|
|
8873
|
+
role,
|
|
8874
|
+
myShipId: affinity?.shipId ?? null,
|
|
8875
|
+
shipName: ship.name,
|
|
8876
|
+
checkout: (t) => {
|
|
8877
|
+
const r = repoConfigFor(route, t);
|
|
8878
|
+
return r ? { dir: r.target.dir, cfg: r.cfg } : null;
|
|
8879
|
+
},
|
|
8880
|
+
log
|
|
8881
|
+
});
|
|
8882
|
+
} catch (e) {
|
|
8883
|
+
log.warn(`could not check for stray hand-offs: ${e.message}`, { step: "handoff" });
|
|
8884
|
+
}
|
|
8885
|
+
}
|
|
8796
8886
|
|
|
8797
8887
|
// src/connect-wizard.ts
|
|
8798
8888
|
import { delimiter, dirname as dirname8, join as join17 } from "path";
|
|
@@ -12743,8 +12833,9 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
12743
12833
|
emit.warn(`could not beat ship engaged: ${e.message}`, { step: "agent" });
|
|
12744
12834
|
}
|
|
12745
12835
|
try {
|
|
12836
|
+
const runStart = (/* @__PURE__ */ new Date()).toISOString();
|
|
12746
12837
|
await spawnAgent(plan, emit);
|
|
12747
|
-
await guardRunHandoff(route, ship, tracker, role, workingId, emit);
|
|
12838
|
+
await guardRunHandoff(route, ship, tracker, role, workingId, emit, runStart);
|
|
12748
12839
|
} finally {
|
|
12749
12840
|
if (memberId) {
|
|
12750
12841
|
try {
|
|
@@ -14019,8 +14110,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14019
14110
|
wemit.warn(`could not beat ship engaged: ${e.message}`, { step: "agent" });
|
|
14020
14111
|
}
|
|
14021
14112
|
try {
|
|
14113
|
+
const runStart = (/* @__PURE__ */ new Date()).toISOString();
|
|
14022
14114
|
await spawnAgent(fleetPlan, wemit);
|
|
14023
|
-
await guardRunHandoff(w.route, cfg.ship, fleetTracker, w.role, fleetWorkingId, wemit);
|
|
14115
|
+
await guardRunHandoff(w.route, cfg.ship, fleetTracker, w.role, fleetWorkingId, wemit, runStart);
|
|
14024
14116
|
} finally {
|
|
14025
14117
|
dropLock();
|
|
14026
14118
|
if (fleetMemberId) {
|
|
@@ -14234,8 +14326,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14234
14326
|
emit.warn(`could not beat ship engaged: ${e.message}`, { step: "agent" });
|
|
14235
14327
|
}
|
|
14236
14328
|
try {
|
|
14329
|
+
const runStart = (/* @__PURE__ */ new Date()).toISOString();
|
|
14237
14330
|
await spawnAgent(plan, emit);
|
|
14238
|
-
await guardRunHandoff(route, cfg.ship, tracker2, current, workingId, emit);
|
|
14331
|
+
await guardRunHandoff(route, cfg.ship, tracker2, current, workingId, emit, runStart);
|
|
14239
14332
|
} finally {
|
|
14240
14333
|
dropLock();
|
|
14241
14334
|
if (memberId) {
|
package/package.json
CHANGED
|
@@ -478,6 +478,12 @@ existing worktree and note that the status change was not needed.
|
|
|
478
478
|
|
|
479
479
|
- One ticket at a time. Never mix two tickets' changes in one branch,
|
|
480
480
|
worktree or commit.
|
|
481
|
+
- **A ticket that appears mid-run is not yours this run.** A bounce, a
|
|
482
|
+
re-accept or a hand-back that lands while you work another ticket is left for
|
|
483
|
+
a later run, which claims it with `assignee_id` and the ship hold like any
|
|
484
|
+
other. Finish the ticket you claimed. Before touching any worktree, re-fetch
|
|
485
|
+
its record and stop if the ship hold names another ship. Setting `fixed` on a
|
|
486
|
+
ticket you did not claim is bounced by the runner.
|
|
481
487
|
- **Stay in your lane.** Status decides QA's slice; `needs_design` decides
|
|
482
488
|
dev versus design. Both are checked before assignee and before whether a
|
|
483
489
|
worktree exists.
|
|
@@ -478,6 +478,12 @@ existing worktree and note that the status change was not needed.
|
|
|
478
478
|
|
|
479
479
|
- One ticket at a time. Never mix two tickets' changes in one branch,
|
|
480
480
|
worktree or commit.
|
|
481
|
+
- **A ticket that appears mid-run is not yours this run.** A bounce, a
|
|
482
|
+
re-accept or a hand-back that lands while you work another ticket is left for
|
|
483
|
+
a later run, which claims it with `assignee_id` and the ship hold like any
|
|
484
|
+
other. Finish the ticket you claimed. Before touching any worktree, re-fetch
|
|
485
|
+
its record and stop if the ship hold names another ship. Setting `fixed` on a
|
|
486
|
+
ticket you did not claim is bounced by the runner.
|
|
481
487
|
- **Stay in your lane.** Status decides QA's slice (`fixed` and `qa`);
|
|
482
488
|
everything else is dev's. Checked before assignee and before whether a
|
|
483
489
|
worktree exists.
|