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,107 +1,107 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
#
|
|
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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,146 +1,146 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# Task dependency resolution (T2-H2)
|
|
6
|
-
#
|
|
7
|
-
# Checks blocked_by fields in pending task frontmatter. Tasks whose
|
|
8
|
-
# blockers are all resolved (in completed/ or merged/) are eligible
|
|
9
|
-
# for dispatch. Tasks with unresolved blockers are surfaced in the
|
|
10
|
-
# state file so Planning Hub and the dashboard can show them.
|
|
11
|
-
#
|
|
12
|
-
# Dependencies: frontmatter.js, constants.sh
|
|
13
|
-
# Globals required: FORGE_ROOT, TASKS_PENDING, TASKS_COMPLETED,
|
|
14
|
-
# TASKS_MERGED, LOG_FILE
|
|
15
|
-
|
|
16
|
-
# Prevent double-sourcing
|
|
17
|
-
[[ -n "${_DAEMON_DEPENDENCIES_LOADED:-}" ]] && return 0
|
|
18
|
-
_DAEMON_DEPENDENCIES_LOADED=1
|
|
19
|
-
|
|
20
|
-
# Node.js frontmatter helper
|
|
21
|
-
FRONTMATTER_JS="${FRONTMATTER_JS:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/frontmatter.js}"
|
|
22
|
-
|
|
23
|
-
# Count of currently blocked tasks (used by state.sh)
|
|
24
|
-
BLOCKED_TASK_COUNT=0
|
|
25
|
-
|
|
26
|
-
# Check if a task ID exists in completed or merged directories
|
|
27
|
-
_is_task_resolved() {
|
|
28
|
-
local task_id="$1"
|
|
29
|
-
local completed_dir="$FORGE_ROOT/$TASKS_COMPLETED"
|
|
30
|
-
local merged_dir="$FORGE_ROOT/$TASKS_MERGED"
|
|
31
|
-
|
|
32
|
-
# Check completed/
|
|
33
|
-
for f in "$completed_dir"/*.md; do
|
|
34
|
-
[[ -f "$f" ]] || continue
|
|
35
|
-
if grep -q "^id: *${task_id}$" "$f" 2>/dev/null; then
|
|
36
|
-
return 0
|
|
37
|
-
fi
|
|
38
|
-
done
|
|
39
|
-
|
|
40
|
-
# Check merged/
|
|
41
|
-
for f in "$merged_dir"/*.md; do
|
|
42
|
-
[[ -f "$f" ]] || continue
|
|
43
|
-
if grep -q "^id: *${task_id}$" "$f" 2>/dev/null; then
|
|
44
|
-
return 0
|
|
45
|
-
fi
|
|
46
|
-
done
|
|
47
|
-
|
|
48
|
-
return 1
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
# Check all pending tasks for unresolved blockers.
|
|
52
|
-
# Sets BLOCKED_TASK_COUNT and outputs YAML for blocked tasks.
|
|
53
|
-
check_task_dependencies() {
|
|
54
|
-
BLOCKED_TASK_COUNT=0
|
|
55
|
-
local pending_dir="$FORGE_ROOT/$TASKS_PENDING"
|
|
56
|
-
|
|
57
|
-
[[ -d "$pending_dir" ]] || return 0
|
|
58
|
-
|
|
59
|
-
for task_file in "$pending_dir"/*.md; do
|
|
60
|
-
[[ -f "$task_file" && ! -L "$task_file" ]] || continue
|
|
61
|
-
|
|
62
|
-
# Extract blocked_by field (comma or space separated list of task IDs)
|
|
63
|
-
local blocked_by
|
|
64
|
-
blocked_by=$(node "$FRONTMATTER_JS" "$task_file" "blocked_by" 2>/dev/null | sed -n 's/^blocked_by=//p')
|
|
65
|
-
|
|
66
|
-
[[ -z "$blocked_by" ]] && continue
|
|
67
|
-
|
|
68
|
-
# Parse the list (handle comma-separated, space-separated, or YAML array)
|
|
69
|
-
local blockers
|
|
70
|
-
blockers=$(echo "$blocked_by" | tr ',[]' ' ' | tr -s ' ')
|
|
71
|
-
|
|
72
|
-
local unresolved=""
|
|
73
|
-
local all_resolved=true
|
|
74
|
-
|
|
75
|
-
for blocker_id in $blockers; do
|
|
76
|
-
# Skip empty tokens
|
|
77
|
-
[[ -z "$blocker_id" ]] && continue
|
|
78
|
-
|
|
79
|
-
if ! _is_task_resolved "$blocker_id"; then
|
|
80
|
-
all_resolved=false
|
|
81
|
-
if [[ -n "$unresolved" ]]; then
|
|
82
|
-
unresolved="$unresolved, $blocker_id"
|
|
83
|
-
else
|
|
84
|
-
unresolved="$blocker_id"
|
|
85
|
-
fi
|
|
86
|
-
fi
|
|
87
|
-
done
|
|
88
|
-
|
|
89
|
-
if [[ "$all_resolved" == "false" ]]; then
|
|
90
|
-
((BLOCKED_TASK_COUNT++)) || true
|
|
91
|
-
local task_id
|
|
92
|
-
task_id=$(node "$FRONTMATTER_JS" "$task_file" "id" 2>/dev/null | sed -n 's/^id=//p')
|
|
93
|
-
task_id="${task_id:-$(basename "$task_file" .md)}"
|
|
94
|
-
echo "[$(date -Iseconds)] BLOCKED: $task_id waiting on: $unresolved" >> "$LOG_FILE"
|
|
95
|
-
fi
|
|
96
|
-
done
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
# Build YAML section for blocked tasks (called by state.sh update_state)
|
|
100
|
-
build_blocked_tasks() {
|
|
101
|
-
local pending_dir="$FORGE_ROOT/$TASKS_PENDING"
|
|
102
|
-
|
|
103
|
-
[[ -d "$pending_dir" ]] || return 0
|
|
104
|
-
|
|
105
|
-
local has_blocked=false
|
|
106
|
-
|
|
107
|
-
for task_file in "$pending_dir"/*.md; do
|
|
108
|
-
[[ -f "$task_file" && ! -L "$task_file" ]] || continue
|
|
109
|
-
|
|
110
|
-
local blocked_by
|
|
111
|
-
blocked_by=$(node "$FRONTMATTER_JS" "$task_file" "blocked_by" 2>/dev/null | sed -n 's/^blocked_by=//p')
|
|
112
|
-
|
|
113
|
-
[[ -z "$blocked_by" ]] && continue
|
|
114
|
-
|
|
115
|
-
local blockers unresolved_list=""
|
|
116
|
-
blockers=$(echo "$blocked_by" | tr ',[]' ' ' | tr -s ' ')
|
|
117
|
-
|
|
118
|
-
for blocker_id in $blockers; do
|
|
119
|
-
[[ -z "$blocker_id" ]] && continue
|
|
120
|
-
if ! _is_task_resolved "$blocker_id"; then
|
|
121
|
-
if [[ -n "$unresolved_list" ]]; then
|
|
122
|
-
unresolved_list="$unresolved_list, $blocker_id"
|
|
123
|
-
else
|
|
124
|
-
unresolved_list="$blocker_id"
|
|
125
|
-
fi
|
|
126
|
-
fi
|
|
127
|
-
done
|
|
128
|
-
|
|
129
|
-
if [[ -n "$unresolved_list" ]]; then
|
|
130
|
-
if [[ "$has_blocked" == "false" ]]; then
|
|
131
|
-
echo "blocked_tasks:"
|
|
132
|
-
has_blocked=true
|
|
133
|
-
fi
|
|
134
|
-
|
|
135
|
-
local task_id title
|
|
136
|
-
task_id=$(node "$FRONTMATTER_JS" "$task_file" "id" 2>/dev/null | sed -n 's/^id=//p')
|
|
137
|
-
title=$(node "$FRONTMATTER_JS" "$task_file" "title" 2>/dev/null | sed -n 's/^title=//p')
|
|
138
|
-
task_id="${task_id:-$(basename "$task_file" .md)}"
|
|
139
|
-
title="${title:-Untitled}"
|
|
140
|
-
|
|
141
|
-
printf ' - id: %s\n' "$task_id"
|
|
142
|
-
printf ' title: "%s"\n' "$title"
|
|
143
|
-
printf ' waiting_on: "%s"\n' "$unresolved_list"
|
|
144
|
-
fi
|
|
145
|
-
done
|
|
146
|
-
}
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# src/lib/daemon/dependencies.sh
|
|
4
|
+
#
|
|
5
|
+
# Task dependency resolution (T2-H2)
|
|
6
|
+
#
|
|
7
|
+
# Checks blocked_by fields in pending task frontmatter. Tasks whose
|
|
8
|
+
# blockers are all resolved (in completed/ or merged/) are eligible
|
|
9
|
+
# for dispatch. Tasks with unresolved blockers are surfaced in the
|
|
10
|
+
# state file so Planning Hub and the dashboard can show them.
|
|
11
|
+
#
|
|
12
|
+
# Dependencies: frontmatter.js, constants.sh
|
|
13
|
+
# Globals required: FORGE_ROOT, TASKS_PENDING, TASKS_COMPLETED,
|
|
14
|
+
# TASKS_MERGED, LOG_FILE
|
|
15
|
+
|
|
16
|
+
# Prevent double-sourcing
|
|
17
|
+
[[ -n "${_DAEMON_DEPENDENCIES_LOADED:-}" ]] && return 0
|
|
18
|
+
_DAEMON_DEPENDENCIES_LOADED=1
|
|
19
|
+
|
|
20
|
+
# Node.js frontmatter helper
|
|
21
|
+
FRONTMATTER_JS="${FRONTMATTER_JS:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/frontmatter.js}"
|
|
22
|
+
|
|
23
|
+
# Count of currently blocked tasks (used by state.sh)
|
|
24
|
+
BLOCKED_TASK_COUNT=0
|
|
25
|
+
|
|
26
|
+
# Check if a task ID exists in completed or merged directories
|
|
27
|
+
_is_task_resolved() {
|
|
28
|
+
local task_id="$1"
|
|
29
|
+
local completed_dir="$FORGE_ROOT/$TASKS_COMPLETED"
|
|
30
|
+
local merged_dir="$FORGE_ROOT/$TASKS_MERGED"
|
|
31
|
+
|
|
32
|
+
# Check completed/
|
|
33
|
+
for f in "$completed_dir"/*.md; do
|
|
34
|
+
[[ -f "$f" ]] || continue
|
|
35
|
+
if grep -q "^id: *${task_id}$" "$f" 2>/dev/null; then
|
|
36
|
+
return 0
|
|
37
|
+
fi
|
|
38
|
+
done
|
|
39
|
+
|
|
40
|
+
# Check merged/
|
|
41
|
+
for f in "$merged_dir"/*.md; do
|
|
42
|
+
[[ -f "$f" ]] || continue
|
|
43
|
+
if grep -q "^id: *${task_id}$" "$f" 2>/dev/null; then
|
|
44
|
+
return 0
|
|
45
|
+
fi
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
return 1
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Check all pending tasks for unresolved blockers.
|
|
52
|
+
# Sets BLOCKED_TASK_COUNT and outputs YAML for blocked tasks.
|
|
53
|
+
check_task_dependencies() {
|
|
54
|
+
BLOCKED_TASK_COUNT=0
|
|
55
|
+
local pending_dir="$FORGE_ROOT/$TASKS_PENDING"
|
|
56
|
+
|
|
57
|
+
[[ -d "$pending_dir" ]] || return 0
|
|
58
|
+
|
|
59
|
+
for task_file in "$pending_dir"/*.md; do
|
|
60
|
+
[[ -f "$task_file" && ! -L "$task_file" ]] || continue
|
|
61
|
+
|
|
62
|
+
# Extract blocked_by field (comma or space separated list of task IDs)
|
|
63
|
+
local blocked_by
|
|
64
|
+
blocked_by=$(node "$FRONTMATTER_JS" "$task_file" "blocked_by" 2>/dev/null | sed -n 's/^blocked_by=//p')
|
|
65
|
+
|
|
66
|
+
[[ -z "$blocked_by" ]] && continue
|
|
67
|
+
|
|
68
|
+
# Parse the list (handle comma-separated, space-separated, or YAML array)
|
|
69
|
+
local blockers
|
|
70
|
+
blockers=$(echo "$blocked_by" | tr ',[]' ' ' | tr -s ' ')
|
|
71
|
+
|
|
72
|
+
local unresolved=""
|
|
73
|
+
local all_resolved=true
|
|
74
|
+
|
|
75
|
+
for blocker_id in $blockers; do
|
|
76
|
+
# Skip empty tokens
|
|
77
|
+
[[ -z "$blocker_id" ]] && continue
|
|
78
|
+
|
|
79
|
+
if ! _is_task_resolved "$blocker_id"; then
|
|
80
|
+
all_resolved=false
|
|
81
|
+
if [[ -n "$unresolved" ]]; then
|
|
82
|
+
unresolved="$unresolved, $blocker_id"
|
|
83
|
+
else
|
|
84
|
+
unresolved="$blocker_id"
|
|
85
|
+
fi
|
|
86
|
+
fi
|
|
87
|
+
done
|
|
88
|
+
|
|
89
|
+
if [[ "$all_resolved" == "false" ]]; then
|
|
90
|
+
((BLOCKED_TASK_COUNT++)) || true
|
|
91
|
+
local task_id
|
|
92
|
+
task_id=$(node "$FRONTMATTER_JS" "$task_file" "id" 2>/dev/null | sed -n 's/^id=//p')
|
|
93
|
+
task_id="${task_id:-$(basename "$task_file" .md)}"
|
|
94
|
+
echo "[$(date -Iseconds)] BLOCKED: $task_id waiting on: $unresolved" >> "$LOG_FILE"
|
|
95
|
+
fi
|
|
96
|
+
done
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Build YAML section for blocked tasks (called by state.sh update_state)
|
|
100
|
+
build_blocked_tasks() {
|
|
101
|
+
local pending_dir="$FORGE_ROOT/$TASKS_PENDING"
|
|
102
|
+
|
|
103
|
+
[[ -d "$pending_dir" ]] || return 0
|
|
104
|
+
|
|
105
|
+
local has_blocked=false
|
|
106
|
+
|
|
107
|
+
for task_file in "$pending_dir"/*.md; do
|
|
108
|
+
[[ -f "$task_file" && ! -L "$task_file" ]] || continue
|
|
109
|
+
|
|
110
|
+
local blocked_by
|
|
111
|
+
blocked_by=$(node "$FRONTMATTER_JS" "$task_file" "blocked_by" 2>/dev/null | sed -n 's/^blocked_by=//p')
|
|
112
|
+
|
|
113
|
+
[[ -z "$blocked_by" ]] && continue
|
|
114
|
+
|
|
115
|
+
local blockers unresolved_list=""
|
|
116
|
+
blockers=$(echo "$blocked_by" | tr ',[]' ' ' | tr -s ' ')
|
|
117
|
+
|
|
118
|
+
for blocker_id in $blockers; do
|
|
119
|
+
[[ -z "$blocker_id" ]] && continue
|
|
120
|
+
if ! _is_task_resolved "$blocker_id"; then
|
|
121
|
+
if [[ -n "$unresolved_list" ]]; then
|
|
122
|
+
unresolved_list="$unresolved_list, $blocker_id"
|
|
123
|
+
else
|
|
124
|
+
unresolved_list="$blocker_id"
|
|
125
|
+
fi
|
|
126
|
+
fi
|
|
127
|
+
done
|
|
128
|
+
|
|
129
|
+
if [[ -n "$unresolved_list" ]]; then
|
|
130
|
+
if [[ "$has_blocked" == "false" ]]; then
|
|
131
|
+
echo "blocked_tasks:"
|
|
132
|
+
has_blocked=true
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
local task_id title
|
|
136
|
+
task_id=$(node "$FRONTMATTER_JS" "$task_file" "id" 2>/dev/null | sed -n 's/^id=//p')
|
|
137
|
+
title=$(node "$FRONTMATTER_JS" "$task_file" "title" 2>/dev/null | sed -n 's/^title=//p')
|
|
138
|
+
task_id="${task_id:-$(basename "$task_file" .md)}"
|
|
139
|
+
title="${title:-Untitled}"
|
|
140
|
+
|
|
141
|
+
printf ' - id: %s\n' "$task_id"
|
|
142
|
+
printf ' title: "%s"\n' "$title"
|
|
143
|
+
printf ' waiting_on: "%s"\n' "$unresolved_list"
|
|
144
|
+
fi
|
|
145
|
+
done
|
|
146
|
+
}
|