@tablation/crew 0.24.0 → 0.25.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/crew.example.yaml +3 -0
- package/dist/cli.js +86 -15
- package/package.json +1 -1
- package/prompts/default/personas/common.md +36 -3
- package/prompts/default/personas/lane-design.md +6 -0
- package/prompts/default/personas/lane-dev.md +8 -1
- package/prompts/default/personas/lane-qa.md +7 -0
- package/prompts/dev-qa/personas/common.md +36 -3
- package/prompts/dev-qa/personas/lane-dev.md +8 -1
- package/prompts/dev-qa/personas/lane-qa.md +7 -0
package/crew.example.yaml
CHANGED
|
@@ -100,6 +100,9 @@ routes:
|
|
|
100
100
|
# Only needed for a self-hosted tracker.
|
|
101
101
|
# baseUrl: "https://app.tablation.com"
|
|
102
102
|
apiKey: "REPLACE_ME" # or omit if `ship.apiKey` covers this workspace too
|
|
103
|
+
# With no apiKey (and no apiKeyFile/apiKeyVar) crew reads CREW_API_KEY from
|
|
104
|
+
# its process environment. Under launchd that must be set in the job's own
|
|
105
|
+
# environment, not exported from an interactive shell.
|
|
103
106
|
|
|
104
107
|
# Where release state should be SHOWN is a fact about this machine and the
|
|
105
108
|
# person watching it, not about the code — so unlike every other hook,
|
package/dist/cli.js
CHANGED
|
@@ -1065,8 +1065,10 @@ function resolveApiKey(r) {
|
|
|
1065
1065
|
if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
1066
1066
|
}
|
|
1067
1067
|
}
|
|
1068
|
+
const fromEnv = process.env.CREW_API_KEY?.trim();
|
|
1069
|
+
if (fromEnv) return fromEnv;
|
|
1068
1070
|
throw new ConfigError(
|
|
1069
|
-
`route "${r.route}": no API key (
|
|
1071
|
+
`route "${r.route}": no API key (looked for, in order: apiKey; ${r.apiKeyVar ?? "VAR"} in ${r.apiKeyFile ?? "<unset>"}; CREW_API_KEY in the environment)`
|
|
1070
1072
|
);
|
|
1071
1073
|
}
|
|
1072
1074
|
var KEYCHAIN_ELIGIBLE_ROUTES = /* @__PURE__ */ new WeakSet();
|
|
@@ -1475,6 +1477,7 @@ var State = class _State {
|
|
|
1475
1477
|
const failed = `.deploy-failed-sha-${suffix}`;
|
|
1476
1478
|
const gateFailed = `.test-gate-failed-${suffix}`;
|
|
1477
1479
|
const gateReported = `.test-gate-reported-${suffix}`;
|
|
1480
|
+
const heldReported = `.release-held-${suffix}`;
|
|
1478
1481
|
const read = (f) => {
|
|
1479
1482
|
try {
|
|
1480
1483
|
return readFileSync3(this.path(f), "utf8").trim() || null;
|
|
@@ -1525,6 +1528,20 @@ var State = class _State {
|
|
|
1525
1528
|
rmSync(this.path(gateFailed), { force: true });
|
|
1526
1529
|
},
|
|
1527
1530
|
/** Whether the board was already told about this head's red gate. */
|
|
1531
|
+
heldReported: () => {
|
|
1532
|
+
try {
|
|
1533
|
+
return readFileSync3(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
|
|
1534
|
+
} catch {
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1537
|
+
},
|
|
1538
|
+
noteHeldReported: (signature) => {
|
|
1539
|
+
writeFileSync(this.path(heldReported), `${signature}
|
|
1540
|
+
`);
|
|
1541
|
+
},
|
|
1542
|
+
clearHeldReported: () => {
|
|
1543
|
+
rmSync(this.path(heldReported), { force: true });
|
|
1544
|
+
},
|
|
1528
1545
|
testGateReported: (sha) => read(gateReported) === sha,
|
|
1529
1546
|
noteTestGateReported: (sha) => {
|
|
1530
1547
|
writeFileSync(this.path(gateReported), `${sha}
|
|
@@ -3934,15 +3951,18 @@ function blockerInfoMap(tickets, extra = []) {
|
|
|
3934
3951
|
}
|
|
3935
3952
|
function computeBlockedIds(tickets, info) {
|
|
3936
3953
|
const blocked = /* @__PURE__ */ new Set();
|
|
3937
|
-
for (const t of tickets)
|
|
3938
|
-
const unresolved = (t.blocked_by ?? []).some((id) => {
|
|
3939
|
-
const status2 = info[id]?.status;
|
|
3940
|
-
return status2 !== void 0 && !RESOLVED_STATUSES.has(status2);
|
|
3941
|
-
});
|
|
3942
|
-
if (unresolved) blocked.add(t.id);
|
|
3943
|
-
}
|
|
3954
|
+
for (const t of tickets) if (unresolvedBlockers(t, info).length > 0) blocked.add(t.id);
|
|
3944
3955
|
return blocked;
|
|
3945
3956
|
}
|
|
3957
|
+
function unresolvedBlockers(t, info) {
|
|
3958
|
+
return (t.blocked_by ?? []).filter((id) => {
|
|
3959
|
+
const status2 = info[id]?.status;
|
|
3960
|
+
return status2 !== void 0 && !RESOLVED_STATUSES.has(status2);
|
|
3961
|
+
});
|
|
3962
|
+
}
|
|
3963
|
+
function describeUnresolved(t, info) {
|
|
3964
|
+
return unresolvedBlockers(t, info).map((id) => `${info[id]?.issue_tag ?? info[id]?.issue_id ?? "?"} (${info[id]?.status ?? "unknown"})`).join(", ");
|
|
3965
|
+
}
|
|
3946
3966
|
function sweepDiagnostics(tickets, info) {
|
|
3947
3967
|
const dangling = [];
|
|
3948
3968
|
const selfBlocked = [];
|
|
@@ -4258,9 +4278,12 @@ function buildingRoleHasWork(role, i) {
|
|
|
4258
4278
|
}
|
|
4259
4279
|
return { hasWork: false, reason: "nothing startable and no new comments" };
|
|
4260
4280
|
}
|
|
4281
|
+
function qaCandidates(i) {
|
|
4282
|
+
return withinWorkMode(notHeldByOtherShip(qaSlice(i.tickets), i), i.workModes).filter((t) => !isHeld(t, i.holds) && !i.blocked.has(t.id));
|
|
4283
|
+
}
|
|
4261
4284
|
function qaRoleHasWork(i) {
|
|
4262
4285
|
if (!i.seats.qa) return { hasWork: false, reason: "this ship does not crew that role" };
|
|
4263
|
-
const n =
|
|
4286
|
+
const n = qaCandidates(i).length;
|
|
4264
4287
|
return n > 0 ? { hasWork: true, reason: `${n} ticket(s) awaiting or in verification` } : { hasWork: false, reason: "nothing awaiting verification" };
|
|
4265
4288
|
}
|
|
4266
4289
|
function triageRoleHasWork(i) {
|
|
@@ -4276,7 +4299,7 @@ function roleHasWork(role, i) {
|
|
|
4276
4299
|
return buildingRoleHasWork(role, i);
|
|
4277
4300
|
}
|
|
4278
4301
|
function roleCandidates(role, i) {
|
|
4279
|
-
if (role === "qa") return
|
|
4302
|
+
if (role === "qa") return qaCandidates(i);
|
|
4280
4303
|
if (role === "triage") return triageSlice(i.tickets, i.seats.triage, i.workModes).filter((t) => !isHeld(t, i.holds));
|
|
4281
4304
|
if (role === "pair") return [];
|
|
4282
4305
|
const me = i.seats[role];
|
|
@@ -4610,15 +4633,30 @@ function qaDigest(i) {
|
|
|
4610
4633
|
const repo = i.dirFor ? dir ?? "**NO CHECKOUT**" : "";
|
|
4611
4634
|
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"} |`;
|
|
4612
4635
|
};
|
|
4636
|
+
const waiting = i.tickets.filter((t) => (t.status === "qa" || t.status === "fixed") && i.blocked.has(t.id));
|
|
4637
|
+
const takeable = (status2) => i.tickets.filter((t) => t.status === status2 && !i.blocked.has(t.id));
|
|
4638
|
+
const waitingTable = waiting.length === 0 ? "" : `
|
|
4639
|
+
### Waiting on blockers \u2014 not yours to verify yet
|
|
4640
|
+
|
|
4641
|
+
A \`fixed\` or \`qa\` ticket whose \`Blocked by\` names something not yet resolved
|
|
4642
|
+
(\`verified\`, \`closed_deployed\`, \`closed_wont_fix\` or \`closed_duplicate\`) is held out of
|
|
4643
|
+
verification: its change needs the blocker released first. It becomes yours on
|
|
4644
|
+
the first poll after the last blocker resolves. No one needs to change the ticket.
|
|
4645
|
+
|
|
4646
|
+
| ticket | status | blocked by |
|
|
4647
|
+
|---|---|---|
|
|
4648
|
+
${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).join("\n")}
|
|
4649
|
+
`;
|
|
4613
4650
|
return stream([
|
|
4614
4651
|
"## Current queue \u2014 built for you by the poll\n",
|
|
4615
4652
|
'\nAlready filtered to your lane: every ticket at `qa` ("Verification" \u2014\nyours, unfinished) or `fixed` (nobody has checked it yet), from both\nbuilding lanes. Ticket bodies are deliberately omitted: fetch the full\nrecord and the comments of the one ticket you actually pick up. **Do not\nre-fetch the whole tracker.** This digest comes from the same API call the\npoll just made, moments ago.\n',
|
|
4616
4653
|
'\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',
|
|
4617
4654
|
"\n**`repo` is the checkout the ticket's work happens in** \u2014 an area spans\nseveral repositories, so the worktree you verify in sits beside THAT\ndirectory, not beside whichever one this session started in. A ticket\nmarked **NO CHECKOUT** is not yours: this ship has no clone of its\nrepository, and another ship may serve it.\n\n**`branch` is that repository's own branch for the ticket**, found there\nrather than derived. **`<name> (on origin only)`** means another ship built it\nand pushed it: it is testable \u2014 cut a worktree from the remote branch\n(`git worktree add <worktree> <name>`). **MISSING** means the branch is gone\nlocally AND on the remote, so there is nothing left to verify \u2014 say so on the ticket. A `\u2014` means the branch\ncould not be looked for at all, because the repo has no checkout here.\n\n**`worktree` is where that branch should be checked out**, `<repo>/../<worktree>`\n\u2014 rendered the same way the dev/design digest computes it, so you land in\nthe same directory the building lane used.\n",
|
|
4618
4655
|
"\n### Still in verification \u2014 yours, unfinished (take these first)\n",
|
|
4619
|
-
table(
|
|
4656
|
+
table(takeable("qa"), i, header, row),
|
|
4620
4657
|
"\n### Awaiting verification, in pick order\n",
|
|
4621
|
-
table(
|
|
4658
|
+
table(takeable("fixed"), i, header, row),
|
|
4659
|
+
...waitingTable ? [waitingTable] : []
|
|
4622
4660
|
]);
|
|
4623
4661
|
}
|
|
4624
4662
|
|
|
@@ -6562,10 +6600,16 @@ function checkGuards(cwd, base = "main") {
|
|
|
6562
6600
|
detail: `working tree has ${changes.length} change(s)${untrackedNote}: ${names}${more}`
|
|
6563
6601
|
};
|
|
6564
6602
|
}
|
|
6603
|
+
function heldByBlockers(tickets, verifiedStatus, info) {
|
|
6604
|
+
if (!info) return [];
|
|
6605
|
+
return tickets.filter((t) => t.status === verifiedStatus && unresolvedBlockers(t, info).length > 0).map((ticket) => ({ ticket, blockers: describeUnresolved(ticket, info) }));
|
|
6606
|
+
}
|
|
6607
|
+
var describeHeld = (held) => held.map((h) => `${h.ticket.issue_tag ?? h.ticket.issue_id} held: blocked by ${h.blockers}`);
|
|
6565
6608
|
var CHANGELOG_LINE = /^Changelog:[ \t]*(.*)$/gm;
|
|
6566
6609
|
var BUMP_LINE = /^Bump:[ \t]*(major|minor|patch)\b/gim;
|
|
6567
|
-
function planMerge(cwd, tickets, contract, lastReleased, branchFor = (t) => branchForIssue(cwd, t.issue_id), base = "main") {
|
|
6568
|
-
const
|
|
6610
|
+
function planMerge(cwd, tickets, contract, lastReleased, branchFor = (t) => branchForIssue(cwd, t.issue_id), base = "main", blockerInfo) {
|
|
6611
|
+
const held = new Set(heldByBlockers(tickets, contract.statuses.verified, blockerInfo).map((h) => h.ticket.id));
|
|
6612
|
+
const verified = tickets.filter((t) => t.status === contract.statuses.verified && !held.has(t.id)).sort((a, b) => Number.parseInt(a.issue_id.replace(/^\D+/, ""), 10) - Number.parseInt(b.issue_id.replace(/^\D+/, ""), 10));
|
|
6569
6613
|
return verified.map((ticket) => {
|
|
6570
6614
|
const branch = branchFor(ticket);
|
|
6571
6615
|
if (!branch) {
|
|
@@ -6683,12 +6727,13 @@ function decideRelease(cwd, tickets, contract, opts = {}) {
|
|
|
6683
6727
|
const lastTag = latestReleaseTag(cwd, pattern, "HEAD");
|
|
6684
6728
|
const lastReleased = lastTag ? tagCommit(cwd, lastTag) : null;
|
|
6685
6729
|
const head = resolve2(cwd, "HEAD") ?? "";
|
|
6686
|
-
const merges = block ? [] : planMerge(cwd, tickets, contract, lastReleased, opts.branchFor ?? ((t) => branchForIssue(cwd, t.issue_id)), base);
|
|
6730
|
+
const merges = block ? [] : planMerge(cwd, tickets, contract, lastReleased, opts.branchFor ?? ((t) => branchForIssue(cwd, t.issue_id)), base, opts.blockerInfo);
|
|
6687
6731
|
const unreleasedCommits = lastReleased ? countCommits(cwd, `${lastReleased}..HEAD`) : 0;
|
|
6688
6732
|
const unreleasedWork = lastReleased ? (gitOk(cwd, ["log", "--format=%s", `${lastReleased}..HEAD`]) ?? "").split("\n").filter((l) => l && !RELEASE_COMMIT_SUBJECT.test(l)).length : 0;
|
|
6689
6733
|
return {
|
|
6690
6734
|
block,
|
|
6691
6735
|
merges,
|
|
6736
|
+
held: heldByBlockers(tickets, contract.statuses.verified, opts.blockerInfo),
|
|
6692
6737
|
head,
|
|
6693
6738
|
lastTag,
|
|
6694
6739
|
lastReleased,
|
|
@@ -7293,6 +7338,17 @@ function openReleaseCheckout(o) {
|
|
|
7293
7338
|
}
|
|
7294
7339
|
return { dir, branch, close };
|
|
7295
7340
|
}
|
|
7341
|
+
function reportHeld(o, held) {
|
|
7342
|
+
if (held.length === 0) {
|
|
7343
|
+
o.state?.clearHeldReported?.();
|
|
7344
|
+
return;
|
|
7345
|
+
}
|
|
7346
|
+
const lines = describeHeld(held);
|
|
7347
|
+
const signature = lines.join("\n");
|
|
7348
|
+
if (o.dryRun || o.state?.heldReported?.() === signature) return;
|
|
7349
|
+
for (const line of lines) o.emit.emit(line, { step: "release" });
|
|
7350
|
+
o.state?.noteHeldReported?.(signature);
|
|
7351
|
+
}
|
|
7296
7352
|
async function runRelease(o) {
|
|
7297
7353
|
const scoped = (msg) => o.scope ? `${o.scope}: ${msg}` : msg;
|
|
7298
7354
|
const tagPattern = o.repo.release.tagPattern ?? "v*";
|
|
@@ -7301,11 +7357,13 @@ async function runRelease(o) {
|
|
|
7301
7357
|
const decision = decideRelease(o.cwd, o.tickets, o.contract, {
|
|
7302
7358
|
tagPattern,
|
|
7303
7359
|
base: o.repo.branch.base,
|
|
7360
|
+
blockerInfo: o.blockerInfo,
|
|
7304
7361
|
// CREW-1364: a branch another ship built and pushed at `fixed` exists
|
|
7305
7362
|
// here only on the remote. Materialize it so the squash below has a ref.
|
|
7306
7363
|
// A dry run only looks.
|
|
7307
7364
|
branchFor: (t) => o.dryRun ? locateBranchForTicket(o.cwd, o.repo, t)?.branch ?? null : materializeBranchForTicket(o.cwd, o.repo, t)
|
|
7308
7365
|
});
|
|
7366
|
+
reportHeld(o, decision.held);
|
|
7309
7367
|
if (!fresh.ok) {
|
|
7310
7368
|
o.emit.error(scoped(`refusing to release \u2014 ${fresh.why}; a person has to reconcile the two`));
|
|
7311
7369
|
return { merged: [], conflicts: [], deployed: false, stopped: fresh.why, decision };
|
|
@@ -11166,11 +11224,13 @@ ${changed2.map((c) => ` - ${c}
|
|
|
11166
11224
|
releaseModes
|
|
11167
11225
|
);
|
|
11168
11226
|
reportUnplaceable(c.route, unplaceable, tracker.contract.statuses.verified, remit);
|
|
11227
|
+
const blockerInfo = blockerInfoMap(all, await tracker.ticketsByIds(missingBlockerIds(all)));
|
|
11169
11228
|
const outcome = await runRelease({
|
|
11170
11229
|
cwd: target.dir,
|
|
11171
11230
|
repo,
|
|
11172
11231
|
contract: tracker.contract,
|
|
11173
11232
|
tickets,
|
|
11233
|
+
blockerInfo,
|
|
11174
11234
|
emit: remit,
|
|
11175
11235
|
scope,
|
|
11176
11236
|
state: state.release(scope),
|
|
@@ -13187,6 +13247,17 @@ watermark: ${state.watermark()}
|
|
|
13187
13247
|
`);
|
|
13188
13248
|
process.stdout.write(`releaser: ${r.name} \u2014 ${await releaserLine(statusTracker, route.route, r.name, r.config.release)}
|
|
13189
13249
|
`);
|
|
13250
|
+
}
|
|
13251
|
+
try {
|
|
13252
|
+
const open = await statusTracker.openTickets();
|
|
13253
|
+
const held = heldByBlockers(
|
|
13254
|
+
open,
|
|
13255
|
+
statusTracker.contract.statuses.verified,
|
|
13256
|
+
blockerInfoMap(open, await statusTracker.ticketsByIds(missingBlockerIds(open)))
|
|
13257
|
+
);
|
|
13258
|
+
for (const line of describeHeld(held)) process.stdout.write(`release: ${line}
|
|
13259
|
+
`);
|
|
13260
|
+
} catch {
|
|
13190
13261
|
}
|
|
13191
13262
|
if (waiting) {
|
|
13192
13263
|
process.stdout.write(
|
package/package.json
CHANGED
|
@@ -684,9 +684,42 @@ post. They record who filed the ticket and from where. The block lists a column
|
|
|
684
684
|
only when this workspace has it, so a column it does not name is not yours to set.
|
|
685
685
|
|
|
686
686
|
Name the ticket you were working when you noticed it in the new ticket's
|
|
687
|
-
description. Only
|
|
688
|
-
|
|
689
|
-
|
|
687
|
+
description. Only add the new ticket to **your own ticket's** `blocked_by`
|
|
688
|
+
(the column named `blocked by` in the Environment section) if your own
|
|
689
|
+
ticket genuinely cannot proceed without the new one being fixed first —
|
|
690
|
+
merely having noticed it nearby is not a dependency. The dependency always
|
|
691
|
+
points from the ticket that waits to the ticket it waits on, never the
|
|
692
|
+
other way round.
|
|
693
|
+
|
|
694
|
+
### Work your ticket needs in another repository
|
|
695
|
+
|
|
696
|
+
Out-of-scope breakage and **in-scope work that lives somewhere else** are
|
|
697
|
+
different things. If your ticket's acceptance depends on a change in another
|
|
698
|
+
repository (or on a change this repository cannot make), do not ship a
|
|
699
|
+
stand-in for the missing piece and hand the ticket off. A stand-in whose UI,
|
|
700
|
+
docs or hints promise behaviour the dependency has not delivered is a defect
|
|
701
|
+
that QA passes and the release phase ships. Instead:
|
|
702
|
+
|
|
703
|
+
1. File that work as its own ticket in the owning repository's project (the
|
|
704
|
+
Environment section names the projects and repos; if the owning repo is
|
|
705
|
+
not listed there, file it against the project and say which repo in the
|
|
706
|
+
description), setting `repo_id`, `project_id`, `filed_by_id` and
|
|
707
|
+
`ship_id` as in the filing block, and naming your ticket in its
|
|
708
|
+
description.
|
|
709
|
+
2. Add the new ticket's id to **your own ticket's** `blocked_by`.
|
|
710
|
+
3. Post a comment on your ticket naming each dependency and what it must
|
|
711
|
+
deliver.
|
|
712
|
+
4. Set your ticket back to `accepted`. The poll's sweep parks it at
|
|
713
|
+
`blocked` and restores it to `accepted` once every blocker resolves.
|
|
714
|
+
In a `hybrid` project keep `assignee_id` as your own seat (an unassigned
|
|
715
|
+
hybrid ticket is off limits to every lane); in an `automatic` project
|
|
716
|
+
clear it as for any other re-queue.
|
|
717
|
+
5. Leave the branch and worktree in place, committed and pushed if they hold
|
|
718
|
+
work, so the lane that resumes the ticket continues from them.
|
|
719
|
+
|
|
720
|
+
Hand a ticket off as `fixed` only when the branch works against
|
|
721
|
+
dependencies that are already released. A hand-off comment that says "worth
|
|
722
|
+
separate tickets" for work the ticket needs is a hand-off that is not ready.
|
|
690
723
|
|
|
691
724
|
## Step 4 — you never merge, and you never deploy
|
|
692
725
|
|
|
@@ -82,6 +82,12 @@ stopping for this run, do the following instead:
|
|
|
82
82
|
is actually blocked on the operator's input, not as a substitute for
|
|
83
83
|
the ordinary ending above.
|
|
84
84
|
|
|
85
|
+
Before you post, check the comment: it must not name needed-but-unfiled
|
|
86
|
+
work. A design that depends on a change in another repository files that
|
|
87
|
+
ticket and links it in your ticket's `blocked_by` (see "Work your ticket
|
|
88
|
+
needs in another repository"), rather than listing it as a loose
|
|
89
|
+
follow-up.
|
|
90
|
+
|
|
85
91
|
Either way, stop this ticket's work for this run once the flag is set and
|
|
86
92
|
the comment posted. The operator reviews what you posted and takes it
|
|
87
93
|
from there:
|
|
@@ -24,4 +24,11 @@ release phase merges and ships it) or hands it back to you as `in_progress`
|
|
|
24
24
|
with a comment saying what still fails. Take that bounce seriously — it is
|
|
25
25
|
the only reading your work gets before it is deployed.
|
|
26
26
|
|
|
27
|
-
**
|
|
27
|
+
**Before you hand a ticket to QA, check the hand-off comment.** It must not
|
|
28
|
+
name needed-but-unfiled work: anything the ticket's acceptance depends on in
|
|
29
|
+
another repository is a filed ticket in your ticket's `blocked_by` (see "Work
|
|
30
|
+
your ticket needs in another repository"), not a line saying "worth separate
|
|
31
|
+
tickets". If it would name such work, the ticket goes back to `accepted`
|
|
32
|
+
instead of forward to `fixed`.
|
|
33
|
+
|
|
34
|
+
**No extra steps.** Otherwise follow the shared policy as written.
|
|
@@ -117,6 +117,13 @@ human review, so "probably fine" is not a pass.
|
|
|
117
117
|
Missing or stale docs are a reason to bounce the ticket back to the dev
|
|
118
118
|
seat, with a comment naming the page that needs the change. If the
|
|
119
119
|
repository names no such location, skip this check.
|
|
120
|
+
7b. **Check the hand-off for needed-but-unfiled work.** If the dev or design
|
|
121
|
+
comment names work that is needed in another repository (or that this
|
|
122
|
+
repository cannot do) and no ticket for it exists in the ticket's
|
|
123
|
+
`blocked_by`, that is a bounce: say so in your comment and ask for the
|
|
124
|
+
ticket and the `blocked_by` link. A shipped stand-in whose UI, docs or
|
|
125
|
+
hints promise behaviour the dependency has not yet delivered is a defect
|
|
126
|
+
even if every test passes. Bounce it like any other failure (step 8).
|
|
120
127
|
7a. **Ask whether the fix is covered.** A behaviour change with no test that
|
|
121
128
|
would catch its regression is worth naming in your comment; whether it
|
|
122
129
|
is worth bouncing the ticket over is your judgement, and depends on how
|
|
@@ -666,9 +666,42 @@ post. They record who filed the ticket and from where. The block lists a column
|
|
|
666
666
|
only when this workspace has it, so a column it does not name is not yours to set.
|
|
667
667
|
|
|
668
668
|
Name the ticket you were working when you noticed it in the new ticket's
|
|
669
|
-
description. Only
|
|
670
|
-
|
|
671
|
-
|
|
669
|
+
description. Only add the new ticket to **your own ticket's** `blocked_by`
|
|
670
|
+
(the column named `blocked by` in the Environment section) if your own
|
|
671
|
+
ticket genuinely cannot proceed without the new one being fixed first —
|
|
672
|
+
merely having noticed it nearby is not a dependency. The dependency always
|
|
673
|
+
points from the ticket that waits to the ticket it waits on, never the
|
|
674
|
+
other way round.
|
|
675
|
+
|
|
676
|
+
### Work your ticket needs in another repository
|
|
677
|
+
|
|
678
|
+
Out-of-scope breakage and **in-scope work that lives somewhere else** are
|
|
679
|
+
different things. If your ticket's acceptance depends on a change in another
|
|
680
|
+
repository (or on a change this repository cannot make), do not ship a
|
|
681
|
+
stand-in for the missing piece and hand the ticket off. A stand-in whose UI,
|
|
682
|
+
docs or hints promise behaviour the dependency has not delivered is a defect
|
|
683
|
+
that QA passes and the release phase ships. Instead:
|
|
684
|
+
|
|
685
|
+
1. File that work as its own ticket in the owning repository's project (the
|
|
686
|
+
Environment section names the projects and repos; if the owning repo is
|
|
687
|
+
not listed there, file it against the project and say which repo in the
|
|
688
|
+
description), setting `repo_id`, `project_id`, `filed_by_id` and
|
|
689
|
+
`ship_id` as in the filing block, and naming your ticket in its
|
|
690
|
+
description.
|
|
691
|
+
2. Add the new ticket's id to **your own ticket's** `blocked_by`.
|
|
692
|
+
3. Post a comment on your ticket naming each dependency and what it must
|
|
693
|
+
deliver.
|
|
694
|
+
4. Set your ticket back to `accepted`. The poll's sweep parks it at
|
|
695
|
+
`blocked` and restores it to `accepted` once every blocker resolves.
|
|
696
|
+
In a `hybrid` project keep `assignee_id` as your own seat (an unassigned
|
|
697
|
+
hybrid ticket is off limits to every lane); in an `automatic` project
|
|
698
|
+
clear it as for any other re-queue.
|
|
699
|
+
5. Leave the branch and worktree in place, committed and pushed if they hold
|
|
700
|
+
work, so the lane that resumes the ticket continues from them.
|
|
701
|
+
|
|
702
|
+
Hand a ticket off as `fixed` only when the branch works against
|
|
703
|
+
dependencies that are already released. A hand-off comment that says "worth
|
|
704
|
+
separate tickets" for work the ticket needs is a hand-off that is not ready.
|
|
672
705
|
|
|
673
706
|
## Step 4 — you never merge, and you never deploy
|
|
674
707
|
|
|
@@ -23,4 +23,11 @@ release phase merges and ships it) or hands it back to you as `in_progress`
|
|
|
23
23
|
with a comment saying what still fails. Take that bounce seriously — it is
|
|
24
24
|
the only reading your work gets before it is deployed.
|
|
25
25
|
|
|
26
|
-
**
|
|
26
|
+
**Before you hand a ticket to QA, check the hand-off comment.** It must not
|
|
27
|
+
name needed-but-unfiled work: anything the ticket's acceptance depends on in
|
|
28
|
+
another repository is a filed ticket in your ticket's `blocked_by` (see "Work
|
|
29
|
+
your ticket needs in another repository"), not a line saying "worth separate
|
|
30
|
+
tickets". If it would name such work, the ticket goes back to `accepted`
|
|
31
|
+
instead of forward to `fixed`.
|
|
32
|
+
|
|
33
|
+
**No extra steps.** Otherwise follow the shared policy as written.
|
|
@@ -104,6 +104,13 @@ human review, so "probably fine" is not a pass.
|
|
|
104
104
|
Missing or stale docs are a reason to bounce the ticket back to the dev
|
|
105
105
|
seat, with a comment naming the page that needs the change. If the
|
|
106
106
|
repository names no such location, skip this check.
|
|
107
|
+
7b. **Check the hand-off for needed-but-unfiled work.** If the dev or design
|
|
108
|
+
comment names work that is needed in another repository (or that this
|
|
109
|
+
repository cannot do) and no ticket for it exists in the ticket's
|
|
110
|
+
`blocked_by`, that is a bounce: say so in your comment and ask for the
|
|
111
|
+
ticket and the `blocked_by` link. A shipped stand-in whose UI, docs or
|
|
112
|
+
hints promise behaviour the dependency has not yet delivered is a defect
|
|
113
|
+
even if every test passes. Bounce it like any other failure (step 8).
|
|
107
114
|
7a. **Ask whether the fix is covered.** A behaviour change with no test that
|
|
108
115
|
would catch its regression is worth naming in your comment; whether it
|
|
109
116
|
is worth bouncing the ticket over is your judgement, and depends on how
|