refactor(project-context): conversational skill, no script, AGENTS.md block

Refine the skill into an implementation-layer capability: a conversation that
produces one small verified block inside the repo's AGENTS.md. The human is in
the loop for every write; there is no autonomous mode.

- Drop src/scripts/context.py and its tests. Nothing it did is needed once the
  output is a single spliced block rather than a bundle of files.
- Replace guide-contract.md and evidence.md with best-practices.md (admission,
  exclusion, retirement, retrieval, maintenance) and template.md (section list
  plus a worked example, no placeholders).
- Collapse the two-file AGENTS.md/AGENTS-dev.md split into one block. A pointer
  the agent must choose to follow gets skipped; anything load-bearing goes in
  the always-loaded file.
- Replace per-section admission rules with one test: anything derivable from
  source is read live, never stored. Commands stated in package.json, a
  Makefile, or CI config no longer earn a line; their caveats do.
- Ask up front whether a run covers the root only or named sub-projects, gated
  on observable evidence (a workspace manifest, per-directory build manifests).
- Husk bmad-document-project and bmad-generate-project-context onto setup
  intent, and say plainly that the deeper system-explanation altitude is a
  separate capability rather than shipping a thin substitute.
- Align module-help.csv, bmad-correct-course, the analyst menu, and the docs
  set with the block as the output.
This commit is contained in:
Brian Madison
2026-08-08 23:00:12 -05:00
parent 95d93c3e54
commit 8e17763ddc
21 changed files with 359 additions and 1533 deletions
+2 -2
View File
@@ -15,13 +15,13 @@ Quick answers to common questions about working on established projects with the
### Do I have to run document-project first?
`bmad-document-project` is deprecated — its replacement is [`bmad-project-context`](project-context.md), which builds a small verified context system (kernel + bundle) instead of generated documentation. Running it first is highly recommended, especially if:
`bmad-document-project` is deprecated — its replacement is [`bmad-project-context`](project-context.md), which writes a small verified block into your repo's `AGENTS.md` instead of generated documentation. Running it first is highly recommended, especially if:
- No existing documentation
- Documentation is outdated
- AI agents need context about existing code
You can skip it if agents already load a maintained kernel, or you'll use other tools or techniques to aid discovery for the agent to build on an existing system.
You can skip it if your repo already has maintained agent instructions, or you'll use other tools or techniques to aid discovery for the agent to build on an existing system.
### What if I forget to run document-project?
+69 -30
View File
@@ -1,61 +1,100 @@
---
title: "The Theory of Project Context"
description: Why bmad-project-context captures so little, what earns a place in the context system, and what is deliberately left out
description: Why bmad-project-context captures so little, what earns a place in a repository's agent instructions, and what is deliberately left out
sidebar:
order: 11
---
`bmad-project-context` is built on an uncomfortable finding: most documentation written *for* AI agents makes them worse. This page explains the theory behind the skill — what it captures and why, and more importantly, what it deliberately refuses to capture. If you are coming from `bmad-document-project` or `bmad-generate-project-context`, the second half explains exactly what changed and why.
`bmad-project-context` is built on an uncomfortable finding: most documentation written *for* AI agents makes them worse. This page explains the theory behind the skill — what it captures and why, and more importantly, what it deliberately refuses to capture. If you are coming from `bmad-document-project` or `bmad-generate-project-context`, the last section explains exactly what changed.
For what the skill *does* and how to run it, see [Project Context](project-context.md) and the [how-to guide](../how-to/project-context.md).
## The problem with generated documentation
## The line: derivable or not
Three findings drive the design:
Written context earns its cost only when it carries something the agent cannot derive by reading the repository.
1. **LLM-generated context documents measurably degrade agent performance** — lower correctness at higher cost. A generated overview is a paraphrase of the code, and a paraphrase is worse than the code: it drops detail, it drifts the moment the code changes, and the agent trusts it instead of looking.
2. **Every line of always-loaded context is paid for in every session.** A 2,000-line context file is not "thorough" — it is a tax on every future task, most of it spent on things the agent would have gotten right anyway.
3. **Wrong context is worse than no context.** An agent with no documentation explores and finds the truth. An agent with a stale document confidently follows it off a cliff. Staleness is not a cosmetic problem; it is the failure mode.
Two results from different literatures locate the same boundary. Separating code reasoning from documentation memorization across repository-level tasks, **code access delivers the dominant gains over documentation access** — a document describing how the system works loses to the source it describes. Running the inverse experiment — generating requirements *from* code — models prove unreliable at producing anything not already implemented. Current behavior is recoverable from source. **Intent, rationale, and what was deliberately rejected are not.**
The conclusion: the valuable set is the **minimum of non-derivable, verified truths** — everything an agent cannot learn by reading the code, and nothing it can.
So anything derivable is read live and never stored. A stored copy is a stale duplicate of something the agent reads more accurately first-hand, and it is charged on every single call.
## Why most AGENTS.md files measure as worthless
The repository instruction file is the most-studied artifact of 2026, and its record on task correctness is poor. Measured present versus absent: **no improvement in success rate, and +20% inference cost.** Replicated on real repositories, with failures traced to implementation skill gaps rather than missing repository knowledge. In one study at scale, **randomly generated rules matched expert-curated ones.**
That is a damning result until you notice what those files contain. They overwhelmingly restate what the repository already holds — structure, stack, architecture summaries. The studies measured *derivable* written context, not written context in general.
## Why a short index in the always-loaded file is the opposite result
One controlled comparison ran four configurations against framework APIs absent from the model's training data:
| Configuration | Pass rate |
|---|---|
| No documentation | 53% |
| Reusable skill, unaided | 53% |
| Same skill, with explicit instructions to invoke it | 79% |
| **Compressed documentation index in `AGENTS.md`** | **100%** |
Same file format as the null results above. Opposite content: knowledge the model did not have, rather than a restatement of the repo. The index was 8KB, compressed from 40KB with no loss in performance.
The other half of that result matters just as much. The unaided skill was **never invoked in 56% of cases**. Adding explicit instructions raised trigger rates above 95% and still capped at 79%, with outcomes swinging on subtle wording changes.
**Conditional retrieval is unreliable**, and separate measurements agree. In an ablation over a 709-page wiki, agents skipped the index entirely and inferred page paths from the question rather than fetching it.
The rule that reconciles all of it: **an index the agent must choose to fetch gets skipped; an index already in context does not.** Anything load-bearing goes in the always-loaded file. Pointers out of it must name a trigger the agent can *observe* — a path, a file type, a concrete task — never one it must judge or self-monitor.
## What earns a place
The test for every line is the **pruning test**: *would removing this line change agent behavior?* If an agent would do the right thing anyway — because the code shows it, or because it is the ecosystem default — the line is noise. What passes:
The test for every line is the **pruning test**: *would removing this line change agent behavior?*
- **Commands where the obvious guess fails.** `npm install`, never `npm ci`, because lockfiles are deliberately gitignored. An agent cannot derive "deliberately" from a missing file.
- **Conventions that differ from defaults.** Only the divergences. "Use conventional commits" earns a line; "write tests for new code" does not — the agent already believes that.
- **Landmines.** The docs folder that predates two migrations. The workflow that looks live but is broken. The two generations of config variables that must not be mixed. These are the facts whose absence produces confident, wrong work.
- **Decision rationale.** *Why* the architecture is shaped this way — the code shows the shape, never the reason. Decisions are born in `bmad-architecture`; they live here.
- **Org requirements and domain facts** that exist nowhere in the repo at all.
Everything captured is **verified before it is written as truth**: mined claims are checked against code (the trust ladder — code and configs are ground truth, existing docs are untrusted until proven), then confirmed with a human. Every entry carries its trust status (`verified` or `generated`), its sources, and its verification date. A claim nobody confirmed is stored as `generated` — visible as inference, never laundered into fact.
- **What a config file cannot say about running the project.** The invocation itself lives in `package.json`, a `Makefile`, or CI config and is read from there. What does not live there is the correction: the root test script does nothing in this workspace, integration tests need a service up first, the suite is slow enough that you should iterate on single files, CI runs a check the test script does not.
- **Policy the code cannot express.** Frozen paths, generated files, branch rules, security and compliance requirements. Admitted by authority, not by discovery.
- **Conventions that differ from ecosystem defaults.** Only the divergences. An agent follows the norm unless told otherwise, so a fact nobody would get wrong by default is not worth a line.
- **Known pitfalls, from observed failure only.** A repository yields hundreds of trap-looking facts, and no property of the fact separates the few that cause real mistakes — that signal exists only in observed behavior. A surprising scan finding becomes a question, never a line.
- **Negative constraints over positive guidance**, which measured better, and which is why a prohibition here always names the permitted alternative.
## What is deliberately not captured
The negative space is the design. Each exclusion has a reason:
The negative space is the design.
| Not captured | Why |
|---|---|
| **What the code already says** | Agents read source better than they read summaries of source. A paraphrase adds a second copy that rots while the original stays true. |
| **Repo structure and file maps** | Structure changes with every commit — stored maps rot fastest of all. Agents derive structure fresh in seconds with the tools they already have. |
| **Overview and tour documents** | The classic generated deliverable, and the measured harm: long overviews add wasted exploration and misplaced confidence. The kernel's job is to change behavior, not to orient a reader. |
| **Ecosystem defaults** | An LLM already knows how a typical Node, Python, or Go project works. Restating defaults spends budget teaching the agent what it arrived knowing. |
| **History and edit narration** | "We removed X because…" is banned prose. Git and the memlog own history; entries state present truth only, and supersession is a dated frontmatter field, not a story. |
| **Unverified inference presented as fact** | Anything not confirmed stays marked `generated`. The trust field is the contract: `verified` asserts a human was in the loop. |
| **User-facing documentation** | Tutorials, setup guides, and reference sites serve human readers — a different artifact with different rules. The skill will flag user docs that have drifted (as a landmine: "distrust docs/ on these topics") but it does not write or replace them. |
| **Aspirational state** | What the system *should* become belongs in specs and architecture documents. Context describes what *is* — an agent acting on aspiration ships fiction. |
| **What the code already says** | Agents read source better than summaries of source. A paraphrase adds a second copy that rots while the original stays true. |
| **Repo structure and file maps** | Structure changes with every commit — stored maps rot fastest of all, and agents derive structure fresh in seconds. |
| **Overview and tour documents** | The classic generated deliverable, and the measured harm. The block's job is to change behavior, not to orient a reader. |
| **Ecosystem defaults** | An LLM already knows how a typical Node, Python, or Go project works. Restating them spends budget teaching the agent what it arrived knowing. |
| **Anything included for being interesting** | Interest is not evidence of need. This is the failure mode the skill exists to avoid. |
| **Style rules an agent should self-enforce** | That job belongs to a formatter, linter, hook, or CI check. The skill proposes the check instead, and a check that lands deletes its line. |
| **History and edit narration** | "We removed X because…" is banned prose. Git owns history; the block states present truth only. |
| **Aspirational state** | What the system *should* become belongs in specs. An agent acting on aspiration ships fiction. |
The result is small by design. A healthy kernel is a screenful; a healthy bundle for a real repo is a dozen small entries. Small projects need the kernel and nothing else — that outcome is success, not an unfinished job.
The result is small by design. When the evidence supports ten lines, ten lines is the deliverable.
## Retirement runs the other way
There is one rule that inverts the pruning instinct, and getting it wrong quietly destroys the best content in the file.
**A policy or pitfall line retires only when the thing it guards is gone** — removed, or now mechanically enforced — **or when a human retires it.** Absence of recent failures is never grounds. A working rule erases its own evidence, and much of the value of the block is the failures that no longer happen.
## Two altitudes, two artifacts
One artifact cannot serve both coding and planning work. The material divides, and the halves barely overlap.
**Implementation context** — constraints, commands, conventions, pitfalls — is a property of a **code repository**. It is verifiable against the code, executably. It goes stale on every commit. It is loaded on every session, so it must be tiny. That is what this skill owns.
**Planning context** — rationale, rejected approaches, ownership, domain meaning, org standards — is a property of a **project or initiative**. It is traceable only to source documents. It goes stale on organizational time, in months rather than hours. It is consulted in bursts, not loaded continuously. That is a different capability, and it is coming separately.
Trying to serve both from one file is what produced the two skills this one replaced.
## Context is a liability to be re-earned
The old model treated documentation as an asset: more coverage, more value. This skill treats context as a **liability that must keep proving itself**. Staleness sweeps check every claim's sources against the repo; the audit intent applies the pruning test to every line and ends with the context smaller or equal, never larger; entries that merely paraphrase readable code are deleted. When a claim's source disappears, the claim is fixed against the new reality or removed — never quietly re-pointed at a document that happens to still mention it.
The old model treated documentation as an asset: more coverage, more value. This skill treats context as a **liability that must keep proving itself.** Refresh re-checks every caveat and diffs deletions and renames against every line. Audit applies the pruning test and ends with the block smaller or equal, never larger. When a claim's source disappears, the claim is fixed against the new reality or removed — never quietly re-pointed at a document that happens to still mention it.
Generating the first version is the cheap part. Keeping it true is where the value is, and it is why refresh and audit exist as first-class intents rather than a note in the documentation.
## Versus the two replaced skills
`bmad-document-project` scanned a brownfield repo and generated a documentation tree — overview, source tree, per-area deep dives. It embodied the asset model, and the evidence went against it: the output was large, unverified, stale on arrival, and precisely the kind of context that degrades agents. Its valid instinct — *understand the repo before working in it* — survives as the ingest scan, which now feeds verification instead of prose generation. Where it would have described the repo's structure, the new skill lets agents derive structure fresh; where it would have summarized code, the new skill writes nothing.
`bmad-document-project` scanned a brownfield repo and generated a documentation tree — overview, source tree, per-area deep dives. It embodied the asset model, and the evidence went against it: large, unverified, stale on arrival, and precisely the kind of context that degrades agents. Its valid instinct — *understand the repo before working in it* — survives as the discovery pass, which now feeds verification instead of prose generation.
`bmad-generate-project-context` had the right instinct a single small rules file of unobvious, project-specific facts — and that instinct is now the whole architecture. What it lacked was everything around the file: no verification (its content was as trusted as its generation run was lucky), no trust marks, no staleness model, no maintenance loop, and no room for the *why* behind the rules. The kernel is its descendant, held to a measured budget; the bundle carries the rationale it had nowhere to put; ingest/audit keep both true over time. An existing `project-context.md` keeps loading and becomes a mining source on the next ingest.
`bmad-generate-project-context` had the right instinct: a single small rules file of unobvious, project-specific facts. That instinct is now the whole architecture. What it lacked was everything around the file no verification, no maintenance loop, and no way to tell an inference from a confirmed fact.
The one-line version: the old skills wrote more documentation; this skill maintains less truth and less, verified, wins.
The one-line version: the old skills wrote more documentation; this skill maintains less truth, and less, verified, wins.
+28 -17
View File
@@ -1,44 +1,55 @@
---
title: "Project Context"
description: How bmad-project-context curates the verified knowledge AI agents load — a small kernel plus a knowledge bundle
description: How bmad-project-context writes a repository's agent instructions — a small verified block in AGENTS.md
sidebar:
order: 10
---
`bmad-project-context` owns everything the code can't tell an AI agent: why the architecture is shaped this way, which conventions are deliberate, what the org requires, which landmines a fresh session must know before touching anything. It maintains that knowledge as a small, verified context system instead of generated documentation.
`bmad-project-context` sets up a repository so AI agents work well in it. The output is a small verified block inside the repo's `AGENTS.md`: what the org requires, the commands that were actually run, the conventions where the obvious guess is wrong, and the mistakes agents keep making here.
The evidence behind the design is blunt: LLM-generated context docs measurably make agents *worse* (lower correctness at higher cost), and long overview documents add wasted exploration. What works is a tiny always-loaded file, small verified entries loaded on demand, and mechanical maps produced fresh on demand. So the skill curates the minimum non-derivable set and never describes what the code already says. For the full reasoning — including what is deliberately *not* captured and why — see [The Theory of Project Context](project-context-theory.md).
It is a conversation, not a generator. You bring the rules you want followed — governance, security, coding standards — and it discovers and verifies the rest. The human is in the loop for every write; there is no unattended mode.
## The two artifacts
For the full reasoning, including what is deliberately *not* captured and why, see [The Theory of Project Context](project-context-theory.md).
**The kernel** (`kernel.md`) is one small file loaded into every agent session — exact commands where the obvious guess fails, conventions that differ from ecosystem defaults, landmines, hard org requirements. It lives under an instruction budget (~150200 instructions), is priority-ordered, and every line must pass the pruning test: *would removing this line change agent behavior?* Small projects need the kernel and nothing else.
## What goes in, and what doesn't
**The bundle** is a directory of small markdown entries behind the kernel — architecture rationale, the *why* behind conventions, domain facts, decision history. Each entry carries frontmatter with trust signals: `verified` (a human confirmed it, or it was path-checked with a human in the loop) or `generated` (inferred, unconfirmed — everything a headless run writes). `index.md` is the sole entry point; entries are loaded on demand, never wholesale.
The governing line is whether a fact can be derived by reading the repository. Agents read code more accurately than they read prose describing code, and a stored description is a stale duplicate charged on every call. So repo overviews, directory trees and tech-stack lists never enter.
Both live in your `project_knowledge` folder (the standard install setting, default `docs/`). A mechanics script (`context.py`) handles everything mechanical — validation, indexing, staleness sweeps, repo maps, cross-project resolution — so no agent ever guesses at mechanical facts.
What earns a line is what the code cannot say:
## Three intents
- **Policy** the org requires — frozen paths, generated files, branch rules, security and compliance.
- **What a config file cannot say about running the project** — the caveat, not the command. `pnpm test` is already in `package.json`; that the suite takes eleven minutes, or needs a service running first, is not.
- **Conventions that differ from ecosystem defaults**, because an agent follows the norm unless told otherwise.
- **Known pitfalls**, admitted only from observed failure — a lesson already recorded, the maintainer's recollection, a mistake fixed repeatedly in git history, or one the writing session made and caught. A trap-looking fact from a scan becomes a question, never a line.
- **Pointers** to where work lands, and to nested or linked files worth reading first.
Every rule the skill applies is written out in `references/best-practices.md`, with the evidence behind it. The skill uses it to assess what your repo already has, and explains its reasoning back to you at the end.
## Four intents
| Intent | What it does |
|--------|--------------|
| **Ingest** | Build or refresh the context. Brownfield: mine the repo and docs first, then ask only what's genuinely unknowable. Greenfield: seed from a spec or architecture doc, or a short interview. Refresh: diff against the last run — never start over. |
| **Query** | Answer a question from the bundle without loading all of it, with trust metadata attached. |
| **Audit** | Keep the set small and true: staleness sweeps, path verification, the pruning test. Context shrinks or holds — it never accretes. |
| **Setup** | The default. Assess what exists, ask what you bring, discover and verify the rest, show you the block, then write it. |
| **Refresh** | The same run against an existing block: re-run its commands, diff deletions and renames since the recorded commit, update what moved. |
| **Record** | Capture one observed agent mistake at the moment it happens. A recurring or costly one earns a line. |
| **Audit** | Re-verify and prune. The block ends smaller or equal, never larger. |
## How agents load it
On first run the skill asks your **placement** preference:
`AGENTS.md` at the repo root, which every major coding harness reads. BMad owns only the region between `<!-- bmad:context -->` and `<!-- /bmad:context -->`; everything you write outside those markers is preserved byte for byte, and a refresh never touches it.
- **bmad** — the kernel loads through BMad customization arrays; your agent files are never touched.
- **agent-files** — the script maintains managed `<!-- bmad:context -->` blocks in your root and nested `AGENTS.md` files, preserving everything around them. This is the default when there's no BMad install — the skill works standalone in any repo, with no framework at all.
- **both** — arrays plus agent files, kept in sync.
Monorepo components and nested repositories get their own file under the same rules, listed as pointers in the parent. A large rule set bounded to a directory belongs in a nested `AGENTS.md` there, where the harness attaches it by location.
## Repo or home directory
What this skill writes belongs committed to the repository — shared by the team, consistent across machines, versioned with the code it constrains. If you find the same rules repeating across every project, or they are your personal preferences rather than the team's, they belong in your agent's global configuration in your home directory instead.
## Interaction with architecture
Decisions are *born* in `bmad-architecture`; they *live* in project-context. The architecture spine is the premier ingest source, and if ingest surfaces a genuinely contested decision, the skill says it deserves `bmad-architecture` rather than quietly making the call.
Decisions are *born* in `bmad-architecture`. If a genuinely contested design decision surfaces here — real tradeoffs, multiple viable shapes — the skill says it deserves `bmad-architecture` rather than quietly making the call.
## Replaces two earlier skills
:::note[Deprecated: bmad-document-project and bmad-generate-project-context]
Both earlier skills are deprecated and now forward here. `bmad-generate-project-context` produced a single `project-context.md`; `bmad-document-project` scanned a brownfield repo into documentation. Their trigger phrases still work, any existing `project-context.md` keeps loading (and becomes a mining source on the next ingest), and the ideas they carried — "capture unobvious rules only" — are now the whole architecture.
Both earlier skills are deprecated and now forward here. `bmad-generate-project-context` produced a single `project-context.md` — if you have one, setup offers to absorb its content rather than orphaning it. `bmad-document-project` scanned a brownfield repo into generated documentation, which is the approach the evidence went against; the deeper "explain this system and its rationale" material is a different altitude and is coming as its own capability.
:::
+1 -1
View File
@@ -36,7 +36,7 @@ Run the project context skill:
bmad-project-context
```
It scans your codebase and any docs first (mechanically, via its script and parallel subagents), then asks you in short rounds only about what's genuinely unknowable — landmines, frozen areas, org requirements. You end up with a small always-loaded `kernel.md` plus a bundle of verified entries in your `project_knowledge` folder, instead of generated documentation volume. An existing bloated `docs/` folder is treated as a source to verify against code, not something to add to.
It reads what you already have and tells you how it measures up, asks what rules you want followed, then discovers and verifies the rest — running every command before writing it down. You end up with a small verified block in your repo's `AGENTS.md` instead of generated documentation volume. An existing hand-written file is a baseline it improves; a bloated `docs/` folder is a source to verify against code, not something to add to.
[Learn more about project context](../explanation/project-context.md)
+39 -16
View File
@@ -1,23 +1,24 @@
---
title: 'Manage Project Context'
description: Build and maintain your project's verified context system with bmad-project-context
description: Set up and maintain your repository's agent instructions with bmad-project-context
sidebar:
order: 8
---
Use `bmad-project-context` to give every AI agent session the minimum verified, non-derivable knowledge it needs — a small always-loaded kernel plus a knowledge bundle — for a new project or an existing codebase, with or without a BMad install.
Use `bmad-project-context` to set up a repository so AI agents work well in it — for a new project or an existing codebase, with or without a BMad install. The output is a small verified block in your `AGENTS.md`.
:::note[Prerequisites]
- BMad Method installed — or nothing at all: the skill also runs standalone in any repo (it bootstraps its own mechanics script on first run)
- BMad Method installed — or nothing at all: the skill also runs standalone in any repo
:::
## When to use this
- You're starting AI-assisted work in an existing codebase (this is the brownfield on-ramp)
- You're starting a new project and want your stack, conventions, and constraints captured before implementation
- Agents keep making decisions that don't match your project
- Your context feels stale or bloated — run an audit
- You're starting a new project and want your standards followed from the first commit
- You have governance, security or style rules that agents need to respect
- Agents keep making the same mistake and you want it written down
- Your instructions feel stale or bloated — run an audit
## Step 1: Run it
@@ -25,26 +26,48 @@ Use `bmad-project-context` to give every AI agent session the minimum verified,
bmad-project-context
```
Say what you want in plain language — "set up project context", "refresh the context", "audit our context" — and the skill routes itself (ingest is the default). On first run it confirms where the context lives (your `project_knowledge` folder) and asks how agents should load it: through BMad customization arrays, through managed blocks in your `AGENTS.md` files, or both.
Say what you want in plain language — "set up AGENTS.md", "refresh the context", "audit our context", "the agent keeps using the wrong test runner" — and the skill routes itself. Setup is the default.
## Step 2: Answer only what the code can't
Point it at a repo if you're not already in one. If the path resolves to more than one working tree, it asks which before writing anything.
For an existing codebase the skill scans first — code, configs, planning docs, any docs folders — and then asks in short rounds: confirmations of what it inferred (with evidence, so a confirm takes seconds), then only the genuinely unknowable things — landmines, frozen areas, org requirements. It never asks a question the code could answer. Anything you bring from outside the repo (org handbooks, wiki exports, an MCP knowledgebase) gets mined the same way — mention it when asked what sources you have.
## Step 2: Tell it what you bring
## Step 3: Review what exists
The first thing it does is read what's already there — `AGENTS.md`, `CLAUDE.md`, editor rule files, docs — and report back what's good, what's derivable filler, and what looks stale. A hand-written file is a baseline it improves, never something it discards.
You get a kernel (`kernel.md`) that stays under its instruction budget and a bundle of small entries, each marked `verified` (you confirmed it) or `generated` (inferred, unconfirmed). The `index.md` is script-generated. Nothing describes what the code already says — if an entry does, the audit deletes it.
Then it asks what rules you want followed regardless of what the repo does: governance, security and compliance requirements, coding standards, style guides, frozen areas. Bring outside documents too — org handbooks, wiki exports, an MCP knowledgebase.
Keep it healthy over time:
For a greenfield project that conversation is the whole content. For a working codebase it's the half no scan can reach.
- **Refresh** after real change — it diffs against the last run and never re-asks what you already settled
- **Audit** on demand — staleness sweep, path checks, and the pruning test; total size holds or shrinks
- **Query** — other skills (and you) can ask questions answered from the bundle with trust metadata attached
## Step 3: It verifies the rest
It checks every path a line names, and reads your `package.json`, `Makefile` and CI config — not to copy the commands out, since an agent reads those directly, but to know what they already say so the block only carries what they don't.
Then it asks what no scan could answer: what agents keep getting wrong here, what's off limits, what a domain term means, and which commands come with a catch.
## Step 4: Approve the block
You see the complete block before anything is written. Nothing lands without that. On approval it's spliced between the `<!-- bmad:context -->` markers, and everything you wrote outside them is preserved byte for byte.
It never commits. Changes stay in your working tree for you to review.
At the end it tells you what went in, what was left out and why, and the reasoning behind both.
## Keeping it healthy
- **Refresh** after real change — re-checks that the caveats still hold, diffs deletions and renames since the recorded commit, updates what moved, and never re-asks what you already settled
- **Record** the moment an agent gets something wrong — that's the only admissible source for a pitfall line
- **Audit** on demand — re-verifies everything and prunes; the block ends smaller or equal, never larger
A rule stays until the thing it guards is gone or you retire it. Nothing broke lately is never a reason to delete one — a working rule erases its own evidence.
## Repo or home directory
What this writes belongs committed to the repo, shared by the team. If the same rules keep repeating across all your projects, or they're your personal preferences, put those in your agent's global configuration in your home directory instead.
## Deprecated predecessors
:::note[Looking for bmad-generate-project-context or bmad-document-project?]
Both are deprecated and forward here — their trigger phrases still work. An existing `project-context.md` keeps loading for backwards compatibility and becomes a mining source on your next ingest.
Both are deprecated and forward here — their trigger phrases still work. If you have an existing `project-context.md`, setup offers to absorb its content rather than orphaning it.
:::
## Next steps
+5 -5
View File
@@ -109,13 +109,13 @@ this structure, agents make inconsistent decisions.
### Project Context
:::tip[Recommended]
Build your project context so AI agents follow your project's rules and preferences across all workflows: a small
always-loaded kernel plus a bundle of verified entries, maintained by `bmad-project-context`. Seed it from your
architecture at the end of planning, or mine it from an existing codebase at any time.
Set up your repo so AI agents follow your project's rules across all workflows: a small verified block in
`AGENTS.md`, maintained by `bmad-project-context`. Seed it from your architecture at the end of planning, or
discover it from an existing codebase at any time.
:::
**How to create it:**
- Run `bmad-project-context` — greenfield (seeded from your spec or architecture) or brownfield (mined from the codebase, then confirmed with you). The earlier `bmad-generate-project-context` is deprecated and forwards there; an existing `project-context.md` keeps loading.
- Run `bmad-project-context` — greenfield (seeded from your spec or architecture) or brownfield (discovered from the codebase, verified, then confirmed with you). The earlier `bmad-generate-project-context` is deprecated and forwards there; an existing `project-context.md` is offered up for absorption.
[**Learn more about project-context.md**](../explanation/project-context.md)
[**Learn more about project context**](../explanation/project-context.md)
@@ -101,5 +101,5 @@ skill = "bmad-prfaq"
[[agent.menu]]
code = "PC"
description = "Curate the verified project context AI agents load — kernel + knowledge bundle (ingest, query, audit)"
description = "Set up or refresh this repo's agent instructions — verified commands, policy, conventions, pitfalls (setup, refresh, record, audit)"
skill = "bmad-project-context"
+1 -1
View File
@@ -1,6 +1,6 @@
module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs
BMad Method,_meta,,,,,,,,,false,https://docs.bmad-method.org/llms.txt,
BMad Method,bmad-project-context,Project Context,PC,"Curate the verified project context AI agents load: a small always-loaded kernel plus a knowledge bundle. Ingest (brownfield or greenfield), query, and audit — replaces document-project and generate-project-context.",,,anytime,,,false,project_knowledge,kernel.md + context bundle
BMad Method,bmad-project-context,Project Context,PC,"Set up or refresh a repo's agent instructions so AI agents work well in it: verified commands, policy, conventions that differ from defaults, and known pitfalls. Setup, refresh, record, and audit — replaces document-project and generate-project-context.",,,anytime,,,false,repo root,AGENTS.md managed block
BMad Method,bmad-build,Build,BD,Official Phase 4 implementation loop: clarify intent plan implement review and present.,,,ship,bmad-sprint-planning,bmad-code-review,true,implementation_artifacts,spec and project implementation
BMad Method,bmad-spec,Spec,SPC,"Use to distill any intent input (brief, PRD, transcript, brain dump, design folder, mixed multi-source) into a succinct, no-fluff SPEC.md contract + companions that downstream work derives from. Locks the WHAT before the HOW. Works for software, game design, research, editorial, policy, business, anything intent-bearing. Validation mode also available.",,[path],anytime,,,false,{output_folder}/specs/spec-{slug},SPEC.md + companion files
BMad Method,bmad-correct-course,Correct Course,CC,Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories.,,,anytime,,,false,planning_artifacts,change proposal
1 module skill display-name menu-code description action args phase preceded-by followed-by required output-location outputs
2 BMad Method _meta false https://docs.bmad-method.org/llms.txt
3 BMad Method bmad-project-context Project Context PC Curate the verified project context AI agents load: a small always-loaded kernel plus a knowledge bundle. Ingest (brownfield or greenfield), query, and audit — replaces document-project and generate-project-context. Set up or refresh a repo's agent instructions so AI agents work well in it: verified commands, policy, conventions that differ from defaults, and known pitfalls. Setup, refresh, record, and audit — replaces document-project and generate-project-context. anytime false project_knowledge repo root kernel.md + context bundle AGENTS.md managed block
4 BMad Method bmad-build Build BD Official Phase 4 implementation loop: clarify intent plan implement review and present. ship bmad-sprint-planning bmad-code-review true implementation_artifacts spec and project implementation
5 BMad Method bmad-spec Spec SPC Use to distill any intent input (brief, PRD, transcript, brain dump, design folder, mixed multi-source) into a succinct, no-fluff SPEC.md contract + companions that downstream work derives from. Locks the WHAT before the HOW. Works for software, game design, research, editorial, policy, business, anything intent-bearing. Validation mode also available. [path] anytime false {output_folder}/specs/spec-{slug} SPEC.md + companion files
6 BMad Method bmad-correct-course Correct Course CC Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories. anytime false planning_artifacts change proposal
@@ -5,6 +5,10 @@ description: 'Deprecated — forwards to bmad-project-context. Use when the user
# DEPRECATED — forwards to bmad-project-context
Tell the user: this skill is deprecated — `bmad-project-context` now owns this job, and instead of generating documentation volume it curates a small verified context system (an always-loaded kernel plus a knowledge bundle). Invoke `bmad-project-context` next time.
Tell the user two things.
Then invoke `bmad-project-context` with **ingest** intent, forwarding the user's original request and any paths or documents they supplied, verbatim. It takes the workflow from here.
First: this skill is deprecated. Generating documentation volume about a codebase made agents worse, not better — agents read code more accurately than prose describing code, and the generated set was stale on arrival. `bmad-project-context` owns what remains useful: a small verified block in the repo's `AGENTS.md` carrying what the code cannot say — required policy, conventions that differ from defaults, what running the project takes that no config file states, and known pitfalls.
Second, so they are not surprised by what they get: the deeper "explain this system, its rationale and its history" material is a different altitude and is not part of that block. It is coming as its own capability. If that is what they were after, say so plainly rather than producing a thin substitute.
Then invoke `bmad-project-context` with **setup** intent, forwarding the user's original request and any paths or documents they supplied, verbatim. It takes the workflow from here.
@@ -5,6 +5,6 @@ description: 'Deprecated — forwards to bmad-project-context. Use when the user
# DEPRECATED — forwards to bmad-project-context
Tell the user: this skill is deprecated — `bmad-project-context` now owns this job. Instead of one generated `project-context.md`, it curates a small verified context system (an always-loaded kernel plus a knowledge bundle); any existing `project-context.md` keeps loading and becomes a mining source. Invoke `bmad-project-context` next time.
Tell the user: this skill is deprecated — `bmad-project-context` now owns this job. Instead of one generated `project-context.md`, it writes a small verified block inside the repo's `AGENTS.md`, and any existing `project-context.md` is offered up for absorption rather than left orphaned. Invoke `bmad-project-context` next time.
Then invoke `bmad-project-context` with **ingest** intent, forwarding the user's original request and any inputs they supplied (architecture doc, spec, preferences), verbatim. It takes the workflow from here.
Then invoke `bmad-project-context` with **setup** intent, forwarding the user's original request and any inputs they supplied (architecture doc, spec, standards, preferences), verbatim. It takes the workflow from here.
@@ -1,61 +1,109 @@
---
name: bmad-project-context
description: 'Write and maintain a project''s agent guide (AGENTS.md): verified commands, repo policy, non-default conventions, entry points, and known agent pitfalls. Use when the user says "project context", "document project", "generate project context", "set up AGENTS.md", "refresh context", "audit context", or wants to record a mistake agents keep making'
description: 'Set up or refresh agent instructions so AI agents work well in it. Use when the user says "project context", "set up AGENTS.md", "document this project", "refresh context", "audit context", wants to apply coding standards or governance to a repo, or wants to record a mistake agents keep making'
---
# Overview
The product is a good agent guide — a short `AGENTS.md` every session loads, plus an `AGENTS-dev.md` for sessions doing hands-on work with the code. What helps agents is a short guide where every line passed its section's admission rule and its facts are verified. The section plan in `references/guide-contract.md` is fixed: the job is to fill it with verified evidence, not to explore the repository for interesting facts. The repository is where claims get verified; the knowledge itself comes from configuration, history, observed agent mistakes, and the people who maintain the project.
A conversation that produces a repository's agent instructions: a small verified block inside `AGENTS.md`. The user brings rules they want followed — governance, security, standards — and the repository supplies the rest, verified.
Works with a full BMad install or standalone in any repo.
Conversational always; the user approves every write.
**Args:** intent (`bootstrap` | `refresh` | `record` | `audit`); `--auto` for headless; a scope path to limit the run; extra source paths or URLs. Supplied values are used directly and skip their questions.
**Args:** intent (`setup` | `refresh` | `record` | `audit`); a target repo or path; extra source paths or URLs. Supplied values skip their questions.
## Resolution rules
- Bare paths and `{skill-root}` (e.g. `references/guide-contract.md`) resolve from this skill's installed directory.
- Bare paths and `{skill-root}` (e.g. `references/best-practices.md`) resolve from this skill's installed directory.
- `{project-root}` → the project working directory.
- **Target** → the repository being described, defaulting to `{project-root}`. If it resolves to more than one working tree, or to one the user cannot commit in, ask before writing.
## On Activation
1. Resolve customization: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly and use defaults. Execute `{workflow.activation_steps_prepend}`; treat `{workflow.persistent_facts}` entries as standing context (`file:` = paths/globs to load, others verbatim).
2. Config: if `{project-root}/_bmad` exists, `uv run {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` and read `{user_name}`, `{communication_language}` (use it every turn), `{output_folder}`. Standalone: skip, and default `{output_folder}` to `_bmad-output`.
3. Read the existing agent instruction files — root and nested `AGENTS.md`, `CLAUDE.md` and similar — and the memlog at `{output_folder}/project-context/.memlog.md` if present.
4. Detect intent and greet `{user_name}`: **bootstrap** (no memlog — this skill's first run here, whether the repo has no guide, a poor one, or a good handwritten one to build on — the default), **refresh** (memlog exists; update the guide to match the repo), **record** (the user reports an observed agent mistake, lesson, or new rule), **audit** (re-verify and prune). For interactive bootstrap/refresh, ask one opening question: any sources outside the repo (org handbooks, wikis, planning docs, MCP knowledgebases) and any area to focus on — note paths for later, don't read them yet. Add `{workflow.external_sources}` entries to the same list. Execute `{workflow.activation_steps_append}`.
2. Config: if `{project-root}/_bmad` exists, `uv run {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` and read `{user_name}`, `{communication_language}` (use it every turn), `{output_folder}`. Standalone: skip.
3. **Load `references/best-practices.md` and `references/template.md` before anything else.** Every decision below is made against them.
4. Detect intent and greet `{user_name}`: **setup** (no block in the target — the default), **refresh** (a block exists), **record** (the user reports a mistake agents made), **audit** (re-verify and prune). Fold `{workflow.external_sources}` into the source list. Execute `{workflow.activation_steps_append}`.
## Bootstrap and Refresh
## Setup and Refresh Steps
Load `references/guide-contract.md` and `references/evidence.md` before step 1. The order matters: plan before scanning, select before writing.
No writes until step 5!
1. **Plan the guide.** Instantiate the contract's section plan for this repo and list, per section, the evidence it needs. A handwritten `AGENTS.md` is the baseline being improved, never raw material to discard: map its content into the plan first.
2. **Gather evidence.** Launch parallel subagents, one per evidence source in `references/evidence.md` (sources 15; the maintainer, source 6, is step 4). Each returns candidates for the memlog: claim, evidence paths, target section, what behavior it changes, verification status. The scan-scope rules in that file bind every scanner.
3. **Verify.** Run the commands the guide will state and path-check every claim that names a file. Read-only commands run freely; a mutating command (a build, a test suite) waits for its go-ahead, asked as the interview's first question. A claim verified by execution or path-check is verified — never ask the user to confirm it.
4. **Interview.** Only what no scan can reach: org requirements, frozen areas, domain concepts, intent — and always "what do agents keep getting wrong here?". Rules in `references/evidence.md`. Write every answer and rejection to the memlog as it arrives.
5. **Compose.** For each accepted candidate, first ask whether a hook, lint rule, or CI check would enforce it better than prose — if yes, propose the check to the user, and the guide line becomes the fallback if they decline (a check that lands deletes its line). Then decide what each section says from the remaining candidates and write the guide as one coherent document under the contract. Copy-editing comes last; it is not how selection happens. Where an instruction outside the guide contradicts it in a way that changes behavior (a stale `CLAUDE.md` line, a retired command still recommended) — rewording and overlap are not contradictions — propose the concrete fix to that file; leaving two live contradictory instructions is a defect.
6. **Coverage check and close.** Go through the memlog: every accepted candidate must trace to a guide line, a scoped guide, or a rejection with a reason — and, in the other direction, every line in the guide must trace to a memlog candidate. A line with no candidate slipped in at composition time without evidence: backfill it with real evidence or delete it. Check every repo-relative path the guide names against the filesystem; fix dead links before closing. Confirm the guide fits the contract's budget. Tell the user what was written, what was rejected and why, and — whenever `AGENTS.md` carries the guide — that a harness which doesn't auto-load `AGENTS.md` needs its own file to pull it in (e.g. a `CLAUDE.md` containing `@AGENTS.md`).
### 1. Assess and report
**Refresh:** same steps against the existing guide and memlog. Never re-ask what a prior run settled; the interview shrinks to one recall question — what changed about how the team works since the last run — plus whatever new evidence raises. Re-verify the commands and paths the guide states; run `git log --diff-filter=DR --name-only` since the last run and check every deleted or renamed path against every guide line; update or remove lines whose evidence is gone. The guide grows only when new evidence justifies it.
Read `AGENTS.md`, harness or agent specific rule files, docs folders, and any notes carrying lessons. Report what exists and how it measures up, per `best-practices.md`.
**Greenfield:** same process, based on a spec or planning document (or interview alone). Commands that don't exist yet are written as explicit TODO placeholders from the decided stack — never a guessed invocation stated as fact — and verified on the first refresh after code exists. A genuinely contested design decision — real tradeoffs, multiple viable shapes — deserves the `bmad-architecture` skill rather than a call made here.
If the target contains separable units — a workspace manifest listing members, or directories carrying their own build manifest — name them and ask whether this run covers the root only, all of them, or which. Absent that evidence, do not ask. Sibling repositories are not children; each is its own target, offered in turn.
### 2. Ask what they bring
Rules to follow regardless of what the repo does: governance, security and compliance, coding standards, style guides, frozen areas. Ask for outside documents too — handbooks, wikis, architecture docs, MCP knowledgebases. Note the paths; do not read them yet.
Greenfield: this is the whole content. Brownfield: it is the half no scan reaches.
### 3. Discover and verify
Fan out with parallel subagents against what the sections need — executable config and CI for policy and for what they already state, tracked source for conventions and boundaries, targeted history for constraints whose reason must still hold.
`package.json`, a `Makefile`, `pyproject.toml`, and CI config are read to know what the block must not repeat. Their caveats come from the human in step 4. Path-check every claim naming a file.
Each child agreed in step 1 is scanned as its own scope, against its own manifests.
### 4. Interview the gaps
Only what no scan reaches: what agents keep getting wrong here, what is off limits, what a domain term means, why a constraint exists.
- Never ask what a scan could answer. Asking the user to confirm a path-checked claim, or one a config file already states, is a defect.
- Ask recall questions, not review lists. Never hand the user a selection problem a scan created.
- A mistake this session made and caught is observed evidence — offer it.
- Batches of at most eight; fewer is better. A batch yielding nothing new means write.
- When the repo contradicts the user, show the evidence and ask. Never write the claim as given, never drop it silently.
### 5. Show the block, then write it
Compose against `template.md`. For each candidate, ask first whether a hook, lint rule, or CI check enforces it better than prose; if so propose the check, and the line becomes the fallback if they decline.
**Show the complete block before writing it**, and every child block alongside it — one approval covers the set. On approval, splice between the markers, leaving everything outside them byte-identical. Fill each provenance line with today's date and the verified SHA.
Where an instruction elsewhere contradicts the block in a way that changes behavior — a stale `CLAUDE.md` line, a retired command — propose the fix to that file. Two live contradictory instructions is a defect.
Never commit.
### 6. Close
- What went in, and what was left out and why.
- Why, in the user's terms, from `best-practices.md` — why it is small, why what the repo already states stays out, why a pitfall line stays until its cause is gone.
- How it loads, and that other harness files can point at it.
- Maintenance: re-run after significant change, `record` the moment an agent gets something wrong, prefer a check over a new line.
- Rules repeating across their projects, or personal rather than the team's, belong in their global agent config.
### Refresh
Same steps, step 1 as a diff. Read the provenance line, re-verify every path and every caveat, and run `git log --diff-filter=DR --name-only` since the recorded SHA against every line — update or remove lines whose evidence is gone. Never re-ask what a prior run settled; the interview shrinks to what changed about how the team works. The block grows only on new evidence.
### Greenfield
Seeded from a spec or planning document, or interview alone. Commands that do not exist yet are written as explicit TODOs naming the decided stack, never a guessed invocation stated as fact, and verified on the first refresh after code exists. A genuinely contested design decision — real tradeoffs, multiple viable shapes — goes to `bmad-architecture`.
### Migration
If the target has a `project-context.md` from the retired skills, commonly under `{output_folder}`, read it in step 1 and offer to absorb its content. Do not delete it without agreement, and do not silently orphan it.
## Record
Record one observed agent mistake or lesson at the moment it happens. Get the task, the mistake, the correction, and the evidence (a session, a review comment, the user's testimony); log it to the memlog. A first occurrence is a candidate; a recurring or costly mistake gets a line in the guide's pitfalls section now — write it, show the diff. If the mistake is mechanically preventable, propose the hook, lint, or CI check instead: enforcement beats prose.
Capture one observed agent mistake as it happens — the only admissible source for a pitfall line.
Take the task, the mistake, the correction, and its evidence. Check the block for a line already covering it. One occurrence is noted; a recurring or costly mistake earns a line now — write it, show the diff. If it is mechanically preventable, propose the hook, lint rule, or CI check instead.
## Audit
Run every command the guide states, path-check every named file, apply the contract's pruning test to every line, and check for contradictions with other agent instruction files. Lines that fail move to a scoped guide, get fixed, or are deleted — present proposed deletions for confirmation (interactive) before removing. A pitfall or policy line is deleted only when the thing it guards is gone or the user retires it; absence of recent failures is never grounds. Audit ends with the guide smaller or equal, never larger.
Re-check every caveat, path-check every file, follow every pointer, and ask of every line whether removing it would change agent behavior. Check for contradictions with other instruction files.
## Modes
Failing lines move behind an observable trigger, get fixed, or are deleted — confirm deletions first. **A policy or pitfall line goes only when the thing it guards is gone or the user retires it; nothing failing lately is not grounds.** Audit ends smaller or equal.
Interactive is the default. **Auto mode** (headless, or on request) never asks: it skips the interview, writes only what repository evidence supports, and logs every open question and assumption to the memlog so the next interactive run starts there. When an existing guide is not this skill's own work (the memlog doesn't record writing it), auto mode never rewrites it in place: it writes the proposal to `AGENTS.md.proposed` (and `AGENTS-dev.md.proposed`), logs it, and leaves the merge to an interactive run. When invoked headless: if intent is neither supplied nor inferable, halt with a `blocked` JSON status and `reason`. End with JSON:
## Children
```json
{"status": "complete", "intent": "bootstrap", "guide": "AGENTS.md",
"dev_guide": "AGENTS-dev.md", "scoped_guides": ["src/billing/AGENTS.md"],
"memlog": "_bmad-output/project-context/.memlog.md"}
```
A component, nested repository, or extracted rules file gets its own file under the same shape when work keeps landing there and its truths do not belong at the parent level. Rules bounded to a directory go in a nested `AGENTS.md` there, attached by location. Use a linked file only when the trigger is not a path.
## Finalize
A chosen child that ends with nothing its parent does not already say gets no file. Say so and move on.
Confirm the memlog reflects the run — every candidate has a disposition, every interview answer is recorded — and run `{workflow.on_complete}` if non-empty. This skill never commits: everything it wrote stays as uncommitted working-tree changes for the user to review and commit.
List every child in the parent's **Where things are** with one line and its path. Discovery never depends on the harness finding it.
@@ -16,8 +16,8 @@ activation_steps_append = []
persistent_facts = []
on_complete = ""
# Standing outside-the-repo sources fed into every bootstrap/refresh evidence
# sweep (untrusted until verified against the repo or user-confirmed).
# Standing outside-the-repo sources offered at every setup/refresh run
# (untrusted until verified against the repo or user-confirmed).
# Append-only. Entries: "file:{project-root}/..." or "file:/abs/path" for
# docs, "skill:name" to consult a skill, plain text for a standing fact,
# "tool:name" for an MCP knowledgebase.
@@ -1,42 +0,0 @@
[
{
"query": "generate project context for this repo",
"should_trigger": true
},
{
"query": "can you document this project so AI agents understand it",
"should_trigger": true
},
{
"query": "set up an AGENTS.md for this codebase",
"should_trigger": true
},
{
"query": "refresh the project context, a lot has changed",
"should_trigger": true
},
{
"query": "audit our project context, I think it's stale",
"should_trigger": true
},
{
"query": "the agent keeps using jest syntax in our vitest repo, record that so it stops",
"should_trigger": true
},
{
"query": "write a product brief for my new app idea",
"should_trigger": false
},
{
"query": "document my REST API endpoints for the public docs site",
"should_trigger": false
},
{
"query": "help me design the architecture for a new service",
"should_trigger": false
},
{
"query": "summarize this codebase for me real quick",
"should_trigger": false
}
]
@@ -0,0 +1,65 @@
# What belongs in a repo's agent instructions
Rules for deciding what goes in the block, for judging what a repo already has, and for explaining both to the user.
## The test
Can an agent derive this by reading the repository? If yes, leave it out — a stored copy is a stale duplicate of something the agent reads more accurately first-hand, and it is charged on every session. Write down what the code cannot say.
## Admit
- **Policy the code cannot express** — branch rules, frozen and protected paths, generated files, secrets, security and compliance. Stated by a human or read off an enforcing config, never inferred.
- **What a config file cannot say about running the project** — the root test script does nothing in this workspace, integration tests need a service up first, the suite takes eleven minutes so iterate on single files, the `Makefile` is the real entry point and `package.json` is vestigial, CI runs a typecheck the test script does not. The invocation itself is already stated in `package.json`, `Makefile`, `pyproject.toml`, or CI config and does not earn a line — the correction or the caveat does.
- **Conventions that differ from ecosystem defaults.** An agent follows the norm unless told otherwise, so only the divergences earn a line.
- **Pitfalls with observed evidence** — a recorded lesson, the maintainer's recollection, the same mistake fixed repeatedly in history, or one this session made and caught. A repo yields hundreds of trap-looking facts and none of them predict real mistakes; only observed behavior does. A surprising scan finding is a question to ask, not a line to write.
- **Runtime behavior invisible from the repo** — replaying webhooks, lying health endpoints, environment quirks — once a human confirms it.
- **Entry points and pointers** to where work lands.
Prefer prohibitions to advice, and name the permitted alternative in the same line.
## Exclude
| | Why |
|---|---|
| Repo overviews, directory trees, stack lists | Derived fresh, more accurately; stored copies rot |
| Anything included for being interesting | Interest is not need |
| Style rules an agent self-enforces | Belongs in a formatter, linter, hook, or CI check — propose the check instead |
| Platitudes | Already the default |
| Commands already stated in `package.json`, a `Makefile`, or CI config | Read from the source of truth; a copy drifts the moment a script is renamed |
| Pasted code, changelog content, fast-changing facts | Stale immediately |
| Aspirational state | Describe what is; intent belongs in specs |
| History and edit narration | Git holds it; state present truth |
## Retire
A policy or pitfall line goes only when the thing it guards is gone, or the user retires it. Nothing failing lately is not evidence — a working rule erases its own evidence.
Every other line faces one question at each write: would removing it change agent behavior? If no, cut it.
## Size
Every line is paid in every session, and instruction-following degrades as the loaded set grows. Count what other always-loaded files add. Over budget means cut the weakest lines or move them behind a trigger — never raise the budget. Ten lines of evidence means ten lines.
## Retrieval
An index the agent must choose to fetch gets skipped; one already in context does not. Keep everything load-bearing in the block. A pointer out of it names a trigger the agent can observe — a path, a file type, a named task — never one it must judge ("when the task is complex") or track about itself ("before your first edit").
Rules bounded to a directory go in a nested `AGENTS.md` there, attached by location rather than by pointer. Use a linked file only when the trigger is not a path.
## Maintain
- Re-check that caveats still hold — a slow suite that got fast, a workaround for a bug that was fixed.
- Diff deletions and renames since the verified SHA against every line.
- Record provenance in the block so the next run knows what it is diffing from.
- Capture mistakes when they happen, not at review time. One occurrence is a note; recurrence earns a line.
- Route anything mechanically preventable to a hook, lint rule, or CI check. A check that lands deletes its line.
## Repo or home directory
This block belongs committed: shared by the team, consistent across machines, versioned with the code it constrains.
Two things belong in the user's global agent config instead — rules repeating across all their projects, and personal preferences that are theirs rather than the team's.
## Judging an existing file
Report, in this order: what is derivable filler, what is unverifiable or stale, what is missing against the sections above, and what is already good. Keep recorded lessons by default — they are maintainer testimony, and are challenged only with evidence that the thing they name is gone or wrong.
@@ -1,48 +0,0 @@
# Evidence
Where guide content comes from, how it is verified, and how the run is recorded. Verification establishes that a claim is true; a claim is useful only if one of the guide's sections needs it. A verified fact no section needs is rejected, with the reason in the memlog.
## Sources
Sources 15 are scanner work; source 6 is the interview. Later sources hold what earlier ones cannot.
1. **Existing recorded instructions and lessons** — root and nested `AGENTS.md`, `CLAUDE.md`, editor rule files, and lessons written anywhere nearby (notes files, warnings in READMEs). What agents are told today: the baseline, plus conflicts and stale claims to fix. Recorded lessons are standing maintainer testimony — kept by default, challenged only with evidence that a referent is gone or wrong.
2. **Executable configuration and CI** — manifests, lockfiles, workflow files, hooks, Makefiles, linter configs. Most of Commands, Verification, and Policy comes from here, verified by execution.
3. **Tracked source** — boundaries (vendored, generated, frozen), conventions that differ from defaults, entry points. Scanned to answer the section plan's questions, never for novelty: an interesting fact no agent needs is noise, and a trap-looking fact is at most an interview question, never a pitfall line.
4. **Git history** — targeted, never wholesale: when the current state contains a surprising constraint, find the change that introduced it and any reverted attempts to remove it, then check the reason still holds today. Also the source for commit-message and branch-naming conventions, read off recent history. Commit messages are past intent, not current truth.
5. **Agent session logs and review corrections** — when available or pointed at: the only source of *observed* agent mistakes. Extract structure, never transcripts: task, mistake, correction, consequence, occurrence count, source sessions. One occurrence is a candidate; recurrence makes a pitfall line. Ignore one-off noise (tool outages, typos); route mechanically preventable mistakes to a proposed hook, lint, or CI check instead of prose.
6. **The human** — org requirements, domain concepts, frozen areas, intent, priorities, and mistakes they've watched agents make. No scan substitutes.
Documents the user names from outside the repo (org handbooks, wikis, prior architecture docs, MCP knowledgebases) are treated like source 3: scanned for candidates, untrusted until verified against the repo or confirmed by the user.
## Scan scope — binding on every scanner
Scanners read tracked files (`git ls-files`). Dependency, vendored, generated, build-output, and cache directories are out unless a specific claim requires looking inside one, and then the scanner states why. Scanners return candidates with evidence; they never decide what gets written.
## The memlog
`{output_folder}/project-context/.memlog.md` — this skill's memory across runs, kept with the shared memlog tool: `uv run {project-root}/_bmad/scripts/memlog.py init|append --workspace {output_folder}/project-context ...` (standalone, when the script is absent: create and append the same one-line entries by hand). Append-only — nothing is edited or removed; a change of mind or a late answer is a new entry, and a claim's current state is its latest entry.
One entry per fact, the moment it happens, typed by what it is:
```markdown
- (candidate) single test runs need --gtest_filter; suite names don't match file names — sources: test/CMakeLists.txt; section: Commands; changes: agent trusts a 0-test green run; verification: executed; disposition: guide
- (answer by user) no external docs — one-person shop, the maintainer is the source of truth
- (rejection) directory tree — excluded by contract: repo overviews
- (conflict) AGENTS.md says git-commits, CLAUDE.md says tam-commit — asked
- (disposition) git-commits line → deleted; tam-commit is current, on the maintainer's answer
- (assumption) auto mode: treated master as the trunk, unconfirmed
```
A candidate entry carries its sources, target section, what an agent does wrong without it, verification status (`executed | path-checked | user-confirmed | unverified`), and disposition (`guide | scoped:<path> | rejected — <reason> | pending`); when the disposition is settled later or changes, append a new `(disposition)` entry rather than rewriting. Interview answers, rejections with reasons, conflicts found in other instruction files, and auto-mode assumptions all land as entries when they happen. Refresh and audit read the memlog first and don't revisit a recorded disposition unless its evidence changed. Agents never load the memlog; nothing in it counts against the guide's budget.
## Interview rules
- **Never ask what a scan could answer.** A claim verified by execution or path-check proceeds as verified; asking the user to confirm it is a defect.
- **Ask recall questions, never review lists.** "What do agents keep getting wrong?" works because the maintainer's memory has already selected what mattered. Never hand the human a selection problem a scan created.
- A mistake this session itself made and caught while reading or verifying the repo is an observed agent failure (sample of one) — worth offering as a question.
- Ask in batches of at most eight questions; fewer is better. Prefer open questions ("what do agents keep getting wrong here?") over confirmations.
- An unverifiable claim from docs or an outside document is asked as "the docs say X — still true?", never stated as fact.
- When the repo contradicts the user's own testimony, show the evidence and ask — never write the claim as given, never drop it silently. Either the claim or the reading of the evidence gets corrected, and the outcome is recorded in the memlog.
- Before writing, one closing question: say in a line what the guide will contain and ask what's missing — a frozen area, an org rule, a recurring mistake. This material is unrecoverable by any later scan.
- A batch that yields nothing new means it is time to write, not to ask more. Off-topic information the user offers is recorded in the memlog, never ignored.
@@ -1,99 +0,0 @@
# Guide Contract
The guide is the project's agent instructions, written as two files at the repo root:
- **`AGENTS.md`** — loaded by every session, whatever its kind: Orientation, Policy, Where things are, and a closing pointer: "Before your first build, test run, or file edit: read `AGENTS-dev.md` — verified commands, conventions, and known pitfalls. Only planning, reviewing, or answering questions? Skip it." The trigger is the *action*, not the session's kind — a session rarely knows it is a hands-on session until the moment it is.
- **`AGENTS-dev.md`** — read via that pointer by sessions doing hands-on work with the code: Commands, Verification, Conventions, Known pitfalls. Sessions that only plan, review, or answer questions never pay for it.
When the whole guide fits in about 20 instructions, write a single `AGENTS.md` instead — the extra hop isn't worth it. When the maintainer names another frequent session kind (UX, manual testing, data work), it may get its own `AGENTS-<kind>.md` behind its own pointer line, under the same rules as `AGENTS-dev.md` — but a kind with only a few instructions doesn't earn a file, and the kinds come from the maintainer, never from guessing at the repo. Rules that differ by *module* rather than by session kind are the other axis: those go in scoped guides (below), which the harness loads by location. Every line in any of these files has a recurring cost; this contract governs every write.
## Hard rules
- **Instruction budget: ~150200 instructions across everything a coding session loads — a ceiling, not a target.** Instruction-following degrades past this range. Count instructions, not lines (one line carrying three rules is three instructions), and count what `CLAUDE.md` or other always-loaded files add. When the budget is exceeded, the weakest lines move behind links or are deleted; the budget is never raised.
- **Priority order.** Rules whose violation costs the most come first, so a reader who stops halfway got the half that matters most.
- **The pruning test.** *Would removing this line change agent behavior?* If no, delete it. Applied to every line at every write.
## Sections and what admits a line
Each section has its own admission rule. There is no global "non-derivable" test: some sections admit derivable content on purpose, and no section admits content merely for being true.
1. **Orientation** (`AGENTS.md`) — three or four sentences: what this project is, the stack, where planning, tickets, PRs, and deeper docs live. No admission test beyond brevity.
2. **Policy and safety** (`AGENTS.md`) — admitted by **authority**: what the org and the maintainers require and the code cannot express — branch rules, protected and frozen paths, generated files, secrets, what must never be done.
3. **Where things are** (`AGENTS.md`) — admitted by **localization value**: entry points where work actually lands, and "working on X? read Y first" pointers. Planning sessions need these as much as hands-on ones. Earned per pointer, never exhaustive. Details go behind links, never inline.
4. **Commands** (`AGENTS-dev.md`) — admitted by **universal need, verified by execution**: build, test (including a single test), lint, run — exact invocations with flags, plus warnings where an operation is expensive or the obvious guess fails. Derivability is no objection: rediscovery is paid at the start of every session, and a derived command is a guess — both trials found repos where the obvious guess is wrong.
5. **Verification** (`AGENTS-dev.md`) — same rule: what must pass before commit and push, as the exact commands CI runs.
6. **Conventions that differ from defaults** (`AGENTS-dev.md`) — admitted when **the agent's default assumption is wrong**: an agent writing new code follows ecosystem norms unless told otherwise. Each line links its enforcement point or source file. Not admitted for being unusual, intricate, or interesting — a fact nobody would get wrong by default is not a convention line.
7. **Known pitfalls** (`AGENTS-dev.md`) — admitted by **observed failure only**: a lesson already recorded in the repo's instruction files or notes, the maintainer's recollection, session-log evidence, the same mistake fixed repeatedly in git history, or a mistake the writing session itself made and caught while working. A scan cannot nominate a pitfall from how the code *looks*: the repo yields hundreds of trap-looking facts and no property of the fact separates the few that cause real mistakes — that signal exists only in observed behavior. A surprising fact from scanning becomes, at most, an interview question ("do agents actually trip on this?"). Apparent derivability is irrelevant here in both directions: most working pitfall rules restate something readable, because agents misread it anyway.
**Retiring pitfall and policy lines:** a line retires only when the thing it guards is gone (removed, or now mechanically enforced) or the human retires it. Absence of recent failures is never grounds — a working rule erases its own evidence, and half the value of the guide is failures that no longer happen.
## What never enters
| Excluded | Why |
|---|---|
| Repo overviews, directory trees, tech-stack lists | Agents derive structure fresh, and stored copies drift |
| Facts included for being interesting or unusual | Interest is not evidence of need — this is the failure mode this skill replaces |
| Style rules an agent is meant to self-enforce | That job belongs to a formatter, linter, hook, or CI check — propose one instead |
| Platitudes ("write clean code") | Already the agent's default |
| Fast-changing facts, pasted code, changelog content | Go stale quickly; the guide is not a memory system |
| Aspirational state | Belongs in specs; the guide describes what is |
## Shape and style
Terse imperative lines under plain headings — no prose paragraphs beyond Orientation, no introduction, no summary. Every line states what to do or what not to do; a bare fact may appear only as the justification clause of such a line ("Exclude `vendor/` from searches — it is 60% of the tracked files", never "`vendor/` is 60% of the tracked files"). Use the contract's section headings so the guide's shape is recognizable across runs; material that seems to need a new section folds into the nearest one. A prohibition names the permitted alternative — "never force-push; use a new branch" — because a bare "never" leaves the agent stuck at the moment it matters. At most two emphasis markers (IMPORTANT, YOU MUST, bold caps) in the whole guide; past that, emphasis stops working. State present truth only; git holds history. Every named decision, doc, file, or system includes a repo-relative path or URL that exists. Target shape:
`AGENTS.md`:
```markdown
# acme-billing
Payment-processing service for Acme storefronts. TypeScript/Node, pnpm.
Planning lives in docs/planning/, tickets in Linear (ACME board), PRs on GitHub.
## Policy
- Never push to main; PRs only, one approval required.
- `legacy/` is frozen: never modify; it is being replaced.
- `src/generated/` is generated by `pnpm codegen` — never edit by hand.
## Where things are
- Webhook handling: src/routes/webhooks.ts; conventions in docs/webhooks.md
- Working on billing rules? Read docs/billing-model.md first.
Before your first build, test run, or file edit: read `AGENTS-dev.md`
verified commands, conventions, and known pitfalls. Only planning, reviewing,
or answering questions? Skip it.
```
`AGENTS-dev.md`:
```markdown
# Working on acme-billing code
## Commands
- Test: `pnpm test` (vitest — do NOT use jest syntax); single file: `pnpm test -- path/to/file`
- The full suite is slow; run single files while iterating.
## Before pushing
- `pnpm lint && pnpm test` must pass — same commands CI runs.
## Conventions that differ from defaults
- Money is integer cents (`amountCents`), never floats — src/lib/money.ts
- All DB access through repositories in src/repos/ — never call the client directly.
## Known pitfalls
- Stripe webhooks replay in staging every 6h — handlers must be idempotent.
- Agents keep adding jest matchers; this repo is vitest-only.
```
In the single-file form the same sections merge into one `AGENTS.md`, Policy first, and the pointer line disappears.
## Editing an existing guide
A handwritten guide is the baseline, not raw material. Keep its phrasing where it works, propose changes as a diff, and never delete human-written content without agreement. Its recorded lessons — wherever they live: the old `AGENTS.md`, `CLAUDE.md`, notes files, warnings in READMEs — are standing maintainer testimony: keep them by default, and challenge one only with evidence that its referent is gone or wrong, never because scans show no recent failures. Restructuring a single old file into the two-file form is fine; losing its content is not.
## Scoped guides
A subsystem gets its own nested `AGENTS.md` when work keeps landing there and its truths don't belong at root. Discovery must not rely on the harness: every scoped guide gets a "working on X? read Y first" pointer in the root guide's Where things are section — harnesses that auto-load the nearest file make it load twice as reliably, but the pointer is the mechanism. 2535 lines answering, in order: what is this, who owns it, how do I run it, what's surprising, where do I go next. Every path verified. Created when a subsystem needs one, never for every subsystem.
## Small guides are success
When the evidence supports ten lines, ten lines is the deliverable.
@@ -0,0 +1,55 @@
# Block shape
Sections in this order. Omit any section with nothing that passes its rule — never write an empty one. Admission rules: `best-practices.md`.
1. **Orientation** — three or four sentences: what this is, the stack, where planning and deeper docs live.
2. **Policy** — what the org requires.
3. **Where things are** — entry points, and pointers to children and linked files.
4. **Running and verifying** — only what `package.json`, a `Makefile`, or CI config does not already say.
5. **Conventions that differ from defaults**
6. **Known pitfalls**
Terse imperative lines under plain headings. No prose beyond Orientation, no introduction, no summary. A bare fact appears only as the justification clause of an instruction — "Exclude `vendor/` from searches, it is 60% of tracked files", never "`vendor/` is 60% of tracked files". A prohibition names the alternative. At most two emphasis markers in the whole block.
## Worked example
````markdown
<!-- bmad:context -->
<!-- Verified 2026-08-08 against a1b2c3d. Managed by bmad-project-context; edits inside this block are replaced on refresh. Keep anything you want preserved outside the markers. -->
## acme-billing
Payment processing for Acme storefronts. TypeScript/Node, pnpm, Postgres. Planning lives in `docs/planning/`, tickets in Linear (ACME board).
## Policy
- Never push to main; PRs only, one approval.
- Never modify `legacy/` — frozen, being replaced. New work goes in `src/`.
- Never hand-edit `src/generated/` — run `pnpm codegen`.
## Where things are
- Webhook handling: `src/routes/webhooks.ts`; conventions in `docs/webhooks.md`
- Writing a migration? Read `docs/db-rules.md` first — ordering, transaction boundaries, pool limits.
- Billing service has its own guide: `services/billing/AGENTS.md`
## Running and verifying
- Run single test files while iterating; the full suite takes ~11 minutes.
- Integration tests need `docker compose up -d` first, and fail confusingly without it.
- CI also runs `pnpm typecheck`, which `pnpm test` does not cover.
## Conventions that differ from defaults
- Money is integer cents (`amountCents`), never floats — `src/lib/money.ts`
- All DB access goes through repositories in `src/repos/`; never call the client directly.
## Known pitfalls
- Stripe webhooks replay in staging every 6h — handlers must be idempotent.
- Use vitest matchers, not jest — agents repeatedly add jest syntax here.
<!-- /bmad:context -->
````
Fill the provenance line with the real date and the commit SHA verified against. Refresh diffs from that SHA.
@@ -77,7 +77,7 @@ Activation is complete. If `activation_steps_prepend` or `activation_steps_appen
| Architecture | `{planning_artifacts}/*architecture*.md` (whole) or `{planning_artifacts}/*architecture*/*.md` (sharded) | FULL_LOAD |
| UX Design | `{planning_artifacts}/*ux*.md` (whole) or `{planning_artifacts}/*ux*/*.md` (sharded) | FULL_LOAD |
| Spec | `{planning_artifacts}/*spec-*.md` (whole) | FULL_LOAD |
| Document Project | `{project_knowledge}/index.md` (sharded) | INDEX_GUIDED |
| Project Context | `AGENTS.md` in the affected repo (the `bmad:context` block) | FULL_LOAD |
## Execution
@@ -95,12 +95,11 @@ Activation is complete. If `activation_steps_prepend` or `activation_steps_appen
- Process the combined content as a single document
4. **Priority**: If both whole and sharded versions exist, use the whole document
**Discovery Process for INDEX_GUIDED documents (Document Project):**
**Discovery Process for Project Context:**
1. **Search for index file** - Look for `{project_knowledge}/index.md`
2. **If found**: Read the index to understand available documentation sections
3. **Selectively load sections** based on relevance to the change being analyzed — do NOT load everything, only sections that relate to the impacted areas
4. **This document is optional** — skip if `{project_knowledge}` does not exist (greenfield projects)
1. **Read `AGENTS.md`** in the repo the change affects — the block between the `bmad:context` markers carries the policy, frozen paths, and conventions a course correction must respect.
2. **Follow only the pointers that relate to the impacted areas** — nested component files or linked rule files listed under "Where things are". Do not load them all.
3. **This document is optional** — skip if the repo has no `AGENTS.md` (greenfield projects).
**Fuzzy matching**: Be flexible with document names — users may use variations like `prd.md`, `bmm-prd.md`, `product-requirements.md`, etc.
-657
View File
@@ -1,657 +0,0 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# ///
"""context.py — mechanics for bmad-project-context.
One core runtime script (memlog.py lineage): everything mechanical about a project's
context bundle so no LLM ever guesses at mechanical facts. The bundle lives at the
project's `project_knowledge` folder (default `docs/`); the script manages only files
bearing conformant frontmatter and never touches foreign files.
Commands (all accept --json):
validate [root] frontmatter + link + index check; exit 1 on findings
index [root] regenerate index.md (refuses to overwrite a foreign one)
sweep [root] [--today D] staleness report (stale_after passed; sources drifted)
resolve <name> [--refresh] cross-project resolution: self > workspace > cache > remote
compass <path> [root] nearest compass file covering a repo-relative path
sync materialize kernel/compass blocks into AGENTS.md files
(only under the agent-files/both placement)
bootstrap copy this script to {project-root}/_bmad/scripts/context.py
"""
import argparse
import datetime as dt
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
INDEX_MARKER = "<!-- bmad:context index — generated; do not edit -->"
BLOCK_START = "<!-- bmad:context -->"
BLOCK_END = "<!-- /bmad:context -->"
REGISTRY_FILE = "registry.yaml"
REGISTRY_KEY = "bmad_obeya_registry"
DEFAULT_KNOWLEDGE = "docs"
def fail(msg, code=1):
print(msg, file=sys.stderr)
sys.exit(code)
def emit(data, as_json, human=""):
if as_json:
print(json.dumps(data))
elif human:
print(human)
# ── config ───────────────────────────────────────────────────────────────────
# One resolution, shared by every command. Delegates to the installed BMad
# resolver (resolve_config.py, four-layer TOML merge) whenever it is present so
# script and platform can never disagree; falls back to a native TOML merge,
# then legacy YAML files, then defaults.
CONFIG_KEYS = ("project_name", "project_knowledge", "output_folder",
"context_placement", "obeya_remote", "user_name",
"communication_language", "document_output_language")
CONFIG_DEFAULTS = {"project_knowledge": DEFAULT_KNOWLEDGE, "output_folder": "_bmad-output"}
TOML_LAYERS = ("_bmad/config.toml", "_bmad/config.user.toml",
"_bmad/custom/config.toml", "_bmad/custom/config.user.toml")
YAML_LAYERS = ("_bmad/config.yaml", "_bmad/bmm/config.yaml",
"_bmad/bmm/config.user.yaml", "_bmad/context.yaml")
try:
import tomllib
except ImportError: # Python 3.10: TOML layers skipped, YAML fallback still works
tomllib = None
def _installed_resolver_config(project_root: Path):
resolver = project_root / "_bmad" / "scripts" / "resolve_config.py"
if not (resolver.exists() and (project_root / "_bmad" / "config.toml").exists()):
return None
proc = subprocess.run(
[sys.executable, str(resolver), "--project-root", str(project_root)],
capture_output=True, text=True)
if proc.returncode != 0:
return None
try:
data = json.loads(proc.stdout)
except json.JSONDecodeError:
return None
return {k: v for k, v in data.items() if isinstance(v, (str, int, bool))}
def _toml_chain(project_root: Path):
if tomllib is None or not (project_root / TOML_LAYERS[0]).exists():
return None
merged = {}
for rel in TOML_LAYERS:
f = project_root / rel
if not f.exists():
continue
try:
with f.open("rb") as fh:
layer = tomllib.load(fh)
except (tomllib.TOMLDecodeError, OSError):
continue
merged.update({k: v for k, v in layer.items() if isinstance(v, (str, int, bool))})
return merged
def _yaml_chain(project_root: Path):
merged = {}
for rel in YAML_LAYERS: # later layers win per key
f = project_root / rel
if not f.exists():
continue
for line in f.read_text(encoding="utf-8").splitlines():
m = re.match(r"^([A-Za-z_][\w-]*):\s*(.+?)\s*$", line)
if m:
merged[m.group(1)] = m.group(2).strip("'\"")
return merged
def resolve_full_config(project_root: Path) -> dict:
cfg = (_installed_resolver_config(project_root)
or _toml_chain(project_root)
or {})
for k, v in _yaml_chain(project_root).items():
cfg.setdefault(k, v) # YAML fills gaps (e.g. standalone context_placement), never overrides TOML
for k, v in CONFIG_DEFAULTS.items():
cfg.setdefault(k, v)
return cfg
def bundle_root(project_root: Path, override: str | None, cfg: dict) -> Path:
raw = override or str(cfg.get("project_knowledge", DEFAULT_KNOWLEDGE))
raw = raw.replace("{project-root}/", "").replace("{project-root}", "")
return (project_root / raw) if not Path(raw).is_absolute() else Path(raw)
def cmd_config(args, project_root, cfg, as_json):
out = {k: cfg.get(k) for k in CONFIG_KEYS}
out["bundle_root"] = str(bundle_root(project_root, None, cfg))
emit(out, as_json, "\n".join(f"{k}: {v}" for k, v in out.items() if v is not None))
# ── frontmatter ──────────────────────────────────────────────────────────────
def parse_frontmatter(text: str):
"""Returns (fields|None, error|None, body). Naive flat YAML subset."""
if not text.startswith("---\n"):
return None, None, text
end = text.find("\n---\n", 4)
if end == -1:
return None, "unparseable frontmatter (no closing fence)", ""
fields = {}
for line in text[4:end].splitlines():
if not line.strip() or line.startswith("#"):
continue
m = re.match(r"^([A-Za-z_][\w-]*):\s*(.*?)\s*$", line)
if not m:
return None, "unparseable frontmatter", ""
key, val = m.group(1), m.group(2)
if val.startswith("[") and val.endswith("]"):
fields[key] = [v.strip().strip("'\"") for v in val[1:-1].split(",") if v.strip()]
else:
fields[key] = re.split(r"\s+#", val)[0].strip().strip("'\"")
return fields, None, text[end + 5:]
def load_entries(root: Path):
"""Conformant entries at the bundle root: (path, fields|None, error|None, body)."""
out = []
if not root.is_dir():
return out
for f in sorted(root.glob("*.md")):
if f.name in ("kernel.md", "index.md"):
continue
fields, err, body = parse_frontmatter(f.read_text(encoding="utf-8"))
if fields is None and err is None:
continue # foreign file: no frontmatter
if fields is not None and "type" not in fields and "title" not in fields:
continue # foreign file: frontmatter but not ours
out.append((f, fields, err, body))
return out
def load_compasses(root: Path):
out = []
cdir = root / "compass"
if not cdir.is_dir():
return out
for f in sorted(cdir.glob("*.md")):
fields, err, body = parse_frontmatter(f.read_text(encoding="utf-8"))
out.append((f, fields or {}, err, body))
return out
def trust_of(fields: dict) -> str:
return "verified" if "verified" in fields else "generated"
def index_rows(entries):
rows = []
for f, fields, err, _ in entries:
if err or not fields or "type" not in fields or "title" not in fields:
continue
rows.append(f"- [{fields['title']}]({f.name}) — {fields.get('description', '')} "
f"({fields['type']}, {trust_of(fields)})")
return rows
def render_index(entries) -> str:
return INDEX_MARKER + "\n\n" + "\n".join(index_rows(entries)) + "\n"
# ── validate ─────────────────────────────────────────────────────────────────
def cmd_validate(args, project_root, cfg, as_json):
root = bundle_root(project_root, args.root, cfg)
findings = []
entries = load_entries(root)
for f, fields, err, body in entries:
rel = f.name
if err:
findings.append({"file": rel, "issue": err})
continue
for req in ("type", "title", "description"):
if req not in fields:
findings.append({"file": rel, "issue": f"missing required field: {req}"})
has_v, has_g = "verified" in fields, "generated" in fields
if has_v == has_g:
findings.append({"file": rel, "issue":
"exactly one of verified/generated is required"})
for m in re.finditer(r"\[[^\]]*\]\(([^)]+)\)", body):
target = m.group(1)
if target.startswith(("http://", "https://", "#")) or not target.endswith(".md"):
continue
if not (f.parent / target).exists():
findings.append({"file": rel, "issue": f"dangling link: {target}"})
for f, fields, err, _ in load_compasses(root):
rel = f"compass/{f.name}"
if err:
findings.append({"file": rel, "issue": err})
elif "area" not in fields:
findings.append({"file": rel, "issue": "missing required field: area"})
idx = root / "index.md"
listable = {f.name for f, fields, err, _ in entries
if not err and fields and "type" in fields and "title" in fields}
if not idx.exists():
if listable:
findings.append({"file": "index.md", "issue": "index.md missing — run: index"})
else:
text = idx.read_text(encoding="utf-8")
linked = set(re.findall(r"\]\(([^)]+\.md)\)", text))
for name in sorted(listable - linked):
findings.append({"file": name, "issue": f"entry {name} missing from index.md"})
for name in sorted(linked - listable):
findings.append({"file": "index.md", "issue": f"index row points to missing entry: {name}"})
stats = {"kernel": {"lines": 0, "bullets": 0, "tokens": 0}, "entries": {}, "bundle_tokens": 0}
kernel = root / "kernel.md"
if kernel.exists():
ktext = kernel.read_text(encoding="utf-8")
kfields, kerr, kbody = parse_frontmatter(ktext)
if kerr:
findings.append({"file": "kernel.md", "issue": kerr})
elif kfields:
for key in kfields:
if key != "status":
findings.append({"file": "kernel.md",
"issue": f"kernel frontmatter key not allowed: {key}"})
body = kbody or ktext
lines = [ln for ln in body.splitlines() if ln.strip()]
bullets = [ln for ln in lines if ln.lstrip().startswith("- ")]
stats["kernel"] = {"lines": len(lines), "bullets": len(bullets),
"tokens": int(len(body) / 4)}
if len(bullets) > 200 or len(lines) > 250:
findings.append({"file": "kernel.md",
"issue": f"kernel over instruction budget: {len(bullets)} bullets / "
f"{len(lines)} lines (ceiling ~200 instructions)"})
total = stats["kernel"]["tokens"]
for f, fields, err, body in entries:
toks = int(len(body) / 4)
stats["entries"][f.name] = toks
total += toks
if toks > 400:
findings.append({"file": f.name,
"issue": f"entry ~{toks} tokens, approaching a page — "
f"split into two entries or cut"})
stats["bundle_tokens"] = total
emit({"ok": not findings, "findings": findings, "stats": stats}, as_json,
"\n".join(f"{x['file']}: {x['issue']}" for x in findings) or "clean")
sys.exit(1 if findings else 0)
# ── index ────────────────────────────────────────────────────────────────────
def cmd_index(args, project_root, cfg, as_json):
root = bundle_root(project_root, args.root, cfg)
idx = root / "index.md"
first_line = (idx.read_text(encoding="utf-8").splitlines() or [""])[0] if idx.exists() else ""
if first_line and INDEX_MARKER not in first_line:
fail(f"refusing to overwrite foreign index.md at {idx} — move it, or point "
f"project_knowledge at a clean folder")
entries = load_entries(root)
content = render_index(entries)
if not idx.exists() or idx.read_text(encoding="utf-8") != content:
idx.write_text(content, encoding="utf-8")
emit({"ok": True, "entries": len(index_rows(entries)), "written": str(idx)}, as_json)
# ── sweep ────────────────────────────────────────────────────────────────────
def source_date(project_root: Path, source: str):
proc = subprocess.run(["git", "log", "-1", "--format=%cI", "--", source],
cwd=str(project_root), capture_output=True, text=True)
if proc.returncode == 0 and proc.stdout.strip():
return proc.stdout.strip()[:10]
p = project_root / source
if p.exists():
return dt.date.fromtimestamp(p.stat().st_mtime).isoformat()
return None
PATH_TOKEN = re.compile(r"`([^`\s]+/[^`\s]+)`")
def missing_body_paths(project_root: Path, text: str):
"""Backticked repo-relative paths whose top directory exists but the file does not."""
out = []
for tok in PATH_TOKEN.findall(text):
tok = tok.strip().rstrip("/")
if "://" in tok or tok.startswith(("{", "<", "-", "~", "/")) or "*" in tok:
continue
first = tok.split("/")[0]
if (project_root / first).is_dir() and not (project_root / tok).exists():
out.append(tok)
return out
def cmd_sweep(args, project_root, cfg, as_json):
root = bundle_root(project_root, args.root, cfg)
today = args.today or dt.date.today().isoformat()
stale, missing = [], []
kernel = root / "kernel.md"
if kernel.exists():
for tok in missing_body_paths(project_root, kernel.read_text(encoding="utf-8")):
missing.append({"file": "kernel.md", "reason": f"path `{tok}` does not exist"})
for f, fields, err, body in load_entries(root):
if err or not fields:
continue
rel = f.name
if fields.get("stale_after") and str(fields["stale_after"]) < today:
stale.append({"file": rel, "reason": f"stale_after {fields['stale_after']} passed"})
sources = fields.get("sources") or []
for s in (sources if isinstance(sources, list) else [sources]):
if "://" in s:
continue
if not (project_root / s).exists():
missing.append({"file": rel, "reason": f"source {s} does not exist"})
elif fields.get("verified"):
changed = source_date(project_root, s)
if changed and changed > str(fields["verified"]):
stale.append({"file": rel,
"reason": f"source {s} changed {changed}, after verified {fields['verified']}"})
for tok in missing_body_paths(project_root, body):
missing.append({"file": rel, "reason": f"path `{tok}` does not exist"})
emit({"stale": stale, "missing": missing}, as_json,
"\n".join(f"{x['file']}: {x['reason']}" for x in stale + missing) or "clean")
# ── compass ──────────────────────────────────────────────────────────────────
def nearest_compass(root: Path, target: str):
best, best_len = None, -1
for f, fields, err, body in load_compasses(root):
area = str(fields.get("area", "")).rstrip("/")
if err or not area:
continue
if target == area or target.startswith(area + "/"):
if len(area) > best_len:
best, best_len = (f, fields, body), len(area)
return best
def cmd_compass(args, project_root, cfg, as_json):
root = bundle_root(project_root, args.root, cfg)
hit = nearest_compass(root, args.path.rstrip("/"))
if not hit:
emit({"path": None, "content": None}, as_json)
return
f, fields, body = hit
emit({"path": str(f), "area": fields.get("area"), "content": body}, as_json)
if not as_json:
sys.stdout.write(f.read_text(encoding="utf-8"))
# ── resolve ──────────────────────────────────────────────────────────────────
def parse_registry(path: Path) -> dict:
"""Minimal indent parser for the obeya registry: projects.<name>.<key>: value."""
projects, current = {}, None
in_projects = False
for line in path.read_text(encoding="utf-8").splitlines():
if not line.strip() or line.strip().startswith("#"):
continue
indent = len(line) - len(line.lstrip())
key, _, val = line.strip().partition(":")
val = val.strip().strip("'\"")
if indent == 0:
in_projects = key == "projects"
elif in_projects and indent == 2:
current = key
projects[current] = {}
elif in_projects and indent >= 4 and current:
projects[current][key] = val
return projects
def find_workspace(project_root: Path):
"""Walk up for sibling checkouts and an obeya checkout. Returns (siblings, registry)."""
siblings, registry = {}, None
node = project_root.parent
for _ in range(6):
if not node or node == node.parent:
break
try:
children = [c for c in node.iterdir() if c.is_dir()]
except OSError:
break
for c in children:
reg = c / REGISTRY_FILE
if registry is None and reg.exists() and REGISTRY_KEY in reg.read_text(encoding="utf-8"):
registry = parse_registry(reg)
if c != project_root:
siblings.setdefault(c.name, c)
node = node.parent
return siblings, registry
def cache_dir() -> Path:
return Path(os.environ.get("BMAD_CONTEXT_CACHE", str(Path.home() / ".bmad" / "context-cache")))
def cache_lookup(project: str):
pointer = cache_dir() / f"{project}.latest.json"
if pointer.exists():
try:
meta = json.loads(pointer.read_text(encoding="utf-8"))
sha = meta["sha"]
except (json.JSONDecodeError, OSError, KeyError, TypeError):
return None
path = cache_dir() / f"{project}@{sha}"
if path.is_dir():
return {"path": str(path), "sha": sha,
"fetched_at": meta.get("fetched_at"), "source": "cache"}
return None
def sparse_fetch(project: str, record: dict):
remote, branch = record["remote"], record.get("branch", "main")
context_root = record.get("context_root", DEFAULT_KNOWLEDGE)
with tempfile.TemporaryDirectory() as tmp:
clone = Path(tmp) / "clone"
proc = subprocess.run(
["git", "clone", "-q", "--depth", "1", "--filter=blob:none", "--sparse",
"--branch", branch, remote, str(clone)],
capture_output=True, text=True)
if proc.returncode != 0:
return None, proc.stderr.strip()
subprocess.run(["git", "-C", str(clone), "sparse-checkout", "set", context_root],
capture_output=True, check=False)
sha = subprocess.run(["git", "-C", str(clone), "rev-parse", "HEAD"],
capture_output=True, text=True).stdout.strip()
src = clone / context_root
if not src.is_dir():
return None, f"context root {context_root!r} not present in {remote}"
dest = cache_dir() / f"{project}@{sha}"
if dest.exists():
shutil.rmtree(dest)
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(src, dest)
fetched_at = dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds")
pointer = cache_dir() / f"{project}.latest.json"
tmp_pointer = pointer.with_suffix(".json.tmp")
tmp_pointer.write_text(json.dumps({"sha": sha, "fetched_at": fetched_at}), encoding="utf-8")
os.replace(tmp_pointer, pointer)
return {"path": str(dest), "sha": sha, "fetched_at": fetched_at,
"source": "remote"}, None
def git_head(path: Path):
proc = subprocess.run(["git", "rev-parse", "HEAD"], cwd=str(path),
capture_output=True, text=True)
return proc.stdout.strip() if proc.returncode == 0 else None
def sibling_bundle(checkout: Path):
sib_cfg = resolve_full_config(checkout)
root = bundle_root(checkout, None, sib_cfg)
return root if (root / "kernel.md").exists() or (root / "index.md").exists() else None
def cmd_resolve(args, project_root, cfg, as_json):
project, _, entry = args.name.partition(":")
result = None
self_name = cfg.get("project_name") or project_root.name
if project == self_name:
result = {"path": str(bundle_root(project_root, None, cfg)),
"sha": git_head(project_root), "fetched_at": None, "source": "local"}
siblings, registry = (None, None)
if result is None:
siblings, registry = find_workspace(project_root)
checkout = siblings.get(project)
root = sibling_bundle(checkout) if checkout else None
if root:
result = {"path": str(root), "sha": git_head(checkout),
"fetched_at": None, "source": "local"}
if result is None and not args.refresh:
result = cache_lookup(project)
if result is None:
if registry is None and cfg.get("obeya_remote"):
fetched, _ = sparse_fetch("obeya", {"remote": cfg["obeya_remote"],
"context_root": "."})
if fetched:
reg = Path(fetched["path"]) / REGISTRY_FILE
if reg.exists():
registry = parse_registry(reg)
record = (registry or {}).get(project)
if record:
result, err = sparse_fetch(project, record)
if result is None:
cached = cache_lookup(project)
if cached:
cached["warning"] = f"remote unreachable ({err}); serving cache"
result = cached
if result is None:
fail(f"cannot resolve {project!r}: not this project, no workspace checkout, "
f"no cache, and no registry record")
if entry:
hit = next((p for p in Path(result["path"]).glob("*.md")
if p.stem == entry), None)
if not hit:
fail(f"entry {entry!r} not found in {project!r} bundle at {result['path']}")
result["path"] = str(hit)
emit(result, as_json, result["path"])
# ── sync ─────────────────────────────────────────────────────────────────────
def strip_frontmatter(text: str) -> str:
fields, err, body = parse_frontmatter(text)
return text if fields is None and err is None and not body else (body or text)
def rewrite_links(content: str, source_dir: Path, target_dir: Path) -> str:
"""Re-anchor relative .md links so they resolve from the file the block lands in."""
def repl(m):
text, target = m.group(1), m.group(2)
if target.startswith(("http://", "https://", "#", "/")) or not target.endswith(".md"):
return m.group(0)
resolved = (source_dir / target).resolve()
return f"[{text}]({Path(os.path.relpath(resolved, target_dir)).as_posix()})"
return re.sub(r"\[([^\]]*)\]\(([^)]+)\)", repl, content)
def apply_block(target: Path, content: str, dry: bool = False) -> bool:
block = f"{BLOCK_START}\n{content.rstrip()}\n{BLOCK_END}\n"
if target.exists():
text = target.read_text(encoding="utf-8")
if BLOCK_START in text and BLOCK_END in text:
pre = text[:text.index(BLOCK_START)]
post = text[text.index(BLOCK_END) + len(BLOCK_END):].lstrip("\n")
new = pre + block + post
else:
new = text.rstrip("\n") + "\n\n" + block
else:
new = block
if target.exists() and target.read_text(encoding="utf-8") == new:
return False
if not dry:
target.write_text(new, encoding="utf-8")
return True
def cmd_sync(args, project_root, cfg, as_json):
placement = cfg.get("context_placement")
if placement not in ("agent-files", "both"):
fail(f"sync runs only under the agent-files or both placement "
f"(context_placement is {placement!r})")
root = bundle_root(project_root, None, cfg)
dry = getattr(args, "dry_run", False)
written = []
kernel = root / "kernel.md"
if kernel.exists():
content = rewrite_links(strip_frontmatter(kernel.read_text(encoding="utf-8")),
root, project_root)
if apply_block(project_root / "AGENTS.md", content, dry):
written.append(str(project_root / "AGENTS.md"))
for f, fields, err, body in load_compasses(root):
area = str(fields.get("area", "")).rstrip("/")
if err or not area:
continue
area_dir = project_root / area
if area_dir.is_dir():
if apply_block(area_dir / "AGENTS.md",
rewrite_links(body, root / "compass", area_dir), dry):
written.append(str(area_dir / "AGENTS.md"))
emit({"written": written, "dry_run": dry}, as_json, "\n".join(written) or "up to date")
# ── bootstrap ────────────────────────────────────────────────────────────────
def cmd_bootstrap(args, project_root, cfg, as_json):
target = project_root / "_bmad" / "scripts" / "context.py"
target.parent.mkdir(parents=True, exist_ok=True)
self_bytes = Path(__file__).resolve().read_bytes()
if not target.exists() or target.read_bytes() != self_bytes:
target.write_bytes(self_bytes)
emit({"path": str(target)}, as_json, str(target))
# ── main ─────────────────────────────────────────────────────────────────────
def main(argv=None):
p = argparse.ArgumentParser(prog="context.py", description=__doc__)
p.add_argument("--json", action="store_true", dest="as_json")
p.add_argument("--project-root", default=".")
jp = argparse.ArgumentParser(add_help=False) # lets --json follow the subcommand too
jp.add_argument("--json", action="store_true", dest="as_json", default=argparse.SUPPRESS)
sub = p.add_subparsers(dest="command", required=True)
s = sub.add_parser("validate", parents=[jp])
s.add_argument("root", nargs="?")
s = sub.add_parser("index", parents=[jp])
s.add_argument("root", nargs="?")
s = sub.add_parser("sweep", parents=[jp])
s.add_argument("root", nargs="?")
s.add_argument("--today")
s = sub.add_parser("resolve", parents=[jp])
s.add_argument("name")
s.add_argument("--refresh", action="store_true")
s = sub.add_parser("compass", parents=[jp])
s.add_argument("path")
s.add_argument("root", nargs="?")
s = sub.add_parser("sync", parents=[jp])
s.add_argument("--dry-run", action="store_true", dest="dry_run")
sub.add_parser("bootstrap", parents=[jp])
sub.add_parser("config", parents=[jp])
args = p.parse_args(argv)
project_root = Path(args.project_root).resolve()
cfg = resolve_full_config(project_root)
{"validate": cmd_validate, "index": cmd_index, "sweep": cmd_sweep,
"resolve": cmd_resolve, "compass": cmd_compass, "sync": cmd_sync,
"bootstrap": cmd_bootstrap, "config": cmd_config}[args.command](
args, project_root, cfg, args.as_json)
if __name__ == "__main__":
main()
-572
View File
@@ -1,572 +0,0 @@
# /// script
# requires-python = ">=3.10"
# dependencies = ["pytest>=8.0"]
# ///
"""Layer-1 deterministic tests for context.py (bmad-project-context mechanics).
Run: uv run --with pytest pytest src/scripts/tests/test_context.py
Written against the CLI contract in the skill design (§13/§14): each command has a
fixture-driven case list. No LLM, no network — "remote" fixtures are local bare repos
reached via file:// URLs.
"""
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path
import pytest
SCRIPTS_DIR = Path(__file__).resolve().parent.parent
CONTEXT_PY = SCRIPTS_DIR / "context.py"
sys.path.insert(0, str(SCRIPTS_DIR))
INDEX_MARKER = "<!-- bmad:context index"
BLOCK_START = "<!-- bmad:context -->"
BLOCK_END = "<!-- /bmad:context -->"
def run(args, cwd, env_extra=None, check=False):
env = os.environ.copy()
if env_extra:
env.update(env_extra)
proc = subprocess.run(
[sys.executable, str(CONTEXT_PY), *args],
cwd=str(cwd), capture_output=True, text=True, env=env,
)
if check and proc.returncode != 0:
raise AssertionError(f"context.py {args} failed rc={proc.returncode}\n{proc.stderr}")
return proc
def jrun(args, cwd, check=True):
proc = run(["--json", *args], cwd, check=check)
out = proc.stdout.strip()
return (json.loads(out) if out else {}), proc
def git(cwd, *args):
subprocess.run(["git", *args], cwd=str(cwd), capture_output=True, check=True)
def make_git_repo(path):
path.mkdir(parents=True, exist_ok=True)
git(path, "init", "-q", "-b", "main")
git(path, "config", "user.email", "t@t.t")
git(path, "config", "user.name", "t")
return path
ENTRY = """---
type: {type}
title: {title}
description: {description}
{trust}: {date}
{extra}---
{body}
"""
def write_entry(root, name, *, type="decision", title=None, description="d",
trust="verified", date="2026-01-01", extra="", body="Body text."):
root.mkdir(parents=True, exist_ok=True)
(root / f"{name}.md").write_text(ENTRY.format(
type=type, title=title or name, description=description,
trust=trust, date=date, extra=extra, body=body))
def make_bundle(root, entries=("integer-cents", "repo-pattern")):
root.mkdir(parents=True, exist_ok=True)
(root / "kernel.md").write_text("# Project Kernel — fixture\n## Commands\n- Test: `pytest`\n")
for e in entries:
write_entry(root, e)
return root
@pytest.fixture
def proj(tmp_path):
"""A project root with a bundle at docs/ and config naming it."""
p = tmp_path / "proj"
p.mkdir()
make_bundle(p / "docs")
cfg = p / "_bmad"
cfg.mkdir()
(cfg / "context.yaml").write_text("project_name: proj\nproject_knowledge: docs\n")
return p
def indexed(proj_root):
run(["index"], proj_root, check=True)
return proj_root
# ── validate ─────────────────────────────────────────────────────────────────
class TestValidate:
def test_clean_bundle_exit_0(self, proj):
indexed(proj)
proc = run(["validate"], proj)
assert proc.returncode == 0
def test_missing_type_caught(self, proj):
(proj / "docs" / "bad.md").write_text(
"---\ntitle: Bad\ndescription: d\nverified: 2026-01-01\n---\nBody.\n")
indexed(proj)
data, proc = jrun(["validate"], proj, check=False)
assert proc.returncode == 1
assert any("type" in f["issue"] for f in data["findings"])
def test_unparseable_frontmatter_caught(self, proj):
(proj / "docs" / "broken.md").write_text("---\ntype: decision\nno closing fence\n")
data, proc = jrun(["validate"], proj, check=False)
assert proc.returncode == 1
assert any("frontmatter" in f["issue"] for f in data["findings"])
def test_dangling_link_caught(self, proj):
write_entry(proj / "docs", "linker", body="See [gone](missing-entry.md).")
indexed(proj)
data, proc = jrun(["validate"], proj, check=False)
assert proc.returncode == 1
assert any("missing-entry.md" in f["issue"] for f in data["findings"])
def test_entry_missing_from_index_caught(self, proj):
indexed(proj)
write_entry(proj / "docs", "orphan")
data, proc = jrun(["validate"], proj, check=False)
assert proc.returncode == 1
assert any("orphan" in f["issue"] and "index" in f["issue"] for f in data["findings"])
def test_trust_field_required_and_exclusive(self, proj):
(proj / "docs" / "untrusted.md").write_text(
"---\ntype: decision\ntitle: U\ndescription: d\n---\nBody.\n")
write_entry(proj / "docs", "double", extra="generated: 2026-01-02\n")
indexed(proj)
data, proc = jrun(["validate"], proj, check=False)
issues = " | ".join(f["issue"] for f in data["findings"])
assert "untrusted.md" in " ".join(f["file"] for f in data["findings"])
assert "verified" in issues or "generated" in issues
def test_foreign_files_ignored(self, proj):
(proj / "docs" / "human-notes.md").write_text("# Just some human doc\nNo frontmatter.\n")
indexed(proj)
proc = run(["validate"], proj)
assert proc.returncode == 0
# ── index ────────────────────────────────────────────────────────────────────
class TestIndex:
def test_byte_identical_on_unchanged_bundle(self, proj):
indexed(proj)
first = (proj / "docs" / "index.md").read_bytes()
run(["index"], proj, check=True)
assert (proj / "docs" / "index.md").read_bytes() == first
def test_row_format(self, proj):
indexed(proj)
text = (proj / "docs" / "index.md").read_text()
assert INDEX_MARKER in text
assert "- [integer-cents](integer-cents.md) — d (decision, verified)" in text
def test_new_and_removed_entries_reflected(self, proj):
indexed(proj)
write_entry(proj / "docs", "newcomer", trust="generated", date="2026-02-01")
(proj / "docs" / "repo-pattern.md").unlink()
run(["index"], proj, check=True)
text = (proj / "docs" / "index.md").read_text()
assert "newcomer" in text and "(decision, generated)" in text
assert "repo-pattern" not in text
def test_hand_edits_overwritten(self, proj):
indexed(proj)
idx = proj / "docs" / "index.md"
idx.write_text(idx.read_text() + "\n- hand-added row\n")
run(["index"], proj, check=True)
assert "hand-added" not in idx.read_text()
def test_foreign_index_refused(self, proj):
(proj / "docs" / "index.md").write_text("# Docs site index\nHuman-owned.\n")
proc = run(["index"], proj)
assert proc.returncode != 0
assert "index.md" in proc.stderr
assert (proj / "docs" / "index.md").read_text().startswith("# Docs site index")
# ── sweep ────────────────────────────────────────────────────────────────────
class TestSweep:
def test_clean_bundle_empty_report(self, proj):
indexed(proj)
data, proc = jrun(["sweep"], proj)
assert data["stale"] == []
def test_stale_after_passed_flagged(self, proj):
write_entry(proj / "docs", "expiring", extra="stale_after: 2026-06-01\n")
indexed(proj)
data, _ = jrun(["sweep", "--today", "2026-07-01"], proj)
assert any(s["file"].endswith("expiring.md") for s in data["stale"])
data2, _ = jrun(["sweep", "--today", "2026-05-01"], proj)
assert not any(s["file"].endswith("expiring.md") for s in data2["stale"])
def test_sources_changed_after_verified_flagged(self, tmp_path):
p = make_git_repo(tmp_path / "gitproj")
(p / "_bmad").mkdir()
(p / "_bmad" / "context.yaml").write_text("project_name: gitproj\nproject_knowledge: docs\n")
(p / "src").mkdir()
(p / "src" / "money.py").write_text("CENTS = 1\n")
make_bundle(p / "docs", entries=())
write_entry(p / "docs", "drifted", date="2020-01-01",
extra="sources: [src/money.py]\n")
write_entry(p / "docs", "fresh", date="2099-01-01",
extra="sources: [src/money.py]\n")
git(p, "add", "-A")
git(p, "commit", "-qm", "init")
run(["index"], p, check=True)
data, _ = jrun(["sweep"], p)
files = [s["file"] for s in data["stale"]]
assert any(f.endswith("drifted.md") for f in files)
assert not any(f.endswith("fresh.md") for f in files)
# ── compass ──────────────────────────────────────────────────────────────────
class TestCompass:
def test_nearest_path_selection_with_nesting(self, proj):
cdir = proj / "docs" / "compass"
cdir.mkdir()
(cdir / "src.md").write_text(
"---\ntype: compass\ntitle: src\ndescription: d\narea: src\nverified: 2026-01-01\n---\nSRC COMPASS\n")
(cdir / "billing.md").write_text(
"---\ntype: compass\ntitle: billing\ndescription: d\narea: src/billing\nverified: 2026-01-01\n---\nBILLING COMPASS\n")
proc = run(["compass", "src/billing/handlers.py"], proj, check=True)
assert "BILLING COMPASS" in proc.stdout
proc2 = run(["compass", "src/other/x.py"], proj, check=True)
assert "SRC COMPASS" in proc2.stdout
def test_no_compass_empty_exit_0(self, proj):
proc = run(["compass", "src/anything.py"], proj)
assert proc.returncode == 0
assert proc.stdout.strip() == ""
# ── resolve ──────────────────────────────────────────────────────────────────
def make_remote_project(tmp_path, name="payments-api"):
"""A git project with a bundle, plus a bare clone acting as its remote."""
src = make_git_repo(tmp_path / f"{name}-src")
make_bundle(src / "docs", entries=("webhook-conventions",))
git(src, "add", "-A")
git(src, "commit", "-qm", "init")
bare = tmp_path / f"{name}.git"
subprocess.run(["git", "clone", "-q", "--bare", str(src), str(bare)], check=True,
capture_output=True)
return src, bare
def make_obeya(ws, registry: dict):
ob = ws / "obeya"
ob.mkdir(parents=True)
lines = ["bmad_obeya_registry: true", "projects:"]
for name, rec in registry.items():
lines.append(f" {name}:")
for k, v in rec.items():
lines.append(f" {k}: {v}")
(ob / "registry.yaml").write_text("\n".join(lines) + "\n")
return ob
class TestResolve:
@pytest.fixture
def ws(self, tmp_path):
"""Workspace: proj-a (cwd), sibling checkout, obeya with a remote-only record."""
ws = tmp_path / "ws"
a = ws / "proj-a"
a.mkdir(parents=True)
make_bundle(a / "docs")
(a / "_bmad").mkdir()
(a / "_bmad" / "context.yaml").write_text("project_name: proj-a\nproject_knowledge: docs\n")
sib = ws / "sibling-api"
make_bundle(sib / "docs", entries=("sib-entry",))
_, bare = make_remote_project(tmp_path)
make_obeya(ws, {
"payments-api": {"remote": str(bare), "branch": "main", "context_root": "docs"},
"sibling-api": {"remote": str(tmp_path / "nowhere.git"), "branch": "main",
"context_root": "docs"},
})
return ws
def cache_env(self, tmp_path):
return {"BMAD_CONTEXT_CACHE": str(tmp_path / "cache")}
def test_rung1_self(self, ws, tmp_path):
data, _ = jrun(["resolve", "proj-a"], ws / "proj-a")
assert data["source"] == "local"
assert Path(data["path"]) == ws / "proj-a" / "docs"
def test_rung2_workspace_sibling_beats_remote(self, ws, tmp_path):
data, _ = jrun(["resolve", "sibling-api"], ws / "proj-a")
assert data["source"] == "local"
assert Path(data["path"]) == ws / "sibling-api" / "docs"
def test_rung4_sparse_fetch_pins_sha(self, ws, tmp_path):
env = self.cache_env(tmp_path)
proc = run(["--json", "resolve", "payments-api"], ws / "proj-a", env_extra=env)
assert proc.returncode == 0, proc.stderr
data = json.loads(proc.stdout)
assert data["source"] == "remote"
assert len(data["sha"]) == 40
fetched = Path(data["path"])
assert (fetched / "kernel.md").exists()
assert f"payments-api@{data['sha']}" in str(fetched)
def test_rung3_cache_hit_after_fetch(self, ws, tmp_path):
env = self.cache_env(tmp_path)
run(["--json", "resolve", "payments-api"], ws / "proj-a", env_extra=env, check=True)
proc = run(["--json", "resolve", "payments-api"], ws / "proj-a", env_extra=env)
data = json.loads(proc.stdout)
assert data["source"] == "cache"
def test_offline_serves_cache_with_warning(self, ws, tmp_path):
env = self.cache_env(tmp_path)
run(["--json", "resolve", "payments-api"], ws / "proj-a", env_extra=env, check=True)
ob_reg = ws / "obeya" / "registry.yaml"
ob_reg.write_text(ob_reg.read_text().replace("payments-api.git", "gone.git"))
proc = run(["--json", "resolve", "payments-api", "--refresh"], ws / "proj-a", env_extra=env)
data = json.loads(proc.stdout)
assert data["source"] == "cache"
assert data.get("warning")
def test_entry_level_resolution(self, ws, tmp_path):
data, _ = jrun(["resolve", "sibling-api:sib-entry"], ws / "proj-a")
assert data["path"].endswith("sib-entry.md")
def test_unresolvable_nonzero_exit(self, ws, tmp_path):
proc = run(["resolve", "no-such-project"], ws / "proj-a",
env_extra=self.cache_env(tmp_path))
assert proc.returncode != 0
def test_no_obeya_no_resolver(self, tmp_path):
lone = tmp_path / "lone"
lone.mkdir()
make_bundle(lone / "docs")
proc = run(["resolve", "anything-remote"], lone,
env_extra=self.cache_env(tmp_path))
assert proc.returncode != 0
# ── sync ─────────────────────────────────────────────────────────────────────
class TestSync:
def set_placement(self, proj, value):
cfg = proj / "_bmad" / "context.yaml"
cfg.write_text(f"project_name: proj\nproject_knowledge: docs\ncontext_placement: {value}\n")
def test_creates_agents_md_when_absent(self, proj):
self.set_placement(proj, "agent-files")
data, _ = jrun(["sync"], proj)
agents = proj / "AGENTS.md"
assert agents.exists()
text = agents.read_text()
assert BLOCK_START in text and BLOCK_END in text
assert "Project Kernel" in text
def test_idempotent_second_run_zero_diff(self, proj):
self.set_placement(proj, "agent-files")
jrun(["sync"], proj)
first = (proj / "AGENTS.md").read_bytes()
jrun(["sync"], proj)
assert (proj / "AGENTS.md").read_bytes() == first
def test_user_content_byte_preserved(self, proj):
self.set_placement(proj, "agent-files")
user_text = "# My Agents File\n\nMy own rules — do not touch.\n"
(proj / "AGENTS.md").write_text(user_text)
jrun(["sync"], proj)
text = (proj / "AGENTS.md").read_text()
assert user_text.rstrip("\n") in text
assert BLOCK_START in text
(proj / "docs" / "kernel.md").write_text("# Project Kernel — fixture\n- Updated rule\n")
jrun(["sync"], proj)
text2 = (proj / "AGENTS.md").read_text()
assert user_text.rstrip("\n") in text2
assert "Updated rule" in text2
assert text2.count(BLOCK_START) == 1
def test_compass_nested_agents_files(self, proj):
self.set_placement(proj, "agent-files")
cdir = proj / "docs" / "compass"
cdir.mkdir()
(cdir / "billing.md").write_text(
"---\ntype: compass\ntitle: billing\ndescription: d\narea: src/billing\nverified: 2026-01-01\n---\nBILLING COMPASS\n")
(proj / "src" / "billing").mkdir(parents=True)
jrun(["sync"], proj)
nested = proj / "src" / "billing" / "AGENTS.md"
assert nested.exists() and "BILLING COMPASS" in nested.read_text()
def test_relative_links_rewritten_to_resolve_from_target(self, proj):
self.set_placement(proj, "agent-files")
(proj / "docs" / "kernel.md").write_text(
"# Project Kernel — fixture\n- Money is cents. Why: [integer-cents](integer-cents.md)\n")
cdir = proj / "docs" / "compass"
cdir.mkdir()
(cdir / "billing.md").write_text(
"---\ntype: compass\ntitle: billing\ndescription: d\narea: src/billing\nverified: 2026-01-01\n---\n"
"See [integer-cents](../integer-cents.md) and [kernel](../kernel.md).\n")
(proj / "src" / "billing").mkdir(parents=True)
jrun(["sync"], proj)
root_text = (proj / "AGENTS.md").read_text()
assert "(docs/integer-cents.md)" in root_text
assert "(integer-cents.md)" not in root_text.replace("docs/integer-cents.md", "")
nested = (proj / "src" / "billing" / "AGENTS.md").read_text()
assert "(../../docs/integer-cents.md)" in nested
assert "(../../docs/kernel.md)" in nested
def test_refuses_under_bmad_placement(self, proj):
self.set_placement(proj, "bmad")
proc = run(["sync"], proj)
assert proc.returncode != 0
assert not (proj / "AGENTS.md").exists()
# ── config ───────────────────────────────────────────────────────────────────
class TestConfig:
def test_central_toml_chain_with_user_override(self, tmp_path):
p = tmp_path / "proj"
(p / "_bmad").mkdir(parents=True)
(p / "_bmad" / "config.toml").write_text(
'project_name = "proj"\nproject_knowledge = "docs"\noutput_folder = "_bmad-output"\n')
(p / "_bmad" / "config.user.toml").write_text('project_knowledge = "knowledge"\n')
make_bundle(p / "knowledge")
data, _ = jrun(["config"], p)
assert data["project_knowledge"] == "knowledge"
assert data["bundle_root"].endswith("knowledge")
def test_all_commands_share_the_resolved_root(self, tmp_path):
p = tmp_path / "proj"
(p / "_bmad").mkdir(parents=True)
(p / "_bmad" / "config.toml").write_text('project_knowledge = "docs"\n')
(p / "_bmad" / "config.user.toml").write_text('project_knowledge = "knowledge"\n')
make_bundle(p / "knowledge")
run(["index"], p, check=True)
assert (p / "knowledge" / "index.md").exists()
assert not (p / "docs" / "index.md").exists()
proc = run(["validate"], p)
assert proc.returncode == 0
def test_standalone_yaml_fallback_and_defaults(self, proj, tmp_path):
data, _ = jrun(["config"], proj)
assert data["project_knowledge"] == "docs"
bare = tmp_path / "bare"
bare.mkdir()
data2, _ = jrun(["config"], bare)
assert data2["project_knowledge"] == "docs"
def test_json_accepted_after_subcommand(self, proj):
indexed(proj)
proc = run(["validate", "--json"], proj)
assert proc.returncode == 0
assert json.loads(proc.stdout)["ok"] is True
# ── sweep: missing sources ───────────────────────────────────────────────────
class TestSweepMissing:
def test_deleted_source_reported_missing(self, proj):
write_entry(proj / "docs", "ghost", extra="sources: [src/lib/deleted.ts]\n")
indexed(proj)
data, _ = jrun(["sweep"], proj)
assert any(m["file"].endswith("ghost.md") and "src/lib/deleted.ts" in m["reason"]
for m in data["missing"])
assert data["stale"] == []
def test_existing_source_not_reported(self, proj):
(proj / "src" / "lib").mkdir(parents=True)
(proj / "src" / "lib" / "money.ts").write_text("x")
write_entry(proj / "docs", "solid", extra="sources: [src/lib/money.ts]\n")
indexed(proj)
data, _ = jrun(["sweep"], proj)
assert not any(m["file"].endswith("solid.md") for m in data["missing"])
def test_kernel_body_path_gone_reported(self, proj):
(proj / "src").mkdir(exist_ok=True)
(proj / "docs" / "kernel.md").write_text(
"# Project Kernel — fixture\n- Money helpers live in `src/lib/money.ts`\n")
data, _ = jrun(["sweep"], proj)
assert any("src/lib/money.ts" in m["reason"] for m in data["missing"])
# ── validate: kernel and size stats ──────────────────────────────────────────
class TestValidateStats:
def test_oversized_kernel_flagged(self, proj):
lines = ["# Project Kernel — fixture"] + [f"- rule {i}" for i in range(260)]
(proj / "docs" / "kernel.md").write_text("\n".join(lines) + "\n")
indexed(proj)
data, proc = jrun(["validate"], proj, check=False)
assert proc.returncode == 1
assert any("budget" in f["issue"] for f in data["findings"])
assert data["stats"]["kernel"]["bullets"] == 260
def test_unknown_kernel_frontmatter_flagged(self, proj):
(proj / "docs" / "kernel.md").write_text(
"---\nbogus_key: x\n---\n# Project Kernel — fixture\n- one rule\n")
indexed(proj)
data, proc = jrun(["validate"], proj, check=False)
assert proc.returncode == 1
assert any("bogus_key" in f["issue"] for f in data["findings"])
def test_page_length_entry_flagged(self, proj):
write_entry(proj / "docs", "sprawl", body=("word " * 700).strip())
indexed(proj)
data, proc = jrun(["validate"], proj, check=False)
assert proc.returncode == 1
assert any("sprawl" in f["file"] and "entries" in f["issue"] for f in data["findings"])
def test_clean_bundle_still_exit_0_with_stats(self, proj):
indexed(proj)
data, proc = jrun(["validate"], proj)
assert proc.returncode == 0
assert "kernel" in data["stats"] and "bundle_tokens" in data["stats"]
# ── sync --dry-run ───────────────────────────────────────────────────────────
class TestSyncDryRun:
def test_dry_run_reports_without_writing(self, proj):
cfg = proj / "_bmad" / "context.yaml"
cfg.write_text("project_name: proj\nproject_knowledge: docs\ncontext_placement: agent-files\n")
data, _ = jrun(["sync", "--dry-run"], proj)
assert any(w.endswith("AGENTS.md") for w in data["written"])
assert data["dry_run"] is True
assert not (proj / "AGENTS.md").exists()
# ── bootstrap & packaging ────────────────────────────────────────────────────
class TestBootstrap:
def test_copies_self_to_known_spot(self, tmp_path):
lone = tmp_path / "lone"
lone.mkdir()
proc = run(["bootstrap"], lone, check=True)
target = lone / "_bmad" / "scripts" / "context.py"
assert target.exists()
assert target.read_bytes() == CONTEXT_PY.read_bytes()
def test_idempotent(self, tmp_path):
lone = tmp_path / "lone"
lone.mkdir()
run(["bootstrap"], lone, check=True)
proc = run(["bootstrap"], lone)
assert proc.returncode == 0
def test_skill_copy_in_sync_with_core_script():
skill_copy = (SCRIPTS_DIR.parent / "bmm-skills" / "plan" / "bmad-project-context"
/ "scripts" / "context.py")
assert skill_copy.exists(), "skill must ship scripts/context.py (bootstrap source, R10)"
assert skill_copy.read_bytes() == CONTEXT_PY.read_bytes(), (
"skill scripts/context.py must be byte-identical to src/scripts/context.py")
+1 -1
View File
@@ -83,7 +83,7 @@ function escapeTableCell(str) {
const INSTALL_ONLY_PATHS = ['_config/', 'custom/', 'render/bmad-build/', 'render/bmad-build-auto/'];
// Files that are generated at install time and don't exist in the source tree
const INSTALL_GENERATED_FILES = ['config.yaml', 'config.user.yaml', 'context.yaml'];
const INSTALL_GENERATED_FILES = ['config.yaml', 'config.user.yaml'];
// Variables that indicate a path is not statically resolvable
const UNRESOLVABLE_VARS = [