Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ test-output/
site/.cache/
.resources/

# Agent instruction files: per-machine project law, scaffolded by `relay setup`
# from templates/agent-instructions/. Never committed - they name local tooling.
# Root-anchored: an unanchored pattern matches at any depth and would also ignore
# the shipped templates under templates/agent-instructions/.
/AGENTS.md
/CLAUDE.md
/GEMINI.md

# Beads / Dolt files (added by bd init)
.dolt/
*.db
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,20 @@ Rules:

`relay setup` creates the directory and a `README.md` restating these rules, and `relay doctor` reports `resourcesRoot` plus whether it is actually ignored.

## Agent instruction files (`AGENTS.md`, `CLAUDE.md`, `GEMINI.md`)

Every assistant reads project law from a file in the repository root, and each one looks for a different name. Agent Relay ships one layered set so the law is written once:

- `AGENTS.md` is canonical and holds every shared convention. Codex and opencode read it natively.
- `CLAUDE.md` and `GEMINI.md` import it (`@AGENTS.md`) and carry only assistant-specific mechanics.
- Shared conventions are edited in `AGENTS.md` and nowhere else. Duplicating them into the per-assistant files is how they drift.

`relay setup` writes any of the three that are missing from the templates in `templates/agent-instructions/`, and adds all three to the project's local Git exclude state. An existing file is never touched — by then it is the project's own law, and a template is only a starting point. `relay doctor` reports which are present.

They stay local and uncommitted on purpose: they name per-machine tooling and model choices, and a project's rules are not Agent Relay's to version. That also means `git clean -xfd` deletes them, so treat the templates as the recovery path.

Adopting a project means filling in the placeholders: the architecture documents worth reading, the real gate commands, the invariants a newcomer would violate, and who merges. The template ships the parts that are true everywhere — the inherited-`BEADS_DIR` trap, verify-don't-trust delegation, no AI attribution, and Beads as the only durable task system.

## Beads store

The Beads store is project-local. `adapter.beads.requiredDir` defaults to `.beads`, resolved against the project root, which is exactly where `bd init` creates the embedded Dolt database (`.beads/embeddeddolt/`). Nothing depends on a shared or per-user store.
Expand Down
7 changes: 7 additions & 0 deletions skills/relay/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ Use `relay` when the project wants Claude, Codex, and agy to share the same Bead
5. Inspect checkpoints with `relay status [bead-id]`, `relay review <bead-id>`, and `relay gates <bead-id> [gate-name]`.
6. Use `relay cleanup <bead-id>` only after the human has reviewed the retained worktree state.

## Agent instruction files

- Project law lives in `AGENTS.md` at the repository root; `CLAUDE.md` and `GEMINI.md` import it and add only assistant-specific mechanics. Codex and opencode read `AGENTS.md` natively.
- `relay setup` scaffolds any that are missing from `templates/agent-instructions/` and keeps all three out of Git. It never overwrites an existing file.
- Edit shared conventions in `AGENTS.md` only. A convention copied into a per-assistant file will drift from the canonical one.
- Fill in the template placeholders before relying on them: architecture documents, gate commands, the invariants specific to the project, and the branch and merge rules.

## Reference resources

- Cache external documentation under the repository-root `.resources/` directory, one topic per subdirectory with a `SOURCE.md` naming the origin URL and fetch date.
Expand Down
49 changes: 49 additions & 0 deletions src/lib/agent-instructions.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import path from "node:path";
import { readFile, writeFile } from "node:fs/promises";
import { pathExists } from "./fs.mjs";
import { repoPath } from "./paths.mjs";

// The canonical file plus the two assistants that need an importer. Codex and
// opencode read AGENTS.md natively, so they get no file of their own.
export const AGENT_INSTRUCTION_FILES = ["AGENTS.md", "CLAUDE.md", "GEMINI.md"];

function templatePath(fileName) {
return repoPath("templates", "agent-instructions", fileName);
}

// Instruction files are per-machine project law, not deliverables: they name local
// tooling and may quote paths. Keeping them out of the index is what makes it safe
// to write them without asking.
//
// Root-anchored on purpose. A bare `AGENTS.md` pattern matches at every depth, so it
// would also ignore a package-level AGENTS.md the project does track.
export function agentInstructionExcludeMarkers() {
return AGENT_INSTRUCTION_FILES.map((fileName) => `/${fileName}`);
}

// Writes any missing instruction file from its shipped template. An existing file
// is never touched - it is the project's own law by then, and a template is only a
// starting point.
export async function scaffoldAgentInstructions(projectRoot) {
const written = [];
const preserved = [];
for (const fileName of AGENT_INSTRUCTION_FILES) {
const destination = path.join(projectRoot, fileName);
if (await pathExists(destination)) {
preserved.push(fileName);
continue;
}
await writeFile(destination, await readFile(templatePath(fileName), "utf8"), "utf8");
written.push(fileName);
}
return { written, preserved };
}

export async function agentInstructionStatus(projectRoot) {
const present = [];
const missing = [];
for (const fileName of AGENT_INSTRUCTION_FILES) {
(await pathExists(path.join(projectRoot, fileName)) ? present : missing).push(fileName);
}
return { present, missing };
}
17 changes: 15 additions & 2 deletions src/lib/supervisor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ import { prepareIsolatedProviderRun } from "./containment.mjs";
import { classifyProviderFailure, parseWorkerReport, runProviderCommand, providerCommandFromConfig, providerVendor, providerStrength, validateRuntimeProviderConfig } from "./provider.mjs";
import { assertTeamFacingTextClean, sanitizeIssueForPrompt, sanitizePromptText, sanitizeTeamFacingText, slugifyTitle } from "./sanitize.mjs";
import { syncRoleBundles } from "./roles.mjs";
import {
agentInstructionExcludeMarkers,
agentInstructionStatus,
scaffoldAgentInstructions
} from "./agent-instructions.mjs";
import { PROVIDERS } from "./providers/index.mjs";
import { validateReviewReport, validateWorkerReport } from "./validate.mjs";

Expand Down Expand Up @@ -272,7 +277,12 @@ async function ensureProjectState(projectRoot, adapterName, adapter = null) {
const config = await readProjectConfig(projectRoot, adapterName);
const resourcesRootName = resolveResourcesRootName(adapter);
const resourcesRoot = await ensureResourcesRoot(projectRoot, resourcesRootName);
const markers = [".agents/agent-relay/", `${resourcesRootName}/`, beadsExcludeMarker(adapter)].filter(Boolean);
const markers = [
".agents/agent-relay/",
`${resourcesRootName}/`,
beadsExcludeMarker(adapter),
...agentInstructionExcludeMarkers()
].filter(Boolean);
const excludePath = await ensureGitExclude(projectRoot, markers);
const beadsDir = adapter ? resolveBeadsDir(adapter, projectRoot) : null;
return { config, excludePath, stateRoot, resourcesRoot, resourcesRootName, beadsDir };
Expand Down Expand Up @@ -1702,7 +1712,8 @@ export async function doctor({ projectRoot, adapterName = "example-app", env = p
beadsTracked: beadsStoreIsTracked(adapter),
inheritedBeadsDirIgnored: inheritedBeadsDirIgnored(env, beadsDir),
roles: roles.length,
adapters: adapters.length
adapters: adapters.length,
agentInstructions: await agentInstructionStatus(projectRoot)
};
return problems.length > 0
? result("project-misconfigured", "doctor", { problems, ...details })
Expand All @@ -1725,6 +1736,7 @@ export async function setup({ projectRoot, adapterName }) {
const { adapter } = await loadAdapter(adapterName);
const { config, excludePath, resourcesRoot, beadsDir } = await ensureProjectState(projectRoot, adapterName, adapter);
const syncedRoles = await syncProjectRoles(projectRoot);
const agentInstructions = await scaffoldAgentInstructions(projectRoot);
return ok("setup", {
adapter: adapter.name,
config,
Expand All @@ -1735,6 +1747,7 @@ export async function setup({ projectRoot, adapterName }) {
beadsTracked: beadsStoreIsTracked(adapter),
beadsStorePresent: await pathExists(beadsDir),
syncedRoles,
agentInstructions,
localStateRoot: projectStateRoot(projectRoot)
});
}
Expand Down
66 changes: 66 additions & 0 deletions templates/agent-instructions/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Agent Instructions (canonical — ALL assistants)

**This file is the single source of truth for AI-agent instructions in this repository.** `CLAUDE.md` and `GEMINI.md` import it and add only assistant-specific content; Codex and opencode read this file natively. **Edit shared conventions HERE and only here** — never duplicate them into the per-assistant files.

These files are local: they are listed in `.git/info/exclude`, so they never reach a commit, a diff, or a PR. Write them for the machine you are on, but keep personal paths, emails, and hostnames out of them anyway — a gitignored file is one `git add -f` away from being public.

## First action in every session

1. **`bd where`** — confirm it prints this project's `.beads` directory and the expected issue prefix.
- If a `BEADS_DIR` environment variable is exported on this machine, it wins over repository discovery and will silently route every `bd` call into another project's store. Either unset it or pass `BEADS_DIR="$PWD/.beads"` explicitly on every `bd` invocation.
- **Never run `bd init`** when `bd where` looks wrong — you are in the wrong directory or inheriting the wrong environment. Report it; a second store is worse than a failed command.
2. **`bd prime`** — load the persistent memories.
3. **Verify** the output contains a `## Persistent Memories` section. If it says none are stored, fall back to `bd memories --json`; plain `bd memories` prints truncated previews and is not a substitute.

`bd ready` lists available work, `bd show <id>` reads one issue, `bd update <id> --claim` claims it, `bd close <id>` finishes it.

## Read protocol (before non-trivial work)

Don't guess architectural patterns; look them up.

1. **Architecture source of truth**: `<fill in: docs/architecture/, ADRs, or the module map>`. Treat its rules as physical laws.
2. **Current state**: the live queue is `bd ready --json`; persistent knowledge loads through the first-action sequence above.
3. If a doc says "X never does Y", either follow it or change the doc with new reasoning — never silently violate it.

## Execution constraints (physical laws of this codebase)

`<fill in the invariants that a competent newcomer would violate: the guard that must be called before an encode, the boundary that fails at runtime rather than compile time, the thing that must never be hand-edited because it is generated.>`

- **No ghost code** — verify interfaces and files exist (grep or read) before referencing them.
- **Scope containment** — modify only files related to the current objective; never "clean up" unrelated files.

## Build and test

```bash
# <fill in the real gates, exactly as CI runs them>
```

Run the gates you claim to have run. Whoever orchestrates re-runs them independently, and a claimed-but-unrun gate is treated as a failure.

## Conventions

- Use `bd` for ALL task tracking and `bd remember` for persistent knowledge. No markdown TODO lists, no parallel tracking systems.
- Default to no code comments; add one only when the WHY is non-obvious.
- **No magic literals, DRY always.** Inline selectors, addresses, thresholds, or duplicated formulas are defects even when correct. One exported implementation; callers import it.
- No features, refactors, or abstractions beyond what the task requires.
- **No AI attribution anywhere**: no `Co-Authored-By:` trailers naming an assistant, no "Generated with …" footers in PR bodies. Authorship is the human committer.

### Git, commits, PRs

- `<fill in: which branches are protected, whether work lands via PR or directly, and who merges>`.
- Multi-paragraph commit messages via heredoc (`git commit -F -`), without attribution trailers.
- **Beads is local-only — never mention bd issue ids in PR titles, PR bodies, commit messages, or anything else team-facing.** Track the bd↔PR link on the bd issue instead (`bd update <id> --notes "PR #NNN"`).

## Orchestration and delegation protocol

Applies to whichever assistant is the main session.

1. **Orchestrate, don't code.** Route by weight, cheapest first: small exactly-speccable tasks to the cheapest coder tier, medium features to the mid tier, cross-system or design-heavy correctness to the strongest. Each toolchain has its own workers — Claude reads `.claude/agents/`, Codex `.codex/agents/`, agy `.agents/agents/`, opencode `.opencode/agent/`.
2. **Specs embed every decision and every gate.** A dispatched worker should never have to infer a decision you already made, and never runs git or a release step unless told to.
3. **Verify, don't trust.** Independently re-run every gate a worker claims to have passed, and check findings against the code before acting on them.
4. **Worktree discipline**: one isolated worktree per workstream. Shell working directories persist between calls, so `cd` with absolute paths, and after any worker run check `git status` in both the worktree and the main checkout.
5. **A worker that reports a blocked or contradictory spec is doing its job.** Fix the spec rather than pressing the same instruction again.

## Session completion

Before ending a session: run the gates, close finished issues with reasons, file new issues for follow-ups (never a TODO in code), and push committed work. Work is not complete until it is pushed.
18 changes: 18 additions & 0 deletions templates/agent-instructions/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Project Instructions for Claude Code

**Canonical shared instructions live in `AGENTS.md`** (imported below) — read by ALL assistants. Edit shared conventions (execution constraints, build and test, git and PR rules, the bd workflow, the delegation protocol) ONLY in `AGENTS.md`. This file carries ONLY Claude-specific content.

@AGENTS.md

# Claude-specific notes

- Run the `AGENTS.md` first-action sequence manually unless a `SessionStart` hook does it for you. If you add such a hook, it must not export `BEADS_DIR` — the store is repository-local and self-discovering, and a global export routes every other project on the machine into this one's store.
- Keep persistent knowledge in `bd remember`, not in the harness's own memory files, so every assistant shares it.

# Orchestration and delegation

The tool-neutral protocol lives in **`AGENTS.md` §"Orchestration and delegation protocol"**. Claude-specific mechanics only:

- Dispatch workers with the Agent tool; personas live in `.claude/agents/`. Agent Relay generates `orchestrator`, `coder`, and `reviewer` there — run `relay setup` after changing a role source, never hand-edit the generated bundle.
- Route to an external CLI when it is cheaper or when a second vendor's opinion is the point: `codex` and `agy` both read this repository's instruction files. Each provider manifest declares its own default model per role, so ask for the role and let the manifest pick the model.
- Reviews get opinions from a different vendor than the one that wrote the code; a same-vendor review does not satisfy a two-vendor quorum.
21 changes: 21 additions & 0 deletions templates/agent-instructions/GEMINI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Project Instructions for Gemini

**Canonical shared instructions live in `AGENTS.md`** (imported below) — read by ALL assistants. Edit shared conventions ONLY in `AGENTS.md`; this file carries ONLY Gemini-specific content.

@./AGENTS.md

# Gemini-specific notes

- **Run the `AGENTS.md` first-action sequence as your FIRST action.** Nothing injects it for you.
- **If an orchestrator dispatched you**: your brief embeds every decision and every gate. Follow it exactly, do not run git or any release step, and report gate results honestly — they are independently re-run. Pin ACTUAL observed behaviour over the brief's predictions and flag mismatches loudly.
- If the brief contradicts itself or the code, stop and report instead of guessing. A blocked worker is cheaper than a wrong merge.
- Verify you are writing into the intended worktree rather than the main checkout; use absolute paths.

# Eager context

Gemini loads a large context well, so import the documents you would otherwise have to be told to read:

```
@./<architecture doc>
@./<module map>
```
51 changes: 51 additions & 0 deletions test/supervisor.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,53 @@ test("setup writes local state, syncs roles, and marks .agents/agent-relay ignor
assert.equal(claudeLaw.trim(), "# Fixture CLAUDE");
});

test("setup scaffolds missing agent instruction files, keeps existing ones, and excludes all of them", { timeout: 10000 }, async () => {
const projectRoot = await createProjectFixture();
// The fixture models a project that already has its own law: all three files
// exist, so none may be rewritten.
const existing = await setup({ projectRoot, adapterName: "example-app" });
assert.equal(existing.ok, true);
assert.deepEqual(existing.agentInstructions.written, []);
assert.deepEqual(existing.agentInstructions.preserved, ["AGENTS.md", "CLAUDE.md", "GEMINI.md"]);
assert.equal(
(await readFile(path.join(projectRoot, "CLAUDE.md"), "utf8")).trim(),
"# Fixture CLAUDE"
);

// A project with none of them gets all three from the shipped templates.
for (const fileName of ["AGENTS.md", "CLAUDE.md", "GEMINI.md"]) {
await rm(path.join(projectRoot, fileName));
}
const result = await setup({ projectRoot, adapterName: "example-app" });
assert.deepEqual(result.agentInstructions.written, ["AGENTS.md", "CLAUDE.md", "GEMINI.md"]);
assert.deepEqual(result.agentInstructions.preserved, []);

const agentsLaw = await readFile(path.join(projectRoot, "AGENTS.md"), "utf8");
assert.match(agentsLaw, /single source of truth/);
// The template must warn about the inherited-BEADS_DIR trap: it silently routes
// every bd call into another project's store.
assert.match(agentsLaw, /BEADS_DIR/);
assert.match(await readFile(path.join(projectRoot, "GEMINI.md"), "utf8"), /@\.\/AGENTS\.md/);

// Root-anchored entries: a bare `AGENTS.md` would also ignore a package-level
// AGENTS.md that the project legitimately tracks.
const exclude = await readFile(path.join(projectRoot, ".git", "info", "exclude"), "utf8");
for (const fileName of ["AGENTS.md", "CLAUDE.md", "GEMINI.md"]) {
assert.match(exclude, new RegExp(`^/${fileName.replace(".", "\\.")}$`, "m"));
}

// A rerun neither rewrites a file nor duplicates an exclude entry.
await writeFile(path.join(projectRoot, "AGENTS.md"), "# Edited by the project\n", "utf8");
const rerun = await setup({ projectRoot, adapterName: "example-app" });
assert.deepEqual(rerun.agentInstructions.written, []);
assert.equal(
await readFile(path.join(projectRoot, "AGENTS.md"), "utf8"),
"# Edited by the project\n"
);
const excludeAfter = await readFile(path.join(projectRoot, ".git", "info", "exclude"), "utf8");
assert.equal(excludeAfter.split("\n").filter((line) => line === "/AGENTS.md").length, 1);
});

test("setup creates the ignored reference cache and the project-local beads exclude entry", { timeout: 10000 }, async () => {
const projectRoot = await createProjectFixture();
const result = await setup({ projectRoot, adapterName: "example-app" });
Expand Down Expand Up @@ -277,6 +324,10 @@ test("doctor honors the requested adapter and validates required files", { timeo
assert.equal(result.beadsStorePresent, true);
assert.equal(result.resourcesIgnored, true);
assert.equal(result.inheritedBeadsDirIgnored, false);
assert.deepEqual(result.agentInstructions, {
present: ["AGENTS.md", "CLAUDE.md", "GEMINI.md"],
missing: []
});
});

test("doctor reports a missing project-local beads store and an ignored global BEADS_DIR", { timeout: 10000 }, async () => {
Expand Down
Loading