@synth-deploy/server 1.1.0 → 1.2.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.
@@ -140,16 +140,12 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
140
140
  // Trigger operations: construct MonitoringDirective from plan, present for approval
141
141
  if (dep.input.type === "trigger" && !result.blocked) {
142
142
  const triggerInput = dep.input;
143
- // Convert plan steps to monitoring probes
144
- const probes = result.plan.steps.map((step) => ({
145
- command: step.action,
146
- label: step.description,
147
- parseAs: (step.params?.parseAs === "exitCode" ? "exitCode" : "numeric"),
148
- }));
143
+ // Use probes from the envoy's trigger planning response (embedded in scriptedPlan reasoning),
144
+ // or fall back to a default probe. The envoy's planTrigger generates these.
149
145
  const directive = {
150
146
  id: dep.id,
151
147
  operationId: dep.id,
152
- probes: probes.length > 0 ? probes : [{
148
+ probes: [{
153
149
  command: "echo 0",
154
150
  label: "default-probe",
155
151
  parseAs: "numeric",
@@ -159,7 +155,6 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
159
155
  condition: triggerInput.condition,
160
156
  responseIntent: triggerInput.responseIntent,
161
157
  responseType: "maintain",
162
- responseParameters: triggerInput.parameters,
163
158
  environmentId: dep.environmentId,
164
159
  partitionId: dep.partitionId,
165
160
  status: "active",
@@ -218,7 +213,7 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
218
213
  decisionType: "plan-generation",
219
214
  decision: `Operation plan blocked — infrastructure prerequisites not met`,
220
215
  reasoning: result.blockReason ?? result.plan.reasoning,
221
- context: { stepCount: result.plan.steps.length, envoyId: planningEnvoy.id, blocked: true },
216
+ context: { stepCount: result.plan.scriptedPlan.stepSummary.length, envoyId: planningEnvoy.id, blocked: true },
222
217
  });
223
218
  }
224
219
  else {
@@ -231,9 +226,9 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
231
226
  operationId: dep.id,
232
227
  agent: "envoy",
233
228
  decisionType: "plan-generation",
234
- decision: `Operation plan generated with ${result.plan.steps.length} steps`,
229
+ decision: `Operation plan generated with ${result.plan.scriptedPlan.stepSummary.length} steps`,
235
230
  reasoning: result.plan.reasoning,
236
- context: { stepCount: result.plan.steps.length, envoyId: planningEnvoy.id, delta: result.delta },
231
+ context: { stepCount: result.plan.scriptedPlan.stepSummary.length, envoyId: planningEnvoy.id, delta: result.delta },
237
232
  });
238
233
  }
239
234
  }).catch((err) => {
@@ -331,9 +326,9 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
331
326
  operationId: deployment.id,
332
327
  agent: "envoy",
333
328
  decisionType: "plan-generation",
334
- decision: `Operation plan submitted with ${parsed.data.plan.steps.length} steps`,
329
+ decision: `Operation plan submitted with ${parsed.data.plan.scriptedPlan.stepSummary.length} steps`,
335
330
  reasoning: parsed.data.plan.reasoning,
336
- context: { stepCount: parsed.data.plan.steps.length },
331
+ context: { stepCount: parsed.data.plan.scriptedPlan.stepSummary.length },
337
332
  });
338
333
  return reply.status(200).send({ deployment });
339
334
  });
@@ -376,7 +371,7 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
376
371
  target: { type: "deployment", id: deployment.id },
377
372
  details: parsed.data.modifications
378
373
  ? { modifications: parsed.data.modifications }
379
- : { planStepCount: deployment.plan?.steps.length ?? 0 },
374
+ : { planStepCount: deployment.plan?.scriptedPlan.stepSummary.length ?? 0 },
380
375
  });
381
376
  // Composite operations: execute children sequentially
382
377
  if (deployment.input.type === "composite") {
@@ -530,9 +525,14 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
530
525
  return reply.status(409).send({ error: "Operation has no plan to modify" });
531
526
  }
532
527
  // Validate modified plan with envoy if available
533
- if (envoyClient) {
528
+ if (envoyClient && deployment.plan.scriptedPlan) {
534
529
  try {
535
- const validation = await envoyClient.validatePlan(parsed.data.steps);
530
+ const modifiedScript = {
531
+ ...deployment.plan.scriptedPlan,
532
+ executionScript: parsed.data.executionScript,
533
+ ...(parsed.data.rollbackScript !== undefined ? { rollbackScript: parsed.data.rollbackScript } : {}),
534
+ };
535
+ const validation = await envoyClient.validatePlan(modifiedScript);
536
536
  if (!validation.valid) {
537
537
  return reply.status(422).send({
538
538
  error: "Modified plan failed envoy validation",
@@ -544,35 +544,20 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
544
544
  // Envoy unreachable — proceed without validation but note it
545
545
  }
546
546
  }
547
- // Build structured diff: what changed between old and new steps
548
- const oldSteps = deployment.plan.steps;
549
- const newSteps = parsed.data.steps;
550
- const diffLines = [];
551
- const maxLen = Math.max(oldSteps.length, newSteps.length);
552
- for (let i = 0; i < maxLen; i++) {
553
- const old = oldSteps[i];
554
- const cur = newSteps[i];
555
- if (!old) {
556
- diffLines.push(`+ Step ${i + 1} (added): ${cur.action} ${cur.target} — ${cur.description}`);
557
- }
558
- else if (!cur) {
559
- diffLines.push(`- Step ${i + 1} (removed): ${old.action} ${old.target} — ${old.description}`);
560
- }
561
- else if (old.action !== cur.action || old.target !== cur.target || old.description !== cur.description) {
562
- diffLines.push(`~ Step ${i + 1} (changed): ${old.action} ${old.target} → ${cur.action} ${cur.target}`);
563
- if (old.description !== cur.description) {
564
- diffLines.push(` was: ${old.description}`);
565
- diffLines.push(` now: ${cur.description}`);
566
- }
567
- }
568
- }
569
- const diffFromPreviousPlan = diffLines.length > 0
570
- ? diffLines.join("\n")
571
- : "Steps reordered or metadata changed (actions and targets unchanged)";
547
+ // Compute diff description
548
+ const oldScript = deployment.plan.scriptedPlan?.executionScript ?? "";
549
+ const newScript = parsed.data.executionScript;
550
+ const diffFromPreviousPlan = oldScript !== newScript
551
+ ? "Execution script modified by user"
552
+ : "Plan metadata changed (script unchanged)";
572
553
  // Apply modifications
573
554
  deployment.plan = {
574
555
  ...deployment.plan,
575
- steps: parsed.data.steps,
556
+ scriptedPlan: {
557
+ ...deployment.plan.scriptedPlan,
558
+ executionScript: parsed.data.executionScript,
559
+ ...(parsed.data.rollbackScript !== undefined ? { rollbackScript: parsed.data.rollbackScript } : {}),
560
+ },
576
561
  diffFromPreviousPlan,
577
562
  };
578
563
  deployments.save(deployment);
@@ -587,7 +572,6 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
587
572
  reasoning: parsed.data.reason,
588
573
  context: {
589
574
  modifiedBy: actor,
590
- stepCount: parsed.data.steps.length,
591
575
  reason: parsed.data.reason,
592
576
  },
593
577
  actor: request.user?.email,
@@ -596,13 +580,13 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
596
580
  actor,
597
581
  action: "operation.modified",
598
582
  target: { type: "deployment", id: deployment.id },
599
- details: { reason: parsed.data.reason, stepCount: parsed.data.steps.length },
583
+ details: { reason: parsed.data.reason },
600
584
  });
601
585
  telemetry.record({
602
586
  actor,
603
587
  action: "agent.recommendation.overridden",
604
588
  target: { type: "deployment", id: deployment.id },
605
- details: { reason: parsed.data.reason, stepCount: parsed.data.steps.length, diff: diffFromPreviousPlan },
589
+ details: { reason: parsed.data.reason, diff: diffFromPreviousPlan },
606
590
  });
607
591
  return { deployment, modified: true };
608
592
  });
@@ -635,10 +619,9 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
635
619
  try {
636
620
  const validation = await planningClientForValidation.validateRefinementFeedback({
637
621
  feedback: parsed.data.feedback,
638
- currentPlanSteps: (deployment.plan?.steps ?? []).map((s) => ({
622
+ currentPlanSummary: (deployment.plan?.scriptedPlan?.stepSummary ?? []).map((s) => ({
639
623
  description: s.description,
640
- action: s.action,
641
- target: s.target,
624
+ reversible: s.reversible,
642
625
  })),
643
626
  artifactName: artifact?.name ?? "unknown",
644
627
  environmentName: environment?.name ?? "unknown",
@@ -707,9 +690,9 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
707
690
  operationId: dep.id,
708
691
  agent: "envoy",
709
692
  decisionType: "plan-generation",
710
- decision: `Plan regenerated with user feedback (${result.plan.steps.length} steps)`,
693
+ decision: `Plan regenerated with user feedback (${result.plan.scriptedPlan.stepSummary.length} steps)`,
711
694
  reasoning: result.plan.reasoning,
712
- context: { stepCount: result.plan.steps.length, envoyId: planningEnvoy.id, refinementFeedback: parsed.data.feedback },
695
+ context: { stepCount: result.plan.scriptedPlan.stepSummary.length, envoyId: planningEnvoy.id, refinementFeedback: parsed.data.feedback },
713
696
  });
714
697
  return { deployment: dep, replanned: true };
715
698
  });
@@ -785,17 +768,17 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
785
768
  return reply.status(503).send({ error: "No envoy available to generate rollback plan" });
786
769
  }
787
770
  const environment = deployment.environmentId ? environments.get(deployment.environmentId) : undefined;
788
- // Build the list of completed steps from execution record (or plan as fallback)
771
+ // Build the list of completed steps from execution record (or plan step summaries as fallback)
789
772
  const completedSteps = deployment.executionRecord?.steps.map((s) => ({
790
773
  description: s.description,
791
- action: deployment.plan?.steps.find((p) => p.description === s.description)?.action ?? "unknown",
792
- target: deployment.plan?.steps.find((p) => p.description === s.description)?.target ?? "",
774
+ action: "script-step",
775
+ target: "",
793
776
  status: s.status,
794
777
  output: s.output ?? s.error,
795
- })) ?? deployment.plan?.steps.map((s) => ({
778
+ })) ?? deployment.plan?.scriptedPlan?.stepSummary.map((s) => ({
796
779
  description: s.description,
797
- action: s.action,
798
- target: s.target,
780
+ action: "script-step",
781
+ target: "",
799
782
  status: "completed",
800
783
  })) ?? [];
801
784
  const rollbackClient = new EnvoyClient(targetEnvoy.url);
@@ -835,7 +818,7 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
835
818
  reasoning: rollbackPlan.reasoning,
836
819
  context: {
837
820
  requestedBy: actor,
838
- stepCount: rollbackPlan.steps.length,
821
+ stepCount: rollbackPlan.scriptedPlan.stepSummary.length,
839
822
  envoyId: targetEnvoy.id,
840
823
  deploymentStatus: deployment.status,
841
824
  },
@@ -845,7 +828,7 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
845
828
  actor,
846
829
  action: "deployment.rollback-plan-requested",
847
830
  target: { type: "deployment", id: deployment.id },
848
- details: { stepCount: rollbackPlan.steps.length },
831
+ details: { stepCount: rollbackPlan.scriptedPlan.stepSummary.length },
849
832
  });
850
833
  return reply.status(200).send({ deployment, rollbackPlan });
851
834
  }
@@ -890,20 +873,30 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
890
873
  agent: "server",
891
874
  decisionType: "rollback-execution",
892
875
  decision: `Rollback execution initiated for ${artifact?.name ?? getArtifactId(deployment)} v${deployment.version}`,
893
- reasoning: `Rollback requested by ${actor}. Executing ${deployment.rollbackPlan.steps.length} rollback step(s).`,
894
- context: { initiatedBy: actor, stepCount: deployment.rollbackPlan.steps.length },
876
+ reasoning: `Rollback requested by ${actor}. Executing ${deployment.rollbackPlan.scriptedPlan.stepSummary.length} rollback step(s).`,
877
+ context: { initiatedBy: actor, stepCount: deployment.rollbackPlan.scriptedPlan.stepSummary.length },
895
878
  actor: request.user?.email,
896
879
  });
897
880
  telemetry.record({
898
881
  actor,
899
882
  action: "deployment.rollback-executed",
900
883
  target: { type: "deployment", id: deployment.id },
901
- details: { stepCount: deployment.rollbackPlan.steps.length },
884
+ details: { stepCount: deployment.rollbackPlan.scriptedPlan.stepSummary.length },
902
885
  });
903
886
  const rollbackClient = new EnvoyClient(targetEnvoy.url);
904
887
  // Execute the rollback plan as if it were a forward plan — it IS a forward plan
905
888
  // (just in the reverse direction). Use an empty no-op plan as the "rollback of rollback".
906
- const emptyPlan = { steps: [], reasoning: "No rollback of rollback." };
889
+ const emptyPlan = {
890
+ scriptedPlan: {
891
+ platform: "bash",
892
+ executionScript: "# No rollback of rollback",
893
+ dryRunScript: null,
894
+ rollbackScript: null,
895
+ reasoning: "No rollback of rollback.",
896
+ stepSummary: [],
897
+ },
898
+ reasoning: "No rollback of rollback.",
899
+ };
907
900
  rollbackClient.executeApprovedPlan({
908
901
  operationId: deployment.id,
909
902
  plan: deployment.rollbackPlan,
@@ -1059,7 +1052,7 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
1059
1052
  decisionType: "plan-generation",
1060
1053
  decision: `Operation plan blocked — infrastructure prerequisites not met`,
1061
1054
  reasoning: result.blockReason ?? result.plan.reasoning,
1062
- context: { stepCount: result.plan.steps.length, envoyId: planningEnvoy.id, blocked: true },
1055
+ context: { stepCount: result.plan.scriptedPlan.stepSummary.length, envoyId: planningEnvoy.id, blocked: true },
1063
1056
  });
1064
1057
  }
1065
1058
  else {
@@ -1071,9 +1064,9 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
1071
1064
  operationId: dep.id,
1072
1065
  agent: "envoy",
1073
1066
  decisionType: "plan-generation",
1074
- decision: `Operation plan generated with ${result.plan.steps.length} steps`,
1067
+ decision: `Operation plan generated with ${result.plan.scriptedPlan.stepSummary.length} steps`,
1075
1068
  reasoning: result.plan.reasoning,
1076
- context: { stepCount: result.plan.steps.length, envoyId: planningEnvoy.id, delta: result.delta },
1069
+ context: { stepCount: result.plan.scriptedPlan.stepSummary.length, envoyId: planningEnvoy.id, delta: result.delta },
1077
1070
  });
1078
1071
  }
1079
1072
  }).catch((err) => {
@@ -1296,7 +1289,7 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
1296
1289
  id: crypto.randomUUID(),
1297
1290
  input: responseType === "deploy"
1298
1291
  ? { type: "deploy", artifactId: "" }
1299
- : { type: "maintain", intent: triggerInput.responseIntent, parameters: triggerInput.parameters },
1292
+ : { type: "maintain", intent: triggerInput.responseIntent },
1300
1293
  intent: triggerInput.responseIntent,
1301
1294
  lineage: triggerOp.id,
1302
1295
  triggeredBy: "trigger",
@@ -1597,9 +1590,9 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
1597
1590
  operationId: childDep.id,
1598
1591
  agent: "envoy",
1599
1592
  decisionType: "plan-generation",
1600
- decision: `Child operation plan generated with ${result.plan.steps.length} steps`,
1593
+ decision: `Child operation plan generated with ${result.plan.scriptedPlan.stepSummary.length} steps`,
1601
1594
  reasoning: result.plan.reasoning,
1602
- context: { stepCount: result.plan.steps.length, envoyId: planningEnvoy.id, parentOperationId: parentOp.id },
1595
+ context: { stepCount: result.plan.scriptedPlan.stepSummary.length, envoyId: planningEnvoy.id, parentOperationId: parentOp.id },
1603
1596
  });
1604
1597
  }
1605
1598
  catch (err) {
@@ -1631,19 +1624,43 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
1631
1624
  if (!anyFailed) {
1632
1625
  // All children planned — build combined summary plan and await approval
1633
1626
  const allChildren = childIds.map((id) => deployments.get(id)).filter(Boolean);
1634
- const combinedSteps = allChildren.flatMap((c, idx) => {
1635
- if (!c.plan)
1627
+ const combinedStepSummary = allChildren.flatMap((c, idx) => {
1628
+ if (!c.plan?.scriptedPlan)
1636
1629
  return [];
1637
- return c.plan.steps.map((step) => ({
1630
+ return c.plan.scriptedPlan.stepSummary.map((step) => ({
1638
1631
  ...step,
1639
1632
  description: `[${idx + 1}/${allChildren.length}: ${c.input.type}] ${step.description}`,
1640
1633
  }));
1641
1634
  });
1642
1635
  const combinedReasoning = allChildren.map((c, idx) => `Step ${idx + 1} (${c.input.type}): ${c.plan?.reasoning ?? "no reasoning"}`).join("\n\n");
1636
+ // Combine child execution scripts into a single composite script
1637
+ const combinedScript = allChildren
1638
+ .map((c, idx) => `# --- Child ${idx + 1}/${allChildren.length}: ${c.input.type} ---\n${c.plan?.scriptedPlan?.executionScript ?? "# no script"}`)
1639
+ .join("\n\n");
1643
1640
  const parentDep = deployments.get(parentOp.id);
1644
1641
  if (parentDep && parentDep.status === "pending") {
1645
- parentDep.plan = { steps: combinedSteps, reasoning: combinedReasoning };
1646
- parentDep.rollbackPlan = { steps: [], reasoning: "Child operations handle their own rollback" };
1642
+ parentDep.plan = {
1643
+ scriptedPlan: {
1644
+ platform: "bash",
1645
+ executionScript: combinedScript,
1646
+ dryRunScript: null,
1647
+ rollbackScript: null,
1648
+ reasoning: combinedReasoning,
1649
+ stepSummary: combinedStepSummary,
1650
+ },
1651
+ reasoning: combinedReasoning,
1652
+ };
1653
+ parentDep.rollbackPlan = {
1654
+ scriptedPlan: {
1655
+ platform: "bash",
1656
+ executionScript: "# Child operations handle their own rollback",
1657
+ dryRunScript: null,
1658
+ rollbackScript: null,
1659
+ reasoning: "Child operations handle their own rollback",
1660
+ stepSummary: [],
1661
+ },
1662
+ reasoning: "Child operations handle their own rollback",
1663
+ };
1647
1664
  parentDep.status = "awaiting_approval";
1648
1665
  parentDep.recommendation = computeRecommendation(parentDep, deployments);
1649
1666
  deployments.save(parentDep);
@@ -1654,7 +1671,7 @@ export function registerOperationRoutes(app, deployments, debrief, partitions, e
1654
1671
  decisionType: "composite-plan-ready",
1655
1672
  decision: `All ${allChildren.length} child plans ready — composite awaiting approval`,
1656
1673
  reasoning: combinedReasoning,
1657
- context: { childIds, totalSteps: combinedSteps.length },
1674
+ context: { childIds, totalSteps: combinedStepSummary.length },
1658
1675
  });
1659
1676
  }
1660
1677
  }