open source is social v-it.org
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: cap requests — kind:request, vit inbox, vouch --kind want/endorse

Adds the inbound contribution channel for vit:

CLI:
- vit ship --kind request: beacon required (--beacon flag or project config),
text optional, ref auto-generated from title via slugify+hash fallback,
outputs "anyone can implement this. share the ref to build demand."
- vit vouch --kind want/endorse: want-vouches skip trusted gate and beacon
requirement; endorse keeps existing behavior; kind field written to record
- vit skim --kind <kind>: client-side filter on cap kind after beacon filter
- vit explore caps --kind <kind>: passes ?kind= to explore API
- vit inbox: new command — reads readBeaconSet(), queries explore API for
caps addressed to project beacon(s), renders with want-vouch count and age;
supports --kind, --sort want-vouches, --json; graceful degradation

Lexicons:
- org.v-it.cap: adds "request" to kind.knownValues
- org.v-it.vouch: adds optional kind field (endorse|want); omitted = endorse

Explore API:
- /api/caps: adds ?kind= filter and want_vouch_count per cap
- /api/caps: adds ?sort=want-vouches (by want_vouch_count DESC)
- jetstream: stores kind column for caps and vouches
- schema: adds kind column + index to caps and vouches tables
- migrate-cap-requests.sql: migration for existing deployed DB

Vet:
- vit vet <request-ref>: suppresses --trust for kind:request caps,
shows note to vouch --kind want instead

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

+677 -71
+18
explore/migrate-cap-requests.sql
··· 1 + -- Migration: add kind column to caps and vouches tables for cap-requests feature 2 + -- Run: npx wrangler d1 execute vit-explore --file=migrate-cap-requests.sql 3 + 4 + ALTER TABLE caps ADD COLUMN kind TEXT; 5 + CREATE INDEX IF NOT EXISTS idx_caps_kind ON caps(kind); 6 + 7 + -- Backfill kind from record_json for existing cap records 8 + UPDATE caps SET kind = json_extract(record_json, '$.kind') WHERE kind IS NULL AND json_extract(record_json, '$.kind') IS NOT NULL; 9 + 10 + ALTER TABLE vouches ADD COLUMN kind TEXT; 11 + CREATE INDEX IF NOT EXISTS idx_vouches_kind ON vouches(kind); 12 + 13 + -- Backfill vouches: existing vouches without kind treated as 'endorse' 14 + UPDATE vouches SET kind = COALESCE(json_extract(record_json, '$.kind'), 'endorse') WHERE kind IS NULL; 15 + 16 + -- Also relax the NOT NULL constraint on vouches.beacon (want-vouches may lack a project beacon) 17 + -- SQLite doesn't support DROP NOT NULL via ALTER TABLE, so this is handled at the application layer. 18 + -- New vouches inserts allow NULL beacon (see jetstream.js update).
+5 -1
explore/schema.sql
··· 8 8 description TEXT, 9 9 ref TEXT NOT NULL, 10 10 beacon TEXT, 11 + kind TEXT, 11 12 record_json TEXT NOT NULL, 12 13 created_at TEXT NOT NULL, 13 14 indexed_at TEXT NOT NULL DEFAULT (datetime('now')), ··· 17 18 CREATE INDEX IF NOT EXISTS idx_caps_beacon ON caps(beacon); 18 19 CREATE INDEX IF NOT EXISTS idx_caps_created_at ON caps(created_at DESC); 19 20 CREATE INDEX IF NOT EXISTS idx_caps_ref ON caps(ref); 21 + CREATE INDEX IF NOT EXISTS idx_caps_kind ON caps(kind); 20 22 21 23 CREATE TABLE IF NOT EXISTS vouches ( 22 24 id INTEGER PRIMARY KEY AUTOINCREMENT, ··· 26 28 cid TEXT, 27 29 cap_uri TEXT NOT NULL, 28 30 ref TEXT NOT NULL, 29 - beacon TEXT NOT NULL, 31 + beacon TEXT, 32 + kind TEXT, 30 33 record_json TEXT NOT NULL, 31 34 created_at TEXT NOT NULL, 32 35 indexed_at TEXT NOT NULL DEFAULT (datetime('now')), ··· 35 38 36 39 CREATE INDEX IF NOT EXISTS idx_vouches_cap_uri ON vouches(cap_uri); 37 40 CREATE INDEX IF NOT EXISTS idx_vouches_beacon ON vouches(beacon); 41 + CREATE INDEX IF NOT EXISTS idx_vouches_kind ON vouches(kind); 38 42 39 43 CREATE TABLE IF NOT EXISTS beacons ( 40 44 id INTEGER PRIMARY KEY AUTOINCREMENT,
+16 -2
explore/src/api.js
··· 47 47 const cursor = parseCursor(searchParams.get('cursor')); 48 48 const limit = parseLimit(searchParams.get('limit')); 49 49 const beacon = searchParams.get('beacon'); 50 + const kind = searchParams.get('kind'); 51 + const sort = searchParams.get('sort'); 50 52 51 53 const conditions = []; 52 54 const bindings = []; ··· 58 60 bindings.push(...beacons); 59 61 } 60 62 63 + if (kind) { 64 + conditions.push('c.kind = ?'); 65 + bindings.push(kind); 66 + } 67 + 61 68 if (cursor) { 62 69 conditions.push('c.id < ?'); 63 70 bindings.push(cursor); 64 71 } 65 72 66 - let sql = 'SELECT c.*, h.handle FROM caps c LEFT JOIN handles h ON c.did = h.did'; 73 + let sql = `SELECT c.*, h.handle, 74 + (SELECT COUNT(*) FROM vouches v WHERE v.cap_uri = c.uri AND v.kind = 'want') as want_vouch_count 75 + FROM caps c LEFT JOIN handles h ON c.did = h.did`; 67 76 if (conditions.length > 0) { 68 77 sql += ` WHERE ${conditions.join(' AND ')}`; 69 78 } 70 - sql += ' ORDER BY c.id DESC LIMIT ?'; 79 + if (sort === 'want-vouches') { 80 + sql += ' ORDER BY want_vouch_count DESC, c.id DESC'; 81 + } else { 82 + sql += ' ORDER BY c.id DESC'; 83 + } 84 + sql += ' LIMIT ?'; 71 85 bindings.push(limit); 72 86 73 87 const { results } = await env.DB.prepare(sql).bind(...bindings).all();
+13 -5
explore/src/jetstream.js
··· 66 66 .first(); 67 67 const prevBeacon = beaconValue(existing?.beacon); 68 68 69 + const capKind = typeof record?.kind === 'string' && record.kind.length > 0 ? record.kind : null; 70 + 69 71 const stmts = [ 70 72 env.DB.prepare( 71 - `INSERT INTO caps (did, rkey, uri, cid, title, description, ref, beacon, record_json, created_at) 72 - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 73 + `INSERT INTO caps (did, rkey, uri, cid, title, description, ref, beacon, kind, record_json, created_at) 74 + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 73 75 ON CONFLICT(did, rkey) DO UPDATE SET 74 76 cid = excluded.cid, 75 77 title = excluded.title, 76 78 description = excluded.description, 77 79 ref = excluded.ref, 78 80 beacon = excluded.beacon, 81 + kind = excluded.kind, 79 82 record_json = excluded.record_json, 80 83 created_at = excluded.created_at`, 81 84 ).bind( ··· 87 90 record.description || '', 88 91 record.ref, 89 92 nextBeacon, 93 + capKind, 90 94 JSON.stringify(record), 91 95 record.createdAt, 92 96 ), ··· 136 140 .first(); 137 141 const prevBeacon = beaconValue(existing?.beacon); 138 142 143 + const vouchKind = typeof record?.kind === 'string' && record.kind.length > 0 ? record.kind : 'endorse'; 144 + 139 145 const stmts = [ 140 146 env.DB.prepare( 141 - `INSERT INTO vouches (did, rkey, uri, cid, cap_uri, ref, beacon, record_json, created_at) 142 - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) 147 + `INSERT INTO vouches (did, rkey, uri, cid, cap_uri, ref, beacon, kind, record_json, created_at) 148 + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 143 149 ON CONFLICT(did, rkey) DO UPDATE SET 144 150 cid = excluded.cid, 145 151 cap_uri = excluded.cap_uri, 146 152 ref = excluded.ref, 147 153 beacon = excluded.beacon, 154 + kind = excluded.kind, 148 155 record_json = excluded.record_json, 149 156 created_at = excluded.created_at`, 150 157 ).bind( ··· 154 161 cid ?? null, 155 162 record.subject?.uri, 156 163 record.ref, 157 - record.beacon, 164 + record.beacon ?? null, 165 + vouchKind, 158 166 JSON.stringify(record), 159 167 record.createdAt, 160 168 ),
+1 -1
lexicons/org/v-it/cap.json
··· 96 96 "type": "string", 97 97 "maxLength": 64, 98 98 "maxGraphemes": 32, 99 - "knownValues": ["feat", "fix", "test", "docs", "refactor", "chore", "perf", "style"], 99 + "knownValues": ["feat", "fix", "test", "docs", "refactor", "chore", "perf", "style", "request"], 100 100 "description": "Category of the capability." 101 101 }, 102 102 "createdAt": {
+7
lexicons/org/v-it/vouch.json
··· 26 26 "maxLength": 512, 27 27 "description": "Beacon URI scoping this vouch to a project. Included for cap vouches, omitted for skill vouches." 28 28 }, 29 + "kind": { 30 + "type": "string", 31 + "maxLength": 64, 32 + "maxGraphemes": 32, 33 + "knownValues": ["endorse", "want"], 34 + "description": "Intent of the vouch. 'endorse' = quality signal (I've vetted this). 'want' = demand signal (I want this implemented). Omitted kind treated as 'endorse' for backwards compatibility." 35 + }, 29 36 "createdAt": { 30 37 "type": "string", 31 38 "format": "datetime",
+2
src/cli.js
··· 22 22 import registerSetup from './cmd/setup.js'; 23 23 import registerHack from './cmd/hack.js'; 24 24 import registerLink from './cmd/link.js'; 25 + import registerInbox from './cmd/inbox.js'; 25 26 26 27 const program = new Command(); 27 28 program ··· 48 49 registerSetup(program); 49 50 registerHack(program); 50 51 registerLink(program); 52 + registerInbox(program); 51 53 52 54 export { program };
+2
src/cmd/explore.js
··· 212 212 .command('caps') 213 213 .description('List recent caps from the explore index') 214 214 .option('--beacon <beacon>', 'Filter by beacon') 215 + .option('--kind <kind>', 'Filter by cap kind (e.g. request, feat, fix)') 215 216 .option('--limit <n>', 'Limit number of caps') 216 217 .option('--cursor <id>', 'Pagination cursor') 217 218 .option('--json', 'Output as JSON') ··· 240 241 241 242 const url = new URL('/api/caps', baseUrl); 242 243 if (beacon) url.searchParams.set('beacon', beacon); 244 + if (opts.kind) url.searchParams.set('kind', opts.kind); 243 245 if (opts.limit) url.searchParams.set('limit', opts.limit); 244 246 if (opts.cursor) url.searchParams.set('cursor', opts.cursor); 245 247
+131
src/cmd/inbox.js
··· 1 + // SPDX-License-Identifier: MIT 2 + // Copyright (c) 2026 sol pbc 3 + 4 + import { DEFAULT_EXPLORE_URL } from '../lib/constants.js'; 5 + import { readBeaconSet } from '../lib/vit-dir.js'; 6 + import { brand } from '../lib/brand.js'; 7 + import { jsonOk, jsonError } from '../lib/json-output.js'; 8 + 9 + function timeAgo(isoString) { 10 + const seconds = Math.floor((Date.now() - new Date(isoString).getTime()) / 1000); 11 + if (seconds < 60) return `${seconds}s ago`; 12 + const minutes = Math.floor(seconds / 60); 13 + if (minutes < 60) return `${minutes}m ago`; 14 + const hours = Math.floor(minutes / 60); 15 + if (hours < 24) return `${hours}h ago`; 16 + const days = Math.floor(hours / 24); 17 + return `${days}d ago`; 18 + } 19 + 20 + function resolveUrl(opts) { 21 + return opts.exploreUrl || process.env.VIT_EXPLORE_URL || DEFAULT_EXPLORE_URL; 22 + } 23 + 24 + function unavailableMessage(baseUrl) { 25 + try { 26 + return `${new URL(baseUrl).host} is unavailable. try 'vit explore caps --beacon .' for network-wide discovery.`; 27 + } catch { 28 + return `${baseUrl} is unavailable. try 'vit explore caps --beacon .' for network-wide discovery.`; 29 + } 30 + } 31 + 32 + export default function register(program) { 33 + program 34 + .command('inbox') 35 + .description('Show caps addressed to your project beacon (project-centric view)') 36 + .option('--kind <kind>', 'Filter by cap kind (e.g. request)') 37 + .option('--sort <sort>', 'Sort order: recent (default) or want-vouches', 'recent') 38 + .option('--limit <n>', 'Limit number of caps') 39 + .option('--json', 'Output as JSON') 40 + .option('--explore-url <url>', 'Explore API base URL') 41 + .action(async (opts) => { 42 + try { 43 + const beaconSet = readBeaconSet(); 44 + if (beaconSet.size === 0) { 45 + const msg = "no beacon set — run 'vit init' first (inbox requires a project beacon)"; 46 + if (opts.json) { 47 + jsonError(msg); 48 + return; 49 + } 50 + console.error(msg); 51 + process.exitCode = 1; 52 + return; 53 + } 54 + 55 + const beacon = [...beaconSet].join(','); 56 + const baseUrl = resolveUrl(opts); 57 + 58 + let data; 59 + try { 60 + const url = new URL('/api/caps', baseUrl); 61 + url.searchParams.set('beacon', beacon); 62 + if (opts.kind) url.searchParams.set('kind', opts.kind); 63 + if (opts.sort === 'want-vouches') url.searchParams.set('sort', 'want-vouches'); 64 + if (opts.limit) url.searchParams.set('limit', opts.limit); 65 + 66 + const res = await fetch(url); 67 + if (!res.ok) throw new Error(`explore API returned ${res.status}`); 68 + data = await res.json(); 69 + } catch (err) { 70 + const msg = err instanceof Error ? err.message : String(err); 71 + const finalMsg = msg.startsWith('explore API returned ') 72 + ? msg 73 + : unavailableMessage(baseUrl); 74 + if (opts.json) { 75 + jsonError(finalMsg); 76 + return; 77 + } 78 + console.error(finalMsg); 79 + console.error("fallback: try 'vit explore caps --beacon .' to query the explore index directly."); 80 + process.exitCode = 1; 81 + return; 82 + } 83 + 84 + if (opts.json) { 85 + jsonOk({ caps: data.caps || [], cursor: data.cursor || null }); 86 + return; 87 + } 88 + 89 + const caps = data.caps || []; 90 + const beaconDisplay = [...beaconSet][0]; 91 + 92 + console.log(`${brand} inbox — ${beaconDisplay}`); 93 + console.log(''); 94 + 95 + if (caps.length === 0) { 96 + const kindFilter = opts.kind ? ` (kind: ${opts.kind})` : ''; 97 + console.log(`no caps found${kindFilter}.`); 98 + if (opts.kind) { 99 + console.log(`hint: to request a feature, run 'vit ship --kind request --beacon <url>'`); 100 + } 101 + return; 102 + } 103 + 104 + for (const cap of caps) { 105 + const wantCount = cap.want_vouch_count ?? 0; 106 + const wantStr = wantCount === 1 ? '1 want' : `${wantCount} wants`; 107 + const age = cap.created_at ? timeAgo(cap.created_at) : ''; 108 + const handle = cap.handle ? `@${cap.handle}` : cap.did || 'unknown'; 109 + const kind = cap.kind || (cap.record_json ? (() => { try { return JSON.parse(cap.record_json).kind || ''; } catch { return ''; } })() : ''); 110 + 111 + console.log(` ${cap.ref} ${handle} ${wantStr} ${age}`); 112 + if (cap.title) console.log(` ${cap.title}${kind ? ` [${kind}]` : ''}`); 113 + if (cap.description) console.log(` ${cap.description}`); 114 + console.log(''); 115 + } 116 + 117 + const kindNote = opts.kind ? ` ${opts.kind}` : ''; 118 + console.log(`${caps.length} open${kindNote} cap${caps.length === 1 ? '' : 's'}`); 119 + console.log(`tip: 'vit vouch <ref> --kind want' to signal demand`); 120 + console.log(` 'vit ship --recap <ref>' to ship an implementation`); 121 + } catch (err) { 122 + const msg = err instanceof Error ? err.message : String(err); 123 + if (opts.json) { 124 + jsonError(msg); 125 + return; 126 + } 127 + console.error(msg); 128 + process.exitCode = 1; 129 + } 130 + }); 131 + }
+123 -22
src/cmd/ship.js
··· 14 14 import { name } from '../lib/brand.js'; 15 15 import { resolvePds, listRecordsFromPds, batchQuery } from '../lib/pds.js'; 16 16 import { jsonOk, jsonError } from '../lib/json-output.js'; 17 + import { toBeacon } from '../lib/beacon.js'; 18 + import { hashTo3Words } from '../lib/cap-ref.js'; 19 + 20 + const STOP_WORDS = new Set([ 21 + 'a', 'an', 'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 22 + 'of', 'with', 'by', 'from', 'is', 'was', 'be', 'has', 'have', 'had', 23 + 'this', 'that', 'these', 'those', 'it', 'its', 'not', 'no', 'as', 'if', 'so', 24 + ]); 25 + 26 + function slugifyTitle(title) { 27 + const words = title.toLowerCase() 28 + .replace(/[^a-z\s]/g, '') 29 + .split(/\s+/) 30 + .filter(Boolean); 31 + const significant = words.filter(w => !STOP_WORDS.has(w)); 32 + const chosen = significant.length >= 3 ? significant.slice(0, 3) : words.slice(0, 3); 33 + return chosen.join('-'); 34 + } 35 + 36 + function generateRef(title, existingRefs) { 37 + const base = slugifyTitle(title); 38 + if (base && base.split('-').length >= 3 && !existingRefs.has(base)) { 39 + return base; 40 + } 41 + // Fall back to hash-based 3-word ref (always valid, collision-resistant) 42 + const hashed = hashTo3Words(title); 43 + if (!existingRefs.has(hashed)) return hashed; 44 + // Hash of title + timestamp to break hash collision 45 + const hashed2 = hashTo3Words(title + Date.now()); 46 + if (!existingRefs.has(hashed2)) return hashed2; 47 + return null; 48 + } 49 + 50 + function normalizeBeacon(input) { 51 + if (input.startsWith('vit:')) return input; 52 + return 'vit:' + toBeacon(input); 53 + } 17 54 18 55 function parseFrontmatter(text) { 19 56 const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); ··· 335 372 336 373 // preflight: beacon 337 374 const projectConfig = readProjectConfig(); 338 - if (!projectConfig.beacon) { 339 - if (opts.json) { 340 - jsonError('no beacon set', "run 'vit init' first"); 375 + const isRequest = opts.kind === 'request'; 376 + 377 + let beacon; 378 + if (isRequest) { 379 + // Request caps: --beacon flag or project config (in that order) 380 + if (opts.beacon) { 381 + try { 382 + beacon = normalizeBeacon(opts.beacon); 383 + } catch (err) { 384 + if (opts.json) { 385 + jsonError(`invalid --beacon: ${err.message}`); 386 + return; 387 + } 388 + console.error(`error: invalid --beacon: ${err.message}`); 389 + process.exitCode = 1; 390 + return; 391 + } 392 + } else if (projectConfig.beacon) { 393 + beacon = projectConfig.beacon; 394 + } else { 395 + if (opts.json) { 396 + jsonError('request caps must be addressed to a project', 'use --beacon <github-url> or run from a vit-initialized directory'); 397 + return; 398 + } 399 + console.error('error: request caps must be addressed to a project. use --beacon <github-url> or run from a vit-initialized directory.'); 400 + process.exitCode = 1; 401 + return; 402 + } 403 + } else { 404 + if (!projectConfig.beacon) { 405 + if (opts.json) { 406 + jsonError('no beacon set', "run 'vit init' first"); 407 + return; 408 + } 409 + console.error(`no beacon set. run '${name} init' in a project directory first.`); 410 + process.exitCode = 1; 341 411 return; 342 412 } 343 - console.error(`no beacon set. run '${name} init' in a project directory first.`); 344 - process.exitCode = 1; 345 - return; 413 + beacon = projectConfig.beacon; 346 414 } 347 - if (verbose) vlog(`[verbose] beacon: ${projectConfig.beacon}`); 415 + if (verbose) vlog(`[verbose] beacon: ${beacon}`); 348 416 349 417 let text; 350 418 try { ··· 352 420 } catch { 353 421 text = ''; 354 422 } 355 - if (!text) { 423 + if (!text && !isRequest) { 356 424 if (opts.json) { 357 425 jsonError('cap body is required via stdin'); 358 426 return; ··· 362 430 return; 363 431 } 364 432 365 - if (!REF_PATTERN.test(opts.ref)) { 433 + // ref: required for non-request caps; auto-generated for request caps 434 + let ref = opts.ref; 435 + if (!ref && isRequest) { 436 + const caps = readLog('caps.jsonl'); 437 + const existingRefs = new Set(caps.map(e => e.ref)); 438 + ref = generateRef(opts.title || '', existingRefs); 439 + if (!ref) { 440 + if (opts.json) { 441 + jsonError('could not auto-generate ref from title', 'provide --ref explicitly'); 442 + return; 443 + } 444 + console.error('error: could not auto-generate a 3-word ref from the title. provide --ref explicitly.'); 445 + process.exitCode = 1; 446 + return; 447 + } 448 + if (verbose || !opts.json) { 449 + vlog(`ref: ${ref}`); 450 + } 451 + } 452 + 453 + if (!REF_PATTERN.test(ref)) { 366 454 if (opts.json) { 367 455 jsonError('--ref must be exactly three lowercase words separated by dashes'); 368 456 return; ··· 393 481 } 394 482 395 483 if (opts.kind) { 396 - const validKinds = ['feat', 'fix', 'test', 'docs', 'refactor', 'chore', 'perf', 'style']; 484 + const validKinds = ['feat', 'fix', 'test', 'docs', 'refactor', 'chore', 'perf', 'style', 'request']; 397 485 if (!validKinds.includes(opts.kind)) { 398 486 if (opts.json) { 399 487 jsonError(`--kind must be one of: ${validKinds.join(', ')}`); ··· 461 549 462 550 const record = { 463 551 $type: CAP_COLLECTION, 464 - text, 552 + text: text || '', 465 553 title: opts.title, 466 554 description: opts.description, 467 - ref: opts.ref, 555 + ref, 468 556 createdAt: now, 469 557 }; 470 - if (projectConfig.beacon) record.beacon = projectConfig.beacon; 558 + if (beacon) record.beacon = beacon; 471 559 if (opts.kind) record.kind = opts.kind; 472 560 if (opts.recap) record.recap = { uri: recapUri, ref: opts.recap }; 473 561 const rkey = TID.nextStr(); ··· 486 574 ts: now, 487 575 did, 488 576 rkey, 489 - ref: opts.ref, 577 + ref, 490 578 collection: CAP_COLLECTION, 491 579 pds: session.serverMetadata?.issuer, 492 580 uri: putRes.data.uri, ··· 497 585 } 498 586 if (verbose) vlog(`[verbose] Log written to caps.jsonl`); 499 587 if (opts.json) { 500 - jsonOk({ ref: opts.ref, uri: putRes.data.uri }); 588 + const out = { ref, uri: putRes.data.uri }; 589 + if (opts.kind) out.kind = opts.kind; 590 + jsonOk(out); 501 591 return; 502 592 } 503 - console.log(`shipped: ${opts.ref}`); 504 - console.log(`uri: ${putRes.data.uri}`); 593 + if (isRequest) { 594 + console.log(`shipped: ${ref} (kind: request)`); 595 + console.log(`beacon: ${beacon}`); 596 + console.log(`anyone can implement this. share the ref to build demand.`); 597 + } else { 598 + console.log(`shipped: ${ref}`); 599 + console.log(`uri: ${putRes.data.uri}`); 600 + } 505 601 if (verbose) { 506 602 vlog( 507 603 JSON.stringify({ ··· 524 620 .option('--did <did>', 'DID to use (reads saved DID from config if not provided)') 525 621 .option('--title <title>', 'Short title for the cap') 526 622 .option('--description <description>', 'Description of the cap') 527 - .option('--ref <ref>', 'Three lowercase words with dashes (e.g. fast-cache-invalidation)') 623 + .option('--ref <ref>', 'Three lowercase words with dashes (e.g. fast-cache-invalidation); auto-generated from title when --kind request') 624 + .option('--beacon <beacon>', 'Beacon URI or GitHub URL for the cap (required when --kind request outside a vit-initialized dir)') 528 625 .option('--recap <ref>', 'Ref of the cap this derives from (quote-post semantics)') 529 - .option('--kind <kind>', 'Category: feat, fix, test, docs, refactor, chore, perf, style') 626 + .option('--kind <kind>', 'Category: feat, fix, test, docs, refactor, chore, perf, style, request') 530 627 .option('--skill <path>', 'Publish a skill directory (reads SKILL.md + resources)') 531 628 .option('--tags <tags>', 'Comma-separated discovery tags (for skills)') 532 629 .option('--version <version>', 'Version string (for skills, overrides frontmatter)') ··· 555 652 process.exitCode = 1; 556 653 return; 557 654 } 558 - if (!opts.ref) { 655 + if (!opts.ref && opts.kind !== 'request') { 559 656 if (opts.json) { 560 657 jsonError("required option '--ref <ref>' not specified"); 561 658 return; ··· 586 683 --description One sentence explaining what this cap does 587 684 --ref Three lowercase words with dashes (your-ref-name) 588 685 --recap <ref> Optional. Ref of the cap this derives from (links back to original) 589 - --kind <kind> Category: feat, fix, test, docs, refactor, chore, perf, style 590 - body (stdin) Full cap content, piped or via heredoc 686 + --kind <kind> Category: feat, fix, test, docs, refactor, chore, perf, style, request 687 + body (stdin) Full cap content, piped or via heredoc (optional when --kind request) 688 + 689 + Request caps (--kind request): 690 + --beacon <url> GitHub URL or vit: URI for the project being requested (auto-read from .vit if omitted) 691 + --ref Optional; auto-generated from title if not provided 591 692 592 693 Skill fields: 593 694 --skill <path> Path to skill directory containing SKILL.md
+5 -1
src/cmd/skim.js
··· 21 21 .option('--json', 'Output as JSON array') 22 22 .option('--caps', 'Show only caps') 23 23 .option('--skills', 'Show only skills') 24 + .option('--kind <kind>', 'Filter caps by kind (e.g. request, feat, fix)') 24 25 .option('-v, --verbose', 'Show step-by-step details') 25 26 .action(async (opts) => { 26 27 try { ··· 92 93 // Fetch caps (filtered by beacon) 93 94 if (wantCaps && beaconSet.size > 0) { 94 95 const res = await listRecordsFromPds(pds, repoDid, CAP_COLLECTION, 50); 95 - const caps = res.records.filter(r => beaconSet.has(r.value.beacon)); 96 + let caps = res.records.filter(r => beaconSet.has(r.value.beacon)); 97 + if (opts.kind) { 98 + caps = caps.filter(r => r.value.kind === opts.kind); 99 + } 96 100 if (verbose) console.log(`[verbose] ${repoDid}: ${res.records.length} caps, ${caps.length} matching beacon`); 97 101 for (const cap of caps) { 98 102 cap._handle = handleMap.get(repoDid) || repoDid;
+29 -5
src/cmd/vet.js
··· 374 374 return; 375 375 } 376 376 377 + const isRequestCap = record.kind === 'request'; 378 + 377 379 if (opts.trust) { 380 + if (isRequestCap) { 381 + if (opts.json) { 382 + jsonOk({ trusted: false, ref, uri: match.uri, note: 'request caps cannot be trusted — vouch with --kind want to signal demand' }); 383 + return; 384 + } 385 + console.log(`this is a request cap — there is nothing to trust or apply.`); 386 + console.log(`to signal demand, run:`); 387 + console.log(''); 388 + console.log(` vit vouch ${ref} --kind want`); 389 + return; 390 + } 378 391 appendLog('trusted.jsonl', { 379 392 ref, 380 393 uri: match.uri, ··· 394 407 const text = record.text || ''; 395 408 396 409 if (opts.json) { 397 - jsonOk({ ref, type: 'cap', author, title, description, text }); 410 + jsonOk({ ref, type: 'cap', author, title, description, text, ...(record.kind && { kind: record.kind }) }); 398 411 return; 399 412 } 400 413 401 414 console.log(`=== ${brand} cap review ===`); 402 - console.log('Review this cap carefully before trusting it.'); 415 + if (isRequestCap) { 416 + console.log('This is a request cap — review the need, then vouch to signal demand.'); 417 + } else { 418 + console.log('Review this cap carefully before trusting it.'); 419 + } 403 420 console.log(''); 404 421 console.log(` Ref: ${ref}`); 422 + if (record.kind) console.log(` Kind: ${record.kind}`); 405 423 if (title) console.log(` Title: ${title}`); 406 424 console.log(` Author: ${author}`); 407 425 if (description) { ··· 415 433 console.log('---'); 416 434 } 417 435 console.log(''); 418 - console.log('To trust this cap, run:'); 419 - console.log(''); 420 - console.log(` vit vet ${ref} --trust`); 436 + if (isRequestCap) { 437 + console.log('This is a request cap — review the need, then:'); 438 + console.log(''); 439 + console.log(` vit vouch ${ref} --kind want`); 440 + } else { 441 + console.log('To trust this cap, run:'); 442 + console.log(''); 443 + console.log(` vit vet ${ref} --trust`); 444 + } 421 445 } else { 422 446 // Skill vet — no beacon required 423 447 const skillName = nameFromSkillRef(ref);
+62 -34
src/cmd/vouch.js
··· 17 17 program 18 18 .command('vouch') 19 19 .argument('<ref>', 'Cap or skill reference (e.g. fast-cache-invalidation or skill-agent-test-patterns)') 20 - .description('Publicly endorse a vetted cap or skill') 20 + .description('Publicly endorse a vetted cap or skill, or signal demand with --kind want') 21 21 .option('--did <did>', 'DID to use') 22 + .option('--kind <kind>', 'Vouch intent: endorse (default, quality signal) or want (demand signal)') 22 23 .option('--json', 'Output as JSON') 23 24 .option('-v, --verbose', 'Show step-by-step details') 24 25 .action(async (ref, opts) => { ··· 26 27 const { verbose } = opts; 27 28 const vlog = opts.json ? (...a) => console.error(...a) : console.log; 28 29 const isSkill = isSkillRef(ref); 30 + 31 + // Validate --kind if provided 32 + if (opts.kind) { 33 + const validVouchKinds = ['endorse', 'want']; 34 + if (!validVouchKinds.includes(opts.kind)) { 35 + if (opts.json) { 36 + jsonError(`--kind must be one of: ${validVouchKinds.join(', ')}`); 37 + return; 38 + } 39 + console.error(`error: --kind must be one of: ${validVouchKinds.join(', ')}`); 40 + process.exitCode = 1; 41 + return; 42 + } 43 + } 29 44 30 45 // Validate ref format 31 46 if (isSkill) { ··· 159 174 } 160 175 console.log(`${mark} vouched: ${ref} (${match.uri})`); 161 176 } else { 162 - // Cap vouch — requires beacon (check beacon before trusted, matching original behavior) 177 + const vouchKind = opts.kind || 'endorse'; 178 + const isWant = vouchKind === 'want'; 179 + 180 + // Cap vouch — beacon required unless want-vouching (demand signal) 163 181 const beaconSet = readBeaconSet(); 164 - if (beaconSet.size === 0) { 165 - if (opts.json) { 166 - jsonError('no beacon set', "run 'vit init' first"); 182 + 183 + if (!isWant) { 184 + if (beaconSet.size === 0) { 185 + if (opts.json) { 186 + jsonError('no beacon set', "run 'vit init' first"); 187 + return; 188 + } 189 + console.error(`no beacon set. run '${name} init' in a project directory first.`); 190 + process.exitCode = 1; 167 191 return; 168 192 } 169 - console.error(`no beacon set. run '${name} init' in a project directory first.`); 170 - process.exitCode = 1; 171 - return; 172 - } 173 - if (verbose) vlog(`[verbose] beacons: ${[...beaconSet].join(', ')}`); 193 + if (verbose) vlog(`[verbose] beacons: ${[...beaconSet].join(', ')}`); 174 194 175 - const trusted = readLog('trusted.jsonl'); 176 - const trustedEntry = trusted.find(e => e.ref === ref); 177 - if (!trustedEntry) { 178 - if (opts.json) { 179 - jsonError(`cap '${ref}' is not yet vetted`, `run 'vit vet ${ref}' first`); 195 + const trusted = readLog('trusted.jsonl'); 196 + const trustedEntry = trusted.find(e => e.ref === ref); 197 + if (!trustedEntry) { 198 + if (opts.json) { 199 + jsonError(`cap '${ref}' is not yet vetted`, `run 'vit vet ${ref}' first`); 200 + return; 201 + } 202 + console.error(`cap '${ref}' is not yet vetted. vet it first:`); 203 + console.error(''); 204 + console.error(` vit vet ${ref}`); 205 + console.error(''); 206 + console.error('after reviewing, trust it with:'); 207 + console.error(''); 208 + console.error(` vit vet ${ref} --trust`); 209 + process.exitCode = 1; 180 210 return; 181 211 } 182 - console.error(`cap '${ref}' is not yet vetted. vet it first:`); 183 - console.error(''); 184 - console.error(` vit vet ${ref}`); 185 - console.error(''); 186 - console.error('after reviewing, trust it with:'); 187 - console.error(''); 188 - console.error(` vit vet ${ref} --trust`); 189 - process.exitCode = 1; 190 - return; 212 + if (verbose) vlog(`[verbose] trusted entry found, uri: ${trustedEntry.uri}`); 191 213 } 192 - if (verbose) vlog(`[verbose] trusted entry found, uri: ${trustedEntry.uri}`); 193 214 194 215 const { agent } = await restoreAgent(did); 195 216 if (verbose) vlog('[verbose] session restored'); ··· 207 228 let match = null; 208 229 for (const records of allRecords) { 209 230 for (const rec of records) { 210 - if (!beaconSet.has(rec.value.beacon)) continue; 231 + if (!isWant && !beaconSet.has(rec.value.beacon)) continue; 211 232 const recRef = resolveRef(rec.value, rec.cid); 212 233 if (recRef === ref) { 213 234 if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) { ··· 219 240 220 241 if (!match) { 221 242 if (opts.json) { 222 - jsonError(`no cap found with ref '${ref}' for this beacon`); 243 + jsonError(`no cap found with ref '${ref}'${!isWant ? ' for this beacon' : ''}`); 223 244 return; 224 245 } 225 - console.error(`no cap found with ref '${ref}' for this beacon.`); 246 + console.error(`no cap found with ref '${ref}'${!isWant ? ' for this beacon' : ''}.`); 226 247 console.error(''); 227 248 console.error('hint: caps only appear from accounts you follow and your own.'); 228 249 console.error(` vit following check who you're following`); ··· 232 253 } 233 254 234 255 const now = new Date().toISOString(); 235 - const projBeacon = [...beaconSet][0]; 256 + const projBeacon = beaconSet.size > 0 ? [...beaconSet][0] : (match.value.beacon || null); 236 257 const vouchRecord = { 237 258 $type: VOUCH_COLLECTION, 238 259 subject: { ··· 241 262 }, 242 263 createdAt: now, 243 264 ref, 244 - beacon: projBeacon, 265 + kind: vouchKind, 245 266 }; 246 - if (verbose) vlog(`[verbose] creating vouch for ${match.uri}`); 267 + if (projBeacon) vouchRecord.beacon = projBeacon; 268 + if (verbose) vlog(`[verbose] creating vouch (${vouchKind}) for ${match.uri}`); 247 269 const rkey = TID.nextStr(); 248 270 const res = await agent.com.atproto.repo.putRecord({ 249 271 repo: did, 250 272 collection: VOUCH_COLLECTION, 251 273 rkey, 252 274 record: vouchRecord, 253 - validate: true, 275 + validate: false, 254 276 }); 255 277 256 278 try { ··· 259 281 uri: match.uri, 260 282 cid: match.cid, 261 283 vouchUri: res.data.uri, 284 + kind: vouchKind, 262 285 beacon: projBeacon, 263 286 ts: now, 264 287 }); ··· 268 291 if (verbose) vlog('[verbose] logged to vouched.jsonl'); 269 292 270 293 if (opts.json) { 271 - jsonOk({ ref, uri: match.uri, vouchUri: res.data.uri }); 294 + jsonOk({ ref, uri: match.uri, vouchUri: res.data.uri, kind: vouchKind }); 272 295 return; 273 296 } 274 - console.log(`${mark} vouched: ${ref} (${match.uri})`); 297 + if (isWant) { 298 + console.log(`${mark} vouched (want): ${ref}`); 299 + console.log(` demand signal recorded. vouch count visible in vit explore vouches.`); 300 + } else { 301 + console.log(`${mark} vouched: ${ref} (${match.uri})`); 302 + } 275 303 } 276 304 } catch (err) { 277 305 const msg = err instanceof Error ? err.message : String(err);
+16
test/explore.test.js
··· 130 130 expect(data.error).toContain("no skill found with name 'nonexistent-skill-xyz'"); 131 131 }); 132 132 133 + test('caps --kind filter passes kind to API', () => { 134 + const result = run('explore caps --kind request --json --limit 2', '/tmp'); 135 + expect(result.exitCode).toBe(0); 136 + const data = JSON.parse(result.stdout); 137 + expect(data.ok).toBe(true); 138 + expect(Array.isArray(data.caps)).toBe(true); 139 + }); 140 + 141 + test('caps gracefully degrades on unreachable URL with --kind', () => { 142 + const result = run('explore caps --kind request --explore-url http://localhost:1 --json', '/tmp'); 143 + expect(result.exitCode).not.toBe(0); 144 + const data = JSON.parse(result.stdout); 145 + expect(data.ok).toBe(false); 146 + expect(data.error).toContain('unavailable'); 147 + }); 148 + 133 149 test('bare explore returns stats JSON', () => { 134 150 const result = run('explore --json', '/tmp'); 135 151 expect(result.exitCode).toBe(0);
+87
test/inbox.test.js
··· 1 + // SPDX-License-Identifier: MIT 2 + // Copyright (c) 2026 sol pbc 3 + 4 + import { describe, test, expect } from 'bun:test'; 5 + import { run } from './helpers.js'; 6 + import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; 7 + import { tmpdir } from 'node:os'; 8 + import { join } from 'node:path'; 9 + 10 + function makeVitDir(base, beacon) { 11 + const vitDir = join(base, '.vit'); 12 + mkdirSync(vitDir, { recursive: true }); 13 + writeFileSync(join(vitDir, 'config.json'), JSON.stringify({ beacon })); 14 + return base; 15 + } 16 + 17 + describe('vit inbox', () => { 18 + test('errors when no beacon set', () => { 19 + const tmp = join(tmpdir(), '.test-inbox-nobeacon-' + Math.random().toString(36).slice(2)); 20 + mkdirSync(tmp, { recursive: true }); 21 + const r = run('inbox', tmp); 22 + expect(r.exitCode).not.toBe(0); 23 + expect(r.stderr).toContain('no beacon set'); 24 + rmSync(tmp, { recursive: true, force: true }); 25 + }); 26 + 27 + test('--json errors when no beacon set', () => { 28 + const tmp = join(tmpdir(), '.test-inbox-nobeacon-json-' + Math.random().toString(36).slice(2)); 29 + mkdirSync(tmp, { recursive: true }); 30 + const r = run('inbox --json', tmp); 31 + expect(r.exitCode).not.toBe(0); 32 + const data = JSON.parse(r.stdout); 33 + expect(data.ok).toBe(false); 34 + expect(data.error).toContain('no beacon set'); 35 + rmSync(tmp, { recursive: true, force: true }); 36 + }); 37 + 38 + test('--json returns caps array when beacon set', () => { 39 + const tmp = join(tmpdir(), '.test-inbox-json-' + Math.random().toString(36).slice(2)); 40 + makeVitDir(tmp, 'vit:github.com/solpbc/vit'); 41 + const r = run('inbox --json', tmp); 42 + expect(r.exitCode).toBe(0); 43 + const data = JSON.parse(r.stdout); 44 + expect(data.ok).toBe(true); 45 + expect(Array.isArray(data.caps)).toBe(true); 46 + rmSync(tmp, { recursive: true, force: true }); 47 + }); 48 + 49 + test('--json with --kind filters caps', () => { 50 + const tmp = join(tmpdir(), '.test-inbox-kind-' + Math.random().toString(36).slice(2)); 51 + makeVitDir(tmp, 'vit:github.com/solpbc/vit'); 52 + const r = run('inbox --kind request --json', tmp); 53 + expect(r.exitCode).toBe(0); 54 + const data = JSON.parse(r.stdout); 55 + expect(data.ok).toBe(true); 56 + expect(Array.isArray(data.caps)).toBe(true); 57 + rmSync(tmp, { recursive: true, force: true }); 58 + }); 59 + 60 + test('gracefully degrades when explore API unavailable', () => { 61 + const tmp = join(tmpdir(), '.test-inbox-unavail-' + Math.random().toString(36).slice(2)); 62 + makeVitDir(tmp, 'vit:github.com/solpbc/vit'); 63 + const r = run('inbox --explore-url http://localhost:1', tmp); 64 + expect(r.exitCode).not.toBe(0); 65 + expect(r.stderr).toContain('unavailable'); 66 + rmSync(tmp, { recursive: true, force: true }); 67 + }); 68 + 69 + test('gracefully degrades when explore API unavailable --json', () => { 70 + const tmp = join(tmpdir(), '.test-inbox-unavail-json-' + Math.random().toString(36).slice(2)); 71 + makeVitDir(tmp, 'vit:github.com/solpbc/vit'); 72 + const r = run('inbox --explore-url http://localhost:1 --json', tmp); 73 + expect(r.exitCode).not.toBe(0); 74 + const data = JSON.parse(r.stdout); 75 + expect(data.ok).toBe(false); 76 + expect(data.error).toContain('unavailable'); 77 + rmSync(tmp, { recursive: true, force: true }); 78 + }); 79 + 80 + test('shows help', () => { 81 + const r = run('inbox --help'); 82 + expect(r.exitCode).toBe(0); 83 + expect(r.stdout).toContain('inbox'); 84 + expect(r.stdout).toContain('--kind'); 85 + expect(r.stdout).toContain('--sort'); 86 + }); 87 + });
+132
test/ship-request.test.js
··· 1 + // SPDX-License-Identifier: MIT 2 + // Copyright (c) 2026 sol pbc 3 + 4 + import { describe, test, expect } from 'bun:test'; 5 + import { run } from './helpers.js'; 6 + import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; 7 + import { tmpdir } from 'node:os'; 8 + import { join } from 'node:path'; 9 + 10 + const agentEnv = { CLAUDECODE: '1' }; 11 + 12 + function makeVitDir(base, beacon) { 13 + const vitDir = join(base, '.vit'); 14 + mkdirSync(vitDir, { recursive: true }); 15 + writeFileSync(join(vitDir, 'config.json'), JSON.stringify({ beacon })); 16 + return base; 17 + } 18 + 19 + describe('vit ship --kind request', () => { 20 + test('accepts request as a valid kind value (errors at auth, not validation)', () => { 21 + const tmp = join(tmpdir(), '.test-ship-req-' + Math.random().toString(36).slice(2)); 22 + makeVitDir(tmp, 'vit:github.com/test/project'); 23 + const r = run( 24 + 'ship --kind request --title "Add async validation" --description "Need async validators" --did did:plc:abc', 25 + tmp, 26 + agentEnv, 27 + '', 28 + ); 29 + expect(r.stderr).not.toMatch(/--kind must be one of/i); 30 + rmSync(tmp, { recursive: true, force: true }); 31 + }); 32 + 33 + test('rejects request without beacon when not in vit-initialized dir', () => { 34 + const tmp = join(tmpdir(), '.test-ship-req-nobeacon-' + Math.random().toString(36).slice(2)); 35 + mkdirSync(tmp, { recursive: true }); 36 + const r = run( 37 + 'ship --kind request --title "Add async validation" --description "Need async validators" --did did:plc:abc', 38 + tmp, 39 + agentEnv, 40 + '', 41 + ); 42 + expect(r.exitCode).not.toBe(0); 43 + expect(r.stderr).toContain('request caps must be addressed to a project'); 44 + rmSync(tmp, { recursive: true, force: true }); 45 + }); 46 + 47 + test('accepts --beacon flag for request caps outside vit dir', () => { 48 + const tmp = join(tmpdir(), '.test-ship-req-beacon-' + Math.random().toString(36).slice(2)); 49 + mkdirSync(tmp, { recursive: true }); 50 + const r = run( 51 + 'ship --kind request --title "Add async validation" --description "Need async validators" --beacon github.com/pydantic/pydantic --did did:plc:abc', 52 + tmp, 53 + agentEnv, 54 + '', 55 + ); 56 + // Should not fail on beacon validation — will fail at auth 57 + expect(r.stderr).not.toContain('request caps must be addressed to a project'); 58 + expect(r.stderr).not.toMatch(/--kind must be one of/i); 59 + rmSync(tmp, { recursive: true, force: true }); 60 + }); 61 + 62 + test('normalizes github.com URL to vit: beacon', () => { 63 + const tmp = join(tmpdir(), '.test-ship-req-norm-' + Math.random().toString(36).slice(2)); 64 + mkdirSync(tmp, { recursive: true }); 65 + const r = run( 66 + 'ship --kind request --title "Add async validation" --description "Need async validators" --beacon github.com/pydantic/pydantic --did did:plc:abc', 67 + tmp, 68 + agentEnv, 69 + '', 70 + ); 71 + // Beacon normalization succeeds; error is at auth 72 + expect(r.stderr).not.toContain('invalid --beacon'); 73 + rmSync(tmp, { recursive: true, force: true }); 74 + }); 75 + 76 + test('auto-generates ref from title when --ref not provided', () => { 77 + const tmp = join(tmpdir(), '.test-ship-req-autoref-' + Math.random().toString(36).slice(2)); 78 + makeVitDir(tmp, 'vit:github.com/test/project'); 79 + const r = run( 80 + 'ship --kind request --title "Add async validation support" --description "Need async validators" --did did:plc:abc', 81 + tmp, 82 + agentEnv, 83 + '', 84 + ); 85 + // Should not fail on missing --ref 86 + expect(r.stderr).not.toMatch(/--ref.*not specified/i); 87 + // Auto-generated ref is printed (fails at auth, not ref generation) 88 + expect(r.stderr).not.toMatch(/three lowercase words/i); 89 + rmSync(tmp, { recursive: true, force: true }); 90 + }); 91 + 92 + test('requires --title for ref auto-generation', () => { 93 + const tmp = join(tmpdir(), '.test-ship-req-notitle-' + Math.random().toString(36).slice(2)); 94 + makeVitDir(tmp, 'vit:github.com/test/project'); 95 + const r = run( 96 + 'ship --kind request --description "Need async validators" --did did:plc:abc', 97 + tmp, 98 + agentEnv, 99 + '', 100 + ); 101 + // Missing --title is caught before ref generation 102 + expect(r.exitCode).not.toBe(0); 103 + expect(r.stderr).toMatch(/--title/i); 104 + rmSync(tmp, { recursive: true, force: true }); 105 + }); 106 + 107 + test('text is optional for request caps (empty stdin does not error)', () => { 108 + const tmp = join(tmpdir(), '.test-ship-req-notext-' + Math.random().toString(36).slice(2)); 109 + makeVitDir(tmp, 'vit:github.com/test/project'); 110 + const r = run( 111 + 'ship --kind request --title "Add async validation support" --description "Need async validators" --did did:plc:abc', 112 + tmp, 113 + agentEnv, 114 + '', 115 + ); 116 + // Should not error on empty stdin for request caps 117 + expect(r.stderr).not.toMatch(/body is required/i); 118 + rmSync(tmp, { recursive: true, force: true }); 119 + }); 120 + 121 + test('--help shows request in --kind options', () => { 122 + const r = run('ship --help'); 123 + expect(r.exitCode).toBe(0); 124 + expect(r.stdout).toContain('request'); 125 + }); 126 + 127 + test('--help shows --beacon option', () => { 128 + const r = run('ship --help'); 129 + expect(r.exitCode).toBe(0); 130 + expect(r.stdout).toContain('--beacon'); 131 + }); 132 + });
+6
test/skim.test.js
··· 28 28 expect(result.exitCode).not.toBe(0); 29 29 expect(result.stderr).toContain('no beacon set'); 30 30 }); 31 + 32 + test('--kind flag is accepted (shown in help)', () => { 33 + const result = run('skim --help'); 34 + expect(result.exitCode).toBe(0); 35 + expect(result.stdout).toContain('--kind'); 36 + }); 31 37 });
+22
test/vouch.test.js
··· 39 39 expect(result.stderr).toContain('no beacon set'); 40 40 }); 41 41 42 + test('--kind want is accepted (does not error on kind validation)', () => { 43 + const result = run('vouch fast-cache-invalidation --kind want --did did:plc:test123', '/tmp'); 44 + // Should not fail on kind validation — will fail at network lookup 45 + expect(result.stderr).not.toContain('--kind must be one of'); 46 + }); 47 + 48 + test('--kind endorse is accepted', () => { 49 + const result = run('vouch fast-cache-invalidation --kind endorse --did did:plc:test123', '/tmp'); 50 + expect(result.stderr).not.toContain('--kind must be one of'); 51 + }); 52 + 53 + test('rejects invalid --kind value', () => { 54 + const result = run('vouch fast-cache-invalidation --kind badkind --did did:plc:test123', '/tmp'); 55 + expect(result.exitCode).not.toBe(0); 56 + expect(result.stderr).toContain('--kind must be one of'); 57 + }); 58 + 59 + test('shows --kind in help', () => { 60 + const result = run('vouch --help'); 61 + expect(result.stdout).toContain('--kind'); 62 + }); 63 + 42 64 test('works from both agent and non-agent contexts', () => { 43 65 const inAgent = run('vouch fast-cache-invalidation --did did:plc:test123', '/tmp', { CLAUDECODE: '1' }); 44 66 expect(inAgent.stderr).not.toContain('must be run by a human');