Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ dashboard:
| `vxd learn [path]` | Run repo analysis (`--force`, `--pass 1\|2\|3`, `--json`) |
| `vxd security scan [path]` | Run the security agent on a repo (scanners + optional `--llm` review); `--json`, `--min <severity>` for CI exit code; auto-grows the knowledge base |
| `vxd security kb` | Show the security knowledge base — version, baseline + learned rules (`--json`) |
| `vxd figma auth` | One-time INTERACTIVE session: create + paste a Figma personal access token (validated via /v1/me, stored at `<state_dir>/figma.token` 0600). After this, Figma-referencing runs are fire-and-forget again |
| `vxd figma status` | Show whether Figma access is configured and for which account |
| `vxd backup` | Create tar.gz archive of project state (`--output DIR`) |
| `vxd gc` | Garbage-collect branches + expired logs |
| `vxd improve log` | Browse improvement changelog (`--disposition`, `--category`, `--since`, `--errors`) |
Expand Down Expand Up @@ -460,6 +462,14 @@ vxd-built web UIs previously came out as generic "AI slop" (Inter font, purple g
- **Planning:** the Tech-Lead ENGINEERING STANDARDS block now requires the FIRST UI story to establish a design-token foundation (palette/typeface-pairing/spacing as CSS custom properties or the framework theme) with later UI stories consuming those tokens, and UI acceptance criteria to include the accessibility quality floor (`TestPlanner_PromptIncludesEngineeringStandards` pins it).
- **Tests:** `agent/frontend_test.go` (brief injected only when flagged, retry path carries it, size budget), `engine/detect_test.go::TestDetectFrontend` (21 cases incl. substring traps). NXD port done (NXD #89 — threaded through NXD's CLI, native, and retry prompt paths).

### Figma design integration (internal/figma + engine/design_context.go, 2026-07-02)
Requirements can reference figma.com design URLs; vxd builds the UI to match the referenced frames instead of inventing a design.
- **Interactive-once auth, communicated up front:** Figma needs an operator credential, so the FIRST Figma-referencing run breaks fire-and-forget. `vxd req` detects design URLs (`figma.ParseURLs` — design/file/proto/board forms, node-id canonicalised to `12:345`) and fails fast BEFORE any LLM spend when no credential exists, naming the interactive step: `vxd figma auth` (prints the settings URL — never auto-opens a browser — reads the token, validates via `/v1/me`, stores at `<state_dir>/figma.token` 0600) or `FIGMA_TOKEN` env. After that single session, runs are autonomous again.
- **Design pull (`figma.BuildDesignContext`):** fetches referenced nodes (`/v1/files/:key/nodes`, depth 3) + 2x PNG renders (`/v1/images/:key`) into `<repo>/.vxd-design/` — `DESIGN_CONTEXT.md` (frame inventory with dimensions, text styles, solid fills as hex) + the rendered PNGs. Per-ref failures degrade to a note; a deleted frame never strands the requirement.
- **Pipeline flow:** the planner appends the context inside `<design-reference>` data tags (instructing token derivation FROM the design); frontend stories get it in their goal prompt after `FrontendDesignBrief` (the reference overrides generic design guidance) and `copyDesignDir` puts the PNGs in each frontend worktree so agents can open them. `loadDesignContext` injection-scans the markdown (Figma layer names are third-party data — `sanitize.MatchInjectionPattern` hit drops the context loudly) and caps it at 16 KB.
- **Hygiene:** `.vxd-design/` is gitignored (adapter + repo patterns) and stripped from story branches (`stripVXDArtifactsFromBranch`) — design pulls are working material, not deliverables.
- **Tests:** `internal/figma/*_test.go` (URL parse/dedupe, token resolution chain incl. the interactive-step error text, owner-only perms, httptest client + context extraction with hex/typography assertions), `engine/design_context_test.go` (load/injection-drop/size-cap/copy + planner prompt injection), `cli/figma_test.go` (auth validates-then-stores, invalid token not stored, status paths). **NXD: not ported — Figma is a cloud API; NXD stays offline-first.**

### Model ID Compatibility
- **Use undated aliases, not dated snapshots.** Current defaults: `claude-opus-4-8` (tech_lead), `claude-sonnet-4-6` (senior/qa/manager), `claude-haiku-4-5` (cheapest). All three are verified working on the Claude CLI subscription tier.
- **Default execution tiers are all-Anthropic (2026-06-24 fix).** `DefaultConfig` previously set junior/intermediate/supervisor to `{google, gemma-4-27b-it}` — a model that 404s on the Google AI API (it does not exist on `v1beta`). Every low-complexity story spawned a gemini agent that died in ~10s producing no code, then limped forward by escalating to senior. Defaults are now `{anthropic, claude-haiku-4-5}` so a fresh install works with only the Claude CLI configured (no Google AI key/quota). `TestDefaultConfig_NoInvalidJuniorModel` pins this. **A model 404 in the agent runtime surfaces as "agent produced no code changes," NOT as a model error — if a whole tier silently produces nothing, validate the model ID with `gemini -m <id> -p OK` / `claude --model <id> -p OK` first.**
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ vhs docs/demo.tape
- **Supervisor oversight** -- periodic drift detection and reprioritization
- **Senior code review** -- automated review via LLM with approve/request-changes verdicts
- **Frontend design skill** -- UI-facing stories are detected (owned files + story text) and their agents receive an embedded design brief: token-first planning, one signature element, named anti-"AI slop" defaults banned, and a WCAG accessibility floor; the planner requires a design-token foundation story for web UIs
- **Figma design integration** -- requirements can reference figma.com URLs; vxd pulls the frames (structure, styles, rendered PNGs) and the planner + frontend agents build to MATCH the design. Note: the first Figma run needs a one-time interactive auth session (`vxd figma auth`); every run after that is fire-and-forget as usual
- **Automated QA pipeline** -- lint, build, and test with declarative success criteria (6 kinds)
- **Auto-merge with PR creation** -- stories flow from code to merged PR hands-free
- **LLM-powered conflict resolution** -- rebase conflicts auto-resolved; binary files handled without LLM (deterministic policy); complex/multi-file conflicts escalate to Tech Lead with full requirement DAG context
Expand Down Expand Up @@ -236,6 +237,8 @@ vhs docs/demo.tape
| `vxd preflight` | Run pre-flight environment checks (16 checks, 3 severity tiers) |
| `vxd estimate <requirement>` | Estimate cost (`--quick`, `--json`, `--rate`, `--save`) |
| `vxd report <req-id>` | Generate client delivery report (`--html`, `--internal`, `--output`) |
| `vxd figma auth` | One-time interactive session: store a Figma personal access token (validated, 0600) |
| `vxd figma status` | Show whether Figma access is configured and for which account |
| `vxd metrics [--req ID]` | Show pipeline performance metrics with agent activity stats |
| `vxd learn [repo-path]` | Analyse a repository and build a persistent profile (`--pass`, `--force`) |
| `vxd projects` | List all tracked projects |
Expand Down
4 changes: 4 additions & 0 deletions internal/agent/prompts.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type PromptContext struct {
IsBugFix bool // true when the story is about fixing a bug
IsInfrastructure bool // true when the story involves Docker/CI/deployment
IsFrontend bool // true when the story builds/changes a user-facing web UI
DesignContext string // Figma design reference (markdown), set for frontend stories when a design was pulled
WaveContext string // summary of what prior stories built (from WAVE_CONTEXT.md)
DesignApproach string // "ddd-tdd" (default), "tdd", "standard"
}
Expand Down Expand Up @@ -119,6 +120,9 @@ BUG FIX — MANDATORY WORKFLOW:

if ctx.IsFrontend {
base += "\n" + FrontendDesignBrief
if ctx.DesignContext != "" {
base += "\n" + ctx.DesignContext + "\nThe design reference above OVERRIDES the generic design guidance: match the referenced Figma frames. Open the rendered PNGs listed above (they are in this worktree) and derive the palette, typography, spacing, and layout from them.\n"
}
}

if ctx.IsInfrastructure {
Expand Down
137 changes: 137 additions & 0 deletions internal/cli/figma.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package cli

import (
"bufio"
"fmt"
"strings"

"github.com/spf13/cobra"
"github.com/tzone85/vortex-dispatch/internal/figma"
)

// figmaTokenSettingsURL is where a personal access token is created. Printed,
// never auto-opened — the operator clicks it themselves.
const figmaTokenSettingsURL = "https://www.figma.com/settings"

// figmaAPIBase overrides the Figma API base URL in tests ("" = production).
var figmaAPIBase = ""

// newFigmaClient builds a client honoring the test override.
func newFigmaClient(token string) *figma.Client {
c := figma.NewClient(token)
if figmaAPIBase != "" {
c.BaseURL = figmaAPIBase
}
return c
}

func newFigmaCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "figma",
Short: "Figma design integration: authenticate and inspect design access",
Long: `Requirements can reference figma.com design URLs; vxd pulls the referenced
frames (structure, styles, rendered PNGs) into a design context that the
planner and the frontend agents build against.

Figma access needs an operator credential, so the FIRST Figma-referencing run
is interactive-once rather than fire-and-forget: run 'vxd figma auth' a single
time, then Figma runs are autonomous again.`,
SilenceUsage: true,
}
cmd.AddCommand(newFigmaAuthCmd())
cmd.AddCommand(newFigmaStatusCmd())
return cmd
}

func newFigmaAuthCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Short: "One-time interactive session: store a Figma personal access token",
Long: `Interactive (one time): create a personal access token in your Figma
settings, paste it here, and vxd validates + stores it at
<state_dir>/figma.token (mode 0600). Subsequent Figma-referencing runs are
fully autonomous.`,
Args: cobra.NoArgs,
RunE: runFigmaAuth,
}
cmd.SilenceUsage = true
return cmd
}

func runFigmaAuth(cmd *cobra.Command, _ []string) error {
out := cmd.OutOrStdout()
cfgPath, _ := cmd.Flags().GetString("config")
cfg, err := loadConfig(cfgPath)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
stateDir := expandHome(cfg.Workspace.StateDir)

fmt.Fprintln(out, "Figma auth — one-time interactive session")
fmt.Fprintln(out, "")
fmt.Fprintln(out, " 1. Open your Figma settings (Security tab):")
fmt.Fprintf(out, " %s\n", figmaTokenSettingsURL)
fmt.Fprintln(out, " 2. Under 'Personal access tokens', generate a token with File content: Read scope.")
fmt.Fprintln(out, " 3. Paste it below (input is stored at "+figma.TokenPath(stateDir)+", mode 0600).")
fmt.Fprintln(out, "")
fmt.Fprint(out, "Token: ")

reader := bufio.NewReader(cmd.InOrStdin())
line, err := reader.ReadString('\n')
if err != nil && line == "" {
return fmt.Errorf("read token: %w", err)
}
token := strings.TrimSpace(line)
if token == "" {
return fmt.Errorf("no token entered")
}

// Validate before storing so a typo'd token fails HERE, in the
// interactive session, not mid-pipeline hours later.
me, err := newFigmaClient(token).Me(cmd.Context())
if err != nil {
return fmt.Errorf("token validation failed (not stored): %w", err)
}

path, err := figma.SaveToken(stateDir, token)
if err != nil {
return err
}
fmt.Fprintf(out, "\nAuthenticated as %s (%s). Token stored at %s.\n", me.Handle, me.Email, path)
fmt.Fprintln(out, "Figma-referencing runs are now fire-and-forget like every other vxd run.")
return nil
}

func newFigmaStatusCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "status",
Short: "Show whether Figma access is configured and for which account",
Args: cobra.NoArgs,
RunE: runFigmaStatus,
}
cmd.SilenceUsage = true
return cmd
}

func runFigmaStatus(cmd *cobra.Command, _ []string) error {
out := cmd.OutOrStdout()
cfgPath, _ := cmd.Flags().GetString("config")
cfg, err := loadConfig(cfgPath)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
token, source, err := figma.ResolveToken(expandHome(cfg.Workspace.StateDir))
if err != nil {
fmt.Fprintln(out, "Figma: not configured")
fmt.Fprintln(out, err.Error())
return nil
}
me, err := newFigmaClient(token).Me(cmd.Context())
if err != nil {
fmt.Fprintf(out, "Figma: credential found (%s) but validation failed: %v\n", source, err)
fmt.Fprintln(out, "Re-run `vxd figma auth` to refresh it.")
return nil
}
fmt.Fprintf(out, "Figma: authenticated as %s (%s) via %s\n", me.Handle, me.Email, source)
return nil
}
102 changes: 102 additions & 0 deletions internal/cli/figma_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package cli

import (
"bytes"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/tzone85/vortex-dispatch/internal/figma"
)

// newFigmaFixture serves /v1/me for a known-good token.
func newFigmaFixture(t *testing.T) *httptest.Server {
t.Helper()
mux := http.NewServeMux()
mux.HandleFunc("/v1/me", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Figma-Token") != "figd_valid" {
w.WriteHeader(http.StatusForbidden)
return
}
_, _ = w.Write([]byte(`{"id":"u1","email":"op@example.com","handle":"Operator"}`))
})
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
return srv
}

func TestFigmaAuth_ValidatesAndStoresToken(t *testing.T) {
srv := newFigmaFixture(t)
figmaAPIBase = srv.URL
t.Cleanup(func() { figmaAPIBase = "" })

cmd := newFigmaAuthCmd()
out := driveWithVxdYaml(t, cmd)
cmd.SetIn(strings.NewReader("figd_valid\n"))
if err := cmd.Execute(); err != nil {
t.Fatalf("auth: %v", err)
}
got := out.String()
for _, want := range []string{
"one-time interactive session", // the UX promise: interactive ONCE
"figma.com/settings", // URL printed, never auto-opened
"Operator", // validated identity echoed back
"fire-and-forget", // and back to autonomous runs
} {
if !strings.Contains(got, want) {
t.Errorf("auth output missing %q:\n%s", want, got)
}
}
}

func TestFigmaAuth_RejectsInvalidTokenWithoutStoring(t *testing.T) {
srv := newFigmaFixture(t)
figmaAPIBase = srv.URL
t.Cleanup(func() { figmaAPIBase = "" })

cmd := newFigmaAuthCmd()
driveWithVxdYaml(t, cmd)
cmd.SetIn(strings.NewReader("figd_typo\n"))
if err := cmd.Execute(); err == nil || !strings.Contains(err.Error(), "not stored") {
t.Fatalf("invalid token must fail loudly in the interactive session, got %v", err)
}
}

func TestFigmaAuth_EmptyInput(t *testing.T) {
cmd := newFigmaAuthCmd()
driveWithVxdYaml(t, cmd)
cmd.SetIn(bytes.NewReader([]byte("\n")))
if err := cmd.Execute(); err == nil || !strings.Contains(err.Error(), "no token entered") {
t.Fatalf("empty input must be rejected, got %v", err)
}
}

func TestFigmaStatus_NotConfigured(t *testing.T) {
t.Setenv(figma.TokenEnvVar, "")
cmd := newFigmaStatusCmd()
out := driveWithVxdYaml(t, cmd)
if err := cmd.Execute(); err != nil {
t.Fatalf("status must not error when unconfigured: %v", err)
}
got := out.String()
if !strings.Contains(got, "not configured") || !strings.Contains(got, "vxd figma auth") {
t.Errorf("status must explain the interactive step:\n%s", got)
}
}

func TestFigmaStatus_Authenticated(t *testing.T) {
srv := newFigmaFixture(t)
figmaAPIBase = srv.URL
t.Cleanup(func() { figmaAPIBase = "" })
t.Setenv(figma.TokenEnvVar, "figd_valid")

cmd := newFigmaStatusCmd()
out := driveWithVxdYaml(t, cmd)
if err := cmd.Execute(); err != nil {
t.Fatalf("status: %v", err)
}
if !strings.Contains(out.String(), "authenticated as Operator") {
t.Errorf("status must show the account:\n%s", out.String())
}
}
24 changes: 24 additions & 0 deletions internal/cli/req.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/spf13/cobra"
"github.com/tzone85/vortex-dispatch/internal/agent"
"github.com/tzone85/vortex-dispatch/internal/engine"
"github.com/tzone85/vortex-dispatch/internal/figma"
"github.com/tzone85/vortex-dispatch/internal/llm"
"github.com/tzone85/vortex-dispatch/internal/repolearn"
)
Expand Down Expand Up @@ -121,6 +122,29 @@ func runReq(cmd *cobra.Command, args []string) error {
return fmt.Errorf("determine working directory: %w", err)
}

// Figma design references make this run interactive-ONCE: pulling the
// design needs an operator credential. Fail fast here — before any LLM
// spend — with the exact interactive step when it is missing. With a
// credential in place the run stays fire-and-forget.
if refs := figma.ParseURLs(requirement); len(refs) > 0 {
if dryRun {
fmt.Fprintf(cmd.OutOrStdout(), "[DRY RUN] Figma: %d design reference(s) detected — skipping pull\n", len(refs))
} else {
token, source, tokErr := figma.ResolveToken(expandHome(s.Config.Workspace.StateDir))
if tokErr != nil {
return tokErr
}
fmt.Fprintf(cmd.OutOrStdout(), "Figma: %d design reference(s) detected — pulling design context (auth: %s)\n", len(refs), source)
dc, pullErr := figma.BuildDesignContext(cmd.Context(), newFigmaClient(token), refs, filepath.Join(repoPath, figma.DirName))
if pullErr != nil {
return fmt.Errorf("figma pull: %w", pullErr)
}
if dc != nil {
fmt.Fprintf(cmd.OutOrStdout(), "Figma: design context + %d render(s) written to %s/ — the planner and frontend agents will build against them\n", len(dc.Images), figma.DirName)
}
}
}

planner := engine.NewPlanner(client, s.Config, s.Events, s.Proj)
planner.SetProjectDir(s.ProjectDir)

Expand Down
1 change: 1 addition & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func init() {
rootCmd.AddCommand(newDBCmd())
rootCmd.AddCommand(newEstimateCmd())
rootCmd.AddCommand(newPreflightCmd())
rootCmd.AddCommand(newFigmaCmd())
rootCmd.AddCommand(newReportCmd())
rootCmd.AddCommand(newApprovePlanCmd())
rootCmd.AddCommand(newRejectPlanCmd())
Expand Down
Loading
Loading