@phnx-labs/agents-cli 1.20.74 → 1.20.77

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 (231) hide show
  1. package/CHANGELOG.md +1055 -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 +83 -13
  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 +286 -15
  26. package/dist/commands/secrets.js +102 -37
  27. package/dist/commands/sessions-browser.js +12 -2
  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.d.ts +14 -1
  32. package/dist/commands/sessions-picker.js +184 -16
  33. package/dist/commands/sessions.d.ts +56 -14
  34. package/dist/commands/sessions.js +322 -62
  35. package/dist/commands/setup-computer.js +2 -2
  36. package/dist/commands/ssh.d.ts +13 -0
  37. package/dist/commands/ssh.js +110 -37
  38. package/dist/commands/status.js +10 -2
  39. package/dist/commands/versions.js +15 -6
  40. package/dist/commands/view.d.ts +5 -0
  41. package/dist/commands/view.js +24 -4
  42. package/dist/commands/watchdog.d.ts +7 -2
  43. package/dist/commands/watchdog.js +73 -57
  44. package/dist/index.js +59 -7
  45. package/dist/lib/activity.d.ts +17 -6
  46. package/dist/lib/activity.js +466 -40
  47. package/dist/lib/actor.d.ts +50 -0
  48. package/dist/lib/actor.js +166 -0
  49. package/dist/lib/agent-spec/provider.js +2 -1
  50. package/dist/lib/agent-spec/resolve.js +19 -5
  51. package/dist/lib/agent-spec/types.d.ts +9 -1
  52. package/dist/lib/agents.d.ts +21 -0
  53. package/dist/lib/agents.js +70 -6
  54. package/dist/lib/cloud/codex.d.ts +2 -0
  55. package/dist/lib/cloud/codex.js +14 -3
  56. package/dist/lib/cloud/session-index.d.ts +32 -0
  57. package/dist/lib/cloud/session-index.js +58 -0
  58. package/dist/lib/cloud/store.d.ts +7 -0
  59. package/dist/lib/cloud/store.js +25 -0
  60. package/dist/lib/config-transfer.js +4 -0
  61. package/dist/lib/daemon.d.ts +10 -1
  62. package/dist/lib/daemon.js +114 -11
  63. package/dist/lib/devices/connect.d.ts +15 -1
  64. package/dist/lib/devices/connect.js +15 -1
  65. package/dist/lib/devices/fleet.d.ts +37 -1
  66. package/dist/lib/devices/fleet.js +36 -2
  67. package/dist/lib/devices/health-report.d.ts +38 -0
  68. package/dist/lib/devices/health-report.js +214 -0
  69. package/dist/lib/devices/health.js +4 -1
  70. package/dist/lib/devices/reachability.d.ts +31 -0
  71. package/dist/lib/devices/reachability.js +40 -0
  72. package/dist/lib/devices/registry.d.ts +33 -0
  73. package/dist/lib/devices/registry.js +37 -0
  74. package/dist/lib/devices/resolve-target.d.ts +15 -27
  75. package/dist/lib/devices/resolve-target.js +63 -102
  76. package/dist/lib/devices/sync.d.ts +18 -0
  77. package/dist/lib/devices/sync.js +23 -1
  78. package/dist/lib/devices/tailscale.d.ts +3 -0
  79. package/dist/lib/devices/tailscale.js +1 -0
  80. package/dist/lib/events.d.ts +1 -1
  81. package/dist/lib/exec.d.ts +82 -0
  82. package/dist/lib/exec.js +218 -6
  83. package/dist/lib/feed-post.d.ts +63 -0
  84. package/dist/lib/feed-post.js +204 -0
  85. package/dist/lib/feed.d.ts +7 -1
  86. package/dist/lib/feed.js +96 -6
  87. package/dist/lib/fleet/apply.d.ts +32 -2
  88. package/dist/lib/fleet/apply.js +97 -10
  89. package/dist/lib/fleet/types.d.ts +11 -0
  90. package/dist/lib/fs-walk.d.ts +13 -0
  91. package/dist/lib/fs-walk.js +16 -7
  92. package/dist/lib/heal.d.ts +7 -4
  93. package/dist/lib/heal.js +10 -22
  94. package/dist/lib/hooks.d.ts +1 -0
  95. package/dist/lib/hooks.js +156 -0
  96. package/dist/lib/hosts/dispatch.d.ts +16 -0
  97. package/dist/lib/hosts/dispatch.js +26 -4
  98. package/dist/lib/hosts/passthrough.js +9 -2
  99. package/dist/lib/hosts/reconnect.d.ts +78 -0
  100. package/dist/lib/hosts/reconnect.js +127 -0
  101. package/dist/lib/hosts/registry.d.ts +75 -14
  102. package/dist/lib/hosts/registry.js +205 -30
  103. package/dist/lib/hosts/remote-cmd.d.ts +11 -0
  104. package/dist/lib/hosts/remote-cmd.js +32 -8
  105. package/dist/lib/hosts/remote-session-id.d.ts +45 -0
  106. package/dist/lib/hosts/remote-session-id.js +84 -0
  107. package/dist/lib/hosts/run-target.d.ts +17 -5
  108. package/dist/lib/hosts/run-target.js +30 -10
  109. package/dist/lib/hosts/session-index.d.ts +18 -1
  110. package/dist/lib/hosts/session-index.js +40 -5
  111. package/dist/lib/hosts/session-marker.d.ts +33 -0
  112. package/dist/lib/hosts/session-marker.js +51 -0
  113. package/dist/lib/hosts/tasks.d.ts +8 -4
  114. package/dist/lib/import.d.ts +2 -0
  115. package/dist/lib/import.js +35 -2
  116. package/dist/lib/menubar/install-menubar.d.ts +29 -0
  117. package/dist/lib/menubar/install-menubar.js +59 -1
  118. package/dist/lib/menubar/notify-desktop.d.ts +44 -0
  119. package/dist/lib/menubar/notify-desktop.js +78 -0
  120. package/dist/lib/migrate.d.ts +16 -0
  121. package/dist/lib/migrate.js +36 -0
  122. package/dist/lib/models.d.ts +27 -0
  123. package/dist/lib/models.js +54 -1
  124. package/dist/lib/overdue.d.ts +6 -2
  125. package/dist/lib/overdue.js +10 -36
  126. package/dist/lib/picker.js +4 -1
  127. package/dist/lib/platform/process.d.ts +17 -0
  128. package/dist/lib/platform/process.js +70 -0
  129. package/dist/lib/profiles-presets.js +9 -7
  130. package/dist/lib/profiles.d.ts +31 -0
  131. package/dist/lib/profiles.js +70 -0
  132. package/dist/lib/pty-server.d.ts +2 -10
  133. package/dist/lib/pty-server.js +4 -38
  134. package/dist/lib/registry.d.ts +1 -1
  135. package/dist/lib/registry.js +48 -8
  136. package/dist/lib/rotate.d.ts +18 -0
  137. package/dist/lib/rotate.js +28 -0
  138. package/dist/lib/routine-notify.d.ts +76 -0
  139. package/dist/lib/routine-notify.js +190 -0
  140. package/dist/lib/routines-placement.d.ts +38 -0
  141. package/dist/lib/routines-placement.js +79 -0
  142. package/dist/lib/routines-project.d.ts +97 -0
  143. package/dist/lib/routines-project.js +349 -0
  144. package/dist/lib/routines.d.ts +94 -0
  145. package/dist/lib/routines.js +93 -9
  146. package/dist/lib/runner.d.ts +12 -2
  147. package/dist/lib/runner.js +242 -20
  148. package/dist/lib/sandbox.js +10 -0
  149. package/dist/lib/secrets/account-token.d.ts +20 -0
  150. package/dist/lib/secrets/account-token.js +64 -0
  151. package/dist/lib/secrets/agent.d.ts +74 -4
  152. package/dist/lib/secrets/agent.js +181 -107
  153. package/dist/lib/secrets/bundles.d.ts +54 -6
  154. package/dist/lib/secrets/bundles.js +230 -34
  155. package/dist/lib/secrets/index.d.ts +26 -3
  156. package/dist/lib/secrets/index.js +42 -12
  157. package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
  158. package/dist/lib/secrets/rc-hygiene.js +154 -0
  159. package/dist/lib/secrets/remote.d.ts +7 -4
  160. package/dist/lib/secrets/remote.js +7 -4
  161. package/dist/lib/secrets/session-store.d.ts +6 -2
  162. package/dist/lib/secrets/session-store.js +65 -15
  163. package/dist/lib/secrets/sync-commands.d.ts +21 -0
  164. package/dist/lib/secrets/sync-commands.js +21 -0
  165. package/dist/lib/secrets/unlock-hints.d.ts +27 -0
  166. package/dist/lib/secrets/unlock-hints.js +36 -0
  167. package/dist/lib/self-update.d.ts +23 -2
  168. package/dist/lib/self-update.js +86 -5
  169. package/dist/lib/session/active.d.ts +124 -7
  170. package/dist/lib/session/active.js +390 -56
  171. package/dist/lib/session/cloud.js +2 -1
  172. package/dist/lib/session/db.d.ts +54 -0
  173. package/dist/lib/session/db.js +320 -19
  174. package/dist/lib/session/detached.d.ts +30 -0
  175. package/dist/lib/session/detached.js +92 -0
  176. package/dist/lib/session/discover.d.ts +478 -3
  177. package/dist/lib/session/discover.js +1740 -472
  178. package/dist/lib/session/fork.js +2 -1
  179. package/dist/lib/session/hook-sessions.d.ts +43 -0
  180. package/dist/lib/session/hook-sessions.js +135 -0
  181. package/dist/lib/session/linear.d.ts +6 -0
  182. package/dist/lib/session/linear.js +55 -0
  183. package/dist/lib/session/migrate-targets.d.ts +65 -0
  184. package/dist/lib/session/migrate-targets.js +94 -0
  185. package/dist/lib/session/migrations.d.ts +37 -0
  186. package/dist/lib/session/migrations.js +60 -0
  187. package/dist/lib/session/parse.d.ts +18 -0
  188. package/dist/lib/session/parse.js +141 -32
  189. package/dist/lib/session/pid-registry.d.ts +34 -2
  190. package/dist/lib/session/pid-registry.js +49 -2
  191. package/dist/lib/session/prompt.d.ts +7 -0
  192. package/dist/lib/session/prompt.js +37 -0
  193. package/dist/lib/session/provenance.d.ts +7 -0
  194. package/dist/lib/session/render.d.ts +7 -2
  195. package/dist/lib/session/render.js +40 -26
  196. package/dist/lib/session/short-id.d.ts +17 -0
  197. package/dist/lib/session/short-id.js +20 -0
  198. package/dist/lib/session/state.d.ts +4 -0
  199. package/dist/lib/session/state.js +99 -13
  200. package/dist/lib/session/types.d.ts +25 -1
  201. package/dist/lib/session/types.js +8 -0
  202. package/dist/lib/shims.d.ts +1 -1
  203. package/dist/lib/shims.js +20 -6
  204. package/dist/lib/sqlite.d.ts +3 -2
  205. package/dist/lib/sqlite.js +27 -4
  206. package/dist/lib/staleness/detectors/commands.js +1 -1
  207. package/dist/lib/staleness/detectors/workflows.js +13 -2
  208. package/dist/lib/staleness/writers/commands.js +7 -7
  209. package/dist/lib/staleness/writers/workflows.d.ts +4 -2
  210. package/dist/lib/startup/command-registry.d.ts +1 -0
  211. package/dist/lib/startup/command-registry.js +3 -0
  212. package/dist/lib/state.d.ts +8 -4
  213. package/dist/lib/state.js +21 -37
  214. package/dist/lib/teams/agents.d.ts +19 -10
  215. package/dist/lib/teams/agents.js +40 -39
  216. package/dist/lib/types.d.ts +47 -2
  217. package/dist/lib/usage.d.ts +33 -1
  218. package/dist/lib/usage.js +172 -12
  219. package/dist/lib/versions.d.ts +9 -2
  220. package/dist/lib/versions.js +37 -8
  221. package/dist/lib/watchdog/log.d.ts +43 -0
  222. package/dist/lib/watchdog/log.js +69 -0
  223. package/dist/lib/watchdog/routine.d.ts +44 -0
  224. package/dist/lib/watchdog/routine.js +69 -0
  225. package/dist/lib/watchdog/runner.d.ts +51 -7
  226. package/dist/lib/watchdog/runner.js +239 -64
  227. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  228. package/dist/lib/watchdog/watchdog.js +31 -16
  229. package/dist/lib/workflows.d.ts +16 -0
  230. package/dist/lib/workflows.js +110 -1
  231. package/package.json +1 -1
@@ -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[];
@@ -64,23 +64,32 @@ export function classifyTerminal(input) {
64
64
  }
65
65
  return { kind: 'stalled', stalledForMs: age };
66
66
  }
67
- export const WATCHDOG_SYSTEM_PROMPT = `You are a watchdog monitoring AI coding agents that run in terminals.
68
- For each stalled terminal below, decide: NUDGE (send a short message to unstick it) or SKIP.
67
+ export const WATCHDOG_SYSTEM_PROMPT = `You are the watchdog for AI coding agents running in terminals. These agents are
68
+ expected to DRIVE TO COMPLETION end-to-end. They too often stop to ask a needless
69
+ question or pause with the task unfinished. For each stalled terminal below, decide
70
+ NUDGE (send a message that unsticks it and pushes it to finish) or SKIP (it genuinely
71
+ needs the human).
69
72
 
70
- Nudge when:
71
- - The last assistant turn announced an action ("I'll write X", "let me run Y")
72
- but no matching tool call followed.
73
- - The agent appears stuck mid-task with no recent progress and the task is incomplete.
73
+ NUDGE when the agent could have continued on its own:
74
+ - It asked permission for an obvious or already-authorized next step
75
+ ("should I proceed?", "want me to continue?", "shall I run the tests?").
76
+ - It asked a question it could answer itself from the available context or a
77
+ reasonable default.
78
+ - It announced an action ("I'll run X", "let me write Y") but no tool call followed.
79
+ - It paused with the task incomplete and no real blocker.
80
+ The nudge text MUST: restate the goal, tell it to use best judgment and finish
81
+ end-to-end WITHOUT asking again, and give ONE concrete hint — the specific next
82
+ step, a tool it forgot, or the sensible default to take. Imperative, 1-2 sentences,
83
+ no emojis, under 200 characters.
74
84
 
75
- Skip when:
76
- - The agent asked the user a direct question (waiting on human input).
77
- - The task looks complete.
85
+ SKIP when the agent genuinely needs the human:
86
+ - Credentials, auth, login, 2FA, or biometric.
87
+ - An irreversible or outward-facing action that needs sign-off (force-push, delete
88
+ prod data, publish/release, spend money, send an external message).
89
+ - A real product or intent decision with genuine ambiguity (not a trivial default).
90
+ - The task is actually complete.
78
91
  - You cannot tell what the agent is doing.
79
92
 
80
- Nudge text must be:
81
- - One sentence, imperative ("Show me the file.", "Run the tests.").
82
- - No emojis. No apologies. Under 120 characters.
83
-
84
93
  Respond with ONLY a JSON array (no prose, no code fence):
85
94
  [{"terminalId":"<id>","action":"nudge"|"skip","text":"<message or empty>","reason":"<brief>"}]`;
86
95
  // User-editable playbook appended below the built-in prompt. The user maintains
@@ -137,15 +146,21 @@ export function parseWatchdogResponse(stdout) {
137
146
  return decisions;
138
147
  }
139
148
  export function isLikelyTrulyBlocked(candidate) {
140
- if (candidate.stalledForMs >= FORCE_REVIEW_STALL_MS)
141
- return true;
149
+ // With no tail to reason over, only a very long stall counts as blocked.
142
150
  if (candidate.tailLines.length === 0)
143
- return false;
151
+ return candidate.stalledForMs >= FORCE_REVIEW_STALL_MS;
144
152
  const lowerTail = candidate.tailLines.join('\n').toLowerCase();
153
+ // Waiting-on-user and completion hints are checked BEFORE the 15m force-review
154
+ // short-circuit — a long-idle OPEN QUESTION must defer (not be blindly
155
+ // force-nudged) and a finished task is done. The old order tested stall age
156
+ // first, so a 15m-60m idle session whose tail said "waiting on user" / "done"
157
+ // was force-nudged anyway. Precedence fixed here (watchdog-brain-v2).
145
158
  if (WAITING_HINTS.some((hint) => lowerTail.includes(hint)))
146
159
  return false;
147
160
  if (COMPLETION_HINTS.some((hint) => lowerTail.includes(hint)))
148
161
  return false;
162
+ if (candidate.stalledForMs >= FORCE_REVIEW_STALL_MS)
163
+ return true;
149
164
  if (BLOCKED_HINTS.some((hint) => lowerTail.includes(hint)))
150
165
  return true;
151
166
  let sawToolAfter = false;
@@ -296,6 +296,22 @@ export declare function transformWorkflowForKimi(workflowPath: string, name: str
296
296
  export declare function transformWorkflowForAntigravity(workflowPath: string, name: string): string;
297
297
  /** Convert a canonical agents-cli workflow bundle into an OpenClaw Lobster file. */
298
298
  export declare function transformWorkflowForOpenClaw(workflowPath: string, name: string): string;
299
+ /** Marker comment prefix written into agents-cli-managed Grok `.rhai` files. */
300
+ export declare const GROK_WORKFLOW_MARKER = "agents_workflow";
301
+ /**
302
+ * Convert a canonical agents-cli workflow bundle into a Grok native Rhai
303
+ * workflow script. Grok discovers saved workflows as
304
+ * `~/.grok/workflows/<name>.rhai` (and project `.grok/workflows/`) and exposes
305
+ * each as a `/<name>` slash command (enabled by default since v0.2.111).
306
+ *
307
+ * The projection is a single-agent orchestrator that feeds the WORKFLOW.md
308
+ * body as the agent prompt plus the caller's `args.prompt` (or string args).
309
+ * Multi-phase fan-out is left to hand-authored Rhai — agents-cli's job is to
310
+ * land the orchestrator instructions in the native path.
311
+ */
312
+ export declare function transformWorkflowForGrok(workflowPath: string, name: string): string;
313
+ /** Read the agents_workflow marker from a Grok `.rhai` file, if present. */
314
+ export declare function grokWorkflowMarker(filePath: string): string | null;
299
315
  /**
300
316
  * Resolve an `agents run <workflow>` reference.
301
317
  *