@haposoft/cafekit 0.3.11 → 0.4.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.
Files changed (36) hide show
  1. package/README.md +83 -28
  2. package/bin/install.js +125 -1
  3. package/package.json +5 -3
  4. package/src/claude/hooks/agent.cjs +203 -0
  5. package/src/claude/hooks/lib/color.cjs +95 -0
  6. package/src/claude/hooks/lib/config.cjs +831 -0
  7. package/src/claude/hooks/lib/context.cjs +616 -0
  8. package/src/claude/hooks/lib/counter.cjs +103 -0
  9. package/src/claude/hooks/lib/detect.cjs +474 -0
  10. package/src/claude/hooks/lib/git.cjs +143 -0
  11. package/src/claude/hooks/lib/parser.cjs +182 -0
  12. package/src/claude/hooks/session.cjs +360 -0
  13. package/src/claude/hooks/usage.cjs +179 -0
  14. package/src/claude/migration-manifest.json +27 -2
  15. package/src/claude/settings/status.settings.json +54 -0
  16. package/src/claude/status.cjs +539 -0
  17. package/src/common/skills/code/SKILL.md +55 -0
  18. package/src/common/skills/code/references/execution-loop.md +21 -0
  19. package/src/common/skills/impact-analysis/references/change-detection.md +16 -16
  20. package/src/common/skills/impact-analysis/references/dependency-scouting.md +8 -8
  21. package/src/common/skills/impact-analysis/references/edge-case-identification.md +11 -11
  22. package/src/common/skills/impact-analysis/references/industry-techniques.md +36 -36
  23. package/src/common/skills/impact-analysis/references/practical-techniques-guide.md +16 -16
  24. package/src/common/skills/impact-analysis/references/project-detection.md +1 -1
  25. package/src/common/skills/impact-analysis/references/report-template.md +2 -2
  26. package/src/common/skills/impact-analysis/scripts/README.md +3 -3
  27. package/src/common/skills/review/SKILL.md +46 -0
  28. package/src/common/skills/review/references/review-focus.md +28 -0
  29. package/src/common/skills/spec-design/SKILL.md +66 -0
  30. package/src/common/skills/spec-design/references/design-discovery.md +46 -0
  31. package/src/common/skills/spec-init/SKILL.md +61 -0
  32. package/src/common/skills/spec-requirements/SKILL.md +59 -0
  33. package/src/common/skills/spec-requirements/references/requirements-workflow.md +36 -0
  34. package/src/common/skills/spec-tasks/SKILL.md +60 -0
  35. package/src/common/skills/spec-tasks/references/task-sizing.md +36 -0
  36. package/src/common/skills/test/SKILL.md +40 -0
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Git Info Cache - Cross-platform git information batching
6
+ *
7
+ * Problem: 5-6 git process spawns per statusline render are slow on Windows (CreateProcess overhead)
8
+ * Solution: Cache git query results for 3 seconds — subsequent renders read cache (zero processes)
9
+ *
10
+ * Performance: 5 spawns per render → event-driven refresh + 30s TTL fallback
11
+ * Cross-platform: No bash-only syntax (no 2>/dev/null), windowsHide on all exec calls
12
+ */
13
+
14
+ const { execSync } = require('child_process');
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const os = require('os');
18
+
19
+ // Cache TTL — long fallback for external changes (git checkout outside Claude)
20
+ // Active invalidation happens via PostToolUse hooks after Edit/Write/Bash
21
+ const CACHE_TTL = 30000;
22
+ const CACHE_MISS = Symbol('cache_miss');
23
+
24
+ /**
25
+ * Safe command execution wrapper with optional cwd
26
+ */
27
+ function execIn(cmd, cwd) {
28
+ try {
29
+ return execSync(cmd, {
30
+ encoding: 'utf8',
31
+ stdio: ['pipe', 'pipe', 'ignore'],
32
+ windowsHide: true,
33
+ cwd: cwd || undefined
34
+ }).trim();
35
+ } catch {
36
+ return '';
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Get cache file path for current working directory
42
+ */
43
+ function getCachePath(cwd) {
44
+ const hash = require('crypto')
45
+ .createHash('md5')
46
+ .update(cwd)
47
+ .digest('hex')
48
+ .slice(0, 8);
49
+ return path.join(os.tmpdir(), `ck-git-cache-${hash}.json`);
50
+ }
51
+
52
+ /**
53
+ * Read cache if valid (not expired). Returns CACHE_MISS on miss.
54
+ * No existsSync check (TOCTOU race) — just try read and catch.
55
+ */
56
+ function readCache(cachePath) {
57
+ try {
58
+ const cache = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
59
+ if (Date.now() - cache.timestamp < CACHE_TTL) {
60
+ return cache.data; // Can be null (non-git dir) or object (git info)
61
+ }
62
+ } catch {
63
+ // File missing, corrupted, or expired — all treated as cache miss
64
+ }
65
+ return CACHE_MISS;
66
+ }
67
+
68
+ /**
69
+ * Write cache atomically (temp file + rename to avoid partial reads on Windows)
70
+ */
71
+ function writeCache(cachePath, data) {
72
+ const tmpPath = cachePath + '.tmp';
73
+ try {
74
+ fs.writeFileSync(tmpPath, JSON.stringify({ timestamp: Date.now(), data }));
75
+ fs.renameSync(tmpPath, cachePath);
76
+ } catch {
77
+ try { fs.unlinkSync(tmpPath); } catch {}
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Count non-empty lines in a newline-delimited string
83
+ */
84
+ function countLines(str) {
85
+ if (!str) return 0;
86
+ return str.split('\n').filter(l => l.trim()).length;
87
+ }
88
+
89
+ /**
90
+ * Fetch git info directly in-process
91
+ * The cache is what eliminates redundant spawns — not subprocess wrapping
92
+ * @param {string} cwd - Directory to run git commands in
93
+ * Returns: { branch, unstaged, staged, ahead, behind } or null if not git repo
94
+ */
95
+ function fetchGitInfo(cwd) {
96
+ // Check if git repo (fast check) — run in target cwd, not process.cwd()
97
+ if (!execIn('git rev-parse --git-dir', cwd)) {
98
+ return null;
99
+ }
100
+
101
+ const branch = execIn('git branch --show-current', cwd) || execIn('git rev-parse --short HEAD', cwd);
102
+ const unstaged = countLines(execIn('git diff --name-only', cwd));
103
+ const staged = countLines(execIn('git diff --cached --name-only', cwd));
104
+
105
+ // Ahead/behind — no 2>/dev/null (invalid on Windows cmd.exe)
106
+ let ahead = 0, behind = 0;
107
+ const aheadBehind = execIn('git rev-list --left-right --count @{u}...HEAD', cwd);
108
+ if (aheadBehind) {
109
+ const parts = aheadBehind.split(/\s+/);
110
+ behind = parseInt(parts[0], 10) || 0;
111
+ ahead = parseInt(parts[1], 10) || 0;
112
+ }
113
+
114
+ return { branch, unstaged, staged, ahead, behind };
115
+ }
116
+
117
+ /**
118
+ * Get git info with caching
119
+ * Main export function used by statusline
120
+ */
121
+ function getGitInfo(cwd = process.cwd()) {
122
+ const cachePath = getCachePath(cwd);
123
+
124
+ // Try cache first (includes cached null for non-git dirs)
125
+ const cached = readCache(cachePath);
126
+ if (cached !== CACHE_MISS) return cached;
127
+
128
+ // Cache miss or expired, fetch fresh data in target cwd
129
+ const data = fetchGitInfo(cwd);
130
+ // Cache both positive and null results (avoids re-spawning git in non-git dirs)
131
+ writeCache(cachePath, data);
132
+
133
+ return data;
134
+ }
135
+
136
+ /**
137
+ * Invalidate cache for a directory (call after file changes to trigger fresh git query)
138
+ */
139
+ function invalidateCache(cwd = process.cwd()) {
140
+ try { fs.unlinkSync(getCachePath(cwd)); } catch {}
141
+ }
142
+
143
+ module.exports = { getGitInfo, invalidateCache };
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Transcript Parser - Extract tool/agent/todo state from session JSONL
6
+ * @module transcript-parser
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const readline = require('readline');
11
+
12
+ /**
13
+ * Parse transcript JSONL file
14
+ * @param {string} transcriptPath - Path to transcript file
15
+ * @returns {Promise<TranscriptData>}
16
+ */
17
+ async function parseTranscript(transcriptPath) {
18
+ const result = {
19
+ tools: [],
20
+ agents: [],
21
+ todos: [],
22
+ sessionStart: null
23
+ };
24
+
25
+ if (!transcriptPath || !fs.existsSync(transcriptPath)) {
26
+ return result;
27
+ }
28
+
29
+ const toolMap = new Map();
30
+ const agentMap = new Map();
31
+ let latestTodos = [];
32
+
33
+ try {
34
+ const fileStream = fs.createReadStream(transcriptPath);
35
+ const rl = readline.createInterface({
36
+ input: fileStream,
37
+ crlfDelay: Infinity
38
+ });
39
+
40
+ for await (const line of rl) {
41
+ if (!line.trim()) continue;
42
+
43
+ try {
44
+ const entry = JSON.parse(line);
45
+ processEntry(entry, toolMap, agentMap, latestTodos, result);
46
+ } catch {
47
+ // Skip malformed lines
48
+ }
49
+ }
50
+ } catch {
51
+ // Return partial results on error
52
+ }
53
+
54
+ result.tools = Array.from(toolMap.values()).slice(-20);
55
+ result.agents = Array.from(agentMap.values()).slice(-10);
56
+ result.todos = latestTodos;
57
+
58
+ return result;
59
+ }
60
+
61
+ /**
62
+ * Process single JSONL entry
63
+ * @param {Object} entry - Parsed JSON line
64
+ * @param {Map} toolMap - Tool tracking map
65
+ * @param {Map} agentMap - Agent tracking map
66
+ * @param {Array} latestTodos - Latest todo array reference
67
+ * @param {Object} result - Result object
68
+ */
69
+ function processEntry(entry, toolMap, agentMap, latestTodos, result) {
70
+ const timestamp = entry.timestamp ? new Date(entry.timestamp) : new Date();
71
+
72
+ // Track session start
73
+ if (!result.sessionStart && entry.timestamp) {
74
+ result.sessionStart = timestamp;
75
+ }
76
+
77
+ const content = entry.message?.content;
78
+ if (!content || !Array.isArray(content)) return;
79
+
80
+ for (const block of content) {
81
+ // Handle tool_use blocks
82
+ if (block.type === 'tool_use' && block.id && block.name) {
83
+ if (block.name === 'Task') {
84
+ // Agent spawn
85
+ agentMap.set(block.id, {
86
+ id: block.id,
87
+ type: block.input?.subagent_type ?? 'unknown',
88
+ model: block.input?.model ?? null,
89
+ description: block.input?.description ?? null,
90
+ status: 'running',
91
+ startTime: timestamp,
92
+ endTime: null
93
+ });
94
+ } else if (block.name === 'TodoWrite') {
95
+ // Legacy: Replace todo array (deprecated, kept for backwards compatibility)
96
+ if (block.input?.todos && Array.isArray(block.input.todos)) {
97
+ latestTodos.length = 0;
98
+ latestTodos.push(...block.input.todos);
99
+ }
100
+ } else if (block.name === 'TaskCreate') {
101
+ // Native Task API: Add new task
102
+ if (block.input?.subject) {
103
+ latestTodos.push({
104
+ id: block.id,
105
+ content: block.input.subject,
106
+ status: 'pending',
107
+ activeForm: block.input.activeForm || null
108
+ });
109
+ }
110
+ } else if (block.name === 'TaskUpdate') {
111
+ // Native Task API: Update existing task status
112
+ if (block.input?.taskId && block.input?.status) {
113
+ const task = latestTodos.find(t => t.id === block.input.taskId);
114
+ if (task) {
115
+ task.status = block.input.status;
116
+ }
117
+ }
118
+ } else {
119
+ // Regular tool
120
+ toolMap.set(block.id, {
121
+ id: block.id,
122
+ name: block.name,
123
+ target: extractTarget(block.name, block.input),
124
+ status: 'running',
125
+ startTime: timestamp,
126
+ endTime: null
127
+ });
128
+ }
129
+ }
130
+
131
+ // Handle tool_result blocks
132
+ if (block.type === 'tool_result' && block.tool_use_id) {
133
+ const tool = toolMap.get(block.tool_use_id);
134
+ if (tool) {
135
+ tool.status = block.is_error ? 'error' : 'completed';
136
+ tool.endTime = timestamp;
137
+ }
138
+
139
+ const agent = agentMap.get(block.tool_use_id);
140
+ if (agent) {
141
+ agent.status = 'completed';
142
+ agent.endTime = timestamp;
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Extract target from tool input
150
+ * @param {string} toolName - Tool name
151
+ * @param {Object} input - Tool input object
152
+ * @returns {string|null} - Extracted target
153
+ */
154
+ function extractTarget(toolName, input) {
155
+ if (!input) return null;
156
+
157
+ switch (toolName) {
158
+ case 'Read':
159
+ case 'Write':
160
+ case 'Edit':
161
+ return input.file_path ?? input.path ?? null;
162
+
163
+ case 'Glob':
164
+ case 'Grep':
165
+ return input.pattern ?? null;
166
+
167
+ case 'Bash':
168
+ const cmd = input.command;
169
+ if (!cmd) return null;
170
+ return cmd.length > 30 ? cmd.slice(0, 30) + '...' : cmd;
171
+
172
+ default:
173
+ return null;
174
+ }
175
+ }
176
+
177
+ module.exports = {
178
+ parseTranscript,
179
+ // Export for testing
180
+ processEntry,
181
+ extractTarget
182
+ };
@@ -0,0 +1,360 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SessionStart Hook - Initializes session environment with project detection
4
+ *
5
+ * Fires: Once per session (startup, resume, clear, compact)
6
+ * Purpose: Load config, detect project info, persist to env vars, output context
7
+ *
8
+ * Exit Codes:
9
+ * 0 - Success (non-blocking, allows continuation)
10
+ *
11
+ * Core detection logic extracted to lib/detect.cjs for OpenCode plugin reuse.
12
+ */
13
+
14
+ // Crash wrapper
15
+ try {
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const os = require('os');
19
+ const {
20
+ loadConfig,
21
+ writeEnv,
22
+ writeSessionState,
23
+ resolvePlanPath,
24
+ getReportsPath,
25
+ resolveNamingPattern,
26
+ extractTaskListId,
27
+ isHookEnabled
28
+ } = require('./lib/config.cjs');
29
+
30
+ // Early exit if hook disabled in config
31
+ if (!isHookEnabled('session-init')) {
32
+ process.exit(0);
33
+ }
34
+
35
+ // Import shared project detection logic
36
+ const {
37
+ detectProjectType,
38
+ detectPackageManager,
39
+ detectFramework,
40
+ getPythonVersion,
41
+ getGitRemoteUrl,
42
+ getGitBranch,
43
+ getGitRoot,
44
+ getCodingLevelStyleName,
45
+ getCodingLevelGuidelines,
46
+ buildContextOutput,
47
+ execSafe
48
+ } = require('./lib/detect.cjs');
49
+
50
+ /**
51
+ * One-time cleanup for orphaned .shadowed/ directories from skill-dedup hook (Issue #422)
52
+ * The hook was disabled due to race conditions; this restores any orphaned skills.
53
+ */
54
+ function cleanupOrphanedShadowedSkills() {
55
+ const shadowedDir = path.join(process.cwd(), '.claude', 'skills', '.shadowed');
56
+ if (!fs.existsSync(shadowedDir)) return { restored: [], skipped: [], kept: [] };
57
+
58
+ const skillsDir = path.join(process.cwd(), '.claude', 'skills');
59
+ const restored = [];
60
+ const skipped = [];
61
+ const kept = []; // Skills kept for manual review (content differs)
62
+
63
+ try {
64
+ const entries = fs.readdirSync(shadowedDir, { withFileTypes: true });
65
+ for (const entry of entries) {
66
+ if (!entry.isDirectory()) continue;
67
+ const src = path.join(shadowedDir, entry.name);
68
+ const dest = path.join(skillsDir, entry.name);
69
+
70
+ try {
71
+ if (!fs.existsSync(dest)) {
72
+ fs.renameSync(src, dest);
73
+ restored.push(entry.name);
74
+ } else {
75
+ // Skill exists in local - verify content match before deleting orphaned copy
76
+ const orphanedSkill = path.join(src, 'SKILL.md');
77
+ const localSkill = path.join(dest, 'SKILL.md');
78
+ if (fs.existsSync(orphanedSkill) && fs.existsSync(localSkill)) {
79
+ const orphanedContent = fs.readFileSync(orphanedSkill, 'utf8');
80
+ const localContent = fs.readFileSync(localSkill, 'utf8');
81
+ if (orphanedContent === localContent) {
82
+ // Content identical - safe to remove orphaned duplicate
83
+ fs.rmSync(src, { recursive: true, force: true });
84
+ skipped.push(entry.name);
85
+ } else {
86
+ // Content differs - user may have edited orphaned version, keep for review
87
+ kept.push(entry.name);
88
+ }
89
+ } else {
90
+ // Missing SKILL.md - safe to remove orphaned copy
91
+ fs.rmSync(src, { recursive: true, force: true });
92
+ skipped.push(entry.name);
93
+ }
94
+ }
95
+ } catch (err) {
96
+ process.stderr.write(`[session-init] Failed to process "${entry.name}": ${err.message}\n`);
97
+ }
98
+ }
99
+ // Clean up manifest and shadowed dir if empty
100
+ const manifestFile = path.join(shadowedDir, '.dedup-manifest.json');
101
+ if (fs.existsSync(manifestFile)) fs.unlinkSync(manifestFile);
102
+ // Only remove shadowed dir if empty (kept skills may remain)
103
+ const remaining = fs.readdirSync(shadowedDir);
104
+ if (remaining.length === 0) fs.rmdirSync(shadowedDir);
105
+ return { restored, skipped, kept };
106
+ } catch (err) {
107
+ process.stderr.write(`[session-init] Shadowed cleanup error: ${err.message}\n`);
108
+ return { restored, skipped, kept };
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Detect if this session is running inside an Agent Team.
114
+ * Scans ~/.claude/teams/ for active team configs and checks membership.
115
+ * Note: Returns first team found — Claude Code supports one team per session.
116
+ * Note: Team lifecycle (creation/cleanup) is managed by Claude Code, not this hook.
117
+ * @returns {{ teamName: string, memberCount: number } | null}
118
+ */
119
+ function detectAgentTeam() {
120
+ try {
121
+ const teamsDir = path.join(os.homedir(), '.claude', 'teams');
122
+ if (!fs.existsSync(teamsDir)) return null;
123
+
124
+ const teams = fs.readdirSync(teamsDir, { withFileTypes: true });
125
+ for (const entry of teams) {
126
+ if (!entry.isDirectory()) continue;
127
+ const configPath = path.join(teamsDir, entry.name, 'config.json');
128
+ if (!fs.existsSync(configPath)) continue;
129
+ try {
130
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
131
+ if (config.members && config.members.length > 0) {
132
+ return { teamName: entry.name, memberCount: config.members.length };
133
+ }
134
+ } catch { /* skip malformed configs */ }
135
+ }
136
+ return null;
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Main hook execution
144
+ */
145
+ async function main() {
146
+ try {
147
+ // Issue #422: One-time cleanup of orphaned .shadowed/ from disabled skill-dedup hook
148
+ const shadowedCleanup = cleanupOrphanedShadowedSkills();
149
+
150
+ const stdin = fs.readFileSync(0, 'utf-8').trim();
151
+ const data = stdin ? JSON.parse(stdin) : {};
152
+ const envFile = process.env.CLAUDE_ENV_FILE;
153
+ const source = data.source || 'unknown';
154
+ const sessionId = data.session_id || null;
155
+
156
+ const config = loadConfig();
157
+
158
+ const detections = {
159
+ type: detectProjectType(config.project?.type),
160
+ pm: detectPackageManager(config.project?.packageManager),
161
+ framework: detectFramework(config.project?.framework)
162
+ };
163
+
164
+ // Resolve plan - now returns { path, resolvedBy }
165
+ const resolved = resolvePlanPath(null, config);
166
+
167
+ // CRITICAL FIX: Only persist explicitly-set plans to session state
168
+ // Branch-matched plans are "suggested" - stored separately, not as activePlan
169
+ // This prevents stale plan pollution on fresh sessions
170
+ if (sessionId) {
171
+ writeSessionState(sessionId, {
172
+ sessionOrigin: process.cwd(),
173
+ // Only session-resolved plans are truly "active"
174
+ activePlan: resolved.resolvedBy === 'session' ? resolved.path : null,
175
+ // Track suggested plan separately (for UI hints, not for report paths)
176
+ suggestedPlan: resolved.resolvedBy === 'branch' ? resolved.path : null,
177
+ timestamp: Date.now(),
178
+ source
179
+ });
180
+ }
181
+
182
+ // Reports path only uses active plans, not suggested ones
183
+ const reportsPath = getReportsPath(resolved.path, resolved.resolvedBy, config.plan, config.paths);
184
+
185
+ // Extract task list ID for Claude Code Tasks coordination (shared helper)
186
+ const taskListId = extractTaskListId(resolved);
187
+
188
+ // Collect static environment info (computed once per session)
189
+ const staticEnv = {
190
+ nodeVersion: process.version,
191
+ pythonVersion: getPythonVersion(),
192
+ osPlatform: process.platform,
193
+ gitUrl: getGitRemoteUrl(),
194
+ gitBranch: getGitBranch(),
195
+ gitRoot: getGitRoot(),
196
+ user: process.env.USERNAME || process.env.USER || process.env.LOGNAME || os.userInfo().username,
197
+ locale: process.env.LANG || '',
198
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
199
+ claudeSettingsDir: path.resolve(__dirname, '..')
200
+ };
201
+
202
+ // Compute base directory for absolute paths (Issue #327: use CWD for subdirectory support)
203
+ // Git root is kept in staticEnv for reference, but CWD determines where files are created
204
+ const baseDir = process.cwd();
205
+
206
+ // Compute resolved naming pattern (date + issue resolved, {slug} kept as placeholder)
207
+ const namePattern = resolveNamingPattern(config.plan, staticEnv.gitBranch);
208
+
209
+ if (envFile) {
210
+ // Session & plan config
211
+ writeEnv(envFile, 'CK_SESSION_ID', sessionId || '');
212
+ writeEnv(envFile, 'CK_PLAN_NAMING_FORMAT', config.plan.namingFormat);
213
+ writeEnv(envFile, 'CK_PLAN_DATE_FORMAT', config.plan.dateFormat);
214
+ writeEnv(envFile, 'CK_PLAN_ISSUE_PREFIX', config.plan.issuePrefix || '');
215
+ writeEnv(envFile, 'CK_PLAN_REPORTS_DIR', config.plan.reportsDir);
216
+
217
+ // NEW: Resolved naming pattern for DRY file naming in agents
218
+ // Example: "251212-1830-GH-88-{slug}" or "251212-1830-{slug}"
219
+ // Agents use: `{agent-type}-$CK_NAME_PATTERN.md` and substitute {slug}
220
+ writeEnv(envFile, 'CK_NAME_PATTERN', namePattern);
221
+
222
+ // Plan resolution
223
+ writeEnv(envFile, 'CK_ACTIVE_PLAN', resolved.resolvedBy === 'session' ? resolved.path : '');
224
+ writeEnv(envFile, 'CK_SUGGESTED_PLAN', resolved.resolvedBy === 'branch' ? resolved.path : '');
225
+
226
+ // Claude Code Tasks integration - enables multi-session/subagent coordination
227
+ // Task list ID = plan directory name (shared across all sessions working on same plan)
228
+ if (taskListId) {
229
+ writeEnv(envFile, 'CLAUDE_CODE_TASK_LIST_ID', taskListId);
230
+ }
231
+
232
+ // Paths - use absolute paths based on CWD for subdirectory workflow support (Issue #327)
233
+ writeEnv(envFile, 'CK_GIT_ROOT', staticEnv.gitRoot || '');
234
+ writeEnv(envFile, 'CK_REPORTS_PATH', path.join(baseDir, reportsPath));
235
+ writeEnv(envFile, 'CK_DOCS_PATH', path.join(baseDir, config.paths.docs));
236
+ writeEnv(envFile, 'CK_PLANS_PATH', path.join(baseDir, config.paths.plans));
237
+ writeEnv(envFile, 'CK_PROJECT_ROOT', process.cwd());
238
+
239
+ // Project detection
240
+ writeEnv(envFile, 'CK_PROJECT_TYPE', detections.type || '');
241
+ writeEnv(envFile, 'CK_PACKAGE_MANAGER', detections.pm || '');
242
+ writeEnv(envFile, 'CK_FRAMEWORK', detections.framework || '');
243
+
244
+ // NEW: Static environment info (so other hooks don't need to recompute)
245
+ writeEnv(envFile, 'CK_NODE_VERSION', staticEnv.nodeVersion);
246
+ writeEnv(envFile, 'CK_PYTHON_VERSION', staticEnv.pythonVersion || '');
247
+ writeEnv(envFile, 'CK_OS_PLATFORM', staticEnv.osPlatform);
248
+ writeEnv(envFile, 'CK_GIT_URL', staticEnv.gitUrl || '');
249
+ writeEnv(envFile, 'CK_GIT_BRANCH', staticEnv.gitBranch || '');
250
+ writeEnv(envFile, 'CK_USER', staticEnv.user);
251
+ writeEnv(envFile, 'CK_LOCALE', staticEnv.locale);
252
+ writeEnv(envFile, 'CK_TIMEZONE', staticEnv.timezone);
253
+ writeEnv(envFile, 'CK_CLAUDE_SETTINGS_DIR', staticEnv.claudeSettingsDir);
254
+
255
+ // Locale config
256
+ if (config.locale?.thinkingLanguage) {
257
+ writeEnv(envFile, 'CK_THINKING_LANGUAGE', config.locale.thinkingLanguage);
258
+ }
259
+ if (config.locale?.responseLanguage) {
260
+ writeEnv(envFile, 'CK_RESPONSE_LANGUAGE', config.locale.responseLanguage);
261
+ }
262
+
263
+ // Plan validation config (for /plan validate, /plan --hard, /plan --parallel)
264
+ const validation = config.plan?.validation || {};
265
+ writeEnv(envFile, 'CK_VALIDATION_MODE', validation.mode || 'prompt');
266
+ writeEnv(envFile, 'CK_VALIDATION_MIN_QUESTIONS', validation.minQuestions || 3);
267
+ writeEnv(envFile, 'CK_VALIDATION_MAX_QUESTIONS', validation.maxQuestions || 8);
268
+ writeEnv(envFile, 'CK_VALIDATION_FOCUS_AREAS', (validation.focusAreas || ['assumptions', 'risks', 'tradeoffs', 'architecture']).join(','));
269
+
270
+ // Coding level config (for output style selection)
271
+ const codingLevel = config.codingLevel ?? 5;
272
+ writeEnv(envFile, 'CK_CODING_LEVEL', codingLevel);
273
+ writeEnv(envFile, 'CK_CODING_LEVEL_STYLE', getCodingLevelStyleName(codingLevel));
274
+
275
+ }
276
+
277
+ // Agent Teams detection — detect once, used for env vars and console output
278
+ const teamInfo = detectAgentTeam();
279
+ if (envFile && teamInfo) {
280
+ writeEnv(envFile, 'CK_AGENT_TEAM', teamInfo.teamName);
281
+ writeEnv(envFile, 'CK_AGENT_TEAM_MEMBERS', teamInfo.memberCount);
282
+ }
283
+
284
+ console.log(`Session ${source}. ${buildContextOutput(config, detections, resolved, staticEnv.gitRoot)}`);
285
+
286
+ // Issue #422: Notify user if orphaned skills were recovered from .shadowed/
287
+ const hasCleanup = shadowedCleanup.restored.length > 0 || shadowedCleanup.skipped.length > 0 || shadowedCleanup.kept.length > 0;
288
+ if (hasCleanup) {
289
+ console.log(`\n[!] SKILL-DEDUP CLEANUP (Issue #422):`);
290
+ console.log(`Recovered orphaned .shadowed/ directory from disabled skill-dedup hook.`);
291
+ if (shadowedCleanup.restored.length > 0) {
292
+ console.log(`Restored ${shadowedCleanup.restored.length} skill(s): ${shadowedCleanup.restored.join(', ')}`);
293
+ }
294
+ if (shadowedCleanup.skipped.length > 0) {
295
+ console.log(`Removed ${shadowedCleanup.skipped.length} duplicate(s): ${shadowedCleanup.skipped.join(', ')}`);
296
+ }
297
+ if (shadowedCleanup.kept.length > 0) {
298
+ console.log(`[!] Kept ${shadowedCleanup.kept.length} skill(s) for manual review (content differs): ${shadowedCleanup.kept.join(', ')}`);
299
+ console.log(` Review .claude/skills/.shadowed/ and merge changes manually.`);
300
+ }
301
+ }
302
+
303
+ // Agent Teams: Show team context if running inside a team (uses cached result)
304
+ if (teamInfo) {
305
+ console.log(`[i] Agent Team detected: "${teamInfo.teamName}" (${teamInfo.memberCount} members)`);
306
+ console.log(` Team config: ~/.claude/teams/${teamInfo.teamName}/config.json`);
307
+ console.log(` Use /team skill for orchestration templates.`);
308
+ }
309
+
310
+ // Info: Show git root when running from subdirectory (Issue #327: now supported)
311
+ if (staticEnv.gitRoot && staticEnv.gitRoot !== process.cwd()) {
312
+ console.log(`📁 Subdirectory mode: Plans/docs will be created in current directory`);
313
+ console.log(` Git root: ${staticEnv.gitRoot}`);
314
+ }
315
+
316
+ // MITIGATION: Issue #277 - Auto-compact can bypass AskUserQuestion approval gates
317
+ // When context is compacted mid-workflow, the summarization may lose "pending approval" state.
318
+ // This warning reminds Claude to verify if user approval was pending before proceeding.
319
+ // Upstream bug: Claude Code CLI should preserve pending interactive state during compaction.
320
+ if (source === 'compact') {
321
+ console.log(`\n⚠️ CONTEXT COMPACTED - APPROVAL STATE CHECK:`);
322
+ console.log(`If you were waiting for user approval via AskUserQuestion (e.g., Step 4 review gate),`);
323
+ console.log(`you MUST re-confirm with the user before proceeding. Do NOT assume approval was given.`);
324
+ console.log(`Use AskUserQuestion to verify: "Context was compacted. Please confirm approval to continue."`);
325
+ }
326
+
327
+ // Auto-inject coding level guidelines (if not disabled)
328
+ const codingLevel = config.codingLevel ?? -1;
329
+ const guidelines = getCodingLevelGuidelines(codingLevel);
330
+ if (guidelines) {
331
+ console.log(`\n${guidelines}`);
332
+ }
333
+
334
+ if (config.assertions?.length > 0) {
335
+ console.log(`\nUser Assertions:`);
336
+ config.assertions.forEach((assertion, i) => {
337
+ console.log(` ${i + 1}. ${assertion}`);
338
+ });
339
+ }
340
+
341
+ process.exit(0);
342
+ } catch (error) {
343
+ console.error(`SessionStart hook error: ${error.message}`);
344
+ process.exit(0);
345
+ }
346
+ }
347
+
348
+ main();
349
+ } catch (e) {
350
+ // Minimal crash logging (zero deps — only Node builtins)
351
+ try {
352
+ const fs = require('fs');
353
+ const p = require('path');
354
+ const logDir = p.join(__dirname, '.logs');
355
+ if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
356
+ fs.appendFileSync(p.join(logDir, 'hook-log.jsonl'),
357
+ JSON.stringify({ ts: new Date().toISOString(), hook: p.basename(__filename, '.cjs'), status: 'crash', error: e.message }) + '\n');
358
+ } catch (_) {}
359
+ process.exit(0); // fail-open
360
+ }