From 06fa61d7d6e29305930039d85ae6fd7a0d143a89 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Wed, 29 Apr 2026 02:28:59 +0200 Subject: [PATCH] Diagnose missing PreToolUse hook wiring in claim-before-edit health Surface PreToolUse auto-claim coverage as first-class telemetry so a 0% claim-before-edit metric distinguishes "hook is not firing" from "agent skipped the claim", and gives the editor a concrete next call. - Scope Claude installer's PreToolUse/PostToolUse to a write-tool matcher so unrelated tool calls do not fire the auto-claim hook. - Emit PreToolUse warning context via Claude Code's permissionDecision: allow + permissionDecisionReason so the agent actually sees the missing-claim diagnostic instead of it being dropped on stderr. - Embed a copy-pasteable mcp__colony__task_claim_file({...}) next_call and a multi-line actionable message in pre-tool-use warnings. - Track pre_tool_use_signals in claimBeforeEditStats and use it from colony health and hivemind_context's claim-before-edit nudge to distinguish missing-hook from agent-discipline cases and surface a reinstall/restart hint. - Replace "explicit claims first" wording with "claim before edit" plus explicit/manual vs auto-claim breakdown. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../auto-claim-hook-wiring-diagnostics.md | 15 ++++++ apps/cli/src/commands/health.ts | 46 ++++++++++++++++--- apps/cli/src/commands/hook.ts | 35 ++++++++++---- apps/cli/test/health.test.ts | 13 ++++-- apps/mcp-server/src/tools/hivemind.ts | 9 +++- apps/mcp-server/test/server.test.ts | 4 +- packages/hooks/src/handlers/pre-tool-use.ts | 32 +++++++++++-- packages/hooks/test/auto-claim.test.ts | 45 ++++++++++++++---- packages/installers/src/claude-code.ts | 12 +++++ packages/installers/test/installers.test.ts | 15 +++++- packages/storage/src/storage.ts | 15 +++++- .../test/coordination-activity.test.ts | 1 + 12 files changed, 202 insertions(+), 40 deletions(-) create mode 100644 .changeset/auto-claim-hook-wiring-diagnostics.md diff --git a/.changeset/auto-claim-hook-wiring-diagnostics.md b/.changeset/auto-claim-hook-wiring-diagnostics.md new file mode 100644 index 0000000..c6ccff1 --- /dev/null +++ b/.changeset/auto-claim-hook-wiring-diagnostics.md @@ -0,0 +1,15 @@ +--- +"@colony/storage": patch +"@colony/installers": patch +"@colony/hooks": patch +"@colony/mcp-server": patch +"@imdeadpool/colony-cli": patch +--- + +Make PreToolUse auto-claim coverage observable and surface hook-wiring problems instead of agent-discipline ones. + +- The Claude installer now scopes PreToolUse and PostToolUse to a write-tool matcher so the hook does not fire (or get blamed) for unrelated tools. +- `colony hook run pre-tool-use` now writes its warning back through Claude Code's PreToolUse `permissionDecision: allow` so the agent sees the missing-claim warning instead of it being silently dropped on stderr. +- The pre-tool-use warning embeds a concrete `next_call` (an exact `mcp__colony__task_claim_file({...})` invocation) and a multi-line actionable `message`, so an agent that hits ACTIVE_TASK_NOT_FOUND / AMBIGUOUS_ACTIVE_TASK / SESSION_NOT_FOUND knows exactly what to do. +- `claimBeforeEditStats` adds a `pre_tool_use_signals` count of `claim-before-edit` telemetry rows in the window. `colony health` and `hivemind_context`'s claim-before-edit nudge use it to distinguish "hook is not firing" from "agent skipped the claim", and emit an install/restart hint in the former case. +- `colony health` also reports explicit/manual vs auto-claim breakdown and reads "had a claim before edit" instead of "explicit claims first". diff --git a/apps/cli/src/commands/health.ts b/apps/cli/src/commands/health.ts index 247f6cc..e2cb827 100644 --- a/apps/cli/src/commands/health.ts +++ b/apps/cli/src/commands/health.ts @@ -105,6 +105,15 @@ interface ClaimBeforeEditPayload extends ClaimBeforeEditStats { auto_claimed_before_edit: number; edits_without_claim_before: number; claim_before_edit_ratio: number | null; + /** Total claim-before-edit telemetry rows in window — non-zero means the + * PreToolUse hook is firing somewhere; zero with edits > 0 strongly + * suggests the hook is not wired into the active editor session. */ + pre_tool_use_signals: number; + /** True when edits happened but no PreToolUse telemetry was recorded — + * diagnostic that points at hook wiring rather than agent discipline. */ + likely_missing_hook: boolean; + /** User-facing remediation when likely_missing_hook is true. */ + install_hint: string | null; } interface SignalHealthPayload { @@ -606,12 +615,19 @@ function claimBeforeEditPayload( ): ClaimBeforeEditPayload { const editsWithoutClaimBefore = stats.edits_with_file_path - stats.edits_claimed_before; const autoClaimedBeforeEdit = stats.auto_claimed_before_edit ?? 0; + const preToolUseSignals = stats.pre_tool_use_signals ?? 0; const status = stats.edit_tool_calls === 0 ? 'no_data' : stats.edit_tool_calls === stats.edits_with_file_path ? 'available' : 'not_available'; + // If edits landed but no claim-before-edit observation was ever written, + // PreToolUse is almost certainly not firing for the active editor. + const likelyMissingHook = stats.edit_tool_calls > 0 && preToolUseSignals === 0; + const installHint = likelyMissingHook + ? 'PreToolUse auto-claim is not covering edits in this window. Run colony install --ide , restart the editor session, and ensure an active task is bound for the session.' + : null; return { ...stats, status, @@ -622,6 +638,9 @@ function claimBeforeEditPayload( edits_without_claim_before: editsWithoutClaimBefore, claim_before_edit_ratio: status === 'available' ? ratio(stats.edits_claimed_before, stats.edits_with_file_path) : null, + pre_tool_use_signals: preToolUseSignals, + likely_missing_hook: likelyMissingHook, + install_hint: installHint, }; } @@ -751,15 +770,24 @@ function formatClaimBeforeEdit(payload: ClaimBeforeEditPayload): string[] { ); return lines; } + const explicitClaims = Math.max( + payload.edits_claimed_before - payload.auto_claimed_before_edit, + 0, + ); lines.push( - ` ${payload.edits_claimed_before} / ${payload.edits_with_file_path} edits had explicit claims first (${formatPercent( + ` ${payload.edits_claimed_before} / ${payload.edits_with_file_path} edits had a claim before edit (${formatPercent( payload.claim_before_edit_ratio, )})`, ); + lines.push(` explicit/manual claims before edit: ${explicitClaims}`); + lines.push(` auto-claimed before edit: ${payload.auto_claimed_before_edit}`); lines.push(` missing proactive claim: ${payload.edits_without_claim_before}`); lines.push( - ` telemetry: edits_with_claim=${payload.edits_with_claim}, edits_missing_claim=${payload.edits_missing_claim}, auto_claimed_before_edit=${payload.auto_claimed_before_edit}`, + ` telemetry: edits_with_claim=${payload.edits_with_claim}, edits_missing_claim=${payload.edits_missing_claim}, auto_claimed_before_edit=${payload.auto_claimed_before_edit}, pre_tool_use_signals=${payload.pre_tool_use_signals}`, ); + if (payload.likely_missing_hook && payload.install_hint) { + lines.push(kleur.yellow(` ${payload.install_hint}`)); + } return lines; } @@ -819,17 +847,23 @@ function healthActionHints(payload: ColonyHealthPayloadWithoutHints): ActionHint TARGET_CLAIM_BEFORE_EDIT, ) ) { + const missingHook = payload.task_claim_file_before_edits.likely_missing_hook; hints.push({ metric: 'claim-before-edit', status: 'bad', current: formatPercent(payload.task_claim_file_before_edits.claim_before_edit_ratio), target: `${formatPercent(TARGET_CLAIM_BEFORE_EDIT)}+`, - action: 'Call task_claim_file for touched files before Edit or Write tool use.', + action: missingHook + ? 'PreToolUse auto-claim hook is not firing for these edits. Reinstall and restart the editor; PreToolUse will auto-claim before edits.' + : 'Call task_claim_file for touched files before Edit or Write tool use.', tool_call: 'mcp__colony__task_claim_file({ task_id: , session_id: "", file_path: "", note: "pre-edit claim" })', - command: 'colony install --ide # enables pre-edit auto-claim hooks', - prompt: - 'Before editing, call mcp__colony__task_claim_file for each touched path; if agents keep missing this, run colony install --ide to enable pre-edit auto-claim hooks.', + command: missingHook + ? 'colony install --ide # then restart the editor session' + : 'colony install --ide # enables pre-edit auto-claim hooks', + prompt: missingHook + ? 'PreToolUse auto-claim is not covering edits — run colony install --ide , restart the editor session, and ensure an active task is bound. Until the hook fires, call mcp__colony__task_claim_file before each edit.' + : 'Before editing, call mcp__colony__task_claim_file for each touched path; if agents keep missing this, run colony install --ide to enable pre-edit auto-claim hooks.', }); } diff --git a/apps/cli/src/commands/hook.ts b/apps/cli/src/commands/hook.ts index 9d353f9..d48c700 100644 --- a/apps/cli/src/commands/hook.ts +++ b/apps/cli/src/commands/hook.ts @@ -66,18 +66,33 @@ export function registerHookCommand(program: Command): void { } function writeIdeOutput(hook: HookName, result: HookResult): void { - // Only SessionStart and UserPromptSubmit can usefully feed text back into - // the agent. For other hooks we deliberately stay silent on stdout. - if (hook !== 'session-start' && hook !== 'user-prompt-submit') return; const ctx = result.context?.trim(); if (!ctx) return; - const payload = { - hookSpecificOutput: { - hookEventName: CLAUDE_EVENT_NAME[hook], - additionalContext: ctx, - }, - }; - process.stdout.write(`${JSON.stringify(payload)}\n`); + + if (hook === 'session-start' || hook === 'user-prompt-submit') { + const payload = { + hookSpecificOutput: { + hookEventName: CLAUDE_EVENT_NAME[hook], + additionalContext: ctx, + }, + }; + process.stdout.write(`${JSON.stringify(payload)}\n`); + return; + } + + // PreToolUse: surface the auto-claim warning to the agent via + // permissionDecisionReason while keeping the call allowed — we never block + // edits, we annotate so the agent learns the missing claim. + if (hook === 'pre-tool-use') { + const payload = { + hookSpecificOutput: { + hookEventName: CLAUDE_EVENT_NAME[hook], + permissionDecision: 'allow', + permissionDecisionReason: ctx, + }, + }; + process.stdout.write(`${JSON.stringify(payload)}\n`); + } } function safeJson(s: string): Record { diff --git a/apps/cli/test/health.test.ts b/apps/cli/test/health.test.ts index 7fbdf9f..7217eb8 100644 --- a/apps/cli/test/health.test.ts +++ b/apps/cli/test/health.test.ts @@ -214,7 +214,7 @@ describe('colony health payload', () => { expect(text).toContain('task_post vs task_message'); expect(text).toContain('task_post vs OMX notepad'); expect(text).toContain('Search calls per session'); - expect(text).toContain('1 / 2 edits had explicit claims first (50%)'); + expect(text).toContain('1 / 2 edits had a claim before edit (50%)'); expect(text).toContain('Signal health'); expect(text).toContain('Proposal decay/promotions'); expect(text).toContain('Ready-to-claim vs claimed'); @@ -526,10 +526,13 @@ describe('colony health payload', () => { metric: 'claim-before-edit', current: '0%', target: '50%+', - action: expect.stringContaining('task_claim_file'), + // Action wording branches on whether PreToolUse telemetry exists; both + // branches reference task_claim_file, but the missing-hook branch + // recommends reinstalling the hook before relying on agent discipline. + action: expect.stringContaining('PreToolUse auto-claim hook is not firing'), tool_call: expect.stringContaining('mcp__colony__task_claim_file'), command: expect.stringContaining('colony install --ide '), - prompt: expect.stringContaining('pre-edit auto-claim hooks'), + prompt: expect.stringContaining('PreToolUse auto-claim is not covering edits'), }), expect.objectContaining({ metric: 'stale claims', @@ -561,7 +564,9 @@ describe('colony health payload', () => { expect(text).toContain( 'task_ready_for_agent -> claim: 0% (target 30%+) - When ready work fits', ); - expect(text).toContain('claim-before-edit: 0% (target 50%+) - Call task_claim_file'); + expect(text).toContain( + 'claim-before-edit: 0% (target 50%+) - PreToolUse auto-claim hook is not firing', + ); expect(text).toContain( 'tool: mcp__colony__task_claim_file({ task_id: , session_id: "", file_path: "", note: "pre-edit claim" })', ); diff --git a/apps/mcp-server/src/tools/hivemind.ts b/apps/mcp-server/src/tools/hivemind.ts index 4ffd0c4..9b9dd05 100644 --- a/apps/mcp-server/src/tools/hivemind.ts +++ b/apps/mcp-server/src/tools/hivemind.ts @@ -330,11 +330,16 @@ function adoptionNudgesFromMetrics(store: MemoryStore, now: number): HivemindAdo claimBeforeEditRatio !== null && claimBeforeEditRatio < TARGET_CLAIM_BEFORE_EDIT ) { + const preToolUseSignals = claimStats.pre_tool_use_signals ?? 0; + const likelyMissingHook = + claimStats.edits_claimed_before === 0 && preToolUseSignals === 0; nudges.push({ key: 'claim_before_edit_low', tool: 'task_claim_file', - current: `claimed_before_edit=${claimStats.edits_claimed_before}/${claimStats.edits_with_file_path}`, - hint: 'Call task_claim_file for touched files before edit tools.', + current: `claimed_before_edit=${claimStats.edits_claimed_before}/${claimStats.edits_with_file_path}; pre_tool_use_signals=${preToolUseSignals}`, + hint: likelyMissingHook + ? 'PreToolUse auto-claim is not covering edits. Run colony install --ide , restart the editor, or call task_claim_file before editing.' + : 'Call task_claim_file for touched files before edit tools.', }); } } catch { diff --git a/apps/mcp-server/test/server.test.ts b/apps/mcp-server/test/server.test.ts index 721d241..6e3f136 100644 --- a/apps/mcp-server/test/server.test.ts +++ b/apps/mcp-server/test/server.test.ts @@ -440,7 +440,9 @@ describe('MCP server', () => { expect.objectContaining({ key: 'claim_before_edit_low', tool: 'task_claim_file', - current: 'claimed_before_edit=0/1', + // Compact diagnostic now appends pre_tool_use_signals so agents can + // distinguish missing-hook (=0) from missed-by-agent (>0) at a glance. + current: 'claimed_before_edit=0/1; pre_tool_use_signals=0', }), ]); expect(payload.summary.suggested_tools).toEqual([ diff --git a/packages/hooks/src/handlers/pre-tool-use.ts b/packages/hooks/src/handlers/pre-tool-use.ts index 26cc67e..05b3ceb 100644 --- a/packages/hooks/src/handlers/pre-tool-use.ts +++ b/packages/hooks/src/handlers/pre-tool-use.ts @@ -15,6 +15,8 @@ export interface ClaimBeforeEditFallbackWarning { code: AutoClaimFailureCode; message: string; next_tool: 'task_claim_file'; + /** Concrete invocation string the agent can copy verbatim. */ + next_call: string; suggested_args: { task_id: number | '' | ''; session_id: string; @@ -40,10 +42,10 @@ type CompactCandidate = Pick< >; export function preToolUse(store: MemoryStore, input: HookInput): string { + const toolName = input.tool_name ?? input.tool ?? ''; try { return claimBeforeEditWarning(claimBeforeEditFromToolUse(store, input)); } catch { - const toolName = input.tool_name ?? input.tool ?? ''; const files = extractTouchedFiles(toolName, input.tool_input); return claimBeforeEditWarning({ files, @@ -51,7 +53,7 @@ export function preToolUse(store: MemoryStore, input: HookInput): string { edits_missing_claim: files, auto_claimed_before_edit: [], warnings: files.map((file_path) => - claimWarning(input.session_id, file_path, { + claimWarning(input.session_id, file_path, toolName, { ok: false, code: 'COLONY_UNAVAILABLE', error: 'Colony unavailable for auto-claim', @@ -121,7 +123,8 @@ export function claimBeforeEditFromToolUse( error: claim.error, candidates: claim.candidates, }); - if (!warningDebounced) result.warnings.push(claimWarning(input.session_id, file_path, claim)); + if (!warningDebounced) + result.warnings.push(claimWarning(input.session_id, file_path, toolName, claim)); } return result; @@ -221,13 +224,34 @@ function claimBeforeEditWarning(result: ClaimBeforeEditResult): string { function claimWarning( session_id: string, file_path: string, + tool_name: string, claim: AutoClaimFailure, ): ClaimBeforeEditFallbackWarning { const candidates = compactCandidates(claim.candidates); + // Spell out the exact tool call the agent should make next. When there is + // exactly one candidate task we substitute its id; ambiguous candidates and + // no-candidate cases keep a placeholder so the agent picks consciously. + const taskRef = + candidates.length === 1 + ? String(candidates[0]?.task_id ?? '') + : candidates.length > 0 + ? '' + : ''; + const next_call = `mcp__colony__task_claim_file({ task_id: ${taskRef}, session_id: "${session_id}", file_path: "${file_path}", note: "pre-edit claim" })`; + const tool = tool_name || 'edit tool'; + const message = [ + `Missing Colony claim before ${tool} on ${file_path}.`, + `reason=${claim.code}: ${claim.error}`, + `next=${next_call}`, + candidates.length > 0 ? `candidates=${JSON.stringify(candidates)}` : '', + ] + .filter(Boolean) + .join('\n'); return { code: claim.code, - message: `Missing Colony claim before edit. Call task_claim_file for ${file_path} before editing.`, + message, next_tool: 'task_claim_file', + next_call, suggested_args: { task_id: candidates.length > 0 ? '' : '', session_id, diff --git a/packages/hooks/test/auto-claim.test.ts b/packages/hooks/test/auto-claim.test.ts index dfa3947..14600cb 100644 --- a/packages/hooks/test/auto-claim.test.ts +++ b/packages/hooks/test/auto-claim.test.ts @@ -405,12 +405,18 @@ describe('claimBeforeEditFromToolUse', () => { expect(result.auto_claimed_before_edit).toEqual([]); expect(result.edits_missing_claim).toEqual(['src/x.ts']); + const expectedNextCall = + 'mcp__colony__task_claim_file({ task_id: , session_id: "solo", file_path: "src/x.ts", note: "pre-edit claim" })'; expect(result.warnings).toEqual([ { code: 'ACTIVE_TASK_NOT_FOUND', - message: - 'Missing Colony claim before edit. Call task_claim_file for src/x.ts before editing.', + message: [ + 'Missing Colony claim before Edit on src/x.ts.', + 'reason=ACTIVE_TASK_NOT_FOUND: no active Colony task matched session/repo/branch', + `next=${expectedNextCall}`, + ].join('\n'), next_tool: 'task_claim_file', + next_call: expectedNextCall, suggested_args: { task_id: '', session_id: 'solo', @@ -451,9 +457,13 @@ describe('claimBeforeEditFromToolUse', () => { expect(result.warnings).toHaveLength(1); expect(result.warnings[0]).toMatchObject({ code: 'AMBIGUOUS_ACTIVE_TASK', - message: - 'Missing Colony claim before edit. Call task_claim_file for src/x.ts before editing.', + message: expect.stringContaining( + 'reason=AMBIGUOUS_ACTIVE_TASK: multiple active Colony tasks matched session/repo/branch', + ), next_tool: 'task_claim_file', + next_call: expect.stringContaining( + 'mcp__colony__task_claim_file({ task_id: , session_id: "A", file_path: "src/x.ts", note: "pre-edit claim" })', + ), suggested_args: { task_id: '', session_id: 'A', @@ -461,6 +471,8 @@ describe('claimBeforeEditFromToolUse', () => { note: 'pre-edit claim', }, }); + expect(result.warnings[0]?.message).toContain('Missing Colony claim before Edit on src/x.ts.'); + expect(result.warnings[0]?.message).toContain('candidates='); expect(result.warnings[0]?.candidates).toHaveLength(2); expect(result.warnings[0]?.candidates).toEqual( expect.arrayContaining([ @@ -503,12 +515,18 @@ describe('claimBeforeEditFromToolUse', () => { expect(result.auto_claimed_before_edit).toEqual([]); expect(result.edits_missing_claim).toEqual(['src/x.ts']); + const sessionNotFoundNextCall = + 'mcp__colony__task_claim_file({ task_id: , session_id: "missing", file_path: "src/x.ts", note: "pre-edit claim" })'; expect(result.warnings).toEqual([ { code: 'SESSION_NOT_FOUND', - message: - 'Missing Colony claim before edit. Call task_claim_file for src/x.ts before editing.', + message: [ + 'Missing Colony claim before Edit on src/x.ts.', + 'reason=SESSION_NOT_FOUND: Colony session missing was not found', + `next=${sessionNotFoundNextCall}`, + ].join('\n'), next_tool: 'task_claim_file', + next_call: sessionNotFoundNextCall, suggested_args: { task_id: '', session_id: 'missing', @@ -552,9 +570,10 @@ describe('claimBeforeEditFromToolUse', () => { expect(JSON.parse(context)).toMatchObject({ code: 'ACTIVE_TASK_NOT_FOUND', - message: - 'Missing Colony claim before edit. Call task_claim_file for src/x.ts before editing.', + message: expect.stringContaining('Missing Colony claim before Edit on src/x.ts.'), next_tool: 'task_claim_file', + next_call: + 'mcp__colony__task_claim_file({ task_id: , session_id: "json-session", file_path: "src/x.ts", note: "pre-edit claim" })', suggested_args: { task_id: '', session_id: 'json-session', @@ -579,11 +598,17 @@ describe('claimBeforeEditFromToolUse', () => { tool_input: { file_path: 'src/x.ts' }, }); + const unavailableNextCall = + 'mcp__colony__task_claim_file({ task_id: , session_id: "A", file_path: "src/x.ts", note: "pre-edit claim" })'; expect(JSON.parse(context)).toEqual({ code: 'COLONY_UNAVAILABLE', - message: - 'Missing Colony claim before edit. Call task_claim_file for src/x.ts before editing.', + message: [ + 'Missing Colony claim before Edit on src/x.ts.', + 'reason=COLONY_UNAVAILABLE: db unavailable', + `next=${unavailableNextCall}`, + ].join('\n'), next_tool: 'task_claim_file', + next_call: unavailableNextCall, suggested_args: { task_id: '', session_id: 'A', diff --git a/packages/installers/src/claude-code.ts b/packages/installers/src/claude-code.ts index d7ad981..aeaca8a 100644 --- a/packages/installers/src/claude-code.ts +++ b/packages/installers/src/claude-code.ts @@ -24,6 +24,16 @@ const HOOK_NAMES: Array<[string, string]> = [ ['SessionEnd', 'session-end'], ]; +// Scope tool-use hooks to the write-family tools that actually drive the +// auto-claim path. Bash is included because the auto-claim layer parses +// shell redirects (printf > foo, > bar) into file writes. +const FILE_WRITE_TOOL_MATCHER = 'Edit|Write|MultiEdit|NotebookEdit|Bash'; + +function matcherForHook(hookId: string): string | undefined { + if (hookId === 'pre-tool-use' || hookId === 'post-tool-use') return FILE_WRITE_TOOL_MATCHER; + return undefined; +} + function settingsFile(): string { return join(homedir(), '.claude', 'settings.json'); } @@ -39,9 +49,11 @@ function installColonyHook( hookId: string, ): NonNullable[string] { const filtered = removeColonyHook(existing, hookId); + const matcher = matcherForHook(hookId); return [ ...filtered, { + ...(matcher !== undefined ? { matcher } : {}), hooks: [ { type: 'command', diff --git a/packages/installers/test/installers.test.ts b/packages/installers/test/installers.test.ts index a6be8a5..c334862 100644 --- a/packages/installers/test/installers.test.ts +++ b/packages/installers/test/installers.test.ts @@ -64,7 +64,10 @@ describe('claude-code installer', () => { const settingsPath = join(home, '.claude', 'settings.json'); expect(existsSync(settingsPath)).toBe(true); const first = JSON.parse(readFileSync(settingsPath, 'utf8')) as { - hooks: Record }>>; + hooks: Record< + string, + Array<{ matcher?: string; hooks: Array<{ type: string; command: string }> }> + >; mcpServers: Record; }; expect(Object.keys(first.hooks).sort()).toEqual( @@ -80,9 +83,18 @@ describe('claude-code installer', () => { expect(first.hooks.SessionStart?.[0]?.hooks?.[0]?.command).toBe( `${ctx.nodeBin} ${ctx.cliPath} hook run session-start --ide claude-code`, ); + expect(first.hooks.SessionStart?.[0]?.matcher).toBeUndefined(); expect(first.hooks.PreToolUse?.[0]?.hooks?.[0]?.command).toBe( `${ctx.nodeBin} ${ctx.cliPath} hook run pre-tool-use --ide claude-code`, ); + // PreToolUse and PostToolUse run our auto-claim path; scope them to the + // tool calls that actually touch files so unrelated tools don't pay the + // hook cost and so claim-before-edit telemetry has clean coverage. + expect(first.hooks.PreToolUse?.[0]?.matcher).toBe('Edit|Write|MultiEdit|NotebookEdit|Bash'); + expect(first.hooks.PostToolUse?.[0]?.hooks?.[0]?.command).toBe( + `${ctx.nodeBin} ${ctx.cliPath} hook run post-tool-use --ide claude-code`, + ); + expect(first.hooks.PostToolUse?.[0]?.matcher).toBe('Edit|Write|MultiEdit|NotebookEdit|Bash'); expect(first.mcpServers.colony).toEqual({ command: ctx.nodeBin, args: [ctx.cliPath, 'mcp'], @@ -147,6 +159,7 @@ describe('claude-code installer', () => { hooks: [{ type: 'command', command: 'node /home/me/.claude/hooks/context.js' }], }, { + matcher: 'Edit|Write|MultiEdit|NotebookEdit|Bash', hooks: [ { type: 'command', diff --git a/packages/storage/src/storage.ts b/packages/storage/src/storage.ts index f28e662..16fad4c 100644 --- a/packages/storage/src/storage.ts +++ b/packages/storage/src/storage.ts @@ -129,6 +129,10 @@ export interface ClaimBeforeEditStats { edits_with_file_path: number; edits_claimed_before: number; auto_claimed_before_edit?: number; + /** Count of `claim-before-edit` telemetry observations in the window — any + * outcome (success, conflict, failure). Authoritative signal that the + * PreToolUse hook is firing at all in the active editor sessions. */ + pre_tool_use_signals?: number; } export interface ClaimCoverageSnapshot { @@ -1529,20 +1533,27 @@ export class Storage { AND c.kind = 'claim' AND json_extract(c.metadata, '$.source') = 'pre-tool-use' AND json_extract(c.metadata, '$.auto_claimed_before_edit') = 1 - ) AS auto_claimed_before_edit + ) AS auto_claimed_before_edit, + ( + SELECT COUNT(*) FROM observations c + WHERE c.ts > ? + AND c.kind = 'claim-before-edit' + ) AS pre_tool_use_signals FROM edit_rows`, ) - .get(FILE_EDIT_TOOLS_JSON, since_ts, since_ts) as { + .get(FILE_EDIT_TOOLS_JSON, since_ts, since_ts, since_ts) as { edit_tool_calls: number; edits_with_file_path: number | null; edits_claimed_before: number | null; auto_claimed_before_edit: number | null; + pre_tool_use_signals: number | null; }; return { edit_tool_calls: row.edit_tool_calls, edits_with_file_path: row.edits_with_file_path ?? 0, edits_claimed_before: row.edits_claimed_before ?? 0, auto_claimed_before_edit: row.auto_claimed_before_edit ?? 0, + pre_tool_use_signals: row.pre_tool_use_signals ?? 0, }; } diff --git a/packages/storage/test/coordination-activity.test.ts b/packages/storage/test/coordination-activity.test.ts index 88d9ecc..ee85d5e 100644 --- a/packages/storage/test/coordination-activity.test.ts +++ b/packages/storage/test/coordination-activity.test.ts @@ -151,6 +151,7 @@ describe('colony health read queries', () => { edits_with_file_path: 2, edits_claimed_before: 1, auto_claimed_before_edit: 0, + pre_tool_use_signals: 0, }); }); });