@polygraph/claude-plugin 0.4.43 → 0.4.45
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/agents/polygraph-delegate-subagent.md +2 -2
- package/hooks/hooks.json +0 -9
- package/package.json +1 -1
- package/skills/polygraph/SKILL.md +31 -130
- package/skills/polygraph/reference/ci-job-logs.md +38 -0
- package/skills/polygraph/reference/sandboxing.md +33 -0
- package/skills/polygraph/reference/shallow-clone-history.md +9 -0
- package/hooks/check-plugin-version.mjs +0 -294
|
@@ -26,12 +26,12 @@ The main agent provides these parameters in the prompt:
|
|
|
26
26
|
| `sessionId` | The Polygraph session ID |
|
|
27
27
|
| `repo` | Repository to delegate to (e.g., `org/repo-name`) |
|
|
28
28
|
| `instruction` | The task instruction for the child agent |
|
|
29
|
-
| `role` | (Optional) Agent slot within the repo; omit for the default role. Pass the SAME role on every `spawn_agent`/`show_agent`/`stop_agent` call for this delegation. |
|
|
29
|
+
| `role` | (Optional) Agent slot within the repo; omit for the default role and never invent one. Pass the SAME role on every `spawn_agent`/`show_agent`/`stop_agent` call for this delegation. |
|
|
30
30
|
| `context` | (Optional) Additional context to pass to the child agent |
|
|
31
31
|
|
|
32
32
|
## Delegating work
|
|
33
33
|
|
|
34
|
-
Call the `spawn_agent` tool to start a child agent on the repo or to send a follow-up to an active task. Follow-up routing is automatic per (repo, role): if that (repo, role) already has an active child task (working or paused on input), the orchestrator delivers your `instruction` to that task as a follow-up message rather than starting a second run; otherwise it starts a new child run. A repo therefore has at most one active child per role
|
|
34
|
+
Call the `spawn_agent` tool to start a child agent on the repo or to send a follow-up to an active task. Follow-up routing is automatic per (repo, role): if that (repo, role) already has an active child task (working or paused on input), the orchestrator delivers your `instruction` to that task as a follow-up message rather than starting a second run; otherwise it starts a new child run. A repo therefore has at most one active child per role.
|
|
35
35
|
|
|
36
36
|
`repo` must be a repository other than the one the parent agent is working in — never delegate into the parent's own repo.
|
|
37
37
|
|
package/hooks/hooks.json
CHANGED
|
@@ -24,15 +24,6 @@
|
|
|
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
|
-
]
|
|
36
27
|
}
|
|
37
28
|
]
|
|
38
29
|
}
|
package/package.json
CHANGED
|
@@ -17,25 +17,9 @@ Polygraph connects repositories and the agent work happening across them. Its ce
|
|
|
17
17
|
|
|
18
18
|
## Sandboxing in Polygraph Sessions
|
|
19
19
|
|
|
20
|
-
Polygraph
|
|
20
|
+
Polygraph **may** run an agent session inside an OS-level sandbox, but not every session is sandboxed — whether it is on depends on the user's config. When it is on, writes are limited to the repository working tree and session root, network access is restricted to allowlisted hosts, and binding a listening socket (dev servers) fails with `EPERM`. The user may not know whether this session is sandboxed.
|
|
21
21
|
|
|
22
|
-
**
|
|
23
|
-
|
|
24
|
-
**Warn before attempting known-blocked operations.** Before starting a dev server, anything else that listens on a port, or an operation that needs writes or network access outside the allowlist, tell the user up front that it will not work while sandboxing is on and offer the options below instead of attempting it.
|
|
25
|
-
|
|
26
|
-
**What to tell the user.** Explain that Polygraph runs this session in a sandbox, then present both options. Each takes effect on the next agent launch, so the Polygraph session must be relaunched afterwards:
|
|
27
|
-
|
|
28
|
-
1. **Keep the sandbox on and allow the specific operation** (preferred). The sandbox belongs to the agent harness, so exceptions live in harness settings committed to the repository; array settings merge with Polygraph's generated allowlist rather than replacing it. Add writable paths to `.claude/settings.json` (or `.claude/settings.local.json`):
|
|
29
|
-
|
|
30
|
-
```json
|
|
31
|
-
{ "sandbox": { "filesystem": { "allowWrite": ["<path>"] } } }
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
The `.claude/` directory is read-only inside the sandbox, so give the user the exact snippet to commit — you cannot apply it yourself.
|
|
35
|
-
|
|
36
|
-
2. **Turn sandboxing off.** The user quits the agent, runs `polygraph config`, toggles **Agent Options → Claude → sandbox** off (or per-repo under **Repo Options**), and relaunches the Polygraph session. Non-interactive alternative: set `agentOptions.claude.sandbox: false` (or `repoOptions."org/repo".sandbox: false`) in `~/.polygraph/config.json`. Warn that this removes filesystem isolation — the agent can then write anywhere the OS user can.
|
|
37
|
-
|
|
38
|
-
**Never blame the tooling.** A sandbox denial means the environment blocked the operation — not that the repo, tool, or framework is broken. Do not record conclusions like "X is unusable" in memory, session descriptions, or messages based on sandboxed failures.
|
|
22
|
+
**When something fails in a sandbox-shaped way** — `EPERM` binding a port, a blocked network host, a denied write to an ordinary path — the sandbox blocked it. Do NOT retry variations, work around it, or route the command through `!`-prefixed user commands (those run in the same sandbox); one failure is enough evidence. Stop, and read [`reference/sandboxing.md`](reference/sandboxing.md) for how to warn the user, the two remediation options (allow the specific operation via committed harness settings, or turn sandboxing off), and the exact per-harness config snippets. Never conclude the repo, tool, or framework is broken based on a sandboxed failure.
|
|
39
23
|
|
|
40
24
|
## Available Tools
|
|
41
25
|
|
|
@@ -45,29 +29,29 @@ Polygraph functionality is available via both MCP tools and CLI commands. Use wh
|
|
|
45
29
|
| --- | --- | --- |
|
|
46
30
|
| `list_repos` | `polygraph repo list` | Discover candidate repositories. Candidate entries do not include repository descriptions; use `semanticQuery` for natural-language discovery. |
|
|
47
31
|
| `start_session` | `polygraph session start --repo <ids>` | Initialize a Polygraph session with selected repositories |
|
|
48
|
-
| `spawn_agent` | — | Start a
|
|
49
|
-
| `show_agent` | — | Poll one repo's child status (
|
|
50
|
-
| `stop_agent` | — | Cancel an in-progress child
|
|
32
|
+
| `spawn_agent` | — | Start a child task, or send a follow-up to an active task, in another repository. A repeat call for the same (repo, role) is delivered to that task as a follow-up; otherwise a new child starts. Roles and resume behavior are under "Multi-turn tasks". |
|
|
33
|
+
| `show_agent` | — | Poll one repo's child status (one repo per call; `role` narrows to that agent). Status enum and the poll/state-machine flow are under "Multi-turn tasks". |
|
|
34
|
+
| `stop_agent` | — | Cancel an in-progress child; its session is preserved for later read-only context restoration. |
|
|
51
35
|
| `push_branch` | — | Push a local git branch to the remote repository. For the repo you are in, this pushes from your current checkout. Requires a session description. |
|
|
52
36
|
| `create_pr` | — | Create draft PRs with session metadata linking related PRs |
|
|
53
37
|
| `show_session` | `polygraph session show <id> [--details]` | Query status of the current session. Use details when session summary, repo IDs, PR URLs, and PR descriptions are needed. |
|
|
54
|
-
| `update_session` | `polygraph session update --session <id> [--title] [--description]` | Update the session title and/or description (at least one required)
|
|
38
|
+
| `update_session` | `polygraph session update --session <id> [--title] [--description]` | Update the session title and/or description (at least one required); metadata only, independent of PR creation or mark-ready. |
|
|
55
39
|
| `link_reference` | — | Link an external reference to a session. |
|
|
56
40
|
| `mark_pr_ready` | — | Mark draft PRs as ready for review |
|
|
57
41
|
| `associate_pr` | — | Associate an existing PR with a session |
|
|
58
|
-
| `add_repo` | — | Add repositories to a running
|
|
42
|
+
| `add_repo` | — | Add repositories to a running session (pass exact refs directly, skipping `list_repos`). See "Add Repositories to a Session". |
|
|
59
43
|
| `archive_session` | `polygraph session archive <id>` | Archive a session, hiding it from active lists (it can still be resumed) |
|
|
60
44
|
| `get_ci_logs` | — | Retrieve full plain-text log for a specific CI job |
|
|
61
|
-
| `git_fetch` | `polygraph git fetch` | Fetch
|
|
45
|
+
| `git_fetch` | `polygraph git fetch` | Fetch git history for a shallow session clone when git fails with "bad object" or missing-commit errors. See "Fetching Git History for Shallow Clones". |
|
|
62
46
|
| `login` | `polygraph auth login [--token]` | Authenticate with Polygraph (use `--token` for headless/CI) |
|
|
63
47
|
| `logout` | `polygraph auth logout` | Log out of Polygraph |
|
|
64
48
|
| `list_sessions` | `polygraph session list` | List sessions. By default only active sessions created by the current git user; pass `recommendedFilters: false` for all sessions. |
|
|
65
|
-
| `search_sessions` | `polygraph session search` | Find sessions by free-text `query` OR by commit `sha` — pass
|
|
49
|
+
| `search_sessions` | `polygraph session search` | Find sessions by free-text `query` OR by commit `sha` — pass exactly one. See "Finding the Session Behind a Commit or Line". |
|
|
66
50
|
| `list_accounts` | `polygraph account list` | List available organizations |
|
|
67
51
|
| `select_account` | `polygraph account select` | Select the organization that future commands run against |
|
|
68
52
|
| `whoami` | `polygraph whoami` | Show current auth status and org |
|
|
69
53
|
|
|
70
|
-
**Delegation rules:** `list_repos` and `start_session` MUST be called via the `polygraph-init-subagent` as described in
|
|
54
|
+
**Delegation rules:** `list_repos` and `start_session` MUST be called via the `polygraph-init-subagent` as described in the "Initialize or Join Polygraph Session" section. Direct `add_repo` is allowed only when the user provides exact repo refs for an existing session. `spawn_agent` and `show_agent` MUST ALWAYS be called via background Task subagents (`run_in_background: true`) as described in the delegation sections below — NEVER call them directly in the main conversation. The subagents are plugin-namespaced: pass `subagent_type: "polygraph:polygraph-init-subagent"` / `"polygraph:polygraph-delegate-subagent"`; fall back to the bare name only if the namespaced form is not found.
|
|
71
55
|
|
|
72
56
|
## CLI Statefulness
|
|
73
57
|
|
|
@@ -96,7 +80,7 @@ After logging in (or if logged in but no org is selected), use `polygraph accoun
|
|
|
96
80
|
The delegate/monitor/stop steps apply only when working across repos. A single-repo session skips them and still benefits from shared progress, resume, and CI visibility.
|
|
97
81
|
|
|
98
82
|
0. **Initialize or join Polygraph session** - If you were spawned inside an existing session (the startup banner names a session ID), reuse it. Call `show_session` first; if it already has repos and the user did not ask to add more, you're done. If the user asks to add exact repo refs, call `add_repo` directly with those refs and skip candidate discovery. If the session has no repos and no exact refs were provided, launch the `polygraph-init-subagent` with that `sessionId` so it discovers candidates and uses `add_repo` (NOT `start_session`). Only when there is no session ID at all should the init subagent create a new session.
|
|
99
|
-
1. **Delegate work to each repo** - Use the `polygraph-delegate-subagent` to start child agents in other repositories. Delegate only to *other* repos — never to the repo you are in; work on it directly (your regular subagents are fine for local work — only Polygraph delegation is reserved for other repos). Parallel delegation across repos is encouraged
|
|
83
|
+
1. **Delegate work to each repo** - Use the `polygraph-delegate-subagent` to start child agents in other repositories. Delegate only to *other* repos — never to the repo you are in; work on it directly (your regular subagents are fine for local work — only Polygraph delegation is reserved for other repos). Parallel delegation across repos is encouraged. Choose the Simple (fire-and-forget) or Multi-turn (interactive) pattern described below based on whether the child may need clarification.
|
|
100
84
|
|
|
101
85
|
4. **Monitor child agents** - Use `show_agent` to poll one repo's children (`repo` is required; pass `role` to narrow to one agent) and read each entry's `status` and `lastOutputLines` from the `children[]` array.
|
|
102
86
|
5. **Stop child agents** (if needed) - Use `stop_agent` (with `role` when targeting a non-default agent) to cancel an in-progress child agent. The agent's session is preserved for later read-only context restoration; after a resume, wait for explicit user instructions before making changes.
|
|
@@ -110,7 +94,7 @@ The delegate/monitor/stop steps apply only when working across repos. A single-r
|
|
|
110
94
|
|
|
111
95
|
## Step-by-Step Guide
|
|
112
96
|
|
|
113
|
-
###
|
|
97
|
+
### Initialize or Join Polygraph Session
|
|
114
98
|
|
|
115
99
|
There are three cases. Pick exactly one before calling any tool. The case labels are internal routing shorthand — never mention them in anything you show the user.
|
|
116
100
|
|
|
@@ -131,23 +115,7 @@ In case B, call `add_repo` yourself when exact repo refs were provided; otherwis
|
|
|
131
115
|
- The parent conversation is responsible for detecting an existing session ID from current context, the startup banner, or a user-provided session URL/ID, then passing it explicitly to `polygraph-init-subagent`. The init subagent cannot infer parent session context by itself.
|
|
132
116
|
- For a fresh Codex Desktop conversation started with `/polygraph:session-start`, no `sessionId` is expected; launch `polygraph-init-subagent` without `sessionId` so it creates a new session.
|
|
133
117
|
|
|
134
|
-
**Launch the init subagent** (cases B and C — skip in case A):
|
|
135
|
-
|
|
136
|
-
```
|
|
137
|
-
Task(
|
|
138
|
-
subagent_type: "polygraph:polygraph-init-subagent",
|
|
139
|
-
description: "Init Polygraph session",
|
|
140
|
-
prompt: """
|
|
141
|
-
Parameters:
|
|
142
|
-
- sessionId: "<existing-session-id-or-omit-for-new-session>"
|
|
143
|
-
- userContext: "<description of what the user wants to do>"
|
|
144
|
-
|
|
145
|
-
If sessionId is provided, reuse that session and use add_repo to attach repositories — do NOT call start_session. If exact repo refs were provided, pass them directly to add_repo and do NOT call list_repos. If discovery is needed, discover candidates and select relevant repos. If sessionId is omitted, create a new session via start_session. Return a structured summary.
|
|
146
|
-
"""
|
|
147
|
-
)
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
Omit the `sessionId` line for case C. Include it (with the existing session ID) for case B.
|
|
118
|
+
**Launch the init subagent** (cases B and C — skip in case A) as a `Task` with `subagent_type: "polygraph:polygraph-init-subagent"`. Pass `userContext` (what the user wants to do); for case B also pass the existing `sessionId`. Instruct it: with a `sessionId`, reuse that session and attach repos via `add_repo`, never `start_session`; if exact repo refs were given, pass them straight to `add_repo` without `list_repos`; if discovery is needed, discover and select candidates; with no `sessionId`, create the session via `start_session`. It returns a structured summary.
|
|
151
119
|
|
|
152
120
|
The subagent will:
|
|
153
121
|
|
|
@@ -245,6 +213,7 @@ polygraph session search --sha a1b2c3d
|
|
|
245
213
|
|
|
246
214
|
A repository in a session can host multiple child agents at once, distinguished by **role**:
|
|
247
215
|
|
|
216
|
+
- **Omit by default.** Set a `role` only when the user very explicitly asked for a named one, or when a skill the user invoked prescribes one (e.g. `adversarial-review` uses `reviewer`). Never pick one yourself.
|
|
248
217
|
- **Purpose.** Roles let independent streams of work run concurrently in one repo — e.g. a default agent implementing a feature while a `reviewer` or `ci-investigator` runs alongside. Each (repo, role) pair has at most one active child.
|
|
249
218
|
- **Default role.** An omitted `role` means the default role: `spawn_agent` without `role` starts or follows up with the repo's default-role agent.
|
|
250
219
|
- **Logs.** Only default-role agents upload logs to the cloud and appear in the multiplexed log stream (`polygraph session logs`). Inspect non-default agents locally with `polygraph agent attach --role <role>`.
|
|
@@ -253,33 +222,14 @@ A repository in a session can host multiple child agents at once, distinguished
|
|
|
253
222
|
|
|
254
223
|
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.
|
|
255
224
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
1. Launch a background `Task` subagent per repo using `polygraph-delegate-subagent`. The subagent calls `spawn_agent`, then polls `show_agent` via chained `waitForTransitionMs` long-poll calls until terminal.
|
|
225
|
+
Delegate through a background `Task` subagent rather than calling `spawn_agent`/`show_agent` in the main conversation — direct calls flood the context with polling noise. This is a hard requirement, not a suggestion.
|
|
259
226
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
description: "Delegate to <repo-name>",
|
|
265
|
-
prompt: """
|
|
266
|
-
Parameters:
|
|
267
|
-
- sessionId: "<session-id>"
|
|
268
|
-
- repo: "<org/repo-name>"
|
|
269
|
-
- instruction: "<the task instruction>"
|
|
270
|
-
- role: "<optional role>"
|
|
271
|
-
- context: "<optional context>"
|
|
272
|
-
|
|
273
|
-
Delegate the work, poll for completion, and return a structured summary.
|
|
274
|
-
"""
|
|
275
|
-
)
|
|
276
|
-
```
|
|
227
|
+
1. Launch one background `Task` per repo with `subagent_type: "polygraph:polygraph-delegate-subagent"` and `run_in_background: true`, passing `sessionId`, `repo`, `instruction`, and optional `role`/`context`. The subagent calls `spawn_agent`, then polls `show_agent` via chained `waitForTransitionMs` long-poll calls until terminal.
|
|
228
|
+
2. Delegate to several repos in parallel by launching multiple background Tasks at once — one delegation per (repo, role). Read their output files later for progress.
|
|
229
|
+
3. Each subagent watches `child.status` on its `children[]` entry (matching its repo and role) and exits at a terminal status — `'completed'`, `'failed'`, or `'cancelled'`.
|
|
230
|
+
4. Once all report terminal, continue to `push_branch` + `create_pr`.
|
|
277
231
|
|
|
278
|
-
|
|
279
|
-
3. The subagent watches `child.status` on its delegation's `children[]` entry — the one matching its repo and role — and exits when it sees a terminal status — typically `'completed'` or `'failed'` (and `'cancelled'` if it was stopped).
|
|
280
|
-
4. Once all background subagents report a terminal status, continue to `push_branch` + `create_pr`.
|
|
281
|
-
|
|
282
|
-
In rare cases where you need to check the raw child agent status directly (e.g., debugging a stuck subagent), you may call `show_agent` as a one-off tool call. Do NOT use this for regular polling — that MUST happen in background subagents.
|
|
232
|
+
To debug a stuck subagent you can call `show_agent` as a one-off, but routine polling belongs in the background subagents.
|
|
283
233
|
|
|
284
234
|
Use Simple when the task is well-defined and the child will not need clarification.
|
|
285
235
|
|
|
@@ -340,7 +290,9 @@ If you call `allow_agent` while the dialog is already open, you create a race: t
|
|
|
340
290
|
|
|
341
291
|
The `allow_agent` and `deny_agent` tools exist for parents whose MCP clients do NOT advertise elicitation capability (opencode TUI today). They are not part of your flow.
|
|
342
292
|
|
|
343
|
-
|
|
293
|
+
## Publishing and Session Management
|
|
294
|
+
|
|
295
|
+
### Publish Changes (Push Branches, Create PRs, Mark Ready)
|
|
344
296
|
|
|
345
297
|
Publishing covers the branch-to-PR flow: `push_branch` (push local commits; must precede PR creation), `create_pr` (linked draft PRs, including fork PRs via `targetRepository`), `mark_pr_ready` (transition drafts to OPEN), and `associate_pr` (link PRs created outside Polygraph).
|
|
346
298
|
|
|
@@ -352,7 +304,7 @@ Publishing covers the branch-to-PR flow: `push_branch` (push local commits; must
|
|
|
352
304
|
|
|
353
305
|
**Whenever you write or update a session description, read [`reference/session-description.md`](reference/session-description.md) first.** That reference file holds the full policy: the canonical Markdown-heading template (`## Goal` / `## Current progress` / `## What worked` / `## Next steps`), the dual-audience guidance (humans in the web UI now, agents reconstructing history later), and the formatting building blocks the app renders (callouts, tables, mermaid, links, `link_reference`).
|
|
354
306
|
|
|
355
|
-
###
|
|
307
|
+
### Get Current Polygraph Session
|
|
356
308
|
|
|
357
309
|
Check the details of a session using `show_session` or `polygraph session show --details <session-id>`. Returns the full session state — basic metadata like id, url & description timeline, plus the connected repositories, `pullRequests[]`, per-PR `ciStatus`, and `session.linkedReferences`.
|
|
358
310
|
|
|
@@ -410,7 +362,7 @@ link_reference({
|
|
|
410
362
|
|
|
411
363
|
The canonical MCP parameters are `{ sessionId, reference }`. There is no unlink command.
|
|
412
364
|
|
|
413
|
-
###
|
|
365
|
+
### Add Repositories to a Session
|
|
414
366
|
|
|
415
367
|
Use `add_repo` to add repositories to an existing Polygraph session after it has already started.
|
|
416
368
|
|
|
@@ -430,27 +382,11 @@ add_repo(
|
|
|
430
382
|
)
|
|
431
383
|
```
|
|
432
384
|
|
|
433
|
-
###
|
|
385
|
+
### Archive Session
|
|
434
386
|
|
|
435
387
|
**IMPORTANT: Only call this tool when the user explicitly asks to archive or close the session.** Do not archive sessions automatically as part of the workflow.
|
|
436
388
|
|
|
437
|
-
Use `archive_session` (CLI: `polygraph session archive <id>`) to archive the session. Archiving only hides the session from active lists — it can still be resumed and interacted with afterwards. It is idempotent — archiving an already-archived session returns success.
|
|
438
|
-
|
|
439
|
-
**Parameters:**
|
|
440
|
-
|
|
441
|
-
- `sessionId` (required): The Polygraph session ID
|
|
442
|
-
- `clean` (optional): Remove the local clones Polygraph created for delegated repos after archiving
|
|
443
|
-
|
|
444
|
-
**Returns:**
|
|
445
|
-
|
|
446
|
-
- `sessionId`: The session ID
|
|
447
|
-
- `completed`: Boolean indicating the session is archived
|
|
448
|
-
|
|
449
|
-
```
|
|
450
|
-
archive_session(
|
|
451
|
-
sessionId: "<session-id>"
|
|
452
|
-
)
|
|
453
|
-
```
|
|
389
|
+
Use `archive_session` (CLI: `polygraph session archive <id>`) to archive the session. Archiving only hides the session from active lists — it can still be resumed and interacted with afterwards. It is idempotent — archiving an already-archived session returns success. Pass the optional `clean` flag to also remove the local clones Polygraph created for delegated repos.
|
|
454
390
|
|
|
455
391
|
**When to call:** all work is finished, PRs are created and marked ready, and the user explicitly confirms they are done with the session.
|
|
456
392
|
|
|
@@ -458,46 +394,13 @@ archive_session(
|
|
|
458
394
|
|
|
459
395
|
### Retrieving CI Job Logs
|
|
460
396
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
**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).
|
|
464
|
-
|
|
465
|
-
**Parameters:**
|
|
466
|
-
|
|
467
|
-
- `sessionId` (required): The Polygraph session ID
|
|
468
|
-
- `repoId` (required): Repository ID (MongoDB ObjectId hex string, from the session repository entry)
|
|
469
|
-
- `jobId` (required): GitHub Actions job ID (from `ciStatus[prId].externalCIRuns[].jobs[].jobId` in the `show_session` response)
|
|
397
|
+
`get_ci_logs` retrieves the full plain-text log for a specific CI job — the drill-in tool for investigating a failed job. **ONLY use it when NO CIPE (CI Pipeline Execution) exists for the PR** (`ciStatus[prId].cipeUrl` is null); when a CIPE exists, use the Nx MCP `ci_information` tool instead, and do NOT fetch or poll the `cipeUrl` over HTTP.
|
|
470
398
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
- On success: `{ success: true, jobId: number, logFile: string, sizeBytes: number }`
|
|
474
|
-
- On failure: `{ success: false, error: string }`
|
|
475
|
-
|
|
476
|
-
The tool saves the log to a local temp file and returns the path in `logFile`. Use the `Read` tool to examine the file contents. For large logs, use `offset` and `limit` parameters to read specific sections.
|
|
477
|
-
|
|
478
|
-
```
|
|
479
|
-
get_ci_logs(
|
|
480
|
-
sessionId: "<session-id>",
|
|
481
|
-
repoId: "<repo-id>",
|
|
482
|
-
jobId: 12345678
|
|
483
|
-
)
|
|
484
|
-
// Returns: { success: true, jobId: 12345678, logFile: "/tmp/ci-logs/job-12345678.log", sizeBytes: 152340 }
|
|
485
|
-
// Then: Read(logFile) to examine the log
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
**Typical flow:**
|
|
489
|
-
|
|
490
|
-
1. Use `show_session` to see PR CI status
|
|
491
|
-
2. Check `ciStatus[prId].cipeUrl` — if a CIPE exists, use `ci_information` for logs and skip this tool
|
|
492
|
-
3. If NO CIPE exists, check `ciStatus[prId].externalCIRuns` — examine runs and jobs directly from the session data
|
|
493
|
-
4. For a failed job, call `get_ci_logs(sessionId, repoId, jobId)` to save the log to a file
|
|
494
|
-
5. Use `Read(logFile)` to examine the log content — use `offset`/`limit` for large files
|
|
495
|
-
|
|
496
|
-
**Important:** Logs can be large (100KB+). Only fetch logs for failed or relevant jobs, and read only the sections you need.
|
|
399
|
+
When you need to fetch and read a failed job's log, read [`reference/ci-job-logs.md`](reference/ci-job-logs.md) for the parameters, return shape, and the full flow (identify the job from `externalCIRuns`, call `get_ci_logs`, then `Read` the saved log file).
|
|
497
400
|
|
|
498
401
|
### Fetching Git History for Shallow Clones
|
|
499
402
|
|
|
500
|
-
Session repos are shallow (`--depth 1`) clones
|
|
403
|
+
Session repos are shallow (`--depth 1`) clones. When git fails on missing history (`bad object` from `git revert`, `git log`, `git blame`, etc.), call `git_fetch({ sessionId, repo })` and retry. Read [`reference/shallow-clone-history.md`](reference/shallow-clone-history.md) for the CLI form, the `depth`/`refs` options, and the redundant-call behavior.
|
|
501
404
|
|
|
502
405
|
### Update Session Description
|
|
503
406
|
|
|
@@ -533,7 +436,7 @@ If the session has a description timeline, also display:
|
|
|
533
436
|
|
|
534
437
|
## Best Practices
|
|
535
438
|
|
|
536
|
-
1. **
|
|
439
|
+
1. **Delegate via background subagents** — run every `spawn_agent`/`show_agent` through `Task(run_in_background: true)`; direct calls flood the context with polling noise.
|
|
537
440
|
|
|
538
441
|
1. **Poll child status before proceeding** — Always verify child agents have reached a terminal `child.status` (`'completed'`, `'failed'`, or `'cancelled'`) via `show_agent` before pushing branches or creating PRs
|
|
539
442
|
1. **Link PRs in descriptions** - Reference related PRs in each PR body
|
|
@@ -542,8 +445,6 @@ If the session has a description timeline, also display:
|
|
|
542
445
|
1. **Test integration** before marking PRs ready
|
|
543
446
|
1. **Coordinate merge order** if there are deployment dependencies
|
|
544
447
|
|
|
545
|
-
1. **NEVER call `spawn_agent` or `show_agent` directly**. These MUST ALWAYS go through background Task subagents (`run_in_background: true`).
|
|
546
|
-
|
|
547
448
|
1. **Use `stop_agent` to clean up** — Stop child agents that are stuck or no longer needed (pass `role` to target a non-default agent). The child's session is preserved (`sessionPreserved: true`) so the context can be restored later, but after resuming you must wait for explicit user instructions before making changes.
|
|
548
449
|
1. **Only archive sessions when asked** — Only call `archive_session` when the user explicitly requests it. Archiving hides the session from active lists; it can still be resumed later.
|
|
549
450
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Retrieving CI Job Logs
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
**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).
|
|
6
|
+
|
|
7
|
+
**Parameters:**
|
|
8
|
+
|
|
9
|
+
- `sessionId` (required): The Polygraph session ID
|
|
10
|
+
- `repoId` (required): Repository ID (MongoDB ObjectId hex string, from the session repository entry)
|
|
11
|
+
- `jobId` (required): GitHub Actions job ID (from `ciStatus[prId].externalCIRuns[].jobs[].jobId` in the `show_session` response)
|
|
12
|
+
|
|
13
|
+
**Returns:**
|
|
14
|
+
|
|
15
|
+
- On success: `{ success: true, jobId: number, logFile: string, sizeBytes: number }`
|
|
16
|
+
- On failure: `{ success: false, error: string }`
|
|
17
|
+
|
|
18
|
+
The tool saves the log to a local temp file and returns the path in `logFile`. Use the `Read` tool to examine the file contents. For large logs, use `offset` and `limit` parameters to read specific sections.
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
get_ci_logs(
|
|
22
|
+
sessionId: "<session-id>",
|
|
23
|
+
repoId: "<repo-id>",
|
|
24
|
+
jobId: 12345678
|
|
25
|
+
)
|
|
26
|
+
// Returns: { success: true, jobId: 12345678, logFile: "/tmp/ci-logs/job-12345678.log", sizeBytes: 152340 }
|
|
27
|
+
// Then: Read(logFile) to examine the log
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Typical flow:**
|
|
31
|
+
|
|
32
|
+
1. Use `show_session` to see PR CI status
|
|
33
|
+
2. Check `ciStatus[prId].cipeUrl` — if a CIPE exists, use `ci_information` for logs and skip this tool
|
|
34
|
+
3. If NO CIPE exists, check `ciStatus[prId].externalCIRuns` — examine runs and jobs directly from the session data
|
|
35
|
+
4. For a failed job, call `get_ci_logs(sessionId, repoId, jobId)` to save the log to a file
|
|
36
|
+
5. Use `Read(logFile)` to examine the log content — use `offset`/`limit` for large files
|
|
37
|
+
|
|
38
|
+
**Important:** Logs can be large (100KB+). Only fetch logs for failed or relevant jobs, and read only the sections you need.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Sandboxing in Polygraph Sessions
|
|
2
|
+
|
|
3
|
+
Polygraph may run an agent session inside an OS-level sandbox, but not every session is sandboxed — whether it is on depends on the user's config (globally, per-agent, or per-repo). When it is on, writes are limited to the repository working tree, the session root (`~/.polygraph/sessions/<session-id>/`), the system temp directory, and a few allowlisted directories; network access is restricted to allowlisted hosts. In practice: binding a listening socket is denied (dev servers fail with `EPERM`), localhost servers are unreachable, and writes outside the allowlist are rejected. The user may not know whether this session is sandboxed.
|
|
4
|
+
|
|
5
|
+
**Recognize sandbox denials — do not retry or work around them.** When a command fails in a sandbox-shaped way (`EPERM` binding a port, a blocked network host, a denied write to an ordinary path), the sandbox blocked it. Do NOT retry variations, escalate through workarounds, or route the command through `!`-prefixed user commands — those run inside the same sandbox. One failure is enough evidence; stop and inform the user.
|
|
6
|
+
|
|
7
|
+
**Warn before attempting known-blocked operations.** Before starting a dev server, anything else that listens on a port, or an operation that needs writes or network access outside the allowlist, tell the user up front that it will not work while sandboxing is on and offer the options below instead of attempting it.
|
|
8
|
+
|
|
9
|
+
**What to tell the user.** Explain that Polygraph runs this session in a sandbox, then present both options. Each takes effect on the next agent launch, so the Polygraph session must be relaunched afterwards:
|
|
10
|
+
|
|
11
|
+
1. **Keep the sandbox on and allow the specific operation** (preferred). The sandbox belongs to the agent harness, so exceptions live in harness settings committed to the repository; array settings merge with Polygraph's generated allowlist rather than replacing it.
|
|
12
|
+
|
|
13
|
+
**If you are the Claude parent** — add writable paths to `.claude/settings.json` (or `.claude/settings.local.json`):
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{ "sandbox": { "filesystem": { "allowWrite": ["<path>"] } } }
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The `.claude/` directory is read-only inside the sandbox, so give the user the exact snippet to commit — you cannot apply it yourself.
|
|
20
|
+
|
|
21
|
+
**If you are the Codex parent** — add writable roots or network access to `.codex/config.toml`:
|
|
22
|
+
|
|
23
|
+
```toml
|
|
24
|
+
[sandbox_workspace_write]
|
|
25
|
+
network_access = true
|
|
26
|
+
writable_roots = ["<path>"]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The `.codex/` directory is read-only inside the sandbox, so give the user the exact snippet to commit — you cannot apply it yourself.
|
|
30
|
+
|
|
31
|
+
2. **Turn sandboxing off.** The user quits the agent, runs `polygraph config`, toggles **Agent Options → Claude** (or **→ Codex**) **→ sandbox** off (or per-repo under **Repo Options**), and relaunches the Polygraph session. Non-interactive alternative: set `agentOptions.claude.sandbox: false` (or `agentOptions.codex.sandbox: false`, or `repoOptions."org/repo".sandbox: false`) in `~/.polygraph/config.json`. Warn that this removes filesystem isolation — the agent can then write anywhere the OS user can.
|
|
32
|
+
|
|
33
|
+
**Never blame the tooling.** A sandbox denial means the environment blocked the operation — not that the repo, tool, or framework is broken. Do not record conclusions like "X is unusable" in memory, session descriptions, or messages based on sandboxed failures.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Fetching Git History for Shallow Clones
|
|
2
|
+
|
|
3
|
+
Session repos are shallow (`--depth 1`) clones, and plain `git fetch --unshallow` fails on private repos (the clone-time credential is not retained).
|
|
4
|
+
|
|
5
|
+
When git fails on missing history — `bad object` from `git revert`, `git log`, `git blame`, etc. — call `git_fetch({ sessionId, repo })` (CLI: `polygraph git fetch <repo> --session <id> --json`), then retry the git command.
|
|
6
|
+
|
|
7
|
+
- Defaults fetch the default branch's full history.
|
|
8
|
+
- Pass `depth` for a bounded fetch, or `refs` to include extra branches.
|
|
9
|
+
- Safe to call redundantly — returns `alreadyComplete: true` when the history is already present.
|
|
@@ -1,294 +0,0 @@
|
|
|
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
|
-
}
|