toyon 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/README.md +2 -0
  2. package/dist/cli.js +5132 -24
  3. package/dist/daemon.js +1016 -316
  4. package/dist/shell/assets/{Editor-DR-GTFmf.js → Editor-D5UJRxLO.js} +4 -4
  5. package/dist/shell/assets/{XTerm-B3kT_naP.js → XTerm-l6IDHxt8.js} +1 -1
  6. package/dist/shell/assets/{cssMode-CB4CczRa.js → cssMode-DIt1Onbo.js} +1 -1
  7. package/dist/shell/assets/{freemarker2-Wjq3dVyB.js → freemarker2-BlnMjxhl.js} +1 -1
  8. package/dist/shell/assets/{grammar-yCr7BdNP.js → grammar-xCgG7hS1.js} +1 -1
  9. package/dist/shell/assets/{handlebars-Bjvi6pTu.js → handlebars-CgcpCzmy.js} +1 -1
  10. package/dist/shell/assets/{html-iB2mKeN2.js → html-yqzA9zjA.js} +1 -1
  11. package/dist/shell/assets/{htmlMode-DtZVhckD.js → htmlMode-AyUHvgCk.js} +1 -1
  12. package/dist/shell/assets/index-BGLn9_S8.js +241 -0
  13. package/dist/shell/assets/index-Cm07OHxH.css +1 -0
  14. package/dist/shell/assets/{javascript-BpuIkItm.js → javascript-CYZ-NuGG.js} +1 -1
  15. package/dist/shell/assets/{jsonMode-QCLe5ypz.js → jsonMode-BpybSxrR.js} +1 -1
  16. package/dist/shell/assets/{liquid-CQI6mzhA.js → liquid-BHHjN7T9.js} +1 -1
  17. package/dist/shell/assets/{lspLanguageFeatures-DqXzGsHQ.js → lspLanguageFeatures-CHyCsA1P.js} +1 -1
  18. package/dist/shell/assets/{mdx-D2Unh-By.js → mdx-CCgYQauJ.js} +1 -1
  19. package/dist/shell/assets/{python-LLSqiJ4D.js → python-C1JulLv8.js} +1 -1
  20. package/dist/shell/assets/{razor-CJ2qsTB8.js → razor-Dj4sWgNm.js} +1 -1
  21. package/dist/shell/assets/{tsMode-CwjjHUH5.js → tsMode-D1gKgsY6.js} +1 -1
  22. package/dist/shell/assets/{xml-BzqCyEOW.js → xml-BHUNgL0o.js} +1 -1
  23. package/dist/shell/assets/{yaml-N1Bfi3ds.js → yaml-CetvwW8L.js} +1 -1
  24. package/dist/shell/index.html +2 -2
  25. package/package.json +1 -1
  26. package/dist/shell/assets/index-DVg10dTN.js +0 -237
  27. package/dist/shell/assets/index-wt4cLIYn.css +0 -1
package/dist/daemon.js CHANGED
@@ -42,6 +42,21 @@ var LOGIN_STREAM = "login";
42
42
  function streamKey(worktreeId, stream) {
43
43
  return `${worktreeId}/${stream}`;
44
44
  }
45
+ function applyLog(lines, proc, line, retract = 0) {
46
+ let out = lines;
47
+ for (let left = retract;left > 0; left--) {
48
+ let i = out.length - 1;
49
+ while (i >= 0 && out[i]?.proc !== proc)
50
+ i--;
51
+ if (i < 0)
52
+ break;
53
+ out = out === lines ? [...out] : out;
54
+ out.splice(i, 1);
55
+ }
56
+ if (!line)
57
+ return out;
58
+ return [...out, { proc, line }];
59
+ }
45
60
 
46
61
  // ../shared/src/worktree-caps.ts
47
62
  function isMain(wt) {
@@ -302,62 +317,8 @@ function landPolicy(config) {
302
317
  ...config.land?.method ? { merge: config.land.method } : {}
303
318
  };
304
319
  }
305
-
306
- // ../shared/src/pair.ts
307
- var PAIR_TTL_MS = 120000;
308
- var pairLink = (host, code) => `https://${host}/#pair=${code}`;
309
-
310
- // ../shared/src/protocol/limits.ts
311
- var PROTOCOL_VERSION = 65;
312
- var FILE_MAX_CHARS = 5000000;
313
- var IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
314
- var IMAGE_MAX_EDGE = 2576;
315
- var IMAGE_MAX_BYTES = 5 * 1024 * 1024;
316
- var PASTE_MAX_CHARS = 1e5;
317
-
318
- // ../shared/src/paste.ts
319
- var PREVIEW_CHARS = 80;
320
- function pasteSummary(text) {
321
- const lines = text.split(`
322
- `);
323
- const first = lines.find((l) => l.trim().length > 0)?.trim() ?? "";
324
- return { chars: text.length, lines: lines.length, preview: first.slice(0, PREVIEW_CHARS) };
325
- }
326
- function lineSpan({ startLine, endLine }) {
327
- return startLine === endLine ? `${startLine}` : `${startLine}-${endLine}`;
328
- }
329
-
330
- // ../shared/src/ports.ts
331
- var DAEMON_DEFAULT_PORT = 4141;
332
- var SHELL_DEV_PORT = 4142;
333
-
334
- // ../shared/src/project.ts
335
- var PROJECTS_FOLDER = "Projects";
336
- function hasControlChar(s) {
337
- for (const ch of s) {
338
- const code = ch.codePointAt(0) ?? 0;
339
- if (code < 32 || code === 127)
340
- return true;
341
- }
342
- return false;
343
- }
344
- function projectNameError(raw) {
345
- const name = raw.trim();
346
- if (!name)
347
- return "a project name is required";
348
- if (name.length > 100)
349
- return "that name is too long: keep it under 100 characters";
350
- if (name.includes("/") || name.includes("\\"))
351
- return "a project name cannot contain a slash";
352
- if (/\s/.test(name))
353
- return "a project name cannot contain spaces";
354
- if (hasControlChar(name))
355
- return "a project name cannot contain control characters";
356
- if (name === "." || name === "..")
357
- return `"${name}" is not a project name`;
358
- if (name.startsWith("-") || name.startsWith("."))
359
- return "a project name cannot start with a dot or a dash";
360
- return null;
320
+ function shipNoun(op) {
321
+ return { land: "a land", commit: "a commit", "sync-main": "a sync", "pull-main": "a pull" }[op];
361
322
  }
362
323
 
363
324
  // ../../node_modules/.bun/zod@4.5.4/node_modules/zod/v4/core/util.js
@@ -6254,6 +6215,221 @@ function refine(fn, _params = {}) {
6254
6215
  function superRefine(fn, params) {
6255
6216
  return _superRefine(fn, params);
6256
6217
  }
6218
+ // ../shared/src/managed.ts
6219
+ var MANAGED_REMOTE = ["off", "tailscale", "any"];
6220
+ var MANAGED_DEFAULTS = {
6221
+ updates: true,
6222
+ deploy: true,
6223
+ remote: "any",
6224
+ agents: null,
6225
+ customAgents: true,
6226
+ brandedListener: true,
6227
+ planSignIn: true
6228
+ };
6229
+ var MANAGED_STRICTEST = {
6230
+ updates: false,
6231
+ deploy: false,
6232
+ remote: "off",
6233
+ agents: null,
6234
+ customAgents: false,
6235
+ brandedListener: false,
6236
+ planSignIn: false
6237
+ };
6238
+ var managedFileSchema = object({
6239
+ updates: boolean2().optional(),
6240
+ deploy: boolean2().optional(),
6241
+ remote: _enum(MANAGED_REMOTE).optional(),
6242
+ agents: array(string2()).optional(),
6243
+ customAgents: boolean2().optional(),
6244
+ brandedListener: boolean2().optional(),
6245
+ planSignIn: boolean2().optional()
6246
+ });
6247
+ var MANAGED_NONE = { ...MANAGED_DEFAULTS, source: null, problem: null };
6248
+ function managedSources(platform, user) {
6249
+ if (platform === "darwin") {
6250
+ return [
6251
+ { path: "/Library/Managed Preferences/dev.toyon.plist", kind: "plist" },
6252
+ ...user ? [{ path: `/Library/Managed Preferences/${user}/dev.toyon.plist`, kind: "plist" }] : [],
6253
+ { path: "/Library/Application Support/toyon/policy.json", kind: "json" }
6254
+ ];
6255
+ }
6256
+ if (platform === "linux")
6257
+ return [{ path: "/etc/toyon/policy.json", kind: "json" }];
6258
+ return [];
6259
+ }
6260
+ var KEYS = ["updates", "deploy", "remote", "agents", "customAgents", "brandedListener", "planSignIn"];
6261
+ function stricter(key, a, b) {
6262
+ if (key === "remote") {
6263
+ const ra = MANAGED_REMOTE.indexOf(a);
6264
+ const rb = MANAGED_REMOTE.indexOf(b);
6265
+ return ra <= rb ? a : b;
6266
+ }
6267
+ if (key === "agents") {
6268
+ const la = a;
6269
+ const lb = b;
6270
+ if (la === null)
6271
+ return b;
6272
+ if (lb === null)
6273
+ return a;
6274
+ return la.filter((id) => lb.includes(id));
6275
+ }
6276
+ return a && b;
6277
+ }
6278
+ function merge2(into, from) {
6279
+ const out = { ...into };
6280
+ for (const key of KEYS) {
6281
+ const v = from[key];
6282
+ if (v === undefined)
6283
+ continue;
6284
+ out[key] = stricter(key, into[key], v);
6285
+ }
6286
+ return out;
6287
+ }
6288
+ function textHash(text) {
6289
+ let h = 2166136261;
6290
+ for (let i = 0;i < text.length; i++) {
6291
+ h ^= text.charCodeAt(i);
6292
+ h = Math.imul(h, 16777619) >>> 0;
6293
+ }
6294
+ return h.toString(16).padStart(8, "0");
6295
+ }
6296
+ function describeIssue(e) {
6297
+ const issue = e.issues[0];
6298
+ if (!issue)
6299
+ return "not a policy";
6300
+ const at = issue.path.map(String).join(".");
6301
+ return at ? `"${at}" ${issue.message.toLowerCase()}` : issue.message.toLowerCase();
6302
+ }
6303
+ function resolveManaged(reads) {
6304
+ let policy = MANAGED_DEFAULTS;
6305
+ const sources = [];
6306
+ const applied = [];
6307
+ let source = null;
6308
+ let problem = null;
6309
+ for (const r of reads) {
6310
+ const spec = { path: r.path, kind: r.kind };
6311
+ let why = r.problem ?? (r.owned === false ? "not owned by root, or writable by others" : null);
6312
+ if (why === null && r.text === undefined) {
6313
+ sources.push({ ...spec, state: "absent" });
6314
+ continue;
6315
+ }
6316
+ let parsed = null;
6317
+ if (why === null) {
6318
+ try {
6319
+ const json = JSON.parse(r.text ?? "");
6320
+ const result = managedFileSchema.safeParse(json);
6321
+ if (result.success)
6322
+ parsed = result.data;
6323
+ else
6324
+ why = describeIssue(result.error);
6325
+ } catch {
6326
+ why = r.kind === "plist" ? "not a plist plutil can read" : "not valid JSON";
6327
+ }
6328
+ }
6329
+ if (why !== null || parsed === null) {
6330
+ sources.push({ ...spec, state: "invalid", problem: why ?? "unreadable" });
6331
+ if (problem === null) {
6332
+ problem = `${r.path}: ${why ?? "unreadable"}`;
6333
+ source = r.path;
6334
+ }
6335
+ continue;
6336
+ }
6337
+ sources.push({ ...spec, state: "applied" });
6338
+ applied.push(r.text ?? "");
6339
+ if (problem === null && source === null)
6340
+ source = r.path;
6341
+ policy = merge2(policy, parsed);
6342
+ }
6343
+ if (problem !== null)
6344
+ policy = MANAGED_STRICTEST;
6345
+ return {
6346
+ policy,
6347
+ sources,
6348
+ hash: applied.length ? textHash(applied.join(`
6349
+ `)) : null,
6350
+ source,
6351
+ problem
6352
+ };
6353
+ }
6354
+ function managedView(r) {
6355
+ return { ...r.policy, source: r.source, problem: r.problem };
6356
+ }
6357
+ function describeManaged(p) {
6358
+ const out = [];
6359
+ if (!p.updates)
6360
+ out.push("updates off");
6361
+ if (!p.deploy)
6362
+ out.push("deploy off");
6363
+ if (p.remote !== "any")
6364
+ out.push(`remote ${p.remote}`);
6365
+ if (p.agents !== null)
6366
+ out.push(`agents ${p.agents.join(", ") || "none"}`);
6367
+ if (!p.customAgents)
6368
+ out.push("custom agents off");
6369
+ if (!p.brandedListener)
6370
+ out.push("toyon.localhost listener off");
6371
+ if (!p.planSignIn)
6372
+ out.push("plan sign-in off");
6373
+ return out;
6374
+ }
6375
+
6376
+ // ../shared/src/pair.ts
6377
+ var PAIR_TTL_MS = 120000;
6378
+ var pairLink = (host, code) => `https://${host}/#pair=${code}`;
6379
+
6380
+ // ../shared/src/protocol/limits.ts
6381
+ var PROTOCOL_VERSION = 69;
6382
+ var FILE_MAX_CHARS = 5000000;
6383
+ var IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
6384
+ var IMAGE_MAX_EDGE = 2576;
6385
+ var IMAGE_MAX_BYTES = 5 * 1024 * 1024;
6386
+ var PASTE_MAX_CHARS = 1e5;
6387
+
6388
+ // ../shared/src/paste.ts
6389
+ var PREVIEW_CHARS = 80;
6390
+ function pasteSummary(text) {
6391
+ const lines = text.split(`
6392
+ `);
6393
+ const first = lines.find((l) => l.trim().length > 0)?.trim() ?? "";
6394
+ return { chars: text.length, lines: lines.length, preview: first.slice(0, PREVIEW_CHARS) };
6395
+ }
6396
+ function lineSpan({ startLine, endLine }) {
6397
+ return startLine === endLine ? `${startLine}` : `${startLine}-${endLine}`;
6398
+ }
6399
+
6400
+ // ../shared/src/ports.ts
6401
+ var DAEMON_DEFAULT_PORT = 4141;
6402
+ var SHELL_DEV_PORT = 4142;
6403
+
6404
+ // ../shared/src/project.ts
6405
+ var PROJECTS_FOLDER = "Projects";
6406
+ function hasControlChar(s) {
6407
+ for (const ch of s) {
6408
+ const code = ch.codePointAt(0) ?? 0;
6409
+ if (code < 32 || code === 127)
6410
+ return true;
6411
+ }
6412
+ return false;
6413
+ }
6414
+ function projectNameError(raw) {
6415
+ const name = raw.trim();
6416
+ if (!name)
6417
+ return "a project name is required";
6418
+ if (name.length > 100)
6419
+ return "that name is too long: keep it under 100 characters";
6420
+ if (name.includes("/") || name.includes("\\"))
6421
+ return "a project name cannot contain a slash";
6422
+ if (/\s/.test(name))
6423
+ return "a project name cannot contain spaces";
6424
+ if (hasControlChar(name))
6425
+ return "a project name cannot contain control characters";
6426
+ if (name === "." || name === "..")
6427
+ return `"${name}" is not a project name`;
6428
+ if (name.startsWith("-") || name.startsWith("."))
6429
+ return "a project name cannot start with a dot or a dash";
6430
+ return null;
6431
+ }
6432
+
6257
6433
  // ../shared/src/protocol/pick.ts
6258
6434
  var pickMetaSchema = object({
6259
6435
  component: string2().nullable(),
@@ -7559,6 +7735,10 @@ var VIEWERS = [[/\.(png|jpe?g|gif|webp|avif|bmp|ico)$/i, "image"]];
7559
7735
  function viewerOf(path) {
7560
7736
  return VIEWERS.find(([re]) => re.test(path))?.[1] ?? null;
7561
7737
  }
7738
+ var PAGE_ASSETS = /\.(css|svg|woff2?|ttf|otf|mp4|webm|mp3|ogg|wav)$/i;
7739
+ function isPageAsset(path) {
7740
+ return PAGE_ASSETS.test(path);
7741
+ }
7562
7742
 
7563
7743
  // ../shared/src/vscode-theme.ts
7564
7744
  var workbenchKeys = {
@@ -7736,10 +7916,59 @@ function pickSyntax(tokenColors) {
7736
7916
  function slug(s) {
7737
7917
  return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "theme";
7738
7918
  }
7919
+
7920
+ // ../shared/src/managed-load.ts
7921
+ import { readFileSync, statSync } from "fs";
7922
+ import { userInfo } from "os";
7923
+ async function plutilJson(path) {
7924
+ const p = Bun.spawn(["plutil", "-convert", "json", "-o", "-", path], { stdout: "pipe", stderr: "pipe" });
7925
+ const [out, err] = await Promise.all([new Response(p.stdout).text(), new Response(p.stderr).text()]);
7926
+ await p.exited;
7927
+ if (p.exitCode !== 0)
7928
+ throw new Error(err.trim().split(`
7929
+ `)[0] || `plutil exited ${p.exitCode}`);
7930
+ return out;
7931
+ }
7932
+ function ownedByRoot(st) {
7933
+ return st.uid === 0 && (st.mode & 18) === 0;
7934
+ }
7935
+ async function loadManaged(opts = {}) {
7936
+ const platform = opts.platform ?? process.platform;
7937
+ const user = opts.user ?? safeUser();
7938
+ const sources = opts.sources ?? managedSources(platform, user);
7939
+ const reads = [];
7940
+ for (const s of sources) {
7941
+ let st;
7942
+ try {
7943
+ st = statSync(s.path);
7944
+ } catch {
7945
+ reads.push(s);
7946
+ continue;
7947
+ }
7948
+ if (s.kind === "json" && !ownedByRoot(st)) {
7949
+ reads.push({ ...s, owned: false });
7950
+ continue;
7951
+ }
7952
+ try {
7953
+ const text = s.kind === "plist" ? await (opts.plist ?? plutilJson)(s.path) : readFileSync(s.path, "utf8");
7954
+ reads.push({ ...s, text });
7955
+ } catch (e) {
7956
+ reads.push({ ...s, problem: e instanceof Error ? e.message : String(e) });
7957
+ }
7958
+ }
7959
+ return resolveManaged(reads);
7960
+ }
7961
+ function safeUser() {
7962
+ try {
7963
+ return userInfo().username || undefined;
7964
+ } catch {
7965
+ return;
7966
+ }
7967
+ }
7739
7968
  // ../daemon/package.json
7740
7969
  var package_default = {
7741
7970
  name: "@toyon/daemon",
7742
- version: "0.5.0",
7971
+ version: "0.6.0",
7743
7972
  private: true,
7744
7973
  type: "module",
7745
7974
  main: "src/index.ts",
@@ -12084,6 +12313,7 @@ function spawnAcp(app, launch, cwd, tag) {
12084
12313
  }
12085
12314
 
12086
12315
  // ../daemon/src/agent/attachments.ts
12316
+ import { createHash } from "crypto";
12087
12317
  import { mkdir, writeFile } from "fs/promises";
12088
12318
  import { join } from "path";
12089
12319
  var EXT = {
@@ -12093,7 +12323,17 @@ var EXT = {
12093
12323
  "image/webp": "webp"
12094
12324
  };
12095
12325
  var ID = /^[A-Za-z0-9_-]{1,64}$/;
12096
- var FILE = /^\d{1,6}\.(png|jpg|gif|webp|txt)$/;
12326
+ var FILE = /^(\d{1,6}|[0-9a-f]{16})\.(png|jpg|gif|webp|txt)$/;
12327
+ function toolImage(data, mimeType) {
12328
+ const ext = EXT[mimeType];
12329
+ if (!ext)
12330
+ return null;
12331
+ const bytes = Buffer.from(data, "base64");
12332
+ if (bytes.length === 0)
12333
+ return null;
12334
+ const file = `${createHash("sha256").update(bytes).digest("hex").slice(0, 16)}.${ext}`;
12335
+ return { ref: { file, mimeType, bytes: bytes.length }, bytes };
12336
+ }
12097
12337
  var isAttachmentFile = (file) => FILE.test(file);
12098
12338
  function attachmentsDirFor(attachmentsDir, worktreeId) {
12099
12339
  return join(attachmentsDir, worktreeId);
@@ -12101,9 +12341,21 @@ function attachmentsDirFor(attachmentsDir, worktreeId) {
12101
12341
 
12102
12342
  class AttachmentStore {
12103
12343
  dir;
12344
+ pending = new Map;
12104
12345
  constructor(dir) {
12105
12346
  this.dir = dir;
12106
12347
  }
12348
+ async putToolImage(worktreeId, file, bytes) {
12349
+ if (!ID.test(worktreeId) || !FILE.test(file))
12350
+ throw new UserError("bad tool image");
12351
+ const path = join(attachmentsDirFor(this.dir, worktreeId), file);
12352
+ if (this.pending.has(path) || await Bun.file(path).exists())
12353
+ return;
12354
+ await this.write(worktreeId, file, bytes);
12355
+ }
12356
+ whenWritten(worktreeId, file) {
12357
+ return this.pending.get(join(attachmentsDirFor(this.dir, worktreeId), file)) ?? Promise.resolve();
12358
+ }
12107
12359
  async put(worktreeId, n, input) {
12108
12360
  if (!ID.test(worktreeId))
12109
12361
  throw new UserError("bad worktree id");
@@ -12148,10 +12400,12 @@ class AttachmentStore {
12148
12400
  ref: { kind: "paste", n, ...name ? { name } : {}, ...source ? { source } : {}, ...pasteSummary(text), file }
12149
12401
  };
12150
12402
  }
12151
- async write(worktreeId, file, data) {
12403
+ write(worktreeId, file, data) {
12152
12404
  const wtDir = attachmentsDirFor(this.dir, worktreeId);
12153
- await mkdir(wtDir, { recursive: true });
12154
- await writeFile(join(wtDir, file), data);
12405
+ const path = join(wtDir, file);
12406
+ const done = mkdir(wtDir, { recursive: true }).then(() => writeFile(path, data)).finally(() => this.pending.delete(path));
12407
+ this.pending.set(path, done);
12408
+ return done;
12155
12409
  }
12156
12410
  fileFor(worktreeId, file) {
12157
12411
  if (!ID.test(worktreeId) || !FILE.test(file))
@@ -12313,7 +12567,7 @@ class OptionProbe {
12313
12567
  }
12314
12568
 
12315
12569
  // ../daemon/src/agent/registry.ts
12316
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
12570
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
12317
12571
  import { homedir as homedir3 } from "os";
12318
12572
  import { dirname as dirname3, isAbsolute, join as join5, resolve as resolve3 } from "path";
12319
12573
 
@@ -12339,8 +12593,9 @@ var cloud = {
12339
12593
  };
12340
12594
 
12341
12595
  // ../daemon/src/git/exec.ts
12342
- import { createHash } from "crypto";
12343
- import { existsSync, readFileSync } from "fs";
12596
+ import { spawn as spawn2 } from "child_process";
12597
+ import { createHash as createHash2 } from "crypto";
12598
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
12344
12599
  import { join as join2 } from "path";
12345
12600
 
12346
12601
  // ../daemon/src/core/metrics.ts
@@ -12380,10 +12635,10 @@ var LOCKFILES = [
12380
12635
  "Cargo.lock"
12381
12636
  ];
12382
12637
  function lockfileHash(dir) {
12383
- const h = createHash("sha1");
12638
+ const h = createHash2("sha1");
12384
12639
  for (const f of LOCKFILES) {
12385
12640
  try {
12386
- h.update(readFileSync(join2(dir, f)));
12641
+ h.update(readFileSync2(join2(dir, f)));
12387
12642
  } catch {}
12388
12643
  }
12389
12644
  return h.digest("hex");
@@ -12494,6 +12749,80 @@ async function runLive(cmd, args, cwd, opts = {}) {
12494
12749
  }
12495
12750
  }
12496
12751
  }
12752
+ function runWatched(cmd, args, cwd, opts = {}) {
12753
+ const started = Date.now();
12754
+ return new Promise((resolve) => {
12755
+ let timer;
12756
+ let timedOut = false;
12757
+ let stop;
12758
+ let settled = false;
12759
+ const settle = (r) => {
12760
+ if (settled)
12761
+ return;
12762
+ settled = true;
12763
+ if (timer)
12764
+ clearTimeout(timer);
12765
+ if (stop)
12766
+ opts.signal?.removeEventListener("abort", stop);
12767
+ if (cmd === GIT) {
12768
+ lastGit.cmd = `git ${args.slice(0, 3).join(" ")}`;
12769
+ lastGit.ms = Date.now() - started;
12770
+ lastGit.at = Date.now();
12771
+ }
12772
+ resolve(r);
12773
+ };
12774
+ const failed = (msg) => settle({ ok: false, out: "", err: msg, exit: msg, text: msg });
12775
+ let child;
12776
+ try {
12777
+ child = spawn2(cmd, args, {
12778
+ cwd,
12779
+ stdio: ["ignore", "pipe", "pipe"],
12780
+ detached: true,
12781
+ env: { ...process.env, ...SPAWN_ENV, ...opts.env }
12782
+ });
12783
+ } catch (e) {
12784
+ failed(e instanceof Error ? e.message : String(e));
12785
+ return;
12786
+ }
12787
+ child.on("error", (e) => failed(e.message));
12788
+ const exited = new Promise((r) => child.once("exit", () => r()));
12789
+ const kill = () => {
12790
+ if (child.pid)
12791
+ fireAndForget("git", killGroup(child.pid, exited), `stopping ${cmd} ${args[0] ?? ""}`);
12792
+ };
12793
+ if (opts.timeoutMs) {
12794
+ timer = setTimeout(() => {
12795
+ timedOut = true;
12796
+ kill();
12797
+ }, opts.timeoutMs);
12798
+ }
12799
+ if (opts.signal) {
12800
+ stop = kill;
12801
+ if (opts.signal.aborted)
12802
+ kill();
12803
+ else
12804
+ opts.signal.addEventListener("abort", stop, { once: true });
12805
+ }
12806
+ let text = "";
12807
+ const out = [];
12808
+ const err = [];
12809
+ const read = (stream, into) => {
12810
+ const dec = new TextDecoder;
12811
+ stream?.on("data", (b) => {
12812
+ const chunk = dec.decode(b, { stream: true });
12813
+ into.push(chunk);
12814
+ text += chunk;
12815
+ opts.onText?.(chunk);
12816
+ });
12817
+ };
12818
+ read(child.stdout, out);
12819
+ read(child.stderr, err);
12820
+ child.on("close", (code, signal) => {
12821
+ const exit = timedOut ? "timeout" : signal ?? code;
12822
+ settle({ ok: code === 0 && !timedOut, out: out.join("").trim(), err: err.join("").trim(), exit, text });
12823
+ });
12824
+ });
12825
+ }
12497
12826
  async function git(cwd, ...args) {
12498
12827
  const { rawOut: _raw, ...r } = await run(GIT, args, cwd);
12499
12828
  return r;
@@ -12526,7 +12855,7 @@ async function defaultBranch(path) {
12526
12855
  }
12527
12856
 
12528
12857
  // ../daemon/src/agent/confine.ts
12529
- import { statSync } from "fs";
12858
+ import { statSync as statSync2 } from "fs";
12530
12859
  var SANDBOX_EXEC = "/usr/bin/sandbox-exec";
12531
12860
  var uniq = (xs) => [...new Set(xs)];
12532
12861
  var literal2 = (p) => `"${p.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
@@ -12544,7 +12873,7 @@ function seatbeltProfile(b, extraWrite = []) {
12544
12873
  }
12545
12874
  function kindOf(p) {
12546
12875
  try {
12547
- return statSync(p).isDirectory() ? "dir" : "file";
12876
+ return statSync2(p).isDirectory() ? "dir" : "file";
12548
12877
  } catch {
12549
12878
  return null;
12550
12879
  }
@@ -12584,7 +12913,7 @@ function confine(launch, b, extraWrite = [], platform = process.platform, which
12584
12913
  }
12585
12914
 
12586
12915
  // ../daemon/src/agent/native.ts
12587
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
12916
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "fs";
12588
12917
  var OS = { darwin: "darwin", linux: "linux", win32: "windows" };
12589
12918
  function nativePackage(pkg, host) {
12590
12919
  const os = OS[host.platform];
@@ -12614,7 +12943,7 @@ function muslLoader() {
12614
12943
  }
12615
12944
  function cpuFlags() {
12616
12945
  try {
12617
- return readFileSync2("/proc/cpuinfo", "utf8").match(/^flags\s*:(.*)$/m)?.[1]?.trim().split(/\s+/) ?? [];
12946
+ return readFileSync3("/proc/cpuinfo", "utf8").match(/^flags\s*:(.*)$/m)?.[1]?.trim().split(/\s+/) ?? [];
12618
12947
  } catch {
12619
12948
  return [];
12620
12949
  }
@@ -12677,7 +13006,7 @@ function opencodeEnv() {
12677
13006
  }
12678
13007
 
12679
13008
  // ../daemon/src/agent/sandbox.ts
12680
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, renameSync, writeFileSync as writeFileSync2 } from "fs";
13009
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, renameSync, writeFileSync as writeFileSync2 } from "fs";
12681
13010
  import { homedir as homedir2, tmpdir } from "os";
12682
13011
  import { join as join4, resolve as resolve2 } from "path";
12683
13012
 
@@ -12717,7 +13046,7 @@ function ensureDirs(p) {
12717
13046
  }
12718
13047
 
12719
13048
  // ../daemon/src/git/exclude.ts
12720
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
13049
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "fs";
12721
13050
  import { dirname } from "path";
12722
13051
  async function excludeFile(cwd) {
12723
13052
  const r = await git(cwd, "rev-parse", "--path-format=absolute", "--git-path", "info/exclude");
@@ -12730,7 +13059,7 @@ async function excludeFromGit(cwd, pattern) {
12730
13059
  const file = await excludeFile(cwd);
12731
13060
  if (!file)
12732
13061
  return;
12733
- const current = existsSync3(file) ? readFileSync3(file, "utf8") : "";
13062
+ const current = existsSync3(file) ? readFileSync4(file, "utf8") : "";
12734
13063
  if (current.split(`
12735
13064
  `).some((l) => l.trim() === pattern))
12736
13065
  return;
@@ -12746,7 +13075,7 @@ async function excludeBlock(cwd, name, patterns) {
12746
13075
  return;
12747
13076
  const open = `# toyon ${name}`;
12748
13077
  const close = `# toyon end ${name}`;
12749
- const current = existsSync3(file) ? readFileSync3(file, "utf8") : "";
13078
+ const current = existsSync3(file) ? readFileSync4(file, "utf8") : "";
12750
13079
  const lines = current.split(`
12751
13080
  `);
12752
13081
  const from = lines.indexOf(open);
@@ -12767,7 +13096,7 @@ async function unexcludeFromGit(cwd, pattern) {
12767
13096
  const file = await excludeFile(cwd);
12768
13097
  if (!file || !existsSync3(file))
12769
13098
  return;
12770
- const lines = readFileSync3(file, "utf8").split(`
13099
+ const lines = readFileSync4(file, "utf8").split(`
12771
13100
  `);
12772
13101
  const kept = lines.filter((l) => l.trim() !== pattern);
12773
13102
  if (kept.length === lines.length)
@@ -12846,7 +13175,7 @@ async function writeClaudeLocalSettings(cwd, b) {
12846
13175
  let existing = {};
12847
13176
  if (existsSync5(file)) {
12848
13177
  try {
12849
- const parsed = JSON.parse(readFileSync4(file, "utf8"));
13178
+ const parsed = JSON.parse(readFileSync5(file, "utf8"));
12850
13179
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
12851
13180
  existing = parsed;
12852
13181
  } catch (e) {
@@ -12870,6 +13199,7 @@ async function writeClaudeLocalSettings(cwd, b) {
12870
13199
 
12871
13200
  // ../daemon/src/agent/registry.ts
12872
13201
  var CONFINEMENTS = ["claude-settings", "adapter-sandbox", "toyon-sandbox", "none"];
13202
+ var DEFAULT_AGENT_ID = "claude";
12873
13203
  var BUILTIN_AGENTS = [
12874
13204
  {
12875
13205
  id: "claude",
@@ -12878,7 +13208,9 @@ var BUILTIN_AGENTS = [
12878
13208
  builtin: true,
12879
13209
  run: { kind: "npm-bin", pkg: "@agentclientprotocol/claude-agent-acp", version: "0.75.1", bin: "claude-agent-acp" },
12880
13210
  ...cloud.enabled ? { env: { NO_BROWSER: "1" } } : {},
12881
- terminalLogins: { "claude-login": { args: ["--cli", "auth", "login", "--claudeai"], env: { NO_BROWSER: "1" } } },
13211
+ terminalLogins: {
13212
+ "claude-login": { args: ["--cli", "auth", "login", "--claudeai"], env: { NO_BROWSER: "1" }, plan: true }
13213
+ },
12882
13214
  confinement: "claude-settings",
12883
13215
  setup: (cwd, bounds) => writeClaudeLocalSettings(cwd, bounds).then(() => {
12884
13216
  return;
@@ -12952,6 +13284,13 @@ class AgentRegistry {
12952
13284
  list() {
12953
13285
  return [...this.specs.values()];
12954
13286
  }
13287
+ defaultId(persisted) {
13288
+ if (persisted !== undefined && this.specs.has(persisted))
13289
+ return persisted;
13290
+ if (this.specs.has(DEFAULT_AGENT_ID))
13291
+ return DEFAULT_AGENT_ID;
13292
+ return this.list()[0]?.id ?? DEFAULT_AGENT_ID;
13293
+ }
12955
13294
  get(id) {
12956
13295
  return this.specs.get(id);
12957
13296
  }
@@ -12984,7 +13323,7 @@ class AgentRegistry {
12984
13323
  if (!existsSync6(pkgJson))
12985
13324
  return null;
12986
13325
  try {
12987
- const meta = JSON.parse(readFileSync5(pkgJson, "utf8"));
13326
+ const meta = JSON.parse(readFileSync6(pkgJson, "utf8"));
12988
13327
  const rel = typeof meta.bin === "string" ? meta.bin : meta.bin?.[spec.run.bin];
12989
13328
  if (!rel)
12990
13329
  return null;
@@ -13071,7 +13410,7 @@ class AgentRegistry {
13071
13410
  return null;
13072
13411
  const pkgJson = join5(this.dir, spec.id, "node_modules", name, "package.json");
13073
13412
  try {
13074
- return JSON.parse(readFileSync5(pkgJson, "utf8")).version ?? null;
13413
+ return JSON.parse(readFileSync6(pkgJson, "utf8")).version ?? null;
13075
13414
  } catch {
13076
13415
  return null;
13077
13416
  }
@@ -13139,9 +13478,13 @@ function parseCustomAgents(raw) {
13139
13478
  }
13140
13479
  return out;
13141
13480
  }
13142
- function loadAgentRegistry(agentsFile, agentsDir, sandbox) {
13143
- const custom = existsSync6(agentsFile) ? parseCustomAgents(readFileSync5(agentsFile, "utf8")) : [];
13144
- return new AgentRegistry([...BUILTIN_AGENTS, ...custom], agentsDir, undefined, undefined, sandbox);
13481
+ function managedSpecs(specs, managed) {
13482
+ const kept = managed.customAgents ? specs : specs.filter((s) => s.builtin);
13483
+ return managed.agents === null ? kept : kept.filter((s) => managed.agents?.includes(s.id));
13484
+ }
13485
+ function loadAgentRegistry(agentsFile, agentsDir, sandbox, managed = MANAGED_DEFAULTS) {
13486
+ const custom = managed.customAgents && existsSync6(agentsFile) ? parseCustomAgents(readFileSync6(agentsFile, "utf8")) : [];
13487
+ return new AgentRegistry(managedSpecs([...BUILTIN_AGENTS, ...custom], managed), agentsDir, undefined, undefined, sandbox);
13145
13488
  }
13146
13489
 
13147
13490
  // ../daemon/src/agent/acp/session.ts
@@ -13366,7 +13709,7 @@ ${a.text}
13366
13709
  }
13367
13710
 
13368
13711
  // ../daemon/src/agent/transcript.ts
13369
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
13712
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
13370
13713
  import { appendFile } from "fs/promises";
13371
13714
  import { join as join6 } from "path";
13372
13715
  function transcriptPathFor(transcriptsDir, worktreeId) {
@@ -13377,8 +13720,16 @@ function coalesce(entries) {
13377
13720
  for (const entry of entries) {
13378
13721
  const last = out.at(-1);
13379
13722
  const { event } = entry;
13380
- if ((event.type === "text-delta" || event.type === "thinking-delta") && last?.event.type === event.type) {
13381
- out[out.length - 1] = { seq: last.seq, event: { type: event.type, text: last.event.text + event.text } };
13723
+ if (event.type === "text-delta" && last?.event.type === "text-delta") {
13724
+ const messageId = event.messageId ?? last.event.messageId;
13725
+ out[out.length - 1] = {
13726
+ seq: last.seq,
13727
+ event: { type: "text-delta", text: last.event.text + event.text, ...messageId ? { messageId } : {} }
13728
+ };
13729
+ } else if (event.type === "thinking-delta" && last?.event.type === "thinking-delta") {
13730
+ out[out.length - 1] = { seq: last.seq, event: { type: "thinking-delta", text: last.event.text + event.text } };
13731
+ } else if (event.type === "tool-delta" && last?.event.type === "tool-delta" && last.event.toolId === event.toolId) {
13732
+ out[out.length - 1] = { seq: last.seq, event: { ...event, text: last.event.text + event.text } };
13382
13733
  } else {
13383
13734
  out.push(entry);
13384
13735
  }
@@ -13416,7 +13767,7 @@ class Transcript {
13416
13767
  this.seq = (this.entries.at(-1)?.seq ?? -1) + 1;
13417
13768
  }
13418
13769
  read() {
13419
- return existsSync7(this.path) ? parseTranscript(readFileSync6(this.path, "utf8"), this.tag) : [];
13770
+ return existsSync7(this.path) ? parseTranscript(readFileSync7(this.path, "utf8"), this.tag) : [];
13420
13771
  }
13421
13772
  append(event) {
13422
13773
  const entry = { seq: this.seq++, event };
@@ -13724,7 +14075,7 @@ function abandoned(memos, parent) {
13724
14075
  }
13725
14076
  return out;
13726
14077
  }
13727
- function mapUpdate(update, memos, tag) {
14078
+ function mapUpdate(update, memos, tag, sink) {
13728
14079
  switch (update.sessionUpdate) {
13729
14080
  case "agent_message_chunk": {
13730
14081
  const { parentToolId } = spawnOf(update._meta);
@@ -13732,7 +14083,7 @@ function mapUpdate(update, memos, tag) {
13732
14083
  const text = textOf(update.content, tag, "message");
13733
14084
  if (text === null)
13734
14085
  return out;
13735
- out.push(parentToolId ? { type: "tool-delta", toolId: parentToolId, text } : { type: "text-delta", text });
14086
+ out.push(parentToolId ? { type: "tool-delta", toolId: parentToolId, text } : { type: "text-delta", text, ...update.messageId ? { messageId: update.messageId } : {} });
13736
14087
  return out;
13737
14088
  }
13738
14089
  case "agent_thought_chunk": {
@@ -13768,7 +14119,7 @@ function mapUpdate(update, memos, tag) {
13768
14119
  ...spawn
13769
14120
  });
13770
14121
  if (update.status === "completed" || update.status === "failed")
13771
- out.push(endOf(update.toolCallId, memo, update.status));
14122
+ out.push(endOf(update.toolCallId, memo, update.status, sink));
13772
14123
  return out;
13773
14124
  }
13774
14125
  case "tool_call_update": {
@@ -13821,7 +14172,7 @@ function mapUpdate(update, memos, tag) {
13821
14172
  if (update.rawOutput !== undefined)
13822
14173
  memo.rawOutput = update.rawOutput;
13823
14174
  if ((update.status === "completed" || update.status === "failed") && !memo.ended) {
13824
- out.push(endOf(update.toolCallId, memo, update.status));
14175
+ out.push(endOf(update.toolCallId, memo, update.status, sink));
13825
14176
  }
13826
14177
  return out;
13827
14178
  }
@@ -13852,25 +14203,43 @@ function textOf(content, tag, what) {
13852
14203
  log.debug(tag, `acp: dropping non-text ${what} block (${content.type})`);
13853
14204
  return null;
13854
14205
  }
13855
- function endOf(toolId, memo, status) {
14206
+ function endOf(toolId, memo, status, sink) {
13856
14207
  memo.ended = true;
14208
+ const images = sink ? toolImages(memo.content, sink) : [];
13857
14209
  return {
13858
14210
  type: "tool-end",
13859
14211
  toolId,
13860
14212
  output: summarizeToolOutput(memo.content, memo.rawOutput),
13861
- isError: status === "failed" || exitCodeOf(memo.rawOutput) > 0
14213
+ isError: status === "failed" || exitCodeOf(memo.rawOutput) > 0,
14214
+ ...images.length ? { images } : {}
13862
14215
  };
13863
14216
  }
14217
+ function toolImages(content, sink) {
14218
+ const out = [];
14219
+ for (const c of content) {
14220
+ if (c.type !== "content" || c.content.type !== "image")
14221
+ continue;
14222
+ const img = toolImage(c.content.data, c.content.mimeType);
14223
+ if (!img)
14224
+ continue;
14225
+ sink(img.ref.file, img.bytes);
14226
+ out.push(img.ref);
14227
+ }
14228
+ return out;
14229
+ }
13864
14230
  function exitCodeOf(rawOutput) {
13865
14231
  const exit = rawOutput?.metadata?.exit;
13866
14232
  return typeof exit === "number" ? exit : 0;
13867
14233
  }
13868
14234
  function summarizeToolOutput(content, rawOutput) {
13869
14235
  const parts = [];
14236
+ let pictured = false;
13870
14237
  for (const c of content) {
13871
14238
  if (c.type === "content") {
13872
14239
  if (c.content.type === "text")
13873
14240
  parts.push(c.content.text);
14241
+ else if (c.content.type === "image")
14242
+ pictured = true;
13874
14243
  } else if (c.type === "diff") {
13875
14244
  const body = unifiedDiff(c.oldText ?? "", c.newText);
13876
14245
  if (body)
@@ -13882,6 +14251,8 @@ ${body}
13882
14251
  if (parts.length > 0)
13883
14252
  return truncate(parts.join(`
13884
14253
  `));
14254
+ if (pictured)
14255
+ return "";
13885
14256
  if (typeof rawOutput === "string")
13886
14257
  return truncate(rawOutput);
13887
14258
  if (rawOutput && typeof rawOutput === "object") {
@@ -13943,6 +14314,7 @@ class AcpSession {
13943
14314
  running = false;
13944
14315
  interrupted = false;
13945
14316
  stopped = false;
14317
+ held = false;
13946
14318
  conn = null;
13947
14319
  connecting = null;
13948
14320
  live = null;
@@ -14056,7 +14428,7 @@ class AcpSession {
14056
14428
  return log.warn(this.d.worktreeId, "send after close dropped");
14057
14429
  const { context, attachments } = opts;
14058
14430
  const item = { text, context, ...attachments?.length ? { attachments } : {} };
14059
- if ((this.running || this.own) && !this.interrupted && this.live?.conn.steering) {
14431
+ if ((this.running || this.own) && !this.interrupted && !this.held && this.live?.conn.steering) {
14060
14432
  return fireAndForget(this.d.worktreeId, this.steer(item), "agent steer");
14061
14433
  }
14062
14434
  this.enqueue(item);
@@ -14064,9 +14436,21 @@ class AcpSession {
14064
14436
  enqueue(item) {
14065
14437
  this.queue.push(item);
14066
14438
  this.queueChanged();
14067
- if (!this.running)
14439
+ if (!this.running && !this.held)
14068
14440
  fireAndForget(this.d.worktreeId, this.drain(), "agent drain");
14069
14441
  }
14442
+ hold() {
14443
+ this.held = true;
14444
+ let released = false;
14445
+ return () => {
14446
+ if (released)
14447
+ return;
14448
+ released = true;
14449
+ this.held = false;
14450
+ if (!this.running && this.queue.length > 0)
14451
+ fireAndForget(this.d.worktreeId, this.drain(), "agent drain");
14452
+ };
14453
+ }
14070
14454
  async steer(item) {
14071
14455
  item.recorded ??= await this.record(item);
14072
14456
  const live = this.live;
@@ -14144,7 +14528,7 @@ class AcpSession {
14144
14528
  this.setStatus("working");
14145
14529
  let item = null;
14146
14530
  try {
14147
- while (this.queue.length > 0) {
14531
+ while (this.queue.length > 0 && !this.held) {
14148
14532
  item = this.queue.shift();
14149
14533
  this.queueChanged();
14150
14534
  try {
@@ -14168,7 +14552,7 @@ class AcpSession {
14168
14552
  type: "agent-auth-required",
14169
14553
  agent: this.conn.spec.id,
14170
14554
  agentName: this.conn.spec.name,
14171
- methods: this.conn.authMethods.map(authMethodInfo),
14555
+ ...this.offeredMethods(this.conn),
14172
14556
  ...rejected ? { rejected: true } : {},
14173
14557
  ts: Date.now()
14174
14558
  });
@@ -14208,9 +14592,6 @@ class AcpSession {
14208
14592
  caps: { close: conn.closeSupported, delete: conn.deleteSupported },
14209
14593
  tag: this.d.worktreeId
14210
14594
  });
14211
- } catch (e) {
14212
- log.warn(this.d.worktreeId, "ask failed", e);
14213
- return null;
14214
14595
  } finally {
14215
14596
  this.asking--;
14216
14597
  this.maybeArmReaper();
@@ -14221,6 +14602,9 @@ class AcpSession {
14221
14602
  const method = conn.authMethods.find((m) => m.id === methodId);
14222
14603
  if (!method)
14223
14604
  throw new UserError(`${conn.spec.name} offers no login method "${methodId}"`);
14605
+ if (this.planWithheld(conn, methodId)) {
14606
+ throw new UserError("Signing in with a Claude plan is turned off by your organization's policy");
14607
+ }
14224
14608
  const login = terminalLogin(method);
14225
14609
  if (login) {
14226
14610
  const l = this.d.launch(conn.spec);
@@ -14256,6 +14640,14 @@ class AcpSession {
14256
14640
  if (item)
14257
14641
  this.send(item.text, item);
14258
14642
  }
14643
+ planWithheld(conn, methodId) {
14644
+ return this.d.planSignIn?.() === false && conn.spec.terminalLogins?.[methodId]?.plan === true;
14645
+ }
14646
+ offeredMethods(conn) {
14647
+ const kept = conn.authMethods.filter((m) => !this.planWithheld(conn, m.id));
14648
+ const methods2 = kept.map(authMethodInfo);
14649
+ return kept.length === conn.authMethods.length ? { methods: methods2 } : { methods: methods2, withheld: true };
14650
+ }
14259
14651
  rejectedCredential(e) {
14260
14652
  if (!this.conn?.authMethods.length)
14261
14653
  return false;
@@ -14517,7 +14909,7 @@ class AcpSession {
14517
14909
  }
14518
14910
  if (params.update.sessionUpdate === "config_option_update")
14519
14911
  this.absorb(live, params.update.configOptions);
14520
- const events = mapUpdate(params.update, live.tools, this.d.worktreeId);
14912
+ const events = mapUpdate(params.update, live.tools, this.d.worktreeId, (file, bytes) => fireAndForget(this.d.worktreeId, this.d.attachments.putToolImage(this.d.worktreeId, file, bytes), "tool image"));
14521
14913
  if (!this.running)
14522
14914
  this.trackOwn(events);
14523
14915
  for (const ev of events) {
@@ -14775,7 +15167,7 @@ function sameCommands(a, b) {
14775
15167
  }
14776
15168
 
14777
15169
  // ../daemon/src/agent/planDoc.ts
14778
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
15170
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
14779
15171
  import { join as join7 } from "path";
14780
15172
  var PLANS_DIR = join7(".toyon", "plans");
14781
15173
  var PLAN_NAME = /^(\d+)\.md$/;
@@ -14813,7 +15205,7 @@ function planEdited(cwd, rel, proposed) {
14813
15205
  if (!existsSync8(file))
14814
15206
  return false;
14815
15207
  try {
14816
- return readFileSync7(file, "utf8").trim() !== proposed.trim();
15208
+ return readFileSync8(file, "utf8").trim() !== proposed.trim();
14817
15209
  } catch (e) {
14818
15210
  log.warn(cwd, `could not read ${rel}`, e);
14819
15211
  return false;
@@ -15097,8 +15489,8 @@ function expandEnv(env, vars) {
15097
15489
 
15098
15490
  // ../daemon/src/core/remote.ts
15099
15491
  import { createHmac, timingSafeEqual } from "crypto";
15100
- import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
15101
- function loadRemote(file, env = process.env) {
15492
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
15493
+ function loadRemote(file, env = process.env, managed = MANAGED_DEFAULTS) {
15102
15494
  if (env.TOYON_CLOUD === "1") {
15103
15495
  const host = (env.TOYON_PUBLIC_HOST ?? "").toLowerCase();
15104
15496
  if (!isRemoteHost(host)) {
@@ -15112,11 +15504,19 @@ function loadRemote(file, env = process.env) {
15112
15504
  }
15113
15505
  if (!existsSync9(file))
15114
15506
  return null;
15115
- const view = parseRemote(readFileSync8(file, "utf8"));
15507
+ if (managed.remote === "off") {
15508
+ log.warn("remote", `${file} is ignored: remote access is turned off by your organization's policy`);
15509
+ return null;
15510
+ }
15511
+ const view = parseRemote(readFileSync9(file, "utf8"));
15116
15512
  if (!view) {
15117
15513
  log.warn("remote", `${file} names no valid host or previews; remote access is off`);
15118
15514
  return null;
15119
15515
  }
15516
+ if (managed.remote === "tailscale" && !view.host.endsWith(".ts.net")) {
15517
+ log.warn("remote", `${file} names ${view.host}; your organization's policy allows a tailnet name only, so remote access is off`);
15518
+ return null;
15519
+ }
15120
15520
  return { ...view, front: "local" };
15121
15521
  }
15122
15522
  function sameSecret(given, want) {
@@ -15485,31 +15885,50 @@ class PtyStream {
15485
15885
  // ../daemon/src/runtime/lines.ts
15486
15886
  var MAX_PENDING = 64 * 1024;
15487
15887
  var ANSI = /\u001b(?:\[[0-9;?]*[ -/]*[@-~]|\][^\u0007\u001b]*(?:\u0007|\u001b\\)|[@-Z\\-_])/g;
15888
+ var UP = /\u001b\[(\d*)[AF]/g;
15488
15889
  function clean(raw) {
15489
15890
  const parts = raw.replace(ANSI, "").split("\r");
15490
15891
  while (parts.length > 1 && !parts[parts.length - 1]?.trim())
15491
15892
  parts.pop();
15492
15893
  return (parts[parts.length - 1] ?? "").trimEnd();
15493
15894
  }
15895
+ function takeUps(raw) {
15896
+ let n = 0;
15897
+ const rest = raw.replace(UP, (_, count) => {
15898
+ n += count === "" ? 1 : Number(count);
15899
+ return "";
15900
+ });
15901
+ return [rest, n];
15902
+ }
15494
15903
 
15495
15904
  class LineSplitter {
15496
15905
  pending = "";
15497
15906
  feed(chunk) {
15498
15907
  this.pending += chunk;
15499
15908
  const out = [];
15909
+ const emit = (raw) => {
15910
+ const [rest, retract] = takeUps(raw);
15911
+ const line = clean(rest);
15912
+ if (line || retract)
15913
+ out.push({ line, retract });
15914
+ };
15500
15915
  for (let nl = this.pending.indexOf(`
15501
15916
  `);nl !== -1; nl = this.pending.indexOf(`
15502
15917
  `)) {
15503
- const line = clean(this.pending.slice(0, nl));
15918
+ const raw = this.pending.slice(0, nl);
15504
15919
  this.pending = this.pending.slice(nl + 1);
15505
- if (line)
15506
- out.push(line);
15920
+ emit(raw);
15507
15921
  }
15508
15922
  if (this.pending.length > MAX_PENDING) {
15509
- const line = clean(this.pending);
15923
+ const raw = this.pending;
15510
15924
  this.pending = "";
15511
- if (line)
15512
- out.push(line);
15925
+ emit(raw);
15926
+ return out;
15927
+ }
15928
+ const [rest, retract] = takeUps(this.pending);
15929
+ if (retract) {
15930
+ this.pending = rest;
15931
+ out.push({ line: "", retract });
15513
15932
  }
15514
15933
  return out;
15515
15934
  }
@@ -15655,8 +16074,8 @@ class WorktreeProcs {
15655
16074
  ring: PROC_RING
15656
16075
  }, (data) => {
15657
16076
  this.onData(name, data);
15658
- for (const line of mp.lines.feed(data))
15659
- this.pushLine(name, line);
16077
+ for (const ev of mp.lines.feed(data))
16078
+ this.pushLine(name, ev.line, ev.retract);
15660
16079
  }, (code) => this.handleExit(mp, code));
15661
16080
  } catch (e) {
15662
16081
  mp.state.status = "crashed";
@@ -15822,11 +16241,11 @@ class WorktreeProcs {
15822
16241
  states() {
15823
16242
  return [...this.procs.values()].map((p) => ({ ...p.state }));
15824
16243
  }
15825
- pushLine(proc, line) {
15826
- this.lines.push({ proc, line });
16244
+ pushLine(proc, line, retract = 0) {
16245
+ this.lines = applyLog(this.lines, proc, line, retract);
15827
16246
  if (this.lines.length > LOG_RING_SIZE)
15828
16247
  this.lines.shift();
15829
- this.onLog(proc, line);
16248
+ this.onLog(proc, line, retract);
15830
16249
  }
15831
16250
  }
15832
16251
  function execForm(command) {
@@ -15871,7 +16290,6 @@ function terminalEnv(base, own, urls) {
15871
16290
  }
15872
16291
  return { ...env, ...urls, TERM: "xterm-256color", COLORTERM: "truecolor", ...own };
15873
16292
  }
15874
- var DEFAULT_AGENT_ID = "claude";
15875
16293
  var PREVIEW_WAKE_MS = 8000;
15876
16294
  var PREVIEW_POLL_MS = 100;
15877
16295
  function defaultAgent(wt, d, preview, onProcess) {
@@ -15883,7 +16301,7 @@ function defaultAgent(wt, d, preview, onProcess) {
15883
16301
  spec: () => {
15884
16302
  const w = d.state.requireWorktree(wt.id);
15885
16303
  if (!w.agent) {
15886
- w.agent = d.state.defaultAgent ?? DEFAULT_AGENT_ID;
16304
+ w.agent = d.agents.defaultId(d.state.defaultAgent);
15887
16305
  d.state.save();
15888
16306
  d.hub.emit("worktreesChanged");
15889
16307
  }
@@ -15891,6 +16309,7 @@ function defaultAgent(wt, d, preview, onProcess) {
15891
16309
  },
15892
16310
  connect: (app, spec, prepared) => spawnAcp(app, d.agents.launch(spec, prepared), wt.path, wt.id),
15893
16311
  launch: (spec) => d.agents.command(spec),
16312
+ planSignIn: () => d.managed?.planSignIn ?? true,
15894
16313
  transcriptsDir: d.paths.transcriptsDir,
15895
16314
  attachments: d.attachments ?? new AttachmentStore(d.paths.attachmentsDir),
15896
16315
  getSessionId: () => d.state.session(wt.id),
@@ -16429,7 +16848,7 @@ class RuntimeRegistry {
16429
16848
  for (const g of rt.procs?.groups() ?? [])
16430
16849
  live.push(g);
16431
16850
  if (rt.agent.pgid !== null) {
16432
- const agentId = this.deps.state.worktree(id)?.agent ?? this.deps.state.defaultAgent ?? DEFAULT_AGENT_ID;
16851
+ const agentId = this.deps.state.worktree(id)?.agent ?? this.deps.agents.defaultId(this.deps.state.defaultAgent);
16433
16852
  live.push({ name: `agent:${agentId}`, pgid: rt.agent.pgid });
16434
16853
  }
16435
16854
  if (rt.shell?.alive)
@@ -16894,7 +17313,7 @@ function makePlanner(agents) {
16894
17313
  }
16895
17314
 
16896
17315
  // ../daemon/src/core/assets.ts
16897
- import { existsSync as existsSync11, readdirSync as readdirSync3, rmSync, statSync as statSync2 } from "fs";
17316
+ import { existsSync as existsSync11, readdirSync as readdirSync3, rmSync, statSync as statSync3 } from "fs";
16898
17317
  import { join as join9, resolve as resolve5 } from "path";
16899
17318
  function locateAssets(here, env = process.env) {
16900
17319
  const packaged = existsSync11(join9(here, "shell", "index.html"));
@@ -16918,7 +17337,7 @@ function pruneAssets(shellDist) {
16918
17337
  for (const name of names) {
16919
17338
  const path = join9(dir, name);
16920
17339
  try {
16921
- found.push({ path, mtime: statSync2(path).mtimeMs });
17340
+ found.push({ path, mtime: statSync3(path).mtimeMs });
16922
17341
  } catch {}
16923
17342
  }
16924
17343
  if (found.length === 0)
@@ -17025,7 +17444,7 @@ class Hub {
17025
17444
  }
17026
17445
 
17027
17446
  // ../daemon/src/core/restart.ts
17028
- import { spawn as spawn2 } from "child_process";
17447
+ import { spawn as spawn3 } from "child_process";
17029
17448
  import { openSync } from "fs";
17030
17449
  function restartable(env = process.env) {
17031
17450
  if (env.TOYON_WORKTREE !== undefined) {
@@ -17035,7 +17454,7 @@ function restartable(env = process.env) {
17035
17454
  }
17036
17455
  function respawn(logFile) {
17037
17456
  const logFd = openSync(logFile, "a");
17038
- const child = spawn2(process.execPath, process.argv.slice(1), {
17457
+ const child = spawn3(process.execPath, process.argv.slice(1), {
17039
17458
  detached: true,
17040
17459
  stdio: ["ignore", logFd, logFd],
17041
17460
  env: { ...process.env }
@@ -17177,7 +17596,7 @@ class SelfWatch {
17177
17596
 
17178
17597
  // ../daemon/src/core/state.ts
17179
17598
  import { randomBytes as randomBytes2 } from "crypto";
17180
- import { existsSync as existsSync13, readFileSync as readFileSync9, renameSync as renameSync2, writeFileSync as writeFileSync5 } from "fs";
17599
+ import { existsSync as existsSync13, readFileSync as readFileSync10, renameSync as renameSync2, writeFileSync as writeFileSync5 } from "fs";
17181
17600
  var GROUPS_SAVE_MS = 250;
17182
17601
  var SEEN_WORKTREES = 50;
17183
17602
  var empty = { repos: [], worktrees: [], sessions: {} };
@@ -17188,7 +17607,7 @@ function loadState(paths) {
17188
17607
  return structuredClone(empty);
17189
17608
  let raw;
17190
17609
  try {
17191
- raw = readFileSync9(STATE_FILE, "utf8");
17610
+ raw = readFileSync10(STATE_FILE, "utf8");
17192
17611
  } catch (e) {
17193
17612
  log.error("state", `cannot read ${STATE_FILE}; starting empty`, e);
17194
17613
  return structuredClone(empty);
@@ -17237,7 +17656,7 @@ function loadOrCreateToken(paths) {
17237
17656
  ensureDirs(paths);
17238
17657
  const TOKEN_FILE = paths.tokenFile;
17239
17658
  if (existsSync13(TOKEN_FILE))
17240
- return readFileSync9(TOKEN_FILE, "utf8").trim();
17659
+ return readFileSync10(TOKEN_FILE, "utf8").trim();
17241
17660
  const token = randomBytes2(32).toString("hex");
17242
17661
  writeFileSync5(TOKEN_FILE, token, { mode: 384 });
17243
17662
  return token;
@@ -17414,6 +17833,12 @@ class StateStore {
17414
17833
  this.state.defaultAgent = id;
17415
17834
  this.save();
17416
17835
  }
17836
+ clearDefaultAgent() {
17837
+ if (this.state.defaultAgent === undefined)
17838
+ return;
17839
+ delete this.state.defaultAgent;
17840
+ this.save();
17841
+ }
17417
17842
  get theme() {
17418
17843
  return this.state.theme;
17419
17844
  }
@@ -18018,7 +18443,7 @@ async function loadProjectTypescript(root) {
18018
18443
  }
18019
18444
 
18020
18445
  // ../daemon/src/drafts/store.ts
18021
- import { existsSync as existsSync14, readFileSync as readFileSync10, renameSync as renameSync3, writeFileSync as writeFileSync6 } from "fs";
18446
+ import { existsSync as existsSync14, readFileSync as readFileSync11, renameSync as renameSync3, writeFileSync as writeFileSync6 } from "fs";
18022
18447
  var SAVE_DELAY_MS = 1000;
18023
18448
 
18024
18449
  class DraftStore {
@@ -18030,7 +18455,7 @@ class DraftStore {
18030
18455
  if (!existsSync14(d.file))
18031
18456
  return;
18032
18457
  try {
18033
- const raw = JSON.parse(readFileSync10(d.file, "utf8"));
18458
+ const raw = JSON.parse(readFileSync11(d.file, "utf8"));
18034
18459
  if (!raw || typeof raw !== "object")
18035
18460
  return;
18036
18461
  for (const [id, text] of Object.entries(raw)) {
@@ -18106,6 +18531,7 @@ var OUTPUT_CAP = 200000;
18106
18531
  var TIMEOUT_MS = 10 * 60000;
18107
18532
  var KILL_GRACE_MS2 = 3000;
18108
18533
  var DRAIN_GRACE_MS = 500;
18534
+ var LIVE_AFTER_MS = 1500;
18109
18535
 
18110
18536
  class ExecService {
18111
18537
  deps;
@@ -18114,18 +18540,26 @@ class ExecService {
18114
18540
  constructor(deps) {
18115
18541
  this.deps = deps;
18116
18542
  }
18543
+ agentFor(worktreeId) {
18544
+ const wt = this.deps.state.requireWorktree(worktreeId);
18545
+ if (wt.kind === "spare")
18546
+ throw new UserError("no shell for a spare worktree");
18547
+ const agent = this.deps.runtime.agentFor(worktreeId);
18548
+ if (!agent)
18549
+ throw new UserError("worktree still starting; try again in a moment");
18550
+ return agent;
18551
+ }
18117
18552
  run(worktreeId, command) {
18553
+ const op = this.deps.shipping?.(worktreeId);
18554
+ if (op)
18555
+ throw new UserError(`${shipNoun(op.op)} is running here; run the command once it is done`);
18118
18556
  fireAndForget(worktreeId, this.exec(worktreeId, command), `exec ${command}`);
18119
18557
  }
18120
18558
  exec(worktreeId, command, name = SHELL_TOOL, opts = {}) {
18121
18559
  const wt = this.deps.state.requireWorktree(worktreeId);
18122
- if (wt.kind === "spare")
18123
- throw new UserError("no shell for a spare worktree");
18124
18560
  if (wt.kind === "main")
18125
18561
  throw new UserError("nothing runs on main: the plus starts a worktree for it");
18126
- const agent = this.deps.runtime.agentFor(worktreeId);
18127
- if (!agent)
18128
- throw new UserError("worktree still starting; try again in a moment");
18562
+ const agent = this.agentFor(worktreeId);
18129
18563
  const toolId = `${name}-${Date.now().toString(36)}-${++this.n}`;
18130
18564
  const start = { type: "tool-start", toolId, name, input: { command }, kind: "execute" };
18131
18565
  if (!opts.quiet)
@@ -18149,22 +18583,58 @@ class ExecService {
18149
18583
  return Promise.resolve({ exit: reason, text: "" });
18150
18584
  }
18151
18585
  const timer = setTimeout(() => this.kill(worktreeId, toolId), TIMEOUT_MS);
18152
- this.track(worktreeId, toolId, { proc, timer });
18586
+ const stop = () => {
18587
+ proc.kill("SIGTERM");
18588
+ setTimeout(() => {
18589
+ if (this.running.get(worktreeId)?.has(toolId))
18590
+ proc.kill("SIGKILL");
18591
+ }, KILL_GRACE_MS2);
18592
+ };
18593
+ this.track(worktreeId, toolId, { stop, timer });
18153
18594
  this.deps.runtime.hold(worktreeId, `exec:${toolId}`);
18154
18595
  return this.collect(worktreeId, toolId, proc, agent, opts.quiet ? start : undefined);
18155
18596
  }
18156
- record(worktreeId, command, text, exit) {
18157
- const wt = this.deps.state.requireWorktree(worktreeId);
18158
- if (wt.kind === "spare")
18159
- throw new UserError("no shell for a spare worktree");
18160
- const agent = this.deps.runtime.agentFor(worktreeId);
18161
- if (!agent)
18162
- throw new UserError("worktree still starting; try again in a moment");
18597
+ async watch(worktreeId, command, run) {
18598
+ const agent = this.agentFor(worktreeId);
18163
18599
  const toolId = `${SHELL_TOOL}-${Date.now().toString(36)}-${++this.n}`;
18164
- agent.note({ type: "tool-start", toolId, name: SHELL_TOOL, input: { command }, kind: "execute" });
18165
- const truncated = text.length > OUTPUT_CAP;
18166
- const shown = truncated ? text.slice(0, OUTPUT_CAP) : text;
18167
- agent.note({ type: "tool-end", toolId, output: formatOutput(shown, exit, truncated), isError: exit !== 0 });
18600
+ const ctl = new AbortController;
18601
+ this.track(worktreeId, toolId, { stop: () => ctl.abort() });
18602
+ const start = { type: "tool-start", toolId, name: SHELL_TOOL, input: { command }, kind: "execute" };
18603
+ let text = "";
18604
+ let truncated = false;
18605
+ let up = false;
18606
+ const raise = () => {
18607
+ if (up)
18608
+ return;
18609
+ up = true;
18610
+ agent.note(start);
18611
+ if (text)
18612
+ agent.note({ type: "tool-delta", toolId, text });
18613
+ };
18614
+ const timer = setTimeout(raise, this.deps.liveAfterMs ?? LIVE_AFTER_MS);
18615
+ const onText = (chunk) => {
18616
+ if (truncated)
18617
+ return;
18618
+ const take = chunk.slice(0, OUTPUT_CAP - text.length);
18619
+ text += take;
18620
+ if (take.length < chunk.length)
18621
+ truncated = true;
18622
+ if (up && take)
18623
+ agent.note({ type: "tool-delta", toolId, text: take });
18624
+ };
18625
+ let r;
18626
+ try {
18627
+ r = await run(onText, ctl.signal);
18628
+ } finally {
18629
+ clearTimeout(timer);
18630
+ this.untrack(worktreeId, toolId);
18631
+ }
18632
+ if (!up && r.exit === 0)
18633
+ return { ...r, shown: false };
18634
+ if (!up)
18635
+ agent.note(start);
18636
+ agent.note({ type: "tool-end", toolId, output: formatOutput(text, r.exit, truncated), isError: r.exit !== 0 });
18637
+ return { ...r, shown: true };
18168
18638
  }
18169
18639
  stop(worktreeId) {
18170
18640
  for (const toolId of this.running.get(worktreeId)?.keys() ?? [])
@@ -18187,18 +18657,12 @@ class ExecService {
18187
18657
  return r;
18188
18658
  }
18189
18659
  kill(worktreeId, toolId) {
18190
- const r = this.running.get(worktreeId)?.get(toolId);
18191
- if (!r)
18192
- return;
18193
- r.proc.kill("SIGTERM");
18194
- setTimeout(() => {
18195
- if (this.running.get(worktreeId)?.has(toolId))
18196
- r.proc.kill("SIGKILL");
18197
- }, KILL_GRACE_MS2);
18660
+ this.running.get(worktreeId)?.get(toolId)?.stop();
18198
18661
  }
18199
18662
  async collect(worktreeId, toolId, proc, agent, heldStart) {
18200
18663
  let text = "";
18201
18664
  let truncated = false;
18665
+ const live = !heldStart;
18202
18666
  const drain = async (stream) => {
18203
18667
  if (!stream || typeof stream === "number")
18204
18668
  return;
@@ -18210,11 +18674,13 @@ class ExecService {
18210
18674
  return;
18211
18675
  if (truncated)
18212
18676
  continue;
18213
- text += decoder.decode(value, { stream: true });
18214
- if (text.length > OUTPUT_CAP) {
18215
- text = text.slice(0, OUTPUT_CAP);
18677
+ const chunk = decoder.decode(value, { stream: true });
18678
+ const take = chunk.slice(0, OUTPUT_CAP - text.length);
18679
+ text += take;
18680
+ if (take.length < chunk.length)
18216
18681
  truncated = true;
18217
- }
18682
+ if (live && take)
18683
+ agent.note({ type: "tool-delta", toolId, text: take });
18218
18684
  }
18219
18685
  };
18220
18686
  let exit = null;
@@ -18259,7 +18725,7 @@ ${body}
18259
18725
  }
18260
18726
 
18261
18727
  // ../daemon/src/files/service.ts
18262
- import { spawn as spawn3 } from "child_process";
18728
+ import { spawn as spawn4 } from "child_process";
18263
18729
  import { stat, unlink } from "fs/promises";
18264
18730
 
18265
18731
  // ../daemon/src/git/lock.ts
@@ -18797,7 +19263,7 @@ class FileService {
18797
19263
  }
18798
19264
  async viewableFile(worktreeId, path) {
18799
19265
  const r = this.readable(worktreeId);
18800
- if (!r || viewerOf(path) === null)
19266
+ if (!r || viewerOf(path) === null && !isPageAsset(path))
18801
19267
  return null;
18802
19268
  try {
18803
19269
  const target = resolveInside(r.path, path);
@@ -18920,7 +19386,7 @@ class FileService {
18920
19386
  revealPath(target) {
18921
19387
  if (process.platform !== "darwin")
18922
19388
  throw new UserError("reveal is only available on macOS");
18923
- const child = spawn3("open", ["-R", target], { stdio: "ignore" });
19389
+ const child = spawn4("open", ["-R", target], { stdio: "ignore" });
18924
19390
  child.on("error", () => {});
18925
19391
  child.unref();
18926
19392
  }
@@ -19030,11 +19496,11 @@ function runSetup(cmd, cwd, onLine, extra = {}) {
19030
19496
  args: ["-c", cmd],
19031
19497
  ring: SETUP_RING
19032
19498
  }, (data) => {
19033
- for (const line of lines.feed(data))
19034
- onLine(line);
19499
+ for (const ev of lines.feed(data))
19500
+ onLine(ev.line, ev.retract);
19035
19501
  }, resolve);
19036
19502
  } catch (e) {
19037
- onLine(`could not run: ${e instanceof Error ? e.message : String(e)}`);
19503
+ onLine(`could not run: ${e instanceof Error ? e.message : String(e)}`, 0);
19038
19504
  resolve(1);
19039
19505
  }
19040
19506
  });
@@ -19084,9 +19550,10 @@ class AfterLand {
19084
19550
  for (const command of commands) {
19085
19551
  this.tellMain(repo, `afterLand: ${command}`);
19086
19552
  let last = "";
19087
- const code = await runSetup(command, repo.path, (line) => {
19088
- last = line;
19089
- this.tellMain(repo, line);
19553
+ const code = await runSetup(command, repo.path, (line, retract) => {
19554
+ if (line)
19555
+ last = line;
19556
+ this.tellMain(repo, line, retract);
19090
19557
  });
19091
19558
  if (code === 0)
19092
19559
  continue;
@@ -19106,10 +19573,10 @@ class AfterLand {
19106
19573
  this.d.hub.emit("failed", main.id, notice);
19107
19574
  }
19108
19575
  }
19109
- tellMain(repo, line) {
19576
+ tellMain(repo, line, retract) {
19110
19577
  const main = this.mainOf(repo);
19111
19578
  if (main)
19112
- this.d.hub.emit("log", main.id, "land", line);
19579
+ this.d.hub.emit("log", main.id, "land", line, retract);
19113
19580
  }
19114
19581
  mainOf(repo) {
19115
19582
  return this.d.state.worktrees.find((w) => w.repoId === repo.id && w.kind === "main");
@@ -19122,7 +19589,7 @@ import { homedir as homedir7 } from "os";
19122
19589
  import { basename as basename3, dirname as dirname6, join as join16 } from "path";
19123
19590
 
19124
19591
  // ../daemon/src/repos/browse.ts
19125
- import { existsSync as existsSync15, statSync as statSync3 } from "fs";
19592
+ import { existsSync as existsSync15, statSync as statSync4 } from "fs";
19126
19593
  import { readdir } from "fs/promises";
19127
19594
  import { homedir as homedir5 } from "os";
19128
19595
  import { dirname as dirname4, join as join12 } from "path";
@@ -19151,7 +19618,7 @@ var NOWHERE = { exists: false, isDir: false, isRepo: false, parentExists: false
19151
19618
  function describe2(typed, parent) {
19152
19619
  let isDir = false;
19153
19620
  try {
19154
- isDir = statSync3(typed).isDirectory();
19621
+ isDir = statSync4(typed).isDirectory();
19155
19622
  } catch {}
19156
19623
  return {
19157
19624
  exists: existsSync15(typed),
@@ -19189,7 +19656,7 @@ async function browsePath(raw) {
19189
19656
  }
19190
19657
 
19191
19658
  // ../daemon/src/repos/config.ts
19192
- import { closeSync, existsSync as existsSync16, openSync as openSync2, readdirSync as readdirSync4, readFileSync as readFileSync11, readSync } from "fs";
19659
+ import { closeSync, existsSync as existsSync16, openSync as openSync2, readdirSync as readdirSync4, readFileSync as readFileSync12, readSync } from "fs";
19193
19660
  import { join as join13 } from "path";
19194
19661
 
19195
19662
  // ../../node_modules/.bun/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
@@ -20579,7 +21046,7 @@ function readConfigFile(repoPath) {
20579
21046
  for (const rel of files) {
20580
21047
  let raw;
20581
21048
  try {
20582
- raw = parseSettings(readFileSync11(join13(repoPath, rel), "utf8"));
21049
+ raw = parseSettings(readFileSync12(join13(repoPath, rel), "utf8"));
20583
21050
  } catch (e) {
20584
21051
  return { ok: false, reason: `${rel} is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
20585
21052
  }
@@ -20641,7 +21108,7 @@ function configBody(repoPath, rel, config) {
20641
21108
  return config;
20642
21109
  let shared;
20643
21110
  try {
20644
- shared = parseSettings(readFileSync11(abs, "utf8"));
21111
+ shared = parseSettings(readFileSync12(abs, "utf8"));
20645
21112
  } catch {
20646
21113
  return config;
20647
21114
  }
@@ -20654,7 +21121,7 @@ function configText(repoPath, rel, config) {
20654
21121
  let text;
20655
21122
  let was;
20656
21123
  try {
20657
- text = readFileSync11(join13(repoPath, rel), "utf8");
21124
+ text = readFileSync12(join13(repoPath, rel), "utf8");
20658
21125
  was = parseSettings(text);
20659
21126
  } catch {
20660
21127
  return fresh;
@@ -20686,7 +21153,7 @@ function detectConfig(repoPath) {
20686
21153
  log.warn("config", `${repoPath}: ${explicit.reason}; detecting instead`);
20687
21154
  const pkgPath = join13(repoPath, "package.json");
20688
21155
  if (existsSync16(pkgPath)) {
20689
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
21156
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf8"));
20690
21157
  const scripts = pkg.scripts ?? {};
20691
21158
  const runner = detectRunner(repoPath, pkg.packageManager);
20692
21159
  const pages = detectPages(repoPath, scripts, runner);
@@ -20832,7 +21299,7 @@ function detectPages(repoPath, scripts, runner) {
20832
21299
  const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join13(repoPath, f)).find((f) => existsSync16(f));
20833
21300
  if (!cfg)
20834
21301
  return null;
20835
- const out = readFileSync11(cfg, "utf8").match(/pages_build_output_dir"?\s*[:=]\s*"([^"]+)"/)?.[1];
21302
+ const out = readFileSync12(cfg, "utf8").match(/pages_build_output_dir"?\s*[:=]\s*"([^"]+)"/)?.[1];
20836
21303
  if (!out)
20837
21304
  return null;
20838
21305
  return { web: `${runner} run build && npx wrangler pages dev ${out} --ip 127.0.0.1 --port $PORT` };
@@ -20848,7 +21315,7 @@ function detectRunner(repoPath, packageManager) {
20848
21315
  }
20849
21316
 
20850
21317
  // ../daemon/src/repos/create.ts
20851
- import { existsSync as existsSync17, readdirSync as readdirSync5, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
21318
+ import { existsSync as existsSync17, readdirSync as readdirSync5, realpathSync as realpathSync2, statSync as statSync5 } from "fs";
20852
21319
  import { appendFile as appendFile2, mkdir as mkdir2, rm } from "fs/promises";
20853
21320
  import { homedir as homedir6 } from "os";
20854
21321
  import { dirname as dirname5, join as join14, resolve as resolve7 } from "path";
@@ -20879,7 +21346,7 @@ function planInPlace(opts) {
20879
21346
  throw new UserError(`${opts.name} is not a folder name`);
20880
21347
  }
20881
21348
  const typed = join14(resolveParent(opts.parent), name);
20882
- if (!existsSync17(typed) || !statSync4(typed).isDirectory())
21349
+ if (!existsSync17(typed) || !statSync5(typed).isDirectory())
20883
21350
  throw new UserError(`${typed} is not a folder`);
20884
21351
  const dir = realpathSync2(typed);
20885
21352
  if (existsSync17(join14(dir, ".git")))
@@ -20895,7 +21362,7 @@ function resolveParent(raw) {
20895
21362
  throw new UserError(`${raw} is not a folder path`);
20896
21363
  if (!existsSync17(typed))
20897
21364
  throw new UserError(`${raw} does not exist; make it first`);
20898
- if (!statSync4(typed).isDirectory())
21365
+ if (!statSync5(typed).isDirectory())
20899
21366
  throw new UserError(`${raw} is not a folder`);
20900
21367
  return realpathSync2(typed);
20901
21368
  }
@@ -22824,7 +23291,7 @@ class RouteService {
22824
23291
  }
22825
23292
 
22826
23293
  // ../daemon/src/runtime/bridge-script.ts
22827
- import { existsSync as existsSync20, readFileSync as readFileSync12, statSync as statSync5 } from "fs";
23294
+ import { existsSync as existsSync20, readFileSync as readFileSync13, statSync as statSync6 } from "fs";
22828
23295
  var LEARNED_MAX = 8;
22829
23296
 
22830
23297
  class BridgeScript {
@@ -22856,14 +23323,14 @@ class BridgeScript {
22856
23323
  get() {
22857
23324
  if (!existsSync20(this.path))
22858
23325
  return "// toyon bridge not built";
22859
- const s = statSync5(this.path);
23326
+ const s = statSync6(this.path);
22860
23327
  const stamp = `${s.mtimeMs}:${s.size}`;
22861
23328
  if (this.cache && stamp === this.stamp)
22862
23329
  return this.cache;
22863
23330
  this.stamp = stamp;
22864
23331
  const origins = [...this.origins, ...this.learned];
22865
23332
  this.cache = `window.__toyonShellOrigins=${JSON.stringify(origins)};
22866
- ${readFileSync12(this.path, "utf8")}`;
23333
+ ${readFileSync13(this.path, "utf8")}`;
22867
23334
  return this.cache;
22868
23335
  }
22869
23336
  }
@@ -23183,7 +23650,7 @@ class PairCodes {
23183
23650
  }
23184
23651
 
23185
23652
  // ../daemon/src/agent/config.ts
23186
- import { existsSync as existsSync21, readFileSync as readFileSync13 } from "fs";
23653
+ import { existsSync as existsSync21, readFileSync as readFileSync14 } from "fs";
23187
23654
  import { homedir as homedir8 } from "os";
23188
23655
  import { join as join18 } from "path";
23189
23656
  function agentConfigFiles(spec, repoPath, home = homedir8()) {
@@ -23226,7 +23693,7 @@ function readJson(path) {
23226
23693
  if (!existsSync21(path))
23227
23694
  return null;
23228
23695
  try {
23229
- const v = JSON.parse(readFileSync13(path, "utf8"));
23696
+ const v = JSON.parse(readFileSync14(path, "utf8"));
23230
23697
  return v && typeof v === "object" && !Array.isArray(v) ? v : null;
23231
23698
  } catch (e) {
23232
23699
  log.warn("agent-config", `${path} is not valid JSON`, e);
@@ -23248,7 +23715,7 @@ function codexServers(path) {
23248
23715
  return [];
23249
23716
  const out = [];
23250
23717
  let current = null;
23251
- for (const raw of readFileSync13(path, "utf8").split(`
23718
+ for (const raw of readFileSync14(path, "utf8").split(`
23252
23719
  `)) {
23253
23720
  const line = raw.trim();
23254
23721
  const header = /^\[mcp_servers\.("?)([^\]"]+)\1\]$/.exec(line);
@@ -24259,6 +24726,7 @@ function createFetch(opts) {
24259
24726
  pid: process.pid,
24260
24727
  branded: opts.branded(),
24261
24728
  remote: remote && { host: remote.host, previews: remote.previews },
24729
+ managed: opts.managed,
24262
24730
  ...opts.metrics()
24263
24731
  });
24264
24732
  return async function fetch2(req, srv) {
@@ -24371,6 +24839,8 @@ function createFetch(opts) {
24371
24839
  return new Response("unauthorized", { status: 401 });
24372
24840
  const [worktreeId, file, extra] = url.pathname.slice("/attachments/".length).split("/");
24373
24841
  const places = worktreeId && file && !extra ? [opts.attachments.fileFor(worktreeId, file), opts.archivedAttachment(worktreeId, file)] : [];
24842
+ if (worktreeId && file)
24843
+ await opts.attachments.whenWritten(worktreeId, file);
24374
24844
  const path = places.find((p) => !!p && existsSync22(p));
24375
24845
  if (!path)
24376
24846
  return new Response("not found", { status: 404 });
@@ -24523,7 +24993,7 @@ function startServer(opts) {
24523
24993
  s.hub.on("reposChanged", () => broadcast({ t: "repos", repos: s.state.repos }));
24524
24994
  s.hub.on("pendingChanged", () => broadcast({ t: "pending-repos", pending: s.repos.pending }));
24525
24995
  s.hub.on("proc", (worktreeId, proc) => broadcast({ t: "proc", worktreeId, proc }));
24526
- s.hub.on("log", (worktreeId, proc, line) => sendTo(worktreeId, { t: "log", worktreeId, proc, line }));
24996
+ s.hub.on("log", (worktreeId, proc, line, retract) => sendTo(worktreeId, retract ? { t: "log", worktreeId, proc, line, retract } : { t: "log", worktreeId, proc, line }));
24527
24997
  s.hub.on("queue", (worktreeId, items) => sendTo(worktreeId, { t: "queue", worktreeId, items }));
24528
24998
  s.hub.on("agentCommands", (worktreeId, commands) => sendTo(worktreeId, { t: "agent-commands", worktreeId, commands }));
24529
24999
  s.hub.on("termData", (worktreeId, stream, data) => queueTerm(worktreeId, stream, data));
@@ -24603,7 +25073,7 @@ function startServer(opts) {
24603
25073
  const agentsMsg = () => ({
24604
25074
  t: "agents",
24605
25075
  agents: agentInfos(),
24606
- defaultAgent: s.state.defaultAgent ?? DEFAULT_AGENT_ID,
25076
+ defaultAgent: s.agents.defaultId(s.state.defaultAgent),
24607
25077
  agentChosen: s.state.defaultAgent !== undefined
24608
25078
  });
24609
25079
  s.hub.on("agentsChanged", () => broadcast(agentsMsg()));
@@ -24626,7 +25096,7 @@ function startServer(opts) {
24626
25096
  themes: s.themes.themes,
24627
25097
  themePrefs: s.themes.prefs,
24628
25098
  agents: agentInfos(),
24629
- defaultAgent: s.state.defaultAgent ?? DEFAULT_AGENT_ID,
25099
+ defaultAgent: s.agents.defaultId(s.state.defaultAgent),
24630
25100
  agentChosen: s.state.defaultAgent !== undefined,
24631
25101
  home: homedir9(),
24632
25102
  folderDialog: process.platform === "darwin" && !cloud.enabled,
@@ -24637,7 +25107,8 @@ function startServer(opts) {
24637
25107
  visits: s.routes.historyAll(),
24638
25108
  self: s.self.get(),
24639
25109
  update: s.update.get(),
24640
- drafts: s.drafts.all()
25110
+ drafts: s.drafts.all(),
25111
+ managed: managedView(opts.managed)
24641
25112
  };
24642
25113
  };
24643
25114
  let branded = false;
@@ -24655,6 +25126,7 @@ function startServer(opts) {
24655
25126
  metrics,
24656
25127
  noteShellOrigin: opts.noteShellOrigin,
24657
25128
  remote: opts.remote,
25129
+ managed: { source: opts.managed.source, hash: opts.managed.hash },
24658
25130
  preview: (id) => s.runtime.get(id)?.proxy?.handler ?? null,
24659
25131
  bootstrap: helloFrame,
24660
25132
  restart: (now) => s.restarter.request({ now }),
@@ -24753,7 +25225,7 @@ function startServer(opts) {
24753
25225
  };
24754
25226
  const server = Bun.serve({ ...serverConfig, port: opts.port });
24755
25227
  let brandedServer = null;
24756
- if (opts.port !== 80 && !cloud.enabled) {
25228
+ if (opts.port !== 80 && !cloud.enabled && opts.managed.policy.brandedListener) {
24757
25229
  try {
24758
25230
  brandedServer = Bun.serve({ ...serverConfig, hostname: "0.0.0.0", port: 80 });
24759
25231
  branded = true;
@@ -24770,7 +25242,7 @@ function startServer(opts) {
24770
25242
  }
24771
25243
 
24772
25244
  // ../daemon/src/themes/store.ts
24773
- import { existsSync as existsSync23, readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync6, writeFileSync as writeFileSync8 } from "fs";
25245
+ import { existsSync as existsSync23, readdirSync as readdirSync6, readFileSync as readFileSync15, statSync as statSync7, writeFileSync as writeFileSync8 } from "fs";
24774
25246
  import { homedir as homedir10 } from "os";
24775
25247
  import { basename as basename4, dirname as dirname7, join as join20, resolve as resolve8 } from "path";
24776
25248
  var home = homedir10();
@@ -24870,7 +25342,7 @@ class ThemeStore {
24870
25342
  }
24871
25343
  }
24872
25344
  function readJsonc(file) {
24873
- return parse6(readFileSync14(file, "utf8"), [], { allowTrailingComma: true });
25345
+ return parse6(readFileSync15(file, "utf8"), [], { allowTrailingComma: true });
24874
25346
  }
24875
25347
  function readVscodeTheme(file, depth = 0) {
24876
25348
  const json = readJsonc(file);
@@ -24927,9 +25399,9 @@ function discover(dir) {
24927
25399
  const extDir = join20(dir, name);
24928
25400
  const pkgFile = join20(extDir, "package.json");
24929
25401
  try {
24930
- if (!statSync6(extDir).isDirectory() || !existsSync23(pkgFile))
25402
+ if (!statSync7(extDir).isDirectory() || !existsSync23(pkgFile))
24931
25403
  continue;
24932
- const pkg = JSON.parse(readFileSync14(pkgFile, "utf8"));
25404
+ const pkg = JSON.parse(readFileSync15(pkgFile, "utf8"));
24933
25405
  const themes = pkg.contributes?.themes;
24934
25406
  if (!Array.isArray(themes))
24935
25407
  continue;
@@ -24938,7 +25410,7 @@ function discover(dir) {
24938
25410
  const nlsFile = join20(extDir, "package.nls.json");
24939
25411
  if (existsSync23(nlsFile)) {
24940
25412
  try {
24941
- nls = JSON.parse(readFileSync14(nlsFile, "utf8"));
25413
+ nls = JSON.parse(readFileSync15(nlsFile, "utf8"));
24942
25414
  } catch {}
24943
25415
  }
24944
25416
  for (const c of themes) {
@@ -25052,7 +25524,7 @@ class UpdateService {
25052
25524
  this.announce();
25053
25525
  }
25054
25526
  async check() {
25055
- if (this.d.method === "none" || this.d.managed)
25527
+ if (this.d.method === "none" || this.d.managedBy)
25056
25528
  return;
25057
25529
  const answer = await this.d.latest();
25058
25530
  if (answer.version === null) {
@@ -25065,8 +25537,7 @@ class UpdateService {
25065
25537
  await this.auto();
25066
25538
  }
25067
25539
  async updateNow() {
25068
- if (this.d.managed)
25069
- throw new UserError("Updates are turned off for this machine (TOYON_UPDATES=off)");
25540
+ this.refuseManaged();
25070
25541
  const target = this.target();
25071
25542
  if (target === null)
25072
25543
  throw new UserError("Toyon is up to date");
@@ -25078,8 +25549,7 @@ class UpdateService {
25078
25549
  await this.proceed();
25079
25550
  }
25080
25551
  async checkNow() {
25081
- if (this.d.managed)
25082
- throw new UserError("Updates are turned off for this machine (TOYON_UPDATES=off)");
25552
+ this.refuseManaged();
25083
25553
  if (this.d.method === "none") {
25084
25554
  throw new UserError(`Toyon ${this.d.running} runs from a checkout, which does not update itself`);
25085
25555
  }
@@ -25108,10 +25578,16 @@ class UpdateService {
25108
25578
  };
25109
25579
  }
25110
25580
  status() {
25111
- return { managed: this.d.managed, unreachable: this.unreachable, latest: this.latest };
25581
+ return { managedBy: this.d.managedBy, unreachable: this.unreachable, latest: this.latest };
25582
+ }
25583
+ refuseManaged() {
25584
+ if (this.d.managedBy === "policy")
25585
+ throw new UserError("Updates are turned off by your organization's policy");
25586
+ if (this.d.managedBy === "env")
25587
+ throw new UserError("Updates are turned off for this machine (TOYON_UPDATES=off)");
25112
25588
  }
25113
25589
  async auto() {
25114
- if (this.wanted || this.installing || this.d.managed)
25590
+ if (this.wanted || this.installing || this.d.managedBy)
25115
25591
  return;
25116
25592
  const target = this.target();
25117
25593
  if (target === null)
@@ -25313,6 +25789,7 @@ class LandingService {
25313
25789
  d;
25314
25790
  judging = new Map;
25315
25791
  busy = new Set;
25792
+ stopping = false;
25316
25793
  constructor(d) {
25317
25794
  this.d = d;
25318
25795
  d.hub.on("turnSettled", (id, turn) => fireAndForget(id, this.settle(id, turn), "landing verdict"));
@@ -25332,6 +25809,16 @@ class LandingService {
25332
25809
  this.judging.delete(worktreeId);
25333
25810
  this.d.worktrees.setLanding(worktreeId, undefined);
25334
25811
  }
25812
+ boot() {
25813
+ for (const wt of this.d.state.worktrees) {
25814
+ if (wt.landing?.check !== "pending" || !canLand(wt))
25815
+ continue;
25816
+ fireAndForget(wt.id, this.run(wt, { at: wt.landing.at, ask: true, quiet: false }), "landing verdict resumed");
25817
+ }
25818
+ }
25819
+ stop() {
25820
+ this.stopping = true;
25821
+ }
25335
25822
  async settle(worktreeId, turn) {
25336
25823
  const wt = this.d.state.worktree(worktreeId);
25337
25824
  if (!wt || !canLand(wt))
@@ -25409,7 +25896,7 @@ class LandingService {
25409
25896
  this.judging.set(worktreeId, opts.at);
25410
25897
  const started = Date.now();
25411
25898
  this.d.worktrees.setLanding(worktreeId, { at: opts.at, check: "pending", ready: false, fingerprint: "", ...kept });
25412
- const live = () => this.judging.get(worktreeId) === opts.at && !!this.d.state.worktree(worktreeId);
25899
+ const live = () => !this.stopping && this.judging.get(worktreeId) === opts.at && !!this.d.state.worktree(worktreeId);
25413
25900
  let check = "none";
25414
25901
  let checkTail;
25415
25902
  const command = repo.config.check?.trim();
@@ -25434,7 +25921,10 @@ class LandingService {
25434
25921
  try {
25435
25922
  verdict = await this.d.judge(wt, prompt);
25436
25923
  } catch (e) {
25437
- log.warn(worktreeId, "landing verdict failed", e);
25924
+ if (!live())
25925
+ return;
25926
+ log.warn(worktreeId, "landing verdict unanswered; no verdict written", e);
25927
+ return this.clear(worktreeId);
25438
25928
  }
25439
25929
  if (!live())
25440
25930
  return;
@@ -25597,7 +26087,7 @@ async function mergedBranches(repoPath, defaultBr) {
25597
26087
  }
25598
26088
 
25599
26089
  // ../daemon/src/worktrees/discover.ts
25600
- import { createHash as createHash2 } from "crypto";
26090
+ import { createHash as createHash3 } from "crypto";
25601
26091
  import { basename as basename5 } from "path";
25602
26092
 
25603
26093
  // ../daemon/src/git/worktrees.ts
@@ -25666,7 +26156,7 @@ async function listWorktrees(repoPath) {
25666
26156
 
25667
26157
  // ../daemon/src/worktrees/discover.ts
25668
26158
  function discoveredId(path) {
25669
- return `disc-${createHash2("sha1").update(canonical(path)).digest("hex").slice(0, 12)}`;
26159
+ return `disc-${createHash3("sha1").update(canonical(path)).digest("hex").slice(0, 12)}`;
25670
26160
  }
25671
26161
  function knownPaths(known) {
25672
26162
  const paths = new Set;
@@ -25849,12 +26339,15 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
25849
26339
  }
25850
26340
 
25851
26341
  // ../daemon/src/worktrees/service.ts
25852
- import { cpSync, existsSync as existsSync26, readFileSync as readFileSync16, rmSync as rmSync5 } from "fs";
26342
+ import { cpSync, existsSync as existsSync26, readFileSync as readFileSync17, rmSync as rmSync5 } from "fs";
26343
+ import { rm as rm2 } from "fs/promises";
25853
26344
  import { dirname as dirname9, join as join23 } from "path";
25854
26345
 
25855
26346
  // ../daemon/src/git/archive.ts
25856
26347
  import { rmSync as rmSync3 } from "fs";
25857
26348
  var archiveRef = (worktreeId) => `refs/toyon/archive/${worktreeId}`;
26349
+ var SNAPSHOT_SUBJECT = "toyon: uncommitted work";
26350
+ var SNAPSHOT_AUTHOR = "toyon";
25858
26351
  async function keepState(repoPath, wtPath, ref, indexFile) {
25859
26352
  const head = await git(wtPath, "rev-parse", "--verify", "HEAD");
25860
26353
  if (!head.ok)
@@ -25882,7 +26375,7 @@ async function snapshotCommit(wtPath, head, indexFile) {
25882
26375
  return null;
25883
26376
  const commit = await run(GIT, [
25884
26377
  "-c",
25885
- "user.name=toyon",
26378
+ `user.name=${SNAPSHOT_AUTHOR}`,
25886
26379
  "-c",
25887
26380
  "user.email=toyon@localhost",
25888
26381
  "commit-tree",
@@ -25890,13 +26383,29 @@ async function snapshotCommit(wtPath, head, indexFile) {
25890
26383
  "-p",
25891
26384
  head,
25892
26385
  "-m",
25893
- "toyon: uncommitted work"
26386
+ SNAPSHOT_SUBJECT
25894
26387
  ], wtPath);
25895
26388
  return commit.ok ? commit.out : null;
25896
26389
  } finally {
25897
26390
  rmSync3(indexFile, { force: true });
25898
26391
  }
25899
26392
  }
26393
+ async function keptAt(repoPath, ref) {
26394
+ const r = await git(repoPath, "log", "-1", "--format=%H%n%P%n%an%n%s", ref);
26395
+ if (!r.ok)
26396
+ return null;
26397
+ const [commit, parents, author, subject] = r.out.split(`
26398
+ `);
26399
+ if (!commit)
26400
+ return null;
26401
+ const snapshot = subject === SNAPSHOT_SUBJECT && author === SNAPSHOT_AUTHOR && parents && !parents.includes(" ");
26402
+ if (!snapshot)
26403
+ return { head: commit };
26404
+ const files = await git(repoPath, "diff", "--name-only", parents, commit);
26405
+ const dirty = files.ok ? files.out.split(`
26406
+ `).filter(Boolean).length : 0;
26407
+ return { head: parents, snapshot: commit, ...dirty ? { dirty } : {} };
26408
+ }
25900
26409
  async function checkOutKept(repoPath, wtPath, kept, branch) {
25901
26410
  await git(repoPath, "worktree", "prune");
25902
26411
  if (branch.create)
@@ -25956,16 +26465,27 @@ async function dropLandRefs(repoPath, worktreeId) {
25956
26465
  }
25957
26466
 
25958
26467
  // ../daemon/src/git/land.ts
25959
- async function commitWorktree(worktreePath, message) {
26468
+ var STEP_TIMEOUT_MS = 10 * 60000;
26469
+ var stepRun = (cwd, args, onText, signal) => runWatched(GIT, args, cwd, { env: NO_PROMPT, onText, timeoutMs: STEP_TIMEOUT_MS, signal });
26470
+ var UNWATCHED = {
26471
+ step() {},
26472
+ git: (cwd, args) => stepRun(cwd, args)
26473
+ };
26474
+ function refused(what, r, n = 200) {
26475
+ if (r.exit === "timeout") {
26476
+ const minutes = Math.round(STEP_TIMEOUT_MS / 60000);
26477
+ return `${what} gave up after ${minutes} minutes${r.shown ? "; what it printed is on the chat" : ""}`;
26478
+ }
26479
+ return `${what}: ${r.shown ? "what git and its hooks printed is on the chat" : r.err.slice(0, n)}`;
26480
+ }
26481
+ async function commitWorktree(worktreePath, message, w = UNWATCHED) {
25960
26482
  if ((await statusFiles(worktreePath)).length === 0)
25961
26483
  return { ok: false, message: "nothing to commit" };
25962
26484
  await git(worktreePath, "add", "-A");
25963
- const c = await git(worktreePath, "commit", "-m", message);
25964
- if (!c.ok) {
25965
- const output = [c.out, c.err].filter(Boolean).join(`
25966
- `);
25967
- return { ok: false, message: `commit failed: ${c.err.slice(0, 200)}`, output, exit: c.exit };
25968
- }
26485
+ w.step("committing");
26486
+ const c = await w.git(worktreePath, ["commit", "-m", message]);
26487
+ if (!c.ok)
26488
+ return { ok: false, message: refused("commit refused", c) };
25969
26489
  return { ok: true, message: `committed: ${message}` };
25970
26490
  }
25971
26491
  async function requireClean(worktreePath) {
@@ -25984,7 +26504,7 @@ async function requireMainReady(repoPath, defaultBr) {
25984
26504
  }
25985
26505
  return null;
25986
26506
  }
25987
- async function takeMainIn(worktreePath, defaultBr, own) {
26507
+ async function takeMainIn(worktreePath, defaultBr, own, w = UNWATCHED) {
25988
26508
  const cErr = await requireClean(worktreePath);
25989
26509
  if (cErr)
25990
26510
  return cErr;
@@ -25992,26 +26512,27 @@ async function takeMainIn(worktreePath, defaultBr, own) {
25992
26512
  if (behind === 0)
25993
26513
  return { ok: true, message: `already up to date with ${defaultBr}` };
25994
26514
  if (own) {
25995
- const r = await git(worktreePath, "rebase", defaultBr);
26515
+ w.step(`rebasing onto ${defaultBr}`);
26516
+ const r = await w.git(worktreePath, ["rebase", defaultBr]);
25996
26517
  if (!r.ok) {
25997
26518
  await git(worktreePath, "rebase", "--abort");
25998
- const said = [r.out, r.err].filter(Boolean).join(`
25999
- `);
26519
+ const ask = `rebase onto ${defaultBr} conflicts: ask the agent to bring ${defaultBr} in and resolve them`;
26000
26520
  return {
26001
26521
  ok: false,
26002
26522
  conflict: true,
26003
- message: `rebase onto ${defaultBr} conflicts: ask the agent to bring ${defaultBr} in and resolve them (${said.slice(0, 200)})`
26523
+ message: r.shown ? `${ask}; the conflicts are on the chat` : `${ask} (${r.text.slice(0, 200)})`
26004
26524
  };
26005
26525
  }
26006
26526
  return { ok: true, message: `rebased onto ${defaultBr}, ${behind} commit(s) behind before` };
26007
26527
  }
26008
- const m = await git(worktreePath, "merge", "--no-edit", defaultBr);
26528
+ w.step(`merging ${defaultBr} in`);
26529
+ const m = await w.git(worktreePath, ["merge", "--no-edit", defaultBr]);
26009
26530
  if (!m.ok) {
26010
26531
  return mergeFailure(worktreePath, m, `sync conflicts with ${defaultBr}: ask the agent to merge ${defaultBr} and resolve them`);
26011
26532
  }
26012
26533
  return { ok: true, message: `synced ${behind} commit(s) from ${defaultBr}` };
26013
26534
  }
26014
- async function landLocally(worktreePath, branch, repoPath, defaultBr, method, message) {
26535
+ async function landLocally(worktreePath, branch, repoPath, defaultBr, method, message, w = UNWATCHED) {
26015
26536
  const cErr = await requireClean(worktreePath);
26016
26537
  if (cErr)
26017
26538
  return cErr;
@@ -26022,25 +26543,28 @@ async function landLocally(worktreePath, branch, repoPath, defaultBr, method, me
26022
26543
  if (mErr)
26023
26544
  return mErr;
26024
26545
  if (method === "rebase") {
26025
- const ff = await git(repoPath, "merge", "--ff-only", branch);
26546
+ w.step(`moving ${defaultBr} onto the branch`);
26547
+ const ff = await w.git(repoPath, ["merge", "--ff-only", branch]);
26026
26548
  if (!ff.ok)
26027
26549
  return { ok: false, message: `${branch} is not a fast-forward of ${defaultBr}: take ${defaultBr} in first` };
26028
26550
  return { ok: true, message: `${defaultBr} moved onto ${branch}` };
26029
26551
  }
26030
26552
  if (method === "squash") {
26031
- const s = await git(repoPath, "merge", "--squash", branch);
26553
+ w.step(`squashing onto ${defaultBr}`);
26554
+ const s = await w.git(repoPath, ["merge", "--squash", branch]);
26032
26555
  if (!s.ok) {
26033
26556
  await git(repoPath, "reset", "--hard", "HEAD");
26034
26557
  return mergeFailure(repoPath, s, `merge conflicts with ${defaultBr}: take ${defaultBr} in first`);
26035
26558
  }
26036
- const c = await git(repoPath, "commit", "-m", message);
26559
+ const c = await w.git(repoPath, ["commit", "-m", message]);
26037
26560
  if (!c.ok) {
26038
26561
  await git(repoPath, "reset", "--hard", "HEAD");
26039
- return { ok: false, message: `squash commit failed: ${c.err.slice(0, 200)}` };
26562
+ return { ok: false, message: refused("squash commit refused", c) };
26040
26563
  }
26041
26564
  return { ok: true, message: `squashed ${branch} onto ${defaultBr}` };
26042
26565
  }
26043
- const m = await git(repoPath, "merge", "--no-ff", "--no-edit", branch);
26566
+ w.step(`merging into ${defaultBr}`);
26567
+ const m = await w.git(repoPath, ["merge", "--no-ff", "--no-edit", branch]);
26044
26568
  if (!m.ok)
26045
26569
  return mergeFailure(repoPath, m, `merge conflicts with ${defaultBr}: take ${defaultBr} in first`);
26046
26570
  return { ok: true, message: `merged ${branch} into ${defaultBr}` };
@@ -26057,20 +26581,21 @@ async function squashMessage(worktreePath, defaultBr, suggested) {
26057
26581
  const subjects = await git(worktreePath, "log", "--format=%s", `${defaultBr}..HEAD`);
26058
26582
  return subjects.out.trim() || "land";
26059
26583
  }
26060
- async function pullMain(repoPath, defaultBr) {
26584
+ async function pullMain(repoPath, defaultBr, w = UNWATCHED) {
26061
26585
  const cErr = await requireClean(repoPath);
26062
26586
  if (cErr)
26063
26587
  return cErr;
26064
- return fastForwardMain(repoPath, defaultBr);
26588
+ return fastForwardMain(repoPath, defaultBr, w);
26065
26589
  }
26066
- async function fastForwardMain(repoPath, defaultBr) {
26590
+ async function fastForwardMain(repoPath, defaultBr, w = UNWATCHED) {
26067
26591
  const current = await git(repoPath, "branch", "--show-current");
26068
26592
  if (current.out !== defaultBr) {
26069
26593
  return { ok: false, message: `main checkout is on '${current.out}', not ${defaultBr}; switch it first` };
26070
26594
  }
26071
- const f = await run(GIT, ["fetch", "--quiet"], repoPath, NO_PROMPT);
26595
+ w.step(`pulling ${defaultBr} from origin`);
26596
+ const f = await w.git(repoPath, ["fetch", "--quiet"]);
26072
26597
  if (!f.ok)
26073
- return { ok: false, message: `fetch failed: ${f.err.slice(0, 200)}` };
26598
+ return { ok: false, message: refused("fetch failed", f) };
26074
26599
  return fastForwardFetched(repoPath, defaultBr);
26075
26600
  }
26076
26601
  async function fastForwardFetched(repoPath, defaultBr) {
@@ -26103,45 +26628,77 @@ ${m.err}`);
26103
26628
  }
26104
26629
  return { ok: true, moved: true, message: `pulled ${behind} commit(s) from origin` };
26105
26630
  }
26106
- async function pushMain(repoPath, defaultBr) {
26107
- const p = await run(GIT, ["push", "origin", defaultBr], repoPath, NO_PROMPT);
26631
+ async function pushMain(repoPath, defaultBr, w = UNWATCHED) {
26632
+ w.step(`pushing ${defaultBr}`);
26633
+ const p = await w.git(repoPath, ["push", "origin", defaultBr]);
26108
26634
  if (p.ok)
26109
26635
  return { ok: true, message: `pushed ${defaultBr} to origin` };
26110
- const moved = /rejected|fetch first|non-fast-forward/.test(p.err);
26636
+ const moved = /\[rejected\]|fetch first|non-fast-forward/.test(p.err);
26111
26637
  return {
26112
26638
  ok: false,
26113
- message: moved ? `${defaultBr} on origin moved; pull ${defaultBr} and land again` : `push failed: ${p.err.slice(0, 300)}`
26639
+ message: moved ? `${defaultBr} on origin moved; pull ${defaultBr} and land again` : refused("push failed", p, 300)
26114
26640
  };
26115
26641
  }
26116
26642
  async function mergeFailure(cwd, m, conflictMessage) {
26117
- const said = [m.out, m.err].filter(Boolean).join(`
26118
- `);
26119
- const conflict = /CONFLICT|Automatic merge failed/.test(said);
26643
+ const conflict = /CONFLICT|Automatic merge failed/.test(m.text);
26120
26644
  if (conflict) {
26121
26645
  await git(cwd, "merge", "--abort");
26122
- return { ok: false, conflict: true, message: `${conflictMessage} (${said.slice(0, 200)})` };
26646
+ return {
26647
+ ok: false,
26648
+ conflict: true,
26649
+ message: m.shown ? `${conflictMessage}; the conflicts are on the chat` : `${conflictMessage} (${m.text.slice(0, 200)})`
26650
+ };
26651
+ }
26652
+ return {
26653
+ ok: false,
26654
+ message: m.shown ? "merge failed: what git printed is on the chat" : `merge failed: ${m.text.slice(0, 300)}`
26655
+ };
26656
+ }
26657
+ async function fetchTrunk(worktreePath, defaultBr, w = UNWATCHED) {
26658
+ const [remote, up] = await Promise.all([
26659
+ git(worktreePath, "config", "--get", `branch.${defaultBr}.remote`),
26660
+ git(worktreePath, "rev-parse", "--abbrev-ref", `${defaultBr}@{upstream}`)
26661
+ ]);
26662
+ if (!remote.ok || !remote.out || !up.ok || !up.out) {
26663
+ return { ok: true, base: defaultBr, message: `${defaultBr} has no upstream` };
26123
26664
  }
26124
- return { ok: false, message: `merge failed: ${said.slice(0, 300)}` };
26665
+ w.step(`fetching ${up.out}`);
26666
+ const f = await w.git(worktreePath, ["fetch", "--quiet", remote.out, defaultBr]);
26667
+ if (!f.ok)
26668
+ return { ok: false, base: up.out, message: refused("fetch failed", f) };
26669
+ return { ok: true, base: up.out, message: `fetched ${up.out}` };
26125
26670
  }
26126
- async function pushBranch(worktreePath, branch) {
26127
- const push = await run(GIT, ["push", "-u", "--force-with-lease", "origin", branch], worktreePath, NO_PROMPT);
26671
+ async function pushBranch(worktreePath, branch, w = UNWATCHED) {
26672
+ const args = ["push", "-u", "--force-with-lease", "origin", branch];
26673
+ w.step(`pushing ${branch}`);
26674
+ let push = await w.git(worktreePath, args);
26675
+ if (!push.ok && /stale info/.test(push.err) && await goneFromOrigin(worktreePath, branch)) {
26676
+ await git(worktreePath, "update-ref", "-d", `refs/remotes/origin/${branch}`);
26677
+ push = await w.git(worktreePath, args);
26678
+ }
26128
26679
  if (!push.ok)
26129
- return { ok: false, message: `push failed: ${push.err.slice(0, 300)}` };
26680
+ return { ok: false, message: refused("push failed", push, 300) };
26130
26681
  return { ok: true, message: `pushed ${branch}` };
26131
26682
  }
26132
- async function openPr(o) {
26683
+ async function goneFromOrigin(worktreePath, branch) {
26684
+ const r = await run(GIT, ["ls-remote", "--exit-code", "--heads", "origin", branch], worktreePath, NO_PROMPT);
26685
+ return !r.ok && r.exit === 2;
26686
+ }
26687
+ async function openPr(o, w = UNWATCHED) {
26133
26688
  const cErr = await requireClean(o.worktreePath);
26134
26689
  if (cErr)
26135
26690
  return cErr;
26136
- const { ahead } = await aheadBehind(o.worktreePath, o.defaultBr);
26691
+ const base = o.base ?? o.defaultBr;
26692
+ const { ahead } = await aheadBehind(o.worktreePath, base);
26137
26693
  if (ahead === 0)
26138
- return { ok: false, message: `nothing to ship: no commits ahead of ${o.defaultBr}` };
26694
+ return { ok: false, message: `nothing to ship: no commits ahead of ${base}` };
26139
26695
  const remote = await git(o.worktreePath, "remote", "get-url", "origin");
26140
26696
  if (!remote.ok)
26141
26697
  return { ok: false, message: "no 'origin' remote: add one, or land by merging here" };
26142
- const push = await pushBranch(o.worktreePath, o.branch);
26698
+ const push = await pushBranch(o.worktreePath, o.branch, w);
26143
26699
  if (!push.ok)
26144
26700
  return push;
26701
+ w.step("opening the PR");
26145
26702
  const titled = o.subject ? ["--title", o.subject, "--body", o.body ?? ""] : ["--fill"];
26146
26703
  const created = await gh(["pr", "create", "--head", o.branch, "--base", o.defaultBr, ...titled], o.worktreePath);
26147
26704
  let url = null;
@@ -26174,7 +26731,8 @@ ${created.exit}`)) {
26174
26731
  pr: { number, url, state: "open", ...o.automerge ? { automerge: true } : {} }
26175
26732
  };
26176
26733
  }
26177
- async function mergePr(worktreePath, number, wanted) {
26734
+ async function mergePr(worktreePath, number, wanted, w = UNWATCHED) {
26735
+ w.step("merging the PR");
26178
26736
  const method = await ghMethod(worktreePath, wanted);
26179
26737
  if (!method)
26180
26738
  return { ok: false, message: "the repo allows no merge method gh can use" };
@@ -26191,7 +26749,7 @@ function compareUrl(remoteUrl, base, branch) {
26191
26749
  }
26192
26750
 
26193
26751
  // ../daemon/src/worktrees/archive.ts
26194
- import { existsSync as existsSync24, mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync15, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "fs";
26752
+ import { existsSync as existsSync24, mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync16, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "fs";
26195
26753
  import { basename as basename6, dirname as dirname8, join as join21 } from "path";
26196
26754
  class WorktreeArchive {
26197
26755
  dir;
@@ -26204,7 +26762,7 @@ class WorktreeArchive {
26204
26762
  if (!existsSync24(file))
26205
26763
  continue;
26206
26764
  try {
26207
- this.records.set(id, JSON.parse(readFileSync15(file, "utf8")));
26765
+ this.records.set(id, JSON.parse(readFileSync16(file, "utf8")));
26208
26766
  } catch (e) {
26209
26767
  log.warn("archive", `skipped an unreadable record: ${file}`, e);
26210
26768
  }
@@ -26254,7 +26812,7 @@ class WorktreeArchive {
26254
26812
  if (!existsSync24(file))
26255
26813
  return null;
26256
26814
  try {
26257
- return readFileSync15(file, "utf8");
26815
+ return readFileSync16(file, "utf8");
26258
26816
  } catch (e) {
26259
26817
  log.warn(id, `could not read its archived plan ${rel}`, e);
26260
26818
  return null;
@@ -26293,7 +26851,7 @@ function summarize(r, repoId, transcript) {
26293
26851
  function lastUsage(transcript) {
26294
26852
  if (!existsSync24(transcript))
26295
26853
  return null;
26296
- const lines = readFileSync15(transcript, "utf8").split(`
26854
+ const lines = readFileSync16(transcript, "utf8").split(`
26297
26855
  `);
26298
26856
  for (let i = lines.length - 1;i >= 0; i--) {
26299
26857
  if (!lines[i]?.includes('"usage"'))
@@ -26309,7 +26867,7 @@ function lastUsage(transcript) {
26309
26867
  function firstPrompt(transcript) {
26310
26868
  if (!existsSync24(transcript))
26311
26869
  return;
26312
- for (const line of readFileSync15(transcript, "utf8").split(`
26870
+ for (const line of readFileSync16(transcript, "utf8").split(`
26313
26871
  `)) {
26314
26872
  if (!line.includes('"user-message"'))
26315
26873
  continue;
@@ -26541,8 +27099,10 @@ class SparePool {
26541
27099
  this.d.state.save();
26542
27100
  for (const cmd of repo.config.setup ?? []) {
26543
27101
  const tail = [];
26544
- const code = await runSetup(cmd, wt.path, (line) => {
26545
- tail.push(line);
27102
+ const code = await runSetup(cmd, wt.path, (line, retract) => {
27103
+ tail.splice(Math.max(0, tail.length - retract), retract);
27104
+ if (line)
27105
+ tail.push(line);
26546
27106
  if (tail.length > 20)
26547
27107
  tail.shift();
26548
27108
  }, worktreeEnv(wt, repo));
@@ -26770,6 +27330,15 @@ function stopOf(prev, next) {
26770
27330
  // ../daemon/src/worktrees/service.ts
26771
27331
  var LOCAL_CONFIG_FILES = [".env", ".env.local", ".env.development", ".env.development.local", ".dev.vars"];
26772
27332
  var BASE_IGNORE = "base .gitignore";
27333
+ async function deleteTree(path) {
27334
+ try {
27335
+ await rm2(path, { recursive: true, force: true });
27336
+ return true;
27337
+ } catch (e) {
27338
+ log.warn("rm", `could not delete ${path}`, e);
27339
+ return false;
27340
+ }
27341
+ }
26773
27342
  function withoutAttachments(event) {
26774
27343
  if (event.type !== "user-message")
26775
27344
  return event;
@@ -26783,16 +27352,20 @@ function withCarry(context, c) {
26783
27352
  const line = c.moved ? `This worktree started with ${files} the person moved here from ${c.branch} by hand. They are part of the task, not something to clean up.` : `${c.branch} has ${files} the person chose to leave there. This worktree does not have them.`;
26784
27353
  return [...context ?? [], line];
26785
27354
  }
27355
+ var commandLine = (args) => args.map((a) => /^[\w./:=@,+-]+$/.test(a) ? a : JSON.stringify(a.split(`
27356
+ `, 1)[0])).join(" ");
26786
27357
 
26787
27358
  class WorktreeService {
26788
27359
  d;
26789
27360
  spare;
27361
+ ops = new Map;
26790
27362
  trunk;
26791
27363
  archive;
26792
27364
  archiving = new Map;
26793
27365
  going = new Set;
26794
27366
  countsCache = new Map;
26795
27367
  handChecked = new Map;
27368
+ naming = new Set;
26796
27369
  discoverCache = new Map;
26797
27370
  usage = new Map;
26798
27371
  constructor(d) {
@@ -26842,6 +27415,12 @@ class WorktreeService {
26842
27415
  });
26843
27416
  d.hub.on("turnSettled", (worktreeId, turn) => {
26844
27417
  this.countsCache.delete(worktreeId);
27418
+ if (turn.end === "done") {
27419
+ const wt = d.state.worktree(worktreeId);
27420
+ const prompt = wt?.unnamed ? firstAskOf(d.runtime.agentFor(worktreeId)?.transcript() ?? []) : undefined;
27421
+ if (wt && prompt)
27422
+ this.scheduleNaming(wt, prompt, wt.variant);
27423
+ }
26845
27424
  if (turn.end !== "done" && turn.end !== "stopped")
26846
27425
  return;
26847
27426
  for (const p of d.runtime.get(worktreeId)?.procs?.states() ?? []) {
@@ -27139,16 +27718,13 @@ class WorktreeService {
27139
27718
  scheduleNaming(wt, prompt, variant) {
27140
27719
  if (!prompt.trim())
27141
27720
  return;
27142
- if (!variant) {
27143
- fireAndForget(wt.id, (this.d.namer ?? makeNamer(this.d.runtime))(prompt, wt).then((name) => {
27144
- if (name)
27145
- return this.rename(wt.id, name);
27146
- }), "auto-naming");
27721
+ if (variant && variant.index !== 1)
27147
27722
  return;
27148
- }
27149
- if (variant.index !== 1)
27723
+ if (this.naming.has(wt.id))
27150
27724
  return;
27151
- fireAndForget(wt.id, (this.d.namer ?? makeNamer(this.d.runtime))(prompt, wt).then(async (name) => {
27725
+ this.naming.add(wt.id);
27726
+ const asked = (this.d.namer ?? makeNamer(this.d.runtime))(prompt, wt);
27727
+ const applied = variant ? asked.then(async (name) => {
27152
27728
  if (!name)
27153
27729
  return;
27154
27730
  for (const sibling of [wt, ...siblingsOf(wt, this.d.state.worktrees)]) {
@@ -27156,7 +27732,11 @@ class WorktreeService {
27156
27732
  log.warn(sibling.id, "variant rename failed", e);
27157
27733
  });
27158
27734
  }
27159
- }), "auto-naming");
27735
+ }) : asked.then((name) => {
27736
+ if (name)
27737
+ return this.rename(wt.id, name);
27738
+ });
27739
+ fireAndForget(wt.id, applied.finally(() => this.naming.delete(wt.id)), "auto-naming");
27160
27740
  }
27161
27741
  archiveWorktree(worktreeId, opts = {}) {
27162
27742
  const running = this.archiving.get(worktreeId);
@@ -27198,7 +27778,7 @@ class WorktreeService {
27198
27778
  if (archive)
27199
27779
  this.keepPlans(wt);
27200
27780
  try {
27201
- await gitOrThrow(repo.path, "worktree", "remove", "--force", wt.path);
27781
+ await this.removeDirectory(repo, wt);
27202
27782
  } catch (e) {
27203
27783
  rmSync5(this.plansScratch(wt.id), { recursive: true, force: true });
27204
27784
  throw e;
@@ -27226,13 +27806,41 @@ class WorktreeService {
27226
27806
  }
27227
27807
  async keep(repo, wt) {
27228
27808
  const index = join23(this.d.paths.archiveDir, `${wt.id}.index`);
27229
- const kept = await keepState(repo.path, wt.path, archiveRef(wt.id), index);
27809
+ const kept = await keepState(repo.path, wt.path, archiveRef(wt.id), index) ?? await this.keepUnlinked(repo, wt);
27230
27810
  if (!kept)
27231
27811
  log.warn(wt.id, "could not keep its git state");
27232
27812
  else if (kept.lost)
27233
27813
  log.warn(wt.id, "could not keep its uncommitted changes: only its commits are archived");
27234
27814
  return kept;
27235
27815
  }
27816
+ async keepUnlinked(repo, wt) {
27817
+ if (await this.listed(repo, wt))
27818
+ return null;
27819
+ const ref = archiveRef(wt.id);
27820
+ const already = await keptAt(repo.path, ref);
27821
+ if (already)
27822
+ return already;
27823
+ const head = await commitOf(repo.path, `refs/heads/${wt.branch}`);
27824
+ if (!head || !(await git(repo.path, "update-ref", ref, head)).ok)
27825
+ return null;
27826
+ return { head };
27827
+ }
27828
+ async listed(repo, wt) {
27829
+ const path = canonical(wt.path);
27830
+ return (await listWorktrees(repo.path)).some((g) => canonical(g.path) === path);
27831
+ }
27832
+ async removeDirectory(repo, wt) {
27833
+ const r = await git(repo.path, "worktree", "remove", "--force", wt.path);
27834
+ if (r.ok)
27835
+ return;
27836
+ if (await this.listed(repo, wt))
27837
+ throw new Error(`git worktree remove --force ${wt.path} failed (${r.exit}): ${r.err}`);
27838
+ if (await deleteTree(wt.path))
27839
+ return;
27840
+ await run("chmod", ["-R", "u+w", wt.path], dirname9(wt.path));
27841
+ if (!await deleteTree(wt.path))
27842
+ log.warn(wt.id, `its directory could not be deleted and stays at ${wt.path}`);
27843
+ }
27236
27844
  plansScratch(worktreeId) {
27237
27845
  return join23(this.d.paths.archiveDir, `${worktreeId}.plans`);
27238
27846
  }
@@ -27536,7 +28144,7 @@ class WorktreeService {
27536
28144
  const src = join23(repo.path, ".gitignore");
27537
28145
  const tracked = existsSync26(src) && (await git(repo.path, "ls-files", "--", ".gitignore")).out !== "";
27538
28146
  const mirror = existsSync26(src) && !tracked;
27539
- const lines = mirror ? readFileSync16(src, "utf8").split(`
28147
+ const lines = mirror ? readFileSync17(src, "utf8").split(`
27540
28148
  `).filter((l) => l.trim() !== "") : [];
27541
28149
  await excludeBlock(repo.path, BASE_IGNORE, lines);
27542
28150
  }
@@ -27570,7 +28178,7 @@ class WorktreeService {
27570
28178
  log.warn(wt.id, `could not copy ${f}`, r.err);
27571
28179
  }
27572
28180
  for (const cmd of setupCommands ? repo.config.setup ?? [] : []) {
27573
- const code = await runSetup(cmd, wt.path, (line) => this.d.hub.emit("log", wt.id, "setup", line), worktreeEnv(wt, repo));
28181
+ const code = await runSetup(cmd, wt.path, (line, retract) => this.d.hub.emit("log", wt.id, "setup", line, retract), worktreeEnv(wt, repo));
27574
28182
  if (code !== 0)
27575
28183
  this.d.hub.emit("log", wt.id, "setup", `setup failed (exit ${code}): ${cmd}`);
27576
28184
  }
@@ -27631,23 +28239,29 @@ ${wt.landing.body}` : s;
27631
28239
  return this.commitRecorded(wt, this.commitMessage(wt, typed));
27632
28240
  }
27633
28241
  async commitRecorded(wt, message) {
27634
- const result = await commitWorktree(wt.path, message);
28242
+ const result = await commitWorktree(wt.path, message, this.landWatch(wt));
27635
28243
  if (result.ok) {
27636
28244
  await this.verdictSurvives(wt, { dropMessage: true });
27637
28245
  this.headMoved(wt.id);
27638
- return result;
27639
- }
27640
- if (!result.output?.trim() || !this.d.record)
27641
- return result;
27642
- const subject = message.split(`
27643
- `, 1)[0] ?? "";
27644
- try {
27645
- this.d.record(wt.id, `git commit -m ${JSON.stringify(subject)}`, result.output, result.exit ?? null);
27646
- } catch (e) {
27647
- log.warn(wt.id, `could not put the refused commit on the transcript: ${e instanceof Error ? e.message : e}`);
27648
- return result;
27649
28246
  }
27650
- return { ...result, message: "commit refused: what git and its hooks printed is on the chat" };
28247
+ return result;
28248
+ }
28249
+ landWatch(wt) {
28250
+ return {
28251
+ step: (name) => this.step(wt.id, name),
28252
+ git: async (cwd, args) => {
28253
+ const where = cwd === wt.path ? "git" : `git -C ${cwd}`;
28254
+ const run = (onText, signal) => stepRun(cwd, args, onText, signal);
28255
+ if (!this.d.watch)
28256
+ return run(() => {});
28257
+ try {
28258
+ return await this.d.watch(wt.id, `${where} ${commandLine(args)}`, run);
28259
+ } catch (e) {
28260
+ log.warn(wt.id, `landing step runs unwatched: ${e instanceof Error ? e.message : e}`);
28261
+ return run(() => {});
28262
+ }
28263
+ }
28264
+ };
27651
28265
  }
27652
28266
  async verdictSurvives(wt, opts) {
27653
28267
  const l = wt.landing;
@@ -27667,25 +28281,62 @@ ${wt.landing.body}` : s;
27667
28281
  }
27668
28282
  async land(worktreeId, message) {
27669
28283
  const { wt, repo } = this.landable(worktreeId, "land");
28284
+ return this.ship(wt.id, "land", () => this.landOp(wt, repo, message));
28285
+ }
28286
+ shippingOf(id) {
28287
+ return this.ops.get(id);
28288
+ }
28289
+ async ship(id, op, run) {
28290
+ const out = this.ops.get(id);
28291
+ if (out)
28292
+ throw new UserError(`${shipNoun(out.op)} is already running here`);
28293
+ const release = this.d.runtime.agentFor(id)?.hold();
28294
+ this.ops.set(id, { op });
28295
+ this.d.hub.emit("worktreesChanged");
28296
+ try {
28297
+ return await run();
28298
+ } finally {
28299
+ this.ops.delete(id);
28300
+ release?.();
28301
+ this.d.hub.emit("worktreesChanged");
28302
+ }
28303
+ }
28304
+ step(id, name) {
28305
+ const out = this.ops.get(id);
28306
+ if (!out || out.step === name)
28307
+ return;
28308
+ out.step = name;
28309
+ this.d.hub.emit("worktreesChanged");
28310
+ }
28311
+ async landOp(wt, repo, message) {
27670
28312
  const policy = landPolicy(repo.config);
27671
28313
  const own = hasOwnBranch(wt);
28314
+ const w = this.landWatch(wt);
27672
28315
  const suggested = wt.landing?.subject ? wt.landing.body ? `${wt.landing.subject}
27673
28316
 
27674
28317
  ${wt.landing.body}` : wt.landing.subject : undefined;
27675
28318
  if (policy.land === "pr") {
28319
+ if (wt.pr?.state === "merged" && !wt.landed) {
28320
+ const r = await this.prMergedOp(wt.id, undefined, w);
28321
+ return { result: r.ok ? r : { ...r, message: `PR #${wt.pr.number} merged, but ${r.message}` } };
28322
+ }
28323
+ const trunk = await fetchTrunk(wt.path, repo.defaultBranch, w);
28324
+ if (!trunk.ok)
28325
+ return { result: trunk };
28326
+ const base = trunk.base;
27676
28327
  if (wt.pr?.state === "open") {
27677
28328
  const dirty = (await statusFiles(wt.path)).length > 0;
27678
28329
  const missing = dirty || (await aheadUpstream(wt.path) ?? 0) > 0;
27679
28330
  if (!missing)
27680
- return { result: await mergePr(wt.path, wt.pr.number, policy.merge) };
28331
+ return { result: await mergePr(wt.path, wt.pr.number, policy.merge, w) };
27681
28332
  const committed = await this.commitIfDirty(wt, message);
27682
28333
  if (committed && !committed.ok)
27683
28334
  return { result: committed };
27684
- const taken = await takeMainIn(wt.path, repo.defaultBranch, own);
28335
+ const taken = await takeMainIn(wt.path, base, own, w);
27685
28336
  if (!taken.ok)
27686
28337
  return { result: taken };
27687
28338
  this.headMoved(wt.id);
27688
- const pushed = await pushBranch(wt.path, wt.branch);
28339
+ const pushed = await pushBranch(wt.path, wt.branch, w);
27689
28340
  if (!pushed.ok)
27690
28341
  return { result: pushed };
27691
28342
  this.headMoved(wt.id);
@@ -27696,19 +28347,26 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27696
28347
  const committed = await this.commitIfDirty(wt, message);
27697
28348
  if (committed && !committed.ok)
27698
28349
  return { result: committed };
27699
- const taken = await takeMainIn(wt.path, repo.defaultBranch, own);
28350
+ const mark = await landingMark(wt.path, repo.defaultBranch);
28351
+ const taken = await takeMainIn(wt.path, base, own, w);
27700
28352
  if (!taken.ok)
27701
28353
  return { result: taken };
27702
28354
  this.headMoved(wt.id);
28355
+ if ((await aheadBehind(wt.path, base)).ahead === 0) {
28356
+ const r = await this.prMergedOp(wt.id, mark, w);
28357
+ const on = `origin's ${repo.defaultBranch} has this work already`;
28358
+ return { result: { ...r, message: r.ok ? `${on}; ${r.message}` : `${on}, but ${r.message}` } };
28359
+ }
27703
28360
  const result = await openPr({
27704
28361
  worktreePath: wt.path,
27705
28362
  branch: wt.branch,
27706
28363
  defaultBr: repo.defaultBranch,
28364
+ base,
27707
28365
  subject: wt.landing?.subject,
27708
28366
  body: wt.landing?.body,
27709
28367
  automerge: policy.automerge,
27710
28368
  method: policy.merge
27711
- });
28369
+ }, w);
27712
28370
  if (result.ok) {
27713
28371
  this.setLanding(wt.id, undefined);
27714
28372
  if (result.pr)
@@ -27724,24 +28382,24 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27724
28382
  return committed;
27725
28383
  committedHere = committed !== null;
27726
28384
  if (policy.land === "push") {
27727
- const pulled = await fastForwardMain(repo.path, repo.defaultBranch);
28385
+ const pulled = await fastForwardMain(repo.path, repo.defaultBranch, w);
27728
28386
  if (!pulled.ok && !/no upstream/.test(pulled.message))
27729
28387
  return pulled;
27730
28388
  }
27731
- const taken = await takeMainIn(wt.path, repo.defaultBranch, own);
28389
+ const taken = await takeMainIn(wt.path, repo.defaultBranch, own, w);
27732
28390
  if (!taken.ok)
27733
28391
  return taken;
27734
28392
  const mark = await landingMark(wt.path, repo.defaultBranch);
27735
28393
  const method = policy.merge ?? DEFAULT_MERGE_METHOD;
27736
28394
  const squash = method === "squash" ? await squashMessage(wt.path, repo.defaultBranch, suggested) : "";
27737
- const landed = await landLocally(wt.path, wt.branch, repo.path, repo.defaultBranch, method, squash);
28395
+ const landed = await landLocally(wt.path, wt.branch, repo.path, repo.defaultBranch, method, squash, w);
27738
28396
  if (!landed.ok)
27739
28397
  return landed;
27740
28398
  mergedHere = true;
27741
28399
  await this.noteLand(repo, wt, mark);
27742
28400
  await this.restartFromMain(wt, repo.defaultBranch);
27743
28401
  if (policy.land === "push") {
27744
- const pushed = await pushMain(repo.path, repo.defaultBranch);
28402
+ const pushed = await pushMain(repo.path, repo.defaultBranch, w);
27745
28403
  if (!pushed.ok)
27746
28404
  return { ...pushed, message: `merged into ${repo.defaultBranch} here, but ${pushed.message}` };
27747
28405
  }
@@ -27780,10 +28438,16 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27780
28438
  this.headMoved(wt.id);
27781
28439
  }
27782
28440
  async prMerged(worktreeId) {
28441
+ const out = this.ops.get(worktreeId);
28442
+ if (out)
28443
+ return { ok: false, message: `${shipNoun(out.op)} is already running here` };
28444
+ return this.ship(worktreeId, "land", () => this.prMergedOp(worktreeId));
28445
+ }
28446
+ async prMergedOp(worktreeId, carried, w = UNWATCHED) {
27783
28447
  const { wt, repo } = this.d.state.requireWorktreeWithRepo(worktreeId);
27784
28448
  const result = await withRepoLock(repo.path, async () => {
27785
- const mark = await landingMark(wt.path, repo.defaultBranch);
27786
- const pulled = await fastForwardMain(repo.path, repo.defaultBranch);
28449
+ const mark = carried === undefined ? await landingMark(wt.path, repo.defaultBranch) : carried;
28450
+ const pulled = await fastForwardMain(repo.path, repo.defaultBranch, w);
27787
28451
  if (!pulled.ok)
27788
28452
  return pulled;
27789
28453
  await this.noteLand(repo, wt, mark);
@@ -27811,12 +28475,16 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27811
28475
  throw new UserError(`${r.name} is held by another tool`);
27812
28476
  const repo = this.d.state.requireRepo(r.repoId);
27813
28477
  const own = r.wt ? hasOwnBranch(r.wt) : false;
27814
- const result = await withRepoLock(repo.path, () => takeMainIn(r.path, repo.defaultBranch, own));
27815
- if (result.ok) {
27816
- this.headMoved(worktreeId);
27817
- if (r.wt)
27818
- await this.verdictSurvives(r.wt, { dropMessage: false });
27819
- }
28478
+ const w = r.wt ? this.landWatch(r.wt) : UNWATCHED;
28479
+ const result = await this.ship(worktreeId, "sync-main", async () => {
28480
+ const taken = await withRepoLock(repo.path, () => takeMainIn(r.path, repo.defaultBranch, own, w));
28481
+ if (taken.ok) {
28482
+ this.headMoved(worktreeId);
28483
+ if (r.wt)
28484
+ await this.verdictSurvives(r.wt, { dropMessage: false });
28485
+ }
28486
+ return taken;
28487
+ });
27820
28488
  return { result, defaultBranch: repo.defaultBranch };
27821
28489
  }
27822
28490
  async pull(worktreeId) {
@@ -27824,19 +28492,22 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27824
28492
  if (!isMain(wt))
27825
28493
  throw new UserError("pull on main; a worktree syncs from main instead");
27826
28494
  const repo = this.d.state.requireRepo(wt.repoId);
27827
- const result = await withRepoLock(repo.path, () => pullMain(repo.path, repo.defaultBranch));
27828
- if (result.ok) {
27829
- this.invalidateCounts();
27830
- this.headMoved(worktreeId);
27831
- }
27832
- return result;
28495
+ const w = { step: (name) => this.step(worktreeId, name), git: UNWATCHED.git };
28496
+ return this.ship(worktreeId, "pull-main", async () => {
28497
+ const result = await withRepoLock(repo.path, () => pullMain(repo.path, repo.defaultBranch, w));
28498
+ if (result.ok) {
28499
+ this.invalidateCounts();
28500
+ this.headMoved(worktreeId);
28501
+ }
28502
+ return result;
28503
+ });
27833
28504
  }
27834
28505
  async commit(worktreeId, message) {
27835
28506
  const wt = this.d.state.requireWorktree(worktreeId);
27836
28507
  const m = message.trim();
27837
28508
  if (!m)
27838
28509
  throw new UserError("commit message required");
27839
- return this.commitRecorded(wt, m);
28510
+ return this.ship(wt.id, "commit", () => this.commitRecorded(wt, m));
27840
28511
  }
27841
28512
  setEmpty(wt, empty) {
27842
28513
  if (wt.empty === empty)
@@ -27848,8 +28519,14 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27848
28519
  syncTrunk(repoId) {
27849
28520
  return this.trunk.sync(repoId);
27850
28521
  }
27851
- trunks(opts = {}) {
27852
- return this.trunk.all(opts);
28522
+ async trunks(opts = {}) {
28523
+ const all = await this.trunk.all(opts);
28524
+ for (const t of Object.values(all)) {
28525
+ const shipping = this.ops.get(t.id);
28526
+ if (shipping)
28527
+ t.shipping = shipping;
28528
+ }
28529
+ return all;
27853
28530
  }
27854
28531
  usageFor(worktreeId) {
27855
28532
  const known = this.usage.get(worktreeId);
@@ -28112,7 +28789,8 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
28112
28789
  unseen: isUnseen(wt) || undefined,
28113
28790
  usage: this.usageFor(wt.id),
28114
28791
  transcript: transcriptPathFor(this.d.paths.transcriptsDir, wt.id),
28115
- sessionId: this.d.state.session(wt.id)
28792
+ sessionId: this.d.state.session(wt.id),
28793
+ shipping: this.ops.get(wt.id)
28116
28794
  };
28117
28795
  }));
28118
28796
  }
@@ -28121,7 +28799,16 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
28121
28799
  return Promise.all(found.map(async (f) => {
28122
28800
  const repo = this.d.state.repo(f.repoId);
28123
28801
  const { ahead, behind, dirty } = !repo ? {} : quick ? this.countsQuick(f.id, quick) : await this.counts(f.id, f.path, repo.defaultBranch, !!f.branch);
28124
- return { ...f, procs: [], agent: "idle", login: false, ahead, behind, dirty };
28802
+ return {
28803
+ ...f,
28804
+ procs: [],
28805
+ agent: "idle",
28806
+ login: false,
28807
+ ahead,
28808
+ behind,
28809
+ dirty,
28810
+ shipping: this.ops.get(f.id)
28811
+ };
28125
28812
  }));
28126
28813
  }
28127
28814
  }
@@ -28227,11 +28914,12 @@ var {
28227
28914
  var pruned = pruneAssets(SHELL_DIST);
28228
28915
  if (pruned > 0)
28229
28916
  log.debug("daemon", `pruned ${pruned} assets from builds this one has outlived`);
28917
+ var managed = await loadManaged();
28230
28918
  var paths = makePaths();
28231
28919
  ensureDirs(paths);
28232
28920
  var token = loadOrCreateToken(paths);
28233
28921
  var port = Number(process.env.TOYON_PORT ?? DAEMON_DEFAULT_PORT);
28234
- var remote = loadRemote(paths.remoteFile);
28922
+ var remote = loadRemote(paths.remoteFile, process.env, managed.policy);
28235
28923
  if (remote && addressedByPort(remote.previews))
28236
28924
  pinProxyPorts(PREVIEW_PORTS);
28237
28925
  var state = new StateStore(paths);
@@ -28239,7 +28927,9 @@ var hub = new Hub;
28239
28927
  var bridge = new BridgeScript(BRIDGE_JS);
28240
28928
  var linuxSandbox = new LinuxSandbox;
28241
28929
  await linuxSandbox.refresh();
28242
- var agents = loadAgentRegistry(paths.agentsFile, paths.agentsDir, linuxSandbox);
28930
+ var agents = loadAgentRegistry(paths.agentsFile, paths.agentsDir, linuxSandbox, managed.policy);
28931
+ if (state.defaultAgent !== undefined && !agents.get(state.defaultAgent))
28932
+ state.clearDefaultAgent();
28243
28933
  var probe = new OptionProbe({
28244
28934
  infos: () => agents.infos(),
28245
28935
  require: (id) => agents.require(id),
@@ -28266,6 +28956,7 @@ var runtime = new RuntimeRegistry({
28266
28956
  state,
28267
28957
  paths,
28268
28958
  agents,
28959
+ managed: managed.policy,
28269
28960
  accounts,
28270
28961
  attachments: attachments2,
28271
28962
  bridgeScript: () => bridge.get(),
@@ -28276,7 +28967,7 @@ var runtime = new RuntimeRegistry({
28276
28967
  mainLeads: (repoId) => worktrees.spare.current(repoId) === null
28277
28968
  });
28278
28969
  var drafts = new DraftStore({ file: paths.draftsFile, hub });
28279
- var exec = new ExecService({ state, runtime });
28970
+ var exec = new ExecService({ state, runtime, shipping: (id) => worktrees.shippingOf(id) });
28280
28971
  var worktrees = new WorktreeService({
28281
28972
  state,
28282
28973
  hub,
@@ -28284,7 +28975,7 @@ var worktrees = new WorktreeService({
28284
28975
  paths,
28285
28976
  agents,
28286
28977
  drafts,
28287
- record: (id, command, text, exit) => exec.record(id, command, text, exit)
28978
+ watch: (id, command, run) => exec.watch(id, command, run)
28288
28979
  });
28289
28980
  var turns = new TurnService({
28290
28981
  state,
@@ -28359,7 +29050,7 @@ var update = new UpdateService({
28359
29050
  command: (version) => installCommand(method, version),
28360
29051
  install: runInstall,
28361
29052
  busy: () => runtime.anyBusy(),
28362
- managed: process.env.TOYON_UPDATES === "off"
29053
+ managedBy: !managed.policy.updates ? "policy" : process.env.TOYON_UPDATES === "off" ? "env" : null
28363
29054
  });
28364
29055
  var { branded, stop: stopServer } = startServer({
28365
29056
  port,
@@ -28368,6 +29059,7 @@ var { branded, stop: stopServer } = startServer({
28368
29059
  version: package_default.version,
28369
29060
  noteShellOrigin: (origin) => bridge.learnShellOrigin(origin),
28370
29061
  remote,
29062
+ managed,
28371
29063
  services: {
28372
29064
  state,
28373
29065
  hub,
@@ -28415,6 +29107,7 @@ await runtime.reclaimOrphans();
28415
29107
  await repos.boot();
28416
29108
  drafts.prune((id) => !!state.worktree(id) || worktrees.hasArchived(id));
28417
29109
  idle.boot();
29110
+ landing.boot();
28418
29111
  fireAndForget("agents", agents.installMissing().then(() => probe.missing()), "agent adapter install");
28419
29112
  var repoArg = process.argv[2];
28420
29113
  if (repoArg) {
@@ -28438,6 +29131,12 @@ if (remote?.front === "edge") {
28438
29131
  console.log(" the token grants a shell on this machine; keep the link to yourself");
28439
29132
  }
28440
29133
  }
29134
+ for (const src of managed.sources) {
29135
+ if (src.state === "absent")
29136
+ continue;
29137
+ const what = src.state === "applied" ? describeManaged(managed.policy).join(", ") || "nothing turned off" : `${src.problem}; everything it governs is off`;
29138
+ console.log(` policy: ${src.path}: ${what}`);
29139
+ }
28441
29140
  var shuttingDown = false;
28442
29141
  async function shutdown(signal, opts = {}) {
28443
29142
  if (shuttingDown)
@@ -28445,6 +29144,7 @@ async function shutdown(signal, opts = {}) {
28445
29144
  shuttingDown = true;
28446
29145
  log.info("daemon", `${signal}: stopping dev servers`);
28447
29146
  idle.shutdown();
29147
+ landing.stop();
28448
29148
  repos.stopWatchers();
28449
29149
  prs.stop();
28450
29150
  sweep.stop();