@herdingchats/friday 0.1.556 → 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 +890 -556
  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.556", gitHead: "1dcf65d7fd6285d076e72e3d28475c4bd04eae8e" };
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,11 +5426,11 @@ 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, win32 } from "node:path";
5347
- var systemdBooted = () => existsSync6("/run/systemd/system");
5432
+ import { dirname as dirname8, join as join10, win32 } from "node:path";
5433
+ var systemdBooted = () => existsSync7("/run/systemd/system");
5348
5434
  var LAUNCH_AGENT = "com.herdingchats.friday";
5349
5435
  var SYSTEMD_UNIT = "herdingchats-friday.service";
5350
5436
  async function detectService(run4, platform2 = process.platform, override = process.env.HERD_SERVICE, booted = systemdBooted) {
@@ -5360,7 +5446,7 @@ async function detectService(run4, platform2 = process.platform, override = proc
5360
5446
  function servicePath(node, platform2 = process.platform, inherited = process.env.PATH ?? process.env.Path) {
5361
5447
  if (platform2 === "win32") return [win32.dirname(node), inherited].filter(Boolean).join(";");
5362
5448
  return [
5363
- dirname7(node),
5449
+ dirname8(node),
5364
5450
  "/opt/homebrew/bin",
5365
5451
  "/usr/local/bin",
5366
5452
  "/usr/bin",
@@ -5431,13 +5517,13 @@ WantedBy=default.target
5431
5517
  `;
5432
5518
  }
5433
5519
  function writeIfChanged(path, text4) {
5434
- if (existsSync6(path) && readFileSync7(path, "utf8") === text4) return false;
5435
- mkdirSync7(dirname7(path), { recursive: true });
5520
+ if (existsSync7(path) && readFileSync7(path, "utf8") === text4) return false;
5521
+ mkdirSync8(dirname8(path), { recursive: true });
5436
5522
  writeFileSync7(path, text4);
5437
5523
  return true;
5438
5524
  }
5439
5525
  function launchd(context2) {
5440
- const plist = join9(context2.home, "Library", "LaunchAgents", `${LAUNCH_AGENT}.plist`);
5526
+ const plist = join10(context2.home, "Library", "LaunchAgents", `${LAUNCH_AGENT}.plist`);
5441
5527
  const domain = `gui/${userInfo().uid}`;
5442
5528
  const target = `${domain}/${LAUNCH_AGENT}`;
5443
5529
  const loaded = async () => (await context2.run("launchctl", ["print", target])).status === 0;
@@ -5463,7 +5549,7 @@ function launchd(context2) {
5463
5549
  };
5464
5550
  }
5465
5551
  function systemd(context2) {
5466
- const unit = join9(context2.home, ".config", "systemd", "user", SYSTEMD_UNIT);
5552
+ const unit = join10(context2.home, ".config", "systemd", "user", SYSTEMD_UNIT);
5467
5553
  const systemctl = async (...args) => {
5468
5554
  const ran = await context2.run("systemctl", ["--user", ...args]);
5469
5555
  if (ran.status !== 0)
@@ -5487,10 +5573,10 @@ function systemd(context2) {
5487
5573
  };
5488
5574
  }
5489
5575
  function spawnInBackground(command, env, log) {
5490
- mkdirSync7(dirname7(log), { recursive: true });
5576
+ mkdirSync8(dirname8(log), { recursive: true });
5491
5577
  const out = openSync3(log, "a");
5492
5578
  const [program = process.execPath, ...args] = command;
5493
- spawn2(program, args, {
5579
+ spawn3(program, args, {
5494
5580
  detached: true,
5495
5581
  // On Windows, a detached process otherwise opens a console window of its own.
5496
5582
  windowsHide: true,
@@ -5542,25 +5628,25 @@ function serviceFor(kind, context2) {
5542
5628
  // src/friday/start.ts
5543
5629
  init_define_HERD_PACKAGE();
5544
5630
  init_src();
5545
- import { mkdirSync as mkdirSync9, realpathSync as realpathSync3 } from "node:fs";
5546
- 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";
5547
5633
 
5548
5634
  // src/friday/legacy.ts
5549
5635
  init_define_HERD_PACKAGE();
5550
- 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";
5551
5637
  import { userInfo as userInfo2 } from "node:os";
5552
- import { join as join10 } from "node:path";
5638
+ import { join as join11 } from "node:path";
5553
5639
  var OLD_LAUNCH_AGENT = "com.herdingchats.herd";
5554
5640
  var OLD_APPS = ["herd-daemon", "herd-updater"];
5555
5641
  function oldPm2s(paths) {
5556
- const dir = join10(paths.herdHome, "daemon");
5557
- if (!existsSync7(dir)) return [];
5558
- 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));
5559
5645
  }
5560
5646
  async function retireOldDaemon(paths, { home, run: run4 }) {
5561
5647
  const removed = [];
5562
- const plist = join10(home, "Library", "LaunchAgents", `${OLD_LAUNCH_AGENT}.plist`);
5563
- if (existsSync7(plist)) {
5648
+ const plist = join11(home, "Library", "LaunchAgents", `${OLD_LAUNCH_AGENT}.plist`);
5649
+ if (existsSync8(plist)) {
5564
5650
  await run4("launchctl", ["bootout", `gui/${userInfo2().uid}/${OLD_LAUNCH_AGENT}`]);
5565
5651
  rmSync8(plist, { force: true });
5566
5652
  removed.push(`the login item ${OLD_LAUNCH_AGENT}`);
@@ -5584,24 +5670,24 @@ async function retireOldDaemon(paths, { home, run: run4 }) {
5584
5670
 
5585
5671
  // src/friday/versions.ts
5586
5672
  init_define_HERD_PACKAGE();
5587
- import { spawn as spawn3 } from "node:child_process";
5673
+ import { spawn as spawn4 } from "node:child_process";
5588
5674
  import {
5589
- existsSync as existsSync8,
5590
- mkdirSync as mkdirSync8,
5675
+ existsSync as existsSync9,
5676
+ mkdirSync as mkdirSync9,
5591
5677
  readdirSync as readdirSync4,
5592
5678
  readFileSync as readFileSync8,
5593
5679
  readlinkSync,
5594
- realpathSync as realpathSync2,
5680
+ realpathSync as realpathSync3,
5595
5681
  renameSync as renameSync3,
5596
5682
  rmSync as rmSync9,
5597
5683
  symlinkSync,
5598
5684
  writeFileSync as writeFileSync8
5599
5685
  } from "node:fs";
5600
- import { basename as basename2, dirname as dirname8, join as join11 } from "node:path";
5601
- 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) => {
5602
5688
  let stdout = "";
5603
5689
  let stderr = "";
5604
- const child = spawn3(command, args, {
5690
+ const child = spawn4(command, args, {
5605
5691
  cwd: options.cwd,
5606
5692
  env: options.env ?? process.env,
5607
5693
  stdio: ["ignore", "pipe", "pipe"]
@@ -5611,16 +5697,16 @@ var exec = (command, args, options = {}) => new Promise((resolve6) => {
5611
5697
  child.stderr.on("data", (data) => stderr += data);
5612
5698
  child.on("error", (error) => {
5613
5699
  if (timer) clearTimeout(timer);
5614
- resolve6({ status: 127, stdout, stderr: `${stderr}${error.message}` });
5700
+ resolve7({ status: 127, stdout, stderr: `${stderr}${error.message}` });
5615
5701
  });
5616
5702
  child.on("close", (status2) => {
5617
5703
  if (timer) clearTimeout(timer);
5618
- resolve6({ status: status2 ?? 1, stdout, stderr });
5704
+ resolve7({ status: status2 ?? 1, stdout, stderr });
5619
5705
  });
5620
5706
  });
5621
5707
  function npmCommand() {
5622
- const beside = join11(dirname8(process.execPath), "npm");
5623
- return existsSync8(beside) ? beside : "npm";
5708
+ const beside = join12(dirname9(process.execPath), "npm");
5709
+ return existsSync9(beside) ? beside : "npm";
5624
5710
  }
5625
5711
  function currentVersion(paths) {
5626
5712
  try {
@@ -5630,10 +5716,10 @@ function currentVersion(paths) {
5630
5716
  }
5631
5717
  }
5632
5718
  function installedVersions(paths) {
5633
- if (!existsSync8(paths.versions)) return [];
5719
+ if (!existsSync9(paths.versions)) return [];
5634
5720
  return readdirSync4(paths.versions).filter((name) => VERSION.test(name)).sort(compareVersions);
5635
5721
  }
5636
- var binOf = (versionDir, bin) => join11(packageDir(versionDir), `${bin}.mjs`);
5722
+ var binOf = (versionDir, bin) => join12(packageDir(versionDir), `${bin}.mjs`);
5637
5723
  async function versionRuns(versionDir, version, run4 = exec) {
5638
5724
  const ran = await run4(process.execPath, [binOf(versionDir, "herd"), "--version"], {
5639
5725
  timeoutMs: 3e4
@@ -5646,7 +5732,7 @@ async function versionRuns(versionDir, version, run4 = exec) {
5646
5732
  }
5647
5733
  function recordedIntegrity(dir) {
5648
5734
  try {
5649
- const lock = JSON.parse(readFileSync8(join11(dir, "package-lock.json"), "utf8"));
5735
+ const lock = JSON.parse(readFileSync8(join12(dir, "package-lock.json"), "utf8"));
5650
5736
  return lock.packages?.[`node_modules/${PACKAGE_NAME}`]?.integrity ?? null;
5651
5737
  } catch {
5652
5738
  return null;
@@ -5660,8 +5746,8 @@ function npmFreeEnv() {
5660
5746
  async function installVersion(paths, version, { spec, integrity, run: run4 = exec, env = {}, log = () => {
5661
5747
  } } = {}) {
5662
5748
  if (!VERSION.test(version)) return { ok: false, reason: `${version} isn't a version.` };
5663
- const dir = join11(paths.versions, version);
5664
- if (existsSync8(dir)) {
5749
+ const dir = join12(paths.versions, version);
5750
+ if (existsSync9(dir)) {
5665
5751
  const installed2 = recordedIntegrity(dir);
5666
5752
  if (integrity && installed2 !== integrity)
5667
5753
  return { ok: false, reason: `the installed ${version} doesn't have the integrity named.` };
@@ -5670,12 +5756,12 @@ async function installVersion(paths, version, { spec, integrity, run: run4 = exe
5670
5756
  return { ok: false, reason: `the installed ${version} doesn't run: ${broken2}.` };
5671
5757
  }
5672
5758
  sweepStaging(paths);
5673
- const named = join11(paths.versions, `.staging-${version}-${process.pid}`);
5759
+ const named = join12(paths.versions, `.staging-${version}-${process.pid}`);
5674
5760
  rmSync9(named, { recursive: true, force: true });
5675
- mkdirSync8(named, { recursive: true });
5676
- const staging = realpathSync2(named);
5761
+ mkdirSync9(named, { recursive: true });
5762
+ const staging = realpathSync3(named);
5677
5763
  writeFileSync8(
5678
- join11(staging, "package.json"),
5764
+ join12(staging, "package.json"),
5679
5765
  `${JSON.stringify({ private: true, allowScripts: { "node-pty": true } }, null, 2)}
5680
5766
  `
5681
5767
  );
@@ -5710,21 +5796,21 @@ async function installVersion(paths, version, { spec, integrity, run: run4 = exe
5710
5796
  }
5711
5797
  const broken = await versionRuns(staging, version, run4);
5712
5798
  if (broken) return fail(broken);
5713
- renameSync3(staging, join11(realpathSync2(paths.versions), version));
5799
+ renameSync3(staging, join12(realpathSync3(paths.versions), version));
5714
5800
  return { ok: true, dir };
5715
5801
  }
5716
5802
  function sweepStaging(paths) {
5717
- if (!existsSync8(paths.versions)) return;
5803
+ if (!existsSync9(paths.versions)) return;
5718
5804
  for (const name of readdirSync4(paths.versions)) {
5719
5805
  const match = /^\.staging-.+-(\d+)$/.exec(name);
5720
5806
  if (match && !alive2(Number(match[1])))
5721
- rmSync9(join11(paths.versions, name), { recursive: true, force: true });
5807
+ rmSync9(join12(paths.versions, name), { recursive: true, force: true });
5722
5808
  }
5723
5809
  }
5724
5810
  function activate(paths, version) {
5725
5811
  const next = `${paths.current}.next-${process.pid}`;
5726
5812
  rmSync9(next, { force: true });
5727
- symlinkSync(join11("versions", version), next);
5813
+ symlinkSync(join12("versions", version), next);
5728
5814
  renameSync3(next, paths.current);
5729
5815
  }
5730
5816
  function pruneVersions(paths, { inUse = [], keep = 3 } = {}) {
@@ -5739,7 +5825,7 @@ function pruneVersions(paths, { inUse = [], keep = 3 } = {}) {
5739
5825
  const removed = [];
5740
5826
  for (const version of versions) {
5741
5827
  if (kept.has(version)) continue;
5742
- rmSync9(join11(paths.versions, version), { recursive: true, force: true });
5828
+ rmSync9(join12(paths.versions, version), { recursive: true, force: true });
5743
5829
  removed.push(version);
5744
5830
  }
5745
5831
  return removed;
@@ -5749,7 +5835,7 @@ async function hostVersions(paths, run4 = exec) {
5749
5835
  if (ran.status !== 0) return installedVersions(paths);
5750
5836
  const roots = /* @__PURE__ */ new Set([paths.versions]);
5751
5837
  try {
5752
- roots.add(realpathSync2(paths.versions));
5838
+ roots.add(realpathSync3(paths.versions));
5753
5839
  } catch {
5754
5840
  }
5755
5841
  const found = /* @__PURE__ */ new Set();
@@ -5787,13 +5873,13 @@ async function missingBuildTools(run4) {
5787
5873
  }
5788
5874
  var real = (path) => {
5789
5875
  try {
5790
- return realpathSync3(path);
5876
+ return realpathSync4(path);
5791
5877
  } catch {
5792
5878
  return resolve3(path);
5793
5879
  }
5794
5880
  };
5795
5881
  function chooseRoot(cwd, home) {
5796
- return real(cwd) === real(home) ? join12(home, "friday") : resolve3(cwd);
5882
+ return real(cwd) === real(home) ? join13(home, "friday") : resolve3(cwd);
5797
5883
  }
5798
5884
  async function runStart(options) {
5799
5885
  const { paths, run: run4 } = options;
@@ -5803,7 +5889,7 @@ async function runStart(options) {
5803
5889
  const wanted = chooseRoot(options.cwd, options.home);
5804
5890
  const running = runningSupervisor(paths);
5805
5891
  if (existing && real(existing) !== real(wanted)) {
5806
- 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.`);
5807
5893
  if (running) {
5808
5894
  print(`Friday is running there, version ${running.version}, so nothing changed.`);
5809
5895
  await waitForAgents(options, await chooseAndAsk(options, print), print);
@@ -5821,7 +5907,7 @@ async function runStart(options) {
5821
5907
  const repo = insideWorkTree(root);
5822
5908
  if (repo) {
5823
5909
  throw new StartError(
5824
- `${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.`
5825
5911
  );
5826
5912
  }
5827
5913
  if ((options.platform ?? process.platform) === "linux") {
@@ -5843,7 +5929,7 @@ async function runStart(options) {
5843
5929
  `Stopped the old daemon: removed ${retired.join(" and ")}. Its checkout is left as it was.`
5844
5930
  );
5845
5931
  await ensureInstalled(options, print);
5846
- mkdirSync9(root, { recursive: true });
5932
+ mkdirSync10(root, { recursive: true });
5847
5933
  writeRoot(paths, root);
5848
5934
  const command = [process.execPath, binOf(paths.current, "friday"), "supervise"];
5849
5935
  const env = { HERD_HOME: paths.herdHome, ...options.env };
@@ -5921,14 +6007,14 @@ async function ensureInstalled(options, print) {
5921
6007
  // src/friday/supervisor.ts
5922
6008
  init_define_HERD_PACKAGE();
5923
6009
  init_src();
5924
- import { spawn as spawn4 } from "node:child_process";
6010
+ import { spawn as spawn5 } from "node:child_process";
5925
6011
  import {
5926
6012
  closeSync as closeSync3,
5927
- existsSync as existsSync9,
5928
- mkdirSync as mkdirSync10,
6013
+ existsSync as existsSync10,
6014
+ mkdirSync as mkdirSync11,
5929
6015
  openSync as openSync4,
5930
6016
  readFileSync as readFileSync9,
5931
- realpathSync as realpathSync4,
6017
+ realpathSync as realpathSync5,
5932
6018
  renameSync as renameSync4,
5933
6019
  statSync as statSync6
5934
6020
  } from "node:fs";
@@ -5998,7 +6084,7 @@ var Supervisor = class {
5998
6084
  if (this.options.daemonCommand) return this.options.daemonCommand();
5999
6085
  let dir;
6000
6086
  try {
6001
- dir = realpathSync4(this.options.paths.current);
6087
+ dir = realpathSync5(this.options.paths.current);
6002
6088
  } catch {
6003
6089
  throw new Error(`Nothing is installed in ~/.herd/current. Run ${START_COMMAND}.`);
6004
6090
  }
@@ -6007,16 +6093,16 @@ var Supervisor = class {
6007
6093
  spawnDaemon() {
6008
6094
  if (this.stopping) return;
6009
6095
  const { daemonLog, logs } = this.options.paths;
6010
- mkdirSync10(logs, { recursive: true });
6096
+ mkdirSync11(logs, { recursive: true });
6011
6097
  try {
6012
- if (existsSync9(daemonLog) && statSync6(daemonLog).size > MAX_LOG_BYTES2)
6098
+ if (existsSync10(daemonLog) && statSync6(daemonLog).size > MAX_LOG_BYTES2)
6013
6099
  renameSync4(daemonLog, `${daemonLog}.1`);
6014
6100
  } catch {
6015
6101
  }
6016
6102
  const out = openSync4(daemonLog, "a");
6017
6103
  const [program = process.execPath, ...args] = this.daemonCommand();
6018
6104
  const startedAt = Date.now();
6019
- 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 });
6020
6106
  closeSync3(out);
6021
6107
  this.daemon = child;
6022
6108
  this.status.daemonPid = child.pid ?? null;
@@ -6040,7 +6126,7 @@ var Supervisor = class {
6040
6126
  async stopDaemon() {
6041
6127
  const child = this.daemon;
6042
6128
  if (!child || child.exitCode !== null || child.signalCode !== null) return;
6043
- const exited = new Promise((resolve6) => child.once("exit", () => resolve6()));
6129
+ const exited = new Promise((resolve7) => child.once("exit", () => resolve7()));
6044
6130
  child.kill("SIGTERM");
6045
6131
  const timer = setTimeout(() => child.kill("SIGKILL"), this.options.stopMs ?? 15e3);
6046
6132
  await exited;
@@ -6147,8 +6233,8 @@ var HELP = `friday: run HerdingChats' agent daemon on this computer.
6147
6233
 
6148
6234
  Usage: ${FRIDAY_COMMAND} <command>
6149
6235
 
6150
- start Make this computer a machine, with the folder this runs in as its root, where
6151
- 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
6152
6238
  needed, asks how much of each agent you have, starts the daemon now and whenever
6153
6239
  you log in, waits for each agent to check in, then shows what's left to set up.
6154
6240
  Safe to run again. --agents claude=lot,codex=little sets the shares without
@@ -6162,7 +6248,7 @@ Agents use the herd command, which comes with this one. herd setup shows what's
6162
6248
  function defaults() {
6163
6249
  const home = homedir4();
6164
6250
  return {
6165
- paths: fridayPaths(process.env.HERD_HOME ?? join13(home, ".herd")),
6251
+ paths: fridayPaths(process.env.HERD_HOME ?? join14(home, ".herd")),
6166
6252
  home,
6167
6253
  cwd: process.cwd(),
6168
6254
  run: exec,
@@ -6272,12 +6358,12 @@ function agentsFlag(args) {
6272
6358
  return parsed.shares;
6273
6359
  }
6274
6360
  function attachTo(paths, terminal) {
6275
- const herd = existsSync10(binOf(paths.current, "herd")) ? binOf(paths.current, "herd") : join13(dirname9(process.argv[1] ?? ""), "herd.mjs");
6276
- 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], {
6277
6363
  stdio: "inherit",
6278
6364
  env: { ...process.env, HERD_HOME: paths.herdHome }
6279
6365
  });
6280
- return new Promise((resolve6) => child.on("close", () => resolve6()));
6366
+ return new Promise((resolve7) => child.on("close", () => resolve7()));
6281
6367
  }
6282
6368
  async function runStop(deps) {
6283
6369
  const status2 = readStatus(deps.paths);
@@ -6322,13 +6408,28 @@ function runStatus(deps) {
6322
6408
  );
6323
6409
  if (version) print(`Installed: ${version}${pin ? `, pinned to ${pin} in ${paths.pin}` : ""}.`);
6324
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
+ }
6325
6417
  if (running && status2.checkedAt) print(`Last asked for a release ${ago(status2.checkedAt)}.`);
6326
6418
  if (status2?.problem) print(`Last problem: ${status2.problem}`);
6327
6419
  print(`Logs: ${paths.logs}`);
6328
6420
  return running ? 0 : 1;
6329
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
+ }
6330
6431
  function tail(path, count) {
6331
- if (!existsSync10(path)) return [];
6432
+ if (!existsSync11(path)) return [];
6332
6433
  const lines = readFileSync10(path, "utf8").split("\n");
6333
6434
  if (lines.at(-1) === "") lines.pop();
6334
6435
  return lines.slice(-count);
@@ -6336,14 +6437,14 @@ function tail(path, count) {
6336
6437
  async function runLogs(args, deps) {
6337
6438
  const { paths, print } = deps;
6338
6439
  const follow = args.includes("-f") || args.includes("--follow");
6339
- const files = [paths.supervisorLog, paths.daemonLog].filter((file) => existsSync10(file));
6440
+ const files = [paths.supervisorLog, paths.daemonLog].filter((file) => existsSync11(file));
6340
6441
  if (files.length === 0) {
6341
6442
  print(`There are no logs in ${paths.logs} yet.`);
6342
6443
  return 0;
6343
6444
  }
6344
6445
  if (follow) {
6345
- const child = spawn5("tail", ["-n", "50", "-F", ...files], { stdio: "inherit" });
6346
- 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)));
6347
6448
  }
6348
6449
  for (const file of files) {
6349
6450
  print(`==> ${file} <==`);
@@ -6384,9 +6485,9 @@ init_define_HERD_PACKAGE();
6384
6485
 
6385
6486
  // src/build-sandbox.ts
6386
6487
  init_define_HERD_PACKAGE();
6387
- import { spawn as spawn6 } from "node:child_process";
6488
+ import { spawn as spawn7 } from "node:child_process";
6388
6489
  import { appendFileSync, readFileSync as readFileSync11 } from "node:fs";
6389
- import { dirname as dirname10, join as join14 } from "node:path";
6490
+ import { dirname as dirname11, join as join15 } from "node:path";
6390
6491
  var DEPLOY_HOSTS = ["api.cloudflare.com", "api.github.com", "registry.npmjs.org"];
6391
6492
  function buildPolicy(input) {
6392
6493
  const platform2 = input.platform ?? process.platform;
@@ -6402,7 +6503,7 @@ function buildPolicy(input) {
6402
6503
  ...platform2 === "darwin" ? { allowUnixSockets: [input.home] } : { allowAllUnixSockets: true }
6403
6504
  },
6404
6505
  filesystem: {
6405
- denyRead: [.../* @__PURE__ */ new Set([input.ownerHome, input.root])],
6506
+ denyRead: [.../* @__PURE__ */ new Set([input.ownerHome, input.root, input.convos])],
6406
6507
  allowRead: [input.worktree, input.store, input.nodePrefix, input.home],
6407
6508
  allowWrite: [input.worktree, input.worktreeGitDir, input.home],
6408
6509
  denyWrite: []
@@ -6416,7 +6517,7 @@ function describePolicy(policy, platform2 = process.platform) {
6416
6517
  const hosts = policy.network.allowedDomains.join(", ");
6417
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}.`;
6418
6519
  }
6419
- var nodePrefix = (execPath = process.execPath) => dirname10(dirname10(execPath));
6520
+ var nodePrefix = (execPath = process.execPath) => dirname11(dirname11(execPath));
6420
6521
  var shellQuote = (args) => args.map((arg) => `'${arg.replace(/'/g, "'\\''")}'`).join(" ");
6421
6522
  var BLOCKED_LOG = "blocked.log";
6422
6523
  async function runSandboxed(args) {
@@ -6433,7 +6534,7 @@ async function runSandboxed(args) {
6433
6534
  return 3;
6434
6535
  }
6435
6536
  await SandboxManager.initialize(policy, void 0, true);
6436
- const blocked = join14(process.env.HOME ?? ".", BLOCKED_LOG);
6537
+ const blocked = join15(process.env.HOME ?? ".", BLOCKED_LOG);
6437
6538
  let written = 0;
6438
6539
  const unsubscribe = SandboxManager.getSandboxViolationStore().subscribe((violations) => {
6439
6540
  const fresh = violations.slice(written).map((violation) => describeRefusal(violation.line)).filter((line2) => line2 !== null);
@@ -6446,13 +6547,13 @@ async function runSandboxed(args) {
6446
6547
  }
6447
6548
  });
6448
6549
  const wrapped = await SandboxManager.wrapWithSandbox(shellQuote(command));
6449
- const child = spawn6(wrapped, { shell: true, stdio: "inherit" });
6550
+ const child = spawn7(wrapped, { shell: true, stdio: "inherit" });
6450
6551
  const forward = (signal2) => () => child.kill(signal2);
6451
6552
  process.once("SIGTERM", forward("SIGTERM"));
6452
6553
  process.once("SIGINT", forward("SIGINT"));
6453
- const status2 = await new Promise((resolve6) => {
6454
- child.on("error", () => resolve6(1));
6455
- 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)));
6456
6557
  });
6457
6558
  unsubscribe();
6458
6559
  SandboxManager.cleanupAfterCommand();
@@ -6483,7 +6584,7 @@ async function fromTerminal(request) {
6483
6584
  reply = await askDaemon(socket, { secret, ...request }).catch(() => null);
6484
6585
  if (reply) break;
6485
6586
  if (Date.now() > deadline) throw new HerdError("Couldn't reach herd's daemon. Is it running?");
6486
- await new Promise((resolve6) => setTimeout(resolve6, 500));
6587
+ await new Promise((resolve7) => setTimeout(resolve7, 500));
6487
6588
  }
6488
6589
  if (!reply.ok) throw new HerdError(reply.error);
6489
6590
  return reply;
@@ -6910,8 +7011,8 @@ init_define_HERD_PACKAGE();
6910
7011
  // src/build-runner-key.ts
6911
7012
  init_define_HERD_PACKAGE();
6912
7013
  init_src();
6913
- import { chmodSync as chmodSync3, existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
6914
- 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";
6915
7016
  import { z as z24 } from "zod";
6916
7017
  var BUILDS_URL = "https://builds.herdingchats.com";
6917
7018
  var runnerKeySchema = z24.object({
@@ -6919,10 +7020,10 @@ var runnerKeySchema = z24.object({
6919
7020
  key: z24.string().min(20),
6920
7021
  url: z24.string().url()
6921
7022
  });
6922
- var runnerKeyPath = () => join15(herdHome(), "build-runner.json");
7023
+ var runnerKeyPath = () => join16(herdHome(), "build-runner.json");
6923
7024
  function readRunnerKey() {
6924
7025
  const path = runnerKeyPath();
6925
- if (!existsSync11(path)) return null;
7026
+ if (!existsSync12(path)) return null;
6926
7027
  try {
6927
7028
  return runnerKeySchema.parse(JSON.parse(readFileSync13(path, "utf8")));
6928
7029
  } catch {
@@ -6932,7 +7033,7 @@ function readRunnerKey() {
6932
7033
  }
6933
7034
  }
6934
7035
  function saveRunnerKey(key) {
6935
- mkdirSync11(herdHome(), { recursive: true, mode: 448 });
7036
+ mkdirSync12(herdHome(), { recursive: true, mode: 448 });
6936
7037
  const path = runnerKeyPath();
6937
7038
  writeFileSync9(path, JSON.stringify(runnerKeySchema.parse(key), null, 2), { mode: 384 });
6938
7039
  chmodSync3(path, 384);
@@ -6947,7 +7048,7 @@ async function registerRunner({
6947
7048
  url = buildsUrl(),
6948
7049
  fetchImpl = fetch,
6949
7050
  say = (line2) => console.log(line2),
6950
- sleep: sleep8 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms)),
7051
+ sleep: sleep8 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
6951
7052
  pollMs = 2e3,
6952
7053
  waitMs = 15 * 60 * 1e3
6953
7054
  }) {
@@ -6986,8 +7087,8 @@ async function registerRunner({
6986
7087
 
6987
7088
  // src/builds-admin.ts
6988
7089
  init_define_HERD_PACKAGE();
6989
- import { mkdirSync as mkdirSync12, readFileSync as readFileSync14, rmSync as rmSync10, writeFileSync as writeFileSync10 } from "node:fs";
6990
- 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";
6991
7092
  var NOT_ADMIN = "builds are only for the admin's machines";
6992
7093
  async function isAdmin(fetch2) {
6993
7094
  const response = await fetch2("/api/people/me").catch(() => null);
@@ -6995,13 +7096,13 @@ async function isAdmin(fetch2) {
6995
7096
  const body = await response.json().catch(() => ({}));
6996
7097
  return body.admin === true;
6997
7098
  }
6998
- var refusalPath = () => join16(herdHome(), "builds", "refused.json");
7099
+ var refusalPath = () => join17(herdHome(), "builds", "refused.json");
6999
7100
  function writeBuildsRefusal(reason) {
7000
7101
  if (!reason) {
7001
7102
  rmSync10(refusalPath(), { force: true });
7002
7103
  return;
7003
7104
  }
7004
- mkdirSync12(join16(herdHome(), "builds"), { recursive: true });
7105
+ mkdirSync13(join17(herdHome(), "builds"), { recursive: true });
7005
7106
  writeFileSync10(refusalPath(), JSON.stringify({ reason }));
7006
7107
  }
7007
7108
  function readBuildsRefusal() {
@@ -7147,7 +7248,7 @@ async function runConversations(rest) {
7147
7248
  init_define_HERD_PACKAGE();
7148
7249
  init_src();
7149
7250
  import { availableParallelism, totalmem } from "node:os";
7150
- import { join as join31 } from "node:path";
7251
+ import { join as join33 } from "node:path";
7151
7252
 
7152
7253
  // src/agent.ts
7153
7254
  init_define_HERD_PACKAGE();
@@ -7256,17 +7357,34 @@ function readClaudeScreen(raw) {
7256
7357
 
7257
7358
  // src/worktrees.ts
7258
7359
  init_define_HERD_PACKAGE();
7259
- import { isAbsolute as isAbsolute2, join as join17, relative } from "node:path";
7260
- 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");
7261
7363
  var WORD_ID = /^[a-z]+-[a-z]+-[a-z]+$/;
7262
7364
  var conversationPrefix = (conversationId) => WORD_ID.test(conversationId) ? conversationId : conversationId.replace(/[^A-Za-z0-9]/g, "").slice(0, 12).toLowerCase();
7263
7365
  var conversationBranch = (conversationId) => `herd/${conversationPrefix(conversationId)}`;
7264
- 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
+ }
7265
7371
  var within = (dir, path) => {
7266
7372
  const between = relative(dir, path);
7267
7373
  return between !== "" && !between.startsWith("..") && !isAbsolute2(between);
7268
7374
  };
7269
- 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
+ }
7270
7388
 
7271
7389
  // src/agent.ts
7272
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. ";
@@ -7287,7 +7405,7 @@ var ownPrompt = (draft) => OWN_PROMPTS.find(
7287
7405
  var SESSION_ID = /^[A-Za-z0-9-]{1,100}$/;
7288
7406
  var SUBMIT_RETRIES = 3;
7289
7407
  var DEFAULT_TIMING = { settleMs: 800, pollMs: 150, startMs: 6e4, confirmMs: 3e3 };
7290
- var sleep3 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
7408
+ var sleep3 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
7291
7409
  var EXIT_WAIT_MS = 5e3;
7292
7410
  var AgentDriver = class {
7293
7411
  constructor(terminals, timing = {}) {
@@ -7434,12 +7552,12 @@ var AgentDriver = class {
7434
7552
  if (before.draft && !leftover) return { status: "draft", draft: before.draft };
7435
7553
  const sent = leftover ?? text4;
7436
7554
  let taken = false;
7437
- const hooked = new Promise((resolve6) => {
7555
+ const hooked = new Promise((resolve7) => {
7438
7556
  this.waiters.set(name, {
7439
7557
  text: sent,
7440
7558
  taken: () => {
7441
7559
  taken = true;
7442
- resolve6();
7560
+ resolve7();
7443
7561
  }
7444
7562
  });
7445
7563
  });
@@ -7546,9 +7664,9 @@ function readAgyScreen(raw) {
7546
7664
 
7547
7665
  // src/agy-settings.ts
7548
7666
  init_define_HERD_PACKAGE();
7549
- import { lstatSync, mkdirSync as mkdirSync13, realpathSync as realpathSync5, writeFileSync as writeFileSync11 } from "node:fs";
7550
- import { join as join18, sep } from "node:path";
7551
- 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");
7552
7670
  var AGY_HOOK_EVENTS = ["SessionStart", "PreInvocation", "Stop"];
7553
7671
  var agyHookCommand = (event2, herd) => `${herd} hook agy ${event2} || true`;
7554
7672
  function agyHooks(herd) {
@@ -7565,25 +7683,25 @@ function agySettings(herd) {
7565
7683
  return { statusLine: { type: "command", command: `${herd} statusline agy` } };
7566
7684
  }
7567
7685
  function ownFolder(worktree, name) {
7568
- const folder = join18(worktree, name);
7569
- if (lstatSync(folder, { throwIfNoEntry: false })?.isSymbolicLink()) {
7686
+ const folder = join19(worktree, name);
7687
+ if (lstatSync2(folder, { throwIfNoEntry: false })?.isSymbolicLink()) {
7570
7688
  throw new Error(`${name} in the worktree is a link`);
7571
7689
  }
7572
- mkdirSync13(folder, { recursive: true });
7573
- if (!realpathSync5(folder).startsWith(realpathSync5(worktree) + sep)) {
7690
+ mkdirSync14(folder, { recursive: true });
7691
+ if (!realpathSync7(folder).startsWith(realpathSync7(worktree) + sep)) {
7574
7692
  throw new Error(`${name} leads outside the worktree`);
7575
7693
  }
7576
7694
  return folder;
7577
7695
  }
7578
7696
  function writeAgyFiles(worktree, herd) {
7579
7697
  const agents = ownFolder(worktree, ".agents");
7580
- writeFileSync11(join18(agents, "hooks.json"), `${JSON.stringify(agyHooks(herd), null, 2)}
7698
+ writeFileSync11(join19(agents, "hooks.json"), `${JSON.stringify(agyHooks(herd), null, 2)}
7581
7699
  `);
7582
- writeFileSync11(join18(agents, ".gitignore"), "*\n");
7700
+ writeFileSync11(join19(agents, ".gitignore"), "*\n");
7583
7701
  const herdFolder = ownFolder(worktree, ".herd");
7584
- writeFileSync11(join18(herdFolder, ".gitignore"), "*\n");
7702
+ writeFileSync11(join19(herdFolder, ".gitignore"), "*\n");
7585
7703
  writeFileSync11(
7586
- join18(worktree, AGY_SETTINGS_FILE),
7704
+ join19(worktree, AGY_SETTINGS_FILE),
7587
7705
  `${JSON.stringify(agySettings(herd), null, 2)}
7588
7706
  `
7589
7707
  );
@@ -7592,7 +7710,7 @@ function writeAgyFiles(worktree, herd) {
7592
7710
  // src/agy-driver.ts
7593
7711
  var AGY_FLAGS = `--dangerously-skip-permissions --settings ${AGY_SETTINGS_FILE}`;
7594
7712
  var DEFAULT_TIMING2 = { settleMs: 800, pollMs: 150, startMs: 6e4, confirmMs: 3e3 };
7595
- var sleep4 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
7713
+ var sleep4 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
7596
7714
  var EXIT_WAIT_MS2 = 5e3;
7597
7715
  var AgyDriver = class {
7598
7716
  constructor(terminals, timing = {}) {
@@ -7682,12 +7800,12 @@ var AgyDriver = class {
7682
7800
  if (before.draft && !leftover) return { status: "draft", draft: before.draft };
7683
7801
  const sent = leftover ?? text4;
7684
7802
  let taken = false;
7685
- const reported = new Promise((resolve6) => {
7803
+ const reported = new Promise((resolve7) => {
7686
7804
  this.waiters.set(name, {
7687
7805
  text: sent,
7688
7806
  taken: () => {
7689
7807
  taken = true;
7690
- resolve6();
7808
+ resolve7();
7691
7809
  }
7692
7810
  });
7693
7811
  });
@@ -7735,6 +7853,78 @@ var AgyDriver = class {
7735
7853
  }
7736
7854
  };
7737
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
+
7738
7928
  // src/build-connection.ts
7739
7929
  init_define_HERD_PACKAGE();
7740
7930
  init_src();
@@ -7893,28 +8083,28 @@ var BuildConnection = class {
7893
8083
 
7894
8084
  // src/builds.ts
7895
8085
  init_define_HERD_PACKAGE();
7896
- 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";
7897
8087
  import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
7898
8088
  import {
7899
- existsSync as existsSync12,
7900
- mkdirSync as mkdirSync15,
8089
+ existsSync as existsSync15,
8090
+ mkdirSync as mkdirSync17,
7901
8091
  mkdtempSync,
7902
- readdirSync as readdirSync6,
8092
+ readdirSync as readdirSync7,
7903
8093
  readFileSync as readFileSync16,
7904
- realpathSync as realpathSync6,
7905
- rmSync as rmSync12,
8094
+ realpathSync as realpathSync8,
8095
+ rmSync as rmSync13,
7906
8096
  writeFileSync as writeFileSync13
7907
8097
  } from "node:fs";
7908
8098
  import { homedir as homedir6, tmpdir } from "node:os";
7909
- import { join as join20 } from "node:path";
8099
+ import { join as join22 } from "node:path";
7910
8100
  import { promisify as promisify2 } from "node:util";
7911
8101
 
7912
8102
  // src/check-registry.ts
7913
8103
  init_define_HERD_PACKAGE();
7914
- 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";
7915
8105
  import { homedir as homedir5 } from "node:os";
7916
- import { join as join19 } from "node:path";
7917
- 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");
7918
8108
  var RESERVED_MB = 6 * 1024;
7919
8109
  var MIN_MEMORY_MB = 1024;
7920
8110
  var alive3 = (pid) => {
@@ -7926,15 +8116,15 @@ var alive3 = (pid) => {
7926
8116
  }
7927
8117
  };
7928
8118
  function registerCheck(dir, pid, startedAt = Date.now()) {
7929
- mkdirSync14(dir, { recursive: true });
7930
- const file = join19(dir, `${pid}.json`);
8119
+ mkdirSync16(dir, { recursive: true });
8120
+ const file = join21(dir, `${pid}.json`);
7931
8121
  writeFileSync12(file, JSON.stringify({ pid, startedAt }));
7932
- return () => rmSync11(file, { force: true });
8122
+ return () => rmSync12(file, { force: true });
7933
8123
  }
7934
8124
  function runningChecks(dir, isAlive = alive3) {
7935
8125
  let names = [];
7936
8126
  try {
7937
- names = readdirSync5(dir).filter((name) => /^\d+\.json$/.test(name));
8127
+ names = readdirSync6(dir).filter((name) => /^\d+\.json$/.test(name));
7938
8128
  } catch {
7939
8129
  return 0;
7940
8130
  }
@@ -7943,12 +8133,12 @@ function runningChecks(dir, isAlive = alive3) {
7943
8133
  const pid = Number(name.slice(0, -".json".length));
7944
8134
  let entry = null;
7945
8135
  try {
7946
- entry = JSON.parse(readFileSync15(join19(dir, name), "utf8"));
8136
+ entry = JSON.parse(readFileSync15(join21(dir, name), "utf8"));
7947
8137
  } catch {
7948
8138
  continue;
7949
8139
  }
7950
8140
  if (entry?.pid === pid && isAlive(pid)) count += 1;
7951
- else rmSync11(join19(dir, name), { force: true });
8141
+ else rmSync12(join21(dir, name), { force: true });
7952
8142
  }
7953
8143
  return count;
7954
8144
  }
@@ -7956,9 +8146,9 @@ var memoryShareOf = (totalMb, running) => Math.max(MIN_MEMORY_MB, Math.floor((to
7956
8146
 
7957
8147
  // src/builds.ts
7958
8148
  init_self();
7959
- var sandboxStatusPath = () => join20(herdHome(), "builds", "sandbox.json");
8149
+ var sandboxStatusPath = () => join22(herdHome(), "builds", "sandbox.json");
7960
8150
  function writeSandboxStatus(status2, now = Date.now()) {
7961
- mkdirSync15(join20(herdHome(), "builds"), { recursive: true });
8151
+ mkdirSync17(join22(herdHome(), "builds"), { recursive: true });
7962
8152
  writeFileSync13(sandboxStatusPath(), JSON.stringify({ ...status2, at: now }));
7963
8153
  }
7964
8154
  function readSandboxStatus() {
@@ -7982,17 +8172,17 @@ async function storeGit(env, store, ...args) {
7982
8172
  throw new StoreError(gitError(error));
7983
8173
  }
7984
8174
  }
7985
- var real2 = (path) => {
8175
+ var real3 = (path) => {
7986
8176
  try {
7987
- return realpathSync6(path);
8177
+ return realpathSync8(path);
7988
8178
  } catch {
7989
8179
  return path;
7990
8180
  }
7991
8181
  };
7992
8182
  function worktreeOf(dir, store) {
7993
- if (!existsSync12(join20(dir, ".git"))) return false;
8183
+ if (!existsSync15(join22(dir, ".git"))) return false;
7994
8184
  try {
7995
- 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);
7996
8186
  } catch {
7997
8187
  return false;
7998
8188
  }
@@ -8002,23 +8192,23 @@ function cloneFolder(from, to) {
8002
8192
  execFileSync4("cp", [...args, from, to]);
8003
8193
  }
8004
8194
  function moduleFolders(dir) {
8005
- const found = existsSync12(join20(dir, "node_modules")) ? ["node_modules"] : [];
8006
- 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 })) {
8007
8197
  if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
8008
- if (existsSync12(join20(dir, entry.name, "node_modules")))
8009
- found.push(join20(entry.name, "node_modules"));
8198
+ if (existsSync15(join22(dir, entry.name, "node_modules")))
8199
+ found.push(join22(entry.name, "node_modules"));
8010
8200
  }
8011
8201
  }
8012
8202
  return found;
8013
8203
  }
8014
8204
  function lockHash(dir) {
8015
- const lock = readFileSync16(join20(dir, "package-lock.json"));
8205
+ const lock = readFileSync16(join22(dir, "package-lock.json"));
8016
8206
  const major = process.versions.node.split(".")[0];
8017
8207
  return createHash3("sha256").update(lock).update(`${process.platform}-${process.arch}-${major}`).digest("hex").slice(0, 32);
8018
8208
  }
8019
- var npmCi = (dir) => new Promise((resolve6, reject) => {
8209
+ var npmCi = (dir) => new Promise((resolve7, reject) => {
8020
8210
  const network = insideBuildSandbox() ? ["--offline"] : ["--prefer-offline"];
8021
- const child = spawn7(
8211
+ const child = spawn8(
8022
8212
  "npm",
8023
8213
  ["ci", ...network, "--no-audit", "--no-fund", "--no-update-notifier"],
8024
8214
  {
@@ -8029,7 +8219,7 @@ var npmCi = (dir) => new Promise((resolve6, reject) => {
8029
8219
  child.on("error", reject);
8030
8220
  child.on(
8031
8221
  "close",
8032
- (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}`))
8033
8223
  );
8034
8224
  });
8035
8225
  var BuildRunner = class _BuildRunner {
@@ -8039,11 +8229,15 @@ var BuildRunner = class _BuildRunner {
8039
8229
  options;
8040
8230
  running = /* @__PURE__ */ new Map();
8041
8231
  home() {
8042
- return this.options.home ?? join20(herdHome(), "builds");
8232
+ return this.options.home ?? join22(herdHome(), "builds");
8043
8233
  }
8044
- /** Herd's clone of a build's repo in the machine's root, whether or not it's there yet. */
8234
+ /** Where builds' own bare clones are, apart from the root's checkouts. */
8235
+ clonesDir() {
8236
+ return join22(this.home(), "clones");
8237
+ }
8238
+ /** Herd's bare clone of a build's repo, for builds, whether or not it's there yet. */
8045
8239
  storeFor(build) {
8046
- return storePath(this.options.root(), this.source(build));
8240
+ return bareClonePath(this.clonesDir(), this.source(build));
8047
8241
  }
8048
8242
  /** What to clone a build's repo from: a remote the machine knows for it, or its HTTPS URL. */
8049
8243
  source(build) {
@@ -8051,21 +8245,21 @@ var BuildRunner = class _BuildRunner {
8051
8245
  }
8052
8246
  /**
8053
8247
  * The worktree for one of the repo's build slots, made or updated to the build's commit. The repo
8054
- * is cloned into the root first if it isn't there, and fetched otherwise, outside the sandbox and
8055
- * 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.
8056
8251
  */
8057
8252
  async prepare(build, slot) {
8058
- const root = this.options.root();
8059
8253
  const remote = this.source(build);
8060
- const store = storePath(root, remote);
8254
+ const store = bareClonePath(this.clonesDir(), remote);
8061
8255
  const env = this.options.gitEnv?.();
8062
8256
  const name = (repoKey(build.repo) ?? build.repo).replace(/[^A-Za-z0-9._-]+/g, "_");
8063
- const dir = join20(this.home(), name, String(slot));
8064
- mkdirSync15(join20(this.home(), name), { recursive: true });
8257
+ const dir = join22(this.home(), name, String(slot));
8258
+ mkdirSync17(join22(this.home(), name), { recursive: true });
8065
8259
  await withStoreLock(
8066
8260
  store,
8067
8261
  async () => {
8068
- const clone = await ensureClone(root, remote, env);
8262
+ const clone = await ensureBareClone(store, remote, env);
8069
8263
  if (clone.created) this.options.log?.(`Cloned ${build.repo} into ${store}.`);
8070
8264
  if (clone.fetchFailed)
8071
8265
  this.options.log?.(`Couldn't fetch ${build.repo}: ${clone.fetchFailed}`);
@@ -8075,7 +8269,7 @@ var BuildRunner = class _BuildRunner {
8075
8269
  await storeGit(env, store, "fetch", "--quiet", "origin", build.sha);
8076
8270
  }
8077
8271
  if (!worktreeOf(dir, store)) {
8078
- rmSync12(dir, { recursive: true, force: true });
8272
+ rmSync13(dir, { recursive: true, force: true });
8079
8273
  await storeGit(env, store, "worktree", "prune");
8080
8274
  await storeGit(
8081
8275
  env,
@@ -8100,30 +8294,30 @@ var BuildRunner = class _BuildRunner {
8100
8294
  /** Makes `node_modules` match the checkout's lockfile: kept, copied from the cache, or installed. */
8101
8295
  async ensureModules(dir) {
8102
8296
  const wanted = lockHash(dir);
8103
- const marker = join20(dir, "node_modules", ".herd-lock");
8104
- if (existsSync12(marker) && readFileSync16(marker, "utf8") === wanted) return;
8105
- 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);
8106
8300
  for (const folder of moduleFolders(dir))
8107
- rmSync12(join20(dir, folder), { recursive: true, force: true });
8108
- if (existsSync12(join20(cache, ".complete"))) {
8109
- for (const folder of readFileSync16(join20(cache, ".complete"), "utf8").split("\n").filter(Boolean)) {
8110
- 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));
8111
8305
  }
8112
8306
  } else {
8113
8307
  this.options.log?.(`Installing node_modules for lockfile ${wanted.slice(0, 8)}.`);
8114
8308
  await (this.options.install ?? npmCi)(dir);
8115
- const staging = mkdtempSync(join20(this.home(), "node_modules-"));
8309
+ const staging = mkdtempSync(join22(this.home(), "node_modules-"));
8116
8310
  const folders = moduleFolders(dir);
8117
8311
  for (const folder of folders) {
8118
- mkdirSync15(join20(staging, folder, ".."), { recursive: true });
8119
- cloneFolder(join20(dir, folder), join20(staging, folder));
8312
+ mkdirSync17(join22(staging, folder, ".."), { recursive: true });
8313
+ cloneFolder(join22(dir, folder), join22(staging, folder));
8120
8314
  }
8121
- writeFileSync13(join20(staging, ".complete"), folders.join("\n"));
8122
- mkdirSync15(join20(this.home(), "node_modules"), { recursive: true });
8123
- 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 });
8124
8318
  execFileSync4("mv", [staging, cache]);
8125
8319
  }
8126
- mkdirSync15(join20(dir, "node_modules"), { recursive: true });
8320
+ mkdirSync17(join22(dir, "node_modules"), { recursive: true });
8127
8321
  writeFileSync13(marker, wanted);
8128
8322
  }
8129
8323
  /**
@@ -8134,7 +8328,7 @@ var BuildRunner = class _BuildRunner {
8134
8328
  const env = {
8135
8329
  PATH: process.env.PATH ?? "/usr/bin:/bin",
8136
8330
  HOME: home,
8137
- TMPDIR: join20(home, "tmp"),
8331
+ TMPDIR: join22(home, "tmp"),
8138
8332
  LANG: process.env.LANG ?? "en_US.UTF-8",
8139
8333
  HERD_BUILD_ID: build.id,
8140
8334
  HERD_BUILD_SECRET: build.secret,
@@ -8146,7 +8340,7 @@ var BuildRunner = class _BuildRunner {
8146
8340
  // The sandbox's proxy is the only way out, and Node's own requests use it only when asked.
8147
8341
  NODE_USE_ENV_PROXY: "1",
8148
8342
  // Where `srt` points TMPDIR inside the sandbox: the build's own temporary folder.
8149
- CLAUDE_CODE_TMPDIR: join20(home, "tmp")
8343
+ CLAUDE_CODE_TMPDIR: join22(home, "tmp")
8150
8344
  };
8151
8345
  return env;
8152
8346
  }
@@ -8157,8 +8351,8 @@ var BuildRunner = class _BuildRunner {
8157
8351
  /** Runs `scripts/build.mjs` from the build's own commit, in a process group of its own. */
8158
8352
  async run(build, slot) {
8159
8353
  const dir = await this.prepare(build, slot);
8160
- const home = mkdtempSync(join20(this.options.scratch ?? scratchRoot(), "hb-"));
8161
- mkdirSync15(join20(home, "tmp"), { recursive: true });
8354
+ const home = mkdtempSync(join22(this.options.scratch ?? scratchRoot(), "hb-"));
8355
+ mkdirSync17(join22(home, "tmp"), { recursive: true });
8162
8356
  const run4 = randomUUID4();
8163
8357
  const env = {
8164
8358
  ..._BuildRunner.environment(build, home),
@@ -8168,12 +8362,13 @@ var BuildRunner = class _BuildRunner {
8168
8362
  env.HERD_CHECK_CPUS = String(Math.max(1, Math.floor(this.options.cpus())));
8169
8363
  if (this.options.memoryMb)
8170
8364
  env.HERD_CHECK_MEMORY_MB = String(Math.max(1, Math.floor(this.options.memoryMb())));
8171
- let command = [process.execPath, join20(dir, "scripts", "build.mjs")];
8365
+ let command = [process.execPath, join22(dir, "scripts", "build.mjs")];
8172
8366
  let policyPath = null;
8173
8367
  if (this.sandboxed()) {
8174
8368
  const policy = buildPolicy({
8175
8369
  ownerHome: this.options.ownerHome ?? homedir6(),
8176
8370
  root: this.options.root(),
8371
+ convos: convosHome(this.options.root()),
8177
8372
  worktree: dir,
8178
8373
  store: this.storeFor(build),
8179
8374
  worktreeGitDir: git2(dir, "rev-parse", "--path-format=absolute", "--git-dir"),
@@ -8182,8 +8377,8 @@ var BuildRunner = class _BuildRunner {
8182
8377
  main: build.main,
8183
8378
  buildsUrl: build.url
8184
8379
  });
8185
- mkdirSync15(join20(this.home(), "policies"), { recursive: true });
8186
- 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`);
8187
8382
  writeFileSync13(policyPath, JSON.stringify(policy), { mode: 384 });
8188
8383
  env.HERD_BUILD_SANDBOX = describePolicy(policy);
8189
8384
  command = [process.execPath, HERD_BIN, "build-sandbox", policyPath, "--", ...command];
@@ -8191,15 +8386,15 @@ var BuildRunner = class _BuildRunner {
8191
8386
  env.HERD_BUILD_SANDBOX = insideBuildSandbox() ? "Inside the sandbox of the build that started this one." : "";
8192
8387
  }
8193
8388
  const [executable, ...args] = command;
8194
- const child = spawn7(executable, args, { cwd: dir, env, stdio: "ignore", detached: true });
8195
- const done = new Promise((resolve6) => {
8196
- child.on("error", () => resolve6(1));
8197
- 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));
8198
8393
  });
8199
8394
  let markEnded = () => {
8200
8395
  };
8201
- const ended = new Promise((resolve6) => {
8202
- markEnded = resolve6;
8396
+ const ended = new Promise((resolve7) => {
8397
+ markEnded = resolve7;
8203
8398
  });
8204
8399
  const entry = { pid: child.pid ?? 0, done, dir, ended, run: run4 };
8205
8400
  this.running.set(build.id, entry);
@@ -8211,8 +8406,8 @@ var BuildRunner = class _BuildRunner {
8211
8406
  unregister();
8212
8407
  await this.stopBuildProcesses(run4, dir, entry.pid);
8213
8408
  if (this.running.get(build.id) === entry) this.running.delete(build.id);
8214
- rmSync12(home, { recursive: true, force: true });
8215
- if (policyPath) rmSync12(policyPath, { force: true });
8409
+ rmSync13(home, { recursive: true, force: true });
8410
+ if (policyPath) rmSync13(policyPath, { force: true });
8216
8411
  markEnded();
8217
8412
  }
8218
8413
  }
@@ -8232,7 +8427,7 @@ var BuildRunner = class _BuildRunner {
8232
8427
  * It goes only by where they run from, never by a build ID, since the daemon a build's own tests
8233
8428
  * start must never stop the build it runs in.
8234
8429
  */
8235
- static async sweep(home = join20(herdHome(), "builds"), list = listProcesses) {
8430
+ static async sweep(home = join22(herdHome(), "builds"), list = listProcesses) {
8236
8431
  const left = buildProcesses(list(), null, home);
8237
8432
  await stopPids(left);
8238
8433
  return left.length;
@@ -8244,50 +8439,51 @@ var BuildRunner = class _BuildRunner {
8244
8439
  */
8245
8440
  static async probe(options = { buildsUrl: "" }) {
8246
8441
  if (insideBuildSandbox()) return { ok: true };
8247
- const herd = options.home ?? join20(herdHome(), "builds");
8248
- const dir = mkdtempSync(join20(scratchRoot(), "hp-"));
8249
- 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");
8250
8445
  try {
8251
- mkdirSync15(herd, { recursive: true });
8252
- 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));
8253
8448
  writeFileSync13(secret, "probe");
8254
8449
  const policy = buildPolicy({
8255
- ownerHome: join20(dir, "owner"),
8256
- root: join20(dir, "owner"),
8257
- worktree: join20(dir, "work"),
8258
- store: join20(dir, "git"),
8259
- worktreeGitDir: join20(dir, "git"),
8260
- 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"),
8261
8457
  nodePrefix: nodePrefix(),
8262
8458
  main: false,
8263
8459
  buildsUrl: options.buildsUrl || "https://builds.herdingchats.com"
8264
8460
  });
8265
- const policyPath = join20(herd, "probe-policy.json");
8461
+ const policyPath = join22(herd, "probe-policy.json");
8266
8462
  writeFileSync13(policyPath, JSON.stringify(policy), { mode: 384 });
8267
8463
  const script = [
8268
8464
  'const fs = require("node:fs");',
8269
8465
  `try { fs.readFileSync(${JSON.stringify(secret)}); process.exit(10); } catch {}`,
8270
- `fs.writeFileSync(${JSON.stringify(join20(dir, "home", "ok"))}, "ok");`
8466
+ `fs.writeFileSync(${JSON.stringify(join22(dir, "home", "ok"))}, "ok");`
8271
8467
  ].join(" ");
8272
- const child = spawn7(
8468
+ const child = spawn8(
8273
8469
  process.execPath,
8274
8470
  [HERD_BIN, "build-sandbox", policyPath, "--", process.execPath, "-e", script],
8275
8471
  {
8276
- 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") },
8277
8473
  stdio: ["ignore", "ignore", "pipe"]
8278
8474
  }
8279
8475
  );
8280
8476
  let stderr = "";
8281
8477
  child.stderr.on("data", (data) => stderr += data);
8282
8478
  const timer = setTimeout(() => child.kill("SIGKILL"), options.timeoutMs ?? 3e4);
8283
- const status2 = await new Promise((resolve6) => {
8284
- child.on("error", () => resolve6(1));
8285
- 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));
8286
8482
  });
8287
8483
  clearTimeout(timer);
8288
8484
  if (status2 === 10)
8289
8485
  return { ok: false, reason: "the sandbox let a build read the owner's files" };
8290
- if (status2 !== 0 || !existsSync12(join20(dir, "home", "ok"))) {
8486
+ if (status2 !== 0 || !existsSync15(join22(dir, "home", "ok"))) {
8291
8487
  const why = stderr.trim().split("\n").at(-1) || `the probe exited with ${status2}`;
8292
8488
  return { ok: false, reason: why.slice(0, 300) };
8293
8489
  }
@@ -8295,7 +8491,7 @@ var BuildRunner = class _BuildRunner {
8295
8491
  } catch (error) {
8296
8492
  return { ok: false, reason: String(error).slice(0, 300) };
8297
8493
  } finally {
8298
- rmSync12(dir, { recursive: true, force: true });
8494
+ rmSync13(dir, { recursive: true, force: true });
8299
8495
  }
8300
8496
  }
8301
8497
  /** Stops a build's process group and every process it started, however it got away. */
@@ -8304,7 +8500,7 @@ var BuildRunner = class _BuildRunner {
8304
8500
  if (done) {
8305
8501
  const ended = await Promise.race([
8306
8502
  done.then(() => true),
8307
- new Promise((resolve6) => setTimeout(() => resolve6(false), 3e3).unref())
8503
+ new Promise((resolve7) => setTimeout(() => resolve7(false), 3e3).unref())
8308
8504
  ]);
8309
8505
  if (!ended && pid) signal(-pid, "SIGKILL");
8310
8506
  } else if (pid) {
@@ -8324,7 +8520,7 @@ function listProcesses() {
8324
8520
  function listFromProc() {
8325
8521
  let pids = [];
8326
8522
  try {
8327
- pids = readdirSync6("/proc").filter((name) => /^\d+$/.test(name));
8523
+ pids = readdirSync7("/proc").filter((name) => /^\d+$/.test(name));
8328
8524
  } catch {
8329
8525
  return [];
8330
8526
  }
@@ -8364,7 +8560,7 @@ async function stopPids(pids) {
8364
8560
  if (live.length === 0) return;
8365
8561
  const deadline = Date.now() + 3e3;
8366
8562
  while (Date.now() < deadline && live.some((pid) => signal(pid, 0))) {
8367
- await new Promise((resolve6) => setTimeout(resolve6, 100));
8563
+ await new Promise((resolve7) => setTimeout(resolve7, 100));
8368
8564
  }
8369
8565
  for (const pid of live) signal(pid, "SIGKILL");
8370
8566
  }
@@ -8373,32 +8569,32 @@ async function stopPids(pids) {
8373
8569
  init_define_HERD_PACKAGE();
8374
8570
  import { execFile as execFile3 } from "node:child_process";
8375
8571
  function listClaudeAgents(timeoutMs = 1e4, command = "claude") {
8376
- return new Promise((resolve6) => {
8572
+ return new Promise((resolve7) => {
8377
8573
  execFile3(
8378
8574
  command,
8379
8575
  ["agents", "--json", "--all"],
8380
8576
  { env: terminalEnv({}), timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024 },
8381
8577
  (error, stdout) => {
8382
- if (error) return resolve6([]);
8578
+ if (error) return resolve7([]);
8383
8579
  try {
8384
8580
  const rows = JSON.parse(stdout);
8385
- resolve6(Array.isArray(rows) ? rows : []);
8581
+ resolve7(Array.isArray(rows) ? rows : []);
8386
8582
  } catch {
8387
- resolve6([]);
8583
+ resolve7([]);
8388
8584
  }
8389
8585
  }
8390
8586
  );
8391
8587
  });
8392
8588
  }
8393
8589
  function findClaude(shell = process.env.SHELL || "/bin/sh", timeoutMs = 1e4) {
8394
- return new Promise((resolve6) => {
8590
+ return new Promise((resolve7) => {
8395
8591
  execFile3(
8396
8592
  shell,
8397
8593
  ["-ilc", "command -v claude"],
8398
8594
  { env: terminalEnv({}), timeout: timeoutMs },
8399
8595
  (_error, stdout) => {
8400
8596
  const found = String(stdout ?? "").split("\n").map((line2) => line2.trim()).reverse().find((line2) => line2.startsWith("/"));
8401
- resolve6(found ?? null);
8597
+ resolve7(found ?? null);
8402
8598
  }
8403
8599
  );
8404
8600
  });
@@ -8506,7 +8702,7 @@ var DEFAULT_TIMING3 = {
8506
8702
  confirmMs: 8e3,
8507
8703
  pasteMs: 600
8508
8704
  };
8509
- var sleep5 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
8705
+ var sleep5 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
8510
8706
  var EXIT_WAIT_MS3 = 5e3;
8511
8707
  var CodexDriver = class {
8512
8708
  constructor(terminals, timing = {}) {
@@ -8596,12 +8792,12 @@ var CodexDriver = class {
8596
8792
  if (before.draft && !leftover) return { status: "draft", draft: before.draft };
8597
8793
  const sent = leftover ?? text4;
8598
8794
  let taken = false;
8599
- const reported = new Promise((resolve6) => {
8795
+ const reported = new Promise((resolve7) => {
8600
8796
  this.waiters.set(name, {
8601
8797
  text: sent,
8602
8798
  taken: () => {
8603
8799
  taken = true;
8604
- resolve6();
8800
+ resolve7();
8605
8801
  }
8606
8802
  });
8607
8803
  });
@@ -8646,7 +8842,7 @@ var CodexDriver = class {
8646
8842
 
8647
8843
  // src/daemon.ts
8648
8844
  init_define_HERD_PACKAGE();
8649
- import { join as join21 } from "node:path";
8845
+ import { join as join23 } from "node:path";
8650
8846
 
8651
8847
  // src/dispatch-connection.ts
8652
8848
  init_define_HERD_PACKAGE();
@@ -8750,7 +8946,7 @@ var DispatchConnection = class {
8750
8946
 
8751
8947
  // src/daemon.ts
8752
8948
  var CONFIG_POLL_MS = 2e3;
8753
- var daemonSocketPath = () => join21(herdHome(), "daemon.sock");
8949
+ var daemonSocketPath = () => join23(herdHome(), "daemon.sock");
8754
8950
  var Daemon = class {
8755
8951
  constructor(options) {
8756
8952
  this.options = options;
@@ -8832,9 +9028,11 @@ var Daemon = class {
8832
9028
  return this.registering;
8833
9029
  }
8834
9030
  async register() {
8835
- const reposStamp = clonedStamp(this.root());
9031
+ const reposStamp = checkoutsStamp(this.root());
8836
9032
  const gh = this.options.gh?.();
9033
+ const folderUnavailable = this.options.folderUnavailable?.() ?? void 0;
8837
9034
  const facts = {
9035
+ ...folderUnavailable ? { folderUnavailable } : {},
8838
9036
  ...this.options.facts,
8839
9037
  ...this.herdConfig ? { config: this.herdConfig } : {},
8840
9038
  ...this.configError ? { configError: this.configError } : {},
@@ -8844,7 +9042,7 @@ var Daemon = class {
8844
9042
  const response = await this.options.api.fetch("/api/machines", {
8845
9043
  method: "POST",
8846
9044
  headers: { "Content-Type": "application/json" },
8847
- 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())))
8848
9046
  });
8849
9047
  if (!response.ok) {
8850
9048
  const body = await response.json().catch(() => ({}));
@@ -8872,7 +9070,7 @@ var Daemon = class {
8872
9070
  }
8873
9071
  }
8874
9072
  /**
8875
- * 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
8876
9074
  * uses it within seconds.
8877
9075
  */
8878
9076
  async refreshHerdConfig() {
@@ -8882,7 +9080,7 @@ var Daemon = class {
8882
9080
  };
8883
9081
  if (this.refreshing) return false;
8884
9082
  const configChanged = source.stamp() !== this.configStamp;
8885
- if (!configChanged && clonedStamp(this.root()) === this.reposStamp) return false;
9083
+ if (!configChanged && checkoutsStamp(this.root()) === this.reposStamp) return false;
8886
9084
  this.refreshing = true;
8887
9085
  try {
8888
9086
  if (configChanged) await this.readHerdConfig();
@@ -8904,16 +9102,41 @@ var Daemon = class {
8904
9102
  this.connection?.stop();
8905
9103
  await this.local.close();
8906
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
+ }
8907
9120
  async onFrame(frame) {
8908
9121
  if (frame.type === "offer") return this.onOffer(frame.offer);
8909
9122
  if (frame.type === "sign_in") return this.options.onSignIn?.(frame);
8910
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
+ }
8911
9130
  await this.options.work.onMessage(frame.conversationId, frame.delivery);
8912
9131
  this.connection?.acknowledge(frame.conversationId);
8913
9132
  }
8914
9133
  }
8915
9134
  async onOffer(offer) {
8916
- 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);
8917
9140
  const answer2 = decision.accept ? { answer: "accept" } : { answer: "refuse", reason: decision.reason };
8918
9141
  const response = await this.options.api.fetch(
8919
9142
  `/api/machines/${encodeURIComponent(this.config.machineId)}/offers/${encodeURIComponent(offer.id)}`,
@@ -8933,10 +9156,101 @@ var Daemon = class {
8933
9156
  }
8934
9157
  };
8935
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
+
8936
9250
  // src/gh.ts
8937
9251
  init_define_HERD_PACKAGE();
8938
9252
  init_src();
8939
- import { execFile as execFile4, spawn as spawn8 } from "node:child_process";
9253
+ import { execFile as execFile4, spawn as spawn9 } from "node:child_process";
8940
9254
  import { promisify as promisify3 } from "node:util";
8941
9255
  var GH_CHECK_MS = 10 * 60 * 1e3;
8942
9256
  var GH_CODE_WAIT_MS = 30 * 1e3;
@@ -9007,7 +9321,7 @@ function readDeviceCode(output2) {
9007
9321
  return code && url === GITHUB_DEVICE_URL ? { code, url: GITHUB_DEVICE_URL } : null;
9008
9322
  }
9009
9323
  async function signInGh(options) {
9010
- const spawnGh = options.spawnGh ?? ((args) => spawn8("gh", args, {
9324
+ const spawnGh = options.spawnGh ?? ((args) => spawn9("gh", args, {
9011
9325
  env: options.env?.() ?? process.env,
9012
9326
  stdio: ["ignore", "pipe", "pipe"]
9013
9327
  }));
@@ -9016,8 +9330,8 @@ async function signInGh(options) {
9016
9330
  let sent = false;
9017
9331
  let resolveCode = () => {
9018
9332
  };
9019
- const codeShown = new Promise((resolve6) => {
9020
- resolveCode = resolve6;
9333
+ const codeShown = new Promise((resolve7) => {
9334
+ resolveCode = resolve7;
9021
9335
  });
9022
9336
  const read = (chunk) => {
9023
9337
  output2 += chunk.toString();
@@ -9025,9 +9339,9 @@ async function signInGh(options) {
9025
9339
  };
9026
9340
  child.stdout?.on("data", read);
9027
9341
  child.stderr?.on("data", read);
9028
- const exited = new Promise((resolve6) => {
9029
- child.once("close", () => resolve6());
9030
- child.once("error", () => resolve6());
9342
+ const exited = new Promise((resolve7) => {
9343
+ child.once("close", () => resolve7());
9344
+ child.once("error", () => resolve7());
9031
9345
  });
9032
9346
  void exited.then(() => resolveCode(false));
9033
9347
  const codeTimer = setTimeout(() => resolveCode(false), options.codeWaitMs ?? GH_CODE_WAIT_MS);
@@ -9060,23 +9374,23 @@ init_define_HERD_PACKAGE();
9060
9374
  import { execFileSync as execFileSync5 } from "node:child_process";
9061
9375
  import {
9062
9376
  appendFileSync as appendFileSync2,
9063
- existsSync as existsSync13,
9064
- mkdirSync as mkdirSync16,
9377
+ existsSync as existsSync17,
9378
+ mkdirSync as mkdirSync18,
9065
9379
  readFileSync as readFileSync17,
9066
- renameSync as renameSync5,
9067
- writeFileSync as writeFileSync14
9380
+ renameSync as renameSync6,
9381
+ writeFileSync as writeFileSync15
9068
9382
  } from "node:fs";
9069
- import { isAbsolute as isAbsolute3, join as join22 } from "node:path";
9070
- 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");
9071
9385
  var git3 = (cwd, ...args) => execFileSync5("git", ["-C", cwd, ...args], {
9072
9386
  encoding: "utf8",
9073
9387
  stdio: ["ignore", "pipe", "pipe"]
9074
9388
  }).trim();
9075
9389
  var shellQuote2 = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
9076
9390
  function writeWorktreeSettings(worktree, settings, ownCommand) {
9077
- const path = join22(worktree, LOCAL_SETTINGS);
9391
+ const path = join24(worktree, LOCAL_SETTINGS);
9078
9392
  let current = {};
9079
- if (existsSync13(path)) {
9393
+ if (existsSync17(path)) {
9080
9394
  try {
9081
9395
  const parsed = JSON.parse(readFileSync17(path, "utf8"));
9082
9396
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
@@ -9098,11 +9412,11 @@ function writeWorktreeSettings(worktree, settings, ownCommand) {
9098
9412
  }
9099
9413
  next.hooks = hooks;
9100
9414
  }
9101
- mkdirSync16(join22(worktree, ".claude"), { recursive: true });
9415
+ mkdirSync18(join24(worktree, ".claude"), { recursive: true });
9102
9416
  const temporary = `${path}.herd-${process.pid}`;
9103
- writeFileSync14(temporary, `${JSON.stringify(next, null, 2)}
9417
+ writeFileSync15(temporary, `${JSON.stringify(next, null, 2)}
9104
9418
  `);
9105
- renameSync5(temporary, path);
9419
+ renameSync6(temporary, path);
9106
9420
  ignoreLocalSettings(worktree);
9107
9421
  return true;
9108
9422
  }
@@ -9118,12 +9432,12 @@ function ignoreLocalSettings(worktree) {
9118
9432
  } catch {
9119
9433
  return;
9120
9434
  }
9121
- const info = join22(isAbsolute3(common) ? common : join22(worktree, common), "info");
9122
- const exclude = join22(info, "exclude");
9435
+ const info = join24(isAbsolute3(common) ? common : join24(worktree, common), "info");
9436
+ const exclude = join24(info, "exclude");
9123
9437
  const line2 = "/.claude/settings.local.json";
9124
- const existing = existsSync13(exclude) ? readFileSync17(exclude, "utf8") : "";
9438
+ const existing = existsSync17(exclude) ? readFileSync17(exclude, "utf8") : "";
9125
9439
  if (existing.split("\n").includes(line2)) return;
9126
- mkdirSync16(info, { recursive: true });
9440
+ mkdirSync18(info, { recursive: true });
9127
9441
  appendFileSync2(exclude, `${existing && !existing.endsWith("\n") ? "\n" : ""}${line2}
9128
9442
  `);
9129
9443
  }
@@ -9170,7 +9484,7 @@ function loginShellEnv() {
9170
9484
 
9171
9485
  // src/machine-checks.ts
9172
9486
  init_define_HERD_PACKAGE();
9173
- import { existsSync as existsSync14, rmSync as rmSync13 } from "node:fs";
9487
+ import { existsSync as existsSync18, rmSync as rmSync15 } from "node:fs";
9174
9488
  var REQUEST_POLL_MS = 2e3;
9175
9489
  var MachineChecks = class {
9176
9490
  constructor(options) {
@@ -9216,8 +9530,8 @@ var MachineChecks = class {
9216
9530
  /** `friday start` asked for every agent to be activated again. */
9217
9531
  takeRequest() {
9218
9532
  const path = activationRequestPath(this.options.herdHome);
9219
- if (!existsSync14(path)) return;
9220
- rmSync13(path, { force: true });
9533
+ if (!existsSync18(path)) return;
9534
+ rmSync15(path, { force: true });
9221
9535
  this.activateChosen(true);
9222
9536
  }
9223
9537
  start() {
@@ -9265,22 +9579,22 @@ var MachineChecks = class {
9265
9579
 
9266
9580
  // src/terminals.ts
9267
9581
  init_define_HERD_PACKAGE();
9268
- import { spawn as spawn9 } from "node:child_process";
9269
- 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";
9270
9584
  import { connect as connect2 } from "node:net";
9271
- import { join as join23 } from "node:path";
9585
+ import { join as join25 } from "node:path";
9272
9586
  init_self();
9273
9587
  import {
9274
9588
  HOST_PROTOCOL,
9275
9589
  hostVersion
9276
9590
  } from "./terminal-host.mjs";
9277
- var hostsDir = () => join23(herdHome(), "terminals");
9278
- var legacyHostSocketPath = () => join23(herdHome(), "terminals.sock");
9279
- 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`);
9280
9594
  var speaksProtocol = (protocol) => protocol === HOST_PROTOCOL || protocol === HOST_PROTOCOL - 1;
9281
- var sleep6 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
9595
+ var sleep6 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
9282
9596
  function askHost(path, request) {
9283
- return new Promise((resolve6, reject) => {
9597
+ return new Promise((resolve7, reject) => {
9284
9598
  const socket = connect2(path);
9285
9599
  let data = "";
9286
9600
  socket.on("connect", () => socket.write(`${JSON.stringify(request)}
@@ -9289,7 +9603,7 @@ function askHost(path, request) {
9289
9603
  socket.on("end", () => {
9290
9604
  try {
9291
9605
  const reply = JSON.parse(data);
9292
- if (reply.ok) resolve6(reply);
9606
+ if (reply.ok) resolve7(reply);
9293
9607
  else reject(new HerdError(reply.error ?? "The terminal host refused that."));
9294
9608
  } catch {
9295
9609
  reject(new HerdError("The terminal host's answer wasn't readable."));
@@ -9324,10 +9638,10 @@ var HostTerminals = class {
9324
9638
  const dir = hostsDir();
9325
9639
  let names = [];
9326
9640
  try {
9327
- names = readdirSync7(dir).filter((name) => name.endsWith(".sock"));
9641
+ names = readdirSync8(dir).filter((name) => name.endsWith(".sock"));
9328
9642
  } catch {
9329
9643
  }
9330
- const paths = [legacyHostSocketPath(), ...names.map((name) => join23(dir, name))];
9644
+ const paths = [legacyHostSocketPath(), ...names.map((name) => join25(dir, name))];
9331
9645
  const found = [];
9332
9646
  for (const path of paths) {
9333
9647
  try {
@@ -9339,7 +9653,7 @@ var HostTerminals = class {
9339
9653
  speaks: speaksProtocol(hello.protocol)
9340
9654
  });
9341
9655
  } catch (error) {
9342
- if (isDead(error) && path !== legacyHostSocketPath()) rmSync14(path, { force: true });
9656
+ if (isDead(error) && path !== legacyHostSocketPath()) rmSync16(path, { force: true });
9343
9657
  }
9344
9658
  }
9345
9659
  this.hosts = found;
@@ -9360,8 +9674,8 @@ var HostTerminals = class {
9360
9674
  }
9361
9675
  if (this.current()) return;
9362
9676
  const path = currentHostSocketPath(this.version);
9363
- mkdirSync17(hostsDir(), { recursive: true, mode: 448 });
9364
- 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], {
9365
9679
  detached: true,
9366
9680
  stdio: "ignore",
9367
9681
  env: { ...process.env, HERD_HOME: herdHome() }
@@ -9488,7 +9802,7 @@ var HostTerminals = class {
9488
9802
  await sleep6(100);
9489
9803
  if (!await askHost(host.path, { op: "hello" }).catch(() => null)) break;
9490
9804
  }
9491
- rmSync14(host.path, { force: true });
9805
+ rmSync16(host.path, { force: true });
9492
9806
  }
9493
9807
  return true;
9494
9808
  }
@@ -9607,8 +9921,8 @@ var UsageReporter = class {
9607
9921
  // src/workspaces/index.ts
9608
9922
  init_define_HERD_PACKAGE();
9609
9923
  init_src();
9610
- import { mkdirSync as mkdirSync21, readFileSync as readFileSync18, writeFileSync as writeFileSync17 } from "node:fs";
9611
- 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";
9612
9926
 
9613
9927
  // src/workspaces/codex.ts
9614
9928
  init_define_HERD_PACKAGE();
@@ -9620,7 +9934,7 @@ init_define_HERD_PACKAGE();
9620
9934
  init_define_HERD_PACKAGE();
9621
9935
  init_src();
9622
9936
  init_self();
9623
- import { join as join24 } from "node:path";
9937
+ import { join as join26 } from "node:path";
9624
9938
  import { z as z25 } from "zod";
9625
9939
  function hasThread(events) {
9626
9940
  return events.some(
@@ -9655,7 +9969,7 @@ function filesByMessage(events) {
9655
9969
  }
9656
9970
  return files;
9657
9971
  }
9658
- var FILES_FOLDER = join24(".herd", "files");
9972
+ var FILES_FOLDER = join26(".herd", "files");
9659
9973
  function safeFileName(name) {
9660
9974
  const chars = Array.from(name.replace(/[/\\\p{Cc}]/gu, "_").trim());
9661
9975
  while (Buffer.byteLength(chars.join("")) > 200) chars.shift();
@@ -9877,11 +10191,11 @@ async function claimChange(ws, workspace, change) {
9877
10191
  // src/workspaces/delivery.ts
9878
10192
  init_define_HERD_PACKAGE();
9879
10193
  import { randomUUID as randomUUID6 } from "node:crypto";
9880
- import { join as join28 } from "node:path";
10194
+ import { join as join30 } from "node:path";
9881
10195
 
9882
10196
  // src/workspaces/repos.ts
9883
10197
  init_define_HERD_PACKAGE();
9884
- import { join as join25 } from "node:path";
10198
+ import { join as join27 } from "node:path";
9885
10199
  var rootOf = (ws) => ws.options.root?.() ?? ws.options.config().root ?? defaultRoot();
9886
10200
  var gitEnvOf = (ws) => ws.options.gitEnv?.() ?? loginShellEnv();
9887
10201
  function knownRemotes(ws) {
@@ -9933,7 +10247,7 @@ function repoContext(ws, workspace) {
9933
10247
  remote: repo.remote || remotesOf(ws, { ...workspace, worktrees: [repo] })[0] || "",
9934
10248
  branch: repo.branch
9935
10249
  })),
9936
- cloned: clonedRepos(root).map(({ key, remote }) => ({ key, remote })),
10250
+ cloned: checkouts(root).map(({ key, remote }) => ({ key, remote })),
9937
10251
  described: (ws.options.herdConfig?.()?.repos ?? []).map((repo) => ({
9938
10252
  remote: repo.remote,
9939
10253
  key: repoKey(repo.remote),
@@ -9993,7 +10307,7 @@ async function addHints(ws, workspace, hints, handedOff) {
9993
10307
  const added = await addWorktree({
9994
10308
  root,
9995
10309
  remote: cloneSource(hint.remote, knownRemotes(ws)),
9996
- path: join25(workspace.folder, place.name),
10310
+ path: join27(workspace.folder, place.name),
9997
10311
  branch,
9998
10312
  ...handedOff ? { continueFrom: hint.branch ?? branch } : {},
9999
10313
  env: gitEnvOf(ws),
@@ -10013,6 +10327,8 @@ async function addHints(ws, workspace, hints, handedOff) {
10013
10327
  true
10014
10328
  );
10015
10329
  }
10330
+ if (added.copied.length > 0)
10331
+ await ws.note(id2, describeCopied(place.name, added.copied), true);
10016
10332
  const found = reposInFolder(workspace.folder).find((repo) => repo.folder === place.name);
10017
10333
  await recordRepo(ws, id2, {
10018
10334
  folder: place.name,
@@ -10043,14 +10359,14 @@ function handoffRepos(workspace) {
10043
10359
  return workspace.worktrees.map((repo) => ({
10044
10360
  folder: repo.folder,
10045
10361
  branch: repo.branch,
10046
- unpushed: hasUnpushed(join25(workspace.folder, repo.folder))
10362
+ unpushed: hasUnpushed(join27(workspace.folder, repo.folder))
10047
10363
  }));
10048
10364
  }
10049
10365
 
10050
10366
  // src/workspaces/setup.ts
10051
10367
  init_define_HERD_PACKAGE();
10052
- import { chmodSync as chmodSync4, mkdirSync as mkdirSync18, writeFileSync as writeFileSync15 } from "node:fs";
10053
- 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";
10054
10370
 
10055
10371
  // src/hooks.ts
10056
10372
  init_define_HERD_PACKAGE();
@@ -10083,7 +10399,7 @@ function configureClaude(ws, workspace) {
10083
10399
  configureClaudeIn(workspace.folder, (line2) => ws.log(`${workspace.conversationId}: ${line2}`));
10084
10400
  }
10085
10401
  function configureClaudeIn(folder, log) {
10086
- const herd = shellQuote2(join26(herdPathDir(), "herd"));
10402
+ const herd = shellQuote2(join28(herdPathDir(), "herd"));
10087
10403
  try {
10088
10404
  const written = writeWorktreeSettings(
10089
10405
  folder,
@@ -10108,10 +10424,10 @@ function configureClaudeIn(folder, log) {
10108
10424
  }
10109
10425
  }
10110
10426
  function herdPathDir() {
10111
- const dir = join26(herdHome(), "bin");
10112
- mkdirSync18(dir, { recursive: true, mode: 448 });
10113
- const shim = join26(dir, "herd");
10114
- 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
10115
10431
  exec "${process.execPath}" "${herdBin}" "$@"
10116
10432
  `);
10117
10433
  chmodSync4(shim, 493);
@@ -10129,8 +10445,8 @@ async function setUp(ws, conversationId) {
10129
10445
  const hints = ws.offeredRepos.get(conversationId) ?? [];
10130
10446
  ws.offeredRepos.delete(conversationId);
10131
10447
  const handedOff = ws.handedOff.delete(conversationId);
10132
- const folder = conversationFolder(rootOf(ws), conversationId);
10133
- mkdirSync18(folder, { recursive: true });
10448
+ const folder = folderForConversation(rootOf(ws), conversationId);
10449
+ mkdirSync20(folder, { recursive: true });
10134
10450
  const lead = `herd-${conversationPrefix(conversationId)}-1`;
10135
10451
  const secret = ws.options.local().grant({ terminalId: lead, conversationId });
10136
10452
  const workspace = {
@@ -10249,8 +10565,8 @@ init_define_HERD_PACKAGE();
10249
10565
  // src/workspaces/investigation.ts
10250
10566
  init_define_HERD_PACKAGE();
10251
10567
  import { randomUUID as randomUUID5 } from "node:crypto";
10252
- import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync19 } from "node:fs";
10253
- 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";
10254
10570
  function needsInvestigator(ws, workspace, reading) {
10255
10571
  const failedAt = workspace.noticeFailedAt;
10256
10572
  if (workspace.agent === "codex" || workspace.agent === "agy") return false;
@@ -10406,9 +10722,9 @@ async function followInvestigation(ws, workspace) {
10406
10722
  }
10407
10723
  function saveScreen(ws, conversationId, screen, key) {
10408
10724
  try {
10409
- const dir = join27(herdHome(), "dialogs");
10410
- mkdirSync19(dir, { recursive: true });
10411
- 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`);
10412
10728
  appendFileSync3(file, `${screen.join("\n")}
10413
10729
 
10414
10730
  Way out: ${key}
@@ -10612,7 +10928,7 @@ async function deliver(ws, conversationId, delivery) {
10612
10928
  await ensureLeadTerminal(ws, workspace);
10613
10929
  const codex = workspace.agent === "codex";
10614
10930
  if (workspace.agent === "agy") {
10615
- writeAgyFiles(workspace.folder, shellQuote2(join28(herdPathDir(), "herd")));
10931
+ writeAgyFiles(workspace.folder, shellQuote2(join30(herdPathDir(), "herd")));
10616
10932
  } else if (!codex) configureClaude(ws, workspace);
10617
10933
  const ownId = codex || workspace.agent === "agy";
10618
10934
  if (!workspace.started) {
@@ -10746,15 +11062,15 @@ init_define_HERD_PACKAGE();
10746
11062
  init_src();
10747
11063
  import {
10748
11064
  createWriteStream,
10749
- lstatSync as lstatSync2,
10750
- mkdirSync as mkdirSync20,
10751
- realpathSync as realpathSync8,
10752
- renameSync as renameSync6,
10753
- rmSync as rmSync15,
10754
- statSync as statSync8,
10755
- 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
10756
11072
  } from "node:fs";
10757
- 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";
10758
11074
  import { Readable } from "node:stream";
10759
11075
  import { pipeline } from "node:stream/promises";
10760
11076
 
@@ -10767,8 +11083,8 @@ import { readFile } from "node:fs/promises";
10767
11083
  // src/attachments.ts
10768
11084
  init_define_HERD_PACKAGE();
10769
11085
  init_src();
10770
- import { realpathSync as realpathSync7, statSync as statSync7 } from "node:fs";
10771
- 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";
10772
11088
  var TYPES = {
10773
11089
  ".png": "image/png",
10774
11090
  ".jpg": "image/jpeg",
@@ -10798,33 +11114,33 @@ function isInside(root, path) {
10798
11114
  function resolveAttachments(worktree, files) {
10799
11115
  let root;
10800
11116
  try {
10801
- root = realpathSync7(worktree);
11117
+ root = realpathSync9(worktree);
10802
11118
  } catch {
10803
11119
  return { ok: false, error: "This terminal's conversation folder is missing." };
10804
11120
  }
10805
11121
  const resolved = [];
10806
11122
  for (const file of files) {
10807
- let real3;
11123
+ let real4;
10808
11124
  try {
10809
- real3 = realpathSync7(resolve4(worktree, file.path));
11125
+ real4 = realpathSync9(resolve5(worktree, file.path));
10810
11126
  } catch {
10811
11127
  return { ok: false, error: `There's no file at ${file.path}.` };
10812
11128
  }
10813
- if (!isInside(root, real3)) {
11129
+ if (!isInside(root, real4)) {
10814
11130
  return {
10815
11131
  ok: false,
10816
11132
  error: `${file.path} is outside this conversation's folder, so it can't be attached.`
10817
11133
  };
10818
11134
  }
10819
- const stats = statSync7(real3);
11135
+ const stats = statSync8(real4);
10820
11136
  if (!stats.isFile()) return { ok: false, error: `${file.path} isn't a file.` };
10821
11137
  if (stats.size > MAX_FILE_BYTES) {
10822
11138
  return { ok: false, error: `${file.path} is larger than 100 MB, so it can't be attached.` };
10823
11139
  }
10824
11140
  resolved.push({
10825
- path: real3,
11141
+ path: real4,
10826
11142
  name: basename3(file.path),
10827
- type: mediaTypeFor(real3),
11143
+ type: mediaTypeFor(real4),
10828
11144
  size: stats.size,
10829
11145
  ...file.title ? { title: file.title } : {},
10830
11146
  ...file.alt ? { alt: file.alt } : {}
@@ -10982,11 +11298,11 @@ async function permission(ws, workspace, request, gone) {
10982
11298
  }
10983
11299
  if (state.state === "cancelled") return { ok: true, decision: null };
10984
11300
  }
10985
- await new Promise((resolve6) => {
10986
- const timer = setTimeout(resolve6, ws.options.promptPollMs ?? 1500);
11301
+ await new Promise((resolve7) => {
11302
+ const timer = setTimeout(resolve7, ws.options.promptPollMs ?? 1500);
10987
11303
  gone.addEventListener("abort", () => {
10988
11304
  clearTimeout(timer);
10989
- resolve6();
11305
+ resolve7();
10990
11306
  });
10991
11307
  });
10992
11308
  }
@@ -11340,7 +11656,7 @@ async function automations(ws, workspace, request, gone = new AbortController().
11340
11656
  const deadline = Date.now() + (asked.waitMs ?? 9e4);
11341
11657
  const promptPath = `${ws.conversationPath(workspace.conversationId)}/prompts/${encodeURIComponent(promptId ?? "")}`;
11342
11658
  while (promptId && Date.now() < deadline && !gone.aborted) {
11343
- await new Promise((resolve6) => setTimeout(resolve6, ws.options.promptPollMs ?? 1500));
11659
+ await new Promise((resolve7) => setTimeout(resolve7, ws.options.promptPollMs ?? 1500));
11344
11660
  const prompt = await ws.api(promptPath).catch(() => null);
11345
11661
  if (!prompt?.ok) continue;
11346
11662
  const state = await prompt.json();
@@ -11729,34 +12045,34 @@ async function bringIn(ws, workspace, files) {
11729
12045
  return listed;
11730
12046
  }
11731
12047
  async function download(ws, workspace, file) {
11732
- const path = join29(FILES_FOLDER, safeFileName(file.id), safeFileName(file.name));
11733
- const target = join29(workspace.folder, path);
11734
- if (statSync8(target, { throwIfNoEntry: false })?.size === file.size) return path;
11735
- const herd = join29(workspace.folder, ".herd");
11736
- 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()) {
11737
12053
  throw new Error("its folder leads outside the worktree");
11738
12054
  }
11739
- mkdirSync20(dirname11(target), { recursive: true });
11740
- 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)) {
11741
12057
  throw new Error("its folder leads outside the worktree");
11742
12058
  }
11743
- writeFileSync16(join29(herd, ".gitignore"), "*\n");
12059
+ writeFileSync17(join31(herd, ".gitignore"), "*\n");
11744
12060
  const response = await ws.api(
11745
12061
  `${ws.conversationPath(workspace.conversationId)}/files/${encodeURIComponent(file.id)}`
11746
12062
  );
11747
12063
  if (!response.ok || !response.body) throw new Error(`the server answered ${response.status}`);
11748
12064
  const partial = `${target}.partial`;
11749
- rmSync15(partial, { force: true });
12065
+ rmSync17(partial, { force: true });
11750
12066
  try {
11751
12067
  await pipeline(
11752
12068
  Readable.fromWeb(response.body),
11753
12069
  createWriteStream(partial, { flags: "wx" })
11754
12070
  );
11755
- const { size } = statSync8(partial);
12071
+ const { size } = statSync9(partial);
11756
12072
  if (size !== file.size) throw new Error(`it came to ${size} bytes, not ${file.size}`);
11757
- renameSync6(partial, target);
12073
+ renameSync7(partial, target);
11758
12074
  } finally {
11759
- rmSync15(partial, { force: true });
12075
+ rmSync17(partial, { force: true });
11760
12076
  }
11761
12077
  return path;
11762
12078
  }
@@ -12030,7 +12346,7 @@ var Workspaces = class {
12030
12346
  return this.driver;
12031
12347
  }
12032
12348
  // ---- State, kept on disk so a restarted daemon picks up where it was.
12033
- statePath = () => join30(herdHome(), "state.json");
12349
+ statePath = () => join32(herdHome(), "state.json");
12034
12350
  load() {
12035
12351
  try {
12036
12352
  return stateSchema.parse(JSON.parse(readFileSync18(this.statePath(), "utf8"))).workspaces;
@@ -12039,8 +12355,8 @@ var Workspaces = class {
12039
12355
  }
12040
12356
  }
12041
12357
  save(workspaces) {
12042
- mkdirSync21(herdHome(), { recursive: true, mode: 448 });
12043
- 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 });
12044
12360
  }
12045
12361
  get(conversationId) {
12046
12362
  return this.load()[conversationId] ?? null;
@@ -12250,7 +12566,19 @@ async function runDaemon(args) {
12250
12566
  const root = setRoot(defaultRoot()).root;
12251
12567
  console.log(`No root folder is set for this machine, so it works from ${root}.`);
12252
12568
  }
12253
- 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
+ });
12254
12582
  const api = apiUrl();
12255
12583
  const terminals = new HostTerminals({ log: (line2) => console.log(line2) });
12256
12584
  await terminals.ensureHost();
@@ -12261,7 +12589,6 @@ async function runDaemon(args) {
12261
12589
  headers: isLocal(api) ? localSignIn() : { Authorization: `Bearer ${await accessToken()}` }
12262
12590
  })
12263
12591
  };
12264
- let daemon = null;
12265
12592
  let activator = null;
12266
12593
  let checks = null;
12267
12594
  const log = (line2) => console.log(line2);
@@ -12343,7 +12670,7 @@ async function runDaemon(args) {
12343
12670
  env: (secret) => envFor(workspaces, secret),
12344
12671
  // As for a conversation: Claude Code's hooks and status line, or agy's. Codex has none.
12345
12672
  configure: (agent, folder) => {
12346
- if (agent === "agy") writeAgyFiles(folder, shellQuote2(join31(herdPathDir(), "herd")));
12673
+ if (agent === "agy") writeAgyFiles(folder, shellQuote2(join33(herdPathDir(), "herd")));
12347
12674
  else if (agent === "claude-code") configureClaudeIn(folder, log);
12348
12675
  },
12349
12676
  ...shellCommand ? { shellCommand } : {},
@@ -12381,9 +12708,11 @@ async function runDaemon(args) {
12381
12708
  activations: () => activator?.states() ?? {},
12382
12709
  gh: () => checks?.gh ?? null,
12383
12710
  onSignIn: (frame) => void checks?.signIn(frame),
12384
- onConfigChanged: () => checks?.activateChosen()
12711
+ onConfigChanged: () => checks?.activateChosen(),
12712
+ folderUnavailable: () => folders.problem
12385
12713
  });
12386
12714
  await daemon.start();
12715
+ folders.start();
12387
12716
  checks.start();
12388
12717
  usage.start();
12389
12718
  const display = process.env.HERD_DISPLAY === "off" ? null : new (await Promise.resolve().then(() => (init_desk_display(), desk_display_exports))).DeskDisplay({
@@ -12424,9 +12753,11 @@ async function runDaemon(args) {
12424
12753
  headers: { Authorization: `Bearer ${runnerKey.key}` }
12425
12754
  }),
12426
12755
  serviceUrl: runnerKey.url,
12427
- capacity: () => sandbox.ok ? runnerCapacity(daemon?.configuration().config?.builds) : 0,
12428
- // As many as the service's hello takes, to put this runner first for their builds.
12429
- 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),
12430
12761
  runner: new BuildRunner({
12431
12762
  root: () => machineRoot(),
12432
12763
  gitEnv,
@@ -12459,10 +12790,11 @@ async function runDaemon(args) {
12459
12790
  const codexFollower = setInterval(() => {
12460
12791
  void workspaces.followCodex().catch((error) => console.error(`Reading Codex's sessions failed: ${error}`));
12461
12792
  }, 250);
12462
- await new Promise((resolve6) => {
12463
- 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());
12464
12795
  });
12465
12796
  builds?.stop();
12797
+ folders.stop();
12466
12798
  checks.stop();
12467
12799
  activator.stop();
12468
12800
  usage.stop();
@@ -12556,11 +12888,11 @@ function agyAnswer(event2, output2) {
12556
12888
  }
12557
12889
 
12558
12890
  // src/hook-client.ts
12559
- var sleep7 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
12891
+ var sleep7 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
12560
12892
  function within2(promise, ms) {
12561
12893
  let timer;
12562
- const timeout = new Promise((resolve6) => {
12563
- timer = setTimeout(() => resolve6(null), ms);
12894
+ const timeout = new Promise((resolve7) => {
12895
+ timer = setTimeout(() => resolve7(null), ms);
12564
12896
  });
12565
12897
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
12566
12898
  }
@@ -12609,11 +12941,11 @@ async function runAgyHook(event2, stdin, env, options = {}) {
12609
12941
 
12610
12942
  // src/statusline.ts
12611
12943
  init_define_HERD_PACKAGE();
12612
- import { spawn as spawn10 } from "node:child_process";
12944
+ import { spawn as spawn11 } from "node:child_process";
12613
12945
  import { readFileSync as readFileSync21 } from "node:fs";
12614
12946
  import { connect as connect3 } from "node:net";
12615
12947
  import { homedir as homedir7 } from "node:os";
12616
- import { join as join32 } from "node:path";
12948
+ import { join as join34 } from "node:path";
12617
12949
  var MAX_INPUT = 64 * 1024;
12618
12950
  var REPORT_TIMEOUT_MS = 300;
12619
12951
  var OWN_COMMAND_TIMEOUT_MS = 5e3;
@@ -12650,7 +12982,7 @@ function agyStateFrom(input) {
12650
12982
  function ownAgyStatusLine(home = homedir7()) {
12651
12983
  try {
12652
12984
  const settings = JSON.parse(
12653
- readFileSync21(join32(home, ".gemini", "antigravity-cli", "settings.json"), "utf8")
12985
+ readFileSync21(join34(home, ".gemini", "antigravity-cli", "settings.json"), "utf8")
12654
12986
  );
12655
12987
  const command = settings.statusLine?.command;
12656
12988
  if (typeof command !== "string" || !command.trim()) return null;
@@ -12670,25 +13002,25 @@ function commandIn(path) {
12670
13002
  }
12671
13003
  }
12672
13004
  function ownStatusLine(projectDir, home = homedir7()) {
12673
- 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"));
12674
13006
  }
12675
13007
  function runOwnCommand(command, input, cwd, timeoutMs = OWN_COMMAND_TIMEOUT_MS) {
12676
- return new Promise((resolve6) => {
13008
+ return new Promise((resolve7) => {
12677
13009
  let output2 = "";
12678
13010
  let child;
12679
13011
  try {
12680
- child = spawn10("sh", ["-c", command], {
13012
+ child = spawn11("sh", ["-c", command], {
12681
13013
  cwd,
12682
13014
  detached: true,
12683
13015
  stdio: ["pipe", "pipe", "ignore"]
12684
13016
  });
12685
13017
  } catch {
12686
- return resolve6("");
13018
+ return resolve7("");
12687
13019
  }
12688
13020
  const finish = (text4) => {
12689
13021
  clearTimeout(timer);
12690
13022
  child.stdout?.destroy();
12691
- resolve6(text4);
13023
+ resolve7(text4);
12692
13024
  };
12693
13025
  const timer = setTimeout(() => {
12694
13026
  try {
@@ -12706,12 +13038,12 @@ function runOwnCommand(command, input, cwd, timeoutMs = OWN_COMMAND_TIMEOUT_MS)
12706
13038
  });
12707
13039
  }
12708
13040
  function tellDaemon(path, request, timeoutMs = REPORT_TIMEOUT_MS) {
12709
- return new Promise((resolve6) => {
13041
+ return new Promise((resolve7) => {
12710
13042
  const socket = connect3(path);
12711
13043
  const done = () => {
12712
13044
  clearTimeout(timer);
12713
13045
  socket.destroy();
12714
- resolve6();
13046
+ resolve7();
12715
13047
  };
12716
13048
  const timer = setTimeout(done, timeoutMs);
12717
13049
  socket.on("connect", () => socket.write(`${JSON.stringify(request)}
@@ -12963,7 +13295,7 @@ init_define_HERD_PACKAGE();
12963
13295
  import { connect as connect4 } from "node:net";
12964
13296
  var DETACH_KEY = "";
12965
13297
  function attach(name, { stdin, stdout } = { stdin: process.stdin, stdout: process.stdout }, path = currentHostSocketPath()) {
12966
- return new Promise((resolve6, reject) => {
13298
+ return new Promise((resolve7, reject) => {
12967
13299
  const socket = connect4(path);
12968
13300
  const size = () => ({ cols: stdout.columns || 120, rows: stdout.rows || 40 });
12969
13301
  let buffer = "";
@@ -12977,7 +13309,7 @@ function attach(name, { stdin, stdout } = { stdin: process.stdin, stdout: proces
12977
13309
  stdin.pause();
12978
13310
  stdout.off("resize", onResize);
12979
13311
  socket.end();
12980
- resolve6(result);
13312
+ resolve7(result);
12981
13313
  };
12982
13314
  const onInput = (chunk) => {
12983
13315
  const data = chunk.toString("utf8");
@@ -13062,20 +13394,21 @@ async function runTerminalHost(rest) {
13062
13394
 
13063
13395
  // src/commands/repo.ts
13064
13396
  init_define_HERD_PACKAGE();
13065
- import { join as join33 } from "node:path";
13397
+ import { join as join35 } from "node:path";
13066
13398
  import { z as z32 } from "zod";
13067
13399
  var REPO_HELP = `Usage: herd repo <url or owner/name> [folder]
13068
13400
 
13069
13401
  Checks a repo out in this conversation's folder, on a branch named for the conversation, started
13070
- from the freshly fetched default branch, and records it in the conversation. herd clones the repo
13071
- the first time this machine needs it, with your owner's own git credentials, and keeps that clone
13072
- 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
13073
13406
  github.com/owner/name. The folder is the repo's name, or <owner>-<name> when that's taken, unless
13074
13407
  you give one. Running it again for a repo that's already here changes nothing.`;
13075
13408
  var REPOS_HELP = `Usage: herd repos [--json]
13076
13409
 
13077
- Lists the repos in this conversation's folder, the repos herd has cloned on this machine, and the
13078
- 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.`;
13079
13412
  var repoContextSchema = z32.object({
13080
13413
  folder: z32.string(),
13081
13414
  root: z32.string(),
@@ -13149,7 +13482,7 @@ async function runRepo(rest) {
13149
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>.`
13150
13483
  );
13151
13484
  }
13152
- const path = join33(here.folder, place.name);
13485
+ const path = join35(here.folder, place.name);
13153
13486
  const recorded = here.recorded.some((repo) => repo.folder === place.name);
13154
13487
  if (place.existing && recorded) {
13155
13488
  const branch = here.recorded.find((repo) => repo.folder === place.name)?.branch ?? here.branch;
@@ -13191,13 +13524,14 @@ ${error.message}`);
13191
13524
  note = ` The conversation couldn't record it: ${error instanceof Error ? error.message : error}`;
13192
13525
  }
13193
13526
  const lines = [
13194
- 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}.`
13195
13528
  ];
13196
13529
  if (added.fetchFailed) {
13197
13530
  lines.push(
13198
13531
  `Couldn't fetch it, so it started from the last fetched ${added.base}: ${added.fetchFailed}`
13199
13532
  );
13200
13533
  }
13534
+ if (added.copied.length > 0) lines.push(describeCopied(place.name, added.copied));
13201
13535
  if (added.created) lines.push("Its own AGENTS.md or README says how to set it up.");
13202
13536
  console.log(lines.join("\n") + note);
13203
13537
  return 0;
@@ -13209,7 +13543,7 @@ function formatRepos(here) {
13209
13543
  for (const repo of here.recorded)
13210
13544
  lines.push(` ${repo.folder} ${repo.remote} on ${repo.branch}`);
13211
13545
  const cloned = here.cloned.filter((repo) => !inFolder.has(repo.key));
13212
- lines.push("", "Cloned on this machine:");
13546
+ lines.push("", "Checked out in the root:");
13213
13547
  if (cloned.length === 0) lines.push(" none others");
13214
13548
  const about = new Map(here.described.map((repo) => [repo.key, repo.about]));
13215
13549
  for (const repo of cloned) {
@@ -13253,7 +13587,7 @@ async function runRepos(rest) {
13253
13587
  init_define_HERD_PACKAGE();
13254
13588
  init_src();
13255
13589
  import { randomUUID as randomUUID8 } from "node:crypto";
13256
- import { resolve as resolve5 } from "node:path";
13590
+ import { resolve as resolve6 } from "node:path";
13257
13591
  var SEND_HELP = `Usage: herd send <text>
13258
13592
  herd send -- <text that starts with a dash>
13259
13593
  herd send < reply.txt
@@ -13336,7 +13670,7 @@ function withAbsolutePaths(message, cwd = process.cwd()) {
13336
13670
  return {
13337
13671
  ...message,
13338
13672
  files: files.map(
13339
- (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
13340
13674
  )
13341
13675
  };
13342
13676
  }
@@ -13973,7 +14307,7 @@ ${HELP4}`);
13973
14307
  // src/package-entry.ts
13974
14308
  var invoked = (() => {
13975
14309
  try {
13976
- return realpathSync9(process.argv[1] ?? "");
14310
+ return realpathSync11(process.argv[1] ?? "");
13977
14311
  } catch {
13978
14312
  return "";
13979
14313
  }