toyon 0.13.0 → 0.15.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 (29) hide show
  1. package/README.md +2 -2
  2. package/dist/CHANGELOG.md +602 -0
  3. package/dist/bridge.js +1 -1
  4. package/dist/cli.js +60 -53
  5. package/dist/daemon.js +695 -283
  6. package/dist/shell/assets/{Editor-CUcgZuoM.js → Editor-jjf6IGvI.js} +3 -3
  7. package/dist/shell/assets/{XTerm-V6sBDIe-.js → XTerm-ClBPMY5e.js} +1 -1
  8. package/dist/shell/assets/{cssMode-CSyiAvIn.js → cssMode-CgTk4Slg.js} +1 -1
  9. package/dist/shell/assets/{freemarker2-C4-FNK3V.js → freemarker2-B4Eirl4Y.js} +1 -1
  10. package/dist/shell/assets/{grammar-DzHb7e7O.js → grammar-E8nWrhy3.js} +1 -1
  11. package/dist/shell/assets/{handlebars-DcjVaOCM.js → handlebars-C6uEGz8T.js} +1 -1
  12. package/dist/shell/assets/{html-Csl8cogA.js → html-tDAdsEYh.js} +1 -1
  13. package/dist/shell/assets/{htmlMode-rewe9UaO.js → htmlMode-DXGTr81h.js} +1 -1
  14. package/dist/shell/assets/index-1FLxRByi.js +267 -0
  15. package/dist/shell/assets/index-D-4W2x1P.css +1 -0
  16. package/dist/shell/assets/{javascript--pKV-E-G.js → javascript-BeM3ebwd.js} +1 -1
  17. package/dist/shell/assets/{jsonMode-zPVc0E6l.js → jsonMode-CXStIFU4.js} +1 -1
  18. package/dist/shell/assets/{liquid-jZT5xQG-.js → liquid-2UNKoWGQ.js} +1 -1
  19. package/dist/shell/assets/{lspLanguageFeatures-BuiqKs90.js → lspLanguageFeatures-G4xsCwWV.js} +1 -1
  20. package/dist/shell/assets/{mdx-DwnR-ia2.js → mdx-CoF6St9y.js} +1 -1
  21. package/dist/shell/assets/{python-DU3pl9Zu.js → python-rULGuAEe.js} +1 -1
  22. package/dist/shell/assets/{razor-BzVUPqHs.js → razor-0I41g6kD.js} +1 -1
  23. package/dist/shell/assets/{tsMode-ONmZhHHZ.js → tsMode-CyMsmAXC.js} +1 -1
  24. package/dist/shell/assets/{xml-BXW7X3BT.js → xml-BNhTd1tm.js} +1 -1
  25. package/dist/shell/assets/{yaml-DFE79ex1.js → yaml-Cp69rkq5.js} +1 -1
  26. package/dist/shell/index.html +2 -2
  27. package/package.json +1 -1
  28. package/dist/shell/assets/index-Bkn7zNFw.js +0 -267
  29. package/dist/shell/assets/index-iVaPr_If.css +0 -1
package/dist/daemon.js CHANGED
@@ -29,7 +29,7 @@ function isWrittenKind(kind) {
29
29
  return WRITTEN_KINDS.includes(kind ?? "");
30
30
  }
31
31
  function writingCall(call, input) {
32
- return (isWrittenKind(call.kind) || call.name === ASK_TOOL) && emptyInput(input);
32
+ return (isWrittenKind(call.kind) || call.name === ASK_TOOL || call.name === TOOL_SEARCH) && emptyInput(input);
33
33
  }
34
34
  function emptyInput(input) {
35
35
  if (!input || typeof input !== "object")
@@ -6602,7 +6602,7 @@ ${text.slice(text.length - (CONTEXT_CHARS - HEAD_CHARS))}`;
6602
6602
  }
6603
6603
 
6604
6604
  // ../shared/src/protocol/limits.ts
6605
- var PROTOCOL_VERSION = 105;
6605
+ var PROTOCOL_VERSION = 109;
6606
6606
  var PAGE_ERROR_MAX_CHARS = 2000;
6607
6607
  var PAGE_TITLE_MAX_CHARS = 1000;
6608
6608
  var RENDER_ERRORS_MAX = 10;
@@ -6961,6 +6961,7 @@ var clientMsgSchema = discriminatedUnion("t", [
6961
6961
  object({ t: literal("fix"), worktreeId: id, toolId: string2().min(1).max(200) }),
6962
6962
  object({ t: literal("continue"), worktreeId: id }),
6963
6963
  object({ t: literal("reveal"), worktreeId: id, path: relPath.optional() }),
6964
+ object({ t: literal("reveal-path"), path: relPath }),
6964
6965
  object({ t: literal("stop-agent"), worktreeId: id }),
6965
6966
  object({ t: literal("seen"), worktreeId: id }),
6966
6967
  object({ t: literal("mark-unread"), worktreeId: id }),
@@ -8528,7 +8529,7 @@ async function turnRemoteOff(ts, daemonPort, file, was) {
8528
8529
  // ../daemon/package.json
8529
8530
  var package_default = {
8530
8531
  name: "@toyon/daemon",
8531
- version: "0.13.0",
8532
+ version: "0.15.0",
8532
8533
  private: true,
8533
8534
  type: "module",
8534
8535
  main: "src/index.ts",
@@ -14435,11 +14436,27 @@ var BUILTIN_AGENTS = [
14435
14436
  loginHint: "OpenCode has no provider logged in; run `opencode auth login` in the terminal"
14436
14437
  }
14437
14438
  ];
14439
+ function installErrorLine(stderr) {
14440
+ const lines = stderr.split(`
14441
+ `).map((l) => l.trim()).filter(Boolean);
14442
+ const first = lines.find((l) => l.startsWith("error:"))?.slice("error:".length).trim();
14443
+ const line = first || lines.at(-1) || "no output";
14444
+ if (UNTRUSTED_CA.test(line))
14445
+ return `${line}. A certificate bun does not trust, usually a proxy's: set NODE_EXTRA_CA_CERTS to its file and restart Toyon`;
14446
+ return line;
14447
+ }
14448
+ var UNTRUSTED_CA = /UNKNOWN_CERTIFICATE_VERIFICATION_ERROR|SELF_SIGNED_CERT|UNABLE_TO_GET_ISSUER_CERT|UNABLE_TO_VERIFY_LEAF_SIGNATURE/;
14438
14449
  var bunInstall = async (dir) => {
14439
- const r = await run(process.execPath, ["install", "--no-progress", "--no-summary"], dir);
14440
- return { ok: r.ok, err: r.err.split(`
14441
- `).slice(-3).join(" ") };
14450
+ const r = await run(process.execPath, ["install", "--no-progress", "--no-summary"], dir, {
14451
+ NO_COLOR: "1",
14452
+ FORCE_COLOR: "0"
14453
+ });
14454
+ return { ok: r.ok, err: r.ok ? "" : installErrorLine(r.err) };
14442
14455
  };
14456
+ var RETRY_AFTER_MS = 30000;
14457
+ var RETRY_CAP_MS = 60 * 60000;
14458
+ var retryWindow = (retries) => Math.min(RETRY_AFTER_MS * 2 ** retries, RETRY_CAP_MS);
14459
+ var failedReason = (why) => `install failed: ${why}`;
14443
14460
  function jsRuntime() {
14444
14461
  if (process.env.TOYON_ACP_RUNTIME === "node") {
14445
14462
  const node = Bun.which("node");
@@ -14455,15 +14472,18 @@ class AgentRegistry {
14455
14472
  installer;
14456
14473
  host;
14457
14474
  sandbox;
14475
+ now;
14458
14476
  specs = new Map;
14459
14477
  installing = new Map;
14460
14478
  installErrors = new Map;
14479
+ failures = new Map;
14461
14480
  onChange = null;
14462
- constructor(specs, dir, installer = bunInstall, host = hostTraits(), sandbox = { problem: () => null }) {
14481
+ constructor(specs, dir, installer = bunInstall, host = hostTraits(), sandbox = { problem: () => null }, now = Date.now) {
14463
14482
  this.dir = dir;
14464
14483
  this.installer = installer;
14465
14484
  this.host = host;
14466
14485
  this.sandbox = sandbox;
14486
+ this.now = now;
14467
14487
  for (const s of specs)
14468
14488
  this.specs.set(s.id, s);
14469
14489
  }
@@ -14521,22 +14541,26 @@ class AgentRegistry {
14521
14541
  }
14522
14542
  }
14523
14543
  unavailable(spec) {
14524
- return this.notInstalled(spec) ?? (spec.confinement === "none" ? null : this.sandbox.problem());
14525
- }
14526
- notInstalled(spec) {
14527
- if (spec.run.kind !== "command") {
14528
- if (!this.packageName(spec))
14529
- return `no build for ${this.host.platform} ${this.host.arch}`;
14530
- if (this.installed(spec))
14531
- return null;
14532
- if (this.installing.has(spec.id))
14533
- return "installing";
14534
- return this.installErrors.get(spec.id) ?? (spec.onDemand ? "not installed" : "not installed yet");
14535
- }
14536
- const cmd = spec.run.command;
14537
- if (isAbsolute(cmd) ? existsSync8(cmd) : Bun.which(cmd))
14538
- return null;
14539
- return `${cmd} not found on PATH`;
14544
+ return this.status(spec).reason;
14545
+ }
14546
+ status(spec) {
14547
+ const boxed = spec.confinement === "none" ? null : this.sandbox.problem();
14548
+ if (spec.run.kind === "command") {
14549
+ const cmd = spec.run.command;
14550
+ const found = isAbsolute(cmd) ? existsSync8(cmd) : Bun.which(cmd);
14551
+ return { reason: found ? boxed : `${cmd} not found on PATH` };
14552
+ }
14553
+ if (!this.packageName(spec))
14554
+ return { reason: `no build for ${this.host.platform} ${this.host.arch}` };
14555
+ if (this.installed(spec))
14556
+ return { reason: boxed };
14557
+ if (this.installing.has(spec.id))
14558
+ return { reason: "installing" };
14559
+ const failed = this.installErrors.get(spec.id);
14560
+ const missing = failed !== undefined ? failedReason(failed) : spec.onDemand ? "not installed" : "not installed yet";
14561
+ if (boxed)
14562
+ return { reason: failed !== undefined ? `${missing}; ${boxed}` : boxed };
14563
+ return { reason: missing, install: failed !== undefined ? { failed } : "missing" };
14540
14564
  }
14541
14565
  command(spec) {
14542
14566
  const why = this.unavailable(spec);
@@ -14564,23 +14588,29 @@ class AgentRegistry {
14564
14588
  const running = this.installing.get(id);
14565
14589
  if (running)
14566
14590
  return running;
14567
- if (this.installed(spec) && this.installedVersion(spec) === spec.run.version)
14591
+ if (this.installed(spec) && this.installedVersion(spec) === spec.run.version) {
14592
+ this.installErrors.delete(id);
14593
+ this.failures.delete(id);
14568
14594
  return Promise.resolve();
14595
+ }
14569
14596
  const { version } = spec.run;
14570
14597
  const dir = join6(this.dir, id);
14571
14598
  const p = (async () => {
14572
- mkdirSync5(dir, { recursive: true });
14573
- writeFileSync4(join6(dir, "package.json"), `${JSON.stringify({ name: `toyon-agent-${id}`, private: true, dependencies: { [pkg]: version } }, null, 2)}
14599
+ try {
14600
+ mkdirSync5(dir, { recursive: true });
14601
+ writeFileSync4(join6(dir, "package.json"), `${JSON.stringify({ name: `toyon-agent-${id}`, private: true, dependencies: { [pkg]: version } }, null, 2)}
14574
14602
  `);
14575
- log.info("agents", `installing ${pkg}@${version} for ${id}`);
14576
- const r = await this.installer(dir, pkg, version);
14577
- if (r.ok && this.installed(spec)) {
14578
- this.installErrors.delete(id);
14579
- log.info("agents", `${id} ready`);
14580
- } else {
14581
- const why = `install failed: ${r.err || "no bin after install"}`;
14582
- this.installErrors.set(id, why);
14583
- log.warn("agents", `${id}: ${why}`);
14603
+ log.info("agents", `installing ${pkg}@${version} for ${id}`);
14604
+ const r = await this.installer(dir, pkg, version);
14605
+ if (r.ok && this.installed(spec)) {
14606
+ this.installErrors.delete(id);
14607
+ this.failures.delete(id);
14608
+ log.info("agents", `${id} ready`);
14609
+ return;
14610
+ }
14611
+ this.fail(id, r.err || "no bin after install");
14612
+ } catch (e) {
14613
+ this.fail(id, e instanceof Error ? e.message : String(e));
14584
14614
  }
14585
14615
  })().finally(() => {
14586
14616
  this.installing.delete(id);
@@ -14590,6 +14620,11 @@ class AgentRegistry {
14590
14620
  this.onChange?.();
14591
14621
  return p;
14592
14622
  }
14623
+ fail(id, why) {
14624
+ this.installErrors.set(id, why);
14625
+ this.failures.set(id, { at: this.now(), retries: this.failures.get(id)?.retries ?? 0 });
14626
+ log.warn("agents", `${id}: ${failedReason(why)}`);
14627
+ }
14593
14628
  installedVersion(spec) {
14594
14629
  const name = this.packageName(spec);
14595
14630
  if (!name)
@@ -14609,22 +14644,37 @@ class AgentRegistry {
14609
14644
  await this.install(id);
14610
14645
  }
14611
14646
  }
14647
+ async retryFailed() {
14648
+ if (this.installing.size > 0)
14649
+ return;
14650
+ for (const [id, entry] of this.failures) {
14651
+ if (this.now() - entry.at < retryWindow(entry.retries))
14652
+ continue;
14653
+ entry.retries += 1;
14654
+ await this.install(id);
14655
+ }
14656
+ }
14612
14657
  adapters() {
14613
14658
  const out = [];
14614
14659
  for (const spec of this.list()) {
14615
14660
  if (spec.run.kind === "command" || !this.packageName(spec))
14616
14661
  continue;
14617
14662
  const installed = this.installed(spec) ? this.installedVersion(spec) : null;
14618
- const error = this.installErrors.get(spec.id);
14619
- if (spec.onDemand && installed === null && !error)
14663
+ const failed = this.installErrors.get(spec.id);
14664
+ if (spec.onDemand && installed === null && failed === undefined)
14620
14665
  continue;
14621
- out.push({ id: spec.id, installed, pinned: spec.run.version, ...error ? { error } : {} });
14666
+ out.push({
14667
+ id: spec.id,
14668
+ installed,
14669
+ pinned: spec.run.version,
14670
+ ...failed !== undefined ? { error: failedReason(failed) } : {}
14671
+ });
14622
14672
  }
14623
14673
  return out;
14624
14674
  }
14625
14675
  infos() {
14626
14676
  return this.list().map((spec) => {
14627
- const reason = this.unavailable(spec);
14677
+ const { reason, install } = this.status(spec);
14628
14678
  return {
14629
14679
  id: spec.id,
14630
14680
  name: spec.name,
@@ -14632,6 +14682,7 @@ class AgentRegistry {
14632
14682
  available: !reason,
14633
14683
  ...reason ? { reason } : {},
14634
14684
  ...this.installing.has(spec.id) ? { installing: true } : {},
14685
+ ...install !== undefined ? { install } : {},
14635
14686
  ...spec.onDemand ? { onDemand: true } : {},
14636
14687
  sandboxed: spec.confinement !== "none"
14637
14688
  };
@@ -14720,6 +14771,18 @@ function agentModeFor(spec, mode, available) {
14720
14771
  function modeAfterPlan(optionName) {
14721
14772
  return /manual/i.test(optionName) ? "ask" : "auto";
14722
14773
  }
14774
+ function orderPlanOptions(options) {
14775
+ const rank = (id) => {
14776
+ if (id === "exit-plan-default")
14777
+ return 1;
14778
+ if (id.startsWith("exit-plan-clear-"))
14779
+ return 2;
14780
+ if (id.startsWith("exit-plan-"))
14781
+ return 0;
14782
+ return 3;
14783
+ };
14784
+ return [...options].sort((a, b) => rank(a.optionId) - rank(b.optionId));
14785
+ }
14723
14786
 
14724
14787
  // ../daemon/src/agent/origins.ts
14725
14788
  import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
@@ -15091,14 +15154,16 @@ function isNetworkAsk(update) {
15091
15154
  function isToolSearch(update) {
15092
15155
  return update.name === TOOL_SEARCH || asRecord2(asRecord2(update._meta).claudeCode).toolName === TOOL_SEARCH;
15093
15156
  }
15094
- function toolSearchQuery(rawInput) {
15157
+ var TOOL_SEARCH_OPENING = "loading a tool";
15158
+ function toolSearchTitle(rawInput) {
15095
15159
  const query = asRecord2(rawInput).query;
15096
15160
  if (typeof query !== "string")
15097
15161
  return "";
15098
15162
  const names = /^select:(.*)$/.exec(query.trim());
15099
15163
  if (!names)
15100
- return query.trim();
15101
- return (names[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean).join(", ");
15164
+ return `looked for a tool: ${query.trim()}`;
15165
+ const loaded = (names[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean).join(", ");
15166
+ return loaded ? `loaded ${loaded}` : "";
15102
15167
  }
15103
15168
  var TOYON_TOOL = new RegExp(`^(?:mcp__${TOYON_MCP_SERVER}__|${TOYON_MCP_SERVER}/)([A-Za-z0-9_-]+)$`);
15104
15169
  function toyonTool(update) {
@@ -15122,7 +15187,7 @@ function heading(update) {
15122
15187
  if (isNetworkAsk(update) && typeof host === "string")
15123
15188
  return { name: "", title: host, kind: "fetch" };
15124
15189
  if (isToolSearch(update)) {
15125
- return { name: TOOL_SEARCH, title: toolSearchQuery(update.rawInput) || TOOL_SEARCH, kind: "search" };
15190
+ return { name: TOOL_SEARCH, title: toolSearchTitle(update.rawInput) || TOOL_SEARCH_OPENING, kind: "other" };
15126
15191
  }
15127
15192
  const own = toyonTool(update);
15128
15193
  if (own)
@@ -15254,7 +15319,7 @@ function mapUpdate(update, memos, tag, sink, background) {
15254
15319
  });
15255
15320
  }
15256
15321
  const refined = { ...memo.held };
15257
- const title = memo.name === TOOL_SEARCH ? toolSearchQuery(update.rawInput) : memo.name === HANDOFF_TOOL ? update.rawInput !== undefined ? toyonTitle(HANDOFF_TOOL, update.rawInput, update.title) : undefined : update.title;
15322
+ const title = memo.name === TOOL_SEARCH ? toolSearchTitle(update.rawInput) : memo.name === HANDOFF_TOOL ? update.rawInput !== undefined ? toyonTitle(HANDOFF_TOOL, update.rawInput, update.title) : undefined : update.title;
15258
15323
  if (title && title !== memo.title)
15259
15324
  refined.title = memo.title = title;
15260
15325
  if (update.name && update.name !== memo.name && toyonTool(update) !== memo.name)
@@ -15275,7 +15340,7 @@ function mapUpdate(update, memos, tag, sink, background) {
15275
15340
  out.push({ type: "tool-update", toolId: update.toolCallId, ...refined });
15276
15341
  }
15277
15342
  if (update.content)
15278
- memo.content = [...memo.content, ...update.content];
15343
+ memo.content = update.content;
15279
15344
  if (update.rawOutput !== undefined)
15280
15345
  memo.rawOutput = update.rawOutput;
15281
15346
  if ((update.status === "completed" || update.status === "failed") && !memo.ended) {
@@ -16182,15 +16247,30 @@ class AcpSession {
16182
16247
  this.emit({ type: "tool-end", toolId, output: output.join(`
16183
16248
  `), isError: true });
16184
16249
  }
16185
- for (const { event } of this.log.entries)
16250
+ const going = new Set;
16251
+ for (const { event } of this.log.entries) {
16186
16252
  this.trackHandoff(event);
16253
+ if (event.type === "handoff-go")
16254
+ going.add(event.id);
16255
+ else if (event.type === "handoff" || event.type === "handoff-declined")
16256
+ going.delete(event.id);
16257
+ }
16258
+ for (const id of going) {
16259
+ this.emit({
16260
+ type: "handoff-declined",
16261
+ id,
16262
+ outcome: "failed",
16263
+ reason: "Toyon stopped while the worktree was being made",
16264
+ ts: Date.now()
16265
+ });
16266
+ }
16187
16267
  if (this.openHandoffs.size > 0)
16188
16268
  this.syncStatus();
16189
16269
  }
16190
16270
  trackHandoff(event) {
16191
16271
  if (event.type === "handoff-proposed")
16192
16272
  return this.openHandoffs.add(event.id).size > 0;
16193
- if (event.type === "handoff" || event.type === "handoff-declined")
16273
+ if (event.type === "handoff-go" || event.type === "handoff" || event.type === "handoff-declined")
16194
16274
  return this.openHandoffs.delete(event.id);
16195
16275
  return false;
16196
16276
  }
@@ -16409,6 +16489,7 @@ class AcpSession {
16409
16489
  this.queue = [];
16410
16490
  this.steered = [];
16411
16491
  this.queueChanged();
16492
+ this.cancelAsks("expired");
16412
16493
  this.stop();
16413
16494
  await this.dropConn("closing");
16414
16495
  await this.log.flush();
@@ -16849,6 +16930,9 @@ class AcpSession {
16849
16930
  }
16850
16931
  if (params.update.sessionUpdate === "current_mode_update") {
16851
16932
  live.modeId = params.update.currentModeId;
16933
+ const planId = agentModeFor(live.conn.spec, "plan", live.modeIds ?? undefined);
16934
+ if (planId && live.modeId === planId && this.mode() !== "plan")
16935
+ this.d.setMode?.("plan");
16852
16936
  return;
16853
16937
  }
16854
16938
  if (params.update.sessionUpdate === "config_option_update") {
@@ -16921,7 +17005,7 @@ class AcpSession {
16921
17005
  const parts = permissionParts(params);
16922
17006
  const detail = parts.detail ?? "";
16923
17007
  const planPath = plan ? await this.d.onPlan?.(detail) ?? null : null;
16924
- const offered = plan ? params.options : params.options.filter((o) => o.kind !== "allow_always");
17008
+ const offered = plan ? orderPlanOptions(params.options) : params.options.filter((o) => o.kind !== "allow_always");
16925
17009
  const choices = offered.map((o) => ({ id: o.optionId, name: o.name, kind: o.kind }));
16926
17010
  return this.openAsk((id) => ({
16927
17011
  type: "agent-permission",
@@ -17007,9 +17091,9 @@ class AcpSession {
17007
17091
  const text = headers.length === 1 ? `with my answer to "${headers[0]}"` : "with my answers";
17008
17092
  this.post(text, { attachments: followUp.attachments });
17009
17093
  }
17010
- cancelAsks() {
17094
+ cancelAsks(outcome = "cancelled") {
17011
17095
  for (const ask of [...this.asks.values()])
17012
- ask.settle("cancelled");
17096
+ ask.settle(outcome);
17013
17097
  }
17014
17098
  syncStatus() {
17015
17099
  if (this.status === "error")
@@ -18641,6 +18725,8 @@ class RuntimeRegistry {
18641
18725
  }
18642
18726
  if (this.settingUp.has(id))
18643
18727
  return;
18728
+ if (wt.kind === "spare" && wt.phase !== undefined && wt.phase !== "ready")
18729
+ return;
18644
18730
  const repo = this.deps.state.repos.find((r) => r.id === wt.repoId);
18645
18731
  if (repo)
18646
18732
  await this.start(wt, repo);
@@ -19435,7 +19521,7 @@ function openAskOf(entries) {
19435
19521
  open.delete(e.id);
19436
19522
  else if (e.type === "handoff-proposed")
19437
19523
  open.set(e.id, `Continue in ${e.repo.name}?`);
19438
- else if (e.type === "handoff" || e.type === "handoff-declined")
19524
+ else if (e.type === "handoff-go" || e.type === "handoff" || e.type === "handoff-declined")
19439
19525
  open.delete(e.id);
19440
19526
  }
19441
19527
  const last = [...open.values()].at(-1)?.trim();
@@ -19487,7 +19573,7 @@ var LAND_ASK = [
19487
19573
  "Line 2: blank",
19488
19574
  "Line 3: Recap: one sentence under 20 words for the user coming back to this task, saying what the task is and then what just happened or what to do next. Skip root-cause narrative, fix internals and secondary to-dos.",
19489
19575
  "Line 4: blank",
19490
- "Line 5: a commit subject under 60 characters, lowercase, imperative, in the style of the recent subjects when given",
19576
+ "Line 5: a commit subject under 60 characters, lowercase, imperative, naming what the diff summary says the message is for, in the style of the recent subjects when given",
19491
19577
  "Line 6: blank",
19492
19578
  "Then: a short body, 1 to 4 plain sentences on what changed and why. No markdown, no bullets, no dashes as punctuation."
19493
19579
  ].join(`
@@ -19495,17 +19581,25 @@ var LAND_ASK = [
19495
19581
  var LAND_TURNS = 4;
19496
19582
  var LAND_CHARS = 5000;
19497
19583
  function landPrompt(i) {
19498
- const head = [LAND_ASK, "", `Task: ${clip(i.title, 200)}`];
19499
- if (i.firstAsk)
19500
- head.push(`First request: ${clip(i.firstAsk, 300)}`);
19584
+ const own = i.diffStat.trim();
19585
+ const committed = i.committedStat?.trim() ?? "";
19586
+ const head = [LAND_ASK, ""];
19587
+ if (own && i.earlier) {
19588
+ head.push(`Task, as the worktree is titled; background, since the branch committed or landed work before: ${clip(i.title, 200)}`);
19589
+ if (i.firstAsk)
19590
+ head.push(`First request here, which the earlier work may have answered: ${clip(i.firstAsk, 300)}`);
19591
+ } else {
19592
+ head.push(`Task: ${clip(i.title, 200)}`);
19593
+ if (i.firstAsk)
19594
+ head.push(`First request: ${clip(i.firstAsk, 300)}`);
19595
+ }
19501
19596
  if (i.recentSubjects.length)
19502
19597
  head.push(`Recent commit subjects:
19503
19598
  ${i.recentSubjects.map((s) => ` ${s}`).join(`
19504
19599
  `)}`);
19505
19600
  if (i.note?.trim())
19506
19601
  head.push(`Note from the user for the commit message, to be reflected in its wording: ${clip(i.note.trim(), 500)}`);
19507
- const diff = `Diff summary:
19508
- ${i.diffStat.trim() || "(none)"}`;
19602
+ const diff = diffBlock(own, committed);
19509
19603
  let blocks = turnBlocks(i.turns);
19510
19604
  const size = () => [...head, "", ...blocks, "", diff].join(`
19511
19605
 
@@ -19517,6 +19611,22 @@ ${i.diffStat.trim() || "(none)"}`;
19517
19611
 
19518
19612
  `), diff].join(`
19519
19613
 
19614
+ `);
19615
+ }
19616
+ function diffBlock(own, committed) {
19617
+ if (!own && !committed)
19618
+ return `Diff summary:
19619
+ (none)`;
19620
+ const parts = [];
19621
+ if (own)
19622
+ parts.push(`What this commit will contain, which is all the message describes:
19623
+ ${own}`);
19624
+ if (committed)
19625
+ parts.push(own ? `Already committed on this branch, context only, not what the message describes:
19626
+ ${committed}` : `Nothing is uncommitted; the message is for the branch's commits as one:
19627
+ ${committed}`);
19628
+ return parts.join(`
19629
+
19520
19630
  `);
19521
19631
  }
19522
19632
  function turnBlocks(turns) {
@@ -19787,7 +19897,8 @@ function locateAssets(here, env = process.env) {
19787
19897
  helperStub: packaged ? join13(here, "helper-stub") : join13(here, "../dist/helper-stub"),
19788
19898
  cliEntry: packaged ? join13(here, "cli.js") : join13(here, "../../cli/src/cli.ts"),
19789
19899
  sourceRoot: packaged ? null : resolve5(here, "../../.."),
19790
- packageJson: packaged ? join13(here, "..", "package.json") : null
19900
+ packageJson: packaged ? join13(here, "..", "package.json") : null,
19901
+ changelog: packaged ? join13(here, "CHANGELOG.md") : resolve5(here, "../../../CHANGELOG.md")
19791
19902
  };
19792
19903
  }
19793
19904
  var KEEP_MS = 3 * 24 * 60 * 60 * 1000;
@@ -21046,9 +21157,9 @@ class StateStore {
21046
21157
  this.state.worktrees.push(wt);
21047
21158
  this.save();
21048
21159
  }
21049
- removeWorktree(id) {
21160
+ removeWorktree(id, opts = {}) {
21050
21161
  const wt = this.state.worktrees.find((w) => w.id === id);
21051
- const repo = wt && wt.kind !== "spare" ? this.repo(wt.repoId) : undefined;
21162
+ const repo = wt && wt.kind !== "spare" && !opts.refused ? this.repo(wt.repoId) : undefined;
21052
21163
  if (wt && repo)
21053
21164
  repo.promptedAt = Math.max(repo.promptedAt ?? 0, sentAt(wt));
21054
21165
  this.state.worktrees = this.state.worktrees.filter((w) => w.id !== id);
@@ -22738,9 +22849,14 @@ async function behindUpstream(worktreePath) {
22738
22849
  const r = await git(worktreePath, "rev-list", "--count", "HEAD..@{upstream}");
22739
22850
  return r.ok ? Number(r.out) || 0 : null;
22740
22851
  }
22741
- async function aheadUpstream(worktreePath) {
22852
+ async function aheadUpstream(worktreePath, branch) {
22742
22853
  const r = await git(worktreePath, "rev-list", "--count", "@{upstream}..HEAD");
22743
- return r.ok ? Number(r.out) || 0 : null;
22854
+ if (r.ok)
22855
+ return Number(r.out) || 0;
22856
+ if (!branch)
22857
+ return null;
22858
+ const o = await git(worktreePath, "rev-list", "--count", `origin/${branch}..HEAD`);
22859
+ return o.ok ? Number(o.out) || 0 : null;
22744
22860
  }
22745
22861
  async function aheadBehind(worktreePath, defaultBr) {
22746
22862
  const [a, b] = await Promise.all([
@@ -23269,6 +23385,9 @@ class OpenService {
23269
23385
  throw new UserError(`${raw} is a folder: Toyon opens files`);
23270
23386
  return this.openFile(raw, path, st);
23271
23387
  }
23388
+ async where(raw) {
23389
+ return (await this.locate(raw)).path;
23390
+ }
23272
23391
  async locate(raw) {
23273
23392
  const path = canonical(resolve7(expandTilde(raw)));
23274
23393
  const st = await stat3(path).catch((e) => {
@@ -23368,13 +23487,70 @@ function parseAllowed(json) {
23368
23487
  return null;
23369
23488
  }
23370
23489
  }
23371
- var PR_VIEW_FIELDS = "number,url,title,state,mergedAt,reviewDecision,statusCheckRollup,mergeable,autoMergeRequest";
23490
+ var PR_VIEW_FIELDS = "number,url,title,state,mergedAt,reviewDecision,statusCheckRollup,mergeable,autoMergeRequest,headRefOid";
23372
23491
  async function viewPr(repoPath, number) {
23373
23492
  const r = await gh(["pr", "view", String(number), "--json", PR_VIEW_FIELDS], repoPath, 20000);
23374
23493
  if (!r.ok || !r.out)
23375
23494
  return null;
23376
23495
  const parsed = parsePrView(r.out);
23377
- return parsed ? { ...parsed, at: Date.now() } : null;
23496
+ if (!parsed)
23497
+ return null;
23498
+ if (parsed.state === "open" && !parsed.preview) {
23499
+ const head = headOf(r.out);
23500
+ const preview = head ? await viewDeploymentPreview(repoPath, head) : null;
23501
+ if (preview)
23502
+ parsed.preview = preview;
23503
+ }
23504
+ return { ...parsed, at: Date.now() };
23505
+ }
23506
+ function headOf(json) {
23507
+ try {
23508
+ const o = JSON.parse(json);
23509
+ return typeof o.headRefOid === "string" && o.headRefOid ? o.headRefOid : null;
23510
+ } catch {
23511
+ return null;
23512
+ }
23513
+ }
23514
+ var DEPLOYMENTS_QUERY = `query($owner: String!, $name: String!, $oid: GitObjectID!) {
23515
+ repository(owner: $owner, name: $name) {
23516
+ object(oid: $oid) { ... on Commit {
23517
+ deployments(last: 10) { nodes { environment latestStatus { state environmentUrl } } }
23518
+ } }
23519
+ }
23520
+ }`;
23521
+ async function viewDeploymentPreview(repoPath, head) {
23522
+ const r = await gh([
23523
+ "api",
23524
+ "graphql",
23525
+ "-F",
23526
+ "owner={owner}",
23527
+ "-F",
23528
+ "name={repo}",
23529
+ "-f",
23530
+ `oid=${head}`,
23531
+ "-f",
23532
+ `query=${DEPLOYMENTS_QUERY}`
23533
+ ], repoPath, 20000);
23534
+ if (!r.ok || !r.out)
23535
+ return null;
23536
+ return parseDeployments(r.out);
23537
+ }
23538
+ function parseDeployments(json) {
23539
+ let nodes;
23540
+ try {
23541
+ const o = JSON.parse(json);
23542
+ nodes = o.data?.repository?.object?.deployments?.nodes;
23543
+ } catch {
23544
+ return null;
23545
+ }
23546
+ if (!Array.isArray(nodes))
23547
+ return null;
23548
+ for (const d of [...nodes].reverse()) {
23549
+ const st = d?.latestStatus;
23550
+ if (st?.state === "SUCCESS" && isPreviewUrl(st.environmentUrl))
23551
+ return st.environmentUrl;
23552
+ }
23553
+ return null;
23378
23554
  }
23379
23555
  function parsePrView(json) {
23380
23556
  let o;
@@ -23390,6 +23566,7 @@ function parsePrView(json) {
23390
23566
  const checks = foldChecks(o.statusCheckRollup);
23391
23567
  const mergeable = o.mergeable === "MERGEABLE" ? true : o.mergeable === "CONFLICTING" ? false : undefined;
23392
23568
  const automerge = o.autoMergeRequest !== null && o.autoMergeRequest !== undefined;
23569
+ const preview = state === "open" ? previewOf3(o.statusCheckRollup) : undefined;
23393
23570
  return {
23394
23571
  number: o.number,
23395
23572
  url: o.url,
@@ -23398,9 +23575,37 @@ function parsePrView(json) {
23398
23575
  ...review ? { review } : {},
23399
23576
  ...checks ? { checks } : {},
23400
23577
  ...mergeable !== undefined ? { mergeable } : {},
23401
- ...automerge ? { automerge } : {}
23578
+ ...automerge ? { automerge } : {},
23579
+ ...preview ? { preview } : {}
23402
23580
  };
23403
23581
  }
23582
+ function previewOf3(rollup) {
23583
+ if (!Array.isArray(rollup))
23584
+ return;
23585
+ for (const c of rollup) {
23586
+ const name = String(c.context ?? c.name ?? "");
23587
+ if (!/preview/i.test(name))
23588
+ continue;
23589
+ const word = String(c.conclusion ?? c.state ?? "").toUpperCase();
23590
+ if (word !== "SUCCESS")
23591
+ continue;
23592
+ const url = c.targetUrl ?? c.detailsUrl;
23593
+ if (isPreviewUrl(url))
23594
+ return url;
23595
+ }
23596
+ return;
23597
+ }
23598
+ function isPreviewUrl(url) {
23599
+ if (typeof url !== "string")
23600
+ return false;
23601
+ let host;
23602
+ try {
23603
+ host = new URL(url).hostname;
23604
+ } catch {
23605
+ return false;
23606
+ }
23607
+ return host !== "github.com" && !host.endsWith(".github.com");
23608
+ }
23404
23609
  function foldChecks(rollup) {
23405
23610
  if (!Array.isArray(rollup) || rollup.length === 0)
23406
23611
  return;
@@ -28837,6 +29042,9 @@ var handlers = {
28837
29042
  reveal(msg, _ctx, s) {
28838
29043
  s.files.reveal(msg.worktreeId, msg.path);
28839
29044
  },
29045
+ async "reveal-path"(msg, _ctx, s) {
29046
+ s.files.revealPath(await s.opens.where(msg.path));
29047
+ },
28840
29048
  async "discard-files"(msg, _ctx, s) {
28841
29049
  try {
28842
29050
  for (const path of msg.paths)
@@ -28964,10 +29172,12 @@ var handlers = {
28964
29172
  const followUp = msg.attachments?.length ? { attachments: msg.attachments } : undefined;
28965
29173
  s.runtime.agentFor(msg.worktreeId)?.answer(msg.askId, { kind: "answers", answers: msg.answers }, followUp);
28966
29174
  });
29175
+ s.worktrees.markPrompted(msg.worktreeId);
28967
29176
  },
28968
29177
  "agent-decide"(msg, _ctx, s) {
28969
29178
  requireRun(s, msg.worktreeId);
28970
29179
  s.runtime.agentFor(msg.worktreeId)?.answer(msg.askId, { kind: "choice", choiceId: msg.choiceId });
29180
+ s.worktrees.markPrompted(msg.worktreeId);
28971
29181
  },
28972
29182
  async "handoff-answer"(msg, _ctx, s) {
28973
29183
  requireRun(s, msg.worktreeId);
@@ -29061,8 +29271,8 @@ var handlers = {
29061
29271
  ctx.reply({ t: "refs", repoId: msg.repoId, query: msg.query, refs });
29062
29272
  },
29063
29273
  async "search-chats"(msg, ctx, s) {
29064
- const { hits, truncated } = await s.chats.search(msg.repoId, msg.query);
29065
- ctx.reply({ t: "chat-hits", repoId: msg.repoId, query: msg.query, hits, truncated });
29274
+ const { hits, truncated, asks } = await s.chats.search(msg.repoId, msg.query);
29275
+ ctx.reply({ t: "chat-hits", repoId: msg.repoId, query: msg.query, hits, truncated, asks });
29066
29276
  },
29067
29277
  async "open-ref"(msg, _ctx, s) {
29068
29278
  await s.worktrees.openRef(msg.repoId, msg.kind, msg.ref, { createdBy: msg.clientId, pr: msg.pr });
@@ -29341,6 +29551,20 @@ function createFetch(opts) {
29341
29551
  const refused = await opts.restart(url.searchParams.has(RESTART_NOW));
29342
29552
  return refused ? new Response(refused, { status: 409 }) : new Response(null, { status: 202 });
29343
29553
  }
29554
+ if (url.pathname === "/changelog" && req.method === "GET") {
29555
+ if (!authed(req))
29556
+ return unauthorized();
29557
+ const text = await opts.changelog();
29558
+ if (text === null)
29559
+ return new Response("this copy of Toyon carries no release notes", { status: 404 });
29560
+ return new Response(text, {
29561
+ headers: {
29562
+ "content-type": "text/markdown; charset=utf-8",
29563
+ "cache-control": NO_STORE2,
29564
+ "x-content-type-options": "nosniff"
29565
+ }
29566
+ });
29567
+ }
29344
29568
  const paired = await opts.pairing(req, url, d);
29345
29569
  if (paired)
29346
29570
  return paired;
@@ -29856,7 +30080,8 @@ function startServer(opts) {
29856
30080
  restartWait: () => ({ waiting: s.restarter.waitingOn(), asking: s.restarter.asking() }),
29857
30081
  grants: opts.grants,
29858
30082
  trusted: () => s.state.trustedOrigins,
29859
- mcp: (req, id) => s.mcp.fetch(req, id)
30083
+ mcp: (req, id) => s.mcp.fetch(req, id),
30084
+ changelog: () => Bun.file(opts.changelog).text().catch(() => null)
29860
30085
  }),
29861
30086
  websocket: {
29862
30087
  async open(ws) {
@@ -29873,6 +30098,7 @@ function startServer(opts) {
29873
30098
  sockets.add(ws);
29874
30099
  link(ws);
29875
30100
  send(ws, await helloFrame());
30101
+ fireAndForget("agents", s.agents.retryFailed(), "agent install retry");
29876
30102
  for (const o of s.opens.takePending())
29877
30103
  send(ws, openedFrame(o));
29878
30104
  },
@@ -30204,7 +30430,7 @@ var REFRESH_EVERY_MS = 60000;
30204
30430
  var CHECK_EVERY_MS = 6 * 60 * 60000;
30205
30431
  var FIRST_CHECK_MS = 2 * 60000;
30206
30432
  var SETTLED_MS = 2 * 60000;
30207
- var RETRY_AFTER_MS = 24 * 60 * 60000;
30433
+ var RETRY_AFTER_MS2 = 24 * 60 * 60000;
30208
30434
  var INSTALL_SETTLE_MS = 60000;
30209
30435
 
30210
30436
  class UpdateService {
@@ -30356,7 +30582,7 @@ class UpdateService {
30356
30582
  if (this.needsInstall() && this.d.command(target) === null)
30357
30583
  return;
30358
30584
  const failed = this.d.state.updateFailed;
30359
- if (failed?.version === target && this.now() - failed.at < RETRY_AFTER_MS)
30585
+ if (failed?.version === target && this.now() - failed.at < RETRY_AFTER_MS2)
30360
30586
  return;
30361
30587
  if (this.d.busy()) {
30362
30588
  this.busyAt = this.now();
@@ -30815,6 +31041,7 @@ var CHAT_HITS_MAX = 200;
30815
31041
  var MIN_QUERY = 2;
30816
31042
  var SNIPPET = 160;
30817
31043
  var BEFORE = 50;
31044
+ var ASK = 500;
30818
31045
  var bunFs2 = {
30819
31046
  async stat(path) {
30820
31047
  try {
@@ -30886,6 +31113,9 @@ function searchRows(rows, terms, chat, limit = CHAT_HITS_PER_WORKTREE) {
30886
31113
  }
30887
31114
  return { hits, more: false };
30888
31115
  }
31116
+ function firstAsk(rows) {
31117
+ return rows.find((r) => r.role === "user")?.text.slice(0, ASK);
31118
+ }
30889
31119
  function termRanges(lower, terms) {
30890
31120
  const ranges = [];
30891
31121
  for (const term of terms) {
@@ -30907,7 +31137,7 @@ class ChatSearch {
30907
31137
  this.d.state.requireRepo(repoId);
30908
31138
  const terms = needleTerms(query);
30909
31139
  if (terms.length === 0)
30910
- return { hits: [], truncated: false };
31140
+ return { hits: [], truncated: false, asks: {} };
30911
31141
  const chats = [
30912
31142
  ...this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind === "worktree").map((w) => ({ id: w.id, archived: false, path: this.d.transcriptPath(w.id) })),
30913
31143
  ...this.d.archivedChats(repoId).map((a) => ({ id: a.id, archived: true, path: a.transcript }))
@@ -30915,13 +31145,23 @@ class ChatSearch {
30915
31145
  const read = new Set;
30916
31146
  const found = await Promise.all(chats.map(async (c) => {
30917
31147
  const rows = await this.rowsOf(c.id, c.archived, c.path, read);
30918
- return searchRows(rows, terms, { worktreeId: c.id, archived: c.archived });
31148
+ return {
31149
+ id: c.id,
31150
+ ...searchRows(rows, terms, { worktreeId: c.id, archived: c.archived }),
31151
+ ask: firstAsk(rows)
31152
+ };
30919
31153
  }));
30920
31154
  for (const key of this.said.keys())
30921
31155
  if (!read.has(key))
30922
31156
  this.said.delete(key);
30923
31157
  const all = found.filter((f) => f.hits.length > 0).sort((a, b) => b.hits[0].ts - a.hits[0].ts).flatMap((f) => f.hits);
30924
- return { hits: all.slice(0, CHAT_HITS_MAX), truncated: all.length > CHAT_HITS_MAX || found.some((f) => f.more) };
31158
+ const hits = all.slice(0, CHAT_HITS_MAX);
31159
+ const kept = new Set(hits.map((h) => h.worktreeId));
31160
+ const asks = {};
31161
+ for (const f of found)
31162
+ if (f.ask && kept.has(f.id))
31163
+ asks[f.id] = f.ask;
31164
+ return { hits, truncated: all.length > CHAT_HITS_MAX || found.some((f) => f.more), asks };
30925
31165
  }
30926
31166
  async rowsOf(id, archived, path, read) {
30927
31167
  const live = archived ? undefined : this.d.live(id);
@@ -31107,7 +31347,7 @@ function openProposals(entries) {
31107
31347
  for (const { event } of entries) {
31108
31348
  if (event.type === "handoff-proposed")
31109
31349
  open.set(event.id, event);
31110
- else if (event.type === "handoff" || event.type === "handoff-declined")
31350
+ else if (event.type === "handoff-go" || event.type === "handoff" || event.type === "handoff-declined")
31111
31351
  open.delete(event.id);
31112
31352
  }
31113
31353
  return [...open.values()];
@@ -31126,7 +31366,6 @@ function proposedThisTurn(entries) {
31126
31366
 
31127
31367
  class HandoffService {
31128
31368
  d;
31129
- settling = new Set;
31130
31369
  constructor(d) {
31131
31370
  this.d = d;
31132
31371
  d.hub.on("landed", (worktreeId) => this.landed(worktreeId));
@@ -31239,13 +31478,11 @@ class HandoffService {
31239
31478
  if (!openProposals(entries).some((p) => p.id === id)) {
31240
31479
  return log.debug(worktreeId, `answer for a handoff that already closed (${id})`);
31241
31480
  }
31242
- if (this.settling.has(id))
31243
- return;
31244
31481
  if (!go) {
31245
31482
  agent.note({ type: "handoff-declined", id, ts: Date.now() });
31246
31483
  return;
31247
31484
  }
31248
- this.settling.add(id);
31485
+ agent.note({ type: "handoff-go", id, ts: Date.now() });
31249
31486
  try {
31250
31487
  const repo = this.d.state.repo(proposed.repo.id);
31251
31488
  if (!repo)
@@ -31271,8 +31508,6 @@ class HandoffService {
31271
31508
  const reason = e instanceof Error ? e.message : String(e);
31272
31509
  agent.note({ type: "handoff-declined", id, outcome: "failed", reason, ts: Date.now() });
31273
31510
  throw e;
31274
- } finally {
31275
- this.settling.delete(id);
31276
31511
  }
31277
31512
  }
31278
31513
  async ask(worktreeId, repoId, text) {
@@ -31337,6 +31572,7 @@ function match(repos, q) {
31337
31572
  // ../daemon/src/worktrees/landing.ts
31338
31573
  var TAIL_CHARS = 400;
31339
31574
  var DIFF_CHARS = 2000;
31575
+ var CONTEXT_CHARS2 = 800;
31340
31576
  var CHECK_SLOTS = 2;
31341
31577
 
31342
31578
  class LandingService {
@@ -31412,14 +31648,7 @@ class LandingService {
31412
31648
  const repo = this.d.state.requireRepo(wt.repoId);
31413
31649
  this.judging.set(worktreeId, turn.at);
31414
31650
  const started = Date.now();
31415
- const entries = this.d.transcript(worktreeId);
31416
- const prompt = landPrompt({
31417
- title: wt.title,
31418
- firstAsk: firstAskOf(entries),
31419
- turns: turnsSince(entries, 0),
31420
- diffStat: await diffSummary(wt.path, baseOf(repo)),
31421
- recentSubjects: await recentSubjects(wt.path, baseOf(repo))
31422
- });
31651
+ const prompt = landPrompt(await this.landInput(wt, repo));
31423
31652
  let verdict = null;
31424
31653
  try {
31425
31654
  verdict = await this.d.judge(wt, prompt);
@@ -31492,6 +31721,25 @@ class LandingService {
31492
31721
  return;
31493
31722
  fireAndForget(worktreeId, this.run(wt, { at: Date.now(), ask: false, quiet: true }), "landing recheck");
31494
31723
  }
31724
+ async landInput(wt, repo) {
31725
+ const base = baseOf(repo);
31726
+ const entries = this.d.transcript(wt.id);
31727
+ const [diff, subjects, committedAt] = await Promise.all([
31728
+ diffSummary(wt.id, wt.path, base),
31729
+ recentSubjects(wt.path, base),
31730
+ lastOwnCommitAt(wt.id, wt.path, base)
31731
+ ]);
31732
+ const since = Math.max(committedAt, wt.lands?.at(-1)?.at ?? 0);
31733
+ return {
31734
+ title: wt.title,
31735
+ firstAsk: firstAskOf(entries),
31736
+ turns: turnsSince(entries, since),
31737
+ earlier: since > 0,
31738
+ diffStat: diff.own,
31739
+ committedStat: diff.committed,
31740
+ recentSubjects: subjects
31741
+ };
31742
+ }
31495
31743
  async hasWork(wt) {
31496
31744
  const repo = this.d.state.requireRepo(wt.repoId);
31497
31745
  const files = await statusFiles(wt.path);
@@ -31514,14 +31762,14 @@ class LandingService {
31514
31762
  };
31515
31763
  this.judging.set(worktreeId, opts.at);
31516
31764
  const started = Date.now();
31517
- this.d.worktrees.setLanding(worktreeId, { at: opts.at, check: "pending", ready: false, fingerprint: "", ...kept });
31765
+ const pending = { at: opts.at, check: "pending", ready: false, fingerprint: "", ...kept };
31766
+ this.d.worktrees.setLanding(worktreeId, pending);
31518
31767
  const live = () => !this.stopping && this.judging.get(worktreeId) === opts.at && !!this.d.state.worktree(worktreeId);
31519
- let check = "none";
31520
- let checkTail;
31521
- let waited = 0;
31522
- let ran;
31523
31768
  const command = repo.config.check?.trim();
31524
- if (command) {
31769
+ let checkDone = false;
31770
+ const checked = async () => {
31771
+ if (!command)
31772
+ return { check: "none" };
31525
31773
  const ceiling = timeoutFor(repo.config, "check");
31526
31774
  this.d.runs.drop(worktreeId, "check");
31527
31775
  const run = this.d.runs.begin(worktreeId, "check", {
@@ -31530,9 +31778,11 @@ class LandingService {
31530
31778
  });
31531
31779
  if (!await run.admitted)
31532
31780
  return;
31533
- if (!live())
31534
- return run.finish(null);
31535
- waited = Date.now() - started;
31781
+ if (!live()) {
31782
+ run.finish(null);
31783
+ return;
31784
+ }
31785
+ const waited = Date.now() - started;
31536
31786
  let r;
31537
31787
  try {
31538
31788
  r = await this.d.check(worktreeId, command, { quiet: opts.quiet, timeoutMs: ceiling, onSpawn: run.spawned });
@@ -31543,8 +31793,8 @@ class LandingService {
31543
31793
  run.finish(r.exit);
31544
31794
  if (!live())
31545
31795
  return;
31546
- ran = r;
31547
- check = r.exit === 0 ? "pass" : "fail";
31796
+ const check = r.exit === 0 ? "pass" : "fail";
31797
+ let checkTail;
31548
31798
  if (r.exit === "timeout") {
31549
31799
  checkTail = [`gave up after ${describeDuration(ceiling)}`, tail(r.text)].filter(Boolean).join(`
31550
31800
  `);
@@ -31552,19 +31802,14 @@ class LandingService {
31552
31802
  checkTail = tail(r.text) || `exit ${r.exit}`;
31553
31803
  else
31554
31804
  this.d.hub.emit("checkPassed", worktreeId);
31555
- }
31556
- let verdict = null;
31557
- let unanswered = false;
31558
- if (opts.ask && check !== "fail" && this.d.judge) {
31559
- const entries = this.d.transcript(worktreeId);
31560
- const prompt = landPrompt({
31561
- title: wt.title,
31562
- firstAsk: firstAskOf(entries),
31563
- turns: turnsSince(entries, 0),
31564
- diffStat: await diffSummary(wt.path, baseOf(repo)),
31565
- recentSubjects: await recentSubjects(wt.path, baseOf(repo)),
31566
- ...opts.note ? { note: opts.note } : {}
31567
- });
31805
+ return { check, ran: r, waited, ...checkTail ? { checkTail } : {} };
31806
+ };
31807
+ const asked = async () => {
31808
+ if (!opts.ask || !this.d.judge)
31809
+ return { verdict: null, unanswered: false };
31810
+ const prompt = landPrompt({ ...await this.landInput(wt, repo), ...opts.note ? { note: opts.note } : {} });
31811
+ let verdict = null;
31812
+ let unanswered = false;
31568
31813
  try {
31569
31814
  verdict = await this.d.judge(wt, prompt);
31570
31815
  } catch (e) {
@@ -31575,16 +31820,29 @@ class LandingService {
31575
31820
  }
31576
31821
  if (!live())
31577
31822
  return;
31578
- }
31823
+ if (!checkDone && verdict) {
31824
+ this.recapTurn(worktreeId, verdict);
31825
+ this.d.worktrees.setLanding(worktreeId, { ...pending, ...messageOf(verdict) });
31826
+ }
31827
+ return { verdict, unanswered };
31828
+ };
31829
+ const [checkResult, askResult] = await Promise.all([
31830
+ checked().finally(() => {
31831
+ checkDone = true;
31832
+ }),
31833
+ asked()
31834
+ ]);
31835
+ if (!checkResult || !askResult || !live())
31836
+ return;
31837
+ const { check, checkTail, ran, waited = 0 } = checkResult;
31838
+ const { verdict, unanswered } = askResult;
31579
31839
  const landing = {
31580
31840
  at: opts.at,
31581
31841
  check,
31582
31842
  ...checkTail ? { checkTail } : {},
31583
31843
  ready: check !== "fail",
31584
31844
  ...kept,
31585
- ...verdict && !verdict.ready && verdict.why ? { why: verdict.why } : {},
31586
- ...verdict?.subject ? { subject: verdict.subject } : {},
31587
- ...verdict?.body ? { body: verdict.body } : {},
31845
+ ...verdict ? messageOf(verdict) : {},
31588
31846
  ...unanswered ? { unanswered: true } : {},
31589
31847
  fingerprint: await treeFingerprint(wt.path)
31590
31848
  };
@@ -31594,14 +31852,18 @@ class LandingService {
31594
31852
  if (!live())
31595
31853
  return;
31596
31854
  this.judging.delete(worktreeId);
31597
- const record = this.d.state.worktree(worktreeId)?.lastTurn;
31598
- if (record && verdict?.recap)
31599
- record.recap = { at: Date.now(), text: verdict.recap };
31855
+ if (verdict)
31856
+ this.recapTurn(worktreeId, verdict);
31600
31857
  log.info(worktreeId, `landing: check ${check}${landing.why ? ", doubted" : ""}${opts.ask ? "" : ", check only"}, ${Date.now() - started}ms${waited > 100 ? ` (waited ${waited}ms for a check slot)` : ""}`);
31601
31858
  this.d.worktrees.setLanding(worktreeId, landing);
31602
31859
  if (command && check === "fail" && ran && typeof ran.exit === "number" && !opts.quiet)
31603
31860
  this.d.hub.emit("checkFailed", worktreeId, { toolId: ran.toolId, command, text: ran.text });
31604
31861
  }
31862
+ recapTurn(worktreeId, verdict) {
31863
+ const record = this.d.state.worktree(worktreeId)?.lastTurn;
31864
+ if (record && verdict.recap)
31865
+ record.recap = { at: Date.now(), text: verdict.recap };
31866
+ }
31605
31867
  async sharedMigration(wt, repo) {
31606
31868
  if (!repo.services || keepsCopiesApart(repo.config))
31607
31869
  return;
@@ -31611,31 +31873,44 @@ class LandingService {
31611
31873
  return "this changes migrations, and the worktrees share the database";
31612
31874
  }
31613
31875
  }
31876
+ function messageOf(verdict) {
31877
+ return {
31878
+ ...!verdict.ready && verdict.why ? { why: verdict.why } : {},
31879
+ ...verdict.subject ? { subject: verdict.subject } : {},
31880
+ ...verdict.body ? { body: verdict.body } : {}
31881
+ };
31882
+ }
31614
31883
  function tail(text) {
31615
31884
  const t = text.replace(/\s+$/, "");
31616
31885
  return t.length > TAIL_CHARS ? t.slice(-TAIL_CHARS) : t;
31617
31886
  }
31618
- async function diffSummary(path, defaultBr) {
31887
+ async function diffSummary(id, path, defaultBr) {
31619
31888
  const [base, dirty, untracked] = await Promise.all([
31620
31889
  git(path, "merge-base", "HEAD", defaultBr),
31621
31890
  git(path, "diff", "HEAD", "--stat"),
31622
31891
  git(path, "ls-files", "--others", "--exclude-standard")
31623
31892
  ]);
31893
+ if (!base.ok)
31894
+ log.warn(id, `landing question: no merge base with ${defaultBr}: ${base.err.trim()}`);
31624
31895
  const committed = base.ok && base.out ? await git(path, "diff", "--stat", base.out, "HEAD") : null;
31625
- const parts = [
31626
- committed?.out ? `Committed on the branch:
31627
- ${committed.out}` : "",
31628
- dirty.out ? `Uncommitted:
31629
- ${dirty.out}` : "",
31630
- untracked.out ? `New files:
31631
- ${untracked.out}` : ""
31632
- ].filter(Boolean);
31633
- const text = parts.join(`
31896
+ const own = [dirty.out ? `Uncommitted:
31897
+ ${dirty.out}` : "", untracked.out ? `New files:
31898
+ ${untracked.out}` : ""].filter(Boolean).join(`
31634
31899
 
31635
31900
  `);
31636
- return text.length > DIFF_CHARS ? `${text.slice(0, DIFF_CHARS)}
31901
+ return { own: cap(own, DIFF_CHARS), committed: cap(committed?.out ?? "", CONTEXT_CHARS2) };
31902
+ }
31903
+ function cap(text, max) {
31904
+ return text.length > max ? `${text.slice(0, max)}
31637
31905
  ...` : text;
31638
31906
  }
31907
+ async function lastOwnCommitAt(id, path, defaultBr) {
31908
+ const r = await git(path, "log", "-1", "--no-merges", "--format=%at", `${defaultBr}..HEAD`);
31909
+ if (!r.ok)
31910
+ log.warn(id, `landing question: could not read the branch's commits over ${defaultBr}: ${r.err.trim()}`);
31911
+ const seconds = Number(r.out.trim());
31912
+ return r.ok && Number.isFinite(seconds) ? seconds * 1000 : 0;
31913
+ }
31639
31914
  async function recentSubjects(path, defaultBr) {
31640
31915
  const r = await git(path, "log", "--format=%s", "-n", "5", defaultBr);
31641
31916
  return r.ok ? r.out.split(`
@@ -32370,17 +32645,41 @@ async function landLocally(worktreePath, branch, repoPath, defaultBr, method, me
32370
32645
  }
32371
32646
  return { ok: true, message: `merged ${branch} into ${defaultBr}` };
32372
32647
  }
32373
- async function squashMessage(worktreePath, defaultBr, suggested) {
32648
+ async function squashMessage(worktreePath, defaultBr, suggested, typed = false) {
32374
32649
  const n = await git(worktreePath, "rev-list", "--count", `${defaultBr}..HEAD`);
32375
32650
  if (n.out === "1") {
32651
+ if (typed && suggested)
32652
+ return suggested;
32376
32653
  const one = await git(worktreePath, "log", "-1", "--format=%B");
32377
32654
  if (one.ok && one.out.trim())
32378
32655
  return one.out.trim();
32379
32656
  }
32380
- if (suggested)
32381
- return suggested;
32382
- const subjects = await git(worktreePath, "log", "--format=%s", `${defaultBr}..HEAD`);
32383
- return subjects.out.trim() || "land";
32657
+ const log = await git(worktreePath, "log", "--reverse", "--no-merges", "--format=%s", `${defaultBr}..HEAD`);
32658
+ const subjects = log.out.split(`
32659
+ `).filter(Boolean);
32660
+ const [first = "", ...rest] = (suggested ?? subjects.at(-1) ?? "land").split(`
32661
+ `);
32662
+ const subject = first.trim();
32663
+ const others = subjects.at(-1) === subject ? subjects.slice(0, -1) : subjects;
32664
+ const body = [
32665
+ rest.join(`
32666
+ `).trim(),
32667
+ others.length ? `Squashed with the branch's earlier commits:
32668
+ ${others.join(`
32669
+ `)}` : ""
32670
+ ].filter(Boolean).join(`
32671
+
32672
+ `);
32673
+ return body ? `${subject}
32674
+
32675
+ ${body}` : subject;
32676
+ }
32677
+ function splitMessage(text) {
32678
+ const [first = "", ...rest] = text.split(`
32679
+ `);
32680
+ const body = rest.join(`
32681
+ `).trim();
32682
+ return { subject: first.trim(), ...body ? { body } : {} };
32384
32683
  }
32385
32684
  async function fastForwardFetched(repoPath, defaultBr) {
32386
32685
  const current = await git(repoPath, "branch", "--show-current");
@@ -33104,9 +33403,19 @@ class SparePool {
33104
33403
  const added = (async () => {
33105
33404
  if (existsSync32(wt.path))
33106
33405
  return;
33107
- await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--detach", wt.path, baseOf(repo)));
33406
+ const run = this.d.runs.begin(wt.id, "setup", {
33407
+ timeoutMs: timeoutFor(repo.config, "setup"),
33408
+ stage: "checking out"
33409
+ });
33410
+ try {
33411
+ await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--detach", "--no-checkout", wt.path, baseOf(repo)));
33412
+ await withLock(`checkout:${repo.path}`, () => gitOrThrow(wt.path, "reset", "--hard"));
33413
+ } finally {
33414
+ run.finish(0);
33415
+ }
33108
33416
  })();
33109
33417
  const done = (async () => {
33418
+ this.d.runtime.markSetup(wt.id, true);
33110
33419
  try {
33111
33420
  await added;
33112
33421
  if (after) {
@@ -33136,6 +33445,7 @@ class SparePool {
33136
33445
  } catch (e) {
33137
33446
  await this.rollBack(wt, e);
33138
33447
  } finally {
33448
+ this.d.runtime.markSetup(wt.id, false);
33139
33449
  if (this.inflight.get(repoId)?.added === added)
33140
33450
  this.inflight.delete(repoId);
33141
33451
  }
@@ -33194,40 +33504,70 @@ class SparePool {
33194
33504
  this.refreshing.set(repoId, run);
33195
33505
  await run;
33196
33506
  }
33197
- async claim(repoId, title, worktreeId) {
33507
+ async claim(repoId, title, worktreeId, stamps, prepare) {
33198
33508
  const wt = this.spareOf(repoId);
33199
33509
  if (!wt)
33200
33510
  return null;
33201
33511
  if (worktreeId && wt.id !== worktreeId)
33202
33512
  return null;
33203
33513
  const wasReady = wt.phase === "ready";
33204
- if (!wasReady) {
33205
- if (!worktreeId)
33206
- return null;
33207
- const running = this.inflight.get(repoId);
33208
- await running?.added.catch(() => {});
33209
- running?.cut();
33210
- if (wt.kind !== "spare" || !existsSync32(wt.path))
33211
- return null;
33212
- }
33213
- await this.refreshing.get(repoId);
33514
+ if (!wasReady && !worktreeId)
33515
+ return null;
33214
33516
  const repo = this.d.state.requireRepo(repoId);
33215
- const branch = `toyon/${basename11(wt.path)}`;
33216
- await withRepoLock(repo.path, () => gitOrThrow(wt.path, "switch", "-c", branch));
33517
+ const running = this.inflight.get(repoId);
33518
+ const was = { title: wt.title, branch: wt.branch, phase: wt.phase, lockfile: wt.lockfile };
33217
33519
  wt.kind = "worktree";
33218
33520
  delete wt.phase;
33219
33521
  delete wt.lockfile;
33220
- wt.branch = branch;
33522
+ wt.branch = `toyon/${basename11(wt.path)}`;
33221
33523
  wt.title = title;
33222
- wt.unnamed = true;
33223
33524
  wt.createdAt = Date.now();
33224
- if (wasReady)
33225
- fireAndForget(wt.id, this.d.runtime.wake(wt.id), "wake on claim");
33525
+ wt.promptedAt = wt.createdAt;
33526
+ Object.assign(wt, Object.fromEntries(Object.entries(stamps).filter(([, v]) => v !== undefined)));
33226
33527
  const next = repo.needsSetup ? null : await this.record(repoId, repo);
33227
33528
  this.d.state.save();
33228
33529
  if (next)
33229
33530
  fireAndForget(repoId, this.warmUp(next, repo, { after: wt.id }), "spare reserve");
33230
- return wt;
33531
+ const settled = (async () => {
33532
+ try {
33533
+ await running?.added;
33534
+ running?.cut();
33535
+ if (!existsSync32(wt.path))
33536
+ throw new Error("the worktree's directory is gone");
33537
+ await this.refreshing.get(repoId);
33538
+ await prepare(wt);
33539
+ await withRepoLock(repo.path, async () => {
33540
+ await gitOrThrow(wt.path, "branch", wt.branch);
33541
+ await gitOrThrow(wt.path, "symbolic-ref", "HEAD", `refs/heads/${wt.branch}`);
33542
+ });
33543
+ if (wasReady)
33544
+ fireAndForget(wt.id, this.d.runtime.wake(wt.id), "wake on claim");
33545
+ } catch (e) {
33546
+ await this.unclaim(wt, was, stamps, next, e);
33547
+ throw e;
33548
+ }
33549
+ })();
33550
+ return { wt, settled };
33551
+ }
33552
+ async unclaim(wt, was, stamps, next, e) {
33553
+ wt.kind = "spare";
33554
+ wt.title = was.title;
33555
+ wt.branch = was.branch;
33556
+ if (was.phase)
33557
+ wt.phase = was.phase;
33558
+ if (was.lockfile)
33559
+ wt.lockfile = was.lockfile;
33560
+ for (const k of Object.keys(stamps))
33561
+ delete wt[k];
33562
+ delete wt.promptedAt;
33563
+ if (!existsSync32(wt.path)) {
33564
+ await this.rollBack(wt, e);
33565
+ return;
33566
+ }
33567
+ this.d.state.save();
33568
+ this.d.hub.emit("worktreesChanged");
33569
+ if (next)
33570
+ await this.d.discard(next.id).catch((re) => log.warn(wt.repoId, "reserved spare removal failed", re));
33231
33571
  }
33232
33572
  }
33233
33573
 
@@ -33365,23 +33705,8 @@ class TurnService {
33365
33705
  stoppedByHand = new Set;
33366
33706
  constructor(d) {
33367
33707
  this.d = d;
33368
- this.boot();
33369
33708
  d.hub.on("agentStatus", (worktreeId, status) => this.onStatus(worktreeId, status));
33370
33709
  }
33371
- boot() {
33372
- let changed = false;
33373
- for (const wt of this.d.state.worktrees) {
33374
- const turn = wt.lastTurn;
33375
- if (turn?.end !== "asking")
33376
- continue;
33377
- const { ask: _expired, ...facts } = turn.facts;
33378
- turn.end = "stopped";
33379
- turn.facts = facts;
33380
- changed = true;
33381
- }
33382
- if (changed)
33383
- this.d.state.save();
33384
- }
33385
33710
  onStatus(worktreeId, status) {
33386
33711
  const prev = this.lastStatus.get(worktreeId) ?? "idle";
33387
33712
  this.lastStatus.set(worktreeId, status);
@@ -33468,6 +33793,7 @@ var idsOf = (rows, trunks) => [
33468
33793
  ...rows.map((r) => r.id),
33469
33794
  ...Object.values(trunks).map((t) => t.id)
33470
33795
  ];
33796
+ var heldBy = (found) => `${found.name} is held by another tool${found.lockReason ? `: ${found.lockReason}` : ""}`;
33471
33797
  var commandLine = (args) => args.map((a) => /^[\w./:=@,+-]+$/.test(a) ? a : JSON.stringify(a.split(`
33472
33798
  `, 1)[0])).join(" ");
33473
33799
 
@@ -33482,6 +33808,8 @@ class WorktreeService {
33482
33808
  archiving = new Map;
33483
33809
  restoring = new Map;
33484
33810
  going = new Set;
33811
+ arriving = new Set;
33812
+ firstPrompt = new Map;
33485
33813
  counts;
33486
33814
  handChecked = new Map;
33487
33815
  naming = new Set;
@@ -33496,7 +33824,7 @@ class WorktreeService {
33496
33824
  this.archive = new WorktreeArchive(d.paths.archiveDir);
33497
33825
  this.counts = new Counts({
33498
33826
  read: (id) => this.readCounts(id),
33499
- going: (id) => this.going.has(id)
33827
+ going: (id) => this.going.has(id) || this.arriving.has(id)
33500
33828
  });
33501
33829
  this.trunk = new Trunk({
33502
33830
  state: d.state,
@@ -33509,6 +33837,7 @@ class WorktreeService {
33509
33837
  hub: d.hub,
33510
33838
  runtime: d.runtime,
33511
33839
  paths: d.paths,
33840
+ runs: this.runs,
33512
33841
  setupAndStart: (wt, repo) => this.setupAndStart(wt, repo),
33513
33842
  teardown: (wt, repo) => this.teardown(wt, repo),
33514
33843
  discard: async (id) => {
@@ -33580,39 +33909,55 @@ class WorktreeService {
33580
33909
  const agentPrompt = variant && variant.of >= 2 ? [prompt, variantLens(variant.index)].filter(Boolean).join(`
33581
33910
 
33582
33911
  `) : prompt;
33583
- const claimed = await this.spare.claim(repoId, title, opts.worktreeId);
33584
- if (claimed) {
33585
- if (variant)
33586
- claimed.variant = variant;
33587
- if (opts.createdBy)
33588
- claimed.createdBy = opts.createdBy;
33589
- if (given)
33590
- delete claimed.unnamed;
33591
- if (opts.from)
33592
- claimed.from = opts.from;
33593
- claimed.promptedAt = claimed.createdAt;
33594
- claimed.agent = agent;
33595
- if (opts.mode)
33596
- claimed.mode = opts.mode;
33597
- if (opts.model)
33598
- claimed.model = opts.model;
33599
- if (opts.effort)
33600
- claimed.effort = opts.effort;
33601
- if (profile !== undefined && profile !== resolveRun(repo, claimed).profile) {
33602
- claimed.profile = profile;
33603
- this.restartProcs(claimed, repo);
33604
- }
33605
- const carried = await this.carryMain(repo, claimed, !!opts.carry);
33606
- this.d.state.save();
33607
- this.d.hub.emit("worktreesChanged");
33608
- const rt = this.d.runtime.ensureAgent(claimed);
33912
+ const spare = this.d.state.worktrees.find((w) => w.repoId === repoId && w.kind === "spare");
33913
+ const restart = !!spare && profile !== undefined && profile !== resolveRun(repo, spare).profile;
33914
+ let carriedIn;
33915
+ let release;
33916
+ const stamps = {
33917
+ createdBy: opts.createdBy,
33918
+ agent,
33919
+ mode: opts.mode,
33920
+ model: opts.model,
33921
+ effort: opts.effort,
33922
+ variant,
33923
+ from: opts.from,
33924
+ profile,
33925
+ unnamed: given ? undefined : true
33926
+ };
33927
+ const claimed = await this.spare.claim(repoId, title, opts.worktreeId, stamps, async (wt) => {
33928
+ const rt = this.d.runtime.ensureAgent(wt);
33929
+ release = rt.agent.hold();
33609
33930
  if (rt.agent.runningAgent !== null && rt.agent.runningAgent !== agent)
33610
33931
  await rt.agent.restart();
33611
- rt.agent.send(agentPrompt, { context: withCarry(context(claimed.id), carried), attachments });
33612
- this.nameOrAsk(claimed, task, given, variant);
33613
- if (carried.unmoved)
33614
- throw new UserError(carried.unmoved, { delivered: true });
33615
- return claimed;
33932
+ carriedIn = await this.carryMain(repo, wt, !!opts.carry);
33933
+ rt.agent.send(agentPrompt, { context: withCarry(context(wt.id), carriedIn), attachments });
33934
+ });
33935
+ if (claimed) {
33936
+ const { wt } = claimed;
33937
+ const rt = this.d.runtime.ensureAgent(wt);
33938
+ this.firstPrompt.set(wt.id, claimed.settled.then(() => {
33939
+ return;
33940
+ }, () => {
33941
+ return;
33942
+ }));
33943
+ this.d.hub.emit("worktreesChanged");
33944
+ try {
33945
+ await claimed.settled;
33946
+ } catch (e) {
33947
+ if (carriedIn && rt.agent.queueLength > 0)
33948
+ rt.agent.unqueue(0);
33949
+ release?.();
33950
+ throw e instanceof UserError ? e : new UserError(e instanceof Error ? e.message : String(e));
33951
+ } finally {
33952
+ this.firstPrompt.delete(wt.id);
33953
+ }
33954
+ release?.();
33955
+ if (restart)
33956
+ this.restartProcs(wt);
33957
+ this.nameOrAsk(wt, task, given, variant);
33958
+ if (carriedIn?.unmoved)
33959
+ throw new UserError(carriedIn.unmoved, { delivered: true });
33960
+ return wt;
33616
33961
  }
33617
33962
  if (!await isGitRepo(repo.path)) {
33618
33963
  throw new UserError(`git no longer reads ${repo.name} as a checkout, so there is nothing to start a worktree from. If \`git config core.bare\` says true there, \`git config --unset core.bare\` puts it back.`);
@@ -33773,8 +34118,9 @@ class WorktreeService {
33773
34118
  this.launch(wt, repo, repo.path);
33774
34119
  return wt;
33775
34120
  }
33776
- async requireDiscovered(repoId, repoPath, target) {
33777
- const rows = await discoverIn(repoId, repoPath, this.d.state.worktrees);
34121
+ async requireDiscovered(repoId, repoPath, target, except) {
34122
+ const known = this.d.state.worktrees.filter((w) => w.id !== except);
34123
+ const rows = await discoverIn(repoId, repoPath, known);
33778
34124
  const found = rows.find((r) => canonical(r.path) === target);
33779
34125
  if (!found)
33780
34126
  throw new UserError("that worktree is gone, or Toyon already has it");
@@ -33789,38 +34135,50 @@ class WorktreeService {
33789
34135
  const repoId = r.repoId;
33790
34136
  const repo = this.d.state.requireRepo(repoId);
33791
34137
  const target = canonical(r.path);
33792
- const wt = await withRepoLock(repo.path, async () => {
33793
- const found = await this.requireDiscovered(repoId, repo.path, target);
33794
- if (found.locked) {
33795
- throw new UserError(`${found.name} is held by another tool${found.lockReason ? `: ${found.lockReason}` : ""}`);
33796
- }
33797
- if (!found.branch)
33798
- throw new UserError(`${found.name} is detached: check out a branch in it first`);
33799
- const enclosing = [...this.d.state.repos, ...this.d.state.worktrees].find((r) => isInside(target, canonical(r.path)));
33800
- if (enclosing)
33801
- throw new UserError(`${found.name} sits inside ${enclosing.path}, which Toyon already manages`);
33802
- const rec = {
33803
- id: shortId(),
33804
- repoId,
33805
- path: found.path,
33806
- branch: found.branch,
33807
- kind: "worktree",
33808
- proxyPort: await allocateProxyPort(),
33809
- title: found.name,
33810
- createdAt: Date.now(),
33811
- agent: this.d.agents.require(this.d.state.defaultAgent ?? DEFAULT_AGENT_ID).id,
33812
- parked: true,
33813
- ...message ? { promptedAt: Date.now() } : {},
33814
- ...createdBy ? { createdBy } : {}
33815
- };
33816
- return rec;
33817
- });
34138
+ const pushed = this.discoveredById(worktreeId);
34139
+ if (pushed?.locked)
34140
+ throw new UserError(heldBy(pushed));
34141
+ if (!r.branch)
34142
+ throw new UserError(`${r.name} is detached: check out a branch in it first`);
34143
+ const enclosing = [...this.d.state.repos, ...this.d.state.worktrees].find((w) => isInside(target, canonical(w.path)));
34144
+ if (enclosing)
34145
+ throw new UserError(`${r.name} sits inside ${enclosing.path}, which Toyon already manages`);
34146
+ const wt = {
34147
+ id: shortId(),
34148
+ repoId,
34149
+ path: r.path,
34150
+ branch: r.branch,
34151
+ kind: "worktree",
34152
+ proxyPort: await allocateProxyPort(),
34153
+ title: r.name,
34154
+ createdAt: Date.now(),
34155
+ agent: this.d.agents.require(this.d.state.defaultAgent ?? DEFAULT_AGENT_ID).id,
34156
+ parked: true,
34157
+ ...message ? { promptedAt: Date.now() } : {},
34158
+ ...createdBy ? { createdBy } : {}
34159
+ };
34160
+ const { rt, release } = this.listAhead(wt, message);
33818
34161
  this.invalidateDiscovered();
33819
- this.launch(wt, repo, repo.path, { setupCommands: false });
33820
- const { agent } = this.d.runtime.ensureAgent(wt);
33821
- agent.note({ type: "adopted", branch: wt.branch, ts: Date.now() });
33822
- if (message)
33823
- agent.send(message.text, { attachments: message.attachments });
34162
+ this.d.hub.emit("worktreesChanged");
34163
+ try {
34164
+ await withRepoLock(repo.path, async () => {
34165
+ const found = await this.requireDiscovered(repoId, repo.path, target, wt.id);
34166
+ if (found.locked)
34167
+ throw new UserError(heldBy(found));
34168
+ if (!found.branch)
34169
+ throw new UserError(`${found.name} is detached: check out a branch in it first`);
34170
+ wt.branch = found.branch;
34171
+ });
34172
+ } catch (e) {
34173
+ await this.unlist(wt, !!message);
34174
+ this.invalidateDiscovered();
34175
+ this.d.hub.emit("worktreesChanged");
34176
+ throw e;
34177
+ }
34178
+ this.d.state.save();
34179
+ fireAndForget(wt.id, this.setupAndStart(wt, repo, repo.path, { setupCommands: false }), "setup + start");
34180
+ rt.agent.note({ type: "adopted", branch: wt.branch, ts: Date.now() });
34181
+ release();
33824
34182
  return wt;
33825
34183
  }
33826
34184
  async ownProcs(worktreeId, names) {
@@ -33852,8 +34210,8 @@ class WorktreeService {
33852
34210
  throw new UserError(`no profile "${name}" in ${repo.configFile}`);
33853
34211
  return name;
33854
34212
  }
33855
- restartProcs(wt, repo) {
33856
- fireAndForget(wt.id, this.d.runtime.stopProcs(wt.id).then(() => this.d.runtime.start(wt, repo)), "profile restart");
34213
+ restartProcs(wt) {
34214
+ fireAndForget(wt.id, this.d.runtime.stopProcs(wt.id).then(() => this.d.runtime.wake(wt.id)), "profile restart");
33857
34215
  }
33858
34216
  setProfile(worktreeId, name) {
33859
34217
  const { wt, repo } = this.d.state.requireWorktreeWithRepo(worktreeId);
@@ -33865,7 +34223,7 @@ class WorktreeService {
33865
34223
  wt.profile = profile;
33866
34224
  this.d.state.save();
33867
34225
  this.d.hub.emit("worktreesChanged");
33868
- this.restartProcs(wt, repo);
34226
+ this.restartProcs(wt);
33869
34227
  }
33870
34228
  setMode(worktreeId, mode) {
33871
34229
  if (!this.d.state.setMode(worktreeId, mode))
@@ -34235,66 +34593,113 @@ class WorktreeService {
34235
34593
  if (rec.auto)
34236
34594
  log.info(old.id, `restoring a worktree that archived itself (${rec.auto})`);
34237
34595
  const path = existsSync33(old.path) ? join30(dirname12(old.path), freeSlot(dirname12(old.path)).dir) : old.path;
34238
- const branch = await withRepoLock(repo.path, async () => {
34239
- if (!await commitOf(repo.path, archiveRef(old.id))) {
34240
- throw new UserError(`${old.title}'s kept commits are gone from git`);
34241
- }
34242
- let name = old.branch;
34243
- let create = true;
34244
- const tip = await commitOf(repo.path, `refs/heads/${name}`);
34245
- if (tip && hasOwnBranch(old)) {
34246
- name = `${name}-${shortId().slice(0, 3)}`;
34247
- } else if (tip) {
34248
- if (tip !== kept.head && kept.snapshot) {
34249
- throw new UserError(`${name} has moved since ${old.title} was archived: check it out in git instead`);
34250
- }
34251
- create = false;
34252
- }
34253
- try {
34254
- await checkOutKept(repo.path, path, kept, { name, create });
34255
- } catch (e) {
34256
- await git(repo.path, "worktree", "remove", "--force", path);
34257
- if (create)
34258
- await git(repo.path, "branch", "-D", name);
34259
- throw e;
34260
- }
34261
- return name;
34262
- });
34263
34596
  const { variant: _variant, ...rest } = old;
34264
34597
  const wt = {
34265
34598
  ...rest,
34266
34599
  repoId: repo.id,
34267
34600
  path,
34268
- branch,
34601
+ branch: old.branch,
34269
34602
  proxyPort: await allocateProxyPort(),
34270
34603
  promptedAt: Date.now(),
34271
34604
  parked: true,
34272
34605
  ...createdBy ? { createdBy } : {}
34273
34606
  };
34607
+ const chat = { ...this.chatFiles(old.id), plans: this.plansScratch(old.id) };
34274
34608
  try {
34275
- this.archive.take(old.id, { ...this.chatFiles(old.id), plans: join30(path, PLANS_DIR) });
34609
+ this.archive.take(old.id, chat);
34276
34610
  } catch (e) {
34277
34611
  log.warn(old.id, "could not move its chat back out of the archive", e);
34612
+ throw new UserError(`could not move ${old.title}'s chat back out of the archive`);
34278
34613
  }
34279
34614
  if (rec.sessionId && path === old.path)
34280
34615
  this.d.state.setSession(wt.id, rec.sessionId);
34616
+ this.arriving.add(wt.id);
34617
+ const { rt, release } = this.listAhead(wt, message);
34618
+ const run = this.runs.begin(wt.id, "setup", {
34619
+ timeoutMs: timeoutFor(repo.config, "setup"),
34620
+ stage: "checking out the branch"
34621
+ });
34622
+ this.d.hub.emit("worktreesChanged");
34623
+ this.d.hub.emit("archiveChanged", repo.id);
34624
+ let branch;
34625
+ try {
34626
+ branch = await withRepoLock(repo.path, () => this.checkOutArchived(repo, old, kept, path));
34627
+ } catch (e) {
34628
+ await this.takeBack(wt, rec, chat, run, !!message);
34629
+ throw e;
34630
+ }
34631
+ run.finish(0);
34632
+ this.arriving.delete(wt.id);
34633
+ wt.branch = branch;
34634
+ move(chat.plans, join30(path, PLANS_DIR));
34281
34635
  const dropped = await git(repo.path, "update-ref", "-d", archiveRef(old.id));
34282
34636
  if (!dropped.ok)
34283
34637
  log.warn(old.id, `could not drop its archive ref: ${dropped.err}`);
34284
34638
  this.counts.stale(wt.id);
34285
- this.launch(wt, repo, repo.path);
34286
- this.d.runtime.ensureAgent(wt).agent.note({
34639
+ this.d.state.save();
34640
+ this.d.hub.emit("worktreesChanged");
34641
+ fireAndForget(wt.id, this.setupAndStart(wt, repo, repo.path), "setup + start");
34642
+ rt.agent.note({
34287
34643
  type: "restored",
34288
34644
  archivedAt: rec.archivedAt,
34289
34645
  branch,
34290
34646
  uncommitted: !!kept.snapshot,
34291
34647
  ts: Date.now()
34292
34648
  });
34293
- if (message)
34294
- this.d.runtime.ensureAgent(wt).agent.send(message.text, { attachments: message.attachments });
34295
- this.d.hub.emit("archiveChanged", repo.id);
34649
+ release();
34650
+ this.d.hub.emit("filesChanged", wt.id);
34296
34651
  return wt;
34297
34652
  }
34653
+ async checkOutArchived(repo, old, kept, path) {
34654
+ if (!await commitOf(repo.path, archiveRef(old.id))) {
34655
+ throw new UserError(`${old.title}'s kept commits are gone from git`);
34656
+ }
34657
+ let name = old.branch;
34658
+ let create = true;
34659
+ const tip = await commitOf(repo.path, `refs/heads/${name}`);
34660
+ if (tip && hasOwnBranch(old)) {
34661
+ name = `${name}-${shortId().slice(0, 3)}`;
34662
+ } else if (tip) {
34663
+ if (tip !== kept.head && kept.snapshot) {
34664
+ throw new UserError(`${name} has moved since ${old.title} was archived: check it out in git instead`);
34665
+ }
34666
+ create = false;
34667
+ }
34668
+ try {
34669
+ await checkOutKept(repo.path, path, kept, { name, create });
34670
+ } catch (e) {
34671
+ await git(repo.path, "worktree", "remove", "--force", path);
34672
+ if (create)
34673
+ await git(repo.path, "branch", "-D", name);
34674
+ throw e;
34675
+ }
34676
+ return name;
34677
+ }
34678
+ listAhead(wt, message) {
34679
+ this.d.runtime.markSetup(wt.id, true);
34680
+ this.d.state.addWorktree(wt);
34681
+ const rt = this.d.runtime.ensureAgent(wt);
34682
+ const release = rt.agent.hold();
34683
+ if (message)
34684
+ rt.agent.send(message.text, { attachments: message.attachments });
34685
+ return { rt, release };
34686
+ }
34687
+ async takeBack(wt, rec, chat, run, queued) {
34688
+ run.finish(0);
34689
+ await this.unlist(wt, queued);
34690
+ this.arriving.delete(wt.id);
34691
+ this.archive.put(rec, chat);
34692
+ this.d.hub.emit("worktreesChanged");
34693
+ this.d.hub.emit("archiveChanged", wt.repoId);
34694
+ }
34695
+ async unlist(wt, queued) {
34696
+ const agent = this.d.runtime.agentFor(wt.id);
34697
+ if (queued && agent && agent.queueLength > 0)
34698
+ agent.unqueue(0);
34699
+ await this.d.runtime.stop(wt.id);
34700
+ this.d.state.removeWorktree(wt.id, { refused: true });
34701
+ releasePort(wt.proxyPort);
34702
+ }
34298
34703
  async deleteArchived(archiveId) {
34299
34704
  const rec = this.archive.get(archiveId);
34300
34705
  if (!rec)
@@ -34432,7 +34837,9 @@ class WorktreeService {
34432
34837
  const srcNm = join30(depsSource, "node_modules");
34433
34838
  const dstNm = join30(wt.path, "node_modules");
34434
34839
  if (existsSync33(srcNm) && !existsSync33(dstNm)) {
34840
+ const run = this.runs.begin(wt.id, "setup", { timeoutMs: ceiling, stage: "copying dependencies" });
34435
34841
  const r = await cloneTree(srcNm, dstNm);
34842
+ run.finish(0);
34436
34843
  if (r.ok)
34437
34844
  this.d.hub.emit("log", wt.id, "setup", `deps via ${r.how} in ${r.ms}ms`);
34438
34845
  else
@@ -34711,9 +35118,10 @@ ${wt.landing.body}` : s;
34711
35118
  const policy = landPolicy(repo.config);
34712
35119
  const own = hasOwnBranch(wt);
34713
35120
  const w = this.landWatch(wt);
34714
- const suggested = wt.landing?.subject ? wt.landing.body ? `${wt.landing.subject}
35121
+ const typed = message?.trim();
35122
+ const suggested = typed || (wt.landing?.subject ? wt.landing.body ? `${wt.landing.subject}
34715
35123
 
34716
- ${wt.landing.body}` : wt.landing.subject : undefined;
35124
+ ${wt.landing.body}` : wt.landing.subject : undefined);
34717
35125
  const base = baseOf(repo);
34718
35126
  if (policy.land === "pr") {
34719
35127
  if (wt.pr?.state === "merged" && !prTaken(wt)) {
@@ -34725,7 +35133,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
34725
35133
  return { result: fetched };
34726
35134
  if (wt.pr?.state === "open") {
34727
35135
  const dirty = (await statusFiles(wt.path)).length > 0;
34728
- const missing = dirty || (await aheadUpstream(wt.path) ?? 0) > 0;
35136
+ const missing = dirty || (await aheadUpstream(wt.path, wt.branch) ?? 0) > 0;
34729
35137
  if (!missing)
34730
35138
  return { result: await mergePr(wt.path, wt.pr.number, policy.merge, w) };
34731
35139
  const committed = await this.commitIfDirty(wt, message);
@@ -34757,13 +35165,14 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
34757
35165
  const result = { ...r, message: r.ok ? `${on}; ${r.message}` : `${on}, but ${r.message}` };
34758
35166
  return r.ok ? { result, archiveIds: this.siblingIds(wt), mark: noted } : { result };
34759
35167
  }
35168
+ const named = !suggested ? undefined : splitMessage(policy.merge === "squash" ? await squashMessage(wt.path, base, suggested, !!typed) : suggested);
34760
35169
  const result = await openPr({
34761
35170
  worktreePath: wt.path,
34762
35171
  branch: wt.branch,
34763
35172
  defaultBr: repo.defaultBranch,
34764
35173
  base,
34765
- subject: wt.landing?.subject,
34766
- body: wt.landing?.body,
35174
+ subject: named?.subject,
35175
+ body: named?.body,
34767
35176
  automerge: policy.automerge,
34768
35177
  method: policy.merge
34769
35178
  }, w);
@@ -34792,7 +35201,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
34792
35201
  if (!taken.ok)
34793
35202
  return taken;
34794
35203
  const mark = await landingMark(wt.path, base);
34795
- const squash = method === "squash" ? await squashMessage(wt.path, base, suggested) : "";
35204
+ const squash = method === "squash" ? await squashMessage(wt.path, base, suggested, !!message?.trim()) : "";
34796
35205
  const landed = await landLocally(wt.path, wt.branch, repo.path, repo.defaultBranch, method, squash, w);
34797
35206
  if (!landed.ok)
34798
35207
  return landed;
@@ -34843,7 +35252,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
34843
35252
  sha = await commitOf(wt.path, base) ?? "";
34844
35253
  break;
34845
35254
  }
34846
- squash = method === "squash" ? await squashMessage(wt.path, base, suggested) : "";
35255
+ squash = method === "squash" ? await squashMessage(wt.path, base, suggested, !!message?.trim()) : "";
34847
35256
  const built = await landingCommit(wt.path, wt.branch, base, method, squash, own, w);
34848
35257
  await this.headMoved(wt.id);
34849
35258
  if (!built.ok || !built.sha)
@@ -34895,8 +35304,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
34895
35304
  async landedWhat(repo, wt, mark, opts) {
34896
35305
  if (opts.pr?.title)
34897
35306
  return [opts.pr.title];
34898
- const subject = opts.squash?.split(`
34899
- `)[0]?.trim();
35307
+ const subject = opts.squash ? splitMessage(opts.squash).subject : "";
34900
35308
  if (subject)
34901
35309
  return [subject];
34902
35310
  const read = await rangeSubjects(repo.path, mark.base, mark.tip);
@@ -35157,9 +35565,10 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
35157
35565
  return { ...ab, ...unpushed === undefined ? {} : { unpushed }, dirty };
35158
35566
  }
35159
35567
  async unpushed(id, path) {
35160
- if (this.d.state.worktree(id)?.pr?.state !== "open")
35568
+ const wt = this.d.state.worktree(id);
35569
+ if (wt?.pr?.state !== "open")
35161
35570
  return;
35162
- return await aheadUpstream(path) ?? undefined;
35571
+ return await aheadUpstream(path, wt.branch) ?? undefined;
35163
35572
  }
35164
35573
  async freshCounts(worktreeId) {
35165
35574
  if (this.going.has(worktreeId))
@@ -35202,7 +35611,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
35202
35611
  await markTurn(wt.path, wt.id);
35203
35612
  }
35204
35613
  async gitStatus(worktreeId) {
35205
- if (this.going.has(worktreeId))
35614
+ if (this.going.has(worktreeId) || this.arriving.has(worktreeId))
35206
35615
  return null;
35207
35616
  const r = this.readable(worktreeId);
35208
35617
  if (!r)
@@ -35245,7 +35654,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
35245
35654
  }
35246
35655
  if (r.wt && isMain)
35247
35656
  this.setEmpty(r.wt, files.length === 0 ? await treeEmpty(r.path) : false);
35248
- if (this.going.has(worktreeId))
35657
+ if (this.going.has(worktreeId) || this.arriving.has(worktreeId))
35249
35658
  return null;
35250
35659
  this.noteCounts(worktreeId, isMain, files.length, counts);
35251
35660
  return { files, committed, ...turn ? { turn } : {}, head: head.ok ? head.out : undefined, ...counts };
@@ -35291,6 +35700,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
35291
35700
  const wt = this.requireRun(worktreeId);
35292
35701
  if (wt.kind === "main")
35293
35702
  throw new UserError("nothing runs on main: the plus starts a worktree for it");
35703
+ await this.firstPrompt.get(worktreeId);
35294
35704
  const agent = this.d.runtime.agentFor(worktreeId);
35295
35705
  if (!agent)
35296
35706
  throw new UserError("worktree still starting; try again in a moment");
@@ -35496,7 +35906,8 @@ var {
35496
35906
  helperStub: HELPER_STUB,
35497
35907
  cliEntry: CLI_ENTRY,
35498
35908
  sourceRoot: SOURCE_ROOT,
35499
- packageJson: PACKAGE_JSON
35909
+ packageJson: PACKAGE_JSON,
35910
+ changelog: CHANGELOG
35500
35911
  } = locateAssets(here);
35501
35912
  var pruned = pruneAssets(SHELL_DIST);
35502
35913
  if (pruned > 0)
@@ -35740,6 +36151,7 @@ var {
35740
36151
  port,
35741
36152
  token,
35742
36153
  shellDist: SHELL_DIST,
36154
+ changelog: CHANGELOG,
35743
36155
  version: package_default.version,
35744
36156
  noteShellOrigin: (origin) => bridge.learnShellOrigin(origin),
35745
36157
  startLink: () => helper?.startLink() ?? null,