Skip to content

Agent workspace: personas, and the reason verification never ran - #38

Merged
twilson63 merged 2 commits into
mainfrom
agent-workspace-persona
Jul 28, 2026
Merged

Agent workspace: personas, and the reason verification never ran#38
twilson63 merged 2 commits into
mainfrom
agent-workspace-persona

Conversation

@twilson63

Copy link
Copy Markdown
Collaborator

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:

The agent cannot write a persona.

.build/agents/** is refused by fs_write, fs_batch_write and fs_delete, while fs_read stays 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:

  • The starter shipped no tsconfig.json. With no config and no file arguments tsc doesn't typecheck at all — it prints usage and exits 1. npx tsc --noEmit is the headline command of the built-in verify skill, so every turn that tried to verify burned a step on a command that could not succeed.
  • No @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:

Steps Calls Wall Trail says
Before 8 7 100s "Checked the code — found problems"
After 5 4 58s "Checked the code — no problems"

Shorter because the agent is no longer fighting a command that structurally couldn't work. tsconfig.json joins UNDELETABLE: 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 change and nothing else — no file, no reason — which is why this hid so long. executeTool now 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 unexplained exit 1 into the finding above.

Validation

gleam 194 · vitest 282 · server 309 · smokes 25/25 + 14/14 + 37/37, tsc -b and server typecheck clean, plus three live model runs.

🤖 Generated with Claude Code

https://claude.ai/code/session_019gS8yjMedGgujEad6zRD7i

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
twilson63 temporarily deployed to agent-workspace-persona - build PR #38 July 28, 2026 16:32 — with Render Destroyed
@twilson63
twilson63 merged commit 46eb670 into main Jul 28, 2026
3 checks passed
@twilson63
twilson63 deleted the agent-workspace-persona branch July 28, 2026 17:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant