vibe-forge 0.8.1 → 0.8.2

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 (51) hide show
  1. package/.claude/commands/configure-vcs.md +102 -102
  2. package/.claude/commands/forge.md +218 -218
  3. package/.claude/hooks/worker-loop.js +220 -217
  4. package/.claude/settings.json +89 -89
  5. package/README.md +149 -191
  6. package/agents/aegis/personality.md +303 -303
  7. package/agents/anvil/personality.md +278 -278
  8. package/agents/architect/personality.md +260 -260
  9. package/agents/crucible/personality.md +362 -362
  10. package/agents/crucible-x/personality.md +210 -210
  11. package/agents/ember/personality.md +293 -293
  12. package/agents/flux/personality.md +248 -248
  13. package/agents/furnace/personality.md +342 -342
  14. package/agents/herald/personality.md +249 -249
  15. package/agents/oracle/personality.md +284 -284
  16. package/agents/pixel/personality.md +140 -140
  17. package/agents/planning-hub/personality.md +473 -473
  18. package/agents/scribe/personality.md +253 -253
  19. package/agents/slag/personality.md +268 -268
  20. package/agents/temper/personality.md +270 -270
  21. package/bin/cli.js +372 -372
  22. package/bin/forge-daemon.sh +477 -477
  23. package/bin/forge-setup.sh +662 -661
  24. package/bin/forge-spawn.sh +164 -164
  25. package/bin/forge.sh +566 -566
  26. package/docs/commands.md +8 -8
  27. package/package.json +77 -77
  28. package/{bin → src}/lib/agents.sh +177 -177
  29. package/{bin → src}/lib/check-aliases.js +50 -50
  30. package/{bin → src}/lib/colors.sh +45 -44
  31. package/{bin → src}/lib/config.sh +347 -347
  32. package/{bin → src}/lib/constants.sh +241 -241
  33. package/{bin → src}/lib/daemon/budgets.sh +107 -107
  34. package/{bin → src}/lib/daemon/dependencies.sh +146 -146
  35. package/{bin → src}/lib/daemon/display.sh +128 -128
  36. package/{bin → src}/lib/daemon/notifications.sh +273 -273
  37. package/{bin → src}/lib/daemon/routing.sh +93 -93
  38. package/{bin → src}/lib/daemon/state.sh +163 -163
  39. package/{bin → src}/lib/daemon/sync.sh +103 -103
  40. package/{bin → src}/lib/database.sh +357 -357
  41. package/{bin → src}/lib/frontmatter.js +106 -106
  42. package/{bin → src}/lib/heimdall-setup.js +113 -113
  43. package/{bin → src}/lib/heimdall.js +265 -265
  44. package/src/lib/index.sh +25 -0
  45. package/{bin → src}/lib/json.sh +264 -264
  46. package/{bin → src}/lib/terminal.js +452 -452
  47. package/{bin → src}/lib/util.sh +126 -126
  48. package/{bin → src}/lib/vcs.js +349 -349
  49. package/{context → templates}/project-context-template.md +122 -122
  50. package/config/task-template.md +0 -159
  51. package/config/templates/handoff-template.md +0 -40
@@ -1,217 +1,220 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Vibe Forge Worker Loop - Stop Hook (Cross-Platform)
4
- *
5
- * Implements the Ralph Loop technique for Vibe Forge workers.
6
- * When a worker tries to exit, this hook checks if there are pending tasks
7
- * and feeds the worker prompt back to continue working.
8
- *
9
- * Based on the Ralph Loop plugin by Anthropic.
10
- *
11
- * Activation modes:
12
- * 1. Config-based: worker_loop_enabled=true in .forge/config.json
13
- * 2. Runtime: State file created by /worker-loop command
14
- */
15
-
16
- const fs = require('fs');
17
- const path = require('path');
18
- const os = require('os');
19
-
20
- // State file location (runtime toggle)
21
- const claudeLocalDir = process.env.CLAUDE_LOCAL_DIR || path.join(os.homedir(), '.claude');
22
- const stateFile = path.join(claudeLocalDir, 'forge-worker-loop.json');
23
- // SEC-005: Validate FORGE_ROOT to prevent path traversal via attacker-controlled env
24
- const rawForgeRoot = process.env.FORGE_ROOT || process.cwd();
25
- const forgeRoot = validateForgeRoot(rawForgeRoot) ? rawForgeRoot : null;
26
-
27
- function validateForgeRoot(dir) {
28
- try {
29
- // Must contain tasks/ and .forge/ (or config/) to be a valid forge project
30
- const hasTasksDir = fs.existsSync(path.join(dir, 'tasks'));
31
- const hasForgeDir = fs.existsSync(path.join(dir, '.forge')) || fs.existsSync(path.join(dir, 'config'));
32
- return hasTasksDir && hasForgeDir;
33
- } catch (_) {
34
- return false;
35
- }
36
- }
37
-
38
- // Helper to safely parse JSON
39
- function safeJsonParse(filePath, defaultValue = {}) {
40
- try {
41
- if (fs.existsSync(filePath)) {
42
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
43
- }
44
- } catch (e) {
45
- // Ignore parse errors
46
- }
47
- return defaultValue;
48
- }
49
-
50
- // Helper to count matching files
51
- function countFiles(dir, pattern = '*.md') {
52
- try {
53
- if (!fs.existsSync(dir)) return 0;
54
- const files = fs.readdirSync(dir);
55
- return files.filter(f => f.endsWith('.md')).length;
56
- } catch (e) {
57
- return 0;
58
- }
59
- }
60
-
61
- // Helper to count files with specific content
62
- function countFilesWithContent(dir, searchPattern) {
63
- try {
64
- if (!fs.existsSync(dir)) return 0;
65
- const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
66
- let count = 0;
67
- for (const file of files) {
68
- try {
69
- const content = fs.readFileSync(path.join(dir, file), 'utf8');
70
- if (content.includes(searchPattern)) {
71
- count++;
72
- }
73
- } catch (e) {
74
- // Skip files we can't read
75
- }
76
- }
77
- return count;
78
- } catch (e) {
79
- return 0;
80
- }
81
- }
82
-
83
- // Output result and exit
84
- function output(result) {
85
- console.log(JSON.stringify(result));
86
- process.exit(0);
87
- }
88
-
89
- // Main logic
90
- function main() {
91
- // SEC-005: Abort safely if forge root is invalid
92
- if (!forgeRoot) {
93
- output({ decision: 'allow' });
94
- return;
95
- }
96
-
97
- let loopActive = false;
98
- let workerAgent = '';
99
- let maxIdleChecks = 10;
100
- let idleCount = 0;
101
- let pollInterval = 5;
102
-
103
- // Check for runtime state file first (takes precedence)
104
- if (fs.existsSync(stateFile)) {
105
- const state = safeJsonParse(stateFile);
106
- loopActive = true;
107
- workerAgent = state.agent || '';
108
- maxIdleChecks = state.max_idle_checks || 10;
109
- idleCount = state.idle_count || 0;
110
- pollInterval = state.poll_interval || 5;
111
- }
112
-
113
- // If no runtime state, check config-based setting
114
- if (!loopActive) {
115
- const configFile = path.join(forgeRoot, '.forge', 'config.json');
116
- const config = safeJsonParse(configFile);
117
- if (config.worker_loop_enabled === true) {
118
- loopActive = true;
119
- workerAgent = 'any';
120
- }
121
- }
122
-
123
- // Check if worker loop is active
124
- if (!loopActive) {
125
- return output({ decision: 'approve' });
126
- }
127
-
128
- if (!workerAgent) {
129
- // Invalid state, clean up and allow exit
130
- try { fs.unlinkSync(stateFile); } catch (e) {}
131
- return output({ decision: 'approve' });
132
- }
133
-
134
- // Determine tasks directory location
135
- // When running from vibe-forge repo: tasks are at ./tasks/
136
- // When running from a project using vibe-forge as submodule: tasks are at ./_vibe-forge/tasks/
137
- let tasksDir = path.join(forgeRoot, 'tasks');
138
- if (!fs.existsSync(tasksDir)) {
139
- // Fall back to submodule location
140
- tasksDir = path.join(forgeRoot, '_vibe-forge', 'tasks');
141
- }
142
-
143
- // Check for pending tasks
144
- let pendingCount = 0;
145
- let needsChangesCount = 0;
146
-
147
- const pendingDir = path.join(tasksDir, 'pending');
148
- const needsChangesDir = path.join(tasksDir, 'needs-changes');
149
-
150
- if (workerAgent === 'any') {
151
- // Config mode: count all tasks
152
- pendingCount = countFiles(pendingDir);
153
- needsChangesCount = countFiles(needsChangesDir);
154
- } else {
155
- // Runtime mode: count only tasks assigned to specific worker
156
- const searchPattern = `assigned_to: ${workerAgent}`;
157
- pendingCount = countFilesWithContent(pendingDir, searchPattern);
158
- needsChangesCount = countFilesWithContent(needsChangesDir, searchPattern);
159
- }
160
-
161
- const totalTasks = pendingCount + needsChangesCount;
162
-
163
- // Get relative task path for prompts (relative to forgeRoot)
164
- const relativeTasksPath = path.relative(forgeRoot, tasksDir);
165
-
166
- if (totalTasks > 0) {
167
- // Tasks found! Reset idle counter and continue
168
- if (fs.existsSync(stateFile)) {
169
- try {
170
- const state = safeJsonParse(stateFile);
171
- state.idle_count = 0;
172
- fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
173
- } catch (e) {}
174
- }
175
-
176
- return output({
177
- decision: 'block',
178
- message: `[Forge Loop] Found ${totalTasks} pending task(s). Continuing work...`,
179
- prompt: `Check ${relativeTasksPath}/pending/ and ${relativeTasksPath}/needs-changes/ for tasks assigned to you and begin working on them immediately.`
180
- });
181
- }
182
-
183
- // No tasks - handle idle state
184
- if (fs.existsSync(stateFile)) {
185
- idleCount++;
186
-
187
- try {
188
- const state = safeJsonParse(stateFile);
189
- state.idle_count = idleCount;
190
- fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
191
- } catch (e) {}
192
-
193
- if (idleCount >= maxIdleChecks) {
194
- // Max idle checks reached, clean up and allow exit
195
- try { fs.unlinkSync(stateFile); } catch (e) {}
196
- return output({
197
- decision: 'approve',
198
- message: `[Forge Loop] No tasks found after ${maxIdleChecks} checks. Exiting.`
199
- });
200
- }
201
-
202
- // Still within idle limit - wait and check again
203
- return output({
204
- decision: 'block',
205
- message: `[Forge Loop] No tasks available. Idle check ${idleCount}/${maxIdleChecks}. Waiting...`,
206
- prompt: `No tasks currently assigned to you. Wait briefly, then check ${relativeTasksPath}/pending/ and ${relativeTasksPath}/needs-changes/ again for new work. If still no tasks, announce you are idle and ready.`
207
- });
208
- }
209
-
210
- // Config-based mode - no idle tracking, just allow exit when no tasks
211
- return output({
212
- decision: 'approve',
213
- message: '[Forge Loop] No pending tasks. Worker exiting.'
214
- });
215
- }
216
-
217
- main();
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Vibe Forge Worker Loop - Stop Hook (Cross-Platform)
4
+ *
5
+ * Implements the Ralph Loop technique for Vibe Forge workers.
6
+ * When a worker tries to exit, this hook checks if there are pending tasks
7
+ * and feeds the worker prompt back to continue working.
8
+ *
9
+ * Based on the Ralph Loop plugin by Anthropic.
10
+ *
11
+ * Activation modes:
12
+ * 1. Config-based: worker_loop_enabled=true in .forge/config.json
13
+ * 2. Runtime: State file created by /worker-loop command
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const os = require('os');
19
+
20
+ // State file location (runtime toggle)
21
+ const claudeLocalDir = process.env.CLAUDE_LOCAL_DIR || path.join(os.homedir(), '.claude');
22
+ const stateFile = path.join(claudeLocalDir, 'forge-worker-loop.json');
23
+ // SEC-005: Validate FORGE_ROOT to prevent path traversal via attacker-controlled env
24
+ const rawForgeRoot = process.env.FORGE_ROOT || process.cwd();
25
+ const forgeRoot = validateForgeRoot(rawForgeRoot) ? rawForgeRoot : null;
26
+
27
+ function validateForgeRoot(dir) {
28
+ try {
29
+ if (!dir || typeof dir !== 'string') return false;
30
+ // Must be an absolute path to prevent cwd-relative resolution
31
+ if (!path.isAbsolute(dir)) return false;
32
+ // Must contain tasks/ and .forge/ (or config/) to be a valid forge project
33
+ const hasTasksDir = fs.existsSync(path.join(dir, 'tasks'));
34
+ const hasForgeDir = fs.existsSync(path.join(dir, '.forge')) || fs.existsSync(path.join(dir, 'config'));
35
+ return hasTasksDir && hasForgeDir;
36
+ } catch (_) {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ // Helper to safely parse JSON
42
+ function safeJsonParse(filePath, defaultValue = {}) {
43
+ try {
44
+ if (fs.existsSync(filePath)) {
45
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
46
+ }
47
+ } catch (e) {
48
+ // Ignore parse errors
49
+ }
50
+ return defaultValue;
51
+ }
52
+
53
+ // Helper to count matching files
54
+ function countFiles(dir, pattern = '*.md') {
55
+ try {
56
+ if (!fs.existsSync(dir)) return 0;
57
+ const files = fs.readdirSync(dir);
58
+ return files.filter(f => f.endsWith('.md')).length;
59
+ } catch (e) {
60
+ return 0;
61
+ }
62
+ }
63
+
64
+ // Helper to count files with specific content
65
+ function countFilesWithContent(dir, searchPattern) {
66
+ try {
67
+ if (!fs.existsSync(dir)) return 0;
68
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
69
+ let count = 0;
70
+ for (const file of files) {
71
+ try {
72
+ const content = fs.readFileSync(path.join(dir, file), 'utf8');
73
+ if (content.includes(searchPattern)) {
74
+ count++;
75
+ }
76
+ } catch (e) {
77
+ // Skip files we can't read
78
+ }
79
+ }
80
+ return count;
81
+ } catch (e) {
82
+ return 0;
83
+ }
84
+ }
85
+
86
+ // Output result and exit
87
+ function output(result) {
88
+ console.log(JSON.stringify(result));
89
+ process.exit(0);
90
+ }
91
+
92
+ // Main logic
93
+ function main() {
94
+ // SEC-005: Abort safely if forge root is invalid
95
+ if (!forgeRoot) {
96
+ output({ decision: 'allow' });
97
+ return;
98
+ }
99
+
100
+ let loopActive = false;
101
+ let workerAgent = '';
102
+ let maxIdleChecks = 10;
103
+ let idleCount = 0;
104
+ let pollInterval = 5;
105
+
106
+ // Check for runtime state file first (takes precedence)
107
+ if (fs.existsSync(stateFile)) {
108
+ const state = safeJsonParse(stateFile);
109
+ loopActive = true;
110
+ workerAgent = state.agent || '';
111
+ maxIdleChecks = state.max_idle_checks || 10;
112
+ idleCount = state.idle_count || 0;
113
+ pollInterval = state.poll_interval || 5;
114
+ }
115
+
116
+ // If no runtime state, check config-based setting
117
+ if (!loopActive) {
118
+ const configFile = path.join(forgeRoot, '.forge', 'config.json');
119
+ const config = safeJsonParse(configFile);
120
+ if (config.worker_loop_enabled === true) {
121
+ loopActive = true;
122
+ workerAgent = 'any';
123
+ }
124
+ }
125
+
126
+ // Check if worker loop is active
127
+ if (!loopActive) {
128
+ return output({ decision: 'approve' });
129
+ }
130
+
131
+ if (!workerAgent) {
132
+ // Invalid state, clean up and allow exit
133
+ try { fs.unlinkSync(stateFile); } catch (e) {}
134
+ return output({ decision: 'approve' });
135
+ }
136
+
137
+ // Determine tasks directory location
138
+ // When running from vibe-forge repo: tasks are at ./tasks/
139
+ // When running from a project using vibe-forge as submodule: tasks are at ./_vibe-forge/tasks/
140
+ let tasksDir = path.join(forgeRoot, 'tasks');
141
+ if (!fs.existsSync(tasksDir)) {
142
+ // Fall back to submodule location
143
+ tasksDir = path.join(forgeRoot, '_vibe-forge', 'tasks');
144
+ }
145
+
146
+ // Check for pending tasks
147
+ let pendingCount = 0;
148
+ let needsChangesCount = 0;
149
+
150
+ const pendingDir = path.join(tasksDir, 'pending');
151
+ const needsChangesDir = path.join(tasksDir, 'needs-changes');
152
+
153
+ if (workerAgent === 'any') {
154
+ // Config mode: count all tasks
155
+ pendingCount = countFiles(pendingDir);
156
+ needsChangesCount = countFiles(needsChangesDir);
157
+ } else {
158
+ // Runtime mode: count only tasks assigned to specific worker
159
+ const searchPattern = `assigned_to: ${workerAgent}`;
160
+ pendingCount = countFilesWithContent(pendingDir, searchPattern);
161
+ needsChangesCount = countFilesWithContent(needsChangesDir, searchPattern);
162
+ }
163
+
164
+ const totalTasks = pendingCount + needsChangesCount;
165
+
166
+ // Get relative task path for prompts (relative to forgeRoot)
167
+ const relativeTasksPath = path.relative(forgeRoot, tasksDir);
168
+
169
+ if (totalTasks > 0) {
170
+ // Tasks found! Reset idle counter and continue
171
+ if (fs.existsSync(stateFile)) {
172
+ try {
173
+ const state = safeJsonParse(stateFile);
174
+ state.idle_count = 0;
175
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
176
+ } catch (e) {}
177
+ }
178
+
179
+ return output({
180
+ decision: 'block',
181
+ message: `[Forge Loop] Found ${totalTasks} pending task(s). Continuing work...`,
182
+ prompt: `Check ${relativeTasksPath}/pending/ and ${relativeTasksPath}/needs-changes/ for tasks assigned to you and begin working on them immediately.`
183
+ });
184
+ }
185
+
186
+ // No tasks - handle idle state
187
+ if (fs.existsSync(stateFile)) {
188
+ idleCount++;
189
+
190
+ try {
191
+ const state = safeJsonParse(stateFile);
192
+ state.idle_count = idleCount;
193
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
194
+ } catch (e) {}
195
+
196
+ if (idleCount >= maxIdleChecks) {
197
+ // Max idle checks reached, clean up and allow exit
198
+ try { fs.unlinkSync(stateFile); } catch (e) {}
199
+ return output({
200
+ decision: 'approve',
201
+ message: `[Forge Loop] No tasks found after ${maxIdleChecks} checks. Exiting.`
202
+ });
203
+ }
204
+
205
+ // Still within idle limit - wait and check again
206
+ return output({
207
+ decision: 'block',
208
+ message: `[Forge Loop] No tasks available. Idle check ${idleCount}/${maxIdleChecks}. Waiting...`,
209
+ prompt: `No tasks currently assigned to you. Wait briefly, then check ${relativeTasksPath}/pending/ and ${relativeTasksPath}/needs-changes/ again for new work. If still no tasks, announce you are idle and ready.`
210
+ });
211
+ }
212
+
213
+ // Config-based mode - no idle tracking, just allow exit when no tasks
214
+ return output({
215
+ decision: 'approve',
216
+ message: '[Forge Loop] No pending tasks. Worker exiting.'
217
+ });
218
+ }
219
+
220
+ main();
@@ -1,89 +1,89 @@
1
- {
2
- "_comment": "Project-level permissions for Vibe Forge agents. These allow agents to work without --dangerously-skip-permissions while still gating destructive operations.",
3
- "permissions": {
4
- "allow": [
5
- "Read",
6
- "Edit",
7
- "Write",
8
- "Glob",
9
- "Grep",
10
- "Bash(ls:*)",
11
- "Bash(cat:*)",
12
- "Bash(head:*)",
13
- "Bash(tail:*)",
14
- "Bash(wc:*)",
15
- "Bash(mkdir:*)",
16
- "Bash(cp:*)",
17
- "Bash(mv:*)",
18
- "Bash(node:*)",
19
- "Bash(npx:*)",
20
- "Bash(npm test*)",
21
- "Bash(npm run*)",
22
- "Bash(npm ci*)",
23
- "Bash(npm install*)",
24
- "Bash(npm audit*)",
25
- "Bash(npm view*)",
26
- "Bash(npm version*)",
27
- "Bash(git status*)",
28
- "Bash(git diff*)",
29
- "Bash(git log*)",
30
- "Bash(git branch*)",
31
- "Bash(git checkout*)",
32
- "Bash(git switch*)",
33
- "Bash(git add*)",
34
- "Bash(git commit*)",
35
- "Bash(git pull*)",
36
- "Bash(git push*)",
37
- "Bash(git rm*)",
38
- "Bash(git stash*)",
39
- "Bash(git show*)",
40
- "Bash(git merge*)",
41
- "Bash(git rebase*)",
42
- "Bash(gh pr*)",
43
- "Bash(gh run*)",
44
- "Bash(gh repo*)",
45
- "Bash(gh workflow*)",
46
- "Bash(gh secret list*)",
47
- "Bash(sqlite3:*)",
48
- "Bash(find:*)",
49
- "Bash(grep:*)",
50
- "Bash(sed:*)",
51
- "Bash(awk:*)",
52
- "Bash(sort:*)",
53
- "Bash(echo:*)",
54
- "Bash(printf:*)",
55
- "Bash(date:*)",
56
- "Bash(basename:*)",
57
- "Bash(dirname:*)",
58
- "Bash(realpath:*)",
59
- "Bash(which:*)",
60
- "Bash(command:*)",
61
- "Bash(sleep:*)",
62
- "Bash(cd:*)"
63
- ]
64
- },
65
- "hooks": {
66
- "PreToolUse": [
67
- {
68
- "matcher": "Bash",
69
- "hooks": [
70
- {
71
- "type": "command",
72
- "command": "node bin/lib/heimdall.js"
73
- }
74
- ]
75
- }
76
- ],
77
- "Stop": [
78
- {
79
- "matcher": "",
80
- "hooks": [
81
- {
82
- "type": "command",
83
- "command": "node .claude/hooks/worker-loop.js"
84
- }
85
- ]
86
- }
87
- ]
88
- }
89
- }
1
+ {
2
+ "_comment": "Project-level permissions for Vibe Forge agents. These allow agents to work without --dangerously-skip-permissions while still gating destructive operations.",
3
+ "permissions": {
4
+ "allow": [
5
+ "Read",
6
+ "Edit",
7
+ "Write",
8
+ "Glob",
9
+ "Grep",
10
+ "Bash(ls:*)",
11
+ "Bash(cat:*)",
12
+ "Bash(head:*)",
13
+ "Bash(tail:*)",
14
+ "Bash(wc:*)",
15
+ "Bash(mkdir:*)",
16
+ "Bash(cp:*)",
17
+ "Bash(mv:*)",
18
+ "Bash(node:*)",
19
+ "Bash(npx:*)",
20
+ "Bash(npm test*)",
21
+ "Bash(npm run*)",
22
+ "Bash(npm ci*)",
23
+ "Bash(npm install*)",
24
+ "Bash(npm audit*)",
25
+ "Bash(npm view*)",
26
+ "Bash(npm version*)",
27
+ "Bash(git status*)",
28
+ "Bash(git diff*)",
29
+ "Bash(git log*)",
30
+ "Bash(git branch*)",
31
+ "Bash(git checkout*)",
32
+ "Bash(git switch*)",
33
+ "Bash(git add*)",
34
+ "Bash(git commit*)",
35
+ "Bash(git pull*)",
36
+ "Bash(git push*)",
37
+ "Bash(git rm*)",
38
+ "Bash(git stash*)",
39
+ "Bash(git show*)",
40
+ "Bash(git merge*)",
41
+ "Bash(git rebase*)",
42
+ "Bash(gh pr*)",
43
+ "Bash(gh run*)",
44
+ "Bash(gh repo*)",
45
+ "Bash(gh workflow*)",
46
+ "Bash(gh secret list*)",
47
+ "Bash(sqlite3:*)",
48
+ "Bash(find:*)",
49
+ "Bash(grep:*)",
50
+ "Bash(sed:*)",
51
+ "Bash(awk:*)",
52
+ "Bash(sort:*)",
53
+ "Bash(echo:*)",
54
+ "Bash(printf:*)",
55
+ "Bash(date:*)",
56
+ "Bash(basename:*)",
57
+ "Bash(dirname:*)",
58
+ "Bash(realpath:*)",
59
+ "Bash(which:*)",
60
+ "Bash(command:*)",
61
+ "Bash(sleep:*)",
62
+ "Bash(cd:*)"
63
+ ]
64
+ },
65
+ "hooks": {
66
+ "PreToolUse": [
67
+ {
68
+ "matcher": "Bash",
69
+ "hooks": [
70
+ {
71
+ "type": "command",
72
+ "command": "node src/lib/heimdall.js"
73
+ }
74
+ ]
75
+ }
76
+ ],
77
+ "Stop": [
78
+ {
79
+ "matcher": "",
80
+ "hooks": [
81
+ {
82
+ "type": "command",
83
+ "command": "node .claude/hooks/worker-loop.js"
84
+ }
85
+ ]
86
+ }
87
+ ]
88
+ }
89
+ }