@tablation/crew 0.9.0 → 0.10.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
@@ -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) {
@@ -6825,14 +6839,30 @@ async function fetchDivergedPrompt(route, role, opts) {
6825
6839
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
6826
6840
  });
6827
6841
  let row;
6842
+ let agentsModelId;
6828
6843
  try {
6829
6844
  const agentsModel = await client2.dataModels.get("agents", route.resolved.workspaceId);
6830
- row = await client2.records.get(agentsModel.id, agentId);
6845
+ agentsModelId = agentsModel.id;
6846
+ row = await client2.records.get(agentsModelId, agentId);
6831
6847
  } catch {
6832
6848
  return void 0;
6833
6849
  }
6834
6850
  const defaultPrompt = personaDefaultPrompt(route.promptsDir, role);
6835
- return row.prompt !== defaultPrompt ? row.prompt : void 0;
6851
+ if (row.prompt === defaultPrompt) return void 0;
6852
+ const cached = route.resolved.agentPersonas?.[role];
6853
+ if (cached?.agentId === agentId && cached.historyId) {
6854
+ const lastSynced = await fetchLastSyncedPrompt(route, agentsModelId, agentId, cached.historyId, opts.userAgent);
6855
+ if (lastSynced !== void 0 && lastSynced === row.prompt) return void 0;
6856
+ }
6857
+ return row.prompt;
6858
+ }
6859
+ async function fetchLastSyncedPrompt(route, agentsModelId, recordId, historyId, userAgent) {
6860
+ try {
6861
+ const entries = await fetchHistoryEntries(route, agentsModelId, recordId, userAgent);
6862
+ return entries.find((e) => e.history_id === historyId)?.prompt;
6863
+ } catch {
6864
+ return void 0;
6865
+ }
6836
6866
  }
6837
6867
  async function fetchSeatAgentModel(route, role, opts) {
6838
6868
  const agentId = await resolveAgentId(route, role, opts);
@@ -9270,6 +9300,11 @@ if (command === "passenger-sync-daemon") {
9270
9300
  if (!why) return false;
9271
9301
  emit.emit(`${why} \u2014 release skipped`, { step: "release" });
9272
9302
  return true;
9303
+ }, baseHealth = function(r) {
9304
+ const { remote, base } = r.config.branch;
9305
+ const out = refreshBaseBranch(r.dir, remote, base, true);
9306
+ const unsafe = baseBranchUnsafe(out);
9307
+ return { action: out.action, unsafe, detail: unsafe ? describeUnsafeBase(out, remote, base) : out.detail };
9273
9308
  }, reportUnplaceable = function(c, unplaceable, verified, emit2) {
9274
9309
  if (unplaceableReported.has(c.route)) return;
9275
9310
  unplaceableReported.add(c.route);
@@ -9278,7 +9313,7 @@ if (command === "passenger-sync-daemon") {
9278
9313
  else emit2.emit(note.message, { step: "release" });
9279
9314
  }
9280
9315
  };
9281
- requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, reportUnplaceable2 = reportUnplaceable;
9316
+ requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, baseHealth2 = baseHealth, reportUnplaceable2 = reportUnplaceable;
9282
9317
  let cfg;
9283
9318
  let firstRun;
9284
9319
  try {
@@ -11318,14 +11353,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
11318
11353
  let acted = 0;
11319
11354
  let baseChecked = 0;
11320
11355
  let baseActed = 0;
11356
+ let baseUnsafe = 0;
11321
11357
  for (const r of await resolvedRepos(route)) {
11322
11358
  const base = refreshBaseBranch(r.dir, r.config.branch.remote, r.config.branch.base, dryRun);
11323
11359
  if (base.action !== "not-applicable") {
11324
11360
  baseChecked++;
11325
11361
  if (base.action === "fetch-failed") emit.warn(base.detail);
11326
- else if (base.action === "diverged" || base.action === "ff-failed") {
11362
+ else if (baseBranchUnsafe(base)) {
11327
11363
  baseActed++;
11328
- emit.warn(base.detail);
11364
+ baseUnsafe++;
11365
+ emit.error(`${r.name}: DO NOT CUT A WORKTREE FROM THIS BASE \u2014 ${describeUnsafeBase(base, r.config.branch.remote, r.config.branch.base)}`);
11329
11366
  } else if (base.action === "would-fast-forward" || base.action === "fast-forwarded") {
11330
11367
  baseActed++;
11331
11368
  emit.emit(base.detail);
@@ -11357,6 +11394,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
11357
11394
  }
11358
11395
  }
11359
11396
  }
11397
+ if (baseUnsafe > 0) process.exitCode = 1;
11360
11398
  if (baseChecked > 0 && baseActed === 0) emit.emit(`${baseChecked} base branch(es) checked, all level with the remote`);
11361
11399
  if (tracked === 0) emit.emit("no worktree tracks a remote branch \u2014 nothing to sync");
11362
11400
  else if (acted === 0) emit.emit(`${tracked} worktree(s) tracking a remote, all level with it`);
@@ -11487,6 +11525,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
11487
11525
  repos: repos.map((r) => ({
11488
11526
  name: r.name,
11489
11527
  dir: r.dir,
11528
+ // Additive (CREW-1379): `unsafe` means a ship must not cut a
11529
+ // worktree or release from this base. `version` stays 1.
11530
+ baseBranch: baseHealth(r),
11490
11531
  worktreePrefix: r.config.worktrees.prefix,
11491
11532
  // A repo with no explicit `worktrees.prefix` no longer names its
11492
11533
  // worktrees `<prefix><number>` (ISSUE-969) — the checkout's own
@@ -11527,6 +11568,11 @@ watermark: ${state.watermark()}
11527
11568
  }
11528
11569
  for (const i of openShipAttention(state)) {
11529
11570
  process.stdout.write(`attention: ${i.kind} \u2014 ${i.message}
11571
+ `);
11572
+ }
11573
+ for (const r of await resolvedRepos(route)) {
11574
+ const bh = baseHealth(r);
11575
+ if (bh.unsafe) process.stdout.write(`base: ${r.name} \u2014 UNSAFE, no worktree cuts or releases: ${bh.detail}
11530
11576
  `);
11531
11577
  }
11532
11578
  if (waiting) {
@@ -11581,6 +11627,12 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
11581
11627
  process.stdout.write(` - ${p2}
11582
11628
  `);
11583
11629
  }
11630
+ const bh = baseHealth(r);
11631
+ if (bh.action !== "not-applicable") {
11632
+ process.stdout.write(` base ${config.branch.base}: ${bh.unsafe ? "ERROR \u2014 " : ""}${bh.detail}
11633
+ `);
11634
+ if (bh.unsafe) process.exitCode = 1;
11635
+ }
11584
11636
  }
11585
11637
  const missingAuthorship = await tracker.missingAuthorshipColumns();
11586
11638
  process.stdout.write(
@@ -11761,4 +11813,5 @@ var passengerApiKeys2;
11761
11813
  var endpointApiFor2;
11762
11814
  var endpointReconcileDeps2;
11763
11815
  var releaseRefused2;
11816
+ var baseHealth2;
11764
11817
  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.1",
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