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.
Files changed (51) hide show
  1. package/.claude/commands/configure-vcs.md +102 -102
  2. package/.claude/commands/forge.md +218 -218
  3. package/.claude/hooks/worker-loop.js +220 -217
  4. package/.claude/settings.json +89 -89
  5. package/README.md +149 -191
  6. package/agents/aegis/personality.md +303 -303
  7. package/agents/anvil/personality.md +278 -278
  8. package/agents/architect/personality.md +260 -260
  9. package/agents/crucible/personality.md +362 -362
  10. package/agents/crucible-x/personality.md +210 -210
  11. package/agents/ember/personality.md +293 -293
  12. package/agents/flux/personality.md +248 -248
  13. package/agents/furnace/personality.md +342 -342
  14. package/agents/herald/personality.md +249 -249
  15. package/agents/oracle/personality.md +284 -284
  16. package/agents/pixel/personality.md +140 -140
  17. package/agents/planning-hub/personality.md +473 -473
  18. package/agents/scribe/personality.md +253 -253
  19. package/agents/slag/personality.md +268 -268
  20. package/agents/temper/personality.md +270 -270
  21. package/bin/cli.js +372 -372
  22. package/bin/forge-daemon.sh +477 -477
  23. package/bin/forge-setup.sh +662 -661
  24. package/bin/forge-spawn.sh +164 -164
  25. package/bin/forge.sh +566 -566
  26. package/docs/commands.md +8 -8
  27. package/package.json +77 -77
  28. package/{bin → src}/lib/agents.sh +177 -177
  29. package/{bin → src}/lib/check-aliases.js +50 -50
  30. package/{bin → src}/lib/colors.sh +45 -44
  31. package/{bin → src}/lib/config.sh +347 -347
  32. package/{bin → src}/lib/constants.sh +241 -241
  33. package/{bin → src}/lib/daemon/budgets.sh +107 -107
  34. package/{bin → src}/lib/daemon/dependencies.sh +146 -146
  35. package/{bin → src}/lib/daemon/display.sh +128 -128
  36. package/{bin → src}/lib/daemon/notifications.sh +273 -273
  37. package/{bin → src}/lib/daemon/routing.sh +93 -93
  38. package/{bin → src}/lib/daemon/state.sh +163 -163
  39. package/{bin → src}/lib/daemon/sync.sh +103 -103
  40. package/{bin → src}/lib/database.sh +357 -357
  41. package/{bin → src}/lib/frontmatter.js +106 -106
  42. package/{bin → src}/lib/heimdall-setup.js +113 -113
  43. package/{bin → src}/lib/heimdall.js +265 -265
  44. package/src/lib/index.sh +25 -0
  45. package/{bin → src}/lib/json.sh +264 -264
  46. package/{bin → src}/lib/terminal.js +452 -452
  47. package/{bin → src}/lib/util.sh +126 -126
  48. package/{bin → src}/lib/vcs.js +349 -349
  49. package/{context → templates}/project-context-template.md +122 -122
  50. package/config/task-template.md +0 -159
  51. package/config/templates/handoff-template.md +0 -40
@@ -1,93 +1,93 @@
1
- #!/usr/bin/env bash
2
- #
3
- # bin/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
+ #!/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
- # bin/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
- }
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
+ }