@tablation/crew 0.75.6 → 0.76.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.
Files changed (2) hide show
  1. package/dist/cli.js +47 -3
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -3997,6 +3997,9 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
3997
3997
  function pushBranch(cwd, remote, branch, dst = branch) {
3998
3998
  git(cwd, ["push", remote, `${branch}:${dst}`]);
3999
3999
  }
4000
+ function pushBranchUpstream(cwd, remote, branch, dst = branch) {
4001
+ git(cwd, ["push", "--set-upstream", remote, `${branch}:${dst}`]);
4002
+ }
4000
4003
  function worktrees(cwd) {
4001
4004
  const out = gitOk(cwd, ["worktree", "list", "--porcelain"]) ?? "";
4002
4005
  const list = [];
@@ -6989,7 +6992,15 @@ function inspectHandoff(dir, cfg, ticket, role) {
6989
6992
  }
6990
6993
  return { kind: "pushed", branch, sha: origin, remote };
6991
6994
  }
6992
- function bounceComment(v, shipName2) {
6995
+ function pushableBounce(dir, v) {
6996
+ if (!v.branch || !v.local) return false;
6997
+ return !v.origin || isAncestor(dir, v.origin, v.local);
6998
+ }
6999
+ function describeHandoffPush(dir, cfg, ticket, role) {
7000
+ const v = inspectHandoff(dir, cfg, ticket, role);
7001
+ return v.kind === "bounce" && pushableBounce(dir, v) ? `would push ${v.branch} to ${v.remote}` : null;
7002
+ }
7003
+ function bounceComment(v, shipName2, pushError) {
6993
7004
  return `${HANDOFF_BOUNCED_MARKER}
6994
7005
 
6995
7006
  **Hand-off bounced: the branch is not on \`${v.remote}\` at the tip this ticket was built to.**
@@ -6998,7 +7009,7 @@ ${shipName2 ? `Ship: ${shipName2}
6998
7009
  ` : ""}Local HEAD: ${v.local ? `\`${v.local}\`` : "(no local branch)"}
6999
7010
  \`${v.remote}\`: ${v.origin ? `\`${v.origin}\`` : v.branch ? "(branch missing)" : "(no branch to look for)"}
7000
7011
 
7001
- ${v.reason[0].toUpperCase()}${v.reason.slice(1)}. Set back to \`in_progress\`, still held by this ship; push with a plain \`git push --set-upstream ${v.remote} <branch>\` (never \`--force\`) and set \`fixed\` again. Your brief authorizes this push: it is the hand-off, not a release, so do it now rather than asking for permission.`;
7012
+ ${v.reason[0].toUpperCase()}${v.reason.slice(1)}. ${pushError ? ` The runner tried a plain push and git refused it: \`${pushError}\`.` : ""} Set back to \`in_progress\`, still held by this ship; push with a plain \`git push --set-upstream ${v.remote} <branch>\` (never \`--force\`) and set \`fixed\` again. Your brief authorizes this push: it is the hand-off, not a release, so do it now rather than asking for permission.`;
7002
7013
  }
7003
7014
  async function takenOverSince(i, ticketRow) {
7004
7015
  if (!i.since || !i.writer.events) return false;
@@ -7048,12 +7059,35 @@ async function enforceHandoff(i) {
7048
7059
  i.log.warn(`${t.issue_id} has no local branch on this ship, so it is not this ship's hand-off to bounce: ${v.reason}`, log);
7049
7060
  return { kind: "unverified", why: "no local branch on this ship" };
7050
7061
  }
7062
+ let pushError;
7063
+ if (pushableBounce(i.dir, v)) {
7064
+ const wt = worktrees(i.dir).find((w) => w.branch === v.branch);
7065
+ try {
7066
+ pushBranchUpstream(wt?.path ?? i.dir, v.remote, v.branch);
7067
+ const again = inspectHandoff(i.dir, i.cfg, t, i.role);
7068
+ if (again.kind === "pushed") {
7069
+ await i.writer.postEvent(t.id, pushedEventBody(again.sha, again.remote, again.branch), i.seatId, "pushed");
7070
+ await i.writer.postEvent(
7071
+ t.id,
7072
+ `The runner pushed \`${again.branch}\` to \`${again.remote}\` on ${i.shipName ? `${i.shipName}'s` : "this ship's"} behalf: the ${i.role} seat set \`${i.contract.statuses.handoff}\` without pushing. Plain fast-forward push.${i.cfg.hooks.ci_status ? " The repo's `ci_status` hook was NOT run by the hand-off guard, so CI was not checked by the builder." : ""}`,
7073
+ i.seatId,
7074
+ "pushed_by_runner"
7075
+ ).catch(() => i.log.warn("pushed, but the note failed to post", log));
7076
+ i.log.emit(`${t.issue_id}: pushed ${again.branch} to ${again.remote} on the seat's behalf at ${again.sha.slice(0, 7)} \u2014 handed on to QA`, log);
7077
+ return { kind: "pushed", sha: again.sha };
7078
+ }
7079
+ pushError = `the push succeeded but ${again.kind === "bounce" ? again.reason : again.why}`;
7080
+ } catch (e) {
7081
+ pushError = e.message;
7082
+ }
7083
+ i.log.warn(`${t.issue_id}: the runner's own push failed: ${pushError}`, log);
7084
+ }
7051
7085
  await i.writer.updateTicket(t.id, {
7052
7086
  status: i.contract.statuses.building,
7053
7087
  ...t.assignee_id ? {} : { assignee_id: i.seatId },
7054
7088
  ...i.hold && !heldBy ? { [i.hold.column]: i.hold.shipId } : {}
7055
7089
  });
7056
- await i.writer.postEvent(t.id, bounceComment(v, i.shipName), i.seatId, "bounced").catch(() => {
7090
+ await i.writer.postEvent(t.id, bounceComment(v, i.shipName, pushError), i.seatId, "bounced").catch(() => {
7057
7091
  i.log.warn("bounced, but the note failed to post", log);
7058
7092
  });
7059
7093
  i.log.warn(`${t.issue_id} bounced back to ${i.contract.statuses.building}: ${v.reason}`, log);
@@ -18250,6 +18284,16 @@ ${changed2.map((c) => ` - ${c}
18250
18284
  if (dryRun) {
18251
18285
  process.stdout.write(`${describePlan(plan)}
18252
18286
  `);
18287
+ if (workingId && (current === "dev" || current === "design")) {
18288
+ try {
18289
+ const t = await tracker2.ticket(workingId);
18290
+ const r = repoConfigFor(route, t);
18291
+ const would = r && describeHandoffPush(r.target.dir, r.cfg, t, current);
18292
+ if (would) process.stdout.write(`${would}
18293
+ `);
18294
+ } catch {
18295
+ }
18296
+ }
18253
18297
  break;
18254
18298
  }
18255
18299
  const before = snapshot(await tracker2.openTickets());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.75.6",
3
+ "version": "0.76.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",