@tablation/crew 0.40.0 → 0.41.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
@@ -5232,7 +5232,14 @@ function missingBlockerIds(tickets) {
5232
5232
  function blockerInfoMap(tickets, extra = []) {
5233
5233
  const info = {};
5234
5234
  for (const t of [...tickets, ...extra]) {
5235
- info[t.id] = { issue_id: t.issue_id, issue_tag: t.issue_tag, status: t.status };
5235
+ info[t.id] = {
5236
+ issue_id: t.issue_id,
5237
+ issue_tag: t.issue_tag,
5238
+ status: t.status,
5239
+ repo_id: t.repo_id,
5240
+ merged_at: t.merged_at,
5241
+ commit_sha: t.commit_sha
5242
+ };
5236
5243
  }
5237
5244
  return info;
5238
5245
  }
@@ -5241,14 +5248,18 @@ function computeBlockedIds(tickets, info) {
5241
5248
  for (const t of tickets) if (unresolvedBlockers(t, info).length > 0) blocked.add(t.id);
5242
5249
  return blocked;
5243
5250
  }
5244
- function unresolvedBlockers(t, info) {
5251
+ function unresolvedBlockers(t, info, opts = {}) {
5252
+ const mergeGated = opts.mergeGated ?? true;
5245
5253
  return (t.blocked_by ?? []).filter((id) => {
5246
- const status2 = info[id]?.status;
5247
- return status2 !== void 0 && !RESOLVED_STATUSES.has(status2);
5254
+ const b = info[id];
5255
+ const status2 = b?.status;
5256
+ if (!b || status2 === void 0) return false;
5257
+ if (!RESOLVED_STATUSES.has(status2)) return true;
5258
+ return mergeGated && status2 === "verified" && !!t.repo_id && b.repo_id === t.repo_id && !b.merged_at && !b.commit_sha;
5248
5259
  });
5249
5260
  }
5250
- function describeUnresolved(t, info) {
5251
- return unresolvedBlockers(t, info).map((id) => `${info[id]?.issue_tag ?? info[id]?.issue_id ?? "?"} (${info[id]?.status ?? "unknown"})`).join(", ");
5261
+ function describeUnresolved(t, info, opts = {}) {
5262
+ return unresolvedBlockers(t, info, opts).map((id) => `${info[id]?.issue_tag ?? info[id]?.issue_id ?? "?"} (${info[id]?.status ?? "unknown"})`).join(", ");
5252
5263
  }
5253
5264
  function sweepDiagnostics(tickets, info) {
5254
5265
  const dangling = [];
@@ -5317,7 +5328,7 @@ function filingErrors(tickets, opts = {}) {
5317
5328
  function sweepComment(step) {
5318
5329
  return step.action === "park" ? `Parked as **Blocked** by the crew \u2014 waiting on: ${step.blockers}.
5319
5330
 
5320
- This is automatic and reversible: an approved ticket whose \`Blocked by\` entries are not all resolved is parked out of the queue, and restored on the first poll after the last one resolves. Nothing else about the ticket changes. A blocker counts as resolved at \`verified\`, \`closed_deployed\`, \`closed_wont_fix\` or \`closed_duplicate\` \u2014 not at \`fixed\`, which is still an unmerged branch awaiting QA.` : `Restored to **Approved** by the crew \u2014 every blocker has resolved${step.blockers && step.blockers !== "\u2014" ? ` (${step.blockers})` : ""}.
5331
+ This is automatic and reversible: an approved ticket whose \`Blocked by\` entries are not all resolved is parked out of the queue, and restored on the first poll after the last one resolves. Nothing else about the ticket changes. A blocker counts as resolved at \`closed_deployed\`, \`closed_wont_fix\` or \`closed_duplicate\`, or at \`verified\` when it is in a different repository or already merged (\`merged_at\` set) \u2014 not at \`fixed\`, which is still an unmerged branch awaiting QA, and not at \`verified\` for a blocker in this ticket's own repository until the release has merged it, so nothing gets built stacked on an unmerged branch.` : `Restored to **Approved** by the crew \u2014 every blocker has resolved${step.blockers && step.blockers !== "\u2014" ? ` (${step.blockers})` : ""}.
5321
5332
 
5322
5333
  This restores the status it was approved at before being parked; it is back in the queue and a role may pick it up. If a blocker resolved as \`closed_wont_fix\` or \`closed_duplicate\` this ticket may still not be buildable as written \u2014 that is a call for a human, not the crew.`;
5323
5334
  }
@@ -5897,7 +5908,7 @@ ${sorted(held).map((t) => `| ${displayKey2(t)} | ${stopOf(t)} |`).join("\n")}
5897
5908
  "\nWork the first one you are not required to skip. Tickets waiting on an\nunresolved dependency are already out of this table \u2014 see Blocked below.\n",
5898
5909
  table(step2, i, header, row),
5899
5910
  "\n### Blocked \u2014 waiting on a dependency, not yours to start\n",
5900
- '\nListed rather than hidden so "nothing to do" stays distinguishable from\n"everything is parked". The loop parks and restores these itself, on every\npoll: an approved ticket whose `Blocked by` entries are unresolved moves to\n`blocked`, and back to `accepted` once the last one resolves. **Do not pick\none up, do not set or clear `blocked` by hand, and do not "unblock" one by\nediting its `Blocked by` field** \u2014 if a blocker looks wrong, say so in your\nrun summary. A blocker counts as resolved at `verified`, `closed_deployed`,\n`closed_wont_fix` or `closed_duplicate`; `fixed` is still an unmerged branch\nawaiting QA, so it does not count.\n',
5911
+ '\nListed rather than hidden so "nothing to do" stays distinguishable from\n"everything is parked". The loop parks and restores these itself, on every\npoll: an approved ticket whose `Blocked by` entries are unresolved moves to\n`blocked`, and back to `accepted` once the last one resolves. **Do not pick\none up, do not set or clear `blocked` by hand, and do not "unblock" one by\nediting its `Blocked by` field** \u2014 if a blocker looks wrong, say so in your\nrun summary. A blocker counts as resolved at `closed_deployed`,\n`closed_wont_fix` or `closed_duplicate`, or at `verified` when it is in another\nrepository or already merged; `fixed` is still an unmerged branch awaiting QA,\nand a `verified` blocker in the same repository as the blocked ticket is unmerged until the\nrelease merges it, so neither counts.\n',
5901
5912
  blockedTable(i.tickets.filter(isBlocked)),
5902
5913
  ...heldTable ? [heldTable] : []
5903
5914
  ]);
@@ -5926,7 +5937,7 @@ function qaDigest(i) {
5926
5937
  ### Waiting on blockers \u2014 not yours to verify yet
5927
5938
 
5928
5939
  A \`fixed\` or \`qa\` ticket whose \`Blocked by\` names something not yet resolved
5929
- (\`verified\`, \`closed_deployed\`, \`closed_wont_fix\` or \`closed_duplicate\`) is held out of
5940
+ (\`closed_deployed\`, \`closed_wont_fix\` or \`closed_duplicate\`, or \`verified\` once merged or in another repository) is held out of
5930
5941
  verification: its change needs the blocker released first. It becomes yours on
5931
5942
  the first poll after the last blocker resolves. No one needs to change the ticket.
5932
5943
 
@@ -6466,7 +6477,7 @@ function handBackComment(b, merged) {
6466
6477
 
6467
6478
  \`${b.branch}\` was verified, but squash-merging it into \`${b.base}\` conflicts in ${list}. Work that landed on \`${b.base}\` after this branch was cut has changed the same lines.
6468
6479
 
6469
- ${merged ? `\`${b.base}\` has been merged into \`${b.branch}\` in its worktree and the conflict is left in place, so \`git status\` there shows exactly what disagrees. Resolve it, commit, and hand back to QA as usual \u2014 \`git merge --abort\` backs the merge out if you would rather start differently.` : `This ship has no worktree for \`${b.branch}\`, so nothing has been merged for you. Merge \`${b.base}\` into the branch wherever it lives, resolve the conflict, and commit.`}
6480
+ ${merged ? `\`${b.base}\` has been merged into \`${b.branch}\` in its worktree and the conflict is left in place, so \`git status\` there shows exactly what disagrees. Resolve it, commit, push as a plain fast-forward (the branch is pushed, so rebasing it is not an option) and hand back to QA as usual \u2014 \`git merge --abort\` backs the merge out if you would rather start differently.` : `This ship has no worktree for \`${b.branch}\`, so nothing has been merged for you. Merge \`${b.base}\` into the branch wherever it lives, resolve the conflict, and commit.`}
6470
6481
 
6471
6482
  Nothing was written to \`${b.base}\`: the failed merge was rewound, and the rest of the release shipped without this ticket. Re-verifying is what puts it back in the merge queue.`;
6472
6483
  }
@@ -6476,7 +6487,7 @@ function escalateComment(b) {
6476
6487
 
6477
6488
  \`${b.branch}\` was handed back once for a merge conflict, verified again, and conflicts against \`${b.base}\` a second time${b.paths.length ? ` (${b.paths.map((p) => `\`${p}\``).join(", ")})` : ""}.
6478
6489
 
6479
- Either \`${b.base}\` is moving faster than this branch can be rebased onto it, or this ticket overlaps work that has already shipped and needs rethinking rather than merging. That is a call for a person, not another lap through the lanes.`;
6490
+ Either \`${b.base}\` is moving faster than this branch can absorb it, or this ticket overlaps work that has already shipped and needs rethinking rather than merging. That is a call for a person, not another lap through the lanes.`;
6480
6491
  }
6481
6492
  async function applyConflictBounce(writer, b, contract, memberId, log, dryRun) {
6482
6493
  const t = { ticket: b.ticket.issue_id, step: "merge" };
@@ -8421,7 +8432,7 @@ function checkGuards(cwd, base = "main") {
8421
8432
  }
8422
8433
  function heldByBlockers(tickets, verifiedStatus, info) {
8423
8434
  if (!info) return [];
8424
- return tickets.filter((t) => t.status === verifiedStatus && unresolvedBlockers(t, info).length > 0).map((ticket) => ({ ticket, blockers: describeUnresolved(ticket, info) }));
8435
+ return tickets.filter((t) => t.status === verifiedStatus && unresolvedBlockers(t, info, { mergeGated: false }).length > 0).map((ticket) => ({ ticket, blockers: describeUnresolved(ticket, info, { mergeGated: false }) }));
8425
8436
  }
8426
8437
  var describeHeld = (held) => held.map((h) => `${h.ticket.issue_tag ?? h.ticket.issue_id} held: blocked by ${h.blockers}`);
8427
8438
  var CHANGELOG_LINE = /^Changelog:[ \t]*(.*)$/gm;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.40.0",
3
+ "version": "0.41.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",
@@ -153,6 +153,11 @@ pass.
153
153
  base branch or the primary checkout.** The release phase merges verified
154
154
  branches and removes their worktrees; a merge may be in flight while you
155
155
  run.
156
+ - **You never ask a builder to rewrite a pushed branch.** If you check
157
+ mergeability (`git merge-tree`) and the branch conflicts with or trails the
158
+ base, the bounce says to merge `<remote>/<base>` into the branch, resolve and
159
+ push fast-forward; a rebase would need a force push the builder cannot make.
160
+ Or pass the ticket and let the release's conflict path do the merge.
156
161
  - **You only ever enter the worktree of the ticket you are verifying.**
157
162
  - Throwaway verification scripts go in a scratch directory outside the
158
163
  repository — not the worktree root either.
@@ -149,10 +149,11 @@ Then:
149
149
  has posted a new comment since your last one — the comment clears you to
150
150
  act, never the flag. Clearing either flag is always the operator's move.
151
151
  - **Before resuming in an existing worktree, run `crew sync`** (fetches and
152
- fast-forwards every worktree branch that tracks a remote; never merges or
153
- rebases). If it reports your branch as diverged, stop: comment with what it
154
- printed and park the ticket for a person (see "Stopping"); never merge or
155
- rebase it yourself.
152
+ fast-forwards every worktree branch that tracks a remote and does nothing
153
+ else to it). If it reports your branch as diverged, stop: comment with what
154
+ it printed and park the ticket for a person (see "Stopping"); do not try to
155
+ reconcile it yourself. A branch that is merely stale or conflicts with the
156
+ base is not this case: Step 4.4 says how to fix it.
156
157
  - A ticket QA bounced back arrives as `in_progress`, reassigned to your row
157
158
  (or unassigned, when your ship holds it), with a comment saying what still
158
159
  fails. Treat it as new direction: fix what it names and take it back to
@@ -336,7 +337,8 @@ before going further, and never change a ticket's `report_type` yourself.
336
337
  first**: `git push --set-upstream <remote> <branch>` from the worktree
337
338
  (the remote is `origin` unless the repository's configuration says
338
339
  otherwise; a repository with no remote has nothing to push to — say so and
339
- skip it). Push as a plain fast-forward, never `--force`; a rejected push
340
+ skip it). Push as a plain fast-forward, never `--force` (Step 4.4 covers a stale
341
+ branch); a rejected push
340
342
  means someone else moved the branch, so stop and say so on the ticket.
341
343
  Then, in one write, set `status` to `fixed` and hand the ticket to QA: in
342
344
  an `automatic` project **clear `assignee_id`**; in a `hybrid` project set
@@ -376,8 +378,18 @@ worktree, runs the test gate and deploys. Three consequences:
376
378
  reads the diff later to fill them in.
377
379
  3. **A branch that no longer squash-merges cleanly comes back to you** at
378
380
  `in_progress`, reassigned to your lane, with a comment naming the
379
- conflict. Fix it in the same worktree and take it to `fixed` again; never
380
- merge by hand to "help".
381
+ conflict. Fix it in the same worktree and take it to `fixed` again. "Never
382
+ merge" above means never merge INTO the base; merging the base INTO your
383
+ own ticket branch is the expected fix (rule 4), and it is not "by hand to
384
+ help".
385
+ 4. **A pushed branch is never rewritten; merge `<remote>/<base>` into it.**
386
+ Never rebase, reset or amend a branch you have pushed: that needs a force
387
+ push, which this ship denies, and it strands every other ship's copy. When
388
+ the branch is stale or conflicts with `<base>`, run `git fetch <remote>`
389
+ then `git merge <remote>/<base>` in the ticket's worktree, resolve any
390
+ conflict, commit, run the suite, and `git push` as a plain fast-forward.
391
+ The merge commit disappears when the release squash-merges, so the ticket's
392
+ own diff is all that ships.
381
393
 
382
394
  Never set `verified` yourself — that is QA's call (or the operator's), and
383
395
  setting it is what queues a branch for merge. The deployed status is set by
@@ -33,7 +33,7 @@ person says so. **A missing worktree is never a reason to stop.**
33
33
  1. From the primary checkout, run `crew sync` (name the route if this ship
34
34
  serves more than one). It fetches the remote and fast-forwards the primary
35
35
  checkout's base branch, and every worktree branch that tracks a remote,
36
- never merging or rebasing. It is quiet about a checkout that is not cleanly
36
+ doing nothing else to them. It is quiet about a checkout that is not cleanly
37
37
  on the base — something else may be mid-edit there; that is expected.
38
38
  2. If it prints `DO NOT CUT A WORKTREE FROM THIS BASE` and exits non-zero, the
39
39
  base has diverged from the remote: **hard stop.** Do not `git worktree
@@ -50,7 +50,9 @@ person says so. **A missing worktree is never a reason to stop.**
50
50
  When resuming an existing worktree, run `crew sync` first; if it reports your
51
51
  branch as diverged (local commits not on the remote *and* the remote has
52
52
  moved), stop, comment with what it printed, and park the ticket for a person
53
- (`needs_info`, operator as assignee). Never merge or rebase it yourself.
53
+ (`needs_info`, operator as assignee). A branch that is only stale or conflicts
54
+ with the base is a different case: merge `<remote>/<base>` into it, per the lane
55
+ policy, and push fast-forward.
54
56
 
55
57
  ## Making the worktree runnable
56
58
 
@@ -152,6 +152,11 @@ pass.
152
152
  base branch or the primary checkout.** The release phase merges verified
153
153
  branches and removes their worktrees; a merge may be in flight while you
154
154
  run.
155
+ - **You never ask a builder to rewrite a pushed branch.** If you check
156
+ mergeability (`git merge-tree`) and the branch conflicts with or trails the
157
+ base, the bounce says to merge `<remote>/<base>` into the branch, resolve and
158
+ push fast-forward; a rebase would need a force push the builder cannot make.
159
+ Or pass the ticket and let the release's conflict path do the merge.
155
160
  - **You only ever enter the worktree of the ticket you are verifying.**
156
161
  - Throwaway verification scripts go in a scratch directory outside the
157
162
  repository — not the worktree root either.
@@ -149,10 +149,11 @@ Then:
149
149
  has posted a new comment since your last one — the comment clears you to
150
150
  act, never the flag. Clearing either flag is always the operator's move.
151
151
  - **Before resuming in an existing worktree, run `crew sync`** (fetches and
152
- fast-forwards every worktree branch that tracks a remote; never merges or
153
- rebases). If it reports your branch as diverged, stop: comment with what it
154
- printed and park the ticket for a person (see "Stopping"); never merge or
155
- rebase it yourself.
152
+ fast-forwards every worktree branch that tracks a remote and does nothing
153
+ else to it). If it reports your branch as diverged, stop: comment with what
154
+ it printed and park the ticket for a person (see "Stopping"); do not try to
155
+ reconcile it yourself. A branch that is merely stale or conflicts with the
156
+ base is not this case: Step 4.4 says how to fix it.
156
157
  - A ticket QA bounced back arrives as `in_progress`, reassigned to your row
157
158
  (or unassigned, when your ship holds it), with a comment saying what still
158
159
  fails. Treat it as new direction: fix what it names and take it back to
@@ -336,7 +337,8 @@ before going further, and never change a ticket's `report_type` yourself.
336
337
  first**: `git push --set-upstream <remote> <branch>` from the worktree
337
338
  (the remote is `origin` unless the repository's configuration says
338
339
  otherwise; a repository with no remote has nothing to push to — say so and
339
- skip it). Push as a plain fast-forward, never `--force`; a rejected push
340
+ skip it). Push as a plain fast-forward, never `--force` (Step 4.4 covers a stale
341
+ branch); a rejected push
340
342
  means someone else moved the branch, so stop and say so on the ticket.
341
343
  Then, in one write, set `status` to `fixed` and hand the ticket to QA: in
342
344
  an `automatic` project **clear `assignee_id`**; in a `hybrid` project set
@@ -376,8 +378,18 @@ worktree, runs the test gate and deploys. Three consequences:
376
378
  reads the diff later to fill them in.
377
379
  3. **A branch that no longer squash-merges cleanly comes back to you** at
378
380
  `in_progress`, reassigned to your lane, with a comment naming the
379
- conflict. Fix it in the same worktree and take it to `fixed` again; never
380
- merge by hand to "help".
381
+ conflict. Fix it in the same worktree and take it to `fixed` again. "Never
382
+ merge" above means never merge INTO the base; merging the base INTO your
383
+ own ticket branch is the expected fix (rule 4), and it is not "by hand to
384
+ help".
385
+ 4. **A pushed branch is never rewritten; merge `<remote>/<base>` into it.**
386
+ Never rebase, reset or amend a branch you have pushed: that needs a force
387
+ push, which this ship denies, and it strands every other ship's copy. When
388
+ the branch is stale or conflicts with `<base>`, run `git fetch <remote>`
389
+ then `git merge <remote>/<base>` in the ticket's worktree, resolve any
390
+ conflict, commit, run the suite, and `git push` as a plain fast-forward.
391
+ The merge commit disappears when the release squash-merges, so the ticket's
392
+ own diff is all that ships.
381
393
 
382
394
  Never set `verified` yourself — that is QA's call (or the operator's), and
383
395
  setting it is what queues a branch for merge. The deployed status is set by
@@ -33,7 +33,7 @@ person says so. **A missing worktree is never a reason to stop.**
33
33
  1. From the primary checkout, run `crew sync` (name the route if this ship
34
34
  serves more than one). It fetches the remote and fast-forwards the primary
35
35
  checkout's base branch, and every worktree branch that tracks a remote,
36
- never merging or rebasing. It is quiet about a checkout that is not cleanly
36
+ doing nothing else to them. It is quiet about a checkout that is not cleanly
37
37
  on the base — something else may be mid-edit there; that is expected.
38
38
  2. If it prints `DO NOT CUT A WORKTREE FROM THIS BASE` and exits non-zero, the
39
39
  base has diverged from the remote: **hard stop.** Do not `git worktree
@@ -50,7 +50,9 @@ person says so. **A missing worktree is never a reason to stop.**
50
50
  When resuming an existing worktree, run `crew sync` first; if it reports your
51
51
  branch as diverged (local commits not on the remote *and* the remote has
52
52
  moved), stop, comment with what it printed, and park the ticket for a person
53
- (`needs_info`, operator as assignee). Never merge or rebase it yourself.
53
+ (`needs_info`, operator as assignee). A branch that is only stale or conflicts
54
+ with the base is a different case: merge `<remote>/<base>` into it, per the lane
55
+ policy, and push fast-forward.
54
56
 
55
57
  ## Making the worktree runnable
56
58