@phnx-labs/agents-cli 1.20.77 → 1.20.82
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 +589 -0
- package/README.md +27 -25
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +9 -0
- package/dist/commands/activity.js +153 -24
- package/dist/commands/apply.js +11 -4
- package/dist/commands/cli.js +1 -1
- package/dist/commands/commands.d.ts +1 -1
- package/dist/commands/commands.js +7 -6
- package/dist/commands/defaults.js +3 -11
- package/dist/commands/doctor.d.ts +74 -0
- package/dist/commands/doctor.js +718 -40
- package/dist/commands/events.js +1 -1
- package/dist/commands/exec.js +53 -8
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +87 -11
- package/dist/commands/harness.js +2 -2
- package/dist/commands/hooks.js +1 -1
- package/dist/commands/import.js +8 -4
- package/dist/commands/logs.js +6 -0
- package/dist/commands/models.js +1 -1
- package/dist/commands/packages.js +6 -6
- package/dist/commands/permissions.js +1 -1
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/pull.d.ts +1 -1
- package/dist/commands/pull.js +4 -4
- package/dist/commands/repo.d.ts +78 -0
- package/dist/commands/repo.js +195 -11
- package/dist/commands/routines.js +16 -4
- package/dist/commands/rules.js +13 -12
- package/dist/commands/secrets.d.ts +3 -1
- package/dist/commands/secrets.js +108 -34
- package/dist/commands/sessions-browser.d.ts +45 -3
- package/dist/commands/sessions-browser.js +118 -12
- package/dist/commands/sessions-export.d.ts +3 -0
- package/dist/commands/sessions-export.js +13 -10
- package/dist/commands/sessions-inject.d.ts +7 -4
- package/dist/commands/sessions-inject.js +18 -18
- package/dist/commands/sessions-picker.d.ts +15 -1
- package/dist/commands/sessions-picker.js +90 -7
- package/dist/commands/sessions-resume.d.ts +16 -0
- package/dist/commands/sessions-resume.js +14 -13
- package/dist/commands/sessions.d.ts +126 -1
- package/dist/commands/sessions.js +365 -42
- package/dist/commands/set.d.ts +15 -0
- package/dist/commands/set.js +79 -0
- package/dist/commands/setup-fleet.d.ts +21 -0
- package/dist/commands/setup-fleet.js +201 -0
- package/dist/commands/setup-secrets.d.ts +20 -0
- package/dist/commands/setup-secrets.js +223 -0
- package/dist/commands/setup.js +17 -3
- package/dist/commands/skills.js +1 -1
- package/dist/commands/ssh.js +9 -1
- package/dist/commands/sync.js +7 -2
- package/dist/commands/teams-picker.js +0 -1
- package/dist/commands/teams.d.ts +33 -1
- package/dist/commands/teams.js +208 -44
- package/dist/commands/versions.js +10 -5
- package/dist/commands/view.d.ts +1 -0
- package/dist/commands/view.js +11 -0
- package/dist/index.js +8 -13
- package/dist/lib/acp/harnesses.js +0 -7
- package/dist/lib/activity.d.ts +156 -0
- package/dist/lib/activity.js +282 -0
- package/dist/lib/agents.d.ts +9 -23
- package/dist/lib/agents.js +72 -100
- package/dist/lib/auto-pull.d.ts +16 -8
- package/dist/lib/auto-pull.js +23 -28
- package/dist/lib/browser/service.js +3 -0
- package/dist/lib/browser/types.d.ts +7 -0
- package/dist/lib/capabilities.js +6 -2
- package/dist/lib/crabbox/lease.js +2 -2
- package/dist/lib/crabbox/setup-copy.d.ts +4 -4
- package/dist/lib/crabbox/setup-copy.js +4 -4
- package/dist/lib/daemon.d.ts +26 -40
- package/dist/lib/daemon.js +63 -123
- 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 +1 -1
- package/dist/lib/devices/fleet.js +1 -1
- package/dist/lib/devices/health-report.d.ts +10 -2
- package/dist/lib/devices/health-report.js +32 -1
- package/dist/lib/doctor-diff.d.ts +20 -0
- package/dist/lib/doctor-diff.js +6 -1
- package/dist/lib/drift.d.ts +31 -11
- package/dist/lib/drift.js +58 -7
- package/dist/lib/events.d.ts +8 -1
- package/dist/lib/events.js +10 -1
- package/dist/lib/exec.js +35 -8
- package/dist/lib/feed-post.d.ts +28 -1
- package/dist/lib/feed-post.js +110 -2
- package/dist/lib/fleet/apply.js +8 -1
- package/dist/lib/fleet/auth-sync.d.ts +18 -1
- package/dist/lib/fleet/auth-sync.js +25 -11
- package/dist/lib/fleet/remote-login.js +5 -0
- package/dist/lib/git.d.ts +27 -14
- package/dist/lib/git.js +138 -43
- package/dist/lib/hooks.d.ts +42 -0
- package/dist/lib/hooks.js +137 -93
- package/dist/lib/hosts/dispatch.d.ts +28 -1
- package/dist/lib/hosts/dispatch.js +33 -5
- package/dist/lib/hosts/option.js +2 -2
- package/dist/lib/hosts/passthrough.d.ts +22 -1
- package/dist/lib/hosts/passthrough.js +238 -13
- package/dist/lib/hosts/remote-cmd.js +2 -0
- package/dist/lib/hosts/run-target.d.ts +2 -0
- package/dist/lib/hosts/run-target.js +1 -0
- package/dist/lib/hq/floor.d.ts +1 -1
- package/dist/lib/hq/floor.js +6 -1
- package/dist/lib/mcp.js +3 -71
- 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/notify-desktop.d.ts +15 -0
- package/dist/lib/menubar/notify-desktop.js +41 -5
- package/dist/lib/merged-resources.d.ts +11 -0
- package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
- package/dist/lib/permissions.d.ts +0 -32
- package/dist/lib/permissions.js +4 -157
- package/dist/lib/picker.js +1 -1
- package/dist/lib/platform/process.d.ts +16 -5
- package/dist/lib/platform/process.js +54 -0
- package/dist/lib/plugins.d.ts +0 -8
- package/dist/lib/plugins.js +4 -110
- package/dist/lib/project-resources.js +4 -1
- package/dist/lib/refresh.d.ts +5 -3
- package/dist/lib/refresh.js +7 -5
- package/dist/lib/resources/commands.js +4 -1
- package/dist/lib/resources/mcp.js +0 -4
- package/dist/lib/resources/permissions.d.ts +1 -1
- package/dist/lib/resources/permissions.js +1 -5
- package/dist/lib/resources/rules.js +4 -1
- package/dist/lib/resources/skills.js +4 -1
- package/dist/lib/resources/subagents.js +4 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/routines.d.ts +20 -0
- package/dist/lib/routines.js +6 -0
- package/dist/lib/run-defaults.d.ts +1 -0
- package/dist/lib/run-defaults.js +9 -0
- package/dist/lib/runner.d.ts +0 -12
- package/dist/lib/runner.js +71 -29
- package/dist/lib/sandbox.js +0 -16
- 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 +18 -0
- package/dist/lib/secrets/agent.js +131 -17
- package/dist/lib/secrets/audit.d.ts +46 -0
- package/dist/lib/secrets/audit.js +56 -0
- package/dist/lib/secrets/bundles.d.ts +12 -9
- package/dist/lib/secrets/bundles.js +47 -31
- package/dist/lib/secrets/index.d.ts +2 -1
- package/dist/lib/secrets/index.js +8 -2
- package/dist/lib/secrets/remote.d.ts +75 -2
- package/dist/lib/secrets/remote.js +139 -5
- 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 +10 -0
- package/dist/lib/secrets/session-store.js +64 -11
- package/dist/lib/secrets/sync.js +3 -3
- package/dist/lib/session/active.d.ts +117 -26
- package/dist/lib/session/active.js +199 -53
- package/dist/lib/session/actor-sidecar.d.ts +23 -0
- package/dist/lib/session/actor-sidecar.js +101 -0
- package/dist/lib/session/db.d.ts +41 -1
- package/dist/lib/session/db.js +173 -8
- package/dist/lib/session/digest.js +126 -21
- package/dist/lib/session/discover.d.ts +19 -2
- package/dist/lib/session/discover.js +32 -11
- package/dist/lib/session/hook-sessions.d.ts +9 -0
- package/dist/lib/session/hook-sessions.js +57 -8
- package/dist/lib/session/origin-machine.d.ts +18 -0
- package/dist/lib/session/origin-machine.js +34 -0
- package/dist/lib/session/pid-registry.d.ts +13 -0
- package/dist/lib/session/render.d.ts +2 -0
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/state.d.ts +1 -0
- package/dist/lib/session/state.js +1 -1
- package/dist/lib/session/sync/config.js +2 -2
- package/dist/lib/session/types.d.ts +16 -0
- package/dist/lib/smart-launch.d.ts +86 -0
- package/dist/lib/smart-launch.js +172 -0
- package/dist/lib/staleness/detectors/commands.js +2 -2
- package/dist/lib/staleness/detectors/permissions.js +1 -45
- package/dist/lib/staleness/writers/commands.js +4 -5
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +1 -2
- package/dist/lib/startup/command-registry.js +2 -4
- package/dist/lib/subagents-registry.js +5 -12
- package/dist/lib/subagents.d.ts +0 -10
- package/dist/lib/subagents.js +0 -12
- package/dist/lib/teams/agents.d.ts +0 -2
- package/dist/lib/teams/agents.js +10 -27
- package/dist/lib/teams/index.d.ts +1 -1
- package/dist/lib/teams/index.js +1 -1
- package/dist/lib/types.d.ts +17 -9
- package/dist/lib/usage.d.ts +28 -5
- package/dist/lib/usage.js +271 -8
- package/dist/lib/versions.js +7 -1
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +25 -13
- package/package.json +1 -2
- package/dist/commands/check.d.ts +0 -15
- package/dist/commands/check.js +0 -180
- package/dist/commands/resources.d.ts +0 -5
- package/dist/lib/secrets/account-token.d.ts +0 -20
- package/dist/lib/secrets/account-token.js +0 -64
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-device harness divergence for `agents doctor` (RUSH-2027).
|
|
3
|
+
*
|
|
4
|
+
* `agents fleet status` already fans out `agents doctor --json` per device and
|
|
5
|
+
* builds a {@link FleetHealthReport} of coarse device health (reachable, CLIs
|
|
6
|
+
* installed, agents-cli version skew, local sync drift). It deliberately does
|
|
7
|
+
* NOT compare fine-grained *resource presence* across devices — so a plugin like
|
|
8
|
+
* `swarm` installed on one box but missing on another is silent until a user
|
|
9
|
+
* types `/swarm:run` on the wrong machine and gets `Unknown command`.
|
|
10
|
+
*
|
|
11
|
+
* This module is the missing comparison. Each device self-reports its installed
|
|
12
|
+
* inventory (resource kinds, per-agent version sets, `.agents`/`.system` repo
|
|
13
|
+
* state) in its `doctor --json` payload; {@link compareFleetInventories} takes
|
|
14
|
+
* the collected per-device inventories and, treating the local machine as the
|
|
15
|
+
* baseline, flags:
|
|
16
|
+
*
|
|
17
|
+
* 1. Resource presence gaps — a resource present on one device, missing on
|
|
18
|
+
* another (either direction relative to the local baseline).
|
|
19
|
+
* 2. Agent version gaps — an agent version installed on one device but
|
|
20
|
+
* not another (e.g. `yosemite-s0 missing claude@2.1.220`).
|
|
21
|
+
* 3. `.agents`/`.system` repo drift — a device whose config-repo HEAD, branch,
|
|
22
|
+
* or dirty state diverges from the local baseline.
|
|
23
|
+
*
|
|
24
|
+
* Pure and SSH-free: the SSH fan-out lives in the doctor command; this module
|
|
25
|
+
* only consumes the already-collected payloads, so the divergence logic is
|
|
26
|
+
* unit-tested against fixture inventories with no live fleet.
|
|
27
|
+
*/
|
|
28
|
+
export const FLEET_RESOURCE_KINDS = [
|
|
29
|
+
'commands',
|
|
30
|
+
'skills',
|
|
31
|
+
'hooks',
|
|
32
|
+
'rules',
|
|
33
|
+
'mcp',
|
|
34
|
+
'permissions',
|
|
35
|
+
'subagents',
|
|
36
|
+
'plugins',
|
|
37
|
+
'promptcuts',
|
|
38
|
+
'workflows',
|
|
39
|
+
];
|
|
40
|
+
function sortedUnique(list) {
|
|
41
|
+
return Array.from(new Set(list)).sort();
|
|
42
|
+
}
|
|
43
|
+
function repoLabel(repo) {
|
|
44
|
+
return repo === 'agents' ? '.agents' : '.system';
|
|
45
|
+
}
|
|
46
|
+
/** Describe how a remote repo state diverges from the local baseline, or null
|
|
47
|
+
* when they match. Compares HEAD first (the load-bearing difference), then
|
|
48
|
+
* branch, then a dirty tree on either side (naming which side is dirty). */
|
|
49
|
+
function describeRepoDrift(local, remote) {
|
|
50
|
+
if (local.head && remote.head && local.head !== remote.head) {
|
|
51
|
+
return `HEAD ${remote.head} != local ${local.head}`;
|
|
52
|
+
}
|
|
53
|
+
if (local.branch !== remote.branch) {
|
|
54
|
+
return `branch ${remote.branch ?? 'detached'} != local ${local.branch ?? 'detached'}`;
|
|
55
|
+
}
|
|
56
|
+
// Flag a dirty tree on EITHER side (symmetric with HEAD/branch above), and name
|
|
57
|
+
// the side that has the uncommitted changes — the remote, or the local baseline.
|
|
58
|
+
if (remote.dirty !== local.dirty) {
|
|
59
|
+
return remote.dirty
|
|
60
|
+
? 'remote tree has uncommitted changes'
|
|
61
|
+
: 'local tree has uncommitted changes';
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Compare a set of per-device inventories against the local baseline and emit
|
|
67
|
+
* every cross-device divergence. The baseline is the device whose name equals
|
|
68
|
+
* {@link baselineName} (the local machine); if that device has no inventory the
|
|
69
|
+
* report is empty (nothing to compare against). Devices with no inventory are
|
|
70
|
+
* recorded under `skippedDevices` and never produce false "missing" findings.
|
|
71
|
+
*
|
|
72
|
+
* Ordering is deterministic (device, then category, then name) so the human and
|
|
73
|
+
* JSON output — and the tests — are stable.
|
|
74
|
+
*/
|
|
75
|
+
export function compareFleetInventories(devices, baselineName) {
|
|
76
|
+
const baseline = devices.find((d) => d.name === baselineName)?.inventory ?? null;
|
|
77
|
+
const remotes = devices.filter((d) => d.name !== baselineName);
|
|
78
|
+
const comparedDevices = [];
|
|
79
|
+
const skippedDevices = [];
|
|
80
|
+
const divergences = [];
|
|
81
|
+
if (!baseline) {
|
|
82
|
+
// No local baseline to compare against — record every remote as skipped so
|
|
83
|
+
// the caller can say so, but emit no divergences (we can't know the truth).
|
|
84
|
+
for (const d of remotes)
|
|
85
|
+
skippedDevices.push(d.name);
|
|
86
|
+
return {
|
|
87
|
+
baseline: baselineName,
|
|
88
|
+
divergences: [],
|
|
89
|
+
comparedDevices: [],
|
|
90
|
+
skippedDevices: skippedDevices.sort(),
|
|
91
|
+
hasDivergence: false,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
for (const remote of remotes) {
|
|
95
|
+
if (!remote.inventory) {
|
|
96
|
+
skippedDevices.push(remote.name);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
comparedDevices.push(remote.name);
|
|
100
|
+
const inv = remote.inventory;
|
|
101
|
+
// 1) Resource presence, both directions.
|
|
102
|
+
for (const kind of FLEET_RESOURCE_KINDS) {
|
|
103
|
+
const localSet = new Set(baseline.resources[kind] ?? []);
|
|
104
|
+
const remoteSet = new Set(inv.resources[kind] ?? []);
|
|
105
|
+
for (const name of sortedUnique(baseline.resources[kind] ?? [])) {
|
|
106
|
+
if (!remoteSet.has(name)) {
|
|
107
|
+
divergences.push({
|
|
108
|
+
kind: 'resource-missing-remote',
|
|
109
|
+
device: remote.name,
|
|
110
|
+
category: kind,
|
|
111
|
+
name,
|
|
112
|
+
message: `${remote.name} is missing ${kind.replace(/s$/, '')} '${name}' (present on ${baselineName})`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const name of sortedUnique(inv.resources[kind] ?? [])) {
|
|
117
|
+
if (!localSet.has(name)) {
|
|
118
|
+
divergences.push({
|
|
119
|
+
kind: 'resource-missing-local',
|
|
120
|
+
device: remote.name,
|
|
121
|
+
category: kind,
|
|
122
|
+
name,
|
|
123
|
+
message: `${baselineName} is missing ${kind.replace(/s$/, '')} '${name}' (present on ${remote.name})`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// 2) Agent version parity, both directions, per agent id.
|
|
129
|
+
const agentIds = sortedUnique([
|
|
130
|
+
...Object.keys(baseline.agentVersions),
|
|
131
|
+
...Object.keys(inv.agentVersions),
|
|
132
|
+
]);
|
|
133
|
+
for (const agent of agentIds) {
|
|
134
|
+
const localVers = new Set(baseline.agentVersions[agent] ?? []);
|
|
135
|
+
const remoteVers = new Set(inv.agentVersions[agent] ?? []);
|
|
136
|
+
for (const v of sortedUnique(baseline.agentVersions[agent] ?? [])) {
|
|
137
|
+
if (!remoteVers.has(v)) {
|
|
138
|
+
divergences.push({
|
|
139
|
+
kind: 'agent-version-missing-remote',
|
|
140
|
+
device: remote.name,
|
|
141
|
+
category: agent,
|
|
142
|
+
name: v,
|
|
143
|
+
message: `${remote.name} is missing ${agent}@${v} (installed on ${baselineName})`,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const v of sortedUnique(inv.agentVersions[agent] ?? [])) {
|
|
148
|
+
if (!localVers.has(v)) {
|
|
149
|
+
divergences.push({
|
|
150
|
+
kind: 'agent-version-missing-local',
|
|
151
|
+
device: remote.name,
|
|
152
|
+
category: agent,
|
|
153
|
+
name: v,
|
|
154
|
+
message: `${baselineName} is missing ${agent}@${v} (installed on ${remote.name})`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// 3) `.agents` / `.system` repo drift vs the local baseline.
|
|
160
|
+
for (const repo of ['agents', 'system']) {
|
|
161
|
+
const localRepo = baseline.repos[repo];
|
|
162
|
+
const remoteRepo = inv.repos[repo];
|
|
163
|
+
if (!localRepo || !remoteRepo)
|
|
164
|
+
continue; // one side isn't a readable repo
|
|
165
|
+
const drift = describeRepoDrift(localRepo, remoteRepo);
|
|
166
|
+
if (drift) {
|
|
167
|
+
divergences.push({
|
|
168
|
+
kind: 'repo-drift',
|
|
169
|
+
device: remote.name,
|
|
170
|
+
category: repo,
|
|
171
|
+
name: repoLabel(repo),
|
|
172
|
+
message: `${remote.name} ${repoLabel(repo)} repo diverged: ${drift}`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
divergences.sort((a, b) => a.device.localeCompare(b.device) ||
|
|
178
|
+
a.kind.localeCompare(b.kind) ||
|
|
179
|
+
a.category.localeCompare(b.category) ||
|
|
180
|
+
a.name.localeCompare(b.name));
|
|
181
|
+
return {
|
|
182
|
+
baseline: baselineName,
|
|
183
|
+
divergences,
|
|
184
|
+
comparedDevices: comparedDevices.sort(),
|
|
185
|
+
skippedDevices: skippedDevices.sort(),
|
|
186
|
+
hasDivergence: divergences.length > 0,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the self-reported harness inventory a device emits in `doctor --json`
|
|
3
|
+
* for cross-device divergence detection (RUSH-2027).
|
|
4
|
+
*
|
|
5
|
+
* Kept separate from {@link ../devices/fleet-divergence.js} — which stays a pure,
|
|
6
|
+
* SSH-free comparator — because this reads the live install (resource dirs,
|
|
7
|
+
* installed version homes, config repos). One call produces the {@link
|
|
8
|
+
* FleetInventory} that both the local baseline and every remote box serialize
|
|
9
|
+
* into their doctor payload; the comparator then diffs those payloads.
|
|
10
|
+
*/
|
|
11
|
+
import { type FleetInventory } from './fleet-divergence.js';
|
|
12
|
+
/**
|
|
13
|
+
* Collect this machine's harness inventory: installed resources per kind,
|
|
14
|
+
* installed version ids per agent, and `.agents`/`.system` repo state. Pure
|
|
15
|
+
* reads — never mutates the install. `promptcuts` (a single present/absent bit
|
|
16
|
+
* in {@link getAvailableResources}) is surfaced as a one-element list so it
|
|
17
|
+
* compares like any other named resource.
|
|
18
|
+
*/
|
|
19
|
+
export declare function collectLocalFleetInventory(cwd?: string): FleetInventory;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the self-reported harness inventory a device emits in `doctor --json`
|
|
3
|
+
* for cross-device divergence detection (RUSH-2027).
|
|
4
|
+
*
|
|
5
|
+
* Kept separate from {@link ../devices/fleet-divergence.js} — which stays a pure,
|
|
6
|
+
* SSH-free comparator — because this reads the live install (resource dirs,
|
|
7
|
+
* installed version homes, config repos). One call produces the {@link
|
|
8
|
+
* FleetInventory} that both the local baseline and every remote box serialize
|
|
9
|
+
* into their doctor payload; the comparator then diffs those payloads.
|
|
10
|
+
*/
|
|
11
|
+
import { getAvailableResources, listInstalledVersions } from '../versions.js';
|
|
12
|
+
import { getUserAgentsDir, getSystemAgentsDir } from '../state.js';
|
|
13
|
+
import { readRepoState } from '../git.js';
|
|
14
|
+
import { ALL_AGENT_IDS } from '../agents.js';
|
|
15
|
+
import { FLEET_RESOURCE_KINDS } from './fleet-divergence.js';
|
|
16
|
+
function toRepoState(snap) {
|
|
17
|
+
if (!snap)
|
|
18
|
+
return null;
|
|
19
|
+
return { branch: snap.branch, head: snap.head, dirty: snap.dirty };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Collect this machine's harness inventory: installed resources per kind,
|
|
23
|
+
* installed version ids per agent, and `.agents`/`.system` repo state. Pure
|
|
24
|
+
* reads — never mutates the install. `promptcuts` (a single present/absent bit
|
|
25
|
+
* in {@link getAvailableResources}) is surfaced as a one-element list so it
|
|
26
|
+
* compares like any other named resource.
|
|
27
|
+
*/
|
|
28
|
+
export function collectLocalFleetInventory(cwd = process.cwd()) {
|
|
29
|
+
const available = getAvailableResources(cwd);
|
|
30
|
+
const resources = {};
|
|
31
|
+
for (const kind of FLEET_RESOURCE_KINDS) {
|
|
32
|
+
if (kind === 'promptcuts') {
|
|
33
|
+
resources[kind] = available.promptcuts ? ['promptcuts.yaml'] : [];
|
|
34
|
+
}
|
|
35
|
+
else if (kind === 'rules') {
|
|
36
|
+
// getAvailableResources exposes top-level rules under `memory`.
|
|
37
|
+
resources[kind] = [...available.memory].sort();
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
resources[kind] = [...(available[kind] ?? [])].sort();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const agentVersions = {};
|
|
44
|
+
for (const agent of ALL_AGENT_IDS) {
|
|
45
|
+
const versions = listInstalledVersions(agent);
|
|
46
|
+
if (versions.length > 0)
|
|
47
|
+
agentVersions[agent] = [...versions].sort();
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
resources,
|
|
51
|
+
agentVersions,
|
|
52
|
+
repos: {
|
|
53
|
+
agents: toRepoState(readRepoState(getUserAgentsDir())),
|
|
54
|
+
system: toRepoState(readRepoState(getSystemAgentsDir())),
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -48,7 +48,7 @@ export interface FanOutDeviceResult<T> {
|
|
|
48
48
|
export declare function planFleetTargets(reg: DeviceRegistry): FleetTarget[];
|
|
49
49
|
/**
|
|
50
50
|
* Remote fan-out targets for the fleet health/drift gates (`fleet status`,
|
|
51
|
-
* `check --devices`): every planned device except this machine and control-only
|
|
51
|
+
* `doctor --check --devices`): every planned device except this machine and control-only
|
|
52
52
|
* cockpits. A control device never runs agents (mirrors doctor's fan-out, which
|
|
53
53
|
* drops it via `isControlDevice`), so counting it as unreachable/drift would make
|
|
54
54
|
* the CI gate fail on every run for a fleet that merely has a cockpit registered.
|
|
@@ -44,7 +44,7 @@ export function planFleetTargets(reg) {
|
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
46
|
* Remote fan-out targets for the fleet health/drift gates (`fleet status`,
|
|
47
|
-
* `check --devices`): every planned device except this machine and control-only
|
|
47
|
+
* `doctor --check --devices`): every planned device except this machine and control-only
|
|
48
48
|
* cockpits. A control device never runs agents (mirrors doctor's fan-out, which
|
|
49
49
|
* drops it via `isControlDevice`), so counting it as unreachable/drift would make
|
|
50
50
|
* the CI gate fail on every run for a fleet that merely has a cockpit registered.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type DeviceStats } from './health.js';
|
|
2
2
|
import { type HostAuthSummary } from '../auth-health.js';
|
|
3
3
|
import type { OnlineState } from './reachability.js';
|
|
4
|
+
import { type FleetInventory } from './fleet-divergence.js';
|
|
4
5
|
export interface FleetCliStatus {
|
|
5
6
|
installed: boolean;
|
|
6
7
|
path: string | null;
|
|
@@ -40,9 +41,14 @@ export interface FleetHealthRow {
|
|
|
40
41
|
* an offline row. Sourced from the registry's tailscale snapshot / reachability
|
|
41
42
|
* verdict. Undefined when never recorded. */
|
|
42
43
|
lastSeen?: string;
|
|
44
|
+
/** This host's self-reported harness inventory (resources / agent versions /
|
|
45
|
+
* repo state) from its `doctor --json` `fleet` field, for cross-device
|
|
46
|
+
* divergence detection (RUSH-2027). Undefined for an unreachable box or an
|
|
47
|
+
* older CLI that doesn't emit it. */
|
|
48
|
+
inventory?: FleetInventory;
|
|
43
49
|
}
|
|
44
50
|
export interface FleetWarning {
|
|
45
|
-
kind: 'unreachable' | 'drift' | 'cli' | 'version-skew';
|
|
51
|
+
kind: 'unreachable' | 'drift' | 'cli' | 'version-skew' | 'divergence';
|
|
46
52
|
devices: string[];
|
|
47
53
|
message: string;
|
|
48
54
|
}
|
|
@@ -53,7 +59,9 @@ export interface FleetHealthReport {
|
|
|
53
59
|
hasWarnings: boolean;
|
|
54
60
|
hasDrift: boolean;
|
|
55
61
|
}
|
|
56
|
-
export declare function buildFleetHealthReport(rows: FleetHealthRow[], now?: Date
|
|
62
|
+
export declare function buildFleetHealthReport(rows: FleetHealthRow[], now?: Date, opts?: {
|
|
63
|
+
self?: string;
|
|
64
|
+
}): FleetHealthReport;
|
|
57
65
|
export declare function renderFleetWarnings(report: FleetHealthReport): string[];
|
|
58
66
|
export declare function renderFleetMatrix(report: FleetHealthReport): string[];
|
|
59
67
|
/**
|
|
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { padToWidth, stringWidth, terminalWidth, truncateToWidth } from '../session/width.js';
|
|
3
3
|
import { fmtBytes, headroom } from './health.js';
|
|
4
4
|
import { formatCheckedAge } from '../auth-health.js';
|
|
5
|
+
import { compareFleetInventories } from './fleet-divergence.js';
|
|
5
6
|
function driftRows(row) {
|
|
6
7
|
return row.sync.filter((s) => s.status !== 'fresh');
|
|
7
8
|
}
|
|
@@ -12,7 +13,7 @@ function installedCliCount(row) {
|
|
|
12
13
|
total: statuses.length,
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
|
-
export function buildFleetHealthReport(rows, now = new Date()) {
|
|
16
|
+
export function buildFleetHealthReport(rows, now = new Date(), opts = {}) {
|
|
16
17
|
const warnings = [];
|
|
17
18
|
const unreachable = rows
|
|
18
19
|
.filter((r) => r.error || r.skipped)
|
|
@@ -60,6 +61,25 @@ export function buildFleetHealthReport(rows, now = new Date()) {
|
|
|
60
61
|
message: `agents-cli version skew: ${Array.from(versions.keys()).sort().join(', ')}`,
|
|
61
62
|
});
|
|
62
63
|
}
|
|
64
|
+
// Cross-device harness divergence (RUSH-2027): compare every device's
|
|
65
|
+
// self-reported inventory against the local baseline and roll the findings up
|
|
66
|
+
// into one warning per affected device. Only runs when a baseline (`self`) is
|
|
67
|
+
// named and at least one device carries an inventory — an older-CLI fleet with
|
|
68
|
+
// no `fleet` field simply produces no divergence warning.
|
|
69
|
+
if (opts.self && rows.some((r) => r.inventory)) {
|
|
70
|
+
const divergence = compareFleetInventories(rows.map((r) => ({ name: r.name, inventory: r.inventory ?? null })), opts.self);
|
|
71
|
+
const byDevice = new Map();
|
|
72
|
+
for (const d of divergence.divergences)
|
|
73
|
+
byDevice.set(d.device, (byDevice.get(d.device) ?? 0) + 1);
|
|
74
|
+
for (const device of Array.from(byDevice.keys()).sort()) {
|
|
75
|
+
const n = byDevice.get(device);
|
|
76
|
+
warnings.push({
|
|
77
|
+
kind: 'divergence',
|
|
78
|
+
devices: [device],
|
|
79
|
+
message: `${device} diverges from ${opts.self} (${n} resource/version/repo gap${n === 1 ? '' : 's'})`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
63
83
|
return {
|
|
64
84
|
generatedAt: now.toISOString(),
|
|
65
85
|
devices: rows,
|
|
@@ -326,6 +346,17 @@ export function buildFleetAttentionItems(report, now = Date.now()) {
|
|
|
326
346
|
.join(' · ');
|
|
327
347
|
items.push({ glyph: 'warn', subject: 'version skew', detail: summary, fix: 'agents upgrade --fleet' });
|
|
328
348
|
}
|
|
349
|
+
// 4) Cross-device harness divergence — one item per diverged box (RUSH-2027).
|
|
350
|
+
for (const w of report.warnings) {
|
|
351
|
+
if (w.kind !== 'divergence')
|
|
352
|
+
continue;
|
|
353
|
+
items.push({
|
|
354
|
+
glyph: 'warn',
|
|
355
|
+
subject: w.devices[0] ?? 'fleet',
|
|
356
|
+
detail: w.message.replace(`${w.devices[0]} `, ''),
|
|
357
|
+
fix: `agents apply ${w.devices[0] ?? ''}`.trim(),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
329
360
|
return items;
|
|
330
361
|
}
|
|
331
362
|
/** Right-aligned `<content>` on the same line as `left`, clamped so it never
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import type { AgentId } from './types.js';
|
|
23
23
|
import type { DiscoveredPlugin } from './types.js';
|
|
24
|
+
import { type HookWiringReport } from './hooks.js';
|
|
24
25
|
export type DoctorKind = 'commands' | 'skills' | 'hooks' | 'rules' | 'mcp' | 'permissions' | 'subagents' | 'plugins' | 'promptcuts';
|
|
25
26
|
export type DiffStatus = 'ok' | 'diff' | 'missing' | 'extra';
|
|
26
27
|
export type SourceLayer = 'project' | 'user' | 'system' | 'extra';
|
|
@@ -37,6 +38,19 @@ export interface ResourceDiff {
|
|
|
37
38
|
* "0.6.1→0.7.0, missing skills: ship, learn". Currently set for plugins. */
|
|
38
39
|
detail?: string;
|
|
39
40
|
}
|
|
41
|
+
/** A source layer (user / system / an extra repo) that is behind its upstream —
|
|
42
|
+
* reconciled against stale truth. Attached to the report by the doctor command
|
|
43
|
+
* (needs a git probe, kept out of the pure per-version diff). */
|
|
44
|
+
export interface SourceLayerBehind {
|
|
45
|
+
layer: 'user' | 'system' | 'extra';
|
|
46
|
+
/** Human label for the layer's on-disk root (e.g. `~/.agents`). */
|
|
47
|
+
label: string;
|
|
48
|
+
/** `agents repo pull` alias for the remediation hint. */
|
|
49
|
+
alias: string;
|
|
50
|
+
behind: number;
|
|
51
|
+
/** Upstream ref the layer trails (e.g. `origin/main`). */
|
|
52
|
+
branch: string;
|
|
53
|
+
}
|
|
40
54
|
export interface VersionResourceReport {
|
|
41
55
|
agent: AgentId;
|
|
42
56
|
version: string;
|
|
@@ -58,6 +72,12 @@ export interface VersionResourceReport {
|
|
|
58
72
|
missing: number;
|
|
59
73
|
extra: number;
|
|
60
74
|
};
|
|
75
|
+
/** Wiring inspection for the version's native settings.json (claude/droid).
|
|
76
|
+
* Populated only when the `hooks` kind is in scope. */
|
|
77
|
+
hookWiring?: HookWiringReport;
|
|
78
|
+
/** Source layers behind their upstream. Filled by the doctor command (a git
|
|
79
|
+
* probe), not the pure diff — undefined when uncomputed. */
|
|
80
|
+
sourceBehind?: SourceLayerBehind[];
|
|
61
81
|
}
|
|
62
82
|
/**
|
|
63
83
|
* Describe how a version's marketplace MIRROR of a plugin diverges from its
|
package/dist/lib/doctor-diff.js
CHANGED
|
@@ -33,7 +33,7 @@ import { shouldInstallCommandAsSkill, commandSkillMatches, commandSkillName } fr
|
|
|
33
33
|
import { gooseCommandMatches, gooseCommandsDir } from './goose-commands.js';
|
|
34
34
|
import { supports } from './capabilities.js';
|
|
35
35
|
import { listSkillsInVersionHome, getVersionSkillsDir } from './skills.js';
|
|
36
|
-
import { listHooksInVersionHome, listHookEntriesFromDir } from './hooks.js';
|
|
36
|
+
import { listHooksInVersionHome, listHookEntriesFromDir, checkVersionHookWiring } from './hooks.js';
|
|
37
37
|
const RULES_DOC_FILENAME = 'README.md';
|
|
38
38
|
const ALL_KINDS = [
|
|
39
39
|
'commands',
|
|
@@ -611,6 +611,10 @@ export function diffVersionResources(agent, version, options = {}) {
|
|
|
611
611
|
empty.skills = diffSkills(agent, version, cwd, excludeProject);
|
|
612
612
|
if (requested.has('hooks'))
|
|
613
613
|
empty.hooks = diffHooks(agent, version, cwd);
|
|
614
|
+
// Wiring check: a hook FILE can be present and byte-identical to source (ok
|
|
615
|
+
// above) yet never referenced in settings.json, so it never fires. Only
|
|
616
|
+
// meaningful when hooks are in scope.
|
|
617
|
+
const hookWiring = requested.has('hooks') ? checkVersionHookWiring(agent, version) : undefined;
|
|
614
618
|
if (requested.has('rules'))
|
|
615
619
|
empty.rules = diffRules(agent, version, cwd, excludeProject);
|
|
616
620
|
if (requested.has('mcp'))
|
|
@@ -649,6 +653,7 @@ export function diffVersionResources(agent, version, options = {}) {
|
|
|
649
653
|
},
|
|
650
654
|
kinds: empty,
|
|
651
655
|
summary: { ok, diff, missing, extra },
|
|
656
|
+
...(hookWiring ? { hookWiring } : {}),
|
|
652
657
|
};
|
|
653
658
|
}
|
|
654
659
|
export const DOCTOR_ALL_KINDS = ALL_KINDS;
|
package/dist/lib/drift.d.ts
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared drift-detection internals for `agents doctor` (overview mode) and
|
|
3
|
-
* `agents check` (the scriptable, CI-friendly gate).
|
|
3
|
+
* `agents doctor --check` (the scriptable, CI-friendly gate).
|
|
4
4
|
*
|
|
5
5
|
* This is the single source of truth for "is the install out of sync?": the
|
|
6
6
|
* per-version sync status (fresh / stale / never-synced) and the orphan census.
|
|
7
|
-
* `agents doctor` renders it as a human report; `agents check` reduces it
|
|
8
|
-
* exit code. Neither reimplements the diagnostic — both call `computeDrift`.
|
|
7
|
+
* `agents doctor` renders it as a human report; `agents doctor --check` reduces it
|
|
8
|
+
* to an exit code. Neither reimplements the diagnostic — both call `computeDrift`.
|
|
9
9
|
*/
|
|
10
10
|
import type { AgentId } from './types.js';
|
|
11
|
-
import { type VersionResourceReport } from './doctor-diff.js';
|
|
11
|
+
import { type VersionResourceReport, type SourceLayerBehind } from './doctor-diff.js';
|
|
12
12
|
export interface SyncStatusRow {
|
|
13
13
|
agent: AgentId;
|
|
14
14
|
version: string;
|
|
15
15
|
status: 'fresh' | 'stale' | 'never-synced';
|
|
16
16
|
isDefault: boolean;
|
|
17
|
-
/** For stale rows: prioritized lines naming exactly what diverged (plugins first).
|
|
17
|
+
/** For stale rows: prioritized lines naming exactly what diverged (plugins first).
|
|
18
|
+
* Also carries hook-wiring divergence for a version whose hooks are present but
|
|
19
|
+
* not wired into settings.json — independent of manifest staleness. */
|
|
18
20
|
divergence?: string[];
|
|
21
|
+
/** Count of hooks present on disk but NOT wired into the version's settings.json
|
|
22
|
+
* (claude/droid). A non-zero value makes the version out-of-sync even when the
|
|
23
|
+
* manifest reads fresh — the yosemite-s1 blind spot the CI gate must catch. */
|
|
24
|
+
unwiredHooks?: number;
|
|
19
25
|
}
|
|
20
26
|
export interface OrphanRow {
|
|
21
27
|
agent: AgentId;
|
|
@@ -27,6 +33,14 @@ export interface OrphanRow {
|
|
|
27
33
|
export declare function divergenceLines(report: VersionResourceReport): string[];
|
|
28
34
|
export declare function checkSyncStatus(cwd: string): SyncStatusRow[];
|
|
29
35
|
export declare function countOrphans(): OrphanRow[];
|
|
36
|
+
/**
|
|
37
|
+
* Probe each source layer (user, system, enabled extras) for how far it trails
|
|
38
|
+
* its upstream, from the LAST-FETCHED remote-tracking ref (no network). A layer
|
|
39
|
+
* behind origin means every version home is reconciled against stale truth — a
|
|
40
|
+
* drift signal `agents doctor --check` must fail on, not a buried preamble. Returns
|
|
41
|
+
* only the behind layers. Canonical home for both `agents doctor` and `agents doctor --check`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function computeSourceBehind(): SourceLayerBehind[];
|
|
30
44
|
export interface DriftSummary {
|
|
31
45
|
syncRows: SyncStatusRow[];
|
|
32
46
|
orphanRows: OrphanRow[];
|
|
@@ -36,17 +50,23 @@ export interface DriftSummary {
|
|
|
36
50
|
neverSyncedCount: number;
|
|
37
51
|
/** Versions carrying orphan resources (informational — not a drift signal). */
|
|
38
52
|
orphanVersionCount: number;
|
|
53
|
+
/** Versions with hooks present on disk but not wired into settings.json. */
|
|
54
|
+
unwiredHookVersions: number;
|
|
55
|
+
/** Source layers behind their upstream (reconciled against stale truth). */
|
|
56
|
+
sourceBehind: SourceLayerBehind[];
|
|
39
57
|
/**
|
|
40
|
-
* True when
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
58
|
+
* True when the install is out of sync: any installed version is stale,
|
|
59
|
+
* never-synced, or carries unwired hooks, OR a source layer is behind origin.
|
|
60
|
+
* `agents doctor` surfaces it as "run `agents status`"; `agents doctor --check`
|
|
61
|
+
* maps it to a non-zero exit. Orphans are a `prune` concern, not sync drift, so they do
|
|
62
|
+
* NOT set this flag (mirrors the sync-status engine: an orphan alone never
|
|
63
|
+
* flags needsSync).
|
|
44
64
|
*/
|
|
45
65
|
hasDrift: boolean;
|
|
46
66
|
}
|
|
47
67
|
/**
|
|
48
68
|
* Compute the same drift/divergence diagnostic `agents doctor` prints, reduced
|
|
49
|
-
* to a summary with a single `hasDrift` boolean. The gate `agents check`
|
|
50
|
-
* to an exit code; the readout `agents doctor` renders in full.
|
|
69
|
+
* to a summary with a single `hasDrift` boolean. The gate `agents doctor --check`
|
|
70
|
+
* maps to an exit code; the readout `agents doctor` renders in full.
|
|
51
71
|
*/
|
|
52
72
|
export declare function computeDrift(cwd: string): DriftSummary;
|
package/dist/lib/drift.js
CHANGED
|
@@ -4,7 +4,9 @@ import { loadManifest, isStale } from './staleness/index.js';
|
|
|
4
4
|
import { diffVersionResources } from './doctor-diff.js';
|
|
5
5
|
import { diffVersionCommands, iterCommandsCapableVersions } from './commands.js';
|
|
6
6
|
import { diffVersionSkills, iterSkillsCapableVersions } from './skills.js';
|
|
7
|
-
import { iterHooksCapableVersions, listUnmanagedHooksInVersionHome } from './hooks.js';
|
|
7
|
+
import { iterHooksCapableVersions, listUnmanagedHooksInVersionHome, checkVersionHookWiring } from './hooks.js';
|
|
8
|
+
import { commitsBehindUpstream } from './git.js';
|
|
9
|
+
import { getUserAgentsDir, getSystemAgentsDir, getEnabledExtraRepos } from './state.js';
|
|
8
10
|
// Lines naming exactly what's out of sync for a version, plugins prioritized:
|
|
9
11
|
// each divergent plugin gets its own line with specifics (stale mirror version,
|
|
10
12
|
// invalid manifest, or the bundled skills/commands missing from the mirror —
|
|
@@ -46,14 +48,37 @@ export function checkSyncStatus(cwd) {
|
|
|
46
48
|
? 'never-synced'
|
|
47
49
|
: isStale(manifest, agent, version, cwd) ? 'stale' : 'fresh';
|
|
48
50
|
const row = { agent, version, status, isDefault: version === def };
|
|
51
|
+
const divergence = [];
|
|
49
52
|
if (status === 'stale') {
|
|
50
53
|
// Resolve the specifics against non-project layers (the global home is
|
|
51
54
|
// never reconciled against per-cwd project resources).
|
|
52
55
|
const report = diffVersionResources(agent, version, { cwd, excludeProject: true });
|
|
53
|
-
|
|
54
|
-
if (lines.length)
|
|
55
|
-
row.divergence = lines;
|
|
56
|
+
divergence.push(...divergenceLines(report));
|
|
56
57
|
}
|
|
58
|
+
// Hook WIRING is independent of manifest staleness: a hook file can be
|
|
59
|
+
// byte-identical to source (fresh) yet absent from settings.json, so it
|
|
60
|
+
// never fires. Surface that for every version, fresh or stale, so overview
|
|
61
|
+
// AND `agents doctor --check` flag it (claude/droid; other agents report unsupported).
|
|
62
|
+
const wiring = checkVersionHookWiring(agent, version);
|
|
63
|
+
if (wiring.supported) {
|
|
64
|
+
const expected = wiring.expected ?? 0;
|
|
65
|
+
if (wiring.settingsMissing && expected > 0) {
|
|
66
|
+
row.unwiredHooks = expected;
|
|
67
|
+
divergence.push(`hooks settings.json missing — ${expected} declared hook(s) never fire`);
|
|
68
|
+
}
|
|
69
|
+
else if (wiring.settingsUnparseable) {
|
|
70
|
+
row.unwiredHooks = Math.max(1, expected);
|
|
71
|
+
divergence.push(`hooks settings.json unparseable — wiring cannot be verified`);
|
|
72
|
+
}
|
|
73
|
+
else if (wiring.unwired.length > 0) {
|
|
74
|
+
row.unwiredHooks = wiring.unwired.length;
|
|
75
|
+
const names = wiring.unwired.map((u) => u.name);
|
|
76
|
+
const shown = names.slice(0, 3).join(', ');
|
|
77
|
+
divergence.push(`hooks ${wiring.unwired.length} unwired (${shown}${names.length > 3 ? ', …' : ''})`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (divergence.length)
|
|
81
|
+
row.divergence = divergence;
|
|
57
82
|
rows.push(row);
|
|
58
83
|
}
|
|
59
84
|
}
|
|
@@ -91,22 +116,48 @@ export function countOrphans() {
|
|
|
91
116
|
}
|
|
92
117
|
return Array.from(byKey.values()).filter((r) => r.commands + r.skills + r.hooks > 0);
|
|
93
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Probe each source layer (user, system, enabled extras) for how far it trails
|
|
121
|
+
* its upstream, from the LAST-FETCHED remote-tracking ref (no network). A layer
|
|
122
|
+
* behind origin means every version home is reconciled against stale truth — a
|
|
123
|
+
* drift signal `agents doctor --check` must fail on, not a buried preamble. Returns
|
|
124
|
+
* only the behind layers. Canonical home for both `agents doctor` and `agents doctor --check`.
|
|
125
|
+
*/
|
|
126
|
+
export function computeSourceBehind() {
|
|
127
|
+
const out = [];
|
|
128
|
+
const probe = (layer, dir, label, alias) => {
|
|
129
|
+
const r = commitsBehindUpstream(dir);
|
|
130
|
+
if (r && r.behind > 0)
|
|
131
|
+
out.push({ layer, label, alias, behind: r.behind, branch: r.branch });
|
|
132
|
+
};
|
|
133
|
+
probe('user', getUserAgentsDir(), '~/.agents', 'user');
|
|
134
|
+
probe('system', getSystemAgentsDir(), '~/.agents/.system', 'system');
|
|
135
|
+
for (const e of getEnabledExtraRepos())
|
|
136
|
+
probe('extra', e.dir, e.alias, e.alias);
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
94
139
|
/**
|
|
95
140
|
* Compute the same drift/divergence diagnostic `agents doctor` prints, reduced
|
|
96
|
-
* to a summary with a single `hasDrift` boolean. The gate `agents check`
|
|
97
|
-
* to an exit code; the readout `agents doctor` renders in full.
|
|
141
|
+
* to a summary with a single `hasDrift` boolean. The gate `agents doctor --check`
|
|
142
|
+
* maps to an exit code; the readout `agents doctor` renders in full.
|
|
98
143
|
*/
|
|
99
144
|
export function computeDrift(cwd) {
|
|
100
145
|
const syncRows = checkSyncStatus(cwd);
|
|
101
146
|
const orphanRows = countOrphans();
|
|
102
147
|
const staleCount = syncRows.filter((r) => r.status === 'stale').length;
|
|
103
148
|
const neverSyncedCount = syncRows.filter((r) => r.status === 'never-synced').length;
|
|
149
|
+
const unwiredHookVersions = syncRows.filter((r) => (r.unwiredHooks ?? 0) > 0).length;
|
|
150
|
+
const sourceBehind = computeSourceBehind();
|
|
104
151
|
return {
|
|
105
152
|
syncRows,
|
|
106
153
|
orphanRows,
|
|
107
154
|
staleCount,
|
|
108
155
|
neverSyncedCount,
|
|
109
156
|
orphanVersionCount: orphanRows.length,
|
|
110
|
-
|
|
157
|
+
unwiredHookVersions,
|
|
158
|
+
sourceBehind,
|
|
159
|
+
hasDrift: syncRows.some((r) => r.status !== 'fresh') ||
|
|
160
|
+
unwiredHookVersions > 0 ||
|
|
161
|
+
sourceBehind.length > 0,
|
|
111
162
|
};
|
|
112
163
|
}
|