@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.
- package/.codex-plugin/plugin.json +1 -1
- package/hooks/agent-session-link.mjs +146 -0
- package/hooks/hooks.json +13 -1
- package/hooks/record-session-mapping.mjs +34 -243
- package/package.json +1 -1
- package/skills/polygraph/SKILL.md +26 -133
- package/skills/polygraph/reference/publish-changes.md +2 -0
- package/skills/polygraph/reference/session-by-commit.md +25 -0
|
@@ -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": "
|
|
27
|
+
"statusMessage": "Linking Polygraph agent session"
|
|
16
28
|
}
|
|
17
29
|
]
|
|
18
30
|
}
|
|
@@ -1,263 +1,54 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
10
|
+
function readPayload() {
|
|
94
11
|
try {
|
|
95
|
-
|
|
12
|
+
const raw = readFileSync(0, 'utf8');
|
|
13
|
+
return raw ? JSON.parse(raw) : undefined;
|
|
96
14
|
} catch {
|
|
97
|
-
return
|
|
15
|
+
return undefined;
|
|
98
16
|
}
|
|
99
17
|
}
|
|
100
18
|
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
114
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
@@ -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
|
|
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*
|
|
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
|
|
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
|
|
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
|
|
91
|
-
1. **Delegate work to each repo** - Use the `polygraph-delegate-subagent` to start child agents
|
|
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. **
|
|
97
|
-
8. **
|
|
98
|
-
9. **
|
|
99
|
-
10. **
|
|
100
|
-
11. **
|
|
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
|
|
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
|
|
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`
|
|
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
|
-
|
|
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`
|
|
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
|
|
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
|
|
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
|
-
|
|
360
|
-
|
|
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
|
-
|
|
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
|
|
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.
|