@staff0rd/assist 0.757.1 → 0.759.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.757.1",
9
+ version: "0.759.0",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -13058,7 +13058,7 @@ var sessionsConfigHelp = [
13058
13058
  {
13059
13059
  key: "worktree.watcher",
13060
13060
  setter: "assist config set worktree.watcher true -g --repo",
13061
- note: "a backlog run also starts one starred claude session in the clone running /watch, so the built version stays current while the run works in its worktree; requires worktree.enabled, a /watch command in the repo and an auto-build run entry (default off)"
13061
+ 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)"
13062
13062
  },
13063
13063
  {
13064
13064
  key: "worktree.trunk",
@@ -39103,6 +39103,49 @@ function registerVerify(program2) {
39103
39103
  configHelp(verifyCommand, verifyConfigHelp);
39104
39104
  }
39105
39105
 
39106
+ // src/commands/watch/decideLap.ts
39107
+ var RELAUNCH_CODES = /* @__PURE__ */ new Set([0, 2, 4]);
39108
+ function decideLap(end, interrupted) {
39109
+ if (interrupted) return { kind: "exit", code: 130 };
39110
+ if (end.code === null) return { kind: "relaunch" };
39111
+ if (RELAUNCH_CODES.has(end.code)) return { kind: "relaunch" };
39112
+ return { kind: "exit", code: end.code };
39113
+ }
39114
+
39115
+ // src/commands/watch/runWatchLap.ts
39116
+ import { spawn as spawn9 } from "child_process";
39117
+ var WATCH_LAP_ARGS = ["watch", "wait", "--pull", "--build"];
39118
+ function runWatchLap() {
39119
+ return new Promise((resolve26, reject) => {
39120
+ const assistFromPath = spawn9("assist", WATCH_LAP_ARGS, {
39121
+ stdio: "inherit",
39122
+ shell: process.platform === "win32"
39123
+ });
39124
+ assistFromPath.on("close", (code, signal) => resolve26({ code, signal }));
39125
+ assistFromPath.on("error", reject);
39126
+ });
39127
+ }
39128
+
39129
+ // src/commands/watch/watchLoop.ts
39130
+ async function watchLoop() {
39131
+ let interrupted = false;
39132
+ const recordInterruptAndLetLapFinish = () => {
39133
+ interrupted = true;
39134
+ };
39135
+ process.on("SIGINT", recordInterruptAndLetLapFinish);
39136
+ for (let lap = 1; ; lap++) {
39137
+ console.log(`lap ${lap}: assist ${WATCH_LAP_ARGS.join(" ")}`);
39138
+ const end = await runWatchLap();
39139
+ const decision = decideLap(end, interrupted);
39140
+ const how = end.signal ? `killed by ${end.signal}` : `exited ${end.code}`;
39141
+ if (decision.kind === "exit") {
39142
+ console.error(`lap ${lap} ${how} \u2014 stopping`);
39143
+ return process.exit(decision.code);
39144
+ }
39145
+ console.log(`lap ${lap} ${how} \u2014 relaunching`);
39146
+ }
39147
+ }
39148
+
39106
39149
  // src/commands/watch/resolveUpstream.ts
39107
39150
  import { execFileSync as execFileSync21 } from "child_process";
39108
39151
  function runGit3(args, cwd) {
@@ -39717,6 +39760,9 @@ function registerWatch(program2) {
39717
39760
  ).action(
39718
39761
  (options2) => watchWait(options2)
39719
39762
  );
39763
+ watchCommand.command("loop").description(
39764
+ "Run assist watch wait --pull --build as a fresh child per lap, relaunching after exit 0, 2 or 4 or a signal kill, and exit with the child's code on 1, 3 or 130"
39765
+ ).action(() => watchLoop());
39720
39766
  watchCommand.command("report").description(
39721
39767
  "Print the built version, the last 10 commits as a markdown table, the restarts the new commits make necessary, and whether ~/.claude needs a sync"
39722
39768
  ).option(
@@ -41296,6 +41342,7 @@ function dismissSession(sessions, id) {
41296
41342
  s.activityWatcher?.close();
41297
41343
  s.transcriptWatcher?.close();
41298
41344
  s.gitWatcher?.close();
41345
+ s.closeGrace?.cancel();
41299
41346
  removeActivity(s.id);
41300
41347
  if (s.activity?.itemId != null) releaseLock(s.activity.itemId);
41301
41348
  sessions.delete(id);
@@ -41368,6 +41415,28 @@ function processCwd(link3) {
41368
41415
  }
41369
41416
  }
41370
41417
 
41418
+ // src/commands/sessions/daemon/worktree/awaitTreeVacated.ts
41419
+ var POLL_MS = 500;
41420
+ var GRACE_MS = 5e3;
41421
+ function awaitTreeVacated(session, treePath) {
41422
+ return new Promise((resolve26) => {
41423
+ let ticks = GRACE_MS / POLL_MS;
41424
+ const settle = (outcome) => {
41425
+ clearTimeout(timer);
41426
+ session.closeGrace = void 0;
41427
+ resolve26(outcome);
41428
+ };
41429
+ const tick = () => {
41430
+ if (liveProcessesInTree(treePath).length === 0) settle("vacated");
41431
+ else if (--ticks <= 0) settle("occupied");
41432
+ else timer = setTimeout(tick, POLL_MS);
41433
+ };
41434
+ let timer = setTimeout(tick, POLL_MS);
41435
+ session.closeGrace?.cancel();
41436
+ session.closeGrace = { cancel: () => settle("cancelled") };
41437
+ });
41438
+ }
41439
+
41371
41440
  // src/commands/sessions/daemon/worktree/liveOccupantReason.ts
41372
41441
  function liveOccupantReason(treePath) {
41373
41442
  stopInstall(treePath);
@@ -41379,20 +41448,21 @@ function liveOccupantReason(treePath) {
41379
41448
  // src/commands/sessions/daemon/worktree/closeBlockReason.ts
41380
41449
  async function closeBlockReason(sessionId, tree) {
41381
41450
  const durability = await checkDurability(tree.path);
41382
- if (!durability.durable) return durability.reason;
41451
+ if (!durability.durable) return { reason: durability.reason, live: false };
41383
41452
  if (durability.gone) {
41384
41453
  daemonLog(
41385
41454
  `session ${sessionId} closing: worktree ${tree.path} is gone from disk \u2014 released with nothing to land, not landed work`
41386
41455
  );
41387
41456
  return void 0;
41388
41457
  }
41389
- return tree.removable ? liveOccupantReason(tree.path) : void 0;
41458
+ const occupied = tree.removable ? liveOccupantReason(tree.path) : void 0;
41459
+ return occupied ? { reason: occupied, live: true } : void 0;
41390
41460
  }
41391
41461
 
41392
41462
  // src/commands/sessions/daemon/worktree/watchGitState.ts
41393
41463
  import { existsSync as existsSync87, watch } from "fs";
41394
41464
  var DEBOUNCE_MS = 500;
41395
- var POLL_MS = 3e4;
41465
+ var POLL_MS2 = 3e4;
41396
41466
  function watchGitState(cwd, onChange) {
41397
41467
  const common = gitCommonDir(cwd);
41398
41468
  if (!common || !existsSync87(common)) return void 0;
@@ -41423,22 +41493,59 @@ function watchGitDir(common, onChange) {
41423
41493
  }
41424
41494
  }
41425
41495
  function pollGitState(cwd, onChange) {
41426
- const timer = setInterval(onChange, POLL_MS);
41496
+ const timer = setInterval(onChange, POLL_MS2);
41427
41497
  timer.unref?.();
41428
- daemonLog(`git-state polled every ${POLL_MS / 1e3}s for ${cwd}`);
41498
+ daemonLog(`git-state polled every ${POLL_MS2 / 1e3}s for ${cwd}`);
41429
41499
  return { close: () => clearInterval(timer) };
41430
41500
  }
41431
41501
 
41502
+ // src/commands/sessions/daemon/worktree/holdStopped.ts
41503
+ function holdStopped(session, tree, reason4, recheck, notify2) {
41504
+ if (session.undurable?.reason !== reason4)
41505
+ daemonLog(
41506
+ `session ${session.id} stopped; ${tree.removable ? "reap" : "close"} blocked in ${tree.path}: ${reason4}`
41507
+ );
41508
+ session.undurable = { reason: reason4, removesTree: tree.removable };
41509
+ session.closing = void 0;
41510
+ setStatus2(session, "stopped");
41511
+ if (!session.gitWatcher) {
41512
+ let rechecking = false;
41513
+ session.gitWatcher = watchGitState(tree.path, () => {
41514
+ if (session.status !== "stopped" || rechecking) return;
41515
+ rechecking = true;
41516
+ void recheck().finally(() => {
41517
+ rechecking = false;
41518
+ });
41519
+ });
41520
+ }
41521
+ notify2();
41522
+ }
41523
+
41432
41524
  // src/commands/sessions/daemon/worktree/resolveCloseDurability.ts
41433
- async function resolveCloseDurability(session, finalize, notify2) {
41525
+ async function resolveCloseDurability(session, finalize, notify2, graceSpent = false) {
41434
41526
  const tree = treeUnderClose(session);
41435
41527
  if (!tree) {
41436
41528
  finalize();
41437
41529
  return;
41438
41530
  }
41439
41531
  const blocked = await closeBlockReason(session.id, tree);
41532
+ if (blocked?.live && session.closing && !graceSpent) {
41533
+ daemonLog(
41534
+ `session ${session.id} closing: waiting for live processes to leave ${tree.path} (${blocked.reason})`
41535
+ );
41536
+ const outcome = await awaitTreeVacated(session, tree.path);
41537
+ daemonLog(`session ${session.id} closing: live-process wait ${outcome}`);
41538
+ if (outcome === "cancelled") return;
41539
+ return resolveCloseDurability(session, finalize, notify2, true);
41540
+ }
41440
41541
  if (blocked) {
41441
- holdStopped(session, tree, blocked, finalize, notify2);
41542
+ holdStopped(
41543
+ session,
41544
+ tree,
41545
+ blocked.reason,
41546
+ () => resolveCloseDurability(session, finalize, notify2),
41547
+ notify2
41548
+ );
41442
41549
  return;
41443
41550
  }
41444
41551
  if (tree.removable) await reapWorktree(tree.path);
@@ -41450,26 +41557,6 @@ async function resolveCloseDurability(session, finalize, notify2) {
41450
41557
  session.gitWatcher = void 0;
41451
41558
  finalize();
41452
41559
  }
41453
- function holdStopped(session, tree, reason4, finalize, notify2) {
41454
- if (session.undurable?.reason !== reason4)
41455
- daemonLog(
41456
- `session ${session.id} stopped; ${tree.removable ? "reap" : "close"} blocked in ${tree.path}: ${reason4}`
41457
- );
41458
- session.undurable = { reason: reason4, removesTree: tree.removable };
41459
- session.closing = void 0;
41460
- setStatus2(session, "stopped");
41461
- if (!session.gitWatcher) {
41462
- let rechecking = false;
41463
- session.gitWatcher = watchGitState(tree.path, () => {
41464
- if (session.status !== "stopped" || rechecking) return;
41465
- rechecking = true;
41466
- void resolveCloseDurability(session, finalize, notify2).finally(() => {
41467
- rechecking = false;
41468
- });
41469
- });
41470
- }
41471
- notify2();
41472
- }
41473
41560
 
41474
41561
  // src/commands/sessions/daemon/dismissSessionGated.ts
41475
41562
  function dismissSessionGated(sessions, id, notify2, discard = false) {
@@ -41483,6 +41570,7 @@ function dismissSessionGated(sessions, id, notify2, discard = false) {
41483
41570
  if (dismissSession(sessions, id)) notify2();
41484
41571
  };
41485
41572
  const settle = discard ? () => void discardTree(s, removeCard, notify2) : () => void resolveCloseDurability(s, removeCard, notify2);
41573
+ s.closeGrace?.cancel();
41486
41574
  s.closing = true;
41487
41575
  daemonLog(
41488
41576
  `session ${id} closing: ${discard ? "discarding" : "checking durability of"} ${tree.path}`
@@ -42191,7 +42279,7 @@ async function touchedSince(file, sinceMs) {
42191
42279
  }
42192
42280
 
42193
42281
  // src/commands/sessions/daemon/bindCodexSession.ts
42194
- var POLL_MS2 = 1e3;
42282
+ var POLL_MS3 = 1e3;
42195
42283
  var MAX_ATTEMPTS = 120;
42196
42284
  var START_SLACK_MS = 5e3;
42197
42285
  function bindCodexSession(session, notify2) {
@@ -42199,7 +42287,7 @@ function bindCodexSession(session, notify2) {
42199
42287
  if (session.harness !== "codex" || !cwd || session.harnessSessionId) return;
42200
42288
  const since = session.startedAt - START_SLACK_MS;
42201
42289
  let attemptsLeft = MAX_ATTEMPTS;
42202
- const schedule = () => setTimeout(() => void poll(), POLL_MS2).unref();
42290
+ const schedule = () => setTimeout(() => void poll(), POLL_MS3).unref();
42203
42291
  const poll = async () => {
42204
42292
  if (session.harnessSessionId) return;
42205
42293
  const sessionId = await resolveCodexSessionId(cwd, since);
@@ -43171,6 +43259,7 @@ function respawnSession(session, respawn, status2, clients, onStatusChange) {
43171
43259
  session.gitWatcher = void 0;
43172
43260
  session.undurable = void 0;
43173
43261
  session.pendingDismiss = void 0;
43262
+ session.closeGrace?.cancel();
43174
43263
  session.closing = void 0;
43175
43264
  session.scrollback = "";
43176
43265
  session.startedAt = Date.now();
@@ -43796,11 +43885,25 @@ function isOnceLaunch(persisted) {
43796
43885
  return persisted.commandType === "assist" && !!persisted.assistArgs && LAUNCH_COMMANDS.includes(persisted.assistArgs[0]) && persisted.assistArgs.includes("--once");
43797
43886
  }
43798
43887
 
43888
+ // src/commands/sessions/daemon/relaunchConsoleWatcher.ts
43889
+ function isConsoleWatcher(persisted) {
43890
+ return persisted.watcher === true && persisted.commandType === "assist" && !!persisted.assistArgs;
43891
+ }
43892
+ function relaunchConsoleWatcher(id, persisted, base) {
43893
+ const command = ["assist", ...persisted.assistArgs];
43894
+ daemonLog(
43895
+ `relaunching watcher session ${id} running ${command.join(" ")} in ${persisted.cwd ?? "(no cwd)"}`
43896
+ );
43897
+ return runningSession(base, persisted, spawnPty(command, persisted.cwd, id));
43898
+ }
43899
+
43799
43900
  // src/commands/sessions/daemon/restoreSession.ts
43800
43901
  function restoreSession(id, persisted) {
43801
43902
  const base = restoreBase(id, persisted);
43802
43903
  if (persisted.status === "stopped") return stoppedSession(base, persisted);
43803
43904
  if (isUpdate(persisted)) return updatedSession(id, persisted);
43905
+ if (isConsoleWatcher(persisted))
43906
+ return relaunchConsoleWatcher(id, persisted, base);
43804
43907
  const status2 = deriveRestoreStatus(persisted);
43805
43908
  if (needsWrapperRelaunch(persisted)) {
43806
43909
  const resumesWrittenConversation = hasTranscriptOnDisk(persisted);
@@ -44144,10 +44247,10 @@ function restoreAllSessions(spawner, sessions, notify2) {
44144
44247
 
44145
44248
  // src/commands/sessions/daemon/retrySession.ts
44146
44249
  function retrySession(session, clients, onStatusChange) {
44147
- const spawn10 = respawnThunk(session);
44148
- if (!spawn10) return false;
44250
+ const spawn11 = respawnThunk(session);
44251
+ if (!spawn11) return false;
44149
44252
  const respawn = () => {
44150
- respawnSession(session, spawn10, "running", clients, onStatusChange);
44253
+ respawnSession(session, spawn11, "running", clients, onStatusChange);
44151
44254
  daemonLog(`session ${session.id} retried: ${session.name}`);
44152
44255
  };
44153
44256
  const pty2 = session.pty;
@@ -44291,26 +44394,35 @@ function startReusedRunPty(session, assistArgs, itemId2, hold, clients, onStatus
44291
44394
  return true;
44292
44395
  }
44293
44396
 
44397
+ // src/commands/sessions/daemon/createAssistSession.ts
44398
+ function createAssistSession(id, assistArgs, cwd, meta, holdPty) {
44399
+ const startedAt = Date.now();
44400
+ return {
44401
+ id,
44402
+ name: `assist ${assistArgs.join(" ")}`,
44403
+ title: meta?.title,
44404
+ subtitle: meta?.subtitle,
44405
+ commandType: "assist",
44406
+ status: "running",
44407
+ startedAt,
44408
+ runningMs: 0,
44409
+ runningSince: startedAt,
44410
+ waitingSince: null,
44411
+ ...startOrHoldPty(
44412
+ () => spawnPty(["assist", ...assistArgs], cwd, id),
44413
+ holdPty
44414
+ ),
44415
+ scrollback: "",
44416
+ assistArgs,
44417
+ cwd
44418
+ };
44419
+ }
44420
+
44294
44421
  // src/commands/sessions/daemon/createWatcherSession.ts
44295
- import { randomUUID as randomUUID24 } from "crypto";
44296
- var WATCH_PROMPT = "/watch";
44422
+ var WATCHER_ARGS = ["watch", "loop"];
44297
44423
  function createWatcherSession(id, cwd) {
44298
- const claudeSessionId = randomUUID24();
44299
44424
  return {
44300
- ...sessionBase(id, "running"),
44301
- name: `Session ${id}`,
44302
- commandType: "claude",
44303
- pty: spawnClaude2({
44304
- prompt: WATCH_PROMPT,
44305
- cwd,
44306
- sessionId: id,
44307
- claudeSessionId,
44308
- auto: true
44309
- }),
44310
- cwd,
44311
- claudeSessionId,
44312
- initialPrompt: WATCH_PROMPT,
44313
- auto: true,
44425
+ ...createAssistSession(id, WATCHER_ARGS, cwd),
44314
44426
  watcher: true
44315
44427
  };
44316
44428
  }
@@ -44378,7 +44490,7 @@ function ensureWatcher(ctx, cwd) {
44378
44490
  { inPlace: true }
44379
44491
  );
44380
44492
  daemonLog(
44381
- `spawned watcher session ${id} running /watch in the clone ${clone}`
44493
+ `spawned watcher session ${id} running assist ${WATCHER_ARGS.join(" ")} in the clone ${clone}`
44382
44494
  );
44383
44495
  return id;
44384
44496
  }
@@ -45448,30 +45560,6 @@ function addAgentToStream(ctx, targetId, request) {
45448
45560
  };
45449
45561
  }
45450
45562
 
45451
- // src/commands/sessions/daemon/createAssistSession.ts
45452
- function createAssistSession(id, assistArgs, cwd, meta, holdPty) {
45453
- const startedAt = Date.now();
45454
- return {
45455
- id,
45456
- name: `assist ${assistArgs.join(" ")}`,
45457
- title: meta?.title,
45458
- subtitle: meta?.subtitle,
45459
- commandType: "assist",
45460
- status: "running",
45461
- startedAt,
45462
- runningMs: 0,
45463
- runningSince: startedAt,
45464
- waitingSince: null,
45465
- ...startOrHoldPty(
45466
- () => spawnPty(["assist", ...assistArgs], cwd, id),
45467
- holdPty
45468
- ),
45469
- scrollback: "",
45470
- assistArgs,
45471
- cwd
45472
- };
45473
- }
45474
-
45475
45563
  // src/commands/sessions/daemon/worktree/spawnInTree.ts
45476
45564
  function spawnInTree(ctx, { prompt, cwd, design, auto, harness, inPlace }, context) {
45477
45565
  return allocateAndBind(
@@ -45875,10 +45963,10 @@ var handleHelloRequest = (client, m, d) => {
45875
45963
  };
45876
45964
 
45877
45965
  // src/commands/sessions/daemon/creator.ts
45878
- function creator(isNew, spawn10) {
45966
+ function creator(isNew, spawn11) {
45879
45967
  return (client, m, d) => {
45880
45968
  if (m.links.route(client, d)) return;
45881
- const result = spawn10(m, d);
45969
+ const result = spawn11(m, d);
45882
45970
  if (typeof result !== "string") {
45883
45971
  sendTo(client, { type: "error", message: result.error });
45884
45972
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staff0rd/assist",
3
- "version": "0.757.1",
3
+ "version": "0.759.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {