@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.
- package/CHANGELOG.md +279 -0
- package/LICENSE +102 -182
- package/README.md +31 -18
- package/dist/cli/command-registry.d.ts +1 -1
- package/dist/cli/command-registry.js +4 -2
- package/dist/commands/accounts.js +56 -12
- package/dist/commands/auth.js +7 -5
- package/dist/commands/browser-sessions-picker.js +9 -7
- package/dist/commands/browser.js +266 -15
- package/dist/commands/config.js +26 -4
- package/dist/commands/cost.d.ts +1 -1
- package/dist/commands/detach.d.ts +13 -0
- package/dist/commands/detach.js +11 -8
- package/dist/commands/exec.js +55 -38
- package/dist/commands/feed-watch.d.ts +2 -0
- package/dist/commands/feed-watch.js +28 -0
- package/dist/commands/feed.js +27 -0
- package/dist/commands/feedback.js +3 -19
- package/dist/commands/insights.d.ts +1 -1
- package/dist/commands/insights.js +2 -2
- package/dist/commands/lease.js +3 -3
- package/dist/commands/routines.js +1 -1
- package/dist/commands/sessions-picker-factory.d.ts +2 -1
- package/dist/commands/sessions-picker-factory.js +1 -1
- package/dist/commands/sessions-stop.d.ts +24 -0
- package/dist/commands/sessions-stop.js +66 -0
- package/dist/commands/sessions-trace.d.ts +82 -0
- package/dist/commands/sessions-trace.js +387 -0
- package/dist/commands/sessions.js +4 -0
- package/dist/commands/ssh.d.ts +15 -0
- package/dist/commands/ssh.js +194 -16
- package/dist/commands/teams.js +34 -2
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +18 -31
- package/dist/commands/watchdog.js +9 -14
- package/dist/lib/account-capabilities.d.ts +4 -0
- package/dist/lib/account-capabilities.js +13 -9
- package/dist/lib/account-registry.d.ts +2 -0
- package/dist/lib/account-registry.js +30 -2
- package/dist/lib/accounting/rotate.d.ts +9 -0
- package/dist/lib/accounting/rotate.js +12 -2
- package/dist/lib/accounting/usage.d.ts +70 -3
- package/dist/lib/accounting/usage.js +235 -63
- package/dist/lib/agent-modes.js +29 -1
- package/dist/lib/agent-spec/agents.d.ts +4 -3
- package/dist/lib/agent-spec/agents.js +14 -6
- package/dist/lib/analytics/mix-commands.d.ts +1 -1
- package/dist/lib/analytics/mix-commands.js +2 -2
- package/dist/lib/auth-health.d.ts +13 -3
- package/dist/lib/auth-health.js +50 -8
- package/dist/lib/browser/devices.d.ts +19 -0
- package/dist/lib/browser/devices.js +23 -0
- package/dist/lib/browser/ipc.d.ts +1 -1
- package/dist/lib/browser/ipc.js +33 -7
- package/dist/lib/browser/profiles.d.ts +106 -1
- package/dist/lib/browser/profiles.js +300 -17
- package/dist/lib/browser/remote-control.d.ts +26 -2
- package/dist/lib/browser/remote-control.js +33 -3
- package/dist/lib/browser/runtime-state.d.ts +9 -0
- package/dist/lib/browser/runtime-state.js +24 -8
- package/dist/lib/browser/service.d.ts +37 -0
- package/dist/lib/browser/service.js +78 -20
- package/dist/lib/browser/sessions-list.d.ts +1 -3
- package/dist/lib/browser/sessions-list.js +3 -16
- package/dist/lib/browser/types.d.ts +12 -1
- package/dist/lib/codex-policy.d.ts +11 -1
- package/dist/lib/codex-policy.js +20 -5
- package/dist/lib/config-keys.d.ts +4 -3
- package/dist/lib/config-keys.js +17 -7
- package/dist/lib/daemon/runner.js +15 -2
- package/dist/lib/daemon-ticks.js +2 -2
- package/dist/lib/device-config.d.ts +25 -8
- package/dist/lib/device-config.js +90 -17
- package/dist/lib/devices/config-migration.d.ts +7 -0
- package/dist/lib/devices/config-migration.js +67 -3
- package/dist/lib/devices/health.d.ts +27 -2
- package/dist/lib/devices/health.js +39 -9
- package/dist/lib/devices/interactive-host.d.ts +15 -0
- package/dist/lib/devices/interactive-host.js +53 -0
- package/dist/lib/devices/registry.d.ts +61 -4
- package/dist/lib/devices/registry.js +94 -38
- package/dist/lib/devices/stats-cache.d.ts +6 -0
- package/dist/lib/devices/stats-cache.js +10 -2
- package/dist/lib/exec.d.ts +5 -3
- package/dist/lib/exec.js +32 -16
- package/dist/lib/feed/answer.d.ts +29 -0
- package/dist/lib/feed/answer.js +145 -0
- package/dist/lib/feed/attention.d.ts +83 -0
- package/dist/lib/feed/attention.js +0 -0
- package/dist/lib/feed/feed.d.ts +109 -0
- package/dist/lib/feed/feed.js +154 -3
- package/dist/lib/feed/pr-status.d.ts +14 -0
- package/dist/lib/feed/pr-status.js +47 -0
- package/dist/lib/feed/watch.d.ts +59 -0
- package/dist/lib/feed/watch.js +180 -0
- package/dist/lib/fleet/capture.js +6 -0
- package/dist/lib/fleet/remote-login.js +8 -3
- package/dist/lib/fleet/types.d.ts +17 -0
- package/dist/lib/harness/adapter.d.ts +11 -0
- package/dist/lib/harness/adapters/claude.js +38 -24
- package/dist/lib/harness/adapters/codex.js +2 -4
- package/dist/lib/hooks/cache.js +8 -0
- package/dist/lib/hooks/match.d.ts +3 -0
- package/dist/lib/hooks/match.js +25 -0
- package/dist/lib/hosts/passthrough.js +16 -0
- package/dist/lib/hosts/registry.js +7 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +13 -0
- package/dist/lib/menubar/install-menubar.js +26 -2
- package/dist/lib/open-url.d.ts +60 -2
- package/dist/lib/open-url.js +167 -12
- package/dist/lib/refresh-coordinator.d.ts +1 -1
- package/dist/lib/refresh-coordinator.js +1 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/session/active.d.ts +67 -0
- package/dist/lib/session/active.js +69 -1
- package/dist/lib/session/db.js +4 -2
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +12 -6
- package/dist/lib/session/prompt.d.ts +35 -0
- package/dist/lib/session/prompt.js +56 -0
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +6 -1
- package/dist/lib/session/render.js +7 -3
- package/dist/lib/session/session-cache.js +5 -1
- package/dist/lib/session/trajectory-compare.d.ts +47 -0
- package/dist/lib/session/trajectory-compare.js +116 -0
- package/dist/lib/session/trajectory-html.d.ts +15 -0
- package/dist/lib/session/trajectory-html.js +750 -0
- package/dist/lib/session/trajectory-lineage.d.ts +92 -0
- package/dist/lib/session/trajectory-lineage.js +195 -0
- package/dist/lib/session/trajectory-text.d.ts +37 -0
- package/dist/lib/session/trajectory-text.js +277 -0
- package/dist/lib/session/trajectory.d.ts +102 -0
- package/dist/lib/session/trajectory.js +335 -0
- package/dist/lib/startup/command-registry.d.ts +3 -1
- package/dist/lib/startup/command-registry.js +5 -2
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +4 -2
- package/dist/lib/teams/agents.js +9 -0
- package/dist/lib/terminal/inject.d.ts +19 -0
- package/dist/lib/terminal/inject.js +19 -9
- package/dist/lib/tmux/session.d.ts +46 -13
- package/dist/lib/tmux/session.js +173 -27
- package/dist/lib/types.d.ts +16 -0
- package/dist/lib/usage-backoff.d.ts +2 -1
- package/dist/lib/usage-backoff.js +60 -19
- package/dist/lib/usage-refresh.d.ts +17 -3
- package/dist/lib/usage-refresh.js +57 -11
- package/dist/lib/watchdog/log.d.ts +1 -1
- package/dist/lib/watchdog/log.js +1 -1
- package/dist/lib/watchdog/runner.d.ts +36 -45
- package/dist/lib/watchdog/runner.js +150 -119
- package/dist/lib/watchdog/service.d.ts +0 -1
- package/dist/lib/watchdog/service.js +0 -1
- package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
- package/dist/lib/watchdog/watchdog-agent.js +59 -0
- package/dist/lib/watchdog/watchdog.d.ts +11 -2
- package/dist/lib/watchdog/watchdog.js +37 -104
- package/package.json +2 -2
- package/dist/commands/usage.d.ts +0 -27
- package/dist/commands/usage.js +0 -96
|
@@ -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.
|
|
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
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
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.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
-
|
|
103
|
-
|
|
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
|
-
|
|
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, '', '
|
|
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
|
-
|
|
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.
|
|
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"
|
package/dist/commands/usage.d.ts
DELETED
|
@@ -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;
|
package/dist/commands/usage.js
DELETED
|
@@ -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
|
-
}
|