shipwright-cli 1.7.1 → 1.9.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/.claude/agents/code-reviewer.md +90 -0
- package/.claude/agents/devops-engineer.md +142 -0
- package/.claude/agents/pipeline-agent.md +80 -0
- package/.claude/agents/shell-script-specialist.md +150 -0
- package/.claude/agents/test-specialist.md +196 -0
- package/.claude/hooks/post-tool-use.sh +38 -0
- package/.claude/hooks/pre-tool-use.sh +25 -0
- package/.claude/hooks/session-started.sh +37 -0
- package/README.md +212 -814
- package/claude-code/CLAUDE.md.shipwright +54 -0
- package/claude-code/hooks/notify-idle.sh +2 -2
- package/claude-code/hooks/session-start.sh +24 -0
- package/claude-code/hooks/task-completed.sh +6 -2
- package/claude-code/settings.json.template +12 -0
- package/dashboard/public/app.js +4422 -0
- package/dashboard/public/index.html +816 -0
- package/dashboard/public/styles.css +4755 -0
- package/dashboard/server.ts +4315 -0
- package/docs/KNOWN-ISSUES.md +18 -10
- package/docs/TIPS.md +38 -26
- package/docs/patterns/README.md +33 -23
- package/package.json +9 -5
- package/scripts/adapters/iterm2-adapter.sh +1 -1
- package/scripts/adapters/tmux-adapter.sh +52 -23
- package/scripts/adapters/wezterm-adapter.sh +26 -14
- package/scripts/lib/compat.sh +200 -0
- package/scripts/lib/helpers.sh +72 -0
- package/scripts/postinstall.mjs +72 -13
- package/scripts/{cct → sw} +109 -21
- package/scripts/sw-adversarial.sh +274 -0
- package/scripts/sw-architecture-enforcer.sh +330 -0
- package/scripts/sw-checkpoint.sh +390 -0
- package/scripts/{cct-cleanup.sh → sw-cleanup.sh} +3 -1
- package/scripts/sw-connect.sh +619 -0
- package/scripts/{cct-cost.sh → sw-cost.sh} +368 -34
- package/scripts/{cct-daemon.sh → sw-daemon.sh} +2217 -204
- package/scripts/sw-dashboard.sh +477 -0
- package/scripts/sw-developer-simulation.sh +252 -0
- package/scripts/sw-docs.sh +635 -0
- package/scripts/sw-doctor.sh +907 -0
- package/scripts/{cct-fix.sh → sw-fix.sh} +10 -6
- package/scripts/{cct-fleet.sh → sw-fleet.sh} +498 -22
- package/scripts/sw-github-checks.sh +521 -0
- package/scripts/sw-github-deploy.sh +533 -0
- package/scripts/sw-github-graphql.sh +972 -0
- package/scripts/sw-heartbeat.sh +293 -0
- package/scripts/{cct-init.sh → sw-init.sh} +144 -11
- package/scripts/sw-intelligence.sh +1196 -0
- package/scripts/sw-jira.sh +643 -0
- package/scripts/sw-launchd.sh +364 -0
- package/scripts/sw-linear.sh +648 -0
- package/scripts/{cct-logs.sh → sw-logs.sh} +72 -2
- package/scripts/{cct-loop.sh → sw-loop.sh} +534 -44
- package/scripts/{cct-memory.sh → sw-memory.sh} +321 -38
- package/scripts/sw-patrol-meta.sh +417 -0
- package/scripts/sw-pipeline-composer.sh +455 -0
- package/scripts/{cct-pipeline.sh → sw-pipeline.sh} +2319 -178
- package/scripts/sw-predictive.sh +820 -0
- package/scripts/{cct-prep.sh → sw-prep.sh} +339 -49
- package/scripts/{cct-ps.sh → sw-ps.sh} +6 -4
- package/scripts/{cct-reaper.sh → sw-reaper.sh} +6 -4
- package/scripts/sw-remote.sh +687 -0
- package/scripts/sw-self-optimize.sh +947 -0
- package/scripts/sw-session.sh +519 -0
- package/scripts/sw-setup.sh +234 -0
- package/scripts/sw-status.sh +605 -0
- package/scripts/{cct-templates.sh → sw-templates.sh} +9 -4
- package/scripts/sw-tmux.sh +591 -0
- package/scripts/sw-tracker-jira.sh +277 -0
- package/scripts/sw-tracker-linear.sh +292 -0
- package/scripts/sw-tracker.sh +409 -0
- package/scripts/{cct-upgrade.sh → sw-upgrade.sh} +103 -46
- package/scripts/{cct-worktree.sh → sw-worktree.sh} +3 -0
- package/templates/pipelines/autonomous.json +27 -5
- package/templates/pipelines/full.json +12 -0
- package/templates/pipelines/standard.json +12 -0
- package/tmux/{claude-teams-overlay.conf → shipwright-overlay.conf} +27 -9
- package/tmux/templates/accessibility.json +34 -0
- package/tmux/templates/api-design.json +35 -0
- package/tmux/templates/architecture.json +1 -0
- package/tmux/templates/bug-fix.json +9 -0
- package/tmux/templates/code-review.json +1 -0
- package/tmux/templates/compliance.json +36 -0
- package/tmux/templates/data-pipeline.json +36 -0
- package/tmux/templates/debt-paydown.json +34 -0
- package/tmux/templates/devops.json +1 -0
- package/tmux/templates/documentation.json +1 -0
- package/tmux/templates/exploration.json +1 -0
- package/tmux/templates/feature-dev.json +1 -0
- package/tmux/templates/full-stack.json +8 -0
- package/tmux/templates/i18n.json +34 -0
- package/tmux/templates/incident-response.json +36 -0
- package/tmux/templates/migration.json +1 -0
- package/tmux/templates/observability.json +35 -0
- package/tmux/templates/onboarding.json +33 -0
- package/tmux/templates/performance.json +35 -0
- package/tmux/templates/refactor.json +1 -0
- package/tmux/templates/release.json +35 -0
- package/tmux/templates/security-audit.json +8 -0
- package/tmux/templates/spike.json +34 -0
- package/tmux/templates/testing.json +1 -0
- package/tmux/tmux.conf +98 -9
- package/scripts/cct-doctor.sh +0 -414
- package/scripts/cct-session.sh +0 -284
- package/scripts/cct-status.sh +0 -169
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
# ║ Full GitHub integration · Auto-detection · Task tracking · Metrics ║
|
|
5
5
|
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
6
6
|
set -euo pipefail
|
|
7
|
+
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
7
8
|
|
|
8
|
-
VERSION="1.
|
|
9
|
+
VERSION="1.9.0"
|
|
9
10
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
11
|
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
11
12
|
|
|
@@ -20,6 +21,40 @@ DIM='\033[2m'
|
|
|
20
21
|
BOLD='\033[1m'
|
|
21
22
|
RESET='\033[0m'
|
|
22
23
|
|
|
24
|
+
# ─── Cross-platform compatibility ──────────────────────────────────────────
|
|
25
|
+
# shellcheck source=lib/compat.sh
|
|
26
|
+
[[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
|
|
27
|
+
|
|
28
|
+
# ─── Intelligence Engine (optional) ──────────────────────────────────────────
|
|
29
|
+
# shellcheck source=sw-intelligence.sh
|
|
30
|
+
if [[ -f "$SCRIPT_DIR/sw-intelligence.sh" ]]; then
|
|
31
|
+
source "$SCRIPT_DIR/sw-intelligence.sh"
|
|
32
|
+
fi
|
|
33
|
+
# shellcheck source=sw-pipeline-composer.sh
|
|
34
|
+
if [[ -f "$SCRIPT_DIR/sw-pipeline-composer.sh" ]]; then
|
|
35
|
+
source "$SCRIPT_DIR/sw-pipeline-composer.sh"
|
|
36
|
+
fi
|
|
37
|
+
# shellcheck source=sw-developer-simulation.sh
|
|
38
|
+
if [[ -f "$SCRIPT_DIR/sw-developer-simulation.sh" ]]; then
|
|
39
|
+
source "$SCRIPT_DIR/sw-developer-simulation.sh"
|
|
40
|
+
fi
|
|
41
|
+
# shellcheck source=sw-architecture-enforcer.sh
|
|
42
|
+
if [[ -f "$SCRIPT_DIR/sw-architecture-enforcer.sh" ]]; then
|
|
43
|
+
source "$SCRIPT_DIR/sw-architecture-enforcer.sh"
|
|
44
|
+
fi
|
|
45
|
+
# shellcheck source=sw-adversarial.sh
|
|
46
|
+
if [[ -f "$SCRIPT_DIR/sw-adversarial.sh" ]]; then
|
|
47
|
+
source "$SCRIPT_DIR/sw-adversarial.sh"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# ─── GitHub API Modules (optional) ─────────────────────────────────────────
|
|
51
|
+
# shellcheck source=sw-github-graphql.sh
|
|
52
|
+
[[ -f "$SCRIPT_DIR/sw-github-graphql.sh" ]] && source "$SCRIPT_DIR/sw-github-graphql.sh"
|
|
53
|
+
# shellcheck source=sw-github-checks.sh
|
|
54
|
+
[[ -f "$SCRIPT_DIR/sw-github-checks.sh" ]] && source "$SCRIPT_DIR/sw-github-checks.sh"
|
|
55
|
+
# shellcheck source=sw-github-deploy.sh
|
|
56
|
+
[[ -f "$SCRIPT_DIR/sw-github-deploy.sh" ]] && source "$SCRIPT_DIR/sw-github-deploy.sh"
|
|
57
|
+
|
|
23
58
|
# ─── Output Helpers ─────────────────────────────────────────────────────────
|
|
24
59
|
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
25
60
|
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
@@ -29,6 +64,30 @@ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
|
29
64
|
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
30
65
|
now_epoch() { date +%s; }
|
|
31
66
|
|
|
67
|
+
# Parse coverage percentage from test output — multi-framework patterns
|
|
68
|
+
# Usage: parse_coverage_from_output <log_file>
|
|
69
|
+
# Outputs coverage percentage or empty string
|
|
70
|
+
parse_coverage_from_output() {
|
|
71
|
+
local log_file="$1"
|
|
72
|
+
[[ ! -f "$log_file" ]] && return
|
|
73
|
+
local cov=""
|
|
74
|
+
# Jest/Istanbul: "Statements : 85.5%"
|
|
75
|
+
cov=$(grep -oE 'Statements\s*:\s*[0-9.]+' "$log_file" 2>/dev/null | grep -oE '[0-9.]+$' || true)
|
|
76
|
+
# Istanbul table: "All files | 85.5"
|
|
77
|
+
[[ -z "$cov" ]] && cov=$(grep -oE 'All files\s*\|\s*[0-9.]+' "$log_file" 2>/dev/null | grep -oE '[0-9.]+$' || true)
|
|
78
|
+
# pytest-cov: "TOTAL 500 75 85%"
|
|
79
|
+
[[ -z "$cov" ]] && cov=$(grep -oE 'TOTAL\s+[0-9]+\s+[0-9]+\s+[0-9]+%' "$log_file" 2>/dev/null | grep -oE '[0-9]+%' | tr -d '%' | tail -1 || true)
|
|
80
|
+
# Vitest: "All files | 85.5 |"
|
|
81
|
+
[[ -z "$cov" ]] && cov=$(grep -oE 'All files\s*\|\s*[0-9.]+\s*\|' "$log_file" 2>/dev/null | grep -oE '[0-9.]+' | head -1 || true)
|
|
82
|
+
# Go coverage: "coverage: 85.5% of statements"
|
|
83
|
+
[[ -z "$cov" ]] && cov=$(grep -oE 'coverage:\s*[0-9.]+%' "$log_file" 2>/dev/null | grep -oE '[0-9.]+' | tail -1 || true)
|
|
84
|
+
# Cargo tarpaulin: "85.50% coverage"
|
|
85
|
+
[[ -z "$cov" ]] && cov=$(grep -oE '[0-9.]+%\s*coverage' "$log_file" 2>/dev/null | grep -oE '[0-9.]+' | head -1 || true)
|
|
86
|
+
# Generic: "Coverage: 85.5%"
|
|
87
|
+
[[ -z "$cov" ]] && cov=$(grep -oiE 'coverage:?\s*[0-9.]+%' "$log_file" 2>/dev/null | grep -oE '[0-9.]+' | tail -1 || true)
|
|
88
|
+
echo "$cov"
|
|
89
|
+
}
|
|
90
|
+
|
|
32
91
|
format_duration() {
|
|
33
92
|
local secs="$1"
|
|
34
93
|
if [[ "$secs" -ge 3600 ]]; then
|
|
@@ -41,9 +100,9 @@ format_duration() {
|
|
|
41
100
|
}
|
|
42
101
|
|
|
43
102
|
# ─── Structured Event Log ──────────────────────────────────────────────────
|
|
44
|
-
# Appends JSON events to ~/.
|
|
103
|
+
# Appends JSON events to ~/.shipwright/events.jsonl for metrics/traceability
|
|
45
104
|
|
|
46
|
-
EVENTS_DIR="${HOME}/.
|
|
105
|
+
EVENTS_DIR="${HOME}/.shipwright"
|
|
47
106
|
EVENTS_FILE="${EVENTS_DIR}/events.jsonl"
|
|
48
107
|
|
|
49
108
|
emit_event() {
|
|
@@ -94,13 +153,18 @@ REVIEWERS=""
|
|
|
94
153
|
LABELS=""
|
|
95
154
|
BASE_BRANCH="main"
|
|
96
155
|
NO_GITHUB=false
|
|
156
|
+
NO_GITHUB_LABEL=false
|
|
157
|
+
CI_MODE=false
|
|
97
158
|
DRY_RUN=false
|
|
98
159
|
IGNORE_BUDGET=false
|
|
160
|
+
COMPLETED_STAGES=""
|
|
161
|
+
MAX_ITERATIONS_OVERRIDE=""
|
|
99
162
|
PR_NUMBER=""
|
|
100
163
|
AUTO_WORKTREE=false
|
|
101
164
|
WORKTREE_NAME=""
|
|
102
165
|
CLEANUP_WORKTREE=false
|
|
103
166
|
ORIGINAL_REPO_DIR=""
|
|
167
|
+
_cleanup_done=""
|
|
104
168
|
|
|
105
169
|
# GitHub metadata (populated during intake)
|
|
106
170
|
ISSUE_LABELS=""
|
|
@@ -150,11 +214,15 @@ show_help() {
|
|
|
150
214
|
echo -e " ${DIM}--reviewers \"a,b\"${RESET} Request PR reviewers (auto-detected if omitted)"
|
|
151
215
|
echo -e " ${DIM}--labels \"a,b\"${RESET} Add labels to PR (inherited from issue if omitted)"
|
|
152
216
|
echo -e " ${DIM}--no-github${RESET} Disable GitHub integration"
|
|
217
|
+
echo -e " ${DIM}--no-github-label${RESET} Don't modify issue labels"
|
|
218
|
+
echo -e " ${DIM}--ci${RESET} CI mode (skip gates, non-interactive)"
|
|
153
219
|
echo -e " ${DIM}--ignore-budget${RESET} Skip budget enforcement checks"
|
|
154
220
|
echo -e " ${DIM}--worktree [=name]${RESET} Run in isolated git worktree (parallel-safe)"
|
|
155
221
|
echo -e " ${DIM}--dry-run${RESET} Show what would happen without executing"
|
|
156
222
|
echo -e " ${DIM}--slack-webhook <url>${RESET} Send notifications to Slack"
|
|
157
223
|
echo -e " ${DIM}--self-heal <n>${RESET} Build→test retry cycles on failure (default: 2)"
|
|
224
|
+
echo -e " ${DIM}--max-iterations <n>${RESET} Override max build loop iterations"
|
|
225
|
+
echo -e " ${DIM}--completed-stages \"a,b\"${RESET} Skip these stages (CI resume)"
|
|
158
226
|
echo ""
|
|
159
227
|
echo -e "${BOLD}STAGES${RESET} ${DIM}(configurable per pipeline template)${RESET}"
|
|
160
228
|
echo -e " intake → plan → design → build → test → review → pr → deploy → validate → monitor"
|
|
@@ -217,7 +285,7 @@ parse_args() {
|
|
|
217
285
|
case "$1" in
|
|
218
286
|
--goal) GOAL="$2"; shift 2 ;;
|
|
219
287
|
--issue) ISSUE_NUMBER="$2"; shift 2 ;;
|
|
220
|
-
--pipeline)
|
|
288
|
+
--pipeline|--template) PIPELINE_NAME="$2"; shift 2 ;;
|
|
221
289
|
--test-cmd) TEST_CMD="$2"; shift 2 ;;
|
|
222
290
|
--model) MODEL="$2"; shift 2 ;;
|
|
223
291
|
--agents) AGENTS="$2"; shift 2 ;;
|
|
@@ -226,7 +294,11 @@ parse_args() {
|
|
|
226
294
|
--reviewers) REVIEWERS="$2"; shift 2 ;;
|
|
227
295
|
--labels) LABELS="$2"; shift 2 ;;
|
|
228
296
|
--no-github) NO_GITHUB=true; shift ;;
|
|
297
|
+
--no-github-label) NO_GITHUB_LABEL=true; shift ;;
|
|
298
|
+
--ci) CI_MODE=true; SKIP_GATES=true; shift ;;
|
|
229
299
|
--ignore-budget) IGNORE_BUDGET=true; shift ;;
|
|
300
|
+
--max-iterations) MAX_ITERATIONS_OVERRIDE="$2"; shift 2 ;;
|
|
301
|
+
--completed-stages) COMPLETED_STAGES="$2"; shift 2 ;;
|
|
230
302
|
--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 ;;
|
|
231
303
|
--worktree) AUTO_WORKTREE=true; shift ;;
|
|
232
304
|
--dry-run) DRY_RUN=true; shift ;;
|
|
@@ -262,7 +334,7 @@ find_pipeline_config() {
|
|
|
262
334
|
local name="$1"
|
|
263
335
|
local locations=(
|
|
264
336
|
"$REPO_DIR/templates/pipelines/${name}.json"
|
|
265
|
-
"$HOME/.
|
|
337
|
+
"$HOME/.shipwright/pipelines/${name}.json"
|
|
266
338
|
)
|
|
267
339
|
for loc in "${locations[@]}"; do
|
|
268
340
|
if [[ -f "$loc" ]]; then
|
|
@@ -274,6 +346,28 @@ find_pipeline_config() {
|
|
|
274
346
|
}
|
|
275
347
|
|
|
276
348
|
load_pipeline_config() {
|
|
349
|
+
# Check for intelligence-composed pipeline first
|
|
350
|
+
local composed_pipeline="${ARTIFACTS_DIR}/composed-pipeline.json"
|
|
351
|
+
if [[ -f "$composed_pipeline" ]] && type composer_validate_pipeline &>/dev/null; then
|
|
352
|
+
# Use composed pipeline if fresh (< 1 hour old)
|
|
353
|
+
local composed_age=99999
|
|
354
|
+
local composed_mtime
|
|
355
|
+
composed_mtime=$(stat -f %m "$composed_pipeline" 2>/dev/null || stat -c %Y "$composed_pipeline" 2>/dev/null || echo "0")
|
|
356
|
+
if [[ "$composed_mtime" -gt 0 ]]; then
|
|
357
|
+
composed_age=$(( $(now_epoch) - composed_mtime ))
|
|
358
|
+
fi
|
|
359
|
+
if [[ "$composed_age" -lt 3600 ]]; then
|
|
360
|
+
local validate_json
|
|
361
|
+
validate_json=$(cat "$composed_pipeline" 2>/dev/null || echo "")
|
|
362
|
+
if [[ -n "$validate_json" ]] && composer_validate_pipeline "$validate_json" 2>/dev/null; then
|
|
363
|
+
PIPELINE_CONFIG="$composed_pipeline"
|
|
364
|
+
info "Pipeline: ${BOLD}composed${RESET} ${DIM}(intelligence-driven)${RESET}"
|
|
365
|
+
emit_event "pipeline.composed_loaded" "issue=${ISSUE_NUMBER:-0}"
|
|
366
|
+
return
|
|
367
|
+
fi
|
|
368
|
+
fi
|
|
369
|
+
fi
|
|
370
|
+
|
|
277
371
|
PIPELINE_CONFIG=$(find_pipeline_config "$PIPELINE_NAME") || {
|
|
278
372
|
error "Pipeline template not found: $PIPELINE_NAME"
|
|
279
373
|
echo -e " Available templates: ${DIM}shipwright pipeline list${RESET}"
|
|
@@ -298,11 +392,72 @@ TOTAL_INPUT_TOKENS=0
|
|
|
298
392
|
TOTAL_OUTPUT_TOKENS=0
|
|
299
393
|
COST_MODEL_RATES='{"opus":{"input":15,"output":75},"sonnet":{"input":3,"output":15},"haiku":{"input":0.25,"output":1.25}}'
|
|
300
394
|
|
|
395
|
+
# ─── Heartbeat ────────────────────────────────────────────────────────────────
|
|
396
|
+
HEARTBEAT_PID=""
|
|
397
|
+
|
|
398
|
+
start_heartbeat() {
|
|
399
|
+
local job_id="${PIPELINE_NAME:-pipeline-$$}"
|
|
400
|
+
(
|
|
401
|
+
while true; do
|
|
402
|
+
"$SCRIPT_DIR/sw-heartbeat.sh" write "$job_id" \
|
|
403
|
+
--pid $$ \
|
|
404
|
+
--issue "${ISSUE_NUMBER:-0}" \
|
|
405
|
+
--stage "${CURRENT_STAGE_ID:-unknown}" \
|
|
406
|
+
--iteration "0" \
|
|
407
|
+
--activity "$(get_stage_description "${CURRENT_STAGE_ID:-}" 2>/dev/null || echo "Running pipeline")" 2>/dev/null || true
|
|
408
|
+
sleep 30
|
|
409
|
+
done
|
|
410
|
+
) >/dev/null 2>&1 &
|
|
411
|
+
HEARTBEAT_PID=$!
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
stop_heartbeat() {
|
|
415
|
+
if [[ -n "${HEARTBEAT_PID:-}" ]]; then
|
|
416
|
+
kill "$HEARTBEAT_PID" 2>/dev/null || true
|
|
417
|
+
wait "$HEARTBEAT_PID" 2>/dev/null || true
|
|
418
|
+
"$SCRIPT_DIR/sw-heartbeat.sh" clear "${PIPELINE_NAME:-pipeline-$$}" 2>/dev/null || true
|
|
419
|
+
HEARTBEAT_PID=""
|
|
420
|
+
fi
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
# ─── CI Helpers ───────────────────────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
ci_push_partial_work() {
|
|
426
|
+
[[ "${CI_MODE:-false}" != "true" ]] && return 0
|
|
427
|
+
[[ -z "${ISSUE_NUMBER:-}" ]] && return 0
|
|
428
|
+
|
|
429
|
+
local branch="shipwright/issue-${ISSUE_NUMBER}"
|
|
430
|
+
|
|
431
|
+
# Only push if we have uncommitted changes
|
|
432
|
+
if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
|
|
433
|
+
git add -A 2>/dev/null || true
|
|
434
|
+
git commit -m "WIP: partial pipeline progress for #${ISSUE_NUMBER}" --no-verify 2>/dev/null || true
|
|
435
|
+
fi
|
|
436
|
+
|
|
437
|
+
# Push branch (create if needed, force to overwrite previous WIP)
|
|
438
|
+
git push origin "HEAD:refs/heads/$branch" --force 2>/dev/null || true
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
ci_post_stage_event() {
|
|
442
|
+
[[ "${CI_MODE:-false}" != "true" ]] && return 0
|
|
443
|
+
[[ -z "${ISSUE_NUMBER:-}" ]] && return 0
|
|
444
|
+
[[ "${GH_AVAILABLE:-false}" != "true" ]] && return 0
|
|
445
|
+
|
|
446
|
+
local stage="$1" status="$2" elapsed="${3:-0s}"
|
|
447
|
+
local comment="<!-- SHIPWRIGHT-STAGE: ${stage}:${status}:${elapsed} -->"
|
|
448
|
+
gh issue comment "$ISSUE_NUMBER" --body "$comment" 2>/dev/null || true
|
|
449
|
+
}
|
|
450
|
+
|
|
301
451
|
# ─── Signal Handling ───────────────────────────────────────────────────────
|
|
302
452
|
|
|
303
453
|
cleanup_on_exit() {
|
|
454
|
+
[[ "${_cleanup_done:-}" == "true" ]] && return 0
|
|
455
|
+
_cleanup_done=true
|
|
304
456
|
local exit_code=$?
|
|
305
457
|
|
|
458
|
+
# Stop heartbeat writer
|
|
459
|
+
stop_heartbeat
|
|
460
|
+
|
|
306
461
|
# Save state if we were running
|
|
307
462
|
if [[ "$PIPELINE_STATUS" == "running" && -n "$STATE_FILE" ]]; then
|
|
308
463
|
PIPELINE_STATUS="interrupted"
|
|
@@ -311,6 +466,9 @@ cleanup_on_exit() {
|
|
|
311
466
|
echo ""
|
|
312
467
|
warn "Pipeline interrupted — state saved."
|
|
313
468
|
echo -e " Resume: ${DIM}shipwright pipeline resume${RESET}"
|
|
469
|
+
|
|
470
|
+
# Push partial work in CI mode so retries can pick it up
|
|
471
|
+
ci_push_partial_work
|
|
314
472
|
fi
|
|
315
473
|
|
|
316
474
|
# Restore stashed changes
|
|
@@ -373,7 +531,7 @@ preflight_checks() {
|
|
|
373
531
|
echo -e " ${YELLOW}⚠${RESET} $dirty_files uncommitted change(s)"
|
|
374
532
|
if [[ "$SKIP_GATES" == "true" ]]; then
|
|
375
533
|
info "Auto-stashing uncommitted changes..."
|
|
376
|
-
git stash push -m "
|
|
534
|
+
git stash push -m "sw-pipeline: auto-stash before pipeline" --quiet 2>/dev/null && STASHED_CHANGES=true
|
|
377
535
|
if [[ "$STASHED_CHANGES" == "true" ]]; then
|
|
378
536
|
echo -e " ${GREEN}✓${RESET} Changes stashed (will restore on exit)"
|
|
379
537
|
fi
|
|
@@ -409,11 +567,11 @@ preflight_checks() {
|
|
|
409
567
|
errors=$((errors + 1))
|
|
410
568
|
fi
|
|
411
569
|
|
|
412
|
-
# 5.
|
|
413
|
-
if [[ -x "$SCRIPT_DIR/
|
|
570
|
+
# 5. sw loop (needed for build stage)
|
|
571
|
+
if [[ -x "$SCRIPT_DIR/sw-loop.sh" ]]; then
|
|
414
572
|
echo -e " ${GREEN}✓${RESET} shipwright loop available"
|
|
415
573
|
else
|
|
416
|
-
echo -e " ${RED}✗${RESET}
|
|
574
|
+
echo -e " ${RED}✗${RESET} sw-loop.sh not found at $SCRIPT_DIR"
|
|
417
575
|
errors=$((errors + 1))
|
|
418
576
|
fi
|
|
419
577
|
|
|
@@ -580,19 +738,21 @@ gh_get_issue_meta() {
|
|
|
580
738
|
gh_build_progress_body() {
|
|
581
739
|
local body="## 🤖 Pipeline Progress — \`${PIPELINE_NAME}\`
|
|
582
740
|
|
|
583
|
-
|
|
584
|
-
|
|
741
|
+
**Delivering:** ${GOAL}
|
|
742
|
+
|
|
743
|
+
| Stage | Status | Duration | |
|
|
744
|
+
|-------|--------|----------|-|"
|
|
585
745
|
|
|
586
746
|
local stages
|
|
587
747
|
stages=$(jq -c '.stages[]' "$PIPELINE_CONFIG" 2>/dev/null)
|
|
588
|
-
while IFS= read -r stage; do
|
|
748
|
+
while IFS= read -r -u 3 stage; do
|
|
589
749
|
local id enabled
|
|
590
750
|
id=$(echo "$stage" | jq -r '.id')
|
|
591
751
|
enabled=$(echo "$stage" | jq -r '.enabled')
|
|
592
752
|
|
|
593
753
|
if [[ "$enabled" != "true" ]]; then
|
|
594
754
|
body="${body}
|
|
595
|
-
| ${id} | ⏭️ skipped | — |"
|
|
755
|
+
| ${id} | ⏭️ skipped | — | |"
|
|
596
756
|
continue
|
|
597
757
|
fi
|
|
598
758
|
|
|
@@ -601,21 +761,20 @@ gh_build_progress_body() {
|
|
|
601
761
|
local duration
|
|
602
762
|
duration=$(get_stage_timing "$id")
|
|
603
763
|
|
|
604
|
-
local icon
|
|
764
|
+
local icon detail_col
|
|
605
765
|
case "$sstatus" in
|
|
606
|
-
complete) icon="✅" ;;
|
|
607
|
-
running) icon="🔄" ;;
|
|
608
|
-
failed) icon="❌" ;;
|
|
609
|
-
*) icon="⬜" ;;
|
|
766
|
+
complete) icon="✅"; detail_col="" ;;
|
|
767
|
+
running) icon="🔄"; detail_col=$(get_stage_description "$id") ;;
|
|
768
|
+
failed) icon="❌"; detail_col="" ;;
|
|
769
|
+
*) icon="⬜"; detail_col=$(get_stage_description "$id") ;;
|
|
610
770
|
esac
|
|
611
771
|
|
|
612
772
|
body="${body}
|
|
613
|
-
| ${id} | ${icon} ${sstatus:-pending} | ${duration:-—} |"
|
|
614
|
-
done <<< "$stages"
|
|
773
|
+
| ${id} | ${icon} ${sstatus:-pending} | ${duration:-—} | ${detail_col} |"
|
|
774
|
+
done 3<<< "$stages"
|
|
615
775
|
|
|
616
776
|
body="${body}
|
|
617
777
|
|
|
618
|
-
**Goal:** ${GOAL}
|
|
619
778
|
**Branch:** \`${GIT_BRANCH}\`"
|
|
620
779
|
|
|
621
780
|
[[ -n "${GITHUB_ISSUE:-}" ]] && body="${body}
|
|
@@ -628,10 +787,19 @@ gh_build_progress_body() {
|
|
|
628
787
|
**Elapsed:** ${total_dur}"
|
|
629
788
|
fi
|
|
630
789
|
|
|
790
|
+
# Artifacts section
|
|
791
|
+
local artifacts=""
|
|
792
|
+
[[ -f "$ARTIFACTS_DIR/plan.md" ]] && artifacts="${artifacts}[Plan](.claude/pipeline-artifacts/plan.md)"
|
|
793
|
+
[[ -f "$ARTIFACTS_DIR/design.md" ]] && { [[ -n "$artifacts" ]] && artifacts="${artifacts} · "; artifacts="${artifacts}[Design](.claude/pipeline-artifacts/design.md)"; }
|
|
794
|
+
[[ -n "${PR_NUMBER:-}" ]] && { [[ -n "$artifacts" ]] && artifacts="${artifacts} · "; artifacts="${artifacts}PR #${PR_NUMBER}"; }
|
|
795
|
+
[[ -n "$artifacts" ]] && body="${body}
|
|
796
|
+
|
|
797
|
+
📎 **Artifacts:** ${artifacts}"
|
|
798
|
+
|
|
631
799
|
body="${body}
|
|
632
800
|
|
|
633
801
|
---
|
|
634
|
-
_Updated: $(now_iso) ·
|
|
802
|
+
_Updated: $(now_iso) · shipwright pipeline_"
|
|
635
803
|
echo "$body"
|
|
636
804
|
}
|
|
637
805
|
|
|
@@ -725,36 +893,58 @@ detect_test_cmd() {
|
|
|
725
893
|
# Detect project language/framework
|
|
726
894
|
detect_project_lang() {
|
|
727
895
|
local root="$PROJECT_ROOT"
|
|
896
|
+
local detected=""
|
|
897
|
+
|
|
898
|
+
# Fast heuristic detection (grep-based)
|
|
728
899
|
if [[ -f "$root/package.json" ]]; then
|
|
729
900
|
if grep -q "typescript" "$root/package.json" 2>/dev/null; then
|
|
730
|
-
|
|
901
|
+
detected="typescript"
|
|
731
902
|
elif grep -q "\"next\"" "$root/package.json" 2>/dev/null; then
|
|
732
|
-
|
|
903
|
+
detected="nextjs"
|
|
733
904
|
elif grep -q "\"react\"" "$root/package.json" 2>/dev/null; then
|
|
734
|
-
|
|
905
|
+
detected="react"
|
|
735
906
|
else
|
|
736
|
-
|
|
907
|
+
detected="nodejs"
|
|
737
908
|
fi
|
|
738
909
|
elif [[ -f "$root/Cargo.toml" ]]; then
|
|
739
|
-
|
|
910
|
+
detected="rust"
|
|
740
911
|
elif [[ -f "$root/go.mod" ]]; then
|
|
741
|
-
|
|
912
|
+
detected="go"
|
|
742
913
|
elif [[ -f "$root/pyproject.toml" || -f "$root/setup.py" || -f "$root/requirements.txt" ]]; then
|
|
743
|
-
|
|
914
|
+
detected="python"
|
|
744
915
|
elif [[ -f "$root/Gemfile" ]]; then
|
|
745
|
-
|
|
916
|
+
detected="ruby"
|
|
746
917
|
elif [[ -f "$root/pom.xml" || -f "$root/build.gradle" ]]; then
|
|
747
|
-
|
|
918
|
+
detected="java"
|
|
748
919
|
else
|
|
749
|
-
|
|
920
|
+
detected="unknown"
|
|
921
|
+
fi
|
|
922
|
+
|
|
923
|
+
# Intelligence: holistic analysis for polyglot/monorepo detection
|
|
924
|
+
if [[ "$detected" == "unknown" ]] && type intelligence_search_memory &>/dev/null 2>&1 && command -v claude &>/dev/null; then
|
|
925
|
+
local config_files
|
|
926
|
+
config_files=$(ls "$root" 2>/dev/null | grep -E '\.(json|toml|yaml|yml|xml|gradle|lock|mod)$' | head -15)
|
|
927
|
+
if [[ -n "$config_files" ]]; then
|
|
928
|
+
local ai_lang
|
|
929
|
+
ai_lang=$(claude --print --output-format text -p "Based on these config files in a project root, what is the primary language/framework? Reply with ONE word (e.g., typescript, python, rust, go, java, ruby, nodejs):
|
|
930
|
+
|
|
931
|
+
Files: ${config_files}" --model haiku < /dev/null 2>/dev/null || true)
|
|
932
|
+
ai_lang=$(echo "$ai_lang" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
|
|
933
|
+
case "$ai_lang" in
|
|
934
|
+
typescript|python|rust|go|java|ruby|nodejs|react|nextjs|kotlin|swift|elixir|scala)
|
|
935
|
+
detected="$ai_lang" ;;
|
|
936
|
+
esac
|
|
937
|
+
fi
|
|
750
938
|
fi
|
|
939
|
+
|
|
940
|
+
echo "$detected"
|
|
751
941
|
}
|
|
752
942
|
|
|
753
943
|
# Detect likely reviewers from CODEOWNERS or git log
|
|
754
944
|
detect_reviewers() {
|
|
755
945
|
local root="$PROJECT_ROOT"
|
|
756
946
|
|
|
757
|
-
# Check CODEOWNERS
|
|
947
|
+
# Check CODEOWNERS — common paths first, then broader search
|
|
758
948
|
local codeowners=""
|
|
759
949
|
for f in "$root/.github/CODEOWNERS" "$root/CODEOWNERS" "$root/docs/CODEOWNERS"; do
|
|
760
950
|
if [[ -f "$f" ]]; then
|
|
@@ -762,6 +952,10 @@ detect_reviewers() {
|
|
|
762
952
|
break
|
|
763
953
|
fi
|
|
764
954
|
done
|
|
955
|
+
# Broader search if not found at common locations
|
|
956
|
+
if [[ -z "$codeowners" ]]; then
|
|
957
|
+
codeowners=$(find "$root" -maxdepth 3 -name "CODEOWNERS" -type f 2>/dev/null | head -1 || true)
|
|
958
|
+
fi
|
|
765
959
|
|
|
766
960
|
if [[ -n "$codeowners" ]]; then
|
|
767
961
|
# Extract GitHub usernames from CODEOWNERS (lines like: * @user1 @user2)
|
|
@@ -774,22 +968,54 @@ detect_reviewers() {
|
|
|
774
968
|
fi
|
|
775
969
|
fi
|
|
776
970
|
|
|
777
|
-
# Fallback:
|
|
971
|
+
# Fallback: try to extract GitHub usernames from recent commit emails
|
|
972
|
+
# Format: user@users.noreply.github.com → user, or noreply+user@... → user
|
|
778
973
|
local current_user
|
|
779
|
-
current_user=$(gh api user --jq '.login' 2>/dev/null ||
|
|
974
|
+
current_user=$(gh api user --jq '.login' 2>/dev/null || true)
|
|
780
975
|
local contributors
|
|
781
|
-
contributors=$(git log --format='%
|
|
976
|
+
contributors=$(git log --format='%aE' -100 2>/dev/null | \
|
|
977
|
+
grep -oE '[a-zA-Z0-9_-]+@users\.noreply\.github\.com' | \
|
|
978
|
+
sed 's/@users\.noreply\.github\.com//' | sed 's/^[0-9]*+//' | \
|
|
782
979
|
sort | uniq -c | sort -rn | \
|
|
783
980
|
awk '{print $NF}' | \
|
|
784
|
-
grep -v "^${current_user}$" 2>/dev/null | \
|
|
981
|
+
grep -v "^${current_user:-___}$" 2>/dev/null | \
|
|
785
982
|
head -2 | tr '\n' ',')
|
|
786
983
|
contributors="${contributors%,}"
|
|
787
984
|
echo "$contributors"
|
|
788
985
|
}
|
|
789
986
|
|
|
790
|
-
# Get branch prefix from task type
|
|
987
|
+
# Get branch prefix from task type — checks git history for conventions first
|
|
791
988
|
branch_prefix_for_type() {
|
|
792
|
-
|
|
989
|
+
local task_type="$1"
|
|
990
|
+
|
|
991
|
+
# Analyze recent branches for naming conventions
|
|
992
|
+
local branch_prefixes
|
|
993
|
+
branch_prefixes=$(git branch -r 2>/dev/null | sed 's#origin/##' | grep -oE '^[a-z]+/' | sort | uniq -c | sort -rn | head -5 || true)
|
|
994
|
+
if [[ -n "$branch_prefixes" ]]; then
|
|
995
|
+
local total_branches dominant_prefix dominant_count
|
|
996
|
+
total_branches=$(echo "$branch_prefixes" | awk '{s+=$1} END {print s}' || echo "0")
|
|
997
|
+
dominant_prefix=$(echo "$branch_prefixes" | head -1 | awk '{print $2}' | tr -d '/' || true)
|
|
998
|
+
dominant_count=$(echo "$branch_prefixes" | head -1 | awk '{print $1}' || echo "0")
|
|
999
|
+
# If >80% of branches use a pattern, adopt it for the matching type
|
|
1000
|
+
if [[ "$total_branches" -gt 5 ]] && [[ "$dominant_count" -gt 0 ]]; then
|
|
1001
|
+
local pct=$(( (dominant_count * 100) / total_branches ))
|
|
1002
|
+
if [[ "$pct" -gt 80 && -n "$dominant_prefix" ]]; then
|
|
1003
|
+
# Map task type to the repo's convention
|
|
1004
|
+
local mapped=""
|
|
1005
|
+
case "$task_type" in
|
|
1006
|
+
bug) mapped=$(echo "$branch_prefixes" | awk '{print $2}' | tr -d '/' | grep -E '^(fix|bug|hotfix)$' | head -1 || true) ;;
|
|
1007
|
+
feature) mapped=$(echo "$branch_prefixes" | awk '{print $2}' | tr -d '/' | grep -E '^(feat|feature)$' | head -1 || true) ;;
|
|
1008
|
+
esac
|
|
1009
|
+
if [[ -n "$mapped" ]]; then
|
|
1010
|
+
echo "$mapped"
|
|
1011
|
+
return
|
|
1012
|
+
fi
|
|
1013
|
+
fi
|
|
1014
|
+
fi
|
|
1015
|
+
fi
|
|
1016
|
+
|
|
1017
|
+
# Fallback: hardcoded mapping
|
|
1018
|
+
case "$task_type" in
|
|
793
1019
|
bug) echo "fix" ;;
|
|
794
1020
|
refactor) echo "refactor" ;;
|
|
795
1021
|
testing) echo "test" ;;
|
|
@@ -855,6 +1081,84 @@ get_stage_timing() {
|
|
|
855
1081
|
fi
|
|
856
1082
|
}
|
|
857
1083
|
|
|
1084
|
+
get_stage_description() {
|
|
1085
|
+
local stage_id="$1"
|
|
1086
|
+
|
|
1087
|
+
# Try to generate dynamic description from pipeline config
|
|
1088
|
+
if [[ -n "${PIPELINE_CONFIG:-}" && -f "${PIPELINE_CONFIG:-/dev/null}" ]]; then
|
|
1089
|
+
local stage_cfg
|
|
1090
|
+
stage_cfg=$(jq -c --arg id "$stage_id" '.stages[] | select(.id == $id) | .config // {}' "$PIPELINE_CONFIG" 2>/dev/null || echo "{}")
|
|
1091
|
+
case "$stage_id" in
|
|
1092
|
+
test)
|
|
1093
|
+
local cfg_test_cmd cfg_cov_min
|
|
1094
|
+
cfg_test_cmd=$(echo "$stage_cfg" | jq -r '.test_cmd // empty' 2>/dev/null || true)
|
|
1095
|
+
cfg_cov_min=$(echo "$stage_cfg" | jq -r '.coverage_min // empty' 2>/dev/null || true)
|
|
1096
|
+
if [[ -n "$cfg_test_cmd" ]]; then
|
|
1097
|
+
echo "Running ${cfg_test_cmd}${cfg_cov_min:+ with ${cfg_cov_min}% coverage gate}"
|
|
1098
|
+
return
|
|
1099
|
+
fi
|
|
1100
|
+
;;
|
|
1101
|
+
build)
|
|
1102
|
+
local cfg_max_iter cfg_model
|
|
1103
|
+
cfg_max_iter=$(echo "$stage_cfg" | jq -r '.max_iterations // empty' 2>/dev/null || true)
|
|
1104
|
+
cfg_model=$(jq -r '.defaults.model // empty' "$PIPELINE_CONFIG" 2>/dev/null || true)
|
|
1105
|
+
if [[ -n "$cfg_max_iter" ]]; then
|
|
1106
|
+
echo "Building with ${cfg_max_iter} max iterations${cfg_model:+ using ${cfg_model}}"
|
|
1107
|
+
return
|
|
1108
|
+
fi
|
|
1109
|
+
;;
|
|
1110
|
+
monitor)
|
|
1111
|
+
local cfg_dur cfg_thresh
|
|
1112
|
+
cfg_dur=$(echo "$stage_cfg" | jq -r '.duration_minutes // empty' 2>/dev/null || true)
|
|
1113
|
+
cfg_thresh=$(echo "$stage_cfg" | jq -r '.error_threshold // empty' 2>/dev/null || true)
|
|
1114
|
+
if [[ -n "$cfg_dur" ]]; then
|
|
1115
|
+
echo "Monitoring for ${cfg_dur}m${cfg_thresh:+ (threshold: ${cfg_thresh} errors)}"
|
|
1116
|
+
return
|
|
1117
|
+
fi
|
|
1118
|
+
;;
|
|
1119
|
+
esac
|
|
1120
|
+
fi
|
|
1121
|
+
|
|
1122
|
+
# Static fallback descriptions
|
|
1123
|
+
case "$stage_id" in
|
|
1124
|
+
intake) echo "Extracting requirements and auto-detecting project setup" ;;
|
|
1125
|
+
plan) echo "Creating implementation plan with architecture decisions" ;;
|
|
1126
|
+
design) echo "Designing interfaces, data models, and API contracts" ;;
|
|
1127
|
+
build) echo "Writing production code with self-healing iteration" ;;
|
|
1128
|
+
test) echo "Running test suite and validating coverage" ;;
|
|
1129
|
+
review) echo "Code quality, security audit, performance review" ;;
|
|
1130
|
+
compound_quality) echo "Adversarial testing, E2E validation, DoD checklist" ;;
|
|
1131
|
+
pr) echo "Creating pull request with CI integration" ;;
|
|
1132
|
+
merge) echo "Merging PR with branch cleanup" ;;
|
|
1133
|
+
deploy) echo "Deploying to staging/production" ;;
|
|
1134
|
+
validate) echo "Smoke tests and health checks post-deploy" ;;
|
|
1135
|
+
monitor) echo "Production monitoring with auto-rollback" ;;
|
|
1136
|
+
*) echo "" ;;
|
|
1137
|
+
esac
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
# Build inline stage progress string (e.g. "intake:complete plan:running test:pending")
|
|
1141
|
+
build_stage_progress() {
|
|
1142
|
+
local progress=""
|
|
1143
|
+
local stages
|
|
1144
|
+
stages=$(jq -c '.stages[]' "$PIPELINE_CONFIG" 2>/dev/null) || return 0
|
|
1145
|
+
while IFS= read -r -u 3 stage; do
|
|
1146
|
+
local id enabled
|
|
1147
|
+
id=$(echo "$stage" | jq -r '.id')
|
|
1148
|
+
enabled=$(echo "$stage" | jq -r '.enabled')
|
|
1149
|
+
[[ "$enabled" != "true" ]] && continue
|
|
1150
|
+
local sstatus
|
|
1151
|
+
sstatus=$(get_stage_status "$id")
|
|
1152
|
+
sstatus="${sstatus:-pending}"
|
|
1153
|
+
if [[ -n "$progress" ]]; then
|
|
1154
|
+
progress="${progress} ${id}:${sstatus}"
|
|
1155
|
+
else
|
|
1156
|
+
progress="${id}:${sstatus}"
|
|
1157
|
+
fi
|
|
1158
|
+
done 3<<< "$stages"
|
|
1159
|
+
echo "$progress"
|
|
1160
|
+
}
|
|
1161
|
+
|
|
858
1162
|
update_status() {
|
|
859
1163
|
local status="$1" stage="$2"
|
|
860
1164
|
PIPELINE_STATUS="$status"
|
|
@@ -877,6 +1181,20 @@ mark_stage_complete() {
|
|
|
877
1181
|
local body
|
|
878
1182
|
body=$(gh_build_progress_body)
|
|
879
1183
|
gh_update_progress "$body"
|
|
1184
|
+
|
|
1185
|
+
# Notify tracker (Linear/Jira) of stage completion
|
|
1186
|
+
local stage_desc
|
|
1187
|
+
stage_desc=$(get_stage_description "$stage_id")
|
|
1188
|
+
"$SCRIPT_DIR/sw-tracker.sh" notify "stage_complete" "$ISSUE_NUMBER" \
|
|
1189
|
+
"${stage_id}|${timing}|${stage_desc}" 2>/dev/null || true
|
|
1190
|
+
|
|
1191
|
+
# Post structured stage event for CI sweep/retry intelligence
|
|
1192
|
+
ci_post_stage_event "$stage_id" "complete" "$timing"
|
|
1193
|
+
fi
|
|
1194
|
+
|
|
1195
|
+
# Update GitHub Check Run for this stage
|
|
1196
|
+
if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_checks_stage_update &>/dev/null 2>&1; then
|
|
1197
|
+
gh_checks_stage_update "$stage_id" "completed" "success" "Stage $stage_id: ${timing}" 2>/dev/null || true
|
|
880
1198
|
fi
|
|
881
1199
|
}
|
|
882
1200
|
|
|
@@ -899,6 +1217,22 @@ mark_stage_failed() {
|
|
|
899
1217
|
\`\`\`
|
|
900
1218
|
$(tail -5 "$ARTIFACTS_DIR/${stage_id}"*.log 2>/dev/null || echo 'No log available')
|
|
901
1219
|
\`\`\`"
|
|
1220
|
+
|
|
1221
|
+
# Notify tracker (Linear/Jira) of stage failure
|
|
1222
|
+
local error_context
|
|
1223
|
+
error_context=$(tail -5 "$ARTIFACTS_DIR/${stage_id}"*.log 2>/dev/null || echo "No log")
|
|
1224
|
+
"$SCRIPT_DIR/sw-tracker.sh" notify "stage_failed" "$ISSUE_NUMBER" \
|
|
1225
|
+
"${stage_id}|${error_context}" 2>/dev/null || true
|
|
1226
|
+
|
|
1227
|
+
# Post structured stage event for CI sweep/retry intelligence
|
|
1228
|
+
ci_post_stage_event "$stage_id" "failed" "$timing"
|
|
1229
|
+
fi
|
|
1230
|
+
|
|
1231
|
+
# Update GitHub Check Run for this stage
|
|
1232
|
+
if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_checks_stage_update &>/dev/null 2>&1; then
|
|
1233
|
+
local fail_summary
|
|
1234
|
+
fail_summary=$(tail -3 "$ARTIFACTS_DIR/${stage_id}"*.log 2>/dev/null | head -c 500 || echo "Stage $stage_id failed")
|
|
1235
|
+
gh_checks_stage_update "$stage_id" "completed" "failure" "$fail_summary" 2>/dev/null || true
|
|
902
1236
|
fi
|
|
903
1237
|
}
|
|
904
1238
|
|
|
@@ -920,10 +1254,13 @@ initialize_state() {
|
|
|
920
1254
|
STAGE_STATUSES=""
|
|
921
1255
|
STAGE_TIMINGS=""
|
|
922
1256
|
LOG_ENTRIES=""
|
|
1257
|
+
# Clear per-run tracking files
|
|
1258
|
+
rm -f "$ARTIFACTS_DIR/model-routing.log" "$ARTIFACTS_DIR/.plan-failure-sig.txt"
|
|
923
1259
|
write_state
|
|
924
1260
|
}
|
|
925
1261
|
|
|
926
1262
|
write_state() {
|
|
1263
|
+
[[ -z "${STATE_FILE:-}" || -z "${ARTIFACTS_DIR:-}" ]] && return 0
|
|
927
1264
|
local stages_yaml=""
|
|
928
1265
|
while IFS=: read -r sid sstatus; do
|
|
929
1266
|
[[ -z "$sid" ]] && continue
|
|
@@ -936,6 +1273,16 @@ write_state() {
|
|
|
936
1273
|
total_dur=$(format_duration $(( $(now_epoch) - PIPELINE_START_EPOCH )))
|
|
937
1274
|
fi
|
|
938
1275
|
|
|
1276
|
+
# Stage description and progress for dashboard enrichment
|
|
1277
|
+
local cur_stage_desc=""
|
|
1278
|
+
if [[ -n "${CURRENT_STAGE:-}" ]]; then
|
|
1279
|
+
cur_stage_desc=$(get_stage_description "$CURRENT_STAGE")
|
|
1280
|
+
fi
|
|
1281
|
+
local stage_progress=""
|
|
1282
|
+
if [[ -n "${PIPELINE_CONFIG:-}" && -f "${PIPELINE_CONFIG:-/dev/null}" ]]; then
|
|
1283
|
+
stage_progress=$(build_stage_progress)
|
|
1284
|
+
fi
|
|
1285
|
+
|
|
939
1286
|
cat > "$STATE_FILE" <<EOF
|
|
940
1287
|
---
|
|
941
1288
|
pipeline: $PIPELINE_NAME
|
|
@@ -945,6 +1292,8 @@ issue: "${GITHUB_ISSUE:-}"
|
|
|
945
1292
|
branch: "${GIT_BRANCH:-}"
|
|
946
1293
|
template: "${TASK_TYPE:+$(template_for_type "$TASK_TYPE")}"
|
|
947
1294
|
current_stage: $CURRENT_STAGE
|
|
1295
|
+
current_stage_description: "${cur_stage_desc}"
|
|
1296
|
+
stage_progress: "${stage_progress}"
|
|
948
1297
|
started_at: ${STARTED_AT:-$(now_iso)}
|
|
949
1298
|
updated_at: $(now_iso)
|
|
950
1299
|
elapsed: ${total_dur:-0s}
|
|
@@ -980,6 +1329,8 @@ resume_state() {
|
|
|
980
1329
|
issue:*) GITHUB_ISSUE="$(echo "${line#issue:}" | sed 's/^ *"//;s/" *$//')" ;;
|
|
981
1330
|
branch:*) GIT_BRANCH="$(echo "${line#branch:}" | sed 's/^ *"//;s/" *$//')" ;;
|
|
982
1331
|
current_stage:*) CURRENT_STAGE="$(echo "${line#current_stage:}" | xargs)" ;;
|
|
1332
|
+
current_stage_description:*) ;; # computed field — skip on resume
|
|
1333
|
+
stage_progress:*) ;; # computed field — skip on resume
|
|
983
1334
|
started_at:*) STARTED_AT="$(echo "${line#started_at:}" | xargs)" ;;
|
|
984
1335
|
pr_number:*) PR_NUMBER="$(echo "${line#pr_number:}" | xargs)" ;;
|
|
985
1336
|
progress_comment_id:*) PROGRESS_COMMENT_ID="$(echo "${line#progress_comment_id:}" | xargs)" ;;
|
|
@@ -1037,6 +1388,32 @@ ${sid}:${sst}"
|
|
|
1037
1388
|
|
|
1038
1389
|
detect_task_type() {
|
|
1039
1390
|
local goal="$1"
|
|
1391
|
+
|
|
1392
|
+
# Intelligence: Claude classification with confidence score
|
|
1393
|
+
if type intelligence_search_memory &>/dev/null 2>&1 && command -v claude &>/dev/null; then
|
|
1394
|
+
local ai_result
|
|
1395
|
+
ai_result=$(claude --print --output-format text -p "Classify this task into exactly ONE category. Reply in format: CATEGORY|CONFIDENCE (0-100)
|
|
1396
|
+
|
|
1397
|
+
Categories: bug, refactor, testing, security, docs, devops, migration, architecture, feature
|
|
1398
|
+
|
|
1399
|
+
Task: ${goal}" --model haiku < /dev/null 2>/dev/null || true)
|
|
1400
|
+
if [[ -n "$ai_result" ]]; then
|
|
1401
|
+
local ai_type ai_conf
|
|
1402
|
+
ai_type=$(echo "$ai_result" | head -1 | cut -d'|' -f1 | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
|
|
1403
|
+
ai_conf=$(echo "$ai_result" | head -1 | cut -d'|' -f2 | grep -oE '[0-9]+' | head -1 || echo "0")
|
|
1404
|
+
# Use AI classification if confidence >= 70
|
|
1405
|
+
case "$ai_type" in
|
|
1406
|
+
bug|refactor|testing|security|docs|devops|migration|architecture|feature)
|
|
1407
|
+
if [[ "${ai_conf:-0}" -ge 70 ]] 2>/dev/null; then
|
|
1408
|
+
echo "$ai_type"
|
|
1409
|
+
return
|
|
1410
|
+
fi
|
|
1411
|
+
;;
|
|
1412
|
+
esac
|
|
1413
|
+
fi
|
|
1414
|
+
fi
|
|
1415
|
+
|
|
1416
|
+
# Fallback: keyword matching
|
|
1040
1417
|
local lower
|
|
1041
1418
|
lower=$(echo "$goal" | tr '[:upper:]' '[:lower:]')
|
|
1042
1419
|
case "$lower" in
|
|
@@ -1091,6 +1468,7 @@ show_stage_preview() {
|
|
|
1091
1468
|
# ─── Stage Functions ────────────────────────────────────────────────────────
|
|
1092
1469
|
|
|
1093
1470
|
stage_intake() {
|
|
1471
|
+
CURRENT_STAGE_ID="intake"
|
|
1094
1472
|
local project_lang
|
|
1095
1473
|
project_lang=$(detect_project_lang)
|
|
1096
1474
|
info "Project: ${BOLD}$project_lang${RESET}"
|
|
@@ -1188,6 +1566,7 @@ Test cmd: ${TEST_CMD:-none detected}"
|
|
|
1188
1566
|
}
|
|
1189
1567
|
|
|
1190
1568
|
stage_plan() {
|
|
1569
|
+
CURRENT_STAGE_ID="plan"
|
|
1191
1570
|
local plan_file="$ARTIFACTS_DIR/plan.md"
|
|
1192
1571
|
|
|
1193
1572
|
if ! command -v claude &>/dev/null; then
|
|
@@ -1212,6 +1591,62 @@ ${ISSUE_BODY}
|
|
|
1212
1591
|
"
|
|
1213
1592
|
fi
|
|
1214
1593
|
|
|
1594
|
+
# Inject intelligence memory context for similar past plans
|
|
1595
|
+
if type intelligence_search_memory &>/dev/null 2>&1; then
|
|
1596
|
+
local plan_memory
|
|
1597
|
+
plan_memory=$(intelligence_search_memory "plan stage for ${TASK_TYPE:-feature}: ${GOAL:-}" "${HOME}/.shipwright/memory" 5 2>/dev/null) || true
|
|
1598
|
+
if [[ -n "$plan_memory" && "$plan_memory" != *'"results":[]'* && "$plan_memory" != *'"error"'* ]]; then
|
|
1599
|
+
local memory_summary
|
|
1600
|
+
memory_summary=$(echo "$plan_memory" | jq -r '.results[]? | "- \(.)"' 2>/dev/null | head -10 || true)
|
|
1601
|
+
if [[ -n "$memory_summary" ]]; then
|
|
1602
|
+
plan_prompt="${plan_prompt}
|
|
1603
|
+
## Historical Context (from previous pipelines)
|
|
1604
|
+
Previous similar issues were planned as:
|
|
1605
|
+
${memory_summary}
|
|
1606
|
+
"
|
|
1607
|
+
fi
|
|
1608
|
+
fi
|
|
1609
|
+
fi
|
|
1610
|
+
|
|
1611
|
+
# Inject architecture patterns from intelligence layer
|
|
1612
|
+
local repo_hash_plan
|
|
1613
|
+
repo_hash_plan=$(echo -n "$PROJECT_ROOT" | shasum -a 256 2>/dev/null | cut -c1-12 || echo "unknown")
|
|
1614
|
+
local arch_file_plan="${HOME}/.shipwright/memory/${repo_hash_plan}/architecture.json"
|
|
1615
|
+
if [[ -f "$arch_file_plan" ]]; then
|
|
1616
|
+
local arch_patterns
|
|
1617
|
+
arch_patterns=$(jq -r '
|
|
1618
|
+
"Language: \(.language // "unknown")",
|
|
1619
|
+
"Framework: \(.framework // "unknown")",
|
|
1620
|
+
"Patterns: \((.patterns // []) | join(", "))",
|
|
1621
|
+
"Rules: \((.rules // []) | join("; "))"
|
|
1622
|
+
' "$arch_file_plan" 2>/dev/null || true)
|
|
1623
|
+
if [[ -n "$arch_patterns" ]]; then
|
|
1624
|
+
plan_prompt="${plan_prompt}
|
|
1625
|
+
## Architecture Patterns
|
|
1626
|
+
${arch_patterns}
|
|
1627
|
+
"
|
|
1628
|
+
fi
|
|
1629
|
+
fi
|
|
1630
|
+
|
|
1631
|
+
# Task-type-specific guidance
|
|
1632
|
+
case "${TASK_TYPE:-feature}" in
|
|
1633
|
+
bug)
|
|
1634
|
+
plan_prompt="${plan_prompt}
|
|
1635
|
+
## Task Type: Bug Fix
|
|
1636
|
+
Focus on: reproducing the bug, identifying root cause, minimal targeted fix, regression tests.
|
|
1637
|
+
" ;;
|
|
1638
|
+
refactor)
|
|
1639
|
+
plan_prompt="${plan_prompt}
|
|
1640
|
+
## Task Type: Refactor
|
|
1641
|
+
Focus on: preserving all existing behavior, incremental changes, comprehensive test coverage.
|
|
1642
|
+
" ;;
|
|
1643
|
+
security)
|
|
1644
|
+
plan_prompt="${plan_prompt}
|
|
1645
|
+
## Task Type: Security
|
|
1646
|
+
Focus on: threat modeling, OWASP top 10, input validation, authentication/authorization.
|
|
1647
|
+
" ;;
|
|
1648
|
+
esac
|
|
1649
|
+
|
|
1215
1650
|
# Add project context
|
|
1216
1651
|
local project_lang
|
|
1217
1652
|
project_lang=$(detect_project_lang)
|
|
@@ -1247,10 +1682,14 @@ Checklist of completion criteria.
|
|
|
1247
1682
|
plan_model=$(jq -r --arg id "plan" '(.stages[] | select(.id == $id) | .config.model) // .defaults.model // "opus"' "$PIPELINE_CONFIG" 2>/dev/null) || true
|
|
1248
1683
|
[[ -n "$MODEL" ]] && plan_model="$MODEL"
|
|
1249
1684
|
[[ -z "$plan_model" || "$plan_model" == "null" ]] && plan_model="opus"
|
|
1685
|
+
# Intelligence model routing (when no explicit CLI --model override)
|
|
1686
|
+
if [[ -z "$MODEL" && -n "${CLAUDE_MODEL:-}" ]]; then
|
|
1687
|
+
plan_model="$CLAUDE_MODEL"
|
|
1688
|
+
fi
|
|
1250
1689
|
|
|
1251
1690
|
local _token_log="${ARTIFACTS_DIR}/.claude-tokens-plan.log"
|
|
1252
|
-
claude --print --model "$plan_model" --max-turns
|
|
1253
|
-
"$plan_prompt" > "$plan_file" 2>"$_token_log" || true
|
|
1691
|
+
claude --print --model "$plan_model" --max-turns 25 \
|
|
1692
|
+
"$plan_prompt" < /dev/null > "$plan_file" 2>"$_token_log" || true
|
|
1254
1693
|
parse_claude_tokens "$_token_log"
|
|
1255
1694
|
|
|
1256
1695
|
if [[ ! -s "$plan_file" ]]; then
|
|
@@ -1342,6 +1781,163 @@ CC_TASKS_EOF
|
|
|
1342
1781
|
# Extract definition of done for quality gates
|
|
1343
1782
|
sed -n '/[Dd]efinition [Oo]f [Dd]one/,/^#/p' "$plan_file" | head -20 > "$ARTIFACTS_DIR/dod.md" 2>/dev/null || true
|
|
1344
1783
|
|
|
1784
|
+
# ── Plan Validation Gate ──
|
|
1785
|
+
# Ask Claude to validate the plan before proceeding
|
|
1786
|
+
if command -v claude &>/dev/null && [[ -s "$plan_file" ]]; then
|
|
1787
|
+
local validation_attempts=0
|
|
1788
|
+
local max_validation_attempts=2
|
|
1789
|
+
local plan_valid=false
|
|
1790
|
+
|
|
1791
|
+
while [[ "$validation_attempts" -lt "$max_validation_attempts" ]]; do
|
|
1792
|
+
validation_attempts=$((validation_attempts + 1))
|
|
1793
|
+
info "Validating plan (attempt ${validation_attempts}/${max_validation_attempts})..."
|
|
1794
|
+
|
|
1795
|
+
# Build enriched validation prompt with learned context
|
|
1796
|
+
local validation_extra=""
|
|
1797
|
+
|
|
1798
|
+
# Inject rejected plan history from memory
|
|
1799
|
+
if type intelligence_search_memory &>/dev/null 2>&1; then
|
|
1800
|
+
local rejected_plans
|
|
1801
|
+
rejected_plans=$(intelligence_search_memory "rejected plan validation failures for: ${GOAL:-}" "${HOME}/.shipwright/memory" 3 2>/dev/null) || true
|
|
1802
|
+
if [[ -n "$rejected_plans" ]]; then
|
|
1803
|
+
validation_extra="${validation_extra}
|
|
1804
|
+
## Previously Rejected Plans
|
|
1805
|
+
These issues were found in past plan validations for similar tasks:
|
|
1806
|
+
${rejected_plans}
|
|
1807
|
+
"
|
|
1808
|
+
fi
|
|
1809
|
+
fi
|
|
1810
|
+
|
|
1811
|
+
# Inject repo conventions contextually
|
|
1812
|
+
local claudemd="$PROJECT_ROOT/.claude/CLAUDE.md"
|
|
1813
|
+
if [[ -f "$claudemd" ]]; then
|
|
1814
|
+
local conventions_summary
|
|
1815
|
+
conventions_summary=$(head -100 "$claudemd" 2>/dev/null | grep -E '^##|^-|^\*' | head -15 || true)
|
|
1816
|
+
if [[ -n "$conventions_summary" ]]; then
|
|
1817
|
+
validation_extra="${validation_extra}
|
|
1818
|
+
## Repo Conventions
|
|
1819
|
+
${conventions_summary}
|
|
1820
|
+
"
|
|
1821
|
+
fi
|
|
1822
|
+
fi
|
|
1823
|
+
|
|
1824
|
+
# Inject complexity estimate
|
|
1825
|
+
local complexity_hint=""
|
|
1826
|
+
if [[ -n "${INTELLIGENCE_COMPLEXITY:-}" && "${INTELLIGENCE_COMPLEXITY:-0}" -gt 0 ]]; then
|
|
1827
|
+
complexity_hint="This is estimated as complexity ${INTELLIGENCE_COMPLEXITY}/10. Plans for this complexity typically need ${INTELLIGENCE_COMPLEXITY} or more tasks."
|
|
1828
|
+
fi
|
|
1829
|
+
|
|
1830
|
+
local validation_prompt="You are a plan validator. Review this implementation plan and determine if it is valid.
|
|
1831
|
+
|
|
1832
|
+
## Goal
|
|
1833
|
+
${GOAL}
|
|
1834
|
+
${complexity_hint:+
|
|
1835
|
+
## Complexity Estimate
|
|
1836
|
+
${complexity_hint}
|
|
1837
|
+
}
|
|
1838
|
+
## Plan
|
|
1839
|
+
$(cat "$plan_file")
|
|
1840
|
+
${validation_extra}
|
|
1841
|
+
Evaluate:
|
|
1842
|
+
1. Are all requirements from the goal addressed?
|
|
1843
|
+
2. Is the plan decomposed into clear, achievable tasks?
|
|
1844
|
+
3. Are the implementation steps specific enough to execute?
|
|
1845
|
+
|
|
1846
|
+
Respond with EXACTLY one of these on the first line:
|
|
1847
|
+
VALID: true
|
|
1848
|
+
VALID: false
|
|
1849
|
+
|
|
1850
|
+
Then explain your reasoning briefly."
|
|
1851
|
+
|
|
1852
|
+
local validation_model="${plan_model:-opus}"
|
|
1853
|
+
local validation_result
|
|
1854
|
+
validation_result=$(claude --print --output-format text -p "$validation_prompt" --model "$validation_model" < /dev/null 2>"${ARTIFACTS_DIR}/.claude-tokens-plan-validate.log" || true)
|
|
1855
|
+
parse_claude_tokens "${ARTIFACTS_DIR}/.claude-tokens-plan-validate.log"
|
|
1856
|
+
|
|
1857
|
+
# Save validation result
|
|
1858
|
+
echo "$validation_result" > "$ARTIFACTS_DIR/plan-validation.md"
|
|
1859
|
+
|
|
1860
|
+
if echo "$validation_result" | head -5 | grep -qi "VALID: true"; then
|
|
1861
|
+
success "Plan validation passed"
|
|
1862
|
+
plan_valid=true
|
|
1863
|
+
break
|
|
1864
|
+
fi
|
|
1865
|
+
|
|
1866
|
+
warn "Plan validation failed (attempt ${validation_attempts}/${max_validation_attempts})"
|
|
1867
|
+
|
|
1868
|
+
# Analyze failure mode to decide how to recover
|
|
1869
|
+
local failure_mode="unknown"
|
|
1870
|
+
local validation_lower
|
|
1871
|
+
validation_lower=$(echo "$validation_result" | tr '[:upper:]' '[:lower:]')
|
|
1872
|
+
if echo "$validation_lower" | grep -qE 'requirements? unclear|goal.*vague|ambiguous|underspecified'; then
|
|
1873
|
+
failure_mode="requirements_unclear"
|
|
1874
|
+
elif echo "$validation_lower" | grep -qE 'insufficient detail|not specific|too high.level|missing.*steps|lacks.*detail'; then
|
|
1875
|
+
failure_mode="insufficient_detail"
|
|
1876
|
+
elif echo "$validation_lower" | grep -qE 'scope too (large|broad)|too many|overly complex|break.*down'; then
|
|
1877
|
+
failure_mode="scope_too_large"
|
|
1878
|
+
fi
|
|
1879
|
+
|
|
1880
|
+
emit_event "plan.validation_failure" \
|
|
1881
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
1882
|
+
"attempt=$validation_attempts" \
|
|
1883
|
+
"failure_mode=$failure_mode"
|
|
1884
|
+
|
|
1885
|
+
# Track repeated failures — escalate if stuck in a loop
|
|
1886
|
+
if [[ -f "$ARTIFACTS_DIR/.plan-failure-sig.txt" ]]; then
|
|
1887
|
+
local prev_sig
|
|
1888
|
+
prev_sig=$(cat "$ARTIFACTS_DIR/.plan-failure-sig.txt" 2>/dev/null || true)
|
|
1889
|
+
if [[ "$failure_mode" == "$prev_sig" && "$failure_mode" != "unknown" ]]; then
|
|
1890
|
+
warn "Same validation failure mode repeated ($failure_mode) — escalating"
|
|
1891
|
+
emit_event "plan.validation_escalated" \
|
|
1892
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
1893
|
+
"failure_mode=$failure_mode"
|
|
1894
|
+
break
|
|
1895
|
+
fi
|
|
1896
|
+
fi
|
|
1897
|
+
echo "$failure_mode" > "$ARTIFACTS_DIR/.plan-failure-sig.txt"
|
|
1898
|
+
|
|
1899
|
+
if [[ "$validation_attempts" -lt "$max_validation_attempts" ]]; then
|
|
1900
|
+
info "Regenerating plan with validation feedback (mode: ${failure_mode})..."
|
|
1901
|
+
|
|
1902
|
+
# Tailor regeneration prompt based on failure mode
|
|
1903
|
+
local failure_guidance=""
|
|
1904
|
+
case "$failure_mode" in
|
|
1905
|
+
requirements_unclear)
|
|
1906
|
+
failure_guidance="The validator found the requirements unclear. Add more specific acceptance criteria, input/output examples, and concrete success metrics." ;;
|
|
1907
|
+
insufficient_detail)
|
|
1908
|
+
failure_guidance="The validator found the plan lacks detail. Break each task into smaller, more specific implementation steps with exact file paths and function names." ;;
|
|
1909
|
+
scope_too_large)
|
|
1910
|
+
failure_guidance="The validator found the scope too large. Focus on the minimal viable implementation and defer non-essential features to follow-up tasks." ;;
|
|
1911
|
+
esac
|
|
1912
|
+
|
|
1913
|
+
local regen_prompt="${plan_prompt}
|
|
1914
|
+
|
|
1915
|
+
IMPORTANT: A previous plan was rejected by validation. Issues found:
|
|
1916
|
+
$(echo "$validation_result" | tail -20)
|
|
1917
|
+
${failure_guidance:+
|
|
1918
|
+
GUIDANCE: ${failure_guidance}}
|
|
1919
|
+
|
|
1920
|
+
Fix these issues in the new plan."
|
|
1921
|
+
|
|
1922
|
+
claude --print --model "$plan_model" --max-turns 25 \
|
|
1923
|
+
"$regen_prompt" < /dev/null > "$plan_file" 2>"$_token_log" || true
|
|
1924
|
+
parse_claude_tokens "$_token_log"
|
|
1925
|
+
|
|
1926
|
+
line_count=$(wc -l < "$plan_file" | xargs)
|
|
1927
|
+
info "Regenerated plan: ${DIM}$plan_file${RESET} (${line_count} lines)"
|
|
1928
|
+
fi
|
|
1929
|
+
done
|
|
1930
|
+
|
|
1931
|
+
if [[ "$plan_valid" != "true" ]]; then
|
|
1932
|
+
warn "Plan validation did not pass after ${max_validation_attempts} attempts — proceeding anyway"
|
|
1933
|
+
fi
|
|
1934
|
+
|
|
1935
|
+
emit_event "plan.validated" \
|
|
1936
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
1937
|
+
"valid=${plan_valid}" \
|
|
1938
|
+
"attempts=${validation_attempts}"
|
|
1939
|
+
fi
|
|
1940
|
+
|
|
1345
1941
|
log_stage "plan" "Generated plan.md (${line_count} lines, $(echo "$checklist" | wc -l | xargs) tasks)"
|
|
1346
1942
|
}
|
|
1347
1943
|
|
|
@@ -1364,8 +1960,49 @@ stage_design() {
|
|
|
1364
1960
|
|
|
1365
1961
|
# Memory integration — inject context if memory system available
|
|
1366
1962
|
local memory_context=""
|
|
1367
|
-
if
|
|
1368
|
-
|
|
1963
|
+
if type intelligence_search_memory &>/dev/null 2>&1; then
|
|
1964
|
+
local mem_dir="${HOME}/.shipwright/memory"
|
|
1965
|
+
memory_context=$(intelligence_search_memory "design stage architecture patterns for: ${GOAL:-}" "$mem_dir" 5 2>/dev/null) || true
|
|
1966
|
+
fi
|
|
1967
|
+
if [[ -z "$memory_context" ]] && [[ -x "$SCRIPT_DIR/sw-memory.sh" ]]; then
|
|
1968
|
+
memory_context=$(bash "$SCRIPT_DIR/sw-memory.sh" inject "design" 2>/dev/null) || true
|
|
1969
|
+
fi
|
|
1970
|
+
|
|
1971
|
+
# Inject architecture model patterns if available
|
|
1972
|
+
local arch_context=""
|
|
1973
|
+
local repo_hash
|
|
1974
|
+
repo_hash=$(echo -n "$PROJECT_ROOT" | shasum -a 256 2>/dev/null | cut -c1-12 || echo "unknown")
|
|
1975
|
+
local arch_model_file="${HOME}/.shipwright/memory/${repo_hash}/architecture.json"
|
|
1976
|
+
if [[ -f "$arch_model_file" ]]; then
|
|
1977
|
+
local arch_patterns
|
|
1978
|
+
arch_patterns=$(jq -r '
|
|
1979
|
+
[.patterns // [] | .[] | "- \(.name // "unnamed"): \(.description // "no description")"] | join("\n")
|
|
1980
|
+
' "$arch_model_file" 2>/dev/null) || true
|
|
1981
|
+
local arch_layers
|
|
1982
|
+
arch_layers=$(jq -r '
|
|
1983
|
+
[.layers // [] | .[] | "- \(.name // "unnamed"): \(.path // "")"] | join("\n")
|
|
1984
|
+
' "$arch_model_file" 2>/dev/null) || true
|
|
1985
|
+
if [[ -n "$arch_patterns" || -n "$arch_layers" ]]; then
|
|
1986
|
+
arch_context="Previous designs in this repo follow these patterns:
|
|
1987
|
+
${arch_patterns:+Patterns:
|
|
1988
|
+
${arch_patterns}
|
|
1989
|
+
}${arch_layers:+Layers:
|
|
1990
|
+
${arch_layers}}"
|
|
1991
|
+
fi
|
|
1992
|
+
fi
|
|
1993
|
+
|
|
1994
|
+
# Inject rejected design approaches and anti-patterns from memory
|
|
1995
|
+
local design_antipatterns=""
|
|
1996
|
+
if type intelligence_search_memory &>/dev/null 2>&1; then
|
|
1997
|
+
local rejected_designs
|
|
1998
|
+
rejected_designs=$(intelligence_search_memory "rejected design approaches anti-patterns for: ${GOAL:-}" "${HOME}/.shipwright/memory" 3 2>/dev/null) || true
|
|
1999
|
+
if [[ -n "$rejected_designs" ]]; then
|
|
2000
|
+
design_antipatterns="
|
|
2001
|
+
## Rejected Approaches (from past reviews)
|
|
2002
|
+
These design approaches were rejected in past reviews. Avoid repeating them:
|
|
2003
|
+
${rejected_designs}
|
|
2004
|
+
"
|
|
2005
|
+
fi
|
|
1369
2006
|
fi
|
|
1370
2007
|
|
|
1371
2008
|
# Build design prompt with plan + project context
|
|
@@ -1387,7 +2024,10 @@ $(cat "$plan_file")
|
|
|
1387
2024
|
${memory_context:+
|
|
1388
2025
|
## Historical Context (from memory)
|
|
1389
2026
|
${memory_context}
|
|
1390
|
-
}
|
|
2027
|
+
}${arch_context:+
|
|
2028
|
+
## Architecture Model (from previous designs)
|
|
2029
|
+
${arch_context}
|
|
2030
|
+
}${design_antipatterns}
|
|
1391
2031
|
## Required Output — Architecture Decision Record
|
|
1392
2032
|
|
|
1393
2033
|
Produce this EXACT format:
|
|
@@ -1420,10 +2060,14 @@ Be concrete and specific. Reference actual file paths in the codebase. Consider
|
|
|
1420
2060
|
design_model=$(jq -r --arg id "design" '(.stages[] | select(.id == $id) | .config.model) // .defaults.model // "opus"' "$PIPELINE_CONFIG" 2>/dev/null) || true
|
|
1421
2061
|
[[ -n "$MODEL" ]] && design_model="$MODEL"
|
|
1422
2062
|
[[ -z "$design_model" || "$design_model" == "null" ]] && design_model="opus"
|
|
2063
|
+
# Intelligence model routing (when no explicit CLI --model override)
|
|
2064
|
+
if [[ -z "$MODEL" && -n "${CLAUDE_MODEL:-}" ]]; then
|
|
2065
|
+
design_model="$CLAUDE_MODEL"
|
|
2066
|
+
fi
|
|
1423
2067
|
|
|
1424
2068
|
local _token_log="${ARTIFACTS_DIR}/.claude-tokens-design.log"
|
|
1425
|
-
claude --print --model "$design_model" --max-turns
|
|
1426
|
-
"$design_prompt" > "$design_file" 2>"$_token_log" || true
|
|
2069
|
+
claude --print --model "$design_model" --max-turns 25 \
|
|
2070
|
+
"$design_prompt" < /dev/null > "$design_file" 2>"$_token_log" || true
|
|
1427
2071
|
parse_claude_tokens "$_token_log"
|
|
1428
2072
|
|
|
1429
2073
|
if [[ ! -s "$design_file" ]]; then
|
|
@@ -1475,8 +2119,12 @@ stage_build() {
|
|
|
1475
2119
|
|
|
1476
2120
|
# Memory integration — inject context if memory system available
|
|
1477
2121
|
local memory_context=""
|
|
1478
|
-
if
|
|
1479
|
-
|
|
2122
|
+
if type intelligence_search_memory &>/dev/null 2>&1; then
|
|
2123
|
+
local mem_dir="${HOME}/.shipwright/memory"
|
|
2124
|
+
memory_context=$(intelligence_search_memory "build stage for: ${GOAL:-}" "$mem_dir" 5 2>/dev/null) || true
|
|
2125
|
+
fi
|
|
2126
|
+
if [[ -z "$memory_context" ]] && [[ -x "$SCRIPT_DIR/sw-memory.sh" ]]; then
|
|
2127
|
+
memory_context=$(bash "$SCRIPT_DIR/sw-memory.sh" inject "build" 2>/dev/null) || true
|
|
1480
2128
|
fi
|
|
1481
2129
|
|
|
1482
2130
|
# Build enriched goal with full context
|
|
@@ -1512,6 +2160,44 @@ Task tracking (check off items as you complete them):
|
|
|
1512
2160
|
$(cat "$TASKS_FILE")"
|
|
1513
2161
|
fi
|
|
1514
2162
|
|
|
2163
|
+
# Inject file hotspots from GitHub intelligence
|
|
2164
|
+
if [[ "${NO_GITHUB:-}" != "true" ]] && type gh_file_change_frequency &>/dev/null 2>&1; then
|
|
2165
|
+
local build_hotspots
|
|
2166
|
+
build_hotspots=$(gh_file_change_frequency 2>/dev/null | head -5 || true)
|
|
2167
|
+
if [[ -n "$build_hotspots" ]]; then
|
|
2168
|
+
enriched_goal="${enriched_goal}
|
|
2169
|
+
|
|
2170
|
+
File hotspots (most frequently changed — review these carefully):
|
|
2171
|
+
${build_hotspots}"
|
|
2172
|
+
fi
|
|
2173
|
+
fi
|
|
2174
|
+
|
|
2175
|
+
# Inject security alerts context
|
|
2176
|
+
if [[ "${NO_GITHUB:-}" != "true" ]] && type gh_security_alerts &>/dev/null 2>&1; then
|
|
2177
|
+
local build_alerts
|
|
2178
|
+
build_alerts=$(gh_security_alerts 2>/dev/null | head -3 || true)
|
|
2179
|
+
if [[ -n "$build_alerts" ]]; then
|
|
2180
|
+
enriched_goal="${enriched_goal}
|
|
2181
|
+
|
|
2182
|
+
Active security alerts (do not introduce new vulnerabilities):
|
|
2183
|
+
${build_alerts}"
|
|
2184
|
+
fi
|
|
2185
|
+
fi
|
|
2186
|
+
|
|
2187
|
+
# Inject coverage baseline
|
|
2188
|
+
local repo_hash_build
|
|
2189
|
+
repo_hash_build=$(echo -n "$PROJECT_ROOT" | shasum -a 256 2>/dev/null | cut -c1-12 || echo "unknown")
|
|
2190
|
+
local coverage_file_build="${HOME}/.shipwright/baselines/${repo_hash_build}/coverage.json"
|
|
2191
|
+
if [[ -f "$coverage_file_build" ]]; then
|
|
2192
|
+
local coverage_baseline
|
|
2193
|
+
coverage_baseline=$(jq -r '.coverage_percent // empty' "$coverage_file_build" 2>/dev/null || true)
|
|
2194
|
+
if [[ -n "$coverage_baseline" ]]; then
|
|
2195
|
+
enriched_goal="${enriched_goal}
|
|
2196
|
+
|
|
2197
|
+
Coverage baseline: ${coverage_baseline}% — do not decrease coverage."
|
|
2198
|
+
fi
|
|
2199
|
+
fi
|
|
2200
|
+
|
|
1515
2201
|
loop_args+=("$enriched_goal")
|
|
1516
2202
|
|
|
1517
2203
|
# Build loop args from pipeline config + CLI overrides
|
|
@@ -1530,6 +2216,8 @@ $(cat "$TASKS_FILE")"
|
|
|
1530
2216
|
local max_iter
|
|
1531
2217
|
max_iter=$(jq -r --arg id "build" '(.stages[] | select(.id == $id) | .config.max_iterations) // 20' "$PIPELINE_CONFIG" 2>/dev/null) || true
|
|
1532
2218
|
[[ -z "$max_iter" || "$max_iter" == "null" ]] && max_iter=20
|
|
2219
|
+
# CLI --max-iterations override (from CI strategy engine)
|
|
2220
|
+
[[ -n "${MAX_ITERATIONS_OVERRIDE:-}" ]] && max_iter="$MAX_ITERATIONS_OVERRIDE"
|
|
1533
2221
|
|
|
1534
2222
|
local agents="${AGENTS}"
|
|
1535
2223
|
if [[ -z "$agents" ]]; then
|
|
@@ -1537,6 +2225,16 @@ $(cat "$TASKS_FILE")"
|
|
|
1537
2225
|
[[ -z "$agents" || "$agents" == "null" ]] && agents=1
|
|
1538
2226
|
fi
|
|
1539
2227
|
|
|
2228
|
+
# Intelligence: suggest parallelism if design indicates independent work
|
|
2229
|
+
if [[ "${agents:-1}" -le 1 ]] && [[ -s "$ARTIFACTS_DIR/design.md" ]]; then
|
|
2230
|
+
local design_lower
|
|
2231
|
+
design_lower=$(tr '[:upper:]' '[:lower:]' < "$ARTIFACTS_DIR/design.md" 2>/dev/null || true)
|
|
2232
|
+
if echo "$design_lower" | grep -qE 'independent (files|modules|components|services)|separate (modules|packages|directories)|parallel|no shared state'; then
|
|
2233
|
+
info "Design mentions independent modules — consider --agents 2 for parallelism"
|
|
2234
|
+
emit_event "build.parallelism_suggested" "issue=${ISSUE_NUMBER:-0}" "current_agents=$agents"
|
|
2235
|
+
fi
|
|
2236
|
+
fi
|
|
2237
|
+
|
|
1540
2238
|
local audit
|
|
1541
2239
|
audit=$(jq -r --arg id "build" '(.stages[] | select(.id == $id) | .config.audit) // false' "$PIPELINE_CONFIG" 2>/dev/null) || true
|
|
1542
2240
|
local quality
|
|
@@ -1547,15 +2245,30 @@ $(cat "$TASKS_FILE")"
|
|
|
1547
2245
|
build_model=$(jq -r '.defaults.model // "opus"' "$PIPELINE_CONFIG" 2>/dev/null) || true
|
|
1548
2246
|
[[ -z "$build_model" || "$build_model" == "null" ]] && build_model="opus"
|
|
1549
2247
|
fi
|
|
2248
|
+
# Intelligence model routing (when no explicit CLI --model override)
|
|
2249
|
+
if [[ -z "$MODEL" && -n "${CLAUDE_MODEL:-}" ]]; then
|
|
2250
|
+
build_model="$CLAUDE_MODEL"
|
|
2251
|
+
fi
|
|
1550
2252
|
|
|
1551
2253
|
[[ -n "$test_cmd" && "$test_cmd" != "null" ]] && loop_args+=(--test-cmd "$test_cmd")
|
|
1552
2254
|
loop_args+=(--max-iterations "$max_iter")
|
|
1553
2255
|
loop_args+=(--model "$build_model")
|
|
1554
2256
|
[[ "$agents" -gt 1 ]] 2>/dev/null && loop_args+=(--agents "$agents")
|
|
1555
|
-
|
|
1556
|
-
|
|
2257
|
+
|
|
2258
|
+
# Quality gates: always enabled in CI, otherwise from template config
|
|
2259
|
+
if [[ "${CI_MODE:-false}" == "true" ]]; then
|
|
2260
|
+
loop_args+=(--audit --audit-agent --quality-gates)
|
|
2261
|
+
else
|
|
2262
|
+
[[ "$audit" == "true" ]] && loop_args+=(--audit --audit-agent)
|
|
2263
|
+
[[ "$quality" == "true" ]] && loop_args+=(--quality-gates)
|
|
2264
|
+
fi
|
|
2265
|
+
|
|
2266
|
+
# Definition of Done: use plan-extracted DoD if available
|
|
1557
2267
|
[[ -s "$dod_file" ]] && loop_args+=(--definition-of-done "$dod_file")
|
|
1558
2268
|
|
|
2269
|
+
# Skip permissions in CI (no interactive terminal)
|
|
2270
|
+
[[ "${CI_MODE:-false}" == "true" ]] && loop_args+=(--skip-permissions)
|
|
2271
|
+
|
|
1559
2272
|
info "Starting build loop: ${DIM}shipwright loop${RESET} (max ${max_iter} iterations, ${agents} agent(s))"
|
|
1560
2273
|
|
|
1561
2274
|
# Post build start to GitHub
|
|
@@ -1564,7 +2277,8 @@ $(cat "$TASKS_FILE")"
|
|
|
1564
2277
|
fi
|
|
1565
2278
|
|
|
1566
2279
|
local _token_log="${ARTIFACTS_DIR}/.claude-tokens-build.log"
|
|
1567
|
-
|
|
2280
|
+
export PIPELINE_JOB_ID="${PIPELINE_NAME:-pipeline-$$}"
|
|
2281
|
+
sw loop "${loop_args[@]}" < /dev/null 2>"$_token_log" || {
|
|
1568
2282
|
parse_claude_tokens "$_token_log"
|
|
1569
2283
|
error "Build loop failed"
|
|
1570
2284
|
return 1
|
|
@@ -1576,6 +2290,29 @@ $(cat "$TASKS_FILE")"
|
|
|
1576
2290
|
commit_count=$(git log --oneline "${BASE_BRANCH}..HEAD" 2>/dev/null | wc -l | xargs)
|
|
1577
2291
|
info "Build produced ${BOLD}$commit_count${RESET} commit(s)"
|
|
1578
2292
|
|
|
2293
|
+
# Commit quality evaluation when intelligence is enabled
|
|
2294
|
+
if type intelligence_search_memory &>/dev/null 2>&1 && command -v claude &>/dev/null && [[ "${commit_count:-0}" -gt 0 ]]; then
|
|
2295
|
+
local commit_msgs
|
|
2296
|
+
commit_msgs=$(git log --format="%s" "${BASE_BRANCH}..HEAD" 2>/dev/null | head -20)
|
|
2297
|
+
local quality_score
|
|
2298
|
+
quality_score=$(claude --print --output-format text -p "Rate the quality of these git commit messages on a scale of 0-100. Consider: focus (one thing per commit), clarity (describes the why), atomicity (small logical units). Reply with ONLY a number 0-100.
|
|
2299
|
+
|
|
2300
|
+
Commit messages:
|
|
2301
|
+
${commit_msgs}" --model haiku < /dev/null 2>/dev/null || true)
|
|
2302
|
+
quality_score=$(echo "$quality_score" | grep -oE '^[0-9]+' | head -1 || true)
|
|
2303
|
+
if [[ -n "$quality_score" ]]; then
|
|
2304
|
+
emit_event "build.commit_quality" \
|
|
2305
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
2306
|
+
"score=$quality_score" \
|
|
2307
|
+
"commit_count=$commit_count"
|
|
2308
|
+
if [[ "$quality_score" -lt 40 ]] 2>/dev/null; then
|
|
2309
|
+
warn "Commit message quality low (score: ${quality_score}/100)"
|
|
2310
|
+
else
|
|
2311
|
+
info "Commit quality score: ${quality_score}/100"
|
|
2312
|
+
fi
|
|
2313
|
+
fi
|
|
2314
|
+
fi
|
|
2315
|
+
|
|
1579
2316
|
log_stage "build" "Build loop completed ($commit_count commits)"
|
|
1580
2317
|
}
|
|
1581
2318
|
|
|
@@ -1603,34 +2340,53 @@ stage_test() {
|
|
|
1603
2340
|
|
|
1604
2341
|
info "Running tests: ${DIM}$test_cmd${RESET}"
|
|
1605
2342
|
local test_exit=0
|
|
1606
|
-
|
|
2343
|
+
bash -c "$test_cmd" > "$test_log" 2>&1 || test_exit=$?
|
|
1607
2344
|
|
|
1608
2345
|
if [[ "$test_exit" -eq 0 ]]; then
|
|
1609
2346
|
success "Tests passed"
|
|
1610
2347
|
else
|
|
1611
2348
|
error "Tests failed (exit code: $test_exit)"
|
|
1612
|
-
|
|
2349
|
+
# Extract most relevant error section (assertion failures, stack traces)
|
|
2350
|
+
local relevant_output=""
|
|
2351
|
+
relevant_output=$(grep -A5 -E 'FAIL|AssertionError|Expected.*but.*got|Error:|panic:|assert' "$test_log" 2>/dev/null | tail -40 || true)
|
|
2352
|
+
if [[ -z "$relevant_output" ]]; then
|
|
2353
|
+
relevant_output=$(tail -40 "$test_log")
|
|
2354
|
+
fi
|
|
2355
|
+
echo "$relevant_output"
|
|
1613
2356
|
|
|
1614
|
-
# Post failure to GitHub
|
|
2357
|
+
# Post failure to GitHub with more context
|
|
1615
2358
|
if [[ -n "$ISSUE_NUMBER" ]]; then
|
|
1616
|
-
|
|
2359
|
+
local log_lines
|
|
2360
|
+
log_lines=$(wc -l < "$test_log" 2>/dev/null || echo "0")
|
|
2361
|
+
local log_excerpt
|
|
2362
|
+
if [[ "$log_lines" -lt 60 ]]; then
|
|
2363
|
+
log_excerpt="$(cat "$test_log" 2>/dev/null || true)"
|
|
2364
|
+
else
|
|
2365
|
+
log_excerpt="$(head -20 "$test_log" 2>/dev/null || true)
|
|
2366
|
+
... (${log_lines} lines total, showing head + tail) ...
|
|
2367
|
+
$(tail -30 "$test_log" 2>/dev/null || true)"
|
|
2368
|
+
fi
|
|
2369
|
+
gh_comment_issue "$ISSUE_NUMBER" "❌ **Tests failed** (exit code: $test_exit, ${log_lines} lines)
|
|
1617
2370
|
\`\`\`
|
|
1618
|
-
$
|
|
2371
|
+
${log_excerpt}
|
|
1619
2372
|
\`\`\`"
|
|
1620
2373
|
fi
|
|
1621
2374
|
return 1
|
|
1622
2375
|
fi
|
|
1623
2376
|
|
|
1624
|
-
# Coverage check
|
|
2377
|
+
# Coverage check — only enforce when coverage data is actually detected
|
|
1625
2378
|
local coverage=""
|
|
1626
2379
|
if [[ "$coverage_min" -gt 0 ]] 2>/dev/null; then
|
|
1627
|
-
coverage=$(
|
|
1628
|
-
|
|
1629
|
-
|
|
2380
|
+
coverage=$(parse_coverage_from_output "$test_log")
|
|
2381
|
+
if [[ -z "$coverage" ]]; then
|
|
2382
|
+
# No coverage data found — skip enforcement (project may not have coverage tooling)
|
|
2383
|
+
info "No coverage data detected — skipping coverage check (min: ${coverage_min}%)"
|
|
2384
|
+
elif awk -v cov="$coverage" -v min="$coverage_min" 'BEGIN{exit !(cov < min)}' 2>/dev/null; then
|
|
1630
2385
|
warn "Coverage ${coverage}% below minimum ${coverage_min}%"
|
|
1631
2386
|
return 1
|
|
2387
|
+
else
|
|
2388
|
+
info "Coverage: ${coverage}% (min: ${coverage_min}%)"
|
|
1632
2389
|
fi
|
|
1633
|
-
info "Coverage: ${coverage}% (min: ${coverage_min}%)"
|
|
1634
2390
|
fi
|
|
1635
2391
|
|
|
1636
2392
|
# Post test results to GitHub
|
|
@@ -1675,10 +2431,34 @@ stage_review() {
|
|
|
1675
2431
|
diff_stats=$(git diff --stat "${BASE_BRANCH}...${GIT_BRANCH}" 2>/dev/null | tail -1 || echo "")
|
|
1676
2432
|
info "Running AI code review... ${DIM}($diff_stats)${RESET}"
|
|
1677
2433
|
|
|
2434
|
+
# Semantic risk scoring when intelligence is enabled
|
|
2435
|
+
if type intelligence_search_memory &>/dev/null 2>&1 && command -v claude &>/dev/null; then
|
|
2436
|
+
local diff_files
|
|
2437
|
+
diff_files=$(git diff --name-only "${BASE_BRANCH}...${GIT_BRANCH}" 2>/dev/null || true)
|
|
2438
|
+
local risk_score="low"
|
|
2439
|
+
# Fast heuristic: flag high-risk file patterns
|
|
2440
|
+
if echo "$diff_files" | grep -qiE 'migration|schema|auth|crypto|security|password|token|secret|\.env'; then
|
|
2441
|
+
risk_score="high"
|
|
2442
|
+
elif echo "$diff_files" | grep -qiE 'api|route|controller|middleware|hook'; then
|
|
2443
|
+
risk_score="medium"
|
|
2444
|
+
fi
|
|
2445
|
+
emit_event "review.risk_assessed" \
|
|
2446
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
2447
|
+
"risk=$risk_score" \
|
|
2448
|
+
"files_changed=$(echo "$diff_files" | wc -l | xargs)"
|
|
2449
|
+
if [[ "$risk_score" == "high" ]]; then
|
|
2450
|
+
warn "High-risk changes detected (DB schema, auth, crypto, or secrets)"
|
|
2451
|
+
fi
|
|
2452
|
+
fi
|
|
2453
|
+
|
|
1678
2454
|
local review_model="${MODEL:-opus}"
|
|
2455
|
+
# Intelligence model routing (when no explicit CLI --model override)
|
|
2456
|
+
if [[ -z "$MODEL" && -n "${CLAUDE_MODEL:-}" ]]; then
|
|
2457
|
+
review_model="$CLAUDE_MODEL"
|
|
2458
|
+
fi
|
|
1679
2459
|
|
|
1680
|
-
|
|
1681
|
-
|
|
2460
|
+
# Build review prompt with project context
|
|
2461
|
+
local review_prompt="You are a senior code reviewer. Review this git diff thoroughly.
|
|
1682
2462
|
|
|
1683
2463
|
For each issue found, use this format:
|
|
1684
2464
|
- **[SEVERITY]** file:line — description
|
|
@@ -1691,24 +2471,102 @@ Focus on:
|
|
|
1691
2471
|
3. Error handling gaps
|
|
1692
2472
|
4. Performance issues
|
|
1693
2473
|
5. Missing validation
|
|
2474
|
+
6. Project convention violations (see conventions below)
|
|
1694
2475
|
|
|
1695
2476
|
Be specific. Reference exact file paths and line numbers. Only flag genuine issues.
|
|
2477
|
+
If no issues are found, write: \"Review clean — no issues found.\"
|
|
2478
|
+
"
|
|
2479
|
+
|
|
2480
|
+
# Inject previous review findings and anti-patterns from memory
|
|
2481
|
+
if type intelligence_search_memory &>/dev/null 2>&1; then
|
|
2482
|
+
local review_memory
|
|
2483
|
+
review_memory=$(intelligence_search_memory "code review findings anti-patterns for: ${GOAL:-}" "${HOME}/.shipwright/memory" 5 2>/dev/null) || true
|
|
2484
|
+
if [[ -n "$review_memory" ]]; then
|
|
2485
|
+
review_prompt+="
|
|
2486
|
+
## Known Issues from Previous Reviews
|
|
2487
|
+
These anti-patterns and issues have been found in past reviews of this codebase. Flag them if they recur:
|
|
2488
|
+
${review_memory}
|
|
2489
|
+
"
|
|
2490
|
+
fi
|
|
2491
|
+
fi
|
|
2492
|
+
|
|
2493
|
+
# Inject project conventions if CLAUDE.md exists
|
|
2494
|
+
local claudemd="$PROJECT_ROOT/.claude/CLAUDE.md"
|
|
2495
|
+
if [[ -f "$claudemd" ]]; then
|
|
2496
|
+
local conventions
|
|
2497
|
+
conventions=$(grep -A2 'Common Pitfalls\|Shell Standards\|Bash 3.2' "$claudemd" 2>/dev/null | head -20 || true)
|
|
2498
|
+
if [[ -n "$conventions" ]]; then
|
|
2499
|
+
review_prompt+="
|
|
2500
|
+
## Project Conventions
|
|
2501
|
+
${conventions}
|
|
2502
|
+
"
|
|
2503
|
+
fi
|
|
2504
|
+
fi
|
|
2505
|
+
|
|
2506
|
+
# Inject CODEOWNERS focus areas for review
|
|
2507
|
+
if [[ "${NO_GITHUB:-}" != "true" ]] && type gh_codeowners &>/dev/null 2>&1; then
|
|
2508
|
+
local review_owners
|
|
2509
|
+
review_owners=$(gh_codeowners 2>/dev/null | head -10 || true)
|
|
2510
|
+
if [[ -n "$review_owners" ]]; then
|
|
2511
|
+
review_prompt+="
|
|
2512
|
+
## Code Owners (focus areas)
|
|
2513
|
+
${review_owners}
|
|
2514
|
+
"
|
|
2515
|
+
fi
|
|
2516
|
+
fi
|
|
2517
|
+
|
|
2518
|
+
# Inject Definition of Done if present
|
|
2519
|
+
local dod_file="$PROJECT_ROOT/.claude/DEFINITION-OF-DONE.md"
|
|
2520
|
+
if [[ -f "$dod_file" ]]; then
|
|
2521
|
+
review_prompt+="
|
|
2522
|
+
## Definition of Done (verify these)
|
|
2523
|
+
$(cat "$dod_file")
|
|
2524
|
+
"
|
|
2525
|
+
fi
|
|
2526
|
+
|
|
2527
|
+
review_prompt+="
|
|
2528
|
+
## Diff to Review
|
|
2529
|
+
$(cat "$diff_file")"
|
|
2530
|
+
|
|
2531
|
+
# Build claude args — add --dangerously-skip-permissions in CI
|
|
2532
|
+
local review_args=(--print --model "$review_model" --max-turns 25)
|
|
2533
|
+
if [[ "${CI_MODE:-false}" == "true" ]]; then
|
|
2534
|
+
review_args+=(--dangerously-skip-permissions)
|
|
2535
|
+
fi
|
|
1696
2536
|
|
|
1697
|
-
$
|
|
2537
|
+
claude "${review_args[@]}" "$review_prompt" < /dev/null > "$review_file" 2>"${ARTIFACTS_DIR}/.claude-tokens-review.log" || true
|
|
1698
2538
|
parse_claude_tokens "${ARTIFACTS_DIR}/.claude-tokens-review.log"
|
|
1699
2539
|
|
|
1700
2540
|
if [[ ! -s "$review_file" ]]; then
|
|
1701
|
-
warn "Review produced no output"
|
|
2541
|
+
warn "Review produced no output — check ${ARTIFACTS_DIR}/.claude-tokens-review.log for errors"
|
|
1702
2542
|
return 0
|
|
1703
2543
|
fi
|
|
1704
2544
|
|
|
1705
|
-
|
|
1706
|
-
critical_count
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
2545
|
+
# Extract severity counts — try JSON structure first, then grep fallback
|
|
2546
|
+
local critical_count=0 bug_count=0 warning_count=0
|
|
2547
|
+
|
|
2548
|
+
# Check if review output is structured JSON (e.g. from structured review tools)
|
|
2549
|
+
local json_parsed=false
|
|
2550
|
+
if head -1 "$review_file" 2>/dev/null | grep -q '^{' 2>/dev/null; then
|
|
2551
|
+
local j_critical j_bug j_warning
|
|
2552
|
+
j_critical=$(jq -r '.issues | map(select(.severity == "Critical")) | length' "$review_file" 2>/dev/null || echo "")
|
|
2553
|
+
if [[ -n "$j_critical" && "$j_critical" != "null" ]]; then
|
|
2554
|
+
critical_count="$j_critical"
|
|
2555
|
+
bug_count=$(jq -r '.issues | map(select(.severity == "Bug" or .severity == "Security")) | length' "$review_file" 2>/dev/null || echo "0")
|
|
2556
|
+
warning_count=$(jq -r '.issues | map(select(.severity == "Warning" or .severity == "Suggestion")) | length' "$review_file" 2>/dev/null || echo "0")
|
|
2557
|
+
json_parsed=true
|
|
2558
|
+
fi
|
|
2559
|
+
fi
|
|
2560
|
+
|
|
2561
|
+
# Grep fallback for markdown-formatted review output
|
|
2562
|
+
if [[ "$json_parsed" != "true" ]]; then
|
|
2563
|
+
critical_count=$(grep -ciE '\*\*\[?Critical\]?\*\*' "$review_file" 2>/dev/null || true)
|
|
2564
|
+
critical_count="${critical_count:-0}"
|
|
2565
|
+
bug_count=$(grep -ciE '\*\*\[?(Bug|Security)\]?\*\*' "$review_file" 2>/dev/null || true)
|
|
2566
|
+
bug_count="${bug_count:-0}"
|
|
2567
|
+
warning_count=$(grep -ciE '\*\*\[?(Warning|Suggestion)\]?\*\*' "$review_file" 2>/dev/null || true)
|
|
2568
|
+
warning_count="${warning_count:-0}"
|
|
2569
|
+
fi
|
|
1712
2570
|
local total_issues=$((critical_count + bug_count + warning_count))
|
|
1713
2571
|
|
|
1714
2572
|
if [[ "$critical_count" -gt 0 ]]; then
|
|
@@ -1721,6 +2579,68 @@ $(cat "$diff_file")" > "$review_file" 2>"${ARTIFACTS_DIR}/.claude-tokens-review.
|
|
|
1721
2579
|
success "Review clean"
|
|
1722
2580
|
fi
|
|
1723
2581
|
|
|
2582
|
+
# ── Review Blocking Gate ──
|
|
2583
|
+
# Block pipeline on critical/security issues unless compound_quality handles them
|
|
2584
|
+
local security_count
|
|
2585
|
+
security_count=$(grep -ciE '\*\*\[?Security\]?\*\*' "$review_file" 2>/dev/null || true)
|
|
2586
|
+
security_count="${security_count:-0}"
|
|
2587
|
+
|
|
2588
|
+
local blocking_issues=$((critical_count + security_count))
|
|
2589
|
+
|
|
2590
|
+
if [[ "$blocking_issues" -gt 0 ]]; then
|
|
2591
|
+
# Check if compound_quality stage is enabled — if so, let it handle issues
|
|
2592
|
+
local compound_enabled="false"
|
|
2593
|
+
if [[ -n "${PIPELINE_CONFIG:-}" && -f "${PIPELINE_CONFIG:-/dev/null}" ]]; then
|
|
2594
|
+
compound_enabled=$(jq -r '.stages[] | select(.id == "compound_quality") | .enabled' "$PIPELINE_CONFIG" 2>/dev/null) || true
|
|
2595
|
+
[[ -z "$compound_enabled" || "$compound_enabled" == "null" ]] && compound_enabled="false"
|
|
2596
|
+
fi
|
|
2597
|
+
|
|
2598
|
+
# Check if this is a fast template (don't block fast pipelines)
|
|
2599
|
+
local is_fast="false"
|
|
2600
|
+
if [[ "${PIPELINE_NAME:-}" == "fast" || "${PIPELINE_NAME:-}" == "hotfix" ]]; then
|
|
2601
|
+
is_fast="true"
|
|
2602
|
+
fi
|
|
2603
|
+
|
|
2604
|
+
if [[ "$compound_enabled" == "true" ]]; then
|
|
2605
|
+
info "Review found ${blocking_issues} critical/security issue(s) — compound_quality stage will handle"
|
|
2606
|
+
elif [[ "$is_fast" == "true" ]]; then
|
|
2607
|
+
warn "Review found ${blocking_issues} critical/security issue(s) — fast template, not blocking"
|
|
2608
|
+
elif [[ "${SKIP_GATES:-false}" == "true" ]]; then
|
|
2609
|
+
warn "Review found ${blocking_issues} critical/security issue(s) — skip-gates mode, not blocking"
|
|
2610
|
+
else
|
|
2611
|
+
error "Review found ${BOLD}${blocking_issues} critical/security issue(s)${RESET} — blocking pipeline"
|
|
2612
|
+
emit_event "review.blocked" \
|
|
2613
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
2614
|
+
"critical=${critical_count}" \
|
|
2615
|
+
"security=${security_count}"
|
|
2616
|
+
|
|
2617
|
+
# Save blocking issues for self-healing context
|
|
2618
|
+
grep -iE '\*\*\[?(Critical|Security)\]?\*\*' "$review_file" > "$ARTIFACTS_DIR/review-blockers.md" 2>/dev/null || true
|
|
2619
|
+
|
|
2620
|
+
# Post review to GitHub before failing
|
|
2621
|
+
if [[ -n "$ISSUE_NUMBER" ]]; then
|
|
2622
|
+
local review_summary
|
|
2623
|
+
review_summary=$(head -40 "$review_file")
|
|
2624
|
+
gh_comment_issue "$ISSUE_NUMBER" "## 🔍 Code Review — ❌ Blocked
|
|
2625
|
+
|
|
2626
|
+
**Stats:** $diff_stats
|
|
2627
|
+
**Blocking issues:** ${blocking_issues} (${critical_count} critical, ${security_count} security)
|
|
2628
|
+
|
|
2629
|
+
<details>
|
|
2630
|
+
<summary>Review details</summary>
|
|
2631
|
+
|
|
2632
|
+
${review_summary}
|
|
2633
|
+
|
|
2634
|
+
</details>
|
|
2635
|
+
|
|
2636
|
+
_Pipeline will attempt self-healing rebuild._"
|
|
2637
|
+
fi
|
|
2638
|
+
|
|
2639
|
+
log_stage "review" "BLOCKED: $blocking_issues critical/security issues found"
|
|
2640
|
+
return 1
|
|
2641
|
+
fi
|
|
2642
|
+
fi
|
|
2643
|
+
|
|
1724
2644
|
# Post review to GitHub issue
|
|
1725
2645
|
if [[ -n "$ISSUE_NUMBER" ]]; then
|
|
1726
2646
|
local review_summary
|
|
@@ -1747,6 +2667,47 @@ stage_pr() {
|
|
|
1747
2667
|
local test_log="$ARTIFACTS_DIR/test-results.log"
|
|
1748
2668
|
local review_file="$ARTIFACTS_DIR/review.md"
|
|
1749
2669
|
|
|
2670
|
+
# ── PR Hygiene Checks (informational) ──
|
|
2671
|
+
local hygiene_commit_count
|
|
2672
|
+
hygiene_commit_count=$(git log --oneline "${BASE_BRANCH}..HEAD" 2>/dev/null | wc -l | xargs)
|
|
2673
|
+
hygiene_commit_count="${hygiene_commit_count:-0}"
|
|
2674
|
+
|
|
2675
|
+
if [[ "$hygiene_commit_count" -gt 20 ]]; then
|
|
2676
|
+
warn "PR has ${hygiene_commit_count} commits — consider squashing before merge"
|
|
2677
|
+
fi
|
|
2678
|
+
|
|
2679
|
+
# Check for WIP/fixup/squash commits (expanded patterns)
|
|
2680
|
+
local wip_commits
|
|
2681
|
+
wip_commits=$(git log --oneline "${BASE_BRANCH}..HEAD" 2>/dev/null | grep -ciE '^[0-9a-f]+ (WIP|fixup!|squash!|TODO|HACK|TEMP|BROKEN|wip[:-]|temp[:-]|broken[:-]|do not merge)' || true)
|
|
2682
|
+
wip_commits="${wip_commits:-0}"
|
|
2683
|
+
if [[ "$wip_commits" -gt 0 ]]; then
|
|
2684
|
+
warn "Branch has ${wip_commits} WIP/fixup/squash/temp commit(s) — consider cleaning up"
|
|
2685
|
+
fi
|
|
2686
|
+
|
|
2687
|
+
# ── PR Quality Gate: reject PRs with no real code changes ──
|
|
2688
|
+
local real_files
|
|
2689
|
+
real_files=$(git diff --name-only "${BASE_BRANCH}...HEAD" 2>/dev/null | grep -v '^\.claude/' | grep -v '^\.github/' || true)
|
|
2690
|
+
if [[ -z "$real_files" ]]; then
|
|
2691
|
+
error "No real code changes detected — only pipeline artifacts (.claude/ logs)."
|
|
2692
|
+
error "The build agent did not produce meaningful changes. Skipping PR creation."
|
|
2693
|
+
emit_event "pr.rejected" "issue=${ISSUE_NUMBER:-0}" "reason=no_real_changes"
|
|
2694
|
+
# Mark issue so auto-retry knows not to retry empty builds
|
|
2695
|
+
if [[ -n "${ISSUE_NUMBER:-}" && "${ISSUE_NUMBER:-0}" != "0" ]]; then
|
|
2696
|
+
gh issue comment "$ISSUE_NUMBER" --body "<!-- SHIPWRIGHT-NO-CHANGES: true -->" 2>/dev/null || true
|
|
2697
|
+
fi
|
|
2698
|
+
return 1
|
|
2699
|
+
fi
|
|
2700
|
+
local real_file_count
|
|
2701
|
+
real_file_count=$(echo "$real_files" | wc -l | xargs)
|
|
2702
|
+
info "PR quality gate: ${real_file_count} real file(s) changed"
|
|
2703
|
+
|
|
2704
|
+
# Commit any uncommitted changes left by the build agent
|
|
2705
|
+
if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
|
|
2706
|
+
info "Committing remaining uncommitted changes..."
|
|
2707
|
+
git add -A 2>/dev/null || true
|
|
2708
|
+
git commit -m "chore: pipeline cleanup — commit remaining build changes" --no-verify 2>/dev/null || true
|
|
2709
|
+
fi
|
|
2710
|
+
|
|
1750
2711
|
# Auto-rebase onto latest base branch before PR
|
|
1751
2712
|
auto_rebase || {
|
|
1752
2713
|
warn "Rebase/merge failed — pushing as-is"
|
|
@@ -1762,27 +2723,105 @@ stage_pr() {
|
|
|
1762
2723
|
}
|
|
1763
2724
|
}
|
|
1764
2725
|
|
|
1765
|
-
#
|
|
1766
|
-
local
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
2726
|
+
# ── Developer Simulation (pre-PR review) ──
|
|
2727
|
+
local simulation_summary=""
|
|
2728
|
+
if type simulation_review &>/dev/null 2>&1; then
|
|
2729
|
+
local sim_enabled
|
|
2730
|
+
sim_enabled=$(jq -r '.intelligence.simulation_enabled // false' "$PIPELINE_CONFIG" 2>/dev/null || echo "false")
|
|
2731
|
+
# Also check daemon-config
|
|
2732
|
+
local daemon_cfg=".claude/daemon-config.json"
|
|
2733
|
+
if [[ "$sim_enabled" != "true" && -f "$daemon_cfg" ]]; then
|
|
2734
|
+
sim_enabled=$(jq -r '.intelligence.simulation_enabled // false' "$daemon_cfg" 2>/dev/null || echo "false")
|
|
2735
|
+
fi
|
|
2736
|
+
if [[ "$sim_enabled" == "true" ]]; then
|
|
2737
|
+
info "Running developer simulation review..."
|
|
2738
|
+
local diff_for_sim
|
|
2739
|
+
diff_for_sim=$(git diff "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
|
|
2740
|
+
if [[ -n "$diff_for_sim" ]]; then
|
|
2741
|
+
local sim_result
|
|
2742
|
+
sim_result=$(simulation_review "$diff_for_sim" "${GOAL:-}" 2>/dev/null || echo "")
|
|
2743
|
+
if [[ -n "$sim_result" && "$sim_result" != *'"error"'* ]]; then
|
|
2744
|
+
echo "$sim_result" > "$ARTIFACTS_DIR/simulation-review.json"
|
|
2745
|
+
local sim_count
|
|
2746
|
+
sim_count=$(echo "$sim_result" | jq 'length' 2>/dev/null || echo "0")
|
|
2747
|
+
simulation_summary="**Developer simulation:** ${sim_count} reviewer concerns pre-addressed"
|
|
2748
|
+
success "Simulation complete: ${sim_count} concerns found and addressed"
|
|
2749
|
+
emit_event "simulation.complete" "issue=${ISSUE_NUMBER:-0}" "concerns=${sim_count}"
|
|
2750
|
+
else
|
|
2751
|
+
info "Simulation returned no actionable concerns"
|
|
2752
|
+
fi
|
|
2753
|
+
fi
|
|
2754
|
+
fi
|
|
2755
|
+
fi
|
|
2756
|
+
|
|
2757
|
+
# ── Architecture Validation (pre-PR check) ──
|
|
2758
|
+
local arch_summary=""
|
|
2759
|
+
if type architecture_validate_changes &>/dev/null 2>&1; then
|
|
2760
|
+
local arch_enabled
|
|
2761
|
+
arch_enabled=$(jq -r '.intelligence.architecture_enabled // false' "$PIPELINE_CONFIG" 2>/dev/null || echo "false")
|
|
2762
|
+
local daemon_cfg=".claude/daemon-config.json"
|
|
2763
|
+
if [[ "$arch_enabled" != "true" && -f "$daemon_cfg" ]]; then
|
|
2764
|
+
arch_enabled=$(jq -r '.intelligence.architecture_enabled // false' "$daemon_cfg" 2>/dev/null || echo "false")
|
|
2765
|
+
fi
|
|
2766
|
+
if [[ "$arch_enabled" == "true" ]]; then
|
|
2767
|
+
info "Validating architecture..."
|
|
2768
|
+
local diff_for_arch
|
|
2769
|
+
diff_for_arch=$(git diff "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
|
|
2770
|
+
if [[ -n "$diff_for_arch" ]]; then
|
|
2771
|
+
local arch_result
|
|
2772
|
+
arch_result=$(architecture_validate_changes "$diff_for_arch" "" 2>/dev/null || echo "")
|
|
2773
|
+
if [[ -n "$arch_result" && "$arch_result" != *'"error"'* ]]; then
|
|
2774
|
+
echo "$arch_result" > "$ARTIFACTS_DIR/architecture-validation.json"
|
|
2775
|
+
local violation_count
|
|
2776
|
+
violation_count=$(echo "$arch_result" | jq '[.violations[]? | select(.severity == "critical" or .severity == "high")] | length' 2>/dev/null || echo "0")
|
|
2777
|
+
arch_summary="**Architecture validation:** ${violation_count} violations"
|
|
2778
|
+
if [[ "$violation_count" -gt 0 ]]; then
|
|
2779
|
+
warn "Architecture: ${violation_count} high/critical violations found"
|
|
2780
|
+
else
|
|
2781
|
+
success "Architecture validation passed"
|
|
2782
|
+
fi
|
|
2783
|
+
emit_event "architecture.validated" "issue=${ISSUE_NUMBER:-0}" "violations=${violation_count}"
|
|
2784
|
+
else
|
|
2785
|
+
info "Architecture validation returned no results"
|
|
2786
|
+
fi
|
|
2787
|
+
fi
|
|
2788
|
+
fi
|
|
2789
|
+
fi
|
|
2790
|
+
|
|
2791
|
+
# Build PR title — prefer GOAL over plan file first line
|
|
2792
|
+
# (plan file first line often contains Claude analysis text, not a clean title)
|
|
2793
|
+
local pr_title=""
|
|
2794
|
+
if [[ -n "${GOAL:-}" ]]; then
|
|
2795
|
+
pr_title=$(echo "$GOAL" | cut -c1-70)
|
|
2796
|
+
fi
|
|
2797
|
+
if [[ -z "$pr_title" ]] && [[ -s "$plan_file" ]]; then
|
|
2798
|
+
pr_title=$(head -1 "$plan_file" 2>/dev/null | sed 's/^#* *//' | cut -c1-70)
|
|
2799
|
+
fi
|
|
2800
|
+
[[ -z "$pr_title" ]] && pr_title="Pipeline changes for issue ${ISSUE_NUMBER:-unknown}"
|
|
2801
|
+
|
|
2802
|
+
# Build comprehensive PR body
|
|
2803
|
+
local plan_summary=""
|
|
2804
|
+
if [[ -s "$plan_file" ]]; then
|
|
2805
|
+
plan_summary=$(head -20 "$plan_file" 2>/dev/null | tail -15)
|
|
2806
|
+
fi
|
|
2807
|
+
|
|
2808
|
+
local test_summary=""
|
|
2809
|
+
if [[ -s "$test_log" ]]; then
|
|
2810
|
+
test_summary=$(tail -10 "$test_log")
|
|
1779
2811
|
fi
|
|
1780
2812
|
|
|
1781
2813
|
local review_summary=""
|
|
1782
2814
|
if [[ -s "$review_file" ]]; then
|
|
1783
|
-
local total_issues
|
|
1784
|
-
|
|
1785
|
-
|
|
2815
|
+
local total_issues=0
|
|
2816
|
+
# Try JSON structured output first
|
|
2817
|
+
if head -1 "$review_file" 2>/dev/null | grep -q '^{' 2>/dev/null; then
|
|
2818
|
+
total_issues=$(jq -r '.issues | length' "$review_file" 2>/dev/null || echo "0")
|
|
2819
|
+
fi
|
|
2820
|
+
# Grep fallback for markdown
|
|
2821
|
+
if [[ "${total_issues:-0}" -eq 0 ]]; then
|
|
2822
|
+
total_issues=$(grep -ciE '\*\*\[?(Critical|Bug|Security|Warning|Suggestion)\]?\*\*' "$review_file" 2>/dev/null || true)
|
|
2823
|
+
total_issues="${total_issues:-0}"
|
|
2824
|
+
fi
|
|
1786
2825
|
review_summary="**Code review:** $total_issues issues found"
|
|
1787
2826
|
fi
|
|
1788
2827
|
|
|
@@ -1815,6 +2854,8 @@ ${test_summary:-No test output}
|
|
|
1815
2854
|
\`\`\`
|
|
1816
2855
|
|
|
1817
2856
|
${review_summary}
|
|
2857
|
+
${simulation_summary}
|
|
2858
|
+
${arch_summary}
|
|
1818
2859
|
|
|
1819
2860
|
${closes_line}
|
|
1820
2861
|
|
|
@@ -1863,12 +2904,35 @@ EOF
|
|
|
1863
2904
|
info "Milestone: ${DIM}$ISSUE_MILESTONE${RESET}"
|
|
1864
2905
|
fi
|
|
1865
2906
|
|
|
1866
|
-
|
|
1867
|
-
local pr_url
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
2907
|
+
# Check for existing open PR on this branch to avoid duplicates (issue #12)
|
|
2908
|
+
local pr_url=""
|
|
2909
|
+
local existing_pr
|
|
2910
|
+
existing_pr=$(gh pr list --head "$GIT_BRANCH" --state open --json number,url --jq '.[0]' 2>/dev/null || echo "")
|
|
2911
|
+
if [[ -n "$existing_pr" && "$existing_pr" != "null" ]]; then
|
|
2912
|
+
local existing_pr_number existing_pr_url
|
|
2913
|
+
existing_pr_number=$(echo "$existing_pr" | jq -r '.number' 2>/dev/null || echo "")
|
|
2914
|
+
existing_pr_url=$(echo "$existing_pr" | jq -r '.url' 2>/dev/null || echo "")
|
|
2915
|
+
info "Updating existing PR #$existing_pr_number instead of creating duplicate"
|
|
2916
|
+
gh pr edit "$existing_pr_number" --title "$pr_title" --body "$pr_body" 2>/dev/null || true
|
|
2917
|
+
pr_url="$existing_pr_url"
|
|
2918
|
+
else
|
|
2919
|
+
info "Creating PR..."
|
|
2920
|
+
local pr_stderr pr_exit=0
|
|
2921
|
+
pr_url=$(gh pr create "${pr_args[@]}" 2>/tmp/shipwright-pr-stderr.txt) || pr_exit=$?
|
|
2922
|
+
pr_stderr=$(cat /tmp/shipwright-pr-stderr.txt 2>/dev/null || true)
|
|
2923
|
+
rm -f /tmp/shipwright-pr-stderr.txt
|
|
2924
|
+
|
|
2925
|
+
# gh pr create may return non-zero for reviewer issues but still create the PR
|
|
2926
|
+
if [[ "$pr_exit" -ne 0 ]]; then
|
|
2927
|
+
if [[ "$pr_url" == *"github.com"* ]]; then
|
|
2928
|
+
# PR was created but something non-fatal failed (e.g., reviewer not found)
|
|
2929
|
+
warn "PR created with warnings: ${pr_stderr:-unknown}"
|
|
2930
|
+
else
|
|
2931
|
+
error "PR creation failed: ${pr_stderr:-$pr_url}"
|
|
2932
|
+
return 1
|
|
2933
|
+
fi
|
|
2934
|
+
fi
|
|
2935
|
+
fi
|
|
1872
2936
|
|
|
1873
2937
|
success "PR created: ${BOLD}$pr_url${RESET}"
|
|
1874
2938
|
echo "$pr_url" > "$ARTIFACTS_DIR/pr-url.txt"
|
|
@@ -1876,6 +2940,54 @@ EOF
|
|
|
1876
2940
|
# Extract PR number
|
|
1877
2941
|
PR_NUMBER=$(echo "$pr_url" | grep -oE '[0-9]+$' || true)
|
|
1878
2942
|
|
|
2943
|
+
# ── Intelligent Reviewer Selection (GraphQL-enhanced) ──
|
|
2944
|
+
if [[ "${NO_GITHUB:-false}" != "true" && -n "$PR_NUMBER" && -z "$reviewers" ]]; then
|
|
2945
|
+
local reviewer_assigned=false
|
|
2946
|
+
|
|
2947
|
+
# Try CODEOWNERS-based routing via GraphQL API
|
|
2948
|
+
if type gh_codeowners &>/dev/null 2>&1 && [[ -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
|
|
2949
|
+
local codeowners_json
|
|
2950
|
+
codeowners_json=$(gh_codeowners "$REPO_OWNER" "$REPO_NAME" 2>/dev/null || echo "[]")
|
|
2951
|
+
if [[ "$codeowners_json" != "[]" && -n "$codeowners_json" ]]; then
|
|
2952
|
+
local changed_files
|
|
2953
|
+
changed_files=$(git diff --name-only "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
|
|
2954
|
+
if [[ -n "$changed_files" ]]; then
|
|
2955
|
+
local co_reviewers
|
|
2956
|
+
co_reviewers=$(echo "$codeowners_json" | jq -r '.[].owners[]' 2>/dev/null | sort -u | head -3 || true)
|
|
2957
|
+
if [[ -n "$co_reviewers" ]]; then
|
|
2958
|
+
local rev
|
|
2959
|
+
while IFS= read -r rev; do
|
|
2960
|
+
rev="${rev#@}"
|
|
2961
|
+
[[ -n "$rev" ]] && gh pr edit "$PR_NUMBER" --add-reviewer "$rev" 2>/dev/null || true
|
|
2962
|
+
done <<< "$co_reviewers"
|
|
2963
|
+
info "Requested review from CODEOWNERS: $(echo "$co_reviewers" | tr '\n' ',' | sed 's/,$//')"
|
|
2964
|
+
reviewer_assigned=true
|
|
2965
|
+
fi
|
|
2966
|
+
fi
|
|
2967
|
+
fi
|
|
2968
|
+
fi
|
|
2969
|
+
|
|
2970
|
+
# Fallback: contributor-based routing via GraphQL API
|
|
2971
|
+
if [[ "$reviewer_assigned" != "true" ]] && type gh_contributors &>/dev/null 2>&1 && [[ -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
|
|
2972
|
+
local contributors_json
|
|
2973
|
+
contributors_json=$(gh_contributors "$REPO_OWNER" "$REPO_NAME" 2>/dev/null || echo "[]")
|
|
2974
|
+
local top_contributor
|
|
2975
|
+
top_contributor=$(echo "$contributors_json" | jq -r '.[0].login // ""' 2>/dev/null || echo "")
|
|
2976
|
+
local current_user
|
|
2977
|
+
current_user=$(gh api user --jq '.login' 2>/dev/null || echo "")
|
|
2978
|
+
if [[ -n "$top_contributor" && "$top_contributor" != "$current_user" ]]; then
|
|
2979
|
+
gh pr edit "$PR_NUMBER" --add-reviewer "$top_contributor" 2>/dev/null || true
|
|
2980
|
+
info "Requested review from top contributor: $top_contributor"
|
|
2981
|
+
reviewer_assigned=true
|
|
2982
|
+
fi
|
|
2983
|
+
fi
|
|
2984
|
+
|
|
2985
|
+
# Final fallback: auto-approve if no reviewers assigned
|
|
2986
|
+
if [[ "$reviewer_assigned" != "true" ]]; then
|
|
2987
|
+
gh pr review "$PR_NUMBER" --approve 2>/dev/null || warn "Could not auto-approve PR"
|
|
2988
|
+
fi
|
|
2989
|
+
fi
|
|
2990
|
+
|
|
1879
2991
|
# Update issue with PR link
|
|
1880
2992
|
if [[ -n "$ISSUE_NUMBER" ]]; then
|
|
1881
2993
|
gh_remove_label "$ISSUE_NUMBER" "pipeline/in-progress"
|
|
@@ -1883,6 +2995,9 @@ EOF
|
|
|
1883
2995
|
gh_comment_issue "$ISSUE_NUMBER" "🎉 **PR created:** ${pr_url}
|
|
1884
2996
|
|
|
1885
2997
|
Pipeline duration so far: ${total_dur:-unknown}"
|
|
2998
|
+
|
|
2999
|
+
# Notify tracker of review/PR creation
|
|
3000
|
+
"$SCRIPT_DIR/sw-tracker.sh" notify "review" "$ISSUE_NUMBER" "$pr_url" 2>/dev/null || true
|
|
1886
3001
|
fi
|
|
1887
3002
|
|
|
1888
3003
|
# Wait for CI if configured
|
|
@@ -1904,11 +3019,69 @@ stage_merge() {
|
|
|
1904
3019
|
return 0
|
|
1905
3020
|
fi
|
|
1906
3021
|
|
|
3022
|
+
# ── Branch Protection Check ──
|
|
3023
|
+
if type gh_branch_protection &>/dev/null 2>&1 && [[ -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
|
|
3024
|
+
local protection_json
|
|
3025
|
+
protection_json=$(gh_branch_protection "$REPO_OWNER" "$REPO_NAME" "${BASE_BRANCH:-main}" 2>/dev/null || echo '{"protected": false}')
|
|
3026
|
+
local is_protected
|
|
3027
|
+
is_protected=$(echo "$protection_json" | jq -r '.protected // false' 2>/dev/null || echo "false")
|
|
3028
|
+
if [[ "$is_protected" == "true" ]]; then
|
|
3029
|
+
local required_reviews
|
|
3030
|
+
required_reviews=$(echo "$protection_json" | jq -r '.required_pull_request_reviews.required_approving_review_count // 0' 2>/dev/null || echo "0")
|
|
3031
|
+
local required_checks
|
|
3032
|
+
required_checks=$(echo "$protection_json" | jq -r '[.required_status_checks.contexts // [] | .[]] | length' 2>/dev/null || echo "0")
|
|
3033
|
+
|
|
3034
|
+
info "Branch protection: ${required_reviews} required review(s), ${required_checks} required check(s)"
|
|
3035
|
+
|
|
3036
|
+
if [[ "$required_reviews" -gt 0 ]]; then
|
|
3037
|
+
# Check if PR has enough approvals
|
|
3038
|
+
local prot_pr_number
|
|
3039
|
+
prot_pr_number=$(gh pr list --head "$GIT_BRANCH" --json number --jq '.[0].number' 2>/dev/null || echo "")
|
|
3040
|
+
if [[ -n "$prot_pr_number" ]]; then
|
|
3041
|
+
local approvals
|
|
3042
|
+
approvals=$(gh pr view "$prot_pr_number" --json reviews --jq '[.reviews[] | select(.state == "APPROVED")] | length' 2>/dev/null || echo "0")
|
|
3043
|
+
if [[ "$approvals" -lt "$required_reviews" ]]; then
|
|
3044
|
+
warn "PR has $approvals approval(s), needs $required_reviews — skipping auto-merge"
|
|
3045
|
+
info "PR is ready for manual merge after required reviews"
|
|
3046
|
+
emit_event "merge.blocked" "issue=${ISSUE_NUMBER:-0}" "reason=insufficient_reviews" "have=$approvals" "need=$required_reviews"
|
|
3047
|
+
return 0
|
|
3048
|
+
fi
|
|
3049
|
+
fi
|
|
3050
|
+
fi
|
|
3051
|
+
fi
|
|
3052
|
+
fi
|
|
3053
|
+
|
|
1907
3054
|
local merge_method wait_ci_timeout auto_delete_branch auto_merge auto_approve merge_strategy
|
|
1908
3055
|
merge_method=$(jq -r --arg id "merge" '(.stages[] | select(.id == $id) | .config.merge_method) // "squash"' "$PIPELINE_CONFIG" 2>/dev/null) || true
|
|
1909
3056
|
[[ -z "$merge_method" || "$merge_method" == "null" ]] && merge_method="squash"
|
|
1910
|
-
wait_ci_timeout=$(jq -r --arg id "merge" '(.stages[] | select(.id == $id) | .config.wait_ci_timeout_s) //
|
|
1911
|
-
[[ -z "$wait_ci_timeout" || "$wait_ci_timeout" == "null" ]] && wait_ci_timeout=
|
|
3057
|
+
wait_ci_timeout=$(jq -r --arg id "merge" '(.stages[] | select(.id == $id) | .config.wait_ci_timeout_s) // 0' "$PIPELINE_CONFIG" 2>/dev/null) || true
|
|
3058
|
+
[[ -z "$wait_ci_timeout" || "$wait_ci_timeout" == "null" ]] && wait_ci_timeout=0
|
|
3059
|
+
|
|
3060
|
+
# Adaptive CI timeout: 90th percentile of historical times × 1.5 safety margin
|
|
3061
|
+
if [[ "$wait_ci_timeout" -eq 0 ]] 2>/dev/null; then
|
|
3062
|
+
local repo_hash_ci
|
|
3063
|
+
repo_hash_ci=$(echo -n "$PROJECT_ROOT" | shasum -a 256 2>/dev/null | cut -c1-12 || echo "unknown")
|
|
3064
|
+
local ci_times_file="${HOME}/.shipwright/baselines/${repo_hash_ci}/ci-times.json"
|
|
3065
|
+
if [[ -f "$ci_times_file" ]]; then
|
|
3066
|
+
local p90_time
|
|
3067
|
+
p90_time=$(jq '
|
|
3068
|
+
.times | sort |
|
|
3069
|
+
(length * 0.9 | floor) as $idx |
|
|
3070
|
+
.[$idx] // 600
|
|
3071
|
+
' "$ci_times_file" 2>/dev/null || echo "0")
|
|
3072
|
+
if [[ -n "$p90_time" ]] && awk -v t="$p90_time" 'BEGIN{exit !(t > 0)}' 2>/dev/null; then
|
|
3073
|
+
# 1.5x safety margin, clamped to [120, 1800]
|
|
3074
|
+
wait_ci_timeout=$(awk -v p90="$p90_time" 'BEGIN{
|
|
3075
|
+
t = p90 * 1.5;
|
|
3076
|
+
if (t < 120) t = 120;
|
|
3077
|
+
if (t > 1800) t = 1800;
|
|
3078
|
+
printf "%d", t
|
|
3079
|
+
}')
|
|
3080
|
+
fi
|
|
3081
|
+
fi
|
|
3082
|
+
# Default fallback if no history
|
|
3083
|
+
[[ "$wait_ci_timeout" -eq 0 ]] && wait_ci_timeout=600
|
|
3084
|
+
fi
|
|
1912
3085
|
auto_delete_branch=$(jq -r --arg id "merge" '(.stages[] | select(.id == $id) | .config.auto_delete_branch) // "true"' "$PIPELINE_CONFIG" 2>/dev/null) || true
|
|
1913
3086
|
[[ -z "$auto_delete_branch" || "$auto_delete_branch" == "null" ]] && auto_delete_branch="true"
|
|
1914
3087
|
auto_merge=$(jq -r --arg id "merge" '(.stages[] | select(.id == $id) | .config.auto_merge) // false' "$PIPELINE_CONFIG" 2>/dev/null) || true
|
|
@@ -1958,6 +3131,26 @@ stage_merge() {
|
|
|
1958
3131
|
elapsed=$((elapsed + check_interval))
|
|
1959
3132
|
done
|
|
1960
3133
|
|
|
3134
|
+
# Record CI wait time for adaptive timeout calculation
|
|
3135
|
+
if [[ "$elapsed" -gt 0 ]]; then
|
|
3136
|
+
local repo_hash_ci_rec
|
|
3137
|
+
repo_hash_ci_rec=$(echo -n "$PROJECT_ROOT" | shasum -a 256 2>/dev/null | cut -c1-12 || echo "unknown")
|
|
3138
|
+
local ci_times_dir="${HOME}/.shipwright/baselines/${repo_hash_ci_rec}"
|
|
3139
|
+
local ci_times_rec_file="${ci_times_dir}/ci-times.json"
|
|
3140
|
+
mkdir -p "$ci_times_dir"
|
|
3141
|
+
local ci_history="[]"
|
|
3142
|
+
if [[ -f "$ci_times_rec_file" ]]; then
|
|
3143
|
+
ci_history=$(jq '.times // []' "$ci_times_rec_file" 2>/dev/null || echo "[]")
|
|
3144
|
+
fi
|
|
3145
|
+
local updated_ci
|
|
3146
|
+
updated_ci=$(echo "$ci_history" | jq --arg t "$elapsed" '. + [($t | tonumber)] | .[-20:]' 2>/dev/null || echo "[$elapsed]")
|
|
3147
|
+
local tmp_ci
|
|
3148
|
+
tmp_ci=$(mktemp "${ci_times_dir}/ci-times.json.XXXXXX")
|
|
3149
|
+
jq -n --argjson times "$updated_ci" --arg updated "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
3150
|
+
'{times: $times, updated: $updated}' > "$tmp_ci" 2>/dev/null
|
|
3151
|
+
mv "$tmp_ci" "$ci_times_rec_file" 2>/dev/null || true
|
|
3152
|
+
fi
|
|
3153
|
+
|
|
1961
3154
|
if [[ "$elapsed" -ge "$wait_ci_timeout" ]]; then
|
|
1962
3155
|
warn "CI check timeout (${wait_ci_timeout}s) — proceeding with merge anyway"
|
|
1963
3156
|
fi
|
|
@@ -2026,6 +3219,16 @@ stage_deploy() {
|
|
|
2026
3219
|
return 0
|
|
2027
3220
|
fi
|
|
2028
3221
|
|
|
3222
|
+
# Create GitHub deployment tracking
|
|
3223
|
+
local gh_deploy_env="production"
|
|
3224
|
+
[[ -n "$staging_cmd" && -z "$prod_cmd" ]] && gh_deploy_env="staging"
|
|
3225
|
+
if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_deploy_pipeline_start &>/dev/null 2>&1; then
|
|
3226
|
+
if [[ -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
|
|
3227
|
+
gh_deploy_pipeline_start "$REPO_OWNER" "$REPO_NAME" "${GIT_BRANCH:-HEAD}" "$gh_deploy_env" 2>/dev/null || true
|
|
3228
|
+
info "GitHub Deployment: tracking as $gh_deploy_env"
|
|
3229
|
+
fi
|
|
3230
|
+
fi
|
|
3231
|
+
|
|
2029
3232
|
# Post deploy start to GitHub
|
|
2030
3233
|
if [[ -n "$ISSUE_NUMBER" ]]; then
|
|
2031
3234
|
gh_comment_issue "$ISSUE_NUMBER" "🚀 **Deploy started**"
|
|
@@ -2033,9 +3236,13 @@ stage_deploy() {
|
|
|
2033
3236
|
|
|
2034
3237
|
if [[ -n "$staging_cmd" ]]; then
|
|
2035
3238
|
info "Deploying to staging..."
|
|
2036
|
-
|
|
3239
|
+
bash -c "$staging_cmd" > "$ARTIFACTS_DIR/deploy-staging.log" 2>&1 || {
|
|
2037
3240
|
error "Staging deploy failed"
|
|
2038
3241
|
[[ -n "$ISSUE_NUMBER" ]] && gh_comment_issue "$ISSUE_NUMBER" "❌ Staging deploy failed"
|
|
3242
|
+
# Mark GitHub deployment as failed
|
|
3243
|
+
if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_deploy_pipeline_complete &>/dev/null 2>&1; then
|
|
3244
|
+
gh_deploy_pipeline_complete "$REPO_OWNER" "$REPO_NAME" "$gh_deploy_env" false "Staging deploy failed" 2>/dev/null || true
|
|
3245
|
+
fi
|
|
2039
3246
|
return 1
|
|
2040
3247
|
}
|
|
2041
3248
|
success "Staging deploy complete"
|
|
@@ -2043,13 +3250,17 @@ stage_deploy() {
|
|
|
2043
3250
|
|
|
2044
3251
|
if [[ -n "$prod_cmd" ]]; then
|
|
2045
3252
|
info "Deploying to production..."
|
|
2046
|
-
|
|
3253
|
+
bash -c "$prod_cmd" > "$ARTIFACTS_DIR/deploy-prod.log" 2>&1 || {
|
|
2047
3254
|
error "Production deploy failed"
|
|
2048
3255
|
if [[ -n "$rollback_cmd" ]]; then
|
|
2049
3256
|
warn "Rolling back..."
|
|
2050
|
-
|
|
3257
|
+
bash -c "$rollback_cmd" 2>&1 || error "Rollback also failed!"
|
|
2051
3258
|
fi
|
|
2052
3259
|
[[ -n "$ISSUE_NUMBER" ]] && gh_comment_issue "$ISSUE_NUMBER" "❌ Production deploy failed — rollback ${rollback_cmd:+attempted}"
|
|
3260
|
+
# Mark GitHub deployment as failed
|
|
3261
|
+
if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_deploy_pipeline_complete &>/dev/null 2>&1; then
|
|
3262
|
+
gh_deploy_pipeline_complete "$REPO_OWNER" "$REPO_NAME" "$gh_deploy_env" false "Production deploy failed" 2>/dev/null || true
|
|
3263
|
+
fi
|
|
2053
3264
|
return 1
|
|
2054
3265
|
}
|
|
2055
3266
|
success "Production deploy complete"
|
|
@@ -2060,6 +3271,13 @@ stage_deploy() {
|
|
|
2060
3271
|
gh_add_labels "$ISSUE_NUMBER" "deployed"
|
|
2061
3272
|
fi
|
|
2062
3273
|
|
|
3274
|
+
# Mark GitHub deployment as successful
|
|
3275
|
+
if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_deploy_pipeline_complete &>/dev/null 2>&1; then
|
|
3276
|
+
if [[ -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
|
|
3277
|
+
gh_deploy_pipeline_complete "$REPO_OWNER" "$REPO_NAME" "$gh_deploy_env" true "" 2>/dev/null || true
|
|
3278
|
+
fi
|
|
3279
|
+
fi
|
|
3280
|
+
|
|
2063
3281
|
log_stage "deploy" "Deploy complete"
|
|
2064
3282
|
}
|
|
2065
3283
|
|
|
@@ -2079,7 +3297,7 @@ stage_validate() {
|
|
|
2079
3297
|
# Smoke tests
|
|
2080
3298
|
if [[ -n "$smoke_cmd" ]]; then
|
|
2081
3299
|
info "Running smoke tests..."
|
|
2082
|
-
|
|
3300
|
+
bash -c "$smoke_cmd" > "$ARTIFACTS_DIR/smoke.log" 2>&1 || {
|
|
2083
3301
|
error "Smoke tests failed"
|
|
2084
3302
|
if [[ -n "$ISSUE_NUMBER" ]]; then
|
|
2085
3303
|
gh issue create --title "Deploy validation failed: $GOAL" \
|
|
@@ -2171,6 +3389,24 @@ stage_monitor() {
|
|
|
2171
3389
|
[[ "$health_url" == "null" ]] && health_url=""
|
|
2172
3390
|
error_threshold=$(jq -r --arg id "monitor" '(.stages[] | select(.id == $id) | .config.error_threshold) // 5' "$PIPELINE_CONFIG" 2>/dev/null) || true
|
|
2173
3391
|
[[ -z "$error_threshold" || "$error_threshold" == "null" ]] && error_threshold=5
|
|
3392
|
+
|
|
3393
|
+
# Adaptive monitor: use historical baselines if available
|
|
3394
|
+
local repo_hash
|
|
3395
|
+
repo_hash=$(echo "${PROJECT_ROOT:-$(pwd)}" | cksum | awk '{print $1}')
|
|
3396
|
+
local baseline_file="${HOME}/.shipwright/baselines/${repo_hash}/deploy-monitor.json"
|
|
3397
|
+
if [[ -f "$baseline_file" ]]; then
|
|
3398
|
+
local hist_duration hist_threshold
|
|
3399
|
+
hist_duration=$(jq -r '.p90_stabilization_minutes // empty' "$baseline_file" 2>/dev/null || true)
|
|
3400
|
+
hist_threshold=$(jq -r '.p90_error_threshold // empty' "$baseline_file" 2>/dev/null || true)
|
|
3401
|
+
if [[ -n "$hist_duration" && "$hist_duration" != "null" ]]; then
|
|
3402
|
+
duration_minutes="$hist_duration"
|
|
3403
|
+
info "Monitor duration: ${duration_minutes}m ${DIM}(from baseline)${RESET}"
|
|
3404
|
+
fi
|
|
3405
|
+
if [[ -n "$hist_threshold" && "$hist_threshold" != "null" ]]; then
|
|
3406
|
+
error_threshold="$hist_threshold"
|
|
3407
|
+
info "Error threshold: ${error_threshold} ${DIM}(from baseline)${RESET}"
|
|
3408
|
+
fi
|
|
3409
|
+
fi
|
|
2174
3410
|
log_pattern=$(jq -r --arg id "monitor" '(.stages[] | select(.id == $id) | .config.log_pattern) // "ERROR|FATAL|PANIC"' "$PIPELINE_CONFIG" 2>/dev/null) || true
|
|
2175
3411
|
[[ -z "$log_pattern" || "$log_pattern" == "null" ]] && log_pattern="ERROR|FATAL|PANIC"
|
|
2176
3412
|
log_cmd=$(jq -r --arg id "monitor" '(.stages[] | select(.id == $id) | .config.log_cmd) // ""' "$PIPELINE_CONFIG" 2>/dev/null) || true
|
|
@@ -2237,7 +3473,7 @@ stage_monitor() {
|
|
|
2237
3473
|
# Log command check
|
|
2238
3474
|
if [[ -n "$log_cmd" ]]; then
|
|
2239
3475
|
local log_output
|
|
2240
|
-
log_output=$(
|
|
3476
|
+
log_output=$(bash -c "$log_cmd" 2>/dev/null || true)
|
|
2241
3477
|
local error_count=0
|
|
2242
3478
|
if [[ -n "$log_output" ]]; then
|
|
2243
3479
|
error_count=$(echo "$log_output" | grep -cE "$log_pattern" 2>/dev/null || true)
|
|
@@ -2278,7 +3514,7 @@ stage_monitor() {
|
|
|
2278
3514
|
echo "" >> "$report_file"
|
|
2279
3515
|
echo "## Rollback" >> "$report_file"
|
|
2280
3516
|
|
|
2281
|
-
if
|
|
3517
|
+
if bash -c "$rollback_cmd" >> "$report_file" 2>&1; then
|
|
2282
3518
|
success "Rollback executed"
|
|
2283
3519
|
echo "Rollback: ✅ success" >> "$report_file"
|
|
2284
3520
|
|
|
@@ -2289,7 +3525,7 @@ stage_monitor() {
|
|
|
2289
3525
|
|
|
2290
3526
|
if [[ -n "$smoke_cmd" ]]; then
|
|
2291
3527
|
info "Verifying rollback with smoke tests..."
|
|
2292
|
-
if
|
|
3528
|
+
if bash -c "$smoke_cmd" > "$ARTIFACTS_DIR/rollback-smoke.log" 2>&1; then
|
|
2293
3529
|
success "Rollback verified — smoke tests pass"
|
|
2294
3530
|
echo "Rollback verification: ✅ smoke tests pass" >> "$report_file"
|
|
2295
3531
|
emit_event "monitor.rollback_verified" \
|
|
@@ -2369,6 +3605,30 @@ _Created automatically by \`shipwright pipeline\` monitor stage_" 2>/dev/null ||
|
|
|
2369
3605
|
fi
|
|
2370
3606
|
|
|
2371
3607
|
log_stage "monitor" "Clean — ${total_errors} errors in ${duration_minutes}m"
|
|
3608
|
+
|
|
3609
|
+
# Record baseline for adaptive monitoring on future runs
|
|
3610
|
+
local baseline_dir="${HOME}/.shipwright/baselines/${repo_hash}"
|
|
3611
|
+
mkdir -p "$baseline_dir" 2>/dev/null || true
|
|
3612
|
+
local baseline_tmp
|
|
3613
|
+
baseline_tmp="$(mktemp)"
|
|
3614
|
+
if [[ -f "${baseline_dir}/deploy-monitor.json" ]]; then
|
|
3615
|
+
# Append to history and recalculate p90
|
|
3616
|
+
jq --arg dur "$duration_minutes" --arg errs "$total_errors" \
|
|
3617
|
+
'.history += [{"duration_minutes": ($dur | tonumber), "errors": ($errs | tonumber)}] |
|
|
3618
|
+
.p90_stabilization_minutes = ([.history[].duration_minutes] | sort | .[length * 9 / 10 | floor]) |
|
|
3619
|
+
.p90_error_threshold = (([.history[].errors] | sort | .[length * 9 / 10 | floor]) + 2) |
|
|
3620
|
+
.updated_at = now' \
|
|
3621
|
+
"${baseline_dir}/deploy-monitor.json" > "$baseline_tmp" 2>/dev/null && \
|
|
3622
|
+
mv "$baseline_tmp" "${baseline_dir}/deploy-monitor.json" || rm -f "$baseline_tmp"
|
|
3623
|
+
else
|
|
3624
|
+
jq -n --arg dur "$duration_minutes" --arg errs "$total_errors" \
|
|
3625
|
+
'{history: [{"duration_minutes": ($dur | tonumber), "errors": ($errs | tonumber)}],
|
|
3626
|
+
p90_stabilization_minutes: ($dur | tonumber),
|
|
3627
|
+
p90_error_threshold: (($errs | tonumber) + 2),
|
|
3628
|
+
updated_at: now}' \
|
|
3629
|
+
> "$baseline_tmp" 2>/dev/null && \
|
|
3630
|
+
mv "$baseline_tmp" "${baseline_dir}/deploy-monitor.json" || rm -f "$baseline_tmp"
|
|
3631
|
+
fi
|
|
2372
3632
|
}
|
|
2373
3633
|
|
|
2374
3634
|
# ─── Multi-Dimensional Quality Checks ─────────────────────────────────────
|
|
@@ -2427,13 +3687,33 @@ quality_check_bundle_size() {
|
|
|
2427
3687
|
local bundle_size=0
|
|
2428
3688
|
local bundle_dir=""
|
|
2429
3689
|
|
|
2430
|
-
# Find build output directory
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
3690
|
+
# Find build output directory — check config files first, then common dirs
|
|
3691
|
+
# Parse tsconfig.json outDir
|
|
3692
|
+
if [[ -z "$bundle_dir" && -f "tsconfig.json" ]]; then
|
|
3693
|
+
local ts_out
|
|
3694
|
+
ts_out=$(jq -r '.compilerOptions.outDir // empty' tsconfig.json 2>/dev/null || true)
|
|
3695
|
+
[[ -n "$ts_out" && -d "$ts_out" ]] && bundle_dir="$ts_out"
|
|
3696
|
+
fi
|
|
3697
|
+
# Parse package.json build script for output hints
|
|
3698
|
+
if [[ -z "$bundle_dir" && -f "package.json" ]]; then
|
|
3699
|
+
local build_script
|
|
3700
|
+
build_script=$(jq -r '.scripts.build // ""' package.json 2>/dev/null || true)
|
|
3701
|
+
if [[ -n "$build_script" ]]; then
|
|
3702
|
+
# Check for common output flags: --outDir, -o, --out-dir
|
|
3703
|
+
local parsed_out
|
|
3704
|
+
parsed_out=$(echo "$build_script" | grep -oE '(--outDir|--out-dir|-o)\s+[^ ]+' 2>/dev/null | awk '{print $NF}' | head -1 || true)
|
|
3705
|
+
[[ -n "$parsed_out" && -d "$parsed_out" ]] && bundle_dir="$parsed_out"
|
|
2435
3706
|
fi
|
|
2436
|
-
|
|
3707
|
+
fi
|
|
3708
|
+
# Fallback: check common directories
|
|
3709
|
+
if [[ -z "$bundle_dir" ]]; then
|
|
3710
|
+
for dir in dist build out .next target; do
|
|
3711
|
+
if [[ -d "$dir" ]]; then
|
|
3712
|
+
bundle_dir="$dir"
|
|
3713
|
+
break
|
|
3714
|
+
fi
|
|
3715
|
+
done
|
|
3716
|
+
fi
|
|
2437
3717
|
|
|
2438
3718
|
if [[ -z "$bundle_dir" ]]; then
|
|
2439
3719
|
info "No build output directory found — skipping bundle check"
|
|
@@ -2453,23 +3733,106 @@ quality_check_bundle_size() {
|
|
|
2453
3733
|
"size_kb=$bundle_size" \
|
|
2454
3734
|
"directory=$bundle_dir"
|
|
2455
3735
|
|
|
2456
|
-
#
|
|
2457
|
-
local
|
|
2458
|
-
|
|
2459
|
-
|
|
3736
|
+
# Adaptive bundle size check: statistical deviation from historical mean
|
|
3737
|
+
local repo_hash_bundle
|
|
3738
|
+
repo_hash_bundle=$(echo -n "$PROJECT_ROOT" | shasum -a 256 2>/dev/null | cut -c1-12 || echo "unknown")
|
|
3739
|
+
local bundle_baselines_dir="${HOME}/.shipwright/baselines/${repo_hash_bundle}"
|
|
3740
|
+
local bundle_history_file="${bundle_baselines_dir}/bundle-history.json"
|
|
3741
|
+
|
|
3742
|
+
local bundle_history="[]"
|
|
3743
|
+
if [[ -f "$bundle_history_file" ]]; then
|
|
3744
|
+
bundle_history=$(jq '.sizes // []' "$bundle_history_file" 2>/dev/null || echo "[]")
|
|
3745
|
+
fi
|
|
3746
|
+
|
|
3747
|
+
local bundle_hist_count
|
|
3748
|
+
bundle_hist_count=$(echo "$bundle_history" | jq 'length' 2>/dev/null || echo "0")
|
|
3749
|
+
|
|
3750
|
+
if [[ "$bundle_hist_count" -ge 3 ]]; then
|
|
3751
|
+
# Statistical check: alert on growth > 2σ from historical mean
|
|
3752
|
+
local mean_size stddev_size
|
|
3753
|
+
mean_size=$(echo "$bundle_history" | jq 'add / length' 2>/dev/null || echo "0")
|
|
3754
|
+
stddev_size=$(echo "$bundle_history" | jq '
|
|
3755
|
+
(add / length) as $mean |
|
|
3756
|
+
(map(. - $mean | . * .) | add / length | sqrt)
|
|
3757
|
+
' 2>/dev/null || echo "0")
|
|
3758
|
+
|
|
3759
|
+
# Adaptive tolerance: small repos (<1MB mean) get wider tolerance (3σ), large repos get 2σ
|
|
3760
|
+
local sigma_mult
|
|
3761
|
+
sigma_mult=$(awk -v mean="$mean_size" 'BEGIN{ print (mean < 1024 ? 3 : 2) }')
|
|
3762
|
+
local adaptive_max
|
|
3763
|
+
adaptive_max=$(awk -v mean="$mean_size" -v sd="$stddev_size" -v mult="$sigma_mult" \
|
|
3764
|
+
'BEGIN{ t = mean + mult*sd; min_t = mean * 1.1; printf "%.0f", (t > min_t ? t : min_t) }')
|
|
3765
|
+
|
|
3766
|
+
echo "History: ${bundle_hist_count} runs | Mean: ${mean_size}KB | StdDev: ${stddev_size}KB | Max: ${adaptive_max}KB (${sigma_mult}σ)" >> "$metrics_log"
|
|
3767
|
+
|
|
3768
|
+
if [[ "$bundle_size" -gt "$adaptive_max" ]] 2>/dev/null; then
|
|
3769
|
+
local growth_pct
|
|
3770
|
+
growth_pct=$(awk -v cur="$bundle_size" -v mean="$mean_size" 'BEGIN{printf "%d", ((cur - mean) / mean) * 100}')
|
|
3771
|
+
warn "Bundle size ${growth_pct}% above average (${mean_size}KB → ${bundle_size}KB, ${sigma_mult}σ threshold: ${adaptive_max}KB)"
|
|
3772
|
+
return 1
|
|
3773
|
+
fi
|
|
3774
|
+
else
|
|
3775
|
+
# Fallback: legacy memory baseline with hardcoded 20% (not enough history)
|
|
3776
|
+
local baseline_size=""
|
|
3777
|
+
if [[ -x "$SCRIPT_DIR/sw-memory.sh" ]]; then
|
|
3778
|
+
baseline_size=$(bash "$SCRIPT_DIR/sw-memory.sh" get "bundle_size_kb" 2>/dev/null) || true
|
|
3779
|
+
fi
|
|
3780
|
+
if [[ -n "$baseline_size" && "$baseline_size" -gt 0 ]] 2>/dev/null; then
|
|
3781
|
+
local growth_pct
|
|
3782
|
+
growth_pct=$(awk -v cur="$bundle_size" -v base="$baseline_size" 'BEGIN{printf "%d", ((cur - base) / base) * 100}')
|
|
3783
|
+
echo "Baseline: ${baseline_size}KB | Growth: ${growth_pct}%" >> "$metrics_log"
|
|
3784
|
+
if [[ "$growth_pct" -gt 20 ]]; then
|
|
3785
|
+
warn "Bundle size grew ${growth_pct}% (${baseline_size}KB → ${bundle_size}KB)"
|
|
3786
|
+
return 1
|
|
3787
|
+
fi
|
|
3788
|
+
fi
|
|
2460
3789
|
fi
|
|
2461
3790
|
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
3791
|
+
# Append current size to rolling history (keep last 10)
|
|
3792
|
+
mkdir -p "$bundle_baselines_dir"
|
|
3793
|
+
local updated_bundle_hist
|
|
3794
|
+
updated_bundle_hist=$(echo "$bundle_history" | jq --arg sz "$bundle_size" '
|
|
3795
|
+
. + [($sz | tonumber)] | .[-10:]
|
|
3796
|
+
' 2>/dev/null || echo "[$bundle_size]")
|
|
3797
|
+
local tmp_bundle_hist
|
|
3798
|
+
tmp_bundle_hist=$(mktemp "${bundle_baselines_dir}/bundle-history.json.XXXXXX")
|
|
3799
|
+
jq -n --argjson sizes "$updated_bundle_hist" --arg updated "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
3800
|
+
'{sizes: $sizes, updated: $updated}' > "$tmp_bundle_hist" 2>/dev/null
|
|
3801
|
+
mv "$tmp_bundle_hist" "$bundle_history_file" 2>/dev/null || true
|
|
3802
|
+
|
|
3803
|
+
# Intelligence: identify top dependency bloaters
|
|
3804
|
+
if type intelligence_search_memory &>/dev/null 2>&1 && [[ -f "package.json" ]] && command -v jq &>/dev/null; then
|
|
3805
|
+
local dep_sizes=""
|
|
3806
|
+
local deps
|
|
3807
|
+
deps=$(jq -r '.dependencies // {} | keys[]' package.json 2>/dev/null || true)
|
|
3808
|
+
if [[ -n "$deps" ]]; then
|
|
3809
|
+
while IFS= read -r dep; do
|
|
3810
|
+
[[ -z "$dep" ]] && continue
|
|
3811
|
+
local dep_dir="node_modules/${dep}"
|
|
3812
|
+
if [[ -d "$dep_dir" ]]; then
|
|
3813
|
+
local dep_size
|
|
3814
|
+
dep_size=$(du -sk "$dep_dir" 2>/dev/null | cut -f1 || echo "0")
|
|
3815
|
+
dep_sizes="${dep_sizes}${dep_size} ${dep}
|
|
3816
|
+
"
|
|
3817
|
+
fi
|
|
3818
|
+
done <<< "$deps"
|
|
3819
|
+
if [[ -n "$dep_sizes" ]]; then
|
|
3820
|
+
local top_bloaters
|
|
3821
|
+
top_bloaters=$(echo "$dep_sizes" | sort -rn | head -3)
|
|
3822
|
+
if [[ -n "$top_bloaters" ]]; then
|
|
3823
|
+
echo "" >> "$metrics_log"
|
|
3824
|
+
echo "Top 3 dependency sizes:" >> "$metrics_log"
|
|
3825
|
+
echo "$top_bloaters" | while IFS=' ' read -r sz nm; do
|
|
3826
|
+
[[ -z "$nm" ]] && continue
|
|
3827
|
+
echo " ${nm}: ${sz}KB" >> "$metrics_log"
|
|
3828
|
+
done
|
|
3829
|
+
info "Top bloaters: $(echo "$top_bloaters" | head -1 | awk '{print $2 ": " $1 "KB"}')"
|
|
3830
|
+
fi
|
|
3831
|
+
fi
|
|
2469
3832
|
fi
|
|
2470
3833
|
fi
|
|
2471
3834
|
|
|
2472
|
-
info "Bundle size: ${bundle_size_human}"
|
|
3835
|
+
info "Bundle size: ${bundle_size_human}${bundle_hist_count:+ (${bundle_hist_count} historical samples)}"
|
|
2473
3836
|
return 0
|
|
2474
3837
|
}
|
|
2475
3838
|
|
|
@@ -2484,11 +3847,39 @@ quality_check_perf_regression() {
|
|
|
2484
3847
|
return 0
|
|
2485
3848
|
fi
|
|
2486
3849
|
|
|
2487
|
-
# Extract test suite duration
|
|
3850
|
+
# Extract test suite duration — multi-framework patterns
|
|
2488
3851
|
local duration_ms=""
|
|
3852
|
+
# Jest/Vitest: "Time: 12.34 s" or "Duration 12.34s"
|
|
2489
3853
|
duration_ms=$(grep -oE 'Time:\s*[0-9.]+\s*s' "$test_log" 2>/dev/null | grep -oE '[0-9.]+' | tail -1 || true)
|
|
3854
|
+
[[ -z "$duration_ms" ]] && duration_ms=$(grep -oE 'Duration\s+[0-9.]+\s*s' "$test_log" 2>/dev/null | grep -oE '[0-9.]+' | tail -1 || true)
|
|
3855
|
+
# pytest: "passed in 12.34s" or "====== 5 passed in 12.34 seconds ======"
|
|
3856
|
+
[[ -z "$duration_ms" ]] && duration_ms=$(grep -oE 'passed in [0-9.]+s' "$test_log" 2>/dev/null | grep -oE '[0-9.]+' | tail -1 || true)
|
|
3857
|
+
# Go test: "ok pkg 12.345s"
|
|
3858
|
+
[[ -z "$duration_ms" ]] && duration_ms=$(grep -oE '^ok\s+\S+\s+[0-9.]+s' "$test_log" 2>/dev/null | grep -oE '[0-9.]+s' | grep -oE '[0-9.]+' | tail -1 || true)
|
|
3859
|
+
# Cargo test: "test result: ok. ... finished in 12.34s"
|
|
3860
|
+
[[ -z "$duration_ms" ]] && duration_ms=$(grep -oE 'finished in [0-9.]+s' "$test_log" 2>/dev/null | grep -oE '[0-9.]+' | tail -1 || true)
|
|
3861
|
+
# Generic: "12.34 seconds" or "12.34s"
|
|
2490
3862
|
[[ -z "$duration_ms" ]] && duration_ms=$(grep -oE '[0-9.]+ ?s(econds?)?' "$test_log" 2>/dev/null | grep -oE '[0-9.]+' | tail -1 || true)
|
|
2491
3863
|
|
|
3864
|
+
# Claude fallback: parse test output when no pattern matches
|
|
3865
|
+
if [[ -z "$duration_ms" ]]; then
|
|
3866
|
+
local intel_enabled="false"
|
|
3867
|
+
local daemon_cfg="${PROJECT_ROOT}/.claude/daemon-config.json"
|
|
3868
|
+
if [[ -f "$daemon_cfg" ]]; then
|
|
3869
|
+
intel_enabled=$(jq -r '.intelligence.enabled // false' "$daemon_cfg" 2>/dev/null || echo "false")
|
|
3870
|
+
fi
|
|
3871
|
+
if [[ "$intel_enabled" == "true" ]] && command -v claude &>/dev/null; then
|
|
3872
|
+
local tail_output
|
|
3873
|
+
tail_output=$(tail -30 "$test_log" 2>/dev/null || true)
|
|
3874
|
+
if [[ -n "$tail_output" ]]; then
|
|
3875
|
+
duration_ms=$(claude --print -p "Extract ONLY the total test suite duration in seconds from this output. Reply with ONLY a number (e.g. 12.34). If no duration found, reply NONE.
|
|
3876
|
+
|
|
3877
|
+
$tail_output" < /dev/null 2>/dev/null | grep -oE '^[0-9.]+$' | head -1 || true)
|
|
3878
|
+
[[ "$duration_ms" == "NONE" ]] && duration_ms=""
|
|
3879
|
+
fi
|
|
3880
|
+
fi
|
|
3881
|
+
fi
|
|
3882
|
+
|
|
2492
3883
|
if [[ -z "$duration_ms" ]]; then
|
|
2493
3884
|
info "Could not extract test duration — skipping perf check"
|
|
2494
3885
|
echo "Duration not parseable" > "$metrics_log"
|
|
@@ -2501,23 +3892,73 @@ quality_check_perf_regression() {
|
|
|
2501
3892
|
"issue=${ISSUE_NUMBER:-0}" \
|
|
2502
3893
|
"duration_s=$duration_ms"
|
|
2503
3894
|
|
|
2504
|
-
#
|
|
2505
|
-
local
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
3895
|
+
# Adaptive performance check: 2σ from rolling 10-run average
|
|
3896
|
+
local repo_hash_perf
|
|
3897
|
+
repo_hash_perf=$(echo -n "$PROJECT_ROOT" | shasum -a 256 2>/dev/null | cut -c1-12 || echo "unknown")
|
|
3898
|
+
local perf_baselines_dir="${HOME}/.shipwright/baselines/${repo_hash_perf}"
|
|
3899
|
+
local perf_history_file="${perf_baselines_dir}/perf-history.json"
|
|
3900
|
+
|
|
3901
|
+
# Read historical durations (rolling window of last 10 runs)
|
|
3902
|
+
local history_json="[]"
|
|
3903
|
+
if [[ -f "$perf_history_file" ]]; then
|
|
3904
|
+
history_json=$(jq '.durations // []' "$perf_history_file" 2>/dev/null || echo "[]")
|
|
3905
|
+
fi
|
|
3906
|
+
|
|
3907
|
+
local history_count
|
|
3908
|
+
history_count=$(echo "$history_json" | jq 'length' 2>/dev/null || echo "0")
|
|
3909
|
+
|
|
3910
|
+
if [[ "$history_count" -ge 3 ]]; then
|
|
3911
|
+
# Calculate mean and standard deviation from history
|
|
3912
|
+
local mean_dur stddev_dur
|
|
3913
|
+
mean_dur=$(echo "$history_json" | jq 'add / length' 2>/dev/null || echo "0")
|
|
3914
|
+
stddev_dur=$(echo "$history_json" | jq '
|
|
3915
|
+
(add / length) as $mean |
|
|
3916
|
+
(map(. - $mean | . * .) | add / length | sqrt)
|
|
3917
|
+
' 2>/dev/null || echo "0")
|
|
3918
|
+
|
|
3919
|
+
# Threshold: mean + 2σ (but at least 10% above mean)
|
|
3920
|
+
local adaptive_threshold
|
|
3921
|
+
adaptive_threshold=$(awk -v mean="$mean_dur" -v sd="$stddev_dur" \
|
|
3922
|
+
'BEGIN{ t = mean + 2*sd; min_t = mean * 1.1; printf "%.2f", (t > min_t ? t : min_t) }')
|
|
3923
|
+
|
|
3924
|
+
echo "History: ${history_count} runs | Mean: ${mean_dur}s | StdDev: ${stddev_dur}s | Threshold: ${adaptive_threshold}s" >> "$metrics_log"
|
|
3925
|
+
|
|
3926
|
+
if awk -v cur="$duration_ms" -v thresh="$adaptive_threshold" 'BEGIN{exit !(cur > thresh)}' 2>/dev/null; then
|
|
3927
|
+
local slowdown_pct
|
|
3928
|
+
slowdown_pct=$(awk -v cur="$duration_ms" -v mean="$mean_dur" 'BEGIN{printf "%d", ((cur - mean) / mean) * 100}')
|
|
3929
|
+
warn "Tests ${slowdown_pct}% slower than rolling average (${mean_dur}s → ${duration_ms}s, threshold: ${adaptive_threshold}s)"
|
|
2516
3930
|
return 1
|
|
2517
3931
|
fi
|
|
3932
|
+
else
|
|
3933
|
+
# Fallback: legacy memory baseline with hardcoded 30% (not enough history)
|
|
3934
|
+
local baseline_dur=""
|
|
3935
|
+
if [[ -x "$SCRIPT_DIR/sw-memory.sh" ]]; then
|
|
3936
|
+
baseline_dur=$(bash "$SCRIPT_DIR/sw-memory.sh" get "test_duration_s" 2>/dev/null) || true
|
|
3937
|
+
fi
|
|
3938
|
+
if [[ -n "$baseline_dur" ]] && awk -v cur="$duration_ms" -v base="$baseline_dur" 'BEGIN{exit !(base > 0)}' 2>/dev/null; then
|
|
3939
|
+
local slowdown_pct
|
|
3940
|
+
slowdown_pct=$(awk -v cur="$duration_ms" -v base="$baseline_dur" 'BEGIN{printf "%d", ((cur - base) / base) * 100}')
|
|
3941
|
+
echo "Baseline: ${baseline_dur}s | Slowdown: ${slowdown_pct}%" >> "$metrics_log"
|
|
3942
|
+
if [[ "$slowdown_pct" -gt 30 ]]; then
|
|
3943
|
+
warn "Tests ${slowdown_pct}% slower (${baseline_dur}s → ${duration_ms}s)"
|
|
3944
|
+
return 1
|
|
3945
|
+
fi
|
|
3946
|
+
fi
|
|
2518
3947
|
fi
|
|
2519
3948
|
|
|
2520
|
-
|
|
3949
|
+
# Append current duration to rolling history (keep last 10)
|
|
3950
|
+
mkdir -p "$perf_baselines_dir"
|
|
3951
|
+
local updated_history
|
|
3952
|
+
updated_history=$(echo "$history_json" | jq --arg dur "$duration_ms" '
|
|
3953
|
+
. + [($dur | tonumber)] | .[-10:]
|
|
3954
|
+
' 2>/dev/null || echo "[$duration_ms]")
|
|
3955
|
+
local tmp_perf_hist
|
|
3956
|
+
tmp_perf_hist=$(mktemp "${perf_baselines_dir}/perf-history.json.XXXXXX")
|
|
3957
|
+
jq -n --argjson durations "$updated_history" --arg updated "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
3958
|
+
'{durations: $durations, updated: $updated}' > "$tmp_perf_hist" 2>/dev/null
|
|
3959
|
+
mv "$tmp_perf_hist" "$perf_history_file" 2>/dev/null || true
|
|
3960
|
+
|
|
3961
|
+
info "Test duration: ${duration_ms}s${history_count:+ (${history_count} historical samples)}"
|
|
2521
3962
|
return 0
|
|
2522
3963
|
}
|
|
2523
3964
|
|
|
@@ -2525,7 +3966,7 @@ quality_check_api_compat() {
|
|
|
2525
3966
|
info "API compatibility check..."
|
|
2526
3967
|
local compat_log="$ARTIFACTS_DIR/api-compat.log"
|
|
2527
3968
|
|
|
2528
|
-
# Look for OpenAPI/Swagger specs
|
|
3969
|
+
# Look for OpenAPI/Swagger specs — search beyond hardcoded paths
|
|
2529
3970
|
local spec_file=""
|
|
2530
3971
|
for candidate in openapi.json openapi.yaml swagger.json swagger.yaml api/openapi.json docs/openapi.yaml; do
|
|
2531
3972
|
if [[ -f "$candidate" ]]; then
|
|
@@ -2533,6 +3974,10 @@ quality_check_api_compat() {
|
|
|
2533
3974
|
break
|
|
2534
3975
|
fi
|
|
2535
3976
|
done
|
|
3977
|
+
# Broader search if nothing found at common paths
|
|
3978
|
+
if [[ -z "$spec_file" ]]; then
|
|
3979
|
+
spec_file=$(find . -maxdepth 4 \( -name "openapi*.json" -o -name "openapi*.yaml" -o -name "openapi*.yml" -o -name "swagger*.json" -o -name "swagger*.yaml" -o -name "swagger*.yml" \) -type f 2>/dev/null | head -1 || true)
|
|
3980
|
+
fi
|
|
2536
3981
|
|
|
2537
3982
|
if [[ -z "$spec_file" ]]; then
|
|
2538
3983
|
info "No OpenAPI/Swagger spec found — skipping API compat check"
|
|
@@ -2571,21 +4016,64 @@ quality_check_api_compat() {
|
|
|
2571
4016
|
removed_endpoints=$(comm -23 <(echo "$old_paths") <(echo "$new_paths") 2>/dev/null || true)
|
|
2572
4017
|
fi
|
|
2573
4018
|
|
|
4019
|
+
# Enhanced schema diff: parameter changes, response schema, auth changes
|
|
4020
|
+
local param_changes="" schema_changes=""
|
|
4021
|
+
if command -v jq &>/dev/null && [[ "$spec_file" == *.json ]]; then
|
|
4022
|
+
# Detect parameter changes on existing endpoints
|
|
4023
|
+
local common_paths
|
|
4024
|
+
common_paths=$(comm -12 <(echo "$old_spec" | jq -r '.paths | keys[]' 2>/dev/null | sort) <(jq -r '.paths | keys[]' "$spec_file" 2>/dev/null | sort) 2>/dev/null || true)
|
|
4025
|
+
if [[ -n "$common_paths" ]]; then
|
|
4026
|
+
while IFS= read -r path; do
|
|
4027
|
+
[[ -z "$path" ]] && continue
|
|
4028
|
+
local old_params new_params
|
|
4029
|
+
old_params=$(echo "$old_spec" | jq -r --arg p "$path" '.paths[$p] | to_entries[] | .value.parameters // [] | .[].name' 2>/dev/null | sort || true)
|
|
4030
|
+
new_params=$(jq -r --arg p "$path" '.paths[$p] | to_entries[] | .value.parameters // [] | .[].name' "$spec_file" 2>/dev/null | sort || true)
|
|
4031
|
+
local removed_params
|
|
4032
|
+
removed_params=$(comm -23 <(echo "$old_params") <(echo "$new_params") 2>/dev/null || true)
|
|
4033
|
+
[[ -n "$removed_params" ]] && param_changes="${param_changes}${path}: removed params: ${removed_params}
|
|
4034
|
+
"
|
|
4035
|
+
done <<< "$common_paths"
|
|
4036
|
+
fi
|
|
4037
|
+
fi
|
|
4038
|
+
|
|
4039
|
+
# Intelligence: semantic API diff for complex changes
|
|
4040
|
+
local semantic_diff=""
|
|
4041
|
+
if type intelligence_search_memory &>/dev/null 2>&1 && command -v claude &>/dev/null; then
|
|
4042
|
+
local spec_git_diff
|
|
4043
|
+
spec_git_diff=$(git diff "${BASE_BRANCH}...HEAD" -- "$spec_file" 2>/dev/null | head -200 || true)
|
|
4044
|
+
if [[ -n "$spec_git_diff" ]]; then
|
|
4045
|
+
semantic_diff=$(claude --print --output-format text -p "Analyze this API spec diff for breaking changes. List: removed endpoints, changed parameters, altered response schemas, auth changes. Be concise.
|
|
4046
|
+
|
|
4047
|
+
${spec_git_diff}" --model haiku < /dev/null 2>/dev/null || true)
|
|
4048
|
+
fi
|
|
4049
|
+
fi
|
|
4050
|
+
|
|
2574
4051
|
{
|
|
2575
4052
|
echo "Spec: $spec_file"
|
|
2576
4053
|
echo "Changed: yes"
|
|
2577
4054
|
if [[ -n "$removed_endpoints" ]]; then
|
|
2578
4055
|
echo "BREAKING — Removed endpoints:"
|
|
2579
4056
|
echo "$removed_endpoints"
|
|
2580
|
-
|
|
4057
|
+
fi
|
|
4058
|
+
if [[ -n "$param_changes" ]]; then
|
|
4059
|
+
echo "BREAKING — Parameter changes:"
|
|
4060
|
+
echo "$param_changes"
|
|
4061
|
+
fi
|
|
4062
|
+
if [[ -n "$semantic_diff" ]]; then
|
|
4063
|
+
echo ""
|
|
4064
|
+
echo "Semantic analysis:"
|
|
4065
|
+
echo "$semantic_diff"
|
|
4066
|
+
fi
|
|
4067
|
+
if [[ -z "$removed_endpoints" && -z "$param_changes" ]]; then
|
|
2581
4068
|
echo "No breaking changes detected"
|
|
2582
4069
|
fi
|
|
2583
4070
|
} > "$compat_log"
|
|
2584
4071
|
|
|
2585
|
-
if [[ -n "$removed_endpoints" ]]; then
|
|
2586
|
-
local
|
|
2587
|
-
|
|
2588
|
-
|
|
4072
|
+
if [[ -n "$removed_endpoints" || -n "$param_changes" ]]; then
|
|
4073
|
+
local issue_count=0
|
|
4074
|
+
[[ -n "$removed_endpoints" ]] && issue_count=$((issue_count + $(echo "$removed_endpoints" | wc -l | xargs)))
|
|
4075
|
+
[[ -n "$param_changes" ]] && issue_count=$((issue_count + $(echo "$param_changes" | grep -c '.' || true)))
|
|
4076
|
+
warn "API breaking changes: ${issue_count} issue(s) found"
|
|
2589
4077
|
return 1
|
|
2590
4078
|
fi
|
|
2591
4079
|
|
|
@@ -2602,11 +4090,28 @@ quality_check_coverage() {
|
|
|
2602
4090
|
return 0
|
|
2603
4091
|
fi
|
|
2604
4092
|
|
|
2605
|
-
# Extract coverage percentage
|
|
4093
|
+
# Extract coverage percentage using shared parser
|
|
2606
4094
|
local coverage=""
|
|
2607
|
-
coverage=$(
|
|
2608
|
-
|
|
2609
|
-
|
|
4095
|
+
coverage=$(parse_coverage_from_output "$test_log")
|
|
4096
|
+
|
|
4097
|
+
# Claude fallback: parse test output when no pattern matches
|
|
4098
|
+
if [[ -z "$coverage" ]]; then
|
|
4099
|
+
local intel_enabled_cov="false"
|
|
4100
|
+
local daemon_cfg_cov="${PROJECT_ROOT}/.claude/daemon-config.json"
|
|
4101
|
+
if [[ -f "$daemon_cfg_cov" ]]; then
|
|
4102
|
+
intel_enabled_cov=$(jq -r '.intelligence.enabled // false' "$daemon_cfg_cov" 2>/dev/null || echo "false")
|
|
4103
|
+
fi
|
|
4104
|
+
if [[ "$intel_enabled_cov" == "true" ]] && command -v claude &>/dev/null; then
|
|
4105
|
+
local tail_cov_output
|
|
4106
|
+
tail_cov_output=$(tail -40 "$test_log" 2>/dev/null || true)
|
|
4107
|
+
if [[ -n "$tail_cov_output" ]]; then
|
|
4108
|
+
coverage=$(claude --print -p "Extract ONLY the overall code coverage percentage from this test output. Reply with ONLY a number (e.g. 85.5). If no coverage found, reply NONE.
|
|
4109
|
+
|
|
4110
|
+
$tail_cov_output" < /dev/null 2>/dev/null | grep -oE '^[0-9.]+$' | head -1 || true)
|
|
4111
|
+
[[ "$coverage" == "NONE" ]] && coverage=""
|
|
4112
|
+
fi
|
|
4113
|
+
fi
|
|
4114
|
+
fi
|
|
2610
4115
|
|
|
2611
4116
|
if [[ -z "$coverage" ]]; then
|
|
2612
4117
|
info "Could not extract coverage — skipping"
|
|
@@ -2622,16 +4127,30 @@ quality_check_coverage() {
|
|
|
2622
4127
|
coverage_min=$(jq -r --arg id "test" '(.stages[] | select(.id == $id) | .config.coverage_min) // 0' "$PIPELINE_CONFIG" 2>/dev/null) || true
|
|
2623
4128
|
[[ -z "$coverage_min" || "$coverage_min" == "null" ]] && coverage_min=0
|
|
2624
4129
|
|
|
2625
|
-
#
|
|
4130
|
+
# Adaptive baseline: read from baselines file, enforce no-regression (>= baseline - 2%)
|
|
4131
|
+
local repo_hash_cov
|
|
4132
|
+
repo_hash_cov=$(echo -n "$PROJECT_ROOT" | shasum -a 256 2>/dev/null | cut -c1-12 || echo "unknown")
|
|
4133
|
+
local baselines_dir="${HOME}/.shipwright/baselines/${repo_hash_cov}"
|
|
4134
|
+
local coverage_baseline_file="${baselines_dir}/coverage.json"
|
|
4135
|
+
|
|
2626
4136
|
local baseline_coverage=""
|
|
2627
|
-
if [[ -
|
|
2628
|
-
baseline_coverage=$(
|
|
4137
|
+
if [[ -f "$coverage_baseline_file" ]]; then
|
|
4138
|
+
baseline_coverage=$(jq -r '.baseline // empty' "$coverage_baseline_file" 2>/dev/null) || true
|
|
4139
|
+
fi
|
|
4140
|
+
# Fallback: try legacy memory baseline
|
|
4141
|
+
if [[ -z "$baseline_coverage" ]] && [[ -x "$SCRIPT_DIR/sw-memory.sh" ]]; then
|
|
4142
|
+
baseline_coverage=$(bash "$SCRIPT_DIR/sw-memory.sh" get "coverage_pct" 2>/dev/null) || true
|
|
2629
4143
|
fi
|
|
2630
4144
|
|
|
2631
4145
|
local dropped=false
|
|
2632
|
-
if [[ -n "$baseline_coverage" ]] && awk -v cur="$coverage" -v base="$baseline_coverage" 'BEGIN{exit !(
|
|
2633
|
-
|
|
2634
|
-
|
|
4146
|
+
if [[ -n "$baseline_coverage" && "$baseline_coverage" != "0" ]] && awk -v cur="$coverage" -v base="$baseline_coverage" 'BEGIN{exit !(base > 0)}' 2>/dev/null; then
|
|
4147
|
+
# Adaptive: allow 2% regression tolerance from baseline
|
|
4148
|
+
local min_allowed
|
|
4149
|
+
min_allowed=$(awk -v base="$baseline_coverage" 'BEGIN{printf "%d", base - 2}')
|
|
4150
|
+
if awk -v cur="$coverage" -v min="$min_allowed" 'BEGIN{exit !(cur < min)}' 2>/dev/null; then
|
|
4151
|
+
warn "Coverage regression: ${baseline_coverage}% → ${coverage}% (adaptive min: ${min_allowed}%)"
|
|
4152
|
+
dropped=true
|
|
4153
|
+
fi
|
|
2635
4154
|
fi
|
|
2636
4155
|
|
|
2637
4156
|
if [[ "$coverage_min" -gt 0 ]] 2>/dev/null && awk -v cov="$coverage" -v min="$coverage_min" 'BEGIN{exit !(cov < min)}' 2>/dev/null; then
|
|
@@ -2643,7 +4162,17 @@ quality_check_coverage() {
|
|
|
2643
4162
|
return 1
|
|
2644
4163
|
fi
|
|
2645
4164
|
|
|
2646
|
-
|
|
4165
|
+
# Update baseline on success (first run or improvement)
|
|
4166
|
+
if [[ -z "$baseline_coverage" ]] || awk -v cur="$coverage" -v base="$baseline_coverage" 'BEGIN{exit !(cur >= base)}' 2>/dev/null; then
|
|
4167
|
+
mkdir -p "$baselines_dir"
|
|
4168
|
+
local tmp_cov_baseline
|
|
4169
|
+
tmp_cov_baseline=$(mktemp "${baselines_dir}/coverage.json.XXXXXX")
|
|
4170
|
+
jq -n --arg baseline "$coverage" --arg updated "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
4171
|
+
'{baseline: ($baseline | tonumber), updated: $updated}' > "$tmp_cov_baseline" 2>/dev/null
|
|
4172
|
+
mv "$tmp_cov_baseline" "$coverage_baseline_file" 2>/dev/null || true
|
|
4173
|
+
fi
|
|
4174
|
+
|
|
4175
|
+
info "Coverage: ${coverage}%${baseline_coverage:+ (baseline: ${baseline_coverage}%)}"
|
|
2647
4176
|
return 0
|
|
2648
4177
|
}
|
|
2649
4178
|
|
|
@@ -2660,6 +4189,57 @@ run_adversarial_review() {
|
|
|
2660
4189
|
return 0
|
|
2661
4190
|
fi
|
|
2662
4191
|
|
|
4192
|
+
# Delegate to sw-adversarial.sh module when available (uses intelligence cache)
|
|
4193
|
+
if type adversarial_review &>/dev/null 2>&1; then
|
|
4194
|
+
info "Using intelligence-backed adversarial review..."
|
|
4195
|
+
local json_result
|
|
4196
|
+
json_result=$(adversarial_review "$diff_content" "${GOAL:-}" 2>/dev/null || echo "[]")
|
|
4197
|
+
|
|
4198
|
+
# Save raw JSON result
|
|
4199
|
+
echo "$json_result" > "$ARTIFACTS_DIR/adversarial-review.json"
|
|
4200
|
+
|
|
4201
|
+
# Convert JSON findings to markdown for compatibility with compound_rebuild_with_feedback
|
|
4202
|
+
local critical_count high_count
|
|
4203
|
+
critical_count=$(echo "$json_result" | jq '[.[] | select(.severity == "critical")] | length' 2>/dev/null || echo "0")
|
|
4204
|
+
high_count=$(echo "$json_result" | jq '[.[] | select(.severity == "high")] | length' 2>/dev/null || echo "0")
|
|
4205
|
+
local total_findings
|
|
4206
|
+
total_findings=$(echo "$json_result" | jq 'length' 2>/dev/null || echo "0")
|
|
4207
|
+
|
|
4208
|
+
# Generate markdown report from JSON
|
|
4209
|
+
{
|
|
4210
|
+
echo "# Adversarial Review (Intelligence-backed)"
|
|
4211
|
+
echo ""
|
|
4212
|
+
echo "Total findings: ${total_findings} (${critical_count} critical, ${high_count} high)"
|
|
4213
|
+
echo ""
|
|
4214
|
+
echo "$json_result" | jq -r '.[] | "- **[\(.severity // "unknown")]** \(.location // "unknown") — \(.description // .concern // "no description")"' 2>/dev/null || true
|
|
4215
|
+
} > "$ARTIFACTS_DIR/adversarial-review.md"
|
|
4216
|
+
|
|
4217
|
+
emit_event "adversarial.delegated" \
|
|
4218
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
4219
|
+
"findings=$total_findings" \
|
|
4220
|
+
"critical=$critical_count" \
|
|
4221
|
+
"high=$high_count"
|
|
4222
|
+
|
|
4223
|
+
if [[ "$critical_count" -gt 0 ]]; then
|
|
4224
|
+
warn "Adversarial review: ${critical_count} critical, ${high_count} high"
|
|
4225
|
+
return 1
|
|
4226
|
+
elif [[ "$high_count" -gt 0 ]]; then
|
|
4227
|
+
warn "Adversarial review: ${high_count} high-severity issues"
|
|
4228
|
+
return 1
|
|
4229
|
+
fi
|
|
4230
|
+
|
|
4231
|
+
success "Adversarial review: clean"
|
|
4232
|
+
return 0
|
|
4233
|
+
fi
|
|
4234
|
+
|
|
4235
|
+
# Fallback: inline Claude call when module not loaded
|
|
4236
|
+
|
|
4237
|
+
# Inject previous adversarial findings from memory
|
|
4238
|
+
local adv_memory=""
|
|
4239
|
+
if type intelligence_search_memory &>/dev/null 2>&1; then
|
|
4240
|
+
adv_memory=$(intelligence_search_memory "adversarial review security findings for: ${GOAL:-}" "${HOME}/.shipwright/memory" 5 2>/dev/null) || true
|
|
4241
|
+
fi
|
|
4242
|
+
|
|
2663
4243
|
local prompt="You are a hostile code reviewer. Your job is to find EVERY possible issue in this diff.
|
|
2664
4244
|
Look for:
|
|
2665
4245
|
- Bugs (logic errors, off-by-one, null/undefined access, race conditions)
|
|
@@ -2672,12 +4252,16 @@ Look for:
|
|
|
2672
4252
|
|
|
2673
4253
|
Be thorough and adversarial. List every issue with severity [Critical/Bug/Warning].
|
|
2674
4254
|
Format: **[Severity]** file:line — description
|
|
2675
|
-
|
|
4255
|
+
${adv_memory:+
|
|
4256
|
+
## Known Security Issues from Previous Reviews
|
|
4257
|
+
These security issues have been found in past reviews. Check if any recur:
|
|
4258
|
+
${adv_memory}
|
|
4259
|
+
}
|
|
2676
4260
|
Diff:
|
|
2677
4261
|
$diff_content"
|
|
2678
4262
|
|
|
2679
4263
|
local review_output
|
|
2680
|
-
review_output=$(claude --print "$prompt" 2>"${ARTIFACTS_DIR}/.claude-tokens-adversarial.log" || true)
|
|
4264
|
+
review_output=$(claude --print "$prompt" < /dev/null 2>"${ARTIFACTS_DIR}/.claude-tokens-adversarial.log" || true)
|
|
2681
4265
|
parse_claude_tokens "${ARTIFACTS_DIR}/.claude-tokens-adversarial.log"
|
|
2682
4266
|
|
|
2683
4267
|
echo "$review_output" > "$ARTIFACTS_DIR/adversarial-review.md"
|
|
@@ -2721,6 +4305,12 @@ $(head -200 "$file" 2>/dev/null || true)
|
|
|
2721
4305
|
fi
|
|
2722
4306
|
done <<< "$changed_files"
|
|
2723
4307
|
|
|
4308
|
+
# Inject previous negative prompting findings from memory
|
|
4309
|
+
local neg_memory=""
|
|
4310
|
+
if type intelligence_search_memory &>/dev/null 2>&1; then
|
|
4311
|
+
neg_memory=$(intelligence_search_memory "negative prompting findings common concerns for: ${GOAL:-}" "${HOME}/.shipwright/memory" 5 2>/dev/null) || true
|
|
4312
|
+
fi
|
|
4313
|
+
|
|
2724
4314
|
local prompt="You are a pessimistic engineer who assumes everything will break.
|
|
2725
4315
|
Review these changes and answer:
|
|
2726
4316
|
1. What could go wrong in production?
|
|
@@ -2730,7 +4320,11 @@ Review these changes and answer:
|
|
|
2730
4320
|
5. What happens under load/stress?
|
|
2731
4321
|
6. What happens with malicious input?
|
|
2732
4322
|
7. Are there any implicit dependencies that could break?
|
|
2733
|
-
|
|
4323
|
+
${neg_memory:+
|
|
4324
|
+
## Known Concerns from Previous Reviews
|
|
4325
|
+
These issues have been found in past reviews of this codebase. Check if any apply to the current changes:
|
|
4326
|
+
${neg_memory}
|
|
4327
|
+
}
|
|
2734
4328
|
Be specific. Reference actual code. Categorize each concern as [Critical/Concern/Minor].
|
|
2735
4329
|
|
|
2736
4330
|
Files changed: $changed_files
|
|
@@ -2738,7 +4332,7 @@ Files changed: $changed_files
|
|
|
2738
4332
|
$file_contents"
|
|
2739
4333
|
|
|
2740
4334
|
local review_output
|
|
2741
|
-
review_output=$(claude --print "$prompt" 2>"${ARTIFACTS_DIR}/.claude-tokens-negative.log" || true)
|
|
4335
|
+
review_output=$(claude --print "$prompt" < /dev/null 2>"${ARTIFACTS_DIR}/.claude-tokens-negative.log" || true)
|
|
2742
4336
|
parse_claude_tokens "${ARTIFACTS_DIR}/.claude-tokens-negative.log"
|
|
2743
4337
|
|
|
2744
4338
|
echo "$review_output" > "$ARTIFACTS_DIR/negative-review.md"
|
|
@@ -2768,7 +4362,7 @@ run_e2e_validation() {
|
|
|
2768
4362
|
fi
|
|
2769
4363
|
|
|
2770
4364
|
info "Running E2E validation: $test_cmd"
|
|
2771
|
-
if
|
|
4365
|
+
if bash -c "$test_cmd" > "$ARTIFACTS_DIR/e2e-validation.log" 2>&1; then
|
|
2772
4366
|
success "E2E validation passed"
|
|
2773
4367
|
return 0
|
|
2774
4368
|
else
|
|
@@ -2782,7 +4376,7 @@ run_dod_audit() {
|
|
|
2782
4376
|
|
|
2783
4377
|
if [[ ! -f "$dod_file" ]]; then
|
|
2784
4378
|
# Check for alternative locations
|
|
2785
|
-
for alt in "$PROJECT_ROOT/DEFINITION-OF-DONE.md" "$HOME/.
|
|
4379
|
+
for alt in "$PROJECT_ROOT/DEFINITION-OF-DONE.md" "$HOME/.shipwright/templates/definition-of-done.example.md"; do
|
|
2786
4380
|
if [[ -f "$alt" ]]; then
|
|
2787
4381
|
dod_file="$alt"
|
|
2788
4382
|
break
|
|
@@ -2936,6 +4530,9 @@ stage_compound_quality() {
|
|
|
2936
4530
|
strict_quality=$(jq -r --arg id "compound_quality" '(.stages[] | select(.id == $id) | .config.strict_quality) // false' "$PIPELINE_CONFIG" 2>/dev/null) || true
|
|
2937
4531
|
[[ -z "$strict_quality" || "$strict_quality" == "null" ]] && strict_quality="false"
|
|
2938
4532
|
|
|
4533
|
+
# Convergence tracking
|
|
4534
|
+
local prev_issue_count=-1
|
|
4535
|
+
|
|
2939
4536
|
local cycle=0
|
|
2940
4537
|
while [[ "$cycle" -lt "$max_cycles" ]]; do
|
|
2941
4538
|
cycle=$((cycle + 1))
|
|
@@ -2966,7 +4563,87 @@ stage_compound_quality() {
|
|
|
2966
4563
|
fi
|
|
2967
4564
|
fi
|
|
2968
4565
|
|
|
2969
|
-
# 3.
|
|
4566
|
+
# 3. Developer Simulation (intelligence module)
|
|
4567
|
+
if type simulation_review &>/dev/null 2>&1; then
|
|
4568
|
+
local sim_enabled
|
|
4569
|
+
sim_enabled=$(jq -r '.intelligence.simulation_enabled // false' "$PIPELINE_CONFIG" 2>/dev/null || echo "false")
|
|
4570
|
+
local daemon_cfg="${PROJECT_ROOT}/.claude/daemon-config.json"
|
|
4571
|
+
if [[ "$sim_enabled" != "true" && -f "$daemon_cfg" ]]; then
|
|
4572
|
+
sim_enabled=$(jq -r '.intelligence.simulation_enabled // false' "$daemon_cfg" 2>/dev/null || echo "false")
|
|
4573
|
+
fi
|
|
4574
|
+
if [[ "$sim_enabled" == "true" ]]; then
|
|
4575
|
+
echo ""
|
|
4576
|
+
info "Running developer simulation review..."
|
|
4577
|
+
local sim_diff
|
|
4578
|
+
sim_diff=$(git diff "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
|
|
4579
|
+
if [[ -n "$sim_diff" ]]; then
|
|
4580
|
+
local sim_result
|
|
4581
|
+
sim_result=$(simulation_review "$sim_diff" "${GOAL:-}" 2>/dev/null || echo "[]")
|
|
4582
|
+
if [[ -n "$sim_result" && "$sim_result" != "[]" && "$sim_result" != *'"error"'* ]]; then
|
|
4583
|
+
echo "$sim_result" > "$ARTIFACTS_DIR/compound-simulation-review.json"
|
|
4584
|
+
local sim_critical
|
|
4585
|
+
sim_critical=$(echo "$sim_result" | jq '[.[] | select(.severity == "critical" or .severity == "high")] | length' 2>/dev/null || echo "0")
|
|
4586
|
+
local sim_total
|
|
4587
|
+
sim_total=$(echo "$sim_result" | jq 'length' 2>/dev/null || echo "0")
|
|
4588
|
+
if [[ "$sim_critical" -gt 0 ]]; then
|
|
4589
|
+
warn "Developer simulation: ${sim_critical} critical/high concerns (${sim_total} total)"
|
|
4590
|
+
all_passed=false
|
|
4591
|
+
else
|
|
4592
|
+
success "Developer simulation: ${sim_total} concerns (none critical/high)"
|
|
4593
|
+
fi
|
|
4594
|
+
emit_event "compound.simulation" \
|
|
4595
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
4596
|
+
"cycle=$cycle" \
|
|
4597
|
+
"total=$sim_total" \
|
|
4598
|
+
"critical=$sim_critical"
|
|
4599
|
+
else
|
|
4600
|
+
success "Developer simulation: no concerns"
|
|
4601
|
+
fi
|
|
4602
|
+
fi
|
|
4603
|
+
fi
|
|
4604
|
+
fi
|
|
4605
|
+
|
|
4606
|
+
# 4. Architecture Enforcer (intelligence module)
|
|
4607
|
+
if type architecture_validate_changes &>/dev/null 2>&1; then
|
|
4608
|
+
local arch_enabled
|
|
4609
|
+
arch_enabled=$(jq -r '.intelligence.architecture_enabled // false' "$PIPELINE_CONFIG" 2>/dev/null || echo "false")
|
|
4610
|
+
local daemon_cfg="${PROJECT_ROOT}/.claude/daemon-config.json"
|
|
4611
|
+
if [[ "$arch_enabled" != "true" && -f "$daemon_cfg" ]]; then
|
|
4612
|
+
arch_enabled=$(jq -r '.intelligence.architecture_enabled // false' "$daemon_cfg" 2>/dev/null || echo "false")
|
|
4613
|
+
fi
|
|
4614
|
+
if [[ "$arch_enabled" == "true" ]]; then
|
|
4615
|
+
echo ""
|
|
4616
|
+
info "Running architecture validation..."
|
|
4617
|
+
local arch_diff
|
|
4618
|
+
arch_diff=$(git diff "${BASE_BRANCH}...HEAD" 2>/dev/null || true)
|
|
4619
|
+
if [[ -n "$arch_diff" ]]; then
|
|
4620
|
+
local arch_result
|
|
4621
|
+
arch_result=$(architecture_validate_changes "$arch_diff" "" 2>/dev/null || echo "[]")
|
|
4622
|
+
if [[ -n "$arch_result" && "$arch_result" != "[]" && "$arch_result" != *'"error"'* ]]; then
|
|
4623
|
+
echo "$arch_result" > "$ARTIFACTS_DIR/compound-architecture-validation.json"
|
|
4624
|
+
local arch_violations
|
|
4625
|
+
arch_violations=$(echo "$arch_result" | jq '[.[] | select(.severity == "critical" or .severity == "high")] | length' 2>/dev/null || echo "0")
|
|
4626
|
+
local arch_total
|
|
4627
|
+
arch_total=$(echo "$arch_result" | jq 'length' 2>/dev/null || echo "0")
|
|
4628
|
+
if [[ "$arch_violations" -gt 0 ]]; then
|
|
4629
|
+
warn "Architecture validation: ${arch_violations} critical/high violations (${arch_total} total)"
|
|
4630
|
+
all_passed=false
|
|
4631
|
+
else
|
|
4632
|
+
success "Architecture validation: ${arch_total} violations (none critical/high)"
|
|
4633
|
+
fi
|
|
4634
|
+
emit_event "compound.architecture" \
|
|
4635
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
4636
|
+
"cycle=$cycle" \
|
|
4637
|
+
"total=$arch_total" \
|
|
4638
|
+
"violations=$arch_violations"
|
|
4639
|
+
else
|
|
4640
|
+
success "Architecture validation: no violations"
|
|
4641
|
+
fi
|
|
4642
|
+
fi
|
|
4643
|
+
fi
|
|
4644
|
+
fi
|
|
4645
|
+
|
|
4646
|
+
# 5. E2E Validation
|
|
2970
4647
|
if [[ "$e2e_enabled" == "true" ]]; then
|
|
2971
4648
|
echo ""
|
|
2972
4649
|
info "Running E2E validation..."
|
|
@@ -2975,7 +4652,7 @@ stage_compound_quality() {
|
|
|
2975
4652
|
fi
|
|
2976
4653
|
fi
|
|
2977
4654
|
|
|
2978
|
-
#
|
|
4655
|
+
# 6. DoD Audit
|
|
2979
4656
|
if [[ "$dod_enabled" == "true" ]]; then
|
|
2980
4657
|
echo ""
|
|
2981
4658
|
info "Running Definition of Done audit..."
|
|
@@ -2984,7 +4661,7 @@ stage_compound_quality() {
|
|
|
2984
4661
|
fi
|
|
2985
4662
|
fi
|
|
2986
4663
|
|
|
2987
|
-
#
|
|
4664
|
+
# 7. Multi-dimensional quality checks
|
|
2988
4665
|
echo ""
|
|
2989
4666
|
info "Running multi-dimensional quality checks..."
|
|
2990
4667
|
local quality_failures=0
|
|
@@ -3016,15 +4693,37 @@ stage_compound_quality() {
|
|
|
3016
4693
|
success "Multi-dimensional quality: all checks passed"
|
|
3017
4694
|
fi
|
|
3018
4695
|
|
|
4696
|
+
# ── Convergence Detection ──
|
|
4697
|
+
# Count critical/high issues from all review artifacts
|
|
4698
|
+
local current_issue_count=0
|
|
4699
|
+
if [[ -f "$ARTIFACTS_DIR/adversarial-review.md" ]]; then
|
|
4700
|
+
local adv_issues
|
|
4701
|
+
adv_issues=$(grep -ciE '\*\*\[?(Critical|Bug|critical|high)\]?\*\*' "$ARTIFACTS_DIR/adversarial-review.md" 2>/dev/null || true)
|
|
4702
|
+
current_issue_count=$((current_issue_count + ${adv_issues:-0}))
|
|
4703
|
+
fi
|
|
4704
|
+
if [[ -f "$ARTIFACTS_DIR/adversarial-review.json" ]]; then
|
|
4705
|
+
local adv_json_issues
|
|
4706
|
+
adv_json_issues=$(jq '[.[] | select(.severity == "critical" or .severity == "high")] | length' "$ARTIFACTS_DIR/adversarial-review.json" 2>/dev/null || echo "0")
|
|
4707
|
+
current_issue_count=$((current_issue_count + ${adv_json_issues:-0}))
|
|
4708
|
+
fi
|
|
4709
|
+
if [[ -f "$ARTIFACTS_DIR/negative-review.md" ]]; then
|
|
4710
|
+
local neg_issues
|
|
4711
|
+
neg_issues=$(grep -ciE '\[Critical\]' "$ARTIFACTS_DIR/negative-review.md" 2>/dev/null || true)
|
|
4712
|
+
current_issue_count=$((current_issue_count + ${neg_issues:-0}))
|
|
4713
|
+
fi
|
|
4714
|
+
current_issue_count=$((current_issue_count + quality_failures))
|
|
4715
|
+
|
|
3019
4716
|
emit_event "compound.cycle" \
|
|
3020
4717
|
"issue=${ISSUE_NUMBER:-0}" \
|
|
3021
4718
|
"cycle=$cycle" \
|
|
3022
4719
|
"max_cycles=$max_cycles" \
|
|
3023
4720
|
"passed=$all_passed" \
|
|
4721
|
+
"critical_issues=$current_issue_count" \
|
|
3024
4722
|
"self_heal_count=$SELF_HEAL_COUNT"
|
|
3025
4723
|
|
|
3026
|
-
|
|
3027
|
-
|
|
4724
|
+
# Early exit: zero critical/high issues
|
|
4725
|
+
if [[ "$current_issue_count" -eq 0 ]] && $all_passed; then
|
|
4726
|
+
success "Compound quality passed on cycle ${cycle} — zero critical/high issues"
|
|
3028
4727
|
|
|
3029
4728
|
if [[ -n "$ISSUE_NUMBER" ]]; then
|
|
3030
4729
|
gh_comment_issue "$ISSUE_NUMBER" "✅ **Compound quality passed** — cycle ${cycle}/${max_cycles}
|
|
@@ -3032,6 +4731,8 @@ stage_compound_quality() {
|
|
|
3032
4731
|
All quality checks clean:
|
|
3033
4732
|
- Adversarial review: ✅
|
|
3034
4733
|
- Negative prompting: ✅
|
|
4734
|
+
- Developer simulation: ✅
|
|
4735
|
+
- Architecture validation: ✅
|
|
3035
4736
|
- E2E validation: ✅
|
|
3036
4737
|
- DoD audit: ✅
|
|
3037
4738
|
- Security audit: ✅
|
|
@@ -3045,6 +4746,36 @@ All quality checks clean:
|
|
|
3045
4746
|
return 0
|
|
3046
4747
|
fi
|
|
3047
4748
|
|
|
4749
|
+
if $all_passed; then
|
|
4750
|
+
success "Compound quality passed on cycle ${cycle}"
|
|
4751
|
+
|
|
4752
|
+
if [[ -n "$ISSUE_NUMBER" ]]; then
|
|
4753
|
+
gh_comment_issue "$ISSUE_NUMBER" "✅ **Compound quality passed** — cycle ${cycle}/${max_cycles}" 2>/dev/null || true
|
|
4754
|
+
fi
|
|
4755
|
+
|
|
4756
|
+
log_stage "compound_quality" "Passed on cycle ${cycle}/${max_cycles}"
|
|
4757
|
+
return 0
|
|
4758
|
+
fi
|
|
4759
|
+
|
|
4760
|
+
# Check for plateau: issue count unchanged between cycles
|
|
4761
|
+
if [[ "$prev_issue_count" -ge 0 && "$current_issue_count" -eq "$prev_issue_count" && "$cycle" -gt 1 ]]; then
|
|
4762
|
+
warn "Convergence: quality plateau — ${current_issue_count} issues unchanged between cycles"
|
|
4763
|
+
emit_event "compound.plateau" \
|
|
4764
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
4765
|
+
"cycle=$cycle" \
|
|
4766
|
+
"issue_count=$current_issue_count"
|
|
4767
|
+
|
|
4768
|
+
if [[ -n "$ISSUE_NUMBER" ]]; then
|
|
4769
|
+
gh_comment_issue "$ISSUE_NUMBER" "⚠️ **Compound quality plateau** — ${current_issue_count} issues unchanged after cycle ${cycle}. Stopping early." 2>/dev/null || true
|
|
4770
|
+
fi
|
|
4771
|
+
|
|
4772
|
+
log_stage "compound_quality" "Plateau at cycle ${cycle}/${max_cycles} (${current_issue_count} issues)"
|
|
4773
|
+
return 1
|
|
4774
|
+
fi
|
|
4775
|
+
prev_issue_count="$current_issue_count"
|
|
4776
|
+
|
|
4777
|
+
info "Convergence: ${current_issue_count} critical/high issues remaining"
|
|
4778
|
+
|
|
3048
4779
|
# Not all passed — rebuild if we have cycles left
|
|
3049
4780
|
if [[ "$cycle" -lt "$max_cycles" ]]; then
|
|
3050
4781
|
warn "Quality checks failed — rebuilding with feedback (cycle $((cycle + 1))/${max_cycles})"
|
|
@@ -3074,6 +4805,100 @@ Quality issues remain. Check artifacts for details." 2>/dev/null || true
|
|
|
3074
4805
|
return 1
|
|
3075
4806
|
}
|
|
3076
4807
|
|
|
4808
|
+
# ─── Error Classification ──────────────────────────────────────────────────
|
|
4809
|
+
# Classifies errors to determine whether retrying makes sense.
|
|
4810
|
+
# Returns: "infrastructure", "logic", "configuration", or "unknown"
|
|
4811
|
+
|
|
4812
|
+
classify_error() {
|
|
4813
|
+
local stage_id="$1"
|
|
4814
|
+
local log_file="${ARTIFACTS_DIR}/${stage_id}-results.log"
|
|
4815
|
+
[[ ! -f "$log_file" ]] && log_file="${ARTIFACTS_DIR}/test-results.log"
|
|
4816
|
+
[[ ! -f "$log_file" ]] && { echo "unknown"; return; }
|
|
4817
|
+
|
|
4818
|
+
local log_tail
|
|
4819
|
+
log_tail=$(tail -50 "$log_file" 2>/dev/null || echo "")
|
|
4820
|
+
|
|
4821
|
+
# Generate error signature for history lookup
|
|
4822
|
+
local error_sig
|
|
4823
|
+
error_sig=$(echo "$log_tail" | grep -iE 'error|fail|exception|fatal' 2>/dev/null | head -3 | cksum | awk '{print $1}' || echo "0")
|
|
4824
|
+
|
|
4825
|
+
# Check classification history first (learned from previous runs)
|
|
4826
|
+
local class_history="${HOME}/.shipwright/optimization/error-classifications.json"
|
|
4827
|
+
if [[ -f "$class_history" ]]; then
|
|
4828
|
+
local cached_class
|
|
4829
|
+
cached_class=$(jq -r --arg sig "$error_sig" '.[$sig].classification // empty' "$class_history" 2>/dev/null || true)
|
|
4830
|
+
if [[ -n "$cached_class" && "$cached_class" != "null" ]]; then
|
|
4831
|
+
echo "$cached_class"
|
|
4832
|
+
return
|
|
4833
|
+
fi
|
|
4834
|
+
fi
|
|
4835
|
+
|
|
4836
|
+
local classification="unknown"
|
|
4837
|
+
|
|
4838
|
+
# Infrastructure errors: timeout, OOM, network — retry makes sense
|
|
4839
|
+
if echo "$log_tail" | grep -qiE 'timeout|timed out|ETIMEDOUT|ECONNREFUSED|ECONNRESET|network|socket hang up|OOM|out of memory|killed|signal 9|Cannot allocate memory'; then
|
|
4840
|
+
classification="infrastructure"
|
|
4841
|
+
# Configuration errors: missing env, wrong path — don't retry, escalate
|
|
4842
|
+
elif echo "$log_tail" | grep -qiE 'ENOENT|not found|No such file|command not found|MODULE_NOT_FOUND|Cannot find module|missing.*env|undefined variable|permission denied|EACCES'; then
|
|
4843
|
+
classification="configuration"
|
|
4844
|
+
# Logic errors: assertion failures, type errors — retry won't help without code change
|
|
4845
|
+
elif echo "$log_tail" | grep -qiE 'AssertionError|assert.*fail|Expected.*but.*got|TypeError|ReferenceError|SyntaxError|CompileError|type mismatch|cannot assign|incompatible type'; then
|
|
4846
|
+
classification="logic"
|
|
4847
|
+
# Build errors: compilation failures
|
|
4848
|
+
elif echo "$log_tail" | grep -qiE 'error\[E[0-9]+\]|error: aborting|FAILED.*compile|build failed|tsc.*error|eslint.*error'; then
|
|
4849
|
+
classification="logic"
|
|
4850
|
+
# Intelligence fallback: Claude classification for unknown errors
|
|
4851
|
+
elif [[ "$classification" == "unknown" ]] && type intelligence_search_memory &>/dev/null 2>&1 && command -v claude &>/dev/null; then
|
|
4852
|
+
local ai_class
|
|
4853
|
+
ai_class=$(claude --print --output-format text -p "Classify this error as exactly one of: infrastructure, configuration, logic, unknown.
|
|
4854
|
+
|
|
4855
|
+
Error output:
|
|
4856
|
+
$(echo "$log_tail" | tail -20)
|
|
4857
|
+
|
|
4858
|
+
Reply with ONLY the classification word, nothing else." --model haiku < /dev/null 2>/dev/null || true)
|
|
4859
|
+
ai_class=$(echo "$ai_class" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
|
|
4860
|
+
case "$ai_class" in
|
|
4861
|
+
infrastructure|configuration|logic) classification="$ai_class" ;;
|
|
4862
|
+
esac
|
|
4863
|
+
fi
|
|
4864
|
+
|
|
4865
|
+
# Map retry categories to shared taxonomy (from lib/compat.sh SW_ERROR_CATEGORIES)
|
|
4866
|
+
# Retry uses: infrastructure, configuration, logic, unknown
|
|
4867
|
+
# Shared uses: test_failure, build_error, lint_error, timeout, dependency, flaky, config, security, permission, unknown
|
|
4868
|
+
local canonical_category="unknown"
|
|
4869
|
+
case "$classification" in
|
|
4870
|
+
infrastructure) canonical_category="timeout" ;;
|
|
4871
|
+
configuration) canonical_category="config" ;;
|
|
4872
|
+
logic)
|
|
4873
|
+
case "$stage_id" in
|
|
4874
|
+
test) canonical_category="test_failure" ;;
|
|
4875
|
+
*) canonical_category="build_error" ;;
|
|
4876
|
+
esac
|
|
4877
|
+
;;
|
|
4878
|
+
esac
|
|
4879
|
+
|
|
4880
|
+
# Record classification for future runs (using both retry and canonical categories)
|
|
4881
|
+
if [[ -n "$error_sig" && "$error_sig" != "0" ]]; then
|
|
4882
|
+
local class_dir="${HOME}/.shipwright/optimization"
|
|
4883
|
+
mkdir -p "$class_dir" 2>/dev/null || true
|
|
4884
|
+
local tmp_class
|
|
4885
|
+
tmp_class="$(mktemp)"
|
|
4886
|
+
if [[ -f "$class_history" ]]; then
|
|
4887
|
+
jq --arg sig "$error_sig" --arg cls "$classification" --arg canon "$canonical_category" --arg stage "$stage_id" \
|
|
4888
|
+
'.[$sig] = {"classification": $cls, "canonical": $canon, "stage": $stage, "recorded_at": now}' \
|
|
4889
|
+
"$class_history" > "$tmp_class" 2>/dev/null && \
|
|
4890
|
+
mv "$tmp_class" "$class_history" || rm -f "$tmp_class"
|
|
4891
|
+
else
|
|
4892
|
+
jq -n --arg sig "$error_sig" --arg cls "$classification" --arg canon "$canonical_category" --arg stage "$stage_id" \
|
|
4893
|
+
'{($sig): {"classification": $cls, "canonical": $canon, "stage": $stage, "recorded_at": now}}' \
|
|
4894
|
+
> "$tmp_class" 2>/dev/null && \
|
|
4895
|
+
mv "$tmp_class" "$class_history" || rm -f "$tmp_class"
|
|
4896
|
+
fi
|
|
4897
|
+
fi
|
|
4898
|
+
|
|
4899
|
+
echo "$classification"
|
|
4900
|
+
}
|
|
4901
|
+
|
|
3077
4902
|
# ─── Stage Runner ───────────────────────────────────────────────────────────
|
|
3078
4903
|
|
|
3079
4904
|
run_stage_with_retry() {
|
|
@@ -3083,6 +4908,7 @@ run_stage_with_retry() {
|
|
|
3083
4908
|
[[ -z "$max_retries" || "$max_retries" == "null" ]] && max_retries=0
|
|
3084
4909
|
|
|
3085
4910
|
local attempt=0
|
|
4911
|
+
local prev_error_class=""
|
|
3086
4912
|
while true; do
|
|
3087
4913
|
if "stage_${stage_id}"; then
|
|
3088
4914
|
return 0
|
|
@@ -3093,8 +4919,53 @@ run_stage_with_retry() {
|
|
|
3093
4919
|
return 1
|
|
3094
4920
|
fi
|
|
3095
4921
|
|
|
3096
|
-
|
|
3097
|
-
|
|
4922
|
+
# Classify the error to decide whether retry makes sense
|
|
4923
|
+
local error_class
|
|
4924
|
+
error_class=$(classify_error "$stage_id")
|
|
4925
|
+
|
|
4926
|
+
emit_event "retry.classified" \
|
|
4927
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
4928
|
+
"stage=$stage_id" \
|
|
4929
|
+
"attempt=$attempt" \
|
|
4930
|
+
"error_class=$error_class"
|
|
4931
|
+
|
|
4932
|
+
case "$error_class" in
|
|
4933
|
+
infrastructure)
|
|
4934
|
+
info "Error classified as infrastructure (timeout/network/OOM) — retry makes sense"
|
|
4935
|
+
;;
|
|
4936
|
+
configuration)
|
|
4937
|
+
error "Error classified as configuration (missing env/path) — skipping retry, escalating"
|
|
4938
|
+
emit_event "retry.escalated" \
|
|
4939
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
4940
|
+
"stage=$stage_id" \
|
|
4941
|
+
"reason=configuration_error"
|
|
4942
|
+
return 1
|
|
4943
|
+
;;
|
|
4944
|
+
logic)
|
|
4945
|
+
if [[ "$error_class" == "$prev_error_class" ]]; then
|
|
4946
|
+
error "Error classified as logic (assertion/type error) with same class — retry won't help without code change"
|
|
4947
|
+
emit_event "retry.skipped" \
|
|
4948
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
4949
|
+
"stage=$stage_id" \
|
|
4950
|
+
"reason=repeated_logic_error"
|
|
4951
|
+
return 1
|
|
4952
|
+
fi
|
|
4953
|
+
warn "Error classified as logic — retrying once in case build fixes it"
|
|
4954
|
+
;;
|
|
4955
|
+
*)
|
|
4956
|
+
info "Error classification: unknown — retrying"
|
|
4957
|
+
;;
|
|
4958
|
+
esac
|
|
4959
|
+
prev_error_class="$error_class"
|
|
4960
|
+
|
|
4961
|
+
warn "Stage $stage_id failed (attempt $attempt/$((max_retries + 1)), class: $error_class) — retrying..."
|
|
4962
|
+
# Exponential backoff with jitter to avoid thundering herd
|
|
4963
|
+
local backoff=$((2 ** attempt))
|
|
4964
|
+
[[ "$backoff" -gt 16 ]] && backoff=16
|
|
4965
|
+
local jitter=$(( RANDOM % (backoff + 1) ))
|
|
4966
|
+
local total_sleep=$((backoff + jitter))
|
|
4967
|
+
info "Backing off ${total_sleep}s before retry..."
|
|
4968
|
+
sleep "$total_sleep"
|
|
3098
4969
|
done
|
|
3099
4970
|
}
|
|
3100
4971
|
|
|
@@ -3107,6 +4978,25 @@ self_healing_build_test() {
|
|
|
3107
4978
|
local max_cycles="$BUILD_TEST_RETRIES"
|
|
3108
4979
|
local last_test_error=""
|
|
3109
4980
|
|
|
4981
|
+
# Convergence tracking
|
|
4982
|
+
local prev_error_sig="" consecutive_same_error=0
|
|
4983
|
+
local prev_fail_count=0 zero_convergence_streak=0
|
|
4984
|
+
|
|
4985
|
+
# Intelligence: adaptive iteration limit
|
|
4986
|
+
if type composer_estimate_iterations &>/dev/null 2>&1; then
|
|
4987
|
+
local estimated
|
|
4988
|
+
estimated=$(composer_estimate_iterations \
|
|
4989
|
+
"${INTELLIGENCE_ANALYSIS:-{}}" \
|
|
4990
|
+
"${HOME}/.shipwright/optimization/iteration-model.json" 2>/dev/null || echo "")
|
|
4991
|
+
if [[ -n "$estimated" && "$estimated" =~ ^[0-9]+$ && "$estimated" -gt 0 ]]; then
|
|
4992
|
+
max_cycles="$estimated"
|
|
4993
|
+
emit_event "intelligence.adaptive_iterations" \
|
|
4994
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
4995
|
+
"estimated=$estimated" \
|
|
4996
|
+
"original=$BUILD_TEST_RETRIES"
|
|
4997
|
+
fi
|
|
4998
|
+
fi
|
|
4999
|
+
|
|
3110
5000
|
while [[ "$cycle" -le "$max_cycles" ]]; do
|
|
3111
5001
|
cycle=$((cycle + 1))
|
|
3112
5002
|
|
|
@@ -3182,6 +5072,9 @@ Focus on fixing the failing tests while keeping all passing tests working."
|
|
|
3182
5072
|
local timing
|
|
3183
5073
|
timing=$(get_stage_timing "test")
|
|
3184
5074
|
success "Stage ${BOLD}test${RESET} complete ${DIM}(${timing})${RESET}"
|
|
5075
|
+
emit_event "convergence.tests_passed" \
|
|
5076
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
5077
|
+
"cycle=$cycle"
|
|
3185
5078
|
return 0 # Tests passed!
|
|
3186
5079
|
fi
|
|
3187
5080
|
|
|
@@ -3190,6 +5083,59 @@ Focus on fixing the failing tests while keeping all passing tests working."
|
|
|
3190
5083
|
last_test_error=$(tail -30 "$test_log" 2>/dev/null || echo "Test command failed with no output")
|
|
3191
5084
|
mark_stage_failed "test"
|
|
3192
5085
|
|
|
5086
|
+
# ── Convergence Detection ──
|
|
5087
|
+
# Hash the error output to detect repeated failures
|
|
5088
|
+
local error_sig
|
|
5089
|
+
error_sig=$(echo "$last_test_error" | shasum -a 256 2>/dev/null | cut -c1-16 || echo "unknown")
|
|
5090
|
+
|
|
5091
|
+
# Count failing tests (extract from common patterns)
|
|
5092
|
+
local current_fail_count=0
|
|
5093
|
+
current_fail_count=$(grep -ciE 'fail|error|FAIL' "$test_log" 2>/dev/null || true)
|
|
5094
|
+
current_fail_count="${current_fail_count:-0}"
|
|
5095
|
+
|
|
5096
|
+
if [[ "$error_sig" == "$prev_error_sig" ]]; then
|
|
5097
|
+
consecutive_same_error=$((consecutive_same_error + 1))
|
|
5098
|
+
else
|
|
5099
|
+
consecutive_same_error=1
|
|
5100
|
+
fi
|
|
5101
|
+
prev_error_sig="$error_sig"
|
|
5102
|
+
|
|
5103
|
+
# Check: same error 3 times consecutively → stuck
|
|
5104
|
+
if [[ "$consecutive_same_error" -ge 3 ]]; then
|
|
5105
|
+
error "Convergence: stuck on same error for 3 consecutive cycles — exiting early"
|
|
5106
|
+
emit_event "convergence.stuck" \
|
|
5107
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
5108
|
+
"cycle=$cycle" \
|
|
5109
|
+
"error_sig=$error_sig" \
|
|
5110
|
+
"consecutive=$consecutive_same_error"
|
|
5111
|
+
notify "Build Convergence" "Stuck on unfixable error after ${cycle} cycles" "error"
|
|
5112
|
+
return 1
|
|
5113
|
+
fi
|
|
5114
|
+
|
|
5115
|
+
# Track convergence rate: did we reduce failures?
|
|
5116
|
+
if [[ "$cycle" -gt 1 && "$prev_fail_count" -gt 0 ]]; then
|
|
5117
|
+
if [[ "$current_fail_count" -ge "$prev_fail_count" ]]; then
|
|
5118
|
+
zero_convergence_streak=$((zero_convergence_streak + 1))
|
|
5119
|
+
else
|
|
5120
|
+
zero_convergence_streak=0
|
|
5121
|
+
fi
|
|
5122
|
+
|
|
5123
|
+
# Check: zero convergence for 2 consecutive iterations → plateau
|
|
5124
|
+
if [[ "$zero_convergence_streak" -ge 2 ]]; then
|
|
5125
|
+
error "Convergence: no progress for 2 consecutive cycles (${current_fail_count} failures remain) — exiting early"
|
|
5126
|
+
emit_event "convergence.plateau" \
|
|
5127
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
5128
|
+
"cycle=$cycle" \
|
|
5129
|
+
"fail_count=$current_fail_count" \
|
|
5130
|
+
"streak=$zero_convergence_streak"
|
|
5131
|
+
notify "Build Convergence" "No progress after ${cycle} cycles — plateau reached" "error"
|
|
5132
|
+
return 1
|
|
5133
|
+
fi
|
|
5134
|
+
fi
|
|
5135
|
+
prev_fail_count="$current_fail_count"
|
|
5136
|
+
|
|
5137
|
+
info "Convergence: error_sig=${error_sig:0:8} repeat=${consecutive_same_error} failures=${current_fail_count} no_progress=${zero_convergence_streak}"
|
|
5138
|
+
|
|
3193
5139
|
if [[ "$cycle" -le "$max_cycles" ]]; then
|
|
3194
5140
|
warn "Tests failed — will attempt self-healing (cycle $((cycle + 1))/$((max_cycles + 1)))"
|
|
3195
5141
|
notify "Self-Healing" "Tests failed on cycle ${cycle}, retrying..." "warn"
|
|
@@ -3256,7 +5202,7 @@ run_pipeline() {
|
|
|
3256
5202
|
use_self_healing=true
|
|
3257
5203
|
fi
|
|
3258
5204
|
|
|
3259
|
-
while IFS= read -r stage; do
|
|
5205
|
+
while IFS= read -r -u 3 stage; do
|
|
3260
5206
|
local id enabled gate
|
|
3261
5207
|
id=$(echo "$stage" | jq -r '.id')
|
|
3262
5208
|
enabled=$(echo "$stage" | jq -r '.enabled')
|
|
@@ -3264,6 +5210,34 @@ run_pipeline() {
|
|
|
3264
5210
|
|
|
3265
5211
|
CURRENT_STAGE_ID="$id"
|
|
3266
5212
|
|
|
5213
|
+
# Human intervention: check for skip-stage directive
|
|
5214
|
+
if [[ -f "$ARTIFACTS_DIR/skip-stage.txt" ]]; then
|
|
5215
|
+
local skip_list
|
|
5216
|
+
skip_list="$(cat "$ARTIFACTS_DIR/skip-stage.txt" 2>/dev/null || true)"
|
|
5217
|
+
if echo "$skip_list" | grep -qx "$id" 2>/dev/null; then
|
|
5218
|
+
info "Stage ${BOLD}${id}${RESET} skipped by human directive"
|
|
5219
|
+
emit_event "stage.skipped" "issue=${ISSUE_NUMBER:-0}" "stage=$id" "reason=human_skip"
|
|
5220
|
+
# Remove this stage from the skip file
|
|
5221
|
+
local tmp_skip
|
|
5222
|
+
tmp_skip="$(mktemp)"
|
|
5223
|
+
grep -vx "$id" "$ARTIFACTS_DIR/skip-stage.txt" > "$tmp_skip" 2>/dev/null || true
|
|
5224
|
+
mv "$tmp_skip" "$ARTIFACTS_DIR/skip-stage.txt"
|
|
5225
|
+
continue
|
|
5226
|
+
fi
|
|
5227
|
+
fi
|
|
5228
|
+
|
|
5229
|
+
# Human intervention: check for human message
|
|
5230
|
+
if [[ -f "$ARTIFACTS_DIR/human-message.txt" ]]; then
|
|
5231
|
+
local human_msg
|
|
5232
|
+
human_msg="$(cat "$ARTIFACTS_DIR/human-message.txt" 2>/dev/null || true)"
|
|
5233
|
+
if [[ -n "$human_msg" ]]; then
|
|
5234
|
+
echo ""
|
|
5235
|
+
echo -e " ${PURPLE}${BOLD}💬 Human message:${RESET} $human_msg"
|
|
5236
|
+
emit_event "pipeline.human_message" "issue=${ISSUE_NUMBER:-0}" "stage=$id" "message=$human_msg"
|
|
5237
|
+
rm -f "$ARTIFACTS_DIR/human-message.txt"
|
|
5238
|
+
fi
|
|
5239
|
+
fi
|
|
5240
|
+
|
|
3267
5241
|
if [[ "$enabled" != "true" ]]; then
|
|
3268
5242
|
echo -e " ${DIM}○ ${id} — skipped (disabled)${RESET}"
|
|
3269
5243
|
continue
|
|
@@ -3277,6 +5251,15 @@ run_pipeline() {
|
|
|
3277
5251
|
continue
|
|
3278
5252
|
fi
|
|
3279
5253
|
|
|
5254
|
+
# CI resume: skip stages marked as completed from previous run
|
|
5255
|
+
if [[ -n "${COMPLETED_STAGES:-}" ]] && echo "$COMPLETED_STAGES" | tr ',' '\n' | grep -qx "$id"; then
|
|
5256
|
+
echo -e " ${GREEN}✓ ${id}${RESET} ${DIM}— skipped (CI resume)${RESET}"
|
|
5257
|
+
set_stage_status "$id" "complete"
|
|
5258
|
+
completed=$((completed + 1))
|
|
5259
|
+
emit_event "stage.skipped" "issue=${ISSUE_NUMBER:-0}" "stage=$id" "reason=ci_resume"
|
|
5260
|
+
continue
|
|
5261
|
+
fi
|
|
5262
|
+
|
|
3280
5263
|
# Self-healing build→test loop: when we hit build, run both together
|
|
3281
5264
|
if [[ "$id" == "build" && "$use_self_healing" == "true" ]]; then
|
|
3282
5265
|
# Gate check for build
|
|
@@ -3325,9 +5308,9 @@ run_pipeline() {
|
|
|
3325
5308
|
fi
|
|
3326
5309
|
|
|
3327
5310
|
# Budget enforcement check (skip with --ignore-budget)
|
|
3328
|
-
if [[ "$IGNORE_BUDGET" != "true" ]] && [[ -x "$SCRIPT_DIR/
|
|
5311
|
+
if [[ "$IGNORE_BUDGET" != "true" ]] && [[ -x "$SCRIPT_DIR/sw-cost.sh" ]]; then
|
|
3329
5312
|
local budget_rc=0
|
|
3330
|
-
bash "$SCRIPT_DIR/
|
|
5313
|
+
bash "$SCRIPT_DIR/sw-cost.sh" check-budget 2>/dev/null || budget_rc=$?
|
|
3331
5314
|
if [[ "$budget_rc" -eq 2 ]]; then
|
|
3332
5315
|
warn "Daily budget exceeded — pausing pipeline before stage ${BOLD}$id${RESET}"
|
|
3333
5316
|
warn "Resume with --ignore-budget to override, or wait until tomorrow"
|
|
@@ -3337,6 +5320,71 @@ run_pipeline() {
|
|
|
3337
5320
|
fi
|
|
3338
5321
|
fi
|
|
3339
5322
|
|
|
5323
|
+
# Intelligence: per-stage model routing with A/B testing
|
|
5324
|
+
if type intelligence_recommend_model &>/dev/null 2>&1; then
|
|
5325
|
+
local stage_complexity="${INTELLIGENCE_COMPLEXITY:-5}"
|
|
5326
|
+
local budget_remaining=""
|
|
5327
|
+
if [[ -x "$SCRIPT_DIR/sw-cost.sh" ]]; then
|
|
5328
|
+
budget_remaining=$(bash "$SCRIPT_DIR/sw-cost.sh" remaining-budget 2>/dev/null || echo "")
|
|
5329
|
+
fi
|
|
5330
|
+
local recommended_model
|
|
5331
|
+
recommended_model=$(intelligence_recommend_model "$id" "$stage_complexity" "$budget_remaining" 2>/dev/null || echo "")
|
|
5332
|
+
if [[ -n "$recommended_model" && "$recommended_model" != "null" ]]; then
|
|
5333
|
+
# A/B testing: decide whether to use the recommended model
|
|
5334
|
+
local ab_ratio=20 # default 20% use recommended model
|
|
5335
|
+
local daemon_cfg="${PROJECT_ROOT}/.claude/daemon-config.json"
|
|
5336
|
+
if [[ -f "$daemon_cfg" ]]; then
|
|
5337
|
+
local cfg_ratio
|
|
5338
|
+
cfg_ratio=$(jq -r '.intelligence.ab_test_ratio // 0.2' "$daemon_cfg" 2>/dev/null || echo "0.2")
|
|
5339
|
+
# Convert ratio (0.0-1.0) to percentage (0-100)
|
|
5340
|
+
ab_ratio=$(awk -v r="$cfg_ratio" 'BEGIN{printf "%d", r * 100}' 2>/dev/null || echo "20")
|
|
5341
|
+
fi
|
|
5342
|
+
|
|
5343
|
+
# Check if we have enough data points to graduate from A/B testing
|
|
5344
|
+
local routing_file="${HOME}/.shipwright/optimization/model-routing.json"
|
|
5345
|
+
local use_recommended=false
|
|
5346
|
+
local ab_group="control"
|
|
5347
|
+
|
|
5348
|
+
if [[ -f "$routing_file" ]]; then
|
|
5349
|
+
local stage_samples
|
|
5350
|
+
stage_samples=$(jq -r --arg s "$id" '.[$s].sonnet_samples // 0' "$routing_file" 2>/dev/null || echo "0")
|
|
5351
|
+
local total_samples
|
|
5352
|
+
total_samples=$(jq -r --arg s "$id" '((.[$s].sonnet_samples // 0) + (.[$s].opus_samples // 0))' "$routing_file" 2>/dev/null || echo "0")
|
|
5353
|
+
|
|
5354
|
+
if [[ "$total_samples" -ge 50 ]]; then
|
|
5355
|
+
# Enough data — use optimizer's recommendation as default
|
|
5356
|
+
use_recommended=true
|
|
5357
|
+
ab_group="graduated"
|
|
5358
|
+
fi
|
|
5359
|
+
fi
|
|
5360
|
+
|
|
5361
|
+
if [[ "$use_recommended" != "true" ]]; then
|
|
5362
|
+
# A/B test: RANDOM % 100 < ab_ratio → use recommended
|
|
5363
|
+
local roll=$((RANDOM % 100))
|
|
5364
|
+
if [[ "$roll" -lt "$ab_ratio" ]]; then
|
|
5365
|
+
use_recommended=true
|
|
5366
|
+
ab_group="experiment"
|
|
5367
|
+
else
|
|
5368
|
+
ab_group="control"
|
|
5369
|
+
fi
|
|
5370
|
+
fi
|
|
5371
|
+
|
|
5372
|
+
if [[ "$use_recommended" == "true" ]]; then
|
|
5373
|
+
export CLAUDE_MODEL="$recommended_model"
|
|
5374
|
+
else
|
|
5375
|
+
export CLAUDE_MODEL="opus"
|
|
5376
|
+
fi
|
|
5377
|
+
|
|
5378
|
+
emit_event "intelligence.model_ab" \
|
|
5379
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
5380
|
+
"stage=$id" \
|
|
5381
|
+
"recommended=$recommended_model" \
|
|
5382
|
+
"applied=$CLAUDE_MODEL" \
|
|
5383
|
+
"ab_group=$ab_group" \
|
|
5384
|
+
"ab_ratio=$ab_ratio"
|
|
5385
|
+
fi
|
|
5386
|
+
fi
|
|
5387
|
+
|
|
3340
5388
|
echo ""
|
|
3341
5389
|
echo -e "${CYAN}${BOLD}▸ Stage: ${id}${RESET} ${DIM}[$((completed + 1))/${enabled_count}]${RESET}"
|
|
3342
5390
|
update_status "running" "$id"
|
|
@@ -3345,6 +5393,12 @@ run_pipeline() {
|
|
|
3345
5393
|
stage_start_epoch=$(now_epoch)
|
|
3346
5394
|
emit_event "stage.started" "issue=${ISSUE_NUMBER:-0}" "stage=$id"
|
|
3347
5395
|
|
|
5396
|
+
# Mark GitHub Check Run as in-progress
|
|
5397
|
+
if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_checks_stage_update &>/dev/null 2>&1; then
|
|
5398
|
+
gh_checks_stage_update "$id" "in_progress" "" "Stage $id started" 2>/dev/null || true
|
|
5399
|
+
fi
|
|
5400
|
+
|
|
5401
|
+
local stage_model_used="${CLAUDE_MODEL:-${MODEL:-opus}}"
|
|
3348
5402
|
if run_stage_with_retry "$id"; then
|
|
3349
5403
|
mark_stage_complete "$id"
|
|
3350
5404
|
completed=$((completed + 1))
|
|
@@ -3353,6 +5407,8 @@ run_pipeline() {
|
|
|
3353
5407
|
stage_dur_s=$(( $(now_epoch) - stage_start_epoch ))
|
|
3354
5408
|
success "Stage ${BOLD}$id${RESET} complete ${DIM}(${timing})${RESET}"
|
|
3355
5409
|
emit_event "stage.completed" "issue=${ISSUE_NUMBER:-0}" "stage=$id" "duration_s=$stage_dur_s"
|
|
5410
|
+
# Log model used for prediction feedback
|
|
5411
|
+
echo "${id}|${stage_model_used}|true" >> "${ARTIFACTS_DIR}/model-routing.log"
|
|
3356
5412
|
else
|
|
3357
5413
|
mark_stage_failed "$id"
|
|
3358
5414
|
local stage_dur_s
|
|
@@ -3360,9 +5416,11 @@ run_pipeline() {
|
|
|
3360
5416
|
error "Pipeline failed at stage: ${BOLD}$id${RESET}"
|
|
3361
5417
|
update_status "failed" "$id"
|
|
3362
5418
|
emit_event "stage.failed" "issue=${ISSUE_NUMBER:-0}" "stage=$id" "duration_s=$stage_dur_s"
|
|
5419
|
+
# Log model used for prediction feedback
|
|
5420
|
+
echo "${id}|${stage_model_used}|false" >> "${ARTIFACTS_DIR}/model-routing.log"
|
|
3363
5421
|
return 1
|
|
3364
5422
|
fi
|
|
3365
|
-
done <<< "$stages"
|
|
5423
|
+
done 3<<< "$stages"
|
|
3366
5424
|
|
|
3367
5425
|
# Pipeline complete!
|
|
3368
5426
|
update_status "complete" ""
|
|
@@ -3388,8 +5446,8 @@ run_pipeline() {
|
|
|
3388
5446
|
echo ""
|
|
3389
5447
|
|
|
3390
5448
|
# Capture learnings to memory (success or failure)
|
|
3391
|
-
if [[ -x "$SCRIPT_DIR/
|
|
3392
|
-
bash "$SCRIPT_DIR/
|
|
5449
|
+
if [[ -x "$SCRIPT_DIR/sw-memory.sh" ]]; then
|
|
5450
|
+
bash "$SCRIPT_DIR/sw-memory.sh" capture "$STATE_FILE" "$ARTIFACTS_DIR" 2>/dev/null || true
|
|
3393
5451
|
fi
|
|
3394
5452
|
|
|
3395
5453
|
# Final GitHub progress update
|
|
@@ -3482,7 +5540,7 @@ pipeline_start() {
|
|
|
3482
5540
|
# Register worktree cleanup on exit (chain with existing cleanup)
|
|
3483
5541
|
if [[ "$CLEANUP_WORKTREE" == "true" ]]; then
|
|
3484
5542
|
trap 'pipeline_cleanup_worktree; cleanup_on_exit' SIGINT SIGTERM
|
|
3485
|
-
trap 'pipeline_cleanup_worktree' EXIT
|
|
5543
|
+
trap 'pipeline_cleanup_worktree; cleanup_on_exit' EXIT
|
|
3486
5544
|
fi
|
|
3487
5545
|
|
|
3488
5546
|
setup_dirs
|
|
@@ -3508,6 +5566,30 @@ pipeline_start() {
|
|
|
3508
5566
|
load_pipeline_config
|
|
3509
5567
|
initialize_state
|
|
3510
5568
|
|
|
5569
|
+
# CI resume: restore branch + goal context when intake is skipped
|
|
5570
|
+
if [[ -n "${COMPLETED_STAGES:-}" ]] && echo "$COMPLETED_STAGES" | tr ',' '\n' | grep -qx "intake"; then
|
|
5571
|
+
# Intake was completed in a previous run — restore context
|
|
5572
|
+
# The workflow merges the partial work branch, so code changes are on HEAD
|
|
5573
|
+
|
|
5574
|
+
# Restore GOAL from issue if not already set
|
|
5575
|
+
if [[ -z "$GOAL" && -n "$ISSUE_NUMBER" ]]; then
|
|
5576
|
+
GOAL=$(gh issue view "$ISSUE_NUMBER" --json title -q .title 2>/dev/null || echo "Issue #${ISSUE_NUMBER}")
|
|
5577
|
+
info "CI resume: goal from issue — ${GOAL}"
|
|
5578
|
+
fi
|
|
5579
|
+
|
|
5580
|
+
# Restore branch context
|
|
5581
|
+
if [[ -z "$GIT_BRANCH" ]]; then
|
|
5582
|
+
local ci_branch="ci/issue-${ISSUE_NUMBER}"
|
|
5583
|
+
info "CI resume: creating branch ${ci_branch} from current HEAD"
|
|
5584
|
+
git checkout -b "$ci_branch" 2>/dev/null || git checkout "$ci_branch" 2>/dev/null || true
|
|
5585
|
+
GIT_BRANCH="$ci_branch"
|
|
5586
|
+
elif [[ "$(git branch --show-current 2>/dev/null)" != "$GIT_BRANCH" ]]; then
|
|
5587
|
+
info "CI resume: checking out branch ${GIT_BRANCH}"
|
|
5588
|
+
git checkout -b "$GIT_BRANCH" 2>/dev/null || git checkout "$GIT_BRANCH" 2>/dev/null || true
|
|
5589
|
+
fi
|
|
5590
|
+
write_state 2>/dev/null || true
|
|
5591
|
+
fi
|
|
5592
|
+
|
|
3511
5593
|
echo ""
|
|
3512
5594
|
echo -e "${PURPLE}${BOLD}╔═══════════════════════════════════════════════════════════════════╗${RESET}"
|
|
3513
5595
|
echo -e "${PURPLE}${BOLD}║ shipwright pipeline — Autonomous Feature Delivery ║${RESET}"
|
|
@@ -3556,6 +5638,21 @@ pipeline_start() {
|
|
|
3556
5638
|
return 0
|
|
3557
5639
|
fi
|
|
3558
5640
|
|
|
5641
|
+
# Start background heartbeat writer
|
|
5642
|
+
start_heartbeat
|
|
5643
|
+
|
|
5644
|
+
# Initialize GitHub Check Runs for all pipeline stages
|
|
5645
|
+
if [[ "${NO_GITHUB:-false}" != "true" ]] && type gh_checks_pipeline_start &>/dev/null 2>&1; then
|
|
5646
|
+
local head_sha
|
|
5647
|
+
head_sha=$(git rev-parse HEAD 2>/dev/null || echo "")
|
|
5648
|
+
if [[ -n "$head_sha" && -n "$REPO_OWNER" && -n "$REPO_NAME" ]]; then
|
|
5649
|
+
local stages_json
|
|
5650
|
+
stages_json=$(jq -c '[.stages[] | select(.enabled == true) | .id]' "$PIPELINE_CONFIG" 2>/dev/null || echo '[]')
|
|
5651
|
+
gh_checks_pipeline_start "$REPO_OWNER" "$REPO_NAME" "$head_sha" "$stages_json" >/dev/null 2>/dev/null || true
|
|
5652
|
+
info "GitHub Checks: created check runs for pipeline stages"
|
|
5653
|
+
fi
|
|
5654
|
+
fi
|
|
5655
|
+
|
|
3559
5656
|
# Send start notification
|
|
3560
5657
|
notify "Pipeline Started" "Goal: ${GOAL}\nPipeline: ${PIPELINE_NAME}" "info"
|
|
3561
5658
|
|
|
@@ -3597,12 +5694,56 @@ pipeline_start() {
|
|
|
3597
5694
|
"self_heal_count=$SELF_HEAL_COUNT"
|
|
3598
5695
|
|
|
3599
5696
|
# Capture failure learnings to memory
|
|
3600
|
-
if [[ -x "$SCRIPT_DIR/
|
|
3601
|
-
bash "$SCRIPT_DIR/
|
|
3602
|
-
bash "$SCRIPT_DIR/
|
|
5697
|
+
if [[ -x "$SCRIPT_DIR/sw-memory.sh" ]]; then
|
|
5698
|
+
bash "$SCRIPT_DIR/sw-memory.sh" capture "$STATE_FILE" "$ARTIFACTS_DIR" 2>/dev/null || true
|
|
5699
|
+
bash "$SCRIPT_DIR/sw-memory.sh" analyze-failure "$ARTIFACTS_DIR/.claude-tokens-${CURRENT_STAGE_ID:-build}.log" "${CURRENT_STAGE_ID:-unknown}" 2>/dev/null || true
|
|
3603
5700
|
fi
|
|
3604
5701
|
fi
|
|
3605
5702
|
|
|
5703
|
+
# ── Prediction Validation Events ──
|
|
5704
|
+
# Compare predicted vs actual outcomes for feedback loop calibration
|
|
5705
|
+
local pipeline_success="false"
|
|
5706
|
+
[[ "$exit_code" -eq 0 ]] && pipeline_success="true"
|
|
5707
|
+
|
|
5708
|
+
# Complexity prediction vs actual iterations
|
|
5709
|
+
emit_event "prediction.validated" \
|
|
5710
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
5711
|
+
"predicted_complexity=${INTELLIGENCE_COMPLEXITY:-0}" \
|
|
5712
|
+
"actual_iterations=$SELF_HEAL_COUNT" \
|
|
5713
|
+
"success=$pipeline_success"
|
|
5714
|
+
|
|
5715
|
+
# Template outcome tracking
|
|
5716
|
+
emit_event "template.outcome" \
|
|
5717
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
5718
|
+
"template=${PIPELINE_NAME}" \
|
|
5719
|
+
"success=$pipeline_success" \
|
|
5720
|
+
"duration_s=${total_dur_s:-0}"
|
|
5721
|
+
|
|
5722
|
+
# Risk prediction vs actual failure
|
|
5723
|
+
local predicted_risk="${INTELLIGENCE_RISK_SCORE:-0}"
|
|
5724
|
+
emit_event "risk.outcome" \
|
|
5725
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
5726
|
+
"predicted_risk=$predicted_risk" \
|
|
5727
|
+
"actual_failure=$([[ "$exit_code" -ne 0 ]] && echo "true" || echo "false")"
|
|
5728
|
+
|
|
5729
|
+
# Per-stage model outcome events (read from stage timings)
|
|
5730
|
+
local routing_log="${ARTIFACTS_DIR}/model-routing.log"
|
|
5731
|
+
if [[ -f "$routing_log" ]]; then
|
|
5732
|
+
while IFS='|' read -r s_stage s_model s_success; do
|
|
5733
|
+
[[ -z "$s_stage" ]] && continue
|
|
5734
|
+
emit_event "model.outcome" \
|
|
5735
|
+
"issue=${ISSUE_NUMBER:-0}" \
|
|
5736
|
+
"stage=$s_stage" \
|
|
5737
|
+
"model=$s_model" \
|
|
5738
|
+
"success=$s_success"
|
|
5739
|
+
done < "$routing_log"
|
|
5740
|
+
fi
|
|
5741
|
+
|
|
5742
|
+
# Record pipeline outcome for model routing feedback loop
|
|
5743
|
+
if type optimize_analyze_outcome &>/dev/null 2>&1; then
|
|
5744
|
+
optimize_analyze_outcome "$STATE_FILE" 2>/dev/null || true
|
|
5745
|
+
fi
|
|
5746
|
+
|
|
3606
5747
|
# Emit cost event
|
|
3607
5748
|
local model_key="${MODEL:-sonnet}"
|
|
3608
5749
|
local input_cost output_cost total_cost
|
|
@@ -3754,7 +5895,7 @@ pipeline_abort() {
|
|
|
3754
5895
|
pipeline_list() {
|
|
3755
5896
|
local locations=(
|
|
3756
5897
|
"$REPO_DIR/templates/pipelines"
|
|
3757
|
-
"$HOME/.
|
|
5898
|
+
"$HOME/.shipwright/pipelines"
|
|
3758
5899
|
)
|
|
3759
5900
|
|
|
3760
5901
|
echo ""
|
|
@@ -3832,7 +5973,7 @@ case "$SUBCOMMAND" in
|
|
|
3832
5973
|
show) pipeline_show ;;
|
|
3833
5974
|
test)
|
|
3834
5975
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
3835
|
-
exec "$SCRIPT_DIR/
|
|
5976
|
+
exec "$SCRIPT_DIR/sw-pipeline-test.sh" "$@"
|
|
3836
5977
|
;;
|
|
3837
5978
|
help|--help|-h) show_help ;;
|
|
3838
5979
|
*)
|