@zaip-ai/cli 4.0.0 → 4.2.0
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/dist/services/adapter-hook-sources.d.ts +1 -0
- package/dist/services/adapter-hook-sources.d.ts.map +1 -1
- package/dist/services/adapter-hook-sources.js +1 -0
- package/dist/services/adapter-hook-sources.js.map +1 -1
- package/dist/services/adapters.d.ts +1 -1
- package/dist/services/adapters.d.ts.map +1 -1
- package/dist/services/adapters.js +17 -19
- package/dist/services/adapters.js.map +1 -1
- package/dist/services/adapters.test.js +1 -0
- package/dist/services/adapters.test.js.map +1 -1
- package/dist/services/codex-config-template.d.ts +2 -2
- package/dist/services/codex-config-template.d.ts.map +1 -1
- package/dist/services/codex-config-template.js +4 -3
- package/dist/services/codex-config-template.js.map +1 -1
- package/dist/services/codex-pre-tool-hook-template.d.ts.map +1 -1
- package/dist/services/codex-pre-tool-hook-template.js +23 -55
- package/dist/services/codex-pre-tool-hook-template.js.map +1 -1
- package/dist/services/codex-pre-tool-hook-template.test.d.ts +2 -0
- package/dist/services/codex-pre-tool-hook-template.test.d.ts.map +1 -0
- package/dist/services/codex-pre-tool-hook-template.test.js +95 -0
- package/dist/services/codex-pre-tool-hook-template.test.js.map +1 -0
- package/dist/services/codex-pre-tool-support-template.d.ts +2 -0
- package/dist/services/codex-pre-tool-support-template.d.ts.map +1 -0
- package/dist/services/codex-pre-tool-support-template.js +110 -0
- package/dist/services/codex-pre-tool-support-template.js.map +1 -0
- package/dist/services/codex-subagent-hook-template.d.ts +1 -1
- package/dist/services/codex-subagent-hook-template.d.ts.map +1 -1
- package/dist/services/codex-subagent-hook-template.js +40 -8
- package/dist/services/codex-subagent-hook-template.js.map +1 -1
- package/dist/services/codex-windows-hook-command.d.ts +3 -0
- package/dist/services/codex-windows-hook-command.d.ts.map +1 -0
- package/dist/services/codex-windows-hook-command.js +12 -0
- package/dist/services/codex-windows-hook-command.js.map +1 -0
- package/dist/services/codex-windows-hook-command.test.d.ts +2 -0
- package/dist/services/codex-windows-hook-command.test.d.ts.map +1 -0
- package/dist/services/codex-windows-hook-command.test.js +44 -0
- package/dist/services/codex-windows-hook-command.test.js.map +1 -0
- package/dist/services/developer-identity-fixture.d.ts +3 -0
- package/dist/services/developer-identity-fixture.d.ts.map +1 -0
- package/dist/services/developer-identity-fixture.js +17 -0
- package/dist/services/developer-identity-fixture.js.map +1 -0
- package/dist/services/global-codex-config.d.ts.map +1 -1
- package/dist/services/global-codex-config.js +27 -13
- package/dist/services/global-codex-config.js.map +1 -1
- package/dist/services/global-codex-config.test.js +27 -2
- package/dist/services/global-codex-config.test.js.map +1 -1
- package/dist/services/runtime-template.test.js +3 -0
- package/dist/services/runtime-template.test.js.map +1 -1
- package/package.json +1 -1
- package/templates/.agents/config/agents/discovery.json +1 -1
- package/templates/.agents/dashboard/.zaip-managed.json +14 -14
- package/templates/.agents/dashboard/apps/frontend/package.json +1 -1
- package/templates/.agents/dashboard/modules/cost-monitor/package.json +1 -1
- package/templates/.agents/dashboard/modules/operations/package.json +1 -1
- package/templates/.agents/dashboard/modules/traceability/package.json +1 -1
- package/templates/.agents/dashboard/packages/branding/package.json +1 -1
- package/templates/.agents/dashboard/packages/contracts/package.json +1 -1
- package/templates/.agents/dashboard/packages/data-export/package.json +1 -1
- package/templates/.agents/dashboard/packages/local-runtime/dist/server.cjs +141 -141
- package/templates/.agents/dashboard/packages/local-runtime/dist/sqlite-read-model.cjs +2 -2
- package/templates/.agents/dashboard/packages/local-runtime/package.json +1 -1
- package/templates/.agents/dashboard/packages/local-runtime/server.mjs +141 -141
- package/templates/.agents/dashboard/packages/ui/package.json +1 -1
- package/templates/.agents/dashboard/tests/e2e/package.json +1 -1
- package/templates/.agents/design/.zaip-managed.json +3 -3
- package/templates/.agents/design/apps/catalog/package.json +1 -1
- package/templates/.agents/design/packages/local-runtime/package.json +1 -1
- package/templates/.agents/requirements/autonomous-assurance.md +1 -1
- package/templates/.agents/requirements/cloud-hostinger-autonomy.md +1 -1
- package/templates/.agents/requirements/comparative-audit.md +1 -1
- package/templates/.agents/requirements/context-engine.md +1 -1
- package/templates/.agents/requirements/cost-observability.md +1 -1
- package/templates/.agents/requirements/dashboard-launch.md +1 -1
- package/templates/.agents/requirements/data-export.md +1 -1
- package/templates/.agents/requirements/design-system.md +1 -1
- package/templates/.agents/requirements/execution-engine.md +1 -1
- package/templates/.agents/requirements/kanban-dashboard.md +1 -1
- package/templates/.agents/requirements/model-orchestration.md +1 -1
- package/templates/.agents/requirements/public-distribution.md +1 -1
- package/templates/.agents/requirements/software-architecture.md +1 -1
- package/templates/.agents/requirements/strategic-improvements.md +1 -1
- package/templates/.agents/requirements/work-lifecycle.md +1 -1
- package/templates/.agents/requirements/zaip-strategic-95.md +1 -1
- package/templates/.agents/runtime/VERSION +1 -1
- package/templates/.agents/runtime/bin/zaip.mjs +206 -98
- package/templates/.agents/runtime/checksums.sha256 +5 -5
- package/templates/.agents/runtime/manifest.json +1 -1
- package/templates/.agents/runtime/provenance.json +4 -4
- package/templates/.agents/runtime/sbom.cdx.json +1 -1
- package/templates/.agents/zaip-aios/VERSION +1 -1
- package/templates/template-manifest.json +45 -45
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export const CODEX_PRE_TOOL_SUPPORT_HOOK = String.raw `import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { lstat, mkdir, realpath, rename, writeFile } from "node:fs/promises";
|
|
4
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
const execute = promisify(execFile);
|
|
8
|
+
const inside = (path, root) => {
|
|
9
|
+
const scoped = relative(root, resolve(path));
|
|
10
|
+
return scoped === "" || (!scoped.startsWith("..") && !isAbsolute(scoped));
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function selectHookRepositoryRoot(installedRoot, cwd) {
|
|
14
|
+
if (typeof cwd !== "string") throw new Error("Tool execution without a workspace is blocked.");
|
|
15
|
+
const physicalCwd = await realpath(cwd).catch(() => null);
|
|
16
|
+
if (!physicalCwd || physicalCwd.toLowerCase() !== resolve(cwd).toLowerCase()) {
|
|
17
|
+
throw new Error("Tool workspace could not be safely resolved.");
|
|
18
|
+
}
|
|
19
|
+
if (inside(cwd, installedRoot)) return installedRoot;
|
|
20
|
+
try {
|
|
21
|
+
const git = async (path, ...args) => (await execute("git", ["-C", path, ...args], {
|
|
22
|
+
encoding: "utf8", windowsHide: true, timeout: 5000, maxBuffer: 1048576,
|
|
23
|
+
})).stdout.trim();
|
|
24
|
+
const primary = await git(installedRoot, "rev-parse", "--show-toplevel");
|
|
25
|
+
if ((await realpath(primary)).toLowerCase() !== installedRoot.toLowerCase()) throw new Error("primary_not_root");
|
|
26
|
+
const worktrees = await git(installedRoot, "worktree", "list", "--porcelain", "-z");
|
|
27
|
+
const registered = worktrees.split("\0").filter(line => line.startsWith("worktree ")).map(line => line.slice(9));
|
|
28
|
+
const candidate = await realpath(await git(cwd, "rev-parse", "--show-toplevel"));
|
|
29
|
+
if (!registered.some(path => resolve(path).toLowerCase() === candidate.toLowerCase()) || !inside(cwd, candidate)) {
|
|
30
|
+
throw new Error("worktree_not_registered");
|
|
31
|
+
}
|
|
32
|
+
const common = async path => (await realpath(await git(path, "rev-parse", "--path-format=absolute", "--git-common-dir"))).toLowerCase();
|
|
33
|
+
if (await common(installedRoot) !== await common(candidate)) throw new Error("common_git_dir_mismatch");
|
|
34
|
+
return candidate;
|
|
35
|
+
} catch {
|
|
36
|
+
throw new Error("Tool workspace could not be verified as a registered worktree.");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function tokenizeLiteralCommand(command) {
|
|
41
|
+
if (command.includes("\r") || command.includes("\n") || command.includes(";") ||
|
|
42
|
+
command.includes("|") || command.includes("&") || command.includes(String.fromCharCode(96))) return null;
|
|
43
|
+
const tokens = [];
|
|
44
|
+
let token = "", quote = null, started = false;
|
|
45
|
+
for (const character of command) {
|
|
46
|
+
if (quote !== null) {
|
|
47
|
+
if (character === quote) quote = null;
|
|
48
|
+
else token += character;
|
|
49
|
+
} else if (character === "'" || character === '"') {
|
|
50
|
+
quote = character;
|
|
51
|
+
started = true;
|
|
52
|
+
} else if (/\s/.test(character)) {
|
|
53
|
+
if (started) { tokens.push(token); token = ""; started = false; }
|
|
54
|
+
} else {
|
|
55
|
+
token += character;
|
|
56
|
+
started = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (quote !== null) return null;
|
|
60
|
+
if (started) tokens.push(token);
|
|
61
|
+
return tokens;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function captureCliTurn(repositoryRoot, event, command) {
|
|
65
|
+
const tokens = tokenizeLiteralCommand(command);
|
|
66
|
+
if (!tokens || tokens.length < 4 || !/^(?:node|node\.exe)$/i.test(tokens[0])) return;
|
|
67
|
+
const script = resolve(event.cwd, tokens[1]);
|
|
68
|
+
if (script.toLowerCase() !== resolve(repositoryRoot, ".agents/runtime/bin/zaip.mjs").toLowerCase()) return;
|
|
69
|
+
if (!((tokens[2] === "conversation" || tokens[2] === "run") && tokens[3] === "start")) return;
|
|
70
|
+
if (tokens.some(token => token.startsWith("--codex-hook-"))) throw new Error("Codex hook identity cannot be supplied by the caller.");
|
|
71
|
+
const threadId = event.session_id, turnId = event.turn_id, toolUseId = event.tool_use_id;
|
|
72
|
+
if (typeof threadId !== "string" || !/^[A-Za-z0-9_-]{1,256}$/.test(threadId) ||
|
|
73
|
+
typeof turnId !== "string" || !turnId || turnId.length > 256 ||
|
|
74
|
+
typeof toolUseId !== "string" || !toolUseId || toolUseId.length > 256 ||
|
|
75
|
+
(process.env.CODEX_THREAD_ID && process.env.CODEX_THREAD_ID !== threadId) ||
|
|
76
|
+
(process.env.CODEX_TURN_ID && process.env.CODEX_TURN_ID !== turnId)) {
|
|
77
|
+
throw new Error("Codex thread, turn and tool identity could not be captured for the ZAIP CLI.");
|
|
78
|
+
}
|
|
79
|
+
const directory = resolve(repositoryRoot, ".local/codex-hook-turn/v1");
|
|
80
|
+
const argvSha256 = createHash("sha256").update(JSON.stringify(tokens.slice(2))).digest("hex");
|
|
81
|
+
const digest = createHash("sha256").update(JSON.stringify([threadId, toolUseId])).digest("hex");
|
|
82
|
+
const target = resolve(directory, digest + ".json");
|
|
83
|
+
const temporary = resolve(directory, digest + "." + randomUUID() + ".tmp");
|
|
84
|
+
try {
|
|
85
|
+
let parent = repositoryRoot;
|
|
86
|
+
for (const part of [".local", "codex-hook-turn", "v1"]) {
|
|
87
|
+
parent = resolve(parent, part);
|
|
88
|
+
try { await mkdir(parent); } catch (error) { if (error?.code !== "EEXIST") throw error; }
|
|
89
|
+
const info = await lstat(parent), physical = await realpath(parent);
|
|
90
|
+
if (!info.isDirectory() || info.isSymbolicLink() || !inside(physical, repositoryRoot)) throw new Error("Unsafe turn receipt directory");
|
|
91
|
+
}
|
|
92
|
+
await writeFile(temporary, JSON.stringify({
|
|
93
|
+
schema_version: 1, thread_id: threadId, turn_id: turnId,
|
|
94
|
+
tool_use_sha256: digest, argv_sha256: argvSha256, observed_at_ms: Date.now(),
|
|
95
|
+
}) + "\n", { flag: "wx" });
|
|
96
|
+
await rename(temporary, target);
|
|
97
|
+
} catch {
|
|
98
|
+
throw new Error("Codex turn identity could not be persisted for the ZAIP CLI.");
|
|
99
|
+
}
|
|
100
|
+
return command + " --codex-hook-id=" + digest + " --codex-hook-thread=" + threadId;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function rewriteCliInput(toolInput, command, commandFromCmd) {
|
|
104
|
+
return { hookSpecificOutput: {
|
|
105
|
+
hookEventName: "PreToolUse", permissionDecision: "allow",
|
|
106
|
+
updatedInput: { ...toolInput, command, ...(commandFromCmd === undefined ? {} : { cmd: command }) },
|
|
107
|
+
} };
|
|
108
|
+
}
|
|
109
|
+
`;
|
|
110
|
+
//# sourceMappingURL=codex-pre-tool-support-template.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codex-pre-tool-support-template.js","sourceRoot":"","sources":["../../src/services/codex-pre-tool-support-template.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,2BAA2B,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4GpD,CAAC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CODEX_SUBAGENT_LIFECYCLE_HOOK = "import { existsSync } from \"node:fs\";\nimport { promises as fs } from \"node:fs\";\nimport { spawn } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { dirname, relative, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst hookPath = fileURLToPath(import.meta.url);\nconst repositoryRoot = resolve(dirname(hookPath), \"..\", \"..\");\nconst diagnosticPath = resolve(\n repositoryRoot,\n \".agents\",\n \"local\",\n \"hooks\",\n \"subagent-lifecycle.ndjson\",\n);\nconst pendingRoot = resolve(repositoryRoot, \".local\", \"hooks\", \"subagent-pending\");\n\nif (process.argv[2] === \"--capture-runner\") {\n await runCaptureRunner(process.argv[3]);\n process.exit(0);\n}\nif (process.argv[2] === \"--replay-pending\") {\n const files = await fs.readdir(pendingRoot).catch(() => []);\n let replayed = 0;\n for (const file of files.filter((name) => /^[a-f0-9]{64}\\.json$/.test(name)).slice(0, 256)) {\n const path = resolve(pendingRoot, file);\n const receipt = await fs.readFile(path, \"utf8\").then(JSON.parse).catch(() => null);\n if (receipt?.state === \"pending\" && Array.isArray(receipt.capture_arguments)) { await runCaptureRunner(path, true); if (++replayed >= 3) break; }\n }\n process.exit(0);\n}\n\nlet raw = \"\";\nfor await (const chunk of process.stdin) raw += chunk;\n\ntry {\n const event = JSON.parse(raw);\n const cwd = text(event.cwd);\n const scoped = cwd === null ? \"..\" : relative(repositoryRoot, resolve(cwd));\n if (cwd === null || scoped.startsWith(\"..\")) process.exit(0);\n const eventName = text(event.hook_event_name);\n const lifecycleEvent =\n eventName === \"SubagentStart\" ? \"start\" : eventName === \"SubagentStop\" ? \"stop\" : null;\n const session = text(event.session_id);\n const turn = text(event.turn_id);\n const agentId = text(event.agent_id);\n const agentType = text(event.agent_type);\n const model = text(event.model);\n const reasoningEffort = reasoningEffortValue(\n event.model_reasoning_effort ?? event.reasoning_effort,\n );\n const parentAgentId = text(event.parent_agent_id ?? event.parent_id);\n const parentRunId = text(event.parent_run_id ?? event.root_run_id);\n const status = terminalStatus(event.status);\n const required = [lifecycleEvent, session, turn, agentId, agentType];\n if (lifecycleEvent === null) {\n await diagnostic(\"ignored_unrecognized_event\", { eventName });\n process.exit(0);\n }\n const terminalFallback =\n lifecycleEvent === \"stop\" &&\n status !== null &&\n agentId !== null &&\n parentAgentId !== null &&\n parentRunId !== null &&\n (session === null || turn === null);\n if (\n (!terminalFallback && required.some((value) => value === null)) ||\n (lifecycleEvent === \"start\" && model === null) ||\n (lifecycleEvent === \"stop\" && status === null)\n ) {\n await fs.mkdir(pendingRoot, { recursive: true });\n const identity = JSON.stringify({ eventName, session, turn, agentId, agentType, parentAgentId, parentRunId });\n const pending = resolve(pendingRoot, createHash(\"sha256\").update(identity).digest(\"hex\") + \".json\");\n await atomicSave(pending, { schema_version: 1, state: \"pending\", last_error: \"incomplete_host_metadata\", identity: JSON.parse(identity), recovery: \"Capture observed native metadata manually; never infer completed status.\" });\n await diagnostic(\"ignored_incomplete_event\", {\n eventName,\n session,\n turn,\n agentId,\n agentType,\n missing_model: lifecycleEvent === \"start\" && model === null,\n });\n process.exit(0);\n }\n const candidates = [\n resolve(repositoryRoot, \".agents\", \"runtime\", \"bin\", \"zaip.mjs\"),\n resolve(repositoryRoot, \"packages\", \"cli\", \"dist\", \"index.js\"),\n ];\n const cli = candidates.find(existsSync);\n if (cli === undefined) {\n await diagnostic(\"cli_not_found\", { eventName, candidates });\n process.exit(0);\n }\n const captureArguments = terminalFallback\n ? [\n \"subagent\",\n \"capture-terminal\",\n \"--parent-agent-id\",\n parentAgentId,\n \"--parent-run-id\",\n parentRunId,\n \"--agent-id\",\n agentId,\n \"--agent-type\",\n agentType,\n \"--status\",\n status,\n \"--json\",\n ]\n : [\n \"subagent\",\n \"capture\",\n \"--event\",\n lifecycleEvent,\n \"--session\",\n session,\n \"--turn\",\n turn,\n \"--agent-id\",\n agentId,\n \"--agent-type\",\n agentType,\n ...(model === null ? [] : [\"--model\", model]),\n ...(reasoningEffort === null ? [] : [\"--reasoning-effort\", reasoningEffort]),\n ...(parentAgentId === null ? [] : [\"--parent-agent-id\", parentAgentId]),\n ...(lifecycleEvent !== \"stop\" || status === null ? [] : [\"--status\", status]),\n \"--json\",\n ];\n const pending = await persistPending(cli, captureArguments);\n const receipt = await fs.readFile(pending, \"utf8\").then(JSON.parse);\n if (receipt.state !== \"acknowledged\") {\n const runner = spawn(\n process.execPath,\n [hookPath, \"--capture-runner\", pending],\n {\n cwd: repositoryRoot,\n detached: true,\n windowsHide: true,\n stdio: \"ignore\",\n },\n );\n runner.once(\"error\", (error) =>\n diagnostic(\"runner_spawn_failed\", { eventName, error: error.message }),\n );\n runner.unref();\n }\n if (lifecycleEvent === \"start\") {\n process.stdout.write(JSON.stringify({\n hookSpecificOutput: {\n hookEventName: \"SubagentStart\",\n additionalContext:\n \"ZAIP Codex delegation policy: a Sub-Run is only a native collaboration subagent. Do not use create_thread or fork_thread as a Sub-Run, fallback, retry, or delegation proof; those create independent chats and require an explicit user request. If native delegation is unavailable, report the limitation rather than create an independent chat. A catalog name, task_name, title, or prompt is not native ancestry. Before retrying the same bounded dispatch, await or reuse its pending native child; capture only ancestry observed from native host spawn/terminal results or SubagentStart/SubagentStop events, and work only as a cataloged zaip-agent-NOME identity. Delegation is recursive; do not use default, worker, explorer, or ad-hoc agent types.\",\n },\n }));\n }\n} catch (error) {\n await diagnostic(\"hook_exception\", {\n error: error instanceof Error ? error.message : String(error),\n });\n}\n\nasync function atomicSave(path, value) {\n const temporary = path + \".tmp-\" + randomUUID();\n await fs.writeFile(temporary, JSON.stringify(value), { flag: \"wx\", mode: 0o600 });\n for (let retry = 0; ; retry++) {\n try { await fs.rename(temporary, path); break; }\n catch (error) { if (![\"EPERM\", \"EACCES\", \"EBUSY\"].includes(error.code) || retry >= 5) throw error; await new Promise(done => setTimeout(done, 20 * (retry + 1))); }\n }\n}\n\nasync function runCaptureRunner(receiptPath, replay = false) {\n if (typeof receiptPath !== \"string\" || dirname(resolve(receiptPath)) !== pendingRoot) return;\n const lockPath = receiptPath + \".lock\";\n let lease;\n try {\n lease = await fs.open(lockPath, \"wx\");\n await lease.writeFile(JSON.stringify({ pid: process.pid }));\n } catch (error) {\n if (error.code !== \"EEXIST\") return;\n const owner = await fs.readFile(lockPath, \"utf8\").then(JSON.parse).catch(() => null);\n if (!Number.isSafeInteger(owner?.pid) || owner.pid <= 0) return;\n try { process.kill(owner.pid, 0); return; } catch (failure) { if (failure.code !== \"ESRCH\") return; }\n // Serialize stale lock recovery so two recovering workers cannot remove a new lease.\n let recovery;\n try { recovery = await fs.open(lockPath + \".recover\", \"wx\"); } catch { return; }\n try {\n const current = await fs.readFile(lockPath, \"utf8\").then(JSON.parse).catch(() => null);\n if (current?.pid !== owner.pid) return;\n await fs.unlink(lockPath);\n lease = await fs.open(lockPath, \"wx\");\n await lease.writeFile(JSON.stringify({ pid: process.pid }));\n } finally { await recovery.close(); await fs.unlink(lockPath + \".recover\").catch(() => {}); }\n if (!lease) return;\n }\n try {\n const receipt = await fs.readFile(receiptPath, \"utf8\").then(JSON.parse);\n if (receipt.schema_version !== 1 || receipt.state === \"acknowledged\" || (!replay && receipt.attempts >= 3)) return;\n const cli = receipt.cli, args = receipt.capture_arguments;\n const allowedCli = [resolve(repositoryRoot, \".agents/runtime/bin/zaip.mjs\"), resolve(repositoryRoot, \"packages/cli/dist/index.js\")];\n if (!allowedCli.includes(cli) || !Array.isArray(args) || !args.every(x => typeof x === \"string\") || args[0] !== \"subagent\" || ![\"capture\", \"capture-terminal\"].includes(args[1])) return;\n const digest = createHash(\"sha256\").update(JSON.stringify({ cli, arguments: args })).digest(\"hex\");\n if (resolve(pendingRoot, digest + \".json\") !== resolve(receiptPath)) return;\n const value = flag => args[args.indexOf(flag) + 1];\n const stop = args[1] === \"capture-terminal\" || value(\"--event\") === \"stop\";\n if (stop && ![\"completed\", \"failed\", \"cancelled\"].includes(value(\"--status\"))) return;\n for (let retry = 0; retry < 3; retry++) {\n receipt.attempts = (receipt.attempts ?? 0) + 1;\n receipt.updated_at = new Date().toISOString();\n await atomicSave(receiptPath, receipt);\n const outcome = await capture(cli, args);\n let result;\n try { result = JSON.parse(outcome.stdout); } catch { result = null; }\n const runId = result?.run?.run_id ?? result?.run?.id;\n const status = result?.status ?? result?.run?.status;\n if (outcome.code === 0 && result?.captured === true && typeof runId === \"string\" && runId.startsWith(\"RUN-\") && (!stop || status === value(\"--status\"))) {\n receipt.state = \"acknowledged\";\n receipt.run_id = runId;\n receipt.acknowledged_at = new Date().toISOString();\n delete receipt.last_error;\n await atomicSave(receiptPath, receipt);\n return;\n }\n receipt.last_error = outcome.error ?? (outcome.code === 0 ? \"capture_not_confirmed\" : \"capture_exit_nonzero\");\n await atomicSave(receiptPath, receipt);\n if (retry < 2) await new Promise(done => setTimeout(done, 250 * (retry + 1)));\n }\n await persistDiagnostic(\"capture_pending\", { receipt: receiptPath, attempts: receipt.attempts, error: receipt.last_error });\n } catch (error) {\n await persistDiagnostic(\"runner_exception\", { error: error instanceof Error ? error.message : String(error) });\n } finally {\n await lease?.close();\n await fs.unlink(lockPath).catch(() => {});\n }\n}\n\nasync function capture(cli, args) {\n const child = spawn(process.execPath, [cli, ...args], { cwd: repositoryRoot, windowsHide: true, stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n let stdout = \"\", overflow = false;\n child.stdout.on(\"data\", chunk => { stdout += String(chunk); if (stdout.length > 262144) { overflow = true; stdout = \"\"; child.kill(); } });\n child.stderr.resume();\n return await new Promise(done => {\n let settled = false;\n const finish = result => { if (settled) return; settled = true; clearTimeout(timer); done({ ...result, stdout: overflow ? \"\" : stdout }); };\n const timer = setTimeout(() => { child.kill(); finish({ error: \"capture_timeout\" }); }, 15000);\n child.once(\"error\", () => finish({ error: \"capture_spawn_failed\" }));\n child.once(\"close\", code => finish({ code }));\n });\n}\n\nasync function persistPending(cli, captureArguments) {\n const identity = JSON.stringify({ cli, arguments: captureArguments });\n const digest = createHash(\"sha256\").update(identity).digest(\"hex\");\n const path = resolve(pendingRoot, digest + \".json\");\n await fs.mkdir(pendingRoot, { recursive: true });\n const receipt = { schema_version: 1, state: \"pending\", cli, capture_arguments: captureArguments, attempts: 0, created_at: new Date().toISOString() };\n // Publish a complete immutable initial receipt without overwriting another worker's acknowledgement.\n const temporary = path + \".tmp-\" + randomUUID();\n await fs.writeFile(temporary, JSON.stringify(receipt), { flag: \"wx\", mode: 0o600 });\n try { await fs.link(temporary, path); } catch (error) { if (error.code !== \"EEXIST\") throw error; }\n finally { await fs.unlink(temporary); }\n return path;\n}\n\nasync function persistDiagnostic(code, detail) {\n const line = `${JSON.stringify({\n source: \"zaip-subagent-lifecycle-runner\",\n code,\n occurred_at: new Date().toISOString(),\n detail,\n })}\\n`;\n await fs.mkdir(dirname(diagnosticPath), { recursive: true });\n const size = await fs.stat(diagnosticPath).then((entry) => entry.size).catch(() => 0);\n if (size + Buffer.byteLength(line) > 262_144) {\n await fs.writeFile(diagnosticPath, \"\", \"utf8\");\n }\n await fs.appendFile(diagnosticPath, line, \"utf8\");\n}\n\nasync function diagnostic(code, detail) {\n await persistDiagnostic(code, detail);\n process.stderr.write(\n `${JSON.stringify({ source: \"zaip-subagent-lifecycle\", code, detail })}\\n`,\n );\n}\n\nfunction text(value) {\n if (typeof value !== \"string\") return null;\n const normalized = value.trim();\n return normalized.length > 0 && normalized.length <= 200 ? normalized : null;\n}\n\nfunction reasoningEffortValue(value) {\n const normalized = text(value);\n return [\"low\", \"medium\", \"high\", \"xhigh\"].includes(normalized) ? normalized : null;\n}\n\nfunction terminalStatus(value) {\n const normalized = text(value);\n return [\"completed\", \"failed\", \"cancelled\"].includes(normalized) ? normalized : null;\n}\n";
|
|
1
|
+
export declare const CODEX_SUBAGENT_LIFECYCLE_HOOK = "import { existsSync, realpathSync } from \"node:fs\";\nimport { promises as fs } from \"node:fs\";\nimport { execFileSync, spawn } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { dirname, isAbsolute, relative, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst hookPath = fileURLToPath(import.meta.url);\nconst installedRoot = resolve(dirname(hookPath), \"..\", \"..\");\nlet repositoryRoot = installedRoot;\nlet diagnosticPath = resolve(\n repositoryRoot,\n \".agents\",\n \"local\",\n \"hooks\",\n \"subagent-lifecycle.ndjson\",\n);\nlet pendingRoot = resolve(repositoryRoot, \".local\", \"hooks\", \"subagent-pending\");\n\nfunction inside(path, root) {\n const scoped = relative(root, resolve(path));\n return scoped === \"\" || (!scoped.startsWith(\"..\") && !isAbsolute(scoped));\n}\nfunction selectRoot(cwd) {\n if (typeof cwd !== \"string\") return false;\n try {\n if (realpathSync(cwd).toLowerCase() !== resolve(cwd).toLowerCase()) return false;\n if (!inside(cwd, installedRoot)) {\n const git = (root, ...args) => execFileSync(\"git\", [\"-C\", root, ...args], {\n encoding: \"utf8\", windowsHide: true, timeout: 5000,\n }).trim();\n if (realpathSync(git(installedRoot, \"rev-parse\", \"--show-toplevel\")).toLowerCase() !== installedRoot.toLowerCase()) return false;\n const registered = git(installedRoot, \"worktree\", \"list\", \"--porcelain\", \"-z\")\n .split(\"\\0\").filter(line => line.startsWith(\"worktree \")).map(line => resolve(line.slice(9)).toLowerCase());\n const candidate = realpathSync(git(cwd, \"rev-parse\", \"--show-toplevel\"));\n if (!registered.includes(candidate.toLowerCase()) || !inside(cwd, candidate)) return false;\n const common = root => realpathSync(git(root, \"rev-parse\", \"--path-format=absolute\", \"--git-common-dir\")).toLowerCase();\n if (common(installedRoot) !== common(candidate)) return false;\n repositoryRoot = candidate;\n }\n diagnosticPath = resolve(repositoryRoot, \".agents\", \"local\", \"hooks\", \"subagent-lifecycle.ndjson\");\n pendingRoot = resolve(repositoryRoot, \".local\", \"hooks\", \"subagent-pending\");\n return true;\n } catch { return false; }\n}\n\nif (process.argv[2] === \"--capture-runner\") {\n if (!selectRoot(process.cwd())) process.exit(0);\n await runCaptureRunner(process.argv[3]);\n process.exit(0);\n}\nif (process.argv[2] === \"--replay-pending\") {\n if (!selectRoot(process.cwd())) process.exit(0);\n const files = await fs.readdir(pendingRoot).catch(() => []);\n let replayed = 0;\n for (const file of files.filter((name) => /^[a-f0-9]{64}\\.json$/.test(name)).slice(0, 256)) {\n const path = resolve(pendingRoot, file);\n const receipt = await fs.readFile(path, \"utf8\").then(JSON.parse).catch(() => null);\n if (receipt?.state === \"pending\" && Array.isArray(receipt.capture_arguments)) { await runCaptureRunner(path, true); if (++replayed >= 3) break; }\n }\n process.exit(0);\n}\n\nlet raw = \"\";\nfor await (const chunk of process.stdin) raw += chunk;\n\ntry {\n const event = JSON.parse(raw);\n const cwd = text(event.cwd);\n if (cwd === null || !selectRoot(cwd)) process.exit(0);\n const eventName = text(event.hook_event_name);\n const lifecycleEvent =\n eventName === \"SubagentStart\" ? \"start\" : eventName === \"SubagentStop\" ? \"stop\" : null;\n const session = text(event.session_id);\n const turn = text(event.turn_id);\n const agentId = text(event.agent_id);\n const agentType = text(event.agent_type);\n const model = text(event.model);\n const reasoningEffort = reasoningEffortValue(\n event.model_reasoning_effort ?? event.reasoning_effort,\n );\n const parentAgentId = text(event.parent_agent_id ?? event.parent_id);\n const parentRunId = text(event.parent_run_id ?? event.root_run_id);\n const status = terminalStatus(event.status);\n const required = [lifecycleEvent, session, turn, agentId, agentType];\n if (lifecycleEvent === null) {\n await diagnostic(\"ignored_unrecognized_event\", { eventName });\n process.exit(0);\n }\n const terminalFallback =\n lifecycleEvent === \"stop\" &&\n status !== null &&\n agentId !== null &&\n parentAgentId !== null &&\n parentRunId !== null &&\n (session === null || turn === null);\n if (\n (!terminalFallback && required.some((value) => value === null)) ||\n (lifecycleEvent === \"start\" && model === null) ||\n (lifecycleEvent === \"stop\" && status === null)\n ) {\n await fs.mkdir(pendingRoot, { recursive: true });\n const identity = JSON.stringify({ eventName, session, turn, agentId, agentType, parentAgentId, parentRunId });\n const pending = resolve(pendingRoot, createHash(\"sha256\").update(identity).digest(\"hex\") + \".json\");\n await atomicSave(pending, { schema_version: 1, state: \"pending\", last_error: \"incomplete_host_metadata\", identity: JSON.parse(identity), recovery: \"Capture observed native metadata manually; never infer completed status.\" });\n await diagnostic(\"ignored_incomplete_event\", {\n eventName,\n session,\n turn,\n agentId,\n agentType,\n missing_model: lifecycleEvent === \"start\" && model === null,\n });\n process.exit(0);\n }\n const candidates = [\n resolve(repositoryRoot, \".agents\", \"runtime\", \"bin\", \"zaip.mjs\"),\n resolve(repositoryRoot, \"packages\", \"cli\", \"dist\", \"index.js\"),\n ];\n const cli = candidates.find(existsSync);\n if (cli === undefined) {\n await diagnostic(\"cli_not_found\", { eventName, candidates });\n process.exit(0);\n }\n const captureArguments = terminalFallback\n ? [\n \"subagent\",\n \"capture-terminal\",\n \"--parent-agent-id\",\n parentAgentId,\n \"--parent-run-id\",\n parentRunId,\n \"--agent-id\",\n agentId,\n \"--agent-type\",\n agentType,\n \"--status\",\n status,\n \"--json\",\n ]\n : [\n \"subagent\",\n \"capture\",\n \"--event\",\n lifecycleEvent,\n \"--session\",\n session,\n \"--turn\",\n turn,\n \"--agent-id\",\n agentId,\n \"--agent-type\",\n agentType,\n ...(model === null ? [] : [\"--model\", model]),\n ...(reasoningEffort === null ? [] : [\"--reasoning-effort\", reasoningEffort]),\n ...(parentAgentId === null ? [] : [\"--parent-agent-id\", parentAgentId]),\n ...(lifecycleEvent !== \"stop\" || status === null ? [] : [\"--status\", status]),\n \"--json\",\n ];\n const pending = await persistPending(cli, captureArguments);\n const receipt = await fs.readFile(pending, \"utf8\").then(JSON.parse);\n if (receipt.state !== \"acknowledged\") {\n const runner = spawn(\n process.execPath,\n [hookPath, \"--capture-runner\", pending],\n {\n cwd: repositoryRoot,\n detached: true,\n windowsHide: true,\n stdio: \"ignore\",\n },\n );\n runner.once(\"error\", (error) =>\n diagnostic(\"runner_spawn_failed\", { eventName, error: error.message }),\n );\n runner.unref();\n }\n if (lifecycleEvent === \"start\") {\n process.stdout.write(JSON.stringify({\n hookSpecificOutput: {\n hookEventName: \"SubagentStart\",\n additionalContext:\n \"ZAIP Codex delegation policy: a Sub-Run is only a native collaboration subagent. Do not use create_thread or fork_thread as a Sub-Run, fallback, retry, or delegation proof; those create independent chats and require an explicit user request. If native delegation is unavailable, report the limitation rather than create an independent chat. A catalog name, task_name, title, or prompt is not native ancestry. Before retrying the same bounded dispatch, await or reuse its pending native child; capture only ancestry observed from native host spawn/terminal results or SubagentStart/SubagentStop events, and work only as a cataloged zaip-agent-NOME identity. Delegation is recursive; do not use default, worker, explorer, or ad-hoc agent types.\",\n },\n }));\n } else {\n // Codex expects one JSON object on stdout for SubagentStop as well.\n process.stdout.write(JSON.stringify({}));\n }\n} catch (error) {\n await diagnostic(\"hook_exception\", {\n error: error instanceof Error ? error.message : String(error),\n });\n}\n\nasync function atomicSave(path, value) {\n const temporary = path + \".tmp-\" + randomUUID();\n await fs.writeFile(temporary, JSON.stringify(value), { flag: \"wx\", mode: 0o600 });\n for (let retry = 0; ; retry++) {\n try { await fs.rename(temporary, path); break; }\n catch (error) { if (![\"EPERM\", \"EACCES\", \"EBUSY\"].includes(error.code) || retry >= 5) throw error; await new Promise(done => setTimeout(done, 20 * (retry + 1))); }\n }\n}\n\nasync function runCaptureRunner(receiptPath, replay = false) {\n if (typeof receiptPath !== \"string\" || dirname(resolve(receiptPath)) !== pendingRoot) return;\n const lockPath = receiptPath + \".lock\";\n let lease;\n try {\n lease = await fs.open(lockPath, \"wx\");\n await lease.writeFile(JSON.stringify({ pid: process.pid }));\n } catch (error) {\n if (error.code !== \"EEXIST\") return;\n const owner = await fs.readFile(lockPath, \"utf8\").then(JSON.parse).catch(() => null);\n if (!Number.isSafeInteger(owner?.pid) || owner.pid <= 0) return;\n try { process.kill(owner.pid, 0); return; } catch (failure) { if (failure.code !== \"ESRCH\") return; }\n // Serialize stale lock recovery so two recovering workers cannot remove a new lease.\n let recovery;\n try { recovery = await fs.open(lockPath + \".recover\", \"wx\"); } catch { return; }\n try {\n const current = await fs.readFile(lockPath, \"utf8\").then(JSON.parse).catch(() => null);\n if (current?.pid !== owner.pid) return;\n await fs.unlink(lockPath);\n lease = await fs.open(lockPath, \"wx\");\n await lease.writeFile(JSON.stringify({ pid: process.pid }));\n } finally { await recovery.close(); await fs.unlink(lockPath + \".recover\").catch(() => {}); }\n if (!lease) return;\n }\n try {\n const receipt = await fs.readFile(receiptPath, \"utf8\").then(JSON.parse);\n if (receipt.schema_version !== 1 || receipt.state === \"acknowledged\" || (!replay && receipt.attempts >= 3)) return;\n const cli = receipt.cli, args = receipt.capture_arguments;\n const allowedCli = [resolve(repositoryRoot, \".agents/runtime/bin/zaip.mjs\"), resolve(repositoryRoot, \"packages/cli/dist/index.js\")];\n if (!allowedCli.includes(cli) || !Array.isArray(args) || !args.every(x => typeof x === \"string\") || args[0] !== \"subagent\" || ![\"capture\", \"capture-terminal\"].includes(args[1])) return;\n const digest = createHash(\"sha256\").update(JSON.stringify({ cli, arguments: args })).digest(\"hex\");\n if (resolve(pendingRoot, digest + \".json\") !== resolve(receiptPath)) return;\n const value = flag => args[args.indexOf(flag) + 1];\n const stop = args[1] === \"capture-terminal\" || value(\"--event\") === \"stop\";\n if (stop && ![\"completed\", \"failed\", \"cancelled\"].includes(value(\"--status\"))) return;\n for (let retry = 0; retry < 3; retry++) {\n receipt.attempts = (receipt.attempts ?? 0) + 1;\n receipt.updated_at = new Date().toISOString();\n await atomicSave(receiptPath, receipt);\n const outcome = await capture(cli, args);\n let result;\n try { result = JSON.parse(outcome.stdout); } catch { result = null; }\n const runId = result?.run?.run_id ?? result?.run?.id;\n const status = result?.status ?? result?.run?.status;\n if (outcome.code === 0 && result?.captured === true && typeof runId === \"string\" && runId.startsWith(\"RUN-\") && (!stop || status === value(\"--status\"))) {\n receipt.state = \"acknowledged\";\n receipt.run_id = runId;\n receipt.acknowledged_at = new Date().toISOString();\n delete receipt.last_error;\n await atomicSave(receiptPath, receipt);\n return;\n }\n receipt.last_error = outcome.error ?? (outcome.code === 0 ? \"capture_not_confirmed\" : \"capture_exit_nonzero\");\n await atomicSave(receiptPath, receipt);\n if (retry < 2) await new Promise(done => setTimeout(done, 250 * (retry + 1)));\n }\n await persistDiagnostic(\"capture_pending\", { receipt: receiptPath, attempts: receipt.attempts, error: receipt.last_error });\n } catch (error) {\n await persistDiagnostic(\"runner_exception\", { error: error instanceof Error ? error.message : String(error) });\n } finally {\n await lease?.close();\n await fs.unlink(lockPath).catch(() => {});\n }\n}\n\nasync function capture(cli, args) {\n const child = spawn(process.execPath, [cli, ...args], { cwd: repositoryRoot, windowsHide: true, stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n let stdout = \"\", overflow = false;\n child.stdout.on(\"data\", chunk => { stdout += String(chunk); if (stdout.length > 262144) { overflow = true; stdout = \"\"; child.kill(); } });\n child.stderr.resume();\n return await new Promise(done => {\n let settled = false;\n const finish = result => { if (settled) return; settled = true; clearTimeout(timer); done({ ...result, stdout: overflow ? \"\" : stdout }); };\n const timer = setTimeout(() => { child.kill(); finish({ error: \"capture_timeout\" }); }, 15000);\n child.once(\"error\", () => finish({ error: \"capture_spawn_failed\" }));\n child.once(\"close\", code => finish({ code }));\n });\n}\n\nasync function persistPending(cli, captureArguments) {\n const identity = JSON.stringify({ cli, arguments: captureArguments });\n const digest = createHash(\"sha256\").update(identity).digest(\"hex\");\n const path = resolve(pendingRoot, digest + \".json\");\n await fs.mkdir(pendingRoot, { recursive: true });\n const receipt = { schema_version: 1, state: \"pending\", cli, capture_arguments: captureArguments, attempts: 0, created_at: new Date().toISOString() };\n // Publish a complete immutable initial receipt without overwriting another worker's acknowledgement.\n const temporary = path + \".tmp-\" + randomUUID();\n await fs.writeFile(temporary, JSON.stringify(receipt), { flag: \"wx\", mode: 0o600 });\n try { await fs.link(temporary, path); } catch (error) { if (error.code !== \"EEXIST\") throw error; }\n finally { await fs.unlink(temporary); }\n return path;\n}\n\nasync function persistDiagnostic(code, detail) {\n const line = `${JSON.stringify({\n source: \"zaip-subagent-lifecycle-runner\",\n code,\n occurred_at: new Date().toISOString(),\n detail,\n })}\\n`;\n await fs.mkdir(dirname(diagnosticPath), { recursive: true });\n const size = await fs.stat(diagnosticPath).then((entry) => entry.size).catch(() => 0);\n if (size + Buffer.byteLength(line) > 262_144) {\n await fs.writeFile(diagnosticPath, \"\", \"utf8\");\n }\n await fs.appendFile(diagnosticPath, line, \"utf8\");\n}\n\nasync function diagnostic(code, detail) {\n await persistDiagnostic(code, detail);\n process.stderr.write(\n `${JSON.stringify({ source: \"zaip-subagent-lifecycle\", code, detail })}\\n`,\n );\n}\n\nfunction text(value) {\n if (typeof value !== \"string\") return null;\n const normalized = value.trim();\n return normalized.length > 0 && normalized.length <= 200 ? normalized : null;\n}\n\nfunction reasoningEffortValue(value) {\n const normalized = text(value);\n return [\"low\", \"medium\", \"high\", \"xhigh\"].includes(normalized) ? normalized : null;\n}\n\nfunction terminalStatus(value) {\n const normalized = text(value);\n return [\"completed\", \"failed\", \"cancelled\"].includes(normalized) ? normalized : null;\n}\n";
|
|
2
2
|
//# sourceMappingURL=codex-subagent-hook-template.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codex-subagent-hook-template.d.ts","sourceRoot":"","sources":["../../src/services/codex-subagent-hook-template.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,6BAA6B,
|
|
1
|
+
{"version":3,"file":"codex-subagent-hook-template.d.ts","sourceRoot":"","sources":["../../src/services/codex-subagent-hook-template.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,6BAA6B,m7eA8UzC,CAAC"}
|
|
@@ -1,26 +1,56 @@
|
|
|
1
|
-
export const CODEX_SUBAGENT_LIFECYCLE_HOOK = `import { existsSync } from "node:fs";
|
|
1
|
+
export const CODEX_SUBAGENT_LIFECYCLE_HOOK = `import { existsSync, realpathSync } from "node:fs";
|
|
2
2
|
import { promises as fs } from "node:fs";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
3
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
4
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
-
import { dirname, relative, resolve } from "node:path";
|
|
5
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
8
|
const hookPath = fileURLToPath(import.meta.url);
|
|
9
|
-
const
|
|
10
|
-
|
|
9
|
+
const installedRoot = resolve(dirname(hookPath), "..", "..");
|
|
10
|
+
let repositoryRoot = installedRoot;
|
|
11
|
+
let diagnosticPath = resolve(
|
|
11
12
|
repositoryRoot,
|
|
12
13
|
".agents",
|
|
13
14
|
"local",
|
|
14
15
|
"hooks",
|
|
15
16
|
"subagent-lifecycle.ndjson",
|
|
16
17
|
);
|
|
17
|
-
|
|
18
|
+
let pendingRoot = resolve(repositoryRoot, ".local", "hooks", "subagent-pending");
|
|
19
|
+
|
|
20
|
+
function inside(path, root) {
|
|
21
|
+
const scoped = relative(root, resolve(path));
|
|
22
|
+
return scoped === "" || (!scoped.startsWith("..") && !isAbsolute(scoped));
|
|
23
|
+
}
|
|
24
|
+
function selectRoot(cwd) {
|
|
25
|
+
if (typeof cwd !== "string") return false;
|
|
26
|
+
try {
|
|
27
|
+
if (realpathSync(cwd).toLowerCase() !== resolve(cwd).toLowerCase()) return false;
|
|
28
|
+
if (!inside(cwd, installedRoot)) {
|
|
29
|
+
const git = (root, ...args) => execFileSync("git", ["-C", root, ...args], {
|
|
30
|
+
encoding: "utf8", windowsHide: true, timeout: 5000,
|
|
31
|
+
}).trim();
|
|
32
|
+
if (realpathSync(git(installedRoot, "rev-parse", "--show-toplevel")).toLowerCase() !== installedRoot.toLowerCase()) return false;
|
|
33
|
+
const registered = git(installedRoot, "worktree", "list", "--porcelain", "-z")
|
|
34
|
+
.split("\\0").filter(line => line.startsWith("worktree ")).map(line => resolve(line.slice(9)).toLowerCase());
|
|
35
|
+
const candidate = realpathSync(git(cwd, "rev-parse", "--show-toplevel"));
|
|
36
|
+
if (!registered.includes(candidate.toLowerCase()) || !inside(cwd, candidate)) return false;
|
|
37
|
+
const common = root => realpathSync(git(root, "rev-parse", "--path-format=absolute", "--git-common-dir")).toLowerCase();
|
|
38
|
+
if (common(installedRoot) !== common(candidate)) return false;
|
|
39
|
+
repositoryRoot = candidate;
|
|
40
|
+
}
|
|
41
|
+
diagnosticPath = resolve(repositoryRoot, ".agents", "local", "hooks", "subagent-lifecycle.ndjson");
|
|
42
|
+
pendingRoot = resolve(repositoryRoot, ".local", "hooks", "subagent-pending");
|
|
43
|
+
return true;
|
|
44
|
+
} catch { return false; }
|
|
45
|
+
}
|
|
18
46
|
|
|
19
47
|
if (process.argv[2] === "--capture-runner") {
|
|
48
|
+
if (!selectRoot(process.cwd())) process.exit(0);
|
|
20
49
|
await runCaptureRunner(process.argv[3]);
|
|
21
50
|
process.exit(0);
|
|
22
51
|
}
|
|
23
52
|
if (process.argv[2] === "--replay-pending") {
|
|
53
|
+
if (!selectRoot(process.cwd())) process.exit(0);
|
|
24
54
|
const files = await fs.readdir(pendingRoot).catch(() => []);
|
|
25
55
|
let replayed = 0;
|
|
26
56
|
for (const file of files.filter((name) => /^[a-f0-9]{64}\\.json$/.test(name)).slice(0, 256)) {
|
|
@@ -37,8 +67,7 @@ for await (const chunk of process.stdin) raw += chunk;
|
|
|
37
67
|
try {
|
|
38
68
|
const event = JSON.parse(raw);
|
|
39
69
|
const cwd = text(event.cwd);
|
|
40
|
-
|
|
41
|
-
if (cwd === null || scoped.startsWith("..")) process.exit(0);
|
|
70
|
+
if (cwd === null || !selectRoot(cwd)) process.exit(0);
|
|
42
71
|
const eventName = text(event.hook_event_name);
|
|
43
72
|
const lifecycleEvent =
|
|
44
73
|
eventName === "SubagentStart" ? "start" : eventName === "SubagentStop" ? "stop" : null;
|
|
@@ -154,6 +183,9 @@ try {
|
|
|
154
183
|
"ZAIP Codex delegation policy: a Sub-Run is only a native collaboration subagent. Do not use create_thread or fork_thread as a Sub-Run, fallback, retry, or delegation proof; those create independent chats and require an explicit user request. If native delegation is unavailable, report the limitation rather than create an independent chat. A catalog name, task_name, title, or prompt is not native ancestry. Before retrying the same bounded dispatch, await or reuse its pending native child; capture only ancestry observed from native host spawn/terminal results or SubagentStart/SubagentStop events, and work only as a cataloged zaip-agent-NOME identity. Delegation is recursive; do not use default, worker, explorer, or ad-hoc agent types.",
|
|
155
184
|
},
|
|
156
185
|
}));
|
|
186
|
+
} else {
|
|
187
|
+
// Codex expects one JSON object on stdout for SubagentStop as well.
|
|
188
|
+
process.stdout.write(JSON.stringify({}));
|
|
157
189
|
}
|
|
158
190
|
} catch (error) {
|
|
159
191
|
await diagnostic("hook_exception", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codex-subagent-hook-template.js","sourceRoot":"","sources":["../../src/services/codex-subagent-hook-template.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,6BAA6B,GAAG
|
|
1
|
+
{"version":3,"file":"codex-subagent-hook-template.js","sourceRoot":"","sources":["../../src/services/codex-subagent-hook-template.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,6BAA6B,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8U5C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codex-windows-hook-command.d.ts","sourceRoot":"","sources":["../../src/services/codex-windows-hook-command.ts"],"names":[],"mappings":"AAEA,wEAAwE;AACxE,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAQpF"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
/** Encode PowerShell before TOML and cmd.exe parse the hook command. */
|
|
3
|
+
export function codexWindowsHookCommand(hook, workspaceRoot) {
|
|
4
|
+
if (!/^zaip-[a-z-]+\.mjs$/.test(hook))
|
|
5
|
+
throw new Error(`Invalid ZAIP hook name: ${hook}`);
|
|
6
|
+
const literalRoot = workspaceRoot === undefined
|
|
7
|
+
? "(Get-Location)"
|
|
8
|
+
: `'${resolve(workspaceRoot).replaceAll("'", "''")}'`;
|
|
9
|
+
const script = `$git = Join-Path ${literalRoot} '.local\\git\\cmd\\git.exe'; if (!(Test-Path -LiteralPath $git -PathType Leaf)) { $git = (Get-Command git -ErrorAction SilentlyContinue).Source }; if (!$git) { exit 0 }; $root = & $git rev-parse --show-toplevel 2>$null; if ($LASTEXITCODE -ne 0 -or !$root) { exit 0 }; $hook = Join-Path $root '.codex/hooks/${hook}'; if (!(Test-Path -LiteralPath $hook -PathType Leaf)) { exit 0 }; & node $hook; exit $LASTEXITCODE`;
|
|
10
|
+
return `powershell -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(script, "utf16le").toString("base64")}`;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=codex-windows-hook-command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codex-windows-hook-command.js","sourceRoot":"","sources":["../../src/services/codex-windows-hook-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,wEAAwE;AACxE,MAAM,UAAU,uBAAuB,CAAC,IAAY,EAAE,aAAsB;IAC1E,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,EAAE,CAAC,CAAC;IAC1F,MAAM,WAAW,GACf,aAAa,KAAK,SAAS;QACzB,CAAC,CAAC,gBAAgB;QAClB,CAAC,CAAC,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC;IAC1D,MAAM,MAAM,GAAG,oBAAoB,WAAW,sTAAsT,IAAI,qGAAqG,CAAC;IAC9c,OAAO,yDAAyD,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;AACtH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codex-windows-hook-command.test.d.ts","sourceRoot":"","sources":["../../src/services/codex-windows-hook-command.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { codexWindowsHookCommand } from "./codex-windows-hook-command.js";
|
|
7
|
+
const roots = [];
|
|
8
|
+
afterEach(async () => {
|
|
9
|
+
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
10
|
+
});
|
|
11
|
+
describe.skipIf(process.platform !== "win32")("Codex Windows hook command", () => {
|
|
12
|
+
it("passes stdin and JSON stdout through cmd.exe from a path with spaces", async () => {
|
|
13
|
+
const root = await mkdtemp(join(tmpdir(), "zaip hook workspace "));
|
|
14
|
+
roots.push(root);
|
|
15
|
+
const init = spawnSync("git", ["init", "-q"], { cwd: root, encoding: "utf8" });
|
|
16
|
+
expect(init.status).toBe(0);
|
|
17
|
+
const hooks = join(root, ".codex", "hooks");
|
|
18
|
+
await mkdir(hooks, { recursive: true });
|
|
19
|
+
await writeFile(join(hooks, "zaip-pre-tool-use.mjs"), 'let value = ""; for await (const chunk of process.stdin) value += chunk; process.stdout.write(JSON.stringify({input: JSON.parse(value)}));\n');
|
|
20
|
+
const command = codexWindowsHookCommand("zaip-pre-tool-use.mjs");
|
|
21
|
+
const result = spawnSync("cmd.exe", ["/d", "/s", "/c", command], {
|
|
22
|
+
cwd: root,
|
|
23
|
+
input: JSON.stringify({ hook_event_name: "PreToolUse", tool_input: { cmd: "git status" } }),
|
|
24
|
+
encoding: "utf8",
|
|
25
|
+
});
|
|
26
|
+
expect(result.status).toBe(0);
|
|
27
|
+
expect(JSON.parse(result.stdout)).toEqual({
|
|
28
|
+
input: { hook_event_name: "PreToolUse", tool_input: { cmd: "git status" } },
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
it("exits successfully outside a repository or when its hook is absent", async () => {
|
|
32
|
+
const root = await mkdtemp(join(tmpdir(), "zaip hook absent "));
|
|
33
|
+
roots.push(root);
|
|
34
|
+
const command = codexWindowsHookCommand("zaip-subagent-lifecycle.mjs");
|
|
35
|
+
const result = spawnSync("cmd.exe", ["/d", "/s", "/c", command], {
|
|
36
|
+
cwd: root,
|
|
37
|
+
input: "{}",
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
});
|
|
40
|
+
expect(result.status).toBe(0);
|
|
41
|
+
expect(result.stdout).toBe("");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
//# sourceMappingURL=codex-windows-hook-command.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codex-windows-hook-command.test.js","sourceRoot":"","sources":["../../src/services/codex-windows-hook-command.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"developer-identity-fixture.d.ts","sourceRoot":"","sources":["../../src/services/developer-identity-fixture.ts"],"names":[],"mappings":"AAGA,oFAAoF;AACpF,wBAAsB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB7E"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
/** Root Run integration fixtures need identity without depending on live GitHub. */
|
|
4
|
+
export async function seedVerifiedFixtureIdentity(root) {
|
|
5
|
+
const verifiedAt = new Date().toISOString();
|
|
6
|
+
await writeFile(resolve(root, ".local/developer-identity.json"), `${JSON.stringify({
|
|
7
|
+
schema_version: 1,
|
|
8
|
+
provider: "github",
|
|
9
|
+
account_id: "12345",
|
|
10
|
+
login_snapshot: "fixture-developer",
|
|
11
|
+
attribution_source: "verified_local_account",
|
|
12
|
+
verified_at: verifiedAt,
|
|
13
|
+
last_verified_at: verifiedAt,
|
|
14
|
+
expires_at: new Date(Date.now() + 60_000).toISOString(),
|
|
15
|
+
})}\n`, "utf8");
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=developer-identity-fixture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"developer-identity-fixture.js","sourceRoot":"","sources":["../../src/services/developer-identity-fixture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,oFAAoF;AACpF,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAAC,IAAY;IAC5D,MAAM,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC5C,MAAM,SAAS,CACb,OAAO,CAAC,IAAI,EAAE,gCAAgC,CAAC,EAC/C,GAAG,IAAI,CAAC,SAAS,CAAC;QAChB,cAAc,EAAE,CAAC;QACjB,QAAQ,EAAE,QAAQ;QAClB,UAAU,EAAE,OAAO;QACnB,cAAc,EAAE,mBAAmB;QACnC,kBAAkB,EAAE,wBAAwB;QAC5C,WAAW,EAAE,UAAU;QACvB,gBAAgB,EAAE,UAAU;QAC5B,UAAU,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,CAAC,WAAW,EAAE;KACxD,CAAC,IAAI,EACN,MAAM,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"global-codex-config.d.ts","sourceRoot":"","sources":["../../src/services/global-codex-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAM1E,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,QAAQ,EAAE,OAAO,QAAQ,CAAC;IAC1B,MAAM,EAAE,OAAO,MAAM,CAAC;IACtB,EAAE,EAAE,OAAO,EAAE,CAAC;IACd,SAAS,EAAE,OAAO,SAAS,CAAC;CAC7B;
|
|
1
|
+
{"version":3,"file":"global-codex-config.d.ts","sourceRoot":"","sources":["../../src/services/global-codex-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAM1E,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,QAAQ,EAAE,OAAO,QAAQ,CAAC;IAC1B,MAAM,EAAE,OAAO,MAAM,CAAC;IACtB,EAAE,EAAE,OAAO,EAAE,CAAC;IACd,SAAS,EAAE,OAAO,SAAS,CAAC;CAC7B;AAgBD;;;;GAIG;AACH,wBAAsB,0BAA0B,CAC9C,SAAS,SAAsD,EAC/D,UAAU,SAAiD,EAC3D,UAAU,GAAE,2BAA4C,GACvD,OAAO,CAAC,uBAAuB,CAAC,CAuClC;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAY1D;AAED,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,EACd,gBAAgB,GAAE,SAAS,MAAM,EAAO,GACvC;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,oBAAoB,EAAE,MAAM,CAAC;CAC9B,CASA"}
|
|
@@ -6,11 +6,15 @@ import { CODEX_PRE_TOOL_USE_HOOK } from "./codex-config-template.js";
|
|
|
6
6
|
const nodeFileSystem = { mkdir, readFile, rename, rm, writeFile };
|
|
7
7
|
const ANY_TOML_TABLE = /^\[\[?[^\]\r\n]+\]\]?(?:\r?\n|$)/;
|
|
8
8
|
const AGENT_TABLE = /^\[agents\."([^"]+)"\](?:\r?\n|$)/;
|
|
9
|
-
const
|
|
10
|
-
const PRE_TOOL_USE_CHILD_TABLE = /^\[\[hooks\.PreToolUse\.[^\]]+\]\](?:\r?\n|$)/;
|
|
9
|
+
const HOOK_TABLE = /^\[\[hooks\.(PreToolUse|Stop|SubagentStart|SubagentStop)\]\](?:\r?\n|$)/;
|
|
11
10
|
const ZAIP_HOOK_SIGNATURES = [
|
|
12
11
|
".codex/hooks/zaip-pre-tool-use.mjs",
|
|
12
|
+
".codex/hooks/zaip-conversation-lifecycle.mjs",
|
|
13
|
+
".codex/hooks/zaip-subagent-lifecycle.mjs",
|
|
13
14
|
'statusMessage = "Applying ZAIP workspace safety policy"',
|
|
15
|
+
'statusMessage = "Reconciling ZAIP task closure and telemetry"',
|
|
16
|
+
'statusMessage = "Attributing ZAIP subagent Run"',
|
|
17
|
+
'statusMessage = "Finalizing ZAIP subagent Run"',
|
|
14
18
|
];
|
|
15
19
|
/**
|
|
16
20
|
* Removes only catalog-declared legacy Codex identity aliases and installs the
|
|
@@ -59,7 +63,7 @@ export async function reconcileGlobalCodexConfig(codexHome = process.env.CODEX_H
|
|
|
59
63
|
}
|
|
60
64
|
}
|
|
61
65
|
export function installSecurityHook(config) {
|
|
62
|
-
const zaipHookSpans =
|
|
66
|
+
const zaipHookSpans = zaipOwnedHookSpans(config);
|
|
63
67
|
if (zaipHookSpans.length === 1 && normalizeNewlines(config).includes(CODEX_PRE_TOOL_USE_HOOK)) {
|
|
64
68
|
return config;
|
|
65
69
|
}
|
|
@@ -100,7 +104,7 @@ function legacyAgentTableSpans(config, legacyAgentNames) {
|
|
|
100
104
|
}
|
|
101
105
|
return tables.flatMap((table, index) => table.legacy ? [{ start: table.start, end: tables[index + 1]?.start ?? config.length }] : []);
|
|
102
106
|
}
|
|
103
|
-
function
|
|
107
|
+
function zaipOwnedHookSpans(config) {
|
|
104
108
|
const tables = [];
|
|
105
109
|
let offset = 0;
|
|
106
110
|
let inMultilineBasic = false;
|
|
@@ -121,16 +125,26 @@ function zaipSecurityHookSpans(config) {
|
|
|
121
125
|
offset += text.length;
|
|
122
126
|
}
|
|
123
127
|
return tables.flatMap((table, index) => {
|
|
124
|
-
|
|
128
|
+
const eventName = HOOK_TABLE.exec(table.text)?.[1];
|
|
129
|
+
if (eventName === undefined)
|
|
125
130
|
return [];
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
: []
|
|
131
|
+
const childTable = new RegExp(`^\\[\\[hooks\\.${eventName}\\.[^\\]]+\\]\\]`);
|
|
132
|
+
let boundary = index + 1;
|
|
133
|
+
while (boundary < tables.length && childTable.test(tables[boundary]?.text ?? ""))
|
|
134
|
+
boundary += 1;
|
|
135
|
+
const end = tables[boundary]?.start ?? config.length;
|
|
136
|
+
const children = tables.slice(index + 1, boundary).map((child, childIndex, siblings) => ({
|
|
137
|
+
start: child.start,
|
|
138
|
+
end: siblings[childIndex + 1]?.start ?? end,
|
|
139
|
+
}));
|
|
140
|
+
const isZaip = (span) => ZAIP_HOOK_SIGNATURES.some((signature) => config.slice(span.start, span.end).includes(signature));
|
|
141
|
+
if (children.length === 0)
|
|
142
|
+
return isZaip({ start: table.start, end }) ? [{ start: table.start, end }] : [];
|
|
143
|
+
const owned = children.filter(isZaip);
|
|
144
|
+
if (owned.length === children.length)
|
|
145
|
+
return [{ start: table.start, end }];
|
|
146
|
+
// An event can contain user and ZAIP hooks. Keep the event matcher and every user child intact.
|
|
147
|
+
return owned;
|
|
134
148
|
});
|
|
135
149
|
}
|
|
136
150
|
async function catalogedCodexAliases(sourcePath, filesystem) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"global-codex-config.js","sourceRoot":"","sources":["../../src/services/global-codex-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC1E,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAE,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAoBrE,MAAM,cAAc,GAAgC,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC;AAC/F,MAAM,cAAc,GAAG,kCAAkC,CAAC;AAC1D,MAAM,WAAW,GAAG,mCAAmC,CAAC;AACxD,MAAM,
|
|
1
|
+
{"version":3,"file":"global-codex-config.js","sourceRoot":"","sources":["../../src/services/global-codex-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC1E,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAE,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAoBrE,MAAM,cAAc,GAAgC,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC;AAC/F,MAAM,cAAc,GAAG,kCAAkC,CAAC;AAC1D,MAAM,WAAW,GAAG,mCAAmC,CAAC;AACxD,MAAM,UAAU,GAAG,yEAAyE,CAAC;AAC7F,MAAM,oBAAoB,GAAG;IAC3B,oCAAoC;IACpC,8CAA8C;IAC9C,0CAA0C;IAC1C,yDAAyD;IACzD,+DAA+D;IAC/D,iDAAiD;IACjD,gDAAgD;CACxC,CAAC;AAEX;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,EAC/D,UAAU,GAAG,8CAA8C,EAC3D,UAAU,GAAgC,cAAc;IAExD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;IAC5C,MAAM,SAAS,GAAG,GAAG,IAAI,aAAa,OAAO,CAAC,GAAG,EAAE,CAAC;IACpD,MAAM,MAAM,GAAG,GAAG,IAAI,gBAAgB,OAAO,CAAC,GAAG,EAAE,CAAC;IACpD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YAC/E,IAAI,aAAa,CAAC,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YACtC,MAAM,KAAK,CAAC;QACd,CAAC,CAAC,CAAC;QACH,MAAM,gBAAgB,GAAG,MAAM,qBAAqB,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QAC7E,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,oBAAoB,EAAE,GAC/C,OAAO,KAAK,IAAI;YACd,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,oBAAoB,EAAE,CAAC,EAAE;YAC1C,CAAC,CAAC,wBAAwB,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAC;QAC1D,MAAM,OAAO,GAAG,mBAAmB,CAAC,QAAQ,CAAC,CAAC;QAC9C,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;YACxB,MAAM,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;QACpE,CAAC;QACD,OAAO;YACL,IAAI;YACJ,UAAU;YACV,SAAS,EAAE,IAAI;YACf,mBAAmB,EAAE,CAAC;YACtB,oBAAoB;YACpB,qBAAqB,EAAE,IAAI;YAC3B,KAAK,EAAE,IAAI;SACZ,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACvE,OAAO;YACL,IAAI;YACJ,UAAU;YACV,SAAS,EAAE,KAAK;YAChB,mBAAmB,EAAE,CAAC;YACtB,oBAAoB,EAAE,CAAC;YACvB,qBAAqB,EAAE,KAAK;YAC5B,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;SAC9D,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,MAAM,aAAa,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;IACjD,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,IAAI,iBAAiB,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,uBAAuB,CAAC,EAAE,CAAC;QAC9F,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,MAAM,QAAQ,GAAG,aAAa,CAAC,WAAW,CACxC,CAAC,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAC/E,MAAM,CACP,CAAC;IACF,MAAM,MAAM,GACV,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,QAAQ,IAAI,CAAC,CAAC,CAAC,GAAG,QAAQ,MAAM,CAAC;IAC7F,OAAO,GAAG,MAAM,GAAG,uBAAuB,IAAI,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,MAAc,EACd,gBAAgB,GAAsB,EAAE;IAKxC,MAAM,KAAK,GAAG,qBAAqB,CAAC,MAAM,EAAE,IAAI,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACvE,OAAO;QACL,OAAO,EAAE,KAAK,CAAC,WAAW,CACxB,CAAC,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAC/E,MAAM,CACP;QACD,oBAAoB,EAAE,KAAK,CAAC,MAAM;KACnC,CAAC;AACJ,CAAC;AAED,SAAS,qBAAqB,CAC5B,MAAc,EACd,gBAAqC;IAErC,MAAM,MAAM,GAA8C,EAAE,CAAC;IAC7D,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,gBAAgB,GAAG,KAAK,CAAC;IAC7B,IAAI,kBAAkB,GAAG,KAAK,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACrB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAChC,IAAI,CAAC,gBAAgB,IAAI,CAAC,kBAAkB,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1E,MAAM,SAAS,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAC9C,MAAM,CAAC,IAAI,CAAC;gBACV,KAAK,EAAE,MAAM;gBACb,MAAM,EAAE,SAAS,KAAK,SAAS,IAAI,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC;aACnE,CAAC,CAAC;QACL,CAAC;QACD,IAAI,CAAC,kBAAkB,IAAI,uBAAuB,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC;YAChE,gBAAgB,GAAG,CAAC,gBAAgB,CAAC;QACvC,CAAC;QACD,IAAI,CAAC,gBAAgB,IAAI,uBAAuB,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC;YAC9D,kBAAkB,GAAG,CAAC,kBAAkB,CAAC;QAC3C,CAAC;QACD,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC;IACxB,CAAC;IACD,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CACrC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAC7F,CAAC;AACJ,CAAC;AAED,SAAS,kBAAkB,CAAC,MAAc;IACxC,MAAM,MAAM,GAA2C,EAAE,CAAC;IAC1D,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,gBAAgB,GAAG,KAAK,CAAC;IAC7B,IAAI,kBAAkB,GAAG,KAAK,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACrB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAChC,IAAI,CAAC,gBAAgB,IAAI,CAAC,kBAAkB,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1E,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QACvC,CAAC;QACD,IAAI,CAAC,kBAAkB,IAAI,uBAAuB,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC;YAChE,gBAAgB,GAAG,CAAC,gBAAgB,CAAC;QACvC,CAAC;QACD,IAAI,CAAC,gBAAgB,IAAI,uBAAuB,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC;YAC9D,kBAAkB,GAAG,CAAC,kBAAkB,CAAC;QAC3C,CAAC;QACD,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC;IACxB,CAAC;IACD,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;QACrC,MAAM,SAAS,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACnD,IAAI,SAAS,KAAK,SAAS;YAAE,OAAO,EAAE,CAAC;QACvC,MAAM,UAAU,GAAG,IAAI,MAAM,CAAC,kBAAkB,SAAS,kBAAkB,CAAC,CAAC;QAC7E,IAAI,QAAQ,GAAG,KAAK,GAAG,CAAC,CAAC;QACzB,OAAO,QAAQ,GAAG,MAAM,CAAC,MAAM,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC;YAAE,QAAQ,IAAI,CAAC,CAAC;QAChG,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,EAAE,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC;QACrD,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;YACvF,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,GAAG,EAAE,QAAQ,CAAC,UAAU,GAAG,CAAC,CAAC,EAAE,KAAK,IAAI,GAAG;SAC5C,CAAC,CAAC,CAAC;QACJ,MAAM,MAAM,GAAG,CAAC,IAAoC,EAAE,EAAE,CACtD,oBAAoB,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CACtC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CACvD,CAAC;QACJ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YACvB,OAAO,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAClF,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACtC,IAAI,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;YAAE,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;QAC3E,gGAAgG;QAChG,OAAO,KAAK,CAAC;IACf,CAAC,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,qBAAqB,CAClC,UAAkB,EAClB,UAAuC;IAEvC,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IAC1C,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAC7D,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAE3B,CAAC;IACF,OAAO,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAC9C,CAAC,KAAK,CAAC,aAAa,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CACrD,KAAK,CAAC,IAAI,KAAK,YAAY,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CACpF,CACF,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAa;IACtC,OAAO,KAAK,CAAC,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AACxC,CAAC;AAED,SAAS,uBAAuB,CAAC,IAAY,EAAE,KAAoB;IACjE,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,KAAK,IAAI,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC;QACzF,IAAI,KAAK,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,IAAI;YAAE,WAAW,IAAI,CAAC,CAAC;IACpE,CAAC;IACD,OAAO,WAAW,GAAG,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC;AAED,KAAK,UAAU,aAAa,CAC1B,IAAY,EACZ,OAAe,EACf,SAAiB,EACjB,MAAc,EACd,UAAuC;IAEvC,IAAI,mBAAmB,GAAG,KAAK,CAAC;IAChC,MAAM,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3D,MAAM,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,MAAM,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,MAAM,UAAU,CAAC,SAAS,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IACjF,IAAI,CAAC;QACH,MAAM,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACtC,mBAAmB,GAAG,IAAI,CAAC;IAC7B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC;YAAE,MAAM,KAAK,CAAC;IACzC,CAAC;IACD,IAAI,CAAC;QACH,MAAM,UAAU,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAC3C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,mBAAmB;YAAE,MAAM,UAAU,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC/D,MAAM,KAAK,CAAC;IACd,CAAC;IACD,IAAI,mBAAmB;QAAE,MAAM,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AACxE,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC;AACnG,CAAC"}
|
|
@@ -66,7 +66,7 @@ describe("reconcileGlobalCodexConfig", () => {
|
|
|
66
66
|
it("repairs a status-only false positive with the complete canonical hook", () => {
|
|
67
67
|
const repaired = installSecurityHook('# statusMessage = "Applying ZAIP workspace safety policy"\n');
|
|
68
68
|
expect(repaired).toContain("[[hooks.PreToolUse]]");
|
|
69
|
-
expect(repaired).toContain("
|
|
69
|
+
expect(repaired).toContain("-EncodedCommand");
|
|
70
70
|
});
|
|
71
71
|
it("migrates obsolete ZAIP hooks without removing third-party hooks", () => {
|
|
72
72
|
const thirdParty = '[[hooks.PreToolUse]]\nmatcher = "Write"\n\n[[hooks.PreToolUse.hooks]]\ntype = "command"\ncommand = "third-party-zaip-guard"\n\n';
|
|
@@ -74,7 +74,7 @@ describe("reconcileGlobalCodexConfig", () => {
|
|
|
74
74
|
const migrated = installSecurityHook(thirdParty + obsoleteZaip);
|
|
75
75
|
expect(migrated).toContain("third-party-zaip-guard");
|
|
76
76
|
expect(migrated).not.toContain("--legacy");
|
|
77
|
-
expect(migrated.match(
|
|
77
|
+
expect(migrated.match(/\[\[hooks\.PreToolUse\]\]/g)).toHaveLength(2);
|
|
78
78
|
});
|
|
79
79
|
it("deduplicates canonical and obsolete ZAIP hook blocks", () => {
|
|
80
80
|
const canonical = installSecurityHook("");
|
|
@@ -83,6 +83,31 @@ describe("reconcileGlobalCodexConfig", () => {
|
|
|
83
83
|
expect(migrated).not.toContain("--obsolete");
|
|
84
84
|
expect(migrated.match(/Applying ZAIP workspace safety policy/g)).toHaveLength(1);
|
|
85
85
|
});
|
|
86
|
+
it("removes ZAIP-owned global lifecycle hooks while keeping unrelated hook tables", () => {
|
|
87
|
+
const legacy = [
|
|
88
|
+
'[[hooks.Stop]]\nmatcher = "*"\n[[hooks.Stop.hooks]]\ncommand = "node .codex/hooks/zaip-conversation-lifecycle.mjs"\n',
|
|
89
|
+
'[[hooks.SubagentStart]]\nmatcher = "*"\n[[hooks.SubagentStart.hooks]]\ncommand = "node .codex/hooks/zaip-subagent-lifecycle.mjs"\n',
|
|
90
|
+
'[[hooks.SubagentStop]]\nmatcher = "*"\n[[hooks.SubagentStop.hooks]]\ncommand = "node .codex/hooks/zaip-subagent-lifecycle.mjs"\n',
|
|
91
|
+
'[[hooks.Stop]]\nmatcher = "*"\n[[hooks.Stop.hooks]]\ncommand = "my-own-stop-hook"\n',
|
|
92
|
+
'[desktop]\nmode = "keep"\n',
|
|
93
|
+
].join("\n");
|
|
94
|
+
const installed = installSecurityHook(legacy);
|
|
95
|
+
expect(installed).not.toContain("zaip-conversation-lifecycle.mjs");
|
|
96
|
+
expect(installed).not.toContain("zaip-subagent-lifecycle.mjs");
|
|
97
|
+
expect(installed).toContain('command = "my-own-stop-hook"');
|
|
98
|
+
expect(installed).toContain('[desktop]\nmode = "keep"');
|
|
99
|
+
expect(installSecurityHook(installed)).toBe(installed);
|
|
100
|
+
});
|
|
101
|
+
it("preserves a user hook in the same event block byte-for-byte", () => {
|
|
102
|
+
const parent = '[[hooks.Stop]]\r\nmatcher = "*"\r\n\r\n';
|
|
103
|
+
const user = '[[hooks.Stop.hooks]]\r\ntype = "command"\r\ncommand = "my-own-stop-hook"\r\ntimeout = 17\r\n\r\n';
|
|
104
|
+
const zaip = '[[hooks.Stop.hooks]]\r\ntype = "command"\r\ncommand = "node .codex/hooks/zaip-conversation-lifecycle.mjs"\r\n\r\n';
|
|
105
|
+
const after = '[desktop]\r\nmode = "keep"\r\n';
|
|
106
|
+
const migrated = installSecurityHook(parent + user + zaip + after);
|
|
107
|
+
expect(migrated.startsWith(parent + user + after)).toBe(true);
|
|
108
|
+
expect(migrated).not.toContain("zaip-conversation-lifecycle.mjs");
|
|
109
|
+
expect(installSecurityHook(migrated)).toBe(migrated);
|
|
110
|
+
});
|
|
86
111
|
it("handles a final legacy table without consuming following global settings", () => {
|
|
87
112
|
expect(stripLegacyRegistrations('[agents."zaip-agent-builder"]\nconfig_file = "./agents/old.toml"\n\n[desktop]\nkeepRemoteControlAwakeWhilePluggedIn = true\n', ["zaip-agent-builder"])).toEqual({
|
|
88
113
|
content: "[desktop]\nkeepRemoteControlAwakeWhilePluggedIn = true\n",
|