Figma ingestion: pull Dev Mode tokens + exported assets from a linked Figma file into the goal's design_spec#1930
Merged
pulzzejaehoon merged 9 commits intoJul 11, 2026
Conversation
Mirror the Jira/GitHub/Linear/Trello pattern to connect Figma through Interactor's credential-management API — no Figma OAuth app is registered by this app. - registry.ts: add a `figma` provider (serviceId `figma`, `design` category) with Dev Mode read scopes (files:read, file_variables:read, file_dev_resources:read). Kept `available: true`; if Interactor doesn't support Figma yet, the connect route's `service_not_found` path 503s gracefully. - figma-token.ts: `getFigmaToken(credentialId)` resolves a short-lived token via getCredentialToken on demand, never persisting it (mirrors github-token). - callback: make the success redirect provider-aware so non-GitHub providers (Figma) no longer surface a false "GitHub connected" toast; GitHub-specific login enrichment was already gated on `provider === "github"`. - profile page: generic "<Provider> connected successfully" banner. - org integrations page: "Connect Figma" button + connected badge, reusing the generic POST /api/v1/integrations/connect flow. No agent-config update needed: this adds a delegated-credential provider and connect UI only — no new agent-visible tools, entities, or workflows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… signal, figma-token coverage Address review findings on the Figma delegated-credential provider: - registry.test.ts: add 'design' to the category allowlist so the new figma entry passes 'all providers have required fields'; add figma id/scope/category coverage. - tools-callback-provider.test.ts: the non-github success test asserted the stale githubConnected=1 flag; update it to expect the provider-aware connected=<provider> signal, and add a figma case (skips github enrichment, redirects connected=figma, githubLogin null). - figma-token.test.ts: new test covering getFigmaToken — exchanges the opaque credentialId via getCredentialToken and propagates errors — bringing the previously-uncovered helper to 100% and lifting patch coverage above threshold. Test-only change; no product behaviour or agent-visible surface changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ API) A goal can now carry a Figma file/design link as a first-class, goal-scoped source — distinct from file-backed Attachments (Attachment.projectFileId is non-nullable, so it can't hold a URL). The downstream ingestion task will PRODUCE ordinary PNG + asset attachments that flow through the existing engine_goal attachment vehicle unchanged; this change only stores the SOURCE link + its ingest status. - Prisma: inline nullable columns on EngineGoal (mirroring the machineFault* / mergeBlockedReason* column-group convention) — figmaUrl, figmaFileKey, figmaNodeId, plus ingest fields (figmaIngestStatus pending|running|succeeded| failed, figmaLastIngestedAt, figmaIngestError/figmaIngestErrorCode). Additive migration, no backfill. - URL parsing/validation: new pure parseFigmaUrl (src/lib/engine/figma-url.ts) extracts fileKey + colon-normalized node-id from a /design|/file|/proto|/board URL; rejects non-Figma / malformed / keyless links with a plain-language reason carrying the machine-readable figma_url_invalid refusal code (+ detail.reason). - Service: createGoal + updateGoalFields parse/validate the link before writing (bad link → 422 EngineValidationError with the code); a changed link re-arms ingestion (status → pending), an unchanged link is a no-op, null clears it. - API: v2 goals POST + PATCH accept + validate figmaUrl (NOT routed through the file-only attachments route). - UI: a "Figma link" input in the create wizard and the edit dialog, threaded through EngineGoalRow / GoalDetailData so both surfaces edit it without an extra fetch. Client refusal renderer translates figma_url_invalid to plain-language guidance. No PM Agent config change: this is data-plumbing foundation surfaced only in the human wizard/edit UI — it adds no agent-visible tool and the goal_create tool's field set is unchanged. The blocked ingestion task will revisit agent config if the ingestion workflow becomes agent-driven. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deterministic extraction that turns a linked Figma frame into exact design
data + assets — the heart of the ingestion phase. Tokens are the source of
truth; the flat PNG is only a layout reference.
- figma-client.ts: typed Figma REST client (files, nodes, variables/local,
images + asset-byte download) over an injectable fetch. Classifies every
non-OK response into a typed FigmaApiError with a stable `code`
(unauthorized / forbidden / missing_scope / enterprise_only / not_found /
rate_limited / render_failed) so the orchestration task translates
403 / missing-scope / Enterprise-only without string-matching. The Dev
Mode variables endpoint degrades gracefully — a 403 returns null.
- figma-transform.ts: pure, deterministic transform → designSpecPayloadSchema
shape. Colors as exact hex, spacing/padding/gaps as px, radii expressed as
spacing tokens, typography (family/size/weight/line-height), component
inventory, inferred states, and acceptanceCriteria seeded from the exact
values for the visual-conformance gate. Dev Mode variables are the source of
truth for named tokens; node styles augment them and are the fallback when
variables are unavailable (Enterprise-gated). Fully sorted output.
- figma-extract.ts: the service tying client + transform together — returns
{ designSpec, flatPng bytes, asset files[], warnings }; renders the flat
frame PNG and exports vector icons (svg) / export-flagged illustrations (png).
- Unit tests with recorded fixtures (no live network): transform values +
determinism + schema validation, error classification, and the end-to-end
service including the Enterprise-gated fallback and typed-error propagation.
No PM Agent config update needed: this adds an internal extraction library
consumed by the later ingestion-orchestration task; it registers no new
user-facing tool and changes no agent-visible entity/workflow yet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the extraction service (a117860) into the engine so Phases 1-3 consume its output UNCHANGED. Nothing downstream had to learn what Figma is. A BullMQ `figma.ingest` job (integrations queue) fires when a goal's Figma link is set or changed, and from a new "Re-ingest" action. It runs extractFigmaDesign and lands the three outputs in shapes the engine already understands: 1. The flat frame PNG becomes a ProjectFile(kind=attachment) + Attachment( entityType=engine_goal) — the same vehicle a hand-uploaded mock uses. The goal is therefore an ordinary design goal: the daemon stages the PNG into .tasks/attachments/<goalId>/ via engine:goal-files, and the execution prompt + review visual-diff pick it up with no change. 2. Each exported icon/illustration becomes another goal attachment, staged beside the PNG. The design_spec lists them under a new `assets` field and the execution prompt points the agent at the staged files, telling it to COPY them into the project's ProjectDesignProfile.assetDirs — reuse, never redraw. 3. The derived design_spec is machine-authored, not prompted for. This is the production caller phase 1's authoring path was missing (extractFigmaDesign / transformToDesignSpec had no non-test callers, and no investigation prompt asks an agent to submit a design_spec) — so it is wired once here rather than duplicated as an LLM instruction. design_spec wiring — the least-invasive shape. `Artifact.taskId` is non-nullable and a goal's link is normally set before any task exists, so the spec is persisted as a goal-scoped seed (EngineGoal.figmaDesignSpec) and `createTask` copies it onto every task it mints under that goal, exactly as it already seeds acceptance_criteria. Both readers then find what they already query for and neither changed: the TASK-scoped claim reader (execution phase) and the GOAL-scoped goal-detail reader. Ingestion also back-fills tasks that already exist, so a re-ingest reaches a running goal. Failures never surface raw. Every fault — no connected Figma credential, an expired one, a 403 on a private file, a missing Dev Mode scope, Enterprise-only variables, a dead/expired link, a rate limit — is classified into a coded refusal (figma_not_connected, figma_credential_expired, figma_access_denied, figma_missing_scope, figma_enterprise_only, figma_link_unreadable, figma_rate_limited, figma_ingest_failed) with a plain-language sentence saying what happened, that the goal is unharmed, and the one action that fixes it. Codes are persisted on the goal and rendered by describeGoalRefusal, which maps each to its own recovery control (Connect / Reconnect Figma, or a Re-ingest that self-enables when a rate-limit window clears). The goal is always left recoverable. Details worth calling out: - Job idempotency is keyed on the goal's `updatedAt` re-arm stamp, not on the link: keyed on the link, setting A -> B -> A would collapse onto A's original JobRun, enqueue nothing, and strand the goal at `pending` forever. - Re-ingest purges its own prior output via a new `ProjectFile.source` provenance column, not a `figma-` filename convention — a human is free to name their upload `figma-hero.png`, and deleting it for matching a naming rule would be data loss. - The spec is built and validated from deterministic staged paths BEFORE any attachment is purged or uploaded, so a schema drift can't leave the goal holding a replaced file set with no spec. - Setting, changing, or clearing the link drops the seed: it describes the old link, and would otherwise keep seeding tasks with a mock path that no longer exists. AI AGENT CONFIG (mandatory, same PR): ingestion IS agent-visible, so a `goal_figma_ingest` tool is registered in config-sync buildTools() and ALL_TOOL_NAMES, handled in api/agent/tools/[tool]/route.ts (HANDLERS + handleGoalFigmaIngest, refuse-not-throw on a bad link or a goal with no link), added to senior-pm TOOL_NAMES, and taught in a new "Figma-sourced design specs" section of the senior-pm prompt — which also states the precedence the agent otherwise guesses (design_spec/mock > design profile > skill) and that the pull is asynchronous, so it reports `pending` rather than inventing token values. UI: a Figma design source card on the goal detail page (link, ingest state, Re-ingest), and an "Exported assets" section on the design-spec card. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…classify agent tool Two deterministic failures surfaced by the review test gate, both fallout from the figma-ingestion wiring in 96b15b5 that touched shared code paths: 1. createTask now looks up `engineGoal.findUnique({ select: { figmaDesignSpec } })` to copy a goal's machine-authored design_spec seed onto every task it mints. The file-sync/create route test mocks its own db client and only stubbed engineGoal.findFirst/count, so the new call threw `client.engineGoal.findUnique is not a function` and failed all 9 of that file's cases. Add findUnique -> null (these tasks aren't Figma design goals) so the copy block is a no-op. 2. `goal_figma_ingest` was added to senior-pm TOOL_NAMES (which drives action-policy's ACTION_TOOL_NAMES) but never classified in TOOL_KINDS, so resolveActionPolicy fell back to "write" while the coverage test read KIND_DEFAULT_POLICY[undefined]. Classify it as a downgradable write (a re-ingest only regenerates its own provenance-tagged output, never a human's uploads — not the destructive hard floor). No product behavior changes; both fixes align tests/tables with the shipped wiring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The two Figma-source migrations used `ALTER TABLE "EngineGoal"`, but the EngineGoal model maps to physical table `engine_goals` (@@Map). Every other migration correctly uses `engine_goals`. The wrong name applied cleanly against an existing dev DB (Prisma's client hides the mismatch) but broke a fresh `migrate deploy` — exactly the "migration replay (fresh DB)" CI job's failure (P3018: relation "EngineGoal" does not exist), the class of bug that job exists to catch. ProjectFile has no @@Map, so its `source` column reference was already correct and is unchanged. Verified locally: `prisma migrate deploy` from scratch applies all 268 migrations and `prisma migrate status` reports no drift; the 8 figma* columns land on engine_goals and `source` on ProjectFile. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pull-dev-mode-tokens-exported-assets-from-a- # Conflicts: # src/app/(app)/o/[orgSlug]/p/[projectSlug]/goals/[goalId]/engine-goal-detail-client.tsx # src/app/(settings)/settings/profile/page.tsx # src/app/api/agent/tools/[tool]/route.ts # src/app/api/tools/callback/[provider]/route.ts # src/app/api/v1/me/engine/claim/route.ts # src/lib/engine/artifact-kinds.ts # src/lib/engine/goal-detail.test.ts # src/lib/engine/phase-job.test.ts # src/lib/engine/phase-job.ts # src/lib/engine/refusal-codes.ts # src/lib/engine/service.ts
…pull-dev-mode-tokens-exported-assets-from-a-
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Integration PR for this goal — its tasks commit onto this branch (one PR per goal). Opened automatically by the engine on first push; left as a draft until the goal completes.
Delivery summary
What was built for this goal, assembled for review at delivery time.
Figma ingestion: pull Dev Mode tokens + exported assets from a linked Figma file into the goal's design_spec
PHASE 4 of 4 (design-mock fidelity initiative). BLOCKED BY Phase 2 (goal 187a62dd — ProjectDesignProfile, whose asset dirs the exports land in) and transitively Phase 1 (goal e7739259 — design_spec, which this phase machine-authors). PROBLEM: when the mock lives in Figma, exact values (hex, spacing, font size/weight, radii) and exportable assets exist as DATA — extracting them from a flat image is strictly worse. The image should be the layout reference; the tokens the source of truth. SCOPE: 1. Let a goal carry a Figma link: a URL attachment flavor or field on the goal (goal wizard engine-goals-client.tsx + attachments API src/app/api/v1/projects/[projectId]/attachments/route.ts — decide shape at scoping). 2. Figma auth via Interactor's credential-management API per the repo architecture rule (NEVER register OAuth apps with Figma directly; we exchange opaque references — see CLAUDE.md Integrations). 3. Extraction: pull Dev Mode / variables data for the linked frame(s) — exact hex, spacing, typography, radii, component structure — and machine-author the goal's design_spec artifact (Phase 1's kind) from it; export image assets (icons/illustrations) and drop them per the ProjectDesignProfile asset dirs (Phase 2) so agents reuse instead of redrawing. 4. Also export a flat PNG of the frame as a normal image attachment so the whole Phase 1-3 pipeline (staging, execution prompt, visual diff) works unchanged. 5. MANDATORY per CLAUDE.md: agent-config update if any new tool/entity becomes agent-visible. OUT OF SCOPE: Photoshop/Illustrator (no comparable API — those remain flat-image mocks via Phase 1); building our own Figma OAuth app.
Branch:
goal/figma-ingestion-pull-dev-mode-tokens-exported-assets-from-a-· PR: #1930What shipped
Plan of record
DECOMPOSE. Figma ingestion (Phase 4 of the design-mock fidelity initiative) is buildable now — Phase 1 (design_spec artifact kind) and Phase 2 (ProjectDesignProfile) are already on main. Spawned 4 shippable tasks (#1823-#1826): (1) Figma auth via Interactor credential-management [foundation]; (2) let a goal carry a Figma link [schema+wizard+API]; (3) Figma extraction → Dev Mode tokens/variables/assets/flat PNG; (4) ingestion orchestration + design-pipeline wiring + MANDATORY agent-config. Key architectural findings and the decisions they forced are recorded below; open choices were decided (not asked) because each is a reasonable, reversible engineering call. === ARCHITECTURE (from repo exploration) === - design_spec (Phase 1) is an OPEN-registry artifact kind (src/lib/engine/artifact-kinds.ts, designSpecPayloadSchema). CRITICAL CONSTRAINT: Artifact is STRICTLY TASK-SCOPED (Artifact.taskId required, cascade on task). Execution reads the design_spec on its OWN task's investigation output (claim/route.ts:255); goal-detail reads latest across the goal's tasks (goal-detail.ts:349). So a server job canNOT write a goal-scoped design_spec artifact up front. - Attachments are FILE-ONLY (no URL flavor anywhere — UI , API zod fileId uuid, Attachment.projectFileId non-null). A mock is a ProjectFile(kind:attachment) + Attachment(entityType:engine_goal), staged into the worktree by the daemon via /api/v1/files/[fileId]/attachment-bytes. - ProjectDesignProfile.assetDirs are REUSE-POINTERS to existing repo dirs ("reuse, never recreate", rendered by renderDesignProfileText) — NOT export drop-targets; nothing writes assets into them. - Credential pattern is established: initiateOAuth/getCredentialToken (interactor-client.ts), mint-on-demand token like github-token.ts, opaque credential_id only. GoogleCalendarConnection is the clean model to clone. Figma is greenfield (zero repo references). - Worker jobs: enqueue(...) + src/worker/jobs/* + index.ts processor; extract-sow.ts / idea-flow-bootstrap.ts are templates. NO worker-side helper mints a ProjectFile+S3 object yet — one must be added (s3.ts presignPut + db.projectFile.create + db.attachment.create). - Agent-config registration points: senior-pm.ts TOOL_NAMES + prompt, config-sync.ts ALL_TOOL_NAMES/buildTools(), api/agent/tools/[tool]/route.ts HANDLERS. === DECISIONS (made, not asked) === 1. LINK SHAPE → a nullable figmaFileUrl (+figmaNodeId) field on EngineGoal, captured in the wizard. Rejected the 'URL attachment flavor' option: attachments are file-only end-to-end, so a URL flavor is a large cross-cutting change for no benefit here. 2. EXTRACTION LOCUS → a server-side BullMQ job (matches 'so the Phase 1-3 pipeline works unchanged'), not daemon-side. 3. HOW THE MACHINE-AUTHORED SPEC REACHES THE TASK-SCOPED design_spec → ingestion stages the exact computed payload as a figma-design-spec.json GOAL attachment + the flat PNG (figma-frame.png) + exported assets; the investigation-phase prompt instructs the agent to ADOPT the staged JSON VERBATIM as the design_spec (source of truth; PNG = layout reference only). Smallest blast radius; respects the task-scoped artifact model. (Alternative — server auto-materialize the artifact onto each implementation task — noted but not chosen: needs a per-task hook and a goal-scoped store.) 4. FLAT PNG → staged as an ordinary engine_goal image attachment so isDesignGoal + staging + execution + visual-conformance all work UNCHANGED (goal scope item 4). 5. EXPORTED ASSETS → staged as goal attachments; execution prompt names them + the ProjectDesignProfile target assetDir and instructs reuse-not-redraw (the agent copies them into the repo dir at execution — ingestion canNOT write the repo, no worktree exists then). 6. AGENT-CONFIG (mandatory) → expose a goal_link_figma write-tool to the senior-pm orchestrator (parallels goal_create) + prompt note that a Figma link machine-authors design_spec; fallback: prompt-only note + PR justification if no new tool is warranted. === ASSUMPTIONS / DEPENDENCIES === - EXTERNAL DEPENDENCY: assumes Interactor's credential-management exposes a
figmaservice_id. Task #1823 verifies this EARLY via SKB (fetchServiceCapabilities('figma')) and, if absent, surfaces a plain-language operator blocker rather than building direct Figma OAuth (explicitly out of scope). The plan is correct either way; this just gates buildability of the auth path. - designSpecPayloadSchema has NO radii field — corner radii fold into components/acceptanceCriteria, or the schema is extended minimally + additively (arrays default []). - Out of scope (per goal): Photoshop/Illustrator (remain flat-image mocks via Phase 1); building our own Figma OAuth app. === TASK GRAPH / SEQUENCING === - #1823 (auth) and #1824 (goal link field/wizard/API) are independent foundations. - #1825 (extraction) depends on #1823 (getFigmaToken) + #1824 (figmaFileUrl). - #1826 (ingestion orchestration + pipeline prompt wiring + agent-config) depends on #1825 (staged outputs) and #1824 (field/API). The goals-API enqueue call and the worker's enqueueFigmaIngest(goalId) seam are coordinated across #1824/#1826. All land on this goal's single branch/PR, satisfying the same-PR agent-config rule.Acceptance criteria
npx prisma migrate deployapplies every migration including 20261022000000_add_goal_figma_source and exits 0 — no P3018 / SqlState 42P01 "relation does not exist" error.npx prisma migrate statusexits 0 and reports "Database schema is up to date!" (no drift between prisma/migrations and prisma/schema.prisma).prisma migrate resolvestep is required for any environment.success— zero check-runs with conclusionfailure, includingmigration replay (fresh DB)— after the fix is pushed.Review
5 of 5 review finding(s) resolved.
Commits
97674239c0662f7f7662586b7053c9bb1821f892…cf93140653e74300ffe622a465105f7969f2b60ain InteractorOSS/product-manager