@phnx-labs/agents-cli 1.20.76 → 1.20.78

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 (119) hide show
  1. package/CHANGELOG.md +264 -0
  2. package/README.md +2 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/doctor.d.ts +9 -0
  5. package/dist/commands/doctor.js +143 -9
  6. package/dist/commands/exec.js +87 -19
  7. package/dist/commands/repo.js +8 -0
  8. package/dist/commands/routines.js +275 -15
  9. package/dist/commands/secrets.js +65 -17
  10. package/dist/commands/sessions-browser.d.ts +45 -3
  11. package/dist/commands/sessions-browser.js +128 -12
  12. package/dist/commands/sessions-export.d.ts +3 -0
  13. package/dist/commands/sessions-export.js +13 -10
  14. package/dist/commands/sessions-picker.d.ts +28 -1
  15. package/dist/commands/sessions-picker.js +222 -15
  16. package/dist/commands/sessions.d.ts +117 -4
  17. package/dist/commands/sessions.js +350 -62
  18. package/dist/commands/ssh.d.ts +13 -0
  19. package/dist/commands/ssh.js +48 -3
  20. package/dist/index.js +5 -6
  21. package/dist/lib/activity.d.ts +5 -4
  22. package/dist/lib/activity.js +450 -36
  23. package/dist/lib/agents.d.ts +0 -21
  24. package/dist/lib/agents.js +0 -37
  25. package/dist/lib/auto-pull.d.ts +16 -8
  26. package/dist/lib/auto-pull.js +23 -28
  27. package/dist/lib/cloud/session-index.js +2 -2
  28. package/dist/lib/daemon.d.ts +9 -32
  29. package/dist/lib/daemon.js +70 -84
  30. package/dist/lib/devices/fleet-divergence.d.ts +101 -0
  31. package/dist/lib/devices/fleet-divergence.js +188 -0
  32. package/dist/lib/devices/fleet-inventory.d.ts +19 -0
  33. package/dist/lib/devices/fleet-inventory.js +57 -0
  34. package/dist/lib/devices/fleet.d.ts +16 -1
  35. package/dist/lib/devices/fleet.js +10 -2
  36. package/dist/lib/devices/health-report.d.ts +10 -2
  37. package/dist/lib/devices/health-report.js +32 -1
  38. package/dist/lib/events.d.ts +1 -1
  39. package/dist/lib/exec.d.ts +16 -0
  40. package/dist/lib/exec.js +27 -4
  41. package/dist/lib/feed.d.ts +7 -1
  42. package/dist/lib/feed.js +96 -6
  43. package/dist/lib/git.d.ts +13 -0
  44. package/dist/lib/git.js +102 -4
  45. package/dist/lib/heal.d.ts +7 -4
  46. package/dist/lib/heal.js +10 -22
  47. package/dist/lib/hosts/dispatch.d.ts +9 -0
  48. package/dist/lib/hosts/dispatch.js +24 -4
  49. package/dist/lib/hosts/passthrough.js +7 -2
  50. package/dist/lib/hosts/remote-cmd.d.ts +11 -0
  51. package/dist/lib/hosts/remote-cmd.js +33 -8
  52. package/dist/lib/hosts/remote-session-id.d.ts +45 -0
  53. package/dist/lib/hosts/remote-session-id.js +84 -0
  54. package/dist/lib/hosts/run-target.d.ts +4 -0
  55. package/dist/lib/hosts/run-target.js +5 -1
  56. package/dist/lib/hosts/session-index.js +3 -3
  57. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +2 -0
  58. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  59. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  60. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
  61. package/dist/lib/menubar/install-menubar.d.ts +9 -0
  62. package/dist/lib/menubar/install-menubar.js +14 -0
  63. package/dist/lib/menubar/notify-desktop.d.ts +44 -0
  64. package/dist/lib/menubar/notify-desktop.js +78 -0
  65. package/dist/lib/overdue.d.ts +4 -5
  66. package/dist/lib/overdue.js +8 -41
  67. package/dist/lib/routine-notify.d.ts +76 -0
  68. package/dist/lib/routine-notify.js +190 -0
  69. package/dist/lib/routines-placement.d.ts +38 -0
  70. package/dist/lib/routines-placement.js +79 -0
  71. package/dist/lib/routines-project.d.ts +97 -0
  72. package/dist/lib/routines-project.js +349 -0
  73. package/dist/lib/routines.d.ts +68 -0
  74. package/dist/lib/routines.js +74 -7
  75. package/dist/lib/runner.d.ts +12 -2
  76. package/dist/lib/runner.js +156 -28
  77. package/dist/lib/sandbox.js +0 -6
  78. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  79. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
  80. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  81. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  82. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
  83. package/dist/lib/secrets/agent.d.ts +11 -4
  84. package/dist/lib/secrets/agent.js +46 -24
  85. package/dist/lib/secrets/bundles.d.ts +21 -7
  86. package/dist/lib/secrets/bundles.js +82 -38
  87. package/dist/lib/secrets/index.d.ts +11 -2
  88. package/dist/lib/secrets/index.js +21 -9
  89. package/dist/lib/secrets/scope.d.ts +26 -0
  90. package/dist/lib/secrets/scope.js +29 -0
  91. package/dist/lib/secrets/session-store.d.ts +16 -2
  92. package/dist/lib/secrets/session-store.js +118 -15
  93. package/dist/lib/secrets/unlock-hints.d.ts +27 -0
  94. package/dist/lib/secrets/unlock-hints.js +36 -0
  95. package/dist/lib/session/active.d.ts +17 -0
  96. package/dist/lib/session/active.js +105 -7
  97. package/dist/lib/session/db.d.ts +43 -0
  98. package/dist/lib/session/db.js +191 -6
  99. package/dist/lib/session/digest.js +126 -21
  100. package/dist/lib/session/discover.d.ts +28 -3
  101. package/dist/lib/session/discover.js +110 -14
  102. package/dist/lib/session/origin-machine.d.ts +18 -0
  103. package/dist/lib/session/origin-machine.js +34 -0
  104. package/dist/lib/session/parse.js +11 -2
  105. package/dist/lib/session/prompt.d.ts +7 -0
  106. package/dist/lib/session/prompt.js +37 -0
  107. package/dist/lib/session/provenance.d.ts +7 -0
  108. package/dist/lib/session/render.js +18 -16
  109. package/dist/lib/session/state.d.ts +5 -0
  110. package/dist/lib/session/state.js +94 -12
  111. package/dist/lib/session/sync/config.js +2 -2
  112. package/dist/lib/session/types.d.ts +25 -1
  113. package/dist/lib/session/types.js +8 -0
  114. package/dist/lib/smart-launch.d.ts +86 -0
  115. package/dist/lib/smart-launch.js +172 -0
  116. package/dist/lib/state.d.ts +7 -3
  117. package/dist/lib/state.js +20 -5
  118. package/dist/lib/types.d.ts +10 -0
  119. package/package.json +1 -1
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Routine lifecycle desktop notifications (RUSH-2030).
3
+ *
4
+ * The daemon fires a branded notification when a scheduled routine starts and
5
+ * when it finishes (success or failure), routed through the MenubarHelper
6
+ * companion (notify-desktop.ts) so it carries the agents-cli mark.
7
+ *
8
+ * Anti-spam threshold (Acceptance Criteria #4 — "define a sensible threshold so
9
+ * users are not spammed"):
10
+ * - Agent / workflow routines: notify on BOTH start and finish. These are the
11
+ * runs whose output a user actually wants surfaced.
12
+ * - Command routines (deterministic housekeeping — version checks, `git pull`,
13
+ * notify shims that can fire every minute): notify only on FAILURE. A green
14
+ * housekeeping run is noise; a broken one is worth a ping. No start ping.
15
+ * - "Notable output" is folded into the single finish notification rather than
16
+ * sent as a third message: on failure the error reason, on success the first
17
+ * line of the run's report (the user-facing result) when the routine produced
18
+ * one. One start + one finish per run — never a stream.
19
+ *
20
+ * The pure builders (`routineStartNotification` / `routineFinishNotification`)
21
+ * return `null` when the threshold says "don't notify", and are unit-tested; the
22
+ * `notifyRoutine*` wrappers do the filesystem read + dispatch for the daemon.
23
+ */
24
+ import * as fs from 'fs';
25
+ import * as path from 'path';
26
+ import { getRunDir } from './routines.js';
27
+ import { notifyDesktop } from './menubar/notify-desktop.js';
28
+ /** Which flavor of routine a config/meta describes — drives the notify threshold. */
29
+ export function routineKind(r) {
30
+ if (r.command)
31
+ return 'command';
32
+ if (r.workflow)
33
+ return 'workflow';
34
+ return 'agent';
35
+ }
36
+ /** Human label for the routine body ("agent claude", "workflow deploy", "command"). */
37
+ function routineLabel(r) {
38
+ if (r.command)
39
+ return 'command';
40
+ if (r.workflow)
41
+ return `workflow ${r.workflow}`;
42
+ return `agent ${r.agent ?? 'unknown'}`;
43
+ }
44
+ /** "1m 20s" / "45s" / "2h 3m" from a millisecond duration, or null when unknown. */
45
+ export function formatDuration(ms) {
46
+ if (ms === undefined || !Number.isFinite(ms) || ms < 0)
47
+ return null;
48
+ const totalSec = Math.round(ms / 1000);
49
+ if (totalSec < 60)
50
+ return `${totalSec}s`;
51
+ const min = Math.floor(totalSec / 60);
52
+ const sec = totalSec % 60;
53
+ if (min < 60)
54
+ return sec ? `${min}m ${sec}s` : `${min}m`;
55
+ const hr = Math.floor(min / 60);
56
+ const remMin = min % 60;
57
+ return remMin ? `${hr}h ${remMin}m` : `${hr}h`;
58
+ }
59
+ /**
60
+ * First non-empty line of a run report, trimmed to a notification-sized snippet.
61
+ * This is the "notable output" surfaced on a successful finish — the routine's
62
+ * own user-facing result. Returns null for an empty/whitespace report so the
63
+ * caller falls back to a plain "Completed" body.
64
+ */
65
+ export function notableSnippet(report, maxLen = 140) {
66
+ if (!report)
67
+ return null;
68
+ const firstLine = report
69
+ .split('\n')
70
+ .map((l) => l.trim())
71
+ .find((l) => l.length > 0);
72
+ if (!firstLine)
73
+ return null;
74
+ return firstLine.length > maxLen ? `${firstLine.slice(0, maxLen - 1).trimEnd()}…` : firstLine;
75
+ }
76
+ /** Encode a click action that opens a file (report/log) in the default app. */
77
+ function openAction(filePath) {
78
+ return filePath ? `open:${filePath}` : undefined;
79
+ }
80
+ /**
81
+ * Notification for a routine START, or null when the threshold suppresses it
82
+ * (command-mode housekeeping). Clicking opens the runs folder
83
+ * (~/.agents/.history/runs).
84
+ */
85
+ export function routineStartNotification(config) {
86
+ if (routineKind(config) === 'command')
87
+ return null;
88
+ return {
89
+ title: 'Routine started',
90
+ subtitle: config.name,
91
+ body: `Running ${routineLabel(config)}`,
92
+ action: 'routines:list',
93
+ };
94
+ }
95
+ /**
96
+ * Notification for a routine that failed to even START — `executeJobDetached`
97
+ * threw before the child was spawned, so no run record and no finish will ever
98
+ * exist. The daemon fires the START notification unconditionally, so this closes
99
+ * the "exactly one start + one finish" invariant: the orphaned start gets its
100
+ * matching failure banner (RUSH-2030). Unlike a green finish this is never
101
+ * suppressed — a broken start is worth a ping for every routine kind, including
102
+ * command housekeeping. Clicking opens the runs folder (~/.agents/.history/runs)
103
+ * since there is no run report to open.
104
+ */
105
+ export function routineStartFailedNotification(config, error) {
106
+ return {
107
+ title: 'Routine failed',
108
+ subtitle: config.name,
109
+ body: `Failed to start: ${error}`,
110
+ action: 'routines:list',
111
+ };
112
+ }
113
+ /**
114
+ * Notification for a routine FINISH, or null when the threshold suppresses it
115
+ * (a successful command-mode housekeeping run). Success carries the report's
116
+ * first line when present (the notable output); failure carries the reason.
117
+ * Clicking opens the run report/log when one is available, else the runs folder
118
+ * (~/.agents/.history/runs).
119
+ */
120
+ export function routineFinishNotification(meta, opts = {}) {
121
+ const kind = routineKind(meta);
122
+ const ok = meta.status === 'completed';
123
+ if (kind === 'command' && ok)
124
+ return null; // green housekeeping is noise
125
+ const action = openAction(opts.artifactPath) ?? 'routines:list';
126
+ if (ok) {
127
+ const snippet = notableSnippet(opts.report);
128
+ const dur = formatDuration(meta.duration);
129
+ return {
130
+ title: 'Routine finished',
131
+ subtitle: meta.jobName,
132
+ body: snippet ?? (dur ? `Completed in ${dur}` : 'Completed'),
133
+ action,
134
+ };
135
+ }
136
+ // failed | timeout
137
+ const reason = meta.status === 'timeout'
138
+ ? 'Timed out'
139
+ : meta.errorMessage
140
+ ? meta.errorMessage
141
+ : `Exited with code ${meta.exitCode ?? '?'}`;
142
+ return {
143
+ title: 'Routine failed',
144
+ subtitle: meta.jobName,
145
+ body: reason,
146
+ action,
147
+ };
148
+ }
149
+ /** Read a finished run's report text + the best artifact to open on click. */
150
+ function loadRunArtifacts(meta) {
151
+ try {
152
+ const runDir = getRunDir(meta.jobName, meta.runId);
153
+ const reportPath = path.join(runDir, 'report.md');
154
+ const stdoutPath = path.join(runDir, 'stdout.log');
155
+ let report = null;
156
+ let artifactPath = null;
157
+ if (fs.existsSync(reportPath)) {
158
+ report = fs.readFileSync(reportPath, 'utf-8');
159
+ artifactPath = reportPath;
160
+ }
161
+ else if (fs.existsSync(stdoutPath)) {
162
+ artifactPath = stdoutPath;
163
+ }
164
+ return { report, artifactPath };
165
+ }
166
+ catch {
167
+ return { report: null, artifactPath: null };
168
+ }
169
+ }
170
+ /** Daemon glue: fire the START notification for a triggered routine. Best-effort. */
171
+ export function notifyRoutineStart(config) {
172
+ const n = routineStartNotification(config);
173
+ if (n)
174
+ notifyDesktop(n);
175
+ }
176
+ /**
177
+ * Daemon glue: fire the "failed to start" notification when a routine trigger
178
+ * threw before spawning a child. Pairs with the unconditional START ping so a
179
+ * pre-spawn failure never leaves an orphaned "Routine started". Best-effort.
180
+ */
181
+ export function notifyRoutineStartFailed(config, error) {
182
+ notifyDesktop(routineStartFailedNotification(config, error));
183
+ }
184
+ /** Daemon glue: fire the FINISH notification for a completed run. Best-effort. */
185
+ export function notifyRoutineFinish(meta) {
186
+ const { report, artifactPath } = loadRunArtifacts(meta);
187
+ const n = routineFinishNotification(meta, { report, artifactPath });
188
+ if (n)
189
+ notifyDesktop(n);
190
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Runtime placement resolution for routines: local / host / fleet / cloud.
3
+ *
4
+ * The scheduler only decides *whether* this machine may fire the job
5
+ * (`devices` + `jobRunsOnThisDevice`). This module decides *where the job
6
+ * body runs* once it has been fired, using `hostStrategy`.
7
+ *
8
+ * Fleet semantics (RUSH-2035 / RUSH-1980): pick exactly one online device per
9
+ * fire. Cross-device double-fire is prevented by requiring a `devices` firing
10
+ * pin for fleet/host/cloud strategies (applied at add/sync time).
11
+ */
12
+ import type { JobConfig } from './routines.js';
13
+ export type PlacementTarget = {
14
+ mode: 'local';
15
+ } | {
16
+ mode: 'host';
17
+ host: string;
18
+ } | {
19
+ mode: 'cloud';
20
+ };
21
+ /**
22
+ * Pick one online fleet device for `hostStrategy: fleet`.
23
+ *
24
+ * Preference order:
25
+ * 1. This machine, if online and eligible (avoids needless SSH)
26
+ * 2. First eligible online device by name (stable / deterministic)
27
+ *
28
+ * `config.devices` is the *firing* allowlist only (which daemon may fire the
29
+ * job). It is intentionally NOT used as an execution pool filter — otherwise
30
+ * the double-fire pin (`devices: [self]`) would collapse fleet placement to
31
+ * always-local. Control / offline / no-address devices are never chosen.
32
+ */
33
+ export declare function pickFleetDevice(_config?: Pick<JobConfig, 'devices'>): string | null;
34
+ /**
35
+ * Resolve where a fired job's body should execute.
36
+ * Throws a human-readable Error when placement cannot be satisfied.
37
+ */
38
+ export declare function resolvePlacementTarget(config: JobConfig): PlacementTarget;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Runtime placement resolution for routines: local / host / fleet / cloud.
3
+ *
4
+ * The scheduler only decides *whether* this machine may fire the job
5
+ * (`devices` + `jobRunsOnThisDevice`). This module decides *where the job
6
+ * body runs* once it has been fired, using `hostStrategy`.
7
+ *
8
+ * Fleet semantics (RUSH-2035 / RUSH-1980): pick exactly one online device per
9
+ * fire. Cross-device double-fire is prevented by requiring a `devices` firing
10
+ * pin for fleet/host/cloud strategies (applied at add/sync time).
11
+ */
12
+ import { resolveHostStrategy } from './routines.js';
13
+ import { machineId, normalizeHost } from './machine-id.js';
14
+ import { loadDevicesSync } from './devices/registry.js';
15
+ import { planFleetTargets } from './devices/fleet.js';
16
+ /**
17
+ * Pick one online fleet device for `hostStrategy: fleet`.
18
+ *
19
+ * Preference order:
20
+ * 1. This machine, if online and eligible (avoids needless SSH)
21
+ * 2. First eligible online device by name (stable / deterministic)
22
+ *
23
+ * `config.devices` is the *firing* allowlist only (which daemon may fire the
24
+ * job). It is intentionally NOT used as an execution pool filter — otherwise
25
+ * the double-fire pin (`devices: [self]`) would collapse fleet placement to
26
+ * always-local. Control / offline / no-address devices are never chosen.
27
+ */
28
+ export function pickFleetDevice(_config) {
29
+ let reg;
30
+ try {
31
+ reg = loadDevicesSync();
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ const planned = planFleetTargets(reg);
37
+ const candidates = planned.filter((t) => !t.skip).map((t) => t.device.name);
38
+ if (candidates.length === 0) {
39
+ // No registry / nothing online: fall back to self so a single-box fleet
40
+ // without a registry entry still runs locally.
41
+ return machineId();
42
+ }
43
+ const self = machineId();
44
+ const selfMatch = candidates.find((n) => normalizeHost(n) === self);
45
+ if (selfMatch)
46
+ return selfMatch;
47
+ return [...candidates].sort((a, b) => a.localeCompare(b))[0] ?? null;
48
+ }
49
+ /**
50
+ * Resolve where a fired job's body should execute.
51
+ * Throws a human-readable Error when placement cannot be satisfied.
52
+ */
53
+ export function resolvePlacementTarget(config) {
54
+ const strategy = resolveHostStrategy(config);
55
+ switch (strategy) {
56
+ case 'local':
57
+ return { mode: 'local' };
58
+ case 'host': {
59
+ if (!config.host || config.host.trim() === '') {
60
+ throw new Error(`Routine '${config.name}' has hostStrategy: host but no host: — set host: or --run-on`);
61
+ }
62
+ // host: pointing at this machine is local execution (no SSH loop).
63
+ if (normalizeHost(config.host) === machineId())
64
+ return { mode: 'local' };
65
+ return { mode: 'host', host: config.host };
66
+ }
67
+ case 'fleet': {
68
+ const picked = pickFleetDevice(config);
69
+ if (!picked) {
70
+ throw new Error(`Routine '${config.name}' hostStrategy: fleet — no eligible online device to place the run`);
71
+ }
72
+ if (normalizeHost(picked) === machineId())
73
+ return { mode: 'local' };
74
+ return { mode: 'host', host: picked };
75
+ }
76
+ case 'cloud':
77
+ return { mode: 'cloud' };
78
+ }
79
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Project-level routine opt-in, source tracking, and user-layer sync.
3
+ *
4
+ * Project YAML under `<project>/.agents/routines/*.yml` is inspection-only by
5
+ * default (a cloned public repo must never auto-fire agent prompts). After an
6
+ * explicit opt-in, routines are materialised into `~/.agents/routines/` with
7
+ * `source:` provenance so the daemon — which only loads user + system layers —
8
+ * can fire them. `syncProjectRoutines` refreshes the user-layer copies when
9
+ * project YAML changes (also invoked on daemon SIGHUP).
10
+ */
11
+ import { type JobSource } from './routines.js';
12
+ /** Expand `~/…` and resolve to an absolute path. */
13
+ export declare function expandProjectPath(p: string): string;
14
+ /** Store paths home-relative when under $HOME so they travel across machines. */
15
+ export declare function displayProjectPath(abs: string): string;
16
+ /** Project roots currently opted into daemon firing (absolute paths). */
17
+ export declare function listEnabledProjectRoots(): string[];
18
+ /** True when this project root is on the opt-in allowlist. */
19
+ export declare function isProjectRoutinesEnabled(projectRoot: string): boolean;
20
+ /**
21
+ * True when the project's own `agents.yaml` opts into project routines:
22
+ * `routines: { enable: true }`. This is an additional source of opt-in that
23
+ * still requires the project to be present on disk; it does not auto-enable
24
+ * every clone — the project must declare it, and `sync` / `enable-project`
25
+ * still materialises consent into the user layer.
26
+ */
27
+ export declare function projectAgentsYamlEnablesRoutines(projectRoot: string): boolean;
28
+ /** Resolve the project root (parent of `.agents/`) from a cwd, or null. */
29
+ export declare function resolveProjectRoot(cwd?: string): string | null;
30
+ /**
31
+ * Opt a project root into daemon firing. Returns true when newly added,
32
+ * false when it was already enabled.
33
+ */
34
+ export declare function enableProjectRoutines(projectRoot: string): boolean;
35
+ /**
36
+ * Remove a project root from the opt-in allowlist. Optionally deletes the
37
+ * user-layer copies that were materialised from it.
38
+ */
39
+ export declare function disableProjectRoutines(projectRoot: string, opts?: {
40
+ removeSynced?: boolean;
41
+ }): {
42
+ removed: boolean;
43
+ deletedJobs: string[];
44
+ };
45
+ /** Git provenance for a project root (best-effort; never throws). */
46
+ export declare function readProjectGitSource(projectRoot: string): Pick<JobSource, 'repo' | 'branch' | 'commit'>;
47
+ /** List project routine YAML files (name + absolute path). */
48
+ export declare function listProjectRoutineFiles(projectRoot: string): Array<{
49
+ name: string;
50
+ path: string;
51
+ }>;
52
+ export interface SyncProjectResult {
53
+ projectRoot: string;
54
+ synced: string[];
55
+ skipped: Array<{
56
+ name: string;
57
+ reason: string;
58
+ }>;
59
+ removed: string[];
60
+ errors: Array<{
61
+ name: string;
62
+ error: string;
63
+ }>;
64
+ }
65
+ /**
66
+ * Materialise one project's routines into the user layer.
67
+ * - Overwrites user copies that already carry matching `source.projectPath`
68
+ * - Never clobbers a hand-authored user routine (no source / different source)
69
+ * - Removes user copies from this project whose YAML disappeared
70
+ * - Auto-pins `devices` for host/fleet/cloud placement when unset
71
+ */
72
+ export declare function syncProjectRoutines(projectRoot: string): SyncProjectResult;
73
+ export interface SyncAllResult {
74
+ projects: SyncProjectResult[];
75
+ /** Project roots that were listed but missing on disk. */
76
+ missing: string[];
77
+ }
78
+ /**
79
+ * Sync every project root on the user allowlist (`meta.routines.projects`),
80
+ * plus any explicit `extraRoots`. Project `agents.yaml` `routines.enable`
81
+ * alone never opts a path in — enable-project is required.
82
+ */
83
+ export declare function syncAllProjectRoutines(opts?: {
84
+ extraRoots?: string[];
85
+ }): SyncAllResult;
86
+ /**
87
+ * Discover project routines at cwd for the setup UX. Returns null when no
88
+ * project `.agents/routines` dir exists.
89
+ */
90
+ export declare function discoverProjectRoutinesAt(cwd?: string): {
91
+ projectRoot: string;
92
+ files: Array<{
93
+ name: string;
94
+ path: string;
95
+ }>;
96
+ enabled: boolean;
97
+ } | null;