#405 (5527cea) exists because an agent, asked about code at a workdir that was empty and not a
checkout, was handed two non-answers describing no problem — and invented the repository. The fix
wrote a real diagnosis: one sentence, deliberately phrased "to be RELAYED: an agent can say it to
the owner verbatim and it is both true and actionable."
The chat context does not carry it. The model is told needs_attention and nothing else.
Honest — far better than the old false ready — but a bare enum token is not something an agent
can say out loud, and relaying the diagnosis was the point of the ticket.
Where
workers/api/src/agent-think.ts:493-502:
conststatus=r.cloneStatus==="ready"
? "ready"
: r.cloneStatus==="cloning"
? "cloning…"
: r.cloneStatus==="error"
? `clone error${r.cloneError ? `: ${r.cloneError}` : ""}`
: r.cloneStatus;systemPrompt+=`- ${r.name}${r.githubRepo ? ` (${r.githubRepo})` : ""} — ${status}\n`;CloneStatus has six values (lib/coding-types.ts:20):
unknown | cloning | ready | missing_url | error | needs_attention. The chain handles three. The
cloneError field — right there on the same object (:48) — is read on exactly one branch.
So the prompt says:
- apps/chess-academy — needs_attention
when the platform already stored, in clone_error:
The configured checkout /Users/…/apps/chess-academy exists but is EMPTY — nothing was ever
cloned into it, or its contents were moved away. There is no code at that path to read.
Mechanism
Two correct things, unjoined. verifyLocalWorkdir (routes/coding-repos.ts:104-119) persists both
halves together — updateRepoClone(env, repo.id, { cloneStatus: status, cloneError }) — and
verdictFromCheck (lib/coding-workdir.ts:73-113) writes a distinct sentence for each of
missing / not_a_directory / empty / not_a_git_repo / unverified. The prompt builder is a
ternary chain that predates the new statuses, so anything it does not enumerate falls through as
the raw token. Nothing warns: adding a CloneStatus value is type-safe against a fallthrough that
prints it.
unknown falls through the same way, and it is the other state #405 created — "no machine
connected, nobody has looked at this path" — which is a genuinely important thing for an agent to
be able to say instead of guessing.
Do — cheapest first
A separate decision, worth stating rather than drifting into
agent-think.ts:392 reads repos through listRepos (lib/coding-store.ts:104-111), a plain
SELECT. So the chat shows the last persisted verdict, refreshed only when the console's repos
route or add-repo last ran verifyLocalWorkdir. That is probably right — a relay round-trip per
chat turn is not worth it, and the repos list re-checks on every read (capped at
MAX_VERIFY_PER_LIST = 12). But it is currently an accident rather than a decision. If it stays,
consider saying when the verdict was taken, so an agent can distinguish "it is broken" from "it
was broken when someone last looked."
Alternatives considered
- Have the prompt builder run the workdir check itself. Rejected: a relay round-trip on every
chat turn, on the latency path, for a fact that changes rarely and is already refreshed by the
surface the user is looking at. - Put the sentence in memory so the agent "knows" it. Rejected outright — this is live platform
state, and memory holding a stale copy of it is precisely the phantom-repository failure that
repo-chat removed its mem:repository entry to escape.
Acceptance
Regression risk
- Prompt length. Each sentence is ~120 characters, one per broken repo, bounded by the number
of attached repos. Small, but if an instance has many broken repos it is now many sentences —
consider capping at the first N and summarising the rest. - Saying it twice.
agent-think.ts:398 also calls selfDescriptionPrompt(selfModel, { repoSetting, attached })
(lib/agent-self-description.ts:250-300), which renders attached repos for its own purpose. Check
it before adding the sentence in a second place; the console and the chat describing one directory
two different ways is the failure #405 explicitly designed against.
Files: workers/api/src/agent-think.ts:392,398,492-502, workers/api/src/lib/coding-types.ts:20,47-48,
workers/api/src/lib/coding-workdir.ts:43-52,73-131, workers/api/src/routes/coding-repos.ts:104-119,
workers/api/src/lib/coding-store.ts:104-111, workers/api/src/lib/agent-self-description.ts:250-300.
Related: #405 (which produced the diagnosis), #395 (the fabrication it removes the reason
for).
#405(5527cea) exists because an agent, asked about code at a workdir that was empty and not acheckout, was handed two non-answers describing no problem — and invented the repository. The fix
wrote a real diagnosis: one sentence, deliberately phrased "to be RELAYED: an agent can say it to
the owner verbatim and it is both true and actionable."
The chat context does not carry it. The model is told
needs_attentionand nothing else.Honest — far better than the old false
ready— but a bare enum token is not something an agentcan say out loud, and relaying the diagnosis was the point of the ticket.
Where
workers/api/src/agent-think.ts:493-502:CloneStatushas six values (lib/coding-types.ts:20):unknown | cloning | ready | missing_url | error | needs_attention. The chain handles three. ThecloneErrorfield — right there on the same object (:48) — is read on exactly one branch.So the prompt says:
when the platform already stored, in
clone_error:Mechanism
Two correct things, unjoined.
verifyLocalWorkdir(routes/coding-repos.ts:104-119) persists bothhalves together —
updateRepoClone(env, repo.id, { cloneStatus: status, cloneError })— andverdictFromCheck(lib/coding-workdir.ts:73-113) writes a distinct sentence for each ofmissing/not_a_directory/empty/not_a_git_repo/unverified. The prompt builder is aternary chain that predates the new statuses, so anything it does not enumerate falls through as
the raw token. Nothing warns: adding a
CloneStatusvalue is type-safe against a fallthrough thatprints it.
unknownfalls through the same way, and it is the other state#405created — "no machineconnected, nobody has looked at this path" — which is a genuinely important thing for an agent to
be able to say instead of guessing.
Do — cheapest first
cloneErrorfor every non-readystatus, not justerror. One expression:append
`: ${r.cloneError}`whenever it is present.satisfies Record<CloneStatus, string>, so the next new status is a compile error ratherthan a token leaking into a prompt. That is the actual defence; the line above is the fix.
needs_attentionandunknownprose the model can relay — e.g."UNUSABLE — <cloneError>"and"not checked (no machine connected)"— rather than the enumname dressed up.
A separate decision, worth stating rather than drifting into
agent-think.ts:392reads repos throughlistRepos(lib/coding-store.ts:104-111), a plainSELECT. So the chat shows the last persisted verdict, refreshed only when the console's reposroute or add-repo last ran
verifyLocalWorkdir. That is probably right — a relay round-trip perchat turn is not worth it, and the repos list re-checks on every read (capped at
MAX_VERIFY_PER_LIST = 12). But it is currently an accident rather than a decision. If it stays,consider saying when the verdict was taken, so an agent can distinguish "it is broken" from "it
was broken when someone last looked."
Alternatives considered
chat turn, on the latency path, for a fact that changes rarely and is already refreshed by the
surface the user is looking at.
state, and memory holding a stale copy of it is precisely the phantom-repository failure that
repo-chatremoved itsmem:repositoryentry to escape.Acceptance
cloneStatus: "needs_attention"and acloneErrorproduces a line containing that sentence.CloneStatuswithout giving it a phrase fails typecheck.agent_trace) instead of the wordneeds_attention.Regression risk
of attached repos. Small, but if an instance has many broken repos it is now many sentences —
consider capping at the first N and summarising the rest.
agent-think.ts:398also callsselfDescriptionPrompt(selfModel, { repoSetting, attached })(
lib/agent-self-description.ts:250-300), which renders attached repos for its own purpose. Checkit before adding the sentence in a second place; the console and the chat describing one directory
two different ways is the failure
#405explicitly designed against.Files:
workers/api/src/agent-think.ts:392,398,492-502,workers/api/src/lib/coding-types.ts:20,47-48,workers/api/src/lib/coding-workdir.ts:43-52,73-131,workers/api/src/routes/coding-repos.ts:104-119,workers/api/src/lib/coding-store.ts:104-111,workers/api/src/lib/agent-self-description.ts:250-300.Related: #405 (which produced the diagnosis), #395 (the fabrication it removes the reason
for).