@phnx-labs/agents-cli 1.20.74 → 1.20.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1055 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +18 -0
- package/dist/commands/exec.js +83 -13
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +38 -8
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +286 -15
- package/dist/commands/secrets.js +102 -37
- package/dist/commands/sessions-browser.js +12 -2
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.d.ts +14 -1
- package/dist/commands/sessions-picker.js +184 -16
- package/dist/commands/sessions.d.ts +56 -14
- package/dist/commands/sessions.js +322 -62
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/ssh.d.ts +13 -0
- package/dist/commands/ssh.js +110 -37
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +15 -6
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +24 -4
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +59 -7
- package/dist/lib/activity.d.ts +17 -6
- package/dist/lib/activity.js +466 -40
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +70 -6
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.js +4 -0
- package/dist/lib/daemon.d.ts +10 -1
- package/dist/lib/daemon.js +114 -11
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +37 -1
- package/dist/lib/devices/fleet.js +36 -2
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +82 -0
- package/dist/lib/exec.js +218 -6
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/feed.d.ts +7 -1
- package/dist/lib/feed.js +96 -6
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/heal.d.ts +7 -4
- package/dist/lib/heal.js +10 -22
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +16 -0
- package/dist/lib/hosts/dispatch.js +26 -4
- package/dist/lib/hosts/passthrough.js +9 -2
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.d.ts +11 -0
- package/dist/lib/hosts/remote-cmd.js +32 -8
- package/dist/lib/hosts/remote-session-id.d.ts +45 -0
- package/dist/lib/hosts/remote-session-id.js +84 -0
- package/dist/lib/hosts/run-target.d.ts +17 -5
- package/dist/lib/hosts/run-target.js +30 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +40 -5
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +2 -0
- package/dist/lib/import.js +35 -2
- package/dist/lib/menubar/install-menubar.d.ts +29 -0
- package/dist/lib/menubar/install-menubar.js +59 -1
- package/dist/lib/menubar/notify-desktop.d.ts +44 -0
- package/dist/lib/menubar/notify-desktop.js +78 -0
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +6 -2
- package/dist/lib/overdue.js +10 -36
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routine-notify.d.ts +76 -0
- package/dist/lib/routine-notify.js +190 -0
- package/dist/lib/routines-placement.d.ts +38 -0
- package/dist/lib/routines-placement.js +79 -0
- package/dist/lib/routines-project.d.ts +97 -0
- package/dist/lib/routines-project.js +349 -0
- package/dist/lib/routines.d.ts +94 -0
- package/dist/lib/routines.js +93 -9
- package/dist/lib/runner.d.ts +12 -2
- package/dist/lib/runner.js +242 -20
- package/dist/lib/sandbox.js +10 -0
- package/dist/lib/secrets/account-token.d.ts +20 -0
- package/dist/lib/secrets/account-token.js +64 -0
- package/dist/lib/secrets/agent.d.ts +74 -4
- package/dist/lib/secrets/agent.js +181 -107
- package/dist/lib/secrets/bundles.d.ts +54 -6
- package/dist/lib/secrets/bundles.js +230 -34
- package/dist/lib/secrets/index.d.ts +26 -3
- package/dist/lib/secrets/index.js +42 -12
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/session-store.d.ts +6 -2
- package/dist/lib/secrets/session-store.js +65 -15
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/secrets/unlock-hints.d.ts +27 -0
- package/dist/lib/secrets/unlock-hints.js +36 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +124 -7
- package/dist/lib/session/active.js +390 -56
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +54 -0
- package/dist/lib/session/db.js +320 -19
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +478 -3
- package/dist/lib/session/discover.js +1740 -472
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +141 -32
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/prompt.d.ts +7 -0
- package/dist/lib/session/prompt.js +37 -0
- package/dist/lib/session/provenance.d.ts +7 -0
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +40 -26
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.d.ts +4 -0
- package/dist/lib/session/state.js +99 -13
- package/dist/lib/session/types.d.ts +25 -1
- package/dist/lib/session/types.js +8 -0
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +20 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +3 -0
- package/dist/lib/state.d.ts +8 -4
- package/dist/lib/state.js +21 -37
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +47 -2
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +9 -2
- package/dist/lib/versions.js +37 -8
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
package/dist/lib/versions.js
CHANGED
|
@@ -37,7 +37,7 @@ import { importInstallScriptBinary } from './import.js';
|
|
|
37
37
|
import { IS_WINDOWS, composeWin32CommandLine } from './platform/index.js';
|
|
38
38
|
import { listInstalledSubagents } from './subagents.js';
|
|
39
39
|
import { listInstalledWorkflows } from './workflows.js';
|
|
40
|
-
import { pruneVersionHomeHookEntriesFromSettings } from './hooks.js';
|
|
40
|
+
import { parseHookManifest, registerHooksToSettings, selectHookManifest, pruneVersionHomeHookEntriesFromSettings } from './hooks.js';
|
|
41
41
|
import { supports, explainSkip } from './capabilities.js';
|
|
42
42
|
import { discoverPlugins, marketplaceSpecForName } from './plugins.js';
|
|
43
43
|
import { loadManifest, saveManifest, buildManifest as buildSyncManifest, isStale } from './staleness/index.js';
|
|
@@ -2150,7 +2150,17 @@ export async function verifyInstalledBinaryLaunches(agent, version) {
|
|
|
2150
2150
|
* the exact failure this repairs. Agents with a global/native binary (grok,
|
|
2151
2151
|
* droid) have no such tarball and are returned unchanged.
|
|
2152
2152
|
*/
|
|
2153
|
-
export async function ensureAgentRunnable(agent, version, log) {
|
|
2153
|
+
export async function ensureAgentRunnable(agent, version, log, opts) {
|
|
2154
|
+
// Whether this heal may REPOINT the global default (adopt another version, or
|
|
2155
|
+
// install `latest` and pin it). Interactive callers (`agents run`, `agents
|
|
2156
|
+
// add`) allow it. The daemon's unattended 6-hourly launch-health pass sets
|
|
2157
|
+
// this false: a background pass that silently switches the default installs a
|
|
2158
|
+
// fresh version home → a fresh, empty Claude credential scope (macOS keychain
|
|
2159
|
+
// keyed off CLAUDE_CONFIG_DIR; Linux per-version token file), i.e. an
|
|
2160
|
+
// "unprovoked logout" at a time uncorrelated with anything the user did. In
|
|
2161
|
+
// refuse-mode we still repair the CURRENT default in place (no scope change),
|
|
2162
|
+
// but never repoint — we return null so the caller can alert instead.
|
|
2163
|
+
const allowDefaultSwitch = opts?.allowDefaultSwitch ?? true;
|
|
2154
2164
|
const cfg = AGENTS[agent];
|
|
2155
2165
|
if (!cfg?.npmPackage)
|
|
2156
2166
|
return version;
|
|
@@ -2172,6 +2182,14 @@ export async function ensureAgentRunnable(agent, version, log) {
|
|
|
2172
2182
|
log?.(`${cfg.name}@${version} is an isolated install and could not be repaired — leaving your default ${cfg.name} untouched.`);
|
|
2173
2183
|
return null;
|
|
2174
2184
|
}
|
|
2185
|
+
// In-place repair failed → normally adopt another installed version and repoint
|
|
2186
|
+
// the default. When default-switching is disallowed (unattended daemon pass),
|
|
2187
|
+
// refuse: repointing would swap the live credential scope out from under the
|
|
2188
|
+
// user with no signal. Surface it so the caller can notify.
|
|
2189
|
+
if (!allowDefaultSwitch) {
|
|
2190
|
+
log?.(`${cfg.name}@${version} won't launch and could not be repaired in place — NOT repointing your default ${cfg.name} unattended. Run \`agents use ${agent} <version>\` or \`agents add ${agent}@latest\` to choose one.`);
|
|
2191
|
+
return null;
|
|
2192
|
+
}
|
|
2175
2193
|
// In-place repair failed → adopt another installed version that launches.
|
|
2176
2194
|
// Isolated copies are excluded: they are deliberately not promotable.
|
|
2177
2195
|
const others = listInstalledVersions(agent)
|
|
@@ -2221,14 +2239,18 @@ export async function ensureAgentRunnable(agent, version, log) {
|
|
|
2221
2239
|
* `agents run` hits a raw ENOENT — the run-time heal (ensureAgentRunnable) only
|
|
2222
2240
|
* fires once a run is already starting; this catches it in the background.
|
|
2223
2241
|
*
|
|
2224
|
-
* Returns a label (`agent@broken→healed`)
|
|
2225
|
-
*
|
|
2226
|
-
*
|
|
2242
|
+
* Returns `{ repaired, unhealed }`: `repaired` is a label (`agent@broken→healed`)
|
|
2243
|
+
* for each version actually fixed; `unhealed` is `agent@version` for each broken
|
|
2244
|
+
* default that could not be fixed (including, under `allowDefaultSwitch:false`,
|
|
2245
|
+
* one that was deliberately not repointed), so the daemon can log/notify. A
|
|
2246
|
+
* version that already launches costs one cheap `--version` probe and is left
|
|
2247
|
+
* untouched.
|
|
2227
2248
|
*/
|
|
2228
2249
|
const failedRepairAt = new Map();
|
|
2229
2250
|
const REPAIR_COOLDOWN_MS = 24 * 60 * 60_000;
|
|
2230
|
-
export async function healBrokenDefaultLaunches(log) {
|
|
2251
|
+
export async function healBrokenDefaultLaunches(log, opts) {
|
|
2231
2252
|
const repaired = [];
|
|
2253
|
+
const unhealed = [];
|
|
2232
2254
|
for (const agent of Object.keys(AGENTS)) {
|
|
2233
2255
|
if (!AGENTS[agent].npmPackage)
|
|
2234
2256
|
continue; // native/global agents have no gutted-tarball failure mode
|
|
@@ -2248,16 +2270,17 @@ export async function healBrokenDefaultLaunches(log) {
|
|
|
2248
2270
|
continue;
|
|
2249
2271
|
}
|
|
2250
2272
|
log?.(`${AGENTS[agent].name}@${version} won't launch — repairing…`);
|
|
2251
|
-
const healed = await ensureAgentRunnable(agent, version, log);
|
|
2273
|
+
const healed = await ensureAgentRunnable(agent, version, log, opts);
|
|
2252
2274
|
if (healed) {
|
|
2253
2275
|
failedRepairAt.delete(key);
|
|
2254
2276
|
repaired.push(`${agent}@${version}${healed === version ? '' : `→${healed}`}`);
|
|
2255
2277
|
}
|
|
2256
2278
|
else {
|
|
2257
2279
|
failedRepairAt.set(key, Date.now());
|
|
2280
|
+
unhealed.push(key);
|
|
2258
2281
|
}
|
|
2259
2282
|
}
|
|
2260
|
-
return repaired;
|
|
2283
|
+
return { repaired, unhealed };
|
|
2261
2284
|
}
|
|
2262
2285
|
async function getCliVersionFromPath(agent) {
|
|
2263
2286
|
const agentConfig = AGENTS[agent];
|
|
@@ -2640,6 +2663,7 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2640
2663
|
const hooksToSync = selection
|
|
2641
2664
|
? resolveSelection(selection.hooks, available.hooks)
|
|
2642
2665
|
: available.hooks;
|
|
2666
|
+
let hookManifest = {};
|
|
2643
2667
|
if (hooksToSync.length > 0) {
|
|
2644
2668
|
const r = hooksWriter.write({ version, versionHome, selection: hooksToSync, cwd });
|
|
2645
2669
|
// Remove orphan files from version home. The trusted set is the
|
|
@@ -2657,6 +2681,11 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2657
2681
|
}
|
|
2658
2682
|
}
|
|
2659
2683
|
result.hooks = r.synced.length > 0;
|
|
2684
|
+
hookManifest = selectHookManifest(parseHookManifest(), hooksToSync);
|
|
2685
|
+
}
|
|
2686
|
+
const hooksInScope = !userPassedSelection || selection?.hooks !== undefined;
|
|
2687
|
+
if (agent === 'opencode' && hooksInScope) {
|
|
2688
|
+
registerHooksToSettings(agent, versionHome, hookManifest);
|
|
2660
2689
|
}
|
|
2661
2690
|
}
|
|
2662
2691
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Same path the Factory reader pulls (apps/factory/src/core/watchdogLog.ts). */
|
|
2
|
+
export declare const WATCHDOG_LOG_PATH: string;
|
|
3
|
+
/** Kinds the Factory reader accepts. Keep in lockstep with watchdogLog.ts. */
|
|
4
|
+
export type WatchdogEventKind = 'tick' | 'decision' | 'nudge' | 'rotate' | 'error';
|
|
5
|
+
/** JSONL row shape — a verbatim replica of the Factory reader's WatchdogEvent. */
|
|
6
|
+
export interface WatchdogEvent {
|
|
7
|
+
ts: number;
|
|
8
|
+
kind: WatchdogEventKind;
|
|
9
|
+
terminalId?: string;
|
|
10
|
+
agentType?: string;
|
|
11
|
+
message: string;
|
|
12
|
+
reason?: string;
|
|
13
|
+
/** For 'tick' events: the session lines the watchdog actually read. */
|
|
14
|
+
tailLines?: string[];
|
|
15
|
+
/** For 'tick' / 'decision' events: how long the terminal had been stalled. */
|
|
16
|
+
stalledForMs?: number;
|
|
17
|
+
/** Carried so the UI can show what the agent was stuck on. */
|
|
18
|
+
lastUserMessage?: string;
|
|
19
|
+
lastAssistantMessage?: string;
|
|
20
|
+
/** For a 'decision'/'nudge' that injects: the exact text delivered. */
|
|
21
|
+
nudgeText?: string;
|
|
22
|
+
}
|
|
23
|
+
/** Serialize one event to a JSONL line (no trailing newline). */
|
|
24
|
+
export declare function formatEvent(ev: WatchdogEvent): string;
|
|
25
|
+
/**
|
|
26
|
+
* Cap on retained log lines. Mirrors the Factory reader's expectation that the
|
|
27
|
+
* writer trims (watchdogLog.ts trimToLast). Large enough to keep a useful
|
|
28
|
+
* history, small enough to bound the file the UI polls.
|
|
29
|
+
*/
|
|
30
|
+
export declare const WATCHDOG_LOG_MAX_LINES = 2000;
|
|
31
|
+
/** Trim a JSONL body to the last `maxLines` events (same logic as the reader). */
|
|
32
|
+
export declare function trimToLast(text: string, maxLines: number): string;
|
|
33
|
+
/**
|
|
34
|
+
* Append events to the log under a file lock, trimming to WATCHDOG_LOG_MAX_LINES
|
|
35
|
+
* so the file never grows unbounded. Concurrent watchdog ticks share the lock so
|
|
36
|
+
* their appends never interleave into a corrupt line. Best-effort: a filesystem
|
|
37
|
+
* error is swallowed (the Factory card tolerates a stale/missing log), never
|
|
38
|
+
* throwing into the tick.
|
|
39
|
+
*/
|
|
40
|
+
export declare function appendWatchdogEvents(events: WatchdogEvent[], opts?: {
|
|
41
|
+
logPath?: string;
|
|
42
|
+
maxLines?: number;
|
|
43
|
+
}): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical watchdog event log (watchdog-brain-v2).
|
|
3
|
+
*
|
|
4
|
+
* The Factory Floor renders a read-only "Watchdog activity" card from the JSONL
|
|
5
|
+
* feed at ~/.agents/.cache/logs/watchdog.log. That feed was historically written
|
|
6
|
+
* by the retired extension-side watchdog; now the always-on CLI watchdog owns it.
|
|
7
|
+
*
|
|
8
|
+
* The event SHAPE here is a deliberate, verbatim replica of the reader in
|
|
9
|
+
* apps/factory/src/core/watchdogLog.ts (WatchdogEvent / WatchdogEventKind /
|
|
10
|
+
* WATCHDOG_LOG_PATH / the trim cap). The repo forbids cross-app imports
|
|
11
|
+
* (CLAUDE.md repo map), so the two files are kept in sync by hand — a change to
|
|
12
|
+
* the Factory reader's shape must be mirrored here. There is no import between
|
|
13
|
+
* them.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as os from 'os';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import { withFileLock, atomicWriteFileSync, ensureLockTarget } from '../fs-atomic.js';
|
|
19
|
+
/** Same path the Factory reader pulls (apps/factory/src/core/watchdogLog.ts). */
|
|
20
|
+
export const WATCHDOG_LOG_PATH = path.join(os.homedir(), '.agents', '.cache', 'logs', 'watchdog.log');
|
|
21
|
+
/** Serialize one event to a JSONL line (no trailing newline). */
|
|
22
|
+
export function formatEvent(ev) {
|
|
23
|
+
return JSON.stringify(ev);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Cap on retained log lines. Mirrors the Factory reader's expectation that the
|
|
27
|
+
* writer trims (watchdogLog.ts trimToLast). Large enough to keep a useful
|
|
28
|
+
* history, small enough to bound the file the UI polls.
|
|
29
|
+
*/
|
|
30
|
+
export const WATCHDOG_LOG_MAX_LINES = 2000;
|
|
31
|
+
/** Trim a JSONL body to the last `maxLines` events (same logic as the reader). */
|
|
32
|
+
export function trimToLast(text, maxLines) {
|
|
33
|
+
const lines = text.split('\n').filter((l) => l.trim().length > 0);
|
|
34
|
+
if (lines.length <= maxLines)
|
|
35
|
+
return lines.join('\n') + (lines.length ? '\n' : '');
|
|
36
|
+
return lines.slice(lines.length - maxLines).join('\n') + '\n';
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Append events to the log under a file lock, trimming to WATCHDOG_LOG_MAX_LINES
|
|
40
|
+
* so the file never grows unbounded. Concurrent watchdog ticks share the lock so
|
|
41
|
+
* their appends never interleave into a corrupt line. Best-effort: a filesystem
|
|
42
|
+
* error is swallowed (the Factory card tolerates a stale/missing log), never
|
|
43
|
+
* throwing into the tick.
|
|
44
|
+
*/
|
|
45
|
+
export function appendWatchdogEvents(events, opts = {}) {
|
|
46
|
+
if (events.length === 0)
|
|
47
|
+
return;
|
|
48
|
+
const logPath = opts.logPath ?? WATCHDOG_LOG_PATH;
|
|
49
|
+
const maxLines = opts.maxLines ?? WATCHDOG_LOG_MAX_LINES;
|
|
50
|
+
const lockPath = path.join(path.dirname(logPath), '.watchdog.log.lock');
|
|
51
|
+
try {
|
|
52
|
+
ensureLockTarget(lockPath);
|
|
53
|
+
withFileLock(lockPath, () => {
|
|
54
|
+
let existing = '';
|
|
55
|
+
try {
|
|
56
|
+
existing = fs.readFileSync(logPath, 'utf8');
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
/* first write — no file yet */
|
|
60
|
+
}
|
|
61
|
+
const appended = events.map(formatEvent).join('\n') + '\n';
|
|
62
|
+
const body = trimToLast(existing + appended, maxLines);
|
|
63
|
+
atomicWriteFileSync(logPath, body);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* best-effort: never let logging break a tick */
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The watchdog, expressed as a routine.
|
|
3
|
+
*
|
|
4
|
+
* The always-on watchdog is not a bespoke subsystem — it is a plain `command`
|
|
5
|
+
* routine the daemon's existing scheduler fires: `agents watchdog --nudge`, once
|
|
6
|
+
* every couple of minutes. Enabling / disabling the watchdog is enabling /
|
|
7
|
+
* disabling that routine, so there is ONE durable, inspectable concept
|
|
8
|
+
* (`agents routines list` shows it; the scheduler catches it up if the daemon was
|
|
9
|
+
* down) instead of a private sentinel file and a hand-rolled `--watch` loop.
|
|
10
|
+
*
|
|
11
|
+
* A `command` routine — not `agent`/`workflow` — because the tick is deterministic
|
|
12
|
+
* housekeeping: no LLM, no auth, no sandbox. Each fire runs ONE bounded tick (the
|
|
13
|
+
* cron is the loop), so the daemon's per-fire model fits without a long-lived
|
|
14
|
+
* process.
|
|
15
|
+
*/
|
|
16
|
+
import { type JobConfig } from '../routines.js';
|
|
17
|
+
/** The routine name. `agents routines list` / `readJob` key on this. */
|
|
18
|
+
export declare const WATCHDOG_ROUTINE_NAME = "watchdog";
|
|
19
|
+
/**
|
|
20
|
+
* Fire every 2 minutes. Mirrors the extension's 120s tick, and is well inside the
|
|
21
|
+
* 5m stall threshold and 20m nudge cooldown, so a stall is noticed promptly
|
|
22
|
+
* without the daemon spawning a tick more often than the cooldown can matter.
|
|
23
|
+
*/
|
|
24
|
+
export declare const WATCHDOG_ROUTINE_SCHEDULE = "*/2 * * * *";
|
|
25
|
+
/** The plain shell each fire runs: one bounded, deterministic nudge tick. */
|
|
26
|
+
export declare const WATCHDOG_ROUTINE_COMMAND = "agents watchdog --nudge";
|
|
27
|
+
/**
|
|
28
|
+
* Build the watchdog routine config. Pure — touches no disk. `mode`/`effort`/
|
|
29
|
+
* `timeout` are required by JobConfig but ignored for `command` routines;
|
|
30
|
+
* writeJob() drops the default ('auto'/'10m') values so the on-disk file stays
|
|
31
|
+
* to the point (name + schedule + command + enabled).
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildWatchdogRoutine(enabled: boolean): JobConfig;
|
|
34
|
+
/** Is the watchdog routine present on disk (user or system layer)? */
|
|
35
|
+
export declare function watchdogRoutineExists(): boolean;
|
|
36
|
+
/** Is the watchdog routine present AND enabled? */
|
|
37
|
+
export declare function isWatchdogRoutineEnabled(): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Ensure the watchdog routine exists with the requested enabled state, preserving
|
|
40
|
+
* any user edits. Absent → create it; present → only flip `enabled` (never clobber
|
|
41
|
+
* a schedule or command the user tuned). Idempotent: calling it twice with the
|
|
42
|
+
* same state is a no-op after the first.
|
|
43
|
+
*/
|
|
44
|
+
export declare function ensureWatchdogRoutine(enabled: boolean): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The watchdog, expressed as a routine.
|
|
3
|
+
*
|
|
4
|
+
* The always-on watchdog is not a bespoke subsystem — it is a plain `command`
|
|
5
|
+
* routine the daemon's existing scheduler fires: `agents watchdog --nudge`, once
|
|
6
|
+
* every couple of minutes. Enabling / disabling the watchdog is enabling /
|
|
7
|
+
* disabling that routine, so there is ONE durable, inspectable concept
|
|
8
|
+
* (`agents routines list` shows it; the scheduler catches it up if the daemon was
|
|
9
|
+
* down) instead of a private sentinel file and a hand-rolled `--watch` loop.
|
|
10
|
+
*
|
|
11
|
+
* A `command` routine — not `agent`/`workflow` — because the tick is deterministic
|
|
12
|
+
* housekeeping: no LLM, no auth, no sandbox. Each fire runs ONE bounded tick (the
|
|
13
|
+
* cron is the loop), so the daemon's per-fire model fits without a long-lived
|
|
14
|
+
* process.
|
|
15
|
+
*/
|
|
16
|
+
import { readJob, writeJob, setJobEnabled } from '../routines.js';
|
|
17
|
+
/** The routine name. `agents routines list` / `readJob` key on this. */
|
|
18
|
+
export const WATCHDOG_ROUTINE_NAME = 'watchdog';
|
|
19
|
+
/**
|
|
20
|
+
* Fire every 2 minutes. Mirrors the extension's 120s tick, and is well inside the
|
|
21
|
+
* 5m stall threshold and 20m nudge cooldown, so a stall is noticed promptly
|
|
22
|
+
* without the daemon spawning a tick more often than the cooldown can matter.
|
|
23
|
+
*/
|
|
24
|
+
export const WATCHDOG_ROUTINE_SCHEDULE = '*/2 * * * *';
|
|
25
|
+
/** The plain shell each fire runs: one bounded, deterministic nudge tick. */
|
|
26
|
+
export const WATCHDOG_ROUTINE_COMMAND = 'agents watchdog --nudge';
|
|
27
|
+
/**
|
|
28
|
+
* Build the watchdog routine config. Pure — touches no disk. `mode`/`effort`/
|
|
29
|
+
* `timeout` are required by JobConfig but ignored for `command` routines;
|
|
30
|
+
* writeJob() drops the default ('auto'/'10m') values so the on-disk file stays
|
|
31
|
+
* to the point (name + schedule + command + enabled).
|
|
32
|
+
*/
|
|
33
|
+
export function buildWatchdogRoutine(enabled) {
|
|
34
|
+
return {
|
|
35
|
+
name: WATCHDOG_ROUTINE_NAME,
|
|
36
|
+
schedule: WATCHDOG_ROUTINE_SCHEDULE,
|
|
37
|
+
command: WATCHDOG_ROUTINE_COMMAND,
|
|
38
|
+
enabled,
|
|
39
|
+
mode: 'auto',
|
|
40
|
+
effort: 'auto',
|
|
41
|
+
timeout: '10m',
|
|
42
|
+
prompt: '',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Is the watchdog routine present on disk (user or system layer)? */
|
|
46
|
+
export function watchdogRoutineExists() {
|
|
47
|
+
return readJob(WATCHDOG_ROUTINE_NAME) !== null;
|
|
48
|
+
}
|
|
49
|
+
/** Is the watchdog routine present AND enabled? */
|
|
50
|
+
export function isWatchdogRoutineEnabled() {
|
|
51
|
+
const job = readJob(WATCHDOG_ROUTINE_NAME);
|
|
52
|
+
return job !== null && job.enabled === true;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Ensure the watchdog routine exists with the requested enabled state, preserving
|
|
56
|
+
* any user edits. Absent → create it; present → only flip `enabled` (never clobber
|
|
57
|
+
* a schedule or command the user tuned). Idempotent: calling it twice with the
|
|
58
|
+
* same state is a no-op after the first.
|
|
59
|
+
*/
|
|
60
|
+
export function ensureWatchdogRoutine(enabled) {
|
|
61
|
+
const existing = readJob(WATCHDOG_ROUTINE_NAME);
|
|
62
|
+
if (existing === null) {
|
|
63
|
+
writeJob(buildWatchdogRoutine(enabled));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (existing.enabled !== enabled) {
|
|
67
|
+
setJobEnabled(WATCHDOG_ROUTINE_NAME, enabled);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import type { ActiveSession } from '../session/active.js';
|
|
32
32
|
import { type InjectRail } from '../terminal/resolve.js';
|
|
33
|
-
import { type
|
|
33
|
+
import { type InjectResult, type InjectTarget } from '../terminal/inject.js';
|
|
34
|
+
import { type StallStatus, type WatchdogCandidate } from './watchdog.js';
|
|
35
|
+
import { type OpenBlock } from '../feed.js';
|
|
34
36
|
/** Per-session policy sentinel. `keep` is the default (watchdog may nudge). */
|
|
35
37
|
export type WatchdogPolicy = 'off' | 'keep' | 'handsoff';
|
|
36
38
|
/** Stall / cooldown / dormant thresholds (ms). Defaults mirror read.ts. */
|
|
@@ -48,12 +50,15 @@ export interface WatchdogTickOptions {
|
|
|
48
50
|
/** Nudge text delivered into the terminal. Default "Continue." */
|
|
49
51
|
nudgeText?: string;
|
|
50
52
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
53
|
+
* Force the LLM decider (`agents run`) on EVERY stalled candidate instead of the
|
|
54
|
+
* hybrid path. Default false. Even when false the tick still ESCALATES the
|
|
55
|
+
* judgment-heavy cases (parked-on-question, ambiguous stalls) to the smart brain
|
|
56
|
+
* — `smart: true` additionally routes the obvious promise-without-toolcall
|
|
57
|
+
* nudges through it. Non-reproducible; the deterministic pre-filter is the
|
|
58
|
+
* reproducible default.
|
|
54
59
|
*/
|
|
55
60
|
smart?: boolean;
|
|
56
|
-
/** Agent the smart decider runs as. Default 'claude'. */
|
|
61
|
+
/** Agent the smart decider runs as (and the built-in fallback prompt). Default 'claude'. */
|
|
57
62
|
smartAgent?: string;
|
|
58
63
|
/** Threshold overrides. Missing fields fall back to DEFAULT_THRESHOLDS. */
|
|
59
64
|
thresholds?: Partial<WatchdogThresholds>;
|
|
@@ -73,7 +78,25 @@ export interface WatchdogTickOptions {
|
|
|
73
78
|
tailFor?: (s: ActiveSession) => string[];
|
|
74
79
|
/** Per-session policy. Default = the on-disk sentinel. */
|
|
75
80
|
policyFor?: (s: ActiveSession) => WatchdogPolicy;
|
|
81
|
+
/**
|
|
82
|
+
* The smart brain: given a stalled candidate, decide drive-forward vs
|
|
83
|
+
* leave-for-human and craft the message. Default resolves a `watchdog` workflow
|
|
84
|
+
* (repo/user override + `model:` frontmatter) and, failing that, the improved
|
|
85
|
+
* built-in prompt — both via `agents run … --mode plan`. Tests inject a
|
|
86
|
+
* synthetic decider so escalation is exercised without shelling out.
|
|
87
|
+
*/
|
|
88
|
+
smartDecider?: SmartDecider;
|
|
89
|
+
/** Open feed block for a session (parked-on-question detection). Default reads the feed. */
|
|
90
|
+
openBlockFor?: (s: ActiveSession) => OpenBlock | null;
|
|
91
|
+
/** Inject primitive. Default injectIntoTerminal — tests capture the resolved target. */
|
|
92
|
+
injectFn?: (target: InjectTarget, text: string, opts: {
|
|
93
|
+
dryRun?: boolean;
|
|
94
|
+
}) => Promise<InjectResult>;
|
|
95
|
+
/** Override the canonical watchdog.log path (tests point at a tmp file). */
|
|
96
|
+
logPath?: string;
|
|
76
97
|
}
|
|
98
|
+
/** The smart brain seam: a stalled candidate in, a nudge decision out. */
|
|
99
|
+
export type SmartDecider = (session: ActiveSession, candidate: WatchdogCandidate) => Promise<NudgeDecision>;
|
|
77
100
|
/** What the tick decided for a single session — the row `--json` / the tray reads. */
|
|
78
101
|
export interface SessionOutcome {
|
|
79
102
|
sessionId?: string;
|
|
@@ -88,15 +111,19 @@ export interface SessionOutcome {
|
|
|
88
111
|
policy: WatchdogPolicy;
|
|
89
112
|
decision: 'nudge' | 'skip';
|
|
90
113
|
reason: string;
|
|
91
|
-
/** The resolved rail, when
|
|
114
|
+
/** The resolved rail, when delivered by injecting into a terminal split. */
|
|
92
115
|
rail?: InjectRail;
|
|
116
|
+
/** How the nudge was (or would be) delivered: inject | mailbox | resume. */
|
|
117
|
+
via?: NudgeVia;
|
|
93
118
|
/** True when resolveInjectTarget said addressable (only meaningful once we'd nudge). */
|
|
94
119
|
addressable?: boolean;
|
|
95
|
-
/** True when a nudge was actually delivered this tick. */
|
|
120
|
+
/** True when a nudge was actually delivered this tick (any mechanism). */
|
|
96
121
|
injected?: boolean;
|
|
97
122
|
/** The text that was (or would be) delivered. */
|
|
98
123
|
nudgeText?: string;
|
|
99
124
|
}
|
|
125
|
+
/** Delivery mechanism the answer-router picked for a nudge. */
|
|
126
|
+
export type NudgeVia = 'inject' | 'mailbox' | 'resume';
|
|
100
127
|
export interface WatchdogTickResult {
|
|
101
128
|
atMs: number;
|
|
102
129
|
/** Whether this tick was allowed to inject (opts.nudge). */
|
|
@@ -118,6 +145,23 @@ export interface WatchdogTickResult {
|
|
|
118
145
|
export declare function readPolicySentinel(dir: string, sessionId: string): WatchdogPolicy;
|
|
119
146
|
/** Write a per-session policy sentinel (used by the CLI `agents watchdog policy`). */
|
|
120
147
|
export declare function writePolicySentinel(dir: string, sessionId: string, policy: WatchdogPolicy): void;
|
|
148
|
+
/** A decide result shared by the deterministic and smart paths. `text` overrides the default nudge text. */
|
|
149
|
+
export interface NudgeDecision {
|
|
150
|
+
nudge: boolean;
|
|
151
|
+
reason: string;
|
|
152
|
+
text?: string;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The smart brain. Resolves a `watchdog` workflow for the session's cwd
|
|
156
|
+
* (project > user > system precedence, via resolveWorkflowRef) so a repo/user
|
|
157
|
+
* override AND `model:` frontmatter come for free; when a workflow resolves it
|
|
158
|
+
* runs `agents run watchdog --mode plan <prompt>`, else it falls back to the
|
|
159
|
+
* improved built-in prompt via `agents run <agent> --mode plan <prompt>`. Plan
|
|
160
|
+
* mode keeps the decider read-only. Best-effort and NON-deterministic: any
|
|
161
|
+
* failure (decider unavailable, no verdict) returns a SAFE skip — a parked
|
|
162
|
+
* question we cannot judge is left for the human, never blindly nudged.
|
|
163
|
+
*/
|
|
164
|
+
export declare function makeDefaultSmartDecider(agent: string): SmartDecider;
|
|
121
165
|
/**
|
|
122
166
|
* Run ONE watchdog pass. Returns a structured outcome per live session; injects
|
|
123
167
|
* only when `opts.nudge` is set AND the safety gate says addressable AND policy
|