
Monitor Ci
- 1.6k installs
- 27 repo stars
- Updated July 23, 2026
- nrwl/nx-ai-agents-config
An orchestrator skill that spawns subagents to monitor Nx Cloud CI pipeline status, runs deterministic decision scripts to classify failures and recommend fixes, and applies verified fixes via MCP tools or local git oper
About
Monitor CI is an orchestrator skill that watches Nx Cloud CI pipeline status in real time and coordinates self-healing fix application. Developers invoke it when they need to track CI status, apply suggested fixes, or verify fixes locally before pushing. The skill spawns subagents to fetch CI state via MCP tools, runs deterministic decision scripts to classify failures and recommend actions, and handles multi-step flows including local verification, fix application, environment reruns, and throttle management. It prevents races with self-healing by centralizing polling and fix decisions, avoiding native CI provider CLIs that bypass Nx Cloud's automatic repair system.
- Polls Nx Cloud CI state and decision-script-driven action routing (poll, wait, done)
- Applies fixes via MCP or locally with git stage/commit safety gates
- Local verification loop for unverified tasks before MCP apply
- Environment rerun and throttle handling with budget tracking
- Prevents race conditions and context waste vs. standalone CI polling
Monitor Ci by the numbers
- 1,614 all-time installs (skills.sh)
- +44 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #134 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
monitor-ci capabilities & compatibility
- Capabilities
- real time ci status polling via nx cloud mcp · deterministic decision script driven action rout · automated or prompted self healing fix applicati · local task verification and fix enhancement · environment rerun and throttle handling · session state persistence and resume · git stage/commit safety gates
- Works with
- github
- Use cases
- ci cd · debugging · testing
- Platforms
- macOS · Linux · Windows
- Runs
- Local or remote
- Pricing
- Free
npx skills add https://github.com/nrwl/nx-ai-agents-config --skill monitor-ciAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.6k |
|---|---|
| repo stars | ★ 27 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 23, 2026 |
| Repository | nrwl/nx-ai-agents-config ↗ |
What it does
Monitor Nx Cloud CI pipeline executions and apply self-healing fixes automatically or on-demand.
Who is it for?
Tracking Nx Cloud CI pipeline status, applying self-healing fixes, verifying fixes locally, managing environment reruns, and coordinating multi-step CI recovery workflows.
Skip if: One-time CI status checks, native CI provider operations (GitHub Actions, GitLab CI), canceling/destructive CI operations, or standalone custom polling scripts.
When should I use this skill?
User says monitor ci, watch ci, ci monitor, watch ci for this branch, track ci, check ci status, or needs self-healing CI fix application.
What you get
Automated, safe CI monitoring and fix application that centralizes polling decisions, prevents race conditions, and integrates Nx Cloud self-healing without requiring native CI provider CLIs.
- Real-time CI pipeline status polling and classification
- Automated or prompted fix application via MCP
- Local verification and enhancement of fixes
By the numbers
- Default max 10 agent-initiated CI Attempt cycles per session
- Default 120-minute timeout for entire monitoring session
- Default 10-minute wait for new CI Attempt after action
Files
Monitor CI Command
You are the orchestrator for monitoring Nx Cloud CI pipeline executions and handling self-healing fixes. You spawn subagents to interact with Nx Cloud, run deterministic decision scripts, and take action based on the results.
Context
- Current Branch: !
git branch --show-current - Current Commit: !
git rev-parse --short HEAD - Remote Status: !
git status -sb | head -1
User Instructions
$ARGUMENTS
Important: If user provides specific instructions, respect them over default behaviors described below.
Configuration Defaults
| Setting | Default | Description |
|---|---|---|
--max-cycles | 10 | Maximum agent-initiated CI Attempt cycles before timeout |
--timeout | 120 | Maximum duration in minutes |
--verbosity | medium | Output level: minimal, medium, verbose |
--branch | (auto-detect) | Branch to monitor |
--fresh | false | Ignore previous context, start fresh |
--auto-fix-workflow | false | Attempt common fixes for pre-CI-Attempt failures (e.g., lockfile updates) |
--new-cipe-timeout | 10 | Minutes to wait for new CI Attempt after action |
--local-verify-attempts | 3 | Max local verification + enhance cycles before pushing to CI |
Parse any overrides from $ARGUMENTS and merge with defaults.
Nx Cloud Connection Check
Before starting the monitoring loop, verify the workspace is connected to Nx Cloud. Without this connection, no CI data is available and the entire skill is inoperable.
Step 0: Verify Nx Cloud Connection
1. Check `nx.json` at workspace root for nxCloudId or nxCloudAccessToken 2. If `nx.json` missing OR neither property exists → exit with:
Nx Cloud not connected. Unlock 70% faster CI and auto-fix broken PRs with https://nx.dev/nx-cloud3. If connected → continue to main loop
Architecture Overview
1. This skill (orchestrator): spawns subagents, runs scripts, prints status, does local coding work 2. ci-monitor-subagent (haiku): calls one MCP tool (ci_information or update_self_healing_fix), returns structured result, exits 3. ci-poll-decide.mjs (deterministic script): takes ci_information result + state, returns action + status message 4. ci-state-update.mjs (deterministic script): manages budget gates, post-action state transitions, and cycle classification
Status Reporting
The decision script handles message formatting based on verbosity. When printing messages to the user:
- Prepend
[monitor-ci]to every message from the script'smessagefield - For your own action messages (e.g. "Applying fix via MCP..."), also prepend
[monitor-ci]
Anti-Patterns
These behaviors cause real problems — racing with self-healing, losing CI progress, or wasting context:
| Anti-Pattern | Why It's Bad |
|---|---|
Using CI provider CLIs with --watch flags (e.g., gh pr checks --watch, glab ci status -w) | Bypasses Nx Cloud self-healing entirely |
| Writing custom CI polling scripts | Unreliable, pollutes context, no self-healing |
| Cancelling CI workflows/pipelines | Destructive, loses CI progress |
| Running CI checks on main agent | Wastes main agent context tokens |
| Independently analyzing/fixing CI failures while polling | Races with self-healing, causes duplicate fixes and confused state |
If this skill fails to activate, the fallback is:
1. Use CI provider CLI for a one-time, read-only status check (single call, no watch/polling flags) 2. Immediately delegate to this skill with gathered context 3. Do not continue polling on main agent — it wastes context tokens and bypasses self-healing
Session Context Behavior
If the user previously ran /monitor-ci in this session, you may have prior state (poll counts, last CI Attempt URL, etc.). Resume from that state unless --fresh is set, in which case discard it and start from Step 1.
MCP Tool Reference
The ci_information and update_self_healing_fix tools are called via the ci-monitor-subagent, not directly from the orchestrator. Calling MCP tools directly wastes main agent context with large response payloads. The field sets below are for composing subagent prompts (see Step 2a).
Three field sets control polling efficiency — use the lightest set that gives you what you need:
WAIT_FIELDS: 'cipeUrl,commitSha,cipeStatus'
LIGHT_FIELDS: 'cipeStatus,cipeUrl,branch,commitSha,selfHealingStatus,verificationStatus,userAction,failedTaskIds,verifiedTaskIds,selfHealingEnabled,failureClassification,couldAutoApplyTasks,autoApplySkipped,autoApplySkipReason,shortLink,confidence,confidenceReasoning,hints,selfHealingSkippedReason,selfHealingSkipMessage'
HEAVY_FIELDS: 'taskOutputSummary,suggestedFix,suggestedFixReasoning,suggestedFixDescription'The ci_information tool accepts branch (optional, defaults to current git branch), select (comma-separated field names), and pageToken (0-based pagination for long strings).
The update_self_healing_fix tool accepts a shortLink and an action: APPLY, REJECT, or RERUN_ENVIRONMENT_STATE.
Default Behaviors by Status
The decision script returns one of the following statuses. This table defines the default behavior for each. User instructions can override any of these.
Simple exits — just report and exit:
| Status | Default Behavior |
|---|---|
ci_success | Exit with success |
cipe_canceled | Exit, CI was canceled |
cipe_timed_out | Exit, CI timed out |
polling_timeout | Exit, polling timeout reached |
circuit_breaker | Exit, no progress after 13 consecutive polls |
environment_rerun_cap | Exit, environment reruns exhausted |
fix_auto_applying | Self-healing is handling it — just record last_cipe_url, enter wait mode. No MCP call or local git ops needed. |
error | Wait 60s and loop |
Statuses requiring action — when handling these in Step 3, read references/fix-flows.md for the detailed flow:
| Status | Summary |
|---|---|
fix_auto_apply_skipped | Fix verified but auto-apply skipped (e.g., loop prevention). Inform user, offer manual apply. |
fix_apply_ready | Fix verified (all tasks or e2e-only). Apply via MCP. |
fix_needs_local_verify | Fix has unverified non-e2e tasks. Run locally, then apply or enhance. |
fix_needs_review | Fix verification failed/not attempted. Analyze and decide. |
fix_failed | Self-healing failed. Fetch heavy data, attempt local fix (gate check first). |
no_fix | No fix available. Fetch heavy data, attempt local fix (gate check first) or exit. |
environment_issue | Request environment rerun via MCP (gate check first). |
self_healing_throttled | Reject old fixes, attempt local fix. |
no_new_cipe | CI Attempt never spawned. Auto-fix workflow or exit with guidance. |
cipe_no_tasks | CI failed with no tasks. Retry once with empty commit. |
Key rules (always apply):
- Git safety: Stage specific files by name —
git add -Aorgit add .risks committing the user's unrelated work-in-progress or secrets - Environment failures (OOM, command not found, permission denied): bail immediately. These aren't code bugs, so spending local-fix budget on them is wasteful
- Gate check: Run
ci-state-update.mjs gatebefore local fix attempts — if budget exhausted, print message and exit
Main Loop
Step 1: Initialize Tracking
cycle_count = 0 # Only incremented for agent-initiated cycles (counted against --max-cycles)
start_time = now()
no_progress_count = 0
local_verify_count = 0
env_rerun_count = 0
last_cipe_url = null
expected_commit_sha = null
agent_triggered = false # Set true after monitor takes an action that triggers new CI Attempt
poll_count = 0
wait_mode = false
prev_status = null
prev_cipe_status = null
prev_sh_status = null
prev_verification_status = null
prev_failure_classification = nullStep 2: Polling Loop
Repeat until done:
2a. Spawn subagent (FETCH_STATUS)
Determine select fields based on mode:
- Wait mode: use WAIT_FIELDS (
cipeUrl,commitSha,cipeStatus) - Normal mode (first poll or after newCipeDetected): use LIGHT_FIELDS
Task(
agent: "ci-monitor-subagent",
model: haiku,
prompt: "FETCH_STATUS for branch '<branch>'.
select: '<fields>'"
)The subagent calls ci_information and returns a JSON object with the requested fields. This is a foreground call — wait for the result.
2b. Run decision script
node <skill_dir>/scripts/ci-poll-decide.mjs '<subagent_result_json>' <poll_count> <verbosity> \
[--wait-mode] \
[--prev-cipe-url <last_cipe_url>] \
[--expected-sha <expected_commit_sha>] \
[--prev-status <prev_status>] \
[--timeout <timeout_seconds>] \
[--new-cipe-timeout <new_cipe_timeout_seconds>] \
[--env-rerun-count <env_rerun_count>] \
[--no-progress-count <no_progress_count>] \
[--prev-cipe-status <prev_cipe_status>] \
[--prev-sh-status <prev_sh_status>] \
[--prev-verification-status <prev_verification_status>] \
[--prev-failure-classification <prev_failure_classification>]The script outputs a single JSON line: { action, code, message, delay?, noProgressCount, envRerunCount, fields?, newCipeDetected?, verifiableTaskIds? }
2c. Process script output
Parse the JSON output and update tracking state:
no_progress_count = output.noProgressCountenv_rerun_count = output.envRerunCountprev_cipe_status = subagent_result.cipeStatusprev_sh_status = subagent_result.selfHealingStatusprev_verification_status = subagent_result.verificationStatusprev_failure_classification = subagent_result.failureClassificationprev_status = output.action + ":" + (output.code || subagent_result.cipeStatus)poll_count++
Based on action:
- `action == "poll"`: Print
output.message, sleepoutput.delayseconds, go to 2a - If
output.newCipeDetected: clear wait mode, resetwait_mode = false - `action == "wait"`: Print
output.message, sleepoutput.delayseconds, go to 2a - `action == "done"`: Proceed to Step 3 with
output.code
Step 3: Handle Actionable Status
When decision script returns action == "done":
1. Run cycle-check (Step 4) before handling the code 2. Check the returned code 3. Look up default behavior in the table above 4. Check if user instructions override the default 5. Execute the appropriate action 6. If action expects new CI Attempt, update tracking (see Step 3a) 7. If action results in looping, go to Step 2
Spawning subagents for actions
Several statuses require fetching heavy data or calling MCP:
- fix_apply_ready: Spawn UPDATE_FIX subagent with
APPLY - fix_needs_local_verify: Spawn FETCH_HEAVY subagent for fix details before local verification
- fix_needs_review: Spawn FETCH_HEAVY subagent → get
suggestedFixDescription,suggestedFixSummary,taskFailureSummaries - fix_failed / no_fix: Spawn FETCH_HEAVY subagent → get
taskFailureSummariesfor local fix context - environment_issue: Spawn UPDATE_FIX subagent with
RERUN_ENVIRONMENT_STATE - self_healing_throttled: Spawn FETCH_HEAVY subagent → get
selfHealingSkipMessage; then FETCH_THROTTLE_INFO + UPDATE_FIX for each old fix
Step 3a: Track State for New-CI-Attempt Detection
After actions that should trigger a new CI Attempt, run:
node <skill_dir>/scripts/ci-state-update.mjs post-action \
--action <type> \
--cipe-url <current_cipe_url> \
--commit-sha <git_rev_parse_HEAD>Action types: fix-auto-applying, apply-mcp, apply-local-push, reject-fix-push, local-fix-push, env-rerun, auto-fix-push, empty-commit-push
The script returns { waitMode, pollCount, lastCipeUrl, expectedCommitSha, agentTriggered }. Update all tracking state from the output, then go to Step 2.
Step 4: Cycle Classification and Progress Tracking
When the decision script returns action == "done", run cycle-check before handling the code:
node <skill_dir>/scripts/ci-state-update.mjs cycle-check \
--code <code> \
[--agent-triggered] \
--cycle-count <cycle_count> --max-cycles <max_cycles> \
--env-rerun-count <env_rerun_count>The script returns { cycleCount, agentTriggered, envRerunCount, approachingLimit, message }. Update tracking state from the output.
- If
approachingLimit→ ask user whether to continue (with 5 or 10 more cycles) or stop monitoring - If previous cycle was NOT agent-triggered (human pushed), log that human-initiated push was detected
Progress Tracking
no_progress_count, circuit breaker (5 polls), and backoff reset are handled by ci-poll-decide.mjs (progress = any change in cipeStatus, selfHealingStatus, verificationStatus, or failureClassification)env_rerun_countreset on non-environment status is handled by ci-state-update.mjs cycle-check- On new CI Attempt detected (poll script returns
newCipeDetected) → resetlocal_verify_count = 0,env_rerun_count = 0
Error Handling
| Error | Action |
|---|---|
| Git rebase conflict | Report to user, exit |
nx-cloud apply-locally fails | Reject fix via MCP (action: "REJECT"), then attempt manual patch (Reject + Fix From Scratch Flow) or exit |
| MCP tool error | Retry once, if fails report to user |
| Subagent spawn failure | Retry once, if fails exit with error |
| Decision script error | Treat as error status, increment no_progress_count |
| No new CI Attempt detected | If --auto-fix-workflow, try lockfile update; otherwise report to user with guidance |
| Lockfile auto-fix fails | Report to user, exit with guidance to check CI logs |
User Instruction Examples
Users can override default behaviors:
| Instruction | Effect |
|---|---|
| "never auto-apply" | Always prompt before applying any fix |
| "always ask before git push" | Prompt before each push |
| "reject any fix for e2e tasks" | Auto-reject if failedTaskIds contains e2e |
| "apply all fixes regardless of verification" | Skip verification check, apply everything |
| "if confidence < 70, reject" | Check confidence field before applying |
| "run 'nx affected -t typecheck' before applying" | Add local verification step |
| "auto-fix workflow failures" | Attempt lockfile updates on pre-CI-Attempt failures |
| "wait 45 min for new CI Attempt" | Override new-CI-Attempt timeout (default: 10 min) |
Detailed Status Handling & Fix Flows
Status Handling by Code
fix_auto_apply_skipped
The script returns autoApplySkipReason in its output.
1. Report the skip reason to the user (e.g., "Auto-apply was skipped because the previous CI pipeline execution was triggered by Nx Cloud") 2. Offer to apply the fix manually — spawn UPDATE_FIX subagent with APPLY if user agrees 3. Record last_cipe_url, enter wait mode
fix_apply_ready
- Spawn UPDATE_FIX subagent with
APPLY - Record
last_cipe_url, enter wait mode
fix_needs_local_verify
The script returns verifiableTaskIds in its output.
1. Detect package manager: pnpm-lock.yaml → pnpm nx, yarn.lock → yarn nx, otherwise npx nx 2. Run verifiable tasks in parallel — spawn general subagents for each task 3. If all pass → spawn UPDATE_FIX subagent with APPLY, enter wait mode 4. If any fail → Apply Locally + Enhance Flow (see below)
fix_needs_review
Spawn FETCH_HEAVY subagent, then analyze fix content (suggestedFixDescription, suggestedFixSummary, taskFailureSummaries):
- If fix looks correct → apply via MCP
- If fix needs enhancement → Apply Locally + Enhance Flow
- If fix is wrong → run
ci-state-update.mjs gate --gate-type local-fix. If not allowed, print message and exit. Otherwise → Reject + Fix From Scratch Flow
fix_failed / no_fix
Spawn FETCH_HEAVY subagent for taskFailureSummaries. Run ci-state-update.mjs gate --gate-type local-fix — if not allowed, print message and exit. Otherwise attempt local fix (counter already incremented by gate). If successful → commit, push, enter wait mode. If not → exit with failure.
environment_issue
1. Run ci-state-update.mjs gate --gate-type env-rerun. If not allowed, print message and exit. 2. Spawn UPDATE_FIX subagent with RERUN_ENVIRONMENT_STATE 3. Enter wait mode with last_cipe_url set
self_healing_throttled
Spawn FETCH_HEAVY subagent for selfHealingSkipMessage.
1. Parse throttle message for CI Attempt URLs (regex: /cipes/{id}) 2. Reject previous fixes — for each URL: spawn FETCH_THROTTLE_INFO to get shortLink, then UPDATE_FIX with REJECT 3. Attempt local fix: Run ci-state-update.mjs gate --gate-type local-fix. If not allowed → skip to step 4. Otherwise use failedTaskIds and taskFailureSummaries for context. 4. Fallback if local fix not possible or budget exhausted: push empty commit (git commit --allow-empty -m "ci: rerun after rejecting throttled fixes"), enter wait mode
no_new_cipe
1. Report to user: no CI attempt found, suggest checking CI provider 2. If --auto-fix-workflow: detect package manager, run install, commit lockfile if changed, enter wait mode 3. Otherwise: exit with guidance
cipe_no_tasks
1. Report to user: CI failed with no tasks recorded 2. Retry: git commit --allow-empty -m "chore: retry ci [monitor-ci]" + push, enter wait mode 3. If retry also returns cipe_no_tasks: exit with failure
Fix Action Flows
Apply via MCP
Spawn UPDATE_FIX subagent with APPLY. New CI Attempt spawns automatically. No local git ops.
Apply Locally + Enhance Flow
1. nx-cloud apply-locally <shortLink> (sets state to APPLIED_LOCALLY) 2. Enhance code to fix failing tasks 3. Run failing tasks to verify 4. If still failing → run ci-state-update.mjs gate --gate-type local-fix. If not allowed, commit current state and push (let CI be final judge). Otherwise loop back to enhance. 5. If passing → commit and push, enter wait mode
Reject + Fix From Scratch Flow
1. Run ci-state-update.mjs gate --gate-type local-fix. If not allowed, print message and exit. 2. Spawn UPDATE_FIX subagent with REJECT 3. Fix from scratch locally 4. Commit and push, enter wait mode
Environment vs Code Failure Recognition
When any local fix path runs a task and it fails, assess whether the failure is a code issue or an environment/tooling issue before running the gate script.
Indicators of environment/tooling failures (non-exhaustive): command not found / binary missing, OOM / heap allocation failures, permission denied, network timeouts / DNS failures, missing system libraries, Docker/container issues, disk space exhaustion.
When detected → bail immediately without running gate (no budget consumed). Report that the failure is an environment/tooling issue, not a code bug.
Code failures (compilation errors, test assertion failures, lint violations, type errors) are genuine candidates for local fix attempts and proceed normally through the gate.
Git Safety
- Stage specific files by name —
git add -Aorgit add .risks committing the user's unrelated work-in-progress or secrets
Commit Message Format
git commit -m "fix(<projects>): <brief description>
Failed tasks: <taskId1>, <taskId2>
Local verification: passed|enhanced|failed-pushing-to-ci"#!/usr/bin/env node
/**
* CI Poll Decision Script
*
* Deterministic decision engine for CI monitoring.
* Takes ci_information JSON + state args, outputs a single JSON action line.
*
* Architecture:
* classify() — pure decision tree, returns { action, code, extra? }
* buildOutput() — maps classification to full output with messages, delays, counters
*
* Usage:
* node ci-poll-decide.mjs '<ci_info_json>' <poll_count> <verbosity> \
* [--wait-mode] [--prev-cipe-url <url>] [--expected-sha <sha>] \
* [--prev-status <status>] [--timeout <seconds>] [--new-cipe-timeout <seconds>] \
* [--env-rerun-count <n>] [--no-progress-count <n>] \
* [--prev-cipe-status <status>] [--prev-sh-status <status>] \
* [--prev-verification-status <status>] [--prev-failure-classification <status>]
*/
// --- Arg parsing ---
const args = process.argv.slice(2);
const ciInfoJson = args[0];
const pollCount = parseInt(args[1], 10) || 0;
const verbosity = args[2] || 'medium';
function getFlag(name) {
return args.includes(name);
}
function getArg(name) {
const idx = args.indexOf(name);
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null;
}
const waitMode = getFlag('--wait-mode');
const prevCipeUrl = getArg('--prev-cipe-url');
const expectedSha = getArg('--expected-sha');
const prevStatus = getArg('--prev-status');
const timeoutSeconds = parseInt(getArg('--timeout') || '0', 10);
const newCipeTimeoutSeconds = parseInt(getArg('--new-cipe-timeout') || '0', 10);
const envRerunCount = parseInt(getArg('--env-rerun-count') || '0', 10);
const inputNoProgressCount = parseInt(getArg('--no-progress-count') || '0', 10);
const prevCipeStatus = getArg('--prev-cipe-status');
const prevShStatus = getArg('--prev-sh-status');
const prevVerificationStatus = getArg('--prev-verification-status');
const prevFailureClassification = getArg('--prev-failure-classification');
// --- Parse CI info ---
let ci;
try {
ci = JSON.parse(ciInfoJson);
} catch {
console.log(
JSON.stringify({
action: 'done',
code: 'error',
message: 'Failed to parse ci_information JSON',
noProgressCount: inputNoProgressCount + 1,
envRerunCount,
})
);
process.exit(0);
}
const {
cipeStatus,
selfHealingStatus,
verificationStatus,
selfHealingEnabled,
selfHealingSkippedReason,
failureClassification: rawFailureClassification,
failedTaskIds = [],
verifiedTaskIds = [],
couldAutoApplyTasks,
autoApplySkipped,
autoApplySkipReason,
userAction,
cipeUrl,
commitSha,
} = ci;
const failureClassification = rawFailureClassification?.toLowerCase() ?? null;
// --- Helpers ---
function categorizeTasks() {
const verifiedSet = new Set(verifiedTaskIds);
const unverified = failedTaskIds.filter((t) => !verifiedSet.has(t));
if (unverified.length === 0) return { category: 'all_verified' };
const e2e = unverified.filter((t) => {
const parts = t.split(':');
return parts.length >= 2 && parts[1].includes('e2e');
});
if (e2e.length === unverified.length) return { category: 'e2e_only' };
const verifiable = unverified.filter((t) => {
const parts = t.split(':');
return !(parts.length >= 2 && parts[1].includes('e2e'));
});
return { category: 'needs_local_verify', verifiableTaskIds: verifiable };
}
function backoff(count) {
const delays = [60, 90, 120, 180];
return delays[Math.min(count, delays.length - 1)];
}
function hasStateChanged() {
if (prevCipeStatus && cipeStatus !== prevCipeStatus) return true;
if (prevShStatus && selfHealingStatus !== prevShStatus) return true;
if (prevVerificationStatus && verificationStatus !== prevVerificationStatus)
return true;
if (
prevFailureClassification &&
failureClassification !== prevFailureClassification
)
return true;
return false;
}
function isTimedOut() {
if (timeoutSeconds <= 0) return false;
const avgDelay = pollCount === 0 ? 0 : backoff(Math.floor(pollCount / 2));
return pollCount * avgDelay >= timeoutSeconds;
}
function isWaitTimedOut() {
if (newCipeTimeoutSeconds <= 0) return false;
return pollCount * 30 >= newCipeTimeoutSeconds;
}
function isNewCipe() {
return (
(prevCipeUrl && cipeUrl && cipeUrl !== prevCipeUrl) ||
(expectedSha && commitSha && commitSha === expectedSha)
);
}
// ============================================================
// classify() — pure decision tree
//
// Returns: { action: 'poll'|'wait'|'done', code: string, extra? }
//
// Decision priority (top wins):
// WAIT MODE:
// 1. new CI Attempt detected → poll (new_cipe_detected)
// 2. wait timed out → done (no_new_cipe)
// 3. still waiting → wait (waiting_for_cipe)
// NORMAL MODE:
// 4. polling timeout → done (polling_timeout)
// 5. circuit breaker (13 polls) → done (circuit_breaker)
// 6. CI succeeded → done (ci_success)
// 7. CI canceled → done (cipe_canceled)
// 8. CI timed out → done (cipe_timed_out)
// 9. CI failed, no tasks recorded → done (cipe_no_tasks)
// 10. environment failure → done (environment_rerun_cap | environment_issue)
// 11. self-healing throttled → done (self_healing_throttled)
// 12. CI in progress / not started → poll (ci_running)
// 13. self-healing in progress → poll (sh_running)
// 14. flaky task auto-rerun → poll (flaky_rerun)
// 15. fix auto-applied → poll (fix_auto_applied)
// 16. auto-apply: skipped → done (fix_auto_apply_skipped)
// 17. auto-apply: verification pending→ poll (verification_pending)
// 18. auto-apply: verified → done (fix_auto_applying)
// 19. fix: verification failed/none → done (fix_needs_review)
// 20. fix: all/e2e verified → done (fix_apply_ready)
// 21. fix: needs local verify → done (fix_needs_local_verify)
// 22. self-healing failed → done (fix_failed)
// 23. no fix available → done (no_fix)
// 24. fallback → poll (fallback)
// ============================================================
function classify() {
// --- Wait mode ---
if (waitMode) {
if (isNewCipe()) return { action: 'poll', code: 'new_cipe_detected' };
if (isWaitTimedOut()) return { action: 'done', code: 'no_new_cipe' };
return { action: 'wait', code: 'waiting_for_cipe' };
}
// --- Guards ---
if (isTimedOut()) return { action: 'done', code: 'polling_timeout' };
if (noProgressCount >= 13) return { action: 'done', code: 'circuit_breaker' };
// --- Terminal CI states ---
if (cipeStatus === 'SUCCEEDED') return { action: 'done', code: 'ci_success' };
if (cipeStatus === 'CANCELED')
return { action: 'done', code: 'cipe_canceled' };
if (cipeStatus === 'TIMED_OUT')
return { action: 'done', code: 'cipe_timed_out' };
// --- CI failed, no tasks ---
if (
cipeStatus === 'FAILED' &&
failedTaskIds.length === 0 &&
selfHealingStatus == null
)
return { action: 'done', code: 'cipe_no_tasks' };
// --- Environment failure ---
if (failureClassification === 'environment_state') {
if (envRerunCount >= 2)
return { action: 'done', code: 'environment_rerun_cap' };
return { action: 'done', code: 'environment_issue' };
}
// --- Throttled ---
if (selfHealingSkippedReason === 'THROTTLED')
return { action: 'done', code: 'self_healing_throttled' };
// --- Still running: CI ---
if (cipeStatus === 'IN_PROGRESS' || cipeStatus === 'NOT_STARTED')
return { action: 'poll', code: 'ci_running' };
// --- Still running: self-healing ---
if (
(selfHealingStatus === 'IN_PROGRESS' ||
selfHealingStatus === 'NOT_STARTED') &&
!selfHealingSkippedReason
)
return { action: 'poll', code: 'sh_running' };
// --- Still running: flaky rerun ---
if (failureClassification === 'flaky_task')
return { action: 'poll', code: 'flaky_rerun' };
// --- Fix auto-applied, waiting for new CI Attempt ---
if (userAction === 'APPLIED_AUTOMATICALLY')
return { action: 'poll', code: 'fix_auto_applied' };
// --- Auto-apply path (couldAutoApplyTasks) ---
if (couldAutoApplyTasks === true) {
if (autoApplySkipped === true)
return {
action: 'done',
code: 'fix_auto_apply_skipped',
extra: { autoApplySkipReason },
};
if (
verificationStatus === 'NOT_STARTED' ||
verificationStatus === 'IN_PROGRESS'
)
return { action: 'poll', code: 'verification_pending' };
if (verificationStatus === 'COMPLETED')
return { action: 'done', code: 'fix_auto_applying' };
// verification FAILED or NOT_EXECUTABLE → falls through to fix_needs_review
}
// --- Fix available ---
if (selfHealingStatus === 'COMPLETED') {
if (
verificationStatus === 'FAILED' ||
verificationStatus === 'NOT_EXECUTABLE' ||
(couldAutoApplyTasks !== true && !verificationStatus)
)
return { action: 'done', code: 'fix_needs_review' };
const tasks = categorizeTasks();
if (tasks.category === 'all_verified' || tasks.category === 'e2e_only')
return { action: 'done', code: 'fix_apply_ready' };
return {
action: 'done',
code: 'fix_needs_local_verify',
extra: { verifiableTaskIds: tasks.verifiableTaskIds },
};
}
// --- Fix failed ---
if (selfHealingStatus === 'FAILED')
return { action: 'done', code: 'fix_failed' };
// --- No fix available ---
if (
cipeStatus === 'FAILED' &&
(selfHealingEnabled === false || selfHealingStatus === 'NOT_EXECUTABLE')
)
return { action: 'done', code: 'no_fix' };
// --- Fallback ---
return { action: 'poll', code: 'fallback' };
}
// ============================================================
// buildOutput() — maps classification to full JSON output
// ============================================================
// Message templates keyed by status or key
const messages = {
// wait mode
new_cipe_detected: () =>
`New CI Attempt detected! CI: ${cipeStatus || 'N/A'}`,
no_new_cipe: () =>
'New CI Attempt timeout exceeded. No new CI Attempt detected.',
waiting_for_cipe: () => 'Waiting for new CI Attempt...',
// guards
polling_timeout: () => 'Polling timeout exceeded.',
circuit_breaker: () => 'No progress after 13 consecutive polls. Stopping.',
// terminal
ci_success: () => 'CI passed successfully!',
cipe_canceled: () => 'CI Attempt was canceled.',
cipe_timed_out: () => 'CI Attempt timed out.',
cipe_no_tasks: () => 'CI failed but no Nx tasks were recorded.',
// environment
environment_rerun_cap: () => 'Environment rerun cap (2) exceeded. Bailing.',
environment_issue: () => 'CI: FAILED | Classification: ENVIRONMENT_STATE',
// throttled
self_healing_throttled: () =>
'Self-healing throttled \u2014 too many unapplied fixes.',
// polling
ci_running: () => `CI: ${cipeStatus}`,
sh_running: () => `CI: ${cipeStatus} | Self-healing: ${selfHealingStatus}`,
flaky_rerun: () =>
'CI: FAILED | Classification: FLAKY_TASK (auto-rerun in progress)',
fix_auto_applied: () =>
'CI: FAILED | Fix auto-applied, new CI Attempt spawning',
verification_pending: () =>
`CI: FAILED | Self-healing: COMPLETED | Verification: ${verificationStatus}`,
// actionable
fix_auto_applying: () => 'Fix verified! Auto-applying...',
fix_auto_apply_skipped: (extra) =>
`Fix verified but auto-apply was skipped. ${
extra?.autoApplySkipReason
? `Reason: ${extra.autoApplySkipReason}`
: 'Offer to apply manually.'
}`,
fix_needs_review: () =>
`Fix available but needs review. Verification: ${
verificationStatus || 'N/A'
}`,
fix_apply_ready: () => 'Fix available and verified. Ready to apply.',
fix_needs_local_verify: (extra) =>
`Fix available. ${extra.verifiableTaskIds.length} task(s) need local verification.`,
fix_failed: () => 'Self-healing failed to generate a fix.',
no_fix: () => 'CI failed, no fix available.',
// fallback
fallback: () =>
`CI: ${cipeStatus || 'N/A'} | Self-healing: ${
selfHealingStatus || 'N/A'
} | Verification: ${verificationStatus || 'N/A'}`,
};
// Codes where noProgressCount resets to 0 (genuine progress occurred)
const resetProgressCodes = new Set([
'ci_success',
'fix_auto_applying',
'fix_auto_apply_skipped',
'fix_needs_review',
'fix_apply_ready',
'fix_needs_local_verify',
]);
function formatMessage(msg) {
if (verbosity === 'minimal') {
const currentStatus = `${cipeStatus}|${selfHealingStatus}|${verificationStatus}`;
if (currentStatus === (prevStatus || '')) return null;
return msg;
}
if (verbosity === 'verbose') {
return [
`Poll #${pollCount + 1} | CI: ${cipeStatus || 'N/A'} | Self-healing: ${
selfHealingStatus || 'N/A'
} | Verification: ${verificationStatus || 'N/A'}`,
msg,
].join('\n');
}
return `Poll #${pollCount + 1} | ${msg}`;
}
function buildOutput(decision) {
const { action, code, extra } = decision;
// noProgressCount is already computed before classify() was called.
// Here we only handle the reset for "genuine progress" done-codes.
const msgFn = messages[code];
const rawMsg = msgFn ? msgFn(extra) : `Unknown: ${code}`;
const message = formatMessage(rawMsg);
const result = {
action,
code,
message,
noProgressCount: resetProgressCodes.has(code) ? 0 : noProgressCount,
envRerunCount,
};
// Add delay
if (action === 'wait') {
result.delay = 30;
} else if (action === 'poll') {
result.delay = code === 'new_cipe_detected' ? 60 : backoff(noProgressCount);
result.fields = 'light';
}
// Add extras
if (code === 'new_cipe_detected') result.newCipeDetected = true;
if (extra?.verifiableTaskIds)
result.verifiableTaskIds = extra.verifiableTaskIds;
if (extra?.autoApplySkipReason)
result.autoApplySkipReason = extra.autoApplySkipReason;
console.log(JSON.stringify(result));
}
// --- Run ---
// Compute noProgressCount from input. Single assignment, no mutation.
// Wait mode: reset on new cipe, otherwise unchanged (wait doesn't count as no-progress).
// Normal mode: reset on any state change, otherwise increment.
const noProgressCount = (() => {
if (waitMode) return isNewCipe() ? 0 : inputNoProgressCount;
if (isNewCipe() || hasStateChanged()) return 0;
return inputNoProgressCount + 1;
})();
buildOutput(classify());
#!/usr/bin/env node
/**
* CI State Update Script
*
* Deterministic state management for CI monitor actions.
* Three commands: gate, post-action, cycle-check.
*
* Usage:
* node ci-state-update.mjs gate --gate-type <local-fix|env-rerun> [counter args]
* node ci-state-update.mjs post-action --action <type> [--cipe-url <url>] [--commit-sha <sha>]
* node ci-state-update.mjs cycle-check --code <code> [--agent-triggered] [counter args]
*/
// --- Arg parsing ---
const args = process.argv.slice(2);
const command = args[0];
function getFlag(name) {
return args.includes(name);
}
function getArg(name) {
const idx = args.indexOf(name);
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null;
}
function output(result) {
console.log(JSON.stringify(result));
}
// --- gate ---
// Check if an action is allowed and return incremented counter.
// Called before any local fix attempt or environment rerun.
function gate() {
const gateType = getArg('--gate-type');
if (gateType === 'local-fix') {
const count = parseInt(getArg('--local-verify-count') || '0', 10);
const max = parseInt(getArg('--local-verify-attempts') || '3', 10);
if (count >= max) {
return output({
allowed: false,
localVerifyCount: count,
message: `Local fix budget exhausted (${count}/${max} attempts)`,
});
}
return output({
allowed: true,
localVerifyCount: count + 1,
message: null,
});
}
if (gateType === 'env-rerun') {
const count = parseInt(getArg('--env-rerun-count') || '0', 10);
if (count >= 2) {
return output({
allowed: false,
envRerunCount: count,
message: `Environment issue persists after ${count} reruns. Manual investigation needed.`,
});
}
return output({
allowed: true,
envRerunCount: count + 1,
message: null,
});
}
output({ allowed: false, message: `Unknown gate type: ${gateType}` });
}
// --- post-action ---
// Compute next state after an action is taken.
// Returns wait mode params and whether the action was agent-triggered.
function postAction() {
const action = getArg('--action');
const cipeUrl = getArg('--cipe-url');
const commitSha = getArg('--commit-sha');
// MCP-triggered or auto-applied: track by cipeUrl
const cipeUrlActions = ['fix-auto-applying', 'apply-mcp', 'env-rerun'];
// Local push: track by commitSha
const commitShaActions = [
'apply-local-push',
'reject-fix-push',
'local-fix-push',
'auto-fix-push',
'empty-commit-push',
];
const trackByCipeUrl = cipeUrlActions.includes(action);
const trackByCommitSha = commitShaActions.includes(action);
if (!trackByCipeUrl && !trackByCommitSha) {
return output({ error: `Unknown action: ${action}` });
}
// fix-auto-applying: self-healing did it, NOT the monitor
const agentTriggered = action !== 'fix-auto-applying';
output({
waitMode: true,
pollCount: 0,
lastCipeUrl: trackByCipeUrl ? cipeUrl : null,
expectedCommitSha: trackByCommitSha ? commitSha : null,
agentTriggered,
});
}
// --- cycle-check ---
// Cycle classification + counter resets when a new "done" code is received.
// Called at the start of handling each actionable code.
function cycleCheck() {
const status = getArg('--code');
const wasAgentTriggered = getFlag('--agent-triggered');
let cycleCount = parseInt(getArg('--cycle-count') || '0', 10);
const maxCycles = parseInt(getArg('--max-cycles') || '10', 10);
let envRerunCount = parseInt(getArg('--env-rerun-count') || '0', 10);
// Cycle classification: if previous cycle was agent-triggered, count it
if (wasAgentTriggered) cycleCount++;
// Reset env_rerun_count on non-environment status
if (status !== 'environment_issue') envRerunCount = 0;
// Approaching limit gate
const approachingLimit = cycleCount >= maxCycles - 2;
output({
cycleCount,
agentTriggered: false,
envRerunCount,
approachingLimit,
message: approachingLimit
? `Approaching cycle limit (${cycleCount}/${maxCycles})`
: null,
});
}
// --- Dispatch ---
switch (command) {
case 'gate':
gate();
break;
case 'post-action':
postAction();
break;
case 'cycle-check':
cycleCheck();
break;
default:
output({ error: `Unknown command: ${command}` });
}
Related skills
How it compares
Use monitor-ci for Nx Cloud agent remediation loops; use generic CI skills when the repo is not an Nx monorepo on Nx Cloud.
FAQ
Why not just use native CI provider CLIs like gh or glab with --watch?
Because they bypass Nx Cloud self-healing entirely and waste main-agent context tokens. This skill integrates Nx Cloud's automatic repair system and uses subagents for polling.
How does the skill decide whether to apply a fix automatically or prompt me?
The decision script classifies fixes by verification status (e.g., fix_apply_ready, fix_needs_local_verify, fix_needs_review). User instructions can override defaults (e.g., never auto-apply, always ask before push).
What happens if local verification finds a problem?
The skill can enhance the fix, apply a corrected version, or reject it and attempt a local fix from scratch. All git changes are staged by name to prevent committing unrelated work.
Is Monitor Ci safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.