@hasnaxyz/hook-checktasks 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +89 -97
  2. package/dist/cli.js +448 -118
  3. package/dist/hook.js +123 -34
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -1,150 +1,142 @@
1
1
  # @hasnaxyz/hook-checktasks
2
2
 
3
- A Claude Code hook that prevents Claude from stopping when there are pending tasks in your task list.
3
+ A Claude Code hook that prevents Claude from stopping when there are pending tasks.
4
4
 
5
5
  ## What it does
6
6
 
7
- When you have a task list configured (`CLAUDE_CODE_TASK_LIST_ID`), this hook:
7
+ This hook intercepts Claude's "Stop" event and:
8
8
 
9
- 1. Checks if the session name or task list ID contains "dev" (configurable)
10
- 2. Reads the task list from `~/.claude/tasks/{taskListId}/`
11
- 3. If there are pending or in-progress tasks, **blocks the stop** and prompts Claude to continue working
12
- 4. Only allows stopping when all tasks are completed
13
-
14
- This ensures Claude doesn't abandon work mid-session.
9
+ 1. Checks configured task lists for pending/in-progress tasks
10
+ 2. If tasks remain → **blocks the stop** and prompts Claude to continue
11
+ 3. If all complete allows stop
15
12
 
16
13
  ## Installation
17
14
 
18
- ### Prerequisites
19
-
20
- - [Bun](https://bun.sh/) runtime installed
21
- - Access to `@hasnaxyz` npm organization
22
-
23
- ### Quick Install
15
+ ### 1. Install the CLI globally
24
16
 
25
17
  ```bash
26
- # Install globally (applies to all Claude Code sessions)
27
- bunx @hasnaxyz/hook-checktasks install --global
28
-
29
- # Install for current project only
30
- bunx @hasnaxyz/hook-checktasks install
18
+ bun add -g @hasnaxyz/hook-checktasks
19
+ # or
20
+ npm install -g @hasnaxyz/hook-checktasks
31
21
  ```
32
22
 
33
- ### Check Installation Status
23
+ ### 2. Install the hook
34
24
 
35
25
  ```bash
36
- bunx @hasnaxyz/hook-checktasks status
37
- ```
26
+ # Auto-detect (git repo → project, else → prompt)
27
+ hook-checktasks install
38
28
 
39
- ### Uninstall
40
-
41
- ```bash
42
- # Remove from global settings
43
- bunx @hasnaxyz/hook-checktasks uninstall --global
29
+ # Install globally
30
+ hook-checktasks install --global
44
31
 
45
- # Remove from current project
46
- bunx @hasnaxyz/hook-checktasks uninstall
32
+ # Install to specific path
33
+ hook-checktasks install /path/to/project
47
34
  ```
48
35
 
49
- ## Configuration
50
-
51
- Configure via environment variables:
36
+ The installer will prompt you to configure:
37
+ - **Task list ID**: specific list or leave empty for all lists
38
+ - **Keywords**: session/list name keywords to trigger the check (default: "dev")
52
39
 
53
- | Variable | Description | Default |
54
- |----------|-------------|---------|
55
- | `CLAUDE_CODE_TASK_LIST_ID` | Task list to monitor (required for task checking) | - |
56
- | `CHECK_TASKS_KEYWORDS` | Comma-separated keywords that trigger the check | `dev` |
57
- | `CHECK_TASKS_DISABLED` | Set to `1` to disable the hook | - |
40
+ ## Configuration
58
41
 
59
- ### Examples
42
+ ### Update configuration
60
43
 
61
44
  ```bash
62
- # Standard usage with dev task list
63
- CLAUDE_CODE_TASK_LIST_ID=myproject-dev claude
45
+ hook-checktasks config # Current project
46
+ hook-checktasks config --global # Global settings
47
+ ```
64
48
 
65
- # Check for "dev" or "sprint" sessions
66
- CHECK_TASKS_KEYWORDS=dev,sprint CLAUDE_CODE_TASK_LIST_ID=myproject-dev claude
49
+ ### Check status
67
50
 
68
- # Temporarily disable the hook
69
- CHECK_TASKS_DISABLED=1 claude
51
+ ```bash
52
+ hook-checktasks status
70
53
  ```
71
54
 
72
- ## How it works
55
+ Shows:
56
+ - Where hook is installed (global/project)
57
+ - Current configuration
58
+ - Available task lists
59
+
60
+ ### Uninstall
73
61
 
74
- 1. **On Stop hook trigger**: Claude is about to stop/exit
75
- 2. **Check session name**: Reads the session name from transcript (if available)
76
- 3. **Keyword matching**: Checks if session name or task list ID contains configured keywords
77
- 4. **Task count**: Reads task files from `~/.claude/tasks/{taskListId}/`
78
- 5. **Decision**:
79
- - If tasks remain: Block stop with instructions to continue
80
- - If all complete: Allow stop
62
+ ```bash
63
+ hook-checktasks uninstall # Current project
64
+ hook-checktasks uninstall --global # Global
65
+ hook-checktasks uninstall /path # Specific path
66
+ ```
81
67
 
82
- ## What Gets Added to settings.json
68
+ ## How Configuration Works
83
69
 
84
- The install command adds this to your Claude Code settings:
70
+ Configuration is stored in `.claude/settings.json`:
85
71
 
86
72
  ```json
87
73
  {
88
74
  "hooks": {
89
- "Stop": [
90
- {
91
- "hooks": [
92
- {
93
- "type": "command",
94
- "command": "bunx @hasnaxyz/hook-checktasks run",
95
- "timeout": 120
96
- }
97
- ]
98
- }
99
- ]
75
+ "Stop": [{ "hooks": [{ "type": "command", "command": "bunx @hasnaxyz/hook-checktasks run" }] }]
76
+ },
77
+ "checkTasksConfig": {
78
+ "taskListId": "myproject-dev",
79
+ "keywords": ["dev"],
80
+ "enabled": true
100
81
  }
101
82
  }
102
83
  ```
103
84
 
104
- ## Task File Format
85
+ ### Config Options
105
86
 
106
- Tasks are stored as JSON files in `~/.claude/tasks/{taskListId}/`:
87
+ | Option | Description | Default |
88
+ |--------|-------------|---------|
89
+ | `taskListId` | Specific task list to check, or `undefined` for all lists | `undefined` (all) |
90
+ | `keywords` | Keywords to match in session/list names | `["dev"]` |
91
+ | `enabled` | Enable/disable the hook | `true` |
107
92
 
108
- ```json
109
- {
110
- "id": "task-001",
111
- "subject": "Implement user authentication",
112
- "status": "pending"
113
- }
114
- ```
93
+ ### Priority
115
94
 
116
- Status values: `pending`, `in_progress`, `completed`
95
+ 1. Project settings (`.claude/settings.json`)
96
+ 2. Global settings (`~/.claude/settings.json`)
97
+ 3. Environment variables (legacy)
117
98
 
118
- ## CLI Commands
119
-
120
- ```
121
- bunx @hasnaxyz/hook-checktasks <command> [options]
99
+ ### Legacy Environment Variables
122
100
 
123
- Commands:
124
- install [--global] Install the hook to Claude Code settings
125
- uninstall [--global] Remove the hook from Claude Code settings
126
- run Execute the hook (called by Claude Code automatically)
127
- status Show current hook configuration
101
+ Still supported for backwards compatibility:
128
102
 
129
- Options:
130
- --global, -g Apply to global settings (~/.claude/settings.json)
103
+ ```bash
104
+ CLAUDE_CODE_TASK_LIST_ID=myproject-dev claude
105
+ CHECK_TASKS_KEYWORDS=dev,sprint claude
106
+ CHECK_TASKS_DISABLED=1 claude
131
107
  ```
132
108
 
133
- ## Development
109
+ ## CLI Commands
134
110
 
135
- ```bash
136
- # Clone the repo
137
- git clone https://github.com/hasnaxyz/hook-checktasks.git
138
- cd hook-checktasks
111
+ ```
112
+ hook-checktasks install [path] Install the hook
113
+ hook-checktasks config [path] Update configuration
114
+ hook-checktasks uninstall [path] Remove the hook
115
+ hook-checktasks status Show hook status
116
+ hook-checktasks run Execute hook (called by Claude Code)
139
117
 
140
- # Install dependencies
141
- bun install
118
+ Options:
119
+ --global, -g Apply to global settings
120
+ /path/to/repo Apply to specific project
121
+ ```
142
122
 
143
- # Build
144
- bun run build
123
+ ## How it Works
145
124
 
146
- # Test locally
147
- bun ./dist/cli.js status
125
+ ```
126
+ Claude tries to stop
127
+
128
+
129
+ Stop hook fires
130
+
131
+
132
+ Read config from settings.json
133
+
134
+
135
+ Check matching task lists
136
+
137
+ ├── Tasks remaining → BLOCK stop, prompt to continue
138
+
139
+ └── All complete → ALLOW stop
148
140
  ```
149
141
 
150
142
  ## License
package/dist/cli.js CHANGED
@@ -30,6 +30,30 @@ function readStdinJson() {
30
30
  return null;
31
31
  }
32
32
  }
33
+ function readSettings(path) {
34
+ if (!existsSync(path))
35
+ return {};
36
+ try {
37
+ return JSON.parse(readFileSync(path, "utf-8"));
38
+ } catch {
39
+ return {};
40
+ }
41
+ }
42
+ function getConfig(cwd) {
43
+ const projectSettings = readSettings(join(cwd, ".claude", "settings.json"));
44
+ if (projectSettings[CONFIG_KEY]) {
45
+ return projectSettings[CONFIG_KEY];
46
+ }
47
+ const globalSettings = readSettings(join(homedir(), ".claude", "settings.json"));
48
+ if (globalSettings[CONFIG_KEY]) {
49
+ return globalSettings[CONFIG_KEY];
50
+ }
51
+ return {
52
+ taskListId: process.env.CLAUDE_CODE_TASK_LIST_ID,
53
+ keywords: process.env.CHECK_TASKS_KEYWORDS?.split(",").map((k) => k.trim().toLowerCase()).filter(Boolean) || ["dev"],
54
+ enabled: process.env.CHECK_TASKS_DISABLED !== "1"
55
+ };
56
+ }
33
57
  function getSessionName(transcriptPath) {
34
58
  if (!existsSync(transcriptPath))
35
59
  return null;
@@ -59,6 +83,44 @@ function getSessionName(transcriptPath) {
59
83
  return null;
60
84
  }
61
85
  }
86
+ function getAllTaskLists() {
87
+ const tasksDir = join(homedir(), ".claude", "tasks");
88
+ if (!existsSync(tasksDir))
89
+ return [];
90
+ try {
91
+ return readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
92
+ } catch {
93
+ return [];
94
+ }
95
+ }
96
+ function getProjectTaskLists(cwd) {
97
+ const allLists = getAllTaskLists();
98
+ const dirName = cwd.split("/").filter(Boolean).pop() || "";
99
+ const projectLists = allLists.filter((list) => {
100
+ const listLower = list.toLowerCase();
101
+ const dirLower = dirName.toLowerCase();
102
+ if (listLower.startsWith(dirLower + "-"))
103
+ return true;
104
+ if (listLower === dirLower)
105
+ return true;
106
+ return false;
107
+ });
108
+ return projectLists;
109
+ }
110
+ function getTasksFromList(listId) {
111
+ const tasksDir = join(homedir(), ".claude", "tasks", listId);
112
+ if (!existsSync(tasksDir))
113
+ return [];
114
+ try {
115
+ const taskFiles = readdirSync(tasksDir).filter((f) => f.endsWith(".json"));
116
+ return taskFiles.map((file) => {
117
+ const content = readFileSync(join(tasksDir, file), "utf-8");
118
+ return JSON.parse(content);
119
+ });
120
+ } catch {
121
+ return [];
122
+ }
123
+ }
62
124
  function approve() {
63
125
  console.log(JSON.stringify({ decision: "approve" }));
64
126
  process.exit(0);
@@ -68,121 +130,154 @@ function block(reason) {
68
130
  process.exit(0);
69
131
  }
70
132
  function run() {
71
- if (process.env.CHECK_TASKS_DISABLED === "1") {
72
- approve();
73
- }
74
- const taskListId = process.env.CLAUDE_CODE_TASK_LIST_ID;
75
- if (!taskListId) {
133
+ const hookInput = readStdinJson();
134
+ const cwd = hookInput?.cwd || process.cwd();
135
+ const config = getConfig(cwd);
136
+ if (config.enabled === false) {
76
137
  approve();
77
138
  }
78
- const hookInput = readStdinJson();
79
139
  let sessionName = null;
80
140
  if (hookInput?.transcript_path) {
81
141
  sessionName = getSessionName(hookInput.transcript_path);
82
142
  }
83
- const nameToCheck = sessionName || taskListId || "";
84
- const keywords = (process.env.CHECK_TASKS_KEYWORDS || "dev").split(",").map((k) => k.trim().toLowerCase()).filter(Boolean);
85
- const matchesKeyword = keywords.some((keyword) => nameToCheck.toLowerCase().includes(keyword));
86
- if (!matchesKeyword) {
143
+ const nameToCheck = sessionName || config.taskListId || "";
144
+ const keywords = config.keywords || ["dev"];
145
+ const matchesKeyword = keywords.some((keyword) => nameToCheck.toLowerCase().includes(keyword.toLowerCase()));
146
+ if (!matchesKeyword && keywords.length > 0 && nameToCheck) {
87
147
  approve();
88
148
  }
89
- const tasksDir = join(homedir(), ".claude", "tasks", taskListId);
90
- if (!existsSync(tasksDir)) {
91
- approve();
149
+ let listsToCheck = [];
150
+ if (config.taskListId) {
151
+ listsToCheck = [config.taskListId];
152
+ } else {
153
+ const projectLists = getProjectTaskLists(cwd);
154
+ if (projectLists.length > 0) {
155
+ if (keywords.length > 0) {
156
+ listsToCheck = projectLists.filter((list) => keywords.some((keyword) => list.toLowerCase().includes(keyword.toLowerCase())));
157
+ } else {
158
+ listsToCheck = projectLists;
159
+ }
160
+ }
92
161
  }
93
- const taskFiles = readdirSync(tasksDir).filter((f) => f.endsWith(".json"));
94
- if (taskFiles.length === 0) {
162
+ if (listsToCheck.length === 0) {
95
163
  approve();
96
164
  }
97
- const tasks = taskFiles.map((file) => {
98
- const content = readFileSync(join(tasksDir, file), "utf-8");
99
- return JSON.parse(content);
100
- });
101
- const pending = tasks.filter((t) => t.status === "pending");
102
- const inProgress = tasks.filter((t) => t.status === "in_progress");
103
- const completed = tasks.filter((t) => t.status === "completed");
104
- const remainingCount = pending.length + inProgress.length;
165
+ let allPending = [];
166
+ let allInProgress = [];
167
+ let allCompleted = [];
168
+ let activeListId = null;
169
+ for (const listId of listsToCheck) {
170
+ const tasks = getTasksFromList(listId);
171
+ const pending = tasks.filter((t) => t.status === "pending");
172
+ const inProgress = tasks.filter((t) => t.status === "in_progress");
173
+ const completed = tasks.filter((t) => t.status === "completed");
174
+ if (pending.length > 0 || inProgress.length > 0) {
175
+ activeListId = listId;
176
+ }
177
+ allPending.push(...pending);
178
+ allInProgress.push(...inProgress);
179
+ allCompleted.push(...completed);
180
+ }
181
+ const remainingCount = allPending.length + allInProgress.length;
105
182
  if (remainingCount > 0) {
106
- const nextTasks = pending.slice(0, 3).map((t) => `- ${t.subject}`).join(`
183
+ const nextTasks = allPending.slice(0, 3).map((t) => `- ${t.subject}`).join(`
107
184
  `);
185
+ const listInfo = activeListId ? ` in "${activeListId}"` : "";
108
186
  const prompt = `
109
- STOP BLOCKED: You have ${remainingCount} tasks remaining (${pending.length} pending, ${inProgress.length} in progress, ${completed.length} completed).
187
+ STOP BLOCKED: You have ${remainingCount} tasks remaining${listInfo} (${allPending.length} pending, ${allInProgress.length} in progress, ${allCompleted.length} completed).
188
+
189
+ ⛔ DO NOT STOP. DO NOT ASK QUESTIONS. DO NOT WAIT FOR USER INPUT.
110
190
 
111
- You MUST continue working. Do NOT stop until all tasks are completed.
191
+ You MUST continue working AUTONOMOUSLY until ALL tasks are completed.
112
192
 
113
193
  Next pending tasks:
114
194
  ${nextTasks}
115
- ${pending.length > 3 ? `... and ${pending.length - 3} more pending tasks` : ""}
195
+ ${allPending.length > 3 ? `... and ${allPending.length - 3} more pending tasks` : ""}
116
196
 
117
- INSTRUCTIONS:
197
+ MANDATORY INSTRUCTIONS (follow these NOW):
118
198
  1. Use TaskList to see all tasks
119
- 2. Pick the next pending task
120
- 3. Use TaskUpdate to mark it as in_progress
121
- 4. Complete the task
122
- 5. Use TaskUpdate to mark it as completed
123
- 6. Repeat until all tasks are done
199
+ 2. Use TaskGet to read the FIRST pending task's full description
200
+ 3. Use TaskUpdate to mark it as in_progress BEFORE starting work
201
+ 4. Complete the task (write code, run commands, etc.)
202
+ 5. Use TaskUpdate to mark it as completed AFTER finishing
203
+ 6. IMMEDIATELY move to the next task - DO NOT STOP
124
204
 
125
- DO NOT STOP. Continue working now.
205
+ CRITICAL RULES:
206
+ - NEVER ask "would you like me to..." - just DO IT
207
+ - NEVER ask for confirmation - just WORK
208
+ - NEVER stop to explain what you'll do - just DO IT
209
+ - If a task is unclear, make reasonable assumptions and proceed
210
+ - If you encounter an error, fix it and continue
211
+ - Keep working until remainingCount = 0
212
+
213
+ START WORKING NOW. Use TaskList tool in your next response.
126
214
  `.trim();
127
215
  block(prompt);
128
216
  }
129
217
  approve();
130
218
  }
219
+ var CONFIG_KEY = "checkTasksConfig";
131
220
  var init_hook = __esm(() => {
132
221
  if (false) {}
133
222
  });
134
223
 
135
224
  // src/cli.ts
136
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
137
- import { join as join2, dirname } from "path";
225
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync, readdirSync as readdirSync2 } from "fs";
226
+ import { join as join2, dirname, resolve } from "path";
138
227
  import { homedir as homedir2 } from "os";
228
+ import * as readline from "readline";
139
229
  var PACKAGE_NAME = "@hasnaxyz/hook-checktasks";
140
- var colors = {
230
+ var CONFIG_KEY2 = "checkTasksConfig";
231
+ var c = {
141
232
  red: (s) => `\x1B[31m${s}\x1B[0m`,
142
233
  green: (s) => `\x1B[32m${s}\x1B[0m`,
143
234
  yellow: (s) => `\x1B[33m${s}\x1B[0m`,
144
235
  cyan: (s) => `\x1B[36m${s}\x1B[0m`,
236
+ dim: (s) => `\x1B[2m${s}\x1B[0m`,
145
237
  bold: (s) => `\x1B[1m${s}\x1B[0m`
146
238
  };
147
239
  function printUsage() {
148
240
  console.log(`
149
- ${colors.bold("@hasnaxyz/hook-checktasks")} - Claude Code hook that prevents stopping with pending tasks
150
-
151
- ${colors.bold("USAGE:")}
152
- bunx ${PACKAGE_NAME} <command> [options]
241
+ ${c.bold("hook-checktasks")} - Prevents Claude from stopping with pending tasks
153
242
 
154
- ${colors.bold("COMMANDS:")}
155
- install [--global] Install the hook to Claude Code settings
156
- uninstall [--global] Remove the hook from Claude Code settings
157
- run Execute the hook (called by Claude Code automatically)
158
- status Show current hook configuration
243
+ ${c.bold("USAGE:")}
244
+ hook-checktasks install [path] Install the hook
245
+ hook-checktasks config [path] Update configuration
246
+ hook-checktasks uninstall [path] Remove the hook
247
+ hook-checktasks status Show hook status
248
+ hook-checktasks run Execute hook ${c.dim("(called by Claude Code)")}
159
249
 
160
- ${colors.bold("OPTIONS:")}
161
- --global, -g Apply to global settings (~/.claude/settings.json)
162
- Without this flag, applies to current project
250
+ ${c.bold("OPTIONS:")}
251
+ ${c.dim("(no args)")} Auto-detect: if in git repo \u2192 install there, else \u2192 prompt
252
+ --global, -g Apply to ~/.claude/settings.json
253
+ --task-list-id, -t <id> Task list ID (non-interactive)
254
+ --keywords, -k <k1,k2> Keywords, comma-separated (non-interactive)
255
+ --yes, -y Non-interactive mode, use defaults
256
+ /path/to/repo Apply to specific project path
163
257
 
164
- ${colors.bold("EXAMPLES:")}
165
- bunx ${PACKAGE_NAME} install --global # Install globally
166
- bunx ${PACKAGE_NAME} install # Install in current project
167
- bunx ${PACKAGE_NAME} uninstall --global # Remove from global settings
168
- bunx ${PACKAGE_NAME} status # Show hook status
258
+ ${c.bold("EXAMPLES:")}
259
+ hook-checktasks install ${c.dim("# Install with config prompts")}
260
+ hook-checktasks install --global ${c.dim("# Global install")}
261
+ hook-checktasks install -t myproject-dev -y ${c.dim("# Non-interactive")}
262
+ hook-checktasks config ${c.dim("# Update task list ID, keywords")}
263
+ hook-checktasks status ${c.dim("# Check what's installed")}
169
264
 
170
- ${colors.bold("ENVIRONMENT VARIABLES:")}
171
- CLAUDE_CODE_TASK_LIST_ID Task list to monitor (required for task checking)
172
- CHECK_TASKS_KEYWORDS Keywords to trigger check (default: "dev")
173
- CHECK_TASKS_DISABLED Set to "1" to disable the hook
265
+ ${c.bold("GLOBAL CLI INSTALL:")}
266
+ bun add -g ${PACKAGE_NAME}
174
267
  `);
175
268
  }
176
- function getSettingsPath(global) {
177
- if (global) {
269
+ function isGitRepo(path) {
270
+ return existsSync2(join2(path, ".git"));
271
+ }
272
+ function getSettingsPath(targetPath) {
273
+ if (targetPath === "global") {
178
274
  return join2(homedir2(), ".claude", "settings.json");
179
275
  }
180
- return join2(process.cwd(), ".claude", "settings.json");
276
+ return join2(targetPath, ".claude", "settings.json");
181
277
  }
182
- function readSettings(path) {
183
- if (!existsSync2(path)) {
278
+ function readSettings2(path) {
279
+ if (!existsSync2(path))
184
280
  return {};
185
- }
186
281
  try {
187
282
  return JSON.parse(readFileSync2(path, "utf-8"));
188
283
  } catch {
@@ -195,7 +290,7 @@ function writeSettings(path, settings) {
195
290
  `);
196
291
  }
197
292
  function getHookCommand() {
198
- return `bunx ${PACKAGE_NAME} run`;
293
+ return `bunx ${PACKAGE_NAME}@latest run`;
199
294
  }
200
295
  function hookExists(settings) {
201
296
  const hooks = settings.hooks;
@@ -204,15 +299,21 @@ function hookExists(settings) {
204
299
  const stopHooks = hooks.Stop;
205
300
  return stopHooks.some((group) => group.hooks?.some((h) => h.command?.includes(PACKAGE_NAME)));
206
301
  }
302
+ function getConfig2(settings) {
303
+ return settings[CONFIG_KEY2] || {};
304
+ }
305
+ function setConfig(settings, config) {
306
+ settings[CONFIG_KEY2] = config;
307
+ return settings;
308
+ }
207
309
  function addHook(settings) {
208
310
  const hookConfig = {
209
311
  type: "command",
210
312
  command: getHookCommand(),
211
313
  timeout: 120
212
314
  };
213
- if (!settings.hooks) {
315
+ if (!settings.hooks)
214
316
  settings.hooks = {};
215
- }
216
317
  const hooks = settings.hooks;
217
318
  if (!hooks.Stop) {
218
319
  hooks.Stop = [{ hooks: [hookConfig] }];
@@ -237,96 +338,325 @@ function removeHook(settings) {
237
338
  }
238
339
  }
239
340
  hooks.Stop = stopHooks.filter((g) => g.hooks && g.hooks.length > 0);
240
- if (hooks.Stop.length === 0) {
341
+ if (hooks.Stop.length === 0)
241
342
  delete hooks.Stop;
242
- }
343
+ delete settings[CONFIG_KEY2];
243
344
  return settings;
244
345
  }
245
- function install(global) {
246
- const scope = global ? "global" : "project";
247
- const settingsPath = getSettingsPath(global);
346
+ async function prompt(question) {
347
+ const rl = readline.createInterface({
348
+ input: process.stdin,
349
+ output: process.stdout
350
+ });
351
+ return new Promise((resolve2) => {
352
+ rl.question(question, (answer) => {
353
+ rl.close();
354
+ resolve2(answer.trim());
355
+ });
356
+ });
357
+ }
358
+ function getAllTaskLists2() {
359
+ const tasksDir = join2(homedir2(), ".claude", "tasks");
360
+ if (!existsSync2(tasksDir))
361
+ return [];
362
+ try {
363
+ return readdirSync2(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
364
+ } catch {
365
+ return [];
366
+ }
367
+ }
368
+ function getProjectTaskLists2(projectPath) {
369
+ const allLists = getAllTaskLists2();
370
+ const dirName = projectPath.split("/").filter(Boolean).pop() || "";
371
+ const projectLists = allLists.filter((list) => {
372
+ const listLower = list.toLowerCase();
373
+ const dirLower = dirName.toLowerCase();
374
+ if (listLower.startsWith(dirLower + "-"))
375
+ return true;
376
+ if (listLower.includes(dirLower))
377
+ return true;
378
+ return false;
379
+ });
380
+ return projectLists;
381
+ }
382
+ function parseInstallArgs(args) {
383
+ const options = {};
384
+ for (let i = 0;i < args.length; i++) {
385
+ const arg = args[i];
386
+ if (arg === "--global" || arg === "-g") {
387
+ options.global = true;
388
+ } else if (arg === "--yes" || arg === "-y") {
389
+ options.yes = true;
390
+ } else if (arg === "--task-list-id" || arg === "-t") {
391
+ options.taskListId = args[++i];
392
+ } else if (arg === "--keywords" || arg === "-k") {
393
+ options.keywords = args[++i]?.split(",").map((k) => k.trim().toLowerCase()).filter(Boolean);
394
+ } else if (!arg.startsWith("-")) {
395
+ options.path = arg;
396
+ }
397
+ }
398
+ return options;
399
+ }
400
+ async function resolveTarget(args) {
401
+ if (args.includes("--global") || args.includes("-g")) {
402
+ return { path: "global", label: "global (~/.claude/settings.json)" };
403
+ }
404
+ const pathArg = args.find((a) => !a.startsWith("-"));
405
+ if (pathArg) {
406
+ const fullPath = resolve(pathArg);
407
+ if (!existsSync2(fullPath)) {
408
+ console.log(c.red("\u2717"), `Path does not exist: ${fullPath}`);
409
+ return null;
410
+ }
411
+ return { path: fullPath, label: `project (${fullPath})` };
412
+ }
413
+ const cwd = process.cwd();
414
+ if (isGitRepo(cwd)) {
415
+ console.log(c.green("\u2713"), `Detected git repo: ${c.cyan(cwd)}`);
416
+ return { path: cwd, label: `project (${cwd})` };
417
+ }
418
+ console.log(c.yellow("!"), `Current directory: ${c.cyan(cwd)}`);
419
+ console.log(c.dim(` (not a git repository)
420
+ `));
421
+ console.log(`Where would you like to install?
422
+ `);
423
+ console.log(" 1. Here", c.dim(`(${cwd})`));
424
+ console.log(" 2. Global", c.dim("(~/.claude/settings.json)"));
425
+ console.log(` 3. Enter a different path
426
+ `);
427
+ const choice = await prompt("Choice (1/2/3): ");
428
+ if (choice === "1") {
429
+ return { path: cwd, label: `project (${cwd})` };
430
+ } else if (choice === "2") {
431
+ return { path: "global", label: "global (~/.claude/settings.json)" };
432
+ } else if (choice === "3") {
433
+ const inputPath = await prompt("Path: ");
434
+ if (!inputPath) {
435
+ console.log(c.red("\u2717"), "No path provided");
436
+ return null;
437
+ }
438
+ const fullPath = resolve(inputPath);
439
+ if (!existsSync2(fullPath)) {
440
+ console.log(c.red("\u2717"), `Path does not exist: ${fullPath}`);
441
+ return null;
442
+ }
443
+ return { path: fullPath, label: `project (${fullPath})` };
444
+ } else {
445
+ console.log(c.red("\u2717"), "Invalid choice");
446
+ return null;
447
+ }
448
+ }
449
+ async function promptForConfig(existingConfig = {}, projectPath) {
450
+ const config = { ...existingConfig };
451
+ const availableLists = projectPath ? getProjectTaskLists2(projectPath) : getAllTaskLists2();
248
452
  console.log(`
249
- ${colors.bold("Installing hook-checktasks")} (${scope})
453
+ ${c.bold("Configuration")}
250
454
  `);
251
- if (!global && !existsSync2(join2(process.cwd(), ".git"))) {
252
- console.log(colors.yellow("\u26A0 Warning:"), "Current directory is not a git repository");
253
- console.log(` The hook will still be installed to .claude/settings.json
455
+ console.log(c.bold("Task List ID:"));
456
+ if (availableLists.length > 0) {
457
+ console.log(c.dim(" Available lists for this project:"));
458
+ availableLists.forEach((list, i) => {
459
+ console.log(c.dim(` ${i + 1}. ${list}`));
460
+ });
461
+ } else {
462
+ console.log(c.dim(" No task lists found for this project"));
463
+ }
464
+ console.log(c.dim(` Leave empty to check all matching lists
465
+ `));
466
+ const currentList = config.taskListId || "(all lists)";
467
+ const listInput = await prompt(`Task list ID [${c.cyan(currentList)}]: `);
468
+ if (listInput) {
469
+ const num = parseInt(listInput, 10);
470
+ if (!isNaN(num) && num > 0 && num <= availableLists.length) {
471
+ config.taskListId = availableLists[num - 1];
472
+ } else {
473
+ config.taskListId = listInput;
474
+ }
475
+ } else if (!existingConfig.taskListId) {
476
+ config.taskListId = undefined;
477
+ }
478
+ const currentKeywords = config.keywords?.join(", ") || "dev";
479
+ const keywordsInput = await prompt(`Keywords (comma-separated) [${c.cyan(currentKeywords)}]: `);
480
+ if (keywordsInput) {
481
+ config.keywords = keywordsInput.split(",").map((k) => k.trim().toLowerCase()).filter(Boolean);
482
+ } else if (!existingConfig.keywords) {
483
+ config.keywords = ["dev"];
484
+ }
485
+ config.enabled = true;
486
+ return config;
487
+ }
488
+ async function install(args) {
489
+ console.log(`
490
+ ${c.bold("hook-checktasks install")}
254
491
  `);
492
+ const options = parseInstallArgs(args);
493
+ let target = null;
494
+ if (options.global) {
495
+ target = { path: "global", label: "global (~/.claude/settings.json)" };
496
+ } else if (options.path) {
497
+ const fullPath = resolve(options.path);
498
+ if (!existsSync2(fullPath)) {
499
+ console.log(c.red("\u2717"), `Path does not exist: ${fullPath}`);
500
+ return;
501
+ }
502
+ target = { path: fullPath, label: `project (${fullPath})` };
503
+ } else if (options.yes) {
504
+ const cwd = process.cwd();
505
+ target = { path: cwd, label: `project (${cwd})` };
506
+ } else {
507
+ target = await resolveTarget(args);
255
508
  }
256
- const settings = readSettings(settingsPath);
509
+ if (!target)
510
+ return;
511
+ const settingsPath = getSettingsPath(target.path);
512
+ let settings = readSettings2(settingsPath);
257
513
  if (hookExists(settings)) {
258
- console.log(colors.yellow("!"), "Hook already installed in", settingsPath);
259
- console.log(`
260
- To reinstall, run uninstall first:`);
261
- console.log(` bunx ${PACKAGE_NAME} uninstall${global ? " --global" : ""}
514
+ console.log(c.yellow("!"), `Hook already installed in ${target.label}`);
515
+ if (!options.yes) {
516
+ const update = await prompt("Update configuration? (y/n): ");
517
+ if (update.toLowerCase() !== "y")
518
+ return;
519
+ }
520
+ } else {
521
+ settings = addHook(settings);
522
+ }
523
+ const existingConfig = getConfig2(settings);
524
+ let config;
525
+ if (options.yes || options.taskListId || options.keywords) {
526
+ config = {
527
+ ...existingConfig,
528
+ taskListId: options.taskListId || existingConfig.taskListId,
529
+ keywords: options.keywords || existingConfig.keywords || ["dev"],
530
+ enabled: true
531
+ };
532
+ } else {
533
+ const projectPath = target.path === "global" ? undefined : target.path;
534
+ config = await promptForConfig(existingConfig, projectPath);
535
+ }
536
+ settings = setConfig(settings, config);
537
+ writeSettings(settingsPath, settings);
538
+ console.log();
539
+ console.log(c.green("\u2713"), `Installed to ${target.label}`);
540
+ console.log();
541
+ console.log(c.bold("Configuration:"));
542
+ console.log(` Task list: ${config.taskListId || c.cyan("(all lists)")}`);
543
+ console.log(` Keywords: ${config.keywords?.join(", ") || "dev"}`);
544
+ console.log();
545
+ }
546
+ async function configure(args) {
547
+ console.log(`
548
+ ${c.bold("hook-checktasks config")}
262
549
  `);
550
+ const target = await resolveTarget(args);
551
+ if (!target)
552
+ return;
553
+ const settingsPath = getSettingsPath(target.path);
554
+ if (!existsSync2(settingsPath)) {
555
+ console.log(c.red("\u2717"), `No settings file at ${settingsPath}`);
556
+ console.log(c.dim(" Run 'hook-checktasks install' first"));
263
557
  return;
264
558
  }
265
- const updatedSettings = addHook(settings);
266
- writeSettings(settingsPath, updatedSettings);
267
- console.log(colors.green("\u2713"), "Hook installed to", settingsPath);
559
+ let settings = readSettings2(settingsPath);
560
+ if (!hookExists(settings)) {
561
+ console.log(c.red("\u2717"), `Hook not installed in ${target.label}`);
562
+ console.log(c.dim(" Run 'hook-checktasks install' first"));
563
+ return;
564
+ }
565
+ const existingConfig = getConfig2(settings);
566
+ const projectPath = target.path === "global" ? undefined : target.path;
567
+ const config = await promptForConfig(existingConfig, projectPath);
568
+ settings = setConfig(settings, config);
569
+ writeSettings(settingsPath, settings);
268
570
  console.log();
269
- console.log(colors.bold("Configuration (environment variables):"));
270
- console.log(" CLAUDE_CODE_TASK_LIST_ID Task list to monitor (required)");
271
- console.log(" CHECK_TASKS_KEYWORDS Keywords to trigger check (default: 'dev')");
272
- console.log(" CHECK_TASKS_DISABLED Set to '1' to disable the hook");
571
+ console.log(c.green("\u2713"), `Configuration updated`);
273
572
  console.log();
274
- console.log(colors.bold("Usage:"));
275
- console.log(" CLAUDE_CODE_TASK_LIST_ID=myproject-dev claude");
573
+ console.log(c.bold("New configuration:"));
574
+ console.log(` Task list: ${config.taskListId || c.cyan("(all lists)")}`);
575
+ console.log(` Keywords: ${config.keywords?.join(", ") || "dev"}`);
276
576
  console.log();
277
577
  }
278
- function uninstall(global) {
279
- const scope = global ? "global" : "project";
280
- const settingsPath = getSettingsPath(global);
578
+ async function uninstall(args) {
281
579
  console.log(`
282
- ${colors.bold("Uninstalling hook-checktasks")} (${scope})
580
+ ${c.bold("hook-checktasks uninstall")}
283
581
  `);
582
+ const target = await resolveTarget(args);
583
+ if (!target)
584
+ return;
585
+ const settingsPath = getSettingsPath(target.path);
284
586
  if (!existsSync2(settingsPath)) {
285
- console.log(colors.yellow("!"), "No settings file found at", settingsPath);
587
+ console.log(c.yellow("!"), `No settings file at ${settingsPath}`);
286
588
  return;
287
589
  }
288
- const settings = readSettings(settingsPath);
590
+ const settings = readSettings2(settingsPath);
289
591
  if (!hookExists(settings)) {
290
- console.log(colors.yellow("!"), "Hook not found in", settingsPath);
592
+ console.log(c.yellow("!"), `Hook not found in ${target.label}`);
291
593
  return;
292
594
  }
293
- const updatedSettings = removeHook(settings);
294
- writeSettings(settingsPath, updatedSettings);
295
- console.log(colors.green("\u2713"), "Hook removed from", settingsPath);
296
- console.log();
595
+ const updated = removeHook(settings);
596
+ writeSettings(settingsPath, updated);
597
+ console.log(c.green("\u2713"), `Removed from ${target.label}`);
297
598
  }
298
599
  function status() {
299
600
  console.log(`
300
- ${colors.bold("hook-checktasks status")}
601
+ ${c.bold("hook-checktasks status")}
301
602
  `);
302
- const globalPath = getSettingsPath(true);
303
- const projectPath = getSettingsPath(false);
304
- const globalSettings = readSettings(globalPath);
603
+ const globalPath = getSettingsPath("global");
604
+ const globalSettings = readSettings2(globalPath);
305
605
  const globalInstalled = hookExists(globalSettings);
306
- console.log(globalInstalled ? colors.green("\u2713") : colors.red("\u2717"), "Global:", globalInstalled ? "Installed" : "Not installed", colors.cyan(`(${globalPath})`));
606
+ const globalConfig = getConfig2(globalSettings);
607
+ console.log(globalInstalled ? c.green("\u2713") : c.red("\u2717"), "Global:", globalInstalled ? "Installed" : "Not installed", c.dim(`(${globalPath})`));
608
+ if (globalInstalled) {
609
+ console.log(c.dim(` List: ${globalConfig.taskListId || "(all)"}, Keywords: ${globalConfig.keywords?.join(", ") || "dev"}`));
610
+ }
611
+ const cwd = process.cwd();
612
+ const projectPath = getSettingsPath(cwd);
307
613
  if (existsSync2(projectPath)) {
308
- const projectSettings = readSettings(projectPath);
614
+ const projectSettings = readSettings2(projectPath);
309
615
  const projectInstalled = hookExists(projectSettings);
310
- console.log(projectInstalled ? colors.green("\u2713") : colors.red("\u2717"), "Project:", projectInstalled ? "Installed" : "Not installed", colors.cyan(`(${projectPath})`));
616
+ const projectConfig = getConfig2(projectSettings);
617
+ console.log(projectInstalled ? c.green("\u2713") : c.red("\u2717"), "Project:", projectInstalled ? "Installed" : "Not installed", c.dim(`(${projectPath})`));
618
+ if (projectInstalled) {
619
+ console.log(c.dim(` List: ${projectConfig.taskListId || "(all)"}, Keywords: ${projectConfig.keywords?.join(", ") || "dev"}`));
620
+ }
311
621
  } else {
312
- console.log(colors.red("\u2717"), "Project: No .claude/settings.json");
622
+ console.log(c.dim("\xB7"), "Project:", c.dim("No .claude/settings.json"));
623
+ }
624
+ const projectLists = getProjectTaskLists2(cwd);
625
+ if (projectLists.length > 0) {
626
+ console.log();
627
+ console.log(c.bold("Task lists for this project:"));
628
+ projectLists.forEach((list) => console.log(c.dim(` - ${list}`)));
629
+ } else {
630
+ const allLists = getAllTaskLists2();
631
+ if (allLists.length > 0) {
632
+ console.log();
633
+ console.log(c.bold("All task lists:"), c.dim("(none match this project)"));
634
+ allLists.slice(0, 10).forEach((list) => console.log(c.dim(` - ${list}`)));
635
+ if (allLists.length > 10) {
636
+ console.log(c.dim(` ... and ${allLists.length - 10} more`));
637
+ }
638
+ }
639
+ }
640
+ const envTaskList = process.env.CLAUDE_CODE_TASK_LIST_ID;
641
+ if (envTaskList) {
642
+ console.log();
643
+ console.log(c.bold("Environment (legacy):"));
644
+ console.log(` CLAUDE_CODE_TASK_LIST_ID: ${envTaskList}`);
313
645
  }
314
- console.log();
315
- console.log(colors.bold("Environment:"));
316
- console.log(" CLAUDE_CODE_TASK_LIST_ID:", process.env.CLAUDE_CODE_TASK_LIST_ID || colors.yellow("(not set)"));
317
- console.log(" CHECK_TASKS_KEYWORDS:", process.env.CHECK_TASKS_KEYWORDS || "dev (default)");
318
- console.log(" CHECK_TASKS_DISABLED:", process.env.CHECK_TASKS_DISABLED === "1" ? colors.yellow("yes") : "no");
319
646
  console.log();
320
647
  }
321
648
  var args = process.argv.slice(2);
322
649
  var command = args[0];
323
- var isGlobal = args.includes("--global") || args.includes("-g");
650
+ var commandArgs = args.slice(1);
324
651
  switch (command) {
325
652
  case "install":
326
- install(isGlobal);
653
+ install(commandArgs);
654
+ break;
655
+ case "config":
656
+ configure(commandArgs);
327
657
  break;
328
658
  case "uninstall":
329
- uninstall(isGlobal);
659
+ uninstall(commandArgs);
330
660
  break;
331
661
  case "run":
332
662
  Promise.resolve().then(() => (init_hook(), exports_hook)).then((m) => m.run());
@@ -340,7 +670,7 @@ switch (command) {
340
670
  printUsage();
341
671
  break;
342
672
  default:
343
- console.error(colors.red(`Unknown command: ${command}`));
673
+ console.error(c.red(`Unknown command: ${command}`));
344
674
  printUsage();
345
675
  process.exit(1);
346
676
  }
package/dist/hook.js CHANGED
@@ -17,6 +17,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
17
17
  import { readdirSync, readFileSync, existsSync } from "fs";
18
18
  import { join } from "path";
19
19
  import { homedir } from "os";
20
+ var CONFIG_KEY = "checkTasksConfig";
20
21
  function readStdinJson() {
21
22
  try {
22
23
  const stdin = readFileSync(0, "utf-8");
@@ -25,6 +26,30 @@ function readStdinJson() {
25
26
  return null;
26
27
  }
27
28
  }
29
+ function readSettings(path) {
30
+ if (!existsSync(path))
31
+ return {};
32
+ try {
33
+ return JSON.parse(readFileSync(path, "utf-8"));
34
+ } catch {
35
+ return {};
36
+ }
37
+ }
38
+ function getConfig(cwd) {
39
+ const projectSettings = readSettings(join(cwd, ".claude", "settings.json"));
40
+ if (projectSettings[CONFIG_KEY]) {
41
+ return projectSettings[CONFIG_KEY];
42
+ }
43
+ const globalSettings = readSettings(join(homedir(), ".claude", "settings.json"));
44
+ if (globalSettings[CONFIG_KEY]) {
45
+ return globalSettings[CONFIG_KEY];
46
+ }
47
+ return {
48
+ taskListId: process.env.CLAUDE_CODE_TASK_LIST_ID,
49
+ keywords: process.env.CHECK_TASKS_KEYWORDS?.split(",").map((k) => k.trim().toLowerCase()).filter(Boolean) || ["dev"],
50
+ enabled: process.env.CHECK_TASKS_DISABLED !== "1"
51
+ };
52
+ }
28
53
  function getSessionName(transcriptPath) {
29
54
  if (!existsSync(transcriptPath))
30
55
  return null;
@@ -54,6 +79,44 @@ function getSessionName(transcriptPath) {
54
79
  return null;
55
80
  }
56
81
  }
82
+ function getAllTaskLists() {
83
+ const tasksDir = join(homedir(), ".claude", "tasks");
84
+ if (!existsSync(tasksDir))
85
+ return [];
86
+ try {
87
+ return readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
88
+ } catch {
89
+ return [];
90
+ }
91
+ }
92
+ function getProjectTaskLists(cwd) {
93
+ const allLists = getAllTaskLists();
94
+ const dirName = cwd.split("/").filter(Boolean).pop() || "";
95
+ const projectLists = allLists.filter((list) => {
96
+ const listLower = list.toLowerCase();
97
+ const dirLower = dirName.toLowerCase();
98
+ if (listLower.startsWith(dirLower + "-"))
99
+ return true;
100
+ if (listLower === dirLower)
101
+ return true;
102
+ return false;
103
+ });
104
+ return projectLists;
105
+ }
106
+ function getTasksFromList(listId) {
107
+ const tasksDir = join(homedir(), ".claude", "tasks", listId);
108
+ if (!existsSync(tasksDir))
109
+ return [];
110
+ try {
111
+ const taskFiles = readdirSync(tasksDir).filter((f) => f.endsWith(".json"));
112
+ return taskFiles.map((file) => {
113
+ const content = readFileSync(join(tasksDir, file), "utf-8");
114
+ return JSON.parse(content);
115
+ });
116
+ } catch {
117
+ return [];
118
+ }
119
+ }
57
120
  function approve() {
58
121
  console.log(JSON.stringify({ decision: "approve" }));
59
122
  process.exit(0);
@@ -63,61 +126,87 @@ function block(reason) {
63
126
  process.exit(0);
64
127
  }
65
128
  function run() {
66
- if (process.env.CHECK_TASKS_DISABLED === "1") {
67
- approve();
68
- }
69
- const taskListId = process.env.CLAUDE_CODE_TASK_LIST_ID;
70
- if (!taskListId) {
129
+ const hookInput = readStdinJson();
130
+ const cwd = hookInput?.cwd || process.cwd();
131
+ const config = getConfig(cwd);
132
+ if (config.enabled === false) {
71
133
  approve();
72
134
  }
73
- const hookInput = readStdinJson();
74
135
  let sessionName = null;
75
136
  if (hookInput?.transcript_path) {
76
137
  sessionName = getSessionName(hookInput.transcript_path);
77
138
  }
78
- const nameToCheck = sessionName || taskListId || "";
79
- const keywords = (process.env.CHECK_TASKS_KEYWORDS || "dev").split(",").map((k) => k.trim().toLowerCase()).filter(Boolean);
80
- const matchesKeyword = keywords.some((keyword) => nameToCheck.toLowerCase().includes(keyword));
81
- if (!matchesKeyword) {
139
+ const nameToCheck = sessionName || config.taskListId || "";
140
+ const keywords = config.keywords || ["dev"];
141
+ const matchesKeyword = keywords.some((keyword) => nameToCheck.toLowerCase().includes(keyword.toLowerCase()));
142
+ if (!matchesKeyword && keywords.length > 0 && nameToCheck) {
82
143
  approve();
83
144
  }
84
- const tasksDir = join(homedir(), ".claude", "tasks", taskListId);
85
- if (!existsSync(tasksDir)) {
86
- approve();
145
+ let listsToCheck = [];
146
+ if (config.taskListId) {
147
+ listsToCheck = [config.taskListId];
148
+ } else {
149
+ const projectLists = getProjectTaskLists(cwd);
150
+ if (projectLists.length > 0) {
151
+ if (keywords.length > 0) {
152
+ listsToCheck = projectLists.filter((list) => keywords.some((keyword) => list.toLowerCase().includes(keyword.toLowerCase())));
153
+ } else {
154
+ listsToCheck = projectLists;
155
+ }
156
+ }
87
157
  }
88
- const taskFiles = readdirSync(tasksDir).filter((f) => f.endsWith(".json"));
89
- if (taskFiles.length === 0) {
158
+ if (listsToCheck.length === 0) {
90
159
  approve();
91
160
  }
92
- const tasks = taskFiles.map((file) => {
93
- const content = readFileSync(join(tasksDir, file), "utf-8");
94
- return JSON.parse(content);
95
- });
96
- const pending = tasks.filter((t) => t.status === "pending");
97
- const inProgress = tasks.filter((t) => t.status === "in_progress");
98
- const completed = tasks.filter((t) => t.status === "completed");
99
- const remainingCount = pending.length + inProgress.length;
161
+ let allPending = [];
162
+ let allInProgress = [];
163
+ let allCompleted = [];
164
+ let activeListId = null;
165
+ for (const listId of listsToCheck) {
166
+ const tasks = getTasksFromList(listId);
167
+ const pending = tasks.filter((t) => t.status === "pending");
168
+ const inProgress = tasks.filter((t) => t.status === "in_progress");
169
+ const completed = tasks.filter((t) => t.status === "completed");
170
+ if (pending.length > 0 || inProgress.length > 0) {
171
+ activeListId = listId;
172
+ }
173
+ allPending.push(...pending);
174
+ allInProgress.push(...inProgress);
175
+ allCompleted.push(...completed);
176
+ }
177
+ const remainingCount = allPending.length + allInProgress.length;
100
178
  if (remainingCount > 0) {
101
- const nextTasks = pending.slice(0, 3).map((t) => `- ${t.subject}`).join(`
179
+ const nextTasks = allPending.slice(0, 3).map((t) => `- ${t.subject}`).join(`
102
180
  `);
181
+ const listInfo = activeListId ? ` in "${activeListId}"` : "";
103
182
  const prompt = `
104
- STOP BLOCKED: You have ${remainingCount} tasks remaining (${pending.length} pending, ${inProgress.length} in progress, ${completed.length} completed).
183
+ STOP BLOCKED: You have ${remainingCount} tasks remaining${listInfo} (${allPending.length} pending, ${allInProgress.length} in progress, ${allCompleted.length} completed).
184
+
185
+ ⛔ DO NOT STOP. DO NOT ASK QUESTIONS. DO NOT WAIT FOR USER INPUT.
105
186
 
106
- You MUST continue working. Do NOT stop until all tasks are completed.
187
+ You MUST continue working AUTONOMOUSLY until ALL tasks are completed.
107
188
 
108
189
  Next pending tasks:
109
190
  ${nextTasks}
110
- ${pending.length > 3 ? `... and ${pending.length - 3} more pending tasks` : ""}
191
+ ${allPending.length > 3 ? `... and ${allPending.length - 3} more pending tasks` : ""}
111
192
 
112
- INSTRUCTIONS:
193
+ MANDATORY INSTRUCTIONS (follow these NOW):
113
194
  1. Use TaskList to see all tasks
114
- 2. Pick the next pending task
115
- 3. Use TaskUpdate to mark it as in_progress
116
- 4. Complete the task
117
- 5. Use TaskUpdate to mark it as completed
118
- 6. Repeat until all tasks are done
195
+ 2. Use TaskGet to read the FIRST pending task's full description
196
+ 3. Use TaskUpdate to mark it as in_progress BEFORE starting work
197
+ 4. Complete the task (write code, run commands, etc.)
198
+ 5. Use TaskUpdate to mark it as completed AFTER finishing
199
+ 6. IMMEDIATELY move to the next task - DO NOT STOP
200
+
201
+ CRITICAL RULES:
202
+ - NEVER ask "would you like me to..." - just DO IT
203
+ - NEVER ask for confirmation - just WORK
204
+ - NEVER stop to explain what you'll do - just DO IT
205
+ - If a task is unclear, make reasonable assumptions and proceed
206
+ - If you encounter an error, fix it and continue
207
+ - Keep working until remainingCount = 0
119
208
 
120
- DO NOT STOP. Continue working now.
209
+ START WORKING NOW. Use TaskList tool in your next response.
121
210
  `.trim();
122
211
  block(prompt);
123
212
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasnaxyz/hook-checktasks",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Claude Code hook that prevents stopping when there are pending tasks",
5
5
  "type": "module",
6
6
  "bin": {