shipwright-cli 2.4.0 → 3.0.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 +16 -11
- package/completions/_shipwright +1 -1
- package/completions/shipwright.bash +3 -8
- package/completions/shipwright.fish +1 -1
- package/config/defaults.json +111 -0
- package/config/event-schema.json +81 -0
- package/config/policy.json +13 -18
- package/dashboard/coverage/coverage-summary.json +14 -0
- package/dashboard/public/index.html +1 -1
- package/dashboard/server.ts +306 -17
- package/dashboard/src/components/charts/bar.test.ts +79 -0
- package/dashboard/src/components/charts/donut.test.ts +68 -0
- package/dashboard/src/components/charts/pipeline-rail.test.ts +117 -0
- package/dashboard/src/components/charts/sparkline.test.ts +125 -0
- package/dashboard/src/core/api.test.ts +309 -0
- package/dashboard/src/core/helpers.test.ts +301 -0
- package/dashboard/src/core/router.test.ts +307 -0
- package/dashboard/src/core/router.ts +7 -0
- package/dashboard/src/core/sse.test.ts +144 -0
- package/dashboard/src/views/metrics.test.ts +186 -0
- package/dashboard/src/views/overview.test.ts +173 -0
- package/dashboard/src/views/pipelines.test.ts +183 -0
- package/dashboard/src/views/team.test.ts +253 -0
- package/dashboard/vitest.config.ts +14 -5
- package/docs/TIPS.md +1 -1
- package/docs/patterns/README.md +1 -1
- package/package.json +5 -7
- package/scripts/adapters/docker-deploy.sh +1 -1
- package/scripts/adapters/tmux-adapter.sh +11 -1
- package/scripts/adapters/wezterm-adapter.sh +1 -1
- package/scripts/check-version-consistency.sh +1 -1
- package/scripts/lib/architecture.sh +126 -0
- package/scripts/lib/bootstrap.sh +75 -0
- package/scripts/lib/compat.sh +89 -6
- package/scripts/lib/config.sh +91 -0
- package/scripts/lib/daemon-adaptive.sh +3 -3
- package/scripts/lib/daemon-dispatch.sh +39 -16
- package/scripts/lib/daemon-health.sh +1 -1
- package/scripts/lib/daemon-patrol.sh +24 -12
- package/scripts/lib/daemon-poll.sh +37 -25
- package/scripts/lib/daemon-state.sh +115 -23
- package/scripts/lib/daemon-triage.sh +30 -8
- package/scripts/lib/fleet-failover.sh +63 -0
- package/scripts/lib/helpers.sh +30 -6
- package/scripts/lib/pipeline-detection.sh +2 -2
- package/scripts/lib/pipeline-github.sh +9 -9
- package/scripts/lib/pipeline-intelligence.sh +85 -35
- package/scripts/lib/pipeline-quality-checks.sh +16 -16
- package/scripts/lib/pipeline-quality.sh +1 -1
- package/scripts/lib/pipeline-stages.sh +242 -28
- package/scripts/lib/pipeline-state.sh +40 -4
- package/scripts/lib/test-helpers.sh +247 -0
- package/scripts/postinstall.mjs +3 -11
- package/scripts/sw +10 -4
- package/scripts/sw-activity.sh +1 -11
- package/scripts/sw-adaptive.sh +109 -85
- package/scripts/sw-adversarial.sh +4 -14
- package/scripts/sw-architecture-enforcer.sh +1 -11
- package/scripts/sw-auth.sh +8 -17
- package/scripts/sw-autonomous.sh +111 -49
- package/scripts/sw-changelog.sh +1 -11
- package/scripts/sw-checkpoint.sh +144 -20
- package/scripts/sw-ci.sh +2 -12
- package/scripts/sw-cleanup.sh +13 -17
- package/scripts/sw-code-review.sh +16 -36
- package/scripts/sw-connect.sh +5 -12
- package/scripts/sw-context.sh +9 -26
- package/scripts/sw-cost.sh +6 -16
- package/scripts/sw-daemon.sh +75 -70
- package/scripts/sw-dashboard.sh +57 -17
- package/scripts/sw-db.sh +506 -15
- package/scripts/sw-decompose.sh +1 -11
- package/scripts/sw-deps.sh +15 -25
- package/scripts/sw-developer-simulation.sh +1 -11
- package/scripts/sw-discovery.sh +112 -30
- package/scripts/sw-doc-fleet.sh +7 -17
- package/scripts/sw-docs-agent.sh +6 -16
- package/scripts/sw-docs.sh +4 -12
- package/scripts/sw-doctor.sh +134 -43
- package/scripts/sw-dora.sh +11 -19
- package/scripts/sw-durable.sh +35 -52
- package/scripts/sw-e2e-orchestrator.sh +11 -27
- package/scripts/sw-eventbus.sh +115 -115
- package/scripts/sw-evidence.sh +114 -30
- package/scripts/sw-feedback.sh +3 -13
- package/scripts/sw-fix.sh +2 -20
- package/scripts/sw-fleet-discover.sh +1 -11
- package/scripts/sw-fleet-viz.sh +10 -18
- package/scripts/sw-fleet.sh +13 -17
- package/scripts/sw-github-app.sh +6 -16
- package/scripts/sw-github-checks.sh +1 -11
- package/scripts/sw-github-deploy.sh +1 -11
- package/scripts/sw-github-graphql.sh +2 -12
- package/scripts/sw-guild.sh +1 -11
- package/scripts/sw-heartbeat.sh +49 -12
- package/scripts/sw-hygiene.sh +45 -43
- package/scripts/sw-incident.sh +48 -74
- package/scripts/sw-init.sh +35 -37
- package/scripts/sw-instrument.sh +1 -11
- package/scripts/sw-intelligence.sh +362 -51
- package/scripts/sw-jira.sh +5 -14
- package/scripts/sw-launchd.sh +2 -12
- package/scripts/sw-linear.sh +8 -17
- package/scripts/sw-logs.sh +4 -12
- package/scripts/sw-loop.sh +641 -90
- package/scripts/sw-memory.sh +243 -17
- package/scripts/sw-mission-control.sh +2 -12
- package/scripts/sw-model-router.sh +73 -34
- package/scripts/sw-otel.sh +11 -21
- package/scripts/sw-oversight.sh +1 -11
- package/scripts/sw-patrol-meta.sh +5 -11
- package/scripts/sw-pipeline-composer.sh +7 -17
- package/scripts/sw-pipeline-vitals.sh +1 -11
- package/scripts/sw-pipeline.sh +478 -122
- package/scripts/sw-pm.sh +2 -12
- package/scripts/sw-pr-lifecycle.sh +27 -25
- package/scripts/sw-predictive.sh +16 -22
- package/scripts/sw-prep.sh +6 -16
- package/scripts/sw-ps.sh +1 -11
- package/scripts/sw-public-dashboard.sh +2 -12
- package/scripts/sw-quality.sh +77 -10
- package/scripts/sw-reaper.sh +1 -11
- package/scripts/sw-recruit.sh +15 -25
- package/scripts/sw-regression.sh +11 -21
- package/scripts/sw-release-manager.sh +19 -28
- package/scripts/sw-release.sh +8 -16
- package/scripts/sw-remote.sh +1 -11
- package/scripts/sw-replay.sh +48 -44
- package/scripts/sw-retro.sh +70 -92
- package/scripts/sw-review-rerun.sh +1 -1
- package/scripts/sw-scale.sh +109 -32
- package/scripts/sw-security-audit.sh +12 -22
- package/scripts/sw-self-optimize.sh +239 -23
- package/scripts/sw-session.sh +3 -13
- package/scripts/sw-setup.sh +8 -18
- package/scripts/sw-standup.sh +5 -15
- package/scripts/sw-status.sh +32 -23
- package/scripts/sw-strategic.sh +129 -13
- package/scripts/sw-stream.sh +1 -11
- package/scripts/sw-swarm.sh +76 -36
- package/scripts/sw-team-stages.sh +10 -20
- package/scripts/sw-templates.sh +4 -14
- package/scripts/sw-testgen.sh +3 -13
- package/scripts/sw-tmux-pipeline.sh +1 -19
- package/scripts/sw-tmux-role-color.sh +0 -10
- package/scripts/sw-tmux-status.sh +3 -11
- package/scripts/sw-tmux.sh +2 -20
- package/scripts/sw-trace.sh +1 -19
- package/scripts/sw-tracker-github.sh +0 -10
- package/scripts/sw-tracker-jira.sh +1 -11
- package/scripts/sw-tracker-linear.sh +1 -11
- package/scripts/sw-tracker.sh +7 -24
- package/scripts/sw-triage.sh +24 -34
- package/scripts/sw-upgrade.sh +5 -23
- package/scripts/sw-ux.sh +1 -19
- package/scripts/sw-webhook.sh +18 -32
- package/scripts/sw-widgets.sh +3 -21
- package/scripts/sw-worktree.sh +11 -27
- package/scripts/update-homebrew-sha.sh +67 -0
- package/templates/pipelines/tdd.json +72 -0
- package/scripts/sw-pipeline.sh.mock +0 -7
package/scripts/sw-pipeline.sh
CHANGED
|
@@ -11,7 +11,7 @@ unset CLAUDECODE 2>/dev/null || true
|
|
|
11
11
|
# Ignore SIGHUP so tmux attach/detach doesn't kill long-running plan/design/review stages
|
|
12
12
|
trap '' HUP
|
|
13
13
|
|
|
14
|
-
VERSION="
|
|
14
|
+
VERSION="3.0.0"
|
|
15
15
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
16
|
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
17
17
|
|
|
@@ -21,6 +21,7 @@ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
21
21
|
# Canonical helpers (colors, output, events)
|
|
22
22
|
# shellcheck source=lib/helpers.sh
|
|
23
23
|
[[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
|
|
24
|
+
[[ -f "$SCRIPT_DIR/lib/config.sh" ]] && source "$SCRIPT_DIR/lib/config.sh"
|
|
24
25
|
# Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
|
|
25
26
|
[[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
|
|
26
27
|
[[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
|
|
@@ -30,23 +31,6 @@ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
|
|
|
30
31
|
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
31
32
|
now_epoch() { date +%s; }
|
|
32
33
|
fi
|
|
33
|
-
if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
|
|
34
|
-
emit_event() {
|
|
35
|
-
local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
|
|
36
|
-
local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
|
|
37
|
-
while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
|
|
38
|
-
echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
|
|
39
|
-
}
|
|
40
|
-
fi
|
|
41
|
-
CYAN="${CYAN:-\033[38;2;0;212;255m}"
|
|
42
|
-
PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
|
|
43
|
-
BLUE="${BLUE:-\033[38;2;0;102;255m}"
|
|
44
|
-
GREEN="${GREEN:-\033[38;2;74;222;128m}"
|
|
45
|
-
YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
|
|
46
|
-
RED="${RED:-\033[38;2;248;113;113m}"
|
|
47
|
-
DIM="${DIM:-\033[2m}"
|
|
48
|
-
BOLD="${BOLD:-\033[1m}"
|
|
49
|
-
RESET="${RESET:-\033[0m}"
|
|
50
34
|
# Policy + pipeline quality thresholds (config/policy.json via lib/pipeline-quality.sh)
|
|
51
35
|
[[ -f "$SCRIPT_DIR/lib/pipeline-quality.sh" ]] && source "$SCRIPT_DIR/lib/pipeline-quality.sh"
|
|
52
36
|
# shellcheck source=lib/pipeline-state.sh
|
|
@@ -107,6 +91,8 @@ fi
|
|
|
107
91
|
if [[ -f "$SCRIPT_DIR/sw-durable.sh" ]]; then
|
|
108
92
|
source "$SCRIPT_DIR/sw-durable.sh"
|
|
109
93
|
fi
|
|
94
|
+
# shellcheck source=sw-db.sh — for db_save_checkpoint/db_load_checkpoint (durable workflows)
|
|
95
|
+
[[ -f "$SCRIPT_DIR/sw-db.sh" ]] && source "$SCRIPT_DIR/sw-db.sh"
|
|
110
96
|
|
|
111
97
|
# ─── GitHub API Modules (optional) ─────────────────────────────────────────
|
|
112
98
|
# shellcheck source=sw-github-graphql.sh
|
|
@@ -151,6 +137,21 @@ format_duration() {
|
|
|
151
137
|
fi
|
|
152
138
|
}
|
|
153
139
|
|
|
140
|
+
# Rotate event log if needed (standalone mode — daemon has its own rotation in poll loop)
|
|
141
|
+
rotate_event_log_if_needed() {
|
|
142
|
+
local events_file="${EVENTS_FILE:-$HOME/.shipwright/events.jsonl}"
|
|
143
|
+
local max_lines=10000
|
|
144
|
+
[[ ! -f "$events_file" ]] && return
|
|
145
|
+
local lines
|
|
146
|
+
lines=$(wc -l < "$events_file" 2>/dev/null || echo "0")
|
|
147
|
+
if [[ "$lines" -gt "$max_lines" ]]; then
|
|
148
|
+
local tmp="${events_file}.rotating"
|
|
149
|
+
if tail -5000 "$events_file" > "$tmp" 2>/dev/null && mv "$tmp" "$events_file" 2>/dev/null; then
|
|
150
|
+
info "Rotated events.jsonl: ${lines} -> 5000 lines"
|
|
151
|
+
fi
|
|
152
|
+
fi
|
|
153
|
+
}
|
|
154
|
+
|
|
154
155
|
_pipeline_compact_goal() {
|
|
155
156
|
local goal="$1"
|
|
156
157
|
local plan_file="${2:-}"
|
|
@@ -199,33 +200,6 @@ load_composed_pipeline() {
|
|
|
199
200
|
return 0
|
|
200
201
|
}
|
|
201
202
|
|
|
202
|
-
# ─── Structured Event Log ──────────────────────────────────────────────────
|
|
203
|
-
# Appends JSON events to ~/.shipwright/events.jsonl for metrics/traceability
|
|
204
|
-
|
|
205
|
-
EVENTS_DIR="${HOME}/.shipwright"
|
|
206
|
-
EVENTS_FILE="${EVENTS_DIR}/events.jsonl"
|
|
207
|
-
|
|
208
|
-
emit_event() {
|
|
209
|
-
local event_type="$1"
|
|
210
|
-
shift
|
|
211
|
-
# Remaining args are key=value pairs
|
|
212
|
-
local json_fields=""
|
|
213
|
-
for kv in "$@"; do
|
|
214
|
-
local key="${kv%%=*}"
|
|
215
|
-
local val="${kv#*=}"
|
|
216
|
-
# Numbers: don't quote; strings: quote
|
|
217
|
-
if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
|
|
218
|
-
json_fields="${json_fields},\"${key}\":${val}"
|
|
219
|
-
else
|
|
220
|
-
# Escape quotes in value
|
|
221
|
-
val="${val//\"/\\\"}"
|
|
222
|
-
json_fields="${json_fields},\"${key}\":\"${val}\""
|
|
223
|
-
fi
|
|
224
|
-
done
|
|
225
|
-
mkdir -p "$EVENTS_DIR"
|
|
226
|
-
echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
|
|
227
|
-
}
|
|
228
|
-
|
|
229
203
|
# ─── Token / Cost Parsing ─────────────────────────────────────────────────
|
|
230
204
|
parse_claude_tokens() {
|
|
231
205
|
local log_file="$1"
|
|
@@ -237,6 +211,36 @@ parse_claude_tokens() {
|
|
|
237
211
|
TOTAL_OUTPUT_TOKENS=$(( TOTAL_OUTPUT_TOKENS + ${output_tok:-0} ))
|
|
238
212
|
}
|
|
239
213
|
|
|
214
|
+
# Estimate pipeline cost using historical averages from completed pipelines.
|
|
215
|
+
# Falls back to per-stage estimates when no history exists.
|
|
216
|
+
estimate_pipeline_cost() {
|
|
217
|
+
local stages="$1"
|
|
218
|
+
local stage_count
|
|
219
|
+
stage_count=$(echo "$stages" | jq 'length' 2>/dev/null || echo "6")
|
|
220
|
+
[[ ! "$stage_count" =~ ^[0-9]+$ ]] && stage_count=6
|
|
221
|
+
|
|
222
|
+
local events_file="${EVENTS_FILE:-$HOME/.shipwright/events.jsonl}"
|
|
223
|
+
local avg_input=0 avg_output=0
|
|
224
|
+
if [[ -f "$events_file" ]]; then
|
|
225
|
+
local hist
|
|
226
|
+
hist=$(grep '"type":"pipeline.completed"' "$events_file" 2>/dev/null | tail -10)
|
|
227
|
+
if [[ -n "$hist" ]]; then
|
|
228
|
+
avg_input=$(echo "$hist" | jq -s -r '[.[] | .input_tokens // 0 | tonumber] | if length > 0 then (add / length | floor | tostring) else "0" end' 2>/dev/null | head -1)
|
|
229
|
+
avg_output=$(echo "$hist" | jq -s -r '[.[] | .output_tokens // 0 | tonumber] | if length > 0 then (add / length | floor | tostring) else "0" end' 2>/dev/null | head -1)
|
|
230
|
+
fi
|
|
231
|
+
fi
|
|
232
|
+
[[ ! "$avg_input" =~ ^[0-9]+$ ]] && avg_input=0
|
|
233
|
+
[[ ! "$avg_output" =~ ^[0-9]+$ ]] && avg_output=0
|
|
234
|
+
|
|
235
|
+
# Fall back to reasonable per-stage estimates only if no history
|
|
236
|
+
if [[ "$avg_input" -eq 0 ]]; then
|
|
237
|
+
avg_input=$(( stage_count * 8000 )) # More realistic: ~8K input per stage
|
|
238
|
+
avg_output=$(( stage_count * 4000 )) # ~4K output per stage
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
echo "{\"input_tokens\":${avg_input},\"output_tokens\":${avg_output}}"
|
|
242
|
+
}
|
|
243
|
+
|
|
240
244
|
# ─── Defaults ───────────────────────────────────────────────────────────────
|
|
241
245
|
GOAL=""
|
|
242
246
|
ISSUE_NUMBER=""
|
|
@@ -260,6 +264,7 @@ CI_MODE=false
|
|
|
260
264
|
DRY_RUN=false
|
|
261
265
|
IGNORE_BUDGET=false
|
|
262
266
|
COMPLETED_STAGES=""
|
|
267
|
+
RESUME_FROM_CHECKPOINT=false
|
|
263
268
|
MAX_ITERATIONS_OVERRIDE=""
|
|
264
269
|
MAX_RESTARTS_OVERRIDE=""
|
|
265
270
|
FAST_TEST_CMD_OVERRIDE=""
|
|
@@ -285,6 +290,10 @@ GH_AVAILABLE=false
|
|
|
285
290
|
# Timing
|
|
286
291
|
PIPELINE_START_EPOCH=""
|
|
287
292
|
STAGE_TIMINGS=""
|
|
293
|
+
PIPELINE_STAGES_PASSED=""
|
|
294
|
+
PIPELINE_SLOWEST_STAGE=""
|
|
295
|
+
LAST_STAGE_ERROR_CLASS=""
|
|
296
|
+
LAST_STAGE_ERROR=""
|
|
288
297
|
|
|
289
298
|
PROJECT_ROOT=""
|
|
290
299
|
STATE_DIR=""
|
|
@@ -333,6 +342,7 @@ show_help() {
|
|
|
333
342
|
echo -e " ${DIM}--max-iterations <n>${RESET} Override max build loop iterations"
|
|
334
343
|
echo -e " ${DIM}--max-restarts <n>${RESET} Max session restarts in build loop"
|
|
335
344
|
echo -e " ${DIM}--fast-test-cmd <cmd>${RESET} Fast/subset test for build loop"
|
|
345
|
+
echo -e " ${DIM}--tdd${RESET} Test-first: generate tests before implementation"
|
|
336
346
|
echo -e " ${DIM}--completed-stages \"a,b\"${RESET} Skip these stages (CI resume)"
|
|
337
347
|
echo ""
|
|
338
348
|
echo -e "${BOLD}STAGES${RESET} ${DIM}(configurable per pipeline template)${RESET}"
|
|
@@ -413,6 +423,7 @@ parse_args() {
|
|
|
413
423
|
--ignore-budget) IGNORE_BUDGET=true; shift ;;
|
|
414
424
|
--max-iterations) MAX_ITERATIONS_OVERRIDE="$2"; shift 2 ;;
|
|
415
425
|
--completed-stages) COMPLETED_STAGES="$2"; shift 2 ;;
|
|
426
|
+
--resume) RESUME_FROM_CHECKPOINT=true; shift ;;
|
|
416
427
|
--worktree=*) AUTO_WORKTREE=true; WORKTREE_NAME="${1#--worktree=}"; WORKTREE_NAME="${WORKTREE_NAME//[^a-zA-Z0-9_-]/}"; if [[ -z "$WORKTREE_NAME" ]]; then error "Invalid worktree name (alphanumeric, hyphens, underscores only)"; exit 1; fi; shift ;;
|
|
417
428
|
--worktree) AUTO_WORKTREE=true; shift ;;
|
|
418
429
|
--dry-run) DRY_RUN=true; shift ;;
|
|
@@ -427,6 +438,7 @@ parse_args() {
|
|
|
427
438
|
shift 2 ;;
|
|
428
439
|
|
|
429
440
|
--fast-test-cmd) FAST_TEST_CMD_OVERRIDE="$2"; shift 2 ;;
|
|
441
|
+
--tdd) TDD_ENABLED=true; shift ;;
|
|
430
442
|
--help|-h) show_help; exit 0 ;;
|
|
431
443
|
*)
|
|
432
444
|
if [[ -z "$PIPELINE_NAME_ARG" ]]; then
|
|
@@ -487,11 +499,11 @@ find_pipeline_config() {
|
|
|
487
499
|
load_pipeline_config() {
|
|
488
500
|
# Check for intelligence-composed pipeline first
|
|
489
501
|
local composed_pipeline="${ARTIFACTS_DIR}/composed-pipeline.json"
|
|
490
|
-
if [[ -f "$composed_pipeline" ]] && type composer_validate_pipeline
|
|
502
|
+
if [[ -f "$composed_pipeline" ]] && type composer_validate_pipeline >/dev/null 2>&1; then
|
|
491
503
|
# Use composed pipeline if fresh (< 1 hour old)
|
|
492
504
|
local composed_age=99999
|
|
493
505
|
local composed_mtime
|
|
494
|
-
composed_mtime=$(
|
|
506
|
+
composed_mtime=$(file_mtime "$composed_pipeline")
|
|
495
507
|
if [[ "$composed_mtime" -gt 0 ]]; then
|
|
496
508
|
composed_age=$(( $(now_epoch) - composed_mtime ))
|
|
497
509
|
fi
|
|
@@ -513,6 +525,9 @@ load_pipeline_config() {
|
|
|
513
525
|
exit 1
|
|
514
526
|
}
|
|
515
527
|
info "Pipeline: ${BOLD}$PIPELINE_NAME${RESET} ${DIM}($PIPELINE_CONFIG)${RESET}"
|
|
528
|
+
# TDD from template (overridable by --tdd)
|
|
529
|
+
[[ "$(jq -r '.tdd // false' "$PIPELINE_CONFIG" 2>/dev/null)" == "true" ]] && PIPELINE_TDD=true
|
|
530
|
+
return 0
|
|
516
531
|
}
|
|
517
532
|
|
|
518
533
|
CURRENT_STAGE_ID=""
|
|
@@ -522,7 +537,7 @@ SLACK_WEBHOOK=""
|
|
|
522
537
|
NOTIFICATION_ENABLED=false
|
|
523
538
|
|
|
524
539
|
# Self-healing
|
|
525
|
-
BUILD_TEST_RETRIES
|
|
540
|
+
BUILD_TEST_RETRIES=$(_config_get_int "pipeline.build_test_retries" 3 2>/dev/null || echo 3)
|
|
526
541
|
STASHED_CHANGES=false
|
|
527
542
|
SELF_HEAL_COUNT=0
|
|
528
543
|
|
|
@@ -544,7 +559,7 @@ start_heartbeat() {
|
|
|
544
559
|
--stage "${CURRENT_STAGE_ID:-unknown}" \
|
|
545
560
|
--iteration "0" \
|
|
546
561
|
--activity "$(get_stage_description "${CURRENT_STAGE_ID:-}" 2>/dev/null || echo "Running pipeline")" 2>/dev/null || true
|
|
547
|
-
sleep 30
|
|
562
|
+
sleep "$(_config_get_int "pipeline.heartbeat_interval" 30 2>/dev/null || echo 30)"
|
|
548
563
|
done
|
|
549
564
|
) >/dev/null 2>&1 &
|
|
550
565
|
HEARTBEAT_PID=$!
|
|
@@ -574,7 +589,10 @@ ci_push_partial_work() {
|
|
|
574
589
|
fi
|
|
575
590
|
|
|
576
591
|
# Push branch (create if needed, force to overwrite previous WIP)
|
|
577
|
-
git push origin "HEAD:refs/heads/$branch" --force 2>/dev/null
|
|
592
|
+
if ! git push origin "HEAD:refs/heads/$branch" --force 2>/dev/null; then
|
|
593
|
+
warn "git push failed for $branch — remote may be out of sync"
|
|
594
|
+
emit_event "pipeline.push_failed" "branch=$branch"
|
|
595
|
+
fi
|
|
578
596
|
}
|
|
579
597
|
|
|
580
598
|
ci_post_stage_event() {
|
|
@@ -584,7 +602,7 @@ ci_post_stage_event() {
|
|
|
584
602
|
|
|
585
603
|
local stage="$1" status="$2" elapsed="${3:-0s}"
|
|
586
604
|
local comment="<!-- SHIPWRIGHT-STAGE: ${stage}:${status}:${elapsed} -->"
|
|
587
|
-
gh issue comment "$ISSUE_NUMBER" --body "$comment" 2>/dev/null || true
|
|
605
|
+
_timeout "$(_config_get_int "network.gh_timeout" 30 2>/dev/null || echo 30)" gh issue comment "$ISSUE_NUMBER" --body "$comment" 2>/dev/null || true
|
|
588
606
|
}
|
|
589
607
|
|
|
590
608
|
# ─── Signal Handling ───────────────────────────────────────────────────────
|
|
@@ -620,7 +638,10 @@ cleanup_on_exit() {
|
|
|
620
638
|
|
|
621
639
|
# Update GitHub
|
|
622
640
|
if [[ -n "${ISSUE_NUMBER:-}" && "${GH_AVAILABLE:-false}" == "true" ]]; then
|
|
623
|
-
|
|
641
|
+
if ! _timeout "$(_config_get_int "network.gh_timeout" 30 2>/dev/null || echo 30)" gh issue comment "$ISSUE_NUMBER" --body "⏸️ **Pipeline interrupted** at stage: ${CURRENT_STAGE_ID:-unknown}" 2>/dev/null; then
|
|
642
|
+
warn "gh issue comment failed — status update may not have been posted"
|
|
643
|
+
emit_event "pipeline.comment_failed" "issue=$ISSUE_NUMBER"
|
|
644
|
+
fi
|
|
624
645
|
fi
|
|
625
646
|
|
|
626
647
|
exit "$exit_code"
|
|
@@ -641,7 +662,7 @@ preflight_checks() {
|
|
|
641
662
|
local optional_tools=("gh" "claude" "bc" "curl")
|
|
642
663
|
|
|
643
664
|
for tool in "${required_tools[@]}"; do
|
|
644
|
-
if command -v "$tool"
|
|
665
|
+
if command -v "$tool" >/dev/null 2>&1; then
|
|
645
666
|
echo -e " ${GREEN}✓${RESET} $tool"
|
|
646
667
|
else
|
|
647
668
|
echo -e " ${RED}✗${RESET} $tool ${RED}(required)${RESET}"
|
|
@@ -650,7 +671,7 @@ preflight_checks() {
|
|
|
650
671
|
done
|
|
651
672
|
|
|
652
673
|
for tool in "${optional_tools[@]}"; do
|
|
653
|
-
if command -v "$tool"
|
|
674
|
+
if command -v "$tool" >/dev/null 2>&1; then
|
|
654
675
|
echo -e " ${GREEN}✓${RESET} $tool"
|
|
655
676
|
else
|
|
656
677
|
echo -e " ${DIM}○${RESET} $tool ${DIM}(optional — some features disabled)${RESET}"
|
|
@@ -659,7 +680,7 @@ preflight_checks() {
|
|
|
659
680
|
|
|
660
681
|
# 2. Git state
|
|
661
682
|
echo ""
|
|
662
|
-
if git rev-parse --is-inside-work-tree
|
|
683
|
+
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
663
684
|
echo -e " ${GREEN}✓${RESET} Inside git repo"
|
|
664
685
|
else
|
|
665
686
|
echo -e " ${RED}✗${RESET} Not inside a git repository"
|
|
@@ -685,7 +706,7 @@ preflight_checks() {
|
|
|
685
706
|
fi
|
|
686
707
|
|
|
687
708
|
# Check if base branch exists
|
|
688
|
-
if git rev-parse --verify "$BASE_BRANCH"
|
|
709
|
+
if git rev-parse --verify "$BASE_BRANCH" >/dev/null 2>&1; then
|
|
689
710
|
echo -e " ${GREEN}✓${RESET} Base branch: $BASE_BRANCH"
|
|
690
711
|
else
|
|
691
712
|
echo -e " ${RED}✗${RESET} Base branch not found: $BASE_BRANCH"
|
|
@@ -693,8 +714,8 @@ preflight_checks() {
|
|
|
693
714
|
fi
|
|
694
715
|
|
|
695
716
|
# 3. GitHub auth (if gh available and not disabled)
|
|
696
|
-
if [[ "$NO_GITHUB" != "true" ]] && command -v gh
|
|
697
|
-
if gh auth status
|
|
717
|
+
if [[ "$NO_GITHUB" != "true" ]] && command -v gh >/dev/null 2>&1; then
|
|
718
|
+
if gh auth status >/dev/null 2>&1; then
|
|
698
719
|
echo -e " ${GREEN}✓${RESET} GitHub authenticated"
|
|
699
720
|
else
|
|
700
721
|
echo -e " ${YELLOW}⚠${RESET} GitHub not authenticated (features disabled)"
|
|
@@ -702,7 +723,7 @@ preflight_checks() {
|
|
|
702
723
|
fi
|
|
703
724
|
|
|
704
725
|
# 4. Claude CLI
|
|
705
|
-
if command -v claude
|
|
726
|
+
if command -v claude >/dev/null 2>&1; then
|
|
706
727
|
echo -e " ${GREEN}✓${RESET} Claude CLI available"
|
|
707
728
|
else
|
|
708
729
|
echo -e " ${RED}✗${RESET} Claude CLI not found — plan/build stages will fail"
|
|
@@ -754,12 +775,12 @@ notify() {
|
|
|
754
775
|
payload=$(jq -n \
|
|
755
776
|
--arg text "${emoji} *${title}*\n${message}" \
|
|
756
777
|
'{text: $text}')
|
|
757
|
-
curl -sf -X POST -H 'Content-Type: application/json' \
|
|
778
|
+
curl -sf --connect-timeout "$(_config_get_int "network.connect_timeout" 10 2>/dev/null || echo 10)" --max-time "$(_config_get_int "network.max_time" 60 2>/dev/null || echo 60)" -X POST -H 'Content-Type: application/json' \
|
|
758
779
|
-d "$payload" "$SLACK_WEBHOOK" >/dev/null 2>&1 || true
|
|
759
780
|
fi
|
|
760
781
|
|
|
761
|
-
# Custom webhook (env var SHIPWRIGHT_WEBHOOK_URL
|
|
762
|
-
local _webhook_url="${SHIPWRIGHT_WEBHOOK_URL
|
|
782
|
+
# Custom webhook (env var SHIPWRIGHT_WEBHOOK_URL)
|
|
783
|
+
local _webhook_url="${SHIPWRIGHT_WEBHOOK_URL:-}"
|
|
763
784
|
if [[ -n "$_webhook_url" ]]; then
|
|
764
785
|
local payload
|
|
765
786
|
payload=$(jq -n \
|
|
@@ -767,7 +788,7 @@ notify() {
|
|
|
767
788
|
--arg level "$level" --arg pipeline "${PIPELINE_NAME:-}" \
|
|
768
789
|
--arg goal "${GOAL:-}" --arg stage "${CURRENT_STAGE_ID:-}" \
|
|
769
790
|
'{title:$title, message:$message, level:$level, pipeline:$pipeline, goal:$goal, stage:$stage}')
|
|
770
|
-
curl -sf -X POST -H 'Content-Type: application/json' \
|
|
791
|
+
curl -sf --connect-timeout 10 --max-time 30 -X POST -H 'Content-Type: application/json' \
|
|
771
792
|
-d "$payload" "$_webhook_url" >/dev/null 2>&1 || true
|
|
772
793
|
fi
|
|
773
794
|
}
|
|
@@ -815,7 +836,7 @@ classify_error() {
|
|
|
815
836
|
elif echo "$log_tail" | grep -qiE 'error\[E[0-9]+\]|error: aborting|FAILED.*compile|build failed|tsc.*error|eslint.*error'; then
|
|
816
837
|
classification="logic"
|
|
817
838
|
# Intelligence fallback: Claude classification for unknown errors
|
|
818
|
-
elif [[ "$classification" == "unknown" ]] && type intelligence_search_memory
|
|
839
|
+
elif [[ "$classification" == "unknown" ]] && type intelligence_search_memory >/dev/null 2>&1 && command -v claude >/dev/null 2>&1; then
|
|
819
840
|
local ai_class
|
|
820
841
|
ai_class=$(claude --print --output-format text -p "Classify this error as exactly one of: infrastructure, configuration, logic, unknown.
|
|
821
842
|
|
|
@@ -882,14 +903,23 @@ run_stage_with_retry() {
|
|
|
882
903
|
return 0
|
|
883
904
|
fi
|
|
884
905
|
|
|
906
|
+
# Capture error_class and error snippet for stage.failed / pipeline.completed events
|
|
907
|
+
local error_class
|
|
908
|
+
error_class=$(classify_error "$stage_id")
|
|
909
|
+
LAST_STAGE_ERROR_CLASS="$error_class"
|
|
910
|
+
LAST_STAGE_ERROR=""
|
|
911
|
+
local _log_file="${ARTIFACTS_DIR}/${stage_id}-results.log"
|
|
912
|
+
[[ ! -f "$_log_file" ]] && _log_file="${ARTIFACTS_DIR}/test-results.log"
|
|
913
|
+
if [[ -f "$_log_file" ]]; then
|
|
914
|
+
LAST_STAGE_ERROR=$(tail -20 "$_log_file" 2>/dev/null | grep -iE 'error|fail|exception|fatal' 2>/dev/null | head -1 | cut -c1-200 || true)
|
|
915
|
+
fi
|
|
916
|
+
|
|
885
917
|
attempt=$((attempt + 1))
|
|
886
918
|
if [[ "$attempt" -gt "$max_retries" ]]; then
|
|
887
919
|
return 1
|
|
888
920
|
fi
|
|
889
921
|
|
|
890
|
-
# Classify
|
|
891
|
-
local error_class
|
|
892
|
-
error_class=$(classify_error "$stage_id")
|
|
922
|
+
# Classify done above; decide whether retry makes sense
|
|
893
923
|
|
|
894
924
|
emit_event "retry.classified" \
|
|
895
925
|
"issue=${ISSUE_NUMBER:-0}" \
|
|
@@ -926,6 +956,15 @@ run_stage_with_retry() {
|
|
|
926
956
|
esac
|
|
927
957
|
prev_error_class="$error_class"
|
|
928
958
|
|
|
959
|
+
if type db_save_reasoning_trace >/dev/null 2>&1; then
|
|
960
|
+
local job_id="${SHIPWRIGHT_PIPELINE_ID:-$$}"
|
|
961
|
+
local error_msg="${LAST_STAGE_ERROR:-$error_class}"
|
|
962
|
+
db_save_reasoning_trace "$job_id" "retry_reasoning" \
|
|
963
|
+
"stage=$stage_id error=$error_msg" \
|
|
964
|
+
"Stage failed, analyzing error pattern before retry" \
|
|
965
|
+
"retry_strategy=self_heal" 0.6 2>/dev/null || true
|
|
966
|
+
fi
|
|
967
|
+
|
|
929
968
|
warn "Stage $stage_id failed (attempt $attempt/$((max_retries + 1)), class: $error_class) — retrying..."
|
|
930
969
|
# Exponential backoff with jitter to avoid thundering herd
|
|
931
970
|
local backoff=$((2 ** attempt))
|
|
@@ -951,9 +990,9 @@ self_healing_build_test() {
|
|
|
951
990
|
local prev_fail_count=0 zero_convergence_streak=0
|
|
952
991
|
|
|
953
992
|
# Vitals-driven adaptive limit (preferred over static BUILD_TEST_RETRIES)
|
|
954
|
-
if type pipeline_adaptive_limit
|
|
993
|
+
if type pipeline_adaptive_limit >/dev/null 2>&1; then
|
|
955
994
|
local _vitals_json=""
|
|
956
|
-
if type pipeline_compute_vitals
|
|
995
|
+
if type pipeline_compute_vitals >/dev/null 2>&1; then
|
|
957
996
|
_vitals_json=$(pipeline_compute_vitals "$STATE_FILE" "$ARTIFACTS_DIR" "${ISSUE_NUMBER:-}" 2>/dev/null) || true
|
|
958
997
|
fi
|
|
959
998
|
local vitals_limit
|
|
@@ -968,7 +1007,7 @@ self_healing_build_test() {
|
|
|
968
1007
|
"vitals_limit=$vitals_limit"
|
|
969
1008
|
fi
|
|
970
1009
|
# Fallback: intelligence-based adaptive limits
|
|
971
|
-
elif type composer_estimate_iterations
|
|
1010
|
+
elif type composer_estimate_iterations >/dev/null 2>&1; then
|
|
972
1011
|
local estimated
|
|
973
1012
|
estimated=$(composer_estimate_iterations \
|
|
974
1013
|
"${INTELLIGENCE_ANALYSIS:-{}}" \
|
|
@@ -1022,7 +1061,7 @@ self_healing_build_test() {
|
|
|
1022
1061
|
if [[ "$cycle" -gt 1 && -n "$last_test_error" ]]; then
|
|
1023
1062
|
# Query memory for known fixes
|
|
1024
1063
|
local _memory_fix=""
|
|
1025
|
-
if type memory_closed_loop_inject
|
|
1064
|
+
if type memory_closed_loop_inject >/dev/null 2>&1; then
|
|
1026
1065
|
local _error_sig_short
|
|
1027
1066
|
_error_sig_short=$(echo "$last_test_error" | head -3 || echo "")
|
|
1028
1067
|
_memory_fix=$(memory_closed_loop_inject "$_error_sig_short" 2>/dev/null) || true
|
|
@@ -1053,7 +1092,7 @@ Focus on fixing the failing tests while keeping all passing tests working."
|
|
|
1053
1092
|
local timing
|
|
1054
1093
|
timing=$(get_stage_timing "build")
|
|
1055
1094
|
success "Stage ${BOLD}build${RESET} complete ${DIM}(${timing})${RESET}"
|
|
1056
|
-
if type pipeline_emit_progress_snapshot
|
|
1095
|
+
if type pipeline_emit_progress_snapshot >/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
|
|
1057
1096
|
local _diff_count
|
|
1058
1097
|
_diff_count=$(git diff --stat HEAD~1 2>/dev/null | tail -1 | grep -oE '[0-9]+' | head -1) || true
|
|
1059
1098
|
local _snap_files _snap_error
|
|
@@ -1078,7 +1117,7 @@ Focus on fixing the failing tests while keeping all passing tests working."
|
|
|
1078
1117
|
local timing
|
|
1079
1118
|
timing=$(get_stage_timing "build")
|
|
1080
1119
|
success "Stage ${BOLD}build${RESET} complete ${DIM}(${timing})${RESET}"
|
|
1081
|
-
if type pipeline_emit_progress_snapshot
|
|
1120
|
+
if type pipeline_emit_progress_snapshot >/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
|
|
1082
1121
|
local _diff_count
|
|
1083
1122
|
_diff_count=$(git diff --stat HEAD~1 2>/dev/null | tail -1 | grep -oE '[0-9]+' | head -1) || true
|
|
1084
1123
|
local _snap_files _snap_error
|
|
@@ -1109,7 +1148,7 @@ Focus on fixing the failing tests while keeping all passing tests working."
|
|
|
1109
1148
|
emit_event "convergence.tests_passed" \
|
|
1110
1149
|
"issue=${ISSUE_NUMBER:-0}" \
|
|
1111
1150
|
"cycle=$cycle"
|
|
1112
|
-
if type pipeline_emit_progress_snapshot
|
|
1151
|
+
if type pipeline_emit_progress_snapshot >/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
|
|
1113
1152
|
local _diff_count
|
|
1114
1153
|
_diff_count=$(git diff --stat HEAD~1 2>/dev/null | tail -1 | grep -oE '[0-9]+' | head -1) || true
|
|
1115
1154
|
local _snap_files _snap_error
|
|
@@ -1235,6 +1274,9 @@ auto_rebase() {
|
|
|
1235
1274
|
}
|
|
1236
1275
|
|
|
1237
1276
|
run_pipeline() {
|
|
1277
|
+
# Rotate event log if needed (standalone mode)
|
|
1278
|
+
rotate_event_log_if_needed
|
|
1279
|
+
|
|
1238
1280
|
local stages
|
|
1239
1281
|
stages=$(jq -c '.stages[]' "$PIPELINE_CONFIG")
|
|
1240
1282
|
|
|
@@ -1329,6 +1371,10 @@ run_pipeline() {
|
|
|
1329
1371
|
|
|
1330
1372
|
# Self-healing build→test loop: when we hit build, run both together
|
|
1331
1373
|
if [[ "$id" == "build" && "$use_self_healing" == "true" ]]; then
|
|
1374
|
+
# TDD: generate tests before build when enabled
|
|
1375
|
+
if [[ "${TDD_ENABLED:-false}" == "true" || "${PIPELINE_TDD:-}" == "true" ]]; then
|
|
1376
|
+
stage_test_first || true
|
|
1377
|
+
fi
|
|
1332
1378
|
# Gate check for build
|
|
1333
1379
|
local build_gate
|
|
1334
1380
|
build_gate=$(echo "$stage" | jq -r '.gate')
|
|
@@ -1362,6 +1408,11 @@ run_pipeline() {
|
|
|
1362
1408
|
continue
|
|
1363
1409
|
fi
|
|
1364
1410
|
|
|
1411
|
+
# TDD: generate tests before build when enabled (non-self-healing path)
|
|
1412
|
+
if [[ "$id" == "build" && "$use_self_healing" != "true" ]] && [[ "${TDD_ENABLED:-false}" == "true" || "${PIPELINE_TDD:-}" == "true" ]]; then
|
|
1413
|
+
stage_test_first || true
|
|
1414
|
+
fi
|
|
1415
|
+
|
|
1365
1416
|
# Skip test if already handled by self-healing loop
|
|
1366
1417
|
if [[ "$id" == "test" && "$use_self_healing" == "true" ]]; then
|
|
1367
1418
|
stage_status=$(get_stage_status "test")
|
|
@@ -1401,52 +1452,59 @@ run_pipeline() {
|
|
|
1401
1452
|
fi
|
|
1402
1453
|
fi
|
|
1403
1454
|
|
|
1404
|
-
# Intelligence: per-stage model routing
|
|
1405
|
-
|
|
1455
|
+
# Intelligence: per-stage model routing (UCB1 when DB has data, else A/B testing)
|
|
1456
|
+
local recommended_model="" from_ucb1=false
|
|
1457
|
+
if type ucb1_select_model >/dev/null 2>&1; then
|
|
1458
|
+
recommended_model=$(ucb1_select_model "$id" 2>/dev/null || echo "")
|
|
1459
|
+
[[ -n "$recommended_model" ]] && from_ucb1=true
|
|
1460
|
+
fi
|
|
1461
|
+
if [[ -z "$recommended_model" ]] && type intelligence_recommend_model >/dev/null 2>&1; then
|
|
1406
1462
|
local stage_complexity="${INTELLIGENCE_COMPLEXITY:-5}"
|
|
1407
1463
|
local budget_remaining=""
|
|
1408
1464
|
if [[ -x "$SCRIPT_DIR/sw-cost.sh" ]]; then
|
|
1409
1465
|
budget_remaining=$(bash "$SCRIPT_DIR/sw-cost.sh" remaining-budget 2>/dev/null || echo "")
|
|
1410
1466
|
fi
|
|
1411
|
-
local
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1467
|
+
local recommended_json
|
|
1468
|
+
recommended_json=$(intelligence_recommend_model "$id" "$stage_complexity" "$budget_remaining" 2>/dev/null || echo "")
|
|
1469
|
+
recommended_model=$(echo "$recommended_json" | jq -r '.model // empty' 2>/dev/null || echo "")
|
|
1470
|
+
fi
|
|
1471
|
+
if [[ -n "$recommended_model" && "$recommended_model" != "null" ]]; then
|
|
1472
|
+
if [[ "$from_ucb1" == "true" ]]; then
|
|
1473
|
+
# UCB1 already balances exploration/exploitation — use directly
|
|
1474
|
+
export CLAUDE_MODEL="$recommended_model"
|
|
1475
|
+
emit_event "intelligence.model_ucb1" \
|
|
1476
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
1477
|
+
"stage=$id" \
|
|
1478
|
+
"model=$recommended_model"
|
|
1479
|
+
else
|
|
1480
|
+
# A/B testing for intelligence recommendation
|
|
1481
|
+
local ab_ratio=20
|
|
1416
1482
|
local daemon_cfg="${PROJECT_ROOT}/.claude/daemon-config.json"
|
|
1417
1483
|
if [[ -f "$daemon_cfg" ]]; then
|
|
1418
1484
|
local cfg_ratio
|
|
1419
1485
|
cfg_ratio=$(jq -r '.intelligence.ab_test_ratio // 0.2' "$daemon_cfg" 2>/dev/null || echo "0.2")
|
|
1420
|
-
# Convert ratio (0.0-1.0) to percentage (0-100)
|
|
1421
1486
|
ab_ratio=$(awk -v r="$cfg_ratio" 'BEGIN{printf "%d", r * 100}' 2>/dev/null || echo "20")
|
|
1422
1487
|
fi
|
|
1423
1488
|
|
|
1424
|
-
# Check if we have enough data points to graduate from A/B testing
|
|
1425
1489
|
local routing_file="${HOME}/.shipwright/optimization/model-routing.json"
|
|
1426
1490
|
local use_recommended=false
|
|
1427
1491
|
local ab_group="control"
|
|
1428
1492
|
|
|
1429
1493
|
if [[ -f "$routing_file" ]]; then
|
|
1430
|
-
local stage_samples
|
|
1431
|
-
stage_samples=$(jq -r --arg s "$id" '.[$s].sonnet_samples // 0' "$routing_file" 2>/dev/null || echo "0")
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
if [[ "$total_samples" -ge 50 ]]; then
|
|
1436
|
-
# Enough data — use optimizer's recommendation as default
|
|
1494
|
+
local stage_samples total_samples
|
|
1495
|
+
stage_samples=$(jq -r --arg s "$id" '.routes[$s].sonnet_samples // .[$s].sonnet_samples // 0' "$routing_file" 2>/dev/null || echo "0")
|
|
1496
|
+
total_samples=$(jq -r --arg s "$id" '((.routes[$s].sonnet_samples // .[$s].sonnet_samples // 0) + (.routes[$s].opus_samples // .[$s].opus_samples // 0))' "$routing_file" 2>/dev/null || echo "0")
|
|
1497
|
+
if [[ "${total_samples:-0}" -ge 50 ]]; then
|
|
1437
1498
|
use_recommended=true
|
|
1438
1499
|
ab_group="graduated"
|
|
1439
1500
|
fi
|
|
1440
1501
|
fi
|
|
1441
1502
|
|
|
1442
1503
|
if [[ "$use_recommended" != "true" ]]; then
|
|
1443
|
-
# A/B test: RANDOM % 100 < ab_ratio → use recommended
|
|
1444
1504
|
local roll=$((RANDOM % 100))
|
|
1445
1505
|
if [[ "$roll" -lt "$ab_ratio" ]]; then
|
|
1446
1506
|
use_recommended=true
|
|
1447
1507
|
ab_group="experiment"
|
|
1448
|
-
else
|
|
1449
|
-
ab_group="control"
|
|
1450
1508
|
fi
|
|
1451
1509
|
fi
|
|
1452
1510
|
|
|
@@ -1475,7 +1533,7 @@ run_pipeline() {
|
|
|
1475
1533
|
emit_event "stage.started" "issue=${ISSUE_NUMBER:-0}" "stage=$id"
|
|
1476
1534
|
|
|
1477
1535
|
# Mark GitHub Check Run as in-progress
|
|
1478
|
-
if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_checks_stage_update
|
|
1536
|
+
if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_checks_stage_update >/dev/null 2>&1; then
|
|
1479
1537
|
gh_checks_stage_update "$id" "in_progress" "" "Stage $id started" 2>/dev/null || true
|
|
1480
1538
|
fi
|
|
1481
1539
|
|
|
@@ -1491,7 +1549,9 @@ run_pipeline() {
|
|
|
1491
1549
|
timing=$(get_stage_timing "$id")
|
|
1492
1550
|
stage_dur_s=$(( $(now_epoch) - stage_start_epoch ))
|
|
1493
1551
|
success "Stage ${BOLD}$id${RESET} complete ${DIM}(${timing})${RESET}"
|
|
1494
|
-
emit_event "stage.completed" "issue=${ISSUE_NUMBER:-0}" "stage=$id" "duration_s=$stage_dur_s"
|
|
1552
|
+
emit_event "stage.completed" "issue=${ISSUE_NUMBER:-0}" "stage=$id" "duration_s=$stage_dur_s" "result=success"
|
|
1553
|
+
# Record model outcome for UCB1 learning
|
|
1554
|
+
type record_model_outcome >/dev/null 2>&1 && record_model_outcome "$stage_model_used" "$id" 1 "$stage_dur_s" 0 2>/dev/null || true
|
|
1495
1555
|
# Broadcast discovery for cross-pipeline learning
|
|
1496
1556
|
if [[ -x "$SCRIPT_DIR/sw-discovery.sh" ]]; then
|
|
1497
1557
|
local _disc_cat _disc_patterns _disc_text
|
|
@@ -1514,9 +1574,16 @@ run_pipeline() {
|
|
|
1514
1574
|
stage_dur_s=$(( $(now_epoch) - stage_start_epoch ))
|
|
1515
1575
|
error "Pipeline failed at stage: ${BOLD}$id${RESET}"
|
|
1516
1576
|
update_status "failed" "$id"
|
|
1517
|
-
emit_event "stage.failed"
|
|
1577
|
+
emit_event "stage.failed" \
|
|
1578
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
1579
|
+
"stage=$id" \
|
|
1580
|
+
"duration_s=$stage_dur_s" \
|
|
1581
|
+
"error=${LAST_STAGE_ERROR:-unknown}" \
|
|
1582
|
+
"error_class=${LAST_STAGE_ERROR_CLASS:-unknown}"
|
|
1518
1583
|
# Log model used for prediction feedback
|
|
1519
1584
|
echo "${id}|${stage_model_used}|false" >> "${ARTIFACTS_DIR}/model-routing.log"
|
|
1585
|
+
# Record model outcome for UCB1 learning
|
|
1586
|
+
type record_model_outcome >/dev/null 2>&1 && record_model_outcome "$stage_model_used" "$id" 0 "$stage_dur_s" 0 2>/dev/null || true
|
|
1520
1587
|
# Cancel any remaining in_progress check runs
|
|
1521
1588
|
pipeline_cancel_check_runs 2>/dev/null || true
|
|
1522
1589
|
return 1
|
|
@@ -1525,6 +1592,11 @@ run_pipeline() {
|
|
|
1525
1592
|
|
|
1526
1593
|
# Pipeline complete!
|
|
1527
1594
|
update_status "complete" ""
|
|
1595
|
+
PIPELINE_STAGES_PASSED="$completed"
|
|
1596
|
+
PIPELINE_SLOWEST_STAGE=""
|
|
1597
|
+
if type get_slowest_stage >/dev/null 2>&1; then
|
|
1598
|
+
PIPELINE_SLOWEST_STAGE=$(get_slowest_stage 2>/dev/null || true)
|
|
1599
|
+
fi
|
|
1528
1600
|
local total_dur=""
|
|
1529
1601
|
if [[ -n "$PIPELINE_START_EPOCH" ]]; then
|
|
1530
1602
|
total_dur=$(format_duration $(( $(now_epoch) - PIPELINE_START_EPOCH )))
|
|
@@ -1568,7 +1640,7 @@ run_pipeline() {
|
|
|
1568
1640
|
pipeline_post_completion_cleanup() {
|
|
1569
1641
|
local cleaned=0
|
|
1570
1642
|
|
|
1571
|
-
# 1. Clear checkpoints (they only matter for resume; pipeline is done)
|
|
1643
|
+
# 1. Clear checkpoints and context files (they only matter for resume; pipeline is done)
|
|
1572
1644
|
if [[ -d "${ARTIFACTS_DIR}/checkpoints" ]]; then
|
|
1573
1645
|
local cp_count=0
|
|
1574
1646
|
local cp_file
|
|
@@ -1577,6 +1649,11 @@ pipeline_post_completion_cleanup() {
|
|
|
1577
1649
|
rm -f "$cp_file"
|
|
1578
1650
|
cp_count=$((cp_count + 1))
|
|
1579
1651
|
done
|
|
1652
|
+
for cp_file in "${ARTIFACTS_DIR}/checkpoints"/*-claude-context.json; do
|
|
1653
|
+
[[ -f "$cp_file" ]] || continue
|
|
1654
|
+
rm -f "$cp_file"
|
|
1655
|
+
cp_count=$((cp_count + 1))
|
|
1656
|
+
done
|
|
1580
1657
|
if [[ "$cp_count" -gt 0 ]]; then
|
|
1581
1658
|
cleaned=$((cleaned + cp_count))
|
|
1582
1659
|
fi
|
|
@@ -1621,7 +1698,7 @@ pipeline_cancel_check_runs() {
|
|
|
1621
1698
|
return
|
|
1622
1699
|
fi
|
|
1623
1700
|
|
|
1624
|
-
if ! type gh_checks_stage_update
|
|
1701
|
+
if ! type gh_checks_stage_update >/dev/null 2>&1; then
|
|
1625
1702
|
return
|
|
1626
1703
|
fi
|
|
1627
1704
|
|
|
@@ -1673,7 +1750,7 @@ pipeline_setup_worktree() {
|
|
|
1673
1750
|
|
|
1674
1751
|
# Store original dir for cleanup, then cd into worktree
|
|
1675
1752
|
ORIGINAL_REPO_DIR="$(pwd)"
|
|
1676
|
-
cd "$worktree_path"
|
|
1753
|
+
cd "$worktree_path" || { error "Failed to cd into worktree: $worktree_path"; return 1; }
|
|
1677
1754
|
CLEANUP_WORKTREE=true
|
|
1678
1755
|
|
|
1679
1756
|
success "Worktree ready: ${CYAN}${worktree_path}${RESET} (branch: ${branch_name})"
|
|
@@ -1807,7 +1884,7 @@ run_dry_run() {
|
|
|
1807
1884
|
local optional_tools=("gh" "claude" "bc")
|
|
1808
1885
|
|
|
1809
1886
|
for tool in "${required_tools[@]}"; do
|
|
1810
|
-
if command -v "$tool"
|
|
1887
|
+
if command -v "$tool" >/dev/null 2>&1; then
|
|
1811
1888
|
echo -e " ${GREEN}✓${RESET} $tool"
|
|
1812
1889
|
else
|
|
1813
1890
|
echo -e " ${RED}✗${RESET} $tool ${RED}(required)${RESET}"
|
|
@@ -1816,7 +1893,7 @@ run_dry_run() {
|
|
|
1816
1893
|
done
|
|
1817
1894
|
|
|
1818
1895
|
for tool in "${optional_tools[@]}"; do
|
|
1819
|
-
if command -v "$tool"
|
|
1896
|
+
if command -v "$tool" >/dev/null 2>&1; then
|
|
1820
1897
|
echo -e " ${GREEN}✓${RESET} $tool"
|
|
1821
1898
|
else
|
|
1822
1899
|
echo -e " ${DIM}○${RESET} $tool"
|
|
@@ -1825,15 +1902,17 @@ run_dry_run() {
|
|
|
1825
1902
|
|
|
1826
1903
|
echo ""
|
|
1827
1904
|
|
|
1828
|
-
# Cost estimation
|
|
1905
|
+
# Cost estimation: use historical averages from past pipelines when available
|
|
1829
1906
|
echo -e "${BLUE}${BOLD}━━━ Estimated Resource Usage ━━━${RESET}"
|
|
1830
1907
|
echo ""
|
|
1831
1908
|
|
|
1832
|
-
|
|
1833
|
-
|
|
1909
|
+
local stages_json
|
|
1910
|
+
stages_json=$(jq '[.stages[] | select(.enabled == true)]' "$PIPELINE_CONFIG" 2>/dev/null || echo "[]")
|
|
1911
|
+
local est
|
|
1912
|
+
est=$(estimate_pipeline_cost "$stages_json")
|
|
1834
1913
|
local input_tokens_estimate output_tokens_estimate
|
|
1835
|
-
input_tokens_estimate=$(
|
|
1836
|
-
output_tokens_estimate=$(
|
|
1914
|
+
input_tokens_estimate=$(echo "$est" | jq -r '.input_tokens // 0')
|
|
1915
|
+
output_tokens_estimate=$(echo "$est" | jq -r '.output_tokens // 0')
|
|
1837
1916
|
|
|
1838
1917
|
# Calculate cost based on selected model
|
|
1839
1918
|
local input_rate output_rate input_cost output_cost total_cost
|
|
@@ -1848,11 +1927,11 @@ run_dry_run() {
|
|
|
1848
1927
|
echo -e " ${BOLD}Estimated Input Tokens:${RESET} ~$input_tokens_estimate"
|
|
1849
1928
|
echo -e " ${BOLD}Estimated Output Tokens:${RESET} ~$output_tokens_estimate"
|
|
1850
1929
|
echo -e " ${BOLD}Model Cost Rate:${RESET} $stage_model"
|
|
1851
|
-
echo -e " ${BOLD}Estimated Cost:${RESET} \$$total_cost USD
|
|
1930
|
+
echo -e " ${BOLD}Estimated Cost:${RESET} \$$total_cost USD"
|
|
1852
1931
|
echo ""
|
|
1853
1932
|
|
|
1854
1933
|
# Validate composed pipeline if intelligence is enabled
|
|
1855
|
-
if [[ -f "$ARTIFACTS_DIR/composed-pipeline.json" ]] && type composer_validate_pipeline
|
|
1934
|
+
if [[ -f "$ARTIFACTS_DIR/composed-pipeline.json" ]] && type composer_validate_pipeline >/dev/null 2>&1; then
|
|
1856
1935
|
echo -e "${BLUE}${BOLD}━━━ Intelligence-Composed Pipeline ━━━${RESET}"
|
|
1857
1936
|
echo ""
|
|
1858
1937
|
|
|
@@ -1877,6 +1956,100 @@ run_dry_run() {
|
|
|
1877
1956
|
return 0
|
|
1878
1957
|
}
|
|
1879
1958
|
|
|
1959
|
+
# ─── Reasoning Trace Generation ──────────────────────────────────────────────
|
|
1960
|
+
# Multi-step autonomous reasoning traces for pipeline start (before stages run)
|
|
1961
|
+
|
|
1962
|
+
generate_reasoning_trace() {
|
|
1963
|
+
local job_id="${SHIPWRIGHT_PIPELINE_ID:-$$}"
|
|
1964
|
+
local issue="${ISSUE_NUMBER:-}"
|
|
1965
|
+
local goal="${GOAL:-}"
|
|
1966
|
+
|
|
1967
|
+
# Step 1: Analyze issue complexity and risk
|
|
1968
|
+
local complexity="medium"
|
|
1969
|
+
local risk_score=50
|
|
1970
|
+
if [[ -n "$issue" ]] && type intelligence_analyze_issue >/dev/null 2>&1; then
|
|
1971
|
+
local issue_json analysis
|
|
1972
|
+
issue_json=$(gh issue view "$issue" --json number,title,body,labels 2>/dev/null || echo "{}")
|
|
1973
|
+
if [[ -n "$issue_json" && "$issue_json" != "{}" ]]; then
|
|
1974
|
+
analysis=$(intelligence_analyze_issue "$issue_json" 2>/dev/null || echo "")
|
|
1975
|
+
if [[ -n "$analysis" ]]; then
|
|
1976
|
+
local comp_num
|
|
1977
|
+
comp_num=$(echo "$analysis" | jq -r '.complexity // 5' 2>/dev/null || echo "5")
|
|
1978
|
+
if [[ "$comp_num" -le 3 ]]; then
|
|
1979
|
+
complexity="low"
|
|
1980
|
+
elif [[ "$comp_num" -le 6 ]]; then
|
|
1981
|
+
complexity="medium"
|
|
1982
|
+
else
|
|
1983
|
+
complexity="high"
|
|
1984
|
+
fi
|
|
1985
|
+
risk_score=$((100 - $(echo "$analysis" | jq -r '.success_probability // 50' 2>/dev/null || echo "50")))
|
|
1986
|
+
fi
|
|
1987
|
+
fi
|
|
1988
|
+
elif [[ -n "$goal" ]]; then
|
|
1989
|
+
issue_json=$(jq -n --arg title "${goal}" --arg body "" '{title: $title, body: $body, labels: []}')
|
|
1990
|
+
if type intelligence_analyze_issue >/dev/null 2>&1; then
|
|
1991
|
+
analysis=$(intelligence_analyze_issue "$issue_json" 2>/dev/null || echo "")
|
|
1992
|
+
if [[ -n "$analysis" ]]; then
|
|
1993
|
+
local comp_num
|
|
1994
|
+
comp_num=$(echo "$analysis" | jq -r '.complexity // 5' 2>/dev/null || echo "5")
|
|
1995
|
+
if [[ "$comp_num" -le 3 ]]; then complexity="low"; elif [[ "$comp_num" -le 6 ]]; then complexity="medium"; else complexity="high"; fi
|
|
1996
|
+
risk_score=$((100 - $(echo "$analysis" | jq -r '.success_probability // 50' 2>/dev/null || echo "50")))
|
|
1997
|
+
fi
|
|
1998
|
+
fi
|
|
1999
|
+
fi
|
|
2000
|
+
|
|
2001
|
+
# Step 2: Query similar past issues
|
|
2002
|
+
local similar_context=""
|
|
2003
|
+
if type memory_semantic_search >/dev/null 2>&1 && [[ -n "$goal" ]]; then
|
|
2004
|
+
similar_context=$(memory_semantic_search "$goal" "" 3 2>/dev/null || echo "")
|
|
2005
|
+
fi
|
|
2006
|
+
|
|
2007
|
+
# Step 3: Select template using Thompson sampling
|
|
2008
|
+
local selected_template="${PIPELINE_TEMPLATE:-}"
|
|
2009
|
+
if [[ -z "$selected_template" ]] && type thompson_select_template >/dev/null 2>&1; then
|
|
2010
|
+
selected_template=$(thompson_select_template "$complexity" 2>/dev/null || echo "standard")
|
|
2011
|
+
fi
|
|
2012
|
+
[[ -z "$selected_template" ]] && selected_template="standard"
|
|
2013
|
+
|
|
2014
|
+
# Step 4: Predict failure modes from memory
|
|
2015
|
+
local failure_predictions=""
|
|
2016
|
+
if type memory_semantic_search >/dev/null 2>&1 && [[ -n "$goal" ]]; then
|
|
2017
|
+
failure_predictions=$(memory_semantic_search "failure error $goal" "" 3 2>/dev/null || echo "")
|
|
2018
|
+
fi
|
|
2019
|
+
|
|
2020
|
+
# Save reasoning traces to DB
|
|
2021
|
+
if type db_save_reasoning_trace >/dev/null 2>&1; then
|
|
2022
|
+
db_save_reasoning_trace "$job_id" "complexity_analysis" \
|
|
2023
|
+
"issue=$issue goal=$goal" \
|
|
2024
|
+
"Analyzed complexity=$complexity risk=$risk_score" \
|
|
2025
|
+
"complexity=$complexity risk_score=$risk_score" 0.7 2>/dev/null || true
|
|
2026
|
+
|
|
2027
|
+
db_save_reasoning_trace "$job_id" "template_selection" \
|
|
2028
|
+
"complexity=$complexity historical_outcomes" \
|
|
2029
|
+
"Thompson sampling over historical success rates" \
|
|
2030
|
+
"template=$selected_template" 0.8 2>/dev/null || true
|
|
2031
|
+
|
|
2032
|
+
if [[ -n "$similar_context" && "$similar_context" != "[]" ]]; then
|
|
2033
|
+
db_save_reasoning_trace "$job_id" "similar_issues" \
|
|
2034
|
+
"$goal" \
|
|
2035
|
+
"Found similar past issues for context injection" \
|
|
2036
|
+
"$similar_context" 0.6 2>/dev/null || true
|
|
2037
|
+
fi
|
|
2038
|
+
|
|
2039
|
+
if [[ -n "$failure_predictions" && "$failure_predictions" != "[]" ]]; then
|
|
2040
|
+
db_save_reasoning_trace "$job_id" "failure_prediction" \
|
|
2041
|
+
"$goal" \
|
|
2042
|
+
"Predicted potential failure modes from history" \
|
|
2043
|
+
"$failure_predictions" 0.5 2>/dev/null || true
|
|
2044
|
+
fi
|
|
2045
|
+
fi
|
|
2046
|
+
|
|
2047
|
+
# Export for use by pipeline stages
|
|
2048
|
+
[[ -n "$selected_template" && -z "${PIPELINE_TEMPLATE:-}" ]] && export PIPELINE_TEMPLATE="$selected_template"
|
|
2049
|
+
|
|
2050
|
+
emit_event "reasoning.trace" "job_id=$job_id" "complexity=$complexity" "risk=$risk_score" "template=${selected_template:-standard}" 2>/dev/null || true
|
|
2051
|
+
}
|
|
2052
|
+
|
|
1880
2053
|
# ─── Subcommands ────────────────────────────────────────────────────────────
|
|
1881
2054
|
|
|
1882
2055
|
pipeline_start() {
|
|
@@ -1898,6 +2071,13 @@ pipeline_start() {
|
|
|
1898
2071
|
info "Using repository: $ORIGINAL_REPO_DIR"
|
|
1899
2072
|
fi
|
|
1900
2073
|
|
|
2074
|
+
# Bootstrap optimization & memory if cold start (before first intelligence use)
|
|
2075
|
+
if [[ -f "$SCRIPT_DIR/lib/bootstrap.sh" ]]; then
|
|
2076
|
+
source "$SCRIPT_DIR/lib/bootstrap.sh"
|
|
2077
|
+
[[ ! -f "$HOME/.shipwright/optimization/iteration-model.json" ]] && bootstrap_optimization 2>/dev/null || true
|
|
2078
|
+
[[ ! -f "$HOME/.shipwright/memory/patterns.json" ]] && bootstrap_memory 2>/dev/null || true
|
|
2079
|
+
fi
|
|
2080
|
+
|
|
1901
2081
|
if [[ -z "$GOAL" && -z "$ISSUE_NUMBER" ]]; then
|
|
1902
2082
|
error "Must provide --goal or --issue"
|
|
1903
2083
|
echo -e " Example: ${DIM}shipwright pipeline start --goal \"Add JWT auth\"${RESET}"
|
|
@@ -1905,7 +2085,7 @@ pipeline_start() {
|
|
|
1905
2085
|
exit 1
|
|
1906
2086
|
fi
|
|
1907
2087
|
|
|
1908
|
-
if ! command -v jq
|
|
2088
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
1909
2089
|
error "jq is required. Install it: brew install jq"
|
|
1910
2090
|
exit 1
|
|
1911
2091
|
fi
|
|
@@ -1923,6 +2103,13 @@ pipeline_start() {
|
|
|
1923
2103
|
|
|
1924
2104
|
setup_dirs
|
|
1925
2105
|
|
|
2106
|
+
# Generate reasoning trace (complexity analysis, template selection, failure predictions)
|
|
2107
|
+
local user_specified_pipeline="$PIPELINE_NAME"
|
|
2108
|
+
generate_reasoning_trace 2>/dev/null || true
|
|
2109
|
+
if [[ -n "${PIPELINE_TEMPLATE:-}" && "$user_specified_pipeline" == "standard" ]]; then
|
|
2110
|
+
PIPELINE_NAME="$PIPELINE_TEMPLATE"
|
|
2111
|
+
fi
|
|
2112
|
+
|
|
1926
2113
|
# Check for existing pipeline
|
|
1927
2114
|
if [[ -f "$STATE_FILE" ]]; then
|
|
1928
2115
|
local existing_status
|
|
@@ -1942,7 +2129,87 @@ pipeline_start() {
|
|
|
1942
2129
|
gh_init
|
|
1943
2130
|
|
|
1944
2131
|
load_pipeline_config
|
|
1945
|
-
|
|
2132
|
+
|
|
2133
|
+
# Checkpoint resume: when --resume is passed, try DB first, then file-based
|
|
2134
|
+
checkpoint_stage=""
|
|
2135
|
+
checkpoint_iteration=0
|
|
2136
|
+
if $RESUME_FROM_CHECKPOINT && type db_load_checkpoint >/dev/null 2>&1; then
|
|
2137
|
+
local saved_checkpoint
|
|
2138
|
+
saved_checkpoint=$(db_load_checkpoint "pipeline-${SHIPWRIGHT_PIPELINE_ID:-$$}" 2>/dev/null || echo "")
|
|
2139
|
+
if [[ -n "$saved_checkpoint" ]]; then
|
|
2140
|
+
checkpoint_stage=$(echo "$saved_checkpoint" | jq -r '.stage // ""' 2>/dev/null || echo "")
|
|
2141
|
+
if [[ -n "$checkpoint_stage" ]]; then
|
|
2142
|
+
info "Resuming from DB checkpoint: stage=$checkpoint_stage"
|
|
2143
|
+
checkpoint_iteration=$(echo "$saved_checkpoint" | jq -r '.iteration // 0' 2>/dev/null || echo "0")
|
|
2144
|
+
# Build COMPLETED_STAGES: all enabled stages before checkpoint_stage
|
|
2145
|
+
local enabled_list before_list=""
|
|
2146
|
+
enabled_list=$(jq -r '.stages[] | select(.enabled == true) | .id' "$PIPELINE_CONFIG" 2>/dev/null) || true
|
|
2147
|
+
local s
|
|
2148
|
+
while IFS= read -r s; do
|
|
2149
|
+
[[ -z "$s" ]] && continue
|
|
2150
|
+
if [[ "$s" == "$checkpoint_stage" ]]; then
|
|
2151
|
+
break
|
|
2152
|
+
fi
|
|
2153
|
+
[[ -n "$before_list" ]] && before_list="${before_list},${s}" || before_list="$s"
|
|
2154
|
+
done <<< "$enabled_list"
|
|
2155
|
+
if [[ -n "$before_list" ]]; then
|
|
2156
|
+
COMPLETED_STAGES="${before_list}"
|
|
2157
|
+
SELF_HEAL_COUNT="${checkpoint_iteration}"
|
|
2158
|
+
fi
|
|
2159
|
+
fi
|
|
2160
|
+
fi
|
|
2161
|
+
fi
|
|
2162
|
+
if $RESUME_FROM_CHECKPOINT && [[ -z "$checkpoint_stage" ]] && [[ -d "${ARTIFACTS_DIR}/checkpoints" ]]; then
|
|
2163
|
+
local cp_dir="${ARTIFACTS_DIR}/checkpoints"
|
|
2164
|
+
local latest_cp="" latest_mtime=0
|
|
2165
|
+
local f
|
|
2166
|
+
for f in "$cp_dir"/*-checkpoint.json; do
|
|
2167
|
+
[[ -f "$f" ]] || continue
|
|
2168
|
+
local mtime
|
|
2169
|
+
mtime=$(file_mtime "$f" 2>/dev/null || echo "0")
|
|
2170
|
+
if [[ "${mtime:-0}" -gt "$latest_mtime" ]]; then
|
|
2171
|
+
latest_mtime="${mtime}"
|
|
2172
|
+
latest_cp="$f"
|
|
2173
|
+
fi
|
|
2174
|
+
done
|
|
2175
|
+
if [[ -n "$latest_cp" && -x "$SCRIPT_DIR/sw-checkpoint.sh" ]]; then
|
|
2176
|
+
checkpoint_stage="$(basename "$latest_cp" -checkpoint.json)"
|
|
2177
|
+
local cp_json
|
|
2178
|
+
cp_json="$("$SCRIPT_DIR/sw-checkpoint.sh" restore --stage "$checkpoint_stage" 2>/dev/null)" || true
|
|
2179
|
+
if [[ -n "$cp_json" ]] && command -v jq >/dev/null 2>&1; then
|
|
2180
|
+
checkpoint_iteration="$(echo "$cp_json" | jq -r '.iteration // 0' 2>/dev/null)" || checkpoint_iteration=0
|
|
2181
|
+
info "Checkpoint resume: stage=${checkpoint_stage} iteration=${checkpoint_iteration}"
|
|
2182
|
+
# Build COMPLETED_STAGES: all enabled stages before checkpoint_stage
|
|
2183
|
+
local enabled_list before_list=""
|
|
2184
|
+
enabled_list="$(jq -r '.stages[] | select(.enabled == true) | .id' "$PIPELINE_CONFIG" 2>/dev/null)" || true
|
|
2185
|
+
local s
|
|
2186
|
+
while IFS= read -r s; do
|
|
2187
|
+
[[ -z "$s" ]] && continue
|
|
2188
|
+
if [[ "$s" == "$checkpoint_stage" ]]; then
|
|
2189
|
+
break
|
|
2190
|
+
fi
|
|
2191
|
+
[[ -n "$before_list" ]] && before_list="${before_list},${s}" || before_list="$s"
|
|
2192
|
+
done <<< "$enabled_list"
|
|
2193
|
+
if [[ -n "$before_list" ]]; then
|
|
2194
|
+
COMPLETED_STAGES="${before_list}"
|
|
2195
|
+
SELF_HEAL_COUNT="${checkpoint_iteration}"
|
|
2196
|
+
fi
|
|
2197
|
+
fi
|
|
2198
|
+
fi
|
|
2199
|
+
fi
|
|
2200
|
+
|
|
2201
|
+
# Restore from state file if resuming (failed/interrupted pipeline); else initialize fresh
|
|
2202
|
+
if $RESUME_FROM_CHECKPOINT && [[ -f "$STATE_FILE" ]]; then
|
|
2203
|
+
local existing_status
|
|
2204
|
+
existing_status="$(sed -n 's/^status: *//p' "$STATE_FILE" | head -1)"
|
|
2205
|
+
if [[ "$existing_status" == "failed" || "$existing_status" == "interrupted" ]]; then
|
|
2206
|
+
resume_state
|
|
2207
|
+
else
|
|
2208
|
+
initialize_state
|
|
2209
|
+
fi
|
|
2210
|
+
else
|
|
2211
|
+
initialize_state
|
|
2212
|
+
fi
|
|
1946
2213
|
|
|
1947
2214
|
# CI resume: restore branch + goal context when intake is skipped
|
|
1948
2215
|
if [[ -n "${COMPLETED_STAGES:-}" ]] && echo "$COMPLETED_STAGES" | tr ',' '\n' | grep -qx "intake"; then
|
|
@@ -1951,7 +2218,7 @@ pipeline_start() {
|
|
|
1951
2218
|
|
|
1952
2219
|
# Restore GOAL from issue if not already set
|
|
1953
2220
|
if [[ -z "$GOAL" && -n "$ISSUE_NUMBER" ]]; then
|
|
1954
|
-
GOAL=$(gh issue view "$ISSUE_NUMBER" --json title -q .title 2>/dev/null || echo "Issue #${ISSUE_NUMBER}")
|
|
2221
|
+
GOAL=$(_timeout "$(_config_get_int "network.gh_timeout" 30 2>/dev/null || echo 30)" gh issue view "$ISSUE_NUMBER" --json title -q .title 2>/dev/null || echo "Issue #${ISSUE_NUMBER}")
|
|
1955
2222
|
info "CI resume: goal from issue — ${GOAL}"
|
|
1956
2223
|
fi
|
|
1957
2224
|
|
|
@@ -2018,11 +2285,38 @@ pipeline_start() {
|
|
|
2018
2285
|
return $?
|
|
2019
2286
|
fi
|
|
2020
2287
|
|
|
2288
|
+
# Capture predictions for feedback loop (intelligence → actuals → learning)
|
|
2289
|
+
if type intelligence_analyze_issue >/dev/null 2>&1 && (type intelligence_estimate_iterations >/dev/null 2>&1 || type intelligence_predict_cost >/dev/null 2>&1); then
|
|
2290
|
+
local issue_json="${INTELLIGENCE_ANALYSIS:-}"
|
|
2291
|
+
if [[ -z "$issue_json" || "$issue_json" == "{}" ]]; then
|
|
2292
|
+
if [[ -n "$ISSUE_NUMBER" ]]; then
|
|
2293
|
+
issue_json=$(gh issue view "$ISSUE_NUMBER" --json number,title,body,labels 2>/dev/null || echo "{}")
|
|
2294
|
+
else
|
|
2295
|
+
issue_json=$(jq -n --arg title "${GOAL:-untitled}" --arg body "" '{title: $title, body: $body, labels: []}')
|
|
2296
|
+
fi
|
|
2297
|
+
if [[ -n "$issue_json" && "$issue_json" != "{}" ]]; then
|
|
2298
|
+
issue_json=$(intelligence_analyze_issue "$issue_json" 2>/dev/null || echo "{}")
|
|
2299
|
+
fi
|
|
2300
|
+
fi
|
|
2301
|
+
if [[ -n "$issue_json" && "$issue_json" != "{}" ]]; then
|
|
2302
|
+
if type intelligence_estimate_iterations >/dev/null 2>&1; then
|
|
2303
|
+
PREDICTED_ITERATIONS=$(intelligence_estimate_iterations "$issue_json" "" 2>/dev/null || echo "")
|
|
2304
|
+
export PREDICTED_ITERATIONS
|
|
2305
|
+
fi
|
|
2306
|
+
if type intelligence_predict_cost >/dev/null 2>&1; then
|
|
2307
|
+
local cost_json
|
|
2308
|
+
cost_json=$(intelligence_predict_cost "$issue_json" "{}" 2>/dev/null || echo "{}")
|
|
2309
|
+
PREDICTED_COST=$(echo "$cost_json" | jq -r '.estimated_cost_usd // empty' 2>/dev/null || echo "")
|
|
2310
|
+
export PREDICTED_COST
|
|
2311
|
+
fi
|
|
2312
|
+
fi
|
|
2313
|
+
fi
|
|
2314
|
+
|
|
2021
2315
|
# Start background heartbeat writer
|
|
2022
2316
|
start_heartbeat
|
|
2023
2317
|
|
|
2024
2318
|
# Initialize GitHub Check Runs for all pipeline stages
|
|
2025
|
-
if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_checks_pipeline_start
|
|
2319
|
+
if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_checks_pipeline_start >/dev/null 2>&1; then
|
|
2026
2320
|
local head_sha
|
|
2027
2321
|
head_sha=$(git rev-parse HEAD 2>/dev/null || echo "")
|
|
2028
2322
|
if [[ -n "$head_sha" && -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
|
|
@@ -2038,12 +2332,15 @@ pipeline_start() {
|
|
|
2038
2332
|
|
|
2039
2333
|
emit_event "pipeline.started" \
|
|
2040
2334
|
"issue=${ISSUE_NUMBER:-0}" \
|
|
2335
|
+
"template=${PIPELINE_NAME}" \
|
|
2336
|
+
"complexity=${INTELLIGENCE_COMPLEXITY:-0}" \
|
|
2337
|
+
"machine=$(hostname 2>/dev/null || echo "unknown")" \
|
|
2041
2338
|
"pipeline=${PIPELINE_NAME}" \
|
|
2042
2339
|
"model=${MODEL:-opus}" \
|
|
2043
2340
|
"goal=${GOAL}"
|
|
2044
2341
|
|
|
2045
2342
|
# Durable WAL: publish pipeline start event
|
|
2046
|
-
if type publish_event
|
|
2343
|
+
if type publish_event >/dev/null 2>&1; then
|
|
2047
2344
|
publish_event "pipeline.started" "{\"issue\":\"${ISSUE_NUMBER:-0}\",\"pipeline\":\"${PIPELINE_NAME}\",\"goal\":\"${GOAL:0:200}\"}" 2>/dev/null || true
|
|
2048
2345
|
fi
|
|
2049
2346
|
|
|
@@ -2051,6 +2348,18 @@ pipeline_start() {
|
|
|
2051
2348
|
local exit_code=$?
|
|
2052
2349
|
PIPELINE_EXIT_CODE="$exit_code"
|
|
2053
2350
|
|
|
2351
|
+
# Compute total cost for pipeline.completed (prefer actual from Claude when available)
|
|
2352
|
+
local model_key="${MODEL:-sonnet}"
|
|
2353
|
+
local total_cost
|
|
2354
|
+
if [[ -n "${TOTAL_COST_USD:-}" && "${TOTAL_COST_USD}" != "0" && "${TOTAL_COST_USD}" != "null" ]]; then
|
|
2355
|
+
total_cost="${TOTAL_COST_USD}"
|
|
2356
|
+
else
|
|
2357
|
+
local input_cost output_cost
|
|
2358
|
+
input_cost=$(awk -v tokens="$TOTAL_INPUT_TOKENS" -v rate="$(echo "$COST_MODEL_RATES" | jq -r ".${model_key}.input // 3")" 'BEGIN{printf "%.4f", (tokens / 1000000) * rate}')
|
|
2359
|
+
output_cost=$(awk -v tokens="$TOTAL_OUTPUT_TOKENS" -v rate="$(echo "$COST_MODEL_RATES" | jq -r ".${model_key}.output // 15")" 'BEGIN{printf "%.4f", (tokens / 1000000) * rate}')
|
|
2360
|
+
total_cost=$(awk -v i="$input_cost" -v o="$output_cost" 'BEGIN{printf "%.4f", i + o}')
|
|
2361
|
+
fi
|
|
2362
|
+
|
|
2054
2363
|
# Send completion notification + event
|
|
2055
2364
|
local total_dur_s=""
|
|
2056
2365
|
[[ -n "$PIPELINE_START_EPOCH" ]] && total_dur_s=$(( $(now_epoch) - PIPELINE_START_EPOCH ))
|
|
@@ -2064,10 +2373,16 @@ pipeline_start() {
|
|
|
2064
2373
|
"issue=${ISSUE_NUMBER:-0}" \
|
|
2065
2374
|
"result=success" \
|
|
2066
2375
|
"duration_s=${total_dur_s:-0}" \
|
|
2376
|
+
"iterations=$((SELF_HEAL_COUNT + 1))" \
|
|
2377
|
+
"template=${PIPELINE_NAME}" \
|
|
2378
|
+
"complexity=${INTELLIGENCE_COMPLEXITY:-0}" \
|
|
2379
|
+
"stages_passed=${PIPELINE_STAGES_PASSED:-0}" \
|
|
2380
|
+
"slowest_stage=${PIPELINE_SLOWEST_STAGE:-}" \
|
|
2067
2381
|
"pr_url=${pr_url:-}" \
|
|
2068
2382
|
"agent_id=${PIPELINE_AGENT_ID}" \
|
|
2069
2383
|
"input_tokens=$TOTAL_INPUT_TOKENS" \
|
|
2070
2384
|
"output_tokens=$TOTAL_OUTPUT_TOKENS" \
|
|
2385
|
+
"total_cost=$total_cost" \
|
|
2071
2386
|
"self_heal_count=$SELF_HEAL_COUNT"
|
|
2072
2387
|
|
|
2073
2388
|
# Auto-ingest pipeline outcome into recruit profiles
|
|
@@ -2080,10 +2395,15 @@ pipeline_start() {
|
|
|
2080
2395
|
"issue=${ISSUE_NUMBER:-0}" \
|
|
2081
2396
|
"result=failure" \
|
|
2082
2397
|
"duration_s=${total_dur_s:-0}" \
|
|
2398
|
+
"iterations=$((SELF_HEAL_COUNT + 1))" \
|
|
2399
|
+
"template=${PIPELINE_NAME}" \
|
|
2400
|
+
"complexity=${INTELLIGENCE_COMPLEXITY:-0}" \
|
|
2083
2401
|
"failed_stage=${CURRENT_STAGE_ID:-unknown}" \
|
|
2402
|
+
"error_class=${LAST_STAGE_ERROR_CLASS:-unknown}" \
|
|
2084
2403
|
"agent_id=${PIPELINE_AGENT_ID}" \
|
|
2085
2404
|
"input_tokens=$TOTAL_INPUT_TOKENS" \
|
|
2086
2405
|
"output_tokens=$TOTAL_OUTPUT_TOKENS" \
|
|
2406
|
+
"total_cost=$total_cost" \
|
|
2087
2407
|
"self_heal_count=$SELF_HEAL_COUNT"
|
|
2088
2408
|
|
|
2089
2409
|
# Auto-ingest pipeline outcome into recruit profiles
|
|
@@ -2121,7 +2441,7 @@ pipeline_start() {
|
|
|
2121
2441
|
"success=$pipeline_success"
|
|
2122
2442
|
|
|
2123
2443
|
# Close intelligence prediction feedback loop — validate predicted vs actual
|
|
2124
|
-
if type intelligence_validate_prediction
|
|
2444
|
+
if type intelligence_validate_prediction >/dev/null 2>&1 && [[ -n "${ISSUE_NUMBER:-}" ]]; then
|
|
2125
2445
|
intelligence_validate_prediction \
|
|
2126
2446
|
"$ISSUE_NUMBER" \
|
|
2127
2447
|
"${INTELLIGENCE_COMPLEXITY:-0}" \
|
|
@@ -2129,6 +2449,12 @@ pipeline_start() {
|
|
|
2129
2449
|
"$pipeline_success" 2>/dev/null || true
|
|
2130
2450
|
fi
|
|
2131
2451
|
|
|
2452
|
+
# Validate iterations prediction against actuals (cost validation moved below after total_cost is computed)
|
|
2453
|
+
local ACTUAL_ITERATIONS=$((SELF_HEAL_COUNT + 1))
|
|
2454
|
+
if [[ -n "${PREDICTED_ITERATIONS:-}" ]] && type intelligence_validate_prediction >/dev/null 2>&1; then
|
|
2455
|
+
intelligence_validate_prediction "iterations" "$PREDICTED_ITERATIONS" "$ACTUAL_ITERATIONS" 2>/dev/null || true
|
|
2456
|
+
fi
|
|
2457
|
+
|
|
2132
2458
|
# Close predictive anomaly feedback loop — confirm whether flagged anomalies were real
|
|
2133
2459
|
if [[ -x "$SCRIPT_DIR/sw-predictive.sh" ]]; then
|
|
2134
2460
|
local _actual_failure="false"
|
|
@@ -2144,7 +2470,8 @@ pipeline_start() {
|
|
|
2144
2470
|
"issue=${ISSUE_NUMBER:-0}" \
|
|
2145
2471
|
"template=${PIPELINE_NAME}" \
|
|
2146
2472
|
"success=$pipeline_success" \
|
|
2147
|
-
"duration_s=${total_dur_s:-0}"
|
|
2473
|
+
"duration_s=${total_dur_s:-0}" \
|
|
2474
|
+
"complexity=${INTELLIGENCE_COMPLEXITY:-0}"
|
|
2148
2475
|
|
|
2149
2476
|
# Risk prediction vs actual failure
|
|
2150
2477
|
local predicted_risk="${INTELLIGENCE_RISK_SCORE:-0}"
|
|
@@ -2167,20 +2494,26 @@ pipeline_start() {
|
|
|
2167
2494
|
fi
|
|
2168
2495
|
|
|
2169
2496
|
# Record pipeline outcome for model routing feedback loop
|
|
2170
|
-
if type optimize_analyze_outcome
|
|
2497
|
+
if type optimize_analyze_outcome >/dev/null 2>&1; then
|
|
2171
2498
|
optimize_analyze_outcome "$STATE_FILE" 2>/dev/null || true
|
|
2172
|
-
# Tune template weights based on accumulated outcomes
|
|
2173
|
-
if type optimize_tune_templates &>/dev/null 2>&1; then
|
|
2174
|
-
optimize_tune_templates 2>/dev/null || true
|
|
2175
|
-
fi
|
|
2176
2499
|
fi
|
|
2177
2500
|
|
|
2178
|
-
|
|
2501
|
+
# Auto-learn after pipeline completion (non-blocking)
|
|
2502
|
+
if type optimize_tune_templates &>/dev/null; then
|
|
2503
|
+
(
|
|
2504
|
+
optimize_tune_templates 2>/dev/null
|
|
2505
|
+
optimize_learn_iterations 2>/dev/null
|
|
2506
|
+
optimize_route_models 2>/dev/null
|
|
2507
|
+
optimize_learn_risk_keywords 2>/dev/null
|
|
2508
|
+
) &
|
|
2509
|
+
fi
|
|
2510
|
+
|
|
2511
|
+
if type memory_finalize_pipeline >/dev/null 2>&1; then
|
|
2179
2512
|
memory_finalize_pipeline "$STATE_FILE" "$ARTIFACTS_DIR" 2>/dev/null || true
|
|
2180
2513
|
fi
|
|
2181
2514
|
|
|
2182
2515
|
# Broadcast discovery for cross-pipeline learning
|
|
2183
|
-
if type broadcast_discovery
|
|
2516
|
+
if type broadcast_discovery >/dev/null 2>&1; then
|
|
2184
2517
|
local _disc_result="failure"
|
|
2185
2518
|
[[ "$exit_code" -eq 0 ]] && _disc_result="success"
|
|
2186
2519
|
local _disc_files=""
|
|
@@ -2209,6 +2542,29 @@ pipeline_start() {
|
|
|
2209
2542
|
"model=$model_key" \
|
|
2210
2543
|
"cost_usd=$total_cost"
|
|
2211
2544
|
|
|
2545
|
+
# Record pipeline outcome for Thompson sampling / outcome-based learning
|
|
2546
|
+
if type db_record_outcome >/dev/null 2>&1; then
|
|
2547
|
+
local _outcome_success=0
|
|
2548
|
+
[[ "$exit_code" -eq 0 ]] && _outcome_success=1
|
|
2549
|
+
local _outcome_complexity="medium"
|
|
2550
|
+
[[ "${INTELLIGENCE_COMPLEXITY:-5}" -le 3 ]] && _outcome_complexity="low"
|
|
2551
|
+
[[ "${INTELLIGENCE_COMPLEXITY:-5}" -ge 7 ]] && _outcome_complexity="high"
|
|
2552
|
+
db_record_outcome \
|
|
2553
|
+
"${SHIPWRIGHT_PIPELINE_ID:-pipeline-$$-${ISSUE_NUMBER:-0}}" \
|
|
2554
|
+
"${ISSUE_NUMBER:-}" \
|
|
2555
|
+
"${PIPELINE_NAME:-standard}" \
|
|
2556
|
+
"$_outcome_success" \
|
|
2557
|
+
"${total_dur_s:-0}" \
|
|
2558
|
+
"${SELF_HEAL_COUNT:-0}" \
|
|
2559
|
+
"${total_cost:-0}" \
|
|
2560
|
+
"$_outcome_complexity" 2>/dev/null || true
|
|
2561
|
+
fi
|
|
2562
|
+
|
|
2563
|
+
# Validate cost prediction against actual (after total_cost is computed)
|
|
2564
|
+
if [[ -n "${PREDICTED_COST:-}" ]] && type intelligence_validate_prediction >/dev/null 2>&1; then
|
|
2565
|
+
intelligence_validate_prediction "cost" "$PREDICTED_COST" "$total_cost" 2>/dev/null || true
|
|
2566
|
+
fi
|
|
2567
|
+
|
|
2212
2568
|
return $exit_code
|
|
2213
2569
|
}
|
|
2214
2570
|
|