forge-pipeline 0.1.0
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/README.md +68 -0
- package/forge +593 -0
- package/lib/agent.sh +264 -0
- package/lib/phases/00_spec.sh +109 -0
- package/lib/phases/01_plan.sh +83 -0
- package/lib/phases/02_implement.sh +124 -0
- package/lib/phases/03_integrate.sh +162 -0
- package/lib/phases/04_audit.sh +78 -0
- package/lib/phases/05_fix.sh +188 -0
- package/lib/phases/06_finalize.sh +79 -0
- package/lib/prompts.sh +554 -0
- package/lib/utils.sh +223 -0
- package/lib/worktree.sh +82 -0
- package/package.json +20 -0
- package/prompts/architect_spec.md +184 -0
- package/prompts/challenger_review.md +162 -0
- package/prompts/coordinator_final.md +118 -0
- package/prompts/coordinator_plan.md +151 -0
- package/prompts/junior_auditor.md +139 -0
- package/prompts/lead.md +61 -0
- package/prompts/senior_auditor.md +193 -0
- package/prompts/worker.md +143 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Forge
|
|
2
|
+
|
|
3
|
+
Autonomous multi-agent coding pipeline. Give it a task, walk away, come back to finished code.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Clone and set up
|
|
9
|
+
git clone <repo-url> ~/forge
|
|
10
|
+
chmod +x ~/forge/forge
|
|
11
|
+
export PATH="$HOME/forge:$PATH" # Add to your shell profile
|
|
12
|
+
|
|
13
|
+
# Initialize a new project
|
|
14
|
+
forge init my-project
|
|
15
|
+
cd my-project
|
|
16
|
+
forge start "Build a REST API with user authentication"
|
|
17
|
+
|
|
18
|
+
# Or use with an existing project
|
|
19
|
+
cd my-existing-project
|
|
20
|
+
forge init
|
|
21
|
+
forge start "Add rate limiting to all API endpoints"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Commands
|
|
25
|
+
|
|
26
|
+
| Command | Description |
|
|
27
|
+
|---------|-------------|
|
|
28
|
+
| `forge init [name]` | Initialize a project (creates git repo if needed) |
|
|
29
|
+
| `forge start "task"` | Run the full autonomous pipeline |
|
|
30
|
+
| `forge run "task"` | Alias for start |
|
|
31
|
+
| `forge status` | Show current phase and active agents |
|
|
32
|
+
| `forge logs [agent]` | View agent logs |
|
|
33
|
+
| `forge abort` | Kill all running agents |
|
|
34
|
+
| `forge clean` | Remove .forge/ directory and temp branches |
|
|
35
|
+
|
|
36
|
+
## Options
|
|
37
|
+
|
|
38
|
+
| Option | Default | Description |
|
|
39
|
+
|--------|---------|-------------|
|
|
40
|
+
| `--max-workers N` | 4 | Max parallel workers per lead |
|
|
41
|
+
| `--max-leads N` | 4 | Max parallel leads |
|
|
42
|
+
| `--audit-rounds N` | 2 | Max audit-fix cycles |
|
|
43
|
+
| `--timeout N` | 60 | Timeout in minutes |
|
|
44
|
+
| `--dry-run` | - | Only run spec + planning phases |
|
|
45
|
+
| `--verbose` | - | Show real-time output |
|
|
46
|
+
|
|
47
|
+
## How It Works
|
|
48
|
+
|
|
49
|
+
Forge runs 7 phases automatically:
|
|
50
|
+
|
|
51
|
+
0. **Spec** — Architect writes spec, Challenger reviews it (up to 3 rounds)
|
|
52
|
+
1. **Plan** — Coordinator decomposes spec into parallel work units
|
|
53
|
+
2. **Implement** — Lead agents spawn Worker agents in isolated git worktrees
|
|
54
|
+
3. **Integrate** — All branches merged, conflicts resolved, tests run
|
|
55
|
+
4. **Audit** — Senior auditor spawns junior auditors for security, quality, testing review
|
|
56
|
+
5. **Fix** — Audit findings routed back for fixes (up to 2 rounds)
|
|
57
|
+
6. **Finalize** — Merge to main, generate summary report
|
|
58
|
+
|
|
59
|
+
## Requirements
|
|
60
|
+
|
|
61
|
+
- `git` — version control
|
|
62
|
+
- `tmux` — session management for parallel agents
|
|
63
|
+
- `claude` — [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)
|
|
64
|
+
- `jq` — JSON processing
|
|
65
|
+
|
|
66
|
+
## Optional
|
|
67
|
+
|
|
68
|
+
- **Augment codebase-retrieval** — Semantic code search for better results. Auto-detected if configured.
|
package/forge
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
###############################################################################
|
|
5
|
+
# FORGE — Autonomous Coding Pipeline
|
|
6
|
+
# Main CLI entry point
|
|
7
|
+
###############################################################################
|
|
8
|
+
|
|
9
|
+
# Determine installation directory (resolve symlinks for npm global installs)
|
|
10
|
+
SOURCE="${BASH_SOURCE[0]}"
|
|
11
|
+
while [ -L "$SOURCE" ]; do
|
|
12
|
+
DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
|
|
13
|
+
SOURCE="$(readlink "$SOURCE")"
|
|
14
|
+
[[ "$SOURCE" != /* ]] && SOURCE="$DIR/$SOURCE"
|
|
15
|
+
done
|
|
16
|
+
FORGE_DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
|
|
17
|
+
|
|
18
|
+
# Source all libraries
|
|
19
|
+
source "$FORGE_DIR/lib/utils.sh"
|
|
20
|
+
source "$FORGE_DIR/lib/agent.sh"
|
|
21
|
+
source "$FORGE_DIR/lib/worktree.sh"
|
|
22
|
+
source "$FORGE_DIR/lib/prompts.sh"
|
|
23
|
+
source "$FORGE_DIR/lib/phases/00_spec.sh"
|
|
24
|
+
source "$FORGE_DIR/lib/phases/01_plan.sh"
|
|
25
|
+
source "$FORGE_DIR/lib/phases/02_implement.sh"
|
|
26
|
+
source "$FORGE_DIR/lib/phases/03_integrate.sh"
|
|
27
|
+
source "$FORGE_DIR/lib/phases/04_audit.sh"
|
|
28
|
+
source "$FORGE_DIR/lib/phases/05_fix.sh"
|
|
29
|
+
source "$FORGE_DIR/lib/phases/06_finalize.sh"
|
|
30
|
+
|
|
31
|
+
###############################################################################
|
|
32
|
+
# Default configuration
|
|
33
|
+
###############################################################################
|
|
34
|
+
MAX_WORKERS=4
|
|
35
|
+
MAX_LEADS=4
|
|
36
|
+
AUDIT_ROUNDS=2
|
|
37
|
+
TIMEOUT_MINUTES=60
|
|
38
|
+
DRY_RUN=false
|
|
39
|
+
VERBOSE=false
|
|
40
|
+
AUTO_APPROVE=false
|
|
41
|
+
FORGE_TIMEOUT=$((TIMEOUT_MINUTES * 60))
|
|
42
|
+
FORGE_START=$(date +%s)
|
|
43
|
+
AGENT_COUNT=0
|
|
44
|
+
MAX_TOTAL_AGENTS=30
|
|
45
|
+
|
|
46
|
+
export MAX_WORKERS MAX_LEADS AUDIT_ROUNDS TIMEOUT_MINUTES DRY_RUN VERBOSE
|
|
47
|
+
export FORGE_TIMEOUT FORGE_START AGENT_COUNT MAX_TOTAL_AGENTS FORGE_DIR
|
|
48
|
+
|
|
49
|
+
###############################################################################
|
|
50
|
+
# Usage
|
|
51
|
+
###############################################################################
|
|
52
|
+
show_usage() {
|
|
53
|
+
cat <<'EOF'
|
|
54
|
+
Usage:
|
|
55
|
+
forge init [project-name] Initialize a project for forge
|
|
56
|
+
forge start <task description> Run the full pipeline
|
|
57
|
+
forge run <task description> Alias for start
|
|
58
|
+
forge status Show current phase and active agents
|
|
59
|
+
forge logs [agent-name] Tail an agent's log
|
|
60
|
+
forge abort Kill all agents and cleanup
|
|
61
|
+
forge clean Remove .forge/ directory and branches
|
|
62
|
+
|
|
63
|
+
Options:
|
|
64
|
+
--auto Skip approval gate, fully autonomous
|
|
65
|
+
--max-workers N Max parallel workers per lead (default: 4)
|
|
66
|
+
--max-leads N Max parallel leads (default: 4)
|
|
67
|
+
--audit-rounds N Max audit-fix cycles (default: 2)
|
|
68
|
+
--timeout MINUTES Kill everything after this long (default: 60)
|
|
69
|
+
--dry-run Only run spec + planning phases
|
|
70
|
+
--verbose Show real-time agent output
|
|
71
|
+
--help, -h Show this help
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
forge init my-app
|
|
75
|
+
forge start Add user authentication with JWT
|
|
76
|
+
forge start Refactor the payment module to use Stripe
|
|
77
|
+
forge --auto start Build a REST API with CRUD endpoints
|
|
78
|
+
EOF
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
###############################################################################
|
|
82
|
+
# Preflight check
|
|
83
|
+
###############################################################################
|
|
84
|
+
preflight_check() {
|
|
85
|
+
local skip_git_check="${1:-false}"
|
|
86
|
+
local missing=()
|
|
87
|
+
|
|
88
|
+
# Check required tools
|
|
89
|
+
command -v git >/dev/null 2>&1 || missing+=("git")
|
|
90
|
+
command -v tmux >/dev/null 2>&1 || missing+=("tmux")
|
|
91
|
+
command -v claude >/dev/null 2>&1 || missing+=("claude")
|
|
92
|
+
command -v jq >/dev/null 2>&1 || missing+=("jq")
|
|
93
|
+
|
|
94
|
+
if [ ${#missing[@]} -gt 0 ]; then
|
|
95
|
+
log_error "Missing required tools: ${missing[*]}"
|
|
96
|
+
log_error "Please install them before running forge."
|
|
97
|
+
exit 1
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# Check we're in a git repo (unless caller says to skip)
|
|
101
|
+
if [ "$skip_git_check" != "true" ]; then
|
|
102
|
+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
103
|
+
log_error "Not inside a git repository. Run 'forge init' first."
|
|
104
|
+
exit 1
|
|
105
|
+
fi
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# Detect Augment MCP tool
|
|
109
|
+
AUGMENT_AVAILABLE=false
|
|
110
|
+
AUGMENT_TOOL_NAME=""
|
|
111
|
+
|
|
112
|
+
local augment_line
|
|
113
|
+
augment_line="$(claude mcp list 2>/dev/null | grep -i "augment\|auggie\|codebase-retrieval" | head -1 || true)"
|
|
114
|
+
|
|
115
|
+
if [ -n "$augment_line" ]; then
|
|
116
|
+
AUGMENT_AVAILABLE=true
|
|
117
|
+
AUGMENT_TOOL_NAME="$(echo "$augment_line" | awk '{print $1}')"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
export AUGMENT_AVAILABLE AUGMENT_TOOL_NAME
|
|
121
|
+
|
|
122
|
+
# Ensure .forge directory exists
|
|
123
|
+
local forge_root
|
|
124
|
+
forge_root="$(get_forge_root)"
|
|
125
|
+
mkdir -p "$forge_root/.forge"
|
|
126
|
+
|
|
127
|
+
# Save config
|
|
128
|
+
cat > "$forge_root/.forge/config.json" <<CONF
|
|
129
|
+
{
|
|
130
|
+
"max_workers": $MAX_WORKERS,
|
|
131
|
+
"max_leads": $MAX_LEADS,
|
|
132
|
+
"audit_rounds": $AUDIT_ROUNDS,
|
|
133
|
+
"timeout_minutes": $TIMEOUT_MINUTES,
|
|
134
|
+
"dry_run": $DRY_RUN,
|
|
135
|
+
"verbose": $VERBOSE,
|
|
136
|
+
"auto_approve": $AUTO_APPROVE,
|
|
137
|
+
"augment_available": $AUGMENT_AVAILABLE,
|
|
138
|
+
"augment_tool_name": "$AUGMENT_TOOL_NAME",
|
|
139
|
+
"max_total_agents": $MAX_TOTAL_AGENTS,
|
|
140
|
+
"started_at": $FORGE_START
|
|
141
|
+
}
|
|
142
|
+
CONF
|
|
143
|
+
|
|
144
|
+
log_info "Preflight check passed."
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
###############################################################################
|
|
148
|
+
# Global timeout checker
|
|
149
|
+
###############################################################################
|
|
150
|
+
check_global_timeout() {
|
|
151
|
+
local now elapsed_minutes
|
|
152
|
+
now=$(date +%s)
|
|
153
|
+
elapsed_minutes=$(( (now - FORGE_START) / 60 ))
|
|
154
|
+
|
|
155
|
+
if [ "$elapsed_minutes" -ge "$TIMEOUT_MINUTES" ]; then
|
|
156
|
+
log_error "Global timeout reached (${TIMEOUT_MINUTES} minutes). Aborting all agents."
|
|
157
|
+
forge_abort
|
|
158
|
+
exit 1
|
|
159
|
+
fi
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
###############################################################################
|
|
163
|
+
# Init command (idempotent)
|
|
164
|
+
###############################################################################
|
|
165
|
+
forge_init() {
|
|
166
|
+
local project_name="${1:-}"
|
|
167
|
+
|
|
168
|
+
# If project name given, create directory and cd into it
|
|
169
|
+
if [ -n "$project_name" ]; then
|
|
170
|
+
mkdir -p "$project_name"
|
|
171
|
+
cd "$project_name"
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
# If not already a git repo, initialize one
|
|
175
|
+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
176
|
+
git init
|
|
177
|
+
git commit --allow-empty -m "Initial commit"
|
|
178
|
+
log_info "Initialized git repository."
|
|
179
|
+
else
|
|
180
|
+
log_info "Git repository already exists."
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
# Add .forge/ to .gitignore (idempotent — skip if already present)
|
|
184
|
+
if [ -f .gitignore ] && grep -qxF '.forge/' .gitignore 2>/dev/null; then
|
|
185
|
+
log_info ".forge/ already in .gitignore."
|
|
186
|
+
else
|
|
187
|
+
echo '.forge/' >> .gitignore
|
|
188
|
+
git add .gitignore
|
|
189
|
+
if ! git diff --cached --quiet 2>/dev/null; then
|
|
190
|
+
git commit -m "Update gitignore"
|
|
191
|
+
fi
|
|
192
|
+
log_info "Added .forge/ to .gitignore."
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
# Run preflight (skip git check since we just ensured it)
|
|
196
|
+
preflight_check "true"
|
|
197
|
+
|
|
198
|
+
log_success "Forge initialized. Run: forge start <your task>"
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
###############################################################################
|
|
202
|
+
# Approval gate — shows plan summary and asks user to confirm
|
|
203
|
+
###############################################################################
|
|
204
|
+
show_plan_summary() {
|
|
205
|
+
local forge_root="$1"
|
|
206
|
+
|
|
207
|
+
echo ""
|
|
208
|
+
printf "${BOLD}${CYAN}═══════════════════════════════════════${RESET}\n"
|
|
209
|
+
printf "${BOLD}${CYAN} PLAN REVIEW${RESET}\n"
|
|
210
|
+
printf "${BOLD}${CYAN}═══════════════════════════════════════${RESET}\n"
|
|
211
|
+
echo ""
|
|
212
|
+
|
|
213
|
+
# Show leads and workers summary from plan.json
|
|
214
|
+
local plan_file="$forge_root/.forge/plan.json"
|
|
215
|
+
local total_leads
|
|
216
|
+
total_leads="$(jq -r '.leads | length' "$plan_file" 2>/dev/null || echo 0)"
|
|
217
|
+
|
|
218
|
+
echo "Leads: $total_leads"
|
|
219
|
+
echo ""
|
|
220
|
+
|
|
221
|
+
local i=0
|
|
222
|
+
while [ "$i" -lt "$total_leads" ]; do
|
|
223
|
+
local lead_name lead_module workers_count
|
|
224
|
+
lead_name="$(jq -r ".leads[$i].name" "$plan_file")"
|
|
225
|
+
lead_module="$(jq -r ".leads[$i].module // .leads[$i].name" "$plan_file")"
|
|
226
|
+
workers_count="$(jq -r ".leads[$i].workers | length" "$plan_file")"
|
|
227
|
+
|
|
228
|
+
printf " ${BOLD}%s${RESET} (%s workers)\n" "$lead_module" "$workers_count"
|
|
229
|
+
|
|
230
|
+
local j=0
|
|
231
|
+
while [ "$j" -lt "$workers_count" ]; do
|
|
232
|
+
local worker_name worker_task
|
|
233
|
+
worker_name="$(jq -r ".leads[$i].workers[$j].name // .leads[$i].workers[$j].id" "$plan_file")"
|
|
234
|
+
worker_task="$(jq -r ".leads[$i].workers[$j].task // .leads[$i].workers[$j].task_description" "$plan_file" | head -c 80)"
|
|
235
|
+
printf " - %s: %s\n" "$worker_name" "$worker_task"
|
|
236
|
+
j=$((j + 1))
|
|
237
|
+
done
|
|
238
|
+
echo ""
|
|
239
|
+
i=$((i + 1))
|
|
240
|
+
done
|
|
241
|
+
|
|
242
|
+
echo "Full spec: .forge/spec/spec.md"
|
|
243
|
+
echo "Full plan: .forge/plan.json"
|
|
244
|
+
echo ""
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
wait_for_approval() {
|
|
248
|
+
printf "${BOLD}${YELLOW}Approve this plan and start implementation? [Y/n] ${RESET}"
|
|
249
|
+
local answer
|
|
250
|
+
read -r answer </dev/tty
|
|
251
|
+
case "$answer" in
|
|
252
|
+
n|N|no|No|NO)
|
|
253
|
+
log_info "Aborted. Edit .forge/spec/spec.md or .forge/plan.json, then run 'forge start' again."
|
|
254
|
+
exit 0
|
|
255
|
+
;;
|
|
256
|
+
*)
|
|
257
|
+
log_success "Plan approved. Starting autonomous execution..."
|
|
258
|
+
echo ""
|
|
259
|
+
;;
|
|
260
|
+
esac
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
###############################################################################
|
|
264
|
+
# Status command
|
|
265
|
+
###############################################################################
|
|
266
|
+
forge_status() {
|
|
267
|
+
local forge_root
|
|
268
|
+
forge_root="$(get_forge_root)"
|
|
269
|
+
|
|
270
|
+
echo ""
|
|
271
|
+
echo "=== Forge Status ==="
|
|
272
|
+
echo ""
|
|
273
|
+
|
|
274
|
+
# Determine current phase
|
|
275
|
+
if [ -f "$forge_root/.forge/status/finalize.done" ]; then
|
|
276
|
+
echo "Phase: COMPLETE"
|
|
277
|
+
elif [ -f "$forge_root/.forge/status/fix.done" ]; then
|
|
278
|
+
echo "Phase: Post-fix (audit/fix cycles)"
|
|
279
|
+
elif [ -f "$forge_root/.forge/status/audit.done" ]; then
|
|
280
|
+
echo "Phase: Audit complete"
|
|
281
|
+
elif [ -f "$forge_root/.forge/status/integrate.done" ]; then
|
|
282
|
+
echo "Phase: Integration complete"
|
|
283
|
+
elif [ -f "$forge_root/.forge/status/implement.done" ]; then
|
|
284
|
+
echo "Phase: Implementation complete"
|
|
285
|
+
elif [ -f "$forge_root/.forge/status/plan.done" ]; then
|
|
286
|
+
echo "Phase: Planning complete"
|
|
287
|
+
elif [ -f "$forge_root/.forge/status/spec.done" ]; then
|
|
288
|
+
echo "Phase: Spec complete"
|
|
289
|
+
else
|
|
290
|
+
echo "Phase: Not started or in progress"
|
|
291
|
+
fi
|
|
292
|
+
|
|
293
|
+
echo ""
|
|
294
|
+
|
|
295
|
+
# List active tmux sessions
|
|
296
|
+
echo "Active forge sessions:"
|
|
297
|
+
local sessions
|
|
298
|
+
sessions="$(tmux list-sessions 2>/dev/null | grep "^forge-" || true)"
|
|
299
|
+
if [ -n "$sessions" ]; then
|
|
300
|
+
echo "$sessions"
|
|
301
|
+
else
|
|
302
|
+
echo " (none)"
|
|
303
|
+
fi
|
|
304
|
+
|
|
305
|
+
echo ""
|
|
306
|
+
|
|
307
|
+
# Show agent counts
|
|
308
|
+
local expected=0 done=0
|
|
309
|
+
if [ -d "$forge_root/.forge/status" ]; then
|
|
310
|
+
expected=$(find "$forge_root/.forge/status" -name "*.expected" 2>/dev/null | wc -l | tr -d ' ')
|
|
311
|
+
done=$(find "$forge_root/.forge/status" -name "*.done" 2>/dev/null | wc -l | tr -d ' ')
|
|
312
|
+
fi
|
|
313
|
+
echo "Agents: $done / $expected completed"
|
|
314
|
+
echo ""
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
###############################################################################
|
|
318
|
+
# Logs command
|
|
319
|
+
###############################################################################
|
|
320
|
+
forge_logs() {
|
|
321
|
+
local agent_name="${1:-}"
|
|
322
|
+
local forge_root
|
|
323
|
+
forge_root="$(get_forge_root)"
|
|
324
|
+
local log_dir="$forge_root/.forge/logs"
|
|
325
|
+
|
|
326
|
+
if [ ! -d "$log_dir" ]; then
|
|
327
|
+
log_warn "No log directory found at $log_dir"
|
|
328
|
+
return 0
|
|
329
|
+
fi
|
|
330
|
+
|
|
331
|
+
if [ -n "$agent_name" ]; then
|
|
332
|
+
local log_file="$log_dir/${agent_name}.log"
|
|
333
|
+
if [ -f "$log_file" ]; then
|
|
334
|
+
tail -f "$log_file"
|
|
335
|
+
else
|
|
336
|
+
log_error "Log file not found: $log_file"
|
|
337
|
+
echo "Available logs:"
|
|
338
|
+
ls -1 "$log_dir"/*.log 2>/dev/null || echo " (none)"
|
|
339
|
+
fi
|
|
340
|
+
else
|
|
341
|
+
echo "Available log files:"
|
|
342
|
+
ls -1 "$log_dir"/*.log 2>/dev/null || echo " (none)"
|
|
343
|
+
fi
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
###############################################################################
|
|
347
|
+
# Abort command
|
|
348
|
+
###############################################################################
|
|
349
|
+
forge_abort() {
|
|
350
|
+
log_warn "Aborting all forge agents..."
|
|
351
|
+
|
|
352
|
+
local sessions
|
|
353
|
+
sessions="$(tmux list-sessions 2>/dev/null | grep "^forge-" | cut -d: -f1 || true)"
|
|
354
|
+
if [ -n "$sessions" ]; then
|
|
355
|
+
while IFS= read -r session; do
|
|
356
|
+
tmux kill-session -t "$session" 2>/dev/null || true
|
|
357
|
+
log_info "Killed session: $session"
|
|
358
|
+
done <<< "$sessions"
|
|
359
|
+
else
|
|
360
|
+
log_info "No active forge sessions found."
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
log_warn "All forge agents aborted."
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
###############################################################################
|
|
367
|
+
# Clean command
|
|
368
|
+
###############################################################################
|
|
369
|
+
forge_clean() {
|
|
370
|
+
local forge_root
|
|
371
|
+
forge_root="$(get_forge_root)"
|
|
372
|
+
|
|
373
|
+
forge_abort
|
|
374
|
+
|
|
375
|
+
if [ -d "$forge_root/.forge" ]; then
|
|
376
|
+
rm -rf "$forge_root/.forge"
|
|
377
|
+
log_info "Removed .forge directory."
|
|
378
|
+
fi
|
|
379
|
+
|
|
380
|
+
local worktrees
|
|
381
|
+
worktrees="$(git worktree list --porcelain 2>/dev/null | grep "^worktree " | grep "forge" | sed 's/^worktree //' || true)"
|
|
382
|
+
if [ -n "$worktrees" ]; then
|
|
383
|
+
while IFS= read -r wt; do
|
|
384
|
+
git worktree remove --force "$wt" 2>/dev/null || true
|
|
385
|
+
done <<< "$worktrees"
|
|
386
|
+
fi
|
|
387
|
+
|
|
388
|
+
git worktree prune 2>/dev/null || true
|
|
389
|
+
log_success "Forge workspace cleaned."
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
###############################################################################
|
|
393
|
+
# Argument parsing
|
|
394
|
+
#
|
|
395
|
+
# Handles three styles:
|
|
396
|
+
# forge start Add user auth with JWT (all args after "start" = prompt)
|
|
397
|
+
# forge Add user auth with JWT (all non-option args = prompt)
|
|
398
|
+
# forge --auto start Build me an app (options before subcommand)
|
|
399
|
+
###############################################################################
|
|
400
|
+
USER_PROMPT=""
|
|
401
|
+
SUBCOMMAND=""
|
|
402
|
+
INIT_PROJECT=""
|
|
403
|
+
LOGS_AGENT=""
|
|
404
|
+
PROMPT_ARGS=()
|
|
405
|
+
|
|
406
|
+
while [[ $# -gt 0 ]]; do
|
|
407
|
+
case "$1" in
|
|
408
|
+
--max-workers)
|
|
409
|
+
MAX_WORKERS="$2"; shift 2 ;;
|
|
410
|
+
--max-leads)
|
|
411
|
+
MAX_LEADS="$2"; shift 2 ;;
|
|
412
|
+
--audit-rounds)
|
|
413
|
+
AUDIT_ROUNDS="$2"; shift 2 ;;
|
|
414
|
+
--timeout)
|
|
415
|
+
TIMEOUT_MINUTES="$2"
|
|
416
|
+
FORGE_TIMEOUT=$((TIMEOUT_MINUTES * 60))
|
|
417
|
+
shift 2 ;;
|
|
418
|
+
--dry-run)
|
|
419
|
+
DRY_RUN=true; shift ;;
|
|
420
|
+
--verbose)
|
|
421
|
+
VERBOSE=true; shift ;;
|
|
422
|
+
--auto)
|
|
423
|
+
AUTO_APPROVE=true; shift ;;
|
|
424
|
+
--help|-h)
|
|
425
|
+
show_usage; exit 0 ;;
|
|
426
|
+
init)
|
|
427
|
+
SUBCOMMAND="init"
|
|
428
|
+
shift
|
|
429
|
+
# Everything after init is the project name (just first arg)
|
|
430
|
+
INIT_PROJECT="${1:-}"
|
|
431
|
+
[ -n "$INIT_PROJECT" ] && shift
|
|
432
|
+
break
|
|
433
|
+
;;
|
|
434
|
+
start|run)
|
|
435
|
+
SUBCOMMAND="$1"
|
|
436
|
+
shift
|
|
437
|
+
# Everything remaining is the prompt
|
|
438
|
+
PROMPT_ARGS+=("$@")
|
|
439
|
+
break
|
|
440
|
+
;;
|
|
441
|
+
status)
|
|
442
|
+
SUBCOMMAND="status"; shift; break ;;
|
|
443
|
+
logs)
|
|
444
|
+
SUBCOMMAND="logs"; shift
|
|
445
|
+
LOGS_AGENT="${1:-}"
|
|
446
|
+
[ -n "$LOGS_AGENT" ] && shift
|
|
447
|
+
break
|
|
448
|
+
;;
|
|
449
|
+
abort)
|
|
450
|
+
SUBCOMMAND="abort"; shift; break ;;
|
|
451
|
+
clean)
|
|
452
|
+
SUBCOMMAND="clean"; shift; break ;;
|
|
453
|
+
-*)
|
|
454
|
+
log_error "Unknown option: $1"
|
|
455
|
+
show_usage; exit 1 ;;
|
|
456
|
+
*)
|
|
457
|
+
# No subcommand — collect everything as prompt args
|
|
458
|
+
PROMPT_ARGS+=("$1")
|
|
459
|
+
shift
|
|
460
|
+
;;
|
|
461
|
+
esac
|
|
462
|
+
done
|
|
463
|
+
|
|
464
|
+
# Join prompt args into a single string
|
|
465
|
+
if [ ${#PROMPT_ARGS[@]} -gt 0 ]; then
|
|
466
|
+
USER_PROMPT="${PROMPT_ARGS[*]}"
|
|
467
|
+
fi
|
|
468
|
+
|
|
469
|
+
# Re-export updated values after argument parsing
|
|
470
|
+
export MAX_WORKERS MAX_LEADS AUDIT_ROUNDS TIMEOUT_MINUTES DRY_RUN VERBOSE
|
|
471
|
+
export FORGE_TIMEOUT AUTO_APPROVE
|
|
472
|
+
|
|
473
|
+
###############################################################################
|
|
474
|
+
# Main pipeline
|
|
475
|
+
###############################################################################
|
|
476
|
+
run_pipeline() {
|
|
477
|
+
local user_prompt="$1"
|
|
478
|
+
local truncated_prompt="${user_prompt:0:72}"
|
|
479
|
+
|
|
480
|
+
echo ""
|
|
481
|
+
printf "${BOLD}═══════════════════════════════════════${RESET}\n"
|
|
482
|
+
printf "${BOLD} FORGE${RESET}\n"
|
|
483
|
+
printf " ${truncated_prompt}\n"
|
|
484
|
+
printf "${BOLD}═══════════════════════════════════════${RESET}\n"
|
|
485
|
+
echo ""
|
|
486
|
+
|
|
487
|
+
# Initialize
|
|
488
|
+
FORGE_ROOT="$(get_forge_root)"
|
|
489
|
+
export FORGE_ROOT
|
|
490
|
+
|
|
491
|
+
mkdir -p "$FORGE_ROOT/.forge/status"
|
|
492
|
+
mkdir -p "$FORGE_ROOT/.forge/logs"
|
|
493
|
+
|
|
494
|
+
# Preflight
|
|
495
|
+
preflight_check
|
|
496
|
+
|
|
497
|
+
# Phase 0: Spec Creation
|
|
498
|
+
run_phase_spec "$user_prompt" || { log_error "Phase 0 failed"; exit 1; }
|
|
499
|
+
check_global_timeout
|
|
500
|
+
|
|
501
|
+
# Phase 1: Planning
|
|
502
|
+
run_phase_plan || { log_error "Phase 1 failed"; exit 1; }
|
|
503
|
+
check_global_timeout
|
|
504
|
+
|
|
505
|
+
# Dry run stops here
|
|
506
|
+
if [ "$DRY_RUN" = true ]; then
|
|
507
|
+
log_success "Dry run complete. Check .forge/spec/spec.md and .forge/plan.json"
|
|
508
|
+
exit 0
|
|
509
|
+
fi
|
|
510
|
+
|
|
511
|
+
# ── Approval gate ─────────────────────────────────────────────
|
|
512
|
+
if [ "$AUTO_APPROVE" = true ]; then
|
|
513
|
+
log_info "Auto-approve enabled, skipping review."
|
|
514
|
+
else
|
|
515
|
+
show_plan_summary "$FORGE_ROOT"
|
|
516
|
+
wait_for_approval
|
|
517
|
+
fi
|
|
518
|
+
|
|
519
|
+
# ── From here on, fully autonomous ────────────────────────────
|
|
520
|
+
|
|
521
|
+
# Phase 2: Implementation
|
|
522
|
+
run_phase_implement || { log_error "Phase 2 failed"; exit 1; }
|
|
523
|
+
check_global_timeout
|
|
524
|
+
|
|
525
|
+
# Phase 3: Integration
|
|
526
|
+
run_phase_integrate || { log_error "Phase 3 failed"; exit 1; }
|
|
527
|
+
check_global_timeout
|
|
528
|
+
|
|
529
|
+
# Phase 4-5: Audit + Fix cycles
|
|
530
|
+
for round in $(seq 1 "$AUDIT_ROUNDS"); do
|
|
531
|
+
run_phase_audit "$round" || { log_warn "Audit round $round had issues"; }
|
|
532
|
+
check_global_timeout
|
|
533
|
+
|
|
534
|
+
local critical_count warning_count
|
|
535
|
+
critical_count=$(jq '[.findings[] | select(.severity == "critical")] | length' \
|
|
536
|
+
"$FORGE_ROOT/.forge/audit/consolidated-audit.json" 2>/dev/null || echo "0")
|
|
537
|
+
warning_count=$(jq '[.findings[] | select(.severity == "warning")] | length' \
|
|
538
|
+
"$FORGE_ROOT/.forge/audit/consolidated-audit.json" 2>/dev/null || echo "0")
|
|
539
|
+
|
|
540
|
+
if [ "$critical_count" -eq 0 ] && [ "$warning_count" -eq 0 ]; then
|
|
541
|
+
log_success "Clean audit in round $round"
|
|
542
|
+
break
|
|
543
|
+
fi
|
|
544
|
+
|
|
545
|
+
run_phase_fix "$round" || { log_warn "Fix round $round had issues"; }
|
|
546
|
+
check_global_timeout
|
|
547
|
+
done
|
|
548
|
+
|
|
549
|
+
# Phase 6: Finalize
|
|
550
|
+
run_phase_finalize "$user_prompt" || { log_error "Phase 6 failed"; exit 1; }
|
|
551
|
+
|
|
552
|
+
log_success "Done. Read .forge/DONE.md"
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
###############################################################################
|
|
556
|
+
# Entry point
|
|
557
|
+
###############################################################################
|
|
558
|
+
|
|
559
|
+
# No arguments at all → show help
|
|
560
|
+
if [ -z "${SUBCOMMAND:-}" ] && [ -z "${USER_PROMPT:-}" ]; then
|
|
561
|
+
show_usage
|
|
562
|
+
exit 0
|
|
563
|
+
fi
|
|
564
|
+
|
|
565
|
+
# Dispatch subcommands
|
|
566
|
+
case "${SUBCOMMAND:-}" in
|
|
567
|
+
init)
|
|
568
|
+
forge_init "${INIT_PROJECT:-}"
|
|
569
|
+
;;
|
|
570
|
+
status)
|
|
571
|
+
forge_status
|
|
572
|
+
;;
|
|
573
|
+
logs)
|
|
574
|
+
forge_logs "${LOGS_AGENT:-}"
|
|
575
|
+
;;
|
|
576
|
+
abort)
|
|
577
|
+
forge_abort
|
|
578
|
+
;;
|
|
579
|
+
clean)
|
|
580
|
+
forge_clean
|
|
581
|
+
;;
|
|
582
|
+
start|run)
|
|
583
|
+
if [ -z "${USER_PROMPT:-}" ]; then
|
|
584
|
+
log_error "No task provided. Usage: forge start <your task description>"
|
|
585
|
+
exit 1
|
|
586
|
+
fi
|
|
587
|
+
run_pipeline "$USER_PROMPT"
|
|
588
|
+
;;
|
|
589
|
+
"")
|
|
590
|
+
# No subcommand — bare prompt
|
|
591
|
+
run_pipeline "$USER_PROMPT"
|
|
592
|
+
;;
|
|
593
|
+
esac
|