@zaip-ai/cli 4.9.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 (180) 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 +96 -210
  40. package/dist/services/codex-pre-tool-hook-template.js.map +1 -1
  41. package/dist/services/codex-pre-tool-hook-template.test.js +32 -80
  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 +61 -101
  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 +13 -2
  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 +10 -4
  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 +4 -23
  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 +37 -10
  66. package/templates/.agents/config/agents/discovery.json +1 -1
  67. package/templates/.agents/dashboard/.zaip-managed.json +121 -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-Cp_hEVd8.js → DashboardHelpDialog-DhlntQcR.js} +76 -11
  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-Cu40wAFF.js → ProjectSettingsPage-IcncIZ9Y.js} +1 -1
  72. package/templates/.agents/dashboard/apps/frontend/dist/assets/{TraceabilityWorkspace-Bh3YBd3C.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-qVSJnJ0_.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 +193 -191
  106. package/templates/.agents/dashboard/packages/local-runtime/dist/sqlite-read-model.cjs +169 -169
  107. package/templates/.agents/dashboard/packages/local-runtime/package.json +1 -1
  108. package/templates/.agents/dashboard/packages/local-runtime/server.mjs +193 -191
  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 +53 -2
  112. package/templates/.agents/dashboard/packages/local-runtime/src/active-codex-telemetry.ts +18 -1
  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 +28 -45
  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/ui/package.json +1 -1
  134. package/templates/.agents/dashboard/tests/e2e/package.json +1 -1
  135. package/templates/.agents/design/.zaip-managed.json +3 -3
  136. package/templates/.agents/design/apps/catalog/package.json +1 -1
  137. package/templates/.agents/design/packages/local-runtime/package.json +1 -1
  138. package/templates/.agents/requirements/autonomous-assurance.md +11 -2
  139. package/templates/.agents/requirements/cloud-hostinger-autonomy.md +1 -1
  140. package/templates/.agents/requirements/comparative-audit.md +1 -1
  141. package/templates/.agents/requirements/context-engine.md +1 -1
  142. package/templates/.agents/requirements/cost-observability.md +1 -1
  143. package/templates/.agents/requirements/dashboard-launch.md +1 -1
  144. package/templates/.agents/requirements/data-export.md +1 -1
  145. package/templates/.agents/requirements/design-system.md +1 -1
  146. package/templates/.agents/requirements/execution-engine.md +1 -1
  147. package/templates/.agents/requirements/kanban-dashboard.md +1 -1
  148. package/templates/.agents/requirements/model-orchestration.md +1 -1
  149. package/templates/.agents/requirements/public-distribution.md +1 -1
  150. package/templates/.agents/requirements/software-architecture.md +1 -1
  151. package/templates/.agents/requirements/strategic-improvements.md +1 -1
  152. package/templates/.agents/requirements/work-lifecycle.md +2 -1
  153. package/templates/.agents/requirements/zaip-strategic-95.md +1 -1
  154. package/templates/.agents/rules/run-lifecycle-closure.md +2 -1
  155. package/templates/.agents/rules/workflow.md +1 -0
  156. package/templates/.agents/runtime/VERSION +1 -1
  157. package/templates/.agents/runtime/bin/zaip.mjs +1178 -764
  158. package/templates/.agents/runtime/checksums.sha256 +6 -6
  159. package/templates/.agents/runtime/manifest.json +1 -1
  160. package/templates/.agents/runtime/pnpm-lock.sha256 +1 -1
  161. package/templates/.agents/runtime/provenance.json +5 -5
  162. package/templates/.agents/runtime/sbom.cdx.json +59 -59
  163. package/templates/.agents/skills/zaip-skill-execution/SKILL.md +1 -1
  164. package/templates/.agents/skills/zaip-skill-execution/references/controlled-release-recovery.md +14 -0
  165. package/templates/.agents/skills/zaip-skill-execution/references/runtime-governance.md +1 -1
  166. package/templates/.agents/skills/zaip-skill-github-deploy/SKILL.md +5 -1
  167. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/deploy-contract.mjs +12 -0
  168. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/deploy-contract.test.mjs +25 -0
  169. package/templates/.agents/skills/zaip-skill-maintenance-enter/SKILL.md +23 -0
  170. package/templates/.agents/skills/zaip-skill-maintenance-enter/agents/openai.yaml +4 -0
  171. package/templates/.agents/skills/zaip-skill-maintenance-enter/scripts/maintenance.mjs +213 -0
  172. package/templates/.agents/skills/zaip-skill-maintenance-enter/scripts/maintenance.test.mjs +207 -0
  173. package/templates/.agents/skills/zaip-skill-maintenance-exit/SKILL.md +21 -0
  174. package/templates/.agents/skills/zaip-skill-maintenance-exit/agents/openai.yaml +4 -0
  175. package/templates/.agents/skills/zaip-skill-release-qualification/SKILL.md +2 -0
  176. package/templates/.agents/skills/zaip-skill-release-qualification/references/local-preparation.md +37 -8
  177. package/templates/.agents/zaip-aios/VERSION +1 -1
  178. package/templates/.agents/zaip-aios/guides/26-qualidade-e-release.md +38 -0
  179. package/templates/template-manifest.json +188 -116
  180. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-Co2Jiuzh.js +0 -68
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { activeCaptureFailure } from "./active-capture-failure.js";
3
+
4
+ describe("public active capture diagnostics", () => {
5
+ it.each([
6
+ [{ code: "ETIMEDOUT" }, "timeout"],
7
+ [{ code: "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" }, "output_limit"],
8
+ [{ killed: true, signal: "SIGTERM" }, "process_terminated"],
9
+ [{ code: 1 }, "process_exit"],
10
+ [{ code: "ENOENT" }, "spawn_failure"],
11
+ ])("classifies process failures without guessing their cause", (fields, kind) => {
12
+ expect(activeCaptureFailure(Object.assign(new Error("private"), fields))).toMatchObject({
13
+ code: "active_telemetry_capture_failed",
14
+ failure_kind: kind,
15
+ exit_code: "code" in fields && typeof fields.code === "number" ? fields.code : null,
16
+ });
17
+ });
18
+
19
+ it("never exposes child output, command, paths or arbitrary error properties", () => {
20
+ const error = Object.assign(new Error("Bearer sensitive-value"), {
21
+ name: "sensitive-value",
22
+ stdout: "sensitive-value",
23
+ stderr: "sensitive-value",
24
+ cmd: "sensitive-value",
25
+ code: "sensitive-value",
26
+ signal: "sensitive-value",
27
+ });
28
+ const result = activeCaptureFailure(error);
29
+ expect(JSON.stringify(result)).not.toContain("sensitive-value");
30
+ expect(result).toMatchObject({
31
+ failure_kind: "capture_error",
32
+ process_code: null,
33
+ exit_code: null,
34
+ signal: null,
35
+ });
36
+ expect(activeCaptureFailure(null).cause).toBe("unknown_error");
37
+ });
38
+ });
@@ -0,0 +1,35 @@
1
+ /** Public health diagnostics never include command lines, environment or process output. */
2
+ export function activeCaptureFailure(error: unknown) {
3
+ const failure = error instanceof Error ? error : null;
4
+ const fields = failure as (Error & { code?: unknown; signal?: unknown; killed?: unknown }) | null;
5
+ const code = fields?.code;
6
+ const signal = fields?.signal;
7
+ const exitCode = Number.isSafeInteger(code) ? (code as number) : null;
8
+ const processCode =
9
+ typeof code === "string" &&
10
+ ["ENOENT", "EACCES", "EPERM", "ETIMEDOUT", "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"].includes(code)
11
+ ? code
12
+ : null;
13
+ const processSignal =
14
+ typeof signal === "string" && /^SIG[A-Z0-9]{1,12}$/.test(signal) ? signal : null;
15
+ return {
16
+ code: "active_telemetry_capture_failed",
17
+ occurred_at: new Date().toISOString(),
18
+ cause: failure === null ? "unknown_error" : "Error",
19
+ failure_kind:
20
+ processCode === "ETIMEDOUT"
21
+ ? "timeout"
22
+ : processCode === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
23
+ ? "output_limit"
24
+ : fields?.killed === true || processSignal !== null
25
+ ? "process_terminated"
26
+ : exitCode !== null
27
+ ? "process_exit"
28
+ : processCode !== null
29
+ ? "spawn_failure"
30
+ : "capture_error",
31
+ process_code: processCode,
32
+ exit_code: exitCode,
33
+ signal: processSignal,
34
+ };
35
+ }
@@ -2,10 +2,11 @@ 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
4
  import { afterEach, describe, expect, it, vi } from "vitest";
5
-
5
+ import { activeCaptureFailure } from "./active-capture-failure.js";
6
6
  import {
7
7
  captureActiveCodexTelemetry,
8
8
  captureRepositoryActiveCodexTelemetry,
9
+ ObservedCodexCaptureDeadlineError,
9
10
  } from "./active-codex-telemetry.js";
10
11
  import * as sources from "./sqlite-read-model-source-fingerprint.js";
11
12
 
@@ -29,11 +30,61 @@ describe("active Codex telemetry capture", () => {
29
30
  "bounded pending progress",
30
31
  );
31
32
  expect(execute.mock.calls[0]?.[2]).toMatchObject({
32
- timeout: 120_000,
33
+ timeout: 125_000,
34
+ signal: expect.any(AbortSignal),
33
35
  windowsHide: true,
34
36
  cwd: root,
35
37
  });
36
38
  });
39
+ it("marks timeout only when its own child deadline aborts the project process", async () => {
40
+ const root = await runtimeFixture("");
41
+ const deadline = new AbortController();
42
+ const execute = vi.fn(
43
+ async (_file: string, _args: string[], options: { signal: AbortSignal }) =>
44
+ new Promise<never>((_resolve, reject) => {
45
+ options.signal.addEventListener(
46
+ "abort",
47
+ () =>
48
+ reject(
49
+ Object.assign(new Error("private child output"), {
50
+ name: "AbortError",
51
+ code: "ABORT_ERR",
52
+ }),
53
+ ),
54
+ { once: true },
55
+ );
56
+ deadline.abort();
57
+ }),
58
+ );
59
+ await expect(
60
+ captureActiveCodexTelemetry(root, root, execute as never, deadline.signal),
61
+ ).rejects.toBeInstanceOf(ObservedCodexCaptureDeadlineError);
62
+ const failure = activeCaptureFailure(new ObservedCodexCaptureDeadlineError());
63
+ expect(failure).toMatchObject({ failure_kind: "timeout", process_code: "ETIMEDOUT" });
64
+ expect(JSON.stringify(failure)).not.toContain("private child output");
65
+ expect(
66
+ activeCaptureFailure(
67
+ Object.assign(new Error("external termination"), {
68
+ killed: true,
69
+ signal: "SIGTERM",
70
+ }),
71
+ ).failure_kind,
72
+ ).toBe("process_terminated");
73
+ const externalAbort = Object.assign(new Error("external cancellation"), {
74
+ name: "AbortError",
75
+ code: "ABORT_ERR",
76
+ });
77
+ await expect(
78
+ captureActiveCodexTelemetry(
79
+ root,
80
+ root,
81
+ (async () => {
82
+ throw externalAbort;
83
+ }) as never,
84
+ new AbortController().signal,
85
+ ),
86
+ ).rejects.toBe(externalAbort);
87
+ });
37
88
  it("uses the primary CLI for registered worktrees while preserving each target workspace and cwd", async () => {
38
89
  const primary = await runtimeFixture(`import {writeFileSync} from 'node:fs';
39
90
  import {resolve} from 'node:path';
@@ -9,6 +9,14 @@ const execFileAsync = promisify(execFile);
9
9
  // One automatic pass examines at most 32 Runs. Manual --drain remains separate.
10
10
  export const RECENT_CODEX_CAPTURE_TIMEOUT_MS = 120_000;
11
11
 
12
+ export class ObservedCodexCaptureDeadlineError extends Error {
13
+ readonly code = "ETIMEDOUT";
14
+ readonly deadline_observed = true;
15
+ constructor() {
16
+ super("Recent Codex capture exceeded its observed process deadline");
17
+ }
18
+ }
19
+
12
20
  export type ActiveCodexTelemetryCapture = {
13
21
  captured_runs: number;
14
22
  catch_up_complete?: boolean;
@@ -37,6 +45,7 @@ export async function captureActiveCodexTelemetry(
37
45
  workspaceRoot: string,
38
46
  runtimeRoot = workspaceRoot,
39
47
  execute = execFileAsync,
48
+ deadline = AbortSignal.timeout(RECENT_CODEX_CAPTURE_TIMEOUT_MS),
40
49
  ): Promise<ActiveCodexTelemetryCapture> {
41
50
  // The CLI intentionally treats an absent active-work index as an empty pass.
42
51
  // Validate the canonical manifest first so that exit 0 cannot mask missing lifecycle.
@@ -69,7 +78,9 @@ export async function captureActiveCodexTelemetry(
69
78
  cwd: workspaceRoot,
70
79
  encoding: "utf8",
71
80
  windowsHide: true,
72
- timeout: RECENT_CODEX_CAPTURE_TIMEOUT_MS,
81
+ signal: deadline,
82
+ // A secondary child-process guard covers a broken abort delivery.
83
+ timeout: RECENT_CODEX_CAPTURE_TIMEOUT_MS + 5_000,
73
84
  maxBuffer: 1_024 * 1_024,
74
85
  },
75
86
  );
@@ -95,6 +106,12 @@ export async function captureActiveCodexTelemetry(
95
106
  last_capture_at: new Date().toISOString(),
96
107
  };
97
108
  } catch (error) {
109
+ if (
110
+ deadline.aborted &&
111
+ error instanceof Error &&
112
+ (error.name === "AbortError" || ("code" in error && error.code === "ABORT_ERR"))
113
+ )
114
+ throw new ObservedCodexCaptureDeadlineError();
98
115
  if (isManifestUninitialized(error)) {
99
116
  return {
100
117
  captured_runs: 0,
@@ -160,7 +160,12 @@ export function manifestRun(
160
160
  artifact_path: `.agents/records/runs/${summary.run_id}`,
161
161
  status: summary.status ?? "completed",
162
162
  phase: summary.phase ?? "execution",
163
- entrypoint: summary.task_id === null ? "conversation" : "task",
163
+ entrypoint:
164
+ summary.purpose === "observation"
165
+ ? "observation"
166
+ : summary.task_id === null
167
+ ? "conversation"
168
+ : "task",
164
169
  work_type: "new",
165
170
  run_role: delegated ? ("subagent" as const) : ("orchestrator" as const),
166
171
  run_kind: delegated ? ("subrun" as const) : ("run" as const),
@@ -187,7 +192,7 @@ export function manifestRun(
187
192
  terminal_call_count: summary.llm_call_count,
188
193
  failed_call_count: summary.status === "failed" ? summary.llm_call_count : 0,
189
194
  cancelled_call_count: summary.status === "cancelled" ? summary.llm_call_count : 0,
190
- conversation_id: summary.agent_invocation_id,
195
+ conversation_id: summary.purpose === "observation" ? null : summary.agent_invocation_id,
191
196
  execution_id: summary.execution_id,
192
197
  task_id: summary.task_id,
193
198
  spec_id: summary.spec_id,
@@ -226,6 +231,7 @@ function manifestRunPurpose(
226
231
  delegated: boolean,
227
232
  ): RunProjection["run_purpose"] {
228
233
  if (
234
+ value === "observation" ||
229
235
  value === "intake" ||
230
236
  value === "orchestration" ||
231
237
  value === "execution" ||
@@ -0,0 +1,118 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ vi.mock("./server-snapshot-synchronization.js", () => ({
4
+ startSnapshotSynchronization: vi.fn(),
5
+ }));
6
+
7
+ import { createDashboardApp } from "./server.js";
8
+ import { withSqliteReadModelFixture } from "./server.test-fixtures.js";
9
+ import { startSnapshotSynchronization } from "./server-snapshot-synchronization.js";
10
+
11
+ afterEach(() => vi.clearAllMocks());
12
+
13
+ describe("source changes during active telemetry", () => {
14
+ it.each([
15
+ false,
16
+ true,
17
+ ])("waits for an earlier projection before capture (failure=%s)", async (fail) => {
18
+ await withSqliteReadModelFixture("projection-first", async ({ root, filesystem }) => {
19
+ let settle!: () => void;
20
+ const refresh = vi
21
+ .fn(async () => undefined)
22
+ .mockImplementationOnce(async () => {
23
+ await new Promise<void>((resolve) => {
24
+ settle = resolve;
25
+ });
26
+ if (fail) throw new Error("projection failed");
27
+ });
28
+ const capture = vi.fn(async () => ({
29
+ captured_runs: 0,
30
+ captured_segments: null,
31
+ manifest_state: "initialized" as const,
32
+ skipped_reason: null,
33
+ last_capture_at: new Date().toISOString(),
34
+ }));
35
+ const app = createDashboardApp(root, "session", undefined, {
36
+ filesystem,
37
+ refreshReadModel: refresh,
38
+ captureTelemetry: capture,
39
+ });
40
+ const synchronization = vi.mocked(startSnapshotSynchronization).mock.calls.at(-1)?.[0];
41
+ if (!synchronization?.synchronizeTelemetry) throw new Error("Synchronization was not wired");
42
+ const source = synchronization.refresh().catch(() => undefined);
43
+ const capturing = synchronization.synchronizeTelemetry();
44
+ const joined = synchronization.refresh();
45
+ try {
46
+ await Promise.resolve();
47
+ expect(capture).not.toHaveBeenCalled();
48
+ expect(refresh).toHaveBeenCalledOnce();
49
+ } finally {
50
+ settle();
51
+ await Promise.all([source, capturing, joined]);
52
+ }
53
+ expect(capture).toHaveBeenCalledOnce();
54
+ expect(refresh).toHaveBeenCalledTimes(2);
55
+ expect(await (await app.request("/api/v2/health")).json()).toEqual(
56
+ expect.objectContaining({
57
+ active_capture_failure: null,
58
+ projection_refresh_failure: null,
59
+ }),
60
+ );
61
+ });
62
+ });
63
+ it.each([
64
+ false,
65
+ true,
66
+ ])("defers source projection until capture settles (failure=%s)", async (fail) => {
67
+ await withSqliteReadModelFixture("capture-order", async ({ root, filesystem }) => {
68
+ let settle!: () => void;
69
+ const refresh = vi.fn(async () => undefined);
70
+ const capture = vi.fn(async () => {
71
+ await new Promise<void>((resolve) => {
72
+ settle = resolve;
73
+ });
74
+ if (fail) throw Object.assign(new Error("capture failed"), { code: 1 });
75
+ return {
76
+ captured_runs: 1,
77
+ captured_segments: null,
78
+ manifest_state: "initialized" as const,
79
+ skipped_reason: null,
80
+ last_capture_at: new Date().toISOString(),
81
+ };
82
+ });
83
+ const app = createDashboardApp(root, "session", undefined, {
84
+ filesystem,
85
+ refreshReadModel: refresh,
86
+ captureTelemetry: capture,
87
+ });
88
+ const synchronization = vi.mocked(startSnapshotSynchronization).mock.calls.at(-1)?.[0];
89
+ if (!synchronization?.synchronizeTelemetry) throw new Error("Synchronization was not wired");
90
+ const capturing = synchronization.synchronizeTelemetry();
91
+ const sourceChange = synchronization.refresh();
92
+ const secondChange = synchronization.refresh();
93
+ try {
94
+ await Promise.resolve();
95
+ expect(refresh).not.toHaveBeenCalled();
96
+ } finally {
97
+ settle();
98
+ await Promise.all([capturing, sourceChange, secondChange]);
99
+ }
100
+ expect(capture).toHaveBeenCalledOnce();
101
+ expect(refresh).toHaveBeenCalledOnce();
102
+ const health = await (await app.request("/api/v2/health")).json();
103
+ expect(health).toEqual(
104
+ expect.objectContaining({
105
+ active_capture_failure: fail
106
+ ? expect.objectContaining({
107
+ failure_kind: "process_exit",
108
+ exit_code: 1,
109
+ })
110
+ : null,
111
+ }),
112
+ );
113
+ await synchronization.refresh();
114
+ expect(refresh).toHaveBeenCalledTimes(2);
115
+ expect(capture).toHaveBeenCalledOnce();
116
+ });
117
+ });
118
+ });
@@ -0,0 +1,90 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { WorkspaceFileSystem, type WorkspaceLayout } from "@zaip-ai/core";
5
+ import { LifecycleManifestStore } from "@zaip-ai/lifecycle-manifests";
6
+ import { Hono } from "hono";
7
+ import { expect, it } from "vitest";
8
+ import { buildManifestCostRunPage, type CompactRunMetric } from "./server-projections.js";
9
+
10
+ it("searches Task titles through the real compact projection before Run pagination", async () => {
11
+ const root = await mkdtemp(join(tmpdir(), "zaip-cost-title-"));
12
+ try {
13
+ const filesystem = new WorkspaceFileSystem(root);
14
+ await new LifecycleManifestStore(filesystem).replace([
15
+ {
16
+ schema_version: 1,
17
+ event_id: "task-title",
18
+ operation: "upsert",
19
+ occurred_at: "2026-08-09T12:00:00.000Z",
20
+ entity: {
21
+ kind: "task",
22
+ id: "TASK-1",
23
+ title: "Needle title",
24
+ status: "completed",
25
+ risk: "lite",
26
+ priority: null,
27
+ updated_at: "2026-08-09T12:00:00.000Z",
28
+ relations: [],
29
+ usage: null,
30
+ data: { created_at: "2026-08-09T12:00:00.000Z", finished_at: "2026-08-09T12:00:00.000Z" },
31
+ },
32
+ },
33
+ ]);
34
+ const metric = (id: string, task_id: string | null, hour: number): CompactRunMetric => ({
35
+ id,
36
+ task_id,
37
+ started_at: `2026-08-09T${hour}:00:00.000Z`,
38
+ finished_at: null,
39
+ execution_id: null,
40
+ conversation_id: null,
41
+ agent: null,
42
+ agent_name: null,
43
+ provider: null,
44
+ model: null,
45
+ spec_id: null,
46
+ prd_id: null,
47
+ change_id: null,
48
+ phase: "execution",
49
+ parent_run_id: null,
50
+ status: "completed",
51
+ duration_ms: null,
52
+ total_tokens: null,
53
+ cost: null,
54
+ currency: null,
55
+ events: null,
56
+ });
57
+ const summaryPath = ".agents/records/lifecycle/summary.json";
58
+ const summary = JSON.parse(await filesystem.readText(summaryPath));
59
+ summary.run_metrics = [
60
+ metric("RUN-UNRELATED", null, 15),
61
+ metric("RUN-SECOND", "TASK-1", 14),
62
+ metric("RUN-FIRST", "TASK-1", 13),
63
+ ];
64
+ await filesystem.writeTextAtomic(summaryPath, JSON.stringify(summary));
65
+ const app = new Hono();
66
+ app.get("/", async (context) =>
67
+ context.json(
68
+ await buildManifestCostRunPage(
69
+ filesystem,
70
+ { version: 2 } as WorkspaceLayout,
71
+ context,
72
+ { from: "2026-08-09", to: "2026-08-09" },
73
+ 1,
74
+ 1,
75
+ ),
76
+ ),
77
+ );
78
+ const response = await app.request("/?query=needle%20title&currency=USD");
79
+ expect(response.status).toBe(200);
80
+ expect(await response.json()).toMatchObject({
81
+ page_only: true,
82
+ run_page: { total: 2, offset: 1, limit: 1, has_more: false },
83
+ runs: [
84
+ { run_id: "RUN-FIRST", task_title: "Needle title", input_tokens: null, total_tokens: null },
85
+ ],
86
+ });
87
+ } finally {
88
+ await rm(root, { recursive: true, force: true });
89
+ }
90
+ });
@@ -0,0 +1,86 @@
1
+ import type { CostMonitorProjection } from "@zaip-ai/dashboard-cost-monitor/contracts";
2
+ import type { CompactRunMetric } from "./server-projections.js";
3
+
4
+ /** Match the complete compact population before either lifecycle or manifest pagination. */
5
+ export function filterCompactCostRuns(
6
+ runs: readonly CompactRunMetric[],
7
+ query: string,
8
+ tasks: readonly { id: string; title: string }[],
9
+ ): CompactRunMetric[] {
10
+ const needle = query.trim().toLowerCase();
11
+ if (!needle) return [...runs];
12
+ const titles = new Map(tasks.map((task) => [task.id, task.title]));
13
+ return runs.filter((run) =>
14
+ [
15
+ run.id,
16
+ run.task_id,
17
+ run.task_id === null ? null : titles.get(run.task_id),
18
+ run.provider,
19
+ run.agent,
20
+ run.agent_name,
21
+ run.model,
22
+ run.phase,
23
+ run.spec_id,
24
+ run.prd_id,
25
+ run.execution_id,
26
+ run.conversation_id,
27
+ ].some((value) => value?.toLowerCase().includes(needle)),
28
+ );
29
+ }
30
+
31
+ /** Keeps Cost Run transport incremental without changing aggregate semantics. */
32
+ export function pageCostRunRows(
33
+ projection: CostMonitorProjection,
34
+ offset: number,
35
+ limit: number,
36
+ ): CostMonitorProjection {
37
+ const safeOffset = Math.max(0, offset);
38
+ const safeLimit = Math.min(500, Math.max(1, limit));
39
+ return {
40
+ ...projection,
41
+ runs: projection.runs.slice(safeOffset, safeOffset + safeLimit),
42
+ run_page: {
43
+ offset: safeOffset,
44
+ limit: safeLimit,
45
+ total: projection.runs.length,
46
+ has_more: safeOffset + safeLimit < projection.runs.length,
47
+ },
48
+ };
49
+ }
50
+
51
+ /**
52
+ * The Runs endpoint advertises these dimensions as server-side filters. Apply
53
+ * them before offset pagination so the reported total and every page have the
54
+ * same population. Aggregate fields intentionally retain their full filtered
55
+ * projection semantics and are not derived from the transport page.
56
+ */
57
+ export function filterCostRunRows(
58
+ projection: CostMonitorProjection,
59
+ filters: {
60
+ provider: readonly string[];
61
+ model: readonly string[];
62
+ agent: readonly string[];
63
+ phase: readonly string[];
64
+ status: readonly string[];
65
+ },
66
+ ): CostMonitorProjection {
67
+ const includes = (values: readonly string[], value: string) =>
68
+ values.length === 0 || values.includes(value);
69
+ const provider = (value: string | null) => {
70
+ const normalized = value?.trim().toLowerCase();
71
+ return !normalized || normalized === "unknown" || normalized === "codex"
72
+ ? "openai"
73
+ : normalized;
74
+ };
75
+ return {
76
+ ...projection,
77
+ runs: projection.runs.filter(
78
+ (run) =>
79
+ includes(filters.provider, provider(run.provider)) &&
80
+ includes(filters.model, run.model) &&
81
+ includes(filters.agent, run.agent_name) &&
82
+ includes(filters.phase, run.phase) &&
83
+ includes(filters.status, run.task_status),
84
+ ),
85
+ };
86
+ }
@@ -205,7 +205,14 @@ export function registerOperationsRoutes(app: Hono, dependencies: DashboardRoute
205
205
  );
206
206
  }
207
207
  });
208
- registerOperationsRunsRoute(app, filesystem, readAggregate, headOrJson);
208
+ // The compact Run page reads the canonical lifecycle summary, which is not
209
+ // materialized as a SQLite file. Keep SQL-backed Operations views separate.
210
+ registerOperationsRunsRoute(
211
+ app,
212
+ isSqliteSnapshot ? canonicalFilesystem : filesystem,
213
+ readAggregate,
214
+ headOrJson,
215
+ );
209
216
  // Explicit SQL read-model API for the comparison dashboard. It never falls
210
217
  // back to filesystem pagination, making first-fold/entity query plans
211
218
  // inspectable while preserving the established Operations contracts.
@@ -0,0 +1,13 @@
1
+ import type { WorkspaceFileSystem } from "@zaip-ai/core";
2
+ import { PricingRepository } from "@zaip-ai/costs";
3
+ import type { Context } from "hono";
4
+
5
+ export async function readCurrentFxRate(filesystem: WorkspaceFileSystem) {
6
+ return new PricingRepository(filesystem).readLatestFxRate();
7
+ }
8
+
9
+ export function isUnfilteredCostRequest(context: Context): boolean {
10
+ return ["provider", "model", "agent", "phase", "status"].every(
11
+ (name) => (context.req.queries(name)?.length ?? 0) === 0,
12
+ );
13
+ }