@sugar-crash-studios/vibe-forge 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 (201) hide show
  1. package/.claude/commands/clear-attention.md +63 -0
  2. package/.claude/commands/compact-context.md +52 -0
  3. package/.claude/commands/configure-vcs.md +102 -0
  4. package/.claude/commands/forge.md +171 -0
  5. package/.claude/commands/need-help.md +77 -0
  6. package/.claude/commands/update-status.md +64 -0
  7. package/.claude/commands/worker-loop.md +106 -0
  8. package/.claude/hooks/worker-loop.js +198 -0
  9. package/.claude/scripts/setup-worker-loop.sh +45 -0
  10. package/.claude/settings.local.json +46 -0
  11. package/LICENSE +21 -0
  12. package/README.md +238 -0
  13. package/agents/aegis/personality.md +294 -0
  14. package/agents/anvil/personality.md +276 -0
  15. package/agents/architect/personality.md +258 -0
  16. package/agents/crucible/personality.md +360 -0
  17. package/agents/ember/personality.md +291 -0
  18. package/agents/forge-master/capabilities.md +144 -0
  19. package/agents/forge-master/context-template.md +128 -0
  20. package/agents/forge-master/personality.md +138 -0
  21. package/agents/furnace/personality.md +340 -0
  22. package/agents/herald/personality.md +247 -0
  23. package/agents/loki/personality.md +108 -0
  24. package/agents/oracle/personality.md +283 -0
  25. package/agents/pixel/personality.md +113 -0
  26. package/agents/planning-hub/personality.md +320 -0
  27. package/agents/scribe/personality.md +251 -0
  28. package/agents/temper/personality.md +218 -0
  29. package/bin/cli.js +375 -0
  30. package/bin/dashboard/api/agents.js +333 -0
  31. package/bin/dashboard/api/dispatch.js +483 -0
  32. package/bin/dashboard/api/tasks.js +416 -0
  33. package/bin/dashboard/frontend/index.html +13 -0
  34. package/bin/dashboard/frontend/package.json +16 -0
  35. package/bin/dashboard/frontend/src/App.svelte +222 -0
  36. package/bin/dashboard/frontend/src/app.css +1777 -0
  37. package/bin/dashboard/frontend/src/lib/components/AgentCard.svelte +60 -0
  38. package/bin/dashboard/frontend/src/lib/components/AgentsPanel.svelte +57 -0
  39. package/bin/dashboard/frontend/src/lib/components/DispatchModal.svelte +180 -0
  40. package/bin/dashboard/frontend/src/lib/components/Footer.svelte +33 -0
  41. package/bin/dashboard/frontend/src/lib/components/Header.svelte +84 -0
  42. package/bin/dashboard/frontend/src/lib/components/IssueCard.svelte +33 -0
  43. package/bin/dashboard/frontend/src/lib/components/IssuesPanel.svelte +73 -0
  44. package/bin/dashboard/frontend/src/lib/components/KeyboardShortcutsModal.svelte +108 -0
  45. package/bin/dashboard/frontend/src/lib/components/MobileTabs.svelte +52 -0
  46. package/bin/dashboard/frontend/src/lib/components/NotificationCard.svelte +60 -0
  47. package/bin/dashboard/frontend/src/lib/components/NotificationsPanel.svelte +44 -0
  48. package/bin/dashboard/frontend/src/lib/components/TaskCard.svelte +63 -0
  49. package/bin/dashboard/frontend/src/lib/components/TasksPanel.svelte +82 -0
  50. package/bin/dashboard/frontend/src/lib/components/Toast.svelte +45 -0
  51. package/bin/dashboard/frontend/src/lib/stores/agents.js +34 -0
  52. package/bin/dashboard/frontend/src/lib/stores/issues.js +54 -0
  53. package/bin/dashboard/frontend/src/lib/stores/notifications.js +48 -0
  54. package/bin/dashboard/frontend/src/lib/stores/tasks.js +63 -0
  55. package/bin/dashboard/frontend/src/lib/stores/theme.js +33 -0
  56. package/bin/dashboard/frontend/src/lib/stores/toast.js +35 -0
  57. package/bin/dashboard/frontend/src/lib/stores/ui.js +25 -0
  58. package/bin/dashboard/frontend/src/lib/stores/voice.js +275 -0
  59. package/bin/dashboard/frontend/src/lib/stores/websocket.js +295 -0
  60. package/bin/dashboard/frontend/src/lib/utils/api.js +101 -0
  61. package/bin/dashboard/frontend/src/lib/utils/formatters.js +54 -0
  62. package/bin/dashboard/frontend/src/main.js +9 -0
  63. package/bin/dashboard/frontend/svelte.config.js +5 -0
  64. package/bin/dashboard/frontend/vite.config.js +20 -0
  65. package/bin/dashboard/public/assets/index-DnfVj9Ce.css +1 -0
  66. package/bin/dashboard/public/assets/index-Ze5h0kXQ.js +2 -0
  67. package/bin/dashboard/public/index.html +14 -0
  68. package/bin/dashboard/server.js +566 -0
  69. package/bin/forge-daemon.sh +463 -0
  70. package/bin/forge-setup.sh +645 -0
  71. package/bin/forge-spawn.sh +164 -0
  72. package/bin/forge.cmd +83 -0
  73. package/bin/forge.sh +533 -0
  74. package/bin/lib/agents.sh +177 -0
  75. package/bin/lib/colors.sh +44 -0
  76. package/bin/lib/config.sh +347 -0
  77. package/bin/lib/constants.sh +241 -0
  78. package/bin/lib/daemon/display.sh +128 -0
  79. package/bin/lib/daemon/notifications.sh +263 -0
  80. package/bin/lib/daemon/routing.sh +77 -0
  81. package/bin/lib/daemon/state.sh +115 -0
  82. package/bin/lib/daemon/sync.sh +95 -0
  83. package/bin/lib/database.sh +310 -0
  84. package/bin/lib/heimdall-setup.js +113 -0
  85. package/bin/lib/heimdall.js +265 -0
  86. package/bin/lib/json.sh +264 -0
  87. package/bin/lib/terminal.js +451 -0
  88. package/bin/lib/util.sh +126 -0
  89. package/bin/lib/vcs.js +349 -0
  90. package/config/agent-manifest.yaml +203 -0
  91. package/config/agents.json +168 -0
  92. package/config/task-template.md +159 -0
  93. package/config/task-types.yaml +106 -0
  94. package/context/agent-status/aegis.json +7 -0
  95. package/context/agent-status/anvil.json +7 -0
  96. package/context/agent-status/architect.json +7 -0
  97. package/context/agent-status/crucible.json +7 -0
  98. package/context/agent-status/ember.json +7 -0
  99. package/context/agent-status/furnace.json +7 -0
  100. package/context/agent-status/loki.json +7 -0
  101. package/context/agent-status/oracle.json +7 -0
  102. package/context/agent-status/pixel.json +7 -0
  103. package/context/agent-status/planning-hub.json +7 -0
  104. package/context/agent-status/scribe.json +7 -0
  105. package/context/agent-status/temper.json +7 -0
  106. package/context/feature-brainstorm.md +426 -0
  107. package/context/forge-state.yaml +19 -0
  108. package/context/modern-conventions.md +129 -0
  109. package/context/project-context-template.md +122 -0
  110. package/context/project-context.md +122 -0
  111. package/docs/TODO.md +150 -0
  112. package/docs/agents.md +409 -0
  113. package/docs/architecture/decisions/ADR-001-daemon-modularization.md +122 -0
  114. package/docs/architecture/vibe-lab-integration.md +684 -0
  115. package/docs/architecture.md +194 -0
  116. package/docs/bmad-gap-analysis-2026-03-31.md +444 -0
  117. package/docs/cleanup-workflow.md +329 -0
  118. package/docs/commands.md +451 -0
  119. package/docs/dashboard-mockup.html +989 -0
  120. package/docs/getting-started.md +261 -0
  121. package/docs/integration/forge-ownership-policy.md +112 -0
  122. package/docs/npm-publishing.md +132 -0
  123. package/docs/roadmap-2026.md +519 -0
  124. package/docs/security.md +144 -0
  125. package/docs/wireframes/dashboard-mvp.md +1164 -0
  126. package/docs/workflows/README.md +32 -0
  127. package/docs/workflows/azure-devops.md +108 -0
  128. package/docs/workflows/bitbucket.md +104 -0
  129. package/docs/workflows/git-only.md +130 -0
  130. package/docs/workflows/gitea.md +168 -0
  131. package/docs/workflows/github.md +103 -0
  132. package/docs/workflows/gitlab.md +105 -0
  133. package/docs/workflows.md +454 -0
  134. package/package.json +73 -0
  135. package/tasks/completed/ARCH-001-duplicate-agent-config.md +121 -0
  136. package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +88 -0
  137. package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +77 -0
  138. package/tasks/completed/ARCH-009-test-organization.md +78 -0
  139. package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +94 -0
  140. package/tasks/completed/ARCH-012-tmp-files-in-root.md +71 -0
  141. package/tasks/completed/ARCH-013-exit-code-constants.md +65 -0
  142. package/tasks/completed/ARCH-014-sed-incompatibility.md +96 -0
  143. package/tasks/completed/ARCH-015-docs-todo-tracking.md +83 -0
  144. package/tasks/completed/BUG-dash-001-tasks-filter-error.md +31 -0
  145. package/tasks/completed/BUG-dash-002-agents-unknown.md +41 -0
  146. package/tasks/completed/CLEAN-001.md +38 -0
  147. package/tasks/completed/CLEAN-002.md +43 -0
  148. package/tasks/completed/CLEAN-003.md +47 -0
  149. package/tasks/completed/CLEAN-004.md +56 -0
  150. package/tasks/completed/CLEAN-005.md +75 -0
  151. package/tasks/completed/CLEAN-006.md +47 -0
  152. package/tasks/completed/CLEAN-007.md +34 -0
  153. package/tasks/completed/CLEAN-008.md +49 -0
  154. package/tasks/completed/CLEAN-012.md +58 -0
  155. package/tasks/completed/CLEAN-013.md +45 -0
  156. package/tasks/completed/FEATURE-001a-dashboard-wireframes.md +162 -0
  157. package/tasks/completed/IMPL-007a-daemon-notifications-module.md +82 -0
  158. package/tasks/completed/IMPL-007b-daemon-sync-module.md +71 -0
  159. package/tasks/completed/IMPL-007c-daemon-state-module.md +80 -0
  160. package/tasks/completed/IMPL-007d-daemon-routing-module.md +77 -0
  161. package/tasks/completed/IMPL-007e-daemon-display-module.md +77 -0
  162. package/tasks/completed/IMPL-007f-daemon-integration.md +124 -0
  163. package/tasks/completed/PLAT-1-heimdall.md +420 -0
  164. package/tasks/completed/SEC-001-sql-injection-fix.md +58 -0
  165. package/tasks/completed/SEC-002-notification-injection-fix.md +45 -0
  166. package/tasks/completed/SEC-003-eval-injection-fix.md +54 -0
  167. package/tasks/completed/SEC-004-pid-race-condition-fix.md +49 -0
  168. package/tasks/completed/SEC-005-worker-loop-path-fix.md +51 -0
  169. package/tasks/completed/SEC-006-eval-agent-names.md +55 -0
  170. package/tasks/completed/SEC-007-spawn-escaping.md +67 -0
  171. package/tasks/completed/TASK-DASH-001-server-infrastructure.md +185 -0
  172. package/tasks/completed/TASK-anvil-001-dashboard-frontend.md +133 -0
  173. package/tasks/completed/review-bmad-aegis.md +89 -0
  174. package/tasks/completed/review-bmad-anvil.md +80 -0
  175. package/tasks/completed/review-bmad-crucible.md +81 -0
  176. package/tasks/completed/review-bmad-ember.md +90 -0
  177. package/tasks/completed/review-bmad-furnace.md +79 -0
  178. package/tasks/completed/review-bmad-pixel.md +82 -0
  179. package/tasks/completed/review-bmad-scribe.md +92 -0
  180. package/tasks/completed/review-bmad-sentinel.md +83 -0
  181. package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +72 -0
  182. package/tasks/pending/ARCH-005-missing-src-directory.md +95 -0
  183. package/tasks/pending/ARCH-006-task-template-location.md +64 -0
  184. package/tasks/pending/ARCH-008-forge-master-vs-hub.md +81 -0
  185. package/tasks/pending/ARCH-010-missing-index-files.md +84 -0
  186. package/tasks/pending/CLEAN-009.md +31 -0
  187. package/tasks/pending/CLEAN-010.md +30 -0
  188. package/tasks/pending/CLEAN-011.md +30 -0
  189. package/tasks/pending/CLEAN-014.md +32 -0
  190. package/tasks/pending/DESIGN-dash-001-layout-review.md +45 -0
  191. package/tasks/pending/FEATURE-001-dashboard-mvp.md +268 -0
  192. package/tasks/review/ARCH-007-daemon-monolith.md +162 -0
  193. package/tasks/review/bmad-review-aegis.md +349 -0
  194. package/tasks/review/bmad-review-anvil.md +259 -0
  195. package/tasks/review/bmad-review-crucible.md +277 -0
  196. package/tasks/review/bmad-review-ember.md +307 -0
  197. package/tasks/review/bmad-review-furnace.md +285 -0
  198. package/tasks/review/bmad-review-pixel.md +329 -0
  199. package/tasks/review/bmad-review-scribe.md +361 -0
  200. package/tasks/review/bmad-review-sentinel.md +242 -0
  201. package/tasks/review/task-001.md +78 -0
@@ -0,0 +1,420 @@
1
+ ---
2
+ id: PLAT-1
3
+ title: "Heimdall -- forge worker pre-tool hook interceptor"
4
+ type: security
5
+ priority: high
6
+ status: pending
7
+ assigned_to: null
8
+ epic: PLAT
9
+ estimated_complexity: medium
10
+ depends_on: []
11
+ blocked_by: []
12
+ created: 2026-04-01T00:00:00-05:00
13
+ lab_story_id: null
14
+ ---
15
+
16
+ # Context
17
+
18
+ ## Why This Exists
19
+
20
+ Forge workers running in background daemon mode use `--dangerously-skip-permissions`.
21
+ In terminal tab mode, a human can observe and intervene. In unattended background mode
22
+ there is no runtime human gate.
23
+
24
+ Heimdall is the compensating control. It intercepts every tool call before it executes,
25
+ checks it against per-task policy, and blocks or allows with an audit log entry.
26
+
27
+ Named for the Norse watchman of the gods. The Gjallarhorn escalation is the signal that
28
+ something has gone wrong and a human must intervene.
29
+
30
+ ## Architecture Reference
31
+
32
+ `docs/architecture/vibe-lab-integration.md` -- Security Model section
33
+
34
+ ---
35
+
36
+ # Acceptance Criteria
37
+
38
+ 1. Heimdall is registered as a `PreToolUse` hook in `.claude/settings.local.json`
39
+ written by the forge daemon to each worker's working directory at startup
40
+ 2. Every hard-block category listed below returns exit `2` with a clear message
41
+ 3. Every audit-log category is allowed (exit `0`) and appended to the daemon log
42
+ 4. Context file (`.context.json`) is read on every invocation -- policy is
43
+ per-task, not global
44
+ 5. After N violations in a single task (default 3, configurable), Heimdall writes
45
+ `{story-id}.escalation` signal file and logs `HEIMDALL SOUNDED`
46
+ 6. Forge daemon detects `.escalation` file and writes escalation handoff
47
+ 7. All existing forge tests pass -- no regression on forge-native tasks
48
+ 8. Heimdall exits `0` immediately when no context file is present (forge-native
49
+ tasks without a `.context.json` are not restricted by Heimdall)
50
+
51
+ ---
52
+
53
+ # Tasks / Subtasks
54
+
55
+ ## 1. `bin/lib/heimdall.js` -- core interceptor
56
+
57
+ ```javascript
58
+ #!/usr/bin/env node
59
+ // Heimdall -- PreToolUse hook for forge workers running in lab pipeline
60
+ // Reads tool call from stdin, checks policy, exits 0 (allow) or 2 (block)
61
+
62
+ import { readFileSync, existsSync, appendFileSync, writeFileSync } from 'fs'
63
+ import { join, resolve } from 'path'
64
+
65
+ const input = JSON.parse(readFileSync('/dev/stdin', 'utf8'))
66
+ const { tool_name, tool_input } = input
67
+
68
+ // Locate context file -- written by forge daemon alongside the inbox task
69
+ // Convention: .context.json in the working directory
70
+ const contextPath = join(process.cwd(), '.context.json')
71
+
72
+ // No context file = forge-native task, not a lab pipeline task
73
+ // Heimdall does not restrict forge-native work
74
+ if (!existsSync(contextPath)) {
75
+ process.exit(0)
76
+ }
77
+
78
+ const ctx = JSON.parse(readFileSync(contextPath, 'utf8'))
79
+ const { story_id, agent, worktree_path, handoff_dir, has_db_migration, allowed_paths } = ctx
80
+
81
+ const AUDIT_LOG = join(process.env.FORGE_DAEMON_LOG || process.cwd(), 'heimdall-audit.log')
82
+ const VIOLATION_FILE = join(process.cwd(), `${story_id}.violations`)
83
+ const MAX_VIOLATIONS = parseInt(process.env.HEIMDALL_MAX_VIOLATIONS || '3', 10)
84
+
85
+ function timestamp() {
86
+ return new Date().toISOString()
87
+ }
88
+
89
+ function audit(level, message) {
90
+ const line = `[${timestamp()}] HEIMDALL ${level} ${agent}/${story_id}: ${message}\n`
91
+ appendFileSync(AUDIT_LOG, line)
92
+ }
93
+
94
+ function block(reason) {
95
+ audit('BLOCKED', reason)
96
+
97
+ // Track violation count
98
+ let violations = 0
99
+ if (existsSync(VIOLATION_FILE)) {
100
+ violations = parseInt(readFileSync(VIOLATION_FILE, 'utf8').trim(), 10) || 0
101
+ }
102
+ violations++
103
+ writeFileSync(VIOLATION_FILE, String(violations))
104
+
105
+ if (violations >= MAX_VIOLATIONS) {
106
+ const escalationFile = join(process.cwd(), `${story_id}.escalation`)
107
+ writeFileSync(escalationFile, JSON.stringify({
108
+ story_id, agent, violations, reason, timestamp: timestamp()
109
+ }))
110
+ audit('SOUNDED', `${violations} violations, escalating to human review`)
111
+ console.log(
112
+ `[HEIMDALL] Action blocked: ${reason}\n` +
113
+ `[HEIMDALL] GJALLARHORN SOUNDED: ${violations} violations. Story escalated to human review.`
114
+ )
115
+ } else {
116
+ console.log(
117
+ `[HEIMDALL] Action blocked: ${reason}\n` +
118
+ `[HEIMDALL] Violation ${violations}/${MAX_VIOLATIONS}. Self-correct and continue.`
119
+ )
120
+ }
121
+
122
+ process.exit(2)
123
+ }
124
+
125
+ function allow(note) {
126
+ if (note) audit('ALLOWED', note)
127
+ process.exit(0)
128
+ }
129
+
130
+ function isOutsideAllowedPaths(targetPath) {
131
+ const resolved = resolve(targetPath)
132
+ return !allowed_paths.some(p => resolved.startsWith(resolve(p)))
133
+ }
134
+
135
+ // ── BASH tool checks ────────────────────────────────────────────────────────
136
+ if (tool_name === 'Bash') {
137
+ const cmd = (tool_input.command || '').trim()
138
+
139
+ // Catastrophic filesystem operations
140
+ if (/rm\s+-[a-zA-Z]*r[a-zA-Z]*f|rm\s+-[a-zA-Z]*f[a-zA-Z]*r/.test(cmd)) {
141
+ // rm -rf or rm -fr -- check if target is outside worktree
142
+ const rmTarget = cmd.replace(/rm\s+-\S+\s+/, '').trim()
143
+ if (!rmTarget || isOutsideAllowedPaths(rmTarget) ||
144
+ /^(\/|~|\.\.)/.test(rmTarget.replace(/^['"]|['"]$/g, ''))) {
145
+ block(`destructive rm targeting outside worktree: ${cmd}`)
146
+ }
147
+ }
148
+
149
+ // Credential access
150
+ if (/\.(env|pem|key|cert)\b/.test(cmd) ||
151
+ /~\/\.(ssh|aws)/.test(cmd) ||
152
+ /TOKEN|SECRET|PASSWORD|API_KEY/i.test(cmd) && /echo|print|cat|export\s+\w+=/.test(cmd)) {
153
+ block(`credential access attempt: ${cmd}`)
154
+ }
155
+
156
+ // Dangerous git operations
157
+ if (/git\s+push\s+.*--force/.test(cmd)) block(`force push blocked: ${cmd}`)
158
+ if (/git\s+reset\s+--hard\s+HEAD~[2-9]/.test(cmd)) block(`multi-commit reset blocked: ${cmd}`)
159
+ if (/git\s+clean\s+-[a-zA-Z]*f[a-zA-Z]*d/.test(cmd)) block(`git clean -fd blocked: ${cmd}`)
160
+ if (/git\s+push\s+(?!origin\b)/.test(cmd)) block(`push to non-origin remote blocked: ${cmd}`)
161
+
162
+ // DB operations without authorization
163
+ if (/(DROP\s+TABLE|TRUNCATE\s+TABLE)/i.test(cmd) && !has_db_migration) {
164
+ block(`DROP/TRUNCATE requires has_db_migration: true in story submission`)
165
+ }
166
+ if (/DELETE\s+FROM\s+\w+\s*;/i.test(cmd)) {
167
+ block(`DELETE without WHERE clause blocked: ${cmd}`)
168
+ }
169
+
170
+ // High-risk but legitimate -- audit log only
171
+ if (/npm\s+install|pip\s+install|cargo\s+build|yarn\s+add/.test(cmd)) {
172
+ allow(`dependency install: ${cmd}`)
173
+ }
174
+ if (/curl|wget/.test(cmd)) {
175
+ allow(`network request: ${cmd}`)
176
+ }
177
+ if (/git\s+commit\s+--amend/.test(cmd)) {
178
+ allow(`git commit --amend`)
179
+ }
180
+ }
181
+
182
+ // ── WRITE / EDIT tool checks ────────────────────────────────────────────────
183
+ if (tool_name === 'Write' || tool_name === 'Edit') {
184
+ const filePath = tool_input.file_path || tool_input.path || ''
185
+
186
+ // Credential files
187
+ if (/\.(env|pem|key|cert)$/.test(filePath)) {
188
+ block(`write to credential file blocked: ${filePath}`)
189
+ }
190
+
191
+ // Path escape -- must be in allowed paths
192
+ if (filePath && isOutsideAllowedPaths(filePath)) {
193
+ block(`path escape: ${filePath} is outside allowed paths`)
194
+ }
195
+
196
+ // Writes to handoff/inbox dirs -- allowed but logged
197
+ if (filePath.includes('_vibe-chain-output')) {
198
+ allow(`pipeline output write: ${filePath}`)
199
+ }
200
+ }
201
+
202
+ // All other tools -- allow by default
203
+ allow(null)
204
+ ```
205
+
206
+ ## 2. Context file schema
207
+
208
+ Written by the forge daemon to `_vibe-chain-output/worker-inbox/<agent>/{story-id}.context.json`
209
+ alongside each inbox task file. Heimdall reads this on every invocation.
210
+
211
+ ```json
212
+ {
213
+ "story_id": "FORGE-3",
214
+ "agent": "furnace",
215
+ "worktree_path": "/abs/path/to/worktree",
216
+ "assigned_branch": "feature/forge-3-events-api",
217
+ "handoff_dir": "/abs/path/to/_vibe-chain-output/handoffs",
218
+ "has_db_migration": false,
219
+ "has_api_changes": true,
220
+ "allowed_paths": [
221
+ "/abs/path/to/worktree",
222
+ "/abs/path/to/_vibe-chain-output/handoffs",
223
+ "/abs/path/to/_vibe-chain-output/worker-inbox"
224
+ ]
225
+ }
226
+ ```
227
+
228
+ All paths must be absolute. The forge daemon resolves them from `project_root` at inbox-write time.
229
+
230
+ ## 3. `settings.local.json` written by daemon at worker startup
231
+
232
+ The forge daemon must write (or merge) this into `.claude/settings.local.json` in the
233
+ worker's working directory when starting a worker for a lab task. This installs Heimdall
234
+ for that worker session.
235
+
236
+ ```json
237
+ {
238
+ "hooks": {
239
+ "PreToolUse": [
240
+ {
241
+ "matcher": "Bash",
242
+ "hooks": [{ "type": "command", "command": "node bin/lib/heimdall.js" }]
243
+ },
244
+ {
245
+ "matcher": "Write",
246
+ "hooks": [{ "type": "command", "command": "node bin/lib/heimdall.js" }]
247
+ },
248
+ {
249
+ "matcher": "Edit",
250
+ "hooks": [{ "type": "command", "command": "node bin/lib/heimdall.js" }]
251
+ }
252
+ ]
253
+ }
254
+ }
255
+ ```
256
+
257
+ Use a merge strategy: if `.claude/settings.local.json` already exists, merge the hooks
258
+ array rather than overwrite. Preserve any existing hooks the worker session already has.
259
+
260
+ **Implementation:** `bin/lib/daemon/heimdall-setup.js` -- called by the daemon at inbox-write time.
261
+
262
+ ## 4. Escalation signal handling in forge daemon
263
+
264
+ The daemon's task monitoring loop (in `forge-daemon.sh` or the modularized equivalent)
265
+ must check for `.escalation` files:
266
+
267
+ ```bash
268
+ check_heimdall_escalations() {
269
+ local inbox_dir="$1"
270
+ for escalation_file in "$inbox_dir"/**/*.escalation; do
271
+ [[ -f "$escalation_file" ]] || continue
272
+ local story_id
273
+ story_id=$(basename "$escalation_file" .escalation)
274
+ local agent_dir
275
+ agent_dir=$(dirname "$escalation_file")
276
+ local agent
277
+ agent=$(basename "$agent_dir")
278
+
279
+ log "HEIMDALL SOUNDED: $agent/$story_id -- writing escalation handoff"
280
+
281
+ # Write escalation handoff to lab's handoff directory
282
+ # Lab sentinel will route to human review on next cycle
283
+ write_escalation_handoff "$story_id" "$agent" "$escalation_file"
284
+
285
+ # Move escalation file to processed
286
+ mv "$escalation_file" "${escalation_file}.processed"
287
+ done
288
+ }
289
+ ```
290
+
291
+ The escalation handoff format:
292
+ ```yaml
293
+ ---
294
+ type: worker-escalation
295
+ story: "{story_id}"
296
+ from: forge-daemon
297
+ to: human
298
+ created: {ISO8601}
299
+ status: pending
300
+ agent: "{agent}"
301
+ violations: {N}
302
+ ---
303
+
304
+ ## Heimdall Escalation
305
+
306
+ Worker `{agent}` accumulated {N} policy violations on story `{story_id}`.
307
+ The worker has been stopped for this story. Human review required.
308
+
309
+ ## Last Violation
310
+ {reason from .escalation JSON}
311
+
312
+ ## Action Required
313
+ Review the audit log at `heimdall-audit.log` and decide:
314
+ - Fix the story spec and resubmit
315
+ - Manually complete the work
316
+ - Cancel the story
317
+ ```
318
+
319
+ ## 5. Configuration
320
+
321
+ Add to `_vibe-chain/config.yaml` under `agent_personalities`:
322
+
323
+ ```yaml
324
+ agent_personalities:
325
+ heimdall:
326
+ enabled: true # always on when agent_personalities is configured
327
+ max_violations: 3 # violations before Gjallarhorn
328
+ audit_log: _vibe-chain-output/heimdall-audit.log
329
+ ```
330
+
331
+ ## 6. Tests (`tests/heimdall.test.js`)
332
+
333
+ ```javascript
334
+ import { describe, test, expect } from '@jest/globals'
335
+ // Test by invoking heimdall.js as a subprocess with mocked stdin and context file
336
+
337
+ describe('Heimdall -- hard blocks', () => {
338
+ test('blocks rm -rf outside worktree', async () => {
339
+ const result = await runHeimdall(
340
+ { tool_name: 'Bash', tool_input: { command: 'rm -rf /tmp/other' } },
341
+ { worktree_path: '/tmp/worktree', allowed_paths: ['/tmp/worktree'] }
342
+ )
343
+ expect(result.exitCode).toBe(2)
344
+ expect(result.stdout).toContain('destructive rm')
345
+ })
346
+
347
+ test('blocks force push', async () => {
348
+ const result = await runHeimdall(
349
+ { tool_name: 'Bash', tool_input: { command: 'git push origin --force' } },
350
+ baseContext()
351
+ )
352
+ expect(result.exitCode).toBe(2)
353
+ })
354
+
355
+ test('blocks path escape on Write', async () => {
356
+ const result = await runHeimdall(
357
+ { tool_name: 'Write', tool_input: { file_path: '/etc/passwd' } },
358
+ baseContext()
359
+ )
360
+ expect(result.exitCode).toBe(2)
361
+ expect(result.stdout).toContain('path escape')
362
+ })
363
+
364
+ test('blocks DROP TABLE without has_db_migration', async () => {
365
+ const result = await runHeimdall(
366
+ { tool_name: 'Bash', tool_input: { command: 'sqlite3 app.db "DROP TABLE users"' } },
367
+ { ...baseContext(), has_db_migration: false }
368
+ )
369
+ expect(result.exitCode).toBe(2)
370
+ })
371
+
372
+ test('allows DROP TABLE with has_db_migration', async () => {
373
+ const result = await runHeimdall(
374
+ { tool_name: 'Bash', tool_input: { command: 'sqlite3 app.db "DROP TABLE old_schema"' } },
375
+ { ...baseContext(), has_db_migration: true }
376
+ )
377
+ expect(result.exitCode).toBe(0)
378
+ })
379
+ })
380
+
381
+ describe('Heimdall -- audit log allows', () => {
382
+ test('allows npm install with audit entry', async () => {
383
+ const result = await runHeimdall(
384
+ { tool_name: 'Bash', tool_input: { command: 'npm install lodash' } },
385
+ baseContext()
386
+ )
387
+ expect(result.exitCode).toBe(0)
388
+ // Audit log should contain the entry -- checked via log file
389
+ })
390
+ })
391
+
392
+ describe('Heimdall -- forge-native passthrough', () => {
393
+ test('exits 0 immediately when no context file present', async () => {
394
+ const result = await runHeimdall(
395
+ { tool_name: 'Bash', tool_input: { command: 'rm -rf /' } },
396
+ null // no context file
397
+ )
398
+ expect(result.exitCode).toBe(0)
399
+ })
400
+ })
401
+
402
+ describe('Heimdall -- escalation', () => {
403
+ test('writes .escalation file after N violations', async () => {
404
+ // Run N blocking actions, check .escalation exists on Nth
405
+ })
406
+ })
407
+ ```
408
+
409
+ ---
410
+
411
+ # Notes
412
+
413
+ - Heimdall must be fast. It runs on every tool call. Target < 50ms per invocation.
414
+ Read context file once per process start (it is passed as a singleton call).
415
+ Do not make network calls.
416
+ - The `bin/lib/heimdall.js` path in the hook command must be absolute or resolvable
417
+ from the worker's working directory. Use `process.env.FORGE_BIN_DIR` if needed.
418
+ - PLAT-1 is a prerequisite for IPC-7 (background worker daemon mode).
419
+ It also applies to LAB-FORGE-3 (degraded spawn) -- LAB-FORGE-3 must write
420
+ `.claude/settings.local.json` at spawn time, same as IPC workers.
@@ -0,0 +1,58 @@
1
+ ---
2
+ id: SEC-001
3
+ title: Fix SQL Injection vulnerabilities in database.sh
4
+ type: security
5
+ priority: critical
6
+ status: completed
7
+ assigned_to: aegis
8
+ created_at: 2026-01-15T16:30:00Z
9
+ created_by: security-review
10
+ completed_at: 2026-01-15T17:30:00Z
11
+ completed_by: aegis
12
+ ---
13
+
14
+ ## Summary
15
+
16
+ Multiple SQLite queries in `bin/lib/database.sh` interpolate shell variables directly into SQL strings without proper escaping or parameterization.
17
+
18
+ ## Affected Lines
19
+
20
+ - Line 93: `db_set_daemon_state()` - `$new_state` interpolated
21
+ - Lines 136-145: `db_upsert_agent_status()` - `$agent`, `$status`, `$task`, `$message`, `$updated_at` interpolated
22
+ - Line 151: `db_get_agent_mtime()` - `$agent` interpolated
23
+ - Line 166: `db_get_agents_by_status()` - `$status` interpolated
24
+
25
+ ## Attack Scenario
26
+
27
+ A malicious agent status JSON file could contain:
28
+ ```json
29
+ {"agent": "'; DROP TABLE agent_status; --", "status": "working"}
30
+ ```
31
+
32
+ ## Remediation Applied
33
+
34
+ 1. Created `db_escape()` function that:
35
+ - Removes null bytes
36
+ - Escapes single quotes by doubling them (`'` -> `''`)
37
+ - Removes backslashes that could escape quotes
38
+
39
+ 2. Created `db_validate_identifier()` for validating identifiers
40
+
41
+ 3. Applied `db_escape()` to all user-controlled variables in:
42
+ - `db_upsert_agent_status()` - all string parameters
43
+ - `db_get_agent_mtime()` - agent parameter
44
+ - `db_get_agents_by_status()` - status parameter
45
+ - `db_record_status_history()` - all parameters
46
+
47
+ 4. Added numeric validation for:
48
+ - `file_mtime` in `db_upsert_agent_status()`
49
+ - `minutes` in `db_cleanup_stale_agents()`
50
+ - `days` in `db_prune_history()`
51
+
52
+ 5. Added whitelist validation for `db_set_daemon_state()` - only allows `active|idle|stopped`
53
+
54
+ ## Acceptance Criteria
55
+
56
+ - [x] All SQL queries use escaped or parameterized inputs
57
+ - [x] Numeric parameters validated before use
58
+ - [x] State values whitelist-validated
@@ -0,0 +1,45 @@
1
+ ---
2
+ id: SEC-002
3
+ title: Fix Command Injection in system notifications
4
+ type: security
5
+ priority: critical
6
+ status: completed
7
+ assigned_to: aegis
8
+ created_at: 2026-01-15T16:30:00Z
9
+ created_by: security-review
10
+ completed_at: 2026-01-15T17:30:00Z
11
+ completed_by: aegis
12
+ ---
13
+
14
+ ## Summary
15
+
16
+ The `send_system_notification()` function in `bin/forge-daemon.sh` directly interpolates `$message` into PowerShell and osascript commands without sanitization.
17
+
18
+ ## Affected Lines
19
+
20
+ - Lines 146-154: Windows PowerShell - `$message` in CreateTextNode()
21
+ - Line 158: macOS osascript - `$message` in display notification
22
+
23
+ ## Attack Scenario
24
+
25
+ Malicious task file:
26
+ ```yaml
27
+ title: "'); Start-Process calc; #"
28
+ ```
29
+
30
+ When extracted and passed to notification, executes arbitrary commands.
31
+
32
+ ## Remediation Applied
33
+
34
+ 1. Created `sanitize_notification_message()` function that:
35
+ - Removes null bytes
36
+ - Strips all characters except: alphanumeric, spaces, periods, commas, colons, semicolons, exclamation/question marks, hyphens, underscores
37
+ - Limits message length to 200 characters
38
+
39
+ 2. Applied sanitization in `send_system_notification()` before any shell interpolation
40
+
41
+ ## Acceptance Criteria
42
+
43
+ - [x] All notification messages sanitized before shell interpolation
44
+ - [x] Special characters stripped
45
+ - [x] Notifications still display readable messages (safe chars preserved)
@@ -0,0 +1,54 @@
1
+ ---
2
+ id: SEC-003
3
+ title: Fix eval injection in agent config loading
4
+ type: security
5
+ priority: high
6
+ status: completed
7
+ assigned_to: aegis
8
+ created_at: 2026-01-15T16:30:00Z
9
+ created_by: security-review
10
+ completed_at: 2026-01-15T17:30:00Z
11
+ completed_by: aegis
12
+ ---
13
+
14
+ ## Summary
15
+
16
+ The `load_agents_from_json()` function in `bin/lib/config.sh` uses `eval` to execute shell variable assignments generated from JSON file contents.
17
+
18
+ ## Affected Lines
19
+
20
+ - Line 100: `eval "$agent_data"`
21
+
22
+ ## Attack Scenario
23
+
24
+ Malicious `config/agents.json`:
25
+ ```json
26
+ {
27
+ "agents": {
28
+ "anvil$(whoami>/tmp/pwned)": {
29
+ "name": "Anvil"
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ Command substitution executes when `eval` runs.
36
+
37
+ ## Remediation Applied
38
+
39
+ 1. Added `isValidIdentifier()` function in Node.js that validates names match `^[a-z0-9_-]+$`
40
+
41
+ 2. Added `escapeForShell()` function that escapes dangerous characters in string values:
42
+ - Backslashes, double quotes, dollar signs, backticks, newlines
43
+
44
+ 3. All agent names and aliases are validated before output:
45
+ - Invalid names cause immediate exit with clear error message
46
+
47
+ 4. All string values (display names, roles, icons, etc.) are escaped before output
48
+
49
+ ## Acceptance Criteria
50
+
51
+ - [x] Agent names validated to alphanumeric + underscore/hyphen only
52
+ - [x] Special characters in agents.json cause clear error, not execution
53
+ - [x] Existing valid configurations continue to work
54
+ - [x] String values properly escaped for shell safety
@@ -0,0 +1,49 @@
1
+ ---
2
+ id: SEC-004
3
+ title: Fix TOCTOU race condition in PID file handling
4
+ type: security
5
+ priority: high
6
+ status: completed
7
+ assigned_to: aegis
8
+ created_at: 2026-01-15T16:30:00Z
9
+ created_by: security-review
10
+ completed_at: 2026-01-15T17:30:00Z
11
+ completed_by: aegis
12
+ ---
13
+
14
+ ## Summary
15
+
16
+ The daemon start logic in `bin/forge-daemon.sh` has a Time-of-Check-Time-of-Use (TOCTOU) race condition between checking if the daemon is running and writing the new PID file.
17
+
18
+ ## Affected Lines
19
+
20
+ - Lines 541-565: `cmd_start()` - race window between check and PID write
21
+
22
+ ## Attack Scenario
23
+
24
+ Two simultaneous `forge daemon start` commands could both pass the initial check, leading to:
25
+ - Multiple daemon instances
26
+ - PID file corruption
27
+ - Unpredictable behavior
28
+
29
+ ## Remediation Applied
30
+
31
+ 1. Added flock-based exclusive locking at the start of `cmd_start()`:
32
+ - Creates `startup.lock` file in `.forge/` directory
33
+ - Uses file descriptor 200 for the lock
34
+ - Non-blocking lock attempt (`flock -n`)
35
+ - Lock automatically released when script exits
36
+
37
+ 2. Graceful fallback when flock is not available:
38
+ - Falls back to existing PID-based check (less secure but functional)
39
+ - Maintains cross-platform compatibility
40
+
41
+ 3. Defense in depth:
42
+ - Existing LOCK_FILE check remains as secondary protection
43
+
44
+ ## Acceptance Criteria
45
+
46
+ - [x] Use flock or equivalent for atomic lock acquisition
47
+ - [x] Only one daemon instance can start at a time
48
+ - [x] Lock released properly on daemon exit (automatic via fd close)
49
+ - [x] Works on Windows (Git Bash), macOS, Linux (with fallback)
@@ -0,0 +1,51 @@
1
+ ---
2
+ id: SEC-005
3
+ title: Fix path traversal in worker-loop hook
4
+ type: security
5
+ priority: high
6
+ status: completed
7
+ assigned_to: aegis
8
+ created_at: 2026-01-15T16:30:00Z
9
+ created_by: security-review
10
+ completed_at: 2026-01-15T17:30:00Z
11
+ completed_by: aegis
12
+ ---
13
+
14
+ ## Summary
15
+
16
+ The worker-loop hook in `.claude/hooks/worker-loop.sh` defaults `FORGE_ROOT` to the current working directory without validation.
17
+
18
+ ## Affected Lines
19
+
20
+ - Line 20: `FORGE_ROOT="${FORGE_ROOT:-$(pwd)}"`
21
+
22
+ ## Attack Scenario
23
+
24
+ If the hook executes in an attacker-controlled directory (e.g., `/tmp/malicious`), task file checks could be redirected:
25
+ ```
26
+ FORGE_ROOT=/tmp/malicious
27
+ $TASKS_DIR="/tmp/malicious/tasks"
28
+ ```
29
+
30
+ Attacker could place malicious task files that get processed.
31
+
32
+ ## Remediation Applied
33
+
34
+ 1. If FORGE_ROOT is set via environment:
35
+ - Validate it contains `.forge/` or `tasks/` directory
36
+ - Reject with safe JSON response if validation fails
37
+
38
+ 2. If FORGE_ROOT is not set:
39
+ - Derive from script location (`.claude/hooks/` -> `../..`)
40
+ - Validate derived path contains expected directories
41
+ - Fall back to pwd only if it passes validation
42
+
43
+ 3. Safe failure mode:
44
+ - Returns `{"decision": "allow"}` with error message on validation failure
45
+ - Does not process potentially malicious task directories
46
+
47
+ ## Acceptance Criteria
48
+
49
+ - [x] FORGE_ROOT validated before use
50
+ - [x] Hook fails safely if run from invalid directory
51
+ - [x] Normal operation unaffected when run correctly