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.
- package/bin/run.js +1 -1
- package/dist/commands/clear.js +28 -131
- package/dist/commands/init/index.js +3 -3
- package/dist/lib/gitignore-manager.d.ts +32 -0
- package/dist/lib/gitignore-manager.js +141 -2
- package/dist/templates/CLAUDE.md +8 -8
- package/dist/templates/_shared/.claude/commands/handoff-resume.md +64 -0
- package/dist/templates/_shared/.claude/commands/handoff.md +16 -10
- package/dist/templates/_shared/.claude/settings.json +7 -7
- package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -0
- package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -0
- package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -0
- package/dist/templates/_shared/hooks-ts/file-suggestion.ts +130 -0
- package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -0
- package/dist/templates/_shared/hooks-ts/session_end.ts +107 -0
- package/dist/templates/_shared/hooks-ts/session_start.ts +144 -0
- package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -0
- package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -0
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +83 -0
- package/dist/templates/_shared/lib-ts/CLAUDE.md +318 -0
- package/dist/templates/_shared/lib-ts/base/atomic-write.ts +12 -12
- package/dist/templates/_shared/lib-ts/base/constants.ts +22 -15
- package/dist/templates/_shared/lib-ts/base/git-state.ts +1 -1
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +129 -50
- package/dist/templates/_shared/lib-ts/base/inference.ts +28 -21
- package/dist/templates/_shared/lib-ts/base/logger.ts +15 -2
- package/dist/templates/_shared/lib-ts/base/state-io.ts +9 -7
- package/dist/templates/_shared/lib-ts/base/stop-words.ts +131 -131
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +142 -0
- package/dist/templates/_shared/lib-ts/base/utils.ts +69 -69
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +30 -24
- package/dist/templates/_shared/lib-ts/context/context-selector.ts +50 -32
- package/dist/templates/_shared/lib-ts/context/context-store.ts +76 -48
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +43 -23
- package/dist/templates/_shared/lib-ts/context/task-tracker.ts +10 -6
- package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +11 -10
- package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +158 -0
- package/dist/templates/_shared/lib-ts/templates/formatters.ts +6 -4
- package/dist/templates/_shared/lib-ts/types.ts +68 -55
- package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
- package/dist/templates/_shared/scripts/resume_handoff.ts +345 -0
- package/dist/templates/_shared/scripts/save_handoff.ts +3 -3
- package/dist/templates/_shared/scripts/status_line.ts +687 -0
- package/dist/templates/cc-native/.claude/settings.json +175 -185
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +0 -2
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +1027 -0
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +792 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +115 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +120 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +250 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +275 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +107 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +240 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +385 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +14 -1
- package/oclif.manifest.json +1 -1
- package/package.json +2 -2
- package/dist/templates/_shared/hooks/__init__.py +0 -16
- package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/archive_plan.py +0 -177
- package/dist/templates/_shared/hooks/context_monitor.py +0 -270
- package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
- package/dist/templates/_shared/hooks/pre_compact.py +0 -104
- package/dist/templates/_shared/hooks/session_end.py +0 -173
- package/dist/templates/_shared/hooks/session_start.py +0 -206
- package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
- package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
- package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
- package/dist/templates/_shared/lib/__init__.py +0 -1
- package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__init__.py +0 -65
- package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
- package/dist/templates/_shared/lib/base/constants.py +0 -358
- package/dist/templates/_shared/lib/base/hook_utils.py +0 -339
- package/dist/templates/_shared/lib/base/inference.py +0 -307
- package/dist/templates/_shared/lib/base/logger.py +0 -305
- package/dist/templates/_shared/lib/base/stop_words.py +0 -221
- package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
- package/dist/templates/_shared/lib/base/utils.py +0 -263
- package/dist/templates/_shared/lib/context/__init__.py +0 -102
- package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/context_formatter.py +0 -317
- package/dist/templates/_shared/lib/context/context_selector.py +0 -508
- package/dist/templates/_shared/lib/context/context_store.py +0 -653
- package/dist/templates/_shared/lib/context/plan_manager.py +0 -303
- package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
- package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
- package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/document_generator.py +0 -278
- package/dist/templates/_shared/lib/templates/README.md +0 -206
- package/dist/templates/_shared/lib/templates/__init__.py +0 -36
- package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/formatters.py +0 -146
- package/dist/templates/_shared/lib/templates/plan_context.py +0 -73
- package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/save_handoff.py +0 -357
- package/dist/templates/_shared/scripts/status_line.py +0 -716
- package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
- package/dist/templates/cc-native/MIGRATION.md +0 -86
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -130
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -954
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
- package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/constants.py +0 -45
- package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +0 -215
- package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
- package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
- package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1071
- package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +0 -168
- package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +0 -134
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* SessionStart hook: Restore context after /clear (plan/handoff) or compaction.
|
|
4
|
+
* Routes by source field to appropriate handler.
|
|
5
|
+
*/
|
|
6
|
+
import { getProjectRoot } from "../lib-ts/base/constants.js";
|
|
7
|
+
import {
|
|
8
|
+
emitContext, loadHookInput, logDebug,
|
|
9
|
+
logDiagnostic, logError as _logError, logInfo, runHook,
|
|
10
|
+
} from "../lib-ts/base/hook-utils.js";
|
|
11
|
+
import {
|
|
12
|
+
buildRestoreSections, formatHandoffContinuation, getModeDisplay,
|
|
13
|
+
} from "../lib-ts/context/context-formatter.js";
|
|
14
|
+
import {
|
|
15
|
+
bindSession, getAllContexts, getContextBySessionId, updateMode,
|
|
16
|
+
} from "../lib-ts/context/context-store.js";
|
|
17
|
+
import type { ContextState as _ContextState } from "../lib-ts/types.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Handle post-compaction restore: re-inject context that was lost during compaction.
|
|
21
|
+
* Plan content is inlined because Claude doesn't auto-paste after compact.
|
|
22
|
+
*/
|
|
23
|
+
function handleCompactRestore(sessionId: string, projectRoot: string): void {
|
|
24
|
+
const state = getContextBySessionId(sessionId, projectRoot);
|
|
25
|
+
if (!state) {
|
|
26
|
+
logDebug("session_start", `No context for session ${sessionId} (compact)`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const sections: string[] = [
|
|
31
|
+
`## Resuming Context After Compaction: ${state.id}`,
|
|
32
|
+
"",
|
|
33
|
+
`**Summary:** ${state.summary}`,
|
|
34
|
+
`**Mode:** ${getModeDisplay(state.mode) || state.mode}`,
|
|
35
|
+
"",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// Inline plan = true (plan not auto-pasted after compact)
|
|
39
|
+
const restore = buildRestoreSections(state, projectRoot, true);
|
|
40
|
+
if (restore) sections.push(restore);
|
|
41
|
+
|
|
42
|
+
sections.push(
|
|
43
|
+
"",
|
|
44
|
+
"---",
|
|
45
|
+
"*Context was compacted to free up space. The above restores your working state.*",
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
emitContext(sections.join("\n"));
|
|
49
|
+
logInfo("session_start", `Compact restore for ${state.id}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Handle post-clear restore: find staged has_plan or has_handoff context,
|
|
54
|
+
* bind session, transition to active, inject context.
|
|
55
|
+
*/
|
|
56
|
+
function handleClearRestore(sessionId: string, projectRoot: string): void {
|
|
57
|
+
const allContexts = getAllContexts("active", projectRoot);
|
|
58
|
+
|
|
59
|
+
// Priority 1: has_plan contexts
|
|
60
|
+
const hasPlan = allContexts.filter(c => c.mode === "has_plan");
|
|
61
|
+
if (hasPlan.length > 0) {
|
|
62
|
+
// Pick most recently active (getAllContexts sorts by last_active desc)
|
|
63
|
+
const ctx = hasPlan[0]!;
|
|
64
|
+
|
|
65
|
+
bindSession(ctx.id, sessionId, projectRoot);
|
|
66
|
+
updateMode(ctx.id, "active", projectRoot, { plan_consumed: true });
|
|
67
|
+
|
|
68
|
+
logInfo("session_start", `Clear restore: ${ctx.id} has_plan → active (plan_consumed=true)`);
|
|
69
|
+
|
|
70
|
+
const sections: string[] = [
|
|
71
|
+
`## Resuming Context After Plan Clear: ${ctx.id}`,
|
|
72
|
+
"",
|
|
73
|
+
`**Summary:** ${ctx.summary}`,
|
|
74
|
+
`**Mode:** Active (Plan Restored)`,
|
|
75
|
+
"",
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
// inline_plan=false — Claude auto-pastes plan content after /clear
|
|
79
|
+
const restore = buildRestoreSections(ctx, projectRoot, false);
|
|
80
|
+
if (restore) sections.push(restore);
|
|
81
|
+
|
|
82
|
+
sections.push(
|
|
83
|
+
"",
|
|
84
|
+
"---",
|
|
85
|
+
"*Plan has been accepted. The plan content was auto-pasted above. Implement according to the plan.*",
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
emitContext(sections.join("\n"));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Priority 2: has_handoff contexts
|
|
93
|
+
const hasHandoff = allContexts.filter(c => c.mode === "has_handoff");
|
|
94
|
+
if (hasHandoff.length > 0) {
|
|
95
|
+
const ctx = hasHandoff[0]!;
|
|
96
|
+
|
|
97
|
+
bindSession(ctx.id, sessionId, projectRoot);
|
|
98
|
+
updateMode(ctx.id, "active", projectRoot, { handoff_consumed: true });
|
|
99
|
+
|
|
100
|
+
logInfo("session_start", `Clear restore: ${ctx.id} has_handoff → active (handoff_consumed=true)`);
|
|
101
|
+
|
|
102
|
+
const handoffContent = formatHandoffContinuation(ctx, projectRoot);
|
|
103
|
+
emitContext(handoffContent);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Nothing to restore
|
|
108
|
+
logDebug("session_start", "No has_plan or has_handoff contexts found");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function main(): void {
|
|
112
|
+
const payload = loadHookInput();
|
|
113
|
+
if (!payload) return;
|
|
114
|
+
|
|
115
|
+
const sessionId = payload.session_id;
|
|
116
|
+
if (!sessionId) {
|
|
117
|
+
logDebug("session_start", "No session_id");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const projectRoot = getProjectRoot(payload.cwd);
|
|
122
|
+
const source = payload.source ?? "";
|
|
123
|
+
|
|
124
|
+
logDiagnostic("session_start", "entry", `source=${source}, session=${sessionId}`);
|
|
125
|
+
|
|
126
|
+
switch (source) {
|
|
127
|
+
case "clear": {
|
|
128
|
+
handleClearRestore(sessionId, projectRoot);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case "compact": {
|
|
133
|
+
handleCompactRestore(sessionId, projectRoot);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
default: {
|
|
138
|
+
logDebug("session_start", `Unhandled source: ${source}`);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
runHook(main, "session_start");
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* PostToolUse:TaskCreate hook: Persist Claude's TaskCreate calls to state.json.
|
|
4
|
+
*/
|
|
5
|
+
import { getProjectRoot } from "../lib-ts/base/constants.js";
|
|
6
|
+
import {
|
|
7
|
+
checkSkipPersistence, getToolInput, loadHookInput, logDebug,
|
|
8
|
+
logError, logInfo, logWarn, runHook, validateHookEvent,
|
|
9
|
+
} from "../lib-ts/base/hook-utils.js";
|
|
10
|
+
import { getContextBySessionId } from "../lib-ts/context/context-store.js";
|
|
11
|
+
import { addTask } from "../lib-ts/context/task-tracker.js";
|
|
12
|
+
|
|
13
|
+
function main(): void {
|
|
14
|
+
const payload = loadHookInput();
|
|
15
|
+
if (!payload) return;
|
|
16
|
+
if (!validateHookEvent(payload, "PostToolUse", "TaskCreate")) return;
|
|
17
|
+
|
|
18
|
+
const toolInput = getToolInput(payload);
|
|
19
|
+
if (!toolInput) return;
|
|
20
|
+
if (checkSkipPersistence(payload, "task_create_capture")) return;
|
|
21
|
+
|
|
22
|
+
const projectRoot = getProjectRoot(payload.cwd);
|
|
23
|
+
const sessionId = payload.session_id ?? "unknown";
|
|
24
|
+
|
|
25
|
+
const state = getContextBySessionId(sessionId, projectRoot);
|
|
26
|
+
if (!state) {
|
|
27
|
+
logDebug("task_create_capture", `No context for session ${sessionId}`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const subject = toolInput.subject as string | undefined;
|
|
32
|
+
if (!subject) {
|
|
33
|
+
logWarn("task_create_capture", "TaskCreate missing subject field");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const description = (toolInput.description as string) ?? "";
|
|
38
|
+
const activeForm = (toolInput.activeForm as string) ?? "";
|
|
39
|
+
|
|
40
|
+
const task = addTask(state.id, subject, description, activeForm, sessionId, projectRoot);
|
|
41
|
+
if (task) {
|
|
42
|
+
logInfo("task_create_capture", `Persisted task ${task.id}: ${subject}`);
|
|
43
|
+
} else {
|
|
44
|
+
logError("task_create_capture", `Failed to persist task: ${subject}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
runHook(main, "task_create_capture");
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* PostToolUse:TaskUpdate hook: Persist Claude's TaskUpdate calls to state.json.
|
|
4
|
+
* Maps Claude's ephemeral task IDs to persistent aiw-N IDs.
|
|
5
|
+
*/
|
|
6
|
+
import { getProjectRoot } from "../lib-ts/base/constants.js";
|
|
7
|
+
import {
|
|
8
|
+
checkSkipPersistence, getToolInput, loadHookInput, logDebug,
|
|
9
|
+
logError as _logError, logInfo, logWarn, runHook, validateHookEvent,
|
|
10
|
+
} from "../lib-ts/base/hook-utils.js";
|
|
11
|
+
import { getContextBySessionId } from "../lib-ts/context/context-store.js";
|
|
12
|
+
import { deleteTask, updateTask } from "../lib-ts/context/task-tracker.js";
|
|
13
|
+
|
|
14
|
+
function main(): void {
|
|
15
|
+
const payload = loadHookInput();
|
|
16
|
+
if (!payload) return;
|
|
17
|
+
if (!validateHookEvent(payload, "PostToolUse", "TaskUpdate")) return;
|
|
18
|
+
|
|
19
|
+
const toolInput = getToolInput(payload);
|
|
20
|
+
if (!toolInput) return;
|
|
21
|
+
if (checkSkipPersistence(payload, "task_update_capture")) return;
|
|
22
|
+
|
|
23
|
+
const projectRoot = getProjectRoot(payload.cwd);
|
|
24
|
+
const sessionId = payload.session_id ?? "unknown";
|
|
25
|
+
|
|
26
|
+
const state = getContextBySessionId(sessionId, projectRoot);
|
|
27
|
+
if (!state) {
|
|
28
|
+
logDebug("task_update_capture", `No context for session ${sessionId}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const claudeTaskId = toolInput.taskId as string | undefined;
|
|
33
|
+
if (!claudeTaskId) {
|
|
34
|
+
logWarn("task_update_capture", "TaskUpdate missing taskId");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Map Claude's ephemeral ID to persistent ID
|
|
39
|
+
const metadata = (toolInput.metadata ?? {}) as Record<string, any>;
|
|
40
|
+
const persistentId = (metadata.persistent_id as string) ?? `aiw-${claudeTaskId}`;
|
|
41
|
+
|
|
42
|
+
const status = toolInput.status as string | undefined;
|
|
43
|
+
|
|
44
|
+
if (status === "deleted") {
|
|
45
|
+
const ok = deleteTask(state.id, persistentId, projectRoot);
|
|
46
|
+
if (ok) {
|
|
47
|
+
logInfo("task_update_capture", `Deleted task ${persistentId}`);
|
|
48
|
+
} else {
|
|
49
|
+
logWarn("task_update_capture", `Task ${persistentId} not found for deletion`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (status) {
|
|
56
|
+
const opts: Record<string, any> = { status };
|
|
57
|
+
if (metadata.evidence) opts.evidence = metadata.evidence;
|
|
58
|
+
if (metadata.work_summary) opts.work_summary = metadata.work_summary;
|
|
59
|
+
if (metadata.files_changed && Array.isArray(metadata.files_changed)) {
|
|
60
|
+
opts.files_changed = metadata.files_changed;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
opts.session_id = sessionId;
|
|
64
|
+
|
|
65
|
+
const ok = updateTask(state.id, persistentId, opts, projectRoot);
|
|
66
|
+
if (ok) {
|
|
67
|
+
logInfo("task_update_capture", `Updated task ${persistentId} → ${status}`);
|
|
68
|
+
} else {
|
|
69
|
+
logWarn("task_update_capture", `Task ${persistentId} not found for update`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
runHook(main, "task_update_capture");
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* UserPromptSubmit hook: Context enforcement — ensures every prompt belongs
|
|
4
|
+
* to a tracked context. The most complex shared hook.
|
|
5
|
+
*
|
|
6
|
+
* Uses emitContext() for output — context text is passed via hookSpecificOutput JSON.
|
|
7
|
+
* Catches BlockRequest and exits with code 2 to block the prompt.
|
|
8
|
+
*/
|
|
9
|
+
import { getProjectRoot } from "../lib-ts/base/constants.js";
|
|
10
|
+
import {
|
|
11
|
+
emitContext, hookLog, loadHookInput, logBlocking, logDebug, logDiagnostic as _logDiagnostic, logInfo, logWarn as _logWarn, runHookAsync,
|
|
12
|
+
} from "../lib-ts/base/hook-utils.js";
|
|
13
|
+
import { BlockRequest, determineContext } from "../lib-ts/context/context-selector.js";
|
|
14
|
+
import {
|
|
15
|
+
bindSession, getContextBySessionId, maybeActivate, saveState,
|
|
16
|
+
} from "../lib-ts/context/context-store.js";
|
|
17
|
+
|
|
18
|
+
async function asyncMain(): Promise<void> {
|
|
19
|
+
const payload = loadHookInput();
|
|
20
|
+
if (!payload) return;
|
|
21
|
+
|
|
22
|
+
const prompt = (payload as any).prompt as string | undefined;
|
|
23
|
+
const sessionId = payload.session_id;
|
|
24
|
+
const permissionMode = payload.permission_mode ?? "";
|
|
25
|
+
const projectRoot = getProjectRoot(payload.cwd);
|
|
26
|
+
|
|
27
|
+
if (!sessionId) {
|
|
28
|
+
logDebug("user_prompt_submit", "No session_id");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const outputs: string[] = [];
|
|
33
|
+
|
|
34
|
+
// Check if session is already bound to a context
|
|
35
|
+
const existingCtx = getContextBySessionId(sessionId, projectRoot);
|
|
36
|
+
|
|
37
|
+
if (existingCtx) {
|
|
38
|
+
// Returning user — context already bound (stderr: false to avoid "hook error" display)
|
|
39
|
+
try {
|
|
40
|
+
maybeActivate(existingCtx.id, permissionMode, projectRoot, "user_prompt_submit");
|
|
41
|
+
} catch (error) {
|
|
42
|
+
hookLog("warn", "user_prompt_submit", `maybeActivate failed (non-critical): ${error}`, { stderr: false });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
hookLog("debug", "user_prompt_submit", `Session bound to ${existingCtx.id}`, { stderr: false });
|
|
46
|
+
} else if (prompt) {
|
|
47
|
+
// First prompt — need to determine context
|
|
48
|
+
try {
|
|
49
|
+
const [contextId, method, outputText] = await determineContext(prompt, sessionId, projectRoot);
|
|
50
|
+
|
|
51
|
+
if (contextId) {
|
|
52
|
+
bindSession(contextId, sessionId, projectRoot);
|
|
53
|
+
maybeActivate(contextId, permissionMode, projectRoot, "user_prompt_submit");
|
|
54
|
+
|
|
55
|
+
// Clear handoff_path after binding (prevents re-injection)
|
|
56
|
+
const state = getContextBySessionId(sessionId, projectRoot);
|
|
57
|
+
if (state && state.handoff_path) {
|
|
58
|
+
state.handoff_path = null;
|
|
59
|
+
saveState(state.id, state, projectRoot);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
logInfo("user_prompt_submit", `Context ${contextId} via ${method}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (outputText) {
|
|
66
|
+
outputs.push(outputText);
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error instanceof BlockRequest) {
|
|
70
|
+
logBlocking("user_prompt_submit", (error as Error).message);
|
|
71
|
+
process.exit(2); // Block the prompt
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
throw error; // Re-throw unexpected errors
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (outputs.length > 0) {
|
|
79
|
+
emitContext(outputs.join("\n\n"));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
runHookAsync(asyncMain, "user_prompt_submit");
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# Shared TypeScript Library
|
|
2
|
+
|
|
3
|
+
**Location:** `_shared/lib-ts/` — cross-method infrastructure used by ALL templates.
|
|
4
|
+
|
|
5
|
+
**One import gets you started:**
|
|
6
|
+
```typescript
|
|
7
|
+
import { loadHookInput, runHook, logInfo, emitContext } from "../lib-ts/base/hook-utils.js";
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`hook-utils.ts` re-exports the most-used functions from `logger.ts`, `constants.ts`, and `context-store.ts`. Start here. Only import from deeper modules when you need specific capabilities.
|
|
11
|
+
|
|
12
|
+
**Import direction:** Hooks --> method lib --> `_shared/lib-ts/`. Never the reverse.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Critical Rules
|
|
17
|
+
|
|
18
|
+
These cause silent failures or UI noise when violated:
|
|
19
|
+
|
|
20
|
+
- **Entry point:** Every hook MUST use `runHook()` or `runHookAsync()` — never bare `main()` or `process.exit()`
|
|
21
|
+
- **stdout is sacred:** Only hook JSON output goes to stdout. Use logger functions for diagnostics, never `console.log()` or `print()`
|
|
22
|
+
- **stderr is opt-in:** `logDebug/logInfo/logWarn/logError` write to file only. Use `logBlocking()` when you NEED stderr visibility
|
|
23
|
+
- **Catch non-critical errors locally:** Uncaught errors bubble to `runHook` which writes to stderr, showing "hook error" in the UI even on exit 0
|
|
24
|
+
- **No reverse imports:** Never import from method lib (e.g., `_cc-native/lib/`) into shared lib
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Hook Skeleton
|
|
29
|
+
|
|
30
|
+
Copy this for new hooks:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
#!/usr/bin/env bun
|
|
34
|
+
import { loadHookInput, runHook, logDebug, logInfo, emitContext } from "../lib-ts/base/hook-utils.js";
|
|
35
|
+
|
|
36
|
+
function main(): void {
|
|
37
|
+
const payload = loadHookInput();
|
|
38
|
+
if (!payload) return;
|
|
39
|
+
|
|
40
|
+
const sessionId = payload.session_id;
|
|
41
|
+
if (!sessionId) return;
|
|
42
|
+
|
|
43
|
+
// Your hook logic here...
|
|
44
|
+
|
|
45
|
+
emitContext("Context visible to Claude");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
runHook(main, "my_hook_name");
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For async hooks (AI inference, network calls):
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { runHookAsync } from "../lib-ts/base/hook-utils.js";
|
|
55
|
+
|
|
56
|
+
async function asyncMain(): Promise<void> {
|
|
57
|
+
// await something...
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
runHookAsync(asyncMain, "my_async_hook");
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Logging
|
|
66
|
+
|
|
67
|
+
All logging goes to `_output/hook-log.jsonl`. stderr visibility is opt-in.
|
|
68
|
+
|
|
69
|
+
| Tier | Function | Visible in UI? | Use When |
|
|
70
|
+
|------|----------|---------------|----------|
|
|
71
|
+
| File-only | `logDebug()` / `logInfo()` / `logWarn()` / `logError()` | No | 99% of logging: diagnostics, state changes, non-critical errors |
|
|
72
|
+
| Blocking | `logBlocking()` | Yes (stderr) | The hook found a real problem the user or Claude must see |
|
|
73
|
+
| Unhandled | `logHookError()` | Yes (stderr) | Reserved for `runHook` crash handler — do not call directly |
|
|
74
|
+
| Terminal | `eprint()` | Yes (raw stderr) | Usage help, progress indicators — not logged to JSONL |
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { logDebug, logInfo, logWarn, logBlocking } from "../lib-ts/base/hook-utils.js";
|
|
78
|
+
|
|
79
|
+
logInfo("my_hook", "Session started"); // file only
|
|
80
|
+
logWarn("my_hook", `Fallback used: ${reason}`); // file only
|
|
81
|
+
logBlocking("my_hook", "Critical: state corrupt"); // shows in UI
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Hook Output — Three Communication Channels
|
|
87
|
+
|
|
88
|
+
Hooks have three channels back to the session. Pick the right one:
|
|
89
|
+
|
|
90
|
+
| Want to... | Function | Who sees it |
|
|
91
|
+
|------------|----------|-------------|
|
|
92
|
+
| Block tool + return message | `emitContextAndBlock(context, reason)` | Claude + user (denial reason prominent) |
|
|
93
|
+
| Return message, don't block | `emitContext(context)` | Claude + user (in transcript) |
|
|
94
|
+
| Log only (diagnostics) | `logInfo()` / `logWarn()` / etc. | Nobody in session — file only |
|
|
95
|
+
|
|
96
|
+
**There is no way to show something to the user but hide it from Claude, or vice versa.** Both `emitContext()` and `emitContextAndBlock()` produce output visible to both.
|
|
97
|
+
|
|
98
|
+
### Channel 1: Block + Context (PreToolUse only)
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
emitContextAndBlock(
|
|
102
|
+
"Detailed feedback Claude sees", // additionalContext
|
|
103
|
+
"Short reason for the block" // permissionDecisionReason
|
|
104
|
+
);
|
|
105
|
+
// No SystemExit needed — permissionDecision:"deny" with exit 0 is sufficient.
|
|
106
|
+
// runHookAsync drains stdout before exit to ensure pipe consumers receive the JSON.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The tool call is **prevented from executing**. Only works for PreToolUse hooks.
|
|
110
|
+
|
|
111
|
+
### Channel 2: Non-blocking Context (any hook event)
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
emitContext("Information added to Claude's context");
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The tool call / session continues normally. Works for PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Notification, SubagentStart.
|
|
118
|
+
|
|
119
|
+
### Channel 3: Log-only (diagnostics)
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
logInfo("my_hook", "Processing started"); // File only
|
|
123
|
+
logWarn("my_hook", `Fallback used: ${why}`); // File only
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Nobody in the session sees this. Written to `_output/hook-log.jsonl` for debugging.
|
|
127
|
+
|
|
128
|
+
### Hook Output Logging
|
|
129
|
+
|
|
130
|
+
Both `emitContext()` and `emitContextAndBlock()` automatically log their output to `_output/hook-log.jsonl` as `HOOK_OUTPUT` entries. This captures exactly what was sent to Claude via stdout, closing the visibility gap where the agent sees injected context the user doesn't.
|
|
131
|
+
|
|
132
|
+
- **Log level:** `info` — visible unless `HOOK_LOG_LEVEL=warn`
|
|
133
|
+
- **`msg` field:** Scannable summary: `HOOK_OUTPUT [context] 842 chars` or `HOOK_OUTPUT [block] 340 chars, reason="..."`
|
|
134
|
+
- **`data` field:** Full payload including `additionalContext` and `blockReason` (for blocks)
|
|
135
|
+
- **Controlled by:** Existing `HOOK_LOG_LEVEL` and `HOOK_LOG_DISABLE` env vars
|
|
136
|
+
- **No hook changes needed:** Logging happens inside the emit functions themselves
|
|
137
|
+
|
|
138
|
+
### Exit codes and JSON
|
|
139
|
+
|
|
140
|
+
| Exit Code | JSON Parsed? | Effect |
|
|
141
|
+
|-----------|-------------|--------|
|
|
142
|
+
| **0** | Yes | Normal — `hookSpecificOutput` processed |
|
|
143
|
+
| **2** | No | Blocking error — JSON ignored, stderr fed to Claude |
|
|
144
|
+
| **Other** | No | Non-blocking error — stderr shown in verbose mode |
|
|
145
|
+
|
|
146
|
+
You cannot mix exit 2 with JSON decisions. Pick one: exit 0 + JSON, or exit 2 + stderr.
|
|
147
|
+
|
|
148
|
+
### hookSpecificOutput fields by event type
|
|
149
|
+
|
|
150
|
+
| Event | `additionalContext` | `permissionDecision` | `permissionDecisionReason` | Other |
|
|
151
|
+
|-------|:--:|:--:|:--:|-------|
|
|
152
|
+
| **PreToolUse** | Y | Y (allow/deny/ask) | Y | `updatedInput` |
|
|
153
|
+
| **PostToolUse** | Y | - | - | `updatedMCPToolOutput` (MCP only) |
|
|
154
|
+
| **UserPromptSubmit** | Y | - | - | top-level `decision: "block"` |
|
|
155
|
+
| **SessionStart** | Y | - | - | — |
|
|
156
|
+
| **Notification** | Y | - | - | — |
|
|
157
|
+
| **SubagentStart** | Y | - | - | — |
|
|
158
|
+
| **Stop** | - | - | - | top-level `decision`, `reason` |
|
|
159
|
+
| **SessionEnd** | - | - | - | — |
|
|
160
|
+
|
|
161
|
+
**Invalid fields cause silent rejection of the entire output.** No error, no feedback. Conversely, **missing `hookEventName` also causes silent rejection** — see "Hook API: Critical Learnings" below.
|
|
162
|
+
|
|
163
|
+
### Special case: fileSuggestion
|
|
164
|
+
|
|
165
|
+
The `fileSuggestion` settings command is NOT a hook — it uses a different protocol. It outputs a plain JSON array to stdout (e.g., `console.log(JSON.stringify(paths))`). Do not use `emitContext()` for fileSuggestion.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Hook API: Critical Learnings (Verified 2026-02-11)
|
|
170
|
+
|
|
171
|
+
These findings were verified through systematic testing. They document Claude Code's actual behavior, which sometimes differs from what the docs suggest.
|
|
172
|
+
|
|
173
|
+
### hookEventName is REQUIRED (CC 2.1.39+)
|
|
174
|
+
|
|
175
|
+
Claude Code validates `hookSpecificOutput` using a Zod discriminated union keyed on `hookEventName`. If this field is missing:
|
|
176
|
+
|
|
177
|
+
- The entire hook output is silently rejected — no error, no feedback
|
|
178
|
+
- `permissionDecision: "deny"` is never processed
|
|
179
|
+
- The hook appears to "not work" even though it runs successfully
|
|
180
|
+
|
|
181
|
+
**You don't need to handle this manually.** `emitContext()` and `emitContextAndBlock()` auto-detect `hookEventName` from the stdin payload (via `_lastHookEvent`, set by `loadHookInput()`/`runHook()`). This works because hooks are synchronous single-process executions — each `bun` process has its own memory, so there's no concurrency risk between sessions.
|
|
182
|
+
|
|
183
|
+
**If auto-detection fails** (e.g., `loadHookInput()` wasn't called), `hookEventName` is omitted and the output will be silently rejected. This is why `runHook()`/`runHookAsync()` is mandatory — it calls `_earlyReadInput()` first, guaranteeing `_lastHookEvent` is populated.
|
|
184
|
+
|
|
185
|
+
### Exit Code Behavior (Tested)
|
|
186
|
+
|
|
187
|
+
| Exit Code | JSON Parsed? | Blocks Tool? | What Claude Sees | Tested? |
|
|
188
|
+
|-----------|-------------|-------------|------------------|---------|
|
|
189
|
+
| **0** + deny JSON | Yes | Yes (PreToolUse only) | `additionalContext` + denial reason | Yes |
|
|
190
|
+
| **0** + context JSON | Yes | No | `additionalContext` in transcript | Yes |
|
|
191
|
+
| **1** | No | No | stderr in verbose mode only | Yes |
|
|
192
|
+
| **2** | No | Yes (any event) | stderr fed as system-reminder | Yes |
|
|
193
|
+
|
|
194
|
+
**Key insight:** Exit 0 + `permissionDecision: "deny"` is the correct way to block a tool. Exit 2 is a blunt instrument — it ignores your JSON and feeds raw stderr to Claude. Use exit 0 + deny for clean blocking with structured feedback.
|
|
195
|
+
|
|
196
|
+
### ExitPlanMode: Not Special-Cased
|
|
197
|
+
|
|
198
|
+
Early testing suggested ExitPlanMode was "immune" to PreToolUse deny. **This was wrong.** The actual issue was missing `hookEventName` — the Zod validator silently rejected the deny output.
|
|
199
|
+
|
|
200
|
+
**With `hookEventName` included:**
|
|
201
|
+
- PreToolUse `permissionDecision: "deny"` (exit 0) → **blocks ExitPlanMode**, no dialog appears, session stays in plan mode
|
|
202
|
+
- `emitContextAndBlock()` handles this automatically via auto-detection
|
|
203
|
+
|
|
204
|
+
**Without `hookEventName` (the bug):**
|
|
205
|
+
- Deny silently rejected → dialog appeared → looked like ExitPlanMode was special-cased
|
|
206
|
+
- Exit 2 also appeared to "not work" for PreToolUse (JSON was ignored as expected, but the blocking was via stderr, not deny)
|
|
207
|
+
- PostToolUse with exit 2 appeared to work because it used stderr (not JSON), bypassing the Zod issue
|
|
208
|
+
|
|
209
|
+
**Lesson:** When a hook output seems to be "silently ignored," check the JSON schema first. The Zod validator rejects malformed output without any error message.
|
|
210
|
+
|
|
211
|
+
### Debugging Checklist
|
|
212
|
+
|
|
213
|
+
When a hook's deny/context isn't working:
|
|
214
|
+
|
|
215
|
+
1. **Is `hookEventName` in the JSON output?** Check `_output/hook-log.jsonl` for `HOOK_OUTPUT` entries
|
|
216
|
+
2. **Is the hook using `runHook()`/`runHookAsync()`?** Required for auto-detection
|
|
217
|
+
3. **Is `loadHookInput()` called before `emitContext()`?** It populates `_lastHookEvent`
|
|
218
|
+
4. **Is the exit code 0?** Exit 1/2 cause JSON to be ignored
|
|
219
|
+
5. **Are there extra fields in `hookSpecificOutput`?** Invalid fields cause silent rejection of the entire output
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Context Store
|
|
224
|
+
|
|
225
|
+
2-layer CRUD: per-context `state.json` + global `_output/index.json`.
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import { getContextBySessionId, bindSession, maybeActivate, saveState } from "../lib-ts/context/context-store.js";
|
|
229
|
+
|
|
230
|
+
const state = getContextBySessionId(sessionId, projectRoot);
|
|
231
|
+
if (state) {
|
|
232
|
+
// ALWAYS wrap non-critical operations — uncaught errors become UI "hook error"
|
|
233
|
+
try {
|
|
234
|
+
maybeActivate(state.id, permissionMode, projectRoot, "hook_name");
|
|
235
|
+
} catch (e) {
|
|
236
|
+
logWarn("hook_name", `maybeActivate failed (non-critical): ${e}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Valid modes:** `idle` | `has_plan` | `has_handoff` | `active`
|
|
242
|
+
|
|
243
|
+
Transitions: `idle`/`has_plan`/`has_handoff` --> `active` (via `maybeActivate`). `active` --> `has_plan`/`has_handoff` (via `session_end`).
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Module Reference
|
|
248
|
+
|
|
249
|
+
Use this table to find the right file. Read the source for full API details.
|
|
250
|
+
|
|
251
|
+
### `base/` — Core Infrastructure
|
|
252
|
+
|
|
253
|
+
| File | Purpose | Key Exports |
|
|
254
|
+
|------|---------|-------------|
|
|
255
|
+
| `hook-utils.ts` | Hook lifecycle, stdin parsing, output emit, re-exports | `runHook`, `runHookAsync`, `loadHookInput`, `emitContext`, `emitContextAndBlock`, `logDebug`...`logBlocking` |
|
|
256
|
+
| `logger.ts` | JSONL logging engine | `hookLog`, `logDebug`, `logInfo`, `logWarn`, `logError`, `logBlocking`, `logHookError`, `logDiagnostic` |
|
|
257
|
+
| `constants.ts` | Path resolution, limits | `getProjectRoot()`, `getContextDir()`, `MAX_FILE_SIZE` |
|
|
258
|
+
| `atomic-write.ts` | Crash-safe file writes | `atomicWriteFileSync()` |
|
|
259
|
+
| `state-io.ts` | State serialization with mode migration | `readState()`, `writeState()` |
|
|
260
|
+
| `inference.ts` | Claude CLI subprocess calls | `inferText()` |
|
|
261
|
+
| `utils.ts` | Formatting, ID generation | `nowIso()`, `generateContextId()`, `slugify()` |
|
|
262
|
+
| `git-state.ts` | Git snapshot | `captureGitState()` |
|
|
263
|
+
| `subprocess-utils.ts` | Recursive call guard | `isInternalCall()` |
|
|
264
|
+
| `stop-words.ts` | Word list for ID generation | Used by `utils.ts` internally |
|
|
265
|
+
|
|
266
|
+
### `context/` — Context State Management
|
|
267
|
+
|
|
268
|
+
| File | Purpose | Key Exports |
|
|
269
|
+
|------|---------|-------------|
|
|
270
|
+
| `context-store.ts` | CRUD for context state + index | `getContextBySessionId`, `bindSession`, `maybeActivate`, `saveState`, `createContext` |
|
|
271
|
+
| `context-selector.ts` | Route prompts to contexts | `determineContext()`, `BlockRequest` |
|
|
272
|
+
| `context-formatter.ts` | Display formatting | `formatContextSummary()` |
|
|
273
|
+
| `plan-manager.ts` | Plan lifecycle (archive, hash, sign) | `archivePlan()`, `computePlanHash()` |
|
|
274
|
+
| `task-tracker.ts` | Task CRUD on state.json | `addTask()`, `updateTask()`, `getTasks()` |
|
|
275
|
+
|
|
276
|
+
### `handoff/` and `templates/`
|
|
277
|
+
|
|
278
|
+
| File | Purpose | Key Exports |
|
|
279
|
+
|------|---------|-------------|
|
|
280
|
+
| `handoff/document-generator.ts` | Handoff document generation | `generateHandoffDocument()` |
|
|
281
|
+
| `templates/formatters.ts` | Display constants, mode maps, icons | `MODE_MAP`, `STATUS_ICONS` |
|
|
282
|
+
| `templates/plan-context.ts` | Plan evaluation templates | `PLAN_EVALUATION_REMINDER` |
|
|
283
|
+
|
|
284
|
+
### Root
|
|
285
|
+
|
|
286
|
+
| File | Purpose |
|
|
287
|
+
|------|---------|
|
|
288
|
+
| `types.ts` | All shared types: `Mode`, `ContextState`, `Task`, `HookInput`, `HookOutput` |
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Shared Hooks (`_shared/hooks-ts/`)
|
|
293
|
+
|
|
294
|
+
These run for ALL templates. Method-specific hooks live in `_{method}/hooks/`.
|
|
295
|
+
|
|
296
|
+
| Hook | Event | Purpose |
|
|
297
|
+
|------|-------|---------|
|
|
298
|
+
| `user_prompt_submit.ts` | UserPromptSubmit | Context enforcement — binds prompts to tracked contexts |
|
|
299
|
+
| `context_monitor.ts` | PostToolUse:* | Context window tracking, handoff warnings at 30/20/10% |
|
|
300
|
+
| `session_start.ts` | SessionStart | Restores plan/handoff context after `/clear` or compaction |
|
|
301
|
+
| `session_end.ts` | SessionEnd | Stages `active` --> `has_plan`/`has_handoff` for next session |
|
|
302
|
+
| `archive_plan.ts` | PreToolUse:ExitPlanMode | Archives plan file before accept/reject decision |
|
|
303
|
+
| `pre_compact.ts` | PreToolUse:Compact | Pre-compaction state snapshot |
|
|
304
|
+
| `task_create_capture.ts` | PostToolUse:TaskCreate | Persists task creation to context state |
|
|
305
|
+
| `task_update_capture.ts` | PostToolUse:TaskUpdate | Persists task updates to context state |
|
|
306
|
+
| `file-suggestion.ts` | PostToolUse:Write | Suggests file organization improvements |
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Environment Variables
|
|
311
|
+
|
|
312
|
+
| Variable | Effect |
|
|
313
|
+
|----------|--------|
|
|
314
|
+
| `CLAUDE_PROJECT_DIR` | Override project root detection |
|
|
315
|
+
| `HOOK_LOG_DISABLE=1` | Disable all file logging |
|
|
316
|
+
| `HOOK_LOG_LEVEL=warn` | Minimum log level (default: `debug`) |
|
|
317
|
+
| `HOOK_ERROR_LOG_DISABLE=1` | Legacy alias for `HOOK_LOG_DISABLE` |
|
|
318
|
+
| `_CC_INTERNAL=1` | Marks subprocess calls (checked by `isInternalCall()`) |
|