vibe-forge 0.3.6 → 0.3.11
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 -0
- package/.claude/commands/forge.md +4 -0
- package/.claude/commands/need-help.md +77 -0
- package/.claude/commands/update-status.md +64 -0
- package/.claude/commands/worker-loop.md +106 -0
- package/.claude/hooks/worker-loop.sh +141 -0
- package/.claude/scripts/setup-worker-loop.sh +45 -0
- package/.claude/settings.local.json +13 -0
- package/agents/aegis/personality.md +20 -0
- package/agents/anvil/personality.md +19 -0
- package/agents/crucible/personality.md +20 -0
- package/agents/ember/personality.md +19 -0
- package/agents/furnace/personality.md +19 -0
- package/agents/herald/personality.md +20 -0
- package/agents/scribe/personality.md +18 -0
- package/bin/cli.js +49 -0
- package/bin/forge-daemon.sh +226 -7
- package/bin/forge-setup.sh +43 -0
- package/bin/forge-spawn.sh +25 -7
- package/bin/forge.sh +88 -11
- package/bin/lib/agents.sh +20 -0
- package/bin/lib/config.sh +12 -0
- package/bin/lib/constants.sh +28 -0
- package/config/agents.json +18 -9
- package/context/modern-conventions.md +129 -0
- package/docs/TODO.md +121 -10
- package/package.json +1 -1
|
@@ -66,6 +66,24 @@ Where other agents create, Scribe preserves. The README that saves a future deve
|
|
|
66
66
|
9. Move to /tasks/completed/
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
### Status Reporting
|
|
70
|
+
|
|
71
|
+
Keep the Planning Hub and daemon informed of your status:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
/update-status idle # When waiting for tasks
|
|
75
|
+
/update-status working TASK-024 # When starting a task
|
|
76
|
+
/update-status blocked TASK-024 # When stuck (then /need-help if needed)
|
|
77
|
+
/update-status idle # When task complete
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Update status at key moments:
|
|
81
|
+
|
|
82
|
+
1. **Startup**: Report `idle` (ready for work)
|
|
83
|
+
2. **Task pickup**: Report `working` with task ID
|
|
84
|
+
3. **Blocked**: Report `blocked`, then use `/need-help` if human input needed
|
|
85
|
+
4. **Completion**: Report `idle` after moving task to completed
|
|
86
|
+
|
|
69
87
|
### Output Format
|
|
70
88
|
```markdown
|
|
71
89
|
## Completion Summary
|
package/bin/cli.js
CHANGED
|
@@ -21,6 +21,8 @@ const REPO_URL = 'https://github.com/SpasticPalate/vibe-forge.git';
|
|
|
21
21
|
const FORGE_DIR = '_vibe-forge';
|
|
22
22
|
|
|
23
23
|
// Colors for terminal output
|
|
24
|
+
// NOTE: Intentionally self-contained - cli.js runs standalone via npx before
|
|
25
|
+
// the rest of Vibe Forge is installed, so it cannot share with colors.sh
|
|
24
26
|
const colors = {
|
|
25
27
|
reset: '\x1b[0m',
|
|
26
28
|
red: '\x1b[31m',
|
|
@@ -199,6 +201,9 @@ async function initCommand() {
|
|
|
199
201
|
|
|
200
202
|
log('');
|
|
201
203
|
|
|
204
|
+
// Update project's .gitignore to ignore tool internals but keep project data
|
|
205
|
+
updateGitignore();
|
|
206
|
+
|
|
202
207
|
// Run the setup script
|
|
203
208
|
logInfo('Running setup...');
|
|
204
209
|
log('');
|
|
@@ -212,6 +217,50 @@ async function initCommand() {
|
|
|
212
217
|
}
|
|
213
218
|
}
|
|
214
219
|
|
|
220
|
+
function updateGitignore() {
|
|
221
|
+
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
222
|
+
const forgeIgnoreMarker = '# Vibe Forge';
|
|
223
|
+
const forgeIgnoreBlock = `
|
|
224
|
+
# Vibe Forge
|
|
225
|
+
# Tool internals (regenerated on update)
|
|
226
|
+
_vibe-forge/.git/
|
|
227
|
+
_vibe-forge/bin/
|
|
228
|
+
_vibe-forge/agents/
|
|
229
|
+
_vibe-forge/config/
|
|
230
|
+
_vibe-forge/docs/
|
|
231
|
+
_vibe-forge/tests/
|
|
232
|
+
_vibe-forge/src/
|
|
233
|
+
_vibe-forge/node_modules/
|
|
234
|
+
_vibe-forge/package*.json
|
|
235
|
+
_vibe-forge/*.md
|
|
236
|
+
_vibe-forge/LICENSE
|
|
237
|
+
_vibe-forge/.github/
|
|
238
|
+
|
|
239
|
+
# Keep project data (tasks, context) - these ARE committed
|
|
240
|
+
# !_vibe-forge/tasks/
|
|
241
|
+
# !_vibe-forge/context/
|
|
242
|
+
# !_vibe-forge/.claude/
|
|
243
|
+
`;
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
let content = '';
|
|
247
|
+
if (fs.existsSync(gitignorePath)) {
|
|
248
|
+
content = fs.readFileSync(gitignorePath, 'utf8');
|
|
249
|
+
// Check if already has forge entries
|
|
250
|
+
if (content.includes(forgeIgnoreMarker)) {
|
|
251
|
+
return; // Already configured
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Append forge ignore block
|
|
256
|
+
const newContent = content.trimEnd() + '\n' + forgeIgnoreBlock;
|
|
257
|
+
fs.writeFileSync(gitignorePath, newContent);
|
|
258
|
+
logSuccess('Updated .gitignore for Vibe Forge');
|
|
259
|
+
} catch (err) {
|
|
260
|
+
logError(`Warning: Could not update .gitignore: ${err.message}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
215
264
|
async function updateCommand() {
|
|
216
265
|
showBanner();
|
|
217
266
|
|
package/bin/forge-daemon.sh
CHANGED
|
@@ -113,6 +113,7 @@ safe_move_task() {
|
|
|
113
113
|
|
|
114
114
|
notify() {
|
|
115
115
|
local message="$1"
|
|
116
|
+
local urgency="${2:-normal}" # normal or urgent
|
|
116
117
|
local timestamp
|
|
117
118
|
timestamp=$(date -Iseconds)
|
|
118
119
|
|
|
@@ -125,7 +126,41 @@ notify() {
|
|
|
125
126
|
# Terminal bell (works in most terminals)
|
|
126
127
|
printf '\a'
|
|
127
128
|
|
|
128
|
-
#
|
|
129
|
+
# System toast notification for urgent messages
|
|
130
|
+
if [[ "$urgency" == "urgent" ]]; then
|
|
131
|
+
send_system_notification "$message"
|
|
132
|
+
fi
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Send system-level notification (platform-specific)
|
|
136
|
+
send_system_notification() {
|
|
137
|
+
local message="$1"
|
|
138
|
+
local title="Vibe Forge"
|
|
139
|
+
|
|
140
|
+
case "$(uname -s)" in
|
|
141
|
+
MINGW*|MSYS*|CYGWIN*)
|
|
142
|
+
# Windows: Use PowerShell toast notification
|
|
143
|
+
powershell.exe -NoProfile -Command "
|
|
144
|
+
\$null = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
|
|
145
|
+
\$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
|
|
146
|
+
\$textNodes = \$template.GetElementsByTagName('text')
|
|
147
|
+
\$textNodes.Item(0).AppendChild(\$template.CreateTextNode('$title')) | Out-Null
|
|
148
|
+
\$textNodes.Item(1).AppendChild(\$template.CreateTextNode('$message')) | Out-Null
|
|
149
|
+
\$toast = [Windows.UI.Notifications.ToastNotification]::new(\$template)
|
|
150
|
+
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Vibe Forge').Show(\$toast)
|
|
151
|
+
" 2>/dev/null &
|
|
152
|
+
;;
|
|
153
|
+
Darwin)
|
|
154
|
+
# macOS: Use osascript
|
|
155
|
+
osascript -e "display notification \"$message\" with title \"$title\"" 2>/dev/null &
|
|
156
|
+
;;
|
|
157
|
+
Linux)
|
|
158
|
+
# Linux: Use notify-send if available
|
|
159
|
+
if command -v notify-send &>/dev/null; then
|
|
160
|
+
notify-send "$title" "$message" 2>/dev/null &
|
|
161
|
+
fi
|
|
162
|
+
;;
|
|
163
|
+
esac
|
|
129
164
|
}
|
|
130
165
|
|
|
131
166
|
check_new_pending_tasks() {
|
|
@@ -143,10 +178,10 @@ check_new_pending_tasks() {
|
|
|
143
178
|
# Extract task info from frontmatter safely
|
|
144
179
|
local task_id task_title assigned_to
|
|
145
180
|
|
|
146
|
-
# Use head to limit read
|
|
147
|
-
task_id=$(grep -m1 "^id:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 100)
|
|
148
|
-
task_title=$(grep -m1 "^title:" "$task" 2>/dev/null | cut -d':' -f2- | tr -d '"' | head -c 200)
|
|
149
|
-
assigned_to=$(grep -m1 "^assigned_to:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 50)
|
|
181
|
+
# Use head to limit read, tr to sanitize, and strip ANSI escape sequences
|
|
182
|
+
task_id=$(grep -m1 "^id:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | tr -d '\033' | sed 's/\[[0-9;]*m//g' | head -c 100)
|
|
183
|
+
task_title=$(grep -m1 "^title:" "$task" 2>/dev/null | cut -d':' -f2- | tr -d '"' | tr -d '\033' | sed 's/\[[0-9;]*m//g' | head -c 200)
|
|
184
|
+
assigned_to=$(grep -m1 "^assigned_to:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | tr -d '\033' | sed 's/\[[0-9;]*m//g' | head -c 50)
|
|
150
185
|
|
|
151
186
|
# Use filename as fallback
|
|
152
187
|
task_id="${task_id:-$filename}"
|
|
@@ -191,13 +226,93 @@ check_new_pending_tasks() {
|
|
|
191
226
|
done
|
|
192
227
|
}
|
|
193
228
|
|
|
229
|
+
check_attention_needed() {
|
|
230
|
+
# Check for workers needing attention (urgent notifications)
|
|
231
|
+
if [[ ! -d "$FORGE_ROOT/$TASKS_ATTENTION" ]]; then
|
|
232
|
+
return 0
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
for attention_file in "$FORGE_ROOT/$TASKS_ATTENTION"/*.md; do
|
|
236
|
+
if [[ -f "$attention_file" && ! -L "$attention_file" ]]; then
|
|
237
|
+
local filename
|
|
238
|
+
filename=$(basename "$attention_file")
|
|
239
|
+
local notified_key="attention:$filename"
|
|
240
|
+
|
|
241
|
+
if ! grep -qF "$notified_key" "$NOTIFIED_FILE" 2>/dev/null; then
|
|
242
|
+
# Extract attention info
|
|
243
|
+
local agent issue
|
|
244
|
+
agent=$(grep -m1 "^agent:" "$attention_file" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 50)
|
|
245
|
+
issue=$(grep -m1 "^##" "$attention_file" 2>/dev/null | sed 's/^## *//' | head -c 200)
|
|
246
|
+
|
|
247
|
+
agent="${agent:-Unknown}"
|
|
248
|
+
issue="${issue:-Needs attention}"
|
|
249
|
+
|
|
250
|
+
# Ring bell multiple times for attention
|
|
251
|
+
printf '\a\a\a'
|
|
252
|
+
|
|
253
|
+
# Send urgent notification with toast
|
|
254
|
+
notify "🔔 $agent needs help: $issue" "urgent"
|
|
255
|
+
|
|
256
|
+
echo "$notified_key" >> "$NOTIFIED_FILE"
|
|
257
|
+
fi
|
|
258
|
+
fi
|
|
259
|
+
done
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
# Build worker status from agent-status files
|
|
263
|
+
build_worker_status() {
|
|
264
|
+
local status_dir="$FORGE_ROOT/$AGENT_STATUS_DIR"
|
|
265
|
+
local now_epoch
|
|
266
|
+
now_epoch=$(date +%s)
|
|
267
|
+
local stale_threshold=300 # 5 minutes
|
|
268
|
+
|
|
269
|
+
if [[ ! -d "$status_dir" ]]; then
|
|
270
|
+
return 0
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
echo "workers:"
|
|
274
|
+
for status_file in "$status_dir"/*.json; do
|
|
275
|
+
if [[ -f "$status_file" && ! -L "$status_file" ]]; then
|
|
276
|
+
local agent status task message updated updated_epoch age stale_marker
|
|
277
|
+
|
|
278
|
+
# Parse JSON status file
|
|
279
|
+
agent=$(jq -r '.agent // "unknown"' "$status_file" 2>/dev/null)
|
|
280
|
+
status=$(jq -r '.status // "unknown"' "$status_file" 2>/dev/null)
|
|
281
|
+
task=$(jq -r '.task // ""' "$status_file" 2>/dev/null)
|
|
282
|
+
message=$(jq -r '.message // ""' "$status_file" 2>/dev/null | head -c 80)
|
|
283
|
+
updated=$(jq -r '.updated // ""' "$status_file" 2>/dev/null)
|
|
284
|
+
|
|
285
|
+
# Check if stale (not updated in 5+ minutes)
|
|
286
|
+
stale_marker=""
|
|
287
|
+
if [[ -n "$updated" ]]; then
|
|
288
|
+
# Convert ISO timestamp to epoch (cross-platform)
|
|
289
|
+
updated_epoch=$(date -d "$updated" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S" "${updated%Z}" +%s 2>/dev/null || echo "0")
|
|
290
|
+
age=$((now_epoch - updated_epoch))
|
|
291
|
+
if [[ "$age" -gt "$stale_threshold" ]]; then
|
|
292
|
+
stale_marker=" (stale)"
|
|
293
|
+
fi
|
|
294
|
+
fi
|
|
295
|
+
|
|
296
|
+
echo " - agent: $agent"
|
|
297
|
+
echo " status: $status$stale_marker"
|
|
298
|
+
if [[ -n "$task" ]]; then
|
|
299
|
+
echo " task: $task"
|
|
300
|
+
fi
|
|
301
|
+
if [[ -n "$message" ]]; then
|
|
302
|
+
echo " message: \"$message\""
|
|
303
|
+
fi
|
|
304
|
+
echo " updated: $updated"
|
|
305
|
+
fi
|
|
306
|
+
done
|
|
307
|
+
}
|
|
308
|
+
|
|
194
309
|
# =============================================================================
|
|
195
310
|
# Daemon Functions
|
|
196
311
|
# =============================================================================
|
|
197
312
|
|
|
198
313
|
update_state() {
|
|
199
314
|
# Count tasks in each folder (using find with -maxdepth for safety)
|
|
200
|
-
local pending in_progress completed review approved needs_changes merged
|
|
315
|
+
local pending in_progress completed review approved needs_changes merged attention
|
|
201
316
|
pending=$(find "$FORGE_ROOT/$TASKS_PENDING" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
202
317
|
in_progress=$(find "$FORGE_ROOT/$TASKS_IN_PROGRESS" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
203
318
|
completed=$(find "$FORGE_ROOT/$TASKS_COMPLETED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
@@ -205,9 +320,22 @@ update_state() {
|
|
|
205
320
|
approved=$(find "$FORGE_ROOT/$TASKS_APPROVED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
206
321
|
needs_changes=$(find "$FORGE_ROOT/$TASKS_NEEDS_CHANGES" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
207
322
|
merged=$(find "$FORGE_ROOT/$TASKS_MERGED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
323
|
+
attention=$(find "$FORGE_ROOT/$TASKS_ATTENTION" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
208
324
|
|
|
209
325
|
local blocked=0
|
|
210
326
|
|
|
327
|
+
# Build attention details if any workers need help
|
|
328
|
+
local attention_details=""
|
|
329
|
+
if [[ "$attention" -gt 0 ]]; then
|
|
330
|
+
attention_details=$(build_attention_details)
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
# Build worker status from agent-status files
|
|
334
|
+
local worker_status=""
|
|
335
|
+
if [[ -d "$FORGE_ROOT/$AGENT_STATUS_DIR" ]]; then
|
|
336
|
+
worker_status=$(build_worker_status)
|
|
337
|
+
fi
|
|
338
|
+
|
|
211
339
|
# Write state file atomically (write to temp, then move)
|
|
212
340
|
local temp_state="${STATE_FILE}.tmp.$$"
|
|
213
341
|
cat > "$temp_state" << EOF
|
|
@@ -228,12 +356,33 @@ tasks:
|
|
|
228
356
|
needs_changes: $needs_changes
|
|
229
357
|
merged: $merged
|
|
230
358
|
blocked: $blocked
|
|
359
|
+
attention_needed: $attention
|
|
231
360
|
|
|
361
|
+
$attention_details
|
|
362
|
+
$worker_status
|
|
232
363
|
last_updated: $(date -Iseconds)
|
|
233
364
|
EOF
|
|
234
365
|
mv "$temp_state" "$STATE_FILE"
|
|
235
366
|
}
|
|
236
367
|
|
|
368
|
+
build_attention_details() {
|
|
369
|
+
echo "attention:"
|
|
370
|
+
for attention_file in "$FORGE_ROOT/$TASKS_ATTENTION"/*.md; do
|
|
371
|
+
if [[ -f "$attention_file" && ! -L "$attention_file" ]]; then
|
|
372
|
+
local agent created issue
|
|
373
|
+
agent=$(grep -m1 "^agent:" "$attention_file" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 50)
|
|
374
|
+
created=$(grep -m1 "^created:" "$attention_file" 2>/dev/null | cut -d':' -f2- | tr -d ' ' | head -c 30)
|
|
375
|
+
# Get the issue line (first ## heading content or fallback)
|
|
376
|
+
issue=$(sed -n '/^## Issue/,/^##/p' "$attention_file" 2>/dev/null | grep -v "^##" | head -1 | tr -d '\n' | head -c 100)
|
|
377
|
+
issue="${issue:-Needs attention}"
|
|
378
|
+
|
|
379
|
+
echo " - agent: $agent"
|
|
380
|
+
echo " since: $created"
|
|
381
|
+
echo " issue: \"$issue\""
|
|
382
|
+
fi
|
|
383
|
+
done
|
|
384
|
+
}
|
|
385
|
+
|
|
237
386
|
route_completed_to_review() {
|
|
238
387
|
# Move completed tasks to review queue
|
|
239
388
|
for task in "$FORGE_ROOT/$TASKS_COMPLETED"/*.md; do
|
|
@@ -275,6 +424,9 @@ daemon_loop() {
|
|
|
275
424
|
# Check for new tasks and notify
|
|
276
425
|
check_new_pending_tasks
|
|
277
426
|
|
|
427
|
+
# Check for workers needing attention (urgent)
|
|
428
|
+
check_attention_needed
|
|
429
|
+
|
|
278
430
|
# Route tasks
|
|
279
431
|
route_completed_to_review
|
|
280
432
|
route_approved_to_merged
|
|
@@ -334,6 +486,8 @@ cmd_start() {
|
|
|
334
486
|
mkdir -p "$FORGE_ROOT/$TASKS_APPROVED"
|
|
335
487
|
mkdir -p "$FORGE_ROOT/$TASKS_NEEDS_CHANGES"
|
|
336
488
|
mkdir -p "$FORGE_ROOT/$TASKS_MERGED"
|
|
489
|
+
mkdir -p "$FORGE_ROOT/$TASKS_ATTENTION"
|
|
490
|
+
mkdir -p "$FORGE_ROOT/$AGENT_STATUS_DIR"
|
|
337
491
|
|
|
338
492
|
# Start daemon in background
|
|
339
493
|
daemon_loop &
|
|
@@ -394,11 +548,76 @@ cmd_status() {
|
|
|
394
548
|
|
|
395
549
|
if [[ -f "$STATE_FILE" ]]; then
|
|
396
550
|
echo "Task Counts:"
|
|
397
|
-
grep -E "pending:|in_progress:|completed:|in_review:|approved:|needs_changes:|merged:" "$STATE_FILE" | sed 's/^/ /'
|
|
551
|
+
grep -E "pending:|in_progress:|completed:|in_review:|approved:|needs_changes:|merged:|attention_needed:" "$STATE_FILE" | sed 's/^/ /'
|
|
398
552
|
fi
|
|
399
553
|
|
|
400
554
|
echo ""
|
|
401
555
|
|
|
556
|
+
# Show attention-needed workers prominently
|
|
557
|
+
local attention_count
|
|
558
|
+
attention_count=$(find "$FORGE_ROOT/$TASKS_ATTENTION" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
559
|
+
if [[ "$attention_count" -gt 0 ]]; then
|
|
560
|
+
echo -e "${RED}🔔 ATTENTION NEEDED:${NC}"
|
|
561
|
+
for attention_file in "$FORGE_ROOT/$TASKS_ATTENTION"/*.md; do
|
|
562
|
+
if [[ -f "$attention_file" && ! -L "$attention_file" ]]; then
|
|
563
|
+
local agent issue
|
|
564
|
+
agent=$(grep -m1 "^agent:" "$attention_file" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 50)
|
|
565
|
+
issue=$(sed -n '/^## Issue/,/^##/p' "$attention_file" 2>/dev/null | grep -v "^##" | head -1 | head -c 80)
|
|
566
|
+
echo -e " ${YELLOW}$agent${NC}: $issue"
|
|
567
|
+
fi
|
|
568
|
+
done
|
|
569
|
+
echo ""
|
|
570
|
+
fi
|
|
571
|
+
|
|
572
|
+
# Show worker status
|
|
573
|
+
if [[ -d "$FORGE_ROOT/$AGENT_STATUS_DIR" ]]; then
|
|
574
|
+
local status_count
|
|
575
|
+
status_count=$(find "$FORGE_ROOT/$AGENT_STATUS_DIR" -maxdepth 1 -name "*.json" -type f 2>/dev/null | wc -l)
|
|
576
|
+
if [[ "$status_count" -gt 0 ]]; then
|
|
577
|
+
echo "Active Workers:"
|
|
578
|
+
local now_epoch stale_threshold
|
|
579
|
+
now_epoch=$(date +%s)
|
|
580
|
+
stale_threshold=300
|
|
581
|
+
for status_file in "$FORGE_ROOT/$AGENT_STATUS_DIR"/*.json; do
|
|
582
|
+
if [[ -f "$status_file" && ! -L "$status_file" ]]; then
|
|
583
|
+
local agent status task updated stale_marker icon
|
|
584
|
+
agent=$(jq -r '.agent // "unknown"' "$status_file" 2>/dev/null)
|
|
585
|
+
status=$(jq -r '.status // "unknown"' "$status_file" 2>/dev/null)
|
|
586
|
+
task=$(jq -r '.task // ""' "$status_file" 2>/dev/null)
|
|
587
|
+
updated=$(jq -r '.updated // ""' "$status_file" 2>/dev/null)
|
|
588
|
+
|
|
589
|
+
# Check staleness
|
|
590
|
+
stale_marker=""
|
|
591
|
+
if [[ -n "$updated" ]]; then
|
|
592
|
+
local updated_epoch age
|
|
593
|
+
updated_epoch=$(date -d "$updated" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S" "${updated%Z}" +%s 2>/dev/null || echo "0")
|
|
594
|
+
age=$((now_epoch - updated_epoch))
|
|
595
|
+
if [[ "$age" -gt "$stale_threshold" ]]; then
|
|
596
|
+
stale_marker=" ${YELLOW}(stale)${NC}"
|
|
597
|
+
fi
|
|
598
|
+
fi
|
|
599
|
+
|
|
600
|
+
# Status icon
|
|
601
|
+
case "$status" in
|
|
602
|
+
"working") icon="🔨" ;;
|
|
603
|
+
"idle") icon="💤" ;;
|
|
604
|
+
"blocked") icon="🚫" ;;
|
|
605
|
+
"testing") icon="🧪" ;;
|
|
606
|
+
"reviewing") icon="👁️" ;;
|
|
607
|
+
*) icon="❓" ;;
|
|
608
|
+
esac
|
|
609
|
+
|
|
610
|
+
if [[ -n "$task" ]]; then
|
|
611
|
+
echo -e " $icon ${CYAN}$agent${NC}: $status ($task)$stale_marker"
|
|
612
|
+
else
|
|
613
|
+
echo -e " $icon ${CYAN}$agent${NC}: $status$stale_marker"
|
|
614
|
+
fi
|
|
615
|
+
fi
|
|
616
|
+
done
|
|
617
|
+
echo ""
|
|
618
|
+
fi
|
|
619
|
+
fi
|
|
620
|
+
|
|
402
621
|
# Show recent notifications
|
|
403
622
|
if [[ -f "$NOTIFY_FILE" ]]; then
|
|
404
623
|
local notify_count
|
package/bin/forge-setup.sh
CHANGED
|
@@ -304,6 +304,48 @@ configure_daemon() {
|
|
|
304
304
|
fi
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
# =============================================================================
|
|
308
|
+
# STEP 7b: Configure Worker Loop (Persistent Mode)
|
|
309
|
+
# =============================================================================
|
|
310
|
+
|
|
311
|
+
configure_worker_loop() {
|
|
312
|
+
echo ""
|
|
313
|
+
echo "Worker Loop Configuration"
|
|
314
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
315
|
+
echo ""
|
|
316
|
+
echo "Worker Loop keeps agents running continuously."
|
|
317
|
+
echo "When a worker finishes a task, it automatically checks for new work"
|
|
318
|
+
echo "instead of exiting. Great for longer coding sessions."
|
|
319
|
+
echo ""
|
|
320
|
+
|
|
321
|
+
local enable_worker_loop="n"
|
|
322
|
+
|
|
323
|
+
if [[ "$NON_INTERACTIVE" == "false" ]]; then
|
|
324
|
+
read -p "Enable persistent worker mode? (y/N): " choice
|
|
325
|
+
if [[ "$choice" == "y" || "$choice" == "Y" ]]; then
|
|
326
|
+
enable_worker_loop="y"
|
|
327
|
+
fi
|
|
328
|
+
fi
|
|
329
|
+
|
|
330
|
+
# Update config with worker loop preference
|
|
331
|
+
local config_file="$FORGE_ROOT/.forge/config.json"
|
|
332
|
+
if [[ -f "$config_file" ]]; then
|
|
333
|
+
# Add worker_loop_enabled to config
|
|
334
|
+
sed -i 's/"daemon_enabled": \(true\|false\)/"daemon_enabled": \1,\n "worker_loop_enabled": '"$( [[ "$enable_worker_loop" == "y" ]] && echo "true" || echo "false" )"'/' "$config_file" 2>/dev/null || true
|
|
335
|
+
fi
|
|
336
|
+
|
|
337
|
+
if [[ "$enable_worker_loop" == "y" ]]; then
|
|
338
|
+
echo ""
|
|
339
|
+
echo -e "${GREEN}✅ Worker Loop enabled${NC}"
|
|
340
|
+
echo " Workers will keep running and check for new tasks"
|
|
341
|
+
else
|
|
342
|
+
echo ""
|
|
343
|
+
echo -e "${YELLOW}ℹ️ Worker Loop disabled${NC}"
|
|
344
|
+
echo " Workers will exit after completing their tasks"
|
|
345
|
+
echo " Enable later with: forge config worker-loop on"
|
|
346
|
+
fi
|
|
347
|
+
}
|
|
348
|
+
|
|
307
349
|
# =============================================================================
|
|
308
350
|
# STEP 8: Install Slash Command
|
|
309
351
|
# =============================================================================
|
|
@@ -476,6 +518,7 @@ main() {
|
|
|
476
518
|
if validate_setup; then
|
|
477
519
|
configure_terminal
|
|
478
520
|
configure_daemon
|
|
521
|
+
configure_worker_loop
|
|
479
522
|
install_slash_command
|
|
480
523
|
create_project_context
|
|
481
524
|
setup_complete
|
package/bin/forge-spawn.sh
CHANGED
|
@@ -31,7 +31,9 @@ source "$SCRIPT_DIR/lib/agents.sh"
|
|
|
31
31
|
|
|
32
32
|
# Load agent configuration from JSON if available
|
|
33
33
|
if [[ -f "$FORGE_ROOT/$AGENTS_CONFIG" ]]; then
|
|
34
|
-
load_agents_from_json "$FORGE_ROOT/$AGENTS_CONFIG" 2>/dev/null
|
|
34
|
+
if ! load_agents_from_json "$FORGE_ROOT/$AGENTS_CONFIG" 2>/dev/null; then
|
|
35
|
+
log_warn "Failed to load agents.json, using fallback defaults"
|
|
36
|
+
fi
|
|
35
37
|
fi
|
|
36
38
|
|
|
37
39
|
# =============================================================================
|
|
@@ -44,9 +46,17 @@ spawn_windows_terminal() {
|
|
|
44
46
|
log_info "Spawning $agent in Windows Terminal..."
|
|
45
47
|
|
|
46
48
|
if command -v wt &> /dev/null || command -v wt.exe &> /dev/null; then
|
|
47
|
-
# Get display
|
|
48
|
-
local display_name
|
|
49
|
+
# Get display info for tab
|
|
50
|
+
local display_name icon tab_color
|
|
49
51
|
display_name=$(get_agent_display_name "$agent")
|
|
52
|
+
icon=$(get_agent_icon "$agent")
|
|
53
|
+
tab_color=$(get_agent_tab_color "$agent")
|
|
54
|
+
|
|
55
|
+
# Build tab title with icon
|
|
56
|
+
local tab_title="$display_name"
|
|
57
|
+
if [[ -n "$icon" ]]; then
|
|
58
|
+
tab_title="$icon $display_name"
|
|
59
|
+
fi
|
|
50
60
|
|
|
51
61
|
# Convert FORGE_ROOT to Windows path for wt.exe
|
|
52
62
|
# Git Bash paths like /c/foo become C:/foo
|
|
@@ -57,15 +67,21 @@ spawn_windows_terminal() {
|
|
|
57
67
|
win_forge_root="$FORGE_ROOT"
|
|
58
68
|
fi
|
|
59
69
|
|
|
70
|
+
# Build wt command with optional tab color
|
|
71
|
+
local wt_args=(-w 0 new-tab --title "$tab_title")
|
|
72
|
+
if [[ -n "$tab_color" ]]; then
|
|
73
|
+
wt_args+=(--tabColor "$tab_color")
|
|
74
|
+
fi
|
|
75
|
+
|
|
60
76
|
# Windows Terminal: open new tab with forge command
|
|
61
77
|
# SECURITY: $agent has already been validated through resolve_agent
|
|
62
78
|
if [[ -n "$GIT_BASH_PATH" ]]; then
|
|
63
79
|
local bash_path="${GIT_BASH_PATH//\//\\}"
|
|
64
|
-
wt.exe
|
|
80
|
+
wt.exe "${wt_args[@]}" "$bash_path" -c "cd \"$win_forge_root\" && ./bin/forge.sh start \"$agent\""
|
|
65
81
|
else
|
|
66
|
-
wt.exe
|
|
82
|
+
wt.exe "${wt_args[@]}" bash -c "cd \"$win_forge_root\" && ./bin/forge.sh start \"$agent\""
|
|
67
83
|
fi
|
|
68
|
-
log_success "$
|
|
84
|
+
log_success "$tab_title spawned in new Windows Terminal tab"
|
|
69
85
|
else
|
|
70
86
|
log_error "wt command not found."
|
|
71
87
|
echo "Make sure Windows Terminal is installed."
|
|
@@ -123,7 +139,9 @@ main() {
|
|
|
123
139
|
require_forge_config "$FORGE_ROOT"
|
|
124
140
|
|
|
125
141
|
echo ""
|
|
126
|
-
|
|
142
|
+
local icon
|
|
143
|
+
icon=$(get_agent_icon "$resolved")
|
|
144
|
+
log_header "${icon:-🔥} Forge Spawn: $(get_agent_display_name "$resolved")"
|
|
127
145
|
if [[ "$agent" != "$resolved" ]]; then
|
|
128
146
|
echo " (resolved from '$agent')"
|
|
129
147
|
fi
|
package/bin/forge.sh
CHANGED
|
@@ -41,7 +41,9 @@ source "$SCRIPT_DIR/lib/agents.sh"
|
|
|
41
41
|
# Load agent configuration from JSON if available
|
|
42
42
|
# This overwrites the fallback values in constants.sh
|
|
43
43
|
if [[ -f "$FORGE_ROOT/$AGENTS_CONFIG" ]]; then
|
|
44
|
-
load_agents_from_json "$FORGE_ROOT/$AGENTS_CONFIG" 2>/dev/null
|
|
44
|
+
if ! load_agents_from_json "$FORGE_ROOT/$AGENTS_CONFIG" 2>/dev/null; then
|
|
45
|
+
log_warn "Failed to load agents.json, using fallback defaults"
|
|
46
|
+
fi
|
|
45
47
|
fi
|
|
46
48
|
|
|
47
49
|
# =============================================================================
|
|
@@ -116,7 +118,13 @@ On startup, you MUST immediately display the team assembly welcome as shown in t
|
|
|
116
118
|
|
|
117
119
|
# Startup Instructions
|
|
118
120
|
|
|
119
|
-
On startup:
|
|
121
|
+
On startup:
|
|
122
|
+
1. Announce yourself briefly (icon, name, role)
|
|
123
|
+
2. Check tasks/pending/ and tasks/needs-changes/ for tasks assigned to you
|
|
124
|
+
3. If you find assigned tasks, IMMEDIATELY begin working on them (no confirmation needed)
|
|
125
|
+
4. If no tasks found, announce you're idle and ready for work
|
|
126
|
+
|
|
127
|
+
You are autonomous - when assigned work exists, start it without asking permission.
|
|
120
128
|
"
|
|
121
129
|
fi
|
|
122
130
|
|
|
@@ -233,6 +241,73 @@ cmd_daemon() {
|
|
|
233
241
|
esac
|
|
234
242
|
}
|
|
235
243
|
|
|
244
|
+
cmd_config() {
|
|
245
|
+
local setting="${1:-}"
|
|
246
|
+
local value="${2:-}"
|
|
247
|
+
local config_file="$FORGE_ROOT/.forge/config.json"
|
|
248
|
+
|
|
249
|
+
if [[ ! -f "$config_file" ]]; then
|
|
250
|
+
log_error "Forge not initialized. Run 'forge init' first."
|
|
251
|
+
exit 1
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
case "$setting" in
|
|
255
|
+
"worker-loop"|"loop")
|
|
256
|
+
case "$value" in
|
|
257
|
+
"on"|"true"|"1"|"enable"|"enabled")
|
|
258
|
+
# Enable worker loop
|
|
259
|
+
if jq -e '.worker_loop_enabled' "$config_file" > /dev/null 2>&1; then
|
|
260
|
+
jq '.worker_loop_enabled = true' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
|
|
261
|
+
else
|
|
262
|
+
jq '. + {worker_loop_enabled: true}' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
|
|
263
|
+
fi
|
|
264
|
+
log_success "Worker Loop enabled"
|
|
265
|
+
echo "Workers will now keep running to check for new tasks."
|
|
266
|
+
;;
|
|
267
|
+
"off"|"false"|"0"|"disable"|"disabled")
|
|
268
|
+
# Disable worker loop
|
|
269
|
+
if jq -e '.worker_loop_enabled' "$config_file" > /dev/null 2>&1; then
|
|
270
|
+
jq '.worker_loop_enabled = false' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
|
|
271
|
+
else
|
|
272
|
+
jq '. + {worker_loop_enabled: false}' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
|
|
273
|
+
fi
|
|
274
|
+
log_success "Worker Loop disabled"
|
|
275
|
+
echo "Workers will exit after completing their tasks."
|
|
276
|
+
;;
|
|
277
|
+
""|"status")
|
|
278
|
+
# Show current status
|
|
279
|
+
local current
|
|
280
|
+
current=$(jq -r '.worker_loop_enabled // false' "$config_file")
|
|
281
|
+
if [[ "$current" == "true" ]]; then
|
|
282
|
+
echo "Worker Loop: enabled"
|
|
283
|
+
else
|
|
284
|
+
echo "Worker Loop: disabled"
|
|
285
|
+
fi
|
|
286
|
+
;;
|
|
287
|
+
*)
|
|
288
|
+
echo "Usage: forge config worker-loop [on|off|status]"
|
|
289
|
+
;;
|
|
290
|
+
esac
|
|
291
|
+
;;
|
|
292
|
+
"")
|
|
293
|
+
# Show all config
|
|
294
|
+
echo ""
|
|
295
|
+
log_header "Forge Configuration"
|
|
296
|
+
echo ""
|
|
297
|
+
jq '.' "$config_file"
|
|
298
|
+
echo ""
|
|
299
|
+
;;
|
|
300
|
+
*)
|
|
301
|
+
log_error "Unknown setting: $setting"
|
|
302
|
+
echo ""
|
|
303
|
+
echo "Available settings:"
|
|
304
|
+
echo " worker-loop Toggle persistent worker mode (on/off)"
|
|
305
|
+
echo ""
|
|
306
|
+
echo "Usage: forge config <setting> [value]"
|
|
307
|
+
;;
|
|
308
|
+
esac
|
|
309
|
+
}
|
|
310
|
+
|
|
236
311
|
cmd_help() {
|
|
237
312
|
echo ""
|
|
238
313
|
log_header "🔥 Vibe Forge"
|
|
@@ -247,17 +322,15 @@ cmd_help() {
|
|
|
247
322
|
echo " status Show current forge status"
|
|
248
323
|
echo " test Validate setup is working"
|
|
249
324
|
echo " daemon <action> Manage background daemon (start|stop|status|notifications|clear)"
|
|
325
|
+
echo " config <setting> View or change configuration settings"
|
|
250
326
|
echo " help Show this help message"
|
|
251
327
|
echo ""
|
|
252
|
-
|
|
253
|
-
echo "
|
|
254
|
-
echo "
|
|
255
|
-
echo "
|
|
256
|
-
echo "
|
|
257
|
-
echo "
|
|
258
|
-
echo " herald (release, deploy) - Release Manager"
|
|
259
|
-
echo " ember (devops, ops, infra) - DevOps"
|
|
260
|
-
echo " aegis (security, sec, appsec) - Security"
|
|
328
|
+
show_available_agents
|
|
329
|
+
echo ""
|
|
330
|
+
echo "Configuration:"
|
|
331
|
+
echo " forge config Show all settings"
|
|
332
|
+
echo " forge config worker-loop on Enable persistent worker mode"
|
|
333
|
+
echo " forge config worker-loop off Disable persistent worker mode"
|
|
261
334
|
echo ""
|
|
262
335
|
echo "Examples:"
|
|
263
336
|
echo " forge Start Planning Hub"
|
|
@@ -298,6 +371,10 @@ main() {
|
|
|
298
371
|
shift
|
|
299
372
|
cmd_daemon "$@"
|
|
300
373
|
;;
|
|
374
|
+
"config")
|
|
375
|
+
shift
|
|
376
|
+
cmd_config "$@"
|
|
377
|
+
;;
|
|
301
378
|
"help"|"--help"|"-h")
|
|
302
379
|
cmd_help
|
|
303
380
|
;;
|