@tablation/crew 0.20.2 → 0.22.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 +68 -31
- 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
|
@@ -1842,6 +1842,7 @@ var DEFAULT_CONTRACT = {
|
|
|
1842
1842
|
unknownPriorityRank: 2,
|
|
1843
1843
|
coordinatingValue: "coordinating",
|
|
1844
1844
|
manualWorkMode: "manual",
|
|
1845
|
+
hybridWorkMode: "hybrid",
|
|
1845
1846
|
repoExemptReportTypes: ["coordinating", "question", "investigation"]
|
|
1846
1847
|
};
|
|
1847
1848
|
function closedStatuses(c) {
|
|
@@ -1863,6 +1864,7 @@ function resolveContract(override) {
|
|
|
1863
1864
|
unknownPriorityRank: override.unknownPriorityRank ?? DEFAULT_CONTRACT.unknownPriorityRank,
|
|
1864
1865
|
coordinatingValue: override.coordinatingValue ?? DEFAULT_CONTRACT.coordinatingValue,
|
|
1865
1866
|
manualWorkMode: override.manualWorkMode ?? DEFAULT_CONTRACT.manualWorkMode,
|
|
1867
|
+
hybridWorkMode: override.hybridWorkMode ?? DEFAULT_CONTRACT.hybridWorkMode,
|
|
1866
1868
|
// A workspace overriding `coordinatingValue` alone (renaming the rollup
|
|
1867
1869
|
// marker) still gets that renamed value exempted here by default,
|
|
1868
1870
|
// without also having to repeat it — only an explicit
|
|
@@ -3243,14 +3245,33 @@ var SHELL_WORDS = /* @__PURE__ */ new Set([
|
|
|
3243
3245
|
]);
|
|
3244
3246
|
|
|
3245
3247
|
// src/work-mode.ts
|
|
3246
|
-
function
|
|
3247
|
-
const out = /* @__PURE__ */ new
|
|
3248
|
-
for (const r of rows)
|
|
3248
|
+
function projectWorkModes(rows, contract) {
|
|
3249
|
+
const out = /* @__PURE__ */ new Map();
|
|
3250
|
+
for (const r of rows) {
|
|
3251
|
+
const v = r[contract.columns.workMode];
|
|
3252
|
+
out.set(r.id, v === contract.manualWorkMode ? "manual" : v === contract.hybridWorkMode ? "hybrid" : "automatic");
|
|
3253
|
+
}
|
|
3249
3254
|
return out;
|
|
3250
3255
|
}
|
|
3251
|
-
function
|
|
3252
|
-
|
|
3253
|
-
|
|
3256
|
+
function agentAssigneeIds(crewRows, isHoldRow) {
|
|
3257
|
+
return new Set(crewRows.filter((r) => !!r.agent_id && !isHoldRow(r.id)).map((r) => r.id));
|
|
3258
|
+
}
|
|
3259
|
+
function modeOf(t, wm) {
|
|
3260
|
+
if (!t.project_id) return "manual";
|
|
3261
|
+
return wm.modes.get(t.project_id) ?? "automatic";
|
|
3262
|
+
}
|
|
3263
|
+
function withinWorkMode(tickets, wm) {
|
|
3264
|
+
if (!wm || wm.modes.size === 0) return tickets;
|
|
3265
|
+
return tickets.filter((t) => {
|
|
3266
|
+
const mode = modeOf(t, wm);
|
|
3267
|
+
if (mode === "automatic") return true;
|
|
3268
|
+
if (mode === "manual") return false;
|
|
3269
|
+
return !!t.assignee_id && wm.agentAssignees.has(t.assignee_id);
|
|
3270
|
+
});
|
|
3271
|
+
}
|
|
3272
|
+
function isAutomatic(t, wm) {
|
|
3273
|
+
if (!wm || wm.modes.size === 0) return true;
|
|
3274
|
+
return modeOf(t, wm) === "automatic";
|
|
3254
3275
|
}
|
|
3255
3276
|
|
|
3256
3277
|
// src/board-lock.ts
|
|
@@ -3475,19 +3496,19 @@ var Tracker = class {
|
|
|
3475
3496
|
}
|
|
3476
3497
|
}
|
|
3477
3498
|
/**
|
|
3478
|
-
*
|
|
3479
|
-
*
|
|
3480
|
-
*
|
|
3481
|
-
*
|
|
3499
|
+
* Every Projects (area) row's work mode (CREW-1445, CREW-1493). One extra
|
|
3500
|
+
* read per cycle. A workspace with no Projects table, no `work_mode` column
|
|
3501
|
+
* or an unreadable table yields the empty map, which the selection reads as
|
|
3502
|
+
* "no filtering"; a null value is `automatic`.
|
|
3482
3503
|
*/
|
|
3483
|
-
async
|
|
3504
|
+
async projectWorkModes() {
|
|
3484
3505
|
const model = this.route.resolved?.areaModelId;
|
|
3485
|
-
if (!model) return /* @__PURE__ */ new
|
|
3506
|
+
if (!model) return /* @__PURE__ */ new Map();
|
|
3486
3507
|
try {
|
|
3487
3508
|
const rows = await this.client.records.list(model, { limit: 200 });
|
|
3488
|
-
return
|
|
3509
|
+
return projectWorkModes(rows, this.contract);
|
|
3489
3510
|
} catch {
|
|
3490
|
-
return /* @__PURE__ */ new
|
|
3511
|
+
return /* @__PURE__ */ new Map();
|
|
3491
3512
|
}
|
|
3492
3513
|
}
|
|
3493
3514
|
/** Every `Projects` (area) row — what `crew repos add` offers when a new Repos row needs one. */
|
|
@@ -4156,7 +4177,7 @@ function holdWarnings(tickets, ships, contract, humanHolds, now = Date.now()) {
|
|
|
4156
4177
|
// src/select.ts
|
|
4157
4178
|
var qaSlice = (tickets) => tickets.filter((t) => t.status === "fixed" || t.status === "qa");
|
|
4158
4179
|
var buildingSlice = (tickets, role) => tickets.filter((t) => role === "design" ? t.needs_design === true : t.needs_design !== true);
|
|
4159
|
-
var triageSlice = (tickets, triageSeat) => triageSeat ? tickets.filter((t) => t.assignee_id === triageSeat) : [];
|
|
4180
|
+
var triageSlice = (tickets, triageSeat, wm) => triageSeat ? tickets.filter((t) => t.assignee_id === triageSeat && isAutomatic(t, wm)) : [];
|
|
4160
4181
|
function sliceFor(tickets, role) {
|
|
4161
4182
|
if (role === "qa") return qaSlice(tickets);
|
|
4162
4183
|
if (role === "triage") return triageSlice(tickets);
|
|
@@ -4184,7 +4205,7 @@ function notHeldByOtherShip(slice, i) {
|
|
|
4184
4205
|
function buildingRoleHasWork(role, i) {
|
|
4185
4206
|
const me = i.seats[role];
|
|
4186
4207
|
if (!me) return { hasWork: false, reason: "this ship does not crew that role" };
|
|
4187
|
-
const mine = withinWorkMode(notInStoppedRepo(notHeldByOtherShip(sliceFor(i.tickets, role), i), i),
|
|
4208
|
+
const mine = withinWorkMode(notInStoppedRepo(notHeldByOtherShip(sliceFor(i.tickets, role), i), i), i.workModes);
|
|
4188
4209
|
const startable = mine.filter(
|
|
4189
4210
|
(t) => (t.status === "accepted" || t.status === "blocked") && !i.blocked.has(t.id) && !isHeld(t, i.holds)
|
|
4190
4211
|
);
|
|
@@ -4213,12 +4234,12 @@ function buildingRoleHasWork(role, i) {
|
|
|
4213
4234
|
}
|
|
4214
4235
|
function qaRoleHasWork(i) {
|
|
4215
4236
|
if (!i.seats.qa) return { hasWork: false, reason: "this ship does not crew that role" };
|
|
4216
|
-
const n = notHeldByOtherShip(qaSlice(i.tickets), i).filter((t) => !isHeld(t, i.holds)).length;
|
|
4237
|
+
const n = withinWorkMode(notHeldByOtherShip(qaSlice(i.tickets), i), i.workModes).filter((t) => !isHeld(t, i.holds)).length;
|
|
4217
4238
|
return n > 0 ? { hasWork: true, reason: `${n} ticket(s) awaiting or in verification` } : { hasWork: false, reason: "nothing awaiting verification" };
|
|
4218
4239
|
}
|
|
4219
4240
|
function triageRoleHasWork(i) {
|
|
4220
4241
|
if (!i.seats.triage) return { hasWork: false, reason: "this ship does not crew that role" };
|
|
4221
|
-
const n = triageSlice(i.tickets, i.seats.triage).length;
|
|
4242
|
+
const n = triageSlice(i.tickets, i.seats.triage, i.workModes).length;
|
|
4222
4243
|
return n > 0 ? { hasWork: true, reason: `${n} report(s) assigned to triage` } : { hasWork: false, reason: "nothing assigned to triage" };
|
|
4223
4244
|
}
|
|
4224
4245
|
function roleHasWork(role, i) {
|
|
@@ -4229,12 +4250,12 @@ function roleHasWork(role, i) {
|
|
|
4229
4250
|
return buildingRoleHasWork(role, i);
|
|
4230
4251
|
}
|
|
4231
4252
|
function roleCandidates(role, i) {
|
|
4232
|
-
if (role === "qa") return notHeldByOtherShip(qaSlice(i.tickets), i).filter((t) => !isHeld(t, i.holds));
|
|
4233
|
-
if (role === "triage") return triageSlice(i.tickets, i.seats.triage).filter((t) => !isHeld(t, i.holds));
|
|
4253
|
+
if (role === "qa") return withinWorkMode(notHeldByOtherShip(qaSlice(i.tickets), i), i.workModes).filter((t) => !isHeld(t, i.holds));
|
|
4254
|
+
if (role === "triage") return triageSlice(i.tickets, i.seats.triage, i.workModes).filter((t) => !isHeld(t, i.holds));
|
|
4234
4255
|
if (role === "pair") return [];
|
|
4235
4256
|
const me = i.seats[role];
|
|
4236
4257
|
if (!me) return [];
|
|
4237
|
-
return withinWorkMode(notInStoppedRepo(notHeldByOtherShip(sliceFor(i.tickets, role), i), i),
|
|
4258
|
+
return withinWorkMode(notInStoppedRepo(notHeldByOtherShip(sliceFor(i.tickets, role), i), i), i.workModes).filter(
|
|
4238
4259
|
(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)
|
|
4239
4260
|
);
|
|
4240
4261
|
}
|
|
@@ -4479,6 +4500,9 @@ function blockers(t, i) {
|
|
|
4479
4500
|
});
|
|
4480
4501
|
return list.length === 0 ? "\u2014" : list.join(", ");
|
|
4481
4502
|
}
|
|
4503
|
+
function modeCell(t, i) {
|
|
4504
|
+
return i.workModes && i.workModes.modes.size > 0 ? modeOf(t, i.workModes) : "\u2014";
|
|
4505
|
+
}
|
|
4482
4506
|
var sorted = (ts) => [...ts].sort(compareRank);
|
|
4483
4507
|
var stream = (segments) => `${segments.join("\n")}
|
|
4484
4508
|
`;
|
|
@@ -4490,14 +4514,14 @@ ${sorted(ts).map(row).join("\n")}
|
|
|
4490
4514
|
`;
|
|
4491
4515
|
}
|
|
4492
4516
|
function buildingDigest(i) {
|
|
4493
|
-
const header = "| ticket | repo | branch | worktree | status | assignee | filed by | sev | pri | eff | updated | last comment | new since last poll |\n
|
|
4517
|
+
const header = "| ticket | repo | branch | worktree | mode | status | assignee | filed by | sev | pri | eff | updated | last comment | new since last poll |\n|---|---|---|---|---|---|---|---|---|---|---|---|---|---|";
|
|
4494
4518
|
const row = (t) => {
|
|
4495
4519
|
const n = newFromOthers(i, t.id);
|
|
4496
4520
|
const branch = i.branchFor ? i.branchFor(t) : "";
|
|
4497
4521
|
const worktree = i.worktreeFor ? i.worktreeFor(t) : "";
|
|
4498
4522
|
const dir = i.dirFor ? i.dirFor(t) : null;
|
|
4499
4523
|
const repo = i.dirFor ? dir ?? "**NO CHECKOUT**" : "";
|
|
4500
|
-
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"} |`;
|
|
4524
|
+
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"} |`;
|
|
4501
4525
|
};
|
|
4502
4526
|
const blockedRow = (t) => `| ${displayKey2(t)} | ${t.status} | ${who(t, i)} | p${effectivePriority(t)} | ${blockers(t, i)} |`;
|
|
4503
4527
|
const blockedTable = (ts) => ts.length === 0 ? "\n_None._\n" : `
|
|
@@ -4529,7 +4553,7 @@ ${sorted(held).map((t) => `| ${displayKey2(t)} | ${stopOf(t)} |`).join("\n")}
|
|
|
4529
4553
|
"\nAlready filtered to your lane, and already ordered by the Step 2 rule.\nTicket bodies are deliberately omitted: fetch the full record of only the\nticket you actually pick up. **Do not re-fetch the whole tracker.** This\ndigest comes from the same API call the poll just made, moments ago.\n",
|
|
4530
4554
|
'\n"new since last poll" counts comments from someone other than you since\nthe poll watermark \u2014 the same signal that woke this run.\n',
|
|
4531
4555
|
"\n**`repo` is the checkout a ticket's work happens in** \u2014 an area spans\nseveral repositories, so cut the worktree beside THAT directory, not beside\nwhichever one you started in. A ticket marked **NO CHECKOUT** is not yours:\nthis ship has no clone of its repository, and another ship may serve it.\n\n**Use the `branch` and `worktree` columns verbatim.** They are that\nrepository's own naming convention, rendered for THIS ticket \u2014 cut the\nworktree at `<repo>/../<worktree>`, on branch `<branch>`. Do not derive\neither yourself, and prefer these over the Environment section's own worked\nexample if the two ever disagree \u2014 that example is illustrative only,\nrendered before any ticket is picked.\n",
|
|
4532
|
-
...i.
|
|
4556
|
+
...i.workModes && [...i.workModes.modes.values()].some((m) => m !== "automatic") ? ["\n**Manual / hybrid projects.** Some projects on this board are not fully automatic. In a `manual` project no lane touches any ticket, and a ticket with no project is treated the same way, so none appear below. In a `hybrid` project a ticket appears below only when it is assigned to an agent. An `accepted` ticket that is absent for either reason is not yours to claim \u2014 do not go looking for it.\n"] : [],
|
|
4533
4557
|
"\n### Step 1 \u2014 open tickets that may be yours to act on\n",
|
|
4534
4558
|
"\n`fixed` tickets are deliberately absent: they belong to the QA lane.\n",
|
|
4535
4559
|
table(step1, i, header, row),
|
|
@@ -4543,7 +4567,7 @@ ${sorted(held).map((t) => `| ${displayKey2(t)} | ${stopOf(t)} |`).join("\n")}
|
|
|
4543
4567
|
]);
|
|
4544
4568
|
}
|
|
4545
4569
|
function qaDigest(i) {
|
|
4546
|
-
const header = "| ticket | repo | status | built by | assignee | filed by | sev | pri | eff | branch | worktree | updated | last comment | new since last poll |\n
|
|
4570
|
+
const header = "| ticket | repo | mode | status | built by | assignee | filed by | sev | pri | eff | branch | worktree | updated | last comment | new since last poll |\n|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|";
|
|
4547
4571
|
const branchCell = (t) => {
|
|
4548
4572
|
if (i.dirFor && i.dirFor(t) === null) return "\u2014";
|
|
4549
4573
|
if (!i.existingBranchFor) return "";
|
|
@@ -4558,7 +4582,7 @@ function qaDigest(i) {
|
|
|
4558
4582
|
const n = newFromOthers(i, t.id);
|
|
4559
4583
|
const dir = i.dirFor ? i.dirFor(t) : null;
|
|
4560
4584
|
const repo = i.dirFor ? dir ?? "**NO CHECKOUT**" : "";
|
|
4561
|
-
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"} |`;
|
|
4585
|
+
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"} |`;
|
|
4562
4586
|
};
|
|
4563
4587
|
return stream([
|
|
4564
4588
|
"## Current queue \u2014 built for you by the poll\n",
|
|
@@ -4717,13 +4741,13 @@ async function decideCycle(o) {
|
|
|
4717
4741
|
const { route, ship, state, emit } = o;
|
|
4718
4742
|
const tracker = new Tracker(route, ship);
|
|
4719
4743
|
emit.enter("poll");
|
|
4720
|
-
const [tickets, comments, crewRows, epicRows, ships,
|
|
4744
|
+
const [tickets, comments, crewRows, epicRows, ships, projectModes] = await Promise.all([
|
|
4721
4745
|
tracker.openTickets(),
|
|
4722
4746
|
tracker.comments(200),
|
|
4723
4747
|
tracker.crewRows(),
|
|
4724
4748
|
tracker.epicRows(),
|
|
4725
4749
|
tracker.shipRows(),
|
|
4726
|
-
tracker.
|
|
4750
|
+
tracker.projectWorkModes()
|
|
4727
4751
|
]);
|
|
4728
4752
|
emit.emit(`${tickets.length} open ticket(s), ${comments.length} comment(s)`, {
|
|
4729
4753
|
data: { tickets: tickets.length, comments: comments.length }
|
|
@@ -4755,6 +4779,7 @@ async function decideCycle(o) {
|
|
|
4755
4779
|
emit.enter("sweep");
|
|
4756
4780
|
const roster = buildRoster(configuredMembers(route), crewRows);
|
|
4757
4781
|
const holds2 = new Set(holdIds(roster));
|
|
4782
|
+
const workModes = { modes: projectModes, agentAssignees: agentAssigneeIds(crewRows, (id) => holds2.has(id)) };
|
|
4758
4783
|
const diag = sweepDiagnostics(tickets, info);
|
|
4759
4784
|
for (const d of diag.dangling) emit.warn(`dangling blocked_by reference, ignored: ${d}`);
|
|
4760
4785
|
if (diag.selfBlocked.length) {
|
|
@@ -4831,7 +4856,7 @@ async function decideCycle(o) {
|
|
|
4831
4856
|
await checkRepoDisks({ state, emit, route, ship });
|
|
4832
4857
|
const selectionInput = {
|
|
4833
4858
|
repoStop: repoStopFor(openShipAttention(state), route),
|
|
4834
|
-
|
|
4859
|
+
workModes,
|
|
4835
4860
|
tickets,
|
|
4836
4861
|
comments,
|
|
4837
4862
|
watermark,
|
|
@@ -4926,8 +4951,8 @@ function writeDigest(o, d, role, stateDir) {
|
|
|
4926
4951
|
}
|
|
4927
4952
|
return existingBranchForTicket(dir, repoFor(dir), t, role);
|
|
4928
4953
|
},
|
|
4929
|
-
tickets: role === "triage" ? d.tickets
|
|
4930
|
-
|
|
4954
|
+
tickets: role === "triage" ? triageSlice(d.tickets, o.route.resolved?.seats.triage, d.selectionInput.workModes) : withinWorkMode(sliceFor(d.tickets, role), d.selectionInput.workModes),
|
|
4955
|
+
workModes: d.selectionInput.workModes,
|
|
4931
4956
|
comments: d.comments,
|
|
4932
4957
|
me,
|
|
4933
4958
|
roster: d.roster,
|
|
@@ -5424,6 +5449,18 @@ function renderEnvironment(i) {
|
|
|
5424
5449
|
"another (the digest prints other ships by name) \u2014 write its name, never a Ships row id."
|
|
5425
5450
|
);
|
|
5426
5451
|
}
|
|
5452
|
+
const seatIds = Object.entries(route.resolved?.seats ?? {}).filter(([, id]) => !!id);
|
|
5453
|
+
if (seatIds.length > 0) {
|
|
5454
|
+
lines.push(
|
|
5455
|
+
"",
|
|
5456
|
+
"## Seats on this ship",
|
|
5457
|
+
"",
|
|
5458
|
+
"The Crew row id of each role's seat on this ship, to write verbatim where a hand-off",
|
|
5459
|
+
"assigns the next lane (a ticket whose digest `mode` is `hybrid`):",
|
|
5460
|
+
"",
|
|
5461
|
+
...seatIds.map(([role, id]) => `- ${role}: \`${id}\``)
|
|
5462
|
+
);
|
|
5463
|
+
}
|
|
5427
5464
|
lines.push(
|
|
5428
5465
|
"",
|
|
5429
5466
|
"## What the columns are called here",
|
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
|