vibe-forge 0.4.0 → 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 (129) 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 +5 -5
  4. package/.claude/commands/forge.md +50 -3
  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 +37 -4
  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 +211 -232
  13. package/agents/aegis/personality.md +35 -1
  14. package/agents/anvil/personality.md +39 -1
  15. package/agents/architect/personality.md +26 -0
  16. package/agents/crucible/personality.md +54 -1
  17. package/agents/crucible-x/personality.md +210 -0
  18. package/agents/ember/personality.md +29 -1
  19. package/agents/flux/personality.md +248 -0
  20. package/agents/furnace/personality.md +52 -1
  21. package/agents/herald/personality.md +3 -1
  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 +222 -0
  26. package/agents/scribe/personality.md +3 -1
  27. package/agents/slag/personality.md +268 -0
  28. package/agents/{sentinel → temper}/personality.md +85 -9
  29. package/bin/cli.js +77 -30
  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 +176 -550
  38. package/bin/forge-setup.sh +28 -11
  39. package/bin/forge-spawn.sh +5 -5
  40. package/bin/forge.cmd +83 -83
  41. package/bin/forge.sh +210 -31
  42. package/config/agent-manifest.yaml +237 -243
  43. package/config/agents.json +207 -132
  44. package/config/task-types.yaml +111 -106
  45. package/context/agent-overrides/README.md +41 -0
  46. package/context/architecture.md +42 -0
  47. package/context/modern-conventions.md +129 -129
  48. package/docs/agents.md +473 -409
  49. package/docs/architecture.md +194 -162
  50. package/docs/commands.md +451 -388
  51. package/docs/security.md +195 -144
  52. package/package.json +38 -11
  53. package/src/lib/check-aliases.js +50 -0
  54. package/{bin → src}/lib/colors.sh +2 -1
  55. package/src/lib/config.sh +347 -0
  56. package/{bin → src}/lib/constants.sh +48 -13
  57. package/src/lib/daemon/budgets.sh +107 -0
  58. package/src/lib/daemon/dependencies.sh +146 -0
  59. package/src/lib/daemon/display.sh +128 -0
  60. package/src/lib/daemon/notifications.sh +273 -0
  61. package/src/lib/daemon/routing.sh +93 -0
  62. package/src/lib/daemon/state.sh +163 -0
  63. package/src/lib/daemon/sync.sh +103 -0
  64. package/{bin → src}/lib/database.sh +52 -0
  65. package/src/lib/frontmatter.js +106 -0
  66. package/src/lib/heimdall-setup.js +113 -0
  67. package/src/lib/heimdall.js +265 -0
  68. package/src/lib/index.sh +25 -0
  69. package/{bin → src}/lib/json.sh +7 -1
  70. package/{bin → src}/lib/terminal.js +7 -1
  71. package/.claude/settings.local.json +0 -33
  72. package/agents/forge-master/capabilities.md +0 -144
  73. package/agents/forge-master/context-template.md +0 -128
  74. package/agents/forge-master/personality.md +0 -138
  75. package/bin/lib/config.sh +0 -313
  76. package/config/task-template.md +0 -87
  77. package/context/forge-state.yaml +0 -19
  78. package/docs/TODO.md +0 -150
  79. package/docs/getting-started.md +0 -243
  80. package/docs/npm-publishing.md +0 -95
  81. package/docs/workflows/README.md +0 -32
  82. package/docs/workflows/azure-devops.md +0 -108
  83. package/docs/workflows/bitbucket.md +0 -104
  84. package/docs/workflows/git-only.md +0 -130
  85. package/docs/workflows/gitea.md +0 -168
  86. package/docs/workflows/github.md +0 -103
  87. package/docs/workflows/gitlab.md +0 -105
  88. package/docs/workflows.md +0 -454
  89. package/tasks/completed/ARCH-001-duplicate-agent-config.md +0 -121
  90. package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +0 -88
  91. package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +0 -77
  92. package/tasks/completed/ARCH-009-test-organization.md +0 -78
  93. package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +0 -94
  94. package/tasks/completed/ARCH-012-tmp-files-in-root.md +0 -71
  95. package/tasks/completed/ARCH-013-exit-code-constants.md +0 -65
  96. package/tasks/completed/ARCH-014-sed-incompatibility.md +0 -96
  97. package/tasks/completed/ARCH-015-docs-todo-tracking.md +0 -83
  98. package/tasks/completed/CLEAN-001.md +0 -38
  99. package/tasks/completed/CLEAN-003.md +0 -47
  100. package/tasks/completed/CLEAN-004.md +0 -56
  101. package/tasks/completed/CLEAN-005.md +0 -75
  102. package/tasks/completed/CLEAN-006.md +0 -47
  103. package/tasks/completed/CLEAN-007.md +0 -34
  104. package/tasks/completed/CLEAN-008.md +0 -49
  105. package/tasks/completed/CLEAN-012.md +0 -58
  106. package/tasks/completed/CLEAN-013.md +0 -45
  107. package/tasks/completed/SEC-001-sql-injection-fix.md +0 -58
  108. package/tasks/completed/SEC-002-notification-injection-fix.md +0 -45
  109. package/tasks/completed/SEC-003-eval-injection-fix.md +0 -54
  110. package/tasks/completed/SEC-004-pid-race-condition-fix.md +0 -49
  111. package/tasks/completed/SEC-005-worker-loop-path-fix.md +0 -51
  112. package/tasks/completed/SEC-006-eval-agent-names.md +0 -55
  113. package/tasks/completed/SEC-007-spawn-escaping.md +0 -67
  114. package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +0 -72
  115. package/tasks/pending/ARCH-005-missing-src-directory.md +0 -95
  116. package/tasks/pending/ARCH-006-task-template-location.md +0 -64
  117. package/tasks/pending/ARCH-007-daemon-monolith.md +0 -91
  118. package/tasks/pending/ARCH-008-forge-master-vs-hub.md +0 -81
  119. package/tasks/pending/ARCH-010-missing-index-files.md +0 -84
  120. package/tasks/pending/CLEAN-002.md +0 -29
  121. package/tasks/pending/CLEAN-009.md +0 -31
  122. package/tasks/pending/CLEAN-010.md +0 -30
  123. package/tasks/pending/CLEAN-011.md +0 -30
  124. package/tasks/pending/CLEAN-014.md +0 -32
  125. package/tasks/review/task-001.md +0 -78
  126. /package/{bin → src}/lib/agents.sh +0 -0
  127. /package/{bin → src}/lib/util.sh +0 -0
  128. /package/{bin → src}/lib/vcs.js +0 -0
  129. /package/{context → templates}/project-context-template.md +0 -0
@@ -0,0 +1,347 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Vibe Forge - Configuration Management
4
+ # Source this file in other scripts: source "$SCRIPT_DIR/lib/config.sh"
5
+ #
6
+ # SECURITY: This module provides safe JSON parsing without grep/cut vulnerabilities.
7
+ #
8
+
9
+ # Ensure colors are loaded for error messages
10
+ if ! type log_error &>/dev/null; then
11
+ echo "Error: colors.sh must be sourced before config.sh" >&2
12
+ exit 1
13
+ fi
14
+
15
+ # =============================================================================
16
+ # Agent Configuration Loading
17
+ # =============================================================================
18
+
19
+ # load_agents_from_json AGENTS_JSON_FILE
20
+ # Loads agent configuration from JSON file into shell variables.
21
+ # Sets: VALID_AGENTS array, AGENT_ALIASES associative array, AGENT_DISPLAY_NAMES
22
+ #
23
+ # SECURITY: Uses safe JSON parsing via Node.js
24
+ load_agents_from_json() {
25
+ local agents_file="$1"
26
+
27
+ if [[ ! -f "$agents_file" ]]; then
28
+ return 1
29
+ fi
30
+
31
+ if ! command -v node &>/dev/null; then
32
+ log_error "Node.js required for agent configuration"
33
+ return 1
34
+ fi
35
+
36
+ # Derive forge root from agents file location (agents_file is $FORGE_ROOT/config/agents.json)
37
+ local forge_root
38
+ forge_root="$(cd "$(dirname "$agents_file")/.." 2>/dev/null && pwd)"
39
+ local env_file="$forge_root/.forge/agents.env"
40
+
41
+ # Regenerate the static env file only when agents.json is newer or cache missing
42
+ # SECURITY: Writing to a file and sourcing it is auditable; eval of dynamic strings is not
43
+ if [[ ! -f "$env_file" || "$agents_file" -nt "$env_file" ]]; then
44
+ mkdir -p "$(dirname "$env_file")"
45
+
46
+ # Write header + validated shell assignments to the static cache file
47
+ # SECURITY: File path passed as argument, not interpolated into the script
48
+ # SECURITY: Agent names and aliases are validated to prevent shell injection
49
+ # NOTE: We output direct assignments (not declare -A) since arrays are pre-declared globally
50
+ {
51
+ printf '# AUTO-GENERATED by load_agents_from_json() -- DO NOT EDIT\n'
52
+ printf '# Source: config/agents.json\n'
53
+ printf '# Regenerated automatically when agents.json is newer than this file\n'
54
+ node -e '
55
+ const fs = require("fs");
56
+ const file = process.argv[1];
57
+
58
+ // SECURITY: Validate identifier contains only safe characters
59
+ // Allows: lowercase letters, numbers, underscore, hyphen
60
+ function isValidIdentifier(name) {
61
+ return /^[a-z0-9_-]+$/.test(name);
62
+ }
63
+
64
+ // SECURITY: Escape string for safe use in shell double-quoted string
65
+ // Escapes: $, `, ", \, newlines
66
+ function escapeForShell(str) {
67
+ if (typeof str !== "string") return "";
68
+ return str
69
+ .replace(/\\/g, "\\\\")
70
+ .replace(/"/g, "\\\"")
71
+ .replace(/\$/g, "\\$")
72
+ .replace(/`/g, "\\`")
73
+ .replace(/\n/g, "\\n")
74
+ .replace(/\r/g, "");
75
+ }
76
+
77
+ try {
78
+ const data = JSON.parse(fs.readFileSync(file, "utf8"));
79
+ const agents = data.agents || {};
80
+
81
+ // SECURITY: Validate all agent names before processing
82
+ for (const name of Object.keys(agents)) {
83
+ if (!isValidIdentifier(name)) {
84
+ console.error("SECURITY ERROR: Invalid agent name: " + name);
85
+ console.error("Agent names must contain only: a-z, 0-9, underscore, hyphen");
86
+ process.exit(1);
87
+ }
88
+ // Also validate aliases
89
+ const info = agents[name];
90
+ if (info.aliases) {
91
+ for (const alias of info.aliases) {
92
+ if (!isValidIdentifier(alias)) {
93
+ console.error("SECURITY ERROR: Invalid alias: " + alias);
94
+ console.error("Aliases must contain only: a-z, 0-9, underscore, hyphen");
95
+ process.exit(1);
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ // Output VALID_AGENTS array (names validated above)
102
+ const validAgents = Object.keys(agents);
103
+ console.log("VALID_AGENTS=(" + validAgents.map(a => `"${a}"`).join(" ") + ")");
104
+
105
+ // Output AGENT_ALIASES assignments (array already declared globally)
106
+ for (const [canonical, info] of Object.entries(agents)) {
107
+ // Add self-mapping
108
+ console.log(`AGENT_ALIASES["${canonical}"]="${canonical}"`);
109
+ // Add aliases (validated above)
110
+ if (info.aliases) {
111
+ for (const alias of info.aliases) {
112
+ console.log(`AGENT_ALIASES["${alias}"]="${canonical}"`);
113
+ }
114
+ }
115
+ }
116
+
117
+ // Output AGENT_DISPLAY_NAMES assignments
118
+ // SECURITY: Display names are escaped since they come from user input
119
+ for (const [canonical, info] of Object.entries(agents)) {
120
+ const displayName = escapeForShell(info.name || canonical);
121
+ console.log(`AGENT_DISPLAY_NAMES["${canonical}"]="${displayName}"`);
122
+ }
123
+
124
+ // Output AGENT_ROLES assignments
125
+ for (const [canonical, info] of Object.entries(agents)) {
126
+ const role = escapeForShell(info.role || "");
127
+ console.log(`AGENT_ROLES["${canonical}"]="${role}"`);
128
+ }
129
+
130
+ // Output AGENT_PERSONALITY_FILES assignments
131
+ for (const [canonical, info] of Object.entries(agents)) {
132
+ const pfile = escapeForShell(info.personality_file || "");
133
+ console.log(`AGENT_PERSONALITY_FILES["${canonical}"]="${pfile}"`);
134
+ }
135
+
136
+ // Output AGENT_ICONS assignments
137
+ for (const [canonical, info] of Object.entries(agents)) {
138
+ const icon = escapeForShell(info.icon || "");
139
+ console.log(`AGENT_ICONS["${canonical}"]="${icon}"`);
140
+ }
141
+
142
+ // Output AGENT_TAB_COLORS assignments
143
+ for (const [canonical, info] of Object.entries(agents)) {
144
+ const tabColor = escapeForShell(info.tab_color || "");
145
+ console.log(`AGENT_TAB_COLORS["${canonical}"]="${tabColor}"`);
146
+ }
147
+
148
+ } catch (e) {
149
+ console.error("Error parsing agents.json:", e.message);
150
+ process.exit(1);
151
+ }
152
+ ' -- "$agents_file" 2>/dev/null
153
+ } > "$env_file" || { rm -f "$env_file"; return 1; }
154
+ fi
155
+
156
+ # Source the static cache file instead of eval-ing dynamic Node.js output
157
+ # SECURITY: Auditable static file; Node.js only runs when agents.json changes
158
+ # shellcheck source=/dev/null
159
+ if ! source "$env_file"; then
160
+ # Corrupted or invalid cache file — remove it so it regenerates on next call
161
+ rm -f "$env_file"
162
+ return 1
163
+ fi
164
+
165
+ # Mark as loaded
166
+ AGENTS_LOADED="true"
167
+ return 0
168
+ }
169
+
170
+ # json_get_string FILE KEY
171
+ # Safely extracts a string value from a JSON file.
172
+ # Uses node.js for safe parsing (available since we require Node 16+)
173
+ #
174
+ # SECURITY: This avoids grep/cut vulnerabilities by using proper JSON parsing.
175
+ # SECURITY: File and key are passed as command-line arguments, not interpolated.
176
+ json_get_string() {
177
+ local file="$1"
178
+ local key="$2"
179
+
180
+ if [[ ! -f "$file" ]]; then
181
+ return 1
182
+ fi
183
+
184
+ # Use Node.js for safe JSON parsing
185
+ # SECURITY: Pass file and key as arguments to avoid injection
186
+ if command -v node &>/dev/null; then
187
+ node -e '
188
+ const fs = require("fs");
189
+ const file = process.argv[1];
190
+ const key = process.argv[2];
191
+ try {
192
+ const data = JSON.parse(fs.readFileSync(file, "utf8"));
193
+ const value = data[key];
194
+ if (value !== undefined && value !== null) {
195
+ console.log(String(value));
196
+ }
197
+ } catch (e) {
198
+ process.exit(1);
199
+ }
200
+ ' -- "$file" "$key" 2>/dev/null
201
+ return $?
202
+ fi
203
+
204
+ # Fallback: Use Python if available
205
+ # SECURITY: Pass file and key as arguments to avoid injection
206
+ if command -v python3 &>/dev/null; then
207
+ python3 -c '
208
+ import json, sys
209
+ try:
210
+ file_path = sys.argv[1]
211
+ key = sys.argv[2]
212
+ with open(file_path) as f:
213
+ data = json.load(f)
214
+ value = data.get(key)
215
+ if value is not None:
216
+ print(str(value))
217
+ except:
218
+ sys.exit(1)
219
+ ' "$file" "$key" 2>/dev/null
220
+ return $?
221
+ fi
222
+
223
+ # No safe parser available - exit with error
224
+ log_error "No JSON parser available. Install Node.js or Python 3."
225
+ return 1
226
+ }
227
+
228
+ # load_forge_config CONFIG_FILE
229
+ # Loads configuration from the forge config file into environment variables.
230
+ # Sets: PLATFORM, GIT_BASH_PATH, TERMINAL_TYPE, FORGE_VALIDATED
231
+ #
232
+ # Returns: 0 on success, 1 on failure
233
+ load_forge_config() {
234
+ local config_file="$1"
235
+
236
+ if [[ ! -f "$config_file" ]]; then
237
+ log_error "Vibe Forge not initialized."
238
+ echo "Run 'forge init' first." >&2
239
+ return 1
240
+ fi
241
+
242
+ # Load config values safely
243
+ PLATFORM=$(json_get_string "$config_file" "platform") || PLATFORM=""
244
+ GIT_BASH_PATH=$(json_get_string "$config_file" "git_bash_path") || GIT_BASH_PATH=""
245
+ TERMINAL_TYPE=$(json_get_string "$config_file" "terminal_type") || TERMINAL_TYPE="manual"
246
+ FORGE_VALIDATED=$(json_get_string "$config_file" "validated") || FORGE_VALIDATED="false"
247
+
248
+ # Validate required fields
249
+ if [[ -z "$PLATFORM" ]]; then
250
+ log_error "Invalid config: missing platform"
251
+ return 1
252
+ fi
253
+
254
+ return 0
255
+ }
256
+
257
+ # setup_windows_env
258
+ # Sets up Windows-specific environment variables and PATH.
259
+ # Call this after load_forge_config on Windows.
260
+ setup_windows_env() {
261
+ if [[ "$PLATFORM" != "windows" ]]; then
262
+ return 0
263
+ fi
264
+
265
+ # Export Git Bash path for Claude Code
266
+ if [[ -n "$GIT_BASH_PATH" ]]; then
267
+ # Convert forward slashes to backslashes for Windows
268
+ local git_bash_win="${GIT_BASH_PATH//\//\\}"
269
+ export CLAUDE_CODE_GIT_BASH_PATH="$git_bash_win"
270
+ fi
271
+
272
+ # Add npm global path if not already in PATH
273
+ local npm_path=""
274
+
275
+ # Try with USER variable
276
+ if [[ -n "$USER" ]]; then
277
+ npm_path="/c/Users/$USER/AppData/Roaming/npm"
278
+ fi
279
+
280
+ # Try with USERPROFILE
281
+ if [[ -z "$npm_path" || ! -d "$npm_path" ]] && [[ -n "$USERPROFILE" ]]; then
282
+ npm_path="${USERPROFILE//\\//}/AppData/Roaming/npm"
283
+ fi
284
+
285
+ # Add to PATH if exists and not already there
286
+ if [[ -n "$npm_path" && -d "$npm_path" && ":$PATH:" != *":$npm_path:"* ]]; then
287
+ export PATH="$npm_path:$PATH"
288
+ fi
289
+ }
290
+
291
+ # require_forge_config FORGE_ROOT
292
+ # Loads config and exits with error if not initialized.
293
+ # Also applies local overrides from .forge/config.local.json if present.
294
+ # config.local.json is gitignored and safe for per-developer settings
295
+ # (e.g. custom terminal path, personal daemon preferences).
296
+ require_forge_config() {
297
+ local forge_root="$1"
298
+ local config_file="$forge_root/.forge/config.json"
299
+ local local_config_file="$forge_root/.forge/config.local.json"
300
+
301
+ load_forge_config "$config_file" || exit 1
302
+
303
+ # Apply local overrides if present (not committed, per-developer)
304
+ if [[ -f "$local_config_file" ]]; then
305
+ local local_terminal local_git_bash local_daemon local_loop
306
+ local_terminal=$(json_get_string "$local_config_file" "terminal_type") && TERMINAL_TYPE="$local_terminal"
307
+ local_git_bash=$(json_get_string "$local_config_file" "git_bash_path") && GIT_BASH_PATH="$local_git_bash"
308
+ local_daemon=$(json_get_string "$local_config_file" "daemon_enabled") && DAEMON_ENABLED="$local_daemon"
309
+ local_loop=$(json_get_string "$local_config_file" "worker_loop_enabled") && WORKER_LOOP_ENABLED="$local_loop"
310
+ fi
311
+
312
+ setup_windows_env
313
+ }
314
+
315
+ # write_json_config FILE KEY VALUE
316
+ # Safely writes/updates a key in a JSON config file.
317
+ # Creates file if it doesn't exist.
318
+ #
319
+ # SECURITY: File, key, and value are passed as command-line arguments, not interpolated.
320
+ write_json_config() {
321
+ local file="$1"
322
+ local key="$2"
323
+ local value="$3"
324
+
325
+ # Use Node.js for safe JSON manipulation
326
+ # SECURITY: Pass all values as arguments to avoid injection
327
+ if command -v node &>/dev/null; then
328
+ node -e '
329
+ const fs = require("fs");
330
+ const file = process.argv[1];
331
+ const key = process.argv[2];
332
+ const value = process.argv[3];
333
+ let data = {};
334
+ try {
335
+ if (fs.existsSync(file)) {
336
+ data = JSON.parse(fs.readFileSync(file, "utf8"));
337
+ }
338
+ } catch (e) {}
339
+ data[key] = value;
340
+ fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n");
341
+ ' -- "$file" "$key" "$value" 2>/dev/null
342
+ return $?
343
+ fi
344
+
345
+ log_error "Node.js required for config writing"
346
+ return 1
347
+ }
@@ -63,10 +63,10 @@ AGENTS_LOADED="${AGENTS_LOADED:-false}"
63
63
  # Valid agent names (whitelist for security)
64
64
  # These are the ONLY valid canonical agent names
65
65
  # NOTE: Must stay in sync with config/agents.json (which is the source of truth)
66
- # Last sync: 2026-01-16
66
+ # Last sync: 2026-04-03 (added loki, renamed sentinel→temper)
67
67
  VALID_AGENTS=(
68
68
  "hub"
69
- "sentinel"
69
+ "temper"
70
70
  "anvil"
71
71
  "furnace"
72
72
  "crucible"
@@ -75,6 +75,9 @@ VALID_AGENTS=(
75
75
  "ember"
76
76
  "aegis"
77
77
  "architect"
78
+ "pixel"
79
+ "oracle"
80
+ "loki"
78
81
  )
79
82
 
80
83
  # Agent aliases map (for resolve_agent)
@@ -86,11 +89,11 @@ declare -A AGENT_ALIASES=(
86
89
  ["planning"]="hub"
87
90
  ["master"]="hub"
88
91
  ["forge-master"]="hub"
89
- # Sentinel aliases (Code Reviewer)
90
- ["sentinel"]="sentinel"
91
- ["review"]="sentinel"
92
- ["reviewer"]="sentinel"
93
- ["cr"]="sentinel"
92
+ # Temper aliases (Code Reviewer)
93
+ ["temper"]="temper"
94
+ ["review"]="temper"
95
+ ["reviewer"]="temper"
96
+ ["cr"]="temper"
94
97
  # Anvil aliases (Frontend)
95
98
  ["anvil"]="anvil"
96
99
  ["frontend"]="anvil"
@@ -131,14 +134,31 @@ declare -A AGENT_ALIASES=(
131
134
  # Architect aliases (System Design)
132
135
  ["architect"]="architect"
133
136
  ["arch"]="architect"
134
- ["design"]="architect"
135
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"
136
156
  )
137
157
 
138
158
  # Agent display names
139
159
  declare -A AGENT_DISPLAY_NAMES=(
140
160
  ["hub"]="Planning Hub"
141
- ["sentinel"]="Sentinel"
161
+ ["temper"]="Temper"
142
162
  ["anvil"]="Anvil"
143
163
  ["furnace"]="Furnace"
144
164
  ["crucible"]="Crucible"
@@ -147,12 +167,15 @@ declare -A AGENT_DISPLAY_NAMES=(
147
167
  ["ember"]="Ember"
148
168
  ["aegis"]="Aegis"
149
169
  ["architect"]="Architect"
170
+ ["pixel"]="Pixel"
171
+ ["oracle"]="Oracle"
172
+ ["loki"]="Loki"
150
173
  )
151
174
 
152
175
  # Agent roles
153
176
  declare -A AGENT_ROLES=(
154
177
  ["hub"]="Chief Orchestrator"
155
- ["sentinel"]="Code Reviewer"
178
+ ["temper"]="Code Reviewer"
156
179
  ["anvil"]="Frontend Developer"
157
180
  ["furnace"]="Backend Developer"
158
181
  ["crucible"]="Tester / QA"
@@ -161,12 +184,15 @@ declare -A AGENT_ROLES=(
161
184
  ["ember"]="DevOps Engineer"
162
185
  ["aegis"]="Security Specialist"
163
186
  ["architect"]="System Architect"
187
+ ["pixel"]="UX Designer"
188
+ ["oracle"]="Product Owner / Requirements Analyst"
189
+ ["loki"]="Lateral Thinker"
164
190
  )
165
191
 
166
192
  # Agent personality files (relative to FORGE_ROOT)
167
193
  declare -A AGENT_PERSONALITY_FILES=(
168
194
  ["hub"]="agents/planning-hub/personality.md"
169
- ["sentinel"]="agents/sentinel/personality.md"
195
+ ["temper"]="agents/temper/personality.md"
170
196
  ["anvil"]="agents/anvil/personality.md"
171
197
  ["furnace"]="agents/furnace/personality.md"
172
198
  ["crucible"]="agents/crucible/personality.md"
@@ -175,12 +201,15 @@ declare -A AGENT_PERSONALITY_FILES=(
175
201
  ["ember"]="agents/ember/personality.md"
176
202
  ["aegis"]="agents/aegis/personality.md"
177
203
  ["architect"]="agents/architect/personality.md"
204
+ ["pixel"]="agents/pixel/personality.md"
205
+ ["oracle"]="agents/oracle/personality.md"
206
+ ["loki"]="agents/loki/personality.md"
178
207
  )
179
208
 
180
209
  # Agent icons
181
210
  declare -A AGENT_ICONS=(
182
211
  ["hub"]="⚒️"
183
- ["sentinel"]="🛡️"
212
+ ["temper"]="⚖️"
184
213
  ["anvil"]="🔨"
185
214
  ["furnace"]="🔥"
186
215
  ["crucible"]="🧪"
@@ -189,12 +218,15 @@ declare -A AGENT_ICONS=(
189
218
  ["ember"]="⚙️"
190
219
  ["aegis"]="🔒"
191
220
  ["architect"]="🏛️"
221
+ ["pixel"]="🎨"
222
+ ["oracle"]="🔮"
223
+ ["loki"]="🎭"
192
224
  )
193
225
 
194
226
  # Agent tab colors for Windows Terminal (hex format)
195
227
  declare -A AGENT_TAB_COLORS=(
196
228
  ["hub"]="#FF6B35"
197
- ["sentinel"]="#8B5CF6"
229
+ ["temper"]="#8B5CF6"
198
230
  ["anvil"]="#3B82F6"
199
231
  ["furnace"]="#EF4444"
200
232
  ["crucible"]="#10B981"
@@ -203,4 +235,7 @@ declare -A AGENT_TAB_COLORS=(
203
235
  ["ember"]="#F97316"
204
236
  ["aegis"]="#06B6D4"
205
237
  ["architect"]="#6366F1"
238
+ ["pixel"]="#D946EF"
239
+ ["oracle"]="#FBBF24"
240
+ ["loki"]="#7C3AED"
206
241
  )
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # src/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
+ }