aiwcli 0.10.3 → 0.11.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 (191) hide show
  1. package/bin/run.js +1 -1
  2. package/dist/commands/clear.js +28 -131
  3. package/dist/commands/init/index.js +3 -3
  4. package/dist/lib/gitignore-manager.d.ts +32 -0
  5. package/dist/lib/gitignore-manager.js +141 -2
  6. package/dist/templates/CLAUDE.md +8 -8
  7. package/dist/templates/_shared/.claude/commands/handoff-resume.md +64 -0
  8. package/dist/templates/_shared/.claude/commands/handoff.md +16 -10
  9. package/dist/templates/_shared/.claude/settings.json +7 -7
  10. package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -0
  11. package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -0
  12. package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -0
  13. package/dist/templates/_shared/hooks-ts/file-suggestion.ts +130 -0
  14. package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -0
  15. package/dist/templates/_shared/hooks-ts/session_end.ts +107 -0
  16. package/dist/templates/_shared/hooks-ts/session_start.ts +144 -0
  17. package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -0
  18. package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -0
  19. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +83 -0
  20. package/dist/templates/_shared/lib-ts/CLAUDE.md +318 -0
  21. package/dist/templates/_shared/lib-ts/base/atomic-write.ts +12 -12
  22. package/dist/templates/_shared/lib-ts/base/constants.ts +22 -15
  23. package/dist/templates/_shared/lib-ts/base/git-state.ts +1 -1
  24. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +129 -50
  25. package/dist/templates/_shared/lib-ts/base/inference.ts +28 -21
  26. package/dist/templates/_shared/lib-ts/base/logger.ts +15 -2
  27. package/dist/templates/_shared/lib-ts/base/state-io.ts +9 -7
  28. package/dist/templates/_shared/lib-ts/base/stop-words.ts +131 -131
  29. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +142 -0
  30. package/dist/templates/_shared/lib-ts/base/utils.ts +69 -69
  31. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +30 -24
  32. package/dist/templates/_shared/lib-ts/context/context-selector.ts +50 -32
  33. package/dist/templates/_shared/lib-ts/context/context-store.ts +76 -48
  34. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +43 -23
  35. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +10 -6
  36. package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +11 -10
  37. package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +158 -0
  38. package/dist/templates/_shared/lib-ts/templates/formatters.ts +6 -4
  39. package/dist/templates/_shared/lib-ts/types.ts +68 -55
  40. package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
  41. package/dist/templates/_shared/scripts/resume_handoff.ts +345 -0
  42. package/dist/templates/_shared/scripts/save_handoff.ts +3 -3
  43. package/dist/templates/_shared/scripts/status_line.ts +687 -0
  44. package/dist/templates/cc-native/.claude/settings.json +175 -185
  45. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
  46. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +0 -2
  47. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
  48. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
  49. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +1027 -0
  50. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
  51. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -0
  52. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +792 -0
  53. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
  54. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -0
  55. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
  56. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
  57. package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +115 -0
  58. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
  59. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +120 -0
  60. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -0
  61. package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
  62. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +250 -0
  63. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +275 -0
  64. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
  65. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +107 -0
  66. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
  67. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
  68. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +240 -0
  69. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
  70. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +385 -0
  71. package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
  72. package/dist/templates/cc-native/_cc-native/plan-review.config.json +14 -1
  73. package/oclif.manifest.json +1 -1
  74. package/package.json +2 -2
  75. package/dist/templates/_shared/hooks/__init__.py +0 -16
  76. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  77. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  78. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  79. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  80. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  81. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  82. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  83. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  84. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  85. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  86. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  87. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  88. package/dist/templates/_shared/hooks/archive_plan.py +0 -177
  89. package/dist/templates/_shared/hooks/context_monitor.py +0 -270
  90. package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
  91. package/dist/templates/_shared/hooks/pre_compact.py +0 -104
  92. package/dist/templates/_shared/hooks/session_end.py +0 -173
  93. package/dist/templates/_shared/hooks/session_start.py +0 -206
  94. package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
  95. package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
  96. package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
  97. package/dist/templates/_shared/lib/__init__.py +0 -1
  98. package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  99. package/dist/templates/_shared/lib/base/__init__.py +0 -65
  100. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  101. package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
  102. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  103. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  104. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  105. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  106. package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
  107. package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
  108. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  109. package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
  110. package/dist/templates/_shared/lib/base/constants.py +0 -358
  111. package/dist/templates/_shared/lib/base/hook_utils.py +0 -339
  112. package/dist/templates/_shared/lib/base/inference.py +0 -307
  113. package/dist/templates/_shared/lib/base/logger.py +0 -305
  114. package/dist/templates/_shared/lib/base/stop_words.py +0 -221
  115. package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
  116. package/dist/templates/_shared/lib/base/utils.py +0 -263
  117. package/dist/templates/_shared/lib/context/__init__.py +0 -102
  118. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  119. package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
  120. package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
  121. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  122. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  123. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  124. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  125. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  126. package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
  127. package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
  128. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  129. package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
  130. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  131. package/dist/templates/_shared/lib/context/context_formatter.py +0 -317
  132. package/dist/templates/_shared/lib/context/context_selector.py +0 -508
  133. package/dist/templates/_shared/lib/context/context_store.py +0 -653
  134. package/dist/templates/_shared/lib/context/plan_manager.py +0 -303
  135. package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
  136. package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
  137. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  138. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  139. package/dist/templates/_shared/lib/handoff/document_generator.py +0 -278
  140. package/dist/templates/_shared/lib/templates/README.md +0 -206
  141. package/dist/templates/_shared/lib/templates/__init__.py +0 -36
  142. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  143. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  144. package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
  145. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  146. package/dist/templates/_shared/lib/templates/formatters.py +0 -146
  147. package/dist/templates/_shared/lib/templates/plan_context.py +0 -73
  148. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  149. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  150. package/dist/templates/_shared/scripts/save_handoff.py +0 -357
  151. package/dist/templates/_shared/scripts/status_line.py +0 -716
  152. package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
  153. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
  154. package/dist/templates/cc-native/MIGRATION.md +0 -86
  155. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  156. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  157. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  158. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  159. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  160. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  161. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -130
  162. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -954
  163. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
  164. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
  165. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
  166. package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
  167. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  168. package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
  169. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  170. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  171. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  172. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  173. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  174. package/dist/templates/cc-native/_cc-native/lib/constants.py +0 -45
  175. package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
  176. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
  177. package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
  178. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  179. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  180. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  181. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  182. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  183. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +0 -215
  184. package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
  185. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
  186. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
  187. package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
  188. package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1071
  189. package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
  190. package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +0 -168
  191. package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +0 -134
@@ -5,10 +5,10 @@
5
5
  * See SPEC.md §4
6
6
  */
7
7
 
8
+ import * as crypto from "node:crypto";
8
9
  import * as fs from "node:fs";
10
+ import * as _os from "node:os";
9
11
  import * as path from "node:path";
10
- import * as os from "node:os";
11
- import * as crypto from "node:crypto";
12
12
 
13
13
  /**
14
14
  * Write file atomically with retry logic.
@@ -21,7 +21,7 @@ export function atomicWrite(
21
21
  content: string,
22
22
  maxAttempts = 2,
23
23
  backoffMs: number[] = [500, 1000],
24
- ): [boolean, string | null] {
24
+ ): [boolean, null | string] {
25
25
  // Ensure parent directory exists
26
26
  const dir = path.dirname(filePath);
27
27
  fs.mkdirSync(dir, { recursive: true });
@@ -53,7 +53,7 @@ export function atomicWrite(
53
53
  fs.renameSync(tmpPath, filePath);
54
54
 
55
55
  return [true, null];
56
- } catch (e: any) {
56
+ } catch (error: any) {
57
57
  // Clean up temp file
58
58
  try {
59
59
  fs.unlinkSync(tmpPath);
@@ -62,11 +62,11 @@ export function atomicWrite(
62
62
  }
63
63
 
64
64
  if (attempt < maxAttempts - 1) {
65
- const waitMs = backoffMs[Math.min(attempt, backoffMs.length - 1)] ?? backoffMs[backoffMs.length - 1] ?? 500;
65
+ const waitMs = backoffMs[Math.min(attempt, backoffMs.length - 1)] ?? backoffMs.at(-1) ?? 500;
66
66
  sleepSync(waitMs);
67
67
  } else {
68
- const errType = e?.constructor?.name ?? "Error";
69
- const errMsg = String(e).split("\n")[0]?.slice(0, 200) ?? "";
68
+ const errType = error?.constructor?.name ?? "Error";
69
+ const errMsg = String(error).split("\n")[0]?.slice(0, 200) ?? "";
70
70
  return [false, `${errType}: ${errMsg}`];
71
71
  }
72
72
  }
@@ -85,7 +85,7 @@ export function atomicAppend(
85
85
  content: string,
86
86
  maxAttempts = 2,
87
87
  backoffMs: number[] = [500, 1000],
88
- ): [boolean, string | null] {
88
+ ): [boolean, null | string] {
89
89
  // Ensure parent directory exists
90
90
  const dir = path.dirname(filePath);
91
91
  fs.mkdirSync(dir, { recursive: true });
@@ -112,13 +112,13 @@ export function atomicAppend(
112
112
  }
113
113
 
114
114
  return [true, null];
115
- } catch (e: any) {
115
+ } catch (error: any) {
116
116
  if (attempt < maxAttempts - 1) {
117
- const waitMs = backoffMs[Math.min(attempt, backoffMs.length - 1)] ?? backoffMs[backoffMs.length - 1] ?? 500;
117
+ const waitMs = backoffMs[Math.min(attempt, backoffMs.length - 1)] ?? backoffMs.at(-1) ?? 500;
118
118
  sleepSync(waitMs);
119
119
  } else {
120
- const errType = e?.constructor?.name ?? "Error";
121
- const errMsg = String(e).split("\n")[0]?.slice(0, 200) ?? "";
120
+ const errType = error?.constructor?.name ?? "Error";
121
+ const errMsg = String(error).split("\n")[0]?.slice(0, 200) ?? "";
122
122
  return [false, `${errType}: ${errMsg}`];
123
123
  }
124
124
  }
@@ -3,8 +3,9 @@
3
3
  * See SPEC.md §2
4
4
  */
5
5
 
6
- import * as path from "node:path";
7
6
  import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+
8
9
  import { logWarn } from "./logger.js";
9
10
 
10
11
  // Directory names (relative to project root)
@@ -28,9 +29,9 @@ export const RETRY_BACKOFF_MS = [500, 1000];
28
29
 
29
30
  // Windows reserved filenames
30
31
  const WINDOWS_RESERVED = new Set([
31
- "CON", "PRN", "AUX", "NUL",
32
- "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
33
- "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
32
+ "AUX", "COM1", "COM2", "COM3",
33
+ "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "CON", "LPT1", "LPT2",
34
+ "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "NUL", "PRN",
34
35
  ]);
35
36
 
36
37
  /**
@@ -41,9 +42,9 @@ export function sanitizeContextId(contextId: string): string {
41
42
  if (!contextId) return "context";
42
43
 
43
44
  let result = contextId.toLowerCase();
44
- result = result.replace(/[^a-z0-9_-]/g, "-");
45
- result = result.replace(/[-_]+/g, "-");
46
- result = result.replace(/^[-_]+|[-_]+$/g, "");
45
+ result = result.replaceAll(/[^a-z0-9_-]/g, "-");
46
+ result = result.replaceAll(/[-_]+/g, "-");
47
+ result = result.replaceAll(/^[-_]+|[-_]+$/g, "");
47
48
 
48
49
  if (result.length > MAX_CONTEXT_ID_LENGTH) {
49
50
  result = result.slice(0, MAX_CONTEXT_ID_LENGTH).replace(/[-_]+$/, "");
@@ -101,13 +102,18 @@ export function getProjectRoot(payloadCwd?: string): string {
101
102
  } else {
102
103
  return envDir;
103
104
  }
104
- }
105
+ }
106
+
105
107
  if (payloadCwd) return payloadCwd;
106
108
  return process.cwd();
107
109
  }
108
110
 
109
111
  // §2.4 — Path functions
110
112
 
113
+ export function getAiwcliDir(projectRoot?: string): string {
114
+ return path.join(projectRoot ?? getProjectRoot(), ".aiwcli");
115
+ }
116
+
111
117
  export function getOutputDir(projectRoot?: string): string {
112
118
  return path.join(projectRoot ?? getProjectRoot(), OUTPUT_DIR);
113
119
  }
@@ -230,7 +236,8 @@ export function getHandoffFolderPath(
230
236
  while (fs.existsSync(folder)) {
231
237
  folder = path.join(handoffsDir, `${ts}-${counter}`);
232
238
  counter++;
233
- }
239
+ }
240
+
234
241
  return folder;
235
242
  }
236
243
 
@@ -268,8 +275,8 @@ export function sanitizeFilename(
268
275
  maxLen = 32,
269
276
  allowLeadingDot = false,
270
277
  ): string {
271
- let result = s.replace(/[^A-Za-z0-9._-]+/g, "_");
272
- result = result.replace(/^[._-]+|[._-]+$/g, "").slice(0, maxLen) || "unknown";
278
+ let result = s.replaceAll(/[^A-Za-z0-9._-]+/g, "_");
279
+ result = result.replaceAll(/^[._-]+|[._-]+$/g, "").slice(0, maxLen) || "unknown";
273
280
 
274
281
  if (!allowLeadingDot) {
275
282
  result = result.replace(/^\.+/, "");
@@ -285,10 +292,10 @@ export function sanitizeFilename(
285
292
 
286
293
  export function sanitizeTitle(s: string, maxLen = 50): string {
287
294
  let result = s.toLowerCase().trim();
288
- result = result.replace(/ /g, "-");
289
- result = result.replace(/[^a-z0-9._-]+/g, "_");
290
- result = result.replace(/[-_]+/g, "-");
291
- result = result.replace(/^[._-]+|[._-]+$/g, "").slice(0, maxLen) || "unknown";
295
+ result = result.replaceAll(' ', "-");
296
+ result = result.replaceAll(/[^a-z0-9._-]+/g, "_");
297
+ result = result.replaceAll(/[-_]+/g, "-");
298
+ result = result.replaceAll(/^[._-]+|[._-]+$/g, "").slice(0, maxLen) || "unknown";
292
299
 
293
300
  const baseName = (result.split(".")[0] ?? result).toUpperCase();
294
301
  if (WINDOWS_RESERVED.has(baseName)) {
@@ -23,7 +23,7 @@ export function getGitState(projectRoot: string): Record<string, any> {
23
23
  try {
24
24
  const status = execFileSync("git", ["status", "--short"], opts);
25
25
  if (status) {
26
- const files = status.trim().split("\n")
26
+ const files = status.trim().split(/\r?\n/)
27
27
  .filter(Boolean)
28
28
  .slice(0, 10)
29
29
  .map(line => line.trimStart().split(/\s+/).slice(1).join(" "));
@@ -5,24 +5,26 @@
5
5
  */
6
6
 
7
7
  import * as fs from "node:fs";
8
- import { logDebug, logInfo, logWarn, logError, logHookError, logDiagnostic, hookLog, setContextPath, getContextPath as _getContextPath } from "./logger.js";
8
+
9
9
  import { getProjectRoot } from "./constants.js";
10
+ import { getContextPath as _getContextPath, hookLog, logDebug, setSessionId } from "./logger.js";
10
11
  import { getContextBySessionId } from "../context/context-store.js";
11
12
  import type { HookInput, HookOutput } from "../types.js";
12
13
 
13
14
  // Re-export logger functions for convenience (matches Python hook_utils re-exports)
14
- export { logDebug, logInfo, logWarn, logError, logHookError, logDiagnostic, hookLog, setContextPath };
15
+
15
16
 
16
17
  // Context window baseline: tokens not visible in hook data §5.9
17
18
  export const CONTEXT_BASELINE_TOKENS = 22_600;
18
19
  export const DEFAULT_CONTEXT_WINDOW_SIZE = 200_000;
19
20
 
20
21
  // Event metadata stash — populated by loadHookInput(), read by runHook()
21
- let _lastHookEvent: string | null = null;
22
- let _lastToolName: string | null = null;
22
+ let _lastHookEvent: null | string = null;
23
+ let _lastToolName: null | string = null;
24
+ let _cachedHookName: null | string = null;
23
25
 
24
26
  // Pre-fetched input stash
25
- let _prefetchedInput: Record<string, any> | null = null;
27
+ let _prefetchedInput: null | Record<string, any> = null;
26
28
 
27
29
  /**
28
30
  * Load and parse JSON from stdin (or return prefetched input if set).
@@ -37,12 +39,13 @@ export function loadHookInput(): HookInput | null {
37
39
  _lastHookEvent = result.hook_event_name ?? null;
38
40
  _lastToolName = result.tool_name ?? null;
39
41
  }
42
+
40
43
  return result as HookInput;
41
44
  }
42
45
 
43
46
  try {
44
47
  // Read entire stdin using fd 0 (cross-platform, works on Windows)
45
- const inputData = fs.readFileSync(0, "utf-8").trim();
48
+ const inputData = fs.readFileSync(0, "utf8").trim();
46
49
  if (!inputData) return null;
47
50
 
48
51
  const result = JSON.parse(inputData);
@@ -50,6 +53,7 @@ export function loadHookInput(): HookInput | null {
50
53
  _lastHookEvent = result.hook_event_name ?? null;
51
54
  _lastToolName = result.tool_name ?? null;
52
55
  }
56
+
53
57
  return result as HookInput;
54
58
  } catch {
55
59
  return null;
@@ -76,7 +80,7 @@ export function validateHookEvent(
76
80
  */
77
81
  export function getToolInput(
78
82
  payload: HookInput,
79
- ): Record<string, any> | null {
83
+ ): null | Record<string, any> {
80
84
  const toolInput = payload.tool_input;
81
85
  return toolInput && typeof toolInput === "object" ? toolInput : null;
82
86
  }
@@ -92,53 +96,76 @@ export function checkSkipPersistence(
92
96
  const toolInput = getToolInput(payload);
93
97
  if (!toolInput) return false;
94
98
 
95
- const metadata = toolInput.metadata;
99
+ const {metadata} = toolInput;
96
100
  if (metadata && typeof metadata === "object" && metadata.skip_persistence) {
97
101
  logDebug(hookName, "Skipping persistence (skip_persistence flag set)");
98
102
  return true;
99
103
  }
104
+
100
105
  return false;
101
106
  }
102
107
 
103
108
  /**
104
109
  * Emit hookSpecificOutput with additionalContext to stdout.
110
+ * hookEventName is required by Claude Code's Zod validator (discriminated union).
111
+ * Auto-detected from stdin payload (set by loadHookInput/runHook).
105
112
  * See SPEC.md §5.5
106
113
  */
107
114
  export function emitContext(additionalContext: string): void {
115
+ const eventName = _lastHookEvent ?? undefined;
108
116
  const out: HookOutput = {
109
117
  hookSpecificOutput: {
118
+ ...(eventName ? { hookEventName: eventName } : {}),
110
119
  additionalContext,
111
120
  },
112
121
  };
113
- process.stdout.write(JSON.stringify(out) + "\n");
122
+ const json = JSON.stringify(out);
123
+ _logEmit("context", additionalContext.length, { additionalContext });
124
+ process.stdout.write(json + "\n");
114
125
  }
115
126
 
116
127
  /**
117
128
  * Emit hookSpecificOutput that denies the tool call with context and reason.
129
+ * hookEventName is required by Claude Code's Zod validator (discriminated union).
130
+ * Auto-detected from stdin payload (set by loadHookInput/runHook).
118
131
  * See SPEC.md §5.6
119
132
  */
120
133
  export function emitContextAndBlock(
121
134
  additionalContext: string,
122
135
  reason: string,
123
136
  ): void {
137
+ const eventName = _lastHookEvent ?? undefined;
124
138
  const out: HookOutput = {
125
139
  hookSpecificOutput: {
140
+ ...(eventName ? { hookEventName: eventName } : {}),
126
141
  additionalContext,
127
142
  permissionDecision: "deny",
128
143
  permissionDecisionReason: reason,
129
144
  },
130
145
  };
131
- process.stdout.write(JSON.stringify(out) + "\n");
146
+ const json = JSON.stringify(out);
147
+ _logEmit("block", additionalContext.length, { additionalContext, blockReason: reason });
148
+ process.stdout.write(json + "\n");
149
+ }
150
+
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 {
153
+ const hook = _cachedHookName ?? "unknown";
154
+ const msg = type === "block"
155
+ ? `HOOK_OUTPUT [${type}] ${chars} chars, reason="${(payload.blockReason ?? "").slice(0, 80)}"`
156
+ : `HOOK_OUTPUT [${type}] ${chars} chars`;
157
+ hookLog("info", hook, msg, { data: payload });
132
158
  }
133
159
 
134
160
  /**
135
161
  * Auto-detect template origin from the hook script path.
136
162
  */
137
163
  function detectTemplate(scriptPath = ""): string {
138
- const p = (scriptPath || (process.argv[1] ?? "")).replace(/\\/g, "/");
164
+ const p = (scriptPath || (process.argv[1] ?? "")).replaceAll('\\', "/");
139
165
  if (p.includes("/_shared/hooks/") || p.startsWith("_shared/hooks/")) {
140
166
  return "shared";
141
167
  }
168
+
142
169
  const match = p.match(/_([a-z][a-z0-9-]*)\/hooks\//);
143
170
  if (match?.[1]) return match[1]; // e.g., "cc-native"
144
171
  return "unknown";
@@ -151,7 +178,7 @@ function detectTemplate(scriptPath = ""): string {
151
178
  */
152
179
  export function parseContextWindow(
153
180
  hookInput: HookInput,
154
- ): [number | null, number | null] {
181
+ ): [null | number, null | number] {
155
182
  const contextWindow = hookInput.context_window;
156
183
  if (!contextWindow) return [null, null];
157
184
 
@@ -177,7 +204,7 @@ export function parseContextWindow(
177
204
  */
178
205
  export function getContextPercentRemaining(
179
206
  hookInput: HookInput,
180
- ): [number | null, number | null, number | null] {
207
+ ): [null | number, null | number, null | number] {
181
208
  const [tokensUsed, maxTokens] = parseContextWindow(hookInput);
182
209
 
183
210
  if (tokensUsed !== null && maxTokens !== null && maxTokens > 0) {
@@ -206,6 +233,45 @@ export function getContextPercentRemaining(
206
233
  return [null, null, null];
207
234
  }
208
235
 
236
+ /**
237
+ * Read stdin early and extract session_id + event metadata.
238
+ * Stashes parsed input for loadHookInput() to consume later.
239
+ */
240
+ function _earlyReadInput(prefetchedInput?: Record<string, any>): void {
241
+ if (prefetchedInput !== undefined) {
242
+ _prefetchedInput = prefetchedInput;
243
+ }
244
+
245
+ // If we already have prefetched input, extract metadata from it
246
+ if (_prefetchedInput && typeof _prefetchedInput === "object") {
247
+ _lastHookEvent = _prefetchedInput.hook_event_name ?? null;
248
+ _lastToolName = _prefetchedInput.tool_name ?? null;
249
+ if (_prefetchedInput.session_id) {
250
+ setSessionId(_prefetchedInput.session_id);
251
+ }
252
+
253
+ return;
254
+ }
255
+
256
+ // Read stdin now so HOOK_START can include sid
257
+ try {
258
+ const inputData = fs.readFileSync(0, "utf8").trim();
259
+ if (inputData) {
260
+ const parsed = JSON.parse(inputData);
261
+ if (parsed && typeof parsed === "object") {
262
+ _prefetchedInput = parsed;
263
+ _lastHookEvent = parsed.hook_event_name ?? null;
264
+ _lastToolName = parsed.tool_name ?? null;
265
+ if (parsed.session_id) {
266
+ setSessionId(parsed.session_id);
267
+ }
268
+ }
269
+ }
270
+ } catch {
271
+ // Non-fatal — loadHookInput will return null
272
+ }
273
+ }
274
+
209
275
  /**
210
276
  * Standard hook entry point with lifecycle logging.
211
277
  * See SPEC.md §5.7
@@ -215,23 +281,21 @@ export function runHook(
215
281
  hookName = "unknown",
216
282
  prefetchedInput?: Record<string, any>,
217
283
  ): never {
218
- if (prefetchedInput !== undefined) {
219
- _prefetchedInput = prefetchedInput;
220
- }
284
+ _earlyReadInput(prefetchedInput);
285
+ _cachedHookName = hookName;
221
286
 
222
287
  const startTime = performance.now();
223
288
  const template = detectTemplate();
224
289
  const event = _lastHookEvent ?? "unknown";
225
290
  const tool = _lastToolName;
226
291
 
227
- // HOOK_START
228
292
  const startData: Record<string, any> = {
229
293
  lifecycle: "start",
230
294
  template,
231
295
  event,
232
296
  };
233
297
  if (tool) startData.tool = tool;
234
- logInfo(hookName, "HOOK_START", { data: startData });
298
+ hookLog("info", hookName, "HOOK_START", { data: startData });
235
299
 
236
300
  let exitCode = 0;
237
301
  let status = "success";
@@ -240,17 +304,17 @@ export function runHook(
240
304
  try {
241
305
  const result = mainFunc();
242
306
  exitCode = typeof result === "number" ? result : 0;
243
- status = exitCode !== 0 ? "blocked" : "success";
244
- } catch (e: any) {
245
- if (e instanceof Error && e.message.startsWith("SystemExit:")) {
246
- const code = parseInt(e.message.slice(11), 10);
247
- exitCode = isNaN(code) ? (e.message.slice(11) ? 1 : 0) : code;
307
+ status = exitCode === 0 ? "success" : "blocked";
308
+ } catch (error: any) {
309
+ if (error instanceof Error && error.message.startsWith("SystemExit:")) {
310
+ const code = parseInt(error.message.slice(11), 10);
311
+ exitCode = isNaN(code) ? (error.message.slice(11) ? 1 : 0) : code;
248
312
  status = exitCode !== 0 ? "blocked" : "success";
249
313
  } else {
250
314
  exitCode = 0; // Non-blocking
251
315
  status = "error";
252
- const stack = e instanceof Error ? e.stack ?? "" : "";
253
- errorInfo = [e instanceof Error ? e : new Error(String(e)), stack];
316
+ const stack = error instanceof Error ? error.stack ?? "" : "";
317
+ errorInfo = [error instanceof Error ? error : new Error(String(error)), stack];
254
318
  }
255
319
  }
256
320
 
@@ -268,47 +332,45 @@ export function runHookAsync(
268
332
  hookName = "unknown",
269
333
  prefetchedInput?: Record<string, any>,
270
334
  ): void {
271
- if (prefetchedInput !== undefined) {
272
- _prefetchedInput = prefetchedInput;
273
- }
335
+ _earlyReadInput(prefetchedInput);
336
+ _cachedHookName = hookName;
274
337
 
275
338
  const startTime = performance.now();
276
339
  const template = detectTemplate();
277
340
  const event = _lastHookEvent ?? "unknown";
278
341
  const tool = _lastToolName;
279
342
 
280
- // HOOK_START
281
343
  const startData: Record<string, any> = {
282
344
  lifecycle: "start",
283
345
  template,
284
346
  event,
285
347
  };
286
348
  if (tool) startData.tool = tool;
287
- logInfo(hookName, "HOOK_START", { data: startData });
349
+ hookLog("info", hookName, "HOOK_START", { data: startData });
288
350
 
289
351
  mainFunc()
290
352
  .then((result) => {
291
353
  const exitCode = typeof result === "number" ? result : 0;
292
- _emitHookEnd(hookName, startTime, exitCode, exitCode !== 0 ? "blocked" : "success", null, startData, event, tool, template);
293
- process.exit(exitCode);
354
+ _emitHookEnd(hookName, startTime, exitCode, exitCode === 0 ? "success" : "blocked", null, startData, event, tool, template);
355
+ _drainAndExit(exitCode);
294
356
  })
295
- .catch((e: any) => {
357
+ .catch((error: any) => {
296
358
  let exitCode = 0;
297
359
  let status = "error";
298
360
  let errorInfo: [Error, string] | null = null;
299
361
 
300
- if (e instanceof Error && e.message.startsWith("SystemExit:")) {
301
- const code = parseInt(e.message.slice(11), 10);
302
- exitCode = isNaN(code) ? (e.message.slice(11) ? 1 : 0) : code;
362
+ if (error instanceof Error && error.message.startsWith("SystemExit:")) {
363
+ const code = parseInt(error.message.slice(11), 10);
364
+ exitCode = isNaN(code) ? (error.message.slice(11) ? 1 : 0) : code;
303
365
  status = exitCode !== 0 ? "blocked" : "success";
304
366
  } else {
305
367
  exitCode = 0; // Non-blocking (fail open)
306
- const stack = e instanceof Error ? e.stack ?? "" : "";
307
- errorInfo = [e instanceof Error ? e : new Error(String(e)), stack];
368
+ const stack = error instanceof Error ? error.stack ?? "" : "";
369
+ errorInfo = [error instanceof Error ? error : new Error(String(error)), stack];
308
370
  }
309
371
 
310
372
  _emitHookEnd(hookName, startTime, exitCode, status, errorInfo, startData, event, tool, template);
311
- process.exit(exitCode);
373
+ _drainAndExit(exitCode);
312
374
  });
313
375
  }
314
376
 
@@ -321,17 +383,13 @@ function _emitHookEnd(
321
383
  errorInfo: [Error, string] | null,
322
384
  startData: Record<string, any>,
323
385
  event: string,
324
- tool: string | null,
386
+ tool: null | string,
325
387
  template: string,
326
388
  ): void {
327
- // Retroactive HOOK_START to per-context log
389
+ // Retroactive HOOK_START to per-context log (context_path resolved after main runs)
328
390
  const resolvedAfter = _getContextPath();
329
391
  if (resolvedAfter && fs.existsSync(resolvedAfter)) {
330
- hookLog("info", hookName, "HOOK_START", {
331
- data: startData,
332
- context_path: resolvedAfter,
333
- stderr: false,
334
- });
392
+ hookLog("info", hookName, "HOOK_START", { data: startData });
335
393
  }
336
394
 
337
395
  const durationMs = Math.round((performance.now() - startTime) * 10) / 10;
@@ -350,11 +408,32 @@ function _emitHookEnd(
350
408
  if (errorInfo) {
351
409
  const [err, tb] = errorInfo;
352
410
  endData.error_type = err.constructor.name;
353
- logHookError(hookName, err, endEvent, tb);
354
- logError(hookName, `HOOK_END: ${err}`, { data: endData, traceback_str: tb });
411
+ hookLog("error", hookName, `[${endEvent}] ${err.constructor.name}: ${String(err).replaceAll(/[\n\r]/g, " ").slice(0, 200)}`, { traceback_str: tb });
412
+ hookLog("error", hookName, `HOOK_END: ${err}`, { data: endData, traceback_str: tb });
355
413
  } else if (status === "blocked") {
356
- logWarn(hookName, "HOOK_END", { data: endData });
414
+ hookLog("warn", hookName, "HOOK_END", { data: endData });
357
415
  } else {
358
- logInfo(hookName, "HOOK_END", { data: endData });
416
+ hookLog("info", hookName, "HOOK_END", { data: endData });
359
417
  }
360
418
  }
419
+
420
+ /**
421
+ * Drain stdout before exiting to ensure pipe consumers receive all data.
422
+ * On Windows, stdout to a pipe is fully buffered — process.exit() can
423
+ * discard unflushed data. This waits for the write buffer to drain.
424
+ */
425
+ function _drainAndExit(code: number): void {
426
+ // If stdout is already finished or not writable, exit immediately
427
+ if (!process.stdout.writable || process.stdout.writableFinished) {
428
+ process.exit(code);
429
+ }
430
+
431
+ // Attempt to end stdout and wait for drain
432
+ const timeout = setTimeout(() => process.exit(code), 1000); // safety fallback
433
+ process.stdout.end(() => {
434
+ clearTimeout(timeout);
435
+ process.exit(code);
436
+ });
437
+ }
438
+
439
+ export {hookLog, logBlocking, logDebug, logDiagnostic, logError, logHookError, logInfo, logWarn, setContextPath, setSessionId} from "./logger.js";