@synth-deploy/server 0.1.0 → 1.1.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 (96) hide show
  1. package/dist/agent/envoy-client.d.ts +62 -7
  2. package/dist/agent/envoy-client.d.ts.map +1 -1
  3. package/dist/agent/envoy-client.js +56 -6
  4. package/dist/agent/envoy-client.js.map +1 -1
  5. package/dist/agent/stale-deployment-detector.js +1 -1
  6. package/dist/agent/stale-deployment-detector.js.map +1 -1
  7. package/dist/agent/synth-agent.d.ts +7 -5
  8. package/dist/agent/synth-agent.d.ts.map +1 -1
  9. package/dist/agent/synth-agent.js +42 -39
  10. package/dist/agent/synth-agent.js.map +1 -1
  11. package/dist/alert-webhooks/alert-parsers.d.ts +21 -0
  12. package/dist/alert-webhooks/alert-parsers.d.ts.map +1 -0
  13. package/dist/alert-webhooks/alert-parsers.js +184 -0
  14. package/dist/alert-webhooks/alert-parsers.js.map +1 -0
  15. package/dist/api/agent.d.ts +0 -6
  16. package/dist/api/agent.d.ts.map +1 -1
  17. package/dist/api/agent.js +6 -459
  18. package/dist/api/agent.js.map +1 -1
  19. package/dist/api/alert-webhooks.d.ts +13 -0
  20. package/dist/api/alert-webhooks.d.ts.map +1 -0
  21. package/dist/api/alert-webhooks.js +279 -0
  22. package/dist/api/alert-webhooks.js.map +1 -0
  23. package/dist/api/envoy-reports.js +2 -2
  24. package/dist/api/envoy-reports.js.map +1 -1
  25. package/dist/api/envoys.js +1 -1
  26. package/dist/api/envoys.js.map +1 -1
  27. package/dist/api/fleet.d.ts.map +1 -1
  28. package/dist/api/fleet.js +14 -15
  29. package/dist/api/fleet.js.map +1 -1
  30. package/dist/api/graph.js +3 -3
  31. package/dist/api/graph.js.map +1 -1
  32. package/dist/api/operations.d.ts +7 -0
  33. package/dist/api/operations.d.ts.map +1 -0
  34. package/dist/api/operations.js +1883 -0
  35. package/dist/api/operations.js.map +1 -0
  36. package/dist/api/partitions.js +1 -1
  37. package/dist/api/partitions.js.map +1 -1
  38. package/dist/api/schemas.d.ts +194 -10
  39. package/dist/api/schemas.d.ts.map +1 -1
  40. package/dist/api/schemas.js +38 -5
  41. package/dist/api/schemas.js.map +1 -1
  42. package/dist/api/system.d.ts.map +1 -1
  43. package/dist/api/system.js +22 -21
  44. package/dist/api/system.js.map +1 -1
  45. package/dist/artifact-analyzer.js +2 -2
  46. package/dist/artifact-analyzer.js.map +1 -1
  47. package/dist/fleet/fleet-executor.js +1 -1
  48. package/dist/fleet/fleet-executor.js.map +1 -1
  49. package/dist/graph/graph-executor.js +2 -2
  50. package/dist/graph/graph-executor.js.map +1 -1
  51. package/dist/index.js +44 -40
  52. package/dist/index.js.map +1 -1
  53. package/dist/mcp/resources.js +3 -3
  54. package/dist/mcp/resources.js.map +1 -1
  55. package/dist/mcp/tools.d.ts.map +1 -1
  56. package/dist/mcp/tools.js +2 -9
  57. package/dist/mcp/tools.js.map +1 -1
  58. package/dist/middleware/auth.js +1 -1
  59. package/dist/middleware/auth.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/agent/envoy-client.ts +107 -15
  62. package/src/agent/stale-deployment-detector.ts +1 -1
  63. package/src/agent/synth-agent.ts +59 -45
  64. package/src/alert-webhooks/alert-parsers.ts +291 -0
  65. package/src/api/agent.ts +9 -528
  66. package/src/api/alert-webhooks.ts +354 -0
  67. package/src/api/envoy-reports.ts +2 -2
  68. package/src/api/envoys.ts +1 -1
  69. package/src/api/fleet.ts +14 -15
  70. package/src/api/graph.ts +3 -3
  71. package/src/api/operations.ts +2240 -0
  72. package/src/api/partitions.ts +1 -1
  73. package/src/api/schemas.ts +43 -7
  74. package/src/api/system.ts +23 -21
  75. package/src/artifact-analyzer.ts +2 -2
  76. package/src/fleet/fleet-executor.ts +1 -1
  77. package/src/graph/graph-executor.ts +2 -2
  78. package/src/index.ts +46 -40
  79. package/src/mcp/resources.ts +3 -3
  80. package/src/mcp/tools.ts +5 -9
  81. package/src/middleware/auth.ts +1 -1
  82. package/tests/agent-mode.test.ts +5 -376
  83. package/tests/api-handlers.test.ts +27 -27
  84. package/tests/composite-operations.test.ts +557 -0
  85. package/tests/decision-diary.test.ts +62 -63
  86. package/tests/diary-reader.test.ts +14 -18
  87. package/tests/mcp-tools.test.ts +1 -1
  88. package/tests/orchestration.test.ts +34 -30
  89. package/tests/partition-isolation.test.ts +4 -9
  90. package/tests/rbac-enforcement.test.ts +8 -8
  91. package/tests/ui-journey.test.ts +9 -9
  92. package/dist/api/deployments.d.ts +0 -11
  93. package/dist/api/deployments.d.ts.map +0 -1
  94. package/dist/api/deployments.js +0 -1098
  95. package/dist/api/deployments.js.map +0 -1
  96. package/src/api/deployments.ts +0 -1347
@@ -85,7 +85,7 @@ export function registerPartitionRoutes(
85
85
  // Log cascade deletion to Decision Diary
86
86
  debrief.record({
87
87
  partitionId: id,
88
- deploymentId: null,
88
+ operationId: null,
89
89
  agent: "server",
90
90
  decisionType: "system",
91
91
  decision: `Cascade-deleted partition "${partition.name}" with ${linkedDeployments.length} deployment(s)`,
@@ -211,6 +211,17 @@ export const UpdateSettingsSchema = z.object({
211
211
  description: z.string().optional(),
212
212
  })).optional(),
213
213
  llm: LlmProviderConfigSchema.partial().optional(),
214
+ approvalDefaults: z.object({
215
+ query: z.enum(["auto", "required"]).optional(),
216
+ investigate: z.enum(["auto", "required"]).optional(),
217
+ trigger: z.enum(["auto", "required"]).optional(),
218
+ deploy: z.enum(["auto", "required"]).optional(),
219
+ maintain: z.enum(["auto", "required"]).optional(),
220
+ composite: z.enum(["auto", "required"]).optional(),
221
+ environmentOverrides: z.record(
222
+ z.record(z.enum(["auto", "required"])).optional(),
223
+ ).optional(),
224
+ }).optional(),
214
225
  });
215
226
 
216
227
  // --- Artifacts (update) ---
@@ -232,6 +243,37 @@ export const CreateDeploymentSchema = z.object({
232
243
  version: z.string().optional(),
233
244
  });
234
245
 
246
+ const ChildOperationInputSchema = z.discriminatedUnion("type", [
247
+ z.object({ type: z.literal("deploy"), artifactId: z.string().min(1), artifactVersionId: z.string().optional() }),
248
+ z.object({ type: z.literal("maintain"), intent: z.string().min(1), parameters: z.record(z.unknown()).optional() }),
249
+ z.object({ type: z.literal("query"), intent: z.string().min(1) }),
250
+ z.object({ type: z.literal("investigate"), intent: z.string().min(1), allowWrite: z.boolean().optional() }),
251
+ z.object({ type: z.literal("trigger"), condition: z.string().min(1), responseIntent: z.string().min(1), parameters: z.record(z.unknown()).optional() }),
252
+ ]);
253
+
254
+ // --- Operations ---
255
+
256
+ export const CreateOperationSchema = z.object({
257
+ artifactId: z.string().min(1).optional(),
258
+ environmentId: z.string().min(1).optional(),
259
+ partitionId: z.string().optional(),
260
+ envoyId: z.string().optional(),
261
+ version: z.string().optional(),
262
+ type: z.enum(["deploy", "maintain", "query", "investigate", "trigger", "composite"]).default("deploy"),
263
+ intent: z.string().optional(),
264
+ allowWrite: z.boolean().optional(),
265
+ /** Trigger-specific: condition expression (e.g. "disk_usage > 85") */
266
+ condition: z.string().optional(),
267
+ /** Trigger-specific: what to do when the condition fires */
268
+ responseIntent: z.string().optional(),
269
+ /** Parent operation that spawned this one (e.g. investigation → resolution) */
270
+ parentOperationId: z.string().optional(),
271
+ /** Override to require manual approval even when auto-approve would apply */
272
+ requireApproval: z.boolean().optional(),
273
+ /** Composite-specific: child operations to execute sequentially */
274
+ operations: z.array(ChildOperationInputSchema).optional(),
275
+ });
276
+
235
277
  export const ApproveDeploymentSchema = z.object({
236
278
  approvedBy: z.string().min(1),
237
279
  modifications: z.string().optional(),
@@ -291,6 +333,7 @@ export const DebriefQuerySchema = z.object({
291
333
  limit: z.coerce.number().int().positive().optional(),
292
334
  partitionId: z.string().optional(),
293
335
  decisionType: z.string().optional(),
336
+ q: z.string().optional(),
294
337
  });
295
338
 
296
339
  // --- Progress Events (from envoy callback) ---
@@ -325,13 +368,6 @@ export const TelemetryQuerySchema = z.object({
325
368
  offset: z.coerce.number().int().nonnegative().optional(),
326
369
  });
327
370
 
328
- // --- Agent ---
329
-
330
- export const QueryRequestSchema = z.object({
331
- query: z.string().min(1),
332
- conversationId: z.string().optional(),
333
- });
334
-
335
371
  // --- Auth ---
336
372
 
337
373
  export const LoginSchema = z.object({
package/src/api/system.ts CHANGED
@@ -156,6 +156,8 @@ export function registerSystemRoutes(
156
156
  const allEnvoys = envoyRegistry.list();
157
157
  const allDeployments = deployments.list();
158
158
  const allEnvironments = environments.list();
159
+ const getArtifactId = (op: (typeof allDeployments)[number]): string =>
160
+ op.input?.type === "deploy" ? (op.input as { type: "deploy"; artifactId: string }).artifactId : "";
159
161
  const allPartitions = partitions.list();
160
162
 
161
163
  // --- Stats ---
@@ -264,11 +266,11 @@ export function registerSystemRoutes(
264
266
  { time: nowTime(), event: "Signal raised" },
265
267
  ],
266
268
  relatedDeployments: recentToEnvoy.map((d) => {
267
- const artName = allArtifacts.find((a) => a.id === d.artifactId)?.name ?? d.artifactId.slice(0, 8);
269
+ const artName = allArtifacts.find((a) => a.id === getArtifactId(d))?.name ?? getArtifactId(d).slice(0, 8);
268
270
  const envName = allEnvironments.find((e) => e.id === d.environmentId)?.name ?? "unknown";
269
271
  return {
270
272
  artifact: artName,
271
- version: d.version,
273
+ version: d.version ?? "",
272
274
  target: envName,
273
275
  status: d.status,
274
276
  time: timeAgo(d.createdAt),
@@ -291,9 +293,9 @@ export function registerSystemRoutes(
291
293
  type FailureGroup = { artifactId: string; environmentId: string | undefined; failures: typeof recentFailures };
292
294
  const failureGroups = new Map<string, FailureGroup>();
293
295
  for (const dep of recentFailures) {
294
- const key = `${dep.artifactId}::${dep.environmentId}`;
296
+ const key = `${getArtifactId(dep)}::${dep.environmentId}`;
295
297
  if (!failureGroups.has(key)) {
296
- failureGroups.set(key, { artifactId: dep.artifactId, environmentId: dep.environmentId, failures: [] });
298
+ failureGroups.set(key, { artifactId: getArtifactId(dep), environmentId: dep.environmentId, failures: [] });
297
299
  }
298
300
  failureGroups.get(key)!.failures.push(dep);
299
301
  }
@@ -303,7 +305,7 @@ export function registerSystemRoutes(
303
305
 
304
306
  const hasRecovery = allDeployments.some(
305
307
  (d) =>
306
- d.artifactId === group.artifactId &&
308
+ getArtifactId(d) === group.artifactId &&
307
309
  d.environmentId === group.environmentId &&
308
310
  d.status === "succeeded" &&
309
311
  new Date(d.createdAt).getTime() > new Date(group.failures[0].createdAt).getTime(),
@@ -320,7 +322,7 @@ export function registerSystemRoutes(
320
322
  const prevSuccessful = allDeployments
321
323
  .filter(
322
324
  (d) =>
323
- d.artifactId === group.artifactId &&
325
+ getArtifactId(d) === group.artifactId &&
324
326
  d.environmentId === group.environmentId &&
325
327
  d.status === "succeeded",
326
328
  )
@@ -379,7 +381,7 @@ export function registerSystemRoutes(
379
381
  ],
380
382
  relatedDeployments: sorted.map((d) => ({
381
383
  artifact: artifactName,
382
- version: d.version,
384
+ version: d.version ?? "",
383
385
  target: envName,
384
386
  status: d.status,
385
387
  time: timeAgo(d.createdAt),
@@ -397,7 +399,7 @@ export function registerSystemRoutes(
397
399
  type EnvLatest = { dep: (typeof succeededDeps)[0]; envName: string };
398
400
  const latestByTarget = new Map<string, EnvLatest>();
399
401
  for (const dep of succeededDeps) {
400
- const key = `${dep.artifactId}::${dep.environmentId}`;
402
+ const key = `${getArtifactId(dep)}::${dep.environmentId}`;
401
403
  const existing = latestByTarget.get(key);
402
404
  if (!existing || new Date(dep.createdAt) > new Date(existing.dep.createdAt)) {
403
405
  const envName = allEnvironments.find((e) => e.id === dep.environmentId)?.name ?? "unknown";
@@ -408,13 +410,13 @@ export function registerSystemRoutes(
408
410
  for (const { dep, envName } of latestByTarget.values()) {
409
411
  if (new Date(dep.createdAt).getTime() > thirtyDaysAgo) continue; // Not stale yet
410
412
 
411
- const artifactName = allArtifacts.find((a) => a.id === dep.artifactId)?.name ?? "unknown";
413
+ const artifactName = allArtifacts.find((a) => a.id === getArtifactId(dep))?.name ?? "unknown";
412
414
  const weeksAgo = Math.floor((now - new Date(dep.createdAt).getTime()) / (7 * 24 * 60 * 60 * 1000));
413
415
 
414
416
  // Check if newer versions of this artifact have been deployed to any other environment
415
417
  const newerElsewhere = succeededDeps.filter(
416
418
  (d) =>
417
- d.artifactId === dep.artifactId &&
419
+ getArtifactId(d) === getArtifactId(dep) &&
418
420
  d.environmentId !== dep.environmentId &&
419
421
  new Date(d.createdAt).getTime() > new Date(dep.createdAt).getTime(),
420
422
  );
@@ -427,7 +429,7 @@ export function registerSystemRoutes(
427
429
  severity: "info",
428
430
  title: `Stale deployment: ${artifactName} in ${envName}`,
429
431
  detail: `v${dep.version} deployed ${weeksAgo}w ago. ${newerVersions.length} newer version${newerVersions.length > 1 ? "s" : ""} running elsewhere. May be intentional.`,
430
- relatedEntity: { type: "artifact", id: dep.artifactId, name: artifactName },
432
+ relatedEntity: { type: "artifact", id: getArtifactId(dep), name: artifactName },
431
433
  investigation: {
432
434
  title: `Stale Deployment — ${artifactName} in ${envName}`,
433
435
  entity: `${artifactName} in ${envName}`,
@@ -461,10 +463,10 @@ export function registerSystemRoutes(
461
463
  { time: nowTime(), event: `Signal raised — ${weeksAgo}w without update, newer versions exist` },
462
464
  ],
463
465
  relatedDeployments: [
464
- { artifact: artifactName, version: dep.version, target: envName, status: "succeeded", time: timeAgo(dep.createdAt) },
466
+ { artifact: artifactName, version: dep.version ?? "", target: envName, status: "succeeded", time: timeAgo(dep.createdAt) },
465
467
  ...newerElsewhere.slice(0, 3).map((d) => {
466
468
  const env = allEnvironments.find((e) => e.id === d.environmentId)?.name ?? "unknown";
467
- return { artifact: artifactName, version: d.version, target: env, status: d.status, time: timeAgo(d.createdAt) };
469
+ return { artifact: artifactName, version: d.version ?? "", target: env, status: d.status, time: timeAgo(d.createdAt) };
468
470
  }),
469
471
  ],
470
472
  },
@@ -475,11 +477,11 @@ export function registerSystemRoutes(
475
477
  // pattern that suggests a missed or skipped promotion.
476
478
  const artifactEnvVersions = new Map<string, Map<string, { version: string; deployedAt: Date }>>();
477
479
  for (const { dep, envName } of latestByTarget.values()) {
478
- if (!artifactEnvVersions.has(dep.artifactId)) {
479
- artifactEnvVersions.set(dep.artifactId, new Map());
480
+ if (!artifactEnvVersions.has(getArtifactId(dep))) {
481
+ artifactEnvVersions.set(getArtifactId(dep), new Map());
480
482
  }
481
- artifactEnvVersions.get(dep.artifactId)!.set(envName, {
482
- version: dep.version,
483
+ artifactEnvVersions.get(getArtifactId(dep))!.set(envName, {
484
+ version: dep.version ?? "",
483
485
  deployedAt: new Date(dep.createdAt),
484
486
  });
485
487
  }
@@ -503,7 +505,7 @@ export function registerSystemRoutes(
503
505
 
504
506
  // Also require that the ahead env has more recent deployments of this artifact (not just same artifact)
505
507
  const aheadHasMultiple = succeededDeps.filter(
506
- (d) => d.artifactId === artifactId &&
508
+ (d) => getArtifactId(d) === artifactId &&
507
509
  allEnvironments.find((e) => e.id === d.environmentId)?.name === aheadEnv,
508
510
  ).length >= 2;
509
511
  if (!aheadHasMultiple) continue;
@@ -620,10 +622,10 @@ export function registerSystemRoutes(
620
622
  { time: nowTime(), event: "Signal raised" },
621
623
  ],
622
624
  relatedDeployments: recentToEnv.map((d) => {
623
- const artName = allArtifacts.find((a) => a.id === d.artifactId)?.name ?? d.artifactId.slice(0, 8);
625
+ const artName = allArtifacts.find((a) => a.id === getArtifactId(d))?.name ?? getArtifactId(d).slice(0, 8);
624
626
  return {
625
627
  artifact: artName,
626
- version: d.version,
628
+ version: d.version ?? "",
627
629
  target: env.name,
628
630
  status: d.status,
629
631
  time: timeAgo(d.createdAt),
@@ -682,7 +684,7 @@ export function registerSystemRoutes(
682
684
  detail = infos[0].detail;
683
685
  } else if (activeDeployments.length > 0) {
684
686
  const d = activeDeployments[0];
685
- const artName = allArtifacts.find((a) => a.id === d.artifactId)?.name ?? "A deployment";
687
+ const artName = allArtifacts.find((a) => a.id === getArtifactId(d))?.name ?? "A deployment";
686
688
  const envName = allEnvironments.find((e) => e.id === d.environmentId)?.name ?? "target";
687
689
  headline = "Deployment in progress.";
688
690
  detail = `${artName} is being deployed to ${envName}. All other environments are stable.`;
@@ -367,7 +367,7 @@ Produce a JSON analysis that incorporates all user corrections. Raise confidence
367
367
 
368
368
  this._debrief.record({
369
369
  partitionId: null,
370
- deploymentId: null,
370
+ operationId: null,
371
371
  agent: "server",
372
372
  decisionType: "artifact-analysis",
373
373
  decision: `Re-analyzed "${artifact.name}" with ${artifact.annotations.length} user correction(s). Confidence: ${revised.confidence}.`,
@@ -406,7 +406,7 @@ Produce a JSON analysis that incorporates all user corrections. Raise confidence
406
406
 
407
407
  this._debrief.record({
408
408
  partitionId: null,
409
- deploymentId: null,
409
+ operationId: null,
410
410
  agent: "server",
411
411
  decisionType: "artifact-analysis",
412
412
  decision: `Analyzed artifact "${artifact.name}" (${artifactType}) via ${method}. ` +
@@ -197,7 +197,7 @@ export class FleetExecutor {
197
197
  const client = this.createEnvoyClient(entry.url, entry.token);
198
198
  try {
199
199
  await client.executeApprovedPlan({
200
- deploymentId: fleetDeployment.id,
200
+ operationId: fleetDeployment.id,
201
201
  plan,
202
202
  rollbackPlan: rollbackPlan ?? plan,
203
203
  artifactType: "fleet",
@@ -243,7 +243,7 @@ export class GraphExecutor {
243
243
  : plan;
244
244
 
245
245
  const result: EnvoyDeployResult = await client.executeApprovedPlan({
246
- deploymentId: node.deploymentId ?? node.id,
246
+ operationId: node.deploymentId ?? node.id,
247
247
  plan: enrichedPlan,
248
248
  rollbackPlan: {
249
249
  steps: [],
@@ -416,7 +416,7 @@ export class GraphExecutor {
416
416
 
417
417
  try {
418
418
  await client.executeApprovedPlan({
419
- deploymentId: node.deploymentId ?? nodeId,
419
+ operationId: node.deploymentId ?? nodeId,
420
420
  plan,
421
421
  rollbackPlan: {
422
422
  steps: [],
package/src/index.ts CHANGED
@@ -9,13 +9,13 @@ import fastifyStatic from "@fastify/static";
9
9
  import fastifyFormBody from "@fastify/formbody";
10
10
  import fastifyMultipart from "@fastify/multipart";
11
11
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
12
- import { PersistentDecisionDebrief, openEntityDatabase, PersistentPartitionStore, PersistentEnvironmentStore, PersistentSettingsStore, PersistentDeploymentStore, PersistentArtifactStore, PersistentSecurityBoundaryStore, PersistentTelemetryStore, PersistentUserStore, PersistentRoleStore, PersistentUserRoleStore, PersistentSessionStore, PersistentIdpProviderStore, PersistentRoleMappingStore, PersistentApiKeyStore, PersistentEnvoyRegistryStore, PersistentRegistryPollerVersionStore, LlmClient, buildLlmConfigFromSettings, initEdition, EditionError } from "@synth-deploy/core";
12
+ import { PersistentDecisionDebrief, openEntityDatabase, PersistentPartitionStore, PersistentEnvironmentStore, PersistentSettingsStore, PersistentDeploymentStore, PersistentArtifactStore, PersistentSecurityBoundaryStore, PersistentTelemetryStore, PersistentUserStore, PersistentRoleStore, PersistentUserRoleStore, PersistentSessionStore, PersistentIdpProviderStore, PersistentRoleMappingStore, PersistentApiKeyStore, PersistentEnvoyRegistryStore, PersistentRegistryPollerVersionStore, PersistentAlertWebhookStore, LlmClient, buildLlmConfigFromSettings, initEdition, EditionError } from "@synth-deploy/core";
13
13
  import type { Deployment, Artifact, ArtifactVersion, SecurityBoundary, Permission, RoleId } from "@synth-deploy/core";
14
14
  import { SynthAgent } from "./agent/synth-agent.js";
15
15
  import { EnvoyHealthChecker } from "./agent/health-checker.js";
16
16
  import { McpClientManager } from "./agent/mcp-client-manager.js";
17
17
  import { createMcpServer } from "./mcp/server.js";
18
- import { registerDeploymentRoutes } from "./api/deployments.js";
18
+ import { registerOperationRoutes } from "./api/operations.js";
19
19
  import { registerHealthRoutes } from "./api/health.js";
20
20
  import { registerEnvoyReportRoutes } from "./api/envoy-reports.js";
21
21
  import { registerArtifactRoutes } from "./api/artifacts.js";
@@ -41,6 +41,7 @@ import { registerFleetRoutes } from "./api/fleet.js";
41
41
  import { FleetDeploymentStore, FleetExecutor } from "./fleet/index.js";
42
42
  import { IntakeChannelStore, IntakeEventStore, IntakeProcessor, RegistryPoller } from "./intake/index.js";
43
43
  import { registerIntakeRoutes } from "./api/intake.js";
44
+ import { registerAlertWebhookRoutes } from "./api/alert-webhooks.js";
44
45
  import { ArtifactAnalyzer } from "./artifact-analyzer.js";
45
46
  import { DeploymentGraphStore, GraphInferenceEngine } from "./graph/index.js";
46
47
  import { registerGraphRoutes } from "./api/graph.js";
@@ -291,7 +292,7 @@ if (process.env.SYNTH_SEED_DEMO !== 'false' && partitions.list().length === 0) {
291
292
  // --- Deployments (mix of statuses and ages) ---
292
293
 
293
294
  const dep1: Deployment = {
294
- id: crypto.randomUUID() as Deployment["id"], artifactId: webAppArtifact.id as Deployment["artifactId"], partitionId: acmePartition.id as Deployment["partitionId"],
295
+ id: crypto.randomUUID() as Deployment["id"], input: { type: "deploy" as const, artifactId: webAppArtifact.id }, partitionId: acmePartition.id as Deployment["partitionId"],
295
296
  environmentId: prodEnv.id as Deployment["environmentId"], version: "2.3.0", status: "succeeded",
296
297
  variables: { ...acmePartition.variables, ...prodEnv.variables },
297
298
  plan: {
@@ -311,21 +312,21 @@ if (process.env.SYNTH_SEED_DEMO !== 'false' && partitions.list().length === 0) {
311
312
  createdAt: hoursAgo(72), completedAt: hoursAgo(71.5), failureReason: undefined,
312
313
  };
313
314
  const dep2: Deployment = {
314
- id: crypto.randomUUID() as Deployment["id"], artifactId: webAppArtifact.id as Deployment["artifactId"], partitionId: acmePartition.id as Deployment["partitionId"],
315
+ id: crypto.randomUUID() as Deployment["id"], input: { type: "deploy" as const, artifactId: webAppArtifact.id }, partitionId: acmePartition.id as Deployment["partitionId"],
315
316
  environmentId: prodEnv.id as Deployment["environmentId"], version: "2.4.0", status: "succeeded",
316
317
  variables: { ...acmePartition.variables, ...prodEnv.variables },
317
318
  debriefEntryIds: [],
318
319
  createdAt: hoursAgo(48), completedAt: hoursAgo(47.8), failureReason: undefined,
319
320
  };
320
321
  const dep3: Deployment = {
321
- id: crypto.randomUUID() as Deployment["id"], artifactId: webAppArtifact.id as Deployment["artifactId"], partitionId: acmePartition.id as Deployment["partitionId"],
322
+ id: crypto.randomUUID() as Deployment["id"], input: { type: "deploy" as const, artifactId: webAppArtifact.id }, partitionId: acmePartition.id as Deployment["partitionId"],
322
323
  environmentId: prodEnv.id as Deployment["environmentId"], version: "2.4.1", status: "succeeded",
323
324
  variables: { ...acmePartition.variables, ...prodEnv.variables },
324
325
  debriefEntryIds: [],
325
326
  createdAt: hoursAgo(24), completedAt: hoursAgo(23.7), failureReason: undefined,
326
327
  };
327
328
  const dep4: Deployment = {
328
- id: crypto.randomUUID() as Deployment["id"], artifactId: apiArtifact.id as Deployment["artifactId"], partitionId: acmePartition.id as Deployment["partitionId"],
329
+ id: crypto.randomUUID() as Deployment["id"], input: { type: "deploy" as const, artifactId: apiArtifact.id }, partitionId: acmePartition.id as Deployment["partitionId"],
329
330
  environmentId: prodEnv.id as Deployment["environmentId"], version: "1.11.0", status: "failed",
330
331
  variables: { ...acmePartition.variables, ...prodEnv.variables },
331
332
  debriefEntryIds: [],
@@ -333,21 +334,21 @@ if (process.env.SYNTH_SEED_DEMO !== 'false' && partitions.list().length === 0) {
333
334
  failureReason: "Health check failed after 3 retries: connection refused on port 8080",
334
335
  };
335
336
  const dep5: Deployment = {
336
- id: crypto.randomUUID() as Deployment["id"], artifactId: apiArtifact.id as Deployment["artifactId"], partitionId: acmePartition.id as Deployment["partitionId"],
337
+ id: crypto.randomUUID() as Deployment["id"], input: { type: "deploy" as const, artifactId: apiArtifact.id }, partitionId: acmePartition.id as Deployment["partitionId"],
337
338
  environmentId: prodEnv.id as Deployment["environmentId"], version: "1.12.0", status: "succeeded",
338
339
  variables: { ...acmePartition.variables, ...prodEnv.variables },
339
340
  debriefEntryIds: [],
340
341
  createdAt: hoursAgo(12), completedAt: hoursAgo(11.8), failureReason: undefined,
341
342
  };
342
343
  const dep6: Deployment = {
343
- id: crypto.randomUUID() as Deployment["id"], artifactId: webAppArtifact.id as Deployment["artifactId"], partitionId: globexPartition.id as Deployment["partitionId"],
344
+ id: crypto.randomUUID() as Deployment["id"], input: { type: "deploy" as const, artifactId: webAppArtifact.id }, partitionId: globexPartition.id as Deployment["partitionId"],
344
345
  environmentId: stagingEnv.id as Deployment["environmentId"], version: "2.5.0-rc.1", status: "succeeded",
345
346
  variables: { ...globexPartition.variables, ...stagingEnv.variables },
346
347
  debriefEntryIds: [],
347
348
  createdAt: hoursAgo(6), completedAt: hoursAgo(5.8), failureReason: undefined,
348
349
  };
349
350
  const dep7: Deployment = {
350
- id: crypto.randomUUID() as Deployment["id"], artifactId: workerArtifact.id as Deployment["artifactId"], partitionId: initechPartition.id as Deployment["partitionId"],
351
+ id: crypto.randomUUID() as Deployment["id"], input: { type: "deploy" as const, artifactId: workerArtifact.id }, partitionId: initechPartition.id as Deployment["partitionId"],
351
352
  environmentId: prodEnv.id as Deployment["environmentId"], version: "2.9.0", status: "failed",
352
353
  variables: { ...initechPartition.variables, ...prodEnv.variables },
353
354
  debriefEntryIds: [],
@@ -355,14 +356,14 @@ if (process.env.SYNTH_SEED_DEMO !== 'false' && partitions.list().length === 0) {
355
356
  failureReason: "Queue depth exceeded threshold (342 > 100) during verification",
356
357
  };
357
358
  const dep8: Deployment = {
358
- id: crypto.randomUUID() as Deployment["id"], artifactId: workerArtifact.id as Deployment["artifactId"], partitionId: initechPartition.id as Deployment["partitionId"],
359
+ id: crypto.randomUUID() as Deployment["id"], input: { type: "deploy" as const, artifactId: workerArtifact.id }, partitionId: initechPartition.id as Deployment["partitionId"],
359
360
  environmentId: prodEnv.id as Deployment["environmentId"], version: "3.0.0", status: "succeeded",
360
361
  variables: { ...initechPartition.variables, ...prodEnv.variables },
361
362
  debriefEntryIds: [],
362
363
  createdAt: hoursAgo(3), completedAt: hoursAgo(2.7), failureReason: undefined,
363
364
  };
364
365
  const dep9: Deployment = {
365
- id: crypto.randomUUID() as Deployment["id"], artifactId: apiArtifact.id as Deployment["artifactId"], partitionId: globexPartition.id as Deployment["partitionId"],
366
+ id: crypto.randomUUID() as Deployment["id"], input: { type: "deploy" as const, artifactId: apiArtifact.id }, partitionId: globexPartition.id as Deployment["partitionId"],
366
367
  environmentId: stagingEnv.id as Deployment["environmentId"], version: "1.13.0-beta.2", status: "running",
367
368
  variables: { ...globexPartition.variables, ...stagingEnv.variables },
368
369
  plan: {
@@ -378,7 +379,7 @@ if (process.env.SYNTH_SEED_DEMO !== 'false' && partitions.list().length === 0) {
378
379
  createdAt: hoursAgo(0.5),
379
380
  };
380
381
  const dep11: Deployment = {
381
- id: crypto.randomUUID() as Deployment["id"], artifactId: workerArtifact.id as Deployment["artifactId"], partitionId: globexPartition.id as Deployment["partitionId"],
382
+ id: crypto.randomUUID() as Deployment["id"], input: { type: "deploy" as const, artifactId: workerArtifact.id }, partitionId: globexPartition.id as Deployment["partitionId"],
382
383
  environmentId: prodEnv.id as Deployment["environmentId"], version: "3.1.0", status: "awaiting_approval",
383
384
  variables: { ...globexPartition.variables, ...prodEnv.variables },
384
385
  plan: {
@@ -396,7 +397,7 @@ if (process.env.SYNTH_SEED_DEMO !== 'false' && partitions.list().length === 0) {
396
397
  createdAt: hoursAgo(0.1),
397
398
  };
398
399
  const dep10: Deployment = {
399
- id: crypto.randomUUID() as Deployment["id"], artifactId: webAppArtifact.id as Deployment["artifactId"], partitionId: initechPartition.id as Deployment["partitionId"],
400
+ id: crypto.randomUUID() as Deployment["id"], input: { type: "deploy" as const, artifactId: webAppArtifact.id }, partitionId: initechPartition.id as Deployment["partitionId"],
400
401
  environmentId: prodEnv.id as Deployment["environmentId"], version: "2.4.1", status: "rolled_back",
401
402
  variables: { ...initechPartition.variables, ...prodEnv.variables },
402
403
  debriefEntryIds: [],
@@ -428,7 +429,7 @@ if (process.env.SYNTH_SEED_DEMO !== 'false' && partitions.list().length === 0) {
428
429
  // --- Debrief entries (rich decision diary) ---
429
430
 
430
431
  debrief.record({
431
- partitionId: null, deploymentId: null, agent: "server", decisionType: "system",
432
+ partitionId: null, operationId: null, agent: "server", decisionType: "system",
432
433
  decision: "Command initialized with demo data",
433
434
  reasoning: "Seeded 3 partitions, 3 environments, 3 artifacts, 10 deployments, and 2 envoy security boundary sets.",
434
435
  context: { partitions: 3, environments: 3, deployments: 10, artifacts: 3, securityBoundaries: 2 },
@@ -436,31 +437,31 @@ if (process.env.SYNTH_SEED_DEMO !== 'false' && partitions.list().length === 0) {
436
437
 
437
438
  // dep1 — web-app 2.3.0 succeeded
438
439
  debrief.record({
439
- partitionId: acmePartition.id, deploymentId: dep1.id, agent: "server", decisionType: "pipeline-plan",
440
+ partitionId: acmePartition.id, operationId: dep1.id, agent: "server", decisionType: "pipeline-plan",
440
441
  decision: "Planned deployment pipeline for web-app v2.3.0 to Acme Corp production",
441
442
  reasoning: "Standard 3-step pipeline: install deps, run migrations, health check. No variable conflicts.",
442
443
  context: { version: "2.3.0", steps: 3 },
443
444
  });
444
445
  debrief.record({
445
- partitionId: acmePartition.id, deploymentId: dep1.id, agent: "server", decisionType: "configuration-resolved",
446
+ partitionId: acmePartition.id, operationId: dep1.id, agent: "server", decisionType: "configuration-resolved",
446
447
  decision: "Resolved 4 variables for Acme Corp production (partition + environment merged)",
447
448
  reasoning: "Merged partition variables (APP_ENV, DB_HOST, REGION) with environment variables (APP_ENV, LOG_LEVEL). APP_ENV conflict resolved: environment value takes precedence.",
448
449
  context: { resolvedCount: 4, conflicts: 1, policy: "environment-wins" },
449
450
  });
450
451
  debrief.record({
451
- partitionId: acmePartition.id, deploymentId: dep1.id, agent: "envoy", decisionType: "deployment-execution",
452
+ partitionId: acmePartition.id, operationId: dep1.id, agent: "envoy", decisionType: "deployment-execution",
452
453
  decision: "Executed deployment web-app v2.3.0 on Acme Corp production",
453
454
  reasoning: "All 3 steps completed. Total execution time: 28.4s.",
454
455
  context: { duration: 28400 },
455
456
  });
456
457
  debrief.record({
457
- partitionId: acmePartition.id, deploymentId: dep1.id, agent: "envoy", decisionType: "health-check",
458
+ partitionId: acmePartition.id, operationId: dep1.id, agent: "envoy", decisionType: "health-check",
458
459
  decision: "Health check passed on first attempt",
459
460
  reasoning: "GET /health returned 200 with body {\"status\":\"ok\"} in 45ms.",
460
461
  context: { attempts: 1, responseTime: 45 },
461
462
  });
462
463
  debrief.record({
463
- partitionId: acmePartition.id, deploymentId: dep1.id, agent: "server", decisionType: "deployment-completion",
464
+ partitionId: acmePartition.id, operationId: dep1.id, agent: "server", decisionType: "deployment-completion",
464
465
  decision: "Deployment web-app v2.3.0 completed successfully",
465
466
  reasoning: "All pipeline steps passed. Health check confirmed. Marked as succeeded.",
466
467
  context: { status: "succeeded" },
@@ -468,31 +469,31 @@ if (process.env.SYNTH_SEED_DEMO !== 'false' && partitions.list().length === 0) {
468
469
 
469
470
  // dep4 — api-service 1.11.0 failed
470
471
  debrief.record({
471
- partitionId: acmePartition.id, deploymentId: dep4.id, agent: "server", decisionType: "pipeline-plan",
472
+ partitionId: acmePartition.id, operationId: dep4.id, agent: "server", decisionType: "pipeline-plan",
472
473
  decision: "Planned deployment pipeline for api-service v1.11.0 to Acme Corp production",
473
474
  reasoning: "2-step pipeline: pull image, verify endpoint.",
474
475
  context: { version: "1.11.0", steps: 2 },
475
476
  });
476
477
  debrief.record({
477
- partitionId: acmePartition.id, deploymentId: dep4.id, agent: "envoy", decisionType: "deployment-execution",
478
+ partitionId: acmePartition.id, operationId: dep4.id, agent: "envoy", decisionType: "deployment-execution",
478
479
  decision: "Image pull succeeded, starting verification",
479
480
  reasoning: "docker pull completed in 12.3s. Image sha256:a4f8e... verified.",
480
481
  context: { step: "Pull image", duration: 12300 },
481
482
  });
482
483
  debrief.record({
483
- partitionId: acmePartition.id, deploymentId: dep4.id, agent: "envoy", decisionType: "health-check",
484
+ partitionId: acmePartition.id, operationId: dep4.id, agent: "envoy", decisionType: "health-check",
484
485
  decision: "Health check failed after 3 retries",
485
486
  reasoning: "Connection refused on port 8080. Retry 1: refused (5s). Retry 2: refused (10s). Retry 3: refused (15s). Container logs: \"Error: EADDRINUSE :::8080\".",
486
487
  context: { attempts: 3, lastError: "ECONNREFUSED", containerLog: "EADDRINUSE" },
487
488
  });
488
489
  debrief.record({
489
- partitionId: acmePartition.id, deploymentId: dep4.id, agent: "envoy", decisionType: "diagnostic-investigation",
490
+ partitionId: acmePartition.id, operationId: dep4.id, agent: "envoy", decisionType: "diagnostic-investigation",
490
491
  decision: "Root cause: port 8080 bound by stale process from previous deployment",
491
492
  reasoning: "Found zombie process from api-service v1.10.0 holding port 8080. Previous deployment did not cleanly shut down.",
492
493
  context: { rootCause: "port-conflict", stalePid: 14823 },
493
494
  });
494
495
  debrief.record({
495
- partitionId: acmePartition.id, deploymentId: dep4.id, agent: "server", decisionType: "deployment-failure",
496
+ partitionId: acmePartition.id, operationId: dep4.id, agent: "server", decisionType: "deployment-failure",
496
497
  decision: "Deployment api-service v1.11.0 failed — health check could not connect",
497
498
  reasoning: "Envoy diagnostic identified port conflict from stale process. Recommend adding a pre-deploy cleanup step.",
498
499
  context: { status: "failed", recommendation: "Add cleanup step" },
@@ -500,25 +501,25 @@ if (process.env.SYNTH_SEED_DEMO !== 'false' && partitions.list().length === 0) {
500
501
 
501
502
  // dep7 — worker-service 2.9.0 failed
502
503
  debrief.record({
503
- partitionId: initechPartition.id, deploymentId: dep7.id, agent: "server", decisionType: "pipeline-plan",
504
+ partitionId: initechPartition.id, operationId: dep7.id, agent: "server", decisionType: "pipeline-plan",
504
505
  decision: "Planned deployment pipeline for worker-service v2.9.0 to Initech production",
505
506
  reasoning: "4-step pipeline with full verification strategy.",
506
507
  context: { version: "2.9.0", steps: 4, verificationStrategy: "full" },
507
508
  });
508
509
  debrief.record({
509
- partitionId: initechPartition.id, deploymentId: dep7.id, agent: "envoy", decisionType: "deployment-execution",
510
+ partitionId: initechPartition.id, operationId: dep7.id, agent: "envoy", decisionType: "deployment-execution",
510
511
  decision: "Workers stopped and binary deployed successfully",
511
512
  reasoning: "Pre-deploy steps completed. Workers stopped gracefully (0 in-flight jobs lost). Binary copied.",
512
513
  context: { stepsCompleted: 2, jobsLost: 0 },
513
514
  });
514
515
  debrief.record({
515
- partitionId: initechPartition.id, deploymentId: dep7.id, agent: "envoy", decisionType: "deployment-verification",
516
+ partitionId: initechPartition.id, operationId: dep7.id, agent: "envoy", decisionType: "deployment-verification",
516
517
  decision: "Verification failed: queue depth 342 exceeds threshold of 100",
517
518
  reasoning: "Workers restarted but queue depth grew rapidly. v2.9.0 introduced a regression in the message processing loop causing 10x slowdown.",
518
519
  context: { queueDepth: 342, threshold: 100, processingRate: "0.3/s vs expected 3/s" },
519
520
  });
520
521
  debrief.record({
521
- partitionId: initechPartition.id, deploymentId: dep7.id, agent: "server", decisionType: "deployment-failure",
522
+ partitionId: initechPartition.id, operationId: dep7.id, agent: "server", decisionType: "deployment-failure",
522
523
  decision: "Deployment worker-service v2.9.0 failed — queue depth exceeded threshold",
523
524
  reasoning: "Queue depth check returned 342 (max 100). Processing regression in v2.9.0.",
524
525
  context: { status: "failed" },
@@ -526,25 +527,25 @@ if (process.env.SYNTH_SEED_DEMO !== 'false' && partitions.list().length === 0) {
526
527
 
527
528
  // dep10 — web-app 2.4.1 rolled back
528
529
  debrief.record({
529
- partitionId: initechPartition.id, deploymentId: dep10.id, agent: "server", decisionType: "pipeline-plan",
530
+ partitionId: initechPartition.id, operationId: dep10.id, agent: "server", decisionType: "pipeline-plan",
530
531
  decision: "Planned deployment pipeline for web-app v2.4.1 to Initech production",
531
532
  reasoning: "Standard 3-step pipeline.",
532
533
  context: { version: "2.4.1", steps: 3 },
533
534
  });
534
535
  debrief.record({
535
- partitionId: initechPartition.id, deploymentId: dep10.id, agent: "envoy", decisionType: "deployment-execution",
536
+ partitionId: initechPartition.id, operationId: dep10.id, agent: "envoy", decisionType: "deployment-execution",
536
537
  decision: "All deployment steps completed, starting post-deploy verification",
537
538
  reasoning: "Dependencies installed (14.2s), migrations ran (3.1s), health check passed (0.2s).",
538
539
  context: { totalDuration: 17500 },
539
540
  });
540
541
  debrief.record({
541
- partitionId: initechPartition.id, deploymentId: dep10.id, agent: "envoy", decisionType: "deployment-verification",
542
+ partitionId: initechPartition.id, operationId: dep10.id, agent: "envoy", decisionType: "deployment-verification",
542
543
  decision: "Post-deploy smoke test detected 502 errors on /api/v2/users",
543
544
  reasoning: "12 endpoint checks: 10 passed, 2 returned 502 (GET and POST /api/v2/users). The v2 users endpoint depends on a schema migration that was partially applied.",
544
545
  context: { passed: 10, failed: 2, failedEndpoints: ["/api/v2/users"] },
545
546
  });
546
547
  debrief.record({
547
- partitionId: initechPartition.id, deploymentId: dep10.id, agent: "server", decisionType: "deployment-failure",
548
+ partitionId: initechPartition.id, operationId: dep10.id, agent: "server", decisionType: "deployment-failure",
548
549
  decision: "Initiated rollback of web-app v2.4.1 on Initech production",
549
550
  reasoning: "502 errors on critical user endpoints. Rolling back to previous known-good version.",
550
551
  context: { status: "rolled_back", rolledBackFrom: "2.4.1" },
@@ -552,19 +553,19 @@ if (process.env.SYNTH_SEED_DEMO !== 'false' && partitions.list().length === 0) {
552
553
 
553
554
  // dep6 — web-app 2.5.0-rc.1 with variable conflict
554
555
  debrief.record({
555
- partitionId: globexPartition.id, deploymentId: dep6.id, agent: "server", decisionType: "pipeline-plan",
556
+ partitionId: globexPartition.id, operationId: dep6.id, agent: "server", decisionType: "pipeline-plan",
556
557
  decision: "Planned deployment for web-app v2.5.0-rc.1 to Globex staging",
557
558
  reasoning: "Standard 3-step pipeline. Release candidate — permissive conflict policy.",
558
559
  context: { version: "2.5.0-rc.1", steps: 3 },
559
560
  });
560
561
  debrief.record({
561
- partitionId: globexPartition.id, deploymentId: dep6.id, agent: "server", decisionType: "variable-conflict",
562
+ partitionId: globexPartition.id, operationId: dep6.id, agent: "server", decisionType: "variable-conflict",
562
563
  decision: "Variable conflict: APP_ENV defined in both partition and environment",
563
564
  reasoning: "Partition sets APP_ENV=production, environment sets APP_ENV=staging. Permissive policy — using environment value.",
564
565
  context: { variable: "APP_ENV", partitionValue: "production", environmentValue: "staging", resolution: "environment-wins" },
565
566
  });
566
567
  debrief.record({
567
- partitionId: globexPartition.id, deploymentId: dep6.id, agent: "server", decisionType: "deployment-completion",
568
+ partitionId: globexPartition.id, operationId: dep6.id, agent: "server", decisionType: "deployment-completion",
568
569
  decision: "Deployment web-app v2.5.0-rc.1 completed on Globex staging",
569
570
  reasoning: "All steps passed despite variable conflict. RC verified in staging.",
570
571
  context: { status: "succeeded" },
@@ -572,13 +573,13 @@ if (process.env.SYNTH_SEED_DEMO !== 'false' && partitions.list().length === 0) {
572
573
 
573
574
  // dep9 — in-progress
574
575
  debrief.record({
575
- partitionId: globexPartition.id, deploymentId: dep9.id, agent: "server", decisionType: "pipeline-plan",
576
+ partitionId: globexPartition.id, operationId: dep9.id, agent: "server", decisionType: "pipeline-plan",
576
577
  decision: "Planned deployment for api-service v1.13.0-beta.2 to Globex staging",
577
578
  reasoning: "2-step pipeline for staging. Beta version — monitoring closely.",
578
579
  context: { version: "1.13.0-beta.2", steps: 2 },
579
580
  });
580
581
  debrief.record({
581
- partitionId: globexPartition.id, deploymentId: dep9.id, agent: "envoy", decisionType: "deployment-execution",
582
+ partitionId: globexPartition.id, operationId: dep9.id, agent: "envoy", decisionType: "deployment-execution",
582
583
  decision: "Image pull in progress for api-service v1.13.0-beta.2",
583
584
  reasoning: "Pulling docker image from registry. Download progress: 67%.",
584
585
  context: { step: "Pull image", progress: "67%" },
@@ -586,13 +587,13 @@ if (process.env.SYNTH_SEED_DEMO !== 'false' && partitions.list().length === 0) {
586
587
 
587
588
  // Environment scans
588
589
  debrief.record({
589
- partitionId: acmePartition.id, deploymentId: null, agent: "envoy", decisionType: "environment-scan",
590
+ partitionId: acmePartition.id, operationId: null, agent: "envoy", decisionType: "environment-scan",
590
591
  decision: "Environment scan completed for Acme Corp production",
591
592
  reasoning: "Current versions: web-app v2.4.1, api-service v1.12.0. Disk: 62%. Memory: 71%. No drift detected.",
592
593
  context: { versions: { "web-app": "2.4.1", "api-service": "1.12.0" }, diskUsage: "62%", memoryUsage: "71%" },
593
594
  });
594
595
  debrief.record({
595
- partitionId: initechPartition.id, deploymentId: null, agent: "envoy", decisionType: "environment-scan",
596
+ partitionId: initechPartition.id, operationId: null, agent: "envoy", decisionType: "environment-scan",
596
597
  decision: "Environment scan for Initech production — drift detected",
597
598
  reasoning: "worker-service v3.0.0 running. web-app at v2.4.0 (v2.4.1 was rolled back). Drift: LOG_LEVEL manually changed from 'warn' to 'debug' outside deployment pipeline.",
598
599
  context: { drift: true, driftDetails: "LOG_LEVEL changed outside pipeline" },
@@ -706,7 +707,7 @@ registerHealthRoutes(app, {
706
707
  });
707
708
  const progressStore = new ProgressEventStore();
708
709
  const defaultEnvoyClient = new EnvoyClient(settings.get().envoy.url, settings.get().envoy.timeoutMs);
709
- registerDeploymentRoutes(app, deployments, debrief, partitions, environments, artifactStore, settings, telemetryStore, progressStore, defaultEnvoyClient, envoyRegistry, llm);
710
+ registerOperationRoutes(app, deployments, debrief, partitions, environments, artifactStore, settings, telemetryStore, progressStore, defaultEnvoyClient, envoyRegistry, llm);
710
711
  registerEnvoyReportRoutes(app, debrief, deployments, envoyRegistry);
711
712
  registerArtifactRoutes(app, artifactStore, telemetryStore, artifactAnalyzer);
712
713
  registerSecurityBoundaryRoutes(app, securityBoundaryStore, telemetryStore);
@@ -757,6 +758,11 @@ for (const ch of intakeChannelStore.list()) {
757
758
  }
758
759
  }
759
760
 
761
+ // --- Alert Webhooks (external monitoring triggers) ---
762
+
763
+ const alertWebhookStore = new PersistentAlertWebhookStore(entityDb);
764
+ registerAlertWebhookRoutes(app, alertWebhookStore, deployments, debrief, environments, partitions, telemetryStore, envoyRegistry);
765
+
760
766
  // --- Serve UI static files if built ---
761
767
 
762
768
  const __dirname = path.dirname(fileURLToPath(import.meta.url));