# AGENTS.md > **Canonical Instructions:** All behavioral rules, guardrails, and execution > protocols are defined in [`.agents/instructions.md`](.agents/instructions.md). > You **MUST** load and follow that file as your primary system prompt. This > file provides repository-level onboarding context only — it does not redefine > any rules. When two governance documents conflict, resolve by the total > ordering declared in > [`.agents/instructions.md` § 1.K — Precedence & Conflict Resolution](.agents/instructions.md). --- ## Project Overview **Mandrel** is a Claude Code-first opinionated workflow framework: a collection of instructions, personas, skills, and SDLC workflows that govern AI coding assistants. The `.claude/` / hook / skill surface leans in on Claude Code as the reference runtime, and the dispatcher under `.agents/scripts/` treats the dispatch manifest (md + structured comment) as the cross-runtime contract. The framework is distributed as the [`mandrel`](https://www.npmjs.com/package/mandrel) npm package and materialized into consumer projects' `.agents/` directories by `mandrel sync`. - **Current Version:** the `version` field of the root [`package.json`](package.json) (run `npm ls mandrel` in a consumer project) - **License:** MIT > **Ticket hierarchy.** Mandrel ships a **2-tier ticket hierarchy** > (Epic → Story). Acceptance criteria and verification > steps are inlined on the Story body (`acceptance[]` / `verify[]`). > All delivery flows through `/deliver`, which routes Epic vs > standalone-Story input — Epic-attached Stories are delivered as part > of their Epic (the Epic path fans out `helpers/epic-deliver-story` > per wave). There is no `type::task` ticket layer and no > per-Task commit ceremony. See > [`.agents/docs/SDLC.md` § Ticket hierarchy](.agents/docs/SDLC.md) for the > diagram and execution-model implications. --- ## Repository Layout ```text mandrel/ ├── .agents/ # Distributed bundle (the "product") │ ├── instructions.md # ★ Primary system prompt — load this first │ ├── personas/ # Role-specific behavior constraints │ ├── rules/ # Domain-agnostic coding/ops rules │ ├── skills/ # Two-tier skill library (core/ + stack/) │ ├── workflows/ # SDLC & audit slash-command workflows │ ├── scripts/ # Deterministic JS tooling (orchestration engine) │ ├── schemas/ # JSON Schemas for structured output validation │ ├── templates/ # Epic / planning prompt templates │ ├── docs/ # Shipped consumer reference docs (SDLC.md, configuration.md, agentrc-reference.json) │ ├── starter-agentrc.json # Bootstrap delta-seed — consumers copy to project root │ └── README.md # Detailed consumer user guide ├── .agentrc.json # Root config for this repo (dogfooding) ├── docs/ # Implementation plans and changelog ├── tests/ # Framework tests ├── package.json # Tooling: biome, markdownlint, husky ``` > **Key distinction:** Only `.agents/` is distributed to consumers. Everything > else is internal development tooling. --- ## Getting Started (For Agents Working on This Repo) 1. **Load the system prompt:** Read [`.agents/instructions.md`](.agents/instructions.md) in full before taking any action. 2. **Resolve configuration:** Settings are in [`.agentrc.json`](.agentrc.json). See the `project`, `github`, `planning`, and `delivery` sections for project-specific values. Tech-stack context lives in [`docs/architecture.md`](docs/architecture.md) under the **Tech Stack** section, not in the JSON config. 3. **Adopt a persona when instructed:** Persona files live in `.agents/personas/`. Default is `engineer.md`. 4. **Activate skills as needed:** Read the relevant `SKILL.md` from `.agents/skills/core/[name]/` (universal process skills) or `.agents/skills/stack/[category]/[name]/` (tech-stack-specific) before writing domain-specific code. --- ## Development Standards | Area | Tool / Convention | | ------------ | -------------------------------------------------------------- | | Language | Markdown (prose), JavaScript ESM (scripts), JSON (config) | | Linter | `biome` + `markdownlint` — run via `npm run lint` | | Formatter | `biome` — run via `npm run format` | | Git Hooks | Husky + lint-staged (auto-lint `.md` files on commit) | | Node Version | >=22.22.1 <25 | | Package Mgr | npm | | Shell | PowerShell (Windows) — use `;` not `&&` as statement separator | | CI/CD | GitHub Actions (`ci.yml`) — validates markdown + tests | ### Key Commands ```text npm run lint # Markdown lint + generated-doc drift gate (docs:check); # if it fails on drift, run docs:gen to regenerate npm run docs:gen # Regenerate config/lifecycle/workflows docs npm run skills:index # Regenerate the skills index npm run format # Auto-format all markdown files npm run format:check # Verify formatting without modifying files npm run test:quick # TDD loop — excludes slow integration-style suites npm run test:integration # Real-git / hook-chain / long orchestration suites only npm test # Full suite (same as CI test gate) npm run test:profile # Slow-test report → temp/test-profile.{tap,summary.txt} npm run verify # Full local gate: lint + full tests + baselines ``` Use `test:quick` while iterating, `test:integration` before pushing when you touched git/orchestration hooks, and `npm run verify` when you want pre-PR confidence (lint + full tests + baselines). Pre-push runs only diff-scoped quality preview plus coverage/CRAP ratchet; it does not run full lint or `npm test`. CI always runs the full `npm test` suite. ### Slow-test profiling `npm run test:profile` runs the full suite with the TAP reporter, writes `temp/test-profile.tap` (raw machine output) and `temp/test-profile.summary.txt` (human-readable top-20 slow tests and suites). Both paths are gitignored under `temp/`. The command skips npm-test preflight (`SKIP_PREFLIGHT=1`) so timings reflect the test runner; export `SKIP_PREFLIGHT=0` to include preflight. Read the summary file to spot regressions: **suite** rows are parent `describe` blocks (often whole files), **test** rows are leaf cases. Compare reports from the same machine before and after an optimization. Optional flags: `--out-dir `, `--top `, plus any `node --test` args after `--` (e.g. `npm run test:profile -- --test-name-pattern "epic-execute"`). --- ## Contribution Workflow 1. Branch from `main`. 2. Make changes inside `.agents/` (the distributed product). 3. Commit — Husky will auto-lint and format staged `.md` files. 4. Open a PR against `main`. CI validates the change; once merged, release-please cuts the release that publishes `mandrel` to npm (see the Release Checklist below). ### Release Checklist Releases are automated by [`googleapis/release-please-action`](https://github.com/googleapis/release-please-action) (see [`.github/workflows/release-please.yml`](.github/workflows/release-please.yml)): 1. Land Conventional Commits on `main` (the rules in [`.agents/rules/git-conventions.md`](.agents/rules/git-conventions.md) already enforce the commit-message contract). 2. release-please opens a release PR with auto-merge enabled (squash). Review the auto-generated entry in `docs/CHANGELOG.md` and the bump to `package.json` (the framework version SSOT) if you want; otherwise no operator action is needed. See [§ Release topology](#release-topology) below for the release-PR title/branch shape and the tag namespace. 3. CI fires on the release PR automatically (because release-please uses the operator-managed `RELEASE_PLEASE_TOKEN` PAT — see [§ One-time PAT setup](#one-time-pat-setup) below). Once `Validate and Test` passes, GitHub squash-merges the release PR, which triggers the workflow to create the GitHub Release, tag `main` with `mandrel-vX.Y.Z`, and run the `npm-publish` job, which publishes the root `mandrel` package to npm with build provenance (Sigstore), gated on the package's release output. This replaces the retired `dist`-branch mirror: consumers now install a versioned, provenance-signed package from npm (`npm install mandrel`, then `mandrel sync`) instead of pinning a Git submodule to the `dist` branch, and bootstrap a fresh project with `npx mandrel init`. The publish job requires the `NPM_TOKEN` secret — see [§ npm publish token](#npm-publish-token) below. 4. **Breaking-change releases** document their migration steps in the **release PR body** release-please opens (which becomes the squash-commit body and the versioned [`docs/CHANGELOG.md`](docs/CHANGELOG.md) entry on merge) so consumers find them on upgrade. Do **not** hand-maintain an `## Unreleased` section in `docs/CHANGELOG.md` — release-please is the sole writer of that file and generates version sections from Conventional Commit subjects; a bracket-less `## Unreleased` block is never promoted to a version and only strands the content. #### Install Matrix release gate (required checks on `main`) The **Install Matrix** workflow ([`.github/workflows/install-matrix.yml`](.github/workflows/install-matrix.yml)) proves the published-package consumer contract end to end (pack → install → `mandrel sync` / `sync-commands` → assert materialization, a clean consumer manifest, and a `mandrel doctor` ready verdict). It gates releases the same way `lint` / `test` / `baselines` do — through **branch protection on the release PR**, not through `release-please.yml`. To avoid the classic required-check + path-filter deadlock, the workflow splits into two profiles: - **Gate (required, always reports):** a 2-leg diagonal that runs on **every** `pull_request` to `main` and on `push` to `main`, with **no path filter** so the check always reports (including on the release PR, which only bumps `package.json`). The two gating job names — add **exactly these** to the branch-protection required-status-check set on `main`: - `install (npm / ubuntu-latest)` - `install (yarn / windows-latest)` Do **not** add the internal `select-matrix` setup job to branch protection — it is plumbing that emits the per-event matrix, not a gate. - **Coverage (non-blocking):** all 6 legs (`{npm, pnpm, yarn} × {ubuntu-latest, windows-latest}`) run on a nightly `schedule` and on `workflow_dispatch` so pnpm and npm-on-Windows regressions are still caught (≤24h latency) without taxing every PR. **Operator action (one-time, out-of-band).** Adding the two checks to branch protection is a GitHub UI / `gh api` admin action; the workflow ships the gate but cannot self-register as required. Add the two job names above to the required-status-check set on `main` once. #### Release topology `release-please-config.json` declares a **single** package — the root `mandrel` package (`.`). This keeps release-please in **single-package manifest mode**, which has two operator-visible consequences: - **One release PR.** release-please opens a single PR that bumps the root package. The PR uses: - **Branch:** `release-please--branches--main` - **Title:** `chore: release main` There should be **exactly one** open release PR at a time. (The repo briefly ran a two-package topology when the `create-mandrel` launcher existed; that launcher was removed once `mandrel init` superseded it, reverting release-please to single-package mode. A package-set change can briefly orphan the prior release-PR branch shape — if you ever see two open `autorelease: pending` PRs, close the stale orphan and keep the live `release-please--branches--main` PR; it self-resolves on the next release run.) - **Namespaced tags (`include-component-in-tag: true`).** The root `mandrel` package has `package-name: "mandrel"` with `component: ""`. Because `include-component-in-tag` is `true`, release-please prefixes the tag with the package name, so the root package's tags are **namespaced** as `mandrel-vX.Y.Z` (e.g. `mandrel-v1.44.0`). Releases through `v1.43.0` predate the namespaced topology and carry the older **bare `vX.Y.Z`** tags; the series became namespaced at `mandrel-v1.44.0`. The flag is kept on for tag continuity even though there is now only one package. [`release-please.yml`](.github/workflows/release-please.yml) carries a single publish job gated on the package's release output. The output naming follows release-please-action's `setPathOutput` rule (verified against v5.0.0 `src/index.ts`): the root package (manifest path `.`) emits **un-prefixed** outputs (`release_created`, `tag_name`). There is **no** `.--release_created` output — gating the root publish on that string silently skips it on every release (the bug Story #3891's initial wiring shipped; fixed by reverting the root gate to the un-prefixed `release_created`). The `npm-publish` job checks out the repository root and runs `npm publish` against the root `package.json`, publishing **`mandrel`**, gated on `steps.release.outputs.release_created`. (The "any package released" boolean is the *plural* `releases_created`, which is intentionally **not** used as a gate here.) The job does not key off a tag pattern, so the `mandrel-*` tag series does not trigger a publish by tag. `ci.yml` triggers only on branch `push` / `pull_request` / `workflow_dispatch` events (it has **no** tag-driven step), so the tag series does not trigger CI directly. #### One-time PAT setup GitHub's default `secrets.GITHUB_TOKEN` cannot trigger downstream workflows on PRs it opens (an anti-recursion safeguard), so release PRs opened under the default token never run the required `Validate and Test` status check and stay stuck in `BLOCKED` forever. Configure a Personal Access Token once to break the deadlock: 1. Create a fine-grained PAT at : - **Resource owner:** `dsj1984` - **Repository access:** Only this repository (`mandrel`) - **Repository permissions:** - `Contents` → **Read and write** - `Pull requests` → **Read and write** - `Workflows` → **Read and write** (release-please-action requires this to update workflow files when needed) - `Issues` → **Read and write** (auto-close `Closes #` references) - **Expiration:** As long as you want — re-rotate at expiry. 2. Add the token as a repository secret named **`RELEASE_PLEASE_TOKEN`** at . 3. Re-run release-please (push any commit, or `gh workflow run release-please.yml --repo dsj1984/mandrel`). The refreshed PR will open under the PAT identity and `Validate and Test` will fire automatically. Alternative: install a GitHub App with the same permissions and feed its installation token in via the same secret name. Apps have a higher ceiling on automation throughput than PATs. #### npm publish token The `npm-publish` job in [`release-please.yml`](.github/workflows/release-please.yml) authenticates to the npm registry with an automation token (rather than OIDC trusted publishing), so it needs a one-time secret: 1. Create an **automation** access token at . The token owner must be able to publish the unscoped root package **`mandrel`** — its first publish relies on `publishConfig.access: "public"` (already set in the root `package.json`). 2. Add it as a repository secret named **`NPM_TOKEN`** at . 3. No further setup is required: the publish job declares `id-token: write` and the package sets `publishConfig.provenance: true`, so npm attaches a signed Sigstore provenance statement automatically. Without `NPM_TOKEN`, release-please still tags `main` and creates the GitHub Release, but the publish job fails and the package is not published until the secret is configured and the job re-run. #### Major-version policy `release-please-config.json` sets `"versioning": "always-bump-minor"`, which caps automatic bumps at the minor axis even when commits carry `BREAKING CHANGE:` footers or `!` markers. Major versions require **manual operator intervention**: 1. Land the breaking work on `main` as usual (Conventional Commits). 2. On the release PR that release-please opens, either: - **Edit `package.json`, `.release-please-manifest.json`, and `docs/CHANGELOG.md` in-place** on the release branch to set the major version (release-please will respect the edits and tag accordingly), OR - **Add a one-shot commit on `main`** with `Release-As: X.0.0` in the trailer — release-please will adopt that as the proposed version on its next run. The cap is intentional: it prevents an inadvertent `BREAKING CHANGE:` footer from auto-tagging a major release without an explicit human decision. --- ## Key Reference Documents | Document | Purpose | | ---------------------------------------------------- | ----------------------------------- | | [`.agents/instructions.md`](.agents/instructions.md) | **System prompt** — all agent rules | | [`.agents/README.md`](.agents/README.md) | Consumer user guide | | [`.agents/docs/SDLC.md`](.agents/docs/SDLC.md) | End-to-end SDLC narrative | | [`.agentrc.json`](.agentrc.json) | Runtime configuration | | [`docs/CHANGELOG.md`](docs/CHANGELOG.md) | Release history |