@phnx-labs/agents-cli 1.20.35 → 1.20.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/LICENSE +185 -21
  3. package/README.md +8 -4
  4. package/dist/commands/audit.d.ts +14 -0
  5. package/dist/commands/audit.js +68 -0
  6. package/dist/commands/browser.js +82 -8
  7. package/dist/commands/check.d.ts +15 -0
  8. package/dist/commands/check.js +84 -0
  9. package/dist/commands/cloud.js +143 -3
  10. package/dist/commands/computer.js +81 -0
  11. package/dist/commands/daemon.js +4 -1
  12. package/dist/commands/doctor.js +1 -89
  13. package/dist/commands/events.js +3 -3
  14. package/dist/commands/exec.d.ts +19 -0
  15. package/dist/commands/exec.js +277 -11
  16. package/dist/commands/hosts.js +10 -6
  17. package/dist/commands/inspect.js +8 -9
  18. package/dist/commands/lock.d.ts +12 -0
  19. package/dist/commands/lock.js +70 -0
  20. package/dist/commands/message.d.ts +15 -0
  21. package/dist/commands/message.js +56 -0
  22. package/dist/commands/routines.js +101 -5
  23. package/dist/commands/secrets-migrate.js +106 -57
  24. package/dist/commands/secrets.d.ts +31 -18
  25. package/dist/commands/secrets.js +156 -75
  26. package/dist/commands/serve.d.ts +10 -0
  27. package/dist/commands/serve.js +37 -0
  28. package/dist/commands/sessions-inject.d.ts +14 -0
  29. package/dist/commands/sessions-inject.js +111 -0
  30. package/dist/commands/sessions-picker.d.ts +2 -0
  31. package/dist/commands/sessions-picker.js +24 -3
  32. package/dist/commands/sessions-resume.js +20 -8
  33. package/dist/commands/sessions.d.ts +71 -1
  34. package/dist/commands/sessions.js +365 -37
  35. package/dist/commands/setup.js +4 -2
  36. package/dist/commands/sync.d.ts +3 -1
  37. package/dist/commands/sync.js +156 -4
  38. package/dist/commands/teams.js +217 -0
  39. package/dist/commands/versions.js +2 -4
  40. package/dist/commands/watchdog.d.ts +18 -0
  41. package/dist/commands/watchdog.js +238 -0
  42. package/dist/index.js +25 -2
  43. package/dist/lib/audit/log.d.ts +92 -0
  44. package/dist/lib/audit/log.js +177 -0
  45. package/dist/lib/auto-pull.js +2 -1
  46. package/dist/lib/browser/chrome.d.ts +10 -0
  47. package/dist/lib/browser/chrome.js +18 -7
  48. package/dist/lib/browser/drivers/ssh.js +2 -1
  49. package/dist/lib/browser/har.d.ts +84 -0
  50. package/dist/lib/browser/har.js +77 -0
  51. package/dist/lib/browser/ipc.js +24 -3
  52. package/dist/lib/browser/profiles.d.ts +1 -1
  53. package/dist/lib/browser/profiles.js +8 -10
  54. package/dist/lib/browser/refs.d.ts +65 -0
  55. package/dist/lib/browser/refs.js +73 -1
  56. package/dist/lib/browser/runtime-state.js +1 -0
  57. package/dist/lib/browser/service.d.ts +38 -2
  58. package/dist/lib/browser/service.js +112 -8
  59. package/dist/lib/browser/types.d.ts +14 -1
  60. package/dist/lib/budget/live-cloud.d.ts +42 -0
  61. package/dist/lib/budget/live-cloud.js +79 -0
  62. package/dist/lib/budget/live-team.d.ts +31 -0
  63. package/dist/lib/budget/live-team.js +115 -0
  64. package/dist/lib/cloud/codex.js +4 -0
  65. package/dist/lib/cloud/rush.d.ts +12 -1
  66. package/dist/lib/cloud/rush.js +13 -3
  67. package/dist/lib/cloud/types.d.ts +9 -0
  68. package/dist/lib/computer/dispatch.d.ts +8 -0
  69. package/dist/lib/computer/dispatch.js +125 -0
  70. package/dist/lib/computer/loop.d.ts +62 -0
  71. package/dist/lib/computer/loop.js +98 -0
  72. package/dist/lib/computer/model.d.ts +44 -0
  73. package/dist/lib/computer/model.js +157 -0
  74. package/dist/lib/concurrency.d.ts +19 -0
  75. package/dist/lib/concurrency.js +33 -0
  76. package/dist/lib/daemon.d.ts +57 -0
  77. package/dist/lib/daemon.js +192 -16
  78. package/dist/lib/devices/registry.d.ts +7 -0
  79. package/dist/lib/devices/registry.js +24 -0
  80. package/dist/lib/devices/tailscale.js +1 -1
  81. package/dist/lib/drift.d.ts +52 -0
  82. package/dist/lib/drift.js +112 -0
  83. package/dist/lib/events.d.ts +1 -1
  84. package/dist/lib/events.js +31 -13
  85. package/dist/lib/exec.d.ts +17 -0
  86. package/dist/lib/exec.js +79 -13
  87. package/dist/lib/git.d.ts +27 -0
  88. package/dist/lib/git.js +56 -1
  89. package/dist/lib/hooks/cache.d.ts +6 -0
  90. package/dist/lib/hooks/cache.js +54 -12
  91. package/dist/lib/hooks.d.ts +27 -0
  92. package/dist/lib/hooks.js +127 -8
  93. package/dist/lib/hosts/dispatch.d.ts +15 -0
  94. package/dist/lib/hosts/dispatch.js +39 -6
  95. package/dist/lib/hosts/logs.js +30 -1
  96. package/dist/lib/hosts/option.js +1 -1
  97. package/dist/lib/hosts/passthrough.js +3 -1
  98. package/dist/lib/hosts/ready.d.ts +29 -6
  99. package/dist/lib/hosts/ready.js +66 -15
  100. package/dist/lib/hosts/registry.d.ts +19 -2
  101. package/dist/lib/hosts/registry.js +58 -2
  102. package/dist/lib/hosts/remote-cmd.d.ts +62 -1
  103. package/dist/lib/hosts/remote-cmd.js +70 -1
  104. package/dist/lib/hosts/remote-os.d.ts +17 -0
  105. package/dist/lib/hosts/remote-os.js +30 -0
  106. package/dist/lib/hosts/session-index.d.ts +34 -0
  107. package/dist/lib/hosts/session-index.js +56 -0
  108. package/dist/lib/hosts/tasks.d.ts +14 -0
  109. package/dist/lib/hosts/tasks.js +15 -0
  110. package/dist/lib/lock.d.ts +93 -0
  111. package/dist/lib/lock.js +207 -0
  112. package/dist/lib/loop.js +16 -1
  113. package/dist/lib/machine-id.d.ts +21 -0
  114. package/dist/lib/machine-id.js +26 -0
  115. package/dist/lib/mailbox-target.d.ts +36 -0
  116. package/dist/lib/mailbox-target.js +45 -0
  117. package/dist/lib/mailbox.d.ts +47 -0
  118. package/dist/lib/mailbox.js +194 -0
  119. package/dist/lib/mcp.d.ts +5 -0
  120. package/dist/lib/mcp.js +24 -8
  121. package/dist/lib/migrate.d.ts +19 -0
  122. package/dist/lib/migrate.js +134 -26
  123. package/dist/lib/overdue.js +3 -0
  124. package/dist/lib/picker.d.ts +2 -0
  125. package/dist/lib/picker.js +4 -1
  126. package/dist/lib/platform/exec.d.ts +46 -0
  127. package/dist/lib/platform/exec.js +74 -0
  128. package/dist/lib/platform/process.d.ts +31 -0
  129. package/dist/lib/platform/process.js +34 -1
  130. package/dist/lib/platform/winpath.js +2 -0
  131. package/dist/lib/plugins.js +16 -6
  132. package/dist/lib/profiles.d.ts +25 -0
  133. package/dist/lib/profiles.js +22 -6
  134. package/dist/lib/pty-client.js +2 -1
  135. package/dist/lib/rotate.d.ts +61 -0
  136. package/dist/lib/rotate.js +52 -0
  137. package/dist/lib/routines.d.ts +40 -2
  138. package/dist/lib/routines.js +66 -8
  139. package/dist/lib/runner.d.ts +11 -2
  140. package/dist/lib/runner.js +49 -7
  141. package/dist/lib/scheduler.js +6 -1
  142. package/dist/lib/secrets/bundles.d.ts +60 -4
  143. package/dist/lib/secrets/bundles.js +131 -12
  144. package/dist/lib/secrets/filestore.d.ts +3 -0
  145. package/dist/lib/secrets/filestore.js +42 -16
  146. package/dist/lib/secrets/index.d.ts +43 -2
  147. package/dist/lib/secrets/index.js +102 -3
  148. package/dist/lib/secrets/mcp.d.ts +93 -0
  149. package/dist/lib/secrets/mcp.js +205 -0
  150. package/dist/lib/secrets/remote.js +12 -5
  151. package/dist/lib/secrets/sync.js +83 -4
  152. package/dist/lib/secrets/windows.js +14 -3
  153. package/dist/lib/serve/data.d.ts +81 -0
  154. package/dist/lib/serve/data.js +91 -0
  155. package/dist/lib/serve/page.d.ts +7 -0
  156. package/dist/lib/serve/page.js +140 -0
  157. package/dist/lib/serve/server.d.ts +46 -0
  158. package/dist/lib/serve/server.js +115 -0
  159. package/dist/lib/session/active.d.ts +54 -0
  160. package/dist/lib/session/active.js +190 -19
  161. package/dist/lib/session/discover.d.ts +37 -0
  162. package/dist/lib/session/discover.js +111 -28
  163. package/dist/lib/session/inject.d.ts +18 -0
  164. package/dist/lib/session/inject.js +21 -0
  165. package/dist/lib/session/parse.js +23 -20
  166. package/dist/lib/session/pid-registry.d.ts +1 -0
  167. package/dist/lib/session/pid-registry.js +24 -0
  168. package/dist/lib/session/provenance.d.ts +14 -2
  169. package/dist/lib/session/provenance.js +39 -8
  170. package/dist/lib/session/remote-active.js +19 -7
  171. package/dist/lib/session/remote-list.d.ts +51 -0
  172. package/dist/lib/session/remote-list.js +213 -0
  173. package/dist/lib/session/remote.d.ts +7 -1
  174. package/dist/lib/session/remote.js +16 -2
  175. package/dist/lib/session/sync/config.d.ts +1 -15
  176. package/dist/lib/session/sync/config.js +4 -20
  177. package/dist/lib/session/types.d.ts +17 -0
  178. package/dist/lib/shims.d.ts +36 -6
  179. package/dist/lib/shims.js +91 -29
  180. package/dist/lib/ssh-exec.js +2 -0
  181. package/dist/lib/ssh-tunnel.js +2 -1
  182. package/dist/lib/startup/command-registry.d.ts +6 -0
  183. package/dist/lib/startup/command-registry.js +13 -1
  184. package/dist/lib/state.d.ts +13 -0
  185. package/dist/lib/state.js +103 -9
  186. package/dist/lib/sync-umbrella.d.ts +14 -7
  187. package/dist/lib/sync-umbrella.js +17 -9
  188. package/dist/lib/teams/forEach.d.ts +110 -0
  189. package/dist/lib/teams/forEach.js +186 -0
  190. package/dist/lib/teams/index.d.ts +1 -0
  191. package/dist/lib/teams/index.js +1 -0
  192. package/dist/lib/teams/pr-watch.d.ts +226 -0
  193. package/dist/lib/teams/pr-watch.js +371 -0
  194. package/dist/lib/teams/supervisor.d.ts +14 -1
  195. package/dist/lib/teams/supervisor.js +19 -0
  196. package/dist/lib/teams/worktree.d.ts +9 -0
  197. package/dist/lib/teams/worktree.js +32 -0
  198. package/dist/lib/terminal/backends/index.d.ts +2 -1
  199. package/dist/lib/terminal/backends/index.js +3 -1
  200. package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
  201. package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
  202. package/dist/lib/terminal/index.d.ts +4 -1
  203. package/dist/lib/terminal/index.js +4 -1
  204. package/dist/lib/terminal/inject.d.ts +204 -0
  205. package/dist/lib/terminal/inject.js +247 -0
  206. package/dist/lib/terminal/resolve.d.ts +64 -0
  207. package/dist/lib/terminal/resolve.js +90 -0
  208. package/dist/lib/terminal/types.d.ts +1 -1
  209. package/dist/lib/triggers/webhook.d.ts +85 -0
  210. package/dist/lib/triggers/webhook.js +141 -0
  211. package/dist/lib/versions.d.ts +23 -0
  212. package/dist/lib/versions.js +119 -13
  213. package/dist/lib/watchdog/index.d.ts +3 -0
  214. package/dist/lib/watchdog/index.js +5 -0
  215. package/dist/lib/watchdog/read.d.ts +35 -0
  216. package/dist/lib/watchdog/read.js +149 -0
  217. package/dist/lib/watchdog/runner.d.ts +127 -0
  218. package/dist/lib/watchdog/runner.js +322 -0
  219. package/dist/lib/watchdog/watchdog.d.ts +40 -0
  220. package/dist/lib/watchdog/watchdog.js +166 -0
  221. package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
  222. package/dist/lib/watchdog/watchdogTail.js +154 -0
  223. package/dist/lib/workflows.d.ts +166 -0
  224. package/dist/lib/workflows.js +193 -0
  225. package/package.json +5 -4
@@ -0,0 +1,238 @@
1
+ /**
2
+ * `agents watchdog` — the watchdog CONSUMER (RUSH-1415).
3
+ *
4
+ * Runs the tick loop that ties the merged pieces together: list active sessions,
5
+ * classify stalls, read the tail, decide (deterministic promise-without-toolcall
6
+ * by default), run the resolver safety gate, and inject "Continue." into the EXACT
7
+ * split — all without the Swift menu-bar. See src/lib/watchdog/runner.ts.
8
+ *
9
+ * agents watchdog one tick, dry — prints what it WOULD nudge/skip and why
10
+ * agents watchdog --nudge one tick, actually injects (explicit opt-in)
11
+ * agents watchdog --watch daemon loop (poll every --interval)
12
+ * agents watchdog --json machine-readable tick output (for the menu-bar)
13
+ * agents watchdog enable|disable flip the global auto-nudge sentinel (default OFF)
14
+ * agents watchdog policy <id> <p> per-session policy: off | keep | handsoff
15
+ */
16
+ import chalk from 'chalk';
17
+ import * as fs from 'fs';
18
+ import * as path from 'path';
19
+ import { setHelpSections } from '../lib/help.js';
20
+ import { parseDuration } from '../lib/hooks/cache.js';
21
+ import { getRuntimeStateDir } from '../lib/state.js';
22
+ import { runWatchdogTick, writePolicySentinel, DEFAULT_THRESHOLDS, } from '../lib/watchdog/runner.js';
23
+ /** Default state dir the runner and these subcommands share. */
24
+ function stateDir() {
25
+ return path.join(getRuntimeStateDir(), 'watchdog');
26
+ }
27
+ /** Global auto-nudge sentinel — present = enabled. Default OFF (opt-in). */
28
+ function enabledSentinelPath() {
29
+ return path.join(stateDir(), 'enabled');
30
+ }
31
+ function isGloballyEnabled() {
32
+ return fs.existsSync(enabledSentinelPath());
33
+ }
34
+ function setGloballyEnabled(on) {
35
+ const p = enabledSentinelPath();
36
+ if (on) {
37
+ fs.mkdirSync(path.dirname(p), { recursive: true });
38
+ fs.writeFileSync(p, 'enabled\n');
39
+ }
40
+ else {
41
+ try {
42
+ fs.rmSync(p);
43
+ }
44
+ catch { /* already off */ }
45
+ }
46
+ }
47
+ /** Parse a duration flag ("60s", "5m", "1h") to ms, or fall back to `fallbackMs`. */
48
+ function durationMsOr(raw, fallbackMs) {
49
+ if (raw === undefined)
50
+ return fallbackMs;
51
+ const secs = parseDuration(raw);
52
+ return secs === null ? fallbackMs : secs * 1000;
53
+ }
54
+ function humanMs(ms) {
55
+ if (ms >= 3600_000)
56
+ return `${Math.round(ms / 3600_000)}h`;
57
+ if (ms >= 60_000)
58
+ return `${Math.round(ms / 60_000)}m`;
59
+ return `${Math.round(ms / 1000)}s`;
60
+ }
61
+ function colorForOutcome(o) {
62
+ if (o.injected)
63
+ return chalk.green;
64
+ if (o.addressable === false)
65
+ return chalk.yellow;
66
+ if (o.stall === 'stalled' && o.decision === 'nudge')
67
+ return chalk.cyan;
68
+ return chalk.dim;
69
+ }
70
+ /** Render one tick's outcomes as a human status block. */
71
+ function printTick(result, willInject) {
72
+ const { counts } = result;
73
+ const mode = willInject ? chalk.green('nudge') : chalk.dim('dry');
74
+ console.log(`${chalk.bold('watchdog')} ${mode} ` +
75
+ `${counts.total} live · ${counts.stalled} stalled · ` +
76
+ `${chalk.green(String(counts.nudged))} nudged · ` +
77
+ `${chalk.yellow(String(counts.unaddressable))} un-addressable`);
78
+ for (const o of result.outcomes) {
79
+ const tag = o.injected ? 'NUDGED'
80
+ : o.addressable === false ? 'FLAGGED'
81
+ : o.decision === 'nudge' ? 'WOULD-NUDGE'
82
+ : 'skip';
83
+ const c = colorForOutcome(o);
84
+ const who = o.label || o.sessionId?.slice(0, 8) || o.kind;
85
+ const where = o.host ? chalk.dim(`[${o.host}]`) : '';
86
+ const rail = o.rail ? chalk.dim(`→${o.rail}`) : '';
87
+ console.log(` ${c(tag.padEnd(11))} ${chalk.bold(who)} ${where} ${rail}`);
88
+ console.log(` ${chalk.dim(o.reason)}`);
89
+ }
90
+ }
91
+ async function sleep(ms) {
92
+ return new Promise((resolve) => setTimeout(resolve, ms));
93
+ }
94
+ /** Register the `agents watchdog` command tree. */
95
+ export function registerWatchdogCommand(program) {
96
+ const cmd = program
97
+ .command('watchdog')
98
+ .description('Auto-nudge stalled agent terminals: detect stalls, resolve the exact split, inject "Continue." — no menu-bar needed.')
99
+ .option('--nudge', 'Actually inject (default is a dry run that only reports what it would do)')
100
+ .option('--watch', 'Daemon loop: run a tick every --interval until interrupted')
101
+ .option('--interval <dur>', 'Poll interval in --watch mode (e.g. 30s, 1m)', '30s')
102
+ .option('--stall <dur>', 'Idle time before a session counts as stalled', humanMs(DEFAULT_THRESHOLDS.stallMs))
103
+ .option('--cooldown <dur>', 'Minimum time between nudges to the same session', humanMs(DEFAULT_THRESHOLDS.cooldownMs))
104
+ .option('--dormant <dur>', 'Idle time after which a session is left alone (dormant)', humanMs(DEFAULT_THRESHOLDS.dormantMs))
105
+ .option('--text <text>', 'Nudge text delivered into the terminal', 'Continue.')
106
+ .option('--smart', 'Use the LLM decider (agents run) instead of the deterministic path (non-reproducible)')
107
+ .option('--smart-agent <agent>', 'Agent the --smart decider runs as', 'claude')
108
+ .option('--allow-ghostty-focus', 'Permit the coarse, focus-stealing Ghostty path (off by default)')
109
+ .option('--json', 'Emit the tick result as JSON (for the menu-bar / scripts)')
110
+ .action(async (opts) => {
111
+ const thresholds = {
112
+ stallMs: durationMsOr(opts.stall, DEFAULT_THRESHOLDS.stallMs),
113
+ cooldownMs: durationMsOr(opts.cooldown, DEFAULT_THRESHOLDS.cooldownMs),
114
+ dormantMs: durationMsOr(opts.dormant, DEFAULT_THRESHOLDS.dormantMs),
115
+ };
116
+ // Injection gate: --nudge is the explicit per-run opt-in; the global sentinel
117
+ // enables the automatic daemon. Default OFF: bare `agents watchdog` is dry.
118
+ // Re-read the sentinel every tick so a running `--watch` daemon reflects a
119
+ // later `agents watchdog enable`/`disable` (or the Swift menu-bar toggle)
120
+ // from another shell without a restart.
121
+ const computeWillInject = () => opts.nudge === true || (opts.watch === true && isGloballyEnabled());
122
+ const tickOnce = async (willInject) => runWatchdogTick({
123
+ nudge: willInject,
124
+ nudgeText: opts.text,
125
+ smart: opts.smart === true,
126
+ smartAgent: opts.smartAgent,
127
+ thresholds,
128
+ allowGhosttyFocus: opts.allowGhosttyFocus === true,
129
+ stateDir: stateDir(),
130
+ });
131
+ if (!opts.watch) {
132
+ const willInject = computeWillInject();
133
+ const result = await tickOnce(willInject);
134
+ if (opts.json)
135
+ console.log(JSON.stringify(result, null, 2));
136
+ else
137
+ printTick(result, willInject);
138
+ return;
139
+ }
140
+ // Daemon loop.
141
+ const intervalMs = durationMsOr(opts.interval, 30_000);
142
+ if (!computeWillInject() && !opts.json) {
143
+ console.log(chalk.yellow(`watchdog --watch is DETECT-ONLY (global auto-nudge is ${isGloballyEnabled() ? 'on' : 'off'}). ` +
144
+ `Pass --nudge or run 'agents watchdog enable' to inject.`));
145
+ }
146
+ // eslint-disable-next-line no-constant-condition
147
+ while (true) {
148
+ // Re-evaluated each tick: picks up enable/disable flips mid-run.
149
+ const willInject = computeWillInject();
150
+ const result = await tickOnce(willInject);
151
+ if (opts.json)
152
+ console.log(JSON.stringify(result));
153
+ else
154
+ printTick(result, willInject);
155
+ await sleep(intervalMs);
156
+ }
157
+ });
158
+ setHelpSections(cmd, {
159
+ examples: `
160
+ # One tick, dry — see what it WOULD nudge and why (safe, no injection)
161
+ agents watchdog
162
+
163
+ # One tick, actually inject "Continue." into stalled+addressable splits
164
+ agents watchdog --nudge
165
+
166
+ # Watch loop every 30s, tighter stall threshold
167
+ agents watchdog --watch --interval 30s --stall 60s --cooldown 5m
168
+
169
+ # Machine-readable for the menu-bar
170
+ agents watchdog --json
171
+
172
+ # Turn on the global auto-nudge (so --watch injects without --nudge)
173
+ agents watchdog enable
174
+
175
+ # Leave one session detected-but-untouched
176
+ agents watchdog policy <sessionId> handsoff
177
+ `,
178
+ notes: `
179
+ Decision path (default, deterministic): a session is nudged only when its
180
+ transcript tail shows it ANNOUNCED an action but no tool call followed
181
+ (promise-without-toolcall) AND it is not waiting on the user. Completions and
182
+ open questions are skipped.
183
+
184
+ Safety gate: a nudge is delivered ONLY when the resolver can name the EXACT
185
+ split the agent lives in (tmux / iTerm / IDE terminal). Un-addressable stalls
186
+ (e.g. Ghostty with no tmux) are flagged for the menu-bar and SKIPPED — never
187
+ a guessed or frontmost target.
188
+
189
+ Policy: global auto-nudge defaults OFF (opt-in via 'enable' or --nudge).
190
+ Per-session: off (ignore), keep (default), handsoff (detect + flag, never inject).
191
+
192
+ State (tray-readable): ${path.join('~/.agents/.cache/state/watchdog', '{nudges,flags,last-tick}.json')}
193
+ `,
194
+ });
195
+ // --- global enable/disable/status -----------------------------------------
196
+ cmd.command('enable')
197
+ .description('Turn ON global auto-nudge (so `agents watchdog --watch` injects without --nudge).')
198
+ .action(() => {
199
+ setGloballyEnabled(true);
200
+ console.log(chalk.green('watchdog: global auto-nudge ENABLED'));
201
+ });
202
+ cmd.command('disable')
203
+ .description('Turn OFF global auto-nudge (back to detect-only unless --nudge is passed).')
204
+ .action(() => {
205
+ setGloballyEnabled(false);
206
+ console.log(chalk.yellow('watchdog: global auto-nudge DISABLED'));
207
+ });
208
+ cmd.command('status')
209
+ .description('Show whether global auto-nudge is on and where state is written.')
210
+ .option('--json', 'Emit status as JSON (for the menu-bar / scripts)')
211
+ .action((_opts, command) => {
212
+ // The parent `watchdog` command also declares --json and greedily parses it
213
+ // before dispatching here, so `watchdog status --json` lands the flag on the
214
+ // parent, not this subcommand. optsWithGlobals() merges both levels, so we
215
+ // read it correctly regardless of which command commander bound it to.
216
+ const json = command.optsWithGlobals().json === true;
217
+ const on = isGloballyEnabled();
218
+ if (json) {
219
+ console.log(JSON.stringify({ enabled: on, stateDir: stateDir() }));
220
+ return;
221
+ }
222
+ console.log(`global auto-nudge: ${on ? chalk.green('ON') : chalk.dim('off')}`);
223
+ console.log(`state dir: ${chalk.dim(stateDir())}`);
224
+ });
225
+ // --- per-session policy ----------------------------------------------------
226
+ cmd.command('policy <sessionId> <policy>')
227
+ .description('Set per-session policy: off (ignore) | keep (default) | handsoff (detect + flag, never inject).')
228
+ .action((sessionId, policy) => {
229
+ const p = policy.toLowerCase();
230
+ if (p !== 'off' && p !== 'keep' && p !== 'handsoff') {
231
+ console.error(chalk.red(`invalid policy '${policy}'. Use: off | keep | handsoff`));
232
+ process.exitCode = 1;
233
+ return;
234
+ }
235
+ writePolicySentinel(stateDir(), sessionId, p);
236
+ console.log(`watchdog: session ${chalk.bold(sessionId.slice(0, 8))} policy = ${chalk.cyan(p)}`);
237
+ });
238
+ }
package/dist/index.js CHANGED
@@ -51,7 +51,7 @@ if (IS_DEV_BUILD) {
51
51
  // module on each invocation (which loaded the whole ~50-module tree before the
52
52
  // first byte of output), the registry maps a command name to a thunk that
53
53
  // imports only what that command needs. See src/lib/startup/command-registry.ts.
54
- import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadSsh, loadPull, loadPush, loadRepo, loadSetup, } from './lib/startup/command-registry.js';
54
+ import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadSsh, loadPull, loadPush, loadRepo, loadSetup, } from './lib/startup/command-registry.js';
55
55
  import { applyGlobalHelpConventions } from './lib/help.js';
56
56
  import { renderWhatsNew } from './lib/whats-new.js';
57
57
  import { IS_WINDOWS } from './lib/platform/index.js';
@@ -183,6 +183,7 @@ Credentials and profiles:
183
183
 
184
184
  Diagnostics:
185
185
  doctor [agent[@version]] Diagnose CLI availability, sync status, and resource divergence
186
+ check CI drift gate: exit non-zero when resources are out of sync
186
187
  usage [agent] Show rate-limit and quota usage per agent
187
188
 
188
189
  Config sync:
@@ -190,6 +191,7 @@ Config sync:
190
191
  pull Clone or pull the system repo at ~/.agents/.system/
191
192
  repo init --path <dir> Scaffold your own editable repo from a template
192
193
  repo add <path|gh:user/repo> Merge an extra repo after the system repo
194
+ lock [--frozen] Write/verify agents.lock (SHA-256 of resolved resources); --frozen fails on drift
193
195
 
194
196
  Beta features:
195
197
  beta Enable preview features (factory, drive, and more)
@@ -789,6 +791,7 @@ async function registerAllEagerCommands() {
789
791
  await reg(loadTrash);
790
792
  await reg(loadRestore);
791
793
  await reg(loadDoctor);
794
+ await reg(loadCheck);
792
795
  await reg(loadStatus);
793
796
  registerExecAliasCommand(program);
794
797
  await reg(loadProfiles);
@@ -798,6 +801,7 @@ async function registerAllEagerCommands() {
798
801
  await reg(loadMenubar);
799
802
  await reg(loadBeta);
800
803
  await reg(loadSync);
804
+ await reg(loadLock);
801
805
  await reg(loadRefreshRules);
802
806
  await reg(loadDrive);
803
807
  await reg(loadFactory);
@@ -807,11 +811,13 @@ async function registerAllEagerCommands() {
807
811
  await reg(loadAlias);
808
812
  await reg(loadPty);
809
813
  await reg(loadTmux);
814
+ await reg(loadWatchdog);
810
815
  await reg(loadBrowser);
811
816
  await reg(loadComputer);
812
817
  await reg(loadHosts);
813
818
  await reg(loadLogs);
814
819
  await reg(loadEvents);
820
+ await reg(loadAudit);
815
821
  await reg(loadSsh);
816
822
  registerJobsCronAliasCommand(program, 'jobs');
817
823
  registerJobsCronAliasCommand(program, 'cron');
@@ -983,7 +989,7 @@ if (process.env.AGENTS_SKIP_MIGRATION !== '1') {
983
989
  // Bumping the suffix re-runs migrations for every user; binary releases that
984
990
  // don't change the schema must NOT re-run (they would destroy user content
985
991
  // when migration steps overlap with user-authored paths). See issue #20.
986
- const sentinelValue = 'v10';
992
+ const sentinelValue = 'v11';
987
993
  let needRun = true;
988
994
  try {
989
995
  if (fs.existsSync(sentinel) && fs.readFileSync(sentinel, 'utf-8').trim() === sentinelValue) {
@@ -1016,6 +1022,15 @@ if (process.platform === 'darwin' && process.env.AGENTS_SKIP_MIGRATION !== '1')
1016
1022
  }
1017
1023
  catch { /* never block CLI startup on the menu bar */ }
1018
1024
  }
1025
+ // Bare invocation prints the root help. Commander only auto-displays help on
1026
+ // an empty parse when subcommands are registered, and the lazy-startup path
1027
+ // registers none for a bare call — without this branch, `agents` exits
1028
+ // silently. Runs after first-run setup and migrations so those still fire;
1029
+ // exits 0 to match `agents --help` (and the pre-fix exit code).
1030
+ if (passedArgs.length === 0) {
1031
+ program.outputHelp();
1032
+ process.exit(0);
1033
+ }
1019
1034
  try {
1020
1035
  await maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequested);
1021
1036
  await program.parseAsync();
@@ -1036,6 +1051,14 @@ catch (err) {
1036
1051
  console.error(err.message);
1037
1052
  process.exit(1);
1038
1053
  }
1054
+ // A --host targeting a password-auth device throws this from resolveHost.
1055
+ // It carries an actionable message (switch to key auth / enroll as a host);
1056
+ // handling it here covers every resolveHost caller (run, hosts check/rm,
1057
+ // secrets --host) at the source instead of a catch at each call site.
1058
+ if (err.name === 'DeviceOffloadUnsupportedError') {
1059
+ console.error(err.message);
1060
+ process.exit(1);
1061
+ }
1039
1062
  }
1040
1063
  throw err;
1041
1064
  }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Cross-agent, tamper-evident audit log of every dispatched run (issue #347).
3
+ *
4
+ * Every run that reaches the single dispatch chokepoint in `agents run`
5
+ * (src/commands/exec.ts) appends ONE record here. Records form a hash chain:
6
+ * each record embeds the previous record's `hash` as `prevHash`, and its own
7
+ * `hash` is `sha256(canonicalJSON(record-without-hash))`. Because `prevHash`
8
+ * is part of the hashed payload, rewriting or reordering any record breaks the
9
+ * chain from that point forward — `verifyAuditChain()` reports the first index
10
+ * that fails to reproduce.
11
+ *
12
+ * Storage is append-only JSONL under the durable-runtime bucket
13
+ * (`~/.agents/.history/audit/log.jsonl`), one record per line. `.history/` is
14
+ * machine-local, gitignored, and NEVER synced by `agents repo push/pull` — so
15
+ * the `repo` field (a git remote url that can embed an access token) never
16
+ * lands in a version-controlled DotAgents repo, and a cross-machine pull can't
17
+ * splice a foreign chain into this one. The write path is deliberately cheap
18
+ * and non-fatal: a logging failure must never crash a run (see
19
+ * `recordDispatchedRun`). Concurrent writers (parallel teams/routines dispatch)
20
+ * are serialized by an advisory lock file so the chain never forks — see
21
+ * `appendAuditRecord`.
22
+ *
23
+ * Governance context: this is the evidence trail an operator needs to answer
24
+ * "which agent, at which version, ran against which repo, and did it succeed?"
25
+ * — the kind of automated-decision logging the EU AI Act (Art. 12) expects of
26
+ * high-risk systems, kept tamper-evident so the log itself can be trusted.
27
+ */
28
+ /** First record's `prevHash` — a fixed anchor so the chain has a root. */
29
+ export declare const GENESIS_HASH = "GENESIS";
30
+ /** A run's outcome, derived from its process exit code. */
31
+ export type AuditOutcome = 'ok' | 'fail';
32
+ /** One immutable audit record. `hash` seals every other field, `prevHash` included. */
33
+ export interface AuditRecord {
34
+ ts: string;
35
+ agent: string;
36
+ version: string;
37
+ repo: string;
38
+ mode: string;
39
+ outcome: AuditOutcome;
40
+ exit: number;
41
+ prevHash: string;
42
+ hash: string;
43
+ }
44
+ /** The caller-supplied fields — the chain fields (`prevHash`/`hash`) are computed here. */
45
+ export type AuditEntry = Omit<AuditRecord, 'prevHash' | 'hash'>;
46
+ /**
47
+ * Absolute path to the append-only audit log. Lives under `.history/` — the
48
+ * durable-but-machine-local runtime bucket that is gitignored and never synced
49
+ * by `agents repo push/pull`. Keeping it here (not under a top-level, tracked
50
+ * `~/.agents/` path) means the token-bearing `repo` field can never leak into a
51
+ * version-controlled DotAgents repo, and no cross-machine pull can fork the chain.
52
+ */
53
+ export declare function getAuditLogPath(): string;
54
+ /**
55
+ * Append one record to the hash chain and return it. Links to the previous
56
+ * record's `hash` (or `GENESIS_HASH` for the first), computes the sealing
57
+ * hash, and writes a single JSONL line. Synchronous so the record is durable
58
+ * before the caller proceeds.
59
+ *
60
+ * Read-last-hash + append run under the canonical advisory file lock
61
+ * (`withFileLock`, backed by proper-lockfile). Without it, two concurrent
62
+ * writers — the norm under parallel teams/routines dispatch — both read the
63
+ * same last hash and both write `prevHash=H`, forking the chain into a false
64
+ * "tampered" verdict. The lock forces a total order so every record links off
65
+ * the genuinely-previous one. `ensureLockTarget` creates the file first because
66
+ * proper-lockfile locks an existing path.
67
+ */
68
+ export declare function appendAuditRecord(entry: AuditEntry, logPath?: string): AuditRecord;
69
+ /**
70
+ * Walk the chain and confirm every record reproduces. A record is valid when
71
+ * (a) its `prevHash` matches the prior record's `hash` (GENESIS for the first)
72
+ * AND (b) recomputing its sealing hash from its own fields reproduces the
73
+ * stored `hash`. Returns the first failing index in `brokenAt`.
74
+ */
75
+ export declare function verifyAuditChain(logPath?: string): {
76
+ ok: boolean;
77
+ brokenAt?: number;
78
+ };
79
+ /** Read the whole chain, oldest-first. Exposed for `agents audit list`. */
80
+ export declare function readAuditLog(logPath?: string): AuditRecord[];
81
+ /**
82
+ * Record ONE dispatched run at the single exec chokepoint. Non-fatal by
83
+ * contract: any failure is caught and warned, never thrown — an audit-log
84
+ * hiccup must not crash a run that already finished.
85
+ */
86
+ export declare function recordDispatchedRun(run: {
87
+ agent: string;
88
+ version: string;
89
+ mode: string;
90
+ cwd: string;
91
+ exitCode: number;
92
+ }): void;
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Cross-agent, tamper-evident audit log of every dispatched run (issue #347).
3
+ *
4
+ * Every run that reaches the single dispatch chokepoint in `agents run`
5
+ * (src/commands/exec.ts) appends ONE record here. Records form a hash chain:
6
+ * each record embeds the previous record's `hash` as `prevHash`, and its own
7
+ * `hash` is `sha256(canonicalJSON(record-without-hash))`. Because `prevHash`
8
+ * is part of the hashed payload, rewriting or reordering any record breaks the
9
+ * chain from that point forward — `verifyAuditChain()` reports the first index
10
+ * that fails to reproduce.
11
+ *
12
+ * Storage is append-only JSONL under the durable-runtime bucket
13
+ * (`~/.agents/.history/audit/log.jsonl`), one record per line. `.history/` is
14
+ * machine-local, gitignored, and NEVER synced by `agents repo push/pull` — so
15
+ * the `repo` field (a git remote url that can embed an access token) never
16
+ * lands in a version-controlled DotAgents repo, and a cross-machine pull can't
17
+ * splice a foreign chain into this one. The write path is deliberately cheap
18
+ * and non-fatal: a logging failure must never crash a run (see
19
+ * `recordDispatchedRun`). Concurrent writers (parallel teams/routines dispatch)
20
+ * are serialized by an advisory lock file so the chain never forks — see
21
+ * `appendAuditRecord`.
22
+ *
23
+ * Governance context: this is the evidence trail an operator needs to answer
24
+ * "which agent, at which version, ran against which repo, and did it succeed?"
25
+ * — the kind of automated-decision logging the EU AI Act (Art. 12) expects of
26
+ * high-risk systems, kept tamper-evident so the log itself can be trusted.
27
+ */
28
+ import * as fs from 'fs';
29
+ import * as path from 'path';
30
+ import { spawnSync } from 'child_process';
31
+ import { sha256 } from '../staleness/fingerprint.js';
32
+ import { getHistoryDir } from '../state.js';
33
+ import { ensureLockTarget, withFileLock } from '../fs-atomic.js';
34
+ /** First record's `prevHash` — a fixed anchor so the chain has a root. */
35
+ export const GENESIS_HASH = 'GENESIS';
36
+ /**
37
+ * Absolute path to the append-only audit log. Lives under `.history/` — the
38
+ * durable-but-machine-local runtime bucket that is gitignored and never synced
39
+ * by `agents repo push/pull`. Keeping it here (not under a top-level, tracked
40
+ * `~/.agents/` path) means the token-bearing `repo` field can never leak into a
41
+ * version-controlled DotAgents repo, and no cross-machine pull can fork the chain.
42
+ */
43
+ export function getAuditLogPath() {
44
+ return path.join(getHistoryDir(), 'audit', 'log.jsonl');
45
+ }
46
+ /**
47
+ * Deterministic JSON: object keys sorted recursively so the same logical
48
+ * record always serializes to the same bytes (hashing must be reproducible
49
+ * across processes and machines). Values here are all primitives, but the
50
+ * recursion keeps it correct if the record ever nests.
51
+ */
52
+ function canonicalJSON(value) {
53
+ if (value === null || typeof value !== 'object')
54
+ return JSON.stringify(value);
55
+ if (Array.isArray(value))
56
+ return '[' + value.map(canonicalJSON).join(',') + ']';
57
+ const obj = value;
58
+ const keys = Object.keys(obj).sort();
59
+ return '{' + keys.map(k => JSON.stringify(k) + ':' + canonicalJSON(obj[k])).join(',') + '}';
60
+ }
61
+ /** Compute the sealing hash for a record whose `prevHash` is already set. */
62
+ function hashRecord(record) {
63
+ return sha256(canonicalJSON(record));
64
+ }
65
+ /** Read + parse every record in the log, oldest-first. Missing file → []. */
66
+ function readRecords(logPath) {
67
+ let raw;
68
+ try {
69
+ raw = fs.readFileSync(logPath, 'utf-8');
70
+ }
71
+ catch {
72
+ return []; // no log yet
73
+ }
74
+ const records = [];
75
+ for (const line of raw.split('\n')) {
76
+ const trimmed = line.trim();
77
+ if (!trimmed)
78
+ continue;
79
+ records.push(JSON.parse(trimmed));
80
+ }
81
+ return records;
82
+ }
83
+ /**
84
+ * Append one record to the hash chain and return it. Links to the previous
85
+ * record's `hash` (or `GENESIS_HASH` for the first), computes the sealing
86
+ * hash, and writes a single JSONL line. Synchronous so the record is durable
87
+ * before the caller proceeds.
88
+ *
89
+ * Read-last-hash + append run under the canonical advisory file lock
90
+ * (`withFileLock`, backed by proper-lockfile). Without it, two concurrent
91
+ * writers — the norm under parallel teams/routines dispatch — both read the
92
+ * same last hash and both write `prevHash=H`, forking the chain into a false
93
+ * "tampered" verdict. The lock forces a total order so every record links off
94
+ * the genuinely-previous one. `ensureLockTarget` creates the file first because
95
+ * proper-lockfile locks an existing path.
96
+ */
97
+ export function appendAuditRecord(entry, logPath = getAuditLogPath()) {
98
+ ensureLockTarget(logPath);
99
+ return withFileLock(logPath, () => {
100
+ const existing = readRecords(logPath);
101
+ const prevHash = existing.length ? existing[existing.length - 1].hash : GENESIS_HASH;
102
+ const unsealed = { ...entry, prevHash };
103
+ const record = { ...unsealed, hash: hashRecord(unsealed) };
104
+ fs.appendFileSync(logPath, JSON.stringify(record) + '\n');
105
+ return record;
106
+ });
107
+ }
108
+ /**
109
+ * Walk the chain and confirm every record reproduces. A record is valid when
110
+ * (a) its `prevHash` matches the prior record's `hash` (GENESIS for the first)
111
+ * AND (b) recomputing its sealing hash from its own fields reproduces the
112
+ * stored `hash`. Returns the first failing index in `brokenAt`.
113
+ */
114
+ export function verifyAuditChain(logPath = getAuditLogPath()) {
115
+ let records;
116
+ try {
117
+ records = readRecords(logPath);
118
+ }
119
+ catch (err) {
120
+ // A line that won't even parse is itself tamper evidence — the log is corrupt.
121
+ return { ok: false, brokenAt: 0 };
122
+ }
123
+ let expectedPrev = GENESIS_HASH;
124
+ for (let i = 0; i < records.length; i++) {
125
+ const r = records[i];
126
+ if (r.prevHash !== expectedPrev)
127
+ return { ok: false, brokenAt: i };
128
+ const { hash, ...unsealed } = r;
129
+ if (hashRecord(unsealed) !== hash)
130
+ return { ok: false, brokenAt: i };
131
+ expectedPrev = r.hash;
132
+ }
133
+ return { ok: true };
134
+ }
135
+ /** Read the whole chain, oldest-first. Exposed for `agents audit list`. */
136
+ export function readAuditLog(logPath = getAuditLogPath()) {
137
+ return readRecords(logPath);
138
+ }
139
+ /**
140
+ * Resolve a stable repo label for a run: the git remote origin url when the
141
+ * cwd is inside a repo with one, otherwise the cwd itself. Best-effort — any
142
+ * failure falls back to the cwd.
143
+ */
144
+ function repoLabel(cwd) {
145
+ try {
146
+ const res = spawnSync('git', ['-C', cwd, 'config', '--get', 'remote.origin.url'], {
147
+ encoding: 'utf-8',
148
+ timeout: 2000,
149
+ });
150
+ const url = res.status === 0 ? res.stdout.trim() : '';
151
+ return url || cwd;
152
+ }
153
+ catch {
154
+ return cwd;
155
+ }
156
+ }
157
+ /**
158
+ * Record ONE dispatched run at the single exec chokepoint. Non-fatal by
159
+ * contract: any failure is caught and warned, never thrown — an audit-log
160
+ * hiccup must not crash a run that already finished.
161
+ */
162
+ export function recordDispatchedRun(run) {
163
+ try {
164
+ appendAuditRecord({
165
+ ts: new Date().toISOString(),
166
+ agent: run.agent,
167
+ version: run.version,
168
+ repo: repoLabel(run.cwd),
169
+ mode: run.mode,
170
+ outcome: run.exitCode === 0 ? 'ok' : 'fail',
171
+ exit: run.exitCode,
172
+ });
173
+ }
174
+ catch (err) {
175
+ process.stderr.write(`[agents] audit log write failed: ${err.message}\n`);
176
+ }
177
+ }
@@ -14,6 +14,7 @@ import * as path from 'path';
14
14
  import { spawn } from 'child_process';
15
15
  import { fileURLToPath } from 'url';
16
16
  import { getFetchCacheDir } from './state.js';
17
+ import { backgroundSpawnOptions } from './platform/process.js';
17
18
  /** Where lock files and per-repo status markers live. */
18
19
  function fetchStateDir() {
19
20
  return getFetchCacheDir();
@@ -38,7 +39,7 @@ export function spawnDetachedSync() {
38
39
  return;
39
40
  try {
40
41
  const child = spawn(process.execPath, [workerPath], {
41
- detached: true,
42
+ ...backgroundSpawnOptions(),
42
43
  stdio: 'ignore',
43
44
  env: process.env,
44
45
  });
@@ -36,6 +36,16 @@ export declare function getRunningChromeInfo(profileName: string): {
36
36
  pid: number;
37
37
  port: number;
38
38
  } | null;
39
+ /**
40
+ * Is a TCP port currently bound? `lsof` on POSIX, `netstat -ano` on Windows
41
+ * (lsof doesn't exist there). Returns false on any tooling error so port
42
+ * allocation degrades to "assume free" rather than throwing.
43
+ *
44
+ * Exported as the canonical cross-platform probe — `findFreeProfilePort`
45
+ * (profiles.ts) must use this rather than shelling out to lsof directly,
46
+ * or every port scans as free on Windows.
47
+ */
48
+ export declare function isPortInUse(port: number): boolean;
39
49
  export declare function allocatePort(): number;
40
50
  export interface PortOccupant {
41
51
  pid: number;