@cxi-lmai/ci-agent-platform 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/bin/init.mjs +236 -0
  4. package/package.json +47 -0
  5. package/payload/INSTALL.md +113 -0
  6. package/payload/agents/agent-architect.md +101 -0
  7. package/payload/agents/code-reviewer.md +87 -0
  8. package/payload/agents/codebase-auditor.md +73 -0
  9. package/payload/agents/coder.md +56 -0
  10. package/payload/agents/decomposer.md +70 -0
  11. package/payload/agents/docs-sync.md +115 -0
  12. package/payload/agents/e2e-test-writer.md +47 -0
  13. package/payload/agents/migration-reviewer.md +100 -0
  14. package/payload/agents/orchestrator.md +50 -0
  15. package/payload/agents/performance-reviewer.md +82 -0
  16. package/payload/agents/postmortem.md +83 -0
  17. package/payload/agents/release-mr.md +274 -0
  18. package/payload/agents/security-reviewer.md +122 -0
  19. package/payload/agents/test-fix.md +33 -0
  20. package/payload/agents/test-writer.md +40 -0
  21. package/payload/ci-templates/claude-pipeline.gitlab-ci.yml +233 -0
  22. package/payload/ci-templates/github/README.md +76 -0
  23. package/payload/ci-templates/github/claude-issue-pipeline.yml +141 -0
  24. package/payload/ci-templates/github/claude-pipeline.yml +141 -0
  25. package/payload/ci-templates/github/claude-test-fix.yml +104 -0
  26. package/payload/ci-templates/scripts/code.sh +114 -0
  27. package/payload/ci-templates/scripts/lib/issue-loop.sh +430 -0
  28. package/payload/ci-templates/scripts/lib/pipeline-common.sh +280 -0
  29. package/payload/ci-templates/scripts/lib/platform.sh +177 -0
  30. package/payload/ci-templates/scripts/lib/usage-capture.sh +110 -0
  31. package/payload/ci-templates/scripts/orchestrate.sh +294 -0
  32. package/payload/ci-templates/scripts/postmortem.sh +45 -0
  33. package/payload/ci-templates/scripts/review-fix.sh +90 -0
  34. package/payload/ci-templates/scripts/review.sh +93 -0
  35. package/payload/ci-templates/scripts/test-fix.sh +58 -0
  36. package/payload/skills/fix-review-findings/SKILL.md +79 -0
  37. package/payload/skills/fix-tests/SKILL.md +70 -0
  38. package/payload/skills/implement-issue/SKILL.md +62 -0
  39. package/payload/skills/init-pipeline-config/SKILL.md +96 -0
  40. package/payload/skills/postmortem-mr/SKILL.md +50 -0
  41. package/payload/skills/review-mr/SKILL.md +82 -0
  42. package/payload/skills/triage-issue/SKILL.md +74 -0
  43. package/payload/templates/pipeline-config.template.md +98 -0
  44. package/payload/templates/review_suppressions.template.md +25 -0
  45. package/payload/templates/spec-issue.template.md +64 -0
@@ -0,0 +1,294 @@
1
+ #!/bin/bash
2
+ # Runner for the `orchestrate` job (issue -> code loop, orchestrator side).
3
+ # Ensures the lifecycle labels, runs the release poll (promote unblocked issues,
4
+ # close finished decomposed parents), then triages each ready issue by calling
5
+ # the /triage-issue skill and acting on its decision: queue the coder, ask for
6
+ # clarification, or decompose into sub-issues. Finally it fires the queued coder
7
+ # pipelines. All reasoning is in the skill; this script does token and git work.
8
+ #
9
+ # Generalized from a single-project original: labels and branches
10
+ # come from PIPE_ variables, the triage/decompose prompts moved into the skill,
11
+ # and the platform calls go through lib/issue-loop.sh (GitLab + GitHub).
12
+ set -e
13
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
14
+ # shellcheck source=lib/pipeline-common.sh
15
+ source "$SCRIPT_DIR/lib/pipeline-common.sh"
16
+ # shellcheck source=lib/issue-loop.sh
17
+ source "$SCRIPT_DIR/lib/issue-loop.sh"
18
+ pipe_defaults
19
+
20
+ pipe_sanitize_branch() {
21
+ # lowercase, collapse non-alphanumerics to single hyphens, trim, cap at 100.
22
+ echo "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' | cut -c1-100
23
+ }
24
+
25
+ # --- 1. Ensure the lifecycle labels exist (idempotent) ----------------------
26
+ pipe_log "Ensuring lifecycle labels"
27
+ issue_ensure_label "$PIPE_LABEL_READY" "#2ECC71" "Issue is ready for the autonomous pipeline (the single human action)"
28
+ issue_ensure_label "$PIPE_LABEL_WIP" "#3498DB" "Pipeline is implementing this issue"
29
+ issue_ensure_label "$PIPE_LABEL_BLOCKER" "#E67E22" "Needs clarification before work can proceed"
30
+ issue_ensure_label "$PIPE_LABEL_STUCK" "#B60205" "Automation exhausted, human intervention required"
31
+ issue_ensure_label "$PIPE_LABEL_DECOMPOSED" "#8E44AD" "Split into child sub-issues, auto-closed when all children close"
32
+ issue_ensure_label "$PIPE_LABEL_BLOCKED" "#95A5A6" "Waiting on a prerequisite issue before work begins"
33
+
34
+ # --- 2. Release poll --------------------------------------------------------
35
+ # 2a. Promote blocked issues whose blockers are all closed.
36
+ pipe_log "Release poll: promoting unblocked issues"
37
+ BLOCKED=$(issues_by_label "$PIPE_LABEL_BLOCKED")
38
+ while IFS= read -r IID; do
39
+ [ -z "$IID" ] && continue
40
+ OPEN=$(issue_blockers_open_count "$IID")
41
+ if [ "${OPEN:-1}" = "0" ]; then
42
+ pipe_log " #$IID unblocked, promoting to $PIPE_LABEL_READY"
43
+ issue_set_labels "$IID" "$PIPE_LABEL_READY" "$PIPE_LABEL_BLOCKED"
44
+ else
45
+ pipe_log " #$IID still has $OPEN open blocker(s), skipping"
46
+ fi
47
+ done < <(echo "$BLOCKED" | jq -r '.[].iid' 2>/dev/null || true)
48
+
49
+ # 2b. Close decomposed parents whose children are all closed.
50
+ pipe_log "Release poll: closing completed decomposed parents"
51
+ DECOMPOSED=$(issues_by_label "$PIPE_LABEL_DECOMPOSED")
52
+ while IFS= read -r IID; do
53
+ [ -z "$IID" ] && continue
54
+ read -r TOTAL OPEN <<< "$(issue_children_state "$IID")"
55
+ if [ "${TOTAL:-0}" -gt 0 ] && [ "${OPEN:-1}" = "0" ]; then
56
+ pipe_log " #$IID all children closed, closing parent"
57
+ issue_close "$IID"
58
+ else
59
+ pipe_log " #$IID has ${OPEN:-?} of ${TOTAL:-?} children open, skipping"
60
+ fi
61
+ done < <(echo "$DECOMPOSED" | jq -r '.[].iid' 2>/dev/null || true)
62
+
63
+ # 2c. Janitor: a merged MR/PR closes its issue, but nothing removes the wip
64
+ # label from it. Strip lifecycle labels from closed issues so the issue board
65
+ # stays clean.
66
+ pipe_log "Release poll: stripping lifecycle labels from closed issues"
67
+ for JANITOR_LABEL in "$PIPE_LABEL_WIP" "$PIPE_LABEL_READY"; do
68
+ while IFS= read -r IID; do
69
+ [ -z "$IID" ] && continue
70
+ pipe_log " #$IID is closed, removing $JANITOR_LABEL"
71
+ issue_set_labels "$IID" "" "$JANITOR_LABEL"
72
+ done < <(issues_closed_by_label "$JANITOR_LABEL" | jq -r '.[].iid' 2>/dev/null || true)
73
+ done
74
+
75
+ # --- 3. Triage ready issues -------------------------------------------------
76
+ READY=$(issues_by_label "$PIPE_LABEL_READY")
77
+ COUNT=$(echo "$READY" | jq 'length' 2>/dev/null || echo 0)
78
+ pipe_log "Found $COUNT ready issue(s)"
79
+ [ "$COUNT" = "0" ] && { pipe_log "Nothing to do"; exit 0; }
80
+
81
+ QUEUE=() # "IID:BRANCH" entries to trigger, capped at PIPE_CODER_CAP
82
+
83
+ emit_decision_metric() {
84
+ # $1=iid $2=verdict $3=decision_type $4=reason (+ optional $5 extra jq object)
85
+ pipe_metric_event orchestrator orchestrator_decision \
86
+ "$(jq -n --argjson i "$1" --arg v "$2" --arg dt "$3" --arg r "$4" --argjson x "${5:-{\}}" \
87
+ '{issue_iid: $i, verdict: $v, decision_type: $dt, reason: $r} + $x')"
88
+ }
89
+
90
+ while IFS= read -r ISSUE; do
91
+ [ "${#QUEUE[@]}" -ge "$PIPE_CODER_CAP" ] && { pipe_log "Coder cap ($PIPE_CODER_CAP) reached, deferring the rest"; break; }
92
+
93
+ IID=$(echo "$ISSUE" | jq -r '.iid')
94
+ TITLE=$(echo "$ISSUE" | jq -r '.title')
95
+ DESC=$(echo "$ISSUE" | jq -r '.description // "(no description)"' | head -c 32000)
96
+ pipe_log "--- Triaging #$IID: $TITLE"
97
+
98
+ # Comments arrive newest-first from issue_notes (both platforms). Cap the
99
+ # volume so a long discussion cannot crowd out the description, and mark the
100
+ # cut visibly instead of dropping the tail silently. Newest-first ordering
101
+ # keeps the most relevant comment above the line when the cap is hit.
102
+ COMMENTS=$(issue_notes "$IID")
103
+ PIPE_COMMENTS_MAX="${PIPE_COMMENTS_MAX:-12000}"
104
+ if [ "${#COMMENTS}" -gt "$PIPE_COMMENTS_MAX" ]; then
105
+ COMMENTS="$(printf '%s' "$COMMENTS" | head -c "$PIPE_COMMENTS_MAX")
106
+ [... older comments truncated ...]"
107
+ fi
108
+
109
+ # Build the context file the skill reads. No API access inside the skill.
110
+ {
111
+ echo "# Issue #$IID"
112
+ echo
113
+ echo "Title: $TITLE"
114
+ echo
115
+ echo "## Description"
116
+ echo "$DESC"
117
+ [ -n "$COMMENTS" ] && { echo; echo "## Comments"; echo "$COMMENTS"; }
118
+ } > "$PIPE_CONTEXT_DIR/issue-context.md"
119
+
120
+ export PIPE_ISSUE_IID="$IID"
121
+ rm -f "$PIPE_CONTEXT_DIR/triage.env" "$PIPE_CONTEXT_DIR/triage.json"
122
+ pipe_run_claude orchestrator "/triage-issue" "Agent,Read,Write,Edit,Glob,Grep,Bash" "$PIPE_MODEL_TRIAGE" < /dev/null
123
+
124
+ TRIAGE_ENV="$PIPE_CONTEXT_DIR/triage.env"
125
+ TRIAGE_JSON="$PIPE_CONTEXT_DIR/triage.json"
126
+ DECISION=$(pipe_get_env "$TRIAGE_ENV" TRIAGE_DECISION)
127
+ if [ -z "$DECISION" ] || [ ! -s "$TRIAGE_JSON" ]; then
128
+ pipe_log " no usable triage output for #$IID, skipping"
129
+ continue
130
+ fi
131
+
132
+ case "$DECISION" in
133
+ implement)
134
+ BRANCH=$(pipe_get_env "$TRIAGE_ENV" TRIAGE_BRANCH)
135
+ BRANCH=$(pipe_sanitize_branch "${BRANCH:-$IID-$TITLE}")
136
+ [ -z "$BRANCH" ] && BRANCH=$(pipe_sanitize_branch "$IID-issue")
137
+ QUEUE+=("$IID:$BRANCH")
138
+ emit_decision_metric "$IID" actionable implement "Issue is right-sized for one coder session"
139
+ pipe_log " queued #$IID on branch $BRANCH"
140
+ ;;
141
+
142
+ ask)
143
+ QUESTION=$(jq -r '.question // "Please provide more detail before this issue can be implemented."' "$TRIAGE_JSON")
144
+ issue_comment "$IID" "🤖 **The pipeline needs clarification before implementing:**
145
+
146
+ $QUESTION
147
+
148
+ Put the complete spec in the issue **description**, not in a comment: the pipeline reads only the description."
149
+ issue_set_labels "$IID" "$PIPE_LABEL_BLOCKER" "$PIPE_LABEL_READY"
150
+ emit_decision_metric "$IID" clarification-needed ask "$QUESTION"
151
+ pipe_log " asked for clarification on #$IID"
152
+ ;;
153
+
154
+ decompose_failed)
155
+ SUGGEST=$(jq -r '.suggestion // "Please split this issue into smaller pieces."' "$TRIAGE_JSON")
156
+ issue_comment "$IID" "**This issue is clear but too large for a single agent session.**
157
+
158
+ Suggested decomposition:
159
+
160
+ $SUGGEST
161
+
162
+ Please split it into smaller issues and label each \`$PIPE_LABEL_READY\`."
163
+ issue_set_labels "$IID" "$PIPE_LABEL_BLOCKER" "$PIPE_LABEL_READY"
164
+ emit_decision_metric "$IID" clarification-needed ask "Too large; decomposition suggested"
165
+ pipe_log " posted decomposition suggestion for #$IID"
166
+ ;;
167
+
168
+ decompose)
169
+ # Re-validate deterministically before mutating the platform.
170
+ if ! pipe_validate_decomposition "$(cat "$TRIAGE_JSON")"; then
171
+ pipe_log " decomposition failed the gate: $DECOMP_REJECT_REASON"
172
+ issue_comment "$IID" "**This issue is too large and automatic decomposition did not produce a valid split.**
173
+
174
+ Reason: $DECOMP_REJECT_REASON
175
+
176
+ Please split it manually into smaller issues and label each \`$PIPE_LABEL_READY\`."
177
+ issue_set_labels "$IID" "$PIPE_LABEL_BLOCKER" "$PIPE_LABEL_READY"
178
+ emit_decision_metric "$IID" clarification-needed ask "Decomposition rejected by gate"
179
+ continue
180
+ fi
181
+
182
+ CONFIDENCE=$(pipe_get_env "$TRIAGE_ENV" TRIAGE_CONFIDENCE)
183
+ CONFIDENCE=${CONFIDENCE:-$(jq -r '.confidence // "low"' "$TRIAGE_JSON")}
184
+ REASON=$(jq -r '.reason // ""' "$TRIAGE_JSON")
185
+ SUB_COUNT=$(jq '.sub_issues | length' "$TRIAGE_JSON")
186
+ pipe_log " decomposing #$IID into $SUB_COUNT sub-issues (confidence=$CONFIDENCE)"
187
+
188
+ # Create sub-issues, mapping key -> new IID.
189
+ declare -A KEY_TO_IID=()
190
+ CREATED=()
191
+ FAILED=false
192
+ while IFS= read -r SUB; do
193
+ SUB_KEY=$(echo "$SUB" | jq -r '.key')
194
+ SUB_TITLE=$(echo "$SUB" | jq -r '.title')
195
+ SUB_SPEC=$(echo "$SUB" | jq -r '.spec_markdown')
196
+ HAS_DEPS=$(echo "$SUB" | jq '(.depends_on // []) | length > 0')
197
+ if [ "$CONFIDENCE" = "high" ] && [ "$HAS_DEPS" = "false" ]; then
198
+ SUB_LABEL="$PIPE_LABEL_READY"
199
+ elif [ "$CONFIDENCE" = "low" ]; then
200
+ SUB_LABEL="$PIPE_LABEL_BLOCKER"
201
+ else
202
+ SUB_LABEL="$PIPE_LABEL_BLOCKED"
203
+ fi
204
+ NEW_IID=$(issue_create "$SUB_TITLE" "$SUB_SPEC" "$SUB_LABEL")
205
+ if [ -z "$NEW_IID" ]; then
206
+ pipe_log " ERROR: failed to create sub-issue for key '$SUB_KEY'"
207
+ FAILED=true
208
+ break
209
+ fi
210
+ KEY_TO_IID["$SUB_KEY"]="$NEW_IID"
211
+ CREATED+=("$NEW_IID")
212
+ pipe_log " created #$NEW_IID ($SUB_LABEL): $SUB_TITLE"
213
+ done < <(jq -c '.sub_issues[]' "$TRIAGE_JSON")
214
+
215
+ if [ "$FAILED" = "true" ]; then
216
+ CHILD_LIST=$(printf '#%s ' "${CREATED[@]:-}")
217
+ issue_comment "$IID" "**Auto-decomposition partially failed.** Created so far: ${CHILD_LIST:-none}. Relabeled \`$PIPE_LABEL_BLOCKER\` to prevent re-decomposition. Please finish manually."
218
+ issue_set_labels "$IID" "$PIPE_LABEL_BLOCKER" "$PIPE_LABEL_READY"
219
+ unset KEY_TO_IID
220
+ continue
221
+ fi
222
+
223
+ # Blocking links. GitLab uses native links. GitHub appends the resolved
224
+ # "Blocked by #N" relationship to the dependent issue body now that local
225
+ # decomposition keys have real platform issue numbers.
226
+ while IFS= read -r SUB; do
227
+ SUB_KEY=$(echo "$SUB" | jq -r '.key')
228
+ BLOCKED_IID="${KEY_TO_IID[$SUB_KEY]:-}"
229
+ [ -z "$BLOCKED_IID" ] && continue
230
+ while IFS= read -r DEP_KEY; do
231
+ [ -z "$DEP_KEY" ] && continue
232
+ BLOCKING_IID="${KEY_TO_IID[$DEP_KEY]:-}"
233
+ [ -n "$BLOCKING_IID" ] && issue_link_blocked_by "$BLOCKED_IID" "$BLOCKING_IID"
234
+ done < <(echo "$SUB" | jq -r '(.depends_on // [])[]' 2>/dev/null || true)
235
+ done < <(jq -c '.sub_issues[]' "$TRIAGE_JSON")
236
+
237
+ # Relabel parent and comment (embed a machine marker listing the children,
238
+ # so the GitHub release poll can find them without native links).
239
+ CHILD_LIST=$(printf '#%s ' "${CREATED[@]}")
240
+ issue_set_labels "$IID" "$PIPE_LABEL_DECOMPOSED" "$PIPE_LABEL_READY"
241
+ issue_comment "$IID" "**Auto-decomposed by the pipeline** (confidence: \`$CONFIDENCE\`).
242
+
243
+ Reason: $REASON
244
+
245
+ Split into $SUB_COUNT sub-issues: $CHILD_LIST
246
+
247
+ Sub-issues with prerequisites are labeled \`$PIPE_LABEL_BLOCKED\` and are promoted automatically when their blockers close. This parent closes automatically when all children close.
248
+
249
+ <!-- pipe-children: $CHILD_LIST -->"
250
+ emit_decision_metric "$IID" actionable decompose "Decomposed into $SUB_COUNT sub-issues (confidence: $CONFIDENCE)" \
251
+ "$(jq -n --argjson sc "$SUB_COUNT" --arg conf "$CONFIDENCE" '{sub_issue_count: $sc, confidence: $conf}')"
252
+
253
+ # Queue high-confidence, unblocked children for the coder.
254
+ if [ "$CONFIDENCE" = "high" ]; then
255
+ while IFS= read -r SUB; do
256
+ [ "${#QUEUE[@]}" -ge "$PIPE_CODER_CAP" ] && break
257
+ HAS_DEPS=$(echo "$SUB" | jq '(.depends_on // []) | length > 0')
258
+ [ "$HAS_DEPS" = "true" ] && continue
259
+ SUB_KEY=$(echo "$SUB" | jq -r '.key')
260
+ SUB_IID="${KEY_TO_IID[$SUB_KEY]:-}"
261
+ [ -z "$SUB_IID" ] && continue
262
+ SUB_TITLE=$(echo "$SUB" | jq -r '.title')
263
+ QUEUE+=("$SUB_IID:$(pipe_sanitize_branch "$SUB_IID-$SUB_TITLE")")
264
+ pipe_log " queued child #$SUB_IID"
265
+ done < <(jq -c '.sub_issues[]' "$TRIAGE_JSON")
266
+ fi
267
+ unset KEY_TO_IID
268
+ ;;
269
+
270
+ *)
271
+ pipe_log " unknown decision '$DECISION' for #$IID, skipping"
272
+ ;;
273
+ esac
274
+ done < <(echo "$READY" | jq -c '.[]')
275
+
276
+ # --- 4. Trigger the queued coder pipelines ----------------------------------
277
+ [ "${#QUEUE[@]}" -eq 0 ] && { pipe_log "No issues queued for the coder"; exit 0; }
278
+ pipe_log "Triggering ${#QUEUE[@]} coder run(s)"
279
+ for ITEM in "${QUEUE[@]}"; do
280
+ IID="${ITEM%%:*}"
281
+ BRANCH="${ITEM#*:}"
282
+ # Atomic claim: relabel ready -> wip first, so a concurrent orchestrator does
283
+ # not double-trigger. Revert on trigger failure.
284
+ if issue_set_labels "$IID" "$PIPE_LABEL_WIP" "$PIPE_LABEL_READY"; then
285
+ if code_trigger "$IID" "$BRANCH"; then
286
+ pipe_log " triggered coder for #$IID on $BRANCH"
287
+ else
288
+ pipe_log " ERROR: trigger failed for #$IID, reverting label"
289
+ issue_set_labels "$IID" "$PIPE_LABEL_READY" "$PIPE_LABEL_WIP"
290
+ fi
291
+ else
292
+ pipe_log " ERROR: could not claim #$IID, skipping to avoid a double trigger"
293
+ fi
294
+ done
@@ -0,0 +1,45 @@
1
+ #!/bin/bash
2
+ # Runner for a standalone escalation / diagnose step. The fix skills run the
3
+ # postmortem flow inline when they hit their cap, so the fix-job runners only
4
+ # call pipe_apply_escalation. This script exists for the other callers named in
5
+ # the audit (for example a deploy-fix job) that want a fresh diagnostic as a
6
+ # separate step. It prepares the input, runs /postmortem-mr, then applies labels
7
+ # and posts the comment.
8
+ #
9
+ # The caller sets the failure context via env vars before invoking:
10
+ # DIAGNOSE_ERROR original failure text (test failures, review body, deploy json)
11
+ # DIAGNOSE_GIT_LOG git log of the fix attempts (oneline)
12
+ # DIAGNOSE_CONTEXT which loop got stuck (e.g. "deploy-fix")
13
+ set -e
14
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
15
+ # shellcheck source=lib/pipeline-common.sh
16
+ source "$SCRIPT_DIR/lib/pipeline-common.sh"
17
+ pipe_defaults
18
+
19
+ # Prepare the input file the skill reads, unless the caller already did.
20
+ if [ ! -f "$PIPE_CONTEXT_DIR/postmortem-input.md" ]; then
21
+ META=$(platform_mr_meta)
22
+ {
23
+ echo "# Postmortem input"
24
+ echo
25
+ echo "MR/PR: !${PIPE_MR_IID:-} $(echo "$META" | jq -r '.title // ""')"
26
+ echo "Context: ${DIAGNOSE_CONTEXT:-unknown}"
27
+ echo
28
+ echo "## Changed files"
29
+ git diff --name-only "origin/$PIPE_TARGET_BRANCH..HEAD" 2>/dev/null | head -30 || true
30
+ echo
31
+ echo "## Fix attempts"
32
+ echo "${DIAGNOSE_GIT_LOG:-$(git log "origin/$PIPE_TARGET_BRANCH..HEAD" --oneline 2>/dev/null || true)}"
33
+ echo
34
+ echo "## Original failure"
35
+ echo "${DIAGNOSE_ERROR:-}"
36
+ } > "$PIPE_CONTEXT_DIR/postmortem-input.md"
37
+ fi
38
+
39
+ # Run the postmortem skill unless a prior inline flow already produced the marker.
40
+ if [ ! -f "$PIPE_CONTEXT_DIR/postmortem.env" ]; then
41
+ pipe_run_claude postmortem "/postmortem-mr" "Agent,Read,Write,Edit,Glob,Grep,Bash" "$PIPE_MODEL_TRIAGE" < /dev/null
42
+ fi
43
+
44
+ # Apply the runner side: labels, comment, metric.
45
+ pipe_apply_escalation "" "${DIAGNOSE_CONTEXT:-unknown}"
@@ -0,0 +1,90 @@
1
+ #!/bin/bash
2
+ # Runner for the `review-fix` job. Runs on failure after `review` on a wip MR/PR.
3
+ # Fetches the latest review comment, runs the /fix-review-findings skill, then
4
+ # pushes the fix commit and posts the applied/dismissed comments, or escalates.
5
+ # The skill does the cap check, the coder delegation, and the verdict marker.
6
+ set -e
7
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+ # shellcheck source=lib/pipeline-common.sh
9
+ source "$SCRIPT_DIR/lib/pipeline-common.sh"
10
+ pipe_defaults
11
+
12
+ REVIEW_MARKER="${PIPE_REVIEW_MARKER:-**Automated Code Review**}"
13
+
14
+ # 1. Gate: only run when review flagged fixable bugs. REVIEW_HAS_BUGS is a
15
+ # runtime dotenv value from the review job, so rules cannot pre-evaluate it.
16
+ if [ "${REVIEW_HAS_BUGS:-false}" != "true" ]; then
17
+ pipe_log "No fixable bugs flagged by review, skipping"
18
+ exit 0
19
+ fi
20
+
21
+ # 2. Gate: only fix wip MRs/PRs.
22
+ LABELS=$(platform_mr_labels)
23
+ if ! echo "$LABELS" | grep -q "$PIPE_LABEL_WIP"; then
24
+ pipe_log "Not a $PIPE_LABEL_WIP MR/PR, skipping review-fix"
25
+ exit 0
26
+ fi
27
+
28
+ # 3. Git identity and real source-branch checkout.
29
+ pipe_git_identity
30
+ pipe_checkout_source
31
+ FIX_COUNT_BEFORE=$(pipe_count_fix_commits "$PIPE_COMMIT_REVIEWFIX")
32
+
33
+ # 4. Fetch the latest bot review comment into the file the skill reads.
34
+ platform_resolve_bot_user
35
+ NOTES=$(platform_notes)
36
+ echo "$NOTES" | jq -r --arg u "${PIPE_BOT_USER:-}" --arg m "$REVIEW_MARKER" \
37
+ '[.[] | select(.author == $u and (.body | startswith($m)))] | last | .body // "(no review found)"' \
38
+ > "$PIPE_CONTEXT_DIR/review-body.md"
39
+
40
+ # 5. Run the fix skill. It caps, gathers prior fix diffs, delegates to the coder,
41
+ # verifies, writes the result JSON, and the review-fix.env marker. On cap it
42
+ # runs the shared postmortem flow (writing postmortem.md + postmortem.env).
43
+ pipe_run_claude coder "/fix-review-findings" "Agent,Read,Write,Edit,Glob,Grep,Bash" "$PIPE_MODEL_CODE" < /dev/null
44
+
45
+ # 6. Read the marker the skill wrote.
46
+ RF_ENV="$PIPE_CONTEXT_DIR/review-fix.env"
47
+ RF_ESCALATE=$(pipe_get_env "$RF_ENV" RF_ESCALATE); RF_ESCALATE=${RF_ESCALATE:-false}
48
+ RF_CHANGED=$(pipe_get_env "$RF_ENV" RF_CHANGED); RF_CHANGED=${RF_CHANGED:-false}
49
+ RF_APPLIED=$(pipe_get_env "$RF_ENV" RF_APPLIED); RF_APPLIED=${RF_APPLIED:-0}
50
+ RF_DISMISSED=$(pipe_get_env "$RF_ENV" RF_DISMISSED); RF_DISMISSED=${RF_DISMISSED:-0}
51
+ CATEGORY=$(pipe_get_env "$RF_ENV" FAILURE_CATEGORY)
52
+
53
+ # Verify a commit actually landed this run before pushing.
54
+ FIX_COUNT_AFTER=$(pipe_count_fix_commits "$PIPE_COMMIT_REVIEWFIX")
55
+ COMMIT_MADE=false
56
+ [ "$FIX_COUNT_AFTER" -gt "$FIX_COUNT_BEFORE" ] && COMMIT_MADE=true
57
+
58
+ pipe_metric_event review-fix review_findings_applied \
59
+ "$(jq -n --argjson a "${RF_APPLIED:-0}" --argjson d "${RF_DISMISSED:-0}" '{applied: $a, dismissed: $d}')"
60
+
61
+ post_dismissed() {
62
+ local dj
63
+ dj=$(jq -r '.dismissed // [] | map("- **" + .finding + "**: " + .reason) | join("\n")' "$PIPE_RESULT_REVIEWFIX" 2>/dev/null || true)
64
+ [ -z "$dj" ] && return 0
65
+ platform_post_comment "**Review findings dismissed**
66
+
67
+ The auto-fix agent determined the following findings were false positives and require no code change:
68
+
69
+ $dj"
70
+ }
71
+
72
+ # 7. Act on the outcome.
73
+ if [ "$RF_ESCALATE" = "true" ]; then
74
+ pipe_log "Skill signalled escalation"
75
+ pipe_apply_escalation "$CATEGORY" "review-fix"
76
+ elif [ "$COMMIT_MADE" = "true" ]; then
77
+ if pipe_rebase_and_push; then
78
+ ATTEMPT=$((FIX_COUNT_BEFORE + 1))
79
+ platform_post_comment "**Auto-fix applied (attempt ${ATTEMPT}/${PIPE_FIX_LOOP_CAP})**
80
+
81
+ Fixes committed for confirmed bugs from the code review. A new pipeline will run to verify and re-review the changes."
82
+ post_dismissed
83
+ fi
84
+ elif [ "$RF_CHANGED" = "false" ]; then
85
+ pipe_log "Agent determined the findings are false positives"
86
+ post_dismissed
87
+ else
88
+ pipe_log "No commit and no result, escalating"
89
+ pipe_apply_escalation "$CATEGORY" "review-fix-no-progress"
90
+ fi
@@ -0,0 +1,93 @@
1
+ #!/bin/bash
2
+ # Runner for the `review` job. Prepares the MR/PR context from the platform API,
3
+ # computes the diff base, runs the /review-mr skill, posts the merged review as a
4
+ # comment, emits metrics, and gates the pipeline. All reasoning is in the skill.
5
+ set -e
6
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
+ # shellcheck source=lib/pipeline-common.sh
8
+ source "$SCRIPT_DIR/lib/pipeline-common.sh"
9
+ pipe_defaults
10
+
11
+ REVIEW_MARKER="${PIPE_REVIEW_MARKER:-**Automated Code Review**}"
12
+
13
+ # 1. Fetch MR/PR meta and build the context file the skill reads (token work).
14
+ META=$(platform_mr_meta)
15
+ MR_TITLE=$(echo "$META" | jq -r '.title // ""')
16
+ MR_DESC=$(echo "$META" | jq -r '.description // ""')
17
+
18
+ # Linked issues referenced in the description (e.g. "Closes #42").
19
+ LINKED_ISSUES=$(printf '%s' "$MR_DESC" | grep -oE '#[0-9]+' | grep -oE '[0-9]+' | sort -u || true)
20
+ ISSUES_CONTEXT=""
21
+ for IID in $LINKED_ISSUES; do
22
+ ISSUE=$(platform_issue "$IID")
23
+ ISSUES_CONTEXT="$ISSUES_CONTEXT
24
+ Issue #$(echo "$ISSUE" | jq -r '.iid'): $(echo "$ISSUE" | jq -r '.title')
25
+ $(echo "$ISSUE" | jq -r '.description')"
26
+ done
27
+
28
+ # Previous bot review comment and previous auto-fix activity, for context.
29
+ platform_resolve_bot_user
30
+ NOTES=$(platform_notes)
31
+ PREV_REVIEW=$(echo "$NOTES" | jq -r --arg u "${PIPE_BOT_USER:-}" --arg m "$REVIEW_MARKER" \
32
+ '[.[] | select(.author == $u and (.body | startswith($m)))] | last | .body // ""' 2>/dev/null || true)
33
+ PRIOR_FIX=$(echo "$NOTES" | jq -r --arg u "${PIPE_BOT_USER:-}" \
34
+ '[.[] | select(.author == $u and ((.body | startswith("**Auto-fix applied")) or (.body | startswith("**Review findings dismissed"))))] | map(.body) | join("\n\n---\n\n")' 2>/dev/null || true)
35
+
36
+ # 2. Compute the diff base and incremental flag (platform plumbing over API + git).
37
+ platform_compute_diff_base
38
+ pipe_log "diff base=$PIPE_DIFF_BASE incremental=$PIPE_IS_INCREMENTAL head=$PIPE_HEAD_SHA"
39
+
40
+ # 3. Write the context file the skill reads. No API access inside the skill.
41
+ {
42
+ echo "# MR/PR context"
43
+ echo
44
+ echo "Title: $MR_TITLE"
45
+ echo
46
+ echo "## Description"
47
+ echo "$MR_DESC"
48
+ [ -n "$ISSUES_CONTEXT" ] && { echo; echo "## Linked issues"; echo "$ISSUES_CONTEXT"; }
49
+ [ -n "$PREV_REVIEW" ] && { echo; echo "## Previous review comment"; echo "$PREV_REVIEW"; }
50
+ [ -n "$PRIOR_FIX" ] && { echo; echo "## Previous auto-fix activity"; echo "$PRIOR_FIX"; }
51
+ } > "$PIPE_CONTEXT_DIR/mr-context.md"
52
+
53
+ # 4. Run the review skill. It builds the diff, runs the reviewers, writes the
54
+ # merged body to $PIPE_RESULT_REVIEW and the verdict to review.env.
55
+ pipe_run_claude review "/review-mr" "Agent,Read,Write,Edit,Glob,Grep,Bash" "$PIPE_MODEL_REVIEW" < /dev/null
56
+
57
+ # 5. Read the verdict the skill wrote (drives the status emoji), then post
58
+ # the review comment (token work). The emoji goes after the marker line,
59
+ # never before it: the marker prefix is how review.sh and review-fix.sh
60
+ # find previous bot comments.
61
+ REVIEW_ENV="$PIPE_CONTEXT_DIR/review.env"
62
+ REVIEW_HAS_BUGS=$(pipe_get_env "$REVIEW_ENV" REVIEW_HAS_BUGS); REVIEW_HAS_BUGS=${REVIEW_HAS_BUGS:-false}
63
+ REVIEW_STATUS=$(pipe_get_env "$REVIEW_ENV" REVIEW_STATUS); REVIEW_STATUS=${REVIEW_STATUS:-clean}
64
+ FINDINGS_TOTAL=$(pipe_get_env "$REVIEW_ENV" REVIEW_FINDINGS_TOTAL); FINDINGS_TOTAL=${FINDINGS_TOTAL:-0}
65
+
66
+ if [ -s "$PIPE_RESULT_REVIEW" ]; then
67
+ VERDICT_EMOJI="✅"
68
+ [ "$REVIEW_HAS_BUGS" = "true" ] && VERDICT_EMOJI="❌"
69
+ BODY="$REVIEW_MARKER
70
+
71
+ $VERDICT_EMOJI $(cat "$PIPE_RESULT_REVIEW")"
72
+ else
73
+ BODY="$REVIEW_MARKER
74
+
75
+ ⚠️ Review generation failed. Check the CI job log for details."
76
+ fi
77
+ platform_post_comment "$BODY"
78
+
79
+ # 6. Emit metrics.
80
+ pipe_metric_event code-reviewer review_findings \
81
+ "$(jq -n --argjson t "${FINDINGS_TOTAL:-0}" --arg s "$REVIEW_STATUS" \
82
+ --argjson b "$([ "$REVIEW_HAS_BUGS" = "true" ] && echo true || echo false)" \
83
+ --argjson inc "$([ "${PIPE_IS_INCREMENTAL:-false}" = "true" ] && echo true || echo false)" \
84
+ '{findings_total: $t, status: $s, has_fixable_bugs: $b, is_incremental: $inc}')"
85
+
86
+ # 7. Gate. review.env is exported so the fix job can read REVIEW_HAS_BUGS at
87
+ # runtime. On GitLab the job exits non-zero to block the pipeline. On GitHub
88
+ # the workflow step owns the gate (it reads the output first), so it sets
89
+ # PIPE_NO_GATE=1 and this script leaves the exit to the workflow.
90
+ if [ "$REVIEW_HAS_BUGS" = "true" ]; then
91
+ pipe_log "Review found confirmed bugs"
92
+ [ "${PIPE_NO_GATE:-0}" = "1" ] || exit 1
93
+ fi
@@ -0,0 +1,58 @@
1
+ #!/bin/bash
2
+ # Runner for the `test-fix` job. Runs on failure after the project's test stage
3
+ # on a wip MR/PR. Runs the /fix-tests skill (which picks the case, caps, and
4
+ # delegates), then pushes the fix commit or escalates. The project test job and
5
+ # its artifacts stay in the project; this job only consumes them.
6
+ set -e
7
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+ # shellcheck source=lib/pipeline-common.sh
9
+ source "$SCRIPT_DIR/lib/pipeline-common.sh"
10
+ pipe_defaults
11
+
12
+ # 1. Gate: only fix wip MRs/PRs.
13
+ LABELS=$(platform_mr_labels)
14
+ if ! echo "$LABELS" | grep -q "$PIPE_LABEL_WIP"; then
15
+ pipe_log "Not a $PIPE_LABEL_WIP MR/PR, skipping test-fix"
16
+ exit 0
17
+ fi
18
+
19
+ # 2. Git identity and real source-branch checkout.
20
+ pipe_git_identity
21
+ pipe_checkout_source
22
+ TESTFIX_BEFORE=$(pipe_count_fix_commits "$PIPE_COMMIT_TESTFIX")
23
+ COVERAGE_BEFORE=$(pipe_count_fix_commits "$PIPE_COMMIT_COVERAGE")
24
+
25
+ # 3. Run the fix skill. It reads the test reports / coverage signal, picks the
26
+ # case (tests | coverage | compile | none), caps, delegates to test-fix or
27
+ # test-writer, verifies, and writes fix-tests.env. On cap it runs the shared
28
+ # postmortem flow (writing postmortem.md + postmortem.env).
29
+ pipe_run_claude test-fix "/fix-tests" "Agent,Read,Write,Edit,Glob,Grep,Bash" "$PIPE_MODEL_CODE" < /dev/null
30
+
31
+ # 4. Read the marker the skill wrote.
32
+ FT_ENV="$PIPE_CONTEXT_DIR/fix-tests.env"
33
+ FIX_BRANCH=$(pipe_get_env "$FT_ENV" FIX_BRANCH); FIX_BRANCH=${FIX_BRANCH:-none}
34
+ FIX_ESCALATE=$(pipe_get_env "$FT_ENV" FIX_ESCALATE); FIX_ESCALATE=${FIX_ESCALATE:-false}
35
+ CATEGORY=$(pipe_get_env "$FT_ENV" FAILURE_CATEGORY)
36
+
37
+ # Verify a commit landed (either the test-fix or the coverage subject).
38
+ if [ "$FIX_BRANCH" = "coverage" ]; then
39
+ AFTER=$(pipe_count_fix_commits "$PIPE_COMMIT_COVERAGE"); BEFORE=$COVERAGE_BEFORE
40
+ else
41
+ AFTER=$(pipe_count_fix_commits "$PIPE_COMMIT_TESTFIX"); BEFORE=$TESTFIX_BEFORE
42
+ fi
43
+ COMMIT_MADE=false
44
+ [ "${AFTER:-0}" -gt "${BEFORE:-0}" ] && COMMIT_MADE=true
45
+
46
+ # 5. Act on the outcome.
47
+ if [ "$FIX_ESCALATE" = "true" ]; then
48
+ pipe_log "Skill signalled escalation"
49
+ pipe_apply_escalation "$CATEGORY" "test-fix"
50
+ elif [ "$FIX_BRANCH" = "none" ]; then
51
+ pipe_log "Nothing to fix (false-alarm trigger)"
52
+ elif [ "$COMMIT_MADE" = "true" ]; then
53
+ if pipe_rebase_and_push; then
54
+ pipe_log "Pushed $FIX_BRANCH fix commit, a new pipeline will run"
55
+ fi
56
+ else
57
+ pipe_log "Agent made no new commit, nothing to push"
58
+ fi
@@ -0,0 +1,79 @@
1
+ ---
2
+ name: fix-review-findings
3
+ description: Fix the confirmed bugs from an automated code review of a merge request or pull request. Reads the latest review comment and any prior fix attempts, respects the fix-loop cap, delegates the fix to the coder agent, verifies, and commits without pushing. Records applied fixes and dismissed false positives for the pipeline. Use this when a review flagged fixable bugs and CI asks to fix them.
4
+ ---
5
+
6
+ # fix-review-findings
7
+
8
+ Generic replacement for `review-fix.sh`. The CI job runs `claude "/fix-review-findings"` after a review that flagged fixable bugs. This skill drives the fix. The runner keeps checkout, rebase, push, and the label and comment API calls.
9
+
10
+ ## Step 1: read the project configuration
11
+
12
+ Read `$PIPE_CONFIG_PATH` (default `.claude/pipeline-config.md`). You need the Git & Platform section, the Build & Tests section, the Documentation Map, and the Suppressions path. If the file is missing, say so on the first line and continue generically.
13
+
14
+ ## Step 2: inputs
15
+
16
+ - `$PIPE_CONTEXT_DIR/review-body.md`: the latest bot review comment, fetched by the job with the platform token. These are the findings to act on.
17
+ - `$PIPE_VERIFY_CMD`: verification command (compile plus unit tests) to run after a fix.
18
+ - `$PIPE_FIX_LOOP_CAP` (default `2`): maximum consecutive review-fix commits before escalating.
19
+ - `$PIPE_COMMIT_REVIEWFIX`: exact commit subject for a review fix. Its recurrence drives the cap count.
20
+ - `$PIPE_TARGET_BRANCH`: the branch the MR/PR targets, used as the base for the commit-history walk.
21
+ - `$PIPE_RESULT_REVIEWFIX` (default `$PIPE_CONTEXT_DIR/review-fix-result.json`): where the coder writes its structured result.
22
+ - `$PIPE_CONTEXT_DIR` (default `build/pipeline`): where you write the result marker.
23
+
24
+ ## Step 3: cap check
25
+
26
+ Count consecutive commits from the branch tip back toward `$PIPE_TARGET_BRANCH` whose subject matches `$PIPE_COMMIT_REVIEWFIX`, using local git:
27
+
28
+ ```
29
+ git log "origin/$PIPE_TARGET_BRANCH"..HEAD --format="%s"
30
+ ```
31
+
32
+ Stop at the first non-matching subject. If the count is at or above `$PIPE_FIX_LOOP_CAP`, do not attempt another fix. Escalate: follow the postmortem-mr flow (spawn the `postmortem` agent with the review body as the failure text, the fix-attempt git log, and the changed files, then read `failure_category`). Write the marker with `RF_ESCALATE=true` and the `FAILURE_CATEGORY`, and stop.
33
+
34
+ ## Step 4: gather prior attempts
35
+
36
+ If the count from step 3 is above zero, this is a retry. Collect the diffs of the earlier review-fix commits so the coder does not repeat a failed approach:
37
+
38
+ ```
39
+ git log "origin/$PIPE_TARGET_BRANCH"..HEAD --grep="$PIPE_COMMIT_REVIEWFIX" -p
40
+ ```
41
+
42
+ ## Step 5: delegate to the coder
43
+
44
+ Spawn the `coder` agent (Agent tool, `subagent_type: coder`). Pass it:
45
+
46
+ - the review findings from `$PIPE_CONTEXT_DIR/review-body.md`,
47
+ - the prior fix diffs from step 4, if any, with the instruction not to repeat those approaches,
48
+ - these constraints:
49
+ - fix only the confirmed bugs and security issues in the review, not style notes, nitpicks, or "potential" items,
50
+ - do not rewrite the feature, target the specific defects,
51
+ - before fixing each finding, read the source and verify the claim is accurate,
52
+ - for a finding that is a false positive or an intentional deviation, do not change code. Record it in the Suppressions file (per the coder's own protocol) so it is not re-raised.
53
+ - the exact commit subject `$PIPE_COMMIT_REVIEWFIX` for any commit, because the cap counter matches on it,
54
+ - the instruction to write `$PIPE_RESULT_REVIEWFIX` in this shape and not to run `git push`:
55
+
56
+ ```json
57
+ {"changed": true, "fixes": ["description of each fix"], "dismissed": [{"finding": "brief quote", "reason": "why it is a false positive"}]}
58
+ ```
59
+
60
+ Set `changed` to `false` with an empty `fixes` list when no code changed.
61
+
62
+ ## Step 6: verify
63
+
64
+ Run `$PIPE_VERIFY_CMD`. Do not leave a non-compiling state for the push.
65
+
66
+ ## Step 7: emit the result marker
67
+
68
+ Read `$PIPE_RESULT_REVIEWFIX` and write `$PIPE_CONTEXT_DIR/review-fix.env` as dotenv:
69
+
70
+ ```
71
+ RF_COMMIT_MADE=true|false
72
+ RF_CHANGED=true|false
73
+ RF_APPLIED=<count of fixes>
74
+ RF_DISMISSED=<count of dismissed>
75
+ RF_ESCALATE=true|false
76
+ FAILURE_CATEGORY=<category or empty>
77
+ ```
78
+
79
+ Set `RF_COMMIT_MADE=true` only when the consecutive-commit count grew during this run. The job pushes when a commit was made, posts the applied-fix and dismissed-finding comments from the result JSON, and escalates the labels when a commit was made but no result was produced, or when `RF_ESCALATE=true`.