@phnx-labs/agents-cli 1.22.56 → 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.
- package/CHANGELOG.md +70 -0
- package/README.md +4 -4
- package/dist/bootstrap.js +11 -2
- package/dist/cli/command-registry.d.ts +0 -1
- package/dist/cli/command-registry.js +0 -3
- package/dist/commands/accounts.js +7 -3
- package/dist/commands/apply.js +10 -2
- package/dist/commands/exec.js +1 -1
- package/dist/commands/fork.d.ts +23 -10
- package/dist/commands/fork.js +115 -58
- package/dist/commands/hooks.js +4 -4
- package/dist/commands/insights.d.ts +7 -5
- package/dist/commands/insights.js +16 -9
- package/dist/commands/monitors.js +11 -0
- package/dist/commands/perf.d.ts +16 -7
- package/dist/commands/perf.js +29 -20
- 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/rules.js +1 -1
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +16 -0
- package/dist/commands/sessions.js +1 -0
- package/dist/commands/share.d.ts +14 -0
- package/dist/commands/share.js +43 -2
- package/dist/commands/ssh.js +24 -14
- package/dist/commands/status.js +1 -1
- package/dist/commands/sync.js +83 -7
- package/dist/commands/traces.js +7 -0
- package/dist/commands/trash.d.ts +2 -2
- package/dist/commands/trash.js +2 -6
- package/dist/commands/versions.d.ts +2 -2
- package/dist/commands/versions.js +1 -10
- package/dist/commands/view.d.ts +2 -2
- package/dist/commands/view.js +7 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +14 -0
- 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-ingest.d.ts +1 -0
- package/dist/lib/accounting/usage-ingest.js +75 -0
- package/dist/lib/accounting/usage-sync.d.ts +97 -0
- package/dist/lib/accounting/usage-sync.js +203 -0
- package/dist/lib/accounting/usage.d.ts +48 -2
- package/dist/lib/accounting/usage.js +79 -2
- package/dist/lib/agent-spec/agents.js +1 -1
- package/dist/lib/analytics/mix-commands.d.ts +8 -7
- package/dist/lib/analytics/mix-commands.js +50 -73
- package/dist/lib/auth-mint.d.ts +11 -1
- package/dist/lib/auth-mint.js +21 -6
- 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/daemon/daemon.js +5 -0
- package/dist/lib/daemon/runner.d.ts +3 -0
- package/dist/lib/daemon/runner.js +95 -53
- package/dist/lib/daemon/usage-sync-service.d.ts +21 -0
- package/dist/lib/daemon/usage-sync-service.js +42 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/device-config.d.ts +17 -6
- package/dist/lib/device-config.js +25 -11
- package/dist/lib/devices/connect.d.ts +17 -8
- package/dist/lib/devices/connect.js +31 -14
- package/dist/lib/devices/pool.d.ts +4 -3
- package/dist/lib/devices/pool.js +13 -5
- package/dist/lib/doctor-diff.js +77 -7
- package/dist/lib/exec.d.ts +6 -41
- package/dist/lib/exec.js +6 -41
- package/dist/lib/fleet/manifest.d.ts +17 -0
- package/dist/lib/fleet/manifest.js +26 -0
- package/dist/lib/git.d.ts +13 -1
- package/dist/lib/git.js +36 -7
- package/dist/lib/harness/adapter.d.ts +7 -7
- package/dist/lib/harness/adapters/claude.js +3 -2
- package/dist/lib/hooks/install.d.ts +27 -11
- package/dist/lib/hooks/install.js +42 -17
- package/dist/lib/hosts/reconnect.d.ts +52 -203
- package/dist/lib/hosts/reconnect.js +64 -284
- package/dist/lib/hosts/remote-cmd.d.ts +9 -0
- package/dist/lib/hosts/remote-cmd.js +22 -0
- 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 +26 -133
- package/dist/lib/installations/versions.js +41 -204
- package/dist/lib/perf/db.d.ts +1 -1
- package/dist/lib/perf/db.js +1 -1
- package/dist/lib/plugins/skills.d.ts +8 -1
- package/dist/lib/plugins/skills.js +18 -2
- 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/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 +23 -0
- package/dist/lib/self-update.js +50 -0
- package/dist/lib/session/active.d.ts +16 -32
- package/dist/lib/session/active.js +10 -68
- package/dist/lib/session/db.d.ts +24 -36
- package/dist/lib/session/db.js +143 -44
- package/dist/lib/session/discover.d.ts +6 -58
- package/dist/lib/session/discover.js +5 -43
- package/dist/lib/session/fork.d.ts +45 -26
- package/dist/lib/session/fork.js +32 -95
- package/dist/lib/session/parse.d.ts +1 -19
- package/dist/lib/session/parse.js +2 -15
- 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/staleness/writers/sources.d.ts +5 -0
- package/dist/lib/staleness/writers/sources.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +8 -2
- package/dist/lib/startup/command-registry.js +12 -4
- 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/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 +15 -0
- package/dist/lib/traces/sync.js +104 -19
- package/dist/lib/traces/worker-template.js +154 -1
- package/package.json +1 -1
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';
|
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
626
|
-
|
|
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'))
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -502,49 +502,10 @@ export type TmuxWrapDecision =
|
|
|
502
502
|
kind: 'undurable';
|
|
503
503
|
};
|
|
504
504
|
/**
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
* The wrap is opt-in via this device's `tmux.enabled` (`configEnabled`): a
|
|
509
|
-
* unique `%pane` handle so `agents sessions --active` can tell co-located
|
|
510
|
-
* agents apart and `agents focus` re-attaches without forking, plus scrollback
|
|
511
|
-
* and mouse at the operator's keyboard. Off means OFF, for local and remote
|
|
512
|
-
* runs alike (PHNX-3316) — a followed `--device` run left bare is protected by
|
|
513
|
-
* reconnect-and-resume (lib/hosts/reconnect.ts), which rejoins the live pane
|
|
514
|
-
* when one exists and resumes the harness session from disk when it does not.
|
|
515
|
-
* The RUSH-3125 forced remote wrap conflated durability with that preference
|
|
516
|
-
* and surprised every operator who had explicitly left tmux off.
|
|
517
|
-
*
|
|
518
|
-
* One case still wraps regardless: a followed remote run whose launcher has
|
|
519
|
-
* no TTY (CI, scripts, another agent) gives the peer nothing to attach to —
|
|
520
|
-
* the detached pane is the run's only interface, not an ergonomics choice.
|
|
521
|
-
*
|
|
522
|
-
* The per-run opt-outs bind everything: `--raw` / `--no-tmux` /
|
|
523
|
-
* `AGENTS_NO_TMUX=1` are explicit "I want the bare process" requests, and an
|
|
524
|
-
* escape hatch that silently stopped applying over `--device` would be worse
|
|
525
|
-
* than the bare run the user asked for.
|
|
526
|
-
*
|
|
527
|
-
* Pure, so the gate is unit-tested independently of the (side-effecting) spawn.
|
|
505
|
+
* Tmux is opt-in except when a TTY-less remote launch would otherwise have no
|
|
506
|
+
* interface. Explicit per-run opt-outs always win.
|
|
528
507
|
*/
|
|
529
508
|
export declare function resolveTmuxWrap(ctx: TmuxWrapContext): TmuxWrapDecision;
|
|
530
|
-
/**
|
|
531
|
-
* Build the shell command that runs an agent inside a tmux pane with the exact
|
|
532
|
-
* env the bare spawn would use. tmux runs it via `sh -c <cmd>`; we `exec env
|
|
533
|
-
* K=V … <agent> <args…>` so:
|
|
534
|
-
* - `env` materializes the full agent env INTO the pane, independent of the
|
|
535
|
-
* (possibly stale, shared) tmux server environment — additive, so tmux's own
|
|
536
|
-
* $TMUX / $TMUX_PANE still reach the agent for provenance detection;
|
|
537
|
-
* - `exec` replaces the shell so the agent is the pane's leaf process (clean
|
|
538
|
-
* `#{pane_pid}`, clean signal delivery on detach/kill).
|
|
539
|
-
* Keys are filtered to valid identifiers so exported shell functions
|
|
540
|
-
* (`BASH_FUNC_*%%`) can't make `env` choke.
|
|
541
|
-
*
|
|
542
|
-
* `redactEnvValues` replaces every value with a `<redacted>` marker while keeping
|
|
543
|
-
* the KEY names. The env map here carries resolved secrets bundles (options.env),
|
|
544
|
-
* so the real string would embed secret VALUES — which get persisted verbatim
|
|
545
|
-
* into SessionMeta.cmd on disk (tmux/session.ts). The launched command uses the
|
|
546
|
-
* real values; the stored/informational copy uses the redacted form (RUSH-1758).
|
|
547
|
-
*/
|
|
548
509
|
/**
|
|
549
510
|
* True when `options.sessionId` is an id the HARNESS actually received, and so a
|
|
550
511
|
* real, resumable handle — rather than one the launcher generated for its own
|
|
@@ -562,6 +523,10 @@ export declare function resolveTmuxWrap(ctx: TmuxWrapContext): TmuxWrapDecision;
|
|
|
562
523
|
* or this reports a genuine id as fabricated.
|
|
563
524
|
*/
|
|
564
525
|
export declare function isHarnessKnownSessionId(agent: AgentId, sessionId: string | undefined, resume: boolean | undefined): boolean;
|
|
526
|
+
/**
|
|
527
|
+
* Build the pane command without inheriting stale tmux-server env. Redacted
|
|
528
|
+
* copies keep secret values out of persisted SessionMeta commands.
|
|
529
|
+
*/
|
|
565
530
|
export declare function buildTmuxAgentCommand(executable: string, args: string[], env: NodeJS.ProcessEnv, opts?: {
|
|
566
531
|
redactEnvValues?: boolean;
|
|
567
532
|
envFile?: string;
|
package/dist/lib/exec.js
CHANGED
|
@@ -1241,29 +1241,8 @@ export function isPaneKnownAliveFromQueryResult(code, stdout) {
|
|
|
1241
1241
|
return code === 0 && stdout.trim() === '0';
|
|
1242
1242
|
}
|
|
1243
1243
|
/**
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1246
|
-
*
|
|
1247
|
-
* The wrap is opt-in via this device's `tmux.enabled` (`configEnabled`): a
|
|
1248
|
-
* unique `%pane` handle so `agents sessions --active` can tell co-located
|
|
1249
|
-
* agents apart and `agents focus` re-attaches without forking, plus scrollback
|
|
1250
|
-
* and mouse at the operator's keyboard. Off means OFF, for local and remote
|
|
1251
|
-
* runs alike (PHNX-3316) — a followed `--device` run left bare is protected by
|
|
1252
|
-
* reconnect-and-resume (lib/hosts/reconnect.ts), which rejoins the live pane
|
|
1253
|
-
* when one exists and resumes the harness session from disk when it does not.
|
|
1254
|
-
* The RUSH-3125 forced remote wrap conflated durability with that preference
|
|
1255
|
-
* and surprised every operator who had explicitly left tmux off.
|
|
1256
|
-
*
|
|
1257
|
-
* One case still wraps regardless: a followed remote run whose launcher has
|
|
1258
|
-
* no TTY (CI, scripts, another agent) gives the peer nothing to attach to —
|
|
1259
|
-
* the detached pane is the run's only interface, not an ergonomics choice.
|
|
1260
|
-
*
|
|
1261
|
-
* The per-run opt-outs bind everything: `--raw` / `--no-tmux` /
|
|
1262
|
-
* `AGENTS_NO_TMUX=1` are explicit "I want the bare process" requests, and an
|
|
1263
|
-
* escape hatch that silently stopped applying over `--device` would be worse
|
|
1264
|
-
* than the bare run the user asked for.
|
|
1265
|
-
*
|
|
1266
|
-
* Pure, so the gate is unit-tested independently of the (side-effecting) spawn.
|
|
1244
|
+
* Tmux is opt-in except when a TTY-less remote launch would otherwise have no
|
|
1245
|
+
* interface. Explicit per-run opt-outs always win.
|
|
1267
1246
|
*/
|
|
1268
1247
|
export function resolveTmuxWrap(ctx) {
|
|
1269
1248
|
// A headless `-p` run has no TTY to attach, Windows has no tmux path, and
|
|
@@ -1297,24 +1276,6 @@ export function resolveTmuxWrap(ctx) {
|
|
|
1297
1276
|
return ctx.remoteDispatch ? { kind: 'undurable' } : { kind: 'bare' };
|
|
1298
1277
|
return { kind: 'wrap' };
|
|
1299
1278
|
}
|
|
1300
|
-
/**
|
|
1301
|
-
* Build the shell command that runs an agent inside a tmux pane with the exact
|
|
1302
|
-
* env the bare spawn would use. tmux runs it via `sh -c <cmd>`; we `exec env
|
|
1303
|
-
* K=V … <agent> <args…>` so:
|
|
1304
|
-
* - `env` materializes the full agent env INTO the pane, independent of the
|
|
1305
|
-
* (possibly stale, shared) tmux server environment — additive, so tmux's own
|
|
1306
|
-
* $TMUX / $TMUX_PANE still reach the agent for provenance detection;
|
|
1307
|
-
* - `exec` replaces the shell so the agent is the pane's leaf process (clean
|
|
1308
|
-
* `#{pane_pid}`, clean signal delivery on detach/kill).
|
|
1309
|
-
* Keys are filtered to valid identifiers so exported shell functions
|
|
1310
|
-
* (`BASH_FUNC_*%%`) can't make `env` choke.
|
|
1311
|
-
*
|
|
1312
|
-
* `redactEnvValues` replaces every value with a `<redacted>` marker while keeping
|
|
1313
|
-
* the KEY names. The env map here carries resolved secrets bundles (options.env),
|
|
1314
|
-
* so the real string would embed secret VALUES — which get persisted verbatim
|
|
1315
|
-
* into SessionMeta.cmd on disk (tmux/session.ts). The launched command uses the
|
|
1316
|
-
* real values; the stored/informational copy uses the redacted form (RUSH-1758).
|
|
1317
|
-
*/
|
|
1318
1279
|
/**
|
|
1319
1280
|
* True when `options.sessionId` is an id the HARNESS actually received, and so a
|
|
1320
1281
|
* real, resumable handle — rather than one the launcher generated for its own
|
|
@@ -1338,6 +1299,10 @@ export function isHarnessKnownSessionId(agent, sessionId, resume) {
|
|
|
1338
1299
|
return true;
|
|
1339
1300
|
return agent === 'claude';
|
|
1340
1301
|
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Build the pane command without inheriting stale tmux-server env. Redacted
|
|
1304
|
+
* copies keep secret values out of persisted SessionMeta commands.
|
|
1305
|
+
*/
|
|
1341
1306
|
export function buildTmuxAgentCommand(executable, args, env, opts = {}) {
|
|
1342
1307
|
const agentCmd = [executable, ...args].map(shellQuote).join(' ');
|
|
1343
1308
|
// envFile: source the values instead of inlining them, so no VALUE ever lands
|
|
@@ -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
|
@@ -152,6 +152,15 @@ export declare function getCurrentBranch(repoPath: string): Promise<string>;
|
|
|
152
152
|
* repo" as a requested source before adopting it.
|
|
153
153
|
*/
|
|
154
154
|
export declare function canonicalGitRemote(url: string): string;
|
|
155
|
+
/**
|
|
156
|
+
* True when a git remote URL (any transport form: ssh, https, scp-style) points
|
|
157
|
+
* at the system DotAgents repo — {@link DEFAULT_SYSTEM_REPO}'s current slug OR
|
|
158
|
+
* its `phnx-labs/.agents` rename target (PHNX-3394), which
|
|
159
|
+
* {@link canonicalGitRemote} folds onto it via {@link RENAMED_REMOTE_ALIASES}.
|
|
160
|
+
* Pure string check with no git spawn, so it is unit-testable off a live
|
|
161
|
+
* checkout; {@link isSystemRepoOrigin} reads a dir's origin and delegates here.
|
|
162
|
+
*/
|
|
163
|
+
export declare function isSystemRepoRemote(remote: string | null | undefined): boolean;
|
|
155
164
|
/** True when two git remote URLs point at the same repo across transport forms. */
|
|
156
165
|
export declare function sameGitRemote(a: string | null | undefined, b: string | null | undefined): boolean;
|
|
157
166
|
/**
|
|
@@ -354,7 +363,10 @@ export declare function adoptUserRepoIfNeeded(dir: string, opts?: {
|
|
|
354
363
|
needsUrl?: boolean;
|
|
355
364
|
}) | null>;
|
|
356
365
|
/**
|
|
357
|
-
* Check if the repo's origin points to the system repo
|
|
366
|
+
* Check if the repo's origin points to the system repo — `phnx-labs/.agents-system`
|
|
367
|
+
* or its GitHub rename target `phnx-labs/.agents` (PHNX-3394), across any
|
|
368
|
+
* transport form. Reads the dir's origin and delegates the match to the pure
|
|
369
|
+
* {@link isSystemRepoRemote}.
|
|
358
370
|
*/
|
|
359
371
|
export declare function isSystemRepoOrigin(dir: string): Promise<boolean>;
|
|
360
372
|
/**
|
package/dist/lib/git.js
CHANGED
|
@@ -498,7 +498,7 @@ export async function getCurrentBranch(repoPath) {
|
|
|
498
498
|
* repo" as a requested source before adopting it.
|
|
499
499
|
*/
|
|
500
500
|
export function canonicalGitRemote(url) {
|
|
501
|
-
|
|
501
|
+
const canonical = url
|
|
502
502
|
.trim()
|
|
503
503
|
.replace(/\/+$/, '') // trailing slashes first, so a trailing-slash-after-.git still strips
|
|
504
504
|
.replace(/\.git$/i, '')
|
|
@@ -506,6 +506,36 @@ export function canonicalGitRemote(url) {
|
|
|
506
506
|
.replace(/^[^@/]+@/, '') // strip user@ (git@, ssh user)
|
|
507
507
|
.replace(':', '/') // scp-style host:owner/repo → host/owner/repo (first colon only)
|
|
508
508
|
.toLowerCase();
|
|
509
|
+
// Fold a renamed repo's old name onto its new one so both compare equal
|
|
510
|
+
// everywhere (see RENAMED_REMOTE_ALIASES).
|
|
511
|
+
return RENAMED_REMOTE_ALIASES[canonical] ?? canonical;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Git remotes that denote the SAME repository under an old and a new name,
|
|
515
|
+
* keyed by canonical `host/owner/repo`. `phnx-labs/.agents-system` was renamed
|
|
516
|
+
* to `phnx-labs/.agents` on GitHub (PHNX-3394); {@link DEFAULT_SYSTEM_REPO}
|
|
517
|
+
* still points at the pre-rename slug (GitHub's own redirect makes that
|
|
518
|
+
* resolve fine), so folding the new name onto it here means both compare equal
|
|
519
|
+
* everywhere remotes are compared: {@link sameGitRemote} (repo adoption),
|
|
520
|
+
* {@link isSystemRepoRemote} (the system-origin check), and the
|
|
521
|
+
* DotAgents-layer classifier in state.ts.
|
|
522
|
+
*/
|
|
523
|
+
const RENAMED_REMOTE_ALIASES = {
|
|
524
|
+
'github.com/phnx-labs/.agents': 'github.com/phnx-labs/.agents-system',
|
|
525
|
+
};
|
|
526
|
+
/**
|
|
527
|
+
* True when a git remote URL (any transport form: ssh, https, scp-style) points
|
|
528
|
+
* at the system DotAgents repo — {@link DEFAULT_SYSTEM_REPO}'s current slug OR
|
|
529
|
+
* its `phnx-labs/.agents` rename target (PHNX-3394), which
|
|
530
|
+
* {@link canonicalGitRemote} folds onto it via {@link RENAMED_REMOTE_ALIASES}.
|
|
531
|
+
* Pure string check with no git spawn, so it is unit-testable off a live
|
|
532
|
+
* checkout; {@link isSystemRepoOrigin} reads a dir's origin and delegates here.
|
|
533
|
+
*/
|
|
534
|
+
export function isSystemRepoRemote(remote) {
|
|
535
|
+
if (!remote)
|
|
536
|
+
return false;
|
|
537
|
+
const c = canonicalGitRemote(remote);
|
|
538
|
+
return c === canonicalGitRemote(`https://github.com/${systemRepoSlug(DEFAULT_SYSTEM_REPO)}`);
|
|
509
539
|
}
|
|
510
540
|
/** True when two git remote URLs point at the same repo across transport forms. */
|
|
511
541
|
export function sameGitRemote(a, b) {
|
|
@@ -1062,18 +1092,17 @@ export async function adoptUserRepoIfNeeded(dir, opts = {}) {
|
|
|
1062
1092
|
return adoptRepoInPlace(dir, url);
|
|
1063
1093
|
}
|
|
1064
1094
|
/**
|
|
1065
|
-
* Check if the repo's origin points to the system repo
|
|
1095
|
+
* Check if the repo's origin points to the system repo — `phnx-labs/.agents-system`
|
|
1096
|
+
* or its GitHub rename target `phnx-labs/.agents` (PHNX-3394), across any
|
|
1097
|
+
* transport form. Reads the dir's origin and delegates the match to the pure
|
|
1098
|
+
* {@link isSystemRepoRemote}.
|
|
1066
1099
|
*/
|
|
1067
1100
|
export async function isSystemRepoOrigin(dir) {
|
|
1068
1101
|
try {
|
|
1069
1102
|
const git = simpleGit(dir);
|
|
1070
1103
|
const remotes = await git.getRemotes(true);
|
|
1071
1104
|
const origin = remotes.find(r => r.name === 'origin');
|
|
1072
|
-
|
|
1073
|
-
return false;
|
|
1074
|
-
const url = origin.refs.fetch.toLowerCase();
|
|
1075
|
-
const currentSlug = systemRepoSlug(DEFAULT_SYSTEM_REPO).toLowerCase();
|
|
1076
|
-
return url.includes(currentSlug);
|
|
1105
|
+
return isSystemRepoRemote(origin?.refs?.fetch);
|
|
1077
1106
|
}
|
|
1078
1107
|
catch {
|
|
1079
1108
|
/* not a git repo or no remotes */
|
|
@@ -44,13 +44,13 @@ export interface ExecConfigEnvCtx {
|
|
|
44
44
|
/** resolveInteractive(options) — computed once by the caller. */
|
|
45
45
|
interactive: boolean;
|
|
46
46
|
/**
|
|
47
|
-
* The role marked on THIS machine (worker | personal | undefined),
|
|
48
|
-
* once by the caller from selfConfiguredDeviceRole(). A
|
|
49
|
-
*
|
|
50
|
-
* credential decision MUST defer to it for EVERY run — interactive
|
|
51
|
-
* — never the worker-only setup-token (RUSH-2395). Injected as a
|
|
52
|
-
* (not imported) to keep the adapter import-leaf. Absent/undefined
|
|
53
|
-
* as non-
|
|
47
|
+
* The role marked on THIS machine (worker | personal | desktop | undefined),
|
|
48
|
+
* resolved once by the caller from selfConfiguredDeviceRole(). A headed device
|
|
49
|
+
* (`personal` or `desktop` — see isHeadedDeviceRole) holds a real per-version
|
|
50
|
+
* login and the credential decision MUST defer to it for EVERY run — interactive
|
|
51
|
+
* OR headless — never the worker-only setup-token (RUSH-2395). Injected as a
|
|
52
|
+
* plain value (not imported) to keep the adapter import-leaf. Absent/undefined
|
|
53
|
+
* is treated as non-headed (worker-equivalent).
|
|
54
54
|
*/
|
|
55
55
|
deviceRole?: ConfiguredDeviceRole;
|
|
56
56
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
2
|
import { stripForeignConfigDir } from '../adapter.js';
|
|
3
|
+
import { isHeadedDeviceRole } from '../../device-config.js';
|
|
3
4
|
export const claudeAdapter = {
|
|
4
5
|
id: 'claude',
|
|
5
6
|
applyExecConfigEnv(result, ctx) {
|
|
@@ -47,8 +48,8 @@ export const claudeAdapter = {
|
|
|
47
48
|
// path — agents.ts `isClaudeCredentialFileBlank`), so this path defers to
|
|
48
49
|
// Claude Code, which reads its own ACL-trusted login item without a prompt and
|
|
49
50
|
// asks a present human to log in only if the login is missing.
|
|
50
|
-
const
|
|
51
|
-
if (ctx.interactive ||
|
|
51
|
+
const headedDevice = isHeadedDeviceRole(ctx.deviceRole);
|
|
52
|
+
if (ctx.interactive || headedDevice) {
|
|
52
53
|
// Drop an INHERITED copy of OUR OWN setup-token: a launch from inside a
|
|
53
54
|
// headless agent's shell inherits that agent's injected value via
|
|
54
55
|
// sanitizeProcessEnv(process.env) and would keep authenticating as it,
|
|
@@ -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.
|