# Architecture This document describes the internal architecture of Mandrel β€” a framework of instructions, personas, skills, and SDLC workflows that govern AI coding assistants. It is the authoritative reference for how the system is structured, how components interact, and where to find each subsystem. > **For the end-to-end workflow narrative** β€” how the commands compose, label > transitions, HITL touchpoints β€” see [`.agents/docs/SDLC.md`](../.agents/docs/SDLC.md). > This file covers the *architecture* (modules, interfaces, data flow) that > the workflow runs on top of. The slash-command reference index lives in > [`.agents/docs/workflows.md`](../.agents/docs/workflows.md). > > **Coupling stance.** Mandrel is a **Claude Code-first opinionated > workflow framework**. The dispatcher / `.agents/scripts/` library > produces a **dispatch manifest** (md + structured comment) as the > cross-runtime contract; the workflow / `.claude/` / hook / skill > surface leans in on Claude Code as the in-session reference runtime. > See ADR `20260512-coupling-stance` and the adapter-removal ADR in > [`decisions.md`](decisions.md) for the rationale and what it > explicitly is and isn't. --- ## High-Level Overview Mandrel follows an **Epic-Centric GitHub Orchestration** model where GitHub Issues, Labels, and Projects V2 serve as the Single Source of Truth (SSOT). The framework decomposes product initiatives (Epics) into executable agent tasks, dispatches them across parallel waves, and integrates the results β€” all without local state files. ```mermaid graph TB classDef human fill:#f9d0c4,stroke:#333,stroke-width:2px,color:#000 classDef agent fill:#c4f9d0,stroke:#333,stroke-width:2px,color:#000 classDef infra fill:#c4d9f9,stroke:#333,stroke-width:2px,color:#000 classDef data fill:#ececec,stroke:#333,stroke-width:1px,stroke-dasharray: 5 5,color:#000 H["πŸ‘€ Human Operator"]:::human IDE["Agentic IDE"]:::agent subgraph Framework [".agents/ β€” Distributed Bundle"] direction TB INS["instructions.md"]:::infra PER["Personas"]:::infra RUL["Rules"]:::infra SKL["Skills (core/ + stack/)"]:::infra WFL["Workflows (slash commands)"]:::infra SCR["Scripts Engine"]:::agent SCH["Schemas"]:::data TPL["Templates"]:::data end subgraph GitHub ["GitHub Platform"] direction TB ISS["Issues & Labels"]:::data SUB["Sub-Issues API"]:::data PRJ["Projects V2"]:::data end H -->|"/plan (ideation or existing Epic)"| IDE H -->|"/deliver"| IDE IDE --> INS INS --> PER & RUL & SKL IDE --> SCR SCR -->|"API calls"| ISS SCR -->|"Links hierarchy"| SUB SCR -.->|"Validates"| SCH ``` --- ## Repository Layout The repository has a clear separation between the **distributed product** (`.agents/`) and **development tooling** (root-level files). ```text mandrel/ β”œβ”€β”€ .agents/ ← Distributed bundle (the "product") β”‚ β”œβ”€β”€ instructions.md ← Primary system prompt (all agent rules) β”‚ β”œβ”€β”€ README.md ← Consumer documentation β”‚ β”œβ”€β”€ starter-agentrc.json ← Bootstrap delta-seed (copy to .agentrc.json) β”‚ β”‚ β”‚ β”œβ”€β”€ personas/ ← Role-specific behavior files β”‚ β”œβ”€β”€ rules/ ← Domain-agnostic coding standards β”‚ β”œβ”€β”€ skills/ ← Two-tier skill library β”‚ β”‚ β”œβ”€β”€ core/ ← Universal process skills β”‚ β”‚ └── stack/ ← Tech-stack guardrails β”‚ β”œβ”€β”€ workflows/ ← Slash-command workflows β”‚ β”œβ”€β”€ scripts/ ← Deterministic JavaScript tooling β”‚ β”‚ β”œβ”€β”€ lib/ ← Shared modules & interfaces β”‚ β”‚ └── providers/ ← Ticketing provider implementations β”‚ β”œβ”€β”€ schemas/ ← JSON Schema for structured output β”‚ β”œβ”€β”€ templates/ ← Prompt and planning templates β”‚ └── docs/ ← Shipped consumer reference docs β”‚ β”œβ”€β”€ SDLC.md ← End-to-end workflow guide β”‚ β”œβ”€β”€ configuration.md ← Every .agentrc.json key (shipped) β”‚ └── agentrc-reference.json ← Exhaustive editor reference β”‚ β”œβ”€β”€ .agentrc.json ← Runtime configuration (dogfooding) β”œβ”€β”€ .github/workflows/ ← CI/CD pipeline (ci.yml) β”œβ”€β”€ docs/ ← Project documentation β”œβ”€β”€ tests/ ← Framework test suite β”‚ └── lib/ ← Library-specific unit tests β”œβ”€β”€ temp/ ← Ephemeral runtime artifacts (git-ignored) β”œβ”€β”€ biome.json ← Biome linter/formatter config β”œβ”€β”€ package.json ← npm tooling + dev dependencies └── AGENTS.md ← Repository-level onboarding ``` --- ## Core Subsystems ### 1. Instruction Layer The instruction layer defines **what agents are** and **how they must behave**. | Component | Path | Purpose | | ------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | | System Prompt | `.agents/instructions.md` | Master behavioral contract β€” guardrails, FinOps, shell protocol, philosophy, quality discipline, Git conventions. | | Personas | `.agents/personas/*.md` | Role-specific constraint files (architect, engineer, qa-engineer, etc.) that override default behavior when activated. | | Rules | `.agents/rules/*.md` | Domain-agnostic coding standards (API conventions, git conventions, security baseline, testing, etc.). | | Skills | `.agents/skills/{core,stack}/` | Two-tier library of callable capabilities. | #### Persona Routing ```mermaid graph LR classDef active fill:#c4f9d0,stroke:#333,color:#000 S["Story Ticket"] --> L{"persona label?"} L -->|"architect"| A["architect.md"]:::active L -->|"engineer"| E["engineer.md"]:::active L -->|"qa-engineer"| Q["qa-engineer.md"]:::active L -->|"missing"| D["engineer.md (default)"]:::active ``` #### Skill Architecture Skills use a **two-tier layout**: - **`core/`** β€” Universal, process-driven skills (debugging, TDD, security, code review, context engineering, etc.) - **`stack/`** β€” Technology-specific skills organized by category: - `architecture/` β€” Monorepo strategies, system design - `backend/` β€” Server frameworks, API patterns - `frontend/` β€” UI frameworks, CSS systems - `qa/` β€” Testing frameworks (Playwright, Vitest) - `security/` β€” Hardening patterns Each skill contains a `SKILL.md` file with constraints and an optional `examples/` directory. --- ### 2. Orchestration Engine The orchestration engine is the **runtime brain** β€” a set of JavaScript ESM scripts that automate the entire SDLC from planning through integration. The operator-facing surface is two slash commands on the SDL critical path β€” `/plan` (with optional ideation entry) and `/deliver` β€” where `/deliver` also routes single-Story drives off the dispatch table. Planning is **git-state-free**: `/plan` produces a declarative `epic.yaml` artifact (Epic #1182) that is diff-able, replayable, and reconcilable against GitHub via `epic-reconcile.js`; the Epic branch is no longer created at plan time. `/deliver`'s host LLM owns the wave loop and fans Story sub-agents out directly via the Agent tool inside the operator's Claude session β€” there is no intermediate wave skill, no subprocess spawn pathway, and no GitHub Actions runner. The PR opened by `/deliver` Phase 7 is the sole promotion gate to `main`; the workflow never executes `git merge` against `main` itself. When the run is end-to-end clean, Phase 8.5's auto-merge gate arms `gh pr merge --auto --squash --delete-branch` (Story #3901); otherwise the operator merges via the GitHub UI. #### Component Diagram ```mermaid graph TB classDef script fill:#e8d5f5,stroke:#333,color:#000 classDef lib fill:#d5e8f5,stroke:#333,color:#000 classDef iface fill:#f5e8d5,stroke:#333,color:#000 subgraph Scripts ["Orchestration Scripts"] EP["epic-plan-spec.js"]:::script TD["epic-plan-decompose.js"]:::script DI["dispatcher.js"]:::script EDP["epic-deliver-prepare.js"]:::script LE["lifecycle-emit.js"]:::script SI["story-init.js"]:::script SC["story-close.js"]:::script CH["hydrate-context.js"]:::script NO["notify.js"]:::script UTS["update-ticket-state.js"]:::script end subgraph Lib ["Shared Library (lib/)"] CR["config-resolver.js"]:::lib PF["provider-factory.js"]:::lib GH["Graph.js (DAG)"]:::lib DP["dependency-parser.js"]:::lib GMO["git-merge-orchestrator.js"]:::lib GU["git-utils.js"]:::lib LG["Logger.js"]:::lib end subgraph Interfaces ["Abstract Interfaces"] ITP["ITicketingProvider"]:::iface end subgraph Implementations ["Concrete Implementations"] GHP["providers/github.js"]:::script end DI --> CR & PF & GH & DP & CH EP --> CR & PF TD --> CR & PF & DP PF --> ITP ITP -.->|"implements"| GHP ``` #### Key Scripts | Script | Responsibility | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `epic-plan-spec.js` | Authoring wrapper for PRD + Tech Spec; flips Epic to `agent::review-spec` and writes the `epic-plan-state` checkpoint. Threads `docsContext` and a `codebaseSnapshot` (Story #2634, see below) into the spec-author envelope so the Architect persona cites real modules instead of doc-only names. | | `epic-plan-decompose.js` | Authoring wrapper for the Epic's child-Story backlog; flips Epic to `agent::ready` and posts the dispatch manifest. | | `dispatcher.js` | Builds dependency DAG, computes execution waves, posts the dispatch manifest (consumed by `/deliver`). | | `epic-deliver-prepare.js` | Snapshots the Epic, builds the wave plan, and initialises the `epic-run-state` checkpoint at the start of `/deliver` Phase 1. | | `lifecycle-emit.js` | Generic argv-driven emit helper. `/deliver` Phase 7 / 8.5 / 9 fire `epic.close.end` / `epic.automerge.start` / `epic.merge.armed` through this CLI; the matching listener chain (`Finalizer`, `AutomergePredicate` + `AutomergeArmer`, `Cleaner`) runs the PR open, auto-merge arm, and branch reap. | | `story-init.js` | Initialises a Story worktree and flips the Story to `agent::executing`. | | `story-close.js` | Validates, merges, reaps, and cascades on Story completion. Thin CLI shell over `lib/orchestration/story-close/{merge-runner,cleanup-reconciler,comment-bodies}`. | | `hydrate-context.js` | Assembles self-contained prompts (protocol + persona + skills + hierarchy + task). Emits the JSON envelope by default; `--emit prompt` writes the raw prompt. The only supported hydration CLI. | | `update-ticket-state.js` | Syncs ticket status via GitHub labels (`agent::ready` β†’ `agent::done`). | | `notify.js` | Dispatches notifications via @mention and webhook channels. | | `analyze-execution.js` | Reads per-Story `signals.ndjson` and emits the `story-perf-summary` / `epic-perf-report` consumed by the retro. Wired into the post-merge pipeline and into `/deliver` Phase 6. | #### In-process orchestration modules These modules fold the close-tail into the deliver runner so `/deliver` runs end-to-end without spawning helper sessions: | Module | Role | | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `lib/orchestration/code-review.js` | Phase 5 inline review. Companion to `helpers/code-review.md`; halts the runner on critical findings and persists results as a `code-review` structured comment. | | `lib/orchestration/retro-runner.js` | Phase 6 retro authoring. Extracted from the (now-deleted) retro helper; aggregates perf signals, friction, hotfixes, recuts, parked stories, and HITL count. | | `lib/duplicate-search.js` | `/plan` ideation entry β€” cross-Epic title + body keyword search; returns a ranked list of overlapping open Epics or `[]`. | #### Dispatch Engine Submodules `lib/orchestration/dispatch-engine.js` is a coordinator for the 2-tier dispatch path. Consumers (`dispatcher.js`, tests) import `dispatch`, `resolveAndDispatch`, and the `AGENT_*` constants from the coordinator path. Every Epic is 2-tier (Epic β†’ Story); `dispatch()` computes a Story-level wave plan and emits a 2-tier manifest. The legacy The retired 4-tier dispatch runtime (legacy fetcher, wave fan-out, and Epic-completion detector) was removed in Epic #3163; per-Story execution is owned by `/deliver` (`story-init` β†’ `story-close`). | Submodule | Responsibility | | ----------------------------- | ----------------------------------------------------------------------------------------- | | `dispatch-pipeline.js` | Resolve context, fetch Epic, reconcile Story/Feature hierarchy, build the Story DAG. | | `risk-gate-handler.js` | Risk labels are metadata only; no runtime gate. | #### Presentation Layer Submodules `lib/presentation/manifest-renderer.js` is a faΓ§ade composing: | Submodule | Responsibility | | ------------------------- | -------------------------------------------------------------------------------------------------- | | `manifest-formatter.js` | Pure Markdown / CLI rendering (`formatManifestMarkdown`, `printStoryDispatchTable`). No fs access. | | `manifest-persistence.js` | File I/O β€” writes dispatch and story manifests to `temp/`. | The data-shape owner (`lib/orchestration/manifest-builder.js`) is unchanged. Only the faΓ§ade file is part of the stable public surface β€” downstream consumers continue to import `renderManifestMarkdown`, `renderStoryManifestMarkdown`, `persistManifest`, `printStoryDispatchTable`, `postManifestEpicComment`, and `postParkedFollowOnsComment` from `lib/presentation/manifest-renderer.js`. #### ErrorJournal `ErrorJournal` (`lib/orchestration/error-journal.js`) writes structured JSONL to `temp/epic--errors.log` via `errorJournal?.record({ phase, error, context })`, so failure sites that would otherwise be silent `catch (err) { logger.warn(...) }` blocks stay auditable after a run completes. See [`docs/patterns.md`](patterns.md) for the pattern and the `errorJournal?.record(...)` idiom. The typed context classes that once threaded it through an in-process runner (`OrchestrationContext` / `EpicRunnerContext` / `PlanRunnerContext` in `lib/orchestration/context.js`) were deleted with the dead in-process epic-runner stratum (#3908) β€” the host LLM drives the CLIs directly. Progress reporting is helper-based, not class-based: `lib/orchestration/epic-runner/progress-reporter/` holds the `composition.js` / `signals.js` / `transport.js` helpers consumed by `epic-execute-record-wave.js`, `lib/orchestration/wave-record-notifications.js`, and the `NotifyDispatcher` lifecycle listener to render and post wave-record progress surfaces. #### Codebase snapshot (Phase 7) `lib/codebase-snapshot.js` emits a structural view of the consumer repo that Phase 7 spec authoring threads into the `planner-context.json` envelope under `codebaseSnapshot`. The snapshot is bounded β€” file tree (filtered by the configured include/exclude globs), `package.json` exports + scripts, recently-touched directories from `git log`, and the detected test/BDD surface. Tier is controlled by `planning.codebaseSnapshot.tier` in `.agentrc.json`: - **`skinny` (default)** β€” file paths only. Target: ~2–5k tokens on a mid-size repo. - **`medium`** β€” skinny + a per-file export signature list extracted via a regex pass over each `.js` / `.ts` body. Target: ~15–30k tokens. The Architect persona is instructed (via the `epic-plan-spec-author` skill) to prefer module / file names that appear in the snapshot over names that appear only in `docsContext.items[]`, because the docs may be stale relative to the source tree. Any error in the snapshot generation degrades to a `Logger.warn` and an empty envelope so Phase 7 stays non-blocking. #### Resilience layers | Module | Role | | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `lib/orchestration/wave-record-io.js` | `verifySingleResult` β€” the zero-commit "done" guard. Each "done" claim in a wave record is re-fetched from the provider and downgraded (`verify-error`) when the ticket is not actually at `agent::done`/closed. | | `lib/observability/signals-writer.js` | Append-only NDJSON writer for `friction` / trace records under `temp/epic-/stories/story-/signals.ndjson`. The single producer for the telemetry pipeline; readers are the `read()` async generator in `lib/signals/read.js` and the perf-report readers in `lib/observability/perf-report-readers.js`. | | `lib/orchestration/column-sync.js` | Drives the Projects v2 Status column from `agent::` labels (best-effort). Invoked from inside `transitionTicketState` (Story #2548) so every label flip β€” Epic and Story β€” mirrors onto the board. | The earlier `CommitAssertion` post-wave guard was epic-runner-scoped and was deleted with the in-process epic-runner stratum (#3908); `verifySingleResult` in `wave-record-io.js` is the surviving guard against a Story being reported "done" without verifiable completion. #### Throughput primitives | Module | Role | | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `lib/util/concurrent-map.js` | `concurrentMap(items, fn, { concurrency })` bounded-concurrency fanout. Adopters: `detect-merges.js`, `hierarchy-gate.js`, `providers/github/issues.js`, `providers/github/sub-issues.js`, `lib/observability/perf-report-readers.js`, `lib/orchestration/wave-record-io.js`. | | `providers/github/cache.js` | Per-instance ticket cache; `peekFresh(ticketId, maxAgeMs)` treats entries older than the caller's max age as cache misses, and the cache is primed after bulk ticket fetches. | | `providers/github/issues.js` | Bulk `GET /issues?labels=agent::*&state=open` path replaces per-ticket probes when the tracked-story set is large; per-ticket fallback on errors. | | `lib/util/phase-timer.js` + `phase-timer-state.js` | Records `{ phase, elapsedMs }` spans across the `story-init` β†’ sub-agent β†’ `story-close` boundaries. Posts `phase-timings` comments on Story close. | #### Concurrency caps The wave fan-out cap is resolved deterministically by `resolveConcurrencyCap` in `lib/orchestration/wave-record-projection.js` and surfaced on the wave-plan envelopes that `wave-tick.js` and `stories-wave-tick.js` emit. The epic-runner-era concurrency surface β€” `wave-gate.js`, `lib/orchestration/concurrency.js` (`DEFAULT_CONCURRENCY` / `resolveConcurrency`), `CommitAssertion`, and the `ProgressReporter` listener class β€” was deleted with the dead in-process stratum (#3908); do not confuse the surviving `resolveConcurrencyCap` with the deleted `resolveConcurrency`. Consumers monitoring throughput read the `epic-perf-report` structured comment posted by `analyze-execution.js` at Epic close β€” it surfaces per-phase p50/p95 and the workload signals the retro consumes. #### Direct CLIs (no MCP server) The framework ships no MCP server. Every orchestration capability is a direct Node CLI under `.agents/scripts/`, with `lib/orchestration/ticketing.js` as the authoritative SDK for runtime callers. Operators see the simplification at first-run time (no MCP-server bootstrap step) and at secrets-resolution time (`GITHUB_TOKEN` and `NOTIFICATION_WEBHOOK_URL` read only from `process.env`). #### Lifecycle vs. Runtime partition boundary Scripts in the Mandrel framework divide into two non-overlapping sets. The partition is determined by **who invokes the script**: | Partition | Invocation context | Resident path | | ------------- | -------------------------------------------------------- | -------------------------------- | | **Lifecycle** | Human operators (CLI, one-time setup, consumer sync) | `bin/` (mandrel CLI subcommands) | | **Runtime** | Agent sessions, git hooks, CI pipelines | `.agents/scripts/` | **Lifecycle scripts** are invoked only by human operators β€” never by agent sessions, git hooks, or CI pipelines. They were moved to the `mandrel` CLI bin (under `bin/`) as part of Epic #3435 to make the boundary explicit and machine-enforceable: - `bootstrap.js` β€” one-time consumer onboarding - `agents-bootstrap-github.js` β€” GitHub-side bootstrap (labels, branch protection) - `sync-claude-commands.js` β€” projects `.agents/workflows/` into the flat Claude Code `.claude/commands/` tree (invoked as `/`) - `sync-agentrc.js` β€” merges upstream `starter-agentrc.json` deltas into the consumer's `.agentrc.json` - `check-windows-git-perf.js` β€” one-time Windows git performance diagnostic - `lib/bootstrap/*` β€” shared bootstrap helper modules **Runtime orchestration scripts** are invoked by agent sessions, git hooks, or CI pipelines and must remain at their `.agents/scripts/` paths (because agents and hooks resolve them via that stable path): - `story-init.js`, `story-close.js` β€” Story worktree lifecycle - `wave-tick.js` β€” idle watchdog tick - `update-ticket-state.js` β€” GitHub label transitions - `dispatcher.js` β€” DAG computation and dispatch manifest - And all other scripts under `.agents/scripts/` not listed above **The rule:** a script is *lifecycle* if it is only ever invoked by a human operator at the terminal; it is *runtime* if it is invoked by an agent session, a git hook, or a CI step via the `.agents/scripts/` path. Moving a lifecycle script to `bin/` without updating the hook or CI invocation site breaks the calling surface; moving a runtime script away from `.agents/scripts/` breaks agent sessions and git hooks. This partition is enforced by the invariant test at `tests/cli/partition.test.js`: it asserts that `.claude/settings.json`'s `UserPromptSubmit` hook no longer contains a bare `node .agents/scripts/sync-claude-commands.js` invocation β€” evidence that the hook migration from Story #3451 has taken effect and the lifecycle script is now correctly invoked through the `mandrel` CLI. --- ### 3. Provider Abstraction Layer All ticketing interactions are mediated through the **`ITicketingProvider`** abstract interface, enabling future portability beyond GitHub. ```mermaid classDiagram class ITicketingProvider { <> +getEpics(filters) Promise +getEpic(epicId) Promise +getTickets(epicId, filters) Promise +getSubTickets(parentId) Promise +getTicket(ticketId) Promise +getTicketDependencies(ticketId) Promise +createTicket(parentId, ticketData) Promise +addSubIssue(parentId, childId) Promise +updateTicket(ticketId, mutations) Promise +postComment(ticketId, payload) Promise +createPullRequest(branchName, ticketId) Promise +ensureLabels(labelDefs) Promise +ensureProjectFields(fieldDefs) Promise } class GitHubProvider { -owner: string -repo: string -token: string +getEpics(filters) Promise +getEpic(epicId) Promise ...all interface methods } ITicketingProvider <|-- GitHubProvider ``` **Resolution**: `provider-factory.js` instantiates `GitHubProvider` β€” the only shipped concrete class. The post-reshape canonical config has no provider-selector key; the factory's `PROVIDERS` map is the registry. **Internal layout**: `provider-factory.js` is the canonical entrypoint for obtaining a `GitHubProvider`; callers go through the factory rather than constructing the class directly. `providers/github.js` is a thin composition root over focused modules under `providers/github/` (tickets, sub-issues, comments, labels, branch-protection, merge-methods, PRs, project-board, and issues gateways). The barrel is **not** a single public re-export point: it re-exports the `GitHubProvider` class plus the five error-classification helpers (`classifyGithubError`, `extractErrorFields`, `isPermissionSignal`, `isTransientByCodeOrMessage`, `isTransientStatus`) β€” the mapper, auth, and sub-issue symbols were removed as dead exports in Story #3650 (Epic #3599). The remaining `providers/github/*` helper modules (e.g. `blocked-by-add.js`, `board-add.js`) are imported **directly** at their call sites, not through the faΓ§ade. --- ### 4. Execution Path Mandrel runs Claude-Code-in-session: `/deliver` fans out via the `Agent` tool over a wave of Story sub-agents, each driving the per-Story implementation loop directly from the Story worktree. There is no separate adapter abstraction β€” `lib/orchestration/manifest-builder.js` synthesizes the `{ taskId, dispatchId, status }` record inline at the dispatch site, and the **dispatch manifest** (md + structured comment, schema [`dispatch-manifest.json`](../.agents/schemas/dispatch-manifest.json)) is the load-bearing artifact downstream tooling (and operators) read. The manifest is the cross-runtime contract: any future host that wants to replay or audit a Mandrel dispatch consumes the manifest, not an in-process interface. The `executor` field on the manifest is fixed to `"claude-code"`. See the adapter-removal ADR in [`decisions.md`](decisions.md) (Epic #2646) for the rationale; the deletion landed as a hard cutover with no shim layer, per the policy codified there. --- ### 5. Configuration System Configuration follows a **layered resolution** pattern with operational settings organised into a **grouped contract**. Optional `.agentrc.local.json` (gitignored) deep-merges on top of `.agentrc.json`; built-in defaults fill any remaining gaps. Absent local file is a no-op. ```mermaid graph LR classDef cfg fill:#fff3cd,stroke:#333,color:#000 A[".agentrc.json"]:::cfg -->|"Priority 1"| R["config-resolver.js"] L[".agentrc.local.json"]:::cfg -->|"Priority 1.5 (gitignored)"| R B["Built-in Defaults"]:::cfg -->|"Priority 2"| R C[".env file"]:::cfg -->|"Env overlay"| R R --> P["project.paths"] R --> CMD["project.commands"] R --> Q["delivery.quality"] R --> LM["planning + delivery limits"] R --> O["github + delivery blocks"] ``` The runtime AJV schemas in `lib/config-schema.js` and `lib/config-settings-schema.js` are the source of truth; the static mirror at `.agents/schemas/agentrc.schema.json` exists for editor tooling and human readers, kept in sync by a drift test. #### Key Configuration Sections | Section | Purpose | | ------------------------ | ---------------------------------------------------------------------- | | `project.paths` | Required filesystem roots (`agentRoot`, `docsRoot`, `tempRoot`). | | `project.commands` | Validate / lint / test / typecheck / build commands; `null` disables. | | `delivery.quality` | Maintainability + CRAP + lint baselines and gate configuration. | | `planning.context`, `delivery.maxTokenBudget`, `delivery.execution` | Resource ceilings (planning-context budget, token budget, execution timeout). | | `github` + `delivery` | GitHub provider config, worktree isolation, deliver-runner tuning. | Each grouped block is read through a typed accessor (`getPaths(config)`, `getCommands(config)`, `getQuality(config)`, `getLimits(config)`) β€” there are no flat-key reads anywhere in the resolver or its consumers. > See [`.agents/docs/configuration.md`](../.agents/docs/configuration.md) for the canonical > reader-facing reference: every key, default, and required-vs-optional flag, > the root-dogfood-vs-distributed-template diff table, and baseline > conventions (canonical `/baselines/` vs per-wave drift snapshots under > `.agents/state/`). Project-specific technology context lives under the > **Tech Stack** section below β€” intentionally not in `.agentrc.json`. **Security**: The config resolver blocks shell metacharacter injection (`; & | \`` `` $()`) in all string values that flow into subprocesses, and the schema enforces non-empty strings on every command field. --- ### 6. Dependency Graph Engine The `Graph.js` module provides the mathematical foundation for task scheduling: | Function | Algorithm | Complexity | | ------------------------- | ------------------------------------------ | ---------- | | `buildGraph()` | Adjacency list construction | O(N) | | `detectCycle()` | DFS 3-color cycle detection | O(V+E) | | `assignLayers()` | Memoized layer assignment | O(V+E) | | `computeWaves()` | Layer-grouped wave partitioning | O(V+E) | | `topologicalSort()` | Kahn's algorithm (deterministic tie-break) | O(V+E) | | `transitiveReduction()` | DFS-based edge pruning | O(VΒ·(V+E)) | | `autoSerializeOverlaps()` | Focus-area conflict serialization | O(NΒ²+VΒ·E) | | `computeReachability()` | Memoized DFS transitive closure | O(VΒ·(V+E)) | The auto-serialization pass prevents file-level merge conflicts by injecting synthetic dependency edges between tasks with overlapping `focusAreas`. --- ## Data Flow: Epic Lifecycle ```mermaid sequenceDiagram participant H as Human participant P as /plan participant EP as epic-plan-spec.js participant TD as epic-plan-decompose.js participant D as /deliver participant EDR as /deliver (in-session) participant CH as hydrate-context.js participant A as Agent (IDE) participant GH as GitHub H->>P: /plan (ideation) or /plan #EPIC P->>GH: Open Epic (ideation path) / read existing Epic P->>EP: Generate PRD + Tech Spec EP->>GH: Create linked context issues EP->>TD: Decompose into Stories TD->>GH: Create the Story backlog H->>D: /deliver #EPIC D->>EDR: Build DAG, compute waves, run the nine-phase flow EDR->>GH: Create epic/ and story/ branches EDR->>CH: Hydrate story context CH-->>EDR: Self-contained prompt EDR->>A: Dispatch story (Agent-tool sub-agent) A->>GH: Update labels (agent::executing β†’ done) EDR->>GH: Open PR to main (Phase 7), iterate CI to green (Phase 8) EDR->>GH: Clean run β†’ Phase 8.5 arms gh pr merge --auto --squash H->>GH: Otherwise operator merges via GitHub UI β†’ Epic flips to agent::done ``` --- ## Epic Deliver Runner The `/deliver ` slash command is the sole entry point for Epic delivery. It runs end-to-end inside the operator's Claude session, composing the orchestration primitives into a nine-phase execution coordinator (Phases 1–9, with the Phase 8.5 auto-merge gate between watch-and-iterate and cleanup β€” see [`helpers/deliver-epic.md`](../.agents/workflows/helpers/deliver-epic.md)) with the lifecycle bus chain at its core. There is no remote-trigger surface β€” delivery only ever runs locally, in the operator's session, with Story sub-agents launched through the Agent tool. Story #2259 (Epic #2172) retired the legacy deliver-runner CLI wrapper; the slash command supplants it entirely. The bus is the **single canonical runner model** under Epic #2172: every phase transition, ticket-state flip, structured-comment upsert, and webhook fan-out is emitted as a typed event that fixed-roster listeners consume. The append-only NDJSON ledger at `temp/epic-/lifecycle.ndjson` is the resume contract. See [`LIFECYCLE.md`](LIFECYCLE.md) for the bus contract, event taxonomy, ledger format, and listener model β€” that document is the canonical reference for the lifecycle bus, and the older "phase boundaries inline-emit comments" framing is retired. ### State machine (Epic labels) ```text /plan ideation creates the Epic with type::epic only (no state::* label at creation) β”‚ β”‚ PRD + Tech Spec authored β–Ό agent::review-spec ◄── operator reviews on GitHub β”‚ β”‚ operator confirms; decomposition runs β–Ό agent::ready ◄── /plan terminates here β”‚ β”‚ operator runs /deliver β–Ό agent::executing ◄── wave loop + close-validation + β”‚ epic-audit + code-review + β”‚ retro + finalize β”‚ wave halts on blocker β–Ό agent::blocked ──── operator flips back ───┐ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ /deliver Phase 7 opens PR to main; β”‚ Phase 8 iterates CI to green; β”‚ Epic stays at agent::executing β”‚ Phase 8.5 β€” clean run? AutomergeArmer β”‚ fires gh pr merge --auto --squash β”‚ --delete-branch; otherwise the β”‚ operator merges via the GitHub UI β–Ό agent::done ◄── set via standard label-transition pathway when the PR merge fires (no GitHub Action; retro already ran) ``` ### Submodules | Module | Role | | ------------------- | --------------------------------------------------------------------------------------------------- | | `wave-scheduler` | Iterates waves from `Graph.computeWaves()`; never spawns workers. | | `story-launcher` | Fans out up to `concurrencyCap` `/deliver ` Agent-tool sub-agents in one message. | | `checkpointer` | Upserts the `epic-run-state` structured comment; handles phase-granular resume across the phases. | | `notification-hook` | Fire-and-forget webhook; never blocks execution. | | `column-sync` | Drives the Projects v2 Status column from `agent::` labels (best-effort). | | `code-review` | `lib/orchestration/code-review.js` β€” Phase 5 inline review (companion to `helpers/code-review.md`). | | `retro-runner` | `lib/orchestration/retro-runner.js` β€” Phase 6 retro authoring (extracted from the helper). | The epic-runner-era `blocker-handler` and `wave-observer` listeners were deleted with the in-process stratum (#3908); `agent::blocked` remains the sole runtime pause point, enforced by the workflow prose rather than a resident listener. ### Phase 4 β€” epic-audit (change-set lenses) Between close-validation (Phase 3) and code-review (Phase 5), `/deliver` runs an inline **epic-audit** stage ([`helpers/epic-audit.md`](../.agents/workflows/helpers/epic-audit.md)). `epic-audit-prepare.js` wraps the audit-suite `selectAudits` SDK: it diffs `main..epic/`, selects the audit lenses whose file patterns or keyword triggers match the change-set, unions in lenses mapped from the Epic's model-judged high-risk axes (Story #3889), and resolves an audit depth (`light` / `standard` / `deep`, Story #3939). Findings feed a bounded auto-fix loop governed by `delivery.epicAudit` (`maxFixAttempts` retry cap per finding; `maxFixScopeFiles` files per auto-fix before escalating to `agent::blocked`). `epic-audit-recheck.js` re-selects only the lenses whose patterns overlap files touched by the later code-review auto-fix tail. Operators can skip the stage with `--skip-epic-audit` (logged override; for known-irrelevant change-sets such as docs-only Epics). ### HITL touchpoints One runtime pause point β€” `agent::blocked` on the Epic. `risk::high` is metadata; mid-run changes are ignored. Branch protection on `main` (set up during `node .agents/scripts/bootstrap.js`) is the load-bearing destructive-action guard on the promotion path: the Epic PR merges either via the Phase 8.5 auto-merge gate (armed only when the clean-run predicate passes β€” zero manual interventions, zero πŸ”΄/🟠 review findings, compact retro; Story #3901) or via the operator's GitHub-UI merge on the fallback path. Either way, required status checks gate the squash onto `main`. ### Per-Story acceptance self-eval Inside each Story delivery (Epic-attached `helpers/epic-deliver-story` Step 1a, and the standalone path alike), a bounded **acceptance self-eval** loop runs after the implementation commits land and before the Story proceeds to close. A **fresh-context critic** sub-agent β€” independent of the implementing turn β€” scores the working diff against each inline `acceptance[]` item, using `verify[]` as evidence; `acceptance-eval.js` records the per-criterion verdict (pass `--epic` on the Epic-attached path so the signal lands on the Epic-scoped stream). On `proceed` the Story flips to `closing`; unmet criteria trigger a redraft round, bounded by `delivery.acceptanceEval.maxRounds` (default 2, clamped into `[1, hard ceiling]` β€” the loop cannot be disabled). If the round cap is reached with criteria still unmet, the Story escalates: `agent::blocked`, a `friction` comment naming the unmet criteria, and a non-zero exit. The single prose home for the mechanic is [`helpers/acceptance-self-eval.md`](../.agents/workflows/helpers/acceptance-self-eval.md). ### Standalone multi-Story delivery (no Epic) Stories without an `Epic: #N` reference do not enter the Epic wave loop. `/deliver [...]` routes them to [`helpers/deliver-stories.md`](../.agents/workflows/helpers/deliver-stories.md), which builds a dependency-aware wave plan and fans out one `helpers/single-story-deliver` Agent call per Story per wave β€” parallel within a wave, serialised across waves. The script surface parallels the Epic-attached trio (`story-init.js` / `wave-tick.js` / `story-close.js`): | Script | Responsibility | | ------------------------------ | ----------------------------------------------------------------------------------------------------------------- | | `single-story-init.js` | Validates the standalone Story, branches directly from `main` (no `epic/` seed, no dispatch-manifest gate). | | `stories-wave-tick.js` | Continuous ready-set planner for the standalone fan-out β€” a thin adapter over the shared `selectReadySet` core; emits the per-beat dispatch set on the `stories-ready-set` envelope and resolves the global `concurrencyCap` (default 3). | | `story-phase.js` | Phase snapshot + heartbeat writer at each Story-level transition (init β†’ implementing β†’ closing β†’ done / blocked). | | `single-story-close.js` | Runs the canonical close-validation gate chain against the base branch, opens the PR straight to `main` with auto-merge armed, and rests the Story at `agent::closing`. | | `single-story-confirm-merge.js` | Post-merge confirmation: once `gh pr checks --watch` exits green and the PR merges, flips `agent::closing β†’ agent::done` and closes the issue. | The exit contract differs from the Epic path: each standalone Story reaches `main` via its own human-visible PR (auto-merge armed at close), and the deferred confirm-merge step β€” not an in-script merge β€” performs the terminal label flip after GitHub's asynchronous auto-merge completes. ### Operator-tunable delivery knobs Several schema-declared `delivery.*` blocks tune the runner without changing its shape (full per-field reference: [`.agents/docs/configuration.md`](../.agents/docs/configuration.md)): - **`delivery.codeReview.providers` / `provider` / `providerConfig`** β€” pluggable review backend for Phase 5. The legacy single-string `provider` selects one of `native` / `codex` / `security-review`; the `providers: []` chain shape (which wins when non-empty) sequences multiple providers with per-entry scopes, label conditions, and the `ultrareview` manual-prompt entry. `providerConfig` is an open-shape escape hatch for adapter-specific options. Tune when you want an external or layered review chain instead of the native single pass. - **`delivery.mergeWatch.{intervalSeconds,maxBudgetSeconds}`** β€” poll cadence and total wall-clock budget for the `MergeWatcher` lifecycle listener after `epic.merge.armed` (defaults 30s / 3600s); exceeding the budget surfaces `agent::blocked` with reason `budget-exceeded`. Tune on repos with slow required checks. - **`delivery.refactorStage.enabled`** β€” opt-in (default off) advisory post-green refactor checkpoint in story-deliver (`refactorer` persona + `core/refactoring-discipline` skill); never alters close-validation gate semantics. - **`delivery.feedbackLoop.{codeReviewAutoFile,auditResultsAutoFile}`** β€” both default `true`: the Epic finalize listener auto-files non-blocking code-review / audit findings as follow-up issues (routed via `lib/feedback-loop/`). Set `false` to keep findings only in the Epic's structured comments. - **`delivery.ci.skipForStoryPushes`** β€” default `true`: pre-push tooling appends `[skip ci]` to Story-branch commit subjects so intermediate pushes don't stampede CI; the Epic-branch merge commit never carries the marker. - **`delivery.failOnConcurrencyHazards`** β€” default off: when `true`, `epic-deliver-prepare` refuses to flip the Epic to `agent::executing` while the upcoming waves carry an unresolved conflict finding. --- ## Ticket Hierarchy The framework uses a **2-tier GitHub Issue hierarchy** (Epic β†’ Story) with label-based typing and `blocked by #NNN` dependency wiring. Thematic grouping lives as prose in the Epic body / Tech Spec, never as a ticket: ```text Epic (type::epic) β”œβ”€β”€ PRD (context::prd) β”œβ”€β”€ Tech Spec (context::tech-spec) β”œβ”€β”€ Story (type::story) β”‚ β”œβ”€β”€ acceptance[] ← inline on Story body β”‚ └── verify[] ← inline on Story body └── Story (type::story) ``` `/deliver` runs a single Story-implementation phase per Story. The state machine, cascade behavior, and worktree-isolation contract documented below apply at the Story tier. ### State Machine Each Story progresses through a label-driven state machine: ```mermaid stateDiagram-v2 [*] --> agent_ready: Created by decomposer agent_ready --> agent_executing: Dispatcher picks up agent_executing --> agent_done: story-close.js fires agent_done --> [*] agent_executing --> agent_ready: Hotfix rollback ``` ### Cascade Behavior When a Story transitions to `agent::done`, `cascadeCompletion()` walks upward through the hierarchy and closes parents whose children are all done. The cascade is **not** uniform across tiers β€” the table below is the authoritative contract: | Parent tier | Auto-closes via cascade? | How it closes | | ----------------------------------------------- | ------------------------ | ---------------------------------------------------------------- | | Epic (`type::epic`) | **No** β€” cascade stops. | The `/deliver` PR merges β€” auto-merge when the Phase 8.5 clean-run gate armed it, otherwise the operator merges via the GitHub UI. | | Planning (`context::prd`, `context::tech-spec`) | **No** β€” cascade stops. | Operator close after the Epic PR is merged. | **Why neither tier auto-closes.** Epics gate on a real pull-request merge β€” cascade must not pre-empt the operator's required-checks review. Planning tickets (PRD, Tech Spec) are narrative artefacts the operator closes once the Epic PR is merged. Implementation: [`.agents/scripts/lib/orchestration/ticketing.js`](../.agents/scripts/lib/orchestration/ticketing.js) β€” `cascadeCompletion()` explicitly skips `type::epic`, `context::prd`, and `context::tech-spec` parents; every other parent tier is eligible. The `fromState` lookup inside `transitionTicketState()` has a deliberate try/catch β€” a network flake reading the prior state label must not block a legitimate transition; failures emit a `debug`-level log instead of swallowing silently. --- ## Workflow System The shipped slash commands (under `.agents/workflows/`) fall into six categories β€” planning, execution, closure, audits, git operations, and setup/meta. The canonical reference is [`.agents/docs/workflows.md`](../.agents/docs/workflows.md); the workflow narrative that wires them together lives in [`.agents/docs/SDLC.md`](../.agents/docs/SDLC.md). ### Worktree Isolation When `delivery.worktreeIsolation.enabled` is `true`, each dispatched story runs inside its own `git worktree` at `.worktrees/story-/`. The main checkout's HEAD never moves during a parallel run; branch swaps, staging operations, and reflog activity are isolated per-story. The `WorktreeManager` (`.agents/scripts/lib/worktree-manager.js`) is the single authority for worktree `ensure`/`reap`/`list`/`isSafeToRemove`/`gc`. No other script may call `git worktree` directly. All git calls are argv-based (no shell interpolation) and validate `storyId` / `branch` before shelling out. `reap` only reaches `git worktree remove --force` after its safety gate has already established the Story worktree is removable and the plain remove path has exhausted Windows lock/cwd retry. **Internal submodule layout.** `worktree-manager.js` is a faΓ§ade composing four cohesive submodules under `.agents/scripts/lib/worktree/`: | Submodule | Responsibility | | -------------------------- | ------------------------------------------------------------------------------------------------------- | | `lifecycle-manager.js` | `ensure`, `reap`, `list`, `gc`, `prune`, `sweepStaleLocks`, Windows-lock-aware remove recovery. | | `node-modules-strategy.js` | `applyNodeModulesStrategy` + `installDependencies` for `per-worktree` / `symlink` / `pnpm-store`. | | `bootstrapper.js` | Bootstrap-file copy (`.env`) into a freshly created worktree. | | `inspector.js` | Pure porcelain parsing, path helpers (`samePath`, `storyIdFromPath`, `isInsideWorktree`), Windows path warnings. | The submodules are **internal implementation detail**. Downstream projects must continue to import `WorktreeManager` from `lib/worktree-manager.js`. Dispatcher integration: - **Ensure before dispatch**: `dispatch()` calls `wm.ensure(storyId, branch)` and threads the resolved worktree path as `cwd` on the dispatch record. Downstream consumers of the dispatch manifest can use the `cwd` to pin sub-agent execution to the right worktree. - **Reap on merge**: `story-close` calls `wm.reap` after a successful merge. The reap refuses dirty trees and logs a warning. - **GC on dispatch start**: `dispatch()` sweeps orphaned worktrees whose stories have no remaining live work. Refuses to delete unmerged branches. Setting `delivery.worktreeIsolation.enabled: false` (or omitting the block) restores single-tree behavior. The `assert-branch.js` pre-commit guard and focus-area wave serialization remain in place as defense-in-depth in both modes. See [`worktree-lifecycle.md`](../.agents/workflows/helpers/worktree-lifecycle.md) for the operator reference, node_modules strategies, Windows long-path handling, and escape hatches. ### Execution-model modes The unified `/deliver` execution surface runs in two execution-model modes that share one codepath and differ only in whether worktrees are created. The `resolveWorktreeEnabled` function in `lib/config-resolver.js` selects the mode at startup based on `AP_WORKTREE_ENABLED` and `CLAUDE_CODE_REMOTE` (precedence in [`patterns.md`](patterns.md)): ```text β”Œβ”€β”€β”€β”€ Local-parallel (worktrees on, default) ─────┐ β”Œβ”€β”€β”€β”€ Web-parallel (worktrees off, auto) ─────┐ β”‚ β”‚ β”‚ β”‚ β”‚ one machine, one clone of the repo β”‚ β”‚ N web tabs, each its own sandboxed clone β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”Œβ”€ main checkout ──────────────────────┐ β”‚ β”‚ β”Œβ”€ tab 1 (clone A) ─┐ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ story-680 β”‚ β”‚ β”‚ β”‚ HEAD never moves while waves run β”‚ β”‚ β”‚ β”‚ branch HEAD β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”Œβ”€ .worktrees/story-680/ ─┐ β”‚ β”‚ β”‚ β”Œβ”€ tab 2 (clone B) ─┐ β”‚ β”‚ β”‚ β”‚ story-680 branch HEAD β”‚ β”‚ β”‚ β”‚ β”‚ story-681 β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ branch HEAD β”‚ β”‚ β”‚ β”‚ β”Œβ”€ .worktrees/story-681/ ─┐ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ story-681 branch HEAD β”‚ β”‚ β”‚ β”‚ β”Œβ”€ tab 3 (clone C) ─┐ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ story-682 β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ branch HEAD β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ Concurrency primitive: git worktree β”‚ β”‚ Concurrency primitive: separate clones β”‚ β”‚ Coordination at close: filesystem lock β”‚ β”‚ Coordination at close: bounded push retry β”‚ β”‚ Operator launches: N IDE windows β”‚ β”‚ Operator launches: N web tabs β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β–² β–² β”‚ β”‚ └────────── shared launch primitive β”€β”€β”€β”€β”€β”€β”˜ operator picks Story id from /plan dispatch table, one session per id ``` Both modes share: - The same `/deliver` Agent-tool sub-agent contract and the same parent-driven dispatch logic out of `/deliver`'s wave loop. - The launch-time blocker pre-flight (`validateBlockers` in `lib/story-init/blocker-validator.js`, run by `story-init.js`) that refuses a story with unmerged blockers. - Deterministic, operator-driven story assignment β€” `/deliver` always takes an explicit Story id. There is no per-launch label race. - The bounded retry on the epic-branch push (`lib/push-epic-retry.js`, using the framework-internal `DEFAULT_STORY_MERGE_RETRY` constant) so concurrent closes from separate clones converge cleanly. They differ only in: - **Filesystem layout.** Worktrees create `.worktrees/story-/` siblings to the main checkout; web sessions write directly into the cloned workspace because the session is already isolated. - **`node_modules` strategy.** `nodeModulesStrategy` runs only in worktree-on mode. Web sessions install once at the workspace root. - **Path-length warnings.** Windows long-path warnings come from worktree paths β€” they don't fire on web (Linux) or in worktree-off mode generally. - **GC scope.** `WorktreeManager.gc()` runs at dispatch start in worktree-on mode; in worktree-off mode it is a no-op. --- ## Security Architecture ### Input Validation - **Shell injection protection**: `config-resolver.js` scans all config string values against a metacharacter regex (`/([;&|`]|\$\()/`) before they reach subprocess calls. - **Branch name validation**: `dependency-parser.js` enforces safe branch component characters (alphanumeric, hyphens, underscores, dots, slashes). - **Schema validation**: `orchestration` config is validated against an embedded JSON Schema via `ajv`. The static `.agents/schemas/*.json` mirrors and the runtime AJV schemas declare `additionalProperties: false` on every nested object as well as the document roots of `audit-results`, `friction-event`, `agentrc`, and `epic.yaml`, and use a closed enum for `validation-evidence.gateName`. Payloads with extra keys or free-text discriminators fail validation rather than silently passing. ### HITL pause point The sole runtime pause is `agent::blocked` on the Epic. `risk::high` is informational/planning metadata only β€” it ranks work in the dispatch table and helps reviewers prioritize, but does not pause execution. `planning.riskHeuristics` in `.agentrc.json` drives the ranking heuristics. ### Anti-Thrashing Protocol The protocol is **qualitative, agent-judgment-based** β€” there are no numeric thresholds and no config keys to tune, because no framework code increments a counter or fires at a boundary. The authoritative contract is [`.agents/instructions.md` Β§ 1.I](../.agents/instructions.md); the agent stops, summarizes, and re-plans (or yields to the operator) on any of three cues: - **Failure cluster**: a handful of tool calls in a row return errors of the same shape and the next attempt is unlikely to surface new information. - **Research drift**: several steps of reading code or docs without writing anything, and the additional reads no longer narrow the problem space. - **Same fix, same failure**: the same kind of fix has been applied more than once for the same error class and the failure mode hasn't changed β€” the diagnosis is wrong. The protocol has a runtime substrate: a Story delivery sub-agent emits a `story.heartbeat` lifecycle event on each Task transition (or when it stalls on a long-running step), and the parent `/deliver` idle watchdog (`wave-tick.js --check-idle`, re-ticked every 30 minutes) consumes those heartbeats to distinguish a child still making progress from a dead one. A child with no recent `story.heartbeat`, no commit on its `story-` branch, and no `agent::blocked` label is the failure mode the watchdog re-dispatches or escalates without the child's participation. --- ## Observability ### Performance-Signal Telemetry The framework emits a closed taxonomy of NDJSON record kinds β€” the active detectors `friction`, `hotspot`, `rework`, `retry`, plus the raw `trace` (schema: [`signal-event.schema.json`](../.agents/schemas/signal-event.schema.json)). The schema also reserves `churn` and `idle` slots for future use; their detectors and config keys were dropped under Epic #1721 (see ADR in [`docs/decisions.md`](decisions.md)) but the names remain in `EVENT_KINDS` so a future re-introduction does not need a schema bump. Records are written **append-only to local disk** under `temp/epic-/stories/story-/signals.ndjson` (and a sibling `traces.ndjson` for `kind: trace`). GitHub tickets receive **summaries only**, never raw events. The model has three layers: 1. **Producers β€” `signals-writer.js`.** Detectors and the runtime `tool-trace-hook.js` funnel through `appendSignal` / `appendTrace`. The writer is **best-effort and unbuffered**: every call opens, writes one newline-terminated JSON line, and closes. fs / JSON failures are swallowed via `Logger.warn` so observability never halts a wave, and detectors that fire from inside a sub-agent that may exit abruptly do not lose their tail. The per-Story directory is created lazily on the first write; `epicId` / `storyId` must be positive integers. 2. **Detectors β€” `diagnose-friction.js` and the per-detector pure modules under `lib/signals/detectors/` (`rework.js`, `retry.js`, `hotspot.js`).** Rework + retry run inside the post-Story close pipeline (`lib/orchestration/post-merge-pipeline.js`); hotspot signals are aggregated at Epic close by the retro's signal-gathering phase (`lib/orchestration/retro/phases/gather-signals.js`) and the epic-perf-report pipeline. Each call site resolves thresholds via `getSignals(config)` (defaults: `hotspot.p95Multiplier=1.25`, `rework.editsPerFile=5`, `retry.repeatCount=3`). Operators override individual keys in `.agentrc.json` under `delivery.signals.*`; the resolver shallow- merges per detector, so a re-tuned `hotspot.p95Multiplier` does not require re-listing the others. 3. **Analyzers β€” Story close + Epic deliver retro.** At Story close, `story-close.js` rolls the local NDJSON into a single [`structured:story-perf-summary`](../.agents/schemas/story-perf-summary.schema.json) comment carrying friction counts by category, phase timings, top-slow phases vs baseline, a rework score, and retry density. At `/deliver` Phase 6, `analyze-execution.js` aggregates every Story's NDJSON into one [`structured:epic-perf-report`](../.agents/schemas/epic-perf-report.schema.json) comment alongside the retro: per-kind signal counts, per-wave parallelism utilization, top hotspots, and the most-friction Stories. The split β€” events local, summaries on tickets β€” keeps the GitHub comment surface bounded (one summary per Story, one report per Epic) and keeps the raw stream cheap enough that detectors can fire on every tool-call without rate-limiting or batching. The per-Epic temp tree is reaped together with the worktree on `WorktreeManager.reap`. See [`docs/decisions.md`](decisions.md) ADR for the architectural rationale. ### Log Levels `lib/Logger.js` is the single orchestrator logger. Level is selected via `AGENT_LOG_LEVEL`: - `silent` β€” only `fatal` emits. - `info` β€” default. `info` / `warn` / `error` / `fatal` emit; `debug` is suppressed. - `verbose` β€” all levels emit, including `debug` trace output. `debug` is accepted as a backward-compatible alias for `verbose`. ### Notification System | Event | Severity | Channel | | ------------------- | -------- | ------------------ | | `task-complete` | INFO | GitHub @mention | | `feature-complete` | INFO | GitHub @mention | | `epic-complete` | INFO | @mention + webhook | | `review-needed` | ACTION | @mention + webhook | | `approval-required` | ACTION | Webhook | | `blocked` | ACTION | Webhook | `github.notifications` carries two independent per-channel gates, both using the same event-name-allowlist model: `commentEvents` filters GitHub-ticket comment posting; `webhookEvents` filters `NOTIFICATION_WEBHOOK_URL` deliveries. There is no fallback chain; raising or lowering one channel never affects the other. The default comment allowlist is `state-transition`, `story-merged`, `operator-message`; the default webhook allowlist is the curated `epic-*` vocabulary β€” `epic-started`, `epic-progress`, `epic-blocked`, `epic-unblocked`, `epic-complete` β€” so Slack consumers see the epic narrative (% progress + blockers) without the per-story firehose. `transitionTicketState` suppresses the `notify()` dispatch entirely for low-severity transitions (non-terminal story / epic flips) so the comment channel sees only the medium-severity story-level events operators expect. Severity is carried as envelope metadata and still drives `@mention` behavior on the comment channel but is no longer a routing factor for either channel. Webhook subscribers receive a typed envelope (`{ text, severity, ticketId, event?, level?, epicId?, phase? }`) so allowlisted events stay routable by event name and hierarchy level. --- ## Testing The test suite uses the **Node.js native test runner** (`node --test`) with no external test framework dependencies. Tests live under `tests/` with `tests/lib/` for library-specific unit tests and `tests/epic-runner/` for runner-integration tests. Run with `npm test`. --- ## CI/CD Pipeline A single GitHub Actions workflow (`ci.yml`) runs on every push and PR, with three jobs: 1. **Validate and Test** β€” the main job, in step order: `npm audit` (SCA), TruffleHog secret scanning, **Lint and Format** via `npm run lint` (which folds in the Biome format check β€” Story #1829), **Maintainability Check** via `npm run maintainability:check` (`node .agents/scripts/check-baselines.js --gate maintainability`; diff-scoped on PRs, `BASELINE_SCOPE=full` on push-to-main), and **Run Tests with Coverage** via `npm run test:coverage`. Artifacts: `test-results` (test output) and `coverage-final` (c8 coverage map). 2. **baselines** β€” `node .agents/scripts/check-baselines.js --format text`, surfaced as its own required status check (Epic #1943 / Story #1981); the unified floor + tolerance + schema gate that replaced the retired per-kind `check-maintainability` / `check-crap` / `check-mutation` scripts. 3. **Windows Smoke** β€” advisory (non-required) Windows leg (Story #3389): bootstrap dry-run, command sync, and config-resolution tests. Distribution is **not** handled by `ci.yml`. A separate `release-please.yml` workflow cuts releases and runs the `npm-publish` job that publishes `mandrel` to npm with Sigstore build provenance once a release is tagged. The retired `dist`-branch mirror that `ci.yml` once synced no longer exists. The baseline-refresh CI guardrail was removed alongside the bot-approver pipeline; the `baseline-refresh:` commit subject + non-empty body convention is preserved (the pre-push hook and local close-validation still consume it) but it is no longer machine-enforced on PRs. The operator owns refresh justification during `/deliver`'s Phase 8 watch-and-iterate loop. ### Quality-gate diagram ```text β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” local β–Ά β”‚ pre-push (.husky/pre-push): β”‚ β”‚ quality-preview (diff) β†’ β”‚ β”‚ coverage-capture β†’ crap:check β”‚ β”‚ (full lint+test: npm run verify) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” close β–Ά β”‚ close-validation DEFAULT_GATES: β”‚ β”‚ typecheck β†’ lint β†’ test β†’ format β†’ β”‚ β”‚ coverage-capture β†’ check-baselines β”‚ β”‚ (test drops when crap.enabled; β”‚ β”‚ each gate skips when SHA-keyed β”‚ β”‚ evidence still matches) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” CI β–Ά β”‚ ci.yml: β”‚ β”‚ audit+secrets β†’ lint+format β†’ MI β”‚ β”‚ (check-baselines --gate maint.) β†’ β”‚ β”‚ test:coverage β†’ baselines job β”‚ β”‚ (check-baselines --format text) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ### Evidence-aware gate caching Local close-validation, the `helpers/code-review.md` review pass, and `/deliver` Phase 3 (close-validation) wrap each gate in `evidence-gate.js`. On a successful run the wrapper records `{ gateName, commitSha, commandConfigHash, timestamp }` under the per-Epic tree at `temp/epic-/validation-evidence.json` for Epic-scoped gates and `temp/epic-/stories/story-/validation-evidence.json` for Story-scoped gates (both gitignored via `temp/`). Callers must pass both `--scope-id` and `--epic-id`. Subsequent invocations against the same `git rev-parse HEAD` and resolved command config skip in milliseconds. `--no-evidence` forces a re-run; pre-push and CI ignore the evidence file entirely so independent verification is never bypassed. All three sites converge on the same `check-baselines.js` runner (per-kind invocations use `--gate `, e.g. `check-baselines.js --gate crap`) and the same `baselines/` artifacts, so a regression caught at any one site fails the gate identically at the others. ### Local Hooks - **Husky** + **lint-staged**: Auto-lint and format staged files on commit. #### `lint-staged` biome config: `--no-errors-on-unmatched` The biome steps in `.lintstagedrc` (`biome check` / `biome format`) carry the `--no-errors-on-unmatched` flag. This is the canonical fix for the defect tracked in **Story #3529**. **Background.** `biome.json` sets `vcs.useIgnoreFile=true`, so biome honours `.gitignore`. Epic #3436 (PR #3485) briefly added `/.agents/` to `.gitignore` as part of the in-flight npm-distribution migration. Because `.agents/` is the framework's own committed source tree, every staged `.agents/**/*.js` change was then handed to biome as an *ignored* path: biome processed 0 files and **exited 1**, hard-failing the pre-commit hook on any framework `.js` commit. Story #3489 (PR #3531) removed the `/.agents/` ignore in this source repo (the `.gitignore` NOTE block records why the framework repo keeps `.agents/` tracked while consumer projects ignore their materialized copy), which eliminates the original trigger. **Why the flag stays.** `--no-errors-on-unmatched` is retained as a defensive default rather than reverted now that #3489 fixed the root cause. Without it, biome treats an "all staged paths ignored" set as an error and exits non-zero; with it, biome still lints/formats every *non-ignored* staged file (no silent coverage loss β€” verified: a staged `.agents/scripts/` edit passes the hook and is linted) but no longer hard-fails when a commit happens to stage only ignored paths. The `.gitignore` in this repo still ignores local-override paths (`.agents/*.local.md`, `.agents/*local.json`) and consumer projects ignore their entire materialized `/.agents/` copy, so an all-ignored staged set remains a reachable state the flag guards against at zero cost. `.lintstagedrc` is plain JSON and cannot carry an inline comment, so this rationale lives here. --- ## FinOps Model The framework limits context and dispatch cost through **estimation**, not live LLM metering: ### Budget protocol - **`delivery.maxTokenBudget`**: caps hydrated task prompts. `hydrate-context` estimates tokens (β‰ˆ4 characters per token) and elides sections when the envelope exceeds the cap (`elideEnvelope` in `context-envelope.js`). - **`delivery.preflight.*`** (optional): `epic-deliver-preflight.js` compares pre-dispatch estimates (stories, waves, install time, GitHub API calls, Claude quota tokens) to configured ceilings before `/deliver` fan-out. - **Host runtime**: session quota and billing hard stops are enforced by the operator's editor / CLI provider, not by Mandrel scripts. --- ## Distribution Model Mandrel is distributed as the [`mandrel`](https://www.npmjs.com/package/mandrel) npm package. The package payload is materialized into the consumer's `./.agents/` directory as plain regular files by `mandrel sync` (run best-effort from the package `postinstall`, or invoked directly): ```text Consumer Project/ β”œβ”€β”€ node_modules/ β”‚ └── mandrel/ ← installed package (pinned, provenance-signed) β”œβ”€β”€ .agents/ ← materialized by `mandrel sync` (copy-only, never a symlink) β”‚ β”œβ”€β”€ instructions.md β”‚ β”œβ”€β”€ personas/ β”‚ β”œβ”€β”€ rules/ β”‚ β”œβ”€β”€ skills/ β”‚ β”œβ”€β”€ workflows/ β”‚ β”œβ”€β”€ scripts/ β”‚ └── ... β”œβ”€β”€ .agentrc.json ← Project-specific configuration └── ... ``` Consumers `npm install mandrel` (which pins an exact, provenance-signed version in the lockfile), run `mandrel sync` to materialize `./.agents/`, copy `starter-agentrc.json` to their project root as `.agentrc.json`, and configure their `orchestration` block β€” see `.agents/docs/agentrc-reference.json` for the exhaustive reference. The ongoing upgrade path is `mandrel update` (bump β†’ sync β†’ migrate β†’ doctor). Project-specific technology context lives in `docs/architecture.md` under the **Tech Stack** section below β€” not in `.agentrc.json`. --- ## Tech Stack This section is the authoritative reference for the technology choices the agent should assume when working in this repository. Keep it **current**: the agent reads this to decide how to write code, which commands to run, and which conventions to follow. > **Template note:** Downstream projects should maintain their own > `## Tech Stack` section in their own `docs/architecture.md`. Mandrel > does not ship a standalone template β€” this section doubles as the working > example. ### Runtime & Language - **Runtime:** Node.js (ESM, `"type": "module"` in `package.json`) - **Language:** JavaScript with JSDoc for type hints (no TypeScript build step) - **Package manager:** npm ### Tooling - **Linter & formatter:** Biome (`@biomejs/biome`) - **Markdown lint:** `markdownlint-cli` - **Markdown format:** Prettier (markdown only) - **Git hooks:** Husky + `lint-staged` - **JSON Schema validation:** Ajv + `ajv-formats` - **In-memory filesystem for tests:** `memfs` - **Shell argv parsing:** `string-argv` - **Complexity metrics:** `typhonjs-escomplex` (maintainability baseline enforcement) ### Testing - **Framework:** Node.js native test runner (`node --test`) - **Test file pattern:** `tests/**/*.test.js` - **Coverage:** `node --experimental-test-coverage` with absolute floors enforced per-file: lines β‰₯ 90, branches β‰₯ 85, functions β‰₯ 90, MI β‰₯ 70, CRAP ≀ 20. See [`.agents/docs/quality-gates.md`](../.agents/docs/quality-gates.md) for the ratchet-plus-floor policy. ### Key Scripts - **Orchestration engine:** `.agents/scripts/lib/orchestration/` β€” dispatch, manifest build, story execution, context hydration - **Ticketing provider abstraction:** `.agents/scripts/lib/ITicketingProvider.js` with a shipped GitHub implementation in `.agents/scripts/providers/github.js` - **Execution path:** Claude-Code-in-session; the dispatch record is synthesized inline at `lib/orchestration/manifest-builder.js` and the [dispatch manifest](../.agents/schemas/dispatch-manifest.json) is the cross-runtime contract. Epic #2646 removed the previous `IExecutionAdapter` abstraction as a hard cutover. - **Config resolution:** `.agents/scripts/lib/config-resolver.js` + `config-schema.js` (shell-metacharacter injection guards built in) - **Operator scripts catalog:** [`.agents/scripts/README.md`](../.agents/scripts/README.md) documents the optional operator-only CLIs (`loc-delta.js`, `validate-docs-freshness.js`, `update-mutation-baseline.js`) that are not wired into `npm` / Husky / CI. ### Ticketing & CI - **Ticketing provider:** GitHub (Issues, Labels, Projects V2, Sub-Issues API) - **CI:** GitHub Actions - **Distribution:** GitHub Releases (tagged from `main` post-PR-merge; tagging is operator-driven since `/deliver` exits at PR-open). ### Testing Contract Consumers of the framework follow a **pyramid-aware** testing contract defined in `.agents/rules/testing-standards.md`. Every test belongs to exactly one of three tiers and carries distinct scope, dependency, and assertion rules: - **Unit** β€” pure logic, no I/O; assertions on return values and rendered output. - **Contract** β€” API ↔ DB invariants and schema conformance; this is the sole correct home for HTTP status codes, response body shapes, and error-envelope assertions. - **E2E / Acceptance** β€” `.feature` files authored against `.agents/rules/gherkin-standards.md` (the SSOT for the tag taxonomy and forbidden patterns) and executed via `/qa-run`, whose sweep summary and structured findings are the canonical evidence artifact consumed by the `workflows/helpers/epic-testing.md` helper. Stack skills `skills/stack/qa/gherkin-authoring` and `skills/stack/qa/playwright-bdd` provide authoring guidance and runtime wiring respectively; neither redefines the rule. Scripts in this repository do not themselves run `.feature` files β€” they ship the contract that consumer projects implement. ### Agent-driven QA harness The E2E/Acceptance tier is executed by the **agent-driven QA harness** (`/qa-run`, Epic #3214). It is the successor to the framework's earlier headless BDD runner (now retired): rather than a Node orchestrator running Cucumber headlessly, the harness is a **prose workflow** the host LLM executes against a **real browser** through the `chrome-devtools` MCP surface, with a human observing. For the harness, the deterministic Node helpers under `.agents/scripts/lib/qa/` do contract resolution (`resolve-qa-contract.js`), scenario selection (`resolve-selection.js`), and console filtering (`console-allowlist.js`); the LLM owns navigation, assertion, and triage. The same `lib/qa/` directory also houses the shared exploratory-QA core consumed by `/qa-assist` and `/qa-explore` (see below): `qa-session.js`, `redact-evidence.js`, `coverage-verdict.js`, `coverage-report.js`, `propose-missing-test.js`, and `qa-context-hydrator.js`. The full run procedure is the SSOT in [`.agents/workflows/qa-run.md`](../.agents/workflows/qa-run.md); the instrumentation conventions live in the `skills/stack/qa/qa-harness` skill. **How it is invoked.** `/qa-run `, where the selector scopes the sweep to a concrete, deterministic scenario set: - `feature:` β€” the single `.feature` file whose `featureRoot`-relative path stem (or basename) matches the id. - `tag:` β€” the scenario set satisfying a cucumber boolean tag expression (`@smoke and not @wip`). - `domain:` β€” every scenario under the `featureRoot`-relative subdirectory `name`. **Run pipeline.** Each sweep runs the same fixed sequence: 1. **Resolve** the consumer's `qa` contract via `resolveQaContract(config)`. The resolver **fails loudly** β€” there is no auto-detection fallback β€” when the block is absent, malformed, or missing a required field. 2. **Select** the scenario set deterministically (`(file, line)`-sorted) so re-running the same selector scopes the identical set and evidence stays diffable. 3. **Sign in** once per persona via the contract's `signInSeam` β€” a dev-only seam; real credentials are never entered. 4. **Drive** each scenario **navigation-first**: start at a root and reach the surface under test only via UI affordances (never URL-jump to a deep link), and assert every `Then` **semantically** against the accessibility snapshot β€” never against DOM/CSS selectors, HTTP status codes, or DB rows (those are contract-tier concerns). 5. **Instrument & inspect** per surface: capture console and network, filter console through the `consoleAllowlist`, and spot-check against `designTokens` when set. Each surviving signal becomes one structured `F#` finding. 6. **Draft** follow-ups bundled by likely root cause for **operator sign-off** β€” the harness never files tickets autonomously. **The `qa` contract block.** Binding the harness is opt-in: a consumer adds a top-level `qa` block to `.agentrc.json`. The block is *optional in the schema* (so config validation never breaks a non-QA consumer); presence is enforced at run time by `resolveQaContract`. The full reference shape lives in [`.agents/docs/agentrc-reference.json`](../.agents/docs/agentrc-reference.json). Fields: | Field | Required | Meaning | | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `featureRoot` | yes | Filesystem root the selector resolves `.feature` files against. | | `fixturesManifest` | yes | Path to the persona β†’ seed-data manifest loaded before sign-in. | | `signInSeam` | yes | Discriminated union: `{ urlTemplate }` (substitute `{persona}` into a dev sign-in URL) **or** `{ skill }` (invoke a named consumer skill for procedural sign-in). | | `personas` | yes | Either a name-only `string[]` (the honest shape under a `{ urlTemplate }` seam, where the persona name is the sole input) **or** a map of persona name β†’ `{ credentialRef }` / `{ signInSkill }` (per-persona auth material, consulted only under a `{ skill }` or credential seam). Never an inline secret. The resolver normalizes both to one canonical map keyed by persona name. | | `consoleAllowlist` | no | Benign-console substring patterns to suppress (default `[]`). A noise filter, **not** a security control β€” never expand it to silence a genuine error. | | `designTokens` | no | Pointer to the token/style source for visual spot-checks (default `null`). When `null`, the design-token check is skipped entirely. | **Findings β€” the `F#` shape.** Every captured problem is normalized into a structured finding validated against [`.agents/schemas/qa-finding.schema.json`](../.agents/schemas/qa-finding.schema.json): `{ id, classification, surface, symptom, likelyRootCause, disposition (blocker | follow-up), acceptance, foldsInto?, evidence: { console[], network[] } }`. Captured evidence is scrubbed of tokens, session cookies, and PII before any finding is rendered, because findings are posted to GitHub at approval time. #### Exploratory QA: `/qa-assist` and `/qa-explore` Two sibling prose workflows complement the scenario-stepping harness with open-ended QA, both routed through the same shared core under `.agents/scripts/lib/qa/` and `.agents/scripts/lib/findings/`: - **`/qa-assist`** β€” **human-led**, single-observation-at-a-time (Intake β†’ Enrich β†’ Record). The operator reports one observation; the agent enriches it into a triage-ready ledger item β€” clean repro, root-cause locus (`file:line`), and a coverage verdict β€” asking clarifying questions when the observation is ambiguous, and appends it only after explicit operator confirmation. - **`/qa-explore`** β€” **agent-led**, bounded per-surface exploration (Plan β†’ Capture β†’ Triage), HITL-gated at every phase transition. The agent plans an explicit static-vs-drive method choice, drives the surface (browser MCP or static) under a strictly read-only capture invariant, then triages the ledger into routed, classified, dedup'd follow-up dispositions. Both record observations as `QaLedgerItem`s (`.agents/schemas/qa-ledger.schema.json`) in a **persistent, resumable rolling session under `temp/qa/`** (`qa-session.js` owns session/ledger resolution), so items from either entry point flow through the identical machinery: dedup/classification/routing (`lib/findings/`), coverage verdicts (`coverage-verdict.js` / `coverage-report.js`), missing-test proposals (`propose-missing-test.js`), context hydration (`qa-context-hydrator.js`), and evidence redaction (`redact-evidence.js`). Procedure SSOT remains the workflow files: [`qa-assist.md`](../.agents/workflows/qa-assist.md) and [`qa-explore.md`](../.agents/workflows/qa-explore.md). ### What the Agent Should **Not** Assume - There is no monorepo tool (no Turborepo, no pnpm workspaces) β€” this is a single-package repository. - There is no web, mobile, database, or auth layer β€” this repo is a framework of protocols and scripts, not an application. - There is no TypeScript compilation step; do not add `tsc` invocations. - There is no bundler; scripts are executed directly with `node`.