From 55480f3cf240a92ffb4df332b1ac41de1ab54bb2 Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Sun, 21 Jun 2026 05:36:19 +0200 Subject: [PATCH 1/6] fix(beads): harden worktree identity checks --- .agents/workflow.md | 4 +- .beads/.gitignore | 7 +- .beads/metadata.json | 7 - AGENTS.md | 1 + docs/runbooks/beads-worktree-recovery.md | 99 ++++++ package.json | 1 + scripts/check-beads-context.ts | 385 +++++++++++++++++++++++ 7 files changed, 494 insertions(+), 10 deletions(-) delete mode 100644 .beads/metadata.json create mode 100644 docs/runbooks/beads-worktree-recovery.md create mode 100644 scripts/check-beads-context.ts diff --git a/.agents/workflow.md b/.agents/workflow.md index 9f95f7f16..6d42bcbdc 100644 --- a/.agents/workflow.md +++ b/.agents/workflow.md @@ -9,7 +9,8 @@ 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. +- The only repo-local Beads files intentionally tracked are `.beads/config.yaml` and `.beads/.gitignore`. `.beads/metadata.json` is checkout-local identity state. Never commit or copy it, the embedded Dolt database, JSONL exports, backups, locks, logs, or runtime state. +- AgentV code lives in the public `EntityProcess/agentv` repository. Beads coordination data lives in the private `EntityProcess/agentv-beads` repository. Run `bun scripts/check-beads-context.ts` before `bd bootstrap`, `bd dolt push`, or `bd federation sync` in a new checkout or worktree, and stop if the check reports that Beads data would sync from the code repo. - Do not commit project-local coordination config files. The safe Beads defaults above are the exception. - 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. @@ -38,6 +39,7 @@ cp "$(git worktree list --porcelain | head -1 | sed 's/worktree //')/.env" .env ``` - Both steps are required before running builds, tests, or evals in the worktree. +- Do not copy `.beads/metadata.json` or embedded `.beads/` runtime state into worktrees. The tracked `.beads/config.yaml` points at `EntityProcess/agentv-beads`; run `bun scripts/check-beads-context.ts`, then `bd bootstrap --dry-run`, then `bd bootstrap` so checkout-local Beads identity is created in place. - If you discover you are on a stale base or have uncoordinated dirty files, stop and fix that before changing code. - Whenever you `git checkout`, `gh pr checkout`, `git pull`, or otherwise switch to a ref that may have changed `package.json` or `bun.lock`, run `bun install` before building or testing. diff --git a/.beads/.gitignore b/.beads/.gitignore index cda3a0ae5..edb84a5ed 100644 --- a/.beads/.gitignore +++ b/.beads/.gitignore @@ -73,7 +73,10 @@ backup/ *.db-shm db.sqlite bd.db + +# Checkout-local identity. `bd bootstrap` recreates this per checkout. +metadata.json + # 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 stays tracked so each checkout knows the AgentV Beads remote. diff --git a/.beads/metadata.json b/.beads/metadata.json deleted file mode 100644 index 94ac64bae..000000000 --- a/.beads/metadata.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "database": "dolt", - "backend": "dolt", - "dolt_mode": "embedded", - "dolt_database": "av", - "project_id": "a7aea826-0087-45fc-93f5-9084e9924e8b" -} diff --git a/AGENTS.md b/AGENTS.md index 02f407f23..884c2cc13 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,6 +36,7 @@ Read the full rationale and examples in [.agents/product-boundary.md](.agents/pr - Start every repo change with `git fetch origin` and `git status --short --branch`. - Use `bun` for package and script operations. - Use the operator-supplied tracker when present. Do not commit tracker runtime state, local coordination config, or other machine-local artifacts. +- AgentV code lives in `EntityProcess/agentv`; Beads coordination data lives in `EntityProcess/agentv-beads`. Never point Beads or Dolt data at the public code repo, and never commit `.beads/metadata.json`. - Do not use `git stash` on shared checkouts. Stage explicit paths only, and never push directly to `main`. - Every merge to `main` requires a GitHub pull request with passing GitHub Actions. Do not locally merge feature or integration branches into `main` as a substitute for opening a PR. - Prefer the primary checkout only for small, clean, bounded work. Use a dedicated worktree from the latest `origin/main` for non-trivial, risky, long-running, or parallel changes. diff --git a/docs/runbooks/beads-worktree-recovery.md b/docs/runbooks/beads-worktree-recovery.md new file mode 100644 index 000000000..e289fe1f3 --- /dev/null +++ b/docs/runbooks/beads-worktree-recovery.md @@ -0,0 +1,99 @@ +# AgentV Beads Worktree Recovery + +AgentV uses two different repositories: + +- Public code: `EntityProcess/agentv` +- Private coordination Beads data: `EntityProcess/agentv-beads` + +Do not point Beads or Dolt data at the public code repo. The committed +`.beads/config.yaml` is the repo-owned pointer to the Beads federation remote. +`.beads/metadata.json`, embedded Dolt data, locks, JSONL exports, and other +runtime files are checkout-local and must not be copied between worktrees or +committed. + +## Preflight + +Run this before `bd bootstrap`, `bd dolt push`, or `bd federation sync` in a +new checkout or worktree: + +```bash +bun scripts/check-beads-context.ts +``` + +For a file-only check that cannot open `bd`: + +```bash +bun scripts/check-beads-context.ts --skip-bd +``` + +For a deeper local diagnostic that asks `bd` to inspect federation status and +Dolt remotes in readonly mode: + +```bash +bun scripts/check-beads-context.ts --deep +``` + +A healthy setup has: + +```bash +git remote get-url origin +# https://github.com/EntityProcess/agentv.git + +rg '^federation\.remote:' .beads/config.yaml +# federation.remote: "git+https://github.com/EntityProcess/agentv-beads.git" + +bd --readonly context --json +bd --readonly bootstrap --dry-run --json +``` + +`bd bootstrap --dry-run` must not report a `sync_remote` under +`EntityProcess/agentv.git`. If it does, stop before pushing or bootstrapping +and recover the Dolt remote first. + +## Fresh Worktree Rule + +Do not copy `.beads/metadata.json` or `.beads/embeddeddolt/` from another +checkout into a worktree. Let `bd bootstrap` create checkout-local identity +state after the Beads remote has been verified. + +For a newly created AgentV worktree: + +```bash +bun install +cp "$(git worktree list --porcelain | head -1 | sed 's/worktree //')/.env" .env +bun scripts/check-beads-context.ts +bd bootstrap --dry-run +bd bootstrap +``` + +If the preflight reports that `bd bootstrap` would sync from +`EntityProcess/agentv.git`, do not run `bd bootstrap` yet. + +## Re-point A Wrong Dolt Remote + +Use these commands when `bd dolt remote list`, `bd bootstrap --dry-run`, or +`bd federation status` shows Beads data pointed at the public code repo: + +```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 +``` + +If `bd federation status` reports a project identity or `project_id` mismatch, +remove copied checkout-local metadata before re-running bootstrap: + +```bash +rm -f .beads/metadata.json +bd bootstrap --dry-run +bd bootstrap +bd federation status +``` + +Do not delete `.beads/embeddeddolt/`, `.beads/dolt/`, or backups as a first +step. Those may contain local tracker data. If re-pointing plus bootstrap does +not clear the mismatch, stop and hand the checkout to the coordinator with the +preflight output. 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..6537e8335 --- /dev/null +++ b/scripts/check-beads-context.ts @@ -0,0 +1,385 @@ +#!/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 --deep + */ + +import { existsSync, readFileSync } from 'node:fs'; +import { 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 metadataPath = '.beads/metadata.json'; +const configPath = '.beads/config.yaml'; + +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; +} + +const decoder = new TextDecoder(); +const args = new Set(process.argv.slice(2)); + +if (args.has('--help') || args.has('-h')) { + console.log(`Usage: bun scripts/check-beads-context.ts [--skip-bd] [--deep] + +Checks that AgentV's code checkout uses ${expectedCodeRepo} while Beads +coordination uses ${expectedBeadsRepo}. The default mode is read-only and runs +bd context plus bd bootstrap --dry-run when bd is installed. + +Options: + --skip-bd Only inspect git and committed .beads config files. + --deep Also run bd federation status and bd dolt remote list in readonly mode.`); + process.exit(0); +} + +const skipBd = args.has('--skip-bd'); +const deep = args.has('--deep'); +const findings: Finding[] = []; + +function record(level: Level, message: string, detail?: string, fix?: string): void { + findings.push({ level, message, detail, fix }); +} + +function run(command: string, commandArgs: readonly string[]): CommandResult { + try { + const result = Bun.spawnSync([command, ...commandArgs], { + cwd: root, + 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('WARN', `${label} failed`, combinedOutput(result)); + return undefined; + } + + try { + return JSON.parse(result.stdout) as T; + } catch (error) { + record('WARN', `${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 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 sameRepo(left: string | undefined, right: string | undefined): boolean { + const leftSlug = repoSlug(left); + const rightSlug = repoSlug(right); + return Boolean(leftSlug && rightSlug && leftSlug === rightSlug); +} + +function stringValue(value: unknown): string | undefined { + return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined; +} + +function readFederationRemote(): string | undefined { + if (!existsSync(resolve(root, configPath))) { + record( + 'ERROR', + `${configPath} is missing`, + undefined, + `Restore ${configPath} with federation.remote: "${expectedBeadsRemote}"`, + ); + return undefined; + } + + const raw = readFileSync(resolve(root, configPath), 'utf8'); + let parsed: Record | null; + + try { + const value = parseYaml(raw) as unknown; + parsed = + value && typeof value === 'object' && !Array.isArray(value) + ? (value as Record) + : null; + } catch (error) { + record('ERROR', `${configPath} is not valid YAML`, String(error)); + return undefined; + } + + const nested = parsed?.federation; + return ( + stringValue(parsed?.['federation.remote']) ?? + (nested && typeof nested === 'object' + ? stringValue((nested as Record).remote) + : undefined) + ); +} + +function checkTrackedBeadsFiles(): void { + const configTracked = run('git', ['ls-files', '--error-unmatch', configPath]); + if (configTracked.exitCode === 0) { + record('OK', `${configPath} is tracked`); + } else { + record('ERROR', `${configPath} is not tracked`); + } + + const metadataTracked = run('git', ['ls-files', '--error-unmatch', metadataPath]); + if (metadataTracked.exitCode === 0) { + record( + 'ERROR', + `${metadataPath} is tracked but must be checkout-local`, + undefined, + `Run: git rm --cached ${metadataPath}`, + ); + } else { + record('OK', `${metadataPath} is not tracked`); + } + + const metadataIgnored = run('git', ['check-ignore', '-q', metadataPath]); + if (metadataIgnored.exitCode === 0) { + record('OK', `${metadataPath} is ignored for future bootstraps`); + } else { + record( + 'ERROR', + `${metadataPath} is not ignored`, + undefined, + `Add ${metadataPath.replace('.beads/', '')} to .beads/.gitignore`, + ); + } +} + +function checkRepoSplit(federationRemote: string | undefined): void { + const gitOrigin = run('git', ['remote', 'get-url', 'origin']); + const gitOriginUrl = gitOrigin.exitCode === 0 ? gitOrigin.stdout : undefined; + const gitOriginRepo = repoSlug(gitOriginUrl); + const federationRepo = repoSlug(federationRemote); + + if (!gitOriginUrl) { + record('WARN', 'git origin is not configured', combinedOutput(gitOrigin)); + } else if (gitOriginRepo === expectedBeadsRepo) { + record( + 'ERROR', + 'git origin points at the Beads coordination repo', + gitOriginUrl, + `Set the code repo origin back to https://github.com/${expectedCodeRepo}.git`, + ); + } else { + record('OK', `git origin is ${gitOriginRepo ?? gitOriginUrl}`); + } + + if (!federationRemote) { + record( + 'ERROR', + 'Beads federation.remote is missing', + undefined, + `Set ${configPath} to federation.remote: "${expectedBeadsRemote}"`, + ); + } else if (federationRepo !== expectedBeadsRepo) { + record( + 'ERROR', + 'Beads federation.remote does not point at agentv-beads', + federationRemote, + `Set ${configPath} to federation.remote: "${expectedBeadsRemote}"`, + ); + } else { + record('OK', `Beads federation.remote is ${federationRepo}`); + } + + if (gitOriginRepo && federationRepo && gitOriginRepo === federationRepo) { + record( + 'ERROR', + 'git origin and Beads federation.remote point at the same repository', + `git origin: ${gitOriginUrl}\nfederation.remote: ${federationRemote}`, + `AgentV code stays in ${expectedCodeRepo}; Beads data stays in ${expectedBeadsRepo}.`, + ); + } else if (gitOriginRepo && federationRepo) { + record('OK', 'code repo and Beads repo are split'); + } +} + +function checkBdContext(federationRemote: string | undefined): void { + const context = parseJsonOutput<{ + readonly beads_dir?: string; + readonly cwd_repo_root?: string; + readonly is_worktree?: boolean; + readonly project_id?: string; + readonly repo_root?: string; + }>(run('bd', ['--readonly', 'context', '--json']), 'bd context --json'); + + if (context?.project_id) { + record('OK', `bd context project_id is ${context.project_id}`); + } + + if (context?.is_worktree && context.beads_dir) { + record('OK', `bd worktree context uses beads_dir ${context.beads_dir}`); + } + + const bootstrap = parseJsonOutput<{ + readonly action?: string; + readonly reason?: string; + readonly sync_remote?: string; + }>( + run('bd', ['--readonly', 'bootstrap', '--dry-run', '--json']), + 'bd bootstrap --dry-run --json', + ); + + const bootstrapRemote = bootstrap?.sync_remote; + if (!bootstrapRemote) { + record('WARN', 'bd bootstrap dry-run did not report a sync_remote', bootstrap?.reason); + return; + } + + if (federationRemote && !sameRepo(bootstrapRemote, federationRemote)) { + record( + 'ERROR', + 'bd bootstrap would sync from a remote that differs from federation.remote', + `bootstrap sync_remote: ${bootstrapRemote}\nfederation.remote: ${federationRemote}\nreason: ${ + bootstrap?.reason ?? 'unknown' + }`, + `Do not run bd bootstrap or bd dolt push from this checkout until the Dolt remote is pointed at ${expectedBeadsRemote}.`, + ); + return; + } + + if (repoSlug(bootstrapRemote) === expectedCodeRepo) { + record( + 'ERROR', + 'bd bootstrap would sync Beads data from the public code repo', + bootstrapRemote, + `Expected Beads data remote: ${expectedBeadsRemote}`, + ); + return; + } + + record( + 'OK', + `bd bootstrap dry-run sync_remote is ${repoSlug(bootstrapRemote) ?? bootstrapRemote}`, + ); +} + +function checkDeepBdState(): void { + const federationStatus = run('bd', ['--readonly', 'federation', 'status']); + const federationText = combinedOutput(federationStatus); + + if (federationStatus.exitCode !== 0) { + const looksLikeIdentityMismatch = /identity mismatch|project[_ -]?id|metadata/i.test( + federationText, + ); + record( + looksLikeIdentityMismatch ? 'ERROR' : 'WARN', + 'bd federation status failed', + federationText, + looksLikeIdentityMismatch + ? `Re-point Dolt origin to ${expectedBeadsRemote}, remove copied ${metadataPath}, then run bd bootstrap --dry-run before bd bootstrap.` + : undefined, + ); + } else if (/identity mismatch|project[_ -]?id mismatch/i.test(federationText)) { + record( + 'ERROR', + 'bd federation status reports project identity drift', + federationText, + `Re-point Dolt origin to ${expectedBeadsRemote}, remove copied ${metadataPath}, then run bd bootstrap --dry-run before bd bootstrap.`, + ); + } else { + record('OK', 'bd federation status completed'); + } + + const doltRemoteList = run('bd', ['--readonly', 'dolt', 'remote', 'list']); + const doltText = combinedOutput(doltRemoteList); + if (doltRemoteList.exitCode !== 0) { + record('WARN', 'bd dolt remote list failed', doltText); + } else if (doltText.includes(expectedCodeRepo) && !doltText.includes(expectedBeadsRepo)) { + record( + 'ERROR', + 'Dolt origin appears to point at the public code repo', + doltText, + `Run: bd dolt remote remove origin\nThen: bd dolt remote add origin ${expectedBeadsRemote}`, + ); + } else { + record('OK', 'bd dolt remote list does not expose the code repo as the only remote'); + } +} + +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'); +} + +const federationRemote = readFederationRemote(); + +checkTrackedBeadsFiles(); +checkRepoSplit(federationRemote); + +if (skipBd) { + record('WARN', 'skipped bd diagnostics'); +} else { + checkBdContext(federationRemote); +} + +if (deep) { + checkDeepBdState(); +} + +printFindings(); + +process.exit(findings.some((finding) => finding.level === 'ERROR') ? 1 : 0); From 9860c245f3a3e35b42df1d9d332833b512f720a9 Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Sun, 21 Jun 2026 06:58:58 +0200 Subject: [PATCH 2/6] docs(beads): defer workflow details to global skill --- .agents/workflow.md | 4 +- AGENTS.md | 2 +- docs/runbooks/beads-worktree-recovery.md | 99 ------ package.json | 1 - scripts/check-beads-context.ts | 385 ----------------------- 5 files changed, 2 insertions(+), 489 deletions(-) delete mode 100644 docs/runbooks/beads-worktree-recovery.md delete mode 100644 scripts/check-beads-context.ts diff --git a/.agents/workflow.md b/.agents/workflow.md index 6d42bcbdc..36a482580 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` and `.beads/.gitignore`. `.beads/metadata.json` is checkout-local identity state. Never commit or copy it, the embedded Dolt database, JSONL exports, backups, locks, logs, or runtime state. -- AgentV code lives in the public `EntityProcess/agentv` repository. Beads coordination data lives in the private `EntityProcess/agentv-beads` repository. Run `bun scripts/check-beads-context.ts` before `bd bootstrap`, `bd dolt push`, or `bd federation sync` in a new checkout or worktree, and stop if the check reports that Beads data would sync from the code repo. +- If using Beads, follow the global Beads skill. The only repo-local Beads files intentionally tracked are `.beads/config.yaml` and `.beads/.gitignore`; `.beads/metadata.json` and runtime state stay checkout-local. - Do not commit project-local coordination config files. The safe Beads defaults above are the exception. - 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. @@ -39,7 +38,6 @@ cp "$(git worktree list --porcelain | head -1 | sed 's/worktree //')/.env" .env ``` - Both steps are required before running builds, tests, or evals in the worktree. -- Do not copy `.beads/metadata.json` or embedded `.beads/` runtime state into worktrees. The tracked `.beads/config.yaml` points at `EntityProcess/agentv-beads`; run `bun scripts/check-beads-context.ts`, then `bd bootstrap --dry-run`, then `bd bootstrap` so checkout-local Beads identity is created in place. - If you discover you are on a stale base or have uncoordinated dirty files, stop and fix that before changing code. - Whenever you `git checkout`, `gh pr checkout`, `git pull`, or otherwise switch to a ref that may have changed `package.json` or `bun.lock`, run `bun install` before building or testing. diff --git a/AGENTS.md b/AGENTS.md index 884c2cc13..5770fb71a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,7 @@ Read the full rationale and examples in [.agents/product-boundary.md](.agents/pr - Start every repo change with `git fetch origin` and `git status --short --branch`. - Use `bun` for package and script operations. - Use the operator-supplied tracker when present. Do not commit tracker runtime state, local coordination config, or other machine-local artifacts. -- AgentV code lives in `EntityProcess/agentv`; Beads coordination data lives in `EntityProcess/agentv-beads`. Never point Beads or Dolt data at the public code repo, and never commit `.beads/metadata.json`. +- If using Beads, follow the global Beads skill. AgentV Beads data belongs in `EntityProcess/agentv-beads`; never commit `.beads/metadata.json` or Beads runtime state. - Do not use `git stash` on shared checkouts. Stage explicit paths only, and never push directly to `main`. - Every merge to `main` requires a GitHub pull request with passing GitHub Actions. Do not locally merge feature or integration branches into `main` as a substitute for opening a PR. - Prefer the primary checkout only for small, clean, bounded work. Use a dedicated worktree from the latest `origin/main` for non-trivial, risky, long-running, or parallel changes. diff --git a/docs/runbooks/beads-worktree-recovery.md b/docs/runbooks/beads-worktree-recovery.md deleted file mode 100644 index e289fe1f3..000000000 --- a/docs/runbooks/beads-worktree-recovery.md +++ /dev/null @@ -1,99 +0,0 @@ -# AgentV Beads Worktree Recovery - -AgentV uses two different repositories: - -- Public code: `EntityProcess/agentv` -- Private coordination Beads data: `EntityProcess/agentv-beads` - -Do not point Beads or Dolt data at the public code repo. The committed -`.beads/config.yaml` is the repo-owned pointer to the Beads federation remote. -`.beads/metadata.json`, embedded Dolt data, locks, JSONL exports, and other -runtime files are checkout-local and must not be copied between worktrees or -committed. - -## Preflight - -Run this before `bd bootstrap`, `bd dolt push`, or `bd federation sync` in a -new checkout or worktree: - -```bash -bun scripts/check-beads-context.ts -``` - -For a file-only check that cannot open `bd`: - -```bash -bun scripts/check-beads-context.ts --skip-bd -``` - -For a deeper local diagnostic that asks `bd` to inspect federation status and -Dolt remotes in readonly mode: - -```bash -bun scripts/check-beads-context.ts --deep -``` - -A healthy setup has: - -```bash -git remote get-url origin -# https://github.com/EntityProcess/agentv.git - -rg '^federation\.remote:' .beads/config.yaml -# federation.remote: "git+https://github.com/EntityProcess/agentv-beads.git" - -bd --readonly context --json -bd --readonly bootstrap --dry-run --json -``` - -`bd bootstrap --dry-run` must not report a `sync_remote` under -`EntityProcess/agentv.git`. If it does, stop before pushing or bootstrapping -and recover the Dolt remote first. - -## Fresh Worktree Rule - -Do not copy `.beads/metadata.json` or `.beads/embeddeddolt/` from another -checkout into a worktree. Let `bd bootstrap` create checkout-local identity -state after the Beads remote has been verified. - -For a newly created AgentV worktree: - -```bash -bun install -cp "$(git worktree list --porcelain | head -1 | sed 's/worktree //')/.env" .env -bun scripts/check-beads-context.ts -bd bootstrap --dry-run -bd bootstrap -``` - -If the preflight reports that `bd bootstrap` would sync from -`EntityProcess/agentv.git`, do not run `bd bootstrap` yet. - -## Re-point A Wrong Dolt Remote - -Use these commands when `bd dolt remote list`, `bd bootstrap --dry-run`, or -`bd federation status` shows Beads data pointed at the public code repo: - -```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 -``` - -If `bd federation status` reports a project identity or `project_id` mismatch, -remove copied checkout-local metadata before re-running bootstrap: - -```bash -rm -f .beads/metadata.json -bd bootstrap --dry-run -bd bootstrap -bd federation status -``` - -Do not delete `.beads/embeddeddolt/`, `.beads/dolt/`, or backups as a first -step. Those may contain local tracker data. If re-pointing plus bootstrap does -not clear the mismatch, stop and hand the checkout to the coordinator with the -preflight output. diff --git a/package.json b/package.json index a87fd2468..53c575408 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "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 deleted file mode 100644 index 6537e8335..000000000 --- a/scripts/check-beads-context.ts +++ /dev/null @@ -1,385 +0,0 @@ -#!/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 --deep - */ - -import { existsSync, readFileSync } from 'node:fs'; -import { 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 metadataPath = '.beads/metadata.json'; -const configPath = '.beads/config.yaml'; - -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; -} - -const decoder = new TextDecoder(); -const args = new Set(process.argv.slice(2)); - -if (args.has('--help') || args.has('-h')) { - console.log(`Usage: bun scripts/check-beads-context.ts [--skip-bd] [--deep] - -Checks that AgentV's code checkout uses ${expectedCodeRepo} while Beads -coordination uses ${expectedBeadsRepo}. The default mode is read-only and runs -bd context plus bd bootstrap --dry-run when bd is installed. - -Options: - --skip-bd Only inspect git and committed .beads config files. - --deep Also run bd federation status and bd dolt remote list in readonly mode.`); - process.exit(0); -} - -const skipBd = args.has('--skip-bd'); -const deep = args.has('--deep'); -const findings: Finding[] = []; - -function record(level: Level, message: string, detail?: string, fix?: string): void { - findings.push({ level, message, detail, fix }); -} - -function run(command: string, commandArgs: readonly string[]): CommandResult { - try { - const result = Bun.spawnSync([command, ...commandArgs], { - cwd: root, - 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('WARN', `${label} failed`, combinedOutput(result)); - return undefined; - } - - try { - return JSON.parse(result.stdout) as T; - } catch (error) { - record('WARN', `${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 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 sameRepo(left: string | undefined, right: string | undefined): boolean { - const leftSlug = repoSlug(left); - const rightSlug = repoSlug(right); - return Boolean(leftSlug && rightSlug && leftSlug === rightSlug); -} - -function stringValue(value: unknown): string | undefined { - return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined; -} - -function readFederationRemote(): string | undefined { - if (!existsSync(resolve(root, configPath))) { - record( - 'ERROR', - `${configPath} is missing`, - undefined, - `Restore ${configPath} with federation.remote: "${expectedBeadsRemote}"`, - ); - return undefined; - } - - const raw = readFileSync(resolve(root, configPath), 'utf8'); - let parsed: Record | null; - - try { - const value = parseYaml(raw) as unknown; - parsed = - value && typeof value === 'object' && !Array.isArray(value) - ? (value as Record) - : null; - } catch (error) { - record('ERROR', `${configPath} is not valid YAML`, String(error)); - return undefined; - } - - const nested = parsed?.federation; - return ( - stringValue(parsed?.['federation.remote']) ?? - (nested && typeof nested === 'object' - ? stringValue((nested as Record).remote) - : undefined) - ); -} - -function checkTrackedBeadsFiles(): void { - const configTracked = run('git', ['ls-files', '--error-unmatch', configPath]); - if (configTracked.exitCode === 0) { - record('OK', `${configPath} is tracked`); - } else { - record('ERROR', `${configPath} is not tracked`); - } - - const metadataTracked = run('git', ['ls-files', '--error-unmatch', metadataPath]); - if (metadataTracked.exitCode === 0) { - record( - 'ERROR', - `${metadataPath} is tracked but must be checkout-local`, - undefined, - `Run: git rm --cached ${metadataPath}`, - ); - } else { - record('OK', `${metadataPath} is not tracked`); - } - - const metadataIgnored = run('git', ['check-ignore', '-q', metadataPath]); - if (metadataIgnored.exitCode === 0) { - record('OK', `${metadataPath} is ignored for future bootstraps`); - } else { - record( - 'ERROR', - `${metadataPath} is not ignored`, - undefined, - `Add ${metadataPath.replace('.beads/', '')} to .beads/.gitignore`, - ); - } -} - -function checkRepoSplit(federationRemote: string | undefined): void { - const gitOrigin = run('git', ['remote', 'get-url', 'origin']); - const gitOriginUrl = gitOrigin.exitCode === 0 ? gitOrigin.stdout : undefined; - const gitOriginRepo = repoSlug(gitOriginUrl); - const federationRepo = repoSlug(federationRemote); - - if (!gitOriginUrl) { - record('WARN', 'git origin is not configured', combinedOutput(gitOrigin)); - } else if (gitOriginRepo === expectedBeadsRepo) { - record( - 'ERROR', - 'git origin points at the Beads coordination repo', - gitOriginUrl, - `Set the code repo origin back to https://github.com/${expectedCodeRepo}.git`, - ); - } else { - record('OK', `git origin is ${gitOriginRepo ?? gitOriginUrl}`); - } - - if (!federationRemote) { - record( - 'ERROR', - 'Beads federation.remote is missing', - undefined, - `Set ${configPath} to federation.remote: "${expectedBeadsRemote}"`, - ); - } else if (federationRepo !== expectedBeadsRepo) { - record( - 'ERROR', - 'Beads federation.remote does not point at agentv-beads', - federationRemote, - `Set ${configPath} to federation.remote: "${expectedBeadsRemote}"`, - ); - } else { - record('OK', `Beads federation.remote is ${federationRepo}`); - } - - if (gitOriginRepo && federationRepo && gitOriginRepo === federationRepo) { - record( - 'ERROR', - 'git origin and Beads federation.remote point at the same repository', - `git origin: ${gitOriginUrl}\nfederation.remote: ${federationRemote}`, - `AgentV code stays in ${expectedCodeRepo}; Beads data stays in ${expectedBeadsRepo}.`, - ); - } else if (gitOriginRepo && federationRepo) { - record('OK', 'code repo and Beads repo are split'); - } -} - -function checkBdContext(federationRemote: string | undefined): void { - const context = parseJsonOutput<{ - readonly beads_dir?: string; - readonly cwd_repo_root?: string; - readonly is_worktree?: boolean; - readonly project_id?: string; - readonly repo_root?: string; - }>(run('bd', ['--readonly', 'context', '--json']), 'bd context --json'); - - if (context?.project_id) { - record('OK', `bd context project_id is ${context.project_id}`); - } - - if (context?.is_worktree && context.beads_dir) { - record('OK', `bd worktree context uses beads_dir ${context.beads_dir}`); - } - - const bootstrap = parseJsonOutput<{ - readonly action?: string; - readonly reason?: string; - readonly sync_remote?: string; - }>( - run('bd', ['--readonly', 'bootstrap', '--dry-run', '--json']), - 'bd bootstrap --dry-run --json', - ); - - const bootstrapRemote = bootstrap?.sync_remote; - if (!bootstrapRemote) { - record('WARN', 'bd bootstrap dry-run did not report a sync_remote', bootstrap?.reason); - return; - } - - if (federationRemote && !sameRepo(bootstrapRemote, federationRemote)) { - record( - 'ERROR', - 'bd bootstrap would sync from a remote that differs from federation.remote', - `bootstrap sync_remote: ${bootstrapRemote}\nfederation.remote: ${federationRemote}\nreason: ${ - bootstrap?.reason ?? 'unknown' - }`, - `Do not run bd bootstrap or bd dolt push from this checkout until the Dolt remote is pointed at ${expectedBeadsRemote}.`, - ); - return; - } - - if (repoSlug(bootstrapRemote) === expectedCodeRepo) { - record( - 'ERROR', - 'bd bootstrap would sync Beads data from the public code repo', - bootstrapRemote, - `Expected Beads data remote: ${expectedBeadsRemote}`, - ); - return; - } - - record( - 'OK', - `bd bootstrap dry-run sync_remote is ${repoSlug(bootstrapRemote) ?? bootstrapRemote}`, - ); -} - -function checkDeepBdState(): void { - const federationStatus = run('bd', ['--readonly', 'federation', 'status']); - const federationText = combinedOutput(federationStatus); - - if (federationStatus.exitCode !== 0) { - const looksLikeIdentityMismatch = /identity mismatch|project[_ -]?id|metadata/i.test( - federationText, - ); - record( - looksLikeIdentityMismatch ? 'ERROR' : 'WARN', - 'bd federation status failed', - federationText, - looksLikeIdentityMismatch - ? `Re-point Dolt origin to ${expectedBeadsRemote}, remove copied ${metadataPath}, then run bd bootstrap --dry-run before bd bootstrap.` - : undefined, - ); - } else if (/identity mismatch|project[_ -]?id mismatch/i.test(federationText)) { - record( - 'ERROR', - 'bd federation status reports project identity drift', - federationText, - `Re-point Dolt origin to ${expectedBeadsRemote}, remove copied ${metadataPath}, then run bd bootstrap --dry-run before bd bootstrap.`, - ); - } else { - record('OK', 'bd federation status completed'); - } - - const doltRemoteList = run('bd', ['--readonly', 'dolt', 'remote', 'list']); - const doltText = combinedOutput(doltRemoteList); - if (doltRemoteList.exitCode !== 0) { - record('WARN', 'bd dolt remote list failed', doltText); - } else if (doltText.includes(expectedCodeRepo) && !doltText.includes(expectedBeadsRepo)) { - record( - 'ERROR', - 'Dolt origin appears to point at the public code repo', - doltText, - `Run: bd dolt remote remove origin\nThen: bd dolt remote add origin ${expectedBeadsRemote}`, - ); - } else { - record('OK', 'bd dolt remote list does not expose the code repo as the only remote'); - } -} - -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'); -} - -const federationRemote = readFederationRemote(); - -checkTrackedBeadsFiles(); -checkRepoSplit(federationRemote); - -if (skipBd) { - record('WARN', 'skipped bd diagnostics'); -} else { - checkBdContext(federationRemote); -} - -if (deep) { - checkDeepBdState(); -} - -printFindings(); - -process.exit(findings.some((finding) => finding.level === 'ERROR') ? 1 : 0); From 6f9192bd752b390ffb4252b2b1b96b52dd75a6a8 Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Sun, 21 Jun 2026 08:52:28 +0200 Subject: [PATCH 3/6] chore(beads): keep live config local --- .agents/workflow.md | 4 ++-- .beads/.gitignore | 5 +++-- .beads/{config.yaml => config.yaml.example} | 2 ++ AGENTS.md | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) rename .beads/{config.yaml => config.yaml.example} (94%) diff --git a/.agents/workflow.md b/.agents/workflow.md index 36a482580..ed265ceb9 100644 --- a/.agents/workflow.md +++ b/.agents/workflow.md @@ -9,8 +9,8 @@ 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. -- If using Beads, follow the global Beads skill. The only repo-local Beads files intentionally tracked are `.beads/config.yaml` and `.beads/.gitignore`; `.beads/metadata.json` and runtime state stay checkout-local. -- Do not commit project-local coordination config files. The safe Beads defaults above are the exception. +- If using Beads, follow the global Beads skill. The only repo-local Beads files intentionally tracked are `.beads/config.yaml.example` and `.beads/.gitignore`; `.beads/config.yaml`, `.beads/metadata.json`, and runtime state stay checkout-local. +- Do not commit project-local coordination config files. The Beads template above is the exception. - 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 diff --git a/.beads/.gitignore b/.beads/.gitignore index edb84a5ed..317f9c8b8 100644 --- a/.beads/.gitignore +++ b/.beads/.gitignore @@ -74,9 +74,10 @@ backup/ db.sqlite bd.db -# Checkout-local identity. `bd bootstrap` recreates this per checkout. +# Checkout-local config and identity. Copy config.yaml.example when using Beads. +config.yaml metadata.json # NOTE: Do NOT add negation patterns here. # They would override fork protection in .git/info/exclude. -# config.yaml stays tracked so each checkout knows the AgentV Beads remote. +# 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 94% rename from .beads/config.yaml rename to .beads/config.yaml.example index 900c258d2..4ff5a45d3 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 diff --git a/AGENTS.md b/AGENTS.md index 5770fb71a..55a43e8dc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,7 @@ Read the full rationale and examples in [.agents/product-boundary.md](.agents/pr - Start every repo change with `git fetch origin` and `git status --short --branch`. - Use `bun` for package and script operations. - Use the operator-supplied tracker when present. Do not commit tracker runtime state, local coordination config, or other machine-local artifacts. -- If using Beads, follow the global Beads skill. AgentV Beads data belongs in `EntityProcess/agentv-beads`; never commit `.beads/metadata.json` or Beads runtime state. +- If using Beads, follow the global Beads skill. AgentV Beads data belongs in `EntityProcess/agentv-beads`; keep `.beads/config.yaml`, `.beads/metadata.json`, and Beads runtime state local. - Do not use `git stash` on shared checkouts. Stage explicit paths only, and never push directly to `main`. - Every merge to `main` requires a GitHub pull request with passing GitHub Actions. Do not locally merge feature or integration branches into `main` as a substitute for opening a PR. - Prefer the primary checkout only for small, clean, bounded work. Use a dedicated worktree from the latest `origin/main` for non-trivial, risky, long-running, or parallel changes. From d2f4ae59ebae8ecc4231937fb00ec53d49705c20 Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Sun, 21 Jun 2026 09:15:50 +0200 Subject: [PATCH 4/6] docs(beads): remove top-level workflow guidance --- .agents/workflow.md | 3 +-- AGENTS.md | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.agents/workflow.md b/.agents/workflow.md index ed265ceb9..8bf81ff86 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. -- If using Beads, follow the global Beads skill. The only repo-local Beads files intentionally tracked are `.beads/config.yaml.example` and `.beads/.gitignore`; `.beads/config.yaml`, `.beads/metadata.json`, and runtime state stay checkout-local. -- Do not commit project-local coordination config files. The Beads template above is 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 diff --git a/AGENTS.md b/AGENTS.md index 55a43e8dc..02f407f23 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,6 @@ Read the full rationale and examples in [.agents/product-boundary.md](.agents/pr - Start every repo change with `git fetch origin` and `git status --short --branch`. - Use `bun` for package and script operations. - Use the operator-supplied tracker when present. Do not commit tracker runtime state, local coordination config, or other machine-local artifacts. -- If using Beads, follow the global Beads skill. AgentV Beads data belongs in `EntityProcess/agentv-beads`; keep `.beads/config.yaml`, `.beads/metadata.json`, and Beads runtime state local. - Do not use `git stash` on shared checkouts. Stage explicit paths only, and never push directly to `main`. - Every merge to `main` requires a GitHub pull request with passing GitHub Actions. Do not locally merge feature or integration branches into `main` as a substitute for opening a PR. - Prefer the primary checkout only for small, clean, bounded work. Use a dedicated worktree from the latest `origin/main` for non-trivial, risky, long-running, or parallel changes. From 0daeacb809888171796269aa086c6dfcb5d100c3 Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Mon, 22 Jun 2026 00:10:07 +0200 Subject: [PATCH 5/6] fix(beads): preserve bootstrap identity --- .beads/.gitignore | 6 ++++-- .beads/config.yaml.example | 3 +++ .beads/metadata.json | 7 +++++++ 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 .beads/metadata.json diff --git a/.beads/.gitignore b/.beads/.gitignore index 317f9c8b8..a7e032465 100644 --- a/.beads/.gitignore +++ b/.beads/.gitignore @@ -74,9 +74,11 @@ backup/ db.sqlite bd.db -# Checkout-local config and identity. Copy config.yaml.example when using Beads. +# Checkout-local config. Copy config.yaml.example when using Beads. config.yaml -metadata.json + +# 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. diff --git a/.beads/config.yaml.example b/.beads/config.yaml.example index 4ff5a45d3..51da45582 100644 --- a/.beads/config.yaml.example +++ b/.beads/config.yaml.example @@ -55,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/.beads/metadata.json b/.beads/metadata.json new file mode 100644 index 000000000..94ac64bae --- /dev/null +++ b/.beads/metadata.json @@ -0,0 +1,7 @@ +{ + "database": "dolt", + "backend": "dolt", + "dolt_mode": "embedded", + "dolt_database": "av", + "project_id": "a7aea826-0087-45fc-93f5-9084e9924e8b" +} From 4cd9a3ceb38544e1ce9c1ed3a3c8659c08b7e855 Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Mon, 22 Jun 2026 00:10:13 +0200 Subject: [PATCH 6/6] docs(beads): add worktree recovery guard --- .agents/workflow.md | 1 + AGENTS.md | 1 + docs/runbooks/beads-worktree-recovery.md | 83 ++++ package.json | 1 + scripts/check-beads-context.ts | 461 +++++++++++++++++++++++ 5 files changed, 547 insertions(+) create mode 100644 docs/runbooks/beads-worktree-recovery.md create mode 100644 scripts/check-beads-context.ts diff --git a/.agents/workflow.md b/.agents/workflow.md index 8bf81ff86..d9a1e868a 100644 --- a/.agents/workflow.md +++ b/.agents/workflow.md @@ -19,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/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);