@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
@@ -9,8 +9,12 @@ set -u
9
9
  SCRIPT_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
10
  # shellcheck source=./usage-capture.sh
11
11
  source "$SCRIPT_LIB_DIR/usage-capture.sh"
12
+ # shellcheck source=./usage-capture-omp.sh
13
+ source "$SCRIPT_LIB_DIR/usage-capture-omp.sh"
12
14
  # shellcheck source=./platform.sh
13
15
  source "$SCRIPT_LIB_DIR/platform.sh"
16
+ # shellcheck source=./failure-notice.sh
17
+ source "$SCRIPT_LIB_DIR/failure-notice.sh"
14
18
 
15
19
  # --- Defaults ---------------------------------------------------------------
16
20
  # Every PIPE_ variable has a default here so a project that overrides nothing
@@ -23,28 +27,87 @@ pipe_defaults() {
23
27
  : "${PIPE_CONFIG_PATH:=.claude/pipeline-config.md}"
24
28
  : "${PIPE_CONTEXT_DIR:=build/pipeline}"
25
29
  : "${PIPE_METRICS_DIR:=$PIPE_CONTEXT_DIR/metrics}"
30
+ : "${PIPE_AGENT_STDERR:=$PIPE_CONTEXT_DIR/agent-stderr.log}"
26
31
  : "${PIPE_TARGET_BRANCH:=main}"
27
32
  : "${PIPE_FIX_LOOP_CAP:=2}"
28
33
  : "${PIPE_CODER_CAP:=3}"
29
34
  : "${PIPE_ISSUE_SCAN:=20}"
30
- : "${PIPE_MODEL_TRIAGE:=haiku}"
31
- : "${PIPE_MODEL_CODE:=sonnet}"
32
- : "${PIPE_MODEL_REVIEW:=sonnet}"
35
+ : "${PIPE_HARNESS:=claude}"
36
+ # Model defaults track the active harness. Each PIPE_MODEL_* variable remains
37
+ # explicitly overridable.
38
+ if [ "$PIPE_HARNESS" = "omp" ]; then
39
+ : "${PIPE_MODEL_TRIAGE:=openrouter/anthropic/claude-haiku-4-5}"
40
+ : "${PIPE_MODEL_CODE:=openrouter/anthropic/claude-sonnet-5-0}"
41
+ : "${PIPE_MODEL_REVIEW:=openrouter/anthropic/claude-sonnet-5-0}"
42
+ else
43
+ : "${PIPE_MODEL_TRIAGE:=haiku}"
44
+ : "${PIPE_MODEL_CODE:=claude-sonnet-5-0}"
45
+ : "${PIPE_MODEL_REVIEW:=claude-sonnet-5-0}"
46
+ fi
33
47
  : "${PIPE_COMMIT_TESTFIX:=Fix test errors}"
34
48
  : "${PIPE_COMMIT_REVIEWFIX:=Fix review findings}"
35
49
  : "${PIPE_COMMIT_COVERAGE:=Add coverage tests}"
50
+ : "${PIPE_COMMIT_DOCSSYNC:=Update docs per docs-sync findings}"
51
+ : "${PIPE_COMMIT_AUDIT:=docs: apply convention audit findings}"
52
+ : "${PIPE_COMMIT_CVEFIX:=Update dependencies to resolve CVEs}"
36
53
  : "${PIPE_LABEL_READY:=pipe-ready}"
37
54
  : "${PIPE_LABEL_WIP:=pipe-wip}"
38
55
  : "${PIPE_LABEL_STUCK:=pipe-stuck}"
39
56
  : "${PIPE_LABEL_BLOCKER:=pipe-blocker}"
40
57
  : "${PIPE_LABEL_BLOCKED:=pipe-blocked}"
41
58
  : "${PIPE_LABEL_DECOMPOSED:=pipe-decomposed}"
59
+ : "${PIPE_LABEL_AUDIT:=codebase-audit}"
42
60
  : "${PIPE_RESULT_REVIEW:=$PIPE_CONTEXT_DIR/code-review-result.txt}"
43
61
  : "${PIPE_RESULT_REVIEWFIX:=$PIPE_CONTEXT_DIR/review-fix-result.json}"
62
+ : "${PIPE_RESULT_DOCSSYNC:=$PIPE_CONTEXT_DIR/docs-sync.json}"
44
63
  : "${PIPE_VERIFY_CMD:=}"
45
64
  : "${PIPE_COMPILE_CMD:=${PIPE_VERIFY_CMD}}"
46
65
  : "${PIPE_TEST_REPORT_GLOB:=}"
66
+ : "${PIPE_COVERAGE_RATCHET:=0}"
67
+ : "${PIPE_COVERAGE_REPORT:=}"
68
+ : "${PIPE_COVERAGE_REPORT_KIND:=jacoco}"
47
69
  : "${PIPE_COVERAGE_SIGNAL:=}"
70
+ # CVE remediation. Off by default: project-local scanners provide the
71
+ # HIGH/CRITICAL identifier list when PIPE_CVE_FIX=1 enables this job.
72
+ : "${PIPE_CVE_FIX:=0}"
73
+ : "${PIPE_DEPENDENCY_MANIFEST:=build.gradle}"
74
+ : "${PIPE_CVE_SUPPRESSIONS:=owasp-suppressions.xml}"
75
+ # Issue-driven E2E generation. Off by default; PIPE_VERIFY_CMD is the
76
+ # configured verification command and must be non-empty before tests run.
77
+ : "${PIPE_E2E_TEST_GEN:=0}"
78
+ : "${PIPE_LABEL_TESTREADY:=test-ready}"
79
+ : "${PIPE_LABEL_E2E_SCOPE:=}"
80
+ : "${PIPE_E2E_TEST_DIR:=e2e/tests}"
81
+ : "${PIPE_E2E_BASE_URL:=}"
82
+ : "${PIPE_E2E_VERIFY_CMD:=}"
83
+ # Documentation sync. Off by default: the job only runs when the pipeline
84
+ # sets PIPE_DOCS_SYNC=1. PIPE_DOCS_ROOTS is the comma-separated set of paths
85
+ # the docs-sync agent may edit and the runner may commit.
86
+ : "${PIPE_DOCS_SYNC:=0}"
87
+ : "${PIPE_DOCS_ROOTS:=docs,CLAUDE.md,.claude/memory}"
88
+ # Codebase audit. Off by default: the job only runs on a schedule that sets
89
+ # PIPE_CODEBASE_AUDIT=1. Reuses PIPE_DOCS_ROOTS for the optional auto-fix
90
+ # phase's coder scope.
91
+ : "${PIPE_CODEBASE_AUDIT:=0}"
92
+ # Agent architect. Off by default: a scheduled pipeline explicitly enables
93
+ # this evidence-backed proposal job after a project has reviewed its output.
94
+ : "${PIPE_AGENT_ARCHITECT:=0}"
95
+ : "${PIPE_ARCHITECT_WINDOW_DAYS:=7}"
96
+ : "${PIPE_ARCHITECT_DRY_RUN:=true}"
97
+ : "${PIPE_LABEL_IMPROVEMENT:=pipe-improvement}"
98
+ # Metrics aggregation. Off by default: the job only runs on a schedule that
99
+ # sets PIPE_METRICS_SNAPSHOT=1, and it publishes an artifact unless the
100
+ # project explicitly opts in to committing generated data to its repository.
101
+ : "${PIPE_METRICS_SNAPSHOT:=0}"
102
+ : "${PIPE_METRICS_OUTPUT_DIR:=data/metrics/snapshots}"
103
+ : "${PIPE_METRICS_WINDOW_DAYS:=1}"
104
+ : "${PIPE_METRICS_COMMIT_REPO:=false}"
105
+ : "${PIPE_METRICS_REVIEW_AGENTS:=code-reviewer,review-verdict,review-fix}"
106
+ # Commit subjects that mark a defect in already-merged work. Keep in sync with
107
+ # the Metrics Conventions section of the pipeline config.
108
+ : "${PIPE_METRICS_DEFECT_REVERT:=Reverts !}"
109
+ : "${PIPE_METRICS_DEFECT_REGRESSION:=Fixes regression from !}"
110
+ : "${PIPE_METRICS_DEFECT_REINTRODUCE:=Reintroduces work from !}"
48
111
  # Spec template path. GitHub projects usually keep it under .github, GitLab
49
112
  # under .gitlab, so the default follows the detected platform.
50
113
  if [ "$PIPE_PLATFORM" = "github" ]; then
@@ -58,13 +121,21 @@ pipe_defaults() {
58
121
  # would pick up result files (E2E finding N-6). A `*` gitignore inside the
59
122
  # dir hides everything in it, including itself, from any git add.
60
123
  [ -f "$PIPE_CONTEXT_DIR/.gitignore" ] || printf '*\n' > "$PIPE_CONTEXT_DIR/.gitignore"
61
- export PIPE_PLATFORM PIPE_CONFIG_PATH PIPE_CONTEXT_DIR PIPE_METRICS_DIR \
124
+ export PIPE_PLATFORM PIPE_CONFIG_PATH PIPE_CONTEXT_DIR PIPE_METRICS_DIR PIPE_AGENT_STDERR \
62
125
  PIPE_TARGET_BRANCH PIPE_FIX_LOOP_CAP PIPE_CODER_CAP PIPE_ISSUE_SCAN PIPE_COMMIT_TESTFIX \
63
- PIPE_COMMIT_REVIEWFIX PIPE_COMMIT_COVERAGE PIPE_LABEL_READY PIPE_LABEL_WIP \
64
- PIPE_LABEL_STUCK PIPE_LABEL_BLOCKER PIPE_LABEL_BLOCKED PIPE_LABEL_DECOMPOSED \
65
- PIPE_RESULT_REVIEW PIPE_RESULT_REVIEWFIX PIPE_VERIFY_CMD PIPE_COMPILE_CMD \
66
- PIPE_TEST_REPORT_GLOB PIPE_COVERAGE_SIGNAL PIPE_SPEC_TEMPLATE_PATH \
67
- PIPE_MODEL_TRIAGE PIPE_MODEL_CODE PIPE_MODEL_REVIEW
126
+ PIPE_COMMIT_REVIEWFIX PIPE_COMMIT_COVERAGE PIPE_COMMIT_CVEFIX PIPE_LABEL_READY PIPE_LABEL_WIP \
127
+ PIPE_LABEL_STUCK PIPE_LABEL_BLOCKER PIPE_LABEL_BLOCKED PIPE_LABEL_DECOMPOSED PIPE_LABEL_AUDIT \
128
+ PIPE_RESULT_REVIEW PIPE_RESULT_REVIEWFIX PIPE_RESULT_DOCSSYNC PIPE_COMMIT_DOCSSYNC PIPE_COMMIT_AUDIT \
129
+ PIPE_CVE_FIX PIPE_DEPENDENCY_MANIFEST PIPE_CVE_SUPPRESSIONS PIPE_E2E_TEST_GEN PIPE_LABEL_TESTREADY \
130
+ PIPE_LABEL_E2E_SCOPE PIPE_E2E_TEST_DIR PIPE_E2E_BASE_URL PIPE_E2E_VERIFY_CMD PIPE_DOCS_SYNC PIPE_DOCS_ROOTS PIPE_VERIFY_CMD PIPE_COMPILE_CMD \
131
+ PIPE_TEST_REPORT_GLOB PIPE_COVERAGE_RATCHET PIPE_COVERAGE_REPORT PIPE_COVERAGE_REPORT_KIND \
132
+ PIPE_COVERAGE_SIGNAL PIPE_SPEC_TEMPLATE_PATH PIPE_CODEBASE_AUDIT PIPE_AGENT_ARCHITECT \
133
+ PIPE_ARCHITECT_WINDOW_DAYS PIPE_ARCHITECT_DRY_RUN PIPE_LABEL_IMPROVEMENT \
134
+ PIPE_METRICS_SNAPSHOT PIPE_METRICS_OUTPUT_DIR PIPE_METRICS_WINDOW_DAYS \
135
+ PIPE_METRICS_COMMIT_REPO PIPE_METRICS_REVIEW_AGENTS PIPE_METRICS_DEFECT_REVERT \
136
+ PIPE_METRICS_DEFECT_REGRESSION PIPE_METRICS_DEFECT_REINTRODUCE \
137
+ PIPE_METRICS_SNAPSHOT_DATE PIPE_METRICS_RAW_DIR PIPE_METRICS_MRS_FILE \
138
+ PIPE_HARNESS PIPE_MODEL_TRIAGE PIPE_MODEL_CODE PIPE_MODEL_REVIEW
68
139
  }
69
140
 
70
141
  pipe_log() { echo "[pipeline] $*"; }
@@ -88,6 +159,17 @@ pipe_git_identity() {
88
159
  # Checkout the real source branch so commits land on the branch tip, not on the
89
160
  # detached MR/PR ref that can fall behind and cause non-fast-forward push errors.
90
161
  pipe_checkout_source() {
162
+ # The MR/PR itself is the authoritative source of the branch name. A
163
+ # workflow_run or branch pipeline carries no merge-request source ref, and
164
+ # without one this checkout used to no-op silently, leaving the fix commit on
165
+ # a detached head that no push could land.
166
+ if [ -z "${PIPE_SOURCE_BRANCH:-}" ] && [ -n "${PIPE_MR_IID:-}" ]; then
167
+ # `|| PIPE_SOURCE_BRANCH=""` is the whole error path: an unreachable or
168
+ # unconfigured API must leave the caller where it was, not abort it.
169
+ PIPE_SOURCE_BRANCH=$(platform_mr_meta 2>/dev/null | jq -r '.source_branch // ""' 2>/dev/null) \
170
+ || PIPE_SOURCE_BRANCH=""
171
+ export PIPE_SOURCE_BRANCH
172
+ fi
91
173
  [ -z "${PIPE_SOURCE_BRANCH:-}" ] && return 0
92
174
  git fetch origin "$PIPE_TARGET_BRANCH" "$PIPE_SOURCE_BRANCH" 2>/dev/null || true
93
175
  git checkout -B "$PIPE_SOURCE_BRANCH" "origin/$PIPE_SOURCE_BRANCH" 2>/dev/null || true
@@ -106,15 +188,17 @@ pipe_rebase_and_push() {
106
188
  }
107
189
 
108
190
  # --- Cap counting -----------------------------------------------------------
109
- # Count consecutive commits from the tip back toward the target branch whose
110
- # subject matches a pattern. The skill also computes this, but the runner needs
111
- # it to decide whether the agent actually committed this run.
112
-
191
+ # Count consecutive matching commits from the tip back toward the target branch.
192
+ # Docs-sync commits are transparent: they do not count, but do not interrupt a
193
+ # fix streak. The skills use the same exact-subject rules for their cap checks;
194
+ # the runner needs this count only to detect whether a fix commit landed.
113
195
  pipe_count_fix_commits() {
114
196
  local pattern="$1" count=0 subject
115
197
  while IFS= read -r subject; do
116
- if echo "$subject" | grep -qF "$pattern"; then
198
+ if [ "$subject" = "$pattern" ]; then
117
199
  count=$((count + 1))
200
+ elif [ "$subject" = "$PIPE_COMMIT_DOCSSYNC" ]; then
201
+ continue
118
202
  else
119
203
  break
120
204
  fi
@@ -131,14 +215,22 @@ pipe_count_fix_commits() {
131
215
 
132
216
  pipe_agent_secret_var() {
133
217
  # Return success when an exported variable looks credential-bearing and is
134
- # not explicitly required by the Claude API itself or allowlisted for a
135
- # project verification command. The allowlist is comma/space separated.
218
+ # not explicitly required by the active harness's own auth or allowlisted
219
+ # for a project verification command. The allowlist is comma/space
220
+ # separated.
136
221
  local name="$1" allow=" ${PIPE_AGENT_ENV_ALLOWLIST:-} "
137
222
  allow=" ${allow//,/ } "
138
- # The Claude process must keep its own credential regardless of which of the
139
- # three supported auth variables the project uses.
140
- case "$name" in
141
- ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|CLAUDE_CODE_OAUTH_TOKEN) return 1 ;;
223
+ # Preserve only the active harness's built-in model credentials. Credentials
224
+ # for the inactive harness fall through to the generic secret-name check.
225
+ case "${PIPE_HARNESS:-claude}" in
226
+ omp)
227
+ [ "$name" = "OPENROUTER_API_KEY" ] && return 1
228
+ ;;
229
+ *)
230
+ case "$name" in
231
+ ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|CLAUDE_CODE_OAUTH_TOKEN) return 1 ;;
232
+ esac
233
+ ;;
142
234
  esac
143
235
  [[ "$allow" == *" $name "* ]] && return 1
144
236
  case "$name" in
@@ -213,6 +305,115 @@ pipe_run_claude() {
213
305
  rm -f "$stdin_file"
214
306
  }
215
307
 
308
+ # --- omp invocation -----------------------------------------------------
309
+ # Runs `omp -p "/<skill>"` wrapped in usage-capture, the omp equivalent of
310
+ # pipe_run_claude. Same non-root/su-user dance (unverified whether omp
311
+ # tolerates root under --yolo; the same-user fallback Claude Code needs is
312
+ # always safe, so it ships unconditionally either way).
313
+
314
+ pipe_translate_tools_to_omp() {
315
+ # $1 = Claude Code-style comma-separated tool list, e.g.
316
+ # "Agent,Read,Write,Edit,Glob,Grep,Bash". Echoes the omp-equivalent
317
+ # comma-separated list, deduplicated and order-preserving. Tokens with no
318
+ # omp equivalent (LS, NotebookRead, Skill, KillShell, BashOutput) are
319
+ # dropped; mcp__* and any unrecognized token pass through unchanged. See
320
+ # the translation table in
321
+ # docs/superpowers/specs/2026-08-26-omp-openrouter-harness-design.md.
322
+ local tools="$1" tok mapped seen="," result=""
323
+ IFS=',' read -ra tokens <<< "$tools"
324
+ for tok in "${tokens[@]}"; do
325
+ case "$tok" in
326
+ Agent) mapped=task ;;
327
+ Bash) mapped=bash ;;
328
+ Edit|MultiEdit) mapped=edit ;;
329
+ Glob) mapped=glob ;;
330
+ Grep) mapped="grep" ;;
331
+ Read|WebFetch) mapped="read" ;;
332
+ Write) mapped="write" ;;
333
+ TodoWrite) mapped=todo ;;
334
+ WebSearch) mapped=web_search ;;
335
+ LS|NotebookRead|Skill|KillShell|BashOutput) continue ;;
336
+ *) mapped="$tok" ;;
337
+ esac
338
+ case "$seen" in *",$mapped,"*) continue ;; esac
339
+ seen="$seen$mapped,"
340
+ result="$result,$mapped"
341
+ done
342
+ echo "${result#,}"
343
+ }
344
+
345
+ pipe_run_omp() {
346
+ # $1 = agent label for metrics, $2 = skill invocation (e.g. "/review-mr"),
347
+ # $3 = allowed tools in Claude Code naming (optional, translated
348
+ # internally), $4 = model for the main loop (optional, e.g.
349
+ # $PIPE_MODEL_REVIEW; subagents keep the models from their own
350
+ # frontmatter), stdin = extra prompt (optional, usually empty).
351
+ local label="$1" skill="$2" tools="${3:-Agent,Read,Write,Edit,Glob,Grep,Bash}" model="${4:-}"
352
+ local omp_tools; omp_tools=$(pipe_translate_tools_to_omp "$tools")
353
+ local stdin_file; stdin_file=$(mktemp)
354
+ cat > "$stdin_file" || true
355
+
356
+ if [ "$(id -u)" -eq 0 ]; then
357
+ local runuser="pipelinebot"
358
+ id "$runuser" &>/dev/null || useradd -m -s /bin/bash "$runuser"
359
+ chown -R "$runuser:$runuser" "$PWD" "$PIPE_CONTEXT_DIR" "$stdin_file" 2>/dev/null || true
360
+ local runner; runner=$(mktemp)
361
+ {
362
+ printf '#!/bin/bash\nset -e\nexport HOME=/home/%s\n' "$runuser"
363
+ # Re-export the runtime environment the skill and usage-capture need.
364
+ local v
365
+ for v in $(compgen -v | grep -E '^(PIPE_|CI_|GH_|GITHUB_|OPENROUTER_)'); do
366
+ pipe_agent_secret_var "$v" && continue
367
+ printf 'export %s=%q\n' "$v" "${!v}"
368
+ done
369
+ # `su -m` preserves the parent environment, so explicitly remove every
370
+ # detected credential after exporting the safe runtime values above.
371
+ for v in $(compgen -e); do
372
+ pipe_agent_secret_var "$v" && printf 'unset %q\n' "$v"
373
+ done
374
+ printf 'cd %q\n' "$PWD"
375
+ printf 'source %q\n' "$SCRIPT_LIB_DIR/usage-capture-omp.sh"
376
+ printf 'export PIPE_CAPTURE_MODEL=%q\n' "$model"
377
+ if [ -n "$model" ]; then
378
+ printf 'capture_omp %q -- --yolo --no-session --no-title --model %q --tools %q -p %q < %q\n' \
379
+ "$label" "$model" "$omp_tools" "$skill" "$stdin_file"
380
+ else
381
+ printf 'capture_omp %q -- --yolo --no-session --no-title --tools %q -p %q < %q\n' \
382
+ "$label" "$omp_tools" "$skill" "$stdin_file"
383
+ fi
384
+ } > "$runner"
385
+ chmod +x "$runner"
386
+ chown "$runuser:$runuser" "$runner"
387
+ su -m "$runuser" -s /bin/bash "$runner" || true
388
+ chown -R root:root "$PWD" 2>/dev/null || true
389
+ rm -f "$runner"
390
+ else
391
+ (
392
+ pipe_scrub_agent_secrets
393
+ export PIPE_CAPTURE_MODEL="$model"
394
+ if [ -n "$model" ]; then
395
+ capture_omp "$label" -- --yolo --no-session --no-title --model "$model" --tools "$omp_tools" -p "$skill" < "$stdin_file"
396
+ else
397
+ capture_omp "$label" -- --yolo --no-session --no-title --tools "$omp_tools" -p "$skill" < "$stdin_file"
398
+ fi
399
+ ) || true
400
+ fi
401
+ rm -f "$stdin_file"
402
+ }
403
+
404
+ # --- Harness dispatch ---------------------------------------------------
405
+ # Single entry point every job script calls. Same signature/contract as
406
+ # pipe_run_claude/pipe_run_omp themselves: $1 label, $2 skill, $3 tools
407
+ # (optional, Claude Code naming, translated internally when the target is
408
+ # omp), $4 model (optional).
409
+
410
+ pipe_run_agent() {
411
+ case "${PIPE_HARNESS:-claude}" in
412
+ omp) pipe_run_omp "$@" ;;
413
+ *) pipe_run_claude "$@" ;;
414
+ esac
415
+ }
416
+
216
417
  # --- dotenv -----------------------------------------------------------------
217
418
 
218
419
  # Load KEY=VALUE lines from a marker file into the current shell.
@@ -240,23 +441,24 @@ pipe_get_env() {
240
441
  pipe_apply_escalation() {
241
442
  local pm_env="$PIPE_CONTEXT_DIR/postmortem.env"
242
443
  local pm_body="$PIPE_CONTEXT_DIR/postmortem.md"
243
- local add remove category diag
444
+ local add remove category comment_file
244
445
  add=$(pipe_get_env "$pm_env" ADD_LABELS); add=${add:-$PIPE_LABEL_STUCK}
245
446
  remove=$(pipe_get_env "$pm_env" REMOVE_LABELS); remove=${remove:-$PIPE_LABEL_WIP}
246
447
  category=$(pipe_get_env "$pm_env" FAILURE_CATEGORY); category=${category:-${1:-other}}
247
448
 
248
449
  platform_set_labels "$add" "$remove"
249
450
 
250
- if [ -s "$pm_body" ]; then
251
- diag=$(cat "$pm_body")
252
- else
253
- diag="Automated fixes were exhausted after $PIPE_FIX_LOOP_CAP attempts. A human needs to take over."
254
- fi
255
- platform_post_comment "🤖 **Auto-fix stuck after $PIPE_FIX_LOOP_CAP attempts. Diagnostic analysis:**
256
-
257
- $diag
258
-
259
- Human intervention needed."
451
+ comment_file="$PIPE_CONTEXT_DIR/auto-fix-stuck.md"
452
+ {
453
+ printf '🤖 **Auto-fix stuck after %s attempts. Diagnostic analysis:**\n\n' "$PIPE_FIX_LOOP_CAP"
454
+ if [ -s "$pm_body" ]; then
455
+ cat "$pm_body"
456
+ else
457
+ printf 'Automated fixes were exhausted after %s attempts. A human needs to take over.' "$PIPE_FIX_LOOP_CAP"
458
+ fi
459
+ printf '\n\nHuman intervention needed.\n'
460
+ } > "$comment_file"
461
+ platform_post_comment_file "$comment_file"
260
462
 
261
463
  pipe_metric_event postmortem dev_stuck \
262
464
  "$(jq -n --arg c "$category" --arg ctx "${2:-unknown}" '{failure_category: $c, context: $ctx}')"