@zaip-ai/cli 4.4.0 → 4.6.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 (214) hide show
  1. package/README.md +2 -0
  2. package/dist/services/adapters.d.ts +1 -1
  3. package/dist/services/adapters.js +4 -4
  4. package/dist/services/adapters.js.map +1 -1
  5. package/dist/services/codex-conversation-lifecycle-hook.d.ts.map +1 -1
  6. package/dist/services/codex-conversation-lifecycle-hook.js +24 -5
  7. package/dist/services/codex-conversation-lifecycle-hook.js.map +1 -1
  8. package/dist/services/codex-native-tool-authority-template.d.ts.map +1 -1
  9. package/dist/services/codex-native-tool-authority-template.js +188 -9
  10. package/dist/services/codex-native-tool-authority-template.js.map +1 -1
  11. package/dist/services/codex-pre-tool-hook-template.d.ts.map +1 -1
  12. package/dist/services/codex-pre-tool-hook-template.js +50 -128
  13. package/dist/services/codex-pre-tool-hook-template.js.map +1 -1
  14. package/dist/services/codex-pre-tool-hook-template.test.js +4 -0
  15. package/dist/services/codex-pre-tool-hook-template.test.js.map +1 -1
  16. package/dist/services/codex-pre-tool-support-template.d.ts.map +1 -1
  17. package/dist/services/codex-pre-tool-support-template.js +1 -4
  18. package/dist/services/codex-pre-tool-support-template.js.map +1 -1
  19. package/dist/services/codex-stale-git-lock-template.d.ts +2 -0
  20. package/dist/services/codex-stale-git-lock-template.d.ts.map +1 -0
  21. package/dist/services/codex-stale-git-lock-template.js +64 -0
  22. package/dist/services/codex-stale-git-lock-template.js.map +1 -0
  23. package/dist/services/global-codex-config-migration.test.js +1 -4
  24. package/dist/services/global-codex-config-migration.test.js.map +1 -1
  25. package/dist/services/install-template-policy.d.ts.map +1 -1
  26. package/dist/services/install-template-policy.js +2 -0
  27. package/dist/services/install-template-policy.js.map +1 -1
  28. package/dist/services/logger.js +2 -1
  29. package/dist/services/logger.js.map +1 -1
  30. package/dist/services/logger.test.js +71 -0
  31. package/dist/services/logger.test.js.map +1 -1
  32. package/dist/services/runtime-template-test-support.d.ts +3 -0
  33. package/dist/services/runtime-template-test-support.d.ts.map +1 -0
  34. package/dist/services/runtime-template-test-support.js +32 -0
  35. package/dist/services/runtime-template-test-support.js.map +1 -0
  36. package/dist/services/runtime-template.test.js +23 -31
  37. package/dist/services/runtime-template.test.js.map +1 -1
  38. package/package.json +1 -1
  39. package/templates/.agents/README.md +8 -0
  40. package/templates/.agents/config/agents/discovery.json +1 -1
  41. package/templates/.agents/config/deploy-policy.json +5 -0
  42. package/templates/.agents/dashboard/.zaip-managed.json +211 -75
  43. package/templates/.agents/dashboard/README.md +2 -0
  44. package/templates/.agents/dashboard/apps/frontend/dist/assets/{CostCalculationGuidePage-f4ZGJRg3.js → CostCalculationGuidePage-BRtiPijr.js} +1 -1
  45. package/templates/.agents/dashboard/apps/frontend/dist/assets/{DashboardHelpDialog-CkQ_y8kg.js → DashboardHelpDialog-BR4ZeYpE.js} +99 -10
  46. package/templates/.agents/dashboard/apps/frontend/dist/assets/{PricingSettingsDialog-v9zGRTlk.js → PricingSettingsDialog-Dr4QDFmn.js} +1 -1
  47. package/templates/.agents/dashboard/apps/frontend/dist/assets/{ProjectSettingsPage-doIMhei7.js → ProjectSettingsPage-BG0N5dw_.js} +1 -1
  48. package/templates/.agents/dashboard/apps/frontend/dist/assets/{TraceabilityWorkspace-C_4ftjyB.js → TraceabilityWorkspace-BvoM-YQ0.js} +1 -1
  49. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-fTQhSL85.js +68 -0
  50. package/templates/.agents/dashboard/apps/frontend/dist/assets/{icons-vendor-Cv3WBFIF.js → icons-vendor-Bc6vux5t.js} +1 -1
  51. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-BMfzKaAR.js +2 -0
  52. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-DZhppOIk.css +1 -0
  53. package/templates/.agents/dashboard/apps/frontend/dist/assets/{react-vendor-mHgsmxb6.js → react-vendor-DNJQFov3.js} +1 -1
  54. package/templates/.agents/dashboard/apps/frontend/dist/assets/{traceability-B0CPyEVq.js → traceability-B7tjIB7W.js} +1 -1
  55. package/templates/.agents/dashboard/apps/frontend/dist/index.html +6 -6
  56. package/templates/.agents/dashboard/apps/frontend/package.json +1 -1
  57. package/templates/.agents/dashboard/apps/frontend/src/AppOverlays.tsx +6 -34
  58. package/templates/.agents/dashboard/apps/frontend/src/ReconciliationFeedback.dom.test.ts +130 -0
  59. package/templates/.agents/dashboard/apps/frontend/src/ReconciliationFeedback.tsx +226 -0
  60. package/templates/.agents/dashboard/apps/frontend/src/dashboard-api-sqlite-view.ts +45 -0
  61. package/templates/.agents/dashboard/apps/frontend/src/dashboard-api.test.ts +15 -0
  62. package/templates/.agents/dashboard/apps/frontend/src/dashboard-api.ts +17 -46
  63. package/templates/.agents/dashboard/apps/frontend/src/reconciliation-feedback-fixtures.ts +23 -0
  64. package/templates/.agents/dashboard/apps/frontend/src/reconciliation-feedback-model.test.ts +69 -0
  65. package/templates/.agents/dashboard/apps/frontend/src/reconciliation-feedback-model.ts +59 -0
  66. package/templates/.agents/dashboard/apps/frontend/src/reconciliation-feedback.css +131 -0
  67. package/templates/.agents/dashboard/apps/frontend/src/styles.css +0 -32
  68. package/templates/.agents/dashboard/apps/frontend/src/use-dashboard-app-model.tsx +2 -2
  69. package/templates/.agents/dashboard/modules/cost-monitor/package.json +1 -1
  70. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.test.ts +2 -2
  71. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.tsx +1 -1
  72. package/templates/.agents/dashboard/modules/operations/package.json +1 -1
  73. package/templates/.agents/dashboard/modules/operations/src/react/OperationsPage.tsx +1 -4
  74. package/templates/.agents/dashboard/modules/operations/src/react/UnifiedWorkBoard.navigation.test.ts +236 -0
  75. package/templates/.agents/dashboard/modules/operations/src/react/UnifiedWorkBoard.tsx +1 -24
  76. package/templates/.agents/dashboard/modules/operations/src/react/operations-overview-period-filter.test.ts +3 -2
  77. package/templates/.agents/dashboard/modules/traceability/package.json +1 -1
  78. package/templates/.agents/dashboard/packages/branding/package.json +1 -1
  79. package/templates/.agents/dashboard/packages/contracts/package.json +1 -1
  80. package/templates/.agents/dashboard/packages/data-export/package.json +1 -1
  81. package/templates/.agents/dashboard/packages/local-runtime/DIAGNOSTICS.md +42 -0
  82. package/templates/.agents/dashboard/packages/local-runtime/dashboard-diagnostics.d.mts +10 -0
  83. package/templates/.agents/dashboard/packages/local-runtime/dashboard-diagnostics.mjs +101 -0
  84. package/templates/.agents/dashboard/packages/local-runtime/dashboard-diagnostics.test.mjs +58 -0
  85. package/templates/.agents/dashboard/packages/local-runtime/dist/server.cjs +232 -217
  86. package/templates/.agents/dashboard/packages/local-runtime/dist/sqlite-read-model.cjs +201 -188
  87. package/templates/.agents/dashboard/packages/local-runtime/package.json +1 -1
  88. package/templates/.agents/dashboard/packages/local-runtime/scripts/test-sqlite-dashboard-startup.mjs +233 -0
  89. package/templates/.agents/dashboard/packages/local-runtime/scripts/validate-provider-nonzero.mjs +45 -0
  90. package/templates/.agents/dashboard/packages/local-runtime/scripts/verify-dashboard-executable-startup.mjs +107 -0
  91. package/templates/.agents/dashboard/packages/local-runtime/scripts/verify-dashboard-reconciliation-executable.mjs +271 -0
  92. package/templates/.agents/dashboard/packages/local-runtime/server.mjs +232 -217
  93. package/templates/.agents/dashboard/packages/local-runtime/src/active-codex-telemetry.test.ts +78 -12
  94. package/templates/.agents/dashboard/packages/local-runtime/src/active-codex-telemetry.ts +36 -56
  95. package/templates/.agents/dashboard/packages/local-runtime/src/canonical-source-revision-cache.test.ts +125 -0
  96. package/templates/.agents/dashboard/packages/local-runtime/src/canonical-source-revision-cache.ts +168 -0
  97. package/templates/.agents/dashboard/packages/local-runtime/src/dashboard-diagnostics.test.ts +57 -0
  98. package/templates/.agents/dashboard/packages/local-runtime/src/server-agent-catalog-routes.ts +76 -0
  99. package/templates/.agents/dashboard/packages/local-runtime/src/server-dual-reads-manifest-summaries-without-losing.test.ts +5 -0
  100. package/templates/.agents/dashboard/packages/local-runtime/src/server-reconciliation-diagnostics.test.ts +118 -0
  101. package/templates/.agents/dashboard/packages/local-runtime/src/server-reconciliation-result.test.ts +35 -6
  102. package/templates/.agents/dashboard/packages/local-runtime/src/server-reconciliation-route.ts +105 -0
  103. package/templates/.agents/dashboard/packages/local-runtime/src/server-snapshot-synchronization.test.ts +64 -9
  104. package/templates/.agents/dashboard/packages/local-runtime/src/server-snapshot-synchronization.ts +19 -27
  105. package/templates/.agents/dashboard/packages/local-runtime/src/server-telemetry-command.test.ts +111 -0
  106. package/templates/.agents/dashboard/packages/local-runtime/src/server-telemetry-refresh.test.ts +59 -0
  107. package/templates/.agents/dashboard/packages/local-runtime/src/server-telemetry-refresh.ts +44 -0
  108. package/templates/.agents/dashboard/packages/local-runtime/src/server-work-reconciliation.test.ts +20 -0
  109. package/templates/.agents/dashboard/packages/local-runtime/src/server-work-reconciliation.ts +12 -7
  110. package/templates/.agents/dashboard/packages/local-runtime/src/server-workspace.ts +48 -6
  111. package/templates/.agents/dashboard/packages/local-runtime/src/server.test.ts +2 -2
  112. package/templates/.agents/dashboard/packages/local-runtime/src/server.ts +126 -134
  113. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-agent-page.ts +14 -2
  114. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-dashboard-startup.test.ts +163 -0
  115. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-dashboard-startup.ts +78 -0
  116. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-header-timestamp.test.ts +31 -0
  117. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-provider-nonzero.test.ts +271 -0
  118. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-provider-visibility.ts +32 -0
  119. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-query-operations-duration.ts +58 -0
  120. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-query-provider-dimension.ts +12 -3
  121. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-query-view-page.ts +9 -58
  122. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-rebuild.ts +9 -35
  123. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-schema.ts +10 -8
  124. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-source-fingerprint.ts +43 -45
  125. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.test.ts +5 -9
  126. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.ts +5 -1
  127. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-startup-seed.test.ts +87 -0
  128. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-logging.mjs +6 -2
  129. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-server.mjs +7 -1
  130. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.mjs +4 -5
  131. package/templates/.agents/dashboard/packages/ui/package.json +1 -1
  132. package/templates/.agents/dashboard/packages/ui/src/DashboardHeader.test.mjs +33 -4
  133. package/templates/.agents/dashboard/packages/ui/src/DashboardHeader.tsx +8 -7
  134. package/templates/.agents/dashboard/packages/windows-launcher/Program.cs +46 -3
  135. package/templates/.agents/dashboard/tests/e2e/package.json +1 -1
  136. package/templates/.agents/dashboard/tests/e2e/qualify-kanban-popup.mjs +56 -0
  137. package/templates/.agents/dashboard/tests/e2e/specs/dashboard-update-label.spec.ts +49 -0
  138. package/templates/.agents/design/.zaip-managed.json +17 -5
  139. package/templates/.agents/design/apps/catalog/dist/assets/index-D3uhpycC.js +72 -0
  140. package/templates/.agents/design/apps/catalog/dist/index.html +1 -1
  141. package/templates/.agents/design/apps/catalog/package.json +1 -1
  142. package/templates/.agents/design/apps/catalog/src/pages/ComponentsPage.tsx +13 -1
  143. package/templates/.agents/design/apps/catalog/src/specimens/ReconciliationSpecimen.stories.tsx +6 -0
  144. package/templates/.agents/design/apps/catalog/src/specimens/ReconciliationSpecimen.tsx +101 -0
  145. package/templates/.agents/design/packages/local-runtime/package.json +1 -1
  146. package/templates/.agents/requirements/autonomous-assurance.md +1 -1
  147. package/templates/.agents/requirements/cloud-hostinger-autonomy.md +1 -1
  148. package/templates/.agents/requirements/comparative-audit.md +1 -1
  149. package/templates/.agents/requirements/context-engine.md +1 -1
  150. package/templates/.agents/requirements/cost-observability.md +1 -1
  151. package/templates/.agents/requirements/dashboard-launch.md +1 -1
  152. package/templates/.agents/requirements/data-export.md +1 -1
  153. package/templates/.agents/requirements/design-system.md +1 -1
  154. package/templates/.agents/requirements/execution-engine.md +1 -1
  155. package/templates/.agents/requirements/kanban-dashboard.md +1 -1
  156. package/templates/.agents/requirements/model-orchestration.md +21 -6
  157. package/templates/.agents/requirements/public-distribution.md +4 -4
  158. package/templates/.agents/requirements/software-architecture.md +1 -1
  159. package/templates/.agents/requirements/strategic-improvements.md +1 -1
  160. package/templates/.agents/requirements/work-lifecycle.md +1 -1
  161. package/templates/.agents/requirements/zaip-strategic-95.md +1 -1
  162. package/templates/.agents/rules/core.md +3 -1
  163. package/templates/.agents/rules/workflow.md +3 -2
  164. package/templates/.agents/runtime/VERSION +1 -1
  165. package/templates/.agents/runtime/bin/zaip.mjs +705 -516
  166. package/templates/.agents/runtime/checksums.sha256 +5 -5
  167. package/templates/.agents/runtime/manifest.json +1 -1
  168. package/templates/.agents/runtime/provenance.json +4 -4
  169. package/templates/.agents/runtime/sbom.cdx.json +1 -1
  170. package/templates/.agents/skills/zaip-skill-config/SKILL.md +25 -3
  171. package/templates/.agents/skills/zaip-skill-execution/SKILL.md +4 -0
  172. package/templates/.agents/skills/zaip-skill-execution/references/autonomous-continuation.md +4 -0
  173. package/templates/.agents/skills/zaip-skill-execution/references/candidate-workflow.md +1 -1
  174. package/templates/.agents/skills/zaip-skill-execution/references/runtime-governance.md +2 -2
  175. package/templates/.agents/skills/zaip-skill-general-operations/SKILL.md +1 -0
  176. package/templates/.agents/skills/zaip-skill-github-deploy/SKILL.md +36 -48
  177. package/templates/.agents/skills/zaip-skill-github-deploy/agents/openai.yaml +2 -2
  178. package/templates/.agents/skills/zaip-skill-github-deploy/references/deployment-workflow.md +67 -99
  179. package/templates/.agents/skills/zaip-skill-github-deploy/references/release-identity.md +33 -30
  180. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/deploy-contract.mjs +303 -0
  181. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/deploy-contract.test.mjs +334 -0
  182. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/github-deploy-contract.test.mjs +41 -28
  183. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/guarded-commits.mjs +212 -0
  184. package/templates/.agents/skills/zaip-skill-lifecycle-closure/SKILL.md +4 -0
  185. package/templates/.agents/skills/zaip-skill-prd/SKILL.md +4 -0
  186. package/templates/.agents/skills/zaip-skill-release-qualification/SKILL.md +44 -68
  187. package/templates/.agents/skills/zaip-skill-release-qualification/references/local-preparation.md +91 -44
  188. package/templates/.agents/skills/zaip-skill-send-email/SKILL.md +15 -0
  189. package/templates/.agents/skills/zaip-skill-send-email/agents/openai.yaml +4 -0
  190. package/templates/.agents/skills/zaip-skill-setup/SKILL.md +5 -2
  191. package/templates/.agents/skills/zaip-skill-spec/SKILL.md +4 -0
  192. package/templates/.agents/skills/zaip-skill-task/SKILL.md +4 -0
  193. package/templates/.agents/skills/zaip-skill-testing-strategy/SKILL.md +19 -0
  194. package/templates/.agents/skills/zaip-skill-tool-setup/SKILL.md +2 -0
  195. package/templates/.agents/skills/zaip-skill-use-connectors/SKILL.md +15 -0
  196. package/templates/.agents/skills/zaip-skill-use-connectors/agents/openai.yaml +4 -0
  197. package/templates/.agents/skills/zaip-skill-use-connectors/scripts/connector-guidance.test.mjs +29 -0
  198. package/templates/.agents/skills/zaip-skill-use-connectors/scripts/verify-connector-lock.mjs +30 -0
  199. package/templates/.agents/tools/cloud-runner.integration.test.mjs +3 -0
  200. package/templates/.agents/zaip-aios/VERSION +1 -1
  201. package/templates/.agents/zaip-aios/guides/03-tasks.md +6 -0
  202. package/templates/.agents/zaip-aios/guides/04-specs.md +6 -0
  203. package/templates/.agents/zaip-aios/guides/05-prds.md +4 -0
  204. package/templates/.agents/zaip-aios/guides/06-execucao.md +6 -0
  205. package/templates/.agents/zaip-aios/guides/16-lifecycle-e-reconciliacao.md +6 -0
  206. package/templates/.agents/zaip-aios/guides/19-assurance-e-autonomia.md +4 -0
  207. package/templates/.agents/zaip-aios/guides/22-fluxos-e-roteamento.md +6 -0
  208. package/templates/.agents/zaip-aios/guides/23-orquestracao-de-modelos.md +36 -4
  209. package/templates/.agents/zaip-aios/guides/26-qualidade-e-release.md +15 -4
  210. package/templates/.agents/zaip-aios/guides/28-lifecycle-completo.md +8 -0
  211. package/templates/template-manifest.json +330 -142
  212. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-D7PrDZy9.js +0 -68
  213. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-OBJXZOwe.css +0 -1
  214. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-Rbmpz4UO.js +0 -2
@@ -0,0 +1,111 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { createDashboardApp } from "./server.js";
5
+ import { withSqliteReadModelFixture } from "./server.test-fixtures.js";
6
+
7
+ describe("recent telemetry command readiness", () => {
8
+ it("requires a POST capability and stays updating until capture and the SQLite commit finish", async () => {
9
+ await withSqliteReadModelFixture("telemetry-open", async ({ root, filesystem }) => {
10
+ await mkdir(join(root, ".agents/records/lifecycle"), { recursive: true });
11
+ await writeFile(
12
+ join(root, ".agents/records/lifecycle/manifest.json"),
13
+ JSON.stringify({
14
+ schema_version: 1,
15
+ updated_at: new Date().toISOString(),
16
+ offsets: {},
17
+ }),
18
+ );
19
+ await writeFile(
20
+ join(root, ".agents/records/lifecycle/summary.json"),
21
+ JSON.stringify({
22
+ schema_version: 1,
23
+ updated_at: new Date().toISOString(),
24
+ counts: { total: 0, by_kind: {}, by_status: {} },
25
+ freshness: {},
26
+ usage: {},
27
+ periods: {},
28
+ }),
29
+ );
30
+ let releaseCapture!: () => void;
31
+ let releaseCommit!: () => void;
32
+ const capture = vi.fn(async () => {
33
+ await new Promise<void>((resolve) => {
34
+ releaseCapture = resolve;
35
+ });
36
+ return {
37
+ captured_runs: 1,
38
+ captured_segments: null,
39
+ manifest_state: "initialized" as const,
40
+ skipped_reason: null,
41
+ last_capture_at: new Date().toISOString(),
42
+ };
43
+ });
44
+ const commit = vi.fn(async () => {
45
+ await new Promise<void>((resolve) => {
46
+ releaseCommit = resolve;
47
+ });
48
+ });
49
+ const reconcile = vi.fn();
50
+ const app = createDashboardApp(root, "session", undefined, {
51
+ filesystem,
52
+ captureTelemetry: capture,
53
+ refreshReadModel: commit,
54
+ reconcileOpenWork: reconcile,
55
+ });
56
+ const headers = { "x-zaip-session": "session" };
57
+ const ready = async () => (await app.request("/api/v2/ready", { headers })).json();
58
+ expect((await app.request("/api/v2/capture-telemetry", { method: "POST" })).status).toBe(401);
59
+ expect(capture).not.toHaveBeenCalled();
60
+ const response = await app.request("/api/v2/capture-telemetry", { method: "POST", headers });
61
+ expect(response.status).toBe(202);
62
+ expect(await response.json()).toEqual({ snapshot_state: "updating" });
63
+ await expect(ready()).resolves.toMatchObject({ snapshot_state: "updating" });
64
+ // Passive probes cannot start another capture. A second opening shares it.
65
+ await ready();
66
+ await app.request("/api/v2/capture-telemetry", { method: "POST", headers });
67
+ expect(capture).toHaveBeenCalledOnce();
68
+ expect(commit).not.toHaveBeenCalled();
69
+ releaseCapture();
70
+ await vi.waitFor(() => expect(commit).toHaveBeenCalledOnce());
71
+ await expect(ready()).resolves.toMatchObject({ snapshot_state: "updating" });
72
+ releaseCommit();
73
+ await vi.waitFor(async () =>
74
+ expect(await ready()).toMatchObject({
75
+ status: "ready",
76
+ snapshot_state: "current",
77
+ active_capture: { captured_segments: null },
78
+ }),
79
+ );
80
+ expect(reconcile).not.toHaveBeenCalled();
81
+ });
82
+ });
83
+
84
+ it("cannot advertise current after a failed capture even when canonical projection succeeds", async () => {
85
+ await withSqliteReadModelFixture("telemetry-failed", async ({ root, filesystem }) => {
86
+ const commit = vi.fn(async () => undefined);
87
+ const app = createDashboardApp(root, "session", undefined, {
88
+ filesystem,
89
+ refreshReadModel: commit,
90
+ captureTelemetry: async () => {
91
+ throw new Error("transcript timed out");
92
+ },
93
+ });
94
+ const headers = { "x-zaip-session": "session" };
95
+ await app.request("/api/v2/capture-telemetry", { method: "POST", headers });
96
+ await vi.waitFor(() => expect(commit).toHaveBeenCalledOnce());
97
+ const ready = await app.request("/api/v2/ready", { headers });
98
+ expect(await ready.json()).toMatchObject({
99
+ status: "degraded",
100
+ snapshot_state: "updating",
101
+ active_capture_failure: { code: "active_telemetry_capture_failed" },
102
+ });
103
+ const manual = await app.request("/api/v2/refresh-projection", { method: "POST", headers });
104
+ expect(manual.status).toBe(503);
105
+ expect(await manual.json()).toMatchObject({
106
+ error: "Telemetry capture is unavailable",
107
+ snapshot_state: "updating",
108
+ });
109
+ });
110
+ });
111
+ });
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createTelemetryProjectionRefresh } from "./server-telemetry-refresh.js";
3
+
4
+ describe("telemetry before projection", () => {
5
+ it("shares startup/browser/refresh requests and cannot finish before capture or rebuild", async () => {
6
+ let finishCapture!: () => void;
7
+ let finishProjection!: () => void;
8
+ const order: string[] = [];
9
+ const capture = vi.fn(async () => {
10
+ order.push("capture");
11
+ await new Promise<void>((resolve) => {
12
+ finishCapture = resolve;
13
+ });
14
+ return {
15
+ captured_runs: 1,
16
+ captured_segments: null,
17
+ manifest_state: "initialized" as const,
18
+ skipped_reason: null,
19
+ last_capture_at: new Date().toISOString(),
20
+ };
21
+ });
22
+ const refresh = vi.fn(async () => {
23
+ order.push("projection");
24
+ await new Promise<void>((resolve) => {
25
+ finishProjection = resolve;
26
+ });
27
+ });
28
+ const sync = createTelemetryProjectionRefresh({ capture, refresh });
29
+ const startup = sync.refresh();
30
+ const browser = sync.refresh();
31
+ expect(browser).toBe(startup);
32
+ expect(sync.pending).toBe(true);
33
+ expect(order).toEqual(["capture"]);
34
+ finishCapture();
35
+ await vi.waitFor(() => expect(refresh).toHaveBeenCalledOnce());
36
+ expect(sync.pending).toBe(true);
37
+ finishProjection();
38
+ await startup;
39
+ expect(sync.pending).toBe(false);
40
+ expect(order).toEqual(["capture", "projection"]);
41
+ });
42
+
43
+ it("retains a failed capture while projecting canonical data and allows retry", async () => {
44
+ const failure = new Error("unavailable telemetry");
45
+ const onCaptureFailure = vi.fn();
46
+ const refresh = vi.fn(async () => undefined);
47
+ const sync = createTelemetryProjectionRefresh({
48
+ capture: vi.fn().mockRejectedValue(failure),
49
+ refresh,
50
+ onCaptureFailure,
51
+ });
52
+ await sync.refresh();
53
+ expect(onCaptureFailure).toHaveBeenCalledWith(failure);
54
+ expect(refresh).toHaveBeenCalledOnce();
55
+ expect(sync.pending).toBe(false);
56
+ await sync.refresh();
57
+ expect(refresh).toHaveBeenCalledTimes(2);
58
+ });
59
+ });
@@ -0,0 +1,44 @@
1
+ import type { ActiveCodexTelemetryCapture } from "./active-codex-telemetry.js";
2
+
3
+ /** Startup, explicit browser opening, and manual refresh share one writer. */
4
+ export function createTelemetryProjectionRefresh(input: {
5
+ capture?: () => Promise<ActiveCodexTelemetryCapture>;
6
+ refresh: () => Promise<void>;
7
+ onCaptureSuccess?: (result: ActiveCodexTelemetryCapture) => void;
8
+ onCaptureFailure?: (error: unknown) => void;
9
+ }) {
10
+ let inFlight: Promise<void> | null = null;
11
+ let projecting = false;
12
+ const refresh = (catchUp = false): Promise<void> => {
13
+ if (inFlight !== null) {
14
+ // An explicit refresh may observe a source change after the SQLite
15
+ // worker started. Preserve the projection's bounded catch-up request.
16
+ if (catchUp && projecting) void input.refresh().catch(() => undefined);
17
+ return inFlight;
18
+ }
19
+ inFlight = (async () => {
20
+ try {
21
+ if (input.capture !== undefined) {
22
+ const result = await input.capture();
23
+ input.onCaptureSuccess?.(result);
24
+ }
25
+ } catch (error) {
26
+ input.onCaptureFailure?.(error);
27
+ }
28
+ // Keep canonical records readable even when telemetry is unavailable.
29
+ // The caller retains the capture failure separately from SQLite health.
30
+ projecting = true;
31
+ await input.refresh();
32
+ })().finally(() => {
33
+ projecting = false;
34
+ inFlight = null;
35
+ });
36
+ return inFlight;
37
+ };
38
+ return {
39
+ refresh,
40
+ get pending() {
41
+ return inFlight !== null;
42
+ },
43
+ };
44
+ }
@@ -13,6 +13,26 @@ const changed: ReconciliationResult = {
13
13
  afterEach(() => vi.useRealTimers());
14
14
 
15
15
  describe("canonical host recovery", () => {
16
+ it("associates deduplicated queued requests with the original job instead of the preceding job", async () => {
17
+ let release!: (value: ReconciliationResult) => void;
18
+ const first = new Promise<ReconciliationResult>((resolve) => {
19
+ release = resolve;
20
+ });
21
+ const reconcile = vi.fn().mockReturnValueOnce(first).mockResolvedValue(clean);
22
+ const run = createWorkReconciliation({ reconcile, refresh: async () => undefined });
23
+ const previous = run("host-terminal", "JOB-HOST");
24
+ const queued = run("manual", "JOB-MANUAL");
25
+ const deduplicated = run("manual", "JOB-SECOND-CLICK");
26
+ expect(run.jobIdFor(previous)).toBe("JOB-HOST");
27
+ expect(run.jobIdFor(queued)).toBe("JOB-MANUAL");
28
+ expect(run.jobIdFor(deduplicated)).toBe("JOB-MANUAL");
29
+ release(clean);
30
+ await queued;
31
+ expect(reconcile.mock.calls).toEqual([
32
+ ["host-terminal", "JOB-HOST"],
33
+ ["manual", "JOB-MANUAL"],
34
+ ]);
35
+ });
16
36
  it("queues manual recovery after host-only work and deduplicates concurrent clicks", async () => {
17
37
  let release!: (value: ReconciliationResult) => void;
18
38
  const first = new Promise<ReconciliationResult>((resolve) => {
@@ -34,13 +34,14 @@ export async function waitForWorkReconciliation(
34
34
 
35
35
  /** Serialize canonical mutations from the button and the one-shot host recovery. */
36
36
  export function createWorkReconciliation(input: {
37
- reconcile: (mode: ReconciliationMode) => Promise<ReconciliationResult>;
38
- refresh: () => Promise<void>;
37
+ reconcile: (mode: ReconciliationMode, jobId?: string) => Promise<ReconciliationResult>;
38
+ refresh: (jobId?: string) => Promise<void>;
39
39
  }) {
40
40
  let tail = Promise.resolve();
41
41
  const pending = new Map<ReconciliationMode, Promise<ReconciliationResult>>();
42
- const refresh = () => void input.refresh().catch(() => undefined);
43
- return (mode: ReconciliationMode): Promise<ReconciliationResult> => {
42
+ const jobIds = new WeakMap<Promise<ReconciliationResult>, string>();
43
+ const refresh = (jobId?: string) => void input.refresh(jobId).catch(() => undefined);
44
+ const run = (mode: ReconciliationMode, jobId?: string): Promise<ReconciliationResult> => {
44
45
  // A manual command is an explicit operator action. It may wait for an
45
46
  // already-writing command, but it must never be represented by the host
46
47
  // recovery promise: that made the dialog appear to run forever while the
@@ -48,24 +49,28 @@ export function createWorkReconciliation(input: {
48
49
  const current = pending.get(mode);
49
50
  if (current !== undefined) return current;
50
51
  const job = tail
51
- .then(() => input.reconcile(mode))
52
+ .then(() => (jobId === undefined ? input.reconcile(mode) : input.reconcile(mode, jobId)))
52
53
  .then(
53
54
  (result) => {
54
- if (result.reconciled_ids.length > 0 || result.blocked.length > 0) refresh();
55
+ if (result.reconciled_ids.length > 0 || result.blocked.length > 0) refresh(jobId);
55
56
  return result;
56
57
  },
57
58
  (error: unknown) => {
58
59
  // A failed command may already have persisted part of the graph.
59
- refresh();
60
+ refresh(jobId);
60
61
  throw error;
61
62
  },
62
63
  )
63
64
  .finally(() => pending.delete(mode));
64
65
  pending.set(mode, job);
66
+ if (jobId !== undefined) jobIds.set(job, jobId);
65
67
  tail = job.then(
66
68
  () => undefined,
67
69
  () => undefined,
68
70
  );
69
71
  return job;
70
72
  };
73
+ return Object.assign(run, {
74
+ jobIdFor: (job: Promise<ReconciliationResult>) => jobIds.get(job) ?? null,
75
+ });
71
76
  }
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
3
3
  import { resolve } from "node:path";
4
4
  import { promisify } from "node:util";
5
5
  import type { WorkspaceFileSystem } from "@zaip-ai/core";
6
+ import { createDashboardDiagnostics } from "../dashboard-diagnostics.mjs";
6
7
 
7
8
  const DASHBOARD_CONFIG_PATH = ".agents/dashboard/dashboard.config.json";
8
9
  const execFileAsync = promisify(execFile);
@@ -66,6 +67,15 @@ export interface ReconciliationResult {
66
67
  reconciled_ids: string[];
67
68
  blocked: ReconciliationItem[];
68
69
  not_reconciled: ReconciliationItem[];
70
+ outcomes?: Array<ReconciliationItem & { status: string }>;
71
+ diagnostics?: {
72
+ job_id: string;
73
+ started_at: string;
74
+ completed_at: string;
75
+ log_directory: string;
76
+ projection_updated_at: string | null;
77
+ latest_token_collected_at: string | null;
78
+ };
69
79
  }
70
80
 
71
81
  export const reconciliationCommandTimeoutMs = 170_000;
@@ -82,15 +92,19 @@ export class ReconciliationCommandTimeoutError extends Error {
82
92
  export async function reconcileOpenWork(
83
93
  filesystem: WorkspaceFileSystem,
84
94
  workspaceRoot: string,
85
- options: { hostTerminalOnly?: boolean } = {},
95
+ options: { hostTerminalOnly?: boolean; jobId?: string } = {},
86
96
  ): Promise<ReconciliationResult> {
97
+ const diagnostics = createDashboardDiagnostics(workspaceRoot, "reconciliation");
98
+ const job_id = options.jobId ?? diagnostics.correlationId;
99
+ diagnostics.log("cli_start", { job_id, host_terminal_only: options.hostTerminalOnly === true });
87
100
  if (!(await filesystem.exists(".agents/runtime/bin/zaip.mjs"))) {
101
+ diagnostics.log("cli_unavailable", { job_id });
88
102
  throw new Error("ZAIP runtime is unavailable; open work could not be reconciled.");
89
103
  }
90
104
  const cliPath = resolve(workspaceRoot, ".agents", "runtime", "bin", "zaip.mjs");
91
105
  let stdout: string;
92
106
  try {
93
- ({ stdout } = await execFileAsync(
107
+ const output = await execFileAsync(
94
108
  process.execPath,
95
109
  [
96
110
  cliPath,
@@ -107,20 +121,35 @@ export async function reconcileOpenWork(
107
121
  timeout: reconciliationCommandTimeoutMs,
108
122
  killSignal: "SIGTERM",
109
123
  },
110
- ));
124
+ );
125
+ stdout = output.stdout;
126
+ diagnostics.log("cli_output", { job_id, stdout, stderr: output.stderr });
111
127
  } catch (error) {
128
+ diagnostics.log("cli_error", { job_id, error });
112
129
  if (typeof error === "object" && error !== null && "killed" in error && error.killed === true) {
113
130
  throw new ReconciliationCommandTimeoutError();
114
131
  }
115
132
  throw error;
116
133
  }
117
- return reconciliationResult(JSON.parse(stdout) as unknown);
134
+ try {
135
+ const result = reconciliationResult(JSON.parse(stdout) as unknown);
136
+ diagnostics.log("cli_completed", { job_id, result });
137
+ return result;
138
+ } catch (error) {
139
+ diagnostics.log("cli_invalid_result", { job_id, error, stdout });
140
+ throw error;
141
+ }
118
142
  }
119
143
 
120
144
  export function reconciliationResult(value: unknown): ReconciliationResult {
121
145
  const reconciled = new Set<string>();
122
146
  const blocked = new Map<string, string | null>();
123
147
  const notReconciled = new Map<string, string | null>();
148
+ const outcomes = new Map<string, ReconciliationItem & { status: string }>();
149
+ const recordOutcome = (id: unknown, status: unknown, reason: unknown) => {
150
+ if (typeof id === "string" && typeof status === "string")
151
+ outcomes.set(id, { id, status, reason: typeof reason === "string" ? reason : null });
152
+ };
124
153
  const remember = (target: Map<string, string | null>, id: unknown, reason: unknown): void => {
125
154
  if (typeof id !== "string") return;
126
155
  target.set(id, typeof reason === "string" && reason.trim() ? reason : null);
@@ -145,6 +174,7 @@ export function reconciliationResult(value: unknown): ReconciliationResult {
145
174
  typeof record.id === "string"
146
175
  ) {
147
176
  reconciled.add(record.id);
177
+ recordOutcome(record.id, record.status, record.status_reason ?? reason);
148
178
  }
149
179
  if (record.status === "blocked") {
150
180
  remember(blocked, record.id, record.status_reason ?? reason);
@@ -159,8 +189,18 @@ export function reconciliationResult(value: unknown): ReconciliationResult {
159
189
  const closedRuns = Array.isArray(records.runs) ? records.runs : [];
160
190
  closedRuns.forEach((candidate) => {
161
191
  if (typeof candidate !== "object" || candidate === null) return;
162
- const runId = (candidate as { run?: { run_id?: unknown } }).run?.run_id;
163
- if (typeof runId === "string") reconciled.add(runId);
192
+ // CLI returns RunV2Fold: the started header is under run, terminal
193
+ // status/summary belong to the fold and its finished record.
194
+ const fold = candidate as {
195
+ run?: { run_id?: unknown };
196
+ status?: unknown;
197
+ finished?: { status?: unknown; summary?: unknown };
198
+ };
199
+ const run = fold.run;
200
+ if (typeof run?.run_id === "string") {
201
+ reconciled.add(run.run_id);
202
+ recordOutcome(run.run_id, fold.status ?? fold.finished?.status, fold.finished?.summary);
203
+ }
164
204
  });
165
205
  const orphanedExecutions = Array.isArray(records.orphaned_executions)
166
206
  ? records.orphaned_executions
@@ -183,6 +223,7 @@ export function reconciliationResult(value: unknown): ReconciliationResult {
183
223
  const entity = candidate as Record<string, unknown>;
184
224
  if (typeof entity.id !== "string" || !terminalStatuses.has(entity.status as string)) continue;
185
225
  reconciled.add(entity.id);
226
+ recordOutcome(entity.id, entity.status, entity.status_reason);
186
227
  if (entity.status === "blocked") remember(blocked, entity.id, entity.status_reason);
187
228
  }
188
229
  }
@@ -206,5 +247,6 @@ export function reconciliationResult(value: unknown): ReconciliationResult {
206
247
  reconciled_ids: [...reconciled].sort(),
207
248
  blocked: entries(blocked),
208
249
  not_reconciled: entries(notReconciled),
250
+ outcomes: [...outcomes.values()].sort((a, b) => a.id.localeCompare(b.id)),
209
251
  };
210
252
  }
@@ -283,6 +283,7 @@ describe("dashboard backend", () => {
283
283
  expect(reconcile).toHaveBeenCalledOnce();
284
284
  expect(reconcile).toHaveBeenCalledWith(canonicalFilesystem, workspaceRoot, {
285
285
  hostTerminalOnly: false,
286
+ jobId: expect.any(String),
286
287
  });
287
288
  expect(refresh).toHaveBeenCalledOnce();
288
289
  const rejected = await app.request("/api/v2/reconcile-open-work", { method: "POST" });
@@ -359,7 +360,6 @@ describe("dashboard backend", () => {
359
360
  summary: { overview_metrics: { task_status_counts: Record<string, number> } };
360
361
  };
361
362
  expect(beforePayload.summary.overview_metrics.task_status_counts.executing).toBe(1);
362
-
363
363
  await writeFile(taskIndexPath, `${JSON.stringify(task("completed"))}\n`);
364
364
  await writeFile(taskRecordPath, taskRecord("completed"));
365
365
  const previousSeedPreference = process.env.ZAIP_SQLITE_PREFER_LAST_VALID_SEED;
@@ -439,7 +439,7 @@ describe("dashboard backend", () => {
439
439
  });
440
440
  await expect(
441
441
  canonicalFilesystem.readText(".agents/dashboard/work-priority.json"),
442
- ).resolves.toContain(priority.item_ids[0]!);
442
+ ).resolves.toContain(priority.item_ids[0]);
443
443
  const readsAfterPut = priorityReads.mock.calls.filter(
444
444
  ([path]) => path === ".agents/dashboard/work-priority.json",
445
445
  ).length;