@polygraph/opencode-plugin 0.4.50 → 0.5.0

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/README.md CHANGED
@@ -61,7 +61,7 @@ Neither is required — they are a throughput optimization, not a correctness fi
61
61
  ## Skills
62
62
 
63
63
  - **polygraph** — Comprehensive guidance for Polygraph sessions: shared context, repository graph visibility, PR/CI state, delegation, and session management.
64
- - **adversarial-review** — Second-opinion review of a session's work by independent reviewer agents, one per repo, running under a read-only `reviewer` role.
64
+ - **adversarial-review** — Second-opinion review by independent per-repo agents that presents and attaches one consolidated review artifact.
65
65
  - **await-polygraph-ci** — Wait for CI pipelines to settle across all repos in a session, investigate failures, and present fix options.
66
66
  - **get-latest-ci** — One-shot fetch of the latest CI pipeline execution for the current branch.
67
67
  - **session-debrief** — Analyze the raw logs of past Polygraph sessions and produce structured, rank-ordered debriefs for use in a different session.
@@ -82,6 +82,18 @@ npm install
82
82
  npm run sync-artifacts
83
83
  ```
84
84
 
85
+ Estimate the compiled prompt cost of every skill and subagent for Claude Code,
86
+ Codex, and OpenCode:
87
+
88
+ ```sh
89
+ npm run report:token-costs
90
+ ```
91
+
92
+ The report uses four characters per estimated token by default. Override the
93
+ ratio when needed with `npm run report:token-costs -- --characters-per-token 3.5`.
94
+ Pull requests run the same report when opened or updated and keep the latest
95
+ results in a single collapsible comment.
96
+
85
97
  ## Releasing
86
98
 
87
99
  Run the `Release PR` GitHub Actions workflow with a version bump (`patch`, `minor`, or `major`).
@@ -1,10 +1,10 @@
1
1
  // source/hooks/agent-session-link.mjs
2
2
  import { appendFileSync, mkdirSync, renameSync, statSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
- import { join } from "node:path";
4
+ import { basename, join } from "node:path";
5
5
  import { spawnSync } from "node:child_process";
6
6
  var HOOK_LOG_MAX_BYTES = 5 * 1024 * 1024;
7
- var AGENT_TYPES = /* @__PURE__ */ new Set(["claude", "codex", "opencode"]);
7
+ var AGENT_TYPES = /* @__PURE__ */ new Set(["claude", "codex", "opencode", "cursor"]);
8
8
  var COMMAND_HOOK_TOOL = /^mcp__(?:plugin_polygraph_)?polygraph[-_]mcp__/;
9
9
  var OPENCODE_TOOL = /^polygraph(?:(?:-|_)mcp)?_/;
10
10
  function nonEmptyString(value) {
@@ -24,7 +24,8 @@ function buildLinkAgentSessionArgs({
24
24
  cwd,
25
25
  transcriptPath,
26
26
  pid,
27
- source
27
+ source,
28
+ hookOperation
28
29
  }) {
29
30
  const session = nonEmptyString(polygraphSessionId);
30
31
  const harnessSession = nonEmptyString(agentSessionId);
@@ -42,23 +43,37 @@ function buildLinkAgentSessionArgs({
42
43
  if (Number.isSafeInteger(pid) && pid > 0) {
43
44
  args.push("--pid", String(pid));
44
45
  }
46
+ let input;
47
+ if (hookOperation && typeof hookOperation === "object") {
48
+ args.push("--hook-operation-stdin");
49
+ input = JSON.stringify(hookOperation);
50
+ }
45
51
  args.push("--source", claimSource);
46
- return args;
52
+ return { args, input };
53
+ }
54
+ function nodeRuntime() {
55
+ const base = basename(process.execPath).toLowerCase();
56
+ return base === "node" || base === "node.exe" ? process.execPath : "node";
47
57
  }
48
58
  function linkAgentSession(claim, spawn = spawnSync, env = process.env) {
49
59
  if (isManagedChildEnvironment(env)) return false;
50
- const args = buildLinkAgentSessionArgs(claim);
60
+ const { args, input } = buildLinkAgentSessionArgs(claim);
51
61
  const command = nonEmptyString(env?.POLYGRAPH_CLI) ?? "polygraph";
52
62
  const commandEnv = nonEmptyString(claim.polygraphSessionId) ? env : { ...env };
53
63
  if (commandEnv !== env) {
54
64
  delete commandEnv.POLYGRAPH_SESSION_ID;
55
65
  delete commandEnv.POLYGRAPH_CAPTURE_TOKEN;
56
66
  }
57
- const result = spawn(command, args, {
67
+ const spawnOptions = {
58
68
  encoding: "utf8",
59
69
  env: commandEnv,
60
- stdio: ["ignore", "ignore", "pipe"]
61
- });
70
+ stdio: [input === void 0 ? "ignore" : "pipe", "ignore", "pipe"],
71
+ ...input === void 0 ? {} : { input }
72
+ };
73
+ let result = spawn(command, args, spawnOptions);
74
+ if (result?.error && /\.[cm]?js$/i.test(command)) {
75
+ result = spawn(nodeRuntime(), [command, ...args], spawnOptions);
76
+ }
62
77
  if (result?.error) throw result.error;
63
78
  if (result?.status !== 0) {
64
79
  const detail = nonEmptyString(result?.stderr);
@@ -0,0 +1,17 @@
1
+ import matter from '@11ty/gray-matter';
2
+
3
+ export function parseFrontmatter(raw, sourcePath) {
4
+ try {
5
+ const { data, content } = matter(raw);
6
+ return { data: data ?? {}, content };
7
+ } catch (error) {
8
+ if (!sourcePath) {
9
+ throw error;
10
+ }
11
+
12
+ const message = error instanceof Error ? error.message : String(error);
13
+ throw new Error(`Failed to parse frontmatter in ${sourcePath}: ${message}`, {
14
+ cause: error,
15
+ });
16
+ }
17
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polygraph/opencode-plugin",
3
- "version": "0.4.50",
3
+ "version": "0.5.0",
4
4
  "description": "AI agent skills and subagents for Polygraph sessions, repository context, and coordination",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -25,6 +25,7 @@
25
25
  "files": [
26
26
  "server.js",
27
27
  "agent-session-link.mjs",
28
+ "frontmatter.mjs",
28
29
  "skills/",
29
30
  "agents/",
30
31
  "README.md"
@@ -35,6 +36,6 @@
35
36
  },
36
37
  "main": "./server.js",
37
38
  "dependencies": {
38
- "js-yaml": "^4.1.1"
39
+ "@11ty/gray-matter": "3.0.0"
39
40
  }
40
41
  }
package/server.js CHANGED
@@ -12,7 +12,6 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
12
12
  import { homedir } from 'node:os';
13
13
  import path from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
- import yaml from 'js-yaml';
16
15
 
17
16
  import {
18
17
  createOpenCodeSessionLinker,
@@ -20,6 +19,7 @@ import {
20
19
  linkOpenCodeSessionCreatedEvent,
21
20
  logHookFailure,
22
21
  } from './agent-session-link.mjs';
22
+ import { parseFrontmatter } from './frontmatter.mjs';
23
23
 
24
24
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
25
  const packageRoot = __dirname;
@@ -110,7 +110,7 @@ function loadAgents() {
110
110
 
111
111
  const name = path.basename(entry.name, '.md');
112
112
  const raw = readFileSync(path.join(agentsDir, entry.name), 'utf8');
113
- const { data, content } = parseFrontmatter(raw);
113
+ const { data, content } = parseFrontmatter(raw, entry.name);
114
114
  const description = stringValue(data.description);
115
115
  if (!description) {
116
116
  throw new Error(`OpenCode agent ${entry.name} must define a description`);
@@ -130,18 +130,6 @@ function loadAgents() {
130
130
  return result;
131
131
  }
132
132
 
133
- function parseFrontmatter(raw) {
134
- const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
135
- if (!match) {
136
- return { data: {}, content: raw };
137
- }
138
-
139
- return {
140
- data: recordValue(yaml.load(match[1])) ?? {},
141
- content: match[2],
142
- };
143
- }
144
-
145
133
  function stringValue(value) {
146
134
  return typeof value === 'string' ? value : undefined;
147
135
  }
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  name: adversarial-review
3
- description: Independent second-opinion review of a Polygraph session's work. USE WHEN user says "adversarial review", "review this session", or when the Polygraph CLI launches an agent with an instruction to load this skill.
3
+ description: Review a Polygraph session with independent per-repo reviewers and attach one consolidated review artifact.
4
4
 
5
5
  ---
6
6
 
7
7
  # Adversarial Review
8
8
 
9
- 1. **Pick the agent.** Ask whether Claude, Codex, or OpenCode should review `claude`, `codex`, or `opencode` for `spawn_agent`'s `agent` parameter. Skip if the user already named one.
9
+ 1. Skip this step entirely if a reviewer agent was already named — by the user, or in the instruction that launched you (e.g. from `polygraph session review --adversarial`); in that case use that agent and do not ask. Otherwise, **pick the agent**: Ask which should review—`claude`, `codex`, or `opencode` for `spawn_agent`'s `agent` parameter. If the user names a model, pass it via `spawn_agent`'s optional `model` parameter; don't ask about models.
10
10
  2. **Get the session description.**
11
- 3. **Get each repo's plan.** Ask each repo's agent to provide the plan.
12
- 4. **Delegate one reviewer per repo** in parallel, with `role: "reviewer"`. Pass the overall plan and that repo's plan, and ask it to review the code, identify issues, and return a summary. Do the delegation even for the "initiator" repo.
13
- 5. **Summarize.** Once every review is back, analyze them and present one summary to the user.
14
- 6. **Ask what next.** Address the feedback, upload the summary via `upload_artifact`, or continue with the session. Skip if the user already said.
11
+ 3. **Get each repo's plan.**
12
+ 4. **Delegate one reviewer per repo** in parallel, `role: "reviewer"`. Ask to review, identify issues. Do the delegation even for the "initiator" repo.
13
+ 5. **Summarize and attach.** Consolidate reviews into one consolidated Markdown review with per-repo sections and present it to the user. Then call `upload_artifact` once with `sessionId`, that review as `content`, `kind: "review"`, `format: "markdown"`, name `adversarial-review-YYYY-MM-DDTHH-mm-ssZ.md`. If the upload fails, report that separately without suppressing the review.
14
+ 6. **Ask what next.** Address feedback or continue. Skip if the user already said.
15
15
  7. If the user selects "address the feedback", pass each repo's feedback to the repo default agent (not the reviewer). The initiator should fix things itself without delegating.
@@ -20,10 +20,14 @@ spawn_agent(
20
20
  repo: "<org/repo-name>",
21
21
  instruction: "<the task instruction>",
22
22
  role: "<optional role>",
23
- context: "<optional context>"
23
+ context: "<optional context>",
24
+ agent: "<optional: claude | codex | opencode>",
25
+ model: "<optional model override>"
24
26
  )
25
27
  ```
26
28
 
29
+ `agent` picks the child's harness and `model` overrides its default model; include either only when the user named one.
30
+
27
31
  Write the instruction as if to a competent engineer who cannot see your conversation: state the goal, the constraints, and what "done" looks like. The child has its own repo and its own context; it inherits nothing from yours.
28
32
 
29
33
  Delegate to several repos in parallel by calling `spawn_agent` once per repo before waiting on any of them.
@@ -36,7 +40,12 @@ For each id, launch one background poller subagent whose entire job is to block
36
40
 
37
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.
38
42
  - **OpenCode** — invoke `@polygraph-delegate-subagent`.
39
- - **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.
40
49
 
41
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.
42
51