EYAS v0.8.15-beta — Designs your agents follow
A design stops being a picture you keep somewhere else and becomes something the
work follows. Multi-artboard canvases in the Claude Design format, rendered by
EYAS's own MIT runtime: create one, import one, edit it by hand or on the canvas
or by asking, version every change through a single validator, attach it to a
conversation or a project, and export it to PNG and PDF.
Getting an agent to actually use one took longer than building it, and that is
most of what follows. The tool inventory was clipped to 15% of itself. A matched
skill emptied the tool list. Nothing that was ever written to memory could reach
a prompt, and nothing wrote to memory either. Each was invisible on its own, and
together they were why a design sat attached to a conversation and changed
nothing.
Design (F2)
- A "Design" menu item. Multi-artboard canvases on a pan-and-zoom surface, in
the Claude Design container format:<Name>.dc.htmlartboards, acanvas.json
layout manifest with pages and sticky notes, and images stored as bare base64
under their filename. A canvas exported here re-seeds there, and one published
there imports and renders here. - EYAS's own runtime. The hosting platform's editor is a ~2.4 MB precompiled
payload under a licence this repository cannot redistribute, so the Design
Components dialect is implemented from scratch as MIT code: dotted-path holes,
<sc-for>,<sc-if>,<dc-import>, JSX-camelCase event binding, and real
execution of the artboard'sclass Component extends DCLogic— so clickable
prototypes, variant switches and selection state work. - The isolation that makes executing AI-authored JavaScript acceptable: a
srcdociframe withsandbox="allow-scripts"and neverallow-same-origin, a
CSP inside the srcdoc withconnect-src 'none', and Google Fonts as the only
external origin. No route serves an artboard as a document — the render endpoint
returns the srcdoc and the sandbox value in one JSON payload so they cannot
drift apart. The runtime moves<helmet>content into<head>but drops any
<script>there. - A validator gate on every write. Hand edit, import or AI result, all of it
is checked before it can become a version: an artboard with no<x-dc>root, a
layout entry naming a file that is not there, an image reference with nothing
behind it, a case-insensitive artboard-stem collision, alaunchpointing at
nothing, a stray top-level key incanvas.json, and the}} ?ternary inside a
style attribute that the format drops silently. A rejected edit leaves the
previous version byte-identical. - One AI pipeline, not one per vendor. The same prompt and the same gate
whatever the provider; only the executor tier varies — whole-canvas rewrite for
small canvases, per-artboard iteration for large ones, both on plain text
completion so a local model works too. A failed attempt is retried once with the
validator's own output as the feedback. - Agents get
design_list,design_read,design_writeanddesign_create,
allcategory: 'custom'so they survive the MCP bridge and exist on the CLI
providers. A design linked to a conversation travels with every turn as a
design-contextsection; a large canvas is summarised and the agent fetches what
it needs. - Import from a published canvas page, export as raw files, as a portable canvas
document, or as a standalone HTML page that opens and prints anywhere.
WYSIWYG (F4)
- Click an element, change it in a panel, and it lands in the source.
Typography, colour, box, border, radius and layout, including grid tracks that
round-trip throughrepeat(N, minmax(0, 1fr)). Text is editable in place
unless it is bound to a{{hole}}, which the panel says rather than silently
overwriting the binding. - The design that this forced. The artboard iframe has no
allow-same-origin, so the app cannot reach its DOM. Rather than parse and
mutate the template in the app — which would need a server-side DOM and a
rendered-node-to-source mapping — the runtime owns the mutation: it stamps
every template element with a stable index at parse time, applies the edit to
its own copy, re-serialises, and posts the finished template back. The app
splices it into the.dc.htmlfile with the head marker, helmet and logic
script preserved byte-for-byte. - Style edits keep
{{holes}}in declarations they did not touch. The patch
works on the style attribute as text, declaration by declaration; a DOM style
API would have destroyed the binding silently. - The splice refuses anything that does not read back as what was written.
Checking that the result merely parses is too weak: a</x-dc>inside a
template closes the element early, and the file still parses — into a
truncated artboard. - Messages are attributed to the artboard's own frame before they are acted
on, and validated against a strict shape. They come from an opaque origin and
are exactly as untrusted as the artboard. - Tweak chips from
data-propsre-render live; pinning one writes it back as
the artboard's declared default. - Undo/redo per artboard with Cmd/Ctrl+Z, and one version per explicit save
rather than one per keystroke. - The runtime defaults to interact, not edit: the canvas shows working
prototypes, and an artboard markedis_interactivenever enters edit mode.
Print, PDF and PNG (F5)
- A design canvas exports as PNG and PDF. One artboard at 1× or 2×, one
artboard as a PDF at its own natural size, or the whole canvas as a single
multi-page PDF.print: 'fixed'artboards come out as one page at exactly
their frame — a CSS pixel is 1/96 inch, so the size passes through without a
conversion;print: 'flow'artboards paginate onto A4 or Letter. - The browser fact that shaped it: Chromium will not paginate inside an
iframe. It lays a frame out as a fixed box and clips the overflow, so a
flowing artboard printed in the preview's sandboxed iframe would come out as
one truncated page. Every artboard is therefore rendered as its own top-level
document, and a canvas PDF is those PDFs concatenated withpdf-lib. That is
the better answer anyway: each page keeps its natural size, a flowing report
still paginates, and one artboard's<helmet>CSS cannot leak into the next. - Losing the sandbox attribute meant replacing it with three things. Every
print page opens in a throwaway browser context with no cookies and an opaque
origin; every request is aborted in the browser process except the two Google
Fonts origins the format admits; and the page carries the sameARTBOARD_CSP
as the preview, imported rather than re-typed so the two cannot drift. - A broken artboard is refused, not exported blank. Both failure layers are
checked — the mount throwing, and the runtime's own marker when a component
constructor orrenderVals()throws. A PDF whose only content is
"renderVals() threw: …" is worse than an error message. - The browser is optional and says so.
playwright-coreis a real
dependency (Apache-2.0, no postinstall, no runtime dependencies of its own);
the ~150 MB browser binary is not. It is resolved fromEYAS_CHROMIUM_PATH,
then Playwright's own registry, then known system paths, and when there is
none/api/v1/designs/print-statusanswersavailable: falsewith the remedy
and the UI disables the buttons. The Docker image installs Chromium and the
fonts a headless browser needs; deleting that layer costs ~350 MB and switches
these two features off cleanly. - The Chromium sandbox is never disabled automatically. A sandbox failure
does not fall back to--no-sandbox: the renderer is the process that
executes AI-authored artboard JavaScript, and turning a deployment problem
into a silent security downgrade there is not a trade-off worth making
quietly. It takes an explicitEYAS_CHROMIUM_NO_SANDBOX=1, and the error
message says so. playwrightis gone as a shimmed optional module. The browser tools and
the print pipeline now share one resolver, so there is a single place that
knows how to find a Chromium, and the SSRF predicates moved to
shared/net-guardwhere the headless browser can apply them per request.- New dependencies:
playwright-core(Apache-2.0) andpdf-lib(MIT). The
latter is not in the design spec's dependency list — it was added because
concatenating per-artboard PDFs is what makes a mixed canvas correct instead
of compromised onto one uniform paper.
Canvas usability, and one field taken back out
- The canvas takes a scroll wheel. Plain scroll pans, Shift scrolls
sideways, Ctrl/⌘ + scroll zooms — anchored on the pointer, so the thing under
the cursor stays under the cursor. The listener is attached natively with
passive: false, because React routesonWheelthrough a passive root
listener wherepreventDefault()is ignored and Ctrl+wheel zooms the browser
instead. - An artboard can be opened on its own. A control on its title row (or a
double-click on the title) fits it to the viewport; Esc returns to the
previous view. This is what finally makesartboardEntry.expanddo something:
fitshrinks the whole artboard to the viewport,fillwidens the frame to
the viewport at natural scale and lets it scroll. - "Fit" now fits. It measures the page's actual bounding box — artboards and
annotations — instead of resetting to a hardcoded 60% at 40,40. - Nothing was put over the frames. An overlay would make the entire surface
pannable, but it would also silence everyis_interactiveprototype until you
clicked into it. Dragging the background works, so opening an artboard is an
explicit control rather than a gesture over a frame. - A design can be renamed in place from its header.
designs.statusis gone. It rendered a badge in the list and did nothing
else: it gated nothing, no UI ever called its filter, and it could not be
changed from anywhere. A declared surface with no consumer — removed rather
than given a job it did not need. Existing installs keep the column inert
(NOT NULL DEFAULT 'draft', so an INSERT that omits it still works); a table
rebuild is not worth it, and a test pins that the old shape still works.
Deleting a design, and watching a long AI edit
- A design can be deleted from the interface. The capability had been in
the API since the canvas shipped, with no button anywhere — the manual said
so out loud. The bin sits in the detail header beside rename, not on the list
cards: those are links, and a destructive control inside a navigation target
is a misclick waiting to happen. - The confirmation names what goes with it — saved versions, and every
conversation or project the design is attached to. The attachment count is
the part nobody can see from the design's own page, soGET /designs/:idnow
carries alinkssummary next to the design. Asking blind is the worse
version of asking. - A nine-minute AI edit no longer looks like nothing happening. A measured
edit on a CLI provider took 8 min 43 s behind a bare spinner. The panel now
counts the elapsed time, says that a large canvas can take minutes, and the
AI button in the header spins while a run is open — so it is visible without
opening the panel. - A failed AI edit keeps its reason. Every attempt is recorded before the
model is asked and closed on every exit, including the throw, so a reload, a
dropped connection or a proxy timing the request out no longer destroys the
answer: the server finishes, the row records it, the panel reads it back. The
request itself stays synchronous — turning it into a job id would only
shorten the HTTP hold, at the cost of the existing API and the candidate
preview path. - A restart is not a model failure. A run orphaned by a dead process is
closed asinterruptedwith its own message when the module registers,
rather than spinning forever or being reported as something the AI got wrong. - Two clocks again, handled at the source. "How long has this been running"
subtracts a server timestamp from a browser one. The runs response carries the
server's ownnow, the frontend derives the skew from it, and the elapsed
figure is measured on one clock. The columns are epoch milliseconds rather
than this module's usual ISO text, becausedatetime('now')produces a string
new Date()reads as local time. - A second AI edit cannot start while one is running on the same canvas.
Attaching a design where it is actually used
- A design can be attached to a conversation from the UI — an icon in the
top bar with a count, not a field: the bar is full, and this is an occasional
act. - And to a project, which every conversation created in it then inherits.
- A project's designs are COPIED onto a conversation created in it, the same
wayindexedSourcesandworkingDirectoriesalready are — set on the
project, and a new conversation starts with them; not set, and it does not.
The conversation owns them from then on and can detach any one, and nothing
resolves the project again at read time. The copy is additive and idempotent,
so it never removes a design somebody attached on purpose. design_linkanddesign_unlinktools, so an agent can attach the canvas
it just made. They default to the run's own conversation from the tool
context, never a model-supplied id, andscopeis a closed namespace —
conversationorproject— so a model cannot file a link somewhere nothing
reads it.- Background runs see attached designs.
buildDesignContextwas wired into
interactive chat only, which meant a scheduled run worked blind on the very
design it was supposed to be working from.
What an agent sees of an attached design
- A design announces itself; it does not hand its contents over. The block
says a design is attached and what KIND of data each of its parts holds —
tokens, typography, components, patterns — and names the two calls that fetch
them. On the shipped Odoo canvas that is 652 characters against a 46 763
character design, and it stays flat: twenty artboards fold into the same
handful of role lines as two. design_readtakes apart. It used to return either one whole artboard
or the entire canvas, so "read only what you need" meant reading 10 KB to
find five hex codes. A part returns the derived values for one role and
nothing from another, off the same derivation the announcement is built from
— the two cannot disagree about what exists.- Nothing is inlined, at any size. An earlier version put the palette in the
block, and before that the whole canvas when it fit. Both were paid on EVERY
turn; a fetch is paid once. At two turns the fetch already wins, and only the
fetch stops the cost growing with the canvas. - The block instructs rather than announces. It says to follow the design
and not to invent styling, instead of merely noting that one is attached. - The index is derived, never stored. A stored copy would be a second thing
to keep in sync, would travel in exports, and would confuse the Claude Design
interop. - The design prompt teaches the same role vocabulary, so newly generated
canvases name their artboards accordingly — the structure is the design's own,
not a classification imposed afterwards. Artboards are classified by file name
and title, never by body text: a body that mentions "pattern" proves nothing. - Two regex traps found on the first real design: JavaScript's
\bis
ASCII-only, so\bűrlapnever matches "Űrlapelemek"; and Hungarian lengthens
the stem vowel in the plural, so "minta" is not a prefix of "minták".
Foundation (F0)
- One assembly path.
assembleSystemPromptis now the single fail-soft "assemble and flatten"
helper, andexecuteAgentpluschannel-run-agentgo through it. Delegated subagents and
channel replies get the project cascade and the workspace files they were silently missing. The
agent definition's own prompt is appended, never replaced, so nothing that worked stops working. - A missing
SOUL.style.jsonno longer empties the prompt. The active-voice resolver ran inside
the assembler'sPromise.all; one agent without that file madebuildForPrimarythrow and the
interactive path sendsystem: ''. It now degrades to a neutral profile. projectIdreaches where it was lost: orchestrator children,ToolContext, and
ConversationService.create().- New entry points in the context inspector:
delegatedandchannel. A channel reply now
records what its prompt was made of, which it never did before. - Route-scoped body limits. The 1 MiB global cap is now raised per prefix. This also fixes
document uploads, which believed they allowed 50 MB while HTTP rejected them at 1 MiB. - Public asset route.
<dataDir>/publicis served withCross-Origin-Resource-Policy: cross-origin, so an EYAS-hosted image can finally load in an email client or an exported page.
Binary types only — SVG is refused on an origin that holds the session cookie.
Outgoing HTML, without a second entity to configure
- A deterministic renderer owns every byte of the HTML EYAS sends —
notification email, channel replies, and the body of an approved email draft,
composed at SEND time rather than frozen into the draft row. - It takes Markdown, never HTML. That is the security property, not a
convenience: accepting markup would need a sanitizer, and instead the body
goes through the existing escape-by-construction markdown renderer while the
shell is built in code. A caller that passes HTML gets a refusal that says to
send Markdown. - The notification email channel stops building its own HTML. It routed
around the template engine with an unescaped inline string — which is why
render()andregisterTemplate()had zero callers. Both the escaping bug
and the bypass are gone, and the transport is injectable so the send path is
actually tested. ChannelContent.htmlis finally populated. It had been plumbed all the
way to MIME since the channel layer was written and never filled in.render_html_documenttool, so an agent can turn Markdown into a
self-contained page or email body through the same renderer.
The brand entity, considered and removed
- A second entity (
design_systems: palette, typography, tone, logo) was built
alongside designs, with its own table, CRUD, versioning, project column,
settings card,brand_gettool, compliance critic and app skin. It is
gone. Two adjacent concepts — "brand" and "design" — are misleading side by
side, and what was useful about it the design already carries: an attached
canvas puts its artboard source in the prompt, so the agent sees the colours,
the type and the components. - What survived, and why: the deterministic HTML renderer above. Its palette
is a constant now. Everything else went with the entity — the compliance
critic, the app skin, the URL extraction, thebrand-contextprompt section
and the 800 tokens carved out for it (projectCascadegets its full 3000
back; the total is still 8400). - Existing installs keep two inert columns (
projects.design_system_id,
conversations.design_system_id) that nothing reads; a table rebuild is not
worth it.
Tools that reach the agent, and output you can find
- The design tools reached nobody. They were registered, both MCP bridges
would have served them, and every agent's allow-list had been written before
the design module existed. The symptom was a model saying "design_readis
not wired", working around it, and producing the result twice — once without
the design and once with the palette it scraped out of the prompt. - Seeded agents are brought up to the tool set their template grants today.
Same shape as the design-prompt seed migration: keep every previously shipped
tool set, upgrade only rows that still match one exactly. An agent whose
tools somebody edited is left alone, because that was a decision. - Every module that registers tools now declares
toolsas a dependency.
Eight did not. Registration is(ctx as any).tools?.registry— an optional
chain that gives up silently — so a module ordered before the tools module
loses its tools with no error anywhere. It worked only because bootstrap
happens to register the tools module earlier in the file. A contract test now
runs the real loader over the real dependency graph. - The MCP bridge logs the tool list it serves. Without it, "did the model
actually get that tool" is archaeology. - A conversation always has a working directory. With none, the agent
picks: observed output landed in/tmpand on the Desktop in the same
session. When neither the request nor the project names one, the conversation
gets a directory of its own under the data directory. - What an agent writes hangs off the message that produced it, so it is
visible in the conversation as it happens and still there after a reload —
not only in a side panel. The outputs are collected before the assistant
message is stored and attached to it. - A non-image attachment is finally visible. Every attachment rendered as
an<img>that hid itself on error, so an HTML page, a PDF or a CSV became
an invisible broken image: an agent could write a file, register it
correctly, and still leave no trace anywhere a user looks. Images still
render as images; everything else is a chip with its name, its size and a
link that opens it. - Background runs collect their output too, not only interactive chat — a
scheduled report otherwise existed only on disk. - The token figure says what it counts. "299 931 token" next to a one-line
question reads as if EYAS had sent that much context. It is what the provider
billed for the run, and a CLI agent re-sends everything it has read on each
of its internal turns, so it is the sum of those. The label and its tooltip
now say so. (The number itself was correct — this is a labelling fix, not an
accounting one.) - Two clocks, one comparison. Output collection compared
Date.now()with
the filesystem's mtime and required strictly newer. A fast run writes its
file in the same millisecond, some filesystems keep mtime only to the second,
and the file was dropped — a test that passed alone and failed in a warm
suite. There is a two-second tolerance now: wider than any rounding, far
narrower than the age of anything already in the workspace. - What an agent writes also shows up in the attachments panel. A CLI
provider writes with its own file tool, so there is nowrite_filecall to
intercept and nothing lands in the documents table. Instead, after a turn,
the working directory is scanned for files newer than the turn and they are
registered as documents on the conversation. Deliberately conservative: two
directory levels, no build or checkout directories, an allow-list of
extensions a person would open, and a cap — a working directory can be a
repository, and a build must not become four thousand attachments.
The tool list the model was shown was 15% of the tool list
- 56 tools rendered to 13 586 characters against a 2 000-character budget, so
the model saw eight. Everydesign_*tool was among the 11 517 characters
dropped, and the clip landed mid-sentence — taking with it the line that tells
the model where the real schemas come from. One half of the prompt referenced
tools the other half did not list. The observable result: an agent narrating
"the design tool may have a different name, I'm looking for it", writing the
requested page without the attached design, and writing it again once it had
worked the palette out from the design index instead. - Descriptions are now the first thing given up, and names the last. An
inventory's job is to say what exists; the schemas arrive over the provider's
tool API, which the footer states. Names-only puts all 56 tools in about
1 000 characters — the complete list, inside the same budget. - The footer survives at every size, because it is where the model learns
the descriptions it is no longer being shown are available elsewhere. - Nothing trails off. If even the names do not fit, the section says how
many it could not list. That is the case for skills today: 228 of them need
roughly 3 700 characters against a 1 600-character bucket, so about a third
are named and the rest are counted. A 228-entry inventory in a system prompt
is its own problem, now visible instead of silently cut.
A skill has to be accepted, and never takes the tools away
- "Make a simple HTML file that shows the time" matched the
google-drive-integrationskill at 0.9, was injected silently, and the same
code path emptied the tool list —if (activeSkill) tools.length = 0.
design_readwas named in the prompt and in the tool inventory and was not
callable, so the agent read a stale file off disk and produced the wrong
design. Verified from the recorded prompt: the inventory was complete and
untruncated, the design announcement was there, and the tools never arrived. - The tool-stripping rule is gone, and was not narrowed to one skill type
either. Anintegrationskill exists to be used WITH tools; atoolskill IS
one; aknowledgeskill is reference material, which is no reason an agent
should stop being able to read a file. Nothing tested it and nothing depended
on it. - A matched skill is now a proposal, and the turn waits for the answer.
Nothing is streamed, no assistant message is written and the model is not
called: the run stops at the match, which happens before the stream opens.
The user's message stays stored — it really was sent. - The answer is two buttons, and the card says WHY the skill matched: name,
score and the matched pattern. On the failure above it would have read
Google Drive · 0.9 · name: Google Driveon a request to print the time. - Both answers are remembered, per conversation. Accepting applies the skill
silently from then on, because it was approved; declining means it is never
proposed again there. A skill can be right for one conversation and wrong for
the next, so the decision is not global. - Resuming is a re-run, not a suspended request. Holding an SSE connection
open until a human clicks would survive neither a restart nor a closed tab.
The client re-sends withresume: true, which skips storing the user message
a second time — pinned by a test that counts the messages across the stop and
the resume. - The background path proposes nothing. There is nobody to ask, so it uses
what was already accepted and otherwise runs without a skill. - Without a decision store no skill is applied at all. A decision that
cannot be recorded would be asked for again every turn, and injecting
silently instead is the exact bug this closes. - The weak match that started it is left standing as a separate problem — a
skill scoring 0.9 on an unrelated request means the matcher needs work — but a
bad match now costs a click, not every tool and a wrong answer.
Every tool call spun for ever on a finished run
- The ACP client knew which tool call had ended and threw the id away. It
emittedtool_use_startwith the realtoolCallIdand then a bare
tool_use_end— no id, no status — so the panel could never match an end to
a start. Seven tool rows kept spinning under a run badgedcompleted. tool_use_endnow carriesidandstatus, optional so the nine providers
that emit it bare stay valid. The grok-cli path fills both, the SSE relays
forward them, and the two CLI providers stop guessing the id for their
orchestration event.- Three ways a call can settle, in order of trust: the
tool_resultthat
names it, thetool_use_endthat names it, and — for a provider that names
nothing — the oldest still-running call, because these CLIs run their tools
one at a time. A first outcome always stands; a late end cannot turn a
reported success into an error. - And a last line of defence: finishing a run settles anything still
running. A completed run cannot have a tool call in flight.
Memory the model can actually see
- The vault reaches the prompt. A durable note — a markdown file with
frontmatter — now contributes one line to an index the agent gets on every
turn, on both the interactive and the background path. Before this, no memory
tier had any path into the prompt except a tool nobody called, so a memory
system that had been empty for 24 conversations would have stayed invisible
even once it filled. - Two frontmatter fields carry it:
kind(user,feedback,project,
reference) andsummary.userandfeedbackrank first because they
change how every answer is produced. - A note with no
kindis never read asuser. It becomesfeedbackunder
procedural/andreferenceotherwise. Promoting an undeclared note to a
fact about the owner would put it at the top of every prompt on a guess. - A hand-written Obsidian note works as-is. With no
summary, the note's
first real line becomes its index entry — no EYAS-specific frontmatter needed
for the vault to be useful. - The index says what it is. A note's body is conversation text replayed
into a system prompt later, which is a delayed prompt-injection channel; the
block is labelled background context, not instructions. - Nothing is silently cut. Whole lines are dropped to fit the budget and the
count of what did not fit is printed. Half a summary is noise the model has to
guess at. - Per turn, not a cache-prefix section —
DEFAULT_BUDGET_FULLsums to 8400
against a shrink target of 8800, so a new prefix section would quietly scale
every other section down for every agent. - Capture is deliberately not in this change. The vault holds exactly what
was put there on purpose.
The user manual
- The design chapter is rewritten in all six languages
(knowledge/design.md). It had grown by appending a section per feature and
read like a build log: "Editing" and "Editing on the canvas" as separate
topics, panning documented twice, "Import and export" competing with
"Printing and export", and — worse — a stale claim that a small canvas is
injected whole and a large one summarised, which stopped being true when the
index landed. - Eleven sections in a reading order, identical across the six languages:
creating, getting around, opening one artboard, the three ways to edit,
tweaks, versions, naming artboards so an agent can find them, attaching,
what an agent actually sees, exporting and printing, and renaming and
deleting. - Writing the chapter is what found the missing delete button. A "what is
not there yet" section had to exist to admit it. Both are gone now — the
button was built, and the section it needed had nothing left to say.
Fixed along the way
triage.tscalledrequire()three times in an ESM codebase.
COMPLEXITY_TO_TIERandCATEGORY_TIER_OVERRIDEare runtime constants that
had been placed in animport type— which erases them — so three call sites
reached forrequire('./types.js')to get them back. That resolves only once
something else has loaded the module, so any chat-route test running on its
own died with "Cannot find module './types.js'" while passing in a warm suite.
They are a value import now.
Security
- Artboard HTML never gains
allow-same-origin; the public asset route serves an
allow-list of binary types only, withnosniffand an explicit CORP header, because it bypasses
Hono and therefore every security-header middleware. - DOMPurify added (taken under its Apache-2.0 option) for the HTML surfaces that follow.
Known issues
- The 58 pre-existing test failures across 9 files (test-fixture drift) are unchanged.
- The design source editor is a plain textarea, matching the workspace file editor
already in the app. A real code editor is deliberately deferred rather than
introducing a second editing paradigm in the same release. - The agentic executor tier — a CLI provider editing the materialised canvas with
its own file tools — is designed but not wired; it needs the agent-runner
integration.chooseTiernever returns it, and nothing pretends otherwise. bun run full-docsregenerates every documentation page from the generator's metadata and
destroys hand-written prose in the process. The committed docs and the generator have
diverged; the design documentation was therefore hand-written into the six pages rather
than generated.- The skill matcher scores badly.
google-drive-integrationmatched a request
to print the time at 0.9, against a 0.1 threshold, out of 228 enabled skills.
The acceptance gate means a bad match now costs a click instead of a wrong
answer, and it is what finally makes the matcher's real quality measurable —
but the scoring itself is untouched. - 228 skills do not fit an inventory. Their names alone need roughly 3 700
characters against a 1 600 character budget, so about a third are named and
the rest are counted. Honest, and still a lot of skills to put in front of a
model on every turn. - Durable memory does not fill itself yet. Recall works — a note in the vault
reaches every prompt as one index line — but nothing writes one automatically.
The vault holds exactly what was put there on purpose. - The permission bridge has no deterministic working-directory check. Its
gate is model-judged, so it refused one write to the project root and allowed
another; an agent told to write only under its workspace put a file in the
repository root anyway. A path comparison would settle it.