@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
@@ -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"
@@ -0,0 +1,225 @@
1
+ #!/bin/bash
2
+ # Runner for the opt-in `docs-sync` job. Finds documentation gaps AND acts on
3
+ # them in a single pass: on a wip MR/PR the skill applies the documentation
4
+ # edits and this script commits and pushes them, on any other MR/PR the skill
5
+ # reports the gaps and this script posts them as an advisory comment.
6
+ #
7
+ # The runner is mechanical: it classifies the MR/PR, computes the changed files
8
+ # and the full diff, runs the /docs-sync skill, posts the verdict, and commits.
9
+ # Every judgement about what needs documenting lives in the skill and in the
10
+ # docs-sync agent it spawns.
11
+ #
12
+ # The diff is always base..head, never an incremental slice: a gap introduced by
13
+ # an earlier push must still be found on a later one.
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
+ pipe_defaults
19
+
20
+ DOCS_MARKER="**Automated Documentation Sync**"
21
+ CONTEXT_FILE="$PIPE_CONTEXT_DIR/docs-sync-context.md"
22
+ FILES_FILE="$PIPE_CONTEXT_DIR/docs-sync-files.txt"
23
+ DIFF_FILE="$PIPE_CONTEXT_DIR/docs-sync-diff.patch"
24
+ DOCS_ENV="$PIPE_CONTEXT_DIR/docs-sync.env"
25
+ COMMENT_FILE="$PIPE_CONTEXT_DIR/docs-sync-comment.md"
26
+
27
+ # Documentation roots the agent may edit and this script may commit. Comma
28
+ # separated in PIPE_DOCS_ROOTS; a root is a directory or a single file.
29
+ # The split is a bash field split, not `tr ',' '\n'` piped into `read`: that
30
+ # pipeline emits no trailing newline, so `read` silently drops the LAST root.
31
+ # Surrounding whitespace is trimmed per element so "docs, CLAUDE.md" works.
32
+ DOCS_ROOTS=()
33
+ IFS=',' read -ra DOCS_ROOTS_RAW <<< "$PIPE_DOCS_ROOTS"
34
+ for root in "${DOCS_ROOTS_RAW[@]}"; do
35
+ root="${root#"${root%%[![:space:]]*}"}"
36
+ root="${root%"${root##*[![:space:]]}"}"
37
+ if [ -n "$root" ]; then
38
+ DOCS_ROOTS+=("$root")
39
+ fi
40
+ done
41
+ if [ "${#DOCS_ROOTS[@]}" -eq 0 ]; then
42
+ pipe_log "PIPE_DOCS_ROOTS is empty, nothing to sync"
43
+ exit 0
44
+ fi
45
+
46
+ # True when the path is one of the documentation roots or sits under one.
47
+ docs_root_path() {
48
+ local root
49
+ for root in "${DOCS_ROOTS[@]}"; do
50
+ [ "$1" = "$root" ] && return 0
51
+ case "$1" in "$root"/*) return 0 ;; esac
52
+ done
53
+ return 1
54
+ }
55
+
56
+ # 1. Fetch MR/PR meta (token work). The source branch comes from the same read:
57
+ # it is what an applied documentation commit is pushed to, and the CI runtime
58
+ # does not always provide it.
59
+ META=$(platform_mr_meta)
60
+ MR_TITLE=$(echo "$META" | jq -r '.title // ""')
61
+ MR_LABELS=$(echo "$META" | jq -r '.labels // ""')
62
+ META_SOURCE_BRANCH=$(echo "$META" | jq -r '.source_branch // ""')
63
+ if [ -z "${PIPE_SOURCE_BRANCH:-}" ] && [ -n "$META_SOURCE_BRANCH" ]; then
64
+ PIPE_SOURCE_BRANCH="$META_SOURCE_BRANCH"
65
+ export PIPE_SOURCE_BRANCH
66
+ fi
67
+
68
+ # 2. Classify. Only an autonomous (wip) MR/PR gets its documentation edited;
69
+ # a human-authored one gets a report.
70
+ DOCS_MODE=report
71
+ if echo "$MR_LABELS" | grep -q "$PIPE_LABEL_WIP"; then
72
+ DOCS_MODE=apply
73
+ fi
74
+
75
+ # 3. Full-MR diff base. platform_compute_diff_base would hand back the previous
76
+ # push head on a re-run, which is the wrong scope for this job, so the base
77
+ # is always the merge base with the target branch.
78
+ git fetch origin "$PIPE_TARGET_BRANCH" 2>/dev/null || true
79
+ DIFF_BASE=$(git merge-base "origin/$PIPE_TARGET_BRANCH" "$PIPE_HEAD_SHA" 2>/dev/null || echo "origin/$PIPE_TARGET_BRANCH")
80
+ pipe_log "mode=$DOCS_MODE diff base=$DIFF_BASE head=$PIPE_HEAD_SHA"
81
+
82
+ git diff --name-only "$DIFF_BASE" "$PIPE_HEAD_SHA" > "$FILES_FILE" 2>/dev/null || : > "$FILES_FILE"
83
+ if [ ! -s "$FILES_FILE" ]; then
84
+ pipe_log "No changed files in $DIFF_BASE..$PIPE_HEAD_SHA, skipping docs-sync"
85
+ exit 0
86
+ fi
87
+
88
+ # 4. Skip when the MR/PR only touches documentation: there is no new behaviour
89
+ # to document, and the author is already editing the docs.
90
+ CODE_CHANGED=false
91
+ while IFS= read -r file; do
92
+ [ -n "$file" ] || continue
93
+ if ! docs_root_path "$file"; then
94
+ CODE_CHANGED=true
95
+ break
96
+ fi
97
+ done < "$FILES_FILE"
98
+ if [ "$CODE_CHANGED" = "false" ]; then
99
+ pipe_log "Only documentation paths changed, skipping docs-sync"
100
+ exit 0
101
+ fi
102
+
103
+ git diff "$DIFF_BASE" "$PIPE_HEAD_SHA" > "$DIFF_FILE" 2>/dev/null || : > "$DIFF_FILE"
104
+
105
+ # 5. In apply mode, check out the real source branch so the edits and the commit
106
+ # land on its tip. Loop guard: at most one consecutive automated docs commit
107
+ # per branch streak, otherwise apply -> pipeline -> apply would never settle.
108
+ # The guard degrades this run to report mode instead of skipping it, so the
109
+ # MR/PR still gets a verdict.
110
+ if [ "$DOCS_MODE" = "apply" ]; then
111
+ pipe_git_identity
112
+ pipe_checkout_source
113
+ if [ "$(pipe_count_fix_commits "$PIPE_COMMIT_DOCSSYNC")" -gt 0 ]; then
114
+ pipe_log "Tip is already an automated docs commit, degrading to report mode"
115
+ DOCS_MODE=report
116
+ fi
117
+ fi
118
+
119
+ # 6. Write the context file the skill reads. Unbounded text (the diff, the file
120
+ # list) stays in its own file, never in argv or the environment.
121
+ {
122
+ echo "# Documentation sync context"
123
+ echo
124
+ echo "Mode: $DOCS_MODE"
125
+ echo "Title: $MR_TITLE"
126
+ echo "Documentation roots: $PIPE_DOCS_ROOTS"
127
+ echo "Changed files: $FILES_FILE"
128
+ echo "Full diff: $DIFF_FILE"
129
+ echo "Diff range: $DIFF_BASE..$PIPE_HEAD_SHA"
130
+ } > "$CONTEXT_FILE"
131
+
132
+ # 7. Run the skill. It reads the context, spawns the docs-sync agent in the
133
+ # requested mode, writes $PIPE_RESULT_DOCSSYNC, the comment body, and the
134
+ # docs-sync.env marker this script reads back.
135
+ pipe_run_agent docs-sync "/docs-sync" "Agent,Read,Write,Edit,Glob,Grep,Bash" "$PIPE_MODEL_REVIEW" < /dev/null
136
+
137
+ # 8. Read the marker. DOCS_COMMENT_PATH carries the body because a comment is
138
+ # unbounded text. A result file written by a model can be malformed JSON, so
139
+ # the fallback extraction is defensive: an unparseable or comment-less result
140
+ # is treated exactly like a missing one.
141
+ DOCS_HAS_GAPS=$(pipe_get_env "$DOCS_ENV" DOCS_HAS_GAPS); DOCS_HAS_GAPS=${DOCS_HAS_GAPS:-false}
142
+ DOCS_CHANGED=$(pipe_get_env "$DOCS_ENV" DOCS_CHANGED); DOCS_CHANGED=${DOCS_CHANGED:-false}
143
+ DOCS_BODY=$(pipe_get_env "$DOCS_ENV" DOCS_COMMENT_PATH); DOCS_BODY=${DOCS_BODY:-$PIPE_CONTEXT_DIR/docs-sync-body.md}
144
+ if [ ! -s "$DOCS_BODY" ] && [ -f "$PIPE_RESULT_DOCSSYNC" ]; then
145
+ RESULT_BODY="$PIPE_CONTEXT_DIR/docs-sync-result-body.md"
146
+ jq -re '.comment // empty' "$PIPE_RESULT_DOCSSYNC" > "$RESULT_BODY" 2>/dev/null || true
147
+ if [ -s "$RESULT_BODY" ]; then
148
+ DOCS_BODY="$RESULT_BODY"
149
+ fi
150
+ fi
151
+
152
+ DOCS_FAILED=false
153
+ if [ -s "$DOCS_BODY" ]; then
154
+ {
155
+ printf '%s\n\n' "$DOCS_MARKER"
156
+ cat "$DOCS_BODY"
157
+ } > "$COMMENT_FILE"
158
+ else
159
+ DOCS_FAILED=true
160
+ {
161
+ printf '%s\n\n⚠️ ' "$DOCS_MARKER"
162
+ pipe_failure_notice_files "documentation sync" \
163
+ "check by hand whether these changes need updates under $PIPE_DOCS_ROOTS" \
164
+ "$PIPE_AGENT_STDERR" "$PIPE_RESULT_DOCSSYNC"
165
+ } > "$COMMENT_FILE"
166
+ fi
167
+
168
+ # 9. Always post the verdict. Metrics reflect the runner's actual commit
169
+ # decision, not the skill's informational DOCS_CHANGED marker.
170
+ DOCS_COMMITTED=false
171
+ emit_docs_metric() {
172
+ pipe_metric_event docs-sync docs_sync_gaps \
173
+ "$(jq -n --arg m "$DOCS_MODE" \
174
+ --argjson g "$([ "$DOCS_HAS_GAPS" = "true" ] && echo true || echo false)" \
175
+ --argjson c "$([ "$DOCS_COMMITTED" = "true" ] && echo true || echo false)" \
176
+ --argjson f "$([ "$DOCS_FAILED" = "true" ] && echo true || echo false)" \
177
+ '{mode: $m, has_gaps: $g, changed: $c, failed: $f}')"
178
+ }
179
+
180
+ platform_post_comment_file "$COMMENT_FILE"
181
+
182
+ # 10. Commit and push what the agent applied. A failed agent applied nothing, so
183
+ # the notice posted above is the whole output and the exit is 0: an
184
+ # automation outage is not a documentation gap.
185
+ if [ "$DOCS_FAILED" = "true" ]; then
186
+ emit_docs_metric
187
+ pipe_log "Agent did not complete, manual-review notice posted, nothing to commit"
188
+ exit 0
189
+ fi
190
+ if [ "$DOCS_MODE" != "apply" ]; then
191
+ emit_docs_metric
192
+ exit 0
193
+ fi
194
+ if [ "$DOCS_CHANGED" != "true" ]; then
195
+ pipe_log "Skill reported no documentation edits, checking the staged diff anyway"
196
+ fi
197
+
198
+ # Stage each root on its own: one missing root must not stop the others from
199
+ # being staged, which a single `git add` of the whole set would do.
200
+ for root in "${DOCS_ROOTS[@]}"; do
201
+ [ -e "$root" ] || continue
202
+ git add -- "$root" || true
203
+ done
204
+ if git diff --cached --quiet; then
205
+ emit_docs_metric
206
+ pipe_log "No documentation changes staged, nothing to commit"
207
+ exit 0
208
+ fi
209
+
210
+ git commit -m "$PIPE_COMMIT_DOCSSYNC"
211
+ DOCS_COMMITTED=true
212
+ emit_docs_metric
213
+
214
+ # The agent may only touch the documentation roots, but an edit outside them
215
+ # would leave the working tree dirty and abort the rebase below before it
216
+ # starts, after which `git rebase --abort` fails too. Discard out-of-scope
217
+ # edits so the tree is clean, enforcing the documented boundary.
218
+ if ! git diff --quiet; then
219
+ pipe_log "Discarding out-of-scope working-tree changes left outside $PIPE_DOCS_ROOTS:"
220
+ git diff --name-only
221
+ git checkout -- .
222
+ fi
223
+
224
+ pipe_rebase_and_push
225
+ pipe_log "Pushed the docs-sync commit, a new pipeline will run"