@phnx-labs/agents-cli 1.22.57 → 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 +294 -0
- package/README.md +29 -0
- package/dist/bootstrap.js +39 -1
- package/dist/commands/accounts.js +7 -3
- package/dist/commands/apply.js +10 -2
- package/dist/commands/fork.d.ts +23 -10
- package/dist/commands/fork.js +115 -58
- package/dist/commands/monitors.js +198 -23
- package/dist/commands/prune.js +5 -3
- package/dist/commands/routines.d.ts +8 -0
- package/dist/commands/routines.js +57 -3
- 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-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +16 -0
- package/dist/commands/sessions-stats.js +37 -5
- package/dist/commands/sessions.js +40 -5
- package/dist/commands/share.d.ts +14 -0
- package/dist/commands/share.js +43 -2
- package/dist/commands/ssh.js +12 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/sync.js +83 -7
- package/dist/commands/traces.js +7 -0
- package/dist/commands/versions.js +12 -4
- package/dist/commands/view.js +7 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -1
- package/dist/lib/account-registry.d.ts +5 -1
- package/dist/lib/account-registry.js +47 -14
- package/dist/lib/accounting/capacity.d.ts +18 -7
- package/dist/lib/accounting/capacity.js +19 -8
- package/dist/lib/accounting/usage-sync.d.ts +29 -1
- package/dist/lib/accounting/usage-sync.js +76 -2
- package/dist/lib/accounting/usage.js +7 -1
- package/dist/lib/auth-mint.d.ts +11 -1
- package/dist/lib/auth-mint.js +21 -6
- package/dist/lib/auto-pull-worker.js +7 -2
- package/dist/lib/browser/ipc.d.ts +8 -0
- package/dist/lib/browser/ipc.js +87 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +96 -11
- package/dist/lib/browser/sessions-list.js +10 -1
- 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/runner.d.ts +3 -0
- package/dist/lib/daemon/runner.js +86 -45
- package/dist/lib/daemon/session-index-service.js +9 -1
- package/dist/lib/daemon/usage-sync-service.d.ts +3 -3
- package/dist/lib/daemon/usage-sync-service.js +14 -8
- package/dist/lib/daemon-services.js +1 -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/connect.d.ts +17 -8
- package/dist/lib/devices/connect.js +31 -14
- 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 +120 -9
- package/dist/lib/fleet/manifest.d.ts +17 -0
- package/dist/lib/fleet/manifest.js +26 -0
- package/dist/lib/git.d.ts +38 -0
- package/dist/lib/git.js +58 -0
- package/dist/lib/hooks/install.d.ts +27 -11
- package/dist/lib/hooks/install.js +42 -17
- package/dist/lib/hosts/ready.d.ts +8 -0
- package/dist/lib/hosts/ready.js +13 -2
- package/dist/lib/hosts/reconnect.d.ts +52 -203
- package/dist/lib/hosts/reconnect.js +64 -284
- package/dist/lib/installations/migrate.d.ts +6 -120
- package/dist/lib/installations/migrate.js +27 -259
- package/dist/lib/installations/shims.d.ts +13 -95
- package/dist/lib/installations/shims.js +22 -139
- package/dist/lib/installations/store.js +1 -1
- package/dist/lib/installations/versions.d.ts +43 -133
- package/dist/lib/installations/versions.js +94 -206
- 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/plugins/skills.d.ts +8 -1
- package/dist/lib/plugins/skills.js +18 -2
- package/dist/lib/pty-server.d.ts +14 -0
- package/dist/lib/pty-server.js +49 -5
- package/dist/lib/refresh.d.ts +9 -0
- package/dist/lib/refresh.js +3 -1
- package/dist/lib/routine-readiness.d.ts +15 -1
- package/dist/lib/routine-readiness.js +41 -0
- package/dist/lib/sandbox.d.ts +4 -1
- package/dist/lib/sandbox.js +30 -1
- package/dist/lib/secrets/agent.d.ts +80 -225
- package/dist/lib/secrets/agent.js +139 -401
- package/dist/lib/secrets/bundles.d.ts +73 -222
- package/dist/lib/secrets/bundles.js +168 -467
- package/dist/lib/secrets/drivers/rush.js +5 -0
- package/dist/lib/secrets/reaper.d.ts +28 -70
- package/dist/lib/secrets/reaper.js +30 -85
- package/dist/lib/secrets/remote.d.ts +42 -129
- package/dist/lib/secrets/remote.js +55 -173
- package/dist/lib/self-heal/checks/install-staging.d.ts +4 -0
- package/dist/lib/self-heal/checks/install-staging.js +96 -0
- package/dist/lib/self-heal/registry.js +2 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/self-update.d.ts +65 -0
- package/dist/lib/self-update.js +138 -0
- package/dist/lib/session/active.d.ts +13 -1
- package/dist/lib/session/active.js +2 -0
- package/dist/lib/session/cloud.js +5 -0
- package/dist/lib/session/db.d.ts +51 -6
- package/dist/lib/session/db.js +266 -20
- package/dist/lib/session/fork.d.ts +45 -26
- package/dist/lib/session/fork.js +32 -95
- package/dist/lib/session/tool-calls.d.ts +43 -1
- package/dist/lib/session/tool-calls.js +74 -44
- package/dist/lib/session/tool-store.d.ts +33 -2
- package/dist/lib/session/tool-store.js +56 -3
- 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/sources.d.ts +5 -0
- package/dist/lib/staleness/writers/sources.js +2 -1
- 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/sync-status.d.ts +22 -0
- package/dist/lib/sync-status.js +27 -0
- package/dist/lib/sync-umbrella.d.ts +9 -0
- package/dist/lib/sync-umbrella.js +21 -2
- package/dist/lib/teams/scheduler.d.ts +10 -0
- package/dist/lib/teams/scheduler.js +8 -0
- package/dist/lib/traces/insights.d.ts +47 -14
- package/dist/lib/traces/insights.js +92 -21
- package/dist/lib/traces/phenotype.d.ts +23 -3
- package/dist/lib/traces/phenotype.js +72 -24
- package/dist/lib/traces/sync.d.ts +128 -6
- package/dist/lib/traces/sync.js +294 -35
- package/dist/lib/traces/worker-template.js +154 -1
- package/dist/lib/view-types.d.ts +12 -0
- package/package.json +2 -2
|
@@ -8,12 +8,8 @@ import type { DeviceProfile } from './registry.js';
|
|
|
8
8
|
* rewrites the cache, unreachable boxes included).
|
|
9
9
|
*/
|
|
10
10
|
export declare const STATS_STALE_MS: number;
|
|
11
|
-
/** Static hardware totals are valid for seven days. */
|
|
12
|
-
export declare const SPECS_STALE_MS: number;
|
|
13
11
|
/** True when a cached row is still within {@link STATS_STALE_MS}. */
|
|
14
12
|
export declare function isFreshDeviceStats(stats: DeviceStats, now?: number): boolean;
|
|
15
|
-
/** True when cached core, RAM-total, and disk-total facts remain current. */
|
|
16
|
-
export declare function isFreshDeviceSpecs(stats: DeviceStats, now?: number): boolean;
|
|
17
13
|
/**
|
|
18
14
|
* Carry a device's last successfully-probed hardware facts onto a row whose
|
|
19
15
|
* probe just came back unreachable (RUSH-3096).
|
|
@@ -54,8 +50,6 @@ export interface FleetStatsResult {
|
|
|
54
50
|
export interface LoadFleetStatsOptions {
|
|
55
51
|
/** Skip the cache and live-probe every device (the `--refresh`/`--live` path). */
|
|
56
52
|
forceRefresh?: boolean;
|
|
57
|
-
/** Read only static hardware facts, whose cache lifetime is seven days. */
|
|
58
|
-
specsOnly?: boolean;
|
|
59
53
|
/** Device name of THIS machine — always probed locally (no ssh), never cached-served. */
|
|
60
54
|
selfName?: string;
|
|
61
55
|
/** Injectable probes + cache IO for tests (default to the real ssh/local/disk ones). */
|
|
@@ -37,17 +37,10 @@ const CACHE_FILE = '.fleet-stats.json';
|
|
|
37
37
|
* rewrites the cache, unreachable boxes included).
|
|
38
38
|
*/
|
|
39
39
|
export const STATS_STALE_MS = 3 * 60_000;
|
|
40
|
-
/** Static hardware totals are valid for seven days. */
|
|
41
|
-
export const SPECS_STALE_MS = 7 * 24 * 60 * 60_000;
|
|
42
40
|
/** True when a cached row is still within {@link STATS_STALE_MS}. */
|
|
43
41
|
export function isFreshDeviceStats(stats, now = Date.now()) {
|
|
44
42
|
return now - stats.fetchedAt <= STATS_STALE_MS;
|
|
45
43
|
}
|
|
46
|
-
/** True when cached core, RAM-total, and disk-total facts remain current. */
|
|
47
|
-
export function isFreshDeviceSpecs(stats, now = Date.now()) {
|
|
48
|
-
const fetchedAt = stats.specsFetchedAt ?? stats.fetchedAt;
|
|
49
|
-
return now - fetchedAt <= SPECS_STALE_MS;
|
|
50
|
-
}
|
|
51
44
|
/**
|
|
52
45
|
* Carry a device's last successfully-probed hardware facts onto a row whose
|
|
53
46
|
* probe just came back unreachable (RUSH-3096).
|
|
@@ -141,13 +134,13 @@ export async function loadFleetStats(devices, opts = {}) {
|
|
|
141
134
|
const toProbe = [];
|
|
142
135
|
let servedFromCache = false;
|
|
143
136
|
for (const d of devices) {
|
|
144
|
-
if (d.name === self
|
|
137
|
+
if (d.name === self) {
|
|
145
138
|
// This machine is always probed locally — cheap, no ssh, always live.
|
|
146
139
|
toProbe.push(d);
|
|
147
140
|
continue;
|
|
148
141
|
}
|
|
149
142
|
const cached = opts.forceRefresh ? undefined : cache[d.name];
|
|
150
|
-
const cacheFresh = cached &&
|
|
143
|
+
const cacheFresh = cached && isFreshDeviceStats(cached, now);
|
|
151
144
|
if (cached && cacheFresh) {
|
|
152
145
|
stats.set(d.name, cached);
|
|
153
146
|
servedFromCache = true;
|
|
@@ -82,6 +82,20 @@ export interface VersionResourceReport {
|
|
|
82
82
|
* probe), not the pure diff — undefined when uncomputed. */
|
|
83
83
|
sourceBehind?: SourceLayerBehind[];
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* True when `filePath` is a git symlink that was CHECKED OUT AS A PLAIN TEXT
|
|
87
|
+
* FILE — the shape git produces on a client without symlink support (Windows
|
|
88
|
+
* without Developer Mode, `core.symlinks=false`). Such a file holds exactly the
|
|
89
|
+
* link target (a relative path, no trailing newline) instead of the pointed-to
|
|
90
|
+
* content. `lstat().isSymbolicLink()` is FALSE for it, so callers that only
|
|
91
|
+
* skip real symlinks (e.g. the rules/permissions alias files CLAUDE.md /
|
|
92
|
+
* GEMINI.md, which are symlinks to AGENTS.md in the repo) wrongly treat it as
|
|
93
|
+
* an independent resource that no sync can ever reconcile (PHNX-3187). The
|
|
94
|
+
* signal is unambiguous: a whole file with no newline whose entire content
|
|
95
|
+
* resolves to an existing sibling path. Real markdown rule files always contain
|
|
96
|
+
* newlines, so this never false-positives on genuine content.
|
|
97
|
+
*/
|
|
98
|
+
export declare function isCheckedOutSymlink(filePath: string): boolean;
|
|
85
99
|
/**
|
|
86
100
|
* Describe how a version's marketplace MIRROR of a plugin diverges from its
|
|
87
101
|
* central source — the detail presence-only checks miss. Surfaces a stale mirror
|
package/dist/lib/doctor-diff.js
CHANGED
|
@@ -29,9 +29,11 @@ import { discoverPlugins, marketplaceSpecForName } from './plugins/plugins.js';
|
|
|
29
29
|
import { pluginInstallDir, repairableManifestFields } from './plugins/plugin-marketplace.js';
|
|
30
30
|
import { markdownToToml } from './convert.js';
|
|
31
31
|
import { listCommandsInVersionHome, getVersionCommandsDir, listPluginCommandNames } from './commands.js';
|
|
32
|
-
import { shouldInstallCommandAsSkill, commandSkillMatches, commandSkillName } from './command-skills.js';
|
|
32
|
+
import { shouldInstallCommandAsSkill, commandSkillMatches, commandSkillName, skillSourceExists, readSkillSourceCommandMarker } from './command-skills.js';
|
|
33
|
+
import { trustedSkillRoots } from './staleness/writers/sources.js';
|
|
33
34
|
import { gooseCommandMatches, gooseCommandsDir } from './goose-commands.js';
|
|
34
35
|
import { supports } from './capabilities.js';
|
|
36
|
+
import { isDirectoryDoc } from './resources.js';
|
|
35
37
|
import { listSkillsInVersionHome, getVersionSkillsDir } from './plugins/skills.js';
|
|
36
38
|
import { listHookEntriesFromDir } from './hooks/install.js';
|
|
37
39
|
import { getResourceInventory } from './resource-inventory.js';
|
|
@@ -61,6 +63,42 @@ function readSafe(file) {
|
|
|
61
63
|
function fileExists(p) {
|
|
62
64
|
return !!p && fs.existsSync(p) && !fs.lstatSync(p).isSymbolicLink();
|
|
63
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* True when `filePath` is a git symlink that was CHECKED OUT AS A PLAIN TEXT
|
|
68
|
+
* FILE — the shape git produces on a client without symlink support (Windows
|
|
69
|
+
* without Developer Mode, `core.symlinks=false`). Such a file holds exactly the
|
|
70
|
+
* link target (a relative path, no trailing newline) instead of the pointed-to
|
|
71
|
+
* content. `lstat().isSymbolicLink()` is FALSE for it, so callers that only
|
|
72
|
+
* skip real symlinks (e.g. the rules/permissions alias files CLAUDE.md /
|
|
73
|
+
* GEMINI.md, which are symlinks to AGENTS.md in the repo) wrongly treat it as
|
|
74
|
+
* an independent resource that no sync can ever reconcile (PHNX-3187). The
|
|
75
|
+
* signal is unambiguous: a whole file with no newline whose entire content
|
|
76
|
+
* resolves to an existing sibling path. Real markdown rule files always contain
|
|
77
|
+
* newlines, so this never false-positives on genuine content.
|
|
78
|
+
*/
|
|
79
|
+
export function isCheckedOutSymlink(filePath) {
|
|
80
|
+
let content;
|
|
81
|
+
try {
|
|
82
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
// A git symlink blob is the bare target with no newline; any newline means
|
|
88
|
+
// this is real file content, not a link.
|
|
89
|
+
if (content.length === 0 || content.length > 255 || /[\r\n]/.test(content))
|
|
90
|
+
return false;
|
|
91
|
+
const target = content.trim();
|
|
92
|
+
if (!target)
|
|
93
|
+
return false;
|
|
94
|
+
const resolved = path.resolve(path.dirname(filePath), target);
|
|
95
|
+
try {
|
|
96
|
+
return fs.statSync(resolved).isFile();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
64
102
|
function findFirst(candidates) {
|
|
65
103
|
for (const c of candidates) {
|
|
66
104
|
if (fileExists(c.path) || (fs.existsSync(c.path) && fs.lstatSync(c.path).isDirectory())) {
|
|
@@ -115,16 +153,46 @@ function diffCommands(agent, version, cwd, excludeProject = false) {
|
|
|
115
153
|
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
116
154
|
continue;
|
|
117
155
|
const name = entry.name.replace(/\.md$/, '');
|
|
156
|
+
// Directory docs (README/AGENTS/CLAUDE/GEMINI) live in commands/ but are
|
|
157
|
+
// documentation, not commands. `discoverCommands` and `resolveResource`
|
|
158
|
+
// both refuse them, so the sync writer never installs them — mirror that
|
|
159
|
+
// here or every one false-reports as a missing command no sync can clear.
|
|
160
|
+
if (isDirectoryDoc('commands', name))
|
|
161
|
+
continue;
|
|
118
162
|
if (sourceByName.has(name))
|
|
119
163
|
continue;
|
|
120
164
|
sourceByName.set(name, { layer: base.layer, path: path.join(base.path, entry.name), alias: base.alias });
|
|
121
165
|
}
|
|
122
166
|
}
|
|
167
|
+
// The trusted skill roots the commands-as-skills writer consults to decide
|
|
168
|
+
// whether a real skill of the same name already owns a command's target slot.
|
|
169
|
+
const skillRoots = asSkill ? trustedSkillRoots() : [];
|
|
123
170
|
const rows = [];
|
|
124
171
|
const seen = new Set();
|
|
125
172
|
for (const [name, src] of sourceByName) {
|
|
126
173
|
seen.add(name);
|
|
127
174
|
if (!installed.has(name)) {
|
|
175
|
+
// Command-as-skill agents (codex >= 0.117, kimi): when a real skill of the
|
|
176
|
+
// same name exists in the sources, the skill wins the shared
|
|
177
|
+
// `skills/<name>/` slot and `installCommandSkillToVersion` deliberately
|
|
178
|
+
// writes no command wrapper (versions.ts keeps the real skill in
|
|
179
|
+
// skillsToSync and overwrites any wrapper). The command's behavior is
|
|
180
|
+
// provided by that same-named skill, so it is NOT missing drift — reporting
|
|
181
|
+
// it so drove an unclearable "N missing" loop (PHNX-3186). Mirror the
|
|
182
|
+
// writer's skip predicate exactly: a skill source of this name whose
|
|
183
|
+
// `agents_command` marker is not this command (a real skill, or a different
|
|
184
|
+
// command).
|
|
185
|
+
if (asSkill && skillSourceExists(name, skillRoots) && readSkillSourceCommandMarker(name, skillRoots) !== name) {
|
|
186
|
+
rows.push({
|
|
187
|
+
kind: 'commands',
|
|
188
|
+
name,
|
|
189
|
+
status: 'ok',
|
|
190
|
+
source: src.layer,
|
|
191
|
+
sourcePath: src.path,
|
|
192
|
+
detail: 'provided by same-named skill',
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
128
196
|
rows.push({ kind: 'commands', name, status: 'missing', source: src.layer, sourcePath: src.path });
|
|
129
197
|
continue;
|
|
130
198
|
}
|
|
@@ -180,6 +248,11 @@ function diffCommands(agent, version, cwd, excludeProject = false) {
|
|
|
180
248
|
for (const name of installed) {
|
|
181
249
|
if (seen.has(name))
|
|
182
250
|
continue;
|
|
251
|
+
// Directory docs are excluded from the source scan above; a leftover copy in
|
|
252
|
+
// the home is not a command orphan — leave it out of the command diff
|
|
253
|
+
// entirely rather than flip it to a spurious `extra` row.
|
|
254
|
+
if (isDirectoryDoc('commands', name))
|
|
255
|
+
continue;
|
|
183
256
|
if (pluginCommands.has(name))
|
|
184
257
|
continue;
|
|
185
258
|
const extraHome = asSkill
|
|
@@ -382,8 +455,13 @@ function listRulesNames(cwd, excludeProject = false) {
|
|
|
382
455
|
for (const file of entries) {
|
|
383
456
|
if (!file.endsWith('.md') || file === RULES_DOC_FILENAME)
|
|
384
457
|
continue;
|
|
385
|
-
const
|
|
386
|
-
|
|
458
|
+
const filePath = path.join(base.path, file);
|
|
459
|
+
const stat = fs.lstatSync(filePath);
|
|
460
|
+
// Skip the CLAUDE.md / GEMINI.md alias symlinks — both when they are real
|
|
461
|
+
// symlinks (posix) and when git checked them out as plain text files
|
|
462
|
+
// (Windows), so they are never mistaken for independent rule sources the
|
|
463
|
+
// writer can't produce (PHNX-3187).
|
|
464
|
+
if (stat.isSymbolicLink() || isCheckedOutSymlink(filePath))
|
|
387
465
|
continue;
|
|
388
466
|
const name = file.replace(/\.md$/, '');
|
|
389
467
|
if (out.has(name))
|
|
@@ -421,11 +499,15 @@ function diffRules(agent, version, cwd, excludeProject = false) {
|
|
|
421
499
|
const versionHome = getVersionHomePath(agent, version);
|
|
422
500
|
const configDir = path.join(versionHome, agentConfigDirName(agent));
|
|
423
501
|
const sourcesByName = listRulesNames(cwd, excludeProject);
|
|
424
|
-
// Files actually present in the version home.
|
|
502
|
+
// Files actually present in the version home. Include *.md siblings AND the
|
|
503
|
+
// agent's own instructions filename even when it is not *.md — cursor's rules
|
|
504
|
+
// file is `.cursorrules`, so an `.md`-only scan never saw it and reported the
|
|
505
|
+
// AGENTS rule `missing` on every sync, a phantom no reconcile could clear
|
|
506
|
+
// (PHNX-3186).
|
|
425
507
|
const homeFiles = new Set();
|
|
426
508
|
if (fs.existsSync(configDir)) {
|
|
427
509
|
for (const f of fs.readdirSync(configDir)) {
|
|
428
|
-
if (!f.endsWith('.md'))
|
|
510
|
+
if (!f.endsWith('.md') && f !== agentConfig.instructionsFile)
|
|
429
511
|
continue;
|
|
430
512
|
homeFiles.add(f);
|
|
431
513
|
}
|
|
@@ -537,14 +619,38 @@ export function describePluginDrift(central, mirrorDir) {
|
|
|
537
619
|
parts.push(`${mVer}→${cVer}`);
|
|
538
620
|
if (mManifest && repairableManifestFields(mManifest).length > 0)
|
|
539
621
|
parts.push('invalid manifest');
|
|
622
|
+
const centralSkills = listPluginSkillDirs(central.root);
|
|
540
623
|
const mirrorSkills = new Set(listPluginSkillDirs(mirrorDir));
|
|
541
|
-
const missSkills =
|
|
624
|
+
const missSkills = centralSkills.filter((s) => !mirrorSkills.has(s)).sort();
|
|
625
|
+
const centralCmds = listPluginCommandFiles(central.root);
|
|
542
626
|
const mirrorCmds = new Set(listPluginCommandFiles(mirrorDir));
|
|
543
|
-
const missCmds =
|
|
627
|
+
const missCmds = centralCmds.filter((c) => !mirrorCmds.has(c)).sort();
|
|
544
628
|
if (missSkills.length)
|
|
545
629
|
parts.push(`missing skill${missSkills.length > 1 ? 's' : ''}: ${missSkills.join(', ')}`);
|
|
546
630
|
if (missCmds.length)
|
|
547
631
|
parts.push(`missing command${missCmds.length > 1 ? 's' : ''}: ${missCmds.join(', ')}`);
|
|
632
|
+
// Content drift of a skill/command that exists in BOTH — the mirror kept the
|
|
633
|
+
// dir/file but its bytes went stale (PHNX-2955: a skill edit landed on origin
|
|
634
|
+
// and was pulled into central, but the per-version marketplace copy was never
|
|
635
|
+
// refreshed, so agents kept executing the OLD skill text while `plugins list`
|
|
636
|
+
// and `doctor` reported it `everywhere`/`ok`). Presence alone missed this; a
|
|
637
|
+
// content compare is what turns a stale mirror into a reportable `diff`.
|
|
638
|
+
const staleSkills = centralSkills
|
|
639
|
+
.filter((s) => mirrorSkills.has(s))
|
|
640
|
+
.filter((s) => !dirsContentMatch(path.join(central.root, 'skills', s), path.join(mirrorDir, 'skills', s)))
|
|
641
|
+
.sort();
|
|
642
|
+
const staleCmds = centralCmds
|
|
643
|
+
.filter((c) => mirrorCmds.has(c))
|
|
644
|
+
.filter((c) => {
|
|
645
|
+
const a = readSafe(path.join(central.root, 'commands', `${c}.md`));
|
|
646
|
+
const b = readSafe(path.join(mirrorDir, 'commands', `${c}.md`));
|
|
647
|
+
return a == null || b == null || normalize(a) !== normalize(b);
|
|
648
|
+
})
|
|
649
|
+
.sort();
|
|
650
|
+
if (staleSkills.length)
|
|
651
|
+
parts.push(`stale skill${staleSkills.length > 1 ? 's' : ''}: ${staleSkills.join(', ')}`);
|
|
652
|
+
if (staleCmds.length)
|
|
653
|
+
parts.push(`stale command${staleCmds.length > 1 ? 's' : ''}: ${staleCmds.join(', ')}`);
|
|
548
654
|
return parts.length ? parts.join(', ') : null;
|
|
549
655
|
}
|
|
550
656
|
function diffPlugins(agent, version, cwd) {
|
|
@@ -622,8 +728,13 @@ export function diffVersionResources(agent, version, options = {}) {
|
|
|
622
728
|
empty.mcp = diffPresenceOnly('mcp', available.mcp, synced.mcp);
|
|
623
729
|
if (requested.has('permissions'))
|
|
624
730
|
empty.permissions = diffPresenceOnly('permissions', available.permissions, synced.permissions);
|
|
625
|
-
|
|
626
|
-
|
|
731
|
+
// Subagents are version-gated (e.g. kimi >= 0.29.0). A version below the floor
|
|
732
|
+
// is never written any subagent by the sync writer, so counting the source
|
|
733
|
+
// ones as "missing" is phantom drift no sync can clear (PHNX-3186). Zero the
|
|
734
|
+
// available set when unsupported; a stale installed copy still surfaces `extra`.
|
|
735
|
+
if (requested.has('subagents')) {
|
|
736
|
+
empty.subagents = diffPresenceOnly('subagents', supports(agent, 'subagents', version).ok ? available.subagents : [], synced.subagents);
|
|
737
|
+
}
|
|
627
738
|
if (requested.has('plugins'))
|
|
628
739
|
empty.plugins = diffPlugins(agent, version, cwd);
|
|
629
740
|
if (requested.has('promptcuts'))
|
|
@@ -33,3 +33,20 @@ export interface ResolveContext {
|
|
|
33
33
|
* sync scopes, and `login: skip` are still returned (probe/report only).
|
|
34
34
|
*/
|
|
35
35
|
export declare function resolveDesired(manifest: FleetManifest, ctx: ResolveContext): DeviceDesired[];
|
|
36
|
+
/**
|
|
37
|
+
* The message to print when a reconcile resolved zero target devices.
|
|
38
|
+
*
|
|
39
|
+
* An explicitly empty roster (`fleet.devices: {}` — the default on a fresh box)
|
|
40
|
+
* is the ACTIONABLE case: the engine ran fine, there is simply nothing declared
|
|
41
|
+
* to converge, so name the empty key and how to fill it. A gray "nothing to
|
|
42
|
+
* apply" there reads like the command is dead — the exact confusion that made
|
|
43
|
+
* `apply` look like a broken command during the RUSH-2981 surface review
|
|
44
|
+
* (PHNX-3422). Any OTHER zero-target case (a `devices: all` fleet with no online
|
|
45
|
+
* peers, or an explicit roster whose every name was unresolved — off-tailnet,
|
|
46
|
+
* ignored, or a typo, the only names `resolveDesired` drops) already had its
|
|
47
|
+
* reason surfaced above, so it stays a plain note.
|
|
48
|
+
*/
|
|
49
|
+
export declare function emptyTargetsMessage(manifest: FleetManifest): {
|
|
50
|
+
style: 'hint' | 'plain';
|
|
51
|
+
lines: string[];
|
|
52
|
+
};
|
|
@@ -169,3 +169,29 @@ export function resolveDesired(manifest, ctx) {
|
|
|
169
169
|
}
|
|
170
170
|
return out;
|
|
171
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* The message to print when a reconcile resolved zero target devices.
|
|
174
|
+
*
|
|
175
|
+
* An explicitly empty roster (`fleet.devices: {}` — the default on a fresh box)
|
|
176
|
+
* is the ACTIONABLE case: the engine ran fine, there is simply nothing declared
|
|
177
|
+
* to converge, so name the empty key and how to fill it. A gray "nothing to
|
|
178
|
+
* apply" there reads like the command is dead — the exact confusion that made
|
|
179
|
+
* `apply` look like a broken command during the RUSH-2981 surface review
|
|
180
|
+
* (PHNX-3422). Any OTHER zero-target case (a `devices: all` fleet with no online
|
|
181
|
+
* peers, or an explicit roster whose every name was unresolved — off-tailnet,
|
|
182
|
+
* ignored, or a typo, the only names `resolveDesired` drops) already had its
|
|
183
|
+
* reason surfaced above, so it stays a plain note.
|
|
184
|
+
*/
|
|
185
|
+
export function emptyTargetsMessage(manifest) {
|
|
186
|
+
const rosterEmpty = manifest.devices !== 'all' && Object.keys(manifest.devices).length === 0;
|
|
187
|
+
if (rosterEmpty) {
|
|
188
|
+
return {
|
|
189
|
+
style: 'hint',
|
|
190
|
+
lines: [
|
|
191
|
+
'fleet.devices is empty — nothing to converge.',
|
|
192
|
+
'Declare a roster in agents.yaml: `fleet: { devices: all }` for every online box, or name them (`fleet: { devices: { <name>: {} } }`), then re-run.',
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return { style: 'plain', lines: ['No target devices — nothing to apply.'] };
|
|
197
|
+
}
|
package/dist/lib/git.d.ts
CHANGED
|
@@ -161,6 +161,21 @@ export declare function canonicalGitRemote(url: string): string;
|
|
|
161
161
|
* checkout; {@link isSystemRepoOrigin} reads a dir's origin and delegates here.
|
|
162
162
|
*/
|
|
163
163
|
export declare function isSystemRepoRemote(remote: string | null | undefined): boolean;
|
|
164
|
+
/**
|
|
165
|
+
* True when `remote` is the origin the system repo is EXPECTED to track on this
|
|
166
|
+
* machine, honouring an operator's `AGENTS_SYSTEM_REPO` override.
|
|
167
|
+
*
|
|
168
|
+
* The system repo ships hooks that register as shell `command` strings run on
|
|
169
|
+
* every tool event, and its checkout auto-fast-forwards from origin — so a
|
|
170
|
+
* fast-forward from an origin the operator never chose is remote code execution
|
|
171
|
+
* on the next command that loads a system resource (PHNX-2957). This is the
|
|
172
|
+
* pinning predicate every auto-pull of the system repo gates on: pull only when
|
|
173
|
+
* origin is the canonical {@link isSystemRepoRemote} repo, or the exact
|
|
174
|
+
* `AGENTS_SYSTEM_REPO` the operator pointed at instead. Anything else — a
|
|
175
|
+
* repointed origin, a fork, an unset-then-swapped remote — is refused, not
|
|
176
|
+
* pulled. Pure string check; no git spawn.
|
|
177
|
+
*/
|
|
178
|
+
export declare function isExpectedSystemRepoRemote(remote: string | null | undefined): boolean;
|
|
164
179
|
/** True when two git remote URLs point at the same repo across transport forms. */
|
|
165
180
|
export declare function sameGitRemote(a: string | null | undefined, b: string | null | undefined): boolean;
|
|
166
181
|
/**
|
|
@@ -500,6 +515,29 @@ export declare function tryAutoPull(dir: string): Promise<{
|
|
|
500
515
|
pulled: boolean;
|
|
501
516
|
error?: string;
|
|
502
517
|
}>;
|
|
518
|
+
/** Result of {@link tryAutoPullSystemRepo}. `refused` is set only when the pull
|
|
519
|
+
* was blocked because origin is not the expected system remote. */
|
|
520
|
+
export interface SystemRepoPullResult {
|
|
521
|
+
pulled: boolean;
|
|
522
|
+
error?: string;
|
|
523
|
+
/** True when origin is present but is NOT the expected system remote; no
|
|
524
|
+
* fast-forward was attempted. `actualRemote` names what was found. */
|
|
525
|
+
refused?: boolean;
|
|
526
|
+
/** The origin fetch URL that was examined (present when a remote exists). */
|
|
527
|
+
actualRemote?: string;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Auto-pull the system repo ONLY after verifying its origin is the expected
|
|
531
|
+
* system remote (PHNX-2957). The system repo ships hooks that run as shell on
|
|
532
|
+
* tool events, so fast-forwarding it from an unexpected/repointed origin is
|
|
533
|
+
* remote code execution. An origin that fails {@link isExpectedSystemRepoRemote}
|
|
534
|
+
* is REFUSED loud (`refused: true`), never pulled — the canonical system repo
|
|
535
|
+
* (or an operator's `AGENTS_SYSTEM_REPO`) still fast-forwards exactly as before.
|
|
536
|
+
*
|
|
537
|
+
* A system dir with no origin at all is a plain no-op (`pulled: false`), not a
|
|
538
|
+
* refusal — there is nothing to pull from and nothing to distrust.
|
|
539
|
+
*/
|
|
540
|
+
export declare function tryAutoPullSystemRepo(dir: string): Promise<SystemRepoPullResult>;
|
|
503
541
|
/**
|
|
504
542
|
* How many commits `dir`'s checked-out branch is behind its upstream, read from
|
|
505
543
|
* the LAST-FETCHED remote-tracking ref — no network call. Returns null when the
|
package/dist/lib/git.js
CHANGED
|
@@ -537,6 +537,33 @@ export function isSystemRepoRemote(remote) {
|
|
|
537
537
|
const c = canonicalGitRemote(remote);
|
|
538
538
|
return c === canonicalGitRemote(`https://github.com/${systemRepoSlug(DEFAULT_SYSTEM_REPO)}`);
|
|
539
539
|
}
|
|
540
|
+
/**
|
|
541
|
+
* True when `remote` is the origin the system repo is EXPECTED to track on this
|
|
542
|
+
* machine, honouring an operator's `AGENTS_SYSTEM_REPO` override.
|
|
543
|
+
*
|
|
544
|
+
* The system repo ships hooks that register as shell `command` strings run on
|
|
545
|
+
* every tool event, and its checkout auto-fast-forwards from origin — so a
|
|
546
|
+
* fast-forward from an origin the operator never chose is remote code execution
|
|
547
|
+
* on the next command that loads a system resource (PHNX-2957). This is the
|
|
548
|
+
* pinning predicate every auto-pull of the system repo gates on: pull only when
|
|
549
|
+
* origin is the canonical {@link isSystemRepoRemote} repo, or the exact
|
|
550
|
+
* `AGENTS_SYSTEM_REPO` the operator pointed at instead. Anything else — a
|
|
551
|
+
* repointed origin, a fork, an unset-then-swapped remote — is refused, not
|
|
552
|
+
* pulled. Pure string check; no git spawn.
|
|
553
|
+
*/
|
|
554
|
+
export function isExpectedSystemRepoRemote(remote) {
|
|
555
|
+
if (!remote)
|
|
556
|
+
return false;
|
|
557
|
+
const override = process.env.AGENTS_SYSTEM_REPO?.trim();
|
|
558
|
+
if (override) {
|
|
559
|
+
// The override is a source spec (`gh:owner/repo`) or a full clone URL. Match
|
|
560
|
+
// the GitHub-slug form the setup path clones, and the raw spec itself, so a
|
|
561
|
+
// non-GitHub override URL still verifies.
|
|
562
|
+
return (sameGitRemote(remote, `https://github.com/${systemRepoSlug(override)}`) ||
|
|
563
|
+
sameGitRemote(remote, override.replace(/^gh:/, '')));
|
|
564
|
+
}
|
|
565
|
+
return isSystemRepoRemote(remote);
|
|
566
|
+
}
|
|
540
567
|
/** True when two git remote URLs point at the same repo across transport forms. */
|
|
541
568
|
export function sameGitRemote(a, b) {
|
|
542
569
|
if (!a || !b)
|
|
@@ -1665,6 +1692,37 @@ export async function tryAutoPull(dir) {
|
|
|
1665
1692
|
return { pulled: false, error: err.message };
|
|
1666
1693
|
}
|
|
1667
1694
|
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Auto-pull the system repo ONLY after verifying its origin is the expected
|
|
1697
|
+
* system remote (PHNX-2957). The system repo ships hooks that run as shell on
|
|
1698
|
+
* tool events, so fast-forwarding it from an unexpected/repointed origin is
|
|
1699
|
+
* remote code execution. An origin that fails {@link isExpectedSystemRepoRemote}
|
|
1700
|
+
* is REFUSED loud (`refused: true`), never pulled — the canonical system repo
|
|
1701
|
+
* (or an operator's `AGENTS_SYSTEM_REPO`) still fast-forwards exactly as before.
|
|
1702
|
+
*
|
|
1703
|
+
* A system dir with no origin at all is a plain no-op (`pulled: false`), not a
|
|
1704
|
+
* refusal — there is nothing to pull from and nothing to distrust.
|
|
1705
|
+
*/
|
|
1706
|
+
export async function tryAutoPullSystemRepo(dir) {
|
|
1707
|
+
if (!isGitRepo(dir))
|
|
1708
|
+
return { pulled: false };
|
|
1709
|
+
let remote;
|
|
1710
|
+
try {
|
|
1711
|
+
const git = simpleGit(dir);
|
|
1712
|
+
const remotes = await git.getRemotes(true);
|
|
1713
|
+
remote = remotes.find(r => r.name === 'origin')?.refs?.fetch;
|
|
1714
|
+
}
|
|
1715
|
+
catch {
|
|
1716
|
+
return { pulled: false };
|
|
1717
|
+
}
|
|
1718
|
+
if (!remote)
|
|
1719
|
+
return { pulled: false };
|
|
1720
|
+
if (!isExpectedSystemRepoRemote(remote)) {
|
|
1721
|
+
return { pulled: false, refused: true, actualRemote: remote };
|
|
1722
|
+
}
|
|
1723
|
+
const res = await tryAutoPull(dir);
|
|
1724
|
+
return { ...res, actualRemote: remote };
|
|
1725
|
+
}
|
|
1668
1726
|
/**
|
|
1669
1727
|
* How many commits `dir`'s checked-out branch is behind its upstream, read from
|
|
1670
1728
|
* the LAST-FETCHED remote-tracking ref — no network call. Returns null when the
|
|
@@ -334,21 +334,37 @@ export declare function parseHookManifest(opts?: {
|
|
|
334
334
|
}): Record<string, ManifestHook>;
|
|
335
335
|
export declare function selectHookManifest(manifest: Record<string, ManifestHook>, selected: string[]): Record<string, ManifestHook>;
|
|
336
336
|
/**
|
|
337
|
-
* Hook
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
337
|
+
* Hook files present in a version home but absent from every configured
|
|
338
|
+
* SOURCE — genuine orphans left behind by a removed/renamed source hook.
|
|
339
|
+
*
|
|
340
|
+
* The definition is deliberately source-based, not manifest-based (PHNX-2693).
|
|
341
|
+
* Sync copies EVERY source hook file into a version home — registered hooks AND
|
|
342
|
+
* the helper / test / benchmark scripts that sit alongside them — but only the
|
|
343
|
+
* registered ones appear in `parseHookManifest`. Diffing installed names against
|
|
344
|
+
* the manifest therefore flagged every source-present-but-unregistered file
|
|
345
|
+
* (e.g. `permission-handler`, `verify-work-state`, `*_test`, `benchmark_*`) as
|
|
346
|
+
* an orphan, so `agents prune cleanup` offered to trash ~1000 in-use files. An
|
|
347
|
+
* installed hook is only truly orphaned when NO configured source still carries
|
|
348
|
+
* a file of that name — which is exactly what the `prune`/`doctor` help already
|
|
349
|
+
* promised ("present in a version home but missing from every configured
|
|
350
|
+
* source").
|
|
343
351
|
*
|
|
344
352
|
* Pure on purpose (no disk reads) so it is trivially testable; callers pass the
|
|
345
|
-
* installed hook names and the
|
|
353
|
+
* installed hook names and the source hook script paths.
|
|
354
|
+
*/
|
|
355
|
+
export declare function unmanagedHookNames(installedHookNames: string[], sourceHookScripts: string[]): string[];
|
|
356
|
+
/**
|
|
357
|
+
* Every hook script path across the resolved SOURCE roots — user
|
|
358
|
+
* (`~/.agents/hooks`), system (`~/.agents/.system/hooks`), and each enabled
|
|
359
|
+
* extra repo's `hooks/` — grouped the same way sync materializes them
|
|
360
|
+
* ({@link listHookEntriesFromDir}, which also descends one-level event-group
|
|
361
|
+
* dirs). This is the set an installed hook must be absent from to count as an
|
|
362
|
+
* orphan.
|
|
346
363
|
*/
|
|
347
|
-
export declare function
|
|
364
|
+
export declare function listResolvedSourceHookScripts(): string[];
|
|
348
365
|
/**
|
|
349
|
-
* The
|
|
350
|
-
*
|
|
351
|
-
* shadow/override warnings the registrar path prints.
|
|
366
|
+
* The orphan hooks (see {@link unmanagedHookNames}) sitting in one version home:
|
|
367
|
+
* installed hook files whose name matches no file in any configured source root.
|
|
352
368
|
*/
|
|
353
369
|
export declare function listUnmanagedHooksInVersionHome(agent: AgentId, version: string): string[];
|
|
354
370
|
/**
|
|
@@ -1594,33 +1594,58 @@ export function selectHookManifest(manifest, selected) {
|
|
|
1594
1594
|
return Object.fromEntries(Object.entries(manifest).filter(([name, hook]) => selectedHooks.has(name) || selectedHooks.has(path.basename(hook.script))));
|
|
1595
1595
|
}
|
|
1596
1596
|
/**
|
|
1597
|
-
* Hook
|
|
1598
|
-
*
|
|
1599
|
-
*
|
|
1600
|
-
*
|
|
1601
|
-
*
|
|
1602
|
-
*
|
|
1597
|
+
* Hook files present in a version home but absent from every configured
|
|
1598
|
+
* SOURCE — genuine orphans left behind by a removed/renamed source hook.
|
|
1599
|
+
*
|
|
1600
|
+
* The definition is deliberately source-based, not manifest-based (PHNX-2693).
|
|
1601
|
+
* Sync copies EVERY source hook file into a version home — registered hooks AND
|
|
1602
|
+
* the helper / test / benchmark scripts that sit alongside them — but only the
|
|
1603
|
+
* registered ones appear in `parseHookManifest`. Diffing installed names against
|
|
1604
|
+
* the manifest therefore flagged every source-present-but-unregistered file
|
|
1605
|
+
* (e.g. `permission-handler`, `verify-work-state`, `*_test`, `benchmark_*`) as
|
|
1606
|
+
* an orphan, so `agents prune cleanup` offered to trash ~1000 in-use files. An
|
|
1607
|
+
* installed hook is only truly orphaned when NO configured source still carries
|
|
1608
|
+
* a file of that name — which is exactly what the `prune`/`doctor` help already
|
|
1609
|
+
* promised ("present in a version home but missing from every configured
|
|
1610
|
+
* source").
|
|
1603
1611
|
*
|
|
1604
1612
|
* Pure on purpose (no disk reads) so it is trivially testable; callers pass the
|
|
1605
|
-
* installed hook names and the
|
|
1613
|
+
* installed hook names and the source hook script paths.
|
|
1614
|
+
*/
|
|
1615
|
+
export function unmanagedHookNames(installedHookNames, sourceHookScripts) {
|
|
1616
|
+
const inSource = new Set(sourceHookScripts.map((s) => path.basename(s).replace(/\.[^.]+$/, '')));
|
|
1617
|
+
return installedHookNames.filter((name) => !inSource.has(name)).sort();
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Every hook script path across the resolved SOURCE roots — user
|
|
1621
|
+
* (`~/.agents/hooks`), system (`~/.agents/.system/hooks`), and each enabled
|
|
1622
|
+
* extra repo's `hooks/` — grouped the same way sync materializes them
|
|
1623
|
+
* ({@link listHookEntriesFromDir}, which also descends one-level event-group
|
|
1624
|
+
* dirs). This is the set an installed hook must be absent from to count as an
|
|
1625
|
+
* orphan.
|
|
1606
1626
|
*/
|
|
1607
|
-
export function
|
|
1608
|
-
const
|
|
1609
|
-
|
|
1627
|
+
export function listResolvedSourceHookScripts() {
|
|
1628
|
+
const roots = [
|
|
1629
|
+
getUserHooksDir(),
|
|
1630
|
+
getSystemHooksDir(),
|
|
1631
|
+
...getEnabledExtraRepos().map((e) => path.join(e.dir, 'hooks')),
|
|
1632
|
+
];
|
|
1633
|
+
const scripts = [];
|
|
1634
|
+
for (const root of roots) {
|
|
1635
|
+
for (const entry of listHookEntriesFromDir(root))
|
|
1636
|
+
scripts.push(entry.scriptPath);
|
|
1637
|
+
}
|
|
1638
|
+
return scripts;
|
|
1610
1639
|
}
|
|
1611
1640
|
/**
|
|
1612
|
-
* The
|
|
1613
|
-
*
|
|
1614
|
-
* shadow/override warnings the registrar path prints.
|
|
1641
|
+
* The orphan hooks (see {@link unmanagedHookNames}) sitting in one version home:
|
|
1642
|
+
* installed hook files whose name matches no file in any configured source root.
|
|
1615
1643
|
*/
|
|
1616
1644
|
export function listUnmanagedHooksInVersionHome(agent, version) {
|
|
1617
1645
|
if (!AGENTS[agent].supportsHooks)
|
|
1618
1646
|
return [];
|
|
1619
|
-
const scripts = Object.values(parseHookManifest({ warn: false }))
|
|
1620
|
-
.map((h) => h.script)
|
|
1621
|
-
.filter((s) => typeof s === 'string');
|
|
1622
1647
|
const installed = listHooksInVersionHome(agent, version).map((e) => e.name);
|
|
1623
|
-
return unmanagedHookNames(installed,
|
|
1648
|
+
return unmanagedHookNames(installed, listResolvedSourceHookScripts());
|
|
1624
1649
|
}
|
|
1625
1650
|
// Codex events that support a matcher field (matches tool name or session type).
|
|
1626
1651
|
// UserPromptSubmit and Stop never include a matcher.
|
|
@@ -100,6 +100,14 @@ export interface ViewAgentAccountEligibility {
|
|
|
100
100
|
* --json`. A picker may route to a signed-out version because launching it is
|
|
101
101
|
* the login flow, but it must not route to a device whose every signed-in
|
|
102
102
|
* account is throttled.
|
|
103
|
+
*
|
|
104
|
+
* The sign-in gate reads the per-version `launchable` field — the strict
|
|
105
|
+
* per-version launch truth (`isLaunchableSignedIn`) — so a remote box is judged
|
|
106
|
+
* by the SAME launchability the local candidate uses (`collectRunCandidates` →
|
|
107
|
+
* `isLaunchableSignedIn`), not the display `signedIn` that inherits the
|
|
108
|
+
* active/global HOME login and passes a box that dies at spawn (PHNX-3466). An
|
|
109
|
+
* older remote CLI omits `launchable`, so it falls back to `signedIn` — the
|
|
110
|
+
* pre-fix behavior, so a rolling fleet does not regress.
|
|
103
111
|
*/
|
|
104
112
|
export declare function viewAgentAccountEligibility(view: string, agent: string): ViewAgentAccountEligibility;
|
|
105
113
|
export declare function viewAgentSignedIn(view: string, agent: string): boolean | undefined;
|
package/dist/lib/hosts/ready.js
CHANGED
|
@@ -213,6 +213,14 @@ export function missingPinnedVersionMessage(hostName, agent, version, installed)
|
|
|
213
213
|
* --json`. A picker may route to a signed-out version because launching it is
|
|
214
214
|
* the login flow, but it must not route to a device whose every signed-in
|
|
215
215
|
* account is throttled.
|
|
216
|
+
*
|
|
217
|
+
* The sign-in gate reads the per-version `launchable` field — the strict
|
|
218
|
+
* per-version launch truth (`isLaunchableSignedIn`) — so a remote box is judged
|
|
219
|
+
* by the SAME launchability the local candidate uses (`collectRunCandidates` →
|
|
220
|
+
* `isLaunchableSignedIn`), not the display `signedIn` that inherits the
|
|
221
|
+
* active/global HOME login and passes a box that dies at spawn (PHNX-3466). An
|
|
222
|
+
* older remote CLI omits `launchable`, so it falls back to `signedIn` — the
|
|
223
|
+
* pre-fix behavior, so a rolling fleet does not regress.
|
|
216
224
|
*/
|
|
217
225
|
export function viewAgentAccountEligibility(view, agent) {
|
|
218
226
|
try {
|
|
@@ -223,12 +231,15 @@ export function viewAgentAccountEligibility(view, agent) {
|
|
|
223
231
|
const verdicts = (row.versions ?? []).flatMap((version) => {
|
|
224
232
|
if (typeof version.signedIn !== 'boolean')
|
|
225
233
|
return [];
|
|
234
|
+
// Prefer the strict per-version launch signal; fall back to the display
|
|
235
|
+
// `signedIn` for an older remote CLI that does not emit `launchable`.
|
|
236
|
+
const launchable = typeof version.launchable === 'boolean' ? version.launchable : version.signedIn;
|
|
226
237
|
const throttled = version.usageStatus === 'rate_limited' || version.usageStatus === 'out_of_credits';
|
|
227
238
|
const authBlocked = version.authVerdict !== null
|
|
228
239
|
&& version.authVerdict !== undefined
|
|
229
240
|
&& isDeadVerdict(version.authVerdict);
|
|
230
|
-
const ready =
|
|
231
|
-
return [{ ready, pickerEligible: ready || !
|
|
241
|
+
const ready = launchable && !authBlocked && !throttled;
|
|
242
|
+
return [{ ready, pickerEligible: ready || !launchable || authBlocked }];
|
|
232
243
|
});
|
|
233
244
|
if (verdicts.length === 0)
|
|
234
245
|
return { signedIn: undefined, pickerEligible: undefined };
|