@staff0rd/assist 0.773.1 → 0.774.1

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.
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@staff0rd/assist",
9
- version: "0.773.1",
9
+ version: "0.774.1",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -546,6 +546,13 @@ var LEGACY_SESSIONS_KEYS = [
546
546
  "windowsDaemonPort",
547
547
  "windowsVersionCheck"
548
548
  ];
549
+ var LEGACY_WORKTREE_KEYS = ["watcher"];
550
+ function withoutKeys(value, keys) {
551
+ if (!value || typeof value !== "object") return value;
552
+ const rest = { ...value };
553
+ for (const key of keys) delete rest[key];
554
+ return rest;
555
+ }
549
556
  function stripLegacyConfigKeys(config) {
550
557
  const stripped = { ...config };
551
558
  const news = stripped.news;
@@ -553,12 +560,10 @@ function stripLegacyConfigKeys(config) {
553
560
  const { feeds: _feeds, ...rest } = news;
554
561
  stripped.news = rest;
555
562
  }
556
- const sessions = stripped.sessions;
557
- if (sessions && typeof sessions === "object") {
558
- const rest = { ...sessions };
559
- for (const key of LEGACY_SESSIONS_KEYS) delete rest[key];
560
- stripped.sessions = rest;
561
- }
563
+ if (stripped.sessions)
564
+ stripped.sessions = withoutKeys(stripped.sessions, LEGACY_SESSIONS_KEYS);
565
+ if (stripped.worktree)
566
+ stripped.worktree = withoutKeys(stripped.worktree, LEGACY_WORKTREE_KEYS);
562
567
  return stripped;
563
568
  }
564
569
 
@@ -906,9 +911,11 @@ var assistConfigShape = {
906
911
  })
907
912
  ).default([])
908
913
  }).optional(),
914
+ autoUpdate: z5.strictObject({
915
+ enabled: z5.boolean().default(true)
916
+ }).optional(),
909
917
  worktree: z5.strictObject({
910
918
  enabled: z5.boolean().default(false),
911
- watcher: z5.boolean().default(false),
912
919
  trunk: z5.boolean().default(false),
913
920
  includeDrafts: z5.boolean().default(false),
914
921
  root: z5.string().optional(),
@@ -11241,7 +11248,7 @@ var defaultDeps = {
11241
11248
  httpsPort: tailnetHttpsPort
11242
11249
  };
11243
11250
  async function ensureTailscaleServe(port, deps2 = {}) {
11244
- const { enabled, status: status2, serveStatus, serve, sleep: sleep2, httpsPort } = {
11251
+ const { enabled, status: status2, serveStatus, serve, sleep: sleep3, httpsPort } = {
11245
11252
  ...defaultDeps,
11246
11253
  ...deps2
11247
11254
  };
@@ -11256,7 +11263,7 @@ async function ensureTailscaleServe(port, deps2 = {}) {
11256
11263
  return `serving ${plan2.target} at ${plan2.url}`;
11257
11264
  } catch (error) {
11258
11265
  if (attempt >= SERVE_WRITE_ATTEMPTS) throw error;
11259
- await sleep2(attempt * 500 + Math.random() * 500);
11266
+ await sleep3(attempt * 500 + Math.random() * 500);
11260
11267
  }
11261
11268
  }
11262
11269
  }
@@ -12786,6 +12793,15 @@ var harnessConfigHelp = [
12786
12793
  }
12787
12794
  ];
12788
12795
 
12796
+ // src/commands/sessions/daemon/autoUpdateConfigHelp.ts
12797
+ var autoUpdateConfigHelp = [
12798
+ {
12799
+ key: "autoUpdate.enabled",
12800
+ setter: "assist config set autoUpdate.enabled false -g",
12801
+ note: "when this install is a git clone, the daemon pulls and builds it in the background as origin moves (assist watch wait --pull --build), logging each lap to ~/.assist/watchers/; a branch that cannot fast-forward starts a claude session to reconcile it and the loop resumes once it closes. Set false to stop it (default on)"
12802
+ }
12803
+ ];
12804
+
12789
12805
  // src/commands/branch/branchConfigHelp.ts
12790
12806
  var branchConfigHelp = [
12791
12807
  {
@@ -13279,11 +13295,6 @@ var sessionsConfigHelp = [
13279
13295
  setter: "assist config set worktree.enabled true -g --repo",
13280
13296
  note: "opt in per repo: spill concurrent sessions into adjacent <clone>-N worktrees (default off)"
13281
13297
  },
13282
- {
13283
- key: "worktree.watcher",
13284
- setter: "assist config set worktree.watcher true -g --repo",
13285
- note: "a backlog run also starts one starred console session in the clone running assist watch loop (no claude, relaunched after a daemon restart), so the built version stays current while the run works in its worktree; requires worktree.enabled and an auto-build run entry (default off)"
13286
- },
13287
13298
  {
13288
13299
  key: "worktree.trunk",
13289
13300
  setter: "assist config set worktree.trunk true -g --repo",
@@ -13390,6 +13401,7 @@ var verifyConfigHelp = [
13390
13401
  var configHelpEntries = [
13391
13402
  ...Object.values(rootConfigHelp).flat(),
13392
13403
  ...adviceConfigHelp,
13404
+ ...autoUpdateConfigHelp,
13393
13405
  ...backlogConfigHelp,
13394
13406
  ...backupConfigHelp,
13395
13407
  ...branchConfigHelp,
@@ -21316,10 +21328,15 @@ import { readFile as readFile4 } from "fs/promises";
21316
21328
  import { join as join49 } from "path";
21317
21329
  import chalk138 from "chalk";
21318
21330
 
21331
+ // src/commands/criteriaExtension/latestUpdate.ts
21332
+ function latestUpdate(manifest, updates) {
21333
+ const { id } = JSON.parse(manifest).browser_specific_settings.gecko;
21334
+ return JSON.parse(updates).addons[id]?.updates.at(-1);
21335
+ }
21336
+
21319
21337
  // src/commands/criteriaExtension/latestUpdateLink.ts
21320
21338
  function latestUpdateLink(manifest, updates) {
21321
- const { id } = JSON.parse(manifest).browser_specific_settings.gecko;
21322
- return JSON.parse(updates).addons[id]?.updates.at(-1)?.update_link;
21339
+ return latestUpdate(manifest, updates)?.update_link;
21323
21340
  }
21324
21341
 
21325
21342
  // src/commands/criteriaExtension/printCriteriaExtensionUrl.ts
@@ -25856,7 +25873,6 @@ function wouldDisturbWorkInProgress(clone, forCheckout) {
25856
25873
  // src/commands/sessions/daemon/worktree/worktreeConfigFor.ts
25857
25874
  var DISABLED = {
25858
25875
  enabled: false,
25859
- watcher: false,
25860
25876
  trunk: false,
25861
25877
  includeDrafts: false,
25862
25878
  install: true,
@@ -25989,7 +26005,6 @@ function toPersistedSession(session) {
25989
26005
  assistArgs: session.assistArgs,
25990
26006
  activity: session.activity,
25991
26007
  starred: session.starred,
25992
- watcher: session.watcher,
25993
26008
  divergenceEscalation: session.divergenceEscalation,
25994
26009
  design: session.design,
25995
26010
  auto: session.auto,
@@ -41400,7 +41415,6 @@ function toSessionInfo(session) {
41400
41415
  autoRun,
41401
41416
  autoAdvance,
41402
41417
  starred,
41403
- watcher,
41404
41418
  design,
41405
41419
  verifying,
41406
41420
  pendingPrPreview,
@@ -41427,7 +41441,6 @@ function toSessionInfo(session) {
41427
41441
  autoRun,
41428
41442
  autoAdvance,
41429
41443
  starred,
41430
- watcher,
41431
41444
  design,
41432
41445
  verifying,
41433
41446
  pendingPrPreview,
@@ -41983,6 +41996,359 @@ function createRunSession(id, { runName, runArgs, cwd, meta }) {
41983
41996
  };
41984
41997
  }
41985
41998
 
41999
+ // src/commands/sessions/daemon/autoUpdate/startAutoUpdate.ts
42000
+ import { setTimeout as sleep2 } from "timers/promises";
42001
+
42002
+ // src/commands/sessions/daemon/watcherNote.ts
42003
+ function watcherNote(text17) {
42004
+ return `\r
42005
+ \x1B[2m\u2500\u2500 ${text17} \u2500\u2500\x1B[0m\r
42006
+ `;
42007
+ }
42008
+
42009
+ // src/commands/sessions/daemon/worktree/appendWatcherLog.ts
42010
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync34 } from "fs";
42011
+ import { dirname as dirname42 } from "path";
42012
+
42013
+ // src/commands/sessions/daemon/worktree/watcherLogPath.ts
42014
+ import { createHash as createHash4 } from "crypto";
42015
+ import { homedir as homedir26 } from "os";
42016
+ import { basename as basename28, join as join101 } from "path";
42017
+ function watcherLogPath(cwd) {
42018
+ const clone = canonicalTreePath(cwd);
42019
+ const key = createHash4("sha1").update(clone).digest("hex").slice(0, 8);
42020
+ const dir = process.env.ASSIST_WATCHER_LOG_DIR ?? join101(homedir26(), ".assist", "watchers");
42021
+ return join101(dir, `${basename28(clone) || "root"}-${key}.log`);
42022
+ }
42023
+
42024
+ // src/commands/sessions/daemon/worktree/appendWatcherLog.ts
42025
+ function appendWatcherLog(cwd, data) {
42026
+ try {
42027
+ const path106 = watcherLogPath(cwd);
42028
+ mkdirSync34(dirname42(path106), { recursive: true });
42029
+ appendFileSync3(path106, data);
42030
+ } catch {
42031
+ }
42032
+ }
42033
+
42034
+ // src/commands/sessions/daemon/worktree/detectExistingWorktree.ts
42035
+ function detectExistingWorktree(cwd) {
42036
+ const repoRoot2 = findRepoRoot(cwd) ?? cwd;
42037
+ if (!worktreeConfigFor(repoRoot2).enabled) return void 0;
42038
+ const clone = mainWorktree(repoRoot2);
42039
+ if (clone && clone !== repoRoot2) return { path: repoRoot2, clone };
42040
+ return void 0;
42041
+ }
42042
+
42043
+ // src/commands/sessions/daemon/worktree/bindNewWorktree.ts
42044
+ function bindNewWorktree(session, alloc, notify2, startHeld = () => {
42045
+ }) {
42046
+ if (alloc.kind !== "worktree" || !alloc.cwd || !alloc.clone || !session)
42047
+ return;
42048
+ session.worktree = { path: alloc.cwd, clone: alloc.clone };
42049
+ if (alloc.created)
42050
+ seedWorktree(alloc.cwd, alloc.clone, () => startHeld(session));
42051
+ notify2();
42052
+ }
42053
+ function bindRestoredWorktrees(sessions) {
42054
+ for (const session of sessions.values()) {
42055
+ if (session.worktree || !session.cwd) continue;
42056
+ const worktree = detectExistingWorktree(session.cwd);
42057
+ if (worktree) session.worktree = worktree;
42058
+ }
42059
+ }
42060
+ function bindResumedWorktree(session, cwd, notify2) {
42061
+ if (!session) return;
42062
+ const worktree = detectExistingWorktree(cwd);
42063
+ if (!worktree) return;
42064
+ session.worktree = worktree;
42065
+ notify2();
42066
+ }
42067
+
42068
+ // src/commands/sessions/daemon/worktree/boundTreeRoots.ts
42069
+ function boundTreeRoots(sessions, exclude) {
42070
+ const roots = /* @__PURE__ */ new Set();
42071
+ for (const session of sessions.values()) {
42072
+ if (session === exclude) continue;
42073
+ const root2 = treeRootOf(session);
42074
+ if (root2 && !roots.has(root2) && holdsTree(session, root2)) roots.add(root2);
42075
+ }
42076
+ return roots;
42077
+ }
42078
+ function treeRootOf(session) {
42079
+ if (!session.cwd) return void 0;
42080
+ return canonicalTreePath(findRepoRoot(session.cwd) ?? session.cwd);
42081
+ }
42082
+ function holdsTree(session, root2) {
42083
+ if (session.closing) return true;
42084
+ if (session.status !== "done" && session.status !== "error") return true;
42085
+ if (!worktreeConfigFor(root2).enabled) return true;
42086
+ const durability = checkDurabilitySync(root2);
42087
+ daemonLog(`tree ${root2} ${releaseReason(durability, session)}`);
42088
+ return !durability.durable;
42089
+ }
42090
+ function releaseReason(durability, session) {
42091
+ if (!durability.durable)
42092
+ return `still held by finished session ${session.id}: ${durability.reason}`;
42093
+ if (durability.gone)
42094
+ return `free for allocation: session ${session.id}'s directory is gone from disk \u2014 nothing to land, not landed work`;
42095
+ return `free for allocation: session ${session.id} finished and its work is landed`;
42096
+ }
42097
+
42098
+ // src/commands/sessions/daemon/worktree/allocateAndBind.ts
42099
+ function allocateAndBind(ctx, cwd, create, options2 = {}, context) {
42100
+ const alloc = allocateTree(cwd, boundTreeRoots(ctx.sessions), options2);
42101
+ const needsSeeding = alloc.kind === "worktree" && alloc.created === true;
42102
+ const id = ctx.spawnWith(
42103
+ (sid) => create(sid, alloc.cwd, needsSeeding),
42104
+ context
42105
+ );
42106
+ bindNewWorktree(ctx.sessions.get(id), alloc, ctx.notify, ctx.startHeld);
42107
+ return id;
42108
+ }
42109
+
42110
+ // src/commands/sessions/daemon/worktree/divergencePrompt.ts
42111
+ var MAX_REASON_LINES = 20;
42112
+ var MAX_REASON_CHARS = 2e3;
42113
+ var RECONCILE_STEPS = [
42114
+ "Reconcile it yourself, without discarding any commits on either side:",
42115
+ "",
42116
+ "1. Inspect `git status`, `git log --oneline @{u}..@` and `git log --oneline @..@{u}`.",
42117
+ '2. Stash any uncommitted work with `git stash push -u -m "assist divergence"`, and restore it with `git stash pop` once the branch is reconciled.',
42118
+ "3. Rebase the local commits onto the upstream with `git rebase @{u}`, resolving conflicts so both sides' changes survive. If the rebase cannot be resolved cleanly, `git rebase --abort` and `git merge @{u}` instead.",
42119
+ "4. Push the result with a plain `git push` so the branch matches its upstream again.",
42120
+ "5. Build the clone with `assist run auto-build`.",
42121
+ "6. Run /close. The daemon resumes its auto-update loop once this session closes.",
42122
+ "",
42123
+ "Never force-push, `git reset`, or otherwise discard commits. If you cannot reconcile the branch without doing so, explain what is blocking it and leave this session open for the user instead of closing it."
42124
+ ];
42125
+ var SIMULATED_STEPS = [
42126
+ "This divergence was simulated to test the escalation; there is nothing to reconcile. Run /close now. The daemon resumes its auto-update loop once this session closes."
42127
+ ];
42128
+ function divergenceReason(scrollback) {
42129
+ return stripAnsi(scrollback).split(/\r?\n|\r/).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0).slice(-MAX_REASON_LINES).join("\n").slice(-MAX_REASON_CHARS);
42130
+ }
42131
+ function divergencePrompt(clone, scrollback) {
42132
+ const reason4 = divergenceReason(scrollback);
42133
+ return [
42134
+ `The daemon's auto-update loop for the assist install in ${clone} paused because \`assist watch wait --pull --build\` could not fast-forward the branch (exit 3). The tail of its output:`,
42135
+ "",
42136
+ "```",
42137
+ reason4 || "(no output captured)",
42138
+ "```",
42139
+ "",
42140
+ ...reason4.includes(simulatedDivergenceNote) ? SIMULATED_STEPS : RECONCILE_STEPS
42141
+ ].join("\n");
42142
+ }
42143
+
42144
+ // src/commands/sessions/daemon/worktree/liveEscalationIn.ts
42145
+ var ENDED = ["done", "error"];
42146
+ function liveEscalationIn(sessions, clone) {
42147
+ return [...sessions.values()].find(
42148
+ (s) => s.divergenceEscalation === true && s.cwd !== void 0 && canonicalTreePath(s.cwd) === clone && !ENDED.includes(s.status)
42149
+ );
42150
+ }
42151
+
42152
+ // src/commands/sessions/daemon/worktree/escalateDivergence.ts
42153
+ function escalateDivergence(ctx, cwd, output) {
42154
+ const clone = canonicalTreePath(cwd);
42155
+ const live = liveEscalationIn(ctx.sessions, clone);
42156
+ if (live) {
42157
+ daemonLog(
42158
+ `auto-update diverged in the clone ${clone}: no escalation spawned, session ${live.id} is already diagnosing it (${live.status})`
42159
+ );
42160
+ return live.id;
42161
+ }
42162
+ const prompt = divergencePrompt(clone, output);
42163
+ const id = allocateAndBind(
42164
+ ctx,
42165
+ clone,
42166
+ (sid, resolvedCwd) => ({
42167
+ ...createSession(sid, { prompt, cwd: resolvedCwd ?? clone }),
42168
+ divergenceEscalation: true
42169
+ }),
42170
+ { inPlace: true }
42171
+ );
42172
+ daemonLog(
42173
+ `auto-update diverged in the clone ${clone}: spawned escalation session ${id} to diagnose it`
42174
+ );
42175
+ return id;
42176
+ }
42177
+
42178
+ // src/commands/sessions/daemon/autoUpdate/autoUpdateEnabled.ts
42179
+ function autoUpdateEnabled(installDir) {
42180
+ try {
42181
+ return loadConfigFrom(installDir).autoUpdate?.enabled ?? true;
42182
+ } catch (error) {
42183
+ daemonLog(
42184
+ `auto-update: config failed to load, keeping the default (on): ${error instanceof Error ? error.message : String(error)}`
42185
+ );
42186
+ return true;
42187
+ }
42188
+ }
42189
+
42190
+ // src/commands/sessions/daemon/autoUpdate/lineRecorder.ts
42191
+ function lineRecorder(onLine) {
42192
+ let pending = "";
42193
+ return (text17) => {
42194
+ const lines2 = (pending + text17).split(/\r?\n|\r/);
42195
+ pending = lines2.pop() ?? "";
42196
+ for (const line of lines2) {
42197
+ const clean2 = stripAnsi(line).trimEnd();
42198
+ if (clean2.trim()) onLine(clean2);
42199
+ }
42200
+ };
42201
+ }
42202
+
42203
+ // src/commands/sessions/daemon/autoUpdate/holdForEscalation.ts
42204
+ var ESCALATION_POLL_MS = 5e3;
42205
+ async function holdForEscalation(sessionId, deps2) {
42206
+ deps2.note(`paused while session ${sessionId} reconciles the divergence`);
42207
+ while (deps2.isLive(sessionId)) await deps2.sleep(ESCALATION_POLL_MS);
42208
+ deps2.note(`session ${sessionId} ended; resuming`);
42209
+ }
42210
+
42211
+ // src/commands/sessions/daemon/autoUpdate/runLapCapturingTail.ts
42212
+ var OUTPUT_TAIL = 16 * 1024;
42213
+ async function runLapCapturingTail(deps2) {
42214
+ let tail = "";
42215
+ const end = await deps2.runLap((text17) => {
42216
+ tail = (tail + text17).slice(-OUTPUT_TAIL);
42217
+ deps2.record(text17);
42218
+ });
42219
+ return { end, tail };
42220
+ }
42221
+
42222
+ // src/commands/sessions/daemon/autoUpdate/runAutoUpdateLoop.ts
42223
+ var DIVERGED_EXIT_CODE = 3;
42224
+ var RETRY_AFTER_FAILURE_MS = 5 * 60 * 1e3;
42225
+ async function runAutoUpdateLoop(deps2, maxLaps = Number.POSITIVE_INFINITY) {
42226
+ let held = deps2.liveEscalation();
42227
+ for (let lap = 1; lap <= maxLaps; lap++) {
42228
+ if (held) await holdForEscalation(held, deps2);
42229
+ held = void 0;
42230
+ deps2.note(
42231
+ `lap ${lap} at ${(/* @__PURE__ */ new Date()).toLocaleString()}: assist ${WATCH_LAP_ARGS.join(" ")}`
42232
+ );
42233
+ let result;
42234
+ try {
42235
+ result = await runLapCapturingTail(deps2);
42236
+ } catch (error) {
42237
+ deps2.note(
42238
+ `lap ${lap} could not start (${error instanceof Error ? error.message : String(error)}); retrying in ${RETRY_AFTER_FAILURE_MS / 6e4}m`
42239
+ );
42240
+ await deps2.sleep(RETRY_AFTER_FAILURE_MS);
42241
+ continue;
42242
+ }
42243
+ const { end, tail } = result;
42244
+ const how = end.signal ? `killed by ${end.signal}` : `exited ${end.code}`;
42245
+ if (end.code === DIVERGED_EXIT_CODE) {
42246
+ held = deps2.escalate(tail);
42247
+ deps2.note(`lap ${lap} ${how} (divergence); escalated to session ${held}`);
42248
+ continue;
42249
+ }
42250
+ if (decideLap(end, false).kind === "relaunch") {
42251
+ deps2.note(`lap ${lap} ${how}; relaunching`);
42252
+ continue;
42253
+ }
42254
+ deps2.note(
42255
+ `lap ${lap} ${how}; retrying in ${RETRY_AFTER_FAILURE_MS / 6e4}m`
42256
+ );
42257
+ await deps2.sleep(RETRY_AFTER_FAILURE_MS);
42258
+ }
42259
+ }
42260
+
42261
+ // src/commands/sessions/daemon/autoUpdate/runUpdateLap.ts
42262
+ import { spawn as spawn11 } from "child_process";
42263
+ function runUpdateLap(cwd, onOutput) {
42264
+ return new Promise((resolve28, reject) => {
42265
+ const { ASSIST_ACTIVITY_ID: _activity, ...env } = process.env;
42266
+ const child = spawn11(
42267
+ process.execPath,
42268
+ [process.argv[1], ...WATCH_LAP_ARGS],
42269
+ {
42270
+ cwd,
42271
+ env,
42272
+ stdio: ["ignore", "pipe", "pipe"],
42273
+ windowsHide: true
42274
+ }
42275
+ );
42276
+ const killWithDaemon = () => {
42277
+ child.kill();
42278
+ };
42279
+ process.on("exit", killWithDaemon);
42280
+ child.stdout.on("data", (chunk) => onOutput(chunk.toString()));
42281
+ child.stderr.on("data", (chunk) => onOutput(chunk.toString()));
42282
+ child.on("close", (code, signal) => {
42283
+ process.off("exit", killWithDaemon);
42284
+ resolve28({ code, signal });
42285
+ });
42286
+ child.on("error", (error) => {
42287
+ process.off("exit", killWithDaemon);
42288
+ reject(error);
42289
+ });
42290
+ });
42291
+ }
42292
+
42293
+ // src/commands/sessions/daemon/autoUpdate/trimWatcherLog.ts
42294
+ import { readFileSync as readFileSync65, writeFileSync as writeFileSync54 } from "fs";
42295
+ var KEEP_TAIL = 256 * 1024;
42296
+ var MAX_LOG_SIZE = 4 * 1024 * 1024;
42297
+ function trimWatcherLog(cwd) {
42298
+ const path106 = watcherLogPath(cwd);
42299
+ let log2;
42300
+ try {
42301
+ log2 = readFileSync65(path106, "utf8");
42302
+ } catch {
42303
+ return;
42304
+ }
42305
+ if (log2.length <= MAX_LOG_SIZE) return;
42306
+ writeFileSync54(path106, log2.slice(-KEEP_TAIL));
42307
+ daemonLog(`auto-update: log ${path106} trimmed to its last ${KEEP_TAIL} chars`);
42308
+ }
42309
+
42310
+ // src/commands/sessions/daemon/autoUpdate/startAutoUpdate.ts
42311
+ function loopDeps(installDir, ctx) {
42312
+ const clone = canonicalTreePath(installDir);
42313
+ const logLine = lineRecorder((line) => daemonLog(`auto-update: ${line}`));
42314
+ return {
42315
+ runLap: (onOutput) => runUpdateLap(installDir, onOutput),
42316
+ record: (text17) => {
42317
+ appendWatcherLog(installDir, text17);
42318
+ logLine(text17);
42319
+ },
42320
+ note: (text17) => {
42321
+ appendWatcherLog(installDir, watcherNote(text17));
42322
+ daemonLog(`auto-update: ${text17}`);
42323
+ },
42324
+ liveEscalation: () => liveEscalationIn(ctx().sessions, clone)?.id,
42325
+ escalate: (output) => escalateDivergence(ctx(), installDir, output),
42326
+ isLive: (sessionId) => {
42327
+ const status2 = ctx().sessions.get(sessionId)?.status;
42328
+ return status2 !== void 0 && status2 !== "done" && status2 !== "error";
42329
+ },
42330
+ sleep: (ms) => sleep2(ms)
42331
+ };
42332
+ }
42333
+ function startAutoUpdate(ctx) {
42334
+ const installDir = getInstallDir();
42335
+ if (!autoUpdateEnabled(installDir)) {
42336
+ daemonLog("auto-update: off (autoUpdate.enabled is false)");
42337
+ return;
42338
+ }
42339
+ if (!isGitRepo(installDir)) {
42340
+ daemonLog(`auto-update: off (${installDir} is not a git clone)`);
42341
+ return;
42342
+ }
42343
+ trimWatcherLog(installDir);
42344
+ daemonLog(`auto-update: pulling and building ${installDir} as origin moves`);
42345
+ void runAutoUpdateLoop(loopDeps(installDir, ctx)).catch(
42346
+ (error) => daemonLog(
42347
+ `auto-update: loop stopped: ${error instanceof Error ? error.message : String(error)}`
42348
+ )
42349
+ );
42350
+ }
42351
+
41986
42352
  // src/commands/sessions/daemon/setStatus.ts
41987
42353
  function setStatus2(session, newStatus) {
41988
42354
  const now = Date.now();
@@ -42005,16 +42371,16 @@ function setStatus2(session, newStatus) {
42005
42371
 
42006
42372
  // src/commands/sessions/daemon/worktree/reapWorktree.ts
42007
42373
  import { existsSync as existsSync87 } from "fs";
42008
- import { basename as basename28 } from "path";
42374
+ import { basename as basename29 } from "path";
42009
42375
 
42010
42376
  // src/commands/sessions/daemon/worktree/deleteStrandedTree.ts
42011
42377
  import { existsSync as existsSync86 } from "fs";
42012
- import { join as join102 } from "path";
42378
+ import { join as join103 } from "path";
42013
42379
 
42014
42380
  // src/commands/sessions/daemon/worktree/deleteTreeDirectly.ts
42015
42381
  import { statSync as statSync15 } from "fs";
42016
42382
  import { rm as rm3 } from "fs/promises";
42017
- import { join as join101 } from "path";
42383
+ import { join as join102 } from "path";
42018
42384
  async function deleteTreeDirectly(clone, worktreePath, why) {
42019
42385
  if (holdsAGitDirectoryRatherThanALink(worktreePath)) {
42020
42386
  const refusal = "it is a clone of its own, not a linked worktree";
@@ -42042,7 +42408,7 @@ async function deleteTreeDirectly(clone, worktreePath, why) {
42042
42408
  return { removed: true };
42043
42409
  }
42044
42410
  function holdsAGitDirectoryRatherThanALink(worktreePath) {
42045
- return statSync15(join101(worktreePath, ".git"), {
42411
+ return statSync15(join102(worktreePath, ".git"), {
42046
42412
  throwIfNoEntry: false
42047
42413
  })?.isDirectory() === true;
42048
42414
  }
@@ -42078,7 +42444,7 @@ async function deleteStrandedTree(clone, worktreePath, cause) {
42078
42444
  );
42079
42445
  }
42080
42446
  function strandedReason(worktreePath, cause) {
42081
- if (!existsSync86(join102(worktreePath, ".git")))
42447
+ if (!existsSync86(join103(worktreePath, ".git")))
42082
42448
  return "its .git link is already gone";
42083
42449
  if (/not a working tree|not a git repository/i.test(reason2(cause)))
42084
42450
  return "git no longer recognises it as a working tree";
@@ -42148,7 +42514,7 @@ async function reapWorktree(worktreePath, force = false) {
42148
42514
  const clone = owningClone(worktreePath);
42149
42515
  const removal = await removeTree(clone, worktreePath, force);
42150
42516
  if (!removal.removed) return removal;
42151
- await deleteWorktreeBranch(clone, basename28(worktreePath));
42517
+ await deleteWorktreeBranch(clone, basename29(worktreePath));
42152
42518
  forgetWorktree(worktreePath);
42153
42519
  daemonLog(`worktree ${worktreePath} reaped${force ? " (forced)" : ""}`);
42154
42520
  return removal;
@@ -42222,34 +42588,6 @@ function killPtyTree(pty2) {
42222
42588
  }
42223
42589
  }
42224
42590
 
42225
- // src/commands/sessions/daemon/worktree/liveWatcherFor.ts
42226
- function liveWatcherFor(sessions, clone) {
42227
- for (const session of sessions.values())
42228
- if (session.watcher === true && session.cwd !== void 0 && (clone === void 0 || canonicalTreePath(session.cwd) === clone) && session.status !== "stopped" && session.status !== "error")
42229
- return session;
42230
- return void 0;
42231
- }
42232
-
42233
- // src/commands/sessions/daemon/worktree/resolveClone.ts
42234
- function resolveClone(cwd) {
42235
- const repoRoot2 = findRepoRoot(cwd) ?? cwd;
42236
- return canonicalTreePath(mainWorktree(repoRoot2) ?? repoRoot2);
42237
- }
42238
-
42239
- // src/commands/sessions/daemon/worktree/orphanedWatcherFor.ts
42240
- function orphanedWatcherFor(sessions, dismissed) {
42241
- if (dismissed.watcher === true || !dismissed.cwd) return void 0;
42242
- if (!liveWatcherFor(sessions)) return void 0;
42243
- const known = dismissed.worktree?.clone ?? dismissed.releasedFromClone;
42244
- const clone = known ? canonicalTreePath(known) : resolveClone(dismissed.cwd);
42245
- const watcher = liveWatcherFor(sessions, clone);
42246
- if (!watcher) return void 0;
42247
- const kept = [...sessions.values()].some(
42248
- (s) => s.watcher !== true && s.cwd !== void 0 && resolveClone(s.cwd) === clone
42249
- );
42250
- return kept ? void 0 : { watcher, clone };
42251
- }
42252
-
42253
42591
  // src/commands/sessions/daemon/worktree/treeUnderClose.ts
42254
42592
  function treeUnderClose(session) {
42255
42593
  if (session.worktree) return { path: session.worktree.path, removable: true };
@@ -42280,13 +42618,6 @@ function dismissSession(sessions, id) {
42280
42618
  if (s.activity?.itemId != null) releaseLock(s.activity.itemId);
42281
42619
  sessions.delete(id);
42282
42620
  daemonLog(`session ${id} dismissed (${s.name})`);
42283
- const orphan = orphanedWatcherFor(sessions, s);
42284
- if (orphan) {
42285
- daemonLog(
42286
- `reaping watcher session ${orphan.watcher.id} for the clone ${orphan.clone}: its last session ${id} was dismissed`
42287
- );
42288
- dismissSession(sessions, orphan.watcher.id);
42289
- }
42290
42621
  if (!s.worktree) return true;
42291
42622
  if (sharedWith.length > 0)
42292
42623
  daemonLog(
@@ -42521,43 +42852,6 @@ function dismissSessionGated(sessions, id, notify2, discard = false) {
42521
42852
  settle();
42522
42853
  }
42523
42854
 
42524
- // src/commands/sessions/daemon/worktree/resumeWatcherAfterEscalation.ts
42525
- var ENDED = ["done", "error"];
42526
- var endedViaDismissOrExit = /* @__PURE__ */ new WeakSet();
42527
- function resumeWatcherAfterEscalation(sessions, escalation, restart) {
42528
- if (escalation.divergenceEscalation !== true || !escalation.cwd) return;
42529
- if (endedViaDismissOrExit.has(escalation)) return;
42530
- endedViaDismissOrExit.add(escalation);
42531
- const clone = canonicalTreePath(escalation.cwd);
42532
- const watcher = [...sessions.values()].find(
42533
- (s) => s.watcher === true && s.cwd !== void 0 && canonicalTreePath(s.cwd) === clone
42534
- );
42535
- if (!watcher) {
42536
- daemonLog(
42537
- `escalation session ${escalation.id} ended: no watcher left in the clone ${clone} to restart`
42538
- );
42539
- return;
42540
- }
42541
- if (!ENDED.includes(watcher.status)) {
42542
- daemonLog(
42543
- `escalation session ${escalation.id} ended: watcher session ${watcher.id} in the clone ${clone} is already ${watcher.status}`
42544
- );
42545
- return;
42546
- }
42547
- daemonLog(
42548
- `escalation session ${escalation.id} ended: restarting watcher session ${watcher.id} in the clone ${clone}`
42549
- );
42550
- watcher.watcherRestartReason = `after escalation session ${escalation.id} ended`;
42551
- restart(watcher.id);
42552
- }
42553
-
42554
- // src/commands/sessions/daemon/dismissAndResumeWatcher.ts
42555
- function dismissAndResumeWatcher(sessions, id, notify2, restart) {
42556
- const session = sessions.get(id);
42557
- dismissSessionGated(sessions, id, notify2);
42558
- if (session) resumeWatcherAfterEscalation(sessions, session, restart);
42559
- }
42560
-
42561
42855
  // src/commands/sessions/daemon/drainSessions.ts
42562
42856
  function drainSessions(sessions, notify2) {
42563
42857
  const drained = [...sessions.values()].map((s) => ({
@@ -42971,37 +43265,10 @@ function startSessionTitleGeneration(session, notify2) {
42971
43265
  runSessionTitleGeneration(session, prompt, notify2, singleLineTitle(prompt));
42972
43266
  }
42973
43267
 
42974
- // src/commands/sessions/daemon/worktree/appendWatcherLog.ts
42975
- import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync34 } from "fs";
42976
- import { dirname as dirname42 } from "path";
42977
-
42978
- // src/commands/sessions/daemon/worktree/watcherLogPath.ts
42979
- import { createHash as createHash4 } from "crypto";
42980
- import { homedir as homedir26 } from "os";
42981
- import { basename as basename29, join as join103 } from "path";
42982
- function watcherLogPath(cwd) {
42983
- const clone = canonicalTreePath(cwd);
42984
- const key = createHash4("sha1").update(clone).digest("hex").slice(0, 8);
42985
- const dir = process.env.ASSIST_WATCHER_LOG_DIR ?? join103(homedir26(), ".assist", "watchers");
42986
- return join103(dir, `${basename29(clone) || "root"}-${key}.log`);
42987
- }
42988
-
42989
- // src/commands/sessions/daemon/worktree/appendWatcherLog.ts
42990
- function appendWatcherLog(session, data) {
42991
- if (session.watcher !== true || !session.cwd) return;
42992
- try {
42993
- const path106 = watcherLogPath(session.cwd);
42994
- mkdirSync34(dirname42(path106), { recursive: true });
42995
- appendFileSync3(path106, data);
42996
- } catch {
42997
- }
42998
- }
42999
-
43000
43268
  // src/commands/sessions/daemon/emitSessionOutput.ts
43001
43269
  var MAX_SCROLLBACK = 256 * 1024;
43002
43270
  function emitSessionOutput(session, clients, data) {
43003
43271
  session.scrollback += data;
43004
- appendWatcherLog(session, data);
43005
43272
  if (session.scrollback.length > MAX_SCROLLBACK) {
43006
43273
  session.scrollback = session.scrollback.slice(-MAX_SCROLLBACK);
43007
43274
  }
@@ -44072,13 +44339,6 @@ function applyUsageRecord(sessions, windows, notify2, claudeSessionId, transcrip
44072
44339
  notify2();
44073
44340
  }
44074
44341
 
44075
- // src/commands/sessions/daemon/watcherNote.ts
44076
- function watcherNote(text17) {
44077
- return `\r
44078
- \x1B[2m\u2500\u2500 ${text17} \u2500\u2500\x1B[0m\r
44079
- `;
44080
- }
44081
-
44082
44342
  // src/commands/sessions/daemon/shouldAutoRun.ts
44083
44343
  function shouldAutoRun(session, status2 = session.status) {
44084
44344
  if (!session.autoRun) return { run: false, reason: null };
@@ -44133,7 +44393,6 @@ function shouldAutoDismiss(session, exitCode) {
44133
44393
  }
44134
44394
 
44135
44395
  // src/commands/sessions/daemon/finishStatusChange.ts
44136
- var DIVERGED_EXIT_CODE = 3;
44137
44396
  function finishStatusChange(session, status2, exitCode, deps2) {
44138
44397
  daemonLog(`session ${session.id} status: ${session.status} -> ${status2}`);
44139
44398
  void flushPhaseActiveMs(session);
@@ -44141,9 +44400,6 @@ function finishStatusChange(session, status2, exitCode, deps2) {
44141
44400
  if (status2 === "waiting")
44142
44401
  startTranscriptTitleGeneration(session, deps2.notify);
44143
44402
  if (status2 !== "stopped") session.undurable = void 0;
44144
- if (session.watcher === true && exitCode === DIVERGED_EXIT_CODE)
44145
- deps2.escalateDivergence?.(session);
44146
- if (status2 === "done" || status2 === "error") deps2.resumeWatcher?.(session);
44147
44403
  const autoRun = shouldAutoRun(session);
44148
44404
  if (autoRun.run) {
44149
44405
  deps2.reuseForRun(session, autoRun.itemId);
@@ -44189,40 +44445,6 @@ function makeStatusChangeHandler(sessions, deps2) {
44189
44445
  );
44190
44446
  }
44191
44447
 
44192
- // src/commands/sessions/daemon/worktree/detectExistingWorktree.ts
44193
- function detectExistingWorktree(cwd) {
44194
- const repoRoot2 = findRepoRoot(cwd) ?? cwd;
44195
- if (!worktreeConfigFor(repoRoot2).enabled) return void 0;
44196
- const clone = mainWorktree(repoRoot2);
44197
- if (clone && clone !== repoRoot2) return { path: repoRoot2, clone };
44198
- return void 0;
44199
- }
44200
-
44201
- // src/commands/sessions/daemon/worktree/bindNewWorktree.ts
44202
- function bindNewWorktree(session, alloc, notify2, startHeld = () => {
44203
- }) {
44204
- if (alloc.kind !== "worktree" || !alloc.cwd || !alloc.clone || !session)
44205
- return;
44206
- session.worktree = { path: alloc.cwd, clone: alloc.clone };
44207
- if (alloc.created)
44208
- seedWorktree(alloc.cwd, alloc.clone, () => startHeld(session));
44209
- notify2();
44210
- }
44211
- function bindRestoredWorktrees(sessions) {
44212
- for (const session of sessions.values()) {
44213
- if (session.worktree || !session.cwd) continue;
44214
- const worktree = detectExistingWorktree(session.cwd);
44215
- if (worktree) session.worktree = worktree;
44216
- }
44217
- }
44218
- function bindResumedWorktree(session, cwd, notify2) {
44219
- if (!session) return;
44220
- const worktree = detectExistingWorktree(cwd);
44221
- if (!worktree) return;
44222
- session.worktree = worktree;
44223
- notify2();
44224
- }
44225
-
44226
44448
  // src/commands/sessions/daemon/attachReusedRun.ts
44227
44449
  function attachReusedRun(session, alloc, clients, onStatusChange, tree) {
44228
44450
  broadcast(clients, { type: "clear", sessionId: session.id });
@@ -44301,137 +44523,6 @@ function startReusedRunPty(session, assistArgs, itemId2, hold, clients, onStatus
44301
44523
  return true;
44302
44524
  }
44303
44525
 
44304
- // src/commands/sessions/daemon/createAssistSession.ts
44305
- function createAssistSession(id, assistArgs, cwd, meta, holdPty) {
44306
- const startedAt = Date.now();
44307
- return {
44308
- id,
44309
- name: `assist ${assistArgs.join(" ")}`,
44310
- title: meta?.title,
44311
- subtitle: meta?.subtitle,
44312
- commandType: "assist",
44313
- status: "running",
44314
- startedAt,
44315
- runningMs: 0,
44316
- runningSince: startedAt,
44317
- waitingSince: null,
44318
- ...startOrHoldPty(
44319
- () => spawnPty(["assist", ...assistArgs], cwd, id),
44320
- holdPty
44321
- ),
44322
- scrollback: "",
44323
- assistArgs,
44324
- cwd
44325
- };
44326
- }
44327
-
44328
- // src/commands/sessions/daemon/worktree/seedWatcherScrollback.ts
44329
- import { readFileSync as readFileSync65, writeFileSync as writeFileSync54 } from "fs";
44330
- var SCROLLBACK_TAIL = 256 * 1024;
44331
- var MAX_LOG_SIZE = 4 * 1024 * 1024;
44332
- function readLogTail2(path106) {
44333
- let log2;
44334
- try {
44335
- log2 = readFileSync65(path106, "utf8");
44336
- } catch {
44337
- return "";
44338
- }
44339
- if (log2.length > MAX_LOG_SIZE) {
44340
- log2 = log2.slice(-SCROLLBACK_TAIL);
44341
- writeFileSync54(path106, log2);
44342
- daemonLog(`watcher log ${path106} trimmed to its last ${log2.length} chars`);
44343
- }
44344
- return log2.slice(-SCROLLBACK_TAIL);
44345
- }
44346
- function seedWatcherScrollback(cwd, event) {
44347
- const path106 = watcherLogPath(cwd);
44348
- const history = readLogTail2(path106);
44349
- const note = watcherNote(`${event} at ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
44350
- appendWatcherLog({ watcher: true, cwd }, note);
44351
- daemonLog(
44352
- `watcher in ${cwd} seeded with ${history.length} chars of history from ${path106} (${event})`
44353
- );
44354
- return history + note;
44355
- }
44356
-
44357
- // src/commands/sessions/daemon/createWatcherSession.ts
44358
- var WATCHER_ARGS = ["watch", "loop"];
44359
- function createWatcherSession(id, cwd) {
44360
- return {
44361
- ...createAssistSession(id, WATCHER_ARGS, cwd),
44362
- watcher: true,
44363
- scrollback: seedWatcherScrollback(cwd, "watcher started")
44364
- };
44365
- }
44366
-
44367
- // src/commands/sessions/daemon/worktree/boundTreeRoots.ts
44368
- function boundTreeRoots(sessions, exclude) {
44369
- const roots = /* @__PURE__ */ new Set();
44370
- for (const session of sessions.values()) {
44371
- if (session === exclude) continue;
44372
- const root2 = treeRootOf(session);
44373
- if (root2 && !roots.has(root2) && holdsTree(session, root2)) roots.add(root2);
44374
- }
44375
- return roots;
44376
- }
44377
- function treeRootOf(session) {
44378
- if (!session.cwd) return void 0;
44379
- return canonicalTreePath(findRepoRoot(session.cwd) ?? session.cwd);
44380
- }
44381
- function holdsTree(session, root2) {
44382
- if (session.closing) return true;
44383
- if (session.status !== "done" && session.status !== "error") return true;
44384
- if (!worktreeConfigFor(root2).enabled) return true;
44385
- const durability = checkDurabilitySync(root2);
44386
- daemonLog(`tree ${root2} ${releaseReason(durability, session)}`);
44387
- return !durability.durable;
44388
- }
44389
- function releaseReason(durability, session) {
44390
- if (!durability.durable)
44391
- return `still held by finished session ${session.id}: ${durability.reason}`;
44392
- if (durability.gone)
44393
- return `free for allocation: session ${session.id}'s directory is gone from disk \u2014 nothing to land, not landed work`;
44394
- return `free for allocation: session ${session.id} finished and its work is landed`;
44395
- }
44396
-
44397
- // src/commands/sessions/daemon/worktree/allocateAndBind.ts
44398
- function allocateAndBind(ctx, cwd, create, options2 = {}, context) {
44399
- const alloc = allocateTree(cwd, boundTreeRoots(ctx.sessions), options2);
44400
- const needsSeeding = alloc.kind === "worktree" && alloc.created === true;
44401
- const id = ctx.spawnWith(
44402
- (sid) => create(sid, alloc.cwd, needsSeeding),
44403
- context
44404
- );
44405
- bindNewWorktree(ctx.sessions.get(id), alloc, ctx.notify, ctx.startHeld);
44406
- return id;
44407
- }
44408
-
44409
- // src/commands/sessions/daemon/worktree/ensureWatcher.ts
44410
- function ensureWatcher(ctx, cwd) {
44411
- if (!cwd) return void 0;
44412
- const repoRoot2 = findRepoRoot(cwd) ?? cwd;
44413
- const cfg = worktreeConfigFor(repoRoot2);
44414
- if (!cfg.enabled || cfg.watcher !== true) return void 0;
44415
- const clone = resolveClone(repoRoot2);
44416
- const live = liveWatcherFor(ctx.sessions, clone);
44417
- if (live) {
44418
- daemonLog(
44419
- `no watcher spawned for the clone ${clone}: session ${live.id} is already watching it (${live.status})`
44420
- );
44421
- return void 0;
44422
- }
44423
- const id = allocateAndBind(
44424
- ctx,
44425
- clone,
44426
- (sid, resolvedCwd) => createWatcherSession(sid, resolvedCwd ?? clone),
44427
- { inPlace: true }
44428
- );
44429
- daemonLog(
44430
- `spawned watcher session ${id} running assist ${WATCHER_ARGS.join(" ")} in the clone ${clone}`
44431
- );
44432
- return id;
44433
- }
44434
-
44435
44526
  // src/commands/sessions/daemon/worktree/isBacklogRunArgs.ts
44436
44527
  var ITEM_ID = /^a?\d+$/;
44437
44528
  function isBacklogRunArgs(args) {
@@ -44495,7 +44586,6 @@ function failChainedRun(session, itemId2, error, clients, tree) {
44495
44586
  // src/commands/sessions/daemon/reuseSessionForRun.ts
44496
44587
  function reuseSessionForRun(session, itemId2, clients, onStatusChange, tree) {
44497
44588
  const assistArgs = ["backlog", "run", String(itemId2)];
44498
- const originCwd = session.cwd;
44499
44589
  resetCardForRun(session, assistArgs);
44500
44590
  let alloc;
44501
44591
  try {
@@ -44517,13 +44607,71 @@ function reuseSessionForRun(session, itemId2, clients, onStatusChange, tree) {
44517
44607
  onStatusChange
44518
44608
  );
44519
44609
  if (!started) return;
44520
- if (tree) ensureWatcher(tree, originCwd);
44521
44610
  attachReusedRun(session, alloc, clients, onStatusChange, tree);
44522
44611
  daemonLog(
44523
44612
  `session ${session.id} reused for backlog run ${itemId2}: ${session.name}`
44524
44613
  );
44525
44614
  }
44526
44615
 
44616
+ // src/commands/sessions/daemon/managerStatusChangeHandler.ts
44617
+ function managerStatusChangeHandler(sessions, clients, dismiss, notify2, treeCtx) {
44618
+ const handler = makeStatusChangeHandler(sessions, {
44619
+ dismiss,
44620
+ notify: notify2,
44621
+ reuseForRun: (session, itemId2) => reuseSessionForRun(session, itemId2, clients, handler, treeCtx())
44622
+ });
44623
+ return handler;
44624
+ }
44625
+
44626
+ // src/commands/sessions/daemon/applySetStatus.ts
44627
+ function applySetStatus(sessions, id, status2, source, onStatusChange, rebind) {
44628
+ const session = sessions.get(id);
44629
+ if (!session) {
44630
+ daemonLog(
44631
+ `set-status for unknown session id=${id} status=${status2} (no live session; ignoring)`
44632
+ );
44633
+ return;
44634
+ }
44635
+ if (session.status === "stopped") {
44636
+ daemonLog(
44637
+ `set-status for stopped session id=${id} status=${status2} (held pending durable close; ignoring)`
44638
+ );
44639
+ return;
44640
+ }
44641
+ rebind?.(session);
44642
+ if (source === "permission") session.permissionActive = true;
44643
+ if (status2 === "running") session.permissionActive = false;
44644
+ onStatusChange(session, status2);
44645
+ }
44646
+
44647
+ // src/commands/sessions/daemon/rebindClaudeSession.ts
44648
+ function rebindClaudeSession(session, claudeSessionId, notify2, onStatusChange) {
44649
+ if (!claudeSessionId) return;
44650
+ if (session.claudeSessionId === claudeSessionId) return;
44651
+ daemonLog(
44652
+ `session ${session.id} claude session moved: ${session.claudeSessionId ?? "none"} -> ${claudeSessionId}`
44653
+ );
44654
+ session.claudeSessionId = claudeSessionId;
44655
+ watchTranscript(session, notify2, onStatusChange);
44656
+ }
44657
+
44658
+ // src/commands/sessions/daemon/setStatusFromHook.ts
44659
+ function setStatusFromHook(sessions, report, notify2, onStatusChange) {
44660
+ applySetStatus(
44661
+ sessions,
44662
+ report.id,
44663
+ report.status,
44664
+ report.source,
44665
+ onStatusChange,
44666
+ (session) => rebindClaudeSession(
44667
+ session,
44668
+ report.claudeSessionId,
44669
+ notify2,
44670
+ onStatusChange
44671
+ )
44672
+ );
44673
+ }
44674
+
44527
44675
  // src/commands/sessions/daemon/respawnSession.ts
44528
44676
  function respawnSession(session, respawn, status2, clients, onStatusChange) {
44529
44677
  session.gitWatcher?.close();
@@ -44532,8 +44680,7 @@ function respawnSession(session, respawn, status2, clients, onStatusChange) {
44532
44680
  session.pendingDismiss = void 0;
44533
44681
  session.closeGrace?.cancel();
44534
44682
  session.closing = void 0;
44535
- const keepHistory = session.watcher === true;
44536
- if (!keepHistory) session.scrollback = "";
44683
+ session.scrollback = "";
44537
44684
  session.startedAt = Date.now();
44538
44685
  session.runningMs = 0;
44539
44686
  session.runningSince = null;
@@ -44550,15 +44697,7 @@ function respawnSession(session, respawn, status2, clients, onStatusChange) {
44550
44697
  session.pty?.resize(session.cols, session.rows);
44551
44698
  } catch {
44552
44699
  }
44553
- if (keepHistory) {
44554
- const reason4 = session.watcherRestartReason;
44555
- session.watcherRestartReason = void 0;
44556
- emitSessionOutput(
44557
- session,
44558
- clients,
44559
- watcherNote(reason4 ? `watcher restarted ${reason4}` : "watcher restarted")
44560
- );
44561
- } else broadcast(clients, { type: "clear", sessionId: session.id });
44700
+ broadcast(clients, { type: "clear", sessionId: session.id });
44562
44701
  wirePtyEvents(session, clients, onStatusChange);
44563
44702
  }
44564
44703
 
@@ -44729,144 +44868,6 @@ function restartManagedSession(sessions, id, clients, onStatusChange) {
44729
44868
  return { ok: true };
44730
44869
  }
44731
44870
 
44732
- // src/commands/sessions/daemon/worktree/divergencePrompt.ts
44733
- var MAX_REASON_LINES = 20;
44734
- var MAX_REASON_CHARS = 2e3;
44735
- var RECONCILE_STEPS = [
44736
- "Reconcile it yourself, without discarding any commits on either side:",
44737
- "",
44738
- "1. Inspect `git status`, `git log --oneline @{u}..@` and `git log --oneline @..@{u}`.",
44739
- '2. Stash any uncommitted work with `git stash push -u -m "assist divergence"`, and restore it with `git stash pop` once the branch is reconciled.',
44740
- "3. Rebase the local commits onto the upstream with `git rebase @{u}`, resolving conflicts so both sides' changes survive. If the rebase cannot be resolved cleanly, `git rebase --abort` and `git merge @{u}` instead.",
44741
- "4. Push the result with a plain `git push` so the branch matches its upstream again.",
44742
- "5. Build the clone with `assist run auto-build`.",
44743
- "6. Run /close. The daemon restarts the watcher once this session closes.",
44744
- "",
44745
- "Never force-push, `git reset`, or otherwise discard commits. If you cannot reconcile the branch without doing so, explain what is blocking it and leave this session open for the user instead of closing it."
44746
- ];
44747
- var SIMULATED_STEPS = [
44748
- "This divergence was simulated to test the escalation; there is nothing to reconcile. Run /close now. The daemon restarts the watcher once this session closes."
44749
- ];
44750
- function divergenceReason(scrollback) {
44751
- return stripAnsi(scrollback).split(/\r?\n|\r/).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0).slice(-MAX_REASON_LINES).join("\n").slice(-MAX_REASON_CHARS);
44752
- }
44753
- function divergencePrompt(clone, scrollback) {
44754
- const reason4 = divergenceReason(scrollback);
44755
- return [
44756
- `The watcher in ${clone} stopped because \`assist watch wait --pull --build\` could not fast-forward the branch (exit 3). The tail of its output:`,
44757
- "",
44758
- "```",
44759
- reason4 || "(no output captured)",
44760
- "```",
44761
- "",
44762
- ...reason4.includes(simulatedDivergenceNote) ? SIMULATED_STEPS : RECONCILE_STEPS
44763
- ].join("\n");
44764
- }
44765
-
44766
- // src/commands/sessions/daemon/worktree/escalateDivergence.ts
44767
- var ENDED2 = ["done", "error", "stopped"];
44768
- function escalateDivergence(ctx, watcher) {
44769
- if (!watcher.cwd) return void 0;
44770
- const clone = canonicalTreePath(watcher.cwd);
44771
- const live = [...ctx.sessions.values()].find(
44772
- (s) => s.divergenceEscalation === true && s.cwd !== void 0 && canonicalTreePath(s.cwd) === clone && !ENDED2.includes(s.status)
44773
- );
44774
- if (live) {
44775
- daemonLog(
44776
- `watcher session ${watcher.id} exited 3 (divergence) in the clone ${clone}: no escalation spawned, session ${live.id} is already diagnosing it (${live.status})`
44777
- );
44778
- return void 0;
44779
- }
44780
- const prompt = divergencePrompt(clone, watcher.scrollback);
44781
- const id = allocateAndBind(
44782
- ctx,
44783
- clone,
44784
- (sid, resolvedCwd) => ({
44785
- ...createSession(sid, { prompt, cwd: resolvedCwd ?? clone }),
44786
- divergenceEscalation: true
44787
- }),
44788
- { inPlace: true }
44789
- );
44790
- daemonLog(
44791
- `watcher session ${watcher.id} exited 3 (divergence) in the clone ${clone}: spawned escalation session ${id} to diagnose it`
44792
- );
44793
- return id;
44794
- }
44795
-
44796
- // src/commands/sessions/daemon/managerStatusChangeHandler.ts
44797
- function managerStatusChangeHandler(sessions, clients, dismiss, notify2, treeCtx) {
44798
- const handler = makeStatusChangeHandler(sessions, {
44799
- dismiss,
44800
- notify: notify2,
44801
- reuseForRun: (session, itemId2) => reuseSessionForRun(session, itemId2, clients, handler, treeCtx()),
44802
- escalateDivergence: (watcher) => {
44803
- const id = escalateDivergence(treeCtx(), watcher);
44804
- if (id)
44805
- emitSessionOutput(
44806
- watcher,
44807
- clients,
44808
- watcherNote(
44809
- `divergence escalated to session ${id}; the watcher restarts once it ends`
44810
- )
44811
- );
44812
- },
44813
- resumeWatcher: (escalation) => resumeWatcherAfterEscalation(sessions, escalation, (watcherId) => {
44814
- if (restartManagedSession(sessions, watcherId, clients, handler).ok)
44815
- notify2();
44816
- })
44817
- });
44818
- return handler;
44819
- }
44820
-
44821
- // src/commands/sessions/daemon/applySetStatus.ts
44822
- function applySetStatus(sessions, id, status2, source, onStatusChange, rebind) {
44823
- const session = sessions.get(id);
44824
- if (!session) {
44825
- daemonLog(
44826
- `set-status for unknown session id=${id} status=${status2} (no live session; ignoring)`
44827
- );
44828
- return;
44829
- }
44830
- if (session.status === "stopped") {
44831
- daemonLog(
44832
- `set-status for stopped session id=${id} status=${status2} (held pending durable close; ignoring)`
44833
- );
44834
- return;
44835
- }
44836
- rebind?.(session);
44837
- if (source === "permission") session.permissionActive = true;
44838
- if (status2 === "running") session.permissionActive = false;
44839
- onStatusChange(session, status2);
44840
- }
44841
-
44842
- // src/commands/sessions/daemon/rebindClaudeSession.ts
44843
- function rebindClaudeSession(session, claudeSessionId, notify2, onStatusChange) {
44844
- if (!claudeSessionId) return;
44845
- if (session.claudeSessionId === claudeSessionId) return;
44846
- daemonLog(
44847
- `session ${session.id} claude session moved: ${session.claudeSessionId ?? "none"} -> ${claudeSessionId}`
44848
- );
44849
- session.claudeSessionId = claudeSessionId;
44850
- watchTranscript(session, notify2, onStatusChange);
44851
- }
44852
-
44853
- // src/commands/sessions/daemon/setStatusFromHook.ts
44854
- function setStatusFromHook(sessions, report, notify2, onStatusChange) {
44855
- applySetStatus(
44856
- sessions,
44857
- report.id,
44858
- report.status,
44859
- report.source,
44860
- onStatusChange,
44861
- (session) => rebindClaudeSession(
44862
- session,
44863
- report.claudeSessionId,
44864
- notify2,
44865
- onStatusChange
44866
- )
44867
- );
44868
- }
44869
-
44870
44871
  // src/commands/sessions/daemon/restartAndNotify.ts
44871
44872
  function restartAndNotify(sessions, id, clients, onStatusChange, notify2) {
44872
44873
  const result = restartManagedSession(sessions, id, clients, onStatusChange);
@@ -44908,8 +44909,7 @@ function restoreBase(id, persisted) {
44908
44909
  cwd: persisted.cwd,
44909
44910
  assistArgs: persisted.assistArgs,
44910
44911
  initialPrompt: persisted.initialPrompt,
44911
- starred: persisted.watcher ? false : persisted.starred,
44912
- watcher: persisted.watcher,
44912
+ starred: persisted.starred,
44913
44913
  divergenceEscalation: persisted.divergenceEscalation,
44914
44914
  design: persisted.design,
44915
44915
  auto: persisted.auto,
@@ -44969,12 +44969,19 @@ function underTempRoot(path106) {
44969
44969
  );
44970
44970
  }
44971
44971
 
44972
- // src/commands/sessions/daemon/dropTempRootedSessions.ts
44973
- function dropTempRootedSessions(persisted) {
44972
+ // src/commands/sessions/daemon/dropUnrestorableSessions.ts
44973
+ function droppedReason(entry) {
44974
+ if (underTempRoot(entry.cwd)) return "lies outside any project root";
44975
+ if (entry.watcher === true)
44976
+ return "is a retired watcher session (the daemon now runs the assist auto-update loop itself)";
44977
+ return void 0;
44978
+ }
44979
+ function dropUnrestorableSessions(persisted) {
44974
44980
  return persisted.filter((entry) => {
44975
- if (!underTempRoot(entry.cwd)) return true;
44981
+ const reason4 = droppedReason(entry);
44982
+ if (!reason4) return true;
44976
44983
  daemonLog(
44977
- `persisted session ${describePersistedSession(entry)} lies outside any project root; dropped rather than restored`
44984
+ `persisted session ${describePersistedSession(entry)} ${reason4}; dropped rather than restored`
44978
44985
  );
44979
44986
  return false;
44980
44987
  });
@@ -45299,25 +45306,6 @@ function isUpdate(persisted) {
45299
45306
  return persisted.commandType === "assist" && persisted.assistArgs?.[0] === "update";
45300
45307
  }
45301
45308
 
45302
- // src/commands/sessions/daemon/migrateClaudeWatcher.ts
45303
- function migrateClaudeWatcher(id, persisted) {
45304
- if (!persisted.watcher || persisted.commandType !== "claude")
45305
- return persisted;
45306
- daemonLog(
45307
- `migrating claude /watch watcher session ${id} to assist ${WATCHER_ARGS.join(" ")}`
45308
- );
45309
- return {
45310
- ...persisted,
45311
- name: `assist ${WATCHER_ARGS.join(" ")}`,
45312
- commandType: "assist",
45313
- assistArgs: WATCHER_ARGS,
45314
- initialPrompt: void 0,
45315
- claudeSessionId: void 0,
45316
- harness: void 0,
45317
- harnessSessionId: void 0
45318
- };
45319
- }
45320
-
45321
45309
  // src/commands/sessions/daemon/needsWrapperRelaunch.ts
45322
45310
  var LAUNCH_COMMANDS = ["draft", "feat", "bug", "refine"];
45323
45311
  function needsWrapperRelaunch(persisted) {
@@ -45330,36 +45318,11 @@ function isOnceLaunch(persisted) {
45330
45318
  return persisted.commandType === "assist" && !!persisted.assistArgs && LAUNCH_COMMANDS.includes(persisted.assistArgs[0]) && persisted.assistArgs.includes("--once");
45331
45319
  }
45332
45320
 
45333
- // src/commands/sessions/daemon/relaunchConsoleWatcher.ts
45334
- function isConsoleWatcher(persisted) {
45335
- return persisted.watcher === true && persisted.commandType === "assist" && !!persisted.assistArgs;
45336
- }
45337
- function relaunchConsoleWatcher(id, persisted, base) {
45338
- const command = ["assist", ...persisted.assistArgs];
45339
- daemonLog(
45340
- `relaunching watcher session ${id} running ${command.join(" ")} in ${persisted.cwd ?? "(no cwd)"}`
45341
- );
45342
- const session = runningSession(
45343
- base,
45344
- persisted,
45345
- spawnPty(command, persisted.cwd, id)
45346
- );
45347
- if (persisted.cwd)
45348
- session.scrollback = seedWatcherScrollback(
45349
- persisted.cwd,
45350
- "daemon restarted; watcher relaunched"
45351
- );
45352
- return session;
45353
- }
45354
-
45355
45321
  // src/commands/sessions/daemon/restoreSession.ts
45356
- function restoreSession(id, stored) {
45357
- const persisted = migrateClaudeWatcher(id, stored);
45322
+ function restoreSession(id, persisted) {
45358
45323
  const base = restoreBase(id, persisted);
45359
45324
  if (persisted.status === "stopped") return stoppedSession(base, persisted);
45360
45325
  if (isUpdate(persisted)) return updatedSession(id, persisted);
45361
- if (isConsoleWatcher(persisted))
45362
- return relaunchConsoleWatcher(id, persisted, base);
45363
45326
  const status2 = deriveRestoreStatus(persisted);
45364
45327
  if (needsWrapperRelaunch(persisted)) {
45365
45328
  const resumesWrittenConversation = hasTranscriptOnDisk(persisted);
@@ -45413,7 +45376,7 @@ function logRestoreError(persisted, error) {
45413
45376
 
45414
45377
  // src/commands/sessions/daemon/restoreAll.ts
45415
45378
  function restoreAll(spawner, sessions) {
45416
- const persisted = dropTempRootedSessions(loadPersistedSessions());
45379
+ const persisted = dropUnrestorableSessions(loadPersistedSessions());
45417
45380
  const cap2 = sessionLimits.maxRestore();
45418
45381
  const restoredIds = /* @__PURE__ */ new Map();
45419
45382
  const names = persisted.slice(0, cap2).map((entry) => {
@@ -45669,10 +45632,10 @@ function restoreAllSessions(spawner, sessions, notify2) {
45669
45632
 
45670
45633
  // src/commands/sessions/daemon/retrySession.ts
45671
45634
  function retrySession(session, clients, onStatusChange) {
45672
- const spawn11 = respawnThunk(session);
45673
- if (!spawn11) return false;
45635
+ const spawn12 = respawnThunk(session);
45636
+ if (!spawn12) return false;
45674
45637
  const respawn = () => {
45675
- respawnSession(session, spawn11, "running", clients, onStatusChange);
45638
+ respawnSession(session, spawn12, "running", clients, onStatusChange);
45676
45639
  daemonLog(`session ${session.id} retried: ${session.name}`);
45677
45640
  };
45678
45641
  const pty2 = session.pty;
@@ -46701,6 +46664,30 @@ function addAgentToStream(ctx, targetId, request) {
46701
46664
  };
46702
46665
  }
46703
46666
 
46667
+ // src/commands/sessions/daemon/createAssistSession.ts
46668
+ function createAssistSession(id, assistArgs, cwd, meta, holdPty) {
46669
+ const startedAt = Date.now();
46670
+ return {
46671
+ id,
46672
+ name: `assist ${assistArgs.join(" ")}`,
46673
+ title: meta?.title,
46674
+ subtitle: meta?.subtitle,
46675
+ commandType: "assist",
46676
+ status: "running",
46677
+ startedAt,
46678
+ runningMs: 0,
46679
+ runningSince: startedAt,
46680
+ waitingSince: null,
46681
+ ...startOrHoldPty(
46682
+ () => spawnPty(["assist", ...assistArgs], cwd, id),
46683
+ holdPty
46684
+ ),
46685
+ scrollback: "",
46686
+ assistArgs,
46687
+ cwd
46688
+ };
46689
+ }
46690
+
46704
46691
  // src/commands/sessions/daemon/worktree/spawnInTree.ts
46705
46692
  function spawnInTree(ctx, { prompt, cwd, design, auto, harness, inPlace }, context) {
46706
46693
  return allocateAndBind(
@@ -46719,7 +46706,7 @@ function spawnInTree(ctx, { prompt, cwd, design, auto, harness, inPlace }, conte
46719
46706
  );
46720
46707
  }
46721
46708
  function spawnAssistInTree(ctx, assistArgs, cwd, meta, context) {
46722
- const id = allocateAndBind(
46709
+ return allocateAndBind(
46723
46710
  ctx,
46724
46711
  cwd,
46725
46712
  (sid, resolvedCwd, holdUntilSeeded) => createAssistSession(sid, assistArgs, resolvedCwd, meta, holdUntilSeeded),
@@ -46732,8 +46719,6 @@ function spawnAssistInTree(ctx, assistArgs, cwd, meta, context) {
46732
46719
  },
46733
46720
  context
46734
46721
  );
46735
- if (isBacklogRunArgs(assistArgs)) ensureWatcher(ctx, cwd);
46736
- return id;
46737
46722
  }
46738
46723
 
46739
46724
  // src/commands/sessions/daemon/worktree/resumeInTree.ts
@@ -46960,13 +46945,11 @@ var SessionManager = class {
46960
46945
  );
46961
46946
  }
46962
46947
  dismissSession = (id) => {
46963
- dismissAndResumeWatcher(
46964
- this.sessions,
46965
- id,
46966
- this.notify,
46967
- (watcherId) => this.restart(watcherId)
46968
- );
46948
+ dismissSessionGated(this.sessions, id, this.notify);
46969
46949
  };
46950
+ startAutoUpdate() {
46951
+ startAutoUpdate(() => this.treeCtx());
46952
+ }
46970
46953
  discardSession = (id) => {
46971
46954
  dismissSessionGated(this.sessions, id, this.notify, true);
46972
46955
  };
@@ -47044,10 +47027,10 @@ var handleHelloRequest = (client, m, d) => {
47044
47027
  };
47045
47028
 
47046
47029
  // src/commands/sessions/daemon/creator.ts
47047
- function creator(isNew, spawn11) {
47030
+ function creator(isNew, spawn12) {
47048
47031
  return (client, m, d) => {
47049
47032
  if (m.links.route(client, d)) return;
47050
- const result = spawn11(m, d);
47033
+ const result = spawn12(m, d);
47051
47034
  if (typeof result !== "string") {
47052
47035
  sendTo(client, { type: "error", message: result.error });
47053
47036
  return;
@@ -47713,6 +47696,7 @@ function onListening(manager, checkAutoExit) {
47713
47696
  );
47714
47697
  daemonLog(`listening on ${daemonPaths.socket}`);
47715
47698
  manager.links.reload();
47699
+ manager.startAutoUpdate();
47716
47700
  checkAutoExit(manager.isIdle());
47717
47701
  }
47718
47702
  function cleanupOwnedFiles() {
@@ -47815,6 +47799,7 @@ function registerDaemon(program2) {
47815
47799
  cmd.command("drain").description(
47816
47800
  "Remove all sessions from the local daemon for a clean slate (does not affect the Windows daemon); prompts before closing live sessions, and a session holding unpushed work is stopped, not removed"
47817
47801
  ).option("--yes", "Close live sessions without confirming").action((options2) => drainDaemon(options2));
47802
+ configHelp(cmd, autoUpdateConfigHelp);
47818
47803
  }
47819
47804
 
47820
47805
  // src/commands/sessions/closeSession.ts
@@ -48075,14 +48060,14 @@ async function linkStatus(name, query = queryNodes) {
48075
48060
  return (await query())?.links.find((l) => l.name === name);
48076
48061
  }
48077
48062
  async function awaitLinkReturn(name, options2, deps2 = {}) {
48078
- const { query = queryNodes, sleep: sleep2 = wait, now = Date.now } = deps2;
48063
+ const { query = queryNodes, sleep: sleep3 = wait, now = Date.now } = deps2;
48079
48064
  const deadline = now() + options2.timeoutMs;
48080
48065
  let sawDown = options2.sawDown;
48081
48066
  while (now() < deadline) {
48082
48067
  const link3 = await linkStatus(name, query);
48083
48068
  if (link3?.state !== "connected") sawDown = true;
48084
48069
  else if (sawDown) return link3;
48085
- await sleep2(POLL_MS4);
48070
+ await sleep3(POLL_MS4);
48086
48071
  }
48087
48072
  return void 0;
48088
48073
  }