Monorepos
DocGuard checks one project directory per run. In a repository with several packages, you choose what that directory is: the repository root, with one set of documents for everything, or each package, with its own configuration and documents. This guide covers both, how DocGuard picks the directory, and what it does not do.
How DocGuard picks the project directory
Every command works on the current directory, or on the directory you pass with --dir:
docguard guard # the current directory
docguard guard --dir packages/api # an explicit directory
From that directory DocGuard reads .docguard.json, .docguardignore, the canonical documents and the baseline. It never changes the directory on its own.
Ancestor-root guidance
If you run a command inside a package that has no .docguard.json of its own, DocGuard checks whether a parent directory governs it. It walks up (at most 32 levels, and never past the Git repository root) looking for either of these:
- a
.docguard.json; - a workspace declaration that lists the package:
workspacesinpackage.json, orpackagesinpnpm-workspace.yaml.
When it finds one, it still checks the directory you are in, and prints on stderr how to check the whole repository instead:
DocGuard is checking /repo/packages/api.
/repo appears to govern this package via package.json#workspaces; the scan scope was not changed.
Re-run for repository scope: docguard guard --dir /repo
With --format json, sarif or junit, stdout is unchanged and stays valid. Stderr gets one JSON line of type docguard.repository-root-guidance, with the selected directory, the suggested one, the reason and the rerun command.
The guidance does not appear when:
- the current directory has its own
.docguard.json; - you passed
--dir(any explicit path counts, including--dir .); - the workspace patterns do not match the package;
- a nested Git repository sits between the package and the ancestor.
Layout 1: one project at the repository root
Use this when the repository has one architecture and one set of documents, and the packages are parts of one system.
repo/
├── .docguard.json
├── docs-canonical/
├── package.json # "workspaces": ["packages/*"]
└── packages/
├── api/
└── web/
Run docguard init and every other command from the root.
Where DocGuard looks for code
Routes, environment variables, data models, dependencies and test files are collected from these source roots, in order:
sourceRootin.docguard.json, if set;- each workspace package from
package.jsonworkspaces(an array, or an object withpackages) and frompnpm-workspace.yaml; - the conventional directories that exist:
src,app,lib,server,api,functions,backend/src,backend,cli(andpages,components,hooks,utilsin a Next.js project).
The repository root itself is scanned only when none of these exist.
{ "sourceRoot": "services/backend/src" }
Note:
sourceRootalso accepts an array of directories, for example["services/api/src", "services/web/src"].
Project type
Type detection reads the manifest at the root only. A workspace root whose own package.json names no framework is typed unknown. That type assumes the project needs documented environment variables, an .env.example and a data model, which may not match your repository.
init writes both projectType and the projectTypeConfig flags derived from it, and the flags in the file win. Edit both:
{
"projectType": "api",
"projectTypeConfig": {
"needsEnvVars": true,
"needsEnvExample": true,
"needsE2E": false,
"needsDatabase": true
}
}
Valid types are cli, library, webapp, api and unknown. The flags each type implies are listed in Configuration, under “Project Type Defaults”.
Where DocGuard looks for documents
docs-canonical/at the root, including its subdirectories.- Conventional documentation directories at the root only:
docs,doc,documentation,docs-canonical,docs-implementation,guides,guide,handbook,manual,wiki,extensions, andwebsite/docs. - Documents mapped with
docs.roles.
A package’s own packages/api/docs/ is not found automatically. The guard summary counts it as “outside any tier”. Add it with docs.dirs:
{ "docs": { "dirs": ["packages/api/docs", "packages/web/docs"] } }
docs.dirs adds directories to the inventory and to freshness review. It does not make them canonical documents. A canonical role (architecture, dataModel, …) maps to one file for the whole project, through docs.roles. If each package needs its own architecture document checked as canonical, use layout 2.
Agent instructions in packages
Nested AGENTS.md files work as a chain from the root down to each directory. STR004 reports a chain over the byte budget (32 KiB by default, Codex’s limit). To freeze existing debt in one package, key an allowance by the deepest file in its chain:
{
"agentInstructions": {
"allowances": { "packages/big/AGENTS.md": 40000 }
}
}
Leaving a package out
List it in .docguardignore at the root:
packages/legacy/
A dir/ pattern matches that directory at any depth, as in .gitignore.
Layout 2: each package is its own project
Use this when packages are owned by different teams, released separately, or need their own canonical documents.
repo/
└── packages/
├── api/
│ ├── .docguard.json
│ └── docs-canonical/
└── web/
├── .docguard.json
└── docs-canonical/
Adopt each package on its own, following Existing Projects:
docguard init --dir packages/api
docguard guard --dir packages/api
Each package keeps its own .docguard.json, documents, .docguard.baseline.json and .docguard/ state directory. Git-based checks still work, because Git metadata is found in an ancestor directory.
Note: A run at the root does not read the packages’
.docguard.jsonfiles or theirdocs-canonical/directories, and does not combine their results. Run each package separately.
Mapped document roles cannot point outside the package: docs.roles rejects absolute paths and parent traversal (../).
Both layouts can coexist. A root project for repository-wide documents and a project per package are separate runs.
CI per package
GitHub Actions
Give each package its own job and set working-directory:
Replace
X.Y.Zwith the release you pin; Installation shows how to find the latest.
name: DocGuard
on: [pull_request]
permissions: { contents: read }
jobs:
docguard-api:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: raccioly/docguard@vX.Y.Z
with:
command: guard
working-directory: packages/api
pr-comment: 'false'
docguard-web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: raccioly/docguard@vX.Y.Z
with:
command: guard
working-directory: packages/web
pr-comment: 'false'
Set pr-comment: 'false' when more than one job runs the action on the same pull request. The sticky comment is found by a fixed marker, so each job would overwrite the last one’s comment. Inline annotations still work: the action rewrites finding paths to be relative to the repository root.
A matrix over package directories works the same way, with the package path in working-directory.
Other CI systems
Loop over the packages and fail on errors (exit 1), not on warnings (exit 2):
failed=0
for dir in packages/api packages/web; do
docguard ci --dir "$dir"
[ $? -eq 1 ] && failed=1
done
exit "$failed"
More CI recipes are in CI.
The MCP server in a monorepo
docguard mcp serves one directory, and a tool call’s projectDir must sit inside it. Started at the repository root, it can answer for any package. Started inside a package, it refuses other packages unless you add --root <dir> for each extra tree. See MCP.
What is not supported
- A single run that finds and combines per-package projects. Run each package with
--dir, or in its own CI job. - Workspace declarations other than
package.jsonworkspacesandpnpm-workspace.yaml. Nx project files,lerna.jsonpackage lists, Rush, Cargo and Go workspaces, and Maven or Gradle multi-module builds do not add source roots, and do not trigger ancestor-root guidance. SetsourceRootor rely on the conventional directories, or use layout 2. - Checking only the packages a change touched. DocGuard has no changed-package detection. Use your CI’s path filters to decide which package jobs run. Inside one package,
guard --changed-onlylimits the run to changed files. - Conventional documentation directories inside packages, at a root run. Add them with
docs.dirs.