@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.
- package/CHANGELOG.md +264 -0
- package/README.md +2 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.js +143 -9
- package/dist/commands/exec.js +87 -19
- package/dist/commands/repo.js +8 -0
- package/dist/commands/routines.js +275 -15
- package/dist/commands/secrets.js +65 -17
- package/dist/commands/sessions-browser.d.ts +45 -3
- package/dist/commands/sessions-browser.js +128 -12
- package/dist/commands/sessions-export.d.ts +3 -0
- package/dist/commands/sessions-export.js +13 -10
- package/dist/commands/sessions-picker.d.ts +28 -1
- package/dist/commands/sessions-picker.js +222 -15
- package/dist/commands/sessions.d.ts +117 -4
- package/dist/commands/sessions.js +350 -62
- package/dist/commands/ssh.d.ts +13 -0
- package/dist/commands/ssh.js +48 -3
- package/dist/index.js +5 -6
- package/dist/lib/activity.d.ts +5 -4
- package/dist/lib/activity.js +450 -36
- package/dist/lib/agents.d.ts +0 -21
- package/dist/lib/agents.js +0 -37
- package/dist/lib/auto-pull.d.ts +16 -8
- package/dist/lib/auto-pull.js +23 -28
- package/dist/lib/cloud/session-index.js +2 -2
- package/dist/lib/daemon.d.ts +9 -32
- package/dist/lib/daemon.js +70 -84
- package/dist/lib/devices/fleet-divergence.d.ts +101 -0
- package/dist/lib/devices/fleet-divergence.js +188 -0
- package/dist/lib/devices/fleet-inventory.d.ts +19 -0
- package/dist/lib/devices/fleet-inventory.js +57 -0
- package/dist/lib/devices/fleet.d.ts +16 -1
- package/dist/lib/devices/fleet.js +10 -2
- package/dist/lib/devices/health-report.d.ts +10 -2
- package/dist/lib/devices/health-report.js +32 -1
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +16 -0
- package/dist/lib/exec.js +27 -4
- package/dist/lib/feed.d.ts +7 -1
- package/dist/lib/feed.js +96 -6
- package/dist/lib/git.d.ts +13 -0
- package/dist/lib/git.js +102 -4
- package/dist/lib/heal.d.ts +7 -4
- package/dist/lib/heal.js +10 -22
- package/dist/lib/hosts/dispatch.d.ts +9 -0
- package/dist/lib/hosts/dispatch.js +24 -4
- package/dist/lib/hosts/passthrough.js +7 -2
- package/dist/lib/hosts/remote-cmd.d.ts +11 -0
- package/dist/lib/hosts/remote-cmd.js +33 -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 +4 -0
- package/dist/lib/hosts/run-target.js +5 -1
- package/dist/lib/hosts/session-index.js +3 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +2 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
- package/dist/lib/menubar/install-menubar.d.ts +9 -0
- package/dist/lib/menubar/install-menubar.js +14 -0
- package/dist/lib/menubar/notify-desktop.d.ts +44 -0
- package/dist/lib/menubar/notify-desktop.js +78 -0
- package/dist/lib/overdue.d.ts +4 -5
- package/dist/lib/overdue.js +8 -41
- 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 +68 -0
- package/dist/lib/routines.js +74 -7
- package/dist/lib/runner.d.ts +12 -2
- package/dist/lib/runner.js +156 -28
- package/dist/lib/sandbox.js +0 -6
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/secrets/agent.d.ts +11 -4
- package/dist/lib/secrets/agent.js +46 -24
- package/dist/lib/secrets/bundles.d.ts +21 -7
- package/dist/lib/secrets/bundles.js +82 -38
- package/dist/lib/secrets/index.d.ts +11 -2
- package/dist/lib/secrets/index.js +21 -9
- package/dist/lib/secrets/scope.d.ts +26 -0
- package/dist/lib/secrets/scope.js +29 -0
- package/dist/lib/secrets/session-store.d.ts +16 -2
- package/dist/lib/secrets/session-store.js +118 -15
- package/dist/lib/secrets/unlock-hints.d.ts +27 -0
- package/dist/lib/secrets/unlock-hints.js +36 -0
- package/dist/lib/session/active.d.ts +17 -0
- package/dist/lib/session/active.js +105 -7
- package/dist/lib/session/db.d.ts +43 -0
- package/dist/lib/session/db.js +191 -6
- package/dist/lib/session/digest.js +126 -21
- package/dist/lib/session/discover.d.ts +28 -3
- package/dist/lib/session/discover.js +110 -14
- package/dist/lib/session/origin-machine.d.ts +18 -0
- package/dist/lib/session/origin-machine.js +34 -0
- package/dist/lib/session/parse.js +11 -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.js +18 -16
- package/dist/lib/session/state.d.ts +5 -0
- package/dist/lib/session/state.js +94 -12
- package/dist/lib/session/sync/config.js +2 -2
- package/dist/lib/session/types.d.ts +25 -1
- package/dist/lib/session/types.js +8 -0
- package/dist/lib/smart-launch.d.ts +86 -0
- package/dist/lib/smart-launch.js +172 -0
- package/dist/lib/state.d.ts +7 -3
- package/dist/lib/state.js +20 -5
- package/dist/lib/types.d.ts +10 -0
- package/package.json +1 -1
package/dist/commands/exec.js
CHANGED
|
@@ -20,8 +20,9 @@ import { sshResolve } from '../lib/hosts/ssh-config.js';
|
|
|
20
20
|
import * as fs from 'fs';
|
|
21
21
|
import * as path from 'path';
|
|
22
22
|
import * as os from 'os';
|
|
23
|
-
import { randomUUID } from 'crypto';
|
|
24
23
|
import { spawnSync } from 'child_process';
|
|
24
|
+
import { randomUUID } from 'crypto';
|
|
25
|
+
import { isSessionTrackedAgent } from '../lib/session/types.js';
|
|
25
26
|
/** Distinguish a terminal account-picker marker from an explicit @version pin. */
|
|
26
27
|
export function parseRunAccountPickerRequest(agentSpec) {
|
|
27
28
|
const requested = agentSpec.endsWith('@');
|
|
@@ -382,8 +383,8 @@ export function registerRunCommand(program) {
|
|
|
382
383
|
.option('--budget <tokens>', 'Loop token hard-cap: stop once cumulative tokens reach this (stoppedBy: budget), enforced outside the agent. Loop only.')
|
|
383
384
|
.option('--until <signal>', 'Loop stop condition. `signal` reads <runDir>/loop-signal.json {continue,reason} each iteration; absent or continue:false stops (fail-closed). Loop only.')
|
|
384
385
|
.option('--interval <dur>', 'Loop delay between iterations ("0" back-to-back, "30m" paces). Loop only.')
|
|
385
|
-
.option('--host <name>', 'Offload this run onto another machine over SSH
|
|
386
|
-
.option('--device <name>', 'Alias of --host
|
|
386
|
+
.option('--host <name>', 'Offload this run onto another machine over SSH — a device name, registered host, or user@host. Pass "auto" to pick from 14d usage affinity (most-used online device has highest probability). See `agents devices`.')
|
|
387
|
+
.option('--device <name>', 'Alias of --host. Pass "auto" for affinity-based device pick (same as --host auto).')
|
|
387
388
|
.option('--remote-cwd <dir>', "Explicit host working directory for --host runs, used VERBATIM (overrides --cwd; usually --cwd suffices — it re-roots a local-home path onto the remote home). Pass a single-quoted '$HOME/…' or a valid remote absolute path; a local ~ expands here and won't exist there (/Users/you vs /home/you).")
|
|
388
389
|
.option('--no-follow', 'With --host, dispatch detached and return immediately (track via `agents hosts ps/logs`).')
|
|
389
390
|
.option('--any', 'With --host <cap> (a capability tag), pick any matching host instead of erroring when several match.')
|
|
@@ -398,6 +399,8 @@ export function registerRunCommand(program) {
|
|
|
398
399
|
// `--on` and `--computer` are hidden aliases of `--host` — same behavior.
|
|
399
400
|
runCmd.addOption(new Option('--on <name>', 'Alias of --host.').hideHelp());
|
|
400
401
|
runCmd.addOption(new Option('--computer <name>', 'Alias of --host.').hideHelp());
|
|
402
|
+
// Deprecated one-release alias: `agents run … --smart` → treat as `--device auto`.
|
|
403
|
+
runCmd.addOption(new Option('--smart', 'Deprecated: use --device auto (affinity host pick).').hideHelp());
|
|
401
404
|
// Internal: the `--host` dispatch forwards this so the REMOTE run prints its
|
|
402
405
|
// resolved session id as a one-line stdout sentinel (hosts/session-marker.ts),
|
|
403
406
|
// letting the launcher relate the remote-created session back to itself for
|
|
@@ -494,11 +497,14 @@ export function registerRunCommand(program) {
|
|
|
494
497
|
// their existing meaning in every dispatch path below.
|
|
495
498
|
const accountPicker = parseRunAccountPickerRequest(agentSpec);
|
|
496
499
|
const accountPickerRequested = accountPicker.requested;
|
|
497
|
-
|
|
500
|
+
let normalizedAgentSpec = accountPicker.normalizedAgentSpec;
|
|
498
501
|
if (!accountPicker.valid) {
|
|
499
502
|
console.error(chalk.red(`Invalid account picker target: ${agentSpec}. Use agents run <agent>@.`));
|
|
500
503
|
process.exit(1);
|
|
501
504
|
}
|
|
505
|
+
// Account-picker conflict check runs BEFORE device=auto may set balanced,
|
|
506
|
+
// so an implicit balanced preference never surfaces as a fake
|
|
507
|
+
// "cannot be combined with --balanced" when the user only typed trailing @.
|
|
502
508
|
if (accountPickerRequested) {
|
|
503
509
|
const conflicts = runAccountPickerConflicts(options);
|
|
504
510
|
if (conflicts.length > 0) {
|
|
@@ -507,6 +513,27 @@ export function registerRunCommand(program) {
|
|
|
507
513
|
process.exit(1);
|
|
508
514
|
}
|
|
509
515
|
}
|
|
516
|
+
// --device auto / --host auto (and deprecated --smart): affinity-pick host.
|
|
517
|
+
// Harness is always the agent the user typed — never auto-picked.
|
|
518
|
+
// Affinity failure degrades to local (does not kill the run).
|
|
519
|
+
{
|
|
520
|
+
const { applyDeviceAutoToOptions } = await import('../lib/smart-launch.js');
|
|
521
|
+
const result = applyDeviceAutoToOptions(options, {
|
|
522
|
+
accountPickerRequested,
|
|
523
|
+
});
|
|
524
|
+
if (!options.quiet && result.deprecationSmart) {
|
|
525
|
+
process.stderr.write(chalk.yellow('[agents] --smart is deprecated; use --device auto\n'));
|
|
526
|
+
}
|
|
527
|
+
if (!options.quiet && result.skipped) {
|
|
528
|
+
process.stderr.write(chalk.yellow(`[agents] device=auto skipped: ${result.skipped} (running local)\n`));
|
|
529
|
+
}
|
|
530
|
+
if (!options.quiet && result.banner) {
|
|
531
|
+
const { hostLabel, deviceHint, acctNote } = result.banner;
|
|
532
|
+
process.stderr.write(chalk.gray(`[agents] device=auto → ${hostLabel}` +
|
|
533
|
+
(deviceHint ? ` (affinity ${deviceHint})` : '') +
|
|
534
|
+
` · ${acctNote}\n`));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
510
537
|
// --lease: invent a disposable cloud box for this run (via crabbox), run
|
|
511
538
|
// the agent there, then tear it down. --box: reuse a named warm crabbox
|
|
512
539
|
// box, run the same bootstrap there, and leave the box running.
|
|
@@ -877,7 +904,7 @@ export function registerRunCommand(program) {
|
|
|
877
904
|
process.exit(1);
|
|
878
905
|
}
|
|
879
906
|
const hostName = hostGiven[0];
|
|
880
|
-
const { resolveHostRunTarget, dispatchPromptToHost, HostResolutionError } = await import('../lib/hosts/run-target.js');
|
|
907
|
+
const { resolveHostRunTarget, resolveHostSessionId, dispatchPromptToHost, HostResolutionError } = await import('../lib/hosts/run-target.js');
|
|
881
908
|
const { runInteractiveOnHost } = await import('../lib/hosts/dispatch.js');
|
|
882
909
|
const { registerInteractiveHostSession } = await import('../lib/hosts/session-index.js');
|
|
883
910
|
const { RUN_OPTION_REJECT_MESSAGES } = await import('../lib/hosts/remote-cmd.js');
|
|
@@ -1035,10 +1062,21 @@ export function registerRunCommand(program) {
|
|
|
1035
1062
|
process.exit(1);
|
|
1036
1063
|
}
|
|
1037
1064
|
// Mirror the local path (lib/exec.ts): only Claude accepts a forced
|
|
1038
|
-
// `--session-id`.
|
|
1039
|
-
//
|
|
1040
|
-
//
|
|
1041
|
-
const hostSessionId = runAgent
|
|
1065
|
+
// `--session-id`. Adopt the caller's id when present; otherwise mint
|
|
1066
|
+
// one here. Registering that same id keeps the local index aligned
|
|
1067
|
+
// with the remote agent. On resume, don't mint a new one.
|
|
1068
|
+
const hostSessionId = resolveHostSessionId(runAgent, resumeId, options.sessionId);
|
|
1069
|
+
// For every OTHER agent the remote coins its own id, which we can't
|
|
1070
|
+
// know up front. Forward a launch id we control as AGENT_LAUNCH_ID:
|
|
1071
|
+
// the remote `agents run` adopts it (exec.ts resolveLaunchId) and its
|
|
1072
|
+
// SessionStart hook records the real id under that exact key, so after
|
|
1073
|
+
// the stream we resolve the id by one ssh read of the remote hook
|
|
1074
|
+
// record — the same launch-id join used locally (RUSH-2034). Not
|
|
1075
|
+
// needed for Claude (id forced) or resume (id already known).
|
|
1076
|
+
const correlationLaunchId = !hostSessionId && !resumeId && isSessionTrackedAgent(runAgent) ? randomUUID() : undefined;
|
|
1077
|
+
const hostEnv = correlationLaunchId
|
|
1078
|
+
? [...options.env, `AGENT_LAUNCH_ID=${correlationLaunchId}`]
|
|
1079
|
+
: options.env;
|
|
1042
1080
|
if (hostSessionId) {
|
|
1043
1081
|
registerInteractiveHostSession({
|
|
1044
1082
|
cwd: process.cwd(),
|
|
@@ -1058,7 +1096,7 @@ export function registerRunCommand(program) {
|
|
|
1058
1096
|
mode: options.mode,
|
|
1059
1097
|
model: options.model,
|
|
1060
1098
|
effort: options.effort,
|
|
1061
|
-
env:
|
|
1099
|
+
env: hostEnv,
|
|
1062
1100
|
addDir: hostAddDirs,
|
|
1063
1101
|
json: options.json,
|
|
1064
1102
|
verbose: options.verbose,
|
|
@@ -1074,14 +1112,41 @@ export function registerRunCommand(program) {
|
|
|
1074
1112
|
forceInteractive: options.interactive,
|
|
1075
1113
|
copyCreds: hostCopyCreds,
|
|
1076
1114
|
});
|
|
1115
|
+
// Resolve a non-Claude agent's REAL remote session id now the run has
|
|
1116
|
+
// booted (its hook has fired on the peer): one ssh read of the remote
|
|
1117
|
+
// hook record, keyed by the launch id we forwarded. Register it so the
|
|
1118
|
+
// run shows in `agents sessions` and can be reconnected/focused —
|
|
1119
|
+
// closing the non-Claude gap RUSH-2033 left. Best-effort: an
|
|
1120
|
+
// unreachable host or a not-yet-landed record leaves the run un-mapped
|
|
1121
|
+
// rather than mis-mapped.
|
|
1122
|
+
let resolvedRemoteId;
|
|
1123
|
+
if (correlationLaunchId) {
|
|
1124
|
+
const { resolveRemoteSessionId } = await import('../lib/hosts/remote-session-id.js');
|
|
1125
|
+
const { sshTargetFor } = await import('../lib/hosts/types.js');
|
|
1126
|
+
try {
|
|
1127
|
+
resolvedRemoteId = resolveRemoteSessionId(sshTargetFor(host), correlationLaunchId);
|
|
1128
|
+
}
|
|
1129
|
+
catch {
|
|
1130
|
+
/* ssh read is best-effort — keep the run un-mapped, never guess */
|
|
1131
|
+
}
|
|
1132
|
+
if (resolvedRemoteId) {
|
|
1133
|
+
registerInteractiveHostSession({
|
|
1134
|
+
cwd: process.cwd(),
|
|
1135
|
+
host: host.name,
|
|
1136
|
+
agent: runAgent,
|
|
1137
|
+
sessionId: resolvedRemoteId,
|
|
1138
|
+
name: options.name,
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1077
1142
|
// A network drop kills the local ssh client (exit 255) but the remote
|
|
1078
1143
|
// agent survives in its detached tmux session. With a known session id
|
|
1079
|
-
// (Claude,
|
|
1080
|
-
//
|
|
1081
|
-
// the
|
|
1082
|
-
//
|
|
1083
|
-
//
|
|
1084
|
-
const reconnectId = hostSessionId ?? resumeId;
|
|
1144
|
+
// (Claude's forced id, a resumed run, or a non-Claude id we just
|
|
1145
|
+
// resolved from the remote hook record) and a tmux-hosted run,
|
|
1146
|
+
// re-attach the live pane automatically instead of exiting — the user
|
|
1147
|
+
// never has to notice the drop and `agents sessions focus` by hand.
|
|
1148
|
+
// `raw` runs aren't tmux wrapped, so there is nothing to reconnect to.
|
|
1149
|
+
const reconnectId = hostSessionId ?? resolvedRemoteId ?? resumeId;
|
|
1085
1150
|
if (reconnectId && !isRaw) {
|
|
1086
1151
|
const { reconnectInteractiveSession, SSH_CONN_FAILURE } = await import('../lib/hosts/reconnect.js');
|
|
1087
1152
|
if (exitCode === SSH_CONN_FAILURE) {
|
|
@@ -1127,6 +1192,7 @@ export function registerRunCommand(program) {
|
|
|
1127
1192
|
remoteCwd: hostCwd,
|
|
1128
1193
|
name: options.name,
|
|
1129
1194
|
resume: resumeId,
|
|
1195
|
+
sessionId: options.sessionId,
|
|
1130
1196
|
follow: options.follow !== false,
|
|
1131
1197
|
passthroughArgs,
|
|
1132
1198
|
copyCreds: hostCopyCreds,
|
|
@@ -1883,11 +1949,13 @@ export function registerRunCommand(program) {
|
|
|
1883
1949
|
else {
|
|
1884
1950
|
const { bundle, env: bundleEnv } = readAndResolveBundleEnv(bundleName, {
|
|
1885
1951
|
caller: `agent ${agent}`,
|
|
1952
|
+
agent,
|
|
1886
1953
|
keys: secretsKeysSubset,
|
|
1887
1954
|
allowExpired: options.allowExpired,
|
|
1888
|
-
//
|
|
1889
|
-
//
|
|
1890
|
-
//
|
|
1955
|
+
// The harness identity scopes any cached grant. It no longer lets the
|
|
1956
|
+
// requesting agent wait for interactive approval — an agent launch
|
|
1957
|
+
// resolves broker-only and fails fast naming
|
|
1958
|
+
// `agents secrets unlock <bundle>` (bundles.ts:interactiveUnlock).
|
|
1891
1959
|
agentOnly: isHeadlessSecretsContext(),
|
|
1892
1960
|
});
|
|
1893
1961
|
const entries = describeBundle(bundle);
|
package/dist/commands/repo.js
CHANGED
|
@@ -856,6 +856,10 @@ export function registerRepoCommands(program) {
|
|
|
856
856
|
}
|
|
857
857
|
else {
|
|
858
858
|
spinner.fail(`${formatRepoTarget(t.alias, t.dir)}: ${result.error}`);
|
|
859
|
+
// A failed repo must fail the command. Without this, `agents fleet run
|
|
860
|
+
// "agents repo pull user"` reported 11 ok across a fleet that pulled
|
|
861
|
+
// nothing — the silence that hid RUSH-2056. Matches commands/sync.ts.
|
|
862
|
+
process.exitCode = 1;
|
|
859
863
|
}
|
|
860
864
|
}
|
|
861
865
|
// RUSH-1980: a pull rewrites the routine YAML on disk, but the daemon's
|
|
@@ -911,6 +915,10 @@ export function registerRepoCommands(program) {
|
|
|
911
915
|
}
|
|
912
916
|
else {
|
|
913
917
|
spinner.fail(`${formatRepoTarget(t.alias, t.dir)}: ${result.error}`);
|
|
918
|
+
// A failed repo must fail the command. Without this, `agents fleet run
|
|
919
|
+
// "agents repo push user"` reported ok across a fleet that pushed
|
|
920
|
+
// nothing — the silence that hid RUSH-2056. Matches commands/sync.ts.
|
|
921
|
+
process.exitCode = 1;
|
|
914
922
|
}
|
|
915
923
|
}
|
|
916
924
|
});
|
|
@@ -12,7 +12,8 @@ import * as path from 'path';
|
|
|
12
12
|
import * as yaml from 'yaml';
|
|
13
13
|
import { isDaemonRunning, signalDaemonReload, startDaemon, stopDaemon, readDaemonLog, getDaemonStatus, } from '../lib/daemon.js';
|
|
14
14
|
import { humanizeCron, humanizeNextRun, formatRepoLink, REPO_DISPLAY_MAX } from '../lib/routines-format.js';
|
|
15
|
-
import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, getLatestRun, getRunDir, getJobPath, parseAtTime, jobRunsOnThisDevice, checkJobDeviceEligibility, normalizeTriggerEvent, } from '../lib/routines.js';
|
|
15
|
+
import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, getLatestRun, getRunDir, getJobPath, parseAtTime, jobRunsOnThisDevice, checkJobDeviceEligibility, normalizeTriggerEvent, parseHostStrategy, resolveHostStrategy, placementRequiresFiringPin, HOST_STRATEGIES, } from '../lib/routines.js';
|
|
16
|
+
import { discoverProjectRoutinesAt, enableProjectRoutines, disableProjectRoutines, syncProjectRoutines, syncAllProjectRoutines, listEnabledProjectRoots, resolveProjectRoot, displayProjectPath, listProjectRoutineFiles, } from '../lib/routines-project.js';
|
|
16
17
|
import { fireWebhookJobs, matchJobsToWebhook } from '../lib/triggers/webhook.js';
|
|
17
18
|
import { getRoutinesDir } from '../lib/state.js';
|
|
18
19
|
import { IS_WINDOWS } from '../lib/platform/index.js';
|
|
@@ -131,6 +132,24 @@ function ensureSchedulerRunning(opts = {}) {
|
|
|
131
132
|
function writeJson(payload) {
|
|
132
133
|
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
133
134
|
}
|
|
135
|
+
function printSyncResult(sync) {
|
|
136
|
+
console.log(chalk.bold(displayProjectPath(sync.projectRoot)));
|
|
137
|
+
if (sync.synced.length === 0 && sync.skipped.length === 0 && sync.removed.length === 0 && sync.errors.length === 0) {
|
|
138
|
+
console.log(chalk.gray(' (no project routines)'));
|
|
139
|
+
}
|
|
140
|
+
if (sync.synced.length > 0) {
|
|
141
|
+
console.log(chalk.green(` synced: ${sync.synced.join(', ')}`));
|
|
142
|
+
}
|
|
143
|
+
if (sync.removed.length > 0) {
|
|
144
|
+
console.log(chalk.gray(` removed: ${sync.removed.join(', ')}`));
|
|
145
|
+
}
|
|
146
|
+
for (const s of sync.skipped) {
|
|
147
|
+
console.log(chalk.yellow(` skipped ${s.name}: ${s.reason}`));
|
|
148
|
+
}
|
|
149
|
+
for (const e of sync.errors) {
|
|
150
|
+
console.log(chalk.red(` error ${e.name}: ${e.error}`));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
134
153
|
function runMetaJson(run) {
|
|
135
154
|
return {
|
|
136
155
|
jobId: run.jobName,
|
|
@@ -233,6 +252,14 @@ export function registerRoutinesCommands(program) {
|
|
|
233
252
|
# Create from YAML (for complex routines with multiple settings)
|
|
234
253
|
agents routines add weekly-report.yml
|
|
235
254
|
|
|
255
|
+
# Place the job body on a fleet device / cloud / named host (not --host)
|
|
256
|
+
agents routines add drain --schedule "0 3 * * *" --agent claude --placement fleet --prompt "Drain queue"
|
|
257
|
+
agents routines add review --schedule "0 9 * * 1" --agent claude --placement cloud --prompt "Review open PRs"
|
|
258
|
+
|
|
259
|
+
# Opt a project's .agents/routines/*.yml into daemon firing (never auto)
|
|
260
|
+
agents routines enable-project --yes
|
|
261
|
+
agents routines sync
|
|
262
|
+
|
|
236
263
|
# List all routines and their next run times
|
|
237
264
|
agents routines list
|
|
238
265
|
|
|
@@ -270,8 +297,10 @@ export function registerRoutinesCommands(program) {
|
|
|
270
297
|
- Detached/daemon fires use the pre-flight pick only (next tick re-selects).
|
|
271
298
|
- Diagnostic lines log which account was picked, which were skipped, and
|
|
272
299
|
each failover hop: look for "[agents] routine <name>:" in the run log.
|
|
273
|
-
-
|
|
274
|
-
|
|
300
|
+
- Claude auth: a routine authenticates through the pinned account's own
|
|
301
|
+
on-disk login (the same one 'agents run claude' uses on this device).
|
|
302
|
+
The daemon injects no token; if that account's login has expired the
|
|
303
|
+
run is skipped up front with a re-login hint.
|
|
275
304
|
`,
|
|
276
305
|
});
|
|
277
306
|
routinesCmd
|
|
@@ -323,6 +352,10 @@ export function registerRoutinesCommands(program) {
|
|
|
323
352
|
timezone: job.timezone ?? null,
|
|
324
353
|
devices: job.devices ?? [],
|
|
325
354
|
host: job.host ?? null,
|
|
355
|
+
hostStrategy: resolveHostStrategy(job),
|
|
356
|
+
source: job.source ?? null,
|
|
357
|
+
sourceRepo: job.source?.repo ?? job.repo ?? null,
|
|
358
|
+
sourceBranch: job.source?.branch ?? null,
|
|
326
359
|
runsHere: jobRunsOnThisDevice(job),
|
|
327
360
|
enabled: job.enabled,
|
|
328
361
|
overdue: overdueSet.has(job.name),
|
|
@@ -368,7 +401,12 @@ export function registerRoutinesCommands(program) {
|
|
|
368
401
|
}
|
|
369
402
|
const latestRun = getLatestRun(job.name);
|
|
370
403
|
const lastStatus = latestRun?.status || '-';
|
|
371
|
-
|
|
404
|
+
// Prefer project-source repo (with optional @branch) over bare job.repo.
|
|
405
|
+
const sourceRepo = job.source?.repo ?? job.repo;
|
|
406
|
+
const sourceLabel = sourceRepo
|
|
407
|
+
? (job.source?.branch ? `${sourceRepo}@${job.source.branch}` : sourceRepo)
|
|
408
|
+
: null;
|
|
409
|
+
const repoInfo = formatRepoLink(sourceLabel ?? job.repo);
|
|
372
410
|
const repoCell = link(repoInfo.display, repoInfo.href);
|
|
373
411
|
// Pad based on the display string, not the raw cell (which may include escape codes).
|
|
374
412
|
const repoPadding = Math.max(0, REPO_W - repoInfo.display.length);
|
|
@@ -376,8 +414,16 @@ export function registerRoutinesCommands(program) {
|
|
|
376
414
|
// chalk adds escape codes; pad the raw word and let chalk wrap it.
|
|
377
415
|
const enabledWord = job.enabled ? 'yes' : 'no';
|
|
378
416
|
const enabledPad = Math.max(0, ENABLED_W - enabledWord.length);
|
|
379
|
-
// Placement
|
|
380
|
-
const
|
|
417
|
+
// Placement rides in the Devices cell: eligibility →execution strategy.
|
|
418
|
+
const strategy = resolveHostStrategy(job);
|
|
419
|
+
const placementTag = strategy === 'local'
|
|
420
|
+
? (job.host ? `→${job.host}` : '')
|
|
421
|
+
: strategy === 'host'
|
|
422
|
+
? `→${job.host ?? '?'}`
|
|
423
|
+
: strategy === 'fleet'
|
|
424
|
+
? '→fleet'
|
|
425
|
+
: '→cloud';
|
|
426
|
+
const deviceFull = [job.devices?.join(',') ?? '', placementTag]
|
|
381
427
|
.filter(Boolean)
|
|
382
428
|
.join(' ');
|
|
383
429
|
const deviceWord = deviceFull.length === 0
|
|
@@ -423,7 +469,8 @@ export function registerRoutinesCommands(program) {
|
|
|
423
469
|
.option('-t, --timeout <timeout>', 'Kill the agent if it runs longer than this (e.g., 10m, 2h, 3d, 1w; max 1w)', '10m')
|
|
424
470
|
.option('--timezone <tz>', 'Interpret schedule in this timezone (e.g., America/Los_Angeles)')
|
|
425
471
|
.option('--devices <names>', 'Fleet allowlist (comma-separated): only listed devices schedule and fire this routine. Omit for unrestricted.')
|
|
426
|
-
.option('--run-on <name>', 'Execute the job body on this machine over SSH (a registered host, device, capability tag, or user@host). Placement, not eligibility — see --devices. Auto-pins devices to THIS machine unless --devices is given.')
|
|
472
|
+
.option('--run-on <name>', 'Execute the job body on this machine over SSH (a registered host, device, capability tag, or user@host). Sets hostStrategy=host. Placement, not eligibility — see --devices. Auto-pins devices to THIS machine unless --devices is given.')
|
|
473
|
+
.option('--placement <strategy>', `Where the job body runs: ${HOST_STRATEGIES.join('|')} (default: local, or host when --run-on is set). Not the same as --host (which manages routines on a remote machine).`)
|
|
427
474
|
.option('--run-cwd <dir>', 'Working directory on the --run-on host (--remote-cwd is taken by the remote-management passthrough)')
|
|
428
475
|
.option('--at <time>', 'One-shot mode: run once at this time (e.g., "14:30" or "2026-02-24 09:00"), then disable')
|
|
429
476
|
.option('--on <source:event>', 'Webhook trigger instead of/in addition to a schedule: github:pull_request or linear:Issue')
|
|
@@ -490,12 +537,29 @@ export function registerRoutinesCommands(program) {
|
|
|
490
537
|
if (options.devices !== undefined) {
|
|
491
538
|
devices = await parseAndValidateDevices(options.devices);
|
|
492
539
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
540
|
+
let hostStrategy;
|
|
541
|
+
try {
|
|
542
|
+
hostStrategy = parseHostStrategy(options.placement) ?? undefined;
|
|
543
|
+
}
|
|
544
|
+
catch (err) {
|
|
545
|
+
console.error(chalk.red(err.message));
|
|
546
|
+
process.exit(1);
|
|
547
|
+
}
|
|
548
|
+
// --run-on implies host strategy when the user didn't pick one.
|
|
549
|
+
if (options.runOn && !hostStrategy)
|
|
550
|
+
hostStrategy = 'host';
|
|
551
|
+
if (hostStrategy === 'host' && !options.runOn) {
|
|
552
|
+
console.error(chalk.red('--placement host requires --run-on <name>'));
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
// Off-box placement without a --devices pin would fire on EVERY daemon
|
|
556
|
+
// in the fleet, each dispatching once — duplicate runs (RUSH-1980).
|
|
557
|
+
// Pin to this machine unless the user chose an explicit eligibility set.
|
|
558
|
+
const strategyForPin = hostStrategy
|
|
559
|
+
?? (options.runOn ? 'host' : 'local');
|
|
560
|
+
if (placementRequiresFiringPin(strategyForPin) && !devices) {
|
|
497
561
|
devices = [machineId()];
|
|
498
|
-
console.error(chalk.gray(`--
|
|
562
|
+
console.error(chalk.gray(`--placement ${strategyForPin} with no --devices: pinned firing to this machine (${devices[0]}).`));
|
|
499
563
|
}
|
|
500
564
|
const config = {
|
|
501
565
|
name: nameOrPath,
|
|
@@ -512,6 +576,7 @@ export function registerRoutinesCommands(program) {
|
|
|
512
576
|
timezone: options.timezone,
|
|
513
577
|
...(devices ? { devices } : {}),
|
|
514
578
|
...(options.runOn ? { host: options.runOn } : {}),
|
|
579
|
+
...(hostStrategy ? { hostStrategy } : {}),
|
|
515
580
|
...(options.runCwd ? { remoteCwd: options.runCwd } : {}),
|
|
516
581
|
...(runOnce ? { runOnce: true } : {}),
|
|
517
582
|
...(options.endAt ? { endAt: options.endAt } : {}),
|
|
@@ -586,11 +651,12 @@ export function registerRoutinesCommands(program) {
|
|
|
586
651
|
enabled: true,
|
|
587
652
|
...parsed,
|
|
588
653
|
};
|
|
589
|
-
// Same duplicate-fire guard as
|
|
654
|
+
// Same duplicate-fire guard as --placement/--run-on: off-box placement
|
|
590
655
|
// with no eligibility pin would fire from every daemon in the fleet.
|
|
591
|
-
|
|
656
|
+
const fileStrategy = resolveHostStrategy(config);
|
|
657
|
+
if (placementRequiresFiringPin(fileStrategy) && (!config.devices || config.devices.length === 0)) {
|
|
592
658
|
config.devices = [machineId()];
|
|
593
|
-
console.error(chalk.gray(
|
|
659
|
+
console.error(chalk.gray(`${fileStrategy} placement with no devices pin: pinned firing to this machine (${config.devices[0]}).`));
|
|
594
660
|
}
|
|
595
661
|
writeJob(config);
|
|
596
662
|
if (options.json) {
|
|
@@ -1120,6 +1186,12 @@ export function registerRoutinesCommands(program) {
|
|
|
1120
1186
|
process.exit(1);
|
|
1121
1187
|
}
|
|
1122
1188
|
if (options.clear) {
|
|
1189
|
+
const strategy = resolveHostStrategy(job);
|
|
1190
|
+
if (placementRequiresFiringPin(strategy)) {
|
|
1191
|
+
console.error(chalk.red(`Cannot clear devices for hostStrategy: ${strategy} — without a pin every fleet daemon would dispatch once.`));
|
|
1192
|
+
console.error(chalk.gray(`Keep a single-machine pin (e.g. --set ${machineId()}) or switch to --placement local.`));
|
|
1193
|
+
process.exit(1);
|
|
1194
|
+
}
|
|
1123
1195
|
job.devices = undefined;
|
|
1124
1196
|
writeJob(job);
|
|
1125
1197
|
console.log(chalk.green(`Devices cleared for '${name}' — runs on all devices`));
|
|
@@ -1130,6 +1202,12 @@ export function registerRoutinesCommands(program) {
|
|
|
1130
1202
|
if (hasSet) {
|
|
1131
1203
|
const devices = await parseAndValidateDevices(options.set);
|
|
1132
1204
|
job.devices = devices;
|
|
1205
|
+
// Re-validate so host/fleet/cloud can't be left with an empty set via --set ""
|
|
1206
|
+
const errs = validateJob(job);
|
|
1207
|
+
if (errs.length > 0) {
|
|
1208
|
+
console.error(chalk.red(errs.join('\n')));
|
|
1209
|
+
process.exit(1);
|
|
1210
|
+
}
|
|
1133
1211
|
writeJob(job);
|
|
1134
1212
|
console.log(chalk.green(`Devices for '${name}' set to: ${devices.join(', ')}`));
|
|
1135
1213
|
if (isDaemonRunning())
|
|
@@ -1158,6 +1236,11 @@ export function registerRoutinesCommands(program) {
|
|
|
1158
1236
|
})),
|
|
1159
1237
|
});
|
|
1160
1238
|
if (selected.length === 0) {
|
|
1239
|
+
const strategy = resolveHostStrategy(job);
|
|
1240
|
+
if (placementRequiresFiringPin(strategy)) {
|
|
1241
|
+
console.error(chalk.red(`Cannot clear devices for hostStrategy: ${strategy} — without a pin every fleet daemon would dispatch once.`));
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1161
1244
|
job.devices = undefined;
|
|
1162
1245
|
writeJob(job);
|
|
1163
1246
|
console.log(chalk.green(`Devices cleared for '${name}' — runs on all devices`));
|
|
@@ -1250,6 +1333,183 @@ export function registerRoutinesCommands(program) {
|
|
|
1250
1333
|
console.log(chalk.gray('\n Start the scheduler to begin firing routines: agents routines start'));
|
|
1251
1334
|
}
|
|
1252
1335
|
});
|
|
1336
|
+
routinesCmd
|
|
1337
|
+
.command('enable-project [path]')
|
|
1338
|
+
.description('Opt a project\'s .agents/routines/*.yml into daemon firing. Requires explicit approval — project routines never auto-fire from a cloned repo. Materialises copies into ~/.agents/routines/ with source provenance.')
|
|
1339
|
+
.option('--yes', 'Skip the interactive confirmation prompt')
|
|
1340
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
1341
|
+
.action(async (projectPath, options) => {
|
|
1342
|
+
const root = projectPath
|
|
1343
|
+
? path.resolve(projectPath)
|
|
1344
|
+
: resolveProjectRoot(process.cwd());
|
|
1345
|
+
if (!root) {
|
|
1346
|
+
console.error(chalk.red('No project .agents/ directory found from the current directory.'));
|
|
1347
|
+
console.error(chalk.gray('Run from inside a project, or pass the project path: agents routines enable-project /path/to/repo'));
|
|
1348
|
+
process.exit(1);
|
|
1349
|
+
}
|
|
1350
|
+
const files = listProjectRoutineFiles(root);
|
|
1351
|
+
if (files.length === 0) {
|
|
1352
|
+
console.error(chalk.red(`No routines found under ${path.join(root, '.agents', 'routines')}`));
|
|
1353
|
+
process.exit(1);
|
|
1354
|
+
}
|
|
1355
|
+
if (!options.yes) {
|
|
1356
|
+
if (!isInteractiveTerminal()) {
|
|
1357
|
+
console.error(chalk.red('Refusing to enable project routines non-interactively without --yes.'));
|
|
1358
|
+
console.error(chalk.gray(`Found ${files.length} routine(s) in ${displayProjectPath(root)}. Re-run with --yes to confirm.`));
|
|
1359
|
+
process.exit(1);
|
|
1360
|
+
}
|
|
1361
|
+
try {
|
|
1362
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
1363
|
+
const ok = await confirm({
|
|
1364
|
+
message: `Enable daemon firing for ${files.length} project routine(s) in ${displayProjectPath(root)}?`,
|
|
1365
|
+
default: false,
|
|
1366
|
+
});
|
|
1367
|
+
if (!ok) {
|
|
1368
|
+
console.log(chalk.gray('Cancelled'));
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
catch (err) {
|
|
1373
|
+
if (isPromptCancelled(err)) {
|
|
1374
|
+
console.log(chalk.gray('Cancelled'));
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
throw err;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
const newly = enableProjectRoutines(root);
|
|
1381
|
+
const sync = syncProjectRoutines(root);
|
|
1382
|
+
if (isDaemonRunning())
|
|
1383
|
+
signalDaemonReload();
|
|
1384
|
+
if (options.json) {
|
|
1385
|
+
writeJson({
|
|
1386
|
+
ok: true,
|
|
1387
|
+
projectRoot: root,
|
|
1388
|
+
newlyEnabled: newly,
|
|
1389
|
+
synced: sync.synced,
|
|
1390
|
+
skipped: sync.skipped,
|
|
1391
|
+
removed: sync.removed,
|
|
1392
|
+
errors: sync.errors,
|
|
1393
|
+
});
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
console.log(chalk.green(newly
|
|
1397
|
+
? `Enabled project routines for ${displayProjectPath(root)}`
|
|
1398
|
+
: `Project routines already enabled for ${displayProjectPath(root)}`));
|
|
1399
|
+
if (sync.synced.length > 0) {
|
|
1400
|
+
console.log(chalk.gray(` Synced: ${sync.synced.join(', ')}`));
|
|
1401
|
+
}
|
|
1402
|
+
for (const s of sync.skipped) {
|
|
1403
|
+
console.log(chalk.yellow(` Skipped ${s.name}: ${s.reason}`));
|
|
1404
|
+
}
|
|
1405
|
+
for (const e of sync.errors) {
|
|
1406
|
+
console.log(chalk.red(` Error ${e.name}: ${e.error}`));
|
|
1407
|
+
}
|
|
1408
|
+
console.log(chalk.gray('Daemon will fire these after reload. Re-sync later with: agents routines sync'));
|
|
1409
|
+
});
|
|
1410
|
+
routinesCmd
|
|
1411
|
+
.command('disable-project [path]')
|
|
1412
|
+
.description('Remove a project from the project-routines allowlist. Use --remove-synced to also delete the user-layer copies.')
|
|
1413
|
+
.option('--remove-synced', 'Delete user-layer routines that were materialised from this project')
|
|
1414
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
1415
|
+
.action(async (projectPath, options) => {
|
|
1416
|
+
const root = projectPath
|
|
1417
|
+
? path.resolve(projectPath)
|
|
1418
|
+
: resolveProjectRoot(process.cwd());
|
|
1419
|
+
if (!root) {
|
|
1420
|
+
console.error(chalk.red('No project .agents/ directory found from the current directory.'));
|
|
1421
|
+
process.exit(1);
|
|
1422
|
+
}
|
|
1423
|
+
const result = disableProjectRoutines(root, { removeSynced: options.removeSynced });
|
|
1424
|
+
if (isDaemonRunning())
|
|
1425
|
+
signalDaemonReload();
|
|
1426
|
+
if (options.json) {
|
|
1427
|
+
writeJson({ ok: true, projectRoot: root, ...result });
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
if (!result.removed) {
|
|
1431
|
+
console.log(chalk.gray(`Project ${displayProjectPath(root)} was not on the allowlist`));
|
|
1432
|
+
}
|
|
1433
|
+
else {
|
|
1434
|
+
console.log(chalk.green(`Disabled project routines for ${displayProjectPath(root)}`));
|
|
1435
|
+
}
|
|
1436
|
+
if (result.deletedJobs.length > 0) {
|
|
1437
|
+
console.log(chalk.gray(` Removed user-layer copies: ${result.deletedJobs.join(', ')}`));
|
|
1438
|
+
}
|
|
1439
|
+
});
|
|
1440
|
+
routinesCmd
|
|
1441
|
+
.command('sync [path]')
|
|
1442
|
+
.description('Refresh user-layer copies of opted-in project routines from their .agents/routines/*.yml sources. With no path, syncs every enabled project. Also runs automatically on daemon reload (SIGHUP).')
|
|
1443
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
1444
|
+
.action(async (projectPath, options) => {
|
|
1445
|
+
if (projectPath) {
|
|
1446
|
+
const root = path.resolve(projectPath);
|
|
1447
|
+
const isEnabled = listEnabledProjectRoots().some((p) => p === root);
|
|
1448
|
+
if (!isEnabled) {
|
|
1449
|
+
console.error(chalk.red(`Project ${displayProjectPath(root)} is not enabled. Run: agents routines enable-project ${root}`));
|
|
1450
|
+
process.exit(1);
|
|
1451
|
+
}
|
|
1452
|
+
const sync = syncProjectRoutines(root);
|
|
1453
|
+
if (isDaemonRunning())
|
|
1454
|
+
signalDaemonReload();
|
|
1455
|
+
if (options.json) {
|
|
1456
|
+
writeJson({ ok: true, ...sync });
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
printSyncResult(sync);
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
const all = syncAllProjectRoutines();
|
|
1463
|
+
if (isDaemonRunning())
|
|
1464
|
+
signalDaemonReload();
|
|
1465
|
+
if (options.json) {
|
|
1466
|
+
writeJson({ ok: true, ...all });
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
if (all.projects.length === 0 && all.missing.length === 0) {
|
|
1470
|
+
console.log(chalk.gray('No project roots on the routines allowlist.'));
|
|
1471
|
+
console.log(chalk.gray(' Enable one with: agents routines enable-project'));
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
for (const p of all.projects)
|
|
1475
|
+
printSyncResult(p);
|
|
1476
|
+
for (const m of all.missing) {
|
|
1477
|
+
console.log(chalk.yellow(`Missing project root (still on allowlist): ${displayProjectPath(m)}`));
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
routinesCmd
|
|
1481
|
+
.command('projects')
|
|
1482
|
+
.description('List project roots opted into daemon-fired project routines')
|
|
1483
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
1484
|
+
.action((options) => {
|
|
1485
|
+
const roots = listEnabledProjectRoots();
|
|
1486
|
+
if (options.json) {
|
|
1487
|
+
writeJson(roots.map((r) => ({
|
|
1488
|
+
path: r,
|
|
1489
|
+
display: displayProjectPath(r),
|
|
1490
|
+
routines: listProjectRoutineFiles(r).map((f) => f.name),
|
|
1491
|
+
})));
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
if (roots.length === 0) {
|
|
1495
|
+
console.log(chalk.gray('No projects enabled. Use: agents routines enable-project'));
|
|
1496
|
+
// Offer a discovery hint for the current project.
|
|
1497
|
+
const discovered = discoverProjectRoutinesAt(process.cwd());
|
|
1498
|
+
if (discovered) {
|
|
1499
|
+
console.log(chalk.gray(` Found ${discovered.files.length} routine(s) in ${displayProjectPath(discovered.projectRoot)} — enable with: agents routines enable-project`));
|
|
1500
|
+
}
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
console.log(chalk.bold('Enabled project routines\n'));
|
|
1504
|
+
for (const r of roots) {
|
|
1505
|
+
const files = listProjectRoutineFiles(r);
|
|
1506
|
+
console.log(` ${chalk.cyan(displayProjectPath(r))} ${chalk.gray(`(${files.length} routine${files.length === 1 ? '' : 's'})`)}`);
|
|
1507
|
+
for (const f of files) {
|
|
1508
|
+
console.log(chalk.gray(` - ${f.name}`));
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
console.log();
|
|
1512
|
+
});
|
|
1253
1513
|
routinesCmd
|
|
1254
1514
|
.command('scheduler-logs')
|
|
1255
1515
|
.description('Read scheduler log output (for debugging why a routine did not fire). Use --follow to stream.')
|