@primitive.ai/prim 0.1.0-alpha.8 → 0.1.0-alpha.81
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/README.md +295 -37
- package/SKILL.md +269 -0
- package/dist/chunk-3EEFL6NJ.js +529 -0
- package/dist/chunk-4CZTNXIS.js +505 -0
- package/dist/chunk-4GLVJLEQ.js +55 -0
- package/dist/chunk-BQUJ4AFJ.js +771 -0
- package/dist/chunk-CUE2LKLR.js +155 -0
- package/dist/chunk-EZXEWFDG.js +210 -0
- package/dist/chunk-FI3LIRCB.js +262 -0
- package/dist/chunk-HWSS4BV5.js +1400 -0
- package/dist/chunk-LEMX3IOG.js +79 -0
- package/dist/chunk-LMRYZL3D.js +209 -0
- package/dist/chunk-OUXH3UEP.js +247 -0
- package/dist/chunk-PMRDE2DA.js +310 -0
- package/dist/chunk-RO3F6JYV.js +126 -0
- package/dist/chunk-TJTVHDJ5.js +321 -0
- package/dist/chunk-WB7AUZ4C.js +52 -0
- package/dist/chunk-XGADQPCI.js +26 -0
- package/dist/chunk-YWO7O6QG.js +327 -0
- package/dist/chunk-Z7YPFWAZ.js +917 -0
- package/dist/chunk-ZOSNEO5N.js +77 -0
- package/dist/daemon/server.js +15329 -0
- package/dist/hook-runtime/daemon/server.js +15329 -0
- package/dist/hook-runtime/hooks/post-commit.js +1128 -0
- package/dist/hook-runtime/hooks/post-rewrite.js +1148 -0
- package/dist/hook-runtime/hooks/post-tool-use.js +7586 -0
- package/dist/hook-runtime/hooks/pre-commit.js +1314 -0
- package/dist/hook-runtime/hooks/pre-tool-use.js +7099 -0
- package/dist/hook-runtime/hooks/prim-hook.js +7691 -0
- package/dist/hook-runtime/hooks/session-end.js +495 -0
- package/dist/hook-runtime/hooks/session-start.js +11313 -0
- package/dist/hook-runtime/index.js +44696 -0
- package/dist/hook-runtime/statusline-main.js +765 -0
- package/dist/hooks/post-commit.js +179 -0
- package/dist/hooks/post-rewrite.js +197 -0
- package/dist/hooks/post-tool-use.js +250 -0
- package/dist/hooks/pre-commit.js +21 -74
- package/dist/hooks/pre-tool-use.js +348 -0
- package/dist/hooks/prim-hook.js +195 -0
- package/dist/hooks/session-end.js +70 -0
- package/dist/hooks/session-start.js +299 -0
- package/dist/index.js +20981 -519
- package/dist/statusline-main.js +765 -0
- package/package.json +35 -13
- package/dist/chunk-3APLWTLB.js +0 -165
package/dist/hooks/pre-commit.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
checkAffectedDecisions,
|
|
4
|
+
formatDecisionsWarning
|
|
5
|
+
} from "../chunk-RO3F6JYV.js";
|
|
6
|
+
import "../chunk-Z7YPFWAZ.js";
|
|
7
|
+
import "../chunk-CUE2LKLR.js";
|
|
8
|
+
import "../chunk-3EEFL6NJ.js";
|
|
5
9
|
|
|
6
10
|
// src/hooks/pre-commit.ts
|
|
7
11
|
import { execSync } from "child_process";
|
|
@@ -11,81 +15,24 @@ function getStagedFiles() {
|
|
|
11
15
|
});
|
|
12
16
|
return output.trim().split("\n").filter((f) => f.length > 0);
|
|
13
17
|
}
|
|
14
|
-
function
|
|
15
|
-
const regexStr = pattern.replaceAll("**", "\xA7GLOBSTAR\xA7").replaceAll("*", "[^/]*").replaceAll("\xA7GLOBSTAR\xA7", ".*");
|
|
16
|
-
const regex = new RegExp(`^${regexStr}$`);
|
|
17
|
-
return regex.test(filePath);
|
|
18
|
-
}
|
|
19
|
-
function findAffectedContexts(stagedFiles, specs) {
|
|
20
|
-
const affected = /* @__PURE__ */ new Map();
|
|
21
|
-
for (const file of stagedFiles) {
|
|
22
|
-
for (const spec of specs) {
|
|
23
|
-
for (const pattern of spec.filePatterns) {
|
|
24
|
-
if (matchPattern(file, pattern)) {
|
|
25
|
-
const existing = affected.get(spec._id);
|
|
26
|
-
if (existing) {
|
|
27
|
-
existing.matchedFiles.push(file);
|
|
28
|
-
} else {
|
|
29
|
-
affected.set(spec._id, {
|
|
30
|
-
contextId: spec._id,
|
|
31
|
-
matchedFiles: [file]
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
break;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return affected;
|
|
40
|
-
}
|
|
41
|
-
var HOOK_TIMEOUT_MS = 1e4;
|
|
42
|
-
async function main() {
|
|
18
|
+
async function runDecisionsCheck() {
|
|
43
19
|
const stagedFiles = getStagedFiles();
|
|
44
20
|
if (stagedFiles.length === 0) {
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
const client = getClient();
|
|
48
|
-
let mappings = [];
|
|
49
|
-
try {
|
|
50
|
-
mappings = await client.get("/api/cli/specs/mappings", {
|
|
51
|
-
signal: AbortSignal.timeout(HOOK_TIMEOUT_MS)
|
|
52
|
-
});
|
|
53
|
-
} catch {
|
|
54
|
-
process.exit(0);
|
|
55
|
-
}
|
|
56
|
-
if (mappings.length === 0) {
|
|
57
|
-
process.exit(0);
|
|
21
|
+
return { decisions: [], truncated: false };
|
|
58
22
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const ctx = await client.get(`/api/cli/contexts/${contextId}`, {
|
|
67
|
-
signal: AbortSignal.timeout(HOOK_TIMEOUT_MS)
|
|
68
|
-
});
|
|
69
|
-
if (!ctx._id) {
|
|
70
|
-
console.log(` [skip] ${contextId} \u2014 not found`);
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (!ctx.isSpecDocument) {
|
|
74
|
-
console.log(` [skip] ${contextId} \u2014 not a spec document`);
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
await client.post(`/api/cli/contexts/${contextId}/sync`, void 0, {
|
|
78
|
-
signal: AbortSignal.timeout(HOOK_TIMEOUT_MS)
|
|
79
|
-
});
|
|
80
|
-
console.log(` [synced] ${contextId} \u2014 ${ctx.name ?? "(unnamed)"}`);
|
|
81
|
-
} catch (error) {
|
|
82
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
83
|
-
console.error(` [error] ${contextId} \u2014 ${message}`);
|
|
84
|
-
}
|
|
23
|
+
return checkAffectedDecisions(stagedFiles);
|
|
24
|
+
}
|
|
25
|
+
async function main() {
|
|
26
|
+
const decisionsResult = await runDecisionsCheck();
|
|
27
|
+
const warning = formatDecisionsWarning(decisionsResult);
|
|
28
|
+
if (warning) {
|
|
29
|
+
console.error(warning);
|
|
85
30
|
}
|
|
86
31
|
process.exit(0);
|
|
87
32
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
33
|
+
if (!process.env.VITEST) {
|
|
34
|
+
main().catch((error) => {
|
|
35
|
+
console.error("[prim] Pre-commit hook error:", error);
|
|
36
|
+
process.exit(0);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
analyzeShellTargets,
|
|
4
|
+
buildCodexOutput,
|
|
5
|
+
buildHermesOutput,
|
|
6
|
+
buildHookOutput,
|
|
7
|
+
demoteForMode,
|
|
8
|
+
extractFileTargets,
|
|
9
|
+
failOpenCodex,
|
|
10
|
+
failOpenHermes,
|
|
11
|
+
failOpenOutput,
|
|
12
|
+
readHookMode
|
|
13
|
+
} from "../chunk-BQUJ4AFJ.js";
|
|
14
|
+
import {
|
|
15
|
+
appendCodexContext,
|
|
16
|
+
hasVisibleCodexMessage,
|
|
17
|
+
prepareCodexContext
|
|
18
|
+
} from "../chunk-4CZTNXIS.js";
|
|
19
|
+
import {
|
|
20
|
+
packageVersion
|
|
21
|
+
} from "../chunk-PMRDE2DA.js";
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_RULES,
|
|
24
|
+
scrub
|
|
25
|
+
} from "../chunk-FI3LIRCB.js";
|
|
26
|
+
import {
|
|
27
|
+
boundedHealthError
|
|
28
|
+
} from "../chunk-LEMX3IOG.js";
|
|
29
|
+
import {
|
|
30
|
+
canonicalGitRoot,
|
|
31
|
+
canonicalRepositoryPath,
|
|
32
|
+
isRepoActive,
|
|
33
|
+
repoSyncId
|
|
34
|
+
} from "../chunk-TJTVHDJ5.js";
|
|
35
|
+
import {
|
|
36
|
+
getClient
|
|
37
|
+
} from "../chunk-Z7YPFWAZ.js";
|
|
38
|
+
import {
|
|
39
|
+
normalizeEnvelope,
|
|
40
|
+
parseAgent
|
|
41
|
+
} from "../chunk-WB7AUZ4C.js";
|
|
42
|
+
import "../chunk-CUE2LKLR.js";
|
|
43
|
+
import "../chunk-3EEFL6NJ.js";
|
|
44
|
+
|
|
45
|
+
// src/hooks/preflight-v3.ts
|
|
46
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
47
|
+
var PREFLIGHT_PROTOCOL_VERSION = 3;
|
|
48
|
+
var PREFLIGHT_TIMEOUT_MS = 6500;
|
|
49
|
+
var MAX_PREFLIGHT_PATHS = 32;
|
|
50
|
+
var MAX_PROPOSAL_BYTES = 6144;
|
|
51
|
+
var MAX_CLIENT_VERSION_CHARS = 32;
|
|
52
|
+
var CLIENT_VERSION_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
|
|
53
|
+
var MAX_DECISION_DISCLOSURES = 16;
|
|
54
|
+
var DISCLOSED_DECISION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u;
|
|
55
|
+
var DISCLOSED_DECISION_SHORT_ID_RE = /^[0-9a-f]{8}$/u;
|
|
56
|
+
function boundedClientVersion(raw) {
|
|
57
|
+
return typeof raw === "string" && raw.length <= MAX_CLIENT_VERSION_CHARS && CLIENT_VERSION_RE.test(raw) ? raw : "unknown";
|
|
58
|
+
}
|
|
59
|
+
function resolvePreflightTargets(args) {
|
|
60
|
+
let rawPaths;
|
|
61
|
+
let coverage = "complete";
|
|
62
|
+
let mutation = "present";
|
|
63
|
+
if (args.agent === "claude_code" && args.toolName === "Bash") {
|
|
64
|
+
const command = args.toolInput && typeof args.toolInput === "object" ? args.toolInput.command : void 0;
|
|
65
|
+
if (typeof command !== "string") {
|
|
66
|
+
return { paths: [], coverage: "unverified", mutation: "present" };
|
|
67
|
+
}
|
|
68
|
+
const shell = analyzeShellTargets(command);
|
|
69
|
+
rawPaths = shell.paths;
|
|
70
|
+
coverage = shell.coverage;
|
|
71
|
+
mutation = shell.mutation;
|
|
72
|
+
} else {
|
|
73
|
+
const extracted = extractFileTargets(args.toolName, args.toolInput, args.agent);
|
|
74
|
+
if (!extracted) return { paths: [], coverage: "complete", mutation: "none" };
|
|
75
|
+
rawPaths = extracted.paths;
|
|
76
|
+
if (!extracted.complete) coverage = "unverified";
|
|
77
|
+
}
|
|
78
|
+
const root = canonicalGitRoot(args.cwd);
|
|
79
|
+
const paths = /* @__PURE__ */ new Set();
|
|
80
|
+
for (const rawPath of rawPaths) {
|
|
81
|
+
const canonical = canonicalRepositoryPath(rawPath, args.cwd, root);
|
|
82
|
+
if (canonical) paths.add(canonical);
|
|
83
|
+
else coverage = "unverified";
|
|
84
|
+
}
|
|
85
|
+
const bounded = [...paths];
|
|
86
|
+
if (bounded.length > MAX_PREFLIGHT_PATHS) coverage = "unverified";
|
|
87
|
+
return { paths: bounded.slice(0, MAX_PREFLIGHT_PATHS), coverage, mutation };
|
|
88
|
+
}
|
|
89
|
+
function truncateUtf8(value, maxBytes) {
|
|
90
|
+
const encoded = Buffer2.from(value);
|
|
91
|
+
if (encoded.byteLength <= maxBytes) return value;
|
|
92
|
+
return encoded.subarray(0, maxBytes).toString("utf8").replace(/\uFFFD$/u, "");
|
|
93
|
+
}
|
|
94
|
+
function proposalFor(toolInput) {
|
|
95
|
+
const redacted = scrub(toolInput, DEFAULT_RULES);
|
|
96
|
+
return truncateUtf8(JSON.stringify(redacted) ?? "", MAX_PROPOSAL_BYTES);
|
|
97
|
+
}
|
|
98
|
+
function parsePreflightResponse(value) {
|
|
99
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
100
|
+
const response = value;
|
|
101
|
+
const verdicts = /* @__PURE__ */ new Set(["allow", "warn", "ask", "block", "unavailable"]);
|
|
102
|
+
if (response.protocolVersion !== PREFLIGHT_PROTOCOL_VERSION || typeof response.verdict !== "string" || !verdicts.has(response.verdict) || typeof response.reasonCode !== "string" || typeof response.message !== "string" || !Array.isArray(response.conflicts) || !Array.isArray(response.bypassed)) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const disclosures = response.decisionDisclosures;
|
|
106
|
+
if (disclosures !== void 0) {
|
|
107
|
+
if (!Array.isArray(disclosures) || disclosures.length === 0 || disclosures.length > MAX_DECISION_DISCLOSURES) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const seen = /* @__PURE__ */ new Set();
|
|
111
|
+
for (const value2 of disclosures) {
|
|
112
|
+
if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return null;
|
|
113
|
+
const disclosure = value2;
|
|
114
|
+
if (Object.keys(disclosure).length !== 3 || !Object.hasOwn(disclosure, "decisionId") || !Object.hasOwn(disclosure, "shortId") || !Object.hasOwn(disclosure, "participation") || typeof disclosure.decisionId !== "string" || !DISCLOSED_DECISION_ID_RE.test(disclosure.decisionId) || typeof disclosure.shortId !== "string" || !DISCLOSED_DECISION_SHORT_ID_RE.test(disclosure.shortId) || disclosure.participation !== "candidate" && disclosure.participation !== "reconcile_bypass" || seen.has(disclosure.decisionId)) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
seen.add(disclosure.decisionId);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return response;
|
|
121
|
+
}
|
|
122
|
+
function decisionDisclosureContext(response) {
|
|
123
|
+
return response.decisionDisclosures?.map(({ decisionId }) => `[primitive] hidden Decision: prim decisions show ${decisionId}`).join("\n") ?? "";
|
|
124
|
+
}
|
|
125
|
+
async function requestPreflight(request, signal = AbortSignal.timeout(PREFLIGHT_TIMEOUT_MS)) {
|
|
126
|
+
const response = await getClient().post("/api/cli/decisions/conflict-check", request, {
|
|
127
|
+
signal,
|
|
128
|
+
quietRefresh: true
|
|
129
|
+
});
|
|
130
|
+
return parsePreflightResponse(response);
|
|
131
|
+
}
|
|
132
|
+
function resultForPreflight(response) {
|
|
133
|
+
const verdict = response.verdict === "block" ? "deny" : response.verdict;
|
|
134
|
+
const message = boundedHealthError(response.message) ?? "";
|
|
135
|
+
const unavailable = message || boundedHealthError(response.reasonCode);
|
|
136
|
+
const safe = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
137
|
+
const directives = response.conflicts.flatMap((value) => {
|
|
138
|
+
if (!value || typeof value !== "object") return [];
|
|
139
|
+
const { decisionId, shortId } = value;
|
|
140
|
+
const id = typeof shortId === "string" && safe.test(shortId) ? `dec_${shortId}` : typeof decisionId === "string" && safe.test(decisionId) ? decisionId : void 0;
|
|
141
|
+
return id ? [`To reconcile, run: prim reconcile ${id}`] : [];
|
|
142
|
+
});
|
|
143
|
+
const reason = [message, ...new Set(directives)].filter(Boolean).join("\n");
|
|
144
|
+
const disclosureContext = decisionDisclosureContext(response);
|
|
145
|
+
return {
|
|
146
|
+
verdict,
|
|
147
|
+
conflicts: response.conflicts,
|
|
148
|
+
reason,
|
|
149
|
+
additionalContext: disclosureContext || (["warn", "ask", "block"].includes(response.verdict) ? reason : ""),
|
|
150
|
+
truncated: false,
|
|
151
|
+
unavailable: response.verdict === "unavailable" ? unavailable : void 0
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function unverifiedResult(message) {
|
|
155
|
+
return {
|
|
156
|
+
verdict: "unavailable",
|
|
157
|
+
conflicts: [],
|
|
158
|
+
reason: "",
|
|
159
|
+
additionalContext: "",
|
|
160
|
+
truncated: false,
|
|
161
|
+
unavailable: message
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/hooks/pre-tool-use.ts
|
|
166
|
+
var STDIN_TIMEOUT_MS = 1e3;
|
|
167
|
+
async function readStdin() {
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
const chunks = [];
|
|
170
|
+
const timer = setTimeout(() => {
|
|
171
|
+
reject(new Error("stdin read timeout"));
|
|
172
|
+
}, STDIN_TIMEOUT_MS);
|
|
173
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
174
|
+
process.stdin.on("end", () => {
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
resolve(Buffer.concat(chunks).toString("utf-8"));
|
|
177
|
+
});
|
|
178
|
+
process.stdin.on("error", (err) => {
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
reject(err);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
var agent = parseAgent(process.argv);
|
|
185
|
+
async function emit(output) {
|
|
186
|
+
return await new Promise((resolve) => {
|
|
187
|
+
try {
|
|
188
|
+
process.stdout.write(`${JSON.stringify(output)}
|
|
189
|
+
`, (error) => resolve(!error));
|
|
190
|
+
} catch {
|
|
191
|
+
resolve(false);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
async function emitWithAcknowledgment(output, acknowledge) {
|
|
196
|
+
const handedOff = await emit(output);
|
|
197
|
+
await acknowledge?.(handedOff);
|
|
198
|
+
}
|
|
199
|
+
function failOpen() {
|
|
200
|
+
if (agent === "hermes") return failOpenHermes();
|
|
201
|
+
return agent === "codex" ? failOpenCodex() : failOpenOutput();
|
|
202
|
+
}
|
|
203
|
+
function invocationId(envelope) {
|
|
204
|
+
const value = agent === "hermes" ? envelope.extra?.tool_call_id : envelope.tool_use_id;
|
|
205
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
206
|
+
}
|
|
207
|
+
function hermesAdvisory(result, aggregate) {
|
|
208
|
+
const parts = [];
|
|
209
|
+
if (aggregate === "warn") parts.push(result.reason);
|
|
210
|
+
if (result.verdict === "unavailable") {
|
|
211
|
+
parts.push(result.unavailable ?? result.reason);
|
|
212
|
+
}
|
|
213
|
+
if (result.additionalContext && result.additionalContext !== result.reason) {
|
|
214
|
+
parts.push(result.additionalContext);
|
|
215
|
+
}
|
|
216
|
+
const message = [...new Set(parts.filter(Boolean))].join("\n");
|
|
217
|
+
return message.startsWith("[primitive]") ? message : message ? `[primitive] ${message}` : "";
|
|
218
|
+
}
|
|
219
|
+
async function emitUnverified(message, envelope) {
|
|
220
|
+
const result = unverifiedResult(message);
|
|
221
|
+
if (agent === "hermes") {
|
|
222
|
+
process.stderr.write(`[primitive] ${message}
|
|
223
|
+
`);
|
|
224
|
+
await emit(failOpenHermes());
|
|
225
|
+
} else {
|
|
226
|
+
let output = agent === "codex" ? buildCodexOutput("allow", [result]) : buildHookOutput("allow", [result]);
|
|
227
|
+
if (agent === "codex" && typeof envelope?.session_id === "string" && envelope.session_id) {
|
|
228
|
+
try {
|
|
229
|
+
const context = await prepareCodexContext({
|
|
230
|
+
cwd: envelope.cwd ?? process.cwd(),
|
|
231
|
+
sessionId: envelope.session_id,
|
|
232
|
+
includeDigest: false
|
|
233
|
+
});
|
|
234
|
+
output = appendCodexContext(output, context.context);
|
|
235
|
+
await emitWithAcknowledgment(output, context.acknowledge);
|
|
236
|
+
return;
|
|
237
|
+
} catch {
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
await emit(output);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function main() {
|
|
244
|
+
let raw;
|
|
245
|
+
try {
|
|
246
|
+
raw = await readStdin();
|
|
247
|
+
} catch {
|
|
248
|
+
await emit(failOpen());
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
let envelope;
|
|
252
|
+
try {
|
|
253
|
+
envelope = normalizeEnvelope(
|
|
254
|
+
JSON.parse(raw),
|
|
255
|
+
agent
|
|
256
|
+
);
|
|
257
|
+
} catch {
|
|
258
|
+
await emit(failOpen());
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (envelope.hook_event_name !== "PreToolUse") {
|
|
262
|
+
await emit(failOpen());
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const env = process.env;
|
|
266
|
+
const mode = readHookMode(env);
|
|
267
|
+
if (mode === "off") {
|
|
268
|
+
await emit(failOpen());
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const toolName = typeof envelope.tool_name === "string" ? envelope.tool_name : "";
|
|
272
|
+
const cwd = typeof envelope.cwd === "string" && envelope.cwd.length > 0 ? envelope.cwd : process.cwd();
|
|
273
|
+
const targets = resolvePreflightTargets({
|
|
274
|
+
toolName,
|
|
275
|
+
toolInput: envelope.tool_input,
|
|
276
|
+
agent,
|
|
277
|
+
cwd
|
|
278
|
+
});
|
|
279
|
+
if (targets.mutation === "none") {
|
|
280
|
+
await emit(failOpen());
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (!isRepoActive(cwd)) {
|
|
284
|
+
await emit(failOpen());
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (targets.paths.length === 0) {
|
|
288
|
+
await emit(failOpen());
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const binding = repoSyncId(cwd);
|
|
292
|
+
const sessionId = envelope.session_id;
|
|
293
|
+
const callId = invocationId(envelope);
|
|
294
|
+
if (!binding || typeof sessionId !== "string" || !sessionId || !callId) {
|
|
295
|
+
await emitUnverified(
|
|
296
|
+
"GitHub repo connection is required for repository-specific file attribution, Conflict Gate verification, and commit correlation; or tool invocation identity is unavailable",
|
|
297
|
+
envelope
|
|
298
|
+
);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const request = {
|
|
302
|
+
protocolVersion: PREFLIGHT_PROTOCOL_VERSION,
|
|
303
|
+
agent,
|
|
304
|
+
clientMode: mode,
|
|
305
|
+
clientVersion: boundedClientVersion(packageVersion()),
|
|
306
|
+
sessionId,
|
|
307
|
+
invocationId: callId,
|
|
308
|
+
repoSyncId: binding,
|
|
309
|
+
paths: targets.paths,
|
|
310
|
+
coverage: targets.coverage,
|
|
311
|
+
proposal: proposalFor(envelope.tool_input)
|
|
312
|
+
};
|
|
313
|
+
let result;
|
|
314
|
+
try {
|
|
315
|
+
const response = await requestPreflight(request);
|
|
316
|
+
if (!response) {
|
|
317
|
+
await emitUnverified("enforcement service returned an incompatible response", envelope);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
result = resultForPreflight(response);
|
|
321
|
+
} catch {
|
|
322
|
+
await emitUnverified("enforcement service unavailable; change was not verified", envelope);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const aggregate = demoteForMode(
|
|
326
|
+
result.verdict === "unavailable" ? "allow" : result.verdict,
|
|
327
|
+
mode
|
|
328
|
+
);
|
|
329
|
+
if (agent === "hermes" && aggregate !== "deny" && aggregate !== "ask") {
|
|
330
|
+
const advisory = hermesAdvisory(result, aggregate);
|
|
331
|
+
if (advisory) process.stderr.write(`${advisory}
|
|
332
|
+
`);
|
|
333
|
+
}
|
|
334
|
+
let output = agent === "hermes" ? buildHermesOutput(aggregate, [result]) : agent === "codex" ? buildCodexOutput(aggregate, [result]) : buildHookOutput(aggregate, [result]);
|
|
335
|
+
if (agent === "codex" && hasVisibleCodexMessage(output) && typeof sessionId === "string" && sessionId.length > 0) {
|
|
336
|
+
try {
|
|
337
|
+
const context = await prepareCodexContext({ cwd, sessionId, includeDigest: false });
|
|
338
|
+
output = appendCodexContext(output, context.context);
|
|
339
|
+
await emitWithAcknowledgment(output, context.acknowledge);
|
|
340
|
+
return;
|
|
341
|
+
} catch {
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
await emit(output);
|
|
345
|
+
}
|
|
346
|
+
main().catch(async () => {
|
|
347
|
+
await emit(failOpen());
|
|
348
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
buildHookOutput,
|
|
4
|
+
handoffHookOutput
|
|
5
|
+
} from "../chunk-4GLVJLEQ.js";
|
|
6
|
+
import {
|
|
7
|
+
FEEDBACK_DEADLINE_MS,
|
|
8
|
+
acknowledgeDecisionFeedback,
|
|
9
|
+
leaseDecisionFeedback,
|
|
10
|
+
renderFeedback
|
|
11
|
+
} from "../chunk-LMRYZL3D.js";
|
|
12
|
+
import {
|
|
13
|
+
enrichHookPayloadWithFileRefs,
|
|
14
|
+
preserveHookFileMetadata
|
|
15
|
+
} from "../chunk-ZOSNEO5N.js";
|
|
16
|
+
import {
|
|
17
|
+
postToolInvocationId,
|
|
18
|
+
shouldFlushAfter,
|
|
19
|
+
toMove
|
|
20
|
+
} from "../chunk-OUXH3UEP.js";
|
|
21
|
+
import {
|
|
22
|
+
getOrCreateWorkspaceId
|
|
23
|
+
} from "../chunk-EZXEWFDG.js";
|
|
24
|
+
import {
|
|
25
|
+
appendMove,
|
|
26
|
+
resolveOrg
|
|
27
|
+
} from "../chunk-YWO7O6QG.js";
|
|
28
|
+
import "../chunk-BQUJ4AFJ.js";
|
|
29
|
+
import {
|
|
30
|
+
prepareCodexContext
|
|
31
|
+
} from "../chunk-4CZTNXIS.js";
|
|
32
|
+
import "../chunk-PMRDE2DA.js";
|
|
33
|
+
import {
|
|
34
|
+
scrubFromCwd,
|
|
35
|
+
writeHookDebug
|
|
36
|
+
} from "../chunk-FI3LIRCB.js";
|
|
37
|
+
import "../chunk-LEMX3IOG.js";
|
|
38
|
+
import {
|
|
39
|
+
isRepoActiveForCapture,
|
|
40
|
+
repoSyncId,
|
|
41
|
+
resolveRepositoryContext
|
|
42
|
+
} from "../chunk-TJTVHDJ5.js";
|
|
43
|
+
import "../chunk-Z7YPFWAZ.js";
|
|
44
|
+
import {
|
|
45
|
+
normalizeEnvelope,
|
|
46
|
+
parseAgent
|
|
47
|
+
} from "../chunk-WB7AUZ4C.js";
|
|
48
|
+
import "../chunk-CUE2LKLR.js";
|
|
49
|
+
import "../chunk-3EEFL6NJ.js";
|
|
50
|
+
|
|
51
|
+
// src/hooks/prim-hook.ts
|
|
52
|
+
import { spawn } from "child_process";
|
|
53
|
+
import { readFileSync } from "fs";
|
|
54
|
+
import { dirname, join } from "path";
|
|
55
|
+
import { fileURLToPath } from "url";
|
|
56
|
+
|
|
57
|
+
// src/hooks/codex-message-context.ts
|
|
58
|
+
var defaultDeps = { prepare: prepareCodexContext };
|
|
59
|
+
async function processCodexMessageContext(envelope, deps = defaultDeps) {
|
|
60
|
+
const event = envelope.hook_event_name;
|
|
61
|
+
if (event === "Stop") return { output: {} };
|
|
62
|
+
if (event !== "UserPromptSubmit") return { output: {} };
|
|
63
|
+
if (typeof envelope.session_id !== "string" || envelope.session_id.length === 0) {
|
|
64
|
+
return { output: {} };
|
|
65
|
+
}
|
|
66
|
+
let prepared;
|
|
67
|
+
try {
|
|
68
|
+
prepared = await deps.prepare({
|
|
69
|
+
cwd: typeof envelope.cwd === "string" ? envelope.cwd : process.cwd(),
|
|
70
|
+
sessionId: envelope.session_id
|
|
71
|
+
});
|
|
72
|
+
} catch {
|
|
73
|
+
return { output: {} };
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
output: prepared.context ? {
|
|
77
|
+
hookSpecificOutput: {
|
|
78
|
+
hookEventName: "UserPromptSubmit",
|
|
79
|
+
additionalContext: prepared.context
|
|
80
|
+
}
|
|
81
|
+
} : {},
|
|
82
|
+
// Even an empty prompt response is a successful handoff. Committing it
|
|
83
|
+
// advances verified feed state and report dedup without inventing output.
|
|
84
|
+
acknowledge: async () => {
|
|
85
|
+
await prepared.acknowledge(true);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/hooks/prim-hook.ts
|
|
91
|
+
var here = dirname(fileURLToPath(import.meta.url));
|
|
92
|
+
var outputAttempted = false;
|
|
93
|
+
function emitOutput(output, acknowledge) {
|
|
94
|
+
outputAttempted = true;
|
|
95
|
+
return handoffHookOutput(output, acknowledge);
|
|
96
|
+
}
|
|
97
|
+
function resolveCliVersion() {
|
|
98
|
+
try {
|
|
99
|
+
const pkg = JSON.parse(readFileSync(join(here, "..", "..", "package.json"), "utf-8"));
|
|
100
|
+
return pkg.version ?? "unknown";
|
|
101
|
+
} catch {
|
|
102
|
+
return "unknown";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function spawnBackgroundFlush() {
|
|
106
|
+
const entry = join(here, "..", "index.js");
|
|
107
|
+
spawn(process.execPath, [entry, "moves", "flush"], {
|
|
108
|
+
detached: true,
|
|
109
|
+
stdio: "ignore"
|
|
110
|
+
}).unref();
|
|
111
|
+
}
|
|
112
|
+
async function main() {
|
|
113
|
+
const feedbackSignal = AbortSignal.timeout(FEEDBACK_DEADLINE_MS);
|
|
114
|
+
const agent = parseAgent(process.argv);
|
|
115
|
+
let raw;
|
|
116
|
+
let parsed;
|
|
117
|
+
try {
|
|
118
|
+
raw = readFileSync(0, "utf-8");
|
|
119
|
+
parsed = normalizeEnvelope(JSON.parse(raw), agent);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
writeHookDebug("capture failed", error);
|
|
122
|
+
await emitOutput(buildHookOutput({}));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const cwd = parsed.cwd ?? process.cwd();
|
|
126
|
+
const isClaudeStop = agent === "claude_code" && parsed.hook_event_name === "Stop";
|
|
127
|
+
const isCodexContextEvent = agent === "codex" && (parsed.hook_event_name === "UserPromptSubmit" || parsed.hook_event_name === "Stop");
|
|
128
|
+
if (!isRepoActiveForCapture(cwd)) {
|
|
129
|
+
if (isClaudeStop || isCodexContextEvent) await emitOutput(buildHookOutput({}));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const identity = getOrCreateWorkspaceId(cwd);
|
|
133
|
+
const workspaceId = identity.status === "ready" ? identity.workspaceId : void 0;
|
|
134
|
+
const resolvedRepository = resolveRepositoryContext(cwd);
|
|
135
|
+
const repository = resolvedRepository ? { ...resolvedRepository, repoSyncId: repoSyncId(cwd) } : null;
|
|
136
|
+
try {
|
|
137
|
+
const enrichment = repository ? enrichHookPayloadWithFileRefs({ parsed, agent, cwd, repository }) : void 0;
|
|
138
|
+
const enriched = enrichment?.parsed ?? parsed;
|
|
139
|
+
const invocationId = postToolInvocationId(enriched, agent);
|
|
140
|
+
const base = toMove(
|
|
141
|
+
enriched,
|
|
142
|
+
resolveCliVersion(),
|
|
143
|
+
agent,
|
|
144
|
+
workspaceId,
|
|
145
|
+
repository,
|
|
146
|
+
invocationId
|
|
147
|
+
);
|
|
148
|
+
const scrubbed = await scrubFromCwd(enriched, cwd);
|
|
149
|
+
const move = {
|
|
150
|
+
...base,
|
|
151
|
+
payload: enrichment ? preserveHookFileMetadata(scrubbed, enrichment.resolution) : scrubbed
|
|
152
|
+
};
|
|
153
|
+
const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd });
|
|
154
|
+
appendMove(move, orgId);
|
|
155
|
+
if (shouldFlushAfter(move.eventType)) {
|
|
156
|
+
spawnBackgroundFlush();
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
writeHookDebug("capture failed", error);
|
|
160
|
+
}
|
|
161
|
+
if (isCodexContextEvent) {
|
|
162
|
+
const result = await processCodexMessageContext(parsed);
|
|
163
|
+
await emitOutput(result.output, result.acknowledge);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!isClaudeStop) return;
|
|
167
|
+
const sessionId = typeof parsed.session_id === "string" ? parsed.session_id : "";
|
|
168
|
+
if (!workspaceId || !sessionId) {
|
|
169
|
+
await emitOutput(buildHookOutput({}));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const lease = await leaseDecisionFeedback(
|
|
173
|
+
{ workspaceId, currentSessionId: sessionId, signal: feedbackSignal },
|
|
174
|
+
{ onError: (error) => writeHookDebug("feedback failed", error) }
|
|
175
|
+
);
|
|
176
|
+
const rendered = lease ? renderFeedback(lease) : void 0;
|
|
177
|
+
await emitOutput(
|
|
178
|
+
buildHookOutput({ systemMessage: rendered?.systemMessage }),
|
|
179
|
+
rendered ? async () => {
|
|
180
|
+
await acknowledgeDecisionFeedback(
|
|
181
|
+
{
|
|
182
|
+
protocolVersion: rendered.protocolVersion,
|
|
183
|
+
workspaceId,
|
|
184
|
+
deliveries: rendered.deliveries,
|
|
185
|
+
signal: feedbackSignal
|
|
186
|
+
},
|
|
187
|
+
{ onError: (error) => writeHookDebug("feedback failed", error) }
|
|
188
|
+
);
|
|
189
|
+
} : void 0
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
void main().catch(async (error) => {
|
|
193
|
+
writeHookDebug("capture failed", error);
|
|
194
|
+
if (!outputAttempted) await emitOutput(buildHookOutput({}));
|
|
195
|
+
});
|