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.
- package/.claude/commands/clear-attention.md +63 -63
- package/.claude/commands/compact-context.md +52 -0
- package/.claude/commands/configure-vcs.md +5 -5
- package/.claude/commands/forge.md +50 -3
- package/.claude/commands/need-help.md +77 -77
- package/.claude/commands/update-status.md +64 -64
- package/.claude/commands/worker-loop.md +106 -106
- package/.claude/hooks/worker-loop.js +37 -4
- package/.claude/scripts/setup-worker-loop.sh +45 -45
- package/.claude/settings.json +89 -0
- package/LICENSE +21 -21
- package/README.md +211 -232
- package/agents/aegis/personality.md +35 -1
- package/agents/anvil/personality.md +39 -1
- package/agents/architect/personality.md +26 -0
- package/agents/crucible/personality.md +54 -1
- package/agents/crucible-x/personality.md +210 -0
- package/agents/ember/personality.md +29 -1
- package/agents/flux/personality.md +248 -0
- package/agents/furnace/personality.md +52 -1
- package/agents/herald/personality.md +3 -1
- package/agents/loki/personality.md +108 -0
- package/agents/oracle/personality.md +284 -0
- package/agents/pixel/personality.md +140 -0
- package/agents/planning-hub/personality.md +222 -0
- package/agents/scribe/personality.md +3 -1
- package/agents/slag/personality.md +268 -0
- package/agents/{sentinel → temper}/personality.md +85 -9
- package/bin/cli.js +77 -30
- package/bin/dashboard/api/agents.js +333 -0
- package/bin/dashboard/api/dispatch.js +507 -0
- package/bin/dashboard/api/tasks.js +416 -0
- package/bin/dashboard/public/assets/index-BpHfsx1r.js +2 -0
- package/bin/dashboard/public/assets/index-QODv4Zn9.css +1 -0
- package/bin/dashboard/public/index.html +14 -0
- package/bin/dashboard/server.js +645 -0
- package/bin/forge-daemon.sh +176 -550
- package/bin/forge-setup.sh +28 -11
- package/bin/forge-spawn.sh +5 -5
- package/bin/forge.cmd +83 -83
- package/bin/forge.sh +210 -31
- package/config/agent-manifest.yaml +237 -243
- package/config/agents.json +207 -132
- package/config/task-types.yaml +111 -106
- package/context/agent-overrides/README.md +41 -0
- package/context/architecture.md +42 -0
- package/context/modern-conventions.md +129 -129
- package/docs/agents.md +473 -409
- package/docs/architecture.md +194 -162
- package/docs/commands.md +451 -388
- package/docs/security.md +195 -144
- package/package.json +38 -11
- package/src/lib/check-aliases.js +50 -0
- package/{bin → src}/lib/colors.sh +2 -1
- package/src/lib/config.sh +347 -0
- package/{bin → src}/lib/constants.sh +48 -13
- package/src/lib/daemon/budgets.sh +107 -0
- package/src/lib/daemon/dependencies.sh +146 -0
- package/src/lib/daemon/display.sh +128 -0
- package/src/lib/daemon/notifications.sh +273 -0
- package/src/lib/daemon/routing.sh +93 -0
- package/src/lib/daemon/state.sh +163 -0
- package/src/lib/daemon/sync.sh +103 -0
- package/{bin → src}/lib/database.sh +52 -0
- package/src/lib/frontmatter.js +106 -0
- package/src/lib/heimdall-setup.js +113 -0
- package/src/lib/heimdall.js +265 -0
- package/src/lib/index.sh +25 -0
- package/{bin → src}/lib/json.sh +7 -1
- package/{bin → src}/lib/terminal.js +7 -1
- package/.claude/settings.local.json +0 -33
- package/agents/forge-master/capabilities.md +0 -144
- package/agents/forge-master/context-template.md +0 -128
- package/agents/forge-master/personality.md +0 -138
- package/bin/lib/config.sh +0 -313
- package/config/task-template.md +0 -87
- package/context/forge-state.yaml +0 -19
- package/docs/TODO.md +0 -150
- package/docs/getting-started.md +0 -243
- package/docs/npm-publishing.md +0 -95
- package/docs/workflows/README.md +0 -32
- package/docs/workflows/azure-devops.md +0 -108
- package/docs/workflows/bitbucket.md +0 -104
- package/docs/workflows/git-only.md +0 -130
- package/docs/workflows/gitea.md +0 -168
- package/docs/workflows/github.md +0 -103
- package/docs/workflows/gitlab.md +0 -105
- package/docs/workflows.md +0 -454
- package/tasks/completed/ARCH-001-duplicate-agent-config.md +0 -121
- package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +0 -88
- package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +0 -77
- package/tasks/completed/ARCH-009-test-organization.md +0 -78
- package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +0 -94
- package/tasks/completed/ARCH-012-tmp-files-in-root.md +0 -71
- package/tasks/completed/ARCH-013-exit-code-constants.md +0 -65
- package/tasks/completed/ARCH-014-sed-incompatibility.md +0 -96
- package/tasks/completed/ARCH-015-docs-todo-tracking.md +0 -83
- package/tasks/completed/CLEAN-001.md +0 -38
- package/tasks/completed/CLEAN-003.md +0 -47
- package/tasks/completed/CLEAN-004.md +0 -56
- package/tasks/completed/CLEAN-005.md +0 -75
- package/tasks/completed/CLEAN-006.md +0 -47
- package/tasks/completed/CLEAN-007.md +0 -34
- package/tasks/completed/CLEAN-008.md +0 -49
- package/tasks/completed/CLEAN-012.md +0 -58
- package/tasks/completed/CLEAN-013.md +0 -45
- package/tasks/completed/SEC-001-sql-injection-fix.md +0 -58
- package/tasks/completed/SEC-002-notification-injection-fix.md +0 -45
- package/tasks/completed/SEC-003-eval-injection-fix.md +0 -54
- package/tasks/completed/SEC-004-pid-race-condition-fix.md +0 -49
- package/tasks/completed/SEC-005-worker-loop-path-fix.md +0 -51
- package/tasks/completed/SEC-006-eval-agent-names.md +0 -55
- package/tasks/completed/SEC-007-spawn-escaping.md +0 -67
- package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +0 -72
- package/tasks/pending/ARCH-005-missing-src-directory.md +0 -95
- package/tasks/pending/ARCH-006-task-template-location.md +0 -64
- package/tasks/pending/ARCH-007-daemon-monolith.md +0 -91
- package/tasks/pending/ARCH-008-forge-master-vs-hub.md +0 -81
- package/tasks/pending/ARCH-010-missing-index-files.md +0 -84
- package/tasks/pending/CLEAN-002.md +0 -29
- package/tasks/pending/CLEAN-009.md +0 -31
- package/tasks/pending/CLEAN-010.md +0 -30
- package/tasks/pending/CLEAN-011.md +0 -30
- package/tasks/pending/CLEAN-014.md +0 -32
- package/tasks/review/task-001.md +0 -78
- /package/{bin → src}/lib/agents.sh +0 -0
- /package/{bin → src}/lib/util.sh +0 -0
- /package/{bin → src}/lib/vcs.js +0 -0
- /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-
|
|
66
|
+
# Last sync: 2026-04-03 (added loki, renamed sentinel→temper)
|
|
67
67
|
VALID_AGENTS=(
|
|
68
68
|
"hub"
|
|
69
|
-
"
|
|
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
|
-
#
|
|
90
|
-
["
|
|
91
|
-
["review"]="
|
|
92
|
-
["reviewer"]="
|
|
93
|
-
["cr"]="
|
|
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
|
-
["
|
|
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
|
-
["
|
|
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
|
-
["
|
|
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
|
-
["
|
|
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
|
-
["
|
|
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
|
+
}
|