@polygraph/cursor-plugin 0.4.51 → 0.4.53
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { appendFileSync, mkdirSync, renameSync, statSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
|
|
6
6
|
const HOOK_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
@@ -8,6 +8,13 @@ const HOOK_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
|
8
8
|
const AGENT_TYPES = new Set(['claude', 'codex', 'opencode', 'cursor']);
|
|
9
9
|
const COMMAND_HOOK_TOOL = /^mcp__(?:plugin_polygraph_)?polygraph[-_]mcp__/;
|
|
10
10
|
const OPENCODE_TOOL = /^polygraph(?:(?:-|_)mcp)?_/;
|
|
11
|
+
// Cursor reports MCP tools as `MCP:<tool>` with no server namespace, so the
|
|
12
|
+
// shim filters on the claim-worthy tool names to avoid spawning the CLI on
|
|
13
|
+
// every unrelated MCP call. This list is an optimization mirror of
|
|
14
|
+
// PARENT_CLAIM_POLICIES in the Polygraph CLI (parent-session-claim-evidence);
|
|
15
|
+
// classification authority stays in the CLI.
|
|
16
|
+
const CURSOR_MCP_CLAIM_TOOL =
|
|
17
|
+
/^MCP:(?:add_repo|allow_agent|archive_session|associate_pr|create_pr|deny_agent|git_fetch|link_reference|mark_pr_ready|pack_and_copy|push_branch|spawn_agent|start_session|stop_agent|update_session|upload_artifact)$/;
|
|
11
18
|
|
|
12
19
|
function nonEmptyString(value) {
|
|
13
20
|
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
@@ -30,6 +37,7 @@ export function buildLinkAgentSessionArgs({
|
|
|
30
37
|
transcriptPath,
|
|
31
38
|
pid,
|
|
32
39
|
source,
|
|
40
|
+
hookOperation,
|
|
33
41
|
}) {
|
|
34
42
|
const session = nonEmptyString(polygraphSessionId);
|
|
35
43
|
const harnessSession = nonEmptyString(agentSessionId);
|
|
@@ -52,14 +60,38 @@ export function buildLinkAgentSessionArgs({
|
|
|
52
60
|
args.push('--pid', String(pid));
|
|
53
61
|
}
|
|
54
62
|
|
|
63
|
+
// Cursor post-tool evidence rides the hook payload (the transcript stores
|
|
64
|
+
// no tool results); forwarded verbatim, classified by the CLI. The
|
|
65
|
+
// operation travels on STDIN, never argv: toolInput can carry an entire
|
|
66
|
+
// upload_artifact document and Linux caps one argv string at 128KB, so an
|
|
67
|
+
// inline argument would kill the spawn with E2BIG and silently lose the
|
|
68
|
+
// evidence. The flag tells the CLI to read stdin; older strict CLIs
|
|
69
|
+
// reject it, which is why publication is gated on the Ocean deployment.
|
|
70
|
+
let input;
|
|
71
|
+
if (hookOperation && typeof hookOperation === 'object') {
|
|
72
|
+
args.push('--hook-operation-stdin');
|
|
73
|
+
input = JSON.stringify(hookOperation);
|
|
74
|
+
}
|
|
75
|
+
|
|
55
76
|
args.push('--source', claimSource);
|
|
56
|
-
return args;
|
|
77
|
+
return { args, input };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Node runtime for the JS-entry fallback. This shim also runs inside
|
|
82
|
+
* non-Node hosts (the opencode plugin executes it in-process, and opencode
|
|
83
|
+
* is a compiled Bun binary), where process.execPath is not a Node
|
|
84
|
+
* executable — fall back to PATH resolution there.
|
|
85
|
+
*/
|
|
86
|
+
function nodeRuntime() {
|
|
87
|
+
const base = basename(process.execPath).toLowerCase();
|
|
88
|
+
return base === 'node' || base === 'node.exe' ? process.execPath : 'node';
|
|
57
89
|
}
|
|
58
90
|
|
|
59
91
|
export function linkAgentSession(claim, spawn = spawnSync, env = process.env) {
|
|
60
92
|
if (isManagedChildEnvironment(env)) return false;
|
|
61
93
|
|
|
62
|
-
const args = buildLinkAgentSessionArgs(claim);
|
|
94
|
+
const { args, input } = buildLinkAgentSessionArgs(claim);
|
|
63
95
|
const command = nonEmptyString(env?.POLYGRAPH_CLI) ?? 'polygraph';
|
|
64
96
|
const commandEnv = nonEmptyString(claim.polygraphSessionId) ? env : { ...env };
|
|
65
97
|
if (commandEnv !== env) {
|
|
@@ -67,11 +99,23 @@ export function linkAgentSession(claim, spawn = spawnSync, env = process.env) {
|
|
|
67
99
|
delete commandEnv.POLYGRAPH_CAPTURE_TOKEN;
|
|
68
100
|
}
|
|
69
101
|
|
|
70
|
-
const
|
|
102
|
+
const spawnOptions = {
|
|
71
103
|
encoding: 'utf8',
|
|
72
104
|
env: commandEnv,
|
|
73
|
-
stdio: ['ignore', 'ignore', 'pipe'],
|
|
74
|
-
|
|
105
|
+
stdio: [input === undefined ? 'ignore' : 'pipe', 'ignore', 'pipe'],
|
|
106
|
+
...(input === undefined ? {} : { input }),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
let result = spawn(command, args, spawnOptions);
|
|
110
|
+
|
|
111
|
+
// POLYGRAPH_CLI may point at a plain JS entry that cannot be spawned
|
|
112
|
+
// directly: a dev build without the executable bit, or a platform that
|
|
113
|
+
// cannot exec scripts. A spawn that failed to LAUNCH ran nothing, so the
|
|
114
|
+
// retry under a Node runtime is side-effect free — and anything that
|
|
115
|
+
// spawns directly today keeps its exact behavior.
|
|
116
|
+
if (result?.error && /\.[cm]?js$/i.test(command)) {
|
|
117
|
+
result = spawn(nodeRuntime(), [command, ...args], spawnOptions);
|
|
118
|
+
}
|
|
75
119
|
|
|
76
120
|
if (result?.error) throw result.error;
|
|
77
121
|
if (result?.status !== 0) {
|
|
@@ -126,6 +170,24 @@ export function buildCommandHookLink(payload, agentType, env = process.env) {
|
|
|
126
170
|
return isPolygraphMcpToolName(payload.tool_name) ? common : undefined;
|
|
127
171
|
}
|
|
128
172
|
|
|
173
|
+
// Cursor's camelCase postToolUse: the payload carries the whole operation
|
|
174
|
+
// (tool_name `MCP:<tool>`, tool_input, tool_output) and is forwarded as
|
|
175
|
+
// evidence because the cursor transcript stores no tool results and lags
|
|
176
|
+
// the hook. The CLI classifies the operation; this filter only avoids
|
|
177
|
+
// spawning the CLI for unrelated tools.
|
|
178
|
+
if (payload.hook_event_name === 'postToolUse') {
|
|
179
|
+
const toolName = nonEmptyString(payload.tool_name);
|
|
180
|
+
if (!toolName || !CURSOR_MCP_CLAIM_TOOL.test(toolName)) return undefined;
|
|
181
|
+
return {
|
|
182
|
+
...common,
|
|
183
|
+
hookOperation: {
|
|
184
|
+
toolName,
|
|
185
|
+
toolInput: payload.tool_input,
|
|
186
|
+
toolOutput: payload.tool_output,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
129
191
|
return undefined;
|
|
130
192
|
}
|
|
131
193
|
|
package/hooks/hooks.json
CHANGED
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -43,6 +43,8 @@ Polygraph functionality is available via both MCP tools and CLI commands. Use wh
|
|
|
43
43
|
| `select_account` | `polygraph account select` | Select the organization that future commands run against |
|
|
44
44
|
| `whoami` | `polygraph whoami` | Show current auth status and org |
|
|
45
45
|
|
|
46
|
+
**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` is a fast, non-blocking call and IS allowed directly in the main conversation — it returns a delegation id. Waited `show_agent` POLLING must run in a background `Task` (`subagent_type: "polygraph-delegate-subagent"`, `run_in_background: true`), collected with `Await`, never inline. One-off unwaited `show_agent` reads in the main conversation are fine and expected — that is how you read a child's result. See [`reference/delegation.md`](reference/delegation.md). The init subagent is launched the same way, by its bare name: a `Task` with `subagent_type: "polygraph-init-subagent"` — without `run_in_background`, since you need its summary before continuing.
|
|
47
|
+
|
|
46
48
|
## CLI Statefulness
|
|
47
49
|
|
|
48
50
|
The Polygraph CLI (`polygraph`) is **stateful**. When you select an organization — via `polygraph account select` or the equivalent MCP tool — that selection is saved globally and all subsequent CLI commands and MCP tool calls operate against it. You do not need to pass the org on every command.
|
|
@@ -69,8 +71,8 @@ After logging in (or if logged in but no org is selected), use `polygraph accoun
|
|
|
69
71
|
|
|
70
72
|
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.
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
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 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.
|
|
75
|
+
1. **Delegate work to each repo** - Call `spawn_agent` for each repo to get a delegation id, then launch one background `polygraph-delegate-subagent` per id to wait on it. With the default role, 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). Delegating into the repo you are in is allowed only with an explicit non-default `role`. Parallel delegation across repos is encouraged. Read [`reference/delegation.md`](reference/delegation.md) before delegating.
|
|
74
76
|
|
|
75
77
|
4. **Monitor child agents** - Let the background poller subagent do the waiting. When it exits, read that child's answer with a single unwaited `show_agent(sessionId, id)` — `result.text` is the child's final message.
|
|
76
78
|
5. **Stop child agents** (if needed) - Use `stop_agent` with the delegation id 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.
|
|
@@ -97,7 +99,7 @@ There are three cases. Pick exactly one before calling any tool. The case labels
|
|
|
97
99
|
|
|
98
100
|
**Case C — No session at all.** Launch the `polygraph-init-subagent` with only `userContext` (no `sessionId`). The subagent will discover candidates and call `start_session` to create a new session.
|
|
99
101
|
|
|
100
|
-
In case B,
|
|
102
|
+
In case B, call `add_repo` yourself when exact repo refs were provided; otherwise the subagent handles discovery and attachment. In case C the subagent handles session creation. In case A you call `show_session` yourself.
|
|
101
103
|
|
|
102
104
|
**Session ID handling:**
|
|
103
105
|
|
|
@@ -281,7 +283,7 @@ If the session has a description timeline, also display:
|
|
|
281
283
|
|
|
282
284
|
## Best Practices
|
|
283
285
|
|
|
284
|
-
1. **
|
|
286
|
+
1. **Wait in background subagents** — `spawn_agent` is fine to call directly and returns a delegation id, but every waited `show_agent` poll belongs in a background `Task` with `subagent_type: "polygraph-delegate-subagent"` and `run_in_background: true`. Collect that Task with `Await`, and if `Await` returns while the poller is still running, call `Await` again with the same background-task id. Inline polling floods the context with status noise.
|
|
285
287
|
|
|
286
288
|
1. **Read each result once** — when a poller exits, read that child with a single unwaited `show_agent(sessionId, id)`; `result.text` is the child's final message. Only reach for an explicit `tail` if that is not enough.
|
|
287
289
|
1. **Poll child status before proceeding** — Always verify child agents have reached a terminal `child.status` (`'completed'`, `'failed'`, or `'cancelled'`) before pushing branches or creating PRs
|
|
@@ -291,4 +293,6 @@ If the session has a description timeline, also display:
|
|
|
291
293
|
1. **Test integration** before marking PRs ready
|
|
292
294
|
1. **Coordinate merge order** if there are deployment dependencies
|
|
293
295
|
|
|
296
|
+
1. **NEVER run a waited `show_agent` loop in the main conversation**. Waiting MUST run inside `polygraph-delegate-subagent`, launched as a background `Task` and collected with `Await`.
|
|
297
|
+
|
|
294
298
|
1. **Use `stop_agent` to clean up** — Stop child agents that are stuck or no longer needed (pass the delegation id). 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.
|
|
@@ -40,7 +40,12 @@ For each id, launch one background poller subagent whose entire job is to block
|
|
|
40
40
|
|
|
41
41
|
- **Claude Code** — a background `Task` with `subagent_type: "polygraph:polygraph-delegate-subagent"`, `run_in_background: true`, and description `Delegate to <repo>`. Fall back to the bare agent name only if the namespaced form is not found.
|
|
42
42
|
- **OpenCode** — invoke `@polygraph-delegate-subagent`.
|
|
43
|
-
- **Codex** — launch `agent_type: "polygraph-delegate-subagent"` via Codex's own `spawn_agent`, and collect it with `wait_agent
|
|
43
|
+
- **Codex** — launch `agent_type: "polygraph-delegate-subagent"` via Codex's own `spawn_agent`, and collect it with `wait_agent`, passing a long `timeout_ms` (five minutes or more): `wait_agent` returns as soon as the poller stops, so a short timeout only adds wake-ups that burn tokens and fill the user-visible transcript with waiting noise.
|
|
44
|
+
- **Cursor** — a background `Task` with `subagent_type: "polygraph-delegate-subagent"`, `run_in_background: true`, and description `Delegate to <repo>`. Collect it with `Await`.
|
|
45
|
+
|
|
46
|
+
A collect step that returns while the poller subagent is still running has not failed. It has only reached the end of its collection window. Collect the same background-task id again, as many times as it takes for the poller subagent to stop. A poller that runs for several minutes is ordinary, and it is never a reason to take the wait back into the main conversation.
|
|
47
|
+
|
|
48
|
+
The background-task id is the handle your own harness returned when you launched the poller. It is not the Polygraph delegation id (`frontend-1`), which addresses the child agent. Once the poller subagent has finished, do not collect it again: read the child instead, as described below. A child that stops for attention also ends the poller, so treat that as a finished poller and not as a collection window running out.
|
|
44
49
|
|
|
45
50
|
The poller has exactly one tool and cannot read logs. It exits with a few lines naming the repo, the id, and the final status. That message is a doorbell, not a report — it tells you the child is worth reading, and nothing about what the child did.
|
|
46
51
|
|