@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.
- package/CHANGELOG.md +43 -0
- package/assets/scaffold/core/scripts/checkpoint-stop-gate.sh +347 -0
- package/assets/scaffold/core/scripts/hook-lib.sh +557 -0
- package/assets/scaffold/core/scripts/hq-session.sh +251 -0
- package/assets/scaffold/core/scripts/lib/session-id.sh +96 -0
- package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +52 -0
- package/dist/commands/core-checkpoint.js +11 -3
- package/dist/commands/core.js +60 -5
- package/dist/commands/doctor.d.ts +97 -0
- package/dist/commands/doctor.js +228 -0
- package/dist/commands/scaffold-fast.d.ts +41 -0
- package/dist/commands/scaffold-fast.js +57 -0
- package/dist/fast-core.d.ts +16 -0
- package/dist/fast-core.js +47 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +10 -1
- package/dist/lib/core-utils/soft-timeout.d.ts +55 -0
- package/dist/lib/core-utils/soft-timeout.js +205 -0
- package/dist/lib/core-utils/timeout-guard.d.ts +62 -0
- package/dist/lib/core-utils/timeout-guard.js +207 -0
- package/dist/lib/doctor/__testing__/fake-hq-tree.d.ts +194 -0
- package/dist/lib/doctor/__testing__/fake-hq-tree.js +357 -0
- package/dist/lib/doctor/allowed-divergence.d.ts +72 -0
- package/dist/lib/doctor/allowed-divergence.js +134 -0
- package/dist/lib/doctor/checks/claude-wiring.d.ts +55 -0
- package/dist/lib/doctor/checks/claude-wiring.js +524 -0
- package/dist/lib/doctor/checks/codex-wiring.d.ts +45 -0
- package/dist/lib/doctor/checks/codex-wiring.js +376 -0
- package/dist/lib/doctor/checks/grok-wiring.d.ts +35 -0
- package/dist/lib/doctor/checks/grok-wiring.js +186 -0
- package/dist/lib/doctor/checks/runtime-probe.d.ts +101 -0
- package/dist/lib/doctor/checks/runtime-probe.js +335 -0
- package/dist/lib/doctor/compat.d.ts +85 -0
- package/dist/lib/doctor/compat.js +102 -0
- package/dist/lib/doctor/deep/classify.d.ts +61 -0
- package/dist/lib/doctor/deep/classify.js +75 -0
- package/dist/lib/doctor/deep/effects.d.ts +107 -0
- package/dist/lib/doctor/deep/effects.js +229 -0
- package/dist/lib/doctor/deep/executor.d.ts +112 -0
- package/dist/lib/doctor/deep/executor.js +369 -0
- package/dist/lib/doctor/deep/parity.d.ts +129 -0
- package/dist/lib/doctor/deep/parity.js +355 -0
- package/dist/lib/doctor/deep/sandbox.d.ts +190 -0
- package/dist/lib/doctor/deep/sandbox.js +572 -0
- package/dist/lib/doctor/fix/apply.d.ts +119 -0
- package/dist/lib/doctor/fix/apply.js +352 -0
- package/dist/lib/doctor/fix/backup.d.ts +40 -0
- package/dist/lib/doctor/fix/backup.js +64 -0
- package/dist/lib/doctor/fix/remediation.d.ts +71 -0
- package/dist/lib/doctor/fix/remediation.js +103 -0
- package/dist/lib/doctor/fixtures/discover.d.ts +96 -0
- package/dist/lib/doctor/fixtures/discover.js +287 -0
- package/dist/lib/doctor/fixtures/schema.d.ts +171 -0
- package/dist/lib/doctor/fixtures/schema.js +248 -0
- package/dist/lib/doctor/hook-gate-profiles.d.ts +55 -0
- package/dist/lib/doctor/hook-gate-profiles.js +107 -0
- package/dist/lib/doctor/json-output.d.ts +90 -0
- package/dist/lib/doctor/json-output.js +76 -0
- package/dist/lib/doctor/payload-shapes.d.ts +170 -0
- package/dist/lib/doctor/payload-shapes.js +275 -0
- package/dist/lib/doctor/platform.d.ts +244 -0
- package/dist/lib/doctor/platform.js +490 -0
- package/dist/lib/doctor/registry.d.ts +49 -0
- package/dist/lib/doctor/registry.js +176 -0
- package/dist/lib/doctor/report.d.ts +87 -0
- package/dist/lib/doctor/report.js +164 -0
- package/dist/lib/doctor/types.d.ts +87 -0
- package/dist/lib/doctor/types.js +29 -0
- package/dist/main.js +6 -0
- package/dist/utils/version-check.js +2 -2
- package/dist/utils/version-gate.d.ts +1 -1
- package/dist/utils/version-gate.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-platform hook payload shapes, tool-name aliasing, and block protocols
|
|
3
|
+
* (US-010).
|
|
4
|
+
*
|
|
5
|
+
* `hq doctor --deep-test` replays one fixture case through all three platform
|
|
6
|
+
* adapters and compares their verdicts (parity.ts). The three platforms do not
|
|
7
|
+
* speak the same wire language, and this module owns every difference so the
|
|
8
|
+
* replay orchestration can stay platform-agnostic:
|
|
9
|
+
*
|
|
10
|
+
* 1. Payload shape. Claude and Codex hooks read a snake_case document
|
|
11
|
+
* (`hook_event_name` / `tool_name` / `tool_input`). Grok speaks camelCase
|
|
12
|
+
* (`hookEventName` / `toolName` / `toolInput`) and its own tool names
|
|
13
|
+
* (`Shell`, `StrReplace`, …). {@link toClaudePayload}, {@link toCodexPayload},
|
|
14
|
+
* and {@link toGrokPayload} normalise one case into each shape.
|
|
15
|
+
*
|
|
16
|
+
* 2. Tool-name aliases. Grok surfaces the same underlying tool under several
|
|
17
|
+
* names — `Shell` / `run_terminal_command` for Bash, `StrReplace` /
|
|
18
|
+
* `search_replace` for Edit, `write` for Write, and so on. The alias tables
|
|
19
|
+
* here mirror the case statement in `.grok/hooks/hq-grok-hook-adapter.sh`
|
|
20
|
+
* exactly, so a payload we emit is one the real adapter recognises, and a
|
|
21
|
+
* Grok name the adapter would receive maps back to the canonical Claude
|
|
22
|
+
* tool the fixtures are written against.
|
|
23
|
+
*
|
|
24
|
+
* 3. Block protocol. Claude and Codex signal a block with a non-zero exit.
|
|
25
|
+
* Grok's PreToolUse block is a stdout `{"decision":"deny"}` document AND/OR
|
|
26
|
+
* a non-zero exit — {@link interpretGrokBlock} recognises either, so a
|
|
27
|
+
* correct Grok block that exits 0 with a deny decision is not misread as an
|
|
28
|
+
* allow. Grok's passive events (everything but PreToolUse) run side-effect
|
|
29
|
+
* hooks but cannot inject model-facing context or emit a decision at all;
|
|
30
|
+
* {@link grokCanBlock} names that limitation for the replay to report NA.
|
|
31
|
+
*
|
|
32
|
+
* Everything here is pure and exhaustively unit-tested; parity.ts wires these to
|
|
33
|
+
* the real adapters and to the verdict comparison.
|
|
34
|
+
*/
|
|
35
|
+
/** The three platforms a fixture case is replayed through. */
|
|
36
|
+
export type PlatformId = "claude" | "codex" | "grok";
|
|
37
|
+
/** Canonical platform order for side-by-side reporting. */
|
|
38
|
+
export declare const PARITY_PLATFORMS: readonly PlatformId[];
|
|
39
|
+
/**
|
|
40
|
+
* The minimal slice of a fixture case the payload builders need: the lifecycle
|
|
41
|
+
* event, the tool name (canonical Claude name, as fixtures are authored), and
|
|
42
|
+
* the opaque tool input. A {@link import("./fixtures/schema.js").FixtureCase} is
|
|
43
|
+
* assignable to this.
|
|
44
|
+
*/
|
|
45
|
+
export interface CaseShape {
|
|
46
|
+
/** Lifecycle event, e.g. `PreToolUse`. */
|
|
47
|
+
event: string;
|
|
48
|
+
/** Canonical Claude tool name, e.g. `Bash`, `Edit`, `Read`. */
|
|
49
|
+
tool: string;
|
|
50
|
+
/** The tool input payload, opaque to normalisation. */
|
|
51
|
+
input: unknown;
|
|
52
|
+
}
|
|
53
|
+
/** Common fields stamped onto every payload so session-scoped hooks behave. */
|
|
54
|
+
export interface PayloadContext {
|
|
55
|
+
/** Working directory the hook should see (the sandbox root). */
|
|
56
|
+
cwd: string;
|
|
57
|
+
/** Session id, so per-session dedupe does not collapse into a shared ledger. */
|
|
58
|
+
sessionId: string;
|
|
59
|
+
}
|
|
60
|
+
/** The snake_case document Claude and Codex hooks read from stdin. */
|
|
61
|
+
export interface SnakeCasePayload {
|
|
62
|
+
hook_event_name: string;
|
|
63
|
+
tool_name: string;
|
|
64
|
+
tool_input: unknown;
|
|
65
|
+
cwd: string;
|
|
66
|
+
session_id: string;
|
|
67
|
+
}
|
|
68
|
+
/** The camelCase document the Grok adapter reads from stdin. */
|
|
69
|
+
export interface GrokPayload {
|
|
70
|
+
hookEventName: string;
|
|
71
|
+
toolName: string;
|
|
72
|
+
toolInput: unknown;
|
|
73
|
+
cwd: string;
|
|
74
|
+
sessionId: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The canonical Grok tool name to emit for each canonical Claude tool. These are
|
|
78
|
+
* names the real adapter's case statement recognises; an unlisted tool is passed
|
|
79
|
+
* through unchanged (the adapter's `*` arm does the same). The AC requires at
|
|
80
|
+
* minimum `Shell` (Bash), `StrReplace` (Edit), `Read`, and `Write`.
|
|
81
|
+
*/
|
|
82
|
+
export declare const CLAUDE_TO_GROK_TOOL: Readonly<Record<string, string>>;
|
|
83
|
+
/**
|
|
84
|
+
* Every Grok tool alias mapped back to its canonical Claude tool, mirroring the
|
|
85
|
+
* `case "$GTOOL" in …` block of `.grok/hooks/hq-grok-hook-adapter.sh`. Includes
|
|
86
|
+
* the snake_case aliases the AC calls out — `run_terminal_command`,
|
|
87
|
+
* `search_replace`, `write` — alongside the CamelCase forms.
|
|
88
|
+
*/
|
|
89
|
+
export declare const GROK_TO_CLAUDE_TOOL: Readonly<Record<string, string>>;
|
|
90
|
+
/** The Grok tool name to emit for a canonical Claude tool (identity if unknown). */
|
|
91
|
+
export declare function toGrokToolName(claudeTool: string): string;
|
|
92
|
+
/**
|
|
93
|
+
* The canonical Claude tool a Grok tool name maps to, mirroring the adapter.
|
|
94
|
+
* An unrecognised name is returned unchanged (the adapter's `*` arm keeps it).
|
|
95
|
+
*/
|
|
96
|
+
export declare function resolveGrokTool(grokTool: string): string;
|
|
97
|
+
/**
|
|
98
|
+
* Normalise a case into the Claude payload shape: snake_case keys, the canonical
|
|
99
|
+
* tool name, and the input under `tool_input`. This is exactly what
|
|
100
|
+
* `.claude/hooks/hook-gate.sh` delegates to a hook on stdin.
|
|
101
|
+
*/
|
|
102
|
+
export declare function toClaudePayload(c: CaseShape, ctx: PayloadContext): SnakeCasePayload;
|
|
103
|
+
/**
|
|
104
|
+
* Normalise a case into the Codex payload shape. Codex hooks read the same
|
|
105
|
+
* snake_case document Claude does (the Codex adapter normalises its own edit
|
|
106
|
+
* events into this shape before the gate sees them), so this is structurally a
|
|
107
|
+
* Claude payload — kept a distinct function so a future Codex-specific quirk
|
|
108
|
+
* (e.g. an `apply_patch` representation) has one obvious seam to grow at.
|
|
109
|
+
*/
|
|
110
|
+
export declare function toCodexPayload(c: CaseShape, ctx: PayloadContext): SnakeCasePayload;
|
|
111
|
+
/**
|
|
112
|
+
* Normalise a case into the Grok payload shape: camelCase keys and the Grok
|
|
113
|
+
* tool alias, matching what the real Grok adapter reads. The inner `toolInput`
|
|
114
|
+
* field names stay as the case authored them (the adapter reads
|
|
115
|
+
* `.toolInput.command` / `.toolInput.file_path` / …), so only the outer envelope
|
|
116
|
+
* is renamed.
|
|
117
|
+
*/
|
|
118
|
+
export declare function toGrokPayload(c: CaseShape, ctx: PayloadContext): GrokPayload;
|
|
119
|
+
/**
|
|
120
|
+
* Convert a Grok camelCase payload back into the Claude-shaped snake_case
|
|
121
|
+
* document the gate delegates on stdin — exactly what
|
|
122
|
+
* `.grok/hooks/hq-grok-hook-adapter.sh` does internally before it pipes to
|
|
123
|
+
* `hook-gate.sh`: it reads the camelCase envelope, maps the Grok tool alias back
|
|
124
|
+
* to the canonical Claude tool, and builds `CLAUDE_JSON`. Reproducing that
|
|
125
|
+
* normalisation lets the replay route one Grok case through the same canonical
|
|
126
|
+
* `.claude/` gate + script the adapter ultimately execs, isolating a single hook.
|
|
127
|
+
*/
|
|
128
|
+
export declare function grokToSnakePayload(payload: GrokPayload): SnakeCasePayload;
|
|
129
|
+
/**
|
|
130
|
+
* Grok's passive lifecycle events: they run side-effect hooks (autocommit,
|
|
131
|
+
* checkpoints, policy evaluation) but, unlike Claude/Codex, cannot inject
|
|
132
|
+
* model-facing context or emit a block decision. Only `PreToolUse` can deny.
|
|
133
|
+
* The set mirrors the adapter's event handlers.
|
|
134
|
+
*/
|
|
135
|
+
export declare const GROK_PASSIVE_EVENTS: readonly string[];
|
|
136
|
+
/**
|
|
137
|
+
* Normalise the event-name spellings the Grok adapter accepts (`pre_tool_use`
|
|
138
|
+
* and `PreToolUse` both occur in Grok docs) to the canonical Claude spelling.
|
|
139
|
+
*/
|
|
140
|
+
export declare function normalizeGrokEvent(event: string): string;
|
|
141
|
+
/**
|
|
142
|
+
* Whether Grok can produce a comparable block/allow verdict for this event.
|
|
143
|
+
* Only `PreToolUse` can — every other event is passive under Grok and its
|
|
144
|
+
* verdict must be reported NA rather than compared (AC5), never PASS or FAIL.
|
|
145
|
+
*/
|
|
146
|
+
export declare function grokCanBlock(event: string): boolean;
|
|
147
|
+
/**
|
|
148
|
+
* Claude/Codex block protocol: a hook (through the gate) signals a block with a
|
|
149
|
+
* non-zero exit. Timeouts are handled upstream, so a `null` exit (killed) is not
|
|
150
|
+
* expected here; treated conservatively as non-zero.
|
|
151
|
+
*/
|
|
152
|
+
export declare function interpretExitBlock(exitCode: number | null): boolean;
|
|
153
|
+
/**
|
|
154
|
+
* The decision a Grok adapter stdout carries, or null when none. Scans the
|
|
155
|
+
* output line by line for a JSON object with a string `decision` field —
|
|
156
|
+
* diagnostics on other lines (or on stderr) are ignored — and returns the last
|
|
157
|
+
* such decision seen, so a trailing `deny` is not masked by an earlier `allow`.
|
|
158
|
+
*/
|
|
159
|
+
export declare function parseGrokDecision(stdout: string): "deny" | "allow" | null;
|
|
160
|
+
/**
|
|
161
|
+
* Grok block protocol: a block is either a non-zero exit OR a stdout `deny`
|
|
162
|
+
* decision — the adapter's `deny()` emits both, but a deny with exit 0 is still
|
|
163
|
+
* a block (AC3), so both signals are honoured. A `null` exit (timeout/kill) is
|
|
164
|
+
* handled upstream; here it counts as non-zero and therefore a block.
|
|
165
|
+
*/
|
|
166
|
+
export declare function interpretGrokBlock(output: {
|
|
167
|
+
exitCode: number | null;
|
|
168
|
+
stdout: string;
|
|
169
|
+
}): boolean;
|
|
170
|
+
//# sourceMappingURL=payload-shapes.d.ts.map
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-platform hook payload shapes, tool-name aliasing, and block protocols
|
|
3
|
+
* (US-010).
|
|
4
|
+
*
|
|
5
|
+
* `hq doctor --deep-test` replays one fixture case through all three platform
|
|
6
|
+
* adapters and compares their verdicts (parity.ts). The three platforms do not
|
|
7
|
+
* speak the same wire language, and this module owns every difference so the
|
|
8
|
+
* replay orchestration can stay platform-agnostic:
|
|
9
|
+
*
|
|
10
|
+
* 1. Payload shape. Claude and Codex hooks read a snake_case document
|
|
11
|
+
* (`hook_event_name` / `tool_name` / `tool_input`). Grok speaks camelCase
|
|
12
|
+
* (`hookEventName` / `toolName` / `toolInput`) and its own tool names
|
|
13
|
+
* (`Shell`, `StrReplace`, …). {@link toClaudePayload}, {@link toCodexPayload},
|
|
14
|
+
* and {@link toGrokPayload} normalise one case into each shape.
|
|
15
|
+
*
|
|
16
|
+
* 2. Tool-name aliases. Grok surfaces the same underlying tool under several
|
|
17
|
+
* names — `Shell` / `run_terminal_command` for Bash, `StrReplace` /
|
|
18
|
+
* `search_replace` for Edit, `write` for Write, and so on. The alias tables
|
|
19
|
+
* here mirror the case statement in `.grok/hooks/hq-grok-hook-adapter.sh`
|
|
20
|
+
* exactly, so a payload we emit is one the real adapter recognises, and a
|
|
21
|
+
* Grok name the adapter would receive maps back to the canonical Claude
|
|
22
|
+
* tool the fixtures are written against.
|
|
23
|
+
*
|
|
24
|
+
* 3. Block protocol. Claude and Codex signal a block with a non-zero exit.
|
|
25
|
+
* Grok's PreToolUse block is a stdout `{"decision":"deny"}` document AND/OR
|
|
26
|
+
* a non-zero exit — {@link interpretGrokBlock} recognises either, so a
|
|
27
|
+
* correct Grok block that exits 0 with a deny decision is not misread as an
|
|
28
|
+
* allow. Grok's passive events (everything but PreToolUse) run side-effect
|
|
29
|
+
* hooks but cannot inject model-facing context or emit a decision at all;
|
|
30
|
+
* {@link grokCanBlock} names that limitation for the replay to report NA.
|
|
31
|
+
*
|
|
32
|
+
* Everything here is pure and exhaustively unit-tested; parity.ts wires these to
|
|
33
|
+
* the real adapters and to the verdict comparison.
|
|
34
|
+
*/
|
|
35
|
+
/** Canonical platform order for side-by-side reporting. */
|
|
36
|
+
export const PARITY_PLATFORMS = [
|
|
37
|
+
"claude",
|
|
38
|
+
"codex",
|
|
39
|
+
"grok",
|
|
40
|
+
];
|
|
41
|
+
// --- tool-name aliases ---------------------------------------------------------
|
|
42
|
+
/**
|
|
43
|
+
* The canonical Grok tool name to emit for each canonical Claude tool. These are
|
|
44
|
+
* names the real adapter's case statement recognises; an unlisted tool is passed
|
|
45
|
+
* through unchanged (the adapter's `*` arm does the same). The AC requires at
|
|
46
|
+
* minimum `Shell` (Bash), `StrReplace` (Edit), `Read`, and `Write`.
|
|
47
|
+
*/
|
|
48
|
+
export const CLAUDE_TO_GROK_TOOL = {
|
|
49
|
+
Bash: "Shell",
|
|
50
|
+
Edit: "StrReplace",
|
|
51
|
+
Write: "Write",
|
|
52
|
+
Read: "Read",
|
|
53
|
+
Grep: "Grep",
|
|
54
|
+
Glob: "ListDir",
|
|
55
|
+
WebSearch: "WebSearch",
|
|
56
|
+
Task: "spawn_subagent",
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Every Grok tool alias mapped back to its canonical Claude tool, mirroring the
|
|
60
|
+
* `case "$GTOOL" in …` block of `.grok/hooks/hq-grok-hook-adapter.sh`. Includes
|
|
61
|
+
* the snake_case aliases the AC calls out — `run_terminal_command`,
|
|
62
|
+
* `search_replace`, `write` — alongside the CamelCase forms.
|
|
63
|
+
*/
|
|
64
|
+
export const GROK_TO_CLAUDE_TOOL = {
|
|
65
|
+
// Bash
|
|
66
|
+
run_terminal_command: "Bash",
|
|
67
|
+
Shell: "Bash",
|
|
68
|
+
Bash: "Bash",
|
|
69
|
+
bash: "Bash",
|
|
70
|
+
// Edit
|
|
71
|
+
search_replace: "Edit",
|
|
72
|
+
StrReplace: "Edit",
|
|
73
|
+
Edit: "Edit",
|
|
74
|
+
MultiEdit: "Edit",
|
|
75
|
+
apply_patch: "Edit",
|
|
76
|
+
// Write
|
|
77
|
+
write: "Write",
|
|
78
|
+
Write: "Write",
|
|
79
|
+
// Read
|
|
80
|
+
read_file: "Read",
|
|
81
|
+
Read: "Read",
|
|
82
|
+
// Grep
|
|
83
|
+
grep: "Grep",
|
|
84
|
+
Grep: "Grep",
|
|
85
|
+
// Glob
|
|
86
|
+
list_dir: "Glob",
|
|
87
|
+
Glob: "Glob",
|
|
88
|
+
ListDir: "Glob",
|
|
89
|
+
// WebSearch
|
|
90
|
+
web_search: "WebSearch",
|
|
91
|
+
WebSearch: "WebSearch",
|
|
92
|
+
// Task
|
|
93
|
+
spawn_subagent: "Task",
|
|
94
|
+
Task: "Task",
|
|
95
|
+
};
|
|
96
|
+
/** The Grok tool name to emit for a canonical Claude tool (identity if unknown). */
|
|
97
|
+
export function toGrokToolName(claudeTool) {
|
|
98
|
+
return CLAUDE_TO_GROK_TOOL[claudeTool] ?? claudeTool;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The canonical Claude tool a Grok tool name maps to, mirroring the adapter.
|
|
102
|
+
* An unrecognised name is returned unchanged (the adapter's `*` arm keeps it).
|
|
103
|
+
*/
|
|
104
|
+
export function resolveGrokTool(grokTool) {
|
|
105
|
+
return GROK_TO_CLAUDE_TOOL[grokTool] ?? grokTool;
|
|
106
|
+
}
|
|
107
|
+
// --- payload builders ----------------------------------------------------------
|
|
108
|
+
/**
|
|
109
|
+
* Normalise a case into the Claude payload shape: snake_case keys, the canonical
|
|
110
|
+
* tool name, and the input under `tool_input`. This is exactly what
|
|
111
|
+
* `.claude/hooks/hook-gate.sh` delegates to a hook on stdin.
|
|
112
|
+
*/
|
|
113
|
+
export function toClaudePayload(c, ctx) {
|
|
114
|
+
return {
|
|
115
|
+
hook_event_name: c.event,
|
|
116
|
+
tool_name: c.tool,
|
|
117
|
+
tool_input: c.input,
|
|
118
|
+
cwd: ctx.cwd,
|
|
119
|
+
session_id: ctx.sessionId,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Normalise a case into the Codex payload shape. Codex hooks read the same
|
|
124
|
+
* snake_case document Claude does (the Codex adapter normalises its own edit
|
|
125
|
+
* events into this shape before the gate sees them), so this is structurally a
|
|
126
|
+
* Claude payload — kept a distinct function so a future Codex-specific quirk
|
|
127
|
+
* (e.g. an `apply_patch` representation) has one obvious seam to grow at.
|
|
128
|
+
*/
|
|
129
|
+
export function toCodexPayload(c, ctx) {
|
|
130
|
+
return {
|
|
131
|
+
hook_event_name: c.event,
|
|
132
|
+
tool_name: c.tool,
|
|
133
|
+
tool_input: c.input,
|
|
134
|
+
cwd: ctx.cwd,
|
|
135
|
+
session_id: ctx.sessionId,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Normalise a case into the Grok payload shape: camelCase keys and the Grok
|
|
140
|
+
* tool alias, matching what the real Grok adapter reads. The inner `toolInput`
|
|
141
|
+
* field names stay as the case authored them (the adapter reads
|
|
142
|
+
* `.toolInput.command` / `.toolInput.file_path` / …), so only the outer envelope
|
|
143
|
+
* is renamed.
|
|
144
|
+
*/
|
|
145
|
+
export function toGrokPayload(c, ctx) {
|
|
146
|
+
return {
|
|
147
|
+
hookEventName: c.event,
|
|
148
|
+
toolName: toGrokToolName(c.tool),
|
|
149
|
+
toolInput: c.input,
|
|
150
|
+
cwd: ctx.cwd,
|
|
151
|
+
sessionId: ctx.sessionId,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Convert a Grok camelCase payload back into the Claude-shaped snake_case
|
|
156
|
+
* document the gate delegates on stdin — exactly what
|
|
157
|
+
* `.grok/hooks/hq-grok-hook-adapter.sh` does internally before it pipes to
|
|
158
|
+
* `hook-gate.sh`: it reads the camelCase envelope, maps the Grok tool alias back
|
|
159
|
+
* to the canonical Claude tool, and builds `CLAUDE_JSON`. Reproducing that
|
|
160
|
+
* normalisation lets the replay route one Grok case through the same canonical
|
|
161
|
+
* `.claude/` gate + script the adapter ultimately execs, isolating a single hook.
|
|
162
|
+
*/
|
|
163
|
+
export function grokToSnakePayload(payload) {
|
|
164
|
+
return {
|
|
165
|
+
hook_event_name: payload.hookEventName,
|
|
166
|
+
tool_name: resolveGrokTool(payload.toolName),
|
|
167
|
+
tool_input: payload.toolInput,
|
|
168
|
+
cwd: payload.cwd,
|
|
169
|
+
session_id: payload.sessionId,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// --- Grok events ---------------------------------------------------------------
|
|
173
|
+
/**
|
|
174
|
+
* Grok's passive lifecycle events: they run side-effect hooks (autocommit,
|
|
175
|
+
* checkpoints, policy evaluation) but, unlike Claude/Codex, cannot inject
|
|
176
|
+
* model-facing context or emit a block decision. Only `PreToolUse` can deny.
|
|
177
|
+
* The set mirrors the adapter's event handlers.
|
|
178
|
+
*/
|
|
179
|
+
export const GROK_PASSIVE_EVENTS = [
|
|
180
|
+
"SessionStart",
|
|
181
|
+
"UserPromptSubmit",
|
|
182
|
+
"PostToolUse",
|
|
183
|
+
"Stop",
|
|
184
|
+
"PreCompact",
|
|
185
|
+
"SessionEnd",
|
|
186
|
+
];
|
|
187
|
+
/**
|
|
188
|
+
* Normalise the event-name spellings the Grok adapter accepts (`pre_tool_use`
|
|
189
|
+
* and `PreToolUse` both occur in Grok docs) to the canonical Claude spelling.
|
|
190
|
+
*/
|
|
191
|
+
export function normalizeGrokEvent(event) {
|
|
192
|
+
switch (event) {
|
|
193
|
+
case "pre_tool_use":
|
|
194
|
+
case "PreToolUse":
|
|
195
|
+
return "PreToolUse";
|
|
196
|
+
case "post_tool_use":
|
|
197
|
+
case "PostToolUse":
|
|
198
|
+
return "PostToolUse";
|
|
199
|
+
case "session_start":
|
|
200
|
+
case "SessionStart":
|
|
201
|
+
return "SessionStart";
|
|
202
|
+
case "user_prompt_submit":
|
|
203
|
+
case "UserPromptSubmit":
|
|
204
|
+
return "UserPromptSubmit";
|
|
205
|
+
case "pre_compact":
|
|
206
|
+
case "PreCompact":
|
|
207
|
+
return "PreCompact";
|
|
208
|
+
case "stop":
|
|
209
|
+
case "Stop":
|
|
210
|
+
return "Stop";
|
|
211
|
+
case "session_end":
|
|
212
|
+
case "SessionEnd":
|
|
213
|
+
return "SessionEnd";
|
|
214
|
+
default:
|
|
215
|
+
return event;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Whether Grok can produce a comparable block/allow verdict for this event.
|
|
220
|
+
* Only `PreToolUse` can — every other event is passive under Grok and its
|
|
221
|
+
* verdict must be reported NA rather than compared (AC5), never PASS or FAIL.
|
|
222
|
+
*/
|
|
223
|
+
export function grokCanBlock(event) {
|
|
224
|
+
return normalizeGrokEvent(event) === "PreToolUse";
|
|
225
|
+
}
|
|
226
|
+
// --- block protocols -----------------------------------------------------------
|
|
227
|
+
/**
|
|
228
|
+
* Claude/Codex block protocol: a hook (through the gate) signals a block with a
|
|
229
|
+
* non-zero exit. Timeouts are handled upstream, so a `null` exit (killed) is not
|
|
230
|
+
* expected here; treated conservatively as non-zero.
|
|
231
|
+
*/
|
|
232
|
+
export function interpretExitBlock(exitCode) {
|
|
233
|
+
return exitCode !== 0;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The decision a Grok adapter stdout carries, or null when none. Scans the
|
|
237
|
+
* output line by line for a JSON object with a string `decision` field —
|
|
238
|
+
* diagnostics on other lines (or on stderr) are ignored — and returns the last
|
|
239
|
+
* such decision seen, so a trailing `deny` is not masked by an earlier `allow`.
|
|
240
|
+
*/
|
|
241
|
+
export function parseGrokDecision(stdout) {
|
|
242
|
+
let decision = null;
|
|
243
|
+
for (const rawLine of stdout.split("\n")) {
|
|
244
|
+
const line = rawLine.trim();
|
|
245
|
+
if (!line.startsWith("{"))
|
|
246
|
+
continue;
|
|
247
|
+
let parsed;
|
|
248
|
+
try {
|
|
249
|
+
parsed = JSON.parse(line);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (!parsed || typeof parsed !== "object")
|
|
255
|
+
continue;
|
|
256
|
+
const value = parsed.decision;
|
|
257
|
+
if (value === "deny" || value === "block")
|
|
258
|
+
decision = "deny";
|
|
259
|
+
else if (value === "allow")
|
|
260
|
+
decision = "allow";
|
|
261
|
+
}
|
|
262
|
+
return decision;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Grok block protocol: a block is either a non-zero exit OR a stdout `deny`
|
|
266
|
+
* decision — the adapter's `deny()` emits both, but a deny with exit 0 is still
|
|
267
|
+
* a block (AC3), so both signals are honoured. A `null` exit (timeout/kill) is
|
|
268
|
+
* handled upstream; here it counts as non-zero and therefore a block.
|
|
269
|
+
*/
|
|
270
|
+
export function interpretGrokBlock(output) {
|
|
271
|
+
if (output.exitCode !== 0)
|
|
272
|
+
return true;
|
|
273
|
+
return parseGrokDecision(output.stdout) === "deny";
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=payload-shapes.js.map
|