@polygraph/codex-plugin 0.4.45 → 0.4.46

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.45",
3
+ "version": "0.4.46",
4
4
  "description": "AI agent skills and subagents for Polygraph sessions, repository context, and coordination",
5
5
  "author": {
6
6
  "name": "Narwhal Technologies Inc",
@@ -0,0 +1,146 @@
1
+ import { appendFileSync, mkdirSync, renameSync, statSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+
6
+ const HOOK_LOG_MAX_BYTES = 5 * 1024 * 1024;
7
+
8
+ const AGENT_TYPES = new Set(['claude', 'codex', 'opencode']);
9
+ const COMMAND_HOOK_TOOL = /^mcp__(?:plugin_polygraph_)?polygraph[-_]mcp__/;
10
+ const OPENCODE_TOOL = /^polygraph(?:(?:-|_)mcp)?_/;
11
+
12
+ function nonEmptyString(value) {
13
+ return typeof value === 'string' && value.trim() ? value : undefined;
14
+ }
15
+
16
+ function isManagedChildEnvironment(env) {
17
+ return Boolean(env && Object.hasOwn(env, 'POLYGRAPH_CHILD_AGENT'));
18
+ }
19
+
20
+ export function isPolygraphMcpToolName(toolName) {
21
+ const name = nonEmptyString(toolName);
22
+ return Boolean(name && (COMMAND_HOOK_TOOL.test(name) || OPENCODE_TOOL.test(name)));
23
+ }
24
+
25
+ export function buildLinkAgentSessionArgs({
26
+ polygraphSessionId,
27
+ agentType,
28
+ agentSessionId,
29
+ cwd,
30
+ transcriptPath,
31
+ pid,
32
+ source,
33
+ }) {
34
+ const session = nonEmptyString(polygraphSessionId);
35
+ const harnessSession = nonEmptyString(agentSessionId);
36
+ const claimSource = nonEmptyString(source);
37
+ if (!AGENT_TYPES.has(agentType)) throw new Error(`Unsupported agent type: ${agentType}`);
38
+ if (!harnessSession) throw new Error('agentSessionId is required');
39
+ if (!claimSource) throw new Error('source is required');
40
+
41
+ const args = ['_link-agent-session'];
42
+ if (session) args.push('--session', session);
43
+ args.push('--agent-type', agentType, '--agent-session-id', harnessSession);
44
+
45
+ const workingDirectory = nonEmptyString(cwd);
46
+ if (workingDirectory) args.push('--cwd', workingDirectory);
47
+
48
+ const transcript = nonEmptyString(transcriptPath);
49
+ if (transcript) args.push('--transcript-path', transcript);
50
+
51
+ if (Number.isSafeInteger(pid) && pid > 0) {
52
+ args.push('--pid', String(pid));
53
+ }
54
+
55
+ args.push('--source', claimSource);
56
+ return args;
57
+ }
58
+
59
+ export function linkAgentSession(claim, spawn = spawnSync, env = process.env) {
60
+ if (isManagedChildEnvironment(env)) return false;
61
+
62
+ const args = buildLinkAgentSessionArgs(claim);
63
+ const command = nonEmptyString(env?.POLYGRAPH_CLI) ?? 'polygraph';
64
+ const commandEnv = nonEmptyString(claim.polygraphSessionId) ? env : { ...env };
65
+ if (commandEnv !== env) {
66
+ delete commandEnv.POLYGRAPH_SESSION_ID;
67
+ delete commandEnv.POLYGRAPH_CAPTURE_TOKEN;
68
+ }
69
+
70
+ const result = spawn(command, args, {
71
+ encoding: 'utf8',
72
+ env: commandEnv,
73
+ stdio: ['ignore', 'ignore', 'pipe'],
74
+ });
75
+
76
+ if (result?.error) throw result.error;
77
+ if (result?.status !== 0) {
78
+ const detail = nonEmptyString(result?.stderr);
79
+ throw new Error(
80
+ `polygraph _link-agent-session exited with status ${String(result?.status)}` +
81
+ (detail ? `: ${detail}` : '')
82
+ );
83
+ }
84
+
85
+ return true;
86
+ }
87
+
88
+ export function buildCommandHookLink(payload, agentType, env = process.env) {
89
+ if (!payload || typeof payload !== 'object') return undefined;
90
+ if (isManagedChildEnvironment(env)) return undefined;
91
+
92
+ const agentSessionId = nonEmptyString(payload.session_id);
93
+ if (!agentSessionId) return undefined;
94
+
95
+ const common = {
96
+ agentType,
97
+ agentSessionId,
98
+ cwd: nonEmptyString(payload.cwd),
99
+ transcriptPath: nonEmptyString(payload.transcript_path),
100
+ source: 'hook',
101
+ };
102
+
103
+ if (payload.hook_event_name === 'SessionStart') {
104
+ const polygraphSessionId = nonEmptyString(env.POLYGRAPH_SESSION_ID);
105
+ return polygraphSessionId ? { ...common, polygraphSessionId } : undefined;
106
+ }
107
+
108
+ if (payload.hook_event_name === 'PostToolUse') {
109
+ return isPolygraphMcpToolName(payload.tool_name) ? common : undefined;
110
+ }
111
+
112
+ return undefined;
113
+ }
114
+
115
+ export function logHookFailure(
116
+ hook,
117
+ error,
118
+ meta = {},
119
+ home = process.env.HOME?.trim() || homedir()
120
+ ) {
121
+ try {
122
+ const logsDir = join(home, '.polygraph', 'logs');
123
+ mkdirSync(logsDir, { recursive: true });
124
+ const logFile = join(logsDir, 'hooks.log');
125
+
126
+ try {
127
+ if (statSync(logFile).size > HOOK_LOG_MAX_BYTES) {
128
+ renameSync(logFile, `${logFile}.1`);
129
+ }
130
+ } catch {
131
+ // There may be no prior log, and logging must stay best-effort.
132
+ }
133
+
134
+ const entry = {
135
+ time: new Date().toISOString(),
136
+ hook,
137
+ pid: process.pid,
138
+ ...meta,
139
+ error: error instanceof Error ? error.message : String(error),
140
+ ...(error instanceof Error && error.stack ? { stack: error.stack } : {}),
141
+ };
142
+ appendFileSync(logFile, JSON.stringify(entry) + '\n');
143
+ } catch {
144
+ // Hook diagnostics must never break the harness event that triggered them.
145
+ }
146
+ }
package/hooks/hooks.json CHANGED
@@ -1,5 +1,17 @@
1
1
  {
2
2
  "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "mcp__plugin_polygraph_.*|mcp__polygraph.*",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node ${PLUGIN_ROOT}/hooks/record-session-mapping.mjs codex",
10
+ "statusMessage": "Linking Polygraph agent session"
11
+ }
12
+ ]
13
+ }
14
+ ],
3
15
  "SessionStart": [
4
16
  {
5
17
  "matcher": "startup|resume|compact",
@@ -12,7 +24,7 @@
12
24
  {
13
25
  "type": "command",
14
26
  "command": "node ${PLUGIN_ROOT}/hooks/record-session-mapping.mjs codex",
15
- "statusMessage": "Recording Polygraph agent capture mapping"
27
+ "statusMessage": "Linking Polygraph agent session"
16
28
  }
17
29
  ]
18
30
  }
@@ -1,263 +1,54 @@
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
- // <sessionsRoot>/<POLYGRAPH_SESSION_ID>/sidecars/mapping-<agentType>-<agentSessionId>.json
9
- // where sessionsRoot = $POLYGRAPH_ROOT, else `globalRoot` from
10
- // ~/.polygraph/config.json, else ~/.polygraph/sessions
11
- // Legacy fallback, used ONLY when <sessionsRoot>/<POLYGRAPH_SESSION_ID>
12
- // does not exist (for real sessions nothing new is written here):
13
- // ~/.polygraph/sidecars/<POLYGRAPH_SESSION_ID>/mapping-<agentType>-<agentSessionId>.json
14
- //
15
- // The session folder is a trustworthy location for this parent-transcript
16
- // binding because the Polygraph CLI's child-agent sandboxes exclude the
17
- // session root — children cannot write there. The CLI reads mappings from
18
- // the session folder first, with the flat dir as a read-only fallback.
19
- //
20
- // Behaviour:
21
- // - Silent no-op when POLYGRAPH_SESSION_ID is unset.
22
- // - Silent no-op when POLYGRAPH_CHILD_AGENT is set (child agents must not
23
- // register themselves as parents).
24
- // - Atomic write: write to <path>.tmp-<pid>, then rename over final path.
25
- // - Refresh: when a valid prior mapping for the same session already exists
26
- // (checked in the new location first, then the legacy flat dir), preserve
27
- // its firstSeenAt and only update lastSeenAt + mutable fields — so
28
- // migrating a mapping from the legacy dir keeps firstSeenAt continuity.
29
- // - All failures are silently swallowed; never writes to stdout (Claude Code
30
- // injects hook stdout into the model context); never exits non-zero.
31
-
32
- import {
33
- appendFileSync,
34
- existsSync,
35
- mkdirSync,
36
- readFileSync,
37
- realpathSync,
38
- renameSync,
39
- statSync,
40
- writeFileSync,
41
- } from 'node:fs';
42
- import { homedir } from 'node:os';
43
- import { join } from 'node:path';
1
+ import { readFileSync, realpathSync } from 'node:fs';
44
2
  import { fileURLToPath } from 'node:url';
45
3
 
46
- const HOOK_LOG_MAX_BYTES = 5 * 1024 * 1024;
47
-
48
- // Append a one-line JSON record of a hook failure to ~/.polygraph/logs/hooks.log.
49
- // This hook swallows its errors silently and must never write to stdout (Claude
50
- // Code injects hook stdout into the model context), so this on-disk log is the
51
- // only record that something went wrong. The logger is itself failure-proof.
52
- function logHookFailure(
53
- hook,
54
- error,
55
- meta = {},
56
- home = process.env.HOME?.trim() || homedir()
57
- ) {
58
- try {
59
- const logsDir = join(home, '.polygraph', 'logs');
60
- mkdirSync(logsDir, { recursive: true });
61
- const logFile = join(logsDir, 'hooks.log');
62
-
63
- try {
64
- if (statSync(logFile).size > HOOK_LOG_MAX_BYTES) {
65
- renameSync(logFile, `${logFile}.1`);
66
- }
67
- } catch {
68
- // no prior log, or rotation failed — ignore
69
- }
70
-
71
- const entry = {
72
- time: new Date().toISOString(),
73
- hook,
74
- pid: process.pid,
75
- ...meta,
76
- error: error instanceof Error ? error.message : String(error),
77
- ...(error instanceof Error && error.stack ? { stack: error.stack } : {}),
78
- };
79
- appendFileSync(logFile, JSON.stringify(entry) + '\n');
80
- } catch {
81
- // Logging must never throw — a failing logger must not break the hook.
82
- }
83
- }
84
-
85
- function readStdin() {
86
- try {
87
- return readFileSync(0, 'utf8');
88
- } catch {
89
- return '';
90
- }
91
- }
4
+ import {
5
+ buildCommandHookLink,
6
+ linkAgentSession,
7
+ logHookFailure,
8
+ } from './agent-session-link.mjs';
92
9
 
93
- function tryParseJson(str) {
10
+ function readPayload() {
94
11
  try {
95
- return JSON.parse(str);
12
+ const raw = readFileSync(0, 'utf8');
13
+ return raw ? JSON.parse(raw) : undefined;
96
14
  } catch {
97
- return null;
15
+ return undefined;
98
16
  }
99
17
  }
100
18
 
101
- function sanitizeFilename(str) {
102
- return str.replace(/[^A-Za-z0-9._-]/g, '_');
103
- }
104
-
105
- // Resolve the root directory that holds per-session folders:
106
- // $POLYGRAPH_ROOT, else `globalRoot` from ~/.polygraph/config.json, else
107
- // ~/.polygraph/sessions. Must match the Polygraph CLI's own resolution.
108
- export function sessionsRoot(home = process.env.HOME?.trim() || homedir()) {
109
- const fromEnv = process.env.POLYGRAPH_ROOT?.trim();
110
- if (fromEnv) return fromEnv;
111
-
19
+ export function main({
20
+ payload = readPayload(),
21
+ agentType = process.argv[2],
22
+ env = process.env,
23
+ pid = process.ppid,
24
+ spawn,
25
+ } = {}) {
112
26
  try {
113
- const config = tryParseJson(
114
- readFileSync(join(home, '.polygraph', 'config.json'), 'utf8')
27
+ const link = buildCommandHookLink(payload, agentType, env);
28
+ if (!link) return false;
29
+
30
+ return linkAgentSession(
31
+ {
32
+ ...link,
33
+ pid,
34
+ cwd: link.cwd ?? process.cwd(),
35
+ },
36
+ spawn,
37
+ env
115
38
  );
116
- if (typeof config?.globalRoot === 'string' && config.globalRoot.trim()) {
117
- return config.globalRoot.trim();
118
- }
119
- } catch {
120
- // no config — use the default
121
- }
122
-
123
- return join(home, '.polygraph', 'sessions');
124
- }
125
-
126
- /**
127
- * Write (or refresh) the agent-capture mapping file.
128
- *
129
- * Written into the session folder (`<sessionsRoot>/<sessionId>/sidecars/`)
130
- * when the session directory exists; only when it does not exist does the
131
- * write fall back to the legacy flat `~/.polygraph/sidecars/<sessionId>/`
132
- * dir — for real sessions nothing new lands under the flat dir.
133
- *
134
- * @param {object} opts
135
- * @param {string} opts.agentType 'claude' | 'codex'
136
- * @param {string} opts.agentSessionId The harness's own session id.
137
- * @param {string} opts.polygraphSessionId Value of POLYGRAPH_SESSION_ID.
138
- * @param {string} opts.cwd Agent working directory.
139
- * @param {string} [opts.transcriptPath] Absolute transcript path; omit when unknown.
140
- * @param {number} [opts.pid] Harness process id; omit when not knowable.
141
- * @param {string} [home] Override HOME for testing.
142
- */
143
- export function writeCaptureMapping(
144
- { agentType, agentSessionId, polygraphSessionId, cwd, transcriptPath, pid },
145
- home = process.env.HOME?.trim() || homedir()
146
- ) {
147
- const filenamePart = sanitizeFilename(`${agentType}-${agentSessionId}`);
148
- const fileName = `mapping-${filenamePart}.json`;
149
-
150
- const sessionDir = join(sessionsRoot(home), polygraphSessionId);
151
- const sessionSidecarDir = join(sessionDir, 'sidecars');
152
- const legacyDir = join(home, '.polygraph', 'sidecars', polygraphSessionId);
153
-
154
- // New location when the session directory exists; legacy flat dir only
155
- // when it does not.
156
- const targetDir = existsSync(sessionDir) ? sessionSidecarDir : legacyDir;
157
- mkdirSync(targetDir, { recursive: true });
158
-
159
- const finalPath = join(targetDir, fileName);
160
- const tmpPath = `${finalPath}.tmp-${process.pid}`;
161
-
162
- const now = Date.now();
163
-
164
- // Refresh semantics: preserve firstSeenAt from a valid prior mapping.
165
- // Check the new location first, then the legacy flat dir — this keeps
166
- // firstSeenAt continuity when migrating a mapping from the legacy dir.
167
- let firstSeenAt = now;
168
- for (const candidate of [
169
- join(sessionSidecarDir, fileName),
170
- join(legacyDir, fileName),
171
- ]) {
172
- if (!existsSync(candidate)) continue;
173
- const existing = tryParseJson(readFileSync(candidate, 'utf8'));
174
- if (
175
- existing !== null &&
176
- existing.version === 1 &&
177
- existing.polygraphSessionId === polygraphSessionId &&
178
- existing.agentSessionId === agentSessionId &&
179
- Number.isFinite(existing.firstSeenAt)
180
- ) {
181
- firstSeenAt = existing.firstSeenAt;
182
- break;
183
- }
184
- }
185
-
186
- const mapping = {
187
- version: 1,
188
- polygraphSessionId,
189
- agentType,
190
- agentSessionId,
191
- cwd,
192
- ...(transcriptPath != null ? { transcriptPath } : {}),
193
- ...(pid != null ? { pid } : {}),
194
- source: 'hook',
195
- firstSeenAt,
196
- lastSeenAt: now,
197
- };
198
-
199
- writeFileSync(tmpPath, JSON.stringify(mapping, null, 2) + '\n');
200
- renameSync(tmpPath, finalPath);
201
- }
202
-
203
- export function main() {
204
- try {
205
- const polygraphSessionId = process.env.POLYGRAPH_SESSION_ID;
206
- if (!polygraphSessionId) return;
207
- if (process.env.POLYGRAPH_CHILD_AGENT) return;
208
-
209
- const agentType = process.argv[2];
210
- if (!agentType) return;
211
-
212
- let payload = {};
213
- const raw = readStdin();
214
- if (raw) {
215
- const parsed = tryParseJson(raw);
216
- if (parsed !== null) payload = parsed;
217
- }
218
-
219
- const agentSessionId =
220
- typeof payload.session_id === 'string' ? payload.session_id : '';
221
- if (!agentSessionId) return;
222
-
223
- const cwd =
224
- typeof payload.cwd === 'string' && payload.cwd
225
- ? payload.cwd
226
- : process.cwd();
227
-
228
- // transcript_path is present on Claude/Codex payloads; may be null — omit
229
- // the field when absent or null rather than writing null into the mapping.
230
- const transcriptPath =
231
- typeof payload.transcript_path === 'string' && payload.transcript_path
232
- ? payload.transcript_path
233
- : undefined;
234
-
235
- writeCaptureMapping({
236
- agentType,
237
- agentSessionId,
238
- polygraphSessionId,
239
- cwd,
240
- transcriptPath,
241
- // process.ppid is the harness pid when the hook is spawned as a child.
242
- pid: process.ppid,
243
- });
244
39
  } catch (error) {
245
- // Silent toward the agent a broken hook must never break the session —
246
- // but record it so failures are not invisible.
247
- logHookFailure(`${process.argv[2] || 'unknown'}:record-session-mapping`, error);
40
+ logHookFailure(`${agentType || 'unknown'}:link-agent-session`, error, {
41
+ hookEventName: payload?.hook_event_name,
42
+ agentSessionId: payload?.session_id,
43
+ });
44
+ return false;
248
45
  }
249
46
  }
250
47
 
251
- // Run only when executed directly as a hook, not when imported (e.g. by tests).
252
- // realpathSync both sides so the check holds when the plugin lives under a
253
- // symlinked path (e.g. macOS /tmp -> /private/tmp).
254
48
  function isMainModule() {
255
49
  if (!process.argv[1]) return false;
256
50
  try {
257
- return (
258
- realpathSync(process.argv[1]) ===
259
- realpathSync(fileURLToPath(import.meta.url))
260
- );
51
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
261
52
  } catch {
262
53
  return false;
263
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polygraph/codex-plugin",
3
- "version": "0.4.45",
3
+ "version": "0.4.46",
4
4
  "description": "AI agent skills and subagents for Polygraph sessions, repository context, and coordination",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -16,12 +16,11 @@ Read this before the tool table below — it determines which tools are yours to
16
16
  - **For new sessions:** call Codex `spawn_agent` with `agent_type: "polygraph-init-subagent"`. Do NOT call Polygraph MCP `list_repos` or `start_session` directly from this conversation.
17
17
  - **For explicit repo additions to an existing session:** if the user gives exact refs by ID, short name, full name, GitHub `owner/repo` slug, or URL-like slug, call Polygraph MCP `add_repo` directly with those refs. Do NOT call `list_repos` or launch candidate discovery first.
18
18
  - **For repo work:** call Codex `spawn_agent` with `agent_type: "polygraph-delegate-subagent"`. Do NOT call Polygraph MCP `spawn_agent` or `show_agent` directly from this conversation; collect results with `wait_agent` when needed.
19
- - **Allowed direct Polygraph MCP calls from the parent:** `whoami`, `login`, `list_accounts`, `select_account`, `show_session` for read-only inspection of an existing session, `update_session` for session metadata updates, `link_reference` for linking external references to sessions, and `add_repo` only for explicit repo additions to an existing session.
20
19
  - Do NOT pass `fork_context: true` to Codex `spawn_agent` when `agent_type` is a custom agent — Codex rejects it.
21
20
 
22
- Polygraph connects repositories and the agent work happening across them. Its central artifact is the session, which groups the repositories, branches, PRs, and CI status for one piece of work and can be shared and resumed: use it to coordinate changes across multiple repositories, and also on its own to share the session URL with collaborators, hand off progress via the session description, resume prior work, and watch CI across the session's PRs.
21
+ Polygraph connects repos and the agent work happening across them. Its central artifact is the session, which groups the repos, branches, PRs, and CI status for one piece of work and can be shared and resumed: use it to coordinate changes across multiple repos or in a single repoto share the session URL with collaborators, hand off progress via the session description, resume prior work, and watch CI across the session's PRs.
23
22
 
24
- **Polygraph operates on the current repo in place.** Starting or joining a session never clones or modifies the repository you are in — you keep working in your real working directory, and `push_branch` pushes your local commits from that checkout. Only *other* repositories are worked on in separate Polygraph-managed clones via `spawn_agent`.
23
+ **Polygraph operates on the current repo in place.** Starting or joining a session never clones or modifies the repository you are in — you keep working in your real working directory, and `push_branch` pushes your local commits from that checkout. Only *other* repos are worked on in separate Polygraph-managed clones via `spawn_agent`.
25
24
 
26
25
  ## Sandboxing in Polygraph Sessions
27
26
 
@@ -76,29 +75,28 @@ Use `polygraph whoami` (or the `whoami` MCP tool) before session work to check i
76
75
  - If the user **is logged in** and an org is selected → proceed to the workflow.
77
76
  - If auth is **missing, expired, or no org is selected** → stop session work. Do not keep trying session creation, repository discovery, delegation, or CI checks.
78
77
  - Facilitate user reauth through the browser-based login flow, such as `polygraph auth login` (or the `login` MCP tool). In interactive desktop clients, browser reauth is usually user-driven; surface the need clearly and wait for the user to complete it.
79
- - After login, an organization must be selected. Use `polygraph account select` (or the MCP equivalent) when needed.
78
+ - After login, an organization must be selected. Use `polygraph account select` (or MCP equivalent) when needed.
80
79
  - Re-run `polygraph whoami` (or `whoami`) after reauth and org selection. Continue only after it confirms a valid login and selected organization.
81
80
 
82
81
  ### Select Organization
83
82
 
84
- After logging in (or if logged in but no org is selected), use `polygraph account select` (or the equivalent MCP tool) to choose the organization that future commands will run against.
83
+ After logging in (or if logged in but no org is selected), use `polygraph account select` (or MCP equivalent) to choose the organization that future commands will run against.
85
84
 
86
85
  ## Workflow Overview
87
86
 
88
87
  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.
89
88
 
90
- 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 with those refs 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.
91
- 1. **Delegate work to each repo** - Use the `polygraph-delegate-subagent` to start child agents in other repositories. 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). Parallel delegation across repos is encouraged. Choose the Simple (fire-and-forget) or Multi-turn (interactive) pattern described below based on whether the child may need clarification.
89
+ 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.
90
+ 1. **Delegate work to each repo** - Use the `polygraph-delegate-subagent` to start child agents. 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). Parallel delegation across repos is encouraged. Choose the Simple (fire-and-forget) or Multi-turn (interactive) pattern described below based on whether the child may need clarification.
92
91
 
93
92
  4. **Monitor child agents** - Use `show_agent` to poll one repo's children (`repo` is required; pass `role` to narrow to one agent) and read each entry's `status` and `lastOutputLines` from the `children[]` array.
94
93
  5. **Stop child agents** (if needed) - Use `stop_agent` (with `role` when targeting a non-default agent) 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.
95
94
  6. **Push branches** - Use `push_branch` after making commits. A required `description` must follow the Session Description Policy.
96
- 7. **Update session description** - Use `update_session` to update the session description; must follow the Session Description Policy. Independent of PR creation or mark-ready.
97
- 8. **Create draft PRs** - Use `create_pr` to create linked draft PRs. Always pass `description` following the Session Description Policy.
98
- 9. **Associate existing PRs** (optional) - Use `associate_pr` to link PRs created outside Polygraph.
99
- 10. **Query PR status** - Use `show_session` to check progress.
100
- 11. **Mark PRs ready** - Use `mark_pr_ready` when work is complete.
101
- 12. **Archive session** - Use `archive_session` to archive the session when the user requests it.
95
+ 7. **Create draft PRs** - Use `create_pr` to create linked draft PRs. Always pass `description` following the Session Description Policy.
96
+ 8. **Associate existing PRs** (optional) - Use `associate_pr` to link PRs created outside Polygraph.
97
+ 9. **Query PR status** - Use `show_session` to check progress.
98
+ 10. **Mark PRs ready** - Use `mark_pr_ready` when work is complete.
99
+ 11. **Archive session** - Use `archive_session` to archive the session when the user requests it.
102
100
 
103
101
  ## Step-by-Step Guide
104
102
 
@@ -106,7 +104,7 @@ The delegate/monitor/stop steps apply only when working across repos. A single-r
106
104
 
107
105
  There are three cases. Pick exactly one before calling any tool. The case labels are internal routing shorthand — never mention them in anything you show the user.
108
106
 
109
- **Hard rule: if a session ID is already in scope (e.g., the startup banner says "You're in Polygraph session …", or the user passed one), that session ID is authoritative for this entire conversation. NEVER call `start_session` — doing so creates a brand-new session and orphans the one the parent harness is pointed at. Reuse the existing session via `show_session` and, if needed, `add_repo`.**
107
+ **Hard rule: if a session ID is already in scope (e.g., the startup banner says "You're in Polygraph session …", or the user passed one or you are provided one by a reminder hook), that session ID is authoritative for this entire conversation. NEVER call `start_session` — doing so creates a brand-new session and orphans the one the parent harness is pointed at. Reuse the existing session via `show_session` and, if needed, `add_repo`.**
110
108
 
111
109
  **Case A — Existing session, already has repos.** Call `show_session` directly with the known session ID. Skip the init subagent entirely, show the session details (format below), and proceed.
112
110
 
@@ -119,29 +117,10 @@ In case B, call `add_repo` yourself when exact repo refs were provided; otherwis
119
117
  **Session ID handling:**
120
118
 
121
119
  - For a new session (case C), `start_session` auto-generates a unique session ID. You do NOT need to pass one.
122
- - For cases A and B, the session ID already exists; reuse it everywhere — never let `start_session` run in this conversation.
120
+ - For cases A and B, the session ID already exists; reuse it everywhere
123
121
  - The parent conversation is responsible for detecting an existing session ID from current context, the startup banner, or a user-provided session URL/ID, then passing it explicitly to `polygraph-init-subagent`. The init subagent cannot infer parent session context by itself.
124
122
  - For a fresh Codex Desktop conversation started with `/polygraph:session-start`, no `sessionId` is expected; launch `polygraph-init-subagent` without `sessionId` so it creates a new session.
125
123
 
126
- **Launch `polygraph-init-subagent`** (cases B and C — skip in case A):
127
-
128
- Use Codex's `spawn_agent` tool to start the custom Polygraph init subagent:
129
-
130
- ```
131
- spawn_agent(
132
- agent_type: "polygraph-init-subagent",
133
- message: """
134
- Parameters:
135
- - sessionId: "<existing-session-id-or-omit-for-new-session>"
136
- - userContext: "<description of what the user wants to do>"
137
-
138
- If sessionId is provided, reuse that session and use add_repo to attach repositories — do NOT call start_session. If exact repo refs were provided, pass them directly to add_repo and do NOT call list_repos. If discovery is needed, discover candidates and select relevant repos. If sessionId is omitted, create a new session via start_session. Return a structured summary.
139
- """
140
- )
141
- ```
142
-
143
- Omit the `sessionId` line for case C. Include it (with the existing session ID) for case B. When the main flow needs the session before proceeding, collect the result with `wait_agent`.
144
-
145
124
  The subagent will:
146
125
 
147
126
  1. Use exact repo refs directly when provided for an existing session; otherwise call `list_repos` to discover available repositories
@@ -150,7 +129,7 @@ The subagent will:
150
129
  4. Call `show_session` to retrieve session details
151
130
  5. Return a summary with session URL and repo info
152
131
 
153
- **When the init subagent has just created a brand-new session,** render the session welcome card instead of the session-details block below. Prefer the `session_intro` MCP tool call it with the session ID; it returns the card as markdown. If that tool is unavailable, run `polygraph session intro -s <sessionId>` via the CLI instead. The CLI command is intentionally hidden/internal and may not appear in public command listings, but it remains the correct skill fallback for rendering the welcome card. Either way, print the result to the user verbatim as markdown — do NOT wrap it in a code block or reformat it (the logo is pre-fenced; the rest is live markdown). It needs no other input, and you do not need to call `show_session` first. Then continue (ask the user what they want, or start the requested task).
132
+ **When the init subagent has just created a brand-new session,** render the session welcome card instead of the session-details block below. Prefer the `session_intro` MCP tool (or the hidden `polygraph session intro -s <sessionId>` via the CLI) call it with the session ID; it returns the card as markdown. Print the result to the user verbatim as markdown — do NOT wrap it in a code block or reformat it (the logo is pre-fenced; the rest is live markdown). It needs no other input, and you do not need to call `show_session` first. Then continue.
154
133
 
155
134
  **For an existing session — after `show_session` returns or the init subagent's summary arrives — show the session details:**
156
135
 
@@ -170,7 +149,7 @@ Use this workflow when the user gives a Polygraph session ID and asks to underst
170
149
  **Resume is not a work command.** If the user's intent is to resume, reconnect, or reconstruct a prior Polygraph session, fetch and summarize the restored context, then stop. Do not edit files, push branches, add repos, delegate new work, or continue previous changes until the user explicitly asks for changes. Treat "resume" as context restoration followed by waiting for user instructions.
171
150
 
172
151
  1. Fetch detailed session context:
173
- - Prefer `show_session` with `details: true` when the MCP tool exposes that option.
152
+ - Prefer `show_session` with `details: true`
174
153
  - Otherwise run `polygraph session show --details <session-id>`.
175
154
  2. Treat the detailed output as authoritative context. It should include:
176
155
  - `<summary>` — the session summary.
@@ -210,29 +189,8 @@ Inspect the PR commits/diff and investigate the requested behavior. Report findi
210
189
 
211
190
  ### Finding the Session Behind a Commit or Line
212
191
 
213
- Use this workflow when the user asks which Polygraph session produced, is behind, or changed a particular commit — or a particular line of code.
214
-
215
- **Given a commit sha.** When the user names a sha, or asks what session is behind a commit, resolve it with `search_sessions` using the `sha` parameter (CLI: `polygraph session search --sha <sha>`):
216
-
217
- - Pass **exactly one** of `query` or `sha` — they are mutually exclusive.
218
- - `sha` accepts a full or partial sha, 7-40 hex chars.
219
- - The lookup is exact and one-shot: it returns the session(s) linked to that commit, newest first, scoped to the current org, and only explicit sessions.
220
-
221
- ```
222
- search_sessions(sha: "a1b2c3d")
223
- # CLI equivalent:
224
- polygraph session search --sha a1b2c3d
225
- ```
226
-
227
- **Given a line number.** There is no line-number lookup — a line MUST first be resolved to a commit sha with `git blame`, then that sha is fed into the sha lookup:
228
-
229
- 1. `git blame -L <line>,<line> -- <file>` to get the commit that last touched the line.
230
- 2. Pass that sha to `search_sessions(sha: ...)` (or `polygraph session search --sha <sha>`).
231
-
232
- **Reading the results.**
233
-
234
- - Multiple sessions may match a sha. They come back newest first — pick the most relevant one and report the others if they matter.
235
- - **A "no match" result does NOT prove the commit had no work behind it.** Not every commit is linked to an explicit session: commits pushed directly (rather than via an ingested PR) and gaps in ingestion metadata mean the sha may simply not be recorded, and implicit sessions are never returned. Report "no linked session found for that sha" — never assert that no work exists behind the commit.
192
+ Use this workflow when the user asks which Polygraph session produced, is behind, or changed a particular commit — or a particular line of code.
193
+ **Read [`reference/session-by-commit.md`](reference/session-by-commit.md) before running any lookup.** That reference file holds clear, reliable steps for answering questions related to this.
236
194
 
237
195
  ## Agent roles
238
196
 
@@ -271,7 +229,7 @@ spawn_agent(
271
229
  3. The subagent watches `child.status` on its delegation's `children[]` entry — the one matching its repo and role — and exits when it sees a terminal status — typically `'completed'` or `'failed'` (and `'cancelled'` if it was stopped).
272
230
  4. Collect completed results with `wait_agent` when the main flow needs them, then continue to `push_branch` + `create_pr`.
273
231
 
274
- In rare cases where you need to check the raw child agent status directly (e.g., debugging a stuck subagent), you may call the Polygraph MCP `show_agent` as a one-off tool call. Do NOT use this for regular polling that belongs inside `polygraph-delegate-subagent`.
232
+ To debug a stuck subagent you can call `show_agent` as a one-off, but routine polling belongs in the background subagents.
275
233
 
276
234
  Use Simple when the task is well-defined and the child will not need clarification.
277
235
 
@@ -326,7 +284,7 @@ Use Multi-turn when the child may need clarification, the task is exploratory, o
326
284
 
327
285
  Your MCP client supports the native permission dialog. When a child agent requests permission, the dialog renders directly in your UI and the user picks — the decision routes back through `polygraph-mcp` automatically.
328
286
 
329
- **Critical: do NOT call `allow_agent` or `deny_agent` yourself.** If `show_agent` (or `cloud_polygraph_child_status`) briefly reports a child in `permission-required` state with `pendingPermission` populated, that is a transient state the dialog is in the middle of resolving. Your job is to keep polling — the next poll will see the child back in `in-progress` (or `failed` / `cancelled` if the user denied or dismissed).
287
+ **Critical: do NOT call `allow_agent` or `deny_agent` yourself.** If `show_agent` briefly reports a child in `permission-required` state with `pendingPermission` populated, that is a transient state the dialog is in the middle of resolving. Your job is to keep polling — the next poll will see the child back in `in-progress` (or `failed` / `cancelled` if the user denied or dismissed).
330
288
 
331
289
  If you call `allow_agent` while the dialog is already open, you create a race: the user's pick lands first and the explicit allow fails with `Task <id> is in state 'completed', not 'permission-required'`. The child receives the user's choice; your call is wasted work.
332
290
 
@@ -338,30 +296,16 @@ The `allow_agent` and `deny_agent` tools exist for parents whose MCP clients do
338
296
 
339
297
  Publishing covers the branch-to-PR flow: `push_branch` (push local commits; must precede PR creation), `create_pr` (linked draft PRs, including fork PRs via `targetRepository`), `mark_pr_ready` (transition drafts to OPEN), and `associate_pr` (link PRs created outside Polygraph).
340
298
 
341
- **Whenever you push a branch, create or associate a PR, or mark PRs ready, read [`reference/publish-changes.md`](reference/publish-changes.md) first.** That reference file holds the full flow: parameters and examples for each tool, `push_branch` local-checkout semantics, the PR title format rules, and the session-URL printing steps. `push_branch`, `create_pr`, and `associate_pr` all require a `description` following the Session Description Policy below.
299
+ **Whenever you push a branch, create or associate a PR, or mark PRs ready, read [`reference/publish-changes.md`](reference/publish-changes.md) first.** That reference file holds the full flow.
342
300
 
343
301
  ### Session Description Policy
344
302
 
345
303
  `description` is user-facing Polygraph session context. It is required for `push_branch`, `create_pr`, and `associate_pr`, and is the primary input to `update_session` (`mark_pr_ready` does not take a description).
346
304
 
347
- **Whenever you write or update a session description, read [`reference/session-description.md`](reference/session-description.md) first.** That reference file holds the full policy: the canonical Markdown-heading template (`## Goal` / `## Current progress` / `## What worked` / `## Next steps`), the dual-audience guidance (humans in the web UI now, agents reconstructing history later), and the formatting building blocks the app renders (callouts, tables, mermaid, links, `link_reference`).
348
-
349
- ### Get Current Polygraph Session
350
-
351
- Check the details of a session using `show_session` or `polygraph session show --details <session-id>`. Returns the full session state — basic metadata like id, url & description timeline, plus the connected repositories, `pullRequests[]`, per-PR `ciStatus`, and `session.linkedReferences`.
352
-
353
- **Parameters:**
354
-
355
- - `sessionId` (required): The Polygraph session ID
356
-
357
- **CI status rules:**
305
+ **Whenever you write or update a session description, read [`reference/session-description.md`](reference/session-description.md) first.** That reference file holds the full policy.
358
306
 
359
- - `ciStatus[prId].cipeUrl` (null if no CIPE) is a human-facing Nx Cloud web link — display it to the user, but never fetch, curl, or poll it directly; CIPE data is only accessible programmatically via the Nx MCP `ci_information` tool.
360
- - When no CIPE exists, external CI data (e.g., GitHub Actions) appears in `ciStatus[prId].externalCIRuns[]` as runs with nested `jobs[]`; each job's `jobId` is the input for `get_ci_logs`.
361
-
362
- ```
363
- show_session(sessionId: "<session-id>")
364
- ```
307
+ Use `update_session` directly when the user asks to summarize progress, update the session description, or capture the current state.
308
+ Be liberal about updating the session description when you make changes that affect the scope of the session, how logic flows between repos, or anything else important for posterity. Avoid updating it for small implementation details that are not relevant outside of this session. An up-to-date session description matters for maintainability.
365
309
 
366
310
  ### Linked References
367
311
 
@@ -375,55 +319,16 @@ Use `link_reference` to link an external reference to the current Polygraph sess
375
319
 
376
320
  When an external resource is mentioned during a Polygraph session and appears relevant to the current work, the parent agent should record it with `link_reference({ sessionId, reference })`. This applies to relevant external resources such as pull requests, GitHub issues, other Polygraph sessions, and Linear issues.
377
321
 
378
- Invoke the MCP tool with a single object containing `{ sessionId, reference }`. For example, to record a relevant pull request:
379
-
380
- ```
381
- link_reference({
382
- sessionId: "<current-session-id>",
383
- reference: {
384
- type: "github_pr",
385
- url: "https://github.com/nrwl/polygraph-skills/pull/123",
386
- label: "Implementation PR"
387
- }
388
- })
389
- ```
390
-
391
- To record a relevant Polygraph session, use the same invocation shape and include `reference.sessionId`:
392
-
393
- ```
394
- link_reference({
395
- sessionId: "<current-session-id>",
396
- reference: {
397
- type: "session",
398
- url: "https://polygraph.example/s/<inspected-session-id>",
399
- label: "Inspected Polygraph session",
400
- sessionId: "<inspected-session-id>"
401
- }
402
- })
403
- ```
404
-
405
- The canonical MCP parameters are `{ sessionId, reference }`. There is no unlink command.
322
+ The canonical MCP parameters are `{ sessionId, reference }`. There is no unlink command; `show_session` returns a session's existing links as `session.linkedReferences`.
406
323
 
407
324
  ### Add Repositories to a Session
408
325
 
409
326
  Use `add_repo` to add repositories to an existing Polygraph session after it has already started.
410
327
 
411
- **Direct-add rule:** When the user provides exact repo refs by ID, short name, full name, GitHub `owner/repo` slug, or URL-like slug, pass those refs directly to `add_repo` and do not call `list_repos` first. Candidate discovery remains account-repo-only and is only for cases where the user does not know the exact repo or asks to choose/filter candidates.
328
+ **Direct-add rule:** When the user provides exact repo refs by ID, short name, full name, GitHub `owner/repo` slug, or URL-like slug, pass those refs directly to `add_repo` and do not call `list_repos` first. Candidate discovery is only for cases where the user does not know the exact repo.
412
329
 
413
330
  **Not limited to your organization:** repos outside the org — including public open-source repos — can be added by GitHub `owner/repo` slug or URL. Only `list_repos` discovery is org-scoped, so a repo missing from `list_repos` can still be added directly.
414
331
 
415
- **Parameters:**
416
-
417
- - `sessionId` (required): The Polygraph session ID
418
- - `repoIds` (required): Repository IDs or exact repository refs to add. Accepts IDs, short names, full names, GitHub `owner/repo` slugs, and URL-like slugs.
419
-
420
- ```
421
- add_repo(
422
- sessionId: "<session-id>",
423
- repoIds: ["org/repo-name", "facebook/react"]
424
- )
425
- ```
426
-
427
332
  ### Archive Session
428
333
 
429
334
  **IMPORTANT: Only call this tool when the user explicitly asks to archive or close the session.** Do not archive sessions automatically as part of the workflow.
@@ -444,14 +349,6 @@ When you need to fetch and read a failed job's log, read [`reference/ci-job-logs
444
349
 
445
350
  Session repos are shallow (`--depth 1`) clones. When git fails on missing history (`bad object` from `git revert`, `git log`, `git blame`, etc.), call `git_fetch({ sessionId, repo })` and retry. Read [`reference/shallow-clone-history.md`](reference/shallow-clone-history.md) for the CLI form, the `depth`/`refs` options, and the redundant-call behavior.
446
351
 
447
- ### Update Session Description
448
-
449
- Use this when the user asks to summarize progress, update the session description, or capture the current state.
450
-
451
- Read [`reference/session-description.md`](reference/session-description.md) for the full update procedure (what to read before writing, how to append vs. replace) and the canonical Markdown-heading format. Then call `update_session` with the resulting summary as `description`.
452
-
453
- Be liberal about updating the session description when you make changes that affect the scope of the session, how logic flows between repos, or anything else important for posterity. Avoid updating it for small implementation details that are not relevant outside of this session. An up-to-date session description matters for maintainability.
454
-
455
352
  ### Print Polygraph Session Details
456
353
 
457
354
  When asked to print polygraph session details, use `show_session` or `polygraph session show --details <session-id>` and display in the following format.
@@ -472,7 +369,7 @@ If the session has a description timeline, also display:
472
369
  - PR_URL, PR_TITLE, PR_STATUS: from `pullRequests[]`
473
370
  - CI_STATUS: from `ciStatus[prId].status`
474
371
  - SELF_HEALING_STATUS: from `ciStatus[prId].selfHealingStatus` (omit or show `-` if null)
475
- - CIPE_URL: from `ciStatus[prId].cipeUrl`
372
+ - CIPE_URL: from `ciStatus[prId].cipeUrl` (null if no CIPE — omit the CI Link cell) — a human-facing Nx Cloud link: render it for the user, never fetch, curl, or poll it. CIPE data is only reachable via the Nx MCP `ci_information` tool.
476
373
  - POLYGRAPH_SESSION_URL: from `polygraphSessionUrl`
477
374
  - SESSION_DESCRIPTION: from the latest/current item in `description`
478
375
 
@@ -490,7 +387,3 @@ If the session has a description timeline, also display:
490
387
  1. **NEVER call the Polygraph MCP `spawn_agent` or `show_agent` directly for routine delegation**. These MUST run inside `polygraph-delegate-subagent`.
491
388
 
492
389
  1. **Use `stop_agent` to clean up** — Stop child agents that are stuck or no longer needed (pass `role` to target a non-default agent). 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.
493
- 1. **Only archive sessions when asked** — Only call `archive_session` when the user explicitly requests it. Archiving hides the session from active lists; it can still be resumed later.
494
-
495
- 1. **Respect the sandbox** — When a command fails with a sandbox denial (`EPERM` binding a port, blocked host, denied write), stop instead of retrying and point the user to the options in "Sandboxing in Polygraph Sessions": commit harness sandbox settings to the repo, or toggle sandboxing via `polygraph config`.
496
-
@@ -6,6 +6,8 @@ The branch-to-PR flow: push branches, create draft PRs, mark them ready, and ass
6
6
 
7
7
  Once work is complete in a repository, push the branch using `push_branch`. This must be done before creating a PR.
8
8
 
9
+ **If `push_branch` fails, don't guess at the cause.** Report the tool's actual error to the user first. Only once the real cause is clear, offer to fall back to a manual `git push` — let the user decide, rather than falling back unprompted or asserting what a credential/token can or cannot do.
10
+
9
11
  `push_branch` pushes from the local checkout: for the repo you are in, that is your current working directory with your commits; for delegated repos, it is the Polygraph-managed clone the child agent worked in. There is no separate session copy of the current repo.
10
12
 
11
13
  **Parameters:**
@@ -0,0 +1,25 @@
1
+ # Finding the Session Behind a Commit or Line
2
+
3
+ Use this workflow when the user asks which Polygraph session produced, is behind, or changed a particular commit — or a particular line of code.
4
+
5
+ **Given a commit sha.** When the user names a sha, or asks what session is behind a commit, resolve it with `search_sessions` using the `sha` parameter (CLI: `polygraph session search --sha <sha>`):
6
+
7
+ - Pass **exactly one** of `query` or `sha` — they are mutually exclusive.
8
+ - `sha` accepts a full or partial sha, 7-40 hex chars.
9
+ - The lookup is exact and one-shot: it returns the session(s) linked to that commit, newest first, scoped to the current org, and only explicit sessions.
10
+
11
+ ```
12
+ search_sessions(sha: "a1b2c3d")
13
+ # CLI equivalent:
14
+ polygraph session search --sha a1b2c3d
15
+ ```
16
+
17
+ **Given a line number.** There is no line-number lookup — a line MUST first be resolved to a commit sha with `git blame`, then that sha is fed into the sha lookup:
18
+
19
+ 1. `git blame -L <line>,<line> -- <file>` to get the commit that last touched the line.
20
+ 2. Pass that sha to `search_sessions(sha: ...)` (or `polygraph session search --sha <sha>`).
21
+
22
+ **Reading the results.**
23
+
24
+ - Multiple sessions may match a sha. They come back newest first — pick the most relevant one and report the others if they matter.
25
+ - **A "no match" result does NOT prove the commit had no work behind it.** Not every commit is linked to an explicit session: commits pushed directly (rather than via an ingested PR) and gaps in ingestion metadata mean the sha may simply not be recorded, and implicit sessions are never returned. Report "no linked session found for that sha" — never assert that no work exists behind the commit.