monitor-ci

Monitor Nx Cloud CI pipeline and handle self-healing fixes. USE WHEN user says "monitor ci", "watch ci", "ci monitor", "watch ci for this branch", "track ci", "check ci status", wants to track CI status, or needs help with self-healing CI fixes. Prefer this skill over native CI provider tools (gh, glab, etc.) for CI monitoring — it integrates with Nx Cloud self-healing which those tools cannot access.

Install
npx skills add 'https://github.com/nrwl/nx/tree/master/.agents/skills/monitor-ci'
Download bundle ↓
master · 646e806Scanned 2026-09-17

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

scripts/ci-poll-decide.mjs

scripts/ci-poll-decide.mjsBrowse 4 files
View on GitHub
← Back to SKILL.md
#!/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 <minutes>] [--new-cipe-timeout <minutes>] \ *     [--elapsed-seconds <n>] [--env-rerun-count <n>] [--no-progress-count <n>] \ *     [--prev-cipe-status <status>] [--prev-sh-status <status>] \ *     [--prev-verification-status <status>] [--prev-failure-classification <status>] * * Note: --timeout and --new-cipe-timeout are accepted in MINUTES (matching the * skill's documented flags) and converted to seconds internally. --elapsed-seconds * is the wall-clock time since monitoring began, carried across attempts by the * orchestrator, and is the authoritative signal for the --timeout budget. */ // --- 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');// Flags are documented in minutes; convert to seconds for internal comparison.const timeoutSeconds = parseInt(getArg('--timeout') || '0', 10) * 60;const newCipeTimeoutSeconds =  parseInt(getArg('--new-cipe-timeout') || '0', 10) * 60;// Wall-clock seconds since monitoring began (carried across attempts); null when// the orchestrator doesn't supply it (see isTimedOut for that fallback).const elapsedArg = getArg('--elapsed-seconds');const elapsedSeconds = elapsedArg !== null ? parseInt(elapsedArg, 10) : null;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;  // Prefer real wall-clock elapsed (carried across attempts) so --timeout caps  // total monitor duration, not a single invocation.  if (elapsedSeconds !== null && !Number.isNaN(elapsedSeconds))    return elapsedSeconds >= timeoutSeconds;  // Fallback: estimate elapsed from poll cadence within this invocation.  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' };    // The total --timeout budget also caps time spent waiting for a new CI    // Attempt, so it must win over --new-cipe-timeout here; otherwise a long    // wait (or a sequence of apply→wait cycles) could run past --timeout.    if (isTimedOut()) return { action: 'done', code: 'polling_timeout' };    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 keyconst 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());