Shared-key callers (exporter, admin-console) arrive as keywords on
auth-key-id, so transit persisted them as ~:exporter while regular traffic
stored app. Coerce with d/name at the single origin so every audit and
telemetry copy carries a plain string. Adds regression tests for the origin
and the push-audit-events path, including caller spoofing precedence.
Closes#11628
AI-assisted-by: muse-spark-1.3-contributor
Migrate docker/images/Dockerfile.media-processor from ubuntu:26.04 to
dhi.io/node (Debian 13/trixie), which also drops the manual Node tarball
download since the base image ships it. The -dev tag stays as the final
image: fontforge, woff2 and the graphics libraries are needed at runtime.
scripts/build now assembles the release bundle under target/ (dist/ plus
the manifests and a generated setup script), the way the other modules do,
since esbuild leaves the runtime dependencies external. manage.sh gains
build-media-processor-bundle and build-media-processor-docker-image, both
wired into build-bundle and build-docker-images.
The CI workflows are intentionally left untouched: the module is still work
in progress and its images are not published yet, so this only enables
local builds."
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
sha is the full 40-char SHA in build-bundle.yml's output and
build-docker.yml's new checkout-pinning input, matching git and
GitHub's own convention. short_sha stays internal to each workflow
for the S3 keys and image tags that already used it — build-bundle.yml
no longer exposes it externally since nothing outside consumed it.
No S3 key, image tag, or marker value changes anywhere.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
sha is the full 40-char SHA in build-bundle.yml's output and
build-docker.yml's new checkout-pinning input, matching git and
GitHub's own convention. short_sha stays internal to each workflow
for the S3 keys and image tags that already used it — build-bundle.yml
no longer exposes it externally since nothing outside consumed it.
No S3 key, image tag, or marker value changes anywhere.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Add a new memory file documenting the backend audit log system:
purpose, storage schema, RPC producers, frontend ingestion,
webhooks/error-reporter/telemetry consumers, and Nexus archival.
Also wire a reference to it from the backend core memory so it
is discoverable through the memory graph.
AI-assisted-by: longcat-2.0
* 🐛 Align WASM viewer layers during zoom
Keep the absolute WASM layer at the logical viewer size so its canvas CSS
box and hotspot SVG remain aligned as zoom changes.
Add a Playwright regression covering DOM bounds, drawing-buffer sizing, and
hotspot clicks at zoom 1 and below 1.
Closes#11689
AI-assisted-by: GPT-5
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Align WASM zoom regression clicks
Use the rendered WASM DOM selector and design-space points mapped through
canvas bounds for visual clicks.
Wait for the canvas and SVG bounds before checking zoomed-out positions.
AI-assisted-by: GPT-5
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Fix WASM zoom render wait
Use Screen2 coordinates for the visual interaction hotspot.
Wait for non-empty canvas pixels after each viewer render.
AI-assisted-by: GPT-5
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Match WASM render marker in zoom spec
Require the expected frame screenshot marker before reading canvas pixels.
Keep resized buffers blocked until the new frame draws.
AI-assisted-by: GPT-5
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* ✨ Run WASM zoom regression in DPR project
Move viewer zoom coverage under the render-wasm Playwright project.
Assert DPR-scaled buffers and use the stable Zoom out role.
AI-assisted-by: GPT-5
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Simplify WASM render wait
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Remove unreliable WASM viewer regression
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
---------
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
_DEVELOP and _STAGING chain build-bundle.yml -> build-docker.yml,
both invoked with gh_ref as a branch name. Each reusable workflow
re-resolves that branch with its own checkout, at its own time.
build-docker's checkout happens minutes after build-bundle's, after
new commits can have landed on the branch, so it can compute a
different sha than the one build-bundle actually bundled and
uploaded to S3.
The Prepare job then requests a key that was never uploaded:
fatal error: An error occurred (404) when calling the HeadObject
operation: Key "penpot-sha-<newer-sha>.zip" does not exist
Expose build-bundle.yml's resolved sha as a workflow_call output and
thread it through _DEVELOP, _STAGING and _ADHOC as build-docker.yml's
new optional sha input, which pins its checkout instead of
re-resolving gh_ref. gh_ref keeps naming the branch tag to move.
_TAG is unaffected: it builds from a pushed git tag, which cannot
move the way a branch can, so build-bundle and build-docker already
agree on one commit there.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
dhi.io/node:24.20.0-debian13-dev does not pre-create /usr/local/bin
the way a regular Debian image does, so tar -xzf ... -C /usr/local/bin
failed with "Cannot open: No such file or directory" right after the
checksum check passed. Introduced in #11790 when pnpm moved from
Corepack to a downloaded standalone binary.
Dockerfile.media-processor and docker/devenv/Dockerfile are unaffected:
both extract into /opt/node/bin, which already exists from the prior
Node.js install step.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
_DEVELOP and _STAGING chain build-bundle.yml -> build-docker.yml,
both invoked with gh_ref as a branch name. Each reusable workflow
re-resolves that branch with its own checkout, at its own time.
build-docker's checkout happens minutes after build-bundle's, after
new commits can have landed on the branch, so it can compute a
different sha than the one build-bundle actually bundled and
uploaded to S3.
The Prepare job then requests a key that was never uploaded:
fatal error: An error occurred (404) when calling the HeadObject
operation: Key "penpot-sha-<newer-sha>.zip" does not exist
Expose build-bundle.yml's resolved sha as a workflow_call output and
thread it through _DEVELOP, _STAGING and _ADHOC as build-docker.yml's
new optional sha input, which pins its checkout instead of
re-resolving gh_ref. gh_ref keeps naming the branch tag to move.
_TAG is unaffected: it builds from a pushed git tag, which cannot
move the way a branch can, so build-bundle and build-docker already
agree on one commit there.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Mirrors the devenv change in 3385a65 (docker/devenv/docker-compose.infra.yml).
Mailpit keeps messages in memory only (no persistence), consistent with
mailcatcher's previous behavior; the volume and MP_DATABASE env var are
left commented for anyone who wants to opt in later. UI port changed from
1080 to Mailpit's native 8025.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
The seven creation commands no longer accept an optional client
id: create-file, create-project, create-team,
create-team-with-invitations, upload-file-media-object,
create-file-media-object-from-url and assemble-file-media-object.
The server always generates the identifier; a sent id is ignored.
Malli maps are open and the RPC layer never strips unknown params,
so the handlers that would still honor an id (create-file,
create-project) now drop it explicitly. Internal callers that pass
remapped ids (project duplicate, binfile import) keep working.
Closes#11783
AI-assisted-by: muse-spark-1.3-contributor
dhi.io/node:24.20.0-debian13-dev does not pre-create /usr/local/bin
the way a regular Debian image does, so tar -xzf ... -C /usr/local/bin
failed with "Cannot open: No such file or directory" right after the
checksum check passed. Introduced in #11790 when pnpm moved from
Corepack to a downloaded standalone binary.
Dockerfile.media-processor and docker/devenv/Dockerfile are unaffected:
both extract into /opt/node/bin, which already exists from the prior
Node.js install step.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
* 🐛 Bound decompressed size of JSON entries on binfile v3 import
Every JSON/text zip entry (manifest, files, pages, shapes, colors, components, typographies, tokens, plugin-data) was decompressed without any size limit, letting a small .penpot archive exhaust the backend heap (GHSA-qcw7-v626-g6cf). Only binary storage blobs were guarded. Reuse the existing size-limiting-stream guard on the text path: 20 MiB cap per entry, 200 MiB cumulative budget per import job, plus a cheap declared-size pre-check. Both limits are configurable and wired through the binfile, management and debug entry points. Adds zip-bomb regression tests for the file entry, the synchronous manifest read and the cumulative budget. Closes#11606
AI-assisted-by: muse-spark-1.3-contributor
* ♻️ Uniform binfile import limits behind init-limits
Move the binfile import limits to a single source of truth in app.binfile.common (default-* vars) and drop the duplicated entries from config/default; env overrides keep working through the schema. Resolve all limits once per job with init-limits (::max-size, ::total-max, ::current-size, ::max-object-size, ::max-zip-entries) instead of rebuilding the map per zip entry. Thread cfg as the first arg through the v3 readers, collapse read-plain-entry into read-entry, and give size-limiting-stream a single explicit-counter arity. v1 keeps using the compiled default (mechanical rename only). No behavior change. AI-assisted-by: muse-spark-1.3-contributor
* ♻️ Rename binfile limits to text-entry/binary-entry terms
Use text-entry/binary-entry vocabulary consistently across config keys, bfc input keys, default-* vars and the limits resolved by setup-limits (::max-text-entry-size, ::max-text-total-size, ::current-text-size, ::max-binary-entry-size). Rename init-limits to setup-limits. No behavior change. AI-assisted-by: muse-spark-1.3-contributor
* ♻️ Rename cumulative text counter and document binary limit
Rename ::current-text-size to ::accumulated-total-text-size for clarity and expand the default-max-binary-entry-size comment to match the other limit vars. No behavior change. AI-assisted-by: muse-spark-1.3-contributor
* ♻️ Harden binfile guards and prove budget accumulation
Add a regression test that only passes when text bytes accumulate across entries (budget between largest entry and summed total; verified red against a per-entry atom). Include the entry name in streaming-guard errors, count skipped bytes against the budget with a direct unit test, and forward all four limit keys in get-manifest. No behavior change. AI-assisted-by: muse-spark-1.3-contributor
* ♻️ Thread cfg through get-manifest
get-manifest now takes the caller cfg and resolves limits with setup-limits like the import job itself, instead of building a single-use mini-cfg from cf/get. No behavior change. AI-assisted-by: muse-spark-1.3-contributor
Lock in the fix from 31b73460c3 (#11803) with a regression
test for the exact reported scenario: loading the viewer
with a URL that already contains `zoom=fill`.
At 2.18.0-RC5 `update-zoom-querystring` navigated without
any comparison, so the load sequence bundle-fetched →
zoom-to-fill → update-zoom-querystring → nav → navigated
re-ran forever and crashed the page with React error #185
("maximum update depth exceeded"). The guard added in
31b73460c3 breaks the cycle; the new test asserts that a
bundle fetch against a `zoom=fill` route emits no
navigation events.
Also updates the dashboard/viewer frontend memory to
document the guard and the loop it prevents.
AI-assisted-by: glm-5.3-flash
* 🐛 Trim linked-library data in view-only bundle for share links
The anonymous get-view-only-bundle RPC merged each linked library whole,
exposing library pages the share link never granted. For share-link
permissions, each library is now reduced to the narrow data keys with
its own pages dropped and only the components referenced by the allowed
pages kept (nested references followed); membership bundles are
unchanged.
Closes#11617
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Resolve nested library components via main instance in bundle trim
Stored components carry no objects, so the transitive walk missed nested
components. Follow references through the main-instance subtree instead,
share the narrow data keys between the primary and library scopes, and
cover component filtering with a real-instance RPC test.
Review follow-ups F1-F3 for #11617
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Address review findings on view-only library trim
Hoist invariant refs out of the fixpoint, make the cross-library
main-instance fallback deterministic, pin the trimmed envelope,
and add RPC tests for disallowed-page isolation and
cross-library nesting.
Follow-ups to #11617
AI-assisted-by: muse-spark-1.3-contributor
* ♻️ Index libraries by id with d/index-by
Replace the manual into/juxt index with the shared helper.
No behavior change.
AI-assisted-by: muse-spark-1.3-contributor
* 🌐 Complete Catalan translations in frontend
Complete the Catalan (ca.po) locale to 100% coverage against en.po,
using es.po as support reference. Adds the 1439 missing entries
across workspace, dashboard, labels, shortcuts, subscription,
errors, modals and onboarding, keeping vosaltres treatment and
IEC/Termcat terminology consistent with the existing strings.
Normalizes placeholders and plural forms, drops the 14 stale
obsolete entries and canonicalizes the file with the repo
translations script.
Closes#11739
AI-assisted-by: muse-spark-1.3-contributor
* 📚 Add frontend translations memory with Catalan criteria
Record the PO workflow, the sync fuzzy-flag gotcha and the
Catalan glossary and tone agreed upon while completing ca.po,
and link the new memory from the frontend core routing.
AI-assisted-by: muse-spark-1.3-contributor
* 🔧 Add gettext to devenv image
Provide msgfmt and msgattrib in the dev environment for
checking PO translation files.
AI-assisted-by: muse-spark-1.3-contributor
* 🌐 Fix Catalan translations and add PO checker
Review of the missing-whitespace pattern found ~90 glued words
across 75 entries, plus 4 lost plural forms and 2 placeholder
mismatches verified against tr call sites. All fixed in ca.po.
Adds frontend/scripts/check-translations.js (vocabulary-free PO
QA: glued words, punctuation, placeholders, plurals) with
--self-test, wired as pnpm run check-translations and
documented in mem:frontend/translations.
AI-assisted-by: muse-spark-1.3-contributor
* 🌐 Multi-locale PO checker with word catalogs
Split the checker engine from its word lists: ca/es catalogs now
live in scripts/check-translations/words.<locale>.txt and all
messages are in English. Adds an es seed (calibrated to zero
errors) and fixes 7 typos it found in es.po. Universal checks
(placeholders, plurals, punctuation) run without a catalog.
AI-assisted-by: muse-spark-1.3-contributor
* 🌐 Merge PO checker into translations.js
Fold check-translations.js into translations.js as a check
subcommand reusing its locale helpers; word lists stay in
scripts/check-translations/words.<locale>.txt. Also fixes the
getopts stopEarly bug that made -l useless after the command
(sync -l ca synced every locale), drops dead lodash import
and code, unifies help and exit codes. Removes the
check-translations package alias; use translations.js
check -l <locale> with explicit -l.
AI-assisted-by: muse-spark-1.3-contributor
* 🌐 Keep unused placeholders out of the gate
Reverts the %s-stripping on unused auth.terms-privacy-agreement:
the links mirror its markdown sibling and a reactivation may
need them. Placeholder mismatches on #, unused keys now warn
instead of failing, and the rule is recorded in
mem:frontend/translations.
AI-assisted-by: muse-spark-1.3-contributor
* ✨ Enable closed schemas for RPC methods
* 🐛 Fix duplicate make-dummy-request test helper definition
The branch added a variadic DummyRequest/make-dummy-request pair but
left the pre-existing single-arg definition in place. Because it was
loaded last, zero-arg (make-dummy-request) calls added by
prepare-rpc-params and rpc-nitrate-test threw ArityException, which
broke 384 tests and caused 14 downstream assertion failures.
Remove the stale duplicate so the variadic definition is the only
one, and drop the now-unused yrq alias and duplicate yres alias.
AI-assisted-by: deepseek-v4.1-flash
* ✨ Add focused tests for make-dummy-request helper
Pin the call contract of make-dummy-request, which the suite uses
in three styles: no arguments, a single options map, and keyword
arguments. The helper's redefinition shadowing in 8ca95adb98 was
only caught by a full-suite run with hundreds of unrelated errors;
these tests fail locally in a focused --focus run.
Cover the zero-arg defaults, map and keyword overrides, the
:body-bytes -> ByteArrayInputStream wrapping, :body-stream
precedence, and cookie readback. Also clarify the docstring to
list all supported call styles.
AI-assisted-by: deepseek-v4.1-flash
* 🚑 Prevent RPC client params from overriding auth context
Strip qualified keys from decoded request params before merging
them with the server-built auth context, so transit bodies can
no longer override ::profile-id, ::auth-type or ::token-perms.
Adds a regression test proving the override and the fix.
AI-assisted-by: muse-spark-1.3-contributor
* 📚 Merge backend subtleties memories under generic name
Rename rpc-db-worker-subtleties to subtleties and fold in
http-storage-filedata-subtleties, so the name no longer
enumerates topics. Update all mem: references accordingly.
AI-assisted-by: muse-spark-1.3-contributor
* ✨ Add realistic tests for RPC auth override
Cover the transit wire vector and the real wrapped :get-profile
method with two database profiles, proving a session cannot read
another profile by smuggling :app.rpc/profile-id in the body.
AI-assisted-by: muse-spark-1.3-contributor
* ✨ Add e2e test for RPC auth context override
Parametrize rpcPost with contentType, accept and query so e2e
can send hand-written transit bodies without new dependencies.
The new test proves a transit-smuggled :app.rpc/profile-id no
longer overrides the session in get-profile. Also fix the demo
email assertion in auth-flow to the current uuid format.
AI-assisted-by: muse-spark-1.3-contributor
The unauthenticated REPL server started in --multi-user mode whenever DEVENV or REPL_ENABLE was set, unlike developer tools which are already blocked there. Gate its creation behind shouldStartReplServer, mirroring the developer tools policy, so it never starts with --multi-user. Give it a dedicated PENPOT_MCP_REPL_HOST knob defaulting to localhost so it no longer inherits the main server 0.0.0.0 bind in Docker, keep the devenv script on 0.0.0.0, and document and test the new behavior. Closes#11631 AI-assisted-by: muse-spark-1.3-contributor
The plugin WebSocket bridge ignored PENPOT_MCP_SERVER_HOST and bound all interfaces,
exposing unauthenticated task dispatch to the network in single-user mode. Pass mcpServer.host
into WebSocketServer, mirroring the ReplServer fix, so the bridge binds localhost by default and
0.0.0.0 only on explicit opt-in.
Closes#11603.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Restore the missing handler when aligning a one-handler node
A curve node with one of its handlers removed could not be switched
to aligned or equal: the collapsed handler stayed on the node, or the
line on the other side stayed a line, so nothing happened. Switching
to equal could even collapse the remaining handler.
Such a node now gets a mirrored handler on the other side, turning
that line into a curve when needed, also across the start node of a
closed path. Removing a handler also resets the node to independent.
Closes#11703
AI-assisted-by: claude-opus-5
Signed-off-by: Shreyash Agare <264953665+ShreyashAgare26@users.noreply.github.com>
* 🐛 Add some quality improvements
---------
Signed-off-by: Shreyash Agare <264953665+ShreyashAgare26@users.noreply.github.com>
Co-authored-by: Shreyash Agare <264953665+ShreyashAgare26@users.noreply.github.com>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
Subscribe-file and subscribe-team handlers now verify the requesting
profile has read permissions on the target resource before creating
a subscription. Pointer-update handler now validates that the message
file-id matches the subscribed file-id before publishing.
Closes#11067
AI-assisted-by: mimo-v2.5-pro
Snapshot only the scaled shadow bounds instead of the full reusable
filter surface. This avoids retaining viewport-sized textures for each
small shadow while keeping cached pixels valid when the surface is reused.
Use the bounded snapshot for both shape and frame shadow caches. Add
regression tests for rounding, scaling, surface limits, and snapshot reuse.
AI-assisted-by: gpt-6
* ⚡ Build the frontend bundle once for all E2E suites
Merge tests-integration, tests-composable-suite and tests-plugin-api-suite
into one "CI: E2E" workflow. Each of the three ran its own full
frontend/scripts/build on every PR, so one PR paid the build three times.
The new build-bundle job restores actions/cache key frontend-bundle-<sha>,
runs frontend/scripts/build only on a miss and saves the key before the
job ends. The integration shards, the composable suite and the mocked
Plugin API suite now all need build-bundle and restore the same key with
fail-on-cache-miss, so none of them builds. A workflow re-run of the same
SHA reuses the cached bundle instead of rebuilding it.
Triggers become the union of the previous paths (frontend, common,
render-wasm, plugins): the bundle embeds the built plugins, so a plugins
change runs the whole set. workflow_dispatch keeps running the
integration job only, as before.
Job names are kept identical on purpose: they are the GitHub check
contexts and branch protection may match them by name.
Docs: new mem:frontend/e2e-ci-workflow records the build-once contract,
referenced from mem:frontend/core and mem:frontend/testing; the composable
memory and both suite READMEs are updated.
AI-assisted-by: deepseek-v4.1-flash
* 🐛 Fix mocked plugin suites crashing without frontend deps
The mocked CI drivers shelled out to frontend/scripts/e2e-server.js,
which imports express from frontend/node_modules. CI jobs install
only plugins/ deps, so the import failed with ERR_MODULE_NOT_FOUND
and the run timed out waiting for localhost:3000.
Serve the prebuilt bundle with a zero-dependency static server
built into each driver (ci/static-server.ts, kept in sync in both
suites) plus node:test coverage for it.
AI-assisted-by: muse-spark-1.3-contributor
* ⬆️ Update devenv dependencies
Update Node.js, OpenCode, clj-kondo, Babashka, Pixi, GitHub CLI, uv,
and Serena to their current stable releases.
AI-assisted-by: gpt-5.6-sol
* ⬆️ Update devenv to Java 27
Use Zulu JDK 27 in the development image for compatibility testing.
Update the official checksums for both supported architectures.
AI-assisted-by: gpt-5.6-sol
* 🐳 Replace MinIO with RustFS in devenv
Run RustFS as the development S3 service and wait for its health check.
Install a pinned AWS CLI with checksums and use it to create the bucket
idempotently from each backend entry point.
Keep the old MinIO volume untouched and use a new RustFS volume.
AI-assisted-by: gpt-5.6-sol
* 🐳 Replace MailCatcher with persistent Mailpit
Run Mailpit as the devenv SMTP sink while preserving mailer:1025 and the
localhost:1080 UI.
Store its SQLite inbox in a named volume and wait for the readiness
endpoint before starting runtime containers. Bind the web UI to loopback so
development emails stay local.
AI-assisted-by: gpt-5.6-sol
* ⬆️ Update Node.js to 24.21.0
Align the host NVM version with the Node.js version used by devenv.
AI-assisted-by: gpt-5.6-sol
* ⬆️ Update devenv to PostgreSQL 18.6
Run PostgreSQL 18 with its versioned volume layout and a TCP readiness
check that ignores the temporary initialization server.
Install the matching client, create penpot_nexus, and preserve the old
PostgreSQL 16 volume for rollback or logical migration.
AI-assisted-by: gpt-5.6-sol
* 🐳 Expose RustFS ports in devenv
Publish the RustFS S3 API and management console on localhost port 9000
and 9001.
Keep both bindings on loopback so object storage is not exposed to the local
network.
AI-assisted-by: gpt-5.6-sol
* 🐳 Install standalone pnpm in devenv
Install pnpm 12.5.0 from architecture-specific release archives and
verify their published checksums.
Remove the Corepack setup while allowing pnpm to honor the project
packageManager pins.
AI-assisted-by: gpt-5.6-sol
* 🔥 Remove corepack, use system pnpm everywhere
Corepack is gone from Node 25+, so every `corepack enable` call
fails. pnpm now ships as a system binary (devenv, CI runners and
Docker images install it directly) and auto-downloads the version
pinned in `packageManager` on mismatch.
Scripts, workflows and Dockerfiles call `pnpm` straight away; the
three deploy workflows use a single `pnpm/setup@v2` step; and the
new `scripts/sync-pnpm-version` stamps all 35 `packageManager`
fields from the system pnpm, replacing the `corepack use` sweep.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Fix exporter watch missing render-wasm build step
The exporter watch compiled CLJS requiring the generated
src/app/wasm/shared.js, which only render-wasm/build export
produces. Without it shadow-cljs failed with a cryptic missing
./shared.js dependency. Run build:wasm before watching, as
the frontend watch:app and exporter scripts/build already do.
AI-assisted-by: muse-spark-1.3-contributor
* 🔧 Add opencode V2 support and adapt plugins
Register the penpot tools for both opencode V1 (server())
and V2 (setup() with JSON Schema inputs) from a single
dependency-free plugin file, sharing the psql and
paren-repair runners between both paths.
Install the opencode2 binary side-by-side with V1 in the
devenv image and document the dual registration in the
paren-repair and psql memories.
AI-assisted-by: muse-spark-1.3-contributor
* ⬆️ Update pnpm and opencode
* ✨ Restrict optional RPC ids to user-provided UUIDs
Add ::sm/user-provided-uuid, backed by a version and variant
aware regex that only accepts v4, v7 and v8 instances. Use it
for the optional :id of the creation RPC commands so reserved
versions such as v3 are rejected at validation time. Reads
such as get-team keep the lax ::sm/uuid. Cover the predicate
and the schema on both JVM and JS runtimes.
AI-assisted-by: muse-spark-1.3-contributor
* ✨ Cover id version restriction at the RPC boundary
Add backend regression tests proving the seven creation commands
reject reserved-version ids (v3) with :params-validation and
accept v4 ids (plus v7/v8 on create-team) through the real
decode and validate path. Also drop two duplicated assertions
and document the version and variant of every fixture UUID in
user-provided-test.
AI-assisted-by: muse-spark-1.3-contributor
* ♻️ Keep agent content on standard paths, drop client shims
Agent guidance stays where the ecosystem reads it: AGENTS.md at the
root and the skills in .agents/skills. Codex scans .agents/skills and
follows symlinks, opencode lists it among its discovery locations, and
Cursor, Zed, Warp, Cline and Amp use the same project path.
Claude Code reads neither, so it needed CLAUDE.md and .claude/skills.
Committing those two paths is not a neutral default. Both are
gitignored, git treats ignored files as expendable, and a checkout
that needs the path deletes whatever a contributor keeps there without
a warning. Neither sparse-checkout nor skip-worktree offers a way out,
because both make the next pull abort. The two links are now created
by the contributor, once per clone, and documented in
.agents/README.md.
AGENTS.local.md and .agents/local/ give every client one personal
layer that composes on top of the shared one, instead of one file per
client inside the repository.
sync-workspace seeds the two links for ws1 and above, the way it
already seeds the gitignored config.js, so the devenv workflow does
not change.
AI-assisted-by: claude-opus-5
Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
* ♻️ Lean on Claude Code reading AGENTS.md, drop the pointer
Claude Code 2.1.277 reads AGENTS.md through the built-in agents-md
plugin, whose default mode loads it in a project that has no
instruction file of its own. Deleting the committed CLAUDE.md is what
turns that on here, so the pointer a Claude user had to create is no
longer part of the setup, and only the .claude/skills link remains:
the plugin covers instruction files, and skills still load from that
path alone.
The instruction that replaces the pointer is a removal. A CLAUDE.md,
.claude/CLAUDE.md or CLAUDE.local.md anywhere from the root down to
the working directory hands the project back to CLAUDE.md and hides
this repository's AGENTS.md, while .claude/rules/*.md and the
person's own ~/.claude/CLAUDE.md do not count and stay usable for
personal steering. CLAUDE.local.md joins the ignore list, and a hard
rule in AGENTS.md forbids all three.
The docs name the version, say the fallback has not reached Bedrock,
Vertex or Foundry, and say the stable auto-update channel runs about
a week behind the default one, so the symlink is documented as the
stopgap for those cases and as something to drop afterwards.
sync-workspace seeds the skills link alone for the same reason.
AI-assisted-by: claude-opus-5
Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
* 📎 Remove skills directory mention on AGENTS.md
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
---------
Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>