@tablation/crew 0.20.2 → 0.21.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 +49 -27
- package/package.json +1 -1
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
|
}
|
|
@@ -4529,7 +4550,7 @@ ${sorted(held).map((t) => `| ${displayKey2(t)} | ${stopOf(t)} |`).join("\n")}
|
|
|
4529
4550
|
"\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
4551
|
'\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
4552
|
"\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.
|
|
4553
|
+
...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
4554
|
"\n### Step 1 \u2014 open tickets that may be yours to act on\n",
|
|
4534
4555
|
"\n`fixed` tickets are deliberately absent: they belong to the QA lane.\n",
|
|
4535
4556
|
table(step1, i, header, row),
|
|
@@ -4717,13 +4738,13 @@ async function decideCycle(o) {
|
|
|
4717
4738
|
const { route, ship, state, emit } = o;
|
|
4718
4739
|
const tracker = new Tracker(route, ship);
|
|
4719
4740
|
emit.enter("poll");
|
|
4720
|
-
const [tickets, comments, crewRows, epicRows, ships,
|
|
4741
|
+
const [tickets, comments, crewRows, epicRows, ships, projectModes] = await Promise.all([
|
|
4721
4742
|
tracker.openTickets(),
|
|
4722
4743
|
tracker.comments(200),
|
|
4723
4744
|
tracker.crewRows(),
|
|
4724
4745
|
tracker.epicRows(),
|
|
4725
4746
|
tracker.shipRows(),
|
|
4726
|
-
tracker.
|
|
4747
|
+
tracker.projectWorkModes()
|
|
4727
4748
|
]);
|
|
4728
4749
|
emit.emit(`${tickets.length} open ticket(s), ${comments.length} comment(s)`, {
|
|
4729
4750
|
data: { tickets: tickets.length, comments: comments.length }
|
|
@@ -4755,6 +4776,7 @@ async function decideCycle(o) {
|
|
|
4755
4776
|
emit.enter("sweep");
|
|
4756
4777
|
const roster = buildRoster(configuredMembers(route), crewRows);
|
|
4757
4778
|
const holds2 = new Set(holdIds(roster));
|
|
4779
|
+
const workModes = { modes: projectModes, agentAssignees: agentAssigneeIds(crewRows, (id) => holds2.has(id)) };
|
|
4758
4780
|
const diag = sweepDiagnostics(tickets, info);
|
|
4759
4781
|
for (const d of diag.dangling) emit.warn(`dangling blocked_by reference, ignored: ${d}`);
|
|
4760
4782
|
if (diag.selfBlocked.length) {
|
|
@@ -4831,7 +4853,7 @@ async function decideCycle(o) {
|
|
|
4831
4853
|
await checkRepoDisks({ state, emit, route, ship });
|
|
4832
4854
|
const selectionInput = {
|
|
4833
4855
|
repoStop: repoStopFor(openShipAttention(state), route),
|
|
4834
|
-
|
|
4856
|
+
workModes,
|
|
4835
4857
|
tickets,
|
|
4836
4858
|
comments,
|
|
4837
4859
|
watermark,
|
|
@@ -4926,8 +4948,8 @@ function writeDigest(o, d, role, stateDir) {
|
|
|
4926
4948
|
}
|
|
4927
4949
|
return existingBranchForTicket(dir, repoFor(dir), t, role);
|
|
4928
4950
|
},
|
|
4929
|
-
tickets: role === "triage" ? d.tickets
|
|
4930
|
-
|
|
4951
|
+
tickets: role === "triage" ? triageSlice(d.tickets, o.route.resolved?.seats.triage, d.selectionInput.workModes) : withinWorkMode(sliceFor(d.tickets, role), d.selectionInput.workModes),
|
|
4952
|
+
workModes: d.selectionInput.workModes,
|
|
4931
4953
|
comments: d.comments,
|
|
4932
4954
|
me,
|
|
4933
4955
|
roster: d.roster,
|
package/package.json
CHANGED