aiwcli 0.12.6 → 0.12.8

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 (163) hide show
  1. package/bin/dev.cmd +3 -3
  2. package/bin/dev.js +16 -16
  3. package/bin/run.cmd +3 -3
  4. package/bin/run.js +21 -21
  5. package/dist/commands/branch.js +7 -2
  6. package/dist/lib/bmad-installer.js +37 -37
  7. package/dist/lib/terminal.d.ts +2 -0
  8. package/dist/lib/terminal.js +57 -7
  9. package/dist/templates/CLAUDE.md +232 -205
  10. package/dist/templates/_shared/.claude/settings.json +65 -65
  11. package/dist/templates/_shared/.claude/{commands/handoff.md → skills/handoff/SKILL.md} +13 -12
  12. package/dist/templates/_shared/.claude/{commands/handoff-resume.md → skills/handoff-resume/SKILL.md} +13 -12
  13. package/dist/templates/_shared/.codex/workflows/handoff.md +226 -226
  14. package/dist/templates/_shared/.windsurf/workflows/handoff.md +226 -226
  15. package/dist/templates/_shared/handoff-system/CLAUDE.md +15 -3
  16. package/dist/templates/_shared/handoff-system/lib/document-generator.ts +215 -215
  17. package/dist/templates/_shared/handoff-system/lib/handoff-reader.ts +158 -158
  18. package/dist/templates/_shared/handoff-system/scripts/resume_handoff.ts +373 -373
  19. package/dist/templates/_shared/handoff-system/scripts/save_handoff.ts +469 -469
  20. package/dist/templates/_shared/handoff-system/workflows/handoff-resume.md +66 -66
  21. package/dist/templates/_shared/handoff-system/workflows/handoff.md +254 -254
  22. package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -2
  23. package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -159
  24. package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -147
  25. package/dist/templates/_shared/hooks-ts/file-suggestion.ts +128 -128
  26. package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -49
  27. package/dist/templates/_shared/hooks-ts/session_end.ts +196 -196
  28. package/dist/templates/_shared/hooks-ts/session_start.ts +163 -163
  29. package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -48
  30. package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -74
  31. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +93 -93
  32. package/dist/templates/_shared/lib-ts/CLAUDE.md +367 -367
  33. package/dist/templates/_shared/lib-ts/base/atomic-write.ts +138 -138
  34. package/dist/templates/_shared/lib-ts/base/constants.ts +24 -6
  35. package/dist/templates/_shared/lib-ts/base/git-state.ts +58 -58
  36. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +582 -582
  37. package/dist/templates/_shared/lib-ts/base/inference.ts +301 -301
  38. package/dist/templates/_shared/lib-ts/base/logger.ts +247 -247
  39. package/dist/templates/_shared/lib-ts/base/state-io.ts +202 -202
  40. package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -184
  41. package/dist/templates/_shared/lib-ts/base/utils.ts +184 -184
  42. package/dist/templates/_shared/lib-ts/context/CLAUDE.md +134 -0
  43. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +566 -566
  44. package/dist/templates/_shared/lib-ts/context/context-selector.ts +524 -524
  45. package/dist/templates/_shared/lib-ts/context/context-store.ts +712 -712
  46. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +312 -312
  47. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +185 -185
  48. package/dist/templates/_shared/lib-ts/package.json +20 -20
  49. package/dist/templates/_shared/lib-ts/templates/formatters.ts +102 -102
  50. package/dist/templates/_shared/lib-ts/templates/plan-context.ts +58 -58
  51. package/dist/templates/_shared/lib-ts/tsconfig.json +13 -13
  52. package/dist/templates/_shared/lib-ts/types.ts +186 -186
  53. package/dist/templates/_shared/scripts/resolve_context.ts +33 -33
  54. package/dist/templates/_shared/scripts/status_line.ts +687 -690
  55. package/dist/templates/cc-native/.claude/commands/cc-native/rlm/ask.md +136 -136
  56. package/dist/templates/cc-native/.claude/commands/cc-native/rlm/index.md +21 -21
  57. package/dist/templates/cc-native/.claude/commands/cc-native/rlm/overview.md +56 -56
  58. package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -10
  59. package/dist/templates/cc-native/.claude/settings.json +3 -2
  60. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -8
  61. package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -8
  62. package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -8
  63. package/dist/templates/cc-native/CC-NATIVE-README.md +189 -189
  64. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +304 -304
  65. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +143 -143
  66. package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +213 -213
  67. package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -70
  68. package/dist/templates/cc-native/_cc-native/artifacts/CLAUDE.md +64 -0
  69. package/dist/templates/cc-native/_cc-native/{lib-ts/artifacts → artifacts/lib}/format.ts +1 -1
  70. package/dist/templates/cc-native/_cc-native/{lib-ts/artifacts → artifacts/lib}/write.ts +2 -2
  71. package/dist/templates/cc-native/_cc-native/cc-native.config.json +96 -96
  72. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +14 -24
  73. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +1 -1
  74. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +54 -54
  75. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +51 -51
  76. package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -53
  77. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -61
  78. package/dist/templates/cc-native/_cc-native/hooks/validate_task_prompt.ts +76 -0
  79. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +9 -2
  80. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +319 -319
  81. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -144
  82. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -57
  83. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -83
  84. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +79 -79
  85. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +4 -4
  86. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -168
  87. package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +80 -80
  88. package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -41
  89. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/CLAUDE.md +480 -480
  90. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/embedding-indexer.ts +287 -287
  91. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/hyde.ts +148 -148
  92. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/index.ts +54 -54
  93. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/logger.ts +58 -58
  94. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/ollama-client.ts +208 -208
  95. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/retrieval-pipeline.ts +460 -460
  96. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +446 -446
  97. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-loader.ts +280 -280
  98. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-searcher.ts +274 -274
  99. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/types.ts +201 -201
  100. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/vector-store.ts +278 -278
  101. package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +184 -184
  102. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +275 -275
  103. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -18
  104. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +1 -1
  105. package/dist/templates/cc-native/_cc-native/plan-review/CLAUDE.md +149 -0
  106. package/dist/templates/cc-native/_cc-native/plan-review/agents/CLAUDE.md +143 -0
  107. package/dist/templates/cc-native/_cc-native/plan-review/agents/PLAN-ORCHESTRATOR.md +213 -0
  108. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-questions/PLAN-QUESTIONER.md +70 -0
  109. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/ARCH-EVOLUTION.md +62 -0
  110. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/ARCH-PATTERNS.md +61 -0
  111. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/ARCH-STRUCTURE.md +62 -0
  112. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/ASSUMPTION-TRACER.md +56 -0
  113. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/CLARITY-AUDITOR.md +53 -0
  114. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/COMPLETENESS-FEASIBILITY.md +66 -0
  115. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/COMPLETENESS-GAPS.md +70 -0
  116. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/COMPLETENESS-ORDERING.md +62 -0
  117. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/CONSTRAINT-VALIDATOR.md +72 -0
  118. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/DESIGN-ADR-VALIDATOR.md +61 -0
  119. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/DESIGN-SCALE-MATCHER.md +64 -0
  120. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/DEVILS-ADVOCATE.md +56 -0
  121. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/DOCUMENTATION-PHILOSOPHY.md +86 -0
  122. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/HANDOFF-READINESS.md +59 -0
  123. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/HIDDEN-COMPLEXITY.md +58 -0
  124. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/INCREMENTAL-DELIVERY.md +66 -0
  125. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/RISK-DEPENDENCY.md +62 -0
  126. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/RISK-FMEA.md +66 -0
  127. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/RISK-PREMORTEM.md +71 -0
  128. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/RISK-REVERSIBILITY.md +74 -0
  129. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/SCOPE-BOUNDARY.md +77 -0
  130. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/SIMPLICITY-GUARDIAN.md +62 -0
  131. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/SKEPTIC.md +68 -0
  132. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TESTDRIVEN-BEHAVIOR-AUDITOR.md +61 -0
  133. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TESTDRIVEN-CHARACTERIZATION.md +71 -0
  134. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TESTDRIVEN-FIRST-VALIDATOR.md +61 -0
  135. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TESTDRIVEN-PYRAMID-ANALYZER.md +61 -0
  136. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TRADEOFF-COSTS.md +67 -0
  137. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TRADEOFF-STAKEHOLDERS.md +65 -0
  138. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/VERIFY-COVERAGE.md +74 -0
  139. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/VERIFY-STRENGTH.md +69 -0
  140. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/agent-selection.ts +3 -3
  141. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/corroboration.ts +1 -1
  142. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/graduation.ts +1 -1
  143. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/orchestrator.ts +2 -2
  144. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/output-builder.ts +3 -3
  145. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/plan-questions.ts +6 -6
  146. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/review-pipeline.ts +15 -15
  147. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/agent.ts +5 -5
  148. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/base/base-agent.ts +4 -4
  149. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/providers/claude-agent.ts +4 -4
  150. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/providers/codex-agent.ts +6 -6
  151. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/providers/gemini-agent.ts +1 -1
  152. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/providers/orchestrator-claude-agent.ts +4 -4
  153. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/types.ts +3 -3
  154. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/verdict.ts +1 -1
  155. package/oclif.manifest.json +1 -1
  156. package/package.json +108 -108
  157. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +0 -21
  158. package/dist/templates/cc-native/_cc-native/lib-ts/nul +0 -3
  159. /package/dist/templates/cc-native/_cc-native/{lib-ts/artifacts → artifacts/lib}/index.ts +0 -0
  160. /package/dist/templates/cc-native/_cc-native/{lib-ts/artifacts → artifacts/lib}/tracker.ts +0 -0
  161. /package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/index.ts +0 -0
  162. /package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/schemas.ts +0 -0
  163. /package/dist/templates/cc-native/_cc-native/{workflows → plan-review/workflows}/specdev.md +0 -0
@@ -1,586 +1,586 @@
1
- /**
2
- * Common utilities for hook scripts.
3
- * Standardized boilerplate for JSON parsing, validation, error handling.
4
- * See SPEC.md §5
5
- */
6
-
1
+ /**
2
+ * Common utilities for hook scripts.
3
+ * Standardized boilerplate for JSON parsing, validation, error handling.
4
+ * See SPEC.md §5
5
+ */
6
+
7
7
  import * as fs from "node:fs";
8
-
9
- import { getProjectRoot } from "./constants.js";
10
- import { logDebug, logWarn, hookLog, setSessionId, getContextPath as _getContextPath } from "./logger.js";
11
- import { getContextBySessionId } from "../context/context-store.js";
12
- import type { HookInput, HookOutput, PermissionRequestOutput } from "../types.js";
13
-
14
- // Re-export logger functions for convenience (matches Python hook_utils re-exports)
15
- export { setSessionId };
16
-
17
- // Context window baseline: tokens not visible in hook data §5.9
18
- export const CONTEXT_BASELINE_TOKENS = 22_600;
19
- export const DEFAULT_CONTEXT_WINDOW_SIZE = 200_000;
20
-
21
- // Event metadata stash — populated by loadHookInput(), read by runHook()
22
- let _lastHookEvent: string | null = null;
23
- let _lastToolName: string | null = null;
24
- let _cachedHookName: string | null = null;
25
-
26
- // Pre-fetched input stash
27
- let _prefetchedInput: Record<string, any> | null = null;
28
-
29
- /**
30
- * Load and parse JSON from stdin (or return prefetched input if set).
31
- * Returns null if stdin is empty or invalid JSON.
32
- * See SPEC.md §5.1
33
- */
34
- export function loadHookInput(): HookInput | null {
35
- if (_prefetchedInput !== null) {
36
- const result = _prefetchedInput;
37
- _prefetchedInput = null; // consume once
38
- if (result && typeof result === "object") {
39
- _lastHookEvent = result.hook_event_name ?? null;
40
- _lastToolName = result.tool_name ?? null;
41
- }
42
- return result as HookInput;
43
- }
44
-
45
- try {
46
- // Read entire stdin using fd 0 (cross-platform, works on Windows)
47
- const inputData = fs.readFileSync(0, "utf-8").trim();
48
- if (!inputData) return null;
49
-
50
- const result = JSON.parse(inputData);
51
- if (result && typeof result === "object") {
52
- _lastHookEvent = result.hook_event_name ?? null;
53
- _lastToolName = result.tool_name ?? null;
54
- }
55
- return result as HookInput;
56
- } catch {
57
- return null;
58
- }
59
- }
60
-
61
- /**
62
- * Validate hook event type and optional tool name.
63
- * See SPEC.md §5.2
64
- */
65
- export function validateHookEvent(
66
- payload: HookInput,
67
- expectedEvent: string,
68
- expectedTool?: string,
69
- ): boolean {
70
- if (payload.hook_event_name !== expectedEvent) return false;
71
- if (expectedTool && payload.tool_name !== expectedTool) return false;
72
- return true;
73
- }
74
-
75
- /**
76
- * Extract and validate tool_input from payload.
77
- * See SPEC.md §5.3
78
- */
79
- export function getToolInput(
80
- payload: HookInput,
81
- ): Record<string, any> | null {
82
- const toolInput = payload.tool_input;
83
- return toolInput && typeof toolInput === "object" ? toolInput : null;
84
- }
85
-
86
- /**
87
- * Check if persistence should be skipped based on metadata flags.
88
- * See SPEC.md §5.4
89
- */
90
- export function checkSkipPersistence(
91
- payload: HookInput,
92
- hookName = "hook",
93
- ): boolean {
94
- const toolInput = getToolInput(payload);
95
- if (!toolInput) return false;
96
-
97
- const {metadata} = toolInput;
98
- if (metadata && typeof metadata === "object" && metadata.skip_persistence) {
99
- logDebug(hookName, "Skipping persistence (skip_persistence flag set)");
100
- return true;
101
- }
102
- return false;
103
- }
104
-
105
- /**
106
- * Emit hookSpecificOutput with additionalContext to stdout.
107
- * hookEventName is required by Claude Code's Zod validator (discriminated union).
108
- * Auto-detected from stdin payload (set by loadHookInput/runHook).
109
- *
110
- * SubagentStop and Stop events use top-level systemMessage field instead of hookSpecificOutput.
111
- * See SPEC.md §5.5
112
- */
113
- export function emitContext(additionalContext: string): void {
114
- const eventName = _lastHookEvent ?? undefined;
115
- const tool = _lastToolName;
116
-
117
- // SubagentStop and Stop use top-level systemMessage field
118
- if (eventName === "SubagentStop" || eventName === "Stop") {
119
- const out = { systemMessage: additionalContext };
120
- process.stdout.write(JSON.stringify(out) + "\n");
121
- _logEmit("systemMessage", additionalContext.length, { event: eventName ?? "unknown", systemMessage: additionalContext });
122
- return;
123
- }
124
-
125
- // All other events use hookSpecificOutput
126
- const out: HookOutput = {
127
- hookSpecificOutput: {
128
- ...(eventName ? { hookEventName: eventName } : {}),
129
- additionalContext,
130
- },
131
- };
132
- const json = JSON.stringify(out);
133
- const eventDesc = tool ? `${eventName}:${tool}` : eventName ?? "unknown";
134
- _logEmit("context", additionalContext.length, { event: eventDesc, additionalContext });
135
- process.stdout.write(json + "\n");
136
- }
137
-
138
- /**
139
- * Emit hookSpecificOutput that denies the tool call with context and reason.
140
- * hookEventName is required by Claude Code's Zod validator (discriminated union).
141
- * Auto-detected from stdin payload (set by loadHookInput/runHook).
142
- * See SPEC.md §5.6
143
- */
144
- export function emitContextAndBlock(
145
- additionalContext: string,
146
- reason: string,
147
- ): void {
148
- const eventName = _lastHookEvent ?? undefined;
149
- if (eventName && eventName !== "PreToolUse") {
150
- logWarn(_cachedHookName ?? "unknown",
151
- `emitContextAndBlock() called from ${eventName} — permissionDecision only works for PreToolUse. ` +
152
- `Use emitBlock() or the event-specific function instead.`);
153
- }
154
- const tool = _lastToolName;
155
- const out: HookOutput = {
156
- hookSpecificOutput: {
157
- ...(eventName ? { hookEventName: eventName } : {}),
158
- additionalContext,
159
- permissionDecision: "deny",
160
- permissionDecisionReason: reason,
161
- },
162
- };
163
- const json = JSON.stringify(out);
164
- const eventDesc = tool ? `${eventName}:${tool}` : eventName ?? "unknown";
165
- _logEmit("block", additionalContext.length, { event: eventDesc, additionalContext, blockReason: reason });
166
- process.stdout.write(json + "\n");
167
- }
168
-
169
- /** Log hook output (context, systemMessage, or block) to hook-log.jsonl for visibility. */
170
- function _logEmit(type: "context" | "systemMessage" | "block", chars: number, payload: Record<string, any>): void {
171
- const hook = _cachedHookName ?? "unknown";
172
- const event = payload.event ?? "unknown";
173
- const mechanism = payload.mechanism ? ` via ${payload.mechanism}` : "";
174
- const msg = type === "block"
175
- ? `HOOK_OUTPUT [${type}] ${event} ${chars} chars${mechanism}, reason="${(payload.blockReason ?? "").slice(0, 80)}"`
176
- : `HOOK_OUTPUT [${type}] ${event} ${chars} chars`;
177
- hookLog("info", hook, msg, { data: payload });
178
- }
179
-
180
- /**
181
- * Block a user prompt submission with a reason.
182
- * Only works for UserPromptSubmit hooks.
183
- * Output: top-level { decision: "block", reason } + optional hookSpecificOutput.additionalContext
184
- */
185
- export function emitBlockPrompt(reason: string, context?: string): void {
186
- const eventName = _lastHookEvent ?? undefined;
187
- if (eventName && eventName !== "UserPromptSubmit") {
188
- logWarn(_cachedHookName ?? "unknown",
189
- `emitBlockPrompt() called from ${eventName} — only works for UserPromptSubmit`);
190
- }
191
- const out: HookOutput = {
192
- decision: "block",
193
- reason,
194
- ...(context ? {
195
- hookSpecificOutput: {
196
- ...(eventName ? { hookEventName: eventName } : {}),
197
- additionalContext: context,
198
- }
199
- } : {}),
200
- };
201
- _logEmit("block", context?.length ?? 0, { event: eventName ?? "unknown", additionalContext: context, blockReason: reason });
202
- process.stdout.write(JSON.stringify(out) + "\n");
203
- }
204
-
205
- /**
206
- * Block via exit code 2 + stderr feedback.
207
- * Works for PostToolUse, PostToolUseFailure.
208
- * The reason becomes the stderr message (fed to Claude as system-reminder).
209
- * If context is provided, it's prepended to the stderr message for richer feedback.
210
- * NOTE: Exit 2 causes Claude Code to ignore all JSON stdout — only stderr matters.
211
- */
212
- export function emitBlockViaExit(reason: string, context?: string): void {
213
- const stderrMessage = context ? `${context}\n\n${reason}` : reason;
214
- _logEmit("block", stderrMessage.length, {
215
- event: _lastHookEvent ?? "unknown",
216
- blockReason: reason,
217
- mechanism: "exit2",
218
- });
219
- process.stderr.write(stderrMessage + "\n");
220
- throw new Error("SystemExit:2");
221
- }
222
-
223
- /**
224
- * Block via top-level { decision: "block", reason }.
225
- * Works for Stop and SubagentStop events.
226
- * These events do NOT support additionalContext — only reason is available.
227
- */
228
- export function emitBlockTopLevel(reason: string): void {
229
- const eventName = _lastHookEvent ?? undefined;
230
- if (eventName && eventName !== "Stop" && eventName !== "SubagentStop") {
231
- logWarn(_cachedHookName ?? "unknown",
232
- `emitBlockTopLevel() called from ${eventName} — only works for Stop/SubagentStop`);
233
- }
234
- const out = { decision: "block", reason };
235
- _logEmit("block", reason.length, {
236
- event: eventName ?? "unknown",
237
- blockReason: reason,
238
- mechanism: "topLevelDecision",
239
- });
240
- process.stdout.write(JSON.stringify(out) + "\n");
241
- }
242
-
243
- /**
244
- * Respond to a PermissionRequest with allow/deny.
245
- * Only works for PermissionRequest hooks.
246
- */
247
- export function emitPermissionDecision(
248
- behavior: "allow" | "deny",
249
- opts?: { message?: string; updatedInput?: Record<string, unknown>; updatedPermissions?: Record<string, unknown> },
250
- ): void {
251
- const out: PermissionRequestOutput = {
252
- decision: {
253
- behavior,
254
- ...(opts?.message ? { message: opts.message } : {}),
255
- ...(opts?.updatedInput ? { updatedInput: opts.updatedInput } : {}),
256
- ...(opts?.updatedPermissions ? { updatedPermissions: opts.updatedPermissions } : {}),
257
- },
258
- };
259
- _logEmit("block", 0, {
260
- event: _lastHookEvent ?? "unknown",
261
- blockReason: `permission:${behavior}`,
262
- mechanism: "permissionRequest",
263
- });
264
- process.stdout.write(JSON.stringify(out) + "\n");
265
- }
266
-
267
- /**
268
- * Unified block dispatcher — auto-detects the correct blocking mechanism
269
- * based on the current hook event type.
270
- *
271
- * PreToolUse → permissionDecision: "deny" (via emitContextAndBlock)
272
- * UserPromptSubmit → top-level decision: "block" (via emitBlockPrompt)
273
- * PostToolUse/PostToolUseFailure → exit(2) + stderr (via emitBlockViaExit)
274
- * Stop/SubagentStop → top-level { decision: "block", reason } (via emitBlockTopLevel)
275
- * PermissionRequest → decision: { behavior: "deny" } (via emitPermissionDecision)
276
- * SessionStart/Notification/SubagentStart/SessionEnd/etc. → warn and no-op
277
- *
278
- * This is the RECOMMENDED universal blocking API. Hook authors should use
279
- * emitBlock() and let the library handle event-specific dispatch.
280
- */
281
- export function emitBlock(reason: string, context?: string): void {
282
- const event = _lastHookEvent;
283
- switch (event) {
284
- case "PermissionRequest":
285
- emitPermissionDecision("deny", { message: reason });
286
- break;
287
- case "PostToolUse":
288
- case "PostToolUseFailure":
289
- emitBlockViaExit(reason, context);
290
- break;
291
- case "PreToolUse":
292
- emitContextAndBlock(context ?? reason, reason);
293
- break;
294
- case "Stop":
295
- case "SubagentStop":
296
- emitBlockTopLevel(reason);
297
- break;
298
- case "UserPromptSubmit":
299
- emitBlockPrompt(reason, context);
300
- break;
301
- default: {
302
- logWarn(_cachedHookName ?? "unknown",
303
- `emitBlock() called from ${event ?? "unknown"} — no blocking mechanism exists for this event type, ignoring`);
8
+
9
+ import { getProjectRoot } from "./constants.js";
10
+ import { logDebug, logWarn, hookLog, setSessionId, getContextPath as _getContextPath } from "./logger.js";
11
+ import { getContextBySessionId } from "../context/context-store.js";
12
+ import type { HookInput, HookOutput, PermissionRequestOutput } from "../types.js";
13
+
14
+ // Re-export logger functions for convenience (matches Python hook_utils re-exports)
15
+ export { setSessionId };
16
+
17
+ // Context window baseline: tokens not visible in hook data §5.9
18
+ export const CONTEXT_BASELINE_TOKENS = 22_600;
19
+ export const DEFAULT_CONTEXT_WINDOW_SIZE = 200_000;
20
+
21
+ // Event metadata stash — populated by loadHookInput(), read by runHook()
22
+ let _lastHookEvent: string | null = null;
23
+ let _lastToolName: string | null = null;
24
+ let _cachedHookName: string | null = null;
25
+
26
+ // Pre-fetched input stash
27
+ let _prefetchedInput: Record<string, any> | null = null;
28
+
29
+ /**
30
+ * Load and parse JSON from stdin (or return prefetched input if set).
31
+ * Returns null if stdin is empty or invalid JSON.
32
+ * See SPEC.md §5.1
33
+ */
34
+ export function loadHookInput(): HookInput | null {
35
+ if (_prefetchedInput !== null) {
36
+ const result = _prefetchedInput;
37
+ _prefetchedInput = null; // consume once
38
+ if (result && typeof result === "object") {
39
+ _lastHookEvent = result.hook_event_name ?? null;
40
+ _lastToolName = result.tool_name ?? null;
41
+ }
42
+ return result as HookInput;
43
+ }
44
+
45
+ try {
46
+ // Read entire stdin using fd 0 (cross-platform, works on Windows)
47
+ const inputData = fs.readFileSync(0, "utf-8").trim();
48
+ if (!inputData) return null;
49
+
50
+ const result = JSON.parse(inputData);
51
+ if (result && typeof result === "object") {
52
+ _lastHookEvent = result.hook_event_name ?? null;
53
+ _lastToolName = result.tool_name ?? null;
54
+ }
55
+ return result as HookInput;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Validate hook event type and optional tool name.
63
+ * See SPEC.md §5.2
64
+ */
65
+ export function validateHookEvent(
66
+ payload: HookInput,
67
+ expectedEvent: string,
68
+ expectedTool?: string,
69
+ ): boolean {
70
+ if (payload.hook_event_name !== expectedEvent) return false;
71
+ if (expectedTool && payload.tool_name !== expectedTool) return false;
72
+ return true;
73
+ }
74
+
75
+ /**
76
+ * Extract and validate tool_input from payload.
77
+ * See SPEC.md §5.3
78
+ */
79
+ export function getToolInput(
80
+ payload: HookInput,
81
+ ): Record<string, any> | null {
82
+ const toolInput = payload.tool_input;
83
+ return toolInput && typeof toolInput === "object" ? toolInput : null;
84
+ }
85
+
86
+ /**
87
+ * Check if persistence should be skipped based on metadata flags.
88
+ * See SPEC.md §5.4
89
+ */
90
+ export function checkSkipPersistence(
91
+ payload: HookInput,
92
+ hookName = "hook",
93
+ ): boolean {
94
+ const toolInput = getToolInput(payload);
95
+ if (!toolInput) return false;
96
+
97
+ const {metadata} = toolInput;
98
+ if (metadata && typeof metadata === "object" && metadata.skip_persistence) {
99
+ logDebug(hookName, "Skipping persistence (skip_persistence flag set)");
100
+ return true;
101
+ }
102
+ return false;
103
+ }
104
+
105
+ /**
106
+ * Emit hookSpecificOutput with additionalContext to stdout.
107
+ * hookEventName is required by Claude Code's Zod validator (discriminated union).
108
+ * Auto-detected from stdin payload (set by loadHookInput/runHook).
109
+ *
110
+ * SubagentStop and Stop events use top-level systemMessage field instead of hookSpecificOutput.
111
+ * See SPEC.md §5.5
112
+ */
113
+ export function emitContext(additionalContext: string): void {
114
+ const eventName = _lastHookEvent ?? undefined;
115
+ const tool = _lastToolName;
116
+
117
+ // SubagentStop and Stop use top-level systemMessage field
118
+ if (eventName === "SubagentStop" || eventName === "Stop") {
119
+ const out = { systemMessage: additionalContext };
120
+ process.stdout.write(JSON.stringify(out) + "\n");
121
+ _logEmit("systemMessage", additionalContext.length, { event: eventName ?? "unknown", systemMessage: additionalContext });
122
+ return;
123
+ }
124
+
125
+ // All other events use hookSpecificOutput
126
+ const out: HookOutput = {
127
+ hookSpecificOutput: {
128
+ ...(eventName ? { hookEventName: eventName } : {}),
129
+ additionalContext,
130
+ },
131
+ };
132
+ const json = JSON.stringify(out);
133
+ const eventDesc = tool ? `${eventName}:${tool}` : eventName ?? "unknown";
134
+ _logEmit("context", additionalContext.length, { event: eventDesc, additionalContext });
135
+ process.stdout.write(json + "\n");
136
+ }
137
+
138
+ /**
139
+ * Emit hookSpecificOutput that denies the tool call with context and reason.
140
+ * hookEventName is required by Claude Code's Zod validator (discriminated union).
141
+ * Auto-detected from stdin payload (set by loadHookInput/runHook).
142
+ * See SPEC.md §5.6
143
+ */
144
+ export function emitContextAndBlock(
145
+ additionalContext: string,
146
+ reason: string,
147
+ ): void {
148
+ const eventName = _lastHookEvent ?? undefined;
149
+ if (eventName && eventName !== "PreToolUse") {
150
+ logWarn(_cachedHookName ?? "unknown",
151
+ `emitContextAndBlock() called from ${eventName} — permissionDecision only works for PreToolUse. ` +
152
+ `Use emitBlock() or the event-specific function instead.`);
153
+ }
154
+ const tool = _lastToolName;
155
+ const out: HookOutput = {
156
+ hookSpecificOutput: {
157
+ ...(eventName ? { hookEventName: eventName } : {}),
158
+ additionalContext,
159
+ permissionDecision: "deny",
160
+ permissionDecisionReason: reason,
161
+ },
162
+ };
163
+ const json = JSON.stringify(out);
164
+ const eventDesc = tool ? `${eventName}:${tool}` : eventName ?? "unknown";
165
+ _logEmit("block", additionalContext.length, { event: eventDesc, additionalContext, blockReason: reason });
166
+ process.stdout.write(json + "\n");
167
+ }
168
+
169
+ /** Log hook output (context, systemMessage, or block) to hook-log.jsonl for visibility. */
170
+ function _logEmit(type: "context" | "systemMessage" | "block", chars: number, payload: Record<string, any>): void {
171
+ const hook = _cachedHookName ?? "unknown";
172
+ const event = payload.event ?? "unknown";
173
+ const mechanism = payload.mechanism ? ` via ${payload.mechanism}` : "";
174
+ const msg = type === "block"
175
+ ? `HOOK_OUTPUT [${type}] ${event} ${chars} chars${mechanism}, reason="${(payload.blockReason ?? "").slice(0, 80)}"`
176
+ : `HOOK_OUTPUT [${type}] ${event} ${chars} chars`;
177
+ hookLog("info", hook, msg, { data: payload });
178
+ }
179
+
180
+ /**
181
+ * Block a user prompt submission with a reason.
182
+ * Only works for UserPromptSubmit hooks.
183
+ * Output: top-level { decision: "block", reason } + optional hookSpecificOutput.additionalContext
184
+ */
185
+ export function emitBlockPrompt(reason: string, context?: string): void {
186
+ const eventName = _lastHookEvent ?? undefined;
187
+ if (eventName && eventName !== "UserPromptSubmit") {
188
+ logWarn(_cachedHookName ?? "unknown",
189
+ `emitBlockPrompt() called from ${eventName} — only works for UserPromptSubmit`);
190
+ }
191
+ const out: HookOutput = {
192
+ decision: "block",
193
+ reason,
194
+ ...(context ? {
195
+ hookSpecificOutput: {
196
+ ...(eventName ? { hookEventName: eventName } : {}),
197
+ additionalContext: context,
198
+ }
199
+ } : {}),
200
+ };
201
+ _logEmit("block", context?.length ?? 0, { event: eventName ?? "unknown", additionalContext: context, blockReason: reason });
202
+ process.stdout.write(JSON.stringify(out) + "\n");
203
+ }
204
+
205
+ /**
206
+ * Block via exit code 2 + stderr feedback.
207
+ * Works for PostToolUse, PostToolUseFailure.
208
+ * The reason becomes the stderr message (fed to Claude as system-reminder).
209
+ * If context is provided, it's prepended to the stderr message for richer feedback.
210
+ * NOTE: Exit 2 causes Claude Code to ignore all JSON stdout — only stderr matters.
211
+ */
212
+ export function emitBlockViaExit(reason: string, context?: string): void {
213
+ const stderrMessage = context ? `${context}\n\n${reason}` : reason;
214
+ _logEmit("block", stderrMessage.length, {
215
+ event: _lastHookEvent ?? "unknown",
216
+ blockReason: reason,
217
+ mechanism: "exit2",
218
+ });
219
+ process.stderr.write(stderrMessage + "\n");
220
+ throw new Error("SystemExit:2");
221
+ }
222
+
223
+ /**
224
+ * Block via top-level { decision: "block", reason }.
225
+ * Works for Stop and SubagentStop events.
226
+ * These events do NOT support additionalContext — only reason is available.
227
+ */
228
+ export function emitBlockTopLevel(reason: string): void {
229
+ const eventName = _lastHookEvent ?? undefined;
230
+ if (eventName && eventName !== "Stop" && eventName !== "SubagentStop") {
231
+ logWarn(_cachedHookName ?? "unknown",
232
+ `emitBlockTopLevel() called from ${eventName} — only works for Stop/SubagentStop`);
233
+ }
234
+ const out = { decision: "block", reason };
235
+ _logEmit("block", reason.length, {
236
+ event: eventName ?? "unknown",
237
+ blockReason: reason,
238
+ mechanism: "topLevelDecision",
239
+ });
240
+ process.stdout.write(JSON.stringify(out) + "\n");
241
+ }
242
+
243
+ /**
244
+ * Respond to a PermissionRequest with allow/deny.
245
+ * Only works for PermissionRequest hooks.
246
+ */
247
+ export function emitPermissionDecision(
248
+ behavior: "allow" | "deny",
249
+ opts?: { message?: string; updatedInput?: Record<string, unknown>; updatedPermissions?: Record<string, unknown> },
250
+ ): void {
251
+ const out: PermissionRequestOutput = {
252
+ decision: {
253
+ behavior,
254
+ ...(opts?.message ? { message: opts.message } : {}),
255
+ ...(opts?.updatedInput ? { updatedInput: opts.updatedInput } : {}),
256
+ ...(opts?.updatedPermissions ? { updatedPermissions: opts.updatedPermissions } : {}),
257
+ },
258
+ };
259
+ _logEmit("block", 0, {
260
+ event: _lastHookEvent ?? "unknown",
261
+ blockReason: `permission:${behavior}`,
262
+ mechanism: "permissionRequest",
263
+ });
264
+ process.stdout.write(JSON.stringify(out) + "\n");
265
+ }
266
+
267
+ /**
268
+ * Unified block dispatcher — auto-detects the correct blocking mechanism
269
+ * based on the current hook event type.
270
+ *
271
+ * PreToolUse → permissionDecision: "deny" (via emitContextAndBlock)
272
+ * UserPromptSubmit → top-level decision: "block" (via emitBlockPrompt)
273
+ * PostToolUse/PostToolUseFailure → exit(2) + stderr (via emitBlockViaExit)
274
+ * Stop/SubagentStop → top-level { decision: "block", reason } (via emitBlockTopLevel)
275
+ * PermissionRequest → decision: { behavior: "deny" } (via emitPermissionDecision)
276
+ * SessionStart/Notification/SubagentStart/SessionEnd/etc. → warn and no-op
277
+ *
278
+ * This is the RECOMMENDED universal blocking API. Hook authors should use
279
+ * emitBlock() and let the library handle event-specific dispatch.
280
+ */
281
+ export function emitBlock(reason: string, context?: string): void {
282
+ const event = _lastHookEvent;
283
+ switch (event) {
284
+ case "PermissionRequest":
285
+ emitPermissionDecision("deny", { message: reason });
286
+ break;
287
+ case "PostToolUse":
288
+ case "PostToolUseFailure":
289
+ emitBlockViaExit(reason, context);
290
+ break;
291
+ case "PreToolUse":
292
+ emitContextAndBlock(context ?? reason, reason);
293
+ break;
294
+ case "Stop":
295
+ case "SubagentStop":
296
+ emitBlockTopLevel(reason);
304
297
  break;
305
- }
306
- }
307
- }
308
-
309
- /**
310
- * Auto-detect template origin from the hook script path.
311
- */
312
- function detectTemplate(scriptPath = ""): string {
313
- const p = (scriptPath || (process.argv[1] ?? "")).replaceAll('\\', "/");
314
- if (p.includes("/_shared/hooks/") || p.startsWith("_shared/hooks/")) {
315
- return "shared";
316
- }
317
- const match = p.match(/_([a-z][a-z0-9-]*)\/hooks\//);
318
- if (match?.[1]) return match[1]; // e.g., "cc-native"
319
- return "unknown";
320
- }
321
-
322
- /**
323
- * Parse context window from hook input.
324
- * Returns [tokensUsed, maxTokens] or [null, null].
325
- * See SPEC.md §5.9
326
- */
327
- export function parseContextWindow(
328
- hookInput: HookInput,
329
- ): [number | null, number | null] {
330
- const contextWindow = hookInput.context_window;
331
- if (!contextWindow) return [null, null];
332
-
333
- const currentUsage = contextWindow.current_usage;
334
- if (!currentUsage) return [null, null];
335
-
336
- const cacheRead = currentUsage.cache_read_input_tokens ?? 0;
337
- const inputTokens = currentUsage.input_tokens ?? 0;
338
- const cacheCreation = currentUsage.cache_creation_input_tokens ?? 0;
339
- const outputTokens = currentUsage.output_tokens ?? 0;
340
-
341
- const contentTokens = cacheRead + inputTokens + cacheCreation + outputTokens;
342
- const tokensUsed = contentTokens + CONTEXT_BASELINE_TOKENS;
343
- const maxTokens = contextWindow.context_window_size ?? DEFAULT_CONTEXT_WINDOW_SIZE;
344
-
345
- return [tokensUsed, maxTokens];
346
- }
347
-
348
- /**
349
- * Get context percentage remaining with fallback.
350
- * Returns [percentRemaining, tokensUsed, maxTokens] or [null, null, null].
351
- * See SPEC.md §5.9
352
- */
353
- export function getContextPercentRemaining(
354
- hookInput: HookInput,
355
- ): [number | null, number | null, number | null] {
356
- const [tokensUsed, maxTokens] = parseContextWindow(hookInput);
357
-
358
- if (tokensUsed !== null && maxTokens !== null && maxTokens > 0) {
359
- const remaining = maxTokens - tokensUsed;
360
- const percentRemaining = Math.max(
361
- 0,
362
- Math.min(100, Math.round((remaining / maxTokens) * 100)),
363
- );
364
- return [percentRemaining, tokensUsed, maxTokens];
365
- }
366
-
367
- // Source 2: context.json fallback (written by status_line.py)
368
- try {
369
- const sessionId = hookInput.session_id;
370
- if (sessionId) {
371
- const projectRoot = getProjectRoot(hookInput.cwd);
372
- const context = getContextBySessionId(sessionId, projectRoot);
373
- if (context?.last_session?.context_remaining_pct !== undefined) {
374
- return [context.last_session.context_remaining_pct, null, null];
375
- }
376
- }
377
- } catch {
378
- // Fallback failed — degrade gracefully
379
- }
380
-
381
- return [null, null, null];
382
- }
383
-
384
- /**
385
- * Read stdin early and extract session_id + event metadata.
386
- * Stashes parsed input for loadHookInput() to consume later.
387
- */
388
- function _earlyReadInput(prefetchedInput?: Record<string, any>): void {
389
- if (prefetchedInput !== undefined) {
390
- _prefetchedInput = prefetchedInput;
391
- }
392
-
393
- // If we already have prefetched input, extract metadata from it
394
- if (_prefetchedInput && typeof _prefetchedInput === "object") {
395
- _lastHookEvent = _prefetchedInput.hook_event_name ?? null;
396
- _lastToolName = _prefetchedInput.tool_name ?? null;
397
- if (_prefetchedInput.session_id) {
398
- setSessionId(_prefetchedInput.session_id);
399
- }
400
- return;
401
- }
402
-
403
- // Read stdin now so HOOK_START can include sid
404
- try {
405
- const inputData = fs.readFileSync(0, "utf-8").trim();
406
- if (inputData) {
407
- const parsed = JSON.parse(inputData);
408
- if (parsed && typeof parsed === "object") {
409
- _prefetchedInput = parsed;
410
- _lastHookEvent = parsed.hook_event_name ?? null;
411
- _lastToolName = parsed.tool_name ?? null;
412
- if (parsed.session_id) {
413
- setSessionId(parsed.session_id);
414
- }
415
- }
416
- }
417
- } catch {
418
- // Non-fatal loadHookInput will return null
419
- }
420
- }
421
-
422
- /**
423
- * Standard hook entry point with lifecycle logging.
424
- * See SPEC.md §5.7
425
- */
426
- export function runHook(
427
- mainFunc: () => number | void,
428
- hookName = "unknown",
429
- prefetchedInput?: Record<string, any>,
430
- ): never {
431
- _earlyReadInput(prefetchedInput);
432
- _cachedHookName = hookName;
433
-
434
- const startTime = performance.now();
435
- const template = detectTemplate();
436
- const event = _lastHookEvent ?? "unknown";
437
- const tool = _lastToolName;
438
-
439
- const startData: Record<string, any> = {
440
- lifecycle: "start",
441
- template,
442
- event,
443
- };
444
- if (tool) startData.tool = tool;
445
- hookLog("info", hookName, "HOOK_START", { data: startData });
446
-
447
- let exitCode = 0;
448
- let status = "success";
449
- let errorInfo: [Error, string] | null = null;
450
-
451
- try {
452
- const result = mainFunc();
453
- exitCode = typeof result === "number" ? result : 0;
454
- status = exitCode !== 0 ? "blocked" : "success";
455
- } catch (error: any) {
456
- if (error instanceof Error && error.message.startsWith("SystemExit:")) {
457
- const code = parseInt(error.message.slice(11), 10);
458
- exitCode = isNaN(code) ? (error.message.slice(11) ? 1 : 0) : code;
459
- status = exitCode !== 0 ? "blocked" : "success";
460
- } else {
461
- exitCode = 0; // Non-blocking
462
- status = "error";
463
- const stack = error instanceof Error ? error.stack ?? "" : "";
464
- errorInfo = [error instanceof Error ? error : new Error(String(error)), stack];
465
- }
466
- }
467
-
468
- _emitHookEnd(hookName, startTime, exitCode, status, errorInfo, startData, event, tool, template);
469
- process.exit(exitCode);
470
- }
471
-
472
- /**
473
- * Async variant of runHook for hooks that need await (e.g., AI inference).
474
- * Provides identical structured JSONL lifecycle logging as runHook.
475
- * See SPEC.md §5.7
476
- */
477
- export function runHookAsync(
478
- mainFunc: () => Promise<number | void>,
479
- hookName = "unknown",
480
- prefetchedInput?: Record<string, any>,
481
- ): void {
482
- _earlyReadInput(prefetchedInput);
483
- _cachedHookName = hookName;
484
-
485
- const startTime = performance.now();
486
- const template = detectTemplate();
487
- const event = _lastHookEvent ?? "unknown";
488
- const tool = _lastToolName;
489
-
490
- const startData: Record<string, any> = {
491
- lifecycle: "start",
492
- template,
493
- event,
494
- };
495
- if (tool) startData.tool = tool;
496
- hookLog("info", hookName, "HOOK_START", { data: startData });
497
-
498
- mainFunc()
499
- .then((result) => {
500
- const exitCode = typeof result === "number" ? result : 0;
501
- _emitHookEnd(hookName, startTime, exitCode, exitCode !== 0 ? "blocked" : "success", null, startData, event, tool, template);
502
- _drainAndExit(exitCode);
503
- })
504
- .catch((error: any) => {
505
- let exitCode = 0;
506
- let status = "error";
507
- let errorInfo: [Error, string] | null = null;
508
-
509
- if (error instanceof Error && error.message.startsWith("SystemExit:")) {
510
- const code = parseInt(error.message.slice(11), 10);
511
- exitCode = isNaN(code) ? (error.message.slice(11) ? 1 : 0) : code;
512
- status = exitCode !== 0 ? "blocked" : "success";
513
- } else {
514
- exitCode = 0; // Non-blocking (fail open)
515
- const stack = error instanceof Error ? error.stack ?? "" : "";
516
- errorInfo = [error instanceof Error ? error : new Error(String(error)), stack];
517
- }
518
-
519
- _emitHookEnd(hookName, startTime, exitCode, status, errorInfo, startData, event, tool, template);
520
- _drainAndExit(exitCode);
521
- });
522
- }
523
-
524
- /** Shared HOOK_END logic for runHook and runHookAsync */
525
- function _emitHookEnd(
526
- hookName: string,
527
- startTime: number,
528
- exitCode: number,
529
- status: string,
530
- errorInfo: [Error, string] | null,
531
- startData: Record<string, any>,
532
- event: string,
533
- tool: string | null,
534
- template: string,
535
- ): void {
536
- // Retroactive HOOK_START to per-context log (context_path resolved after main runs)
537
- const resolvedAfter = _getContextPath();
538
- if (resolvedAfter && fs.existsSync(resolvedAfter)) {
539
- hookLog("info", hookName, "HOOK_START", { data: startData });
540
- }
541
-
542
- const durationMs = Math.round((performance.now() - startTime) * 10) / 10;
543
- const endEvent = _lastHookEvent ?? event;
544
- const endTool = _lastToolName ?? tool;
545
- const endData: Record<string, any> = {
546
- lifecycle: "end",
547
- status,
548
- duration_ms: durationMs,
549
- exit_code: exitCode,
550
- template,
551
- event: endEvent,
552
- };
553
- if (endTool) endData.tool = endTool;
554
-
555
- if (errorInfo) {
556
- const [err, tb] = errorInfo;
557
- endData.error_type = err.constructor.name;
558
- hookLog("error", hookName, `[${endEvent}] ${err.constructor.name}: ${String(err).replaceAll(/[\n\r]/g, " ").slice(0, 200)}`, { traceback_str: tb });
559
- hookLog("error", hookName, `HOOK_END: ${err}`, { data: endData, traceback_str: tb });
560
- } else if (status === "blocked") {
561
- hookLog("warn", hookName, "HOOK_END", { data: endData });
562
- } else {
563
- hookLog("info", hookName, "HOOK_END", { data: endData });
564
- }
565
- }
566
-
567
- /**
568
- * Drain stdout before exiting to ensure pipe consumers receive all data.
569
- * On Windows, stdout to a pipe is fully buffered — process.exit() can
570
- * discard unflushed data. This waits for the write buffer to drain.
571
- */
572
- function _drainAndExit(code: number): void {
573
- // If stdout is already finished or not writable, exit immediately
574
- if (!process.stdout.writable || process.stdout.writableFinished) {
575
- process.exit(code);
576
- }
577
-
578
- // Attempt to end stdout and wait for drain
579
- const timeout = setTimeout(() => process.exit(code), 1000); // safety fallback
580
- process.stdout.end(() => {
581
- clearTimeout(timeout);
582
- process.exit(code);
583
- });
584
- }
298
+ case "UserPromptSubmit":
299
+ emitBlockPrompt(reason, context);
300
+ break;
301
+ default: {
302
+ logWarn(_cachedHookName ?? "unknown",
303
+ `emitBlock() called from ${event ?? "unknown"} no blocking mechanism exists for this event type, ignoring`);
304
+ break;
305
+ }
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Auto-detect template origin from the hook script path.
311
+ */
312
+ function detectTemplate(scriptPath = ""): string {
313
+ const p = (scriptPath || (process.argv[1] ?? "")).replaceAll('\\', "/");
314
+ if (p.includes("/_shared/hooks/") || p.startsWith("_shared/hooks/")) {
315
+ return "shared";
316
+ }
317
+ const match = p.match(/_([a-z][a-z0-9-]*)\/hooks\//);
318
+ if (match?.[1]) return match[1]; // e.g., "cc-native"
319
+ return "unknown";
320
+ }
321
+
322
+ /**
323
+ * Parse context window from hook input.
324
+ * Returns [tokensUsed, maxTokens] or [null, null].
325
+ * See SPEC.md §5.9
326
+ */
327
+ export function parseContextWindow(
328
+ hookInput: HookInput,
329
+ ): [number | null, number | null] {
330
+ const contextWindow = hookInput.context_window;
331
+ if (!contextWindow) return [null, null];
332
+
333
+ const currentUsage = contextWindow.current_usage;
334
+ if (!currentUsage) return [null, null];
335
+
336
+ const cacheRead = currentUsage.cache_read_input_tokens ?? 0;
337
+ const inputTokens = currentUsage.input_tokens ?? 0;
338
+ const cacheCreation = currentUsage.cache_creation_input_tokens ?? 0;
339
+ const outputTokens = currentUsage.output_tokens ?? 0;
340
+
341
+ const contentTokens = cacheRead + inputTokens + cacheCreation + outputTokens;
342
+ const tokensUsed = contentTokens + CONTEXT_BASELINE_TOKENS;
343
+ const maxTokens = contextWindow.context_window_size ?? DEFAULT_CONTEXT_WINDOW_SIZE;
344
+
345
+ return [tokensUsed, maxTokens];
346
+ }
347
+
348
+ /**
349
+ * Get context percentage remaining with fallback.
350
+ * Returns [percentRemaining, tokensUsed, maxTokens] or [null, null, null].
351
+ * See SPEC.md §5.9
352
+ */
353
+ export function getContextPercentRemaining(
354
+ hookInput: HookInput,
355
+ ): [number | null, number | null, number | null] {
356
+ const [tokensUsed, maxTokens] = parseContextWindow(hookInput);
357
+
358
+ if (tokensUsed !== null && maxTokens !== null && maxTokens > 0) {
359
+ const remaining = maxTokens - tokensUsed;
360
+ const percentRemaining = Math.max(
361
+ 0,
362
+ Math.min(100, Math.round((remaining / maxTokens) * 100)),
363
+ );
364
+ return [percentRemaining, tokensUsed, maxTokens];
365
+ }
366
+
367
+ // Source 2: context.json fallback (written by status_line.py)
368
+ try {
369
+ const sessionId = hookInput.session_id;
370
+ if (sessionId) {
371
+ const projectRoot = getProjectRoot(hookInput.cwd);
372
+ const context = getContextBySessionId(sessionId, projectRoot);
373
+ if (context?.last_session?.context_remaining_pct !== undefined) {
374
+ return [context.last_session.context_remaining_pct, null, null];
375
+ }
376
+ }
377
+ } catch {
378
+ // Fallback failed degrade gracefully
379
+ }
380
+
381
+ return [null, null, null];
382
+ }
383
+
384
+ /**
385
+ * Read stdin early and extract session_id + event metadata.
386
+ * Stashes parsed input for loadHookInput() to consume later.
387
+ */
388
+ function _earlyReadInput(prefetchedInput?: Record<string, any>): void {
389
+ if (prefetchedInput !== undefined) {
390
+ _prefetchedInput = prefetchedInput;
391
+ }
392
+
393
+ // If we already have prefetched input, extract metadata from it
394
+ if (_prefetchedInput && typeof _prefetchedInput === "object") {
395
+ _lastHookEvent = _prefetchedInput.hook_event_name ?? null;
396
+ _lastToolName = _prefetchedInput.tool_name ?? null;
397
+ if (_prefetchedInput.session_id) {
398
+ setSessionId(_prefetchedInput.session_id);
399
+ }
400
+ return;
401
+ }
402
+
403
+ // Read stdin now so HOOK_START can include sid
404
+ try {
405
+ const inputData = fs.readFileSync(0, "utf-8").trim();
406
+ if (inputData) {
407
+ const parsed = JSON.parse(inputData);
408
+ if (parsed && typeof parsed === "object") {
409
+ _prefetchedInput = parsed;
410
+ _lastHookEvent = parsed.hook_event_name ?? null;
411
+ _lastToolName = parsed.tool_name ?? null;
412
+ if (parsed.session_id) {
413
+ setSessionId(parsed.session_id);
414
+ }
415
+ }
416
+ }
417
+ } catch {
418
+ // Non-fatal — loadHookInput will return null
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Standard hook entry point with lifecycle logging.
424
+ * See SPEC.md §5.7
425
+ */
426
+ export function runHook(
427
+ mainFunc: () => number | void,
428
+ hookName = "unknown",
429
+ prefetchedInput?: Record<string, any>,
430
+ ): never {
431
+ _earlyReadInput(prefetchedInput);
432
+ _cachedHookName = hookName;
433
+
434
+ const startTime = performance.now();
435
+ const template = detectTemplate();
436
+ const event = _lastHookEvent ?? "unknown";
437
+ const tool = _lastToolName;
438
+
439
+ const startData: Record<string, any> = {
440
+ lifecycle: "start",
441
+ template,
442
+ event,
443
+ };
444
+ if (tool) startData.tool = tool;
445
+ hookLog("info", hookName, "HOOK_START", { data: startData });
446
+
447
+ let exitCode = 0;
448
+ let status = "success";
449
+ let errorInfo: [Error, string] | null = null;
450
+
451
+ try {
452
+ const result = mainFunc();
453
+ exitCode = typeof result === "number" ? result : 0;
454
+ status = exitCode !== 0 ? "blocked" : "success";
455
+ } catch (error: any) {
456
+ if (error instanceof Error && error.message.startsWith("SystemExit:")) {
457
+ const code = parseInt(error.message.slice(11), 10);
458
+ exitCode = isNaN(code) ? (error.message.slice(11) ? 1 : 0) : code;
459
+ status = exitCode !== 0 ? "blocked" : "success";
460
+ } else {
461
+ exitCode = 0; // Non-blocking
462
+ status = "error";
463
+ const stack = error instanceof Error ? error.stack ?? "" : "";
464
+ errorInfo = [error instanceof Error ? error : new Error(String(error)), stack];
465
+ }
466
+ }
467
+
468
+ _emitHookEnd(hookName, startTime, exitCode, status, errorInfo, startData, event, tool, template);
469
+ process.exit(exitCode);
470
+ }
471
+
472
+ /**
473
+ * Async variant of runHook for hooks that need await (e.g., AI inference).
474
+ * Provides identical structured JSONL lifecycle logging as runHook.
475
+ * See SPEC.md §5.7
476
+ */
477
+ export function runHookAsync(
478
+ mainFunc: () => Promise<number | void>,
479
+ hookName = "unknown",
480
+ prefetchedInput?: Record<string, any>,
481
+ ): void {
482
+ _earlyReadInput(prefetchedInput);
483
+ _cachedHookName = hookName;
484
+
485
+ const startTime = performance.now();
486
+ const template = detectTemplate();
487
+ const event = _lastHookEvent ?? "unknown";
488
+ const tool = _lastToolName;
489
+
490
+ const startData: Record<string, any> = {
491
+ lifecycle: "start",
492
+ template,
493
+ event,
494
+ };
495
+ if (tool) startData.tool = tool;
496
+ hookLog("info", hookName, "HOOK_START", { data: startData });
497
+
498
+ mainFunc()
499
+ .then((result) => {
500
+ const exitCode = typeof result === "number" ? result : 0;
501
+ _emitHookEnd(hookName, startTime, exitCode, exitCode !== 0 ? "blocked" : "success", null, startData, event, tool, template);
502
+ _drainAndExit(exitCode);
503
+ })
504
+ .catch((error: any) => {
505
+ let exitCode = 0;
506
+ let status = "error";
507
+ let errorInfo: [Error, string] | null = null;
508
+
509
+ if (error instanceof Error && error.message.startsWith("SystemExit:")) {
510
+ const code = parseInt(error.message.slice(11), 10);
511
+ exitCode = isNaN(code) ? (error.message.slice(11) ? 1 : 0) : code;
512
+ status = exitCode !== 0 ? "blocked" : "success";
513
+ } else {
514
+ exitCode = 0; // Non-blocking (fail open)
515
+ const stack = error instanceof Error ? error.stack ?? "" : "";
516
+ errorInfo = [error instanceof Error ? error : new Error(String(error)), stack];
517
+ }
518
+
519
+ _emitHookEnd(hookName, startTime, exitCode, status, errorInfo, startData, event, tool, template);
520
+ _drainAndExit(exitCode);
521
+ });
522
+ }
523
+
524
+ /** Shared HOOK_END logic for runHook and runHookAsync */
525
+ function _emitHookEnd(
526
+ hookName: string,
527
+ startTime: number,
528
+ exitCode: number,
529
+ status: string,
530
+ errorInfo: [Error, string] | null,
531
+ startData: Record<string, any>,
532
+ event: string,
533
+ tool: string | null,
534
+ template: string,
535
+ ): void {
536
+ // Retroactive HOOK_START to per-context log (context_path resolved after main runs)
537
+ const resolvedAfter = _getContextPath();
538
+ if (resolvedAfter && fs.existsSync(resolvedAfter)) {
539
+ hookLog("info", hookName, "HOOK_START", { data: startData });
540
+ }
541
+
542
+ const durationMs = Math.round((performance.now() - startTime) * 10) / 10;
543
+ const endEvent = _lastHookEvent ?? event;
544
+ const endTool = _lastToolName ?? tool;
545
+ const endData: Record<string, any> = {
546
+ lifecycle: "end",
547
+ status,
548
+ duration_ms: durationMs,
549
+ exit_code: exitCode,
550
+ template,
551
+ event: endEvent,
552
+ };
553
+ if (endTool) endData.tool = endTool;
554
+
555
+ if (errorInfo) {
556
+ const [err, tb] = errorInfo;
557
+ endData.error_type = err.constructor.name;
558
+ hookLog("error", hookName, `[${endEvent}] ${err.constructor.name}: ${String(err).replaceAll(/[\n\r]/g, " ").slice(0, 200)}`, { traceback_str: tb });
559
+ hookLog("error", hookName, `HOOK_END: ${err}`, { data: endData, traceback_str: tb });
560
+ } else if (status === "blocked") {
561
+ hookLog("warn", hookName, "HOOK_END", { data: endData });
562
+ } else {
563
+ hookLog("info", hookName, "HOOK_END", { data: endData });
564
+ }
565
+ }
566
+
567
+ /**
568
+ * Drain stdout before exiting to ensure pipe consumers receive all data.
569
+ * On Windows, stdout to a pipe is fully buffered — process.exit() can
570
+ * discard unflushed data. This waits for the write buffer to drain.
571
+ */
572
+ function _drainAndExit(code: number): void {
573
+ // If stdout is already finished or not writable, exit immediately
574
+ if (!process.stdout.writable || process.stdout.writableFinished) {
575
+ process.exit(code);
576
+ }
577
+
578
+ // Attempt to end stdout and wait for drain
579
+ const timeout = setTimeout(() => process.exit(code), 1000); // safety fallback
580
+ process.stdout.end(() => {
581
+ clearTimeout(timeout);
582
+ process.exit(code);
583
+ });
584
+ }
585
585
 
586
586
  export {logInfo, logError, logBlocking, logHookError, logDiagnostic, setContextPath, hookLog, logDebug, logWarn} from "./logger.js";