@cxi-lmai/ci-agent-platform 3.0.1 → 3.1.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 (34) hide show
  1. package/README.md +12 -4
  2. package/package.json +2 -2
  3. package/payload/INSTALL.md +8 -2
  4. package/payload/ci-templates/claude-pipeline.gitlab-ci.yml +210 -0
  5. package/payload/ci-templates/scripts/agent-architect.sh +233 -0
  6. package/payload/ci-templates/scripts/code.sh +26 -27
  7. package/payload/ci-templates/scripts/codebase-audit.sh +252 -0
  8. package/payload/ci-templates/scripts/coverage-ratchet.sh +77 -0
  9. package/payload/ci-templates/scripts/cve-fix.sh +246 -0
  10. package/payload/ci-templates/scripts/docs-sync.sh +225 -0
  11. package/payload/ci-templates/scripts/e2e-test-gen.sh +201 -0
  12. package/payload/ci-templates/scripts/lib/failure-notice.sh +104 -0
  13. package/payload/ci-templates/scripts/lib/issue-loop.sh +155 -40
  14. package/payload/ci-templates/scripts/lib/pipeline-common.sh +95 -21
  15. package/payload/ci-templates/scripts/lib/platform.sh +270 -13
  16. package/payload/ci-templates/scripts/lib/usage-capture-omp.sh +7 -1
  17. package/payload/ci-templates/scripts/lib/usage-capture.sh +7 -1
  18. package/payload/ci-templates/scripts/metrics-snapshot.sh +377 -0
  19. package/payload/ci-templates/scripts/orchestrate.sh +55 -37
  20. package/payload/ci-templates/scripts/postmortem.sh +10 -0
  21. package/payload/ci-templates/scripts/review-fix.sh +22 -11
  22. package/payload/ci-templates/scripts/review.sh +11 -7
  23. package/payload/ci-templates/scripts/test-fix.sh +7 -0
  24. package/payload/skills/agent-architect/SKILL.md +45 -0
  25. package/payload/skills/codebase-audit/SKILL.md +84 -0
  26. package/payload/skills/cve-fix/SKILL.md +98 -0
  27. package/payload/skills/docs-sync/SKILL.md +74 -0
  28. package/payload/skills/e2e-test-gen/SKILL.md +65 -0
  29. package/payload/skills/fix-review-findings/SKILL.md +2 -2
  30. package/payload/skills/fix-tests/SKILL.md +2 -2
  31. package/payload/templates/memory-index.template.md +34 -0
  32. package/payload/templates/pipeline-config.template.md +11 -1
  33. package/payload/templates/review_suppressions.template.md +55 -0
  34. package/payload/templates/spec-issue.template.md +39 -7
@@ -61,24 +61,21 @@ COMMITS=$(git rev-list "origin/$PIPE_TARGET_BRANCH..HEAD" --count 2>/dev/null ||
61
61
  # issue_implemented is emitted only when commits exist (E2E finding N-7:
62
62
  # emitting it before this check produced the metric for stuck runs too).
63
63
  if [ "${COMMITS:-0}" = "0" ]; then
64
- pipe_log "Coder made no commits, marking #$CODER_ISSUE stuck"
65
- pipe_metric_event coder issue_stuck \
66
- "$(jq -n --argjson i "$CODER_ISSUE" '{issue_iid: $i, reason: "no_commits"}')"
67
- # The agent's own account of why it stopped is the most useful thing in a
68
- # stuck comment, and it was being read and then dropped.
69
- EXPL=$(head -c 3000 "$PIPE_CONTEXT_DIR/implement.env" 2>/dev/null || true)
70
- issue_comment "$CODER_ISSUE" "šŸ¤– **Coder agent made no commits for issue #$CODER_ISSUE.**
71
-
72
- The implementation agent ran but produced no commits. A human needs to take over.
73
- ${EXPL:+
74
- What the agent reported before stopping:
75
-
76
- \`\`\`
77
- $EXPL
78
- \`\`\`
79
- }
80
- ${PIPE_JOB_URL:+CI job: $PIPE_JOB_URL}"
81
- issue_set_labels "$CODER_ISSUE" "$PIPE_LABEL_STUCK" "$PIPE_LABEL_READY,$PIPE_LABEL_WIP"
64
+ NOTICE_FILE="$PIPE_CONTEXT_DIR/implementation-failure.md"
65
+ {
66
+ printf 'šŸ¤– '
67
+ pipe_failure_notice_files "implementation of issue #$CODER_ISSUE" "implement this issue by hand" \
68
+ "$PIPE_AGENT_STDERR" "$PIPE_CONTEXT_DIR/implement.env"
69
+ } > "$NOTICE_FILE"
70
+ issue_comment_file "$CODER_ISSUE" "$NOTICE_FILE"
71
+ if pipe_is_credit_exhaustion_files "$PIPE_AGENT_STDERR" "$PIPE_CONTEXT_DIR/implement.env"; then
72
+ pipe_log "Coder credit exhausted; #$CODER_ISSUE remains $PIPE_LABEL_READY"
73
+ else
74
+ pipe_log "Coder made no commits, marking #$CODER_ISSUE stuck"
75
+ pipe_metric_event coder issue_stuck \
76
+ "$(jq -n --argjson i "$CODER_ISSUE" '{issue_iid: $i, reason: "no_commits"}')"
77
+ issue_set_labels "$CODER_ISSUE" "$PIPE_LABEL_STUCK" "$PIPE_LABEL_READY,$PIPE_LABEL_WIP"
78
+ fi
82
79
  exit 0
83
80
  fi
84
81
 
@@ -89,25 +86,27 @@ pipe_metric_event coder issue_implemented \
89
86
  git push -u origin "$CODER_BRANCH"
90
87
 
91
88
  # --- 8. Open the MR/PR ------------------------------------------------------
92
- SUMMARY=$(head -c 6000 "$PIPE_CONTEXT_DIR/mr-summary.md" 2>/dev/null || true)
93
- [ -z "$SUMMARY" ] && SUMMARY="Implemented by the ci-agent-platform coder agent."
89
+ MR_DESC_FILE="$PIPE_CONTEXT_DIR/mr-description.md"
90
+ {
91
+ printf 'Closes #%s\n\n' "$CODER_ISSUE"
92
+ if [ -s "$PIPE_CONTEXT_DIR/mr-summary.md" ]; then
93
+ cat "$PIPE_CONTEXT_DIR/mr-summary.md"
94
+ else
95
+ printf 'Implemented by the ci-agent-platform coder agent.'
96
+ fi
97
+ printf '\n\n---\nšŸ¤– Implemented by the ci-agent-platform coder\n'
98
+ } > "$MR_DESC_FILE"
94
99
 
95
100
  # Inherit the issue labels onto the MR/PR: drop the ready label, add wip.
96
101
  MR_LABELS=$( { echo "$ISSUE_LABELS" | tr ',' '\n' | grep -vxF "$PIPE_LABEL_READY" | grep -v '^$'; echo "$PIPE_LABEL_WIP"; } \
97
102
  | awk '!seen[$0]++' | paste -sd, - )
98
103
 
99
104
  MR_TITLE="Resolve \"$ISSUE_TITLE\""
100
- MR_DESC="Closes #$CODER_ISSUE
101
-
102
- $SUMMARY
103
-
104
- ---
105
- šŸ¤– Implemented by the ci-agent-platform coder"
106
105
 
107
106
  # Ensure the MR/PR labels exist (repo/project may not have them yet).
108
107
  issue_ensure_label "$PIPE_LABEL_WIP" "#3498DB" "Pipeline is working on this change"
109
108
 
110
- MR_URL=$(mr_create "$CODER_BRANCH" "$PIPE_TARGET_BRANCH" "$MR_TITLE" "$MR_DESC" "$MR_LABELS")
109
+ MR_URL=$(mr_create_file "$CODER_BRANCH" "$PIPE_TARGET_BRANCH" "$MR_TITLE" "$MR_DESC_FILE" "$MR_LABELS")
111
110
  if [ -n "$MR_URL" ]; then
112
111
  pipe_log "Opened MR/PR: $MR_URL"
113
112
  else
@@ -0,0 +1,252 @@
1
+ #!/bin/bash
2
+ # Runner for the opt-in `codebase-audit` job (periodic convention-drift scan).
3
+ # Fetches merged MRs/PRs since the last audit, runs the `/codebase-audit` skill
4
+ # in `audit` mode (delegates to the codebase-auditor agent), files the findings
5
+ # as an issue, and — when a push-capable identity is configured and the report
6
+ # suggests documentation updates — runs the skill again in `apply-docs` mode
7
+ # (delegates to the coder agent), then pushes the branch and opens the MR/PR.
8
+ # All reasoning is in the skill; this script does token, API and git work.
9
+ #
10
+ # Generalized from a single-project original: the last-audited-MR lookup, the
11
+ # MR/PR summary, and the doc-suggestion extraction moved into the skill/runner
12
+ # split, platform calls go through lib/platform.sh and lib/issue-loop.sh, and
13
+ # the codebase-auditor/coder agents run via pipe_run_agent.
14
+ set -e
15
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
16
+ # shellcheck source=lib/pipeline-common.sh
17
+ source "$SCRIPT_DIR/lib/pipeline-common.sh"
18
+ # shellcheck source=lib/issue-loop.sh
19
+ source "$SCRIPT_DIR/lib/issue-loop.sh"
20
+ pipe_defaults
21
+
22
+ CONTEXT_FILE="$PIPE_CONTEXT_DIR/audit-context.md"
23
+ REPORT_FILE="$PIPE_CONTEXT_DIR/audit-report.md"
24
+ SUGGESTIONS_FILE="$PIPE_CONTEXT_DIR/audit-doc-suggestions.md"
25
+ AUDIT_ENV="$PIPE_CONTEXT_DIR/audit.env"
26
+
27
+ # --- 1. Last-audited MR/PR IID, parsed from the most recent audit issue -----
28
+ # The most recent codebase-audit-labeled issue regardless of state: a closed
29
+ # one still marks how far the previous audit got. A genuine fetch failure
30
+ # must not be mistaken for "no prior audit issue" (which legitimately yields
31
+ # an empty array and keeps LAST_AUDITED_IID at 0): capture the helper's own
32
+ # exit status explicitly instead of piping it straight into jq.
33
+ LAST_AUDITED_IID=0
34
+ LAST_AUDIT_ISSUES=$(issues_by_label_created "$PIPE_LABEL_AUDIT" all 1) || {
35
+ pipe_log "ERROR: failed to fetch the most recent $PIPE_LABEL_AUDIT-labeled issue"
36
+ exit 1
37
+ }
38
+ LAST_AUDIT_DESC=$(jq -r '.[0].description // ""' <<< "$LAST_AUDIT_ISSUES" 2>/dev/null || echo "")
39
+ if [ -n "$LAST_AUDIT_DESC" ]; then
40
+ PARSED_IID=$(printf '%s' "$LAST_AUDIT_DESC" | grep -oE 'audited-up-to-mr-iid: [0-9]+' | grep -oE '[0-9]+$' || echo "")
41
+ [ -n "$PARSED_IID" ] && LAST_AUDITED_IID="$PARSED_IID"
42
+ fi
43
+ pipe_log "Last audited MR/PR IID: $LAST_AUDITED_IID"
44
+
45
+ # --- 2. Merged MRs/PRs since then, with a changed-file summary --------------
46
+ MRS=$(platform_merged_mrs "$PIPE_TARGET_BRANCH" "$LAST_AUDITED_IID" 50) || {
47
+ pipe_log "ERROR: failed to fetch merged MRs/PRs"
48
+ exit 1
49
+ }
50
+ MR_COUNT=$(echo "$MRS" | jq 'length' 2>/dev/null || echo "")
51
+ if ! [[ "$MR_COUNT" =~ ^[0-9]+$ ]]; then
52
+ pipe_log "ERROR: unexpected API response while counting merged MRs/PRs"
53
+ exit 1
54
+ fi
55
+ pipe_log "Analyzing $MR_COUNT recently merged MR/PR(s) (since IID $LAST_AUDITED_IID)"
56
+ [ "$MR_COUNT" = "0" ] && { pipe_log "No new MRs/PRs since last audit — skipping"; exit 0; }
57
+ MAX_AUDITED_IID=$(echo "$MRS" | jq '[.[].iid] | max // 0' 2>/dev/null || echo "")
58
+ if ! [[ "$MAX_AUDITED_IID" =~ ^[0-9]+$ ]]; then
59
+ pipe_log "ERROR: could not determine the max merged MR/PR IID from the API response"
60
+ exit 1
61
+ fi
62
+
63
+ MR_SUMMARY=""
64
+ while IFS= read -r IID; do
65
+ TITLE=$(echo "$MRS" | jq -r --argjson i "$IID" '.[] | select(.iid == $i) | .title')
66
+ # A single MR's changed-file fetch failing is an acceptable degradation (an
67
+ # empty file list in its summary line), not grounds to abort the whole audit.
68
+ FILES=$(platform_mr_changes "$IID" 10 | paste -sd, -) || true
69
+ MR_SUMMARY="${MR_SUMMARY}MR/PR !${IID} ${TITLE} -- Files ${FILES}; "
70
+ done < <(echo "$MRS" | jq -r '.[].iid')
71
+
72
+ # --- 3. Open audit issues, as dedup context for the auditor ------------------
73
+ OPEN_AUDIT_CONTEXT=""
74
+ # Dedup context only tells the auditor which findings are already filed; a
75
+ # transient fetch failure here must not abort the whole scheduled audit
76
+ # (unlike step 1's watermark read, which is load-bearing). Degrade to an
77
+ # empty result and continue, analogous to a single MR's changes-fetch
78
+ # degradation in step 2.
79
+ OPEN_AUDITS=$(issues_by_label_created "$PIPE_LABEL_AUDIT" opened 5) || {
80
+ pipe_log "WARNING: failed to fetch open $PIPE_LABEL_AUDIT-labeled issues for dedup context — continuing without it"
81
+ OPEN_AUDITS="[]"
82
+ }
83
+ OPEN_COUNT=$(echo "$OPEN_AUDITS" | jq 'length' 2>/dev/null || echo 0)
84
+ if [ "$OPEN_COUNT" != "0" ] && [ "$OPEN_COUNT" != "null" ]; then
85
+ OPEN_AUDIT_CONTEXT=$(echo "$OPEN_AUDITS" | jq -r '.[] | "Issue #\(.iid) (\(.created_at[0:10])): \(.title)\n\(.description[0:800])\n---"' 2>/dev/null | head -c 8000 || echo "")
86
+ fi
87
+
88
+ # --- 4. Run the /codebase-audit skill in audit mode --------------------------
89
+ # Write the context file the skill and the codebase-auditor agent read.
90
+ # Unbounded text stays out of argv/env; this content is already bounded above.
91
+ rm -f "$AUDIT_ENV" "$REPORT_FILE" "$SUGGESTIONS_FILE"
92
+ {
93
+ echo "# Codebase audit context"
94
+ echo
95
+ echo "Mode: audit"
96
+ echo
97
+ echo "## Recently merged MRs/PRs"
98
+ echo
99
+ printf '%s\n' "$MR_SUMMARY"
100
+ if [ -n "$OPEN_AUDIT_CONTEXT" ]; then
101
+ echo
102
+ echo "## Already-open ${PIPE_LABEL_AUDIT}-labeled issues (skip findings already covered here)"
103
+ echo
104
+ printf '%s\n' "$OPEN_AUDIT_CONTEXT"
105
+ fi
106
+ } > "$CONTEXT_FILE"
107
+
108
+ pipe_run_agent codebase-audit "/codebase-audit" "Agent,Read,Write,Edit,Glob,Grep,Bash" "$PIPE_MODEL_TRIAGE" < /dev/null
109
+
110
+ AUDIT_STATUS=$(pipe_get_env "$AUDIT_ENV" AUDIT_STATUS)
111
+ AUDIT_FINDINGS_TOTAL=$(pipe_get_env "$AUDIT_ENV" AUDIT_FINDINGS_TOTAL); AUDIT_FINDINGS_TOTAL=${AUDIT_FINDINGS_TOTAL:-0}
112
+ # pipe_get_env does no shell expansion: a marker that copies the sample line
113
+ # literally (e.g. `AUDIT_REPORT_PATH=$PIPE_CONTEXT_DIR/audit-report.md`
114
+ # unexpanded) or otherwise points at a path that doesn't exist must not
115
+ # discard a perfectly good report at the runner's own default path (review
116
+ # round 2 finding).
117
+ AUDIT_REPORT_PATH=$(pipe_get_env "$AUDIT_ENV" AUDIT_REPORT_PATH)
118
+ if [ ! -s "$AUDIT_REPORT_PATH" ] && [ -s "$REPORT_FILE" ]; then
119
+ AUDIT_REPORT_PATH="$REPORT_FILE"
120
+ fi
121
+ AUDIT_REPORT_PATH=${AUDIT_REPORT_PATH:-$REPORT_FILE}
122
+
123
+ # --- 5. Empty output, or a missing/empty report file: log-only notice -------
124
+ # (no issue exists yet to post to). A model-authored marker claiming ok/
125
+ # no_findings with no actual report on disk is a reachable contract failure,
126
+ # not just a literal no_output status; guard the file the same way
127
+ # docs-sync.sh guards its own agent output before touching it.
128
+ if [ -z "$AUDIT_STATUS" ] || [ "$AUDIT_STATUS" = "no_output" ] || [ ! -s "$AUDIT_REPORT_PATH" ]; then
129
+ pipe_failure_log_notice_files "the periodic codebase audit" \
130
+ "audit recent MRs/PRs by hand for convention violations, or re-run this job after topping up" \
131
+ "$PIPE_AGENT_STDERR" "$AUDIT_REPORT_PATH" | while IFS= read -r line; do pipe_log "$line"; done
132
+ exit 0
133
+ fi
134
+
135
+ # --- 6. No findings above threshold: nothing to file --------------------------
136
+ if [ "$AUDIT_STATUS" = "no_findings" ]; then
137
+ pipe_log "No findings above threshold in either section — skipping issue creation"
138
+ exit 0
139
+ fi
140
+
141
+ # --- 7. File the findings as an issue -----------------------------------------
142
+ issue_ensure_label "$PIPE_LABEL_AUDIT" "#6F42C1" "Periodic convention-drift audit findings"
143
+ ISSUE_BODY_FILE="$PIPE_CONTEXT_DIR/audit-issue-body.md"
144
+ {
145
+ printf '<!-- audited-up-to-mr-iid: %s -->\n' "$MAX_AUDITED_IID"
146
+ head -c 20000 "$AUDIT_REPORT_PATH"
147
+ } > "$ISSUE_BODY_FILE"
148
+ ISSUE_IID=$(issue_create_file "Codebase Audit — $(date +%Y-%m-%d)" "$ISSUE_BODY_FILE" "$PIPE_LABEL_AUDIT")
149
+ if [ -z "$ISSUE_IID" ]; then
150
+ pipe_log "ERROR: failed to create the audit findings issue"
151
+ exit 0
152
+ fi
153
+ pipe_log "Filed audit findings as #$ISSUE_IID"
154
+ # The runner is the one that knows the IID: append it now so the marker
155
+ # artifact is complete when the job finishes (see the skill's own contract).
156
+ printf 'AUDIT_ISSUE_IID=%s\n' "$ISSUE_IID" >> "$AUDIT_ENV"
157
+
158
+ pipe_metric_event codebase-audit audit_filed \
159
+ "$(jq -n --argjson i "$ISSUE_IID" --argjson f "$AUDIT_FINDINGS_TOTAL" '{issue_iid: $i, findings_total: $f}')"
160
+
161
+ # --- 8. No push-capable identity: audit-only run, stop here -----------------
162
+ PUSH_TOKEN=""
163
+ case "$PIPE_PLATFORM" in
164
+ gitlab) PUSH_TOKEN="${PIPE_TOKEN:-}" ;;
165
+ github) PUSH_TOKEN="${GH_TOKEN:-${GITHUB_TOKEN:-}}" ;;
166
+ esac
167
+ if [ -z "$PUSH_TOKEN" ]; then
168
+ pipe_log "No push-capable identity configured — skipping auto-fix phase"
169
+ exit 0
170
+ fi
171
+
172
+ # --- 9. No doc-update suggestions: nothing for the coder to apply -----------
173
+ if [ ! -s "$SUGGESTIONS_FILE" ]; then
174
+ pipe_log "No documentation update suggestions — skipping auto-fix phase"
175
+ exit 0
176
+ fi
177
+ pipe_log "Doc suggestions found — spawning the coder agent"
178
+
179
+ # --- 10. Branch, then run the skill again in apply-docs mode -----------------
180
+ pipe_git_identity
181
+ git fetch origin "$PIPE_TARGET_BRANCH"
182
+ BRANCH="audit-$(date +%Y-%m-%d)"
183
+ # A same-day retry (transient failure, manual re-trigger) can find this
184
+ # branch already on the remote from an earlier attempt. Fetch it so the
185
+ # --force-with-lease push below has a known expected state instead of
186
+ # refusing for lack of a remote-tracking ref.
187
+ git fetch origin "$BRANCH" 2>/dev/null || true
188
+ git checkout -B "$BRANCH" "origin/$PIPE_TARGET_BRANCH"
189
+
190
+ APPLY_ENV="$PIPE_CONTEXT_DIR/audit-apply.env"
191
+ rm -f "$APPLY_ENV"
192
+ {
193
+ echo "# Codebase audit apply-docs context"
194
+ echo
195
+ echo "Mode: apply-docs"
196
+ echo
197
+ echo "Issue: #$ISSUE_IID"
198
+ echo "Documentation roots: $PIPE_DOCS_ROOTS"
199
+ echo "Commit subject: $PIPE_COMMIT_AUDIT"
200
+ echo "Doc suggestions file: $SUGGESTIONS_FILE"
201
+ echo "Full audit report: $AUDIT_REPORT_PATH"
202
+ } > "$CONTEXT_FILE"
203
+
204
+ pipe_run_agent codebase-audit-apply "/codebase-audit" "Agent,Read,Write,Edit,Glob,Grep,Bash" "$PIPE_MODEL_CODE" < /dev/null
205
+
206
+ # --- 11. Authoritative commit count (the runner owns the push decision) ------
207
+ AHEAD=$(git rev-list "origin/$PIPE_TARGET_BRANCH..HEAD" --count 2>/dev/null || echo 0)
208
+ if [ "${AHEAD:-0}" = "0" ]; then
209
+ pipe_log "Coder made no commits — skipping MR/PR"
210
+ # "No commits" reads as "nothing needed fixing", but a credit outage produces
211
+ # the same silence. Issue #$ISSUE_IID already holds the findings, so on a
212
+ # credit outage tell its readers the apply step never ran rather than
213
+ # leaving them to assume it found nothing.
214
+ pipe_failure_log_notice_files "the audit apply step" \
215
+ "apply the findings in this issue by hand" \
216
+ "$PIPE_AGENT_STDERR" "$APPLY_ENV" | while IFS= read -r line; do pipe_log "$line"; done
217
+ if pipe_is_credit_exhaustion_files "$PIPE_AGENT_STDERR" "$APPLY_ENV"; then
218
+ NOTICE_FILE="$PIPE_CONTEXT_DIR/audit-apply-credit-notice.md"
219
+ pipe_failure_notice_files "application of these audit findings" \
220
+ "apply the findings above by hand, or re-run the audit job after topping up" \
221
+ "$PIPE_AGENT_STDERR" "$APPLY_ENV" > "$NOTICE_FILE"
222
+ issue_comment_file "$ISSUE_IID" "$NOTICE_FILE"
223
+ pipe_log "Posted credit-exhaustion notice to issue #$ISSUE_IID"
224
+ fi
225
+ git checkout "$PIPE_TARGET_BRANCH" 2>/dev/null || true
226
+ git branch -D "$BRANCH" 2>/dev/null || true
227
+ exit 0
228
+ fi
229
+
230
+ pipe_metric_event codebase-audit audit_apply_committed \
231
+ "$(jq -n --argjson i "$ISSUE_IID" --argjson c "$AHEAD" '{issue_iid: $i, commits: $c}')"
232
+
233
+ # --- 12. Push and open the MR/PR ---------------------------------------------
234
+ # --force-with-lease: this is a dedicated bot branch reset fresh from the
235
+ # target branch every run, so a same-day retry intentionally replaces
236
+ # whatever an earlier attempt left on the remote. --force-with-lease (rather
237
+ # than a bare --force) still refuses if the branch changed unexpectedly
238
+ # between the fetch above and now.
239
+ git push --force-with-lease -u origin "$BRANCH"
240
+
241
+ MR_DESC_FILE="$PIPE_CONTEXT_DIR/audit-mr-description.md"
242
+ {
243
+ printf 'Automated documentation updates from the periodic convention audit.\n\nCloses #%s\n\n---\n\n' "$ISSUE_IID"
244
+ head -c 10000 "$AUDIT_REPORT_PATH"
245
+ } > "$MR_DESC_FILE"
246
+
247
+ MR_URL=$(mr_create_file "$BRANCH" "$PIPE_TARGET_BRANCH" "docs: codebase audit updates ($(date +%Y-%m-%d))" "$MR_DESC_FILE" "$PIPE_LABEL_AUDIT")
248
+ if [ -n "$MR_URL" ]; then
249
+ pipe_log "Opened MR/PR: $MR_URL"
250
+ else
251
+ pipe_log "WARNING: MR/PR creation returned no URL (it may already exist for this branch)"
252
+ fi
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bash
2
+ # Opt-in coverage gate. Reads the configured coverage report, compares it with
3
+ # the target branch's latest successful pipeline, and sends coverage drops on
4
+ # wip merge requests into the existing test-fix loop.
5
+ set -e
6
+
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
+ coverage_from_jacoco() {
13
+ local covered missed
14
+ if ! covered=$(xmllint --xpath 'string(//report/counter[@type="INSTRUCTION"]/@covered)' \
15
+ "$PIPE_COVERAGE_REPORT" 2>/dev/null) ||
16
+ ! missed=$(xmllint --xpath 'string(//report/counter[@type="INSTRUCTION"]/@missed)' \
17
+ "$PIPE_COVERAGE_REPORT" 2>/dev/null); then
18
+ echo "ERROR: unable to parse JaCoCo instruction coverage from: $PIPE_COVERAGE_REPORT" >&2
19
+ return 1
20
+ fi
21
+
22
+ if [[ "$covered" =~ ^[0-9]+$ ]] && [[ "$missed" =~ ^[0-9]+$ ]] && \
23
+ [ "$((covered + missed))" -gt 0 ]; then
24
+ printf '%s\n' "$((covered * 100 / (covered + missed)))"
25
+ else
26
+ echo "ERROR: unable to parse JaCoCo instruction coverage from: $PIPE_COVERAGE_REPORT" >&2
27
+ return 1
28
+ fi
29
+ }
30
+
31
+ case "$PIPE_COVERAGE_REPORT_KIND" in
32
+ jacoco) ;;
33
+ *) echo "ERROR: unsupported coverage report kind: $PIPE_COVERAGE_REPORT_KIND" >&2; exit 1 ;;
34
+ esac
35
+
36
+ [ -z "${PIPE_MR_IID:-}" ] && exit 0
37
+
38
+ if [ -z "$PIPE_COVERAGE_REPORT" ] || [ ! -f "$PIPE_COVERAGE_REPORT" ]; then
39
+ echo "Coverage ratchet inactive: coverage report is not available: ${PIPE_COVERAGE_REPORT:-<unset>}"
40
+ exit 0
41
+ fi
42
+
43
+ command -v xmllint >/dev/null 2>&1 || {
44
+ echo "ERROR: xmllint is required to parse PIPE_COVERAGE_REPORT" >&2
45
+ exit 1
46
+ }
47
+
48
+ CURRENT_COV=$(coverage_from_jacoco)
49
+
50
+ if [ "$PIPE_COVERAGE_RATCHET" = "1" ] && [ -z "$PIPE_COVERAGE_SIGNAL" ]; then
51
+ echo "ERROR: PIPE_COVERAGE_SIGNAL must be set when PIPE_COVERAGE_RATCHET=1" >&2
52
+ exit 1
53
+ fi
54
+
55
+ BASELINE=$(platform_latest_success_coverage "$PIPE_TARGET_BRANCH" 2>/dev/null || true)
56
+ BASELINE=${BASELINE%%.*}
57
+ [[ "$BASELINE" =~ ^[0-9]+$ ]] || BASELINE=0
58
+
59
+ echo "Total coverage: ${CURRENT_COV}%"
60
+ echo "Coverage: current=${CURRENT_COV}% baseline=${BASELINE}%"
61
+
62
+ if [ "$CURRENT_COV" -lt "$BASELINE" ]; then
63
+ echo "WARNING: Coverage dropped from ${BASELINE}% to ${CURRENT_COV}%"
64
+ COMMENT_FILE="$PIPE_CONTEXT_DIR/coverage-ratchet.md"
65
+ printf '**Coverage dropped** from %s%% to %s%%. Please check whether new code is missing tests.\n' \
66
+ "$BASELINE" "$CURRENT_COV" > "$COMMENT_FILE"
67
+ platform_post_comment_file "$COMMENT_FILE" || true
68
+
69
+ LABELS=$(platform_mr_labels)
70
+ case ",$LABELS," in
71
+ *",$PIPE_LABEL_WIP,"*)
72
+ printf 'COVERAGE_DROP:%s:%s\n' "$BASELINE" "$CURRENT_COV" > "$PIPE_COVERAGE_SIGNAL"
73
+ echo "Coverage dropped on $PIPE_LABEL_WIP MR/PR — exiting 1 to trigger test-fix"
74
+ exit 1
75
+ ;;
76
+ esac
77
+ fi
@@ -0,0 +1,246 @@
1
+ #!/bin/bash
2
+ # Runner for the opt-in CVE remediation job. A project-local scanner supplies
3
+ # HIGH/CRITICAL identifiers in PIPE_CVE_LIST; this runner scopes the coder,
4
+ # verifies its commit and compile result, and alone owns rebase/push/comments.
5
+ set -euo pipefail
6
+
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
+ CONTEXT_FILE="$PIPE_CONTEXT_DIR/cve-fix-context.md"
13
+ CVE_LIST_FILE="$PIPE_CONTEXT_DIR/cve-fix-list.txt"
14
+ MANIFEST_FILE="$PIPE_CONTEXT_DIR/cve-fix-manifest.txt"
15
+ CVE_ENV="$PIPE_CONTEXT_DIR/cve-fix.env"
16
+ COMMENT_FILE="$PIPE_CONTEXT_DIR/cve-fix-comment.md"
17
+
18
+ # The scanner controls both selection and severity filtering. Bound its output
19
+ # before it reaches a context or comment, and never forward it in argv/env.
20
+ head -c 8000 <<< "${PIPE_CVE_LIST:-}" > "$CVE_LIST_FILE"
21
+ # A cap can cut the final list entry mid-line. Add only a record terminator
22
+ # before filtering/counting; the caller-provided content remains bounded.
23
+ printf '\n' >> "$CVE_LIST_FILE"
24
+ sed -i '/^[[:space:]]*$/d' "$CVE_LIST_FILE"
25
+ CVE_COUNT=$(awk 'NF { count++ } END { print count + 0 }' "$CVE_LIST_FILE")
26
+
27
+ write_marker() {
28
+ # Marker values are canonical runner output. Never source model-written data.
29
+ printf 'CVE_FIX_APPLIED=%s\nCVE_COMPILED=%s\nCVE_COUNT=%s\nCVE_MANUAL_REQUIRED=%s\n' \
30
+ "$1" "$2" "$CVE_COUNT" "$3" > "$CVE_ENV"
31
+ }
32
+
33
+ post_comment() {
34
+ # Posts the complete, bounded body already written to $COMMENT_FILE.
35
+ platform_post_comment_file "$COMMENT_FILE" || pipe_log "Could not post CVE remediation comment"
36
+ }
37
+
38
+ post_manual_comment() {
39
+ # $1 is the short manual-action explanation. The list is bounded above.
40
+ {
41
+ printf '**Automated CVE remediation requires manual action.**\n\n%s\n' "$1"
42
+ if [ -s "$CVE_LIST_FILE" ]; then
43
+ printf '\nAffected HIGH/CRITICAL vulnerability identifiers:\n'
44
+ sed 's/^/- /' "$CVE_LIST_FILE"
45
+ fi
46
+ } > "$COMMENT_FILE"
47
+ post_comment
48
+ }
49
+
50
+ count_matching_commits() {
51
+ local count=0 subject
52
+ while IFS= read -r subject; do
53
+ [ "$subject" = "$PIPE_COMMIT_CVEFIX" ] && count=$((count + 1))
54
+ done < <(git log "origin/$PIPE_TARGET_BRANCH..HEAD" --format='%s' 2>/dev/null)
55
+ printf '%s\n' "$count"
56
+ }
57
+
58
+ marker_is_valid() {
59
+ local line applied=0 compiled=0 count=0 manual=0
60
+ [ -f "$CVE_ENV" ] || return 1
61
+ while IFS= read -r line || [ -n "$line" ]; do
62
+ case "$line" in
63
+ CVE_FIX_APPLIED=0|CVE_FIX_APPLIED=1) applied=$((applied + 1)) ;;
64
+ CVE_COMPILED=0|CVE_COMPILED=1) compiled=$((compiled + 1)) ;;
65
+ CVE_MANUAL_REQUIRED=0|CVE_MANUAL_REQUIRED=1) manual=$((manual + 1)) ;;
66
+ CVE_COUNT=*)
67
+ [[ "$line" =~ ^CVE_COUNT=[0-9]+$ ]] || return 1
68
+ count=$((count + 1))
69
+ ;;
70
+ *) return 1 ;;
71
+ esac
72
+ done < "$CVE_ENV"
73
+ [ "$applied" -eq 1 ] && [ "$compiled" -eq 1 ] && [ "$count" -eq 1 ] && [ "$manual" -eq 1 ]
74
+ }
75
+
76
+ marker_value() {
77
+ # marker_is_valid has already limited this to a literal 0, 1, or decimal.
78
+ pipe_get_env "$CVE_ENV" "$1"
79
+ }
80
+
81
+ # This job has no purpose outside a merge/pull-request pipeline. Do not attempt
82
+ # a forge lookup that can accidentally turn a branch pipeline into a comment.
83
+ if [ -z "${PIPE_MR_IID:-}" ]; then
84
+ pipe_log "No merge request/pull request context; CVE remediation is inactive"
85
+ exit 0
86
+ fi
87
+
88
+ # Empty scanner output is normal and must remain silent. Check it before any
89
+ # metadata/WIP gate so enabling the optional job does not spam ordinary MRs.
90
+ if [ "$CVE_COUNT" = "0" ]; then
91
+ pipe_log "No vulnerability identifiers supplied; CVE remediation is inactive"
92
+ write_marker 0 0 1
93
+ exit 0
94
+ fi
95
+
96
+ # The list was supplied by the caller and can be arbitrarily large. The only
97
+ # bounded copy is the context file above; do not let the dispatcher inherit it.
98
+ unset PIPE_CVE_LIST
99
+
100
+ META=$(platform_mr_meta 2>/dev/null || true)
101
+ MR_LABELS=$(jq -r '.labels // ""' <<< "$META" 2>/dev/null || true)
102
+ META_SOURCE_BRANCH=$(jq -r '.source_branch // ""' <<< "$META" 2>/dev/null || true)
103
+ if [ -n "$META_SOURCE_BRANCH" ]; then
104
+ PIPE_SOURCE_BRANCH="$META_SOURCE_BRANCH"
105
+ export PIPE_SOURCE_BRANCH
106
+ fi
107
+ if [ -z "${PIPE_SOURCE_BRANCH:-}" ]; then
108
+ pipe_log "MR/PR metadata has no source branch"
109
+ post_manual_comment "The source branch metadata is unavailable; update the dependencies manually."
110
+ write_marker 0 0 1
111
+ exit 0
112
+ fi
113
+
114
+ if ! grep -qF "$PIPE_LABEL_WIP" <<< "$MR_LABELS"; then
115
+ pipe_log "MR/PR is not labeled $PIPE_LABEL_WIP; requesting a manual dependency update"
116
+ post_manual_comment "This MR/PR is not marked for autonomous changes; update the dependencies manually."
117
+ write_marker 0 0 1
118
+ exit 0
119
+ fi
120
+
121
+ # A dependency manifest is project-relative by contract. Reject paths that
122
+ # could point outside this checkout before using them in git or file commands.
123
+ case "$PIPE_DEPENDENCY_MANIFEST" in
124
+ ''|/*|..|../*|*/..|*/../*)
125
+ pipe_log "Invalid project-relative dependency manifest path: $PIPE_DEPENDENCY_MANIFEST"
126
+ post_manual_comment "The configured dependency manifest path is invalid; update the dependencies manually."
127
+ write_marker 0 0 1
128
+ exit 0
129
+ ;;
130
+ esac
131
+
132
+ pipe_git_identity
133
+ pipe_checkout_source
134
+ if ! git rev-parse --verify "origin/$PIPE_SOURCE_BRANCH" >/dev/null 2>&1 \
135
+ || [ "$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)" != "$PIPE_SOURCE_BRANCH" ]; then
136
+ pipe_log "Unable to check out source branch safely: $PIPE_SOURCE_BRANCH"
137
+ post_manual_comment "The source branch could not be checked out safely; update the dependencies manually."
138
+ write_marker 0 0 1
139
+ exit 0
140
+ fi
141
+
142
+ EXISTING_FIXES=$(count_matching_commits)
143
+ if [ "$EXISTING_FIXES" -ge "$PIPE_FIX_LOOP_CAP" ]; then
144
+ pipe_log "CVE remediation reached its commit budget ($EXISTING_FIXES/$PIPE_FIX_LOOP_CAP)"
145
+ post_manual_comment "The CVE remediation commit budget is exhausted; update the dependencies manually."
146
+ write_marker 0 0 1
147
+ exit 0
148
+ fi
149
+
150
+ if [ ! -f "$PIPE_DEPENDENCY_MANIFEST" ]; then
151
+ pipe_log "Dependency manifest is missing: $PIPE_DEPENDENCY_MANIFEST"
152
+ post_manual_comment "Dependency manifest is missing: \`$PIPE_DEPENDENCY_MANIFEST\`. Update the dependencies manually."
153
+ write_marker 0 0 1
154
+ exit 0
155
+ fi
156
+ head -c 8000 "$PIPE_DEPENDENCY_MANIFEST" > "$MANIFEST_FILE"
157
+ REMAINING_BUDGET=$((PIPE_FIX_LOOP_CAP - EXISTING_FIXES))
158
+ rm -f "$CVE_ENV"
159
+ {
160
+ printf '# CVE remediation context\n\n'
161
+ printf 'Mode: apply\n'
162
+ printf 'Vulnerability List File: %s\n' "$CVE_LIST_FILE"
163
+ printf 'Dependency Manifest Path: %s\n' "$PIPE_DEPENDENCY_MANIFEST"
164
+ printf 'Dependency Manifest Content File: %s\n' "$MANIFEST_FILE"
165
+ printf 'Compile Command: %s\n' "$PIPE_COMPILE_CMD"
166
+ printf 'Commit Subject: %s\n' "$PIPE_COMMIT_CVEFIX"
167
+ printf 'Source Branch: %s\n' "$PIPE_SOURCE_BRANCH"
168
+ printf 'Target Branch: %s\n' "$PIPE_TARGET_BRANCH"
169
+ printf 'Suppression Path: %s\n' "$PIPE_CVE_SUPPRESSIONS"
170
+ printf 'Remaining Commit Budget: %s\n' "$REMAINING_BUDGET"
171
+ printf 'Marker File: %s\n' "$CVE_ENV"
172
+ } > "$CONTEXT_FILE"
173
+
174
+ # The skill decides dependency versions. This is the same code-writing policy
175
+ # as other runners; it receives no shell-built remediation prompt.
176
+ PRE_AGENT_SHA=$(git rev-parse HEAD)
177
+ pipe_run_agent cve-fix "/cve-fix" "Agent,Read,Write,Edit,Glob,Grep,Bash" "$PIPE_MODEL_CODE" < /dev/null
178
+ POST_AGENT_SHA=$(git rev-parse HEAD)
179
+ NEW_FIXES=$(count_matching_commits)
180
+ RANGE_COMMITS=$(git rev-list --count "$PRE_AGENT_SHA..$POST_AGENT_SHA" 2>/dev/null || echo 0)
181
+ RANGE_HAS_DIFF=false
182
+ if ! git diff --quiet "$PRE_AGENT_SHA..$POST_AGENT_SHA" 2>/dev/null; then
183
+ RANGE_HAS_DIFF=true
184
+ fi
185
+ RANGE_MANIFEST_ONLY=true
186
+ while IFS= read -r path; do
187
+ [ "$path" = "$PIPE_DEPENDENCY_MANIFEST" ] || { RANGE_MANIFEST_ONLY=false; break; }
188
+ done < <(git diff --name-only "$PRE_AGENT_SHA..$POST_AGENT_SHA" 2>/dev/null)
189
+
190
+ if ! marker_is_valid || [ "$(marker_value CVE_FIX_APPLIED)" != "1" ] \
191
+ || [ "$(marker_value CVE_COMPILED)" != "1" ] || [ "$(marker_value CVE_MANUAL_REQUIRED)" != "0" ] \
192
+ || [ "$NEW_FIXES" -ne $((EXISTING_FIXES + 1)) ] || [ "$RANGE_COMMITS" != "1" ] \
193
+ || [ "$(git log -1 --format='%s' 2>/dev/null)" != "$PIPE_COMMIT_CVEFIX" ] \
194
+ || [ "$RANGE_HAS_DIFF" != true ] || [ "$RANGE_MANIFEST_ONLY" != true ]; then
195
+ # This range and its dirty tracked files belong to the agent. Restore the
196
+ # checked-out remote tip; do not carry unverified work into a later rebase.
197
+ git reset --hard "$PRE_AGENT_SHA" >/dev/null 2>&1 || git checkout -- . || true
198
+ pipe_failure_log_notice_files "CVE remediation" "update the listed dependencies manually" "$PIPE_AGENT_STDERR" >&2
199
+ {
200
+ pipe_failure_notice_files "CVE remediation" "update the listed dependencies manually" "$PIPE_AGENT_STDERR"
201
+ printf '\n'
202
+ if [ -s "$CVE_LIST_FILE" ]; then
203
+ printf 'Affected HIGH/CRITICAL vulnerability identifiers:\n'
204
+ sed 's/^/- /' "$CVE_LIST_FILE"
205
+ fi
206
+ printf '\nThe coder made no verified remediation commit, so manual dependency update is required.\n'
207
+ } > "$COMMENT_FILE"
208
+ post_comment
209
+ write_marker 0 0 1
210
+ exit 0
211
+ fi
212
+
213
+ # The accepted commit is authoritative. Reset index and worktree to it so
214
+ # compile/rebase see exactly what the runner will push; context artifacts stay
215
+ # untracked and are therefore preserved.
216
+ git reset --hard HEAD
217
+
218
+ if [ -z "$PIPE_COMPILE_CMD" ]; then
219
+ pipe_log "Configured compile command is empty; refusing to push an unverified CVE remediation"
220
+ post_manual_comment "The configured compile command is empty, so the automated dependency update cannot be verified."
221
+ write_marker 1 0 1
222
+ exit 1
223
+ fi
224
+ if ! bash -c "$PIPE_COMPILE_CMD"; then
225
+ pipe_log "CVE remediation compile command failed; refusing to push"
226
+ post_manual_comment "The automated dependency update was committed locally but the compile command failed; manual review is required."
227
+ write_marker 1 0 1
228
+ exit 1
229
+ fi
230
+
231
+ if ! pipe_rebase_and_push; then
232
+ pipe_log "CVE remediation rebase/push failed; human work was not overwritten"
233
+ post_manual_comment "The automated dependency update could not be rebased and pushed safely; resolve the branch changes manually."
234
+ write_marker 1 1 1
235
+ exit 1
236
+ fi
237
+
238
+ {
239
+ printf '**Automated CVE remediation succeeded.**\n\n'
240
+ printf 'The configured dependency manifest was updated, compiled, rebased, and pushed.\n\n'
241
+ printf 'Remediated HIGH/CRITICAL vulnerability identifiers:\n'
242
+ sed 's/^/- /' "$CVE_LIST_FILE"
243
+ } > "$COMMENT_FILE"
244
+ post_comment
245
+ write_marker 1 1 0
246
+ pipe_log "Pushed verified CVE remediation commit"