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

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.3",
3
+ "version": "3.1.4",
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",
@@ -79,6 +79,18 @@ variables:
79
79
  PIPE_ORCHESTRATE: "0" # set to "1" in the schedule to run orchestrate
80
80
  PIPE_SPEC_TEMPLATE_PATH: ".gitlab/issue_templates/Spec.md"
81
81
 
82
+ # --- Agent deadline (omp harness) -----------------------------------------
83
+ # A job `timeout:` is a hard kill — the runner SIGKILLs the process tree and
84
+ # the runner script never posts its comment, metrics or marker file. So
85
+ # `pipe_run_omp` bounds the agent itself with `omp --max-time`, derived from
86
+ # GitLab's own CI_JOB_TIMEOUT minus the time already spent minus the reserve
87
+ # below, which leaves room for the post-agent steps. An overrun then takes
88
+ # the normal degraded path (failure-notice comment + metrics) instead of
89
+ # dying silently. Set PIPE_AGENT_MAX_TIME (e.g. "20m") to override the
90
+ # derivation for every job; GitHub Actions exposes no job-timeout variable,
91
+ # so there the flag is omitted and timeout-minutes stays the only bound.
92
+ PIPE_AGENT_TIME_RESERVE: "180" # seconds of the job budget kept for the post-agent steps
93
+
82
94
  # --- Build / verify --------------------------------------------------------
83
95
  PIPE_VERIFY_CMD: "" # compile + unit tests, run after a fix
84
96
  PIPE_COMPILE_CMD: "" # compile only (defaults to PIPE_VERIFY_CMD)
@@ -212,11 +224,15 @@ variables:
212
224
 
213
225
  # =============================================================================
214
226
  # review: run the automated review on a non-draft MR that changes code.
227
+ # The 45m budget matches `code` and is not generous: the skill fans out to the
228
+ # code-reviewer plus up to three specialist subagents, and observed runs on a
229
+ # real project spread from 1m to over 20m with diff size. It was 20m until
230
+ # three consecutive cap-outs on large MRs were measured in production.
215
231
  # =============================================================================
216
232
  review:
217
233
  extends: .claude-base
218
234
  stage: review
219
- timeout: 20m
235
+ timeout: 45m
220
236
  script:
221
237
  - bash "$PIPE_SCRIPTS_DIR/review.sh"
222
238
  artifacts:
@@ -369,6 +369,49 @@ pipe_translate_tools_to_omp() {
369
369
  echo "${result#,}"
370
370
  }
371
371
 
372
+ # --- Agent deadline -----------------------------------------------------
373
+ # A CI job timeout is a hard kill: the runner SIGKILLs the whole process tree,
374
+ # so the runner script never reaches the steps that post the comment, write
375
+ # the metrics record and emit the marker file. An agent that overruns
376
+ # therefore leaves a red job with no output at all (observed in production:
377
+ # three review timeouts in one week, each at the cap with the agent mid-run).
378
+ # Bounding the agent itself with `omp --max-time` turns an overrun into the
379
+ # platform's normal degraded path — failure-notice comment plus metrics —
380
+ # instead of a hard kill.
381
+
382
+ pipe_agent_max_time() {
383
+ # Echoes the --max-time value to pass to omp, or nothing when no deadline
384
+ # can be derived (GitHub Actions exposes no CI_JOB_TIMEOUT equivalent;
385
+ # there the workflow's timeout-minutes stays the only bound). An explicit
386
+ # PIPE_AGENT_MAX_TIME wins and is passed through verbatim, so a project can
387
+ # set "20m" or "1h" by hand. Otherwise the budget is the job timeout, minus
388
+ # the time already spent (image pull, checkout, before_script, API prep),
389
+ # minus PIPE_AGENT_TIME_RESERVE for the post-agent steps.
390
+ if [ -n "${PIPE_AGENT_MAX_TIME:-}" ]; then
391
+ printf '%s' "$PIPE_AGENT_MAX_TIME"
392
+ return 0
393
+ fi
394
+ local budget="${CI_JOB_TIMEOUT:-}"
395
+ case "$budget" in ''|*[!0-9]*) return 0 ;; esac
396
+ local reserve="${PIPE_AGENT_TIME_RESERVE:-180}" elapsed=0 started now
397
+ # Guard the emptiness explicitly: `date -d ""` does not fail, it silently
398
+ # resolves to midnight today, which would read as a many-hour-old job.
399
+ if [ -n "${CI_JOB_STARTED_AT:-}" ]; then
400
+ started=$(date -d "$CI_JOB_STARTED_AT" +%s 2>/dev/null || true)
401
+ if [ -n "$started" ]; then
402
+ now=$(date +%s)
403
+ elapsed=$(( now - started ))
404
+ [ "$elapsed" -lt 0 ] && elapsed=0
405
+ fi
406
+ fi
407
+ local left=$(( budget - elapsed - reserve ))
408
+ # A job already out of budget gets no flag: let the agent start and be cut
409
+ # by the runner exactly as it was before this function existed, rather than
410
+ # be killed one second in with a nonsensical deadline.
411
+ [ "$left" -lt 60 ] && return 0
412
+ printf '%s' "$left"
413
+ }
414
+
372
415
  pipe_run_omp() {
373
416
  # $1 = agent label for metrics, $2 = skill invocation (e.g. "/review-mr"),
374
417
  # $3 = allowed tools in Claude Code naming (optional, translated
@@ -377,6 +420,14 @@ pipe_run_omp() {
377
420
  # frontmatter), stdin = extra prompt (optional, usually empty).
378
421
  local label="$1" skill="$2" tools="${3:-Agent,Read,Write,Edit,Glob,Grep,Bash}" model="${4:-}"
379
422
  local omp_tools; omp_tools=$(pipe_translate_tools_to_omp "$tools")
423
+ # Bound the run below the job's own hard kill (see pipe_agent_max_time).
424
+ local max_time; max_time=$(pipe_agent_max_time)
425
+ local -a max_time_args=()
426
+ local max_time_txt=""
427
+ if [ -n "$max_time" ]; then
428
+ max_time_args=(--max-time "$max_time")
429
+ max_time_txt=$(printf ' --max-time %q' "$max_time")
430
+ fi
380
431
  pipe_require_harness omp
381
432
  local stdin_file; stdin_file=$(mktemp)
382
433
  cat > "$stdin_file" || true
@@ -402,12 +453,14 @@ pipe_run_omp() {
402
453
  printf 'cd %q\n' "$PWD"
403
454
  printf 'source %q\n' "$SCRIPT_LIB_DIR/usage-capture-omp.sh"
404
455
  printf 'export PIPE_CAPTURE_MODEL=%q\n' "$model"
456
+ # The %s after `--` is the optional " --max-time <d>" fragment, already
457
+ # shell-quoted; it collapses to nothing when no deadline was derived.
405
458
  if [ -n "$model" ]; then
406
- printf 'capture_omp %q -- --yolo --no-session --no-title --model %q --tools %q -p %q < %q\n' \
407
- "$label" "$model" "$omp_tools" "$skill" "$stdin_file"
459
+ printf 'capture_omp %q --%s --yolo --no-session --no-title --model %q --tools %q -p %q < %q\n' \
460
+ "$label" "$max_time_txt" "$model" "$omp_tools" "$skill" "$stdin_file"
408
461
  else
409
- printf 'capture_omp %q -- --yolo --no-session --no-title --tools %q -p %q < %q\n' \
410
- "$label" "$omp_tools" "$skill" "$stdin_file"
462
+ printf 'capture_omp %q --%s --yolo --no-session --no-title --tools %q -p %q < %q\n' \
463
+ "$label" "$max_time_txt" "$omp_tools" "$skill" "$stdin_file"
411
464
  fi
412
465
  } > "$runner"
413
466
  chmod +x "$runner"
@@ -420,9 +473,9 @@ pipe_run_omp() {
420
473
  pipe_scrub_agent_secrets
421
474
  export PIPE_CAPTURE_MODEL="$model"
422
475
  if [ -n "$model" ]; then
423
- capture_omp "$label" -- --yolo --no-session --no-title --model "$model" --tools "$omp_tools" -p "$skill" < "$stdin_file"
476
+ capture_omp "$label" -- "${max_time_args[@]}" --yolo --no-session --no-title --model "$model" --tools "$omp_tools" -p "$skill" < "$stdin_file"
424
477
  else
425
- capture_omp "$label" -- --yolo --no-session --no-title --tools "$omp_tools" -p "$skill" < "$stdin_file"
478
+ capture_omp "$label" -- "${max_time_args[@]}" --yolo --no-session --no-title --tools "$omp_tools" -p "$skill" < "$stdin_file"
426
479
  fi
427
480
  ) || true
428
481
  fi
@@ -186,9 +186,13 @@ MRS_VIEW=$(jq -c \
186
186
  elif ($body | test($reintroduce; "i")) then "reintroduces"
187
187
  else null end
188
188
  ),
189
+ # match/2 with "g", not scan/2: a Debian bookworm based runner image
190
+ # ships jq 1.6, which has no scan/2 and fails the whole program with
191
+ # a compile error.
189
192
  references: [
190
193
  $body
191
- | scan("(?:" + $revert + "|" + $regression + "|" + $reintroduce + ")[[:space:]]*[!#]?([0-9]+)"; "i")[]?
194
+ | match("(?:" + $revert + "|" + $regression + "|" + $reintroduce + ")[[:space:]]*[!#]?([0-9]+)"; "gi")
195
+ | .captures[].string
192
196
  | select(. != null)
193
197
  | tonumber
194
198
  ]