@herdingchats/friday 0.1.556 → 0.1.563

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 +892 -557
  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.563", gitHead: "055f38c1feb0ea86dac8b463c249d3f7cd782018" };
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
  });
@@ -7485,6 +7603,7 @@ var QUEUED = /\(alt\+m to send immediately\)\s*$|Press up to edit queued message
7485
7603
  var TRUST_QUESTION = /^\s*Do you trust the contents of this project\?/;
7486
7604
  var TRUST_YES = /^\s*(>)?\s*Yes, I trust this folder/;
7487
7605
  var NOT_SIGNED_IN = /You are currently not signed in\./;
7606
+ var LOGIN_METHODS = /^\s*Select login method:/;
7488
7607
  var RESUME_HINT = /^Resume with -c/;
7489
7608
  function promptBox2(lines) {
7490
7609
  for (let bottom = lines.length - 1; bottom >= 2; bottom--) {
@@ -7523,7 +7642,7 @@ function readAgyScreen(raw) {
7523
7642
  trustPrompt: { yesHighlighted: Boolean(yes && TRUST_YES.exec(yes)?.[1]) }
7524
7643
  };
7525
7644
  }
7526
- if (lines.some((line2) => NOT_SIGNED_IN.test(line2))) {
7645
+ if (lines.some((line2) => NOT_SIGNED_IN.test(line2)) && lines.some((line2) => LOGIN_METHODS.test(line2))) {
7527
7646
  return { ...reading, program: "agy", state: "signed_out" };
7528
7647
  }
7529
7648
  const last = [...lines].reverse().find((line2) => line2.trim().length > 0) ?? "";
@@ -7546,9 +7665,9 @@ function readAgyScreen(raw) {
7546
7665
 
7547
7666
  // src/agy-settings.ts
7548
7667
  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");
7668
+ import { lstatSync as lstatSync2, mkdirSync as mkdirSync14, realpathSync as realpathSync7, writeFileSync as writeFileSync11 } from "node:fs";
7669
+ import { join as join19, sep } from "node:path";
7670
+ var AGY_SETTINGS_FILE = join19(".herd", "agy-settings.json");
7552
7671
  var AGY_HOOK_EVENTS = ["SessionStart", "PreInvocation", "Stop"];
7553
7672
  var agyHookCommand = (event2, herd) => `${herd} hook agy ${event2} || true`;
7554
7673
  function agyHooks(herd) {
@@ -7565,25 +7684,25 @@ function agySettings(herd) {
7565
7684
  return { statusLine: { type: "command", command: `${herd} statusline agy` } };
7566
7685
  }
7567
7686
  function ownFolder(worktree, name) {
7568
- const folder = join18(worktree, name);
7569
- if (lstatSync(folder, { throwIfNoEntry: false })?.isSymbolicLink()) {
7687
+ const folder = join19(worktree, name);
7688
+ if (lstatSync2(folder, { throwIfNoEntry: false })?.isSymbolicLink()) {
7570
7689
  throw new Error(`${name} in the worktree is a link`);
7571
7690
  }
7572
- mkdirSync13(folder, { recursive: true });
7573
- if (!realpathSync5(folder).startsWith(realpathSync5(worktree) + sep)) {
7691
+ mkdirSync14(folder, { recursive: true });
7692
+ if (!realpathSync7(folder).startsWith(realpathSync7(worktree) + sep)) {
7574
7693
  throw new Error(`${name} leads outside the worktree`);
7575
7694
  }
7576
7695
  return folder;
7577
7696
  }
7578
7697
  function writeAgyFiles(worktree, herd) {
7579
7698
  const agents = ownFolder(worktree, ".agents");
7580
- writeFileSync11(join18(agents, "hooks.json"), `${JSON.stringify(agyHooks(herd), null, 2)}
7699
+ writeFileSync11(join19(agents, "hooks.json"), `${JSON.stringify(agyHooks(herd), null, 2)}
7581
7700
  `);
7582
- writeFileSync11(join18(agents, ".gitignore"), "*\n");
7701
+ writeFileSync11(join19(agents, ".gitignore"), "*\n");
7583
7702
  const herdFolder = ownFolder(worktree, ".herd");
7584
- writeFileSync11(join18(herdFolder, ".gitignore"), "*\n");
7703
+ writeFileSync11(join19(herdFolder, ".gitignore"), "*\n");
7585
7704
  writeFileSync11(
7586
- join18(worktree, AGY_SETTINGS_FILE),
7705
+ join19(worktree, AGY_SETTINGS_FILE),
7587
7706
  `${JSON.stringify(agySettings(herd), null, 2)}
7588
7707
  `
7589
7708
  );
@@ -7592,7 +7711,7 @@ function writeAgyFiles(worktree, herd) {
7592
7711
  // src/agy-driver.ts
7593
7712
  var AGY_FLAGS = `--dangerously-skip-permissions --settings ${AGY_SETTINGS_FILE}`;
7594
7713
  var DEFAULT_TIMING2 = { settleMs: 800, pollMs: 150, startMs: 6e4, confirmMs: 3e3 };
7595
- var sleep4 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
7714
+ var sleep4 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
7596
7715
  var EXIT_WAIT_MS2 = 5e3;
7597
7716
  var AgyDriver = class {
7598
7717
  constructor(terminals, timing = {}) {
@@ -7682,12 +7801,12 @@ var AgyDriver = class {
7682
7801
  if (before.draft && !leftover) return { status: "draft", draft: before.draft };
7683
7802
  const sent = leftover ?? text4;
7684
7803
  let taken = false;
7685
- const reported = new Promise((resolve6) => {
7804
+ const reported = new Promise((resolve7) => {
7686
7805
  this.waiters.set(name, {
7687
7806
  text: sent,
7688
7807
  taken: () => {
7689
7808
  taken = true;
7690
- resolve6();
7809
+ resolve7();
7691
7810
  }
7692
7811
  });
7693
7812
  });
@@ -7735,6 +7854,78 @@ var AgyDriver = class {
7735
7854
  }
7736
7855
  };
7737
7856
 
7857
+ // src/bare-clones.ts
7858
+ init_define_HERD_PACKAGE();
7859
+ import { randomBytes as randomBytes5 } from "node:crypto";
7860
+ import { existsSync as existsSync14, mkdirSync as mkdirSync15, readdirSync as readdirSync5, renameSync as renameSync5, rmSync as rmSync11, statSync as statSync7 } from "node:fs";
7861
+ import { dirname as dirname12, join as join20 } from "node:path";
7862
+ function bareClonePath(dir, remote) {
7863
+ const key = repoKey(remote);
7864
+ if (!key) throw new StoreError(`${remote} isn't a repo's URL.`);
7865
+ return join20(dir, `${key}.git`);
7866
+ }
7867
+ async function ensureBareClone(store, remote, env) {
7868
+ if (existsSync14(store)) {
7869
+ let fetchFailed = null;
7870
+ try {
7871
+ await git(env, store, "fetch", "--quiet", "--prune", "origin");
7872
+ await git(env, store, "remote", "set-head", "origin", "--auto").catch(() => "");
7873
+ } catch (error) {
7874
+ fetchFailed = error instanceof Error ? error.message : String(error);
7875
+ }
7876
+ return { store, created: false, fetchFailed };
7877
+ }
7878
+ const building = `${store}.tmp-${process.pid}-${randomBytes5(4).toString("hex")}`;
7879
+ mkdirSync15(dirname12(store), { recursive: true });
7880
+ try {
7881
+ await git(env, dirname12(store), "init", "--quiet", "--bare", building);
7882
+ await git(env, building, "remote", "add", "origin", remote);
7883
+ await git(
7884
+ env,
7885
+ building,
7886
+ "config",
7887
+ "remote.origin.fetch",
7888
+ "+refs/heads/*:refs/remotes/origin/*"
7889
+ );
7890
+ await git(env, building, "fetch", "--quiet", "origin");
7891
+ await git(env, building, "remote", "set-head", "origin", "--auto").catch(() => "");
7892
+ renameSync5(building, store);
7893
+ } catch (error) {
7894
+ rmSync11(building, { recursive: true, force: true });
7895
+ throw error instanceof StoreError ? error : new StoreError(gitError(error));
7896
+ }
7897
+ return { store, created: true, fetchFailed: null };
7898
+ }
7899
+ function bareClones(dir) {
7900
+ const found = [];
7901
+ const walk = (at, depth) => {
7902
+ let entries;
7903
+ try {
7904
+ entries = readdirSync5(at);
7905
+ } catch {
7906
+ return;
7907
+ }
7908
+ for (const name of entries.sort()) {
7909
+ const path = join20(at, name);
7910
+ if (name.endsWith(".git")) {
7911
+ try {
7912
+ const remote = gitSync(path, "config", "--get", "remote.origin.url");
7913
+ const key = repoKey(remote);
7914
+ if (key) found.push({ key, remote, store: path });
7915
+ } catch {
7916
+ }
7917
+ } else if (depth < 8 && !name.includes(".tmp-") && !name.endsWith(".lock")) {
7918
+ try {
7919
+ if (statSync7(path).isDirectory()) walk(path, depth + 1);
7920
+ } catch {
7921
+ }
7922
+ }
7923
+ }
7924
+ };
7925
+ walk(dir, 0);
7926
+ return found;
7927
+ }
7928
+
7738
7929
  // src/build-connection.ts
7739
7930
  init_define_HERD_PACKAGE();
7740
7931
  init_src();
@@ -7893,28 +8084,28 @@ var BuildConnection = class {
7893
8084
 
7894
8085
  // src/builds.ts
7895
8086
  init_define_HERD_PACKAGE();
7896
- import { execFile as execFile2, execFileSync as execFileSync4, spawn as spawn7 } from "node:child_process";
8087
+ import { execFile as execFile2, execFileSync as execFileSync4, spawn as spawn8 } from "node:child_process";
7897
8088
  import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
7898
8089
  import {
7899
- existsSync as existsSync12,
7900
- mkdirSync as mkdirSync15,
8090
+ existsSync as existsSync15,
8091
+ mkdirSync as mkdirSync17,
7901
8092
  mkdtempSync,
7902
- readdirSync as readdirSync6,
8093
+ readdirSync as readdirSync7,
7903
8094
  readFileSync as readFileSync16,
7904
- realpathSync as realpathSync6,
7905
- rmSync as rmSync12,
8095
+ realpathSync as realpathSync8,
8096
+ rmSync as rmSync13,
7906
8097
  writeFileSync as writeFileSync13
7907
8098
  } from "node:fs";
7908
8099
  import { homedir as homedir6, tmpdir } from "node:os";
7909
- import { join as join20 } from "node:path";
8100
+ import { join as join22 } from "node:path";
7910
8101
  import { promisify as promisify2 } from "node:util";
7911
8102
 
7912
8103
  // src/check-registry.ts
7913
8104
  init_define_HERD_PACKAGE();
7914
- import { mkdirSync as mkdirSync14, readdirSync as readdirSync5, readFileSync as readFileSync15, rmSync as rmSync11, writeFileSync as writeFileSync12 } from "node:fs";
8105
+ import { mkdirSync as mkdirSync16, readdirSync as readdirSync6, readFileSync as readFileSync15, rmSync as rmSync12, writeFileSync as writeFileSync12 } from "node:fs";
7915
8106
  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");
8107
+ import { join as join21 } from "node:path";
8108
+ var checkRegistryDir = () => process.env.HERDINGCHATS_CHECK_RUNNING ?? join21(homedir5(), ".cache", "herdingchats", "check", "running");
7918
8109
  var RESERVED_MB = 6 * 1024;
7919
8110
  var MIN_MEMORY_MB = 1024;
7920
8111
  var alive3 = (pid) => {
@@ -7926,15 +8117,15 @@ var alive3 = (pid) => {
7926
8117
  }
7927
8118
  };
7928
8119
  function registerCheck(dir, pid, startedAt = Date.now()) {
7929
- mkdirSync14(dir, { recursive: true });
7930
- const file = join19(dir, `${pid}.json`);
8120
+ mkdirSync16(dir, { recursive: true });
8121
+ const file = join21(dir, `${pid}.json`);
7931
8122
  writeFileSync12(file, JSON.stringify({ pid, startedAt }));
7932
- return () => rmSync11(file, { force: true });
8123
+ return () => rmSync12(file, { force: true });
7933
8124
  }
7934
8125
  function runningChecks(dir, isAlive = alive3) {
7935
8126
  let names = [];
7936
8127
  try {
7937
- names = readdirSync5(dir).filter((name) => /^\d+\.json$/.test(name));
8128
+ names = readdirSync6(dir).filter((name) => /^\d+\.json$/.test(name));
7938
8129
  } catch {
7939
8130
  return 0;
7940
8131
  }
@@ -7943,12 +8134,12 @@ function runningChecks(dir, isAlive = alive3) {
7943
8134
  const pid = Number(name.slice(0, -".json".length));
7944
8135
  let entry = null;
7945
8136
  try {
7946
- entry = JSON.parse(readFileSync15(join19(dir, name), "utf8"));
8137
+ entry = JSON.parse(readFileSync15(join21(dir, name), "utf8"));
7947
8138
  } catch {
7948
8139
  continue;
7949
8140
  }
7950
8141
  if (entry?.pid === pid && isAlive(pid)) count += 1;
7951
- else rmSync11(join19(dir, name), { force: true });
8142
+ else rmSync12(join21(dir, name), { force: true });
7952
8143
  }
7953
8144
  return count;
7954
8145
  }
@@ -7956,9 +8147,9 @@ var memoryShareOf = (totalMb, running) => Math.max(MIN_MEMORY_MB, Math.floor((to
7956
8147
 
7957
8148
  // src/builds.ts
7958
8149
  init_self();
7959
- var sandboxStatusPath = () => join20(herdHome(), "builds", "sandbox.json");
8150
+ var sandboxStatusPath = () => join22(herdHome(), "builds", "sandbox.json");
7960
8151
  function writeSandboxStatus(status2, now = Date.now()) {
7961
- mkdirSync15(join20(herdHome(), "builds"), { recursive: true });
8152
+ mkdirSync17(join22(herdHome(), "builds"), { recursive: true });
7962
8153
  writeFileSync13(sandboxStatusPath(), JSON.stringify({ ...status2, at: now }));
7963
8154
  }
7964
8155
  function readSandboxStatus() {
@@ -7982,17 +8173,17 @@ async function storeGit(env, store, ...args) {
7982
8173
  throw new StoreError(gitError(error));
7983
8174
  }
7984
8175
  }
7985
- var real2 = (path) => {
8176
+ var real3 = (path) => {
7986
8177
  try {
7987
- return realpathSync6(path);
8178
+ return realpathSync8(path);
7988
8179
  } catch {
7989
8180
  return path;
7990
8181
  }
7991
8182
  };
7992
8183
  function worktreeOf(dir, store) {
7993
- if (!existsSync12(join20(dir, ".git"))) return false;
8184
+ if (!existsSync15(join22(dir, ".git"))) return false;
7994
8185
  try {
7995
- return real2(git2(dir, "rev-parse", "--path-format=absolute", "--git-common-dir")) === real2(store);
8186
+ return real3(git2(dir, "rev-parse", "--path-format=absolute", "--git-common-dir")) === real3(store);
7996
8187
  } catch {
7997
8188
  return false;
7998
8189
  }
@@ -8002,23 +8193,23 @@ function cloneFolder(from, to) {
8002
8193
  execFileSync4("cp", [...args, from, to]);
8003
8194
  }
8004
8195
  function moduleFolders(dir) {
8005
- const found = existsSync12(join20(dir, "node_modules")) ? ["node_modules"] : [];
8006
- for (const entry of readdirSync6(dir, { withFileTypes: true })) {
8196
+ const found = existsSync15(join22(dir, "node_modules")) ? ["node_modules"] : [];
8197
+ for (const entry of readdirSync7(dir, { withFileTypes: true })) {
8007
8198
  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"));
8199
+ if (existsSync15(join22(dir, entry.name, "node_modules")))
8200
+ found.push(join22(entry.name, "node_modules"));
8010
8201
  }
8011
8202
  }
8012
8203
  return found;
8013
8204
  }
8014
8205
  function lockHash(dir) {
8015
- const lock = readFileSync16(join20(dir, "package-lock.json"));
8206
+ const lock = readFileSync16(join22(dir, "package-lock.json"));
8016
8207
  const major = process.versions.node.split(".")[0];
8017
8208
  return createHash3("sha256").update(lock).update(`${process.platform}-${process.arch}-${major}`).digest("hex").slice(0, 32);
8018
8209
  }
8019
- var npmCi = (dir) => new Promise((resolve6, reject) => {
8210
+ var npmCi = (dir) => new Promise((resolve7, reject) => {
8020
8211
  const network = insideBuildSandbox() ? ["--offline"] : ["--prefer-offline"];
8021
- const child = spawn7(
8212
+ const child = spawn8(
8022
8213
  "npm",
8023
8214
  ["ci", ...network, "--no-audit", "--no-fund", "--no-update-notifier"],
8024
8215
  {
@@ -8029,7 +8220,7 @@ var npmCi = (dir) => new Promise((resolve6, reject) => {
8029
8220
  child.on("error", reject);
8030
8221
  child.on(
8031
8222
  "close",
8032
- (code) => code === 0 ? resolve6() : reject(new Error(`npm ci exited with ${code}`))
8223
+ (code) => code === 0 ? resolve7() : reject(new Error(`npm ci exited with ${code}`))
8033
8224
  );
8034
8225
  });
8035
8226
  var BuildRunner = class _BuildRunner {
@@ -8039,11 +8230,15 @@ var BuildRunner = class _BuildRunner {
8039
8230
  options;
8040
8231
  running = /* @__PURE__ */ new Map();
8041
8232
  home() {
8042
- return this.options.home ?? join20(herdHome(), "builds");
8233
+ return this.options.home ?? join22(herdHome(), "builds");
8043
8234
  }
8044
- /** Herd's clone of a build's repo in the machine's root, whether or not it's there yet. */
8235
+ /** Where builds' own bare clones are, apart from the root's checkouts. */
8236
+ clonesDir() {
8237
+ return join22(this.home(), "clones");
8238
+ }
8239
+ /** Herd's bare clone of a build's repo, for builds, whether or not it's there yet. */
8045
8240
  storeFor(build) {
8046
- return storePath(this.options.root(), this.source(build));
8241
+ return bareClonePath(this.clonesDir(), this.source(build));
8047
8242
  }
8048
8243
  /** What to clone a build's repo from: a remote the machine knows for it, or its HTTPS URL. */
8049
8244
  source(build) {
@@ -8051,21 +8246,21 @@ var BuildRunner = class _BuildRunner {
8051
8246
  }
8052
8247
  /**
8053
8248
  * 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.
8249
+ * is cloned bare into the builds' own folder first if it isn't there, and fetched otherwise,
8250
+ * outside the sandbox and holding the clone's lock. Builds never work from the root's checkouts,
8251
+ * which hold the person's own local files.
8056
8252
  */
8057
8253
  async prepare(build, slot) {
8058
- const root = this.options.root();
8059
8254
  const remote = this.source(build);
8060
- const store = storePath(root, remote);
8255
+ const store = bareClonePath(this.clonesDir(), remote);
8061
8256
  const env = this.options.gitEnv?.();
8062
8257
  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 });
8258
+ const dir = join22(this.home(), name, String(slot));
8259
+ mkdirSync17(join22(this.home(), name), { recursive: true });
8065
8260
  await withStoreLock(
8066
8261
  store,
8067
8262
  async () => {
8068
- const clone = await ensureClone(root, remote, env);
8263
+ const clone = await ensureBareClone(store, remote, env);
8069
8264
  if (clone.created) this.options.log?.(`Cloned ${build.repo} into ${store}.`);
8070
8265
  if (clone.fetchFailed)
8071
8266
  this.options.log?.(`Couldn't fetch ${build.repo}: ${clone.fetchFailed}`);
@@ -8075,7 +8270,7 @@ var BuildRunner = class _BuildRunner {
8075
8270
  await storeGit(env, store, "fetch", "--quiet", "origin", build.sha);
8076
8271
  }
8077
8272
  if (!worktreeOf(dir, store)) {
8078
- rmSync12(dir, { recursive: true, force: true });
8273
+ rmSync13(dir, { recursive: true, force: true });
8079
8274
  await storeGit(env, store, "worktree", "prune");
8080
8275
  await storeGit(
8081
8276
  env,
@@ -8100,30 +8295,30 @@ var BuildRunner = class _BuildRunner {
8100
8295
  /** Makes `node_modules` match the checkout's lockfile: kept, copied from the cache, or installed. */
8101
8296
  async ensureModules(dir) {
8102
8297
  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);
8298
+ const marker = join22(dir, "node_modules", ".herd-lock");
8299
+ if (existsSync15(marker) && readFileSync16(marker, "utf8") === wanted) return;
8300
+ const cache = join22(this.home(), "node_modules", wanted);
8106
8301
  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));
8302
+ rmSync13(join22(dir, folder), { recursive: true, force: true });
8303
+ if (existsSync15(join22(cache, ".complete"))) {
8304
+ for (const folder of readFileSync16(join22(cache, ".complete"), "utf8").split("\n").filter(Boolean)) {
8305
+ cloneFolder(join22(cache, folder), join22(dir, folder));
8111
8306
  }
8112
8307
  } else {
8113
8308
  this.options.log?.(`Installing node_modules for lockfile ${wanted.slice(0, 8)}.`);
8114
8309
  await (this.options.install ?? npmCi)(dir);
8115
- const staging = mkdtempSync(join20(this.home(), "node_modules-"));
8310
+ const staging = mkdtempSync(join22(this.home(), "node_modules-"));
8116
8311
  const folders = moduleFolders(dir);
8117
8312
  for (const folder of folders) {
8118
- mkdirSync15(join20(staging, folder, ".."), { recursive: true });
8119
- cloneFolder(join20(dir, folder), join20(staging, folder));
8313
+ mkdirSync17(join22(staging, folder, ".."), { recursive: true });
8314
+ cloneFolder(join22(dir, folder), join22(staging, folder));
8120
8315
  }
8121
- writeFileSync13(join20(staging, ".complete"), folders.join("\n"));
8122
- mkdirSync15(join20(this.home(), "node_modules"), { recursive: true });
8123
- rmSync12(cache, { recursive: true, force: true });
8316
+ writeFileSync13(join22(staging, ".complete"), folders.join("\n"));
8317
+ mkdirSync17(join22(this.home(), "node_modules"), { recursive: true });
8318
+ rmSync13(cache, { recursive: true, force: true });
8124
8319
  execFileSync4("mv", [staging, cache]);
8125
8320
  }
8126
- mkdirSync15(join20(dir, "node_modules"), { recursive: true });
8321
+ mkdirSync17(join22(dir, "node_modules"), { recursive: true });
8127
8322
  writeFileSync13(marker, wanted);
8128
8323
  }
8129
8324
  /**
@@ -8134,7 +8329,7 @@ var BuildRunner = class _BuildRunner {
8134
8329
  const env = {
8135
8330
  PATH: process.env.PATH ?? "/usr/bin:/bin",
8136
8331
  HOME: home,
8137
- TMPDIR: join20(home, "tmp"),
8332
+ TMPDIR: join22(home, "tmp"),
8138
8333
  LANG: process.env.LANG ?? "en_US.UTF-8",
8139
8334
  HERD_BUILD_ID: build.id,
8140
8335
  HERD_BUILD_SECRET: build.secret,
@@ -8146,7 +8341,7 @@ var BuildRunner = class _BuildRunner {
8146
8341
  // The sandbox's proxy is the only way out, and Node's own requests use it only when asked.
8147
8342
  NODE_USE_ENV_PROXY: "1",
8148
8343
  // Where `srt` points TMPDIR inside the sandbox: the build's own temporary folder.
8149
- CLAUDE_CODE_TMPDIR: join20(home, "tmp")
8344
+ CLAUDE_CODE_TMPDIR: join22(home, "tmp")
8150
8345
  };
8151
8346
  return env;
8152
8347
  }
@@ -8157,8 +8352,8 @@ var BuildRunner = class _BuildRunner {
8157
8352
  /** Runs `scripts/build.mjs` from the build's own commit, in a process group of its own. */
8158
8353
  async run(build, slot) {
8159
8354
  const dir = await this.prepare(build, slot);
8160
- const home = mkdtempSync(join20(this.options.scratch ?? scratchRoot(), "hb-"));
8161
- mkdirSync15(join20(home, "tmp"), { recursive: true });
8355
+ const home = mkdtempSync(join22(this.options.scratch ?? scratchRoot(), "hb-"));
8356
+ mkdirSync17(join22(home, "tmp"), { recursive: true });
8162
8357
  const run4 = randomUUID4();
8163
8358
  const env = {
8164
8359
  ..._BuildRunner.environment(build, home),
@@ -8168,12 +8363,13 @@ var BuildRunner = class _BuildRunner {
8168
8363
  env.HERD_CHECK_CPUS = String(Math.max(1, Math.floor(this.options.cpus())));
8169
8364
  if (this.options.memoryMb)
8170
8365
  env.HERD_CHECK_MEMORY_MB = String(Math.max(1, Math.floor(this.options.memoryMb())));
8171
- let command = [process.execPath, join20(dir, "scripts", "build.mjs")];
8366
+ let command = [process.execPath, join22(dir, "scripts", "build.mjs")];
8172
8367
  let policyPath = null;
8173
8368
  if (this.sandboxed()) {
8174
8369
  const policy = buildPolicy({
8175
8370
  ownerHome: this.options.ownerHome ?? homedir6(),
8176
8371
  root: this.options.root(),
8372
+ convos: convosHome(this.options.root()),
8177
8373
  worktree: dir,
8178
8374
  store: this.storeFor(build),
8179
8375
  worktreeGitDir: git2(dir, "rev-parse", "--path-format=absolute", "--git-dir"),
@@ -8182,8 +8378,8 @@ var BuildRunner = class _BuildRunner {
8182
8378
  main: build.main,
8183
8379
  buildsUrl: build.url
8184
8380
  });
8185
- mkdirSync15(join20(this.home(), "policies"), { recursive: true });
8186
- policyPath = join20(this.home(), "policies", `${build.id}.json`);
8381
+ mkdirSync17(join22(this.home(), "policies"), { recursive: true });
8382
+ policyPath = join22(this.home(), "policies", `${build.id}.json`);
8187
8383
  writeFileSync13(policyPath, JSON.stringify(policy), { mode: 384 });
8188
8384
  env.HERD_BUILD_SANDBOX = describePolicy(policy);
8189
8385
  command = [process.execPath, HERD_BIN, "build-sandbox", policyPath, "--", ...command];
@@ -8191,15 +8387,15 @@ var BuildRunner = class _BuildRunner {
8191
8387
  env.HERD_BUILD_SANDBOX = insideBuildSandbox() ? "Inside the sandbox of the build that started this one." : "";
8192
8388
  }
8193
8389
  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));
8390
+ const child = spawn8(executable, args, { cwd: dir, env, stdio: "ignore", detached: true });
8391
+ const done = new Promise((resolve7) => {
8392
+ child.on("error", () => resolve7(1));
8393
+ child.on("close", (code) => resolve7(code ?? 1));
8198
8394
  });
8199
8395
  let markEnded = () => {
8200
8396
  };
8201
- const ended = new Promise((resolve6) => {
8202
- markEnded = resolve6;
8397
+ const ended = new Promise((resolve7) => {
8398
+ markEnded = resolve7;
8203
8399
  });
8204
8400
  const entry = { pid: child.pid ?? 0, done, dir, ended, run: run4 };
8205
8401
  this.running.set(build.id, entry);
@@ -8211,8 +8407,8 @@ var BuildRunner = class _BuildRunner {
8211
8407
  unregister();
8212
8408
  await this.stopBuildProcesses(run4, dir, entry.pid);
8213
8409
  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 });
8410
+ rmSync13(home, { recursive: true, force: true });
8411
+ if (policyPath) rmSync13(policyPath, { force: true });
8216
8412
  markEnded();
8217
8413
  }
8218
8414
  }
@@ -8232,7 +8428,7 @@ var BuildRunner = class _BuildRunner {
8232
8428
  * It goes only by where they run from, never by a build ID, since the daemon a build's own tests
8233
8429
  * start must never stop the build it runs in.
8234
8430
  */
8235
- static async sweep(home = join20(herdHome(), "builds"), list = listProcesses) {
8431
+ static async sweep(home = join22(herdHome(), "builds"), list = listProcesses) {
8236
8432
  const left = buildProcesses(list(), null, home);
8237
8433
  await stopPids(left);
8238
8434
  return left.length;
@@ -8244,50 +8440,51 @@ var BuildRunner = class _BuildRunner {
8244
8440
  */
8245
8441
  static async probe(options = { buildsUrl: "" }) {
8246
8442
  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");
8443
+ const herd = options.home ?? join22(herdHome(), "builds");
8444
+ const dir = mkdtempSync(join22(scratchRoot(), "hp-"));
8445
+ const secret = join22(dir, "owner", "secret");
8250
8446
  try {
8251
- mkdirSync15(herd, { recursive: true });
8252
- for (const sub of ["work", "git", "home", "owner"]) mkdirSync15(join20(dir, sub));
8447
+ mkdirSync17(herd, { recursive: true });
8448
+ for (const sub of ["work", "git", "home", "owner"]) mkdirSync17(join22(dir, sub));
8253
8449
  writeFileSync13(secret, "probe");
8254
8450
  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"),
8451
+ ownerHome: join22(dir, "owner"),
8452
+ root: join22(dir, "owner"),
8453
+ convos: join22(dir, "owner"),
8454
+ worktree: join22(dir, "work"),
8455
+ store: join22(dir, "git"),
8456
+ worktreeGitDir: join22(dir, "git"),
8457
+ home: join22(dir, "home"),
8261
8458
  nodePrefix: nodePrefix(),
8262
8459
  main: false,
8263
8460
  buildsUrl: options.buildsUrl || "https://builds.herdingchats.com"
8264
8461
  });
8265
- const policyPath = join20(herd, "probe-policy.json");
8462
+ const policyPath = join22(herd, "probe-policy.json");
8266
8463
  writeFileSync13(policyPath, JSON.stringify(policy), { mode: 384 });
8267
8464
  const script = [
8268
8465
  'const fs = require("node:fs");',
8269
8466
  `try { fs.readFileSync(${JSON.stringify(secret)}); process.exit(10); } catch {}`,
8270
- `fs.writeFileSync(${JSON.stringify(join20(dir, "home", "ok"))}, "ok");`
8467
+ `fs.writeFileSync(${JSON.stringify(join22(dir, "home", "ok"))}, "ok");`
8271
8468
  ].join(" ");
8272
- const child = spawn7(
8469
+ const child = spawn8(
8273
8470
  process.execPath,
8274
8471
  [HERD_BIN, "build-sandbox", policyPath, "--", process.execPath, "-e", script],
8275
8472
  {
8276
- env: { PATH: process.env.PATH ?? "/usr/bin:/bin", HOME: join20(dir, "home") },
8473
+ env: { PATH: process.env.PATH ?? "/usr/bin:/bin", HOME: join22(dir, "home") },
8277
8474
  stdio: ["ignore", "ignore", "pipe"]
8278
8475
  }
8279
8476
  );
8280
8477
  let stderr = "";
8281
8478
  child.stderr.on("data", (data) => stderr += data);
8282
8479
  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));
8480
+ const status2 = await new Promise((resolve7) => {
8481
+ child.on("error", () => resolve7(1));
8482
+ child.on("close", (code) => resolve7(code ?? 1));
8286
8483
  });
8287
8484
  clearTimeout(timer);
8288
8485
  if (status2 === 10)
8289
8486
  return { ok: false, reason: "the sandbox let a build read the owner's files" };
8290
- if (status2 !== 0 || !existsSync12(join20(dir, "home", "ok"))) {
8487
+ if (status2 !== 0 || !existsSync15(join22(dir, "home", "ok"))) {
8291
8488
  const why = stderr.trim().split("\n").at(-1) || `the probe exited with ${status2}`;
8292
8489
  return { ok: false, reason: why.slice(0, 300) };
8293
8490
  }
@@ -8295,7 +8492,7 @@ var BuildRunner = class _BuildRunner {
8295
8492
  } catch (error) {
8296
8493
  return { ok: false, reason: String(error).slice(0, 300) };
8297
8494
  } finally {
8298
- rmSync12(dir, { recursive: true, force: true });
8495
+ rmSync13(dir, { recursive: true, force: true });
8299
8496
  }
8300
8497
  }
8301
8498
  /** Stops a build's process group and every process it started, however it got away. */
@@ -8304,7 +8501,7 @@ var BuildRunner = class _BuildRunner {
8304
8501
  if (done) {
8305
8502
  const ended = await Promise.race([
8306
8503
  done.then(() => true),
8307
- new Promise((resolve6) => setTimeout(() => resolve6(false), 3e3).unref())
8504
+ new Promise((resolve7) => setTimeout(() => resolve7(false), 3e3).unref())
8308
8505
  ]);
8309
8506
  if (!ended && pid) signal(-pid, "SIGKILL");
8310
8507
  } else if (pid) {
@@ -8324,7 +8521,7 @@ function listProcesses() {
8324
8521
  function listFromProc() {
8325
8522
  let pids = [];
8326
8523
  try {
8327
- pids = readdirSync6("/proc").filter((name) => /^\d+$/.test(name));
8524
+ pids = readdirSync7("/proc").filter((name) => /^\d+$/.test(name));
8328
8525
  } catch {
8329
8526
  return [];
8330
8527
  }
@@ -8364,7 +8561,7 @@ async function stopPids(pids) {
8364
8561
  if (live.length === 0) return;
8365
8562
  const deadline = Date.now() + 3e3;
8366
8563
  while (Date.now() < deadline && live.some((pid) => signal(pid, 0))) {
8367
- await new Promise((resolve6) => setTimeout(resolve6, 100));
8564
+ await new Promise((resolve7) => setTimeout(resolve7, 100));
8368
8565
  }
8369
8566
  for (const pid of live) signal(pid, "SIGKILL");
8370
8567
  }
@@ -8373,32 +8570,32 @@ async function stopPids(pids) {
8373
8570
  init_define_HERD_PACKAGE();
8374
8571
  import { execFile as execFile3 } from "node:child_process";
8375
8572
  function listClaudeAgents(timeoutMs = 1e4, command = "claude") {
8376
- return new Promise((resolve6) => {
8573
+ return new Promise((resolve7) => {
8377
8574
  execFile3(
8378
8575
  command,
8379
8576
  ["agents", "--json", "--all"],
8380
8577
  { env: terminalEnv({}), timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024 },
8381
8578
  (error, stdout) => {
8382
- if (error) return resolve6([]);
8579
+ if (error) return resolve7([]);
8383
8580
  try {
8384
8581
  const rows = JSON.parse(stdout);
8385
- resolve6(Array.isArray(rows) ? rows : []);
8582
+ resolve7(Array.isArray(rows) ? rows : []);
8386
8583
  } catch {
8387
- resolve6([]);
8584
+ resolve7([]);
8388
8585
  }
8389
8586
  }
8390
8587
  );
8391
8588
  });
8392
8589
  }
8393
8590
  function findClaude(shell = process.env.SHELL || "/bin/sh", timeoutMs = 1e4) {
8394
- return new Promise((resolve6) => {
8591
+ return new Promise((resolve7) => {
8395
8592
  execFile3(
8396
8593
  shell,
8397
8594
  ["-ilc", "command -v claude"],
8398
8595
  { env: terminalEnv({}), timeout: timeoutMs },
8399
8596
  (_error, stdout) => {
8400
8597
  const found = String(stdout ?? "").split("\n").map((line2) => line2.trim()).reverse().find((line2) => line2.startsWith("/"));
8401
- resolve6(found ?? null);
8598
+ resolve7(found ?? null);
8402
8599
  }
8403
8600
  );
8404
8601
  });
@@ -8506,7 +8703,7 @@ var DEFAULT_TIMING3 = {
8506
8703
  confirmMs: 8e3,
8507
8704
  pasteMs: 600
8508
8705
  };
8509
- var sleep5 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
8706
+ var sleep5 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
8510
8707
  var EXIT_WAIT_MS3 = 5e3;
8511
8708
  var CodexDriver = class {
8512
8709
  constructor(terminals, timing = {}) {
@@ -8596,12 +8793,12 @@ var CodexDriver = class {
8596
8793
  if (before.draft && !leftover) return { status: "draft", draft: before.draft };
8597
8794
  const sent = leftover ?? text4;
8598
8795
  let taken = false;
8599
- const reported = new Promise((resolve6) => {
8796
+ const reported = new Promise((resolve7) => {
8600
8797
  this.waiters.set(name, {
8601
8798
  text: sent,
8602
8799
  taken: () => {
8603
8800
  taken = true;
8604
- resolve6();
8801
+ resolve7();
8605
8802
  }
8606
8803
  });
8607
8804
  });
@@ -8646,7 +8843,7 @@ var CodexDriver = class {
8646
8843
 
8647
8844
  // src/daemon.ts
8648
8845
  init_define_HERD_PACKAGE();
8649
- import { join as join21 } from "node:path";
8846
+ import { join as join23 } from "node:path";
8650
8847
 
8651
8848
  // src/dispatch-connection.ts
8652
8849
  init_define_HERD_PACKAGE();
@@ -8750,7 +8947,7 @@ var DispatchConnection = class {
8750
8947
 
8751
8948
  // src/daemon.ts
8752
8949
  var CONFIG_POLL_MS = 2e3;
8753
- var daemonSocketPath = () => join21(herdHome(), "daemon.sock");
8950
+ var daemonSocketPath = () => join23(herdHome(), "daemon.sock");
8754
8951
  var Daemon = class {
8755
8952
  constructor(options) {
8756
8953
  this.options = options;
@@ -8832,9 +9029,11 @@ var Daemon = class {
8832
9029
  return this.registering;
8833
9030
  }
8834
9031
  async register() {
8835
- const reposStamp = clonedStamp(this.root());
9032
+ const reposStamp = checkoutsStamp(this.root());
8836
9033
  const gh = this.options.gh?.();
9034
+ const folderUnavailable = this.options.folderUnavailable?.() ?? void 0;
8837
9035
  const facts = {
9036
+ ...folderUnavailable ? { folderUnavailable } : {},
8838
9037
  ...this.options.facts,
8839
9038
  ...this.herdConfig ? { config: this.herdConfig } : {},
8840
9039
  ...this.configError ? { configError: this.configError } : {},
@@ -8844,7 +9043,7 @@ var Daemon = class {
8844
9043
  const response = await this.options.api.fetch("/api/machines", {
8845
9044
  method: "POST",
8846
9045
  headers: { "Content-Type": "application/json" },
8847
- body: JSON.stringify(registration(this.config, this.clis, facts, clonedRepos(this.root())))
9046
+ body: JSON.stringify(registration(this.config, this.clis, facts, checkouts(this.root())))
8848
9047
  });
8849
9048
  if (!response.ok) {
8850
9049
  const body = await response.json().catch(() => ({}));
@@ -8872,7 +9071,7 @@ var Daemon = class {
8872
9071
  }
8873
9072
  }
8874
9073
  /**
8875
- * Registers again when `herd.config.ts` changed, or a repo was cloned into the root, so routing
9074
+ * Registers again when `herd.config.ts` changed, or a checkout came or went in the root, so routing
8876
9075
  * uses it within seconds.
8877
9076
  */
8878
9077
  async refreshHerdConfig() {
@@ -8882,7 +9081,7 @@ var Daemon = class {
8882
9081
  };
8883
9082
  if (this.refreshing) return false;
8884
9083
  const configChanged = source.stamp() !== this.configStamp;
8885
- if (!configChanged && clonedStamp(this.root()) === this.reposStamp) return false;
9084
+ if (!configChanged && checkoutsStamp(this.root()) === this.reposStamp) return false;
8886
9085
  this.refreshing = true;
8887
9086
  try {
8888
9087
  if (configChanged) await this.readHerdConfig();
@@ -8904,16 +9103,41 @@ var Daemon = class {
8904
9103
  this.connection?.stop();
8905
9104
  await this.local.close();
8906
9105
  }
9106
+ /** Messages held while a folder isn't there, by conversation, delivered once it is. */
9107
+ held = /* @__PURE__ */ new Map();
9108
+ /**
9109
+ * A folder came or went: registers again, so dispatch knows, and once nothing is missing,
9110
+ * delivers what was held meanwhile.
9111
+ */
9112
+ async foldersChanged() {
9113
+ await this.reregister();
9114
+ if (this.options.folderUnavailable?.()) return;
9115
+ for (const [conversationId, delivery] of [...this.held]) {
9116
+ this.held.delete(conversationId);
9117
+ await this.options.work.onMessage(conversationId, delivery);
9118
+ this.connection?.acknowledge(conversationId);
9119
+ }
9120
+ }
8907
9121
  async onFrame(frame) {
8908
9122
  if (frame.type === "offer") return this.onOffer(frame.offer);
8909
9123
  if (frame.type === "sign_in") return this.options.onSignIn?.(frame);
8910
9124
  if (frame.type === "message") {
9125
+ if (this.options.folderUnavailable?.()) {
9126
+ if (this.held.get(frame.conversationId) !== "interrupt") {
9127
+ this.held.set(frame.conversationId, frame.delivery);
9128
+ }
9129
+ return;
9130
+ }
8911
9131
  await this.options.work.onMessage(frame.conversationId, frame.delivery);
8912
9132
  this.connection?.acknowledge(frame.conversationId);
8913
9133
  }
8914
9134
  }
8915
9135
  async onOffer(offer) {
8916
- const decision = await this.options.work.onOffer(offer, this.config);
9136
+ const missing = this.options.folderUnavailable?.();
9137
+ const decision = missing ? {
9138
+ accept: false,
9139
+ reason: `A folder this machine works in isn't there: ${missing.target ? `${missing.path} links to ${missing.target}` : missing.path}.`
9140
+ } : await this.options.work.onOffer(offer, this.config);
8917
9141
  const answer2 = decision.accept ? { answer: "accept" } : { answer: "refuse", reason: decision.reason };
8918
9142
  const response = await this.options.api.fetch(
8919
9143
  `/api/machines/${encodeURIComponent(this.config.machineId)}/offers/${encodeURIComponent(offer.id)}`,
@@ -8933,10 +9157,101 @@ var Daemon = class {
8933
9157
  }
8934
9158
  };
8935
9159
 
9160
+ // src/folders.ts
9161
+ init_define_HERD_PACKAGE();
9162
+ import { existsSync as existsSync16, lstatSync as lstatSync3, readlinkSync as readlinkSync2, rmSync as rmSync14, writeFileSync as writeFileSync14 } from "node:fs";
9163
+ import { dirname as dirname13, resolve as resolve4 } from "node:path";
9164
+ var FOLDER_WAIT_MS = 2 * 60 * 1e3;
9165
+ var FOLDER_CHECK_MS = 30 * 1e3;
9166
+ function linkOf(path) {
9167
+ try {
9168
+ if (!lstatSync3(path).isSymbolicLink()) return null;
9169
+ return resolve4(dirname13(path), readlinkSync2(path));
9170
+ } catch {
9171
+ return null;
9172
+ }
9173
+ }
9174
+ function missingFolder(path, required, { exists = existsSync16, linkTarget = linkOf } = {}) {
9175
+ if (exists(path)) return null;
9176
+ const target = linkTarget(path);
9177
+ if (target) return { path, target };
9178
+ return required ? { path } : null;
9179
+ }
9180
+ function machineFolderProblem(root, convos, checks = {}) {
9181
+ return missingFolder(dirname13(root), true, checks) ?? missingFolder(convos, false, checks);
9182
+ }
9183
+ async function waitForFolders(problem, {
9184
+ timeoutMs = FOLDER_WAIT_MS,
9185
+ pollMs = 2e3,
9186
+ sleep: sleep8 = (ms) => new Promise((done) => setTimeout(done, ms)),
9187
+ now = Date.now,
9188
+ log = (line2) => console.log(line2)
9189
+ } = {}) {
9190
+ let missing = problem();
9191
+ if (!missing) return null;
9192
+ log(`Waiting for ${describeMissing(missing)} to be there, as when its drive mounts.`);
9193
+ const deadline = now() + timeoutMs;
9194
+ while (missing && now() < deadline) {
9195
+ await sleep8(pollMs);
9196
+ missing = problem();
9197
+ }
9198
+ if (!missing) {
9199
+ log("It's there.");
9200
+ return null;
9201
+ }
9202
+ log(
9203
+ `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.`
9204
+ );
9205
+ return missing;
9206
+ }
9207
+ var waitForRoot = (root, options = {}) => waitForFolders(() => missingFolder(dirname13(root), true, options), options);
9208
+ var waitForConvos = (convos, options = {}) => waitForFolders(() => missingFolder(convos, false, options), options);
9209
+ var describeMissing = (missing) => missing.target ? `${missing.path}, which links to ${missing.target},` : missing.path;
9210
+ var FolderWatch = class {
9211
+ constructor(options) {
9212
+ this.options = options;
9213
+ this.current = options.initial;
9214
+ this.write();
9215
+ }
9216
+ options;
9217
+ current;
9218
+ timer = null;
9219
+ /** What's missing now, or null. */
9220
+ get problem() {
9221
+ return this.current;
9222
+ }
9223
+ start() {
9224
+ this.timer = setInterval(() => void this.tick(), this.options.everyMs ?? FOLDER_CHECK_MS);
9225
+ this.timer.unref?.();
9226
+ }
9227
+ stop() {
9228
+ if (this.timer) clearInterval(this.timer);
9229
+ }
9230
+ /** Looks once, and tells `onChange` when what's missing changed. */
9231
+ async tick() {
9232
+ const next = this.options.check();
9233
+ if (JSON.stringify(next) === JSON.stringify(this.current)) return;
9234
+ this.current = next;
9235
+ this.write();
9236
+ const log = this.options.log ?? ((line2) => console.log(line2));
9237
+ log(
9238
+ next ? `Taking no work or builds: ${describeMissing(next)} isn't there.` : "The machine's folders are there again, so it takes work again."
9239
+ );
9240
+ await this.options.onChange(next);
9241
+ }
9242
+ write() {
9243
+ try {
9244
+ if (this.current) writeFileSync14(this.options.statusPath, JSON.stringify(this.current));
9245
+ else rmSync14(this.options.statusPath, { force: true });
9246
+ } catch {
9247
+ }
9248
+ }
9249
+ };
9250
+
8936
9251
  // src/gh.ts
8937
9252
  init_define_HERD_PACKAGE();
8938
9253
  init_src();
8939
- import { execFile as execFile4, spawn as spawn8 } from "node:child_process";
9254
+ import { execFile as execFile4, spawn as spawn9 } from "node:child_process";
8940
9255
  import { promisify as promisify3 } from "node:util";
8941
9256
  var GH_CHECK_MS = 10 * 60 * 1e3;
8942
9257
  var GH_CODE_WAIT_MS = 30 * 1e3;
@@ -9007,7 +9322,7 @@ function readDeviceCode(output2) {
9007
9322
  return code && url === GITHUB_DEVICE_URL ? { code, url: GITHUB_DEVICE_URL } : null;
9008
9323
  }
9009
9324
  async function signInGh(options) {
9010
- const spawnGh = options.spawnGh ?? ((args) => spawn8("gh", args, {
9325
+ const spawnGh = options.spawnGh ?? ((args) => spawn9("gh", args, {
9011
9326
  env: options.env?.() ?? process.env,
9012
9327
  stdio: ["ignore", "pipe", "pipe"]
9013
9328
  }));
@@ -9016,8 +9331,8 @@ async function signInGh(options) {
9016
9331
  let sent = false;
9017
9332
  let resolveCode = () => {
9018
9333
  };
9019
- const codeShown = new Promise((resolve6) => {
9020
- resolveCode = resolve6;
9334
+ const codeShown = new Promise((resolve7) => {
9335
+ resolveCode = resolve7;
9021
9336
  });
9022
9337
  const read = (chunk) => {
9023
9338
  output2 += chunk.toString();
@@ -9025,9 +9340,9 @@ async function signInGh(options) {
9025
9340
  };
9026
9341
  child.stdout?.on("data", read);
9027
9342
  child.stderr?.on("data", read);
9028
- const exited = new Promise((resolve6) => {
9029
- child.once("close", () => resolve6());
9030
- child.once("error", () => resolve6());
9343
+ const exited = new Promise((resolve7) => {
9344
+ child.once("close", () => resolve7());
9345
+ child.once("error", () => resolve7());
9031
9346
  });
9032
9347
  void exited.then(() => resolveCode(false));
9033
9348
  const codeTimer = setTimeout(() => resolveCode(false), options.codeWaitMs ?? GH_CODE_WAIT_MS);
@@ -9060,23 +9375,23 @@ init_define_HERD_PACKAGE();
9060
9375
  import { execFileSync as execFileSync5 } from "node:child_process";
9061
9376
  import {
9062
9377
  appendFileSync as appendFileSync2,
9063
- existsSync as existsSync13,
9064
- mkdirSync as mkdirSync16,
9378
+ existsSync as existsSync17,
9379
+ mkdirSync as mkdirSync18,
9065
9380
  readFileSync as readFileSync17,
9066
- renameSync as renameSync5,
9067
- writeFileSync as writeFileSync14
9381
+ renameSync as renameSync6,
9382
+ writeFileSync as writeFileSync15
9068
9383
  } from "node:fs";
9069
- import { isAbsolute as isAbsolute3, join as join22 } from "node:path";
9070
- var LOCAL_SETTINGS = join22(".claude", "settings.local.json");
9384
+ import { isAbsolute as isAbsolute3, join as join24 } from "node:path";
9385
+ var LOCAL_SETTINGS = join24(".claude", "settings.local.json");
9071
9386
  var git3 = (cwd, ...args) => execFileSync5("git", ["-C", cwd, ...args], {
9072
9387
  encoding: "utf8",
9073
9388
  stdio: ["ignore", "pipe", "pipe"]
9074
9389
  }).trim();
9075
9390
  var shellQuote2 = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
9076
9391
  function writeWorktreeSettings(worktree, settings, ownCommand) {
9077
- const path = join22(worktree, LOCAL_SETTINGS);
9392
+ const path = join24(worktree, LOCAL_SETTINGS);
9078
9393
  let current = {};
9079
- if (existsSync13(path)) {
9394
+ if (existsSync17(path)) {
9080
9395
  try {
9081
9396
  const parsed = JSON.parse(readFileSync17(path, "utf8"));
9082
9397
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
@@ -9098,11 +9413,11 @@ function writeWorktreeSettings(worktree, settings, ownCommand) {
9098
9413
  }
9099
9414
  next.hooks = hooks;
9100
9415
  }
9101
- mkdirSync16(join22(worktree, ".claude"), { recursive: true });
9416
+ mkdirSync18(join24(worktree, ".claude"), { recursive: true });
9102
9417
  const temporary = `${path}.herd-${process.pid}`;
9103
- writeFileSync14(temporary, `${JSON.stringify(next, null, 2)}
9418
+ writeFileSync15(temporary, `${JSON.stringify(next, null, 2)}
9104
9419
  `);
9105
- renameSync5(temporary, path);
9420
+ renameSync6(temporary, path);
9106
9421
  ignoreLocalSettings(worktree);
9107
9422
  return true;
9108
9423
  }
@@ -9118,12 +9433,12 @@ function ignoreLocalSettings(worktree) {
9118
9433
  } catch {
9119
9434
  return;
9120
9435
  }
9121
- const info = join22(isAbsolute3(common) ? common : join22(worktree, common), "info");
9122
- const exclude = join22(info, "exclude");
9436
+ const info = join24(isAbsolute3(common) ? common : join24(worktree, common), "info");
9437
+ const exclude = join24(info, "exclude");
9123
9438
  const line2 = "/.claude/settings.local.json";
9124
- const existing = existsSync13(exclude) ? readFileSync17(exclude, "utf8") : "";
9439
+ const existing = existsSync17(exclude) ? readFileSync17(exclude, "utf8") : "";
9125
9440
  if (existing.split("\n").includes(line2)) return;
9126
- mkdirSync16(info, { recursive: true });
9441
+ mkdirSync18(info, { recursive: true });
9127
9442
  appendFileSync2(exclude, `${existing && !existing.endsWith("\n") ? "\n" : ""}${line2}
9128
9443
  `);
9129
9444
  }
@@ -9170,7 +9485,7 @@ function loginShellEnv() {
9170
9485
 
9171
9486
  // src/machine-checks.ts
9172
9487
  init_define_HERD_PACKAGE();
9173
- import { existsSync as existsSync14, rmSync as rmSync13 } from "node:fs";
9488
+ import { existsSync as existsSync18, rmSync as rmSync15 } from "node:fs";
9174
9489
  var REQUEST_POLL_MS = 2e3;
9175
9490
  var MachineChecks = class {
9176
9491
  constructor(options) {
@@ -9216,8 +9531,8 @@ var MachineChecks = class {
9216
9531
  /** `friday start` asked for every agent to be activated again. */
9217
9532
  takeRequest() {
9218
9533
  const path = activationRequestPath(this.options.herdHome);
9219
- if (!existsSync14(path)) return;
9220
- rmSync13(path, { force: true });
9534
+ if (!existsSync18(path)) return;
9535
+ rmSync15(path, { force: true });
9221
9536
  this.activateChosen(true);
9222
9537
  }
9223
9538
  start() {
@@ -9265,22 +9580,22 @@ var MachineChecks = class {
9265
9580
 
9266
9581
  // src/terminals.ts
9267
9582
  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";
9583
+ import { spawn as spawn10 } from "node:child_process";
9584
+ import { mkdirSync as mkdirSync19, readdirSync as readdirSync8, rmSync as rmSync16 } from "node:fs";
9270
9585
  import { connect as connect2 } from "node:net";
9271
- import { join as join23 } from "node:path";
9586
+ import { join as join25 } from "node:path";
9272
9587
  init_self();
9273
9588
  import {
9274
9589
  HOST_PROTOCOL,
9275
9590
  hostVersion
9276
9591
  } 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`);
9592
+ var hostsDir = () => join25(herdHome(), "terminals");
9593
+ var legacyHostSocketPath = () => join25(herdHome(), "terminals.sock");
9594
+ var currentHostSocketPath = (version = hostVersion()) => join25(hostsDir(), `${version}.sock`);
9280
9595
  var speaksProtocol = (protocol) => protocol === HOST_PROTOCOL || protocol === HOST_PROTOCOL - 1;
9281
- var sleep6 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
9596
+ var sleep6 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
9282
9597
  function askHost(path, request) {
9283
- return new Promise((resolve6, reject) => {
9598
+ return new Promise((resolve7, reject) => {
9284
9599
  const socket = connect2(path);
9285
9600
  let data = "";
9286
9601
  socket.on("connect", () => socket.write(`${JSON.stringify(request)}
@@ -9289,7 +9604,7 @@ function askHost(path, request) {
9289
9604
  socket.on("end", () => {
9290
9605
  try {
9291
9606
  const reply = JSON.parse(data);
9292
- if (reply.ok) resolve6(reply);
9607
+ if (reply.ok) resolve7(reply);
9293
9608
  else reject(new HerdError(reply.error ?? "The terminal host refused that."));
9294
9609
  } catch {
9295
9610
  reject(new HerdError("The terminal host's answer wasn't readable."));
@@ -9324,10 +9639,10 @@ var HostTerminals = class {
9324
9639
  const dir = hostsDir();
9325
9640
  let names = [];
9326
9641
  try {
9327
- names = readdirSync7(dir).filter((name) => name.endsWith(".sock"));
9642
+ names = readdirSync8(dir).filter((name) => name.endsWith(".sock"));
9328
9643
  } catch {
9329
9644
  }
9330
- const paths = [legacyHostSocketPath(), ...names.map((name) => join23(dir, name))];
9645
+ const paths = [legacyHostSocketPath(), ...names.map((name) => join25(dir, name))];
9331
9646
  const found = [];
9332
9647
  for (const path of paths) {
9333
9648
  try {
@@ -9339,7 +9654,7 @@ var HostTerminals = class {
9339
9654
  speaks: speaksProtocol(hello.protocol)
9340
9655
  });
9341
9656
  } catch (error) {
9342
- if (isDead(error) && path !== legacyHostSocketPath()) rmSync14(path, { force: true });
9657
+ if (isDead(error) && path !== legacyHostSocketPath()) rmSync16(path, { force: true });
9343
9658
  }
9344
9659
  }
9345
9660
  this.hosts = found;
@@ -9360,8 +9675,8 @@ var HostTerminals = class {
9360
9675
  }
9361
9676
  if (this.current()) return;
9362
9677
  const path = currentHostSocketPath(this.version);
9363
- mkdirSync17(hostsDir(), { recursive: true, mode: 448 });
9364
- const child = spawn9(process.execPath, [herdBin, "terminal-host", "--socket", path], {
9678
+ mkdirSync19(hostsDir(), { recursive: true, mode: 448 });
9679
+ const child = spawn10(process.execPath, [herdBin, "terminal-host", "--socket", path], {
9365
9680
  detached: true,
9366
9681
  stdio: "ignore",
9367
9682
  env: { ...process.env, HERD_HOME: herdHome() }
@@ -9488,7 +9803,7 @@ var HostTerminals = class {
9488
9803
  await sleep6(100);
9489
9804
  if (!await askHost(host.path, { op: "hello" }).catch(() => null)) break;
9490
9805
  }
9491
- rmSync14(host.path, { force: true });
9806
+ rmSync16(host.path, { force: true });
9492
9807
  }
9493
9808
  return true;
9494
9809
  }
@@ -9607,8 +9922,8 @@ var UsageReporter = class {
9607
9922
  // src/workspaces/index.ts
9608
9923
  init_define_HERD_PACKAGE();
9609
9924
  init_src();
9610
- import { mkdirSync as mkdirSync21, readFileSync as readFileSync18, writeFileSync as writeFileSync17 } from "node:fs";
9611
- import { join as join30 } from "node:path";
9925
+ import { mkdirSync as mkdirSync23, readFileSync as readFileSync18, writeFileSync as writeFileSync18 } from "node:fs";
9926
+ import { join as join32 } from "node:path";
9612
9927
 
9613
9928
  // src/workspaces/codex.ts
9614
9929
  init_define_HERD_PACKAGE();
@@ -9620,7 +9935,7 @@ init_define_HERD_PACKAGE();
9620
9935
  init_define_HERD_PACKAGE();
9621
9936
  init_src();
9622
9937
  init_self();
9623
- import { join as join24 } from "node:path";
9938
+ import { join as join26 } from "node:path";
9624
9939
  import { z as z25 } from "zod";
9625
9940
  function hasThread(events) {
9626
9941
  return events.some(
@@ -9655,7 +9970,7 @@ function filesByMessage(events) {
9655
9970
  }
9656
9971
  return files;
9657
9972
  }
9658
- var FILES_FOLDER = join24(".herd", "files");
9973
+ var FILES_FOLDER = join26(".herd", "files");
9659
9974
  function safeFileName(name) {
9660
9975
  const chars = Array.from(name.replace(/[/\\\p{Cc}]/gu, "_").trim());
9661
9976
  while (Buffer.byteLength(chars.join("")) > 200) chars.shift();
@@ -9877,11 +10192,11 @@ async function claimChange(ws, workspace, change) {
9877
10192
  // src/workspaces/delivery.ts
9878
10193
  init_define_HERD_PACKAGE();
9879
10194
  import { randomUUID as randomUUID6 } from "node:crypto";
9880
- import { join as join28 } from "node:path";
10195
+ import { join as join30 } from "node:path";
9881
10196
 
9882
10197
  // src/workspaces/repos.ts
9883
10198
  init_define_HERD_PACKAGE();
9884
- import { join as join25 } from "node:path";
10199
+ import { join as join27 } from "node:path";
9885
10200
  var rootOf = (ws) => ws.options.root?.() ?? ws.options.config().root ?? defaultRoot();
9886
10201
  var gitEnvOf = (ws) => ws.options.gitEnv?.() ?? loginShellEnv();
9887
10202
  function knownRemotes(ws) {
@@ -9933,7 +10248,7 @@ function repoContext(ws, workspace) {
9933
10248
  remote: repo.remote || remotesOf(ws, { ...workspace, worktrees: [repo] })[0] || "",
9934
10249
  branch: repo.branch
9935
10250
  })),
9936
- cloned: clonedRepos(root).map(({ key, remote }) => ({ key, remote })),
10251
+ cloned: checkouts(root).map(({ key, remote }) => ({ key, remote })),
9937
10252
  described: (ws.options.herdConfig?.()?.repos ?? []).map((repo) => ({
9938
10253
  remote: repo.remote,
9939
10254
  key: repoKey(repo.remote),
@@ -9993,7 +10308,7 @@ async function addHints(ws, workspace, hints, handedOff) {
9993
10308
  const added = await addWorktree({
9994
10309
  root,
9995
10310
  remote: cloneSource(hint.remote, knownRemotes(ws)),
9996
- path: join25(workspace.folder, place.name),
10311
+ path: join27(workspace.folder, place.name),
9997
10312
  branch,
9998
10313
  ...handedOff ? { continueFrom: hint.branch ?? branch } : {},
9999
10314
  env: gitEnvOf(ws),
@@ -10013,6 +10328,8 @@ async function addHints(ws, workspace, hints, handedOff) {
10013
10328
  true
10014
10329
  );
10015
10330
  }
10331
+ if (added.copied.length > 0)
10332
+ await ws.note(id2, describeCopied(place.name, added.copied), true);
10016
10333
  const found = reposInFolder(workspace.folder).find((repo) => repo.folder === place.name);
10017
10334
  await recordRepo(ws, id2, {
10018
10335
  folder: place.name,
@@ -10043,14 +10360,14 @@ function handoffRepos(workspace) {
10043
10360
  return workspace.worktrees.map((repo) => ({
10044
10361
  folder: repo.folder,
10045
10362
  branch: repo.branch,
10046
- unpushed: hasUnpushed(join25(workspace.folder, repo.folder))
10363
+ unpushed: hasUnpushed(join27(workspace.folder, repo.folder))
10047
10364
  }));
10048
10365
  }
10049
10366
 
10050
10367
  // src/workspaces/setup.ts
10051
10368
  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";
10369
+ import { chmodSync as chmodSync4, mkdirSync as mkdirSync20, writeFileSync as writeFileSync16 } from "node:fs";
10370
+ import { join as join28 } from "node:path";
10054
10371
 
10055
10372
  // src/hooks.ts
10056
10373
  init_define_HERD_PACKAGE();
@@ -10083,7 +10400,7 @@ function configureClaude(ws, workspace) {
10083
10400
  configureClaudeIn(workspace.folder, (line2) => ws.log(`${workspace.conversationId}: ${line2}`));
10084
10401
  }
10085
10402
  function configureClaudeIn(folder, log) {
10086
- const herd = shellQuote2(join26(herdPathDir(), "herd"));
10403
+ const herd = shellQuote2(join28(herdPathDir(), "herd"));
10087
10404
  try {
10088
10405
  const written = writeWorktreeSettings(
10089
10406
  folder,
@@ -10108,10 +10425,10 @@ function configureClaudeIn(folder, log) {
10108
10425
  }
10109
10426
  }
10110
10427
  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
10428
+ const dir = join28(herdHome(), "bin");
10429
+ mkdirSync20(dir, { recursive: true, mode: 448 });
10430
+ const shim = join28(dir, "herd");
10431
+ writeFileSync16(shim, `#!/bin/sh
10115
10432
  exec "${process.execPath}" "${herdBin}" "$@"
10116
10433
  `);
10117
10434
  chmodSync4(shim, 493);
@@ -10129,8 +10446,8 @@ async function setUp(ws, conversationId) {
10129
10446
  const hints = ws.offeredRepos.get(conversationId) ?? [];
10130
10447
  ws.offeredRepos.delete(conversationId);
10131
10448
  const handedOff = ws.handedOff.delete(conversationId);
10132
- const folder = conversationFolder(rootOf(ws), conversationId);
10133
- mkdirSync18(folder, { recursive: true });
10449
+ const folder = folderForConversation(rootOf(ws), conversationId);
10450
+ mkdirSync20(folder, { recursive: true });
10134
10451
  const lead = `herd-${conversationPrefix(conversationId)}-1`;
10135
10452
  const secret = ws.options.local().grant({ terminalId: lead, conversationId });
10136
10453
  const workspace = {
@@ -10249,8 +10566,8 @@ init_define_HERD_PACKAGE();
10249
10566
  // src/workspaces/investigation.ts
10250
10567
  init_define_HERD_PACKAGE();
10251
10568
  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";
10569
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync21 } from "node:fs";
10570
+ import { join as join29 } from "node:path";
10254
10571
  function needsInvestigator(ws, workspace, reading) {
10255
10572
  const failedAt = workspace.noticeFailedAt;
10256
10573
  if (workspace.agent === "codex" || workspace.agent === "agy") return false;
@@ -10406,9 +10723,9 @@ async function followInvestigation(ws, workspace) {
10406
10723
  }
10407
10724
  function saveScreen(ws, conversationId, screen, key) {
10408
10725
  try {
10409
- const dir = join27(herdHome(), "dialogs");
10410
- mkdirSync19(dir, { recursive: true });
10411
- const file = join27(dir, `${conversationId}-${Date.now()}.txt`);
10726
+ const dir = join29(herdHome(), "dialogs");
10727
+ mkdirSync21(dir, { recursive: true });
10728
+ const file = join29(dir, `${conversationId}-${Date.now()}.txt`);
10412
10729
  appendFileSync3(file, `${screen.join("\n")}
10413
10730
 
10414
10731
  Way out: ${key}
@@ -10612,7 +10929,7 @@ async function deliver(ws, conversationId, delivery) {
10612
10929
  await ensureLeadTerminal(ws, workspace);
10613
10930
  const codex = workspace.agent === "codex";
10614
10931
  if (workspace.agent === "agy") {
10615
- writeAgyFiles(workspace.folder, shellQuote2(join28(herdPathDir(), "herd")));
10932
+ writeAgyFiles(workspace.folder, shellQuote2(join30(herdPathDir(), "herd")));
10616
10933
  } else if (!codex) configureClaude(ws, workspace);
10617
10934
  const ownId = codex || workspace.agent === "agy";
10618
10935
  if (!workspace.started) {
@@ -10746,15 +11063,15 @@ init_define_HERD_PACKAGE();
10746
11063
  init_src();
10747
11064
  import {
10748
11065
  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
11066
+ lstatSync as lstatSync4,
11067
+ mkdirSync as mkdirSync22,
11068
+ realpathSync as realpathSync10,
11069
+ renameSync as renameSync7,
11070
+ rmSync as rmSync17,
11071
+ statSync as statSync9,
11072
+ writeFileSync as writeFileSync17
10756
11073
  } from "node:fs";
10757
- import { dirname as dirname11, join as join29, sep as sep2 } from "node:path";
11074
+ import { dirname as dirname14, join as join31, sep as sep2 } from "node:path";
10758
11075
  import { Readable } from "node:stream";
10759
11076
  import { pipeline } from "node:stream/promises";
10760
11077
 
@@ -10767,8 +11084,8 @@ import { readFile } from "node:fs/promises";
10767
11084
  // src/attachments.ts
10768
11085
  init_define_HERD_PACKAGE();
10769
11086
  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";
11087
+ import { realpathSync as realpathSync9, statSync as statSync8 } from "node:fs";
11088
+ import { basename as basename3, extname, isAbsolute as isAbsolute4, relative as relative2, resolve as resolve5 } from "node:path";
10772
11089
  var TYPES = {
10773
11090
  ".png": "image/png",
10774
11091
  ".jpg": "image/jpeg",
@@ -10798,33 +11115,33 @@ function isInside(root, path) {
10798
11115
  function resolveAttachments(worktree, files) {
10799
11116
  let root;
10800
11117
  try {
10801
- root = realpathSync7(worktree);
11118
+ root = realpathSync9(worktree);
10802
11119
  } catch {
10803
11120
  return { ok: false, error: "This terminal's conversation folder is missing." };
10804
11121
  }
10805
11122
  const resolved = [];
10806
11123
  for (const file of files) {
10807
- let real3;
11124
+ let real4;
10808
11125
  try {
10809
- real3 = realpathSync7(resolve4(worktree, file.path));
11126
+ real4 = realpathSync9(resolve5(worktree, file.path));
10810
11127
  } catch {
10811
11128
  return { ok: false, error: `There's no file at ${file.path}.` };
10812
11129
  }
10813
- if (!isInside(root, real3)) {
11130
+ if (!isInside(root, real4)) {
10814
11131
  return {
10815
11132
  ok: false,
10816
11133
  error: `${file.path} is outside this conversation's folder, so it can't be attached.`
10817
11134
  };
10818
11135
  }
10819
- const stats = statSync7(real3);
11136
+ const stats = statSync8(real4);
10820
11137
  if (!stats.isFile()) return { ok: false, error: `${file.path} isn't a file.` };
10821
11138
  if (stats.size > MAX_FILE_BYTES) {
10822
11139
  return { ok: false, error: `${file.path} is larger than 100 MB, so it can't be attached.` };
10823
11140
  }
10824
11141
  resolved.push({
10825
- path: real3,
11142
+ path: real4,
10826
11143
  name: basename3(file.path),
10827
- type: mediaTypeFor(real3),
11144
+ type: mediaTypeFor(real4),
10828
11145
  size: stats.size,
10829
11146
  ...file.title ? { title: file.title } : {},
10830
11147
  ...file.alt ? { alt: file.alt } : {}
@@ -10982,11 +11299,11 @@ async function permission(ws, workspace, request, gone) {
10982
11299
  }
10983
11300
  if (state.state === "cancelled") return { ok: true, decision: null };
10984
11301
  }
10985
- await new Promise((resolve6) => {
10986
- const timer = setTimeout(resolve6, ws.options.promptPollMs ?? 1500);
11302
+ await new Promise((resolve7) => {
11303
+ const timer = setTimeout(resolve7, ws.options.promptPollMs ?? 1500);
10987
11304
  gone.addEventListener("abort", () => {
10988
11305
  clearTimeout(timer);
10989
- resolve6();
11306
+ resolve7();
10990
11307
  });
10991
11308
  });
10992
11309
  }
@@ -11340,7 +11657,7 @@ async function automations(ws, workspace, request, gone = new AbortController().
11340
11657
  const deadline = Date.now() + (asked.waitMs ?? 9e4);
11341
11658
  const promptPath = `${ws.conversationPath(workspace.conversationId)}/prompts/${encodeURIComponent(promptId ?? "")}`;
11342
11659
  while (promptId && Date.now() < deadline && !gone.aborted) {
11343
- await new Promise((resolve6) => setTimeout(resolve6, ws.options.promptPollMs ?? 1500));
11660
+ await new Promise((resolve7) => setTimeout(resolve7, ws.options.promptPollMs ?? 1500));
11344
11661
  const prompt = await ws.api(promptPath).catch(() => null);
11345
11662
  if (!prompt?.ok) continue;
11346
11663
  const state = await prompt.json();
@@ -11729,34 +12046,34 @@ async function bringIn(ws, workspace, files) {
11729
12046
  return listed;
11730
12047
  }
11731
12048
  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()) {
12049
+ const path = join31(FILES_FOLDER, safeFileName(file.id), safeFileName(file.name));
12050
+ const target = join31(workspace.folder, path);
12051
+ if (statSync9(target, { throwIfNoEntry: false })?.size === file.size) return path;
12052
+ const herd = join31(workspace.folder, ".herd");
12053
+ if (lstatSync4(herd, { throwIfNoEntry: false })?.isSymbolicLink()) {
11737
12054
  throw new Error("its folder leads outside the worktree");
11738
12055
  }
11739
- mkdirSync20(dirname11(target), { recursive: true });
11740
- if (!realpathSync8(dirname11(target)).startsWith(realpathSync8(workspace.folder) + sep2)) {
12056
+ mkdirSync22(dirname14(target), { recursive: true });
12057
+ if (!realpathSync10(dirname14(target)).startsWith(realpathSync10(workspace.folder) + sep2)) {
11741
12058
  throw new Error("its folder leads outside the worktree");
11742
12059
  }
11743
- writeFileSync16(join29(herd, ".gitignore"), "*\n");
12060
+ writeFileSync17(join31(herd, ".gitignore"), "*\n");
11744
12061
  const response = await ws.api(
11745
12062
  `${ws.conversationPath(workspace.conversationId)}/files/${encodeURIComponent(file.id)}`
11746
12063
  );
11747
12064
  if (!response.ok || !response.body) throw new Error(`the server answered ${response.status}`);
11748
12065
  const partial = `${target}.partial`;
11749
- rmSync15(partial, { force: true });
12066
+ rmSync17(partial, { force: true });
11750
12067
  try {
11751
12068
  await pipeline(
11752
12069
  Readable.fromWeb(response.body),
11753
12070
  createWriteStream(partial, { flags: "wx" })
11754
12071
  );
11755
- const { size } = statSync8(partial);
12072
+ const { size } = statSync9(partial);
11756
12073
  if (size !== file.size) throw new Error(`it came to ${size} bytes, not ${file.size}`);
11757
- renameSync6(partial, target);
12074
+ renameSync7(partial, target);
11758
12075
  } finally {
11759
- rmSync15(partial, { force: true });
12076
+ rmSync17(partial, { force: true });
11760
12077
  }
11761
12078
  return path;
11762
12079
  }
@@ -12030,7 +12347,7 @@ var Workspaces = class {
12030
12347
  return this.driver;
12031
12348
  }
12032
12349
  // ---- State, kept on disk so a restarted daemon picks up where it was.
12033
- statePath = () => join30(herdHome(), "state.json");
12350
+ statePath = () => join32(herdHome(), "state.json");
12034
12351
  load() {
12035
12352
  try {
12036
12353
  return stateSchema.parse(JSON.parse(readFileSync18(this.statePath(), "utf8"))).workspaces;
@@ -12039,8 +12356,8 @@ var Workspaces = class {
12039
12356
  }
12040
12357
  }
12041
12358
  save(workspaces) {
12042
- mkdirSync21(herdHome(), { recursive: true, mode: 448 });
12043
- writeFileSync17(this.statePath(), JSON.stringify({ workspaces }, null, 2), { mode: 384 });
12359
+ mkdirSync23(herdHome(), { recursive: true, mode: 448 });
12360
+ writeFileSync18(this.statePath(), JSON.stringify({ workspaces }, null, 2), { mode: 384 });
12044
12361
  }
12045
12362
  get(conversationId) {
12046
12363
  return this.load()[conversationId] ?? null;
@@ -12250,7 +12567,19 @@ async function runDaemon(args) {
12250
12567
  const root = setRoot(defaultRoot()).root;
12251
12568
  console.log(`No root folder is set for this machine, so it works from ${root}.`);
12252
12569
  }
12253
- await waitForRoot(machineRoot(), { pollMs: Number(process.env.HERD_ROOT_POLL_MS) || 2e3 });
12570
+ const folderTiming = {
12571
+ pollMs: Number(process.env.HERD_ROOT_POLL_MS) || 2e3,
12572
+ ...process.env.HERD_FOLDER_WAIT_MS ? { timeoutMs: Number(process.env.HERD_FOLDER_WAIT_MS) } : {}
12573
+ };
12574
+ const missing = await waitForRoot(machineRoot(), folderTiming) ?? await waitForConvos(convosPath(machineRoot()), folderTiming);
12575
+ let daemon = null;
12576
+ const folders = new FolderWatch({
12577
+ check: () => machineFolderProblem(machineRoot(), convosPath(machineRoot())),
12578
+ initial: missing,
12579
+ statusPath: join33(herdHome(), "folder-unavailable.json"),
12580
+ ...process.env.HERD_FOLDER_CHECK_MS ? { everyMs: Number(process.env.HERD_FOLDER_CHECK_MS) } : {},
12581
+ onChange: () => daemon?.foldersChanged()
12582
+ });
12254
12583
  const api = apiUrl();
12255
12584
  const terminals = new HostTerminals({ log: (line2) => console.log(line2) });
12256
12585
  await terminals.ensureHost();
@@ -12261,7 +12590,6 @@ async function runDaemon(args) {
12261
12590
  headers: isLocal(api) ? localSignIn() : { Authorization: `Bearer ${await accessToken()}` }
12262
12591
  })
12263
12592
  };
12264
- let daemon = null;
12265
12593
  let activator = null;
12266
12594
  let checks = null;
12267
12595
  const log = (line2) => console.log(line2);
@@ -12343,7 +12671,7 @@ async function runDaemon(args) {
12343
12671
  env: (secret) => envFor(workspaces, secret),
12344
12672
  // As for a conversation: Claude Code's hooks and status line, or agy's. Codex has none.
12345
12673
  configure: (agent, folder) => {
12346
- if (agent === "agy") writeAgyFiles(folder, shellQuote2(join31(herdPathDir(), "herd")));
12674
+ if (agent === "agy") writeAgyFiles(folder, shellQuote2(join33(herdPathDir(), "herd")));
12347
12675
  else if (agent === "claude-code") configureClaudeIn(folder, log);
12348
12676
  },
12349
12677
  ...shellCommand ? { shellCommand } : {},
@@ -12381,9 +12709,11 @@ async function runDaemon(args) {
12381
12709
  activations: () => activator?.states() ?? {},
12382
12710
  gh: () => checks?.gh ?? null,
12383
12711
  onSignIn: (frame) => void checks?.signIn(frame),
12384
- onConfigChanged: () => checks?.activateChosen()
12712
+ onConfigChanged: () => checks?.activateChosen(),
12713
+ folderUnavailable: () => folders.problem
12385
12714
  });
12386
12715
  await daemon.start();
12716
+ folders.start();
12387
12717
  checks.start();
12388
12718
  usage.start();
12389
12719
  const display = process.env.HERD_DISPLAY === "off" ? null : new (await Promise.resolve().then(() => (init_desk_display(), desk_display_exports))).DeskDisplay({
@@ -12424,9 +12754,11 @@ async function runDaemon(args) {
12424
12754
  headers: { Authorization: `Bearer ${runnerKey.key}` }
12425
12755
  }),
12426
12756
  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),
12757
+ // None while a folder it works in isn't there, as for its conversations.
12758
+ capacity: () => sandbox.ok && !folders.problem ? runnerCapacity(daemon?.configuration().config?.builds) : 0,
12759
+ // The repos builds have cloned here, as many as the service's hello takes, to put this runner
12760
+ // first for their builds.
12761
+ repos: () => bareClones(join33(herdHome(), "builds", "clones")).map((repo) => repo.key).filter((key) => key.length <= 300).slice(0, 100),
12430
12762
  runner: new BuildRunner({
12431
12763
  root: () => machineRoot(),
12432
12764
  gitEnv,
@@ -12459,10 +12791,11 @@ async function runDaemon(args) {
12459
12791
  const codexFollower = setInterval(() => {
12460
12792
  void workspaces.followCodex().catch((error) => console.error(`Reading Codex's sessions failed: ${error}`));
12461
12793
  }, 250);
12462
- await new Promise((resolve6) => {
12463
- for (const signal2 of ["SIGINT", "SIGTERM"]) process.once(signal2, () => resolve6());
12794
+ await new Promise((resolve7) => {
12795
+ for (const signal2 of ["SIGINT", "SIGTERM"]) process.once(signal2, () => resolve7());
12464
12796
  });
12465
12797
  builds?.stop();
12798
+ folders.stop();
12466
12799
  checks.stop();
12467
12800
  activator.stop();
12468
12801
  usage.stop();
@@ -12556,11 +12889,11 @@ function agyAnswer(event2, output2) {
12556
12889
  }
12557
12890
 
12558
12891
  // src/hook-client.ts
12559
- var sleep7 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
12892
+ var sleep7 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
12560
12893
  function within2(promise, ms) {
12561
12894
  let timer;
12562
- const timeout = new Promise((resolve6) => {
12563
- timer = setTimeout(() => resolve6(null), ms);
12895
+ const timeout = new Promise((resolve7) => {
12896
+ timer = setTimeout(() => resolve7(null), ms);
12564
12897
  });
12565
12898
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
12566
12899
  }
@@ -12609,11 +12942,11 @@ async function runAgyHook(event2, stdin, env, options = {}) {
12609
12942
 
12610
12943
  // src/statusline.ts
12611
12944
  init_define_HERD_PACKAGE();
12612
- import { spawn as spawn10 } from "node:child_process";
12945
+ import { spawn as spawn11 } from "node:child_process";
12613
12946
  import { readFileSync as readFileSync21 } from "node:fs";
12614
12947
  import { connect as connect3 } from "node:net";
12615
12948
  import { homedir as homedir7 } from "node:os";
12616
- import { join as join32 } from "node:path";
12949
+ import { join as join34 } from "node:path";
12617
12950
  var MAX_INPUT = 64 * 1024;
12618
12951
  var REPORT_TIMEOUT_MS = 300;
12619
12952
  var OWN_COMMAND_TIMEOUT_MS = 5e3;
@@ -12650,7 +12983,7 @@ function agyStateFrom(input) {
12650
12983
  function ownAgyStatusLine(home = homedir7()) {
12651
12984
  try {
12652
12985
  const settings = JSON.parse(
12653
- readFileSync21(join32(home, ".gemini", "antigravity-cli", "settings.json"), "utf8")
12986
+ readFileSync21(join34(home, ".gemini", "antigravity-cli", "settings.json"), "utf8")
12654
12987
  );
12655
12988
  const command = settings.statusLine?.command;
12656
12989
  if (typeof command !== "string" || !command.trim()) return null;
@@ -12670,25 +13003,25 @@ function commandIn(path) {
12670
13003
  }
12671
13004
  }
12672
13005
  function ownStatusLine(projectDir, home = homedir7()) {
12673
- return commandIn(join32(projectDir, ".claude", "settings.json")) ?? commandIn(join32(home, ".claude", "settings.json"));
13006
+ return commandIn(join34(projectDir, ".claude", "settings.json")) ?? commandIn(join34(home, ".claude", "settings.json"));
12674
13007
  }
12675
13008
  function runOwnCommand(command, input, cwd, timeoutMs = OWN_COMMAND_TIMEOUT_MS) {
12676
- return new Promise((resolve6) => {
13009
+ return new Promise((resolve7) => {
12677
13010
  let output2 = "";
12678
13011
  let child;
12679
13012
  try {
12680
- child = spawn10("sh", ["-c", command], {
13013
+ child = spawn11("sh", ["-c", command], {
12681
13014
  cwd,
12682
13015
  detached: true,
12683
13016
  stdio: ["pipe", "pipe", "ignore"]
12684
13017
  });
12685
13018
  } catch {
12686
- return resolve6("");
13019
+ return resolve7("");
12687
13020
  }
12688
13021
  const finish = (text4) => {
12689
13022
  clearTimeout(timer);
12690
13023
  child.stdout?.destroy();
12691
- resolve6(text4);
13024
+ resolve7(text4);
12692
13025
  };
12693
13026
  const timer = setTimeout(() => {
12694
13027
  try {
@@ -12706,12 +13039,12 @@ function runOwnCommand(command, input, cwd, timeoutMs = OWN_COMMAND_TIMEOUT_MS)
12706
13039
  });
12707
13040
  }
12708
13041
  function tellDaemon(path, request, timeoutMs = REPORT_TIMEOUT_MS) {
12709
- return new Promise((resolve6) => {
13042
+ return new Promise((resolve7) => {
12710
13043
  const socket = connect3(path);
12711
13044
  const done = () => {
12712
13045
  clearTimeout(timer);
12713
13046
  socket.destroy();
12714
- resolve6();
13047
+ resolve7();
12715
13048
  };
12716
13049
  const timer = setTimeout(done, timeoutMs);
12717
13050
  socket.on("connect", () => socket.write(`${JSON.stringify(request)}
@@ -12963,7 +13296,7 @@ init_define_HERD_PACKAGE();
12963
13296
  import { connect as connect4 } from "node:net";
12964
13297
  var DETACH_KEY = "";
12965
13298
  function attach(name, { stdin, stdout } = { stdin: process.stdin, stdout: process.stdout }, path = currentHostSocketPath()) {
12966
- return new Promise((resolve6, reject) => {
13299
+ return new Promise((resolve7, reject) => {
12967
13300
  const socket = connect4(path);
12968
13301
  const size = () => ({ cols: stdout.columns || 120, rows: stdout.rows || 40 });
12969
13302
  let buffer = "";
@@ -12977,7 +13310,7 @@ function attach(name, { stdin, stdout } = { stdin: process.stdin, stdout: proces
12977
13310
  stdin.pause();
12978
13311
  stdout.off("resize", onResize);
12979
13312
  socket.end();
12980
- resolve6(result);
13313
+ resolve7(result);
12981
13314
  };
12982
13315
  const onInput = (chunk) => {
12983
13316
  const data = chunk.toString("utf8");
@@ -13062,20 +13395,21 @@ async function runTerminalHost(rest) {
13062
13395
 
13063
13396
  // src/commands/repo.ts
13064
13397
  init_define_HERD_PACKAGE();
13065
- import { join as join33 } from "node:path";
13398
+ import { join as join35 } from "node:path";
13066
13399
  import { z as z32 } from "zod";
13067
13400
  var REPO_HELP = `Usage: herd repo <url or owner/name> [folder]
13068
13401
 
13069
13402
  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
13403
+ from the freshly fetched default branch, and records it in the conversation. It comes from your
13404
+ owner's own checkout of the repo in the machine's root, and gets that checkout's local files git
13405
+ ignores, such as .env, copied in. A repo the root doesn't have is cloned into it first, with your
13406
+ owner's own git credentials. owner/name means one of the repos herd repos lists, or else
13073
13407
  github.com/owner/name. The folder is the repo's name, or <owner>-<name> when that's taken, unless
13074
13408
  you give one. Running it again for a repo that's already here changes nothing.`;
13075
13409
  var REPOS_HELP = `Usage: herd repos [--json]
13076
13410
 
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.`;
13411
+ Lists the repos in this conversation's folder, the repos checked out in this machine's root, and
13412
+ the repos your owner's herd.config.ts describes, with what each is for. Add one with herd repo.`;
13079
13413
  var repoContextSchema = z32.object({
13080
13414
  folder: z32.string(),
13081
13415
  root: z32.string(),
@@ -13149,7 +13483,7 @@ async function runRepo(rest) {
13149
13483
  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
13484
  );
13151
13485
  }
13152
- const path = join33(here.folder, place.name);
13486
+ const path = join35(here.folder, place.name);
13153
13487
  const recorded = here.recorded.some((repo) => repo.folder === place.name);
13154
13488
  if (place.existing && recorded) {
13155
13489
  const branch = here.recorded.find((repo) => repo.folder === place.name)?.branch ?? here.branch;
@@ -13191,13 +13525,14 @@ ${error.message}`);
13191
13525
  note = ` The conversation couldn't record it: ${error instanceof Error ? error.message : error}`;
13192
13526
  }
13193
13527
  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}.`
13528
+ 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
13529
  ];
13196
13530
  if (added.fetchFailed) {
13197
13531
  lines.push(
13198
13532
  `Couldn't fetch it, so it started from the last fetched ${added.base}: ${added.fetchFailed}`
13199
13533
  );
13200
13534
  }
13535
+ if (added.copied.length > 0) lines.push(describeCopied(place.name, added.copied));
13201
13536
  if (added.created) lines.push("Its own AGENTS.md or README says how to set it up.");
13202
13537
  console.log(lines.join("\n") + note);
13203
13538
  return 0;
@@ -13209,7 +13544,7 @@ function formatRepos(here) {
13209
13544
  for (const repo of here.recorded)
13210
13545
  lines.push(` ${repo.folder} ${repo.remote} on ${repo.branch}`);
13211
13546
  const cloned = here.cloned.filter((repo) => !inFolder.has(repo.key));
13212
- lines.push("", "Cloned on this machine:");
13547
+ lines.push("", "Checked out in the root:");
13213
13548
  if (cloned.length === 0) lines.push(" none others");
13214
13549
  const about = new Map(here.described.map((repo) => [repo.key, repo.about]));
13215
13550
  for (const repo of cloned) {
@@ -13253,7 +13588,7 @@ async function runRepos(rest) {
13253
13588
  init_define_HERD_PACKAGE();
13254
13589
  init_src();
13255
13590
  import { randomUUID as randomUUID8 } from "node:crypto";
13256
- import { resolve as resolve5 } from "node:path";
13591
+ import { resolve as resolve6 } from "node:path";
13257
13592
  var SEND_HELP = `Usage: herd send <text>
13258
13593
  herd send -- <text that starts with a dash>
13259
13594
  herd send < reply.txt
@@ -13336,7 +13671,7 @@ function withAbsolutePaths(message, cwd = process.cwd()) {
13336
13671
  return {
13337
13672
  ...message,
13338
13673
  files: files.map(
13339
- (file) => file && typeof file === "object" && typeof file.path === "string" ? { ...file, path: resolve5(cwd, file.path) } : file
13674
+ (file) => file && typeof file === "object" && typeof file.path === "string" ? { ...file, path: resolve6(cwd, file.path) } : file
13340
13675
  )
13341
13676
  };
13342
13677
  }
@@ -13973,7 +14308,7 @@ ${HELP4}`);
13973
14308
  // src/package-entry.ts
13974
14309
  var invoked = (() => {
13975
14310
  try {
13976
- return realpathSync9(process.argv[1] ?? "");
14311
+ return realpathSync11(process.argv[1] ?? "");
13977
14312
  } catch {
13978
14313
  return "";
13979
14314
  }