feat: card-level shared agent cwd with activeCards artifact protection - #5
Merged
Conversation
…sk ops
Two cohesive changes that landed together:
1. Daemon isolation refactor — drop `--bare` + isolated HOME in favor of
the operator's real ~/.claude with workdir-staged `.claude/` for
project-scope auto-discovery. `seedIsolatedHome` removed; replaced by
`stageWorkdirClaudeDir`. `overlayPlugins` renamed to
`overlayPluginsIntoClaudeDir`. Workdir layout, plugin overlay tests,
skills tests, e2e tests updated accordingly.
2. brainrot CLI auth + new task subcommands —
- Daemon prepends brainrot CLI's directory to spawned-agent PATH so
`brainrot ...` works without operator PATH plumbing
(resolveBrainrotBin: BRAINROT_CLI_BIN > $PATH > sibling-of-daemon).
- Server `Runtime.ClaimTasks` mints a 2h-TTL PAT for the runtime
owner (agent_runtime.user_id) on every claim; daemon injects it as
BRAINROT_TOKEN in the agent env so the agent's CLI calls
authenticate. Token never written to disk. Identity model is
"option A" (borrows runtime owner) — option B (per-agent
service-account) tracked in frontend/docs/BACKEND_GAPS.md.
- `Task.UpdateMeta` service method + PATCH /tasks/{id} accepts
title/summary in addition to status. Publishes events.TaskUpdated.
- New CLI subcommands: `brainrot task create`, `task update`,
`task dispatch` (with UUID validation + warn-on-0-runs).
- docs/API.md updated.
Move each agent's working directory from the per-(workspace,card,agent) layout (<root>/<ws8>_<card8>/<agent8>/workdir) to a per-card shared cwd (<root>/_projects/<projectID>/cards/<card8>/). All agents that run on a card now share one cwd, so an agent sees the outputs of sibling agents on the same card by listing its own directory. Cross-agent claude --resume isolation is provided by claude itself (distinct --session-id values land in distinct jsonl files under the same encoded-cwd; verified empirically on claude 2.1.156). Key changes: - prepareWorkdir: new path scoped by projectID (daemon is already per-workspace, so wsShort is dropped); nil projectID routes to a named "_no-project" bucket instead of the all-zero UUID dir. - Daemon.activeCards (sync.Map): cards currently running on this daemon. handleTask Stores the card on entry, Deletes on exit. - reconcileProjectArtifacts: artifacts stay a SEPARATE read-only pool (NOT merged into the cwd tree -- merging would break the non-re-entrant lock registry, the scanNewFiles uploader, and introduce a download/write race). Now SKIPS the delete/prune pass for active cards (their pooled files may be fresh, not-yet-uploaded outputs) while STILL allowing downloads, which fixes a real data-loss path: a card re-homed to a new daemon can now fetch its own prior outputs, and a live card's unsaved outputs are never RemoveAll'd. - shouldSkipForArtifacts: also skip .agent_context/ and the root CLAUDE.md so daemon-written scaffolding is not uploaded as agent output. - GC split into gcCardCwds (new layout, LRU+retention, never evicts an active card) and gcLegacyLayout (old-layout residue). - writeProjectClaudeMD: drop a CLAUDE.md at the cwd root describing the inputs/outputs conventions and assets/artifacts paths; claude auto-loads it in -p mode. buildPrompt no longer injects those paths into every turn. - cleanupLegacyLayout: one-time startup removal of pre-refactor top-level dirs, guarded by a .migrated-v2 sentinel. Old session jsonl is intentionally not migrated (hard cutover) -- first post-cutover run per card starts fresh. Tests: TDD throughout (prepareWorkdir layout, cross-agent cwd sharing, inputs/ wipe + outputs persistence, reconcile active/inactive x empty/ non-empty manifest, GC active-card protection + pool preservation, CLAUDE.md skip, encodeClaudeCwd contract, legacy cleanup idempotency).
Follow-up to the card-level shared cwd refactor (8aacecc). Bring docs and code comments that described the pre-refactor layout in line with reality. - daemon-workdir-layout.md: add a top banner marking the doc superseded, with a quick-reference of the current _projects/<projectID>/cards/<cardShort>/ layout; keep the historical body (the old <wsShort>_ layout + the --resume incident postmortem) as an accurate record of the past. - API.md (cross-workspace execution): the local workdir path is now _projects/<projectID>/cards/<cardShort>, keyed by projectID (not a wsShort prefix) to avoid cross-workspace collisions. - assets.go: the GC note now reflects gcCardCwds reclaiming only _projects/<pid>/cards/* and never the assets/artifacts sibling pools. - agent.go / client.go: skills/plugins are materialized as loose files into the workdir .claude/{skills,commands,agents}/ tree (claude auto-discovers at startup), not into a .brainrot-plugin/ tree loaded via --plugin-dir --bare. Comment-only / doc-only; no behavior change. Build + daemon/service tests pass.
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.
What
Move each agent's working directory from the per-(workspace, card, agent) layout
(
<root>/<ws8>_<card8>/<agent8>/workdir) to a per-card shared cwd(
<root>/_projects/<projectID>/cards/<card8>/). All agents that run on a card nowshare one cwd, so an agent sees the outputs of sibling agents on the same card by
listing its own directory — "a task card's outputs = every run's outputs, in one place."
Cross-agent
claude --resumeisolation is provided by claude itself: distinct--session-idvalues land in distinct jsonl files under the same encoded-cwd(verified empirically on claude 2.1.156).
Why
The old layout split an agent's outputs across
<agent8>/workdir/and a separatelycopied
artifacts/tree, and scoped paths by workspace. The product goal is that acard's outputs are a single, agent-legible place. Along the way this also fixes a
real data-loss path in artifact reconciliation.
Key changes
prepareWorkdir— path scoped byprojectID(the daemon is alreadyper-workspace, so
wsShortis dropped); a nilprojectIDroutes to a named_no-projectbucket instead of the all-zero UUID dir.Daemon.activeCards(sync.Map) — cards currently running on this daemon.handleTaskStores on entry, Deletes on exit.reconcileProjectArtifacts— artifacts stay a separate read-only pool(NOT merged into the cwd tree; merging would break the non-re-entrant lock
registry, the
scanNewFilesuploader, and introduce a download/write race — seedesign note below). Now skips the delete/prune pass for active cards (their
pooled files may be fresh, not-yet-uploaded outputs) while still allowing
downloads. This fixes the data-loss path: a card re-homed to a new daemon can
fetch its own prior outputs, and a live card's unsaved outputs are never
RemoveAll'd.shouldSkipForArtifacts— also skips.agent_context/and the rootCLAUDE.mdso daemon-written scaffolding isn't uploaded as agent output.gcCardCwds(new layout, LRU + retention, never evicts anactive card) and
gcLegacyLayout(old-layout residue).writeProjectClaudeMD— drop aCLAUDE.mdat the cwd root describing theinputs/outputs conventions and assets/artifacts paths; claude auto-loads it in
-pmode.buildPromptno longer injects those paths on every turn.cleanupLegacyLayout— one-time startup removal of pre-refactor top-leveldirs, guarded by a
.migrated-v2sentinel. Old session jsonl is intentionallynot migrated (hard cutover) — the first post-cutover run per card starts fresh.
Design note: why a separate artifact pool (not a merged tree)
An adversarial design review found that physically co-locating the artifact copies
into
cards/would trigger four independent failures: the process-global,non-re-entrant lock registry would clobber itself;
scanNewFileswould re-uploaddownloaded sibling artifacts as this card's output; a download-vs-agent-write race;
and a card-dir naming realignment. The "unified view" the product wants is delivered
in the logical layer — the agent sees its cwd plus
--add-dir <artifacts pool>simultaneously — without co-locating bytes. Separate pool: zero of those failures.
Tests
TDD throughout: prepareWorkdir layout, cross-agent cwd sharing,
inputs/wipe +outputs persistence, reconcile (active/inactive × empty/non-empty manifest), GC
active-card protection + pool preservation,
CLAUDE.mdartifact-skip,encodeClaudeCwdcontract, legacy-cleanup idempotency.A final 11-agent adversarial review (concurrency / data-loss / edge-cases, each
finding independently verified) surfaced 0 confirmed bugs.
Test plan
go test -short ./...passesgo build ./cmd/daemon ./cmd/serversucceedsagent sees its own prior outputs and the live card's outputs are never pruned
--resumecontinuity across runs of the same card post-deployCompanion PR
Frontend: live artifact refresh on
artifact.added—Jarad-z/brainrot-frontendbranchfeat/artifact-added-live-refresh.🤖 Generated with Claude Code