forgedock 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -8
- package/bin/batch-runner.mjs +339 -0
- package/bin/demo.mjs +92 -7
- package/bin/engine/phases.mjs +24 -2
- package/bin/engine/state.mjs +8 -3
- package/bin/engine-cli.mjs +177 -5
- package/bin/forgedock.mjs +409 -26
- package/bin/hooks/interactive-engine.mjs +509 -0
- package/bin/hooks/pre-tool-use.mjs +498 -0
- package/bin/hooks/subagent-stop-enforce.mjs +197 -0
- package/bin/journey.mjs +72 -14
- package/bin/registry.mjs +139 -0
- package/bin/report.mjs +447 -0
- package/bin/runner.mjs +70 -20
- package/bin/settings-hook.mjs +235 -1
- package/commands/adopt.md +4 -1
- package/commands/analytics.md +4 -1
- package/commands/audit-agents.md +110 -27
- package/commands/audit.md +20 -1
- package/commands/autopilot.md +518 -361
- package/commands/changelog.md +2 -1
- package/commands/ci-audit.md +2 -1
- package/commands/cleanup.md +161 -1
- package/commands/compat-audit.md +2 -1
- package/commands/deploy-info.md +58 -1
- package/commands/deploy-pr.md +485 -0
- package/commands/design-bench.md +5 -1
- package/commands/design-render-critique-loop.md +1 -1
- package/commands/diagnose.md +2 -1
- package/commands/explain.md +2 -1
- package/commands/fix-ci.md +577 -0
- package/commands/forge-stats.md +121 -2
- package/commands/forgedock-init.md +1 -1
- package/commands/geo-audit.md +3 -0
- package/commands/incident-response.md +2 -1
- package/commands/issue.md +23 -2
- package/commands/milestone.md +77 -12
- package/commands/optimize.md +2 -1
- package/commands/orchestrate/config.md +67 -0
- package/commands/orchestrate/phase-1-resolve.md +159 -0
- package/commands/orchestrate/phase-2-triage.md +126 -0
- package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
- package/commands/orchestrate/phase-3-dependency.md +591 -0
- package/commands/orchestrate/phase-4-execution.md +962 -0
- package/commands/orchestrate/phase-5-cleanup.md +47 -0
- package/commands/orchestrate/phase-6-report.md +198 -0
- package/commands/orchestrate/safety.md +62 -0
- package/commands/orchestrate.md +25 -2045
- package/commands/pipeline-health/config.md +34 -0
- package/commands/pipeline-health/phase-1-context.md +180 -0
- package/commands/pipeline-health/phase-2-metrics.md +1729 -0
- package/commands/pipeline-health/phase-3-analyze.md +204 -0
- package/commands/pipeline-health/phase-4-proposals.md +117 -0
- package/commands/pipeline-health/phase-5-report.md +284 -0
- package/commands/pipeline-health/phase-6-summary.md +52 -0
- package/commands/pipeline-health.md +25 -2401
- package/commands/pipeline-resume.md +24 -1
- package/commands/pipeline-status.md +65 -0
- package/commands/qa-sweep.md +4 -1
- package/commands/quality-gate.md +248 -10
- package/commands/recover-orphans.md +576 -0
- package/commands/replay.md +2 -1
- package/commands/review-pr-agents/api.md +135 -0
- package/commands/review-pr-agents/auth.md +137 -0
- package/commands/review-pr-agents/billing.md +101 -0
- package/commands/review-pr-agents/concurrency.md +145 -0
- package/commands/review-pr-agents/database.md +103 -0
- package/commands/review-pr-agents/frontend.md +89 -0
- package/commands/review-pr-agents/infra.md +385 -0
- package/commands/review-pr-agents/protocols.md +185 -0
- package/commands/review-pr-agents/scraper.md +185 -0
- package/commands/review-pr-agents/security.md +406 -0
- package/commands/review-pr-agents.md +45 -1722
- package/commands/review-pr-staging.md +196 -40
- package/commands/review-pr.md +591 -52
- package/commands/rollback.md +2 -1
- package/commands/scope.md +2 -1
- package/commands/security-audit.md +2 -1
- package/commands/signal-planner.md +623 -0
- package/commands/sync-ecosystem.md +1 -0
- package/commands/test-gate.md +17 -1
- package/commands/validate.md +2 -1
- package/commands/work-on/build/architect.md +28 -0
- package/commands/work-on/build/context.md +2 -0
- package/commands/work-on/build/implement.md +3 -1
- package/commands/work-on/build/validate.md +26 -7
- package/commands/work-on/build.md +163 -24
- package/commands/work-on/close.md +130 -3
- package/commands/work-on/decompose.md +19 -1
- package/commands/work-on/investigate.md +117 -2
- package/commands/work-on/review.md +24 -4
- package/commands/work-on-monolithic.md +7 -2
- package/commands/work-on.md +202 -4
- package/docs/BRAND.md +14 -7
- package/docs/CI.md +16 -2
- package/docs/CODEX.md +36 -1
- package/docs/CONFIG.md +5 -8
- package/docs/FORGE-PROTOCOL.md +15 -1064
- package/docs/anthropic-devrel-outreach.md +2 -2
- package/docs/articles/command-decomposition-token-savings.md +214 -0
- package/docs/articles/model-release-playbook.md +131 -0
- package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
- package/docs/claude-breakpoints.json +75 -5
- package/docs/design/abc-benchmark.md +1 -1
- package/docs/design/design-spec-schema.md +1 -1
- package/docs/design/divergent-generation.md +1 -1
- package/docs/design/eval-gate-runbook.md +178 -0
- package/docs/design/show-hn-draft.md +26 -1
- package/docs/design/token-cost-baseline-1298.md +59 -0
- package/docs/eval/README.md +120 -0
- package/docs/eval/corpus/seed-v1.json +6 -0
- package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
- package/docs/site/annotations-explained.md +24 -0
- package/docs/site/command-learning-path.md +51 -14
- package/docs/site/command-reference.md +386 -111
- package/docs/site/for-companies.md +95 -0
- package/docs/site/forge-annotation-protocol.md +12 -256
- package/docs/site/getting-started.md +43 -4
- package/docs/site/github-is-the-memory.md +113 -0
- package/docs/site/how-it-works.md +3 -13
- package/docs/site/index.md +9 -1
- package/docs/spec/eval-run-result.md +147 -0
- package/docs/spec/forge-protocol-v1.md +441 -18
- package/docs/spec/label-state-machine.md +55 -0
- package/docs/spec/review-protocol.md +158 -0
- package/docs/spec-graph-schema.md +28 -0
- package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
- package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
- package/examples/forgedock-demo/README.md +41 -22
- package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
- package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
- package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
- package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
- package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
- package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
- package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
- package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
- package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
- package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
- package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
- package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
- package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
- package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
- package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
- package/examples/forgedock-demo/scripts/smoke.js +18 -1
- package/examples/forgedock-demo/src/db.js +36 -11
- package/examples/forgedock-demo/src/routes/notes.js +106 -9
- package/examples/forgedock-demo/src/server.js +4 -1
- package/package.json +3 -1
- package/packages/protocol/LICENSE +21 -0
- package/packages/protocol/src/cli.js +135 -0
- package/packages/protocol/src/emit.js +146 -0
- package/packages/protocol/src/index.js +43 -0
- package/packages/protocol/src/parse.js +254 -0
- package/packages/protocol/src/types.js +171 -0
- package/packages/protocol/src/validate.js +122 -0
- package/scripts/bench-topology-cost.mjs +372 -0
- package/scripts/check-command-docs-drift.sh +198 -0
- package/scripts/check-protocol-restatements.sh +176 -0
- package/scripts/check-registry/README.md +72 -0
- package/scripts/check-registry/manifest.json +6 -0
- package/scripts/code-index.sh +454 -0
- package/scripts/conformance-check.mjs +168 -0
- package/scripts/doctor-pipeline-state.sh +484 -0
- package/scripts/eval-gate-baseline.json +17 -0
- package/scripts/eval-gate-scorecard.mjs +242 -0
- package/scripts/eval-scorecard-aggregator.mjs +252 -0
- package/scripts/flaky-quarantine.sh +241 -0
- package/scripts/forge-annotation.sh +567 -0
- package/scripts/issue-dedup.sh +236 -0
- package/scripts/validate-annotation-node.mjs +233 -0
- package/scripts/worktree-lifecycle.sh +3 -3
- package/templates/devdocs/agent/using-forgedock.md +14 -0
- package/docs/site/.vitepress/config.mjs +0 -85
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Automated CI failure resolution loop — diagnoses failures on a PR, applies targeted fixes, pushes, and loops until green or max attempts reached.
|
|
3
|
+
argument-hint: <PR number or URL> [--max-attempts N] [--repo owner/repo]
|
|
4
|
+
---
|
|
5
|
+
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
6
|
+
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
|
7
|
+
|
|
8
|
+
# /fix-ci — Automated CI Failure Resolution
|
|
9
|
+
|
|
10
|
+
**Input**: $ARGUMENTS
|
|
11
|
+
|
|
12
|
+
**Invoked by**: `/deploy-pr` (CI gate step) or directly by a user when a PR's CI is failing.
|
|
13
|
+
|
|
14
|
+
**Output**: Structured result — `{ pr, status, attempts, fixes_applied }` — printed to stdout and available to the calling skill.
|
|
15
|
+
|
|
16
|
+
**Agent model policy**: `model: "sonnet"` (standard tier). Fallback: `model: "opus"` if rate-limited. User can override with `--model <name>`. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154.
|
|
17
|
+
**NEVER use plan mode (EnterPlanMode).**
|
|
18
|
+
**NEVER force-push or amend commits** — always create new commits.
|
|
19
|
+
|
|
20
|
+
<!-- FORGE:SPEC_LOADED — fix-ci.md loaded and active. Agent is bound by this spec. -->
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Config Preamble
|
|
25
|
+
|
|
26
|
+
Read `forge.yaml` before running any phase:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
|
|
30
|
+
if [ ! -f "$CONFIG_FILE" ]; then
|
|
31
|
+
echo "ERROR: forge.yaml not found. Run: npx forgedock init"
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$CONFIG_FILE")
|
|
35
|
+
GH_FLAG="-R ${GH_REPO}"
|
|
36
|
+
REPO_PATH=$(yq '.paths.root' "$CONFIG_FILE")
|
|
37
|
+
STAGING_BRANCH=$(yq '.branches.staging // "staging"' "$CONFIG_FILE")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Argument Parsing
|
|
41
|
+
|
|
42
|
+
Parse `$ARGUMENTS`:
|
|
43
|
+
|
|
44
|
+
| Argument | Variable | Default |
|
|
45
|
+
|----------|----------|---------|
|
|
46
|
+
| PR number or URL | `PR_NUMBER` | (required) |
|
|
47
|
+
| `--max-attempts N` | `MAX_ATTEMPTS` | `5` |
|
|
48
|
+
| `--repo owner/repo` | `GH_REPO` | from forge.yaml |
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Extract PR number (accept raw number or full GitHub URL)
|
|
52
|
+
PR_INPUT=$(echo "$ARGUMENTS" | awk '{print $1}')
|
|
53
|
+
if echo "$PR_INPUT" | grep -qE '^https?://'; then
|
|
54
|
+
PR_NUMBER=$(echo "$PR_INPUT" | grep -oE '[0-9]+$')
|
|
55
|
+
else
|
|
56
|
+
PR_NUMBER=$(echo "$PR_INPUT" | tr -d '#')
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
MAX_ATTEMPTS=5
|
|
60
|
+
if echo "$ARGUMENTS" | grep -q -- "--max-attempts"; then
|
|
61
|
+
MAX_ATTEMPTS=$(echo "$ARGUMENTS" | grep -oE -- '--max-attempts [0-9]+' | awk '{print $2}')
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
if [ -z "$PR_NUMBER" ]; then
|
|
65
|
+
echo "ERROR: PR number or URL is required."
|
|
66
|
+
echo "Usage: /fix-ci <PR number or URL> [--max-attempts N]"
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
echo "fix-ci: PR #${PR_NUMBER}, max attempts: ${MAX_ATTEMPTS}"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Phase 0: Assess the PR
|
|
76
|
+
|
|
77
|
+
### 0A: Load PR state
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
PR_DATA=$(gh pr view "$PR_NUMBER" $GH_FLAG \
|
|
81
|
+
--json number,title,headRefName,baseRefName,state,isDraft,url,headRepository 2>/dev/null)
|
|
82
|
+
|
|
83
|
+
if [ -z "$PR_DATA" ]; then
|
|
84
|
+
echo "ERROR: PR #${PR_NUMBER} not found or not accessible."
|
|
85
|
+
exit 1
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
PR_HEAD=$(echo "$PR_DATA" | jq -r '.headRefName')
|
|
89
|
+
PR_BASE=$(echo "$PR_DATA" | jq -r '.baseRefName')
|
|
90
|
+
PR_STATE=$(echo "$PR_DATA" | jq -r '.state')
|
|
91
|
+
PR_URL=$(echo "$PR_DATA" | jq -r '.url')
|
|
92
|
+
|
|
93
|
+
echo "PR #${PR_NUMBER}: ${PR_HEAD} → ${PR_BASE} (${PR_STATE})"
|
|
94
|
+
|
|
95
|
+
if [ "$PR_STATE" = "MERGED" ]; then
|
|
96
|
+
echo "PR #${PR_NUMBER} is already merged — nothing to fix."
|
|
97
|
+
echo '{"pr": '"$PR_NUMBER"', "status": "already_merged", "attempts": 0, "fixes_applied": []}'
|
|
98
|
+
exit 0
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
if [ "$PR_STATE" = "CLOSED" ]; then
|
|
102
|
+
echo "PR #${PR_NUMBER} is closed — cannot fix CI on a closed PR."
|
|
103
|
+
exit 1
|
|
104
|
+
fi
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 0B: Determine fix target branch
|
|
108
|
+
|
|
109
|
+
The fix target is the branch the PR's **head** points to. CI fixes are committed directly to the PR's head branch — not to the base branch.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Smart target detection: where do fixes land?
|
|
113
|
+
# - staging → main PRs: fixes go to staging (the head branch)
|
|
114
|
+
# - milestone/* → staging: fixes go to the milestone branch (the head branch)
|
|
115
|
+
# - feature/* PRs: fixes go to the feature branch (the head branch)
|
|
116
|
+
# In all cases: FIX_TARGET = PR_HEAD (the PR's source branch)
|
|
117
|
+
FIX_TARGET="$PR_HEAD"
|
|
118
|
+
echo "Fix target branch: ${FIX_TARGET}"
|
|
119
|
+
|
|
120
|
+
# HARD RULE: never target main directly
|
|
121
|
+
if [ "$FIX_TARGET" = "main" ] || [ "$FIX_TARGET" = "master" ]; then
|
|
122
|
+
echo "ERROR: fix-ci refuses to commit fixes directly to '${FIX_TARGET}'."
|
|
123
|
+
echo "This PR's head branch is a protected branch — something is wrong with the PR setup."
|
|
124
|
+
exit 1
|
|
125
|
+
fi
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 0C: Check initial CI status
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
LATEST_RUN=$(gh run list $GH_FLAG --branch "$PR_HEAD" --limit 1 \
|
|
132
|
+
--json databaseId,status,conclusion,name,createdAt 2>/dev/null | jq '.[0] // empty')
|
|
133
|
+
|
|
134
|
+
if [ -z "$LATEST_RUN" ]; then
|
|
135
|
+
echo "No CI runs found for branch ${PR_HEAD} — CI may not have triggered yet."
|
|
136
|
+
echo "Waiting up to 60s for CI to start..."
|
|
137
|
+
sleep 15
|
|
138
|
+
LATEST_RUN=$(gh run list $GH_FLAG --branch "$PR_HEAD" --limit 1 \
|
|
139
|
+
--json databaseId,status,conclusion,name,createdAt 2>/dev/null | jq '.[0] // empty')
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
RUN_STATUS=$(echo "$LATEST_RUN" | jq -r '.status // "unknown"')
|
|
143
|
+
RUN_CONCLUSION=$(echo "$LATEST_RUN" | jq -r '.conclusion // "unknown"')
|
|
144
|
+
RUN_ID=$(echo "$LATEST_RUN" | jq -r '.databaseId // ""')
|
|
145
|
+
|
|
146
|
+
echo "Latest CI run ${RUN_ID}: status=${RUN_STATUS}, conclusion=${RUN_CONCLUSION}"
|
|
147
|
+
|
|
148
|
+
if [ "$RUN_CONCLUSION" = "success" ]; then
|
|
149
|
+
echo "CI is already green on PR #${PR_NUMBER} — no fixes needed."
|
|
150
|
+
echo '{"pr": '"$PR_NUMBER"', "status": "success", "attempts": 0, "fixes_applied": []}'
|
|
151
|
+
exit 0
|
|
152
|
+
fi
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Phase 1: CI Fix Loop
|
|
158
|
+
|
|
159
|
+
Initialize tracking state:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
ATTEMPT=0
|
|
163
|
+
FIXES_APPLIED="[]"
|
|
164
|
+
LOOP_STATUS="pending"
|
|
165
|
+
|
|
166
|
+
cd "$REPO_PATH"
|
|
167
|
+
git fetch origin
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Execute up to `MAX_ATTEMPTS` iterations:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
while ATTEMPT < MAX_ATTEMPTS:
|
|
174
|
+
ATTEMPT += 1
|
|
175
|
+
echo "=== fix-ci: Attempt ${ATTEMPT}/${MAX_ATTEMPTS} ==="
|
|
176
|
+
|
|
177
|
+
→ Phase 1A: Wait for CI to finish
|
|
178
|
+
→ Phase 1B: Check CI result — if green, exit loop with success
|
|
179
|
+
→ Phase 1C: Diagnose failure
|
|
180
|
+
→ Phase 1D: Apply fix
|
|
181
|
+
→ Phase 1E: Format, commit, push
|
|
182
|
+
→ Loop back (CI re-triggers on new commit)
|
|
183
|
+
|
|
184
|
+
if loop exits without success AND ATTEMPT == MAX_ATTEMPTS:
|
|
185
|
+
→ Phase 2: Report max-attempts failure
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Phase 1A: Wait for CI
|
|
189
|
+
|
|
190
|
+
Poll the latest run on the PR's head branch. Poll every 20 seconds, timeout at 10 minutes.
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
POLL_TIMEOUT=600 # 10 minutes
|
|
194
|
+
POLL_START=$(date +%s)
|
|
195
|
+
POLL_INTERVAL=20
|
|
196
|
+
|
|
197
|
+
echo "Waiting for CI run to complete (timeout: ${POLL_TIMEOUT}s)..."
|
|
198
|
+
|
|
199
|
+
while true; do
|
|
200
|
+
CURRENT_RUN=$(gh run list $GH_FLAG --branch "$PR_HEAD" --limit 1 \
|
|
201
|
+
--json databaseId,status,conclusion 2>/dev/null | jq '.[0] // empty')
|
|
202
|
+
|
|
203
|
+
CURRENT_STATUS=$(echo "$CURRENT_RUN" | jq -r '.status // "unknown"')
|
|
204
|
+
CURRENT_CONCLUSION=$(echo "$CURRENT_RUN" | jq -r '.conclusion // "unknown"')
|
|
205
|
+
CURRENT_RUN_ID=$(echo "$CURRENT_RUN" | jq -r '.databaseId // ""')
|
|
206
|
+
|
|
207
|
+
if [ "$CURRENT_STATUS" = "completed" ]; then
|
|
208
|
+
echo "CI run ${CURRENT_RUN_ID} completed: ${CURRENT_CONCLUSION}"
|
|
209
|
+
break
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
NOW=$(date +%s)
|
|
213
|
+
ELAPSED=$((NOW - POLL_START))
|
|
214
|
+
if [ "$ELAPSED" -ge "$POLL_TIMEOUT" ]; then
|
|
215
|
+
echo "Timeout waiting for CI after ${POLL_TIMEOUT}s — run ${CURRENT_RUN_ID} is still ${CURRENT_STATUS}"
|
|
216
|
+
LOOP_STATUS="timeout"
|
|
217
|
+
break
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
echo " CI run ${CURRENT_RUN_ID} is ${CURRENT_STATUS} (${ELAPSED}s elapsed) — polling again in ${POLL_INTERVAL}s..."
|
|
221
|
+
sleep "$POLL_INTERVAL"
|
|
222
|
+
done
|
|
223
|
+
|
|
224
|
+
RUN_CONCLUSION="$CURRENT_CONCLUSION"
|
|
225
|
+
RUN_ID="$CURRENT_RUN_ID"
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Phase 1B: Check CI result
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
if [ "$RUN_CONCLUSION" = "success" ]; then
|
|
232
|
+
echo "CI is green after ${ATTEMPT} attempt(s)."
|
|
233
|
+
LOOP_STATUS="success"
|
|
234
|
+
break # exit the fix loop
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
if [ "$LOOP_STATUS" = "timeout" ]; then
|
|
238
|
+
break # exit the fix loop — reported in Phase 2
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
echo "CI failed (run ${RUN_ID}, conclusion: ${RUN_CONCLUSION}) — diagnosing..."
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Phase 1C: Diagnose the failure
|
|
245
|
+
|
|
246
|
+
Download failed job logs and pattern-match against known failure classes.
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
# Download logs from the failed run
|
|
250
|
+
RUN_LOG=$(gh run view "$RUN_ID" $GH_FLAG --log-failed 2>/dev/null | head -500)
|
|
251
|
+
|
|
252
|
+
if [ -z "$RUN_LOG" ]; then
|
|
253
|
+
echo "WARNING: Could not retrieve logs for run ${RUN_ID}"
|
|
254
|
+
RUN_LOG="(logs unavailable)"
|
|
255
|
+
fi
|
|
256
|
+
|
|
257
|
+
echo "--- Failed run log (first 500 lines) ---"
|
|
258
|
+
echo "$RUN_LOG" | head -100
|
|
259
|
+
echo "---"
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Pattern-match against known failure classes** (in priority order — first match wins):
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
FAILURE_CLASS="unknown"
|
|
266
|
+
FAILURE_DETAIL=""
|
|
267
|
+
|
|
268
|
+
# 1. Format errors (prettier, black, ruff, eslint --fix-dry-run)
|
|
269
|
+
if echo "$RUN_LOG" | grep -qiE "(formatting|format check|prettier|black|ruff.*would reformat|eslint.*fixable)"; then
|
|
270
|
+
FAILURE_CLASS="format"
|
|
271
|
+
FAILURE_DETAIL=$(echo "$RUN_LOG" | grep -iE "(formatting|would reformat|fixable)" | head -5)
|
|
272
|
+
|
|
273
|
+
# 2. Import/module errors (missing imports, circular imports, unresolved modules)
|
|
274
|
+
elif echo "$RUN_LOG" | grep -qiE "(ImportError|ModuleNotFoundError|Cannot find module|import.*error|no module named)"; then
|
|
275
|
+
FAILURE_CLASS="import"
|
|
276
|
+
FAILURE_DETAIL=$(echo "$RUN_LOG" | grep -iE "(ImportError|ModuleNotFoundError|Cannot find module|no module named)" | head -5)
|
|
277
|
+
|
|
278
|
+
# 3. Syntax errors (Python, JS/TS, YAML)
|
|
279
|
+
elif echo "$RUN_LOG" | grep -qiE "(SyntaxError|syntax error|Unexpected token|unexpected end of|ParseError)"; then
|
|
280
|
+
FAILURE_CLASS="syntax"
|
|
281
|
+
FAILURE_DETAIL=$(echo "$RUN_LOG" | grep -iE "(SyntaxError|syntax error|Unexpected token|ParseError)" | head -5)
|
|
282
|
+
|
|
283
|
+
# 4. TypeScript/type errors
|
|
284
|
+
elif echo "$RUN_LOG" | grep -qiE "(TS[0-9]+:|type error|TypeScript error|tsc.*error|type '.*' is not assignable)"; then
|
|
285
|
+
FAILURE_CLASS="types"
|
|
286
|
+
FAILURE_DETAIL=$(echo "$RUN_LOG" | grep -iE "(TS[0-9]+:|type error|tsc.*error)" | head -5)
|
|
287
|
+
|
|
288
|
+
# 5. Test failures (pytest, jest, mocha)
|
|
289
|
+
elif echo "$RUN_LOG" | grep -qiE "(FAILED|test.*failed|AssertionError|expect.*received|FAIL .*\.test\.)"; then
|
|
290
|
+
FAILURE_CLASS="tests"
|
|
291
|
+
FAILURE_DETAIL=$(echo "$RUN_LOG" | grep -iE "(FAILED|AssertionError|expect.*received)" | head -5)
|
|
292
|
+
|
|
293
|
+
# 6. Build errors (webpack, vite, esbuild, cargo, maven)
|
|
294
|
+
elif echo "$RUN_LOG" | grep -qiE "(Build failed|build error|compilation failed|error\[E[0-9]+\]|BUILD FAILURE)"; then
|
|
295
|
+
FAILURE_CLASS="build"
|
|
296
|
+
FAILURE_DETAIL=$(echo "$RUN_LOG" | grep -iE "(Build failed|compilation failed|BUILD FAILURE)" | head -5)
|
|
297
|
+
|
|
298
|
+
# 7. Linting errors (eslint, pylint, flake8)
|
|
299
|
+
elif echo "$RUN_LOG" | grep -qiE "(eslint.*error|pylint|flake8|linting.*error|lint.*fail)"; then
|
|
300
|
+
FAILURE_CLASS="lint"
|
|
301
|
+
FAILURE_DETAIL=$(echo "$RUN_LOG" | grep -iE "(eslint|pylint|flake8)" | head -5)
|
|
302
|
+
|
|
303
|
+
# 8. YAML/config validation errors
|
|
304
|
+
elif echo "$RUN_LOG" | grep -qiE "(yaml.*error|YAML parse|invalid yaml|config.*invalid|schema.*error)"; then
|
|
305
|
+
FAILURE_CLASS="config"
|
|
306
|
+
FAILURE_DETAIL=$(echo "$RUN_LOG" | grep -iE "(yaml.*error|config.*invalid|schema.*error)" | head -5)
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
echo "Failure class: ${FAILURE_CLASS}"
|
|
310
|
+
echo "Failure detail: ${FAILURE_DETAIL}"
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Phase 1D: Apply targeted fix
|
|
314
|
+
|
|
315
|
+
Work on the PR's head branch directly. Check out the branch in the repo root (not a worktree — we push to the existing PR head):
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
git checkout "$FIX_TARGET" 2>/dev/null || git checkout -b "$FIX_TARGET" "origin/${FIX_TARGET}" 2>/dev/null
|
|
319
|
+
git pull origin "$FIX_TARGET" --rebase 2>/dev/null || true
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Apply fix based on failure class**:
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
case FAILURE_CLASS:
|
|
326
|
+
|
|
327
|
+
"format":
|
|
328
|
+
Read forge.yaml → verification.commands for configured formatters.
|
|
329
|
+
Run all configured formatters:
|
|
330
|
+
PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null)
|
|
331
|
+
TS_FORMAT=$(yq '.verification.commands.typescript.format // ""' forge.yaml 2>/dev/null)
|
|
332
|
+
[ -n "$PYTHON_FORMAT" ] && eval "$PYTHON_FORMAT" 2>&1 || true
|
|
333
|
+
[ -n "$TS_FORMAT" ] && eval "$TS_FORMAT" 2>&1 || true
|
|
334
|
+
If no formatters configured, try auto-detection:
|
|
335
|
+
- Python files changed → try: black . 2>/dev/null || ruff format . 2>/dev/null || true
|
|
336
|
+
- JS/TS files changed → try: npx prettier --write . 2>/dev/null || true
|
|
337
|
+
FIX_DESCRIPTION="auto-format: run configured formatters"
|
|
338
|
+
|
|
339
|
+
"import":
|
|
340
|
+
Read FAILURE_DETAIL to identify the missing module name.
|
|
341
|
+
Search the codebase for the module to determine if it is:
|
|
342
|
+
a) A typo in the import → fix the import path
|
|
343
|
+
b) A missing dependency in package.json / requirements.txt → add it
|
|
344
|
+
c) A file that was moved or deleted → update the import path
|
|
345
|
+
Apply the targeted fix with Read + Edit tools.
|
|
346
|
+
FIX_DESCRIPTION="fix import: ${FAILURE_DETAIL}"
|
|
347
|
+
|
|
348
|
+
"syntax":
|
|
349
|
+
Read FAILURE_DETAIL to identify the file and line number.
|
|
350
|
+
Read the affected file with the Read tool.
|
|
351
|
+
Apply targeted fix for the syntax error (missing comma, bracket, quote, etc.).
|
|
352
|
+
FIX_DESCRIPTION="fix syntax error: ${FAILURE_DETAIL}"
|
|
353
|
+
|
|
354
|
+
"types":
|
|
355
|
+
Read FAILURE_DETAIL to extract TS error code and location (file:line).
|
|
356
|
+
Read the affected file.
|
|
357
|
+
Apply targeted type fix: add missing type annotation, correct mismatched type,
|
|
358
|
+
add type assertion, or widen/narrow the type as appropriate.
|
|
359
|
+
FIX_DESCRIPTION="fix type error: ${FAILURE_DETAIL}"
|
|
360
|
+
|
|
361
|
+
"tests":
|
|
362
|
+
Read FAILURE_DETAIL to find failing test name and assertion.
|
|
363
|
+
Read the test file and the source file it tests.
|
|
364
|
+
Determine if the failure is:
|
|
365
|
+
a) A test expectation that needs updating (API changed) → update the expectation
|
|
366
|
+
b) A regression in source code → fix the source code
|
|
367
|
+
NEVER delete or skip a failing test without understanding the root cause.
|
|
368
|
+
FIX_DESCRIPTION="fix failing test: ${FAILURE_DETAIL}"
|
|
369
|
+
|
|
370
|
+
"build":
|
|
371
|
+
Read full build log for the specific error message and file.
|
|
372
|
+
Read the affected file.
|
|
373
|
+
Fix the build error: resolve missing dependency, fix webpack config,
|
|
374
|
+
resolve circular import, fix incompatible API usage.
|
|
375
|
+
FIX_DESCRIPTION="fix build error: ${FAILURE_DETAIL}"
|
|
376
|
+
|
|
377
|
+
"lint":
|
|
378
|
+
Read FAILURE_DETAIL to identify specific lint rule and file.
|
|
379
|
+
Read the affected file.
|
|
380
|
+
Apply targeted lint fix respecting the project's lint configuration.
|
|
381
|
+
Do NOT use disable comments unless the lint rule is a false positive.
|
|
382
|
+
FIX_DESCRIPTION="fix lint error: ${FAILURE_DETAIL}"
|
|
383
|
+
|
|
384
|
+
"config":
|
|
385
|
+
Read FAILURE_DETAIL to identify the config file and the parse error.
|
|
386
|
+
Read the config file.
|
|
387
|
+
Fix the YAML/JSON/TOML syntax error or schema violation.
|
|
388
|
+
FIX_DESCRIPTION="fix config error: ${FAILURE_DETAIL}"
|
|
389
|
+
|
|
390
|
+
"unknown":
|
|
391
|
+
Read the full log (first 200 lines) and attempt to identify the error manually.
|
|
392
|
+
If the error is identifiable: apply a targeted fix and describe it.
|
|
393
|
+
If the error is not fixable automatically:
|
|
394
|
+
LOOP_STATUS="undiagnosable"
|
|
395
|
+
break # exit the fix loop — Phase 2 will report
|
|
396
|
+
FIX_DESCRIPTION="fix unclassified CI error (see log)"
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Record the fix applied:
|
|
400
|
+
```bash
|
|
401
|
+
FIX_ENTRY=$(jq -nc \
|
|
402
|
+
--arg attempt "$ATTEMPT" \
|
|
403
|
+
--arg class "$FAILURE_CLASS" \
|
|
404
|
+
--arg desc "$FIX_DESCRIPTION" \
|
|
405
|
+
--arg run_id "$RUN_ID" \
|
|
406
|
+
'{"attempt": ($attempt|tonumber), "failure_class": $class, "description": $desc, "run_id": $run_id}')
|
|
407
|
+
FIXES_APPLIED=$(echo "$FIXES_APPLIED" | jq ". + [$FIX_ENTRY]")
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Phase 1E: Format, commit, push
|
|
411
|
+
|
|
412
|
+
**Format** (always run before committing, regardless of failure class):
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
# Run formatters unconditionally before commit
|
|
416
|
+
PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
|
|
417
|
+
TS_FORMAT=$(yq '.verification.commands.typescript.format // ""' forge.yaml 2>/dev/null || echo '')
|
|
418
|
+
|
|
419
|
+
[ -n "$PYTHON_FORMAT" ] && eval "$PYTHON_FORMAT" 2>&1 | tail -5 || true
|
|
420
|
+
[ -n "$TS_FORMAT" ] && eval "$TS_FORMAT" 2>&1 | tail -5 || true
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**Commit** (new commit only — no amend, no force-push):
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
git add -u
|
|
427
|
+
CHANGED=$(git diff --cached --name-only)
|
|
428
|
+
|
|
429
|
+
if [ -z "$CHANGED" ]; then
|
|
430
|
+
echo "No changes after fix attempt ${ATTEMPT} — fix did not produce a diff."
|
|
431
|
+
echo "This may indicate the fix was already applied or the failure class is misdiagnosed."
|
|
432
|
+
LOOP_STATUS="no_diff"
|
|
433
|
+
break
|
|
434
|
+
fi
|
|
435
|
+
|
|
436
|
+
git commit -s -m "fix(ci): attempt ${ATTEMPT}/${MAX_ATTEMPTS} — ${FIX_DESCRIPTION} (PR #${PR_NUMBER})"
|
|
437
|
+
echo "Committed fix for attempt ${ATTEMPT}"
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Push** (normal push only — no force):
|
|
441
|
+
|
|
442
|
+
```bash
|
|
443
|
+
git push origin "$FIX_TARGET" 2>&1
|
|
444
|
+
PUSH_EXIT=$?
|
|
445
|
+
|
|
446
|
+
if [ "$PUSH_EXIT" -ne 0 ]; then
|
|
447
|
+
echo "Push failed (exit ${PUSH_EXIT}) — attempting pull-rebase then re-push..."
|
|
448
|
+
git pull origin "$FIX_TARGET" --rebase 2>&1 && \
|
|
449
|
+
git push origin "$FIX_TARGET" 2>&1
|
|
450
|
+
PUSH_EXIT=$?
|
|
451
|
+
fi
|
|
452
|
+
|
|
453
|
+
if [ "$PUSH_EXIT" -ne 0 ]; then
|
|
454
|
+
echo "ERROR: Push failed after retry. Manual intervention required."
|
|
455
|
+
LOOP_STATUS="push_failed"
|
|
456
|
+
break
|
|
457
|
+
fi
|
|
458
|
+
|
|
459
|
+
echo "Pushed fix commit for attempt ${ATTEMPT} — CI will re-trigger on ${FIX_TARGET}"
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**Wait briefly for CI to pick up the new commit** before looping:
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
sleep 10
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
→ Loop back to Phase 1A with `ATTEMPT += 1`.
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## Phase 2: Result Report
|
|
473
|
+
|
|
474
|
+
After the loop exits, determine the final status and report.
|
|
475
|
+
|
|
476
|
+
### 2A: Determine final status
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
case "$LOOP_STATUS" in
|
|
480
|
+
"success")
|
|
481
|
+
FINAL_STATUS="success"
|
|
482
|
+
FINAL_MESSAGE="CI is green after ${ATTEMPT} fix attempt(s)."
|
|
483
|
+
;;
|
|
484
|
+
"timeout")
|
|
485
|
+
FINAL_STATUS="timeout"
|
|
486
|
+
FINAL_MESSAGE="CI did not complete within the polling timeout (${POLL_TIMEOUT}s) on attempt ${ATTEMPT}."
|
|
487
|
+
;;
|
|
488
|
+
"no_diff")
|
|
489
|
+
FINAL_STATUS="blocked"
|
|
490
|
+
FINAL_MESSAGE="Fix attempt ${ATTEMPT} produced no diff — the failure class '${FAILURE_CLASS}' may require manual intervention."
|
|
491
|
+
;;
|
|
492
|
+
"push_failed")
|
|
493
|
+
FINAL_STATUS="blocked"
|
|
494
|
+
FINAL_MESSAGE="Push failed on attempt ${ATTEMPT} — manual intervention required."
|
|
495
|
+
;;
|
|
496
|
+
"undiagnosable")
|
|
497
|
+
FINAL_STATUS="blocked"
|
|
498
|
+
FINAL_MESSAGE="CI failure on attempt ${ATTEMPT} could not be automatically diagnosed. Full log required."
|
|
499
|
+
;;
|
|
500
|
+
"pending"|*)
|
|
501
|
+
# Loop exhausted MAX_ATTEMPTS without success
|
|
502
|
+
FINAL_STATUS="max_attempts"
|
|
503
|
+
FINAL_MESSAGE="CI still failing after ${MAX_ATTEMPTS} fix attempt(s). Manual intervention required."
|
|
504
|
+
;;
|
|
505
|
+
esac
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### 2B: Post result to PR
|
|
509
|
+
|
|
510
|
+
Post a comment on the PR summarizing what was attempted:
|
|
511
|
+
|
|
512
|
+
```bash
|
|
513
|
+
REPORT_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
514
|
+
FIXES_SUMMARY=$(echo "$FIXES_APPLIED" | jq -r '.[] | "- Attempt \(.attempt): \(.failure_class) — \(.description) (run \(.run_id))"')
|
|
515
|
+
|
|
516
|
+
gh pr comment "$PR_NUMBER" $GH_FLAG --body "<!-- FORGE:FIX_CI -->
|
|
517
|
+
## /fix-ci Result
|
|
518
|
+
|
|
519
|
+
**Status**: \`${FINAL_STATUS}\`
|
|
520
|
+
**Attempts**: ${ATTEMPT}/${MAX_ATTEMPTS}
|
|
521
|
+
**Branch**: \`${FIX_TARGET}\`
|
|
522
|
+
**Timestamp**: ${REPORT_TIMESTAMP}
|
|
523
|
+
|
|
524
|
+
### Summary
|
|
525
|
+
${FINAL_MESSAGE}
|
|
526
|
+
|
|
527
|
+
### Fixes Applied
|
|
528
|
+
${FIXES_SUMMARY:-_No fixes applied_}
|
|
529
|
+
|
|
530
|
+
$([ "$FINAL_STATUS" != "success" ] && echo "### Next Steps
|
|
531
|
+
The CI failure was not resolved automatically. Recommended actions:
|
|
532
|
+
1. Review the latest CI run logs: \`gh run view --log-failed -R ${GH_REPO}\`
|
|
533
|
+
2. Check the failure class: \`${FAILURE_CLASS}\`
|
|
534
|
+
3. Apply a manual fix and push to \`${FIX_TARGET}\`
|
|
535
|
+
4. Re-run \`/fix-ci ${PR_NUMBER}\` after the manual fix")"
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### 2C: Return structured result
|
|
539
|
+
|
|
540
|
+
Print the machine-readable result to stdout for the calling skill (`/deploy-pr`, `/autopilot`):
|
|
541
|
+
|
|
542
|
+
```bash
|
|
543
|
+
RESULT=$(jq -nc \
|
|
544
|
+
--argjson pr "$PR_NUMBER" \
|
|
545
|
+
--arg status "$FINAL_STATUS" \
|
|
546
|
+
--argjson attempts "$ATTEMPT" \
|
|
547
|
+
--argjson fixes_applied "$FIXES_APPLIED" \
|
|
548
|
+
'{pr: $pr, status: $status, attempts: $attempts, fixes_applied: $fixes_applied}')
|
|
549
|
+
|
|
550
|
+
echo ""
|
|
551
|
+
echo "=== fix-ci result ==="
|
|
552
|
+
echo "$RESULT"
|
|
553
|
+
echo "====================="
|
|
554
|
+
|
|
555
|
+
# Exit with success (0) only if CI is green; non-zero for all other terminal states
|
|
556
|
+
if [ "$FINAL_STATUS" = "success" ]; then
|
|
557
|
+
exit 0
|
|
558
|
+
else
|
|
559
|
+
exit 1
|
|
560
|
+
fi
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
## Error Handling
|
|
566
|
+
|
|
567
|
+
| Condition | Behavior |
|
|
568
|
+
|-----------|----------|
|
|
569
|
+
| PR not found | Error with message, exit 1 |
|
|
570
|
+
| PR already merged | Report `already_merged`, exit 0 |
|
|
571
|
+
| PR is closed | Error with message, exit 1 |
|
|
572
|
+
| Fix target is `main`/`master` | Hard error — refuses to commit to protected branch |
|
|
573
|
+
| CI timeout | Reports `timeout` status, exits 1 |
|
|
574
|
+
| Push fails after retry | Reports `blocked` status, exits 1 |
|
|
575
|
+
| Unknown failure class | Attempts manual diagnosis; if undiagnosable, reports `blocked` |
|
|
576
|
+
| Max attempts reached | Reports `max_attempts`, exits 1 |
|
|
577
|
+
| No diff after fix | Reports `blocked` with hint, exits 1 |
|