diff --git a/.agents/workflow.md b/.agents/workflow.md index 9f95f7f16..d9a1e868a 100644 --- a/.agents/workflow.md +++ b/.agents/workflow.md @@ -9,8 +9,7 @@ This file expands [AGENTS.md](../AGENTS.md) for day-to-day repo work: tracker ha - Keep private launcher names, local paths, session aliases, dispatch policy, and operator workspace details outside this public repository. - GitHub remains the PR, CI, review, and merge surface. Use GitHub Issues or Projects for external collaboration only when the user or operator explicitly asks for that workflow. - Do not add repo-local tracker directories, tracker JSONL exports, dispatch logs, cross-repo research records, or operator decision records to AgentV commits unless the user explicitly asks for repository-local tracker artifacts. -- The only repo-local Beads files intentionally tracked are `.beads/config.yaml`, `.beads/metadata.json`, and `.beads/.gitignore`. Never commit the embedded Dolt database, JSONL exports, backups, locks, logs, or runtime state. -- Do not commit project-local coordination config files. The safe Beads defaults above are the exception. +- Do not commit project-local coordination config files. - Do not use `git stash` on shared checkouts. Inspect `git status`, stage only your files, use a dedicated worktree, or ask before moving uncommitted changes. ## Worktree Setup @@ -20,6 +19,7 @@ This file expands [AGENTS.md](../AGENTS.md) for day-to-day repo work: tracker ha - When working in the primary checkout, stage explicit paths only. Do not commit another agent's files, project-local coordination config, generated evidence, or unrelated tracker or doc state. - Use a dedicated git worktree based on the latest `origin/main` for non-trivial, risky, cross-cutting, long-running, or parallel implementation, or whenever the primary checkout is stale or dirty in paths you need. - Before starting implementation in a dedicated worktree, verify its `HEAD` is based on the current `origin/main` commit. +- Before Beads bootstrap, copy `.beads/config.yaml.example` to `.beads/config.yaml` and run `bun run beads:check`. Keep `.beads/metadata.json` tracked; it preserves AgentV's `av` embedded Dolt identity. See [docs/runbooks/beads-worktree-recovery.md](../docs/runbooks/beads-worktree-recovery.md). Manual setup: diff --git a/.beads/.gitignore b/.beads/.gitignore index cda3a0ae5..a7e032465 100644 --- a/.beads/.gitignore +++ b/.beads/.gitignore @@ -73,7 +73,13 @@ backup/ *.db-shm db.sqlite bd.db + +# Checkout-local config. Copy config.yaml.example when using Beads. +config.yaml + +# metadata.json is intentionally tracked. It preserves the embedded Dolt +# database identity for AgentV's av Beads workspace. + # NOTE: Do NOT add negation patterns here. # They would override fork protection in .git/info/exclude. -# Config files (metadata.json, config.yaml) are tracked by git by default -# since no pattern above ignores them. +# config.yaml.example is the tracked template for the AgentV Beads remote. diff --git a/.beads/config.yaml b/.beads/config.yaml.example similarity index 86% rename from .beads/config.yaml rename to .beads/config.yaml.example index 900c258d2..51da45582 100644 --- a/.beads/config.yaml +++ b/.beads/config.yaml.example @@ -1,4 +1,6 @@ # Beads Configuration File +# Copy this file to .beads/config.yaml when using Beads in this checkout. +# Keep .beads/config.yaml local; do not commit it. # This file configures default behavior for all bd commands in this repository # All settings can also be set via environment variables (BD_* prefix) # or overridden with command-line flags @@ -53,4 +55,7 @@ # - github.org # - github.repo +# AgentV code lives in EntityProcess/agentv. Beads coordination data lives in +# EntityProcess/agentv-beads. Keep both Beads remotes pointed at agentv-beads. +sync.remote: "git+https://github.com/EntityProcess/agentv-beads.git" federation.remote: "git+https://github.com/EntityProcess/agentv-beads.git" diff --git a/AGENTS.md b/AGENTS.md index 02f407f23..3810c54ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,6 +72,7 @@ Common entry points: - Product or architecture decisions: start with [STRATEGY.md](STRATEGY.md), [ROADMAP.md](ROADMAP.md), and [.agents/product-boundary.md](.agents/product-boundary.md). - Tracker, worktree, or PR flow questions: read [.agents/workflow.md](.agents/workflow.md). +- Beads bootstrap or recovery questions: read [docs/runbooks/beads-worktree-recovery.md](docs/runbooks/beads-worktree-recovery.md). - Dashboard, docs, CLI UX, or grader verification work: read [.agents/verification.md](.agents/verification.md). - Wire-format, naming, or grader-type changes: read [.agents/conventions.md](.agents/conventions.md). - Version bumps or npm publishing: read [.agents/publish.md](.agents/publish.md). diff --git a/docs/runbooks/beads-worktree-recovery.md b/docs/runbooks/beads-worktree-recovery.md new file mode 100644 index 000000000..909c36fab --- /dev/null +++ b/docs/runbooks/beads-worktree-recovery.md @@ -0,0 +1,83 @@ +# AgentV Beads Worktree Recovery + +AgentV keeps code and tracker data in separate repositories: + +- Code repository: `EntityProcess/agentv` +- Beads coordination repository: `EntityProcess/agentv-beads` + +Do not point Beads, Dolt, bootstrap, or federation data at the code repository. +The tracked `.beads/metadata.json` preserves AgentV's embedded Dolt identity: +database `av` and project `a7aea826-0087-45fc-93f5-9084e9924e8b`. + +`.beads/config.yaml` is checkout-local. Copy `.beads/config.yaml.example` +before running `bd bootstrap`; the example pins both `sync.remote` and +`federation.remote` to `EntityProcess/agentv-beads`. + +## Preflight + +Run the guard before `bd bootstrap`, `bd dolt push`, or `bd federation sync`: + +```bash +bun run beads:check +``` + +For static repository checks only: + +```bash +bun run beads:check -- --skip-bd +``` + +For a disposable fresh-bootstrap fixture: + +```bash +bun run beads:check -- --fixture +``` + +A healthy setup reports database `av`, project +`a7aea826-0087-45fc-93f5-9084e9924e8b`, and bootstrap `sync_remote` +`git+https://github.com/EntityProcess/agentv-beads.git`. + +## Fresh Worktree + +After creating an AgentV worktree: + +```bash +bun install +cp "$(git worktree list --porcelain | head -1 | sed 's/worktree //')/.env" .env +cp .beads/config.yaml.example .beads/config.yaml +bun run beads:check +bd bootstrap --dry-run +bd bootstrap +``` + +Stop if `bd bootstrap --dry-run` plans to sync from +`git+https://github.com/EntityProcess/agentv.git` or database `beads`. + +## Recovery + +If `bd --readonly context --json` reports database `beads`, no project ID, or a +project ID other than `a7aea826-0087-45fc-93f5-9084e9924e8b`, restore the tracked +identity and local config first: + +```bash +git restore -- .beads/metadata.json +cp .beads/config.yaml.example .beads/config.yaml +bun run beads:check +``` + +If the Dolt origin or bootstrap plan still points at the code repository, +re-point the Dolt remote before pushing: + +```bash +bd dolt remote list +bd dolt remote remove origin +bd dolt remote add origin git+https://github.com/EntityProcess/agentv-beads.git +bd bootstrap --dry-run +bd bootstrap +bd federation status +``` + +Do not delete `.beads/embeddeddolt/`, `.beads/dolt/`, or backups as the first +recovery step. They may contain local coordination data. If the guard still +reports a metadata or project mismatch after restoring metadata and re-pointing +the remote, preserve the checkout and hand the guard output to the coordinator. diff --git a/package.json b/package.json index 53c575408..a87fd2468 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "test:watch": "bun --filter @agentv/core test:watch & bun --filter agentv test:watch", "agentv": "bun apps/cli/src/cli.ts", "agentv:buildrun": "bun run build && bun apps/cli/dist/cli.js", + "beads:check": "bun scripts/check-beads-context.ts", "validate:examples": "EVAL_CRITERIA=placeholder CUSTOM_SYSTEM_PROMPT=placeholder bun scripts/validate-example-evals.ts", "eval:baseline-check": "bun scripts/check-eval-baselines.ts", "release": "bun scripts/release.ts", diff --git a/scripts/check-beads-context.ts b/scripts/check-beads-context.ts new file mode 100644 index 000000000..fec931f03 --- /dev/null +++ b/scripts/check-beads-context.ts @@ -0,0 +1,461 @@ +#!/usr/bin/env bun +/** + * Read-only guard for AgentV's public code repo vs private Beads repo split. + * + * Usage: + * bun scripts/check-beads-context.ts + * bun scripts/check-beads-context.ts --skip-bd + * bun scripts/check-beads-context.ts --fixture + */ + +import { + chmodSync, + copyFileSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { parse as parseYaml } from 'yaml'; + +const root = resolve(import.meta.dirname, '..'); +const expectedCodeRepo = 'EntityProcess/agentv'; +const expectedBeadsRepo = 'EntityProcess/agentv-beads'; +const expectedBeadsRemote = `git+https://github.com/${expectedBeadsRepo}.git`; +const expectedDatabase = 'av'; +const expectedProjectId = 'a7aea826-0087-45fc-93f5-9084e9924e8b'; + +const gitignorePath = '.beads/.gitignore'; +const configExamplePath = '.beads/config.yaml.example'; +const configPath = '.beads/config.yaml'; +const metadataPath = '.beads/metadata.json'; + +type Level = 'OK' | 'WARN' | 'ERROR'; + +interface CommandResult { + readonly exitCode: number; + readonly stdout: string; + readonly stderr: string; +} + +interface Finding { + readonly level: Level; + readonly message: string; + readonly detail?: string; + readonly fix?: string; +} + +interface BeadsMetadata { + readonly backend?: string; + readonly database?: string; + readonly dolt_database?: string; + readonly dolt_mode?: string; + readonly project_id?: string; +} + +interface BdContext { + readonly database?: string; + readonly project_id?: string; + readonly repo_root?: string; + readonly beads_dir?: string; +} + +interface BootstrapPlan { + readonly action?: string; + readonly database?: string; + readonly reason?: string; + readonly sync_remote?: string; +} + +const decoder = new TextDecoder(); +const args = new Set(process.argv.slice(2)); +const findings: Finding[] = []; + +if (args.has('--help') || args.has('-h')) { + console.log(`Usage: bun scripts/check-beads-context.ts [--skip-bd] [--fixture] + +Checks that AgentV's code checkout uses ${expectedCodeRepo} while Beads +coordination uses ${expectedBeadsRepo}. + +Default mode checks the current checkout and asks bd for read-only context plus +bootstrap dry-run output. Copy ${configExamplePath} to ${configPath} before +running default mode in a new checkout. + +Options: + --skip-bd Only inspect tracked files and local config files. + --fixture Build a disposable git repo, copy the Beads template to local + config, and verify bd fresh-bootstrap identity and sync remote.`); + process.exit(0); +} + +const skipBd = args.has('--skip-bd'); +const runFixture = args.has('--fixture'); + +function record(level: Level, message: string, detail?: string, fix?: string): void { + findings.push({ level, message, detail, fix }); +} + +function run(command: string, commandArgs: readonly string[], cwd = root): CommandResult { + try { + const result = Bun.spawnSync([command, ...commandArgs], { + cwd, + stdout: 'pipe', + stderr: 'pipe', + }); + + return { + exitCode: result.exitCode, + stdout: decoder.decode(result.stdout).trim(), + stderr: decoder.decode(result.stderr).trim(), + }; + } catch (error) { + return { + exitCode: 127, + stdout: '', + stderr: error instanceof Error ? error.message : String(error), + }; + } +} + +function parseJsonOutput(result: CommandResult, label: string): T | undefined { + if (result.exitCode !== 0) { + record('ERROR', `${label} failed`, combinedOutput(result)); + return undefined; + } + + try { + return JSON.parse(result.stdout) as T; + } catch (error) { + record('ERROR', `${label} returned non-JSON output`, combinedOutput(result, String(error))); + return undefined; + } +} + +function combinedOutput(result: CommandResult, extra?: string): string { + return [result.stdout, result.stderr, extra].filter(Boolean).join('\n'); +} + +function readJsonFile(relativePath: string): T | undefined { + try { + return JSON.parse(readFileSync(resolve(root, relativePath), 'utf8')) as T; + } catch (error) { + record('ERROR', `${relativePath} is not valid JSON`, String(error)); + return undefined; + } +} + +function readYamlFile(relativePath: string): Record | undefined { + try { + const value = parseYaml(readFileSync(resolve(root, relativePath), 'utf8')) as unknown; + if (value && typeof value === 'object' && !Array.isArray(value)) { + return value as Record; + } + + record('ERROR', `${relativePath} must contain a YAML object`); + return undefined; + } catch (error) { + record('ERROR', `${relativePath} is not valid YAML`, String(error)); + return undefined; + } +} + +function configValue(config: Record, key: 'federation.remote' | 'sync.remote') { + const direct = stringValue(config[key]); + if (direct) return direct; + + const [section, field] = key.split('.'); + const nested = config[section]; + if (nested && typeof nested === 'object' && !Array.isArray(nested)) { + return stringValue((nested as Record)[field]); + } + + return undefined; +} + +function stringValue(value: unknown): string | undefined { + return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined; +} + +function repoSlug(remote: string | undefined): string | undefined { + if (!remote) return undefined; + + const clean = remote + .trim() + .replace(/^git\+/, '') + .replace(/\/$/, '') + .replace(/\.git$/, ''); + const sshMatch = clean.match(/^git@github\.com:(?[^/]+)\/(?[^/]+)$/); + if (sshMatch?.groups) return `${sshMatch.groups.owner}/${sshMatch.groups.repo}`; + + const httpsMatch = clean.match(/^https:\/\/github\.com\/(?[^/]+)\/(?[^/]+)$/); + if (httpsMatch?.groups) return `${httpsMatch.groups.owner}/${httpsMatch.groups.repo}`; + + return undefined; +} + +function checkTracked(path: string): boolean { + const result = run('git', ['ls-files', '--error-unmatch', path]); + if (result.exitCode === 0) { + record('OK', `${path} is tracked`); + return true; + } + + record('ERROR', `${path} is not tracked`); + return false; +} + +function checkNotTracked(path: string): void { + const result = run('git', ['ls-files', '--error-unmatch', path]); + if (result.exitCode === 0) { + record('ERROR', `${path} is tracked but must stay checkout-local`); + } else { + record('OK', `${path} is checkout-local`); + } +} + +function checkIgnored(path: string): void { + const result = run('git', ['check-ignore', '-q', path]); + if (result.exitCode === 0) { + record('OK', `${path} is ignored`); + } else { + record( + 'WARN', + `${path} is not ignored`, + undefined, + `Add ${path.replace('.beads/', '')} to ${gitignorePath}.`, + ); + } +} + +function checkNotIgnored(path: string): void { + const result = run('git', ['check-ignore', '-q', path]); + if (result.exitCode === 0) { + record( + 'ERROR', + `${path} is ignored but must remain versioned`, + undefined, + `Remove ${path.replace('.beads/', '')} from ${gitignorePath}.`, + ); + } else { + record('OK', `${path} is not ignored`); + } +} + +function checkMetadata(): void { + if (!checkTracked(metadataPath)) return; + checkNotIgnored(metadataPath); + + const metadata = readJsonFile(metadataPath); + if (!metadata) return; + + if ( + metadata.backend === 'dolt' && + metadata.database === 'dolt' && + metadata.dolt_mode === 'embedded' && + metadata.dolt_database === expectedDatabase && + metadata.project_id === expectedProjectId + ) { + record('OK', `${metadataPath} preserves database ${expectedDatabase}`); + return; + } + + record( + 'ERROR', + `${metadataPath} does not match AgentV Beads identity`, + JSON.stringify(metadata, null, 2), + `Run: git restore -- ${metadataPath}`, + ); +} + +function checkConfigFile(relativePath: string, required: boolean): void { + if (!existsSync(resolve(root, relativePath))) { + record( + required ? 'ERROR' : 'WARN', + `${relativePath} is missing`, + undefined, + required ? undefined : `Run: cp ${configExamplePath} ${configPath}`, + ); + return; + } + + const config = readYamlFile(relativePath); + if (!config) return; + + for (const key of ['sync.remote', 'federation.remote'] as const) { + const remote = configValue(config, key); + const slug = repoSlug(remote); + if (slug === expectedBeadsRepo) { + record('OK', `${relativePath} ${key} points at ${expectedBeadsRepo}`); + } else { + record( + 'ERROR', + `${relativePath} ${key} must point at ${expectedBeadsRepo}`, + remote, + `Set ${key}: "${expectedBeadsRemote}"`, + ); + } + } +} + +function checkConfig(): void { + checkTracked(gitignorePath); + checkTracked(configExamplePath); + checkConfigFile(configExamplePath, true); + + checkNotTracked(configPath); + checkIgnored(configPath); + checkConfigFile(configPath, false); +} + +function checkRepoSplit(): void { + const origin = run('git', ['remote', 'get-url', 'origin']); + if (origin.exitCode !== 0) { + record('WARN', 'git origin is not configured', combinedOutput(origin)); + return; + } + + const originSlug = repoSlug(origin.stdout); + if (originSlug === expectedCodeRepo) { + record('OK', `git origin is ${expectedCodeRepo}`); + } else if (originSlug === expectedBeadsRepo) { + record( + 'ERROR', + 'git origin points at the Beads coordination repo', + origin.stdout, + `Set origin back to https://github.com/${expectedCodeRepo}.git`, + ); + } else { + record('WARN', `git origin is ${originSlug ?? origin.stdout}`); + } +} + +function checkBdCurrent(): void { + const context = parseJsonOutput( + run('bd', ['--readonly', 'context', '--json']), + 'bd context --json', + ); + if (context) checkContextIdentity(context, 'current bd context'); + + if (context?.repo_root && resolve(context.repo_root) !== root) { + record( + 'WARN', + 'bd context is using a Beads directory outside this checkout', + `repo_root: ${context.repo_root}\nbeads_dir: ${context.beads_dir ?? 'unknown'}`, + `Use --fixture to verify this branch template. If operating on the shared Beads directory, copy ${configExamplePath} to that checkout's ${configPath}.`, + ); + return; + } + + const bootstrap = parseJsonOutput( + run('bd', ['--readonly', 'bootstrap', '--dry-run', '--json']), + 'bd bootstrap --dry-run --json', + ); + if (bootstrap) checkBootstrapPlan(bootstrap, 'current bootstrap dry-run'); +} + +function checkFixture(): void { + const fixture = mkdtempSync(join(tmpdir(), 'agentv-beads-context-')); + + try { + run('git', ['init', '-q', fixture]); + run('git', [ + '-C', + fixture, + 'remote', + 'add', + 'origin', + `https://github.com/${expectedCodeRepo}.git`, + ]); + mkdirSync(join(fixture, '.beads'), { recursive: true }); + chmodSync(join(fixture, '.beads'), 0o700); + copyFileSync(resolve(root, gitignorePath), join(fixture, gitignorePath)); + copyFileSync(resolve(root, metadataPath), join(fixture, metadataPath)); + copyFileSync(resolve(root, configExamplePath), join(fixture, configPath)); + + const context = parseJsonOutput( + run('bd', ['-C', fixture, '--readonly', 'context', '--json']), + 'fixture bd context --json', + ); + if (context) checkContextIdentity(context, 'fixture bd context'); + + const bootstrap = parseJsonOutput( + run('bd', ['-C', fixture, '--readonly', 'bootstrap', '--dry-run', '--json']), + 'fixture bd bootstrap --dry-run --json', + ); + if (bootstrap) checkBootstrapPlan(bootstrap, 'fixture bootstrap dry-run'); + } finally { + rmSync(fixture, { force: true, recursive: true }); + } +} + +function checkContextIdentity(context: BdContext, label: string): void { + if (context.database === expectedDatabase && context.project_id === expectedProjectId) { + record('OK', `${label} uses database ${expectedDatabase} and expected project ID`); + return; + } + + record( + 'ERROR', + `${label} does not use AgentV Beads identity`, + JSON.stringify(context, null, 2), + `Restore ${metadataPath}, copy ${configExamplePath} to ${configPath}, then run bd bootstrap --dry-run before any push.`, + ); +} + +function checkBootstrapPlan(plan: BootstrapPlan, label: string): void { + const syncSlug = repoSlug(plan.sync_remote); + const databaseOk = plan.database === expectedDatabase; + const remoteOk = syncSlug === expectedBeadsRepo; + + if (databaseOk && remoteOk) { + record('OK', `${label} uses ${expectedDatabase} from ${expectedBeadsRepo}`); + return; + } + + record( + 'ERROR', + `${label} would not bootstrap from AgentV Beads`, + JSON.stringify(plan, null, 2), + `Copy ${configExamplePath} to ${configPath} and keep sync.remote at "${expectedBeadsRemote}".`, + ); +} + +function printFindings(): void { + console.log('AgentV Beads context preflight\n'); + + for (const finding of findings) { + console.log(`${finding.level}: ${finding.message}`); + if (finding.detail) console.log(indent(finding.detail)); + if (finding.fix) console.log(indent(`Fix: ${finding.fix}`)); + } + + const errorCount = findings.filter((finding) => finding.level === 'ERROR').length; + const warningCount = findings.filter((finding) => finding.level === 'WARN').length; + console.log(`\n${errorCount} error(s), ${warningCount} warning(s)`); +} + +function indent(text: string): string { + return text + .split('\n') + .map((line) => ` ${line}`) + .join('\n'); +} + +checkMetadata(); +checkConfig(); +checkRepoSplit(); + +if (!skipBd && !runFixture) { + checkBdCurrent(); +} + +if (runFixture) { + checkFixture(); +} + +printFindings(); + +process.exit(findings.some((finding) => finding.level === 'ERROR') ? 1 : 0);