@phnx-labs/agents-cli 1.22.31 → 1.22.33
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 +72 -0
- package/README.md +8 -2
- package/dist/bin/agents +0 -0
- package/dist/commands/daemon.js +52 -12
- package/dist/commands/doctor.d.ts +19 -0
- package/dist/commands/doctor.js +119 -17
- package/dist/commands/routines.js +164 -36
- package/dist/commands/sessions-browser.js +2 -2
- package/dist/commands/sessions.d.ts +1 -1
- package/dist/commands/sessions.js +66 -22
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +148 -0
- package/dist/index.js +3 -1
- package/dist/lib/catchup.js +4 -1
- package/dist/lib/daemon.d.ts +17 -0
- package/dist/lib/daemon.js +69 -3
- package/dist/lib/devices/doctor-findings.d.ts +7 -2
- package/dist/lib/devices/doctor-findings.js +53 -2
- package/dist/lib/devices/doctor-overview-cache.d.ts +7 -0
- package/dist/lib/devices/doctor-overview-cache.js +15 -0
- package/dist/lib/devices/fleet-divergence.d.ts +11 -0
- package/dist/lib/devices/fleet-divergence.js +6 -0
- package/dist/lib/devices/fleet-inventory.js +16 -2
- package/dist/lib/drift.d.ts +6 -1
- package/dist/lib/drift.js +9 -0
- package/dist/lib/hooks/cache.js +20 -1
- package/dist/lib/hooks.d.ts +91 -1
- package/dist/lib/hooks.js +289 -3
- package/dist/lib/hosts/passthrough.js +3 -0
- package/dist/lib/installations/index.d.ts +14 -0
- package/dist/lib/installations/index.js +14 -0
- package/dist/lib/installations/resolve.d.ts +43 -0
- package/dist/lib/installations/resolve.js +93 -0
- package/dist/lib/installations/store.d.ts +56 -0
- package/dist/lib/installations/store.js +196 -0
- package/dist/lib/installations/strategies.d.ts +73 -0
- package/dist/lib/installations/strategies.js +293 -0
- package/dist/lib/installations/types.d.ts +78 -0
- package/dist/lib/installations/types.js +8 -0
- package/dist/lib/installations/update.d.ts +40 -0
- package/dist/lib/installations/update.js +131 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +27 -0
- package/dist/lib/migrate.js +112 -2
- package/dist/lib/routine-context.d.ts +144 -0
- package/dist/lib/routine-context.js +268 -0
- package/dist/lib/routine-readiness.d.ts +47 -0
- package/dist/lib/routine-readiness.js +239 -0
- package/dist/lib/routines.d.ts +97 -1
- package/dist/lib/routines.js +107 -1
- package/dist/lib/runner.d.ts +18 -4
- package/dist/lib/runner.js +291 -98
- package/dist/lib/scheduler.d.ts +7 -1
- package/dist/lib/scheduler.js +5 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/self-heal/checks/hook-runtime.d.ts +2 -0
- package/dist/lib/self-heal/checks/hook-runtime.js +16 -0
- package/dist/lib/self-heal/registry.js +5 -2
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/session/state.js +4 -1
- package/dist/lib/session/team-filter.d.ts +11 -0
- package/dist/lib/session/team-filter.js +10 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/versions.d.ts +24 -0
- package/dist/lib/versions.js +49 -16
- package/package.json +2 -2
package/dist/lib/drift.js
CHANGED
|
@@ -60,6 +60,12 @@ export function checkSyncStatus(cwd) {
|
|
|
60
60
|
// never fires. Surface that for every version, fresh or stale, so overview
|
|
61
61
|
// AND `agents doctor --check` flag it (claude/droid; other agents report unsupported).
|
|
62
62
|
const wiring = checkVersionHookWiring(agent, version);
|
|
63
|
+
if (wiring.runtimeBroken.length > 0) {
|
|
64
|
+
row.brokenHookRuntime = wiring.runtimeBroken.length;
|
|
65
|
+
const names = wiring.runtimeBroken.map((issue) => issue.name);
|
|
66
|
+
const shown = names.slice(0, 3).join(', ');
|
|
67
|
+
divergence.push(`hooks ${names.length} generated wrapper${names.length === 1 ? '' : 's'} broken (${shown}${names.length > 3 ? ', …' : ''})`);
|
|
68
|
+
}
|
|
63
69
|
if (wiring.supported) {
|
|
64
70
|
const expected = wiring.expected ?? 0;
|
|
65
71
|
if (wiring.settingsMissing && expected > 0) {
|
|
@@ -147,6 +153,7 @@ export function computeDrift(cwd) {
|
|
|
147
153
|
const staleCount = syncRows.filter((r) => r.status === 'stale').length;
|
|
148
154
|
const neverSyncedCount = syncRows.filter((r) => r.status === 'never-synced').length;
|
|
149
155
|
const unwiredHookVersions = syncRows.filter((r) => (r.unwiredHooks ?? 0) > 0).length;
|
|
156
|
+
const brokenHookRuntimeVersions = syncRows.filter((r) => (r.brokenHookRuntime ?? 0) > 0).length;
|
|
150
157
|
const sourceBehind = computeSourceBehind();
|
|
151
158
|
return {
|
|
152
159
|
syncRows,
|
|
@@ -155,9 +162,11 @@ export function computeDrift(cwd) {
|
|
|
155
162
|
neverSyncedCount,
|
|
156
163
|
orphanVersionCount: orphanRows.length,
|
|
157
164
|
unwiredHookVersions,
|
|
165
|
+
brokenHookRuntimeVersions,
|
|
158
166
|
sourceBehind,
|
|
159
167
|
hasDrift: syncRows.some((r) => r.status !== 'fresh') ||
|
|
160
168
|
unwiredHookVersions > 0 ||
|
|
169
|
+
brokenHookRuntimeVersions > 0 ||
|
|
161
170
|
sourceBehind.length > 0,
|
|
162
171
|
};
|
|
163
172
|
}
|
package/dist/lib/hooks/cache.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import * as fs from 'fs';
|
|
20
20
|
import * as path from 'path';
|
|
21
|
+
import * as crypto from 'crypto';
|
|
21
22
|
import { getHookCacheDir, getHookShimsDir, getLogsDir, getPerfDir } from '../state.js';
|
|
22
23
|
/**
|
|
23
24
|
* Parse a `cache:` value from hooks.yaml into the canonical config form.
|
|
@@ -128,7 +129,25 @@ export function generateHookShim(args) {
|
|
|
128
129
|
catch { /* rewrite */ }
|
|
129
130
|
}
|
|
130
131
|
if (existing !== content) {
|
|
131
|
-
|
|
132
|
+
// A hook may fire while a background self-heal repairs another stale shim.
|
|
133
|
+
// Write in the destination directory and rename only after its mode and
|
|
134
|
+
// complete contents are ready, so observers see either the old complete
|
|
135
|
+
// wrapper or the new complete wrapper — never a truncated shell script.
|
|
136
|
+
const tempPath = path.join(shimsDir, `.${path.basename(shimPath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
137
|
+
try {
|
|
138
|
+
fs.writeFileSync(tempPath, content, { mode: 0o755 });
|
|
139
|
+
fs.chmodSync(tempPath, 0o755);
|
|
140
|
+
fs.renameSync(tempPath, shimPath);
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
// Rename removes the temp path on success. On a failed write/rename this
|
|
144
|
+
// best-effort cleanup prevents a bounded repair failure from leaving
|
|
145
|
+
// growing debris behind for every periodic pass.
|
|
146
|
+
try {
|
|
147
|
+
fs.unlinkSync(tempPath);
|
|
148
|
+
}
|
|
149
|
+
catch { /* already renamed or unavailable */ }
|
|
150
|
+
}
|
|
132
151
|
}
|
|
133
152
|
else {
|
|
134
153
|
// Ensure exec bit even when content unchanged (file mode can drift).
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export declare function toPortableCommand(absPath: string, home?: string, sep?:
|
|
|
28
28
|
* outside version homes are user-owned and remain untouched.
|
|
29
29
|
*/
|
|
30
30
|
export declare function deduplicateVersionHookCommands(commands: string[], activeVersionHome: string): string[];
|
|
31
|
-
import type { AgentId, InstalledHook, ManifestHook } from './types.js';
|
|
31
|
+
import type { AgentId, HookCacheConfig, HookMatches, InstalledHook, ManifestHook } from './types.js';
|
|
32
32
|
export type HookEntry = {
|
|
33
33
|
name: string;
|
|
34
34
|
scriptPath: string;
|
|
@@ -110,6 +110,27 @@ export interface HookWiringIssue {
|
|
|
110
110
|
/** The command the harness-native config should reference under `event`. */
|
|
111
111
|
command: string;
|
|
112
112
|
}
|
|
113
|
+
/** A generated, agents-managed hook wrapper and the inputs needed to recreate it. */
|
|
114
|
+
export interface ManagedHookRuntimeArtifact {
|
|
115
|
+
agent: AgentId;
|
|
116
|
+
version: string;
|
|
117
|
+
name: string;
|
|
118
|
+
scriptPath: string;
|
|
119
|
+
shimPath: string;
|
|
120
|
+
cache: HookCacheConfig | null;
|
|
121
|
+
matches?: HookMatches;
|
|
122
|
+
}
|
|
123
|
+
/** A generated hook wrapper that is referenced by managed configuration but cannot run. */
|
|
124
|
+
export interface BrokenManagedHookRuntimeArtifact extends ManagedHookRuntimeArtifact {
|
|
125
|
+
/** Stable, human-readable filesystem failure; the hook is never executed to find it. */
|
|
126
|
+
reason: string;
|
|
127
|
+
}
|
|
128
|
+
/** Public, doctor-safe shape for a broken generated hook wrapper. */
|
|
129
|
+
export interface HookRuntimeIssue {
|
|
130
|
+
name: string;
|
|
131
|
+
path: string;
|
|
132
|
+
reason: string;
|
|
133
|
+
}
|
|
113
134
|
export interface HookWiringReport {
|
|
114
135
|
/** Whether this agent's hook config format is understood by the inspector. */
|
|
115
136
|
supported: boolean;
|
|
@@ -128,7 +149,76 @@ export interface HookWiringReport {
|
|
|
128
149
|
* Empty whenever wiring cannot be verified (unsupported family, missing or
|
|
129
150
|
* unparseable settings). */
|
|
130
151
|
wired: HookWiringIssue[];
|
|
152
|
+
/** Generated agents-managed wrappers that are absent or unusable. This is
|
|
153
|
+
* independent of whether the native config format itself is understood. */
|
|
154
|
+
runtimeBroken: HookRuntimeIssue[];
|
|
155
|
+
}
|
|
156
|
+
/** Inspect managed generated hook wrappers without executing user hook code. */
|
|
157
|
+
export declare function inspectBrokenManagedHookRuntimeArtifacts(filter?: {
|
|
158
|
+
agent?: AgentId;
|
|
159
|
+
version?: string;
|
|
160
|
+
}, platform?: NodeJS.Platform): BrokenManagedHookRuntimeArtifact[];
|
|
161
|
+
/** Outcome of one generation attempt for a unique shim path. */
|
|
162
|
+
export interface HookRuntimeRepairAttempt {
|
|
163
|
+
name: string;
|
|
164
|
+
path: string;
|
|
165
|
+
reasonBefore: string;
|
|
166
|
+
/** True when generateHookShim ran for this path in this pass. */
|
|
167
|
+
attempted: boolean;
|
|
168
|
+
repaired: boolean;
|
|
169
|
+
/** Stable failure text when not repaired after an attempt (or dry-run skip). */
|
|
170
|
+
reason?: string;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Result of one bounded repair pass over agents-managed generated hook shims.
|
|
174
|
+
* Shared by self-heal and doctor --fix; additive types for the doctor track.
|
|
175
|
+
*/
|
|
176
|
+
export interface HookRuntimeRepairReport {
|
|
177
|
+
/** Broken artifacts found before any write (inspect-only snapshot). */
|
|
178
|
+
brokenBefore: BrokenManagedHookRuntimeArtifact[];
|
|
179
|
+
/** Unique shim paths considered for generation in this pass. */
|
|
180
|
+
attemptedPaths: string[];
|
|
181
|
+
attempts: HookRuntimeRepairAttempt[];
|
|
182
|
+
/** Human-readable lines for CheckResult.fixed (includes dry-run would-fix). */
|
|
183
|
+
fixed: string[];
|
|
184
|
+
/** Human-readable lines for CheckResult.needsAttention — stable wording. */
|
|
185
|
+
needsAttention: string[];
|
|
131
186
|
}
|
|
187
|
+
export interface RepairManagedHookRuntimeOptions {
|
|
188
|
+
/** Detect only — never write. Default false. */
|
|
189
|
+
dryRun?: boolean;
|
|
190
|
+
filter?: {
|
|
191
|
+
agent?: AgentId;
|
|
192
|
+
version?: string;
|
|
193
|
+
};
|
|
194
|
+
platform?: NodeJS.Platform;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Regenerate one known-broken wrapper and prove the result is usable. Callers
|
|
198
|
+
* provide a snapshot from inspectBrokenManagedHookRuntimeArtifacts; this never
|
|
199
|
+
* loops or retries and returns a stable error for the current pass.
|
|
200
|
+
*
|
|
201
|
+
* Generation is delegated to generateHookShim (idempotent — preserves mtime when
|
|
202
|
+
* content already matches). This path never calls registerHooksToSettings,
|
|
203
|
+
* installHooks, or any sync routine.
|
|
204
|
+
*/
|
|
205
|
+
export declare function repairManagedHookRuntimeArtifact(artifact: ManagedHookRuntimeArtifact, platform?: NodeJS.Platform): {
|
|
206
|
+
repaired: boolean;
|
|
207
|
+
reason?: string;
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* Bounded repair of all broken agents-managed generated hook shims.
|
|
211
|
+
*
|
|
212
|
+
* - Inspect first (read-only, no hook execution).
|
|
213
|
+
* - One generation attempt per unique shim path per call (no retry, no timer).
|
|
214
|
+
* - Canonical owner per shared path: global default, else newest non-isolated.
|
|
215
|
+
* - Post-repair reinspection; unresolved findings become stable needsAttention.
|
|
216
|
+
* - Never recurses into resource sync / registerHooksToSettings.
|
|
217
|
+
*
|
|
218
|
+
* This is the shared routine used by the self-heal `hook-runtime` check and
|
|
219
|
+
* exported for the doctor track.
|
|
220
|
+
*/
|
|
221
|
+
export declare function repairManagedHookRuntimeArtifacts(opts?: RepairManagedHookRuntimeOptions): HookRuntimeRepairReport;
|
|
132
222
|
/**
|
|
133
223
|
* Verify that every hook the manifest says should be wired is actually
|
|
134
224
|
* referenced in that version's harness-native config, not merely present as a
|
package/dist/lib/hooks.js
CHANGED
|
@@ -264,7 +264,7 @@ function collapseVersionHookEntries(entries, activeVersionHome) {
|
|
|
264
264
|
return true;
|
|
265
265
|
});
|
|
266
266
|
}
|
|
267
|
-
import { getEffectiveHome, getVersionHomePath, listInstalledVersions, resolveVersion } from './versions.js';
|
|
267
|
+
import { getEffectiveHome, getGlobalDefault, getVersionHomePath, isVersionIsolated, listInstalledVersions, resolveVersion, } from './versions.js';
|
|
268
268
|
import { generateHookShim, getHookShimPath, isValidHookShimName, parseCacheConfig, removeHookShim } from './hooks/cache.js';
|
|
269
269
|
import { getHookShimsDir } from './state.js';
|
|
270
270
|
function hookContentHash(scriptPath) {
|
|
@@ -772,6 +772,285 @@ export function listHooksInVersionHome(agent, version) {
|
|
|
772
772
|
const SETTINGS_JSON_HOOK_FAMILY = ['claude', 'droid', 'muse'];
|
|
773
773
|
const HOOKS_JSON_HOOK_FAMILY = ['grok'];
|
|
774
774
|
const TOML_ARRAY_HOOK_FAMILY = ['kimi'];
|
|
775
|
+
/**
|
|
776
|
+
* Check the filesystem properties a shell hook needs without invoking it. A
|
|
777
|
+
* dangling symlink is deliberately distinguished from an absent file because
|
|
778
|
+
* the repair/remediation is the same but the diagnostic is materially clearer.
|
|
779
|
+
*/
|
|
780
|
+
function shellQuoteForHookShim(value) {
|
|
781
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
782
|
+
}
|
|
783
|
+
function hookRuntimeProblem(artifact, platform = process.platform) {
|
|
784
|
+
const { shimPath } = artifact;
|
|
785
|
+
let link;
|
|
786
|
+
try {
|
|
787
|
+
link = fs.lstatSync(shimPath);
|
|
788
|
+
}
|
|
789
|
+
catch (err) {
|
|
790
|
+
const code = err.code;
|
|
791
|
+
if (code === 'ENOENT')
|
|
792
|
+
return 'missing';
|
|
793
|
+
// Stable: errno code only — never absolute path text (fleet/menu aggregation).
|
|
794
|
+
return `cannot inspect (${code || 'error'})`;
|
|
795
|
+
}
|
|
796
|
+
let target;
|
|
797
|
+
try {
|
|
798
|
+
target = link.isSymbolicLink() ? fs.statSync(shimPath) : link;
|
|
799
|
+
}
|
|
800
|
+
catch (err) {
|
|
801
|
+
const code = err.code;
|
|
802
|
+
if (code === 'ENOENT')
|
|
803
|
+
return 'broken symlink';
|
|
804
|
+
return `cannot inspect (${code || 'error'})`;
|
|
805
|
+
}
|
|
806
|
+
if (!target.isFile())
|
|
807
|
+
return 'not a regular file';
|
|
808
|
+
// Generation always writes non-empty content; an empty placeholder is broken.
|
|
809
|
+
if (target.size === 0)
|
|
810
|
+
return 'broken (empty)';
|
|
811
|
+
// Windows does not use a POSIX executable bit; requiring it there would
|
|
812
|
+
// continuously rewrite healthy .sh files on Windows hosts.
|
|
813
|
+
if (platform !== 'win32' && (target.mode & 0o111) === 0)
|
|
814
|
+
return 'not executable';
|
|
815
|
+
try {
|
|
816
|
+
const body = fs.readFileSync(shimPath, 'utf-8');
|
|
817
|
+
// The global shim must name the selected live version-home script. A
|
|
818
|
+
// wrapper can remain executable while its SOURCE points to an older or
|
|
819
|
+
// deleted version; detect that without running either script.
|
|
820
|
+
if (!body.includes(`SOURCE=${shellQuoteForHookShim(artifact.scriptPath)}`)) {
|
|
821
|
+
return 'source mismatch';
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
catch (err) {
|
|
825
|
+
const code = err.code;
|
|
826
|
+
return `cannot inspect (${code || 'error'})`;
|
|
827
|
+
}
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Gather managed generated-shim expectations for one installed version. The
|
|
832
|
+
* script must resolve inside that version home (or be an existing absolute
|
|
833
|
+
* subrule), matching the registrar's selection rules. This function never
|
|
834
|
+
* creates a shim or runs a hook.
|
|
835
|
+
*/
|
|
836
|
+
function managedHookRuntimeArtifactsForVersion(agent, version) {
|
|
837
|
+
if (!AGENTS[agent].supportsHooks)
|
|
838
|
+
return [];
|
|
839
|
+
const localHooksDir = getVersionHooksDir(agent, version);
|
|
840
|
+
const resolveScript = (script) => {
|
|
841
|
+
if (path.isAbsolute(script) && fs.existsSync(script))
|
|
842
|
+
return script;
|
|
843
|
+
return resolveContainedHookPath(localHooksDir, script);
|
|
844
|
+
};
|
|
845
|
+
const artifacts = [];
|
|
846
|
+
for (const [name, hookDef] of Object.entries(parseHookManifest({ warn: false }))) {
|
|
847
|
+
if (!hookDef.events || hookDef.events.length === 0 || !isValidHookShimName(name))
|
|
848
|
+
continue;
|
|
849
|
+
const scriptPath = resolveScript(hookDef.script);
|
|
850
|
+
if (!scriptPath)
|
|
851
|
+
continue;
|
|
852
|
+
const cache = parseCacheConfig(hookDef.cache);
|
|
853
|
+
const hasMatches = hookDef.matches != null && Object.keys(hookDef.matches).length > 0;
|
|
854
|
+
if (!cache && !hasMatches && !hookDef.matcher)
|
|
855
|
+
continue;
|
|
856
|
+
artifacts.push({
|
|
857
|
+
agent,
|
|
858
|
+
version,
|
|
859
|
+
name,
|
|
860
|
+
scriptPath,
|
|
861
|
+
shimPath: getHookShimPath(name),
|
|
862
|
+
cache,
|
|
863
|
+
matches: hookDef.matches,
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
return artifacts;
|
|
867
|
+
}
|
|
868
|
+
/** Inspect managed generated hook wrappers without executing user hook code. */
|
|
869
|
+
export function inspectBrokenManagedHookRuntimeArtifacts(filter, platform = process.platform) {
|
|
870
|
+
// Generated destinations are shared across every harness. Always select the
|
|
871
|
+
// owner from the global population first; an agent-scoped doctor call may
|
|
872
|
+
// restrict which shim names are relevant, but must not change the expected
|
|
873
|
+
// SOURCE for a shared wrapper.
|
|
874
|
+
const versions = iterHooksCapableVersions();
|
|
875
|
+
if (filter?.agent &&
|
|
876
|
+
filter.version &&
|
|
877
|
+
!versions.some((v) => v.agent === filter.agent && v.version === filter.version)) {
|
|
878
|
+
versions.push({ agent: filter.agent, version: filter.version });
|
|
879
|
+
}
|
|
880
|
+
const artifacts = [];
|
|
881
|
+
for (const { agent, version } of versions) {
|
|
882
|
+
artifacts.push(...managedHookRuntimeArtifactsForVersion(agent, version));
|
|
883
|
+
}
|
|
884
|
+
const requestedArtifacts = filter?.agent
|
|
885
|
+
? artifacts.filter((artifact) => artifact.agent === filter.agent &&
|
|
886
|
+
(!filter.version || artifact.version === filter.version))
|
|
887
|
+
: undefined;
|
|
888
|
+
const relevantShimPaths = requestedArtifacts
|
|
889
|
+
? new Set(eligibleHookRuntimeArtifacts(requestedArtifacts).map((artifact) => artifact.shimPath))
|
|
890
|
+
: undefined;
|
|
891
|
+
const broken = [];
|
|
892
|
+
for (const artifact of selectCanonicalHookRuntimeArtifacts(artifacts)) {
|
|
893
|
+
if (relevantShimPaths && !relevantShimPaths.has(artifact.shimPath))
|
|
894
|
+
continue;
|
|
895
|
+
const reason = hookRuntimeProblem(artifact, platform);
|
|
896
|
+
if (reason)
|
|
897
|
+
broken.push({ ...artifact, reason });
|
|
898
|
+
}
|
|
899
|
+
return broken.sort((a, b) => a.shimPath.localeCompare(b.shimPath) ||
|
|
900
|
+
`${a.agent}@${a.version}/${a.name}`.localeCompare(`${b.agent}@${b.version}/${b.name}`));
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Prefer the agent's global default when it appears in the candidate set;
|
|
904
|
+
* otherwise the newest non-isolated version. Shared shim paths embed a single
|
|
905
|
+
* SOURCE script — picking the wrong version permanently points every harness
|
|
906
|
+
* at a stale or dead path.
|
|
907
|
+
*/
|
|
908
|
+
function pickCanonicalHookRuntimeArtifact(candidates) {
|
|
909
|
+
if (candidates.length === 1)
|
|
910
|
+
return candidates[0];
|
|
911
|
+
const agents = Array.from(new Set(candidates.map((c) => c.agent))).sort();
|
|
912
|
+
for (const agent of agents) {
|
|
913
|
+
const defaultVersion = getGlobalDefault(agent);
|
|
914
|
+
if (!defaultVersion || isVersionIsolated(agent, defaultVersion))
|
|
915
|
+
continue;
|
|
916
|
+
const active = candidates.find((c) => c.agent === agent && c.version === defaultVersion);
|
|
917
|
+
if (active)
|
|
918
|
+
return active;
|
|
919
|
+
}
|
|
920
|
+
// listInstalledVersions is the canonical semver ordering. Scan backward
|
|
921
|
+
// rather than inventing another version comparator at this call site.
|
|
922
|
+
for (const agent of agents) {
|
|
923
|
+
for (const version of [...listInstalledVersions(agent)].reverse()) {
|
|
924
|
+
const newest = candidates.find((c) => c.agent === agent && c.version === version);
|
|
925
|
+
if (newest)
|
|
926
|
+
return newest;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
return candidates[0];
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* One repair target per unique shim path. Generated shims are global, so both
|
|
933
|
+
* unattended and explicit repair leave isolated/private version homes out.
|
|
934
|
+
*/
|
|
935
|
+
function selectCanonicalHookRuntimeArtifacts(artifacts) {
|
|
936
|
+
// No hook runtime exists for a version that does not support hooks. Isolated
|
|
937
|
+
// versions never own a global generated shim, even when a caller names one:
|
|
938
|
+
// selecting it would repoint every harness at a private version home.
|
|
939
|
+
const pool = eligibleHookRuntimeArtifacts(artifacts);
|
|
940
|
+
const byPath = new Map();
|
|
941
|
+
for (const artifact of pool) {
|
|
942
|
+
const list = byPath.get(artifact.shimPath) ?? [];
|
|
943
|
+
list.push(artifact);
|
|
944
|
+
byPath.set(artifact.shimPath, list);
|
|
945
|
+
}
|
|
946
|
+
const selected = [];
|
|
947
|
+
for (const group of byPath.values()) {
|
|
948
|
+
selected.push(pickCanonicalHookRuntimeArtifact(group));
|
|
949
|
+
}
|
|
950
|
+
return selected;
|
|
951
|
+
}
|
|
952
|
+
/** Versions that may own — and therefore diagnose — a shared generated shim. */
|
|
953
|
+
function eligibleHookRuntimeArtifacts(artifacts) {
|
|
954
|
+
return artifacts.filter((artifact) => supports(artifact.agent, 'hooks', artifact.version).ok &&
|
|
955
|
+
!isVersionIsolated(artifact.agent, artifact.version));
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Stable failure text: errno code + detector reason only.
|
|
959
|
+
* Never include absolute paths or randomized atomic-temp basenames — those
|
|
960
|
+
* break cross-device / menu-bar aggregation of identical failures. The hook
|
|
961
|
+
* name is attached by the caller (`hook shim <name>: …`).
|
|
962
|
+
*/
|
|
963
|
+
function stableHookRuntimeRepairFailure(before, err) {
|
|
964
|
+
const code = err?.code;
|
|
965
|
+
return `repair failed [${code || 'UNKNOWN'}]: ${before}`;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Regenerate one known-broken wrapper and prove the result is usable. Callers
|
|
969
|
+
* provide a snapshot from inspectBrokenManagedHookRuntimeArtifacts; this never
|
|
970
|
+
* loops or retries and returns a stable error for the current pass.
|
|
971
|
+
*
|
|
972
|
+
* Generation is delegated to generateHookShim (idempotent — preserves mtime when
|
|
973
|
+
* content already matches). This path never calls registerHooksToSettings,
|
|
974
|
+
* installHooks, or any sync routine.
|
|
975
|
+
*/
|
|
976
|
+
export function repairManagedHookRuntimeArtifact(artifact, platform = process.platform) {
|
|
977
|
+
const before = hookRuntimeProblem(artifact, platform);
|
|
978
|
+
// Already healthy (e.g. race with another pass) — no-op, no needsAttention.
|
|
979
|
+
if (!before)
|
|
980
|
+
return { repaired: false };
|
|
981
|
+
try {
|
|
982
|
+
generateHookShim({
|
|
983
|
+
name: artifact.name,
|
|
984
|
+
scriptPath: artifact.scriptPath,
|
|
985
|
+
cache: artifact.cache,
|
|
986
|
+
matches: artifact.matches,
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
catch (err) {
|
|
990
|
+
return { repaired: false, reason: stableHookRuntimeRepairFailure(before, err) };
|
|
991
|
+
}
|
|
992
|
+
// Post-repair reinspection — prove the artifact is usable without executing it.
|
|
993
|
+
const after = hookRuntimeProblem(artifact, platform);
|
|
994
|
+
return after
|
|
995
|
+
? { repaired: false, reason: `${before}; repair did not produce a usable shim (${after})` }
|
|
996
|
+
: { repaired: true };
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Bounded repair of all broken agents-managed generated hook shims.
|
|
1000
|
+
*
|
|
1001
|
+
* - Inspect first (read-only, no hook execution).
|
|
1002
|
+
* - One generation attempt per unique shim path per call (no retry, no timer).
|
|
1003
|
+
* - Canonical owner per shared path: global default, else newest non-isolated.
|
|
1004
|
+
* - Post-repair reinspection; unresolved findings become stable needsAttention.
|
|
1005
|
+
* - Never recurses into resource sync / registerHooksToSettings.
|
|
1006
|
+
*
|
|
1007
|
+
* This is the shared routine used by the self-heal `hook-runtime` check and
|
|
1008
|
+
* exported for the doctor track.
|
|
1009
|
+
*/
|
|
1010
|
+
export function repairManagedHookRuntimeArtifacts(opts = {}) {
|
|
1011
|
+
const platform = opts.platform ?? process.platform;
|
|
1012
|
+
const dryRun = opts.dryRun ?? false;
|
|
1013
|
+
const brokenBefore = inspectBrokenManagedHookRuntimeArtifacts(opts.filter, platform);
|
|
1014
|
+
const targets = selectCanonicalHookRuntimeArtifacts(brokenBefore);
|
|
1015
|
+
const attemptedPaths = [];
|
|
1016
|
+
const attempts = [];
|
|
1017
|
+
const fixed = [];
|
|
1018
|
+
const needsAttention = [];
|
|
1019
|
+
for (const artifact of targets) {
|
|
1020
|
+
attemptedPaths.push(artifact.shimPath);
|
|
1021
|
+
if (dryRun) {
|
|
1022
|
+
// Would-fix: same shape as a real fix so doctor dry-run and daemon previews
|
|
1023
|
+
// stay consistent with other HealChecks.
|
|
1024
|
+
attempts.push({
|
|
1025
|
+
name: artifact.name,
|
|
1026
|
+
path: artifact.shimPath,
|
|
1027
|
+
reasonBefore: artifact.reason,
|
|
1028
|
+
attempted: false,
|
|
1029
|
+
repaired: false,
|
|
1030
|
+
});
|
|
1031
|
+
fixed.push(`hook shim ${artifact.name}`);
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
const result = repairManagedHookRuntimeArtifact(artifact, platform);
|
|
1035
|
+
attempts.push({
|
|
1036
|
+
name: artifact.name,
|
|
1037
|
+
path: artifact.shimPath,
|
|
1038
|
+
reasonBefore: artifact.reason,
|
|
1039
|
+
attempted: true,
|
|
1040
|
+
repaired: result.repaired,
|
|
1041
|
+
reason: result.reason,
|
|
1042
|
+
});
|
|
1043
|
+
if (result.repaired) {
|
|
1044
|
+
fixed.push(`hook shim ${artifact.name}`);
|
|
1045
|
+
}
|
|
1046
|
+
else if (result.reason) {
|
|
1047
|
+
// Stable needs-attention wording for aggregation across devices / menubar.
|
|
1048
|
+
// No reason means already-healthy no-op (race) — do not emit attention noise.
|
|
1049
|
+
needsAttention.push(`hook shim ${artifact.name}: ${result.reason}`);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
return { brokenBefore, attemptedPaths, attempts, fixed, needsAttention };
|
|
1053
|
+
}
|
|
775
1054
|
/**
|
|
776
1055
|
* Verify that every hook the manifest says should be wired is actually
|
|
777
1056
|
* referenced in that version's harness-native config, not merely present as a
|
|
@@ -787,11 +1066,16 @@ const TOML_ARRAY_HOOK_FAMILY = ['kimi'];
|
|
|
787
1066
|
* resolveHookCommand performs, so it never mutates the version home.
|
|
788
1067
|
*/
|
|
789
1068
|
export function checkVersionHookWiring(agent, version) {
|
|
1069
|
+
// Runtime verification does not rely on parsing a native settings format.
|
|
1070
|
+
// That lets doctor catch a deleted generated shim for every hooks-capable
|
|
1071
|
+
// harness, including families whose wiring schema is intentionally unsupported.
|
|
1072
|
+
const runtimeBroken = inspectBrokenManagedHookRuntimeArtifacts({ agent, version })
|
|
1073
|
+
.map(({ name, shimPath, reason }) => ({ name, path: shimPath, reason }));
|
|
790
1074
|
if (!AGENTS[agent].supportsHooks ||
|
|
791
1075
|
(!SETTINGS_JSON_HOOK_FAMILY.includes(agent) &&
|
|
792
1076
|
!HOOKS_JSON_HOOK_FAMILY.includes(agent) &&
|
|
793
1077
|
!TOML_ARRAY_HOOK_FAMILY.includes(agent))) {
|
|
794
|
-
return { supported: false, unwired: [], wired: [] };
|
|
1078
|
+
return { supported: false, unwired: [], wired: [], runtimeBroken };
|
|
795
1079
|
}
|
|
796
1080
|
const versionHome = getVersionHomePath(agent, version);
|
|
797
1081
|
const settingsPath = HOOKS_JSON_HOOK_FAMILY.includes(agent)
|
|
@@ -855,6 +1139,7 @@ export function checkVersionHookWiring(agent, version) {
|
|
|
855
1139
|
settingsMissing: expected.length > 0,
|
|
856
1140
|
unwired: [],
|
|
857
1141
|
wired: [],
|
|
1142
|
+
runtimeBroken,
|
|
858
1143
|
};
|
|
859
1144
|
}
|
|
860
1145
|
let config;
|
|
@@ -872,6 +1157,7 @@ export function checkVersionHookWiring(agent, version) {
|
|
|
872
1157
|
settingsUnparseable: true,
|
|
873
1158
|
unwired: [],
|
|
874
1159
|
wired: [],
|
|
1160
|
+
runtimeBroken,
|
|
875
1161
|
};
|
|
876
1162
|
}
|
|
877
1163
|
// Command strings actually referenced, keyed by (event, matcher) — a hook wired
|
|
@@ -921,7 +1207,7 @@ export function checkVersionHookWiring(agent, version) {
|
|
|
921
1207
|
const isWired = (entry) => wiredByGroup.get(groupKey(entry.event, entry.matcher))?.has(entry.command) ?? false;
|
|
922
1208
|
const unwired = expected.filter((entry) => !isWired(entry));
|
|
923
1209
|
const wired = expected.filter(isWired);
|
|
924
|
-
return { supported: true, settingsPath, expected: expected.length, unwired, wired };
|
|
1210
|
+
return { supported: true, settingsPath, expected: expected.length, unwired, wired, runtimeBroken };
|
|
925
1211
|
}
|
|
926
1212
|
/**
|
|
927
1213
|
* Check if a hook installed in a specific version matches central content.
|
|
@@ -79,6 +79,9 @@ export const REMOTE_PASSTHROUGH = {
|
|
|
79
79
|
profiles: {},
|
|
80
80
|
defaults: {},
|
|
81
81
|
alias: {},
|
|
82
|
+
// Installations are per-machine, so updating one on a peer means running it
|
|
83
|
+
// there — the same local/remote shape `add` would need.
|
|
84
|
+
update: {},
|
|
82
85
|
// lifecycle
|
|
83
86
|
teams: {},
|
|
84
87
|
message: {},
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frozen agent installations.
|
|
3
|
+
*
|
|
4
|
+
* An installation's identity (`id`, `label`) is stable for its whole life; the
|
|
5
|
+
* vendor release it carries moves only on an explicit `agents update`. This
|
|
6
|
+
* barrel is the supported surface — import from here, not from the files behind
|
|
7
|
+
* it, so the internal split can change without breaking callers (the Cursor
|
|
8
|
+
* per-installation isolation track consumes exactly this).
|
|
9
|
+
*/
|
|
10
|
+
export { INSTALLATION_RECORD_FILE, INSTALLATION_SCHEMA, type Installation, type InstallationRelease, type UpdateOutcome, type UpdateStrategyId, } from './types.js';
|
|
11
|
+
export { createInstallation, ensureInstallation, installationDir, installationRecordPath, listInstallationLabels, listInstallations, mintInstallationId, readInstallation, recordRelease, writeInstallation, } from './store.js';
|
|
12
|
+
export { InstallationAmbiguousError, InstallationNotFoundError, describeInstallation, resolveInstallation, type ResolveInstallationOptions, } from './resolve.js';
|
|
13
|
+
export { assertValidRelease, selectUpdateStrategy, supportsPinnedUpdate, type UpdateStrategy, } from './strategies.js';
|
|
14
|
+
export { updateInstallation, type UpdateInstallationOptions } from './update.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frozen agent installations.
|
|
3
|
+
*
|
|
4
|
+
* An installation's identity (`id`, `label`) is stable for its whole life; the
|
|
5
|
+
* vendor release it carries moves only on an explicit `agents update`. This
|
|
6
|
+
* barrel is the supported surface — import from here, not from the files behind
|
|
7
|
+
* it, so the internal split can change without breaking callers (the Cursor
|
|
8
|
+
* per-installation isolation track consumes exactly this).
|
|
9
|
+
*/
|
|
10
|
+
export { INSTALLATION_RECORD_FILE, INSTALLATION_SCHEMA, } from './types.js';
|
|
11
|
+
export { createInstallation, ensureInstallation, installationDir, installationRecordPath, listInstallationLabels, listInstallations, mintInstallationId, readInstallation, recordRelease, writeInstallation, } from './store.js';
|
|
12
|
+
export { InstallationAmbiguousError, InstallationNotFoundError, describeInstallation, resolveInstallation, } from './resolve.js';
|
|
13
|
+
export { assertValidRelease, selectUpdateStrategy, supportsPinnedUpdate, } from './strategies.js';
|
|
14
|
+
export { updateInstallation } from './update.js';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AgentId } from '../types.js';
|
|
2
|
+
import type { Installation } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Addressing a frozen installation.
|
|
5
|
+
*
|
|
6
|
+
* A selector matches either the installation's stable {@link Installation.label}
|
|
7
|
+
* or the vendor release it currently carries — the label because that is what
|
|
8
|
+
* every persisted reference and every `agents add` invocation uses, the release
|
|
9
|
+
* because after an update the two differ and a user reading `agents view` may
|
|
10
|
+
* name either. Matching both is also precisely what makes duplicate same-release
|
|
11
|
+
* installations addressable at all: two installs can share a release, so the
|
|
12
|
+
* release alone is not an identifier and the ambiguity has to be reported rather
|
|
13
|
+
* than silently resolved to whichever sorted first.
|
|
14
|
+
*/
|
|
15
|
+
export declare class InstallationNotFoundError extends Error {
|
|
16
|
+
readonly agent: AgentId;
|
|
17
|
+
readonly selector: string | undefined;
|
|
18
|
+
readonly available: readonly Installation[];
|
|
19
|
+
constructor(agent: AgentId, selector: string | undefined, available: readonly Installation[]);
|
|
20
|
+
}
|
|
21
|
+
export declare class InstallationAmbiguousError extends Error {
|
|
22
|
+
readonly agent: AgentId;
|
|
23
|
+
readonly selector: string | undefined;
|
|
24
|
+
readonly candidates: readonly Installation[];
|
|
25
|
+
constructor(agent: AgentId, selector: string | undefined, candidates: readonly Installation[]);
|
|
26
|
+
}
|
|
27
|
+
/** `2.0.65` when frozen at its original release, `2.0.65 (release 2.0.71)` after an update. */
|
|
28
|
+
export declare function describeInstallation(installation: Installation): string;
|
|
29
|
+
export interface ResolveInstallationOptions {
|
|
30
|
+
/**
|
|
31
|
+
* An `agents accounts` label. Narrows to the installation currently signed
|
|
32
|
+
* into that account before the selector is applied.
|
|
33
|
+
*/
|
|
34
|
+
account?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolve `<agent>[@<selector>]` to exactly one installation.
|
|
38
|
+
*
|
|
39
|
+
* With no selector: the agent's default installation when one is pinned, else
|
|
40
|
+
* the sole installation. Never a "newest wins" guess — picking for the user
|
|
41
|
+
* across several installs is how an update lands on the wrong one.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveInstallation(agent: AgentId, selector: string | undefined, options?: ResolveInstallationOptions): Promise<Installation>;
|