vibe-forge 0.8.1 → 0.8.3

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,107 +1,107 @@
1
- #!/usr/bin/env bash
2
- #
3
- # bin/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
+ #!/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
- # bin/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
- }
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
+ }