@tablation/crew 0.18.0 → 0.19.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.
Files changed (2) hide show
  1. package/dist/cli.js +143 -11
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2575,6 +2575,9 @@ function detectSystemd() {
2575
2575
  return !res.error;
2576
2576
  }
2577
2577
 
2578
+ // src/ship-attention.ts
2579
+ import { statfsSync } from "fs";
2580
+
2578
2581
  // src/git.ts
2579
2582
  import { execFileSync } from "child_process";
2580
2583
  import {
@@ -2732,7 +2735,20 @@ function patchIdOf(cwd, from, to) {
2732
2735
  return null;
2733
2736
  }
2734
2737
  }
2738
+ var BRANCH_TIP_TRAILER = "Branch-tip";
2739
+ function findSquashByBranchTip(cwd, key, branch, range) {
2740
+ const tip = gitOk(cwd, ["rev-parse", "--verify", "-q", `${branch}^{commit}`]);
2741
+ if (!tip) return null;
2742
+ const wanted = `${BRANCH_TIP_TRAILER}: ${tip}`;
2743
+ for (const sha of keyCommitsInRange(cwd, key, range)) {
2744
+ const body = gitOk(cwd, ["show", "-s", "--format=%B", sha]) ?? "";
2745
+ if (body.split("\n").some((l) => l.trim() === wanted)) return sha;
2746
+ }
2747
+ return null;
2748
+ }
2735
2749
  function findSquashOfBranch(cwd, key, branch, base, range) {
2750
+ const byTip = findSquashByBranchTip(cwd, key, branch, range);
2751
+ if (byTip) return byTip;
2736
2752
  const candidates = keyCommitsInRange(cwd, key, range);
2737
2753
  if (candidates.length === 0) return null;
2738
2754
  const mb = gitOk(cwd, ["merge-base", base, branch]);
@@ -3049,14 +3065,94 @@ function baseAttention(repo, outcome, remote, base, nowIso = (/* @__PURE__ */ ne
3049
3065
  if (outcome.action === "level" || outcome.action === "fast-forwarded") return { clear: baseUnsafeKey(repo) };
3050
3066
  return null;
3051
3067
  }
3068
+ var diskLowKey = (repo) => `disk_low:${repo}`;
3069
+ var LOW_DISK_BYTES = 2 * 1024 ** 3;
3070
+ var gib = (bytes) => (bytes / 1024 ** 3).toFixed(1);
3071
+ function diskAttention(repo, freeBytes, thresholdBytes = LOW_DISK_BYTES, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
3072
+ if (freeBytes >= thresholdBytes) return { clear: diskLowKey(repo) };
3073
+ return {
3074
+ raise: {
3075
+ kind: "disk_low",
3076
+ key: diskLowKey(repo),
3077
+ since: nowIso,
3078
+ message: `${repo}: only ${gib(freeBytes)} GiB free beside the checkout (need ${gib(thresholdBytes)}) \u2014 no worktree can be set up until space is freed. Resumes automatically once there is room`
3079
+ }
3080
+ };
3081
+ }
3082
+ function freeBytesAt(dir) {
3083
+ try {
3084
+ const s = statfsSync(dir);
3085
+ return Number(s.bavail) * Number(s.bsize);
3086
+ } catch {
3087
+ return null;
3088
+ }
3089
+ }
3090
+ async function checkRepoDisks(o, freeAt = freeBytesAt, thresholdBytes = LOW_DISK_BYTES) {
3091
+ for (const target of reposOf(o.route)) {
3092
+ const free = freeAt(target.dir);
3093
+ if (free === null) continue;
3094
+ const att = diskAttention(target.name, free, thresholdBytes);
3095
+ if ("raise" in att) await raiseShipAttention(o, att.raise);
3096
+ else clearShipAttention(o.state, att.clear);
3097
+ }
3098
+ }
3099
+ function parseHookMissing(key, routeName) {
3100
+ const prefix = `hook_missing:${routeName}/`;
3101
+ if (!key.startsWith(prefix)) return null;
3102
+ const rest = key.slice(prefix.length);
3103
+ const slash = rest.lastIndexOf("/");
3104
+ if (slash <= 0) return null;
3105
+ return { repo: rest.slice(0, slash), hook: rest.slice(slash + 1) };
3106
+ }
3052
3107
  function repoStopFor(items, route) {
3053
- const stops = new Map(items.filter((i) => i.kind === "base_unsafe").map((i) => [i.key, i.message]));
3108
+ const stops = /* @__PURE__ */ new Map();
3109
+ for (const i of items) {
3110
+ let repo = null;
3111
+ if (i.kind === "base_unsafe") repo = i.key.slice("base_unsafe:".length);
3112
+ else if (i.kind === "disk_low") repo = i.key.slice("disk_low:".length);
3113
+ else if (i.kind === "hook_missing") {
3114
+ const h = parseHookMissing(i.key, route.route);
3115
+ if (h?.hook === "setup") repo = h.repo;
3116
+ }
3117
+ if (repo && !stops.has(repo)) stops.set(repo, i.message);
3118
+ }
3054
3119
  if (stops.size === 0) return () => null;
3055
3120
  return (t) => {
3056
3121
  const name = repoTargetFor(route, t.repo_id)?.name;
3057
- return name ? stops.get(baseUnsafeKey(name)) ?? null : null;
3122
+ return name ? stops.get(name) ?? null : null;
3058
3123
  };
3059
3124
  }
3125
+ function recheckEnvironmentStops(state, route, ship, deps = {}) {
3126
+ const onPath = deps.onPath ?? findOnPath;
3127
+ const dockerProblem = deps.dockerProblem ?? dockerPathProblem;
3128
+ const cleared = [];
3129
+ for (const item of state.shipAttention().previous()) {
3130
+ if (item.kind === "docker_missing") {
3131
+ if (!dockerProblem(ship)) {
3132
+ clearShipAttention(state, item.key);
3133
+ cleared.push("docker");
3134
+ }
3135
+ continue;
3136
+ }
3137
+ if (item.kind !== "hook_missing") continue;
3138
+ const h = parseHookMissing(item.key, route.route);
3139
+ const target = h && reposOf(route).find((t) => t.name === h.repo);
3140
+ if (!h || !target) continue;
3141
+ let cfg;
3142
+ try {
3143
+ cfg = resolveRepoConfig(loadRepoConfig(target.dir), void 0, target.dir);
3144
+ } catch {
3145
+ continue;
3146
+ }
3147
+ const script = cfg.hooks[h.hook];
3148
+ const cmd = script ? hookCommand(script) : null;
3149
+ if (!cmd || onPath(cmd, pathFor(ship))) {
3150
+ clearShipAttention(state, item.key);
3151
+ cleared.push(`${h.repo}/${h.hook}`);
3152
+ }
3153
+ }
3154
+ return cleared;
3155
+ }
3060
3156
  function recheckBaseStops(state, route) {
3061
3157
  const open = state.shipAttention().previous().filter((i) => i.kind === "base_unsafe");
3062
3158
  const cleared = [];
@@ -4695,6 +4791,10 @@ async function decideCycle(o) {
4695
4791
  for (const repo of recheckBaseStops(state, route)) {
4696
4792
  emit.emit(`${repo}: primary checkout is level again \u2014 building lanes resume for that repo`, { step: "select" });
4697
4793
  }
4794
+ for (const what of recheckEnvironmentStops(state, route, ship)) {
4795
+ emit.emit(`${what}: no longer missing from the scheduler PATH \u2014 stop cleared`, { step: "select" });
4796
+ }
4797
+ await checkRepoDisks({ state, emit, route, ship });
4698
4798
  const selectionInput = {
4699
4799
  repoStop: repoStopFor(openShipAttention(state), route),
4700
4800
  tickets,
@@ -6391,14 +6491,9 @@ function planMerge(cwd, tickets, contract, lastReleased, branchFor = (t) => bran
6391
6491
  skipReason: "never-built"
6392
6492
  };
6393
6493
  }
6394
- const squash = findSquashOfBranch(
6395
- cwd,
6396
- referenceKeys(ticket),
6397
- branch,
6398
- base,
6399
- lastReleased ? `${lastReleased}..${base}` : base
6400
- );
6494
+ const squash = findSquashOfBranch(cwd, referenceKeys(ticket), branch, base, base);
6401
6495
  if (squash) {
6496
+ const released = !!lastReleased && isAncestor(cwd, squash, lastReleased);
6402
6497
  return {
6403
6498
  ticket,
6404
6499
  branch,
@@ -6406,7 +6501,7 @@ function planMerge(cwd, tickets, contract, lastReleased, branchFor = (t) => bran
6406
6501
  usedFallback: false,
6407
6502
  bump: "patch",
6408
6503
  majorRequested: false,
6409
- skipReason: "already-merged",
6504
+ skipReason: released ? "already-released" : "already-merged",
6410
6505
  mergedSha: squash
6411
6506
  };
6412
6507
  }
@@ -6424,6 +6519,20 @@ function planMerge(cwd, tickets, contract, lastReleased, branchFor = (t) => bran
6424
6519
  };
6425
6520
  });
6426
6521
  }
6522
+ function planReleasedBranches(cwd, tickets, contract, branchFor, base = "main", tagPattern = "v*") {
6523
+ const eligible = /* @__PURE__ */ new Set([contract.statuses.building, contract.statuses.needsHuman]);
6524
+ const out = [];
6525
+ for (const ticket of tickets) {
6526
+ if (!eligible.has(ticket.status)) continue;
6527
+ const branch = branchFor(ticket);
6528
+ if (!branch) continue;
6529
+ const sha = findSquashByBranchTip(cwd, referenceKeys(ticket), branch, base);
6530
+ if (!sha) continue;
6531
+ const tag = firstReleaseTagContaining(cwd, sha, tagPattern);
6532
+ if (tag) out.push({ ticket, sha, tag });
6533
+ }
6534
+ return out;
6535
+ }
6427
6536
  function requestedBump(bodies) {
6428
6537
  const asks = [...bodies.matchAll(BUMP_LINE)].map((m) => m[1].toLowerCase());
6429
6538
  const majorRequested = asks.includes("major");
@@ -6886,9 +6995,13 @@ function mergeOne(o, c) {
6886
6995
  );
6887
6996
  return "noop";
6888
6997
  }
6998
+ const tip = gitOk(o.cwd, ["rev-parse", "--verify", "-q", `${c.branch}^{commit}`]);
6999
+ const trailer = tip ? `
7000
+
7001
+ ${BRANCH_TIP_TRAILER}: ${tip}` : "";
6889
7002
  git(o.cwd, ["commit", "-m", `${subject}
6890
7003
 
6891
- Closes ${c.ticket.issue_id}.`]);
7004
+ Closes ${c.ticket.issue_id}.${trailer}`]);
6892
7005
  const sha = headSha(o.cwd);
6893
7006
  o.emit.emit(`merged ${c.branch}`, { ticket: c.ticket.issue_id });
6894
7007
  return { applied: true, sha };
@@ -10903,6 +11016,25 @@ ${changed2.map((c) => ` - ${c}
10903
11016
  target.dir
10904
11017
  );
10905
11018
  }
11019
+ for (const r of planReleasedBranches(
11020
+ target.dir,
11021
+ tickets,
11022
+ tracker.contract,
11023
+ (t) => locateBranchForTicket(target.dir, repo, t)?.branch ?? null,
11024
+ repo.branch.base,
11025
+ repo.release.tagPattern ?? "v*"
11026
+ )) {
11027
+ remit.enter("reconcile");
11028
+ await applyStamp(
11029
+ tracker,
11030
+ [{ ticket: r.ticket, reason: `branch already released in ${r.tag}`, sha: r.sha }],
11031
+ r.tag.replace(/^v/, ""),
11032
+ tracker.contract,
11033
+ remit,
11034
+ dryRun,
11035
+ target.dir
11036
+ );
11037
+ }
10906
11038
  if (outcome.gateRed && !dryRun) {
10907
11039
  try {
10908
11040
  const g = outcome.gateRed;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.18.0",
3
+ "version": "0.19.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",