@zaip-ai/cli 4.2.0 → 4.3.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.
Files changed (165) hide show
  1. package/dist/commands/init.d.ts.map +1 -1
  2. package/dist/commands/init.js +3 -1
  3. package/dist/commands/init.js.map +1 -1
  4. package/dist/services/adapters.d.ts +2 -2
  5. package/dist/services/adapters.d.ts.map +1 -1
  6. package/dist/services/adapters.js +11 -63
  7. package/dist/services/adapters.js.map +1 -1
  8. package/dist/services/agent-adapters.d.ts.map +1 -1
  9. package/dist/services/agent-adapters.js +3 -7
  10. package/dist/services/agent-adapters.js.map +1 -1
  11. package/dist/services/agent-adapters.test.js +43 -11
  12. package/dist/services/agent-adapters.test.js.map +1 -1
  13. package/dist/services/codex-config-template.js +2 -2
  14. package/dist/services/codex-conversation-lifecycle-hook.d.ts.map +1 -1
  15. package/dist/services/codex-conversation-lifecycle-hook.js +38 -14
  16. package/dist/services/codex-conversation-lifecycle-hook.js.map +1 -1
  17. package/dist/services/codex-hook-activation.d.ts +4 -0
  18. package/dist/services/codex-hook-activation.d.ts.map +1 -0
  19. package/dist/services/codex-hook-activation.js +64 -0
  20. package/dist/services/codex-hook-activation.js.map +1 -0
  21. package/dist/services/codex-model-policy.d.ts +4 -0
  22. package/dist/services/codex-model-policy.d.ts.map +1 -0
  23. package/dist/services/codex-model-policy.js +24 -0
  24. package/dist/services/codex-model-policy.js.map +1 -0
  25. package/dist/services/codex-native-tool-authority-template.d.ts +2 -0
  26. package/dist/services/codex-native-tool-authority-template.d.ts.map +1 -0
  27. package/dist/services/codex-native-tool-authority-template.js +89 -0
  28. package/dist/services/codex-native-tool-authority-template.js.map +1 -0
  29. package/dist/services/codex-pre-tool-hook-template.d.ts.map +1 -1
  30. package/dist/services/codex-pre-tool-hook-template.js +59 -86
  31. package/dist/services/codex-pre-tool-hook-template.js.map +1 -1
  32. package/dist/services/codex-pre-tool-hook-template.test.js +200 -1
  33. package/dist/services/codex-pre-tool-hook-template.test.js.map +1 -1
  34. package/dist/services/codex-pre-tool-support-template.d.ts.map +1 -1
  35. package/dist/services/codex-pre-tool-support-template.js +303 -12
  36. package/dist/services/codex-pre-tool-support-template.js.map +1 -1
  37. package/dist/services/codex-subagent-hook-template.d.ts +1 -1
  38. package/dist/services/codex-subagent-hook-template.d.ts.map +1 -1
  39. package/dist/services/codex-subagent-hook-template.js +11 -4
  40. package/dist/services/codex-subagent-hook-template.js.map +1 -1
  41. package/dist/services/codex-validation-runtime-template.d.ts +3 -0
  42. package/dist/services/codex-validation-runtime-template.d.ts.map +1 -0
  43. package/dist/services/codex-validation-runtime-template.js +18 -0
  44. package/dist/services/codex-validation-runtime-template.js.map +1 -0
  45. package/dist/services/codex-windows-hook-command.js +1 -1
  46. package/dist/services/codex-windows-hook-command.js.map +1 -1
  47. package/dist/services/codex-windows-hook-command.test.js +22 -0
  48. package/dist/services/codex-windows-hook-command.test.js.map +1 -1
  49. package/dist/services/global-codex-config-fixtures.d.ts +12 -0
  50. package/dist/services/global-codex-config-fixtures.d.ts.map +1 -0
  51. package/dist/services/global-codex-config-fixtures.js +38 -0
  52. package/dist/services/global-codex-config-fixtures.js.map +1 -0
  53. package/dist/services/global-codex-config-migration.test.d.ts +2 -0
  54. package/dist/services/global-codex-config-migration.test.d.ts.map +1 -0
  55. package/dist/services/global-codex-config-migration.test.js +301 -0
  56. package/dist/services/global-codex-config-migration.test.js.map +1 -0
  57. package/dist/services/global-codex-config-transaction.d.ts +15 -0
  58. package/dist/services/global-codex-config-transaction.d.ts.map +1 -0
  59. package/dist/services/global-codex-config-transaction.js +89 -0
  60. package/dist/services/global-codex-config-transaction.js.map +1 -0
  61. package/dist/services/global-codex-config-transaction.test.d.ts +2 -0
  62. package/dist/services/global-codex-config-transaction.test.d.ts.map +1 -0
  63. package/dist/services/global-codex-config-transaction.test.js +145 -0
  64. package/dist/services/global-codex-config-transaction.test.js.map +1 -0
  65. package/dist/services/global-codex-config.d.ts +10 -11
  66. package/dist/services/global-codex-config.d.ts.map +1 -1
  67. package/dist/services/global-codex-config.js +225 -107
  68. package/dist/services/global-codex-config.js.map +1 -1
  69. package/dist/services/global-codex-config.test.js +6 -6
  70. package/dist/services/global-codex-config.test.js.map +1 -1
  71. package/dist/services/hook-readiness.d.ts +7 -0
  72. package/dist/services/hook-readiness.d.ts.map +1 -0
  73. package/dist/services/hook-readiness.js +17 -0
  74. package/dist/services/hook-readiness.js.map +1 -0
  75. package/dist/services/logger.d.ts +2 -0
  76. package/dist/services/logger.d.ts.map +1 -1
  77. package/dist/services/logger.js +15 -1
  78. package/dist/services/logger.js.map +1 -1
  79. package/dist/services/logger.test.js +3 -0
  80. package/dist/services/logger.test.js.map +1 -1
  81. package/dist/services/managed-adapter.d.ts +6 -0
  82. package/dist/services/managed-adapter.d.ts.map +1 -0
  83. package/dist/services/managed-adapter.js +63 -0
  84. package/dist/services/managed-adapter.js.map +1 -0
  85. package/dist/types.d.ts +2 -0
  86. package/dist/types.d.ts.map +1 -1
  87. package/package.json +1 -1
  88. package/templates/.agents/README.md +8 -0
  89. package/templates/.agents/config/agents/discovery.json +1 -1
  90. package/templates/.agents/config/clients/codex/agents/zaip-agent-backend.toml +1 -2
  91. package/templates/.agents/config/clients/codex/agents/zaip-agent-brand-strategist.toml +1 -2
  92. package/templates/.agents/config/clients/codex/agents/zaip-agent-builder.toml +1 -2
  93. package/templates/.agents/config/clients/codex/agents/zaip-agent-cloud-engineer.toml +1 -2
  94. package/templates/.agents/config/clients/codex/agents/zaip-agent-data-analyst.toml +1 -2
  95. package/templates/.agents/config/clients/codex/agents/zaip-agent-designer.toml +1 -2
  96. package/templates/.agents/config/clients/codex/agents/zaip-agent-frontend.toml +1 -2
  97. package/templates/.agents/config/clients/codex/agents/zaip-agent-general-operations.toml +1 -2
  98. package/templates/.agents/config/clients/codex/agents/zaip-agent-governance-compliance.toml +1 -2
  99. package/templates/.agents/config/clients/codex/agents/zaip-agent-growth-strategist.toml +1 -2
  100. package/templates/.agents/config/clients/codex/agents/zaip-agent-marketing-analyst.toml +1 -2
  101. package/templates/.agents/config/clients/codex/agents/zaip-agent-orchestrator.toml +1 -2
  102. package/templates/.agents/config/clients/codex/agents/zaip-agent-refactoring-specialist.toml +1 -2
  103. package/templates/.agents/config/clients/codex/agents/zaip-agent-sales-specialist.toml +1 -2
  104. package/templates/.agents/config/clients/codex/agents/zaip-agent-task-executor.toml +1 -2
  105. package/templates/.agents/config/clients/codex/agents/zaip-agent-website-developer.toml +1 -2
  106. package/templates/.agents/config/clients/codex/agents/zaip-agent-writer.toml +1 -2
  107. package/templates/.agents/dashboard/.zaip-managed.json +40 -32
  108. package/templates/.agents/dashboard/apps/frontend/dist/assets/{DashboardHelpDialog-DlN5EMmD.js → DashboardHelpDialog-CkQ_y8kg.js} +60 -1
  109. package/templates/.agents/dashboard/apps/frontend/dist/assets/{ProjectSettingsPage-BRdoeTWk.js → ProjectSettingsPage-doIMhei7.js} +1 -1
  110. package/templates/.agents/dashboard/apps/frontend/dist/assets/{TraceabilityWorkspace-B_5u4Xnp.js → TraceabilityWorkspace-C_4ftjyB.js} +1 -1
  111. package/templates/.agents/dashboard/apps/frontend/dist/assets/{index-C4rLXrz_.js → index-Rbmpz4UO.js} +2 -2
  112. package/templates/.agents/dashboard/apps/frontend/dist/index.html +1 -1
  113. package/templates/.agents/dashboard/apps/frontend/package.json +1 -1
  114. package/templates/.agents/dashboard/modules/cost-monitor/package.json +1 -1
  115. package/templates/.agents/dashboard/modules/operations/package.json +1 -1
  116. package/templates/.agents/dashboard/modules/traceability/package.json +1 -1
  117. package/templates/.agents/dashboard/packages/branding/package.json +1 -1
  118. package/templates/.agents/dashboard/packages/contracts/package.json +1 -1
  119. package/templates/.agents/dashboard/packages/data-export/package.json +1 -1
  120. package/templates/.agents/dashboard/packages/local-runtime/dist/server.cjs +175 -175
  121. package/templates/.agents/dashboard/packages/local-runtime/dist/sqlite-read-model.cjs +135 -135
  122. package/templates/.agents/dashboard/packages/local-runtime/package.json +1 -1
  123. package/templates/.agents/dashboard/packages/local-runtime/server.mjs +175 -175
  124. package/templates/.agents/dashboard/packages/local-runtime/src/server-operation-detail.test.ts +106 -0
  125. package/templates/.agents/dashboard/packages/local-runtime/src/server.test.ts +0 -13
  126. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-counts-every-root-execution-day-returns.test.ts +17 -230
  127. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-counts-fixture.test-support.ts +239 -0
  128. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-launch-page.test.mjs +28 -8
  129. package/templates/.agents/dashboard/packages/ui/package.json +1 -1
  130. package/templates/.agents/dashboard/tests/e2e/package.json +1 -1
  131. package/templates/.agents/design/.zaip-managed.json +3 -3
  132. package/templates/.agents/design/apps/catalog/package.json +1 -1
  133. package/templates/.agents/design/packages/local-runtime/package.json +1 -1
  134. package/templates/.agents/requirements/autonomous-assurance.md +1 -1
  135. package/templates/.agents/requirements/cloud-hostinger-autonomy.md +1 -1
  136. package/templates/.agents/requirements/comparative-audit.md +1 -1
  137. package/templates/.agents/requirements/context-engine.md +1 -1
  138. package/templates/.agents/requirements/cost-observability.md +1 -1
  139. package/templates/.agents/requirements/dashboard-launch.md +1 -1
  140. package/templates/.agents/requirements/data-export.md +1 -1
  141. package/templates/.agents/requirements/design-system.md +1 -1
  142. package/templates/.agents/requirements/execution-engine.md +1 -1
  143. package/templates/.agents/requirements/kanban-dashboard.md +1 -1
  144. package/templates/.agents/requirements/model-orchestration.md +1 -1
  145. package/templates/.agents/requirements/public-distribution.md +1 -1
  146. package/templates/.agents/requirements/software-architecture.md +1 -1
  147. package/templates/.agents/requirements/strategic-improvements.md +1 -1
  148. package/templates/.agents/requirements/work-lifecycle.md +1 -1
  149. package/templates/.agents/requirements/zaip-strategic-95.md +1 -1
  150. package/templates/.agents/rules/astra-diagnostic.md +1 -1
  151. package/templates/.agents/runtime/VERSION +1 -1
  152. package/templates/.agents/runtime/bin/zaip.mjs +627 -177
  153. package/templates/.agents/runtime/checksums.sha256 +5 -5
  154. package/templates/.agents/runtime/manifest.json +1 -1
  155. package/templates/.agents/runtime/provenance.json +4 -4
  156. package/templates/.agents/runtime/sbom.cdx.json +1 -1
  157. package/templates/.agents/skills/zaip-skill-execution/SKILL.md +1 -1
  158. package/templates/.agents/skills/zaip-skill-execution/references/correction-cycle.md +26 -0
  159. package/templates/.agents/skills/zaip-skill-execution/references/runtime-governance.md +2 -0
  160. package/templates/.agents/skills/zaip-skill-release-qualification/references/local-preparation.md +31 -1
  161. package/templates/.agents/skills/zaip-skill-setup/SKILL.md +26 -1
  162. package/templates/.agents/skills/zaip-skill-task/references/commit-policy.md +8 -1
  163. package/templates/.agents/zaip-aios/VERSION +1 -1
  164. package/templates/.agents/zaip-aios/guides/14-troubleshooting.md +52 -1
  165. package/templates/template-manifest.json +95 -87
@@ -1,11 +1,13 @@
1
1
  export const CODEX_PRE_TOOL_SUPPORT_HOOK = String.raw `import { execFile } from "node:child_process";
2
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";
3
+ import { link, lstat, mkdir, readFile, realpath, unlink, writeFile } from "node:fs/promises";
4
+ import { dirname, isAbsolute, relative, resolve, toNamespacedPath } from "node:path";
5
+ import { homedir } from "node:os";
5
6
  import { promisify } from "node:util";
6
7
 
7
8
  const execute = promisify(execFile);
8
- const inside = (path, root) => {
9
+ const pathIdentity = value => process.platform === "win32" ? toNamespacedPath(resolve(value)).toLowerCase() : resolve(value);
10
+ export const inside = (path, root) => {
9
11
  const scoped = relative(root, resolve(path));
10
12
  return scoped === "" || (!scoped.startsWith("..") && !isAbsolute(scoped));
11
13
  };
@@ -13,10 +15,16 @@ const inside = (path, root) => {
13
15
  export async function selectHookRepositoryRoot(installedRoot, cwd) {
14
16
  if (typeof cwd !== "string") throw new Error("Tool execution without a workspace is blocked.");
15
17
  const physicalCwd = await realpath(cwd).catch(() => null);
16
- if (!physicalCwd || physicalCwd.toLowerCase() !== resolve(cwd).toLowerCase()) {
18
+ if (!physicalCwd || pathIdentity(physicalCwd) !== pathIdentity(cwd)) {
17
19
  throw new Error("Tool workspace could not be safely resolved.");
18
20
  }
19
- if (inside(cwd, installedRoot)) return installedRoot;
21
+ if (resolve(cwd).toLowerCase() === installedRoot.toLowerCase()) return installedRoot;
22
+ if (inside(cwd, installedRoot)) {
23
+ let cursor = resolve(cwd);
24
+ while (cursor.toLowerCase() !== installedRoot.toLowerCase() &&
25
+ !(await lstat(resolve(cursor, ".git")).catch(() => null))) cursor = dirname(cursor);
26
+ if (cursor.toLowerCase() === installedRoot.toLowerCase()) return installedRoot;
27
+ }
20
28
  try {
21
29
  const git = async (path, ...args) => (await execute("git", ["-C", path, ...args], {
22
30
  encoding: "utf8", windowsHide: true, timeout: 5000, maxBuffer: 1048576,
@@ -37,13 +45,28 @@ export async function selectHookRepositoryRoot(installedRoot, cwd) {
37
45
  }
38
46
  }
39
47
 
48
+ export async function selectHookExecutionWorkspace(installedRoot, event) {
49
+ // Codex normalizes shell tools to Bash; preserve the host-supplied literal workdir.
50
+ await selectHookRepositoryRoot(installedRoot, event.cwd);
51
+ const workdir = ["exec_command", "shell_command", "Bash"].includes(event.tool_name) ? event.tool_input?.workdir : undefined;
52
+ if (workdir !== undefined && (typeof workdir !== "string" || !isAbsolute(workdir)))
53
+ throw new Error("Tool workdir must be an absolute literal workspace.");
54
+ const cwd = workdir ?? event.cwd;
55
+ return { cwd, repositoryRoot: await selectHookRepositoryRoot(installedRoot, cwd) };
56
+ }
57
+
40
58
  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;
59
+ if (command.includes("\r") || command.includes("\n")) return null;
43
60
  const tokens = [];
44
61
  let token = "", quote = null, started = false;
45
- for (const character of command) {
62
+ for (let index = 0; index < command.length; index++) {
63
+ const character = command[index];
46
64
  if (quote !== null) {
65
+ if (quote === '"' && (character === "$" || character === String.fromCharCode(96))) return null;
66
+ if (quote === "'" && character === "'" && command[index + 1] === "'" && process.platform === "win32") {
67
+ token += "'"; index++; continue;
68
+ }
69
+ if (process.platform !== "win32" && quote === '"' && character === "\\") return null;
47
70
  if (character === quote) quote = null;
48
71
  else token += character;
49
72
  } else if (character === "'" || character === '"') {
@@ -52,6 +75,7 @@ export function tokenizeLiteralCommand(command) {
52
75
  } else if (/\s/.test(character)) {
53
76
  if (started) { tokens.push(token); token = ""; started = false; }
54
77
  } else {
78
+ if (";|&<>(){}$%".includes(character) || character === String.fromCharCode(96)) return null;
55
79
  token += character;
56
80
  started = true;
57
81
  }
@@ -61,12 +85,248 @@ export function tokenizeLiteralCommand(command) {
61
85
  return tokens;
62
86
  }
63
87
 
88
+ export function shellSyntax(command) {
89
+ let quote = null, interpolation = false, redirection = false, grouping = false;
90
+ for (let index = 0; index < command.length; index += 1) {
91
+ const character = command[index];
92
+ if (quote === "'") {
93
+ if (character === "'" && command[index + 1] === "'") index += 1;
94
+ else if (character === "'") quote = null;
95
+ continue;
96
+ }
97
+ if (character === "'" && quote === null) { quote = "'"; continue; }
98
+ if (character === '"') { quote = quote === '"' ? null : '"'; continue; }
99
+ if (character === "$" && command[index + 1] === "(") interpolation = true;
100
+ if (character === String.fromCharCode(96)) interpolation = true;
101
+ if (character === ">" && quote === null) redirection = true;
102
+ if (quote === null && /[(){}]/.test(character)) grouping = true;
103
+ }
104
+ return { interpolation, redirection, grouping };
105
+ }
106
+
107
+ const shellMutationPattern = /\b(?:Set-Content|Add-Content|Out-File|New-Item|Copy-Item|Move-Item|Remove-Item|sc|ac|ni|cp|mv|mi|ri|tee|sed\b[^\r\n;|]*(?:\s-[A-Za-z]*i[^\s;|]*|--in-place(?:=[^\s;|]*)?)(?=\s|$)|git\s+apply|npm\s+version|pnpm\s+version)\b/i;
108
+
109
+ export function requiresShellMutationRun(command, tokens, cwd, repositoryRoot) {
110
+ const syntax = shellSyntax(command);
111
+ if (!syntax.interpolation && !syntax.redirection && !syntax.grouping && isLiteralReadSequence(command)) return false;
112
+ const executable = tokens?.[0] ?? "";
113
+ const canonicalCli = /^(?:node|node\.exe)$/i.test(executable) && tokens?.[1] &&
114
+ resolve(cwd, tokens[1]).toLowerCase() === resolve(repositoryRoot, ".agents/runtime/bin/zaip.mjs").toLowerCase();
115
+ const dataCommand = canonicalCli || /^(?:Write-Output|echo|printf)$/i.test(executable);
116
+ if (tokens && !syntax.interpolation && !syntax.grouping && dataCommand) {
117
+ return syntax.redirection;
118
+ }
119
+ // Unknown executables, wrappers and assignments retain the original gate.
120
+ return (tokens && isSedMutation(tokens)) || shellMutationPattern.test(command) || syntax.redirection;
121
+ }
122
+
123
+ // Recognize only literal read-only pipelines/sequences. Quoted search text is data,
124
+ // not a PowerShell alias (for example the AC prefix in acceptance-criterion IDs).
125
+ export function isLiteralReadSequence(command) {
126
+ const parts = [];
127
+ let start = 0, quote = null;
128
+ for (let index = 0; index < command.length; index++) {
129
+ const char = command[index];
130
+ if (quote !== null) {
131
+ if (char === quote) {
132
+ if (quote === "'" && command[index + 1] === "'" && process.platform === "win32") index++;
133
+ else quote = null;
134
+ }
135
+ } else if (char === "'" || char === '"') quote = char;
136
+ else if (char === ";" || char === "|") { parts.push(command.slice(start, index)); start = index + 1; }
137
+ }
138
+ if (quote !== null) return false;
139
+ parts.push(command.slice(start));
140
+ return parts.every(part => {
141
+ const values = tokenizeLiteralCommand(part);
142
+ if (!values?.length) return false;
143
+ if (/^(?:Get-Content|Measure-Object)$/i.test(values[0])) return true;
144
+ if (!/^(?:rg|rg\.exe)$/i.test(values[0])) return false;
145
+ return !values.slice(1).some(value => /^--(?:pre|hostname-bin)(?:=|$)/i.test(value));
146
+ });
147
+ }
148
+
149
+ function isSedMutation(tokens) {
150
+ const executable = ((tokens[0] ?? "").split(/[\\/]/).pop() ?? "").replace(/\.(?:exe|cmd|bat)$/i, "");
151
+ return /^sed$/i.test(executable) && tokens.slice(1).some((token) =>
152
+ /^-[A-Za-z]*i/i.test(token) || /^--in-place(?:=|$)/i.test(token) ||
153
+ /^(?:-f|--file)(?:=|$)/i.test(token) || /(?:^|[;\/#@])w(?:\s+\S|[^\s\/;#@]+\/)/.test(token));
154
+ }
155
+
156
+ export function gitInvocation(tokens, deny) {
157
+ if (tokens === null || !/^(?:git|git\.exe)$/i.test(tokens[0] ?? "")) return null;
158
+ let index = 1;
159
+ while (index < tokens.length && tokens[index].startsWith("-")) {
160
+ const rawOption = tokens[index];
161
+ const option = rawOption.toLowerCase();
162
+ if (option === "--version" || option === "--help") return null;
163
+ if (option === "--no-pager" || option === "--literal-pathspecs") { index += 1; continue; }
164
+ if (rawOption === "-c" || rawOption.startsWith("-c") || option.startsWith("--config") ||
165
+ option === "--git-dir" || option === "--work-tree" || option === "--namespace") {
166
+ deny("Git global configuration and repository-routing overrides are blocked.");
167
+ }
168
+ if (rawOption === "-C" || rawOption.startsWith("-C")) {
169
+ const directory = rawOption === "-C" ? tokens[index + 1] : rawOption.slice(2);
170
+ if (directory !== ".") deny("Git working-directory overrides outside the repository root are blocked.");
171
+ index += rawOption === "-C" ? 2 : 1;
172
+ continue;
173
+ }
174
+ deny("Unknown Git global option is blocked.");
175
+ }
176
+ const subcommand = tokens[index]?.toLowerCase();
177
+ return subcommand === undefined ? null : { subcommand, arguments: tokens.slice(index + 1) };
178
+ }
179
+
180
+ export function checkDestructivePatterns(command, patterns, deny) {
181
+ if (/^(?:Write-Output|echo|printf)\s+'(?:[^']|'')*'\s*$/i.test(command)) return;
182
+ for (const entry of patterns) if (entry.pattern.test(command)) deny(entry.reason);
183
+ }
184
+
185
+ export async function reviewedReleasePaths(command, cwd, repositoryRoot, credentialReferencePattern, deny) {
186
+ const tokens = tokenizeLiteralCommand(command);
187
+ if (!tokens || tokens[0] !== "node" || ![
188
+ "scripts/local-release-preparation.mjs", "scripts/local-release-deploy.mjs",
189
+ ].includes(tokens[1])) return [];
190
+ if (resolve(cwd) !== repositoryRoot) return [];
191
+ const helper = resolve(repositoryRoot, tokens[1]);
192
+ try {
193
+ const info = await lstat(helper);
194
+ if (!info.isFile() || info.isSymbolicLink() || resolve(await realpath(helper)) !== helper) return [];
195
+ } catch { return []; }
196
+ const preparation = tokens[1] === "scripts/local-release-preparation.mjs";
197
+ const mode = preparation ? tokens[2] : "deploy";
198
+ const start = preparation && ["prepare", "verify"].includes(mode) ? 3 : !preparation ? 2 : -1;
199
+ if (start < 0) return [];
200
+ const values = new Map();
201
+ const switches = new Set(["--initial", "--dry-run", "--offline-dry-run", "--draft-only"]);
202
+ const allowed = new Set(mode === "verify"
203
+ ? ["--repository", "--expected-repository", "--receipt", "--tarball", "--policy", "--trust-store", "--sha", "--tree-sha256", "--channel", "--version", "--profile"]
204
+ : mode === "prepare"
205
+ ? ["--repository", "--expected-repository", "--sha", "--output-dir", "--policy", "--key-id", "--private-key", "--trust-store", "--channel", "--version", "--expires-at", "--profile"]
206
+ : ["--repository", "--expected-repository", "--branch", "--output-dir", "--policy", "--private-key", "--trust-store", "--key-id", "--sha", "--channel", "--version", "--expires-at", "--profile", "--notes-file", "--inventory-file", "--from", ...switches]);
207
+ for (let i = start; i < tokens.length; i += 1) {
208
+ const flag = tokens[i];
209
+ if (!allowed.has(flag) || values.has(flag)) return [];
210
+ if (switches.has(flag)) values.set(flag, true);
211
+ else {
212
+ const value = tokens[++i];
213
+ if (!value || value.startsWith("--")) return [];
214
+ values.set(flag, value);
215
+ }
216
+ }
217
+ const required = mode === "verify"
218
+ ? ["--repository", "--expected-repository", "--receipt", "--tarball", "--policy", "--trust-store", "--sha", "--tree-sha256", "--channel", "--version"]
219
+ : mode === "prepare"
220
+ ? ["--repository", "--expected-repository", "--sha", "--output-dir", "--policy", "--key-id", "--channel", "--version", "--expires-at"]
221
+ : ["--repository", "--expected-repository", "--branch", "--output-dir", "--policy", "--key-id", "--sha", "--channel", "--version", "--expires-at", "--notes-file", "--inventory-file"];
222
+ if (required.some(flag => !values.has(flag)) || values.get("--expected-repository") !== "zaip-ai/zaip-aios" ||
223
+ !/^[a-f0-9]{40}$/i.test(values.get("--sha")) ||
224
+ (mode === "verify" && !/^[a-f0-9]{64}$/i.test(values.get("--tree-sha256"))) ||
225
+ !["beta", "latest"].includes(values.get("--channel")) ||
226
+ !/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.test(values.get("--version")) ||
227
+ (values.has("--profile") && !["essential", "full"].includes(values.get("--profile"))) ||
228
+ (mode !== "verify" && (
229
+ !/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(values.get("--key-id")) ||
230
+ !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?Z$/.test(values.get("--expires-at")))) ||
231
+ (mode === "deploy" && (values.get("--branch") !== "main" ||
232
+ (values.has("--from") && !/^v\d+\.\d+$/.test(values.get("--from"))) ||
233
+ (values.has("--offline-dry-run") && !values.has("--dry-run")) ||
234
+ (values.has("--from") === values.has("--initial"))))) return [];
235
+ const repository = values.get("--repository");
236
+ if (!isAbsolute(repository) && repository !== ".") return [];
237
+ const releaseRoot = resolve(cwd, repository);
238
+ // The host may expose only Bash/chat cwd. This exact reviewed helper targets its explicit repository internally.
239
+ if (releaseRoot !== repositoryRoot) {
240
+ try { if (await selectHookRepositoryRoot(repositoryRoot, releaseRoot) !== releaseRoot) return []; }
241
+ catch { return []; }
242
+ }
243
+ const keyRoot = resolve(homedir(), ".codex/release-keys/zaip-aios");
244
+ const candidateRoot = resolve(homedir(), ".codex/release-candidates/zaip-aios");
245
+ const profile = values.get("--profile") ?? "essential";
246
+ const candidateName = new RegExp("^v" + values.get("--version").replace(".", "\\.") + "-" + profile + "-" + values.get("--sha").slice(0, 8) + "-[0-9]{8}$", "i");
247
+ const external = releaseRoot !== repositoryRoot ? [repository] : [];
248
+ let selectedCandidate = null;
249
+ let selectedKey = values.get("--key-id") ?? null;
250
+ for (const flag of ["--receipt", "--tarball", "--policy", "--trust-store", "--private-key", "--output-dir", "--notes-file", "--inventory-file"]) {
251
+ const value = values.get(flag);
252
+ if (!value) continue;
253
+ if ((!isAbsolute(value) && !inside(resolve(cwd, value), repositoryRoot)) ||
254
+ /[\x00-\x1f*?{}\[\]$%<>]/.test(value) || value.split(/[\\/]/).includes("..")) return [];
255
+ const path = resolve(cwd, value);
256
+ let parent = path;
257
+ while (parent !== dirname(parent)) {
258
+ try { if ((await lstat(parent)).isSymbolicLink()) deny("Reviewed release paths must not traverse links."); }
259
+ catch (error) { if (error?.code !== "ENOENT") deny("Reviewed release paths could not be safely resolved."); }
260
+ parent = dirname(parent);
261
+ }
262
+ if (inside(path, repositoryRoot) || inside(path, releaseRoot)) deny("Reviewed release artifacts and credentials must stay outside the repository.");
263
+ if (path !== (process.platform === "win32" ? value.replaceAll("/", "\\") : value)) return [];
264
+ if (flag !== "--private-key" && credentialReferencePattern.test(value)) return [];
265
+ const keyRelative = relative(keyRoot, path).replaceAll("\\", "/");
266
+ const keyMatch = /^([a-z0-9][a-z0-9._-]{0,63})\/(.+)$/i.exec(keyRelative);
267
+ const keyId = keyMatch?.[1];
268
+ const keyFile = keyMatch?.[2];
269
+ const keyPath = flag === "--trust-store" && keyFile === "trust" ||
270
+ flag === "--private-key" && keyFile === "release-private.pem" ||
271
+ flag === "--policy" && keyFile === "policy-v" + values.get("--version") + "-" + profile + ".json";
272
+ if (keyPath) {
273
+ if (!keyId || selectedKey && selectedKey !== keyId) return [];
274
+ selectedKey = keyId;
275
+ }
276
+ const candidateRelative = relative(candidateRoot, path).replaceAll("\\", "/");
277
+ const candidateParts = candidateRelative.split("/");
278
+ const candidate = candidateParts[0];
279
+ const candidatePath = candidateName.test(candidate) &&
280
+ (flag === "--output-dir" && candidateParts.length === 1 ||
281
+ flag !== "--output-dir" && candidateParts.length === 2 && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(candidateParts[1]));
282
+ if (candidatePath) {
283
+ if (selectedCandidate && selectedCandidate !== candidate) return [];
284
+ selectedCandidate = candidate;
285
+ }
286
+ if (!keyPath && !candidatePath) return [];
287
+ if (["--receipt", "--tarball", "--output-dir", "--notes-file", "--inventory-file"].includes(flag) && !candidatePath ||
288
+ ["--trust-store", "--private-key", "--policy"].includes(flag) && !keyPath) return [];
289
+ if (flag === "--receipt" && !path.endsWith("local-release-receipt.json") ||
290
+ flag === "--tarball" && !path.endsWith(".tgz") ||
291
+ flag === "--policy" && !path.endsWith(".json") ||
292
+ flag === "--notes-file" && !path.endsWith(".md") ||
293
+ flag === "--inventory-file" && !path.endsWith(".json")) return [];
294
+ external.push(value);
295
+ }
296
+ return external;
297
+ }
298
+
299
+ /** Narrow read capability, never a general external-path or executable allowlist. */
300
+ export async function reviewedContextReadPaths(command, credentialPattern) {
301
+ const tokens = tokenizeLiteralCommand(command);
302
+ if (!tokens || tokens.length < 2 || credentialPattern.test(command)) return [];
303
+ const executable = tokens[0].toLowerCase();
304
+ if (!["get-content", "rg", "rg.exe"].includes(executable)) return [];
305
+ const flags = executable === "get-content"
306
+ ? new Set(["-literalpath", "-path", "-raw", "-totalcount", "-tail", "-encoding"])
307
+ : new Set(["-n", "--line-number", "-i", "--ignore-case", "-f", "--fixed-strings", "-e", "--regexp", "-m", "--max-count", "--"]);
308
+ // -f means a pattern file in ripgrep, not fixed strings: do not permit it.
309
+ if (tokens.slice(1).some(token => token.startsWith("-") && (!flags.has(token.toLowerCase()) || token === "-f"))) return [];
310
+ const roots = ["memories", "skills", "plugins/cache"].map(part => resolve(homedir(), ".codex", part));
311
+ const paths = tokens.slice(1).filter(value => isAbsolute(value));
312
+ if (!paths.length) return [];
313
+ for (const path of paths) {
314
+ if (!roots.some(root => inside(path, root)) || !/\.(?:md|txt|json|yaml|yml|toml)$/i.test(path)) return [];
315
+ try {
316
+ const info = await lstat(path);
317
+ if (!info.isFile() || info.isSymbolicLink() || pathIdentity(await realpath(path)) !== pathIdentity(path)) return [];
318
+ } catch { return []; }
319
+ }
320
+ return paths;
321
+ }
322
+
64
323
  export async function captureCliTurn(repositoryRoot, event, command) {
65
324
  const tokens = tokenizeLiteralCommand(command);
66
325
  if (!tokens || tokens.length < 4 || !/^(?:node|node\.exe)$/i.test(tokens[0])) return;
67
326
  const script = resolve(event.cwd, tokens[1]);
68
327
  if (script.toLowerCase() !== resolve(repositoryRoot, ".agents/runtime/bin/zaip.mjs").toLowerCase()) return;
69
328
  if (!((tokens[2] === "conversation" || tokens[2] === "run") && tokens[3] === "start")) return;
329
+ if (tokens.includes("--help") || tokens.includes("-h")) return;
70
330
  if (tokens.some(token => token.startsWith("--codex-hook-"))) throw new Error("Codex hook identity cannot be supplied by the caller.");
71
331
  const threadId = event.session_id, turnId = event.turn_id, toolUseId = event.tool_use_id;
72
332
  if (typeof threadId !== "string" || !/^[A-Za-z0-9_-]{1,256}$/.test(threadId) ||
@@ -89,17 +349,48 @@ export async function captureCliTurn(repositoryRoot, event, command) {
89
349
  const info = await lstat(parent), physical = await realpath(parent);
90
350
  if (!info.isDirectory() || info.isSymbolicLink() || !inside(physical, repositoryRoot)) throw new Error("Unsafe turn receipt directory");
91
351
  }
92
- await writeFile(temporary, JSON.stringify({
352
+ const value = {
93
353
  schema_version: 1, thread_id: threadId, turn_id: turnId,
94
354
  tool_use_sha256: digest, argv_sha256: argvSha256, observed_at_ms: Date.now(),
95
- }) + "\n", { flag: "wx" });
96
- await rename(temporary, target);
355
+ };
356
+ await writeFile(temporary, JSON.stringify(value) + "\n", { flag: "wx", mode: 0o600 });
357
+ try { await link(temporary, target); }
358
+ catch (error) {
359
+ if (error.code !== "EEXIST") throw error;
360
+ const info = await lstat(target);
361
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 4096 ||
362
+ pathIdentity(await realpath(target)) !== pathIdentity(target)) throw new Error("Unsafe existing turn receipt");
363
+ const prior = JSON.parse(await readFile(target, "utf8"));
364
+ for (const key of ["schema_version", "thread_id", "turn_id", "tool_use_sha256", "argv_sha256"])
365
+ if (prior[key] !== value[key]) throw new Error("Conflicting existing turn receipt");
366
+ if (!Number.isSafeInteger(prior.observed_at_ms) || prior.observed_at_ms > Date.now()) throw new Error("Invalid turn receipt time");
367
+ }
97
368
  } catch {
98
369
  throw new Error("Codex turn identity could not be persisted for the ZAIP CLI.");
99
- }
370
+ } finally { await unlink(temporary).catch(() => {}); }
100
371
  return command + " --codex-hook-id=" + digest + " --codex-hook-thread=" + threadId;
101
372
  }
102
373
 
374
+ export function validateInlineExecution(command, deny) {
375
+ if (shellSyntax(command).interpolation) {
376
+ deny("Nested shell execution is blocked; use a reviewed repository script.");
377
+ }
378
+ if (/(?:^|[;&|\r\n])\s*&\s*\(/.test(command)) deny("Dynamic shell invocation is blocked; use a literal command.");
379
+ const inlinePatterns = [
380
+ /\b(?:node|deno)\s+(?:-e|--eval)\b/i,
381
+ /\b(?:python(?:3)?|ruby|perl|php)\s+(?:-c|-e)\b/i,
382
+ /\bdeno\s+eval\b/i,
383
+ /\b(?:powershell|pwsh)(?:\.exe)?\b[\s\S]*\s-(?:Command|EncodedCommand|enc)\b/i,
384
+ /\bcmd(?:\.exe)?\s+\/c\b/i,
385
+ /\b(?:bash|sh|zsh|dash|fish)(?:\.exe)?\b[^\r\n;|]*\s-[A-Za-z]*c(?=\s|$)/i,
386
+ /\|\s*(?:node|deno|python(?:3)?|ruby|perl|php|powershell|pwsh|cmd)\b/i,
387
+ /\b(?:Invoke-Expression|iex)\b/i,
388
+ ];
389
+ if (inlinePatterns.some((pattern) => pattern.test(command))) {
390
+ deny("Inline or piped interpreter execution is blocked; use a reviewed repository script.");
391
+ }
392
+ }
393
+
103
394
  export function rewriteCliInput(toolInput, command, commandFromCmd) {
104
395
  return { hookSpecificOutput: {
105
396
  hookEventName: "PreToolUse", permissionDecision: "allow",
@@ -1 +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
+ {"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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+YpD,CAAC"}
@@ -1,2 +1,2 @@
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";
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 activation = await import(\"./zaip-hook-activation.mjs\").catch(() => null);\nconst installedRoot = resolve(dirname(hookPath), \"..\", \"..\");\nlet repositoryRoot = installedRoot;\nlet diagnosticPath = resolve(\n repositoryRoot,\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 let nestedGit = false;\n if (inside(cwd, installedRoot)) {\n for (let cursor = resolve(cwd); cursor.toLowerCase() !== installedRoot.toLowerCase(); cursor = dirname(cursor)) {\n if (existsSync(resolve(cursor, \".git\"))) { nestedGit = true; break; }\n }\n }\n if (!inside(cwd, installedRoot) || nestedGit) {\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, \".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 activation?.recordActivation(repositoryRoot, event);\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,m7eA8UzC,CAAC"}
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,80fAqVzC,CAAC"}
@@ -6,12 +6,12 @@ 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 activation = await import("./zaip-hook-activation.mjs").catch(() => null);
9
10
  const installedRoot = resolve(dirname(hookPath), "..", "..");
10
11
  let repositoryRoot = installedRoot;
11
12
  let diagnosticPath = resolve(
12
13
  repositoryRoot,
13
- ".agents",
14
- "local",
14
+ ".local",
15
15
  "hooks",
16
16
  "subagent-lifecycle.ndjson",
17
17
  );
@@ -25,7 +25,13 @@ function selectRoot(cwd) {
25
25
  if (typeof cwd !== "string") return false;
26
26
  try {
27
27
  if (realpathSync(cwd).toLowerCase() !== resolve(cwd).toLowerCase()) return false;
28
- if (!inside(cwd, installedRoot)) {
28
+ let nestedGit = false;
29
+ if (inside(cwd, installedRoot)) {
30
+ for (let cursor = resolve(cwd); cursor.toLowerCase() !== installedRoot.toLowerCase(); cursor = dirname(cursor)) {
31
+ if (existsSync(resolve(cursor, ".git"))) { nestedGit = true; break; }
32
+ }
33
+ }
34
+ if (!inside(cwd, installedRoot) || nestedGit) {
29
35
  const git = (root, ...args) => execFileSync("git", ["-C", root, ...args], {
30
36
  encoding: "utf8", windowsHide: true, timeout: 5000,
31
37
  }).trim();
@@ -38,7 +44,7 @@ function selectRoot(cwd) {
38
44
  if (common(installedRoot) !== common(candidate)) return false;
39
45
  repositoryRoot = candidate;
40
46
  }
41
- diagnosticPath = resolve(repositoryRoot, ".agents", "local", "hooks", "subagent-lifecycle.ndjson");
47
+ diagnosticPath = resolve(repositoryRoot, ".local", "hooks", "subagent-lifecycle.ndjson");
42
48
  pendingRoot = resolve(repositoryRoot, ".local", "hooks", "subagent-pending");
43
49
  return true;
44
50
  } catch { return false; }
@@ -69,6 +75,7 @@ try {
69
75
  const cwd = text(event.cwd);
70
76
  if (cwd === null || !selectRoot(cwd)) process.exit(0);
71
77
  const eventName = text(event.hook_event_name);
78
+ activation?.recordActivation(repositoryRoot, event);
72
79
  const lifecycleEvent =
73
80
  eventName === "SubagentStart" ? "start" : eventName === "SubagentStop" ? "stop" : null;
74
81
  const session = text(event.session_id);
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8U5C,CAAC"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqV5C,CAAC"}
@@ -0,0 +1,3 @@
1
+ /** Appended to native authority; receives the shared literal parser. */
2
+ export declare const CODEX_VALIDATION_RUNTIME_SUPPORT: string;
3
+ //# sourceMappingURL=codex-validation-runtime-template.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-validation-runtime-template.d.ts","sourceRoot":"","sources":["../../src/services/codex-validation-runtime-template.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,eAAO,MAAM,gCAAgC,QAe5C,CAAC"}
@@ -0,0 +1,18 @@
1
+ /** Appended to native authority; receives the shared literal parser. */
2
+ export const CODEX_VALIDATION_RUNTIME_SUPPORT = String.raw `
3
+ /** Candidate validation requires this exact host runtime, not arbitrary executables. */
4
+ export async function reviewedValidationRuntimePaths(command, tokenizeLiteralCommand) {
5
+ const tokens = tokenizeLiteralCommand(command);
6
+ if (!tokens || !/^(?:node|node\.exe)$/i.test(tokens[0]) ||
7
+ !/^\.agents[\\/]runtime[\\/]bin[\\/]zaip\.mjs$/.test(tokens[1] ?? "") ||
8
+ tokens[2] !== "validation" || !["batch", "execute"].includes(tokens[3])) return [];
9
+ const positions = tokens.flatMap((value, index) => value === "--executable" ? [index] : []);
10
+ if (positions.length !== 1) return [];
11
+ const path = tokens[positions[0] + 1];
12
+ if (!path || !isAbsolute(path) || command.split(path).length !== 2) return [];
13
+ try {
14
+ return await realpath(path) === await realpath(process.execPath) ? [path] : [];
15
+ } catch { return []; }
16
+ }
17
+ `;
18
+ //# sourceMappingURL=codex-validation-runtime-template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-validation-runtime-template.js","sourceRoot":"","sources":["../../src/services/codex-validation-runtime-template.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,MAAM,CAAC,MAAM,gCAAgC,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;CAezD,CAAC"}
@@ -6,7 +6,7 @@ export function codexWindowsHookCommand(hook, workspaceRoot) {
6
6
  const literalRoot = workspaceRoot === undefined
7
7
  ? "(Get-Location)"
8
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`;
9
+ const script = `$ProgressPreference = 'SilentlyContinue'; $search = ${literalRoot}; $git = $null; while ($search) { $candidate = Join-Path $search '.local/git/cmd/git.exe'; if (Test-Path -LiteralPath $candidate -PathType Leaf) { $git = $candidate; break }; $parent = Split-Path -Path $search -Parent; if (!$parent -or $parent -eq $search) { break }; $search = $parent }; if (!$git) { $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
10
  return `powershell -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(script, "utf16le").toString("base64")}`;
11
11
  }
12
12
  //# sourceMappingURL=codex-windows-hook-command.js.map
@@ -1 +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"}
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,uDAAuD,WAAW,ihBAAihB,IAAI,qGAAqG,CAAC;IAC5sB,OAAO,yDAAyD,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;AACtH,CAAC"}
@@ -3,11 +3,20 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { afterEach, describe, expect, it } from "vitest";
6
+ import { CODEX_CONFIG } from "./codex-config-template.js";
6
7
  import { codexWindowsHookCommand } from "./codex-windows-hook-command.js";
7
8
  const roots = [];
8
9
  afterEach(async () => {
9
10
  await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
10
11
  });
12
+ describe("installed Codex hook configuration", () => {
13
+ it("gives each Windows subagent lifecycle hook a cold-start-safe timeout", () => {
14
+ const subagentTimeouts = [
15
+ ...CODEX_CONFIG.matchAll(/\[\[hooks\.Subagent(?:Start|Stop)\.hooks\]\][\s\S]*?timeout = (\d+)/g),
16
+ ].map((match) => Number(match[1]));
17
+ expect(subagentTimeouts).toEqual([10, 10]);
18
+ });
19
+ });
11
20
  describe.skipIf(process.platform !== "win32")("Codex Windows hook command", () => {
12
21
  it("passes stdin and JSON stdout through cmd.exe from a path with spaces", async () => {
13
22
  const root = await mkdtemp(join(tmpdir(), "zaip hook workspace "));
@@ -27,6 +36,19 @@ describe.skipIf(process.platform !== "win32")("Codex Windows hook command", () =
27
36
  expect(JSON.parse(result.stdout)).toEqual({
28
37
  input: { hook_event_name: "PreToolUse", tool_input: { cmd: "git status" } },
29
38
  });
39
+ expect(result.stderr).toBe("");
40
+ const nested = join(root, "nested workspace");
41
+ await mkdir(nested);
42
+ const fromNestedDirectory = spawnSync("cmd.exe", ["/d", "/s", "/c", command], {
43
+ cwd: nested,
44
+ input: JSON.stringify({ hook_event_name: "PreToolUse", tool_input: { cmd: "git status" } }),
45
+ encoding: "utf8",
46
+ timeout: 10_000,
47
+ });
48
+ expect(fromNestedDirectory.status, fromNestedDirectory.stderr).toBe(0);
49
+ expect(JSON.parse(fromNestedDirectory.stdout)).toMatchObject({
50
+ input: { hook_event_name: "PreToolUse" },
51
+ });
30
52
  });
31
53
  it("exits successfully outside a repository or when its hook is absent", async () => {
32
54
  const root = await mkdtemp(join(tmpdir(), "zaip hook absent "));