@ghl-ai/aw 0.1.55-beta.0 → 0.1.56-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/commands/init.mjs CHANGED
@@ -27,7 +27,7 @@ import { linkWorkspace } from '../link.mjs';
27
27
  import { generateCommands, copyInstructions, initAwDocs, syncHomeHarnessInstructions } from '../integrate.mjs';
28
28
  import { setupMcp } from '../mcp.mjs';
29
29
  import { ensureContextModeIntegration, isContextModeRequested } from '../integrations/context-mode.mjs';
30
- import { applyStoredStartupPreferences, ensureAwRuntimeHook } from '../startup.mjs';
30
+ import { applyStoredStartupPreferences, ensureAwRuntimeHook, isDefaultRoutingEnabled } from '../startup.mjs';
31
31
  import { installLocalCommitHook } from '../hooks.mjs';
32
32
  import { autoUpdate, promptUpdate } from '../update.mjs';
33
33
  import { installGlobalHooks } from '../hooks.mjs';
@@ -51,6 +51,7 @@ import {
51
51
  } from '../git.mjs';
52
52
  import { REGISTRY_DIR, REGISTRY_REPO, REGISTRY_URL, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR, RULES_RUNTIME_DIR } from '../constants.mjs';
53
53
  import { syncFileTree } from '../file-tree.mjs';
54
+ import { installAwUsageHooks, formatAwUsageHooksInstallReport } from '../install-aw-usage-hooks.mjs';
54
55
 
55
56
  const __dirname = dirname(fileURLToPath(import.meta.url));
56
57
  const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
@@ -607,12 +608,30 @@ export async function initCommand(args) {
607
608
  ];
608
609
  if (!silent) fmt.logStep(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
609
610
 
610
- // Write hook manifest after all hook installation is complete
611
- try { writeHookManifest({ eccVersion: AW_ECC_TAG, awVersion: VERSION }); } catch { /* best effort */ }
612
-
613
611
  // Ensure telemetry config exists (generates machine_id on first run)
614
612
  ensureTelemetryConfig();
615
613
 
614
+ // Install bundled aw-usage producer hooks into ~/.claude.
615
+ // Copies the scripts + lib files and non-destructively merges 5 hook phases
616
+ // (SessionStart, UserPromptSubmit, PostToolUse, PostToolUseFailure, Stop)
617
+ // into ~/.claude/settings.json. Idempotent. After this, every Claude Code
618
+ // session emits usage_events to the live telemetry API automatically.
619
+ let awUsageHooksReport = null;
620
+ try {
621
+ if (isDefaultRoutingEnabled(HOME, process.env)) {
622
+ const result = installAwUsageHooks();
623
+ awUsageHooksReport = formatAwUsageHooksInstallReport(result);
624
+ }
625
+ } catch (e) {
626
+ // Non-fatal — telemetry is observational. Surface the error in silent mode
627
+ // logs but don't block the install.
628
+ if (!silent) fmt.note(`aw-usage hooks install: ${e.message}`, 'Telemetry');
629
+ }
630
+
631
+ // Write hook manifest after all hook installation is complete, including
632
+ // bundled usage hooks, so `aw nuke` can prune AW-managed settings entries.
633
+ try { writeHookManifest({ eccVersion: AW_ECC_TAG, awVersion: VERSION }); } catch { /* best effort */ }
634
+
616
635
  // Auto-install suggested integrations (Codex, Caveman, Graphify, etc) - unless --no-integrations
617
636
  let installedIntegrations = [];
618
637
  if (!silent && !skipIntegrations) {
@@ -636,6 +655,7 @@ export async function initCommand(args) {
636
655
  ? ` ${chalk.green('✓')} Removed ${removedLegacyStartupFiles.length} legacy repo startup file${removedLegacyStartupFiles.length > 1 ? 's' : ''}`
637
656
  : null,
638
657
  hooksInstalled ? ` ${chalk.green('✓')} Git hooks: auto-sync on pull/clone (core.hooksPath)` : null,
658
+ awUsageHooksReport ? ` ${chalk.green('✓')} ${awUsageHooksReport}` : null,
639
659
  ` ${chalk.green('✓')} IDE task: auto-sync on workspace open`,
640
660
  cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')} Linked in current project` : null,
641
661
  installedIntegrations.length > 0 ? ` ${chalk.green('✓')} Integrations: ${installedIntegrations.join(', ')}` : null,
package/ecc.mjs CHANGED
@@ -12,7 +12,7 @@ import { applyStoredStartupPreferences } from "./startup.mjs";
12
12
 
13
13
  const AW_ECC_REPO_SSH = "git@github.com:shreyansh-ghl/aw-ecc.git";
14
14
  const AW_ECC_REPO_HTTPS = "https://github.com/shreyansh-ghl/aw-ecc.git";
15
- export const AW_ECC_TAG = "v1.4.61";
15
+ export const AW_ECC_TAG = "v1.4.62";
16
16
 
17
17
  const MARKETPLACE_NAME = "aw-marketplace";
18
18
  const PLUGIN_KEY = `aw@${MARKETPLACE_NAME}`;
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Usage telemetry — commit_created event.
4
+ *
5
+ * Called from the post-commit git hook when the commit has an AW
6
+ * Co-Authored-By trailer. Works for all harnesses (Claude, Cursor, Codex)
7
+ * since it fires from a git-level hook, not a harness-specific one.
8
+ *
9
+ * Usage: node aw-usage-commit-created.js <commit_hash> <branch>
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const { buildEvent, sendAsync, isDisabled } = require('../lib/aw-usage-telemetry');
15
+
16
+ if (isDisabled()) process.exit(0);
17
+
18
+ const commitHash = process.argv[2] || 'unknown';
19
+ const branch = process.argv[3] || 'unknown';
20
+
21
+ // Minimal input — no harness session context in a git hook.
22
+ // detectHarness() will return 'claude' (default) but the event
23
+ // payload makes it clear this is harness-agnostic.
24
+ const event = buildEvent({}, 'commit_created', {
25
+ commit_hash: commitHash,
26
+ branch,
27
+ });
28
+
29
+ // Override harness to 'git' since this fires from a git hook, not a harness
30
+ event.harness = 'git';
31
+
32
+ sendAsync(event);
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Usage telemetry — PostToolUseFailure hook.
4
+ *
5
+ * Claude: error (string/object) + is_interrupt (boolean).
6
+ * Cursor: error_message (string) + failure_type ("timeout"/"error"/"permission_denied").
7
+ * Codex: does NOT support PostToolUseFailure — this hook won't fire.
8
+ *
9
+ * Outputs {} on stdout.
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const { buildEvent, sendAsync } = require('../lib/aw-usage-telemetry');
15
+
16
+ const MAX_STDIN = 1024 * 1024;
17
+ let raw = '';
18
+
19
+ function classifyFailureMessage(errorMessage) {
20
+ if (typeof errorMessage !== 'string' || !errorMessage.trim()) return 'tool_failed';
21
+ if (/\bNo such file or directory\b/i.test(errorMessage)) return 'no_such_file_or_directory';
22
+ if (/\bPermission denied\b/i.test(errorMessage)) return 'permission_denied';
23
+ if (/\bOperation not permitted\b/i.test(errorMessage)) return 'operation_not_permitted';
24
+ if (/\bcommand not found\b/i.test(errorMessage)) return 'command_not_found';
25
+ if (/\bcannot access\b/i.test(errorMessage)) return 'cannot_access_path';
26
+ if (/\bis a directory\b/i.test(errorMessage)) return 'path_is_directory';
27
+ return 'tool_failed';
28
+ }
29
+
30
+ process.stdin.setEncoding('utf8');
31
+ process.stdin.on('data', chunk => {
32
+ if (raw.length < MAX_STDIN) {
33
+ raw += chunk.substring(0, MAX_STDIN - raw.length);
34
+ }
35
+ });
36
+
37
+ process.stdin.on('end', () => {
38
+ try {
39
+ const input = JSON.parse(raw);
40
+ const toolName = input.tool_name || 'unknown';
41
+
42
+ // Normalize error fields across Claude and Cursor
43
+ let errorMessage;
44
+ let failureType;
45
+
46
+ if (input.error_message !== undefined) {
47
+ // Cursor format
48
+ errorMessage = String(input.error_message || '');
49
+ failureType = input.failure_type || 'error';
50
+ } else {
51
+ // Claude format
52
+ const err = input.error;
53
+ errorMessage = typeof err === 'string' ? err : (err?.message || JSON.stringify(err) || 'unknown');
54
+ failureType = input.is_interrupt ? 'interrupt' : 'error';
55
+ }
56
+
57
+ sendAsync(buildEvent(input, 'tool_error', {
58
+ tool_name: toolName,
59
+ error_message: classifyFailureMessage(errorMessage),
60
+ failure_type: failureType,
61
+ }));
62
+ } catch {
63
+ // Non-blocking.
64
+ }
65
+
66
+ process.stdout.write('{}');
67
+ });
@@ -0,0 +1,373 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Usage telemetry — PostToolUse hook.
4
+ *
5
+ * Detects: skill invocations, agent spawns, skill pushes.
6
+ * PR contribution is tracked via Co-Authored-By trailer in prepare-commit-msg hook.
7
+ * Outputs {} on stdout (Claude/Codex parse stdout as JSON).
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const {
13
+ buildEvent,
14
+ detectHarness,
15
+ sendAsync,
16
+ readSessionSkill,
17
+ readSessionLastSlashCommand,
18
+ tryAcquireDedupe,
19
+ } = require('../lib/aw-usage-telemetry');
20
+
21
+ const MAX_STDIN = 1024 * 1024;
22
+
23
+ // ── Test-file detection ──────────────────────────────────────────────
24
+ // Match common test conventions across JS/TS, Python, Go, Rust, Java, Kotlin.
25
+ // Designed to be permissive: a hit means "this was probably a test file";
26
+ // false positives are cheap (dashboard groups by guess) and false negatives
27
+ // are worse (lost SDLC signal). Use forward slashes — normalize backslashes
28
+ // before matching since Windows paths come through as \.
29
+ function normalizePath(p) {
30
+ return typeof p === 'string' ? p.replace(/\\/g, '/') : '';
31
+ }
32
+
33
+ // Patterns are evaluated top-to-bottom; earlier matches win. Order from
34
+ // most-specific to least so e.g. `tests/foo.rs` is `rust-test`, not pytest.
35
+ const TEST_PATH_PATTERNS = [
36
+ // JS / TS (jest, vitest, mocha, jasmine)
37
+ { regex: /(?:^|\/)__tests__\/.+\.(?:ts|tsx|js|jsx|mjs|cjs)$/i, framework: 'jest-like' },
38
+ { regex: /\.(?:spec|test)\.(?:ts|tsx|js|jsx|mjs|cjs)$/i, framework: 'jest-vitest' },
39
+ // Go (`xxx_test.go`)
40
+ { regex: /_test\.go$/i, framework: 'go-test' },
41
+ // Rust (`#[cfg(test)]` lives inline, but `tests/` dir is canonical)
42
+ { regex: /(?:^|\/)tests\/.+\.rs$/i, framework: 'rust-test' },
43
+ // Java / Kotlin
44
+ { regex: /(?:^|\/)src\/test\/.+\.(?:java|kt|kts)$/i, framework: 'junit' },
45
+ { regex: /Test\.(?:java|kt)$/, framework: 'junit' },
46
+ // Python (pytest / unittest) — keyed on .py to avoid claiming Rust/Go test dirs.
47
+ { regex: /(?:^|\/)tests?\/.+\.py$/i, framework: 'pytest' },
48
+ { regex: /\/test_[a-z0-9_-]+\.py$/i, framework: 'pytest' },
49
+ { regex: /_test\.py$/i, framework: 'pytest' },
50
+ ];
51
+
52
+ function detectTestFramework(filePath) {
53
+ const norm = normalizePath(filePath);
54
+ if (!norm) return null;
55
+ for (const { regex, framework } of TEST_PATH_PATTERNS) {
56
+ if (regex.test(norm)) return framework;
57
+ }
58
+ return null;
59
+ }
60
+
61
+ // ── SDLC artifact detection ──────────────────────────────────────────
62
+ // Plans, PRDs, specs, tasks, design docs, ADRs, learnings — written by
63
+ // the /aw:plan, /aw:ship, /aw:execute, etc. flows. Maps to an artifact_type
64
+ // + a coarse sdlc_stage so the funnel query can group.
65
+ const SDLC_ARTIFACT_PATTERNS = [
66
+ { regex: /(?:^|\/)plan\.md$/i, artifact_type: 'plan', sdlc_stage: 'plan' },
67
+ { regex: /(?:^|\/)prd\.md$/i, artifact_type: 'prd', sdlc_stage: 'plan' },
68
+ { regex: /(?:^|\/)spec\.md$/i, artifact_type: 'spec', sdlc_stage: 'plan' },
69
+ { regex: /(?:^|\/)tasks\.md$/i, artifact_type: 'tasks', sdlc_stage: 'plan' },
70
+ { regex: /(?:^|\/)design\.md$/i, artifact_type: 'design', sdlc_stage: 'plan' },
71
+ { regex: /(?:^|\/)tech_doc\.md$/i, artifact_type: 'tech_doc', sdlc_stage: 'plan' },
72
+ { regex: /(?:^|\/)architecture\.md$/i, artifact_type: 'architecture', sdlc_stage: 'plan' },
73
+ { regex: /\/\.aw_docs\/runs\/[^/]+\/plan\.md$/i, artifact_type: 'run_plan', sdlc_stage: 'plan' },
74
+ { regex: /\/\.aw_docs\/features\/[^/]+\/[^/]+\.md$/i, artifact_type: 'feature_doc', sdlc_stage: 'plan' },
75
+ { regex: /\/\.aw_docs\/learnings\/[^/]+\.md$/i, artifact_type: 'learning', sdlc_stage: 'learn' },
76
+ // ADR convention
77
+ { regex: /(?:^|\/)adr-\d+[a-z0-9-]*\.md$/i, artifact_type: 'adr', sdlc_stage: 'plan' },
78
+ ];
79
+
80
+ function detectSdlcArtifact(filePath) {
81
+ const norm = normalizePath(filePath);
82
+ if (!norm) return null;
83
+ for (const { regex, artifact_type, sdlc_stage } of SDLC_ARTIFACT_PATTERNS) {
84
+ if (regex.test(norm)) return { artifact_type, sdlc_stage };
85
+ }
86
+ return null;
87
+ }
88
+ function parseMaybeJsonObject(value) {
89
+ if (typeof value !== 'string') return value;
90
+ const trimmed = value.trim();
91
+ if (!trimmed || !/^[{[]/.test(trimmed)) return value;
92
+ try {
93
+ return JSON.parse(trimmed);
94
+ } catch {
95
+ return value;
96
+ }
97
+ }
98
+
99
+ function toObject(value) {
100
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
101
+ }
102
+
103
+ function getSessionId(input) {
104
+ return input?.session_id || input?.conversation_id || null;
105
+ }
106
+
107
+ function getCommand(input) {
108
+ return String(
109
+ input?.tool_input?.command
110
+ || input?.tool_input?.args?.command
111
+ || ''
112
+ );
113
+ }
114
+
115
+ function normalizeToolResult(input) {
116
+ const rawToolResponse = parseMaybeJsonObject(input?.tool_response);
117
+ const rawToolOutput = parseMaybeJsonObject(input?.tool_output);
118
+ const toolResponse = toObject(rawToolResponse);
119
+ const toolOutput = toObject(rawToolOutput);
120
+ const exitCode = [
121
+ input?.exit_code,
122
+ toolResponse.exit_code,
123
+ toolResponse.exitCode,
124
+ toolOutput.exit_code,
125
+ toolOutput.exitCode,
126
+ ].find(value => value !== undefined && value !== null && value !== '');
127
+ const rawMessage = [
128
+ toolResponse.stderr,
129
+ toolOutput.stderr,
130
+ toolResponse.output,
131
+ toolOutput.output,
132
+ typeof rawToolResponse === 'string' ? rawToolResponse : '',
133
+ typeof rawToolOutput === 'string' ? rawToolOutput : '',
134
+ input?.stderr,
135
+ input?.output,
136
+ ].filter(value => typeof value === 'string' && value.trim()).join('\n');
137
+
138
+ return {
139
+ exitCode: exitCode === undefined ? null : Number(exitCode),
140
+ rawErrorMessage: rawMessage,
141
+ errorMessage: classifyFailureMessage(rawMessage),
142
+ };
143
+ }
144
+
145
+ function classifyFailureMessage(errorMessage) {
146
+ if (typeof errorMessage !== 'string' || !errorMessage.trim()) return 'tool_failed';
147
+ if (/\bNo such file or directory\b/i.test(errorMessage)) return 'no_such_file_or_directory';
148
+ if (/\bPermission denied\b/i.test(errorMessage)) return 'permission_denied';
149
+ if (/\bOperation not permitted\b/i.test(errorMessage)) return 'operation_not_permitted';
150
+ if (/\bcommand not found\b/i.test(errorMessage)) return 'command_not_found';
151
+ if (/\bcannot access\b/i.test(errorMessage)) return 'cannot_access_path';
152
+ if (/\bis a directory\b/i.test(errorMessage)) return 'path_is_directory';
153
+ return 'tool_failed';
154
+ }
155
+
156
+ function isExplicitFailureExitCode(exitCode) {
157
+ return exitCode !== null && Number.isFinite(exitCode) && exitCode !== 0;
158
+ }
159
+
160
+ function inferShellFailureFromMessage(toolName, errorMessage) {
161
+ if (toolName !== 'Shell' && toolName !== 'Bash') return false;
162
+ if (typeof errorMessage !== 'string' || !errorMessage.trim()) return false;
163
+ const patterns = [
164
+ /(?:^|\n)[^:\n]+:\s.*\bNo such file or directory\b/i,
165
+ /(?:^|\n)[^:\n]+:\s.*\bPermission denied\b/i,
166
+ /(?:^|\n)[^:\n]+:\s.*\bOperation not permitted\b/i,
167
+ /(?:^|\n)(?:bash|zsh|sh): .*?\bcommand not found\b/i,
168
+ /(?:^|\n)[^:\n]+:\s.*\bcannot access\b/i,
169
+ /(?:^|\n)[^:\n]+:\s.*\bis a directory\b/i,
170
+ ];
171
+ return patterns.some(pattern => pattern.test(errorMessage));
172
+ }
173
+
174
+ function shouldEmitSkillName(skillName) {
175
+ return Boolean(skillName) && skillName !== 'using-aw-skills';
176
+ }
177
+
178
+ function collectPostToolUseEvents(input, options = {}) {
179
+ const events = [];
180
+ const toolName = input?.tool_name || '';
181
+ const promptSkillOverride = options.promptSkillOverride || null;
182
+ // Session-scoped slash command from the most recent /aw:*, /tdd, etc.
183
+ // Used to correlate test/artifact writes back to the originating SDLC stage.
184
+ const slashCmd = options.sessionSlashCommand || null;
185
+
186
+ // Skill detection: Claude uses tool_name='Skill', Cursor reads SKILL.md via 'Read' tool.
187
+ const filePath = input?.tool_input?.file_path || '';
188
+ const isSkillRead = toolName === 'Read' && /\/SKILL\.md$/i.test(filePath);
189
+
190
+ if (toolName === 'Skill' || isSkillRead) {
191
+ let skillName = input?.tool_input?.skill || input?.tool_input?.args?.skill || '';
192
+ // For Cursor: extract skill name from path (e.g. .../skills/<skill-name>/SKILL.md)
193
+ if (!skillName && isSkillRead) {
194
+ const pathMatch = filePath.match(/\/skills\/([^/]+)\/SKILL\.md$/i);
195
+ skillName = pathMatch ? pathMatch[1] : filePath.split('/').slice(-2, -1)[0] || '';
196
+ }
197
+ if (shouldEmitSkillName(skillName)) {
198
+ events.push({
199
+ eventType: 'skill_invoked',
200
+ payload: {
201
+ skill_name: skillName,
202
+ args: input?.tool_input?.args || '',
203
+ },
204
+ });
205
+ }
206
+ } else if (toolName === 'Agent') {
207
+ events.push({
208
+ eventType: 'agent_spawned',
209
+ payload: {
210
+ agent_type: input?.tool_input?.subagent_type || 'general-purpose',
211
+ description: input?.tool_input?.description || '',
212
+ },
213
+ });
214
+ }
215
+
216
+ const toolResult = normalizeToolResult(input);
217
+ if (isExplicitFailureExitCode(toolResult.exitCode)
218
+ || inferShellFailureFromMessage(toolName, toolResult.rawErrorMessage)) {
219
+ const payload = {
220
+ tool_name: toolName || 'unknown',
221
+ error_message: toolResult.errorMessage,
222
+ failure_type: 'error',
223
+ };
224
+ if (isExplicitFailureExitCode(toolResult.exitCode)) {
225
+ payload.exit_code = toolResult.exitCode;
226
+ }
227
+ events.push({
228
+ eventType: 'tool_error',
229
+ payload,
230
+ });
231
+ }
232
+
233
+ if (toolName === 'Shell' || toolName === 'Bash') {
234
+ const cmd = getCommand(input);
235
+ // Codex skill detection: Bash commands that read SKILL.md files.
236
+ if (!promptSkillOverride) {
237
+ const skillCmdMatch = cmd.match(/\/skills\/([^/]+)\/SKILL\.md/i);
238
+ if (skillCmdMatch && shouldEmitSkillName(skillCmdMatch[1])) {
239
+ events.push({
240
+ eventType: 'skill_invoked',
241
+ payload: {
242
+ skill_name: skillCmdMatch[1],
243
+ args: '',
244
+ },
245
+ });
246
+ }
247
+ }
248
+ if (/\baw\s+push\b/.test(cmd)) {
249
+ const skillMatch = cmd.match(/aw\s+push\s+(\S+)/);
250
+ events.push({
251
+ eventType: 'skill_pushed',
252
+ payload: {
253
+ skill_name: skillMatch ? skillMatch[1] : '',
254
+ },
255
+ });
256
+ }
257
+ }
258
+
259
+ // Test-file write detection — match Write/Edit/MultiEdit on common test paths.
260
+ // Emits once per matching write, with a framework guess and the originating
261
+ // slash command (e.g. /aw:test) if the session is in an SDLC stage. The
262
+ // dashboard funnel uses sdlc_correlated_command to compute "tests written
263
+ // via /aw:test" vs "tests written ad-hoc".
264
+ if (toolName === 'Write' || toolName === 'Edit' || toolName === 'MultiEdit') {
265
+ const framework = detectTestFramework(filePath);
266
+ if (framework) {
267
+ events.push({
268
+ eventType: 'test_file_written',
269
+ payload: {
270
+ file_path: filePath,
271
+ test_framework_guess: framework,
272
+ tool_name: toolName,
273
+ sdlc_correlated_command: slashCmd ? slashCmd.command_name : null,
274
+ sdlc_correlated_namespace: slashCmd ? slashCmd.command_namespace : null,
275
+ sdlc_correlated_is_sdlc_stage: slashCmd ? Boolean(slashCmd.is_sdlc_stage) : false,
276
+ },
277
+ });
278
+ }
279
+
280
+ // SDLC artifact creation — emit on Write only (creation, not Edit) so
281
+ // we don't double-count every save of an existing plan.md.
282
+ if (toolName === 'Write') {
283
+ const artifact = detectSdlcArtifact(filePath);
284
+ if (artifact) {
285
+ events.push({
286
+ eventType: 'sdlc_artifact_created',
287
+ payload: {
288
+ file_path: filePath,
289
+ artifact_type: artifact.artifact_type,
290
+ sdlc_stage: artifact.sdlc_stage,
291
+ sdlc_correlated_command: slashCmd ? slashCmd.command_name : null,
292
+ sdlc_correlated_namespace: slashCmd ? slashCmd.command_namespace : null,
293
+ sdlc_correlated_is_sdlc_stage: slashCmd ? Boolean(slashCmd.is_sdlc_stage) : false,
294
+ },
295
+ });
296
+ }
297
+ }
298
+ }
299
+
300
+ return events;
301
+ }
302
+
303
+ function processPostToolUseInput(input, deps = {}) {
304
+ const emit = typeof deps.emit === 'function' ? deps.emit : () => {};
305
+ const sessionId = getSessionId(input);
306
+ const promptSkillOverride = deps.promptSkillOverride || readSessionSkill(
307
+ sessionId,
308
+ input?.turn_id || null,
309
+ );
310
+ const sessionSlashCommand = deps.sessionSlashCommand
311
+ || readSessionLastSlashCommand(sessionId);
312
+ const events = collectPostToolUseEvents(input, {
313
+ promptSkillOverride,
314
+ sessionSlashCommand,
315
+ });
316
+ for (const event of events) {
317
+ emit(event.eventType, event.payload);
318
+ }
319
+ return events;
320
+ }
321
+
322
+ function main() {
323
+ let raw = '';
324
+
325
+ process.stdin.setEncoding('utf8');
326
+ process.stdin.on('data', chunk => {
327
+ if (raw.length < MAX_STDIN) {
328
+ raw += chunk.substring(0, MAX_STDIN - raw.length);
329
+ }
330
+ });
331
+
332
+ process.stdin.on('end', () => {
333
+ try {
334
+ const input = JSON.parse(raw);
335
+ const shouldDedup = detectHarness(input) === 'codex' || Boolean(input?.tool_use_id);
336
+ if (!shouldDedup || tryAcquireDedupe('post-tool-use', [
337
+ getSessionId(input),
338
+ input?.turn_id || '',
339
+ input?.tool_use_id || '',
340
+ input?.tool_name || '',
341
+ getCommand(input),
342
+ input?.tool_input?.file_path || '',
343
+ input?.tool_input?.description || '',
344
+ ])) {
345
+ processPostToolUseInput(input, {
346
+ emit(eventType, payload) {
347
+ sendAsync(buildEvent(input, eventType, payload));
348
+ },
349
+ });
350
+ }
351
+ } catch {
352
+ // Non-blocking — never fail the hook.
353
+ }
354
+
355
+ process.stdout.write('{}');
356
+ });
357
+ }
358
+
359
+ if (require.main === module) {
360
+ main();
361
+ }
362
+
363
+ module.exports = {
364
+ collectPostToolUseEvents,
365
+ detectSdlcArtifact,
366
+ detectTestFramework,
367
+ inferShellFailureFromMessage,
368
+ isExplicitFailureExitCode,
369
+ normalizeToolResult,
370
+ parseMaybeJsonObject,
371
+ processPostToolUseInput,
372
+ shouldEmitSkillName,
373
+ };