Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
fd59108
feat: ground-truth reader and validation tools to stop token/endpoint…
owjs3901 Sep 2, 2026
d7c58a1
fix: guarantee handoff completion and ban hand-interpreting the node …
owjs3901 Sep 2, 2026
2e31067
fix: make Figma handoffs self-enforcing across MCP clients
owjs3901 Sep 2, 2026
77c8936
feat: skip Dynamic Client Registration when a pre-registered Figma cl…
owjs3901 Sep 2, 2026
e83e0f5
feat(figma): add text-first fast envelope transport
owjs3901 Sep 3, 2026
2a97fbb
feat(figma): emit bounded text envelopes from fast scripts
owjs3901 Sep 3, 2026
ed7f8dd
feat(figma): preserve successful screens after partial collection fai…
owjs3901 Sep 3, 2026
1b5345a
fix(server): recognize Section errors in host handoffs
owjs3901 Sep 3, 2026
0025411
feat(server): report partial Section export failures
owjs3901 Sep 3, 2026
3700a69
docs(server): require per-screen Section exports
owjs3901 Sep 3, 2026
d8ccae6
docs: explain Section continuation and partial failures
owjs3901 Sep 3, 2026
665fa02
fix(server): match brief's literal nextAction shape for Section selec…
owjs3901 Sep 3, 2026
e5e9d27
feat(figma): shrink fast snapshot manifest and paginate the text enve…
owjs3901 Sep 3, 2026
fb73463
docs: describe manifest-scoped fast snapshot and text pagination
owjs3901 Sep 3, 2026
52d0fe8
fix(figma): emit offset on the fast snapshot cursor marker
owjs3901 Sep 3, 2026
3ff34e4
perf(figma): halve the fast envelope again and bound it by the text l…
owjs3901 Sep 3, 2026
14a6eaa
fix(devup-ui): keep a paint's opacity when formatting its colour
owjs3901 Sep 3, 2026
7028532
fix(devup-ui): only report an effect fallback when an effect is actua…
owjs3901 Sep 3, 2026
6f713f9
chore: ignore local agent state directories
owjs3901 Sep 3, 2026
c153721
refactor: emit every diagnostic and guidance string in English
owjs3901 Sep 3, 2026
3ed2f63
feat(figma): make the direct Figma OAuth path work end to end
owjs3901 Sep 3, 2026
6696f2d
fix(figma): tolerate a re-serializing relay when decoding upstream re…
owjs3901 Sep 3, 2026
7f6803e
fix(figma): drop the now-dead utf8Bytes plumbing and move the metadat…
owjs3901 Sep 3, 2026
adf6fa6
ci: release every MCP binary on a changepacks version bump, and requi…
owjs3901 Sep 3, 2026
b8ff8c7
ci: consolidate verification and release into one changepacks-driven …
owjs3901 Sep 3, 2026
78bafa7
fix(server): accept an output root reached through a symlink
owjs3901 Sep 3, 2026
f39d3cf
fix(server): answer a Section link with its screens on the direct path
owjs3901 Sep 3, 2026
42c73fc
fix(figma): export SVG assets, and say what a missing payload actuall…
owjs3901 Sep 3, 2026
e0d43d2
fix(figma): discover an asset the way the Figma plugin does
owjs3901 Sep 3, 2026
ced186d
fix(devup-ui): stop flattening a container with text into a single image
owjs3901 Sep 3, 2026
5d8fc23
fix(devup-ui): count layout coverage against the real asset boundary
owjs3901 Sep 3, 2026
5dfdb9f
fix(devup-ui): keep the pinned size of absolutely positioned nodes
owjs3901 Sep 3, 2026
89995cb
fix(devup-ui): stop reporting a loss the absolute conversion no longe…
owjs3901 Sep 3, 2026
d575259
fix(figma): report an upstream refusal as itself, and hidden nodes as…
owjs3901 Sep 3, 2026
19921fb
fix(server): let the schema and the errors state what is actually acc…
owjs3901 Sep 3, 2026
9674eaf
fix(figma): classify a quota refusal as retryable instead of permanent
owjs3901 Sep 3, 2026
a2a2481
fix(figma): describe quota recovery as it works, not as a reset
owjs3901 Sep 3, 2026
d1d2993
fix(devup-ui): place children of a frame that has no auto-layout
owjs3901 Sep 3, 2026
2b29ed2
fix(devup-ui): only space apart children that are actually rendered
owjs3901 Sep 3, 2026
8750864
fix(figma): collect the text truncation setting instead of assuming it
owjs3901 Sep 3, 2026
180aa89
fix(devup-ui): stop turning a screen's canvas width into a constraint
owjs3901 Sep 3, 2026
1be20d0
feat(devup-ui): give each image fill its own file instead of one shar…
owjs3901 Sep 3, 2026
dfe4417
fix(devup-ui): read a derived padding as the single value it is
owjs3901 Sep 3, 2026
cfa1b99
fix(devup-ui): do not anchor children a folded asset no longer has
owjs3901 Sep 3, 2026
c4d7ab9
fix(devup-ui): drop the anchor once padding already places the child
owjs3901 Sep 3, 2026
3537c44
fix(devup-ui): stop counting the canvas size the output withholds on …
owjs3901 Sep 3, 2026
c761d1d
fix(figma): collect defaultVariant, and hold the collector to what th…
owjs3901 Sep 3, 2026
ceb8b69
fix(devup-ui): put rasters with the images, and state a pinned size o…
owjs3901 Sep 3, 2026
e67ac65
test(devup-ui): replay real screens without spending the Figma allowance
owjs3901 Sep 3, 2026
575f37c
fix(figma): let a Section without screens still offer what it holds
owjs3901 Sep 4, 2026
b1817fd
fix(figma): stop rejecting a multi-chunk collection over its own cursor
owjs3901 Sep 4, 2026
9faf25f
test(devup-ui): compare generated code against what each case states
owjs3901 Sep 4, 2026
78cccbc
fix(figma): stop a Section from hiding every case behind its notes
owjs3901 Sep 4, 2026
42ce114
fix(figma): offer a Section's children where the choosing actually ha…
owjs3901 Sep 4, 2026
074142e
test(devup-ui): give each stated case one note and one only
owjs3901 Sep 4, 2026
491dad1
test(devup-ui): let the note say which node it is describing
owjs3901 Sep 4, 2026
75fdab3
fix(mcp): stop offering a connection that cannot carry a collection
owjs3901 Sep 4, 2026
66cedc5
docs: drop the connection path that cannot carry a collection
owjs3901 Sep 4, 2026
db1b795
fix(mcp): wait out a spent allowance instead of losing the collection
owjs3901 Sep 4, 2026
5018283
test(devup-ui): prefer the candidate a note is actually describing
owjs3901 Sep 4, 2026
52f9cb1
docs: correct what client_name devup-mcp registers under
owjs3901 Sep 4, 2026
7ff59f1
test(devup-ui): say why a token name differs before it reads as a defect
owjs3901 Sep 4, 2026
5d5c701
feat!: collect over the direct connection only
owjs3901 Sep 4, 2026
957a885
feat(export): return the screen as primitives and as components together
owjs3901 Sep 4, 2026
c41385b
feat(figma): collect a screen's other widths with it
owjs3901 Sep 4, 2026
0473a11
docs: write down how the plugin merges breakpoints, from its own output
owjs3901 Sep 4, 2026
fa84e15
docs: say which branch of the merge is the point and which is the cost
owjs3901 Sep 4, 2026
cedd03d
feat(devup-ui): line up the widths a screen is drawn at, and say wher…
owjs3901 Sep 4, 2026
1c58653
docs: keep what the plugin answered, and say it is not the same as right
owjs3901 Sep 4, 2026
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 .changepacks/changepack_log_direct_oauth_path.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": {
"crates/devup-mcp/Cargo.toml": "Minor",
"crates/devup-mcp-figma/Cargo.toml": "Minor",
"crates/devup-mcp-devup-ui/Cargo.toml": "Patch",
"crates/devup-mcp-visual/Cargo.toml": "Patch"
},
"note": "Make the direct Figma OAuth path work end to end, so a URL converts to DevupUI TSX without a host Figma MCP or an agent relay in the loop. Three defects each independently blocked it: Dynamic Client Registration always sent a client_name that Figma's catalog allowlist rejects with a plain-text 403, and the name is now configurable through --figma-client-name / DEVUP_FIGMA_CLIENT_NAME with doctor reporting the active value; the client_secret issued by registration was discarded even though Figma advertises only client_secret_basic/client_secret_post, so the token exchange answered a bare 400 after registration and browser consent had both succeeded, and the secret is now kept beside its client_id for the authorization-code exchange and refresh; and auth_network_error dropped the underlying transport error entirely, so every failure surfaced identically with no details, and it now carries kind/status/url/cause-chain with the URL reduced to scheme, host and path so a query string cannot carry a code or token into a log. Also tolerate a relay that re-serializes upstream results: get_metadata is no longer bare XML because Figma prepends a selected-nodes block and appends an instruction footer, and the fast envelope no longer requires integrity.utf8Bytes to equal the received byte length, since truncation is already caught by JSON parsing plus the node, resource-reference and resource-presence checks that read content rather than its serialized form. Every diagnostic and guidance string is now emitted in English, because these are returned to an LLM agent over MCP where Korean prose costs several times the tokens; Korean Figma fixture data is preserved where tests use it deliberately to exercise CJK handling. Consolidates CI and release into the single workflow the other org projects use, driven by changepacks/action: the action cuts draft releases and reports them through pending_releases, a build matrix compiles devup-mcp and devup-mcp-visual for Linux, Windows and a macOS universal binary and uploads them onto those drafts, and a finalize step publishes the drafts only after the uploads succeed, so a release is never visible without its binaries. A changepack-required gate fails any pull request that edits a crate without leaving a changepack log, since such a change never moves the version and therefore never releases. Also fixes output-root resolution for a root reached through a symlink: the root was canonicalised when the policy opened it while the requested outputPath was not, so a caller passing a path under the spelling it was given was refused with outputPath is outside the allowed root. On macOS that was the normal case rather than an edge case, because /tmp and the system temp directory both resolve through /var to /private/var. Fixes Section targets on the direct path: the fast snapshot script throws DEVUP_TARGET_IS_SECTION and MCP delivers a thrown error as a successful call carrying isError, which the direct path handed to accept and then failed with snapshot data not found, so a Section link had no way to reveal the screens inside it. It is now rejected exactly as the handoff path already did, so the collector switches to the section index and answers with selectable screens. Fixes SVG asset export, which failed for every request while PNG worked: Figma's remote MCP returns a written PNG as an image attachment but does not return a written .svg at all, so the bytes never reached devup-mcp. SVG is now exported as a string and carried inline beside the descriptor under a bounded size, and the payload search steps through the JSON encoding of a text block and accepts a text payload as well as base64. The missing-payload error now reports which content shapes and mime types the response actually carried, so an absent attachment, a wrong mime type and an unread field stay distinguishable. Adds server instructions covering that the generated component name and asset paths are starting points rather than contracts, that a fixed asset must be exported through assetRequests with an outputPath instead of referenced by a path that does not exist, and that resource delivery should be preferred over inlining bytes.",
"date": "2026-09-03T17:20:00+09:00"
}
8 changes: 8 additions & 0 deletions .changepacks/changepack_log_groundtruth_tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"changes": {
"crates/devup-mcp/Cargo.toml": "Minor",
"crates/devup-mcp-devup-ui/Cargo.toml": "Minor"
},
"note": "Add three read-only ground-truth tools so an agent can never fabricate a project identifier it never verified: devup_project_context reads a project's real devup.json theme tokens, openapi.json endpoints/schemas, or Vespertide models/*.json tables/columns/enums fresh on every call (no session cache), returning a shared {found:false,guardrail:{action:'stop-and-report',...}} envelope instead of guessing when the target file is missing; devup_ui_validate parses DevupUI TSX with the existing oxc_parser/oxc_allocator/oxc_span stack via a new oxc_ast_visit-based walker and flags unknown $token references (with edit-distance-suggested existing tokens), hardcoded hex colors/px lengths that match an existing token, unknown props on Box/Flex/Text/Center/Grid/Image (checked against the published devup-ui Style Props API reference, not invented), and non-static values inside css()/globalCss()/keyframes() calls specifically -- verified against devup-ui's own docs and css-utils-literal-only ESLint rule that plain JSX style props (bg={dynamic}) are valid devup-ui and must not be flagged; devup_stack_diff detects drift across vespertide model -> sea-orm entity -> vespera route -> openapi.json -> devup-api client with every finding carrying an explicit low/medium confidence since none of the checks is a real compiler front end. Regression-tested against the exact incident that motivated this work: three agents independently inventing a $gray100 color token, a 16px bubble radius, and a 36px avatar size that did not exist in the real project devup.json.",
"date": "2026-09-02T00:00:00+09:00"
}
2 changes: 1 addition & 1 deletion .changepacks/config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"ignore": ["**", "!/crates/*/Cargo.toml"],
"baseBranch": "main",
"latestPackage": null
"latestPackage": "crates/devup-mcp/Cargo.toml"
}
206 changes: 205 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,89 @@
name: CI

# One workflow, matching devup-ui and the other org projects: verification,
# changepacks version management, binary builds and release publication all
# live here rather than in a second file that can drift out of step.
#
# Release flow (driven by changepacks/action, not by hand):
# 1. A pull request touching crates/ must carry a changepack. `changepacks`
# comments the detected packs; `changepack-required` makes it a gate.
# 2. On push to main with pending changepacks, the action opens an
# "Update Versions" pull request that runs `changepacks update`.
# 3. Merging that PR leaves no changepacks, so the action cuts tags and
# *draft* releases and reports them in `pending_releases`.
# 4. `build` compiles every MCP binary for all three platforms and uploads
# them onto those drafts.
# 5. `finalize` publishes the drafts, but only once the uploads succeeded —
# so a release is never visible without its binaries attached.

on:
pull_request:
push:
branches: [main]
pull_request:

permissions:
contents: write
pull-requests: write

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

jobs:
# The action comments the changepack status on a pull request but does not
# fail it. A crate change that ships without a changepack never moves the
# version, so it never releases — this turns that silent outcome into a
# red check with the command to fix it.
changepack-required:
name: changepack required
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Require a changepack for crate changes
shell: bash
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
base="$(git merge-base "$BASE_SHA" "$HEAD_SHA")"
changed="$(git diff --name-only "$base" "$HEAD_SHA")"

crate_changes="$(printf '%s\n' "$changed" | grep -E '^crates/' || true)"
if [ -z "$crate_changes" ]; then
echo "No crate sources touched; a changepack is not required."
exit 0
fi

log_changes="$(printf '%s\n' "$changed" \
| grep -E '^\.changepacks/changepack_log_.*\.json$' || true)"
if [ -n "$log_changes" ]; then
echo "Changepack present:"
printf ' %s\n' $log_changes
exit 0
fi

{
echo "This pull request changes crate sources but adds no changepack log."
echo
echo "Without one the workspace version never moves, so the change"
echo "ships to main and is never released."
echo
echo " cargo install changepacks"
echo " changepacks"
echo
echo "Pick the affected crates, choose Major/Minor/Patch, and write"
echo "the release note, then commit the generated"
echo ".changepacks/changepack_log_*.json alongside your change."
echo
echo "--- crate files changed without a changepack ---"
printf ' %s\n' $crate_changes
} >&2
exit 1

verify:
strategy:
matrix:
Expand All @@ -27,3 +105,129 @@ jobs:
- run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- run: cargo insta test --workspace --all-features --check
- run: cargo build --workspace --release

changepacks:
name: changepacks
needs: verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
# changepacks diffs HEAD against the previous release commit; the
# default shallow fetch grafts away every parent, so that lookup
# fails and the release never publishes.
fetch-depth: 0
fetch-tags: true
- uses: changepacks/action@main
id: changepacks
with:
token: ${{ secrets.GITHUB_TOKEN }}
create_release: true
outputs:
changepacks: ${{ steps.changepacks.outputs.changepacks }}
release_assets_urls: ${{ steps.changepacks.outputs.release_assets_urls }}
pending_releases: ${{ steps.changepacks.outputs.pending_releases }}

build:
name: build (${{ matrix.os }})
needs: changepacks
# Only when a draft release is actually waiting for assets. On a pull
# request, or on a push that merely opened the Update Versions PR, there
# is nothing to attach to.
if: >-
needs.changepacks.outputs.pending_releases != ''
&& needs.changepacks.outputs.pending_releases != '{}'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
targets: x86_64-unknown-linux-gnu
suffix: linux-x86_64
ext: ""
- os: windows-latest
targets: x86_64-pc-windows-msvc
suffix: windows-x86_64
ext: ".exe"
- os: macos-latest
# Fused into one universal binary so a single macOS asset runs on
# both Apple Silicon and Intel.
targets: aarch64-apple-darwin x86_64-apple-darwin
suffix: macos-universal
ext: ""
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@1.98.0
- uses: Swatinem/rust-cache@v2
- name: Build release binaries
shell: bash
env:
TARGETS: ${{ matrix.targets }}
SUFFIX: ${{ matrix.suffix }}
EXT: ${{ matrix.ext }}
OS: ${{ matrix.os }}
run: |
set -euo pipefail
for target in $TARGETS; do
rustup target add "$target"
cargo build --release --target "$target" -p devup-mcp -p devup-mcp-visual
done
mkdir -p dist
for bin in devup-mcp devup-mcp-visual; do
out="dist/${bin}-${SUFFIX}${EXT}"
if [ "$OS" = "macos-latest" ]; then
lipo -create -output "$out" \
"target/aarch64-apple-darwin/release/${bin}" \
"target/x86_64-apple-darwin/release/${bin}"
file "$out"
else
set -- $TARGETS
cp "target/$1/release/${bin}${EXT}" "$out"
fi
done
ls -l dist
- name: Upload binaries onto the draft release
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ASSET_URLS: ${{ needs.changepacks.outputs.release_assets_urls }}
run: |
set -euo pipefail
# release_assets_urls maps project path -> asset upload URL. The
# binaries belong to the devup-mcp crate; the library crates get
# their own releases with no assets.
upload="$(printf '%s' "$ASSET_URLS" \
| jq -r '.["crates/devup-mcp/Cargo.toml"] // empty')"
if [ -z "$upload" ]; then
echo "no asset upload URL for crates/devup-mcp/Cargo.toml" >&2
printf '%s\n' "$ASSET_URLS" >&2
exit 1
fi
# Drop the RFC 6570 template suffix, e.g. "{?name,label}".
upload="${upload%%\{*}"
for file in dist/*; do
name="$(basename "$file")"
echo "uploading $name"
curl --fail-with-body -sS -X POST \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @"$file" \
"${upload}?name=${name}" >/dev/null
done

finalize:
name: finalize release
needs: [changepacks, build]
if: >-
needs.changepacks.outputs.pending_releases != ''
&& needs.changepacks.outputs.pending_releases != '{}'
runs-on: ubuntu-latest
steps:
# Finalize-only: the action neither installs changepacks nor touches the
# repository here, so no checkout is needed. Running it after `build`
# is what guarantees a published release always has its binaries.
- uses: changepacks/action@main
with:
token: ${{ secrets.GITHUB_TOKEN }}
finalize_releases: ${{ needs.changepacks.outputs.pending_releases }}
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,12 @@
.env.*
!.env.example

# Agent session state (oh-my-claudecode / omo), local to a working copy
.omc/
.omo/

# Snapshots captured from a live Figma file to iterate on codegen without
# spending the tool-call allowance. Scratch, not ground truth: the pinned
# corpus under fixtures/devup-figma-plugin is what decides correctness.
/fixtures/local-screens/

14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ keyring = "4.2"
insta = { version = "1.48.0", features = ["glob", "json"] }
image = { version = "=0.25.10", default-features = false, features = ["png"] }
oxc_allocator = "=0.148.0"
oxc_ast = "=0.148.0"
oxc_ast_visit = "=0.148.0"
oxc_parser = "=0.148.0"
oxc_span = "=0.148.0"
rand = "0.10"
Expand Down
Loading
Loading