@tablation/crew 0.21.0 → 0.23.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 +74 -12
- package/package.json +1 -1
- package/prompts/default/personas/common.md +15 -5
- package/prompts/default/personas/lane-design.md +5 -0
- package/prompts/default/personas/lane-qa.md +5 -0
- package/prompts/dev-qa/personas/common.md +16 -1
- package/prompts/dev-qa/personas/lane-qa.md +5 -0
package/dist/cli.js
CHANGED
|
@@ -3273,6 +3273,26 @@ function isAutomatic(t, wm) {
|
|
|
3273
3273
|
if (!wm || wm.modes.size === 0) return true;
|
|
3274
3274
|
return modeOf(t, wm) === "automatic";
|
|
3275
3275
|
}
|
|
3276
|
+
function skippedByMode(tickets, wm) {
|
|
3277
|
+
const out = { manual: 0, hybrid: 0 };
|
|
3278
|
+
if (!wm || wm.modes.size === 0) return out;
|
|
3279
|
+
const kept = new Set(withinWorkMode(tickets, wm));
|
|
3280
|
+
for (const t of tickets) {
|
|
3281
|
+
if (kept.has(t)) continue;
|
|
3282
|
+
out[modeOf(t, wm) === "manual" ? "manual" : "hybrid"]++;
|
|
3283
|
+
}
|
|
3284
|
+
return out;
|
|
3285
|
+
}
|
|
3286
|
+
function withoutManualVerified(tickets, verifiedStatus, wm) {
|
|
3287
|
+
if (!wm || wm.modes.size === 0) return tickets;
|
|
3288
|
+
return tickets.filter((t) => t.status !== verifiedStatus || modeOf(t, wm) !== "manual");
|
|
3289
|
+
}
|
|
3290
|
+
function projectModeLabels(rows) {
|
|
3291
|
+
return rows.map((r) => {
|
|
3292
|
+
const name = r.name ?? r.id;
|
|
3293
|
+
return { id: r.id, name, mode: r.mode, label: `${name} (${r.mode})` };
|
|
3294
|
+
});
|
|
3295
|
+
}
|
|
3276
3296
|
|
|
3277
3297
|
// src/board-lock.ts
|
|
3278
3298
|
import { StaleWriteError } from "@tablation/client";
|
|
@@ -3517,6 +3537,12 @@ var Tracker = class {
|
|
|
3517
3537
|
if (!model) return [];
|
|
3518
3538
|
return this.client.records.list(model, { limit: 200 });
|
|
3519
3539
|
}
|
|
3540
|
+
/** Every Projects row with its raw `work_mode`, for `crew status`. */
|
|
3541
|
+
async projectRowsWithModes() {
|
|
3542
|
+
const rows = await this.projectRows();
|
|
3543
|
+
const modes = await this.projectWorkModes();
|
|
3544
|
+
return rows.map((r) => ({ ...r, mode: modes.get(r.id) ?? "automatic" }));
|
|
3545
|
+
}
|
|
3520
3546
|
/** Create a `Repos` row. Throws when the workspace has no Repos table. */
|
|
3521
3547
|
async createRepoRow(fields) {
|
|
3522
3548
|
const model = this.route.resolved?.reposModelId;
|
|
@@ -3929,9 +3955,9 @@ function sweepDiagnostics(tickets, info) {
|
|
|
3929
3955
|
return { dangling, selfBlocked: [...new Set(selfBlocked)] };
|
|
3930
3956
|
}
|
|
3931
3957
|
var describeBlockers = (t, info) => (t.blocked_by ?? []).map((id) => `${info[id]?.issue_id ?? "?"} (${info[id]?.status ?? "unknown"})`).join(", ");
|
|
3932
|
-
function planSweep(tickets, info, blocked) {
|
|
3958
|
+
function planSweep(tickets, info, blocked, wm) {
|
|
3933
3959
|
const steps = [];
|
|
3934
|
-
for (const t of tickets) {
|
|
3960
|
+
for (const t of withinWorkMode(tickets, wm)) {
|
|
3935
3961
|
if (t.status === "accepted" && blocked.has(t.id)) {
|
|
3936
3962
|
steps.push({ action: "park", ticket: t, blockers: describeBlockers(t, info), to: "blocked" });
|
|
3937
3963
|
} else if (t.status === "blocked" && !blocked.has(t.id)) {
|
|
@@ -4500,6 +4526,9 @@ function blockers(t, i) {
|
|
|
4500
4526
|
});
|
|
4501
4527
|
return list.length === 0 ? "\u2014" : list.join(", ");
|
|
4502
4528
|
}
|
|
4529
|
+
function modeCell(t, i) {
|
|
4530
|
+
return i.workModes && i.workModes.modes.size > 0 ? modeOf(t, i.workModes) : "\u2014";
|
|
4531
|
+
}
|
|
4503
4532
|
var sorted = (ts) => [...ts].sort(compareRank);
|
|
4504
4533
|
var stream = (segments) => `${segments.join("\n")}
|
|
4505
4534
|
`;
|
|
@@ -4511,14 +4540,14 @@ ${sorted(ts).map(row).join("\n")}
|
|
|
4511
4540
|
`;
|
|
4512
4541
|
}
|
|
4513
4542
|
function buildingDigest(i) {
|
|
4514
|
-
const header = "| ticket | repo | branch | worktree | status | assignee | filed by | sev | pri | eff | updated | last comment | new since last poll |\n
|
|
4543
|
+
const header = "| ticket | repo | branch | worktree | mode | status | assignee | filed by | sev | pri | eff | updated | last comment | new since last poll |\n|---|---|---|---|---|---|---|---|---|---|---|---|---|---|";
|
|
4515
4544
|
const row = (t) => {
|
|
4516
4545
|
const n = newFromOthers(i, t.id);
|
|
4517
4546
|
const branch = i.branchFor ? i.branchFor(t) : "";
|
|
4518
4547
|
const worktree = i.worktreeFor ? i.worktreeFor(t) : "";
|
|
4519
4548
|
const dir = i.dirFor ? i.dirFor(t) : null;
|
|
4520
4549
|
const repo = i.dirFor ? dir ?? "**NO CHECKOUT**" : "";
|
|
4521
|
-
return `| ${displayKey2(t)} | ${repo} | ${branch} | ${worktree} | ${t.status} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | p${effectivePriority(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
|
|
4550
|
+
return `| ${displayKey2(t)} | ${repo} | ${branch} | ${worktree} | ${modeCell(t, i)} | ${t.status} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | p${effectivePriority(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
|
|
4522
4551
|
};
|
|
4523
4552
|
const blockedRow = (t) => `| ${displayKey2(t)} | ${t.status} | ${who(t, i)} | p${effectivePriority(t)} | ${blockers(t, i)} |`;
|
|
4524
4553
|
const blockedTable = (ts) => ts.length === 0 ? "\n_None._\n" : `
|
|
@@ -4564,7 +4593,7 @@ ${sorted(held).map((t) => `| ${displayKey2(t)} | ${stopOf(t)} |`).join("\n")}
|
|
|
4564
4593
|
]);
|
|
4565
4594
|
}
|
|
4566
4595
|
function qaDigest(i) {
|
|
4567
|
-
const header = "| ticket | repo | status | built by | assignee | filed by | sev | pri | eff | branch | worktree | updated | last comment | new since last poll |\n
|
|
4596
|
+
const header = "| ticket | repo | mode | status | built by | assignee | filed by | sev | pri | eff | branch | worktree | updated | last comment | new since last poll |\n|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|";
|
|
4568
4597
|
const branchCell = (t) => {
|
|
4569
4598
|
if (i.dirFor && i.dirFor(t) === null) return "\u2014";
|
|
4570
4599
|
if (!i.existingBranchFor) return "";
|
|
@@ -4579,7 +4608,7 @@ function qaDigest(i) {
|
|
|
4579
4608
|
const n = newFromOthers(i, t.id);
|
|
4580
4609
|
const dir = i.dirFor ? i.dirFor(t) : null;
|
|
4581
4610
|
const repo = i.dirFor ? dir ?? "**NO CHECKOUT**" : "";
|
|
4582
|
-
return `| ${displayKey2(t)} | ${repo} | ${t.status} | ${builtBy(t)} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | p${effectivePriority(t)} | ${branchCell(t)} | ${worktreeCell(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
|
|
4611
|
+
return `| ${displayKey2(t)} | ${repo} | ${modeCell(t, i)} | ${t.status} | ${builtBy(t)} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | p${effectivePriority(t)} | ${branchCell(t)} | ${worktreeCell(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
|
|
4583
4612
|
};
|
|
4584
4613
|
return stream([
|
|
4585
4614
|
"## Current queue \u2014 built for you by the poll\n",
|
|
@@ -4649,7 +4678,7 @@ var ms = (iso) => {
|
|
|
4649
4678
|
const n = iso ? Date.parse(iso) : NaN;
|
|
4650
4679
|
return Number.isFinite(n) ? n : 0;
|
|
4651
4680
|
};
|
|
4652
|
-
function planStalled(tickets, comments, ships, contract, holds2 = /* @__PURE__ */ new Set(), now = Date.now()) {
|
|
4681
|
+
function planStalled(tickets, comments, ships, contract, holds2 = /* @__PURE__ */ new Set(), now = Date.now(), wm) {
|
|
4653
4682
|
const watched = /* @__PURE__ */ new Set([contract.statuses.building, contract.statuses.handoff, contract.statuses.verifying]);
|
|
4654
4683
|
const byTicket = /* @__PURE__ */ new Map();
|
|
4655
4684
|
for (const c of comments) {
|
|
@@ -4658,7 +4687,7 @@ function planStalled(tickets, comments, ships, contract, holds2 = /* @__PURE__ *
|
|
|
4658
4687
|
else byTicket.set(c.ticket_id, [c]);
|
|
4659
4688
|
}
|
|
4660
4689
|
const out = [];
|
|
4661
|
-
for (const t of tickets) {
|
|
4690
|
+
for (const t of withinWorkMode(tickets, wm)) {
|
|
4662
4691
|
if (!watched.has(t.status)) continue;
|
|
4663
4692
|
if (t.assignee_id && holds2.has(t.assignee_id)) continue;
|
|
4664
4693
|
const mine = byTicket.get(t.id) ?? [];
|
|
@@ -4782,9 +4811,15 @@ async function decideCycle(o) {
|
|
|
4782
4811
|
if (diag.selfBlocked.length) {
|
|
4783
4812
|
emit.warn(`ticket(s) blocking themselves, parked permanently: ${diag.selfBlocked.join(", ")}`);
|
|
4784
4813
|
}
|
|
4785
|
-
const sweep = planSweep(tickets, info, blocked);
|
|
4814
|
+
const sweep = planSweep(tickets, info, blocked, workModes);
|
|
4786
4815
|
const holdReleases = planHoldReleases(tickets, tracker.contract);
|
|
4787
|
-
const stalled = planStalled(tickets, comments, ships, tracker.contract, holds2);
|
|
4816
|
+
const stalled = planStalled(tickets, comments, ships, tracker.contract, holds2, Date.now(), workModes);
|
|
4817
|
+
const skipped = skippedByMode(tickets, workModes);
|
|
4818
|
+
if (skipped.manual || skipped.hybrid) {
|
|
4819
|
+
emit.emit(`sweeps skipped ${skipped.manual} manual/project-less and ${skipped.hybrid} unassigned hybrid ticket(s)`, {
|
|
4820
|
+
data: { sweepSkipped: skipped }
|
|
4821
|
+
});
|
|
4822
|
+
}
|
|
4788
4823
|
const blockedIssues = tickets.filter((t) => blocked.has(t.id)).map((t) => t.issue_id);
|
|
4789
4824
|
emit.emit(`${blockedIssues.length} ticket(s) parked as blocked`, { data: { blocked: blockedIssues } });
|
|
4790
4825
|
const qaHeld = tickets.filter((t) => t.status === tracker.contract.statuses.verifying).map((t) => t.issue_id);
|
|
@@ -5446,6 +5481,18 @@ function renderEnvironment(i) {
|
|
|
5446
5481
|
"another (the digest prints other ships by name) \u2014 write its name, never a Ships row id."
|
|
5447
5482
|
);
|
|
5448
5483
|
}
|
|
5484
|
+
const seatIds = Object.entries(route.resolved?.seats ?? {}).filter(([, id]) => !!id);
|
|
5485
|
+
if (seatIds.length > 0) {
|
|
5486
|
+
lines.push(
|
|
5487
|
+
"",
|
|
5488
|
+
"## Seats on this ship",
|
|
5489
|
+
"",
|
|
5490
|
+
"The Crew row id of each role's seat on this ship, to write verbatim where a hand-off",
|
|
5491
|
+
"assigns the next lane (a ticket whose digest `mode` is `hybrid`):",
|
|
5492
|
+
"",
|
|
5493
|
+
...seatIds.map(([role, id]) => `- ${role}: \`${id}\``)
|
|
5494
|
+
);
|
|
5495
|
+
}
|
|
5449
5496
|
lines.push(
|
|
5450
5497
|
"",
|
|
5451
5498
|
"## What the columns are called here",
|
|
@@ -11019,7 +11066,12 @@ ${changed2.map((c) => ` - ${c}
|
|
|
11019
11066
|
let seen = precheckTickets.get(c.route);
|
|
11020
11067
|
if (!seen) precheckTickets.set(c.route, seen = tracker.openTickets());
|
|
11021
11068
|
const { byRepo: preByRepo, unplaceable: preUnplaceable } = ticketsByRepo(c, await seen);
|
|
11022
|
-
const
|
|
11069
|
+
const preModes = { modes: await tracker.projectWorkModes(), agentAssignees: /* @__PURE__ */ new Set() };
|
|
11070
|
+
const pre = withoutManualVerified(
|
|
11071
|
+
preByRepo.get(target.name) ?? [],
|
|
11072
|
+
tracker.contract.statuses.verified,
|
|
11073
|
+
preModes
|
|
11074
|
+
);
|
|
11023
11075
|
reportUnplaceable(c.route, preUnplaceable, tracker.contract.statuses.verified, remit);
|
|
11024
11076
|
const d = decideRelease(target.dir, pre, tracker.contract, {
|
|
11025
11077
|
tagPattern: repo.release.tagPattern ?? "v*",
|
|
@@ -11067,7 +11119,12 @@ ${changed2.map((c) => ` - ${c}
|
|
|
11067
11119
|
}
|
|
11068
11120
|
const all = await tracker.openTickets();
|
|
11069
11121
|
const { byRepo, unplaceable } = ticketsByRepo(c, all);
|
|
11070
|
-
const
|
|
11122
|
+
const releaseModes = { modes: await tracker.projectWorkModes(), agentAssignees: /* @__PURE__ */ new Set() };
|
|
11123
|
+
const tickets = withoutManualVerified(
|
|
11124
|
+
byRepo.get(target.name) ?? [],
|
|
11125
|
+
tracker.contract.statuses.verified,
|
|
11126
|
+
releaseModes
|
|
11127
|
+
);
|
|
11071
11128
|
reportUnplaceable(c.route, unplaceable, tracker.contract.statuses.verified, remit);
|
|
11072
11129
|
const outcome = await runRelease({
|
|
11073
11130
|
cwd: target.dir,
|
|
@@ -12952,6 +13009,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
12952
13009
|
const engagedTickets = engagedTicketIds.length ? await statusTracker.ticketsByIds(engagedTicketIds) : [];
|
|
12953
13010
|
const issueLabelById = new Map(engagedTickets.map((t) => [t.id, t.issue_id]));
|
|
12954
13011
|
const todo = await statusTracker.openTickets().then((open) => operatorTodo(open, statusTracker.contract, route.resolved?.operator ?? "")).catch(() => []);
|
|
13012
|
+
const projects = projectModeLabels(await statusTracker.projectRowsWithModes().catch(() => []));
|
|
12955
13013
|
if (flag("json")) {
|
|
12956
13014
|
const repos = await resolvedRepos(route);
|
|
12957
13015
|
const seatIds = route.resolved?.seats ?? {};
|
|
@@ -13001,6 +13059,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13001
13059
|
watermark: state.watermark(),
|
|
13002
13060
|
route: {
|
|
13003
13061
|
name: route.route,
|
|
13062
|
+
// Additive (CREW-1496): each project and its work mode. `version` stays 1.
|
|
13063
|
+
projects: projects.map((p) => ({ id: p.id, name: p.name, workMode: p.mode })),
|
|
13004
13064
|
dir: route.dir,
|
|
13005
13065
|
enabled: route.enabled,
|
|
13006
13066
|
area: route.area ?? null,
|
|
@@ -13058,6 +13118,8 @@ watermark: ${state.watermark()}
|
|
|
13058
13118
|
if (state.isRolePaused(r)) process.stdout.write(`role ${r}: paused
|
|
13059
13119
|
`);
|
|
13060
13120
|
}
|
|
13121
|
+
if (projects.length) process.stdout.write(`projects: ${projects.map((p) => p.label).join(", ")}
|
|
13122
|
+
`);
|
|
13061
13123
|
for (const i of openShipAttention(state)) {
|
|
13062
13124
|
process.stdout.write(`attention: ${i.kind} \u2014 ${i.message}
|
|
13063
13125
|
`);
|
package/package.json
CHANGED
|
@@ -305,10 +305,18 @@ Never start work on a ticket without first setting `assignee_id` to your
|
|
|
305
305
|
own id (Step 3.3 already does this) — that claim is itself part of what
|
|
306
306
|
keeps a concurrent interactive session from re-entering the same ticket.
|
|
307
307
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
308
|
+
Every ticket's project has a **work mode**, shown in the digest's `mode`
|
|
309
|
+
column: `automatic` (lanes work tickets by status), `hybrid` (a lane considers a
|
|
310
|
+
ticket only when it is assigned to an agent — a Crew row backed by an Agent, not a
|
|
311
|
+
hold), or `manual` (no automation touches it; a ticket with no project counts as
|
|
312
|
+
manual). The digest says so when a project is not automatic; do not go looking for
|
|
313
|
+
a ticket it leaves out. **One hand-off rule:** in a `hybrid` project, hand a ticket
|
|
314
|
+
to the next lane by *assigning that lane's seat on this ship* (ids under "Seats on
|
|
315
|
+
this ship" in the Environment section, else the roster); in an `automatic` project,
|
|
316
|
+
clear `assignee_id` as before. If there is no seat for the target role, assign any
|
|
317
|
+
Crew row with that role's Agent; if there is none, clear it and say so in the
|
|
318
|
+
hand-off comment. Parks (`needs_info`, `needs_planning` → the operator) are the
|
|
319
|
+
same in every mode.
|
|
312
320
|
|
|
313
321
|
Never touch a ticket still at `new` — only `accepted` tickets are yours to
|
|
314
322
|
pick up; triage (a separate process) is what promotes `new` → `accepted`/
|
|
@@ -558,7 +566,9 @@ if `report_type` is either of those.
|
|
|
558
566
|
this is your own isolated worktree, no special permission needed for this
|
|
559
567
|
part).
|
|
560
568
|
9. Set the ticket's `status` to `fixed` once you have verified it yourself,
|
|
561
|
-
and
|
|
569
|
+
and hand the ticket to the QA lane: in an `automatic` project **clear `assignee_id`**; in a
|
|
570
|
+
`hybrid` project **set `assignee_id` to this ship's QA seat** instead (the digest's `mode`
|
|
571
|
+
column says which). That pair is the hand-off to the QA lane.
|
|
562
572
|
If the Issues table has a `held_by_ship_id` column, clear it in the same
|
|
563
573
|
write (the hold is for the building roles; QA on any ship may take a `fixed`
|
|
564
574
|
ticket). A QA bounce or `needs_info` park leaves it set; QA clears it again
|
|
@@ -98,6 +98,11 @@ stopping for this run, do the following instead:
|
|
|
98
98
|
it) — revise the comp against what they said and post again. This cycle
|
|
99
99
|
repeats as many times as the operator wants.
|
|
100
100
|
|
|
101
|
+
In a `hybrid` project (the digest's `mode` column), where the operator
|
|
102
|
+
leaves the hand-off to you, assign this ship's Developer seat when
|
|
103
|
+
`needs_design` is cleared rather than leaving the assignee blank, so the dev lane
|
|
104
|
+
still sees the ticket; in an `automatic` project nothing changes.
|
|
105
|
+
|
|
101
106
|
Never implement past the comp to "just finish it" because the fix looks
|
|
102
107
|
small, and never set `needs_design`, `accepted`, or clear
|
|
103
108
|
`needs_planning`/`needs_review` yourself — all of that is the operator's
|
|
@@ -141,6 +141,11 @@ human review, so "probably fine" is not a pass.
|
|
|
141
141
|
bump the version, and deploy this cycle or the next. Say in
|
|
142
142
|
the comment what you exercised, so the record shows what "verified"
|
|
143
143
|
covered.
|
|
144
|
+
- **Person-built hybrid ticket with no branch** — a `hybrid` ticket at
|
|
145
|
+
`fixed` whose branch under the repo's naming convention does not exist
|
|
146
|
+
(the digest's `branch` column says **MISSING**) is not a bounce to a
|
|
147
|
+
builder: set `needs_info` and assign the operator, with a comment naming the
|
|
148
|
+
branch you expected. A person built it; the operator has to push it.
|
|
144
149
|
- **It doesn't** → `status` = `in_progress`, `assignee_id` = the building
|
|
145
150
|
lane's own Crew row id from the roster above — the dev seat if
|
|
146
151
|
`needs_design` is false or null, the design seat if true. **Exception:** when the Issues table has a `held_by_ship_id` column, **clear** `assignee_id` *and* `held_by_ship_id` instead of naming a seat — the
|
|
@@ -286,6 +286,19 @@ Never start work on a ticket without first setting `assignee_id` to your
|
|
|
286
286
|
own id (Step 3.3 already does this) — that claim is itself part of what
|
|
287
287
|
keeps a concurrent interactive session from re-entering the same ticket.
|
|
288
288
|
|
|
289
|
+
Every ticket's project has a **work mode**, shown in the digest's `mode`
|
|
290
|
+
column: `automatic` (lanes work tickets by status), `hybrid` (a lane considers a
|
|
291
|
+
ticket only when it is assigned to an agent — a Crew row backed by an Agent, not a
|
|
292
|
+
hold), or `manual` (no automation touches it; a ticket with no project counts as
|
|
293
|
+
manual). The digest says so when a project is not automatic; do not go looking for
|
|
294
|
+
a ticket it leaves out. **One hand-off rule:** in a `hybrid` project, hand a ticket
|
|
295
|
+
to the next lane by *assigning that lane's seat on this ship* (ids under "Seats on
|
|
296
|
+
this ship" in the Environment section, else the roster); in an `automatic` project,
|
|
297
|
+
clear `assignee_id` as before. If there is no seat for the target role, assign any
|
|
298
|
+
Crew row with that role's Agent; if there is none, clear it and say so in the
|
|
299
|
+
hand-off comment. Parks (`needs_info`, `needs_planning` → the operator) are the
|
|
300
|
+
same in every mode.
|
|
301
|
+
|
|
289
302
|
Never touch a ticket still at `new` — only `accepted` tickets are yours to
|
|
290
303
|
pick up; triage (a separate process) is what promotes `new` → `accepted`/
|
|
291
304
|
`needs_info`.
|
|
@@ -535,7 +548,9 @@ if `report_type` is either of those.
|
|
|
535
548
|
this is your own isolated worktree, no special permission needed for this
|
|
536
549
|
part).
|
|
537
550
|
9. Set the ticket's `status` to `fixed` once you have verified it yourself,
|
|
538
|
-
and
|
|
551
|
+
and hand the ticket to the QA lane: in an `automatic` project **clear `assignee_id`**; in a
|
|
552
|
+
`hybrid` project **set `assignee_id` to this ship's QA seat** instead (the digest's `mode`
|
|
553
|
+
column says which). That pair is the hand-off to the QA lane.
|
|
539
554
|
If the Issues table has a `held_by_ship_id` column, clear it in the same
|
|
540
555
|
write (the hold is for the building roles; QA on any ship may take a `fixed`
|
|
541
556
|
ticket). A QA bounce or `needs_info` park leaves it set; QA clears it again
|
|
@@ -128,6 +128,11 @@ human review, so "probably fine" is not a pass.
|
|
|
128
128
|
bump the version, and deploy this cycle or the next. Say in
|
|
129
129
|
the comment what you exercised, so the record shows what "verified"
|
|
130
130
|
covered.
|
|
131
|
+
- **Person-built hybrid ticket with no branch** — a `hybrid` ticket at
|
|
132
|
+
`fixed` whose branch under the repo's naming convention does not exist
|
|
133
|
+
(the digest's `branch` column says **MISSING**) is not a bounce to a
|
|
134
|
+
builder: set `needs_info` and assign the operator, with a comment naming the
|
|
135
|
+
branch you expected. A person built it; the operator has to push it.
|
|
131
136
|
- **It doesn't** → `status` = `in_progress`, `assignee_id` = the dev
|
|
132
137
|
seat's own Crew row id from the roster above. **Exception:** when the Issues table has a `held_by_ship_id` column, **clear** `assignee_id` *and* `held_by_ship_id` instead of naming a seat — the
|
|
133
138
|
hold on a `qa` ticket is yours, as verifier, and seats are per ship, so
|