@phnx-labs/agents-cli 1.22.57 → 1.22.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/bootstrap.js +8 -1
  3. package/dist/commands/accounts.js +7 -3
  4. package/dist/commands/apply.js +10 -2
  5. package/dist/commands/fork.d.ts +23 -10
  6. package/dist/commands/fork.js +115 -58
  7. package/dist/commands/monitors.js +11 -0
  8. package/dist/commands/prune.js +5 -3
  9. package/dist/commands/routines.d.ts +8 -0
  10. package/dist/commands/routines.js +57 -3
  11. package/dist/commands/sessions-picker.d.ts +11 -0
  12. package/dist/commands/sessions-picker.js +16 -0
  13. package/dist/commands/sessions.js +1 -0
  14. package/dist/commands/share.d.ts +14 -0
  15. package/dist/commands/share.js +43 -2
  16. package/dist/commands/status.js +1 -1
  17. package/dist/commands/sync.js +83 -7
  18. package/dist/commands/traces.js +7 -0
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +6 -1
  21. package/dist/lib/account-registry.d.ts +5 -1
  22. package/dist/lib/account-registry.js +47 -14
  23. package/dist/lib/accounting/capacity.d.ts +18 -7
  24. package/dist/lib/accounting/capacity.js +19 -8
  25. package/dist/lib/accounting/usage-sync.d.ts +29 -1
  26. package/dist/lib/accounting/usage-sync.js +76 -2
  27. package/dist/lib/accounting/usage.js +7 -1
  28. package/dist/lib/auth-mint.d.ts +11 -1
  29. package/dist/lib/auth-mint.js +21 -6
  30. package/dist/lib/browser/ipc.d.ts +8 -0
  31. package/dist/lib/browser/ipc.js +87 -0
  32. package/dist/lib/browser/service.d.ts +19 -0
  33. package/dist/lib/browser/service.js +96 -11
  34. package/dist/lib/browser/sessions-list.js +10 -1
  35. package/dist/lib/daemon/runner.d.ts +3 -0
  36. package/dist/lib/daemon/runner.js +86 -45
  37. package/dist/lib/daemon/usage-sync-service.d.ts +3 -3
  38. package/dist/lib/daemon/usage-sync-service.js +14 -8
  39. package/dist/lib/daemon-services.js +1 -1
  40. package/dist/lib/devices/connect.d.ts +17 -8
  41. package/dist/lib/devices/connect.js +31 -14
  42. package/dist/lib/doctor-diff.js +77 -7
  43. package/dist/lib/fleet/manifest.d.ts +17 -0
  44. package/dist/lib/fleet/manifest.js +26 -0
  45. package/dist/lib/hooks/install.d.ts +27 -11
  46. package/dist/lib/hooks/install.js +42 -17
  47. package/dist/lib/hosts/reconnect.d.ts +52 -203
  48. package/dist/lib/hosts/reconnect.js +64 -284
  49. package/dist/lib/installations/migrate.d.ts +6 -120
  50. package/dist/lib/installations/migrate.js +27 -259
  51. package/dist/lib/installations/shims.d.ts +13 -95
  52. package/dist/lib/installations/shims.js +22 -139
  53. package/dist/lib/installations/store.js +1 -1
  54. package/dist/lib/installations/versions.d.ts +26 -133
  55. package/dist/lib/installations/versions.js +41 -204
  56. package/dist/lib/plugins/skills.d.ts +8 -1
  57. package/dist/lib/plugins/skills.js +18 -2
  58. package/dist/lib/refresh.d.ts +9 -0
  59. package/dist/lib/refresh.js +3 -1
  60. package/dist/lib/routine-readiness.d.ts +15 -1
  61. package/dist/lib/routine-readiness.js +41 -0
  62. package/dist/lib/sandbox.d.ts +4 -1
  63. package/dist/lib/sandbox.js +30 -1
  64. package/dist/lib/secrets/agent.d.ts +80 -225
  65. package/dist/lib/secrets/agent.js +139 -401
  66. package/dist/lib/secrets/bundles.d.ts +73 -222
  67. package/dist/lib/secrets/bundles.js +168 -467
  68. package/dist/lib/secrets/reaper.d.ts +28 -70
  69. package/dist/lib/secrets/reaper.js +30 -85
  70. package/dist/lib/secrets/remote.d.ts +42 -129
  71. package/dist/lib/secrets/remote.js +55 -173
  72. package/dist/lib/self-heal/checks/install-staging.d.ts +4 -0
  73. package/dist/lib/self-heal/checks/install-staging.js +96 -0
  74. package/dist/lib/self-heal/registry.js +2 -0
  75. package/dist/lib/self-heal/types.d.ts +1 -1
  76. package/dist/lib/self-update.d.ts +23 -0
  77. package/dist/lib/self-update.js +50 -0
  78. package/dist/lib/session/active.d.ts +13 -1
  79. package/dist/lib/session/active.js +2 -0
  80. package/dist/lib/session/db.d.ts +20 -1
  81. package/dist/lib/session/db.js +139 -9
  82. package/dist/lib/session/fork.d.ts +45 -26
  83. package/dist/lib/session/fork.js +32 -95
  84. package/dist/lib/session/tool-calls.d.ts +43 -1
  85. package/dist/lib/session/tool-calls.js +74 -44
  86. package/dist/lib/session/tool-store.d.ts +33 -2
  87. package/dist/lib/session/tool-store.js +56 -3
  88. package/dist/lib/staleness/writers/sources.d.ts +5 -0
  89. package/dist/lib/staleness/writers/sources.js +2 -1
  90. package/dist/lib/sync-status.d.ts +22 -0
  91. package/dist/lib/sync-status.js +27 -0
  92. package/dist/lib/sync-umbrella.d.ts +9 -0
  93. package/dist/lib/sync-umbrella.js +21 -2
  94. package/dist/lib/traces/insights.d.ts +47 -14
  95. package/dist/lib/traces/insights.js +92 -21
  96. package/dist/lib/traces/phenotype.d.ts +23 -3
  97. package/dist/lib/traces/phenotype.js +72 -24
  98. package/dist/lib/traces/sync.d.ts +15 -0
  99. package/dist/lib/traces/sync.js +104 -19
  100. package/dist/lib/traces/worker-template.js +154 -1
  101. package/package.json +1 -1
@@ -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';
@@ -115,16 +117,46 @@ function diffCommands(agent, version, cwd, excludeProject = false) {
115
117
  if (!entry.isFile() || !entry.name.endsWith('.md'))
116
118
  continue;
117
119
  const name = entry.name.replace(/\.md$/, '');
120
+ // Directory docs (README/AGENTS/CLAUDE/GEMINI) live in commands/ but are
121
+ // documentation, not commands. `discoverCommands` and `resolveResource`
122
+ // both refuse them, so the sync writer never installs them — mirror that
123
+ // here or every one false-reports as a missing command no sync can clear.
124
+ if (isDirectoryDoc('commands', name))
125
+ continue;
118
126
  if (sourceByName.has(name))
119
127
  continue;
120
128
  sourceByName.set(name, { layer: base.layer, path: path.join(base.path, entry.name), alias: base.alias });
121
129
  }
122
130
  }
131
+ // The trusted skill roots the commands-as-skills writer consults to decide
132
+ // whether a real skill of the same name already owns a command's target slot.
133
+ const skillRoots = asSkill ? trustedSkillRoots() : [];
123
134
  const rows = [];
124
135
  const seen = new Set();
125
136
  for (const [name, src] of sourceByName) {
126
137
  seen.add(name);
127
138
  if (!installed.has(name)) {
139
+ // Command-as-skill agents (codex >= 0.117, kimi): when a real skill of the
140
+ // same name exists in the sources, the skill wins the shared
141
+ // `skills/<name>/` slot and `installCommandSkillToVersion` deliberately
142
+ // writes no command wrapper (versions.ts keeps the real skill in
143
+ // skillsToSync and overwrites any wrapper). The command's behavior is
144
+ // provided by that same-named skill, so it is NOT missing drift — reporting
145
+ // it so drove an unclearable "N missing" loop (PHNX-3186). Mirror the
146
+ // writer's skip predicate exactly: a skill source of this name whose
147
+ // `agents_command` marker is not this command (a real skill, or a different
148
+ // command).
149
+ if (asSkill && skillSourceExists(name, skillRoots) && readSkillSourceCommandMarker(name, skillRoots) !== name) {
150
+ rows.push({
151
+ kind: 'commands',
152
+ name,
153
+ status: 'ok',
154
+ source: src.layer,
155
+ sourcePath: src.path,
156
+ detail: 'provided by same-named skill',
157
+ });
158
+ continue;
159
+ }
128
160
  rows.push({ kind: 'commands', name, status: 'missing', source: src.layer, sourcePath: src.path });
129
161
  continue;
130
162
  }
@@ -180,6 +212,11 @@ function diffCommands(agent, version, cwd, excludeProject = false) {
180
212
  for (const name of installed) {
181
213
  if (seen.has(name))
182
214
  continue;
215
+ // Directory docs are excluded from the source scan above; a leftover copy in
216
+ // the home is not a command orphan — leave it out of the command diff
217
+ // entirely rather than flip it to a spurious `extra` row.
218
+ if (isDirectoryDoc('commands', name))
219
+ continue;
183
220
  if (pluginCommands.has(name))
184
221
  continue;
185
222
  const extraHome = asSkill
@@ -421,11 +458,15 @@ function diffRules(agent, version, cwd, excludeProject = false) {
421
458
  const versionHome = getVersionHomePath(agent, version);
422
459
  const configDir = path.join(versionHome, agentConfigDirName(agent));
423
460
  const sourcesByName = listRulesNames(cwd, excludeProject);
424
- // Files actually present in the version home.
461
+ // Files actually present in the version home. Include *.md siblings AND the
462
+ // agent's own instructions filename even when it is not *.md — cursor's rules
463
+ // file is `.cursorrules`, so an `.md`-only scan never saw it and reported the
464
+ // AGENTS rule `missing` on every sync, a phantom no reconcile could clear
465
+ // (PHNX-3186).
425
466
  const homeFiles = new Set();
426
467
  if (fs.existsSync(configDir)) {
427
468
  for (const f of fs.readdirSync(configDir)) {
428
- if (!f.endsWith('.md'))
469
+ if (!f.endsWith('.md') && f !== agentConfig.instructionsFile)
429
470
  continue;
430
471
  homeFiles.add(f);
431
472
  }
@@ -537,14 +578,38 @@ export function describePluginDrift(central, mirrorDir) {
537
578
  parts.push(`${mVer}→${cVer}`);
538
579
  if (mManifest && repairableManifestFields(mManifest).length > 0)
539
580
  parts.push('invalid manifest');
581
+ const centralSkills = listPluginSkillDirs(central.root);
540
582
  const mirrorSkills = new Set(listPluginSkillDirs(mirrorDir));
541
- const missSkills = listPluginSkillDirs(central.root).filter((s) => !mirrorSkills.has(s)).sort();
583
+ const missSkills = centralSkills.filter((s) => !mirrorSkills.has(s)).sort();
584
+ const centralCmds = listPluginCommandFiles(central.root);
542
585
  const mirrorCmds = new Set(listPluginCommandFiles(mirrorDir));
543
- const missCmds = listPluginCommandFiles(central.root).filter((c) => !mirrorCmds.has(c)).sort();
586
+ const missCmds = centralCmds.filter((c) => !mirrorCmds.has(c)).sort();
544
587
  if (missSkills.length)
545
588
  parts.push(`missing skill${missSkills.length > 1 ? 's' : ''}: ${missSkills.join(', ')}`);
546
589
  if (missCmds.length)
547
590
  parts.push(`missing command${missCmds.length > 1 ? 's' : ''}: ${missCmds.join(', ')}`);
591
+ // Content drift of a skill/command that exists in BOTH — the mirror kept the
592
+ // dir/file but its bytes went stale (PHNX-2955: a skill edit landed on origin
593
+ // and was pulled into central, but the per-version marketplace copy was never
594
+ // refreshed, so agents kept executing the OLD skill text while `plugins list`
595
+ // and `doctor` reported it `everywhere`/`ok`). Presence alone missed this; a
596
+ // content compare is what turns a stale mirror into a reportable `diff`.
597
+ const staleSkills = centralSkills
598
+ .filter((s) => mirrorSkills.has(s))
599
+ .filter((s) => !dirsContentMatch(path.join(central.root, 'skills', s), path.join(mirrorDir, 'skills', s)))
600
+ .sort();
601
+ const staleCmds = centralCmds
602
+ .filter((c) => mirrorCmds.has(c))
603
+ .filter((c) => {
604
+ const a = readSafe(path.join(central.root, 'commands', `${c}.md`));
605
+ const b = readSafe(path.join(mirrorDir, 'commands', `${c}.md`));
606
+ return a == null || b == null || normalize(a) !== normalize(b);
607
+ })
608
+ .sort();
609
+ if (staleSkills.length)
610
+ parts.push(`stale skill${staleSkills.length > 1 ? 's' : ''}: ${staleSkills.join(', ')}`);
611
+ if (staleCmds.length)
612
+ parts.push(`stale command${staleCmds.length > 1 ? 's' : ''}: ${staleCmds.join(', ')}`);
548
613
  return parts.length ? parts.join(', ') : null;
549
614
  }
550
615
  function diffPlugins(agent, version, cwd) {
@@ -622,8 +687,13 @@ export function diffVersionResources(agent, version, options = {}) {
622
687
  empty.mcp = diffPresenceOnly('mcp', available.mcp, synced.mcp);
623
688
  if (requested.has('permissions'))
624
689
  empty.permissions = diffPresenceOnly('permissions', available.permissions, synced.permissions);
625
- if (requested.has('subagents'))
626
- empty.subagents = diffPresenceOnly('subagents', available.subagents, synced.subagents);
690
+ // Subagents are version-gated (e.g. kimi >= 0.29.0). A version below the floor
691
+ // is never written any subagent by the sync writer, so counting the source
692
+ // ones as "missing" is phantom drift no sync can clear (PHNX-3186). Zero the
693
+ // available set when unsupported; a stale installed copy still surfaces `extra`.
694
+ if (requested.has('subagents')) {
695
+ empty.subagents = diffPresenceOnly('subagents', supports(agent, 'subagents', version).ok ? available.subagents : [], synced.subagents);
696
+ }
627
697
  if (requested.has('plugins'))
628
698
  empty.plugins = diffPlugins(agent, version, cwd);
629
699
  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
+ }
@@ -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 script files present on disk that no manifest entry declares — "dead"
338
- * hooks. The registrar only wires manifest-declared hooks into an agent's
339
- * native config (settings.json / config.toml), matching the installed file to a
340
- * manifest entry by script basename. So a file whose basename matches no
341
- * manifest `script:` is never registered: it occupies the hooks dir and shows
342
- * up in listings, but no lifecycle event ever fires it.
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 manifest's script paths.
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 unmanagedHookNames(installedHookNames: string[], manifestScripts: string[]): string[];
364
+ export declare function listResolvedSourceHookScripts(): string[];
348
365
  /**
349
- * The dead hooks (see {@link unmanagedHookNames}) sitting in one version home.
350
- * Reads the merged hook manifest silently — a diagnostic must not emit the
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 script files present on disk that no manifest entry declares — "dead"
1598
- * hooks. The registrar only wires manifest-declared hooks into an agent's
1599
- * native config (settings.json / config.toml), matching the installed file to a
1600
- * manifest entry by script basename. So a file whose basename matches no
1601
- * manifest `script:` is never registered: it occupies the hooks dir and shows
1602
- * up in listings, but no lifecycle event ever fires it.
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 manifest's script paths.
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 unmanagedHookNames(installedHookNames, manifestScripts) {
1608
- const managed = new Set(manifestScripts.map((s) => path.basename(s).replace(/\.[^.]+$/, '')));
1609
- return installedHookNames.filter((name) => !managed.has(name)).sort();
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 dead hooks (see {@link unmanagedHookNames}) sitting in one version home.
1613
- * Reads the merged hook manifest silently — a diagnostic must not emit the
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, scripts);
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.