@polygraph/codex-plugin 0.4.24 → 0.4.25

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.24",
3
+ "version": "0.4.25",
4
4
  "description": "AI agent skills and subagents for Polygraph multi-repo coordination",
5
5
  "author": {
6
6
  "name": "Narwhal Technologies Inc",
@@ -20,6 +20,7 @@
20
20
  ],
21
21
  "skills": "./skills/",
22
22
  "mcpServers": "./.mcp.json",
23
+ "hooks": "./hooks/hooks.json",
23
24
  "interface": {
24
25
  "displayName": "Polygraph",
25
26
  "shortDescription": "Multi-repo coordination skills for Codex.",
@@ -0,0 +1,16 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "matcher": "startup|resume|compact",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node ${PLUGIN_ROOT}/hooks/reinject-polygraph-context.mjs",
10
+ "statusMessage": "Re-injecting Polygraph session context"
11
+ }
12
+ ]
13
+ }
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,161 @@
1
+ // SessionStart hook: when the calling agent (Claude Code or Codex) is running
2
+ // inside a Polygraph session, re-inject the Polygraph session id and basic
3
+ // session info as context. This restores facts that context compaction may
4
+ // have dropped, and re-establishes them on resume.
5
+ //
6
+ // Shared by the Claude and Codex plugins — both fire a SessionStart hook whose
7
+ // stdin carries `session_id` and whose stdout `additionalContext` is injected
8
+ // into the model.
9
+ //
10
+ // Everything is read from local Polygraph state — no network calls:
11
+ // ~/.polygraph/sidecars/<polygraphSessionId>/parent-<agentSessionId>.json
12
+ // maps this agent session id -> Polygraph session id (the "parent log
13
+ // sidecar" the CLI uses to stream parent-agent activity to the UI).
14
+ // ~/.polygraph/sessions/<polygraphSessionId>/session/session.json
15
+ // holds the session's repos, agentType, and orgId.
16
+ // ~/.polygraph/config.json
17
+ // holds selectedUrl, used to build the session URL.
18
+ //
19
+ // Outside a Polygraph session (no matching sidecar) the hook is a silent no-op.
20
+
21
+ import { readFileSync, readdirSync, existsSync, realpathSync } from 'node:fs';
22
+ import { homedir } from 'node:os';
23
+ import path from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+
26
+ export function polygraphRoot(home = homedir()) {
27
+ return path.join(home, '.polygraph');
28
+ }
29
+
30
+ function readJson(file) {
31
+ try {
32
+ return JSON.parse(readFileSync(file, 'utf8'));
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ // Find the sidecar that maps an agent session id to a Polygraph session.
39
+ // Returns the parsed sidecar object, or null when none matches.
40
+ export function findSidecar(agentSessionId, root = polygraphRoot()) {
41
+ if (!agentSessionId) return null;
42
+
43
+ const sidecarsDir = path.join(root, 'sidecars');
44
+ if (!existsSync(sidecarsDir)) return null;
45
+
46
+ const fileName = `parent-${agentSessionId}.json`;
47
+ let entries;
48
+ try {
49
+ entries = readdirSync(sidecarsDir, { withFileTypes: true });
50
+ } catch {
51
+ return null;
52
+ }
53
+
54
+ for (const entry of entries) {
55
+ if (!entry.isDirectory()) continue;
56
+ const candidate = path.join(sidecarsDir, entry.name, fileName);
57
+ if (existsSync(candidate)) {
58
+ return readJson(candidate);
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+
64
+ // Build the context block for a Polygraph session, or null when the agent is
65
+ // not running inside a Polygraph session.
66
+ export function buildPolygraphContext(agentSessionId, root = polygraphRoot()) {
67
+ const sidecar = findSidecar(agentSessionId, root);
68
+ if (!sidecar || !sidecar.sessionId) return null;
69
+
70
+ const polygraphSessionId = sidecar.sessionId;
71
+ const agentType = sidecar.parentAgentType || 'agent';
72
+ const session =
73
+ readJson(
74
+ path.join(root, 'sessions', polygraphSessionId, 'session', 'session.json')
75
+ ) ?? {};
76
+ const config = readJson(path.join(root, 'config.json')) ?? {};
77
+
78
+ const baseUrl = config.selectedUrl;
79
+ const orgId = session.orgId;
80
+ const sessionUrl =
81
+ baseUrl && orgId
82
+ ? `${baseUrl}/orgs/${orgId}/sessions/${polygraphSessionId}`
83
+ : null;
84
+
85
+ const repos = Array.isArray(session.repos) ? session.repos : [];
86
+ const repoLines = repos.map((repo) => {
87
+ const role = repo.isInitiator ? ' (initiator)' : '';
88
+ const strategy = repo.materialization?.strategy
89
+ ? ` [${repo.materialization.strategy}]`
90
+ : '';
91
+ return ` - ${repo.repoFullName}${role}${strategy}`;
92
+ });
93
+
94
+ const lines = [
95
+ 'You are running inside a Polygraph session. Keep this in mind across compaction:',
96
+ `- Polygraph session id: ${polygraphSessionId}`,
97
+ sessionUrl ? `- Session URL: ${sessionUrl}` : null,
98
+ `- Parent agent (${agentType}) session id: ${agentSessionId}`,
99
+ repoLines.length
100
+ ? ['- Repositories in this session:', ...repoLines].join('\n')
101
+ : '- Repositories in this session: (none recorded)',
102
+ '- To act in this session (delegating work, monitoring CI, opening PRs, etc.), load the polygraph skill for guidance.',
103
+ ].filter((line) => line != null);
104
+
105
+ return lines.join('\n');
106
+ }
107
+
108
+ function readStdin() {
109
+ try {
110
+ // fd 0 — Claude Code / Codex pipe the hook payload as JSON on stdin.
111
+ return readFileSync(0, 'utf8');
112
+ } catch {
113
+ return '';
114
+ }
115
+ }
116
+
117
+ export function main() {
118
+ let payload = {};
119
+ const raw = readStdin();
120
+ if (raw) {
121
+ try {
122
+ payload = JSON.parse(raw);
123
+ } catch {
124
+ payload = {};
125
+ }
126
+ }
127
+
128
+ const agentSessionId =
129
+ payload.session_id || process.env.CLAUDE_CODE_SESSION_ID || '';
130
+
131
+ const context = buildPolygraphContext(agentSessionId);
132
+ if (!context) return; // not a Polygraph session — stay silent
133
+
134
+ process.stdout.write(
135
+ JSON.stringify({
136
+ hookSpecificOutput: {
137
+ hookEventName: 'SessionStart',
138
+ additionalContext: context,
139
+ },
140
+ })
141
+ );
142
+ }
143
+
144
+ // Run only when executed directly as a hook, not when imported (e.g. by tests).
145
+ // realpath both sides so the check holds when the plugin lives under a symlinked
146
+ // path (e.g. macOS /tmp -> /private/tmp, or a symlinked plugin install dir),
147
+ // where import.meta.url is realpath'd by Node but process.argv[1] is not.
148
+ function isMainModule() {
149
+ if (!process.argv[1]) return false;
150
+ try {
151
+ return (
152
+ realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))
153
+ );
154
+ } catch {
155
+ return false;
156
+ }
157
+ }
158
+
159
+ if (isMainModule()) {
160
+ main();
161
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polygraph/codex-plugin",
3
- "version": "0.4.24",
3
+ "version": "0.4.25",
4
4
  "description": "AI agent skills and subagents for Polygraph multi-repo coordination",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -26,6 +26,7 @@
26
26
  ".codex-plugin/",
27
27
  "skills/",
28
28
  "agents/",
29
+ "hooks/",
29
30
  ".mcp.json",
30
31
  "README.md",
31
32
  "bin/"