@polygraph/claude-plugin 0.4.37 → 0.4.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/hooks/check-plugin-version.mjs +294 -0
- package/hooks/hooks.json +9 -0
- package/package.json +1 -1
- package/skills/await-polygraph-ci/SKILL.md +13 -3
- package/skills/get-latest-ci/SKILL.md +1 -0
- package/skills/polygraph/SKILL.md +30 -3
- package/skills/session-debrief/SKILL.md +17 -6
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// SessionStart hook — checks whether the installed Polygraph plugin is
|
|
2
|
+
// outdated and, when it is, emits a single stdout message so the agent
|
|
3
|
+
// surfaces the problem to the user. Stale plugin versions have silently
|
|
4
|
+
// caused incorrect Polygraph behavior in the past; this makes it visible
|
|
5
|
+
// for agent launches that bypass the polygraph CLI (e.g. desktop apps).
|
|
6
|
+
//
|
|
7
|
+
// Unlike the sibling hooks, this one deliberately writes to stdout — but
|
|
8
|
+
// ONLY when the plugin is outdated. When current, unknown, offline, or on
|
|
9
|
+
// any error it prints nothing and exits 0.
|
|
10
|
+
//
|
|
11
|
+
// The harness ('claude' | 'codex') is passed as the first CLI argument so
|
|
12
|
+
// the same script ships in both plugin artifacts.
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
appendFileSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
readFileSync,
|
|
18
|
+
realpathSync,
|
|
19
|
+
renameSync,
|
|
20
|
+
statSync,
|
|
21
|
+
writeFileSync,
|
|
22
|
+
} from 'node:fs';
|
|
23
|
+
import { homedir } from 'node:os';
|
|
24
|
+
import { dirname, join } from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const HOOK_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
28
|
+
const CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
29
|
+
const FETCH_TIMEOUT_MS = 3000;
|
|
30
|
+
const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
|
|
31
|
+
|
|
32
|
+
const PACKAGE_BY_HARNESS = {
|
|
33
|
+
claude: '@polygraph/claude-plugin',
|
|
34
|
+
codex: '@polygraph/codex-plugin',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const REMEDIATION_BY_HARNESS = {
|
|
38
|
+
claude: 'run `claude plugins update polygraph@polygraph-plugins`',
|
|
39
|
+
codex:
|
|
40
|
+
'run `npx --prefer-online @polygraph/codex-plugin@latest install` then `codex plugin add polygraph@polygraph-plugins`',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Append a one-line JSON record of a hook failure to ~/.polygraph/logs/hooks.log.
|
|
44
|
+
// This hook swallows its errors silently, so this on-disk log is the only
|
|
45
|
+
// record that something went wrong. The logger is itself failure-proof.
|
|
46
|
+
function logHookFailure(
|
|
47
|
+
hook,
|
|
48
|
+
error,
|
|
49
|
+
meta = {},
|
|
50
|
+
home = process.env.HOME?.trim() || homedir()
|
|
51
|
+
) {
|
|
52
|
+
try {
|
|
53
|
+
const logsDir = join(home, '.polygraph', 'logs');
|
|
54
|
+
mkdirSync(logsDir, { recursive: true });
|
|
55
|
+
const logFile = join(logsDir, 'hooks.log');
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
if (statSync(logFile).size > HOOK_LOG_MAX_BYTES) {
|
|
59
|
+
renameSync(logFile, `${logFile}.1`);
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// no prior log, or rotation failed — ignore
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const entry = {
|
|
66
|
+
time: new Date().toISOString(),
|
|
67
|
+
hook,
|
|
68
|
+
pid: process.pid,
|
|
69
|
+
...meta,
|
|
70
|
+
error: error instanceof Error ? error.message : String(error),
|
|
71
|
+
...(error instanceof Error && error.stack ? { stack: error.stack } : {}),
|
|
72
|
+
};
|
|
73
|
+
appendFileSync(logFile, JSON.stringify(entry) + '\n');
|
|
74
|
+
} catch {
|
|
75
|
+
// Logging must never throw — a failing logger must not break the hook.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function tryParseJson(str) {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(str);
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseSemver(version) {
|
|
88
|
+
if (typeof version !== 'string') return null;
|
|
89
|
+
const match = version
|
|
90
|
+
.trim()
|
|
91
|
+
.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
92
|
+
if (!match) return null;
|
|
93
|
+
return {
|
|
94
|
+
major: Number(match[1]),
|
|
95
|
+
minor: Number(match[2]),
|
|
96
|
+
patch: Number(match[3]),
|
|
97
|
+
prerelease: match[4] ? match[4].split('.') : [],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Returns -1, 0, or 1 when a is lower than, equal to, or higher than b.
|
|
102
|
+
// Returns null when either version is unparseable.
|
|
103
|
+
export function compareSemver(a, b) {
|
|
104
|
+
const pa = parseSemver(a);
|
|
105
|
+
const pb = parseSemver(b);
|
|
106
|
+
if (!pa || !pb) return null;
|
|
107
|
+
|
|
108
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
109
|
+
if (pa[key] !== pb[key]) return pa[key] < pb[key] ? -1 : 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Same core version: a prerelease sorts below a release.
|
|
113
|
+
if (pa.prerelease.length && !pb.prerelease.length) return -1;
|
|
114
|
+
if (!pa.prerelease.length && pb.prerelease.length) return 1;
|
|
115
|
+
|
|
116
|
+
const len = Math.max(pa.prerelease.length, pb.prerelease.length);
|
|
117
|
+
for (let i = 0; i < len; i++) {
|
|
118
|
+
const ia = pa.prerelease[i];
|
|
119
|
+
const ib = pb.prerelease[i];
|
|
120
|
+
if (ia === undefined) return -1;
|
|
121
|
+
if (ib === undefined) return 1;
|
|
122
|
+
if (ia === ib) continue;
|
|
123
|
+
const na = /^\d+$/.test(ia) ? Number(ia) : null;
|
|
124
|
+
const nb = /^\d+$/.test(ib) ? Number(ib) : null;
|
|
125
|
+
if (na !== null && nb !== null) return na < nb ? -1 : 1;
|
|
126
|
+
if (na !== null) return -1; // numeric identifiers sort below alphanumeric
|
|
127
|
+
if (nb !== null) return 1;
|
|
128
|
+
return ia < ib ? -1 : 1;
|
|
129
|
+
}
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Resolve the installed plugin version from the manifest shipped alongside
|
|
134
|
+
// this script: <pluginRoot>/hooks/check-plugin-version.mjs sits next to
|
|
135
|
+
// .claude-plugin/plugin.json (Claude), .codex-plugin/plugin.json (Codex),
|
|
136
|
+
// or package.json.
|
|
137
|
+
export function resolveInstalledVersion(pluginRoot) {
|
|
138
|
+
const manifests = [
|
|
139
|
+
join(pluginRoot, '.claude-plugin', 'plugin.json'),
|
|
140
|
+
join(pluginRoot, '.codex-plugin', 'plugin.json'),
|
|
141
|
+
join(pluginRoot, 'package.json'),
|
|
142
|
+
];
|
|
143
|
+
for (const manifestPath of manifests) {
|
|
144
|
+
let raw;
|
|
145
|
+
try {
|
|
146
|
+
raw = readFileSync(manifestPath, 'utf8');
|
|
147
|
+
} catch {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const parsed = tryParseJson(raw);
|
|
151
|
+
if (parsed && parseSemver(parsed.version)) return parsed.version.trim();
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function cachePath(harness, home) {
|
|
157
|
+
return join(home, '.polygraph', 'logs', `plugin-version-check-${harness}.json`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function readCache(harness, home) {
|
|
161
|
+
try {
|
|
162
|
+
return tryParseJson(readFileSync(cachePath(harness, home), 'utf8'));
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// A cache entry is only trusted when it is recent, was recorded for the
|
|
169
|
+
// currently installed version (updating the plugin invalidates it), and holds
|
|
170
|
+
// either a parseable latest version or null (a negatively-cached failed
|
|
171
|
+
// fetch, so an offline machine does not re-stall on every session start).
|
|
172
|
+
export function isCacheFresh(cache, installed, now) {
|
|
173
|
+
return Boolean(
|
|
174
|
+
cache &&
|
|
175
|
+
Number.isFinite(cache.checkedAt) &&
|
|
176
|
+
now - cache.checkedAt >= 0 &&
|
|
177
|
+
now - cache.checkedAt < CACHE_MAX_AGE_MS &&
|
|
178
|
+
cache.installed === installed &&
|
|
179
|
+
(cache.latest === null || parseSemver(cache.latest))
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function writeCache(harness, home, entry) {
|
|
184
|
+
const path = cachePath(harness, home);
|
|
185
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
186
|
+
const tmpPath = `${path}.tmp-${process.pid}`;
|
|
187
|
+
writeFileSync(tmpPath, JSON.stringify(entry) + '\n');
|
|
188
|
+
renameSync(tmpPath, path);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function fetchLatestVersion(packageName, fetchImpl) {
|
|
192
|
+
const registry = (process.env.npm_config_registry?.trim() || DEFAULT_REGISTRY)
|
|
193
|
+
.replace(/\/+$/, '');
|
|
194
|
+
const url = `${registry}/-/package/${packageName.replace('/', '%2f')}/dist-tags`;
|
|
195
|
+
const response = await fetchImpl(url, {
|
|
196
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
197
|
+
});
|
|
198
|
+
if (!response.ok) throw new Error(`registry responded ${response.status}`);
|
|
199
|
+
const distTags = await response.json();
|
|
200
|
+
return typeof distTags?.latest === 'string' ? distTags.latest : null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function buildOutdatedMessage(harness, installed, latest) {
|
|
204
|
+
return (
|
|
205
|
+
`The Polygraph plugin is outdated: ${installed} installed, ${latest} latest. ` +
|
|
206
|
+
'Stale plugin versions cause incorrect Polygraph behavior. ' +
|
|
207
|
+
`Tell the user to update it now: ${REMEDIATION_BY_HARNESS[harness]} ` +
|
|
208
|
+
'(or re-run `polygraph config`), then restart the agent session.'
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Check whether the installed plugin is outdated.
|
|
214
|
+
*
|
|
215
|
+
* @param {object} opts
|
|
216
|
+
* @param {string} opts.harness 'claude' | 'codex'
|
|
217
|
+
* @param {string} opts.pluginRoot Directory containing the plugin manifest.
|
|
218
|
+
* @param {string} [opts.home] Override HOME for testing.
|
|
219
|
+
* @param {Function} [opts.fetchImpl] Override fetch for testing.
|
|
220
|
+
* @param {number} [opts.now] Override the clock for testing.
|
|
221
|
+
* @returns {Promise<string|null>} The message to emit, or null to stay silent.
|
|
222
|
+
*/
|
|
223
|
+
export async function checkPluginVersion({
|
|
224
|
+
harness,
|
|
225
|
+
pluginRoot,
|
|
226
|
+
home = process.env.HOME?.trim() || homedir(),
|
|
227
|
+
fetchImpl = fetch,
|
|
228
|
+
now = Date.now(),
|
|
229
|
+
}) {
|
|
230
|
+
const packageName = PACKAGE_BY_HARNESS[harness];
|
|
231
|
+
if (!packageName) return null;
|
|
232
|
+
|
|
233
|
+
const installed = resolveInstalledVersion(pluginRoot);
|
|
234
|
+
if (!installed) return null;
|
|
235
|
+
|
|
236
|
+
let latest;
|
|
237
|
+
const cache = readCache(harness, home);
|
|
238
|
+
if (isCacheFresh(cache, installed, now)) {
|
|
239
|
+
if (cache.latest === null) return null;
|
|
240
|
+
latest = cache.latest;
|
|
241
|
+
} else {
|
|
242
|
+
try {
|
|
243
|
+
latest = await fetchLatestVersion(packageName, fetchImpl);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
// Negative cache: remember the failed fetch so an offline machine
|
|
246
|
+
// does not re-stall for the fetch timeout on every session start.
|
|
247
|
+
writeCache(harness, home, { checkedAt: now, installed, latest: null });
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
if (!parseSemver(latest)) {
|
|
251
|
+
writeCache(harness, home, { checkedAt: now, installed, latest: null });
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
writeCache(harness, home, { checkedAt: now, installed, latest });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (compareSemver(installed, latest) === -1) {
|
|
258
|
+
return buildOutdatedMessage(harness, installed, latest);
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function main() {
|
|
264
|
+
const harness = process.argv[2];
|
|
265
|
+
try {
|
|
266
|
+
const pluginRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
267
|
+
const message = await checkPluginVersion({ harness, pluginRoot });
|
|
268
|
+
if (message) process.stdout.write(message + '\n');
|
|
269
|
+
} catch (error) {
|
|
270
|
+
// Offline or broken registry must never block or pollute the session,
|
|
271
|
+
// but record it so failures are not invisible.
|
|
272
|
+
logHookFailure(`${harness || 'unknown'}:check-plugin-version`, error);
|
|
273
|
+
}
|
|
274
|
+
process.exitCode = 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Run only when executed directly as a hook, not when imported (e.g. by tests).
|
|
278
|
+
// realpathSync both sides so the check holds when the plugin lives under a
|
|
279
|
+
// symlinked path (e.g. macOS /tmp -> /private/tmp).
|
|
280
|
+
function isMainModule() {
|
|
281
|
+
if (!process.argv[1]) return false;
|
|
282
|
+
try {
|
|
283
|
+
return (
|
|
284
|
+
realpathSync(process.argv[1]) ===
|
|
285
|
+
realpathSync(fileURLToPath(import.meta.url))
|
|
286
|
+
);
|
|
287
|
+
} catch {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (isMainModule()) {
|
|
293
|
+
main();
|
|
294
|
+
}
|
package/hooks/hooks.json
CHANGED
|
@@ -24,6 +24,15 @@
|
|
|
24
24
|
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/record-session-mapping.mjs claude"
|
|
25
25
|
}
|
|
26
26
|
]
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"matcher": "startup|resume",
|
|
30
|
+
"hooks": [
|
|
31
|
+
{
|
|
32
|
+
"type": "command",
|
|
33
|
+
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/check-plugin-version.mjs claude"
|
|
34
|
+
}
|
|
35
|
+
]
|
|
27
36
|
}
|
|
28
37
|
]
|
|
29
38
|
}
|
package/package.json
CHANGED
|
@@ -29,6 +29,8 @@ show_session(sessionId: "<session-id>")
|
|
|
29
29
|
|
|
30
30
|
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?.…`).
|
|
31
31
|
|
|
32
|
+
**`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.
|
|
33
|
+
|
|
32
34
|
## Prerequisite: Nx MCP server (CIPE deep-dive + self-healing)
|
|
33
35
|
|
|
34
36
|
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.
|
|
@@ -38,7 +40,14 @@ If the Nx MCP server is **not** available, this skill can still:
|
|
|
38
40
|
- Monitor CI to a terminal state (Phases 1–3) via `show_session`, and
|
|
39
41
|
- Download and inspect **external-CI** job logs via `get_ci_logs` (a polygraph-mcp tool).
|
|
40
42
|
|
|
41
|
-
But it **cannot** perform CIPE deep-dives (`ci_information`) or apply self-healing fixes (`update_self_healing_fix`) without the Nx MCP server.
|
|
43
|
+
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.
|
|
44
|
+
|
|
45
|
+
If nx-mcp is missing, don't just report the limitation — tell the user how to install it:
|
|
46
|
+
|
|
47
|
+
- 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
|
|
48
|
+
- Add the server manually as a stdio MCP server: `npx nx-mcp@latest` (see https://github.com/nrwl/nx-ai-agents-config for details).
|
|
49
|
+
|
|
50
|
+
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
51
|
|
|
43
52
|
## Phase 1: Session Setup
|
|
44
53
|
|
|
@@ -134,7 +143,7 @@ Include self-healing status for any repo that has one.
|
|
|
134
143
|
|
|
135
144
|
For each repo with `ciStatus: FAILED`, branch on the PR's `ci` object from `show_session` (`pullRequests[]`):
|
|
136
145
|
|
|
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
|
|
146
|
+
- **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
147
|
- **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
148
|
|
|
140
149
|
1. Display known info from the PR's `ci` object before delegating:
|
|
@@ -188,7 +197,7 @@ For each repo with `ciStatus: FAILED`, branch on the PR's `ci` object from `show
|
|
|
188
197
|
2. Identify cross-repo dependency issues (e.g., shared-lib build failure blocking frontend)
|
|
189
198
|
3. Suggest fix order based on dependency graph (upstream repos first)
|
|
190
199
|
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.
|
|
200
|
+
- 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
201
|
- If self-healing was already applied → offer to **resume monitoring** to watch the re-triggered CI
|
|
193
202
|
- **Delegate fixes**: use Polygraph to send fix instructions to child agents (for repos without self-healing or where self-healing was rejected/failed)
|
|
194
203
|
- **Get more details**: drill into a specific repo's failure
|
|
@@ -198,6 +207,7 @@ For each repo with `ciStatus: FAILED`, branch on the PR's `ci` object from `show
|
|
|
198
207
|
|
|
199
208
|
- 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
209
|
- 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.
|
|
210
|
+
- `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.
|
|
201
211
|
- All heavy CI data inspection happens in child agents via `spawn_agent` to keep this context window clean.
|
|
202
212
|
|
|
203
213
|
- 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.
|
|
@@ -153,6 +153,7 @@ When `cipeStatus == 'FAILED'` AND `failedTaskIds` is empty AND `selfHealingStatu
|
|
|
153
153
|
## Important
|
|
154
154
|
|
|
155
155
|
- This skill is **read-only**. Do NOT apply fixes, push code, or modify anything.
|
|
156
|
+
- `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.
|
|
156
157
|
|
|
157
158
|
- Always delegate the MCP call to a subagent. Do NOT call ci_information yourself.
|
|
158
159
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: polygraph
|
|
3
|
-
description: Guidance for working with Polygraph sessions, shared/resumable agent context, repository graph visibility, linked PR/CI state, and cross-repo expansion when needed. Use when starting, joining, resuming, inspecting, or sharing a Polygraph session; handing off progress; discovering related repositories; coordinating changes/branches/PRs across repos; delegating tasks to child agents in different repos;
|
|
3
|
+
description: Guidance for working with Polygraph sessions, shared/resumable agent context, repository graph visibility, linked PR/CI state, and cross-repo expansion when needed. Use when starting, joining, resuming, inspecting, or sharing a Polygraph session; handing off progress; discovering related repositories; coordinating changes/branches/PRs across repos; delegating tasks to child agents in different repos; checking CI status and logs; or tracing a commit or line of code back to the session that produced it. TRIGGER when user mentions "polygraph", resuming or sharing a session, "other repos", "other repositories", "who uses this", "what uses this", "cross-repo", "multi-repo", "consuming this API/endpoint", "dependent repositories", asks about what other repos are doing with shared code/APIs/endpoints, or asks about a "commit sha", "session behind this commit", "which session changed this line", "find session by sha", "git blame".
|
|
4
4
|
|
|
5
5
|
allowed-tools:
|
|
6
6
|
- mcp__plugin_polygraph_polygraph-mcp
|
|
@@ -61,6 +61,7 @@ Polygraph functionality is available via both MCP tools and CLI commands. Use wh
|
|
|
61
61
|
| `login` | `polygraph auth login [--token]` | Authenticate with Polygraph (use `--token` for headless/CI) |
|
|
62
62
|
| `logout` | `polygraph auth logout` | Log out of Polygraph |
|
|
63
63
|
| `list_sessions` | `polygraph session list` | List sessions. By default only active sessions created by the current git user; pass `recommendedFilters: false` for all sessions. |
|
|
64
|
+
| `search_sessions` | `polygraph session search` | Find sessions by free-text `query` OR by commit `sha` — pass EXACTLY ONE of the two (they are mutually exclusive). `sha` (CLI: `--sha <sha>`) is an exact lookup of the session(s) linked to a commit, full or partial, 7-40 hex chars; it returns matching sessions newest first, org-scoped, and explicit sessions only (implicit sessions are never returned). Supports `--json` and `--limit` (1-50). See "Finding the Session Behind a Commit or Line". |
|
|
64
65
|
| `list_accounts` | `polygraph account list` | List available organizations |
|
|
65
66
|
| `select_account` | `polygraph account select` | Select the organization that future commands run against |
|
|
66
67
|
| `whoami` | `polygraph whoami` | Show current auth status and org |
|
|
@@ -213,6 +214,32 @@ Description:
|
|
|
213
214
|
Inspect the PR commits/diff and investigate the requested behavior. Report findings with file paths and concrete evidence.
|
|
214
215
|
```
|
|
215
216
|
|
|
217
|
+
### Finding the Session Behind a Commit or Line
|
|
218
|
+
|
|
219
|
+
Use this workflow when the user asks which Polygraph session produced, is behind, or changed a particular commit — or a particular line of code.
|
|
220
|
+
|
|
221
|
+
**Given a commit sha.** When the user names a sha, or asks what session is behind a commit, resolve it with `search_sessions` using the `sha` parameter (CLI: `polygraph session search --sha <sha>`):
|
|
222
|
+
|
|
223
|
+
- Pass **exactly one** of `query` or `sha` — they are mutually exclusive.
|
|
224
|
+
- `sha` accepts a full or partial sha, 7-40 hex chars.
|
|
225
|
+
- The lookup is exact and one-shot: it returns the session(s) linked to that commit, newest first, scoped to the current org, and only explicit sessions.
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
search_sessions(sha: "a1b2c3d")
|
|
229
|
+
# CLI equivalent:
|
|
230
|
+
polygraph session search --sha a1b2c3d
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Given a line number.** There is no line-number lookup — a line MUST first be resolved to a commit sha with `git blame`, then that sha is fed into the sha lookup:
|
|
234
|
+
|
|
235
|
+
1. `git blame -L <line>,<line> -- <file>` to get the commit that last touched the line.
|
|
236
|
+
2. Pass that sha to `search_sessions(sha: ...)` (or `polygraph session search --sha <sha>`).
|
|
237
|
+
|
|
238
|
+
**Reading the results.**
|
|
239
|
+
|
|
240
|
+
- Multiple sessions may match a sha. They come back newest first — pick the most relevant one and report the others if they matter.
|
|
241
|
+
- **A "no match" result does NOT prove the commit had no work behind it.** Not every commit is linked to an explicit session: commits pushed directly (rather than via an ingested PR) and gaps in ingestion metadata mean the sha may simply not be recorded, and implicit sessions are never returned. Report "no linked session found for that sha" — never assert that no work exists behind the commit.
|
|
242
|
+
|
|
216
243
|
## Simple tasks (fire-and-forget)
|
|
217
244
|
|
|
218
245
|
Use this pattern when the task is well-defined and the child is not expected to need clarification. It is a single-round delegation: kick it off, poll until terminal, then push branch + create PR.
|
|
@@ -431,7 +458,7 @@ Check the details of a session using `show_session` or `polygraph session show -
|
|
|
431
458
|
- `relatedPRs`: Array of related PR URLs across repos
|
|
432
459
|
- `session.ciStatus`: CI pipeline status keyed by PR ID, each containing:
|
|
433
460
|
- `status`: One of `SUCCEEDED`, `FAILED`, `IN_PROGRESS`, `NOT_STARTED` (null if no CIPE and no external CI)
|
|
434
|
-
- `cipeUrl`: URL to the CI pipeline execution details (null if no CIPE)
|
|
461
|
+
- `cipeUrl`: URL to the CI pipeline execution details (null if no CIPE). This is a human-facing Nx Cloud web link — display it to the user, but never fetch, curl, or poll it directly; CIPE data is only accessible programmatically via the Nx MCP `ci_information` tool
|
|
435
462
|
- `completedAt`: Epoch millis timestamp, set only when the CIPE has completed (null otherwise)
|
|
436
463
|
- `selfHealingStatus`: The self-healing fix status string from Nx Cloud's AI fix feature (null if no AI fix exists)
|
|
437
464
|
- `externalCIRuns`: Array of external CI runs (present when no CIPE but external CI data exists, e.g., GitHub Actions). Each run contains:
|
|
@@ -601,7 +628,7 @@ archive_session(
|
|
|
601
628
|
|
|
602
629
|
Use `get_ci_logs` to retrieve the full plain-text log for a specific CI job. This is the drill-in tool for investigating CI failures after identifying a failed job from the session's CI status.
|
|
603
630
|
|
|
604
|
-
**ONLY use this tool when NO CIPE (CI Pipeline Execution) exists for the PR.** When a CIPE exists (`ciStatus[prId].cipeUrl` is non-null), logs and failure data are available through the CIPE system (Nx Cloud) via `ci_information` — do NOT call `get_ci_logs
|
|
631
|
+
**ONLY use this tool when NO CIPE (CI Pipeline Execution) exists for the PR.** When a CIPE exists (`ciStatus[prId].cipeUrl` is non-null), logs and failure data are available through the CIPE system (Nx Cloud) via the Nx MCP `ci_information` tool — do NOT call `get_ci_logs`, and do NOT fetch or poll the `cipeUrl` over HTTP (it is a browser link for the user, not an API). This tool is specifically for PRs where only external CI runs exist (e.g., GitHub Actions runs without an Nx Cloud CIPE).
|
|
605
632
|
|
|
606
633
|
**Parameters:**
|
|
607
634
|
|
|
@@ -14,14 +14,22 @@ You produce debriefs of PAST Polygraph sessions so a parent agent working on a N
|
|
|
14
14
|
|
|
15
15
|
## Procedure
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Speed matters: the parent agent keeps working while it waits for you, and it folds your debrief in whenever it lands.
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
2. `polygraph session logs -s <sessionId> --json` — the full parent transcript (`--json` returns the full transcript by default).
|
|
21
|
-
3. If the session delegated work to other repositories (child-agent steps exist), pull those transcripts too: `polygraph session logs -s <sessionId> --all --json`, or `--repo <org/repo> --json` for one repo.
|
|
22
|
-
4. Write the debrief section (format below) before moving to the next session.
|
|
19
|
+
**Single session (the usual case).** The parent normally launches one background debrief agent per related session, so your input will usually contain exactly one session. Run the "Debriefing one session" steps directly — do not spawn a subagent for a single session.
|
|
23
20
|
|
|
24
|
-
|
|
21
|
+
**Multiple sessions: fan out.** When given more than one session, debrief them CONCURRENTLY, never one after another: spawn one subagent per session, all in a single message so they run in parallel. Give each subagent: its session entry (sessionId, title, url, rank), the current-task statement, the "Debriefing one session" steps, and the per-session output template — copied into its prompt, since it cannot see this skill. Each subagent returns its completed debrief section as its final message. Assemble the returned sections in rank order and return them; do not rewrite them. Only if your environment cannot spawn subagents, run the "Debriefing one session" steps yourself, sequentially in rank order.
|
|
22
|
+
|
|
23
|
+
### Debriefing one session
|
|
24
|
+
|
|
25
|
+
Invoke the CLI as `${POLYGRAPH_CLI:-polygraph}` in every command: when the session was launched from a specific CLI build, `POLYGRAPH_CLI` points at it and children must use the same one. When you copy these steps into a subagent prompt, keep the `${POLYGRAPH_CLI:-polygraph}` form verbatim.
|
|
26
|
+
|
|
27
|
+
1. `${POLYGRAPH_CLI:-polygraph} session show --details <sessionId>` — metadata, description timeline, repositories, PRs. The description timeline often already summarizes goals and outcomes; mine it before reading transcripts.
|
|
28
|
+
2. Duplicate-work check: if the metadata shows this session pursuing the SAME task as the current one (not merely related work) and it is unfinished or recently active, do NOT read the transcripts. Return the debrief section immediately, with `**DUPLICATE WORK IN FLIGHT**` as the first line after the heading, followed by the session's status and last activity, one line of evidence for the match, and what resuming it would restore. The parent halts and asks the user to choose between resuming that session and continuing the current one, so speed matters more than depth here.
|
|
29
|
+
3. `${POLYGRAPH_CLI:-polygraph} session logs -s <sessionId> --all --tail none > "$TMPDIR/<sessionId>-logs.txt" 2>&1` — the parent transcript plus every child transcript, rendered as plain text, in ONE call. Then read the file directly (with offsets for large files). Do NOT fetch `--json` and do NOT query the transcript with node/python one-liners — reading the rendered text is faster and you extract while reading.
|
|
30
|
+
4. Write the debrief section (format below).
|
|
31
|
+
|
|
32
|
+
Large transcripts: read the file in a few large chunks, prioritizing user prompts, assistant text and final messages, tool errors and failure events, and task notifications. Routine tool-use noise (file reads, searches) is safe to skim. Do not make repeated small queries against the transcript; each round trip costs more than reading a bigger chunk.
|
|
25
33
|
|
|
26
34
|
## Output
|
|
27
35
|
|
|
@@ -29,6 +37,7 @@ Return ONE consolidated debrief as your final message — it is consumed by the
|
|
|
29
37
|
|
|
30
38
|
### Rank N — <session title> (<sessionId>)
|
|
31
39
|
|
|
40
|
+
**DUPLICATE WORK IN FLIGHT** — only when the duplicate-work check fired: status, last activity, one-line evidence. Omit this line otherwise.
|
|
32
41
|
**URL:** <session url>
|
|
33
42
|
**Goal:** what the session set out to do.
|
|
34
43
|
**What happened:** condensed narrative of the work performed.
|
|
@@ -44,3 +53,5 @@ Return ONE consolidated debrief as your final message — it is consumed by the
|
|
|
44
53
|
- If a session's logs are hidden or unavailable, say which (`hidden: true` in the CLI output means hidden by the author; empty steps mean no logs uploaded) and debrief from `session show --details` metadata, description timeline, and PRs alone.
|
|
45
54
|
- No speculation: when the transcript does not show why a decision was made, write "rationale not recorded".
|
|
46
55
|
- Read-only: the inspected sessions must be byte-for-byte unaffected by your work.
|
|
56
|
+
- Session data only: debrief from what the CLI and MCP tools return (metadata, description timeline, transcripts, PRs). Do NOT read repository code, run git or gh, or fetch PR diffs to verify claims — report what the session shows and leave verification to the parent.
|
|
57
|
+
- Fail fast: if a CLI command errors, retry it once at most, then report the error verbatim in your debrief section and move on. Do not build workarounds (no copying auth/config to a fake HOME, no POLYGRAPH_ROOT redirection, no privilege or sandbox escapes) — a debrief that says "logs unavailable: <error>" is more useful than one that arrives late.
|