aiwcli 0.11.1 → 0.12.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 (117) hide show
  1. package/dist/commands/clear.d.ts +8 -0
  2. package/dist/commands/clear.js +86 -0
  3. package/dist/lib/bmad-installer.d.ts +2 -27
  4. package/dist/lib/bmad-installer.js +3 -43
  5. package/dist/lib/claude-settings-types.d.ts +2 -1
  6. package/dist/lib/env-compat.d.ts +0 -8
  7. package/dist/lib/env-compat.js +0 -12
  8. package/dist/lib/git/index.d.ts +0 -1
  9. package/dist/lib/gitignore-manager.d.ts +0 -2
  10. package/dist/lib/gitignore-manager.js +1 -1
  11. package/dist/lib/hooks-merger.d.ts +1 -15
  12. package/dist/lib/hooks-merger.js +1 -1
  13. package/dist/lib/index.d.ts +3 -7
  14. package/dist/lib/index.js +3 -11
  15. package/dist/lib/output.d.ts +2 -1
  16. package/dist/lib/settings-hierarchy.d.ts +1 -13
  17. package/dist/lib/settings-hierarchy.js +1 -1
  18. package/dist/lib/template-installer.d.ts +5 -9
  19. package/dist/lib/template-installer.js +3 -13
  20. package/dist/lib/template-linter.d.ts +3 -10
  21. package/dist/lib/template-linter.js +2 -2
  22. package/dist/lib/template-resolver.d.ts +6 -0
  23. package/dist/lib/template-resolver.js +10 -0
  24. package/dist/lib/template-settings-reconstructor.d.ts +1 -1
  25. package/dist/lib/template-settings-reconstructor.js +17 -24
  26. package/dist/lib/terminal.d.ts +3 -14
  27. package/dist/lib/terminal.js +0 -4
  28. package/dist/lib/version.d.ts +2 -11
  29. package/dist/lib/version.js +3 -3
  30. package/dist/lib/windsurf-hooks-merger.d.ts +1 -15
  31. package/dist/lib/windsurf-hooks-merger.js +1 -1
  32. package/dist/templates/_shared/.codex/workflows/handoff.md +1 -1
  33. package/dist/templates/_shared/.windsurf/workflows/handoff.md +1 -1
  34. package/dist/templates/_shared/hooks-ts/session_end.ts +75 -4
  35. package/dist/templates/_shared/hooks-ts/session_start.ts +11 -13
  36. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +6 -8
  37. package/dist/templates/_shared/lib-ts/CLAUDE.md +56 -7
  38. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +176 -29
  39. package/dist/templates/_shared/lib-ts/base/logger.ts +1 -1
  40. package/dist/templates/_shared/lib-ts/base/state-io.ts +11 -2
  41. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +181 -165
  42. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +14 -13
  43. package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +3 -2
  44. package/dist/templates/_shared/lib-ts/package.json +1 -2
  45. package/dist/templates/_shared/lib-ts/templates/plan-context.ts +27 -34
  46. package/dist/templates/_shared/lib-ts/types.ts +17 -2
  47. package/dist/templates/_shared/scripts/resume_handoff.ts +4 -4
  48. package/dist/templates/_shared/scripts/save_handoff.ts +7 -7
  49. package/dist/templates/_shared/scripts/status_line.ts +104 -71
  50. package/dist/templates/_shared/workflows/handoff.md +1 -1
  51. package/dist/templates/cc-native/.claude/settings.json +182 -175
  52. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +23 -1
  53. package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -0
  54. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +6 -1
  55. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +142 -111
  56. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +54 -0
  57. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +52 -0
  58. package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -0
  59. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +19 -19
  60. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +6 -5
  61. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +114 -83
  62. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +107 -10
  63. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +1 -1
  64. package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +6 -2
  65. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +0 -4
  66. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +40 -219
  67. package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -0
  68. package/dist/templates/cc-native/_cc-native/lib-ts/plan-questions.ts +102 -0
  69. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +26 -227
  70. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/base/base-agent.ts +217 -0
  71. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +4 -2
  72. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/claude-agent.ts +65 -0
  73. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/codex-agent.ts +185 -0
  74. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/gemini-agent.ts +39 -0
  75. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +195 -0
  76. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/schemas.ts +201 -0
  77. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +2 -2
  78. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +17 -16
  79. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +13 -108
  80. package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +3 -3
  81. package/dist/templates/cc-native/_cc-native/plan-review.config.json +2 -14
  82. package/oclif.manifest.json +1 -1
  83. package/package.json +1 -2
  84. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +0 -119
  85. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +0 -130
  86. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +0 -107
  87. /package/dist/templates/cc-native/_cc-native/agents/{ARCH-EVOLUTION.md → plan-review/ARCH-EVOLUTION.md} +0 -0
  88. /package/dist/templates/cc-native/_cc-native/agents/{ARCH-PATTERNS.md → plan-review/ARCH-PATTERNS.md} +0 -0
  89. /package/dist/templates/cc-native/_cc-native/agents/{ARCH-STRUCTURE.md → plan-review/ARCH-STRUCTURE.md} +0 -0
  90. /package/dist/templates/cc-native/_cc-native/agents/{ASSUMPTION-TRACER.md → plan-review/ASSUMPTION-TRACER.md} +0 -0
  91. /package/dist/templates/cc-native/_cc-native/agents/{CLARITY-AUDITOR.md → plan-review/CLARITY-AUDITOR.md} +0 -0
  92. /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-FEASIBILITY.md → plan-review/COMPLETENESS-FEASIBILITY.md} +0 -0
  93. /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-GAPS.md → plan-review/COMPLETENESS-GAPS.md} +0 -0
  94. /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-ORDERING.md → plan-review/COMPLETENESS-ORDERING.md} +0 -0
  95. /package/dist/templates/cc-native/_cc-native/agents/{CONSTRAINT-VALIDATOR.md → plan-review/CONSTRAINT-VALIDATOR.md} +0 -0
  96. /package/dist/templates/cc-native/_cc-native/agents/{DESIGN-ADR-VALIDATOR.md → plan-review/DESIGN-ADR-VALIDATOR.md} +0 -0
  97. /package/dist/templates/cc-native/_cc-native/agents/{DESIGN-SCALE-MATCHER.md → plan-review/DESIGN-SCALE-MATCHER.md} +0 -0
  98. /package/dist/templates/cc-native/_cc-native/agents/{DEVILS-ADVOCATE.md → plan-review/DEVILS-ADVOCATE.md} +0 -0
  99. /package/dist/templates/cc-native/_cc-native/agents/{DOCUMENTATION-PHILOSOPHY.md → plan-review/DOCUMENTATION-PHILOSOPHY.md} +0 -0
  100. /package/dist/templates/cc-native/_cc-native/agents/{HANDOFF-READINESS.md → plan-review/HANDOFF-READINESS.md} +0 -0
  101. /package/dist/templates/cc-native/_cc-native/agents/{HIDDEN-COMPLEXITY.md → plan-review/HIDDEN-COMPLEXITY.md} +0 -0
  102. /package/dist/templates/cc-native/_cc-native/agents/{INCREMENTAL-DELIVERY.md → plan-review/INCREMENTAL-DELIVERY.md} +0 -0
  103. /package/dist/templates/cc-native/_cc-native/agents/{RISK-DEPENDENCY.md → plan-review/RISK-DEPENDENCY.md} +0 -0
  104. /package/dist/templates/cc-native/_cc-native/agents/{RISK-FMEA.md → plan-review/RISK-FMEA.md} +0 -0
  105. /package/dist/templates/cc-native/_cc-native/agents/{RISK-PREMORTEM.md → plan-review/RISK-PREMORTEM.md} +0 -0
  106. /package/dist/templates/cc-native/_cc-native/agents/{RISK-REVERSIBILITY.md → plan-review/RISK-REVERSIBILITY.md} +0 -0
  107. /package/dist/templates/cc-native/_cc-native/agents/{SCOPE-BOUNDARY.md → plan-review/SCOPE-BOUNDARY.md} +0 -0
  108. /package/dist/templates/cc-native/_cc-native/agents/{SIMPLICITY-GUARDIAN.md → plan-review/SIMPLICITY-GUARDIAN.md} +0 -0
  109. /package/dist/templates/cc-native/_cc-native/agents/{SKEPTIC.md → plan-review/SKEPTIC.md} +0 -0
  110. /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-BEHAVIOR-AUDITOR.md → plan-review/TESTDRIVEN-BEHAVIOR-AUDITOR.md} +0 -0
  111. /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-CHARACTERIZATION.md → plan-review/TESTDRIVEN-CHARACTERIZATION.md} +0 -0
  112. /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-FIRST-VALIDATOR.md → plan-review/TESTDRIVEN-FIRST-VALIDATOR.md} +0 -0
  113. /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-PYRAMID-ANALYZER.md → plan-review/TESTDRIVEN-PYRAMID-ANALYZER.md} +0 -0
  114. /package/dist/templates/cc-native/_cc-native/agents/{TRADEOFF-COSTS.md → plan-review/TRADEOFF-COSTS.md} +0 -0
  115. /package/dist/templates/cc-native/_cc-native/agents/{TRADEOFF-STAKEHOLDERS.md → plan-review/TRADEOFF-STAKEHOLDERS.md} +0 -0
  116. /package/dist/templates/cc-native/_cc-native/agents/{VERIFY-COVERAGE.md → plan-review/VERIFY-COVERAGE.md} +0 -0
  117. /package/dist/templates/cc-native/_cc-native/agents/{VERIFY-STRENGTH.md → plan-review/VERIFY-STRENGTH.md} +0 -0
@@ -4,27 +4,27 @@
4
4
  * See SPEC.md §5
5
5
  */
6
6
 
7
- import * as fs from "node:fs";
7
+ import * as fs from "node:fs";
8
8
 
9
9
  import { getProjectRoot } from "./constants.js";
10
- import { getContextPath as _getContextPath, hookLog, logDebug, setSessionId } from "./logger.js";
10
+ import { logDebug, logWarn, hookLog, setSessionId, getContextPath as _getContextPath } from "./logger.js";
11
11
  import { getContextBySessionId } from "../context/context-store.js";
12
- import type { HookInput, HookOutput } from "../types.js";
12
+ import type { HookInput, HookOutput, PermissionRequestOutput } from "../types.js";
13
13
 
14
14
  // Re-export logger functions for convenience (matches Python hook_utils re-exports)
15
-
15
+ export { setSessionId };
16
16
 
17
17
  // Context window baseline: tokens not visible in hook data §5.9
18
18
  export const CONTEXT_BASELINE_TOKENS = 22_600;
19
19
  export const DEFAULT_CONTEXT_WINDOW_SIZE = 200_000;
20
20
 
21
21
  // Event metadata stash — populated by loadHookInput(), read by runHook()
22
- let _lastHookEvent: null | string = null;
23
- let _lastToolName: null | string = null;
24
- let _cachedHookName: null | string = null;
22
+ let _lastHookEvent: string | null = null;
23
+ let _lastToolName: string | null = null;
24
+ let _cachedHookName: string | null = null;
25
25
 
26
26
  // Pre-fetched input stash
27
- let _prefetchedInput: null | Record<string, any> = null;
27
+ let _prefetchedInput: Record<string, any> | null = null;
28
28
 
29
29
  /**
30
30
  * Load and parse JSON from stdin (or return prefetched input if set).
@@ -39,13 +39,12 @@ export function loadHookInput(): HookInput | null {
39
39
  _lastHookEvent = result.hook_event_name ?? null;
40
40
  _lastToolName = result.tool_name ?? null;
41
41
  }
42
-
43
42
  return result as HookInput;
44
43
  }
45
44
 
46
45
  try {
47
46
  // Read entire stdin using fd 0 (cross-platform, works on Windows)
48
- const inputData = fs.readFileSync(0, "utf8").trim();
47
+ const inputData = fs.readFileSync(0, "utf-8").trim();
49
48
  if (!inputData) return null;
50
49
 
51
50
  const result = JSON.parse(inputData);
@@ -53,7 +52,6 @@ export function loadHookInput(): HookInput | null {
53
52
  _lastHookEvent = result.hook_event_name ?? null;
54
53
  _lastToolName = result.tool_name ?? null;
55
54
  }
56
-
57
55
  return result as HookInput;
58
56
  } catch {
59
57
  return null;
@@ -80,7 +78,7 @@ export function validateHookEvent(
80
78
  */
81
79
  export function getToolInput(
82
80
  payload: HookInput,
83
- ): null | Record<string, any> {
81
+ ): Record<string, any> | null {
84
82
  const toolInput = payload.tool_input;
85
83
  return toolInput && typeof toolInput === "object" ? toolInput : null;
86
84
  }
@@ -101,7 +99,6 @@ export function checkSkipPersistence(
101
99
  logDebug(hookName, "Skipping persistence (skip_persistence flag set)");
102
100
  return true;
103
101
  }
104
-
105
102
  return false;
106
103
  }
107
104
 
@@ -109,10 +106,23 @@ export function checkSkipPersistence(
109
106
  * Emit hookSpecificOutput with additionalContext to stdout.
110
107
  * hookEventName is required by Claude Code's Zod validator (discriminated union).
111
108
  * Auto-detected from stdin payload (set by loadHookInput/runHook).
109
+ *
110
+ * SubagentStop and Stop events use top-level systemMessage field instead of hookSpecificOutput.
112
111
  * See SPEC.md §5.5
113
112
  */
114
113
  export function emitContext(additionalContext: string): void {
115
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
116
126
  const out: HookOutput = {
117
127
  hookSpecificOutput: {
118
128
  ...(eventName ? { hookEventName: eventName } : {}),
@@ -120,7 +130,8 @@ export function emitContext(additionalContext: string): void {
120
130
  },
121
131
  };
122
132
  const json = JSON.stringify(out);
123
- _logEmit("context", additionalContext.length, { additionalContext });
133
+ const eventDesc = tool ? `${eventName}:${tool}` : eventName ?? "unknown";
134
+ _logEmit("context", additionalContext.length, { event: eventDesc, additionalContext });
124
135
  process.stdout.write(json + "\n");
125
136
  }
126
137
 
@@ -135,6 +146,12 @@ export function emitContextAndBlock(
135
146
  reason: string,
136
147
  ): void {
137
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;
138
155
  const out: HookOutput = {
139
156
  hookSpecificOutput: {
140
157
  ...(eventName ? { hookEventName: eventName } : {}),
@@ -144,19 +161,151 @@ export function emitContextAndBlock(
144
161
  },
145
162
  };
146
163
  const json = JSON.stringify(out);
147
- _logEmit("block", additionalContext.length, { additionalContext, blockReason: reason });
164
+ const eventDesc = tool ? `${eventName}:${tool}` : eventName ?? "unknown";
165
+ _logEmit("block", additionalContext.length, { event: eventDesc, additionalContext, blockReason: reason });
148
166
  process.stdout.write(json + "\n");
149
167
  }
150
168
 
151
- /** Log hook output (context or block) to hook-log.jsonl for visibility. */
152
- function _logEmit(type: "block" | "context", chars: number, payload: Record<string, any>): void {
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 {
153
171
  const hook = _cachedHookName ?? "unknown";
172
+ const event = payload.event ?? "unknown";
173
+ const mechanism = payload.mechanism ? ` via ${payload.mechanism}` : "";
154
174
  const msg = type === "block"
155
- ? `HOOK_OUTPUT [${type}] ${chars} chars, reason="${(payload.blockReason ?? "").slice(0, 80)}"`
156
- : `HOOK_OUTPUT [${type}] ${chars} chars`;
175
+ ? `HOOK_OUTPUT [${type}] ${event} ${chars} chars${mechanism}, reason="${(payload.blockReason ?? "").slice(0, 80)}"`
176
+ : `HOOK_OUTPUT [${type}] ${event} ${chars} chars`;
157
177
  hookLog("info", hook, msg, { data: payload });
158
178
  }
159
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`);
304
+ break;
305
+ }
306
+ }
307
+ }
308
+
160
309
  /**
161
310
  * Auto-detect template origin from the hook script path.
162
311
  */
@@ -165,7 +314,6 @@ function detectTemplate(scriptPath = ""): string {
165
314
  if (p.includes("/_shared/hooks/") || p.startsWith("_shared/hooks/")) {
166
315
  return "shared";
167
316
  }
168
-
169
317
  const match = p.match(/_([a-z][a-z0-9-]*)\/hooks\//);
170
318
  if (match?.[1]) return match[1]; // e.g., "cc-native"
171
319
  return "unknown";
@@ -178,7 +326,7 @@ function detectTemplate(scriptPath = ""): string {
178
326
  */
179
327
  export function parseContextWindow(
180
328
  hookInput: HookInput,
181
- ): [null | number, null | number] {
329
+ ): [number | null, number | null] {
182
330
  const contextWindow = hookInput.context_window;
183
331
  if (!contextWindow) return [null, null];
184
332
 
@@ -204,7 +352,7 @@ export function parseContextWindow(
204
352
  */
205
353
  export function getContextPercentRemaining(
206
354
  hookInput: HookInput,
207
- ): [null | number, null | number, null | number] {
355
+ ): [number | null, number | null, number | null] {
208
356
  const [tokensUsed, maxTokens] = parseContextWindow(hookInput);
209
357
 
210
358
  if (tokensUsed !== null && maxTokens !== null && maxTokens > 0) {
@@ -249,13 +397,12 @@ function _earlyReadInput(prefetchedInput?: Record<string, any>): void {
249
397
  if (_prefetchedInput.session_id) {
250
398
  setSessionId(_prefetchedInput.session_id);
251
399
  }
252
-
253
400
  return;
254
401
  }
255
402
 
256
403
  // Read stdin now so HOOK_START can include sid
257
404
  try {
258
- const inputData = fs.readFileSync(0, "utf8").trim();
405
+ const inputData = fs.readFileSync(0, "utf-8").trim();
259
406
  if (inputData) {
260
407
  const parsed = JSON.parse(inputData);
261
408
  if (parsed && typeof parsed === "object") {
@@ -304,7 +451,7 @@ export function runHook(
304
451
  try {
305
452
  const result = mainFunc();
306
453
  exitCode = typeof result === "number" ? result : 0;
307
- status = exitCode === 0 ? "success" : "blocked";
454
+ status = exitCode !== 0 ? "blocked" : "success";
308
455
  } catch (error: any) {
309
456
  if (error instanceof Error && error.message.startsWith("SystemExit:")) {
310
457
  const code = parseInt(error.message.slice(11), 10);
@@ -351,7 +498,7 @@ export function runHookAsync(
351
498
  mainFunc()
352
499
  .then((result) => {
353
500
  const exitCode = typeof result === "number" ? result : 0;
354
- _emitHookEnd(hookName, startTime, exitCode, exitCode === 0 ? "success" : "blocked", null, startData, event, tool, template);
501
+ _emitHookEnd(hookName, startTime, exitCode, exitCode !== 0 ? "blocked" : "success", null, startData, event, tool, template);
355
502
  _drainAndExit(exitCode);
356
503
  })
357
504
  .catch((error: any) => {
@@ -383,7 +530,7 @@ function _emitHookEnd(
383
530
  errorInfo: [Error, string] | null,
384
531
  startData: Record<string, any>,
385
532
  event: string,
386
- tool: null | string,
533
+ tool: string | null,
387
534
  template: string,
388
535
  ): void {
389
536
  // Retroactive HOOK_START to per-context log (context_path resolved after main runs)
@@ -435,5 +582,5 @@ function _drainAndExit(code: number): void {
435
582
  process.exit(code);
436
583
  });
437
584
  }
438
-
439
- export {hookLog, logBlocking, logDebug, logDiagnostic, logError, logHookError, logInfo, logWarn, setContextPath, setSessionId} from "./logger.js";
585
+
586
+ export {logInfo, logError, logBlocking, logHookError, logDiagnostic, setContextPath, hookLog, logDebug, logWarn} from "./logger.js";
@@ -235,7 +235,7 @@ export function logHookError(
235
235
  tracebackStr = "",
236
236
  ): void {
237
237
  const errStr = typeof error === "string" ? error : String(error);
238
- const msg = errStr.replace(/[\n\r]/g, " ").slice(0, 200);
238
+ const msg = errStr.replaceAll(/[\n\r]/g, " ").slice(0, 200);
239
239
  const errType =
240
240
  typeof error === "object" && error !== null
241
241
  ? error.constructor.name
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import * as fs from "node:fs";
8
- import * as path from "node:path";
8
+ import * as path from "node:path";
9
9
 
10
10
  import { atomicWrite } from "./atomic-write.js";
11
11
  import { getContextDir } from "./constants.js";
@@ -30,7 +30,7 @@ export function toDict(state: ContextState): Record<string, unknown> {
30
30
  if (value !== null && value !== undefined) {
31
31
  result[key] = value;
32
32
  }
33
- }
33
+ }
34
34
 
35
35
  return result;
36
36
  }
@@ -112,5 +112,14 @@ export function dictToState(data: Record<string, any>): ContextState {
112
112
  if ("handoff_path" in data) state.handoff_path = data.handoff_path;
113
113
  if ("last_session" in data) state.last_session = data.last_session;
114
114
 
115
+ // Preserve method-specific extension data (e.g., cc_native) that isn't
116
+ // part of the core ContextState interface. Without this, round-trip
117
+ // read→write cycles silently drop extension fields.
118
+ for (const key of Object.keys(data)) {
119
+ if (!(key in state)) {
120
+ state[key] = data[key];
121
+ }
122
+ }
123
+
115
124
  return state as ContextState;
116
125
  }