@phnx-labs/agents-cli 1.22.46 → 1.22.49
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 +663 -0
- package/LICENSE +102 -182
- package/README.md +96 -49
- package/dist/bootstrap.js +4 -2
- package/dist/cli/command-registry.d.ts +2 -1
- package/dist/cli/command-registry.js +6 -2
- package/dist/commands/accounts.d.ts +58 -0
- package/dist/commands/accounts.js +243 -25
- package/dist/commands/artifacts.js +7 -6
- package/dist/commands/attach.js +1 -1
- package/dist/commands/auth.js +7 -5
- package/dist/commands/browser-sessions-picker.js +9 -7
- package/dist/commands/browser.js +711 -62
- package/dist/commands/config.d.ts +0 -1
- package/dist/commands/config.js +26 -24
- package/dist/commands/cost.d.ts +1 -1
- package/dist/commands/daemon.d.ts +21 -0
- package/dist/commands/daemon.js +147 -12
- package/dist/commands/detach.d.ts +13 -0
- package/dist/commands/detach.js +11 -8
- package/dist/commands/exec.js +107 -53
- 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/focus.d.ts +14 -0
- package/dist/commands/focus.js +48 -5
- package/dist/commands/go.d.ts +3 -0
- package/dist/commands/go.js +16 -3
- package/dist/commands/insights.d.ts +1 -1
- package/dist/commands/insights.js +2 -2
- package/dist/commands/inspect.js +2 -2
- package/dist/commands/lease.d.ts +1 -1
- package/dist/commands/lease.js +7 -6
- package/dist/commands/mailboxes.js +4 -3
- package/dist/commands/menubar.js +11 -4
- package/dist/commands/projects.d.ts +16 -1
- package/dist/commands/projects.js +58 -64
- package/dist/commands/resume.js +1 -1
- package/dist/commands/route.js +4 -4
- package/dist/commands/routines.js +4 -3
- package/dist/commands/run-broadcast.d.ts +1 -1
- package/dist/commands/run-broadcast.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-picker.d.ts +22 -0
- package/dist/commands/sessions-picker.js +48 -9
- 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 +5 -0
- package/dist/commands/setup-secrets.js +23 -0
- package/dist/commands/share.d.ts +20 -5
- package/dist/commands/share.js +152 -52
- package/dist/commands/ssh.d.ts +15 -0
- package/dist/commands/ssh.js +243 -18
- package/dist/commands/teams.js +34 -2
- package/dist/commands/traces.d.ts +2 -0
- package/dist/commands/traces.js +184 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +32 -62
- package/dist/commands/watchdog.js +9 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- 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 +84 -6
- package/dist/lib/accounting/usage.js +293 -75
- 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 +25 -15
- 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/drivers/ssh.d.ts +16 -2
- package/dist/lib/browser/drivers/ssh.js +36 -9
- package/dist/lib/browser/ipc.d.ts +1 -1
- package/dist/lib/browser/ipc.js +40 -9
- package/dist/lib/browser/profiles.d.ts +104 -65
- package/dist/lib/browser/profiles.js +277 -165
- package/dist/lib/browser/registry.d.ts +39 -0
- package/dist/lib/browser/registry.js +149 -0
- package/dist/lib/browser/remote-control.d.ts +28 -2
- package/dist/lib/browser/remote-control.js +35 -3
- package/dist/lib/browser/resolve-target.d.ts +68 -0
- package/dist/lib/browser/resolve-target.js +250 -0
- package/dist/lib/browser/runtime-state.d.ts +44 -11
- package/dist/lib/browser/runtime-state.js +98 -15
- package/dist/lib/browser/service.d.ts +65 -9
- package/dist/lib/browser/service.js +196 -61
- package/dist/lib/browser/sessions-list.d.ts +1 -3
- package/dist/lib/browser/sessions-list.js +3 -16
- package/dist/lib/browser/task-index.d.ts +70 -0
- package/dist/lib/browser/task-index.js +180 -0
- package/dist/lib/browser/types.d.ts +12 -1
- package/dist/lib/byok-usage.js +2 -2
- package/dist/lib/claude-statusline.d.ts +29 -0
- package/dist/lib/claude-statusline.js +139 -0
- package/dist/lib/cloud/host.d.ts +1 -1
- package/dist/lib/cloud/host.js +1 -1
- package/dist/lib/cloud/rush.js +1 -1
- package/dist/lib/codex-policy.d.ts +11 -1
- package/dist/lib/codex-policy.js +20 -5
- package/dist/lib/computer/computer-rpc.js +2 -2
- package/dist/lib/computer/download.d.ts +15 -15
- package/dist/lib/computer/download.js +35 -98
- package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
- package/dist/lib/computer/ssh-tunnel.js +12 -22
- package/dist/lib/config-keys.d.ts +6 -11
- package/dist/lib/config-keys.js +18 -19
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +2 -2
- package/dist/lib/crabbox/lease.d.ts +9 -0
- package/dist/lib/crabbox/lease.js +70 -15
- package/dist/lib/crabbox/runtimes.js +1 -1
- package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
- package/dist/lib/daemon/account-state-daemon-service.js +26 -0
- package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
- package/dist/lib/daemon/browser-ipc-service.js +46 -0
- package/dist/lib/daemon/daemon.d.ts +3 -0
- package/dist/lib/daemon/daemon.js +212 -444
- package/dist/lib/daemon/device-probe-service.d.ts +26 -0
- package/dist/lib/daemon/device-probe-service.js +47 -0
- package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
- package/dist/lib/daemon/keychain-reap-service.js +32 -0
- package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
- package/dist/lib/daemon/monitor-engine-service.js +29 -0
- package/dist/lib/daemon/runner.js +15 -2
- package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
- package/dist/lib/daemon/secrets-broker-service.js +66 -0
- package/dist/lib/daemon/self-heal-service.d.ts +27 -0
- package/dist/lib/daemon/self-heal-service.js +48 -0
- package/dist/lib/daemon/service.d.ts +102 -0
- package/dist/lib/daemon/service.js +98 -0
- package/dist/lib/daemon/session-index-service.d.ts +22 -0
- package/dist/lib/daemon/session-index-service.js +40 -0
- package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
- package/dist/lib/daemon/state-dir-check-service.js +63 -0
- package/dist/lib/daemon/supervisor.d.ts +87 -0
- package/dist/lib/daemon/supervisor.js +298 -0
- package/dist/lib/daemon/watchdog-service.d.ts +19 -0
- package/dist/lib/daemon/watchdog-service.js +40 -0
- package/dist/lib/daemon-health.d.ts +15 -0
- package/dist/lib/daemon-health.js +29 -5
- package/dist/lib/daemon-services.d.ts +8 -1
- package/dist/lib/daemon-services.js +55 -0
- package/dist/lib/daemon-ticks.d.ts +13 -2
- package/dist/lib/daemon-ticks.js +22 -20
- package/dist/lib/device-config.d.ts +25 -10
- package/dist/lib/device-config.js +90 -39
- 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 +26 -0
- package/dist/lib/devices/stats-cache.js +59 -7
- package/dist/lib/devices/worker-pick.d.ts +48 -0
- package/dist/lib/devices/worker-pick.js +113 -0
- package/dist/lib/exec.d.ts +70 -14
- package/dist/lib/exec.js +110 -38
- 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/harness/adapters/cursor.js +8 -11
- package/dist/lib/helper-download.d.ts +108 -0
- package/dist/lib/helper-download.js +194 -0
- package/dist/lib/helper-versions.d.ts +52 -0
- package/dist/lib/helper-versions.js +58 -0
- package/dist/lib/hooks/cache.js +19 -0
- package/dist/lib/hooks/install.js +2 -2
- package/dist/lib/hooks/match.d.ts +3 -0
- package/dist/lib/hooks/match.js +25 -0
- package/dist/lib/hosts/credentials.d.ts +1 -1
- package/dist/lib/hosts/credentials.js +1 -1
- package/dist/lib/hosts/dispatch.d.ts +7 -1
- package/dist/lib/hosts/dispatch.js +24 -5
- package/dist/lib/hosts/passthrough.d.ts +8 -15
- package/dist/lib/hosts/passthrough.js +35 -16
- package/dist/lib/hosts/providers/devices.d.ts +1 -1
- package/dist/lib/hosts/providers/devices.js +1 -1
- package/dist/lib/hosts/reconnect.d.ts +179 -21
- package/dist/lib/hosts/reconnect.js +266 -45
- package/dist/lib/hosts/registry.js +7 -0
- package/dist/lib/installations/migrate.d.ts +4 -5
- package/dist/lib/installations/migrate.js +6 -7
- package/dist/lib/installations/shims.d.ts +1 -1
- package/dist/lib/installations/shims.js +23 -13
- package/dist/lib/installations/versions.js +25 -2
- package/dist/lib/linear-autoclose.d.ts +1 -1
- package/dist/lib/linear-autoclose.js +1 -1
- package/dist/lib/loop.d.ts +2 -2
- package/dist/lib/loop.js +1 -1
- package/dist/lib/menubar/download-menubar.d.ts +47 -0
- package/dist/lib/menubar/download-menubar.js +60 -0
- package/dist/lib/menubar/install-menubar.d.ts +44 -7
- package/dist/lib/menubar/install-menubar.js +105 -23
- package/dist/lib/menubar/notify-desktop.d.ts +3 -3
- package/dist/lib/menubar/notify-desktop.js +6 -6
- package/dist/lib/model-tiers.d.ts +1 -1
- package/dist/lib/open-url.d.ts +60 -2
- package/dist/lib/open-url.js +167 -12
- package/dist/lib/project-probe.d.ts +1 -1
- package/dist/lib/project-probe.js +1 -1
- package/dist/lib/project-pull.d.ts +1 -1
- package/dist/lib/project-pull.js +2 -2
- package/dist/lib/projects.d.ts +1 -1
- package/dist/lib/projects.js +1 -1
- package/dist/lib/refresh-coordinator.d.ts +1 -1
- package/dist/lib/refresh-coordinator.js +1 -1
- package/dist/lib/sandbox.js +6 -7
- package/dist/lib/secrets/download-keychain.d.ts +47 -0
- package/dist/lib/secrets/download-keychain.js +70 -0
- package/dist/lib/secrets/install-helper.d.ts +20 -1
- package/dist/lib/secrets/install-helper.js +44 -3
- package/dist/lib/session/active.d.ts +67 -0
- package/dist/lib/session/active.js +86 -2
- package/dist/lib/session/db.d.ts +11 -1
- package/dist/lib/session/db.js +142 -8
- package/dist/lib/session/discover.d.ts +23 -1
- package/dist/lib/session/discover.js +44 -10
- package/dist/lib/session/highlights.d.ts +49 -1
- package/dist/lib/session/highlights.js +95 -0
- package/dist/lib/session/host-link.d.ts +16 -2
- package/dist/lib/session/host-link.js +7 -1
- package/dist/lib/session/parse.js +33 -1
- package/dist/lib/session/prompt.d.ts +35 -0
- package/dist/lib/session/prompt.js +56 -0
- package/dist/lib/session/remote/remote-list.d.ts +9 -0
- package/dist/lib/session/remote/remote-list.js +19 -2
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +17 -3
- package/dist/lib/session/render.js +7 -3
- package/dist/lib/session/session-cache.d.ts +53 -0
- package/dist/lib/session/session-cache.js +93 -1
- package/dist/lib/session/state.js +1 -1
- package/dist/lib/session/tool-calls.d.ts +2 -0
- package/dist/lib/session/tool-calls.js +1 -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 +19 -0
- package/dist/lib/session/trajectory-html.js +770 -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 +284 -0
- package/dist/lib/session/trajectory.d.ts +102 -0
- package/dist/lib/session/trajectory.js +384 -0
- package/dist/lib/session/types.d.ts +15 -0
- package/dist/lib/sha256-asset.d.ts +26 -0
- package/dist/lib/sha256-asset.js +41 -0
- package/dist/lib/share/backend.d.ts +95 -0
- package/dist/lib/share/backend.js +139 -0
- package/dist/lib/share/delete.d.ts +6 -2
- package/dist/lib/share/delete.js +8 -9
- package/dist/lib/share/html.d.ts +22 -0
- package/dist/lib/share/html.js +88 -0
- package/dist/lib/share/provision.d.ts +18 -4
- package/dist/lib/share/provision.js +38 -4
- package/dist/lib/share/publish.d.ts +39 -15
- package/dist/lib/share/publish.js +55 -26
- package/dist/lib/share/worker-template.js +171 -11
- package/dist/lib/sqlite.js +16 -3
- package/dist/lib/ssh-exec.d.ts +65 -0
- package/dist/lib/ssh-exec.js +120 -4
- package/dist/lib/startup/command-registry.d.ts +3 -1
- package/dist/lib/startup/command-registry.js +6 -3
- package/dist/lib/startup/dev-build.js +3 -3
- package/dist/lib/state.d.ts +2 -2
- package/dist/lib/state.js +11 -5
- package/dist/lib/subagents-registry.js +1 -1
- package/dist/lib/teams/agents.js +9 -0
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/inject.d.ts +19 -0
- package/dist/lib/terminal/inject.js +19 -9
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/terminal/types.js +1 -1
- package/dist/lib/tmux/orphan-reap.js +1 -1
- package/dist/lib/tmux/session.d.ts +46 -13
- package/dist/lib/tmux/session.js +204 -27
- package/dist/lib/traces/backend.d.ts +29 -0
- package/dist/lib/traces/backend.js +46 -0
- package/dist/lib/traces/classify.d.ts +52 -0
- package/dist/lib/traces/classify.js +72 -0
- package/dist/lib/traces/config.d.ts +10 -0
- package/dist/lib/traces/config.js +2 -0
- package/dist/lib/traces/provision.d.ts +19 -0
- package/dist/lib/traces/provision.js +30 -0
- package/dist/lib/traces/sync.d.ts +150 -0
- package/dist/lib/traces/sync.js +447 -0
- package/dist/lib/traces/worker-template.d.ts +4 -0
- package/dist/lib/traces/worker-template.js +159 -0
- package/dist/lib/types.d.ts +34 -19
- package/dist/lib/types.js +14 -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 +18 -4
- package/dist/lib/usage-refresh.js +68 -19
- 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/dist/lib/workflows.d.ts +1 -1
- package/package.json +5 -7
- package/dist/commands/usage.d.ts +0 -27
- package/dist/commands/usage.js +0 -96
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -24
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -123
- package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
- package/dist/lib/usage-fleet.d.ts +0 -32
- package/dist/lib/usage-fleet.js +0 -125
|
@@ -3,19 +3,23 @@
|
|
|
3
3
|
* working auto-nudge (RUSH-1415). The `agents watchdog` command drives it, so
|
|
4
4
|
* the whole loop runs WITHOUT the Swift menu-bar.
|
|
5
5
|
*
|
|
6
|
-
* One tick
|
|
6
|
+
* One tick:
|
|
7
7
|
*
|
|
8
8
|
* getActiveSessions() (session/active.ts)
|
|
9
|
-
* -> classifyTerminal(...)
|
|
10
|
-
* -> readWatchdogTail(...)
|
|
11
|
-
* ->
|
|
9
|
+
* -> classifyTerminal(...) per session (watchdog/watchdog.ts) — which are idle?
|
|
10
|
+
* -> readWatchdogTail(...) for the idle ones (watchdog/read.ts) — task + transcript tail
|
|
11
|
+
* -> the watchdog AGENT judges them ALL at once (watchdog/watchdog-agent.ts)
|
|
12
|
+
* — idle-but-unfinished -> nudge; idle-and-done / needs-human -> skip
|
|
12
13
|
* -> resolveInjectTargetForSession(...) (terminal/resolve.ts) — THE safety gate: addressable or an honest refusal
|
|
13
|
-
* -> injectIntoTerminal(target,text) (terminal/inject.ts) — deliver
|
|
14
|
+
* -> injectIntoTerminal(target,text) (terminal/inject.ts) — deliver into the EXACT split, then CONFIRM
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* There is no heuristic decider — no regex over the tail guessing done-vs-stuck.
|
|
17
|
+
* The agent is the whole decider, given every idle session's task + tail in ONE
|
|
18
|
+
* `agents run --mode plan` call per tick. The safety gate is absolute: a nudge is
|
|
19
|
+
* delivered ONLY when the resolver returns `addressable: true`, and it is booked in
|
|
20
|
+
* the cooldown ledger ONLY when delivery is CONFIRMED. On `addressable: false` the
|
|
21
|
+
* reason is recorded to a state file the menu-bar can surface and the session is
|
|
22
|
+
* SKIPPED — never a guessed / frontmost target.
|
|
19
23
|
*
|
|
20
24
|
* Persistence (all under ~/.agents/.cache/state/watchdog/, tray-readable):
|
|
21
25
|
* - nudges.json — { [sessionId]: lastNudgeMs } — enforces the cooldown.
|
|
@@ -25,15 +29,16 @@
|
|
|
25
29
|
* - rotate/<sessionId>.json — the in-place rotate state machine (rotate.ts).
|
|
26
30
|
* - rotate-skips.json — zero-healthy skip suppression: { [sessionId]: suppressUntilMs }.
|
|
27
31
|
*
|
|
28
|
-
* The pure logic (classifyTerminal
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
+
* The pure logic (classifyTerminal) is imported and never re-implemented; the
|
|
33
|
+
* runner only supplies its I/O (sessions, tails, clock, policy, the agent decider,
|
|
34
|
+
* injection) — each an injectable seam so runner.test.ts drives real synthetic
|
|
35
|
+
* sessions without a live terminal or a real `agents run`.
|
|
32
36
|
*/
|
|
33
37
|
import type { ActiveSession } from '../session/active.js';
|
|
34
38
|
import { type PresenceTransition } from '../session/presence.js';
|
|
35
39
|
import { type InjectRail, type InjectResult, type InjectTarget } from '../terminal/index.js';
|
|
36
40
|
import { type StallStatus, type WatchdogCandidate } from './watchdog.js';
|
|
41
|
+
import { type WatchdogAgentDecider } from './watchdog-agent.js';
|
|
37
42
|
import { type OpenBlock } from '../feed/feed.js';
|
|
38
43
|
import { type RotateGateResult, type RotatePhase, type RotateState } from './rotate.js';
|
|
39
44
|
/** Per-session policy sentinel. `keep` is the default (watchdog may nudge). */
|
|
@@ -52,16 +57,7 @@ export interface WatchdogTickOptions {
|
|
|
52
57
|
nudge?: boolean;
|
|
53
58
|
/** Nudge text delivered into the terminal. Default "Continue." */
|
|
54
59
|
nudgeText?: string;
|
|
55
|
-
/**
|
|
56
|
-
* Force the LLM decider (`agents run`) on EVERY stalled candidate instead of the
|
|
57
|
-
* hybrid path. Default false. Even when false the tick still ESCALATES the
|
|
58
|
-
* judgment-heavy cases (parked-on-question, ambiguous stalls) to the smart brain
|
|
59
|
-
* — `smart: true` additionally routes the obvious promise-without-toolcall
|
|
60
|
-
* nudges through it. Non-reproducible; the deterministic pre-filter is the
|
|
61
|
-
* reproducible default.
|
|
62
|
-
*/
|
|
63
|
-
smart?: boolean;
|
|
64
|
-
/** Agent the smart decider runs as (and the built-in fallback prompt). Default 'claude'. */
|
|
60
|
+
/** Agent the watchdog decider runs as (and the built-in prompt). Default 'claude'. */
|
|
65
61
|
smartAgent?: string;
|
|
66
62
|
/** Threshold overrides. Missing fields fall back to DEFAULT_THRESHOLDS. */
|
|
67
63
|
thresholds?: Partial<WatchdogThresholds>;
|
|
@@ -82,13 +78,20 @@ export interface WatchdogTickOptions {
|
|
|
82
78
|
/** Per-session policy. Default = the on-disk sentinel. */
|
|
83
79
|
policyFor?: (s: ActiveSession) => WatchdogPolicy;
|
|
84
80
|
/**
|
|
85
|
-
* The
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
81
|
+
* The decider seam: given an idle candidate, decide nudge vs skip and craft the
|
|
82
|
+
* message. Production leaves this unset — the tick judges every idle session in
|
|
83
|
+
* ONE batched `agents run … --mode plan` call (watchdog-agent.ts). Tests inject a
|
|
84
|
+
* synthetic per-candidate decider so the decision path is exercised without
|
|
85
|
+
* shelling out.
|
|
90
86
|
*/
|
|
91
87
|
smartDecider?: SmartDecider;
|
|
88
|
+
/**
|
|
89
|
+
* The batched agent decider used in production (all idle candidates → one call).
|
|
90
|
+
* Default `makeWatchdogAgentDecider`. Tests inject one (e.g. returning an empty
|
|
91
|
+
* map) to exercise the batched path and the no-verdict fallback without shelling
|
|
92
|
+
* out. Ignored when `smartDecider` (the per-candidate test seam) is set.
|
|
93
|
+
*/
|
|
94
|
+
agentDecider?: WatchdogAgentDecider;
|
|
92
95
|
/** Open feed block for a session (parked-on-question detection). Default reads the feed. */
|
|
93
96
|
openBlockFor?: (s: ActiveSession) => OpenBlock | null;
|
|
94
97
|
/** Inject primitive. Default injectIntoTerminal — tests capture the resolved target. */
|
|
@@ -206,31 +209,19 @@ export interface WatchdogTickResult {
|
|
|
206
209
|
export declare function readPolicySentinel(dir: string, sessionId: string): WatchdogPolicy;
|
|
207
210
|
/** Write a per-session policy sentinel (used by the CLI `agents watchdog policy`). */
|
|
208
211
|
export declare function writePolicySentinel(dir: string, sessionId: string, policy: WatchdogPolicy): void;
|
|
209
|
-
/** A
|
|
212
|
+
/** A decision for one idle session. `text` overrides the default nudge text. */
|
|
210
213
|
export interface NudgeDecision {
|
|
211
214
|
nudge: boolean;
|
|
212
215
|
reason: string;
|
|
213
216
|
text?: string;
|
|
214
217
|
/**
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* "session is done" skip does NOT trigger a reminder.
|
|
218
|
+
* On a skip, `true` = the agent judged the session genuinely needs the human
|
|
219
|
+
* (surface it — self-file reminder / owner page); `false` / absent = idle-and-
|
|
220
|
+
* done, leave it alone. Gates the self-file reminder so a finished session is
|
|
221
|
+
* never poked.
|
|
220
222
|
*/
|
|
221
223
|
needsHuman?: boolean;
|
|
222
224
|
}
|
|
223
|
-
/**
|
|
224
|
-
* The smart brain. Resolves a `watchdog` workflow for the session's cwd
|
|
225
|
-
* (project > user > system precedence, via resolveWorkflowRef) so a repo/user
|
|
226
|
-
* override AND `model:` frontmatter come for free; when a workflow resolves it
|
|
227
|
-
* runs `agents run watchdog --mode plan <prompt>`, else it falls back to the
|
|
228
|
-
* improved built-in prompt via `agents run <agent> --mode plan <prompt>`. Plan
|
|
229
|
-
* mode keeps the decider read-only. Best-effort and NON-deterministic: any
|
|
230
|
-
* failure (decider unavailable, no verdict) returns a SAFE skip — a parked
|
|
231
|
-
* question we cannot judge is left for the human, never blindly nudged.
|
|
232
|
-
*/
|
|
233
|
-
export declare function makeDefaultSmartDecider(agent: string): SmartDecider;
|
|
234
225
|
/**
|
|
235
226
|
* Run ONE watchdog pass. Returns a structured outcome per live session; injects
|
|
236
227
|
* only when `opts.nudge` is set AND the safety gate says addressable AND policy
|
|
@@ -3,19 +3,23 @@
|
|
|
3
3
|
* working auto-nudge (RUSH-1415). The `agents watchdog` command drives it, so
|
|
4
4
|
* the whole loop runs WITHOUT the Swift menu-bar.
|
|
5
5
|
*
|
|
6
|
-
* One tick
|
|
6
|
+
* One tick:
|
|
7
7
|
*
|
|
8
8
|
* getActiveSessions() (session/active.ts)
|
|
9
|
-
* -> classifyTerminal(...)
|
|
10
|
-
* -> readWatchdogTail(...)
|
|
11
|
-
* ->
|
|
9
|
+
* -> classifyTerminal(...) per session (watchdog/watchdog.ts) — which are idle?
|
|
10
|
+
* -> readWatchdogTail(...) for the idle ones (watchdog/read.ts) — task + transcript tail
|
|
11
|
+
* -> the watchdog AGENT judges them ALL at once (watchdog/watchdog-agent.ts)
|
|
12
|
+
* — idle-but-unfinished -> nudge; idle-and-done / needs-human -> skip
|
|
12
13
|
* -> resolveInjectTargetForSession(...) (terminal/resolve.ts) — THE safety gate: addressable or an honest refusal
|
|
13
|
-
* -> injectIntoTerminal(target,text) (terminal/inject.ts) — deliver
|
|
14
|
+
* -> injectIntoTerminal(target,text) (terminal/inject.ts) — deliver into the EXACT split, then CONFIRM
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* There is no heuristic decider — no regex over the tail guessing done-vs-stuck.
|
|
17
|
+
* The agent is the whole decider, given every idle session's task + tail in ONE
|
|
18
|
+
* `agents run --mode plan` call per tick. The safety gate is absolute: a nudge is
|
|
19
|
+
* delivered ONLY when the resolver returns `addressable: true`, and it is booked in
|
|
20
|
+
* the cooldown ledger ONLY when delivery is CONFIRMED. On `addressable: false` the
|
|
21
|
+
* reason is recorded to a state file the menu-bar can surface and the session is
|
|
22
|
+
* SKIPPED — never a guessed / frontmost target.
|
|
19
23
|
*
|
|
20
24
|
* Persistence (all under ~/.agents/.cache/state/watchdog/, tray-readable):
|
|
21
25
|
* - nudges.json — { [sessionId]: lastNudgeMs } — enforces the cooldown.
|
|
@@ -25,10 +29,10 @@
|
|
|
25
29
|
* - rotate/<sessionId>.json — the in-place rotate state machine (rotate.ts).
|
|
26
30
|
* - rotate-skips.json — zero-healthy skip suppression: { [sessionId]: suppressUntilMs }.
|
|
27
31
|
*
|
|
28
|
-
* The pure logic (classifyTerminal
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
+
* The pure logic (classifyTerminal) is imported and never re-implemented; the
|
|
33
|
+
* runner only supplies its I/O (sessions, tails, clock, policy, the agent decider,
|
|
34
|
+
* injection) — each an injectable seam so runner.test.ts drives real synthetic
|
|
35
|
+
* sessions without a live terminal or a real `agents run`.
|
|
32
36
|
*/
|
|
33
37
|
import * as fs from 'fs';
|
|
34
38
|
import * as path from 'path';
|
|
@@ -36,7 +40,8 @@ import * as crypto from 'crypto';
|
|
|
36
40
|
import { getActiveSessions } from '../session/active.js';
|
|
37
41
|
import { reconcilePresence, loadPresence, savePresence, observedFromActive, } from '../session/presence.js';
|
|
38
42
|
import { resolveInjectTargetForSession, injectIntoTerminal, } from '../terminal/index.js';
|
|
39
|
-
import { classifyTerminal,
|
|
43
|
+
import { classifyTerminal, } from './watchdog.js';
|
|
44
|
+
import { makeWatchdogAgentDecider } from './watchdog-agent.js';
|
|
40
45
|
import { readWatchdogTail, WATCHDOG_STALL_MS, WATCHDOG_COOLDOWN_MS, WATCHDOG_DORMANT_MS, WATCHDOG_TAIL_LINES, } from './read.js';
|
|
41
46
|
import { getRuntimeStateDir } from '../state.js';
|
|
42
47
|
import { withFileLock, atomicWriteFileSync, ensureLockTarget } from '../fs-atomic.js';
|
|
@@ -113,80 +118,6 @@ function defaultLastActivity(s) {
|
|
|
113
118
|
}
|
|
114
119
|
return s.startedAtMs;
|
|
115
120
|
}
|
|
116
|
-
/**
|
|
117
|
-
* COMPLETION_HINTS mirror of watchdog.ts. The deterministic pre-filter screens
|
|
118
|
-
* completions to `skip` before the promise check so a finished-but-idle session
|
|
119
|
-
* is never nudged. (isLikelyTrulyBlocked also guards completion now that its 15m
|
|
120
|
-
* precedence is fixed; this explicit check keeps the pre-filter self-contained.)
|
|
121
|
-
*/
|
|
122
|
-
const COMPLETION_HINTS = ['done', 'completed', 'all set', 'finished'];
|
|
123
|
-
function tailShowsCompletion(candidate) {
|
|
124
|
-
if (candidate.tailLines.length === 0)
|
|
125
|
-
return false;
|
|
126
|
-
const lowerTail = candidate.tailLines.join('\n').toLowerCase();
|
|
127
|
-
return COMPLETION_HINTS.some((hint) => lowerTail.includes(hint));
|
|
128
|
-
}
|
|
129
|
-
function deterministicDecision(session, candidate) {
|
|
130
|
-
// Parked on a question — the case Muqsit cares about most. Do NOT drop it:
|
|
131
|
-
// escalate to the brain, which decides drive-forward vs leave-for-human.
|
|
132
|
-
if (session.activity === 'waiting_input') {
|
|
133
|
-
return {
|
|
134
|
-
kind: 'escalate',
|
|
135
|
-
reason: `parked on a question${session.awaitingReason ? ` (${session.awaitingReason})` : ''} — escalate to the brain`,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
// Clearly complete — cheap skip, no LLM.
|
|
139
|
-
if (tailShowsCompletion(candidate)) {
|
|
140
|
-
return { kind: 'skip', reason: 'tail shows completion (done / finished / all set) — skip' };
|
|
141
|
-
}
|
|
142
|
-
// Clear promise-without-toolcall — cheap nudge, no LLM.
|
|
143
|
-
if (isLikelyTrulyBlocked(candidate)) {
|
|
144
|
-
return { kind: 'nudge', reason: 'stalled after announcing an action with no follow-through' };
|
|
145
|
-
}
|
|
146
|
-
// Ambiguous stall — let the brain judge rather than blindly skip.
|
|
147
|
-
return { kind: 'escalate', reason: 'ambiguous stall — escalate to the brain' };
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* The smart brain. Resolves a `watchdog` workflow for the session's cwd
|
|
151
|
-
* (project > user > system precedence, via resolveWorkflowRef) so a repo/user
|
|
152
|
-
* override AND `model:` frontmatter come for free; when a workflow resolves it
|
|
153
|
-
* runs `agents run watchdog --mode plan <prompt>`, else it falls back to the
|
|
154
|
-
* improved built-in prompt via `agents run <agent> --mode plan <prompt>`. Plan
|
|
155
|
-
* mode keeps the decider read-only. Best-effort and NON-deterministic: any
|
|
156
|
-
* failure (decider unavailable, no verdict) returns a SAFE skip — a parked
|
|
157
|
-
* question we cannot judge is left for the human, never blindly nudged.
|
|
158
|
-
*/
|
|
159
|
-
export function makeDefaultSmartDecider(agent) {
|
|
160
|
-
return async (session, candidate) => {
|
|
161
|
-
const prompt = renderWatchdogPrompt([candidate]);
|
|
162
|
-
try {
|
|
163
|
-
const [{ resolveWorkflowRef }, { execFile }, { promisify }] = await Promise.all([
|
|
164
|
-
import('../workflows.js'),
|
|
165
|
-
import('child_process'),
|
|
166
|
-
import('util'),
|
|
167
|
-
]);
|
|
168
|
-
const cwd = session.cwd || process.cwd();
|
|
169
|
-
const workflowPath = resolveWorkflowRef('watchdog', cwd);
|
|
170
|
-
// A resolved `watchdog` workflow runs by name so its WORKFLOW.md body + model
|
|
171
|
-
// frontmatter apply; otherwise the bare agent runs the built-in prompt.
|
|
172
|
-
const runTarget = workflowPath ? 'watchdog' : agent;
|
|
173
|
-
const execFileAsync = promisify(execFile);
|
|
174
|
-
const { stdout } = await execFileAsync('agents', ['run', runTarget, '--mode', 'plan', prompt], {
|
|
175
|
-
encoding: 'utf8',
|
|
176
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
177
|
-
timeout: 120_000,
|
|
178
|
-
});
|
|
179
|
-
const decisions = parseWatchdogResponse(stdout);
|
|
180
|
-
const d = decisions.find((x) => x.terminalId === candidate.terminalId) ?? decisions[0];
|
|
181
|
-
if (!d)
|
|
182
|
-
return { nudge: false, reason: 'smart decider returned no verdict' };
|
|
183
|
-
return { nudge: d.action === 'nudge', reason: d.reason || `smart: ${d.action}`, text: d.text || undefined };
|
|
184
|
-
}
|
|
185
|
-
catch (err) {
|
|
186
|
-
return { nudge: false, reason: `smart decider unavailable: ${err instanceof Error ? err.message : String(err)}` };
|
|
187
|
-
}
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
121
|
/**
|
|
191
122
|
* Pick the delivery mechanism. resolveAnswerRoute (answer-router.ts) chooses
|
|
192
123
|
* mailbox vs resume vs refuse; resolveInjectTargetForSession (resolve.ts) supplies
|
|
@@ -349,7 +280,6 @@ export async function runWatchdogTick(opts = {}) {
|
|
|
349
280
|
const lastActivityFor = opts.lastActivityFor ?? defaultLastActivity;
|
|
350
281
|
const tailFor = opts.tailFor ?? ((s) => (s.sessionId ? readWatchdogTail(s.sessionId, s.kind, WATCHDOG_TAIL_LINES) : []));
|
|
351
282
|
const policyFor = opts.policyFor ?? ((s) => (s.sessionId ? readPolicySentinel(dir, s.sessionId) : 'keep'));
|
|
352
|
-
const smartDecider = opts.smartDecider ?? makeDefaultSmartDecider(opts.smartAgent ?? 'claude');
|
|
353
283
|
const openBlockFor = opts.openBlockFor ?? defaultOpenBlockFor;
|
|
354
284
|
const injectFn = opts.injectFn ?? injectIntoTerminal;
|
|
355
285
|
const publishBlockFn = opts.publishBlockFn ?? publishBlock;
|
|
@@ -401,6 +331,103 @@ export async function runWatchdogTick(opts = {}) {
|
|
|
401
331
|
injectDryRun: opts.injectDryRun,
|
|
402
332
|
logEvents, flags,
|
|
403
333
|
};
|
|
334
|
+
// --- decide, once, with the AGENT ------------------------------------------
|
|
335
|
+
// Classify every session up front, collect the idle ones (with their task +
|
|
336
|
+
// tail), and hand the WHOLE idle set to the watchdog agent in ONE call
|
|
337
|
+
// (WD-GAP-1: the decider sees the fleet at once, not a lone tail). Tails are
|
|
338
|
+
// cached so the main loop below does not re-read them. Tests inject a
|
|
339
|
+
// per-candidate `smartDecider`; production runs one batched
|
|
340
|
+
// `agents run --mode plan` (watchdog-agent.ts). A session the agent returns no
|
|
341
|
+
// verdict for is a SAFE skip, never a blind nudge.
|
|
342
|
+
const tailCache = new Map();
|
|
343
|
+
const idleCandidates = [];
|
|
344
|
+
for (const session of sessions) {
|
|
345
|
+
const sid = session.sessionId;
|
|
346
|
+
if (!sid)
|
|
347
|
+
continue;
|
|
348
|
+
if (policyFor(session) === 'off')
|
|
349
|
+
continue;
|
|
350
|
+
const la = lastActivityFor(session);
|
|
351
|
+
if (la === undefined)
|
|
352
|
+
continue;
|
|
353
|
+
const st = classifyTerminal({
|
|
354
|
+
lastActivityMs: la, nowMs, lastNudgeMs: ledger[sid] ?? null, optedOut: false,
|
|
355
|
+
stallMs: thresholds.stallMs, cooldownMs: thresholds.cooldownMs, dormantMs: thresholds.dormantMs,
|
|
356
|
+
});
|
|
357
|
+
if (st.kind !== 'stalled')
|
|
358
|
+
continue;
|
|
359
|
+
const tail = tailFor(session);
|
|
360
|
+
tailCache.set(sid, tail);
|
|
361
|
+
// Rotate takes precedence over the nudge agent: a session with an in-flight
|
|
362
|
+
// rotate (or in the failed-rotate cooldown) or a HARD account-limit tail is
|
|
363
|
+
// owned by the rotate machine in the loop below — a capped account cannot be
|
|
364
|
+
// "Continue."d, so the agent must not judge it (and is never even consulted).
|
|
365
|
+
if (rotateEnabled) {
|
|
366
|
+
const inflight = readRotateState(dir, sid);
|
|
367
|
+
if (inflight && (isInflightPhase(inflight.phase) || (inflight.phase === 'failed' && (inflight.suppressUntilMs ?? 0) > nowMs)))
|
|
368
|
+
continue;
|
|
369
|
+
if (classifyTailForRotate(tail, nowMs).kind === 'rate_limited')
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
idleCandidates.push({
|
|
373
|
+
session,
|
|
374
|
+
candidate: {
|
|
375
|
+
terminalId: sid,
|
|
376
|
+
agentType: session.kind === 'codex' || session.kind === 'gemini' ? session.kind : 'claude',
|
|
377
|
+
tailLines: tail,
|
|
378
|
+
stalledForMs: st.stalledForMs,
|
|
379
|
+
task: session.topic ?? session.label ?? session.name,
|
|
380
|
+
cwd: session.cwd,
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
const decisionByTerminal = new Map();
|
|
385
|
+
if (idleCandidates.length > 0) {
|
|
386
|
+
if (opts.smartDecider) {
|
|
387
|
+
// Test seam: a synthetic per-candidate decider, called once per candidate. A
|
|
388
|
+
// skip with no explicit needsHuman defaults to surfacing it (same rule as the
|
|
389
|
+
// agent path) so an unfinished session is never silently abandoned.
|
|
390
|
+
for (const { session, candidate } of idleCandidates) {
|
|
391
|
+
const raw = await opts.smartDecider(session, candidate);
|
|
392
|
+
decisionByTerminal.set(candidate.terminalId, raw.nudge ? raw : { ...raw, needsHuman: raw.needsHuman ?? true });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
// Production: ONE batched agent call for every idle session this tick.
|
|
397
|
+
const decide = opts.agentDecider ?? makeWatchdogAgentDecider(opts.smartAgent ?? 'claude');
|
|
398
|
+
const verdicts = await decide(idleCandidates.map((e) => e.candidate));
|
|
399
|
+
// A decider outage (agent unavailable / timeout) returns no verdicts while
|
|
400
|
+
// idle sessions exist — surface it as an error so it is not an invisible
|
|
401
|
+
// no-op tick (the watchdog silently steering nothing is the failure mode
|
|
402
|
+
// this whole subsystem exists to prevent).
|
|
403
|
+
if (verdicts.size === 0) {
|
|
404
|
+
logEvents.push({
|
|
405
|
+
ts: nowMs, kind: 'error',
|
|
406
|
+
message: `watchdog agent returned no verdicts for ${idleCandidates.length} idle session(s) — decider unavailable this tick`,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
for (const { candidate } of idleCandidates) {
|
|
410
|
+
const d = verdicts.get(candidate.terminalId);
|
|
411
|
+
decisionByTerminal.set(candidate.terminalId, d
|
|
412
|
+
? {
|
|
413
|
+
nudge: d.action === 'nudge',
|
|
414
|
+
reason: d.reason || `agent: ${d.action}`,
|
|
415
|
+
text: d.text || undefined,
|
|
416
|
+
// A skip with no explicit needsHuman defaults to surfacing it —
|
|
417
|
+
// never silently abandon an unfinished session (the highest-risk state).
|
|
418
|
+
needsHuman: d.action === 'skip' ? d.needsHuman ?? true : undefined,
|
|
419
|
+
}
|
|
420
|
+
// No verdict for this session — the agent couldn't decide (partial or a
|
|
421
|
+
// decider outage). A NEUTRAL safe-skip: NOT "done" (needsHuman stays
|
|
422
|
+
// undefined, so it is never logged as finished and the reminder never
|
|
423
|
+
// fires), nothing booked, so the next tick re-evaluates it once the
|
|
424
|
+
// decider is back. Marking it done (needsHuman:false) would let an
|
|
425
|
+
// outage abandon every idle session; marking it needsHuman would spam a
|
|
426
|
+
// "you're stuck" reminder into every idle terminal on any outage.
|
|
427
|
+
: { nudge: false, reason: 'watchdog agent returned no verdict — retry next tick' });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
404
431
|
for (const session of sessions) {
|
|
405
432
|
const policy = policyFor(session);
|
|
406
433
|
const base = {
|
|
@@ -464,8 +491,8 @@ export async function runWatchdogTick(opts = {}) {
|
|
|
464
491
|
continue;
|
|
465
492
|
}
|
|
466
493
|
base.stalledForMs = status.stalledForMs;
|
|
467
|
-
// Stalled —
|
|
468
|
-
const tailLines = tailFor(session);
|
|
494
|
+
// Stalled — reuse the tail the pre-pass already read for the agent.
|
|
495
|
+
const tailLines = tailCache.get(session.sessionId) ?? tailFor(session);
|
|
469
496
|
const candidate = {
|
|
470
497
|
terminalId: session.sessionId,
|
|
471
498
|
agentType: (session.kind === 'codex' || session.kind === 'gemini' ? session.kind : 'claude'),
|
|
@@ -653,27 +680,10 @@ export async function runWatchdogTick(opts = {}) {
|
|
|
653
680
|
continue;
|
|
654
681
|
}
|
|
655
682
|
}
|
|
656
|
-
// The
|
|
657
|
-
//
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
if (opts.smart) {
|
|
661
|
-
const d = await smartDecider(session, candidate);
|
|
662
|
-
// Mark needsHuman when the brain explicitly concluded "leave for human".
|
|
663
|
-
decision = d.nudge ? d : { ...d, needsHuman: true };
|
|
664
|
-
}
|
|
665
|
-
else {
|
|
666
|
-
const det = deterministicDecision(session, candidate);
|
|
667
|
-
if (det.kind === 'escalate') {
|
|
668
|
-
const d = await smartDecider(session, candidate);
|
|
669
|
-
// Mark needsHuman when the brain escalation path concluded "leave for human".
|
|
670
|
-
decision = d.nudge ? d : { ...d, needsHuman: true };
|
|
671
|
-
}
|
|
672
|
-
else {
|
|
673
|
-
// Cheap deterministic path — completion or clear stall. Never "needs human".
|
|
674
|
-
decision = { nudge: det.kind === 'nudge', reason: det.reason };
|
|
675
|
-
}
|
|
676
|
-
}
|
|
683
|
+
// The decision was made up front by the watchdog agent over the whole idle
|
|
684
|
+
// set (see the pre-pass). A session with no verdict is a safe skip.
|
|
685
|
+
const decision = decisionByTerminal.get(session.sessionId) ??
|
|
686
|
+
{ nudge: false, reason: 'not evaluated by the watchdog agent', needsHuman: false };
|
|
677
687
|
const chosenText = decision.text ?? nudgeText;
|
|
678
688
|
// Log the decision for the Factory watchdog card.
|
|
679
689
|
const summary = summarizeWatchdogTail(tailLines, candidate.agentType);
|
|
@@ -796,34 +806,37 @@ export async function runWatchdogTick(opts = {}) {
|
|
|
796
806
|
}
|
|
797
807
|
// Deliver. injectDryRun exercises the path without a real side effect: inject
|
|
798
808
|
// still calls injectFn (which honors dryRun), mailbox/resume are short-circuited.
|
|
809
|
+
// `confirmed` distinguishes a delivery we KNOW reached the agent (tmux/iterm/pty,
|
|
810
|
+
// mailbox, resume) from one merely dispatched (vscodium's fire-and-forget
|
|
811
|
+
// --open-url). Only a CONFIRMED delivery is booked as a landed nudge.
|
|
799
812
|
let delivered;
|
|
800
813
|
if (plan.via === 'inject') {
|
|
801
814
|
try {
|
|
802
815
|
const r = await injectFn(plan.target, chosenText, { dryRun: opts.injectDryRun });
|
|
803
|
-
delivered = { ok: r.ok, error: r.error };
|
|
816
|
+
delivered = { ok: r.ok, confirmed: r.confirmed, error: r.error };
|
|
804
817
|
}
|
|
805
818
|
catch (err) {
|
|
806
|
-
delivered = { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
819
|
+
delivered = { ok: false, confirmed: false, error: err instanceof Error ? err.message : String(err) };
|
|
807
820
|
}
|
|
808
821
|
}
|
|
809
822
|
else if (plan.via === 'mailbox') {
|
|
810
823
|
if (opts.injectDryRun) {
|
|
811
|
-
delivered = { ok: true };
|
|
824
|
+
delivered = { ok: true, confirmed: true };
|
|
812
825
|
}
|
|
813
826
|
else {
|
|
814
827
|
try {
|
|
815
828
|
deliverViaMailbox(plan.mailboxId, chosenText, block);
|
|
816
|
-
delivered = { ok: true };
|
|
829
|
+
delivered = { ok: true, confirmed: true };
|
|
817
830
|
}
|
|
818
831
|
catch (err) {
|
|
819
|
-
delivered = { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
832
|
+
delivered = { ok: false, confirmed: false, error: err instanceof Error ? err.message : String(err) };
|
|
820
833
|
}
|
|
821
834
|
}
|
|
822
835
|
}
|
|
823
836
|
else {
|
|
824
|
-
delivered = opts.injectDryRun ? { ok: true } : await deliverViaResume(session, chosenText);
|
|
837
|
+
delivered = opts.injectDryRun ? { ok: true, confirmed: true } : { ...(await deliverViaResume(session, chosenText)), confirmed: true };
|
|
825
838
|
}
|
|
826
|
-
if (delivered.ok) {
|
|
839
|
+
if (delivered.ok && delivered.confirmed) {
|
|
827
840
|
ledgerUpdates[session.sessionId] = nowMs; // start the cooldown clock
|
|
828
841
|
logEvents.push({
|
|
829
842
|
ts: nowMs, kind: 'nudge', terminalId: session.sessionId, agentType: candidate.agentType,
|
|
@@ -835,6 +848,24 @@ export async function runWatchdogTick(opts = {}) {
|
|
|
835
848
|
nudgeText: chosenText,
|
|
836
849
|
});
|
|
837
850
|
}
|
|
851
|
+
else if (delivered.ok && !delivered.confirmed) {
|
|
852
|
+
// Dispatched but UNCONFIRMED (vscodium --open-url handed off, but the ext may
|
|
853
|
+
// have no-op'd the verb). Do NOT claim it landed — record `undelivered` in
|
|
854
|
+
// history so the phantom-nudge is visible. Still start the cooldown so a
|
|
855
|
+
// possibly-working ext session is not re-nudged every tick; the honest
|
|
856
|
+
// `undelivered` signal tells the operator to ship the swarm-ext ack.
|
|
857
|
+
ledgerUpdates[session.sessionId] = nowMs;
|
|
858
|
+
logEvents.push({
|
|
859
|
+
ts: nowMs, kind: 'undelivered', terminalId: session.sessionId, agentType: candidate.agentType,
|
|
860
|
+
message: `dispatched via ${viaLabel(plan)} but UNCONFIRMED (needs swarm-ext ack)`,
|
|
861
|
+
reason: decision.reason, nudgeText: chosenText,
|
|
862
|
+
});
|
|
863
|
+
outcomes.push({
|
|
864
|
+
...base, decision: 'skip', addressable, rail, via: plan.via, injected: false,
|
|
865
|
+
reason: `nudge dispatched via ${viaLabel(plan)} but delivery is UNCONFIRMED (swarm-ext ack pending)`,
|
|
866
|
+
nudgeText: chosenText,
|
|
867
|
+
});
|
|
868
|
+
}
|
|
838
869
|
else {
|
|
839
870
|
outcomes.push({
|
|
840
871
|
...base, decision: 'skip', addressable, rail, via: plan.via, injected: false,
|
|
@@ -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
|
+
}
|