@zaip-ai/cli 1.3.2 → 1.3.4

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 (81) hide show
  1. package/LICENSE +6 -3
  2. package/README.md +8 -2
  3. package/dist/services/codex-pre-tool-hook-template.js +1 -1
  4. package/dist/services/runtime-template.test.js +9 -8
  5. package/dist/services/runtime-template.test.js.map +1 -1
  6. package/package.json +2 -2
  7. package/templates/.agents/dashboard/.zaip-managed.json +401 -381
  8. package/templates/.agents/dashboard/apps/frontend/dist/assets/{CostCalculationGuidePage-ovxurkIk.js → CostCalculationGuidePage-BZWjXQB-.js} +1 -1
  9. package/templates/.agents/dashboard/apps/frontend/dist/assets/{DashboardHelpDialog-BXy6nti1.js → DashboardHelpDialog-D8qqeIPE.js} +30 -1
  10. package/templates/.agents/dashboard/apps/frontend/dist/assets/{PricingSettingsDialog-vSvvOHQp.js → PricingSettingsDialog-Cm12_A0v.js} +1 -1
  11. package/templates/.agents/dashboard/apps/frontend/dist/assets/{ProjectSettingsPage-zH758vfs.js → ProjectSettingsPage-DxO9F8B4.js} +1 -1
  12. package/templates/.agents/dashboard/apps/frontend/dist/assets/{TraceabilityWorkspace-CTH2wDs5.js → TraceabilityWorkspace-_x0twOcS.js} +1 -1
  13. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-Cpp8xFij.js +68 -0
  14. package/templates/.agents/dashboard/apps/frontend/dist/assets/{icons-vendor-B_lDDHn1.js → icons-vendor-CR4Fbf3g.js} +1 -1
  15. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-ncC1rxap.js +2 -0
  16. package/templates/.agents/dashboard/apps/frontend/dist/assets/{react-vendor-B3TJrD4O.js → react-vendor-DRJQbKnp.js} +1 -1
  17. package/templates/.agents/dashboard/apps/frontend/dist/assets/{traceability-7f1j5wx3.js → traceability-CMrDYqjR.js} +1 -1
  18. package/templates/.agents/dashboard/apps/frontend/dist/index.html +5 -5
  19. package/templates/.agents/dashboard/apps/frontend/src/App.tsx +48 -3
  20. package/templates/.agents/dashboard/apps/frontend/src/AppRequestGuards.tsx +44 -0
  21. package/templates/.agents/dashboard/apps/frontend/src/cost-period-refresh.test.ts +92 -1
  22. package/templates/.agents/dashboard/apps/frontend/src/snapshot-coherence.test.ts +2 -2
  23. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostDeveloperAttributionPanel.test.ts +60 -0
  24. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostDeveloperAttributionPanel.tsx +26 -9
  25. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorNavigation.tsx +189 -0
  26. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.test.ts +122 -2
  27. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.tsx +70 -155
  28. package/templates/.agents/dashboard/modules/operations/src/react/export/ExcelExportAction.test.ts +59 -0
  29. package/templates/.agents/dashboard/modules/operations/src/react/export/ExcelExportAction.tsx +55 -2
  30. package/templates/.agents/dashboard/packages/local-runtime/dist/server.cjs +274 -246
  31. package/templates/.agents/dashboard/packages/local-runtime/dist/sqlite-read-model.cjs +255 -227
  32. package/templates/.agents/dashboard/packages/local-runtime/server.mjs +263 -235
  33. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-dimension-facts.test.ts +282 -0
  34. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-dimension-facts.ts +71 -0
  35. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-launch-progress.ts +18 -0
  36. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-stale-seed.ts +50 -0
  37. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.test.ts +9 -6
  38. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.ts +45 -73
  39. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-snapshot-seed.test.ts +79 -2
  40. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-work-item-index.test.ts +56 -0
  41. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-launch-page.mjs +26 -8
  42. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-launch-page.test.mjs +45 -4
  43. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-lifecycle.mjs +58 -0
  44. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-lifecycle.test.mjs +81 -0
  45. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.mjs +171 -71
  46. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.test.mjs +152 -11
  47. package/templates/.agents/dashboard/packages/windows-launcher/Program.cs +4 -9
  48. package/templates/.agents/dashboard/tests/e2e/playwright.config.ts +9 -1
  49. package/templates/.agents/dashboard/tests/e2e/specs/cost-monitor-period-transition.spec.ts +232 -0
  50. package/templates/.agents/dashboard/tests/e2e/specs/dashboard.spec.ts +26 -5
  51. package/templates/.agents/requirements/autonomous-assurance.md +1 -1
  52. package/templates/.agents/requirements/comparative-audit.md +1 -1
  53. package/templates/.agents/requirements/context-engine.md +1 -1
  54. package/templates/.agents/requirements/cost-observability.md +1 -1
  55. package/templates/.agents/requirements/dashboard-launch.md +1 -1
  56. package/templates/.agents/requirements/data-export.md +1 -1
  57. package/templates/.agents/requirements/design-system.md +1 -1
  58. package/templates/.agents/requirements/execution-engine.md +1 -1
  59. package/templates/.agents/requirements/kanban-dashboard.md +1 -1
  60. package/templates/.agents/requirements/model-orchestration.md +1 -1
  61. package/templates/.agents/requirements/public-distribution.md +1 -1
  62. package/templates/.agents/requirements/software-architecture.md +1 -1
  63. package/templates/.agents/requirements/strategic-improvements.md +1 -1
  64. package/templates/.agents/requirements/work-lifecycle.md +1 -1
  65. package/templates/.agents/requirements/zaip-strategic-95.md +1 -1
  66. package/templates/.agents/runtime/bin/zaip.mjs +7 -7
  67. package/templates/.agents/runtime/checksums.sha256 +3 -3
  68. package/templates/.agents/runtime/provenance.json +3 -3
  69. package/templates/.agents/runtime/sbom.cdx.json +1595 -3491
  70. package/templates/.agents/skills/zaip-skill-lifecycle-closure/SKILL.md +16 -0
  71. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-local-telemetry.mjs +19 -20
  72. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-local-telemetry.test.mjs +143 -0
  73. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-lib.mjs +14 -0
  74. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-remeasurement.mjs +3 -1
  75. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-remeasurement.test.mjs +21 -0
  76. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/source-size-telemetry/model-identity-reconciliation.mjs +3 -0
  77. package/templates/.agents/zaip-aios/guides/26-qualidade-e-release.md +29 -0
  78. package/templates/template-manifest.json +536 -932
  79. package/templates/.agents/dashboard/.tmp-run-list.json +0 -254
  80. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-DKwP1stV.js +0 -68
  81. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-CuMcoUni.js +0 -2
@@ -0,0 +1,81 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import {
5
+ awaitOwnedDashboardExit,
6
+ createPhaseTimer,
7
+ presentLaunchFailure,
8
+ } from "./start-dashboard-lifecycle.mjs";
9
+
10
+ describe("dashboard launcher lifecycle", () => {
11
+ it("reports the existing failure state and closes the preparation page", () => {
12
+ const calls = [];
13
+ const launchPage = {
14
+ setFailure: (failure) => calls.push(["failure", failure]),
15
+ closeAfterFailureObservation: () => calls.push(["close"]),
16
+ };
17
+
18
+ expect(
19
+ presentLaunchFailure(
20
+ launchPage,
21
+ new Error("boom"),
22
+ (message, limit) => `${limit}:${message}`,
23
+ ),
24
+ ).toBeNull();
25
+ expect(calls).toEqual([
26
+ [
27
+ "failure",
28
+ {
29
+ percent: 100,
30
+ phase: "Não foi possível preparar o dashboard",
31
+ detail: "900:boom",
32
+ },
33
+ ],
34
+ ["close"],
35
+ ]);
36
+ expect(presentLaunchFailure(null, "unused", () => "unused")).toBeNull();
37
+ });
38
+
39
+ it("waits for the child exit before clearing the owned session", async () => {
40
+ const child = new EventEmitter();
41
+ const processRef = new EventEmitter();
42
+ const calls = [];
43
+ child.kill = (signal) => calls.push(["kill", signal]);
44
+ const completion = awaitOwnedDashboardExit({
45
+ child,
46
+ currentSession: { dbPath: "C:/tmp/dashboard.sqlite" },
47
+ clearOwnedDashboardSession: async () => calls.push(["clear"]),
48
+ recordPortStatus: async (_session, status) => calls.push(["status", status]),
49
+ removeGeneratedReadModel: async (path) => calls.push(["remove", path]),
50
+ processRef,
51
+ });
52
+
53
+ expect(calls).toEqual([]);
54
+ processRef.emit("SIGINT");
55
+ child.emit("exit", null);
56
+
57
+ await expect(completion).resolves.toBe(0);
58
+ expect(calls).toEqual([
59
+ ["kill", "SIGINT"],
60
+ ["clear"],
61
+ ["status", "closed"],
62
+ ["remove", "C:/tmp/dashboard.sqlite"],
63
+ ]);
64
+ });
65
+
66
+ it("records launcher phase timing with the same process identity", () => {
67
+ const entries = [];
68
+ createPhaseTimer((entry) => entries.push(JSON.parse(entry)), 42)("ready", 1_000, {
69
+ port: 4417,
70
+ });
71
+
72
+ expect(entries).toEqual([
73
+ expect.objectContaining({
74
+ event: "dashboard_sqlite_phase",
75
+ phase: "ready",
76
+ launcher_pid: 42,
77
+ port: 4417,
78
+ }),
79
+ ]);
80
+ });
81
+ });
@@ -19,6 +19,12 @@ import process from "node:process";
19
19
  import { fileURLToPath } from "node:url";
20
20
  import { collectPreviousDashboardSessions } from "./start-dashboard-history.mjs";
21
21
  import { createDashboardLaunchPage } from "./start-dashboard-launch-page.mjs";
22
+ import {
23
+ awaitOwnedDashboardExit,
24
+ createPhaseTimer,
25
+ presentLaunchFailure,
26
+ writeLauncherStatus,
27
+ } from "./start-dashboard-lifecycle.mjs";
22
28
  import {
23
29
  isProcessAlive,
24
30
  stopOwnedDashboardProcess,
@@ -62,18 +68,66 @@ const healthPath = "/api/v2/health";
62
68
  const lockTimeoutMs = 210_000;
63
69
  const staleLockMs = 30_000;
64
70
  const lockHeartbeatFloorMs = 10;
65
- // A cold immutable seed can legitimately take ~176 s on a busy Windows
66
- // volume. Keep the immediate preparation page and fresh-port handoff, but do
67
- // not kill that one-time canonical build before it can populate the cache.
68
- const startupTimeoutMs = 180_000;
69
71
  const maxServerStartAttempts = 4;
70
72
  const maxPortHistory = 50;
71
- const dashboardSourceRoots = [
72
- resolve(workspaceRoot, ".agents/dashboard/apps/frontend/src"),
73
- resolve(workspaceRoot, ".agents/dashboard/modules"),
74
- resolve(workspaceRoot, ".agents/dashboard/packages/ui/src"),
75
- resolve(packageRoot, "src"),
76
- ];
73
+ // Keep each build invalidation scoped to the artifact it can actually change.
74
+ // The runtime bundle includes the server-side dashboard modules, workspace
75
+ // packages, settings and build scripts. Browser-only changes never rebuild it.
76
+ export const dashboardBuildTargets = {
77
+ frontend: {
78
+ artifacts: [resolve(frontendRoot, "index.html")],
79
+ sourceRoots: [
80
+ resolve(workspaceRoot, ".agents/dashboard/apps/frontend/src"),
81
+ resolve(workspaceRoot, ".agents/dashboard/apps/frontend/public"),
82
+ resolve(workspaceRoot, ".agents/dashboard/apps/frontend/index.html"),
83
+ resolve(workspaceRoot, ".agents/dashboard/modules/operations/src"),
84
+ resolve(workspaceRoot, ".agents/dashboard/modules/cost-monitor/src"),
85
+ resolve(workspaceRoot, ".agents/dashboard/modules/traceability/src"),
86
+ resolve(workspaceRoot, ".agents/dashboard/modules/operations/src/contracts.ts"),
87
+ resolve(workspaceRoot, ".agents/dashboard/modules/cost-monitor/src/contracts.ts"),
88
+ resolve(workspaceRoot, ".agents/dashboard/modules/traceability/src/contracts.ts"),
89
+ resolve(workspaceRoot, ".agents/dashboard/packages/contracts/src"),
90
+ resolve(workspaceRoot, ".agents/dashboard/packages/ui/src"),
91
+ resolve(workspaceRoot, ".agents/dashboard/apps/frontend/vite.config.ts"),
92
+ resolve(workspaceRoot, ".agents/dashboard/apps/frontend/package.json"),
93
+ resolve(workspaceRoot, ".agents/dashboard/apps/frontend/tsconfig.json"),
94
+ resolve(workspaceRoot, "tsconfig.base.json"),
95
+ resolve(workspaceRoot, "pnpm-lock.yaml"),
96
+ resolve(workspaceRoot, "README.md"),
97
+ ],
98
+ excludedSourcePaths: ["/server/"],
99
+ },
100
+ runtime: {
101
+ artifacts: [
102
+ sourceServer,
103
+ resolve(packageRoot, "dist/server.js"),
104
+ resolve(packageRoot, "dist/sqlite-read-model.cjs"),
105
+ installedServer,
106
+ ],
107
+ sourceRoots: [
108
+ resolve(packageRoot, "src"),
109
+ resolve(packageRoot, "scripts"),
110
+ resolve(packageRoot, "package.json"),
111
+ resolve(packageRoot, "tsconfig.json"),
112
+ resolve(workspaceRoot, ".agents/dashboard/modules/operations/src"),
113
+ resolve(workspaceRoot, ".agents/dashboard/modules/cost-monitor/src"),
114
+ resolve(workspaceRoot, ".agents/dashboard/modules/traceability/src"),
115
+ resolve(workspaceRoot, ".agents/dashboard/packages/contracts/src"),
116
+ resolve(workspaceRoot, ".agents/dashboard/packages/data-export/src"),
117
+ resolve(workspaceRoot, "packages/core/src"),
118
+ resolve(workspaceRoot, "packages/costs/src"),
119
+ resolve(workspaceRoot, "packages/decisions/src"),
120
+ resolve(workspaceRoot, "packages/lifecycle-manifests/src"),
121
+ resolve(workspaceRoot, "packages/specs/src"),
122
+ resolve(workspaceRoot, "packages/tasks/src"),
123
+ resolve(workspaceRoot, "package.json"),
124
+ resolve(workspaceRoot, "pnpm-workspace.yaml"),
125
+ resolve(workspaceRoot, "pnpm-lock.yaml"),
126
+ resolve(workspaceRoot, "tsconfig.base.json"),
127
+ ],
128
+ excludedSourcePaths: ["/react/"],
129
+ },
130
+ };
77
131
  export function preferredPort(value = process.env.ZAIP_SERVER_PORT) {
78
132
  const parsed = Number.parseInt(value ?? "", 10);
79
133
  return Number.isInteger(parsed) && parsed > 0 && parsed <= 65_535 ? parsed : defaultPort;
@@ -263,25 +317,40 @@ export async function findReusableDashboardSession({
263
317
  async function backgroundRefreshIsRunning() {
264
318
  return refreshIsRunning(join(sessionDirectory, "dashboard-sqlite-refresh.json"), isProcessAlive);
265
319
  }
266
- async function latestModifiedAt(paths) {
320
+ export async function latestModifiedAt(paths, excludedSourcePaths = []) {
267
321
  let latest = 0;
268
322
  for (const path of paths) {
269
323
  const details = await stat(path).catch(() => null);
270
324
  if (details === null) continue;
271
- latest = Math.max(latest, details.mtimeMs);
272
- if (!details.isDirectory()) continue;
325
+ if (!details.isDirectory()) {
326
+ if (isBuildInputPath(path, excludedSourcePaths)) latest = Math.max(latest, details.mtimeMs);
327
+ continue;
328
+ }
329
+ // Retain included directory mtimes so removing or renaming a source file
330
+ // after a build still invalidates its artifact. Excluded trees stay out.
331
+ if (isBuildInputPath(path, excludedSourcePaths)) latest = Math.max(latest, details.mtimeMs);
273
332
  const entries = await readdir(path, { withFileTypes: true }).catch(() => []);
274
333
  latest = Math.max(
275
334
  latest,
276
335
  await latestModifiedAt(
277
336
  entries
278
- .filter((entry) => !new Set(["dist", "node_modules", ".turbo"]).has(entry.name))
337
+ .filter((entry) => isBuildInputPath(join(path, entry.name), excludedSourcePaths))
279
338
  .map((entry) => join(path, entry.name)),
339
+ excludedSourcePaths,
280
340
  ),
281
341
  );
282
342
  }
283
343
  return latest;
284
344
  }
345
+ function isBuildInputPath(path, excludedSourcePaths) {
346
+ const normalized = path.replaceAll("\\", "/");
347
+ const name = normalized.slice(normalized.lastIndexOf("/") + 1);
348
+ if (new Set(["dist", "node_modules", ".turbo"]).has(name)) return false;
349
+ if (/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(name)) return false;
350
+ return !excludedSourcePaths.some(
351
+ (excluded) => normalized.includes(excluded) || normalized.endsWith(excluded.slice(0, -1)),
352
+ );
353
+ }
285
354
  async function runSqliteBuild(target) {
286
355
  const frontend = target === "frontend";
287
356
  const command = process.execPath;
@@ -320,17 +389,75 @@ async function runSqliteBuild(target) {
320
389
  });
321
390
  return output;
322
391
  }
323
- async function ensureFreshDashboardBuild() {
324
- const artifacts = [resolve(frontendRoot, "index.html"), sourceServer];
325
- const [sourcesModifiedAt, artifactsModifiedAt] = await Promise.all([
326
- latestModifiedAt(dashboardSourceRoots),
327
- latestModifiedAt(artifacts),
328
- ]);
329
- const forceRebuild = process.env.ZAIP_DASHBOARD_FORCE_REBUILD === "1";
330
- if (forceRebuild || artifactsModifiedAt < sourcesModifiedAt) {
331
- await runSqliteBuild("frontend");
332
- await runSqliteBuild("runtime");
392
+ export async function inspectDashboardBuildTarget(target) {
393
+ const artifactStats = await Promise.all(
394
+ target.artifacts.map(async (path) => ({ path, details: await stat(path).catch(() => null) })),
395
+ );
396
+ const missingArtifacts = artifactStats
397
+ .filter(({ details }) => details === null)
398
+ .map(({ path }) => path);
399
+ const artifactsModifiedAt = Math.min(
400
+ ...artifactStats.map(({ details }) => details?.mtimeMs ?? 0),
401
+ );
402
+ const sourcesModifiedAt = await latestModifiedAt(target.sourceRoots, target.excludedSourcePaths);
403
+ return { artifactsModifiedAt, missingArtifacts, sourcesModifiedAt };
404
+ }
405
+ function buildProgress(onProgress, percent, phase, detail) {
406
+ onProgress({ percent, phase, detail });
407
+ }
408
+ export async function refreshDashboardBuildTargets({
409
+ targets = dashboardBuildTargets,
410
+ forceRebuild = process.env.ZAIP_DASHBOARD_FORCE_REBUILD === "1",
411
+ inspectTarget = inspectDashboardBuildTarget,
412
+ runBuild = runSqliteBuild,
413
+ onProgress = () => {},
414
+ } = {}) {
415
+ const builtTargets = [];
416
+ for (const [name, target] of Object.entries(targets)) {
417
+ buildProgress(
418
+ onProgress,
419
+ name === "frontend" ? 30 : 35,
420
+ `Verificando ${name === "frontend" ? "a interface" : "o runtime local"}`,
421
+ "Conferindo somente as dependências que geram este artefato.",
422
+ );
423
+ const state = await inspectTarget(target);
424
+ const needsBuild =
425
+ forceRebuild ||
426
+ state.missingArtifacts.length > 0 ||
427
+ state.artifactsModifiedAt < state.sourcesModifiedAt;
428
+ if (!needsBuild) {
429
+ buildProgress(
430
+ onProgress,
431
+ name === "frontend" ? 33 : 39,
432
+ `${name === "frontend" ? "Interface" : "Runtime local"} já está atualizado`,
433
+ "O artefato existente corresponde às dependências verificadas.",
434
+ );
435
+ continue;
436
+ }
437
+ const reason = forceRebuild
438
+ ? "A atualização foi solicitada explicitamente."
439
+ : state.missingArtifacts.length > 0
440
+ ? "Um artefato necessário está ausente."
441
+ : "Uma dependência deste artefato foi modificada.";
442
+ buildProgress(
443
+ onProgress,
444
+ name === "frontend" ? 33 : 39,
445
+ `Atualizando ${name === "frontend" ? "a interface" : "o runtime local"}`,
446
+ reason,
447
+ );
448
+ await runBuild(name);
449
+ builtTargets.push(name);
450
+ buildProgress(
451
+ onProgress,
452
+ name === "frontend" ? 35 : 42,
453
+ `${name === "frontend" ? "Interface" : "Runtime local"} atualizado`,
454
+ "O artefato foi gerado com as dependências atuais.",
455
+ );
333
456
  }
457
+ return { artifacts: Object.values(targets).flatMap((target) => target.artifacts), builtTargets };
458
+ }
459
+ async function ensureFreshDashboardBuild(onProgress = () => {}) {
460
+ const { artifacts } = await refreshDashboardBuildTargets({ onProgress });
334
461
  const artifactFingerprint = await Promise.all(
335
462
  [...artifacts, resolve(packageRoot, "start-dashboard.mjs")].map(async (path) => {
336
463
  const details = await stat(path);
@@ -481,6 +608,11 @@ export function startDashboardLaunchPage(port) {
481
608
  return createDashboardLaunchPage({ port, host, runtimeKind, healthPath, delay });
482
609
  }
483
610
  async function redirectLaunchPage(launchPage, target) {
611
+ launchPage.setProgress({
612
+ percent: 100,
613
+ phase: "Dashboard pronto",
614
+ detail: "A tela principal está pronta para abrir.",
615
+ });
484
616
  launchPage.redirect(target);
485
617
  const redirected =
486
618
  process.env.ZAIP_UI_NO_BROWSER === "1" ? true : await launchPage.waitForRedirect();
@@ -699,30 +831,13 @@ async function refreshSnapshotSeedInBackground() {
699
831
  writeJsonAtomic,
700
832
  });
701
833
  }
702
- async function writeLauncherStatus(status, message = "") {
703
- const path = process.env.ZAIP_LAUNCH_STATUS;
704
- if (!path) return;
705
- const temporaryPath = `${path}.${process.pid}.tmp`;
706
- await writeFile(temporaryPath, `${status}\n${message}`.trimEnd(), "utf8");
707
- await rename(temporaryPath, path);
708
- }
709
- function phaseTiming(phase, startedAt, details = {}) {
710
- appendLauncherLog(
711
- JSON.stringify({
712
- event: "dashboard_sqlite_phase",
713
- phase,
714
- elapsed_ms: Date.now() - startedAt,
715
- launcher_pid: process.pid,
716
- ...details,
717
- }),
718
- );
719
- }
720
834
  async function main() {
721
835
  let ownedChild = null;
722
836
  let currentSession = null;
723
837
  let launchPage = null;
724
838
  const token = process.env.ZAIP_UI_TOKEN ?? randomBytes(24).toString("base64url");
725
839
  const launchStartedAt = Date.now();
840
+ const phaseTiming = createPhaseTimer(appendLauncherLog, process.pid);
726
841
  // Bind the preparation page atomically on an ephemeral port before waiting
727
842
  // for the launch lock. Looking up a free port and binding it later lets a
728
843
  // concurrent preparation page claim that port, which makes the runtime
@@ -755,7 +870,9 @@ async function main() {
755
870
  phase: "Verificando os artefatos do dashboard",
756
871
  detail: "Usando o build existente quando ele já está atualizado.",
757
872
  });
758
- const buildSignature = await ensureFreshDashboardBuild();
873
+ const buildSignature = await ensureFreshDashboardBuild((progress) =>
874
+ launchPage?.setProgress(progress),
875
+ );
759
876
  phaseTiming("build_complete", buildStartedAt, { build_signature: buildSignature });
760
877
  // A second deliberate click must use the already healthy compatible
761
878
  // runtime. Build signature makes this safe across dashboard updates;
@@ -892,31 +1009,17 @@ async function main() {
892
1009
  },
893
1010
  );
894
1011
  } catch (error) {
895
- if (launchPage !== null) {
896
- launchPage.setFailure({
897
- percent: 100,
898
- phase: "Não foi possível preparar o dashboard",
899
- detail: formatLauncherError(error instanceof Error ? error.message : String(error), 900),
900
- });
901
- launchPage.closeAfterFailureObservation();
902
- launchPage = null;
903
- }
1012
+ launchPage = presentLaunchFailure(launchPage, error, formatLauncherError);
904
1013
  throw error;
905
1014
  }
906
1015
  if (ownedChild === null) return;
907
- for (const signal of ["SIGINT", "SIGTERM"]) {
908
- process.once(signal, () => ownedChild?.kill(signal));
909
- }
910
- const exitCode = await new Promise((resolveExit, rejectExit) => {
911
- ownedChild.once("error", rejectExit);
912
- ownedChild.once("exit", (code) => resolveExit(code ?? 0));
1016
+ process.exitCode = await awaitOwnedDashboardExit({
1017
+ child: ownedChild,
1018
+ currentSession,
1019
+ clearOwnedDashboardSession,
1020
+ recordPortStatus,
1021
+ removeGeneratedReadModel,
913
1022
  });
914
- await clearOwnedDashboardSession();
915
- if (currentSession !== null) {
916
- await recordPortStatus(currentSession, "closed");
917
- await removeGeneratedReadModel(currentSession.dbPath);
918
- }
919
- process.exitCode = exitCode;
920
1023
  }
921
1024
  function isPortFree(port) {
922
1025
  return new Promise((resolvePort) => {
@@ -927,8 +1030,10 @@ function isPortFree(port) {
927
1030
  });
928
1031
  }
929
1032
  async function waitForDashboard(port, child) {
930
- const deadline = Date.now() + startupTimeoutMs;
931
- while (Date.now() < deadline) {
1033
+ // A cold SQLite read model has no meaningful fixed duration: canonical
1034
+ // history and host disk speed both affect it. Keep request timeouts below,
1035
+ // but wait for an observable failure instead of a wall-clock cutoff.
1036
+ while (true) {
932
1037
  if (child.exitCode !== null) {
933
1038
  throw new Error(`Dashboard server exited before becoming healthy (${child.exitCode})`);
934
1039
  }
@@ -962,11 +1067,6 @@ async function waitForDashboard(port, child) {
962
1067
  }
963
1068
  await delay(100);
964
1069
  }
965
- // A cold read-model timeout is not a port collision. Retrying would start
966
- // up to four more full builds and exceed the launcher deadline.
967
- const timedOut = new Error(`Timed out waiting for ${dashboardHealthUrl(port)}`);
968
- timedOut.retryable = false;
969
- throw timedOut;
970
1070
  }
971
1071
  function exists(path) {
972
1072
  return access(path).then(
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from "node:events";
2
- import { mkdir, mkdtemp, readFile, rm, utimes } from "node:fs/promises";
2
+ import { mkdir, mkdtemp, readFile, rm, utimes, writeFile } from "node:fs/promises";
3
3
  import { createServer } from "node:net";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
@@ -12,13 +12,16 @@ import {
12
12
  findReusableDashboardSession,
13
13
  formatLauncherError,
14
14
  host,
15
+ inspectDashboardBuildTarget,
15
16
  isAncestorProcess,
16
17
  isProcessAlive,
18
+ latestModifiedAt,
17
19
  openBrowser,
18
20
  previousDashboardSessions,
19
21
  probeDashboardPort,
20
22
  readPortRegistry,
21
23
  recordPortStatus,
24
+ refreshDashboardBuildTargets,
22
25
  registeredListenerMatches,
23
26
  runtimeKind,
24
27
  stopOwnedDashboardProcess,
@@ -92,11 +95,143 @@ describe("dashboard executable session", () => {
92
95
  expect(excludedPorts).toContain("launchPort,");
93
96
  });
94
97
 
98
+ it("checks directed build inputs, including missing roots and every required artifact", async () => {
99
+ const directory = await mkdtemp(join(tmpdir(), "zaip-dashboard-build-inputs-"));
100
+ temporaryDirectories.push(directory);
101
+ const inputDirectory = join(directory, "input");
102
+ const artifactDirectory = join(directory, "artifacts");
103
+ const source = join(inputDirectory, "source.ts");
104
+ const testSource = join(inputDirectory, "source.test.ts");
105
+ const removedSource = join(inputDirectory, "removed-source.ts");
106
+ const excludedDirectory = join(inputDirectory, "excluded");
107
+ const excludedSource = join(excludedDirectory, "shared.ts");
108
+ const olderArtifact = join(artifactDirectory, "older-artifact.js");
109
+ const newerArtifact = join(artifactDirectory, "newer-artifact.js");
110
+ await Promise.all([mkdir(inputDirectory), mkdir(artifactDirectory)]);
111
+ await Promise.all([
112
+ writeFile(source, "source"),
113
+ writeFile(testSource, "test"),
114
+ writeFile(removedSource, "removed"),
115
+ mkdir(excludedDirectory),
116
+ writeFile(olderArtifact, "older"),
117
+ writeFile(newerArtifact, "newer"),
118
+ ]);
119
+ await writeFile(excludedSource, "excluded");
120
+ const now = Date.now();
121
+ await Promise.all([
122
+ utimes(source, new Date(now), new Date(now)),
123
+ utimes(olderArtifact, new Date(now - 5_000), new Date(now - 5_000)),
124
+ utimes(newerArtifact, new Date(now + 5_000), new Date(now + 5_000)),
125
+ utimes(testSource, new Date(now + 10_000), new Date(now + 10_000)),
126
+ utimes(excludedSource, new Date(now + 10_000), new Date(now + 10_000)),
127
+ ]);
128
+
129
+ await expect(latestModifiedAt([join(directory, "missing-root")])).resolves.toBe(0);
130
+ await expect(latestModifiedAt([inputDirectory], ["/excluded/"])).resolves.toBeLessThan(
131
+ now + 5_000,
132
+ );
133
+ await expect(
134
+ inspectDashboardBuildTarget({
135
+ artifacts: [olderArtifact, newerArtifact, join(directory, "missing-artifact.js")],
136
+ sourceRoots: [source, join(directory, "missing-root")],
137
+ }),
138
+ ).resolves.toMatchObject({
139
+ artifactsModifiedAt: 0,
140
+ missingArtifacts: [join(directory, "missing-artifact.js")],
141
+ sourcesModifiedAt: expect.any(Number),
142
+ });
143
+ await expect(
144
+ inspectDashboardBuildTarget({
145
+ artifacts: [olderArtifact, newerArtifact],
146
+ sourceRoots: [source],
147
+ }),
148
+ ).resolves.toMatchObject({
149
+ artifactsModifiedAt: expect.any(Number),
150
+ missingArtifacts: [],
151
+ sourcesModifiedAt: expect.any(Number),
152
+ });
153
+ const state = await inspectDashboardBuildTarget({
154
+ artifacts: [olderArtifact, newerArtifact],
155
+ sourceRoots: [source],
156
+ });
157
+ expect(state.artifactsModifiedAt).toBeLessThan(state.sourcesModifiedAt);
158
+ await rm(removedSource);
159
+ const stateAfterRemoval = await inspectDashboardBuildTarget({
160
+ artifacts: [olderArtifact, newerArtifact],
161
+ sourceRoots: [inputDirectory],
162
+ });
163
+ expect(stateAfterRemoval.artifactsModifiedAt).toBeLessThan(stateAfterRemoval.sourcesModifiedAt);
164
+ });
165
+
166
+ it("keeps factual build milestones wired to the preparation page", async () => {
167
+ const source = await readFile(new URL("./start-dashboard.mjs", import.meta.url), "utf8");
168
+
169
+ expect(source).toContain("export const dashboardBuildTargets = {");
170
+ expect(source).toContain('resolve(workspaceRoot, "packages/core/src")');
171
+ expect(source).toContain('resolve(workspaceRoot, "packages/costs/src")');
172
+ expect(source).toContain('resolve(workspaceRoot, "packages/decisions/src")');
173
+ expect(source).toContain('resolve(workspaceRoot, "packages/lifecycle-manifests/src")');
174
+ expect(source).toContain('resolve(workspaceRoot, "packages/specs/src")');
175
+ expect(source).toContain('resolve(workspaceRoot, "packages/tasks/src")');
176
+ expect(source).toContain(
177
+ 'resolve(workspaceRoot, ".agents/dashboard/packages/data-export/src")',
178
+ );
179
+ expect(source).toContain('resolve(workspaceRoot, ".agents/dashboard/packages/contracts/src")');
180
+ expect(source).toContain('resolve(workspaceRoot, "pnpm-lock.yaml")');
181
+ expect(source).toContain('resolve(packageRoot, "scripts")');
182
+ expect(source).toContain('excludedSourcePaths: ["/server/"]');
183
+ expect(source).toContain('excludedSourcePaths: ["/react/"]');
184
+ expect(source).toContain("if (/\\.(?:test|spec)\\.[cm]?[jt]sx?$/.test(name)) return false;");
185
+ expect(source).toContain("state.missingArtifacts.length > 0");
186
+ expect(source).toContain("Conferindo somente as dependências que geram este artefato.");
187
+ expect(source).toContain("launchPage?.setProgress(progress)");
188
+ expect(source).toContain("percent: 100,");
189
+ });
190
+
191
+ it("builds only the stale target and honors missing artifacts and an explicit force", async () => {
192
+ const targets = {
193
+ frontend: { artifacts: ["frontend-artifact"], sourceRoots: [] },
194
+ runtime: { artifacts: ["runtime-artifact"], sourceRoots: [] },
195
+ };
196
+ const fresh = { artifactsModifiedAt: 20, missingArtifacts: [], sourcesModifiedAt: 10 };
197
+ const stale = { artifactsModifiedAt: 10, missingArtifacts: [], sourcesModifiedAt: 20 };
198
+ const missing = {
199
+ artifactsModifiedAt: 0,
200
+ missingArtifacts: ["missing"],
201
+ sourcesModifiedAt: 10,
202
+ };
203
+ const verify = async (states, forceRebuild, expected) => {
204
+ const calls = [];
205
+ const progress = [];
206
+ const result = await refreshDashboardBuildTargets({
207
+ targets,
208
+ forceRebuild,
209
+ inspectTarget: async (target) => states[target.artifacts[0]],
210
+ runBuild: async (target) => calls.push(target),
211
+ onProgress: (event) => progress.push(event),
212
+ });
213
+ expect(calls).toEqual(expected);
214
+ expect(result.builtTargets).toEqual(expected);
215
+ expect(progress.every((event) => typeof event.phase === "string")).toBe(true);
216
+ };
217
+
218
+ await verify({ "frontend-artifact": fresh, "runtime-artifact": stale }, false, ["runtime"]);
219
+ await verify({ "frontend-artifact": stale, "runtime-artifact": fresh }, false, ["frontend"]);
220
+ await verify({ "frontend-artifact": fresh, "runtime-artifact": fresh }, false, []);
221
+ await verify({ "frontend-artifact": missing, "runtime-artifact": fresh }, false, ["frontend"]);
222
+ await verify({ "frontend-artifact": fresh, "runtime-artifact": fresh }, true, [
223
+ "frontend",
224
+ "runtime",
225
+ ]);
226
+ });
227
+
95
228
  it("opens preparation before checking freshness and keeps it through server health", async () => {
96
229
  const source = await readFile(new URL("./start-dashboard.mjs", import.meta.url), "utf8");
97
230
  const lock = source.indexOf("await withLauncherLock(");
98
231
  const preparation = source.indexOf("launchPage = await startDashboardLaunchPage(0);");
99
- const freshness = source.indexOf("const buildSignature = await ensureFreshDashboardBuild();");
232
+ const freshness = source.indexOf(
233
+ "const buildSignature = await ensureFreshDashboardBuild((progress) =>",
234
+ );
100
235
  const server = source.indexOf(
101
236
  "const { child, port, dbPath, snapshotCache } = await startDashboardServer(",
102
237
  );
@@ -109,7 +244,7 @@ describe("dashboard executable session", () => {
109
244
  expect(redirectAfterServer).toBeGreaterThan(server);
110
245
  });
111
246
 
112
- it("reports readiness failures as actionable without retrying a build storm", async () => {
247
+ it("reports real readiness failures without imposing a wall-clock limit on a cold rebuild", async () => {
113
248
  const source = await readFile(new URL("./start-dashboard.mjs", import.meta.url), "utf8");
114
249
  const wait = source.slice(
115
250
  source.indexOf("async function waitForDashboard"),
@@ -117,7 +252,8 @@ describe("dashboard executable session", () => {
117
252
  );
118
253
  expect(wait).toContain("Dashboard data readiness returned ${readiness.status}");
119
254
  expect(wait).toContain("unavailable.retryable = false;");
120
- expect(wait).toContain("timedOut.retryable = false;");
255
+ expect(wait).toContain("while (true)");
256
+ expect(wait).not.toContain("startupTimeoutMs");
121
257
  expect(wait).toContain("if (error?.retryable === false) throw error;");
122
258
  expect(wait).toContain("await delay(100);");
123
259
  });
@@ -611,7 +747,7 @@ describe("dashboard executable session", () => {
611
747
  const preparation = source.indexOf("launchPage = await startDashboardLaunchPage(0);");
612
748
  const lock = source.indexOf("await withLauncherLock(");
613
749
  const buildSignature = source.indexOf(
614
- "const buildSignature = await ensureFreshDashboardBuild();",
750
+ "const buildSignature = await ensureFreshDashboardBuild((progress) =>",
615
751
  );
616
752
  const reusable = source.indexOf("const reusable = await findReusableDashboardSession({");
617
753
 
@@ -624,8 +760,13 @@ describe("dashboard executable session", () => {
624
760
  expect(source).toContain(
625
761
  'snapshotState: snapshotCache === "stale_seed" ? "updating" : "current",',
626
762
  );
627
- expect(source).toContain("launchPage.setFailure({");
628
- expect(source).toContain("launchPage.closeAfterFailureObservation();");
763
+ expect(source).toContain("presentLaunchFailure(launchPage, error, formatLauncherError)");
764
+ const lifecycleSource = await readFile(
765
+ new URL("./start-dashboard-lifecycle.mjs", import.meta.url),
766
+ "utf8",
767
+ );
768
+ expect(lifecycleSource).toContain("launchPage.setFailure({");
769
+ expect(lifecycleSource).toContain("launchPage.closeAfterFailureObservation();");
629
770
  expect(source).toContain('await writeLauncherStatus("ok");');
630
771
  });
631
772
 
@@ -640,15 +781,15 @@ describe("dashboard executable session", () => {
640
781
  expect(beforeLock).not.toContain("findAvailablePortWithFallback(");
641
782
  });
642
783
 
643
- it("gives the Windows wrapper margin beyond the bounded Node launcher wait", async () => {
784
+ it("keeps the Windows wrapper alive until Node reports success or a real failure", async () => {
644
785
  const source = await readFile(
645
786
  new URL("../windows-launcher/Program.cs", import.meta.url),
646
787
  "utf8",
647
788
  );
648
789
 
649
- expect(source).toContain("DateTime.UtcNow.AddSeconds(420)");
650
- expect(source).toContain("lock window (210 s)");
651
- expect(source).toContain("bounded 180 s server");
790
+ expect(source).toContain("while (true)");
791
+ expect(source).toContain("process.HasExited");
792
+ expect(source).not.toContain("AddSeconds(420)");
652
793
  });
653
794
  });
654
795