@phnx-labs/agents-cli 1.22.58 → 1.22.59
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 +238 -0
- package/README.md +29 -0
- package/dist/bootstrap.js +32 -1
- package/dist/commands/monitors.js +187 -23
- package/dist/commands/routines.test-fixture.js +5 -0
- package/dist/commands/send.d.ts +2 -1
- package/dist/commands/send.js +7 -5
- package/dist/commands/sessions-stats.js +37 -5
- package/dist/commands/sessions.js +39 -5
- package/dist/commands/ssh.js +12 -1
- package/dist/commands/versions.js +12 -4
- package/dist/commands/view.js +7 -2
- package/dist/lib/auto-pull-worker.js +7 -2
- package/dist/lib/cloud/rush.d.ts +7 -0
- package/dist/lib/cloud/rush.js +29 -1
- package/dist/lib/daemon/daemon.d.ts +22 -0
- package/dist/lib/daemon/daemon.js +39 -0
- package/dist/lib/daemon/session-index-service.js +9 -1
- package/dist/lib/daemon-ticks.d.ts +15 -0
- package/dist/lib/daemon-ticks.js +26 -0
- package/dist/lib/device-config.d.ts +5 -1
- package/dist/lib/device-config.js +2 -2
- package/dist/lib/devices/health.js +5 -1
- package/dist/lib/devices/pool.d.ts +25 -2
- package/dist/lib/devices/pool.js +32 -2
- package/dist/lib/devices/stats-cache.d.ts +0 -6
- package/dist/lib/devices/stats-cache.js +2 -9
- package/dist/lib/doctor-diff.d.ts +14 -0
- package/dist/lib/doctor-diff.js +43 -2
- package/dist/lib/git.d.ts +38 -0
- package/dist/lib/git.js +58 -0
- package/dist/lib/hosts/ready.d.ts +8 -0
- package/dist/lib/hosts/ready.js +13 -2
- package/dist/lib/installations/versions.d.ts +17 -0
- package/dist/lib/installations/versions.js +53 -2
- package/dist/lib/monitors/config.d.ts +71 -3
- package/dist/lib/monitors/config.js +100 -12
- package/dist/lib/monitors/pid-watch.d.ts +35 -0
- package/dist/lib/monitors/pid-watch.js +45 -0
- package/dist/lib/monitors/remote.d.ts +18 -0
- package/dist/lib/monitors/remote.js +11 -0
- package/dist/lib/permissions.js +7 -2
- package/dist/lib/plugins/plugins.d.ts +17 -3
- package/dist/lib/plugins/plugins.js +84 -9
- package/dist/lib/pty-server.d.ts +14 -0
- package/dist/lib/pty-server.js +49 -5
- package/dist/lib/secrets/drivers/rush.js +5 -0
- package/dist/lib/self-update.d.ts +42 -0
- package/dist/lib/self-update.js +88 -0
- package/dist/lib/session/cloud.js +5 -0
- package/dist/lib/session/db.d.ts +32 -6
- package/dist/lib/session/db.js +128 -12
- package/dist/lib/smart-launch.d.ts +6 -0
- package/dist/lib/smart-launch.js +5 -2
- package/dist/lib/staleness/writers/plugins.js +5 -2
- package/dist/lib/staleness/writers/subagents.js +13 -3
- package/dist/lib/state.d.ts +7 -4
- package/dist/lib/state.js +7 -4
- package/dist/lib/subagents.js +8 -2
- package/dist/lib/teams/scheduler.d.ts +10 -0
- package/dist/lib/teams/scheduler.js +8 -0
- package/dist/lib/traces/sync.d.ts +113 -6
- package/dist/lib/traces/sync.js +193 -19
- package/dist/lib/view-types.d.ts +12 -0
- package/package.json +2 -2
|
@@ -14,10 +14,10 @@ import * as yaml from 'yaml';
|
|
|
14
14
|
import { isDaemonRunning, signalDaemonReload, startDaemon, } from '../lib/daemon/daemon.js';
|
|
15
15
|
import { findDuplicateMonitor, monitorFingerprint } from '../lib/monitors/fingerprint.js';
|
|
16
16
|
import { gatherFleetMonitors, NO_MONITOR_FANOUT_ENV } from '../lib/monitors/remote.js';
|
|
17
|
-
import { listMonitors, readMonitor, writeMonitor, deleteMonitor, setMonitorEnabled, getMonitorPath, validateMonitor, monitorRunsOnThisDevice, parseInterval, } from '../lib/monitors/config.js';
|
|
17
|
+
import { listMonitors, readMonitor, writeMonitor, deleteMonitor, setMonitorEnabled, getMonitorPath, validateMonitor, monitorRunsOnThisDevice, requiresSingleOwner, monitorSharedInputOwner, parseInterval, } from '../lib/monitors/config.js';
|
|
18
18
|
import { formatRelativeTime } from '../lib/session/relative-time.js';
|
|
19
19
|
import { evaluateMonitorOnce, POLL_SOURCE_TYPES } from '../lib/monitors/engine.js';
|
|
20
|
-
import { listFires, readState, readLiveness, resolveFireOutcome } from '../lib/monitors/state.js';
|
|
20
|
+
import { listFires, readState, readLiveness, resolveFireOutcome, getMonitorHistoryDir } from '../lib/monitors/state.js';
|
|
21
21
|
import { listRuns, getLatestRun, getRunDir } from '../lib/scheduling/routines.js';
|
|
22
22
|
import { getMonitorsDir } from '../lib/state.js';
|
|
23
23
|
import { IS_WINDOWS } from '../lib/platform/index.js';
|
|
@@ -26,6 +26,8 @@ import { machineId, normalizeHost } from '../lib/machine-id.js';
|
|
|
26
26
|
import { loadDevices } from '../lib/devices/registry.js';
|
|
27
27
|
import { assertDaemonEnabled } from '../lib/device-config.js';
|
|
28
28
|
import { setHelpSections } from '../lib/help.js';
|
|
29
|
+
import { isPidAlive } from '../lib/session/active.js';
|
|
30
|
+
import { PID_WATCH_EXITED_TOKEN, pidLivenessCommand } from '../lib/monitors/pid-watch.js';
|
|
29
31
|
import { isInteractiveTerminal, requireInteractiveSelection } from './utils.js';
|
|
30
32
|
function stdoutJson(payload) {
|
|
31
33
|
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
@@ -94,8 +96,79 @@ function ownerLabel(monitor) {
|
|
|
94
96
|
return monitor.device;
|
|
95
97
|
if (monitor.devices && monitor.devices.length > 0)
|
|
96
98
|
return monitor.devices.join(',');
|
|
99
|
+
// An unpinned shared-input monitor (a system built-in by default) is NOT
|
|
100
|
+
// fleet-wide — it fires only on the single resolved owner (SING-9). Show that
|
|
101
|
+
// owner, not a misleading "all"; surface the unresolved case loudly.
|
|
102
|
+
if (requiresSingleOwner(monitor)) {
|
|
103
|
+
const owner = monitorSharedInputOwner();
|
|
104
|
+
return owner ? `${owner} (owner)` : 'unowned — set interactive.host';
|
|
105
|
+
}
|
|
97
106
|
return 'all';
|
|
98
107
|
}
|
|
108
|
+
/** The `(built-in)` tag for a system-layer monitor, mirroring routines' list. */
|
|
109
|
+
function builtinTag(monitor) {
|
|
110
|
+
return monitor.scope === 'system' ? chalk.gray(' (built-in)') : '';
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* A one-line liveness note for a monitor living on a PEER, reconstructed from the
|
|
114
|
+
* `--json` fields that box reported (it computes the colorized local label, which
|
|
115
|
+
* doesn't cross the wire). Deliberately terse — the owning box's `monitors view`
|
|
116
|
+
* has the full detail.
|
|
117
|
+
*/
|
|
118
|
+
function remoteLivenessNote(d) {
|
|
119
|
+
if (!d)
|
|
120
|
+
return chalk.gray('—');
|
|
121
|
+
if (d.enabled === false)
|
|
122
|
+
return chalk.gray('paused');
|
|
123
|
+
if (d.stalled)
|
|
124
|
+
return chalk.red('STALLED');
|
|
125
|
+
if (d.lastActionFailed)
|
|
126
|
+
return chalk.red('ACTION FAILED');
|
|
127
|
+
if (d.lastFiredAt)
|
|
128
|
+
return chalk.green(`fired ${formatRelativeTime(d.lastFiredAt)}`);
|
|
129
|
+
if (typeof d.checkCount === 'number' && d.checkCount > 0)
|
|
130
|
+
return chalk.gray(`checked ${d.checkCount}x`);
|
|
131
|
+
if (d.lastCheckedAt)
|
|
132
|
+
return chalk.gray(`checked ${formatRelativeTime(d.lastCheckedAt)}`);
|
|
133
|
+
return chalk.yellow('never polled');
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Print a stderr note when the fleet fan-out couldn't consult every box, so a
|
|
137
|
+
* partial listing never silently reads as "these are all the monitors there are".
|
|
138
|
+
*/
|
|
139
|
+
function fleetReachNote(fleet) {
|
|
140
|
+
if (fleet.discoveryFailed) {
|
|
141
|
+
stderrLine(chalk.yellow(' Note: could not reach the device registry — the fleet was not checked, only this device is shown.'));
|
|
142
|
+
}
|
|
143
|
+
else if (fleet.skipped.length > 0) {
|
|
144
|
+
stderrLine(chalk.yellow(` Note: could not reach ${fleet.skipped.length} device(s): ${fleet.skipped.join(', ')} — their monitors are not shown.`));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** A remote monitor rendered into the same `--json` row shape as a local one. */
|
|
148
|
+
function remoteMonitorJsonRow(r) {
|
|
149
|
+
const d = r.display;
|
|
150
|
+
return {
|
|
151
|
+
machine: r.machine,
|
|
152
|
+
name: r.monitor.name,
|
|
153
|
+
enabled: d?.enabled ?? null,
|
|
154
|
+
source: r.monitor.source,
|
|
155
|
+
condition: r.monitor.condition,
|
|
156
|
+
action: r.monitor.action,
|
|
157
|
+
owner: d?.owner ?? null,
|
|
158
|
+
scope: d?.scope ?? null,
|
|
159
|
+
builtin: d?.scope === 'system',
|
|
160
|
+
runsHere: false,
|
|
161
|
+
lastSeenAt: null,
|
|
162
|
+
lastFiredAt: d?.lastFiredAt ?? null,
|
|
163
|
+
lastCheckedAt: d?.lastCheckedAt ?? null,
|
|
164
|
+
checkCount: d?.checkCount ?? 0,
|
|
165
|
+
lastError: null,
|
|
166
|
+
consecutiveErrors: 0,
|
|
167
|
+
stalled: d?.stalled ?? false,
|
|
168
|
+
lastActionStatus: null,
|
|
169
|
+
lastActionFailed: d?.lastActionFailed ?? false,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
99
172
|
/** A monitor's evaluation cadence in ms (falls back to the engine default). */
|
|
100
173
|
function monitorIntervalMs(monitor) {
|
|
101
174
|
if (monitor.source.interval)
|
|
@@ -230,11 +303,28 @@ async function validateDevice(name) {
|
|
|
230
303
|
}
|
|
231
304
|
return normalized;
|
|
232
305
|
}
|
|
233
|
-
/** Parse the source flags into a MonitorSource, exiting on missing/ambiguous input. */
|
|
234
|
-
function buildSource(options) {
|
|
306
|
+
/** Parse the source flags into a MonitorSource, exiting on missing/ambiguous input. `name` seeds the --watch-pid running-seen marker. */
|
|
307
|
+
function buildSource(options, name) {
|
|
235
308
|
const chosen = [];
|
|
236
309
|
if (options.watch)
|
|
237
310
|
chosen.push({ type: 'command', source: { type: 'command', command: options.watch } });
|
|
311
|
+
if (options.watchPid) {
|
|
312
|
+
const pid = Number(options.watchPid);
|
|
313
|
+
if (!Number.isInteger(pid) || pid < 1) {
|
|
314
|
+
stderrLine(chalk.red(`--watch-pid must be a positive integer pid, got '${options.watchPid}'`));
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
// Fail loud rather than silently arming a watcher on a corpse (PHNX-3023):
|
|
318
|
+
// a pid that is already gone at arm time can never transition to "exited",
|
|
319
|
+
// so the monitor would sit enabled and never fire.
|
|
320
|
+
if (!isPidAlive(pid) && !options.force) {
|
|
321
|
+
stderrLine(chalk.red(`Process ${pid} is not running — there is nothing to watch.`));
|
|
322
|
+
stderrLine(chalk.gray('Pass --force to arm it anyway (e.g. the pid is about to be spawned by a concurrent step).'));
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
const seenRunningMarkerPath = path.join(getMonitorHistoryDir(name), 'pid-watch-seen-running');
|
|
326
|
+
chosen.push({ type: 'command', source: { type: 'command', command: pidLivenessCommand(pid, seenRunningMarkerPath) } });
|
|
327
|
+
}
|
|
238
328
|
if (options.poll) {
|
|
239
329
|
chosen.push({ type: 'poll', source: { type: 'poll', command: options.poll[0], interval: options.poll[1] } });
|
|
240
330
|
}
|
|
@@ -274,7 +364,7 @@ function buildSource(options) {
|
|
|
274
364
|
chosen.push({ type: 'webhook', source: { type: 'webhook', webhook } });
|
|
275
365
|
}
|
|
276
366
|
if (chosen.length === 0) {
|
|
277
|
-
stderrLine(chalk.red('A source is required: --watch, --poll, --poll-http, --ws, --watch-file, --watch-device, or --on'));
|
|
367
|
+
stderrLine(chalk.red('A source is required: --watch, --watch-pid, --poll, --poll-http, --ws, --watch-file, --watch-device, or --on'));
|
|
278
368
|
process.exit(1);
|
|
279
369
|
}
|
|
280
370
|
if (chosen.length > 1) {
|
|
@@ -283,7 +373,12 @@ function buildSource(options) {
|
|
|
283
373
|
}
|
|
284
374
|
return chosen[0].source;
|
|
285
375
|
}
|
|
286
|
-
/**
|
|
376
|
+
/**
|
|
377
|
+
* Parse the condition flags into a MonitorCondition (default on-change).
|
|
378
|
+
* `--watch-pid` with no explicit mode defaults to firing on exit rather than
|
|
379
|
+
* on-change, since "running"/"exited" is a two-value observation where
|
|
380
|
+
* on-change would fire the instant the daemon takes its first poll.
|
|
381
|
+
*/
|
|
287
382
|
function buildCondition(options) {
|
|
288
383
|
const modes = [];
|
|
289
384
|
if (options.onChange)
|
|
@@ -296,6 +391,9 @@ function buildCondition(options) {
|
|
|
296
391
|
stderrLine(chalk.red('--on-change, --match, and --every are mutually exclusive'));
|
|
297
392
|
process.exit(1);
|
|
298
393
|
}
|
|
394
|
+
if (options.watchPid && modes.length === 0) {
|
|
395
|
+
return { mode: 'match', match: PID_WATCH_EXITED_TOKEN };
|
|
396
|
+
}
|
|
299
397
|
const mode = modes[0] ?? (options.match ? 'match' : 'on-change');
|
|
300
398
|
const condition = { mode };
|
|
301
399
|
if (options.match)
|
|
@@ -458,11 +556,17 @@ export function registerMonitorsCommands(program) {
|
|
|
458
556
|
# A fleet box going loaded → spin up an agent
|
|
459
557
|
agents monitors add box-loaded --watch-device yosemite-s0 --match loaded \\
|
|
460
558
|
--run claude --prompt 'yosemite-s0 is loaded: {event}. Investigate.'
|
|
559
|
+
|
|
560
|
+
# A backgrounded shell that will never exit on its own (a watch loop, gh pr
|
|
561
|
+
# checks --watch, a long sleep) — arm a REAL watcher instead of trusting the
|
|
562
|
+
# harness's own exit hook (which only fires when the process dies):
|
|
563
|
+
agents monitors add pr-checks-1234 --watch-pid 48213 \\
|
|
564
|
+
--run claude --prompt 'PID 48213 exited: {event}. Resume and check the result.'
|
|
461
565
|
`,
|
|
462
566
|
notes: `
|
|
463
567
|
A monitor is a routine whose trigger is a watched SOURCE instead of a clock.
|
|
464
568
|
It has three parts:
|
|
465
|
-
- SOURCE (--watch, --poll, --poll-http, --ws, --watch-file, --watch-device, --on)
|
|
569
|
+
- SOURCE (--watch, --watch-pid, --poll, --poll-http, --ws, --watch-file, --watch-device, --on)
|
|
466
570
|
- CONDITION (--on-change [default], --match <re>, --every; --dedupe-key)
|
|
467
571
|
- ACTION (--run <agent> --prompt, --routine, --notify, --webhook-out)
|
|
468
572
|
--postcondition <cmd> on --run/--routine asserts the effect
|
|
@@ -477,6 +581,11 @@ export function registerMonitorsCommands(program) {
|
|
|
477
581
|
v1 evaluates poll sources (command, poll, poll-http, file, device). Push
|
|
478
582
|
sources (ws, webhook) are accepted but delivered through a receiver wired in
|
|
479
583
|
a follow-up.
|
|
584
|
+
|
|
585
|
+
--watch-pid <pid> refuses to arm (fails loud) when the pid is already dead —
|
|
586
|
+
a "will re-invoke me" watcher pointed at a corpse never fires. It defaults
|
|
587
|
+
the condition to fire on exit, unlike a raw --watch which defaults to
|
|
588
|
+
on-change.
|
|
480
589
|
`,
|
|
481
590
|
});
|
|
482
591
|
// ─── add ────────────────────────────────────────────────────────────────────
|
|
@@ -485,6 +594,7 @@ export function registerMonitorsCommands(program) {
|
|
|
485
594
|
.description('Create a monitor from inline flags or a YAML file. Auto-starts the daemon unless daemon.enabled is false.')
|
|
486
595
|
// SOURCE
|
|
487
596
|
.option('--watch <cmd>', 'Run a shell command; its stdout is the observation')
|
|
597
|
+
.option('--watch-pid <pid>', 'Watch a backgrounded process for exit — a reliable, daemon-polled alternative to a harness exit hook. Fails loud if the pid is already gone. Defaults to firing on exit')
|
|
488
598
|
.option('--poll <cmd...>', 'Re-run a command every interval: --poll "<cmd>" <interval> (e.g. 30s)')
|
|
489
599
|
.option('--poll-http <url...>', 'GET a URL every interval: --poll-http <url> <interval> (e.g. 15m)')
|
|
490
600
|
.option('--on <source:event>', 'Webhook trigger source: github:pull_request or linear:Issue')
|
|
@@ -522,7 +632,7 @@ export function registerMonitorsCommands(program) {
|
|
|
522
632
|
.option('--force', 'Overwrite a same-named monitor, or add one that duplicates an existing watcher')
|
|
523
633
|
.action(async (nameOrPath, options) => {
|
|
524
634
|
// File mode: a single arg pointing at an existing .yml with no source flags.
|
|
525
|
-
const hasSourceFlag = Boolean(options.watch || options.poll || options.pollHttp || options.on || options.ws || options.watchFile || options.watchDevice);
|
|
635
|
+
const hasSourceFlag = Boolean(options.watch || options.watchPid || options.poll || options.pollHttp || options.on || options.ws || options.watchFile || options.watchDevice);
|
|
526
636
|
if (!hasSourceFlag && nameOrPath && /\.ya?ml$/.test(nameOrPath) && fs.existsSync(path.resolve(nameOrPath))) {
|
|
527
637
|
const resolved = path.resolve(nameOrPath);
|
|
528
638
|
let parsed;
|
|
@@ -555,7 +665,7 @@ export function registerMonitorsCommands(program) {
|
|
|
555
665
|
stderrLine(chalk.gray('Usage: agents monitors add <name> --poll "<cmd>" 30s --match fail --run claude --prompt "..."'));
|
|
556
666
|
process.exit(1);
|
|
557
667
|
}
|
|
558
|
-
const source = buildSource(options);
|
|
668
|
+
const source = buildSource(options, nameOrPath);
|
|
559
669
|
// A --watch-device source name must resolve to a registered fleet member —
|
|
560
670
|
// validate it (fail fast with the registered list) so a typo/removed device
|
|
561
671
|
// can't silently watch the local machine (same gate as --device/--devices).
|
|
@@ -628,17 +738,37 @@ export function registerMonitorsCommands(program) {
|
|
|
628
738
|
// ─── list ────────────────────────────────────────────────────────────────────
|
|
629
739
|
monitorsCmd
|
|
630
740
|
.command('list')
|
|
631
|
-
.description('See all monitors: source, condition, action, owner, and
|
|
741
|
+
.description('See all monitors across the fleet: source, condition, action, owner, last fire, and the box each lives on.')
|
|
632
742
|
.option('--json', 'Emit machine-readable JSON')
|
|
633
|
-
.
|
|
743
|
+
.option('--local', 'This device only — skip the fleet fan-out')
|
|
744
|
+
.action(async (options) => {
|
|
745
|
+
const self = machineId();
|
|
634
746
|
const monitors = listMonitors();
|
|
747
|
+
// A peer answering the fan-out (NO_MONITOR_FANOUT_ENV set) reports its own
|
|
748
|
+
// box only, so the parent's gather is a flat union and never recurses. The
|
|
749
|
+
// duplicate guard reads exactly this bare local array, so its shape must not
|
|
750
|
+
// change when the env is set. `--local` is the same opt-out for a human.
|
|
751
|
+
const peerMode = !!process.env[NO_MONITOR_FANOUT_ENV];
|
|
752
|
+
const localOnly = peerMode || options.local === true;
|
|
753
|
+
let fleet = null;
|
|
754
|
+
if (!localOnly) {
|
|
755
|
+
// Same cross-machine fan-out `sessions --active` and the add-time guard
|
|
756
|
+
// use — visibility, not git-sync (PHNX-2506 item 3). Never throws; an
|
|
757
|
+
// unreachable fleet degrades to an empty remote set plus skipped names.
|
|
758
|
+
fleet = await gatherFleetMonitors();
|
|
759
|
+
}
|
|
760
|
+
// Peers may echo this box's own monitors back (a synced mirror, or the box
|
|
761
|
+
// resolving its own name); drop anything on `self` so local rows aren't
|
|
762
|
+
// double-listed.
|
|
763
|
+
const remote = (fleet?.monitors ?? []).filter((r) => normalizeHost(r.machine) !== self);
|
|
635
764
|
if (options.json) {
|
|
636
|
-
const
|
|
765
|
+
const localRows = monitors.map((m) => {
|
|
637
766
|
const state = readState(m.name);
|
|
638
767
|
const liveness = readLiveness(m.name);
|
|
639
768
|
const latestFire = listFires(m.name).at(-1);
|
|
640
769
|
const latestOutcome = latestFire ? resolveFireOutcome(m.name, latestFire) : null;
|
|
641
770
|
return {
|
|
771
|
+
machine: self,
|
|
642
772
|
name: m.name,
|
|
643
773
|
enabled: m.enabled,
|
|
644
774
|
source: m.source,
|
|
@@ -649,6 +779,11 @@ export function registerMonitorsCommands(program) {
|
|
|
649
779
|
// exactly the case it exists for. `source` already ships whole.
|
|
650
780
|
action: m.action,
|
|
651
781
|
owner: ownerLabel(m),
|
|
782
|
+
// `scope`/`builtin` (PHNX-2506 item 2): where the monitor came from,
|
|
783
|
+
// so "is this a shipped built-in?" is answerable without knowing the
|
|
784
|
+
// system mirror exists. Peers read `scope` off this field.
|
|
785
|
+
scope: m.scope ?? 'user',
|
|
786
|
+
builtin: m.scope === 'system',
|
|
652
787
|
runsHere: monitorRunsOnThisDevice(m),
|
|
653
788
|
lastSeenAt: state?.lastSeenAt ?? null,
|
|
654
789
|
lastFiredAt: state?.lastFiredAt ?? null,
|
|
@@ -663,24 +798,48 @@ export function registerMonitorsCommands(program) {
|
|
|
663
798
|
lastActionFailed: latestOutcome ? !latestOutcome.ok : false,
|
|
664
799
|
};
|
|
665
800
|
});
|
|
666
|
-
stdoutJson(
|
|
801
|
+
stdoutJson([...localRows, ...remote.map(remoteMonitorJsonRow)]);
|
|
667
802
|
return;
|
|
668
803
|
}
|
|
669
|
-
if (monitors.length === 0) {
|
|
804
|
+
if (monitors.length === 0 && remote.length === 0) {
|
|
670
805
|
console.log(chalk.gray('No monitors configured'));
|
|
671
806
|
console.log(chalk.gray(' Add one: agents monitors add <name> --poll "<cmd>" 30s --match fail --run claude --prompt "..."'));
|
|
807
|
+
if (fleet && (fleet.discoveryFailed || fleet.skipped.length > 0))
|
|
808
|
+
fleetReachNote(fleet);
|
|
672
809
|
return;
|
|
673
810
|
}
|
|
674
811
|
console.log(chalk.bold('Monitors\n'));
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
812
|
+
// This box first — it has full liveness detail.
|
|
813
|
+
if (monitors.length > 0) {
|
|
814
|
+
if (remote.length > 0)
|
|
815
|
+
console.log(chalk.gray(` ${self} (this device)`));
|
|
816
|
+
for (const m of monitors) {
|
|
817
|
+
const state = readState(m.name);
|
|
818
|
+
const liveness = readLiveness(m.name);
|
|
819
|
+
const enabled = m.enabled ? chalk.green('on') : chalk.gray('off');
|
|
820
|
+
const here = monitorRunsOnThisDevice(m);
|
|
821
|
+
const owner = here ? ownerLabel(m) : chalk.gray(ownerLabel(m));
|
|
822
|
+
console.log(` ${chalk.cyan(m.name.padEnd(22))} ${enabled.padEnd(3)} ${sourceLabel(m.source)}${builtinTag(m)}`);
|
|
823
|
+
console.log(` ${' '.repeat(22)} ${chalk.gray(`[${m.condition.mode}]`)} → ${actionLabel(m.action)} ${chalk.gray(`owner: ${owner}`)} ${livenessLabel(m, state, liveness)}`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
// Then each peer's monitors, grouped by the box they live on.
|
|
827
|
+
const byMachine = new Map();
|
|
828
|
+
for (const r of remote) {
|
|
829
|
+
const key = r.machine;
|
|
830
|
+
(byMachine.get(key) ?? byMachine.set(key, []).get(key)).push(r);
|
|
831
|
+
}
|
|
832
|
+
for (const machine of [...byMachine.keys()].sort()) {
|
|
833
|
+
console.log(chalk.gray(`\n ${machine}`));
|
|
834
|
+
for (const r of byMachine.get(machine)) {
|
|
835
|
+
const m = r.monitor;
|
|
836
|
+
const enabled = r.display?.enabled === false ? chalk.gray('off') : chalk.green('on');
|
|
837
|
+
console.log(` ${chalk.cyan(m.name.padEnd(22))} ${enabled.padEnd(3)} ${sourceLabel(m.source)}${builtinTag({ scope: r.display?.scope })}`);
|
|
838
|
+
console.log(` ${' '.repeat(22)} ${chalk.gray(`[${m.condition.mode}]`)} → ${actionLabel(m.action)} ${chalk.gray(`owner: ${r.display?.owner ?? machine}`)} ${remoteLivenessNote(r.display)}`);
|
|
839
|
+
}
|
|
683
840
|
}
|
|
841
|
+
if (fleet && (fleet.discoveryFailed || fleet.skipped.length > 0))
|
|
842
|
+
fleetReachNote(fleet);
|
|
684
843
|
console.log();
|
|
685
844
|
});
|
|
686
845
|
// ─── view ──────────────────────────────────────────────────────────────────
|
|
@@ -820,7 +979,12 @@ export function registerMonitorsCommands(program) {
|
|
|
820
979
|
monitorPath = safeJoin(dir, `${name}.yml`);
|
|
821
980
|
const builtIn = readMonitor(name);
|
|
822
981
|
if (builtIn) {
|
|
823
|
-
|
|
982
|
+
// Route through writeMonitor, NOT a raw yaml.stringify: readMonitor
|
|
983
|
+
// stamps the derived `scope` (and a built-in's `enabled: true`), and
|
|
984
|
+
// only writeMonitor strips those from the on-disk schema. A raw dump
|
|
985
|
+
// would persist `scope: system` into the user copy — dead, contradictory
|
|
986
|
+
// state that violates the "scope never persists" contract.
|
|
987
|
+
writeMonitor(builtIn);
|
|
824
988
|
console.log(chalk.gray(`Editing a copy of built-in monitor '${name}' in your user dir: ${monitorPath}`));
|
|
825
989
|
}
|
|
826
990
|
else {
|
|
@@ -149,6 +149,11 @@ export function createDaemonHarness(fileSlug) {
|
|
|
149
149
|
// and kills real tmux-wrapped processes every five-minute tick (RUSH-2545).
|
|
150
150
|
AGENTS_HISTORY_DIR: path.join(home, '.agents', '.history'),
|
|
151
151
|
AGENTS_SKIP_MIGRATION: '1',
|
|
152
|
+
// PHNX-2545 test-home tripwire: name the isolated home this daemon must
|
|
153
|
+
// resolve its state dir under. If the HOME override above ever failed to
|
|
154
|
+
// reach the child, runDaemon()'s assertTestDaemonHome() refuses to boot
|
|
155
|
+
// instead of ticking its scheduler against the operator's real host.
|
|
156
|
+
AGENTS_DAEMON_TEST_HOME: home,
|
|
152
157
|
},
|
|
153
158
|
detached: true,
|
|
154
159
|
stdio: 'ignore',
|
package/dist/commands/send.d.ts
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* Compat: positional text still works (`agents send "hi" --channel … --to …`).
|
|
12
12
|
*
|
|
13
13
|
* `agents notify` is the same delivery path with owner defaults
|
|
14
|
-
* (`send --to owner`)
|
|
14
|
+
* (`send --to owner`) and is deprecated — new callers should use
|
|
15
|
+
* `agents feed post`. Not a second stack. Not agent control — use
|
|
15
16
|
* `agents message` / `agents sessions inject` for running agents.
|
|
16
17
|
*
|
|
17
18
|
* Feed / activity are a different plane (record + read); feed.broadcast may
|
package/dist/commands/send.js
CHANGED
|
@@ -47,7 +47,7 @@ async function runSend(positionalText, opts, ownerMode) {
|
|
|
47
47
|
}
|
|
48
48
|
const SHARED_NOTES = `
|
|
49
49
|
Planes (do not mix them up):
|
|
50
|
-
send
|
|
50
|
+
send - DELIVER a message to a recipient (this command)
|
|
51
51
|
feed post - RECORD progress / milestones (optional broadcast may call send)
|
|
52
52
|
activity - READ the activity stream (not a send path)
|
|
53
53
|
message / inject - CONTROL a running agent (mailbox answer or terminal keystroke)
|
|
@@ -97,7 +97,7 @@ export function registerSendCommand(program) {
|
|
|
97
97
|
});
|
|
98
98
|
const notifyCmd = program
|
|
99
99
|
.command('notify [text]')
|
|
100
|
-
.description('Deliver to the owner (alias of send --to owner).
|
|
100
|
+
.description('[DEPRECATED] Deliver to the owner (alias of send --to owner). Use "agents feed post" for new code.')
|
|
101
101
|
.option('--text <text>', 'message body (preferred over positional text)')
|
|
102
102
|
.option('--channel <name>', 'override owner channel')
|
|
103
103
|
.option('--to <target>', 'override owner target (or pass a non-owner dest with --channel)')
|
|
@@ -110,7 +110,7 @@ export function registerSendCommand(program) {
|
|
|
110
110
|
.option('--dry-run', 'resolve + build but do not send');
|
|
111
111
|
setHelpSections(notifyCmd, {
|
|
112
112
|
examples: `
|
|
113
|
-
#
|
|
113
|
+
# Deprecated: prefer "agents feed post --title \"…\" \"…\" --level important"
|
|
114
114
|
agents notify --text "Build finished — PR #1346 is green"
|
|
115
115
|
agents notify "legacy positional still works"
|
|
116
116
|
|
|
@@ -120,13 +120,15 @@ export function registerSendCommand(program) {
|
|
|
120
120
|
agents notify --text "probe" --dry-run --json
|
|
121
121
|
`,
|
|
122
122
|
notes: `
|
|
123
|
-
notify ≡ send --to owner
|
|
124
|
-
|
|
123
|
+
DEPRECATED. notify ≡ send --to owner and still works, but new callers
|
|
124
|
+
should use "agents feed post" (record + optional broadcast) instead.
|
|
125
|
+
Set notify.owner.{channel,to} in agents.yaml once per machine/fleet.
|
|
125
126
|
|
|
126
127
|
${SHARED_NOTES}
|
|
127
128
|
`,
|
|
128
129
|
});
|
|
129
130
|
notifyCmd.action(async (text, opts) => {
|
|
131
|
+
console.error(chalk.yellow('Warning: "agents notify" is deprecated. Use "agents feed post" for progress posts that can also reach the owner.'));
|
|
130
132
|
await runSend(text, opts, true);
|
|
131
133
|
});
|
|
132
134
|
}
|
|
@@ -6,6 +6,19 @@ import { discoverPlugins } from '../lib/plugins/plugins.js';
|
|
|
6
6
|
import { setHelpSections } from '../lib/help.js';
|
|
7
7
|
import { terminalWidth, truncateToWidth, padToWidth, stringWidth } from '../lib/session/width.js';
|
|
8
8
|
const DEFAULT_TOP = 20;
|
|
9
|
+
/**
|
|
10
|
+
* The recorded-set the zero-counts must be read against (PHNX-2301). A resource
|
|
11
|
+
* only records an EXPLICIT invocation from a harness that emits the event:
|
|
12
|
+
* `Skill` tool calls come from Claude + Kimi, slash-commands from Claude only
|
|
13
|
+
* (`SKILL_TOOL_NAME_BY_AGENT` in `lib/session/highlights.ts`, slash-command
|
|
14
|
+
* parsing in `lib/session/parse.ts`). A session under any other harness — or an
|
|
15
|
+
* auto-triggered skill, which emits no event on any harness — contributes
|
|
16
|
+
* nothing, so its resources read as 0 whether or not they were used. Keep these
|
|
17
|
+
* in lockstep with the writer: widening them here without a verified transcript
|
|
18
|
+
* tool-name would make the caveat lie about coverage it does not have.
|
|
19
|
+
*/
|
|
20
|
+
const RECORDING_SKILL_HARNESSES = ['claude', 'kimi'];
|
|
21
|
+
const RECORDING_COMMAND_HARNESSES = ['claude'];
|
|
9
22
|
/**
|
|
10
23
|
* The both-ends "dead weight" set: installed resources whose identity
|
|
11
24
|
* (kind + name — name already embeds `plugin:short`, so it matches the stored
|
|
@@ -128,7 +141,7 @@ async function statsAction(cmd) {
|
|
|
128
141
|
const totalInvocations = allInvoked.reduce((s, r) => s + r.invocations, 0);
|
|
129
142
|
if (opts.json) {
|
|
130
143
|
process.stdout.write(JSON.stringify({
|
|
131
|
-
schemaVersion:
|
|
144
|
+
schemaVersion: 2,
|
|
132
145
|
kind: 'sessions-stats',
|
|
133
146
|
generatedAt: new Date().toISOString(),
|
|
134
147
|
filters: {
|
|
@@ -142,9 +155,21 @@ async function statsAction(cmd) {
|
|
|
142
155
|
signal: {
|
|
143
156
|
explicitOnly: true,
|
|
144
157
|
note: 'Explicit invocations only (slash commands + Skill tool calls). Auto-triggered skills emit no event and read as 0. Skill invocations are recorded for Claude and Kimi; slash-commands for Claude only.',
|
|
158
|
+
// Structured recorded-set so a machine consumer can tell a zero from a
|
|
159
|
+
// non-recording harness apart from a genuine non-use (PHNX-2301): a
|
|
160
|
+
// zeroInvoked resource may simply have been auto-triggered, or only
|
|
161
|
+
// used under a harness not in these sets.
|
|
162
|
+
recording: {
|
|
163
|
+
skills: RECORDING_SKILL_HARNESSES,
|
|
164
|
+
commands: RECORDING_COMMAND_HARNESSES,
|
|
165
|
+
},
|
|
145
166
|
},
|
|
146
167
|
coverage: {
|
|
168
|
+
// sessionsWithUsage/sessionsIndexed kept for the SES-IF-4b envelope
|
|
169
|
+
// contract; sessionsScanned is the honest backfill-coverage numerator
|
|
170
|
+
// (ledger rows), distinct from the absolute sessionsWithUsage count.
|
|
147
171
|
sessionsWithUsage: coverage.covered,
|
|
172
|
+
sessionsScanned: coverage.scanned,
|
|
148
173
|
sessionsIndexed: coverage.total,
|
|
149
174
|
},
|
|
150
175
|
totals: {
|
|
@@ -210,8 +235,12 @@ function renderHuman(args) {
|
|
|
210
235
|
out.push(chalk.bold('Resource usage') +
|
|
211
236
|
chalk.gray(` · ${allInvokedCount} invoked · ${totalInvocations} invocation${totalInvocations !== 1 ? 's' : ''}` +
|
|
212
237
|
(scopeBits.length ? ` · ${scopeBits.join(' · ')}` : '')));
|
|
213
|
-
|
|
214
|
-
|
|
238
|
+
// Scan coverage (ledger) is the honest "has the backfill folded in history?"
|
|
239
|
+
// signal and drives the hint; sessionsWithUsage is an ABSOLUTE count of sessions
|
|
240
|
+
// carrying ≥1 explicit invocation, which stays small at full scan coverage
|
|
241
|
+
// because most sessions invoke nothing (PHNX-2301).
|
|
242
|
+
out.push(chalk.gray(` coverage: ${coverage.scanned}/${coverage.total} sessions scanned · ${coverage.covered} carry an explicit invocation`) +
|
|
243
|
+
(coverage.total > 0 && coverage.scanned / coverage.total < 0.5
|
|
215
244
|
? chalk.yellow(' — run `agents sessions backfill resources` to fold in history')
|
|
216
245
|
: ''));
|
|
217
246
|
out.push(chalk.gray(' signal: explicit invocations only (slash commands + Skill tool); auto-triggered skills read as 0; skills from Claude+Kimi, slash-commands Claude-only'));
|
|
@@ -228,8 +257,11 @@ function renderHuman(args) {
|
|
|
228
257
|
}
|
|
229
258
|
out.push('');
|
|
230
259
|
}
|
|
231
|
-
// Zero-invoked section (the dead weight).
|
|
232
|
-
|
|
260
|
+
// Zero-invoked section (the dead weight). "Never invoked" is scoped to the
|
|
261
|
+
// recorded set: never EXPLICITLY invoked under a recording harness (skills
|
|
262
|
+
// Claude+Kimi, commands Claude-only) — it may still have been auto-triggered or
|
|
263
|
+
// used under another harness, which is why this is dead-weight EVIDENCE, not proof.
|
|
264
|
+
out.push(chalk.bold('Installed but never explicitly invoked') + chalk.gray(` (${zeroInvoked.length})`));
|
|
233
265
|
if (zeroInvoked.length === 0) {
|
|
234
266
|
out.push(chalk.gray(' (every installed resource has at least one explicit invocation in this window)'));
|
|
235
267
|
}
|
|
@@ -2316,9 +2316,16 @@ limitSource) {
|
|
|
2316
2316
|
return;
|
|
2317
2317
|
}
|
|
2318
2318
|
// --device / --devices values are merged into options.host for internal routing. A bare `all` / `fleet` sentinel means
|
|
2319
|
-
// "search every peer" — which is already the default
|
|
2320
|
-
//
|
|
2321
|
-
|
|
2319
|
+
// "search every peer" — which is already the default for `--active` and the
|
|
2320
|
+
// interactive listing — so it resolves to no explicit host set rather than
|
|
2321
|
+
// erroring on a device literally named "all".
|
|
2322
|
+
const rawDeviceTargets = [...(options.device ?? []), ...(options.devices ?? [])];
|
|
2323
|
+
// But the HISTORICAL `--json` listing does NOT fan out by default (it stays a
|
|
2324
|
+
// deterministic local slice for scripts), so the sentinel would otherwise be
|
|
2325
|
+
// silently dropped and the fleet never reached (PHNX-2673). Remember it so that
|
|
2326
|
+
// path can sweep every peer, exactly as the interactive listing already does.
|
|
2327
|
+
const fleetWide = rawDeviceTargets.some(t => t.toLowerCase() === 'all' || t.toLowerCase() === 'fleet');
|
|
2328
|
+
const deviceTargets = rawDeviceTargets
|
|
2322
2329
|
.filter(t => t.toLowerCase() !== 'all' && t.toLowerCase() !== 'fleet');
|
|
2323
2330
|
if (deviceTargets.length > 0) {
|
|
2324
2331
|
options.host = [...(options.host ?? []), ...deviceTargets];
|
|
@@ -2775,16 +2782,43 @@ limitSource) {
|
|
|
2775
2782
|
// each peer) would fall to FTS content search and return every transcript
|
|
2776
2783
|
// that merely MENTIONS the id, defeating exact remote resolution. A genuine
|
|
2777
2784
|
// search phrase keeps the ranked metadata+content path.
|
|
2778
|
-
|
|
2785
|
+
let filtered = searchQuery
|
|
2779
2786
|
? resolveSessionQuery(sessions, searchQuery, {
|
|
2780
2787
|
scope: { agent: options.agent, project: options.project, routine: options.routine },
|
|
2781
2788
|
}).matches
|
|
2782
2789
|
: sessions;
|
|
2790
|
+
// `--device all`/`--fleet` on a historical `--json` query fans out to every
|
|
2791
|
+
// peer and merges their rows in — the SAME SSH sweep the interactive listing
|
|
2792
|
+
// below uses, and the whole-fleet twin of the explicit `--device <host>
|
|
2793
|
+
// --json` path (`runRemoteSessionsJson`). Without this the documented
|
|
2794
|
+
// "search the whole fleet" flag returned local rows only (PHNX-2673). Guarded
|
|
2795
|
+
// to the sentinel so a bare `--json` stays a deterministic local slice for
|
|
2796
|
+
// scripts, and skipped under `--local`/the peer recursion guard. Dead peers
|
|
2797
|
+
// are skipped by the fan-out, never fatal — enrichment, not a dependency.
|
|
2798
|
+
const forceLocalJson = options.local === true || process.env[NO_FANOUT_ENV] === '1';
|
|
2799
|
+
if (fleetWide && !forceLocalJson) {
|
|
2800
|
+
const forwarded = ensureWholeIndex(buildForwardedArgs(process.argv, new Set(options.host ?? [])));
|
|
2801
|
+
if (!forwarded.includes('--json'))
|
|
2802
|
+
forwarded.push('--json');
|
|
2803
|
+
const fanSpinner = isInteractiveTerminal() ? ora('Reaching other machines...').start() : null;
|
|
2804
|
+
try {
|
|
2805
|
+
const { sessions: remoteSessions } = await gatherRemoteList(forwarded, undefined);
|
|
2806
|
+
if (remoteSessions.length > 0) {
|
|
2807
|
+
filtered = mergeLocalFirst([...filtered, ...remoteSessions], machineId());
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
catch {
|
|
2811
|
+
// fan-out is an enrichment, never a hard dependency
|
|
2812
|
+
}
|
|
2813
|
+
finally {
|
|
2814
|
+
fanSpinner?.stop();
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2783
2817
|
// JSON is the canonical picker contract: enrich the durable rows once
|
|
2784
2818
|
// from the shared live cache so every consumer gets lifecycle/recovery
|
|
2785
2819
|
// metadata without performing its own join or transcript scan.
|
|
2786
2820
|
const live = await gatherActiveSessions({
|
|
2787
|
-
local:
|
|
2821
|
+
local: forceLocalJson,
|
|
2788
2822
|
hosts: options.host,
|
|
2789
2823
|
});
|
|
2790
2824
|
process.stdout.write(serializeSessionsJson(serializeSessionPickerRows(filtered, live.sessions)));
|
package/dist/commands/ssh.js
CHANGED
|
@@ -130,7 +130,11 @@ function pctCell(v, width) {
|
|
|
130
130
|
* header — which defeats the scannability this column exists for. */
|
|
131
131
|
const SPEC_WIDTH_MIN = 12;
|
|
132
132
|
/** The static hardware as one compact cell — `12c 64G 1T`: cores, total RAM,
|
|
133
|
-
* total root disk via fmtBytes. `—`
|
|
133
|
+
* total root disk via fmtBytes. `—` covers two cases: no probe has ever seen
|
|
134
|
+
* the box, or the probe answered but yielded no usable core count (`parseNcpu`
|
|
135
|
+
* finding none, or the Windows `ncpu` group failing the finite-and-positive
|
|
136
|
+
* check) — the cell is keyed on `ncpu` because a spec string without it would
|
|
137
|
+
* read as a machine with zero cores.
|
|
134
138
|
*
|
|
135
139
|
* Deliberately NOT gated on `reachable`: hardware does not change while a
|
|
136
140
|
* machine is down, so an offline device keeps rendering the spec from its last
|
|
@@ -2298,6 +2302,13 @@ box; \`agents doctor\` names it.
|
|
|
2298
2302
|
|
|
2299
2303
|
A box whose probe cannot answer (no POSIX shell, e.g. Windows) is reported
|
|
2300
2304
|
\`unverified\` rather than counted as a success.
|
|
2305
|
+
|
|
2306
|
+
The upgrade OWNS its global bin links: after installing, it verifies that
|
|
2307
|
+
\`<prefix>/bin/{agents,ag,browser,computer}\` resolve to the freshly-installed
|
|
2308
|
+
copy and RESTORES any the package manager dropped — the state that once left a
|
|
2309
|
+
box upgraded in place but with every \`agents\` invocation "command not found"
|
|
2310
|
+
(PHNX-2768). A link it cannot make resolve fails the upgrade loud, so that box
|
|
2311
|
+
is reported \`failed\` (exit non-zero), never a stranded \`ok\`.
|
|
2301
2312
|
`)
|
|
2302
2313
|
.action(async (version) => {
|
|
2303
2314
|
let cmd;
|
|
@@ -11,7 +11,7 @@ import { installVersion, removeVersion, listInstalledVersions, isVersionInstalle
|
|
|
11
11
|
import { carryForwardSettings } from '../lib/settings-manifest.js';
|
|
12
12
|
import { createShim, createVersionedAlias, supportsIsolatedInstall, isIsolationProtected, CONFIG_ENV_ISOLATED_AGENTS, removeShim, shimExists, getShimsDir, getShimPath, getPathShadowingExecutable, isShimsInPath, getPathSetupInstructions, addShimsToPath, switchConfigSymlink, switchHomeFileSymlinks, } from '../lib/installations/shims.js';
|
|
13
13
|
import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
|
|
14
|
-
import {
|
|
14
|
+
import { tryAutoPullSystemRepo } from '../lib/git.js';
|
|
15
15
|
import { getAgentsDir, getTrashVersionsDir } from '../lib/state.js';
|
|
16
16
|
import { setHelpSections } from '../lib/help.js';
|
|
17
17
|
import { updateSessionFilePaths } from '../lib/session/db.js';
|
|
@@ -630,10 +630,18 @@ export function registerVersionsCommands(program) {
|
|
|
630
630
|
useCmd.action(async (agentArg, versionArg, options) => {
|
|
631
631
|
try {
|
|
632
632
|
const skipPrompts = options.yes || !isInteractiveTerminal();
|
|
633
|
-
// Auto-pull ~/.agents/.system if it's a git repo
|
|
633
|
+
// Auto-pull ~/.agents/.system if it's a git repo tracking the EXPECTED
|
|
634
|
+
// system remote (silent on success). The system repo ships hooks that run
|
|
635
|
+
// as shell on tool events, so pulling from a repointed origin would be
|
|
636
|
+
// remote code execution — tryAutoPullSystemRepo refuses that (PHNX-2957).
|
|
634
637
|
const agentsDir = getAgentsDir();
|
|
635
|
-
const pullResult = await
|
|
636
|
-
if (pullResult.
|
|
638
|
+
const pullResult = await tryAutoPullSystemRepo(agentsDir);
|
|
639
|
+
if (pullResult.refused) {
|
|
640
|
+
console.error(chalk.red(`Refusing to auto-sync ~/.agents/.system: its origin (${pullResult.actualRemote}) is not the expected system repo.`));
|
|
641
|
+
console.error(chalk.gray('The system repo ships hooks that run on tool events; a fast-forward from an unexpected origin is not applied. ' +
|
|
642
|
+
'Re-point it (git -C ~/.agents/.system remote set-url origin <expected>) or set AGENTS_SYSTEM_REPO, then re-run `agents setup --force`.'));
|
|
643
|
+
}
|
|
644
|
+
else if (pullResult.pulled) {
|
|
637
645
|
console.log(chalk.gray('Synced ~/.agents/.system from remote'));
|
|
638
646
|
}
|
|
639
647
|
// Support both "claude 2.0.65" and "claude@2.0.65" formats
|