@polygraph/opencode-plugin 0.4.28 → 0.4.30
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/agent-capture-mapping.mjs +90 -0
- package/agents/polygraph-init-subagent.md +17 -19
- package/package.json +2 -1
- package/server.js +18 -0
- package/skills/polygraph/SKILL.md +1 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Agent-capture mapping writer for the OpenCode plugin.
|
|
2
|
+
// Exported from this sibling module (NOT from server.js) so it can be tested
|
|
3
|
+
// independently. See server.js for the constraint on why server.js itself must
|
|
4
|
+
// stay export-free beyond its plugin entry.
|
|
5
|
+
//
|
|
6
|
+
// Records the (agentSessionId ↔ polygraphSessionId) mapping file so the
|
|
7
|
+
// Polygraph CLI can bind parent-log capture deterministically. Written on
|
|
8
|
+
// every session start and compaction; the CLI reader looks for mapping-*.json
|
|
9
|
+
// files in the per-session sidecars directory.
|
|
10
|
+
//
|
|
11
|
+
// File contract:
|
|
12
|
+
// ~/.polygraph/sidecars/<POLYGRAPH_SESSION_ID>/mapping-opencode-<sessionId>.json
|
|
13
|
+
//
|
|
14
|
+
// Behaviour:
|
|
15
|
+
// - Silent no-op when POLYGRAPH_SESSION_ID is unset or POLYGRAPH_CHILD_AGENT is set.
|
|
16
|
+
// - Atomic write via tmp-file rename.
|
|
17
|
+
// - Refresh: preserves firstSeenAt when a valid prior mapping exists.
|
|
18
|
+
// - All failures are silently swallowed.
|
|
19
|
+
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
|
|
24
|
+
function sanitizeMappingFilename(str) {
|
|
25
|
+
return str.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Write (or refresh) the agent-capture mapping for an OpenCode session.
|
|
30
|
+
* Reads POLYGRAPH_SESSION_ID and POLYGRAPH_CHILD_AGENT from process.env.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} agentSessionId The OpenCode session id (input.sessionID).
|
|
33
|
+
* @param {string} [home] Override HOME for testing.
|
|
34
|
+
*/
|
|
35
|
+
export function writeAgentCaptureMapping(
|
|
36
|
+
agentSessionId,
|
|
37
|
+
home = process.env.HOME?.trim() || homedir()
|
|
38
|
+
) {
|
|
39
|
+
try {
|
|
40
|
+
const polygraphSessionId = process.env.POLYGRAPH_SESSION_ID;
|
|
41
|
+
if (!polygraphSessionId) return;
|
|
42
|
+
if (process.env.POLYGRAPH_CHILD_AGENT) return;
|
|
43
|
+
if (!agentSessionId) return;
|
|
44
|
+
|
|
45
|
+
const sidecarDir = path.join(home, '.polygraph', 'sidecars', polygraphSessionId);
|
|
46
|
+
mkdirSync(sidecarDir, { recursive: true });
|
|
47
|
+
|
|
48
|
+
const filenamePart = sanitizeMappingFilename(`opencode-${agentSessionId}`);
|
|
49
|
+
const finalPath = path.join(sidecarDir, `mapping-${filenamePart}.json`);
|
|
50
|
+
const tmpPath = `${finalPath}.tmp-${process.pid}`;
|
|
51
|
+
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
let firstSeenAt = now;
|
|
54
|
+
|
|
55
|
+
if (existsSync(finalPath)) {
|
|
56
|
+
try {
|
|
57
|
+
const existing = JSON.parse(readFileSync(finalPath, 'utf8'));
|
|
58
|
+
if (
|
|
59
|
+
existing.version === 1 &&
|
|
60
|
+
existing.polygraphSessionId === polygraphSessionId &&
|
|
61
|
+
existing.agentSessionId === agentSessionId &&
|
|
62
|
+
Number.isFinite(existing.firstSeenAt)
|
|
63
|
+
) {
|
|
64
|
+
firstSeenAt = existing.firstSeenAt;
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// ignore — treat as missing
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const mapping = {
|
|
72
|
+
version: 1,
|
|
73
|
+
polygraphSessionId,
|
|
74
|
+
agentType: 'opencode',
|
|
75
|
+
agentSessionId,
|
|
76
|
+
cwd: process.cwd(),
|
|
77
|
+
// OpenCode transcripts are resolved by the CLI from its own storage by
|
|
78
|
+
// session id, so we omit transcriptPath here.
|
|
79
|
+
pid: process.pid,
|
|
80
|
+
source: 'hook',
|
|
81
|
+
firstSeenAt,
|
|
82
|
+
lastSeenAt: now,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
writeFileSync(tmpPath, JSON.stringify(mapping, null, 2) + '\n');
|
|
86
|
+
renameSync(tmpPath, finalPath);
|
|
87
|
+
} catch {
|
|
88
|
+
// Silent — a broken plugin hook must never break the agent session.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -15,7 +15,7 @@ These tools are available via MCP and CLI. Use whichever is available in your en
|
|
|
15
15
|
|
|
16
16
|
| MCP Tool | CLI Equivalent | Description |
|
|
17
17
|
| --- | --- | --- |
|
|
18
|
-
| `list_repos` | `polygraph repo list` | Discover candidate repositories
|
|
18
|
+
| `list_repos` | `polygraph repo list` | Discover candidate repositories. |
|
|
19
19
|
| `start_session` | `polygraph session start --repo <ids>` | Initialize a NEW session with selected repositories. Only use when no `sessionId` was provided. |
|
|
20
20
|
| `add_repo` | — | Attach repositories to an EXISTING session. Use when `sessionId` was provided and the session has no repos yet, or when the user wants to add more. |
|
|
21
21
|
| `show_session` | `polygraph session show <id> [--details]` | Get full session details including URL, and use details when session summary, repo IDs, PR URLs, and PR descriptions are needed |
|
|
@@ -58,16 +58,22 @@ Call `list_repos` to discover available candidate repositories:
|
|
|
58
58
|
list_repos()
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
`list_repos` accepts these optional parameters; set whichever apply. Refer to the tool schema for each parameter.
|
|
62
|
+
|
|
63
|
+
- `connectedTo`: repo ID, name, or full name (e.g. `nrwl/ocean`); pair with `connectionType`
|
|
64
|
+
- `connectionType`: `directly-upstream` | `directly-downstream` | `directly-both` (default) | `upstream` | `downstream` | `both`
|
|
65
|
+
- `publishedPackages`, `consumedPackages`, `publishedApis`, `consumedApis`: arrays of package names / API paths
|
|
66
|
+
- `nameFilter`: array of repo name patterns (e.g. `nrwl/*`)
|
|
67
|
+
- `semanticQuery`: free-text description of the repositories you want
|
|
68
|
+
|
|
61
69
|
This returns:
|
|
62
70
|
|
|
63
|
-
- **`
|
|
64
|
-
- **`candidates`**: Candidate account repositories, each with:
|
|
71
|
+
- **`repos`**: Candidate account repositories, each with:
|
|
65
72
|
- `id`: Repository ID
|
|
66
73
|
- `name`: Repository name
|
|
74
|
+
- `repository`: Full repo name (e.g., `org/repo`)
|
|
75
|
+
- `provider`: VCS provider (e.g., `GITHUB`)
|
|
67
76
|
- `description`: AI-generated description of what the repository does (may be null)
|
|
68
|
-
- `vcsConfiguration.repositoryFullName`: Full repo name (e.g., `org/repo`)
|
|
69
|
-
- `graphRelationship`: How this repository relates to the initiator (`distance`, `direction`, `path`), or `null` if the repository is not in the dependency graph. When `initiator` is null, `graphRelationship` will be null for all candidates.
|
|
70
|
-
- **`dependencyGraph`**: Graph of repository dependency `edges` (always available, independent of initiator)
|
|
71
77
|
|
|
72
78
|
### Step 2: Select Relevant Repos
|
|
73
79
|
|
|
@@ -77,14 +83,11 @@ If `selectedRepoIds` or exact repo refs were provided by the main agent, use tho
|
|
|
77
83
|
|
|
78
84
|
Otherwise, analyze the candidates using the `userContext` to determine which repos are relevant:
|
|
79
85
|
|
|
80
|
-
1. Read each
|
|
81
|
-
2. Match against the `userContext`
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
- Direction (upstream/downstream based on the nature of the change)
|
|
86
|
-
3. Select only the repos that are clearly relevant to the task
|
|
87
|
-
4. If uncertain which repos are relevant, include all candidates (safe default)
|
|
86
|
+
1. Read each repo's `description`
|
|
87
|
+
2. Match repo descriptions against the `userContext` to identify relevant repos
|
|
88
|
+
3. Select the repos that are relevant to the task
|
|
89
|
+
4. When uncertain, include all candidates
|
|
90
|
+
5. When the user described the task in natural language and the result is large, re-query with `semanticQuery` set to that description
|
|
88
91
|
|
|
89
92
|
### Step 3: Initialize Polygraph Session or Attach Repos
|
|
90
93
|
|
|
@@ -140,11 +143,6 @@ Return a structured summary in this format:
|
|
|
140
143
|
| Repo | Repository ID | Description | Selected |
|
|
141
144
|
| --- | --- | --- | --- |
|
|
142
145
|
| REPO_FULL_NAME | REPOSITORY_ID | DESCRIPTION | Yes/No |
|
|
143
|
-
|
|
144
|
-
### Initiator
|
|
145
|
-
(Only include this section if `list_repos` was called and `initiator` is non-null)
|
|
146
|
-
- **Name:** <initiator name>
|
|
147
|
-
- **Repo:** <initiator repo full name>
|
|
148
146
|
```
|
|
149
147
|
|
|
150
148
|
## Important Notes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polygraph/opencode-plugin",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.30",
|
|
4
4
|
"description": "AI agent skills and subagents for Polygraph multi-repo coordination",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"server.js",
|
|
27
|
+
"agent-capture-mapping.mjs",
|
|
27
28
|
"skills/",
|
|
28
29
|
"agents/",
|
|
29
30
|
"README.md"
|
package/server.js
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
+
// ⚠️ IMPORTANT — READ BEFORE EDITING OR ADDING CODE TO THIS FILE ⚠️
|
|
2
|
+
//
|
|
3
|
+
// OpenCode loads this file directly as a plugin module. Any `export` beyond
|
|
4
|
+
// the plugin entry (`PolygraphPlugin` / `export default`) breaks OpenCode
|
|
5
|
+
// plugin loading ENTIRELY for every user who has the plugin installed.
|
|
6
|
+
//
|
|
7
|
+
// DO NOT add new exports to this file. Put shared or testable logic in sibling
|
|
8
|
+
// modules under source/opencode/ (e.g. agent-capture-mapping.mjs) and import
|
|
9
|
+
// it here instead.
|
|
10
|
+
|
|
1
11
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
12
|
import { homedir } from 'node:os';
|
|
3
13
|
import path from 'node:path';
|
|
4
14
|
import { fileURLToPath } from 'node:url';
|
|
5
15
|
import yaml from 'js-yaml';
|
|
6
16
|
|
|
17
|
+
import { writeAgentCaptureMapping } from './agent-capture-mapping.mjs';
|
|
18
|
+
|
|
7
19
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
20
|
const packageRoot = __dirname;
|
|
9
21
|
const skillsDir = path.join(packageRoot, 'skills');
|
|
@@ -29,6 +41,9 @@ export const PolygraphPlugin = async () => {
|
|
|
29
41
|
'shell.env': async (input, output) => {
|
|
30
42
|
output.env.POLYGRAPH_AGENT_SESSION_ID = input.sessionID;
|
|
31
43
|
output.env.POLYGRAPH_AGENT_TYPE = 'opencode';
|
|
44
|
+
// Record the agent-capture mapping so the Polygraph CLI can bind
|
|
45
|
+
// parent-log capture deterministically for this session.
|
|
46
|
+
writeAgentCaptureMapping(input.sessionID);
|
|
32
47
|
},
|
|
33
48
|
|
|
34
49
|
// OpenCode has no SessionStart hook, but the Polygraph CLI already seeds the
|
|
@@ -41,6 +56,9 @@ export const PolygraphPlugin = async () => {
|
|
|
41
56
|
if (note) {
|
|
42
57
|
output.context.push(note);
|
|
43
58
|
}
|
|
59
|
+
// Refresh the mapping on compaction (same refresh semantics as Claude/Codex
|
|
60
|
+
// SessionStart hooks firing on 'compact').
|
|
61
|
+
writeAgentCaptureMapping(input.sessionID);
|
|
44
62
|
},
|
|
45
63
|
};
|
|
46
64
|
};
|
|
@@ -18,7 +18,7 @@ Polygraph functionality is available via both MCP tools and CLI commands. Use wh
|
|
|
18
18
|
|
|
19
19
|
| MCP Tool | CLI Equivalent | Description |
|
|
20
20
|
| --- | --- | --- |
|
|
21
|
-
| `list_repos` | `polygraph repo list` | Discover candidate repositories
|
|
21
|
+
| `list_repos` | `polygraph repo list` | Discover candidate repositories. |
|
|
22
22
|
| `start_session` | `polygraph session start --repo <ids>` | Initialize a Polygraph session with selected repositories |
|
|
23
23
|
| `spawn_agent` | — | Start a new child task or send a follow-up to an active task in another repository. Input: `{ sessionId, repo, instruction, context? }`. Output: `{ taskId, message, status: 'delegated' }`. Follow-up routing is automatic: if the repo already has an active child task, the instruction is delivered to it as a follow-up message; otherwise a new child run starts. A repo has at most one active child at a time. A session resume or reconstruction is read-only context restoration; after resuming, do not use `spawn_agent` to continue changes unless the user explicitly asks for changes. |
|
|
24
24
|
| `show_agent` | — | Poll flat per-child status for the session. Output: `{ children: PolygraphChildStatusItem[] }` where each item exposes `repositoryId`, `repoFullName`, `status`, `lastOutputLines`, `durationMs`, `instruction`, `agentType?`, `inputRequiredQuestion?`. `status` is an AcpRunStatus: `'created' \| 'in-progress' \| 'input-required' \| 'permission-required' \| 'completed' \| 'failed' \| 'cancelled'` (British double-L on `'cancelled'`). `inputRequiredQuestion` is populated only when `status === 'input-required'`. |
|