···21211. Run `vit init` to initialize `.vit/` directory (derives beacon from git remotes).
22222. Run `vit follow <handle>` to follow accounts whose caps you want to see.
23233. Run `vit skim --json` to read caps from followed accounts filtered by beacon.
2424-4. Run `vit ship <text> --title <t> --description <d> --ref <ref>` to publish a cap.
2424+4. Run `vit ship --title <t> --description <d> --ref <ref> <<'EOF' ... EOF` to publish a cap (body on stdin).
25252626Handoffs:
2727- If no DID is configured, tell the user to run `vit login <handle>`.
···8989- Output: `handle (did)` lines or `no followings`.
9090- Common errors: malformed following file content.
91919292-### `vit ship <text>`
9393-- Description: Publish a cap to ATProto.
9494-- Usage: `vit ship <text> --title <title> --description <description> --ref <ref>`
9595-- Key flags: required `--title <title>`, `--description <description>`, `--ref <ref>`; optional `--did <did>`, `-v, --verbose`
9292+### `vit ship`
9393+- Description: Publish a cap to ATProto from stdin body input.
9494+- Usage: `vit ship --title <title> --description <description> --ref <ref> [--recap <ref>] <<'EOF' ... EOF`
9595+- Key flags: required `--title <title>`, `--description <description>`, `--ref <ref>`; optional `--recap <ref>`, `--did <did>`, `-v, --verbose`
9696+- Input: cap body is required via stdin (pipe or heredoc).
9797+- Gate: agent-only (`requireAgent()`).
9698- Output: JSON object on success.
9797-- Common errors: no DID, invalid ref, session expired.
9999+- Common errors: not running in an agent context, missing stdin body, no DID, invalid ref, recap ref not found, session expired.
9810099101### `vit beacon <target>`
100102- Description: Probe a remote repo and report whether its beacon is lit.
+120-11
src/cmd/ship.js
···22// Copyright (c) 2026 sol pbc
3344import { TID } from '@atproto/common-web';
55+import { readFileSync } from 'node:fs';
56import { CAP_COLLECTION } from '../lib/constants.js';
66-import { loadConfig } from '../lib/config.js';
77+import { requireAgent } from '../lib/agent.js';
88+import { requireDid } from '../lib/config.js';
79import { restoreAgent } from '../lib/oauth.js';
88-import { appendLog, readProjectConfig } from '../lib/vit-dir.js';
99-import { REF_PATTERN } from '../lib/cap-ref.js';
1010+import { appendLog, readProjectConfig, readLog, readFollowing } from '../lib/vit-dir.js';
1111+import { REF_PATTERN, resolveRef } from '../lib/cap-ref.js';
10121113export default function register(program) {
1214 program
1315 .command('ship')
1414- .argument('<text>', 'Cap text to publish')
1516 .description('Publish a cap to your feed')
1617 .option('-v, --verbose', 'Show step-by-step details')
1718 .option('--did <did>', 'DID to use (reads saved DID from config if not provided)')
1819 .requiredOption('--title <title>', 'Short title for the cap')
1920 .requiredOption('--description <description>', 'Description of the cap')
2021 .requiredOption('--ref <ref>', 'Three lowercase words with dashes (e.g. fast-cache-invalidation)')
2121- .action(async (text, opts) => {
2222+ .option('--recap <ref>', 'Ref of the cap this derives from (quote-post semantics)')
2323+ .action(async (opts) => {
2224 try {
2323- const { verbose } = opts;
2424- const did = opts.did || loadConfig().did;
2525- if (!did) {
2626- console.error("No DID configured. Run 'vit login <handle>' first or pass --did.");
2525+ const gate = requireAgent();
2626+ if (!gate.ok) {
2727+ console.error('vit ship should be run by a coding agent (e.g. claude code, gemini cli).');
2828+ console.error("open your agent and ask it to run 'vit ship' for you.");
2729 process.exitCode = 1;
2830 return;
2931 }
3232+3333+ const { verbose } = opts;
3434+ const did = requireDid(opts);
3535+ if (!did) return;
3036 if (verbose) console.log(`[verbose] DID: ${did}`);
31373838+ let text;
3939+ try {
4040+ text = readFileSync('/dev/stdin', 'utf-8').trim();
4141+ } catch {
4242+ text = '';
4343+ }
4444+ if (!text) {
4545+ console.error('error: cap body is required via stdin (pipe or heredoc)');
4646+ process.exitCode = 1;
4747+ return;
4848+ }
4949+3250 if (!REF_PATTERN.test(opts.ref)) {
3351 console.error('error: --ref must be exactly three lowercase words separated by dashes (e.g. fast-cache-invalidation)');
3452 process.exitCode = 1;
3553 return;
3654 }
5555+5656+ let recapUri = null;
5757+ if (opts.recap) {
5858+ if (!REF_PATTERN.test(opts.recap)) {
5959+ console.error('error: --recap must be exactly three lowercase words separated by dashes (e.g. fast-cache-invalidation)');
6060+ process.exitCode = 1;
6161+ return;
6262+ }
6363+6464+ const caps = readLog('caps.jsonl');
6565+ const localMatch = caps.find(e => e.ref === opts.recap);
6666+ if (localMatch) {
6767+ recapUri = localMatch.uri;
6868+ if (verbose) console.log(`[verbose] recap resolved locally: ${recapUri}`);
6969+ }
7070+ }
7171+3772 const now = new Date().toISOString();
38733974 const { agent, session } = await restoreAgent(did);
4075 if (verbose) console.log(`[verbose] Session restored, PDS: ${session.serverMetadata?.issuer}`);
41764242- const record = { $type: CAP_COLLECTION, text, title: opts.title, description: opts.description, ref: opts.ref, createdAt: now };
7777+ if (opts.recap && !recapUri) {
7878+ const following = readFollowing();
7979+ const dids = following.map(e => e.did);
8080+ dids.push(did);
8181+ if (verbose) console.log(`[verbose] recap: querying ${dids.length} accounts`);
8282+8383+ let match = null;
8484+ for (const repoDid of dids) {
8585+ try {
8686+ const res = await agent.com.atproto.repo.listRecords({
8787+ repo: repoDid,
8888+ collection: CAP_COLLECTION,
8989+ limit: 50,
9090+ });
9191+ for (const rec of res.data.records) {
9292+ const recRef = resolveRef(rec.value, rec.cid);
9393+ if (recRef === opts.recap) {
9494+ if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) {
9595+ match = rec;
9696+ }
9797+ }
9898+ }
9999+ } catch (err) {
100100+ if (verbose) console.log(`[verbose] ${repoDid}: error fetching caps: ${err.message}`);
101101+ }
102102+ }
103103+104104+ if (match) {
105105+ recapUri = match.uri;
106106+ if (verbose) console.log(`[verbose] recap resolved remotely: ${recapUri}`);
107107+ } else {
108108+ console.error(`error: could not find cap with ref '${opts.recap}' to recap`);
109109+ process.exitCode = 1;
110110+ return;
111111+ }
112112+ }
113113+114114+ const record = {
115115+ $type: CAP_COLLECTION,
116116+ text,
117117+ title: opts.title,
118118+ description: opts.description,
119119+ ref: opts.ref,
120120+ createdAt: now,
121121+ };
43122 const projectConfig = readProjectConfig();
44123 if (projectConfig.beacon) record.beacon = projectConfig.beacon;
124124+ if (opts.recap) record.recap = { uri: recapUri, ref: opts.recap };
45125 if (verbose && projectConfig.beacon) console.log(`[verbose] Beacon: ${projectConfig.beacon}`);
46126 const rkey = TID.nextStr();
47127 if (verbose) console.log(`[verbose] Record built, rkey: ${rkey}`);
···59139 ts: now,
60140 did,
61141 rkey,
142142+ ref: opts.ref,
62143 collection: CAP_COLLECTION,
63144 pds: session.serverMetadata?.issuer,
64145 uri: putRes.data.uri,
···81162 console.error(err instanceof Error ? err.message : String(err));
82163 process.exitCode = 1;
83164 }
8484- });
165165+ })
166166+ .addHelpText('after', `
167167+Authoring guidance (for coding agents):
168168+169169+ Fields:
170170+ --title Short name for the cap (2-5 words)
171171+ --description One sentence explaining what this cap does
172172+ --ref Three lowercase words with dashes (your-ref-name)
173173+ --recap <ref> Optional. Ref of the cap this derives from (links back to original)
174174+ body (stdin) Full cap content, piped or via heredoc
175175+176176+ What makes a good cap:
177177+ - Title is a concise noun phrase: "Fast LRU Cache", "JWT Auth Middleware"
178178+ - Description explains the value: "Thread-safe LRU cache with O(1) eviction"
179179+ - Body contains the complete, self-contained capability text
180180+ - Ref is a memorable three-word slug for discovery
181181+182182+ When to use --recap:
183183+ Use --recap when this cap is derived from another cap (e.g. after vit remix).
184184+ It creates a link back to the original, like a quote-post.
185185+186186+ Example:
187187+ vit ship --title "Fast LRU Cache" \\
188188+ --description "Thread-safe LRU cache with O(1) eviction" \\
189189+ --ref "fast-lru-cache" \\
190190+ --recap "original-cache-ref" \\
191191+ <<'EOF'
192192+ ... full cap body text ...
193193+ EOF`);
85194}