shipwright-cli 2.2.1 → 2.3.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 +19 -19
- package/dashboard/public/index.html +224 -8
- package/dashboard/public/styles.css +1078 -4
- package/dashboard/server.ts +1100 -15
- package/dashboard/src/canvas/interactions.ts +74 -0
- package/dashboard/src/canvas/layout.ts +85 -0
- package/dashboard/src/canvas/overlays.ts +117 -0
- package/dashboard/src/canvas/particles.ts +105 -0
- package/dashboard/src/canvas/renderer.ts +191 -0
- package/dashboard/src/components/charts/bar.ts +54 -0
- package/dashboard/src/components/charts/donut.ts +25 -0
- package/dashboard/src/components/charts/pipeline-rail.ts +105 -0
- package/dashboard/src/components/charts/sparkline.ts +82 -0
- package/dashboard/src/components/header.ts +616 -0
- package/dashboard/src/components/modal.ts +413 -0
- package/dashboard/src/components/terminal.ts +144 -0
- package/dashboard/src/core/api.ts +381 -0
- package/dashboard/src/core/helpers.ts +118 -0
- package/dashboard/src/core/router.ts +190 -0
- package/dashboard/src/core/sse.ts +38 -0
- package/dashboard/src/core/state.ts +150 -0
- package/dashboard/src/core/ws.ts +143 -0
- package/dashboard/src/design/icons.ts +131 -0
- package/dashboard/src/design/tokens.ts +160 -0
- package/dashboard/src/main.ts +68 -0
- package/dashboard/src/types/api.ts +337 -0
- package/dashboard/src/views/activity.ts +185 -0
- package/dashboard/src/views/agent-cockpit.ts +236 -0
- package/dashboard/src/views/agents.ts +72 -0
- package/dashboard/src/views/fleet-map.ts +299 -0
- package/dashboard/src/views/insights.ts +298 -0
- package/dashboard/src/views/machines.ts +162 -0
- package/dashboard/src/views/metrics.ts +420 -0
- package/dashboard/src/views/overview.ts +409 -0
- package/dashboard/src/views/pipeline-theater.ts +219 -0
- package/dashboard/src/views/pipelines.ts +595 -0
- package/dashboard/src/views/team.ts +362 -0
- package/dashboard/src/views/timeline.ts +389 -0
- package/dashboard/tsconfig.json +21 -0
- package/docs/AGI-PLATFORM-PLAN.md +5 -5
- package/docs/AGI-WHATS-NEXT.md +19 -16
- package/docs/README.md +2 -0
- package/package.json +8 -1
- package/scripts/check-version-consistency.sh +72 -0
- package/scripts/lib/daemon-adaptive.sh +610 -0
- package/scripts/lib/daemon-dispatch.sh +489 -0
- package/scripts/lib/daemon-failure.sh +387 -0
- package/scripts/lib/daemon-patrol.sh +1113 -0
- package/scripts/lib/daemon-poll.sh +1202 -0
- package/scripts/lib/daemon-state.sh +550 -0
- package/scripts/lib/daemon-triage.sh +490 -0
- package/scripts/lib/helpers.sh +81 -0
- package/scripts/lib/pipeline-intelligence.sh +0 -6
- package/scripts/lib/pipeline-quality-checks.sh +3 -1
- package/scripts/lib/pipeline-stages.sh +20 -0
- package/scripts/sw +109 -168
- package/scripts/sw-activity.sh +1 -1
- package/scripts/sw-adaptive.sh +2 -2
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +14 -6
- package/scripts/sw-autonomous.sh +1 -1
- package/scripts/sw-changelog.sh +2 -2
- package/scripts/sw-checkpoint.sh +1 -1
- package/scripts/sw-ci.sh +1 -1
- package/scripts/sw-cleanup.sh +1 -1
- package/scripts/sw-code-review.sh +1 -1
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +1 -1
- package/scripts/sw-cost.sh +1 -1
- package/scripts/sw-daemon.sh +53 -4817
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +1 -1
- package/scripts/sw-decompose.sh +1 -1
- package/scripts/sw-deps.sh +1 -1
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +1 -1
- package/scripts/sw-doc-fleet.sh +1 -1
- package/scripts/sw-docs-agent.sh +1 -1
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +49 -1
- package/scripts/sw-dora.sh +1 -1
- package/scripts/sw-durable.sh +1 -1
- package/scripts/sw-e2e-orchestrator.sh +1 -1
- package/scripts/sw-eventbus.sh +1 -1
- package/scripts/sw-feedback.sh +1 -1
- package/scripts/sw-fix.sh +6 -5
- package/scripts/sw-fleet-discover.sh +1 -1
- package/scripts/sw-fleet-viz.sh +3 -3
- package/scripts/sw-fleet.sh +1 -1
- package/scripts/sw-github-app.sh +5 -2
- package/scripts/sw-github-checks.sh +1 -1
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +1 -1
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +1 -1
- package/scripts/sw-incident.sh +1 -1
- package/scripts/sw-init.sh +112 -9
- package/scripts/sw-instrument.sh +6 -1
- package/scripts/sw-intelligence.sh +5 -1
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +1 -1
- package/scripts/sw-linear.sh +20 -9
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +2 -1
- package/scripts/sw-memory.sh +10 -1
- package/scripts/sw-mission-control.sh +1 -1
- package/scripts/sw-model-router.sh +4 -1
- package/scripts/sw-otel.sh +4 -4
- package/scripts/sw-oversight.sh +1 -1
- package/scripts/sw-pipeline-composer.sh +3 -1
- package/scripts/sw-pipeline-vitals.sh +4 -6
- package/scripts/sw-pipeline.sh +19 -56
- package/scripts/sw-pipeline.sh.mock +7 -0
- package/scripts/sw-pm.sh +5 -2
- package/scripts/sw-pr-lifecycle.sh +1 -1
- package/scripts/sw-predictive.sh +4 -1
- package/scripts/sw-prep.sh +3 -2
- package/scripts/sw-ps.sh +1 -1
- package/scripts/sw-public-dashboard.sh +10 -4
- package/scripts/sw-quality.sh +1 -1
- package/scripts/sw-reaper.sh +1 -1
- package/scripts/sw-recruit.sh +25 -1
- package/scripts/sw-regression.sh +2 -1
- package/scripts/sw-release-manager.sh +1 -1
- package/scripts/sw-release.sh +7 -5
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +1 -1
- package/scripts/sw-retro.sh +1 -1
- package/scripts/sw-scale.sh +11 -5
- package/scripts/sw-security-audit.sh +1 -1
- package/scripts/sw-self-optimize.sh +172 -7
- package/scripts/sw-session.sh +1 -1
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +4 -3
- package/scripts/sw-status.sh +1 -1
- package/scripts/sw-strategic.sh +2 -1
- package/scripts/sw-stream.sh +8 -2
- package/scripts/sw-swarm.sh +12 -10
- package/scripts/sw-team-stages.sh +1 -1
- package/scripts/sw-templates.sh +1 -1
- package/scripts/sw-testgen.sh +3 -2
- package/scripts/sw-tmux-pipeline.sh +2 -1
- package/scripts/sw-tmux.sh +1 -1
- package/scripts/sw-trace.sh +1 -1
- package/scripts/sw-tracker-jira.sh +1 -0
- package/scripts/sw-tracker-linear.sh +1 -0
- package/scripts/sw-tracker.sh +24 -6
- package/scripts/sw-triage.sh +1 -1
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +1 -1
- package/scripts/sw-webhook.sh +1 -1
- package/scripts/sw-widgets.sh +2 -2
- package/scripts/sw-worktree.sh +1 -1
- package/dashboard/public/app.js +0 -4422
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
# daemon-dispatch.sh — Spawn, reap, on_success (for sw-daemon.sh)
|
|
2
|
+
# Source from sw-daemon.sh. Requires state, failure, helpers.
|
|
3
|
+
[[ -n "${_DAEMON_DISPATCH_LOADED:-}" ]] && return 0
|
|
4
|
+
_DAEMON_DISPATCH_LOADED=1
|
|
5
|
+
|
|
6
|
+
# ─── Org-Wide Repo Management ─────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
daemon_ensure_repo() {
|
|
9
|
+
local owner="$1" repo="$2"
|
|
10
|
+
local repo_dir="$DAEMON_DIR/repos/${owner}/${repo}"
|
|
11
|
+
|
|
12
|
+
if [[ -d "$repo_dir/.git" ]]; then
|
|
13
|
+
# Pull latest
|
|
14
|
+
(cd "$repo_dir" && git pull --ff-only 2>/dev/null) || {
|
|
15
|
+
daemon_log WARN "Failed to update ${owner}/${repo} — using existing clone"
|
|
16
|
+
}
|
|
17
|
+
else
|
|
18
|
+
mkdir -p "$DAEMON_DIR/repos/${owner}"
|
|
19
|
+
if ! git clone --depth=1 "https://github.com/${owner}/${repo}.git" "$repo_dir" 2>/dev/null; then
|
|
20
|
+
daemon_log ERROR "Failed to clone ${owner}/${repo}"
|
|
21
|
+
return 1
|
|
22
|
+
fi
|
|
23
|
+
daemon_log INFO "Cloned ${owner}/${repo} to ${repo_dir}"
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
echo "$repo_dir"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# ─── Spawn Pipeline ─────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
daemon_spawn_pipeline() {
|
|
32
|
+
local issue_num="$1"
|
|
33
|
+
local issue_title="${2:-}"
|
|
34
|
+
local repo_full_name="${3:-}" # owner/repo (org mode only)
|
|
35
|
+
shift 3 2>/dev/null || true
|
|
36
|
+
local extra_pipeline_args=("$@") # Optional extra args passed to sw-pipeline.sh
|
|
37
|
+
|
|
38
|
+
daemon_log INFO "Spawning pipeline for issue #${issue_num}: ${issue_title}"
|
|
39
|
+
|
|
40
|
+
# ── Issue decomposition (if decomposer available) ──
|
|
41
|
+
local decompose_script="${SCRIPT_DIR}/sw-decompose.sh"
|
|
42
|
+
if [[ -x "$decompose_script" && "$NO_GITHUB" != "true" ]]; then
|
|
43
|
+
local decompose_result=""
|
|
44
|
+
decompose_result=$("$decompose_script" auto "$issue_num" 2>/dev/null) || true
|
|
45
|
+
if [[ "$decompose_result" == *"decomposed"* ]]; then
|
|
46
|
+
daemon_log INFO "Issue #${issue_num} decomposed into subtasks — skipping pipeline"
|
|
47
|
+
# Remove the shipwright label so decomposed parent doesn't re-queue
|
|
48
|
+
gh issue edit "$issue_num" --remove-label "shipwright" 2>/dev/null || true
|
|
49
|
+
return 0
|
|
50
|
+
fi
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Extract goal text from issue (title + first line of body)
|
|
54
|
+
local issue_goal="$issue_title"
|
|
55
|
+
if [[ "$NO_GITHUB" != "true" ]]; then
|
|
56
|
+
local issue_body_first
|
|
57
|
+
issue_body_first=$(gh issue view "$issue_num" --json body --jq '.body' 2>/dev/null | head -3 | tr '\n' ' ' | cut -c1-200 || true)
|
|
58
|
+
if [[ -n "$issue_body_first" ]]; then
|
|
59
|
+
issue_goal="${issue_title}: ${issue_body_first}"
|
|
60
|
+
fi
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# ── Predictive risk assessment (if enabled) ──
|
|
64
|
+
if [[ "${PREDICTION_ENABLED:-false}" == "true" ]] && type predict_pipeline_risk &>/dev/null 2>&1; then
|
|
65
|
+
local issue_json_for_pred=""
|
|
66
|
+
if [[ "$NO_GITHUB" != "true" ]]; then
|
|
67
|
+
issue_json_for_pred=$(gh issue view "$issue_num" --json number,title,body,labels 2>/dev/null || echo "")
|
|
68
|
+
fi
|
|
69
|
+
if [[ -n "$issue_json_for_pred" ]]; then
|
|
70
|
+
local risk_result
|
|
71
|
+
risk_result=$(predict_pipeline_risk "$issue_json_for_pred" "" 2>/dev/null || echo "")
|
|
72
|
+
if [[ -n "$risk_result" ]]; then
|
|
73
|
+
local overall_risk
|
|
74
|
+
overall_risk=$(echo "$risk_result" | jq -r '.overall_risk // 50' 2>/dev/null || echo "50")
|
|
75
|
+
if [[ "$overall_risk" -gt 80 ]]; then
|
|
76
|
+
daemon_log WARN "HIGH RISK (${overall_risk}%) predicted for issue #${issue_num} — upgrading model"
|
|
77
|
+
export CLAUDE_MODEL="opus"
|
|
78
|
+
elif [[ "$overall_risk" -lt 30 ]]; then
|
|
79
|
+
daemon_log INFO "LOW RISK (${overall_risk}%) predicted for issue #${issue_num}"
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# Check disk space before spawning
|
|
86
|
+
local free_space_kb
|
|
87
|
+
free_space_kb=$(df -k "." 2>/dev/null | tail -1 | awk '{print $4}')
|
|
88
|
+
if [[ -n "$free_space_kb" ]] && [[ "$free_space_kb" -lt 1048576 ]] 2>/dev/null; then
|
|
89
|
+
daemon_log WARN "Low disk space ($(( free_space_kb / 1024 ))MB) — skipping issue #${issue_num}"
|
|
90
|
+
return 1
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
local work_dir="" branch_name="daemon/issue-${issue_num}"
|
|
94
|
+
|
|
95
|
+
if [[ "$WATCH_MODE" == "org" && -n "$repo_full_name" ]]; then
|
|
96
|
+
# Org mode: use cloned repo directory
|
|
97
|
+
local owner="${repo_full_name%%/*}"
|
|
98
|
+
local repo="${repo_full_name##*/}"
|
|
99
|
+
work_dir=$(daemon_ensure_repo "$owner" "$repo") || return 1
|
|
100
|
+
|
|
101
|
+
# Create branch in the cloned repo
|
|
102
|
+
(
|
|
103
|
+
cd "$work_dir"
|
|
104
|
+
git checkout -B "$branch_name" "${BASE_BRANCH}" 2>/dev/null
|
|
105
|
+
) || {
|
|
106
|
+
daemon_log ERROR "Failed to create branch in ${repo_full_name}"
|
|
107
|
+
return 1
|
|
108
|
+
}
|
|
109
|
+
daemon_log INFO "Org mode: working in ${work_dir} (${repo_full_name})"
|
|
110
|
+
else
|
|
111
|
+
# Standard mode: use git worktree
|
|
112
|
+
work_dir="${WORKTREE_DIR}/daemon-issue-${issue_num}"
|
|
113
|
+
|
|
114
|
+
# Serialize worktree operations with a lock file (run in subshell to auto-close FD)
|
|
115
|
+
mkdir -p "$WORKTREE_DIR"
|
|
116
|
+
local wt_ok=0
|
|
117
|
+
(
|
|
118
|
+
flock -w 30 200 2>/dev/null || true
|
|
119
|
+
|
|
120
|
+
# Clean up stale worktree if it exists
|
|
121
|
+
if [[ -d "$work_dir" ]]; then
|
|
122
|
+
git worktree remove "$work_dir" --force 2>/dev/null || true
|
|
123
|
+
fi
|
|
124
|
+
git branch -D "$branch_name" 2>/dev/null || true
|
|
125
|
+
|
|
126
|
+
git worktree add "$work_dir" -b "$branch_name" "$BASE_BRANCH" 2>/dev/null
|
|
127
|
+
) 200>"${WORKTREE_DIR}/.worktree.lock"
|
|
128
|
+
wt_ok=$?
|
|
129
|
+
|
|
130
|
+
if [[ $wt_ok -ne 0 ]]; then
|
|
131
|
+
daemon_log ERROR "Failed to create worktree for issue #${issue_num}"
|
|
132
|
+
return 1
|
|
133
|
+
fi
|
|
134
|
+
daemon_log INFO "Worktree created at ${work_dir}"
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# If template is "composed", copy the composed spec into the worktree
|
|
138
|
+
if [[ "$PIPELINE_TEMPLATE" == "composed" ]]; then
|
|
139
|
+
local _src_composed="${REPO_DIR:-.}/.claude/pipeline-artifacts/composed-pipeline.json"
|
|
140
|
+
if [[ -f "$_src_composed" ]]; then
|
|
141
|
+
local _dst_artifacts="${work_dir}/.claude/pipeline-artifacts"
|
|
142
|
+
mkdir -p "$_dst_artifacts"
|
|
143
|
+
cp "$_src_composed" "$_dst_artifacts/composed-pipeline.json" 2>/dev/null || true
|
|
144
|
+
daemon_log INFO "Copied composed pipeline spec to worktree"
|
|
145
|
+
fi
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# Build pipeline args
|
|
149
|
+
local pipeline_args=("start" "--issue" "$issue_num" "--pipeline" "$PIPELINE_TEMPLATE")
|
|
150
|
+
if [[ "$SKIP_GATES" == "true" ]]; then
|
|
151
|
+
pipeline_args+=("--skip-gates")
|
|
152
|
+
fi
|
|
153
|
+
if [[ -n "$MODEL" ]]; then
|
|
154
|
+
pipeline_args+=("--model" "$MODEL")
|
|
155
|
+
fi
|
|
156
|
+
if [[ "$NO_GITHUB" == "true" ]]; then
|
|
157
|
+
pipeline_args+=("--no-github")
|
|
158
|
+
fi
|
|
159
|
+
# Pass session restart config
|
|
160
|
+
if [[ "${MAX_RESTARTS_CFG:-0}" -gt 0 ]]; then
|
|
161
|
+
pipeline_args+=("--max-restarts" "$MAX_RESTARTS_CFG")
|
|
162
|
+
fi
|
|
163
|
+
# Pass fast test command
|
|
164
|
+
if [[ -n "${FAST_TEST_CMD_CFG:-}" ]]; then
|
|
165
|
+
pipeline_args+=("--fast-test-cmd" "$FAST_TEST_CMD_CFG")
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
# Append any extra pipeline args (from retry escalation, etc.)
|
|
169
|
+
if [[ ${#extra_pipeline_args[@]} -gt 0 ]]; then
|
|
170
|
+
pipeline_args+=("${extra_pipeline_args[@]}")
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
# Run pipeline in work directory (background)
|
|
174
|
+
# Ignore SIGHUP so tmux attach/detach and process group changes don't kill the pipeline
|
|
175
|
+
echo -e "\n\n===== Pipeline run $(date -u +%Y-%m-%dT%H:%M:%SZ) =====" >> "$LOG_DIR/issue-${issue_num}.log" 2>/dev/null || true
|
|
176
|
+
(
|
|
177
|
+
trap '' HUP
|
|
178
|
+
cd "$work_dir"
|
|
179
|
+
exec "$SCRIPT_DIR/sw-pipeline.sh" "${pipeline_args[@]}"
|
|
180
|
+
) >> "$LOG_DIR/issue-${issue_num}.log" 2>&1 200>&- &
|
|
181
|
+
local pid=$!
|
|
182
|
+
|
|
183
|
+
daemon_log INFO "Pipeline started for issue #${issue_num} (PID: ${pid})"
|
|
184
|
+
|
|
185
|
+
# Track the job (include repo and goal for org mode)
|
|
186
|
+
daemon_track_job "$issue_num" "$pid" "$work_dir" "$issue_title" "$repo_full_name" "$issue_goal"
|
|
187
|
+
emit_event "daemon.spawn" "issue=$issue_num" "pid=$pid" "repo=${repo_full_name:-local}"
|
|
188
|
+
"$SCRIPT_DIR/sw-tracker.sh" notify "spawn" "$issue_num" 2>/dev/null || true
|
|
189
|
+
|
|
190
|
+
# Comment on the issue
|
|
191
|
+
if [[ "$NO_GITHUB" != "true" ]]; then
|
|
192
|
+
local gh_args=()
|
|
193
|
+
if [[ -n "$repo_full_name" ]]; then
|
|
194
|
+
gh_args+=("--repo" "$repo_full_name")
|
|
195
|
+
fi
|
|
196
|
+
gh issue comment "$issue_num" ${gh_args[@]+"${gh_args[@]}"} --body "## 🤖 Pipeline Started
|
|
197
|
+
|
|
198
|
+
**Delivering:** ${issue_title}
|
|
199
|
+
|
|
200
|
+
| Field | Value |
|
|
201
|
+
|-------|-------|
|
|
202
|
+
| Template | \`${PIPELINE_TEMPLATE}\` |
|
|
203
|
+
| Branch | \`${branch_name}\` |
|
|
204
|
+
| Repo | \`${repo_full_name:-local}\` |
|
|
205
|
+
| Started | $(now_iso) |
|
|
206
|
+
|
|
207
|
+
_Progress updates will appear below as the pipeline advances through each stage._" 2>/dev/null || true
|
|
208
|
+
fi
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# ─── Track Job ───────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
daemon_track_job() {
|
|
214
|
+
local issue_num="$1" pid="$2" worktree="$3" title="${4:-}" repo="${5:-}" goal="${6:-}"
|
|
215
|
+
|
|
216
|
+
# Write to SQLite (non-blocking, best-effort)
|
|
217
|
+
if type db_save_job &>/dev/null; then
|
|
218
|
+
local job_id="daemon-${issue_num}-$(now_epoch)"
|
|
219
|
+
db_save_job "$job_id" "$issue_num" "$title" "$pid" "$worktree" "" "${PIPELINE_TEMPLATE:-autonomous}" "$goal" 2>/dev/null || true
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
# Always write to JSON state file (primary for now)
|
|
223
|
+
locked_state_update \
|
|
224
|
+
--argjson num "$issue_num" \
|
|
225
|
+
--argjson pid "$pid" \
|
|
226
|
+
--arg wt "$worktree" \
|
|
227
|
+
--arg title "$title" \
|
|
228
|
+
--arg started "$(now_iso)" \
|
|
229
|
+
--arg repo "$repo" \
|
|
230
|
+
--arg goal "$goal" \
|
|
231
|
+
'.active_jobs += [{
|
|
232
|
+
issue: $num,
|
|
233
|
+
pid: $pid,
|
|
234
|
+
worktree: $wt,
|
|
235
|
+
title: $title,
|
|
236
|
+
started_at: $started,
|
|
237
|
+
repo: $repo,
|
|
238
|
+
goal: $goal
|
|
239
|
+
}]'
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
# ─── Reap Completed Jobs ────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
daemon_reap_completed() {
|
|
245
|
+
if [[ ! -f "$STATE_FILE" ]]; then
|
|
246
|
+
return
|
|
247
|
+
fi
|
|
248
|
+
|
|
249
|
+
local jobs
|
|
250
|
+
jobs=$(jq -c '.active_jobs[]' "$STATE_FILE" 2>/dev/null || true)
|
|
251
|
+
if [[ -z "$jobs" ]]; then
|
|
252
|
+
return
|
|
253
|
+
fi
|
|
254
|
+
|
|
255
|
+
local _retry_spawned_for=""
|
|
256
|
+
|
|
257
|
+
while IFS= read -r job; do
|
|
258
|
+
local issue_num pid worktree
|
|
259
|
+
issue_num=$(echo "$job" | jq -r '.issue // empty')
|
|
260
|
+
pid=$(echo "$job" | jq -r '.pid // empty')
|
|
261
|
+
worktree=$(echo "$job" | jq -r '.worktree // empty')
|
|
262
|
+
|
|
263
|
+
# Skip malformed entries (corrupted state file)
|
|
264
|
+
[[ -z "$issue_num" || ! "$issue_num" =~ ^[0-9]+$ ]] && continue
|
|
265
|
+
[[ -z "$pid" || ! "$pid" =~ ^[0-9]+$ ]] && continue
|
|
266
|
+
|
|
267
|
+
# Check if process is still running
|
|
268
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
269
|
+
continue
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
# Process is dead — determine exit code
|
|
273
|
+
# Note: wait returns 127 if process was already reaped (e.g., by init)
|
|
274
|
+
# In that case, check pipeline log for success/failure indicators
|
|
275
|
+
local exit_code=0
|
|
276
|
+
wait "$pid" 2>/dev/null || exit_code=$?
|
|
277
|
+
if [[ "$exit_code" -eq 127 ]]; then
|
|
278
|
+
# Process already reaped — check log file for real outcome
|
|
279
|
+
local issue_log="$LOG_DIR/issue-${issue_num}.log"
|
|
280
|
+
if [[ -f "$issue_log" ]]; then
|
|
281
|
+
if grep -q "Pipeline completed successfully" "$issue_log" 2>/dev/null; then
|
|
282
|
+
exit_code=0
|
|
283
|
+
elif grep -q "Pipeline failed\|ERROR.*stage.*failed\|exited with status" "$issue_log" 2>/dev/null; then
|
|
284
|
+
exit_code=1
|
|
285
|
+
else
|
|
286
|
+
daemon_log WARN "Could not determine exit code for issue #${issue_num} (PID ${pid} already reaped) — marking as failure"
|
|
287
|
+
exit_code=1
|
|
288
|
+
fi
|
|
289
|
+
else
|
|
290
|
+
exit_code=1
|
|
291
|
+
fi
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
local started_at duration_str="" start_epoch=0 end_epoch=0
|
|
295
|
+
started_at=$(echo "$job" | jq -r '.started_at // empty')
|
|
296
|
+
if [[ -n "$started_at" ]]; then
|
|
297
|
+
# macOS date -j for parsing ISO dates (TZ=UTC to parse Z-suffix correctly)
|
|
298
|
+
start_epoch=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$started_at" +%s 2>/dev/null || date -d "$started_at" +%s 2>/dev/null || echo "0")
|
|
299
|
+
end_epoch=$(now_epoch)
|
|
300
|
+
if [[ "$start_epoch" -gt 0 ]]; then
|
|
301
|
+
duration_str=$(format_duration $((end_epoch - start_epoch)))
|
|
302
|
+
fi
|
|
303
|
+
fi
|
|
304
|
+
|
|
305
|
+
local result_str="success"
|
|
306
|
+
[[ "$exit_code" -ne 0 ]] && result_str="failure"
|
|
307
|
+
local dur_s=0
|
|
308
|
+
[[ "$start_epoch" -gt 0 ]] && dur_s=$((end_epoch - start_epoch))
|
|
309
|
+
emit_event "daemon.reap" "issue=$issue_num" "result=$result_str" "duration_s=$dur_s"
|
|
310
|
+
|
|
311
|
+
# Update SQLite (mark job complete/failed)
|
|
312
|
+
if type db_complete_job &>/dev/null && type db_fail_job &>/dev/null; then
|
|
313
|
+
local _db_job_id="daemon-${issue_num}-${start_epoch}"
|
|
314
|
+
if [[ "$exit_code" -eq 0 ]]; then
|
|
315
|
+
db_complete_job "$_db_job_id" "$result_str" 2>/dev/null || true
|
|
316
|
+
else
|
|
317
|
+
db_fail_job "$_db_job_id" "$result_str" 2>/dev/null || true
|
|
318
|
+
fi
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
if [[ "$exit_code" -eq 0 ]]; then
|
|
322
|
+
daemon_on_success "$issue_num" "$duration_str"
|
|
323
|
+
else
|
|
324
|
+
daemon_on_failure "$issue_num" "$exit_code" "$duration_str"
|
|
325
|
+
|
|
326
|
+
# Cancel any lingering in_progress GitHub Check Runs for failed job
|
|
327
|
+
if [[ "${NO_GITHUB:-false}" != "true" && -n "$worktree" ]]; then
|
|
328
|
+
local check_ids_file="${worktree}/.claude/pipeline-artifacts/check-run-ids.json"
|
|
329
|
+
if [[ -f "$check_ids_file" ]]; then
|
|
330
|
+
daemon_log INFO "Cancelling in-progress check runs for issue #${issue_num}"
|
|
331
|
+
local _stage
|
|
332
|
+
while IFS= read -r _stage; do
|
|
333
|
+
[[ -z "$_stage" ]] && continue
|
|
334
|
+
# Direct API call since we're in daemon context
|
|
335
|
+
local _run_id
|
|
336
|
+
_run_id=$(jq -r --arg s "$_stage" '.[$s] // empty' "$check_ids_file" 2>/dev/null || true)
|
|
337
|
+
if [[ -n "$_run_id" && "$_run_id" != "null" ]]; then
|
|
338
|
+
local _detected
|
|
339
|
+
_detected=$(git remote get-url origin 2>/dev/null | sed 's|.*github.com[:/]\(.*\)\.git$|\1|' || true)
|
|
340
|
+
if [[ -n "$_detected" ]]; then
|
|
341
|
+
local _owner="${_detected%%/*}" _repo="${_detected##*/}"
|
|
342
|
+
gh api "repos/${_owner}/${_repo}/check-runs/${_run_id}" \
|
|
343
|
+
--method PATCH \
|
|
344
|
+
--field status=completed \
|
|
345
|
+
--field conclusion=cancelled \
|
|
346
|
+
--silent 2>/dev/null || true
|
|
347
|
+
fi
|
|
348
|
+
fi
|
|
349
|
+
done < <(jq -r 'keys[]' "$check_ids_file" 2>/dev/null || true)
|
|
350
|
+
fi
|
|
351
|
+
fi
|
|
352
|
+
fi
|
|
353
|
+
|
|
354
|
+
# Finalize memory (capture failure patterns for future runs)
|
|
355
|
+
if type memory_finalize_pipeline &>/dev/null 2>&1; then
|
|
356
|
+
local _job_state _job_artifacts
|
|
357
|
+
_job_state="${worktree:-.}/.claude/pipeline-state.md"
|
|
358
|
+
_job_artifacts="${worktree:-.}/.claude/pipeline-artifacts"
|
|
359
|
+
memory_finalize_pipeline "$_job_state" "$_job_artifacts" 2>/dev/null || true
|
|
360
|
+
fi
|
|
361
|
+
|
|
362
|
+
# Clean up progress tracking for this job
|
|
363
|
+
daemon_clear_progress "$issue_num"
|
|
364
|
+
|
|
365
|
+
# Release claim lock (label-based coordination)
|
|
366
|
+
local reap_machine_name
|
|
367
|
+
reap_machine_name=$(jq -r '.machines[] | select(.role == "primary") | .name' "$HOME/.shipwright/machines.json" 2>/dev/null || hostname -s)
|
|
368
|
+
release_claim "$issue_num" "$reap_machine_name"
|
|
369
|
+
|
|
370
|
+
# Always remove the OLD job entry from active_jobs to prevent
|
|
371
|
+
# re-reaping of the dead PID on the next cycle. When a retry was
|
|
372
|
+
# spawned, daemon_spawn_pipeline already added a fresh entry with
|
|
373
|
+
# the new PID — we must not leave the stale one behind.
|
|
374
|
+
locked_state_update --argjson num "$issue_num" \
|
|
375
|
+
--argjson old_pid "${pid:-0}" \
|
|
376
|
+
'.active_jobs = [.active_jobs[] | select(.issue != $num or .pid != $old_pid)]'
|
|
377
|
+
untrack_priority_job "$issue_num"
|
|
378
|
+
|
|
379
|
+
if [[ "$_retry_spawned_for" == "$issue_num" ]]; then
|
|
380
|
+
daemon_log INFO "Retry spawned for issue #${issue_num} — skipping worktree cleanup"
|
|
381
|
+
else
|
|
382
|
+
# Clean up worktree (skip for org-mode clones — they persist)
|
|
383
|
+
local job_repo
|
|
384
|
+
job_repo=$(echo "$job" | jq -r '.repo // ""')
|
|
385
|
+
if [[ -z "$job_repo" ]] && [[ -d "$worktree" ]]; then
|
|
386
|
+
git worktree remove "$worktree" --force 2>/dev/null || true
|
|
387
|
+
daemon_log INFO "Cleaned worktree: $worktree"
|
|
388
|
+
git branch -D "daemon/issue-${issue_num}" 2>/dev/null || true
|
|
389
|
+
elif [[ -n "$job_repo" ]]; then
|
|
390
|
+
daemon_log INFO "Org-mode: preserving clone for ${job_repo}"
|
|
391
|
+
fi
|
|
392
|
+
fi
|
|
393
|
+
|
|
394
|
+
# Dequeue next issue if available AND we have capacity
|
|
395
|
+
# NOTE: locked_get_active_count prevents TOCTOU race with the
|
|
396
|
+
# active_jobs removal above. A tiny window remains between
|
|
397
|
+
# the count read and dequeue_next's own lock acquisition, but
|
|
398
|
+
# dequeue_next is itself locked, so the worst case is a
|
|
399
|
+
# missed dequeue that the next poll cycle will pick up.
|
|
400
|
+
local current_active
|
|
401
|
+
current_active=$(locked_get_active_count)
|
|
402
|
+
if [[ "$current_active" -lt "$MAX_PARALLEL" ]]; then
|
|
403
|
+
local next_issue
|
|
404
|
+
next_issue=$(dequeue_next)
|
|
405
|
+
if [[ -n "$next_issue" ]]; then
|
|
406
|
+
local next_title
|
|
407
|
+
next_title=$(jq -r --arg n "$next_issue" '.titles[$n] // ""' "$STATE_FILE" 2>/dev/null || true)
|
|
408
|
+
daemon_log INFO "Dequeuing issue #${next_issue}: ${next_title}"
|
|
409
|
+
daemon_spawn_pipeline "$next_issue" "$next_title"
|
|
410
|
+
fi
|
|
411
|
+
fi
|
|
412
|
+
done <<< "$jobs"
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
# ─── Success Handler ────────────────────────────────────────────────────────
|
|
416
|
+
|
|
417
|
+
daemon_on_success() {
|
|
418
|
+
local issue_num="$1" duration="${2:-}"
|
|
419
|
+
|
|
420
|
+
# Reset consecutive failure tracking on any success
|
|
421
|
+
reset_failure_tracking
|
|
422
|
+
|
|
423
|
+
daemon_log SUCCESS "Pipeline completed for issue #${issue_num} (${duration:-unknown})"
|
|
424
|
+
|
|
425
|
+
# Record pipeline duration for adaptive threshold learning
|
|
426
|
+
if [[ -n "$duration" && "$duration" != "unknown" ]]; then
|
|
427
|
+
# Parse duration string back to seconds (e.g. "5m 30s" → 330)
|
|
428
|
+
local dur_secs=0
|
|
429
|
+
local _h _m _s
|
|
430
|
+
_h=$(echo "$duration" | grep -oE '[0-9]+h' | grep -oE '[0-9]+' || true)
|
|
431
|
+
_m=$(echo "$duration" | grep -oE '[0-9]+m' | grep -oE '[0-9]+' || true)
|
|
432
|
+
_s=$(echo "$duration" | grep -oE '[0-9]+s' | grep -oE '[0-9]+' || true)
|
|
433
|
+
dur_secs=$(( ${_h:-0} * 3600 + ${_m:-0} * 60 + ${_s:-0} ))
|
|
434
|
+
if [[ "$dur_secs" -gt 0 ]]; then
|
|
435
|
+
record_pipeline_duration "$PIPELINE_TEMPLATE" "$dur_secs" "success"
|
|
436
|
+
record_scaling_outcome "$MAX_PARALLEL" "success"
|
|
437
|
+
fi
|
|
438
|
+
fi
|
|
439
|
+
|
|
440
|
+
# Record in completed list + clear retry count for this issue
|
|
441
|
+
locked_state_update \
|
|
442
|
+
--argjson num "$issue_num" \
|
|
443
|
+
--arg result "success" \
|
|
444
|
+
--arg dur "${duration:-unknown}" \
|
|
445
|
+
--arg completed_at "$(now_iso)" \
|
|
446
|
+
'.completed += [{
|
|
447
|
+
issue: $num,
|
|
448
|
+
result: $result,
|
|
449
|
+
duration: $dur,
|
|
450
|
+
completed_at: $completed_at
|
|
451
|
+
}] | .completed = .completed[-500:]
|
|
452
|
+
| del(.retry_counts[($num | tostring)])'
|
|
453
|
+
|
|
454
|
+
if [[ "$NO_GITHUB" != "true" ]]; then
|
|
455
|
+
# Remove watch label, add success label
|
|
456
|
+
gh issue edit "$issue_num" \
|
|
457
|
+
--remove-label "$ON_SUCCESS_REMOVE_LABEL" \
|
|
458
|
+
--add-label "$ON_SUCCESS_ADD_LABEL" 2>/dev/null || true
|
|
459
|
+
|
|
460
|
+
# Comment on issue
|
|
461
|
+
gh issue comment "$issue_num" --body "## ✅ Pipeline Complete
|
|
462
|
+
|
|
463
|
+
The autonomous pipeline finished successfully.
|
|
464
|
+
|
|
465
|
+
| Field | Value |
|
|
466
|
+
|-------|-------|
|
|
467
|
+
| Duration | ${duration:-unknown} |
|
|
468
|
+
| Completed | $(now_iso) |
|
|
469
|
+
|
|
470
|
+
Check the associated PR for the implementation." 2>/dev/null || true
|
|
471
|
+
|
|
472
|
+
# Optionally close the issue
|
|
473
|
+
if [[ "$ON_SUCCESS_CLOSE_ISSUE" == "true" ]]; then
|
|
474
|
+
gh issue close "$issue_num" 2>/dev/null || true
|
|
475
|
+
fi
|
|
476
|
+
fi
|
|
477
|
+
|
|
478
|
+
notify "Pipeline Complete — Issue #${issue_num}" \
|
|
479
|
+
"Duration: ${duration:-unknown}" "success"
|
|
480
|
+
"$SCRIPT_DIR/sw-tracker.sh" notify "completed" "$issue_num" 2>/dev/null || true
|
|
481
|
+
|
|
482
|
+
# PM agent: record success for learning
|
|
483
|
+
if [[ -x "$SCRIPT_DIR/sw-pm.sh" ]]; then
|
|
484
|
+
bash "$SCRIPT_DIR/sw-pm.sh" learn "$issue_num" success 2>/dev/null || true
|
|
485
|
+
fi
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
# ─── Failure Classification ─────────────────────────────────────────────────
|
|
489
|
+
|