@polygraph/cursor-plugin 0.4.51

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.
@@ -0,0 +1,163 @@
1
+ import { appendFileSync, mkdirSync, renameSync, statSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+
6
+ const HOOK_LOG_MAX_BYTES = 5 * 1024 * 1024;
7
+
8
+ const AGENT_TYPES = new Set(['claude', 'codex', 'opencode', 'cursor']);
9
+ const COMMAND_HOOK_TOOL = /^mcp__(?:plugin_polygraph_)?polygraph[-_]mcp__/;
10
+ const OPENCODE_TOOL = /^polygraph(?:(?:-|_)mcp)?_/;
11
+
12
+ function nonEmptyString(value) {
13
+ return typeof value === 'string' && value.trim() ? value : undefined;
14
+ }
15
+
16
+ function isManagedChildEnvironment(env) {
17
+ return Boolean(env && Object.hasOwn(env, 'POLYGRAPH_CHILD_AGENT'));
18
+ }
19
+
20
+ export function isPolygraphMcpToolName(toolName) {
21
+ const name = nonEmptyString(toolName);
22
+ return Boolean(name && (COMMAND_HOOK_TOOL.test(name) || OPENCODE_TOOL.test(name)));
23
+ }
24
+
25
+ export function buildLinkAgentSessionArgs({
26
+ polygraphSessionId,
27
+ agentType,
28
+ agentSessionId,
29
+ cwd,
30
+ transcriptPath,
31
+ pid,
32
+ source,
33
+ }) {
34
+ const session = nonEmptyString(polygraphSessionId);
35
+ const harnessSession = nonEmptyString(agentSessionId);
36
+ const claimSource = nonEmptyString(source);
37
+ if (!AGENT_TYPES.has(agentType)) throw new Error(`Unsupported agent type: ${agentType}`);
38
+ if (!harnessSession) throw new Error('agentSessionId is required');
39
+ if (!claimSource) throw new Error('source is required');
40
+
41
+ const args = ['_link-agent-session'];
42
+ if (session) args.push('--session', session);
43
+ args.push('--agent-type', agentType, '--agent-session-id', harnessSession);
44
+
45
+ const workingDirectory = nonEmptyString(cwd);
46
+ if (workingDirectory) args.push('--cwd', workingDirectory);
47
+
48
+ const transcript = nonEmptyString(transcriptPath);
49
+ if (transcript) args.push('--transcript-path', transcript);
50
+
51
+ if (Number.isSafeInteger(pid) && pid > 0) {
52
+ args.push('--pid', String(pid));
53
+ }
54
+
55
+ args.push('--source', claimSource);
56
+ return args;
57
+ }
58
+
59
+ export function linkAgentSession(claim, spawn = spawnSync, env = process.env) {
60
+ if (isManagedChildEnvironment(env)) return false;
61
+
62
+ const args = buildLinkAgentSessionArgs(claim);
63
+ const command = nonEmptyString(env?.POLYGRAPH_CLI) ?? 'polygraph';
64
+ const commandEnv = nonEmptyString(claim.polygraphSessionId) ? env : { ...env };
65
+ if (commandEnv !== env) {
66
+ delete commandEnv.POLYGRAPH_SESSION_ID;
67
+ delete commandEnv.POLYGRAPH_CAPTURE_TOKEN;
68
+ }
69
+
70
+ const result = spawn(command, args, {
71
+ encoding: 'utf8',
72
+ env: commandEnv,
73
+ stdio: ['ignore', 'ignore', 'pipe'],
74
+ });
75
+
76
+ if (result?.error) throw result.error;
77
+ if (result?.status !== 0) {
78
+ const detail = nonEmptyString(result?.stderr);
79
+ throw new Error(
80
+ `polygraph _link-agent-session exited with status ${String(result?.status)}` +
81
+ (detail ? `: ${detail}` : '')
82
+ );
83
+ }
84
+
85
+ return true;
86
+ }
87
+
88
+ export function buildCommandHookLink(payload, agentType, env = process.env) {
89
+ if (!payload || typeof payload !== 'object') return undefined;
90
+ if (isManagedChildEnvironment(env)) return undefined;
91
+
92
+ // Cursor payloads carry the id in both session_id and conversation_id;
93
+ // the fallback keeps the link working if one of them disappears.
94
+ const agentSessionId =
95
+ nonEmptyString(payload.session_id) ?? nonEmptyString(payload.conversation_id);
96
+ if (!agentSessionId) return undefined;
97
+
98
+ // Cursor has no top-level cwd; workspace_roots[0] is the launch directory.
99
+ const workspaceRoot = Array.isArray(payload.workspace_roots)
100
+ ? nonEmptyString(payload.workspace_roots[0])
101
+ : undefined;
102
+
103
+ const common = {
104
+ agentType,
105
+ agentSessionId,
106
+ cwd: nonEmptyString(payload.cwd) ?? workspaceRoot,
107
+ transcriptPath: nonEmptyString(payload.transcript_path),
108
+ source: 'hook',
109
+ };
110
+
111
+ // Claude and Codex send PascalCase event names; cursor sends camelCase.
112
+ if (
113
+ payload.hook_event_name === 'SessionStart' ||
114
+ payload.hook_event_name === 'sessionStart'
115
+ ) {
116
+ const polygraphSessionId = nonEmptyString(env.POLYGRAPH_SESSION_ID);
117
+ if (polygraphSessionId) return { ...common, polygraphSessionId };
118
+
119
+ // Ordinary sessions of every supported harness are eligible for
120
+ // speculative capture, so later session searches can find them even when
121
+ // the session was not launched with Polygraph session evidence.
122
+ return AGENT_TYPES.has(agentType) ? common : undefined;
123
+ }
124
+
125
+ if (payload.hook_event_name === 'PostToolUse') {
126
+ return isPolygraphMcpToolName(payload.tool_name) ? common : undefined;
127
+ }
128
+
129
+ return undefined;
130
+ }
131
+
132
+ export function logHookFailure(
133
+ hook,
134
+ error,
135
+ meta = {},
136
+ home = process.env.HOME?.trim() || homedir()
137
+ ) {
138
+ try {
139
+ const logsDir = join(home, '.polygraph', 'logs');
140
+ mkdirSync(logsDir, { recursive: true });
141
+ const logFile = join(logsDir, 'hooks.log');
142
+
143
+ try {
144
+ if (statSync(logFile).size > HOOK_LOG_MAX_BYTES) {
145
+ renameSync(logFile, `${logFile}.1`);
146
+ }
147
+ } catch {
148
+ // There may be no prior log, and logging must stay best-effort.
149
+ }
150
+
151
+ const entry = {
152
+ time: new Date().toISOString(),
153
+ hook,
154
+ pid: process.pid,
155
+ ...meta,
156
+ error: error instanceof Error ? error.message : String(error),
157
+ ...(error instanceof Error && error.stack ? { stack: error.stack } : {}),
158
+ };
159
+ appendFileSync(logFile, JSON.stringify(entry) + '\n');
160
+ } catch {
161
+ // Hook diagnostics must never break the harness event that triggered them.
162
+ }
163
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "sessionStart": [
5
+ {
6
+ "command": "node hooks/record-session-mapping.mjs cursor"
7
+ }
8
+ ]
9
+ }
10
+ }
@@ -0,0 +1,62 @@
1
+ import { readFileSync, realpathSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ import {
5
+ buildCommandHookLink,
6
+ linkAgentSession,
7
+ logHookFailure,
8
+ } from './agent-session-link.mjs';
9
+
10
+ function readPayload() {
11
+ try {
12
+ const raw = readFileSync(0, 'utf8');
13
+ return raw ? JSON.parse(raw) : undefined;
14
+ } catch {
15
+ return undefined;
16
+ }
17
+ }
18
+
19
+ export function main({
20
+ payload = readPayload(),
21
+ agentType = process.argv[2],
22
+ env = process.env,
23
+ pid = process.ppid,
24
+ spawn,
25
+ } = {}) {
26
+ try {
27
+ const link = buildCommandHookLink(payload, agentType, env);
28
+ if (!link) return false;
29
+
30
+ const claim = {
31
+ ...link,
32
+ cwd: link.cwd ?? process.cwd(),
33
+ };
34
+ // Claude lifecycle hooks deliberately forward only the exact harness
35
+ // identity, transcript, cwd, and hook source. PID is not identity and can
36
+ // be stale by the time an asynchronous SessionStart hook runs.
37
+ if (!(agentType === 'claude' && payload?.hook_event_name === 'SessionStart')) {
38
+ claim.pid = pid;
39
+ }
40
+
41
+ return linkAgentSession(claim, spawn, env);
42
+ } catch (error) {
43
+ logHookFailure(`${agentType || 'unknown'}:link-agent-session`, error, {
44
+ hookEventName: payload?.hook_event_name,
45
+ agentSessionId: payload?.session_id,
46
+ });
47
+ return false;
48
+ }
49
+ }
50
+
51
+ function isMainModule() {
52
+ if (!process.argv[1]) return false;
53
+ try {
54
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ if (isMainModule()) {
61
+ main();
62
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@polygraph/cursor-plugin",
3
+ "version": "0.4.51",
4
+ "description": "AI agent skills and subagents for Polygraph sessions, repository context, and coordination",
5
+ "license": "UNLICENSED",
6
+ "private": false,
7
+ "author": {
8
+ "name": "Narwhal Technologies Inc",
9
+ "email": "hello@nrwl.io",
10
+ "url": "https://nx.dev"
11
+ },
12
+ "homepage": "https://github.com/nrwl/polygraph-skills#readme",
13
+ "repository": "https://github.com/nrwl/polygraph-skills",
14
+ "keywords": [
15
+ "claude",
16
+ "codex",
17
+ "nx",
18
+ "opencode",
19
+ "polygraph",
20
+ "skills"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "files": [
26
+ "plugin.json",
27
+ "skills/",
28
+ "agents/",
29
+ "hooks/",
30
+ "bin/",
31
+ "README.md"
32
+ ],
33
+ "bin": {
34
+ "polygraph-cursor-plugin": "./bin/polygraph-cursor-plugin.mjs"
35
+ }
36
+ }
package/plugin.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "polygraph",
3
+ "version": "0.4.51",
4
+ "description": "AI agent skills and subagents for Polygraph sessions, repository context, and coordination"
5
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: adversarial-review
3
+ description: Review a Polygraph session with independent per-repo reviewers and attach one consolidated review artifact.
4
+
5
+ ---
6
+
7
+ # Adversarial Review
8
+
9
+ 1. Skip this step entirely if a reviewer agent was already named — by the user, or in the instruction that launched you (e.g. from `polygraph session review --adversarial`); in that case use that agent and do not ask. Otherwise, **pick the agent**: Ask which should review—`claude`, `codex`, or `opencode` for `spawn_agent`'s `agent` parameter. If the user names a model, pass it via `spawn_agent`'s optional `model` parameter; don't ask about models.
10
+ 2. **Get the session description.**
11
+ 3. **Get each repo's plan.**
12
+ 4. **Delegate one reviewer per repo** in parallel, `role: "reviewer"`. Ask to review, identify issues. Do the delegation even for the "initiator" repo.
13
+ 5. **Summarize and attach.** Consolidate reviews into one consolidated Markdown review with per-repo sections and present it to the user. Then call `upload_artifact` once with `sessionId`, that review as `content`, `kind: "review"`, `format: "markdown"`, name `adversarial-review-YYYY-MM-DDTHH-mm-ssZ.md`. If the upload fails, report that separately without suppressing the review.
14
+ 6. **Ask what next.** Address feedback or continue. Skip if the user already said.
15
+ 7. If the user selects "address the feedback", pass each repo's feedback to the repo default agent (not the reviewer). The initiator should fix things itself without delegating.
@@ -0,0 +1,206 @@
1
+ ---
2
+ name: await-polygraph-ci
3
+ description: Wait for CI to settle across all repos in a Polygraph session, then report results and investigate failures. USE WHEN user says "await polygraph", "wait for polygraph ci", "polygraph ci status", "check polygraph ci", "watch polygraph session", "monitor polygraph".
4
+
5
+ ---
6
+
7
+ # Await Polygraph CI
8
+
9
+ Wait for all CI pipelines in a Polygraph session to reach a stable state (succeeded, failed, etc.), then produce a unified summary. If any pipelines failed, investigate via child agents and present fix options.
10
+
11
+ Some Polygraph tools have both MCP and CLI equivalents — use whichever is available in your environment. See the polygraph skill's tool table for the full mapping.
12
+
13
+ ## CI status source
14
+
15
+ CI polling uses the polygraph-mcp `show_session` tool, which now carries per-PR CI status directly on each pull request:
16
+
17
+ ```
18
+ show_session(sessionId: "<session-id>")
19
+ ```
20
+
21
+ It returns the full session, including `repositories[]` (for repo display names) and `pullRequests[]`. Each `pullRequests[]` entry has `id`, `repositoryId`, `url`, `branch`, `status` (PR status: `DRAFT` / `OPEN` / `MERGED` / `CLOSED`), and a `ci` object — **`ci` may be absent if the PR has no CI**. When present, `ci` has `status`, `cipeUrl` (non-null ⇒ CIPE; null + `externalCIRuns` ⇒ external CI), `completedAt`, `selfHealingStatus`, and `externalCIRuns[]` (each with `runId`, `name`, `status`, `conclusion`, `url`, and `jobs[]`). Always read `ci` defensively (`pr.ci?.…`).
22
+
23
+ **`cipeUrl` is a human-facing web link, NOT a data source.** It points at the Nx Cloud web app, which requires browser authentication and returns no machine-readable data. Never fetch, curl, WebFetch, or poll `cipeUrl` (or any other Nx Cloud URL) directly. CI *status* comes only from polling `show_session`; CIPE *details* (failed tasks, logs, self-healing) come only from the Nx MCP `ci_information` tool. The only thing to do with `cipeUrl` is display it to the user so they can open it in a browser.
24
+
25
+ ## Prerequisite: Nx MCP server (CIPE deep-dive + self-healing)
26
+
27
+ CIPE failure investigation (`ci_information`) and applying/rejecting self-healing fixes (`update_self_healing_fix`) are **not** polygraph-mcp tools — they are provided by the **Nx MCP server** (`mcp__plugin_nx_nx-mcp`). Before relying on the Phase 4 CIPE deep-dive or the Phase 5 self-healing actions, install the Nx MCP server and verify it is available.
28
+
29
+ If the Nx MCP server is **not** available, this skill can still:
30
+
31
+ - Monitor CI to a terminal state (Phases 1–3) via `show_session`, and
32
+ - Download and inspect **external-CI** job logs via `get_ci_logs` (a polygraph-mcp tool).
33
+
34
+ But it **cannot** perform CIPE deep-dives (`ci_information`) or apply self-healing fixes (`update_self_healing_fix`) without the Nx MCP server. Do NOT compensate by fetching or scraping `cipeUrl` — there is no HTTP fallback for CIPE data; the Nx MCP server is the only programmatic access.
35
+
36
+ If nx-mcp is missing, don't just report the limitation — tell the user how to install it:
37
+
38
+ - In an Nx workspace, run `nx configure-ai-agents` — it sets up the Nx MCP server (and Nx agent skills) for their AI tools, or
39
+ - Add the server manually as a stdio MCP server: `npx nx-mcp@latest` (see https://github.com/nrwl/nx-ai-agents-config for details).
40
+
41
+ MCP servers load at session start, so the user must restart the agent session after installing before the deep-dive and self-healing actions become available.
42
+
43
+ ## Phase 1: Session Setup
44
+
45
+ Fetch the session using `show_session` and read per-PR CI status from `pullRequests[]`.
46
+
47
+ **Parameters:**
48
+
49
+ - `sessionId` (required): The Polygraph session ID
50
+
51
+ ```
52
+ show_session(sessionId: "<session-id>")
53
+ ```
54
+
55
+ 1. Record `monitorStartedAt` = current timestamp (epoch millis).
56
+ 2. Build a tracking table from `pullRequests[]`. For each PR, map:
57
+ - `repo`: repository display name from the matching `repositories[]` entry by `pr.repositoryId` (fall back to `pr.branch` if not found)
58
+ - `repoId`: `pr.repositoryId` (pass straight to `get_ci_logs`)
59
+ - `prUrl`: `pr.url`
60
+ - `prStatus`: `pr.status` (DRAFT / OPEN / MERGED / CLOSED)
61
+ - `ciStatus`: `pr.ci?.status` (may already be a terminal status from a previous run)
62
+ - `cipeUrl`: `pr.ci?.cipeUrl` (null if none → external CI or no CI)
63
+ - `cipeCompletedAt`: `pr.ci?.completedAt` (epoch millis, null if CIPE is active or absent)
64
+ - `selfHealingStatus`: `pr.ci?.selfHealingStatus` (null if none)
65
+ - `jobs`: `pr.ci?.externalCIRuns?.flatMap(r => r.jobs)` (external-CI job list, used in Phase 4)
66
+ - `firstSeenAt`: current timestamp
67
+ 3. If `pullRequests[]` is empty, report "No PRs in session" and exit.
68
+ 4. **Stale detection**: For each PR, determine if its CI status is **stale** — meaning it reflects a previous run, not a current one. A PR's CI status is stale if:
69
+ - `cipeCompletedAt` is non-null AND `cipeCompletedAt < monitorStartedAt` (the CIPE finished before the monitor started)
70
+ - Mark these PRs as `stale: true`
71
+ 5. Display the initial status table, annotating stale PRs:
72
+ ```
73
+ backend: SUCCEEDED (stale) | frontend: SUCCEEDED (stale) | shared-lib: NOT_STARTED
74
+ ```
75
+
76
+ ## Phase 2: Polling Loop
77
+
78
+ **Configuration:**
79
+
80
+ - Timeout: 30 minutes total
81
+ - Backoff: 60s → 90s → 120s (cap)
82
+ - Circuit breaker: exit after 5 consecutive polls with no status change
83
+
84
+ **Each poll iteration:**
85
+
86
+ 1. Call `show_session(sessionId: <session-id>)`
87
+ 2. Update each tracked PR from its matching `pullRequests[]` entry (by PR `id` / `repositoryId`): `ciStatus` ← `pr.ci?.status`, `cipeUrl` ← `pr.ci?.cipeUrl`, `cipeCompletedAt` ← `pr.ci?.completedAt`, `selfHealingStatus` ← `pr.ci?.selfHealingStatus`, `prStatus` ← `pr.status`, and `jobs` ← `pr.ci?.externalCIRuns?.flatMap(r => r.jobs)`
88
+ 3. **Clear stale flag**: If a PR was marked `stale: true` and its `cipeCompletedAt` has changed (or become null, meaning a new CIPE is active), clear the stale flag — this PR now has fresh CI data.
89
+ 4. Display status update:
90
+ ```
91
+ [await-polygraph-ci] Poll #N | Elapsed: Xm | Repos: Y total, Z completed
92
+ backend: SUCCEEDED | frontend: FAILED (self-healing: PENDING) | shared-lib: SUCCEEDED (stale)
93
+ ```
94
+ Include `selfHealingStatus` inline when non-null. Annotate stale PRs.
95
+ 5. Check exclusion rule: if a PR has `prStatus: DRAFT` and `ciStatus: NOT_STARTED` for more than 5 minutes since `firstSeenAt`, mark it as `EXCLUDED` (DRAFT PRs may not trigger CI)
96
+ 6. Check terminal conditions — a PR is terminal when:
97
+ - It is NOT stale, AND:
98
+ - CI status is `SUCCEEDED`, `CANCELED`, or `TIMED_OUT`, OR
99
+ - CI status is `FAILED` AND there is no active self-healing (i.e., `selfHealingStatus` is null or a final state like `APPLIED`, `REJECTED`, `FAILED`)
100
+ - A `FAILED` PR with `selfHealingStatus` indicating an in-progress fix (e.g., `PENDING`, `IN_PROGRESS`) is NOT terminal — keep polling to track the self-healing outcome
101
+ - A **stale** PR is NOT terminal — keep polling until it gets a fresh CIPE or is excluded
102
+ 7. **Stale timeout**: If a stale PR remains stale for more than 5 minutes, assume no new CI is expected for it. Clear the stale flag and treat its current status as final.
103
+ 8. If all non-excluded PRs are terminal → proceed to Phase 3
104
+ 9. If timeout or circuit breaker hit → proceed to Phase 3 with partial results
105
+ 10. Otherwise → wait with backoff, then poll again
106
+
107
+ ## Phase 3: Results Analysis
108
+
109
+ Categorize repos into: succeeded, failed, canceled, timed_out, excluded, in_progress (if timed out).
110
+
111
+ Display final summary table. When showing self-healing status, distinguish clearly between these states:
112
+
113
+ - `COMPLETED` = a fix was **generated and verified**, but **NOT yet applied**. Display as `fix available`.
114
+ - `APPLIED` = the fix was **applied** by the user or agent. Display as `fix applied, awaiting re-run`.
115
+ - `IN_PROGRESS` / `PENDING` = the fix is still being generated. Display as `in progress`.
116
+ - `REJECTED` = the fix was rejected. Display as `fix rejected`.
117
+ - `FAILED` = self-healing failed to produce a fix. Display as `fix failed`.
118
+
119
+ ```
120
+ [await-polygraph-ci] Final Results | Elapsed: Xm
121
+ SUCCEEDED: backend, shared-lib
122
+ FAILED: frontend (self-healing: fix available)
123
+ EXCLUDED: docs (DRAFT, no CI)
124
+ ```
125
+
126
+ Include self-healing status for any repo that has one.
127
+
128
+ - If all succeeded → report success and exit
129
+ - If any failed with `selfHealingStatus: APPLIED`, inform the user that the fix was applied and a CI re-run may be in progress or needed
130
+ - If any failed with `selfHealingStatus: COMPLETED`, inform the user that a fix is **available but not yet applied**, and offer to apply it
131
+ - If any failed → proceed to Phase 4
132
+
133
+ ## Phase 4: Failure Investigation (Child Agent Delegation)
134
+
135
+ For each repo with `ciStatus: FAILED`, branch on the PR's `ci` object from `show_session` (`pullRequests[]`):
136
+
137
+ - **If `pr.ci.cipeUrl` is non-null** → CIPE is authoritative. Delegate investigation using the Nx MCP `ci_information` tool (requires the Nx MCP server — see the prerequisite note above; if nx-mcp is unavailable, report the CIPE URL to the user, offer the install steps from the prerequisite section, and do NOT fetch the URL as a substitute).
138
+ - **If `pr.ci.cipeUrl` is null but `pr.ci.externalCIRuns` exists** → external CI only. Examine failed jobs from `pr.ci.externalCIRuns[].jobs` and use `get_ci_logs(sessionId, repositoryId, jobId)` (a polygraph-mcp tool) for log retrieval, passing `pr.repositoryId` and the failed job's `jobId` straight from the same PR object.
139
+
140
+ 1. Display known info from the PR's `ci` object before delegating:
141
+
142
+ ```
143
+ Repository: frontend
144
+ CI Source: <CIPE or External CI (GitHub Actions)>
145
+ CI Pipeline: <pr.ci.cipeUrl, or GitHub Actions run URL>
146
+ Self-healing: <pr.ci.selfHealingStatus, or "None">
147
+ Investigating failure details...
148
+ ```
149
+
150
+ 2. **Delegate investigation** (non-blocking) — call `spawn_agent` for each failed repo:
151
+
152
+ - `sessionId`: the session ID
153
+ - `repo`: the repository name
154
+ - `instruction` (when CIPE exists): Use the Nx MCP `ci_information` tool to investigate the CI failure on this branch (the Nx MCP server must be installed). Return a structured summary with: (1) list of failed task IDs with a one-line error summary each, (2) failure category (Build / Test / Lint / E2E / Infra / Other).
155
+ - `instruction` (when no CIPE, external CI only): The PR's `ci` object shows external CI failures with these failed jobs: [list `jobId` + `name` from `pr.ci.externalCIRuns[].jobs` where `conclusion` is `failure`]. Use `get_ci_logs(sessionId, repositoryId, jobId)` to save the log for each failed job to a local file, then use the `Read` tool to examine the log file contents. Return a structured summary with: (1) one-line error summary per failed job, (2) failure category (Build / Test / Lint / E2E / Infra / Other), (3) relevant log excerpts.
156
+ - `context`: Polygraph session monitoring — investigating CI failure for unified summary. The repository ID for this repo is the PR's `repositoryId`.
157
+
158
+ Since `spawn_agent` is non-blocking, you can delegate to multiple failed repos in parallel.
159
+
160
+ 3. **Monitor investigation progress** — launch one background `polygraph-delegate-subagent` per delegation id and let it do the waiting. When it exits, read that investigation with a single unwaited `show_agent`:
161
+
162
+ ```
163
+ show_agent(sessionId: "<session-id>", id: "<delegation-id>")
164
+ ```
165
+
166
+ `result.text` is the child's investigation summary. Never run a waited `show_agent` loop here, and do not pass `tail` unless `result.text` alone is insufficient.
167
+
168
+ 4. Collect each child agent's response from its unwaited `show_agent` read. If a child agent fails or gets stuck, use `stop_agent` with its delegation id to terminate it and skip that repo.
169
+
170
+ 5. Display failure summary for each repo:
171
+
172
+ ```
173
+ Repository: frontend
174
+ CI Pipeline: <cipeUrl>
175
+ Failed Tasks (2):
176
+ - frontend:build → TypeScript error in src/app.tsx:42
177
+ - frontend:test → 3 test suites failed
178
+ Category: Build + Test failures
179
+ Self-healing: <selfHealingStatus>
180
+ Job Logs: <number of logs retrieved, if any>
181
+ ```
182
+
183
+ If CI job logs were retrieved via `get_ci_logs`, include relevant excerpts (error messages, stack traces) in the summary. Keep excerpts concise — only the most relevant lines.
184
+
185
+ ## Phase 5: Fix Planning
186
+
187
+ 1. Group failures by category (Build, Test, Lint, E2E, Infra)
188
+ 2. Identify cross-repo dependency issues (e.g., shared-lib build failure blocking frontend)
189
+ 3. Suggest fix order based on dependency graph (upstream repos first)
190
+ 4. Present next actions to the user based on self-healing status:
191
+ - If any repo has `selfHealingStatus` with an available fix → offer to **apply self-healing** via `update_self_healing_fix(action: "APPLY")` or **reject** it. `update_self_healing_fix` is an **Nx MCP** tool (`mcp__plugin_nx_nx-mcp`) — it requires the Nx MCP server. If nx-mcp is unavailable, report that a fix is available but cannot be applied from here, and offer the install steps from the prerequisite section.
192
+ - If self-healing was already applied → offer to **resume monitoring** to watch the re-triggered CI
193
+ - **Delegate fixes**: use Polygraph to send fix instructions to child agents (for repos without self-healing or where self-healing was rejected/failed)
194
+ - **Get more details**: drill into a specific repo's failure
195
+ - **Exit**: done monitoring
196
+
197
+ ## Notes
198
+
199
+ - This skill does NOT push code directly. The only write action it may take is applying/rejecting a self-healing fix via `update_self_healing_fix`, an **Nx MCP** tool that performs an Nx Cloud operation (not a local code change) and requires the Nx MCP server.
200
+ - Both `ci_information` and `update_self_healing_fix` are **Nx MCP** tools (`mcp__plugin_nx_nx-mcp`), not polygraph-mcp tools. Their responses include a `hints` array with contextual guidance (e.g., disclaimers about which CI Attempt was retrieved). Always check and surface non-empty hints.
201
+ - `cipeUrl` is a browser link for the user — never fetch, curl, WebFetch, or poll it (in the main agent or in child agents). CIPE data is only available via the Nx MCP `ci_information` tool.
202
+ - All heavy CI data inspection happens in child agents via `spawn_agent` to keep this context window clean.
203
+
204
+ - Child agents can use `get_ci_logs` to save CI job logs to local files, but ONLY when no CIPE exists for the PR (`pr.ci.cipeUrl` is null). When a CIPE exists, logs come from the CIPE system via the Nx MCP `ci_information` tool. Job IDs come from `pr.ci.externalCIRuns[].jobs[].jobId` in the `show_session` response. The tool returns a file path (`logFile`) and size (`sizeBytes`) — use the `Read` tool to examine the log content. Logs can be large (100KB+), so only fetch logs for failed or relevant jobs.
205
+ - `spawn_agent` is **non-blocking** — it starts the child agent and returns a delegation id immediately. Waiting on that id belongs in a background poller subagent; an unwaited `show_agent` by id returns the result, and `stop_agent` by id terminates a stuck agent.
206
+ - The `show_session` response is compact and safe to poll from the main agent.
@@ -0,0 +1,150 @@
1
+ ---
2
+ name: get-latest-ci
3
+ description: Fetch the latest CI pipeline execution for the current branch. Returns the most recent CIPE which may be completed, in progress, or null. Use when you need to review CI status, check failures, or inspect CI state.
4
+
5
+ ---
6
+
7
+ # Get Latest CI Information
8
+
9
+ Fetch the latest CI pipeline execution for the current branch. This is a **one-shot fetch** — return results immediately. Do NOT poll, loop, or wait for status changes.
10
+
11
+ ## Context
12
+
13
+ - **Current Branch:** !`git branch --show-current`
14
+ - **Current Commit:** !`git rev-parse --short HEAD`
15
+
16
+ ## Step 1: Fetch CI Status via Subagent
17
+
18
+ Call the `ci_information` tool from the nx MCP server with these parameters:
19
+
20
+ ```yaml
21
+ select: 'cipeStatus,cipeUrl,branch,commitSha,selfHealingStatus,verificationStatus,userAction,failedTaskIds,verifiedTaskIds,selfHealingEnabled,failureClassification,couldAutoApplyTasks,shortLink,confidence,confidenceReasoning,hints'
22
+ ```
23
+
24
+ If `cipeStatus` is `FAILED` and `selfHealingStatus` is `COMPLETED` or `FAILED` and there are `failedTaskIds`, make a second call with:
25
+
26
+ ```yaml
27
+ select: 'taskOutputSummary,suggestedFix,suggestedFixReasoning,suggestedFixDescription'
28
+ ```
29
+
30
+ Only return the first page — do not paginate.
31
+
32
+ ## Step 2: Report Results
33
+
34
+ Based on the subagent's response, report to the user. Always include the CIPE URL when available.
35
+
36
+ If the response contains a non-empty `hints` array, include those hints in the output to the user.
37
+
38
+ ### No CIPE found (null/empty response)
39
+
40
+ ```
41
+ [get-latest-ci] No CI pipeline execution found for branch '<branch>'.
42
+ ```
43
+
44
+ ### CI Succeeded
45
+
46
+ ```
47
+ [get-latest-ci] CI passed!
48
+ [get-latest-ci] URL: <cipeUrl>
49
+ [get-latest-ci] Commit: <commitSha>
50
+ ```
51
+
52
+ ### CI In Progress / Not Started
53
+
54
+ ```
55
+ [get-latest-ci] CI is <IN_PROGRESS|NOT_STARTED>.
56
+ [get-latest-ci] URL: <cipeUrl>
57
+ [get-latest-ci] Commit: <commitSha>
58
+ ```
59
+
60
+ If self-healing is also in progress, add:
61
+
62
+ ```
63
+ [get-latest-ci] Self-healing: <selfHealingStatus> | Verification: <verificationStatus>
64
+ ```
65
+
66
+ ### CI Failed — With Self-Healing Fix Available
67
+
68
+ When `cipeStatus == 'FAILED'` AND `selfHealingStatus == 'COMPLETED'` AND `suggestedFix != null`:
69
+
70
+ ```
71
+ [get-latest-ci] CI failed.
72
+ [get-latest-ci] URL: <cipeUrl>
73
+ [get-latest-ci] Commit: <commitSha>
74
+ [get-latest-ci] Failed tasks: <failedTaskIds>
75
+ [get-latest-ci]
76
+ [get-latest-ci] Self-healing fix available!
77
+ [get-latest-ci] Short link: <shortLink>
78
+ [get-latest-ci] Confidence: <confidence> — <confidenceReasoning>
79
+ [get-latest-ci] Verification: <verificationStatus>
80
+ [get-latest-ci] Auto-apply eligible: <couldAutoApplyTasks>
81
+ [get-latest-ci]
82
+ [get-latest-ci] Fix description: <suggestedFixDescription>
83
+ [get-latest-ci] Fix reasoning: <suggestedFixReasoning> (truncated to first page)
84
+ ```
85
+
86
+ ### CI Failed — Self-Healing In Progress
87
+
88
+ When `cipeStatus == 'FAILED'` AND `selfHealingStatus == 'IN_PROGRESS'`:
89
+
90
+ ```
91
+ [get-latest-ci] CI failed. Self-healing is generating a fix...
92
+ [get-latest-ci] URL: <cipeUrl>
93
+ [get-latest-ci] Commit: <commitSha>
94
+ [get-latest-ci] Failed tasks: <failedTaskIds>
95
+ [get-latest-ci]
96
+ [get-latest-ci] Use /monitor-ci to wait for the fix and apply it.
97
+ ```
98
+
99
+ ### CI Failed — Self-Healing Failed or Not Available
100
+
101
+ When `cipeStatus == 'FAILED'` AND (`selfHealingStatus` is `FAILED`, `NOT_EXECUTABLE`, or `null`):
102
+
103
+ ```
104
+ [get-latest-ci] CI failed.
105
+ [get-latest-ci] URL: <cipeUrl>
106
+ [get-latest-ci] Commit: <commitSha>
107
+ [get-latest-ci] Failed tasks: <failedTaskIds>
108
+ [get-latest-ci] Self-healing: <selfHealingStatus or "not available">
109
+ [get-latest-ci] Classification: <failureClassification>
110
+ ```
111
+
112
+ If `taskOutputSummary` was fetched, include a brief summary of failures.
113
+
114
+ ### CI Failed — Environment Issue
115
+
116
+ When `failureClassification == 'ENVIRONMENT_STATE'`:
117
+
118
+ ```
119
+ [get-latest-ci] CI failed due to environment issue.
120
+ [get-latest-ci] URL: <cipeUrl>
121
+ [get-latest-ci] Classification: ENVIRONMENT_STATE
122
+ [get-latest-ci]
123
+ [get-latest-ci] Use /monitor-ci to request an environment rerun.
124
+ ```
125
+
126
+ ### CI Canceled / Timed Out
127
+
128
+ ```
129
+ [get-latest-ci] CI was <CANCELED|TIMED_OUT>.
130
+ [get-latest-ci] URL: <cipeUrl>
131
+ [get-latest-ci] Commit: <commitSha>
132
+ ```
133
+
134
+ ### CI Failed — No Tasks Recorded
135
+
136
+ When `cipeStatus == 'FAILED'` AND `failedTaskIds` is empty AND `selfHealingStatus` is null:
137
+
138
+ ```
139
+ [get-latest-ci] CI failed but no Nx tasks were recorded (likely infrastructure issue).
140
+ [get-latest-ci] URL: <cipeUrl>
141
+ [get-latest-ci] Check CI provider logs for details.
142
+ ```
143
+
144
+ ## Important
145
+
146
+ - This skill is **read-only**. Do NOT apply fixes, push code, or modify anything.
147
+ - `cipeUrl` and `shortLink` are human-facing web links — include them in the output for the user to open in a browser, but never fetch, curl, or poll them yourself. All CIPE data comes from the Nx MCP `ci_information` tool.
148
+
149
+ - If the user wants to act on the results (apply a fix, monitor, etc.), suggest `/monitor-ci`.
150
+ - If the subagent returns an error, report it and suggest the user check their Nx Cloud connection.