@cxi-lmai/ci-agent-platform 3.0.0 → 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 (69) hide show
  1. package/README.md +27 -5
  2. package/package.json +2 -2
  3. package/payload/INSTALL.md +14 -5
  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 +3 -3
  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 +4 -4
  15. package/payload/agents/test-writer.md +3 -3
  16. package/payload/agents-omp/agent-architect.md +101 -0
  17. package/payload/agents-omp/code-reviewer.md +86 -0
  18. package/payload/agents-omp/codebase-auditor.md +73 -0
  19. package/payload/agents-omp/coder.md +57 -0
  20. package/payload/agents-omp/decomposer.md +70 -0
  21. package/payload/agents-omp/docs-sync.md +114 -0
  22. package/payload/agents-omp/e2e-test-writer.md +47 -0
  23. package/payload/agents-omp/migration-reviewer.md +99 -0
  24. package/payload/agents-omp/orchestrator.md +50 -0
  25. package/payload/agents-omp/performance-reviewer.md +81 -0
  26. package/payload/agents-omp/postmortem.md +82 -0
  27. package/payload/agents-omp/release-mr.md +274 -0
  28. package/payload/agents-omp/security-reviewer.md +121 -0
  29. package/payload/agents-omp/test-fix.md +33 -0
  30. package/payload/agents-omp/test-writer.md +39 -0
  31. package/payload/ci-templates/claude-pipeline.gitlab-ci.yml +220 -7
  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 +35 -35
  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 +233 -31
  45. package/payload/ci-templates/scripts/lib/platform.sh +270 -13
  46. package/payload/ci-templates/scripts/lib/usage-capture-omp.sh +123 -0
  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 +56 -38
  50. package/payload/ci-templates/scripts/postmortem.sh +11 -1
  51. package/payload/ci-templates/scripts/review-fix.sh +23 -12
  52. package/payload/ci-templates/scripts/review.sh +12 -8
  53. package/payload/ci-templates/scripts/test-fix.sh +8 -1
  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 +3 -3
  60. package/payload/skills/fix-tests/SKILL.md +4 -4
  61. package/payload/skills/implement-issue/SKILL.md +1 -1
  62. package/payload/skills/init-pipeline-config/SKILL.md +4 -4
  63. package/payload/skills/postmortem-mr/SKILL.md +1 -1
  64. package/payload/skills/review-mr/SKILL.md +1 -1
  65. package/payload/skills/triage-issue/SKILL.md +2 -2
  66. package/payload/templates/memory-index.template.md +34 -0
  67. package/payload/templates/pipeline-config.template.md +11 -1
  68. package/payload/templates/review_suppressions.template.md +55 -0
  69. package/payload/templates/spec-issue.template.md +39 -7
@@ -0,0 +1,377 @@
1
+ #!/bin/bash
2
+ # Aggregates one window of pipeline metrics into a single JSON snapshot.
3
+ #
4
+ # Two record kinds share the metrics directory. An invocation record carries an
5
+ # `event` ending in `_invocation` plus the token and cost fields of one agent
6
+ # call, and every other record is a custom pipeline event. Older runners
7
+ # appended custom events to events.jsonl, which is still read when present.
8
+ #
9
+ # Inputs:
10
+ # - artifacts of pipelines finished inside the window, fetched through the
11
+ # platform abstraction, skipped entirely when OFFLINE=true
12
+ # - records written by this run's own jobs, under PIPE_METRICS_DIR
13
+ #
14
+ # Environment:
15
+ # PIPE_METRICS_WINDOW_DAYS days of history to aggregate
16
+ # PIPE_METRICS_OUTPUT_DIR directory the snapshot is written to
17
+ # PIPE_METRICS_COMMIT_REPO "true" also commits the snapshot to the target branch
18
+ # PIPE_METRICS_REVIEW_AGENTS comma-separated agents counted as code review
19
+ # PIPE_METRICS_DEFECT_* commit subjects that mark a post-merge defect
20
+ # PIPE_METRICS_SNAPSHOT_DATE overrides the snapshot date, default today UTC
21
+ # OFFLINE "true" skips every platform call
22
+ # PIPE_METRICS_RAW_DIR OFFLINE only, directory of pre-staged records
23
+ # PIPE_METRICS_MRS_FILE OFFLINE only, JSON array of merge requests
24
+ #
25
+ # Writes PIPE_METRICS_OUTPUT_DIR/<date>.json. The snapshot is a job artifact;
26
+ # committing it back to the repository is opt-in because a project may refuse
27
+ # generated data on its integration branch.
28
+ set -euo pipefail
29
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
30
+ # shellcheck source=lib/pipeline-common.sh
31
+ source "$SCRIPT_DIR/lib/pipeline-common.sh"
32
+ pipe_defaults
33
+
34
+ SNAPSHOT_DATE="${PIPE_METRICS_SNAPSHOT_DATE:-$(date -u +%Y-%m-%d)}"
35
+ mkdir -p "$PIPE_METRICS_OUTPUT_DIR"
36
+
37
+ TMP=$(mktemp -d)
38
+ trap 'rm -rf "$TMP"' EXIT
39
+
40
+ RAW_DIR="$TMP/raw"
41
+ mkdir -p "$RAW_DIR"
42
+ MRS_FILE="$TMP/mrs.json"
43
+ echo '[]' > "$MRS_FILE"
44
+
45
+ # Escape a configured marker so it can be used inside a regular expression.
46
+ re_escape() { printf '%s' "$1" | sed 's/[][\\.^$*+?(){}|]/\\&/g'; }
47
+
48
+ # Artifact archives are read with whichever extractor the runner image has.
49
+ # Only JSON members are unpacked, at any depth: a pipeline's other artifacts
50
+ # (build output, test reports, packaged binaries) share these archives, and
51
+ # unpacking them wholesale would both cost time and feed foreign documents into
52
+ # the aggregation. Depth-independence is required because the two forges lay
53
+ # their archives out differently: job archives keep repository-relative paths,
54
+ # while uploaded-artifact archives are rooted at the uploaded directory itself.
55
+ # collect_records applies the record-shape filter that decides what counts.
56
+ metrics_extract() {
57
+ local zip="$1" dest="$2"
58
+ if command -v unzip >/dev/null 2>&1; then
59
+ unzip -o -q "$zip" '*.json' '*.jsonl' -d "$dest" 2>/dev/null || true
60
+ elif command -v python3 >/dev/null 2>&1; then
61
+ python3 -c 'import sys, zipfile
62
+ try:
63
+ with zipfile.ZipFile(sys.argv[1]) as archive:
64
+ wanted = [m for m in archive.namelist()
65
+ if not m.endswith("/") and (m.endswith(".json") or m.endswith(".jsonl"))]
66
+ archive.extractall(sys.argv[2], wanted)
67
+ except Exception:
68
+ pass' "$zip" "$dest" || true
69
+ else
70
+ pipe_log "WARNING: no unzip and no python3, cannot read artifact $zip"
71
+ fi
72
+ }
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Phase 1: collect the window's records
76
+ # ---------------------------------------------------------------------------
77
+
78
+ if [ "${OFFLINE:-false}" = "true" ]; then
79
+ if [ -n "${PIPE_METRICS_RAW_DIR:-}" ] && [ -d "${PIPE_METRICS_RAW_DIR}" ]; then
80
+ RAW_DIR="$PIPE_METRICS_RAW_DIR"
81
+ fi
82
+ if [ -n "${PIPE_METRICS_MRS_FILE:-}" ] && [ -s "${PIPE_METRICS_MRS_FILE}" ]; then
83
+ MRS_FILE="$PIPE_METRICS_MRS_FILE"
84
+ fi
85
+ pipe_log "Offline mode: reading staged records from $RAW_DIR"
86
+ else
87
+ SINCE=$(date -u -d "$PIPE_METRICS_WINDOW_DAYS days ago" '+%Y-%m-%dT%H:%M:%SZ')
88
+ pipe_log "Collecting pipelines finished since $SINCE"
89
+
90
+ PIPELINES=$(platform_completed_pipelines "$PIPE_METRICS_WINDOW_DAYS" || echo '[]')
91
+ pipe_log "Found $(printf '%s' "$PIPELINES" | jq -r 'length') pipeline(s)"
92
+
93
+ while read -r PIPELINE_ID; do
94
+ [ -n "$PIPELINE_ID" ] || continue
95
+ ARTIFACTS=$(platform_pipeline_artifacts "$PIPELINE_ID" || echo '[]')
96
+ while read -r JOB_ID; do
97
+ [ -n "$JOB_ID" ] || continue
98
+ ZIP="$TMP/$PIPELINE_ID-$JOB_ID.zip"
99
+ # One unreadable job must not abort the whole aggregation.
100
+ if platform_download_artifact "$JOB_ID" "$ZIP"; then
101
+ DEST="$RAW_DIR/$PIPELINE_ID-$JOB_ID"
102
+ mkdir -p "$DEST"
103
+ metrics_extract "$ZIP" "$DEST"
104
+ else
105
+ pipe_log "WARNING: artifacts for job $JOB_ID are unavailable, skipping"
106
+ fi
107
+ rm -f "$ZIP"
108
+ done < <(printf '%s' "$ARTIFACTS" | jq -r '.[] | select(.artifact_available) | .job_id')
109
+ done < <(printf '%s' "$PIPELINES" | jq -r '.[].id')
110
+
111
+ if ! platform_merge_requests_updated_since "$SINCE" > "$MRS_FILE"; then
112
+ pipe_log "WARNING: merge request list unavailable, continuing without it"
113
+ echo '[]' > "$MRS_FILE"
114
+ fi
115
+ platform_resolve_bot_user || true
116
+ fi
117
+
118
+ SEARCH_DIRS=("$RAW_DIR")
119
+ if [ -d "$PIPE_METRICS_DIR" ] && [ "$PIPE_METRICS_DIR" != "$RAW_DIR" ]; then
120
+ SEARCH_DIRS+=("$PIPE_METRICS_DIR")
121
+ fi
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # Phase 2 and 3: invocation records and custom events
125
+ # ---------------------------------------------------------------------------
126
+
127
+ # One record file that is truncated, or that is not a record at all, must cost
128
+ # only itself: parse each file on its own and skip the ones that do not read as
129
+ # a JSON object. Slurping every file in one pass would turn a single unreadable
130
+ # file into an empty, silently wrong snapshot.
131
+ #
132
+ # A job that consumes an upstream job's artifacts republishes the records it
133
+ # received, so the same record can arrive under several per-job directories.
134
+ # Record file names are unique per record, so identical duplicates are
135
+ # byte-identical and collapse on their content.
136
+ # A record is a JSON object carrying a string `agent` and a string `event`.
137
+ # Anything else found next to the records, for example a project manifest that
138
+ # a collected artifact happened to contain, is not a record and is ignored.
139
+ IS_RECORD='(type == "object") and ((.agent | type) == "string") and ((.event | type) == "string")'
140
+
141
+ collect_records() { # $1 = jq predicate selecting the record kind
142
+ local file
143
+ {
144
+ while IFS= read -r -d '' file; do
145
+ jq -c "select($IS_RECORD) | select($1)" "$file" 2>/dev/null || true
146
+ done < <(find "${SEARCH_DIRS[@]}" -type f -name '*.json' -print0 2>/dev/null)
147
+ } | jq -s -c 'unique_by(tojson)' 2>/dev/null || echo '[]'
148
+ }
149
+
150
+ INVOCATIONS=$(collect_records '(.event // "") | endswith("_invocation")')
151
+ EVENT_FILES=$(collect_records '((.event // "") | endswith("_invocation")) | not')
152
+ EVENT_LINES=$({
153
+ while IFS= read -r -d '' file; do
154
+ jq -c "select($IS_RECORD)" "$file" 2>/dev/null || true
155
+ done < <(find "${SEARCH_DIRS[@]}" -type f -name 'events.jsonl' -print0 2>/dev/null)
156
+ } | jq -s -c 'unique_by(tojson)' 2>/dev/null || echo '[]')
157
+ EVENTS=$(jq -nc --argjson a "$EVENT_FILES" --argjson b "$EVENT_LINES" '($a + $b) | unique_by(tojson)')
158
+
159
+ pipe_log "Collected $(printf '%s' "$INVOCATIONS" | jq -r 'length') invocation record(s)"
160
+ pipe_log "Collected $(printf '%s' "$EVENTS" | jq -r 'length') event(s)"
161
+
162
+ # ---------------------------------------------------------------------------
163
+ # Phase 4: merge request view with defect detection
164
+ # ---------------------------------------------------------------------------
165
+
166
+ RE_REVERT=$(re_escape "$PIPE_METRICS_DEFECT_REVERT")
167
+ RE_REGRESSION=$(re_escape "$PIPE_METRICS_DEFECT_REGRESSION")
168
+ RE_REINTRODUCE=$(re_escape "$PIPE_METRICS_DEFECT_REINTRODUCE")
169
+
170
+ MRS_VIEW=$(jq -c \
171
+ --arg revert "$RE_REVERT" \
172
+ --arg regression "$RE_REGRESSION" \
173
+ --arg reintroduce "$RE_REINTRODUCE" \
174
+ '[.[] | . as $mr | ($mr.description // "") as $body | {
175
+ iid: $mr.iid,
176
+ title: ($mr.title // ""),
177
+ state: ($mr.state // ""),
178
+ author: ($mr.author // ""),
179
+ labels: ($mr.labels // []),
180
+ created_at: ($mr.created_at // ""),
181
+ merged_at: $mr.merged_at,
182
+ description: $body,
183
+ defect_type: (
184
+ if ($body | test($revert; "i")) then "revert"
185
+ elif ($body | test($regression; "i")) then "regression-fix"
186
+ elif ($body | test($reintroduce; "i")) then "reintroduces"
187
+ else null end
188
+ ),
189
+ references: [
190
+ $body
191
+ | scan("(?:" + $revert + "|" + $regression + "|" + $reintroduce + ")[[:space:]]*[!#]?([0-9]+)"; "i")[]?
192
+ | select(. != null)
193
+ | tonumber
194
+ ]
195
+ }]' "$MRS_FILE")
196
+
197
+ pipe_log "Processed $(printf '%s' "$MRS_VIEW" | jq -r 'length') merge request(s)"
198
+
199
+ # ---------------------------------------------------------------------------
200
+ # Phase 5: cost and tokens per agent
201
+ # ---------------------------------------------------------------------------
202
+
203
+ BY_AGENT=$(printf '%s' "$INVOCATIONS" | jq -c '
204
+ group_by(.agent)
205
+ | map({
206
+ key: (.[0].agent // "unknown"),
207
+ value: {
208
+ count: length,
209
+ cost_usd: (map(.total_cost_usd // 0) | add // 0),
210
+ input_tokens: (map(.input_tokens // 0) | add // 0),
211
+ output_tokens: (map(.output_tokens // 0) | add // 0),
212
+ cache_read: (map(.cache_read_tokens // 0) | add // 0),
213
+ cache_creation: (map(.cache_creation_tokens // 0) | add // 0)
214
+ }
215
+ })
216
+ | from_entries')
217
+
218
+ # ---------------------------------------------------------------------------
219
+ # Phase 5b: split each agent's cost by the author of the change it acted on
220
+ #
221
+ # Review jobs run on bot and human merge requests alike, so the cost of the
222
+ # autonomous loop is only separable when each invocation is attributed to the
223
+ # author of the merge request it reviewed. Invocations without a merge request
224
+ # are "none", and a merge request outside this window is "unknown".
225
+ # ---------------------------------------------------------------------------
226
+
227
+ AUTHOR_TAG='
228
+ ($mrs[0] | map({key: (.iid | tostring), value: .author}) | from_entries) as $authors
229
+ | map(. + {author_type: (
230
+ if (.mr_iid == null) then "none"
231
+ else ($authors[(.mr_iid | tostring)] // null) as $author
232
+ | if $author == null then "unknown"
233
+ elif $author == $bot then "bot"
234
+ else "human" end
235
+ end)})'
236
+
237
+ BY_AGENT_AUTHOR=$(printf '%s' "$INVOCATIONS" | jq -c \
238
+ --argjson mrs "[$MRS_VIEW]" --arg bot "${PIPE_BOT_USER:-}" "
239
+ $AUTHOR_TAG
240
+ | group_by(.agent)
241
+ | map({
242
+ key: (.[0].agent // \"unknown\"),
243
+ value: (group_by(.author_type) | map({
244
+ key: .[0].author_type,
245
+ value: {
246
+ count: length,
247
+ cost_usd: (map(.total_cost_usd // 0) | add // 0),
248
+ input_tokens: (map(.input_tokens // 0) | add // 0),
249
+ output_tokens: (map(.output_tokens // 0) | add // 0),
250
+ cache_read: (map(.cache_read_tokens // 0) | add // 0),
251
+ cache_creation: (map(.cache_creation_tokens // 0) | add // 0)
252
+ }
253
+ }) | from_entries)
254
+ })
255
+ | from_entries")
256
+
257
+ REVIEW_COST_BY_AUTHOR=$(printf '%s' "$INVOCATIONS" | jq -c \
258
+ --argjson mrs "[$MRS_VIEW]" --arg bot "${PIPE_BOT_USER:-}" \
259
+ --arg review_agents "$PIPE_METRICS_REVIEW_AGENTS" "
260
+ (\$review_agents | split(\",\") | map(select(length > 0))) as \$reviewers
261
+ | $AUTHOR_TAG
262
+ | map(select((.agent // \"\") | IN(\$reviewers[])))
263
+ | {
264
+ bot: (map(select(.author_type == \"bot\") | .total_cost_usd // 0) | add // 0),
265
+ human: (map(select(.author_type == \"human\") | .total_cost_usd // 0) | add // 0),
266
+ unknown: (map(select(.author_type == \"unknown\") | .total_cost_usd // 0) | add // 0)
267
+ }")
268
+
269
+ # ---------------------------------------------------------------------------
270
+ # Phase 6: escalation failure categories
271
+ # ---------------------------------------------------------------------------
272
+
273
+ FAILURE_CATEGORIES=$(printf '%s' "$EVENTS" | jq -c '
274
+ [.[] | select(.event == "dev_stuck") | (.failure_category // .category // "other")]
275
+ | group_by(.)
276
+ | map({key: .[0], value: length})
277
+ | from_entries')
278
+
279
+ # ---------------------------------------------------------------------------
280
+ # Phase 6b: post-merge defects counted from commit subjects
281
+ #
282
+ # The three subjects are conventions, not guesses: they are configured in the
283
+ # project's pipeline config so the aggregator and the humans writing commits
284
+ # agree. Offline runs have no repository history to scan.
285
+ # ---------------------------------------------------------------------------
286
+
287
+ count_subject() { # $1 = escaped subject prefix, stdin = commit messages
288
+ local matches
289
+ matches=$(grep -cE "^$1" || true)
290
+ printf '%s' "${matches:-0}"
291
+ }
292
+
293
+ if [ "${OFFLINE:-false}" = "true" ]; then
294
+ POST_MERGE_DEFECTS='{"reverts":0,"fixes":0,"reintroduces":0}'
295
+ else
296
+ GIT_LOG_FILE="$TMP/commit-subjects.txt"
297
+ git log --since="$SINCE" --format=%B > "$GIT_LOG_FILE" 2>/dev/null || : > "$GIT_LOG_FILE"
298
+ DEFECT_REVERTS=$(count_subject "$RE_REVERT" < "$GIT_LOG_FILE")
299
+ DEFECT_FIXES=$(count_subject "$RE_REGRESSION" < "$GIT_LOG_FILE")
300
+ DEFECT_REINTRODUCES=$(count_subject "$RE_REINTRODUCE" < "$GIT_LOG_FILE")
301
+ pipe_log "Post-merge defects: reverts=$DEFECT_REVERTS fixes=$DEFECT_FIXES reintroduces=$DEFECT_REINTRODUCES"
302
+ POST_MERGE_DEFECTS=$(jq -nc \
303
+ --argjson reverts "$DEFECT_REVERTS" \
304
+ --argjson fixes "$DEFECT_FIXES" \
305
+ --argjson reintroduces "$DEFECT_REINTRODUCES" \
306
+ '{reverts: $reverts, fixes: $fixes, reintroduces: $reintroduces}')
307
+ fi
308
+
309
+ # ---------------------------------------------------------------------------
310
+ # Phase 7: write the snapshot
311
+ # ---------------------------------------------------------------------------
312
+
313
+ OUT_FILE="$PIPE_METRICS_OUTPUT_DIR/$SNAPSHOT_DATE.json"
314
+ jq -n \
315
+ --arg date "$SNAPSHOT_DATE" \
316
+ --arg bot "${PIPE_BOT_USER:-}" \
317
+ --argjson invocations "$INVOCATIONS" \
318
+ --argjson events "$EVENTS" \
319
+ --argjson mrs "$MRS_VIEW" \
320
+ --argjson by_agent "$BY_AGENT" \
321
+ --argjson by_agent_author "$BY_AGENT_AUTHOR" \
322
+ --argjson review_cost_by_author "$REVIEW_COST_BY_AUTHOR" \
323
+ --argjson failure_categories "$FAILURE_CATEGORIES" \
324
+ --argjson post_merge_defects "$POST_MERGE_DEFECTS" \
325
+ '{
326
+ date: $date,
327
+ invocations: $invocations,
328
+ events: $events,
329
+ mrs: $mrs,
330
+ totals: {
331
+ cost_usd: ($invocations | map(.total_cost_usd // 0) | add // 0),
332
+ input_tokens: ($invocations | map(.input_tokens // 0) | add // 0),
333
+ output_tokens: ($invocations | map(.output_tokens // 0) | add // 0),
334
+ cache_read_tokens: ($invocations | map(.cache_read_tokens // 0) | add // 0),
335
+ cache_creation_tokens: ($invocations | map(.cache_creation_tokens // 0) | add // 0),
336
+ operations_count: ($invocations | length),
337
+ merged_mrs: ($mrs | map(select(.state == "merged")) | length),
338
+ bot_mrs: ($mrs | map(select($bot != "" and .author == $bot)) | length),
339
+ defect_references: ($mrs | map(.references | length) | add // 0),
340
+ orchestrator_decisions: ($events | map(select(.event == "orchestrator_decision")) | length),
341
+ review_rounds: ($events | map(select(.event == "review_findings")) | length),
342
+ dev_stuck: ($events | map(select(.event == "dev_stuck")) | length)
343
+ },
344
+ by_agent: $by_agent,
345
+ by_agent_author: $by_agent_author,
346
+ review_cost_by_author: $review_cost_by_author,
347
+ failure_categories: $failure_categories,
348
+ post_merge_defects: $post_merge_defects
349
+ }' > "$OUT_FILE"
350
+
351
+ pipe_log "Wrote $OUT_FILE"
352
+
353
+ # ---------------------------------------------------------------------------
354
+ # Phase 8: commit the snapshot, only when the project opted in
355
+ # ---------------------------------------------------------------------------
356
+
357
+ if [ "${OFFLINE:-false}" != "true" ] && [ "$PIPE_METRICS_COMMIT_REPO" = "true" ]; then
358
+ # The snapshot is written on whatever ref the schedule ran, so it is untracked
359
+ # there. Move it aside before switching: if the target branch already carries a
360
+ # snapshot for this date, git would refuse to overwrite an untracked file and
361
+ # the job would fail with a git error instead of reaching the no-op branch.
362
+ pipe_git_identity
363
+ git fetch origin "$PIPE_TARGET_BRANCH"
364
+ cp "$OUT_FILE" "$TMP/snapshot.json"
365
+ git checkout -f -B "$PIPE_TARGET_BRANCH" "origin/$PIPE_TARGET_BRANCH"
366
+ mkdir -p "$PIPE_METRICS_OUTPUT_DIR"
367
+ cp "$TMP/snapshot.json" "$OUT_FILE"
368
+ git add "$OUT_FILE"
369
+ if git diff --cached --quiet; then
370
+ pipe_log "Snapshot already current, nothing to commit"
371
+ else
372
+ # [skip ci] keeps a data commit from starting another pipeline.
373
+ git commit -m "chore(metrics): snapshot $SNAPSHOT_DATE [skip ci]"
374
+ git push origin "$PIPE_TARGET_BRANCH"
375
+ pipe_log "Pushed snapshot to $PIPE_TARGET_BRANCH"
376
+ fi
377
+ fi
@@ -119,12 +119,18 @@ while IFS= read -r ISSUE; do
119
119
 
120
120
  export PIPE_ISSUE_IID="$IID"
121
121
  rm -f "$PIPE_CONTEXT_DIR/triage.env" "$PIPE_CONTEXT_DIR/triage.json"
122
- pipe_run_claude orchestrator "/triage-issue" "Agent,Read,Write,Edit,Glob,Grep,Bash" "$PIPE_MODEL_TRIAGE" < /dev/null
122
+ pipe_run_agent orchestrator "/triage-issue" "Agent,Read,Write,Edit,Glob,Grep,Bash" "$PIPE_MODEL_TRIAGE" < /dev/null
123
123
 
124
124
  TRIAGE_ENV="$PIPE_CONTEXT_DIR/triage.env"
125
125
  TRIAGE_JSON="$PIPE_CONTEXT_DIR/triage.json"
126
126
  DECISION=$(pipe_get_env "$TRIAGE_ENV" TRIAGE_DECISION)
127
127
  if [ -z "$DECISION" ] || [ ! -s "$TRIAGE_JSON" ]; then
128
+ if pipe_is_credit_exhaustion_files "$PIPE_AGENT_STDERR" "$TRIAGE_JSON"; then
129
+ pipe_failure_log_notice_files "triage of issue #$IID" "triage this issue by hand" \
130
+ "$PIPE_AGENT_STDERR" "$TRIAGE_JSON" | while IFS= read -r line; do pipe_log "$line"; done
131
+ pipe_log " model credit exhausted; aborting triage batch"
132
+ break
133
+ fi
128
134
  pipe_log " no usable triage output for #$IID, skipping"
129
135
  continue
130
136
  fi
@@ -140,26 +146,30 @@ while IFS= read -r ISSUE; do
140
146
  ;;
141
147
 
142
148
  ask)
143
- QUESTION=$(jq -r '.question // "Please provide more detail before this issue can be implemented."' "$TRIAGE_JSON")
144
- issue_comment "$IID" "🤖 **The pipeline needs clarification before implementing:**
145
-
146
- $QUESTION
147
-
148
- Put the complete spec in the issue **description**, not in a comment: the pipeline reads only the description."
149
+ QUESTION_FILE="$PIPE_CONTEXT_DIR/triage-question.md"
150
+ jq -r '.question // "Please provide more detail before this issue can be implemented."' "$TRIAGE_JSON" > "$QUESTION_FILE"
151
+ COMMENT_FILE="$PIPE_CONTEXT_DIR/triage-clarification.md"
152
+ {
153
+ printf '🤖 **The pipeline needs clarification before implementing:**\n\n'
154
+ cat "$QUESTION_FILE"
155
+ printf '\n\nPut the complete spec in the issue **description**, not in a comment: the pipeline reads only the description.\n'
156
+ } > "$COMMENT_FILE"
157
+ issue_comment_file "$IID" "$COMMENT_FILE"
149
158
  issue_set_labels "$IID" "$PIPE_LABEL_BLOCKER" "$PIPE_LABEL_READY"
150
- emit_decision_metric "$IID" clarification-needed ask "$QUESTION"
159
+ emit_decision_metric "$IID" clarification-needed ask "Clarification requested"
151
160
  pipe_log " asked for clarification on #$IID"
152
161
  ;;
153
162
 
154
163
  decompose_failed)
155
- SUGGEST=$(jq -r '.suggestion // "Please split this issue into smaller pieces."' "$TRIAGE_JSON")
156
- issue_comment "$IID" "**This issue is clear but too large for a single agent session.**
157
-
158
- Suggested decomposition:
159
-
160
- $SUGGEST
161
-
162
- Please split it into smaller issues and label each \`$PIPE_LABEL_READY\`."
164
+ SUGGEST_FILE="$PIPE_CONTEXT_DIR/triage-suggestion.md"
165
+ jq -r '.suggestion // "Please split this issue into smaller pieces."' "$TRIAGE_JSON" > "$SUGGEST_FILE"
166
+ COMMENT_FILE="$PIPE_CONTEXT_DIR/triage-decomposition-suggestion.md"
167
+ {
168
+ printf '**This issue is clear but too large for a single agent session.**\n\nSuggested decomposition:\n\n'
169
+ cat "$SUGGEST_FILE"
170
+ printf '\n\nPlease split it into smaller issues and label each `%s`.\n' "$PIPE_LABEL_READY"
171
+ } > "$COMMENT_FILE"
172
+ issue_comment_file "$IID" "$COMMENT_FILE"
163
173
  issue_set_labels "$IID" "$PIPE_LABEL_BLOCKER" "$PIPE_LABEL_READY"
164
174
  emit_decision_metric "$IID" clarification-needed ask "Too large; decomposition suggested"
165
175
  pipe_log " posted decomposition suggestion for #$IID"
@@ -167,13 +177,14 @@ Please split it into smaller issues and label each \`$PIPE_LABEL_READY\`."
167
177
 
168
178
  decompose)
169
179
  # Re-validate deterministically before mutating the platform.
170
- if ! pipe_validate_decomposition "$(cat "$TRIAGE_JSON")"; then
180
+ if ! pipe_validate_decomposition_file "$TRIAGE_JSON"; then
171
181
  pipe_log " decomposition failed the gate: $DECOMP_REJECT_REASON"
172
- issue_comment "$IID" "**This issue is too large and automatic decomposition did not produce a valid split.**
173
-
174
- Reason: $DECOMP_REJECT_REASON
175
-
176
- Please split it manually into smaller issues and label each \`$PIPE_LABEL_READY\`."
182
+ COMMENT_FILE="$PIPE_CONTEXT_DIR/triage-decomposition-rejected.md"
183
+ {
184
+ printf '**This issue is too large and automatic decomposition did not produce a valid split.**\n\nReason: %s\n\n' "$DECOMP_REJECT_REASON"
185
+ printf 'Please split it manually into smaller issues and label each `%s`.\n' "$PIPE_LABEL_READY"
186
+ } > "$COMMENT_FILE"
187
+ issue_comment_file "$IID" "$COMMENT_FILE"
177
188
  issue_set_labels "$IID" "$PIPE_LABEL_BLOCKER" "$PIPE_LABEL_READY"
178
189
  emit_decision_metric "$IID" clarification-needed ask "Decomposition rejected by gate"
179
190
  continue
@@ -181,7 +192,8 @@ Please split it manually into smaller issues and label each \`$PIPE_LABEL_READY\
181
192
 
182
193
  CONFIDENCE=$(pipe_get_env "$TRIAGE_ENV" TRIAGE_CONFIDENCE)
183
194
  CONFIDENCE=${CONFIDENCE:-$(jq -r '.confidence // "low"' "$TRIAGE_JSON")}
184
- REASON=$(jq -r '.reason // ""' "$TRIAGE_JSON")
195
+ REASON_FILE="$PIPE_CONTEXT_DIR/triage-reason.md"
196
+ jq -r '.reason // ""' "$TRIAGE_JSON" > "$REASON_FILE"
185
197
  SUB_COUNT=$(jq '.sub_issues | length' "$TRIAGE_JSON")
186
198
  pipe_log " decomposing #$IID into $SUB_COUNT sub-issues (confidence=$CONFIDENCE)"
187
199
 
@@ -189,11 +201,14 @@ Please split it manually into smaller issues and label each \`$PIPE_LABEL_READY\
189
201
  declare -A KEY_TO_IID=()
190
202
  CREATED=()
191
203
  FAILED=false
204
+ SUB_INDEX=0
192
205
  while IFS= read -r SUB; do
193
- SUB_KEY=$(echo "$SUB" | jq -r '.key')
194
- SUB_TITLE=$(echo "$SUB" | jq -r '.title')
195
- SUB_SPEC=$(echo "$SUB" | jq -r '.spec_markdown')
196
- HAS_DEPS=$(echo "$SUB" | jq '(.depends_on // []) | length > 0')
206
+ SUB_KEY=$(printf '%s' "$SUB" | jq -r '.key')
207
+ SUB_TITLE=$(printf '%s' "$SUB" | jq -r '.title')
208
+ SUB_SPEC_FILE="$PIPE_CONTEXT_DIR/sub-issue-${SUB_INDEX}.md"
209
+ printf '%s' "$SUB" | jq -r '.spec_markdown' > "$SUB_SPEC_FILE"
210
+ SUB_INDEX=$((SUB_INDEX + 1))
211
+ HAS_DEPS=$(printf '%s' "$SUB" | jq '(.depends_on // []) | length > 0')
197
212
  if [ "$CONFIDENCE" = "high" ] && [ "$HAS_DEPS" = "false" ]; then
198
213
  SUB_LABEL="$PIPE_LABEL_READY"
199
214
  elif [ "$CONFIDENCE" = "low" ]; then
@@ -201,7 +216,7 @@ Please split it manually into smaller issues and label each \`$PIPE_LABEL_READY\
201
216
  else
202
217
  SUB_LABEL="$PIPE_LABEL_BLOCKED"
203
218
  fi
204
- NEW_IID=$(issue_create "$SUB_TITLE" "$SUB_SPEC" "$SUB_LABEL")
219
+ NEW_IID=$(issue_create_file "$SUB_TITLE" "$SUB_SPEC_FILE" "$SUB_LABEL")
205
220
  if [ -z "$NEW_IID" ]; then
206
221
  pipe_log " ERROR: failed to create sub-issue for key '$SUB_KEY'"
207
222
  FAILED=true
@@ -214,7 +229,10 @@ Please split it manually into smaller issues and label each \`$PIPE_LABEL_READY\
214
229
 
215
230
  if [ "$FAILED" = "true" ]; then
216
231
  CHILD_LIST=$(printf '#%s ' "${CREATED[@]:-}")
217
- issue_comment "$IID" "**Auto-decomposition partially failed.** Created so far: ${CHILD_LIST:-none}. Relabeled \`$PIPE_LABEL_BLOCKER\` to prevent re-decomposition. Please finish manually."
232
+ COMMENT_FILE="$PIPE_CONTEXT_DIR/triage-decomposition-partial.md"
233
+ printf '**Auto-decomposition partially failed.** Created so far: %s. Relabeled `%s` to prevent re-decomposition. Please finish manually.\n' \
234
+ "${CHILD_LIST:-none}" "$PIPE_LABEL_BLOCKER" > "$COMMENT_FILE"
235
+ issue_comment_file "$IID" "$COMMENT_FILE"
218
236
  issue_set_labels "$IID" "$PIPE_LABEL_BLOCKER" "$PIPE_LABEL_READY"
219
237
  unset KEY_TO_IID
220
238
  continue
@@ -238,15 +256,15 @@ Please split it manually into smaller issues and label each \`$PIPE_LABEL_READY\
238
256
  # so the GitHub release poll can find them without native links).
239
257
  CHILD_LIST=$(printf '#%s ' "${CREATED[@]}")
240
258
  issue_set_labels "$IID" "$PIPE_LABEL_DECOMPOSED" "$PIPE_LABEL_READY"
241
- issue_comment "$IID" "**Auto-decomposed by the pipeline** (confidence: \`$CONFIDENCE\`).
242
-
243
- Reason: $REASON
244
-
245
- Split into $SUB_COUNT sub-issues: $CHILD_LIST
246
-
247
- Sub-issues with prerequisites are labeled \`$PIPE_LABEL_BLOCKED\` and are promoted automatically when their blockers close. This parent closes automatically when all children close.
248
-
249
- <!-- pipe-children: $CHILD_LIST -->"
259
+ COMMENT_FILE="$PIPE_CONTEXT_DIR/triage-decomposed.md"
260
+ {
261
+ printf '**Auto-decomposed by the pipeline** (confidence: `%s`).\n\nReason: ' "$CONFIDENCE"
262
+ cat "$REASON_FILE"
263
+ printf '\n\nSplit into %s sub-issues: %s\n\n' "$SUB_COUNT" "$CHILD_LIST"
264
+ printf 'Sub-issues with prerequisites are labeled `%s` and are promoted automatically when their blockers close. This parent closes automatically when all children close.\n\n' "$PIPE_LABEL_BLOCKED"
265
+ printf '<!-- pipe-children: %s -->\n' "$CHILD_LIST"
266
+ } > "$COMMENT_FILE"
267
+ issue_comment_file "$IID" "$COMMENT_FILE"
250
268
  emit_decision_metric "$IID" actionable decompose "Decomposed into $SUB_COUNT sub-issues (confidence: $CONFIDENCE)" \
251
269
  "$(jq -n --argjson sc "$SUB_COUNT" --arg conf "$CONFIDENCE" '{sub_issue_count: $sc, confidence: $conf}')"
252
270
 
@@ -38,7 +38,17 @@ fi
38
38
 
39
39
  # Run the postmortem skill unless a prior inline flow already produced the marker.
40
40
  if [ ! -f "$PIPE_CONTEXT_DIR/postmortem.env" ]; then
41
- pipe_run_claude postmortem "/postmortem-mr" "Agent,Read,Write,Edit,Glob,Grep,Bash" "$PIPE_MODEL_TRIAGE" < /dev/null
41
+ pipe_run_agent postmortem "/postmortem-mr" "Agent,Read,Write,Edit,Glob,Grep,Bash" "$PIPE_MODEL_TRIAGE" < /dev/null
42
+ if [ ! -s "$PIPE_CONTEXT_DIR/postmortem.md" ]; then
43
+ if [ -s "$PIPE_AGENT_STDERR" ]; then
44
+ pipe_failure_notice_files "postmortem diagnosis" "investigate this failure by hand" \
45
+ "$PIPE_AGENT_STDERR" > "$PIPE_CONTEXT_DIR/postmortem.md"
46
+ if pipe_is_credit_exhaustion_files "$PIPE_AGENT_STDERR"; then
47
+ platform_post_comment_file "$PIPE_CONTEXT_DIR/postmortem.md"
48
+ exit 0
49
+ fi
50
+ fi
51
+ fi
42
52
  fi
43
53
 
44
54
  # Apply the runner side: labels, comment, metric.
@@ -40,7 +40,7 @@ echo "$NOTES" | jq -r --arg u "${PIPE_BOT_USER:-}" --arg m "$REVIEW_MARKER" \
40
40
  # 5. Run the fix skill. It caps, gathers prior fix diffs, delegates to the coder,
41
41
  # verifies, writes the result JSON, and the review-fix.env marker. On cap it
42
42
  # runs the shared postmortem flow (writing postmortem.md + postmortem.env).
43
- pipe_run_claude coder "/fix-review-findings" "Agent,Read,Write,Edit,Glob,Grep,Bash" "$PIPE_MODEL_CODE" < /dev/null
43
+ pipe_run_agent coder "/fix-review-findings" "Agent,Read,Write,Edit,Glob,Grep,Bash" "$PIPE_MODEL_CODE" < /dev/null
44
44
 
45
45
  # 6. Read the marker the skill wrote.
46
46
  RF_ENV="$PIPE_CONTEXT_DIR/review-fix.env"
@@ -59,26 +59,37 @@ pipe_metric_event review-fix review_findings_applied \
59
59
  "$(jq -n --argjson a "${RF_APPLIED:-0}" --argjson d "${RF_DISMISSED:-0}" '{applied: $a, dismissed: $d}')"
60
60
 
61
61
  post_dismissed() {
62
- local dj
63
- dj=$(jq -r '.dismissed // [] | map("- **" + .finding + "**: " + .reason) | join("\n")' "$PIPE_RESULT_REVIEWFIX" 2>/dev/null || true)
64
- [ -z "$dj" ] && return 0
65
- platform_post_comment "**Review findings dismissed**
66
-
67
- The auto-fix agent determined the following findings were false positives and require no code change:
68
-
69
- $dj"
62
+ local dismissed_file comment_file
63
+ dismissed_file="$PIPE_CONTEXT_DIR/review-dismissed.md"
64
+ jq -er '.dismissed // [] | map("- **" + .finding + "**: " + .reason) | join("\n") | select(length > 0)' \
65
+ "$PIPE_RESULT_REVIEWFIX" > "$dismissed_file" 2>/dev/null || true
66
+ [ -s "$dismissed_file" ] || return 0
67
+ comment_file="$PIPE_CONTEXT_DIR/review-dismissed-comment.md"
68
+ {
69
+ printf '**Review findings dismissed**\n\nThe auto-fix agent determined the following findings were false positives and require no code change:\n\n'
70
+ cat "$dismissed_file"
71
+ } > "$comment_file"
72
+ platform_post_comment_file "$comment_file"
70
73
  }
71
74
 
72
75
  # 7. Act on the outcome.
73
76
  if [ "$RF_ESCALATE" = "true" ]; then
74
77
  pipe_log "Skill signalled escalation"
78
+ if pipe_is_credit_exhaustion_files "$PIPE_AGENT_STDERR"; then
79
+ COMMENT_FILE="$PIPE_CONTEXT_DIR/review-fix-credit-exhausted.md"
80
+ pipe_failure_notice_files "review fixes" "resolve the review findings by hand" \
81
+ "$PIPE_AGENT_STDERR" > "$COMMENT_FILE"
82
+ platform_post_comment_file "$COMMENT_FILE"
83
+ exit 0
84
+ fi
75
85
  pipe_apply_escalation "$CATEGORY" "review-fix"
76
86
  elif [ "$COMMIT_MADE" = "true" ]; then
77
87
  if pipe_rebase_and_push; then
78
88
  ATTEMPT=$((FIX_COUNT_BEFORE + 1))
79
- platform_post_comment "**Auto-fix applied (attempt ${ATTEMPT}/${PIPE_FIX_LOOP_CAP})**
80
-
81
- Fixes committed for confirmed bugs from the code review. A new pipeline will run to verify and re-review the changes."
89
+ COMMENT_FILE="$PIPE_CONTEXT_DIR/review-fix-applied.md"
90
+ printf '**Auto-fix applied (attempt %s/%s)**\n\nFixes committed for confirmed bugs from the code review. A new pipeline will run to verify and re-review the changes.\n' \
91
+ "$ATTEMPT" "$PIPE_FIX_LOOP_CAP" > "$COMMENT_FILE"
92
+ platform_post_comment_file "$COMMENT_FILE"
82
93
  post_dismissed
83
94
  fi
84
95
  elif [ "$RF_CHANGED" = "false" ]; then