@indigoai-us/hq-cli 5.98.3 → 5.99.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/assets/scaffold/core/scripts/checkpoint-stop-gate.sh +347 -0
  3. package/assets/scaffold/core/scripts/hook-lib.sh +557 -0
  4. package/assets/scaffold/core/scripts/hq-session.sh +251 -0
  5. package/assets/scaffold/core/scripts/lib/session-id.sh +96 -0
  6. package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +52 -0
  7. package/dist/commands/core-checkpoint.js +11 -3
  8. package/dist/commands/core.js +60 -5
  9. package/dist/commands/doctor.d.ts +97 -0
  10. package/dist/commands/doctor.js +228 -0
  11. package/dist/commands/scaffold-fast.d.ts +41 -0
  12. package/dist/commands/scaffold-fast.js +57 -0
  13. package/dist/fast-core.d.ts +16 -0
  14. package/dist/fast-core.js +47 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +10 -1
  17. package/dist/lib/core-utils/soft-timeout.d.ts +55 -0
  18. package/dist/lib/core-utils/soft-timeout.js +205 -0
  19. package/dist/lib/core-utils/timeout-guard.d.ts +62 -0
  20. package/dist/lib/core-utils/timeout-guard.js +207 -0
  21. package/dist/lib/doctor/__testing__/fake-hq-tree.d.ts +194 -0
  22. package/dist/lib/doctor/__testing__/fake-hq-tree.js +357 -0
  23. package/dist/lib/doctor/allowed-divergence.d.ts +72 -0
  24. package/dist/lib/doctor/allowed-divergence.js +134 -0
  25. package/dist/lib/doctor/checks/claude-wiring.d.ts +55 -0
  26. package/dist/lib/doctor/checks/claude-wiring.js +524 -0
  27. package/dist/lib/doctor/checks/codex-wiring.d.ts +45 -0
  28. package/dist/lib/doctor/checks/codex-wiring.js +376 -0
  29. package/dist/lib/doctor/checks/grok-wiring.d.ts +35 -0
  30. package/dist/lib/doctor/checks/grok-wiring.js +186 -0
  31. package/dist/lib/doctor/checks/runtime-probe.d.ts +101 -0
  32. package/dist/lib/doctor/checks/runtime-probe.js +335 -0
  33. package/dist/lib/doctor/compat.d.ts +85 -0
  34. package/dist/lib/doctor/compat.js +102 -0
  35. package/dist/lib/doctor/deep/classify.d.ts +61 -0
  36. package/dist/lib/doctor/deep/classify.js +75 -0
  37. package/dist/lib/doctor/deep/effects.d.ts +107 -0
  38. package/dist/lib/doctor/deep/effects.js +229 -0
  39. package/dist/lib/doctor/deep/executor.d.ts +112 -0
  40. package/dist/lib/doctor/deep/executor.js +369 -0
  41. package/dist/lib/doctor/deep/parity.d.ts +129 -0
  42. package/dist/lib/doctor/deep/parity.js +355 -0
  43. package/dist/lib/doctor/deep/sandbox.d.ts +190 -0
  44. package/dist/lib/doctor/deep/sandbox.js +572 -0
  45. package/dist/lib/doctor/fix/apply.d.ts +119 -0
  46. package/dist/lib/doctor/fix/apply.js +352 -0
  47. package/dist/lib/doctor/fix/backup.d.ts +40 -0
  48. package/dist/lib/doctor/fix/backup.js +64 -0
  49. package/dist/lib/doctor/fix/remediation.d.ts +71 -0
  50. package/dist/lib/doctor/fix/remediation.js +103 -0
  51. package/dist/lib/doctor/fixtures/discover.d.ts +96 -0
  52. package/dist/lib/doctor/fixtures/discover.js +287 -0
  53. package/dist/lib/doctor/fixtures/schema.d.ts +171 -0
  54. package/dist/lib/doctor/fixtures/schema.js +248 -0
  55. package/dist/lib/doctor/hook-gate-profiles.d.ts +55 -0
  56. package/dist/lib/doctor/hook-gate-profiles.js +107 -0
  57. package/dist/lib/doctor/json-output.d.ts +90 -0
  58. package/dist/lib/doctor/json-output.js +76 -0
  59. package/dist/lib/doctor/payload-shapes.d.ts +170 -0
  60. package/dist/lib/doctor/payload-shapes.js +275 -0
  61. package/dist/lib/doctor/platform.d.ts +244 -0
  62. package/dist/lib/doctor/platform.js +490 -0
  63. package/dist/lib/doctor/registry.d.ts +49 -0
  64. package/dist/lib/doctor/registry.js +176 -0
  65. package/dist/lib/doctor/report.d.ts +87 -0
  66. package/dist/lib/doctor/report.js +164 -0
  67. package/dist/lib/doctor/types.d.ts +87 -0
  68. package/dist/lib/doctor/types.js +29 -0
  69. package/dist/main.js +6 -0
  70. package/dist/utils/version-check.js +2 -2
  71. package/dist/utils/version-gate.d.ts +1 -1
  72. package/dist/utils/version-gate.js +1 -1
  73. package/package.json +1 -1
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Runtime probe (US-006): did HQ's hooks actually FIRE in this session?
3
+ *
4
+ * Every other hooks check answers "is the configuration correct?". This one
5
+ * answers the question that correct configuration cannot: "did the host actually
6
+ * dispatch the hooks it loaded?". `personal-context.md` and
7
+ * `core/docs/hq/HOOKS-NOT-FIRING.md` document that the Claude Code **app** and
8
+ * **SDK** runtimes load `.claude/settings.json` correctly and then never
9
+ * dispatch a single command hook — so a tree that passes every wiring check can
10
+ * still have zero live enforcement. Only evidence that a hook *ran* can catch
11
+ * that, and the one durable trace a real dispatch leaves is the policy-trigger
12
+ * ledger under `workspace/orchestrator/policy-trigger-state/`.
13
+ *
14
+ * ## Two layers, on purpose
15
+ *
16
+ * 1. {@link reproduceCheckHqHooks} is a faithful TypeScript port of
17
+ * `core/scripts/check-hq-hooks.sh --require-ledger`: same settings/wiring
18
+ * checks, same ledger rule, same overall pass/fail. It is the piece US-012
19
+ * reduces that shell script to a wrapper over, and the piece the agreement
20
+ * test pins against the live script. Its verdict is a plain boolean, exactly
21
+ * like the script's exit code.
22
+ *
23
+ * 2. {@link checkRuntimeProbe} turns that raw verdict into an honest,
24
+ * platform-aware doctor result. The raw script FAILs on any missing ledger;
25
+ * the doctor is more discerning, because a missing ledger means different
26
+ * things on different hosts:
27
+ * - app / SDK → FAIL: those hosts never dispatch, so a missing ledger is
28
+ * proof of no enforcement (AC3).
29
+ * - unknown → UNKNOWN: the host could not be identified, so live
30
+ * enforcement cannot be claimed either way (AC4) — never PASS, never a
31
+ * confident FAIL.
32
+ * - CLI / Codex / Grok → UNTESTED: those hosts DO dispatch, so an empty
33
+ * ledger just means nothing has triggered a policy hook yet this
34
+ * session. "Wired but not yet exercised" is UNTESTED, not a failure.
35
+ *
36
+ * The probe is strictly read-only: it stats and reads files and NEVER writes to
37
+ * the ledger or mutates session state (AC6). Reading it can never manufacture the
38
+ * very evidence it is looking for.
39
+ */
40
+ import type { CheckContext, CheckResult } from "../types.js";
41
+ /** Common id prefix for every result the runtime probe emits. */
42
+ export declare const RUNTIME_PROBE_PREFIX = "hooks.runtime";
43
+ /**
44
+ * The ledger directory, relative to the HQ root. A hook that fires through the
45
+ * policy-trigger path appends a `<session-id>.txt` file here; its presence is the
46
+ * one durable proof that dispatch happened. Kept in exact parity with
47
+ * `check-hq-hooks.sh` (`LEDGER_DIR="$HQ_ROOT/workspace/orchestrator/policy-trigger-state"`).
48
+ */
49
+ export declare const POLICY_TRIGGER_LEDGER_RELPATH = "workspace/orchestrator/policy-trigger-state";
50
+ /** The two lifecycle events `check-hq-hooks.sh` requires a command hook on. */
51
+ export declare const REQUIRED_HOOK_EVENTS: readonly ["SessionStart", "PreToolUse"];
52
+ /** Whether the ledger was found. Mirrors the script's `present`/`missing`. */
53
+ export type LedgerState = "present" | "missing";
54
+ /** Options for {@link reproduceCheckHqHooks}. */
55
+ export interface ReproduceOptions {
56
+ /** Absolute HQ tree root to inspect. */
57
+ hqRoot: string;
58
+ /**
59
+ * Scope the ledger check to this exact session (the `--session-id` flag). When
60
+ * set, only `<sessionId>.txt` counts; an older session's ledger cannot stand in
61
+ * for the current runtime. When absent, any ledger file suffices, exactly as
62
+ * the script behaves without `--session-id`.
63
+ */
64
+ sessionId?: string;
65
+ }
66
+ /**
67
+ * The reproduced verdict of `check-hq-hooks.sh --require-ledger`. `ok` is the
68
+ * boolean twin of the script's exit code: true ⇔ exit 0, false ⇔ exit non-zero.
69
+ */
70
+ export interface HookLoadReproduction {
71
+ /** True iff the script would exit 0 (no issues, ledger present). */
72
+ ok: boolean;
73
+ /** Whether the (optionally session-scoped) ledger was found. */
74
+ ledgerState: LedgerState;
75
+ /** The human-readable issues, mirroring the script's `ISSUES` array. */
76
+ issues: string[];
77
+ }
78
+ /**
79
+ * Faithfully reproduce `core/scripts/check-hq-hooks.sh --require-ledger` in
80
+ * TypeScript. The checks, in the script's order:
81
+ *
82
+ * 1. `.claude/settings.json` present and valid JSON.
83
+ * 2. A `type: "command"` hook on both SessionStart and PreToolUse.
84
+ * 3. No hook command word-splits `$CLAUDE_PROJECT_DIR` (the quote-aware scan
85
+ * shared with the Claude wiring tier), and every script a command actually
86
+ * runs exists on disk.
87
+ * 4. The optional `.claude/settings.local.json` overlay, when present, is valid
88
+ * JSON and passes the same command scan.
89
+ * 5. The policy-trigger ledger exists (session-scoped when `sessionId` is set).
90
+ *
91
+ * Never throws: an unreadable or malformed input becomes an issue, not an
92
+ * exception, so the probe degrades exactly like the defensively-written script.
93
+ */
94
+ export declare function reproduceCheckHqHooks(opts: ReproduceOptions): HookLoadReproduction;
95
+ /**
96
+ * The runtime-probe check family entry. Reproduces the script verdict, then
97
+ * renders it as a single platform-aware doctor result. See the module header for
98
+ * the per-platform status rationale.
99
+ */
100
+ export declare function checkRuntimeProbe(context: CheckContext): CheckResult[];
101
+ //# sourceMappingURL=runtime-probe.d.ts.map
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Runtime probe (US-006): did HQ's hooks actually FIRE in this session?
3
+ *
4
+ * Every other hooks check answers "is the configuration correct?". This one
5
+ * answers the question that correct configuration cannot: "did the host actually
6
+ * dispatch the hooks it loaded?". `personal-context.md` and
7
+ * `core/docs/hq/HOOKS-NOT-FIRING.md` document that the Claude Code **app** and
8
+ * **SDK** runtimes load `.claude/settings.json` correctly and then never
9
+ * dispatch a single command hook — so a tree that passes every wiring check can
10
+ * still have zero live enforcement. Only evidence that a hook *ran* can catch
11
+ * that, and the one durable trace a real dispatch leaves is the policy-trigger
12
+ * ledger under `workspace/orchestrator/policy-trigger-state/`.
13
+ *
14
+ * ## Two layers, on purpose
15
+ *
16
+ * 1. {@link reproduceCheckHqHooks} is a faithful TypeScript port of
17
+ * `core/scripts/check-hq-hooks.sh --require-ledger`: same settings/wiring
18
+ * checks, same ledger rule, same overall pass/fail. It is the piece US-012
19
+ * reduces that shell script to a wrapper over, and the piece the agreement
20
+ * test pins against the live script. Its verdict is a plain boolean, exactly
21
+ * like the script's exit code.
22
+ *
23
+ * 2. {@link checkRuntimeProbe} turns that raw verdict into an honest,
24
+ * platform-aware doctor result. The raw script FAILs on any missing ledger;
25
+ * the doctor is more discerning, because a missing ledger means different
26
+ * things on different hosts:
27
+ * - app / SDK → FAIL: those hosts never dispatch, so a missing ledger is
28
+ * proof of no enforcement (AC3).
29
+ * - unknown → UNKNOWN: the host could not be identified, so live
30
+ * enforcement cannot be claimed either way (AC4) — never PASS, never a
31
+ * confident FAIL.
32
+ * - CLI / Codex / Grok → UNTESTED: those hosts DO dispatch, so an empty
33
+ * ledger just means nothing has triggered a policy hook yet this
34
+ * session. "Wired but not yet exercised" is UNTESTED, not a failure.
35
+ *
36
+ * The probe is strictly read-only: it stats and reads files and NEVER writes to
37
+ * the ledger or mutates session state (AC6). Reading it can never manufacture the
38
+ * very evidence it is looking for.
39
+ */
40
+ import * as fs from "node:fs";
41
+ import * as path from "node:path";
42
+ import { scanHookCommand } from "./claude-wiring.js";
43
+ /** Common id prefix for every result the runtime probe emits. */
44
+ export const RUNTIME_PROBE_PREFIX = "hooks.runtime";
45
+ /**
46
+ * The ledger directory, relative to the HQ root. A hook that fires through the
47
+ * policy-trigger path appends a `<session-id>.txt` file here; its presence is the
48
+ * one durable proof that dispatch happened. Kept in exact parity with
49
+ * `check-hq-hooks.sh` (`LEDGER_DIR="$HQ_ROOT/workspace/orchestrator/policy-trigger-state"`).
50
+ */
51
+ export const POLICY_TRIGGER_LEDGER_RELPATH = "workspace/orchestrator/policy-trigger-state";
52
+ /** The two lifecycle events `check-hq-hooks.sh` requires a command hook on. */
53
+ export const REQUIRED_HOOK_EVENTS = ["SessionStart", "PreToolUse"];
54
+ /**
55
+ * Faithfully reproduce `core/scripts/check-hq-hooks.sh --require-ledger` in
56
+ * TypeScript. The checks, in the script's order:
57
+ *
58
+ * 1. `.claude/settings.json` present and valid JSON.
59
+ * 2. A `type: "command"` hook on both SessionStart and PreToolUse.
60
+ * 3. No hook command word-splits `$CLAUDE_PROJECT_DIR` (the quote-aware scan
61
+ * shared with the Claude wiring tier), and every script a command actually
62
+ * runs exists on disk.
63
+ * 4. The optional `.claude/settings.local.json` overlay, when present, is valid
64
+ * JSON and passes the same command scan.
65
+ * 5. The policy-trigger ledger exists (session-scoped when `sessionId` is set).
66
+ *
67
+ * Never throws: an unreadable or malformed input becomes an issue, not an
68
+ * exception, so the probe degrades exactly like the defensively-written script.
69
+ */
70
+ export function reproduceCheckHqHooks(opts) {
71
+ const { hqRoot, sessionId } = opts;
72
+ const issues = [];
73
+ const rootHasSpace = /\s/.test(hqRoot);
74
+ const settingsPath = path.join(hqRoot, ".claude", "settings.json");
75
+ const localPath = path.join(hqRoot, ".claude", "settings.local.json");
76
+ // --- 1 + 2 + 3: the shipped settings file -----------------------------------
77
+ const base = readJson(settingsPath);
78
+ if (!base.present) {
79
+ issues.push(".claude/settings.json is missing");
80
+ }
81
+ else if (base.invalid) {
82
+ issues.push(".claude/settings.json is not valid JSON");
83
+ }
84
+ else {
85
+ for (const event of REQUIRED_HOOK_EVENTS) {
86
+ if (!hasCommandHook(base.value, event)) {
87
+ issues.push(`${event} has no command hook in .claude/settings.json`);
88
+ }
89
+ }
90
+ scanSettingsFile(base.value, ".claude/settings.json", hqRoot, rootHasSpace, issues);
91
+ }
92
+ // --- 4: the optional local overlay ------------------------------------------
93
+ // Its absence is never an issue, but when present Claude Code merges its hooks
94
+ // in, so a command hiding there fails a spaced root exactly like a shipped one.
95
+ const local = readJson(localPath);
96
+ if (local.present) {
97
+ if (local.invalid) {
98
+ issues.push(".claude/settings.local.json is not valid JSON");
99
+ }
100
+ else {
101
+ scanSettingsFile(local.value, ".claude/settings.local.json", hqRoot, rootHasSpace, issues);
102
+ }
103
+ }
104
+ // --- 5: the policy-trigger ledger -------------------------------------------
105
+ const ledgerState = resolveLedgerState(hqRoot, sessionId);
106
+ if (ledgerState === "missing") {
107
+ issues.push(sessionId
108
+ ? `policy-trigger ledger was not found for session ${sessionId} under ${POLICY_TRIGGER_LEDGER_RELPATH}`
109
+ : `policy-trigger ledger was not found under ${POLICY_TRIGGER_LEDGER_RELPATH}`);
110
+ }
111
+ return { ok: issues.length === 0, ledgerState, issues };
112
+ }
113
+ /**
114
+ * The runtime-probe check family entry. Reproduces the script verdict, then
115
+ * renders it as a single platform-aware doctor result. See the module header for
116
+ * the per-platform status rationale.
117
+ */
118
+ export function checkRuntimeProbe(context) {
119
+ const hqRoot = context.hqRoot;
120
+ const sessionId = context.sessionId;
121
+ const platformId = context.platform?.id ?? "unknown";
122
+ const checkId = `${RUNTIME_PROBE_PREFIX}.enforcement`;
123
+ const target = POLICY_TRIGGER_LEDGER_RELPATH;
124
+ const scope = sessionId ? ` for session ${sessionId}` : "";
125
+ const repro = reproduceCheckHqHooks({ hqRoot, sessionId });
126
+ // Unknown host: the label cannot be trusted, so live enforcement cannot be
127
+ // verified in either direction. UNKNOWN (never PASS or FAIL) is the honest
128
+ // verdict and, per the exit-code contract, still draws the user's attention.
129
+ if (platformId === "unknown") {
130
+ return [
131
+ {
132
+ status: "UNKNOWN",
133
+ checkId,
134
+ target,
135
+ message: `Host platform is unknown, so whether hooks actually dispatched this ` +
136
+ `session cannot be verified — the policy-trigger ledger is ${repro.ledgerState}${scope}. ` +
137
+ `Not reporting hook enforcement as working.`,
138
+ remediation: `Run hq doctor from the terminal Claude Code CLI (pass --session-id on ` +
139
+ `an app/SDK host) for a definitive runtime verdict.`,
140
+ },
141
+ ];
142
+ }
143
+ // Ledger present ⇒ a hook demonstrably fired this session ⇒ enforcement is live.
144
+ if (repro.ledgerState === "present") {
145
+ return [
146
+ {
147
+ status: "PASS",
148
+ checkId,
149
+ target,
150
+ message: `Hook dispatch was observed this session: the policy-trigger ledger has an entry${scope}.`,
151
+ },
152
+ ];
153
+ }
154
+ // Ledger missing on a non-dispatching host is proof, not ambiguity: the app and
155
+ // SDK runtimes load hook config and never fire it. This is the one failure the
156
+ // whole probe exists to surface.
157
+ if (platformId === "claude-code-app" || platformId === "claude-code-sdk") {
158
+ const host = platformId === "claude-code-app" ? "Claude Code app" : "Claude Code SDK";
159
+ return [
160
+ {
161
+ status: "FAIL",
162
+ checkId,
163
+ target,
164
+ message: `Hook enforcement is NOT observed: no policy-trigger ledger entry exists${scope} and this host ` +
165
+ `is the ${host} runtime, which does not dispatch command hooks. Correct-looking hook ` +
166
+ `configuration is not working enforcement here.`,
167
+ remediation: `Use the terminal Claude Code CLI for hook-enforced work (see ` +
168
+ `core/docs/hq/HOOKS-NOT-FIRING.md). Verify with: ` +
169
+ `bash core/scripts/check-hq-hooks.sh --root "$PWD" --require-ledger.`,
170
+ },
171
+ ];
172
+ }
173
+ // Dispatching host (CLI / Codex / Grok) with an empty ledger: hooks are wired
174
+ // and this host does fire them, but nothing has triggered a policy hook yet
175
+ // this session. Wired-but-not-yet-exercised is UNTESTED, never a failure.
176
+ return [
177
+ {
178
+ status: "UNTESTED",
179
+ checkId,
180
+ target,
181
+ message: `Hooks are wired but dispatch has not been observed yet: the policy-trigger ledger has no ` +
182
+ `entry${scope}. This host (${platformId}) does dispatch hooks, so trigger a policy-gated action ` +
183
+ `and re-check to confirm live enforcement.`,
184
+ remediation: `After a real action, verify with: bash core/scripts/check-hq-hooks.sh --root "$PWD" ` +
185
+ `--require-ledger${sessionId ? ` --session-id ${sessionId}` : ""}.`,
186
+ },
187
+ ];
188
+ }
189
+ // --- ledger ------------------------------------------------------------------
190
+ /**
191
+ * Resolve whether the ledger counts as present. Session-scoped: only
192
+ * `<sessionId>.txt` counts. Unscoped: any `*.txt` anywhere under the ledger
193
+ * directory counts, matching the script's recursive `find -type f -name '*.txt'`.
194
+ */
195
+ function resolveLedgerState(hqRoot, sessionId) {
196
+ const ledgerDir = path.join(hqRoot, ...POLICY_TRIGGER_LEDGER_RELPATH.split("/"));
197
+ if (sessionId) {
198
+ return isFile(path.join(ledgerDir, `${sessionId}.txt`))
199
+ ? "present"
200
+ : "missing";
201
+ }
202
+ return ledgerDirHasTxt(ledgerDir) ? "present" : "missing";
203
+ }
204
+ /** True when any `*.txt` file exists anywhere under `dir` (recursive). */
205
+ function ledgerDirHasTxt(dir) {
206
+ let entries;
207
+ try {
208
+ entries = fs.readdirSync(dir, { withFileTypes: true });
209
+ }
210
+ catch {
211
+ return false;
212
+ }
213
+ for (const entry of entries) {
214
+ const full = path.join(dir, entry.name);
215
+ if (entry.isDirectory()) {
216
+ if (ledgerDirHasTxt(full))
217
+ return true;
218
+ }
219
+ else if (entry.isFile() && entry.name.endsWith(".txt")) {
220
+ return true;
221
+ }
222
+ }
223
+ return false;
224
+ }
225
+ // --- settings inspection -----------------------------------------------------
226
+ /**
227
+ * Scan every command hook in one parsed settings object: count the commands that
228
+ * word-split `$CLAUDE_PROJECT_DIR`, and check that every script a command
229
+ * actually runs exists on disk. Appends to `issues`, mirroring the script's
230
+ * `scan_hook_commands`.
231
+ */
232
+ function scanSettingsFile(settings, label, hqRoot, rootHasSpace, issues) {
233
+ const commands = enumerateCommands(settings);
234
+ const scans = commands.map((command) => scanHookCommand(command));
235
+ const unquoted = scans.filter((scan) => scan.unquotedProjectDir).length;
236
+ if (unquoted > 0) {
237
+ issues.push(rootHasSpace
238
+ ? `${unquoted} hook command(s) in ${label} reference $CLAUDE_PROJECT_DIR without quotes and this HQ root contains a space, so /bin/sh splits the path and every one of those hooks is failing right now: ${hqRoot}`
239
+ : `${unquoted} hook command(s) in ${label} reference $CLAUDE_PROJECT_DIR without quotes; they break on any install path containing a space`);
240
+ }
241
+ const required = new Set();
242
+ for (const scan of scans) {
243
+ for (const rp of scan.requiredRelpaths)
244
+ required.add(rp);
245
+ }
246
+ for (const rp of [...required].sort()) {
247
+ if (!fs.existsSync(path.join(hqRoot, rp))) {
248
+ issues.push(`a hook command in ${label} runs a script that does not exist: ${rp}`);
249
+ }
250
+ }
251
+ }
252
+ /**
253
+ * Whether `settings.hooks[event]` carries at least one non-empty `type:
254
+ * "command"` hook. Tolerant of every malformed shape — a non-conforming branch
255
+ * simply does not count.
256
+ */
257
+ function hasCommandHook(settings, event) {
258
+ const entries = hookEntriesFor(settings, event);
259
+ for (const entry of entries) {
260
+ const inner = entry?.hooks;
261
+ if (!Array.isArray(inner))
262
+ continue;
263
+ for (const item of inner) {
264
+ const hook = item;
265
+ if (hook?.type === "command" &&
266
+ typeof hook.command === "string" &&
267
+ hook.command.length > 0) {
268
+ return true;
269
+ }
270
+ }
271
+ }
272
+ return false;
273
+ }
274
+ /** Every non-empty command string across every lifecycle event in `settings`. */
275
+ function enumerateCommands(settings) {
276
+ const out = [];
277
+ const hooks = hooksObject(settings);
278
+ if (!hooks)
279
+ return out;
280
+ for (const entries of Object.values(hooks)) {
281
+ if (!Array.isArray(entries))
282
+ continue;
283
+ for (const entry of entries) {
284
+ const inner = entry?.hooks;
285
+ if (!Array.isArray(inner))
286
+ continue;
287
+ for (const item of inner) {
288
+ const hook = item;
289
+ if (hook?.type === "command" &&
290
+ typeof hook.command === "string" &&
291
+ hook.command.length > 0) {
292
+ out.push(hook.command);
293
+ }
294
+ }
295
+ }
296
+ }
297
+ return out;
298
+ }
299
+ function hookEntriesFor(settings, event) {
300
+ const hooks = hooksObject(settings);
301
+ const entries = hooks?.[event];
302
+ return Array.isArray(entries) ? entries : [];
303
+ }
304
+ function hooksObject(settings) {
305
+ if (!settings || typeof settings !== "object")
306
+ return null;
307
+ const hooks = settings.hooks;
308
+ if (!hooks || typeof hooks !== "object")
309
+ return null;
310
+ return hooks;
311
+ }
312
+ function readJson(file) {
313
+ let raw;
314
+ try {
315
+ raw = fs.readFileSync(file, "utf8");
316
+ }
317
+ catch {
318
+ return { present: false };
319
+ }
320
+ try {
321
+ return { present: true, value: JSON.parse(raw) };
322
+ }
323
+ catch {
324
+ return { present: true, invalid: true };
325
+ }
326
+ }
327
+ function isFile(file) {
328
+ try {
329
+ return fs.statSync(file).isFile();
330
+ }
331
+ catch {
332
+ return false;
333
+ }
334
+ }
335
+ //# sourceMappingURL=runtime-probe.js.map
@@ -0,0 +1,85 @@
1
+ /**
2
+ * check-hq-hooks.sh ⇄ `hq doctor` compatibility (US-012).
3
+ *
4
+ * US-012 reduces `core/scripts/check-hq-hooks.sh` to a thin wrapper over
5
+ * `hq doctor`, so HQ has ONE answer to "are my hooks working" instead of four
6
+ * partially-overlapping tools. The wrapper must, however, keep its narrow
7
+ * contract intact (AC1): the same PASS/FAIL header, the same
8
+ * `HQ runtime enforcement: OBSERVED / NOT OBSERVED` line the app/SDK safety
9
+ * instruction in `personal-context.md` greps for, and the same 0/2 exit codes —
10
+ * and it must degrade to its inline implementation when the `hq` CLI is absent
11
+ * or too old (AC2), because it exists precisely to run when the toolchain is
12
+ * suspect.
13
+ *
14
+ * The wrapper therefore does NOT adopt the full `hq doctor` verdict: the doctor
15
+ * is a strict superset (Codex/Grok parity, three-profile membership, exec bits,
16
+ * fixture coverage) and reports the known Codex drift as FAIL on a healthy tree,
17
+ * which would break every existing check-hq-hooks caller. Instead it maps a
18
+ * *scoped* slice of the doctor's JSON — exactly the checks check-hq-hooks
19
+ * originally performed (settings load + the policy-trigger ledger) — back onto
20
+ * its own contract.
21
+ *
22
+ * This module is that scope and mapping, as one tested TypeScript reference. The
23
+ * shell wrapper mirrors it in bash (`jq` over `hq doctor --json`); the agreement
24
+ * test (`compat.test.ts`) pins the real wrapper's exit code and output against
25
+ * both this reference and the live `hq doctor` on healthy and broken fake trees.
26
+ * It is the same "two implementations, one contract, pinned by a test" pattern
27
+ * that `reproduceCheckHqHooks` (US-006) uses in the other direction.
28
+ */
29
+ import type { DoctorJsonDocument } from "./json-output.js";
30
+ /** Exit code check-hq-hooks.sh returns on success. Preserved by the wrapper. */
31
+ export declare const CHECK_HQ_HOOKS_PASS_EXIT = 0;
32
+ /** Exit code check-hq-hooks.sh returns on a health failure. Preserved. */
33
+ export declare const CHECK_HQ_HOOKS_FAIL_EXIT = 2;
34
+ /** Exit code check-hq-hooks.sh returns on a usage error. Preserved. */
35
+ export declare const CHECK_HQ_HOOKS_USAGE_EXIT = 64;
36
+ /** The runtime-probe result id: whether hooks actually fired (the ledger). */
37
+ export declare const RUNTIME_ENFORCEMENT_CHECK_ID = "hooks.runtime.enforcement";
38
+ /**
39
+ * The `hq doctor` check ids that correspond to check-hq-hooks.sh's *settings
40
+ * load* scope — the subset of the hooks family the wrapper is allowed to fail
41
+ * on. Deliberately narrow: it covers a missing/invalid settings file, an
42
+ * unquoted `$CLAUDE_PROJECT_DIR` that word-splits on a spaced root, and a hook
43
+ * command that runs a script which is not on disk — and nothing else. The many
44
+ * richer doctor findings (executable bits, orphaned scripts, three-profile
45
+ * membership, Codex/Grok parity, fixture coverage) are intentionally OUT of
46
+ * scope, so the wrapper stays byte-for-byte compatible with callers that only
47
+ * ever cared whether the hook configuration could load.
48
+ */
49
+ export declare const CHECK_HQ_HOOKS_SETTINGS_SCOPE: readonly string[];
50
+ /**
51
+ * Whether hook dispatch was observed this session. Mirrors the wrapper's
52
+ * `HQ runtime enforcement: …` line. `NOT CHECKED` is the default when
53
+ * `--require-ledger` was not passed (the wrapper prints "ledger: not checked").
54
+ */
55
+ export type RuntimeObservation = "OBSERVED" | "NOT OBSERVED" | "NOT CHECKED";
56
+ /** The verdict the wrapper renders, derived from a scoped `hq doctor` run. */
57
+ export interface CheckHqHooksVerdict {
58
+ /** True iff the wrapper exits 0 (no in-scope failure, ledger observed when required). */
59
+ ok: boolean;
60
+ /** The exit code the wrapper returns: 0 on pass, 2 on failure. */
61
+ exitCode: number;
62
+ /** The `HQ hook health: …` header value. */
63
+ hookHealth: "PASS" | "FAIL";
64
+ /** The `HQ runtime enforcement: …` observation. */
65
+ runtime: RuntimeObservation;
66
+ /** The failing messages, in the wrapper's ` - <message>` bullet form. */
67
+ issues: string[];
68
+ }
69
+ /** Options for {@link deriveCheckHqHooksVerdict}. */
70
+ export interface DeriveVerdictOptions {
71
+ /**
72
+ * Whether `--require-ledger` (or `--session-id`, which implies it) was passed.
73
+ * When true, an unobserved policy-trigger ledger is a failure, exactly as the
74
+ * inline script treats a missing ledger — regardless of platform.
75
+ */
76
+ requireLedger?: boolean;
77
+ }
78
+ /**
79
+ * Derive the check-hq-hooks.sh verdict from a `hq doctor --json` document,
80
+ * scoped to check-hq-hooks' original concerns. This is the exact logic the
81
+ * shell wrapper reproduces with `jq`; keeping it here as one tested function is
82
+ * what lets `compat.test.ts` prove the wrapper and `hq doctor` agree.
83
+ */
84
+ export declare function deriveCheckHqHooksVerdict(doc: DoctorJsonDocument, options?: DeriveVerdictOptions): CheckHqHooksVerdict;
85
+ //# sourceMappingURL=compat.d.ts.map
@@ -0,0 +1,102 @@
1
+ /**
2
+ * check-hq-hooks.sh ⇄ `hq doctor` compatibility (US-012).
3
+ *
4
+ * US-012 reduces `core/scripts/check-hq-hooks.sh` to a thin wrapper over
5
+ * `hq doctor`, so HQ has ONE answer to "are my hooks working" instead of four
6
+ * partially-overlapping tools. The wrapper must, however, keep its narrow
7
+ * contract intact (AC1): the same PASS/FAIL header, the same
8
+ * `HQ runtime enforcement: OBSERVED / NOT OBSERVED` line the app/SDK safety
9
+ * instruction in `personal-context.md` greps for, and the same 0/2 exit codes —
10
+ * and it must degrade to its inline implementation when the `hq` CLI is absent
11
+ * or too old (AC2), because it exists precisely to run when the toolchain is
12
+ * suspect.
13
+ *
14
+ * The wrapper therefore does NOT adopt the full `hq doctor` verdict: the doctor
15
+ * is a strict superset (Codex/Grok parity, three-profile membership, exec bits,
16
+ * fixture coverage) and reports the known Codex drift as FAIL on a healthy tree,
17
+ * which would break every existing check-hq-hooks caller. Instead it maps a
18
+ * *scoped* slice of the doctor's JSON — exactly the checks check-hq-hooks
19
+ * originally performed (settings load + the policy-trigger ledger) — back onto
20
+ * its own contract.
21
+ *
22
+ * This module is that scope and mapping, as one tested TypeScript reference. The
23
+ * shell wrapper mirrors it in bash (`jq` over `hq doctor --json`); the agreement
24
+ * test (`compat.test.ts`) pins the real wrapper's exit code and output against
25
+ * both this reference and the live `hq doctor` on healthy and broken fake trees.
26
+ * It is the same "two implementations, one contract, pinned by a test" pattern
27
+ * that `reproduceCheckHqHooks` (US-006) uses in the other direction.
28
+ */
29
+ /** Exit code check-hq-hooks.sh returns on success. Preserved by the wrapper. */
30
+ export const CHECK_HQ_HOOKS_PASS_EXIT = 0;
31
+ /** Exit code check-hq-hooks.sh returns on a health failure. Preserved. */
32
+ export const CHECK_HQ_HOOKS_FAIL_EXIT = 2;
33
+ /** Exit code check-hq-hooks.sh returns on a usage error. Preserved. */
34
+ export const CHECK_HQ_HOOKS_USAGE_EXIT = 64;
35
+ /** The runtime-probe result id: whether hooks actually fired (the ledger). */
36
+ export const RUNTIME_ENFORCEMENT_CHECK_ID = "hooks.runtime.enforcement";
37
+ /**
38
+ * The `hq doctor` check ids that correspond to check-hq-hooks.sh's *settings
39
+ * load* scope — the subset of the hooks family the wrapper is allowed to fail
40
+ * on. Deliberately narrow: it covers a missing/invalid settings file, an
41
+ * unquoted `$CLAUDE_PROJECT_DIR` that word-splits on a spaced root, and a hook
42
+ * command that runs a script which is not on disk — and nothing else. The many
43
+ * richer doctor findings (executable bits, orphaned scripts, three-profile
44
+ * membership, Codex/Grok parity, fixture coverage) are intentionally OUT of
45
+ * scope, so the wrapper stays byte-for-byte compatible with callers that only
46
+ * ever cared whether the hook configuration could load.
47
+ */
48
+ export const CHECK_HQ_HOOKS_SETTINGS_SCOPE = [
49
+ "hooks.settings-present",
50
+ "hooks.settings-valid-json",
51
+ "hooks.claude.settings-local-valid-json",
52
+ "hooks.claude.unquoted-project-dir",
53
+ "hooks.claude.script-missing",
54
+ ];
55
+ /** The doctor statuses that count as a failure for check-hq-hooks' purposes. */
56
+ const FAILING_STATUSES = new Set([
57
+ "FAIL",
58
+ "UNKNOWN",
59
+ ]);
60
+ /**
61
+ * Derive the check-hq-hooks.sh verdict from a `hq doctor --json` document,
62
+ * scoped to check-hq-hooks' original concerns. This is the exact logic the
63
+ * shell wrapper reproduces with `jq`; keeping it here as one tested function is
64
+ * what lets `compat.test.ts` prove the wrapper and `hq doctor` agree.
65
+ */
66
+ export function deriveCheckHqHooksVerdict(doc, options = {}) {
67
+ const requireLedger = options.requireLedger === true;
68
+ const scope = new Set(CHECK_HQ_HOOKS_SETTINGS_SCOPE);
69
+ const messages = [];
70
+ // Settings-load concern: any in-scope result that FAILs or is UNKNOWN.
71
+ for (const result of doc.results) {
72
+ if (scope.has(result.checkId) && FAILING_STATUSES.has(result.status)) {
73
+ messages.push(result.message);
74
+ }
75
+ }
76
+ // Runtime/ledger concern: only checked under --require-ledger. The runtime
77
+ // probe reports PASS only when a hook demonstrably fired this session; any
78
+ // other status (UNTESTED on a dispatching host, FAIL on app/SDK, UNKNOWN on an
79
+ // unidentifiable host) means dispatch was not observed — which is a failure
80
+ // for --require-ledger, matching the inline script's "fail on a missing
81
+ // ledger" contract.
82
+ const runtimeResult = doc.results.find((result) => result.checkId === RUNTIME_ENFORCEMENT_CHECK_ID);
83
+ const runtime = observeRuntime(runtimeResult?.status, requireLedger);
84
+ if (requireLedger && runtime !== "OBSERVED") {
85
+ messages.push(runtimeResult?.message ??
86
+ "policy-trigger ledger was not found under workspace/orchestrator/policy-trigger-state");
87
+ }
88
+ const ok = messages.length === 0;
89
+ return {
90
+ ok,
91
+ exitCode: ok ? CHECK_HQ_HOOKS_PASS_EXIT : CHECK_HQ_HOOKS_FAIL_EXIT,
92
+ hookHealth: ok ? "PASS" : "FAIL",
93
+ runtime,
94
+ issues: messages.map((message) => ` - ${message}`),
95
+ };
96
+ }
97
+ function observeRuntime(status, requireLedger) {
98
+ if (!requireLedger)
99
+ return "NOT CHECKED";
100
+ return status === "PASS" ? "OBSERVED" : "NOT OBSERVED";
101
+ }
102
+ //# sourceMappingURL=compat.js.map