@phnx-labs/agents-cli 1.22.46 → 1.22.47

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 (167) hide show
  1. package/CHANGELOG.md +279 -0
  2. package/LICENSE +102 -182
  3. package/README.md +31 -18
  4. package/dist/cli/command-registry.d.ts +1 -1
  5. package/dist/cli/command-registry.js +4 -2
  6. package/dist/commands/accounts.js +56 -12
  7. package/dist/commands/auth.js +7 -5
  8. package/dist/commands/browser-sessions-picker.js +9 -7
  9. package/dist/commands/browser.js +266 -15
  10. package/dist/commands/config.js +26 -4
  11. package/dist/commands/cost.d.ts +1 -1
  12. package/dist/commands/detach.d.ts +13 -0
  13. package/dist/commands/detach.js +11 -8
  14. package/dist/commands/exec.js +55 -38
  15. package/dist/commands/feed-watch.d.ts +2 -0
  16. package/dist/commands/feed-watch.js +28 -0
  17. package/dist/commands/feed.js +27 -0
  18. package/dist/commands/feedback.js +3 -19
  19. package/dist/commands/insights.d.ts +1 -1
  20. package/dist/commands/insights.js +2 -2
  21. package/dist/commands/lease.js +3 -3
  22. package/dist/commands/routines.js +1 -1
  23. package/dist/commands/sessions-picker-factory.d.ts +2 -1
  24. package/dist/commands/sessions-picker-factory.js +1 -1
  25. package/dist/commands/sessions-stop.d.ts +24 -0
  26. package/dist/commands/sessions-stop.js +66 -0
  27. package/dist/commands/sessions-trace.d.ts +82 -0
  28. package/dist/commands/sessions-trace.js +387 -0
  29. package/dist/commands/sessions.js +4 -0
  30. package/dist/commands/ssh.d.ts +15 -0
  31. package/dist/commands/ssh.js +194 -16
  32. package/dist/commands/teams.js +34 -2
  33. package/dist/commands/view.d.ts +2 -0
  34. package/dist/commands/view.js +18 -31
  35. package/dist/commands/watchdog.js +9 -14
  36. package/dist/lib/account-capabilities.d.ts +4 -0
  37. package/dist/lib/account-capabilities.js +13 -9
  38. package/dist/lib/account-registry.d.ts +2 -0
  39. package/dist/lib/account-registry.js +30 -2
  40. package/dist/lib/accounting/rotate.d.ts +9 -0
  41. package/dist/lib/accounting/rotate.js +12 -2
  42. package/dist/lib/accounting/usage.d.ts +70 -3
  43. package/dist/lib/accounting/usage.js +235 -63
  44. package/dist/lib/agent-modes.js +29 -1
  45. package/dist/lib/agent-spec/agents.d.ts +4 -3
  46. package/dist/lib/agent-spec/agents.js +14 -6
  47. package/dist/lib/analytics/mix-commands.d.ts +1 -1
  48. package/dist/lib/analytics/mix-commands.js +2 -2
  49. package/dist/lib/auth-health.d.ts +13 -3
  50. package/dist/lib/auth-health.js +50 -8
  51. package/dist/lib/browser/devices.d.ts +19 -0
  52. package/dist/lib/browser/devices.js +23 -0
  53. package/dist/lib/browser/ipc.d.ts +1 -1
  54. package/dist/lib/browser/ipc.js +33 -7
  55. package/dist/lib/browser/profiles.d.ts +106 -1
  56. package/dist/lib/browser/profiles.js +300 -17
  57. package/dist/lib/browser/remote-control.d.ts +26 -2
  58. package/dist/lib/browser/remote-control.js +33 -3
  59. package/dist/lib/browser/runtime-state.d.ts +9 -0
  60. package/dist/lib/browser/runtime-state.js +24 -8
  61. package/dist/lib/browser/service.d.ts +37 -0
  62. package/dist/lib/browser/service.js +78 -20
  63. package/dist/lib/browser/sessions-list.d.ts +1 -3
  64. package/dist/lib/browser/sessions-list.js +3 -16
  65. package/dist/lib/browser/types.d.ts +12 -1
  66. package/dist/lib/codex-policy.d.ts +11 -1
  67. package/dist/lib/codex-policy.js +20 -5
  68. package/dist/lib/config-keys.d.ts +4 -3
  69. package/dist/lib/config-keys.js +17 -7
  70. package/dist/lib/daemon/runner.js +15 -2
  71. package/dist/lib/daemon-ticks.js +2 -2
  72. package/dist/lib/device-config.d.ts +25 -8
  73. package/dist/lib/device-config.js +90 -17
  74. package/dist/lib/devices/config-migration.d.ts +7 -0
  75. package/dist/lib/devices/config-migration.js +67 -3
  76. package/dist/lib/devices/health.d.ts +27 -2
  77. package/dist/lib/devices/health.js +39 -9
  78. package/dist/lib/devices/interactive-host.d.ts +15 -0
  79. package/dist/lib/devices/interactive-host.js +53 -0
  80. package/dist/lib/devices/registry.d.ts +61 -4
  81. package/dist/lib/devices/registry.js +94 -38
  82. package/dist/lib/devices/stats-cache.d.ts +6 -0
  83. package/dist/lib/devices/stats-cache.js +10 -2
  84. package/dist/lib/exec.d.ts +5 -3
  85. package/dist/lib/exec.js +32 -16
  86. package/dist/lib/feed/answer.d.ts +29 -0
  87. package/dist/lib/feed/answer.js +145 -0
  88. package/dist/lib/feed/attention.d.ts +83 -0
  89. package/dist/lib/feed/attention.js +0 -0
  90. package/dist/lib/feed/feed.d.ts +109 -0
  91. package/dist/lib/feed/feed.js +154 -3
  92. package/dist/lib/feed/pr-status.d.ts +14 -0
  93. package/dist/lib/feed/pr-status.js +47 -0
  94. package/dist/lib/feed/watch.d.ts +59 -0
  95. package/dist/lib/feed/watch.js +180 -0
  96. package/dist/lib/fleet/capture.js +6 -0
  97. package/dist/lib/fleet/remote-login.js +8 -3
  98. package/dist/lib/fleet/types.d.ts +17 -0
  99. package/dist/lib/harness/adapter.d.ts +11 -0
  100. package/dist/lib/harness/adapters/claude.js +38 -24
  101. package/dist/lib/harness/adapters/codex.js +2 -4
  102. package/dist/lib/hooks/cache.js +8 -0
  103. package/dist/lib/hooks/match.d.ts +3 -0
  104. package/dist/lib/hooks/match.js +25 -0
  105. package/dist/lib/hosts/passthrough.js +16 -0
  106. package/dist/lib/hosts/registry.js +7 -0
  107. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  108. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  109. package/dist/lib/menubar/install-menubar.d.ts +13 -0
  110. package/dist/lib/menubar/install-menubar.js +26 -2
  111. package/dist/lib/open-url.d.ts +60 -2
  112. package/dist/lib/open-url.js +167 -12
  113. package/dist/lib/refresh-coordinator.d.ts +1 -1
  114. package/dist/lib/refresh-coordinator.js +1 -1
  115. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  116. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
  117. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  118. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  119. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
  120. package/dist/lib/session/active.d.ts +67 -0
  121. package/dist/lib/session/active.js +69 -1
  122. package/dist/lib/session/db.js +4 -2
  123. package/dist/lib/session/discover.d.ts +2 -0
  124. package/dist/lib/session/discover.js +12 -6
  125. package/dist/lib/session/prompt.d.ts +35 -0
  126. package/dist/lib/session/prompt.js +56 -0
  127. package/dist/lib/session/remote/watch.d.ts +1 -1
  128. package/dist/lib/session/remote/watch.js +6 -1
  129. package/dist/lib/session/render.js +7 -3
  130. package/dist/lib/session/session-cache.js +5 -1
  131. package/dist/lib/session/trajectory-compare.d.ts +47 -0
  132. package/dist/lib/session/trajectory-compare.js +116 -0
  133. package/dist/lib/session/trajectory-html.d.ts +15 -0
  134. package/dist/lib/session/trajectory-html.js +750 -0
  135. package/dist/lib/session/trajectory-lineage.d.ts +92 -0
  136. package/dist/lib/session/trajectory-lineage.js +195 -0
  137. package/dist/lib/session/trajectory-text.d.ts +37 -0
  138. package/dist/lib/session/trajectory-text.js +277 -0
  139. package/dist/lib/session/trajectory.d.ts +102 -0
  140. package/dist/lib/session/trajectory.js +335 -0
  141. package/dist/lib/startup/command-registry.d.ts +3 -1
  142. package/dist/lib/startup/command-registry.js +5 -2
  143. package/dist/lib/state.d.ts +1 -1
  144. package/dist/lib/state.js +4 -2
  145. package/dist/lib/teams/agents.js +9 -0
  146. package/dist/lib/terminal/inject.d.ts +19 -0
  147. package/dist/lib/terminal/inject.js +19 -9
  148. package/dist/lib/tmux/session.d.ts +46 -13
  149. package/dist/lib/tmux/session.js +173 -27
  150. package/dist/lib/types.d.ts +16 -0
  151. package/dist/lib/usage-backoff.d.ts +2 -1
  152. package/dist/lib/usage-backoff.js +60 -19
  153. package/dist/lib/usage-refresh.d.ts +17 -3
  154. package/dist/lib/usage-refresh.js +57 -11
  155. package/dist/lib/watchdog/log.d.ts +1 -1
  156. package/dist/lib/watchdog/log.js +1 -1
  157. package/dist/lib/watchdog/runner.d.ts +36 -45
  158. package/dist/lib/watchdog/runner.js +150 -119
  159. package/dist/lib/watchdog/service.d.ts +0 -1
  160. package/dist/lib/watchdog/service.js +0 -1
  161. package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
  162. package/dist/lib/watchdog/watchdog-agent.js +59 -0
  163. package/dist/lib/watchdog/watchdog.d.ts +11 -2
  164. package/dist/lib/watchdog/watchdog.js +37 -104
  165. package/package.json +2 -2
  166. package/dist/commands/usage.d.ts +0 -27
  167. package/dist/commands/usage.js +0 -96
@@ -3,7 +3,6 @@ import { type WatchdogThresholds, type WatchdogTickResult } from './runner.js';
3
3
  export interface WatchdogPassOptions {
4
4
  nudge: boolean;
5
5
  nudgeText?: string;
6
- smart?: boolean;
7
6
  smartAgent?: string;
8
7
  thresholds?: WatchdogThresholds;
9
8
  allowGhosttyFocus?: boolean;
@@ -26,7 +26,6 @@ export async function runWatchdogPass(opts) {
26
26
  const result = await runWatchdogTick({
27
27
  nudge: opts.nudge,
28
28
  nudgeText: opts.nudgeText,
29
- smart: opts.smart,
30
29
  smartAgent: opts.smartAgent,
31
30
  thresholds: opts.thresholds,
32
31
  allowGhosttyFocus: opts.allowGhosttyFocus,
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The watchdog AGENT — the whole decider, in ONE call per tick.
3
+ *
4
+ * The watchdog does not use a heuristic script to guess idle-vs-unfinished. It
5
+ * hands every idle session's task + tail to an agent (via `agents run <target>
6
+ * --mode plan`, read-only) and asks it, for each, whether it is idle-but-unfinished
7
+ * (→ nudge) or idle-and-done / needs-human (→ skip). The whole idle set goes in ONE
8
+ * invocation, so the cost is one bounded plan-mode call per tick regardless of how
9
+ * many sessions are idle — not one agent per session, and only when something is
10
+ * actually idle (the caller does not invoke this with an empty list).
11
+ *
12
+ * A resolved `watchdog` workflow (repo > user > system, via resolveWorkflowRef)
13
+ * runs by name so its WORKFLOW.md body + `model:` frontmatter apply; otherwise the
14
+ * bare agent runs the built-in WATCHDOG_SYSTEM_PROMPT. Best-effort: any failure
15
+ * (agent unavailable, timeout, no verdict) yields an empty map, and the caller
16
+ * treats an unlisted terminal as a SAFE skip — never a blind nudge.
17
+ */
18
+ import { type WatchdogCandidate, type Decision } from './watchdog.js';
19
+ /** Judge every idle candidate at once; returns decisions keyed by terminalId. */
20
+ export type WatchdogAgentDecider = (candidates: WatchdogCandidate[]) => Promise<Map<string, Decision>>;
21
+ /**
22
+ * Runs the agent once and returns its raw stdout. Injectable so a test can assert
23
+ * the ONE-call-per-tick property and the assembled prompt without shelling out.
24
+ * `runTarget` is the resolved `agents run` target (a `watchdog` workflow or the
25
+ * bare agent id).
26
+ */
27
+ export type WatchdogAgentRunner = (runTarget: string, prompt: string) => Promise<string>;
28
+ /**
29
+ * The default agent decider. `agent` is the harness the built-in prompt runs as
30
+ * (default 'claude'); `workflowCwd` is where a `watchdog` workflow override is
31
+ * resolved from (the daemon's cwd — the batch spans many projects, so there is no
32
+ * single per-session cwd to key on); `run` is the injectable subprocess seam
33
+ * (tests pass a synthetic one). The whole idle set goes to the runner in ONE call.
34
+ */
35
+ export declare function makeWatchdogAgentDecider(agent: string, opts?: {
36
+ workflowCwd?: string;
37
+ run?: WatchdogAgentRunner;
38
+ }): WatchdogAgentDecider;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The watchdog AGENT — the whole decider, in ONE call per tick.
3
+ *
4
+ * The watchdog does not use a heuristic script to guess idle-vs-unfinished. It
5
+ * hands every idle session's task + tail to an agent (via `agents run <target>
6
+ * --mode plan`, read-only) and asks it, for each, whether it is idle-but-unfinished
7
+ * (→ nudge) or idle-and-done / needs-human (→ skip). The whole idle set goes in ONE
8
+ * invocation, so the cost is one bounded plan-mode call per tick regardless of how
9
+ * many sessions are idle — not one agent per session, and only when something is
10
+ * actually idle (the caller does not invoke this with an empty list).
11
+ *
12
+ * A resolved `watchdog` workflow (repo > user > system, via resolveWorkflowRef)
13
+ * runs by name so its WORKFLOW.md body + `model:` frontmatter apply; otherwise the
14
+ * bare agent runs the built-in WATCHDOG_SYSTEM_PROMPT. Best-effort: any failure
15
+ * (agent unavailable, timeout, no verdict) yields an empty map, and the caller
16
+ * treats an unlisted terminal as a SAFE skip — never a blind nudge.
17
+ */
18
+ import { renderWatchdogPrompt, parseWatchdogResponse } from './watchdog.js';
19
+ /** The real runner: one `agents run <target> --mode plan <prompt>` subprocess. */
20
+ async function defaultAgentRunner(runTarget, prompt) {
21
+ const [{ execFile }, { promisify }] = await Promise.all([import('child_process'), import('util')]);
22
+ const execFileAsync = promisify(execFile);
23
+ const { stdout } = await execFileAsync('agents', ['run', runTarget, '--mode', 'plan', prompt], {
24
+ encoding: 'utf8',
25
+ maxBuffer: 4 * 1024 * 1024,
26
+ timeout: 120_000,
27
+ });
28
+ return stdout;
29
+ }
30
+ /**
31
+ * The default agent decider. `agent` is the harness the built-in prompt runs as
32
+ * (default 'claude'); `workflowCwd` is where a `watchdog` workflow override is
33
+ * resolved from (the daemon's cwd — the batch spans many projects, so there is no
34
+ * single per-session cwd to key on); `run` is the injectable subprocess seam
35
+ * (tests pass a synthetic one). The whole idle set goes to the runner in ONE call.
36
+ */
37
+ export function makeWatchdogAgentDecider(agent, opts = {}) {
38
+ return async (candidates) => {
39
+ const result = new Map();
40
+ if (candidates.length === 0)
41
+ return result;
42
+ try {
43
+ const { resolveWorkflowRef } = await import('../workflows.js');
44
+ const cwd = opts.workflowCwd || process.cwd();
45
+ const workflowPath = resolveWorkflowRef('watchdog', cwd);
46
+ const runTarget = workflowPath ? 'watchdog' : agent;
47
+ const prompt = renderWatchdogPrompt(candidates);
48
+ const run = opts.run ?? defaultAgentRunner;
49
+ const stdout = await run(runTarget, prompt);
50
+ for (const d of parseWatchdogResponse(stdout))
51
+ result.set(d.terminalId, d);
52
+ }
53
+ catch {
54
+ // Agent unavailable / timed out — return what we have (possibly empty); the
55
+ // caller safe-skips any terminal with no verdict. Never a blind nudge.
56
+ }
57
+ return result;
58
+ };
59
+ }
@@ -3,12 +3,22 @@ export interface WatchdogCandidate {
3
3
  agentType: 'claude' | 'codex' | 'gemini';
4
4
  tailLines: string[];
5
5
  stalledForMs: number;
6
+ /** The originating task / first prompt / topic — so the agent can judge "was given a task but hasn't finished it". */
7
+ task?: string;
8
+ /** Working directory of the session, for context. */
9
+ cwd?: string;
6
10
  }
7
11
  export interface Decision {
8
12
  terminalId: string;
9
13
  action: 'nudge' | 'skip';
10
14
  text: string;
11
15
  reason: string;
16
+ /**
17
+ * Set by the agent on a SKIP to distinguish "genuinely needs the human"
18
+ * (true → surface it) from "the task is actually done" (false/absent → leave
19
+ * it alone, do not poke). `done` is a distinct terminal state from `idle`.
20
+ */
21
+ needsHuman?: boolean;
12
22
  }
13
23
  export type StallStatus = {
14
24
  kind: 'active';
@@ -33,8 +43,7 @@ export interface ClassifyInput {
33
43
  dormantMs: number;
34
44
  }
35
45
  export declare function classifyTerminal(input: ClassifyInput): StallStatus;
36
- export declare const WATCHDOG_SYSTEM_PROMPT = "You are the watchdog for AI coding agents running in terminals. Your one job is to get\nIDLE agents moving to completion: each agent has a goal and is expected to DRIVE TO\nCOMPLETION end-to-end, but the terminals below have gone idle. Read each one's goal and\nWHY it stopped, then decide NUDGE (send a message that unsticks it and drives it to\nfinish) or SKIP (it genuinely needs the human).\n\nRead the transcript before judging \u2014 an agent that already reached a decision needs \"do\nit,\" not \"decide.\"\n\nNUDGE when the agent went idle and could keep going 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, or by using a tool it already has.\n- It announced an action (\"I'll run X\", \"let me write Y\") but no tool call followed.\n- It already decided what to do, then stalled without doing it.\n- It paused with the task incomplete and no real blocker.\n\nThe nudge text MUST carry context, not shove:\n- Restate the goal and reference the conclusion the agent ALREADY reached.\n- Give ONE concrete next step \u2014 the specific action, the sensible default, or a TOOL it\n forgot it has (e.g. \"agents computer\" to drive the Mac, \"agents browser\" for the web,\n \"agents ssh <mac> \\\"agents computer \u2026\\\"\" to drive a Mac from another box).\n- Split the ask: drive the reversible, goal-advancing part now; flag only a genuinely\n disruptive sub-step for the human.\n- Tell it to use best judgment and finish end-to-end WITHOUT asking again.\n- Imperative, 1-2 sentences, no emojis, under 240 characters.\n\nSKIP when the agent genuinely needs the human (these belong in the user's feed, not a\nnudge):\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) \u2014 UNLESS the House\n Rules below authorize it.\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>\"}]";
46
+ export declare const WATCHDOG_SYSTEM_PROMPT = "You are the watchdog for AI coding agents running in terminals. You are given the idle\nsessions on this machine \u2014 each with its originating TASK, how long it has been idle, and\nthe tail of its transcript. Your one job is to tell, for each one, whether it is\nIDLE-BUT-UNFINISHED (it was given a task, went quiet, and has NOT finished or handed it\noff) or IDLE-AND-DONE (it finished, or it genuinely needs the human). Idle-but-unfinished\nis the dangerous state \u2014 the work is most likely to be silently abandoned \u2014 so those get a\nNUDGE that drives them to finish. Everything else is a SKIP.\n\nRead each transcript before judging \u2014 an agent that already reached a decision needs \"do\nit,\" not \"decide.\" Judge from the task + tail, not from keywords.\n\nNUDGE when the agent went idle and could keep going 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, or by using a tool it already has.\n- It announced an action (\"I'll run X\", \"let me write Y\") but no tool call followed.\n- It already decided what to do, then stalled without doing it.\n- It paused with the task incomplete and no real blocker.\n\nThe nudge text MUST carry context, not shove:\n- Restate the goal and reference the conclusion the agent ALREADY reached.\n- Give ONE concrete next step \u2014 the specific action, the sensible default, or a TOOL it\n forgot it has (e.g. \"agents computer\" to drive the Mac, \"agents browser\" for the web,\n \"agents ssh <mac> \\\"agents computer \u2026\\\"\" to drive a Mac from another box).\n- Split the ask: drive the reversible, goal-advancing part now; flag only a genuinely\n disruptive sub-step for the human.\n- Tell it to use best judgment and finish end-to-end WITHOUT asking again.\n- Imperative, 1-2 sentences, no emojis, under 240 characters.\n\nSKIP in two distinct cases \u2014 mark which with \"needsHuman\":\n- needsHuman=true \u2014 the agent is genuinely blocked on a human (these belong in the user's\n feed): credentials, auth, login, 2FA, or biometric; an irreversible or outward-facing\n action needing sign-off (force-push, delete prod data, publish/release, spend money,\n send an external message) UNLESS the House Rules below authorize it; a real product or\n intent decision with genuine ambiguity (not a trivial default); or you cannot tell what\n the agent is doing.\n- needsHuman=false \u2014 the task is actually complete (idle-and-done). Leave it alone; do NOT\n poke a finished session.\n\nRespond with ONLY a JSON array (no prose, no code fence). Include \"needsHuman\" on every\nskip:\n[{\"terminalId\":\"<id>\",\"action\":\"nudge\"|\"skip\",\"text\":\"<message or empty>\",\"reason\":\"<brief>\",\"needsHuman\":true|false}]";
37
47
  export declare function composePromptWithPlaybook(basePrompt: string, playbook: string): string;
38
48
  export declare function renderWatchdogPrompt(candidates: WatchdogCandidate[], playbook?: string): string;
39
49
  export declare function parseWatchdogResponse(stdout: string): Decision[];
40
- export declare function isLikelyTrulyBlocked(candidate: WatchdogCandidate): boolean;
@@ -1,53 +1,9 @@
1
- // Watchdog: pure logic for detecting stalled agent terminals and rendering
2
- // prompts to a headless decider instance that decides whether to nudge them.
3
- // Ported from Swarmify (extension/src/core/watchdog.ts) behavior verbatim.
4
- // Terminal/session delivery lives elsewhere; this module reads no files and
5
- // touches no host APIs so it can be unit-tested in isolation.
6
- const FORCE_REVIEW_STALL_MS = 15 * 60 * 1000;
7
- const BLOCKED_HINTS = [
8
- 'blocked',
9
- 'stuck',
10
- "can't",
11
- 'cannot',
12
- 'unable',
13
- 'failed',
14
- 'error',
15
- 'exception',
16
- 'traceback',
17
- 'timed out',
18
- 'timeout',
19
- 'rate limit',
20
- 'permission denied',
21
- ];
22
- const WAITING_HINTS = [
23
- 'waiting on user',
24
- 'awaiting user',
25
- 'askuserquestion',
26
- ];
27
- const COMPLETION_HINTS = [
28
- 'done',
29
- 'completed',
30
- 'all set',
31
- 'finished',
32
- ];
33
- const TOOL_CALL_HINTS = [
34
- '"type":"tool_use"',
35
- '"type":"tool_call"',
36
- '"type":"function_call"',
37
- ];
38
- const ASSISTANT_LINE_HINTS = [
39
- '"type":"assistant"',
40
- '"role":"assistant"',
41
- '"payload":{"type":"message"',
42
- ];
43
- const PROMISE_HINTS = [
44
- "i'll",
45
- 'i will',
46
- 'let me',
47
- 'going to',
48
- "next i'll",
49
- 'next i will',
50
- ];
1
+ // Watchdog: pure logic for detecting stalled agent terminals and rendering the
2
+ // prompt that the watchdog AGENT reads to decide idle-vs-unfinished and craft a
3
+ // nudge. There is deliberately no heuristic decider here (no regex over the tail
4
+ // guessing "done" vs "stuck") that judgment is the agent's job. This module
5
+ // only classifies idleness by timestamp and renders/parses the agent's I/O, so
6
+ // it reads no files and touches no host APIs and can be unit-tested in isolation.
51
7
  export function classifyTerminal(input) {
52
8
  if (input.optedOut)
53
9
  return { kind: 'opted_out' };
@@ -64,14 +20,16 @@ export function classifyTerminal(input) {
64
20
  }
65
21
  return { kind: 'stalled', stalledForMs: age };
66
22
  }
67
- export const WATCHDOG_SYSTEM_PROMPT = `You are the watchdog for AI coding agents running in terminals. Your one job is to get
68
- IDLE agents moving to completion: each agent has a goal and is expected to DRIVE TO
69
- COMPLETION end-to-end, but the terminals below have gone idle. Read each one's goal and
70
- WHY it stopped, then decide NUDGE (send a message that unsticks it and drives it to
71
- finish) or SKIP (it genuinely needs the human).
23
+ export const WATCHDOG_SYSTEM_PROMPT = `You are the watchdog for AI coding agents running in terminals. You are given the idle
24
+ sessions on this machine each with its originating TASK, how long it has been idle, and
25
+ the tail of its transcript. Your one job is to tell, for each one, whether it is
26
+ IDLE-BUT-UNFINISHED (it was given a task, went quiet, and has NOT finished or handed it
27
+ off) or IDLE-AND-DONE (it finished, or it genuinely needs the human). Idle-but-unfinished
28
+ is the dangerous state — the work is most likely to be silently abandoned — so those get a
29
+ NUDGE that drives them to finish. Everything else is a SKIP.
72
30
 
73
- Read the transcript before judging — an agent that already reached a decision needs "do
74
- it," not "decide."
31
+ Read each transcript before judging — an agent that already reached a decision needs "do
32
+ it," not "decide." Judge from the task + tail, not from keywords.
75
33
 
76
34
  NUDGE when the agent went idle and could keep going on its own:
77
35
  - It asked permission for an obvious or already-authorized next step
@@ -92,18 +50,19 @@ The nudge text MUST carry context, not shove:
92
50
  - Tell it to use best judgment and finish end-to-end WITHOUT asking again.
93
51
  - Imperative, 1-2 sentences, no emojis, under 240 characters.
94
52
 
95
- SKIP when the agent genuinely needs the human (these belong in the user's feed, not a
96
- nudge):
97
- - Credentials, auth, login, 2FA, or biometric.
98
- - An irreversible or outward-facing action that needs sign-off (force-push, delete
99
- prod data, publish/release, spend money, send an external message) UNLESS the House
100
- Rules below authorize it.
101
- - A real product or intent decision with genuine ambiguity (not a trivial default).
102
- - The task is actually complete.
103
- - You cannot tell what the agent is doing.
53
+ SKIP in two distinct cases mark which with "needsHuman":
54
+ - needsHuman=true — the agent is genuinely blocked on a human (these belong in the user's
55
+ feed): credentials, auth, login, 2FA, or biometric; an irreversible or outward-facing
56
+ action needing sign-off (force-push, delete prod data, publish/release, spend money,
57
+ send an external message) UNLESS the House Rules below authorize it; a real product or
58
+ intent decision with genuine ambiguity (not a trivial default); or you cannot tell what
59
+ the agent is doing.
60
+ - needsHuman=false — the task is actually complete (idle-and-done). Leave it alone; do NOT
61
+ poke a finished session.
104
62
 
105
- Respond with ONLY a JSON array (no prose, no code fence):
106
- [{"terminalId":"<id>","action":"nudge"|"skip","text":"<message or empty>","reason":"<brief>"}]`;
63
+ Respond with ONLY a JSON array (no prose, no code fence). Include "needsHuman" on every
64
+ skip:
65
+ [{"terminalId":"<id>","action":"nudge"|"skip","text":"<message or empty>","reason":"<brief>","needsHuman":true|false}]`;
107
66
  // User-editable playbook appended below the built-in prompt. The user maintains
108
67
  // the source at ~/.agents/playbooks/watchdog.md (read by the delivery layer);
109
68
  // this function is pure so it can be tested without filesystem access.
@@ -115,10 +74,14 @@ export function composePromptWithPlaybook(basePrompt, playbook) {
115
74
  }
116
75
  export function renderWatchdogPrompt(candidates, playbook = '') {
117
76
  const systemPrompt = composePromptWithPlaybook(WATCHDOG_SYSTEM_PROMPT, playbook);
118
- const parts = [systemPrompt, '', 'STALLED TERMINALS:', ''];
77
+ const parts = [systemPrompt, '', 'IDLE SESSIONS:', ''];
119
78
  for (const c of candidates) {
120
79
  const seconds = Math.round(c.stalledForMs / 1000);
121
80
  parts.push(`--- terminal ${c.terminalId} (${c.agentType}, idle ${seconds}s) ---`);
81
+ if (c.task)
82
+ parts.push(`task: ${c.task}`);
83
+ if (c.cwd)
84
+ parts.push(`cwd: ${c.cwd}`);
122
85
  parts.push('last JSONL lines:');
123
86
  for (const line of c.tailLines) {
124
87
  parts.push(line);
@@ -153,41 +116,11 @@ export function parseWatchdogResponse(stdout) {
153
116
  const reason = typeof obj.reason === 'string' ? obj.reason : '';
154
117
  if (!terminalId || !action)
155
118
  continue;
156
- decisions.push({ terminalId, action, text, reason });
119
+ // needsHuman only meaningful on a skip; a nudge is never "needs human".
120
+ const needsHuman = action === 'skip' && obj.needsHuman === true ? true
121
+ : action === 'skip' && obj.needsHuman === false ? false
122
+ : undefined;
123
+ decisions.push({ terminalId, action, text, reason, needsHuman });
157
124
  }
158
125
  return decisions;
159
126
  }
160
- export function isLikelyTrulyBlocked(candidate) {
161
- // With no tail to reason over, only a very long stall counts as blocked.
162
- if (candidate.tailLines.length === 0)
163
- return candidate.stalledForMs >= FORCE_REVIEW_STALL_MS;
164
- const lowerTail = candidate.tailLines.join('\n').toLowerCase();
165
- // Waiting-on-user and completion hints are checked BEFORE the 15m force-review
166
- // short-circuit — a long-idle OPEN QUESTION must defer (not be blindly
167
- // force-nudged) and a finished task is done. The old order tested stall age
168
- // first, so a 15m-60m idle session whose tail said "waiting on user" / "done"
169
- // was force-nudged anyway. Precedence fixed here (watchdog-brain-v2).
170
- if (WAITING_HINTS.some((hint) => lowerTail.includes(hint)))
171
- return false;
172
- if (COMPLETION_HINTS.some((hint) => lowerTail.includes(hint)))
173
- return false;
174
- if (candidate.stalledForMs >= FORCE_REVIEW_STALL_MS)
175
- return true;
176
- if (BLOCKED_HINTS.some((hint) => lowerTail.includes(hint)))
177
- return true;
178
- let sawToolAfter = false;
179
- for (let i = candidate.tailLines.length - 1; i >= 0; i--) {
180
- const line = candidate.tailLines[i].toLowerCase();
181
- if (TOOL_CALL_HINTS.some((hint) => line.includes(hint))) {
182
- sawToolAfter = true;
183
- continue;
184
- }
185
- if (!sawToolAfter) {
186
- const isAssistantLine = ASSISTANT_LINE_HINTS.some((hint) => line.includes(hint));
187
- const hasPromise = PROMISE_HINTS.some((hint) => line.includes(hint));
188
- if (isAssistantLine && hasPromise)
189
- return true;
190
- }
191
- }
192
- return false;
193
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.46",
3
+ "version": "1.22.47",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -79,7 +79,7 @@
79
79
  "multi-agent"
80
80
  ],
81
81
  "author": "Phoenix Labs",
82
- "license": "Apache-2.0",
82
+ "license": "FSL-1.1-Apache-2.0",
83
83
  "engines": {
84
84
  "node": ">=22.5.0",
85
85
  "npm": ">=9"
@@ -1,27 +0,0 @@
1
- /**
2
- * Usage command -- show rate-limit / quota status for each installed agent.
3
- *
4
- * Lists every installed agent with the best available usage snapshot:
5
- * - claude: live OAuth API call (cached for 5 minutes)
6
- * - codex: parsed from latest session log's rate_limits event
7
- * - kimi: live Kimi Code /usages API call (cached for 5 minutes)
8
- * - droid: live Factory billing/limits API call (cached for 5 minutes)
9
- * - grok: parsed from the latest local usage event
10
- * - cursor: live Cursor usage API call (cached for 5 minutes)
11
- * - others: marked as "not exposed by CLI"
12
- */
13
- import type { Command } from 'commander';
14
- import type { AgentId } from '../lib/types.js';
15
- import { getUsageInfoForIdentity } from '../lib/accounting/usage.js';
16
- /** One agent's usage snapshot — the unit the text and --json renderers share. */
17
- export interface AgentUsageRecord {
18
- agent: AgentId;
19
- /** Plain agent name (no ANSI) — safe to emit in --json; the text table colorizes it. */
20
- label: string;
21
- status: 'unsupported' | 'no-version' | 'not-signed-in' | 'ok';
22
- email?: string;
23
- usage?: Awaited<ReturnType<typeof getUsageInfoForIdentity>>;
24
- }
25
- export declare function registerUsageCommand(program: Command): void;
26
- /** Render one usage record as the human table section. */
27
- export declare function formatAgentUsage(rec: AgentUsageRecord): string;
@@ -1,96 +0,0 @@
1
- import { addHostOption } from '../lib/hosts/option.js';
2
- import chalk from 'chalk';
3
- import { ALL_AGENT_IDS, AGENTS, getAccountInfo, agentLabel, resolveAgentName, formatAgentError, } from '../lib/agents.js';
4
- import { listInstalledVersions, getGlobalDefault, getVersionHomePath } from '../lib/installations/versions.js';
5
- import { agentReportsUsage, formatUsageSection, getUsageInfoForIdentity } from '../lib/accounting/usage.js';
6
- export function registerUsageCommand(program) {
7
- addHostOption(program.command('usage [agent]'))
8
- .description('Show rate-limit / quota usage per agent')
9
- .option('--json', 'Emit machine-readable JSON (per-agent usage snapshot) instead of the table')
10
- .option('-r, --refresh', 'Collect fresh usage through the shared device coordinator before rendering')
11
- .addHelpText('after', `
12
- Examples:
13
- agents usage Show usage for all installed agents
14
- agents usage claude Show usage for Claude only
15
- agents usage codex Show usage for Codex only
16
- agents usage claude --refresh Explicitly refresh Claude once, device-wide
17
- agents usage --json Machine-readable snapshot for scripts
18
- `)
19
- .action(async (agentFilter, options) => {
20
- let filter;
21
- if (agentFilter) {
22
- const resolved = resolveAgentName(agentFilter);
23
- if (!resolved) {
24
- console.error(chalk.red(formatAgentError(agentFilter)));
25
- process.exit(1);
26
- }
27
- filter = resolved;
28
- }
29
- const targets = filter
30
- ? [filter]
31
- : ALL_AGENT_IDS.filter((id) => listInstalledVersions(id).length > 0);
32
- if (targets.length === 0) {
33
- if (options.json) {
34
- console.log('[]');
35
- return;
36
- }
37
- console.log(chalk.gray('No agents installed. Run `agents add <agent>` first.'));
38
- return;
39
- }
40
- const records = await Promise.all(targets.map((agentId) => collectAgentUsage(agentId, options.refresh === true)));
41
- if (options.json) {
42
- console.log(JSON.stringify(records, null, 2));
43
- return;
44
- }
45
- console.log(records.map(formatAgentUsage).filter(Boolean).join('\n\n'));
46
- });
47
- }
48
- /** Gather one agent's usage snapshot as structured data (shared by both renderers). */
49
- async function collectAgentUsage(agentId, forceRefresh = false) {
50
- // Plain name — color is applied only at text-render time (formatAgentUsage), so
51
- // `--json` never emits ANSI escapes in `label` (e.g. under FORCE_COLOR=1).
52
- const label = AGENTS[agentId].name;
53
- if (!agentReportsUsage(agentId)) {
54
- return { agent: agentId, label, status: 'unsupported' };
55
- }
56
- const versions = listInstalledVersions(agentId);
57
- const version = getGlobalDefault(agentId) || versions[0];
58
- if (!version) {
59
- return { agent: agentId, label, status: 'no-version' };
60
- }
61
- const home = getVersionHomePath(agentId, version);
62
- const info = await getAccountInfo(agentId, home);
63
- if (!info.usageKey && !info.accountKey) {
64
- return { agent: agentId, label, status: 'not-signed-in' };
65
- }
66
- const usage = await getUsageInfoForIdentity({ agentId, home, info, cliVersion: null }, forceRefresh ? { forceRefresh: true } : undefined);
67
- return { agent: agentId, label, status: 'ok', email: info.email ?? undefined, usage };
68
- }
69
- /** Render one usage record as the human table section. */
70
- export function formatAgentUsage(rec) {
71
- const cfg = AGENTS[rec.agent];
72
- // Colorize the heading here (not in the record) so the plain `label` stays
73
- // clean for --json while the text table keeps its per-agent color.
74
- const heading = agentLabel(rec.agent);
75
- switch (rec.status) {
76
- case 'unsupported':
77
- return [heading, ` ${chalk.dim(`${cfg.name} CLI does not publish usage data.`)}`].join('\n');
78
- case 'no-version':
79
- return [heading, ` ${chalk.dim('No version installed.')}`].join('\n');
80
- case 'not-signed-in':
81
- return [heading, ` ${chalk.dim('Not signed in.')}`].join('\n');
82
- case 'ok': {
83
- const lines = [heading];
84
- if (rec.email)
85
- lines.push(` ${chalk.dim(rec.email)}`);
86
- const section = formatUsageSection(rec.usage);
87
- if (section.length === 0) {
88
- lines.push(` ${chalk.dim('No usage data available right now.')}`);
89
- }
90
- else {
91
- lines.push(...section);
92
- }
93
- return lines.join('\n');
94
- }
95
- }
96
- }