@polygraph/claude-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
  {
2
2
  "name": "polygraph",
3
- "version": "0.4.51",
3
+ "version": "0.4.53",
4
4
  "description": "AI agent skills and subagents for Polygraph sessions, repository context, and coordination",
5
5
  "author": {
6
6
  "name": "Narwhal Technologies Inc",
@@ -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 result = spawn(command, args, {
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polygraph/claude-plugin",
3
- "version": "0.4.51",
3
+ "version": "0.4.53",
4
4
  "description": "AI agent skills and subagents for Polygraph sessions, repository context, and coordination",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -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