Skip to content

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 into
mainfrom
goal/figma-ingestion-pull-dev-mode-tokens-exported-assets-from-a-
Jul 11, 2026
Merged

Figma ingestion: pull Dev Mode tokens + exported assets from a linked Figma file into the goal's design_spec#1930
pulzzejaehoon merged 9 commits into
mainfrom
goal/figma-ingestion-pull-dev-mode-tokens-exported-assets-from-a-

Conversation

@interactor-pm-service

@interactor-pm-service interactor-pm-service Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

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: #1930

What shipped

  • Plan: Figma ingestion: pull Dev Mode tokens + exported assets from a linked Figma file into the goal's design_spec (Planning)
  • Figma auth via Interactor credential-management (Implementation)
  • Let a goal carry a Figma link (model + wizard + API) (Implementation)
  • Figma extraction service: Dev Mode tokens, variables, assets, PNG (Implementation)
  • Figma ingestion orchestration + pipeline wiring + agent-config (Implementation)

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 figma service_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

  • migration-targets-real-table: prisma/migrations/20261022000000_add_goal_figma_source/migration.sql references the physical table "engine_goals" (the @@Map target of model EngineGoal) in all seven ALTER TABLE statements; the string "EngineGoal" no longer appears as a table name in any migration SQL on the goal branch.
  • fresh-db-replay-passes: On a fresh Postgres database with the pgvector extension enabled, npx prisma migrate deploy applies every migration including 20261022000000_add_goal_figma_source and exits 0 — no P3018 / SqlState 42P01 "relation does not exist" error.
  • drift-assert-passes: After a fresh replay, npx prisma migrate status exits 0 and reports "Database schema is up to date!" (no drift between prisma/migrations and prisma/schema.prisma).
  • figma-columns-match-schema: After replay, table engine_goals has exactly the seven nullable columns figmaUrl, figmaFileKey, figmaNodeId, figmaIngestStatus, figmaLastIngestedAt, figmaIngestError, figmaIngestErrorCode with types matching the EngineGoal model in schema.prisma (six text, figmaLastIngestedAt timestamp); the migration remains purely additive with no backfill and no data-destructive statement.
  • branch-refreshed-against-main: The goal branch merges current origin/main (it is ~97 commits behind, and main adds 7 migrations of its own, including a second 20261022000000_* migration on engine_goals), and the merged migration set still satisfies fresh-db-replay-passes and drift-assert-passes.
  • no-failed-migration-state-left-behind: The correction is made such that no database can be left in a Prisma failed-migration (P3018) state: since 20261022000000_add_goal_figma_source has never applied successfully in any environment (it errors on its first statement and rolls back), it is corrected in place rather than by adding a compensating migration, and no prisma migrate resolve step is required for any environment.
  • previously-green-checks-stay-green: All check-runs that were already passing at the branch head remain passing: unit tests, unit tests (shard 1/2), unit tests (shard 2/2), typecheck + lint, build (oss), build (ee), dependency audit, eval scenarios structure, agent eval gate (live), e2e (screenshot capture). In particular tests/unit/api/tools-callback-provider.test.ts stays green.
  • ci-suite-green-on-pr: The check-suite for the head commit of PR Figma ingestion: pull Dev Mode tokens + exported assets from a linked Figma file into the goal's design_spec #1930 (branch goal/figma-ingestion-pull-dev-mode-tokens-exported-assets-from-a-) concludes success — zero check-runs with conclusion failure, including migration replay (fresh DB) — after the fix is pushed.
  • scope-contained-to-ci-fix: The diff added on top of the existing branch head is limited to what is required to make CI green (the migration SQL table-name correction plus the merge of origin/main); no Figma feature behaviour, product runtime code, or unrelated file is changed.

Review

5 of 5 review finding(s) resolved.

Commits

97674239c0662f7f7662586b7053c9bb1821f892…cf93140653e74300ffe622a465105f7969f2b60a in InteractorOSS/product-manager

pulzzejaehoon and others added 7 commits July 9, 2026 21:05
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>
@psdjungpulzze psdjungpulzze marked this pull request as ready for review July 11, 2026 05:05
…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-
@pulzzejaehoon pulzzejaehoon merged commit 35426e7 into main Jul 11, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants