@phnx-labs/agents-cli 1.20.73 → 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 (209) hide show
  1. package/CHANGELOG.md +989 -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 +31 -1
  13. package/dist/commands/exec.js +31 -3
  14. package/dist/commands/export.d.ts +11 -0
  15. package/dist/commands/export.js +215 -0
  16. package/dist/commands/feed.d.ts +7 -1
  17. package/dist/commands/feed.js +47 -4
  18. package/dist/commands/go.d.ts +9 -2
  19. package/dist/commands/go.js +16 -6
  20. package/dist/commands/harness.d.ts +14 -0
  21. package/dist/commands/harness.js +145 -0
  22. package/dist/commands/import.js +87 -9
  23. package/dist/commands/inspect.js +5 -2
  24. package/dist/commands/profiles.d.ts +20 -0
  25. package/dist/commands/profiles.js +68 -21
  26. package/dist/commands/repo.js +14 -0
  27. package/dist/commands/routines.js +15 -2
  28. package/dist/commands/secrets.js +44 -21
  29. package/dist/commands/sessions-browser.js +1 -1
  30. package/dist/commands/sessions-export.js +19 -5
  31. package/dist/commands/sessions-migrate.d.ts +29 -0
  32. package/dist/commands/sessions-migrate.js +596 -0
  33. package/dist/commands/sessions-picker.js +16 -1
  34. package/dist/commands/sessions.d.ts +36 -13
  35. package/dist/commands/sessions.js +210 -40
  36. package/dist/commands/setup-computer.js +2 -2
  37. package/dist/commands/setup.js +6 -1
  38. package/dist/commands/ssh.js +71 -35
  39. package/dist/commands/status.js +10 -2
  40. package/dist/commands/versions.js +51 -11
  41. package/dist/commands/view.d.ts +5 -0
  42. package/dist/commands/view.js +99 -31
  43. package/dist/commands/watchdog.d.ts +7 -2
  44. package/dist/commands/watchdog.js +73 -57
  45. package/dist/index.js +59 -7
  46. package/dist/lib/activity.d.ts +14 -4
  47. package/dist/lib/activity.js +17 -5
  48. package/dist/lib/actor.d.ts +50 -0
  49. package/dist/lib/actor.js +166 -0
  50. package/dist/lib/agent-spec/provider.js +2 -1
  51. package/dist/lib/agent-spec/resolve.js +19 -5
  52. package/dist/lib/agent-spec/types.d.ts +9 -1
  53. package/dist/lib/agents.d.ts +56 -0
  54. package/dist/lib/agents.js +133 -7
  55. package/dist/lib/cloud/codex.d.ts +2 -0
  56. package/dist/lib/cloud/codex.js +14 -3
  57. package/dist/lib/cloud/session-index.d.ts +32 -0
  58. package/dist/lib/cloud/session-index.js +58 -0
  59. package/dist/lib/cloud/store.d.ts +7 -0
  60. package/dist/lib/cloud/store.js +25 -0
  61. package/dist/lib/config-transfer.d.ts +16 -0
  62. package/dist/lib/config-transfer.js +60 -0
  63. package/dist/lib/daemon.js +18 -2
  64. package/dist/lib/devices/connect.d.ts +15 -1
  65. package/dist/lib/devices/connect.js +15 -1
  66. package/dist/lib/devices/fleet.d.ts +21 -0
  67. package/dist/lib/devices/fleet.js +26 -0
  68. package/dist/lib/devices/health-report.d.ts +38 -0
  69. package/dist/lib/devices/health-report.js +214 -0
  70. package/dist/lib/devices/health.js +4 -1
  71. package/dist/lib/devices/reachability.d.ts +31 -0
  72. package/dist/lib/devices/reachability.js +40 -0
  73. package/dist/lib/devices/registry.d.ts +33 -0
  74. package/dist/lib/devices/registry.js +37 -0
  75. package/dist/lib/devices/resolve-target.d.ts +15 -27
  76. package/dist/lib/devices/resolve-target.js +63 -102
  77. package/dist/lib/devices/sync.d.ts +18 -0
  78. package/dist/lib/devices/sync.js +23 -1
  79. package/dist/lib/devices/tailscale.d.ts +3 -0
  80. package/dist/lib/devices/tailscale.js +1 -0
  81. package/dist/lib/events.d.ts +1 -1
  82. package/dist/lib/exec.d.ts +66 -0
  83. package/dist/lib/exec.js +195 -6
  84. package/dist/lib/export.d.ts +72 -0
  85. package/dist/lib/export.js +269 -0
  86. package/dist/lib/feed-post.d.ts +63 -0
  87. package/dist/lib/feed-post.js +204 -0
  88. package/dist/lib/fleet/apply.d.ts +32 -2
  89. package/dist/lib/fleet/apply.js +97 -10
  90. package/dist/lib/fleet/types.d.ts +11 -0
  91. package/dist/lib/fs-walk.d.ts +13 -0
  92. package/dist/lib/fs-walk.js +16 -7
  93. package/dist/lib/hooks.d.ts +1 -0
  94. package/dist/lib/hooks.js +156 -0
  95. package/dist/lib/hosts/dispatch.d.ts +7 -0
  96. package/dist/lib/hosts/dispatch.js +2 -0
  97. package/dist/lib/hosts/passthrough.js +2 -0
  98. package/dist/lib/hosts/reconnect.d.ts +78 -0
  99. package/dist/lib/hosts/reconnect.js +127 -0
  100. package/dist/lib/hosts/registry.d.ts +75 -14
  101. package/dist/lib/hosts/registry.js +205 -30
  102. package/dist/lib/hosts/remote-cmd.js +1 -0
  103. package/dist/lib/hosts/run-target.d.ts +13 -5
  104. package/dist/lib/hosts/run-target.js +26 -10
  105. package/dist/lib/hosts/session-index.d.ts +18 -1
  106. package/dist/lib/hosts/session-index.js +37 -2
  107. package/dist/lib/hosts/session-marker.d.ts +33 -0
  108. package/dist/lib/hosts/session-marker.js +51 -0
  109. package/dist/lib/hosts/tasks.d.ts +8 -4
  110. package/dist/lib/import.d.ts +25 -0
  111. package/dist/lib/import.js +107 -2
  112. package/dist/lib/isolation-boundary-report.d.ts +10 -0
  113. package/dist/lib/isolation-boundary-report.js +35 -0
  114. package/dist/lib/menubar/install-menubar.d.ts +20 -0
  115. package/dist/lib/menubar/install-menubar.js +45 -1
  116. package/dist/lib/migrate.d.ts +16 -0
  117. package/dist/lib/migrate.js +36 -0
  118. package/dist/lib/models.d.ts +27 -0
  119. package/dist/lib/models.js +54 -1
  120. package/dist/lib/overdue.d.ts +7 -2
  121. package/dist/lib/overdue.js +18 -11
  122. package/dist/lib/picker.js +4 -1
  123. package/dist/lib/platform/process.d.ts +17 -0
  124. package/dist/lib/platform/process.js +70 -0
  125. package/dist/lib/profiles-presets.js +9 -7
  126. package/dist/lib/profiles.d.ts +31 -0
  127. package/dist/lib/profiles.js +70 -0
  128. package/dist/lib/project-resources.js +20 -5
  129. package/dist/lib/pty-server.d.ts +2 -10
  130. package/dist/lib/pty-server.js +4 -38
  131. package/dist/lib/registry.d.ts +1 -1
  132. package/dist/lib/registry.js +48 -8
  133. package/dist/lib/rotate.d.ts +18 -0
  134. package/dist/lib/rotate.js +28 -0
  135. package/dist/lib/routines.d.ts +26 -0
  136. package/dist/lib/routines.js +19 -2
  137. package/dist/lib/runner.js +104 -7
  138. package/dist/lib/secrets/agent.d.ts +66 -1
  139. package/dist/lib/secrets/agent.js +154 -90
  140. package/dist/lib/secrets/bundles.d.ts +34 -0
  141. package/dist/lib/secrets/bundles.js +179 -19
  142. package/dist/lib/secrets/index.d.ts +15 -1
  143. package/dist/lib/secrets/index.js +21 -3
  144. package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
  145. package/dist/lib/secrets/rc-hygiene.js +154 -0
  146. package/dist/lib/secrets/remote.d.ts +7 -4
  147. package/dist/lib/secrets/remote.js +7 -4
  148. package/dist/lib/secrets/sync-commands.d.ts +21 -0
  149. package/dist/lib/secrets/sync-commands.js +21 -0
  150. package/dist/lib/self-update.d.ts +23 -2
  151. package/dist/lib/self-update.js +86 -5
  152. package/dist/lib/session/active.d.ts +114 -7
  153. package/dist/lib/session/active.js +323 -52
  154. package/dist/lib/session/cloud.js +2 -1
  155. package/dist/lib/session/db.d.ts +48 -0
  156. package/dist/lib/session/db.js +238 -14
  157. package/dist/lib/session/detached.d.ts +30 -0
  158. package/dist/lib/session/detached.js +92 -0
  159. package/dist/lib/session/discover.d.ts +465 -2
  160. package/dist/lib/session/discover.js +1648 -468
  161. package/dist/lib/session/fork.js +2 -1
  162. package/dist/lib/session/hook-sessions.d.ts +43 -0
  163. package/dist/lib/session/hook-sessions.js +135 -0
  164. package/dist/lib/session/linear.d.ts +6 -0
  165. package/dist/lib/session/linear.js +55 -0
  166. package/dist/lib/session/migrate-targets.d.ts +65 -0
  167. package/dist/lib/session/migrate-targets.js +94 -0
  168. package/dist/lib/session/migrations.d.ts +37 -0
  169. package/dist/lib/session/migrations.js +60 -0
  170. package/dist/lib/session/parse.d.ts +18 -0
  171. package/dist/lib/session/parse.js +130 -30
  172. package/dist/lib/session/pid-registry.d.ts +34 -2
  173. package/dist/lib/session/pid-registry.js +49 -2
  174. package/dist/lib/session/render.d.ts +7 -2
  175. package/dist/lib/session/render.js +22 -10
  176. package/dist/lib/session/short-id.d.ts +17 -0
  177. package/dist/lib/session/short-id.js +20 -0
  178. package/dist/lib/session/state.js +6 -2
  179. package/dist/lib/shims.d.ts +27 -1
  180. package/dist/lib/shims.js +88 -6
  181. package/dist/lib/sqlite.d.ts +3 -2
  182. package/dist/lib/sqlite.js +27 -4
  183. package/dist/lib/staleness/detectors/commands.js +1 -1
  184. package/dist/lib/staleness/detectors/workflows.js +13 -2
  185. package/dist/lib/staleness/writers/commands.js +7 -7
  186. package/dist/lib/staleness/writers/workflows.d.ts +4 -2
  187. package/dist/lib/startup/command-registry.d.ts +2 -0
  188. package/dist/lib/startup/command-registry.js +5 -0
  189. package/dist/lib/state.d.ts +1 -1
  190. package/dist/lib/state.js +15 -36
  191. package/dist/lib/teams/agents.d.ts +19 -10
  192. package/dist/lib/teams/agents.js +40 -39
  193. package/dist/lib/types.d.ts +48 -2
  194. package/dist/lib/uninstall.js +1 -45
  195. package/dist/lib/usage.d.ts +33 -1
  196. package/dist/lib/usage.js +172 -12
  197. package/dist/lib/versions.d.ts +22 -2
  198. package/dist/lib/versions.js +98 -10
  199. package/dist/lib/watchdog/log.d.ts +43 -0
  200. package/dist/lib/watchdog/log.js +69 -0
  201. package/dist/lib/watchdog/routine.d.ts +44 -0
  202. package/dist/lib/watchdog/routine.js +69 -0
  203. package/dist/lib/watchdog/runner.d.ts +51 -7
  204. package/dist/lib/watchdog/runner.js +239 -64
  205. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  206. package/dist/lib/watchdog/watchdog.js +31 -16
  207. package/dist/lib/workflows.d.ts +16 -0
  208. package/dist/lib/workflows.js +110 -1
  209. package/package.json +1 -1
@@ -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
@@ -36,6 +36,13 @@ import { injectIntoTerminal } from '../terminal/inject.js';
36
36
  import { classifyTerminal, isLikelyTrulyBlocked, renderWatchdogPrompt, parseWatchdogResponse, } from './watchdog.js';
37
37
  import { readWatchdogTail, WATCHDOG_STALL_MS, WATCHDOG_COOLDOWN_MS, WATCHDOG_DORMANT_MS, WATCHDOG_TAIL_LINES, } from './read.js';
38
38
  import { getRuntimeStateDir } from '../state.js';
39
+ import { withFileLock, atomicWriteFileSync, ensureLockTarget } from '../fs-atomic.js';
40
+ import { resolveAnswerRoute, isOpenQuestionBlock } from '../answer-router.js';
41
+ import { enqueue, mailboxDir } from '../mailbox.js';
42
+ import { mailboxIdForActiveSession } from '../mailbox-target.js';
43
+ import { readBlock, blockIdForSession } from '../feed.js';
44
+ import { summarizeWatchdogTail } from './watchdogTail.js';
45
+ import { appendWatchdogEvents } from './log.js';
39
46
  export const DEFAULT_THRESHOLDS = {
40
47
  stallMs: WATCHDOG_STALL_MS,
41
48
  cooldownMs: WATCHDOG_COOLDOWN_MS,
@@ -103,12 +110,10 @@ function defaultLastActivity(s) {
103
110
  return s.startedAtMs;
104
111
  }
105
112
  /**
106
- * Minimal local mirror of COMPLETION_HINTS in watchdog.ts:42-47. We cannot lean
107
- * on isLikelyTrulyBlocked to screen completions out: its FORCE_REVIEW_STALL_MS
108
- * short-circuit (watchdog.ts:171 `stalledForMs >= 15m` returns true) fires
109
- * BEFORE its own COMPLETION_HINTS check (watchdog.ts:176), so a session idle
110
- * 15m-60m whose tail says "done"/"finished" would be reported as blocked. Keep
111
- * this list in sync with the core.
113
+ * COMPLETION_HINTS mirror of watchdog.ts. The deterministic pre-filter screens
114
+ * completions to `skip` before the promise check so a finished-but-idle session
115
+ * is never nudged. (isLikelyTrulyBlocked also guards completion now that its 15m
116
+ * precedence is fixed; this explicit check keeps the pre-filter self-contained.)
112
117
  */
113
118
  const COMPLETION_HINTS = ['done', 'completed', 'all set', 'finished'];
114
119
  function tailShowsCompletion(candidate) {
@@ -118,45 +123,130 @@ function tailShowsCompletion(candidate) {
118
123
  return COMPLETION_HINTS.some((hint) => lowerTail.includes(hint));
119
124
  }
120
125
  function deterministicDecision(session, candidate) {
126
+ // Parked on a question — the case Muqsit cares about most. Do NOT drop it:
127
+ // escalate to the brain, which decides drive-forward vs leave-for-human.
121
128
  if (session.activity === 'waiting_input') {
122
- return { nudge: false, reason: `waiting on user${session.awaitingReason ? ` (${session.awaitingReason})` : ''}` };
129
+ return {
130
+ kind: 'escalate',
131
+ reason: `parked on a question${session.awaitingReason ? ` (${session.awaitingReason})` : ''} — escalate to the brain`,
132
+ };
123
133
  }
124
- // Completion is checked EXPLICITLY here never nudge a finished session, even
125
- // past the 15m force-review window where isLikelyTrulyBlocked would return true
126
- // before reaching its completion check (see COMPLETION_HINTS note above).
134
+ // Clearly completecheap skip, no LLM.
127
135
  if (tailShowsCompletion(candidate)) {
128
- return { nudge: false, reason: 'tail shows completion (done / finished / all set) — skip regardless of stall age' };
136
+ return { kind: 'skip', reason: 'tail shows completion (done / finished / all set) — skip' };
129
137
  }
138
+ // Clear promise-without-toolcall — cheap nudge, no LLM.
130
139
  if (isLikelyTrulyBlocked(candidate)) {
131
- return { nudge: true, reason: 'stalled after announcing an action with no follow-through' };
140
+ return { kind: 'nudge', reason: 'stalled after announcing an action with no follow-through' };
132
141
  }
133
- return { nudge: false, reason: 'no promise-without-toolcall signal in tail (completion / question / unclear)' };
142
+ // Ambiguous stall let the brain judge rather than blindly skip.
143
+ return { kind: 'escalate', reason: 'ambiguous stall — escalate to the brain' };
134
144
  }
135
145
  /**
136
- * The optional LLM decider. Shells out to `agents run <agent>` with the watchdog
137
- * prompt and parses its JSON verdict. Best-effort and NON-deterministic the
138
- * default path is deterministicDecision. Isolated here so the tick stays pure
139
- * over its seams; the command layer opts in with --smart.
146
+ * The smart brain. Resolves a `watchdog` workflow for the session's cwd
147
+ * (project > user > system precedence, via resolveWorkflowRef) so a repo/user
148
+ * override AND `model:` frontmatter come for free; when a workflow resolves it
149
+ * runs `agents run watchdog --mode plan <prompt>`, else it falls back to the
150
+ * improved built-in prompt via `agents run <agent> --mode plan <prompt>`. Plan
151
+ * mode keeps the decider read-only. Best-effort and NON-deterministic: any
152
+ * failure (decider unavailable, no verdict) returns a SAFE skip — a parked
153
+ * question we cannot judge is left for the human, never blindly nudged.
140
154
  */
141
- async function smartDecision(candidate, agent) {
142
- const prompt = renderWatchdogPrompt([candidate]);
155
+ export function makeDefaultSmartDecider(agent) {
156
+ return async (session, candidate) => {
157
+ const prompt = renderWatchdogPrompt([candidate]);
158
+ try {
159
+ const [{ resolveWorkflowRef }, { execFile }, { promisify }] = await Promise.all([
160
+ import('../workflows.js'),
161
+ import('child_process'),
162
+ import('util'),
163
+ ]);
164
+ const cwd = session.cwd || process.cwd();
165
+ const workflowPath = resolveWorkflowRef('watchdog', cwd);
166
+ // A resolved `watchdog` workflow runs by name so its WORKFLOW.md body + model
167
+ // frontmatter apply; otherwise the bare agent runs the built-in prompt.
168
+ const runTarget = workflowPath ? 'watchdog' : agent;
169
+ const execFileAsync = promisify(execFile);
170
+ const { stdout } = await execFileAsync('agents', ['run', runTarget, '--mode', 'plan', prompt], {
171
+ encoding: 'utf8',
172
+ maxBuffer: 4 * 1024 * 1024,
173
+ timeout: 120_000,
174
+ });
175
+ const decisions = parseWatchdogResponse(stdout);
176
+ const d = decisions.find((x) => x.terminalId === candidate.terminalId) ?? decisions[0];
177
+ if (!d)
178
+ return { nudge: false, reason: 'smart decider returned no verdict' };
179
+ return { nudge: d.action === 'nudge', reason: d.reason || `smart: ${d.action}`, text: d.text || undefined };
180
+ }
181
+ catch (err) {
182
+ return { nudge: false, reason: `smart decider unavailable: ${err instanceof Error ? err.message : String(err)}` };
183
+ }
184
+ };
185
+ }
186
+ /**
187
+ * Pick the delivery mechanism. resolveAnswerRoute (answer-router.ts) chooses
188
+ * mailbox vs resume vs refuse; resolveInjectTargetForSession (resolve.ts) supplies
189
+ * the precise inject target — CRUCIALLY it handles the vscodium rail that the
190
+ * answer-router's own resolver cannot, so an IDE-terminal (VS Codium / Cursor /
191
+ * VS Code) session parked on a question is injected into its exact terminal rather
192
+ * than being downgraded to resume/refuse. When a precise rail exists it wins
193
+ * (that includes a stalled non-parked agent, so the v1 terminal-inject path is
194
+ * preserved and no nudge is stranded in a mailbox the agent will never poll).
195
+ */
196
+ function planDelivery(session, chosenText, block, allowGhosttyFocus) {
197
+ const sessionId = session.sessionId ?? '';
198
+ const mailboxId = mailboxIdForActiveSession(session) ?? sessionId;
199
+ const resolution = resolveInjectTargetForSession(session, { allowGhosttyFocus });
200
+ const route = resolveAnswerRoute({ mailboxId, answer: chosenText, session, block });
201
+ // A precise split (tmux / iTerm / vscodium / pty) is the authoritative target.
202
+ if (resolution.addressable) {
203
+ return { via: 'inject', rail: resolution.rail, target: resolution.target };
204
+ }
205
+ // No precise rail: honor the answer-router's parked-agent decision.
206
+ if (route.kind === 'resume')
207
+ return { via: 'resume' };
208
+ // Mailbox is correct ONLY for a still-looping agent that has an OPEN question
209
+ // block — it is seen at the next tool call. A stalled agent that simply stopped
210
+ // (no open block) would never poll the mailbox, so it is flagged instead of
211
+ // silently dropping a nudge into a spool it will never read.
212
+ if (route.kind === 'mailbox' && isOpenQuestionBlock(block))
213
+ return { via: 'mailbox', mailboxId };
214
+ return { via: 'refuse', reason: route.kind === 'refuse' ? route.reason : resolution.reason };
215
+ }
216
+ /** Default open-block reader — the same lookup `agents message` uses. */
217
+ function defaultOpenBlockFor(session) {
218
+ const id = mailboxIdForActiveSession(session) ?? session.sessionId;
219
+ if (!id)
220
+ return null;
221
+ const direct = readBlock(blockIdForSession(id));
222
+ if (direct && direct.mailboxId === id)
223
+ return direct;
224
+ return null;
225
+ }
226
+ /** Enqueue a nudge into a session's mailbox (running agent, seen at next tool call). */
227
+ function deliverViaMailbox(mailboxId, text, block) {
228
+ enqueue(mailboxDir(mailboxId), { to: mailboxId, text, from: 'watchdog', blockId: block?.blockId });
229
+ }
230
+ /** Re-enter a parked headless agent with the nudge as its next user turn. */
231
+ async function deliverViaResume(session, text) {
232
+ const sid = session.sessionId;
233
+ if (!sid)
234
+ return { ok: false, error: 'no session id to resume' };
143
235
  try {
144
- const { execFile } = await import('child_process');
145
- const { promisify } = await import('util');
146
- const execFileAsync = promisify(execFile);
147
- const { stdout } = await execFileAsync('agents', ['run', agent, '--mode', 'plan', prompt], {
148
- encoding: 'utf8',
149
- maxBuffer: 4 * 1024 * 1024,
150
- timeout: 120_000,
236
+ const [{ getAgentsInvocation }, { spawn }] = await Promise.all([
237
+ import('../daemon.js'),
238
+ import('child_process'),
239
+ ]);
240
+ const inv = getAgentsInvocation(['run', session.kind, '--resume', sid, '--', text]);
241
+ const code = await new Promise((resolve) => {
242
+ const child = spawn(inv.command, inv.args, { stdio: 'ignore', env: process.env, detached: false });
243
+ child.on('exit', (c) => resolve(c ?? 1));
244
+ child.on('error', () => resolve(1));
151
245
  });
152
- const decisions = parseWatchdogResponse(stdout);
153
- const d = decisions.find((x) => x.terminalId === candidate.terminalId) ?? decisions[0];
154
- if (!d)
155
- return { nudge: false, reason: 'smart decider returned no verdict' };
156
- return { nudge: d.action === 'nudge', reason: d.reason || `smart: ${d.action}`, text: d.text || undefined };
246
+ return code === 0 ? { ok: true } : { ok: false, error: `resume exited ${code}` };
157
247
  }
158
248
  catch (err) {
159
- return { nudge: false, reason: `smart decider unavailable: ${err instanceof Error ? err.message : String(err)}` };
249
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
160
250
  }
161
251
  }
162
252
  // --- the tick ---------------------------------------------------------------
@@ -174,10 +264,18 @@ export async function runWatchdogTick(opts = {}) {
174
264
  const lastActivityFor = opts.lastActivityFor ?? defaultLastActivity;
175
265
  const tailFor = opts.tailFor ?? ((s) => (s.sessionId ? readWatchdogTail(s.sessionId, s.kind, WATCHDOG_TAIL_LINES) : []));
176
266
  const policyFor = opts.policyFor ?? ((s) => (s.sessionId ? readPolicySentinel(dir, s.sessionId) : 'keep'));
267
+ const smartDecider = opts.smartDecider ?? makeDefaultSmartDecider(opts.smartAgent ?? 'claude');
268
+ const openBlockFor = opts.openBlockFor ?? defaultOpenBlockFor;
269
+ const injectFn = opts.injectFn ?? injectIntoTerminal;
177
270
  const sessions = opts.sessions ?? (await getActiveSessions());
178
271
  const ledger = readNudgeLedger(dir);
272
+ // Cooldown timestamps this tick decided to (re)start — merged into the ledger
273
+ // under a lock at the end so a concurrent tick's updates are never lost.
274
+ const ledgerUpdates = {};
179
275
  const flags = {};
180
276
  const outcomes = [];
277
+ const logEvents = [];
278
+ const viaLabel = (plan) => plan.via === 'inject' ? `inject (${plan.rail})` : plan.via;
181
279
  for (const session of sessions) {
182
280
  const policy = policyFor(session);
183
281
  const base = {
@@ -236,39 +334,66 @@ export async function runWatchdogTick(opts = {}) {
236
334
  tailLines,
237
335
  stalledForMs: status.stalledForMs,
238
336
  };
239
- const decision = opts.smart
240
- ? await smartDecision(candidate, opts.smartAgent ?? 'claude')
241
- : deterministicDecision(session, candidate);
337
+ // The brain. The cheap deterministic pre-filter resolves the obvious cases;
338
+ // parked-on-question and ambiguous stalls ESCALATE to the smart brain. `smart`
339
+ // forces every stalled candidate through the brain.
340
+ let decision;
341
+ if (opts.smart) {
342
+ decision = await smartDecider(session, candidate);
343
+ }
344
+ else {
345
+ const det = deterministicDecision(session, candidate);
346
+ decision = det.kind === 'escalate'
347
+ ? await smartDecider(session, candidate)
348
+ : { nudge: det.kind === 'nudge', reason: det.reason };
349
+ }
242
350
  const chosenText = decision.text ?? nudgeText;
351
+ // Log the decision for the Factory watchdog card.
352
+ const summary = summarizeWatchdogTail(tailLines, candidate.agentType);
353
+ logEvents.push({
354
+ ts: nowMs,
355
+ kind: 'decision',
356
+ terminalId: session.sessionId,
357
+ agentType: candidate.agentType,
358
+ message: decision.reason,
359
+ reason: decision.reason,
360
+ stalledForMs: status.stalledForMs,
361
+ tailLines,
362
+ nudgeText: decision.nudge ? chosenText : undefined,
363
+ lastUserMessage: summary.lastUserMessage,
364
+ lastAssistantMessage: summary.lastAssistantMessage,
365
+ });
243
366
  if (!decision.nudge) {
244
367
  outcomes.push({ ...base, decision: 'skip', reason: decision.reason });
245
368
  continue;
246
369
  }
247
- // A nudge is warranted — run it past the safety gate BEFORE any delivery.
248
- const resolution = resolveInjectTargetForSession(session, {
249
- allowGhosttyFocus: opts.allowGhosttyFocus,
250
- });
251
- if (!resolution.addressable) {
252
- // Flag the un-addressable stall for the tray; NEVER guess a target.
253
- flags[session.sessionId] = { reason: resolution.reason, host: session.host, atMs: nowMs };
370
+ // A nudge is warranted — plan delivery (answer-router picks mailbox/resume/
371
+ // refuse; resolveInjectTargetForSession supplies the vscodium-aware inject
372
+ // target) BEFORE any side effect.
373
+ const block = openBlockFor(session);
374
+ const plan = planDelivery(session, chosenText, block, opts.allowGhosttyFocus);
375
+ const rail = plan.via === 'inject' ? plan.rail : undefined;
376
+ const addressable = plan.via === 'inject' ? true : undefined;
377
+ if (plan.via === 'refuse') {
378
+ // No addressable rail and not headless-resumable — flag, NEVER guess.
379
+ flags[session.sessionId] = { reason: plan.reason, host: session.host, atMs: nowMs };
254
380
  outcomes.push({
255
381
  ...base, decision: 'skip', addressable: false,
256
- reason: `nudge-worthy but un-addressable — ${resolution.reason}`,
382
+ reason: `nudge-worthy but un-addressable — ${plan.reason}`,
257
383
  nudgeText: chosenText,
258
384
  });
259
385
  continue;
260
386
  }
261
- // handsoff = detect + flag, but never inject. Record a flag so the tray can
262
- // surface "would-nudge but hands-off" alongside the un-addressable flags.
387
+ // handsoff = detect + flag, but never deliver.
263
388
  if (policy === 'handsoff') {
264
389
  flags[session.sessionId] = {
265
- reason: `handsoff: would nudge via ${resolution.rail} but policy is hands-off`,
390
+ reason: `handsoff: would nudge via ${viaLabel(plan)} but policy is hands-off`,
266
391
  host: session.host,
267
392
  atMs: nowMs,
268
393
  };
269
394
  outcomes.push({
270
- ...base, decision: 'nudge', addressable: true, rail: resolution.rail, injected: false,
271
- reason: `handsoff: flagged, not injected (would nudge via ${resolution.rail})`,
395
+ ...base, decision: 'nudge', addressable, rail, via: plan.via, injected: false,
396
+ reason: `handsoff: flagged, not delivered (would nudge via ${viaLabel(plan)})`,
272
397
  nudgeText: chosenText,
273
398
  });
274
399
  continue;
@@ -276,38 +401,80 @@ export async function runWatchdogTick(opts = {}) {
276
401
  // Dry status (no --nudge): report what WOULD happen, deliver nothing.
277
402
  if (!opts.nudge) {
278
403
  outcomes.push({
279
- ...base, decision: 'nudge', addressable: true, rail: resolution.rail, injected: false,
280
- reason: `would nudge via ${resolution.rail} (dry — pass --nudge to inject)`,
404
+ ...base, decision: 'nudge', addressable, rail, via: plan.via, injected: false,
405
+ reason: `would nudge via ${viaLabel(plan)} (dry — pass --nudge)`,
281
406
  nudgeText: chosenText,
282
407
  });
283
408
  continue;
284
409
  }
285
- // Deliver into the EXACT resolved split.
286
- let result;
287
- try {
288
- result = await injectIntoTerminal(resolution.target, chosenText, { dryRun: opts.injectDryRun });
410
+ // Deliver. injectDryRun exercises the path without a real side effect: inject
411
+ // still calls injectFn (which honors dryRun), mailbox/resume are short-circuited.
412
+ let delivered;
413
+ if (plan.via === 'inject') {
414
+ try {
415
+ const r = await injectFn(plan.target, chosenText, { dryRun: opts.injectDryRun });
416
+ delivered = { ok: r.ok, error: r.error };
417
+ }
418
+ catch (err) {
419
+ delivered = { ok: false, error: err instanceof Error ? err.message : String(err) };
420
+ }
289
421
  }
290
- catch (err) {
291
- result = { ok: false, backend: resolution.target.backend, writes: 0, error: err instanceof Error ? err.message : String(err) };
422
+ else if (plan.via === 'mailbox') {
423
+ if (opts.injectDryRun) {
424
+ delivered = { ok: true };
425
+ }
426
+ else {
427
+ try {
428
+ deliverViaMailbox(plan.mailboxId, chosenText, block);
429
+ delivered = { ok: true };
430
+ }
431
+ catch (err) {
432
+ delivered = { ok: false, error: err instanceof Error ? err.message : String(err) };
433
+ }
434
+ }
435
+ }
436
+ else {
437
+ delivered = opts.injectDryRun ? { ok: true } : await deliverViaResume(session, chosenText);
292
438
  }
293
- if (result.ok) {
294
- ledger[session.sessionId] = nowMs; // start the cooldown clock
439
+ if (delivered.ok) {
440
+ ledgerUpdates[session.sessionId] = nowMs; // start the cooldown clock
441
+ logEvents.push({
442
+ ts: nowMs, kind: 'nudge', terminalId: session.sessionId, agentType: candidate.agentType,
443
+ message: `nudged via ${viaLabel(plan)}`, reason: decision.reason, nudgeText: chosenText,
444
+ });
295
445
  outcomes.push({
296
- ...base, decision: 'nudge', addressable: true, rail: resolution.rail, injected: true,
297
- reason: `nudged via ${resolution.rail}`,
446
+ ...base, decision: 'nudge', addressable, rail, via: plan.via, injected: true,
447
+ reason: `nudged via ${viaLabel(plan)}`,
298
448
  nudgeText: chosenText,
299
449
  });
300
450
  }
301
451
  else {
302
452
  outcomes.push({
303
- ...base, decision: 'skip', addressable: true, rail: resolution.rail, injected: false,
304
- reason: `inject failed via ${resolution.rail}: ${result.error ?? 'unknown error'}`,
453
+ ...base, decision: 'skip', addressable, rail, via: plan.via, injected: false,
454
+ reason: `nudge via ${viaLabel(plan)} failed: ${delivered.error ?? 'unknown error'}`,
305
455
  nudgeText: chosenText,
306
456
  });
307
457
  }
308
458
  }
309
- // Persist the tray-readable state.
310
- writeJsonFile(path.join(dir, 'nudges.json'), ledger);
459
+ // Persist the cooldown ledger under a lock: fresh-read + merge this tick's
460
+ // updates + atomic write, so a concurrent tick's timestamps are never lost
461
+ // (the old unlocked read-at-start / write-at-end was a lost-update race).
462
+ if (Object.keys(ledgerUpdates).length > 0) {
463
+ const nudgesPath = path.join(dir, 'nudges.json');
464
+ const ledgerLock = path.join(dir, '.ledger.lock');
465
+ try {
466
+ ensureLockTarget(ledgerLock);
467
+ withFileLock(ledgerLock, () => {
468
+ const current = readNudgeLedger(dir);
469
+ for (const [sid, ts] of Object.entries(ledgerUpdates))
470
+ current[sid] = ts;
471
+ atomicWriteFileSync(nudgesPath, JSON.stringify(current, null, 2));
472
+ });
473
+ }
474
+ catch {
475
+ /* best-effort: a lock failure must not throw out of a tick */
476
+ }
477
+ }
311
478
  writeJsonFile(path.join(dir, 'flags.json'), flags);
312
479
  const counts = {
313
480
  total: outcomes.length,
@@ -318,5 +485,13 @@ export async function runWatchdogTick(opts = {}) {
318
485
  };
319
486
  const result = { atMs: nowMs, didNudge: opts.nudge === true, outcomes, counts };
320
487
  writeJsonFile(path.join(dir, 'last-tick.json'), result);
488
+ // Heartbeat + decision/nudge events to the canonical watchdog.log the Factory
489
+ // Floor reads. Best-effort; never throws into the tick.
490
+ logEvents.push({
491
+ ts: nowMs,
492
+ kind: 'tick',
493
+ message: `${counts.total} live · ${counts.stalled} stalled · ${counts.nudged} nudged · ${counts.unaddressable} un-addressable`,
494
+ });
495
+ appendWatchdogEvents(logEvents, { logPath: opts.logPath });
321
496
  return result;
322
497
  }
@@ -33,7 +33,7 @@ export interface ClassifyInput {
33
33
  dormantMs: number;
34
34
  }
35
35
  export declare function classifyTerminal(input: ClassifyInput): StallStatus;
36
- export declare const WATCHDOG_SYSTEM_PROMPT = "You are a watchdog monitoring AI coding agents that run in terminals.\nFor each stalled terminal below, decide: NUDGE (send a short message to unstick it) or SKIP.\n\nNudge when:\n- The last assistant turn announced an action (\"I'll write X\", \"let me run Y\")\n but no matching tool call followed.\n- The agent appears stuck mid-task with no recent progress and the task is incomplete.\n\nSkip when:\n- The agent asked the user a direct question (waiting on human input).\n- The task looks complete.\n- You cannot tell what the agent is doing.\n\nNudge text must be:\n- One sentence, imperative (\"Show me the file.\", \"Run the tests.\").\n- No emojis. No apologies. Under 120 characters.\n\nRespond with ONLY a JSON array (no prose, no code fence):\n[{\"terminalId\":\"<id>\",\"action\":\"nudge\"|\"skip\",\"text\":\"<message or empty>\",\"reason\":\"<brief>\"}]";
36
+ export declare const WATCHDOG_SYSTEM_PROMPT = "You are the watchdog for AI coding agents running in terminals. These agents are\nexpected to DRIVE TO COMPLETION end-to-end. They too often stop to ask a needless\nquestion or pause with the task unfinished. For each stalled terminal below, decide\nNUDGE (send a message that unsticks it and pushes it to finish) or SKIP (it genuinely\nneeds the human).\n\nNUDGE when the agent could have continued on its own:\n- It asked permission for an obvious or already-authorized next step\n (\"should I proceed?\", \"want me to continue?\", \"shall I run the tests?\").\n- It asked a question it could answer itself from the available context or a\n reasonable default.\n- It announced an action (\"I'll run X\", \"let me write Y\") but no tool call followed.\n- It paused with the task incomplete and no real blocker.\nThe nudge text MUST: restate the goal, tell it to use best judgment and finish\nend-to-end WITHOUT asking again, and give ONE concrete hint \u2014 the specific next\nstep, a tool it forgot, or the sensible default to take. Imperative, 1-2 sentences,\nno emojis, under 200 characters.\n\nSKIP when the agent genuinely needs the human:\n- Credentials, auth, login, 2FA, or biometric.\n- An irreversible or outward-facing action that needs sign-off (force-push, delete\n prod data, publish/release, spend money, send an external message).\n- A real product or intent decision with genuine ambiguity (not a trivial default).\n- The task is actually complete.\n- You cannot tell what the agent is doing.\n\nRespond with ONLY a JSON array (no prose, no code fence):\n[{\"terminalId\":\"<id>\",\"action\":\"nudge\"|\"skip\",\"text\":\"<message or empty>\",\"reason\":\"<brief>\"}]";
37
37
  export declare function composePromptWithPlaybook(basePrompt: string, playbook: string): string;
38
38
  export declare function renderWatchdogPrompt(candidates: WatchdogCandidate[], playbook?: string): string;
39
39
  export declare function parseWatchdogResponse(stdout: string): Decision[];