@tablation/crew 0.18.0 → 0.19.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 +89 -2
  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 {
@@ -3049,14 +3052,94 @@ function baseAttention(repo, outcome, remote, base, nowIso = (/* @__PURE__ */ ne
3049
3052
  if (outcome.action === "level" || outcome.action === "fast-forwarded") return { clear: baseUnsafeKey(repo) };
3050
3053
  return null;
3051
3054
  }
3055
+ var diskLowKey = (repo) => `disk_low:${repo}`;
3056
+ var LOW_DISK_BYTES = 2 * 1024 ** 3;
3057
+ var gib = (bytes) => (bytes / 1024 ** 3).toFixed(1);
3058
+ function diskAttention(repo, freeBytes, thresholdBytes = LOW_DISK_BYTES, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
3059
+ if (freeBytes >= thresholdBytes) return { clear: diskLowKey(repo) };
3060
+ return {
3061
+ raise: {
3062
+ kind: "disk_low",
3063
+ key: diskLowKey(repo),
3064
+ since: nowIso,
3065
+ 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`
3066
+ }
3067
+ };
3068
+ }
3069
+ function freeBytesAt(dir) {
3070
+ try {
3071
+ const s = statfsSync(dir);
3072
+ return Number(s.bavail) * Number(s.bsize);
3073
+ } catch {
3074
+ return null;
3075
+ }
3076
+ }
3077
+ async function checkRepoDisks(o, freeAt = freeBytesAt, thresholdBytes = LOW_DISK_BYTES) {
3078
+ for (const target of reposOf(o.route)) {
3079
+ const free = freeAt(target.dir);
3080
+ if (free === null) continue;
3081
+ const att = diskAttention(target.name, free, thresholdBytes);
3082
+ if ("raise" in att) await raiseShipAttention(o, att.raise);
3083
+ else clearShipAttention(o.state, att.clear);
3084
+ }
3085
+ }
3086
+ function parseHookMissing(key, routeName) {
3087
+ const prefix = `hook_missing:${routeName}/`;
3088
+ if (!key.startsWith(prefix)) return null;
3089
+ const rest = key.slice(prefix.length);
3090
+ const slash = rest.lastIndexOf("/");
3091
+ if (slash <= 0) return null;
3092
+ return { repo: rest.slice(0, slash), hook: rest.slice(slash + 1) };
3093
+ }
3052
3094
  function repoStopFor(items, route) {
3053
- const stops = new Map(items.filter((i) => i.kind === "base_unsafe").map((i) => [i.key, i.message]));
3095
+ const stops = /* @__PURE__ */ new Map();
3096
+ for (const i of items) {
3097
+ let repo = null;
3098
+ if (i.kind === "base_unsafe") repo = i.key.slice("base_unsafe:".length);
3099
+ else if (i.kind === "disk_low") repo = i.key.slice("disk_low:".length);
3100
+ else if (i.kind === "hook_missing") {
3101
+ const h = parseHookMissing(i.key, route.route);
3102
+ if (h?.hook === "setup") repo = h.repo;
3103
+ }
3104
+ if (repo && !stops.has(repo)) stops.set(repo, i.message);
3105
+ }
3054
3106
  if (stops.size === 0) return () => null;
3055
3107
  return (t) => {
3056
3108
  const name = repoTargetFor(route, t.repo_id)?.name;
3057
- return name ? stops.get(baseUnsafeKey(name)) ?? null : null;
3109
+ return name ? stops.get(name) ?? null : null;
3058
3110
  };
3059
3111
  }
3112
+ function recheckEnvironmentStops(state, route, ship, deps = {}) {
3113
+ const onPath = deps.onPath ?? findOnPath;
3114
+ const dockerProblem = deps.dockerProblem ?? dockerPathProblem;
3115
+ const cleared = [];
3116
+ for (const item of state.shipAttention().previous()) {
3117
+ if (item.kind === "docker_missing") {
3118
+ if (!dockerProblem(ship)) {
3119
+ clearShipAttention(state, item.key);
3120
+ cleared.push("docker");
3121
+ }
3122
+ continue;
3123
+ }
3124
+ if (item.kind !== "hook_missing") continue;
3125
+ const h = parseHookMissing(item.key, route.route);
3126
+ const target = h && reposOf(route).find((t) => t.name === h.repo);
3127
+ if (!h || !target) continue;
3128
+ let cfg;
3129
+ try {
3130
+ cfg = resolveRepoConfig(loadRepoConfig(target.dir), void 0, target.dir);
3131
+ } catch {
3132
+ continue;
3133
+ }
3134
+ const script = cfg.hooks[h.hook];
3135
+ const cmd = script ? hookCommand(script) : null;
3136
+ if (!cmd || onPath(cmd, pathFor(ship))) {
3137
+ clearShipAttention(state, item.key);
3138
+ cleared.push(`${h.repo}/${h.hook}`);
3139
+ }
3140
+ }
3141
+ return cleared;
3142
+ }
3060
3143
  function recheckBaseStops(state, route) {
3061
3144
  const open = state.shipAttention().previous().filter((i) => i.kind === "base_unsafe");
3062
3145
  const cleared = [];
@@ -4695,6 +4778,10 @@ async function decideCycle(o) {
4695
4778
  for (const repo of recheckBaseStops(state, route)) {
4696
4779
  emit.emit(`${repo}: primary checkout is level again \u2014 building lanes resume for that repo`, { step: "select" });
4697
4780
  }
4781
+ for (const what of recheckEnvironmentStops(state, route, ship)) {
4782
+ emit.emit(`${what}: no longer missing from the scheduler PATH \u2014 stop cleared`, { step: "select" });
4783
+ }
4784
+ await checkRepoDisks({ state, emit, route, ship });
4698
4785
  const selectionInput = {
4699
4786
  repoStop: repoStopFor(openShipAttention(state), route),
4700
4787
  tickets,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.18.0",
3
+ "version": "0.19.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",