Skip to content

feat: card-level shared agent cwd with activeCards artifact protection - #5

Merged
Jarad-z merged 3 commits into
mainfrom
feat/daemon-card-cwd
Jun 1, 2026
Merged

feat: card-level shared agent cwd with activeCards artifact protection#5
Jarad-z merged 3 commits into
mainfrom
feat/daemon-card-cwd

Conversation

@Jarad-z

@Jarad-z Jarad-z commented May 29, 2026

Copy link
Copy Markdown
Owner

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 now
share 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 --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).

Why

The old layout split an agent's outputs across <agent8>/workdir/ and a separately
copied artifacts/ tree, and scoped paths by workspace. The product goal is that a
card'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 by projectID (the daemon is already
    per-workspace, so wsShort is dropped); a 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 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 — see
    design 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 root
    CLAUDE.md so daemon-written scaffolding isn't 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 on 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) — 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; scanNewFiles would re-upload
downloaded 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.md artifact-skip,
encodeClaudeCwd contract, 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 ./... passes
  • go build ./cmd/daemon ./cmd/server succeeds
  • Smoke: run a card on one daemon, re-home it to a second daemon, confirm the
    agent sees its own prior outputs and the live card's outputs are never pruned
  • Confirm --resume continuity across runs of the same card post-deploy

Companion PR

Frontend: live artifact refresh on artifact.added
Jarad-z/brainrot-frontend branch feat/artifact-added-live-refresh.

🤖 Generated with Claude Code

…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.
@Jarad-z
Jarad-z merged commit 1a30321 into main Jun 1, 2026
1 of 3 checks passed
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.

2 participants