feat(cli): bdrive capture — file an agent's output into the project with one pipe - #211
Open
ssowonny wants to merge 2 commits into
Open
Conversation
Two behavior-preserving lifts so `bdrive capture` reuses code instead of copying it: the share POST out of shareCmd's RunE, and the accepted-rules volume read out of grep.go (it was about to become a fourth copy). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An agent finishes a piece of work and its output goes wherever the human remembers to put it, or nowhere. `claude -p "..." | bdrive capture` writes stdin verbatim to <mount>/inbox/<timestamp>.md and prints that path; --share syncs it now and prints a public link on the next line. Everything after the write already shipped: the scan journals it, History attributes it, share.go mints the link. The one new piece of judgement is the scope gate — a project narrowed with `init --only` or `scope add` excludes inbox/, and a file written there would sit on one laptop forever with no error ever. It refuses before writing and names `bdrive scope add inbox`, applying Filter.SkipUp with this device's ACCEPTED rules installed, because a bare LoadFilter reads a teammate's pulled `!inbox/` widening as "syncs" while the cycle silently declines to upload it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
TL;DR
claude -p "..." | bdrive capturefiles the output asinbox/<timestamp>.mdin this project and prints the path.--sharesyncs it and prints a link on line 2.share.gomints the link. This is the verb, nothing else.inbox/, it refuses before writing instead of leaving a file that syncs to nobody, silently, forever.mintShare,acceptedRules) so this reuses code instead of copying it — readgit show fb94db4and skip ahead./capturelisting inawesome-claude-code/awesome-claude-skills) is not in here and needs its own issue.Closes BEA-214.
The command is four steps, and only one of them is new
flowchart LR A["<b>1. stdin</b><br/>TTY -> refuse<br/>0 bytes -> refuse"] B["<b>2. mount</b><br/>findProject(cwd)<br/>-> root + proj"] C["<b>3. SCOPE GATE</b><br/>SkipUp(inbox/..)<br/>-> refuse, no file"] D["<b>4. write</b><br/>O_EXCL, -2, -3<br/>--share: one Cycle"] A --> B --> C --> D classDef gate fill:#f59e0b22,stroke:#f59e0b,stroke-width:2px class C gateSteps 1, 2 and 4 are plumbing over helpers that already exist. Step 3 is the whole
judgement call, and it is why the command refuses rather than warns.
Why the gate refuses instead of warning
A project narrowed with
bdrive init --onlyorbdrive scope addcarries!negation rules in the synced
.bdriveignore. Aninbox/outside that scope is aperfectly ordinary local file that simply never leaves the machine — no error, not
now, not ever. The promise of the command is "filed into the project"; a file that
quietly stays on one laptop is exactly the failure the issue describes. So it
refuses before
MkdirAll, leaving not even an emptyinbox/behind, and names thefix:
run `bdrive scope add inbox` first.The accepted-rules floor is what makes the gate correct, and it is the part
worth reading twice.
syncer.LoadFilteralone gives youSkipsemantics. The scanapplies
SkipUpwithstore.SyncState.IgnoreAcceptedinstalled(
syncer.go:456), so a teammate's pulled!inbox/widening rule reads as"syncs" to a bare filter while the cycle silently declines to upload it. Skipping
AcceptRuleshere would reintroduce the exact silent failure this command existsto fix.
TestCaptureScopeGateUsesAcceptedRulesfails if you drop that one line —I checked by dropping it.
Two decisions taken, both overridable in one line
Paused or never-enrolled projects warn and still write (
--shareis theexception and refuses). Rationale: a paused project's
inbox/file syncs themoment someone runs
bdrive initagain, while an out-of-scope path never syncs nomatter what anyone does. Those are different failures and get different answers.
inbox/is hard-coded, no setting — the spec's answer, kept. Reversing it is afolder rename, which sync already handles as ordinary journal ops. A per-project
setting is what to add when a team actually asks.
Two things that are deliberately not what you'd guess
Not
store.WriteFileAtomic. Its temp-file-plus-rename is exactly what wouldlose a race between two captures in the same second; the collision criterion needs
O_EXCLon the real name. The cost is a millisecond window where the daemon couldscan a 0-byte file and journal an empty version — the next scan corrects it, and
--shareruns its own cycle after the write.Not
io.LimitReader.readlog.goandhooksync.gobound an untrusted hookevent; this is a transcript the user piped in, and silently truncating it is the one
unacceptable failure.
A capture holding a credential still syncs
The issue body claimed
internal/syncer/secrets.gorefuses it. It doesn't — thatpath only warns, by design (holding an op back would break the cycle's "degrade to
offline, never fail" posture). The refusal is hub-side at share time. So a capture
with a secret in it reaches the team; only
--shareis blocked, and because thepath is already on stdout by then, nothing is lost.
capturegrows no--forceofits own — the error names
bdrive share <path> --force.What was run
go build ./...,go vet ./...go test ./...internal/webappincludedcmd/bdrive/capture_test.go(new, 8 tests)-2collision, scope refusal (no empty dir left), accepted-rules floor, outside-a-projectinternal/webappTestCLICaptureShare(new)--shareprints path + URL, the URL serves the captured bytes; an AWS-shaped key exits non-zero after the write, still prints the path, prints no/s/, namesbdrive share <path> --forceTestCLIShare*(existing)mintShareextraction's review contractarchitecture/cli-sync.mdparseNo frontend change, so no
npm run build, no Playwright suite, no screenshots.No
syncerchange either, so no new multi-device test — the sync path this exercisesis the ordinary one, covered end to end by
TestCLICaptureShare.Deviations from the reviewed plan
Two, both small:
secretsFoundreturns a typedsecretsErrorrather than a plain error. Theplan had
capturereusemintShare's 409 message as-is, but that message ends"Re-run with
--force" andcapturehas no--force— so it would have named aflag that doesn't exist.
errors.Asletscaptureappend the real way past it.printIfPrivatecame out ofshareCmdtoo, alongsidemintShare. Theprivate-address note is part of "here is your link", and
capture --shareprintsa link.
Neither changes
bdrive share's behavior;TestCLIShareSecretGateandTestCLIShareFolderstill pin all four status paths.Architecture changes
architecture/cli-sync.md—capturejoins theCommandsclass list, and one notestates the gate. Nothing else in the package map moved: no new types, no new seams,
and the two extracted helpers stay inside
cmd/bdrive.flowchart TB Commands["<div style='text-align:left'><b>Commands</b><br/>init login logout<br/>sync stop scope grep stale forget status log<br/>restore url share <span style='background:#22c55e55;padding:0 4px;border-radius:3px'>✅ capture</span> export import<br/>web desktop daemon hooks read-log<br/>resume autostart</div>"] CobraNote["cmd/bdrive — thin cobra layer;<br/>init is the front door, stop pauses"] GrepNote["grep / stale — LoadProject not ResolveMount,<br/>no session, no flock, no network"] CaptureNote["✅ capture: stdin → the mount root's inbox/<timestamp>.md,<br/>prints the path; --share runs one Cycle then mintShare<br/>(extracted out of share, which now only prints what it returns).<br/>The scope gate is the whole command: Filter.SkipUp with<br/>acceptedRules(proj.ID) installed, REFUSES BEFORE WRITING when<br/>inbox/ is excluded — a bare LoadFilter reads a teammate's pulled<br/>!inbox/ widening as "syncs" while the cycle silently declines<br/>to upload it. Paused/unenrolled only warns; --share refuses."] Commands -.- CobraNote Commands -.- GrepNote Commands -.- CaptureNote classDef noteBox fill:#88888822,stroke:#888888,stroke-dasharray:2 2 classDef added fill:#22c55e22,stroke:#22c55e,stroke-width:2px class CobraNote,GrepNote noteBox class CaptureNote added linkStyle 2 stroke:#22c55e,stroke-width:2pxBuild session
(Only works on the machine this ran on.)