@zaip-ai/cli 4.5.0 → 4.7.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 (244) hide show
  1. package/README.md +2 -0
  2. package/dist/commands/init.js +1 -1
  3. package/dist/commands/init.js.map +1 -1
  4. package/dist/services/adapters.d.ts +1 -1
  5. package/dist/services/adapters.d.ts.map +1 -1
  6. package/dist/services/adapters.js +6 -6
  7. package/dist/services/adapters.js.map +1 -1
  8. package/dist/services/codex-config-overlay.d.ts.map +1 -1
  9. package/dist/services/codex-config-overlay.js +27 -10
  10. package/dist/services/codex-config-overlay.js.map +1 -1
  11. package/dist/services/codex-native-tool-authority-template.d.ts.map +1 -1
  12. package/dist/services/codex-native-tool-authority-template.js +188 -9
  13. package/dist/services/codex-native-tool-authority-template.js.map +1 -1
  14. package/dist/services/codex-pre-tool-hook-template.d.ts.map +1 -1
  15. package/dist/services/codex-pre-tool-hook-template.js +50 -128
  16. package/dist/services/codex-pre-tool-hook-template.js.map +1 -1
  17. package/dist/services/codex-pre-tool-hook-template.test.js +4 -0
  18. package/dist/services/codex-pre-tool-hook-template.test.js.map +1 -1
  19. package/dist/services/codex-pre-tool-support-template.d.ts.map +1 -1
  20. package/dist/services/codex-pre-tool-support-template.js +1 -4
  21. package/dist/services/codex-pre-tool-support-template.js.map +1 -1
  22. package/dist/services/codex-stale-git-lock-template.d.ts +2 -0
  23. package/dist/services/codex-stale-git-lock-template.d.ts.map +1 -0
  24. package/dist/services/codex-stale-git-lock-template.js +64 -0
  25. package/dist/services/codex-stale-git-lock-template.js.map +1 -0
  26. package/dist/services/global-codex-config-migration.test.js +1 -4
  27. package/dist/services/global-codex-config-migration.test.js.map +1 -1
  28. package/dist/services/global-codex-config.d.ts.map +1 -1
  29. package/dist/services/global-codex-config.js +2 -1
  30. package/dist/services/global-codex-config.js.map +1 -1
  31. package/dist/services/global-codex-config.test.js +3 -1
  32. package/dist/services/global-codex-config.test.js.map +1 -1
  33. package/dist/services/hook-readiness.d.ts.map +1 -1
  34. package/dist/services/hook-readiness.js.map +1 -1
  35. package/dist/services/install-agents-template.js +2 -2
  36. package/dist/services/install-agents-template.js.map +1 -1
  37. package/dist/services/install-template-policy.d.ts.map +1 -1
  38. package/dist/services/install-template-policy.js +2 -0
  39. package/dist/services/install-template-policy.js.map +1 -1
  40. package/dist/services/logger.d.ts +1 -1
  41. package/dist/services/logger.d.ts.map +1 -1
  42. package/dist/services/logger.js +2 -1
  43. package/dist/services/logger.js.map +1 -1
  44. package/dist/services/logger.test.js +77 -0
  45. package/dist/services/logger.test.js.map +1 -1
  46. package/dist/services/runtime-template-integrity.test.d.ts +2 -0
  47. package/dist/services/runtime-template-integrity.test.d.ts.map +1 -0
  48. package/dist/services/runtime-template-integrity.test.js +26 -0
  49. package/dist/services/runtime-template-integrity.test.js.map +1 -0
  50. package/dist/services/runtime-template-test-support.d.ts +3 -0
  51. package/dist/services/runtime-template-test-support.d.ts.map +1 -0
  52. package/dist/services/runtime-template-test-support.js +32 -0
  53. package/dist/services/runtime-template-test-support.js.map +1 -0
  54. package/dist/services/runtime-template.test.js +37 -48
  55. package/dist/services/runtime-template.test.js.map +1 -1
  56. package/package.json +1 -1
  57. package/templates/.agents/README.md +8 -0
  58. package/templates/.agents/config/agents/discovery.json +1 -1
  59. package/templates/.agents/config/deploy-policy.json +5 -0
  60. package/templates/.agents/dashboard/.zaip-managed.json +235 -83
  61. package/templates/.agents/dashboard/README.md +2 -0
  62. package/templates/.agents/dashboard/apps/frontend/dist/assets/{CostCalculationGuidePage-f4ZGJRg3.js → CostCalculationGuidePage-OUjdwEcM.js} +1 -1
  63. package/templates/.agents/dashboard/apps/frontend/dist/assets/{DashboardHelpDialog-CkQ_y8kg.js → DashboardHelpDialog-Cei1ChjE.js} +99 -10
  64. package/templates/.agents/dashboard/apps/frontend/dist/assets/{PricingSettingsDialog-v9zGRTlk.js → PricingSettingsDialog-_woNCrTT.js} +1 -1
  65. package/templates/.agents/dashboard/apps/frontend/dist/assets/{ProjectSettingsPage-doIMhei7.js → ProjectSettingsPage-B2_7MVOP.js} +1 -1
  66. package/templates/.agents/dashboard/apps/frontend/dist/assets/{TraceabilityWorkspace-C_4ftjyB.js → TraceabilityWorkspace-C_jCsSZe.js} +1 -1
  67. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-C8zQDBT7.js +68 -0
  68. package/templates/.agents/dashboard/apps/frontend/dist/assets/{icons-vendor-Cv3WBFIF.js → icons-vendor-iMPXot7Y.js} +1 -1
  69. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-DZhppOIk.css +1 -0
  70. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-DiCwz0sr.js +2 -0
  71. package/templates/.agents/dashboard/apps/frontend/dist/assets/{react-vendor-mHgsmxb6.js → react-vendor-721Re3Ss.js} +1 -1
  72. package/templates/.agents/dashboard/apps/frontend/dist/assets/{traceability-B0CPyEVq.js → traceability-BjdTkWS0.js} +1 -1
  73. package/templates/.agents/dashboard/apps/frontend/dist/index.html +6 -6
  74. package/templates/.agents/dashboard/apps/frontend/package.json +1 -1
  75. package/templates/.agents/dashboard/apps/frontend/src/AppOverlays.tsx +6 -34
  76. package/templates/.agents/dashboard/apps/frontend/src/ReconciliationFeedback.dom.test.ts +130 -0
  77. package/templates/.agents/dashboard/apps/frontend/src/ReconciliationFeedback.tsx +226 -0
  78. package/templates/.agents/dashboard/apps/frontend/src/dashboard-api-sqlite-view.ts +45 -0
  79. package/templates/.agents/dashboard/apps/frontend/src/dashboard-api.test.ts +15 -0
  80. package/templates/.agents/dashboard/apps/frontend/src/dashboard-api.ts +17 -46
  81. package/templates/.agents/dashboard/apps/frontend/src/reconciliation-feedback-fixtures.ts +23 -0
  82. package/templates/.agents/dashboard/apps/frontend/src/reconciliation-feedback-model.test.ts +69 -0
  83. package/templates/.agents/dashboard/apps/frontend/src/reconciliation-feedback-model.ts +59 -0
  84. package/templates/.agents/dashboard/apps/frontend/src/reconciliation-feedback.css +131 -0
  85. package/templates/.agents/dashboard/apps/frontend/src/styles.css +0 -32
  86. package/templates/.agents/dashboard/apps/frontend/src/use-dashboard-app-model.tsx +2 -2
  87. package/templates/.agents/dashboard/modules/cost-monitor/package.json +1 -1
  88. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.test.ts +2 -2
  89. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.tsx +1 -1
  90. package/templates/.agents/dashboard/modules/operations/package.json +1 -1
  91. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.macro-metrics.test.ts +7 -2
  92. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.tsx +6 -6
  93. package/templates/.agents/dashboard/modules/operations/src/react/OperationsPage.tsx +1 -4
  94. package/templates/.agents/dashboard/modules/operations/src/react/UnifiedWorkBoard.navigation.test.ts +236 -0
  95. package/templates/.agents/dashboard/modules/operations/src/react/UnifiedWorkBoard.tsx +1 -24
  96. package/templates/.agents/dashboard/modules/operations/src/react/operations-overview-period-filter.test.ts +3 -2
  97. package/templates/.agents/dashboard/modules/operations/src/react/overview-duration-hint.ts +14 -0
  98. package/templates/.agents/dashboard/modules/traceability/package.json +1 -1
  99. package/templates/.agents/dashboard/packages/branding/package.json +1 -1
  100. package/templates/.agents/dashboard/packages/contracts/package.json +1 -1
  101. package/templates/.agents/dashboard/packages/data-export/package.json +1 -1
  102. package/templates/.agents/dashboard/packages/local-runtime/DIAGNOSTICS.md +42 -0
  103. package/templates/.agents/dashboard/packages/local-runtime/dashboard-diagnostics.d.mts +10 -0
  104. package/templates/.agents/dashboard/packages/local-runtime/dashboard-diagnostics.mjs +101 -0
  105. package/templates/.agents/dashboard/packages/local-runtime/dashboard-diagnostics.test.mjs +58 -0
  106. package/templates/.agents/dashboard/packages/local-runtime/dist/server.cjs +318 -236
  107. package/templates/.agents/dashboard/packages/local-runtime/dist/sqlite-read-model.cjs +285 -205
  108. package/templates/.agents/dashboard/packages/local-runtime/package.json +1 -1
  109. package/templates/.agents/dashboard/packages/local-runtime/scripts/test-sqlite-dashboard-startup.mjs +233 -0
  110. package/templates/.agents/dashboard/packages/local-runtime/scripts/validate-provider-nonzero.mjs +45 -0
  111. package/templates/.agents/dashboard/packages/local-runtime/scripts/verify-dashboard-executable-startup.mjs +107 -0
  112. package/templates/.agents/dashboard/packages/local-runtime/scripts/verify-dashboard-reconciliation-executable.mjs +271 -0
  113. package/templates/.agents/dashboard/packages/local-runtime/server.mjs +318 -236
  114. package/templates/.agents/dashboard/packages/local-runtime/src/active-codex-telemetry.test.ts +78 -12
  115. package/templates/.agents/dashboard/packages/local-runtime/src/active-codex-telemetry.ts +36 -56
  116. package/templates/.agents/dashboard/packages/local-runtime/src/canonical-source-revision-cache.test.ts +125 -0
  117. package/templates/.agents/dashboard/packages/local-runtime/src/canonical-source-revision-cache.ts +168 -0
  118. package/templates/.agents/dashboard/packages/local-runtime/src/dashboard-diagnostics.test.ts +57 -0
  119. package/templates/.agents/dashboard/packages/local-runtime/src/server-agent-catalog-routes.ts +76 -0
  120. package/templates/.agents/dashboard/packages/local-runtime/src/server-dual-reads-manifest-summaries-without-losing.test.ts +5 -0
  121. package/templates/.agents/dashboard/packages/local-runtime/src/server-reconciliation-diagnostics.test.ts +118 -0
  122. package/templates/.agents/dashboard/packages/local-runtime/src/server-reconciliation-result.test.ts +35 -6
  123. package/templates/.agents/dashboard/packages/local-runtime/src/server-reconciliation-route.ts +105 -0
  124. package/templates/.agents/dashboard/packages/local-runtime/src/server-snapshot-synchronization.test.ts +64 -9
  125. package/templates/.agents/dashboard/packages/local-runtime/src/server-snapshot-synchronization.ts +19 -27
  126. package/templates/.agents/dashboard/packages/local-runtime/src/server-telemetry-command.test.ts +111 -0
  127. package/templates/.agents/dashboard/packages/local-runtime/src/server-telemetry-refresh.test.ts +59 -0
  128. package/templates/.agents/dashboard/packages/local-runtime/src/server-telemetry-refresh.ts +44 -0
  129. package/templates/.agents/dashboard/packages/local-runtime/src/server-work-reconciliation.test.ts +20 -0
  130. package/templates/.agents/dashboard/packages/local-runtime/src/server-work-reconciliation.ts +12 -7
  131. package/templates/.agents/dashboard/packages/local-runtime/src/server-workspace.ts +48 -6
  132. package/templates/.agents/dashboard/packages/local-runtime/src/server.test.ts +2 -2
  133. package/templates/.agents/dashboard/packages/local-runtime/src/server.ts +126 -134
  134. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-agent-page.ts +14 -2
  135. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-summary-elapsed.ts +107 -0
  136. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-dashboard-startup.test.ts +163 -0
  137. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-dashboard-startup.ts +78 -0
  138. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-header-timestamp.test.ts +31 -0
  139. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-provider-nonzero.test.ts +271 -0
  140. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-provider-visibility.ts +32 -0
  141. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-query-operations-duration.ts +58 -0
  142. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-query-provider-dimension.ts +12 -3
  143. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-query-summary.ts +41 -54
  144. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-query-view-page.ts +9 -58
  145. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-canonical.ts +7 -7
  146. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-counts-every-root-execution-day-returns.test.ts +5 -5
  147. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-multi-provider.test.ts +4 -4
  148. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-rebuild.ts +9 -35
  149. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-schema.ts +13 -9
  150. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-source-fingerprint.ts +43 -45
  151. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.test.ts +5 -9
  152. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.ts +5 -1
  153. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-schema-base-views.ts +9 -7
  154. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-schema-dashboard-views.ts +10 -8
  155. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-spec-root-elapsed-view.ts +61 -0
  156. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-spec-root-elapsed.test.ts +210 -0
  157. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-startup-seed.test.ts +87 -0
  158. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-logging.mjs +6 -2
  159. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-server.mjs +7 -1
  160. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.mjs +4 -5
  161. package/templates/.agents/dashboard/packages/ui/package.json +1 -1
  162. package/templates/.agents/dashboard/packages/ui/src/DashboardHeader.test.mjs +33 -4
  163. package/templates/.agents/dashboard/packages/ui/src/DashboardHeader.tsx +8 -7
  164. package/templates/.agents/dashboard/packages/windows-launcher/Program.cs +46 -3
  165. package/templates/.agents/dashboard/tests/e2e/package.json +1 -1
  166. package/templates/.agents/dashboard/tests/e2e/qualify-kanban-popup.mjs +56 -0
  167. package/templates/.agents/dashboard/tests/e2e/specs/dashboard-update-label.spec.ts +49 -0
  168. package/templates/.agents/design/.zaip-managed.json +17 -5
  169. package/templates/.agents/design/apps/catalog/dist/assets/index-D3uhpycC.js +72 -0
  170. package/templates/.agents/design/apps/catalog/dist/index.html +1 -1
  171. package/templates/.agents/design/apps/catalog/package.json +1 -1
  172. package/templates/.agents/design/apps/catalog/src/pages/ComponentsPage.tsx +13 -1
  173. package/templates/.agents/design/apps/catalog/src/specimens/ReconciliationSpecimen.stories.tsx +6 -0
  174. package/templates/.agents/design/apps/catalog/src/specimens/ReconciliationSpecimen.tsx +101 -0
  175. package/templates/.agents/design/packages/local-runtime/package.json +1 -1
  176. package/templates/.agents/requirements/autonomous-assurance.md +1 -1
  177. package/templates/.agents/requirements/cloud-hostinger-autonomy.md +1 -1
  178. package/templates/.agents/requirements/comparative-audit.md +1 -1
  179. package/templates/.agents/requirements/context-engine.md +1 -1
  180. package/templates/.agents/requirements/cost-observability.md +1 -1
  181. package/templates/.agents/requirements/dashboard-launch.md +1 -1
  182. package/templates/.agents/requirements/data-export.md +1 -1
  183. package/templates/.agents/requirements/design-system.md +1 -1
  184. package/templates/.agents/requirements/execution-engine.md +1 -1
  185. package/templates/.agents/requirements/kanban-dashboard.md +1 -1
  186. package/templates/.agents/requirements/model-orchestration.md +21 -6
  187. package/templates/.agents/requirements/public-distribution.md +4 -4
  188. package/templates/.agents/requirements/software-architecture.md +1 -1
  189. package/templates/.agents/requirements/strategic-improvements.md +1 -1
  190. package/templates/.agents/requirements/work-lifecycle.md +1 -1
  191. package/templates/.agents/requirements/zaip-strategic-95.md +1 -1
  192. package/templates/.agents/rules/core.md +3 -1
  193. package/templates/.agents/rules/workflow.md +3 -2
  194. package/templates/.agents/runtime/VERSION +1 -1
  195. package/templates/.agents/runtime/bin/zaip.mjs +368 -199
  196. package/templates/.agents/runtime/checksums.sha256 +5 -5
  197. package/templates/.agents/runtime/manifest.json +1 -1
  198. package/templates/.agents/runtime/provenance.json +4 -4
  199. package/templates/.agents/runtime/sbom.cdx.json +1 -1
  200. package/templates/.agents/skills/zaip-skill-config/SKILL.md +25 -3
  201. package/templates/.agents/skills/zaip-skill-execution/SKILL.md +4 -0
  202. package/templates/.agents/skills/zaip-skill-execution/references/autonomous-continuation.md +4 -0
  203. package/templates/.agents/skills/zaip-skill-execution/references/candidate-workflow.md +1 -1
  204. package/templates/.agents/skills/zaip-skill-execution/references/runtime-governance.md +2 -2
  205. package/templates/.agents/skills/zaip-skill-general-operations/SKILL.md +1 -0
  206. package/templates/.agents/skills/zaip-skill-github-deploy/SKILL.md +38 -48
  207. package/templates/.agents/skills/zaip-skill-github-deploy/agents/openai.yaml +2 -2
  208. package/templates/.agents/skills/zaip-skill-github-deploy/references/deployment-workflow.md +67 -99
  209. package/templates/.agents/skills/zaip-skill-github-deploy/references/release-identity.md +33 -30
  210. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/deploy-contract.mjs +307 -0
  211. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/deploy-contract.test.mjs +337 -0
  212. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/github-deploy-contract.test.mjs +41 -28
  213. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/guarded-commits.mjs +212 -0
  214. package/templates/.agents/skills/zaip-skill-lifecycle-closure/SKILL.md +4 -0
  215. package/templates/.agents/skills/zaip-skill-prd/SKILL.md +4 -0
  216. package/templates/.agents/skills/zaip-skill-release-qualification/SKILL.md +46 -68
  217. package/templates/.agents/skills/zaip-skill-release-qualification/references/local-preparation.md +100 -44
  218. package/templates/.agents/skills/zaip-skill-send-email/SKILL.md +15 -0
  219. package/templates/.agents/skills/zaip-skill-send-email/agents/openai.yaml +4 -0
  220. package/templates/.agents/skills/zaip-skill-setup/SKILL.md +5 -2
  221. package/templates/.agents/skills/zaip-skill-spec/SKILL.md +4 -0
  222. package/templates/.agents/skills/zaip-skill-task/SKILL.md +4 -0
  223. package/templates/.agents/skills/zaip-skill-testing-strategy/SKILL.md +19 -0
  224. package/templates/.agents/skills/zaip-skill-tool-setup/SKILL.md +2 -0
  225. package/templates/.agents/skills/zaip-skill-use-connectors/SKILL.md +15 -0
  226. package/templates/.agents/skills/zaip-skill-use-connectors/agents/openai.yaml +4 -0
  227. package/templates/.agents/skills/zaip-skill-use-connectors/scripts/connector-guidance.test.mjs +29 -0
  228. package/templates/.agents/skills/zaip-skill-use-connectors/scripts/verify-connector-lock.mjs +30 -0
  229. package/templates/.agents/tools/cloud-runner.integration.test.mjs +3 -0
  230. package/templates/.agents/zaip-aios/VERSION +1 -1
  231. package/templates/.agents/zaip-aios/guides/03-tasks.md +6 -0
  232. package/templates/.agents/zaip-aios/guides/04-specs.md +6 -0
  233. package/templates/.agents/zaip-aios/guides/05-prds.md +4 -0
  234. package/templates/.agents/zaip-aios/guides/06-execucao.md +6 -0
  235. package/templates/.agents/zaip-aios/guides/16-lifecycle-e-reconciliacao.md +6 -0
  236. package/templates/.agents/zaip-aios/guides/19-assurance-e-autonomia.md +4 -0
  237. package/templates/.agents/zaip-aios/guides/22-fluxos-e-roteamento.md +6 -0
  238. package/templates/.agents/zaip-aios/guides/23-orquestracao-de-modelos.md +36 -4
  239. package/templates/.agents/zaip-aios/guides/26-qualidade-e-release.md +15 -4
  240. package/templates/.agents/zaip-aios/guides/28-lifecycle-completo.md +8 -0
  241. package/templates/template-manifest.json +354 -150
  242. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-D7PrDZy9.js +0 -68
  243. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-OBJXZOwe.css +0 -1
  244. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-Rbmpz4UO.js +0 -2
@@ -1,33 +1,84 @@
1
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
1
+ import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { afterEach, describe, expect, it } from "vitest";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
5
 
6
- import { captureActiveCodexTelemetry } from "./active-codex-telemetry.js";
6
+ import {
7
+ captureActiveCodexTelemetry,
8
+ captureRepositoryActiveCodexTelemetry,
9
+ } from "./active-codex-telemetry.js";
10
+ import * as sources from "./sqlite-read-model-source-fingerprint.js";
7
11
 
8
12
  const roots: string[] = [];
9
13
 
10
14
  afterEach(async () => {
15
+ vi.restoreAllMocks();
11
16
  await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
12
17
  });
13
18
 
14
19
  describe("active Codex telemetry capture", () => {
20
+ it("allows the measured catch-up budget and still rejects incomplete output", async () => {
21
+ const root = await runtimeFixture("");
22
+ const execute = vi.fn(async (_file: string, _args: string[], _options: unknown) => ({
23
+ stdout: JSON.stringify({
24
+ telemetry: { mode: "recent_exact_session", complete: false, captured: [] },
25
+ }),
26
+ stderr: "",
27
+ }));
28
+ await expect(captureActiveCodexTelemetry(root, root, execute as never)).rejects.toThrow(
29
+ "incomplete",
30
+ );
31
+ expect(execute.mock.calls[0]?.[2]).toMatchObject({
32
+ timeout: 600_000,
33
+ windowsHide: true,
34
+ cwd: root,
35
+ });
36
+ });
37
+ it("uses the primary CLI for registered worktrees while preserving each target workspace and cwd", async () => {
38
+ const primary = await runtimeFixture(`import {writeFileSync} from 'node:fs';
39
+ import {resolve} from 'node:path';
40
+ const target=process.argv[process.argv.indexOf('--workspace')+1];
41
+ if(target!==resolve(process.cwd()))throw new Error('Target workspace mismatch');
42
+ writeFileSync('capture-target.json',JSON.stringify({target,cwd:process.cwd(),cli:process.argv[1]}));
43
+ process.stdout.write(JSON.stringify({telemetry:{mode:'recent_exact_session',complete:true,captured:[{captured:true}]}}));`);
44
+ const secondary = await runtimeFixture(`import {writeFileSync} from 'node:fs';
45
+ writeFileSync('outdated-cli-executed','incorrect runtime');process.exitCode=9;`);
46
+ vi.spyOn(sources, "dashboardCanonicalSourceRoots").mockResolvedValue([primary, secondary]);
47
+ await expect(captureRepositoryActiveCodexTelemetry(primary)).resolves.toMatchObject({
48
+ captured_runs: 2,
49
+ captured_segments: null,
50
+ skipped_reason: null,
51
+ });
52
+ for (const target of [primary, secondary]) {
53
+ const captured = JSON.parse(await readFile(join(target, "capture-target.json"), "utf8"));
54
+ expect(captured).toEqual({
55
+ target,
56
+ cwd: target,
57
+ cli: join(primary, ".agents/runtime/bin/zaip.mjs"),
58
+ });
59
+ }
60
+ await expect(stat(join(secondary, "outdated-cli-executed"))).rejects.toMatchObject({
61
+ code: "ENOENT",
62
+ });
63
+ });
15
64
  it("reports an idle capture explicitly instead of treating it as an opaque success", async () => {
16
65
  const root = await runtimeFixture(
17
- "process.stdout.write(JSON.stringify({ telemetry: { mode: 'active_exact_session', captured: [] } }, null, 2));\n",
66
+ "process.stdout.write(JSON.stringify({ telemetry: { mode: 'recent_exact_session', complete:true, captured: [] } }, null, 2));\n",
18
67
  );
19
68
 
20
69
  await expect(captureActiveCodexTelemetry(root)).resolves.toMatchObject({
21
70
  captured_runs: 0,
22
71
  captured_segments: 0,
23
72
  manifest_state: "initialized",
24
- skipped_reason: "no_active_runs",
73
+ skipped_reason: "no_recent_runs",
25
74
  last_capture_at: expect.any(String),
26
75
  });
27
76
  });
28
77
 
29
- it("does not execute a CLI against historical running entries in a stale worktree", async () => {
30
- const root = await runtimeFixture("process.exitCode = 8;\n");
78
+ it("does not let an empty or stale active index suppress terminal usage capture", async () => {
79
+ const root = await runtimeFixture(
80
+ "if(!process.argv.includes('--recent') || !process.argv.includes('--drain') || process.argv.includes('--active') || process.argv[process.argv.indexOf('--limit')+1]!=='32') process.exit(8); process.stdout.write(JSON.stringify({telemetry:{mode:'recent_exact_session',complete:true,captured:[{captured:true}]}}));\n",
81
+ );
31
82
  await writeFile(
32
83
  join(root, ".agents/records/lifecycle/active-work.json"),
33
84
  JSON.stringify({
@@ -36,13 +87,13 @@ describe("active Codex telemetry capture", () => {
36
87
  }),
37
88
  );
38
89
  await expect(captureActiveCodexTelemetry(root)).resolves.toMatchObject({
39
- skipped_reason: "no_recent_active_runs",
40
- captured_runs: 0,
90
+ skipped_reason: null,
91
+ captured_runs: 1,
41
92
  });
42
93
  });
43
94
  it("still captures a recently active worktree Run", async () => {
44
95
  const root = await runtimeFixture(
45
- "process.stdout.write(JSON.stringify({telemetry:{captured:[{captured:true}]}}));\n",
96
+ "process.stdout.write(JSON.stringify({telemetry:{mode:'recent_exact_session',complete:true,captured:[{captured:true}]}}));\n",
46
97
  );
47
98
  await writeFile(
48
99
  join(root, ".agents/records/lifecycle/active-work.json"),
@@ -71,7 +122,7 @@ describe("active Codex telemetry capture", () => {
71
122
  });
72
123
  it("does not accept CLI idle success as proof of initialized lifecycle", async () => {
73
124
  const root = await runtimeFixture(
74
- "process.stdout.write(JSON.stringify({ telemetry: { captured: [] } }));",
125
+ "process.stdout.write(JSON.stringify({ telemetry: { mode:'recent_exact_session', complete:true, captured: [] } }));",
75
126
  );
76
127
  await rm(join(root, ".agents/records/lifecycle"), { recursive: true });
77
128
  await expect(captureActiveCodexTelemetry(root)).resolves.toMatchObject({
@@ -81,7 +132,7 @@ describe("active Codex telemetry capture", () => {
81
132
  });
82
133
  it("preserves unavailable segment cardinality for a captured Run", async () => {
83
134
  const root = await runtimeFixture(
84
- "process.stdout.write(JSON.stringify({ telemetry: { captured: [{captured:true}] } }, null, 2));",
135
+ "process.stdout.write(JSON.stringify({ telemetry: { mode:'recent_exact_session', complete:true, captured: [{captured:true}] } }, null, 2));",
85
136
  );
86
137
  await expect(captureActiveCodexTelemetry(root)).resolves.toMatchObject({
87
138
  captured_runs: 1,
@@ -95,6 +146,21 @@ describe("active Codex telemetry capture", () => {
95
146
  );
96
147
  await expect(captureActiveCodexTelemetry(root)).rejects.toThrow("Invalid capture telemetry");
97
148
  });
149
+ it("rejects an incomplete bounded pass rather than calling partial collection current", async () => {
150
+ const root = await runtimeFixture(
151
+ "process.stdout.write(JSON.stringify({telemetry:{mode:'recent_exact_session',complete:false,captured:[]}}));",
152
+ );
153
+ await expect(captureActiveCodexTelemetry(root)).rejects.toThrow("incomplete");
154
+ });
155
+ it.each([
156
+ "active_exact_session",
157
+ null,
158
+ ])("rejects incompatible capture receipt mode %s", async (mode) => {
159
+ const root = await runtimeFixture(
160
+ `process.stdout.write(JSON.stringify({telemetry:{mode:${JSON.stringify(mode)},complete:true,captured:[]}}));`,
161
+ );
162
+ await expect(captureActiveCodexTelemetry(root)).rejects.toThrow("incomplete");
163
+ });
98
164
  });
99
165
 
100
166
  async function runtimeFixture(script: string): Promise<string> {
@@ -1,12 +1,15 @@
1
1
  import { execFile } from "node:child_process";
2
- import { readFile, stat } from "node:fs/promises";
3
- import { join, resolve } from "node:path";
2
+ import { resolve } from "node:path";
4
3
  import { promisify } from "node:util";
5
4
  import { WorkspaceFileSystem } from "@zaip-ai/core";
6
5
  import { lifecycleManifestState } from "./server-lifecycle-diagnostics.js";
7
6
  import { dashboardCanonicalSourceRoots } from "./sqlite-read-model-source-fingerprint.js";
8
7
 
9
8
  const execFileAsync = promisify(execFile);
9
+ // Measured complete catch-up of a 98-Run / 30k-envelope backlog exceeded the
10
+ // old 55s deadline. Keep the UI updating while this finite selection commits;
11
+ // expiry remains a failure and can never promote a partial capture to current.
12
+ export const RECENT_CODEX_CAPTURE_TIMEOUT_MS = 600_000;
10
13
 
11
14
  export type ActiveCodexTelemetryCapture = {
12
15
  captured_runs: number;
@@ -18,18 +21,21 @@ export type ActiveCodexTelemetryCapture = {
18
21
  | "manifest_invalid"
19
22
  | "no_active_runs"
20
23
  | "no_recent_active_runs"
24
+ | "no_recent_runs"
21
25
  | "no_new_segments"
22
26
  | null;
23
27
  last_capture_at: string;
24
28
  };
25
29
 
26
30
  /**
27
- * Captures only active Runs whose persisted Codex correlation names a concrete
31
+ * Captures recent active and terminal Runs whose persisted Codex correlation names a concrete
28
32
  * turn. The CLI reuses source segment and loop keys as its durable cursor;
29
33
  * this never reconciles or changes a Run lifecycle.
30
34
  */
31
35
  export async function captureActiveCodexTelemetry(
32
36
  workspaceRoot: string,
37
+ runtimeRoot = workspaceRoot,
38
+ execute = execFileAsync,
33
39
  ): Promise<ActiveCodexTelemetryCapture> {
34
40
  // The CLI intentionally treats an absent active-work index as an empty pass.
35
41
  // Validate the canonical manifest first so that exit 0 cannot mask missing lifecycle.
@@ -42,26 +48,28 @@ export async function captureActiveCodexTelemetry(
42
48
  skipped_reason: manifestState === "missing" ? "manifest_uninitialized" : "manifest_invalid",
43
49
  last_capture_at: new Date().toISOString(),
44
50
  };
45
- const activeRuns = await recentActiveRuns(workspaceRoot);
46
- if (activeRuns !== null && activeRuns.recent === 0) {
47
- return {
48
- captured_runs: 0,
49
- captured_segments: 0,
50
- manifest_state: "initialized",
51
- skipped_reason: activeRuns.stale > 0 ? "no_recent_active_runs" : "no_active_runs",
52
- last_capture_at: new Date().toISOString(),
53
- };
54
- }
55
- const cliPath = resolve(workspaceRoot, ".agents", "runtime", "bin", "zaip.mjs");
51
+ const cliPath = resolve(runtimeRoot, ".agents", "runtime", "bin", "zaip.mjs");
56
52
  try {
57
- const outcome = await execFileAsync(
53
+ const outcome = await execute(
58
54
  process.execPath,
59
- [cliPath, "run", "usage", "--active", "--limit", "1", "--disable-codex-raw", "--json"],
55
+ [
56
+ cliPath,
57
+ "--workspace",
58
+ resolve(workspaceRoot),
59
+ "run",
60
+ "usage",
61
+ "--recent",
62
+ "--limit",
63
+ "32",
64
+ "--drain",
65
+ "--disable-codex-raw",
66
+ "--json",
67
+ ],
60
68
  {
61
69
  cwd: workspaceRoot,
62
70
  encoding: "utf8",
63
71
  windowsHide: true,
64
- timeout: 55_000,
72
+ timeout: RECENT_CODEX_CAPTURE_TIMEOUT_MS,
65
73
  maxBuffer: 1_024 * 1_024,
66
74
  },
67
75
  );
@@ -75,7 +83,7 @@ export async function captureActiveCodexTelemetry(
75
83
  captured_segments: capturedRuns === 0 ? 0 : null,
76
84
  manifest_state: "initialized",
77
85
  skipped_reason:
78
- captured.length === 0 ? "no_active_runs" : capturedRuns === 0 ? "no_new_segments" : null,
86
+ captured.length === 0 ? "no_recent_runs" : capturedRuns === 0 ? "no_new_segments" : null,
79
87
  last_capture_at: new Date().toISOString(),
80
88
  };
81
89
  } catch (error) {
@@ -92,49 +100,18 @@ export async function captureActiveCodexTelemetry(
92
100
  }
93
101
  }
94
102
 
95
- /** A stale lifecycle index is not proof of a live Codex invocation. */
96
- async function recentActiveRuns(
97
- workspaceRoot: string,
98
- ): Promise<{ recent: number; stale: number } | null> {
99
- const path = join(workspaceRoot, ".agents", "records", "lifecycle", "active-work.json");
100
- const metadata = await stat(path).catch((error: NodeJS.ErrnoException) => {
101
- if (error.code === "ENOENT") return null;
102
- throw error;
103
- });
104
- if (metadata === null) return null;
105
- if (metadata.size > 8 * 1024 * 1024) throw new Error("Active-work index exceeds capture limit");
106
- const parsed: unknown = JSON.parse(await readFile(path, "utf8"));
107
- if (
108
- typeof parsed !== "object" ||
109
- parsed === null ||
110
- !Array.isArray((parsed as { items?: unknown }).items)
111
- )
112
- throw new Error("Invalid active-work index for telemetry capture");
113
- const cutoff = Date.now() - 72 * 60 * 60 * 1_000;
114
- const runs = (parsed as { items: unknown[] }).items.filter(
115
- (item) =>
116
- typeof item === "object" &&
117
- item !== null &&
118
- (item as { kind?: unknown }).kind === "run" &&
119
- (item as { status?: unknown }).status === "running",
120
- );
121
- const recent = runs.filter((item) => {
122
- const updated = (item as { updated_at?: unknown }).updated_at;
123
- return (
124
- typeof updated === "string" &&
125
- Number.isFinite(Date.parse(updated)) &&
126
- Date.parse(updated) >= cutoff
127
- );
128
- }).length;
129
- return { recent, stale: runs.length - recent };
130
- }
131
-
132
103
  /** Capture each canonical checkout before refreshing the combined SQLite view. */
133
104
  export async function captureRepositoryActiveCodexTelemetry(
134
105
  workspaceRoot: string,
106
+ runtimeRoot = workspaceRoot,
135
107
  ): Promise<ActiveCodexTelemetryCapture> {
136
108
  const roots = await dashboardCanonicalSourceRoots(workspaceRoot);
137
- const results = await Promise.all(roots.map((root) => captureActiveCodexTelemetry(root)));
109
+ // Registered checkouts contribute records, not executable runtime versions.
110
+ // One current CLI serves every target; --workspace pins its storage and
111
+ // exact source correlation even when the cwd belongs to another checkout.
112
+ const results = await Promise.all(
113
+ roots.map((root) => captureActiveCodexTelemetry(root, runtimeRoot)),
114
+ );
138
115
  const primary = results[0];
139
116
  if (primary === undefined) throw new Error("No dashboard workspace was discovered");
140
117
  const capturedRuns = results.reduce((count, result) => count + result.captured_runs, 0);
@@ -166,6 +143,9 @@ function readCaptured(payload: unknown): Array<{ captured: boolean }> {
166
143
  const telemetry = (payload as { telemetry?: unknown }).telemetry;
167
144
  if (typeof telemetry !== "object" || telemetry === null)
168
145
  throw new Error("Invalid capture telemetry");
146
+ const receipt = telemetry as { mode?: unknown; complete?: unknown };
147
+ if (receipt.mode !== "recent_exact_session" || receipt.complete !== true)
148
+ throw new Error("Recent telemetry capture is incomplete");
169
149
  const captured = (telemetry as { captured?: unknown }).captured;
170
150
  if (!Array.isArray(captured)) throw new Error("Invalid capture entries");
171
151
  return captured.map((item) => {
@@ -0,0 +1,125 @@
1
+ import * as fs from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+ import { CanonicalSourceRevisionCache } from "./canonical-source-revision-cache.js";
5
+ import { sourceFingerprint } from "./sqlite-read-model-source-fingerprint.js";
6
+
7
+ vi.mock("node:fs/promises", async (original) => {
8
+ const actual = await original<typeof import("node:fs/promises")>();
9
+ return { ...actual, readFile: vi.fn(actual.readFile) };
10
+ });
11
+
12
+ const roots: string[] = [];
13
+ const source = ".agents/records/tasks/TASK-CACHE.md";
14
+ const record = (revision: number) =>
15
+ `---\nid: TASK-CACHE\nupdated_at: 2026-09-29T12:00:00Z\nrevision: ${revision}\n---\n`;
16
+ async function fixture() {
17
+ const parent = resolve(import.meta.dirname, "../../../../..", ".local/sqlite/tests");
18
+ await fs.mkdir(parent, { recursive: true });
19
+ const root = await fs.mkdtemp(join(parent, "revision-cache-"));
20
+ roots.push(root);
21
+ await fs.mkdir(join(root, ".agents/records/tasks"), { recursive: true });
22
+ await fs.writeFile(join(root, source), record(1));
23
+ return root;
24
+ }
25
+ afterEach(async () => {
26
+ vi.restoreAllMocks();
27
+ await Promise.all(roots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true })));
28
+ });
29
+
30
+ describe("physical canonical revision cache", () => {
31
+ it("persists peer revisions across workers, selects revision despite older mtime, and bypasses on force", async () => {
32
+ const root = await fixture();
33
+ const peer = await fixture();
34
+ await fs.writeFile(join(peer, source), record(2));
35
+ await fs.utimes(join(peer, source), new Date("2025-01-01"), new Date("2025-01-01"));
36
+ vi.spyOn(Date, "now").mockReturnValue(Date.now() + 10_000);
37
+ const options = { sourceRoots: [root, peer] };
38
+ const initial = await sourceFingerprint(root, options);
39
+ expect(initial.candidates[0]?.path).toBe(join(peer, source));
40
+ expect(initial.duplicateReads).toBe(2);
41
+ const previous = new Map(initial.candidates.map((item) => [item.source, item]));
42
+ const next = await sourceFingerprint(root, { ...options, previous });
43
+ expect(next).toMatchObject({ duplicateReads: 0, duplicateReused: 2, hashedFiles: 0 });
44
+ expect(next.fingerprint).toBe(initial.fingerprint);
45
+ const forced = await sourceFingerprint(root, { ...options, previous, verifyContent: true });
46
+ expect(forced).toMatchObject({ duplicateReads: 2, duplicateReused: 0, hashedFiles: 1 });
47
+ expect(forced.fingerprint).toBe(initial.fingerprint);
48
+ expect(await fs.stat(join(peer, ".local")).catch(() => null)).toBeNull();
49
+ });
50
+
51
+ it("invalidates edits, replacements, additions and removals; keeps ties deterministic", async () => {
52
+ const root = await fixture();
53
+ const peer = await fixture();
54
+ vi.spyOn(Date, "now").mockReturnValue(Date.now() + 10_000);
55
+ const options = { sourceRoots: [root, peer] };
56
+ expect((await sourceFingerprint(root, options)).candidates[0]?.path).toBe(join(root, source));
57
+ const metadata = await fs.stat(join(peer, source));
58
+ await fs.writeFile(join(peer, source), record(2));
59
+ await fs.utimes(join(peer, source), metadata.atime, metadata.mtime);
60
+ expect((await sourceFingerprint(root, options)).candidates[0]?.path).toBe(join(peer, source));
61
+ await fs.writeFile(join(root, "replacement"), record(3));
62
+ await fs.utimes(join(root, "replacement"), metadata.atime, metadata.mtime);
63
+ await fs.rename(join(root, "replacement"), join(root, source));
64
+ expect((await sourceFingerprint(root, options)).candidates[0]?.path).toBe(join(root, source));
65
+ await fs.writeFile(join(peer, ".agents/records/tasks/NEW.md"), record(1));
66
+ expect((await sourceFingerprint(root, options)).candidates).toHaveLength(2);
67
+ await fs.rm(join(root, source));
68
+ expect(
69
+ (await sourceFingerprint(root, options)).candidates.find((item) => item.source === source)
70
+ ?.path,
71
+ ).toBe(join(peer, source));
72
+ });
73
+
74
+ it("does not trust rapid writes or their first settled cache entry", async () => {
75
+ const root = await fixture();
76
+ const peer = await fixture();
77
+ const metadata = await fs.stat(join(root, source));
78
+ const clock = vi.spyOn(Date, "now").mockReturnValue(metadata.ctimeMs);
79
+ const options = { sourceRoots: [root, peer] };
80
+ await sourceFingerprint(root, options);
81
+ for (let value = 2; value < 8; value++) {
82
+ await fs.writeFile(join(peer, source), record(value));
83
+ const result = await sourceFingerprint(root, options);
84
+ expect(result.duplicateReads).toBe(2);
85
+ expect(result.candidates[0]?.path).toBe(join(peer, source));
86
+ }
87
+ clock.mockReturnValue(Date.now() + 20_000);
88
+ expect((await sourceFingerprint(root, options)).duplicateReads).toBe(2);
89
+ expect((await sourceFingerprint(root, options)).duplicateReads).toBe(0);
90
+ });
91
+
92
+ it("retries when file metadata changes while reading its revision", async () => {
93
+ const root = await fixture();
94
+ const cache = await CanonicalSourceRevisionCache.open(root);
95
+ const actualRead = (
96
+ await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises")
97
+ ).readFile;
98
+ let changed = false;
99
+ vi.spyOn(fs, "readFile").mockImplementation(async (...args: Parameters<typeof fs.readFile>) => {
100
+ const bytes = await actualRead(...args);
101
+ if (!changed && String(args[0]) === join(root, source)) {
102
+ changed = true;
103
+ await fs.writeFile(join(root, source), record(3) + "changed length\n");
104
+ }
105
+ return bytes;
106
+ });
107
+ const result = await cache.read(join(root, source), source);
108
+ expect(cache.reads).toBe(2);
109
+ expect(result?.entry.revision[1]).toBe(3);
110
+ });
111
+
112
+ it("treats malformed cache as disposable and prunes origins no longer duplicated", async () => {
113
+ const root = await fixture();
114
+ const peer = await fixture();
115
+ vi.spyOn(Date, "now").mockReturnValue(Date.now() + 10_000);
116
+ const options = { sourceRoots: [root, peer] };
117
+ await sourceFingerprint(root, options);
118
+ const path = join(root, ".local/cache/dashboard-canonical-revisions-v1.json");
119
+ await fs.writeFile(path, "not JSON");
120
+ expect((await sourceFingerprint(root, options)).duplicateReads).toBe(2);
121
+ await fs.rm(join(peer, source));
122
+ await sourceFingerprint(root, options);
123
+ expect(JSON.parse(await fs.readFile(path, "utf8")).entries).toEqual([]);
124
+ });
125
+ });
@@ -0,0 +1,168 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import type { BigIntStats } from "node:fs";
3
+ import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
4
+ import { dirname, resolve } from "node:path";
5
+
6
+ type Revision = [string, number, number];
7
+ type Entry = { metadata: string; revision: Revision; contentHash: string; settled: boolean };
8
+ // Covers the observed 12,712 logical sources across ten registered roots.
9
+ const maxEntries = 200_000;
10
+ const maxBytes = 128 * 1024 * 1024;
11
+
12
+ function identity(value: BigIntStats): string {
13
+ return [value.dev, value.ino, value.birthtimeNs, value.mtimeNs, value.ctimeNs, value.size].join(
14
+ ":",
15
+ );
16
+ }
17
+
18
+ function revision(content: string, source: string): Revision {
19
+ if (source.endsWith(".jsonl")) {
20
+ const dates = [...content.matchAll(/"occurred_at"\s*:\s*"([^"]+)"/gu)];
21
+ return [
22
+ dates.reduce((last, match) => ((match[1] ?? "") > last ? match[1]! : last), ""),
23
+ dates.length,
24
+ content.length,
25
+ ];
26
+ }
27
+ const updated = /^updated_at:\s*([^\r\n]+)/mu.exec(content)?.[1]?.trim() ?? "";
28
+ const created = /^created_at:\s*([^\r\n]+)/mu.exec(content)?.[1]?.trim() ?? "";
29
+ return [
30
+ updated || created,
31
+ Number(/^revision:\s*(\d+)/mu.exec(content)?.[1] ?? 0),
32
+ content.length,
33
+ ];
34
+ }
35
+
36
+ function validEntry(value: unknown): value is Entry {
37
+ if (!value || typeof value !== "object") return false;
38
+ const item = value as Entry;
39
+ return (
40
+ typeof item.metadata === "string" &&
41
+ /^\d+(?::\d+){5}$/u.test(item.metadata) &&
42
+ typeof item.contentHash === "string" &&
43
+ /^[a-f0-9]{64}$/u.test(item.contentHash) &&
44
+ typeof item.settled === "boolean" &&
45
+ Array.isArray(item.revision) &&
46
+ item.revision.length === 3 &&
47
+ typeof item.revision[0] === "string" &&
48
+ item.revision[0].length <= 256 &&
49
+ Number.isSafeInteger(item.revision[1]) &&
50
+ item.revision[1] >= 0 &&
51
+ Number.isSafeInteger(item.revision[2]) &&
52
+ item.revision[2] >= 0
53
+ );
54
+ }
55
+
56
+ /** Disposable local optimization, never a canonical record or a replacement for inventory. */
57
+ export class CanonicalSourceRevisionCache {
58
+ private readonly entries = new Map<string, Entry>();
59
+ private readonly seen = new Set<string>();
60
+ private dirty = false;
61
+ reads = 0;
62
+ reused = 0;
63
+ private constructor(
64
+ private readonly path: string,
65
+ private readonly verify: boolean,
66
+ ) {}
67
+
68
+ static async open(workspaceRoot: string, verify = false): Promise<CanonicalSourceRevisionCache> {
69
+ const cache = new CanonicalSourceRevisionCache(
70
+ resolve(workspaceRoot, ".local/cache/dashboard-canonical-revisions-v1.json"),
71
+ verify,
72
+ );
73
+ if (verify) return cache;
74
+ try {
75
+ if ((await stat(cache.path)).size > maxBytes) return cache;
76
+ const data = JSON.parse(await readFile(cache.path, "utf8")) as {
77
+ version?: unknown;
78
+ entries?: unknown;
79
+ };
80
+ if (data.version !== 1 || !Array.isArray(data.entries) || data.entries.length > maxEntries)
81
+ return cache;
82
+ for (const row of data.entries) {
83
+ if (
84
+ !Array.isArray(row) ||
85
+ row.length !== 2 ||
86
+ typeof row[0] !== "string" ||
87
+ row[0].length > 4096 ||
88
+ !validEntry(row[1])
89
+ )
90
+ continue;
91
+ cache.entries.set(row[0], row[1]);
92
+ }
93
+ } catch {
94
+ /* Missing or invalid cache means reread, never failed freshness. */
95
+ }
96
+ return cache;
97
+ }
98
+
99
+ async read(
100
+ path: string,
101
+ source: string,
102
+ ): Promise<{ entry: Entry; metadata: BigIntStats; reused: boolean } | null> {
103
+ const origin = resolve(path);
104
+ if (this.seen.size < maxEntries) this.seen.add(origin);
105
+ try {
106
+ for (let attempt = 0; attempt < 3; attempt += 1) {
107
+ const before = await stat(origin, { bigint: true });
108
+ const key = identity(before);
109
+ const settled = Date.now() - Number(before.ctimeMs) > 2_000;
110
+ const previous = this.entries.get(origin);
111
+ if (!this.verify && settled && previous?.settled && previous.metadata === key) {
112
+ this.reused += 1;
113
+ return { entry: previous, metadata: before, reused: true };
114
+ }
115
+ const bytes = await readFile(origin);
116
+ this.reads += 1;
117
+ if (identity(await stat(origin, { bigint: true })) !== key) continue;
118
+ const entry: Entry = {
119
+ metadata: key,
120
+ settled,
121
+ revision: revision(bytes.toString("utf8"), source),
122
+ contentHash: createHash("sha256").update(bytes).digest("hex"),
123
+ };
124
+ if (
125
+ !this.verify &&
126
+ this.seen.has(origin) &&
127
+ (this.entries.has(origin) || this.entries.size < maxEntries)
128
+ ) {
129
+ this.entries.set(origin, entry);
130
+ this.dirty = true;
131
+ }
132
+ return { entry, metadata: before, reused: false };
133
+ }
134
+ throw new Error(`Canonical source changed repeatedly during revision selection: ${source}`);
135
+ } catch (error) {
136
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
137
+ this.entries.delete(origin);
138
+ this.dirty = true;
139
+ return null;
140
+ }
141
+ throw error;
142
+ }
143
+ }
144
+
145
+ async persist(): Promise<void> {
146
+ if (this.verify) return;
147
+ for (const origin of this.entries.keys()) {
148
+ if (!this.seen.has(origin)) {
149
+ this.entries.delete(origin);
150
+ this.dirty = true;
151
+ }
152
+ }
153
+ if (!this.dirty) return;
154
+ const retained = [...this.entries].slice(-maxEntries);
155
+ const data = JSON.stringify({ version: 1, entries: retained });
156
+ if (Buffer.byteLength(data) > maxBytes) return;
157
+ const temporary = `${this.path}.${randomUUID()}.tmp`;
158
+ try {
159
+ await mkdir(dirname(this.path), { recursive: true });
160
+ await writeFile(temporary, data, { flag: "wx" });
161
+ await rename(temporary, this.path);
162
+ } catch {
163
+ /* Cache persistence must never prevent canonical verification. */
164
+ } finally {
165
+ await rm(temporary, { force: true }).catch(() => undefined);
166
+ }
167
+ }
168
+ }
@@ -0,0 +1,57 @@
1
+ import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { WorkspaceFileSystem } from "@zaip-ai/core";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { reconcileOpenWork } from "./server-workspace.js";
6
+
7
+ const roots: string[] = [];
8
+ async function fixture(script: string) {
9
+ const base = resolve(import.meta.dirname, "../../../../../../.tmp/dashboard-cli-diagnostics");
10
+ await mkdir(base, { recursive: true });
11
+ const root = await mkdtemp(join(base, "fixture-"));
12
+ roots.push(root);
13
+ await mkdir(join(root, ".agents/runtime/bin"), { recursive: true });
14
+ await writeFile(join(root, ".agents/runtime/bin/zaip.mjs"), script);
15
+ return root;
16
+ }
17
+ async function logs(root: string) {
18
+ const directory = join(root, ".local/dashboard-logs");
19
+ return (
20
+ await Promise.all(
21
+ (await readdir(directory)).map((name) => readFile(join(directory, name), "utf8")),
22
+ )
23
+ ).join("");
24
+ }
25
+ afterEach(async () => {
26
+ await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
27
+ });
28
+ describe("reconciliation CLI diagnostics", () => {
29
+ it("persists real CLI stdout and stderr plus structured outcomes with job correlation", async () => {
30
+ const root = await fixture(
31
+ 'process.stderr.write("diagnostic warning\\n"); process.stdout.write(JSON.stringify({ runs: [{run:{run_id:"RUN-X"},status:"failed",finished:{status:"failed"}}] }));',
32
+ );
33
+ const result = await reconcileOpenWork(new WorkspaceFileSystem(root), root, {
34
+ jobId: "JOB-REAL",
35
+ });
36
+ expect(result.outcomes).toEqual([{ id: "RUN-X", status: "failed", reason: null }]);
37
+ const content = await logs(root);
38
+ expect(content).toContain('"job_id":"JOB-REAL"');
39
+ expect(content).toContain("diagnostic warning");
40
+ expect(content).toContain("cli_completed");
41
+ });
42
+ it("retains failed CLI diagnostics and redacts secrets from stderr", async () => {
43
+ const root = await fixture(
44
+ 'process.stderr.write("Bearer sensitive password=private\\n"); process.exit(2);',
45
+ );
46
+ await expect(reconcileOpenWork(new WorkspaceFileSystem(root), root)).rejects.toThrow();
47
+ const content = await logs(root);
48
+ expect(content).toContain("cli_error");
49
+ expect(content).not.toMatch(/sensitive|private/);
50
+ expect(content).toContain("[redacted]");
51
+ });
52
+ it("reports invalid CLI JSON as failure with retained output", async () => {
53
+ const root = await fixture('process.stdout.write("invalid-json");');
54
+ await expect(reconcileOpenWork(new WorkspaceFileSystem(root), root)).rejects.toThrow();
55
+ expect(await logs(root)).toContain("cli_invalid_result");
56
+ });
57
+ });