@zaip-ai/cli 4.8.0 → 5.0.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 (195) hide show
  1. package/dist/services/adapter-hook-sources.d.ts +3 -0
  2. package/dist/services/adapter-hook-sources.d.ts.map +1 -1
  3. package/dist/services/adapter-hook-sources.js +3 -0
  4. package/dist/services/adapter-hook-sources.js.map +1 -1
  5. package/dist/services/adapters.d.ts +1 -1
  6. package/dist/services/adapters.d.ts.map +1 -1
  7. package/dist/services/adapters.js +5 -2
  8. package/dist/services/adapters.js.map +1 -1
  9. package/dist/services/adapters.test.js +1 -1
  10. package/dist/services/adapters.test.js.map +1 -1
  11. package/dist/services/codex-conversation-lifecycle-hook.d.ts.map +1 -1
  12. package/dist/services/codex-conversation-lifecycle-hook.js +16 -6
  13. package/dist/services/codex-conversation-lifecycle-hook.js.map +1 -1
  14. package/dist/services/codex-hook-activation.d.ts +1 -1
  15. package/dist/services/codex-hook-activation.d.ts.map +1 -1
  16. package/dist/services/codex-hook-activation.js +116 -10
  17. package/dist/services/codex-hook-activation.js.map +1 -1
  18. package/dist/services/codex-pre-tool-cli-capture-template.d.ts +2 -0
  19. package/dist/services/codex-pre-tool-cli-capture-template.d.ts.map +1 -0
  20. package/dist/services/codex-pre-tool-cli-capture-template.js +104 -0
  21. package/dist/services/codex-pre-tool-cli-capture-template.js.map +1 -0
  22. package/dist/services/codex-pre-tool-cloud-template.d.ts +2 -0
  23. package/dist/services/codex-pre-tool-cloud-template.d.ts.map +1 -0
  24. package/dist/services/codex-pre-tool-cloud-template.js +190 -0
  25. package/dist/services/codex-pre-tool-cloud-template.js.map +1 -0
  26. package/dist/services/codex-pre-tool-dependencies.test-support.d.ts +2 -0
  27. package/dist/services/codex-pre-tool-dependencies.test-support.d.ts.map +1 -0
  28. package/dist/services/codex-pre-tool-dependencies.test-support.js +9 -0
  29. package/dist/services/codex-pre-tool-dependencies.test-support.js.map +1 -0
  30. package/dist/services/codex-pre-tool-hook-fixture.test-support.d.ts +5 -0
  31. package/dist/services/codex-pre-tool-hook-fixture.test-support.d.ts.map +1 -0
  32. package/dist/services/codex-pre-tool-hook-fixture.test-support.js +19 -0
  33. package/dist/services/codex-pre-tool-hook-fixture.test-support.js.map +1 -0
  34. package/dist/services/codex-pre-tool-hook-template-bootstrap.test.d.ts +2 -0
  35. package/dist/services/codex-pre-tool-hook-template-bootstrap.test.d.ts.map +1 -0
  36. package/dist/services/codex-pre-tool-hook-template-bootstrap.test.js +107 -0
  37. package/dist/services/codex-pre-tool-hook-template-bootstrap.test.js.map +1 -0
  38. package/dist/services/codex-pre-tool-hook-template.d.ts.map +1 -1
  39. package/dist/services/codex-pre-tool-hook-template.js +152 -190
  40. package/dist/services/codex-pre-tool-hook-template.js.map +1 -1
  41. package/dist/services/codex-pre-tool-hook-template.test.js +63 -75
  42. package/dist/services/codex-pre-tool-hook-template.test.js.map +1 -1
  43. package/dist/services/codex-pre-tool-host-context-template.d.ts +2 -0
  44. package/dist/services/codex-pre-tool-host-context-template.d.ts.map +1 -0
  45. package/dist/services/codex-pre-tool-host-context-template.js +139 -0
  46. package/dist/services/codex-pre-tool-host-context-template.js.map +1 -0
  47. package/dist/services/codex-pre-tool-support-template.d.ts.map +1 -1
  48. package/dist/services/codex-pre-tool-support-template.js +77 -100
  49. package/dist/services/codex-pre-tool-support-template.js.map +1 -1
  50. package/dist/services/codex-subagent-hook-template.d.ts +1 -1
  51. package/dist/services/codex-subagent-hook-template.d.ts.map +1 -1
  52. package/dist/services/codex-subagent-hook-template.js +59 -15
  53. package/dist/services/codex-subagent-hook-template.js.map +1 -1
  54. package/dist/services/codex-validation-runtime-template.d.ts.map +1 -1
  55. package/dist/services/codex-validation-runtime-template.js +37 -10
  56. package/dist/services/codex-validation-runtime-template.js.map +1 -1
  57. package/dist/services/runtime-template-test-support.d.ts +1 -0
  58. package/dist/services/runtime-template-test-support.d.ts.map +1 -1
  59. package/dist/services/runtime-template-test-support.js +23 -2
  60. package/dist/services/runtime-template-test-support.js.map +1 -1
  61. package/dist/services/runtime-template.test.js +7 -24
  62. package/dist/services/runtime-template.test.js.map +1 -1
  63. package/package.json +1 -1
  64. package/templates/.agents/README-ENG.md +49 -7
  65. package/templates/.agents/README.md +41 -10
  66. package/templates/.agents/config/agents/discovery.json +1 -1
  67. package/templates/.agents/dashboard/.zaip-managed.json +125 -73
  68. package/templates/.agents/dashboard/apps/frontend/dist/assets/{CostCalculationGuidePage-BW7Vy1cp.js → CostCalculationGuidePage-w6pjt4OM.js} +1 -1
  69. package/templates/.agents/dashboard/apps/frontend/dist/assets/{DashboardHelpDialog-BN8KQPrr.js → DashboardHelpDialog-DhlntQcR.js} +117 -17
  70. package/templates/.agents/dashboard/apps/frontend/dist/assets/{PricingSettingsDialog-Prcs-ydy.js → PricingSettingsDialog-BHoZT2an.js} +1 -1
  71. package/templates/.agents/dashboard/apps/frontend/dist/assets/{ProjectSettingsPage-HqGUImIG.js → ProjectSettingsPage-IcncIZ9Y.js} +1 -1
  72. package/templates/.agents/dashboard/apps/frontend/dist/assets/{TraceabilityWorkspace-CNpmQMO4.js → TraceabilityWorkspace-C6rORjpr.js} +1 -1
  73. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-uPnOBShc.js +68 -0
  74. package/templates/.agents/dashboard/apps/frontend/dist/assets/{dashboard-localization-CF7B1Hjn.js → dashboard-localization-DWXjEIgc.js} +49 -7
  75. package/templates/.agents/dashboard/apps/frontend/dist/assets/{icons-vendor-DOhy6D2-.js → icons-vendor-Hc9fp_gs.js} +1 -1
  76. package/templates/.agents/dashboard/apps/frontend/dist/assets/{index-DX65Ufuu.js → index-BqeOEZW5.js} +2 -2
  77. package/templates/.agents/dashboard/apps/frontend/dist/assets/{react-vendor-CkPC-yMz.js → react-vendor-C9ta_O10.js} +1 -1
  78. package/templates/.agents/dashboard/apps/frontend/dist/assets/{traceability-BEdGoIDm.js → traceability-IcefoTOv.js} +1 -1
  79. package/templates/.agents/dashboard/apps/frontend/dist/index.html +5 -5
  80. package/templates/.agents/dashboard/apps/frontend/package.json +1 -1
  81. package/templates/.agents/dashboard/modules/cost-monitor/package.json +1 -1
  82. package/templates/.agents/dashboard/modules/cost-monitor/src/contracts.ts +10 -3
  83. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostFilters.run-page.test.ts +5 -1
  84. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostGroupsTable.period-values.test.tsx +2 -2
  85. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostTables-keeps-legacy-codex-identity-fields-blank.test.ts +5 -5
  86. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostTables.test.ts +36 -43
  87. package/templates/.agents/dashboard/modules/cost-monitor/src/react/cost-monitor-loaders.ts +1 -0
  88. package/templates/.agents/dashboard/modules/cost-monitor/src/react/cost-table-formatters.ts +6 -4
  89. package/templates/.agents/dashboard/modules/cost-monitor/src/server/aggregate.ts +2 -2
  90. package/templates/.agents/dashboard/modules/cost-monitor/src/server/build-cost-monitor-projection.ts +1 -1
  91. package/templates/.agents/dashboard/modules/cost-monitor/src/server/build-cost-summary-projection.ts +11 -25
  92. package/templates/.agents/dashboard/modules/cost-monitor/src/server/cost-summary-defaults.ts +26 -0
  93. package/templates/.agents/dashboard/modules/operations/package.json +1 -1
  94. package/templates/.agents/dashboard/modules/operations/src/contracts.ts +11 -16
  95. package/templates/.agents/dashboard/modules/operations/src/planning-contracts.ts +14 -0
  96. package/templates/.agents/dashboard/modules/operations/src/react/DecisionTable.tsx +65 -0
  97. package/templates/.agents/dashboard/modules/operations/src/react/EntityTables.tsx +5 -71
  98. package/templates/.agents/dashboard/modules/operations/src/react/OperationsPage.tsx +10 -36
  99. package/templates/.agents/dashboard/modules/operations/src/react/operations-overview-period-filter.test.ts +7 -2
  100. package/templates/.agents/dashboard/modules/operations/src/react/use-operations-filter-sync.ts +66 -0
  101. package/templates/.agents/dashboard/modules/traceability/package.json +1 -1
  102. package/templates/.agents/dashboard/packages/branding/package.json +1 -1
  103. package/templates/.agents/dashboard/packages/contracts/package.json +1 -1
  104. package/templates/.agents/dashboard/packages/data-export/package.json +1 -1
  105. package/templates/.agents/dashboard/packages/local-runtime/dist/server.cjs +229 -222
  106. package/templates/.agents/dashboard/packages/local-runtime/dist/sqlite-read-model.cjs +167 -167
  107. package/templates/.agents/dashboard/packages/local-runtime/package.json +3 -3
  108. package/templates/.agents/dashboard/packages/local-runtime/server.mjs +229 -222
  109. package/templates/.agents/dashboard/packages/local-runtime/src/active-capture-failure.test.ts +38 -0
  110. package/templates/.agents/dashboard/packages/local-runtime/src/active-capture-failure.ts +35 -0
  111. package/templates/.agents/dashboard/packages/local-runtime/src/active-codex-telemetry.test.ts +69 -6
  112. package/templates/.agents/dashboard/packages/local-runtime/src/active-codex-telemetry.ts +62 -15
  113. package/templates/.agents/dashboard/packages/local-runtime/src/manifest-runs.ts +8 -2
  114. package/templates/.agents/dashboard/packages/local-runtime/src/server-capture-projection-order.test.ts +118 -0
  115. package/templates/.agents/dashboard/packages/local-runtime/src/server-cost-run-search.test.ts +90 -0
  116. package/templates/.agents/dashboard/packages/local-runtime/src/server-cost-run-search.ts +86 -0
  117. package/templates/.agents/dashboard/packages/local-runtime/src/server-operations-routes.ts +8 -1
  118. package/templates/.agents/dashboard/packages/local-runtime/src/server-projection-loaders.ts +13 -0
  119. package/templates/.agents/dashboard/packages/local-runtime/src/server-projections.ts +30 -74
  120. package/templates/.agents/dashboard/packages/local-runtime/src/server-route-handlers.ts +42 -0
  121. package/templates/.agents/dashboard/packages/local-runtime/src/server-routes.test.ts +60 -0
  122. package/templates/.agents/dashboard/packages/local-runtime/src/server-run-projections.test.ts +12 -2
  123. package/templates/.agents/dashboard/packages/local-runtime/src/server-telemetry-command.test.ts +29 -4
  124. package/templates/.agents/dashboard/packages/local-runtime/src/server-telemetry-refresh.ts +7 -0
  125. package/templates/.agents/dashboard/packages/local-runtime/src/server.test.ts +51 -54
  126. package/templates/.agents/dashboard/packages/local-runtime/src/server.ts +37 -48
  127. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-filter-matrix.test.ts +5 -0
  128. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-invocation-page.test.ts +49 -0
  129. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-invocation-page.ts +44 -1
  130. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-query-cached-cost-page.ts +11 -10
  131. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-query-summary.ts +1 -1
  132. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-work-item-index.test.ts +18 -1
  133. package/templates/.agents/dashboard/packages/local-runtime/vitest.config.ts +12 -0
  134. package/templates/.agents/dashboard/packages/ui/package.json +1 -1
  135. package/templates/.agents/dashboard/tests/e2e/package.json +1 -1
  136. package/templates/.agents/dashboard/tests/e2e/specs/cost-monitor-pagination.spec.ts +13 -6
  137. package/templates/.agents/dashboard/tests/e2e/specs/dashboard.spec.ts +1 -1
  138. package/templates/.agents/design/.zaip-managed.json +3 -3
  139. package/templates/.agents/design/apps/catalog/package.json +1 -1
  140. package/templates/.agents/design/packages/local-runtime/package.json +1 -1
  141. package/templates/.agents/requirements/autonomous-assurance.md +39 -3
  142. package/templates/.agents/requirements/cloud-hostinger-autonomy.md +1 -1
  143. package/templates/.agents/requirements/comparative-audit.md +1 -1
  144. package/templates/.agents/requirements/context-engine.md +3 -3
  145. package/templates/.agents/requirements/cost-observability.md +1 -1
  146. package/templates/.agents/requirements/dashboard-launch.md +1 -1
  147. package/templates/.agents/requirements/data-export.md +1 -1
  148. package/templates/.agents/requirements/design-system.md +1 -1
  149. package/templates/.agents/requirements/execution-engine.md +11 -2
  150. package/templates/.agents/requirements/kanban-dashboard.md +1 -1
  151. package/templates/.agents/requirements/model-orchestration.md +1 -1
  152. package/templates/.agents/requirements/public-distribution.md +1 -1
  153. package/templates/.agents/requirements/software-architecture.md +1 -1
  154. package/templates/.agents/requirements/strategic-improvements.md +1 -1
  155. package/templates/.agents/requirements/work-lifecycle.md +2 -1
  156. package/templates/.agents/requirements/zaip-strategic-95.md +1 -1
  157. package/templates/.agents/rules/cloud-hostinger-autonomy.md +1 -1
  158. package/templates/.agents/rules/production-readiness.md +24 -0
  159. package/templates/.agents/rules/run-lifecycle-closure.md +2 -1
  160. package/templates/.agents/rules/security.md +2 -2
  161. package/templates/.agents/rules/workflow.md +4 -3
  162. package/templates/.agents/runtime/VERSION +1 -1
  163. package/templates/.agents/runtime/bin/zaip.mjs +1185 -610
  164. package/templates/.agents/runtime/checksums.sha256 +6 -6
  165. package/templates/.agents/runtime/manifest.json +1 -1
  166. package/templates/.agents/runtime/pnpm-lock.sha256 +1 -1
  167. package/templates/.agents/runtime/provenance.json +5 -5
  168. package/templates/.agents/runtime/sbom.cdx.json +71 -71
  169. package/templates/.agents/skills/zaip-skill-execution/SKILL.md +4 -2
  170. package/templates/.agents/skills/zaip-skill-execution/references/controlled-release-recovery.md +44 -0
  171. package/templates/.agents/skills/zaip-skill-execution/references/runtime-governance.md +1 -1
  172. package/templates/.agents/skills/zaip-skill-github-deploy/SKILL.md +7 -1
  173. package/templates/.agents/skills/zaip-skill-github-deploy/references/release-gate-matrix.md +15 -20
  174. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/deploy-contract.mjs +38 -2
  175. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/deploy-contract.test.mjs +62 -4
  176. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/github-deploy-contract.test.mjs +8 -6
  177. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/guarded-commits.mjs +5 -3
  178. package/templates/.agents/skills/zaip-skill-lifecycle-closure/SKILL.md +7 -0
  179. package/templates/.agents/skills/zaip-skill-maintenance-enter/SKILL.md +23 -0
  180. package/templates/.agents/skills/zaip-skill-maintenance-enter/agents/openai.yaml +4 -0
  181. package/templates/.agents/skills/zaip-skill-maintenance-enter/scripts/maintenance.mjs +213 -0
  182. package/templates/.agents/skills/zaip-skill-maintenance-enter/scripts/maintenance.test.mjs +207 -0
  183. package/templates/.agents/skills/zaip-skill-maintenance-exit/SKILL.md +21 -0
  184. package/templates/.agents/skills/zaip-skill-maintenance-exit/agents/openai.yaml +4 -0
  185. package/templates/.agents/skills/zaip-skill-prd/SKILL.md +6 -0
  186. package/templates/.agents/skills/zaip-skill-release-qualification/SKILL.md +26 -0
  187. package/templates/.agents/skills/zaip-skill-release-qualification/references/local-preparation.md +46 -11
  188. package/templates/.agents/skills/zaip-skill-spec/SKILL.md +9 -0
  189. package/templates/.agents/skills/zaip-skill-testing-strategy/SKILL.md +15 -0
  190. package/templates/.agents/zaip-aios/VERSION +1 -1
  191. package/templates/.agents/zaip-aios/guides/11-observabilidade-e-custos.md +22 -0
  192. package/templates/.agents/zaip-aios/guides/14-troubleshooting.md +1 -1
  193. package/templates/.agents/zaip-aios/guides/26-qualidade-e-release.md +52 -5
  194. package/templates/template-manifest.json +210 -126
  195. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-Co2Jiuzh.js +0 -68
@@ -1,11 +1,8 @@
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 { link, lstat, mkdir, readFile, realpath, unlink, writeFile } from "node:fs/promises";
1
+ export const CODEX_PRE_TOOL_SUPPORT_HOOK = String.raw `import { lstat, readFile, realpath } from "node:fs/promises";
4
2
  import { dirname, isAbsolute, relative, resolve, toNamespacedPath } from "node:path";
5
3
  import { homedir } from "node:os";
6
- import { promisify } from "node:util";
7
-
8
- const execute = promisify(execFile);
4
+ import { createHostContext } from "./zaip-pre-tool-host-context.mjs";
5
+ import { createCliTurnCapture } from "./zaip-pre-tool-cli-capture.mjs";
9
6
  export { readAuthorityRunRecords, checkDestructivePatterns, configuredDeploymentBranches } from "./zaip-native-tool-authority.mjs";
10
7
  const pathIdentity = value => process.platform === "win32" ? toNamespacedPath(resolve(value)).toLowerCase() : resolve(value);
11
8
  export const inside = (path, root) => {
@@ -13,51 +10,27 @@ export const inside = (path, root) => {
13
10
  return scoped === "" || (!scoped.startsWith("..") && !isAbsolute(scoped));
14
11
  };
15
12
 
16
- export async function selectHookRepositoryRoot(installedRoot, cwd) {
17
- if (typeof cwd !== "string") throw new Error("Tool execution without a workspace is blocked.");
18
- const physicalCwd = await realpath(cwd).catch(() => null);
19
- if (!physicalCwd || pathIdentity(physicalCwd) !== pathIdentity(cwd)) {
20
- throw new Error("Tool workspace could not be safely resolved.");
21
- }
22
- if (resolve(cwd).toLowerCase() === installedRoot.toLowerCase()) return installedRoot;
23
- if (inside(cwd, installedRoot)) {
24
- let cursor = resolve(cwd);
25
- while (cursor.toLowerCase() !== installedRoot.toLowerCase() &&
26
- !(await lstat(resolve(cursor, ".git")).catch(() => null))) cursor = dirname(cursor);
27
- if (cursor.toLowerCase() === installedRoot.toLowerCase()) return installedRoot;
28
- }
29
- try {
30
- const git = async (path, ...args) => (await execute("git", ["-C", path, ...args], {
31
- encoding: "utf8", windowsHide: true, timeout: 5000, maxBuffer: 1048576,
32
- })).stdout.trim();
33
- const primary = await git(installedRoot, "rev-parse", "--show-toplevel");
34
- if ((await realpath(primary)).toLowerCase() !== installedRoot.toLowerCase()) throw new Error("primary_not_root");
35
- const worktrees = await git(installedRoot, "worktree", "list", "--porcelain", "-z");
36
- const registered = worktrees.split("\0").filter(line => line.startsWith("worktree ")).map(line => line.slice(9));
37
- const candidate = await realpath(await git(cwd, "rev-parse", "--show-toplevel"));
38
- if (!registered.some(path => resolve(path).toLowerCase() === candidate.toLowerCase()) || !inside(cwd, candidate)) {
39
- throw new Error("worktree_not_registered");
40
- }
41
- const common = async path => (await realpath(await git(path, "rev-parse", "--path-format=absolute", "--git-common-dir"))).toLowerCase();
42
- if (await common(installedRoot) !== await common(candidate)) throw new Error("common_git_dir_mismatch");
43
- return candidate;
44
- } catch {
45
- throw new Error("Tool workspace could not be verified as a registered worktree.");
46
- }
13
+ export async function readBoundedCanonicalText(path, maximumBytes, authorityRoot) {
14
+ const info = await lstat(path);
15
+ if (!info.isFile() || info.isSymbolicLink() || info.size > maximumBytes) throw new Error("Unsafe Run lookup file");
16
+ const physical = await realpath(path);
17
+ const identity = value => process.platform === "win32" ? value.toLowerCase() : value;
18
+ if (!inside(physical, authorityRoot) || identity(physical) !== identity(resolve(path))) throw new Error("Run lookup must not follow links");
19
+ return readFile(path, "utf8");
47
20
  }
48
21
 
49
- export async function selectHookExecutionWorkspace(installedRoot, event) {
50
- // Codex normalizes shell tools to Bash; preserve the host-supplied literal workdir.
51
- await selectHookRepositoryRoot(installedRoot, event.cwd);
52
- const workdir = ["exec_command", "shell_command", "Bash"].includes(event.tool_name) ? event.tool_input?.workdir : undefined;
53
- if (workdir !== undefined && (typeof workdir !== "string" || !isAbsolute(workdir)))
54
- throw new Error("Tool workdir must be an absolute literal workspace.");
55
- const cwd = workdir ?? event.cwd;
56
- return { cwd, repositoryRoot: await selectHookRepositoryRoot(installedRoot, cwd) };
22
+ export function parseHookEvent(raw, deny) {
23
+ try { return JSON.parse(raw); }
24
+ catch { deny("Malformed hook input was rejected."); }
57
25
  }
58
26
 
27
+ export const { configuredPermissionProfile, selectHookRepositoryRoot, selectHookExecutionWorkspace } = createHostContext({ inside, readBoundedCanonicalText });
28
+
59
29
  export function tokenizeLiteralCommand(command) {
60
30
  if (command.includes("\r") || command.includes("\n")) return null;
31
+ // PowerShell's call operator invokes one literal executable; it grants no authority.
32
+ // All interpolation, composition and subsequent operators still fail below.
33
+ if (process.platform === "win32") command = command.replace(/^\s*&\s+/, "");
61
34
  const tokens = [];
62
35
  let token = "", quote = null, started = false;
63
36
  for (let index = 0; index < command.length; index++) {
@@ -83,7 +56,7 @@ export function tokenizeLiteralCommand(command) {
83
56
  }
84
57
  if (quote !== null) return null;
85
58
  if (started) tokens.push(token);
86
- return tokens;
59
+ return tokens[0] === "" ? null : tokens;
87
60
  }
88
61
 
89
62
  export function shellSyntax(command) {
@@ -111,9 +84,12 @@ export function requiresShellMutationRun(command, tokens, cwd, repositoryRoot) {
111
84
  const syntax = shellSyntax(command);
112
85
  if (!syntax.interpolation && !syntax.redirection && !syntax.grouping && isLiteralReadSequence(command)) return false;
113
86
  const executable = tokens?.[0] ?? "";
114
- const canonicalCli = /^(?:node|node\.exe)$/i.test(executable) && tokens?.[1] &&
87
+ const canonicalCli = /^(?:node|node\.exe)$/i.test(executable) || /[\\/]node(?:\.exe)?$/i.test(executable);
88
+ const canonicalRuntime = canonicalCli && tokens?.[1] &&
115
89
  resolve(cwd, tokens[1]).toLowerCase() === resolve(repositoryRoot, ".agents/runtime/bin/zaip.mjs").toLowerCase();
116
- const dataCommand = canonicalCli || /^(?:Write-Output|echo|printf)$/i.test(executable);
90
+ if (canonicalRuntime && tokens?.[2] === "execution" && tokens?.[3] === "native-next" &&
91
+ tokens.includes("--reserve-causal")) return true;
92
+ const dataCommand = canonicalRuntime || /^(?:Write-Output|echo|printf)$/i.test(executable);
117
93
  if (tokens && !syntax.interpolation && !syntax.grouping && dataCommand) {
118
94
  return syntax.redirection;
119
95
  }
@@ -124,6 +100,8 @@ export function requiresShellMutationRun(command, tokens, cwd, repositoryRoot) {
124
100
  // Recognize only literal read-only pipelines/sequences. Quoted search text is data,
125
101
  // not a PowerShell alias (for example the AC prefix in acceptance-criterion IDs).
126
102
  export function isLiteralReadSequence(command) {
103
+ const syntax = shellSyntax(command);
104
+ if (syntax.interpolation || syntax.redirection || syntax.grouping) return false;
127
105
  const parts = [];
128
106
  let start = 0, quote = null;
129
107
  for (let index = 0; index < command.length; index++) {
@@ -141,6 +119,20 @@ export function isLiteralReadSequence(command) {
141
119
  return parts.every(part => {
142
120
  const values = tokenizeLiteralCommand(part);
143
121
  if (!values?.length) return false;
122
+ if (/^Get-Item$/i.test(values[0])) {
123
+ const args = values.slice(1);
124
+ const path = args[0] === "-LiteralPath" ? args[1] : args[0];
125
+ return args.length === (args[0] === "-LiteralPath" ? 2 : 1) &&
126
+ !!path && !/^(?:[a-z][\w-]*:)(?![\/])/i.test(path) && !/[*?]/.test(path);
127
+ }
128
+ if (/^Select-Object$/i.test(values[0])) {
129
+ const args = values.slice(1);
130
+ const metadata = /^(?:Name|FullName|Length|LastWriteTime|LastWriteTimeUtc|CreationTime|Attributes|IsReadOnly|LinkType|Target)(?:,(?:Name|FullName|Length|LastWriteTime|LastWriteTimeUtc|CreationTime|Attributes|IsReadOnly|LinkType|Target))*$/i;
131
+ return (args.length === 2 && args[0] === "-First" && /^\d+$/.test(args[1])) ||
132
+ (args.length === 2 && args[0] === "-ExpandProperty" && args[1] === "Target") ||
133
+ (args.length === 2 && args[0] === "-Property" && metadata.test(args[1])) ||
134
+ (args.length === 1 && metadata.test(args[0]));
135
+ }
144
136
  if (/^(?:Get-Content|Measure-Object)$/i.test(values[0])) return true;
145
137
  if (!/^(?:rg|rg\.exe)$/i.test(values[0])) return false;
146
138
  return !values.slice(1).some(value => /^--(?:pre|hostname-bin)(?:=|$)/i.test(value));
@@ -294,7 +286,7 @@ export async function reviewedReleasePaths(command, cwd, repositoryRoot, credent
294
286
  }
295
287
 
296
288
  /** Narrow read capability, never a general external-path or executable allowlist. */
297
- export async function reviewedContextReadPaths(command, credentialPattern) {
289
+ export async function reviewedContextReadPaths(command, credentialPattern, repositoryRoot) {
298
290
  const tokens = tokenizeLiteralCommand(command);
299
291
  if (!tokens || tokens.length < 2 || credentialPattern.test(command)) return [];
300
292
  const executable = tokens[0].toLowerCase();
@@ -305,10 +297,28 @@ export async function reviewedContextReadPaths(command, credentialPattern) {
305
297
  // -f means a pattern file in ripgrep, not fixed strings: do not permit it.
306
298
  if (tokens.slice(1).some(token => token.startsWith("-") && (!flags.has(token.toLowerCase()) || token === "-f"))) return [];
307
299
  const roots = ["memories", "skills", "plugins/cache"].map(part => resolve(homedir(), ".codex", part));
300
+ // Project-owned policy records explicitly authorized files, never executable
301
+ // commands or a blanket permission for the user's home/attachments directory.
302
+ let authorizedFiles = [];
303
+ if (repositoryRoot) {
304
+ const policyPath = resolve(repositoryRoot, ".agents/config/external-read-policy.json");
305
+ try {
306
+ const info = await lstat(policyPath);
307
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 65536 ||
308
+ pathIdentity(await realpath(policyPath)) !== pathIdentity(policyPath)) return [];
309
+ const policy = JSON.parse(await readFile(policyPath, "utf8"));
310
+ if (policy?.schema_version !== 1 || !Array.isArray(policy.files) || policy.files.length > 128 ||
311
+ Object.keys(policy).sort().join(",") !== "files,schema_version" ||
312
+ policy.files.some(path => typeof path !== "string" || !isAbsolute(path) || hasTraversalSegment(path))) return [];
313
+ authorizedFiles = policy.files.map(pathIdentity);
314
+ } catch (error) { if (error.code !== "ENOENT") return []; }
315
+ }
308
316
  const paths = tokens.slice(1).filter(value => isAbsolute(value));
309
317
  if (!paths.length) return [];
310
318
  for (const path of paths) {
311
- if (!roots.some(root => inside(path, root)) || !/\.(?:md|txt|json|yaml|yml|toml)$/i.test(path)) return [];
319
+ if (hasTraversalSegment(path) ||
320
+ (!roots.some(root => inside(path, root)) && !authorizedFiles.includes(pathIdentity(path))) ||
321
+ !/\.(?:md|txt|json|jsonl|yaml|yml|toml)$/i.test(path)) return [];
312
322
  try {
313
323
  const info = await lstat(path);
314
324
  if (!info.isFile() || info.isSymbolicLink() || pathIdentity(await realpath(path)) !== pathIdentity(path)) return [];
@@ -317,62 +327,26 @@ export async function reviewedContextReadPaths(command, credentialPattern) {
317
327
  return paths;
318
328
  }
319
329
 
320
- export async function captureCliTurn(repositoryRoot, event, command) {
321
- const tokens = tokenizeLiteralCommand(command);
322
- if (!tokens || tokens.length < 4 || !/^(?:node|node\.exe)$/i.test(tokens[0])) return;
323
- const script = resolve(event.cwd, tokens[1]);
324
- if (script.toLowerCase() !== resolve(repositoryRoot, ".agents/runtime/bin/zaip.mjs").toLowerCase()) return;
325
- if (!((tokens[2] === "conversation" || tokens[2] === "run") && tokens[3] === "start")) return;
326
- if (tokens.includes("--help") || tokens.includes("-h")) return;
327
- if (tokens.some(token => token.startsWith("--codex-hook-"))) throw new Error("Codex hook identity cannot be supplied by the caller.");
328
- const threadId = event.session_id, turnId = event.turn_id, toolUseId = event.tool_use_id;
329
- if (typeof threadId !== "string" || !/^[A-Za-z0-9_-]{1,256}$/.test(threadId) ||
330
- typeof turnId !== "string" || !turnId || turnId.length > 256 ||
331
- typeof toolUseId !== "string" || !toolUseId || toolUseId.length > 256 ||
332
- (process.env.CODEX_THREAD_ID && process.env.CODEX_THREAD_ID !== threadId) ||
333
- (process.env.CODEX_TURN_ID && process.env.CODEX_TURN_ID !== turnId)) {
334
- throw new Error("Codex thread, turn and tool identity could not be captured for the ZAIP CLI.");
335
- }
336
- const directory = resolve(repositoryRoot, ".local/codex-hook-turn/v1");
337
- const argvSha256 = createHash("sha256").update(JSON.stringify(tokens.slice(2))).digest("hex");
338
- const digest = createHash("sha256").update(JSON.stringify([threadId, toolUseId])).digest("hex");
339
- const target = resolve(directory, digest + ".json");
340
- const temporary = resolve(directory, digest + "." + randomUUID() + ".tmp");
341
- try {
342
- let parent = repositoryRoot;
343
- for (const part of [".local", "codex-hook-turn", "v1"]) {
344
- parent = resolve(parent, part);
345
- try { await mkdir(parent); } catch (error) { if (error?.code !== "EEXIST") throw error; }
346
- const info = await lstat(parent), physical = await realpath(parent);
347
- if (!info.isDirectory() || info.isSymbolicLink() || !inside(physical, repositoryRoot)) throw new Error("Unsafe turn receipt directory");
348
- }
349
- const value = {
350
- schema_version: 1, thread_id: threadId, turn_id: turnId,
351
- tool_use_sha256: digest, argv_sha256: argvSha256, observed_at_ms: Date.now(),
352
- };
353
- await writeFile(temporary, JSON.stringify(value) + "\n", { flag: "wx", mode: 0o600 });
354
- try { await link(temporary, target); }
355
- catch (error) {
356
- if (error.code !== "EEXIST") throw error;
357
- const info = await lstat(target);
358
- if (!info.isFile() || info.isSymbolicLink() || info.size > 4096 ||
359
- pathIdentity(await realpath(target)) !== pathIdentity(target)) throw new Error("Unsafe existing turn receipt");
360
- const prior = JSON.parse(await readFile(target, "utf8"));
361
- for (const key of ["schema_version", "thread_id", "turn_id", "tool_use_sha256", "argv_sha256"])
362
- if (prior[key] !== value[key]) throw new Error("Conflicting existing turn receipt");
363
- if (!Number.isSafeInteger(prior.observed_at_ms) || prior.observed_at_ms > Date.now()) throw new Error("Invalid turn receipt time");
364
- }
365
- } catch {
366
- throw new Error("Codex turn identity could not be persisted for the ZAIP CLI.");
367
- } finally { await unlink(temporary).catch(() => {}); }
368
- return command + " --codex-hook-id=" + digest + " --codex-hook-thread=" + threadId;
330
+ function hasTraversalSegment(path) {
331
+ return path.split(/[\\/]/).includes("..");
369
332
  }
370
333
 
334
+ export const captureCliTurn = createCliTurnCapture({ inside, tokenizeLiteralCommand });
335
+
371
336
  export function validateInlineExecution(command, deny) {
372
337
  if (shellSyntax(command).interpolation) {
373
338
  deny("Nested shell execution is blocked; use a reviewed repository script.");
374
339
  }
375
340
  if (/(?:^|[;&|\r\n])\s*&\s*\(/.test(command)) deny("Dynamic shell invocation is blocked; use a literal command.");
341
+ const tokens = tokenizeLiteralCommand(command);
342
+ const executable = tokens?.[0]?.split(/[\\/]/).at(-1) ?? "";
343
+ // Quoting and absolute paths do not change Node's inline-execution semantics.
344
+ // Stop at --, which makes subsequent arguments positional rather than options.
345
+ const argumentsBeforeDelimiter = tokens?.slice(1, tokens.includes("--") ? tokens.indexOf("--") : undefined) ?? [];
346
+ if (/^node(?:\.exe)?$/i.test(executable) &&
347
+ argumentsBeforeDelimiter.some(argument => /^(?:--(?:eval|print)(?:=|$)|-[ep](?:[ep]|$))/.test(argument))) {
348
+ deny("Inline or piped interpreter execution is blocked; use a reviewed repository script.");
349
+ }
376
350
  const inlinePatterns = [
377
351
  /\b(?:node|deno)\s+(?:-e|--eval)\b/i,
378
352
  /\b(?:python(?:3)?|ruby|perl|php)\s+(?:-c|-e)\b/i,
@@ -383,7 +357,10 @@ export function validateInlineExecution(command, deny) {
383
357
  /\|\s*(?:node|deno|python(?:3)?|ruby|perl|php|powershell|pwsh|cmd)\b/i,
384
358
  /\b(?:Invoke-Expression|iex)\b/i,
385
359
  ];
386
- if (inlinePatterns.some((pattern) => pattern.test(command))) {
360
+ // A quoted rg expression can contain a literal pipe followed by "node".
361
+ // Only the parsed read command may exempt that data; a real pipe to an
362
+ // interpreter remains outside isLiteralReadSequence and is denied.
363
+ if (!isLiteralReadSequence(command) && inlinePatterns.some((pattern) => pattern.test(command))) {
387
364
  deny("Inline or piped interpreter execution is blocked; use a reviewed repository script.");
388
365
  }
389
366
  }
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4YpD,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqXpD,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 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";
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 if (!(await activation?.hookPolicies?.(repositoryRoot))?.governance) 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 policy = await activation?.hookPolicies?.(repositoryRoot);\n if (policy?.tracking) activation?.replayHookObservations?.(repositoryRoot);\n if (!policy?.governance) { process.stdout.write(JSON.stringify({ attempted: 0, deferred: 0, pending: 0, disabled: true })); process.exit(0); }\n const files = (await fs.readdir(pendingRoot).catch(() => []))\n .filter((name) => /^[a-f0-9]{64}\\.json$/.test(name)).sort();\n const cursorPath = resolve(pendingRoot, \".replay-cursor\");\n const cursor = await fs.readFile(cursorPath, \"utf8\").then(JSON.parse).catch(() => null);\n const start = (files.indexOf(cursor?.last) + 1) % (files.length || 1);\n const selected = [...files.slice(start), ...files.slice(0, start)].slice(0, 256);\n let attempted = 0, deferred = 0, pending = 0;\n let lastAttempted = null;\n for (const file of selected) {\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)) continue;\n if (attempted < 3) {\n attempted++; lastAttempted = file;\n if (await replayPendingReceipt(path) === \"deferred\") deferred++;\n }\n if ((await fs.readFile(path, \"utf8\").then(JSON.parse).catch(() => null))?.state === \"pending\") pending++;\n }\n if (selected.length) await atomicSave(cursorPath, { last: lastAttempted ?? selected.at(-1) });\n process.stdout.write(JSON.stringify({ attempted, deferred, pending, uninspected: files.length - selected.length }));\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 policy = await activation?.hookPolicies?.(repositoryRoot) ?? { governance: false, tracking: false, diagnostic: \"policy_module_unavailable\" };\n activation?.recordPolicyDiagnostic?.(repositoryRoot, policy);\n if (policy.tracking) activation?.recordHookObservation?.(repositoryRoot, event, policy.governance);\n if (policy.tracking && event.hook_event_name === \"SubagentStop\") activation?.replayHookObservations?.(repositoryRoot);\n if (!policy.governance) { process.stdout.write(JSON.stringify({})); 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 const delays = [25, 50, 75, 100, 150, 225, 300, 400, 500];\n try {\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); return; }\n catch (error) {\n if (![\"EPERM\", \"EACCES\", \"EBUSY\"].includes(error.code) || retry >= delays.length) throw error;\n await new Promise(done => setTimeout(done, delays[retry]));\n }\n }\n } finally {\n await fs.unlink(temporary).catch(() => {});\n }\n}\n\nasync function replayPendingReceipt(path) {\n const deadline = Date.now() + 2000;\n for (;;) {\n if (await runCaptureRunner(path, true) !== \"lease_busy\") return \"attempted\";\n if (Date.now() >= deadline) {\n await persistDiagnostic(\"replay_deferred_active_lease\", { receipt: path });\n return \"deferred\";\n }\n await new Promise(done => setTimeout(done, 50));\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 \"lease_busy\";\n try { process.kill(owner.pid, 0); return \"lease_busy\"; } catch (failure) { if (failure.code !== \"ESRCH\") return \"lease_busy\"; }\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 \"lease_busy\"; }\n try {\n const current = await fs.readFile(lockPath, \"utf8\").then(JSON.parse).catch(() => null);\n if (current?.pid !== owner.pid) return \"lease_busy\";\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 \"lease_busy\";\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 policy = await activation?.hookPolicies?.(repositoryRoot);\n const child = spawn(process.execPath, [cli, ...args], { cwd: repositoryRoot, windowsHide: true,\n env: { ...process.env, ZAIP_HOOK_TRACKING_ENABLED: policy?.tracking ? \"true\" : \"false\" }, 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,80fAqVzC,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,i1kBAiYzC,CAAC"}
@@ -52,18 +52,35 @@ function selectRoot(cwd) {
52
52
 
53
53
  if (process.argv[2] === "--capture-runner") {
54
54
  if (!selectRoot(process.cwd())) process.exit(0);
55
+ if (!(await activation?.hookPolicies?.(repositoryRoot))?.governance) process.exit(0);
55
56
  await runCaptureRunner(process.argv[3]);
56
57
  process.exit(0);
57
58
  }
58
59
  if (process.argv[2] === "--replay-pending") {
59
60
  if (!selectRoot(process.cwd())) process.exit(0);
60
- const files = await fs.readdir(pendingRoot).catch(() => []);
61
- let replayed = 0;
62
- for (const file of files.filter((name) => /^[a-f0-9]{64}\\.json$/.test(name)).slice(0, 256)) {
61
+ const policy = await activation?.hookPolicies?.(repositoryRoot);
62
+ if (policy?.tracking) activation?.replayHookObservations?.(repositoryRoot);
63
+ if (!policy?.governance) { process.stdout.write(JSON.stringify({ attempted: 0, deferred: 0, pending: 0, disabled: true })); process.exit(0); }
64
+ const files = (await fs.readdir(pendingRoot).catch(() => []))
65
+ .filter((name) => /^[a-f0-9]{64}\\.json$/.test(name)).sort();
66
+ const cursorPath = resolve(pendingRoot, ".replay-cursor");
67
+ const cursor = await fs.readFile(cursorPath, "utf8").then(JSON.parse).catch(() => null);
68
+ const start = (files.indexOf(cursor?.last) + 1) % (files.length || 1);
69
+ const selected = [...files.slice(start), ...files.slice(0, start)].slice(0, 256);
70
+ let attempted = 0, deferred = 0, pending = 0;
71
+ let lastAttempted = null;
72
+ for (const file of selected) {
63
73
  const path = resolve(pendingRoot, file);
64
74
  const receipt = await fs.readFile(path, "utf8").then(JSON.parse).catch(() => null);
65
- if (receipt?.state === "pending" && Array.isArray(receipt.capture_arguments)) { await runCaptureRunner(path, true); if (++replayed >= 3) break; }
75
+ if (receipt?.state !== "pending" || !Array.isArray(receipt.capture_arguments)) continue;
76
+ if (attempted < 3) {
77
+ attempted++; lastAttempted = file;
78
+ if (await replayPendingReceipt(path) === "deferred") deferred++;
79
+ }
80
+ if ((await fs.readFile(path, "utf8").then(JSON.parse).catch(() => null))?.state === "pending") pending++;
66
81
  }
82
+ if (selected.length) await atomicSave(cursorPath, { last: lastAttempted ?? selected.at(-1) });
83
+ process.stdout.write(JSON.stringify({ attempted, deferred, pending, uninspected: files.length - selected.length }));
67
84
  process.exit(0);
68
85
  }
69
86
 
@@ -74,6 +91,11 @@ try {
74
91
  const event = JSON.parse(raw);
75
92
  const cwd = text(event.cwd);
76
93
  if (cwd === null || !selectRoot(cwd)) process.exit(0);
94
+ const policy = await activation?.hookPolicies?.(repositoryRoot) ?? { governance: false, tracking: false, diagnostic: "policy_module_unavailable" };
95
+ activation?.recordPolicyDiagnostic?.(repositoryRoot, policy);
96
+ if (policy.tracking) activation?.recordHookObservation?.(repositoryRoot, event, policy.governance);
97
+ if (policy.tracking && event.hook_event_name === "SubagentStop") activation?.replayHookObservations?.(repositoryRoot);
98
+ if (!policy.governance) { process.stdout.write(JSON.stringify({})); process.exit(0); }
77
99
  const eventName = text(event.hook_event_name);
78
100
  activation?.recordActivation(repositoryRoot, event);
79
101
  const lifecycleEvent =
@@ -202,10 +224,30 @@ try {
202
224
 
203
225
  async function atomicSave(path, value) {
204
226
  const temporary = path + ".tmp-" + randomUUID();
205
- await fs.writeFile(temporary, JSON.stringify(value), { flag: "wx", mode: 0o600 });
206
- for (let retry = 0; ; retry++) {
207
- try { await fs.rename(temporary, path); break; }
208
- catch (error) { if (!["EPERM", "EACCES", "EBUSY"].includes(error.code) || retry >= 5) throw error; await new Promise(done => setTimeout(done, 20 * (retry + 1))); }
227
+ const delays = [25, 50, 75, 100, 150, 225, 300, 400, 500];
228
+ try {
229
+ await fs.writeFile(temporary, JSON.stringify(value), { flag: "wx", mode: 0o600 });
230
+ for (let retry = 0; ; retry++) {
231
+ try { await fs.rename(temporary, path); return; }
232
+ catch (error) {
233
+ if (!["EPERM", "EACCES", "EBUSY"].includes(error.code) || retry >= delays.length) throw error;
234
+ await new Promise(done => setTimeout(done, delays[retry]));
235
+ }
236
+ }
237
+ } finally {
238
+ await fs.unlink(temporary).catch(() => {});
239
+ }
240
+ }
241
+
242
+ async function replayPendingReceipt(path) {
243
+ const deadline = Date.now() + 2000;
244
+ for (;;) {
245
+ if (await runCaptureRunner(path, true) !== "lease_busy") return "attempted";
246
+ if (Date.now() >= deadline) {
247
+ await persistDiagnostic("replay_deferred_active_lease", { receipt: path });
248
+ return "deferred";
249
+ }
250
+ await new Promise(done => setTimeout(done, 50));
209
251
  }
210
252
  }
211
253
 
@@ -219,19 +261,19 @@ async function runCaptureRunner(receiptPath, replay = false) {
219
261
  } catch (error) {
220
262
  if (error.code !== "EEXIST") return;
221
263
  const owner = await fs.readFile(lockPath, "utf8").then(JSON.parse).catch(() => null);
222
- if (!Number.isSafeInteger(owner?.pid) || owner.pid <= 0) return;
223
- try { process.kill(owner.pid, 0); return; } catch (failure) { if (failure.code !== "ESRCH") return; }
264
+ if (!Number.isSafeInteger(owner?.pid) || owner.pid <= 0) return "lease_busy";
265
+ try { process.kill(owner.pid, 0); return "lease_busy"; } catch (failure) { if (failure.code !== "ESRCH") return "lease_busy"; }
224
266
  // Serialize stale lock recovery so two recovering workers cannot remove a new lease.
225
267
  let recovery;
226
- try { recovery = await fs.open(lockPath + ".recover", "wx"); } catch { return; }
268
+ try { recovery = await fs.open(lockPath + ".recover", "wx"); } catch { return "lease_busy"; }
227
269
  try {
228
270
  const current = await fs.readFile(lockPath, "utf8").then(JSON.parse).catch(() => null);
229
- if (current?.pid !== owner.pid) return;
271
+ if (current?.pid !== owner.pid) return "lease_busy";
230
272
  await fs.unlink(lockPath);
231
273
  lease = await fs.open(lockPath, "wx");
232
274
  await lease.writeFile(JSON.stringify({ pid: process.pid }));
233
275
  } finally { await recovery.close(); await fs.unlink(lockPath + ".recover").catch(() => {}); }
234
- if (!lease) return;
276
+ if (!lease) return "lease_busy";
235
277
  }
236
278
  try {
237
279
  const receipt = await fs.readFile(receiptPath, "utf8").then(JSON.parse);
@@ -248,7 +290,7 @@ async function runCaptureRunner(receiptPath, replay = false) {
248
290
  receipt.attempts = (receipt.attempts ?? 0) + 1;
249
291
  receipt.updated_at = new Date().toISOString();
250
292
  await atomicSave(receiptPath, receipt);
251
- const outcome = await capture(cli, args);
293
+ const outcome = await capture(cli, args);
252
294
  let result;
253
295
  try { result = JSON.parse(outcome.stdout); } catch { result = null; }
254
296
  const runId = result?.run?.run_id ?? result?.run?.id;
@@ -275,7 +317,9 @@ async function runCaptureRunner(receiptPath, replay = false) {
275
317
  }
276
318
 
277
319
  async function capture(cli, args) {
278
- const child = spawn(process.execPath, [cli, ...args], { cwd: repositoryRoot, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
320
+ const policy = await activation?.hookPolicies?.(repositoryRoot);
321
+ const child = spawn(process.execPath, [cli, ...args], { cwd: repositoryRoot, windowsHide: true,
322
+ env: { ...process.env, ZAIP_HOOK_TRACKING_ENABLED: policy?.tracking ? "true" : "false" }, stdio: ["ignore", "pipe", "pipe"] });
279
323
  let stdout = "", overflow = false;
280
324
  child.stdout.on("data", chunk => { stdout += String(chunk); if (stdout.length > 262144) { overflow = true; stdout = ""; child.kill(); } });
281
325
  child.stderr.resume();
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqV5C,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiY5C,CAAC"}
@@ -1 +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"}
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,QA0C5C,CAAC"}
@@ -1,18 +1,45 @@
1
1
  /** Appended to native authority; receives the shared literal parser. */
2
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) {
3
+ /** A local opt-in may name only the Codex-owned Node under this user's profile. */
4
+ export async function reviewedValidationRuntimePaths(command, tokenizeLiteralCommand, repositoryRoot) {
5
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 [];
6
+ if (!tokens) return [];
7
+ const validation = (/^(?:node|node\.exe)$/i.test(tokens[0]) || isAbsolute(tokens[0])) &&
8
+ /^\.agents[\\/]runtime[\\/]bin[\\/]zaip\.mjs$/.test(tokens[1] ?? "") &&
9
+ tokens[2] === "validation" && ["batch", "execute"].includes(tokens[3]);
10
+ const positions = validation ? tokens.flatMap((value, index) => value === "--executable" ? [index] : []) : [];
11
+ const validationPath = positions.length === 1 ? tokens[positions[0] + 1] : null;
12
+ if (validationPath && isAbsolute(validationPath) && command.split(validationPath).length === 2) {
13
+ try { if (await realpath(validationPath) === await realpath(process.execPath)) return [validationPath]; }
14
+ catch { /* A missing executable has no exemption. */ }
15
+ }
16
+ if (!repositoryRoot || !process.env.USERPROFILE) return [];
17
+ let configured;
13
18
  try {
14
- return await realpath(path) === await realpath(process.execPath) ? [path] : [];
19
+ configured = JSON.parse(await readFile(resolve(repositoryRoot, ".local/validation-runtime.json"), "utf8"));
15
20
  } catch { return []; }
21
+ if (!configured || configured.schema_version !== 1 ||
22
+ Object.keys(configured).sort().join(",") !== "executable,schema_version" ||
23
+ typeof configured.executable !== "string" || !isAbsolute(configured.executable)) return [];
24
+ const expected = resolve(process.env.USERPROFILE, ".cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node.exe");
25
+ if (resolve(configured.executable).toLowerCase() !== expected.toLowerCase()) return [];
26
+ // Windows accepts either separator spelling of the same configured literal.
27
+ // Do not admit aliases, traversal or another executable by realpath alone.
28
+ const configuredLiteral = value => typeof value === "string" &&
29
+ value.replaceAll("/", "\\").toLowerCase() === configured.executable.replaceAll("/", "\\").toLowerCase();
30
+ const directVersion = tokens.length === 2 && tokens[0] === configured.executable && tokens[1] === "--version";
31
+ const directPreflight = tokens.length === 3 && tokens[0] === configured.executable &&
32
+ /^packages[\\/]specs[\\/]qualify-proportional-v1\.mjs$/.test(tokens[1]) &&
33
+ tokens[2] === "preflight";
34
+ const configuredLauncher = tokens[0] === configured.executable;
35
+ const validationExecutable = configuredLiteral(validationPath) &&
36
+ (tokens[3] === "execute"
37
+ ? tokens[0] === validationPath
38
+ : configuredLauncher || /^(?:node|node\.exe)$/i.test(tokens[0])) &&
39
+ command.split(validationPath).length === (tokens[0] === validationPath ? 3 : 2);
40
+ if (!directVersion && !directPreflight && !validationExecutable) return [];
41
+ try { return await realpath(configured.executable) === await realpath(expected) ? [validationExecutable ? validationPath : configured.executable] : []; }
42
+ catch { return []; }
16
43
  }
17
44
  `;
18
45
  //# sourceMappingURL=codex-validation-runtime-template.js.map
@@ -1 +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"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0CzD,CAAC"}
@@ -1,3 +1,4 @@
1
1
  export declare function run(runtime: string, cwd: string, arguments_: readonly string[]): Record<string, unknown>;
2
2
  export declare function snapshotTree(root: string): Promise<Record<string, string>>;
3
+ export declare function testTemplate(roots: string[], name: string, files: readonly (readonly [path: string, content: string])[]): Promise<string>;
3
4
  //# sourceMappingURL=runtime-template-test-support.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-template-test-support.d.ts","sourceRoot":"","sources":["../../src/services/runtime-template-test-support.ts"],"names":[],"mappings":"AAMA,wBAAgB,GAAG,CACjB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAezB;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAchF"}
1
+ {"version":3,"file":"runtime-template-test-support.d.ts","sourceRoot":"","sources":["../../src/services/runtime-template-test-support.ts"],"names":[],"mappings":"AAQA,wBAAgB,GAAG,CACjB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAezB;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAchF;AACD,wBAAsB,YAAY,CAChC,KAAK,EAAE,MAAM,EAAE,EACf,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,EAAE,GAC3D,OAAO,CAAC,MAAM,CAAC,CAwBjB"}
@@ -1,6 +1,8 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { readdir, readFile } from "node:fs/promises";
3
- import { relative, resolve } from "node:path";
2
+ import { mkdir, mkdtemp, readdir, readFile, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join, relative, resolve } from "node:path";
5
+ import { digestRuntimeFiles, sha256 } from "./template-manifest.js";
4
6
  let turnSequence = 0;
5
7
  export function run(runtime, cwd, arguments_) {
6
8
  const result = spawnSync(process.execPath, [runtime, ...arguments_], {
@@ -29,4 +31,23 @@ export async function snapshotTree(root) {
29
31
  (await readFile(path)).toString("base64"),
30
32
  ])));
31
33
  }
34
+ export async function testTemplate(roots, name, files) {
35
+ const root = await mkdtemp(join(tmpdir(), `zaip-${name}-template-`));
36
+ roots.push(root);
37
+ const records = await Promise.all(files.map(async ([path, content]) => {
38
+ await mkdir(dirname(resolve(root, path)), { recursive: true });
39
+ await writeFile(resolve(root, path), content, "utf8");
40
+ return { path, sha256: sha256(content) };
41
+ }));
42
+ await writeFile(resolve(root, "template-manifest.json"), `${JSON.stringify({
43
+ schemaVersion: 1,
44
+ templateVersion: "2.2.0",
45
+ minimumCliVersion: "2.2.0",
46
+ source: "test",
47
+ sourceFilesSha256: sha256("test"),
48
+ runtimeFilesSha256: digestRuntimeFiles([]),
49
+ files: records,
50
+ })}\n`, "utf8");
51
+ return root;
52
+ }
32
53
  //# sourceMappingURL=runtime-template-test-support.js.map