From 415a85fd6eaf080e9f61cb8ff6938192f093e555 Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Sun, 21 Jun 2026 05:36:19 +0200 Subject: [PATCH 1/4] 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 98f7ba48b..79e825b75 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 bb40cb785..753a3707c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,6 +28,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`. - 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. - Non-trivial work needs a plan or task list. If the implementation surface starts to balloon, stop and re-plan. 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 70d35d429..32c395c10 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 d2b34ce84af1c4ad5f44cb9b4c24540d45432359 Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Sun, 21 Jun 2026 06:58:58 +0200 Subject: [PATCH 2/4] 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 79e825b75..165c01c80 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 753a3707c..242249c75 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,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`. - 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. - Non-trivial work needs a plan or task list. If the implementation surface starts to balloon, stop and re-plan. 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 32c395c10..70d35d429 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 38fa61fac85ccbd0084209eb20928a35e6958b4c Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Sun, 21 Jun 2026 08:52:28 +0200 Subject: [PATCH 3/4] 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 165c01c80..70ac2362a 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 242249c75..fd2c561fa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,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`. - 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. - Non-trivial work needs a plan or task list. If the implementation surface starts to balloon, stop and re-plan. From b089848a3c49671b8e42d96486f6a98e190ef69e Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Sun, 21 Jun 2026 09:15:50 +0200 Subject: [PATCH 4/4] 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 70ac2362a..710db8e23 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 fd2c561fa..bb40cb785 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,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`. - 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. - Non-trivial work needs a plan or task list. If the implementation surface starts to balloon, stop and re-plan.