catherd-cli 1.1.0 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "catherd-cli",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Herds coding agents: autopilot builds from your own Claude Code session.",
5
5
  "keywords": [
6
6
  "autopilot",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "catherd",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Herds coding agents: Claude plans and verifies, Codex, opencode or headless Claude Code workers write the code, and Jev picks the model and effort for each piece.",
5
5
  "author": {
6
6
  "name": "47vigen",
@@ -1,7 +1,7 @@
1
1
  #!/bin/sh
2
2
  # catherd's MCP launcher (spec 1.1 §12): the plugin's .mcp.json runs it. The release stamps VERSION
3
3
  # (scripts/stamp-plugin-version.mjs), so the plugin always starts the server it was released with.
4
- VERSION="1.1.0"
4
+ VERSION="1.1.1"
5
5
 
6
6
  # a global catherd at this version (catherd init installs one): start it, no resolve at all
7
7
  if command -v catherd >/dev/null 2>&1 && [ "$(catherd --version 2>/dev/null)" = "$VERSION" ]; then
@@ -23,7 +23,7 @@ Your job:
23
23
 
24
24
  The catherd server keeps `state.md` true: it rewrites it on every dispatch, climb and landing.
25
25
 
26
- **Your first call is `status()`.** Its `version` must be the one this plugin pins, `catherd-cli@1.1.0`. If it differs, stop and tell the user to restart Claude Code so the plugin and its server match.
26
+ **Your first call is `status()`.** Its `version` must be the one this plugin pins, `catherd-cli@1.1.1`. If it differs, stop and tell the user to restart Claude Code so the plugin and its server match.
27
27
 
28
28
  **You never edit product files.** Every line of code, tests or docs comes from a role, including a one-line fix.
29
29
 
@@ -274,7 +274,7 @@ The brief is the `brief` text you pass to `dispatch` (catherd writes it to the d
274
274
  4. For a worker:
275
275
  - "Read `<R>/lanes/Mx.Ly.md`": decisions, signatures, data shapes;
276
276
  - its fast check (targeted tests, lint and type check), to run until it passes;
277
- - "Run the full suite only if this brief says so", and "Wrap any full build or full test suite in `bunx catherd-cli@1.1.0 lock -- <command>`": other lanes share the machine.
277
+ - "Run the full suite only if this brief says so", and "Wrap any full build or full test suite in `bunx catherd-cli@1.1.1 lock -- <command>`": other lanes share the machine.
278
278
  5. For a reviewer: the A-lines and the changed files, with new files read in full. It reports every finding as `BLOCKER|BUG|NIT file:line — problem — fix`, covering:
279
279
  - unmet A-lines and edge cases;
280
280
  - code or abstractions nobody needs;
@@ -38,8 +38,63 @@ function realOr(p: string): string {
38
38
  /** The temp dir by its real path (macOS: /var/folders/… is /private/var/folders/…); sandboxes compare real paths. */
39
39
  export const realTmpdir = (): string => realOr(tmpdir());
40
40
 
41
- /** The directories a workspace-write worker writes besides the repo: the heavy-lock dir and the temp dir, real paths. */
42
- export const writableRoots = (): string[] => [realOr(locksDir()), realTmpdir()];
41
+ /** Lines a toolchain prints for a query (`go env GOCACHE GOMODCACHE`, `pnpm store path`); none when it is missing or fails. */
42
+ export type ToolQuery = (bin: string, args: string[]) => string[];
43
+
44
+ const queryTool: ToolQuery = (bin, args) => {
45
+ try {
46
+ const r = Bun.spawnSync([bin, ...args], { stdout: "pipe", stderr: "ignore", timeout: 5_000 });
47
+ return r.exitCode === 0
48
+ ? r.stdout
49
+ .toString()
50
+ .split("\n")
51
+ .map((l) => l.trim())
52
+ .filter((l) => l.startsWith("/"))
53
+ : [];
54
+ } catch {
55
+ return [];
56
+ }
57
+ };
58
+
59
+ let asked: string[] | null = null;
60
+ /** Go's and pnpm's effective cache settings (`go env -w` and `store-dir` live outside the env), asked once per process. */
61
+ const effectiveCaches = (query: ToolQuery): string[] =>
62
+ query === queryTool
63
+ ? (asked ??= [...query("go", ["env", "GOCACHE", "GOMODCACHE"]), ...query("pnpm", ["store", "path"])])
64
+ : [...query("go", ["env", "GOCACHE", "GOMODCACHE"]), ...query("pnpm", ["store", "path"])];
65
+
66
+ /**
67
+ * The toolchain caches a worker's checks write, where they exist: Go's build and module caches and the pnpm store
68
+ * (as the tools themselves report them, else their defaults), Bun's install cache and npm's cache. Without them a
69
+ * sandboxed `go vet` fails or starts cold in every lane.
70
+ */
71
+ export function toolchainCaches(
72
+ env = process.env,
73
+ home = env.HOME || homedir(),
74
+ query: ToolQuery = queryTool,
75
+ ): string[] {
76
+ const mac = process.platform === "darwin";
77
+ const cache = env.XDG_CACHE_HOME || join(home, ".cache");
78
+ const data = env.XDG_DATA_HOME || join(home, ".local", "share");
79
+ const gopath = (env.GOPATH || join(home, "go")).split(":")[0] as string;
80
+ const paths = [
81
+ ...effectiveCaches(query),
82
+ env.GOCACHE || (mac ? join(home, "Library", "Caches", "go-build") : join(cache, "go-build")),
83
+ env.GOMODCACHE || join(gopath, "pkg", "mod"),
84
+ env.npm_config_store_dir || (mac ? join(home, "Library", "pnpm", "store") : join(data, "pnpm", "store")),
85
+ env.BUN_INSTALL_CACHE_DIR || join(home, ".bun", "install", "cache"),
86
+ env.npm_config_cache || join(home, ".npm"),
87
+ ];
88
+ return [...new Set(paths.filter((p) => existsSync(p)).map(realOr))];
89
+ }
90
+
91
+ /**
92
+ * The directories a workspace-write worker writes besides the repo: the heavy-lock dir, the temp dir and the
93
+ * toolchain caches that exist, real paths.
94
+ */
95
+ export const writableRoots = (): string[] => [
96
+ ...new Set([realOr(locksDir()), realTmpdir(), ...toolchainCaches()]),
97
+ ];
43
98
 
44
99
  /** The usual local Docker sockets, in the order they are tried (OrbStack and Docker Desktop link /var/run/docker.sock). */
45
100
  export const dockerSocketCandidates = (home = homedir()): string[] => [
@@ -10,6 +10,16 @@ import { liveEffects } from "./effects.ts";
10
10
  import { fixtureEffects } from "./fixtures.ts";
11
11
  import { createAppKeymap } from "./providers/keymap.tsx";
12
12
  import { Providers } from "./providers/root.tsx";
13
+
14
+ /**
15
+ * OpenTUI's SystemClock counts from process start (performance.now), but the pages compare its time with the
16
+ * wall-clock timestamps catherd writes (admittedAt, createdAt, lastActivity), so it must be epoch milliseconds.
17
+ */
18
+ export class WallClock extends SystemClock {
19
+ override now(): number {
20
+ return Date.now();
21
+ }
22
+ }
13
23
  import { initialState, type Tab } from "./state.ts";
14
24
  import { detectUi, type ThemeMode } from "./theme.ts";
15
25
  import { App } from "./views/app.tsx";
@@ -72,7 +82,7 @@ export async function openTui(o: TuiOptions): Promise<number> {
72
82
  keybinds={keybinds}
73
83
  keymap={createAppKeymap(renderer)}
74
84
  effects={effects}
75
- clock={new SystemClock()}
85
+ clock={new WallClock()}
76
86
  initial={initialState(o.tab ?? "status")}
77
87
  copy={(text) => renderer.copyToClipboardOSC52(text)}
78
88
  onExit={(code, kept) => {