vibe-forge 0.8.1 → 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/configure-vcs.md +102 -102
- package/.claude/commands/forge.md +218 -218
- package/.claude/hooks/worker-loop.js +220 -217
- package/.claude/settings.json +89 -89
- package/README.md +149 -191
- package/agents/aegis/personality.md +303 -303
- package/agents/anvil/personality.md +278 -278
- package/agents/architect/personality.md +260 -260
- package/agents/crucible/personality.md +362 -362
- package/agents/crucible-x/personality.md +210 -210
- package/agents/ember/personality.md +293 -293
- package/agents/flux/personality.md +248 -248
- package/agents/furnace/personality.md +342 -342
- package/agents/herald/personality.md +249 -249
- package/agents/oracle/personality.md +284 -284
- package/agents/pixel/personality.md +140 -140
- package/agents/planning-hub/personality.md +473 -473
- package/agents/scribe/personality.md +253 -253
- package/agents/slag/personality.md +268 -268
- package/agents/temper/personality.md +270 -270
- package/bin/cli.js +372 -372
- package/bin/forge-daemon.sh +477 -477
- package/bin/forge-setup.sh +662 -661
- package/bin/forge-spawn.sh +164 -164
- package/bin/forge.sh +566 -566
- package/docs/commands.md +8 -8
- package/package.json +77 -77
- package/{bin → src}/lib/agents.sh +177 -177
- package/{bin → src}/lib/check-aliases.js +50 -50
- package/{bin → src}/lib/colors.sh +45 -44
- package/{bin → src}/lib/config.sh +347 -347
- package/{bin → src}/lib/constants.sh +241 -241
- package/{bin → src}/lib/daemon/budgets.sh +107 -107
- package/{bin → src}/lib/daemon/dependencies.sh +146 -146
- package/{bin → src}/lib/daemon/display.sh +128 -128
- package/{bin → src}/lib/daemon/notifications.sh +273 -273
- package/{bin → src}/lib/daemon/routing.sh +93 -93
- package/{bin → src}/lib/daemon/state.sh +163 -163
- package/{bin → src}/lib/daemon/sync.sh +103 -103
- package/{bin → src}/lib/database.sh +357 -357
- package/{bin → src}/lib/frontmatter.js +106 -106
- package/{bin → src}/lib/heimdall-setup.js +113 -113
- package/{bin → src}/lib/heimdall.js +265 -265
- package/src/lib/index.sh +25 -0
- package/{bin → src}/lib/json.sh +264 -264
- package/{bin → src}/lib/terminal.js +452 -452
- package/{bin → src}/lib/util.sh +126 -126
- package/{bin → src}/lib/vcs.js +349 -349
- package/{context → templates}/project-context-template.md +122 -122
- package/config/task-template.md +0 -159
- package/config/templates/handoff-template.md +0 -40
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# Task routing functions - moves tasks between state folders
|
|
6
|
-
#
|
|
7
|
-
# Dependencies: constants.sh, util.sh (log_*), database.sh (db_record_status_history)
|
|
8
|
-
# Globals required: FORGE_ROOT, LOG_FILE, FORGE_DB,
|
|
9
|
-
# TASKS_COMPLETED, TASKS_REVIEW, TASKS_APPROVED, TASKS_MERGED
|
|
10
|
-
|
|
11
|
-
# Prevent double-sourcing
|
|
12
|
-
[[ -n "${_DAEMON_ROUTING_LOADED:-}" ]] && return 0
|
|
13
|
-
_DAEMON_ROUTING_LOADED=1
|
|
14
|
-
|
|
15
|
-
# Safe file move with symlink protection
|
|
16
|
-
safe_move_task() {
|
|
17
|
-
local src="$1"
|
|
18
|
-
local dest_dir="$2"
|
|
19
|
-
|
|
20
|
-
# SECURITY: Skip symlinks to prevent symlink attacks
|
|
21
|
-
if [[ -L "$src" ]]; then
|
|
22
|
-
echo "[$(date -Iseconds)] WARNING: Skipping symlink: $src" >> "$LOG_FILE"
|
|
23
|
-
return 1
|
|
24
|
-
fi
|
|
25
|
-
|
|
26
|
-
# SECURITY: Verify source is a regular file
|
|
27
|
-
if [[ ! -f "$src" ]]; then
|
|
28
|
-
return 1
|
|
29
|
-
fi
|
|
30
|
-
|
|
31
|
-
# SECURITY: Verify destination is within FORGE_ROOT
|
|
32
|
-
local real_dest
|
|
33
|
-
real_dest=$(cd "$dest_dir" 2>/dev/null && pwd)
|
|
34
|
-
local forge_root_real
|
|
35
|
-
forge_root_real=$(cd "$FORGE_ROOT" 2>/dev/null && pwd)
|
|
36
|
-
|
|
37
|
-
if [[ "$real_dest" != "$forge_root_real"/* ]]; then
|
|
38
|
-
echo "[$(date -Iseconds)] ERROR: Destination outside FORGE_ROOT: $dest_dir" >> "$LOG_FILE"
|
|
39
|
-
return 1
|
|
40
|
-
fi
|
|
41
|
-
|
|
42
|
-
local filename
|
|
43
|
-
filename=$(basename "$src")
|
|
44
|
-
|
|
45
|
-
# SECURITY: TOCTOU note - there is a theoretical race between the symlink
|
|
46
|
-
# check above and this mv. A local attacker with precise timing could swap
|
|
47
|
-
# the file for a symlink in that window. This risk is accepted because:
|
|
48
|
-
# 1. Exploiting it requires local filesystem write access (already privileged)
|
|
49
|
-
# 2. The destination boundary check above limits the blast radius
|
|
50
|
-
# 3. A fully atomic check+move in bash would require platform-specific tools
|
|
51
|
-
# Mitigation: the destination is always within FORGE_ROOT (verified above).
|
|
52
|
-
mv "$src" "$dest_dir/$filename"
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
route_completed_to_review() {
|
|
56
|
-
# Move completed tasks to review queue — but ONLY if they have
|
|
57
|
-
# ready_for_review: true in frontmatter. Tasks that were moved
|
|
58
|
-
# to completed/ manually (e.g., already reviewed, historical)
|
|
59
|
-
# should not be re-routed.
|
|
60
|
-
for task in "$FORGE_ROOT/$TASKS_COMPLETED"/*.md; do
|
|
61
|
-
if [[ -f "$task" && ! -L "$task" ]]; then
|
|
62
|
-
# Only route tasks explicitly marked for review
|
|
63
|
-
local ready_for_review
|
|
64
|
-
ready_for_review=$(grep -m1 "^ready_for_review:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d " \"'" | head -c 10)
|
|
65
|
-
[[ "$ready_for_review" == "true" ]] || continue
|
|
66
|
-
|
|
67
|
-
local filename task_id assigned_to
|
|
68
|
-
filename=$(basename "$task")
|
|
69
|
-
task_id=$(grep -m1 "^id:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d " \"'" | head -c 50)
|
|
70
|
-
assigned_to=$(grep -m1 "^assigned_to:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d " \"'" | head -c 50)
|
|
71
|
-
echo "[$(date -Iseconds)] Routing $filename to review" >> "$LOG_FILE"
|
|
72
|
-
if safe_move_task "$task" "$FORGE_ROOT/$TASKS_REVIEW"; then
|
|
73
|
-
db_record_status_history "${assigned_to:-unknown}" "review" "${task_id:-$filename}" 2>/dev/null || true
|
|
74
|
-
fi
|
|
75
|
-
fi
|
|
76
|
-
done
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
route_approved_to_merged() {
|
|
80
|
-
# Move approved tasks to merged archive
|
|
81
|
-
for task in "$FORGE_ROOT/$TASKS_APPROVED"/*.md; do
|
|
82
|
-
if [[ -f "$task" && ! -L "$task" ]]; then
|
|
83
|
-
local filename task_id assigned_to
|
|
84
|
-
filename=$(basename "$task")
|
|
85
|
-
task_id=$(grep -m1 "^id:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d " \"'" | head -c 50)
|
|
86
|
-
assigned_to=$(grep -m1 "^assigned_to:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d " \"'" | head -c 50)
|
|
87
|
-
echo "[$(date -Iseconds)] Archiving $filename to merged" >> "$LOG_FILE"
|
|
88
|
-
if safe_move_task "$task" "$FORGE_ROOT/$TASKS_MERGED"; then
|
|
89
|
-
db_record_status_history "${assigned_to:-unknown}" "merged" "${task_id:-$filename}" 2>/dev/null || true
|
|
90
|
-
fi
|
|
91
|
-
fi
|
|
92
|
-
done
|
|
93
|
-
}
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# src/lib/daemon/routing.sh
|
|
4
|
+
#
|
|
5
|
+
# Task routing functions - moves tasks between state folders
|
|
6
|
+
#
|
|
7
|
+
# Dependencies: constants.sh, util.sh (log_*), database.sh (db_record_status_history)
|
|
8
|
+
# Globals required: FORGE_ROOT, LOG_FILE, FORGE_DB,
|
|
9
|
+
# TASKS_COMPLETED, TASKS_REVIEW, TASKS_APPROVED, TASKS_MERGED
|
|
10
|
+
|
|
11
|
+
# Prevent double-sourcing
|
|
12
|
+
[[ -n "${_DAEMON_ROUTING_LOADED:-}" ]] && return 0
|
|
13
|
+
_DAEMON_ROUTING_LOADED=1
|
|
14
|
+
|
|
15
|
+
# Safe file move with symlink protection
|
|
16
|
+
safe_move_task() {
|
|
17
|
+
local src="$1"
|
|
18
|
+
local dest_dir="$2"
|
|
19
|
+
|
|
20
|
+
# SECURITY: Skip symlinks to prevent symlink attacks
|
|
21
|
+
if [[ -L "$src" ]]; then
|
|
22
|
+
echo "[$(date -Iseconds)] WARNING: Skipping symlink: $src" >> "$LOG_FILE"
|
|
23
|
+
return 1
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# SECURITY: Verify source is a regular file
|
|
27
|
+
if [[ ! -f "$src" ]]; then
|
|
28
|
+
return 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# SECURITY: Verify destination is within FORGE_ROOT
|
|
32
|
+
local real_dest
|
|
33
|
+
real_dest=$(cd "$dest_dir" 2>/dev/null && pwd)
|
|
34
|
+
local forge_root_real
|
|
35
|
+
forge_root_real=$(cd "$FORGE_ROOT" 2>/dev/null && pwd)
|
|
36
|
+
|
|
37
|
+
if [[ "$real_dest" != "$forge_root_real"/* ]]; then
|
|
38
|
+
echo "[$(date -Iseconds)] ERROR: Destination outside FORGE_ROOT: $dest_dir" >> "$LOG_FILE"
|
|
39
|
+
return 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
local filename
|
|
43
|
+
filename=$(basename "$src")
|
|
44
|
+
|
|
45
|
+
# SECURITY: TOCTOU note - there is a theoretical race between the symlink
|
|
46
|
+
# check above and this mv. A local attacker with precise timing could swap
|
|
47
|
+
# the file for a symlink in that window. This risk is accepted because:
|
|
48
|
+
# 1. Exploiting it requires local filesystem write access (already privileged)
|
|
49
|
+
# 2. The destination boundary check above limits the blast radius
|
|
50
|
+
# 3. A fully atomic check+move in bash would require platform-specific tools
|
|
51
|
+
# Mitigation: the destination is always within FORGE_ROOT (verified above).
|
|
52
|
+
mv "$src" "$dest_dir/$filename"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
route_completed_to_review() {
|
|
56
|
+
# Move completed tasks to review queue — but ONLY if they have
|
|
57
|
+
# ready_for_review: true in frontmatter. Tasks that were moved
|
|
58
|
+
# to completed/ manually (e.g., already reviewed, historical)
|
|
59
|
+
# should not be re-routed.
|
|
60
|
+
for task in "$FORGE_ROOT/$TASKS_COMPLETED"/*.md; do
|
|
61
|
+
if [[ -f "$task" && ! -L "$task" ]]; then
|
|
62
|
+
# Only route tasks explicitly marked for review
|
|
63
|
+
local ready_for_review
|
|
64
|
+
ready_for_review=$(grep -m1 "^ready_for_review:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d " \"'" | head -c 10)
|
|
65
|
+
[[ "$ready_for_review" == "true" ]] || continue
|
|
66
|
+
|
|
67
|
+
local filename task_id assigned_to
|
|
68
|
+
filename=$(basename "$task")
|
|
69
|
+
task_id=$(grep -m1 "^id:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d " \"'" | head -c 50)
|
|
70
|
+
assigned_to=$(grep -m1 "^assigned_to:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d " \"'" | head -c 50)
|
|
71
|
+
echo "[$(date -Iseconds)] Routing $filename to review" >> "$LOG_FILE"
|
|
72
|
+
if safe_move_task "$task" "$FORGE_ROOT/$TASKS_REVIEW"; then
|
|
73
|
+
db_record_status_history "${assigned_to:-unknown}" "review" "${task_id:-$filename}" 2>/dev/null || true
|
|
74
|
+
fi
|
|
75
|
+
fi
|
|
76
|
+
done
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
route_approved_to_merged() {
|
|
80
|
+
# Move approved tasks to merged archive
|
|
81
|
+
for task in "$FORGE_ROOT/$TASKS_APPROVED"/*.md; do
|
|
82
|
+
if [[ -f "$task" && ! -L "$task" ]]; then
|
|
83
|
+
local filename task_id assigned_to
|
|
84
|
+
filename=$(basename "$task")
|
|
85
|
+
task_id=$(grep -m1 "^id:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d " \"'" | head -c 50)
|
|
86
|
+
assigned_to=$(grep -m1 "^assigned_to:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d " \"'" | head -c 50)
|
|
87
|
+
echo "[$(date -Iseconds)] Archiving $filename to merged" >> "$LOG_FILE"
|
|
88
|
+
if safe_move_task "$task" "$FORGE_ROOT/$TASKS_MERGED"; then
|
|
89
|
+
db_record_status_history "${assigned_to:-unknown}" "merged" "${task_id:-$filename}" 2>/dev/null || true
|
|
90
|
+
fi
|
|
91
|
+
fi
|
|
92
|
+
done
|
|
93
|
+
}
|
|
@@ -1,163 +1,163 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# Daemon state management - forge-state.yaml updates, attention tracking,
|
|
6
|
-
# adaptive polling
|
|
7
|
-
#
|
|
8
|
-
# Dependencies: database.sh, constants.sh
|
|
9
|
-
# Requires: sync.sh (for build_worker_status)
|
|
10
|
-
# Globals required: FORGE_ROOT, STATE_FILE, AGENT_STATUS_DIR,
|
|
11
|
-
# TASKS_PENDING, TASKS_IN_PROGRESS, TASKS_COMPLETED,
|
|
12
|
-
# TASKS_REVIEW, TASKS_APPROVED, TASKS_NEEDS_CHANGES,
|
|
13
|
-
# TASKS_MERGED, TASKS_ATTENTION
|
|
14
|
-
|
|
15
|
-
# Prevent double-sourcing
|
|
16
|
-
[[ -n "${_DAEMON_STATE_LOADED:-}" ]] && return 0
|
|
17
|
-
_DAEMON_STATE_LOADED=1
|
|
18
|
-
|
|
19
|
-
# Node.js frontmatter helper (RT-20260405-001 MEDIUM-5: replaces grep/cut YAML parsing)
|
|
20
|
-
FRONTMATTER_JS="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/frontmatter.js"
|
|
21
|
-
|
|
22
|
-
# Parse a single frontmatter field from a markdown file.
|
|
23
|
-
# Usage: fm_field <file> <field>
|
|
24
|
-
fm_field() {
|
|
25
|
-
node "$FRONTMATTER_JS" "$1" "$2" 2>/dev/null | sed -n "s/^${2}=//p"
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
# Parse a markdown section's first content line.
|
|
29
|
-
# Usage: fm_section <file> <heading>
|
|
30
|
-
fm_section() {
|
|
31
|
-
node "$FRONTMATTER_JS" --section "$1" "$2" 2>/dev/null
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
update_state() {
|
|
35
|
-
# Count tasks in each folder (using find with -maxdepth for safety)
|
|
36
|
-
local pending in_progress completed review approved needs_changes merged attention
|
|
37
|
-
pending=$(find "$FORGE_ROOT/$TASKS_PENDING" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
38
|
-
in_progress=$(find "$FORGE_ROOT/$TASKS_IN_PROGRESS" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
39
|
-
completed=$(find "$FORGE_ROOT/$TASKS_COMPLETED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
40
|
-
review=$(find "$FORGE_ROOT/$TASKS_REVIEW" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
41
|
-
approved=$(find "$FORGE_ROOT/$TASKS_APPROVED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
42
|
-
needs_changes=$(find "$FORGE_ROOT/$TASKS_NEEDS_CHANGES" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
43
|
-
merged=$(find "$FORGE_ROOT/$TASKS_MERGED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
44
|
-
attention=$(find "$FORGE_ROOT/$TASKS_ATTENTION" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
45
|
-
|
|
46
|
-
local blocked=${BLOCKED_TASK_COUNT:-0}
|
|
47
|
-
|
|
48
|
-
# Build active task details for in-progress tasks
|
|
49
|
-
local active_tasks_details=""
|
|
50
|
-
if [[ "$in_progress" -gt 0 ]]; then
|
|
51
|
-
active_tasks_details=$(build_active_tasks)
|
|
52
|
-
fi
|
|
53
|
-
|
|
54
|
-
# Build attention details if any workers need help
|
|
55
|
-
local attention_details=""
|
|
56
|
-
if [[ "$attention" -gt 0 ]]; then
|
|
57
|
-
attention_details=$(build_attention_details)
|
|
58
|
-
fi
|
|
59
|
-
|
|
60
|
-
# Build blocked task details (T2-H2)
|
|
61
|
-
local blocked_details=""
|
|
62
|
-
if [[ "$blocked" -gt 0 ]]; then
|
|
63
|
-
blocked_details=$(build_blocked_tasks)
|
|
64
|
-
fi
|
|
65
|
-
|
|
66
|
-
# Build worker status from agent-status files
|
|
67
|
-
local worker_status=""
|
|
68
|
-
if [[ -d "$FORGE_ROOT/$AGENT_STATUS_DIR" ]]; then
|
|
69
|
-
worker_status=$(build_worker_status)
|
|
70
|
-
fi
|
|
71
|
-
|
|
72
|
-
# Write state file atomically (write to temp, then move)
|
|
73
|
-
local temp_state="${STATE_FILE}.tmp.$$"
|
|
74
|
-
cat > "$temp_state" << EOF
|
|
75
|
-
# Vibe Forge State
|
|
76
|
-
# Auto-updated by forge-daemon
|
|
77
|
-
# Last updated: $(date -Iseconds)
|
|
78
|
-
|
|
79
|
-
forge:
|
|
80
|
-
status: active
|
|
81
|
-
daemon_pid: $$
|
|
82
|
-
|
|
83
|
-
tasks:
|
|
84
|
-
pending: $pending
|
|
85
|
-
in_progress: $in_progress
|
|
86
|
-
completed: $completed
|
|
87
|
-
in_review: $review
|
|
88
|
-
approved: $approved
|
|
89
|
-
needs_changes: $needs_changes
|
|
90
|
-
merged: $merged
|
|
91
|
-
blocked: $blocked
|
|
92
|
-
attention_needed: $attention
|
|
93
|
-
|
|
94
|
-
$active_tasks_details
|
|
95
|
-
$attention_details
|
|
96
|
-
$blocked_details
|
|
97
|
-
$worker_status
|
|
98
|
-
last_updated: $(date -Iseconds)
|
|
99
|
-
EOF
|
|
100
|
-
mv "$temp_state" "$STATE_FILE"
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
build_attention_details() {
|
|
104
|
-
echo "attention:"
|
|
105
|
-
for attention_file in "$FORGE_ROOT/$TASKS_ATTENTION"/*.md; do
|
|
106
|
-
if [[ -f "$attention_file" && ! -L "$attention_file" ]]; then
|
|
107
|
-
local agent created issue
|
|
108
|
-
agent=$(fm_field "$attention_file" "agent")
|
|
109
|
-
created=$(fm_field "$attention_file" "created")
|
|
110
|
-
issue=$(fm_section "$attention_file" "Issue")
|
|
111
|
-
issue="${issue:-Needs attention}"
|
|
112
|
-
|
|
113
|
-
printf ' - agent: %s\n' "$agent"
|
|
114
|
-
printf ' since: %s\n' "$created"
|
|
115
|
-
printf ' issue: "%s"\n' "$issue"
|
|
116
|
-
fi
|
|
117
|
-
done
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
build_active_tasks() {
|
|
121
|
-
echo "active_tasks:"
|
|
122
|
-
for task_file in "$FORGE_ROOT/$TASKS_IN_PROGRESS"/*.md; do
|
|
123
|
-
if [[ -f "$task_file" && ! -L "$task_file" ]]; then
|
|
124
|
-
local task_id title assigned_to
|
|
125
|
-
task_id=$(fm_field "$task_file" "id")
|
|
126
|
-
title=$(fm_field "$task_file" "title")
|
|
127
|
-
assigned_to=$(fm_field "$task_file" "assigned_to")
|
|
128
|
-
|
|
129
|
-
task_id="${task_id:-$(basename "$task_file" .md)}"
|
|
130
|
-
title="${title:-Untitled}"
|
|
131
|
-
assigned_to="${assigned_to:-unassigned}"
|
|
132
|
-
|
|
133
|
-
printf ' - id: %s\n' "$task_id"
|
|
134
|
-
printf ' title: "%s"\n' "$title"
|
|
135
|
-
printf ' assigned_to: %s\n' "$assigned_to"
|
|
136
|
-
fi
|
|
137
|
-
done
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
# Determine daemon state based on activity (for adaptive polling)
|
|
141
|
-
determine_daemon_state() {
|
|
142
|
-
# Check if there are in-progress tasks
|
|
143
|
-
local in_progress_count
|
|
144
|
-
in_progress_count=$(find "$FORGE_ROOT/$TASKS_IN_PROGRESS" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
145
|
-
|
|
146
|
-
# Check if there are active workers
|
|
147
|
-
local active_workers
|
|
148
|
-
active_workers=$(db_count_active_workers 2>/dev/null || echo "0")
|
|
149
|
-
|
|
150
|
-
if [[ "$in_progress_count" -gt 0 ]] || [[ "$active_workers" -gt 0 ]]; then
|
|
151
|
-
echo "active"
|
|
152
|
-
else
|
|
153
|
-
echo "idle"
|
|
154
|
-
fi
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
# Get current poll interval in seconds (from DB, with fallback)
|
|
158
|
-
get_poll_interval() {
|
|
159
|
-
local interval_ms
|
|
160
|
-
interval_ms=$(db_get_poll_interval_ms 2>/dev/null || echo "30000")
|
|
161
|
-
# Convert ms to seconds (bash integer division)
|
|
162
|
-
echo $((interval_ms / 1000))
|
|
163
|
-
}
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# src/lib/daemon/state.sh
|
|
4
|
+
#
|
|
5
|
+
# Daemon state management - forge-state.yaml updates, attention tracking,
|
|
6
|
+
# adaptive polling
|
|
7
|
+
#
|
|
8
|
+
# Dependencies: database.sh, constants.sh
|
|
9
|
+
# Requires: sync.sh (for build_worker_status)
|
|
10
|
+
# Globals required: FORGE_ROOT, STATE_FILE, AGENT_STATUS_DIR,
|
|
11
|
+
# TASKS_PENDING, TASKS_IN_PROGRESS, TASKS_COMPLETED,
|
|
12
|
+
# TASKS_REVIEW, TASKS_APPROVED, TASKS_NEEDS_CHANGES,
|
|
13
|
+
# TASKS_MERGED, TASKS_ATTENTION
|
|
14
|
+
|
|
15
|
+
# Prevent double-sourcing
|
|
16
|
+
[[ -n "${_DAEMON_STATE_LOADED:-}" ]] && return 0
|
|
17
|
+
_DAEMON_STATE_LOADED=1
|
|
18
|
+
|
|
19
|
+
# Node.js frontmatter helper (RT-20260405-001 MEDIUM-5: replaces grep/cut YAML parsing)
|
|
20
|
+
FRONTMATTER_JS="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/frontmatter.js"
|
|
21
|
+
|
|
22
|
+
# Parse a single frontmatter field from a markdown file.
|
|
23
|
+
# Usage: fm_field <file> <field>
|
|
24
|
+
fm_field() {
|
|
25
|
+
node "$FRONTMATTER_JS" "$1" "$2" 2>/dev/null | sed -n "s/^${2}=//p"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Parse a markdown section's first content line.
|
|
29
|
+
# Usage: fm_section <file> <heading>
|
|
30
|
+
fm_section() {
|
|
31
|
+
node "$FRONTMATTER_JS" --section "$1" "$2" 2>/dev/null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
update_state() {
|
|
35
|
+
# Count tasks in each folder (using find with -maxdepth for safety)
|
|
36
|
+
local pending in_progress completed review approved needs_changes merged attention
|
|
37
|
+
pending=$(find "$FORGE_ROOT/$TASKS_PENDING" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
38
|
+
in_progress=$(find "$FORGE_ROOT/$TASKS_IN_PROGRESS" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
39
|
+
completed=$(find "$FORGE_ROOT/$TASKS_COMPLETED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
40
|
+
review=$(find "$FORGE_ROOT/$TASKS_REVIEW" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
41
|
+
approved=$(find "$FORGE_ROOT/$TASKS_APPROVED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
42
|
+
needs_changes=$(find "$FORGE_ROOT/$TASKS_NEEDS_CHANGES" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
43
|
+
merged=$(find "$FORGE_ROOT/$TASKS_MERGED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
44
|
+
attention=$(find "$FORGE_ROOT/$TASKS_ATTENTION" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
45
|
+
|
|
46
|
+
local blocked=${BLOCKED_TASK_COUNT:-0}
|
|
47
|
+
|
|
48
|
+
# Build active task details for in-progress tasks
|
|
49
|
+
local active_tasks_details=""
|
|
50
|
+
if [[ "$in_progress" -gt 0 ]]; then
|
|
51
|
+
active_tasks_details=$(build_active_tasks)
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Build attention details if any workers need help
|
|
55
|
+
local attention_details=""
|
|
56
|
+
if [[ "$attention" -gt 0 ]]; then
|
|
57
|
+
attention_details=$(build_attention_details)
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Build blocked task details (T2-H2)
|
|
61
|
+
local blocked_details=""
|
|
62
|
+
if [[ "$blocked" -gt 0 ]]; then
|
|
63
|
+
blocked_details=$(build_blocked_tasks)
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Build worker status from agent-status files
|
|
67
|
+
local worker_status=""
|
|
68
|
+
if [[ -d "$FORGE_ROOT/$AGENT_STATUS_DIR" ]]; then
|
|
69
|
+
worker_status=$(build_worker_status)
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# Write state file atomically (write to temp, then move)
|
|
73
|
+
local temp_state="${STATE_FILE}.tmp.$$"
|
|
74
|
+
cat > "$temp_state" << EOF
|
|
75
|
+
# Vibe Forge State
|
|
76
|
+
# Auto-updated by forge-daemon
|
|
77
|
+
# Last updated: $(date -Iseconds)
|
|
78
|
+
|
|
79
|
+
forge:
|
|
80
|
+
status: active
|
|
81
|
+
daemon_pid: $$
|
|
82
|
+
|
|
83
|
+
tasks:
|
|
84
|
+
pending: $pending
|
|
85
|
+
in_progress: $in_progress
|
|
86
|
+
completed: $completed
|
|
87
|
+
in_review: $review
|
|
88
|
+
approved: $approved
|
|
89
|
+
needs_changes: $needs_changes
|
|
90
|
+
merged: $merged
|
|
91
|
+
blocked: $blocked
|
|
92
|
+
attention_needed: $attention
|
|
93
|
+
|
|
94
|
+
$active_tasks_details
|
|
95
|
+
$attention_details
|
|
96
|
+
$blocked_details
|
|
97
|
+
$worker_status
|
|
98
|
+
last_updated: $(date -Iseconds)
|
|
99
|
+
EOF
|
|
100
|
+
mv "$temp_state" "$STATE_FILE"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
build_attention_details() {
|
|
104
|
+
echo "attention:"
|
|
105
|
+
for attention_file in "$FORGE_ROOT/$TASKS_ATTENTION"/*.md; do
|
|
106
|
+
if [[ -f "$attention_file" && ! -L "$attention_file" ]]; then
|
|
107
|
+
local agent created issue
|
|
108
|
+
agent=$(fm_field "$attention_file" "agent")
|
|
109
|
+
created=$(fm_field "$attention_file" "created")
|
|
110
|
+
issue=$(fm_section "$attention_file" "Issue")
|
|
111
|
+
issue="${issue:-Needs attention}"
|
|
112
|
+
|
|
113
|
+
printf ' - agent: %s\n' "$agent"
|
|
114
|
+
printf ' since: %s\n' "$created"
|
|
115
|
+
printf ' issue: "%s"\n' "$issue"
|
|
116
|
+
fi
|
|
117
|
+
done
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
build_active_tasks() {
|
|
121
|
+
echo "active_tasks:"
|
|
122
|
+
for task_file in "$FORGE_ROOT/$TASKS_IN_PROGRESS"/*.md; do
|
|
123
|
+
if [[ -f "$task_file" && ! -L "$task_file" ]]; then
|
|
124
|
+
local task_id title assigned_to
|
|
125
|
+
task_id=$(fm_field "$task_file" "id")
|
|
126
|
+
title=$(fm_field "$task_file" "title")
|
|
127
|
+
assigned_to=$(fm_field "$task_file" "assigned_to")
|
|
128
|
+
|
|
129
|
+
task_id="${task_id:-$(basename "$task_file" .md)}"
|
|
130
|
+
title="${title:-Untitled}"
|
|
131
|
+
assigned_to="${assigned_to:-unassigned}"
|
|
132
|
+
|
|
133
|
+
printf ' - id: %s\n' "$task_id"
|
|
134
|
+
printf ' title: "%s"\n' "$title"
|
|
135
|
+
printf ' assigned_to: %s\n' "$assigned_to"
|
|
136
|
+
fi
|
|
137
|
+
done
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Determine daemon state based on activity (for adaptive polling)
|
|
141
|
+
determine_daemon_state() {
|
|
142
|
+
# Check if there are in-progress tasks
|
|
143
|
+
local in_progress_count
|
|
144
|
+
in_progress_count=$(find "$FORGE_ROOT/$TASKS_IN_PROGRESS" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
145
|
+
|
|
146
|
+
# Check if there are active workers
|
|
147
|
+
local active_workers
|
|
148
|
+
active_workers=$(db_count_active_workers 2>/dev/null || echo "0")
|
|
149
|
+
|
|
150
|
+
if [[ "$in_progress_count" -gt 0 ]] || [[ "$active_workers" -gt 0 ]]; then
|
|
151
|
+
echo "active"
|
|
152
|
+
else
|
|
153
|
+
echo "idle"
|
|
154
|
+
fi
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Get current poll interval in seconds (from DB, with fallback)
|
|
158
|
+
get_poll_interval() {
|
|
159
|
+
local interval_ms
|
|
160
|
+
interval_ms=$(db_get_poll_interval_ms 2>/dev/null || echo "30000")
|
|
161
|
+
# Convert ms to seconds (bash integer division)
|
|
162
|
+
echo $((interval_ms / 1000))
|
|
163
|
+
}
|