@phnx-labs/agents-cli 1.20.74 → 1.20.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/CHANGELOG.md +893 -0
  2. package/README.md +36 -10
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.js +21 -3
  5. package/dist/commands/attach.d.ts +10 -0
  6. package/dist/commands/attach.js +41 -0
  7. package/dist/commands/computer.js +3 -3
  8. package/dist/commands/detach-core.d.ts +46 -0
  9. package/dist/commands/detach-core.js +61 -0
  10. package/dist/commands/detach.d.ts +5 -0
  11. package/dist/commands/detach.js +171 -0
  12. package/dist/commands/doctor.js +18 -0
  13. package/dist/commands/exec.js +31 -3
  14. package/dist/commands/feed.d.ts +7 -1
  15. package/dist/commands/feed.js +47 -4
  16. package/dist/commands/go.d.ts +9 -2
  17. package/dist/commands/go.js +16 -6
  18. package/dist/commands/harness.d.ts +14 -0
  19. package/dist/commands/harness.js +145 -0
  20. package/dist/commands/import.js +38 -8
  21. package/dist/commands/inspect.js +5 -2
  22. package/dist/commands/profiles.d.ts +20 -0
  23. package/dist/commands/profiles.js +68 -21
  24. package/dist/commands/repo.js +14 -0
  25. package/dist/commands/routines.js +15 -2
  26. package/dist/commands/secrets.js +44 -21
  27. package/dist/commands/sessions-browser.js +1 -1
  28. package/dist/commands/sessions-export.js +19 -5
  29. package/dist/commands/sessions-migrate.d.ts +29 -0
  30. package/dist/commands/sessions-migrate.js +596 -0
  31. package/dist/commands/sessions-picker.js +16 -1
  32. package/dist/commands/sessions.d.ts +36 -13
  33. package/dist/commands/sessions.js +210 -40
  34. package/dist/commands/setup-computer.js +2 -2
  35. package/dist/commands/ssh.js +71 -35
  36. package/dist/commands/status.js +10 -2
  37. package/dist/commands/versions.js +15 -6
  38. package/dist/commands/view.d.ts +5 -0
  39. package/dist/commands/view.js +24 -4
  40. package/dist/commands/watchdog.d.ts +7 -2
  41. package/dist/commands/watchdog.js +73 -57
  42. package/dist/index.js +58 -7
  43. package/dist/lib/activity.d.ts +14 -4
  44. package/dist/lib/activity.js +17 -5
  45. package/dist/lib/actor.d.ts +50 -0
  46. package/dist/lib/actor.js +166 -0
  47. package/dist/lib/agent-spec/provider.js +2 -1
  48. package/dist/lib/agent-spec/resolve.js +19 -5
  49. package/dist/lib/agent-spec/types.d.ts +9 -1
  50. package/dist/lib/agents.d.ts +21 -0
  51. package/dist/lib/agents.js +70 -6
  52. package/dist/lib/cloud/codex.d.ts +2 -0
  53. package/dist/lib/cloud/codex.js +14 -3
  54. package/dist/lib/cloud/session-index.d.ts +32 -0
  55. package/dist/lib/cloud/session-index.js +58 -0
  56. package/dist/lib/cloud/store.d.ts +7 -0
  57. package/dist/lib/cloud/store.js +25 -0
  58. package/dist/lib/config-transfer.js +4 -0
  59. package/dist/lib/daemon.js +18 -2
  60. package/dist/lib/devices/connect.d.ts +15 -1
  61. package/dist/lib/devices/connect.js +15 -1
  62. package/dist/lib/devices/fleet.d.ts +21 -0
  63. package/dist/lib/devices/fleet.js +26 -0
  64. package/dist/lib/devices/health-report.d.ts +38 -0
  65. package/dist/lib/devices/health-report.js +214 -0
  66. package/dist/lib/devices/health.js +4 -1
  67. package/dist/lib/devices/reachability.d.ts +31 -0
  68. package/dist/lib/devices/reachability.js +40 -0
  69. package/dist/lib/devices/registry.d.ts +33 -0
  70. package/dist/lib/devices/registry.js +37 -0
  71. package/dist/lib/devices/resolve-target.d.ts +15 -27
  72. package/dist/lib/devices/resolve-target.js +63 -102
  73. package/dist/lib/devices/sync.d.ts +18 -0
  74. package/dist/lib/devices/sync.js +23 -1
  75. package/dist/lib/devices/tailscale.d.ts +3 -0
  76. package/dist/lib/devices/tailscale.js +1 -0
  77. package/dist/lib/events.d.ts +1 -1
  78. package/dist/lib/exec.d.ts +66 -0
  79. package/dist/lib/exec.js +195 -6
  80. package/dist/lib/feed-post.d.ts +63 -0
  81. package/dist/lib/feed-post.js +204 -0
  82. package/dist/lib/fleet/apply.d.ts +32 -2
  83. package/dist/lib/fleet/apply.js +97 -10
  84. package/dist/lib/fleet/types.d.ts +11 -0
  85. package/dist/lib/fs-walk.d.ts +13 -0
  86. package/dist/lib/fs-walk.js +16 -7
  87. package/dist/lib/hooks.d.ts +1 -0
  88. package/dist/lib/hooks.js +156 -0
  89. package/dist/lib/hosts/dispatch.d.ts +7 -0
  90. package/dist/lib/hosts/dispatch.js +2 -0
  91. package/dist/lib/hosts/passthrough.js +2 -0
  92. package/dist/lib/hosts/reconnect.d.ts +78 -0
  93. package/dist/lib/hosts/reconnect.js +127 -0
  94. package/dist/lib/hosts/registry.d.ts +75 -14
  95. package/dist/lib/hosts/registry.js +205 -30
  96. package/dist/lib/hosts/remote-cmd.js +1 -0
  97. package/dist/lib/hosts/run-target.d.ts +13 -5
  98. package/dist/lib/hosts/run-target.js +26 -10
  99. package/dist/lib/hosts/session-index.d.ts +18 -1
  100. package/dist/lib/hosts/session-index.js +37 -2
  101. package/dist/lib/hosts/session-marker.d.ts +33 -0
  102. package/dist/lib/hosts/session-marker.js +51 -0
  103. package/dist/lib/hosts/tasks.d.ts +8 -4
  104. package/dist/lib/import.d.ts +2 -0
  105. package/dist/lib/import.js +35 -2
  106. package/dist/lib/menubar/install-menubar.d.ts +20 -0
  107. package/dist/lib/menubar/install-menubar.js +45 -1
  108. package/dist/lib/migrate.d.ts +16 -0
  109. package/dist/lib/migrate.js +36 -0
  110. package/dist/lib/models.d.ts +27 -0
  111. package/dist/lib/models.js +54 -1
  112. package/dist/lib/overdue.d.ts +7 -2
  113. package/dist/lib/overdue.js +18 -11
  114. package/dist/lib/picker.js +4 -1
  115. package/dist/lib/platform/process.d.ts +17 -0
  116. package/dist/lib/platform/process.js +70 -0
  117. package/dist/lib/profiles-presets.js +9 -7
  118. package/dist/lib/profiles.d.ts +31 -0
  119. package/dist/lib/profiles.js +70 -0
  120. package/dist/lib/pty-server.d.ts +2 -10
  121. package/dist/lib/pty-server.js +4 -38
  122. package/dist/lib/registry.d.ts +1 -1
  123. package/dist/lib/registry.js +48 -8
  124. package/dist/lib/rotate.d.ts +18 -0
  125. package/dist/lib/rotate.js +28 -0
  126. package/dist/lib/routines.d.ts +26 -0
  127. package/dist/lib/routines.js +19 -2
  128. package/dist/lib/runner.js +104 -7
  129. package/dist/lib/secrets/agent.d.ts +66 -1
  130. package/dist/lib/secrets/agent.js +154 -90
  131. package/dist/lib/secrets/bundles.d.ts +34 -0
  132. package/dist/lib/secrets/bundles.js +179 -19
  133. package/dist/lib/secrets/index.d.ts +15 -1
  134. package/dist/lib/secrets/index.js +21 -3
  135. package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
  136. package/dist/lib/secrets/rc-hygiene.js +154 -0
  137. package/dist/lib/secrets/remote.d.ts +7 -4
  138. package/dist/lib/secrets/remote.js +7 -4
  139. package/dist/lib/secrets/sync-commands.d.ts +21 -0
  140. package/dist/lib/secrets/sync-commands.js +21 -0
  141. package/dist/lib/self-update.d.ts +23 -2
  142. package/dist/lib/self-update.js +86 -5
  143. package/dist/lib/session/active.d.ts +114 -7
  144. package/dist/lib/session/active.js +323 -52
  145. package/dist/lib/session/cloud.js +2 -1
  146. package/dist/lib/session/db.d.ts +48 -0
  147. package/dist/lib/session/db.js +238 -14
  148. package/dist/lib/session/detached.d.ts +30 -0
  149. package/dist/lib/session/detached.js +92 -0
  150. package/dist/lib/session/discover.d.ts +465 -2
  151. package/dist/lib/session/discover.js +1648 -468
  152. package/dist/lib/session/fork.js +2 -1
  153. package/dist/lib/session/hook-sessions.d.ts +43 -0
  154. package/dist/lib/session/hook-sessions.js +135 -0
  155. package/dist/lib/session/linear.d.ts +6 -0
  156. package/dist/lib/session/linear.js +55 -0
  157. package/dist/lib/session/migrate-targets.d.ts +65 -0
  158. package/dist/lib/session/migrate-targets.js +94 -0
  159. package/dist/lib/session/migrations.d.ts +37 -0
  160. package/dist/lib/session/migrations.js +60 -0
  161. package/dist/lib/session/parse.d.ts +18 -0
  162. package/dist/lib/session/parse.js +130 -30
  163. package/dist/lib/session/pid-registry.d.ts +34 -2
  164. package/dist/lib/session/pid-registry.js +49 -2
  165. package/dist/lib/session/render.d.ts +7 -2
  166. package/dist/lib/session/render.js +22 -10
  167. package/dist/lib/session/short-id.d.ts +17 -0
  168. package/dist/lib/session/short-id.js +20 -0
  169. package/dist/lib/session/state.js +6 -2
  170. package/dist/lib/shims.d.ts +1 -1
  171. package/dist/lib/shims.js +20 -6
  172. package/dist/lib/sqlite.d.ts +3 -2
  173. package/dist/lib/sqlite.js +27 -4
  174. package/dist/lib/staleness/detectors/commands.js +1 -1
  175. package/dist/lib/staleness/detectors/workflows.js +13 -2
  176. package/dist/lib/staleness/writers/commands.js +7 -7
  177. package/dist/lib/staleness/writers/workflows.d.ts +4 -2
  178. package/dist/lib/startup/command-registry.d.ts +1 -0
  179. package/dist/lib/startup/command-registry.js +3 -0
  180. package/dist/lib/state.d.ts +1 -1
  181. package/dist/lib/state.js +1 -32
  182. package/dist/lib/teams/agents.d.ts +19 -10
  183. package/dist/lib/teams/agents.js +40 -39
  184. package/dist/lib/types.d.ts +37 -2
  185. package/dist/lib/usage.d.ts +33 -1
  186. package/dist/lib/usage.js +172 -12
  187. package/dist/lib/versions.d.ts +9 -2
  188. package/dist/lib/versions.js +37 -8
  189. package/dist/lib/watchdog/log.d.ts +43 -0
  190. package/dist/lib/watchdog/log.js +69 -0
  191. package/dist/lib/watchdog/routine.d.ts +44 -0
  192. package/dist/lib/watchdog/routine.js +69 -0
  193. package/dist/lib/watchdog/runner.d.ts +51 -7
  194. package/dist/lib/watchdog/runner.js +239 -64
  195. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  196. package/dist/lib/watchdog/watchdog.js +31 -16
  197. package/dist/lib/workflows.d.ts +16 -0
  198. package/dist/lib/workflows.js +110 -1
  199. package/package.json +1 -1
@@ -444,8 +444,15 @@ export declare function verifyInstalledBinaryLaunches(agent: AgentId, version: s
444
444
  * the exact failure this repairs. Agents with a global/native binary (grok,
445
445
  * droid) have no such tarball and are returned unchanged.
446
446
  */
447
- export declare function ensureAgentRunnable(agent: AgentId, version: string, log?: (message: string) => void): Promise<string | null>;
448
- export declare function healBrokenDefaultLaunches(log?: (m: string) => void): Promise<string[]>;
447
+ export declare function ensureAgentRunnable(agent: AgentId, version: string, log?: (message: string) => void, opts?: {
448
+ allowDefaultSwitch?: boolean;
449
+ }): Promise<string | null>;
450
+ export declare function healBrokenDefaultLaunches(log?: (m: string) => void, opts?: {
451
+ allowDefaultSwitch?: boolean;
452
+ }): Promise<{
453
+ repaired: string[];
454
+ unhealed: string[];
455
+ }>;
449
456
  /** Outcome of syncing resources to a version home, keyed by resource type. */
450
457
  export interface SyncResult {
451
458
  commands: boolean;
@@ -37,7 +37,7 @@ import { importInstallScriptBinary } from './import.js';
37
37
  import { IS_WINDOWS, composeWin32CommandLine } from './platform/index.js';
38
38
  import { listInstalledSubagents } from './subagents.js';
39
39
  import { listInstalledWorkflows } from './workflows.js';
40
- import { pruneVersionHomeHookEntriesFromSettings } from './hooks.js';
40
+ import { parseHookManifest, registerHooksToSettings, selectHookManifest, pruneVersionHomeHookEntriesFromSettings } from './hooks.js';
41
41
  import { supports, explainSkip } from './capabilities.js';
42
42
  import { discoverPlugins, marketplaceSpecForName } from './plugins.js';
43
43
  import { loadManifest, saveManifest, buildManifest as buildSyncManifest, isStale } from './staleness/index.js';
@@ -2150,7 +2150,17 @@ export async function verifyInstalledBinaryLaunches(agent, version) {
2150
2150
  * the exact failure this repairs. Agents with a global/native binary (grok,
2151
2151
  * droid) have no such tarball and are returned unchanged.
2152
2152
  */
2153
- export async function ensureAgentRunnable(agent, version, log) {
2153
+ export async function ensureAgentRunnable(agent, version, log, opts) {
2154
+ // Whether this heal may REPOINT the global default (adopt another version, or
2155
+ // install `latest` and pin it). Interactive callers (`agents run`, `agents
2156
+ // add`) allow it. The daemon's unattended 6-hourly launch-health pass sets
2157
+ // this false: a background pass that silently switches the default installs a
2158
+ // fresh version home → a fresh, empty Claude credential scope (macOS keychain
2159
+ // keyed off CLAUDE_CONFIG_DIR; Linux per-version token file), i.e. an
2160
+ // "unprovoked logout" at a time uncorrelated with anything the user did. In
2161
+ // refuse-mode we still repair the CURRENT default in place (no scope change),
2162
+ // but never repoint — we return null so the caller can alert instead.
2163
+ const allowDefaultSwitch = opts?.allowDefaultSwitch ?? true;
2154
2164
  const cfg = AGENTS[agent];
2155
2165
  if (!cfg?.npmPackage)
2156
2166
  return version;
@@ -2172,6 +2182,14 @@ export async function ensureAgentRunnable(agent, version, log) {
2172
2182
  log?.(`${cfg.name}@${version} is an isolated install and could not be repaired — leaving your default ${cfg.name} untouched.`);
2173
2183
  return null;
2174
2184
  }
2185
+ // In-place repair failed → normally adopt another installed version and repoint
2186
+ // the default. When default-switching is disallowed (unattended daemon pass),
2187
+ // refuse: repointing would swap the live credential scope out from under the
2188
+ // user with no signal. Surface it so the caller can notify.
2189
+ if (!allowDefaultSwitch) {
2190
+ log?.(`${cfg.name}@${version} won't launch and could not be repaired in place — NOT repointing your default ${cfg.name} unattended. Run \`agents use ${agent} <version>\` or \`agents add ${agent}@latest\` to choose one.`);
2191
+ return null;
2192
+ }
2175
2193
  // In-place repair failed → adopt another installed version that launches.
2176
2194
  // Isolated copies are excluded: they are deliberately not promotable.
2177
2195
  const others = listInstalledVersions(agent)
@@ -2221,14 +2239,18 @@ export async function ensureAgentRunnable(agent, version, log) {
2221
2239
  * `agents run` hits a raw ENOENT — the run-time heal (ensureAgentRunnable) only
2222
2240
  * fires once a run is already starting; this catches it in the background.
2223
2241
  *
2224
- * Returns a label (`agent@broken→healed`) for each version actually repaired, so
2225
- * the daemon can log/notify. A version that already launches costs one cheap
2226
- * `--version` probe and is left untouched.
2242
+ * Returns `{ repaired, unhealed }`: `repaired` is a label (`agent@broken→healed`)
2243
+ * for each version actually fixed; `unhealed` is `agent@version` for each broken
2244
+ * default that could not be fixed (including, under `allowDefaultSwitch:false`,
2245
+ * one that was deliberately not repointed), so the daemon can log/notify. A
2246
+ * version that already launches costs one cheap `--version` probe and is left
2247
+ * untouched.
2227
2248
  */
2228
2249
  const failedRepairAt = new Map();
2229
2250
  const REPAIR_COOLDOWN_MS = 24 * 60 * 60_000;
2230
- export async function healBrokenDefaultLaunches(log) {
2251
+ export async function healBrokenDefaultLaunches(log, opts) {
2231
2252
  const repaired = [];
2253
+ const unhealed = [];
2232
2254
  for (const agent of Object.keys(AGENTS)) {
2233
2255
  if (!AGENTS[agent].npmPackage)
2234
2256
  continue; // native/global agents have no gutted-tarball failure mode
@@ -2248,16 +2270,17 @@ export async function healBrokenDefaultLaunches(log) {
2248
2270
  continue;
2249
2271
  }
2250
2272
  log?.(`${AGENTS[agent].name}@${version} won't launch — repairing…`);
2251
- const healed = await ensureAgentRunnable(agent, version, log);
2273
+ const healed = await ensureAgentRunnable(agent, version, log, opts);
2252
2274
  if (healed) {
2253
2275
  failedRepairAt.delete(key);
2254
2276
  repaired.push(`${agent}@${version}${healed === version ? '' : `→${healed}`}`);
2255
2277
  }
2256
2278
  else {
2257
2279
  failedRepairAt.set(key, Date.now());
2280
+ unhealed.push(key);
2258
2281
  }
2259
2282
  }
2260
- return repaired;
2283
+ return { repaired, unhealed };
2261
2284
  }
2262
2285
  async function getCliVersionFromPath(agent) {
2263
2286
  const agentConfig = AGENTS[agent];
@@ -2640,6 +2663,7 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2640
2663
  const hooksToSync = selection
2641
2664
  ? resolveSelection(selection.hooks, available.hooks)
2642
2665
  : available.hooks;
2666
+ let hookManifest = {};
2643
2667
  if (hooksToSync.length > 0) {
2644
2668
  const r = hooksWriter.write({ version, versionHome, selection: hooksToSync, cwd });
2645
2669
  // Remove orphan files from version home. The trusted set is the
@@ -2657,6 +2681,11 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2657
2681
  }
2658
2682
  }
2659
2683
  result.hooks = r.synced.length > 0;
2684
+ hookManifest = selectHookManifest(parseHookManifest(), hooksToSync);
2685
+ }
2686
+ const hooksInScope = !userPassedSelection || selection?.hooks !== undefined;
2687
+ if (agent === 'opencode' && hooksInScope) {
2688
+ registerHooksToSettings(agent, versionHome, hookManifest);
2660
2689
  }
2661
2690
  }
2662
2691
  }
@@ -0,0 +1,43 @@
1
+ /** Same path the Factory reader pulls (apps/factory/src/core/watchdogLog.ts). */
2
+ export declare const WATCHDOG_LOG_PATH: string;
3
+ /** Kinds the Factory reader accepts. Keep in lockstep with watchdogLog.ts. */
4
+ export type WatchdogEventKind = 'tick' | 'decision' | 'nudge' | 'rotate' | 'error';
5
+ /** JSONL row shape — a verbatim replica of the Factory reader's WatchdogEvent. */
6
+ export interface WatchdogEvent {
7
+ ts: number;
8
+ kind: WatchdogEventKind;
9
+ terminalId?: string;
10
+ agentType?: string;
11
+ message: string;
12
+ reason?: string;
13
+ /** For 'tick' events: the session lines the watchdog actually read. */
14
+ tailLines?: string[];
15
+ /** For 'tick' / 'decision' events: how long the terminal had been stalled. */
16
+ stalledForMs?: number;
17
+ /** Carried so the UI can show what the agent was stuck on. */
18
+ lastUserMessage?: string;
19
+ lastAssistantMessage?: string;
20
+ /** For a 'decision'/'nudge' that injects: the exact text delivered. */
21
+ nudgeText?: string;
22
+ }
23
+ /** Serialize one event to a JSONL line (no trailing newline). */
24
+ export declare function formatEvent(ev: WatchdogEvent): string;
25
+ /**
26
+ * Cap on retained log lines. Mirrors the Factory reader's expectation that the
27
+ * writer trims (watchdogLog.ts trimToLast). Large enough to keep a useful
28
+ * history, small enough to bound the file the UI polls.
29
+ */
30
+ export declare const WATCHDOG_LOG_MAX_LINES = 2000;
31
+ /** Trim a JSONL body to the last `maxLines` events (same logic as the reader). */
32
+ export declare function trimToLast(text: string, maxLines: number): string;
33
+ /**
34
+ * Append events to the log under a file lock, trimming to WATCHDOG_LOG_MAX_LINES
35
+ * so the file never grows unbounded. Concurrent watchdog ticks share the lock so
36
+ * their appends never interleave into a corrupt line. Best-effort: a filesystem
37
+ * error is swallowed (the Factory card tolerates a stale/missing log), never
38
+ * throwing into the tick.
39
+ */
40
+ export declare function appendWatchdogEvents(events: WatchdogEvent[], opts?: {
41
+ logPath?: string;
42
+ maxLines?: number;
43
+ }): void;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Canonical watchdog event log (watchdog-brain-v2).
3
+ *
4
+ * The Factory Floor renders a read-only "Watchdog activity" card from the JSONL
5
+ * feed at ~/.agents/.cache/logs/watchdog.log. That feed was historically written
6
+ * by the retired extension-side watchdog; now the always-on CLI watchdog owns it.
7
+ *
8
+ * The event SHAPE here is a deliberate, verbatim replica of the reader in
9
+ * apps/factory/src/core/watchdogLog.ts (WatchdogEvent / WatchdogEventKind /
10
+ * WATCHDOG_LOG_PATH / the trim cap). The repo forbids cross-app imports
11
+ * (CLAUDE.md repo map), so the two files are kept in sync by hand — a change to
12
+ * the Factory reader's shape must be mirrored here. There is no import between
13
+ * them.
14
+ */
15
+ import * as fs from 'fs';
16
+ import * as os from 'os';
17
+ import * as path from 'path';
18
+ import { withFileLock, atomicWriteFileSync, ensureLockTarget } from '../fs-atomic.js';
19
+ /** Same path the Factory reader pulls (apps/factory/src/core/watchdogLog.ts). */
20
+ export const WATCHDOG_LOG_PATH = path.join(os.homedir(), '.agents', '.cache', 'logs', 'watchdog.log');
21
+ /** Serialize one event to a JSONL line (no trailing newline). */
22
+ export function formatEvent(ev) {
23
+ return JSON.stringify(ev);
24
+ }
25
+ /**
26
+ * Cap on retained log lines. Mirrors the Factory reader's expectation that the
27
+ * writer trims (watchdogLog.ts trimToLast). Large enough to keep a useful
28
+ * history, small enough to bound the file the UI polls.
29
+ */
30
+ export const WATCHDOG_LOG_MAX_LINES = 2000;
31
+ /** Trim a JSONL body to the last `maxLines` events (same logic as the reader). */
32
+ export function trimToLast(text, maxLines) {
33
+ const lines = text.split('\n').filter((l) => l.trim().length > 0);
34
+ if (lines.length <= maxLines)
35
+ return lines.join('\n') + (lines.length ? '\n' : '');
36
+ return lines.slice(lines.length - maxLines).join('\n') + '\n';
37
+ }
38
+ /**
39
+ * Append events to the log under a file lock, trimming to WATCHDOG_LOG_MAX_LINES
40
+ * so the file never grows unbounded. Concurrent watchdog ticks share the lock so
41
+ * their appends never interleave into a corrupt line. Best-effort: a filesystem
42
+ * error is swallowed (the Factory card tolerates a stale/missing log), never
43
+ * throwing into the tick.
44
+ */
45
+ export function appendWatchdogEvents(events, opts = {}) {
46
+ if (events.length === 0)
47
+ return;
48
+ const logPath = opts.logPath ?? WATCHDOG_LOG_PATH;
49
+ const maxLines = opts.maxLines ?? WATCHDOG_LOG_MAX_LINES;
50
+ const lockPath = path.join(path.dirname(logPath), '.watchdog.log.lock');
51
+ try {
52
+ ensureLockTarget(lockPath);
53
+ withFileLock(lockPath, () => {
54
+ let existing = '';
55
+ try {
56
+ existing = fs.readFileSync(logPath, 'utf8');
57
+ }
58
+ catch {
59
+ /* first write — no file yet */
60
+ }
61
+ const appended = events.map(formatEvent).join('\n') + '\n';
62
+ const body = trimToLast(existing + appended, maxLines);
63
+ atomicWriteFileSync(logPath, body);
64
+ });
65
+ }
66
+ catch {
67
+ /* best-effort: never let logging break a tick */
68
+ }
69
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The watchdog, expressed as a routine.
3
+ *
4
+ * The always-on watchdog is not a bespoke subsystem — it is a plain `command`
5
+ * routine the daemon's existing scheduler fires: `agents watchdog --nudge`, once
6
+ * every couple of minutes. Enabling / disabling the watchdog is enabling /
7
+ * disabling that routine, so there is ONE durable, inspectable concept
8
+ * (`agents routines list` shows it; the scheduler catches it up if the daemon was
9
+ * down) instead of a private sentinel file and a hand-rolled `--watch` loop.
10
+ *
11
+ * A `command` routine — not `agent`/`workflow` — because the tick is deterministic
12
+ * housekeeping: no LLM, no auth, no sandbox. Each fire runs ONE bounded tick (the
13
+ * cron is the loop), so the daemon's per-fire model fits without a long-lived
14
+ * process.
15
+ */
16
+ import { type JobConfig } from '../routines.js';
17
+ /** The routine name. `agents routines list` / `readJob` key on this. */
18
+ export declare const WATCHDOG_ROUTINE_NAME = "watchdog";
19
+ /**
20
+ * Fire every 2 minutes. Mirrors the extension's 120s tick, and is well inside the
21
+ * 5m stall threshold and 20m nudge cooldown, so a stall is noticed promptly
22
+ * without the daemon spawning a tick more often than the cooldown can matter.
23
+ */
24
+ export declare const WATCHDOG_ROUTINE_SCHEDULE = "*/2 * * * *";
25
+ /** The plain shell each fire runs: one bounded, deterministic nudge tick. */
26
+ export declare const WATCHDOG_ROUTINE_COMMAND = "agents watchdog --nudge";
27
+ /**
28
+ * Build the watchdog routine config. Pure — touches no disk. `mode`/`effort`/
29
+ * `timeout` are required by JobConfig but ignored for `command` routines;
30
+ * writeJob() drops the default ('auto'/'10m') values so the on-disk file stays
31
+ * to the point (name + schedule + command + enabled).
32
+ */
33
+ export declare function buildWatchdogRoutine(enabled: boolean): JobConfig;
34
+ /** Is the watchdog routine present on disk (user or system layer)? */
35
+ export declare function watchdogRoutineExists(): boolean;
36
+ /** Is the watchdog routine present AND enabled? */
37
+ export declare function isWatchdogRoutineEnabled(): boolean;
38
+ /**
39
+ * Ensure the watchdog routine exists with the requested enabled state, preserving
40
+ * any user edits. Absent → create it; present → only flip `enabled` (never clobber
41
+ * a schedule or command the user tuned). Idempotent: calling it twice with the
42
+ * same state is a no-op after the first.
43
+ */
44
+ export declare function ensureWatchdogRoutine(enabled: boolean): void;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The watchdog, expressed as a routine.
3
+ *
4
+ * The always-on watchdog is not a bespoke subsystem — it is a plain `command`
5
+ * routine the daemon's existing scheduler fires: `agents watchdog --nudge`, once
6
+ * every couple of minutes. Enabling / disabling the watchdog is enabling /
7
+ * disabling that routine, so there is ONE durable, inspectable concept
8
+ * (`agents routines list` shows it; the scheduler catches it up if the daemon was
9
+ * down) instead of a private sentinel file and a hand-rolled `--watch` loop.
10
+ *
11
+ * A `command` routine — not `agent`/`workflow` — because the tick is deterministic
12
+ * housekeeping: no LLM, no auth, no sandbox. Each fire runs ONE bounded tick (the
13
+ * cron is the loop), so the daemon's per-fire model fits without a long-lived
14
+ * process.
15
+ */
16
+ import { readJob, writeJob, setJobEnabled } from '../routines.js';
17
+ /** The routine name. `agents routines list` / `readJob` key on this. */
18
+ export const WATCHDOG_ROUTINE_NAME = 'watchdog';
19
+ /**
20
+ * Fire every 2 minutes. Mirrors the extension's 120s tick, and is well inside the
21
+ * 5m stall threshold and 20m nudge cooldown, so a stall is noticed promptly
22
+ * without the daemon spawning a tick more often than the cooldown can matter.
23
+ */
24
+ export const WATCHDOG_ROUTINE_SCHEDULE = '*/2 * * * *';
25
+ /** The plain shell each fire runs: one bounded, deterministic nudge tick. */
26
+ export const WATCHDOG_ROUTINE_COMMAND = 'agents watchdog --nudge';
27
+ /**
28
+ * Build the watchdog routine config. Pure — touches no disk. `mode`/`effort`/
29
+ * `timeout` are required by JobConfig but ignored for `command` routines;
30
+ * writeJob() drops the default ('auto'/'10m') values so the on-disk file stays
31
+ * to the point (name + schedule + command + enabled).
32
+ */
33
+ export function buildWatchdogRoutine(enabled) {
34
+ return {
35
+ name: WATCHDOG_ROUTINE_NAME,
36
+ schedule: WATCHDOG_ROUTINE_SCHEDULE,
37
+ command: WATCHDOG_ROUTINE_COMMAND,
38
+ enabled,
39
+ mode: 'auto',
40
+ effort: 'auto',
41
+ timeout: '10m',
42
+ prompt: '',
43
+ };
44
+ }
45
+ /** Is the watchdog routine present on disk (user or system layer)? */
46
+ export function watchdogRoutineExists() {
47
+ return readJob(WATCHDOG_ROUTINE_NAME) !== null;
48
+ }
49
+ /** Is the watchdog routine present AND enabled? */
50
+ export function isWatchdogRoutineEnabled() {
51
+ const job = readJob(WATCHDOG_ROUTINE_NAME);
52
+ return job !== null && job.enabled === true;
53
+ }
54
+ /**
55
+ * Ensure the watchdog routine exists with the requested enabled state, preserving
56
+ * any user edits. Absent → create it; present → only flip `enabled` (never clobber
57
+ * a schedule or command the user tuned). Idempotent: calling it twice with the
58
+ * same state is a no-op after the first.
59
+ */
60
+ export function ensureWatchdogRoutine(enabled) {
61
+ const existing = readJob(WATCHDOG_ROUTINE_NAME);
62
+ if (existing === null) {
63
+ writeJob(buildWatchdogRoutine(enabled));
64
+ return;
65
+ }
66
+ if (existing.enabled !== enabled) {
67
+ setJobEnabled(WATCHDOG_ROUTINE_NAME, enabled);
68
+ }
69
+ }
@@ -30,7 +30,9 @@
30
30
  */
31
31
  import type { ActiveSession } from '../session/active.js';
32
32
  import { type InjectRail } from '../terminal/resolve.js';
33
- import { type StallStatus } from './watchdog.js';
33
+ import { type InjectResult, type InjectTarget } from '../terminal/inject.js';
34
+ import { type StallStatus, type WatchdogCandidate } from './watchdog.js';
35
+ import { type OpenBlock } from '../feed.js';
34
36
  /** Per-session policy sentinel. `keep` is the default (watchdog may nudge). */
35
37
  export type WatchdogPolicy = 'off' | 'keep' | 'handsoff';
36
38
  /** Stall / cooldown / dormant thresholds (ms). Defaults mirror read.ts. */
@@ -48,12 +50,15 @@ export interface WatchdogTickOptions {
48
50
  /** Nudge text delivered into the terminal. Default "Continue." */
49
51
  nudgeText?: string;
50
52
  /**
51
- * Use the LLM decider (`agents run`) to decide + choose nudge text instead of
52
- * the deterministic promise-without-toolcall path. Default false so ticks are
53
- * reproducible. Best-effort: a decider failure falls back to skip.
53
+ * Force the LLM decider (`agents run`) on EVERY stalled candidate instead of the
54
+ * hybrid path. Default false. Even when false the tick still ESCALATES the
55
+ * judgment-heavy cases (parked-on-question, ambiguous stalls) to the smart brain
56
+ * — `smart: true` additionally routes the obvious promise-without-toolcall
57
+ * nudges through it. Non-reproducible; the deterministic pre-filter is the
58
+ * reproducible default.
54
59
  */
55
60
  smart?: boolean;
56
- /** Agent the smart decider runs as. Default 'claude'. */
61
+ /** Agent the smart decider runs as (and the built-in fallback prompt). Default 'claude'. */
57
62
  smartAgent?: string;
58
63
  /** Threshold overrides. Missing fields fall back to DEFAULT_THRESHOLDS. */
59
64
  thresholds?: Partial<WatchdogThresholds>;
@@ -73,7 +78,25 @@ export interface WatchdogTickOptions {
73
78
  tailFor?: (s: ActiveSession) => string[];
74
79
  /** Per-session policy. Default = the on-disk sentinel. */
75
80
  policyFor?: (s: ActiveSession) => WatchdogPolicy;
81
+ /**
82
+ * The smart brain: given a stalled candidate, decide drive-forward vs
83
+ * leave-for-human and craft the message. Default resolves a `watchdog` workflow
84
+ * (repo/user override + `model:` frontmatter) and, failing that, the improved
85
+ * built-in prompt — both via `agents run … --mode plan`. Tests inject a
86
+ * synthetic decider so escalation is exercised without shelling out.
87
+ */
88
+ smartDecider?: SmartDecider;
89
+ /** Open feed block for a session (parked-on-question detection). Default reads the feed. */
90
+ openBlockFor?: (s: ActiveSession) => OpenBlock | null;
91
+ /** Inject primitive. Default injectIntoTerminal — tests capture the resolved target. */
92
+ injectFn?: (target: InjectTarget, text: string, opts: {
93
+ dryRun?: boolean;
94
+ }) => Promise<InjectResult>;
95
+ /** Override the canonical watchdog.log path (tests point at a tmp file). */
96
+ logPath?: string;
76
97
  }
98
+ /** The smart brain seam: a stalled candidate in, a nudge decision out. */
99
+ export type SmartDecider = (session: ActiveSession, candidate: WatchdogCandidate) => Promise<NudgeDecision>;
77
100
  /** What the tick decided for a single session — the row `--json` / the tray reads. */
78
101
  export interface SessionOutcome {
79
102
  sessionId?: string;
@@ -88,15 +111,19 @@ export interface SessionOutcome {
88
111
  policy: WatchdogPolicy;
89
112
  decision: 'nudge' | 'skip';
90
113
  reason: string;
91
- /** The resolved rail, when addressable. */
114
+ /** The resolved rail, when delivered by injecting into a terminal split. */
92
115
  rail?: InjectRail;
116
+ /** How the nudge was (or would be) delivered: inject | mailbox | resume. */
117
+ via?: NudgeVia;
93
118
  /** True when resolveInjectTarget said addressable (only meaningful once we'd nudge). */
94
119
  addressable?: boolean;
95
- /** True when a nudge was actually delivered this tick. */
120
+ /** True when a nudge was actually delivered this tick (any mechanism). */
96
121
  injected?: boolean;
97
122
  /** The text that was (or would be) delivered. */
98
123
  nudgeText?: string;
99
124
  }
125
+ /** Delivery mechanism the answer-router picked for a nudge. */
126
+ export type NudgeVia = 'inject' | 'mailbox' | 'resume';
100
127
  export interface WatchdogTickResult {
101
128
  atMs: number;
102
129
  /** Whether this tick was allowed to inject (opts.nudge). */
@@ -118,6 +145,23 @@ export interface WatchdogTickResult {
118
145
  export declare function readPolicySentinel(dir: string, sessionId: string): WatchdogPolicy;
119
146
  /** Write a per-session policy sentinel (used by the CLI `agents watchdog policy`). */
120
147
  export declare function writePolicySentinel(dir: string, sessionId: string, policy: WatchdogPolicy): void;
148
+ /** A decide result shared by the deterministic and smart paths. `text` overrides the default nudge text. */
149
+ export interface NudgeDecision {
150
+ nudge: boolean;
151
+ reason: string;
152
+ text?: string;
153
+ }
154
+ /**
155
+ * The smart brain. Resolves a `watchdog` workflow for the session's cwd
156
+ * (project > user > system precedence, via resolveWorkflowRef) so a repo/user
157
+ * override AND `model:` frontmatter come for free; when a workflow resolves it
158
+ * runs `agents run watchdog --mode plan <prompt>`, else it falls back to the
159
+ * improved built-in prompt via `agents run <agent> --mode plan <prompt>`. Plan
160
+ * mode keeps the decider read-only. Best-effort and NON-deterministic: any
161
+ * failure (decider unavailable, no verdict) returns a SAFE skip — a parked
162
+ * question we cannot judge is left for the human, never blindly nudged.
163
+ */
164
+ export declare function makeDefaultSmartDecider(agent: string): SmartDecider;
121
165
  /**
122
166
  * Run ONE watchdog pass. Returns a structured outcome per live session; injects
123
167
  * only when `opts.nudge` is set AND the safety gate says addressable AND policy