@staff0rd/assist 0.774.0 → 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.774.0",
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,
@@ -25861,7 +25873,6 @@ function wouldDisturbWorkInProgress(clone, forCheckout) {
25861
25873
  // src/commands/sessions/daemon/worktree/worktreeConfigFor.ts
25862
25874
  var DISABLED = {
25863
25875
  enabled: false,
25864
- watcher: false,
25865
25876
  trunk: false,
25866
25877
  includeDrafts: false,
25867
25878
  install: true,
@@ -25994,7 +26005,6 @@ function toPersistedSession(session) {
25994
26005
  assistArgs: session.assistArgs,
25995
26006
  activity: session.activity,
25996
26007
  starred: session.starred,
25997
- watcher: session.watcher,
25998
26008
  divergenceEscalation: session.divergenceEscalation,
25999
26009
  design: session.design,
26000
26010
  auto: session.auto,
@@ -41405,7 +41415,6 @@ function toSessionInfo(session) {
41405
41415
  autoRun,
41406
41416
  autoAdvance,
41407
41417
  starred,
41408
- watcher,
41409
41418
  design,
41410
41419
  verifying,
41411
41420
  pendingPrPreview,
@@ -41432,7 +41441,6 @@ function toSessionInfo(session) {
41432
41441
  autoRun,
41433
41442
  autoAdvance,
41434
41443
  starred,
41435
- watcher,
41436
41444
  design,
41437
41445
  verifying,
41438
41446
  pendingPrPreview,
@@ -41988,6 +41996,359 @@ function createRunSession(id, { runName, runArgs, cwd, meta }) {
41988
41996
  };
41989
41997
  }
41990
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
+
41991
42352
  // src/commands/sessions/daemon/setStatus.ts
41992
42353
  function setStatus2(session, newStatus) {
41993
42354
  const now = Date.now();
@@ -42010,16 +42371,16 @@ function setStatus2(session, newStatus) {
42010
42371
 
42011
42372
  // src/commands/sessions/daemon/worktree/reapWorktree.ts
42012
42373
  import { existsSync as existsSync87 } from "fs";
42013
- import { basename as basename28 } from "path";
42374
+ import { basename as basename29 } from "path";
42014
42375
 
42015
42376
  // src/commands/sessions/daemon/worktree/deleteStrandedTree.ts
42016
42377
  import { existsSync as existsSync86 } from "fs";
42017
- import { join as join102 } from "path";
42378
+ import { join as join103 } from "path";
42018
42379
 
42019
42380
  // src/commands/sessions/daemon/worktree/deleteTreeDirectly.ts
42020
42381
  import { statSync as statSync15 } from "fs";
42021
42382
  import { rm as rm3 } from "fs/promises";
42022
- import { join as join101 } from "path";
42383
+ import { join as join102 } from "path";
42023
42384
  async function deleteTreeDirectly(clone, worktreePath, why) {
42024
42385
  if (holdsAGitDirectoryRatherThanALink(worktreePath)) {
42025
42386
  const refusal = "it is a clone of its own, not a linked worktree";
@@ -42047,7 +42408,7 @@ async function deleteTreeDirectly(clone, worktreePath, why) {
42047
42408
  return { removed: true };
42048
42409
  }
42049
42410
  function holdsAGitDirectoryRatherThanALink(worktreePath) {
42050
- return statSync15(join101(worktreePath, ".git"), {
42411
+ return statSync15(join102(worktreePath, ".git"), {
42051
42412
  throwIfNoEntry: false
42052
42413
  })?.isDirectory() === true;
42053
42414
  }
@@ -42083,7 +42444,7 @@ async function deleteStrandedTree(clone, worktreePath, cause) {
42083
42444
  );
42084
42445
  }
42085
42446
  function strandedReason(worktreePath, cause) {
42086
- if (!existsSync86(join102(worktreePath, ".git")))
42447
+ if (!existsSync86(join103(worktreePath, ".git")))
42087
42448
  return "its .git link is already gone";
42088
42449
  if (/not a working tree|not a git repository/i.test(reason2(cause)))
42089
42450
  return "git no longer recognises it as a working tree";
@@ -42153,7 +42514,7 @@ async function reapWorktree(worktreePath, force = false) {
42153
42514
  const clone = owningClone(worktreePath);
42154
42515
  const removal = await removeTree(clone, worktreePath, force);
42155
42516
  if (!removal.removed) return removal;
42156
- await deleteWorktreeBranch(clone, basename28(worktreePath));
42517
+ await deleteWorktreeBranch(clone, basename29(worktreePath));
42157
42518
  forgetWorktree(worktreePath);
42158
42519
  daemonLog(`worktree ${worktreePath} reaped${force ? " (forced)" : ""}`);
42159
42520
  return removal;
@@ -42227,34 +42588,6 @@ function killPtyTree(pty2) {
42227
42588
  }
42228
42589
  }
42229
42590
 
42230
- // src/commands/sessions/daemon/worktree/liveWatcherFor.ts
42231
- function liveWatcherFor(sessions, clone) {
42232
- for (const session of sessions.values())
42233
- if (session.watcher === true && session.cwd !== void 0 && (clone === void 0 || canonicalTreePath(session.cwd) === clone) && session.status !== "stopped" && session.status !== "error")
42234
- return session;
42235
- return void 0;
42236
- }
42237
-
42238
- // src/commands/sessions/daemon/worktree/resolveClone.ts
42239
- function resolveClone(cwd) {
42240
- const repoRoot2 = findRepoRoot(cwd) ?? cwd;
42241
- return canonicalTreePath(mainWorktree(repoRoot2) ?? repoRoot2);
42242
- }
42243
-
42244
- // src/commands/sessions/daemon/worktree/orphanedWatcherFor.ts
42245
- function orphanedWatcherFor(sessions, dismissed) {
42246
- if (dismissed.watcher === true || !dismissed.cwd) return void 0;
42247
- if (!liveWatcherFor(sessions)) return void 0;
42248
- const known = dismissed.worktree?.clone ?? dismissed.releasedFromClone;
42249
- const clone = known ? canonicalTreePath(known) : resolveClone(dismissed.cwd);
42250
- const watcher = liveWatcherFor(sessions, clone);
42251
- if (!watcher) return void 0;
42252
- const kept = [...sessions.values()].some(
42253
- (s) => s.watcher !== true && s.cwd !== void 0 && resolveClone(s.cwd) === clone
42254
- );
42255
- return kept ? void 0 : { watcher, clone };
42256
- }
42257
-
42258
42591
  // src/commands/sessions/daemon/worktree/treeUnderClose.ts
42259
42592
  function treeUnderClose(session) {
42260
42593
  if (session.worktree) return { path: session.worktree.path, removable: true };
@@ -42285,13 +42618,6 @@ function dismissSession(sessions, id) {
42285
42618
  if (s.activity?.itemId != null) releaseLock(s.activity.itemId);
42286
42619
  sessions.delete(id);
42287
42620
  daemonLog(`session ${id} dismissed (${s.name})`);
42288
- const orphan = orphanedWatcherFor(sessions, s);
42289
- if (orphan) {
42290
- daemonLog(
42291
- `reaping watcher session ${orphan.watcher.id} for the clone ${orphan.clone}: its last session ${id} was dismissed`
42292
- );
42293
- dismissSession(sessions, orphan.watcher.id);
42294
- }
42295
42621
  if (!s.worktree) return true;
42296
42622
  if (sharedWith.length > 0)
42297
42623
  daemonLog(
@@ -42526,43 +42852,6 @@ function dismissSessionGated(sessions, id, notify2, discard = false) {
42526
42852
  settle();
42527
42853
  }
42528
42854
 
42529
- // src/commands/sessions/daemon/worktree/resumeWatcherAfterEscalation.ts
42530
- var ENDED = ["done", "error"];
42531
- var endedViaDismissOrExit = /* @__PURE__ */ new WeakSet();
42532
- function resumeWatcherAfterEscalation(sessions, escalation, restart) {
42533
- if (escalation.divergenceEscalation !== true || !escalation.cwd) return;
42534
- if (endedViaDismissOrExit.has(escalation)) return;
42535
- endedViaDismissOrExit.add(escalation);
42536
- const clone = canonicalTreePath(escalation.cwd);
42537
- const watcher = [...sessions.values()].find(
42538
- (s) => s.watcher === true && s.cwd !== void 0 && canonicalTreePath(s.cwd) === clone
42539
- );
42540
- if (!watcher) {
42541
- daemonLog(
42542
- `escalation session ${escalation.id} ended: no watcher left in the clone ${clone} to restart`
42543
- );
42544
- return;
42545
- }
42546
- if (!ENDED.includes(watcher.status)) {
42547
- daemonLog(
42548
- `escalation session ${escalation.id} ended: watcher session ${watcher.id} in the clone ${clone} is already ${watcher.status}`
42549
- );
42550
- return;
42551
- }
42552
- daemonLog(
42553
- `escalation session ${escalation.id} ended: restarting watcher session ${watcher.id} in the clone ${clone}`
42554
- );
42555
- watcher.watcherRestartReason = `after escalation session ${escalation.id} ended`;
42556
- restart(watcher.id);
42557
- }
42558
-
42559
- // src/commands/sessions/daemon/dismissAndResumeWatcher.ts
42560
- function dismissAndResumeWatcher(sessions, id, notify2, restart) {
42561
- const session = sessions.get(id);
42562
- dismissSessionGated(sessions, id, notify2);
42563
- if (session) resumeWatcherAfterEscalation(sessions, session, restart);
42564
- }
42565
-
42566
42855
  // src/commands/sessions/daemon/drainSessions.ts
42567
42856
  function drainSessions(sessions, notify2) {
42568
42857
  const drained = [...sessions.values()].map((s) => ({
@@ -42976,37 +43265,10 @@ function startSessionTitleGeneration(session, notify2) {
42976
43265
  runSessionTitleGeneration(session, prompt, notify2, singleLineTitle(prompt));
42977
43266
  }
42978
43267
 
42979
- // src/commands/sessions/daemon/worktree/appendWatcherLog.ts
42980
- import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync34 } from "fs";
42981
- import { dirname as dirname42 } from "path";
42982
-
42983
- // src/commands/sessions/daemon/worktree/watcherLogPath.ts
42984
- import { createHash as createHash4 } from "crypto";
42985
- import { homedir as homedir26 } from "os";
42986
- import { basename as basename29, join as join103 } from "path";
42987
- function watcherLogPath(cwd) {
42988
- const clone = canonicalTreePath(cwd);
42989
- const key = createHash4("sha1").update(clone).digest("hex").slice(0, 8);
42990
- const dir = process.env.ASSIST_WATCHER_LOG_DIR ?? join103(homedir26(), ".assist", "watchers");
42991
- return join103(dir, `${basename29(clone) || "root"}-${key}.log`);
42992
- }
42993
-
42994
- // src/commands/sessions/daemon/worktree/appendWatcherLog.ts
42995
- function appendWatcherLog(session, data) {
42996
- if (session.watcher !== true || !session.cwd) return;
42997
- try {
42998
- const path106 = watcherLogPath(session.cwd);
42999
- mkdirSync34(dirname42(path106), { recursive: true });
43000
- appendFileSync3(path106, data);
43001
- } catch {
43002
- }
43003
- }
43004
-
43005
43268
  // src/commands/sessions/daemon/emitSessionOutput.ts
43006
43269
  var MAX_SCROLLBACK = 256 * 1024;
43007
43270
  function emitSessionOutput(session, clients, data) {
43008
43271
  session.scrollback += data;
43009
- appendWatcherLog(session, data);
43010
43272
  if (session.scrollback.length > MAX_SCROLLBACK) {
43011
43273
  session.scrollback = session.scrollback.slice(-MAX_SCROLLBACK);
43012
43274
  }
@@ -44077,13 +44339,6 @@ function applyUsageRecord(sessions, windows, notify2, claudeSessionId, transcrip
44077
44339
  notify2();
44078
44340
  }
44079
44341
 
44080
- // src/commands/sessions/daemon/watcherNote.ts
44081
- function watcherNote(text17) {
44082
- return `\r
44083
- \x1B[2m\u2500\u2500 ${text17} \u2500\u2500\x1B[0m\r
44084
- `;
44085
- }
44086
-
44087
44342
  // src/commands/sessions/daemon/shouldAutoRun.ts
44088
44343
  function shouldAutoRun(session, status2 = session.status) {
44089
44344
  if (!session.autoRun) return { run: false, reason: null };
@@ -44138,7 +44393,6 @@ function shouldAutoDismiss(session, exitCode) {
44138
44393
  }
44139
44394
 
44140
44395
  // src/commands/sessions/daemon/finishStatusChange.ts
44141
- var DIVERGED_EXIT_CODE = 3;
44142
44396
  function finishStatusChange(session, status2, exitCode, deps2) {
44143
44397
  daemonLog(`session ${session.id} status: ${session.status} -> ${status2}`);
44144
44398
  void flushPhaseActiveMs(session);
@@ -44146,9 +44400,6 @@ function finishStatusChange(session, status2, exitCode, deps2) {
44146
44400
  if (status2 === "waiting")
44147
44401
  startTranscriptTitleGeneration(session, deps2.notify);
44148
44402
  if (status2 !== "stopped") session.undurable = void 0;
44149
- if (session.watcher === true && exitCode === DIVERGED_EXIT_CODE)
44150
- deps2.escalateDivergence?.(session);
44151
- if (status2 === "done" || status2 === "error") deps2.resumeWatcher?.(session);
44152
44403
  const autoRun = shouldAutoRun(session);
44153
44404
  if (autoRun.run) {
44154
44405
  deps2.reuseForRun(session, autoRun.itemId);
@@ -44194,40 +44445,6 @@ function makeStatusChangeHandler(sessions, deps2) {
44194
44445
  );
44195
44446
  }
44196
44447
 
44197
- // src/commands/sessions/daemon/worktree/detectExistingWorktree.ts
44198
- function detectExistingWorktree(cwd) {
44199
- const repoRoot2 = findRepoRoot(cwd) ?? cwd;
44200
- if (!worktreeConfigFor(repoRoot2).enabled) return void 0;
44201
- const clone = mainWorktree(repoRoot2);
44202
- if (clone && clone !== repoRoot2) return { path: repoRoot2, clone };
44203
- return void 0;
44204
- }
44205
-
44206
- // src/commands/sessions/daemon/worktree/bindNewWorktree.ts
44207
- function bindNewWorktree(session, alloc, notify2, startHeld = () => {
44208
- }) {
44209
- if (alloc.kind !== "worktree" || !alloc.cwd || !alloc.clone || !session)
44210
- return;
44211
- session.worktree = { path: alloc.cwd, clone: alloc.clone };
44212
- if (alloc.created)
44213
- seedWorktree(alloc.cwd, alloc.clone, () => startHeld(session));
44214
- notify2();
44215
- }
44216
- function bindRestoredWorktrees(sessions) {
44217
- for (const session of sessions.values()) {
44218
- if (session.worktree || !session.cwd) continue;
44219
- const worktree = detectExistingWorktree(session.cwd);
44220
- if (worktree) session.worktree = worktree;
44221
- }
44222
- }
44223
- function bindResumedWorktree(session, cwd, notify2) {
44224
- if (!session) return;
44225
- const worktree = detectExistingWorktree(cwd);
44226
- if (!worktree) return;
44227
- session.worktree = worktree;
44228
- notify2();
44229
- }
44230
-
44231
44448
  // src/commands/sessions/daemon/attachReusedRun.ts
44232
44449
  function attachReusedRun(session, alloc, clients, onStatusChange, tree) {
44233
44450
  broadcast(clients, { type: "clear", sessionId: session.id });
@@ -44306,137 +44523,6 @@ function startReusedRunPty(session, assistArgs, itemId2, hold, clients, onStatus
44306
44523
  return true;
44307
44524
  }
44308
44525
 
44309
- // src/commands/sessions/daemon/createAssistSession.ts
44310
- function createAssistSession(id, assistArgs, cwd, meta, holdPty) {
44311
- const startedAt = Date.now();
44312
- return {
44313
- id,
44314
- name: `assist ${assistArgs.join(" ")}`,
44315
- title: meta?.title,
44316
- subtitle: meta?.subtitle,
44317
- commandType: "assist",
44318
- status: "running",
44319
- startedAt,
44320
- runningMs: 0,
44321
- runningSince: startedAt,
44322
- waitingSince: null,
44323
- ...startOrHoldPty(
44324
- () => spawnPty(["assist", ...assistArgs], cwd, id),
44325
- holdPty
44326
- ),
44327
- scrollback: "",
44328
- assistArgs,
44329
- cwd
44330
- };
44331
- }
44332
-
44333
- // src/commands/sessions/daemon/worktree/seedWatcherScrollback.ts
44334
- import { readFileSync as readFileSync65, writeFileSync as writeFileSync54 } from "fs";
44335
- var SCROLLBACK_TAIL = 256 * 1024;
44336
- var MAX_LOG_SIZE = 4 * 1024 * 1024;
44337
- function readLogTail2(path106) {
44338
- let log2;
44339
- try {
44340
- log2 = readFileSync65(path106, "utf8");
44341
- } catch {
44342
- return "";
44343
- }
44344
- if (log2.length > MAX_LOG_SIZE) {
44345
- log2 = log2.slice(-SCROLLBACK_TAIL);
44346
- writeFileSync54(path106, log2);
44347
- daemonLog(`watcher log ${path106} trimmed to its last ${log2.length} chars`);
44348
- }
44349
- return log2.slice(-SCROLLBACK_TAIL);
44350
- }
44351
- function seedWatcherScrollback(cwd, event) {
44352
- const path106 = watcherLogPath(cwd);
44353
- const history = readLogTail2(path106);
44354
- const note = watcherNote(`${event} at ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
44355
- appendWatcherLog({ watcher: true, cwd }, note);
44356
- daemonLog(
44357
- `watcher in ${cwd} seeded with ${history.length} chars of history from ${path106} (${event})`
44358
- );
44359
- return history + note;
44360
- }
44361
-
44362
- // src/commands/sessions/daemon/createWatcherSession.ts
44363
- var WATCHER_ARGS = ["watch", "loop"];
44364
- function createWatcherSession(id, cwd) {
44365
- return {
44366
- ...createAssistSession(id, WATCHER_ARGS, cwd),
44367
- watcher: true,
44368
- scrollback: seedWatcherScrollback(cwd, "watcher started")
44369
- };
44370
- }
44371
-
44372
- // src/commands/sessions/daemon/worktree/boundTreeRoots.ts
44373
- function boundTreeRoots(sessions, exclude) {
44374
- const roots = /* @__PURE__ */ new Set();
44375
- for (const session of sessions.values()) {
44376
- if (session === exclude) continue;
44377
- const root2 = treeRootOf(session);
44378
- if (root2 && !roots.has(root2) && holdsTree(session, root2)) roots.add(root2);
44379
- }
44380
- return roots;
44381
- }
44382
- function treeRootOf(session) {
44383
- if (!session.cwd) return void 0;
44384
- return canonicalTreePath(findRepoRoot(session.cwd) ?? session.cwd);
44385
- }
44386
- function holdsTree(session, root2) {
44387
- if (session.closing) return true;
44388
- if (session.status !== "done" && session.status !== "error") return true;
44389
- if (!worktreeConfigFor(root2).enabled) return true;
44390
- const durability = checkDurabilitySync(root2);
44391
- daemonLog(`tree ${root2} ${releaseReason(durability, session)}`);
44392
- return !durability.durable;
44393
- }
44394
- function releaseReason(durability, session) {
44395
- if (!durability.durable)
44396
- return `still held by finished session ${session.id}: ${durability.reason}`;
44397
- if (durability.gone)
44398
- return `free for allocation: session ${session.id}'s directory is gone from disk \u2014 nothing to land, not landed work`;
44399
- return `free for allocation: session ${session.id} finished and its work is landed`;
44400
- }
44401
-
44402
- // src/commands/sessions/daemon/worktree/allocateAndBind.ts
44403
- function allocateAndBind(ctx, cwd, create, options2 = {}, context) {
44404
- const alloc = allocateTree(cwd, boundTreeRoots(ctx.sessions), options2);
44405
- const needsSeeding = alloc.kind === "worktree" && alloc.created === true;
44406
- const id = ctx.spawnWith(
44407
- (sid) => create(sid, alloc.cwd, needsSeeding),
44408
- context
44409
- );
44410
- bindNewWorktree(ctx.sessions.get(id), alloc, ctx.notify, ctx.startHeld);
44411
- return id;
44412
- }
44413
-
44414
- // src/commands/sessions/daemon/worktree/ensureWatcher.ts
44415
- function ensureWatcher(ctx, cwd) {
44416
- if (!cwd) return void 0;
44417
- const repoRoot2 = findRepoRoot(cwd) ?? cwd;
44418
- const cfg = worktreeConfigFor(repoRoot2);
44419
- if (!cfg.enabled || cfg.watcher !== true) return void 0;
44420
- const clone = resolveClone(repoRoot2);
44421
- const live = liveWatcherFor(ctx.sessions, clone);
44422
- if (live) {
44423
- daemonLog(
44424
- `no watcher spawned for the clone ${clone}: session ${live.id} is already watching it (${live.status})`
44425
- );
44426
- return void 0;
44427
- }
44428
- const id = allocateAndBind(
44429
- ctx,
44430
- clone,
44431
- (sid, resolvedCwd) => createWatcherSession(sid, resolvedCwd ?? clone),
44432
- { inPlace: true }
44433
- );
44434
- daemonLog(
44435
- `spawned watcher session ${id} running assist ${WATCHER_ARGS.join(" ")} in the clone ${clone}`
44436
- );
44437
- return id;
44438
- }
44439
-
44440
44526
  // src/commands/sessions/daemon/worktree/isBacklogRunArgs.ts
44441
44527
  var ITEM_ID = /^a?\d+$/;
44442
44528
  function isBacklogRunArgs(args) {
@@ -44500,7 +44586,6 @@ function failChainedRun(session, itemId2, error, clients, tree) {
44500
44586
  // src/commands/sessions/daemon/reuseSessionForRun.ts
44501
44587
  function reuseSessionForRun(session, itemId2, clients, onStatusChange, tree) {
44502
44588
  const assistArgs = ["backlog", "run", String(itemId2)];
44503
- const originCwd = session.cwd;
44504
44589
  resetCardForRun(session, assistArgs);
44505
44590
  let alloc;
44506
44591
  try {
@@ -44522,13 +44607,71 @@ function reuseSessionForRun(session, itemId2, clients, onStatusChange, tree) {
44522
44607
  onStatusChange
44523
44608
  );
44524
44609
  if (!started) return;
44525
- if (tree) ensureWatcher(tree, originCwd);
44526
44610
  attachReusedRun(session, alloc, clients, onStatusChange, tree);
44527
44611
  daemonLog(
44528
44612
  `session ${session.id} reused for backlog run ${itemId2}: ${session.name}`
44529
44613
  );
44530
44614
  }
44531
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
+
44532
44675
  // src/commands/sessions/daemon/respawnSession.ts
44533
44676
  function respawnSession(session, respawn, status2, clients, onStatusChange) {
44534
44677
  session.gitWatcher?.close();
@@ -44537,8 +44680,7 @@ function respawnSession(session, respawn, status2, clients, onStatusChange) {
44537
44680
  session.pendingDismiss = void 0;
44538
44681
  session.closeGrace?.cancel();
44539
44682
  session.closing = void 0;
44540
- const keepHistory = session.watcher === true;
44541
- if (!keepHistory) session.scrollback = "";
44683
+ session.scrollback = "";
44542
44684
  session.startedAt = Date.now();
44543
44685
  session.runningMs = 0;
44544
44686
  session.runningSince = null;
@@ -44555,15 +44697,7 @@ function respawnSession(session, respawn, status2, clients, onStatusChange) {
44555
44697
  session.pty?.resize(session.cols, session.rows);
44556
44698
  } catch {
44557
44699
  }
44558
- if (keepHistory) {
44559
- const reason4 = session.watcherRestartReason;
44560
- session.watcherRestartReason = void 0;
44561
- emitSessionOutput(
44562
- session,
44563
- clients,
44564
- watcherNote(reason4 ? `watcher restarted ${reason4}` : "watcher restarted")
44565
- );
44566
- } else broadcast(clients, { type: "clear", sessionId: session.id });
44700
+ broadcast(clients, { type: "clear", sessionId: session.id });
44567
44701
  wirePtyEvents(session, clients, onStatusChange);
44568
44702
  }
44569
44703
 
@@ -44734,144 +44868,6 @@ function restartManagedSession(sessions, id, clients, onStatusChange) {
44734
44868
  return { ok: true };
44735
44869
  }
44736
44870
 
44737
- // src/commands/sessions/daemon/worktree/divergencePrompt.ts
44738
- var MAX_REASON_LINES = 20;
44739
- var MAX_REASON_CHARS = 2e3;
44740
- var RECONCILE_STEPS = [
44741
- "Reconcile it yourself, without discarding any commits on either side:",
44742
- "",
44743
- "1. Inspect `git status`, `git log --oneline @{u}..@` and `git log --oneline @..@{u}`.",
44744
- '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.',
44745
- "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.",
44746
- "4. Push the result with a plain `git push` so the branch matches its upstream again.",
44747
- "5. Build the clone with `assist run auto-build`.",
44748
- "6. Run /close. The daemon restarts the watcher once this session closes.",
44749
- "",
44750
- "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."
44751
- ];
44752
- var SIMULATED_STEPS = [
44753
- "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."
44754
- ];
44755
- function divergenceReason(scrollback) {
44756
- 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);
44757
- }
44758
- function divergencePrompt(clone, scrollback) {
44759
- const reason4 = divergenceReason(scrollback);
44760
- return [
44761
- `The watcher in ${clone} stopped because \`assist watch wait --pull --build\` could not fast-forward the branch (exit 3). The tail of its output:`,
44762
- "",
44763
- "```",
44764
- reason4 || "(no output captured)",
44765
- "```",
44766
- "",
44767
- ...reason4.includes(simulatedDivergenceNote) ? SIMULATED_STEPS : RECONCILE_STEPS
44768
- ].join("\n");
44769
- }
44770
-
44771
- // src/commands/sessions/daemon/worktree/escalateDivergence.ts
44772
- var ENDED2 = ["done", "error", "stopped"];
44773
- function escalateDivergence(ctx, watcher) {
44774
- if (!watcher.cwd) return void 0;
44775
- const clone = canonicalTreePath(watcher.cwd);
44776
- const live = [...ctx.sessions.values()].find(
44777
- (s) => s.divergenceEscalation === true && s.cwd !== void 0 && canonicalTreePath(s.cwd) === clone && !ENDED2.includes(s.status)
44778
- );
44779
- if (live) {
44780
- daemonLog(
44781
- `watcher session ${watcher.id} exited 3 (divergence) in the clone ${clone}: no escalation spawned, session ${live.id} is already diagnosing it (${live.status})`
44782
- );
44783
- return void 0;
44784
- }
44785
- const prompt = divergencePrompt(clone, watcher.scrollback);
44786
- const id = allocateAndBind(
44787
- ctx,
44788
- clone,
44789
- (sid, resolvedCwd) => ({
44790
- ...createSession(sid, { prompt, cwd: resolvedCwd ?? clone }),
44791
- divergenceEscalation: true
44792
- }),
44793
- { inPlace: true }
44794
- );
44795
- daemonLog(
44796
- `watcher session ${watcher.id} exited 3 (divergence) in the clone ${clone}: spawned escalation session ${id} to diagnose it`
44797
- );
44798
- return id;
44799
- }
44800
-
44801
- // src/commands/sessions/daemon/managerStatusChangeHandler.ts
44802
- function managerStatusChangeHandler(sessions, clients, dismiss, notify2, treeCtx) {
44803
- const handler = makeStatusChangeHandler(sessions, {
44804
- dismiss,
44805
- notify: notify2,
44806
- reuseForRun: (session, itemId2) => reuseSessionForRun(session, itemId2, clients, handler, treeCtx()),
44807
- escalateDivergence: (watcher) => {
44808
- const id = escalateDivergence(treeCtx(), watcher);
44809
- if (id)
44810
- emitSessionOutput(
44811
- watcher,
44812
- clients,
44813
- watcherNote(
44814
- `divergence escalated to session ${id}; the watcher restarts once it ends`
44815
- )
44816
- );
44817
- },
44818
- resumeWatcher: (escalation) => resumeWatcherAfterEscalation(sessions, escalation, (watcherId) => {
44819
- if (restartManagedSession(sessions, watcherId, clients, handler).ok)
44820
- notify2();
44821
- })
44822
- });
44823
- return handler;
44824
- }
44825
-
44826
- // src/commands/sessions/daemon/applySetStatus.ts
44827
- function applySetStatus(sessions, id, status2, source, onStatusChange, rebind) {
44828
- const session = sessions.get(id);
44829
- if (!session) {
44830
- daemonLog(
44831
- `set-status for unknown session id=${id} status=${status2} (no live session; ignoring)`
44832
- );
44833
- return;
44834
- }
44835
- if (session.status === "stopped") {
44836
- daemonLog(
44837
- `set-status for stopped session id=${id} status=${status2} (held pending durable close; ignoring)`
44838
- );
44839
- return;
44840
- }
44841
- rebind?.(session);
44842
- if (source === "permission") session.permissionActive = true;
44843
- if (status2 === "running") session.permissionActive = false;
44844
- onStatusChange(session, status2);
44845
- }
44846
-
44847
- // src/commands/sessions/daemon/rebindClaudeSession.ts
44848
- function rebindClaudeSession(session, claudeSessionId, notify2, onStatusChange) {
44849
- if (!claudeSessionId) return;
44850
- if (session.claudeSessionId === claudeSessionId) return;
44851
- daemonLog(
44852
- `session ${session.id} claude session moved: ${session.claudeSessionId ?? "none"} -> ${claudeSessionId}`
44853
- );
44854
- session.claudeSessionId = claudeSessionId;
44855
- watchTranscript(session, notify2, onStatusChange);
44856
- }
44857
-
44858
- // src/commands/sessions/daemon/setStatusFromHook.ts
44859
- function setStatusFromHook(sessions, report, notify2, onStatusChange) {
44860
- applySetStatus(
44861
- sessions,
44862
- report.id,
44863
- report.status,
44864
- report.source,
44865
- onStatusChange,
44866
- (session) => rebindClaudeSession(
44867
- session,
44868
- report.claudeSessionId,
44869
- notify2,
44870
- onStatusChange
44871
- )
44872
- );
44873
- }
44874
-
44875
44871
  // src/commands/sessions/daemon/restartAndNotify.ts
44876
44872
  function restartAndNotify(sessions, id, clients, onStatusChange, notify2) {
44877
44873
  const result = restartManagedSession(sessions, id, clients, onStatusChange);
@@ -44913,8 +44909,7 @@ function restoreBase(id, persisted) {
44913
44909
  cwd: persisted.cwd,
44914
44910
  assistArgs: persisted.assistArgs,
44915
44911
  initialPrompt: persisted.initialPrompt,
44916
- starred: persisted.watcher ? false : persisted.starred,
44917
- watcher: persisted.watcher,
44912
+ starred: persisted.starred,
44918
44913
  divergenceEscalation: persisted.divergenceEscalation,
44919
44914
  design: persisted.design,
44920
44915
  auto: persisted.auto,
@@ -44974,12 +44969,19 @@ function underTempRoot(path106) {
44974
44969
  );
44975
44970
  }
44976
44971
 
44977
- // src/commands/sessions/daemon/dropTempRootedSessions.ts
44978
- 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) {
44979
44980
  return persisted.filter((entry) => {
44980
- if (!underTempRoot(entry.cwd)) return true;
44981
+ const reason4 = droppedReason(entry);
44982
+ if (!reason4) return true;
44981
44983
  daemonLog(
44982
- `persisted session ${describePersistedSession(entry)} lies outside any project root; dropped rather than restored`
44984
+ `persisted session ${describePersistedSession(entry)} ${reason4}; dropped rather than restored`
44983
44985
  );
44984
44986
  return false;
44985
44987
  });
@@ -45304,25 +45306,6 @@ function isUpdate(persisted) {
45304
45306
  return persisted.commandType === "assist" && persisted.assistArgs?.[0] === "update";
45305
45307
  }
45306
45308
 
45307
- // src/commands/sessions/daemon/migrateClaudeWatcher.ts
45308
- function migrateClaudeWatcher(id, persisted) {
45309
- if (!persisted.watcher || persisted.commandType !== "claude")
45310
- return persisted;
45311
- daemonLog(
45312
- `migrating claude /watch watcher session ${id} to assist ${WATCHER_ARGS.join(" ")}`
45313
- );
45314
- return {
45315
- ...persisted,
45316
- name: `assist ${WATCHER_ARGS.join(" ")}`,
45317
- commandType: "assist",
45318
- assistArgs: WATCHER_ARGS,
45319
- initialPrompt: void 0,
45320
- claudeSessionId: void 0,
45321
- harness: void 0,
45322
- harnessSessionId: void 0
45323
- };
45324
- }
45325
-
45326
45309
  // src/commands/sessions/daemon/needsWrapperRelaunch.ts
45327
45310
  var LAUNCH_COMMANDS = ["draft", "feat", "bug", "refine"];
45328
45311
  function needsWrapperRelaunch(persisted) {
@@ -45335,36 +45318,11 @@ function isOnceLaunch(persisted) {
45335
45318
  return persisted.commandType === "assist" && !!persisted.assistArgs && LAUNCH_COMMANDS.includes(persisted.assistArgs[0]) && persisted.assistArgs.includes("--once");
45336
45319
  }
45337
45320
 
45338
- // src/commands/sessions/daemon/relaunchConsoleWatcher.ts
45339
- function isConsoleWatcher(persisted) {
45340
- return persisted.watcher === true && persisted.commandType === "assist" && !!persisted.assistArgs;
45341
- }
45342
- function relaunchConsoleWatcher(id, persisted, base) {
45343
- const command = ["assist", ...persisted.assistArgs];
45344
- daemonLog(
45345
- `relaunching watcher session ${id} running ${command.join(" ")} in ${persisted.cwd ?? "(no cwd)"}`
45346
- );
45347
- const session = runningSession(
45348
- base,
45349
- persisted,
45350
- spawnPty(command, persisted.cwd, id)
45351
- );
45352
- if (persisted.cwd)
45353
- session.scrollback = seedWatcherScrollback(
45354
- persisted.cwd,
45355
- "daemon restarted; watcher relaunched"
45356
- );
45357
- return session;
45358
- }
45359
-
45360
45321
  // src/commands/sessions/daemon/restoreSession.ts
45361
- function restoreSession(id, stored) {
45362
- const persisted = migrateClaudeWatcher(id, stored);
45322
+ function restoreSession(id, persisted) {
45363
45323
  const base = restoreBase(id, persisted);
45364
45324
  if (persisted.status === "stopped") return stoppedSession(base, persisted);
45365
45325
  if (isUpdate(persisted)) return updatedSession(id, persisted);
45366
- if (isConsoleWatcher(persisted))
45367
- return relaunchConsoleWatcher(id, persisted, base);
45368
45326
  const status2 = deriveRestoreStatus(persisted);
45369
45327
  if (needsWrapperRelaunch(persisted)) {
45370
45328
  const resumesWrittenConversation = hasTranscriptOnDisk(persisted);
@@ -45418,7 +45376,7 @@ function logRestoreError(persisted, error) {
45418
45376
 
45419
45377
  // src/commands/sessions/daemon/restoreAll.ts
45420
45378
  function restoreAll(spawner, sessions) {
45421
- const persisted = dropTempRootedSessions(loadPersistedSessions());
45379
+ const persisted = dropUnrestorableSessions(loadPersistedSessions());
45422
45380
  const cap2 = sessionLimits.maxRestore();
45423
45381
  const restoredIds = /* @__PURE__ */ new Map();
45424
45382
  const names = persisted.slice(0, cap2).map((entry) => {
@@ -45674,10 +45632,10 @@ function restoreAllSessions(spawner, sessions, notify2) {
45674
45632
 
45675
45633
  // src/commands/sessions/daemon/retrySession.ts
45676
45634
  function retrySession(session, clients, onStatusChange) {
45677
- const spawn11 = respawnThunk(session);
45678
- if (!spawn11) return false;
45635
+ const spawn12 = respawnThunk(session);
45636
+ if (!spawn12) return false;
45679
45637
  const respawn = () => {
45680
- respawnSession(session, spawn11, "running", clients, onStatusChange);
45638
+ respawnSession(session, spawn12, "running", clients, onStatusChange);
45681
45639
  daemonLog(`session ${session.id} retried: ${session.name}`);
45682
45640
  };
45683
45641
  const pty2 = session.pty;
@@ -46706,6 +46664,30 @@ function addAgentToStream(ctx, targetId, request) {
46706
46664
  };
46707
46665
  }
46708
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
+
46709
46691
  // src/commands/sessions/daemon/worktree/spawnInTree.ts
46710
46692
  function spawnInTree(ctx, { prompt, cwd, design, auto, harness, inPlace }, context) {
46711
46693
  return allocateAndBind(
@@ -46724,7 +46706,7 @@ function spawnInTree(ctx, { prompt, cwd, design, auto, harness, inPlace }, conte
46724
46706
  );
46725
46707
  }
46726
46708
  function spawnAssistInTree(ctx, assistArgs, cwd, meta, context) {
46727
- const id = allocateAndBind(
46709
+ return allocateAndBind(
46728
46710
  ctx,
46729
46711
  cwd,
46730
46712
  (sid, resolvedCwd, holdUntilSeeded) => createAssistSession(sid, assistArgs, resolvedCwd, meta, holdUntilSeeded),
@@ -46737,8 +46719,6 @@ function spawnAssistInTree(ctx, assistArgs, cwd, meta, context) {
46737
46719
  },
46738
46720
  context
46739
46721
  );
46740
- if (isBacklogRunArgs(assistArgs)) ensureWatcher(ctx, cwd);
46741
- return id;
46742
46722
  }
46743
46723
 
46744
46724
  // src/commands/sessions/daemon/worktree/resumeInTree.ts
@@ -46965,13 +46945,11 @@ var SessionManager = class {
46965
46945
  );
46966
46946
  }
46967
46947
  dismissSession = (id) => {
46968
- dismissAndResumeWatcher(
46969
- this.sessions,
46970
- id,
46971
- this.notify,
46972
- (watcherId) => this.restart(watcherId)
46973
- );
46948
+ dismissSessionGated(this.sessions, id, this.notify);
46974
46949
  };
46950
+ startAutoUpdate() {
46951
+ startAutoUpdate(() => this.treeCtx());
46952
+ }
46975
46953
  discardSession = (id) => {
46976
46954
  dismissSessionGated(this.sessions, id, this.notify, true);
46977
46955
  };
@@ -47049,10 +47027,10 @@ var handleHelloRequest = (client, m, d) => {
47049
47027
  };
47050
47028
 
47051
47029
  // src/commands/sessions/daemon/creator.ts
47052
- function creator(isNew, spawn11) {
47030
+ function creator(isNew, spawn12) {
47053
47031
  return (client, m, d) => {
47054
47032
  if (m.links.route(client, d)) return;
47055
- const result = spawn11(m, d);
47033
+ const result = spawn12(m, d);
47056
47034
  if (typeof result !== "string") {
47057
47035
  sendTo(client, { type: "error", message: result.error });
47058
47036
  return;
@@ -47718,6 +47696,7 @@ function onListening(manager, checkAutoExit) {
47718
47696
  );
47719
47697
  daemonLog(`listening on ${daemonPaths.socket}`);
47720
47698
  manager.links.reload();
47699
+ manager.startAutoUpdate();
47721
47700
  checkAutoExit(manager.isIdle());
47722
47701
  }
47723
47702
  function cleanupOwnedFiles() {
@@ -47820,6 +47799,7 @@ function registerDaemon(program2) {
47820
47799
  cmd.command("drain").description(
47821
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"
47822
47801
  ).option("--yes", "Close live sessions without confirming").action((options2) => drainDaemon(options2));
47802
+ configHelp(cmd, autoUpdateConfigHelp);
47823
47803
  }
47824
47804
 
47825
47805
  // src/commands/sessions/closeSession.ts
@@ -48080,14 +48060,14 @@ async function linkStatus(name, query = queryNodes) {
48080
48060
  return (await query())?.links.find((l) => l.name === name);
48081
48061
  }
48082
48062
  async function awaitLinkReturn(name, options2, deps2 = {}) {
48083
- const { query = queryNodes, sleep: sleep2 = wait, now = Date.now } = deps2;
48063
+ const { query = queryNodes, sleep: sleep3 = wait, now = Date.now } = deps2;
48084
48064
  const deadline = now() + options2.timeoutMs;
48085
48065
  let sawDown = options2.sawDown;
48086
48066
  while (now() < deadline) {
48087
48067
  const link3 = await linkStatus(name, query);
48088
48068
  if (link3?.state !== "connected") sawDown = true;
48089
48069
  else if (sawDown) return link3;
48090
- await sleep2(POLL_MS4);
48070
+ await sleep3(POLL_MS4);
48091
48071
  }
48092
48072
  return void 0;
48093
48073
  }