@phnx-labs/agents-cli 1.22.14 → 1.22.16

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 (76) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +7 -5
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/cloud.js +4 -6
  5. package/dist/commands/exec.js +96 -11
  6. package/dist/commands/humans.d.ts +10 -0
  7. package/dist/commands/humans.js +91 -0
  8. package/dist/commands/packages.js +90 -22
  9. package/dist/commands/projects.d.ts +2 -5
  10. package/dist/commands/projects.js +92 -65
  11. package/dist/commands/resume.d.ts +17 -0
  12. package/dist/commands/resume.js +80 -0
  13. package/dist/commands/routines.js +53 -71
  14. package/dist/commands/sessions.d.ts +13 -4
  15. package/dist/commands/sessions.js +37 -19
  16. package/dist/commands/setup-watchdog.d.ts +3 -0
  17. package/dist/commands/setup-watchdog.js +54 -0
  18. package/dist/commands/setup.js +8 -1
  19. package/dist/commands/ssh.d.ts +11 -0
  20. package/dist/commands/ssh.js +18 -4
  21. package/dist/commands/watchdog.d.ts +3 -4
  22. package/dist/commands/watchdog.js +40 -28
  23. package/dist/index.js +3 -2
  24. package/dist/lib/auto-dispatch.d.ts +8 -10
  25. package/dist/lib/auto-dispatch.js +20 -42
  26. package/dist/lib/channels/send.d.ts +4 -1
  27. package/dist/lib/channels/send.js +15 -7
  28. package/dist/lib/devices/doctor-findings.d.ts +14 -0
  29. package/dist/lib/devices/doctor-findings.js +92 -51
  30. package/dist/lib/exec.d.ts +6 -2
  31. package/dist/lib/exec.js +22 -4
  32. package/dist/lib/hooks.d.ts +4 -2
  33. package/dist/lib/hooks.js +173 -23
  34. package/dist/lib/humans.d.ts +25 -0
  35. package/dist/lib/humans.js +65 -0
  36. package/dist/lib/memory.js +2 -1
  37. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  38. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  39. package/dist/lib/migrate.d.ts +1 -1
  40. package/dist/lib/migrate.js +116 -7
  41. package/dist/lib/notify.js +6 -2
  42. package/dist/lib/permissions.js +12 -10
  43. package/dist/lib/project-import.d.ts +7 -46
  44. package/dist/lib/project-import.js +9 -108
  45. package/dist/lib/projects.d.ts +20 -3
  46. package/dist/lib/projects.js +36 -18
  47. package/dist/lib/registry.d.ts +1 -1
  48. package/dist/lib/registry.js +11 -0
  49. package/dist/lib/routine-activation.d.ts +14 -0
  50. package/dist/lib/routine-activation.js +78 -0
  51. package/dist/lib/routines-project.d.ts +1 -1
  52. package/dist/lib/routines-project.js +3 -9
  53. package/dist/lib/routines.d.ts +3 -1
  54. package/dist/lib/routines.js +32 -19
  55. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  56. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  57. package/dist/lib/secrets/bundles.d.ts +11 -0
  58. package/dist/lib/secrets/bundles.js +122 -5
  59. package/dist/lib/session/actor-sidecar.d.ts +5 -2
  60. package/dist/lib/session/actor-sidecar.js +4 -2
  61. package/dist/lib/session/db.d.ts +2 -1
  62. package/dist/lib/session/db.js +19 -3
  63. package/dist/lib/session/types.d.ts +4 -0
  64. package/dist/lib/staleness/writers/sources.d.ts +6 -1
  65. package/dist/lib/staleness/writers/sources.js +93 -7
  66. package/dist/lib/startup/command-registry.d.ts +2 -0
  67. package/dist/lib/startup/command-registry.js +5 -0
  68. package/dist/lib/state.d.ts +2 -0
  69. package/dist/lib/state.js +23 -10
  70. package/dist/lib/types.d.ts +64 -1
  71. package/dist/lib/versions.js +48 -14
  72. package/dist/lib/watchdog/rotate.d.ts +1 -1
  73. package/dist/lib/watchdog/rotate.js +1 -1
  74. package/package.json +1 -1
  75. package/dist/lib/watchdog/routine.d.ts +0 -44
  76. package/dist/lib/watchdog/routine.js +0 -69
package/dist/lib/state.js CHANGED
@@ -60,6 +60,10 @@ const LEGACY_SYSTEM_AGENTS_DIR = path.join(HOME, '.agents-system');
60
60
  const META_FILE = path.join(USER_AGENTS_DIR, 'agents.yaml');
61
61
  /** Legacy location — used only for one-shot migration in readMeta(). */
62
62
  const SYSTEM_META_FILE = path.join(SYSTEM_AGENTS_DIR, 'agents.yaml');
63
+ /** Canonical path for the humans.yaml owner-identity/channel config. */
64
+ const HUMANS_FILE = path.join(USER_AGENTS_DIR, 'humans.yaml');
65
+ /** Return the absolute path to the humans.yaml file. */
66
+ export function getHumansFilePath() { return process.env.AGENTS_HUMANS_FILE ?? HUMANS_FILE; }
63
67
  // ─── System resource dirs ─────────────────────────────────────────────────────
64
68
  const SYSTEM_COMMANDS_DIR = path.join(SYSTEM_AGENTS_DIR, 'commands');
65
69
  const SYSTEM_HOOKS_DIR = path.join(SYSTEM_AGENTS_DIR, 'hooks');
@@ -300,7 +304,7 @@ export function getResolvedRulesDir() { return SYSTEM_RULES_DIR; }
300
304
  /** Path to MCP server YAML configs — system repo. */
301
305
  export function getMcpDir() { return SYSTEM_MCP_DIR; }
302
306
  /** Path to permission group YAML files — system repo. */
303
- export function getPermissionsDir() { return SYSTEM_PERMISSIONS_DIR; }
307
+ export function getPermissionsDir() { return process.env.AGENTS_SYSTEM_PERMISSIONS_DIR ?? SYSTEM_PERMISSIONS_DIR; }
304
308
  /** Path to subagent definition directories — system repo. */
305
309
  export function getSubagentsDir() { return SYSTEM_SUBAGENTS_DIR; }
306
310
  /** Path to ~/.agents/.system/hooks/promptcuts.yaml (system defaults). */
@@ -359,7 +363,7 @@ export function getUserHooksDir() { return USER_HOOKS_DIR; }
359
363
  export function getUserSkillsDir() { return USER_SKILLS_DIR; }
360
364
  export function getUserRulesDir() { return USER_RULES_DIR; }
361
365
  export function getUserMcpDir() { return USER_MCP_DIR; }
362
- export function getUserPermissionsDir() { return USER_PERMISSIONS_DIR; }
366
+ export function getUserPermissionsDir() { return process.env.AGENTS_USER_PERMISSIONS_DIR ?? USER_PERMISSIONS_DIR; }
363
367
  export function getUserSubagentsDir() { return USER_SUBAGENTS_DIR; }
364
368
  export function getSystemWorkflowsDir() { return SYSTEM_WORKFLOWS_DIR; }
365
369
  export function getUserWorkflowsDir() { return USER_WORKFLOWS_DIR; }
@@ -674,8 +678,6 @@ export function ensureAgentsDir() {
674
678
  fs.mkdirSync(PACKAGES_DIR, opts);
675
679
  if (!fs.existsSync(ROUTINES_DIR))
676
680
  fs.mkdirSync(ROUTINES_DIR, opts);
677
- if (!fs.existsSync(WEBHOOKS_DIR))
678
- fs.mkdirSync(WEBHOOKS_DIR, opts);
679
681
  if (!fs.existsSync(RUNS_DIR))
680
682
  fs.mkdirSync(RUNS_DIR, opts);
681
683
  if (!fs.existsSync(VERSIONS_DIR))
@@ -694,8 +696,6 @@ export function ensureAgentsDir() {
694
696
  fs.mkdirSync(SYSTEM_PERMISSIONS_DIR, opts);
695
697
  if (!fs.existsSync(SYSTEM_SUBAGENTS_DIR))
696
698
  fs.mkdirSync(SYSTEM_SUBAGENTS_DIR, opts);
697
- if (!fs.existsSync(SYSTEM_WEBHOOKS_DIR))
698
- fs.mkdirSync(SYSTEM_WEBHOOKS_DIR, opts);
699
699
  try {
700
700
  fs.chmodSync(SYSTEM_AGENTS_DIR, 0o700);
701
701
  }
@@ -812,6 +812,7 @@ const META_KEY_SCOPE = {
812
812
  versions: 'device',
813
813
  defaultBrowserProfile: 'device',
814
814
  deviceConfig: 'device',
815
+ deviceRoutines: 'device',
815
816
  // Central — synced via agents.yaml.
816
817
  run: 'central',
817
818
  model: 'central',
@@ -909,7 +910,7 @@ function serializeCentral(central) {
909
910
  return isEmpty ? META_HEADER : doc.toString({ collectionStyle: 'block' });
910
911
  }
911
912
  function writeMetaUnlocked(meta) {
912
- const { agents, isolatedAgents, versions, defaultBrowserProfile, deviceConfig, ...central } = meta;
913
+ const { agents, isolatedAgents, versions, defaultBrowserProfile, deviceConfig, deviceRoutines, ...central } = meta;
913
914
  // Write the machine-local files FIRST, then strip central — so a crash mid-write
914
915
  // never removes pins/versions from central before they're persisted elsewhere.
915
916
  const devicePath = getDeviceMetaPath();
@@ -923,7 +924,8 @@ function writeMetaUnlocked(meta) {
923
924
  // Device-scope config (`maxAgents`, `schedulerEnabled`, …) is per-machine, so it
924
925
  // rides the device doc under `config:` — never the central doc that syncs.
925
926
  const hasDeviceConfig = !!deviceConfig && Object.keys(deviceConfig).length > 0;
926
- if (hasAgents || hasIsolatedAgents || hasDefaultBrowser || hasDeviceConfig) {
927
+ const hasDeviceRoutines = Array.isArray(deviceRoutines);
928
+ if (hasAgents || hasIsolatedAgents || hasDefaultBrowser || hasDeviceConfig || hasDeviceRoutines) {
927
929
  // Device-local doc carries `agents:` pins, `defaultBrowserProfile:`, and the
928
930
  // device-scope `config:` — all per-machine and must never land in central
929
931
  // agents.yaml (which syncs).
@@ -936,6 +938,8 @@ function writeMetaUnlocked(meta) {
936
938
  deviceDoc.defaultBrowserProfile = defaultBrowserProfile;
937
939
  if (hasDeviceConfig)
938
940
  deviceDoc.config = deviceConfig;
941
+ if (hasDeviceRoutines)
942
+ deviceDoc.routines = deviceRoutines;
939
943
  fs.mkdirSync(path.dirname(devicePath), { recursive: true });
940
944
  writeIfChanged(devicePath, META_HEADER + yaml.stringify(deviceDoc));
941
945
  }
@@ -971,8 +975,12 @@ function writeMetaUnlocked(meta) {
971
975
  function overlayMachineLocal(meta) {
972
976
  const devicePath = getDeviceMetaPath();
973
977
  if (fs.existsSync(devicePath)) {
978
+ let dm = null;
974
979
  try {
975
- const dm = yaml.parse(fs.readFileSync(devicePath, 'utf-8'));
980
+ dm = yaml.parse(fs.readFileSync(devicePath, 'utf-8'));
981
+ }
982
+ catch { /* preserve the existing tolerance for malformed legacy device YAML */ }
983
+ if (dm) {
976
984
  if (dm?.agents)
977
985
  meta.agents = { ...meta.agents, ...dm.agents };
978
986
  if (dm?.isolatedAgents)
@@ -981,8 +989,13 @@ function overlayMachineLocal(meta) {
981
989
  meta.defaultBrowserProfile = dm.defaultBrowserProfile;
982
990
  if (dm?.config)
983
991
  meta.deviceConfig = dm.config;
992
+ if (dm && Object.prototype.hasOwnProperty.call(dm, 'routines')) {
993
+ if (!Array.isArray(dm.routines) || dm.routines.some((name) => typeof name !== 'string')) {
994
+ throw new Error(`Device config corrupted at ${devicePath}: routines must be a string list.`);
995
+ }
996
+ meta.deviceRoutines = dm.routines;
997
+ }
984
998
  }
985
- catch { /* ignore malformed device file */ }
986
999
  }
987
1000
  const vrPath = getVersionResourcesPath();
988
1001
  if (fs.existsSync(vrPath)) {
@@ -511,10 +511,16 @@ export interface RegistrySearchResult {
511
511
  }
512
512
  /** A package that has been resolved from a registry and is ready to install. */
513
513
  export interface ResolvedPackage {
514
- type: 'mcp' | 'skill' | 'git';
514
+ /** `plugin` is Phase 5 packaging: `agents install plugin:<spec>` → plugins install. */
515
+ type: 'mcp' | 'skill' | 'git' | 'plugin';
515
516
  source: string;
516
517
  mcpEntry?: McpServerEntry;
517
518
  skillEntry?: SkillEntry;
519
+ /**
520
+ * Plugin install spec (`name@url`, path, or bare source) when `type === 'plugin'`.
521
+ * Same grammar as `agents plugins install <spec>`.
522
+ */
523
+ pluginSpec?: string;
518
524
  }
519
525
  /** Categories of resources that can be synced into an agent version home. */
520
526
  export type ResourceType = 'commands' | 'skills' | 'hooks' | 'memory' | 'mcp' | 'permissions' | 'subagents' | 'plugins' | 'workflows';
@@ -937,6 +943,13 @@ export interface Meta {
937
943
  * device-config registry (`maxAgents`, `schedulerEnabled`, `notes`). Per-machine by design — unset = today's behavior.
938
944
  */
939
945
  deviceConfig?: Record<string, unknown>;
946
+ /**
947
+ * Routine names enabled on this machine. In memory this stays distinct from
948
+ * portable user config; state.ts writes it as top-level `routines:` in
949
+ * `~/.agents/devices/<machine>/agents.yaml`. Presence in the list is the whole
950
+ * activation state: absent means disabled on this device.
951
+ */
952
+ deviceRoutines?: string[];
940
953
  /**
941
954
  * Agent-host registry keyed by host name (`agents hosts`). Portable user
942
955
  * config synced with `agents repo push/pull`. For `ssh-config` hosts this is
@@ -989,6 +1002,56 @@ export interface Meta {
989
1002
  projects?: string[];
990
1003
  };
991
1004
  }
1005
+ /** A single delivery channel entry in humans.yaml. */
1006
+ export interface HumanChannel {
1007
+ /** Canonical channel identifier (e.g. "imessage", "call"). */
1008
+ id: string;
1009
+ /** Provider transport (e.g. "rush", "twilio"). */
1010
+ transport: string;
1011
+ /** If true the channel is watched for incoming messages. */
1012
+ watch?: boolean;
1013
+ /** Shell command to invoke (for call channels). */
1014
+ cmd?: string;
1015
+ /** Credentials bundle name (`agents secrets`). */
1016
+ creds?: string;
1017
+ /** Whether the channel is intrusive (e.g. voice call). */
1018
+ intrusive?: boolean;
1019
+ }
1020
+ /** Severity-to-channel-list escalation policy in humans.yaml. */
1021
+ export interface HumanPolicy {
1022
+ low?: string[];
1023
+ normal?: string[];
1024
+ critical?: string[];
1025
+ }
1026
+ /** Owner identity and contact configuration in humans.yaml. */
1027
+ export interface HumanOwner {
1028
+ /** Display name. */
1029
+ name?: string;
1030
+ /** IANA timezone string (e.g. "America/Los_Angeles"). */
1031
+ timezone?: string;
1032
+ /** Quiet hours as "HH:MM-HH:MM" in local time. */
1033
+ quiet_hours?: string;
1034
+ /** Default notification severity when unspecified. */
1035
+ default_severity?: 'low' | 'normal' | 'critical';
1036
+ /** Short-form channel config for `agents send --to owner` (channel + recipient). */
1037
+ notify?: {
1038
+ channel: string;
1039
+ to: string;
1040
+ };
1041
+ /** Full delivery-channel definitions. */
1042
+ channels?: HumanChannel[];
1043
+ /** Severity-to-channel escalation policy. */
1044
+ policy?: HumanPolicy;
1045
+ }
1046
+ /**
1047
+ * Versioned humans.yaml config — owner identity, channels, and notification
1048
+ * policy. Written to ~/.agents/humans.yaml by `migrateHumans()`.
1049
+ */
1050
+ export interface HumansConfig {
1051
+ /** Schema version. Always 1. */
1052
+ version: 1;
1053
+ owner?: HumanOwner;
1054
+ }
992
1055
  /** Persisted agent-host entry in agents.yaml (overlay or inline). */
993
1056
  export interface HostEntry {
994
1057
  /** `ssh-config`: reach via the bare name (ssh resolves). `inline`: use address/user below. */
@@ -185,15 +185,24 @@ export function getAvailableResources(cwd = process.cwd()) {
185
185
  }
186
186
  }
187
187
  result.skills = filterNamesForActiveResourceProfile('skills', Array.from(skillNames), sourceMapFromResources('skills', cwd));
188
- // Hooks. A hook is either a directory bundle or an actual script file: known
189
- // script extension, OR executable bit on a file with a non-data extension.
190
- // Auxiliary content like `README.md` (docs) or `promptcuts.yaml` (data read
191
- // directly by the expand-promptcuts script) lives in hooks/ but is not a
192
- // hook. Older sync runs chmod 0o755'd everything they copied, so an exec bit
193
- // alone can no longer be trusted as the signal.
188
+ // Hooks:
189
+ // - top-level script files
190
+ // - one-level *group* dirs that contain top-level scripts (session-starts/*.sh)
191
+ // → each script is its own hook resource (install name = basename)
192
+ // - other directories (e.g. tests/ with fixtures only) → directory bundles,
193
+ // copied wholesale as one resource (pre-existing layout)
194
+ // Auxiliary content like README.md / promptcuts.yaml is not a hook. Older sync
195
+ // runs chmod 0o755'd everything, so an exec bit alone is not the signal.
194
196
  const NON_SCRIPT_EXTS = new Set(['.md', '.markdown', '.rst', '.txt', '.yaml', '.yml', '.json', '.toml', '.ini', '.conf']);
195
197
  const SCRIPT_EXTS = new Set(['.sh', '.bash', '.zsh', '.py', '.js', '.ts', '.mjs', '.cjs', '.rb', '.pl', '.ps1']);
198
+ const HOOK_GROUP_SKIP = new Set(['node_modules', '.git', '.cache']);
196
199
  const hookNames = new Set();
200
+ const isHookScriptName = (fileName, mode) => {
201
+ const ext = path.extname(fileName).toLowerCase();
202
+ if (SCRIPT_EXTS.has(ext))
203
+ return true;
204
+ return (mode & 0o111) !== 0 && !NON_SCRIPT_EXTS.has(ext);
205
+ };
197
206
  for (const { base } of resourceBases) {
198
207
  const hooksDir = path.join(base, 'hooks');
199
208
  if (!fs.existsSync(hooksDir))
@@ -202,22 +211,47 @@ export function getAvailableResources(cwd = process.cwd()) {
202
211
  if (name.startsWith('.'))
203
212
  continue;
204
213
  try {
205
- const stat = fs.lstatSync(path.join(hooksDir, name));
214
+ const full = path.join(hooksDir, name);
215
+ const stat = fs.lstatSync(full);
206
216
  if (stat.isSymbolicLink())
207
217
  continue;
208
- if (stat.isDirectory()) {
209
- hookNames.add(name);
218
+ if (stat.isFile()) {
219
+ if (isHookScriptName(name, stat.mode))
220
+ hookNames.add(name);
210
221
  continue;
211
222
  }
212
- if (!stat.isFile())
223
+ if (!stat.isDirectory() || HOOK_GROUP_SKIP.has(name))
213
224
  continue;
214
- const ext = path.extname(name).toLowerCase();
215
- if (SCRIPT_EXTS.has(ext)) {
216
- hookNames.add(name);
225
+ // Group vs bundle: if the dir has any top-level script files, expand
226
+ // them; otherwise treat the whole dir as one hook resource.
227
+ let nestedNames;
228
+ try {
229
+ nestedNames = fs.readdirSync(full);
230
+ }
231
+ catch {
217
232
  continue;
218
233
  }
219
- if ((stat.mode & 0o111) !== 0 && !NON_SCRIPT_EXTS.has(ext))
234
+ const scripts = [];
235
+ for (const nested of nestedNames) {
236
+ if (nested.startsWith('.'))
237
+ continue;
238
+ try {
239
+ const nfull = path.join(full, nested);
240
+ const nstat = fs.lstatSync(nfull);
241
+ if (nstat.isSymbolicLink() || !nstat.isFile())
242
+ continue;
243
+ if (isHookScriptName(nested, nstat.mode))
244
+ scripts.push(nested);
245
+ }
246
+ catch { /* ignore */ }
247
+ }
248
+ if (scripts.length > 0) {
249
+ for (const s of scripts)
250
+ hookNames.add(s);
251
+ }
252
+ else {
220
253
  hookNames.add(name);
254
+ }
221
255
  }
222
256
  catch { /* ignore unreadable */ }
223
257
  }
@@ -166,7 +166,7 @@ export declare function isWatchdogRotateEnabled(): boolean;
166
166
  * Persist `watchdog.rotate: on|off`. Called by the `agents watchdog rotate
167
167
  * on|off` subcommand (commands/watchdog.ts) — the rotate-only switch the
168
168
  * Factory migration uses so a user who opted out of autoRotate keeps nudging
169
- * (rather than `agents watchdog disable`, which kills the whole watchdog).
169
+ * (rather than `agents watchdog off`, which disables the whole watchdog here).
170
170
  */
171
171
  export declare function setWatchdogRotateEnabled(on: boolean): void;
172
172
  export interface RotateGateResult {
@@ -291,7 +291,7 @@ export function isWatchdogRotateEnabled() {
291
291
  * Persist `watchdog.rotate: on|off`. Called by the `agents watchdog rotate
292
292
  * on|off` subcommand (commands/watchdog.ts) — the rotate-only switch the
293
293
  * Factory migration uses so a user who opted out of autoRotate keeps nudging
294
- * (rather than `agents watchdog disable`, which kills the whole watchdog).
294
+ * (rather than `agents watchdog off`, which disables the whole watchdog here).
295
295
  */
296
296
  export function setWatchdogRotateEnabled(on) {
297
297
  const meta = readMeta();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.14",
3
+ "version": "1.22.16",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,44 +0,0 @@
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;
@@ -1,69 +0,0 @@
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
- }