Move build-docker and build-docker-devenv jobs from penpot-extended-runner
to penpot-standar-runner, point tests-exporter at the canonical
penpot-extended-runner label instead of the stale penpot-runner-02 alias,
and switch build-tag/release notify jobs from ubuntu-24.04 to ubuntu-latest.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Cached Skia paragraphs bake absolute image/gradient shaders at layout
time. On move, clones reuse those paragraphs while painting at the new
selrect, so glyphs move and the fill stays put. Record the paint origin
when layout is built and translate the canvas when painting from cache
so shaders track the text. Also sync bounds before update_layout so
fills bake against the current container.
* ♻️ Consolidate HIGHLIGHTS.md into CHANGES.md 🚀 section
Eliminate the redundant HIGHLIGHTS.md file and make CHANGES.md
the single source of truth for version highlights.
- Add 🚀 section for 2.15.0 (MCP server integration)
- Add 4 missing highlight entries to 2.17.0 🚀 section
- Rewrite frontend parser to extract from CHANGES.md 🚀
subsections instead of flat HIGHLIGHTS.md format
- Decouple parse-latest-released-version from highlights
extraction so it works independently of 🚀 content
- Conditionally render highlights section in modal when non-empty
- Rewrite tests for new parser behavior (11 tests, 21 assertions)
- Delete HIGHLIGHTS.md and remove .gitignore exception
- Add step 8b to update-changelog skill for proactively
proposing highlights during release workflows
- Add missing-highlights and missing-highlight-reference
anomaly types to the changelog anomaly report script
Closes#11530
AI-assisted-by: qwen3.7-plus
* ♻️ Use consistent string library and add multi-version test
Address code review findings:
- Use str/split (cuerdas) consistently in extract-rocket-items
instead of mixing cstr/split (clojure.string)
- Add parse-highlights-extracts-multiple-versions test to verify
the parser correctly extracts 🚀 items from multiple
versions in a single CHANGES.md body
AI-assisted-by: qwen3.7-plus
* ♻️ Scope 🚀 checks to X.Y.0 and split gaps from anomalies
Type C now only checks released X.Y.0 versions, since patches never carry 🚀 subsections by design. Type D requires both issue AND PR references with exact format, accepting multi-PR entries. C/D are reported as highlight gaps in their own section and no longer count toward the anomaly total. Key Principles and anomaly definitions updated to match. Addresses review comments on PR #11531.
AI-assisted-by: muse-spark-1.3-contributor
* ✨ Render markdown links and bold in check-updates highlights
The highlights modal showed raw markdown from CHANGES.md 🚀 lines (brackets and URLs). Add a pure parse-highlight-item parser for inline links and bold, render fragments with literal hiccup in the modal (links open in a new tab), and style links and strong elements. Non-http URLs and malformed markup degrade to plain text. Adds 12 unit tests.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Point full changelog link to main instead of staging
The view-changelog button in the check-updates modal linked to the staging branch. Point it to main, which holds the published changelog. Version detection still fetches from staging.
AI-assisted-by: muse-spark-1.3-contributor
SkSVGDevice drops paint image-filters, so re-emit visible layer blur as a
native feGaussianBlur filter on the composite <g>. Match canvas sigma via
radius_to_sigma(value * scale), and skip Skia blur filters on the SVG
VectorRenderer path so shapes do not vanish.
Closes#11380
* 🐛 Fix fontFamilies token property mapping in Plugin API
The Plugin API exposes the font-family token property as `fontFamilies`,
while Penpot stores the canonical applied-token attribute as
`:font-family`. The bidirectional plugin/internal attribute map did not
contain that alias, so explicit `applyToken(..., ["fontFamilies"])`
validation rejected the property and applied-token readback exposed the
undocumented singular `fontFamily`.
Add `:font-family -> :font-families` to the existing canonical alias
map. The reverse mapping is derived automatically, keeping application
and readback symmetric without introducing a font-specific code path.
Closes#11405
AI-assisted-by: Omen Alpha
Signed-off-by: 최준수 <junsoo1172@gmail.com>
* 🐛 Fix fontFamilies e2e test to target a text shape
The fontFamilies end-to-end regression created a flex layout frame,
whose attribute set (frame-with-layout-attributes) excludes
:font-family. The workspace token application filters such shapes,
so the internal binding and readback assertions would pass vacuously
without exercising the alias.
Target an actual `:text` shape (ctho/add-text) instead, so the test
verifies the full JS "fontFamilies" -> schema -> alias -> canonical
:font-family -> camelCase readback path.
AI-assisted-by: Omen Alpha
Signed-off-by: 최준수 <junsoo1172@gmail.com>
* 🐛 Fix fontFamilies test WASM error and add changelog entry
The text-shape fontFamilies e2e applies a layout-affecting token via
wasm renderer path, hitting missing WASM exports under Node. Merge
thw/setup-wasm-mocks! into the :each fixture and add plugins
CHANGELOG entry for the fontFamilies alias fix.
AI-assisted-by: muse-spark-1.3-contributor
Related to #11566
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
---------
Signed-off-by: 최준수 <junsoo1172@gmail.com>
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
Closes#11384
Skia's SVG backend drops save_layer+SrcIn, so image strokes are re-emitted
as a linked <image> clipped to an opaque stroke silhouette (filled outline,
clip-rule evenodd). Open-path caps join the silhouette and grow the image
dest by cap_bounds_margin so markers stay textured.
Store the hide-resolved filter in user storage and restore it when
entering the workspace or viewer, consistent with canvas comment
visibility from #10239. Match the comments filter separator styling to
the main menu and add the missing mentions option in the viewer
dropdown.
Closes#10686
Signed-off-by: Andres Gonzalez <andres.gonzalez79@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* 🎉 Implement independent image bounds resizing
Add canvas resize interaction mode that allows users to resize an
image object's bounding box independently from the underlying bitmap
content without scaling or distortion while holding the Mod key.
AI-assisted-by: gemini-2.5-pro
* ♻️ Address reviewer feedback from elenatorro
- Remove legacy cfh/image-shape? check in shape-has-image-fill?
- Guard bounds-resize with positive dimensions instead of clamping scalev to preserve flipping
- Remove :metadata from transform-attrs in modifiers.cljs
- Restore preserveAspectRatio logic based on keep-ar? in fills.cljs
- Compute source rect against destination rect for raster and SVG fills in WASM renderer
* 🔧 Fix clippy needless borrow warnings in wasm image fills
---------
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
Session tokens now carry an :exp claim anchored to created-at (not
modified-at), so activity cannot extend the session beyond the
absolute maximum (default 30 days, configurable via
PENPOT_AUTH_TOKEN_COOKIE_MAX_AGE_ABSOLUTE). The existing token
verification already rejects expired tokens, so enforcement is
automatic. Also extends the GC task to purge expired
http_session_v2 rows, which were previously never cleaned up.
Closes#11444
AI-assisted-by: longcat-2.0
Teach agents to verify their changes with ./scripts/ci: module list,
task selection flags, log locations under .ci-logs/, and per-module
workflows (lint-only pass, --fix, --paren-repair, common/ consumer
checks). Register the skill in the skills README, point AGENTS.md at
the new memory, and add the script to the critical-info dev scripts.
AI-assisted-by: omen-alpha
The flow only supported continue mode (existing branch) or standalone
mode (issue + branch). Add a "direct" argument (aliases: "no branch",
"direct commit") that skips both and commits on the current branch as
it is, even when it is a base branch — for small tooling changes the
user wants committed in place.
AI-assisted-by: omen-alpha
* 📚 Add enterprise section
* 📚 Update images
* 📚 Add SSO module section to Enterprise plan user guide
* 📚 Add more FAQs to Enterprise plan user guide
* 📚 Add SSO configuration screenshot to Enterprise plan user guide
* 📚 Replace membership screenshot with webp to fix broken reference
The membership image reference already pointed to .webp while the tracked
file was .png, leaving a broken image. Swap the asset to match.
* 📚 Document how to access the Admin Console
Add a paragraph explaining how to reach the Admin Console and an
accompanying screenshot to the Enterprise plan user guide.
* 📚 Update Admin Console access screenshot
* 📚 Open Enterprise plan images in a new tab on click
Wrap the enterprise screenshots in a target="_blank" link so they open
full size, matching the behavior on other user-guide pages. The
organization hierarchy diagram is left inline.
* 📚 Document identity provider configuration for SSO
* 📚 Refine SSO session, Admin Console docs, and screenshots
* 📚 Split Organizations content out of the Enterprise plan page
* 📚 Fix wording in Enterprise plan FAQ answers
---------
Co-authored-by: miryamgduque <miryam.gonzalez@kaleidos.net>
* 🐛 Fix missing text in legacy SVG board thumbnails
Board thumbnails rendered by frame-imposter used React's
renderToStaticMarkup, a synchronous pass with no live DOM to measure
text against. Text shapes without a persisted position-data value
therefore rendered as nothing, so a cached board thumbnail silently
lost its text until the board was hovered, selected, or the canvas
was zoomed past 130%, all of which bypass the cached thumbnail in
favor of live content.
frame-imposter now provides the same is-render? context the
standalone exporter already sets, so text without position-data
falls back to the synchronous foreignObject renderer instead of
rendering nothing.
Thumbnails cached before this fix stay broken until something
regenerates them, so on each page load, board thumbnails containing
text are opportunistically regenerated once per browser (tracked via
local-storage) so existing files self-heal without requiring an
edit.
AI-assisted-by: claude-sonnet-5
* ♻️ Use a transducer in heal-stale-text-thumbnails
Fixes a PR review comment: the frame filtering/mapping was spread
across four separate rx operators. Collapse it into a single
transducer pass over frame-ids, leaving only one rx/map to perform
the mark-healed side effect and build the update-thumbnail action.
The event batch sent to the telemetry server was encoded as a
fressian+zstd base64 blob. Send it as a plain vector of event maps
instead: the JSON encoder handles UUID and temporal types natively,
the payload becomes inspectable, and the receiver schema coerces
values back to proper types.
The receiver (penpot-telemetry) now accepts both the blob and the
plain vector, so it must be deployed before this backend change.
AI-assisted-by: omen-alpha
Document stroke to path, toolbar flyouts, line and arrow tools,
font previews, page multi-select, and dashboard grid/list view.
Relates to #9145, #10403, #10580, #10691
Add the frontend MCP variables PENPOT_MCP_URI and PENPOT_MCP_URI_WS,
the PENPOT_INTERNAL_RESOLVER variable used by nginx to resolve the
internal service names, and the enable-mcp flag to the configuration
guide. All three were used by the frontend docker image but were
missing from the docs.
Closes#11318
AI-assisted-by: claude-opus-5
Signed-off-by: Shreyash Agare <envisiontechdevelopers@gmail.com>
Set storeDir in every pnpm-workspace.yaml: `.pnpm-store` at the repo
root and `../.pnpm-store` in the ten module workspaces, so all of them
resolve to <repo>/.pnpm-store. pnpm resolves the value against the
workspace root, and nested workspaces do not inherit settings, which
had left the root workspace and the modules on two different stores.
Add scripts/clean-node-modules: removes every workspace node_modules
in one pass (ignores external/ and .opencode/), keeps the shared store
unless --store removes it too.
Verified: every workspace resolves the same store path; reinstalls
after a full clean reuse the cache with zero downloads;
frozen-lockfile installs pass in all 11 workspaces with no lockfile
changes; the frontend storybook suite stays green.
AI-assisted-by: omen-alpha
Drop the nested pnpm-workspace.yaml and pnpm-lock.yaml from
plugins/apps/composable-test-suite. It stays a plain member of the
plugins workspace (apps/** glob); its dependencies already resolve
through plugins/pnpm-lock.yaml, so no lockfile change is needed.
Update the pnpm update procedure memory to the single-workspace-per-
module layout, keep the invariant that members carry no nested
pnpm-workspace.yaml, and drop the now-stale 12-workspace count from
critical-info.
AI-assisted-by: omen-alpha
Replace __dirname with import.meta.dirname in the plugins workspace
vite configs (root paths and the plugins-runtime tsconfigPath) and in
frontend/vite.config.js, dropping the fileURLToPath fallback.
Import vite.config.iife with the explicit .ts extension in the
plugin-api-test-suite headless/tests configs and allow it in
tsconfig.node.json (moduleResolution Bundler + noEmit), matching the
mcp/packages/plugin precedent.
Remove .storybook/vitest.setup.ts and its setupFiles entry;
@storybook/addon-vitest 10.3+ provisions preview annotations
automatically.
These clear the Vite `configLoader: 'native'` warnings ahead of that
loader becoming the default.
AI-assisted-by: omen-alpha
Run `corepack use pnpm@next-12` (resolved to 12.3.4) on every directory
with a package.json: the repo root, the 11 module workspaces, and all
submodules. Every packageManager field now carries the same pinned
version and hash; the root and backend move off 11.20.0.
Fix the composable-test-suite workspace config (esbuild allowBuilds
placeholder left by pnpm 12) so its install passes, and add the missing
packageManager fields to frontend/packages/ui and mcp/packages/plugin,
since corepack only updates existing fields.
Document the canonical update procedure in
.serena/memories/workflow/updating-pnpm.md.
AI-assisted-by: omen-alpha
Add a "PENPOT" section to the board size presets list with sizes for
File thumbnail, Template cover, Plugin icon and Plugin cover, so
designers no longer need to look up these dimensions manually when
creating thumbnails for files, template submissions or plugin
listings.
Closes#11561
AI-assisted-by: claude-sonnet-5
Signed-off-by: Andres Gonzalez <andres.gonzalez79@gmail.com>
Two changes to the implement-plan flow:
- It now shows the whole picture before touching the repo: which
issue and branch will be created (or the current branch it
continues on), the execution style, and the task checklist — then
waits for the user's confirmation. Flow detection is read-only;
issue and branch creation moved to the execution step.
- New opt-in execution mode: on request ("step by step"), it runs
one task at a time, commits each one with the create-commit skill,
and waits for the user's review before the next task. The default
stays as it was: every task, one commit.
README and the implement-plan command description updated to match.
AI-assisted-by: omen-alpha
.agents is the shared home that opencode, Claude Code (through the
.claude/skills symlink) and Codex all read, so the skills and the
saved plans now live there instead of .opencode:
- .opencode/skills moved to .agents/skills (24 files, no content
changes).
- .opencode/plans moved to .agents/plans; the .gitignore entry
follows, so plans stay untracked.
- .claude/skills symlink retargeted to ../.agents/skills.
- planner, make-a-plan and review-plan updated to the new plans path;
new .agents/README.md documents every skill with when-to-use
examples and links to each SKILL.md.
- workflow/creating-issues memory: create-issue path updated.
opencode discovers .agents/skills natively, so .opencode needs no
reciprocal link.
AI-assisted-by: omen-alpha
Every skill in .opencode/skills now carries a "When to use" section:
triggers in any phrasing plus the matching /command for the flow
skills, one-line triggers for the utility skills, an
explicit-invocation mirror for ste, and the proactive case for
resolve-git-conflicts.
code-review-criteria drops its old usage bullets ("before merging any
PR ...") for the same role pattern as plan-review-criteria: loaded by
the reviewer subagent of the review-code flow, redirect there to
review code. Flow and criteria skills no longer compete for the same
trigger.
AI-assisted-by: omen-alpha
CLAUDE.md points Claude Code to AGENTS.md as the canonical project
instruction file, and .claude/skills symlinks the opencode skills so
both tools discover the same single source of truth. The root
.gitignore keeps ignoring new files under those paths; the tracked
entries are unaffected from now on.
AI-assisted-by: omen-alpha
Every command in .opencode/commands now only switches to the build
agent, injects the user context, and delegates to a same-named skill
that holds the full flow logic — skills are the primary mechanism for
Claude Code and Codex compatibility.
- make-a-plan, implement-plan, resolve-git-conflicts: flow logic
moved to same-named skills.
- review-plan / review-code: orchestration skills delegating to the
renamed criteria skills plan-review-criteria and
code-review-criteria, with redirecting descriptions so there is no
trigger overlap between flow and criteria.
- create-pr: absorbed the open-pr flow as Open mode (validated
branch-to-PR pipeline using scripts/detect-target-branch) plus a
new Update mode (gh pr edit); /open-pr removed.
AI-assisted-by: omen-alpha
Add a `force` input to Bundles Builder, Docker Images Builder and the
admin-console dispatcher to bypass the existing S3/registry skip
checks and overwrite artifacts unconditionally. Propagate it through
_DEVELOP, _STAGING and _TAG (defaulting to false on non-dispatch
triggers, since schedule/push events carry no inputs).
Add a new _ADHOC workflow to build the full image set (bundle + all
docker images + nitrate admin-console) from an arbitrary ref pair,
for one-off builds of feature branches like test-bameda.
Enrich the build/promote step summaries with the built version
(`git describe --tags --always`), a link to the commit and the build
timestamp. Add run-name to all `_`-prefixed workflows so the target
ref (and, where reliable, the commit sha) is visible directly in the
Actions run list.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Adds the hard rule to AGENTS.md and to mem:critical-info: CHANGES.md
is generated from GitHub milestones during the release process; it
must be updated only via the update-changelog skill flow or on
explicit user request.
AI-assisted-by: omen-alpha
/implement-plan now detects the flow from the current branch instead
of always creating an issue: on a base branch it starts standalone
(issue + branch issue-NNNN from HEAD); on a feature branch it
continues on it with no new scaffolding. Arguments override
detection (standalone, continue, no issue, from origin/<base>);
"no issue" on a base branch creates a plan-<slug> branch. Execution
is direct and the closing suggests /review-code or /open-pr.
/open-pr opens the PR for the current task branch: it detects the
base with scripts/detect-target-branch (canonical: develop, staging,
main), validates commits, issue and remote state, and stops with one
message listing everything missing. It never pushes.
AI-assisted-by: omen-alpha
Add the planning/review command suite for opencode and align the
planner skill with it:
- /make-a-plan (build agent): researches the session subject, drafts
the plan with the planner skill, asks open questions, then saves
the final plan to .opencode/plans/.
- /review-plan: reviews a plan with the plan-review skill; on
approval suggests /implement-plan, on request changes /make-a-plan.
- /review-code: reviews a diff, PR, or code change with the
code-review skill; suggests /make-a-plan for substantive findings.
All commands run as the build agent with explicit read-only rules
that expire when the command's work is done, so follow-up requests
need no agent switching. The planner skill no longer writes the plan
file on its own: it saves only when a command or the user explicitly
instructs it, and it suggests /review-plan and /implement-plan as
next steps.
AI-assisted-by: omen-alpha
Emit clipped `<image href>` for fills with a registered media URL instead
of Skia base64 embeds. Register URLs from the frontend and exporter via
store_image_url. Cover rect, path, frame, and text (glyph clip) cases in
SVG export tests.
Closes#11383
* 🐛 Fix deep-harden of host plugin context on load
ses.harden(context) in loadPlugin deep-freezes every host-owned object
and function reachable through the context. The host keeps needing to
modify those across page navigation (listener wrappers, proxies), so a
later property augmentation (e.g. assigning toString) throws
'TypeError: Cannot assign to read only property toString' and kills the
MCP session (penpot/penpot#11001).
Pass the host context through untouched; sandbox isolation stays at the
compartment boundary (hardened sandbox-owned globals + ses.safeReturn).
Regression test: load-plugin-context.spec.ts (no ses mock).
AI-assisted-by: muse-spark-1.3
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
* 🐛 Add real SES bootstrap to host-context regression test
The previous load-plugin-context.spec.ts had no SES bootstrap, so it
failed on the original code with 'ReferenceError: harden is not defined'
instead of the intended freeze assertion, and passed on the fixed code
merely by avoiding ses.harden.
Now the spec bootstraps real SES (repairIntrinsics + hardenIntrinsics),
adds a control test proving real ses.harden deep-freezes host-owned
functions (Object.isFrozen === true, later toString assignment throws
TypeError - the #11001 crash signature), and keeps the regression test
asserting loadPlugin leaves host functions unfrozen and patchable.
AI-assisted-by: muse-spark-1.3
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
* 🐛 Add production-order hardening contrast evidence
Proves the initialization-ordering hazard behind #11001 (cf. #8636):
in production, index.ts runs repairIntrinsics only at module load while
hardenIntrinsics runs later in createSandbox. The original loadPlugin
called ses.harden(context) between those steps, freezing the shared
Function.prototype with plain data properties so later override taming
is skipped and any subsequent fn.toString assignment throws TypeError.
Kept in a separate spec file so the full SES bootstrap in
load-plugin-context.spec.ts cannot mask the ordering effect.
AI-assisted-by: muse-spark-1.3
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
* 🐛 Apply approved lint fix and CHANGELOG entry
Restores the two approved deliverables missing from the previous push:
the prefer-rest-params fix in load-plugin-harden-order.spec.ts
(replacing the deprecated arguments usage) and the plugins-runtime
CHANGELOG entry for the host-context harden fix (#11001).
AI-assisted-by: muse-spark-1.3
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
* 🐛 Remove deep-hardening of host plugin context on load
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
Co-authored-by: multica-agent <github@multica.ai>
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
* 🐛 Align CHANGELOG and context comment with reviewed evidence
AI-assisted-by: multica-agent
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
Co-authored-by: multica-agent <github@multica.ai>
* 🔥 Remove SES semantic tests from plugin regression coverage
Drop the tests that only verify SES library semantics rather than Penpot
application behavior:
- Delete load-plugin-harden-order.spec.ts (pure SES initialization-order
evidence, never calls loadPlugin).
- Remove the ses.harden control test and its SES bootstrap setup from
load-plugin-context.spec.ts.
- Remove the #8636 hardening-order contrast test and the now-unused ses
import from load-plugin-real-path.spec.ts.
Keep the application-level regression coverage: the real loadPlugin
initialization path, permission enforcement, host-context isolation and
safeReturn protection. No production code changes.
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
AI-assisted-by: Omen Alpha
---------
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>