@tablation/crew 0.78.1 → 0.79.1
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
|
@@ -7131,6 +7131,28 @@ async function finishRunWorktree(dir, cfg, branch) {
|
|
|
7131
7131
|
// src/handoff-guard.ts
|
|
7132
7132
|
var HANDOFF_BOUNCED_MARKER = calloutHeader("WARNING", "Hand-off Bounced");
|
|
7133
7133
|
var pushedEventBody = (sha, remote, branch) => `pushed ${sha} to ${remote}/${branch}`;
|
|
7134
|
+
function acceptanceLines(description) {
|
|
7135
|
+
if (typeof description !== "string") return [];
|
|
7136
|
+
const out = [];
|
|
7137
|
+
let inside = false;
|
|
7138
|
+
for (const raw of description.split("\n")) {
|
|
7139
|
+
const heading = /^#{1,6}\s+(.*?)\s*$/.exec(raw);
|
|
7140
|
+
if (heading) {
|
|
7141
|
+
inside = /^acceptance\b/i.test(heading[1]);
|
|
7142
|
+
continue;
|
|
7143
|
+
}
|
|
7144
|
+
if (!inside) continue;
|
|
7145
|
+
const bullet = /^\s*(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.+?)\s*$/.exec(raw);
|
|
7146
|
+
if (bullet) out.push(bullet[1]);
|
|
7147
|
+
}
|
|
7148
|
+
return out;
|
|
7149
|
+
}
|
|
7150
|
+
var flat = (s) => s.replace(/[`*_>]/g, "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
7151
|
+
function unquotedAcceptance(lines, comment) {
|
|
7152
|
+
const body = flat(comment);
|
|
7153
|
+
return lines.filter((l) => !body.includes(flat(l)));
|
|
7154
|
+
}
|
|
7155
|
+
var ACCEPTANCE_BOUNCED_MARKER = calloutHeader("WARNING", "Acceptance Checklist Missing");
|
|
7134
7156
|
function remoteTip(dir, remote, branch) {
|
|
7135
7157
|
const out = gitOk(dir, ["ls-remote", "--heads", remote, `refs/heads/${branch}`]);
|
|
7136
7158
|
if (out === null) return null;
|
|
@@ -7206,6 +7228,27 @@ async function takenOverSince(i, ticketRow) {
|
|
|
7206
7228
|
return !!by && by !== i.shipName;
|
|
7207
7229
|
});
|
|
7208
7230
|
}
|
|
7231
|
+
async function missingAcceptance(i, t) {
|
|
7232
|
+
const lines = acceptanceLines(t.description);
|
|
7233
|
+
if (lines.length === 0 || !i.writer.comments) return null;
|
|
7234
|
+
const cc = i.contract.comments;
|
|
7235
|
+
const mine = (await i.writer.comments()).map((c) => c).filter((r) => r[cc.parent] === t.id && r[cc.author] === i.seatId).sort((a, b) => String(a[cc.createdAt] ?? "").localeCompare(String(b[cc.createdAt] ?? "")));
|
|
7236
|
+
const last = mine.at(-1);
|
|
7237
|
+
const missing = unquotedAcceptance(lines, last ? String(last[cc.body] ?? "") : "");
|
|
7238
|
+
return missing.length > 0 ? missing : null;
|
|
7239
|
+
}
|
|
7240
|
+
function acceptanceBounceComment(missing, shipName2) {
|
|
7241
|
+
return `${ACCEPTANCE_BOUNCED_MARKER}
|
|
7242
|
+
|
|
7243
|
+
**Hand-off bounced: the hand-off comment does not quote every line of the ticket's Acceptance section.**
|
|
7244
|
+
|
|
7245
|
+
${shipName2 ? `Ship: ${shipName2}
|
|
7246
|
+
|
|
7247
|
+
` : ""}Not quoted verbatim:
|
|
7248
|
+
${missing.map((l) => `- ${l}`).join("\n")}
|
|
7249
|
+
|
|
7250
|
+
Set back to \`in_progress\`. Post a comment that lists every Acceptance line verbatim, each followed by the test, command or manual check that exercised it in this run and its result (a line you could not exercise is listed with the reason, never omitted), then set \`fixed\` again. The branch is already pushed.`;
|
|
7251
|
+
}
|
|
7209
7252
|
async function enforceHandoff(i) {
|
|
7210
7253
|
try {
|
|
7211
7254
|
const t = await i.writer.ticket(i.ticketId);
|
|
@@ -7221,6 +7264,26 @@ async function enforceHandoff(i) {
|
|
|
7221
7264
|
i.log.warn(`${t.issue_id} is at ${t.status} but the push was not checked: ${v.why}`, log);
|
|
7222
7265
|
return { kind: "unverified", why: v.why };
|
|
7223
7266
|
}
|
|
7267
|
+
if (v.kind === "pushed" && !claimed) {
|
|
7268
|
+
const missing = await missingAcceptance(i, t);
|
|
7269
|
+
if (missing) {
|
|
7270
|
+
const heldNow = i.hold ? t[i.hold.column] : void 0;
|
|
7271
|
+
if (i.hold && heldNow && heldNow !== i.hold.shipId || await takenOverSince(i, t.id)) {
|
|
7272
|
+
i.log.warn(`${t.issue_id} is held by another ship now \u2014 the acceptance check is stale, left alone`, log);
|
|
7273
|
+
return { kind: "unverified", why: "ticket is held by another ship" };
|
|
7274
|
+
}
|
|
7275
|
+
await i.writer.updateTicket(t.id, {
|
|
7276
|
+
status: i.contract.statuses.building,
|
|
7277
|
+
...t.assignee_id ? {} : { assignee_id: i.seatId },
|
|
7278
|
+
...i.hold && !heldNow ? { [i.hold.column]: i.hold.shipId } : {}
|
|
7279
|
+
});
|
|
7280
|
+
await i.writer.postEvent(t.id, acceptanceBounceComment(missing, i.shipName), i.seatId, "bounced").catch(() => {
|
|
7281
|
+
i.log.warn("bounced, but the note failed to post", log);
|
|
7282
|
+
});
|
|
7283
|
+
i.log.warn(`${t.issue_id} bounced back to ${i.contract.statuses.building}: hand-off comment lacks the acceptance checklist`, log);
|
|
7284
|
+
return { kind: "bounced" };
|
|
7285
|
+
}
|
|
7286
|
+
}
|
|
7224
7287
|
if (v.kind === "pushed") {
|
|
7225
7288
|
await i.writer.postEvent(t.id, pushedEventBody(v.sha, v.remote, v.branch), i.seatId, "pushed");
|
|
7226
7289
|
i.log.emit(`${t.issue_id} pushed at ${v.sha.slice(0, 7)} \u2014 handed on to QA`, log);
|
|
@@ -11325,14 +11388,14 @@ function dedupeChangelogEntries(entries, ticket) {
|
|
|
11325
11388
|
return out;
|
|
11326
11389
|
}
|
|
11327
11390
|
var BUMP_LINE = /^Bump:[ \t]*(major|minor|patch)\b/gim;
|
|
11328
|
-
function planMerge(cwd, tickets, contract, lastReleased, branchFor = (t) => branchForIssue(cwd, t.issue_id), base = "main", blockerInfo) {
|
|
11391
|
+
function planMerge(cwd, tickets, contract, lastReleased, branchFor = (t) => branchForIssue(cwd, t.issue_id), base = "main", blockerInfo, ref = "HEAD") {
|
|
11329
11392
|
const held = new Set(heldByBlockers(tickets, contract.statuses.verified, blockerInfo).map((h) => h.ticket.id));
|
|
11330
11393
|
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));
|
|
11331
11394
|
return verified.map((ticket) => {
|
|
11332
11395
|
const branch = branchFor(ticket);
|
|
11333
11396
|
if (!branch) {
|
|
11334
11397
|
const keys = referenceKeys(ticket);
|
|
11335
|
-
const unreleased = lastReleased ? findKeyInRange(cwd, keys, `${lastReleased}
|
|
11398
|
+
const unreleased = lastReleased ? findKeyInRange(cwd, keys, `${lastReleased}..${ref}`) : null;
|
|
11336
11399
|
if (unreleased) {
|
|
11337
11400
|
return {
|
|
11338
11401
|
ticket,
|
|
@@ -11344,7 +11407,7 @@ function planMerge(cwd, tickets, contract, lastReleased, branchFor = (t) => bran
|
|
|
11344
11407
|
skipReason: "already-merged"
|
|
11345
11408
|
};
|
|
11346
11409
|
}
|
|
11347
|
-
const anywhere = findKeyInRange(cwd, keys,
|
|
11410
|
+
const anywhere = findKeyInRange(cwd, keys, ref);
|
|
11348
11411
|
if (anywhere) {
|
|
11349
11412
|
return {
|
|
11350
11413
|
ticket,
|
|
@@ -11441,21 +11504,22 @@ ${section}`;
|
|
|
11441
11504
|
return [...lines.slice(0, anchor), section, ...lines.slice(anchor)].join("\n");
|
|
11442
11505
|
}
|
|
11443
11506
|
var RELEASE_COMMIT_SUBJECT = /^Release \S+$/;
|
|
11444
|
-
function subjectsSince(cwd, sha) {
|
|
11445
|
-
if (!isAncestor(cwd, sha,
|
|
11446
|
-
return (gitOk(cwd, ["log", "--format=%h %s", `${sha}
|
|
11507
|
+
function subjectsSince(cwd, sha, ref = "HEAD") {
|
|
11508
|
+
if (!isAncestor(cwd, sha, ref)) return null;
|
|
11509
|
+
return (gitOk(cwd, ["log", "--format=%h %s", `${sha}..${ref}`]) ?? "").split("\n").filter(Boolean);
|
|
11447
11510
|
}
|
|
11448
11511
|
var isReleaseCommitLine = (line) => RELEASE_COMMIT_SUBJECT.test(line.replace(/^\S+ /, ""));
|
|
11449
11512
|
function decideRelease(cwd, tickets, contract, opts = {}) {
|
|
11450
11513
|
const pattern = opts.tagPattern ?? "v*";
|
|
11451
11514
|
const base = opts.base ?? "main";
|
|
11452
|
-
const
|
|
11453
|
-
const
|
|
11515
|
+
const ref = opts.ref ?? "HEAD";
|
|
11516
|
+
const block = opts.guard === false ? null : checkGuards(cwd, base);
|
|
11517
|
+
const lastTag = latestReleaseTag(cwd, pattern, ref);
|
|
11454
11518
|
const lastReleased = lastTag ? tagCommit(cwd, lastTag) : null;
|
|
11455
|
-
const head = resolve2(cwd,
|
|
11456
|
-
const merges = block ? [] : planMerge(cwd, tickets, contract, lastReleased, opts.branchFor ?? ((t) => branchForIssue(cwd, t.issue_id)), base, opts.blockerInfo);
|
|
11457
|
-
const unreleasedCommits = lastReleased ? countCommits(cwd, `${lastReleased}
|
|
11458
|
-
const unreleasedWork = lastReleased ? (gitOk(cwd, ["log", "--format=%s", `${lastReleased}
|
|
11519
|
+
const head = resolve2(cwd, ref) ?? "";
|
|
11520
|
+
const merges = block ? [] : planMerge(cwd, tickets, contract, lastReleased, opts.branchFor ?? ((t) => branchForIssue(cwd, t.issue_id)), opts.ref ?? base, opts.blockerInfo, ref);
|
|
11521
|
+
const unreleasedCommits = lastReleased ? countCommits(cwd, `${lastReleased}..${ref}`) : 0;
|
|
11522
|
+
const unreleasedWork = lastReleased ? (gitOk(cwd, ["log", "--format=%s", `${lastReleased}..${ref}`]) ?? "").split("\n").filter((l) => l && !RELEASE_COMMIT_SUBJECT.test(l)).length : 0;
|
|
11459
11523
|
return {
|
|
11460
11524
|
block,
|
|
11461
11525
|
merges,
|
|
@@ -12170,10 +12234,14 @@ async function runReleaseCycle(o, movedPast2, localRefSkips) {
|
|
|
12170
12234
|
}
|
|
12171
12235
|
if (drop.size) releasable = releasable.filter((t) => !drop.has(t.id));
|
|
12172
12236
|
}
|
|
12237
|
+
const { remote, base } = o.repo.branch;
|
|
12238
|
+
const inPlace = o.dryRun || !!o.mergeOnly || !remoteBranchExists(o.cwd, remote, base);
|
|
12239
|
+
const remoteRef = `${remote}/${base}`;
|
|
12173
12240
|
const decision = decideRelease(o.cwd, releasable, o.contract, {
|
|
12174
12241
|
tagPattern,
|
|
12175
|
-
base
|
|
12242
|
+
base,
|
|
12176
12243
|
blockerInfo: o.blockerInfo,
|
|
12244
|
+
...inPlace ? {} : { ref: remoteRef, guard: false },
|
|
12177
12245
|
// CREW-1364: a branch another ship built and pushed at `fixed` exists
|
|
12178
12246
|
// here only on the remote. Materialize it so the squash below has a ref.
|
|
12179
12247
|
// A dry run only looks.
|
|
@@ -12202,7 +12270,7 @@ async function runReleaseCycle(o, movedPast2, localRefSkips) {
|
|
|
12202
12270
|
o.state?.clearBlocked();
|
|
12203
12271
|
const failedAt = o.state?.deployFailedSha();
|
|
12204
12272
|
if (!o.force && failedAt) {
|
|
12205
|
-
const since2 = failedAt === decision.head ? [] : subjectsSince(o.cwd, failedAt);
|
|
12273
|
+
const since2 = failedAt === decision.head ? [] : subjectsSince(o.cwd, failedAt, inPlace ? "HEAD" : remoteRef);
|
|
12206
12274
|
const real = since2?.filter((l) => !isReleaseCommitLine(l)) ?? null;
|
|
12207
12275
|
if (real && real.length === 0) {
|
|
12208
12276
|
o.emit.warn(
|
|
@@ -12229,10 +12297,13 @@ async function runReleaseCycle(o, movedPast2, localRefSkips) {
|
|
|
12229
12297
|
const review = await reviewHandoff(o, externalClosures, decision.merges, reviewing);
|
|
12230
12298
|
return { merged: [], deployed: false, stopped: "external", decision, externalClosures, review };
|
|
12231
12299
|
}
|
|
12232
|
-
|
|
12233
|
-
if (o.dryRun || o.mergeOnly || !remoteBranchExists(o.cwd, remote, base)) {
|
|
12300
|
+
if (inPlace) {
|
|
12234
12301
|
return mergeAndRelease(o, decision, fresh);
|
|
12235
12302
|
}
|
|
12303
|
+
const primary = checkGuards(o.cwd, base);
|
|
12304
|
+
if (primary) {
|
|
12305
|
+
o.emit.emit(scoped(`primary checkout not fast-forwarded: ${primary.detail}; its dist/ is stale until rebuilt`), { step: "release" });
|
|
12306
|
+
}
|
|
12236
12307
|
let checkout;
|
|
12237
12308
|
const stranded = recoverStrandedReleaseCheckouts(o.cwd);
|
|
12238
12309
|
if (stranded.length > 0) o.emit.warn(scoped(`dropped release checkout(s) stranded by a crash: ${stranded.join(", ")}`));
|
|
@@ -17889,9 +17960,11 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17889
17960
|
preModes
|
|
17890
17961
|
);
|
|
17891
17962
|
reportUnplaceable(c.route, preUnplaceable, tracker.contract.statuses.verified, remit);
|
|
17963
|
+
const remoteBase = remoteBranchExists(target.dir, repo.branch.remote, repo.branch.base);
|
|
17892
17964
|
const d = decideRelease(target.dir, pre, tracker.contract, {
|
|
17893
17965
|
tagPattern: repo.release.tagPattern ?? "v*",
|
|
17894
|
-
base: repo.branch.base
|
|
17966
|
+
base: repo.branch.base,
|
|
17967
|
+
...remoteBase ? { ref: `${repo.branch.remote}/${repo.branch.base}`, guard: false } : {}
|
|
17895
17968
|
});
|
|
17896
17969
|
const unstamped = planReleasedBranches(
|
|
17897
17970
|
target.dir,
|
package/package.json
CHANGED
|
@@ -94,6 +94,11 @@ pass.
|
|
|
94
94
|
Judge by exit code and summary line, and quote the summary in your
|
|
95
95
|
comment.
|
|
96
96
|
7. **Check the hand-off itself.**
|
|
97
|
+
- **Acceptance checklist.** Start from the builder's checklist: their
|
|
98
|
+
comment quotes each Acceptance line with the check that exercised it.
|
|
99
|
+
Re-run the checks that matter, and when a line fails, a bounce cites that
|
|
100
|
+
line by its quoted text. A line the builder marked unexercised is yours
|
|
101
|
+
to exercise.
|
|
97
102
|
- **Documentation.** If the repository's agent instructions (CLAUDE.md,
|
|
98
103
|
AGENTS.md, GEMINI.md or equivalent) name a user-facing documentation
|
|
99
104
|
location, a change to a user-facing surface must come with a matching
|
|
@@ -416,6 +416,14 @@ before going further, and never change a ticket's `report_type` yourself.
|
|
|
416
416
|
used, and anything you could not test yourself. Anything not committed and
|
|
417
417
|
pushed is gone.
|
|
418
418
|
|
|
419
|
+
**When the ticket has an Acceptance section, your last progress comment
|
|
420
|
+
quotes it.** List every line of that section verbatim, each followed by the
|
|
421
|
+
test, command or manual check that exercised it in this run and its result.
|
|
422
|
+
A line you could not exercise is listed with the reason, never omitted. The
|
|
423
|
+
runner checks your newest comment for every line and, if one is missing,
|
|
424
|
+
sets the ticket back to `in_progress` instead of passing it to QA. A ticket
|
|
425
|
+
with no Acceptance section is unaffected.
|
|
426
|
+
|
|
419
427
|
## Step 4 — you never merge, and you never deploy
|
|
420
428
|
|
|
421
429
|
**Nothing you do touches the base branch.** The release phase takes every
|
|
@@ -93,6 +93,11 @@ pass.
|
|
|
93
93
|
Judge by exit code and summary line, and quote the summary in your
|
|
94
94
|
comment.
|
|
95
95
|
7. **Check the hand-off itself.**
|
|
96
|
+
- **Acceptance checklist.** Start from the builder's checklist: their
|
|
97
|
+
comment quotes each Acceptance line with the check that exercised it.
|
|
98
|
+
Re-run the checks that matter, and when a line fails, a bounce cites that
|
|
99
|
+
line by its quoted text. A line the builder marked unexercised is yours
|
|
100
|
+
to exercise.
|
|
96
101
|
- **Documentation.** If the repository's agent instructions (CLAUDE.md,
|
|
97
102
|
AGENTS.md, GEMINI.md or equivalent) name a user-facing documentation
|
|
98
103
|
location, a change to a user-facing surface must come with a matching
|
|
@@ -411,6 +411,14 @@ before going further, and never change a ticket's `report_type` yourself.
|
|
|
411
411
|
used, and anything you could not test yourself. Anything not committed and
|
|
412
412
|
pushed is gone.
|
|
413
413
|
|
|
414
|
+
**When the ticket has an Acceptance section, your last progress comment
|
|
415
|
+
quotes it.** List every line of that section verbatim, each followed by the
|
|
416
|
+
test, command or manual check that exercised it in this run and its result.
|
|
417
|
+
A line you could not exercise is listed with the reason, never omitted. The
|
|
418
|
+
runner checks your newest comment for every line and, if one is missing,
|
|
419
|
+
sets the ticket back to `in_progress` instead of passing it to QA. A ticket
|
|
420
|
+
with no Acceptance section is unaffected.
|
|
421
|
+
|
|
414
422
|
## Step 4 — you never merge, and you never deploy
|
|
415
423
|
|
|
416
424
|
**Nothing you do touches the base branch.** The release phase takes every
|