@polygraph/codex-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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygraph",
3
- "version": "0.4.28",
3
+ "version": "0.4.30",
4
4
  "description": "AI agent skills and subagents for Polygraph multi-repo coordination",
5
5
  "author": {
6
6
  "name": "Narwhal Technologies Inc",
@@ -11,7 +11,7 @@ These tools are available via MCP and CLI. Use whichever is available in your en
11
11
 
12
12
  | MCP Tool | CLI Equivalent | Description |
13
13
  | --- | --- | --- |
14
- | `list_repos` | `polygraph repo list` | Discover candidate repositories with descriptions and graph relationships |
14
+ | `list_repos` | `polygraph repo list` | Discover candidate repositories. |
15
15
  | `start_session` | `polygraph session start --repo <ids>` | Initialize a NEW session with selected repositories. Only use when no `sessionId` was provided. |
16
16
  | `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. |
17
17
  | `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 |
@@ -54,16 +54,22 @@ Call `list_repos` to discover available candidate repositories:
54
54
  list_repos()
55
55
  ```
56
56
 
57
+ `list_repos` accepts these optional parameters; set whichever apply. Refer to the tool schema for each parameter.
58
+
59
+ - `connectedTo`: repo ID, name, or full name (e.g. `nrwl/ocean`); pair with `connectionType`
60
+ - `connectionType`: `directly-upstream` | `directly-downstream` | `directly-both` (default) | `upstream` | `downstream` | `both`
61
+ - `publishedPackages`, `consumedPackages`, `publishedApis`, `consumedApis`: arrays of package names / API paths
62
+ - `nameFilter`: array of repo name patterns (e.g. `nrwl/*`)
63
+ - `semanticQuery`: free-text description of the repositories you want
64
+
57
65
  This returns:
58
66
 
59
- - **`initiator`**: The current repository, or `null` if not running from a specific repo
60
- - **`candidates`**: Candidate account repositories, each with:
67
+ - **`repos`**: Candidate account repositories, each with:
61
68
  - `id`: Repository ID
62
69
  - `name`: Repository name
70
+ - `repository`: Full repo name (e.g., `org/repo`)
71
+ - `provider`: VCS provider (e.g., `GITHUB`)
63
72
  - `description`: AI-generated description of what the repository does (may be null)
64
- - `vcsConfiguration.repositoryFullName`: Full repo name (e.g., `org/repo`)
65
- - `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.
66
- - **`dependencyGraph`**: Graph of repository dependency `edges` (always available, independent of initiator)
67
73
 
68
74
  ### Step 2: Select Relevant Repos
69
75
 
@@ -73,14 +79,11 @@ If `selectedRepoIds` or exact repo refs were provided by the main agent, use tho
73
79
 
74
80
  Otherwise, analyze the candidates using the `userContext` to determine which repos are relevant:
75
81
 
76
- 1. Read each candidate's `description` and `graphRelationship`
77
- 2. Match against the `userContext` consider:
78
- - Repository descriptions that mention relevant functionality
79
- - Graph relationships (closer repos are more likely relevant); note that `graphRelationship` may be `null` for repositories not in the dependency graph — use their `description` to assess relevance
80
- - When `graphRelationship` is null for all candidates (no initiator), rely on `description` fields and the raw `dependencyGraph` edges for selection instead
81
- - Direction (upstream/downstream based on the nature of the change)
82
- 3. Select only the repos that are clearly relevant to the task
83
- 4. If uncertain which repos are relevant, include all candidates (safe default)
82
+ 1. Read each repo's `description`
83
+ 2. Match repo descriptions against the `userContext` to identify relevant repos
84
+ 3. Select the repos that are relevant to the task
85
+ 4. When uncertain, include all candidates
86
+ 5. When the user described the task in natural language and the result is large, re-query with `semanticQuery` set to that description
84
87
 
85
88
  ### Step 3: Initialize Polygraph Session or Attach Repos
86
89
 
@@ -136,11 +139,6 @@ Return a structured summary in this format:
136
139
  | Repo | Repository ID | Description | Selected |
137
140
  | --- | --- | --- | --- |
138
141
  | REPO_FULL_NAME | REPOSITORY_ID | DESCRIPTION | Yes/No |
139
-
140
- ### Initiator
141
- (Only include this section if `list_repos` was called and `initiator` is non-null)
142
- - **Name:** <initiator name>
143
- - **Repo:** <initiator repo full name>
144
142
  ```
145
143
 
146
144
  ## Important Notes
package/hooks/hooks.json CHANGED
@@ -8,6 +8,11 @@
8
8
  "type": "command",
9
9
  "command": "node ${PLUGIN_ROOT}/hooks/reinject-polygraph-context.mjs",
10
10
  "statusMessage": "Re-injecting Polygraph session context"
11
+ },
12
+ {
13
+ "type": "command",
14
+ "command": "node ${PLUGIN_ROOT}/hooks/record-session-mapping.mjs codex",
15
+ "statusMessage": "Recording Polygraph agent capture mapping"
11
16
  }
12
17
  ]
13
18
  }
@@ -0,0 +1,172 @@
1
+ // Hidden SessionStart hook — records an agent-capture mapping file that binds
2
+ // this agent's session id to the Polygraph session id in the environment.
3
+ // Used by both the Claude Code plugin (agentType=claude) and the Codex plugin
4
+ // (agentType=codex). The agentType is passed as the first CLI argument so the
5
+ // same script ships in both plugin artifacts.
6
+ //
7
+ // File contract (must match the Polygraph CLI reader exactly):
8
+ // ~/.polygraph/sidecars/<POLYGRAPH_SESSION_ID>/mapping-<agentType>-<agentSessionId>.json
9
+ //
10
+ // Behaviour:
11
+ // - Silent no-op when POLYGRAPH_SESSION_ID is unset.
12
+ // - Silent no-op when POLYGRAPH_CHILD_AGENT is set (child agents must not
13
+ // register themselves as parents).
14
+ // - Atomic write: write to <path>.tmp-<pid>, then rename over final path.
15
+ // - Refresh: when a valid prior mapping for the same session already exists,
16
+ // preserve its firstSeenAt and only update lastSeenAt + mutable fields.
17
+ // - All failures are silently swallowed; never writes to stdout (Claude Code
18
+ // injects hook stdout into the model context); never exits non-zero.
19
+
20
+ import {
21
+ existsSync,
22
+ mkdirSync,
23
+ readFileSync,
24
+ realpathSync,
25
+ renameSync,
26
+ writeFileSync,
27
+ } from 'node:fs';
28
+ import { homedir } from 'node:os';
29
+ import { join } from 'node:path';
30
+ import { fileURLToPath } from 'node:url';
31
+
32
+ function readStdin() {
33
+ try {
34
+ return readFileSync(0, 'utf8');
35
+ } catch {
36
+ return '';
37
+ }
38
+ }
39
+
40
+ function tryParseJson(str) {
41
+ try {
42
+ return JSON.parse(str);
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ function sanitizeFilename(str) {
49
+ return str.replace(/[^A-Za-z0-9._-]/g, '_');
50
+ }
51
+
52
+ /**
53
+ * Write (or refresh) the agent-capture mapping file.
54
+ *
55
+ * @param {object} opts
56
+ * @param {string} opts.agentType 'claude' | 'codex'
57
+ * @param {string} opts.agentSessionId The harness's own session id.
58
+ * @param {string} opts.polygraphSessionId Value of POLYGRAPH_SESSION_ID.
59
+ * @param {string} opts.cwd Agent working directory.
60
+ * @param {string} [opts.transcriptPath] Absolute transcript path; omit when unknown.
61
+ * @param {number} [opts.pid] Harness process id; omit when not knowable.
62
+ * @param {string} [home] Override HOME for testing.
63
+ */
64
+ export function writeCaptureMapping(
65
+ { agentType, agentSessionId, polygraphSessionId, cwd, transcriptPath, pid },
66
+ home = process.env.HOME?.trim() || homedir()
67
+ ) {
68
+ const sidecarDir = join(home, '.polygraph', 'sidecars', polygraphSessionId);
69
+ mkdirSync(sidecarDir, { recursive: true });
70
+
71
+ const filenamePart = sanitizeFilename(`${agentType}-${agentSessionId}`);
72
+ const finalPath = join(sidecarDir, `mapping-${filenamePart}.json`);
73
+ const tmpPath = `${finalPath}.tmp-${process.pid}`;
74
+
75
+ const now = Date.now();
76
+
77
+ // Refresh semantics: preserve firstSeenAt from a valid prior mapping.
78
+ let firstSeenAt = now;
79
+ if (existsSync(finalPath)) {
80
+ const existing = tryParseJson(readFileSync(finalPath, 'utf8'));
81
+ if (
82
+ existing !== null &&
83
+ existing.version === 1 &&
84
+ existing.polygraphSessionId === polygraphSessionId &&
85
+ existing.agentSessionId === agentSessionId &&
86
+ Number.isFinite(existing.firstSeenAt)
87
+ ) {
88
+ firstSeenAt = existing.firstSeenAt;
89
+ }
90
+ }
91
+
92
+ const mapping = {
93
+ version: 1,
94
+ polygraphSessionId,
95
+ agentType,
96
+ agentSessionId,
97
+ cwd,
98
+ ...(transcriptPath != null ? { transcriptPath } : {}),
99
+ ...(pid != null ? { pid } : {}),
100
+ source: 'hook',
101
+ firstSeenAt,
102
+ lastSeenAt: now,
103
+ };
104
+
105
+ writeFileSync(tmpPath, JSON.stringify(mapping, null, 2) + '\n');
106
+ renameSync(tmpPath, finalPath);
107
+ }
108
+
109
+ export function main() {
110
+ try {
111
+ const polygraphSessionId = process.env.POLYGRAPH_SESSION_ID;
112
+ if (!polygraphSessionId) return;
113
+ if (process.env.POLYGRAPH_CHILD_AGENT) return;
114
+
115
+ const agentType = process.argv[2];
116
+ if (!agentType) return;
117
+
118
+ let payload = {};
119
+ const raw = readStdin();
120
+ if (raw) {
121
+ const parsed = tryParseJson(raw);
122
+ if (parsed !== null) payload = parsed;
123
+ }
124
+
125
+ const agentSessionId =
126
+ typeof payload.session_id === 'string' ? payload.session_id : '';
127
+ if (!agentSessionId) return;
128
+
129
+ const cwd =
130
+ typeof payload.cwd === 'string' && payload.cwd
131
+ ? payload.cwd
132
+ : process.cwd();
133
+
134
+ // transcript_path is present on Claude/Codex payloads; may be null — omit
135
+ // the field when absent or null rather than writing null into the mapping.
136
+ const transcriptPath =
137
+ typeof payload.transcript_path === 'string' && payload.transcript_path
138
+ ? payload.transcript_path
139
+ : undefined;
140
+
141
+ writeCaptureMapping({
142
+ agentType,
143
+ agentSessionId,
144
+ polygraphSessionId,
145
+ cwd,
146
+ transcriptPath,
147
+ // process.ppid is the harness pid when the hook is spawned as a child.
148
+ pid: process.ppid,
149
+ });
150
+ } catch {
151
+ // Silent — a broken hook must never break the agent session.
152
+ }
153
+ }
154
+
155
+ // Run only when executed directly as a hook, not when imported (e.g. by tests).
156
+ // realpathSync both sides so the check holds when the plugin lives under a
157
+ // symlinked path (e.g. macOS /tmp -> /private/tmp).
158
+ function isMainModule() {
159
+ if (!process.argv[1]) return false;
160
+ try {
161
+ return (
162
+ realpathSync(process.argv[1]) ===
163
+ realpathSync(fileURLToPath(import.meta.url))
164
+ );
165
+ } catch {
166
+ return false;
167
+ }
168
+ }
169
+
170
+ if (isMainModule()) {
171
+ main();
172
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polygraph/codex-plugin",
3
- "version": "0.4.28",
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,
@@ -29,7 +29,7 @@ Polygraph functionality is available via both MCP tools and CLI commands. Use wh
29
29
 
30
30
  | MCP Tool | CLI Equivalent | Description |
31
31
  | --- | --- | --- |
32
- | `list_repos` | `polygraph repo list` | Discover candidate repositories with descriptions and graph relationships |
32
+ | `list_repos` | `polygraph repo list` | Discover candidate repositories. |
33
33
  | `start_session` | `polygraph session start --repo <ids>` | Initialize a Polygraph session with selected repositories |
34
34
  | `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. |
35
35
  | `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'`. |