Agent workspace: personas, and the reason verification never ran - #38
Merged
Conversation
Persona slice: `.build/agents/hyper/user.md`, injected ahead of the skills manifest as trusted guidance. That framing rests on one property — the agent cannot write it. `.build/agents/**` is refused by fs_write, fs_batch_write and fs_delete while fs_read stays open. A skill file IS agent-writable, which is why a prompt injection reaching one tool call could plant one and have it re-read with authority every turn after. Lift the write ban and the trusted framing has to go with it; tests pin both halves. Verified live: 4/5 files the agent wrote carried the persona's required header. The miss is style.css, where `//` is not a valid CSS comment. The larger find came from the run, not the feature. exec was failing with tsc's help text, so `npx tsc --noEmit` — the verify skill's headline command — had never typechecked anything. The starter shipped no tsconfig.json (tsc with no config prints usage and exits 1), no @types/react (real errors drown in TS7016 noise), and `typescript: "latest"`, which had silently resolved to TypeScript 7, the native rewrite, in the one dependency this template left unpinned. Fixed on both template surfaces; the drift guard caught the Gleam side. Same live turn, before and after: 8 steps / 100s / "found problems" -> 5 steps / 58s / "no problems". Shorter because the agent is no longer fighting a command that could not work. tsconfig.json joins UNDELETABLE — losing it does not break the app, it breaks the agent's ability to check the app, and the loop would go on reporting success. Refusals were invisible past a one-line trail summary, which is why this hid so long. executeTool now ring-buffers failed tool results and the live suite prints them. gleam 194 · vitest 282 · server 309 · smokes 25/25 + 14/14 + 37/37 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019gS8yjMedGgujEad6zRD7i
The review found the slice was prompt plumbing for a file nothing could write. The agent was banned from `.build/agents/` and no other writer existed, so readPersona() always returned '' in production. Built the editor: a Standing instructions field in the settings panel, in both the BYOK and managed panels, with its own save since the managed panel has no Save button. Being the only writer is exactly what makes the trusted framing legitimate. Two guard tests I added passed vacuously. indexOf returns -1 when the injection is deleted and String.slice(-1) then yields one character, so the "stays trusted" test passed with the injection removed; it also matched lowercase "untrusted" only. Both rewritten to call the builders and inspect real output, then mutation-tested against deletion, capitalized re-framing, and hoist-and-reorder. All three now fail. The server did not own the block it treats as trusted — the client sent a pre-framed system message and the server checked only that it was a string. Client now sends raw text; both builders author and cap it themselves, guarded like SHARED_RULES. ensureVerifiable() backfills tsconfig.json and the React types on project load, so the verify fix reaches existing projects and not just new ones. Additive, never repins typescript, returns the same array when nothing is missing. The Ollama JSON path applied model-supplied paths straight to the project actor with no policy at all. Routed through normalizePath with refusals surfaced. `.build` added to IGNORED_SYNC_DIRS so a container-side .build/ cannot sync back into one path with two contents. Also: the persona ban was case-sensitive, twenty lines below a comment about that exact asymmetry having been an oversight once; and the failed-tool log survived project switches. Both fixed. Found while fixing: three sites read starterFiles[2].path — "the third starter file", which meant src/main.tsx only by accident. Inserting tsconfig.json retargeted all three to index.html. Replaced with a lookup by name. Nine browser checks on the editor. The reload check failed first and found a real bug: the panel starts open, so no SettingsOpened is ever sent on a fresh load and the persona never loaded back. gleam 197 · vitest 290 · server 309 · smokes 25/25 + 14/14 + 37/37 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019gS8yjMedGgujEad6zRD7i
twilson63
temporarily deployed
to
agent-workspace-persona - build PR #38
July 28, 2026 16:32 — with
Render
Destroyed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Personas — the agent workspace's first slice
One file,
.build/agents/hyper/user.md, global scope, injected at the top of every turn ahead of the skills manifest.The design point is the asymmetry. A persona is injected as trusted guidance; the skills manifest, three feet away in the same prompt, is injected as untrusted data. That's defensible for exactly one reason:
.build/agents/**is refused byfs_write,fs_batch_writeandfs_delete, whilefs_readstays open — reading its own instructions is the point. A skill file is agent-writable, which is why a prompt injection reaching one tool call could plant one and have it re-read with authority every turn afterwards: a one-shot compromise made durable. If the write ban is ever lifted, the trusted framing must go with it. There are tests on both halves and each file points at the other.Verified live: 4/5 files the agent wrote carried the persona's required header line. The miss is
style.css, where//isn't a valid CSS comment — correct judgment, not disobedience.The larger find: verification was never actually running
This came out of the run, not the feature. A new refusal log surfaced
exec: [exit 1]followed by tsc's help banner, and it reproduced cleanly outside the browser:tsconfig.json. With no config and no file argumentstscdoesn't typecheck at all — it prints usage and exits 1.npx tsc --noEmitis the headline command of the built-inverifyskill, so every turn that tried to verify burned a step on a command that could not succeed.@types/react/@types/react-dom, so even with a config a real error drowns under TS7016/TS7026 noise.typescript: "latest"had silently resolved to TypeScript 7.0.2, the native rewrite — a major version change under existing projects, in the one dependency this template left unpinned while carefully pinning everything else.Fixed on both duplicated template surfaces; the drift guard caught the Gleam side as designed.
Same live turn, before and after:
Shorter because the agent is no longer fighting a command that structurally couldn't work.
tsconfig.jsonjoinsUNDELETABLE: losing it doesn't break the app, it breaks the agent's ability to check the app — worse, because the loop goes on reporting that it verified.Three regression tests pin why each piece exists, so a future cleanup doesn't read them as boilerplate.
Refusals are now visible
The trail showed
Tried to write files it may not changeand nothing else — no file, no reason — which is why this hid so long.executeToolnow ring-buffers failed tool results (failures only, reason capped at 300 chars, 50 max) and the live suite prints them. That's what turned an unexplainedexit 1into the finding above.Validation
gleam 194 · vitest 282 · server 309 · smokes 25/25 + 14/14 + 37/37,tsc -band server typecheck clean, plus three live model runs.🤖 Generated with Claude Code
https://claude.ai/code/session_019gS8yjMedGgujEad6zRD7i