@herdingchats/friday 0.1.550 → 0.1.561

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/herd.mjs +906 -559
  2. package/package.json +2 -2
package/herd.mjs CHANGED
@@ -18,7 +18,7 @@ var __export = (target, all) => {
18
18
  var define_HERD_PACKAGE_default;
19
19
  var init_define_HERD_PACKAGE = __esm({
20
20
  "<define:HERD_PACKAGE>"() {
21
- define_HERD_PACKAGE_default = { version: "0.1.550", gitHead: "8fdd8ce87ad61c0e396cc832c6e165e69c6fde22" };
21
+ define_HERD_PACKAGE_default = { version: "0.1.561", gitHead: "dc277703d4460a12bee165c4c4e6653088631c16" };
22
22
  }
23
23
  });
24
24
 
@@ -689,6 +689,8 @@ export default {
689
689
  activated: z7.boolean(),
690
690
  activation: agentActivationSchema.optional(),
691
691
  online: z7.boolean(),
692
+ /** A folder its machine works in that isn't there, so it's offered no work until it is. */
693
+ folderUnavailable: z7.object({ path: z7.string().min(1), target: z7.string().min(1).optional() }).optional(),
692
694
  // Its machine's repos, each marked whether herd has cloned it there. Older daemons send no `cloned`.
693
695
  repos: z7.array(
694
696
  z7.object({ name: z7.string(), remote: z7.string(), cloned: z7.boolean().optional() })
@@ -700,7 +702,7 @@ export default {
700
702
 
701
703
  // ../common-chat/src/machines.ts
702
704
  import { z as z8 } from "zod";
703
- var nonEmpty4, time2, terminalStateSchema, agentStatusSchema, machineRepoSchema, isCloned, machineCliSchema, machineRegistrationSchema, machineSchema, machinePresenceSchema, worktreeSchema, MAX_REPO_HINTS, repoHintSchema, terminalSchema, offerSchema, offerAnswerSchema, dispatchFrameSchema, machineMessageSchema;
705
+ var nonEmpty4, time2, terminalStateSchema, agentStatusSchema, machineRepoSchema, isCloned, machineCliSchema, folderUnavailableSchema, machineRegistrationSchema, machineSchema, machinePresenceSchema, worktreeSchema, MAX_REPO_HINTS, repoHintSchema, terminalSchema, offerSchema, offerAnswerSchema, dispatchFrameSchema, machineMessageSchema;
704
706
  var init_machines = __esm({
705
707
  "../common-chat/src/machines.ts"() {
706
708
  "use strict";
@@ -732,6 +734,10 @@ var init_machines = __esm({
732
734
  capabilities: z8.array(capabilitySchema).optional(),
733
735
  activation: agentActivationSchema.optional()
734
736
  });
737
+ folderUnavailableSchema = z8.object({
738
+ path: z8.string().min(1).max(1e3),
739
+ target: z8.string().min(1).max(1e3).optional()
740
+ });
735
741
  machineRegistrationSchema = z8.object({
736
742
  id: nonEmpty4,
737
743
  name: nonEmpty4,
@@ -744,7 +750,9 @@ var init_machines = __esm({
744
750
  config: herdConfigSchema.optional(),
745
751
  configError: z8.string().max(1e3).optional(),
746
752
  // Whether its GitHub CLI is signed in, and as whom.
747
- gh: gitHubCliSchema.optional()
753
+ gh: gitHubCliSchema.optional(),
754
+ // A folder it works in that isn't there, while it takes no work.
755
+ folderUnavailable: folderUnavailableSchema.optional()
748
756
  });
749
757
  machineSchema = machineRegistrationSchema.extend({
750
758
  ownerId: nonEmpty4,
@@ -1458,6 +1466,9 @@ var init_conversation_state = __esm({
1458
1466
  "author_not_linked",
1459
1467
  // The only agents that could take it showed their own sign-in, so a person has to sign in.
1460
1468
  "agent_signed_out",
1469
+ // The only machines that could take it have a folder that isn't there, such as a drive not
1470
+ // yet mounted.
1471
+ "folder_unavailable",
1461
1472
  // The owner's "only" and "avoid" rules left no worker for the work.
1462
1473
  "no_suitable_worker",
1463
1474
  "notice_failed",
@@ -1557,6 +1568,7 @@ var init_conversation_state = __esm({
1557
1568
  author_not_linked: "the author's account isn't linked",
1558
1569
  delegate_waiting: "a delegate is waiting on someone",
1559
1570
  agent_signed_out: "its agent isn't signed in on its machine",
1571
+ folder_unavailable: "its machine's folder isn't there",
1560
1572
  no_suitable_worker: "no worker may take it under its configuration",
1561
1573
  notice_failed: "a notice didn't reach the agent's terminal",
1562
1574
  machine_offline: "its machine is offline",
@@ -1937,12 +1949,12 @@ var init_self = __esm({
1937
1949
 
1938
1950
  // src/desk-display/usb.ts
1939
1951
  import { execFileSync as execFileSync7 } from "node:child_process";
1940
- import { closeSync as closeSync4, constants, openSync as openSync5, readdirSync as readdirSync8, readSync as readSync2, writeSync } from "node:fs";
1952
+ import { closeSync as closeSync4, constants as constants2, openSync as openSync5, readdirSync as readdirSync9, readSync as readSync2, writeSync } from "node:fs";
1941
1953
  import { platform } from "node:os";
1942
1954
  import { setTimeout as delay } from "node:timers/promises";
1943
1955
  function candidates() {
1944
1956
  try {
1945
- const names = readdirSync8("/dev");
1957
+ const names = readdirSync9("/dev");
1946
1958
  return platform() === "darwin" ? names.filter((name) => name.startsWith("cu.usbmodem")).map((name) => `/dev/${name}`) : names.filter((name) => name.startsWith("ttyACM")).map((name) => `/dev/${name}`);
1947
1959
  } catch {
1948
1960
  return [];
@@ -1975,7 +1987,7 @@ var init_usb = __esm({
1975
1987
  fd;
1976
1988
  buffered = "";
1977
1989
  static open(path) {
1978
- const fd = openSync5(path, constants.O_RDWR | constants.O_NOCTTY | constants.O_NONBLOCK);
1990
+ const fd = openSync5(path, constants2.O_RDWR | constants2.O_NOCTTY | constants2.O_NONBLOCK);
1979
1991
  try {
1980
1992
  execFileSync7(
1981
1993
  "stty",
@@ -3134,16 +3146,16 @@ var init_desk_display = __esm({
3134
3146
 
3135
3147
  // src/package-entry.ts
3136
3148
  init_define_HERD_PACKAGE();
3137
- import { realpathSync as realpathSync9 } from "node:fs";
3149
+ import { realpathSync as realpathSync11 } from "node:fs";
3138
3150
  import { fileURLToPath as fileURLToPath3 } from "node:url";
3139
3151
 
3140
3152
  // src/friday/main.ts
3141
3153
  init_define_HERD_PACKAGE();
3142
3154
  init_src();
3143
- import { spawn as spawn5 } from "node:child_process";
3144
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "node:fs";
3155
+ import { spawn as spawn6 } from "node:child_process";
3156
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "node:fs";
3145
3157
  import { homedir as homedir4 } from "node:os";
3146
- import { dirname as dirname9, join as join13 } from "node:path";
3158
+ import { dirname as dirname10, join as join14 } from "node:path";
3147
3159
  import { z as z22 } from "zod";
3148
3160
 
3149
3161
  // src/auth.ts
@@ -3208,7 +3220,7 @@ async function login(openBrowser2, { api = apiUrl(), timeoutMs = 10 * 60 * 1e3 }
3208
3220
  if (!metadata.success) throw new HerdError(`Couldn't find where to sign in for ${api}.`);
3209
3221
  const meta = metadata.data;
3210
3222
  const server = createServer();
3211
- await new Promise((resolve6) => server.listen(0, "127.0.0.1", resolve6));
3223
+ await new Promise((resolve7) => server.listen(0, "127.0.0.1", resolve7));
3212
3224
  const address = server.address();
3213
3225
  const port = typeof address === "object" && address ? address.port : 0;
3214
3226
  const redirectUri = `http://127.0.0.1:${port}/callback`;
@@ -3240,7 +3252,7 @@ async function login(openBrowser2, { api = apiUrl(), timeoutMs = 10 * 60 * 1e3 }
3240
3252
  state,
3241
3253
  resource: api
3242
3254
  }).toString();
3243
- const code = new Promise((resolve6, reject) => {
3255
+ const code = new Promise((resolve7, reject) => {
3244
3256
  const timer = setTimeout(
3245
3257
  () => reject(new HerdError("Nobody signed in within 10 minutes. Run herd login again.")),
3246
3258
  timeoutMs
@@ -3254,7 +3266,7 @@ async function login(openBrowser2, { api = apiUrl(), timeoutMs = 10 * 60 * 1e3 }
3254
3266
  res.writeHead(ok ? 200 : 400, { "Content-Type": "text/plain" }).end(ok ? "Signed in to herd. You can close this tab." : "Sign-in didn't complete.");
3255
3267
  if (error) return reject(new HerdError(`Sign-in was refused: ${error}.`));
3256
3268
  if (!ok) return reject(new HerdError("Sign-in came back for a different request."));
3257
- resolve6(url.searchParams.get("code") ?? "");
3269
+ resolve7(url.searchParams.get("code") ?? "");
3258
3270
  });
3259
3271
  });
3260
3272
  code.catch(() => {
@@ -3453,9 +3465,9 @@ async function runLogout() {
3453
3465
  init_define_HERD_PACKAGE();
3454
3466
  import { execFileSync as execFileSync3 } from "node:child_process";
3455
3467
  import { randomUUID } from "node:crypto";
3456
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
3468
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, writeFileSync as writeFileSync3 } from "node:fs";
3457
3469
  import { homedir as homedir3, hostname } from "node:os";
3458
- import { basename, dirname as dirname3, join as join5, resolve } from "node:path";
3470
+ import { basename, dirname as dirname4, join as join6, resolve } from "node:path";
3459
3471
  import { z as z18 } from "zod";
3460
3472
 
3461
3473
  // src/machine-facts.ts
@@ -3762,8 +3774,8 @@ init_src();
3762
3774
  import { execFile, execFileSync as execFileSync2 } from "node:child_process";
3763
3775
  import { randomBytes as randomBytes2 } from "node:crypto";
3764
3776
  import {
3765
- existsSync,
3766
- mkdirSync as mkdirSync2,
3777
+ existsSync as existsSync2,
3778
+ mkdirSync as mkdirSync3,
3767
3779
  readdirSync as readdirSync2,
3768
3780
  readFileSync as readFileSync3,
3769
3781
  renameSync,
@@ -3771,9 +3783,83 @@ import {
3771
3783
  statSync as statSync3
3772
3784
  } from "node:fs";
3773
3785
  import { writeFile } from "node:fs/promises";
3774
- import { dirname as dirname2, isAbsolute, join as join4 } from "node:path";
3786
+ import { dirname as dirname3, isAbsolute, join as join5 } from "node:path";
3775
3787
  import { fileURLToPath } from "node:url";
3776
3788
  import { promisify } from "node:util";
3789
+
3790
+ // src/local-files.ts
3791
+ init_define_HERD_PACKAGE();
3792
+ import { spawn as spawn2 } from "node:child_process";
3793
+ import { constants, copyFileSync, existsSync, lstatSync, mkdirSync as mkdirSync2 } from "node:fs";
3794
+ import { dirname as dirname2, join as join4 } from "node:path";
3795
+ var INCLUDE_FILE = ".worktreeinclude";
3796
+ var DEFAULT_INCLUDE = ".env*";
3797
+ var HEAVY_FOLDERS = /* @__PURE__ */ new Set([
3798
+ ".git",
3799
+ "node_modules",
3800
+ ".venv",
3801
+ "venv",
3802
+ "__pycache__",
3803
+ ".next",
3804
+ ".turbo",
3805
+ ".cache"
3806
+ ]);
3807
+ var MAX_COPIED = 500;
3808
+ function gitOut(env, cwd, args, input = "") {
3809
+ return new Promise((resolve7) => {
3810
+ const child = spawn2("git", ["-C", cwd, ...args], {
3811
+ env: { ...env ?? process.env, GIT_TERMINAL_PROMPT: "0" },
3812
+ stdio: ["pipe", "pipe", "ignore"]
3813
+ });
3814
+ let out = "";
3815
+ child.stdout.setEncoding("utf8");
3816
+ child.stdout.on("data", (chunk) => {
3817
+ out += chunk;
3818
+ });
3819
+ child.on("error", () => resolve7(""));
3820
+ child.on("close", () => resolve7(out));
3821
+ child.stdin.end(input);
3822
+ });
3823
+ }
3824
+ var split = (output2) => output2.split("\0").filter(Boolean);
3825
+ async function localFiles(checkout, env) {
3826
+ const include = join4(checkout, INCLUDE_FILE);
3827
+ const patterns = existsSync(include) ? [`--exclude-from=${include}`] : [`--exclude=${DEFAULT_INCLUDE}`];
3828
+ const matched = split(
3829
+ await gitOut(env, checkout, ["ls-files", "-z", "--others", "--ignored", ...patterns])
3830
+ ).filter((path) => !path.split("/").some((part) => HEAVY_FOLDERS.has(part)));
3831
+ if (matched.length === 0) return [];
3832
+ const ignored = new Set(
3833
+ split(
3834
+ await gitOut(env, checkout, ["check-ignore", "-z", "--stdin"], `${matched.join("\0")}\0`)
3835
+ )
3836
+ );
3837
+ return matched.filter((path) => ignored.has(path)).sort();
3838
+ }
3839
+ function describeCopied(folder, copied) {
3840
+ const shown = copied.slice(0, 10).join(", ");
3841
+ const more = copied.length > 10 ? `, and ${copied.length - 10} more` : "";
3842
+ return `Copied ${copied.length === 1 ? "a local file" : "local files"} git ignores from the root's checkout into ${folder}: ${shown}${more}.`;
3843
+ }
3844
+ async function copyLocalFiles(checkout, worktree, env) {
3845
+ const copied = [];
3846
+ for (const path of await localFiles(checkout, env)) {
3847
+ if (copied.length >= MAX_COPIED) break;
3848
+ if (path.split("/").includes("..")) continue;
3849
+ const from = join4(checkout, path);
3850
+ const to = join4(worktree, path);
3851
+ try {
3852
+ if (!lstatSync(from).isFile() || existsSync(to)) continue;
3853
+ mkdirSync2(dirname2(to), { recursive: true });
3854
+ copyFileSync(from, to, constants.COPYFILE_EXCL);
3855
+ copied.push(path);
3856
+ } catch {
3857
+ }
3858
+ }
3859
+ return copied;
3860
+ }
3861
+
3862
+ // src/repo-store.ts
3777
3863
  var run = promisify(execFile);
3778
3864
  var StoreError = class extends Error {
3779
3865
  };
@@ -3806,8 +3892,8 @@ function gitError(error) {
3806
3892
  if (stderr) return stderr.slice(0, 1e3);
3807
3893
  return error instanceof Error ? error.message.slice(0, 1e3) : String(error);
3808
3894
  }
3809
- var reposDir = (root) => join4(root, "repos");
3810
- var convosDir = (root) => join4(root, "convos");
3895
+ var legacyConvosDir = (root) => join5(root, "convos");
3896
+ var checkoutLock = (root, key) => join5(root, ".herd", "locks", key);
3811
3897
  function localPath(remote) {
3812
3898
  if (remote.startsWith("file://")) {
3813
3899
  try {
@@ -3829,11 +3915,6 @@ function repoKey(remote) {
3829
3915
  }
3830
3916
  var repoName = (key) => key.split("/").at(-1) ?? key;
3831
3917
  var repoOwner = (key) => key.split("/").at(-2) ?? "";
3832
- function storePath(root, remote) {
3833
- const key = repoKey(remote);
3834
- if (!key) throw new StoreError(`${remote} isn't a repo's URL.`);
3835
- return join4(reposDir(root), `${key}.git`);
3836
- }
3837
3918
  function cloneUrl(key, known = []) {
3838
3919
  return known.find((remote) => repoKey(remote) === key) ?? `https://${key}`;
3839
3920
  }
@@ -3862,13 +3943,13 @@ function isStale(lock, content, staleMs) {
3862
3943
  return true;
3863
3944
  }
3864
3945
  }
3865
- async function withStoreLock(store, work, options = {}) {
3866
- const lock = `${store}.lock`;
3946
+ async function withStoreLock(target, work, options = {}) {
3947
+ const lock = `${target}.lock`;
3867
3948
  const pollMs = options.pollMs ?? LOCK_POLL_MS;
3868
3949
  const deadline = Date.now() + (options.timeoutMs ?? LOCK_TIMEOUT_MS);
3869
3950
  const token = `${process.pid} ${randomBytes2(8).toString("hex")}
3870
3951
  `;
3871
- mkdirSync2(dirname2(lock), { recursive: true });
3952
+ mkdirSync3(dirname3(lock), { recursive: true });
3872
3953
  for (; ; ) {
3873
3954
  try {
3874
3955
  await writeFile(lock, token, { flag: "wx" });
@@ -3892,7 +3973,7 @@ async function withStoreLock(store, work, options = {}) {
3892
3973
  if (Date.now() > deadline) {
3893
3974
  throw new StoreError(`Another process has held ${lock} for too long.`);
3894
3975
  }
3895
- await new Promise((resolve6) => setTimeout(resolve6, pollMs));
3976
+ await new Promise((resolve7) => setTimeout(resolve7, pollMs));
3896
3977
  }
3897
3978
  try {
3898
3979
  return await work();
@@ -3903,38 +3984,63 @@ async function withStoreLock(store, work, options = {}) {
3903
3984
  }
3904
3985
  }
3905
3986
  }
3906
- async function ensureClone(root, remote, env) {
3907
- const store = storePath(root, remote);
3908
- if (existsSync(store)) {
3987
+ function checkouts(root) {
3988
+ const all = reposInFolder(root).map((repo) => ({
3989
+ key: repo.key,
3990
+ remote: repo.remote,
3991
+ folder: repo.folder,
3992
+ path: join5(root, repo.folder)
3993
+ }));
3994
+ const named = (repo) => repo.folder === repoName(repo.key) ? 0 : 1;
3995
+ const best = /* @__PURE__ */ new Map();
3996
+ for (const repo of [...all].sort((a, b) => named(a) - named(b))) {
3997
+ if (!best.has(repo.key)) best.set(repo.key, repo);
3998
+ }
3999
+ return all.filter((repo) => best.get(repo.key) === repo);
4000
+ }
4001
+ var checkoutFor = (root, key) => checkouts(root).find((repo) => repo.key === key) ?? null;
4002
+ function checkoutsStamp(root) {
4003
+ let entries;
4004
+ try {
4005
+ entries = readdirSync2(root);
4006
+ } catch {
4007
+ return "";
4008
+ }
4009
+ return entries.filter((name) => !name.startsWith(".") && existsSync2(join5(root, name, ".git"))).sort().join("\n");
4010
+ }
4011
+ async function ensureCheckout(root, remote, env) {
4012
+ const key = repoKey(remote);
4013
+ if (!key) throw new StoreError(`${remote} isn't a repo's URL.`);
4014
+ const found = checkoutFor(root, key);
4015
+ if (found) {
3909
4016
  let fetchFailed = null;
3910
4017
  try {
3911
- await git(env, store, "fetch", "--quiet", "--prune", "origin");
3912
- await git(env, store, "remote", "set-head", "origin", "--auto").catch(() => "");
4018
+ await git(env, found.path, "fetch", "--quiet", "origin");
4019
+ if (!await hasRef(env, found.path, "refs/remotes/origin/HEAD")) {
4020
+ await git(env, found.path, "remote", "set-head", "origin", "--auto").catch(() => "");
4021
+ }
3913
4022
  } catch (error) {
3914
4023
  fetchFailed = error instanceof Error ? error.message : String(error);
3915
4024
  }
3916
- return { store, created: false, fetchFailed };
4025
+ return { path: found.path, created: false, fetchFailed };
3917
4026
  }
3918
- const building = `${store}.tmp-${process.pid}-${randomBytes2(4).toString("hex")}`;
3919
- mkdirSync2(dirname2(store), { recursive: true });
3920
- try {
3921
- await git(env, dirname2(store), "init", "--quiet", "--bare", building);
3922
- await git(env, building, "remote", "add", "origin", remote);
3923
- await git(
3924
- env,
3925
- building,
3926
- "config",
3927
- "remote.origin.fetch",
3928
- "+refs/heads/*:refs/remotes/origin/*"
4027
+ const name = [repoName(key), `${repoOwner(key)}-${repoName(key)}`].filter(validFolderName).find((candidate) => !existsSync2(join5(root, candidate)));
4028
+ if (!name) {
4029
+ throw new StoreError(
4030
+ `Both ${repoName(key)} and ${repoOwner(key)}-${repoName(key)} are taken in ${root}, so herd can't clone ${key} there.`
3929
4031
  );
3930
- await git(env, building, "fetch", "--quiet", "origin");
3931
- await git(env, building, "remote", "set-head", "origin", "--auto").catch(() => "");
3932
- renameSync(building, store);
4032
+ }
4033
+ const target = join5(root, name);
4034
+ const building = join5(root, `.${name}.tmp-${process.pid}-${randomBytes2(4).toString("hex")}`);
4035
+ mkdirSync3(root, { recursive: true });
4036
+ try {
4037
+ await git(env, root, "clone", "--quiet", remote, building);
4038
+ renameSync(building, target);
3933
4039
  } catch (error) {
3934
4040
  rmSync3(building, { recursive: true, force: true });
3935
4041
  throw error instanceof StoreError ? error : new StoreError(gitError(error));
3936
4042
  }
3937
- return { store, created: true, fetchFailed: null };
4043
+ return { path: target, created: true, fetchFailed: null };
3938
4044
  }
3939
4045
  async function hasRef(env, repo, ref) {
3940
4046
  try {
@@ -3944,16 +4050,16 @@ async function hasRef(env, repo, ref) {
3944
4050
  return false;
3945
4051
  }
3946
4052
  }
3947
- async function remoteDefault(store, env) {
4053
+ async function remoteDefault(repo, env) {
3948
4054
  try {
3949
- return await git(env, store, "symbolic-ref", "--short", "refs/remotes/origin/HEAD");
4055
+ return await git(env, repo, "symbolic-ref", "--short", "refs/remotes/origin/HEAD");
3950
4056
  } catch {
3951
4057
  for (const name of ["main", "master"]) {
3952
- if (await hasRef(env, store, `refs/remotes/origin/${name}`)) return `origin/${name}`;
4058
+ if (await hasRef(env, repo, `refs/remotes/origin/${name}`)) return `origin/${name}`;
3953
4059
  }
3954
4060
  const first = await git(
3955
4061
  env,
3956
- store,
4062
+ repo,
3957
4063
  "for-each-ref",
3958
4064
  "--count=1",
3959
4065
  "--format=%(refname:short)",
@@ -3967,111 +4073,72 @@ async function addWorktree(options) {
3967
4073
  const { root, remote, path, branch, env } = options;
3968
4074
  const key = repoKey(remote);
3969
4075
  if (!key) throw new StoreError(`${remote} isn't a repo's URL.`);
3970
- const store = storePath(root, remote);
3971
4076
  return withStoreLock(
3972
- store,
4077
+ checkoutLock(root, key),
3973
4078
  async () => {
3974
- const clone = await ensureClone(root, remote, env);
4079
+ const checkout = await ensureCheckout(root, remote, env);
4080
+ const repo = checkout.path;
3975
4081
  const common = {
3976
4082
  path,
3977
4083
  remote: key,
3978
4084
  branch,
3979
- fetchFailed: clone.fetchFailed,
3980
- cloned: clone.created
4085
+ fetchFailed: checkout.fetchFailed,
4086
+ cloned: checkout.created,
4087
+ checkout: repo
3981
4088
  };
3982
- if (existsSync(path)) {
4089
+ if (existsSync2(path)) {
3983
4090
  return {
3984
4091
  ...common,
3985
- base: await remoteDefault(store, env).catch(() => "origin/HEAD"),
4092
+ base: await remoteDefault(repo, env).catch(() => "origin/HEAD"),
3986
4093
  created: false,
4094
+ copied: [],
3987
4095
  ...options.continueFrom ? { continued: true } : {}
3988
4096
  };
3989
4097
  }
3990
- mkdirSync2(dirname2(path), { recursive: true });
3991
- const own = await hasRef(env, store, `refs/heads/${branch}`);
4098
+ mkdirSync3(dirname3(path), { recursive: true });
4099
+ const own = await hasRef(env, repo, `refs/heads/${branch}`);
3992
4100
  const pushed = options.continueFrom ? `origin/${options.continueFrom}` : null;
3993
- if (pushed && await hasRef(env, store, `refs/remotes/${pushed}`)) {
4101
+ let base;
4102
+ let continued;
4103
+ if (pushed && await hasRef(env, repo, `refs/remotes/${pushed}`)) {
3994
4104
  if (own) {
3995
- await git(env, store, "worktree", "add", "--quiet", path, branch);
4105
+ await git(env, repo, "worktree", "add", "--quiet", path, branch);
3996
4106
  await git(env, path, "merge", "--quiet", "--ff-only", pushed).catch(() => "");
3997
4107
  } else {
4108
+ await git(env, repo, "worktree", "add", "--quiet", "--track", "-b", branch, path, pushed);
4109
+ }
4110
+ base = pushed;
4111
+ continued = true;
4112
+ } else {
4113
+ base = await remoteDefault(repo, env);
4114
+ if (own) await git(env, repo, "worktree", "add", "--quiet", path, branch);
4115
+ else
3998
4116
  await git(
3999
4117
  env,
4000
- store,
4118
+ repo,
4001
4119
  "worktree",
4002
4120
  "add",
4003
4121
  "--quiet",
4004
- "--track",
4122
+ "--no-track",
4005
4123
  "-b",
4006
4124
  branch,
4007
4125
  path,
4008
- pushed
4126
+ base
4009
4127
  );
4010
- }
4011
- return { ...common, base: pushed, created: true, continued: true };
4128
+ if (options.continueFrom) continued = false;
4012
4129
  }
4013
- const base = await remoteDefault(store, env);
4014
- if (own) await git(env, store, "worktree", "add", "--quiet", path, branch);
4015
- else
4016
- await git(env, store, "worktree", "add", "--quiet", "--no-track", "-b", branch, path, base);
4130
+ const copied = await copyLocalFiles(repo, path, env);
4017
4131
  return {
4018
4132
  ...common,
4019
4133
  base,
4020
4134
  created: true,
4021
- ...options.continueFrom ? { continued: false } : {}
4135
+ copied,
4136
+ ...continued === void 0 ? {} : { continued }
4022
4137
  };
4023
4138
  },
4024
4139
  options.lock
4025
4140
  );
4026
4141
  }
4027
- function clonedRepos(root) {
4028
- const found = [];
4029
- const walk = (dir, depth) => {
4030
- let entries;
4031
- try {
4032
- entries = readdirSync2(dir);
4033
- } catch {
4034
- return;
4035
- }
4036
- for (const name of entries.sort()) {
4037
- const path = join4(dir, name);
4038
- if (name.endsWith(".git")) {
4039
- try {
4040
- const remote = gitSync(path, "config", "--get", "remote.origin.url");
4041
- const key = repoKey(remote);
4042
- if (key) found.push({ key, remote, store: path });
4043
- } catch {
4044
- }
4045
- } else if (depth < 8 && !name.includes(".tmp-") && !name.endsWith(".lock")) {
4046
- try {
4047
- if (statSync3(path).isDirectory()) walk(path, depth + 1);
4048
- } catch {
4049
- }
4050
- }
4051
- }
4052
- };
4053
- walk(reposDir(root), 0);
4054
- return found;
4055
- }
4056
- function clonedStamp(root) {
4057
- const names = [];
4058
- const walk = (dir, depth) => {
4059
- let entries;
4060
- try {
4061
- entries = readdirSync2(dir);
4062
- } catch {
4063
- return;
4064
- }
4065
- for (const name of entries) {
4066
- if (name.endsWith(".git")) names.push(join4(dir, name));
4067
- else if (depth < 8 && !name.includes(".tmp-") && !name.endsWith(".lock")) {
4068
- walk(join4(dir, name), depth + 1);
4069
- }
4070
- }
4071
- };
4072
- walk(reposDir(root), 0);
4073
- return names.sort().join("\n");
4074
- }
4075
4142
  function reposInFolder(folder) {
4076
4143
  let entries;
4077
4144
  try {
@@ -4082,8 +4149,8 @@ function reposInFolder(folder) {
4082
4149
  const found = [];
4083
4150
  for (const name of entries.sort()) {
4084
4151
  if (name.startsWith(".")) continue;
4085
- const path = join4(folder, name);
4086
- if (!existsSync(join4(path, ".git"))) continue;
4152
+ const path = join5(folder, name);
4153
+ if (!existsSync2(join5(path, ".git"))) continue;
4087
4154
  try {
4088
4155
  const remote = gitSync(path, "config", "--get", "remote.origin.url");
4089
4156
  const key = repoKey(remote);
@@ -4097,8 +4164,8 @@ function reposInFolder(folder) {
4097
4164
  }
4098
4165
  var validFolderName = (name) => /^[A-Za-z0-9_][A-Za-z0-9._-]{0,99}$/.test(name) && !name.endsWith(".git");
4099
4166
  function checkoutAt(path) {
4100
- if (!existsSync(path)) return { taken: false, key: null };
4101
- if (!existsSync(join4(path, ".git"))) return { taken: true, key: null };
4167
+ if (!existsSync2(path)) return { taken: false, key: null };
4168
+ if (!existsSync2(join5(path, ".git"))) return { taken: true, key: null };
4102
4169
  try {
4103
4170
  return {
4104
4171
  taken: true,
@@ -4115,7 +4182,7 @@ function folderFor(conversation, key, asked) {
4115
4182
  }
4116
4183
  const candidates2 = asked ? [asked] : [repoName(key), `${repoOwner(key)}-${repoName(key)}`].filter(validFolderName);
4117
4184
  for (const name of candidates2) {
4118
- const at = checkoutAt(join4(conversation, name));
4185
+ const at = checkoutAt(join5(conversation, name));
4119
4186
  if (!at.taken) return { name, existing: false };
4120
4187
  if (at.key === key) return { name, existing: true };
4121
4188
  }
@@ -4138,9 +4205,9 @@ var configSchema = z18.object({
4138
4205
  /** The machine's root folder, holding herd's clones and its conversations' folders. */
4139
4206
  root: z18.string().optional()
4140
4207
  });
4141
- var configPath = () => join5(herdHome(), "config.json");
4208
+ var configPath = () => join6(herdHome(), "config.json");
4142
4209
  function save(config) {
4143
- mkdirSync3(herdHome(), { recursive: true, mode: 448 });
4210
+ mkdirSync4(herdHome(), { recursive: true, mode: 448 });
4144
4211
  writeFileSync3(configPath(), JSON.stringify(config, null, 2), { mode: 384 });
4145
4212
  return config;
4146
4213
  }
@@ -4151,7 +4218,7 @@ function loadConfig() {
4151
4218
  return save({ machineId: randomUUID(), machineName: hostname(), repos: [] });
4152
4219
  }
4153
4220
  }
4154
- var defaultRoot = () => join5(homedir3(), "friday");
4221
+ var defaultRoot = () => join6(homedir3(), "friday");
4155
4222
  function readRoot() {
4156
4223
  try {
4157
4224
  return configSchema.parse(JSON.parse(readFileSync4(configPath(), "utf8"))).root ?? null;
@@ -4162,21 +4229,39 @@ function readRoot() {
4162
4229
  function setRoot(path) {
4163
4230
  return save({ ...loadConfig(), root: resolve(path) });
4164
4231
  }
4165
- async function waitForRoot(root, {
4166
- pollMs = 2e3,
4167
- exists = existsSync2,
4168
- sleep: sleep8 = (ms) => new Promise((done) => setTimeout(done, ms)),
4169
- log = (line2) => console.log(line2)
4170
- } = {}) {
4171
- const parent = dirname3(root);
4172
- if (exists(parent)) return;
4173
- log(
4174
- `Waiting for ${parent}, which holds this machine's root, to be there, as when its drive mounts.`
4175
- );
4176
- while (!exists(parent)) await sleep8(pollMs);
4177
- log(`${parent} is there.`);
4178
- }
4179
4232
  var machineRoot = () => readRoot() ?? defaultRoot();
4233
+ var preferredConvos = () => join6(homedir3(), ".friday_convos");
4234
+ function workTreeOf(folder) {
4235
+ let dir = resolve(folder);
4236
+ while (!existsSync3(dir) && dirname4(dir) !== dir) dir = dirname4(dir);
4237
+ try {
4238
+ const top = execFileSync3("git", ["-C", dir, "rev-parse", "--show-toplevel"], {
4239
+ encoding: "utf8",
4240
+ stdio: ["ignore", "pipe", "ignore"]
4241
+ }).trim();
4242
+ return top || null;
4243
+ } catch {
4244
+ return null;
4245
+ }
4246
+ }
4247
+ var convosChosen = /* @__PURE__ */ new Map();
4248
+ function convosPath(root = machineRoot()) {
4249
+ if (process.env.HERD_CONVOS) return resolve(process.env.HERD_CONVOS);
4250
+ const known = convosChosen.get(root);
4251
+ if (known) return known;
4252
+ const chosen = chooseConvos(root, preferredConvos(), workTreeOf);
4253
+ convosChosen.set(root, chosen);
4254
+ return chosen;
4255
+ }
4256
+ function convosHome(root = machineRoot()) {
4257
+ const path = convosPath(root);
4258
+ try {
4259
+ return realpathSync2(path);
4260
+ } catch {
4261
+ return path;
4262
+ }
4263
+ }
4264
+ var chooseConvos = (root, preferred, inWorkTree) => inWorkTree(preferred) ? join6(root, ".friday_convos") : preferred;
4180
4265
  function describeRepo(path) {
4181
4266
  const full = resolve(path);
4182
4267
  const git4 = (...args) => execFileSync3("git", ["-C", full, ...args], {
@@ -4241,7 +4326,8 @@ function registration(config, clis, facts = {}, cloned = []) {
4241
4326
  ...facts.tools ? { tools: facts.tools } : {},
4242
4327
  ...facts.config ? { config: facts.config } : {},
4243
4328
  ...facts.configError ? { configError: facts.configError.slice(0, 1e3) } : {},
4244
- ...facts.gh ? { gh: facts.gh } : {}
4329
+ ...facts.gh ? { gh: facts.gh } : {},
4330
+ ...facts.folderUnavailable ? { folderUnavailable: facts.folderUnavailable } : {}
4245
4331
  };
4246
4332
  }
4247
4333
  function describeCli(name, version) {
@@ -4332,15 +4418,15 @@ async function readSetup(fetch2, init) {
4332
4418
  // src/friday/agents.ts
4333
4419
  init_define_HERD_PACKAGE();
4334
4420
  init_src();
4335
- import { existsSync as existsSync4, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
4336
- import { dirname as dirname5, join as join7 } from "node:path";
4421
+ import { existsSync as existsSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "node:fs";
4422
+ import { dirname as dirname6, join as join8 } from "node:path";
4337
4423
 
4338
4424
  // src/activation.ts
4339
4425
  init_define_HERD_PACKAGE();
4340
4426
  init_src();
4341
4427
  import { randomBytes as randomBytes4, randomUUID as randomUUID3 } from "node:crypto";
4342
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
4343
- import { dirname as dirname4, join as join6 } from "node:path";
4428
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
4429
+ import { dirname as dirname5, join as join7 } from "node:path";
4344
4430
  import { z as z21 } from "zod";
4345
4431
 
4346
4432
  // src/permissions.ts
@@ -4352,7 +4438,7 @@ import { z as z20 } from "zod";
4352
4438
  // src/local-socket.ts
4353
4439
  init_define_HERD_PACKAGE();
4354
4440
  import { randomBytes as randomBytes3 } from "node:crypto";
4355
- import { chmodSync as chmodSync2, existsSync as existsSync3, rmSync as rmSync4 } from "node:fs";
4441
+ import { chmodSync as chmodSync2, existsSync as existsSync4, rmSync as rmSync4 } from "node:fs";
4356
4442
  import { connect, createServer as createServer2 } from "node:net";
4357
4443
  import { z as z19 } from "zod";
4358
4444
  var localRequestSchema = z19.object({
@@ -4485,11 +4571,11 @@ var LocalSocket = class {
4485
4571
  }
4486
4572
  }
4487
4573
  async listen() {
4488
- if (existsSync3(this.path)) rmSync4(this.path);
4574
+ if (existsSync4(this.path)) rmSync4(this.path);
4489
4575
  this.server = createServer2((socket) => this.serve(socket));
4490
- await new Promise((resolve6, reject) => {
4576
+ await new Promise((resolve7, reject) => {
4491
4577
  this.server?.once("error", reject);
4492
- this.server?.listen(this.path, () => resolve6());
4578
+ this.server?.listen(this.path, () => resolve7());
4493
4579
  });
4494
4580
  chmodSync2(this.path, 384);
4495
4581
  }
@@ -4501,7 +4587,7 @@ var LocalSocket = class {
4501
4587
  async close() {
4502
4588
  this.closing = true;
4503
4589
  for (const socket of this.open) socket.destroy();
4504
- await new Promise((resolve6) => this.server ? this.server.close(resolve6) : resolve6(null));
4590
+ await new Promise((resolve7) => this.server ? this.server.close(resolve7) : resolve7(null));
4505
4591
  rmSync4(this.path, { force: true });
4506
4592
  }
4507
4593
  serve(socket) {
@@ -4548,7 +4634,7 @@ var LocalSocket = class {
4548
4634
  }
4549
4635
  };
4550
4636
  function askDaemon(path, request) {
4551
- return new Promise((resolve6, reject) => {
4637
+ return new Promise((resolve7, reject) => {
4552
4638
  const socket = connect(path);
4553
4639
  let data = "";
4554
4640
  socket.on("connect", () => socket.write(`${JSON.stringify(request)}
@@ -4556,7 +4642,7 @@ function askDaemon(path, request) {
4556
4642
  socket.on("data", (chunk) => data += chunk);
4557
4643
  socket.on("end", () => {
4558
4644
  try {
4559
- resolve6(JSON.parse(data));
4645
+ resolve7(JSON.parse(data));
4560
4646
  } catch {
4561
4647
  reject(new Error("The daemon's answer wasn't readable."));
4562
4648
  }
@@ -4566,14 +4652,14 @@ function askDaemon(path, request) {
4566
4652
  });
4567
4653
  }
4568
4654
  function isAnswering(path) {
4569
- return new Promise((resolve6) => {
4570
- if (!existsSync3(path)) return resolve6(false);
4655
+ return new Promise((resolve7) => {
4656
+ if (!existsSync4(path)) return resolve7(false);
4571
4657
  const socket = connect(path);
4572
4658
  socket.on("connect", () => {
4573
4659
  socket.destroy();
4574
- resolve6(true);
4660
+ resolve7(true);
4575
4661
  });
4576
- socket.on("error", () => resolve6(false));
4662
+ socket.on("error", () => resolve7(false));
4577
4663
  });
4578
4664
  }
4579
4665
 
@@ -4729,7 +4815,7 @@ async function permissionHook(raw, options = {}) {
4729
4815
  } catch {
4730
4816
  if (failingSince === null || Date.now() - started > 5e3) failingSince = Date.now();
4731
4817
  if (Date.now() - failingSince >= reconnectMs) return "";
4732
- await new Promise((resolve6) => setTimeout(resolve6, retryMs));
4818
+ await new Promise((resolve7) => setTimeout(resolve7, retryMs));
4733
4819
  }
4734
4820
  }
4735
4821
  }
@@ -4746,14 +4832,14 @@ function newActivationCode() {
4746
4832
  return [...randomBytes4(8)].map((byte) => CODE_LETTERS[byte % CODE_LETTERS.length]).join("");
4747
4833
  }
4748
4834
  var activationTerminal = (agent) => `herd-activate-${agentShortName(agent)}`;
4749
- var activationFolder = (root, agent) => join6(convosDir(root), ".activate", agentShortName(agent));
4835
+ var activationFolder = (root, agent) => join7(convosHome(root), ".activate", agentShortName(agent));
4750
4836
  var recordSchema = agentActivationSchema.extend({
4751
4837
  /** While it's waiting: the terminal it runs in, which `friday start` can attach to. */
4752
4838
  terminal: z21.string().optional()
4753
4839
  });
4754
4840
  var storeSchema = z21.object({ agents: z21.record(z21.string(), recordSchema) });
4755
- var activationsPath = (home = herdHome()) => join6(home, "activations.json");
4756
- var activationRequestPath = (home = herdHome()) => join6(home, "activate-request");
4841
+ var activationsPath = (home = herdHome()) => join7(home, "activations.json");
4842
+ var activationRequestPath = (home = herdHome()) => join7(home, "activate-request");
4757
4843
  function readActivations(path = activationsPath()) {
4758
4844
  try {
4759
4845
  return storeSchema.parse(JSON.parse(readFileSync5(path, "utf8"))).agents;
@@ -4762,7 +4848,7 @@ function readActivations(path = activationsPath()) {
4762
4848
  }
4763
4849
  }
4764
4850
  function requestActivation(home = herdHome(), now = Date.now()) {
4765
- mkdirSync4(home, { recursive: true, mode: 448 });
4851
+ mkdirSync5(home, { recursive: true, mode: 448 });
4766
4852
  writeFileSync4(activationRequestPath(home), String(now), { mode: 384 });
4767
4853
  }
4768
4854
  function needsActivation(record, version) {
@@ -4779,7 +4865,7 @@ function activateDecision(request, code) {
4779
4865
  }
4780
4866
  };
4781
4867
  }
4782
- var sleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
4868
+ var sleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
4783
4869
  var label = (agent) => AGENTS[agent]?.label ?? agent;
4784
4870
  var Activator = class {
4785
4871
  constructor(options) {
@@ -4807,7 +4893,7 @@ var Activator = class {
4807
4893
  }
4808
4894
  record(agent, record) {
4809
4895
  const agents = { ...readActivations(this.path), [agent]: record };
4810
- mkdirSync4(dirname4(this.path), { recursive: true, mode: 448 });
4896
+ mkdirSync5(dirname5(this.path), { recursive: true, mode: 448 });
4811
4897
  const temporary = `${this.path}.${process.pid}`;
4812
4898
  writeFileSync4(temporary, JSON.stringify({ agents }, null, 2), { mode: 384 });
4813
4899
  renameSync2(temporary, this.path);
@@ -4901,7 +4987,7 @@ var Activator = class {
4901
4987
  const terminal = activationTerminal(agent);
4902
4988
  const folder = activationFolder(this.options.root(), agent);
4903
4989
  rmSync5(folder, { recursive: true, force: true });
4904
- mkdirSync4(folder, { recursive: true });
4990
+ mkdirSync5(folder, { recursive: true });
4905
4991
  const current = { agent, code, terminal, checkedIn: false, wake: () => {
4906
4992
  } };
4907
4993
  this.current = current;
@@ -4955,7 +5041,7 @@ var Activator = class {
4955
5041
  const until = Date.now() + (this.options.killWaitMs ?? KILL_WAIT_MS);
4956
5042
  while (await stillRunning()) {
4957
5043
  if (Date.now() >= until) return;
4958
- await new Promise((resolve6) => setTimeout(resolve6, Math.min(pollMs, 100)));
5044
+ await new Promise((resolve7) => setTimeout(resolve7, Math.min(pollMs, 100)));
4959
5045
  }
4960
5046
  }
4961
5047
  const shell = process.env.SHELL ?? "/bin/sh";
@@ -5009,11 +5095,11 @@ var Activator = class {
5009
5095
  await start();
5010
5096
  }
5011
5097
  if (current.checkedIn) break;
5012
- await new Promise((resolve6) => {
5013
- const timer = setTimeout(resolve6, pollMs);
5098
+ await new Promise((resolve7) => {
5099
+ const timer = setTimeout(resolve7, pollMs);
5014
5100
  current.wake = () => {
5015
5101
  clearTimeout(timer);
5016
- resolve6();
5102
+ resolve7();
5017
5103
  };
5018
5104
  });
5019
5105
  }
@@ -5087,8 +5173,8 @@ async function chooseAgents(options, print) {
5087
5173
  const asked = options.given ? null : await (options.pick?.(choices) ?? Promise.resolve(null));
5088
5174
  const shares = options.given ?? asked ?? // No terminal: what was chosen before, or what's installed.
5089
5175
  Object.fromEntries(choices.map((choice) => [choice.cli, choice.share]));
5090
- if (!sameChoice(saved, shares) || !existsSync4(options.configPath)) {
5091
- mkdirSync5(dirname5(options.configPath), { recursive: true, mode: 448 });
5176
+ if (!sameChoice(saved, shares) || !existsSync5(options.configPath)) {
5177
+ mkdirSync6(dirname6(options.configPath), { recursive: true, mode: 448 });
5092
5178
  writeFileSync5(options.configPath, configSource(withChoice(saved, shares)), { mode: 384 });
5093
5179
  }
5094
5180
  const chosen = ADAPTERS.map(({ cli }) => cli).filter((cli) => (shares[cli] ?? "off") !== "off");
@@ -5101,7 +5187,7 @@ function askToActivate(herdHome2, now = Date.now()) {
5101
5187
  requestActivation(herdHome2, now);
5102
5188
  return now;
5103
5189
  }
5104
- var sleep2 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
5190
+ var sleep2 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
5105
5191
  function settledLine(cli, activation) {
5106
5192
  if (activation.state === "activated") return `${label2(cli)} is activated.`;
5107
5193
  return `${label2(cli)} ${activationText(cli, activation).replace(" there", " on this computer")}.`;
@@ -5156,7 +5242,7 @@ async function awaitActivations(options, chosen, since, print) {
5156
5242
  }
5157
5243
  return settled;
5158
5244
  }
5159
- var herdConfigIn = (herdHome2) => join7(herdHome2, "herd.config.ts");
5245
+ var herdConfigIn = (herdHome2) => join8(herdHome2, "herd.config.ts");
5160
5246
 
5161
5247
  // src/friday/home.ts
5162
5248
  init_define_HERD_PACKAGE();
@@ -5164,33 +5250,33 @@ init_src();
5164
5250
  import { spawnSync } from "node:child_process";
5165
5251
  import {
5166
5252
  closeSync as closeSync2,
5167
- existsSync as existsSync5,
5168
- mkdirSync as mkdirSync6,
5253
+ existsSync as existsSync6,
5254
+ mkdirSync as mkdirSync7,
5169
5255
  openSync as openSync2,
5170
5256
  readFileSync as readFileSync6,
5171
5257
  rmSync as rmSync6,
5172
5258
  statSync as statSync5,
5173
5259
  writeFileSync as writeFileSync6
5174
5260
  } from "node:fs";
5175
- import { dirname as dirname6, join as join8, resolve as resolve2 } from "node:path";
5261
+ import { dirname as dirname7, join as join9, resolve as resolve2 } from "node:path";
5176
5262
  var PACKAGE_NAME = FRIDAY_PACKAGE;
5177
5263
  function fridayPaths(herdHome2) {
5178
- const logs = join8(herdHome2, "logs");
5264
+ const logs = join9(herdHome2, "logs");
5179
5265
  return {
5180
5266
  herdHome: herdHome2,
5181
- config: join8(herdHome2, "config.json"),
5182
- versions: join8(herdHome2, "versions"),
5183
- current: join8(herdHome2, "current"),
5184
- pin: join8(herdHome2, "pin"),
5267
+ config: join9(herdHome2, "config.json"),
5268
+ versions: join9(herdHome2, "versions"),
5269
+ current: join9(herdHome2, "current"),
5270
+ pin: join9(herdHome2, "pin"),
5185
5271
  logs,
5186
- daemonLog: join8(logs, "daemon.log"),
5187
- supervisorLog: join8(logs, "supervisor.log"),
5188
- updateLock: join8(herdHome2, "update.lock"),
5189
- status: join8(herdHome2, "supervisor.json"),
5190
- terminals: join8(herdHome2, "terminals")
5272
+ daemonLog: join9(logs, "daemon.log"),
5273
+ supervisorLog: join9(logs, "supervisor.log"),
5274
+ updateLock: join9(herdHome2, "update.lock"),
5275
+ status: join9(herdHome2, "supervisor.json"),
5276
+ terminals: join9(herdHome2, "terminals")
5191
5277
  };
5192
5278
  }
5193
- var packageDir = (versionDir) => join8(versionDir, "node_modules", ...PACKAGE_NAME.split("/"));
5279
+ var packageDir = (versionDir) => join9(versionDir, "node_modules", ...PACKAGE_NAME.split("/"));
5194
5280
  function alive2(pid) {
5195
5281
  if (!Number.isInteger(pid) || pid <= 0) return false;
5196
5282
  try {
@@ -5201,8 +5287,8 @@ function alive2(pid) {
5201
5287
  }
5202
5288
  }
5203
5289
  function takeLock(path, staleMs = 10 * 6e4) {
5204
- mkdirSync6(dirname6(path), { recursive: true });
5205
- if (existsSync5(path)) {
5290
+ mkdirSync7(dirname7(path), { recursive: true });
5291
+ if (existsSync6(path)) {
5206
5292
  const holder2 = Number(readFileSync6(path, "utf8").trim());
5207
5293
  const old = Date.now() - statSync5(path).mtimeMs > staleMs;
5208
5294
  if (old || !alive2(holder2)) rmSync6(path, { force: true });
@@ -5224,7 +5310,7 @@ function readStatus(paths) {
5224
5310
  }
5225
5311
  }
5226
5312
  function writeStatus(paths, status2) {
5227
- mkdirSync6(paths.herdHome, { recursive: true, mode: 448 });
5313
+ mkdirSync7(paths.herdHome, { recursive: true, mode: 448 });
5228
5314
  writeFileSync6(paths.status, `${JSON.stringify(status2, null, 2)}
5229
5315
  `);
5230
5316
  }
@@ -5247,12 +5333,12 @@ function writeRoot(paths, root) {
5247
5333
  } catch {
5248
5334
  config = {};
5249
5335
  }
5250
- mkdirSync6(paths.herdHome, { recursive: true, mode: 448 });
5336
+ mkdirSync7(paths.herdHome, { recursive: true, mode: 448 });
5251
5337
  writeFileSync6(paths.config, JSON.stringify({ ...config, root }, null, 2), { mode: 384 });
5252
5338
  }
5253
5339
  function insideWorkTree(folder) {
5254
5340
  let dir = resolve2(folder);
5255
- while (!existsSync5(dir) && dirname6(dir) !== dir) dir = dirname6(dir);
5341
+ while (!existsSync6(dir) && dirname7(dir) !== dir) dir = dirname7(dir);
5256
5342
  const result = spawnSync("git", ["-C", dir, "rev-parse", "--show-toplevel"], {
5257
5343
  encoding: "utf8",
5258
5344
  stdio: ["ignore", "pipe", "ignore"]
@@ -5316,7 +5402,7 @@ function pickInTerminal(choices, input = process.stdin, output2 = process.stdout
5316
5402
  for (const line2 of lines) output2.write(`\x1B[2K${line2}
5317
5403
  `);
5318
5404
  };
5319
- return new Promise((resolve6) => {
5405
+ return new Promise((resolve7) => {
5320
5406
  const wasRaw = input.isRaw;
5321
5407
  input.setRawMode(true);
5322
5408
  input.resume();
@@ -5325,7 +5411,7 @@ function pickInTerminal(choices, input = process.stdin, output2 = process.stdout
5325
5411
  input.off("data", onData);
5326
5412
  input.setRawMode(wasRaw);
5327
5413
  input.pause();
5328
- resolve6(shares);
5414
+ resolve7(shares);
5329
5415
  };
5330
5416
  const onData = (data) => {
5331
5417
  const next = pickerKey(state, data.toString("utf8"));
@@ -5340,25 +5426,27 @@ function pickInTerminal(choices, input = process.stdin, output2 = process.stdout
5340
5426
 
5341
5427
  // src/friday/service.ts
5342
5428
  init_define_HERD_PACKAGE();
5343
- import { spawn as spawn2 } from "node:child_process";
5344
- import { existsSync as existsSync6, mkdirSync as mkdirSync7, openSync as openSync3, readFileSync as readFileSync7, rmSync as rmSync7, writeFileSync as writeFileSync7 } from "node:fs";
5429
+ import { spawn as spawn3 } from "node:child_process";
5430
+ import { existsSync as existsSync7, mkdirSync as mkdirSync8, openSync as openSync3, readFileSync as readFileSync7, rmSync as rmSync7, writeFileSync as writeFileSync7 } from "node:fs";
5345
5431
  import { userInfo } from "node:os";
5346
- import { dirname as dirname7, join as join9 } from "node:path";
5432
+ import { dirname as dirname8, join as join10, win32 } from "node:path";
5433
+ var systemdBooted = () => existsSync7("/run/systemd/system");
5347
5434
  var LAUNCH_AGENT = "com.herdingchats.friday";
5348
5435
  var SYSTEMD_UNIT = "herdingchats-friday.service";
5349
- async function detectService(run4, platform2 = process.platform, override = process.env.HERD_SERVICE) {
5436
+ async function detectService(run4, platform2 = process.platform, override = process.env.HERD_SERVICE, booted = systemdBooted) {
5350
5437
  if (override === "launchd" || override === "systemd" || override === "background")
5351
5438
  return override;
5352
5439
  if (platform2 === "darwin") return "launchd";
5353
- if (platform2 === "linux") {
5440
+ if (platform2 === "linux" && booted()) {
5354
5441
  const probe = await run4("systemctl", ["--user", "show-environment"], { timeoutMs: 1e4 });
5355
5442
  if (probe.status === 0) return "systemd";
5356
5443
  }
5357
5444
  return "background";
5358
5445
  }
5359
- function servicePath(node) {
5446
+ function servicePath(node, platform2 = process.platform, inherited = process.env.PATH ?? process.env.Path) {
5447
+ if (platform2 === "win32") return [win32.dirname(node), inherited].filter(Boolean).join(";");
5360
5448
  return [
5361
- dirname7(node),
5449
+ dirname8(node),
5362
5450
  "/opt/homebrew/bin",
5363
5451
  "/usr/local/bin",
5364
5452
  "/usr/bin",
@@ -5429,13 +5517,13 @@ WantedBy=default.target
5429
5517
  `;
5430
5518
  }
5431
5519
  function writeIfChanged(path, text4) {
5432
- if (existsSync6(path) && readFileSync7(path, "utf8") === text4) return false;
5433
- mkdirSync7(dirname7(path), { recursive: true });
5520
+ if (existsSync7(path) && readFileSync7(path, "utf8") === text4) return false;
5521
+ mkdirSync8(dirname8(path), { recursive: true });
5434
5522
  writeFileSync7(path, text4);
5435
5523
  return true;
5436
5524
  }
5437
5525
  function launchd(context2) {
5438
- const plist = join9(context2.home, "Library", "LaunchAgents", `${LAUNCH_AGENT}.plist`);
5526
+ const plist = join10(context2.home, "Library", "LaunchAgents", `${LAUNCH_AGENT}.plist`);
5439
5527
  const domain = `gui/${userInfo().uid}`;
5440
5528
  const target = `${domain}/${LAUNCH_AGENT}`;
5441
5529
  const loaded = async () => (await context2.run("launchctl", ["print", target])).status === 0;
@@ -5461,7 +5549,7 @@ function launchd(context2) {
5461
5549
  };
5462
5550
  }
5463
5551
  function systemd(context2) {
5464
- const unit = join9(context2.home, ".config", "systemd", "user", SYSTEMD_UNIT);
5552
+ const unit = join10(context2.home, ".config", "systemd", "user", SYSTEMD_UNIT);
5465
5553
  const systemctl = async (...args) => {
5466
5554
  const ran = await context2.run("systemctl", ["--user", ...args]);
5467
5555
  if (ran.status !== 0)
@@ -5485,15 +5573,26 @@ function systemd(context2) {
5485
5573
  };
5486
5574
  }
5487
5575
  function spawnInBackground(command, env, log) {
5488
- mkdirSync7(dirname7(log), { recursive: true });
5576
+ mkdirSync8(dirname8(log), { recursive: true });
5489
5577
  const out = openSync3(log, "a");
5490
5578
  const [program = process.execPath, ...args] = command;
5491
- spawn2(program, args, {
5579
+ spawn3(program, args, {
5492
5580
  detached: true,
5581
+ // On Windows, a detached process otherwise opens a console window of its own.
5582
+ windowsHide: true,
5493
5583
  stdio: ["ignore", out, out],
5494
- env: { ...process.env, ...env }
5584
+ env: mergeEnv(process.env, env)
5495
5585
  }).unref();
5496
5586
  }
5587
+ function mergeEnv(base, extra) {
5588
+ const replaced = new Set(Object.keys(extra).map((key) => key.toUpperCase()));
5589
+ const merged = {};
5590
+ for (const [key, value] of Object.entries(base)) {
5591
+ if (value !== void 0 && !(key.toUpperCase() === "PATH" && replaced.has("PATH")))
5592
+ merged[key] = value;
5593
+ }
5594
+ return { ...merged, ...extra };
5595
+ }
5497
5596
  function background(context2) {
5498
5597
  const launch = context2.spawnBackground ?? spawnInBackground;
5499
5598
  const supervisor = () => {
@@ -5529,25 +5628,25 @@ function serviceFor(kind, context2) {
5529
5628
  // src/friday/start.ts
5530
5629
  init_define_HERD_PACKAGE();
5531
5630
  init_src();
5532
- import { mkdirSync as mkdirSync9, realpathSync as realpathSync3 } from "node:fs";
5533
- import { join as join12, resolve as resolve3 } from "node:path";
5631
+ import { mkdirSync as mkdirSync10, realpathSync as realpathSync4 } from "node:fs";
5632
+ import { join as join13, resolve as resolve3 } from "node:path";
5534
5633
 
5535
5634
  // src/friday/legacy.ts
5536
5635
  init_define_HERD_PACKAGE();
5537
- import { existsSync as existsSync7, readdirSync as readdirSync3, rmSync as rmSync8 } from "node:fs";
5636
+ import { existsSync as existsSync8, readdirSync as readdirSync3, rmSync as rmSync8 } from "node:fs";
5538
5637
  import { userInfo as userInfo2 } from "node:os";
5539
- import { join as join10 } from "node:path";
5638
+ import { join as join11 } from "node:path";
5540
5639
  var OLD_LAUNCH_AGENT = "com.herdingchats.herd";
5541
5640
  var OLD_APPS = ["herd-daemon", "herd-updater"];
5542
5641
  function oldPm2s(paths) {
5543
- const dir = join10(paths.herdHome, "daemon");
5544
- if (!existsSync7(dir)) return [];
5545
- return readdirSync3(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join10(dir, entry.name, "node_modules", "pm2", "bin", "pm2")).filter((pm2) => existsSync7(pm2));
5642
+ const dir = join11(paths.herdHome, "daemon");
5643
+ if (!existsSync8(dir)) return [];
5644
+ return readdirSync3(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join11(dir, entry.name, "node_modules", "pm2", "bin", "pm2")).filter((pm2) => existsSync8(pm2));
5546
5645
  }
5547
5646
  async function retireOldDaemon(paths, { home, run: run4 }) {
5548
5647
  const removed = [];
5549
- const plist = join10(home, "Library", "LaunchAgents", `${OLD_LAUNCH_AGENT}.plist`);
5550
- if (existsSync7(plist)) {
5648
+ const plist = join11(home, "Library", "LaunchAgents", `${OLD_LAUNCH_AGENT}.plist`);
5649
+ if (existsSync8(plist)) {
5551
5650
  await run4("launchctl", ["bootout", `gui/${userInfo2().uid}/${OLD_LAUNCH_AGENT}`]);
5552
5651
  rmSync8(plist, { force: true });
5553
5652
  removed.push(`the login item ${OLD_LAUNCH_AGENT}`);
@@ -5571,24 +5670,24 @@ async function retireOldDaemon(paths, { home, run: run4 }) {
5571
5670
 
5572
5671
  // src/friday/versions.ts
5573
5672
  init_define_HERD_PACKAGE();
5574
- import { spawn as spawn3 } from "node:child_process";
5673
+ import { spawn as spawn4 } from "node:child_process";
5575
5674
  import {
5576
- existsSync as existsSync8,
5577
- mkdirSync as mkdirSync8,
5675
+ existsSync as existsSync9,
5676
+ mkdirSync as mkdirSync9,
5578
5677
  readdirSync as readdirSync4,
5579
5678
  readFileSync as readFileSync8,
5580
5679
  readlinkSync,
5581
- realpathSync as realpathSync2,
5680
+ realpathSync as realpathSync3,
5582
5681
  renameSync as renameSync3,
5583
5682
  rmSync as rmSync9,
5584
5683
  symlinkSync,
5585
5684
  writeFileSync as writeFileSync8
5586
5685
  } from "node:fs";
5587
- import { basename as basename2, dirname as dirname8, join as join11 } from "node:path";
5588
- var exec = (command, args, options = {}) => new Promise((resolve6) => {
5686
+ import { basename as basename2, dirname as dirname9, join as join12 } from "node:path";
5687
+ var exec = (command, args, options = {}) => new Promise((resolve7) => {
5589
5688
  let stdout = "";
5590
5689
  let stderr = "";
5591
- const child = spawn3(command, args, {
5690
+ const child = spawn4(command, args, {
5592
5691
  cwd: options.cwd,
5593
5692
  env: options.env ?? process.env,
5594
5693
  stdio: ["ignore", "pipe", "pipe"]
@@ -5598,16 +5697,16 @@ var exec = (command, args, options = {}) => new Promise((resolve6) => {
5598
5697
  child.stderr.on("data", (data) => stderr += data);
5599
5698
  child.on("error", (error) => {
5600
5699
  if (timer) clearTimeout(timer);
5601
- resolve6({ status: 127, stdout, stderr: `${stderr}${error.message}` });
5700
+ resolve7({ status: 127, stdout, stderr: `${stderr}${error.message}` });
5602
5701
  });
5603
5702
  child.on("close", (status2) => {
5604
5703
  if (timer) clearTimeout(timer);
5605
- resolve6({ status: status2 ?? 1, stdout, stderr });
5704
+ resolve7({ status: status2 ?? 1, stdout, stderr });
5606
5705
  });
5607
5706
  });
5608
5707
  function npmCommand() {
5609
- const beside = join11(dirname8(process.execPath), "npm");
5610
- return existsSync8(beside) ? beside : "npm";
5708
+ const beside = join12(dirname9(process.execPath), "npm");
5709
+ return existsSync9(beside) ? beside : "npm";
5611
5710
  }
5612
5711
  function currentVersion(paths) {
5613
5712
  try {
@@ -5617,10 +5716,10 @@ function currentVersion(paths) {
5617
5716
  }
5618
5717
  }
5619
5718
  function installedVersions(paths) {
5620
- if (!existsSync8(paths.versions)) return [];
5719
+ if (!existsSync9(paths.versions)) return [];
5621
5720
  return readdirSync4(paths.versions).filter((name) => VERSION.test(name)).sort(compareVersions);
5622
5721
  }
5623
- var binOf = (versionDir, bin) => join11(packageDir(versionDir), `${bin}.mjs`);
5722
+ var binOf = (versionDir, bin) => join12(packageDir(versionDir), `${bin}.mjs`);
5624
5723
  async function versionRuns(versionDir, version, run4 = exec) {
5625
5724
  const ran = await run4(process.execPath, [binOf(versionDir, "herd"), "--version"], {
5626
5725
  timeoutMs: 3e4
@@ -5633,7 +5732,7 @@ async function versionRuns(versionDir, version, run4 = exec) {
5633
5732
  }
5634
5733
  function recordedIntegrity(dir) {
5635
5734
  try {
5636
- const lock = JSON.parse(readFileSync8(join11(dir, "package-lock.json"), "utf8"));
5735
+ const lock = JSON.parse(readFileSync8(join12(dir, "package-lock.json"), "utf8"));
5637
5736
  return lock.packages?.[`node_modules/${PACKAGE_NAME}`]?.integrity ?? null;
5638
5737
  } catch {
5639
5738
  return null;
@@ -5647,8 +5746,8 @@ function npmFreeEnv() {
5647
5746
  async function installVersion(paths, version, { spec, integrity, run: run4 = exec, env = {}, log = () => {
5648
5747
  } } = {}) {
5649
5748
  if (!VERSION.test(version)) return { ok: false, reason: `${version} isn't a version.` };
5650
- const dir = join11(paths.versions, version);
5651
- if (existsSync8(dir)) {
5749
+ const dir = join12(paths.versions, version);
5750
+ if (existsSync9(dir)) {
5652
5751
  const installed2 = recordedIntegrity(dir);
5653
5752
  if (integrity && installed2 !== integrity)
5654
5753
  return { ok: false, reason: `the installed ${version} doesn't have the integrity named.` };
@@ -5657,12 +5756,12 @@ async function installVersion(paths, version, { spec, integrity, run: run4 = exe
5657
5756
  return { ok: false, reason: `the installed ${version} doesn't run: ${broken2}.` };
5658
5757
  }
5659
5758
  sweepStaging(paths);
5660
- const named = join11(paths.versions, `.staging-${version}-${process.pid}`);
5759
+ const named = join12(paths.versions, `.staging-${version}-${process.pid}`);
5661
5760
  rmSync9(named, { recursive: true, force: true });
5662
- mkdirSync8(named, { recursive: true });
5663
- const staging = realpathSync2(named);
5761
+ mkdirSync9(named, { recursive: true });
5762
+ const staging = realpathSync3(named);
5664
5763
  writeFileSync8(
5665
- join11(staging, "package.json"),
5764
+ join12(staging, "package.json"),
5666
5765
  `${JSON.stringify({ private: true, allowScripts: { "node-pty": true } }, null, 2)}
5667
5766
  `
5668
5767
  );
@@ -5697,21 +5796,21 @@ async function installVersion(paths, version, { spec, integrity, run: run4 = exe
5697
5796
  }
5698
5797
  const broken = await versionRuns(staging, version, run4);
5699
5798
  if (broken) return fail(broken);
5700
- renameSync3(staging, join11(realpathSync2(paths.versions), version));
5799
+ renameSync3(staging, join12(realpathSync3(paths.versions), version));
5701
5800
  return { ok: true, dir };
5702
5801
  }
5703
5802
  function sweepStaging(paths) {
5704
- if (!existsSync8(paths.versions)) return;
5803
+ if (!existsSync9(paths.versions)) return;
5705
5804
  for (const name of readdirSync4(paths.versions)) {
5706
5805
  const match = /^\.staging-.+-(\d+)$/.exec(name);
5707
5806
  if (match && !alive2(Number(match[1])))
5708
- rmSync9(join11(paths.versions, name), { recursive: true, force: true });
5807
+ rmSync9(join12(paths.versions, name), { recursive: true, force: true });
5709
5808
  }
5710
5809
  }
5711
5810
  function activate(paths, version) {
5712
5811
  const next = `${paths.current}.next-${process.pid}`;
5713
5812
  rmSync9(next, { force: true });
5714
- symlinkSync(join11("versions", version), next);
5813
+ symlinkSync(join12("versions", version), next);
5715
5814
  renameSync3(next, paths.current);
5716
5815
  }
5717
5816
  function pruneVersions(paths, { inUse = [], keep = 3 } = {}) {
@@ -5726,7 +5825,7 @@ function pruneVersions(paths, { inUse = [], keep = 3 } = {}) {
5726
5825
  const removed = [];
5727
5826
  for (const version of versions) {
5728
5827
  if (kept.has(version)) continue;
5729
- rmSync9(join11(paths.versions, version), { recursive: true, force: true });
5828
+ rmSync9(join12(paths.versions, version), { recursive: true, force: true });
5730
5829
  removed.push(version);
5731
5830
  }
5732
5831
  return removed;
@@ -5736,7 +5835,7 @@ async function hostVersions(paths, run4 = exec) {
5736
5835
  if (ran.status !== 0) return installedVersions(paths);
5737
5836
  const roots = /* @__PURE__ */ new Set([paths.versions]);
5738
5837
  try {
5739
- roots.add(realpathSync2(paths.versions));
5838
+ roots.add(realpathSync3(paths.versions));
5740
5839
  } catch {
5741
5840
  }
5742
5841
  const found = /* @__PURE__ */ new Set();
@@ -5774,13 +5873,13 @@ async function missingBuildTools(run4) {
5774
5873
  }
5775
5874
  var real = (path) => {
5776
5875
  try {
5777
- return realpathSync3(path);
5876
+ return realpathSync4(path);
5778
5877
  } catch {
5779
5878
  return resolve3(path);
5780
5879
  }
5781
5880
  };
5782
5881
  function chooseRoot(cwd, home) {
5783
- return real(cwd) === real(home) ? join12(home, "friday") : resolve3(cwd);
5882
+ return real(cwd) === real(home) ? join13(home, "friday") : resolve3(cwd);
5784
5883
  }
5785
5884
  async function runStart(options) {
5786
5885
  const { paths, run: run4 } = options;
@@ -5790,7 +5889,7 @@ async function runStart(options) {
5790
5889
  const wanted = chooseRoot(options.cwd, options.home);
5791
5890
  const running = runningSupervisor(paths);
5792
5891
  if (existing && real(existing) !== real(wanted)) {
5793
- print(`This machine's root is ${existing}, where Friday keeps its repos and conversations.`);
5892
+ print(`This machine's root is ${existing}, which holds the repos Friday works on.`);
5794
5893
  if (running) {
5795
5894
  print(`Friday is running there, version ${running.version}, so nothing changed.`);
5796
5895
  await waitForAgents(options, await chooseAndAsk(options, print), print);
@@ -5808,7 +5907,7 @@ async function runStart(options) {
5808
5907
  const repo = insideWorkTree(root);
5809
5908
  if (repo) {
5810
5909
  throw new StartError(
5811
- `${root} is inside the git repo ${repo}, so it can't be the machine's root: every conversation would load that repo's settings. Run ${START_COMMAND} in an empty folder outside any repo, such as ~/friday.`
5910
+ `${root} is inside the git repo ${repo}, so it can't be the machine's root: every conversation would load that repo's settings. Run ${START_COMMAND} in a folder outside any repo, such as the one that holds your checkouts, like ~/code.`
5812
5911
  );
5813
5912
  }
5814
5913
  if ((options.platform ?? process.platform) === "linux") {
@@ -5830,7 +5929,7 @@ async function runStart(options) {
5830
5929
  `Stopped the old daemon: removed ${retired.join(" and ")}. Its checkout is left as it was.`
5831
5930
  );
5832
5931
  await ensureInstalled(options, print);
5833
- mkdirSync9(root, { recursive: true });
5932
+ mkdirSync10(root, { recursive: true });
5834
5933
  writeRoot(paths, root);
5835
5934
  const command = [process.execPath, binOf(paths.current, "friday"), "supervise"];
5836
5935
  const env = { HERD_HOME: paths.herdHome, ...options.env };
@@ -5908,14 +6007,14 @@ async function ensureInstalled(options, print) {
5908
6007
  // src/friday/supervisor.ts
5909
6008
  init_define_HERD_PACKAGE();
5910
6009
  init_src();
5911
- import { spawn as spawn4 } from "node:child_process";
6010
+ import { spawn as spawn5 } from "node:child_process";
5912
6011
  import {
5913
6012
  closeSync as closeSync3,
5914
- existsSync as existsSync9,
5915
- mkdirSync as mkdirSync10,
6013
+ existsSync as existsSync10,
6014
+ mkdirSync as mkdirSync11,
5916
6015
  openSync as openSync4,
5917
6016
  readFileSync as readFileSync9,
5918
- realpathSync as realpathSync4,
6017
+ realpathSync as realpathSync5,
5919
6018
  renameSync as renameSync4,
5920
6019
  statSync as statSync6
5921
6020
  } from "node:fs";
@@ -5985,7 +6084,7 @@ var Supervisor = class {
5985
6084
  if (this.options.daemonCommand) return this.options.daemonCommand();
5986
6085
  let dir;
5987
6086
  try {
5988
- dir = realpathSync4(this.options.paths.current);
6087
+ dir = realpathSync5(this.options.paths.current);
5989
6088
  } catch {
5990
6089
  throw new Error(`Nothing is installed in ~/.herd/current. Run ${START_COMMAND}.`);
5991
6090
  }
@@ -5994,16 +6093,16 @@ var Supervisor = class {
5994
6093
  spawnDaemon() {
5995
6094
  if (this.stopping) return;
5996
6095
  const { daemonLog, logs } = this.options.paths;
5997
- mkdirSync10(logs, { recursive: true });
6096
+ mkdirSync11(logs, { recursive: true });
5998
6097
  try {
5999
- if (existsSync9(daemonLog) && statSync6(daemonLog).size > MAX_LOG_BYTES2)
6098
+ if (existsSync10(daemonLog) && statSync6(daemonLog).size > MAX_LOG_BYTES2)
6000
6099
  renameSync4(daemonLog, `${daemonLog}.1`);
6001
6100
  } catch {
6002
6101
  }
6003
6102
  const out = openSync4(daemonLog, "a");
6004
6103
  const [program = process.execPath, ...args] = this.daemonCommand();
6005
6104
  const startedAt = Date.now();
6006
- const child = spawn4(program, args, { stdio: ["ignore", out, out], env: process.env });
6105
+ const child = spawn5(program, args, { stdio: ["ignore", out, out], env: process.env });
6007
6106
  closeSync3(out);
6008
6107
  this.daemon = child;
6009
6108
  this.status.daemonPid = child.pid ?? null;
@@ -6027,7 +6126,7 @@ var Supervisor = class {
6027
6126
  async stopDaemon() {
6028
6127
  const child = this.daemon;
6029
6128
  if (!child || child.exitCode !== null || child.signalCode !== null) return;
6030
- const exited = new Promise((resolve6) => child.once("exit", () => resolve6()));
6129
+ const exited = new Promise((resolve7) => child.once("exit", () => resolve7()));
6031
6130
  child.kill("SIGTERM");
6032
6131
  const timer = setTimeout(() => child.kill("SIGKILL"), this.options.stopMs ?? 15e3);
6033
6132
  await exited;
@@ -6134,8 +6233,8 @@ var HELP = `friday: run HerdingChats' agent daemon on this computer.
6134
6233
 
6135
6234
  Usage: ${FRIDAY_COMMAND} <command>
6136
6235
 
6137
- start Make this computer a machine, with the folder this runs in as its root, where
6138
- Friday keeps its repos and conversations. Signs in through your browser when
6236
+ start Make this computer a machine, with the folder this runs in as its root, which
6237
+ holds your repos, such as ~/code. Signs in through your browser when
6139
6238
  needed, asks how much of each agent you have, starts the daemon now and whenever
6140
6239
  you log in, waits for each agent to check in, then shows what's left to set up.
6141
6240
  Safe to run again. --agents claude=lot,codex=little sets the shares without
@@ -6149,7 +6248,7 @@ Agents use the herd command, which comes with this one. herd setup shows what's
6149
6248
  function defaults() {
6150
6249
  const home = homedir4();
6151
6250
  return {
6152
- paths: fridayPaths(process.env.HERD_HOME ?? join13(home, ".herd")),
6251
+ paths: fridayPaths(process.env.HERD_HOME ?? join14(home, ".herd")),
6153
6252
  home,
6154
6253
  cwd: process.cwd(),
6155
6254
  run: exec,
@@ -6259,12 +6358,12 @@ function agentsFlag(args) {
6259
6358
  return parsed.shares;
6260
6359
  }
6261
6360
  function attachTo(paths, terminal) {
6262
- const herd = existsSync10(binOf(paths.current, "herd")) ? binOf(paths.current, "herd") : join13(dirname9(process.argv[1] ?? ""), "herd.mjs");
6263
- const child = spawn5(process.execPath, [herd, "attach", terminal], {
6361
+ const herd = existsSync11(binOf(paths.current, "herd")) ? binOf(paths.current, "herd") : join14(dirname10(process.argv[1] ?? ""), "herd.mjs");
6362
+ const child = spawn6(process.execPath, [herd, "attach", terminal], {
6264
6363
  stdio: "inherit",
6265
6364
  env: { ...process.env, HERD_HOME: paths.herdHome }
6266
6365
  });
6267
- return new Promise((resolve6) => child.on("close", () => resolve6()));
6366
+ return new Promise((resolve7) => child.on("close", () => resolve7()));
6268
6367
  }
6269
6368
  async function runStop(deps) {
6270
6369
  const status2 = readStatus(deps.paths);
@@ -6309,13 +6408,28 @@ function runStatus(deps) {
6309
6408
  );
6310
6409
  if (version) print(`Installed: ${version}${pin ? `, pinned to ${pin} in ${paths.pin}` : ""}.`);
6311
6410
  if (root) print(`Root: ${root}`);
6411
+ const missing = readMissingFolder(paths.herdHome);
6412
+ if (running && missing) {
6413
+ print(
6414
+ `Taking no work or builds: ${missing.target ? `${missing.path} links to ${missing.target}, which isn't there` : `${missing.path} isn't there`}. It takes work again once it is.`
6415
+ );
6416
+ }
6312
6417
  if (running && status2.checkedAt) print(`Last asked for a release ${ago(status2.checkedAt)}.`);
6313
6418
  if (status2?.problem) print(`Last problem: ${status2.problem}`);
6314
6419
  print(`Logs: ${paths.logs}`);
6315
6420
  return running ? 0 : 1;
6316
6421
  }
6422
+ function readMissingFolder(herdHome2) {
6423
+ try {
6424
+ return folderUnavailableSchema.parse(
6425
+ JSON.parse(readFileSync10(join14(herdHome2, "folder-unavailable.json"), "utf8"))
6426
+ );
6427
+ } catch {
6428
+ return null;
6429
+ }
6430
+ }
6317
6431
  function tail(path, count) {
6318
- if (!existsSync10(path)) return [];
6432
+ if (!existsSync11(path)) return [];
6319
6433
  const lines = readFileSync10(path, "utf8").split("\n");
6320
6434
  if (lines.at(-1) === "") lines.pop();
6321
6435
  return lines.slice(-count);
@@ -6323,14 +6437,14 @@ function tail(path, count) {
6323
6437
  async function runLogs(args, deps) {
6324
6438
  const { paths, print } = deps;
6325
6439
  const follow = args.includes("-f") || args.includes("--follow");
6326
- const files = [paths.supervisorLog, paths.daemonLog].filter((file) => existsSync10(file));
6440
+ const files = [paths.supervisorLog, paths.daemonLog].filter((file) => existsSync11(file));
6327
6441
  if (files.length === 0) {
6328
6442
  print(`There are no logs in ${paths.logs} yet.`);
6329
6443
  return 0;
6330
6444
  }
6331
6445
  if (follow) {
6332
- const child = spawn5("tail", ["-n", "50", "-F", ...files], { stdio: "inherit" });
6333
- return await new Promise((resolve6) => child.on("close", (code) => resolve6(code ?? 0)));
6446
+ const child = spawn6("tail", ["-n", "50", "-F", ...files], { stdio: "inherit" });
6447
+ return await new Promise((resolve7) => child.on("close", (code) => resolve7(code ?? 0)));
6334
6448
  }
6335
6449
  for (const file of files) {
6336
6450
  print(`==> ${file} <==`);
@@ -6371,9 +6485,9 @@ init_define_HERD_PACKAGE();
6371
6485
 
6372
6486
  // src/build-sandbox.ts
6373
6487
  init_define_HERD_PACKAGE();
6374
- import { spawn as spawn6 } from "node:child_process";
6488
+ import { spawn as spawn7 } from "node:child_process";
6375
6489
  import { appendFileSync, readFileSync as readFileSync11 } from "node:fs";
6376
- import { dirname as dirname10, join as join14 } from "node:path";
6490
+ import { dirname as dirname11, join as join15 } from "node:path";
6377
6491
  var DEPLOY_HOSTS = ["api.cloudflare.com", "api.github.com", "registry.npmjs.org"];
6378
6492
  function buildPolicy(input) {
6379
6493
  const platform2 = input.platform ?? process.platform;
@@ -6389,7 +6503,7 @@ function buildPolicy(input) {
6389
6503
  ...platform2 === "darwin" ? { allowUnixSockets: [input.home] } : { allowAllUnixSockets: true }
6390
6504
  },
6391
6505
  filesystem: {
6392
- denyRead: [.../* @__PURE__ */ new Set([input.ownerHome, input.root])],
6506
+ denyRead: [.../* @__PURE__ */ new Set([input.ownerHome, input.root, input.convos])],
6393
6507
  allowRead: [input.worktree, input.store, input.nodePrefix, input.home],
6394
6508
  allowWrite: [input.worktree, input.worktreeGitDir, input.home],
6395
6509
  denyWrite: []
@@ -6403,7 +6517,7 @@ function describePolicy(policy, platform2 = process.platform) {
6403
6517
  const hosts = policy.network.allowedDomains.join(", ");
6404
6518
  return `Sandboxed with ${kind}: the owner's home is unreadable, writes go only to the worktree and the build's home, and the only hosts it reaches are ${hosts}.`;
6405
6519
  }
6406
- var nodePrefix = (execPath = process.execPath) => dirname10(dirname10(execPath));
6520
+ var nodePrefix = (execPath = process.execPath) => dirname11(dirname11(execPath));
6407
6521
  var shellQuote = (args) => args.map((arg) => `'${arg.replace(/'/g, "'\\''")}'`).join(" ");
6408
6522
  var BLOCKED_LOG = "blocked.log";
6409
6523
  async function runSandboxed(args) {
@@ -6420,7 +6534,7 @@ async function runSandboxed(args) {
6420
6534
  return 3;
6421
6535
  }
6422
6536
  await SandboxManager.initialize(policy, void 0, true);
6423
- const blocked = join14(process.env.HOME ?? ".", BLOCKED_LOG);
6537
+ const blocked = join15(process.env.HOME ?? ".", BLOCKED_LOG);
6424
6538
  let written = 0;
6425
6539
  const unsubscribe = SandboxManager.getSandboxViolationStore().subscribe((violations) => {
6426
6540
  const fresh = violations.slice(written).map((violation) => describeRefusal(violation.line)).filter((line2) => line2 !== null);
@@ -6433,13 +6547,13 @@ async function runSandboxed(args) {
6433
6547
  }
6434
6548
  });
6435
6549
  const wrapped = await SandboxManager.wrapWithSandbox(shellQuote(command));
6436
- const child = spawn6(wrapped, { shell: true, stdio: "inherit" });
6550
+ const child = spawn7(wrapped, { shell: true, stdio: "inherit" });
6437
6551
  const forward = (signal2) => () => child.kill(signal2);
6438
6552
  process.once("SIGTERM", forward("SIGTERM"));
6439
6553
  process.once("SIGINT", forward("SIGINT"));
6440
- const status2 = await new Promise((resolve6) => {
6441
- child.on("error", () => resolve6(1));
6442
- child.on("exit", (code, signal2) => resolve6(code ?? (signal2 ? 1 : 0)));
6554
+ const status2 = await new Promise((resolve7) => {
6555
+ child.on("error", () => resolve7(1));
6556
+ child.on("exit", (code, signal2) => resolve7(code ?? (signal2 ? 1 : 0)));
6443
6557
  });
6444
6558
  unsubscribe();
6445
6559
  SandboxManager.cleanupAfterCommand();
@@ -6470,7 +6584,7 @@ async function fromTerminal(request) {
6470
6584
  reply = await askDaemon(socket, { secret, ...request }).catch(() => null);
6471
6585
  if (reply) break;
6472
6586
  if (Date.now() > deadline) throw new HerdError("Couldn't reach herd's daemon. Is it running?");
6473
- await new Promise((resolve6) => setTimeout(resolve6, 500));
6587
+ await new Promise((resolve7) => setTimeout(resolve7, 500));
6474
6588
  }
6475
6589
  if (!reply.ok) throw new HerdError(reply.error);
6476
6590
  return reply;
@@ -6897,8 +7011,8 @@ init_define_HERD_PACKAGE();
6897
7011
  // src/build-runner-key.ts
6898
7012
  init_define_HERD_PACKAGE();
6899
7013
  init_src();
6900
- import { chmodSync as chmodSync3, existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
6901
- import { join as join15 } from "node:path";
7014
+ import { chmodSync as chmodSync3, existsSync as existsSync12, mkdirSync as mkdirSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
7015
+ import { join as join16 } from "node:path";
6902
7016
  import { z as z24 } from "zod";
6903
7017
  var BUILDS_URL = "https://builds.herdingchats.com";
6904
7018
  var runnerKeySchema = z24.object({
@@ -6906,10 +7020,10 @@ var runnerKeySchema = z24.object({
6906
7020
  key: z24.string().min(20),
6907
7021
  url: z24.string().url()
6908
7022
  });
6909
- var runnerKeyPath = () => join15(herdHome(), "build-runner.json");
7023
+ var runnerKeyPath = () => join16(herdHome(), "build-runner.json");
6910
7024
  function readRunnerKey() {
6911
7025
  const path = runnerKeyPath();
6912
- if (!existsSync11(path)) return null;
7026
+ if (!existsSync12(path)) return null;
6913
7027
  try {
6914
7028
  return runnerKeySchema.parse(JSON.parse(readFileSync13(path, "utf8")));
6915
7029
  } catch {
@@ -6919,7 +7033,7 @@ function readRunnerKey() {
6919
7033
  }
6920
7034
  }
6921
7035
  function saveRunnerKey(key) {
6922
- mkdirSync11(herdHome(), { recursive: true, mode: 448 });
7036
+ mkdirSync12(herdHome(), { recursive: true, mode: 448 });
6923
7037
  const path = runnerKeyPath();
6924
7038
  writeFileSync9(path, JSON.stringify(runnerKeySchema.parse(key), null, 2), { mode: 384 });
6925
7039
  chmodSync3(path, 384);
@@ -6934,7 +7048,7 @@ async function registerRunner({
6934
7048
  url = buildsUrl(),
6935
7049
  fetchImpl = fetch,
6936
7050
  say = (line2) => console.log(line2),
6937
- sleep: sleep8 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms)),
7051
+ sleep: sleep8 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
6938
7052
  pollMs = 2e3,
6939
7053
  waitMs = 15 * 60 * 1e3
6940
7054
  }) {
@@ -6973,8 +7087,8 @@ async function registerRunner({
6973
7087
 
6974
7088
  // src/builds-admin.ts
6975
7089
  init_define_HERD_PACKAGE();
6976
- import { mkdirSync as mkdirSync12, readFileSync as readFileSync14, rmSync as rmSync10, writeFileSync as writeFileSync10 } from "node:fs";
6977
- import { join as join16 } from "node:path";
7090
+ import { mkdirSync as mkdirSync13, readFileSync as readFileSync14, rmSync as rmSync10, writeFileSync as writeFileSync10 } from "node:fs";
7091
+ import { join as join17 } from "node:path";
6978
7092
  var NOT_ADMIN = "builds are only for the admin's machines";
6979
7093
  async function isAdmin(fetch2) {
6980
7094
  const response = await fetch2("/api/people/me").catch(() => null);
@@ -6982,13 +7096,13 @@ async function isAdmin(fetch2) {
6982
7096
  const body = await response.json().catch(() => ({}));
6983
7097
  return body.admin === true;
6984
7098
  }
6985
- var refusalPath = () => join16(herdHome(), "builds", "refused.json");
7099
+ var refusalPath = () => join17(herdHome(), "builds", "refused.json");
6986
7100
  function writeBuildsRefusal(reason) {
6987
7101
  if (!reason) {
6988
7102
  rmSync10(refusalPath(), { force: true });
6989
7103
  return;
6990
7104
  }
6991
- mkdirSync12(join16(herdHome(), "builds"), { recursive: true });
7105
+ mkdirSync13(join17(herdHome(), "builds"), { recursive: true });
6992
7106
  writeFileSync10(refusalPath(), JSON.stringify({ reason }));
6993
7107
  }
6994
7108
  function readBuildsRefusal() {
@@ -7134,7 +7248,7 @@ async function runConversations(rest) {
7134
7248
  init_define_HERD_PACKAGE();
7135
7249
  init_src();
7136
7250
  import { availableParallelism, totalmem } from "node:os";
7137
- import { join as join31 } from "node:path";
7251
+ import { join as join33 } from "node:path";
7138
7252
 
7139
7253
  // src/agent.ts
7140
7254
  init_define_HERD_PACKAGE();
@@ -7243,17 +7357,34 @@ function readClaudeScreen(raw) {
7243
7357
 
7244
7358
  // src/worktrees.ts
7245
7359
  init_define_HERD_PACKAGE();
7246
- import { isAbsolute as isAbsolute2, join as join17, relative } from "node:path";
7247
- var worktreesRoot = () => join17(herdHome(), "worktrees");
7360
+ import { existsSync as existsSync13, realpathSync as realpathSync6 } from "node:fs";
7361
+ import { isAbsolute as isAbsolute2, join as join18, relative } from "node:path";
7362
+ var worktreesRoot = () => join18(herdHome(), "worktrees");
7248
7363
  var WORD_ID = /^[a-z]+-[a-z]+-[a-z]+$/;
7249
7364
  var conversationPrefix = (conversationId) => WORD_ID.test(conversationId) ? conversationId : conversationId.replace(/[^A-Za-z0-9]/g, "").slice(0, 12).toLowerCase();
7250
7365
  var conversationBranch = (conversationId) => `herd/${conversationPrefix(conversationId)}`;
7251
- var conversationFolder = (root, conversationId) => join17(convosDir(root), conversationPrefix(conversationId));
7366
+ var conversationFolder = (convos, conversationId) => join18(convos, conversationPrefix(conversationId));
7367
+ function folderForConversation(root, conversationId) {
7368
+ const earlier = conversationFolder(legacyConvosDir(root), conversationId);
7369
+ return existsSync13(earlier) ? earlier : conversationFolder(convosHome(root), conversationId);
7370
+ }
7252
7371
  var within = (dir, path) => {
7253
7372
  const between = relative(dir, path);
7254
7373
  return between !== "" && !between.startsWith("..") && !isAbsolute2(between);
7255
7374
  };
7256
- var isHerdFolder = (path, root = machineRoot()) => within(convosDir(root), path) || within(worktreesRoot(), path);
7375
+ var real2 = (path) => {
7376
+ try {
7377
+ return realpathSync6(path);
7378
+ } catch {
7379
+ return path;
7380
+ }
7381
+ };
7382
+ function isHerdFolder(path, root = machineRoot()) {
7383
+ const at = real2(path);
7384
+ return [convosPath(root), convosHome(root), legacyConvosDir(root), worktreesRoot()].some(
7385
+ (dir) => within(dir, path) || within(real2(dir), at)
7386
+ );
7387
+ }
7257
7388
 
7258
7389
  // src/agent.ts
7259
7390
  var FOLDER_PARAGRAPH = "This folder is this conversation's own, and holds only the repos added to it: `herd repo <url or owner/name>` checks a repo out here, on a branch named for this conversation, and `herd repos` lists the repos here and the ones to choose from, with what your owner says each is. Work in each repo's folder, and follow each repo's own AGENTS.md or README to set it up. ";
@@ -7274,7 +7405,7 @@ var ownPrompt = (draft) => OWN_PROMPTS.find(
7274
7405
  var SESSION_ID = /^[A-Za-z0-9-]{1,100}$/;
7275
7406
  var SUBMIT_RETRIES = 3;
7276
7407
  var DEFAULT_TIMING = { settleMs: 800, pollMs: 150, startMs: 6e4, confirmMs: 3e3 };
7277
- var sleep3 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
7408
+ var sleep3 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
7278
7409
  var EXIT_WAIT_MS = 5e3;
7279
7410
  var AgentDriver = class {
7280
7411
  constructor(terminals, timing = {}) {
@@ -7421,12 +7552,12 @@ var AgentDriver = class {
7421
7552
  if (before.draft && !leftover) return { status: "draft", draft: before.draft };
7422
7553
  const sent = leftover ?? text4;
7423
7554
  let taken = false;
7424
- const hooked = new Promise((resolve6) => {
7555
+ const hooked = new Promise((resolve7) => {
7425
7556
  this.waiters.set(name, {
7426
7557
  text: sent,
7427
7558
  taken: () => {
7428
7559
  taken = true;
7429
- resolve6();
7560
+ resolve7();
7430
7561
  }
7431
7562
  });
7432
7563
  });
@@ -7533,9 +7664,9 @@ function readAgyScreen(raw) {
7533
7664
 
7534
7665
  // src/agy-settings.ts
7535
7666
  init_define_HERD_PACKAGE();
7536
- import { lstatSync, mkdirSync as mkdirSync13, realpathSync as realpathSync5, writeFileSync as writeFileSync11 } from "node:fs";
7537
- import { join as join18, sep } from "node:path";
7538
- var AGY_SETTINGS_FILE = join18(".herd", "agy-settings.json");
7667
+ import { lstatSync as lstatSync2, mkdirSync as mkdirSync14, realpathSync as realpathSync7, writeFileSync as writeFileSync11 } from "node:fs";
7668
+ import { join as join19, sep } from "node:path";
7669
+ var AGY_SETTINGS_FILE = join19(".herd", "agy-settings.json");
7539
7670
  var AGY_HOOK_EVENTS = ["SessionStart", "PreInvocation", "Stop"];
7540
7671
  var agyHookCommand = (event2, herd) => `${herd} hook agy ${event2} || true`;
7541
7672
  function agyHooks(herd) {
@@ -7552,25 +7683,25 @@ function agySettings(herd) {
7552
7683
  return { statusLine: { type: "command", command: `${herd} statusline agy` } };
7553
7684
  }
7554
7685
  function ownFolder(worktree, name) {
7555
- const folder = join18(worktree, name);
7556
- if (lstatSync(folder, { throwIfNoEntry: false })?.isSymbolicLink()) {
7686
+ const folder = join19(worktree, name);
7687
+ if (lstatSync2(folder, { throwIfNoEntry: false })?.isSymbolicLink()) {
7557
7688
  throw new Error(`${name} in the worktree is a link`);
7558
7689
  }
7559
- mkdirSync13(folder, { recursive: true });
7560
- if (!realpathSync5(folder).startsWith(realpathSync5(worktree) + sep)) {
7690
+ mkdirSync14(folder, { recursive: true });
7691
+ if (!realpathSync7(folder).startsWith(realpathSync7(worktree) + sep)) {
7561
7692
  throw new Error(`${name} leads outside the worktree`);
7562
7693
  }
7563
7694
  return folder;
7564
7695
  }
7565
7696
  function writeAgyFiles(worktree, herd) {
7566
7697
  const agents = ownFolder(worktree, ".agents");
7567
- writeFileSync11(join18(agents, "hooks.json"), `${JSON.stringify(agyHooks(herd), null, 2)}
7698
+ writeFileSync11(join19(agents, "hooks.json"), `${JSON.stringify(agyHooks(herd), null, 2)}
7568
7699
  `);
7569
- writeFileSync11(join18(agents, ".gitignore"), "*\n");
7700
+ writeFileSync11(join19(agents, ".gitignore"), "*\n");
7570
7701
  const herdFolder = ownFolder(worktree, ".herd");
7571
- writeFileSync11(join18(herdFolder, ".gitignore"), "*\n");
7702
+ writeFileSync11(join19(herdFolder, ".gitignore"), "*\n");
7572
7703
  writeFileSync11(
7573
- join18(worktree, AGY_SETTINGS_FILE),
7704
+ join19(worktree, AGY_SETTINGS_FILE),
7574
7705
  `${JSON.stringify(agySettings(herd), null, 2)}
7575
7706
  `
7576
7707
  );
@@ -7579,7 +7710,7 @@ function writeAgyFiles(worktree, herd) {
7579
7710
  // src/agy-driver.ts
7580
7711
  var AGY_FLAGS = `--dangerously-skip-permissions --settings ${AGY_SETTINGS_FILE}`;
7581
7712
  var DEFAULT_TIMING2 = { settleMs: 800, pollMs: 150, startMs: 6e4, confirmMs: 3e3 };
7582
- var sleep4 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
7713
+ var sleep4 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
7583
7714
  var EXIT_WAIT_MS2 = 5e3;
7584
7715
  var AgyDriver = class {
7585
7716
  constructor(terminals, timing = {}) {
@@ -7669,12 +7800,12 @@ var AgyDriver = class {
7669
7800
  if (before.draft && !leftover) return { status: "draft", draft: before.draft };
7670
7801
  const sent = leftover ?? text4;
7671
7802
  let taken = false;
7672
- const reported = new Promise((resolve6) => {
7803
+ const reported = new Promise((resolve7) => {
7673
7804
  this.waiters.set(name, {
7674
7805
  text: sent,
7675
7806
  taken: () => {
7676
7807
  taken = true;
7677
- resolve6();
7808
+ resolve7();
7678
7809
  }
7679
7810
  });
7680
7811
  });
@@ -7722,6 +7853,78 @@ var AgyDriver = class {
7722
7853
  }
7723
7854
  };
7724
7855
 
7856
+ // src/bare-clones.ts
7857
+ init_define_HERD_PACKAGE();
7858
+ import { randomBytes as randomBytes5 } from "node:crypto";
7859
+ import { existsSync as existsSync14, mkdirSync as mkdirSync15, readdirSync as readdirSync5, renameSync as renameSync5, rmSync as rmSync11, statSync as statSync7 } from "node:fs";
7860
+ import { dirname as dirname12, join as join20 } from "node:path";
7861
+ function bareClonePath(dir, remote) {
7862
+ const key = repoKey(remote);
7863
+ if (!key) throw new StoreError(`${remote} isn't a repo's URL.`);
7864
+ return join20(dir, `${key}.git`);
7865
+ }
7866
+ async function ensureBareClone(store, remote, env) {
7867
+ if (existsSync14(store)) {
7868
+ let fetchFailed = null;
7869
+ try {
7870
+ await git(env, store, "fetch", "--quiet", "--prune", "origin");
7871
+ await git(env, store, "remote", "set-head", "origin", "--auto").catch(() => "");
7872
+ } catch (error) {
7873
+ fetchFailed = error instanceof Error ? error.message : String(error);
7874
+ }
7875
+ return { store, created: false, fetchFailed };
7876
+ }
7877
+ const building = `${store}.tmp-${process.pid}-${randomBytes5(4).toString("hex")}`;
7878
+ mkdirSync15(dirname12(store), { recursive: true });
7879
+ try {
7880
+ await git(env, dirname12(store), "init", "--quiet", "--bare", building);
7881
+ await git(env, building, "remote", "add", "origin", remote);
7882
+ await git(
7883
+ env,
7884
+ building,
7885
+ "config",
7886
+ "remote.origin.fetch",
7887
+ "+refs/heads/*:refs/remotes/origin/*"
7888
+ );
7889
+ await git(env, building, "fetch", "--quiet", "origin");
7890
+ await git(env, building, "remote", "set-head", "origin", "--auto").catch(() => "");
7891
+ renameSync5(building, store);
7892
+ } catch (error) {
7893
+ rmSync11(building, { recursive: true, force: true });
7894
+ throw error instanceof StoreError ? error : new StoreError(gitError(error));
7895
+ }
7896
+ return { store, created: true, fetchFailed: null };
7897
+ }
7898
+ function bareClones(dir) {
7899
+ const found = [];
7900
+ const walk = (at, depth) => {
7901
+ let entries;
7902
+ try {
7903
+ entries = readdirSync5(at);
7904
+ } catch {
7905
+ return;
7906
+ }
7907
+ for (const name of entries.sort()) {
7908
+ const path = join20(at, name);
7909
+ if (name.endsWith(".git")) {
7910
+ try {
7911
+ const remote = gitSync(path, "config", "--get", "remote.origin.url");
7912
+ const key = repoKey(remote);
7913
+ if (key) found.push({ key, remote, store: path });
7914
+ } catch {
7915
+ }
7916
+ } else if (depth < 8 && !name.includes(".tmp-") && !name.endsWith(".lock")) {
7917
+ try {
7918
+ if (statSync7(path).isDirectory()) walk(path, depth + 1);
7919
+ } catch {
7920
+ }
7921
+ }
7922
+ }
7923
+ };
7924
+ walk(dir, 0);
7925
+ return found;
7926
+ }
7927
+
7725
7928
  // src/build-connection.ts
7726
7929
  init_define_HERD_PACKAGE();
7727
7930
  init_src();
@@ -7880,28 +8083,28 @@ var BuildConnection = class {
7880
8083
 
7881
8084
  // src/builds.ts
7882
8085
  init_define_HERD_PACKAGE();
7883
- import { execFile as execFile2, execFileSync as execFileSync4, spawn as spawn7 } from "node:child_process";
8086
+ import { execFile as execFile2, execFileSync as execFileSync4, spawn as spawn8 } from "node:child_process";
7884
8087
  import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
7885
8088
  import {
7886
- existsSync as existsSync12,
7887
- mkdirSync as mkdirSync15,
8089
+ existsSync as existsSync15,
8090
+ mkdirSync as mkdirSync17,
7888
8091
  mkdtempSync,
7889
- readdirSync as readdirSync6,
8092
+ readdirSync as readdirSync7,
7890
8093
  readFileSync as readFileSync16,
7891
- realpathSync as realpathSync6,
7892
- rmSync as rmSync12,
8094
+ realpathSync as realpathSync8,
8095
+ rmSync as rmSync13,
7893
8096
  writeFileSync as writeFileSync13
7894
8097
  } from "node:fs";
7895
8098
  import { homedir as homedir6, tmpdir } from "node:os";
7896
- import { join as join20 } from "node:path";
8099
+ import { join as join22 } from "node:path";
7897
8100
  import { promisify as promisify2 } from "node:util";
7898
8101
 
7899
8102
  // src/check-registry.ts
7900
8103
  init_define_HERD_PACKAGE();
7901
- import { mkdirSync as mkdirSync14, readdirSync as readdirSync5, readFileSync as readFileSync15, rmSync as rmSync11, writeFileSync as writeFileSync12 } from "node:fs";
8104
+ import { mkdirSync as mkdirSync16, readdirSync as readdirSync6, readFileSync as readFileSync15, rmSync as rmSync12, writeFileSync as writeFileSync12 } from "node:fs";
7902
8105
  import { homedir as homedir5 } from "node:os";
7903
- import { join as join19 } from "node:path";
7904
- var checkRegistryDir = () => process.env.HERDINGCHATS_CHECK_RUNNING ?? join19(homedir5(), ".cache", "herdingchats", "check", "running");
8106
+ import { join as join21 } from "node:path";
8107
+ var checkRegistryDir = () => process.env.HERDINGCHATS_CHECK_RUNNING ?? join21(homedir5(), ".cache", "herdingchats", "check", "running");
7905
8108
  var RESERVED_MB = 6 * 1024;
7906
8109
  var MIN_MEMORY_MB = 1024;
7907
8110
  var alive3 = (pid) => {
@@ -7913,15 +8116,15 @@ var alive3 = (pid) => {
7913
8116
  }
7914
8117
  };
7915
8118
  function registerCheck(dir, pid, startedAt = Date.now()) {
7916
- mkdirSync14(dir, { recursive: true });
7917
- const file = join19(dir, `${pid}.json`);
8119
+ mkdirSync16(dir, { recursive: true });
8120
+ const file = join21(dir, `${pid}.json`);
7918
8121
  writeFileSync12(file, JSON.stringify({ pid, startedAt }));
7919
- return () => rmSync11(file, { force: true });
8122
+ return () => rmSync12(file, { force: true });
7920
8123
  }
7921
8124
  function runningChecks(dir, isAlive = alive3) {
7922
8125
  let names = [];
7923
8126
  try {
7924
- names = readdirSync5(dir).filter((name) => /^\d+\.json$/.test(name));
8127
+ names = readdirSync6(dir).filter((name) => /^\d+\.json$/.test(name));
7925
8128
  } catch {
7926
8129
  return 0;
7927
8130
  }
@@ -7930,12 +8133,12 @@ function runningChecks(dir, isAlive = alive3) {
7930
8133
  const pid = Number(name.slice(0, -".json".length));
7931
8134
  let entry = null;
7932
8135
  try {
7933
- entry = JSON.parse(readFileSync15(join19(dir, name), "utf8"));
8136
+ entry = JSON.parse(readFileSync15(join21(dir, name), "utf8"));
7934
8137
  } catch {
7935
8138
  continue;
7936
8139
  }
7937
8140
  if (entry?.pid === pid && isAlive(pid)) count += 1;
7938
- else rmSync11(join19(dir, name), { force: true });
8141
+ else rmSync12(join21(dir, name), { force: true });
7939
8142
  }
7940
8143
  return count;
7941
8144
  }
@@ -7943,9 +8146,9 @@ var memoryShareOf = (totalMb, running) => Math.max(MIN_MEMORY_MB, Math.floor((to
7943
8146
 
7944
8147
  // src/builds.ts
7945
8148
  init_self();
7946
- var sandboxStatusPath = () => join20(herdHome(), "builds", "sandbox.json");
8149
+ var sandboxStatusPath = () => join22(herdHome(), "builds", "sandbox.json");
7947
8150
  function writeSandboxStatus(status2, now = Date.now()) {
7948
- mkdirSync15(join20(herdHome(), "builds"), { recursive: true });
8151
+ mkdirSync17(join22(herdHome(), "builds"), { recursive: true });
7949
8152
  writeFileSync13(sandboxStatusPath(), JSON.stringify({ ...status2, at: now }));
7950
8153
  }
7951
8154
  function readSandboxStatus() {
@@ -7969,17 +8172,17 @@ async function storeGit(env, store, ...args) {
7969
8172
  throw new StoreError(gitError(error));
7970
8173
  }
7971
8174
  }
7972
- var real2 = (path) => {
8175
+ var real3 = (path) => {
7973
8176
  try {
7974
- return realpathSync6(path);
8177
+ return realpathSync8(path);
7975
8178
  } catch {
7976
8179
  return path;
7977
8180
  }
7978
8181
  };
7979
8182
  function worktreeOf(dir, store) {
7980
- if (!existsSync12(join20(dir, ".git"))) return false;
8183
+ if (!existsSync15(join22(dir, ".git"))) return false;
7981
8184
  try {
7982
- return real2(git2(dir, "rev-parse", "--path-format=absolute", "--git-common-dir")) === real2(store);
8185
+ return real3(git2(dir, "rev-parse", "--path-format=absolute", "--git-common-dir")) === real3(store);
7983
8186
  } catch {
7984
8187
  return false;
7985
8188
  }
@@ -7989,23 +8192,23 @@ function cloneFolder(from, to) {
7989
8192
  execFileSync4("cp", [...args, from, to]);
7990
8193
  }
7991
8194
  function moduleFolders(dir) {
7992
- const found = existsSync12(join20(dir, "node_modules")) ? ["node_modules"] : [];
7993
- for (const entry of readdirSync6(dir, { withFileTypes: true })) {
8195
+ const found = existsSync15(join22(dir, "node_modules")) ? ["node_modules"] : [];
8196
+ for (const entry of readdirSync7(dir, { withFileTypes: true })) {
7994
8197
  if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
7995
- if (existsSync12(join20(dir, entry.name, "node_modules")))
7996
- found.push(join20(entry.name, "node_modules"));
8198
+ if (existsSync15(join22(dir, entry.name, "node_modules")))
8199
+ found.push(join22(entry.name, "node_modules"));
7997
8200
  }
7998
8201
  }
7999
8202
  return found;
8000
8203
  }
8001
8204
  function lockHash(dir) {
8002
- const lock = readFileSync16(join20(dir, "package-lock.json"));
8205
+ const lock = readFileSync16(join22(dir, "package-lock.json"));
8003
8206
  const major = process.versions.node.split(".")[0];
8004
8207
  return createHash3("sha256").update(lock).update(`${process.platform}-${process.arch}-${major}`).digest("hex").slice(0, 32);
8005
8208
  }
8006
- var npmCi = (dir) => new Promise((resolve6, reject) => {
8209
+ var npmCi = (dir) => new Promise((resolve7, reject) => {
8007
8210
  const network = insideBuildSandbox() ? ["--offline"] : ["--prefer-offline"];
8008
- const child = spawn7(
8211
+ const child = spawn8(
8009
8212
  "npm",
8010
8213
  ["ci", ...network, "--no-audit", "--no-fund", "--no-update-notifier"],
8011
8214
  {
@@ -8016,7 +8219,7 @@ var npmCi = (dir) => new Promise((resolve6, reject) => {
8016
8219
  child.on("error", reject);
8017
8220
  child.on(
8018
8221
  "close",
8019
- (code) => code === 0 ? resolve6() : reject(new Error(`npm ci exited with ${code}`))
8222
+ (code) => code === 0 ? resolve7() : reject(new Error(`npm ci exited with ${code}`))
8020
8223
  );
8021
8224
  });
8022
8225
  var BuildRunner = class _BuildRunner {
@@ -8026,11 +8229,15 @@ var BuildRunner = class _BuildRunner {
8026
8229
  options;
8027
8230
  running = /* @__PURE__ */ new Map();
8028
8231
  home() {
8029
- return this.options.home ?? join20(herdHome(), "builds");
8232
+ return this.options.home ?? join22(herdHome(), "builds");
8233
+ }
8234
+ /** Where builds' own bare clones are, apart from the root's checkouts. */
8235
+ clonesDir() {
8236
+ return join22(this.home(), "clones");
8030
8237
  }
8031
- /** Herd's clone of a build's repo in the machine's root, whether or not it's there yet. */
8238
+ /** Herd's bare clone of a build's repo, for builds, whether or not it's there yet. */
8032
8239
  storeFor(build) {
8033
- return storePath(this.options.root(), this.source(build));
8240
+ return bareClonePath(this.clonesDir(), this.source(build));
8034
8241
  }
8035
8242
  /** What to clone a build's repo from: a remote the machine knows for it, or its HTTPS URL. */
8036
8243
  source(build) {
@@ -8038,21 +8245,21 @@ var BuildRunner = class _BuildRunner {
8038
8245
  }
8039
8246
  /**
8040
8247
  * The worktree for one of the repo's build slots, made or updated to the build's commit. The repo
8041
- * is cloned into the root first if it isn't there, and fetched otherwise, outside the sandbox and
8042
- * holding the clone's lock, which conversations adding worktrees of it share.
8248
+ * is cloned bare into the builds' own folder first if it isn't there, and fetched otherwise,
8249
+ * outside the sandbox and holding the clone's lock. Builds never work from the root's checkouts,
8250
+ * which hold the person's own local files.
8043
8251
  */
8044
8252
  async prepare(build, slot) {
8045
- const root = this.options.root();
8046
8253
  const remote = this.source(build);
8047
- const store = storePath(root, remote);
8254
+ const store = bareClonePath(this.clonesDir(), remote);
8048
8255
  const env = this.options.gitEnv?.();
8049
8256
  const name = (repoKey(build.repo) ?? build.repo).replace(/[^A-Za-z0-9._-]+/g, "_");
8050
- const dir = join20(this.home(), name, String(slot));
8051
- mkdirSync15(join20(this.home(), name), { recursive: true });
8257
+ const dir = join22(this.home(), name, String(slot));
8258
+ mkdirSync17(join22(this.home(), name), { recursive: true });
8052
8259
  await withStoreLock(
8053
8260
  store,
8054
8261
  async () => {
8055
- const clone = await ensureClone(root, remote, env);
8262
+ const clone = await ensureBareClone(store, remote, env);
8056
8263
  if (clone.created) this.options.log?.(`Cloned ${build.repo} into ${store}.`);
8057
8264
  if (clone.fetchFailed)
8058
8265
  this.options.log?.(`Couldn't fetch ${build.repo}: ${clone.fetchFailed}`);
@@ -8062,7 +8269,7 @@ var BuildRunner = class _BuildRunner {
8062
8269
  await storeGit(env, store, "fetch", "--quiet", "origin", build.sha);
8063
8270
  }
8064
8271
  if (!worktreeOf(dir, store)) {
8065
- rmSync12(dir, { recursive: true, force: true });
8272
+ rmSync13(dir, { recursive: true, force: true });
8066
8273
  await storeGit(env, store, "worktree", "prune");
8067
8274
  await storeGit(
8068
8275
  env,
@@ -8087,30 +8294,30 @@ var BuildRunner = class _BuildRunner {
8087
8294
  /** Makes `node_modules` match the checkout's lockfile: kept, copied from the cache, or installed. */
8088
8295
  async ensureModules(dir) {
8089
8296
  const wanted = lockHash(dir);
8090
- const marker = join20(dir, "node_modules", ".herd-lock");
8091
- if (existsSync12(marker) && readFileSync16(marker, "utf8") === wanted) return;
8092
- const cache = join20(this.home(), "node_modules", wanted);
8297
+ const marker = join22(dir, "node_modules", ".herd-lock");
8298
+ if (existsSync15(marker) && readFileSync16(marker, "utf8") === wanted) return;
8299
+ const cache = join22(this.home(), "node_modules", wanted);
8093
8300
  for (const folder of moduleFolders(dir))
8094
- rmSync12(join20(dir, folder), { recursive: true, force: true });
8095
- if (existsSync12(join20(cache, ".complete"))) {
8096
- for (const folder of readFileSync16(join20(cache, ".complete"), "utf8").split("\n").filter(Boolean)) {
8097
- cloneFolder(join20(cache, folder), join20(dir, folder));
8301
+ rmSync13(join22(dir, folder), { recursive: true, force: true });
8302
+ if (existsSync15(join22(cache, ".complete"))) {
8303
+ for (const folder of readFileSync16(join22(cache, ".complete"), "utf8").split("\n").filter(Boolean)) {
8304
+ cloneFolder(join22(cache, folder), join22(dir, folder));
8098
8305
  }
8099
8306
  } else {
8100
8307
  this.options.log?.(`Installing node_modules for lockfile ${wanted.slice(0, 8)}.`);
8101
8308
  await (this.options.install ?? npmCi)(dir);
8102
- const staging = mkdtempSync(join20(this.home(), "node_modules-"));
8309
+ const staging = mkdtempSync(join22(this.home(), "node_modules-"));
8103
8310
  const folders = moduleFolders(dir);
8104
8311
  for (const folder of folders) {
8105
- mkdirSync15(join20(staging, folder, ".."), { recursive: true });
8106
- cloneFolder(join20(dir, folder), join20(staging, folder));
8312
+ mkdirSync17(join22(staging, folder, ".."), { recursive: true });
8313
+ cloneFolder(join22(dir, folder), join22(staging, folder));
8107
8314
  }
8108
- writeFileSync13(join20(staging, ".complete"), folders.join("\n"));
8109
- mkdirSync15(join20(this.home(), "node_modules"), { recursive: true });
8110
- rmSync12(cache, { recursive: true, force: true });
8315
+ writeFileSync13(join22(staging, ".complete"), folders.join("\n"));
8316
+ mkdirSync17(join22(this.home(), "node_modules"), { recursive: true });
8317
+ rmSync13(cache, { recursive: true, force: true });
8111
8318
  execFileSync4("mv", [staging, cache]);
8112
8319
  }
8113
- mkdirSync15(join20(dir, "node_modules"), { recursive: true });
8320
+ mkdirSync17(join22(dir, "node_modules"), { recursive: true });
8114
8321
  writeFileSync13(marker, wanted);
8115
8322
  }
8116
8323
  /**
@@ -8121,7 +8328,7 @@ var BuildRunner = class _BuildRunner {
8121
8328
  const env = {
8122
8329
  PATH: process.env.PATH ?? "/usr/bin:/bin",
8123
8330
  HOME: home,
8124
- TMPDIR: join20(home, "tmp"),
8331
+ TMPDIR: join22(home, "tmp"),
8125
8332
  LANG: process.env.LANG ?? "en_US.UTF-8",
8126
8333
  HERD_BUILD_ID: build.id,
8127
8334
  HERD_BUILD_SECRET: build.secret,
@@ -8133,7 +8340,7 @@ var BuildRunner = class _BuildRunner {
8133
8340
  // The sandbox's proxy is the only way out, and Node's own requests use it only when asked.
8134
8341
  NODE_USE_ENV_PROXY: "1",
8135
8342
  // Where `srt` points TMPDIR inside the sandbox: the build's own temporary folder.
8136
- CLAUDE_CODE_TMPDIR: join20(home, "tmp")
8343
+ CLAUDE_CODE_TMPDIR: join22(home, "tmp")
8137
8344
  };
8138
8345
  return env;
8139
8346
  }
@@ -8144,8 +8351,8 @@ var BuildRunner = class _BuildRunner {
8144
8351
  /** Runs `scripts/build.mjs` from the build's own commit, in a process group of its own. */
8145
8352
  async run(build, slot) {
8146
8353
  const dir = await this.prepare(build, slot);
8147
- const home = mkdtempSync(join20(this.options.scratch ?? scratchRoot(), "hb-"));
8148
- mkdirSync15(join20(home, "tmp"), { recursive: true });
8354
+ const home = mkdtempSync(join22(this.options.scratch ?? scratchRoot(), "hb-"));
8355
+ mkdirSync17(join22(home, "tmp"), { recursive: true });
8149
8356
  const run4 = randomUUID4();
8150
8357
  const env = {
8151
8358
  ..._BuildRunner.environment(build, home),
@@ -8155,12 +8362,13 @@ var BuildRunner = class _BuildRunner {
8155
8362
  env.HERD_CHECK_CPUS = String(Math.max(1, Math.floor(this.options.cpus())));
8156
8363
  if (this.options.memoryMb)
8157
8364
  env.HERD_CHECK_MEMORY_MB = String(Math.max(1, Math.floor(this.options.memoryMb())));
8158
- let command = [process.execPath, join20(dir, "scripts", "build.mjs")];
8365
+ let command = [process.execPath, join22(dir, "scripts", "build.mjs")];
8159
8366
  let policyPath = null;
8160
8367
  if (this.sandboxed()) {
8161
8368
  const policy = buildPolicy({
8162
8369
  ownerHome: this.options.ownerHome ?? homedir6(),
8163
8370
  root: this.options.root(),
8371
+ convos: convosHome(this.options.root()),
8164
8372
  worktree: dir,
8165
8373
  store: this.storeFor(build),
8166
8374
  worktreeGitDir: git2(dir, "rev-parse", "--path-format=absolute", "--git-dir"),
@@ -8169,8 +8377,8 @@ var BuildRunner = class _BuildRunner {
8169
8377
  main: build.main,
8170
8378
  buildsUrl: build.url
8171
8379
  });
8172
- mkdirSync15(join20(this.home(), "policies"), { recursive: true });
8173
- policyPath = join20(this.home(), "policies", `${build.id}.json`);
8380
+ mkdirSync17(join22(this.home(), "policies"), { recursive: true });
8381
+ policyPath = join22(this.home(), "policies", `${build.id}.json`);
8174
8382
  writeFileSync13(policyPath, JSON.stringify(policy), { mode: 384 });
8175
8383
  env.HERD_BUILD_SANDBOX = describePolicy(policy);
8176
8384
  command = [process.execPath, HERD_BIN, "build-sandbox", policyPath, "--", ...command];
@@ -8178,15 +8386,15 @@ var BuildRunner = class _BuildRunner {
8178
8386
  env.HERD_BUILD_SANDBOX = insideBuildSandbox() ? "Inside the sandbox of the build that started this one." : "";
8179
8387
  }
8180
8388
  const [executable, ...args] = command;
8181
- const child = spawn7(executable, args, { cwd: dir, env, stdio: "ignore", detached: true });
8182
- const done = new Promise((resolve6) => {
8183
- child.on("error", () => resolve6(1));
8184
- child.on("close", (code) => resolve6(code ?? 1));
8389
+ const child = spawn8(executable, args, { cwd: dir, env, stdio: "ignore", detached: true });
8390
+ const done = new Promise((resolve7) => {
8391
+ child.on("error", () => resolve7(1));
8392
+ child.on("close", (code) => resolve7(code ?? 1));
8185
8393
  });
8186
8394
  let markEnded = () => {
8187
8395
  };
8188
- const ended = new Promise((resolve6) => {
8189
- markEnded = resolve6;
8396
+ const ended = new Promise((resolve7) => {
8397
+ markEnded = resolve7;
8190
8398
  });
8191
8399
  const entry = { pid: child.pid ?? 0, done, dir, ended, run: run4 };
8192
8400
  this.running.set(build.id, entry);
@@ -8198,8 +8406,8 @@ var BuildRunner = class _BuildRunner {
8198
8406
  unregister();
8199
8407
  await this.stopBuildProcesses(run4, dir, entry.pid);
8200
8408
  if (this.running.get(build.id) === entry) this.running.delete(build.id);
8201
- rmSync12(home, { recursive: true, force: true });
8202
- if (policyPath) rmSync12(policyPath, { force: true });
8409
+ rmSync13(home, { recursive: true, force: true });
8410
+ if (policyPath) rmSync13(policyPath, { force: true });
8203
8411
  markEnded();
8204
8412
  }
8205
8413
  }
@@ -8219,7 +8427,7 @@ var BuildRunner = class _BuildRunner {
8219
8427
  * It goes only by where they run from, never by a build ID, since the daemon a build's own tests
8220
8428
  * start must never stop the build it runs in.
8221
8429
  */
8222
- static async sweep(home = join20(herdHome(), "builds"), list = listProcesses) {
8430
+ static async sweep(home = join22(herdHome(), "builds"), list = listProcesses) {
8223
8431
  const left = buildProcesses(list(), null, home);
8224
8432
  await stopPids(left);
8225
8433
  return left.length;
@@ -8231,50 +8439,51 @@ var BuildRunner = class _BuildRunner {
8231
8439
  */
8232
8440
  static async probe(options = { buildsUrl: "" }) {
8233
8441
  if (insideBuildSandbox()) return { ok: true };
8234
- const herd = options.home ?? join20(herdHome(), "builds");
8235
- const dir = mkdtempSync(join20(scratchRoot(), "hp-"));
8236
- const secret = join20(dir, "owner", "secret");
8442
+ const herd = options.home ?? join22(herdHome(), "builds");
8443
+ const dir = mkdtempSync(join22(scratchRoot(), "hp-"));
8444
+ const secret = join22(dir, "owner", "secret");
8237
8445
  try {
8238
- mkdirSync15(herd, { recursive: true });
8239
- for (const sub of ["work", "git", "home", "owner"]) mkdirSync15(join20(dir, sub));
8446
+ mkdirSync17(herd, { recursive: true });
8447
+ for (const sub of ["work", "git", "home", "owner"]) mkdirSync17(join22(dir, sub));
8240
8448
  writeFileSync13(secret, "probe");
8241
8449
  const policy = buildPolicy({
8242
- ownerHome: join20(dir, "owner"),
8243
- root: join20(dir, "owner"),
8244
- worktree: join20(dir, "work"),
8245
- store: join20(dir, "git"),
8246
- worktreeGitDir: join20(dir, "git"),
8247
- home: join20(dir, "home"),
8450
+ ownerHome: join22(dir, "owner"),
8451
+ root: join22(dir, "owner"),
8452
+ convos: join22(dir, "owner"),
8453
+ worktree: join22(dir, "work"),
8454
+ store: join22(dir, "git"),
8455
+ worktreeGitDir: join22(dir, "git"),
8456
+ home: join22(dir, "home"),
8248
8457
  nodePrefix: nodePrefix(),
8249
8458
  main: false,
8250
8459
  buildsUrl: options.buildsUrl || "https://builds.herdingchats.com"
8251
8460
  });
8252
- const policyPath = join20(herd, "probe-policy.json");
8461
+ const policyPath = join22(herd, "probe-policy.json");
8253
8462
  writeFileSync13(policyPath, JSON.stringify(policy), { mode: 384 });
8254
8463
  const script = [
8255
8464
  'const fs = require("node:fs");',
8256
8465
  `try { fs.readFileSync(${JSON.stringify(secret)}); process.exit(10); } catch {}`,
8257
- `fs.writeFileSync(${JSON.stringify(join20(dir, "home", "ok"))}, "ok");`
8466
+ `fs.writeFileSync(${JSON.stringify(join22(dir, "home", "ok"))}, "ok");`
8258
8467
  ].join(" ");
8259
- const child = spawn7(
8468
+ const child = spawn8(
8260
8469
  process.execPath,
8261
8470
  [HERD_BIN, "build-sandbox", policyPath, "--", process.execPath, "-e", script],
8262
8471
  {
8263
- env: { PATH: process.env.PATH ?? "/usr/bin:/bin", HOME: join20(dir, "home") },
8472
+ env: { PATH: process.env.PATH ?? "/usr/bin:/bin", HOME: join22(dir, "home") },
8264
8473
  stdio: ["ignore", "ignore", "pipe"]
8265
8474
  }
8266
8475
  );
8267
8476
  let stderr = "";
8268
8477
  child.stderr.on("data", (data) => stderr += data);
8269
8478
  const timer = setTimeout(() => child.kill("SIGKILL"), options.timeoutMs ?? 3e4);
8270
- const status2 = await new Promise((resolve6) => {
8271
- child.on("error", () => resolve6(1));
8272
- child.on("close", (code) => resolve6(code ?? 1));
8479
+ const status2 = await new Promise((resolve7) => {
8480
+ child.on("error", () => resolve7(1));
8481
+ child.on("close", (code) => resolve7(code ?? 1));
8273
8482
  });
8274
8483
  clearTimeout(timer);
8275
8484
  if (status2 === 10)
8276
8485
  return { ok: false, reason: "the sandbox let a build read the owner's files" };
8277
- if (status2 !== 0 || !existsSync12(join20(dir, "home", "ok"))) {
8486
+ if (status2 !== 0 || !existsSync15(join22(dir, "home", "ok"))) {
8278
8487
  const why = stderr.trim().split("\n").at(-1) || `the probe exited with ${status2}`;
8279
8488
  return { ok: false, reason: why.slice(0, 300) };
8280
8489
  }
@@ -8282,7 +8491,7 @@ var BuildRunner = class _BuildRunner {
8282
8491
  } catch (error) {
8283
8492
  return { ok: false, reason: String(error).slice(0, 300) };
8284
8493
  } finally {
8285
- rmSync12(dir, { recursive: true, force: true });
8494
+ rmSync13(dir, { recursive: true, force: true });
8286
8495
  }
8287
8496
  }
8288
8497
  /** Stops a build's process group and every process it started, however it got away. */
@@ -8291,7 +8500,7 @@ var BuildRunner = class _BuildRunner {
8291
8500
  if (done) {
8292
8501
  const ended = await Promise.race([
8293
8502
  done.then(() => true),
8294
- new Promise((resolve6) => setTimeout(() => resolve6(false), 3e3).unref())
8503
+ new Promise((resolve7) => setTimeout(() => resolve7(false), 3e3).unref())
8295
8504
  ]);
8296
8505
  if (!ended && pid) signal(-pid, "SIGKILL");
8297
8506
  } else if (pid) {
@@ -8311,7 +8520,7 @@ function listProcesses() {
8311
8520
  function listFromProc() {
8312
8521
  let pids = [];
8313
8522
  try {
8314
- pids = readdirSync6("/proc").filter((name) => /^\d+$/.test(name));
8523
+ pids = readdirSync7("/proc").filter((name) => /^\d+$/.test(name));
8315
8524
  } catch {
8316
8525
  return [];
8317
8526
  }
@@ -8351,7 +8560,7 @@ async function stopPids(pids) {
8351
8560
  if (live.length === 0) return;
8352
8561
  const deadline = Date.now() + 3e3;
8353
8562
  while (Date.now() < deadline && live.some((pid) => signal(pid, 0))) {
8354
- await new Promise((resolve6) => setTimeout(resolve6, 100));
8563
+ await new Promise((resolve7) => setTimeout(resolve7, 100));
8355
8564
  }
8356
8565
  for (const pid of live) signal(pid, "SIGKILL");
8357
8566
  }
@@ -8360,32 +8569,32 @@ async function stopPids(pids) {
8360
8569
  init_define_HERD_PACKAGE();
8361
8570
  import { execFile as execFile3 } from "node:child_process";
8362
8571
  function listClaudeAgents(timeoutMs = 1e4, command = "claude") {
8363
- return new Promise((resolve6) => {
8572
+ return new Promise((resolve7) => {
8364
8573
  execFile3(
8365
8574
  command,
8366
8575
  ["agents", "--json", "--all"],
8367
8576
  { env: terminalEnv({}), timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024 },
8368
8577
  (error, stdout) => {
8369
- if (error) return resolve6([]);
8578
+ if (error) return resolve7([]);
8370
8579
  try {
8371
8580
  const rows = JSON.parse(stdout);
8372
- resolve6(Array.isArray(rows) ? rows : []);
8581
+ resolve7(Array.isArray(rows) ? rows : []);
8373
8582
  } catch {
8374
- resolve6([]);
8583
+ resolve7([]);
8375
8584
  }
8376
8585
  }
8377
8586
  );
8378
8587
  });
8379
8588
  }
8380
8589
  function findClaude(shell = process.env.SHELL || "/bin/sh", timeoutMs = 1e4) {
8381
- return new Promise((resolve6) => {
8590
+ return new Promise((resolve7) => {
8382
8591
  execFile3(
8383
8592
  shell,
8384
8593
  ["-ilc", "command -v claude"],
8385
8594
  { env: terminalEnv({}), timeout: timeoutMs },
8386
8595
  (_error, stdout) => {
8387
8596
  const found = String(stdout ?? "").split("\n").map((line2) => line2.trim()).reverse().find((line2) => line2.startsWith("/"));
8388
- resolve6(found ?? null);
8597
+ resolve7(found ?? null);
8389
8598
  }
8390
8599
  );
8391
8600
  });
@@ -8493,7 +8702,7 @@ var DEFAULT_TIMING3 = {
8493
8702
  confirmMs: 8e3,
8494
8703
  pasteMs: 600
8495
8704
  };
8496
- var sleep5 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
8705
+ var sleep5 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
8497
8706
  var EXIT_WAIT_MS3 = 5e3;
8498
8707
  var CodexDriver = class {
8499
8708
  constructor(terminals, timing = {}) {
@@ -8583,12 +8792,12 @@ var CodexDriver = class {
8583
8792
  if (before.draft && !leftover) return { status: "draft", draft: before.draft };
8584
8793
  const sent = leftover ?? text4;
8585
8794
  let taken = false;
8586
- const reported = new Promise((resolve6) => {
8795
+ const reported = new Promise((resolve7) => {
8587
8796
  this.waiters.set(name, {
8588
8797
  text: sent,
8589
8798
  taken: () => {
8590
8799
  taken = true;
8591
- resolve6();
8800
+ resolve7();
8592
8801
  }
8593
8802
  });
8594
8803
  });
@@ -8633,7 +8842,7 @@ var CodexDriver = class {
8633
8842
 
8634
8843
  // src/daemon.ts
8635
8844
  init_define_HERD_PACKAGE();
8636
- import { join as join21 } from "node:path";
8845
+ import { join as join23 } from "node:path";
8637
8846
 
8638
8847
  // src/dispatch-connection.ts
8639
8848
  init_define_HERD_PACKAGE();
@@ -8737,7 +8946,7 @@ var DispatchConnection = class {
8737
8946
 
8738
8947
  // src/daemon.ts
8739
8948
  var CONFIG_POLL_MS = 2e3;
8740
- var daemonSocketPath = () => join21(herdHome(), "daemon.sock");
8949
+ var daemonSocketPath = () => join23(herdHome(), "daemon.sock");
8741
8950
  var Daemon = class {
8742
8951
  constructor(options) {
8743
8952
  this.options = options;
@@ -8819,9 +9028,11 @@ var Daemon = class {
8819
9028
  return this.registering;
8820
9029
  }
8821
9030
  async register() {
8822
- const reposStamp = clonedStamp(this.root());
9031
+ const reposStamp = checkoutsStamp(this.root());
8823
9032
  const gh = this.options.gh?.();
9033
+ const folderUnavailable = this.options.folderUnavailable?.() ?? void 0;
8824
9034
  const facts = {
9035
+ ...folderUnavailable ? { folderUnavailable } : {},
8825
9036
  ...this.options.facts,
8826
9037
  ...this.herdConfig ? { config: this.herdConfig } : {},
8827
9038
  ...this.configError ? { configError: this.configError } : {},
@@ -8831,7 +9042,7 @@ var Daemon = class {
8831
9042
  const response = await this.options.api.fetch("/api/machines", {
8832
9043
  method: "POST",
8833
9044
  headers: { "Content-Type": "application/json" },
8834
- body: JSON.stringify(registration(this.config, this.clis, facts, clonedRepos(this.root())))
9045
+ body: JSON.stringify(registration(this.config, this.clis, facts, checkouts(this.root())))
8835
9046
  });
8836
9047
  if (!response.ok) {
8837
9048
  const body = await response.json().catch(() => ({}));
@@ -8859,7 +9070,7 @@ var Daemon = class {
8859
9070
  }
8860
9071
  }
8861
9072
  /**
8862
- * Registers again when `herd.config.ts` changed, or a repo was cloned into the root, so routing
9073
+ * Registers again when `herd.config.ts` changed, or a checkout came or went in the root, so routing
8863
9074
  * uses it within seconds.
8864
9075
  */
8865
9076
  async refreshHerdConfig() {
@@ -8869,7 +9080,7 @@ var Daemon = class {
8869
9080
  };
8870
9081
  if (this.refreshing) return false;
8871
9082
  const configChanged = source.stamp() !== this.configStamp;
8872
- if (!configChanged && clonedStamp(this.root()) === this.reposStamp) return false;
9083
+ if (!configChanged && checkoutsStamp(this.root()) === this.reposStamp) return false;
8873
9084
  this.refreshing = true;
8874
9085
  try {
8875
9086
  if (configChanged) await this.readHerdConfig();
@@ -8891,16 +9102,41 @@ var Daemon = class {
8891
9102
  this.connection?.stop();
8892
9103
  await this.local.close();
8893
9104
  }
9105
+ /** Messages held while a folder isn't there, by conversation, delivered once it is. */
9106
+ held = /* @__PURE__ */ new Map();
9107
+ /**
9108
+ * A folder came or went: registers again, so dispatch knows, and once nothing is missing,
9109
+ * delivers what was held meanwhile.
9110
+ */
9111
+ async foldersChanged() {
9112
+ await this.reregister();
9113
+ if (this.options.folderUnavailable?.()) return;
9114
+ for (const [conversationId, delivery] of [...this.held]) {
9115
+ this.held.delete(conversationId);
9116
+ await this.options.work.onMessage(conversationId, delivery);
9117
+ this.connection?.acknowledge(conversationId);
9118
+ }
9119
+ }
8894
9120
  async onFrame(frame) {
8895
9121
  if (frame.type === "offer") return this.onOffer(frame.offer);
8896
9122
  if (frame.type === "sign_in") return this.options.onSignIn?.(frame);
8897
9123
  if (frame.type === "message") {
9124
+ if (this.options.folderUnavailable?.()) {
9125
+ if (this.held.get(frame.conversationId) !== "interrupt") {
9126
+ this.held.set(frame.conversationId, frame.delivery);
9127
+ }
9128
+ return;
9129
+ }
8898
9130
  await this.options.work.onMessage(frame.conversationId, frame.delivery);
8899
9131
  this.connection?.acknowledge(frame.conversationId);
8900
9132
  }
8901
9133
  }
8902
9134
  async onOffer(offer) {
8903
- const decision = await this.options.work.onOffer(offer, this.config);
9135
+ const missing = this.options.folderUnavailable?.();
9136
+ const decision = missing ? {
9137
+ accept: false,
9138
+ reason: `A folder this machine works in isn't there: ${missing.target ? `${missing.path} links to ${missing.target}` : missing.path}.`
9139
+ } : await this.options.work.onOffer(offer, this.config);
8904
9140
  const answer2 = decision.accept ? { answer: "accept" } : { answer: "refuse", reason: decision.reason };
8905
9141
  const response = await this.options.api.fetch(
8906
9142
  `/api/machines/${encodeURIComponent(this.config.machineId)}/offers/${encodeURIComponent(offer.id)}`,
@@ -8920,10 +9156,101 @@ var Daemon = class {
8920
9156
  }
8921
9157
  };
8922
9158
 
9159
+ // src/folders.ts
9160
+ init_define_HERD_PACKAGE();
9161
+ import { existsSync as existsSync16, lstatSync as lstatSync3, readlinkSync as readlinkSync2, rmSync as rmSync14, writeFileSync as writeFileSync14 } from "node:fs";
9162
+ import { dirname as dirname13, resolve as resolve4 } from "node:path";
9163
+ var FOLDER_WAIT_MS = 2 * 60 * 1e3;
9164
+ var FOLDER_CHECK_MS = 30 * 1e3;
9165
+ function linkOf(path) {
9166
+ try {
9167
+ if (!lstatSync3(path).isSymbolicLink()) return null;
9168
+ return resolve4(dirname13(path), readlinkSync2(path));
9169
+ } catch {
9170
+ return null;
9171
+ }
9172
+ }
9173
+ function missingFolder(path, required, { exists = existsSync16, linkTarget = linkOf } = {}) {
9174
+ if (exists(path)) return null;
9175
+ const target = linkTarget(path);
9176
+ if (target) return { path, target };
9177
+ return required ? { path } : null;
9178
+ }
9179
+ function machineFolderProblem(root, convos, checks = {}) {
9180
+ return missingFolder(dirname13(root), true, checks) ?? missingFolder(convos, false, checks);
9181
+ }
9182
+ async function waitForFolders(problem, {
9183
+ timeoutMs = FOLDER_WAIT_MS,
9184
+ pollMs = 2e3,
9185
+ sleep: sleep8 = (ms) => new Promise((done) => setTimeout(done, ms)),
9186
+ now = Date.now,
9187
+ log = (line2) => console.log(line2)
9188
+ } = {}) {
9189
+ let missing = problem();
9190
+ if (!missing) return null;
9191
+ log(`Waiting for ${describeMissing(missing)} to be there, as when its drive mounts.`);
9192
+ const deadline = now() + timeoutMs;
9193
+ while (missing && now() < deadline) {
9194
+ await sleep8(pollMs);
9195
+ missing = problem();
9196
+ }
9197
+ if (!missing) {
9198
+ log("It's there.");
9199
+ return null;
9200
+ }
9201
+ log(
9202
+ `Taking no work or builds: ${describeMissing(missing)} still isn't there after ${Math.round(timeoutMs / 1e3)} seconds. Looking again every ${FOLDER_CHECK_MS / 1e3} seconds.`
9203
+ );
9204
+ return missing;
9205
+ }
9206
+ var waitForRoot = (root, options = {}) => waitForFolders(() => missingFolder(dirname13(root), true, options), options);
9207
+ var waitForConvos = (convos, options = {}) => waitForFolders(() => missingFolder(convos, false, options), options);
9208
+ var describeMissing = (missing) => missing.target ? `${missing.path}, which links to ${missing.target},` : missing.path;
9209
+ var FolderWatch = class {
9210
+ constructor(options) {
9211
+ this.options = options;
9212
+ this.current = options.initial;
9213
+ this.write();
9214
+ }
9215
+ options;
9216
+ current;
9217
+ timer = null;
9218
+ /** What's missing now, or null. */
9219
+ get problem() {
9220
+ return this.current;
9221
+ }
9222
+ start() {
9223
+ this.timer = setInterval(() => void this.tick(), this.options.everyMs ?? FOLDER_CHECK_MS);
9224
+ this.timer.unref?.();
9225
+ }
9226
+ stop() {
9227
+ if (this.timer) clearInterval(this.timer);
9228
+ }
9229
+ /** Looks once, and tells `onChange` when what's missing changed. */
9230
+ async tick() {
9231
+ const next = this.options.check();
9232
+ if (JSON.stringify(next) === JSON.stringify(this.current)) return;
9233
+ this.current = next;
9234
+ this.write();
9235
+ const log = this.options.log ?? ((line2) => console.log(line2));
9236
+ log(
9237
+ next ? `Taking no work or builds: ${describeMissing(next)} isn't there.` : "The machine's folders are there again, so it takes work again."
9238
+ );
9239
+ await this.options.onChange(next);
9240
+ }
9241
+ write() {
9242
+ try {
9243
+ if (this.current) writeFileSync14(this.options.statusPath, JSON.stringify(this.current));
9244
+ else rmSync14(this.options.statusPath, { force: true });
9245
+ } catch {
9246
+ }
9247
+ }
9248
+ };
9249
+
8923
9250
  // src/gh.ts
8924
9251
  init_define_HERD_PACKAGE();
8925
9252
  init_src();
8926
- import { execFile as execFile4, spawn as spawn8 } from "node:child_process";
9253
+ import { execFile as execFile4, spawn as spawn9 } from "node:child_process";
8927
9254
  import { promisify as promisify3 } from "node:util";
8928
9255
  var GH_CHECK_MS = 10 * 60 * 1e3;
8929
9256
  var GH_CODE_WAIT_MS = 30 * 1e3;
@@ -8994,7 +9321,7 @@ function readDeviceCode(output2) {
8994
9321
  return code && url === GITHUB_DEVICE_URL ? { code, url: GITHUB_DEVICE_URL } : null;
8995
9322
  }
8996
9323
  async function signInGh(options) {
8997
- const spawnGh = options.spawnGh ?? ((args) => spawn8("gh", args, {
9324
+ const spawnGh = options.spawnGh ?? ((args) => spawn9("gh", args, {
8998
9325
  env: options.env?.() ?? process.env,
8999
9326
  stdio: ["ignore", "pipe", "pipe"]
9000
9327
  }));
@@ -9003,8 +9330,8 @@ async function signInGh(options) {
9003
9330
  let sent = false;
9004
9331
  let resolveCode = () => {
9005
9332
  };
9006
- const codeShown = new Promise((resolve6) => {
9007
- resolveCode = resolve6;
9333
+ const codeShown = new Promise((resolve7) => {
9334
+ resolveCode = resolve7;
9008
9335
  });
9009
9336
  const read = (chunk) => {
9010
9337
  output2 += chunk.toString();
@@ -9012,9 +9339,9 @@ async function signInGh(options) {
9012
9339
  };
9013
9340
  child.stdout?.on("data", read);
9014
9341
  child.stderr?.on("data", read);
9015
- const exited = new Promise((resolve6) => {
9016
- child.once("close", () => resolve6());
9017
- child.once("error", () => resolve6());
9342
+ const exited = new Promise((resolve7) => {
9343
+ child.once("close", () => resolve7());
9344
+ child.once("error", () => resolve7());
9018
9345
  });
9019
9346
  void exited.then(() => resolveCode(false));
9020
9347
  const codeTimer = setTimeout(() => resolveCode(false), options.codeWaitMs ?? GH_CODE_WAIT_MS);
@@ -9047,23 +9374,23 @@ init_define_HERD_PACKAGE();
9047
9374
  import { execFileSync as execFileSync5 } from "node:child_process";
9048
9375
  import {
9049
9376
  appendFileSync as appendFileSync2,
9050
- existsSync as existsSync13,
9051
- mkdirSync as mkdirSync16,
9377
+ existsSync as existsSync17,
9378
+ mkdirSync as mkdirSync18,
9052
9379
  readFileSync as readFileSync17,
9053
- renameSync as renameSync5,
9054
- writeFileSync as writeFileSync14
9380
+ renameSync as renameSync6,
9381
+ writeFileSync as writeFileSync15
9055
9382
  } from "node:fs";
9056
- import { isAbsolute as isAbsolute3, join as join22 } from "node:path";
9057
- var LOCAL_SETTINGS = join22(".claude", "settings.local.json");
9383
+ import { isAbsolute as isAbsolute3, join as join24 } from "node:path";
9384
+ var LOCAL_SETTINGS = join24(".claude", "settings.local.json");
9058
9385
  var git3 = (cwd, ...args) => execFileSync5("git", ["-C", cwd, ...args], {
9059
9386
  encoding: "utf8",
9060
9387
  stdio: ["ignore", "pipe", "pipe"]
9061
9388
  }).trim();
9062
9389
  var shellQuote2 = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
9063
9390
  function writeWorktreeSettings(worktree, settings, ownCommand) {
9064
- const path = join22(worktree, LOCAL_SETTINGS);
9391
+ const path = join24(worktree, LOCAL_SETTINGS);
9065
9392
  let current = {};
9066
- if (existsSync13(path)) {
9393
+ if (existsSync17(path)) {
9067
9394
  try {
9068
9395
  const parsed = JSON.parse(readFileSync17(path, "utf8"));
9069
9396
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
@@ -9085,11 +9412,11 @@ function writeWorktreeSettings(worktree, settings, ownCommand) {
9085
9412
  }
9086
9413
  next.hooks = hooks;
9087
9414
  }
9088
- mkdirSync16(join22(worktree, ".claude"), { recursive: true });
9415
+ mkdirSync18(join24(worktree, ".claude"), { recursive: true });
9089
9416
  const temporary = `${path}.herd-${process.pid}`;
9090
- writeFileSync14(temporary, `${JSON.stringify(next, null, 2)}
9417
+ writeFileSync15(temporary, `${JSON.stringify(next, null, 2)}
9091
9418
  `);
9092
- renameSync5(temporary, path);
9419
+ renameSync6(temporary, path);
9093
9420
  ignoreLocalSettings(worktree);
9094
9421
  return true;
9095
9422
  }
@@ -9105,12 +9432,12 @@ function ignoreLocalSettings(worktree) {
9105
9432
  } catch {
9106
9433
  return;
9107
9434
  }
9108
- const info = join22(isAbsolute3(common) ? common : join22(worktree, common), "info");
9109
- const exclude = join22(info, "exclude");
9435
+ const info = join24(isAbsolute3(common) ? common : join24(worktree, common), "info");
9436
+ const exclude = join24(info, "exclude");
9110
9437
  const line2 = "/.claude/settings.local.json";
9111
- const existing = existsSync13(exclude) ? readFileSync17(exclude, "utf8") : "";
9438
+ const existing = existsSync17(exclude) ? readFileSync17(exclude, "utf8") : "";
9112
9439
  if (existing.split("\n").includes(line2)) return;
9113
- mkdirSync16(info, { recursive: true });
9440
+ mkdirSync18(info, { recursive: true });
9114
9441
  appendFileSync2(exclude, `${existing && !existing.endsWith("\n") ? "\n" : ""}${line2}
9115
9442
  `);
9116
9443
  }
@@ -9157,7 +9484,7 @@ function loginShellEnv() {
9157
9484
 
9158
9485
  // src/machine-checks.ts
9159
9486
  init_define_HERD_PACKAGE();
9160
- import { existsSync as existsSync14, rmSync as rmSync13 } from "node:fs";
9487
+ import { existsSync as existsSync18, rmSync as rmSync15 } from "node:fs";
9161
9488
  var REQUEST_POLL_MS = 2e3;
9162
9489
  var MachineChecks = class {
9163
9490
  constructor(options) {
@@ -9203,8 +9530,8 @@ var MachineChecks = class {
9203
9530
  /** `friday start` asked for every agent to be activated again. */
9204
9531
  takeRequest() {
9205
9532
  const path = activationRequestPath(this.options.herdHome);
9206
- if (!existsSync14(path)) return;
9207
- rmSync13(path, { force: true });
9533
+ if (!existsSync18(path)) return;
9534
+ rmSync15(path, { force: true });
9208
9535
  this.activateChosen(true);
9209
9536
  }
9210
9537
  start() {
@@ -9252,22 +9579,22 @@ var MachineChecks = class {
9252
9579
 
9253
9580
  // src/terminals.ts
9254
9581
  init_define_HERD_PACKAGE();
9255
- import { spawn as spawn9 } from "node:child_process";
9256
- import { mkdirSync as mkdirSync17, readdirSync as readdirSync7, rmSync as rmSync14 } from "node:fs";
9582
+ import { spawn as spawn10 } from "node:child_process";
9583
+ import { mkdirSync as mkdirSync19, readdirSync as readdirSync8, rmSync as rmSync16 } from "node:fs";
9257
9584
  import { connect as connect2 } from "node:net";
9258
- import { join as join23 } from "node:path";
9585
+ import { join as join25 } from "node:path";
9259
9586
  init_self();
9260
9587
  import {
9261
9588
  HOST_PROTOCOL,
9262
9589
  hostVersion
9263
9590
  } from "./terminal-host.mjs";
9264
- var hostsDir = () => join23(herdHome(), "terminals");
9265
- var legacyHostSocketPath = () => join23(herdHome(), "terminals.sock");
9266
- var currentHostSocketPath = (version = hostVersion()) => join23(hostsDir(), `${version}.sock`);
9591
+ var hostsDir = () => join25(herdHome(), "terminals");
9592
+ var legacyHostSocketPath = () => join25(herdHome(), "terminals.sock");
9593
+ var currentHostSocketPath = (version = hostVersion()) => join25(hostsDir(), `${version}.sock`);
9267
9594
  var speaksProtocol = (protocol) => protocol === HOST_PROTOCOL || protocol === HOST_PROTOCOL - 1;
9268
- var sleep6 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
9595
+ var sleep6 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
9269
9596
  function askHost(path, request) {
9270
- return new Promise((resolve6, reject) => {
9597
+ return new Promise((resolve7, reject) => {
9271
9598
  const socket = connect2(path);
9272
9599
  let data = "";
9273
9600
  socket.on("connect", () => socket.write(`${JSON.stringify(request)}
@@ -9276,7 +9603,7 @@ function askHost(path, request) {
9276
9603
  socket.on("end", () => {
9277
9604
  try {
9278
9605
  const reply = JSON.parse(data);
9279
- if (reply.ok) resolve6(reply);
9606
+ if (reply.ok) resolve7(reply);
9280
9607
  else reject(new HerdError(reply.error ?? "The terminal host refused that."));
9281
9608
  } catch {
9282
9609
  reject(new HerdError("The terminal host's answer wasn't readable."));
@@ -9311,10 +9638,10 @@ var HostTerminals = class {
9311
9638
  const dir = hostsDir();
9312
9639
  let names = [];
9313
9640
  try {
9314
- names = readdirSync7(dir).filter((name) => name.endsWith(".sock"));
9641
+ names = readdirSync8(dir).filter((name) => name.endsWith(".sock"));
9315
9642
  } catch {
9316
9643
  }
9317
- const paths = [legacyHostSocketPath(), ...names.map((name) => join23(dir, name))];
9644
+ const paths = [legacyHostSocketPath(), ...names.map((name) => join25(dir, name))];
9318
9645
  const found = [];
9319
9646
  for (const path of paths) {
9320
9647
  try {
@@ -9326,7 +9653,7 @@ var HostTerminals = class {
9326
9653
  speaks: speaksProtocol(hello.protocol)
9327
9654
  });
9328
9655
  } catch (error) {
9329
- if (isDead(error) && path !== legacyHostSocketPath()) rmSync14(path, { force: true });
9656
+ if (isDead(error) && path !== legacyHostSocketPath()) rmSync16(path, { force: true });
9330
9657
  }
9331
9658
  }
9332
9659
  this.hosts = found;
@@ -9347,8 +9674,8 @@ var HostTerminals = class {
9347
9674
  }
9348
9675
  if (this.current()) return;
9349
9676
  const path = currentHostSocketPath(this.version);
9350
- mkdirSync17(hostsDir(), { recursive: true, mode: 448 });
9351
- const child = spawn9(process.execPath, [herdBin, "terminal-host", "--socket", path], {
9677
+ mkdirSync19(hostsDir(), { recursive: true, mode: 448 });
9678
+ const child = spawn10(process.execPath, [herdBin, "terminal-host", "--socket", path], {
9352
9679
  detached: true,
9353
9680
  stdio: "ignore",
9354
9681
  env: { ...process.env, HERD_HOME: herdHome() }
@@ -9475,7 +9802,7 @@ var HostTerminals = class {
9475
9802
  await sleep6(100);
9476
9803
  if (!await askHost(host.path, { op: "hello" }).catch(() => null)) break;
9477
9804
  }
9478
- rmSync14(host.path, { force: true });
9805
+ rmSync16(host.path, { force: true });
9479
9806
  }
9480
9807
  return true;
9481
9808
  }
@@ -9594,8 +9921,8 @@ var UsageReporter = class {
9594
9921
  // src/workspaces/index.ts
9595
9922
  init_define_HERD_PACKAGE();
9596
9923
  init_src();
9597
- import { mkdirSync as mkdirSync21, readFileSync as readFileSync18, writeFileSync as writeFileSync17 } from "node:fs";
9598
- import { join as join30 } from "node:path";
9924
+ import { mkdirSync as mkdirSync23, readFileSync as readFileSync18, writeFileSync as writeFileSync18 } from "node:fs";
9925
+ import { join as join32 } from "node:path";
9599
9926
 
9600
9927
  // src/workspaces/codex.ts
9601
9928
  init_define_HERD_PACKAGE();
@@ -9607,7 +9934,7 @@ init_define_HERD_PACKAGE();
9607
9934
  init_define_HERD_PACKAGE();
9608
9935
  init_src();
9609
9936
  init_self();
9610
- import { join as join24 } from "node:path";
9937
+ import { join as join26 } from "node:path";
9611
9938
  import { z as z25 } from "zod";
9612
9939
  function hasThread(events) {
9613
9940
  return events.some(
@@ -9642,7 +9969,7 @@ function filesByMessage(events) {
9642
9969
  }
9643
9970
  return files;
9644
9971
  }
9645
- var FILES_FOLDER = join24(".herd", "files");
9972
+ var FILES_FOLDER = join26(".herd", "files");
9646
9973
  function safeFileName(name) {
9647
9974
  const chars = Array.from(name.replace(/[/\\\p{Cc}]/gu, "_").trim());
9648
9975
  while (Buffer.byteLength(chars.join("")) > 200) chars.shift();
@@ -9864,11 +10191,11 @@ async function claimChange(ws, workspace, change) {
9864
10191
  // src/workspaces/delivery.ts
9865
10192
  init_define_HERD_PACKAGE();
9866
10193
  import { randomUUID as randomUUID6 } from "node:crypto";
9867
- import { join as join28 } from "node:path";
10194
+ import { join as join30 } from "node:path";
9868
10195
 
9869
10196
  // src/workspaces/repos.ts
9870
10197
  init_define_HERD_PACKAGE();
9871
- import { join as join25 } from "node:path";
10198
+ import { join as join27 } from "node:path";
9872
10199
  var rootOf = (ws) => ws.options.root?.() ?? ws.options.config().root ?? defaultRoot();
9873
10200
  var gitEnvOf = (ws) => ws.options.gitEnv?.() ?? loginShellEnv();
9874
10201
  function knownRemotes(ws) {
@@ -9920,7 +10247,7 @@ function repoContext(ws, workspace) {
9920
10247
  remote: repo.remote || remotesOf(ws, { ...workspace, worktrees: [repo] })[0] || "",
9921
10248
  branch: repo.branch
9922
10249
  })),
9923
- cloned: clonedRepos(root).map(({ key, remote }) => ({ key, remote })),
10250
+ cloned: checkouts(root).map(({ key, remote }) => ({ key, remote })),
9924
10251
  described: (ws.options.herdConfig?.()?.repos ?? []).map((repo) => ({
9925
10252
  remote: repo.remote,
9926
10253
  key: repoKey(repo.remote),
@@ -9980,7 +10307,7 @@ async function addHints(ws, workspace, hints, handedOff) {
9980
10307
  const added = await addWorktree({
9981
10308
  root,
9982
10309
  remote: cloneSource(hint.remote, knownRemotes(ws)),
9983
- path: join25(workspace.folder, place.name),
10310
+ path: join27(workspace.folder, place.name),
9984
10311
  branch,
9985
10312
  ...handedOff ? { continueFrom: hint.branch ?? branch } : {},
9986
10313
  env: gitEnvOf(ws),
@@ -10000,6 +10327,8 @@ async function addHints(ws, workspace, hints, handedOff) {
10000
10327
  true
10001
10328
  );
10002
10329
  }
10330
+ if (added.copied.length > 0)
10331
+ await ws.note(id2, describeCopied(place.name, added.copied), true);
10003
10332
  const found = reposInFolder(workspace.folder).find((repo) => repo.folder === place.name);
10004
10333
  await recordRepo(ws, id2, {
10005
10334
  folder: place.name,
@@ -10030,14 +10359,14 @@ function handoffRepos(workspace) {
10030
10359
  return workspace.worktrees.map((repo) => ({
10031
10360
  folder: repo.folder,
10032
10361
  branch: repo.branch,
10033
- unpushed: hasUnpushed(join25(workspace.folder, repo.folder))
10362
+ unpushed: hasUnpushed(join27(workspace.folder, repo.folder))
10034
10363
  }));
10035
10364
  }
10036
10365
 
10037
10366
  // src/workspaces/setup.ts
10038
10367
  init_define_HERD_PACKAGE();
10039
- import { chmodSync as chmodSync4, mkdirSync as mkdirSync18, writeFileSync as writeFileSync15 } from "node:fs";
10040
- import { join as join26 } from "node:path";
10368
+ import { chmodSync as chmodSync4, mkdirSync as mkdirSync20, writeFileSync as writeFileSync16 } from "node:fs";
10369
+ import { join as join28 } from "node:path";
10041
10370
 
10042
10371
  // src/hooks.ts
10043
10372
  init_define_HERD_PACKAGE();
@@ -10070,7 +10399,7 @@ function configureClaude(ws, workspace) {
10070
10399
  configureClaudeIn(workspace.folder, (line2) => ws.log(`${workspace.conversationId}: ${line2}`));
10071
10400
  }
10072
10401
  function configureClaudeIn(folder, log) {
10073
- const herd = shellQuote2(join26(herdPathDir(), "herd"));
10402
+ const herd = shellQuote2(join28(herdPathDir(), "herd"));
10074
10403
  try {
10075
10404
  const written = writeWorktreeSettings(
10076
10405
  folder,
@@ -10095,10 +10424,10 @@ function configureClaudeIn(folder, log) {
10095
10424
  }
10096
10425
  }
10097
10426
  function herdPathDir() {
10098
- const dir = join26(herdHome(), "bin");
10099
- mkdirSync18(dir, { recursive: true, mode: 448 });
10100
- const shim = join26(dir, "herd");
10101
- writeFileSync15(shim, `#!/bin/sh
10427
+ const dir = join28(herdHome(), "bin");
10428
+ mkdirSync20(dir, { recursive: true, mode: 448 });
10429
+ const shim = join28(dir, "herd");
10430
+ writeFileSync16(shim, `#!/bin/sh
10102
10431
  exec "${process.execPath}" "${herdBin}" "$@"
10103
10432
  `);
10104
10433
  chmodSync4(shim, 493);
@@ -10116,8 +10445,8 @@ async function setUp(ws, conversationId) {
10116
10445
  const hints = ws.offeredRepos.get(conversationId) ?? [];
10117
10446
  ws.offeredRepos.delete(conversationId);
10118
10447
  const handedOff = ws.handedOff.delete(conversationId);
10119
- const folder = conversationFolder(rootOf(ws), conversationId);
10120
- mkdirSync18(folder, { recursive: true });
10448
+ const folder = folderForConversation(rootOf(ws), conversationId);
10449
+ mkdirSync20(folder, { recursive: true });
10121
10450
  const lead = `herd-${conversationPrefix(conversationId)}-1`;
10122
10451
  const secret = ws.options.local().grant({ terminalId: lead, conversationId });
10123
10452
  const workspace = {
@@ -10236,8 +10565,8 @@ init_define_HERD_PACKAGE();
10236
10565
  // src/workspaces/investigation.ts
10237
10566
  init_define_HERD_PACKAGE();
10238
10567
  import { randomUUID as randomUUID5 } from "node:crypto";
10239
- import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync19 } from "node:fs";
10240
- import { join as join27 } from "node:path";
10568
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync21 } from "node:fs";
10569
+ import { join as join29 } from "node:path";
10241
10570
  function needsInvestigator(ws, workspace, reading) {
10242
10571
  const failedAt = workspace.noticeFailedAt;
10243
10572
  if (workspace.agent === "codex" || workspace.agent === "agy") return false;
@@ -10393,9 +10722,9 @@ async function followInvestigation(ws, workspace) {
10393
10722
  }
10394
10723
  function saveScreen(ws, conversationId, screen, key) {
10395
10724
  try {
10396
- const dir = join27(herdHome(), "dialogs");
10397
- mkdirSync19(dir, { recursive: true });
10398
- const file = join27(dir, `${conversationId}-${Date.now()}.txt`);
10725
+ const dir = join29(herdHome(), "dialogs");
10726
+ mkdirSync21(dir, { recursive: true });
10727
+ const file = join29(dir, `${conversationId}-${Date.now()}.txt`);
10399
10728
  appendFileSync3(file, `${screen.join("\n")}
10400
10729
 
10401
10730
  Way out: ${key}
@@ -10599,7 +10928,7 @@ async function deliver(ws, conversationId, delivery) {
10599
10928
  await ensureLeadTerminal(ws, workspace);
10600
10929
  const codex = workspace.agent === "codex";
10601
10930
  if (workspace.agent === "agy") {
10602
- writeAgyFiles(workspace.folder, shellQuote2(join28(herdPathDir(), "herd")));
10931
+ writeAgyFiles(workspace.folder, shellQuote2(join30(herdPathDir(), "herd")));
10603
10932
  } else if (!codex) configureClaude(ws, workspace);
10604
10933
  const ownId = codex || workspace.agent === "agy";
10605
10934
  if (!workspace.started) {
@@ -10733,15 +11062,15 @@ init_define_HERD_PACKAGE();
10733
11062
  init_src();
10734
11063
  import {
10735
11064
  createWriteStream,
10736
- lstatSync as lstatSync2,
10737
- mkdirSync as mkdirSync20,
10738
- realpathSync as realpathSync8,
10739
- renameSync as renameSync6,
10740
- rmSync as rmSync15,
10741
- statSync as statSync8,
10742
- writeFileSync as writeFileSync16
11065
+ lstatSync as lstatSync4,
11066
+ mkdirSync as mkdirSync22,
11067
+ realpathSync as realpathSync10,
11068
+ renameSync as renameSync7,
11069
+ rmSync as rmSync17,
11070
+ statSync as statSync9,
11071
+ writeFileSync as writeFileSync17
10743
11072
  } from "node:fs";
10744
- import { dirname as dirname11, join as join29, sep as sep2 } from "node:path";
11073
+ import { dirname as dirname14, join as join31, sep as sep2 } from "node:path";
10745
11074
  import { Readable } from "node:stream";
10746
11075
  import { pipeline } from "node:stream/promises";
10747
11076
 
@@ -10754,8 +11083,8 @@ import { readFile } from "node:fs/promises";
10754
11083
  // src/attachments.ts
10755
11084
  init_define_HERD_PACKAGE();
10756
11085
  init_src();
10757
- import { realpathSync as realpathSync7, statSync as statSync7 } from "node:fs";
10758
- import { basename as basename3, extname, isAbsolute as isAbsolute4, relative as relative2, resolve as resolve4 } from "node:path";
11086
+ import { realpathSync as realpathSync9, statSync as statSync8 } from "node:fs";
11087
+ import { basename as basename3, extname, isAbsolute as isAbsolute4, relative as relative2, resolve as resolve5 } from "node:path";
10759
11088
  var TYPES = {
10760
11089
  ".png": "image/png",
10761
11090
  ".jpg": "image/jpeg",
@@ -10785,33 +11114,33 @@ function isInside(root, path) {
10785
11114
  function resolveAttachments(worktree, files) {
10786
11115
  let root;
10787
11116
  try {
10788
- root = realpathSync7(worktree);
11117
+ root = realpathSync9(worktree);
10789
11118
  } catch {
10790
11119
  return { ok: false, error: "This terminal's conversation folder is missing." };
10791
11120
  }
10792
11121
  const resolved = [];
10793
11122
  for (const file of files) {
10794
- let real3;
11123
+ let real4;
10795
11124
  try {
10796
- real3 = realpathSync7(resolve4(worktree, file.path));
11125
+ real4 = realpathSync9(resolve5(worktree, file.path));
10797
11126
  } catch {
10798
11127
  return { ok: false, error: `There's no file at ${file.path}.` };
10799
11128
  }
10800
- if (!isInside(root, real3)) {
11129
+ if (!isInside(root, real4)) {
10801
11130
  return {
10802
11131
  ok: false,
10803
11132
  error: `${file.path} is outside this conversation's folder, so it can't be attached.`
10804
11133
  };
10805
11134
  }
10806
- const stats = statSync7(real3);
11135
+ const stats = statSync8(real4);
10807
11136
  if (!stats.isFile()) return { ok: false, error: `${file.path} isn't a file.` };
10808
11137
  if (stats.size > MAX_FILE_BYTES) {
10809
11138
  return { ok: false, error: `${file.path} is larger than 100 MB, so it can't be attached.` };
10810
11139
  }
10811
11140
  resolved.push({
10812
- path: real3,
11141
+ path: real4,
10813
11142
  name: basename3(file.path),
10814
- type: mediaTypeFor(real3),
11143
+ type: mediaTypeFor(real4),
10815
11144
  size: stats.size,
10816
11145
  ...file.title ? { title: file.title } : {},
10817
11146
  ...file.alt ? { alt: file.alt } : {}
@@ -10969,11 +11298,11 @@ async function permission(ws, workspace, request, gone) {
10969
11298
  }
10970
11299
  if (state.state === "cancelled") return { ok: true, decision: null };
10971
11300
  }
10972
- await new Promise((resolve6) => {
10973
- const timer = setTimeout(resolve6, ws.options.promptPollMs ?? 1500);
11301
+ await new Promise((resolve7) => {
11302
+ const timer = setTimeout(resolve7, ws.options.promptPollMs ?? 1500);
10974
11303
  gone.addEventListener("abort", () => {
10975
11304
  clearTimeout(timer);
10976
- resolve6();
11305
+ resolve7();
10977
11306
  });
10978
11307
  });
10979
11308
  }
@@ -11327,7 +11656,7 @@ async function automations(ws, workspace, request, gone = new AbortController().
11327
11656
  const deadline = Date.now() + (asked.waitMs ?? 9e4);
11328
11657
  const promptPath = `${ws.conversationPath(workspace.conversationId)}/prompts/${encodeURIComponent(promptId ?? "")}`;
11329
11658
  while (promptId && Date.now() < deadline && !gone.aborted) {
11330
- await new Promise((resolve6) => setTimeout(resolve6, ws.options.promptPollMs ?? 1500));
11659
+ await new Promise((resolve7) => setTimeout(resolve7, ws.options.promptPollMs ?? 1500));
11331
11660
  const prompt = await ws.api(promptPath).catch(() => null);
11332
11661
  if (!prompt?.ok) continue;
11333
11662
  const state = await prompt.json();
@@ -11716,34 +12045,34 @@ async function bringIn(ws, workspace, files) {
11716
12045
  return listed;
11717
12046
  }
11718
12047
  async function download(ws, workspace, file) {
11719
- const path = join29(FILES_FOLDER, safeFileName(file.id), safeFileName(file.name));
11720
- const target = join29(workspace.folder, path);
11721
- if (statSync8(target, { throwIfNoEntry: false })?.size === file.size) return path;
11722
- const herd = join29(workspace.folder, ".herd");
11723
- if (lstatSync2(herd, { throwIfNoEntry: false })?.isSymbolicLink()) {
12048
+ const path = join31(FILES_FOLDER, safeFileName(file.id), safeFileName(file.name));
12049
+ const target = join31(workspace.folder, path);
12050
+ if (statSync9(target, { throwIfNoEntry: false })?.size === file.size) return path;
12051
+ const herd = join31(workspace.folder, ".herd");
12052
+ if (lstatSync4(herd, { throwIfNoEntry: false })?.isSymbolicLink()) {
11724
12053
  throw new Error("its folder leads outside the worktree");
11725
12054
  }
11726
- mkdirSync20(dirname11(target), { recursive: true });
11727
- if (!realpathSync8(dirname11(target)).startsWith(realpathSync8(workspace.folder) + sep2)) {
12055
+ mkdirSync22(dirname14(target), { recursive: true });
12056
+ if (!realpathSync10(dirname14(target)).startsWith(realpathSync10(workspace.folder) + sep2)) {
11728
12057
  throw new Error("its folder leads outside the worktree");
11729
12058
  }
11730
- writeFileSync16(join29(herd, ".gitignore"), "*\n");
12059
+ writeFileSync17(join31(herd, ".gitignore"), "*\n");
11731
12060
  const response = await ws.api(
11732
12061
  `${ws.conversationPath(workspace.conversationId)}/files/${encodeURIComponent(file.id)}`
11733
12062
  );
11734
12063
  if (!response.ok || !response.body) throw new Error(`the server answered ${response.status}`);
11735
12064
  const partial = `${target}.partial`;
11736
- rmSync15(partial, { force: true });
12065
+ rmSync17(partial, { force: true });
11737
12066
  try {
11738
12067
  await pipeline(
11739
12068
  Readable.fromWeb(response.body),
11740
12069
  createWriteStream(partial, { flags: "wx" })
11741
12070
  );
11742
- const { size } = statSync8(partial);
12071
+ const { size } = statSync9(partial);
11743
12072
  if (size !== file.size) throw new Error(`it came to ${size} bytes, not ${file.size}`);
11744
- renameSync6(partial, target);
12073
+ renameSync7(partial, target);
11745
12074
  } finally {
11746
- rmSync15(partial, { force: true });
12075
+ rmSync17(partial, { force: true });
11747
12076
  }
11748
12077
  return path;
11749
12078
  }
@@ -12017,7 +12346,7 @@ var Workspaces = class {
12017
12346
  return this.driver;
12018
12347
  }
12019
12348
  // ---- State, kept on disk so a restarted daemon picks up where it was.
12020
- statePath = () => join30(herdHome(), "state.json");
12349
+ statePath = () => join32(herdHome(), "state.json");
12021
12350
  load() {
12022
12351
  try {
12023
12352
  return stateSchema.parse(JSON.parse(readFileSync18(this.statePath(), "utf8"))).workspaces;
@@ -12026,8 +12355,8 @@ var Workspaces = class {
12026
12355
  }
12027
12356
  }
12028
12357
  save(workspaces) {
12029
- mkdirSync21(herdHome(), { recursive: true, mode: 448 });
12030
- writeFileSync17(this.statePath(), JSON.stringify({ workspaces }, null, 2), { mode: 384 });
12358
+ mkdirSync23(herdHome(), { recursive: true, mode: 448 });
12359
+ writeFileSync18(this.statePath(), JSON.stringify({ workspaces }, null, 2), { mode: 384 });
12031
12360
  }
12032
12361
  get(conversationId) {
12033
12362
  return this.load()[conversationId] ?? null;
@@ -12237,7 +12566,19 @@ async function runDaemon(args) {
12237
12566
  const root = setRoot(defaultRoot()).root;
12238
12567
  console.log(`No root folder is set for this machine, so it works from ${root}.`);
12239
12568
  }
12240
- await waitForRoot(machineRoot(), { pollMs: Number(process.env.HERD_ROOT_POLL_MS) || 2e3 });
12569
+ const folderTiming = {
12570
+ pollMs: Number(process.env.HERD_ROOT_POLL_MS) || 2e3,
12571
+ ...process.env.HERD_FOLDER_WAIT_MS ? { timeoutMs: Number(process.env.HERD_FOLDER_WAIT_MS) } : {}
12572
+ };
12573
+ const missing = await waitForRoot(machineRoot(), folderTiming) ?? await waitForConvos(convosPath(machineRoot()), folderTiming);
12574
+ let daemon = null;
12575
+ const folders = new FolderWatch({
12576
+ check: () => machineFolderProblem(machineRoot(), convosPath(machineRoot())),
12577
+ initial: missing,
12578
+ statusPath: join33(herdHome(), "folder-unavailable.json"),
12579
+ ...process.env.HERD_FOLDER_CHECK_MS ? { everyMs: Number(process.env.HERD_FOLDER_CHECK_MS) } : {},
12580
+ onChange: () => daemon?.foldersChanged()
12581
+ });
12241
12582
  const api = apiUrl();
12242
12583
  const terminals = new HostTerminals({ log: (line2) => console.log(line2) });
12243
12584
  await terminals.ensureHost();
@@ -12248,7 +12589,6 @@ async function runDaemon(args) {
12248
12589
  headers: isLocal(api) ? localSignIn() : { Authorization: `Bearer ${await accessToken()}` }
12249
12590
  })
12250
12591
  };
12251
- let daemon = null;
12252
12592
  let activator = null;
12253
12593
  let checks = null;
12254
12594
  const log = (line2) => console.log(line2);
@@ -12330,7 +12670,7 @@ async function runDaemon(args) {
12330
12670
  env: (secret) => envFor(workspaces, secret),
12331
12671
  // As for a conversation: Claude Code's hooks and status line, or agy's. Codex has none.
12332
12672
  configure: (agent, folder) => {
12333
- if (agent === "agy") writeAgyFiles(folder, shellQuote2(join31(herdPathDir(), "herd")));
12673
+ if (agent === "agy") writeAgyFiles(folder, shellQuote2(join33(herdPathDir(), "herd")));
12334
12674
  else if (agent === "claude-code") configureClaudeIn(folder, log);
12335
12675
  },
12336
12676
  ...shellCommand ? { shellCommand } : {},
@@ -12368,9 +12708,11 @@ async function runDaemon(args) {
12368
12708
  activations: () => activator?.states() ?? {},
12369
12709
  gh: () => checks?.gh ?? null,
12370
12710
  onSignIn: (frame) => void checks?.signIn(frame),
12371
- onConfigChanged: () => checks?.activateChosen()
12711
+ onConfigChanged: () => checks?.activateChosen(),
12712
+ folderUnavailable: () => folders.problem
12372
12713
  });
12373
12714
  await daemon.start();
12715
+ folders.start();
12374
12716
  checks.start();
12375
12717
  usage.start();
12376
12718
  const display = process.env.HERD_DISPLAY === "off" ? null : new (await Promise.resolve().then(() => (init_desk_display(), desk_display_exports))).DeskDisplay({
@@ -12411,9 +12753,11 @@ async function runDaemon(args) {
12411
12753
  headers: { Authorization: `Bearer ${runnerKey.key}` }
12412
12754
  }),
12413
12755
  serviceUrl: runnerKey.url,
12414
- capacity: () => sandbox.ok ? runnerCapacity(daemon?.configuration().config?.builds) : 0,
12415
- // As many as the service's hello takes, to put this runner first for their builds.
12416
- repos: () => clonedRepos(machineRoot()).map((repo) => repo.key).filter((key) => key.length <= 300).slice(0, 100),
12756
+ // None while a folder it works in isn't there, as for its conversations.
12757
+ capacity: () => sandbox.ok && !folders.problem ? runnerCapacity(daemon?.configuration().config?.builds) : 0,
12758
+ // The repos builds have cloned here, as many as the service's hello takes, to put this runner
12759
+ // first for their builds.
12760
+ repos: () => bareClones(join33(herdHome(), "builds", "clones")).map((repo) => repo.key).filter((key) => key.length <= 300).slice(0, 100),
12417
12761
  runner: new BuildRunner({
12418
12762
  root: () => machineRoot(),
12419
12763
  gitEnv,
@@ -12446,10 +12790,11 @@ async function runDaemon(args) {
12446
12790
  const codexFollower = setInterval(() => {
12447
12791
  void workspaces.followCodex().catch((error) => console.error(`Reading Codex's sessions failed: ${error}`));
12448
12792
  }, 250);
12449
- await new Promise((resolve6) => {
12450
- for (const signal2 of ["SIGINT", "SIGTERM"]) process.once(signal2, () => resolve6());
12793
+ await new Promise((resolve7) => {
12794
+ for (const signal2 of ["SIGINT", "SIGTERM"]) process.once(signal2, () => resolve7());
12451
12795
  });
12452
12796
  builds?.stop();
12797
+ folders.stop();
12453
12798
  checks.stop();
12454
12799
  activator.stop();
12455
12800
  usage.stop();
@@ -12543,11 +12888,11 @@ function agyAnswer(event2, output2) {
12543
12888
  }
12544
12889
 
12545
12890
  // src/hook-client.ts
12546
- var sleep7 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
12891
+ var sleep7 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
12547
12892
  function within2(promise, ms) {
12548
12893
  let timer;
12549
- const timeout = new Promise((resolve6) => {
12550
- timer = setTimeout(() => resolve6(null), ms);
12894
+ const timeout = new Promise((resolve7) => {
12895
+ timer = setTimeout(() => resolve7(null), ms);
12551
12896
  });
12552
12897
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
12553
12898
  }
@@ -12596,11 +12941,11 @@ async function runAgyHook(event2, stdin, env, options = {}) {
12596
12941
 
12597
12942
  // src/statusline.ts
12598
12943
  init_define_HERD_PACKAGE();
12599
- import { spawn as spawn10 } from "node:child_process";
12944
+ import { spawn as spawn11 } from "node:child_process";
12600
12945
  import { readFileSync as readFileSync21 } from "node:fs";
12601
12946
  import { connect as connect3 } from "node:net";
12602
12947
  import { homedir as homedir7 } from "node:os";
12603
- import { join as join32 } from "node:path";
12948
+ import { join as join34 } from "node:path";
12604
12949
  var MAX_INPUT = 64 * 1024;
12605
12950
  var REPORT_TIMEOUT_MS = 300;
12606
12951
  var OWN_COMMAND_TIMEOUT_MS = 5e3;
@@ -12637,7 +12982,7 @@ function agyStateFrom(input) {
12637
12982
  function ownAgyStatusLine(home = homedir7()) {
12638
12983
  try {
12639
12984
  const settings = JSON.parse(
12640
- readFileSync21(join32(home, ".gemini", "antigravity-cli", "settings.json"), "utf8")
12985
+ readFileSync21(join34(home, ".gemini", "antigravity-cli", "settings.json"), "utf8")
12641
12986
  );
12642
12987
  const command = settings.statusLine?.command;
12643
12988
  if (typeof command !== "string" || !command.trim()) return null;
@@ -12657,25 +13002,25 @@ function commandIn(path) {
12657
13002
  }
12658
13003
  }
12659
13004
  function ownStatusLine(projectDir, home = homedir7()) {
12660
- return commandIn(join32(projectDir, ".claude", "settings.json")) ?? commandIn(join32(home, ".claude", "settings.json"));
13005
+ return commandIn(join34(projectDir, ".claude", "settings.json")) ?? commandIn(join34(home, ".claude", "settings.json"));
12661
13006
  }
12662
13007
  function runOwnCommand(command, input, cwd, timeoutMs = OWN_COMMAND_TIMEOUT_MS) {
12663
- return new Promise((resolve6) => {
13008
+ return new Promise((resolve7) => {
12664
13009
  let output2 = "";
12665
13010
  let child;
12666
13011
  try {
12667
- child = spawn10("sh", ["-c", command], {
13012
+ child = spawn11("sh", ["-c", command], {
12668
13013
  cwd,
12669
13014
  detached: true,
12670
13015
  stdio: ["pipe", "pipe", "ignore"]
12671
13016
  });
12672
13017
  } catch {
12673
- return resolve6("");
13018
+ return resolve7("");
12674
13019
  }
12675
13020
  const finish = (text4) => {
12676
13021
  clearTimeout(timer);
12677
13022
  child.stdout?.destroy();
12678
- resolve6(text4);
13023
+ resolve7(text4);
12679
13024
  };
12680
13025
  const timer = setTimeout(() => {
12681
13026
  try {
@@ -12693,12 +13038,12 @@ function runOwnCommand(command, input, cwd, timeoutMs = OWN_COMMAND_TIMEOUT_MS)
12693
13038
  });
12694
13039
  }
12695
13040
  function tellDaemon(path, request, timeoutMs = REPORT_TIMEOUT_MS) {
12696
- return new Promise((resolve6) => {
13041
+ return new Promise((resolve7) => {
12697
13042
  const socket = connect3(path);
12698
13043
  const done = () => {
12699
13044
  clearTimeout(timer);
12700
13045
  socket.destroy();
12701
- resolve6();
13046
+ resolve7();
12702
13047
  };
12703
13048
  const timer = setTimeout(done, timeoutMs);
12704
13049
  socket.on("connect", () => socket.write(`${JSON.stringify(request)}
@@ -12950,7 +13295,7 @@ init_define_HERD_PACKAGE();
12950
13295
  import { connect as connect4 } from "node:net";
12951
13296
  var DETACH_KEY = "";
12952
13297
  function attach(name, { stdin, stdout } = { stdin: process.stdin, stdout: process.stdout }, path = currentHostSocketPath()) {
12953
- return new Promise((resolve6, reject) => {
13298
+ return new Promise((resolve7, reject) => {
12954
13299
  const socket = connect4(path);
12955
13300
  const size = () => ({ cols: stdout.columns || 120, rows: stdout.rows || 40 });
12956
13301
  let buffer = "";
@@ -12964,7 +13309,7 @@ function attach(name, { stdin, stdout } = { stdin: process.stdin, stdout: proces
12964
13309
  stdin.pause();
12965
13310
  stdout.off("resize", onResize);
12966
13311
  socket.end();
12967
- resolve6(result);
13312
+ resolve7(result);
12968
13313
  };
12969
13314
  const onInput = (chunk) => {
12970
13315
  const data = chunk.toString("utf8");
@@ -13049,20 +13394,21 @@ async function runTerminalHost(rest) {
13049
13394
 
13050
13395
  // src/commands/repo.ts
13051
13396
  init_define_HERD_PACKAGE();
13052
- import { join as join33 } from "node:path";
13397
+ import { join as join35 } from "node:path";
13053
13398
  import { z as z32 } from "zod";
13054
13399
  var REPO_HELP = `Usage: herd repo <url or owner/name> [folder]
13055
13400
 
13056
13401
  Checks a repo out in this conversation's folder, on a branch named for the conversation, started
13057
- from the freshly fetched default branch, and records it in the conversation. herd clones the repo
13058
- the first time this machine needs it, with your owner's own git credentials, and keeps that clone
13059
- for every conversation. owner/name means one of the repos herd repos lists, or else
13402
+ from the freshly fetched default branch, and records it in the conversation. It comes from your
13403
+ owner's own checkout of the repo in the machine's root, and gets that checkout's local files git
13404
+ ignores, such as .env, copied in. A repo the root doesn't have is cloned into it first, with your
13405
+ owner's own git credentials. owner/name means one of the repos herd repos lists, or else
13060
13406
  github.com/owner/name. The folder is the repo's name, or <owner>-<name> when that's taken, unless
13061
13407
  you give one. Running it again for a repo that's already here changes nothing.`;
13062
13408
  var REPOS_HELP = `Usage: herd repos [--json]
13063
13409
 
13064
- Lists the repos in this conversation's folder, the repos herd has cloned on this machine, and the
13065
- repos your owner's herd.config.ts describes, with what each is for. Add one with herd repo.`;
13410
+ Lists the repos in this conversation's folder, the repos checked out in this machine's root, and
13411
+ the repos your owner's herd.config.ts describes, with what each is for. Add one with herd repo.`;
13066
13412
  var repoContextSchema = z32.object({
13067
13413
  folder: z32.string(),
13068
13414
  root: z32.string(),
@@ -13136,7 +13482,7 @@ async function runRepo(rest) {
13136
13482
  folderName ? `${folderName} is already taken in this conversation's folder. Give another name.` : `Both ${repoName(key)} and ${repoOwner(key)}-${repoName(key)} are taken in this conversation's folder. Give a folder name: herd repo ${asked} <folder>.`
13137
13483
  );
13138
13484
  }
13139
- const path = join33(here.folder, place.name);
13485
+ const path = join35(here.folder, place.name);
13140
13486
  const recorded = here.recorded.some((repo) => repo.folder === place.name);
13141
13487
  if (place.existing && recorded) {
13142
13488
  const branch = here.recorded.find((repo) => repo.folder === place.name)?.branch ?? here.branch;
@@ -13178,13 +13524,14 @@ ${error.message}`);
13178
13524
  note = ` The conversation couldn't record it: ${error instanceof Error ? error.message : error}`;
13179
13525
  }
13180
13526
  const lines = [
13181
- added.created ? `Added ${key} in ${path}, on ${added.branch} from ${added.base}.${added.cloned ? " herd cloned it for this machine first." : ""}` : `${key} is already here, in ${path}.`
13527
+ added.created ? `Added ${key} in ${path}, on ${added.branch} from ${added.base}.${added.cloned ? " herd cloned it into the root first." : ""}` : `${key} is already here, in ${path}.`
13182
13528
  ];
13183
13529
  if (added.fetchFailed) {
13184
13530
  lines.push(
13185
13531
  `Couldn't fetch it, so it started from the last fetched ${added.base}: ${added.fetchFailed}`
13186
13532
  );
13187
13533
  }
13534
+ if (added.copied.length > 0) lines.push(describeCopied(place.name, added.copied));
13188
13535
  if (added.created) lines.push("Its own AGENTS.md or README says how to set it up.");
13189
13536
  console.log(lines.join("\n") + note);
13190
13537
  return 0;
@@ -13196,7 +13543,7 @@ function formatRepos(here) {
13196
13543
  for (const repo of here.recorded)
13197
13544
  lines.push(` ${repo.folder} ${repo.remote} on ${repo.branch}`);
13198
13545
  const cloned = here.cloned.filter((repo) => !inFolder.has(repo.key));
13199
- lines.push("", "Cloned on this machine:");
13546
+ lines.push("", "Checked out in the root:");
13200
13547
  if (cloned.length === 0) lines.push(" none others");
13201
13548
  const about = new Map(here.described.map((repo) => [repo.key, repo.about]));
13202
13549
  for (const repo of cloned) {
@@ -13240,7 +13587,7 @@ async function runRepos(rest) {
13240
13587
  init_define_HERD_PACKAGE();
13241
13588
  init_src();
13242
13589
  import { randomUUID as randomUUID8 } from "node:crypto";
13243
- import { resolve as resolve5 } from "node:path";
13590
+ import { resolve as resolve6 } from "node:path";
13244
13591
  var SEND_HELP = `Usage: herd send <text>
13245
13592
  herd send -- <text that starts with a dash>
13246
13593
  herd send < reply.txt
@@ -13323,7 +13670,7 @@ function withAbsolutePaths(message, cwd = process.cwd()) {
13323
13670
  return {
13324
13671
  ...message,
13325
13672
  files: files.map(
13326
- (file) => file && typeof file === "object" && typeof file.path === "string" ? { ...file, path: resolve5(cwd, file.path) } : file
13673
+ (file) => file && typeof file === "object" && typeof file.path === "string" ? { ...file, path: resolve6(cwd, file.path) } : file
13327
13674
  )
13328
13675
  };
13329
13676
  }
@@ -13960,7 +14307,7 @@ ${HELP4}`);
13960
14307
  // src/package-entry.ts
13961
14308
  var invoked = (() => {
13962
14309
  try {
13963
- return realpathSync9(process.argv[1] ?? "");
14310
+ return realpathSync11(process.argv[1] ?? "");
13964
14311
  } catch {
13965
14312
  return "";
13966
14313
  }