vibe-forge 0.8.1 → 0.8.3

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,265 +1,265 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Heimdall -- Forge worker pre-tool hook interceptor (PLAT-1)
4
- *
5
- * Guards the Bifrost: intercepts every tool call before execution,
6
- * checks it against per-task policy, and blocks or allows.
7
- *
8
- * Registered as a PreToolUse hook via .claude/settings.local.json
9
- * written by the forge daemon at worker startup.
10
- *
11
- * Exit codes:
12
- * 0 -- allow (optionally with audit log entry)
13
- * 2 -- block (explanation written to stdout, fed back to model)
14
- *
15
- * Context file (.context.json in process.cwd()) is written by the forge
16
- * daemon alongside each inbox task. If absent, Heimdall exits 0 immediately
17
- * -- forge-native tasks are not restricted.
18
- */
19
-
20
- 'use strict'
21
-
22
- const fs = require('fs')
23
- const path = require('path')
24
-
25
- // ── Input ─────────────────────────────────────────────────────────────────────
26
-
27
- let input
28
- try {
29
- input = JSON.parse(fs.readFileSync(0, 'utf8'))
30
- } catch (e) {
31
- // Malformed input -- allow and exit rather than blocking the worker
32
- process.exit(0)
33
- }
34
-
35
- const { tool_name, tool_input } = input
36
-
37
- // ── Context file ──────────────────────────────────────────────────────────────
38
-
39
- const contextPath = path.join(process.cwd(), '.context.json')
40
-
41
- // No context file = forge-native task. Heimdall does not restrict forge-native work.
42
- if (!fs.existsSync(contextPath)) {
43
- process.exit(0)
44
- }
45
-
46
- let ctx
47
- try {
48
- ctx = JSON.parse(fs.readFileSync(contextPath, 'utf8'))
49
- } catch (e) {
50
- // Unreadable context -- fail open to avoid blocking forge-native work
51
- process.exit(0)
52
- }
53
-
54
- const {
55
- story_id,
56
- agent,
57
- worktree_path,
58
- has_db_migration,
59
- allowed_paths,
60
- escalation_dir,
61
- audit_log: ctxAuditLog,
62
- } = ctx
63
-
64
- // ── Logging ───────────────────────────────────────────────────────────────────
65
-
66
- const AUDIT_LOG = ctxAuditLog
67
- || path.join(process.cwd(), '_vibe-chain-output', 'heimdall-audit.log')
68
-
69
- const MAX_VIOLATIONS = parseInt(process.env.HEIMDALL_MAX_VIOLATIONS || '3', 10)
70
-
71
- // Violation tracking lives in the worktree root (process.cwd())
72
- const VIOLATION_FILE = path.join(process.cwd(), `.${story_id}.violations`)
73
-
74
- // Escalation signal written to the inbox agent dir so the daemon detects it
75
- const ESCALATION_DIR = escalation_dir || process.cwd()
76
-
77
- function timestamp() {
78
- return new Date().toISOString()
79
- }
80
-
81
- function audit(level, message) {
82
- const line = `[${timestamp()}] HEIMDALL ${level} ${agent}/${story_id}: ${message}\n`
83
- try {
84
- const dir = path.dirname(AUDIT_LOG)
85
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
86
- fs.appendFileSync(AUDIT_LOG, line)
87
- } catch (_) {
88
- // Audit log write failure must never block or crash the hook
89
- }
90
- }
91
-
92
- function block(reason) {
93
- audit('BLOCKED', reason)
94
-
95
- // Track violations
96
- let violations = 0
97
- try {
98
- if (fs.existsSync(VIOLATION_FILE)) {
99
- violations = parseInt(fs.readFileSync(VIOLATION_FILE, 'utf8').trim(), 10) || 0
100
- }
101
- } catch (_) {}
102
-
103
- violations++
104
-
105
- try {
106
- fs.writeFileSync(VIOLATION_FILE, String(violations))
107
- } catch (_) {}
108
-
109
- if (violations >= MAX_VIOLATIONS) {
110
- // Sound the Gjallarhorn
111
- const escalationFile = path.join(ESCALATION_DIR, `${story_id}.escalation`)
112
- try {
113
- fs.writeFileSync(escalationFile, JSON.stringify({
114
- story_id,
115
- agent,
116
- violations,
117
- last_reason: reason,
118
- timestamp: timestamp(),
119
- }, null, 2))
120
- } catch (_) {}
121
-
122
- audit('SOUNDED', `${violations} violations -- escalating to human review`)
123
-
124
- console.log(
125
- `[HEIMDALL] Action blocked: ${reason}\n` +
126
- `[HEIMDALL] GJALLARHORN SOUNDED: ${violations} violations recorded.\n` +
127
- `[HEIMDALL] Story ${story_id} has been escalated to human review. Stop working on this story.`
128
- )
129
- } else {
130
- console.log(
131
- `[HEIMDALL] Action blocked: ${reason}\n` +
132
- `[HEIMDALL] Violation ${violations}/${MAX_VIOLATIONS}. Self-correct and continue.`
133
- )
134
- }
135
-
136
- process.exit(2)
137
- }
138
-
139
- function allow(note) {
140
- if (note) audit('ALLOWED', note)
141
- process.exit(0)
142
- }
143
-
144
- // ── Path helpers ──────────────────────────────────────────────────────────────
145
-
146
- function isOutsideAllowedPaths(targetPath) {
147
- if (!targetPath) return false
148
- try {
149
- const resolved = path.resolve(targetPath)
150
- return !allowed_paths.some(p => resolved.startsWith(path.resolve(p)))
151
- } catch (_) {
152
- return true // Unresolvable path -- treat as outside
153
- }
154
- }
155
-
156
- // ── Bash tool checks ──────────────────────────────────────────────────────────
157
-
158
- if (tool_name === 'Bash') {
159
- const cmd = (tool_input && tool_input.command || '').trim()
160
-
161
- // Destructive rm -- check if target escapes worktree
162
- if (/rm\s+-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r/.test(cmd) && /\brm\b/.test(cmd)) {
163
- // Extract everything after the flags as the target
164
- const rmMatch = cmd.match(/rm\s+(?:-\S+\s+)(.+)/)
165
- const rmTarget = rmMatch ? rmMatch[1].trim().replace(/^['"]|['"]$/g, '') : ''
166
-
167
- const isRootTarget = /^(\/|~)/.test(rmTarget) || rmTarget === '' || rmTarget.includes('..')
168
- const isOutside = !rmTarget || isRootTarget || isOutsideAllowedPaths(rmTarget)
169
-
170
- if (isOutside) {
171
- block(`destructive rm outside worktree: ${cmd}`)
172
- }
173
- }
174
-
175
- // Credential access
176
- if (/\.(env|pem|key|cert)\b/.test(cmd) && !/\.env\.(example|sample|template)/.test(cmd)) {
177
- block(`credential file access: ${cmd}`)
178
- }
179
- if (/~\/\.(ssh|aws|gnupg)\b/.test(cmd)) {
180
- block(`credential directory access: ${cmd}`)
181
- }
182
- // Echoing or exporting secrets (but not reading them as part of build)
183
- if (/(echo|printf|export)\s+.*\b(TOKEN|SECRET|PASSWORD|API_KEY)\s*=/.test(cmd)) {
184
- block(`credential assignment in shell: ${cmd}`)
185
- }
186
-
187
- // Dangerous git operations
188
- if (/git\s+push\b.*--force(-with-lease)?/.test(cmd)) {
189
- block(`force push blocked: ${cmd}`)
190
- }
191
- if (/git\s+reset\s+--hard\s+HEAD~[2-9]/.test(cmd)) {
192
- block(`multi-commit hard reset blocked: ${cmd}`)
193
- }
194
- if (/git\s+clean\s+-[a-zA-Z]*f[a-zA-Z]*d|-[a-zA-Z]*d[a-zA-Z]*f/.test(cmd) && /\bgit\b/.test(cmd)) {
195
- block(`git clean -fd blocked: ${cmd}`)
196
- }
197
- // Push to a non-origin remote (e.g. git push upstream)
198
- if (/git\s+push\s+(?!origin\b)(\S+)/.test(cmd)) {
199
- const remoteMatch = cmd.match(/git\s+push\s+(\S+)/)
200
- if (remoteMatch && remoteMatch[1] !== 'origin' && !remoteMatch[1].startsWith('-')) {
201
- block(`push to non-origin remote blocked: ${cmd}`)
202
- }
203
- }
204
- // Direct push to main/master — all changes must go through PRs
205
- if (/git\s+push\b/.test(cmd) && /\b(main|master)\b/.test(cmd)) {
206
- block(`direct push to main/master blocked — create a feature branch and open a PR`)
207
- }
208
-
209
- // DB destructive operations without authorization
210
- if (/(DROP\s+TABLE|TRUNCATE\s+TABLE)/i.test(cmd) && !has_db_migration) {
211
- block(`DROP/TRUNCATE requires has_db_migration: true on the story`)
212
- }
213
- if (/DELETE\s+FROM\s+\S+\s*;?\s*$/i.test(cmd) && !/WHERE\s+/i.test(cmd)) {
214
- block(`DELETE without WHERE clause blocked: ${cmd}`)
215
- }
216
-
217
- // High-risk but legitimate -- audit log only
218
- if (/\b(npm|yarn|pnpm)\s+(install|add|ci)\b/.test(cmd)) {
219
- allow(`dependency install: ${cmd}`)
220
- }
221
- if (/\b(pip|pip3)\s+install\b/.test(cmd)) {
222
- allow(`pip install: ${cmd}`)
223
- }
224
- if (/\bcargo\s+(build|install)\b/.test(cmd)) {
225
- allow(`cargo build: ${cmd}`)
226
- }
227
- if (/\b(curl|wget)\b/.test(cmd)) {
228
- allow(`network request: ${cmd}`)
229
- }
230
- if (/git\s+commit\s+--amend/.test(cmd)) {
231
- allow(`git commit --amend`)
232
- }
233
- }
234
-
235
- // ── Write / Edit tool checks ──────────────────────────────────────────────────
236
-
237
- if (tool_name === 'Write' || tool_name === 'Edit') {
238
- const filePath = tool_input && (tool_input.file_path || tool_input.path) || ''
239
-
240
- if (filePath) {
241
- // Credential files
242
- if (/\.(env|pem|key|cert)$/.test(filePath) && !/\.(example|sample|template)$/.test(filePath)) {
243
- block(`write to credential file: ${filePath}`)
244
- }
245
-
246
- // Settings that could override Heimdall
247
- if (/\.claude[/\\]settings(\.local)?\.json$/.test(filePath)) {
248
- block(`write to .claude/settings blocked -- Heimdall config is daemon-managed`)
249
- }
250
-
251
- // Path escape check
252
- if (isOutsideAllowedPaths(filePath)) {
253
- block(`path escape: ${filePath} is outside allowed paths`)
254
- }
255
-
256
- // Pipeline output writes -- allowed but logged
257
- if (filePath.includes('_vibe-chain-output')) {
258
- allow(`pipeline output write: ${filePath}`)
259
- }
260
- }
261
- }
262
-
263
- // ── Default: allow ────────────────────────────────────────────────────────────
264
-
265
- allow(null)
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Heimdall -- Forge worker pre-tool hook interceptor (PLAT-1)
4
+ *
5
+ * Guards the Bifrost: intercepts every tool call before execution,
6
+ * checks it against per-task policy, and blocks or allows.
7
+ *
8
+ * Registered as a PreToolUse hook via .claude/settings.local.json
9
+ * written by the forge daemon at worker startup.
10
+ *
11
+ * Exit codes:
12
+ * 0 -- allow (optionally with audit log entry)
13
+ * 2 -- block (explanation written to stdout, fed back to model)
14
+ *
15
+ * Context file (.context.json in process.cwd()) is written by the forge
16
+ * daemon alongside each inbox task. If absent, Heimdall exits 0 immediately
17
+ * -- forge-native tasks are not restricted.
18
+ */
19
+
20
+ 'use strict'
21
+
22
+ const fs = require('fs')
23
+ const path = require('path')
24
+
25
+ // ── Input ─────────────────────────────────────────────────────────────────────
26
+
27
+ let input
28
+ try {
29
+ input = JSON.parse(fs.readFileSync(0, 'utf8'))
30
+ } catch (e) {
31
+ // Malformed input -- allow and exit rather than blocking the worker
32
+ process.exit(0)
33
+ }
34
+
35
+ const { tool_name, tool_input } = input
36
+
37
+ // ── Context file ──────────────────────────────────────────────────────────────
38
+
39
+ const contextPath = path.join(process.cwd(), '.context.json')
40
+
41
+ // No context file = forge-native task. Heimdall does not restrict forge-native work.
42
+ if (!fs.existsSync(contextPath)) {
43
+ process.exit(0)
44
+ }
45
+
46
+ let ctx
47
+ try {
48
+ ctx = JSON.parse(fs.readFileSync(contextPath, 'utf8'))
49
+ } catch (e) {
50
+ // Unreadable context -- fail open to avoid blocking forge-native work
51
+ process.exit(0)
52
+ }
53
+
54
+ const {
55
+ story_id,
56
+ agent,
57
+ worktree_path,
58
+ has_db_migration,
59
+ allowed_paths,
60
+ escalation_dir,
61
+ audit_log: ctxAuditLog,
62
+ } = ctx
63
+
64
+ // ── Logging ───────────────────────────────────────────────────────────────────
65
+
66
+ const AUDIT_LOG = ctxAuditLog
67
+ || path.join(process.cwd(), '_vibe-chain-output', 'heimdall-audit.log')
68
+
69
+ const MAX_VIOLATIONS = parseInt(process.env.HEIMDALL_MAX_VIOLATIONS || '3', 10)
70
+
71
+ // Violation tracking lives in the worktree root (process.cwd())
72
+ const VIOLATION_FILE = path.join(process.cwd(), `.${story_id}.violations`)
73
+
74
+ // Escalation signal written to the inbox agent dir so the daemon detects it
75
+ const ESCALATION_DIR = escalation_dir || process.cwd()
76
+
77
+ function timestamp() {
78
+ return new Date().toISOString()
79
+ }
80
+
81
+ function audit(level, message) {
82
+ const line = `[${timestamp()}] HEIMDALL ${level} ${agent}/${story_id}: ${message}\n`
83
+ try {
84
+ const dir = path.dirname(AUDIT_LOG)
85
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
86
+ fs.appendFileSync(AUDIT_LOG, line)
87
+ } catch (_) {
88
+ // Audit log write failure must never block or crash the hook
89
+ }
90
+ }
91
+
92
+ function block(reason) {
93
+ audit('BLOCKED', reason)
94
+
95
+ // Track violations
96
+ let violations = 0
97
+ try {
98
+ if (fs.existsSync(VIOLATION_FILE)) {
99
+ violations = parseInt(fs.readFileSync(VIOLATION_FILE, 'utf8').trim(), 10) || 0
100
+ }
101
+ } catch (_) {}
102
+
103
+ violations++
104
+
105
+ try {
106
+ fs.writeFileSync(VIOLATION_FILE, String(violations))
107
+ } catch (_) {}
108
+
109
+ if (violations >= MAX_VIOLATIONS) {
110
+ // Sound the Gjallarhorn
111
+ const escalationFile = path.join(ESCALATION_DIR, `${story_id}.escalation`)
112
+ try {
113
+ fs.writeFileSync(escalationFile, JSON.stringify({
114
+ story_id,
115
+ agent,
116
+ violations,
117
+ last_reason: reason,
118
+ timestamp: timestamp(),
119
+ }, null, 2))
120
+ } catch (_) {}
121
+
122
+ audit('SOUNDED', `${violations} violations -- escalating to human review`)
123
+
124
+ console.log(
125
+ `[HEIMDALL] Action blocked: ${reason}\n` +
126
+ `[HEIMDALL] GJALLARHORN SOUNDED: ${violations} violations recorded.\n` +
127
+ `[HEIMDALL] Story ${story_id} has been escalated to human review. Stop working on this story.`
128
+ )
129
+ } else {
130
+ console.log(
131
+ `[HEIMDALL] Action blocked: ${reason}\n` +
132
+ `[HEIMDALL] Violation ${violations}/${MAX_VIOLATIONS}. Self-correct and continue.`
133
+ )
134
+ }
135
+
136
+ process.exit(2)
137
+ }
138
+
139
+ function allow(note) {
140
+ if (note) audit('ALLOWED', note)
141
+ process.exit(0)
142
+ }
143
+
144
+ // ── Path helpers ──────────────────────────────────────────────────────────────
145
+
146
+ function isOutsideAllowedPaths(targetPath) {
147
+ if (!targetPath) return false
148
+ try {
149
+ const resolved = path.resolve(targetPath)
150
+ return !allowed_paths.some(p => resolved.startsWith(path.resolve(p)))
151
+ } catch (_) {
152
+ return true // Unresolvable path -- treat as outside
153
+ }
154
+ }
155
+
156
+ // ── Bash tool checks ──────────────────────────────────────────────────────────
157
+
158
+ if (tool_name === 'Bash') {
159
+ const cmd = (tool_input && tool_input.command || '').trim()
160
+
161
+ // Destructive rm -- check if target escapes worktree
162
+ if (/rm\s+-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r/.test(cmd) && /\brm\b/.test(cmd)) {
163
+ // Extract everything after the flags as the target
164
+ const rmMatch = cmd.match(/rm\s+(?:-\S+\s+)(.+)/)
165
+ const rmTarget = rmMatch ? rmMatch[1].trim().replace(/^['"]|['"]$/g, '') : ''
166
+
167
+ const isRootTarget = /^(\/|~)/.test(rmTarget) || rmTarget === '' || rmTarget.includes('..')
168
+ const isOutside = !rmTarget || isRootTarget || isOutsideAllowedPaths(rmTarget)
169
+
170
+ if (isOutside) {
171
+ block(`destructive rm outside worktree: ${cmd}`)
172
+ }
173
+ }
174
+
175
+ // Credential access
176
+ if (/\.(env|pem|key|cert)\b/.test(cmd) && !/\.env\.(example|sample|template)/.test(cmd)) {
177
+ block(`credential file access: ${cmd}`)
178
+ }
179
+ if (/~\/\.(ssh|aws|gnupg)\b/.test(cmd)) {
180
+ block(`credential directory access: ${cmd}`)
181
+ }
182
+ // Echoing or exporting secrets (but not reading them as part of build)
183
+ if (/(echo|printf|export)\s+.*\b(TOKEN|SECRET|PASSWORD|API_KEY)\s*=/.test(cmd)) {
184
+ block(`credential assignment in shell: ${cmd}`)
185
+ }
186
+
187
+ // Dangerous git operations
188
+ if (/git\s+push\b.*--force(-with-lease)?/.test(cmd)) {
189
+ block(`force push blocked: ${cmd}`)
190
+ }
191
+ if (/git\s+reset\s+--hard\s+HEAD~[2-9]/.test(cmd)) {
192
+ block(`multi-commit hard reset blocked: ${cmd}`)
193
+ }
194
+ if (/git\s+clean\s+-[a-zA-Z]*f[a-zA-Z]*d|-[a-zA-Z]*d[a-zA-Z]*f/.test(cmd) && /\bgit\b/.test(cmd)) {
195
+ block(`git clean -fd blocked: ${cmd}`)
196
+ }
197
+ // Push to a non-origin remote (e.g. git push upstream)
198
+ if (/git\s+push\s+(?!origin\b)(\S+)/.test(cmd)) {
199
+ const remoteMatch = cmd.match(/git\s+push\s+(\S+)/)
200
+ if (remoteMatch && remoteMatch[1] !== 'origin' && !remoteMatch[1].startsWith('-')) {
201
+ block(`push to non-origin remote blocked: ${cmd}`)
202
+ }
203
+ }
204
+ // Direct push to main/master — all changes must go through PRs
205
+ if (/git\s+push\b/.test(cmd) && /\b(main|master)\b/.test(cmd)) {
206
+ block(`direct push to main/master blocked — create a feature branch and open a PR`)
207
+ }
208
+
209
+ // DB destructive operations without authorization
210
+ if (/(DROP\s+TABLE|TRUNCATE\s+TABLE)/i.test(cmd) && !has_db_migration) {
211
+ block(`DROP/TRUNCATE requires has_db_migration: true on the story`)
212
+ }
213
+ if (/DELETE\s+FROM\s+\S+\s*;?\s*$/i.test(cmd) && !/WHERE\s+/i.test(cmd)) {
214
+ block(`DELETE without WHERE clause blocked: ${cmd}`)
215
+ }
216
+
217
+ // High-risk but legitimate -- audit log only
218
+ if (/\b(npm|yarn|pnpm)\s+(install|add|ci)\b/.test(cmd)) {
219
+ allow(`dependency install: ${cmd}`)
220
+ }
221
+ if (/\b(pip|pip3)\s+install\b/.test(cmd)) {
222
+ allow(`pip install: ${cmd}`)
223
+ }
224
+ if (/\bcargo\s+(build|install)\b/.test(cmd)) {
225
+ allow(`cargo build: ${cmd}`)
226
+ }
227
+ if (/\b(curl|wget)\b/.test(cmd)) {
228
+ allow(`network request: ${cmd}`)
229
+ }
230
+ if (/git\s+commit\s+--amend/.test(cmd)) {
231
+ allow(`git commit --amend`)
232
+ }
233
+ }
234
+
235
+ // ── Write / Edit tool checks ──────────────────────────────────────────────────
236
+
237
+ if (tool_name === 'Write' || tool_name === 'Edit') {
238
+ const filePath = tool_input && (tool_input.file_path || tool_input.path) || ''
239
+
240
+ if (filePath) {
241
+ // Credential files
242
+ if (/\.(env|pem|key|cert)$/.test(filePath) && !/\.(example|sample|template)$/.test(filePath)) {
243
+ block(`write to credential file: ${filePath}`)
244
+ }
245
+
246
+ // Settings that could override Heimdall
247
+ if (/\.claude[/\\]settings(\.local)?\.json$/.test(filePath)) {
248
+ block(`write to .claude/settings blocked -- Heimdall config is daemon-managed`)
249
+ }
250
+
251
+ // Path escape check
252
+ if (isOutsideAllowedPaths(filePath)) {
253
+ block(`path escape: ${filePath} is outside allowed paths`)
254
+ }
255
+
256
+ // Pipeline output writes -- allowed but logged
257
+ if (filePath.includes('_vibe-chain-output')) {
258
+ allow(`pipeline output write: ${filePath}`)
259
+ }
260
+ }
261
+ }
262
+
263
+ // ── Default: allow ────────────────────────────────────────────────────────────
264
+
265
+ allow(null)
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # src/lib/index.sh
4
+ #
5
+ # Central entry point for sourcing common libraries.
6
+ # Scripts source this instead of individually sourcing each lib.
7
+ #
8
+ # Usage: source "$LIB_DIR/index.sh"
9
+
10
+ # Prevent double-sourcing
11
+ [[ -n "${_LIB_INDEX_LOADED:-}" ]] && return 0
12
+ _LIB_INDEX_LOADED=1
13
+
14
+ _LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+
16
+ # Core libraries (order matters: colors first, then constants, then the rest)
17
+ source "$_LIB_DIR/colors.sh"
18
+ source "$_LIB_DIR/constants.sh"
19
+ source "$_LIB_DIR/json.sh"
20
+ source "$_LIB_DIR/util.sh"
21
+
22
+ # Optional libraries (sourced on demand by scripts that need them)
23
+ # source "$_LIB_DIR/config.sh" # Agent config loading
24
+ # source "$_LIB_DIR/agents.sh" # Agent resolution
25
+ # source "$_LIB_DIR/database.sh" # SQLite operations (daemon only)