From 004cc03ba600d1ebdb5cce17a303f93b10015157 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Mon, 20 Jul 2026 14:33:46 +0100 Subject: [PATCH] docs: add workflow builder v2 plan --- .../plans/2026-07-20-workflow-builder-v2.md | 747 ++++++++++++++++++ 1 file changed, 747 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-20-workflow-builder-v2.md diff --git a/docs/superpowers/plans/2026-07-20-workflow-builder-v2.md b/docs/superpowers/plans/2026-07-20-workflow-builder-v2.md new file mode 100644 index 0000000..5462af4 --- /dev/null +++ b/docs/superpowers/plans/2026-07-20-workflow-builder-v2.md @@ -0,0 +1,747 @@ +# Workflow Builder v2 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Rebuild the workflow builder to match the approved mockup with drag-and-drop, base-step editing/deletion (cascade), step input parameters, an edit-workflow modal, runs navigation, and fix the save crash. + +**Architecture:** Backend gains input-parameter fields on step/ref/resolved models, cascade delete, runner env injection, and a workflow-update handler that returns the updated workflow. Frontend adds `bash`/`pwsh`/`signal` tokens and a `Modal` primitive, then rebuilds the builder page (dotted canvas, 340px node cards, wire env chips, kicker/field inspector, HTML5 drag-and-drop), plus Edit-base-step and Edit-workflow modals and a runs list page. + +**Tech Stack:** Go (gin, mongo-driver v2), Next.js 16 app-router + react-query + Tailwind, HTML5 Drag-and-Drop, MongoDB. + +## Global Constraints + +- **No tests this iteration** — no `*_test.go` or frontend tests. Verify with `go build ./...`, `go vet ./...`, `npm run build`. +- Mongo access pattern: `db.Col("collection_name")` + `context.WithTimeout`. Follow `server/internal/services/workflows.go`. +- Audit every mutation with `services.LogEvent(action, actor, serverID, targetID, message)`. +- Interpreter literals are `"bash"` and `"powershell"`. +- Go module path: `github.com/mrhid6/vantage`. +- Visual target: approved mockup. Builder palette adds amber signal `#f5a524` (`signal`), `#241800` (`signal-ink`), bash `#3fb950`, pwsh `#5b9bff`; keep existing `surface`/`surface-2`/`border`/`text-primary`/`text-secondary`/`danger`/`accent` tokens for panels. Node cards 340px; dotted-grid canvas; dashed-amber `passes` chips on wires; kicker/field inspector. +- Frontend uses `@/lib/api` typed client, `@/components/ui`, react-query. Input styling follows the existing `inputClass` pattern in `web/app/secrets/page.tsx`. + +--- + +## Task 1: Models + step CRUD + cascade delete + +**Files:** +- Modify: `server/internal/models/workflow.go` +- Modify: `server/internal/services/workflows.go` + +**Interfaces:** +- Produces: `models.InputParam{Name,Default,Description}`; `WorkflowStep.DeclaredInputs`, `WorkflowStepRef.Inputs`, `ResolvedStep.Inputs`. `DeleteStep(stepID)` now cascades to workflows. + +- [ ] **Step 1: Add the model fields** + +In `server/internal/models/workflow.go` add: + +```go +type InputParam struct { + Name string `bson:"name" json:"name"` + Default string `bson:"default" json:"default"` + Description string `bson:"description" json:"description"` +} +``` + +- In `WorkflowStep`, add after `DeclaredOutputs`: `DeclaredInputs []InputParam `bson:"declared_inputs" json:"declared_inputs"`` +- In `WorkflowStepRef`, add: `Inputs map[string]string `bson:"inputs,omitempty" json:"inputs,omitempty"`` +- In `ResolvedStep`, add: `Inputs map[string]string `bson:"inputs" json:"inputs"`` + +- [ ] **Step 2: Persist declared_inputs in CreateStep/UpdateStep** + +In `server/internal/services/workflows.go`: +- In `CreateStep`, after the `SecretRefs` nil-guard add: + +```go + if s.DeclaredInputs == nil { + s.DeclaredInputs = []models.InputParam{} + } +``` + +- In `UpdateStep`'s `$set`, add: `"declared_inputs": s.DeclaredInputs,` + +- [ ] **Step 3: Cascade DeleteStep to workflows** + +Replace the body of `DeleteStep` with a version that also strips the step from every workflow: + +```go +func DeleteStep(stepID string) error { + ctx, cancel := wfCtx() + defer cancel() + if _, err := db.Col("workflow_steps").DeleteOne(ctx, bson.M{"step_id": stepID}); err != nil { + return err + } + // Cascade: remove this step from every workflow that references it, re-sequencing orders. + cur, err := db.Col("workflows").Find(ctx, bson.M{"steps.step_id": stepID}) + if err != nil { + return err + } + defer cur.Close(ctx) + var wfs []models.Workflow + if err := cur.All(ctx, &wfs); err != nil { + return err + } + for _, w := range wfs { + kept := make([]models.WorkflowStepRef, 0, len(w.Steps)) + for _, ref := range w.Steps { + if ref.StepID == stepID { + continue + } + kept = append(kept, ref) + } + for i := range kept { + kept[i].Order = i + } + if _, err := db.Col("workflows").UpdateOne(ctx, + bson.M{"workflow_id": w.WorkflowID}, + bson.M{"$set": bson.M{"steps": kept, "updated_at": time.Now()}}, + ); err != nil { + return err + } + } + return nil +} +``` + +Confirm `models` and `time` are imported in this file (they are). + +- [ ] **Step 4: Verify build** + +Run: `cd server && go build ./... && go vet ./...` +Expected: success. + +- [ ] **Step 5: Commit** + +```bash +git add server/internal/models/workflow.go server/internal/services/workflows.go +git commit -m "feat(workflows): step input params model + cascade step delete" +``` + +--- + +## Task 2: Runner input injection + update handler returns workflow + +**Files:** +- Modify: `server/internal/services/workflow_runner.go` +- Modify: `server/internal/api/workflows.go` + +**Interfaces:** +- Consumes: `models.ResolvedStep.Inputs`, `WorkflowStep.DeclaredInputs`, `WorkflowStepRef.Inputs` (Task 1). +- Produces: runner injects input env; `PUT /api/workflows/:id` returns the `Workflow`. + +- [ ] **Step 1: Resolve inputs in `resolveSteps`** + +In `server/internal/services/workflow_runner.go`, in `resolveSteps`, after loading `lib` and before/where the `ResolvedStep` is built, compute the input env and set it. Add inside the loop (after `lib, err := getStep(...)` succeeds): + +```go + inputs := map[string]string{} + for _, p := range lib.DeclaredInputs { + if ref.Inputs != nil { + if v, ok := ref.Inputs[p.Name]; ok { + inputs[p.Name] = v + continue + } + } + inputs[p.Name] = p.Default + } +``` + +and set `Inputs: inputs,` on the `models.ResolvedStep{...}` literal. + +- [ ] **Step 2: Inject inputs into cmdEnv in `runServer`** + +In `runServer`, where `cmdEnv` is built (currently: copy `runEnv`, then overlay `secretVals`), change the layering so inputs are the base layer: + +```go + cmdEnv := map[string]string{} + for k, v := range step.Inputs { + cmdEnv[k] = v + } + for k, v := range runEnv { + cmdEnv[k] = v + } + for k, v := range secretVals { + cmdEnv[k] = v + } +``` + +(Inputs first = lowest precedence; upstream outputs override; secrets win. Keep the existing `secretVals`/`allSecrets` masking logic unchanged.) + +- [ ] **Step 3: `updateWorkflow` handler returns the workflow** + +In `server/internal/api/workflows.go`, change `updateWorkflow` so that after a successful `services.UpdateWorkflow`, it re-fetches and returns the workflow: + +```go +func updateWorkflow(c *gin.Context) { + var w models.Workflow + if err := c.ShouldBindJSON(&w); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := services.UpdateWorkflow(c.Param("id"), w); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + services.LogEvent("workflow.updated", actorFromCtx(c), "", c.Param("id"), "workflow updated") + updated, err := services.GetWorkflow(c.Param("id")) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, updated) +} +``` + +- [ ] **Step 4: Verify build** + +Run: `cd server && go build ./... && go vet ./...` +Expected: success. + +- [ ] **Step 5: Commit** + +```bash +git add server/internal/services/workflow_runner.go server/internal/api/workflows.go +git commit -m "feat(workflows): inject step inputs into env; update returns workflow" +``` + +--- + +## Task 3: Frontend tokens, Modal primitive, API types + +**Files:** +- Modify: `web/tailwind.config.ts` +- Create: `web/components/ui/Modal.tsx` +- Modify: `web/components/ui/index.ts` +- Modify: `web/lib/api.ts` + +**Interfaces:** +- Produces: tokens `bash`/`pwsh`/`signal`/`signal-ink`; `children`; TS `InputParam`, `WorkflowStep.declared_inputs`, `WorkflowStepRef.inputs`. + +- [ ] **Step 1: Add tokens** + +In `web/tailwind.config.ts`, add to `theme.extend.colors`: + +```ts + bash: "#3fb950", + pwsh: "#5b9bff", + signal: "#f5a524", + "signal-ink": "#241800", +``` + +- [ ] **Step 2: Create the Modal primitive** + +`web/components/ui/Modal.tsx`: + +```tsx +"use client"; + +import { useEffect } from "react"; + +export function Modal({ + open, + title, + onClose, + children, + wide, +}: { + open: boolean; + title: string; + onClose: () => void; + children: React.ReactNode; + wide?: boolean; +}) { + useEffect(() => { + if (!open) return; + const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose(); + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [open, onClose]); + + if (!open) return null; + + return ( +
+
+
+
+

{title}

+ +
+
{children}
+
+
+ ); +} +``` + +- [ ] **Step 3: Export it** + +In `web/components/ui/index.ts` add: `export { Modal } from "./Modal";` + +- [ ] **Step 4: API types** + +In `web/lib/api.ts`: +- Add interface: + +```ts +export interface InputParam { + name: string; + default: string; + description: string; +} +``` + +- In `WorkflowStep`, add `declared_inputs: InputParam[];` +- In `WorkflowStepRef`, add `inputs?: Record;` + +(`updateWorkflow` already typed to return `Workflow`; the backend now honors it.) + +- [ ] **Step 5: Verify build** + +Run: `cd web && npm run build` +Expected: success. + +- [ ] **Step 6: Commit** + +```bash +git add web/tailwind.config.ts web/components/ui/Modal.tsx web/components/ui/index.ts web/lib/api.ts +git commit -m "feat(web): builder tokens, Modal primitive, input-param types" +``` + +--- + +## Task 4: Edit-base-step modal + +**Files:** +- Create: `web/components/workflows/EditStepModal.tsx` + +**Interfaces:** +- Consumes: `api.createStep`, `api.updateStep`, `api.deleteStep`, `Modal`, types (Task 3). +- Produces: `` where `step` is a `WorkflowStep` (edit) or `null` (new). + +- [ ] **Step 1: Write the modal** + +Client component. Local form state seeded from `step` (or blank for new). Fields: +- **Name** (text), **Interpreter** (select bash/powershell), **Script** (`