@cxi-lmai/ci-agent-platform 3.1.4 → 3.1.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cxi-lmai/ci-agent-platform",
3
- "version": "3.1.4",
3
+ "version": "3.1.5",
4
4
  "description": "Autonomous dev pipeline on plain GitLab CI or GitHub Actions, driven by Claude Code. A labeled issue goes in, an open merge request comes out.",
5
5
  "keywords": [
6
6
  "claude",
@@ -277,10 +277,16 @@ pipe_run_claude() {
277
277
  # $3 = allowed tools (optional), $4 = model for the main loop (optional,
278
278
  # e.g. $PIPE_MODEL_REVIEW; subagents keep their frontmatter models),
279
279
  # stdin = extra prompt (optional, usually empty)
280
+ #
281
+ # Always returns 0, so a caller running under `set -e` keeps control over what
282
+ # a failed agent means for its job. The agent's real exit code lands in
283
+ # PIPE_AGENT_RC. A caller that must not read a crashed agent as a clean run
284
+ # checks that variable, or calls pipe_require_agent_ran below.
280
285
  local label="$1" skill="$2" tools="${3:-Agent,Read,Write,Edit,Glob,Grep,Bash}" model="${4:-}"
281
286
  pipe_require_harness claude
282
287
  local stdin_file; stdin_file=$(mktemp)
283
288
  cat > "$stdin_file" || true
289
+ PIPE_AGENT_RC=0
284
290
 
285
291
  if [ "$(id -u)" -eq 0 ]; then
286
292
  local runuser="pipelinebot"
@@ -315,7 +321,7 @@ pipe_run_claude() {
315
321
  } > "$runner"
316
322
  chmod +x "$runner"
317
323
  chown "$runuser:$runuser" "$runner"
318
- su -m "$runuser" -s /bin/bash "$runner" || true
324
+ su -m "$runuser" -s /bin/bash "$runner" || PIPE_AGENT_RC=$?
319
325
  chown -R root:root "$PWD" 2>/dev/null || true
320
326
  rm -f "$runner"
321
327
  else
@@ -327,9 +333,32 @@ pipe_run_claude() {
327
333
  else
328
334
  capture_claude "$label" -- --dangerously-skip-permissions --allowedTools "$tools" -p "$skill" < "$stdin_file"
329
335
  fi
330
- ) || true
336
+ ) || PIPE_AGENT_RC=$?
331
337
  fi
332
338
  rm -f "$stdin_file"
339
+ [ "$PIPE_AGENT_RC" -eq 0 ] || pipe_log "agent exited with code $PIPE_AGENT_RC"
340
+ return 0
341
+ }
342
+
343
+ pipe_require_agent_ran() {
344
+ # $1 = file the skill was supposed to write (empty to check the exit code
345
+ # only, for skills whose marker is legitimately optional), $2 = human label.
346
+ # Fails the job when the agent crashed or wrote nothing. Without this an
347
+ # agent that never started is indistinguishable from one that ran and found
348
+ # nothing: every caller reads its marker file, misses it, and falls back to
349
+ # the "all clear" default, so the job goes green without a review, fix, or
350
+ # triage having happened.
351
+ local marker="$1" what="${2:-agent}"
352
+ if [ "${PIPE_AGENT_RC:-0}" -ne 0 ]; then
353
+ pipe_log "ERROR: $what did not complete (exit code $PIPE_AGENT_RC). See the job log above."
354
+ return 1
355
+ fi
356
+ [ -n "$marker" ] || return 0
357
+ if [ ! -s "$marker" ]; then
358
+ pipe_log "ERROR: $what produced no output ($marker is missing or empty)."
359
+ return 1
360
+ fi
361
+ return 0
333
362
  }
334
363
 
335
364
  # --- omp invocation -----------------------------------------------------
@@ -418,6 +447,9 @@ pipe_run_omp() {
418
447
  # internally), $4 = model for the main loop (optional, e.g.
419
448
  # $PIPE_MODEL_REVIEW; subagents keep the models from their own
420
449
  # frontmatter), stdin = extra prompt (optional, usually empty).
450
+ #
451
+ # Same contract as pipe_run_claude: always returns 0 and reports the agent's
452
+ # real exit code in PIPE_AGENT_RC, which pipe_require_agent_ran checks.
421
453
  local label="$1" skill="$2" tools="${3:-Agent,Read,Write,Edit,Glob,Grep,Bash}" model="${4:-}"
422
454
  local omp_tools; omp_tools=$(pipe_translate_tools_to_omp "$tools")
423
455
  # Bound the run below the job's own hard kill (see pipe_agent_max_time).
@@ -431,6 +463,7 @@ pipe_run_omp() {
431
463
  pipe_require_harness omp
432
464
  local stdin_file; stdin_file=$(mktemp)
433
465
  cat > "$stdin_file" || true
466
+ PIPE_AGENT_RC=0
434
467
 
435
468
  if [ "$(id -u)" -eq 0 ]; then
436
469
  local runuser="pipelinebot"
@@ -465,7 +498,7 @@ pipe_run_omp() {
465
498
  } > "$runner"
466
499
  chmod +x "$runner"
467
500
  chown "$runuser:$runuser" "$runner"
468
- su -m "$runuser" -s /bin/bash "$runner" || true
501
+ su -m "$runuser" -s /bin/bash "$runner" || PIPE_AGENT_RC=$?
469
502
  chown -R root:root "$PWD" 2>/dev/null || true
470
503
  rm -f "$runner"
471
504
  else
@@ -477,9 +510,11 @@ pipe_run_omp() {
477
510
  else
478
511
  capture_omp "$label" -- "${max_time_args[@]}" --yolo --no-session --no-title --tools "$omp_tools" -p "$skill" < "$stdin_file"
479
512
  fi
480
- ) || true
513
+ ) || PIPE_AGENT_RC=$?
481
514
  fi
482
515
  rm -f "$stdin_file"
516
+ [ "$PIPE_AGENT_RC" -eq 0 ] || pipe_log "agent exited with code $PIPE_AGENT_RC"
517
+ return 0
483
518
  }
484
519
 
485
520
  # --- Harness dispatch ---------------------------------------------------
@@ -41,6 +41,9 @@ echo "$NOTES" | jq -r --arg u "${PIPE_BOT_USER:-}" --arg m "$REVIEW_MARKER" \
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
43
  pipe_run_agent coder "/fix-review-findings" "Agent,Read,Write,Edit,Glob,Grep,Bash" "$PIPE_MODEL_CODE" < /dev/null
44
+ # A crashed agent writes no marker, which step 6 would read as "nothing changed"
45
+ # and step 7 would report as a completed fix run that had nothing to do.
46
+ pipe_require_agent_ran "" "The review-fix agent" || exit 1
44
47
 
45
48
  # 6. Read the marker the skill wrote.
46
49
  RF_ENV="$PIPE_CONTEXT_DIR/review-fix.env"
@@ -87,7 +87,14 @@ pipe_metric_event code-reviewer review_findings \
87
87
  --argjson inc "$([ "${PIPE_IS_INCREMENTAL:-false}" = "true" ] && echo true || echo false)" \
88
88
  '{findings_total: $t, status: $s, has_fixable_bugs: $b, is_incremental: $inc}')"
89
89
 
90
- # 7. Gate. review.env is exported so the fix job can read REVIEW_HAS_BUGS at
90
+ # 7. Fail the job when no review was produced. The comment above already told
91
+ # the humans; this tells the pipeline. PIPE_NO_GATE does not apply: it turns
92
+ # off the verdict gate, and this is a broken run, not a verdict. Without it a
93
+ # review that never ran (crashed agent, missing ANTHROPIC_API_KEY) reads as a
94
+ # review that ran and found nothing, and the MR goes green unreviewed.
95
+ pipe_require_agent_ran "$PIPE_RESULT_REVIEW" "The review agent" || exit 1
96
+
97
+ # 8. Gate. review.env is exported so the fix job can read REVIEW_HAS_BUGS at
91
98
  # runtime. On GitLab the job exits non-zero to block the pipeline. On GitHub
92
99
  # the workflow step owns the gate (it reads the output first), so it sets
93
100
  # PIPE_NO_GATE=1 and this script leaves the exit to the workflow.
@@ -27,6 +27,9 @@ COVERAGE_BEFORE=$(pipe_count_fix_commits "$PIPE_COMMIT_COVERAGE")
27
27
  # test-writer, verifies, and writes fix-tests.env. On cap it runs the shared
28
28
  # postmortem flow (writing postmortem.md + postmortem.env).
29
29
  pipe_run_agent test-fix "/fix-tests" "Agent,Read,Write,Edit,Glob,Grep,Bash" "$PIPE_MODEL_CODE" < /dev/null
30
+ # A crashed agent writes no marker, which step 4 would read as FIX_BRANCH=none
31
+ # and report as a false-alarm trigger, hiding the failure behind a green job.
32
+ pipe_require_agent_ran "" "The test-fix agent" || exit 1
30
33
 
31
34
  # 4. Read the marker the skill wrote.
32
35
  FT_ENV="$PIPE_CONTEXT_DIR/fix-tests.env"