javi-forge 1.35.1 → 1.37.0

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.
@@ -0,0 +1,106 @@
1
+ /**
2
+ * `javi-forge hooks <install|doctor|repair> codex` — console-only renderer for
3
+ * the Codex PreToolUse guard library (agent-agnostic slice 2). It wires the
4
+ * install/doctor/repair lib fns to human output + exit codes; it adds NO new
5
+ * security logic and never touches `runTransaction`/secure-fs directly.
6
+ *
7
+ * Doctor exit code follows the effective-execution verdict (runnable → 0,
8
+ * blocked → 1, inconclusive → 2), independent of any component health — the same
9
+ * honest-execution contract as the Claude renderer. The UNTRUSTED state is a
10
+ * `blocked` verdict (an untrusted Codex hook is silently skipped), so a fresh
11
+ * install correctly reports blocked until the user grants trust.
12
+ */
13
+ import { doctorCodexPreToolUse, installCodexPreToolUse, repairCodexPreToolUse, } from "../lib/codex-hook-manager.js";
14
+ function renderWarnings(warnings, log) {
15
+ if (warnings.length === 0)
16
+ return;
17
+ log("warnings:");
18
+ for (const w of warnings)
19
+ log(` ${w}`);
20
+ }
21
+ function renderMutation(verb, result, log, logError) {
22
+ if (result.ok) {
23
+ log(`${verb} codex: ok`);
24
+ if (result.changed.length > 0) {
25
+ log("changed:");
26
+ for (const p of result.changed)
27
+ log(` ${p}`);
28
+ }
29
+ else {
30
+ log("changed: nothing (already up to date)");
31
+ }
32
+ log(`trust: ${result.report.trust.state}`);
33
+ if (result.report.trust.state === "untrusted") {
34
+ log(` → ${result.report.trust.grantCommand}`);
35
+ }
36
+ renderWarnings(result.warnings, log);
37
+ return 0;
38
+ }
39
+ logError(`${verb} codex: refused`);
40
+ for (const e of result.errors)
41
+ logError(` ${e}`);
42
+ renderWarnings(result.warnings, log);
43
+ return 1;
44
+ }
45
+ function renderDoctor(report, log) {
46
+ log(`doctor codex: ${report.healthy ? "healthy" : "unhealthy"}`);
47
+ if (report.platformSupport)
48
+ log(` platform-support: ${report.platformSupport.state} — ${report.platformSupport.guidance}`);
49
+ log(` hooks.json: ${report.hooksJson.state}`);
50
+ log(` config: [features] hooks=${report.config.featuresHooks} (readable: ${report.config.readable})`);
51
+ log(` trust: ${report.trust.state}`);
52
+ log(` asset: ${report.asset.state}`);
53
+ log(` node: ${report.node.version ?? "unavailable"} (min-satisfied: ${report.node.satisfiesMinimum})`);
54
+ const onPath = report.nodeOnPath;
55
+ const onPathDetail = onPath.status === "resolved"
56
+ ? ` ${onPath.version}`
57
+ : onPath.status === "unknown"
58
+ ? ` — ${onPath.detail}`
59
+ : "";
60
+ log(` node-on-PATH: ${onPath.status}${onPathDetail} (heuristic: this process' PATH)`);
61
+ log(` execution: ${report.execution.status}`);
62
+ if (report.execution.blockers.length > 0) {
63
+ log(" blockers:");
64
+ for (const b of report.execution.blockers)
65
+ log(` - ${b}`);
66
+ }
67
+ if (report.execution.unknownSources.length > 0) {
68
+ log(" unknown-sources:");
69
+ for (const u of report.execution.unknownSources)
70
+ log(` - ${u}`);
71
+ }
72
+ if (report.execution.residual.length > 0) {
73
+ log(" execution-residual:");
74
+ for (const r of report.execution.residual)
75
+ log(` - ${r}`);
76
+ }
77
+ if (report.remediation.length > 0) {
78
+ log(" remediation:");
79
+ for (const r of report.remediation)
80
+ log(` - ${r}`);
81
+ }
82
+ if (report.execution.status === "blocked")
83
+ return 1;
84
+ if (report.execution.status === "inconclusive")
85
+ return 2;
86
+ return 0;
87
+ }
88
+ export async function runCodexHookCommand(sub, _cwd, opts, deps = {}) {
89
+ const log = deps.log ?? ((m) => console.log(m));
90
+ const logError = deps.logError ?? ((m) => console.error(m));
91
+ const install = deps.install ?? installCodexPreToolUse;
92
+ const doctor = deps.doctor ?? doctorCodexPreToolUse;
93
+ const repair = deps.repair ?? repairCodexPreToolUse;
94
+ // Codex config is user-global (~/.codex). `cwd` is accepted for CLI symmetry
95
+ // with the Claude command; the home dir is what the manager operates on.
96
+ const home = deps.homeDir;
97
+ if (sub === "install") {
98
+ return renderMutation("install", await install(home), log, logError);
99
+ }
100
+ if (sub === "repair") {
101
+ const result = await repair(home, { force: opts.force === true });
102
+ return renderMutation("repair", result, log, logError);
103
+ }
104
+ return renderDoctor(await doctor(home), log);
105
+ }
106
+ //# sourceMappingURL=codex-hooks.js.map
@@ -1,3 +1,4 @@
1
+ import { type PlatformSupport } from "../lib/platform-support.js";
1
2
  import type { InitOptions } from "../types/index.js";
2
3
  import type { StepCallback } from "./init/types.js";
3
4
  /**
@@ -6,5 +7,12 @@ import type { StepCallback } from "./init/types.js";
6
7
  *
7
8
  * Pure dispatch: every step lives in src/commands/init/steps/.
8
9
  */
9
- export declare function initProject(options: InitOptions, onStep: StepCallback): Promise<void>;
10
+ export type InitProjectResult = {
11
+ ok: true;
12
+ } | {
13
+ ok: false;
14
+ refusalCode: string;
15
+ platformSupport: PlatformSupport;
16
+ };
17
+ export declare function initProject(options: InitOptions, onStep: StepCallback): Promise<InitProjectResult>;
10
18
  //# sourceMappingURL=init.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { ensureDirExists } from "../lib/common.js";
2
+ import { resolvePlatformSupport, } from "../lib/platform-support.js";
2
3
  import { stepAgentSkills } from "./init/steps/agent-skills.js";
3
4
  import { stepAISync } from "./init/steps/ai-sync.js";
4
5
  import { stepCITemplate, stepDependabot } from "./init/steps/ci.js";
@@ -15,13 +16,15 @@ import { stepMemory } from "./init/steps/memory.js";
15
16
  import { stepMock } from "./init/steps/mock.js";
16
17
  import { stepSDD } from "./init/steps/sdd.js";
17
18
  import { stepSecurityHooks } from "./init/steps/security.js";
18
- /**
19
- * Main init orchestrator: bootstraps a project with CI, git hooks,
20
- * memory module, AI config sync, SDD, ghagga, and friends.
21
- *
22
- * Pure dispatch: every step lives in src/commands/init/steps/.
23
- */
24
19
  export async function initProject(options, onStep) {
20
+ const platformSupport = resolvePlatformSupport(process.platform);
21
+ if (platformSupport) {
22
+ return {
23
+ ok: false,
24
+ refusalCode: platformSupport.refusalCode,
25
+ platformSupport,
26
+ };
27
+ }
25
28
  const { projectDir, dryRun } = options;
26
29
  // Ensure project directory exists before any steps
27
30
  if (!dryRun && projectDir) {
@@ -47,5 +50,6 @@ export async function initProject(options, onStep) {
47
50
  await stepLocalAi(ctx);
48
51
  await stepAgentSkills(ctx);
49
52
  await stepManifest(ctx);
53
+ return { ok: true };
50
54
  }
51
55
  //# sourceMappingURL=init.js.map
@@ -24,6 +24,12 @@ export declare const LEGACY_FILE_SHA256 = "b4638222ecddc2daac6ec3339596d853a6269
24
24
  export declare const MANAGED_MATCHER = "Bash|PowerShell|Read|Write|Edit";
25
25
  /** Single project-local MJS argument, kept as a literal placeholder path. */
26
26
  export declare const MANAGED_ASSET_ARG = "${CLAUDE_PROJECT_DIR}/.claude/hooks/javi-forge-skillguard-pre-tool-use.mjs";
27
+ /**
28
+ * The agent selector the install writer appends after the asset path. The runtime
29
+ * fails closed without it (no agent config = cannot know what to protect), so it is
30
+ * part of the managed command shape and the canonical settings identity.
31
+ */
32
+ export declare const MANAGED_AGENT_ARG = "--agent=claude";
27
33
  /** Exact asset filename under `.claude/hooks/`. */
28
34
  export declare const ASSET_NAME = "javi-forge-skillguard-pre-tool-use.mjs";
29
35
  /** Exact first-line comment marking the managed asset. */
@@ -25,6 +25,12 @@ export const LEGACY_FILE_SHA256 = "b4638222ecddc2daac6ec3339596d853a626906bbd123
25
25
  export const MANAGED_MATCHER = "Bash|PowerShell|Read|Write|Edit";
26
26
  /** Single project-local MJS argument, kept as a literal placeholder path. */
27
27
  export const MANAGED_ASSET_ARG = "${CLAUDE_PROJECT_DIR}/.claude/hooks/javi-forge-skillguard-pre-tool-use.mjs";
28
+ /**
29
+ * The agent selector the install writer appends after the asset path. The runtime
30
+ * fails closed without it (no agent config = cannot know what to protect), so it is
31
+ * part of the managed command shape and the canonical settings identity.
32
+ */
33
+ export const MANAGED_AGENT_ARG = "--agent=claude";
28
34
  /** Exact asset filename under `.claude/hooks/`. */
29
35
  export const ASSET_NAME = "javi-forge-skillguard-pre-tool-use.mjs";
30
36
  /** Exact first-line comment marking the managed asset. */
@@ -44,7 +50,7 @@ export function managedHandler(assetSha = SAMPLE_ASSET_SHA256) {
44
50
  return {
45
51
  type: "command",
46
52
  command: "node",
47
- args: [MANAGED_ASSET_ARG],
53
+ args: [MANAGED_ASSET_ARG, MANAGED_AGENT_ARG],
48
54
  timeout: 30,
49
55
  statusMessage: managedStatusMessage(assetSha),
50
56
  };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Agent adapter registry (agent-agnostic slice 2). One descriptor per host
3
+ * (`claude`, `codex`) capturing the per-agent facts the SkillGuard installer and
4
+ * doctor need: config paths, the protected managed set, the project-root source,
5
+ * the settings-schema validator (SHARED — both hosts use the identical hooks
6
+ * schema), the managed marker, the deny protocol, and the trust model.
7
+ *
8
+ * This descriptor is additive: Codex is routed through it, while Claude keeps its
9
+ * existing, byte-identical runtime path (the descriptor only DESCRIBES Claude; it
10
+ * does not change how the Claude command executes). The CLI dispatch decides the
11
+ * valid `hooks <sub> <agent>` target set through `isAgentId` (backed by the
12
+ * `AGENT_ADAPTERS` keys) — this registry is the single source of agent truth, and
13
+ * a new agent added here cannot be silently dropped: the dispatch's command-loader
14
+ * map is typed `Record<AgentId, …>`, so omitting a loader is a compile error.
15
+ */
16
+ import { validateSettingsShape } from "./claude-hook-settings.js";
17
+ export type AgentId = "claude" | "codex";
18
+ export type TrustState = "trusted" | "untrusted" | "unknown";
19
+ export interface TrustDescriptor {
20
+ /** Detect whether the installed hook is TRUSTED from the host config text. */
21
+ detect(configText: string, hooksFile: string): TrustState;
22
+ /** The exact step a human runs to grant trust (no non-interactive path exists). */
23
+ grantCommand(hooksFile: string): string;
24
+ }
25
+ export interface AgentAdapter {
26
+ id: AgentId;
27
+ /** Resolve the two host config files from a base dir (project root or home). */
28
+ configPaths(baseDir: string): {
29
+ hooksFile: string;
30
+ settingsFile: string;
31
+ };
32
+ /** Relative protected paths the guard refuses writes to (from AGENT_CONFIGS). */
33
+ managedSet: readonly string[];
34
+ /** Project-root source: an env var, or null → the envelope `cwd` (Codex). */
35
+ projectDir: {
36
+ envVar: string | null;
37
+ };
38
+ /** SHARED settings-schema validator — the hooks container shape is identical. */
39
+ settingsSchema: typeof validateSettingsShape;
40
+ /** The managed asset marker for this host. */
41
+ marker: string;
42
+ /** Deny protocol emitted by the shared `.mjs` — the same on both hosts. */
43
+ emitDeny: "exit2+stderr";
44
+ /** Hook-trust model, or null when the host has none (Claude). */
45
+ trust: TrustDescriptor | null;
46
+ }
47
+ export declare const claudeAdapter: AgentAdapter;
48
+ export declare const codexAdapter: AgentAdapter;
49
+ export declare const AGENT_ADAPTERS: Record<AgentId, AgentAdapter>;
50
+ /**
51
+ * Whether a raw CLI token names a known agent adapter. Backed by the
52
+ * `AGENT_ADAPTERS` registry so there is ONE source deciding valid targets: adding
53
+ * an adapter to the registry automatically widens the accepted CLI set.
54
+ */
55
+ export declare function isAgentId(value: unknown): value is AgentId;
56
+ //# sourceMappingURL=agent-adapter.d.ts.map
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Agent adapter registry (agent-agnostic slice 2). One descriptor per host
3
+ * (`claude`, `codex`) capturing the per-agent facts the SkillGuard installer and
4
+ * doctor need: config paths, the protected managed set, the project-root source,
5
+ * the settings-schema validator (SHARED — both hosts use the identical hooks
6
+ * schema), the managed marker, the deny protocol, and the trust model.
7
+ *
8
+ * This descriptor is additive: Codex is routed through it, while Claude keeps its
9
+ * existing, byte-identical runtime path (the descriptor only DESCRIBES Claude; it
10
+ * does not change how the Claude command executes). The CLI dispatch decides the
11
+ * valid `hooks <sub> <agent>` target set through `isAgentId` (backed by the
12
+ * `AGENT_ADAPTERS` keys) — this registry is the single source of agent truth, and
13
+ * a new agent added here cannot be silently dropped: the dispatch's command-loader
14
+ * map is typed `Record<AgentId, …>`, so omitting a loader is a compile error.
15
+ */
16
+ import path from "node:path";
17
+ import { ASSET_NAME } from "./__fixtures__/claude-hook-ownership.js";
18
+ import { validateSettingsShape } from "./claude-hook-settings.js";
19
+ import { codexConfigPaths, codexTrustGrantCommand, hasCodexTrustEntry, } from "./codex-hook-manager.js";
20
+ const CLAUDE_MANAGED_SET = [
21
+ ".claude/settings.json",
22
+ ".claude/settings.local.json",
23
+ ".claude/CLAUDE.md",
24
+ "CLAUDE.md",
25
+ ".javi-forge/ci.yaml",
26
+ ".claude/hooks/",
27
+ ".claude/agents/",
28
+ ".claude/skills/",
29
+ ];
30
+ const CODEX_MANAGED_SET = [
31
+ ".codex/hooks.json",
32
+ ".claude/settings.json",
33
+ ".claude/settings.local.json",
34
+ ".claude/CLAUDE.md",
35
+ "CLAUDE.md",
36
+ ".javi-forge/ci.yaml",
37
+ ".claude/hooks/",
38
+ ".claude/agents/",
39
+ ".claude/skills/",
40
+ ];
41
+ export const claudeAdapter = {
42
+ id: "claude",
43
+ configPaths(projectDir) {
44
+ return {
45
+ hooksFile: path.join(projectDir, ".claude", "hooks", ASSET_NAME),
46
+ settingsFile: path.join(projectDir, ".claude", "settings.json"),
47
+ };
48
+ },
49
+ managedSet: CLAUDE_MANAGED_SET,
50
+ projectDir: { envVar: "CLAUDE_PROJECT_DIR" },
51
+ settingsSchema: validateSettingsShape,
52
+ marker: "// javi-forge-managed: claude-pretooluse v1",
53
+ emitDeny: "exit2+stderr",
54
+ trust: null,
55
+ };
56
+ export const codexAdapter = {
57
+ id: "codex",
58
+ configPaths(homeDir) {
59
+ const paths = codexConfigPaths(homeDir);
60
+ return { hooksFile: paths.hooksFile, settingsFile: paths.configFile };
61
+ },
62
+ managedSet: CODEX_MANAGED_SET,
63
+ projectDir: { envVar: null },
64
+ settingsSchema: validateSettingsShape,
65
+ marker: "// javi-forge-managed: codex-pretooluse v1",
66
+ emitDeny: "exit2+stderr",
67
+ trust: {
68
+ detect(configText, hooksFile) {
69
+ return hasCodexTrustEntry(configText, hooksFile)
70
+ ? "trusted"
71
+ : "untrusted";
72
+ },
73
+ grantCommand: codexTrustGrantCommand,
74
+ },
75
+ };
76
+ export const AGENT_ADAPTERS = {
77
+ claude: claudeAdapter,
78
+ codex: codexAdapter,
79
+ };
80
+ /**
81
+ * Whether a raw CLI token names a known agent adapter. Backed by the
82
+ * `AGENT_ADAPTERS` registry so there is ONE source deciding valid targets: adding
83
+ * an adapter to the registry automatically widens the accepted CLI set.
84
+ */
85
+ export function isAgentId(value) {
86
+ return typeof value === "string" && Object.hasOwn(AGENT_ADAPTERS, value);
87
+ }
88
+ //# sourceMappingURL=agent-adapter.js.map
@@ -8,6 +8,7 @@
8
8
  * Slice-3 seams — Slice 3 GROWS this file, it does not relocate this code.
9
9
  */
10
10
  import { type ClaudeHookComponentState, type SettingsClassification, type SettingsIdentityManifest } from "./claude-hook-settings.js";
11
+ import { type PlatformSupport } from "./platform-support.js";
11
12
  import { type AclCapability, type SpawnFn } from "./secure-fs-posix.js";
12
13
  import { type PlatformSecureFs } from "./secure-fs-transaction.js";
13
14
  declare const COVERAGE: readonly ["Bash", "PowerShell", "Read", "Write", "Edit"];
@@ -29,6 +30,7 @@ export interface ClaudeHookAssetClassification {
29
30
  }
30
31
  export interface ClaudeHookDoctorReport {
31
32
  healthy: boolean;
33
+ platformSupport?: PlatformSupport;
32
34
  settings: {
33
35
  state: ClaudeHookComponentState;
34
36
  version?: number;
@@ -207,8 +209,9 @@ export declare function doctorClaudePreToolUse(projectDir: string, options?: {
207
209
  execution?: ExecutionProbeEnv;
208
210
  /** Injectable read-only ACL capability probe (defaults to the real one). */
209
211
  aclProbe?: () => Promise<AclCapability>;
212
+ platform?: NodeJS.Platform;
210
213
  }): Promise<ClaudeHookDoctorReport>;
211
- export interface ClaudeHookMutationResult {
214
+ interface ClaudeHookMutationResultWithReport {
212
215
  ok: boolean;
213
216
  changed: string[];
214
217
  backups: string[];
@@ -220,7 +223,19 @@ export interface ClaudeHookMutationResult {
220
223
  * which is strictly worse than an exec-form guard that may not resolve.
221
224
  */
222
225
  warnings: string[];
226
+ lifecycleRefusal?: never;
227
+ }
228
+ interface ClaudeHookLifecycleRefusal {
229
+ ok: false;
230
+ changed: string[];
231
+ backups: string[];
232
+ errors: string[];
233
+ warnings: string[];
234
+ /** The lifecycle gate refused before any doctor or installed-state probe. */
235
+ lifecycleRefusal: PlatformSupport;
236
+ report?: never;
223
237
  }
238
+ export type ClaudeHookMutationResult = ClaudeHookMutationResultWithReport | ClaudeHookLifecycleRefusal;
224
239
  /** Injectable deps so tests drive `_run` with a fake `PlatformSecureFs`. */
225
240
  export interface ClaudeHookRunDeps {
226
241
  secureFs?: PlatformSecureFs | null;
@@ -236,6 +251,7 @@ export interface ClaudeHookRunDeps {
236
251
  aclProbe?: () => Promise<AclCapability>;
237
252
  /** Injectable node-on-PATH heuristic, shared by the warning and the report. */
238
253
  nodeProbe?: () => Promise<NodeOnPathProbe>;
254
+ doctor?: typeof doctorClaudePreToolUse;
239
255
  }
240
256
  /** Internal deps-taking entry; tests drive it with a fake `PlatformSecureFs`. */
241
257
  export declare function _run(projectDir: string, mode: "install" | "repair", options: {
@@ -14,7 +14,8 @@ import os from "node:os";
14
14
  import path from "node:path";
15
15
  import { CLAUDE_HOOK_ASSETS_DIR } from "../constants.js";
16
16
  import { ASSET_MANAGED_MARKER, ASSET_NAME, } from "./__fixtures__/claude-hook-ownership.js";
17
- import { buildManagedContainer, classifySettingsEntry, isPlainObject, LEGACY_FILE_SHA256, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, planForceReplace, planLegacyCohortExcision, planManagedClaudeHookMerge, scanExecutionFlags, } from "./claude-hook-settings.js";
17
+ import { buildManagedContainer, classifySettingsEntry, isPlainObject, LEGACY_FILE_SHA256, MANAGED_AGENT_ARG, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, planForceReplace, planLegacyCohortExcision, planManagedClaudeHookMerge, scanExecutionFlags, } from "./claude-hook-settings.js";
18
+ import { resolvePlatformSupport, } from "./platform-support.js";
18
19
  import { safeReadFile } from "./safe-read.js";
19
20
  import { ACL_DETAIL, probeAclCapability, selectSecureFs, } from "./secure-fs-posix.js";
20
21
  import { runTransaction, } from "./secure-fs-transaction.js";
@@ -174,8 +175,9 @@ function settingsSignals(value, classification, currentAssetSha) {
174
175
  const commandShapeExact = handler.type === "command" &&
175
176
  handler.command === "node" &&
176
177
  Array.isArray(args) &&
177
- args.length === 1 &&
178
+ args.length === 2 &&
178
179
  args[0] === MANAGED_ASSET_ARG &&
180
+ args[1] === MANAGED_AGENT_ARG &&
179
181
  handler.timeout === 30;
180
182
  let assetSettingsConsistent = false;
181
183
  if (typeof handler.statusMessage === "string" &&
@@ -536,7 +538,9 @@ export async function doctorClaudePreToolUse(projectDir, options) {
536
538
  execution.blockers.some((blocker) => blocker.startsWith("guard:"))) {
537
539
  remediation.add(aclRemediation);
538
540
  }
541
+ const platformSupport = resolvePlatformSupport(options?.platform ?? process.platform);
539
542
  return {
543
+ ...(platformSupport ? { platformSupport } : {}),
540
544
  healthy,
541
545
  settings: {
542
546
  state: settings.state,
@@ -711,8 +715,20 @@ function serializeSettings(container) {
711
715
  }
712
716
  /** Internal deps-taking entry; tests drive it with a fake `PlatformSecureFs`. */
713
717
  export async function _run(projectDir, mode, options, deps) {
714
- const manifest = deps.manifest ?? (await readManifest());
715
718
  const platform = deps.platform ?? process.platform;
719
+ const platformSupport = resolvePlatformSupport(platform);
720
+ if (platformSupport) {
721
+ return {
722
+ ok: false,
723
+ changed: [],
724
+ backups: [],
725
+ errors: [platformSupport.refusalCode],
726
+ warnings: [platformSupport.guidance],
727
+ lifecycleRefusal: platformSupport,
728
+ };
729
+ }
730
+ const doctorFn = deps.doctor ?? doctorClaudePreToolUse;
731
+ const manifest = deps.manifest ?? (await readManifest());
716
732
  const secureFs = deps.secureFs !== undefined ? deps.secureFs : selectSecureFs(platform);
717
733
  const clock = deps.clock ?? (() => new Date());
718
734
  const nonce = deps.nonce ?? (() => randomBytes(4).toString("hex"));
@@ -724,7 +740,7 @@ export async function _run(projectDir, mode, options, deps) {
724
740
  // report, so the run never spawns `node --version` twice and the warning, the
725
741
  // report row and the verdict can never disagree about the same PATH.
726
742
  const nodeOnPath = await (deps.nodeProbe ?? probeNodeOnPath)();
727
- const doctor = () => doctorClaudePreToolUse(projectDir, {
743
+ const doctor = () => doctorFn(projectDir, {
728
744
  manifest,
729
745
  aclProbe: deps.aclProbe,
730
746
  execution: { nodeProbe: async () => nodeOnPath },
@@ -8,8 +8,8 @@
8
8
  * removal/merge PLANNING only. Identity is always recomputed from observed
9
9
  * structure; a marker only claims ownership, it never proves it.
10
10
  */
11
- import { ASSET_SHA_PLACEHOLDER, LEGACY_FILE_SHA256, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX } from "./__fixtures__/claude-hook-ownership.js";
12
- export { ASSET_SHA_PLACEHOLDER, LEGACY_FILE_SHA256, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, };
11
+ import { ASSET_SHA_PLACEHOLDER, LEGACY_FILE_SHA256, MANAGED_AGENT_ARG, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX } from "./__fixtures__/claude-hook-ownership.js";
12
+ export { ASSET_SHA_PLACEHOLDER, LEGACY_FILE_SHA256, MANAGED_AGENT_ARG, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, };
13
13
  /** The nine independent component states (identical for asset and settings). */
14
14
  export type ClaudeHookComponentState = "absent" | "managed-current" | "released-outdated" | "exact-legacy" | "edited-managed" | "foreign" | "symlink" | "non-regular" | "malformed";
15
15
  /** A released settings-entry identity: version plus placeholder-normalized hash. */
@@ -149,7 +149,7 @@ export declare function planManagedClaudeHookMerge(parsed: unknown, currentAsset
149
149
  export interface ManagedHandler {
150
150
  type: "command";
151
151
  command: "node";
152
- args: [string];
152
+ args: [string, string];
153
153
  timeout: number;
154
154
  statusMessage: string;
155
155
  }
@@ -9,8 +9,8 @@
9
9
  * structure; a marker only claims ownership, it never proves it.
10
10
  */
11
11
  import { createHash } from "node:crypto";
12
- import { ASSET_SHA_PLACEHOLDER, LEGACY_COHORT, LEGACY_FILE_SHA256, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, } from "./__fixtures__/claude-hook-ownership.js";
13
- export { ASSET_SHA_PLACEHOLDER, LEGACY_FILE_SHA256, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, };
12
+ import { ASSET_SHA_PLACEHOLDER, LEGACY_COHORT, LEGACY_FILE_SHA256, MANAGED_AGENT_ARG, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, } from "./__fixtures__/claude-hook-ownership.js";
13
+ export { ASSET_SHA_PLACEHOLDER, LEGACY_FILE_SHA256, MANAGED_AGENT_ARG, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, };
14
14
  // Shape helpers
15
15
  export function isPlainObject(value) {
16
16
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -443,7 +443,7 @@ export function buildManagedContainer(currentAssetSha) {
443
443
  {
444
444
  type: "command",
445
445
  command: "node",
446
- args: [MANAGED_ASSET_ARG],
446
+ args: [MANAGED_ASSET_ARG, MANAGED_AGENT_ARG],
447
447
  timeout: MANAGED_TIMEOUT,
448
448
  statusMessage: `${MANAGED_STATUS_PREFIX}${currentAssetSha}`,
449
449
  },