@tablation/crew 0.78.1 → 0.79.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 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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.78.1",
3
+ "version": "0.79.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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