@tablation/crew 0.11.0 → 0.12.1
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
|
@@ -1682,7 +1682,8 @@ var DEFAULT_CONTRACT = {
|
|
|
1682
1682
|
reportType: "report_type",
|
|
1683
1683
|
epic: "epic_id",
|
|
1684
1684
|
author: "filed_by_id",
|
|
1685
|
-
ship: "ship_id"
|
|
1685
|
+
ship: "ship_id",
|
|
1686
|
+
heldBy: "held_by_ship_id"
|
|
1686
1687
|
},
|
|
1687
1688
|
comments: {
|
|
1688
1689
|
parent: "ticket_id",
|
|
@@ -2971,6 +2972,20 @@ var Tracker = class {
|
|
|
2971
2972
|
...auth.commentShip && auth.shipId ? { [this.contract.comments.ship]: auth.shipId } : {}
|
|
2972
2973
|
});
|
|
2973
2974
|
}
|
|
2975
|
+
/**
|
|
2976
|
+
* Ship affinity for a claim (CREW-1386): stamp this ship's Ships row on the
|
|
2977
|
+
* ticket in the claim's own write, in the `held_by_ship_id` column (not
|
|
2978
|
+
* `ship_id`, which means "filed from"). `undefined` — claim exactly as
|
|
2979
|
+
* before — when the Issues table has no hold column or this ship has no row.
|
|
2980
|
+
*/
|
|
2981
|
+
async claimAffinity() {
|
|
2982
|
+
const auth = await this.authorshipProbe();
|
|
2983
|
+
if (!auth.issueHeld || !auth.shipId) return void 0;
|
|
2984
|
+
return {
|
|
2985
|
+
column: this.contract.columns.heldBy,
|
|
2986
|
+
shipId: auth.shipId
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2974
2989
|
/**
|
|
2975
2990
|
* The authorship probe (CREW-1371), read once per process: which optional
|
|
2976
2991
|
* `filed_by_id`/`ship_id` columns exist on Issues and Comments, and this
|
|
@@ -2992,6 +3007,7 @@ var Tracker = class {
|
|
|
2992
3007
|
return {
|
|
2993
3008
|
issueAuthor: issueFields.has(this.contract.columns.author),
|
|
2994
3009
|
issueShip: issueFields.has(this.contract.columns.ship),
|
|
3010
|
+
issueHeld: issueFields.has(this.contract.columns.heldBy),
|
|
2995
3011
|
commentShip: commentFields.has(this.contract.comments.ship),
|
|
2996
3012
|
shipId: shipRow?.id ?? null
|
|
2997
3013
|
};
|
|
@@ -3013,6 +3029,7 @@ var Tracker = class {
|
|
|
3013
3029
|
const missing = [];
|
|
3014
3030
|
if (!auth.issueAuthor) missing.push(`Issues.${this.contract.columns.author}`);
|
|
3015
3031
|
if (!auth.issueShip) missing.push(`Issues.${this.contract.columns.ship}`);
|
|
3032
|
+
if (!auth.issueHeld) missing.push(`Issues.${this.contract.columns.heldBy}`);
|
|
3016
3033
|
if (!auth.commentShip) missing.push(`Comments.${this.contract.comments.ship}`);
|
|
3017
3034
|
return missing;
|
|
3018
3035
|
}
|
|
@@ -3333,6 +3350,63 @@ function agedRankScalar(rawRank, aging, now) {
|
|
|
3333
3350
|
return Math.round(rawRank * (1 - fraction));
|
|
3334
3351
|
}
|
|
3335
3352
|
|
|
3353
|
+
// src/ship-affinity.ts
|
|
3354
|
+
var CYCLE_SECONDS = 120;
|
|
3355
|
+
var SHIP_DEAD_AFTER_CYCLES = 10;
|
|
3356
|
+
function shipIsAlive(ship, now) {
|
|
3357
|
+
if (!ship?.last_seen) return false;
|
|
3358
|
+
const seen = Date.parse(ship.last_seen);
|
|
3359
|
+
return Number.isFinite(seen) && now - seen <= SHIP_DEAD_AFTER_CYCLES * CYCLE_SECONDS * 1e3;
|
|
3360
|
+
}
|
|
3361
|
+
function ticketHolderShipId(t, contract) {
|
|
3362
|
+
const v = t[contract.columns.heldBy];
|
|
3363
|
+
return typeof v === "string" && v ? v : null;
|
|
3364
|
+
}
|
|
3365
|
+
function heldByOtherShip(t, a) {
|
|
3366
|
+
const statuses = a.contract.statuses;
|
|
3367
|
+
if ([statuses.handoff, statuses.verifying].includes(t.status)) return false;
|
|
3368
|
+
return heldByLiveOtherShip(t, a);
|
|
3369
|
+
}
|
|
3370
|
+
function heldByLiveOtherShip(t, a) {
|
|
3371
|
+
if (!a.myShipId) return false;
|
|
3372
|
+
const holder = ticketHolderShipId(t, a.contract);
|
|
3373
|
+
if (!holder || holder === a.myShipId) return false;
|
|
3374
|
+
const owner = a.ships.find((s) => s.id === holder);
|
|
3375
|
+
return shipIsAlive(owner, a.now ?? Date.now());
|
|
3376
|
+
}
|
|
3377
|
+
function shipIdByName(ships, name) {
|
|
3378
|
+
const mine = ships.filter((r) => (r.name ?? "").trim() === name.trim());
|
|
3379
|
+
return mine.length === 1 ? mine[0].id : null;
|
|
3380
|
+
}
|
|
3381
|
+
async function noteReaccepted(tracker, result, seat, shipLabel) {
|
|
3382
|
+
if (!result.reaccepted || !result.ticket) return;
|
|
3383
|
+
try {
|
|
3384
|
+
await tracker.postEvent(
|
|
3385
|
+
result.ticket.id,
|
|
3386
|
+
`Re-accepted while held by ${shipLabel}; resuming there, not re-claiming. Setting a held ticket back to \`accepted\` is not needed \u2014 to move it to another ship, clear \`held_by_ship_id\`.`,
|
|
3387
|
+
seat
|
|
3388
|
+
);
|
|
3389
|
+
} catch {
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
function planHoldReleases(tickets, contract) {
|
|
3393
|
+
const ends = [contract.statuses.handoff, contract.statuses.verified];
|
|
3394
|
+
return tickets.filter((t) => ends.includes(t.status) && ticketHolderShipId(t, contract) !== null);
|
|
3395
|
+
}
|
|
3396
|
+
async function applyHoldReleases(writer, tickets, contract, emit) {
|
|
3397
|
+
let cleared = 0;
|
|
3398
|
+
for (const t of tickets) {
|
|
3399
|
+
try {
|
|
3400
|
+
await writer.updateTicket(t.id, { [contract.columns.heldBy]: null }, t.updated_at);
|
|
3401
|
+
cleared++;
|
|
3402
|
+
emit.emit(`released ship hold (${t.status})`, { ticket: t.issue_id, step: "sweep" });
|
|
3403
|
+
} catch (e) {
|
|
3404
|
+
emit.warn(`could not release ship hold: ${e.message}`, { ticket: t.issue_id, step: "sweep" });
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
return cleared;
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3336
3410
|
// src/select.ts
|
|
3337
3411
|
var qaSlice = (tickets) => tickets.filter((t) => t.status === "fixed" || t.status === "qa");
|
|
3338
3412
|
var buildingSlice = (tickets, role) => tickets.filter((t) => role === "design" ? t.needs_design === true : t.needs_design !== true);
|
|
@@ -3344,10 +3418,23 @@ function sliceFor(tickets, role) {
|
|
|
3344
3418
|
return buildingSlice(tickets, role);
|
|
3345
3419
|
}
|
|
3346
3420
|
var isHeld = (t, holds2) => !!t.assignee_id && holds2.has(t.assignee_id);
|
|
3421
|
+
function affinityContext(i) {
|
|
3422
|
+
return i.ships ? { myShipId: i.ships.myShipId, ships: i.ships.rows, contract: i.contract ?? DEFAULT_CONTRACT, now: i.ships.now } : null;
|
|
3423
|
+
}
|
|
3424
|
+
function withHoldCheck(affinity, i) {
|
|
3425
|
+
if (!affinity) return void 0;
|
|
3426
|
+
const a = affinityContext(i);
|
|
3427
|
+
return a ? { ...affinity, isHeldElsewhere: (t) => heldByOtherShip(t, a), isLiveHoldElsewhere: (t) => heldByLiveOtherShip(t, a) } : affinity;
|
|
3428
|
+
}
|
|
3429
|
+
function notHeldByOtherShip(slice, i) {
|
|
3430
|
+
if (!i.ships) return slice;
|
|
3431
|
+
const a = { myShipId: i.ships.myShipId, ships: i.ships.rows, contract: i.contract ?? DEFAULT_CONTRACT, now: i.ships.now };
|
|
3432
|
+
return slice.filter((t) => !heldByOtherShip(t, a));
|
|
3433
|
+
}
|
|
3347
3434
|
function buildingRoleHasWork(role, i) {
|
|
3348
3435
|
const me = i.seats[role];
|
|
3349
3436
|
if (!me) return { hasWork: false, reason: "this ship does not crew that role" };
|
|
3350
|
-
const mine = sliceFor(i.tickets, role);
|
|
3437
|
+
const mine = notHeldByOtherShip(sliceFor(i.tickets, role), i);
|
|
3351
3438
|
const startable = mine.filter(
|
|
3352
3439
|
(t) => (t.status === "accepted" || t.status === "blocked") && !i.blocked.has(t.id) && !isHeld(t, i.holds)
|
|
3353
3440
|
);
|
|
@@ -3397,7 +3484,7 @@ function roleCandidates(role, i) {
|
|
|
3397
3484
|
if (role === "pair") return [];
|
|
3398
3485
|
const me = i.seats[role];
|
|
3399
3486
|
if (!me) return [];
|
|
3400
|
-
return sliceFor(i.tickets, role).filter(
|
|
3487
|
+
return notHeldByOtherShip(sliceFor(i.tickets, role), i).filter(
|
|
3401
3488
|
(t) => ((t.status === "accepted" || t.status === "blocked") && !i.blocked.has(t.id) || t.status === "in_progress" && (t.assignee_id === me || !t.assignee_id)) && !isHeld(t, i.holds)
|
|
3402
3489
|
);
|
|
3403
3490
|
}
|
|
@@ -4220,6 +4307,7 @@ async function decideCycle(o) {
|
|
|
4220
4307
|
emit.warn(`ticket(s) blocking themselves, parked permanently: ${diag.selfBlocked.join(", ")}`);
|
|
4221
4308
|
}
|
|
4222
4309
|
const sweep = planSweep(tickets, info, blocked);
|
|
4310
|
+
const holdReleases = planHoldReleases(tickets, tracker.contract);
|
|
4223
4311
|
const blockedIssues = tickets.filter((t) => blocked.has(t.id)).map((t) => t.issue_id);
|
|
4224
4312
|
emit.emit(`${blockedIssues.length} ticket(s) parked as blocked`, { data: { blocked: blockedIssues } });
|
|
4225
4313
|
const qaHeld = tickets.filter((t) => t.status === tracker.contract.statuses.verifying).map((t) => t.issue_id);
|
|
@@ -4287,7 +4375,8 @@ async function decideCycle(o) {
|
|
|
4287
4375
|
holds: holds2,
|
|
4288
4376
|
seats: route.resolved.seats,
|
|
4289
4377
|
paused: state.pausedRoles(["dev", "design", "qa"]),
|
|
4290
|
-
contract: tracker.contract
|
|
4378
|
+
contract: tracker.contract,
|
|
4379
|
+
ships: { myShipId: shipIdByName(ships, ship.name), rows: ships }
|
|
4291
4380
|
};
|
|
4292
4381
|
const selection = selectRole(selectionInput);
|
|
4293
4382
|
const actionable = actionableSummary(selectionInput, selection.pending);
|
|
@@ -4300,6 +4389,7 @@ async function decideCycle(o) {
|
|
|
4300
4389
|
info,
|
|
4301
4390
|
sweep,
|
|
4302
4391
|
epicSync,
|
|
4392
|
+
holdReleases,
|
|
4303
4393
|
stranded,
|
|
4304
4394
|
selection,
|
|
4305
4395
|
actionable,
|
|
@@ -4400,22 +4490,65 @@ function rosterFor(d, route, role) {
|
|
|
4400
4490
|
|
|
4401
4491
|
// src/claim.ts
|
|
4402
4492
|
import { StaleWriteError as StaleWriteError4 } from "@tablation/client";
|
|
4403
|
-
async function
|
|
4493
|
+
async function stampResumption(tracker, t, affinity) {
|
|
4494
|
+
let current = t;
|
|
4495
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
4496
|
+
try {
|
|
4497
|
+
const stamped = await tracker.updateTicket(current.id, { [affinity.column]: affinity.shipId }, current.updated_at);
|
|
4498
|
+
return { outcome: "stamped", ticket: { ...current, ...stamped } };
|
|
4499
|
+
} catch (e) {
|
|
4500
|
+
if (!(e instanceof StaleWriteError4)) throw e;
|
|
4501
|
+
}
|
|
4502
|
+
if (!tracker.ticket) return { outcome: "unknown" };
|
|
4503
|
+
try {
|
|
4504
|
+
current = await tracker.ticket(t.id);
|
|
4505
|
+
} catch {
|
|
4506
|
+
return { outcome: "unknown" };
|
|
4507
|
+
}
|
|
4508
|
+
if ((affinity.isLiveHoldElsewhere ?? affinity.isHeldElsewhere)?.(current)) return { outcome: "held" };
|
|
4509
|
+
if (current[affinity.column] === affinity.shipId) return { outcome: "stamped", ticket: current };
|
|
4510
|
+
}
|
|
4511
|
+
return { outcome: "unknown" };
|
|
4512
|
+
}
|
|
4513
|
+
async function resolveTopCandidate(tracker, candidates, seat, approvedStatus, buildingStatus, isServable, affinity) {
|
|
4404
4514
|
const contended = [];
|
|
4405
4515
|
const unservable = [];
|
|
4516
|
+
const held = [];
|
|
4406
4517
|
for (const t of candidates) {
|
|
4518
|
+
if (affinity?.isHeldElsewhere?.(t)) {
|
|
4519
|
+
held.push(t.issue_id);
|
|
4520
|
+
continue;
|
|
4521
|
+
}
|
|
4407
4522
|
if (isServable && !isServable(t)) {
|
|
4408
4523
|
unservable.push(t.issue_id);
|
|
4409
4524
|
continue;
|
|
4410
4525
|
}
|
|
4411
|
-
if (t.status !== approvedStatus)
|
|
4526
|
+
if (t.status !== approvedStatus) {
|
|
4527
|
+
if (affinity?.isLiveHoldElsewhere?.(t)) {
|
|
4528
|
+
return { ticket: t, claimed: false, contended, unservable, held, reaccepted: false };
|
|
4529
|
+
}
|
|
4530
|
+
if (affinity && t[affinity.column] !== affinity.shipId) {
|
|
4531
|
+
const stamp = await stampResumption(tracker, t, affinity);
|
|
4532
|
+
if (stamp.outcome === "stamped") {
|
|
4533
|
+
return { ticket: stamp.ticket, claimed: false, contended, unservable, held, reaccepted: false };
|
|
4534
|
+
}
|
|
4535
|
+
(stamp.outcome === "held" ? held : contended).push(t.issue_id);
|
|
4536
|
+
continue;
|
|
4537
|
+
}
|
|
4538
|
+
return { ticket: t, claimed: false, contended, unservable, held, reaccepted: false };
|
|
4539
|
+
}
|
|
4412
4540
|
try {
|
|
4413
4541
|
const claimed = await tracker.updateTicket(
|
|
4414
4542
|
t.id,
|
|
4415
|
-
{
|
|
4543
|
+
{
|
|
4544
|
+
status: buildingStatus,
|
|
4545
|
+
assignee_id: seat,
|
|
4546
|
+
...affinity ? { [affinity.column]: affinity.shipId } : {}
|
|
4547
|
+
},
|
|
4416
4548
|
t.updated_at
|
|
4417
4549
|
);
|
|
4418
|
-
|
|
4550
|
+
const reaccepted = !!affinity && t[affinity.column] === affinity.shipId;
|
|
4551
|
+
return { ticket: { ...t, ...claimed }, claimed: true, contended, unservable, held, reaccepted };
|
|
4419
4552
|
} catch (e) {
|
|
4420
4553
|
if (e instanceof StaleWriteError4) {
|
|
4421
4554
|
contended.push(t.issue_id);
|
|
@@ -4424,7 +4557,7 @@ async function resolveTopCandidate(tracker, candidates, seat, approvedStatus, bu
|
|
|
4424
4557
|
throw e;
|
|
4425
4558
|
}
|
|
4426
4559
|
}
|
|
4427
|
-
return { ticket: null, claimed: false, contended, unservable };
|
|
4560
|
+
return { ticket: null, claimed: false, contended, unservable, held, reaccepted: false };
|
|
4428
4561
|
}
|
|
4429
4562
|
|
|
4430
4563
|
// src/conflict.ts
|
|
@@ -8976,6 +9109,10 @@ async function runOnePass(o) {
|
|
|
8976
9109
|
emit.warn(`could not sweep: ${e.message}`, { step: "sweep" });
|
|
8977
9110
|
}
|
|
8978
9111
|
}
|
|
9112
|
+
if (decision.holdReleases?.length) {
|
|
9113
|
+
const tracker = new Tracker(o.route, o.ship);
|
|
9114
|
+
await applyHoldReleases(tracker, decision.holdReleases, tracker.contract, emit);
|
|
9115
|
+
}
|
|
8979
9116
|
const order = [...decision.selection.pending].sort(
|
|
8980
9117
|
(a, b) => (decision.selection.ranks[a] ?? Number.POSITIVE_INFINITY) - (decision.selection.ranks[b] ?? Number.POSITIVE_INFINITY)
|
|
8981
9118
|
);
|
|
@@ -9052,7 +9189,8 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
9052
9189
|
seat,
|
|
9053
9190
|
contract.statuses.approved,
|
|
9054
9191
|
contract.statuses.building,
|
|
9055
|
-
(t) => dirForRepo(route, t.repo_id) !== null
|
|
9192
|
+
(t) => dirForRepo(route, t.repo_id) !== null,
|
|
9193
|
+
withHoldCheck(await tracker.claimAffinity(), decision.selectionInput)
|
|
9056
9194
|
);
|
|
9057
9195
|
if (result.contended.length) {
|
|
9058
9196
|
emit.emit(`claim contended for ${result.contended.join(", ")} \u2014 moved to the next candidate`, {
|
|
@@ -9066,6 +9204,10 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
9066
9204
|
role
|
|
9067
9205
|
});
|
|
9068
9206
|
}
|
|
9207
|
+
if (result.held.length) {
|
|
9208
|
+
emit.emit(`skipped ${result.held.join(", ")} \u2014 held by another live ship`, { step: "select", role });
|
|
9209
|
+
}
|
|
9210
|
+
await noteReaccepted(tracker, result, seat, ship.name);
|
|
9069
9211
|
if (!result.ticket) {
|
|
9070
9212
|
emit.emit(`${role} skipped this cycle \u2014 every candidate was already claimed elsewhere`);
|
|
9071
9213
|
return;
|
|
@@ -9160,12 +9302,12 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
9160
9302
|
// src/cli.ts
|
|
9161
9303
|
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname10(process.execPath) : resolve4(dirname10(fileURLToPath(import.meta.url)), "..");
|
|
9162
9304
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
9163
|
-
var
|
|
9305
|
+
var CYCLE_SECONDS2 = 120;
|
|
9164
9306
|
var STALE_CYCLES = 3;
|
|
9165
9307
|
function renderShipLine(s, issueLabelById) {
|
|
9166
9308
|
const name = s.name ?? "(unnamed)";
|
|
9167
9309
|
if (!s.last_seen) return `${name.padEnd(20)} never seen`;
|
|
9168
|
-
const staleAfterMs = STALE_CYCLES *
|
|
9310
|
+
const staleAfterMs = STALE_CYCLES * CYCLE_SECONDS2 * 1e3;
|
|
9169
9311
|
const ageMs = Date.now() - Date.parse(s.last_seen);
|
|
9170
9312
|
if (ageMs > staleAfterMs) {
|
|
9171
9313
|
return `${name.padEnd(20)} not seen recently \u2014 last seen ${since(s.last_seen)} ago`;
|
|
@@ -10012,6 +10154,11 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10012
10154
|
const tracker = new Tracker(e.route, cfg.ship);
|
|
10013
10155
|
await applyEpicSync(tracker, e.decision.epicSync, tracker.contract, remit);
|
|
10014
10156
|
}
|
|
10157
|
+
for (const e of fleet.entries) {
|
|
10158
|
+
if (dryRun || e.error || !e.decision?.holdReleases?.length) continue;
|
|
10159
|
+
const tracker = new Tracker(e.route, cfg.ship);
|
|
10160
|
+
await applyHoldReleases(tracker, e.decision.holdReleases, tracker.contract, emit.forRoute(e.route.route));
|
|
10161
|
+
}
|
|
10015
10162
|
for (const e of fleet.entries) {
|
|
10016
10163
|
if (e.error || !e.decision || !e.decision.sweep.length) continue;
|
|
10017
10164
|
const remit = emit.forRoute(e.route.route);
|
|
@@ -10103,7 +10250,8 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10103
10250
|
seat,
|
|
10104
10251
|
contract.statuses.approved,
|
|
10105
10252
|
contract.statuses.building,
|
|
10106
|
-
(t) => dirForRepo(w.route, t.repo_id) !== null
|
|
10253
|
+
(t) => dirForRepo(w.route, t.repo_id) !== null,
|
|
10254
|
+
withHoldCheck(await fleetTracker.claimAffinity(), w.decision.selectionInput)
|
|
10107
10255
|
);
|
|
10108
10256
|
if (result.contended.length) {
|
|
10109
10257
|
wemit.emit(`claim contended for ${result.contended.join(", ")} \u2014 moved to the next candidate`, {
|
|
@@ -10117,6 +10265,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10117
10265
|
{ step: "select", role: w.role }
|
|
10118
10266
|
);
|
|
10119
10267
|
}
|
|
10268
|
+
if (result.held.length) {
|
|
10269
|
+
wemit.emit(`skipped ${result.held.join(", ")} \u2014 held by another live ship`, { step: "select", role: w.role });
|
|
10270
|
+
}
|
|
10271
|
+
await noteReaccepted(fleetTracker, result, seat, cfg.ship.name);
|
|
10120
10272
|
if (!result.ticket) {
|
|
10121
10273
|
wemit.emit(`${w.role} skipped this cycle \u2014 every candidate was already claimed elsewhere`);
|
|
10122
10274
|
if (!skipInlineRelease) await releaseFleet();
|
|
@@ -10227,6 +10379,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10227
10379
|
await applyEpicSync(tracker, decision.epicSync, tracker.contract, emit);
|
|
10228
10380
|
}
|
|
10229
10381
|
}
|
|
10382
|
+
if (!dryRun && decision.holdReleases?.length) {
|
|
10383
|
+
const tracker = new Tracker(route, cfg.ship);
|
|
10384
|
+
await applyHoldReleases(tracker, decision.holdReleases, tracker.contract, emit);
|
|
10385
|
+
}
|
|
10230
10386
|
const role = value("role") ?? decision.selection.selected;
|
|
10231
10387
|
if (command === "poll") break;
|
|
10232
10388
|
if (!dryRun) {
|
|
@@ -10275,7 +10431,8 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10275
10431
|
seat,
|
|
10276
10432
|
contract.statuses.approved,
|
|
10277
10433
|
contract.statuses.building,
|
|
10278
|
-
(t) => dirForRepo(route, t.repo_id) !== null
|
|
10434
|
+
(t) => dirForRepo(route, t.repo_id) !== null,
|
|
10435
|
+
withHoldCheck(await tracker2.claimAffinity(), decision.selectionInput)
|
|
10279
10436
|
);
|
|
10280
10437
|
if (result.contended.length) {
|
|
10281
10438
|
emit.emit(`claim contended for ${result.contended.join(", ")} \u2014 moved to the next candidate`, {
|
|
@@ -10289,6 +10446,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10289
10446
|
{ step: "select", role: current }
|
|
10290
10447
|
);
|
|
10291
10448
|
}
|
|
10449
|
+
if (result.held.length) {
|
|
10450
|
+
emit.emit(`skipped ${result.held.join(", ")} \u2014 held by another live ship`, { step: "select", role: current });
|
|
10451
|
+
}
|
|
10452
|
+
await noteReaccepted(tracker2, result, seat, cfg.ship.name);
|
|
10292
10453
|
if (!result.ticket) {
|
|
10293
10454
|
emit.emit(`${current} skipped this cycle \u2014 every candidate was already claimed elsewhere`);
|
|
10294
10455
|
dropLock();
|
package/package.json
CHANGED
|
@@ -198,7 +198,7 @@ since your last comment: an answer to a question, new direction, or a
|
|
|
198
198
|
the remote has moved), stop: comment on the ticket with what it printed and
|
|
199
199
|
set `needs_info`; do not merge or rebase it yourself.
|
|
200
200
|
- A ticket QA has bounced back to you comes in as `in_progress`,
|
|
201
|
-
reassigned to your row, with a comment saying what still fails. Treat
|
|
201
|
+
reassigned to your row (or unassigned, when your ship holds it), with a comment saying what still fails. Treat
|
|
202
202
|
that exactly like new direction from a hold: read the comment, fix what
|
|
203
203
|
it names, and take it back to `fixed` when it's genuinely right. QA
|
|
204
204
|
bouncing a ticket is the system working, not an accusation.
|
|
@@ -532,6 +532,10 @@ if `report_type` is either of those.
|
|
|
532
532
|
part).
|
|
533
533
|
9. Set the ticket's `status` to `fixed` once you have verified it yourself,
|
|
534
534
|
and **clear `assignee_id`** — that pair is the hand-off to the QA lane.
|
|
535
|
+
If the Issues table has a `held_by_ship_id` column, clear it in the same
|
|
536
|
+
write (the hold is for the building roles; QA on any ship may take a `fixed`
|
|
537
|
+
ticket). A QA bounce or `needs_info` park leaves it set; QA clears it again
|
|
538
|
+
when it sets `verified`.
|
|
535
539
|
**Push the branch first, in the same step:**
|
|
536
540
|
`git push --set-upstream <remote> <branch>` from the worktree (the remote is
|
|
537
541
|
`origin` unless the repo's `.crew.yaml` says otherwise; a repo with no remote
|
|
@@ -114,7 +114,11 @@ human review, so "probably fine" is not a pass.
|
|
|
114
114
|
covered.
|
|
115
115
|
- **It doesn't** → `status` = `in_progress`, `assignee_id` = the building
|
|
116
116
|
lane's own Crew row id from the roster above — the dev seat if
|
|
117
|
-
`needs_design` is false or null, the design seat if true.
|
|
117
|
+
`needs_design` is false or null, the design seat if true. **Exception:** when the ticket carries a `held_by_ship_id`, leave that column
|
|
118
|
+
untouched and **clear** `assignee_id` instead of naming a seat — seats are
|
|
119
|
+
per ship, and the ship that holds the branch resumes an unassigned
|
|
120
|
+
`in_progress` ticket because it is the holder. Naming this ship's seat
|
|
121
|
+
would hand the bounce to a ship that never built it. The comment
|
|
118
122
|
must be actionable:
|
|
119
123
|
exactly what you did, what you expected, what happened, with a
|
|
120
124
|
screenshot or the failing output. This includes a red suite, a
|
|
@@ -179,7 +179,7 @@ since your last comment: an answer to a question, new direction, or a
|
|
|
179
179
|
the remote has moved), stop: comment on the ticket with what it printed and
|
|
180
180
|
set `needs_info`; do not merge or rebase it yourself.
|
|
181
181
|
- A ticket QA has bounced back to you comes in as `in_progress`,
|
|
182
|
-
reassigned to your row, with a comment saying what still fails. Treat
|
|
182
|
+
reassigned to your row (or unassigned, when your ship holds it), with a comment saying what still fails. Treat
|
|
183
183
|
that exactly like new direction from a hold: read the comment, fix what
|
|
184
184
|
it names, and take it back to `fixed` when it's genuinely right. QA
|
|
185
185
|
bouncing a ticket is the system working, not an accusation.
|
|
@@ -514,6 +514,10 @@ if `report_type` is either of those.
|
|
|
514
514
|
part).
|
|
515
515
|
9. Set the ticket's `status` to `fixed` once you have verified it yourself,
|
|
516
516
|
and **clear `assignee_id`** — that pair is the hand-off to the QA lane.
|
|
517
|
+
If the Issues table has a `held_by_ship_id` column, clear it in the same
|
|
518
|
+
write (the hold is for the building roles; QA on any ship may take a `fixed`
|
|
519
|
+
ticket). A QA bounce or `needs_info` park leaves it set; QA clears it again
|
|
520
|
+
when it sets `verified`.
|
|
517
521
|
**Push the branch first, in the same step:**
|
|
518
522
|
`git push --set-upstream <remote> <branch>` from the worktree (the remote is
|
|
519
523
|
`origin` unless the repo's `.crew.yaml` says otherwise; a repo with no remote
|
|
@@ -104,7 +104,11 @@ human review, so "probably fine" is not a pass.
|
|
|
104
104
|
the comment what you exercised, so the record shows what "verified"
|
|
105
105
|
covered.
|
|
106
106
|
- **It doesn't** → `status` = `in_progress`, `assignee_id` = the dev
|
|
107
|
-
seat's own Crew row id from the roster above.
|
|
107
|
+
seat's own Crew row id from the roster above. **Exception:** when the ticket carries a `held_by_ship_id`, leave that column
|
|
108
|
+
untouched and **clear** `assignee_id` instead of naming a seat — seats are
|
|
109
|
+
per ship, and the ship that holds the branch resumes an unassigned
|
|
110
|
+
`in_progress` ticket because it is the holder. Naming this ship's seat
|
|
111
|
+
would hand the bounce to a ship that never built it. The comment must be
|
|
108
112
|
actionable:
|
|
109
113
|
exactly what you did, what you expected, what happened, with a
|
|
110
114
|
screenshot or the failing output. This includes a red suite, a
|