@cxi-lmai/ci-agent-platform 3.0.1 → 3.1.1

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 (64) hide show
  1. package/README.md +12 -4
  2. package/package.json +2 -2
  3. package/payload/INSTALL.md +8 -2
  4. package/payload/agents/agent-architect.md +1 -1
  5. package/payload/agents/code-reviewer.md +1 -1
  6. package/payload/agents/codebase-auditor.md +1 -1
  7. package/payload/agents/coder.md +1 -1
  8. package/payload/agents/decomposer.md +1 -1
  9. package/payload/agents/docs-sync.md +1 -1
  10. package/payload/agents/e2e-test-writer.md +1 -1
  11. package/payload/agents/performance-reviewer.md +1 -1
  12. package/payload/agents/release-mr.md +1 -1
  13. package/payload/agents/security-reviewer.md +1 -1
  14. package/payload/agents/test-fix.md +1 -1
  15. package/payload/agents/test-writer.md +1 -1
  16. package/payload/agents-omp/agent-architect.md +1 -1
  17. package/payload/agents-omp/code-reviewer.md +1 -1
  18. package/payload/agents-omp/codebase-auditor.md +1 -1
  19. package/payload/agents-omp/coder.md +1 -1
  20. package/payload/agents-omp/decomposer.md +1 -1
  21. package/payload/agents-omp/docs-sync.md +1 -1
  22. package/payload/agents-omp/e2e-test-writer.md +1 -1
  23. package/payload/agents-omp/migration-reviewer.md +1 -1
  24. package/payload/agents-omp/orchestrator.md +1 -1
  25. package/payload/agents-omp/performance-reviewer.md +1 -1
  26. package/payload/agents-omp/postmortem.md +1 -1
  27. package/payload/agents-omp/release-mr.md +1 -1
  28. package/payload/agents-omp/security-reviewer.md +1 -1
  29. package/payload/agents-omp/test-fix.md +1 -1
  30. package/payload/agents-omp/test-writer.md +1 -1
  31. package/payload/ci-templates/claude-pipeline.gitlab-ci.yml +210 -0
  32. package/payload/ci-templates/github/claude-issue-pipeline.yml +1 -1
  33. package/payload/ci-templates/github/claude-pipeline.yml +2 -2
  34. package/payload/ci-templates/github/claude-test-fix.yml +1 -1
  35. package/payload/ci-templates/scripts/agent-architect.sh +233 -0
  36. package/payload/ci-templates/scripts/code.sh +26 -27
  37. package/payload/ci-templates/scripts/codebase-audit.sh +252 -0
  38. package/payload/ci-templates/scripts/coverage-ratchet.sh +77 -0
  39. package/payload/ci-templates/scripts/cve-fix.sh +246 -0
  40. package/payload/ci-templates/scripts/docs-sync.sh +225 -0
  41. package/payload/ci-templates/scripts/e2e-test-gen.sh +201 -0
  42. package/payload/ci-templates/scripts/lib/failure-notice.sh +104 -0
  43. package/payload/ci-templates/scripts/lib/issue-loop.sh +155 -40
  44. package/payload/ci-templates/scripts/lib/pipeline-common.sh +100 -26
  45. package/payload/ci-templates/scripts/lib/platform.sh +270 -13
  46. package/payload/ci-templates/scripts/lib/usage-capture-omp.sh +7 -1
  47. package/payload/ci-templates/scripts/lib/usage-capture.sh +7 -1
  48. package/payload/ci-templates/scripts/metrics-snapshot.sh +377 -0
  49. package/payload/ci-templates/scripts/orchestrate.sh +55 -37
  50. package/payload/ci-templates/scripts/postmortem.sh +10 -0
  51. package/payload/ci-templates/scripts/review-fix.sh +22 -11
  52. package/payload/ci-templates/scripts/review.sh +11 -7
  53. package/payload/ci-templates/scripts/test-fix.sh +7 -0
  54. package/payload/skills/agent-architect/SKILL.md +45 -0
  55. package/payload/skills/codebase-audit/SKILL.md +84 -0
  56. package/payload/skills/cve-fix/SKILL.md +98 -0
  57. package/payload/skills/docs-sync/SKILL.md +74 -0
  58. package/payload/skills/e2e-test-gen/SKILL.md +65 -0
  59. package/payload/skills/fix-review-findings/SKILL.md +2 -2
  60. package/payload/skills/fix-tests/SKILL.md +2 -2
  61. package/payload/templates/memory-index.template.md +34 -0
  62. package/payload/templates/pipeline-config.template.md +11 -1
  63. package/payload/templates/review_suppressions.template.md +55 -0
  64. package/payload/templates/spec-issue.template.md +39 -7
@@ -74,7 +74,7 @@ env:
74
74
  PIPE_GIT_NAME: ${{ vars.PIPE_GIT_NAME || 'Pipeline Bot' }}
75
75
  PIPE_GIT_EMAIL: ${{ vars.PIPE_GIT_EMAIL || 'bot@pipeline.ci' }}
76
76
  PIPE_MODEL_TRIAGE: ${{ vars.PIPE_MODEL_TRIAGE || 'haiku' }}
77
- PIPE_MODEL_CODE: ${{ vars.PIPE_MODEL_CODE || 'claude-sonnet-5-0' }}
77
+ PIPE_MODEL_CODE: ${{ vars.PIPE_MODEL_CODE || 'claude-sonnet-5' }}
78
78
  PIPE_AGENT_ENV_ALLOWLIST: ${{ vars.PIPE_AGENT_ENV_ALLOWLIST || '' }}
79
79
  # Runtime mapping onto the PIPE_ names the scripts read.
80
80
  PIPE_REPO: ${{ github.repository }}
@@ -44,8 +44,8 @@ env:
44
44
  PIPE_COMMIT_REVIEWFIX: ${{ vars.PIPE_COMMIT_REVIEWFIX || 'Fix review findings' }}
45
45
  PIPE_GIT_NAME: ${{ vars.PIPE_GIT_NAME || 'Pipeline Bot' }}
46
46
  PIPE_GIT_EMAIL: ${{ vars.PIPE_GIT_EMAIL || 'bot@pipeline.ci' }}
47
- PIPE_MODEL_REVIEW: ${{ vars.PIPE_MODEL_REVIEW || 'claude-sonnet-5-0' }}
48
- PIPE_MODEL_CODE: ${{ vars.PIPE_MODEL_CODE || 'claude-sonnet-5-0' }}
47
+ PIPE_MODEL_REVIEW: ${{ vars.PIPE_MODEL_REVIEW || 'claude-sonnet-5' }}
48
+ PIPE_MODEL_CODE: ${{ vars.PIPE_MODEL_CODE || 'claude-sonnet-5' }}
49
49
  PIPE_AGENT_ENV_ALLOWLIST: ${{ vars.PIPE_AGENT_ENV_ALLOWLIST || '' }}
50
50
  # Runtime mapping onto the PIPE_ names the scripts read.
51
51
  PIPE_REPO: ${{ github.repository }}
@@ -49,7 +49,7 @@ env:
49
49
  PIPE_COMMIT_COVERAGE: ${{ vars.PIPE_COMMIT_COVERAGE || 'Add coverage tests' }}
50
50
  PIPE_GIT_NAME: ${{ vars.PIPE_GIT_NAME || 'Pipeline Bot' }}
51
51
  PIPE_GIT_EMAIL: ${{ vars.PIPE_GIT_EMAIL || 'bot@pipeline.ci' }}
52
- PIPE_MODEL_CODE: ${{ vars.PIPE_MODEL_CODE || 'claude-sonnet-5-0' }}
52
+ PIPE_MODEL_CODE: ${{ vars.PIPE_MODEL_CODE || 'claude-sonnet-5' }}
53
53
  PIPE_AGENT_ENV_ALLOWLIST: ${{ vars.PIPE_AGENT_ENV_ALLOWLIST || '' }}
54
54
  PIPE_TEST_ARTIFACT: ${{ vars.PIPE_TEST_ARTIFACT || 'test-reports' }}
55
55
  PIPE_REPO: ${{ github.repository }}
@@ -0,0 +1,233 @@
1
+ #!/bin/bash
2
+ # Runner for the opt-in weekly agent-architect job. It collects bounded,
3
+ # forge-neutral evidence; the /agent-architect skill performs the analysis.
4
+ set -e
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ # shellcheck source=lib/pipeline-common.sh
7
+ source "$SCRIPT_DIR/lib/pipeline-common.sh"
8
+ # shellcheck source=lib/issue-loop.sh
9
+ source "$SCRIPT_DIR/lib/issue-loop.sh"
10
+ pipe_defaults
11
+
12
+ ARCHITECT_ENV="$PIPE_CONTEXT_DIR/architect.env"
13
+ CONTEXT_FILE="$PIPE_CONTEXT_DIR/architect-context.md"
14
+ REPORT_FILE="$PIPE_CONTEXT_DIR/architect-report.md"
15
+ PROPOSALS_FILE="$PIPE_CONTEXT_DIR/architect-proposals.json"
16
+ if [ "${DRY_RUN+x}" = x ]; then
17
+ DRY_RUN="$DRY_RUN"
18
+ else
19
+ DRY_RUN="${PIPE_ARCHITECT_DRY_RUN:-true}"
20
+ fi
21
+ WINDOW_DAYS="${PIPE_ARCHITECT_WINDOW_DAYS:-7}"
22
+
23
+ write_marker() {
24
+ printf 'ARCHITECT_STATUS=%s\nARCHITECT_PROPOSALS=%s\nARCHITECT_ISSUES_FILED=%s\n' \
25
+ "$1" "$2" "$3" > "$ARCHITECT_ENV"
26
+ }
27
+
28
+ if [ "${PIPE_AGENT_ARCHITECT:-0}" != "1" ]; then
29
+ write_marker disabled 0 0
30
+ pipe_log "Agent architect is disabled"
31
+ exit 0
32
+ fi
33
+
34
+ if ! [[ "$WINDOW_DAYS" =~ ^[1-9][0-9]*$ ]]; then
35
+ write_marker invalid_window 0 0
36
+ pipe_log "ERROR: PIPE_ARCHITECT_WINDOW_DAYS must be a positive integer"
37
+ exit 1
38
+ fi
39
+
40
+ if date --version >/dev/null 2>&1; then
41
+ SINCE=$(date -u -d "$WINDOW_DAYS days ago" '+%Y-%m-%dT%H:%M:%SZ')
42
+ else
43
+ SINCE=$(date -u -v-"$WINDOW_DAYS"d '+%Y-%m-%dT%H:%M:%SZ')
44
+ fi
45
+ WEEK_TAG=$(date -u +%G-W%V)
46
+
47
+ # The updated-since helper has the window semantics the source job needs. It
48
+ # returns both states, so retain only merged MRs/PRs targeting this project’s
49
+ # configured integration branch and cap the prompt input before any per-item IO.
50
+ RECENT_MRS=$(platform_merge_requests_updated_since "$SINCE") || {
51
+ write_marker fetch_failed 0 0
52
+ pipe_log "ERROR: failed to fetch recently updated MRs/PRs"
53
+ exit 1
54
+ }
55
+ MRS=$(jq -c --arg target "$PIPE_TARGET_BRANCH" \
56
+ '[.[] | select(.state == "merged" and .target_branch == $target)] | .[0:30]' <<< "$RECENT_MRS") || {
57
+ write_marker fetch_failed 0 0
58
+ pipe_log "ERROR: unexpected merged MR/PR response"
59
+ exit 1
60
+ }
61
+ MR_COUNT=$(jq -r 'length' <<< "$MRS")
62
+ if ! [[ "$MR_COUNT" =~ ^[0-9]+$ ]]; then
63
+ write_marker fetch_failed 0 0
64
+ pipe_log "ERROR: could not count merged MRs/PRs"
65
+ exit 1
66
+ fi
67
+ if [ "$MR_COUNT" = 0 ]; then
68
+ write_marker no_evidence 0 0
69
+ pipe_log "No merged MRs/PRs in the last $WINDOW_DAYS days"
70
+ exit 0
71
+ fi
72
+
73
+ # Stuck issues are current actionable context. Stuck MRs/PRs are selected from
74
+ # the same bounded recent window, so neither evidence source can expand prompt
75
+ # size without a hard limit.
76
+ STUCK_ISSUES=$(issues_by_labels "$PIPE_LABEL_STUCK" "" opened 20) || {
77
+ pipe_log "WARNING: failed to fetch open $PIPE_LABEL_STUCK issues; continuing without them"
78
+ STUCK_ISSUES='[]'
79
+ }
80
+ STUCK_MRS=$(jq -c --arg label "$PIPE_LABEL_STUCK" \
81
+ '[.[] | select((.labels // []) | index($label))] | .[0:20]' <<< "$RECENT_MRS") || STUCK_MRS='[]'
82
+ OPEN_IMPROVEMENTS=$(issues_by_label_created "$PIPE_LABEL_IMPROVEMENT" opened 10) || {
83
+ pipe_log "WARNING: failed to fetch open $PIPE_LABEL_IMPROVEMENT issues; continuing without dedup context"
84
+ OPEN_IMPROVEMENTS='[]'
85
+ }
86
+
87
+ # Resolve the configured bot identity once so automatic comments do not become
88
+ # evidence. A failed lookup merely leaves the filter empty rather than failing a
89
+ # read-only scheduled analysis.
90
+ platform_resolve_bot_user || true
91
+
92
+ MR_EVIDENCE=""
93
+ while IFS= read -r IID; do
94
+ TITLE=$(jq -r --argjson iid "$IID" '.[] | select(.iid == $iid) | .title' <<< "$MRS")
95
+ AUTHOR=$(jq -r --argjson iid "$IID" '.[] | select(.iid == $iid) | .author' <<< "$MRS")
96
+ LABELS=$(jq -r --argjson iid "$IID" '.[] | select(.iid == $iid) | (.labels // []) | join(",")' <<< "$MRS")
97
+ FILES=$(platform_mr_changes "$IID" 15 2>/dev/null | sed 's/^/ /' || true)
98
+ NOTES=$(platform_mr_notes "$IID" 20 asc 2>/dev/null \
99
+ | jq -r --arg bot "${PIPE_BOT_USER:-}" \
100
+ '[.[] | select($bot == "" or .author != $bot)
101
+ | " [" + .author + "]: " + ((.body | gsub("[\\r\\n]+"; " "))[0:200])]
102
+ | .[0:20] | join("\n")' 2>/dev/null || true)
103
+ MR_EVIDENCE="${MR_EVIDENCE}
104
+ ### MR/PR !${IID}: ${TITLE}
105
+ Author: ${AUTHOR} | Labels: ${LABELS}
106
+ Changed files:
107
+ ${FILES:- (none)}
108
+ Human review comments:
109
+ ${NOTES:- (none)}
110
+ "
111
+ done < <(jq -r '.[].iid' <<< "$MRS")
112
+
113
+ STUCK_EVIDENCE=""
114
+ while IFS= read -r IID; do
115
+ TITLE=$(jq -r --argjson iid "$IID" '.[] | select(.iid == $iid) | .title' <<< "$STUCK_ISSUES")
116
+ DESCRIPTION=$(jq -r --argjson iid "$IID" '.[] | select(.iid == $iid) | .description // "(none)"' <<< "$STUCK_ISSUES" | head -c 400)
117
+ STUCK_EVIDENCE="${STUCK_EVIDENCE}
118
+ ### Stuck issue #${IID}: ${TITLE}
119
+ ${DESCRIPTION}
120
+ "
121
+ done < <(jq -r '.[].iid' <<< "$STUCK_ISSUES")
122
+ while IFS= read -r IID; do
123
+ TITLE=$(jq -r --argjson iid "$IID" '.[] | select(.iid == $iid) | .title' <<< "$STUCK_MRS")
124
+ NOTES=$(platform_mr_notes "$IID" 15 asc 2>/dev/null \
125
+ | jq -r --arg bot "${PIPE_BOT_USER:-}" \
126
+ '[.[] | select($bot == "" or .author != $bot)
127
+ | " [" + .author + "]: " + ((.body | gsub("[\\r\\n]+"; " "))[0:200])]
128
+ | .[0:15] | join("\n")' 2>/dev/null || true)
129
+ STUCK_EVIDENCE="${STUCK_EVIDENCE}
130
+ ### Stuck MR/PR !${IID}: ${TITLE}
131
+ Human comments:
132
+ ${NOTES:- (none)}
133
+ "
134
+ done < <(jq -r '.[].iid' <<< "$STUCK_MRS")
135
+
136
+ OPEN_CONTEXT=$(jq -r '.[] | "Issue #\(.iid) (\(.created_at[0:10])): \(.title)\n\(.description[0:500])\n---"' <<< "$OPEN_IMPROVEMENTS" 2>/dev/null | head -c 6000 || true)
137
+ rm -f "$ARCHITECT_ENV" "$REPORT_FILE" "$PROPOSALS_FILE"
138
+ {
139
+ echo "# Agent architect context"
140
+ echo
141
+ echo "Time window: last $WINDOW_DAYS days (since $SINCE)"
142
+ echo "Week: $WEEK_TAG"
143
+ echo "Ready label: $PIPE_LABEL_READY"
144
+ echo "Improvement label: $PIPE_LABEL_IMPROVEMENT"
145
+ echo
146
+ echo "## Merged MRs/PRs"
147
+ printf '%s\n' "$MR_EVIDENCE"
148
+ echo "## Stuck items"
149
+ printf '%s\n' "${STUCK_EVIDENCE:-(none)}"
150
+ if [ -n "$OPEN_CONTEXT" ]; then
151
+ echo
152
+ echo "## Already-open improvement issues — do not re-propose these"
153
+ printf '%s\n' "$OPEN_CONTEXT"
154
+ fi
155
+ } > "$CONTEXT_FILE"
156
+
157
+ REPORT=$(pipe_run_agent agent-architect "/agent-architect" "Agent,Read,Glob,Grep" "$PIPE_MODEL_REVIEW" < /dev/null)
158
+ printf '%s\n' "$REPORT" > "$REPORT_FILE"
159
+ if [ ! -s "$REPORT_FILE" ]; then
160
+ write_marker no_output 0 0
161
+ pipe_failure_log_notice_files "the weekly agent-architect analysis" \
162
+ "review the recent changes and stuck items by hand" \
163
+ "$PIPE_AGENT_STDERR" "$REPORT_FILE" | while IFS= read -r line; do pipe_log "$line"; done
164
+ exit 0
165
+ fi
166
+
167
+ # Retain at most five complete delimiter blocks. The JSON file keeps body text
168
+ # out of shell words and is also the sole source for issue creation below.
169
+ if ! command -v python3 >/dev/null 2>&1; then
170
+ write_marker parser_failed 0 0
171
+ pipe_log "WARNING: python3 is unavailable; cannot parse architect proposals"
172
+ exit 0
173
+ fi
174
+ if ! python3 - "$REPORT_FILE" "$PROPOSALS_FILE" <<'PY'
175
+ import json
176
+ import re
177
+ import sys
178
+
179
+ report_path, proposals_path = sys.argv[1:]
180
+ text = open(report_path, encoding="utf-8").read()
181
+ proposals = []
182
+ for block in re.findall(r"<!-- ISSUE-START -->(.*?)<!-- ISSUE-END -->", text, re.S)[:5]:
183
+ title = next((line[7:].strip() for line in block.splitlines() if line.startswith("Title: ")), "")
184
+ if not title or len(title) > 72:
185
+ continue
186
+ proposals.append({"title": title, "body": block.strip()[:8000]})
187
+ with open(proposals_path, "w", encoding="utf-8") as output:
188
+ json.dump(proposals, output)
189
+ PY
190
+ then
191
+ rm -f "$PROPOSALS_FILE"
192
+ write_marker parser_failed 0 0
193
+ pipe_log "WARNING: could not parse architect proposals"
194
+ exit 0
195
+ fi
196
+ if ! jq -e 'type == "array"' "$PROPOSALS_FILE" >/dev/null 2>&1; then
197
+ rm -f "$PROPOSALS_FILE"
198
+ write_marker parser_failed 0 0
199
+ pipe_log "WARNING: architect proposal parser produced invalid output"
200
+ exit 0
201
+ fi
202
+ PROPOSAL_COUNT=$(jq -r 'length' "$PROPOSALS_FILE")
203
+ if [ "$PROPOSAL_COUNT" = 0 ]; then
204
+ write_marker no_proposals 0 0
205
+ pipe_log "No concrete architect proposals"
206
+ exit 0
207
+ fi
208
+
209
+ case "$DRY_RUN" in
210
+ false|0) ;;
211
+ *)
212
+ write_marker ok "$PROPOSAL_COUNT" 0
213
+ pipe_log "DRY RUN: $PROPOSAL_COUNT proposal(s), no issues filed"
214
+ cat "$REPORT_FILE"
215
+ exit 0
216
+ ;;
217
+ esac
218
+
219
+ issue_ensure_label "$PIPE_LABEL_IMPROVEMENT" "#6F42C1" "Agent improvement proposals"
220
+ ISSUES_FILE="$PIPE_CONTEXT_DIR/architect-issue.md"
221
+ ISSUES_FILED=0
222
+ while IFS= read -r proposal; do
223
+ TITLE=$(jq -r '.title' <<< "$proposal")
224
+ jq -r '.body' <<< "$proposal" > "$ISSUES_FILE"
225
+ ISSUE_IID=$(issue_create_file "$TITLE" "$ISSUES_FILE" "$PIPE_LABEL_READY,$PIPE_LABEL_IMPROVEMENT")
226
+ if [ -n "$ISSUE_IID" ]; then
227
+ ISSUES_FILED=$((ISSUES_FILED + 1))
228
+ pipe_log "Filed architect proposal as #$ISSUE_IID"
229
+ else
230
+ pipe_log "WARNING: could not file architect proposal: $TITLE"
231
+ fi
232
+ done < <(jq -c '.[]' "$PROPOSALS_FILE")
233
+ write_marker ok "$PROPOSAL_COUNT" "$ISSUES_FILED"
@@ -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