@tablation/crew 0.9.0 → 0.10.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
@@ -3955,6 +3955,13 @@ function fastForward(cwd, branch) {
3955
3955
  if (!s.canFastForward) return false;
3956
3956
  return gitOk(cwd, ["merge", "--ff-only", s.upstream]) !== null;
3957
3957
  }
3958
+ function baseBranchUnsafe(r) {
3959
+ return r.action === "diverged" || r.action === "ahead" || r.action === "ff-failed";
3960
+ }
3961
+ function describeUnsafeBase(r, remote, base) {
3962
+ const counts = r.action === "diverged" ? `${r.ahead} ahead, ${r.behind} behind ${remote}/${base}` : r.action === "ahead" ? `${r.ahead} ahead of ${remote}/${base}` : `${r.behind} behind ${remote}/${base}, cannot fast-forward`;
3963
+ return `${r.detail} [${counts}] \u2014 reconcile it (\`crew sync\` afterwards), or \`git reset --hard ${remote}/${base}\` if the local commits duplicate a release`;
3964
+ }
3958
3965
  function refreshBaseBranch(cwd, remote, base, dryRun = false) {
3959
3966
  if (currentBranch(cwd) !== base || status(cwd).length > 0) {
3960
3967
  return { action: "not-applicable", detail: `${cwd} is not on a clean ${base}` };
@@ -3981,6 +3988,9 @@ function refreshBaseBranch(cwd, remote, base, dryRun = false) {
3981
3988
  detail: `${base} has diverged from ${upstream} (${ahead} ahead, ${behind} behind)`
3982
3989
  };
3983
3990
  }
3991
+ if (behind === 0 && ahead > 0) {
3992
+ return { action: "ahead", ahead, detail: `${base} has ${ahead} commit(s) ${upstream} does not` };
3993
+ }
3984
3994
  if (behind === 0) return { action: "level", detail: `${base} is level with ${upstream}` };
3985
3995
  if (dryRun) {
3986
3996
  return { action: "would-fast-forward", behind, detail: `would fast-forward ${base}: ${behind} behind ${upstream}` };
@@ -5979,9 +5989,13 @@ function refreshBase(o) {
5979
5989
  // base. Fast-forwarding is impossible and merging would be this crew
5980
5990
  // inventing a resolution nobody asked for, on the branch everything
5981
5991
  // ships from.
5992
+ // AHEAD is a stop too (CREW-1379): local commits nobody asked for on the
5993
+ // base are how a second ship's release came to diverge from origin's.
5994
+ // Releasing on top of a hand commit is the failure; reconcile it first.
5995
+ case "ahead":
5982
5996
  case "diverged":
5983
5997
  case "ff-failed":
5984
- return { ok: false, why: r.detail };
5998
+ return { ok: false, why: describeUnsafeBase(r, o.repo.branch.remote, o.repo.branch.base) };
5985
5999
  }
5986
6000
  }
5987
6001
  async function reconfirmLastRelease(o, decision) {
@@ -9270,6 +9284,11 @@ if (command === "passenger-sync-daemon") {
9270
9284
  if (!why) return false;
9271
9285
  emit.emit(`${why} \u2014 release skipped`, { step: "release" });
9272
9286
  return true;
9287
+ }, baseHealth = function(r) {
9288
+ const { remote, base } = r.config.branch;
9289
+ const out = refreshBaseBranch(r.dir, remote, base, true);
9290
+ const unsafe = baseBranchUnsafe(out);
9291
+ return { action: out.action, unsafe, detail: unsafe ? describeUnsafeBase(out, remote, base) : out.detail };
9273
9292
  }, reportUnplaceable = function(c, unplaceable, verified, emit2) {
9274
9293
  if (unplaceableReported.has(c.route)) return;
9275
9294
  unplaceableReported.add(c.route);
@@ -9278,7 +9297,7 @@ if (command === "passenger-sync-daemon") {
9278
9297
  else emit2.emit(note.message, { step: "release" });
9279
9298
  }
9280
9299
  };
9281
- requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, reportUnplaceable2 = reportUnplaceable;
9300
+ requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, baseHealth2 = baseHealth, reportUnplaceable2 = reportUnplaceable;
9282
9301
  let cfg;
9283
9302
  let firstRun;
9284
9303
  try {
@@ -11318,14 +11337,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
11318
11337
  let acted = 0;
11319
11338
  let baseChecked = 0;
11320
11339
  let baseActed = 0;
11340
+ let baseUnsafe = 0;
11321
11341
  for (const r of await resolvedRepos(route)) {
11322
11342
  const base = refreshBaseBranch(r.dir, r.config.branch.remote, r.config.branch.base, dryRun);
11323
11343
  if (base.action !== "not-applicable") {
11324
11344
  baseChecked++;
11325
11345
  if (base.action === "fetch-failed") emit.warn(base.detail);
11326
- else if (base.action === "diverged" || base.action === "ff-failed") {
11346
+ else if (baseBranchUnsafe(base)) {
11327
11347
  baseActed++;
11328
- emit.warn(base.detail);
11348
+ baseUnsafe++;
11349
+ emit.error(`${r.name}: DO NOT CUT A WORKTREE FROM THIS BASE \u2014 ${describeUnsafeBase(base, r.config.branch.remote, r.config.branch.base)}`);
11329
11350
  } else if (base.action === "would-fast-forward" || base.action === "fast-forwarded") {
11330
11351
  baseActed++;
11331
11352
  emit.emit(base.detail);
@@ -11357,6 +11378,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
11357
11378
  }
11358
11379
  }
11359
11380
  }
11381
+ if (baseUnsafe > 0) process.exitCode = 1;
11360
11382
  if (baseChecked > 0 && baseActed === 0) emit.emit(`${baseChecked} base branch(es) checked, all level with the remote`);
11361
11383
  if (tracked === 0) emit.emit("no worktree tracks a remote branch \u2014 nothing to sync");
11362
11384
  else if (acted === 0) emit.emit(`${tracked} worktree(s) tracking a remote, all level with it`);
@@ -11487,6 +11509,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
11487
11509
  repos: repos.map((r) => ({
11488
11510
  name: r.name,
11489
11511
  dir: r.dir,
11512
+ // Additive (CREW-1379): `unsafe` means a ship must not cut a
11513
+ // worktree or release from this base. `version` stays 1.
11514
+ baseBranch: baseHealth(r),
11490
11515
  worktreePrefix: r.config.worktrees.prefix,
11491
11516
  // A repo with no explicit `worktrees.prefix` no longer names its
11492
11517
  // worktrees `<prefix><number>` (ISSUE-969) — the checkout's own
@@ -11527,6 +11552,11 @@ watermark: ${state.watermark()}
11527
11552
  }
11528
11553
  for (const i of openShipAttention(state)) {
11529
11554
  process.stdout.write(`attention: ${i.kind} \u2014 ${i.message}
11555
+ `);
11556
+ }
11557
+ for (const r of await resolvedRepos(route)) {
11558
+ const bh = baseHealth(r);
11559
+ if (bh.unsafe) process.stdout.write(`base: ${r.name} \u2014 UNSAFE, no worktree cuts or releases: ${bh.detail}
11530
11560
  `);
11531
11561
  }
11532
11562
  if (waiting) {
@@ -11581,6 +11611,12 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
11581
11611
  process.stdout.write(` - ${p2}
11582
11612
  `);
11583
11613
  }
11614
+ const bh = baseHealth(r);
11615
+ if (bh.action !== "not-applicable") {
11616
+ process.stdout.write(` base ${config.branch.base}: ${bh.unsafe ? "ERROR \u2014 " : ""}${bh.detail}
11617
+ `);
11618
+ if (bh.unsafe) process.exitCode = 1;
11619
+ }
11584
11620
  }
11585
11621
  const missingAuthorship = await tracker.missingAuthorshipColumns();
11586
11622
  process.stdout.write(
@@ -11761,4 +11797,5 @@ var passengerApiKeys2;
11761
11797
  var endpointApiFor2;
11762
11798
  var endpointReconcileDeps2;
11763
11799
  var releaseRefused2;
11800
+ var baseHealth2;
11764
11801
  var reportUnplaceable2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.9.0",
3
+ "version": "0.10.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",
@@ -346,10 +346,13 @@ if `report_type` is either of those.
346
346
  missing commits another ship or a reviewer pushed since the last sync. It
347
347
  is quiet, not an alert, about a primary checkout that isn't cleanly on the
348
348
  base branch (something else may be mid-edit there) — that's expected, not
349
- a fault, and just means this sync is a no-op. A genuinely DIVERGED base
350
- branch is reported in `crew sync`'s own output; that's for the operator to
351
- reconcile, not something to resolve yourself — carry on and cut the
352
- worktree from whatever HEAD the primary checkout actually has. Then, from
349
+ a fault, and just means this sync is a no-op. A DIVERGED base
350
+ branch (local commits the remote lacks, or one that cannot fast-forward)
351
+ makes `crew sync` print `DO NOT CUT A WORKTREE FROM THIS BASE` and exit
352
+ non-zero. That is a hard stop: do not run `git worktree add`, do not try to
353
+ reconcile it yourself. Post a comment on the ticket naming the repo and the
354
+ counts `crew sync` printed, leave the ticket as you found it, and end the
355
+ run — the operator reconciles the checkout. Otherwise, from
353
356
  the primary checkout, `git worktree add` a sibling worktree for this
354
357
  ticket, on a new branch cut from the base branch's current HEAD. The
355
358
  digest's `worktree` and `branch` columns give this ticket's own directory
@@ -327,10 +327,13 @@ if `report_type` is either of those.
327
327
  missing commits another ship or a reviewer pushed since the last sync. It
328
328
  is quiet, not an alert, about a primary checkout that isn't cleanly on the
329
329
  base branch (something else may be mid-edit there) — that's expected, not
330
- a fault, and just means this sync is a no-op. A genuinely DIVERGED base
331
- branch is reported in `crew sync`'s own output; that's for the operator to
332
- reconcile, not something to resolve yourself — carry on and cut the
333
- worktree from whatever HEAD the primary checkout actually has. Then, from
330
+ a fault, and just means this sync is a no-op. A DIVERGED base
331
+ branch (local commits the remote lacks, or one that cannot fast-forward)
332
+ makes `crew sync` print `DO NOT CUT A WORKTREE FROM THIS BASE` and exit
333
+ non-zero. That is a hard stop: do not run `git worktree add`, do not try to
334
+ reconcile it yourself. Post a comment on the ticket naming the repo and the
335
+ counts `crew sync` printed, leave the ticket as you found it, and end the
336
+ run — the operator reconciles the checkout. Otherwise, from
334
337
  the primary checkout, `git worktree add` a sibling worktree for this
335
338
  ticket, on a new branch cut from the base branch's current HEAD. The
336
339
  digest's `worktree` and `branch` columns give this ticket's own directory