@tablation/crew 0.10.2 → 0.12.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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { resolve as resolve4, dirname as
|
|
4
|
+
import { resolve as resolve4, dirname as dirname10, join as join19, basename as basename7 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { hostname as hostname2, homedir as homedir5 } from "os";
|
|
7
7
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
@@ -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,60 @@ 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
|
+
if (!a.myShipId) return false;
|
|
3367
|
+
const statuses = a.contract.statuses;
|
|
3368
|
+
if ([statuses.handoff, statuses.verifying].includes(t.status)) return false;
|
|
3369
|
+
const holder = ticketHolderShipId(t, a.contract);
|
|
3370
|
+
if (!holder || holder === a.myShipId) return false;
|
|
3371
|
+
const owner = a.ships.find((s) => s.id === holder);
|
|
3372
|
+
return shipIsAlive(owner, a.now ?? Date.now());
|
|
3373
|
+
}
|
|
3374
|
+
function shipIdByName(ships, name) {
|
|
3375
|
+
const mine = ships.filter((r) => (r.name ?? "").trim() === name.trim());
|
|
3376
|
+
return mine.length === 1 ? mine[0].id : null;
|
|
3377
|
+
}
|
|
3378
|
+
async function noteReaccepted(tracker, result, seat, shipLabel) {
|
|
3379
|
+
if (!result.reaccepted || !result.ticket) return;
|
|
3380
|
+
try {
|
|
3381
|
+
await tracker.postEvent(
|
|
3382
|
+
result.ticket.id,
|
|
3383
|
+
`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\`.`,
|
|
3384
|
+
seat
|
|
3385
|
+
);
|
|
3386
|
+
} catch {
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
function planHoldReleases(tickets, contract) {
|
|
3390
|
+
const ends = [contract.statuses.handoff, contract.statuses.verified];
|
|
3391
|
+
return tickets.filter((t) => ends.includes(t.status) && ticketHolderShipId(t, contract) !== null);
|
|
3392
|
+
}
|
|
3393
|
+
async function applyHoldReleases(writer, tickets, contract, emit) {
|
|
3394
|
+
let cleared = 0;
|
|
3395
|
+
for (const t of tickets) {
|
|
3396
|
+
try {
|
|
3397
|
+
await writer.updateTicket(t.id, { [contract.columns.heldBy]: null }, t.updated_at);
|
|
3398
|
+
cleared++;
|
|
3399
|
+
emit.emit(`released ship hold (${t.status})`, { ticket: t.issue_id, step: "sweep" });
|
|
3400
|
+
} catch (e) {
|
|
3401
|
+
emit.warn(`could not release ship hold: ${e.message}`, { ticket: t.issue_id, step: "sweep" });
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
return cleared;
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3336
3407
|
// src/select.ts
|
|
3337
3408
|
var qaSlice = (tickets) => tickets.filter((t) => t.status === "fixed" || t.status === "qa");
|
|
3338
3409
|
var buildingSlice = (tickets, role) => tickets.filter((t) => role === "design" ? t.needs_design === true : t.needs_design !== true);
|
|
@@ -3344,10 +3415,23 @@ function sliceFor(tickets, role) {
|
|
|
3344
3415
|
return buildingSlice(tickets, role);
|
|
3345
3416
|
}
|
|
3346
3417
|
var isHeld = (t, holds2) => !!t.assignee_id && holds2.has(t.assignee_id);
|
|
3418
|
+
function affinityContext(i) {
|
|
3419
|
+
return i.ships ? { myShipId: i.ships.myShipId, ships: i.ships.rows, contract: i.contract ?? DEFAULT_CONTRACT, now: i.ships.now } : null;
|
|
3420
|
+
}
|
|
3421
|
+
function withHoldCheck(affinity, i) {
|
|
3422
|
+
if (!affinity) return void 0;
|
|
3423
|
+
const a = affinityContext(i);
|
|
3424
|
+
return a ? { ...affinity, isHeldElsewhere: (t) => heldByOtherShip(t, a) } : affinity;
|
|
3425
|
+
}
|
|
3426
|
+
function notHeldByOtherShip(slice, i) {
|
|
3427
|
+
if (!i.ships) return slice;
|
|
3428
|
+
const a = { myShipId: i.ships.myShipId, ships: i.ships.rows, contract: i.contract ?? DEFAULT_CONTRACT, now: i.ships.now };
|
|
3429
|
+
return slice.filter((t) => !heldByOtherShip(t, a));
|
|
3430
|
+
}
|
|
3347
3431
|
function buildingRoleHasWork(role, i) {
|
|
3348
3432
|
const me = i.seats[role];
|
|
3349
3433
|
if (!me) return { hasWork: false, reason: "this ship does not crew that role" };
|
|
3350
|
-
const mine = sliceFor(i.tickets, role);
|
|
3434
|
+
const mine = notHeldByOtherShip(sliceFor(i.tickets, role), i);
|
|
3351
3435
|
const startable = mine.filter(
|
|
3352
3436
|
(t) => (t.status === "accepted" || t.status === "blocked") && !i.blocked.has(t.id) && !isHeld(t, i.holds)
|
|
3353
3437
|
);
|
|
@@ -3397,7 +3481,7 @@ function roleCandidates(role, i) {
|
|
|
3397
3481
|
if (role === "pair") return [];
|
|
3398
3482
|
const me = i.seats[role];
|
|
3399
3483
|
if (!me) return [];
|
|
3400
|
-
return sliceFor(i.tickets, role).filter(
|
|
3484
|
+
return notHeldByOtherShip(sliceFor(i.tickets, role), i).filter(
|
|
3401
3485
|
(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
3486
|
);
|
|
3403
3487
|
}
|
|
@@ -3991,16 +4075,20 @@ function refreshBaseBranch(cwd, remote, base, dryRun = false) {
|
|
|
3991
4075
|
const [behindStr, aheadStr] = counts.split(/\s+/);
|
|
3992
4076
|
const behind = Number.parseInt(behindStr ?? "0", 10);
|
|
3993
4077
|
const ahead = Number.parseInt(aheadStr ?? "0", 10);
|
|
4078
|
+
const named = () => {
|
|
4079
|
+
const subjects = (gitOk(cwd, ["log", "--format=%h %s", "-5", `${upstream}..${base}`]) ?? "").split("\n").filter(Boolean);
|
|
4080
|
+
return subjects.length ? `: ${subjects.join("; ")}${ahead > subjects.length ? "; \u2026" : ""}` : "";
|
|
4081
|
+
};
|
|
3994
4082
|
if (ahead > 0 && behind > 0) {
|
|
3995
4083
|
return {
|
|
3996
4084
|
action: "diverged",
|
|
3997
4085
|
ahead,
|
|
3998
4086
|
behind,
|
|
3999
|
-
detail: `${base} has diverged from ${upstream} (${ahead} ahead, ${behind} behind)`
|
|
4087
|
+
detail: `${base} has diverged from ${upstream} (${ahead} ahead, ${behind} behind)${named()}`
|
|
4000
4088
|
};
|
|
4001
4089
|
}
|
|
4002
4090
|
if (behind === 0 && ahead > 0) {
|
|
4003
|
-
return { action: "ahead", ahead, detail: `${base} has ${ahead} commit(s) ${upstream} does not` };
|
|
4091
|
+
return { action: "ahead", ahead, detail: `${base} has ${ahead} commit(s) ${upstream} does not${named()}` };
|
|
4004
4092
|
}
|
|
4005
4093
|
if (behind === 0) return { action: "level", detail: `${base} is level with ${upstream}` };
|
|
4006
4094
|
if (dryRun) {
|
|
@@ -4015,8 +4103,22 @@ function refreshBaseBranch(cwd, remote, base, dryRun = false) {
|
|
|
4015
4103
|
detail: `fast-forwarded ${base} to ${upstream} (${behind} commit(s) from elsewhere)`
|
|
4016
4104
|
};
|
|
4017
4105
|
}
|
|
4018
|
-
function
|
|
4019
|
-
|
|
4106
|
+
function recoverStrandedReleaseCheckouts(cwd) {
|
|
4107
|
+
gitOk(cwd, ["worktree", "prune"]);
|
|
4108
|
+
const out = gitOk(cwd, ["worktree", "list", "--porcelain"]) ?? "";
|
|
4109
|
+
let path = "";
|
|
4110
|
+
for (const line of out.split("\n")) {
|
|
4111
|
+
if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
|
|
4112
|
+
else if (line.startsWith("branch refs/heads/crew/release-") && path) {
|
|
4113
|
+
gitOk(cwd, ["worktree", "remove", "--force", path]);
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
const stranded = (gitOk(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads/crew/release-*"]) ?? "").split("\n").filter(Boolean);
|
|
4117
|
+
for (const b of stranded) gitOk(cwd, ["branch", "-q", "-D", b]);
|
|
4118
|
+
return stranded;
|
|
4119
|
+
}
|
|
4120
|
+
function pushBranch(cwd, remote, branch, dst = branch) {
|
|
4121
|
+
git(cwd, ["push", remote, `${branch}:${dst}`]);
|
|
4020
4122
|
}
|
|
4021
4123
|
function worktrees(cwd) {
|
|
4022
4124
|
const out = gitOk(cwd, ["worktree", "list", "--porcelain"]) ?? "";
|
|
@@ -4202,6 +4304,7 @@ async function decideCycle(o) {
|
|
|
4202
4304
|
emit.warn(`ticket(s) blocking themselves, parked permanently: ${diag.selfBlocked.join(", ")}`);
|
|
4203
4305
|
}
|
|
4204
4306
|
const sweep = planSweep(tickets, info, blocked);
|
|
4307
|
+
const holdReleases = planHoldReleases(tickets, tracker.contract);
|
|
4205
4308
|
const blockedIssues = tickets.filter((t) => blocked.has(t.id)).map((t) => t.issue_id);
|
|
4206
4309
|
emit.emit(`${blockedIssues.length} ticket(s) parked as blocked`, { data: { blocked: blockedIssues } });
|
|
4207
4310
|
const qaHeld = tickets.filter((t) => t.status === tracker.contract.statuses.verifying).map((t) => t.issue_id);
|
|
@@ -4269,7 +4372,8 @@ async function decideCycle(o) {
|
|
|
4269
4372
|
holds: holds2,
|
|
4270
4373
|
seats: route.resolved.seats,
|
|
4271
4374
|
paused: state.pausedRoles(["dev", "design", "qa"]),
|
|
4272
|
-
contract: tracker.contract
|
|
4375
|
+
contract: tracker.contract,
|
|
4376
|
+
ships: { myShipId: shipIdByName(ships, ship.name), rows: ships }
|
|
4273
4377
|
};
|
|
4274
4378
|
const selection = selectRole(selectionInput);
|
|
4275
4379
|
const actionable = actionableSummary(selectionInput, selection.pending);
|
|
@@ -4282,6 +4386,7 @@ async function decideCycle(o) {
|
|
|
4282
4386
|
info,
|
|
4283
4387
|
sweep,
|
|
4284
4388
|
epicSync,
|
|
4389
|
+
holdReleases,
|
|
4285
4390
|
stranded,
|
|
4286
4391
|
selection,
|
|
4287
4392
|
actionable,
|
|
@@ -4382,22 +4487,62 @@ function rosterFor(d, route, role) {
|
|
|
4382
4487
|
|
|
4383
4488
|
// src/claim.ts
|
|
4384
4489
|
import { StaleWriteError as StaleWriteError4 } from "@tablation/client";
|
|
4385
|
-
async function
|
|
4490
|
+
async function stampResumption(tracker, t, affinity) {
|
|
4491
|
+
let current = t;
|
|
4492
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
4493
|
+
try {
|
|
4494
|
+
const stamped = await tracker.updateTicket(current.id, { [affinity.column]: affinity.shipId }, current.updated_at);
|
|
4495
|
+
return { outcome: "stamped", ticket: { ...current, ...stamped } };
|
|
4496
|
+
} catch (e) {
|
|
4497
|
+
if (!(e instanceof StaleWriteError4)) throw e;
|
|
4498
|
+
}
|
|
4499
|
+
if (!tracker.ticket) return { outcome: "unknown" };
|
|
4500
|
+
try {
|
|
4501
|
+
current = await tracker.ticket(t.id);
|
|
4502
|
+
} catch {
|
|
4503
|
+
return { outcome: "unknown" };
|
|
4504
|
+
}
|
|
4505
|
+
if (affinity.isHeldElsewhere?.(current)) return { outcome: "held" };
|
|
4506
|
+
if (current[affinity.column] === affinity.shipId) return { outcome: "stamped", ticket: current };
|
|
4507
|
+
}
|
|
4508
|
+
return { outcome: "unknown" };
|
|
4509
|
+
}
|
|
4510
|
+
async function resolveTopCandidate(tracker, candidates, seat, approvedStatus, buildingStatus, isServable, affinity) {
|
|
4386
4511
|
const contended = [];
|
|
4387
4512
|
const unservable = [];
|
|
4513
|
+
const held = [];
|
|
4388
4514
|
for (const t of candidates) {
|
|
4515
|
+
if (affinity?.isHeldElsewhere?.(t)) {
|
|
4516
|
+
held.push(t.issue_id);
|
|
4517
|
+
continue;
|
|
4518
|
+
}
|
|
4389
4519
|
if (isServable && !isServable(t)) {
|
|
4390
4520
|
unservable.push(t.issue_id);
|
|
4391
4521
|
continue;
|
|
4392
4522
|
}
|
|
4393
|
-
if (t.status !== approvedStatus)
|
|
4523
|
+
if (t.status !== approvedStatus) {
|
|
4524
|
+
if (affinity && t[affinity.column] !== affinity.shipId) {
|
|
4525
|
+
const stamp = await stampResumption(tracker, t, affinity);
|
|
4526
|
+
if (stamp.outcome === "stamped") {
|
|
4527
|
+
return { ticket: stamp.ticket, claimed: false, contended, unservable, held, reaccepted: false };
|
|
4528
|
+
}
|
|
4529
|
+
(stamp.outcome === "held" ? held : contended).push(t.issue_id);
|
|
4530
|
+
continue;
|
|
4531
|
+
}
|
|
4532
|
+
return { ticket: t, claimed: false, contended, unservable, held, reaccepted: false };
|
|
4533
|
+
}
|
|
4394
4534
|
try {
|
|
4395
4535
|
const claimed = await tracker.updateTicket(
|
|
4396
4536
|
t.id,
|
|
4397
|
-
{
|
|
4537
|
+
{
|
|
4538
|
+
status: buildingStatus,
|
|
4539
|
+
assignee_id: seat,
|
|
4540
|
+
...affinity ? { [affinity.column]: affinity.shipId } : {}
|
|
4541
|
+
},
|
|
4398
4542
|
t.updated_at
|
|
4399
4543
|
);
|
|
4400
|
-
|
|
4544
|
+
const reaccepted = !!affinity && t[affinity.column] === affinity.shipId;
|
|
4545
|
+
return { ticket: { ...t, ...claimed }, claimed: true, contended, unservable, held, reaccepted };
|
|
4401
4546
|
} catch (e) {
|
|
4402
4547
|
if (e instanceof StaleWriteError4) {
|
|
4403
4548
|
contended.push(t.issue_id);
|
|
@@ -4406,7 +4551,7 @@ async function resolveTopCandidate(tracker, candidates, seat, approvedStatus, bu
|
|
|
4406
4551
|
throw e;
|
|
4407
4552
|
}
|
|
4408
4553
|
}
|
|
4409
|
-
return { ticket: null, claimed: false, contended, unservable };
|
|
4554
|
+
return { ticket: null, claimed: false, contended, unservable, held, reaccepted: false };
|
|
4410
4555
|
}
|
|
4411
4556
|
|
|
4412
4557
|
// src/conflict.ts
|
|
@@ -5453,8 +5598,9 @@ Wrote ${o.configPath}.
|
|
|
5453
5598
|
}
|
|
5454
5599
|
|
|
5455
5600
|
// src/release-run.ts
|
|
5456
|
-
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync9 } from "fs";
|
|
5457
|
-
import { join as join9 } from "path";
|
|
5601
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync9, copyFileSync, mkdirSync as mkdirSync7, mkdtempSync, rmSync as rmSync2 } from "fs";
|
|
5602
|
+
import { dirname as dirname7, join as join9 } from "path";
|
|
5603
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
5458
5604
|
|
|
5459
5605
|
// src/release.ts
|
|
5460
5606
|
function describeUnplaceable(unplaceable, verifiedStatus) {
|
|
@@ -6029,6 +6175,31 @@ async function confirmLastRelease(o, decision) {
|
|
|
6029
6175
|
if (!confirmed) o.emit.warn(`${decision.lastTag} is still not confirmed live \u2014 tickets stay verified`);
|
|
6030
6176
|
return { confirmed, version };
|
|
6031
6177
|
}
|
|
6178
|
+
function openReleaseCheckout(o) {
|
|
6179
|
+
const { remote, base } = o.repo.branch;
|
|
6180
|
+
const branch = `crew/release-${Date.now()}`;
|
|
6181
|
+
const root = mkdtempSync(join9(tmpdir2(), "crew-release-"));
|
|
6182
|
+
const dir = join9(root, "checkout");
|
|
6183
|
+
const close = () => {
|
|
6184
|
+
gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
|
|
6185
|
+
rmSync2(root, { recursive: true, force: true });
|
|
6186
|
+
gitOk(o.cwd, ["worktree", "prune"]);
|
|
6187
|
+
gitOk(o.cwd, ["branch", "-D", branch]);
|
|
6188
|
+
};
|
|
6189
|
+
try {
|
|
6190
|
+
git(o.cwd, ["worktree", "add", "-q", "-b", branch, dir, `${remote}/${base}`]);
|
|
6191
|
+
} catch (e) {
|
|
6192
|
+
close();
|
|
6193
|
+
throw e;
|
|
6194
|
+
}
|
|
6195
|
+
for (const f of o.repo.worktrees.copy) {
|
|
6196
|
+
const src = join9(o.cwd, f);
|
|
6197
|
+
if (!existsSync9(src)) continue;
|
|
6198
|
+
mkdirSync7(dirname7(join9(dir, f)), { recursive: true });
|
|
6199
|
+
copyFileSync(src, join9(dir, f));
|
|
6200
|
+
}
|
|
6201
|
+
return { dir, branch, close };
|
|
6202
|
+
}
|
|
6032
6203
|
async function runRelease(o) {
|
|
6033
6204
|
const scoped = (msg) => o.scope ? `${o.scope}: ${msg}` : msg;
|
|
6034
6205
|
const tagPattern = o.repo.release.tagPattern ?? "v*";
|
|
@@ -6075,6 +6246,33 @@ async function runRelease(o) {
|
|
|
6075
6246
|
const externalClosures = await confirmExternalReleased(o, landed);
|
|
6076
6247
|
return { merged: [], deployed: false, stopped: "external", decision, externalClosures };
|
|
6077
6248
|
}
|
|
6249
|
+
const { remote, base } = o.repo.branch;
|
|
6250
|
+
if (o.dryRun || o.mergeOnly || !remoteBranchExists(o.cwd, remote, base)) {
|
|
6251
|
+
return mergeAndRelease(o, decision, fresh);
|
|
6252
|
+
}
|
|
6253
|
+
let checkout;
|
|
6254
|
+
const stranded = recoverStrandedReleaseCheckouts(o.cwd);
|
|
6255
|
+
if (stranded.length > 0) o.emit.warn(scoped(`dropped release checkout(s) stranded by a crash: ${stranded.join(", ")}`));
|
|
6256
|
+
try {
|
|
6257
|
+
checkout = openReleaseCheckout(o);
|
|
6258
|
+
} catch (e) {
|
|
6259
|
+
o.emit.error(scoped(`could not cut a release checkout from ${remote}/${base} \u2014 ${e.message}`));
|
|
6260
|
+
return { merged: [], deployed: false, stopped: "release checkout failed", decision };
|
|
6261
|
+
}
|
|
6262
|
+
try {
|
|
6263
|
+
return await mergeAndRelease({ ...o, cwd: checkout.dir }, decision, fresh, checkout.branch);
|
|
6264
|
+
} finally {
|
|
6265
|
+
checkout.close();
|
|
6266
|
+
const refreshed = refreshBaseBranch(o.cwd, remote, base);
|
|
6267
|
+
if (refreshed.action === "fast-forwarded" && o.repo.hooks.build) {
|
|
6268
|
+
const r = await hook(o, "build");
|
|
6269
|
+
if (r && r.code !== 0) {
|
|
6270
|
+
o.emit.warn(scoped(`rebuilding the primary checkout after the release failed (exit ${r.code}) \u2014 its dist/ is stale until \`${o.repo.hooks.build}\` is run there`));
|
|
6271
|
+
}
|
|
6272
|
+
}
|
|
6273
|
+
}
|
|
6274
|
+
}
|
|
6275
|
+
async function mergeAndRelease(o, decision, fresh, releaseBranch) {
|
|
6078
6276
|
o.emit.enter("merge");
|
|
6079
6277
|
const merged = [];
|
|
6080
6278
|
const conflicts = [];
|
|
@@ -6236,6 +6434,16 @@ async function runRelease(o) {
|
|
|
6236
6434
|
}
|
|
6237
6435
|
}
|
|
6238
6436
|
}
|
|
6437
|
+
if (releaseBranch && !o.dryRun) {
|
|
6438
|
+
const { remote, base } = o.repo.branch;
|
|
6439
|
+
if (fetchRemote(o.cwd, remote) && remoteBranchExists(o.cwd, remote, base)) {
|
|
6440
|
+
const moved = resolve2(o.cwd, `${remote}/${base}`);
|
|
6441
|
+
if (moved && !isAncestor(o.cwd, moved, "HEAD")) {
|
|
6442
|
+
o.emit.warn(`${remote}/${base} moved during the release \u2014 discarding it; the tickets stay verified and the next cycle retries`);
|
|
6443
|
+
return { merged, conflicts, unbuildable, version, deployed: false, decision, stopped: "base branch moved during release" };
|
|
6444
|
+
}
|
|
6445
|
+
}
|
|
6446
|
+
}
|
|
6239
6447
|
let deployed = false;
|
|
6240
6448
|
let integrated = false;
|
|
6241
6449
|
if (o.repo.release.mode === "integrate") {
|
|
@@ -6265,7 +6473,7 @@ async function runRelease(o) {
|
|
|
6265
6473
|
o.emit.emit(`would push ${o.repo.branch.base} to ${o.repo.branch.remote}`);
|
|
6266
6474
|
} else {
|
|
6267
6475
|
try {
|
|
6268
|
-
pushBranch(o.cwd, o.repo.branch.remote, o.repo.branch.base);
|
|
6476
|
+
pushBranch(o.cwd, o.repo.branch.remote, releaseBranch ?? o.repo.branch.base, o.repo.branch.base);
|
|
6269
6477
|
o.emit.emit(`pushed ${o.repo.branch.base} to ${o.repo.branch.remote}`);
|
|
6270
6478
|
} catch (e) {
|
|
6271
6479
|
o.emit.error(
|
|
@@ -8020,7 +8228,7 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
8020
8228
|
}
|
|
8021
8229
|
|
|
8022
8230
|
// src/stream-sweep.ts
|
|
8023
|
-
import { existsSync as existsSync15, readdirSync as readdirSync3, rmSync as
|
|
8231
|
+
import { existsSync as existsSync15, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync6 } from "fs";
|
|
8024
8232
|
import { join as join14 } from "path";
|
|
8025
8233
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
8026
8234
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
@@ -8043,7 +8251,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
8043
8251
|
continue;
|
|
8044
8252
|
}
|
|
8045
8253
|
try {
|
|
8046
|
-
|
|
8254
|
+
rmSync3(path, { force: true });
|
|
8047
8255
|
removed++;
|
|
8048
8256
|
log.emit(`removed stream artifact ${path}`, { step: "worktree" });
|
|
8049
8257
|
} catch (e) {
|
|
@@ -8054,14 +8262,14 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
8054
8262
|
}
|
|
8055
8263
|
|
|
8056
8264
|
// src/cli.ts
|
|
8057
|
-
import { readFileSync as readFileSync14, existsSync as existsSync19, mkdirSync as
|
|
8265
|
+
import { readFileSync as readFileSync14, existsSync as existsSync19, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync } from "fs";
|
|
8058
8266
|
import { createInterface } from "readline/promises";
|
|
8059
8267
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
8060
8268
|
|
|
8061
8269
|
// src/ssh-keys.ts
|
|
8062
8270
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
8063
|
-
import { existsSync as existsSync16, mkdirSync as
|
|
8064
|
-
import { dirname as
|
|
8271
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync11, chmodSync } from "fs";
|
|
8272
|
+
import { dirname as dirname9, join as join15 } from "path";
|
|
8065
8273
|
var SshKeyError = class extends Error {
|
|
8066
8274
|
};
|
|
8067
8275
|
function shipSshKeyPathFor(stateDir) {
|
|
@@ -8071,7 +8279,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
8071
8279
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
8072
8280
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
8073
8281
|
if (!existsSync16(privateKeyPath)) {
|
|
8074
|
-
|
|
8282
|
+
mkdirSync8(dirname9(privateKeyPath), { recursive: true });
|
|
8075
8283
|
try {
|
|
8076
8284
|
execFileSync3("ssh-keygen", [
|
|
8077
8285
|
"-t",
|
|
@@ -8308,7 +8516,7 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
8308
8516
|
|
|
8309
8517
|
// src/tunnel.ts
|
|
8310
8518
|
import { spawn as spawn3 } from "child_process";
|
|
8311
|
-
import { existsSync as existsSync17, mkdirSync as
|
|
8519
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, readdirSync as readdirSync4 } from "fs";
|
|
8312
8520
|
import { randomInt } from "crypto";
|
|
8313
8521
|
import { join as join16 } from "path";
|
|
8314
8522
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
@@ -8390,7 +8598,7 @@ function readPersistedTunnel(stateDir, workspaceId) {
|
|
|
8390
8598
|
}
|
|
8391
8599
|
function writePersistedTunnel(stateDir, state) {
|
|
8392
8600
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
8393
|
-
|
|
8601
|
+
mkdirSync9(join16(stateDir, "passengers"), { recursive: true });
|
|
8394
8602
|
writeFileSync7(path, `${JSON.stringify(state, null, 2)}
|
|
8395
8603
|
`);
|
|
8396
8604
|
}
|
|
@@ -8610,7 +8818,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
8610
8818
|
import { createServer as createHttpServer } from "http";
|
|
8611
8819
|
import { spawn as spawn4 } from "child_process";
|
|
8612
8820
|
import { randomBytes } from "crypto";
|
|
8613
|
-
import { existsSync as existsSync18, mkdirSync as
|
|
8821
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync8, unlinkSync as unlinkSync4, readdirSync as readdirSync5 } from "fs";
|
|
8614
8822
|
import { join as join17 } from "path";
|
|
8615
8823
|
|
|
8616
8824
|
// src/secret-compare.ts
|
|
@@ -8713,7 +8921,7 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
|
8713
8921
|
}
|
|
8714
8922
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
8715
8923
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
8716
|
-
|
|
8924
|
+
mkdirSync10(join17(stateDir, "passengers"), { recursive: true });
|
|
8717
8925
|
writeFileSync8(path, `${JSON.stringify(state, null, 2)}
|
|
8718
8926
|
`);
|
|
8719
8927
|
}
|
|
@@ -8895,6 +9103,10 @@ async function runOnePass(o) {
|
|
|
8895
9103
|
emit.warn(`could not sweep: ${e.message}`, { step: "sweep" });
|
|
8896
9104
|
}
|
|
8897
9105
|
}
|
|
9106
|
+
if (decision.holdReleases?.length) {
|
|
9107
|
+
const tracker = new Tracker(o.route, o.ship);
|
|
9108
|
+
await applyHoldReleases(tracker, decision.holdReleases, tracker.contract, emit);
|
|
9109
|
+
}
|
|
8898
9110
|
const order = [...decision.selection.pending].sort(
|
|
8899
9111
|
(a, b) => (decision.selection.ranks[a] ?? Number.POSITIVE_INFINITY) - (decision.selection.ranks[b] ?? Number.POSITIVE_INFINITY)
|
|
8900
9112
|
);
|
|
@@ -8971,7 +9183,8 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
8971
9183
|
seat,
|
|
8972
9184
|
contract.statuses.approved,
|
|
8973
9185
|
contract.statuses.building,
|
|
8974
|
-
(t) => dirForRepo(route, t.repo_id) !== null
|
|
9186
|
+
(t) => dirForRepo(route, t.repo_id) !== null,
|
|
9187
|
+
withHoldCheck(await tracker.claimAffinity(), decision.selectionInput)
|
|
8975
9188
|
);
|
|
8976
9189
|
if (result.contended.length) {
|
|
8977
9190
|
emit.emit(`claim contended for ${result.contended.join(", ")} \u2014 moved to the next candidate`, {
|
|
@@ -8985,6 +9198,10 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
8985
9198
|
role
|
|
8986
9199
|
});
|
|
8987
9200
|
}
|
|
9201
|
+
if (result.held.length) {
|
|
9202
|
+
emit.emit(`skipped ${result.held.join(", ")} \u2014 held by another live ship`, { step: "select", role });
|
|
9203
|
+
}
|
|
9204
|
+
await noteReaccepted(tracker, result, seat, ship.name);
|
|
8988
9205
|
if (!result.ticket) {
|
|
8989
9206
|
emit.emit(`${role} skipped this cycle \u2014 every candidate was already claimed elsewhere`);
|
|
8990
9207
|
return;
|
|
@@ -9077,14 +9294,14 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
9077
9294
|
}
|
|
9078
9295
|
|
|
9079
9296
|
// src/cli.ts
|
|
9080
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
9297
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname10(process.execPath) : resolve4(dirname10(fileURLToPath(import.meta.url)), "..");
|
|
9081
9298
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
9082
|
-
var
|
|
9299
|
+
var CYCLE_SECONDS2 = 120;
|
|
9083
9300
|
var STALE_CYCLES = 3;
|
|
9084
9301
|
function renderShipLine(s, issueLabelById) {
|
|
9085
9302
|
const name = s.name ?? "(unnamed)";
|
|
9086
9303
|
if (!s.last_seen) return `${name.padEnd(20)} never seen`;
|
|
9087
|
-
const staleAfterMs = STALE_CYCLES *
|
|
9304
|
+
const staleAfterMs = STALE_CYCLES * CYCLE_SECONDS2 * 1e3;
|
|
9088
9305
|
const ageMs = Date.now() - Date.parse(s.last_seen);
|
|
9089
9306
|
if (ageMs > staleAfterMs) {
|
|
9090
9307
|
return `${name.padEnd(20)} not seen recently \u2014 last seen ${since(s.last_seen)} ago`;
|
|
@@ -9931,6 +10148,11 @@ ${changed2.map((c) => ` - ${c}
|
|
|
9931
10148
|
const tracker = new Tracker(e.route, cfg.ship);
|
|
9932
10149
|
await applyEpicSync(tracker, e.decision.epicSync, tracker.contract, remit);
|
|
9933
10150
|
}
|
|
10151
|
+
for (const e of fleet.entries) {
|
|
10152
|
+
if (dryRun || e.error || !e.decision?.holdReleases?.length) continue;
|
|
10153
|
+
const tracker = new Tracker(e.route, cfg.ship);
|
|
10154
|
+
await applyHoldReleases(tracker, e.decision.holdReleases, tracker.contract, emit.forRoute(e.route.route));
|
|
10155
|
+
}
|
|
9934
10156
|
for (const e of fleet.entries) {
|
|
9935
10157
|
if (e.error || !e.decision || !e.decision.sweep.length) continue;
|
|
9936
10158
|
const remit = emit.forRoute(e.route.route);
|
|
@@ -10022,7 +10244,8 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10022
10244
|
seat,
|
|
10023
10245
|
contract.statuses.approved,
|
|
10024
10246
|
contract.statuses.building,
|
|
10025
|
-
(t) => dirForRepo(w.route, t.repo_id) !== null
|
|
10247
|
+
(t) => dirForRepo(w.route, t.repo_id) !== null,
|
|
10248
|
+
withHoldCheck(await fleetTracker.claimAffinity(), w.decision.selectionInput)
|
|
10026
10249
|
);
|
|
10027
10250
|
if (result.contended.length) {
|
|
10028
10251
|
wemit.emit(`claim contended for ${result.contended.join(", ")} \u2014 moved to the next candidate`, {
|
|
@@ -10036,6 +10259,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10036
10259
|
{ step: "select", role: w.role }
|
|
10037
10260
|
);
|
|
10038
10261
|
}
|
|
10262
|
+
if (result.held.length) {
|
|
10263
|
+
wemit.emit(`skipped ${result.held.join(", ")} \u2014 held by another live ship`, { step: "select", role: w.role });
|
|
10264
|
+
}
|
|
10265
|
+
await noteReaccepted(fleetTracker, result, seat, cfg.ship.name);
|
|
10039
10266
|
if (!result.ticket) {
|
|
10040
10267
|
wemit.emit(`${w.role} skipped this cycle \u2014 every candidate was already claimed elsewhere`);
|
|
10041
10268
|
if (!skipInlineRelease) await releaseFleet();
|
|
@@ -10146,6 +10373,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10146
10373
|
await applyEpicSync(tracker, decision.epicSync, tracker.contract, emit);
|
|
10147
10374
|
}
|
|
10148
10375
|
}
|
|
10376
|
+
if (!dryRun && decision.holdReleases?.length) {
|
|
10377
|
+
const tracker = new Tracker(route, cfg.ship);
|
|
10378
|
+
await applyHoldReleases(tracker, decision.holdReleases, tracker.contract, emit);
|
|
10379
|
+
}
|
|
10149
10380
|
const role = value("role") ?? decision.selection.selected;
|
|
10150
10381
|
if (command === "poll") break;
|
|
10151
10382
|
if (!dryRun) {
|
|
@@ -10194,7 +10425,8 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10194
10425
|
seat,
|
|
10195
10426
|
contract.statuses.approved,
|
|
10196
10427
|
contract.statuses.building,
|
|
10197
|
-
(t) => dirForRepo(route, t.repo_id) !== null
|
|
10428
|
+
(t) => dirForRepo(route, t.repo_id) !== null,
|
|
10429
|
+
withHoldCheck(await tracker2.claimAffinity(), decision.selectionInput)
|
|
10198
10430
|
);
|
|
10199
10431
|
if (result.contended.length) {
|
|
10200
10432
|
emit.emit(`claim contended for ${result.contended.join(", ")} \u2014 moved to the next candidate`, {
|
|
@@ -10208,6 +10440,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10208
10440
|
{ step: "select", role: current }
|
|
10209
10441
|
);
|
|
10210
10442
|
}
|
|
10443
|
+
if (result.held.length) {
|
|
10444
|
+
emit.emit(`skipped ${result.held.join(", ")} \u2014 held by another live ship`, { step: "select", role: current });
|
|
10445
|
+
}
|
|
10446
|
+
await noteReaccepted(tracker2, result, seat, cfg.ship.name);
|
|
10211
10447
|
if (!result.ticket) {
|
|
10212
10448
|
emit.emit(`${current} skipped this cycle \u2014 every candidate was already claimed elsewhere`);
|
|
10213
10449
|
dropLock();
|
|
@@ -10529,7 +10765,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10529
10765
|
const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync14(resolvedPath, "utf8")) : {};
|
|
10530
10766
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
10531
10767
|
raw.agentSkills = skillResult.agentSkills;
|
|
10532
|
-
|
|
10768
|
+
mkdirSync11(dirOf(resolvedPath), { recursive: true });
|
|
10533
10769
|
writeFileSync9(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
10534
10770
|
`);
|
|
10535
10771
|
}
|
|
@@ -10577,7 +10813,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10577
10813
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
10578
10814
|
const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync14(resolvedPath, "utf8")) : {};
|
|
10579
10815
|
raw.agentSkills = result.agentSkills;
|
|
10580
|
-
|
|
10816
|
+
mkdirSync11(dirOf(resolvedPath), { recursive: true });
|
|
10581
10817
|
writeFileSync9(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
10582
10818
|
`);
|
|
10583
10819
|
}
|
|
@@ -10621,7 +10857,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10621
10857
|
}
|
|
10622
10858
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
10623
10859
|
const text = configText;
|
|
10624
|
-
const base =
|
|
10860
|
+
const base = dirname10(cfg.configFile);
|
|
10625
10861
|
try {
|
|
10626
10862
|
if (sub === "list") {
|
|
10627
10863
|
const repos = configuredRepos(text, routeName, base);
|
|
@@ -10982,8 +11218,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
10982
11218
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
10983
11219
|
`);
|
|
10984
11220
|
} else {
|
|
10985
|
-
|
|
10986
|
-
if (hadPreviousFile)
|
|
11221
|
+
mkdirSync11(dirname10(resolvedPath), { recursive: true });
|
|
11222
|
+
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
10987
11223
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
10988
11224
|
writeFileSync9(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
10989
11225
|
`);
|
|
@@ -10999,7 +11235,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
10999
11235
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
11000
11236
|
`);
|
|
11001
11237
|
} else {
|
|
11002
|
-
|
|
11238
|
+
mkdirSync11(dirname10(apiKeyFile), { recursive: true });
|
|
11003
11239
|
writeFileSync9(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
11004
11240
|
`, { mode: 384 });
|
|
11005
11241
|
process.stderr.write(`API key written to ${apiKeyFile}
|
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.
|
|
@@ -353,9 +353,12 @@ if `report_type` is either of those.
|
|
|
353
353
|
reconcile it yourself. Post a comment on the ticket naming the repo and the
|
|
354
354
|
counts `crew sync` printed, leave the ticket as you found it, and end the
|
|
355
355
|
run — the operator reconciles the checkout. Otherwise, from
|
|
356
|
-
the primary checkout, `git worktree add
|
|
357
|
-
ticket, on a new branch cut from the base
|
|
358
|
-
|
|
356
|
+
the primary checkout, `git worktree add <dir> -b <branch> <remote>/<base>`
|
|
357
|
+
a sibling worktree for this ticket, on a new branch cut from the base
|
|
358
|
+
branch's REMOTE-TRACKING ref (`origin/main`, say), never from the primary
|
|
359
|
+
checkout's own local base — that copy is a cache of the remote, and a
|
|
360
|
+
local commit on it would be carried into every ticket cut from it
|
|
361
|
+
(CREW-1383). The digest's `worktree` and `branch` columns give this ticket's own directory
|
|
359
362
|
and branch name — use them verbatim. The Environment section names the
|
|
360
363
|
base branch and describes the general pattern, but its own worked example
|
|
361
364
|
is illustrative only (rendered before any ticket is chosen, so it cannot
|
|
@@ -529,6 +532,10 @@ if `report_type` is either of those.
|
|
|
529
532
|
part).
|
|
530
533
|
9. Set the ticket's `status` to `fixed` once you have verified it yourself,
|
|
531
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`.
|
|
532
539
|
**Push the branch first, in the same step:**
|
|
533
540
|
`git push --set-upstream <remote> <branch>` from the worktree (the remote is
|
|
534
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.
|
|
@@ -334,8 +334,12 @@ if `report_type` is either of those.
|
|
|
334
334
|
reconcile it yourself. Post a comment on the ticket naming the repo and the
|
|
335
335
|
counts `crew sync` printed, leave the ticket as you found it, and end the
|
|
336
336
|
run — the operator reconciles the checkout. Otherwise, from
|
|
337
|
-
the primary checkout, `git worktree add
|
|
338
|
-
ticket, on a new branch cut from the base
|
|
337
|
+
the primary checkout, `git worktree add <dir> -b <branch> <remote>/<base>`
|
|
338
|
+
a sibling worktree for this ticket, on a new branch cut from the base
|
|
339
|
+
branch's REMOTE-TRACKING ref (`origin/main`, say), never from the primary
|
|
340
|
+
checkout's own local base — that copy is a cache of the remote, and a
|
|
341
|
+
local commit on it would be carried into every ticket cut from it
|
|
342
|
+
(CREW-1383). The
|
|
339
343
|
digest's `worktree` and `branch` columns give this ticket's own directory
|
|
340
344
|
and branch name — use them verbatim. The Environment section names the
|
|
341
345
|
base branch and describes the general pattern, but its own worked example
|
|
@@ -510,6 +514,10 @@ if `report_type` is either of those.
|
|
|
510
514
|
part).
|
|
511
515
|
9. Set the ticket's `status` to `fixed` once you have verified it yourself,
|
|
512
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`.
|
|
513
521
|
**Push the branch first, in the same step:**
|
|
514
522
|
`git push --set-upstream <remote> <branch>` from the worktree (the remote is
|
|
515
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
|