vibe-forge 0.4.0 → 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 (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 +102 -102
  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 -187
  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 -232
  13. package/agents/aegis/personality.md +303 -269
  14. package/agents/anvil/personality.md +278 -240
  15. package/agents/architect/personality.md +260 -234
  16. package/agents/crucible/personality.md +362 -309
  17. package/agents/crucible-x/personality.md +210 -0
  18. package/agents/ember/personality.md +293 -265
  19. package/agents/flux/personality.md +248 -0
  20. package/agents/furnace/personality.md +342 -291
  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 -251
  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 -851
  38. package/bin/forge-setup.sh +661 -645
  39. package/bin/forge-spawn.sh +164 -164
  40. package/bin/forge.cmd +83 -83
  41. package/bin/forge.sh +566 -387
  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 -313
  46. package/bin/lib/constants.sh +241 -206
  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 -305
  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 -258
  59. package/bin/lib/terminal.js +452 -446
  60. package/bin/lib/util.sh +126 -126
  61. package/bin/lib/vcs.js +349 -349
  62. package/config/agent-manifest.yaml +237 -243
  63. package/config/agents.json +207 -132
  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 -409
  72. package/docs/architecture.md +194 -162
  73. package/docs/commands.md +451 -388
  74. package/docs/security.md +195 -144
  75. package/package.json +77 -50
  76. package/.claude/settings.local.json +0 -33
  77. package/agents/forge-master/capabilities.md +0 -144
  78. package/agents/forge-master/context-template.md +0 -128
  79. package/agents/forge-master/personality.md +0 -138
  80. package/agents/sentinel/personality.md +0 -194
  81. package/context/forge-state.yaml +0 -19
  82. package/docs/TODO.md +0 -150
  83. package/docs/getting-started.md +0 -243
  84. package/docs/npm-publishing.md +0 -95
  85. package/docs/workflows/README.md +0 -32
  86. package/docs/workflows/azure-devops.md +0 -108
  87. package/docs/workflows/bitbucket.md +0 -104
  88. package/docs/workflows/git-only.md +0 -130
  89. package/docs/workflows/gitea.md +0 -168
  90. package/docs/workflows/github.md +0 -103
  91. package/docs/workflows/gitlab.md +0 -105
  92. package/docs/workflows.md +0 -454
  93. package/tasks/completed/ARCH-001-duplicate-agent-config.md +0 -121
  94. package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +0 -88
  95. package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +0 -77
  96. package/tasks/completed/ARCH-009-test-organization.md +0 -78
  97. package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +0 -94
  98. package/tasks/completed/ARCH-012-tmp-files-in-root.md +0 -71
  99. package/tasks/completed/ARCH-013-exit-code-constants.md +0 -65
  100. package/tasks/completed/ARCH-014-sed-incompatibility.md +0 -96
  101. package/tasks/completed/ARCH-015-docs-todo-tracking.md +0 -83
  102. package/tasks/completed/CLEAN-001.md +0 -38
  103. package/tasks/completed/CLEAN-003.md +0 -47
  104. package/tasks/completed/CLEAN-004.md +0 -56
  105. package/tasks/completed/CLEAN-005.md +0 -75
  106. package/tasks/completed/CLEAN-006.md +0 -47
  107. package/tasks/completed/CLEAN-007.md +0 -34
  108. package/tasks/completed/CLEAN-008.md +0 -49
  109. package/tasks/completed/CLEAN-012.md +0 -58
  110. package/tasks/completed/CLEAN-013.md +0 -45
  111. package/tasks/completed/SEC-001-sql-injection-fix.md +0 -58
  112. package/tasks/completed/SEC-002-notification-injection-fix.md +0 -45
  113. package/tasks/completed/SEC-003-eval-injection-fix.md +0 -54
  114. package/tasks/completed/SEC-004-pid-race-condition-fix.md +0 -49
  115. package/tasks/completed/SEC-005-worker-loop-path-fix.md +0 -51
  116. package/tasks/completed/SEC-006-eval-agent-names.md +0 -55
  117. package/tasks/completed/SEC-007-spawn-escaping.md +0 -67
  118. package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +0 -72
  119. package/tasks/pending/ARCH-005-missing-src-directory.md +0 -95
  120. package/tasks/pending/ARCH-006-task-template-location.md +0 -64
  121. package/tasks/pending/ARCH-007-daemon-monolith.md +0 -91
  122. package/tasks/pending/ARCH-008-forge-master-vs-hub.md +0 -81
  123. package/tasks/pending/ARCH-010-missing-index-files.md +0 -84
  124. package/tasks/pending/CLEAN-002.md +0 -29
  125. package/tasks/pending/CLEAN-009.md +0 -31
  126. package/tasks/pending/CLEAN-010.md +0 -30
  127. package/tasks/pending/CLEAN-011.md +0 -30
  128. package/tasks/pending/CLEAN-014.md +0 -32
  129. package/tasks/review/task-001.md +0 -78
package/bin/lib/config.sh CHANGED
@@ -1,313 +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
- # Parse agents JSON and output shell variable assignments
37
- # SECURITY: File path passed as argument, not interpolated
38
- # SECURITY: Agent names and aliases are validated to prevent shell injection
39
- # NOTE: We output direct assignments (not declare -A) since arrays are pre-declared globally
40
- local agent_data
41
- agent_data=$(node -e '
42
- const fs = require("fs");
43
- const file = process.argv[1];
44
-
45
- // SECURITY: Validate identifier contains only safe characters
46
- // Allows: lowercase letters, numbers, underscore, hyphen
47
- function isValidIdentifier(name) {
48
- return /^[a-z0-9_-]+$/.test(name);
49
- }
50
-
51
- // SECURITY: Escape string for safe use in shell double-quoted string
52
- // Escapes: $, `, ", \, newlines
53
- function escapeForShell(str) {
54
- if (typeof str !== "string") return "";
55
- return str
56
- .replace(/\\/g, "\\\\")
57
- .replace(/"/g, "\\\"")
58
- .replace(/\$/g, "\\$")
59
- .replace(/`/g, "\\`")
60
- .replace(/\n/g, "\\n")
61
- .replace(/\r/g, "");
62
- }
63
-
64
- try {
65
- const data = JSON.parse(fs.readFileSync(file, "utf8"));
66
- const agents = data.agents || {};
67
-
68
- // SECURITY: Validate all agent names before processing
69
- for (const name of Object.keys(agents)) {
70
- if (!isValidIdentifier(name)) {
71
- console.error("SECURITY ERROR: Invalid agent name: " + name);
72
- console.error("Agent names must contain only: a-z, 0-9, underscore, hyphen");
73
- process.exit(1);
74
- }
75
- // Also validate aliases
76
- const info = agents[name];
77
- if (info.aliases) {
78
- for (const alias of info.aliases) {
79
- if (!isValidIdentifier(alias)) {
80
- console.error("SECURITY ERROR: Invalid alias: " + alias);
81
- console.error("Aliases must contain only: a-z, 0-9, underscore, hyphen");
82
- process.exit(1);
83
- }
84
- }
85
- }
86
- }
87
-
88
- // Output VALID_AGENTS array (names validated above)
89
- const validAgents = Object.keys(agents);
90
- console.log("VALID_AGENTS=(" + validAgents.map(a => `"${a}"`).join(" ") + ")");
91
-
92
- // Output AGENT_ALIASES assignments (array already declared globally)
93
- for (const [canonical, info] of Object.entries(agents)) {
94
- // Add self-mapping
95
- console.log(`AGENT_ALIASES["${canonical}"]="${canonical}"`);
96
- // Add aliases (validated above)
97
- if (info.aliases) {
98
- for (const alias of info.aliases) {
99
- console.log(`AGENT_ALIASES["${alias}"]="${canonical}"`);
100
- }
101
- }
102
- }
103
-
104
- // Output AGENT_DISPLAY_NAMES assignments
105
- // SECURITY: Display names are escaped since they come from user input
106
- for (const [canonical, info] of Object.entries(agents)) {
107
- const displayName = escapeForShell(info.name || canonical);
108
- console.log(`AGENT_DISPLAY_NAMES["${canonical}"]="${displayName}"`);
109
- }
110
-
111
- // Output AGENT_ROLES assignments
112
- for (const [canonical, info] of Object.entries(agents)) {
113
- const role = escapeForShell(info.role || "");
114
- console.log(`AGENT_ROLES["${canonical}"]="${role}"`);
115
- }
116
-
117
- // Output AGENT_PERSONALITY_FILES assignments
118
- for (const [canonical, info] of Object.entries(agents)) {
119
- const pfile = escapeForShell(info.personality_file || "");
120
- console.log(`AGENT_PERSONALITY_FILES["${canonical}"]="${pfile}"`);
121
- }
122
-
123
- // Output AGENT_ICONS assignments
124
- for (const [canonical, info] of Object.entries(agents)) {
125
- const icon = escapeForShell(info.icon || "");
126
- console.log(`AGENT_ICONS["${canonical}"]="${icon}"`);
127
- }
128
-
129
- // Output AGENT_TAB_COLORS assignments
130
- for (const [canonical, info] of Object.entries(agents)) {
131
- const tabColor = escapeForShell(info.tab_color || "");
132
- console.log(`AGENT_TAB_COLORS["${canonical}"]="${tabColor}"`);
133
- }
134
-
135
- } catch (e) {
136
- console.error("Error parsing agents.json:", e.message);
137
- process.exit(1);
138
- }
139
- ' -- "$agents_file" 2>/dev/null) || return 1
140
-
141
- # Evaluate the output to set variables
142
- eval "$agent_data"
143
-
144
- # Mark as loaded
145
- AGENTS_LOADED="true"
146
- return 0
147
- }
148
-
149
- # json_get_string FILE KEY
150
- # Safely extracts a string value from a JSON file.
151
- # Uses node.js for safe parsing (available since we require Node 16+)
152
- #
153
- # SECURITY: This avoids grep/cut vulnerabilities by using proper JSON parsing.
154
- # SECURITY: File and key are passed as command-line arguments, not interpolated.
155
- json_get_string() {
156
- local file="$1"
157
- local key="$2"
158
-
159
- if [[ ! -f "$file" ]]; then
160
- return 1
161
- fi
162
-
163
- # Use Node.js for safe JSON parsing
164
- # SECURITY: Pass file and key as arguments to avoid injection
165
- if command -v node &>/dev/null; then
166
- node -e '
167
- const fs = require("fs");
168
- const file = process.argv[1];
169
- const key = process.argv[2];
170
- try {
171
- const data = JSON.parse(fs.readFileSync(file, "utf8"));
172
- const value = data[key];
173
- if (value !== undefined && value !== null) {
174
- console.log(String(value));
175
- }
176
- } catch (e) {
177
- process.exit(1);
178
- }
179
- ' -- "$file" "$key" 2>/dev/null
180
- return $?
181
- fi
182
-
183
- # Fallback: Use Python if available
184
- # SECURITY: Pass file and key as arguments to avoid injection
185
- if command -v python3 &>/dev/null; then
186
- python3 -c '
187
- import json, sys
188
- try:
189
- file_path = sys.argv[1]
190
- key = sys.argv[2]
191
- with open(file_path) as f:
192
- data = json.load(f)
193
- value = data.get(key)
194
- if value is not None:
195
- print(str(value))
196
- except:
197
- sys.exit(1)
198
- ' "$file" "$key" 2>/dev/null
199
- return $?
200
- fi
201
-
202
- # No safe parser available - exit with error
203
- log_error "No JSON parser available. Install Node.js or Python 3."
204
- return 1
205
- }
206
-
207
- # load_forge_config CONFIG_FILE
208
- # Loads configuration from the forge config file into environment variables.
209
- # Sets: PLATFORM, GIT_BASH_PATH, TERMINAL_TYPE, FORGE_VALIDATED
210
- #
211
- # Returns: 0 on success, 1 on failure
212
- load_forge_config() {
213
- local config_file="$1"
214
-
215
- if [[ ! -f "$config_file" ]]; then
216
- log_error "Vibe Forge not initialized."
217
- echo "Run 'forge init' first." >&2
218
- return 1
219
- fi
220
-
221
- # Load config values safely
222
- PLATFORM=$(json_get_string "$config_file" "platform") || PLATFORM=""
223
- GIT_BASH_PATH=$(json_get_string "$config_file" "git_bash_path") || GIT_BASH_PATH=""
224
- TERMINAL_TYPE=$(json_get_string "$config_file" "terminal_type") || TERMINAL_TYPE="manual"
225
- FORGE_VALIDATED=$(json_get_string "$config_file" "validated") || FORGE_VALIDATED="false"
226
-
227
- # Validate required fields
228
- if [[ -z "$PLATFORM" ]]; then
229
- log_error "Invalid config: missing platform"
230
- return 1
231
- fi
232
-
233
- return 0
234
- }
235
-
236
- # setup_windows_env
237
- # Sets up Windows-specific environment variables and PATH.
238
- # Call this after load_forge_config on Windows.
239
- setup_windows_env() {
240
- if [[ "$PLATFORM" != "windows" ]]; then
241
- return 0
242
- fi
243
-
244
- # Export Git Bash path for Claude Code
245
- if [[ -n "$GIT_BASH_PATH" ]]; then
246
- # Convert forward slashes to backslashes for Windows
247
- local git_bash_win="${GIT_BASH_PATH//\//\\}"
248
- export CLAUDE_CODE_GIT_BASH_PATH="$git_bash_win"
249
- fi
250
-
251
- # Add npm global path if not already in PATH
252
- local npm_path=""
253
-
254
- # Try with USER variable
255
- if [[ -n "$USER" ]]; then
256
- npm_path="/c/Users/$USER/AppData/Roaming/npm"
257
- fi
258
-
259
- # Try with USERPROFILE
260
- if [[ -z "$npm_path" || ! -d "$npm_path" ]] && [[ -n "$USERPROFILE" ]]; then
261
- npm_path="${USERPROFILE//\\//}/AppData/Roaming/npm"
262
- fi
263
-
264
- # Add to PATH if exists and not already there
265
- if [[ -n "$npm_path" && -d "$npm_path" && ":$PATH:" != *":$npm_path:"* ]]; then
266
- export PATH="$npm_path:$PATH"
267
- fi
268
- }
269
-
270
- # require_forge_config FORGE_ROOT
271
- # Loads config and exits with error if not initialized.
272
- # Convenience function that combines load + validation.
273
- require_forge_config() {
274
- local forge_root="$1"
275
- local config_file="$forge_root/.forge/config.json"
276
-
277
- load_forge_config "$config_file" || exit 1
278
- setup_windows_env
279
- }
280
-
281
- # write_json_config FILE KEY VALUE
282
- # Safely writes/updates a key in a JSON config file.
283
- # Creates file if it doesn't exist.
284
- #
285
- # SECURITY: File, key, and value are passed as command-line arguments, not interpolated.
286
- write_json_config() {
287
- local file="$1"
288
- local key="$2"
289
- local value="$3"
290
-
291
- # Use Node.js for safe JSON manipulation
292
- # SECURITY: Pass all values as arguments to avoid injection
293
- if command -v node &>/dev/null; then
294
- node -e '
295
- const fs = require("fs");
296
- const file = process.argv[1];
297
- const key = process.argv[2];
298
- const value = process.argv[3];
299
- let data = {};
300
- try {
301
- if (fs.existsSync(file)) {
302
- data = JSON.parse(fs.readFileSync(file, "utf8"));
303
- }
304
- } catch (e) {}
305
- data[key] = value;
306
- fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n");
307
- ' -- "$file" "$key" "$value" 2>/dev/null
308
- return $?
309
- fi
310
-
311
- log_error "Node.js required for config writing"
312
- return 1
313
- }
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
+ }