vibe-forge 0.3.12 → 0.8.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 (85) hide show
  1. package/.claude/commands/clear-attention.md +63 -63
  2. package/.claude/commands/compact-context.md +52 -0
  3. package/.claude/commands/configure-vcs.md +102 -0
  4. package/.claude/commands/forge.md +218 -171
  5. package/.claude/commands/need-help.md +77 -77
  6. package/.claude/commands/update-status.md +64 -64
  7. package/.claude/commands/worker-loop.md +106 -106
  8. package/.claude/hooks/worker-loop.js +217 -0
  9. package/.claude/scripts/setup-worker-loop.sh +45 -45
  10. package/.claude/settings.json +89 -0
  11. package/LICENSE +21 -21
  12. package/README.md +253 -230
  13. package/agents/aegis/personality.md +303 -269
  14. package/agents/anvil/personality.md +278 -211
  15. package/agents/architect/personality.md +260 -0
  16. package/agents/crucible/personality.md +362 -285
  17. package/agents/crucible-x/personality.md +210 -0
  18. package/agents/ember/personality.md +293 -245
  19. package/agents/flux/personality.md +248 -0
  20. package/agents/furnace/personality.md +342 -262
  21. package/agents/herald/personality.md +249 -247
  22. package/agents/loki/personality.md +108 -0
  23. package/agents/oracle/personality.md +284 -0
  24. package/agents/pixel/personality.md +140 -0
  25. package/agents/planning-hub/personality.md +473 -251
  26. package/agents/scribe/personality.md +253 -231
  27. package/agents/slag/personality.md +268 -0
  28. package/agents/temper/personality.md +270 -0
  29. package/bin/cli.js +372 -325
  30. package/bin/dashboard/api/agents.js +333 -0
  31. package/bin/dashboard/api/dispatch.js +507 -0
  32. package/bin/dashboard/api/tasks.js +416 -0
  33. package/bin/dashboard/public/assets/index-BpHfsx1r.js +2 -0
  34. package/bin/dashboard/public/assets/index-QODv4Zn9.css +1 -0
  35. package/bin/dashboard/public/index.html +14 -0
  36. package/bin/dashboard/server.js +645 -0
  37. package/bin/forge-daemon.sh +477 -775
  38. package/bin/forge-setup.sh +661 -532
  39. package/bin/forge-spawn.sh +164 -159
  40. package/bin/forge.cmd +83 -83
  41. package/bin/forge.sh +566 -393
  42. package/bin/lib/agents.sh +177 -177
  43. package/bin/lib/check-aliases.js +50 -0
  44. package/bin/lib/colors.sh +44 -44
  45. package/bin/lib/config.sh +347 -271
  46. package/bin/lib/constants.sh +241 -171
  47. package/bin/lib/daemon/budgets.sh +107 -0
  48. package/bin/lib/daemon/dependencies.sh +146 -0
  49. package/bin/lib/daemon/display.sh +128 -0
  50. package/bin/lib/daemon/notifications.sh +273 -0
  51. package/bin/lib/daemon/routing.sh +93 -0
  52. package/bin/lib/daemon/state.sh +163 -0
  53. package/bin/lib/daemon/sync.sh +103 -0
  54. package/bin/lib/database.sh +357 -224
  55. package/bin/lib/frontmatter.js +106 -0
  56. package/bin/lib/heimdall-setup.js +113 -0
  57. package/bin/lib/heimdall.js +265 -0
  58. package/bin/lib/json.sh +264 -0
  59. package/bin/lib/terminal.js +452 -0
  60. package/bin/lib/util.sh +126 -0
  61. package/bin/lib/vcs.js +349 -0
  62. package/config/agent-manifest.yaml +237 -230
  63. package/config/agents.json +207 -85
  64. package/config/task-template.md +159 -87
  65. package/config/task-types.yaml +111 -106
  66. package/config/templates/handoff-template.md +40 -0
  67. package/context/agent-overrides/README.md +41 -0
  68. package/context/architecture.md +42 -0
  69. package/context/modern-conventions.md +129 -129
  70. package/context/project-context-template.md +122 -122
  71. package/docs/agents.md +473 -0
  72. package/docs/architecture.md +194 -0
  73. package/docs/commands.md +451 -0
  74. package/docs/security.md +195 -144
  75. package/package.json +77 -48
  76. package/.claude/hooks/worker-loop.sh +0 -141
  77. package/.claude/settings.local.json +0 -29
  78. package/agents/forge-master/capabilities.md +0 -144
  79. package/agents/forge-master/context-template.md +0 -128
  80. package/agents/forge-master/personality.md +0 -138
  81. package/agents/sentinel/personality.md +0 -194
  82. package/context/forge-state.yaml +0 -19
  83. package/docs/TODO.md +0 -176
  84. package/docs/npm-publishing.md +0 -95
  85. package/tasks/review/task-001.md +0 -78
@@ -1,171 +1,241 @@
1
- #!/usr/bin/env bash
2
- #
3
- # Vibe Forge - Shared Constants
4
- # Source this file in other scripts: source "$SCRIPT_DIR/lib/constants.sh"
5
- #
6
- # NOTE: Agent configuration should be loaded from config/agents.json via
7
- # load_agents_from_json() in config.sh. The hardcoded values below are
8
- # fallback defaults for when JSON loading is not possible (e.g., tests).
9
- #
10
-
11
- # Timing
12
- POLL_INTERVAL=2 # seconds between daemon task checks
13
- NOTIFICATION_DEBOUNCE=1 # seconds to avoid notification spam
14
-
15
- # Directory structure (relative to FORGE_ROOT)
16
- CONFIG_DIR=".forge"
17
- TASKS_DIR="tasks"
18
- TASKS_PENDING="$TASKS_DIR/pending"
19
- TASKS_IN_PROGRESS="$TASKS_DIR/in-progress"
20
- TASKS_COMPLETED="$TASKS_DIR/completed"
21
- TASKS_REVIEW="$TASKS_DIR/review"
22
- TASKS_APPROVED="$TASKS_DIR/approved"
23
- TASKS_NEEDS_CHANGES="$TASKS_DIR/needs-changes"
24
- TASKS_MERGED="$TASKS_DIR/merged"
25
- TASKS_ATTENTION="$TASKS_DIR/attention"
26
- CONTEXT_DIR="context"
27
- AGENT_STATUS_DIR="$CONTEXT_DIR/agent-status"
28
- AGENTS_DIR="agents"
29
-
30
- # Config files
31
- AGENTS_CONFIG="config/agents.json"
32
-
33
- # =============================================================================
34
- # Agent Configuration (Fallback Defaults)
35
- # =============================================================================
36
- # These values are overwritten when load_agents_from_json() is called.
37
- # They exist as fallback for contexts where JSON loading isn't available.
38
-
39
- # Flag to track if agents were loaded from JSON
40
- AGENTS_LOADED="${AGENTS_LOADED:-false}"
41
-
42
- # Valid agent names (whitelist for security)
43
- # These are the ONLY valid canonical agent names
44
- VALID_AGENTS=(
45
- "anvil"
46
- "furnace"
47
- "crucible"
48
- "sentinel"
49
- "scribe"
50
- "herald"
51
- "ember"
52
- "aegis"
53
- "hub"
54
- )
55
-
56
- # Agent aliases map (for resolve_agent)
57
- # Format: alias=canonical
58
- declare -A AGENT_ALIASES=(
59
- # Anvil aliases
60
- ["anvil"]="anvil"
61
- ["frontend"]="anvil"
62
- ["ui"]="anvil"
63
- ["fe"]="anvil"
64
- # Furnace aliases
65
- ["furnace"]="furnace"
66
- ["backend"]="furnace"
67
- ["api"]="furnace"
68
- ["be"]="furnace"
69
- # Crucible aliases
70
- ["crucible"]="crucible"
71
- ["test"]="crucible"
72
- ["testing"]="crucible"
73
- ["qa"]="crucible"
74
- ["tester"]="crucible"
75
- # Sentinel aliases
76
- ["sentinel"]="sentinel"
77
- ["review"]="sentinel"
78
- ["reviewer"]="sentinel"
79
- ["cr"]="sentinel"
80
- # Scribe aliases
81
- ["scribe"]="scribe"
82
- ["docs"]="scribe"
83
- ["documentation"]="scribe"
84
- ["doc"]="scribe"
85
- # Herald aliases
86
- ["herald"]="herald"
87
- ["release"]="herald"
88
- ["deploy"]="herald"
89
- ["deployment"]="herald"
90
- # Ember aliases
91
- ["ember"]="ember"
92
- ["devops"]="ember"
93
- ["ops"]="ember"
94
- ["infra"]="ember"
95
- ["infrastructure"]="ember"
96
- # Aegis aliases
97
- ["aegis"]="aegis"
98
- ["security"]="aegis"
99
- ["sec"]="aegis"
100
- ["appsec"]="aegis"
101
- # Hub aliases
102
- ["hub"]="hub"
103
- ["planning"]="hub"
104
- ["master"]="hub"
105
- ["forge-master"]="hub"
106
- )
107
-
108
- # Agent display names
109
- declare -A AGENT_DISPLAY_NAMES=(
110
- ["anvil"]="Anvil"
111
- ["furnace"]="Furnace"
112
- ["crucible"]="Crucible"
113
- ["sentinel"]="Sentinel"
114
- ["scribe"]="Scribe"
115
- ["herald"]="Herald"
116
- ["ember"]="Ember"
117
- ["aegis"]="Aegis"
118
- ["hub"]="Planning Hub"
119
- )
120
-
121
- # Agent roles
122
- declare -A AGENT_ROLES=(
123
- ["anvil"]="Frontend Developer"
124
- ["furnace"]="Backend Developer"
125
- ["crucible"]="Tester / QA"
126
- ["sentinel"]="Code Reviewer"
127
- ["scribe"]="Documentation"
128
- ["herald"]="Release Manager"
129
- ["ember"]="DevOps"
130
- ["aegis"]="Security"
131
- ["hub"]="Planning & Coordination"
132
- )
133
-
134
- # Agent personality files (relative to FORGE_ROOT)
135
- declare -A AGENT_PERSONALITY_FILES=(
136
- ["anvil"]="agents/anvil/personality.md"
137
- ["furnace"]="agents/furnace/personality.md"
138
- ["crucible"]="agents/crucible/personality.md"
139
- ["sentinel"]="agents/sentinel/personality.md"
140
- ["scribe"]="agents/scribe/personality.md"
141
- ["herald"]="agents/herald/personality.md"
142
- ["ember"]="agents/ember/personality.md"
143
- ["aegis"]="agents/aegis/personality.md"
144
- ["hub"]="agents/planning-hub/personality.md"
145
- )
146
-
147
- # Agent icons
148
- declare -A AGENT_ICONS=(
149
- ["anvil"]="🔨"
150
- ["furnace"]="🔥"
151
- ["crucible"]="🧪"
152
- ["sentinel"]="🛡️"
153
- ["scribe"]="📜"
154
- ["herald"]="📯"
155
- ["ember"]="⚙️"
156
- ["aegis"]="🔒"
157
- ["hub"]="🔥"
158
- )
159
-
160
- # Agent tab colors for Windows Terminal (hex format)
161
- declare -A AGENT_TAB_COLORS=(
162
- ["anvil"]="#3B82F6"
163
- ["furnace"]="#EF4444"
164
- ["crucible"]="#10B981"
165
- ["sentinel"]="#8B5CF6"
166
- ["scribe"]="#F59E0B"
167
- ["herald"]="#EC4899"
168
- ["ember"]="#F97316"
169
- ["aegis"]="#06B6D4"
170
- ["hub"]="#FF6B35"
171
- )
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Vibe Forge - Shared Constants
4
+ # Source this file in other scripts: source "$SCRIPT_DIR/lib/constants.sh"
5
+ #
6
+ # NOTE: Agent configuration should be loaded from config/agents.json via
7
+ # load_agents_from_json() in config.sh. The hardcoded values below are
8
+ # fallback defaults for when JSON loading is not possible (e.g., tests).
9
+ #
10
+
11
+ # =============================================================================
12
+ # Exit Codes
13
+ # =============================================================================
14
+ # Standardized exit codes for consistent error handling across scripts
15
+ # Based on BSD sysexits.h conventions with project-specific additions
16
+
17
+ EXIT_SUCCESS=0 # Successful execution
18
+ EXIT_GENERAL_ERROR=1 # Generic/unspecified error
19
+ EXIT_CONFIG_ERROR=2 # Configuration file missing/invalid
20
+ EXIT_DEPENDENCY_MISSING=3 # Required dependency not found (Claude Code, Git Bash, etc.)
21
+ EXIT_INVALID_ARGUMENT=4 # Invalid argument (unknown agent, unknown command)
22
+ EXIT_RUNTIME_ERROR=5 # Runtime error (daemon conflict, spawn failed, etc.)
23
+
24
+ # Timing
25
+ POLL_INTERVAL=2 # seconds between daemon task checks
26
+ NOTIFICATION_DEBOUNCE=1 # seconds to avoid notification spam
27
+
28
+ # Daemon Configuration
29
+ MAX_LOG_SIZE=1048576 # 1MB - log rotation threshold
30
+ MAX_NOTIFY_ENTRIES=1000 # Maximum notification log entries before trimming
31
+ STALE_STATUS_THRESHOLD=300 # 5 minutes - when worker status is considered stale
32
+ MAINTENANCE_INTERVAL=100 # Daemon iterations between maintenance runs
33
+ STALE_CLEANUP_MINUTES=30 # Remove agent status older than this (minutes)
34
+ HISTORY_PRUNE_DAYS=7 # Remove status history older than this (days)
35
+
36
+ # Directory structure (relative to FORGE_ROOT)
37
+ CONFIG_DIR=".forge"
38
+ TASKS_DIR="tasks"
39
+ TASKS_PENDING="$TASKS_DIR/pending"
40
+ TASKS_IN_PROGRESS="$TASKS_DIR/in-progress"
41
+ TASKS_COMPLETED="$TASKS_DIR/completed"
42
+ TASKS_REVIEW="$TASKS_DIR/review"
43
+ TASKS_APPROVED="$TASKS_DIR/approved"
44
+ TASKS_NEEDS_CHANGES="$TASKS_DIR/needs-changes"
45
+ TASKS_MERGED="$TASKS_DIR/merged"
46
+ TASKS_ATTENTION="$TASKS_DIR/attention"
47
+ CONTEXT_DIR="context"
48
+ AGENT_STATUS_DIR="$CONTEXT_DIR/agent-status"
49
+ AGENTS_DIR="agents"
50
+
51
+ # Config files
52
+ AGENTS_CONFIG="config/agents.json"
53
+
54
+ # =============================================================================
55
+ # Agent Configuration (Fallback Defaults)
56
+ # =============================================================================
57
+ # These values are overwritten when load_agents_from_json() is called.
58
+ # They exist as fallback for contexts where JSON loading isn't available.
59
+
60
+ # Flag to track if agents were loaded from JSON
61
+ AGENTS_LOADED="${AGENTS_LOADED:-false}"
62
+
63
+ # Valid agent names (whitelist for security)
64
+ # These are the ONLY valid canonical agent names
65
+ # NOTE: Must stay in sync with config/agents.json (which is the source of truth)
66
+ # Last sync: 2026-04-03 (added loki, renamed sentinel→temper)
67
+ VALID_AGENTS=(
68
+ "hub"
69
+ "temper"
70
+ "anvil"
71
+ "furnace"
72
+ "crucible"
73
+ "scribe"
74
+ "herald"
75
+ "ember"
76
+ "aegis"
77
+ "architect"
78
+ "pixel"
79
+ "oracle"
80
+ "loki"
81
+ )
82
+
83
+ # Agent aliases map (for resolve_agent)
84
+ # Format: alias=canonical
85
+ # NOTE: Must stay in sync with config/agents.json (which is the source of truth)
86
+ declare -A AGENT_ALIASES=(
87
+ # Hub aliases (Chief Orchestrator)
88
+ ["hub"]="hub"
89
+ ["planning"]="hub"
90
+ ["master"]="hub"
91
+ ["forge-master"]="hub"
92
+ # Temper aliases (Code Reviewer)
93
+ ["temper"]="temper"
94
+ ["review"]="temper"
95
+ ["reviewer"]="temper"
96
+ ["cr"]="temper"
97
+ # Anvil aliases (Frontend)
98
+ ["anvil"]="anvil"
99
+ ["frontend"]="anvil"
100
+ ["ui"]="anvil"
101
+ ["fe"]="anvil"
102
+ # Furnace aliases (Backend)
103
+ ["furnace"]="furnace"
104
+ ["backend"]="furnace"
105
+ ["api"]="furnace"
106
+ ["be"]="furnace"
107
+ # Crucible aliases (Testing)
108
+ ["crucible"]="crucible"
109
+ ["test"]="crucible"
110
+ ["testing"]="crucible"
111
+ ["qa"]="crucible"
112
+ ["tester"]="crucible"
113
+ # Scribe aliases (Documentation)
114
+ ["scribe"]="scribe"
115
+ ["docs"]="scribe"
116
+ ["documentation"]="scribe"
117
+ ["doc"]="scribe"
118
+ # Herald aliases (Release)
119
+ ["herald"]="herald"
120
+ ["release"]="herald"
121
+ ["deploy"]="herald"
122
+ ["deployment"]="herald"
123
+ # Ember aliases (DevOps)
124
+ ["ember"]="ember"
125
+ ["devops"]="ember"
126
+ ["ops"]="ember"
127
+ ["infra"]="ember"
128
+ ["infrastructure"]="ember"
129
+ # Aegis aliases (Security)
130
+ ["aegis"]="aegis"
131
+ ["security"]="aegis"
132
+ ["sec"]="aegis"
133
+ ["appsec"]="aegis"
134
+ # Architect aliases (System Design)
135
+ ["architect"]="architect"
136
+ ["arch"]="architect"
137
+ ["sage"]="architect"
138
+ # Pixel aliases (UX Design)
139
+ # NOTE: "design" intentionally removed — ambiguous with architect's technical design domain
140
+ ["pixel"]="pixel"
141
+ ["ux"]="pixel"
142
+ ["ui-design"]="pixel"
143
+ ["user-experience"]="pixel"
144
+ # Oracle aliases (Product / Requirements)
145
+ ["oracle"]="oracle"
146
+ ["product"]="oracle"
147
+ ["po"]="oracle"
148
+ ["requirements"]="oracle"
149
+ ["req"]="oracle"
150
+ ["analyst"]="oracle"
151
+ # Loki aliases (Lateral Thinker)
152
+ ["loki"]="loki"
153
+ ["trickster"]="loki"
154
+ ["contrarian"]="loki"
155
+ ["brainstorm"]="loki"
156
+ )
157
+
158
+ # Agent display names
159
+ declare -A AGENT_DISPLAY_NAMES=(
160
+ ["hub"]="Planning Hub"
161
+ ["temper"]="Temper"
162
+ ["anvil"]="Anvil"
163
+ ["furnace"]="Furnace"
164
+ ["crucible"]="Crucible"
165
+ ["scribe"]="Scribe"
166
+ ["herald"]="Herald"
167
+ ["ember"]="Ember"
168
+ ["aegis"]="Aegis"
169
+ ["architect"]="Architect"
170
+ ["pixel"]="Pixel"
171
+ ["oracle"]="Oracle"
172
+ ["loki"]="Loki"
173
+ )
174
+
175
+ # Agent roles
176
+ declare -A AGENT_ROLES=(
177
+ ["hub"]="Chief Orchestrator"
178
+ ["temper"]="Code Reviewer"
179
+ ["anvil"]="Frontend Developer"
180
+ ["furnace"]="Backend Developer"
181
+ ["crucible"]="Tester / QA"
182
+ ["scribe"]="Documentation Specialist"
183
+ ["herald"]="Release Manager"
184
+ ["ember"]="DevOps Engineer"
185
+ ["aegis"]="Security Specialist"
186
+ ["architect"]="System Architect"
187
+ ["pixel"]="UX Designer"
188
+ ["oracle"]="Product Owner / Requirements Analyst"
189
+ ["loki"]="Lateral Thinker"
190
+ )
191
+
192
+ # Agent personality files (relative to FORGE_ROOT)
193
+ declare -A AGENT_PERSONALITY_FILES=(
194
+ ["hub"]="agents/planning-hub/personality.md"
195
+ ["temper"]="agents/temper/personality.md"
196
+ ["anvil"]="agents/anvil/personality.md"
197
+ ["furnace"]="agents/furnace/personality.md"
198
+ ["crucible"]="agents/crucible/personality.md"
199
+ ["scribe"]="agents/scribe/personality.md"
200
+ ["herald"]="agents/herald/personality.md"
201
+ ["ember"]="agents/ember/personality.md"
202
+ ["aegis"]="agents/aegis/personality.md"
203
+ ["architect"]="agents/architect/personality.md"
204
+ ["pixel"]="agents/pixel/personality.md"
205
+ ["oracle"]="agents/oracle/personality.md"
206
+ ["loki"]="agents/loki/personality.md"
207
+ )
208
+
209
+ # Agent icons
210
+ declare -A AGENT_ICONS=(
211
+ ["hub"]="⚒️"
212
+ ["temper"]="⚖️"
213
+ ["anvil"]="🔨"
214
+ ["furnace"]="🔥"
215
+ ["crucible"]="🧪"
216
+ ["scribe"]="📜"
217
+ ["herald"]="📯"
218
+ ["ember"]="⚙️"
219
+ ["aegis"]="🔒"
220
+ ["architect"]="🏛️"
221
+ ["pixel"]="🎨"
222
+ ["oracle"]="🔮"
223
+ ["loki"]="🎭"
224
+ )
225
+
226
+ # Agent tab colors for Windows Terminal (hex format)
227
+ declare -A AGENT_TAB_COLORS=(
228
+ ["hub"]="#FF6B35"
229
+ ["temper"]="#8B5CF6"
230
+ ["anvil"]="#3B82F6"
231
+ ["furnace"]="#EF4444"
232
+ ["crucible"]="#10B981"
233
+ ["scribe"]="#F59E0B"
234
+ ["herald"]="#EC4899"
235
+ ["ember"]="#F97316"
236
+ ["aegis"]="#06B6D4"
237
+ ["architect"]="#6366F1"
238
+ ["pixel"]="#D946EF"
239
+ ["oracle"]="#FBBF24"
240
+ ["loki"]="#7C3AED"
241
+ )
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # bin/lib/daemon/budgets.sh
4
+ #
5
+ # Token budget warnings (T2-F1)
6
+ #
7
+ # Monitors agent session duration as a proxy for token consumption.
8
+ # Claude Code sessions don't expose token counts externally, so we
9
+ # estimate from how long an agent has been continuously "working" on
10
+ # a single task. Agents working beyond the warning threshold get an
11
+ # attention file so the user knows to check on them.
12
+ #
13
+ # Dependencies: database.sh, constants.sh
14
+ # Globals required: FORGE_ROOT, FORGE_DB, TASKS_ATTENTION, LOG_FILE
15
+
16
+ # Prevent double-sourcing
17
+ [[ -n "${_DAEMON_BUDGETS_LOADED:-}" ]] && return 0
18
+ _DAEMON_BUDGETS_LOADED=1
19
+
20
+ # Thresholds (seconds)
21
+ TOKEN_WARN_DURATION=${TOKEN_WARN_DURATION:-5400} # 90 minutes
22
+ TOKEN_URGENT_DURATION=${TOKEN_URGENT_DURATION:-7200} # 2 hours
23
+
24
+ # Track which agents we've already warned about (reset when they go idle)
25
+ declare -A _budget_warned 2>/dev/null || true
26
+
27
+ check_token_budgets() {
28
+ local now_epoch
29
+ now_epoch=$(date +%s)
30
+
31
+ # Query agents currently in "working" status
32
+ local rows
33
+ rows=$(sqlite3 "$FORGE_DB" \
34
+ "SELECT agent, task, updated_at FROM agent_status WHERE status='working';" 2>/dev/null) || return 0
35
+
36
+ [[ -z "$rows" ]] && return 0
37
+
38
+ while IFS='|' read -r agent task updated; do
39
+ [[ -z "$agent" || -z "$updated" ]] && continue
40
+
41
+ # Calculate how long agent has been working
42
+ local updated_epoch duration
43
+ updated_epoch=$(date -d "$updated" +%s 2>/dev/null || echo "0")
44
+ [[ "$updated_epoch" -eq 0 ]] && continue
45
+ duration=$((now_epoch - updated_epoch))
46
+
47
+ # Skip if we already warned for this agent+task combo
48
+ local warn_key="${agent}:${task}"
49
+ [[ "${_budget_warned[$warn_key]:-}" == "1" ]] && continue
50
+
51
+ local urgency=""
52
+ if [[ "$duration" -ge "$TOKEN_URGENT_DURATION" ]]; then
53
+ urgency="urgent"
54
+ elif [[ "$duration" -ge "$TOKEN_WARN_DURATION" ]]; then
55
+ urgency="normal"
56
+ fi
57
+
58
+ [[ -z "$urgency" ]] && continue
59
+
60
+ # Create attention file
61
+ local attention_dir="$FORGE_ROOT/$TASKS_ATTENTION"
62
+ mkdir -p "$attention_dir"
63
+
64
+ local duration_min=$((duration / 60))
65
+ local attention_file="$attention_dir/token-budget-${agent}.md"
66
+
67
+ # Don't overwrite if attention file already exists
68
+ if [[ -f "$attention_file" ]]; then
69
+ _budget_warned[$warn_key]=1
70
+ continue
71
+ fi
72
+
73
+ cat > "$attention_file" << BUDGET
74
+ ---
75
+ agent: $agent
76
+ created: $(date -Iseconds)
77
+ type: budget-warning
78
+ urgency: $urgency
79
+ ---
80
+
81
+ ## Issue
82
+
83
+ Token budget warning: **$agent** has been working on \`$task\` for ${duration_min} minutes.
84
+
85
+ Long-running sessions risk context degradation. Consider:
86
+ - Check if the agent is stuck or looping
87
+ - Use \`/compact-context\` in the agent's session
88
+ - Split remaining work into a new task for a fresh session
89
+ BUDGET
90
+
91
+ _budget_warned[$warn_key]=1
92
+ echo "[$(date -Iseconds)] TOKEN BUDGET: $agent working ${duration_min}m on $task ($urgency)" >> "$LOG_FILE"
93
+ done <<< "$rows"
94
+
95
+ # Clear warnings for agents that are no longer working
96
+ for key in "${!_budget_warned[@]}"; do
97
+ local agent_part="${key%%:*}"
98
+ local still_working
99
+ still_working=$(sqlite3 "$FORGE_DB" \
100
+ "SELECT COUNT(*) FROM agent_status WHERE agent='$agent_part' AND status='working';" 2>/dev/null || echo "0")
101
+ if [[ "$still_working" -eq 0 ]]; then
102
+ unset "_budget_warned[$key]"
103
+ # Clean up attention file if agent finished
104
+ rm -f "$FORGE_ROOT/$TASKS_ATTENTION/token-budget-${agent_part}.md" 2>/dev/null
105
+ fi
106
+ done
107
+ }
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # bin/lib/daemon/dependencies.sh
4
+ #
5
+ # Task dependency resolution (T2-H2)
6
+ #
7
+ # Checks blocked_by fields in pending task frontmatter. Tasks whose
8
+ # blockers are all resolved (in completed/ or merged/) are eligible
9
+ # for dispatch. Tasks with unresolved blockers are surfaced in the
10
+ # state file so Planning Hub and the dashboard can show them.
11
+ #
12
+ # Dependencies: frontmatter.js, constants.sh
13
+ # Globals required: FORGE_ROOT, TASKS_PENDING, TASKS_COMPLETED,
14
+ # TASKS_MERGED, LOG_FILE
15
+
16
+ # Prevent double-sourcing
17
+ [[ -n "${_DAEMON_DEPENDENCIES_LOADED:-}" ]] && return 0
18
+ _DAEMON_DEPENDENCIES_LOADED=1
19
+
20
+ # Node.js frontmatter helper
21
+ FRONTMATTER_JS="${FRONTMATTER_JS:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/frontmatter.js}"
22
+
23
+ # Count of currently blocked tasks (used by state.sh)
24
+ BLOCKED_TASK_COUNT=0
25
+
26
+ # Check if a task ID exists in completed or merged directories
27
+ _is_task_resolved() {
28
+ local task_id="$1"
29
+ local completed_dir="$FORGE_ROOT/$TASKS_COMPLETED"
30
+ local merged_dir="$FORGE_ROOT/$TASKS_MERGED"
31
+
32
+ # Check completed/
33
+ for f in "$completed_dir"/*.md; do
34
+ [[ -f "$f" ]] || continue
35
+ if grep -q "^id: *${task_id}$" "$f" 2>/dev/null; then
36
+ return 0
37
+ fi
38
+ done
39
+
40
+ # Check merged/
41
+ for f in "$merged_dir"/*.md; do
42
+ [[ -f "$f" ]] || continue
43
+ if grep -q "^id: *${task_id}$" "$f" 2>/dev/null; then
44
+ return 0
45
+ fi
46
+ done
47
+
48
+ return 1
49
+ }
50
+
51
+ # Check all pending tasks for unresolved blockers.
52
+ # Sets BLOCKED_TASK_COUNT and outputs YAML for blocked tasks.
53
+ check_task_dependencies() {
54
+ BLOCKED_TASK_COUNT=0
55
+ local pending_dir="$FORGE_ROOT/$TASKS_PENDING"
56
+
57
+ [[ -d "$pending_dir" ]] || return 0
58
+
59
+ for task_file in "$pending_dir"/*.md; do
60
+ [[ -f "$task_file" && ! -L "$task_file" ]] || continue
61
+
62
+ # Extract blocked_by field (comma or space separated list of task IDs)
63
+ local blocked_by
64
+ blocked_by=$(node "$FRONTMATTER_JS" "$task_file" "blocked_by" 2>/dev/null | sed -n 's/^blocked_by=//p')
65
+
66
+ [[ -z "$blocked_by" ]] && continue
67
+
68
+ # Parse the list (handle comma-separated, space-separated, or YAML array)
69
+ local blockers
70
+ blockers=$(echo "$blocked_by" | tr ',[]' ' ' | tr -s ' ')
71
+
72
+ local unresolved=""
73
+ local all_resolved=true
74
+
75
+ for blocker_id in $blockers; do
76
+ # Skip empty tokens
77
+ [[ -z "$blocker_id" ]] && continue
78
+
79
+ if ! _is_task_resolved "$blocker_id"; then
80
+ all_resolved=false
81
+ if [[ -n "$unresolved" ]]; then
82
+ unresolved="$unresolved, $blocker_id"
83
+ else
84
+ unresolved="$blocker_id"
85
+ fi
86
+ fi
87
+ done
88
+
89
+ if [[ "$all_resolved" == "false" ]]; then
90
+ ((BLOCKED_TASK_COUNT++)) || true
91
+ local task_id
92
+ task_id=$(node "$FRONTMATTER_JS" "$task_file" "id" 2>/dev/null | sed -n 's/^id=//p')
93
+ task_id="${task_id:-$(basename "$task_file" .md)}"
94
+ echo "[$(date -Iseconds)] BLOCKED: $task_id waiting on: $unresolved" >> "$LOG_FILE"
95
+ fi
96
+ done
97
+ }
98
+
99
+ # Build YAML section for blocked tasks (called by state.sh update_state)
100
+ build_blocked_tasks() {
101
+ local pending_dir="$FORGE_ROOT/$TASKS_PENDING"
102
+
103
+ [[ -d "$pending_dir" ]] || return 0
104
+
105
+ local has_blocked=false
106
+
107
+ for task_file in "$pending_dir"/*.md; do
108
+ [[ -f "$task_file" && ! -L "$task_file" ]] || continue
109
+
110
+ local blocked_by
111
+ blocked_by=$(node "$FRONTMATTER_JS" "$task_file" "blocked_by" 2>/dev/null | sed -n 's/^blocked_by=//p')
112
+
113
+ [[ -z "$blocked_by" ]] && continue
114
+
115
+ local blockers unresolved_list=""
116
+ blockers=$(echo "$blocked_by" | tr ',[]' ' ' | tr -s ' ')
117
+
118
+ for blocker_id in $blockers; do
119
+ [[ -z "$blocker_id" ]] && continue
120
+ if ! _is_task_resolved "$blocker_id"; then
121
+ if [[ -n "$unresolved_list" ]]; then
122
+ unresolved_list="$unresolved_list, $blocker_id"
123
+ else
124
+ unresolved_list="$blocker_id"
125
+ fi
126
+ fi
127
+ done
128
+
129
+ if [[ -n "$unresolved_list" ]]; then
130
+ if [[ "$has_blocked" == "false" ]]; then
131
+ echo "blocked_tasks:"
132
+ has_blocked=true
133
+ fi
134
+
135
+ local task_id title
136
+ task_id=$(node "$FRONTMATTER_JS" "$task_file" "id" 2>/dev/null | sed -n 's/^id=//p')
137
+ title=$(node "$FRONTMATTER_JS" "$task_file" "title" 2>/dev/null | sed -n 's/^title=//p')
138
+ task_id="${task_id:-$(basename "$task_file" .md)}"
139
+ title="${title:-Untitled}"
140
+
141
+ printf ' - id: %s\n' "$task_id"
142
+ printf ' title: "%s"\n' "$title"
143
+ printf ' waiting_on: "%s"\n' "$unresolved_list"
144
+ fi
145
+ done
146
+ }