diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ccdddd..54d48e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,47 @@ For each entry, **Upgrade impact** classifies the change against the [three-buck ## Unreleased +### A packaged project reads the framework's reference docs from the plugin + +([0019](docs/decisions/0019-reference-docs-in-the-plugin.md), amending [0016](docs/decisions/0016-packaged-install.md)) + +The four reference docs (`COST-MODEL.md`, `MCP-INTEGRATION.md`, `MEMORY-STRATEGY.md`, `SPEC-MODEL.md`) +are generic, never edited by a project, and already overwritten verbatim by every upgrade. The +`aplyca-adf` plugin now carries them in `docs/`, and a packaged project commits none of them, about 950 +fewer lines. + +- **Skills and agents** name the plugin's copy, `${CLAUDE_PLUGIN_ROOT}/docs/.md`, and say it's + outside the project. Without that line, an agent on Haiku read `docs/SPEC-MODEL.md` in the project + instead. +- **`deep-spec-analysis`** carries the spec model's text in its plugin copy, because Claude Code + doesn't fill in `${CLAUDE_PLUGIN_ROOT}` in a workflow script. +- **The read rule.** Claude Code asks before reading any file outside the project, the plugin's own + folder included. So a packaged project commits + `Read(~/.claude/plugins/cache/aplyca/aplyca-adf/**)` in `permissions.allow`. +- **The session context.** The plugin's SessionStart hook tells a packaged session where the docs + are. +- **Links on GitHub.** The project's files link the docs at the pinned release on GitHub. The new + `scripts/link-reference-docs.py` rewrites the links: `/aplyca-adf:adopt` runs it, and + `/aplyca-adf:upgrade` runs it to move them with each pin, and back on a switch to the committed + install. + +A new session-eval suite, `plugin-docs`, checks the reads in real sessions. The committed install is +unchanged. `TRACKER-INTEGRATION.md` and `PARALLEL-AGENTS.md` stay in the project, because they hold +its settings. + +**Upgrade impact:** + +- **Committed projects:** overwrite `.claude/hooks/session-context.sh` and + `.claude/workflows/deep-spec-analysis.js`. Both behave as before in a committed project: the + workflow's prompts name `docs/SPEC-MODEL.md` through one constant. +- **Packaged projects:** `/aplyca-adf:upgrade` does these steps after moving the pin. + 1. **Delete the four docs** from `docs/` where each is unchanged since your baseline. One your team + edited stays under a name of its own, or the team switches to the committed install. + 2. **Rewrite the links:** + `python3 /scripts/link-reference-docs.py . --packaged v`. + 3. **Add the read rule** to `permissions.allow` in `.claude/settings.json`. + 4. **Add the names note's last sentence** to `CLAUDE.md`, from `docs/SETUP.md` § Packaged install. + ## v1.1.0 — 2026-10-04 — The packaged install by default, checked in real sessions A minor release: a new default for new projects, and evals for the packaged install's paths. Nothing diff --git a/README.md b/README.md index 3cb4f55..8310f75 100644 --- a/README.md +++ b/README.md @@ -116,9 +116,9 @@ any code exists. Step by step: 5. **Add a module later:** `/upgrade` offers the modules you don't have yet, and so does running `/adopt` again in the adopted repository. -By default, adopted repositories use the **packaged** install: the skills, agents, workflows, and hook -scripts come from the `aplyca-adf` plugin, pinned to a release, and the repository commits only its -own layer — about 40 fewer files. A team that also uses other AI tools, or Claude Code's cloud +By default, adopted repositories use the **packaged** install: the skills, agents, workflows, hook +scripts, and the framework's reference docs come from the `aplyca-adf` plugin, pinned to a release, +and the repository commits only its own layer — about 50 fewer files. A team that also uses other AI tools, or Claude Code's cloud sessions, chooses the **committed** install: plain files every AI tool can read, with or without the plugin, which then only installs and maintains them. `/aplyca-adf:adopt` asks which one. ([Packaged install](docs/SETUP.md#packaged-install-claude-code-only) · [why](docs/decisions/0016-packaged-install.md) · [the default](docs/decisions/0018-packaged-by-default.md)) @@ -397,7 +397,8 @@ skeleton/ Portable project skeleton — what an adopting reposit modules/ Optional additions: github/, git-hooks/, clickup/, parallel-agents/ plugins/aplyca-adf/ The Claude Code plugin: /aplyca-adf:adopt, :upgrade, :cost-report — and, for - packaged projects, the skills, agents, workflows, and hooks (generated) + packaged projects, the skills, agents, workflows, hooks, and reference + docs (generated) docs/ Framework docs: SETUP, UPGRADING, ONBOARDING, references, examples, scenarios, decisions evals/ Static checks; hook, module, and plugin tests; triage routing evals and diff --git a/docs/ONBOARDING.md b/docs/ONBOARDING.md index a1dd0ac..23aaa61 100644 --- a/docs/ONBOARDING.md +++ b/docs/ONBOARDING.md @@ -145,7 +145,9 @@ One person, once: ### Day 1: Orientation and guardrails (2 hours) -Read, in your project: +Read, in your project — on the packaged install, the skills and the reference docs (`SPEC-MODEL.md`, +`COST-MODEL.md`, `MEMORY-STRATEGY.md`) come from the plugin: `AGENTS.md` links the docs at the pinned +release, and the originals are linked below: 1. `AGENTS.md` — the contract every AI tool reads: ground rules, how work flows, delivery rules, boundaries. ([original](../skeleton/AGENTS.md); 10 min) @@ -160,7 +162,8 @@ Read, in your project: [`implement`](../skeleton/.claude/skills/implement/SKILL.md) skills — above all their *Rationalizations* tables, the excuses agents (and people) make for skipping a step. (20 min) 6. `docs/COST-MODEL.md` and `docs/MEMORY-STRATEGY.md` — when to escalate a model, where a fact - belongs. `docs/TRACKER-INTEGRATION.md` if requirements arrive through a tracker. (15 min) + belongs (originals: [cost model](../skeleton/docs/COST-MODEL.md), + [memory strategy](../skeleton/docs/MEMORY-STRATEGY.md)). `docs/TRACKER-INTEGRATION.md` if requirements arrive through a tracker. (15 min) 7. The worked example: [examples/newsletter-signup/](examples/newsletter-signup/). (20 min) | Tool | Reads | Skills and agents | Rules | Hooks, permissions | diff --git a/docs/SETUP.md b/docs/SETUP.md index 3b5104c..b42d5eb 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -21,8 +21,9 @@ claude plugin install aplyca-adf@aplyca --scope project [the plugin's README § In the desktop app](../plugins/aplyca-adf/README.md#in-the-desktop-app). `/aplyca-adf:adopt` recommends the **packaged** install by default -([decision 0018](decisions/0018-packaged-by-default.md)): the skills, agents, workflows, and hook -scripts come from the pinned plugin, and the repository commits only its own layer — +([decision 0018](decisions/0018-packaged-by-default.md)): the skills, agents, workflows, hook +scripts, and the framework's reference docs come from the pinned plugin, and the repository commits +only its own layer — [§ Packaged install](#packaged-install-claude-code-only) lists what changes. The manual path below is the same procedure, step by step, for the **committed** install, which a team that also uses other AI tools, or Claude Code's cloud sessions, chooses instead. @@ -147,7 +148,8 @@ Each customizable doc starts with `` — `docs/SPEC-MODEL.md` and `specs/_templates/spec.md` ship with defaults for web projects. To change role names, mandatory sections, or conditional rules, update the template, `docs/SPEC-MODEL.md`, and the -enforcement step in `.claude/skills/write-spec/SKILL.md` together. +enforcement step in `.claude/skills/write-spec/SKILL.md` together. That takes the committed install: +in a packaged project, the spec model and `/aplyca-adf:write-spec` come from the plugin. ## 8. Stamp, commit, and open a draft pull request @@ -172,7 +174,8 @@ Open the pull request as a draft; merge after review like any other change. ## Packaged install (Claude Code only) [Decision 0016](decisions/0016-packaged-install.md). The framework's machinery doesn't enter the -repository: 20 skills, 8 agents, 4 workflows, and the hook scripts come from the `aplyca-adf` plugin, +repository: 20 skills, 8 agents, 4 workflows, the hook scripts, and the framework's reference docs +([decision 0019](decisions/0019-reference-docs-in-the-plugin.md)) come from the `aplyca-adf` plugin, pinned to a release tag. The repository commits its own layer as above, and every module's files. Choose it when the team works in Claude Code only. Cursor, Copilot, and Gemini users would get `AGENTS.md` and the rules but no skills, and Claude Code's cloud sessions don't load the plugin. @@ -184,10 +187,15 @@ v1.0.0 or later; its entry in [`CHANGELOG.md`](../CHANGELOG.md) says what it bri **What changes from the steps above:** 1. **Copy less** (step 1). Leave out `.claude/skills/`, `.claude/agents/`, `.claude/workflows/`, the - scripts and helpers in `.claude/hooks/` (keep `config.sh`), `.claude/hooks/README.md`, `GEMINI.md`, `.agents/`, - and `.cursor/`. Modules copy as usual: `/dispatch` is the one skill a packaged repository commits. + scripts and helpers in `.claude/hooks/` (keep `config.sh`), `.claude/hooks/README.md`, `GEMINI.md`, + `.agents/`, `.cursor/`, and the four reference docs: `docs/COST-MODEL.md`, + `docs/MCP-INTEGRATION.md`, `docs/MEMORY-STRATEGY.md`, and `docs/SPEC-MODEL.md`. Modules copy as usual: `/dispatch` is the one + skill a packaged repository commits. Then point the files that name the reference docs at the + release you pin, from the framework copy you took the skeleton from: + `python3 /scripts/link-reference-docs.py . --packaged v`. 2. **Wire the plugin, not the hooks** (step 5). Drop the `hooks` block from `.claude/settings.json`, - since the plugin wires the same hooks, and pin the marketplace to the release: + since the plugin wires the same hooks, pin the marketplace to the release, and add the read rule to + `permissions.allow`: ```json { @@ -196,18 +204,23 @@ v1.0.0 or later; its entry in [`CHANGELOG.md`](../CHANGELOG.md) says what it bri "source": { "source": "github", "repo": "aplyca/AgenticDevelopmentFramework", "ref": "v" } } }, - "enabledPlugins": { "aplyca-adf@aplyca": true } + "enabledPlugins": { "aplyca-adf@aplyca": true }, + "permissions": { "allow": ["Read(~/.claude/plugins/cache/aplyca/aplyca-adf/**)"] } } ``` The hooks read `.claude/hooks/config.sh` from the project, so step 5's settings apply unchanged. + The read rule lets the plugin's skills and agents open its reference docs: Claude Code asks before + reading any file outside the project, the plugin's own folder included, unless a rule allows it. 3. **Tell people the names** (step 3). Everything a plugin carries goes by the plugin's name. Add this to the start of `CLAUDE.md` § Skills, agents, and workflows: > **This project uses the packaged install.** Skills, agents, and workflows come from the > `aplyca-adf` plugin, pinned in `.claude/settings.json`. Where these files name a skill or > workflow — `/triage`, `/deep-review` — type `/aplyca-adf:triage`, `/aplyca-adf:deep-review`. - > Where they name an agent — `@code-reviewer` — its name is `aplyca-adf:code-reviewer`. + > Where they name an agent — `@code-reviewer` — its name is `aplyca-adf:code-reviewer`. The + > framework's reference docs — the spec model, the cost model, the memory strategy, MCP + > integration — come from the plugin too; these files link them at the pinned release. People read the key commands in `docs/getting-started/DEV-SETUP.md` § AI-assisted development, so write them there by their full names: `/aplyca-adf:triage`, `@aplyca-adf:code-reviewer`. @@ -220,12 +233,14 @@ project and the plugin named: `CLAUDE_PROJECT_DIR="$PWD" CLAUDE_PLUGIN_ROOT= --scope project claude plugin install aplyca-adf@aplyca --scope project +claude -p "" --allowedTools "Read(~/.claude/plugins/cache/aplyca/aplyca-adf/**)" ``` ## Verify diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md index dfd64d0..8325d7d 100644 --- a/docs/UPGRADING.md +++ b/docs/UPGRADING.md @@ -2,7 +2,7 @@ How to pull newer framework changes into a target project that adopted an earlier version of the skeleton — without losing your team's customizations. -In the packaged install — the default for new projects ([decision 0018](decisions/0018-packaged-by-default.md)) — the skills, agents, workflows, and hook scripts come from the pinned plugin; in the committed install, everything is committed into the project, with no runtime dependency. Either way, a project moves from one release to the next on purpose: the committed files file by file, informed by the three-bucket taxonomy below, and the plugin by moving its pin. +In the packaged install — the default for new projects ([decision 0018](decisions/0018-packaged-by-default.md)) — the skills, agents, workflows, hook scripts, and the framework's reference docs come from the pinned plugin; in the committed install, everything is committed into the project, with no runtime dependency. Either way, a project moves from one release to the next on purpose: the committed files file by file, informed by the three-bucket taxonomy below, and the plugin by moving its pin. ## When to upgrade @@ -63,10 +63,10 @@ Every file the skeleton introduces falls into one of three buckets. Your upgrade | `.claude/rules/testing.md` | Universal — testing discipline | | `.claude/rules/security.md` | Universal — OWASP-style baseline | | `.claude/rules/git-workflow.md` | Universal — commit prefixes, branch discipline | -| `docs/SPEC-MODEL.md` | Framework reference doc | -| `docs/COST-MODEL.md` | Framework reference doc | -| `docs/MEMORY-STRATEGY.md` | Framework reference doc | -| `docs/MCP-INTEGRATION.md` | Framework reference doc | +| `docs/SPEC-MODEL.md` | Framework reference doc — a packaged project has none: the plugin carries it | +| `docs/COST-MODEL.md` | Framework reference doc — a packaged project has none: the plugin carries it | +| `docs/MEMORY-STRATEGY.md` | Framework reference doc — a packaged project has none: the plugin carries it | +| `docs/MCP-INTEGRATION.md` | Framework reference doc — a packaged project has none: the plugin carries it | | `docs/process/0000-pdr-template.md` | The PDR template | | `.cursor/rules/*.mdc` | Cursor mirrors of the rules | | `specs/_templates/*` | The spec-folder templates (`spec.md`, `plan.md`, `tasks.md`) — merge instead if your team customized them. Your filled-in specs are project-owned | @@ -145,7 +145,7 @@ cp -R $FW/.claude/agents/* .claude/agents/ mkdir -p .claude/workflows && cp $FW/.claude/workflows/*.js .claude/workflows/ cp $FW/.claude/hooks/*.sh $FW/.claude/hooks/*.jq $FW/.claude/hooks/*.py $FW/.claude/hooks/README.md .claude/hooks/ # not config.sh — that one merges cp $FW/.claude/rules/{code-quality,testing,security,git-workflow}.md .claude/rules/ -cp $FW/docs/{SPEC-MODEL,COST-MODEL,MEMORY-STRATEGY,MCP-INTEGRATION}.md docs/ +cp $FW/docs/{SPEC-MODEL,COST-MODEL,MEMORY-STRATEGY,MCP-INTEGRATION}.md docs/ # committed install only ``` Inspect the diff for surprise (removed files, renamed files). Adjust if the framework has restructured anything. @@ -260,11 +260,14 @@ old plugin from each machine that installed it: ### "We use the packaged install" — or want to A packaged project ([decision 0016](decisions/0016-packaged-install.md)) doesn't commit the skills, -agents, workflows, or hook scripts: they come from the `aplyca-adf` plugin, pinned to a release tag -in `.claude/settings.json`. Upgrading it means two things: +agents, workflows, hook scripts, or the framework's reference docs +([decision 0019](decisions/0019-reference-docs-in-the-plugin.md)): they come from the `aplyca-adf` +plugin, pinned to a release tag in `.claude/settings.json`. Upgrading it means two things: - **Bump the pin:** the marketplace's `"ref"` moves to the new release tag, `vX.Y.Z`. That one line upgrades - every skill, agent, workflow, and hook. Every project moves from release to release, committed ones + every skill, agent, workflow, hook, and reference doc. The project's links to the reference docs + name the release too: `python3 /scripts/link-reference-docs.py . --packaged vX.Y.Z`, + from the new release, moves them. Every project moves from release to release, committed ones too: their pin keeps the plugin's copies at the same release as their committed files. - **Merge the committed layer** as in the procedure above — `AGENTS.md`, `CLAUDE.md`, the settings (never adding a `hooks` block), `config.sh`, the rules, the docs, and the modules — and skip every diff --git a/docs/decisions/0016-packaged-install.md b/docs/decisions/0016-packaged-install.md index 2ac73de..a03229d 100644 --- a/docs/decisions/0016-packaged-install.md +++ b/docs/decisions/0016-packaged-install.md @@ -1,6 +1,6 @@ # 0016: A packaged install — the framework's machinery from its pinned plugin, `aplyca-adf` (opt-in, Claude Code only) -- **Status:** accepted; amended by [0018](0018-packaged-by-default.md) (the packaged install is the default) +- **Status:** accepted; amended by [0018](0018-packaged-by-default.md) (the packaged install is the default) and [0019](0019-reference-docs-in-the-plugin.md) (the reference docs come from the plugin) - **Date:** 2026-10-02 - **Amends:** [0009](0009-optional-modules.md) — what ships as committed files diff --git a/docs/decisions/0019-reference-docs-in-the-plugin.md b/docs/decisions/0019-reference-docs-in-the-plugin.md new file mode 100644 index 0000000..812dab2 --- /dev/null +++ b/docs/decisions/0019-reference-docs-in-the-plugin.md @@ -0,0 +1,98 @@ +# 0019: A packaged project reads the framework's reference docs from the plugin + +- **Status:** accepted +- **Date:** 2026-10-05 +- **Amends:** [0016](0016-packaged-install.md) — what a packaged project commits + +## Context + +Decision 0016 split the skeleton into the project's own files and generic machinery, and counted +every doc as the project's own. Four of them aren't: `COST-MODEL.md`, `MEMORY-STRATEGY.md`, +`SPEC-MODEL.md`, and `MCP-INTEGRATION.md`, about 950 lines. They have no `` +markers, and `/upgrade` already overwrites them verbatim, as "framework reference docs" in its +safe-to-overwrite bucket. A team adopting the packaged install asked why a project copies them at +all. Two other docs, `TRACKER-INTEGRATION.md` and the parallel-agents module's `PARALLEL-AGENTS.md`, +hold the project's settings: which tracker it uses, with its tools and statuses, and what its +worktrees share. They stay in the project. + +These readers use the four docs: + +- **Skills and agents.** The spec-writer and spec-analyzer agents read the spec model. `/handoff`, + `/orchestrate`, `/spec-workflow`, and `/cost-report` cite the cost model. +- **The deep-spec-analysis workflow.** Each of its lenses reads the spec model. +- **Claude in a session.** `AGENTS.md`'s "when to read what" table names the docs. +- **People.** `AGENTS.md`, `CLAUDE.md`, `README.md`, `CONTRIBUTING.md`, `specs/README.md`, and the + spec template link them. + +Sessions run headless on 2026-10-05 (Claude Code 2.1.286, Haiku, default permission mode, no user +settings) showed four things about a plugin's own files: + +1. **`${CLAUDE_PLUGIN_ROOT}` is filled in for skills and agents.** A plugin skill and a plugin agent + each opened the path it named in the plugin. +2. **It isn't filled in for workflows.** A workflow's agent got the literal `${CLAUDE_PLUGIN_ROOT}` + and found the path only by loading one of the plugin's skills. A template literal that named it + threw, because the variable is undefined. +3. **Reading any file outside the project asks first.** This held for the plugin's `docs/` folder, + for a skill's own folder, and for the installed copy under `~/.claude/plugins/cache/`. Headless, + every one of these reads was denied. +4. **A read rule removes the prompt.** With `Read(~/.claude/plugins/cache/aplyca/aplyca-adf/**)` + allowed, the session and a plugin agent both read the installed copy without asking. +5. **A model can take the plugin's path for a project path.** The spec-analyzer agent, on Haiku, was + told to read four files, three of them in the project. It read `docs/SPEC-MODEL.md` in the project + instead of the plugin's copy, although its instructions gave the full path. One line at the top of + its instructions fixed it: the reference docs it names are the plugin's copies, outside the + project. + +## Decision + +- **The plugin carries the four reference docs** in its `docs/` folder. `scripts/build-aplyca-adf.sh` + generates them from `skeleton/docs/`. A packaged project commits none of them. +- **Each component reaches the docs its own way:** + - **Skills and agents** name the plugin's copy, `${CLAUDE_PLUGIN_ROOT}/docs/.md`, and open + with a line that says those copies are outside the project. + - **The deep-spec-analysis workflow** carries the spec model's text, because Claude Code doesn't + fill in the path in a workflow script. + - **A session** gets one line from the plugin's SessionStart hook that names the plugin's docs + folder. +- **A packaged project commits the read rule**, `Read(~/.claude/plugins/cache/aplyca/aplyca-adf/**)`, + in `permissions.allow`, so the docs open without asking. +- **The project's files link the docs on GitHub, at the release the project pins.** People read + them there. `scripts/link-reference-docs.py` rewrites the links: + - `/aplyca-adf:adopt` runs it at adoption. + - `/aplyca-adf:upgrade` runs it to move the links with the pin, and runs it in reverse on a switch + to the committed install. +- **The committed install is unchanged.** It keeps the four docs in `docs/`. +- **Customizing the spec model takes the committed install.** It already meant editing + `/write-spec`, which a packaged project can't do. + +## Consequences + +- **Positive:** + - A packaged project commits four fewer files, about 950 lines, and its upgrades skip them. + - Each release has one copy of each doc, which moves with the pin. +- **Negative / cost:** + - **A permission rule in every packaged project's settings.** It names Claude Code's cache layout, + `cache////`, which the plugin loading reference documents. When + `CLAUDE_CODE_PLUGIN_CACHE_DIR` moves the cache, or a session loads the plugin with + `--plugin-dir`, the rule misses, and Claude asks before reading. + - **Headless runs need the rule passed in.** A headless run (`claude -p`, CI) never applies the + project's allow rules, so it passes the rule with `--allowedTools`. + - **People read the docs on GitHub,** not in their repository. + - **The workflow's prompts grow.** The plugin's copy of deep-spec-analysis carries about 10 KB of + spec model. Each lens gets it in its prompt, the same text each lens read before. + - **One more upgrade step.** A packaged project's upgrades run the link script. + +## Alternatives considered + +- **Keep the four docs in packaged projects.** Declined: they're generic, never edited, and + overwritten by every upgrade, the same cost 0016 removed for the machinery. +- **Put each doc's text into every skill and agent that names it**, so nothing is read at runtime. + Declined for skills and agents: the spec model would be copied into several files, and the three + docs that are only cited would stay reachable only through the web, which asks too. Kept for the + workflow, the one place the path can't reach. +- **Ship the docs as a skill's supporting files.** Declined: reading a skill's own folder asks too + (finding 3). +- **Link the docs on GitHub only,** for Claude as well as people. Declined: Claude would fetch each + doc through the web, which also asks first and returns a summary of the page. +- **Move `TRACKER-INTEGRATION.md` and `PARALLEL-AGENTS.md` too.** Declined: they hold the project's + settings. Moving the generic half of `TRACKER-INTEGRATION.md` into the plugin is a separate change. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 61f4346..0364302 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -26,9 +26,10 @@ projects. | [0013](0013-adapt-practices-not-a-second-workflow.md) | Adapt practices from other skill collections into our skills — never a second workflow | accepted | | [0014](0014-test-first-in-every-lane.md) | Test first in every lane | accepted | | [0015](0015-tool-worktrees-are-workers.md) | Worktrees that Claude Code creates are workers too (parallel-agents module) | accepted | -| [0016](0016-packaged-install.md) | A packaged install — the framework's machinery from its pinned plugin, `aplyca-adf` (opt-in, Claude Code only) | accepted; amended by 0018 | +| [0016](0016-packaged-install.md) | A packaged install — the framework's machinery from its pinned plugin, `aplyca-adf` (opt-in, Claude Code only) | accepted; amended by 0018, 0019 | | [0017](0017-semantic-versioning.md) | Releases follow semantic versioning | accepted | | [0018](0018-packaged-by-default.md) | The packaged install is the default | accepted | +| [0019](0019-reference-docs-in-the-plugin.md) | A packaged project reads the framework's reference docs from the plugin | accepted | Changes that follow from these records are listed, with their upgrade impact, in [`CHANGELOG.md`](../../CHANGELOG.md). diff --git a/evals/dynamic/README.md b/evals/dynamic/README.md index 6362a41..de6788b 100644 --- a/evals/dynamic/README.md +++ b/evals/dynamic/README.md @@ -31,6 +31,10 @@ dynamic/ inspect.sh - checks that the case's hook fired (or stood down): ✓ or ✘ per check stand-down.setup.sh - switches that case's copy to the committed install ... - each of the aplyca-adf plugin's hooks, in a real session + plugin-docs/ + project.sh - builds a packaged project without the reference docs, linked at the release + inspect.sh - checks which reads reached the plugin's docs, asked first, or went to the web + ... - an agent and a session reading the plugin's reference docs; the control without the read rule write-tests/ ... write-docs/ @@ -45,7 +49,7 @@ Each fixture has two files: ## Running -### Session evals — automated (triage, debug, adopt, upgrade, plugin-hooks) +### Session evals — automated (triage, debug, adopt, upgrade, plugin-hooks, plugin-docs) `run-session-evals.sh` runs every fixture in one suite — `fixtures/triage/` by default, or `--suite debug` — against real Claude Code sessions: it builds a fictional project in a temp @@ -63,6 +67,7 @@ signal without installing anything. ./run-session-evals.sh --suite debug # the /debug cases ./run-session-evals.sh --suite adopt --source "$PWD/../.." # adoption, against this checkout ./run-session-evals.sh --suite plugin-hooks # the plugin's hooks, on Haiku +./run-session-evals.sh --suite plugin-docs # the plugin's reference docs, on Haiku ./run-session-evals.sh --suite upgrade --models sonnet # /upgrade, switching to the packaged install ``` @@ -84,6 +89,17 @@ it ✓ or ✘; the summary counts them. A PostToolUse hook's message reaches Cla transcript, not the output stream, so that check reads the transcript. Seven sessions on Haiku cost about $0.40; run it after any change to the hooks. Report: [`reports/2026-10-04-plugin-hooks.md`](reports/2026-10-04-plugin-hooks.md). +The **plugin-docs** suite checks that a packaged project reads the framework's reference docs from the +plugin ([decision 0019](../../docs/decisions/0019-reference-docs-in-the-plugin.md)). Claude Code asks +before reading any file outside the project, so its sessions run as a teammate's would: default +permission mode, no extra directory, no blanket `Read`, and only the read rule a packaged project +commits, passed with `--allowedTools` for the plugin's path in this checkout. `agent-spec-model` has +the spec-analyzer agent open the plugin's spec model. `session-docs` has Claude find a doc through the +session context. The control, `without-rule`, runs without the rule, where the same read is denied. +Agents that ask for `opus` run on Haiku (`ANTHROPIC_DEFAULT_OPUS_MODEL`). Three sessions cost about +$0.60; run it after any change to how the plugin's skills, agents, or hooks name the reference docs. +Report: [`reports/2026-10-05-plugin-docs.md`](reports/2026-10-05-plugin-docs.md). + The **adopt** suite builds its own project (`fixtures/adopt/project.sh`: a new repository with no commits) and appends each run's end state (`inspect.sh`) to the transcript. `{{FRAMEWORK}}` in a prompt becomes `--source`: the GitHub address by default, which tests what's published on `main`, or diff --git a/evals/dynamic/check-packaged.sh b/evals/dynamic/check-packaged.sh index bd6f028..db17657 100755 --- a/evals/dynamic/check-packaged.sh +++ b/evals/dynamic/check-packaged.sh @@ -2,11 +2,13 @@ # # Checks that a project is laid out for the packaged install (decision 0016, docs/SETUP.md § Packaged # install), pinned to : no framework machinery committed, the plugin pinned and turned on, no -# hooks block, the stamp, and the names people type. Prints ✓ or ✘ per check. Read-only. -# Usage: check-packaged.sh +# hooks block, the stamp, and the names people type — and, on a release whose plugin carries the +# reference docs (decision 0019), none of them in docs/, links to them at the release, and the rule that +# lets Claude read them. Prints ✓ or ✘ per check. Read-only. +# Usage: check-packaged.sh [] # cd "$1" || exit 1 -release="$2" commit="$3" +release="$2" commit="$3" fw="${4:-}" check() { if eval "$2"; then echo "- ✓ $1"; else echo "- ✘ $1"; fi; } check "the stamp names $release, its commit $commit, and \`install: packaged\`" \ "head -1 CLAUDE.md | grep -q -F 'Skeleton source: $release · $commit' && head -1 CLAUDE.md | grep -q -F 'install: packaged'" @@ -23,4 +25,12 @@ sys.exit(0 if ok else 1) PY" check "\`CLAUDE.md\` says the project uses the packaged install" "grep -q 'uses the packaged install' CLAUDE.md" check "\`DEV-SETUP.md\` gives the commands by their full names" "grep -q '/aplyca-adf:triage' docs/getting-started/DEV-SETUP.md" +if [ -n "$fw" ] && git -C "$fw" cat-file -e "$release:plugins/aplyca-adf/docs" 2>/dev/null; then + names='COST-MODEL|MCP-INTEGRATION|MEMORY-STRATEGY|SPEC-MODEL' + check "no reference docs in \`docs/\` — the plugin carries them" \ + "! ls docs 2>/dev/null | grep -qE '^($names)\.md\$'" + check "the project's files link the reference docs at $release, none locally" \ + "grep -q 'blob/$release/skeleton/docs/SPEC-MODEL.md' AGENTS.md && ! grep -qE '(^|[^/.A-Za-z0-9_-])docs/($names)\.md' AGENTS.md CLAUDE.md CONTRIBUTING.md README.md specs/README.md specs/_templates/spec.md 2>/dev/null" + check "the settings let Claude read the plugin's folder" "python3 -c 'import json; s = json.load(open(\".claude/settings.json\")); exit(0 if \"Read(~/.claude/plugins/cache/aplyca/aplyca-adf/**)\" in s.get(\"permissions\", {}).get(\"allow\", []) else 1)'" +fi check "everything is committed" "[ -z \"\$(git status --short)\" ]" diff --git a/evals/dynamic/fixtures/adopt/inspect.sh b/evals/dynamic/fixtures/adopt/inspect.sh index fbf3202..a1b4c46 100755 --- a/evals/dynamic/fixtures/adopt/inspect.sh +++ b/evals/dynamic/fixtures/adopt/inspect.sh @@ -55,7 +55,7 @@ echo '```' if [ "${3:-}" = packaged ]; then echo "### Checks — the packaged install" release="$(git -C "$FW" tag --list 'v*' --sort=-v:refname | head -1)" - bash "$(dirname "$0")/../../check-packaged.sh" . "$release" "$(git -C "$FW" rev-parse --short "$release^{commit}")" + bash "$(dirname "$0")/../../check-packaged.sh" . "$release" "$(git -C "$FW" rev-parse --short "$release^{commit}")" "$FW" if grep -q -i 'packaged' docs/process/0001-*.md 2>/dev/null; then echo "- ✓ PDR-0001 records the packaged install" else echo "- ✘ PDR-0001 records the packaged install"; fi fi diff --git a/evals/dynamic/fixtures/plugin-docs/agent-spec-model.expected.md b/evals/dynamic/fixtures/plugin-docs/agent-spec-model.expected.md new file mode 100644 index 0000000..a38eaf9 --- /dev/null +++ b/evals/dynamic/fixtures/plugin-docs/agent-spec-model.expected.md @@ -0,0 +1,7 @@ +# Expected — plugin docs: an agent reads the spec model from the plugin + +The session should satisfy ALL of these invariants. `inspect.sh` checks the first two automatically. + +- [ ] The agent reads `SPEC-MODEL.md` from the plugin's `docs/` folder, by its full path +- [ ] No read of the plugin's folder asks for permission (none is denied in the headless run) +- [ ] The agent doesn't look for `docs/SPEC-MODEL.md` in the project, or on GitHub diff --git a/evals/dynamic/fixtures/plugin-docs/agent-spec-model.input.md b/evals/dynamic/fixtures/plugin-docs/agent-spec-model.input.md new file mode 100644 index 0000000..6a248f5 --- /dev/null +++ b/evals/dynamic/fixtures/plugin-docs/agent-spec-model.input.md @@ -0,0 +1,21 @@ +# Input — plugin docs: an agent reads the spec model from the plugin + + + +This fixture verifies, in a real session, that a plugin agent opens the plugin's copy of a reference doc (decision 0019): Claude Code fills in `${CLAUDE_PLUGIN_ROOT}` in the agent's instructions, and the read rule a packaged project commits lets it read there without asking. + +## Repository context to give the AI + +A project on the packaged install with no reference docs in `docs/`; its files link them at the pinned release on GitHub. The `aplyca-adf` plugin is loaded from this checkout for the session only (`--plugin-dir`), in default permission mode, with the read rule for the plugin's folder. The branch is `feat/newsletter-signup`, with `specs/007-newsletter-signup/` (the delivered example). The agent, which asks for `opus`, runs on Haiku. + +## Prompt to give the AI + +``` +Use the aplyca-adf:spec-analyzer agent to analyze specs/007-newsletter-signup/. When it returns, reply with one line only: did the agent open the spec model, yes or no. +``` + +## What to do with this fixture + +1. Run it with `run-session-evals.sh --suite plugin-docs --cases agent-spec-model` (Haiku by default). +2. `inspect.sh` checks the run and prints ✓ or ✘, then every read the session made. +3. Compare against `agent-spec-model.expected.md`. diff --git a/evals/dynamic/fixtures/plugin-docs/inspect.sh b/evals/dynamic/fixtures/plugin-docs/inspect.sh new file mode 100755 index 0000000..0a1a842 --- /dev/null +++ b/evals/dynamic/fixtures/plugin-docs/inspect.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# +# Checks a plugin-docs run from the session's output, which carries the subagents' tool calls too: +# whether Claude read the plugin's copy of a reference doc, whether anything asked for permission, and +# whether it fell back to the web. Prints ✓ or ✘ per check, then the reads it made. Read-only. +# Usage: inspect.sh +# +work="$1" out="$2" case_name="$3" +python3 - "$out" "$case_name" <<'PY' +import json, sys +out, case = sys.argv[1], sys.argv[2] +reads, results, denials, fetched, context = {}, {}, [], False, False +for line in open(out, encoding="utf-8", errors="replace"): + try: + event = json.loads(line) + except ValueError: + continue + if "The framework's reference docs — SPEC-MODEL.md" in line: + context = True + if not isinstance(event, dict): + continue + if event.get("type") == "result": + denials += event.get("permission_denials") or [] + message = event.get("message") + content = message.get("content") if isinstance(message, dict) else None + for block in content if isinstance(content, list) else []: + if block.get("type") == "tool_use" and block.get("name") == "Read": + reads[block["id"]] = block.get("input", {}).get("file_path", "") + elif block.get("type") == "tool_use" and block.get("name") == "WebFetch": + fetched = True + elif block.get("type") == "tool_result": + text = block.get("content") + results[block.get("tool_use_id")] = (bool(block.get("is_error")), text if isinstance(text, str) else json.dumps(text)) + +def read_ok(doc, heading): + return any(path.endswith("/plugins/aplyca-adf/docs/" + doc) and not results.get(i, (True, ""))[0] and heading in results[i][1] + for i, path in reads.items()) + +plugin_denials = [d for d in denials if "/plugins/aplyca-adf/" in json.dumps(d.get("tool_input", {}))] +def check(label, ok): + print(f"- {'✓' if ok else '✘'} {label}") + +print("### Checks") +if case == "agent-spec-model": + check("the spec-analyzer agent read the plugin's SPEC-MODEL.md", read_ok("SPEC-MODEL.md", "# Spec model")) + check("nothing asked to read the plugin's folder", not plugin_denials) +elif case == "session-docs": + check("the session context named the plugin's docs folder", context) + check("Claude read the plugin's MEMORY-STRATEGY.md", read_ok("MEMORY-STRATEGY.md", "# Memory strategy")) + check("nothing asked to read the plugin's folder", not plugin_denials) + check("no web fetch", not fetched) +elif case == "without-rule": + check("without the rule, reading the plugin's folder asks first (denied headless)", bool(plugin_denials)) +print("### Reads") +print("```") +for i, path in reads.items(): + error, text = results.get(i, (None, "")) + print(f"{'denied/error' if error else 'ok':12} {path}") +print("```") +PY diff --git a/evals/dynamic/fixtures/plugin-docs/project.sh b/evals/dynamic/fixtures/plugin-docs/project.sh new file mode 100755 index 0000000..06a915a --- /dev/null +++ b/evals/dynamic/fixtures/plugin-docs/project.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# +# Builds a project on the packaged install as /adopt leaves it on a release whose plugin carries the +# reference docs (decision 0019): the skeleton without the machinery or the four reference docs, its +# links pointing at the release on GitHub, the names note in CLAUDE.md, and install: packaged in the +# stamp — so the aplyca-adf plugin, loaded per session with --plugin-dir, acts. On a work branch with +# the delivered newsletter-signup spec folder from docs/examples/. +# +cd "$1" || exit 1 +FW="$2" +git init -q -b main && git config user.email dev@example.com && git config user.name dev +cp -R "$FW/skeleton/." . +rm -rf .claude/skills .claude/agents .claude/workflows .cursor GEMINI.md +find .claude/hooks -type f ! -name config.sh -delete +rm -f docs/COST-MODEL.md docs/MCP-INTEGRATION.md docs/MEMORY-STRATEGY.md docs/SPEC-MODEL.md +python3 "$FW/scripts/link-reference-docs.py" . --packaged v1.1.0 >/dev/null +mkdir -p specs/007-newsletter-signup && cp "$FW"/docs/examples/newsletter-signup/{spec,plan,tasks}.md specs/007-newsletter-signup/ +python3 - <<'PY' +import json +from pathlib import Path +claude = Path("CLAUDE.md").read_text().split("\n", 1)[1] +note = ("**This project uses the packaged install.** Skills, agents, and workflows come from the `aplyca-adf` plugin, " + "pinned in `.claude/settings.json`. Where these files name a skill or workflow — `/triage`, `/deep-review` — type " + "`/aplyca-adf:triage`, `/aplyca-adf:deep-review`. Where they name an agent — `@code-reviewer` — its name is " + "`aplyca-adf:code-reviewer`. The framework's reference docs — the spec model, the cost model, the memory strategy, " + "MCP integration — come from the plugin too; these files link them at the pinned release.\n\n") +claude = claude.replace("# [PROJECT NAME] — Claude Code", "# Newsletter Site — Claude Code\n\n" + note, 1) +Path("CLAUDE.md").write_text("\n" + claude) +settings = json.loads(Path(".claude/settings.json").read_text()) +settings.pop("hooks", None) +settings["permissions"]["allow"].append("Read(~/.claude/plugins/cache/aplyca/aplyca-adf/**)") +Path(".claude/settings.json").write_text(json.dumps(settings, indent=2) + "\n") +PY +git add -A && git commit -q -m "chore: adopt the Agentic Development Framework (packaged)" +git switch -q -c feat/newsletter-signup diff --git a/evals/dynamic/fixtures/plugin-docs/session-docs.expected.md b/evals/dynamic/fixtures/plugin-docs/session-docs.expected.md new file mode 100644 index 0000000..dcd0dde --- /dev/null +++ b/evals/dynamic/fixtures/plugin-docs/session-docs.expected.md @@ -0,0 +1,8 @@ +# Expected — plugin docs: the session finds a reference doc in the plugin + +The session should satisfy ALL of these invariants. `inspect.sh` checks them automatically. + +- [ ] The session context names the plugin's `docs/` folder +- [ ] Claude reads `MEMORY-STRATEGY.md` from that folder and answers with its first heading, `# Memory strategy` +- [ ] No read of the plugin's folder asks for permission +- [ ] Claude doesn't fetch the GitHub link diff --git a/evals/dynamic/fixtures/plugin-docs/session-docs.input.md b/evals/dynamic/fixtures/plugin-docs/session-docs.input.md new file mode 100644 index 0000000..fe0b70e --- /dev/null +++ b/evals/dynamic/fixtures/plugin-docs/session-docs.input.md @@ -0,0 +1,21 @@ +# Input — plugin docs: the session finds a reference doc in the plugin + + + +This fixture verifies, in a real session, that Claude itself — not a skill or an agent — finds a reference doc in the plugin: the plugin's SessionStart hook names the folder, and the read rule lets Claude read it, instead of fetching the GitHub link the project's files carry. + +## Repository context to give the AI + +A project on the packaged install with no reference docs in `docs/`; its files link them at the pinned release on GitHub. The `aplyca-adf` plugin is loaded from this checkout for the session only (`--plugin-dir`), in default permission mode, with the read rule for the plugin's folder. + +## Prompt to give the AI + +``` +Which of the framework's documents explains where a piece of project knowledge belongs — AGENTS.md, memory, a spec folder, or a decision record? Open that document and reply with its first heading only. +``` + +## What to do with this fixture + +1. Run it with `run-session-evals.sh --suite plugin-docs --cases session-docs` (Haiku by default). +2. `inspect.sh` checks the run and prints ✓ or ✘, then every read the session made. +3. Compare against `session-docs.expected.md`. diff --git a/evals/dynamic/fixtures/plugin-docs/without-rule.expected.md b/evals/dynamic/fixtures/plugin-docs/without-rule.expected.md new file mode 100644 index 0000000..4e2f250 --- /dev/null +++ b/evals/dynamic/fixtures/plugin-docs/without-rule.expected.md @@ -0,0 +1,6 @@ +# Expected — plugin docs: without the read rule + +`inspect.sh` checks the first automatically. + +- [ ] Claude tries to read the plugin's `docs/` folder and is denied — the prompt a teammate would see +- [ ] Claude says it couldn't open the document rather than inventing its heading diff --git a/evals/dynamic/fixtures/plugin-docs/without-rule.input.md b/evals/dynamic/fixtures/plugin-docs/without-rule.input.md new file mode 100644 index 0000000..2b0e4f8 --- /dev/null +++ b/evals/dynamic/fixtures/plugin-docs/without-rule.input.md @@ -0,0 +1,22 @@ +# Input — plugin docs: without the read rule + + + + +The control for `session-docs`: the same session without the read rule a packaged project commits. Claude Code asks before reading any file outside the project, the plugin's own folder included — which a headless run records as a denial. It shows the rule is what lets the plugin's reference docs open without asking. + +## Repository context to give the AI + +As in `session-docs`, without the read rule. + +## Prompt to give the AI + +``` +Which of the framework's documents explains where a piece of project knowledge belongs — AGENTS.md, memory, a spec folder, or a decision record? Open that document and reply with its first heading only. +``` + +## What to do with this fixture + +1. Run it with `run-session-evals.sh --suite plugin-docs --cases without-rule` (Haiku by default). +2. `inspect.sh` checks that a read of the plugin's folder was denied. +3. Compare against `without-rule.expected.md`. diff --git a/evals/dynamic/fixtures/upgrade/inspect.sh b/evals/dynamic/fixtures/upgrade/inspect.sh index 8fa7642..ef6934d 100755 --- a/evals/dynamic/fixtures/upgrade/inspect.sh +++ b/evals/dynamic/fixtures/upgrade/inspect.sh @@ -20,7 +20,7 @@ echo '```' if [ "${3:-}" = switch-to-packaged ]; then echo "### Checks — the switch to the packaged install" release="$(git -C "$FW" tag --list 'v*' --sort=-v:refname | head -1)" - bash "$(dirname "$0")/../../check-packaged.sh" . "$release" "$(git -C "$FW" rev-parse --short "$release^{commit}")" + bash "$(dirname "$0")/../../check-packaged.sh" . "$release" "$(git -C "$FW" rev-parse --short "$release^{commit}")" "$FW" check() { if eval "$2"; then echo "- ✓ $1"; else echo "- ✘ $1"; fi; } check "a new PDR records the switch to the packaged install" \ "ls docs/process | grep -v -E '^(0000|0001)-|README' | grep -q . && grep -l -i 'packaged' \$(ls docs/process/*.md | grep -v -E '/(0000|0001)-|README') >/dev/null" diff --git a/evals/dynamic/reports/2026-10-05-plugin-docs.md b/evals/dynamic/reports/2026-10-05-plugin-docs.md new file mode 100644 index 0000000..8fd964e --- /dev/null +++ b/evals/dynamic/reports/2026-10-05-plugin-docs.md @@ -0,0 +1,69 @@ +# Session evals — the reference docs in the plugin — 2026-10-05 + +Decision 0019 moves the framework's four reference docs out of packaged projects and into the +`aplyca-adf` plugin. Whether a session can use them there depends on Claude Code, not on the +framework's files. So the design followed headless sessions (Claude Code v2.1.286, Haiku 4.5, default +permission mode, no user settings), run first as experiments and then as the new `plugin-docs` suite. + +## Experiments: what a plugin's own files allow + +These ran on a throwaway plugin with a note in `docs/`, a skill, an agent, and workflows, loaded with +`--plugin-dir`. They also used the installed copy of `aplyca-adf` v1.1.0 under +`~/.claude/plugins/cache/`. + +| Question | What happened | Finding | +|---|---|---| +| Does a skill get `${CLAUDE_PLUGIN_ROOT}` filled in? | The skill read the plugin's real path | Yes | +| Does an agent? | The agent, with only the Read tool, read the real path. Asked to quote its instructions, `spec-analyzer` quoted the full path | Yes | +| Does a workflow script? | The workflow's agent got the literal `${CLAUDE_PLUGIN_ROOT}`, and found the path only by loading one of the plugin's skills. In a template literal, the script threw | No | +| Can a session read the plugin's `docs/` without asking? | Denied (headless) | No | +| A skill's own folder, or another skill's? | Denied, both | No | +| The installed copy in `~/.claude/plugins/cache/`? | Denied, with the plugin loaded from there and without | No | +| With `Read(~/.claude/plugins/cache/aplyca/aplyca-adf/**)` allowed? | The session and a plugin agent each read the installed copy, nothing asked | Yes | +| A project's allow rule, in a session that never trusted the folder? | Ignored: "this workspace has not been trusted" | Headless runs pass the rule with `--allowedTools` | + +That is the design: the plugin carries the docs, skills and agents name its copy, a packaged project +commits the read rule, and the one workflow that needs the spec model carries its text. + +## The suite + +`run-session-evals.sh --suite plugin-docs` builds a packaged project the way `/adopt` leaves it, with +no reference docs in `docs/` and links at the release. Its sessions get no extra directory and no +blanket `Read`. The read rule is passed with `--allowedTools` for the plugin's path in the checkout, +as the project's rule applies to a teammate who trusted the folder. + +| Run | Sessions | Checks | Cost | +|---|---|---|---| +| 1 | 3 | 5 passed, 1 failed | $0.54 | +| 2: `agent-spec-model` after the fix | 1 | 2 passed, 0 failed | $0.41 | +| 3: the suite on the final build | 3 | 7 passed, 0 failed | $0.58 | + +| Case | What happened in the session | Checks | +|---|---|---| +| `agent-spec-model` | Run 1: the spec-analyzer agent read `docs/SPEC-MODEL.md` in the project, which doesn't exist, instead of the plugin's copy. Runs 2 and 3: it read the plugin's copy at its full path, and nothing asked | ✘ ✓ → ✓ ✓ | +| `session-docs` | The plugin's session context named its `docs/` folder. Claude read `MEMORY-STRATEGY.md` there, answered `# Memory strategy`, and fetched nothing | ✓ ✓ ✓ ✓ | +| `without-rule` | The same read was denied. Claude asked for permission to open the document instead of inventing its heading — the prompt a teammate would see | ✓ | + +## What run 1 found + +The agent's instructions gave the full path, which the experiment above confirmed Claude Code had +filled in. Haiku still read the doc as a project path. It was told to read four files, the other +three in the project (`AGENTS.md`, `docs/CONSTITUTION.md`, `specs/README.md`). The build now opens +every plugin skill and agent that names a reference doc with one line: those are the plugin's copies, +outside the project, so read them at the full paths given. Run 2 read the plugin's copy. + +## Negative controls + +Each check was fed a run where it shouldn't pass, and each marked ✘ there: + +- `agent-spec-model`'s read check, on run 1's agent session. +- `session-docs`' read and no-asking checks, on the `without-rule` session. +- `without-rule`'s denial check, on the `session-docs` session. + +## Not run here + +- **`deep-spec-analysis`.** Its plugin copy carries the spec model's text, which the build puts in + and the static checks parse. Running it costs a run of six or more agents for a step the build + already guarantees. +- **The read rule against the cache path, through the suite.** The suite loads the plugin from the + checkout, so its rule names that path. The cache path itself is covered by the experiment above. diff --git a/evals/dynamic/run-session-evals.sh b/evals/dynamic/run-session-evals.sh index 04d48e9..e8ae91f 100755 --- a/evals/dynamic/run-session-evals.sh +++ b/evals/dynamic/run-session-evals.sh @@ -24,6 +24,12 @@ # case drives one of the plugin's hooks in a real session (Haiku by default), and inspect.sh checks # that it fired — or, in a committed project, that it stood down. # +# The plugin-docs suite: the same kind of project, without the reference docs in docs/ (decision +# 0019). Sessions run in default permission mode with no extra directory and no blanket Read, as a +# teammate's would: the plugin's folder is readable only through the rule a packaged project commits, +# passed here with --allowedTools for the plugin's path in this checkout. A case marked +# runs without it. Agents that ask for opus run on Haiku too. +# # The adopt suite: {{FRAMEWORK}} in a prompt becomes --source (the framework's GitHub address by # default — what's published on main; pass this checkout's path to test a branch), and a fixture # marked loads the installer plugin for that session only, so nothing is @@ -32,7 +38,7 @@ # this checkout — with deny rules, which hold in every mode, for `claude` commands, `git push`, and # file-tool edits under your home folder. # -# Usage: ./run-session-evals.sh [--suite triage|debug|adopt|upgrade|plugin-hooks] [--models "sonnet opus"] [--cases "a b ..."] +# Usage: ./run-session-evals.sh [--suite triage|debug|adopt|upgrade|plugin-hooks|plugin-docs] [--models "sonnet opus"] [--cases "a b ..."] # [--out DIR] [--budget USD] [--parallel 4] [--read-only] [--source URL|PATH] # Needs: a signed-in Claude Code CLI (`claude auth login`), git, python3. # @@ -68,11 +74,13 @@ case "$SUITE" in adopt | upgrade) MAX_TURNS=80; BUDGET="${BUDGET:-8.00}"; BYPASS=1 EXTRA_TOOLS="WebFetch|Bash(git:*)|Bash(cp:*)|Bash(mkdir:*)|Bash(mv:*)|Bash(rm:*)|Bash(chmod:*)|Bash(python3:*)|Bash(printf:*)|Bash(echo:*)|Bash(test:*)|Bash(sed:*)|Bash(touch:*)|Bash(diff:*)" ;; plugin-hooks) MAX_TURNS=8; BUDGET="${BUDGET:-0.50}"; MODELS="${MODELS:-haiku}"; EXTRA_TOOLS="Bash(git commit:*)" ;; + plugin-docs) MAX_TURNS=12; BUDGET="${BUDGET:-0.75}"; MODELS="${MODELS:-haiku}"; EXTRA_TOOLS=""; DOCS_RULE=1 ;; *) MAX_TURNS=14; EXTRA_TOOLS="" ;; esac MODELS="${MODELS:-sonnet opus}" BUDGET="${BUDGET:-1.50}" BYPASS="${BYPASS:-}" +DOCS_RULE="${DOCS_RULE:-}" [ -n "$CASES" ] || CASES="$(ls "$FIXTURES" | sed -n 's/\.input\.md$//p' | tr '\n' ' ')" claude auth status 2>/dev/null | grep -q '"loggedIn": true' || { echo "Sign in first: claude auth login" >&2; exit 1; } @@ -236,23 +244,29 @@ def block(heading): open(sys.argv[3] + ".prompt", "w").write(block("## Prompt to give the AI")) open(sys.argv[3] + ".follow-up", "w").write(block("## Follow-up")) PY - local extra=() dirs=() + local extra=() dirs=() reads=(Read Grep Glob) run_env=() [ -n "$EXTRA_TOOLS" ] && IFS='|' read -r -a extra <<< "$EXTRA_TOOLS" local flags=(--model "$model" --output-format stream-json --verbose --max-turns "$MAX_TURNS") if [ -n "$READ_ONLY" ]; then flags+=(--disallowedTools Edit Write MultiEdit NotebookEdit) elif [ -n "$BYPASS" ]; then flags+=(--permission-mode bypassPermissions --disallowedTools "Bash(claude:*)" "Bash(git push:*)" "Edit(~/**)" "Write(~/**)") + elif [ -n "$DOCS_RULE" ]; then flags+=(--permission-mode default) else flags+=(--permission-mode acceptEdits); fi if grep -q '' "$input"; then - flags+=(--plugin-dir "$FWC/plugins/aplyca-adf"); dirs+=("$FWC") + flags+=(--plugin-dir "$FWC/plugins/aplyca-adf") + [ -n "$DOCS_RULE" ] || dirs+=("$FWC") + fi + if [ -n "$DOCS_RULE" ]; then # reads in the project need no rule; the plugin's folder, the committed one + reads=() run_env=(ANTHROPIC_DEFAULT_OPUS_MODEL=claude-haiku-4-5-20251001) + grep -q '' "$input" || reads=("Read(/$FWC/plugins/aplyca-adf/**)") fi [ -d "$SOURCE" ] && [ "$SOURCE" != "$FWC" -o ${#dirs[@]} -eq 0 ] && dirs+=("$SOURCE") [ ${#dirs[@]} -gt 0 ] && flags+=(--add-dir "${dirs[@]}") - flags+=(--allowedTools Read Grep Glob Skill "Bash(git log:*)" "Bash(git status)" "Bash(git show:*)" "Bash(git diff:*)" \ + flags+=(--allowedTools ${reads[@]+"${reads[@]}"} Skill "Bash(git log:*)" "Bash(git status)" "Bash(git show:*)" "Bash(git diff:*)" \ "Bash(git switch:*)" "Bash(git checkout:*)" "Bash(git branch:*)" "Bash(ls:*)" "Bash(grep:*)" "Bash(find:*)" \ "Bash(cat:*)" "Bash(head:*)" "Bash(wc:*)" ${extra[@]+"${extra[@]}"} \ --setting-sources project,local --strict-mcp-config --max-budget-usd "$BUDGET") - (cd "$work" && claude -p "$(cat "$work.prompt")" "${flags[@]}" \ + (cd "$work" && env ${run_env[@]+"${run_env[@]}"} claude -p "$(cat "$work.prompt")" "${flags[@]}" \ < /dev/null > "$OUT/$case_name.$model.jsonl" 2> "$OUT/$case_name.$model.err") if [ -s "$work.follow-up" ]; then local sid @@ -261,7 +275,7 @@ for raw in open(sys.argv[1]): try: d = json.loads(raw) except ValueError: continue if d.get("session_id"): print(d["session_id"]); break' "$OUT/$case_name.$model.jsonl")" - (cd "$work" && claude -p "$(cat "$work.follow-up")" --resume "$sid" "${flags[@]}" \ + (cd "$work" && env ${run_env[@]+"${run_env[@]}"} claude -p "$(cat "$work.follow-up")" --resume "$sid" "${flags[@]}" \ < /dev/null > "$OUT/$case_name.$model.2.jsonl" 2>> "$OUT/$case_name.$model.err") fi [ -f "$FIXTURES/inspect.sh" ] && bash "$FIXTURES/inspect.sh" "$work" "$OUT/$case_name.$model.jsonl" "$case_name" \ @@ -269,7 +283,7 @@ for raw in open(sys.argv[1]): echo " $case_name · $model done" } export -f run_case -export FW FWC WORK REPO OUT FIXTURES BUDGET READ_ONLY MAX_TURNS EXTRA_TOOLS SOURCE BYPASS +export FW FWC WORK REPO OUT FIXTURES BUDGET READ_ONLY MAX_TURNS EXTRA_TOOLS SOURCE BYPASS DOCS_RULE echo "Fixture project: $REPO" echo "Running: $CASES on $MODELS ($PARALLEL at a time, \$$BUDGET cap each)" for c in $CASES; do for m in $MODELS; do echo "$c $m"; done; done | xargs -P "$PARALLEL" -n 2 bash -c 'run_case "$0" "$1"' diff --git a/evals/static/check-skills.sh b/evals/static/check-skills.sh index b668c34..bad88e7 100755 --- a/evals/static/check-skills.sh +++ b/evals/static/check-skills.sh @@ -824,7 +824,7 @@ done check_agent_descriptions_not_misleading echo "" -for workflow in "$WORKFLOWS_DIR"/*.js; do +for workflow in "$WORKFLOWS_DIR"/*.js "$REPO_ROOT"/plugins/aplyca-adf/workflows/*.js; do [ -f "$workflow" ] && check_workflow "$workflow" done diff --git a/evals/static/test-hooks.sh b/evals/static/test-hooks.sh index 8c01969..03d7667 100755 --- a/evals/static/test-hooks.sh +++ b/evals/static/test-hooks.sh @@ -275,6 +275,19 @@ if [ "$untouched" = 0 ]; then else FAIL=$((FAIL+1)); echo "✘ _lib.sh: the plugin's copy acted in a project without the framework" fi +# Decision 0019: a packaged session learns where the plugin's reference docs are — unless the project +# still keeps its own copies. +mkdir -p "$PKG_ROOT/docs" && cp "$REPO_ROOT/plugins/aplyca-adf/docs/"* "$PKG_ROOT/docs/" +pkg_context() { printf '{"cwd":"%s","hook_event_name":"SessionStart"}' "$PROJ" | CLAUDE_PROJECT_DIR="$PROJ" CLAUDE_PLUGIN_ROOT="$PKG_ROOT" "$PKG/session-context.sh"; } +names=$(pkg_context | grep -c -F -- "- The framework's reference docs — SPEC-MODEL.md, COST-MODEL.md, MEMORY-STRATEGY.md, MCP-INTEGRATION.md — are in $PKG_ROOT/docs/") +mkdir -p "$PROJ/docs" && echo '# Spec model' > "$PROJ/docs/SPEC-MODEL.md" +kept=$(pkg_context | grep -c "reference docs") +rm -rf "$PROJ/docs" +if [ "$names" = 1 ] && [ "$kept" = 0 ]; then + PASS=$((PASS+1)); echo "✓ session-context.sh: a packaged session is told where the plugin's reference docs are, unless the project keeps its own" +else + FAIL=$((FAIL+1)); echo "✘ session-context.sh: reference-docs line printed $names time(s) (want 1), $kept with local copies (want 0)" +fi # config.sh is data: quoting styles, indentation, and trailing comments parse, and nothing in it runs. cat > "$PROJ/.claude/hooks/config.sh" <&1); code=$? check "cost-report: a project without transcripts says so and exits non-zero" "[ $code -ne 0 ] && echo \"\$missing\" | grep -q 'No Claude Code transcripts'" +# link-reference-docs.py (decision 0019), on a copy of the skeleton: a packaged project links the +# reference docs at the release it pins; a committed one, in its own docs/. +LINKS="$REPO_ROOT/scripts/link-reference-docs.py" +SITE="$WORK/linked" +cp -R "$REPO_ROOT/skeleton" "$SITE" +NAMES='COST-MODEL|MCP-INTEGRATION|MEMORY-STRATEGY|SPEC-MODEL' +local_refs() { grep -rlE "(^|[^/.A-Za-z0-9_-])docs/($NAMES)\.md" "$SITE" --include='*.md' --include='*.mdc' | grep -v -e "/\.claude/" -e "/docs/[A-Z-]*\.md$"; } +plugin_docs=$(cd "$REPO_ROOT/plugins/aplyca-adf/docs" && ls | sort | tr '\n' ' ') +check "link-reference-docs: the plugin carries the four reference docs (got: $plugin_docs)" \ + "[ '$plugin_docs' = 'COST-MODEL.md MCP-INTEGRATION.md MEMORY-STRATEGY.md SPEC-MODEL.md ' ]" +first=$(python3 "$LINKS" "$SITE" --packaged v9.9.9) +check "link-reference-docs: --packaged links every skeleton file at the release" \ + "[ -z \"\$(local_refs)\" ] && grep -q 'blob/v9.9.9/skeleton/docs/SPEC-MODEL.md' \"\$SITE/AGENTS.md\"" +again=$(python3 "$LINKS" "$SITE" --packaged v9.9.9) +check "link-reference-docs: a second run changes nothing" "echo \"\$again\" | grep -q 'in 0 file(s)'" +python3 "$LINKS" "$SITE" --packaged v9.9.10 >/dev/null +check "link-reference-docs: a new pin moves every link" \ + "! grep -rq 'blob/v9.9.9/' \"\$SITE\" && grep -q 'blob/v9.9.10/skeleton/docs/COST-MODEL.md' \"\$SITE/CLAUDE.md\"" +python3 "$LINKS" "$SITE" --committed >/dev/null +check "link-reference-docs: --committed restores the skeleton's files exactly" "diff -r \"\$REPO_ROOT/skeleton\" \"\$SITE\" >/dev/null" +bad=$(python3 "$LINKS" "$SITE" --packaged main 2>&1); code=$? +check "link-reference-docs: --packaged takes a release tag only" "[ $code -ne 0 ] && echo \"\$bad\" | grep -q 'release tag'" +check "link-reference-docs: the files it rewrote are the skeleton's" "echo \"\$first\" | grep -q 'AGENTS.md, CLAUDE.md, CONTRIBUTING.md'" + echo "=======================================" echo "Results: $PASS passed, $FAIL failed" echo "" diff --git a/plugins/aplyca-adf/.generated b/plugins/aplyca-adf/.generated index 0b8abb3..4bb4f10 100644 --- a/plugins/aplyca-adf/.generated +++ b/plugins/aplyca-adf/.generated @@ -1,4 +1,5 @@ agents +docs hooks skills/commit skills/context-audit diff --git a/plugins/aplyca-adf/README.md b/plugins/aplyca-adf/README.md index 2858dfd..99cbfde 100644 --- a/plugins/aplyca-adf/README.md +++ b/plugins/aplyca-adf/README.md @@ -5,12 +5,13 @@ The [Agentic Development Framework](../../README.md)'s plugin for Claude Code. I - **Install and maintain the framework** in a repository: `/aplyca-adf:adopt`, `/aplyca-adf:upgrade`, and `/aplyca-adf:cost-report`, in every project that uses the framework. - **Carry the framework's machinery for a packaged install** ([decision 0016](../../docs/decisions/0016-packaged-install.md)): - 20 skills, 8 agents, 4 workflows, and the guardrail hooks, pinned to a release. Typed as + 20 skills, 8 agents, 4 workflows, the guardrail hooks, and the framework's reference docs, pinned + to a release. Typed as `/aplyca-adf:triage`, `/aplyca-adf:deep-review`, and so on. By default, an adopting repository uses the **packaged** install ([decision 0018](../../docs/decisions/0018-packaged-by-default.md)): the machinery comes from this -plugin, pinned to a release, and the repository commits only its own layer — about 40 fewer files +plugin, pinned to a release, and the repository commits only its own layer — about 50 fewer files ([docs/SETUP.md § Packaged install](../../docs/SETUP.md#packaged-install-claude-code-only)). A team that also uses other AI tools — Cursor, Copilot, Antigravity, Windsurf, Aider — or Claude Code's cloud sessions chooses the **committed** install: the framework as files in the repository (the @@ -20,8 +21,8 @@ aside: its hooks stand down, and its skills and agents hand over to the committe where the stamp on `CLAUDE.md`'s first line says `install: packaged`. The machinery under `skills/` (except `adopt`, `upgrade`, and `cost-report`), `agents/`, -`workflows/`, and `hooks/` is generated from the skeleton by `scripts/build-aplyca-adf.sh`; never -edit it here. +`workflows/`, `hooks/`, and `docs/` is generated from the skeleton by `scripts/build-aplyca-adf.sh`; +never edit it here. ## Install @@ -126,7 +127,9 @@ branch, and never push without explicit approval. `/aplyca-adf:cost-report` only **In a packaged project,** the rest of the framework comes from here too: the workflow skills (`/aplyca-adf:triage`, `/aplyca-adf:write-spec`, `/aplyca-adf:implement`, `/aplyca-adf:review`, …), the agents (`aplyca-adf:code-reviewer`, `aplyca-adf:spec-analyzer`, …), the `/aplyca-adf:deep-…` -workflows, and the hooks, which read the project's `.claude/hooks/config.sh`. The catalogs: +workflows, the hooks, which read the project's `.claude/hooks/config.sh`, and the framework's +reference docs — the spec model, the cost model, the memory strategy, and MCP integration +([decision 0019](../../docs/decisions/0019-reference-docs-in-the-plugin.md)). The catalogs: [SKILLS-REFERENCE.md](../../docs/SKILLS-REFERENCE.md) and [AGENTS-REFERENCE.md](../../docs/AGENTS-REFERENCE.md). ## For teams @@ -150,6 +153,11 @@ the plugin by a relative path. A machine where nobody trusts the folder — CI } ``` +A packaged project also lets Claude read the plugin's own folder, +`"permissions": { "allow": ["Read(~/.claude/plugins/cache/aplyca/aplyca-adf/**)"] }`, so its skills +and agents open the reference docs without asking +([decision 0019](../../docs/decisions/0019-reference-docs-in-the-plugin.md)). + Every project pins its release with `"ref"`, and `/aplyca-adf:adopt` and `/aplyca-adf:upgrade` keep it equal to the release in the `CLAUDE.md` stamp. In a packaged project the pin chooses the machinery; in a committed one it keeps the plugin's copies at the same release as the committed diff --git a/plugins/aplyca-adf/agents/spec-analyzer.md b/plugins/aplyca-adf/agents/spec-analyzer.md index 8d7005c..9e09eab 100644 --- a/plugins/aplyca-adf/agents/spec-analyzer.md +++ b/plugins/aplyca-adf/agents/spec-analyzer.md @@ -14,6 +14,8 @@ disallowedTools: > **Step 0 — which copy.** This is the packaged copy. Unless this project's `CLAUDE.md` says "This project uses the packaged install", open `.claude/agents/spec-analyzer/agent.md` and follow that file instead of this one. +> **The reference docs this file names are the plugin's copies,** in `${CLAUDE_PLUGIN_ROOT}/docs/` — outside this project, which keeps none in its own `docs/`. Read them at the full paths given. + You are a skeptical reviewer of plans. Your job is to find what a spec folder gets wrong **before** anyone approves it — when a gap is still a sentence to fix instead of a rewrite. Assume the plan is convincing and incomplete: the most common miss is the change surface, the set of files and layers @@ -23,7 +25,7 @@ You do not fix anything. You report gaps with evidence; the author fixes them. ## Before you start -Read `AGENTS.md`, `docs/CONSTITUTION.md`, `specs/README.md`, and `docs/SPEC-MODEL.md`. Then read the +Read `AGENTS.md`, `docs/CONSTITUTION.md`, `specs/README.md`, and `${CLAUDE_PLUGIN_ROOT}/docs/SPEC-MODEL.md`. Then read the whole spec folder: `spec.md` (every section, including any `CR N` change request), `plan.md`, and `tasks.md`. Read nested `AGENTS.md` files and `docs/reference/` pages for the areas the plan touches. diff --git a/plugins/aplyca-adf/agents/spec-writer.md b/plugins/aplyca-adf/agents/spec-writer.md index e6e77c1..6174517 100644 --- a/plugins/aplyca-adf/agents/spec-writer.md +++ b/plugins/aplyca-adf/agents/spec-writer.md @@ -11,17 +11,19 @@ tools: > **Step 0 — which copy.** This is the packaged copy. Unless this project's `CLAUDE.md` says "This project uses the packaged install", open `.claude/agents/spec-writer/agent.md` and follow that file instead of this one. +> **The reference docs this file names are the plugin's copies,** in `${CLAUDE_PLUGIN_ROOT}/docs/` — outside this project, which keeps none in its own `docs/`. Read them at the full paths given. + You are a product specification writer. You capture requirements from every relevant role — business, functional, security, accessibility, privacy, design, performance, testing, documentation, deployment — in one clear, multi-section `spec.md` that drives everything downstream. -This project uses the **multi-perspective spec model** (`docs/SPEC-MODEL.md`) inside **spec folders** +This project uses the **multi-perspective spec model** (`${CLAUDE_PLUGIN_ROOT}/docs/SPEC-MODEL.md`) inside **spec folders** (`specs/README.md`): `spec.md` is the WHAT and WHY; `plan.md` (the HOW, written later by `/aplyca-adf:write-plan`) and `tasks.md` live beside it. ## Before you start -Read `AGENTS.md`, `CLAUDE.md`, `docs/SPEC-MODEL.md`, and `specs/README.md`. Search `specs/` for an +Read `AGENTS.md`, `CLAUDE.md`, `${CLAUDE_PLUGIN_ROOT}/docs/SPEC-MODEL.md`, and `specs/README.md`. Search `specs/` for an existing folder covering this feature — a delivered feature is **amended** with a change request, never re-specified in a new folder. Read `docs/GLOSSARY.md` for terminology. diff --git a/plugins/aplyca-adf/docs/COST-MODEL.md b/plugins/aplyca-adf/docs/COST-MODEL.md new file mode 100644 index 0000000..e683592 --- /dev/null +++ b/plugins/aplyca-adf/docs/COST-MODEL.md @@ -0,0 +1,337 @@ +# Cost model and model tiering + +How to manage AI development costs — tokens and time — in a project that uses this framework. Covers what a session actually costs, the lanes and the effort dials, the three model tiers, per-skill / per-agent recommendations, prompt caching, measuring, cost attribution, and tool integrations. + +The framework is opinionated about WHEN to use which model and HOW to structure context for cache efficiency. Teams that follow these recommendations spend noticeably less than teams that default everything to Opus: at current list prices Sonnet costs about half as much per token and Haiku about a quarter, and a stable, cache-friendly context cuts input costs further. The overall saving depends on your mix of work — measure it rather than assuming a multiple. + +## Why this matters + +By 2026, AI engineering workflows are the #1 line item in many teams' LLM token spend. The difference between "default to Opus for everything" and "default to Sonnet, escalate to Opus only when needed, route triage through Haiku, cache aggressively" shows up directly in the monthly bill — roughly 2× per token from the Sonnet-vs-Opus choice alone, with prompt caching (cache reads bill at a tenth of the base input price or less) often mattering as much as the model choice. + +For consultancies billing AI-assisted work to clients, cost attribution per feature/per project is also a billing requirement, not just an internal concern. + +## What a session costs + +Every call to the model re-reads the whole conversation — instructions, every message, every tool +output so far — mostly from the prompt cache. So a session costs roughly **number of calls × context +size**, plus the tokens the model writes. Measured on 48 sessions in two production projects +(September 2026, Opus 5 and 5.5): + +| Measure | Value | +|---|---| +| Median cost per call | about $0.06 | +| Context at the first call | 46–66k tokens — Claude Code and its tools, the instruction files, memory. This framework's always-loaded instructions are about 5–7k of that | +| Where the money goes | about 47% re-reading cached context, 27% writing the cache, 17% output | +| Sessions under 30 calls | median about $1 and 10 minutes of activity | +| 30–100 calls | median about $4 and 35 minutes | +| Over 100 calls | median about $20 and 2+ hours | + +What drives the bill, in order: + +1. **The number of calls.** Every step — a file read, a test run, a question, a spec section — is a + call. Ceremony that isn't needed adds calls; so do verification loops (re-running a whole suite, + browser checks). +2. **The context size.** Long sessions and verbose output make every later call more expensive: in + sessions over ~90 calls the average call carried 145–190k tokens, against ~60k at the start. +3. **Pauses longer than the cache lifetime.** The prompt cache expires about 5 minutes after its last + use (Claude Code uses a longer-lived cache for some requests). After a longer wait — an + unanswered question, a gate, a meeting — the next call writes the whole context again: about + $0.75–1.20 for 150k tokens on Opus 5.5. +4. **The model** — Opus costs twice Sonnet for output and uncached input; cache reads cost the same + (see the tiers below). + +The framework's fixed overhead is small by comparison: the always-loaded instructions add a fraction +of a cent per call once cached, and the guardrail hooks take 60–140 ms per tool call against 5–10 +seconds of model time. + +## Lanes and what they cost + +The lane (`specs/README.md` § Lanes) is the biggest lever a team controls. Typical ranges on Opus 5.5 +at the measured $0.06–0.09 per call; Sonnet runs about 25–35% less: + +| Work | Lane | Calls | Agent time | Cost | +|---|---|---|---|---| +| Typo, copy, a precise adjustment | Fast | 8–20 | 2–10 min | $0.5–1.5 | +| Bug with a clear cause | Fast | 15–30 | 5–15 min, plus test runtime | $1–2 | +| Either, in a risk area | Careful | fast + 5–15 for the checklist and confirmation | +5–10 min | $1.5–3 | +| Bug, cause unknown | `/aplyca-adf:debug`, then a lane | 30–60 | 15–40 min | $2–5 | +| Change request with something to decide | Full | 60–120 | 30–90 min, plus the wait at the gate | $5–12 | +| New feature | Full | 150–400 | hours | $10–40 | +| A `/deep-*` workflow run | Opt-in | 6–15 agents, each starting from a fresh context | — | $2–8 | + +A precise adjustment pushed through the full lane costs 5–10× the fast lane, and the extra steps +record decisions nobody had to make: the tests, the review, and the human QC — the steps that find +defects — run in every lane. + +## Effort — what to raise, and what it costs + +A developer's intuition that a task needs more care is a valid input — triage honors any request to +raise effort. The dials are independent, so raise the one the intuition points at: + +| Dial | How to ask | What it adds | Cost | +|---|---|---|---| +| **Lane** | "full lane on this", "be careful here" | Careful: the area's checklist and a confirmation. Full: spec, plan, gate | Careful: roughly +20–50%. Full: roughly 5–10× the fast lane | +| **Understanding** | "question my request first" | Clarifying questions before any code | One exchange — though a wait past the cache lifetime re-writes the context | +| **Design** | "compare approaches first" (`/aplyca-adf:evaluate`) | Options with trade-offs and a recommendation | One analysis pass, typically 10–30 calls | +| **Thinking** | The desktop app's effort selector or `effortLevel` in settings; `/model opus` | More reasoning per step | More output tokens per call; Opus doubles output and uncached input | +| **Verification** | "add edge-case tests", "`@aplyca-adf:security-reviewer` on this", "`/aplyca-adf:deep-review` before the PR" | Independent checks | A subagent starts from a fresh ~50–60k-token context; `/aplyca-adf:deep-review` runs 6–15 of them | + +## Choosing between Sonnet and Opus + +Pick the model by the kind of work, with the version-less aliases (`sonnet`, `opus`) — never a pinned +version. The test, from Anthropic's guidance for these models: **does the task have a clear spec and +a way to check the result?** + +| Work | Model | Effort | +|---|---|---| +| Fast lane — a precise change proved by a test | `sonnet` | Default (medium) | +| Careful lane — the same in a risk area | `sonnet` | `high` | +| Bug fix, cause clear or found quickly | `sonnet` | Default; `high` for a stubborn one | +| Bug that resists two hypotheses; concurrency, caching, distributed state | `opus` | `high` | +| Investigation, impact analysis, estimate; reviews; drafting docs | `sonnet` | Default | +| Full lane: spec, plan, and the approval gate — ambiguity and judgment | `opus` | `high` | +| Full lane after the gate: `/aplyca-adf:write-docs`, `/aplyca-adf:implement` — an approved plan with named tests | `sonnet` | Default; `high` for a hard task | +| Architecture-level questions, long-horizon work across many files | `opus` | `high` | + +- **Switch where the cache is cold or small.** Each model has its own prompt cache: after a switch, + the next call re-reads the whole conversation uncached. Switch when the session starts, right + after triage (the context is still small — `/aplyca-adf:triage` says when the model doesn't fit), or in a + fresh session after the approval gate: the spec folder is the handoff, and the wait at the gate has + usually let the cache expire anyway. Don't switch for a single step. +- **Effort before model.** Within Sonnet, `/effort high` for harder or longer work is often enough. + `xhigh` and `max` make Sonnet think longer and cost more — at that point Opus is usually the + better trade. +- **Claude Code's own `default` is Opus.** The project setting (`"model": "sonnet"`) covers new + sessions, but the desktop app's picker and `/model` decide for a session — pick Sonnet for quick + work. `opusplan` (Opus in plan mode, Sonnet otherwise) suits developers who plan in plan mode. +- **Subagents carry their own model** in frontmatter and run in their own context, so their choice + costs no cache switch — reviews on `sonnet`, adversarial analysis and architecture on `opus` (see + Per-agent recommendations). + +## Keeping sessions cheap + +- **One task per session; `/clear` before the next.** A fresh session starts at ~60k tokens; a long + one can carry 200k+ on every call. +- **A 1M-context model is for work that needs it.** With a 1M window (e.g. `opus[1m]`) compaction + happens late, so routine sessions grow far past what the task needs — and every call pays for it. +- **Keep output short.** Quiet test reporters, `| tail -n 40`, reading the lines you need. A log + printed once is paid for on every later call. +- **Targeted tests while iterating, the full gate once** (`.claude/rules/testing.md` § Verification + budget). +- **Browser checks by the agent only when asked or for a visual change.** In one measured bug fix, + 210 browser calls cost more than the rest of the session; the human QC on the preview is the check. +- **Ask together, then wait once.** Batch blocking questions into one round, each with a recommended + answer; state minor implementation choices as assumptions in the pull request. + +## Between phases — continue, clear, hand off, or compact + +A task has phases: triage, spec and plan, implementation, review. Decide what happens to the +conversation at the boundary between two of them, never in the middle of one — compacting mid-phase +loses the thread. Ask in order; the first yes wins. + +1. **Does the next phase need this conversation as it happened?** Continue. Triage → a fast-lane + edit, or clarifying a spec → writing it: the reasoning is the input, and any summary of it loses + something. +2. **Is everything here disposable?** `/clear` — a finished task, an unrelated next one. It's the + cheapest move, and the old session stays resumable. +3. **Does the work travel** — to a teammate, another worktree, another tool? Write a short handoff + (`/aplyca-adf:handoff`): what was decided, what's next, and pointers to the spec folder, branch, and pull + request rather than copies of them. In the full lane after the approval gate, the spec folder + already is the handoff: start a fresh session from it. +4. **Can the next step run unattended?** Give it to an agent (`@aplyca-adf:code-reviewer`, `/aplyca-adf:deep-review`): it + works in its own context and returns a report, and this session stays as it is. +5. **Otherwise, `/compact`** — with an instruction about what the next phase needs + (`/compact keep the open questions and the change surface`). + +Switching the model follows the same logic: `/model opus` in place re-reads the conversation once, +uncached, but keeps all of it; a fresh session is cheaper only when files carry what the next phase +needs. + +## Measuring + +- **`/cost`** in Claude Code shows the current session. +- **The framework plugin's `/cost-report`** reads Claude Code's local transcripts for a project and + reports each session's calls, active time, tokens, and estimated cost, with the patterns above + flagged (long context, many waits, browser loops). Run it after a few weeks on a new lane setup + to see what changed. +- **Track rework next to cost.** Reopened tasks, follow-up fixes, and review rejections per lane tell + you whether the triggers are tight enough; adjust them with a PDR (`docs/process/`). + +## The three tiers + +This framework is built around three Claude model tiers. Use the right tier for the task — over-spec'ing wastes money; under-spec'ing produces worse output that costs more to iterate on. + +| Tier | Model alias (current model, September 2026) | Use when... | Approximate relative cost (vs Haiku) | +|---|---|---|---| +| **Capable** | `haiku` (Haiku 4.5) | Narrow, well-bounded subagent work in a small context: classification, simple lookups, deterministic-ish checks | 1× (cheapest) | +| **Balanced** | `sonnet` (Sonnet 5.5) | Work with a clear spec and a way to check the result — fast and careful lanes, bug fixes, implementing an approved plan, investigation, review, drafting | 2× Haiku input and output | +| **Frontier** | `opus` (Opus 5.5) | Judgment — the full lane's spec and plan, ambiguous or long-horizon work, architecture decisions, bugs that resist diagnosis | 4× Haiku input and output | + +Configure models with these aliases everywhere Claude Code takes one: `.claude/settings.json` (`"model": "sonnet"`), agent frontmatter (`model: haiku`), and `/model`. They're version-less — each resolves to the current model of its tier and moves forward as Claude Code updates, so keep Claude Code current with `claude update` (Sonnet 5.5 needs v2.1.284+, Opus 5.5 v2.1.280+). On Amazon Bedrock, Google Cloud, and Microsoft Foundry an alias can resolve to an older model (e.g. `sonnet` → Sonnet 4.5); pin the provider's model ID there with `ANTHROPIC_DEFAULT_SONNET_MODEL` / `ANTHROPIC_DEFAULT_OPUS_MODEL` / `ANTHROPIC_DEFAULT_HAIKU_MODEL`. Pin a full model ID (e.g. `claude-sonnet-5-5`) only when your team needs a fixed version. + +> **Pricing changes** — ratios are from list prices per million input / output tokens as of September 2026: Haiku 4.5 $1 / $5, Sonnet 5.5 $2 / $10, Opus 5.5 $4 / $20. Sonnet 5.5 and Opus 5.5 use a newer tokenizer that produces roughly 30% more tokens than Haiku 4.5 for the same text, so per unit of work their gap to Haiku is somewhat wider than the per-token ratio. Cache reads cost $0.20 / MTok on both Sonnet 5.5 and Opus 5.5, so in cache-heavy sessions the Sonnet–Opus difference is mostly in output and uncached input. `fable` (Fable 5.1, $10 / $50, 10× Haiku) sits above these tiers; the decision rules below stop at Opus. Always check [current Anthropic pricing](https://platform.claude.com/docs/en/about-claude/pricing) before doing detailed cost projections. The decision rules below stay valid even as absolute prices shift. + +### Decision rules + +- **Default to Sonnet** for work with a clear spec and a way to check the result — most work, once the lanes route it. +- **Use Haiku** for well-defined, bounded work that runs in its own small context: subagents for convention checks and most reviews, classification, routing, simple lookups. Inside a long main conversation, switching to Haiku for one step costs more than it saves — the cache is per model. +- **Use Opus** where judgment is the work: the full lane up to the gate, ambiguous requirements, long-horizon changes, architecture, a bug that resists two hypotheses — and when Sonnet at `high` effort keeps producing inadequate output. +- **Don't escalate "just in case"** — Opus on tasks Sonnet handles well is pure waste. The framework's anti-rationalization tables, plan-then-execute gates, and verification checklists do most of the quality work that escalation would otherwise paper over. + +### Switching tiers in Claude Code + +- `/model` — built-in command to switch the session's model (e.g. `/model opus` before a hard reasoning task, then `/model sonnet` after). Switching mid-session means the new model reads the whole context uncached once — switch at the start of a task, not for a single step. +- Precedence — the desktop app's model picker and `/model` override the project's `.claude/settings.json`, which overrides user settings. A user-level `"model": "opus[1m]"` is a costly default for routine work. +- Agent frontmatter — set `model:` in an agent's `agent.md` to pin that agent to a tier regardless of the session default (aliases here too). Use it for work that should always run on a given tier, e.g. `@aplyca-adf:code-reviewer` / `@aplyca-adf:security-reviewer` on `haiku`. +- `/fast` — built-in Claude Code toggle (research preview) that runs Opus in a faster-output configuration. It is the same model — it does NOT downgrade to a smaller one — with up to ~2.5× faster output at premium pricing ($8 / $40 per MTok on Opus 5.5, vs $4 / $20 standard). Supported on Opus 5.5, Opus 5, and Opus 4.8, and only through the Anthropic API or subscription plans' usage credits (not Bedrock, Google Cloud, or Foundry). Turn it on at the start of a session: enabling it mid-conversation bills the whole existing context at the fast-mode uncached input rate. Useful when you're already on Opus for a hard problem and want quicker streaming; it's a per-user preference (`fastMode` in user settings), not a project-level setting. + +## Per-skill recommendations + +Skills run in your conversation, on its model. A skill could name its own model, but a switch mid-session re-reads the whole context uncached on the new model — so for skills the lever is the **session's** model: Sonnet for fast- and careful-lane sessions, Opus when a session is planning or debugging something hard. + +| Skill | Recommended tier | Why | +|---|---|---| +| `/aplyca-adf:init-project` | Sonnet | Multi-perspective setup decisions; one-time so cost is small | +| `/aplyca-adf:triage` | Sonnet | Reading a task in full and deciding what it needs; cheap, and it prevents the most expensive mistakes | +| `/aplyca-adf:write-spec` | Opus | Full lane: ambiguity, clarification, and the decisions the spec records — judgment work | +| `/aplyca-adf:write-plan` | Opus | The change surface and test strategy decide everything downstream; the gate follows — then hand off to a fresh Sonnet session | +| `/aplyca-adf:write-tests` | Sonnet | AC → test mapping is moderate complexity | +| `/aplyca-adf:write-docs` | Sonnet | Synthesis from spec + tests; matters for tone and accuracy | +| `/aplyca-adf:implement` | Sonnet | An approved plan with named tests is a clear spec with a way to check the result; Opus for a task that turns out genuinely hard | +| `/aplyca-adf:review` | Sonnet | Multi-perspective review of diffs | +| `/aplyca-adf:debug` | Sonnet → Opus | Sonnet for most diagnoses; Opus after two disproven hypotheses, or for concurrency, caching, and distributed state | +| `/aplyca-adf:refactor` | Sonnet | Pattern extraction + maintaining test parity | +| `/aplyca-adf:commit` | Session model | A few short calls; switching to Haiku for them would re-read the whole context uncached | +| `/aplyca-adf:handoff` | Session model | A short message of pointers, written where the context already is | +| `/aplyca-adf:open-pr`, `/aplyca-adf:stakeholder-update` | Sonnet | Short, but every claim must be checked against the diff, the gate results, or the live site | +| `/aplyca-adf:record-decision`, `/aplyca-adf:context-audit`, `/aplyca-adf:spec-drift` | Sonnet | Reading and comparing many files; precision matters more than depth | +| `/aplyca-adf:evaluate` | Sonnet (or Opus for hard decisions) | Deep analysis with options and tradeoffs. The "evaluate" name implies the higher-value work where escalation often pays off. | +| `/aplyca-adf:spec-workflow` | n/a | Reference doc, no AI invocation | + +**Practical guidance:** set your default to Sonnet and pick the model per session, by the work it will do. Escalate to Opus only when you hit the explicit triggers above. + +## Per-agent recommendations + +Agents have a `model:` field in their frontmatter, so the framework CAN enforce model choice for them. Current defaults in `.claude/agents/`: + +| Agent | Current frontmatter | Why | +|---|---|---| +| `@aplyca-adf:spec-writer` | `model: sonnet` | Same reasoning as `/aplyca-adf:write-spec` skill | +| `@aplyca-adf:code-reviewer` | `model: sonnet` | Review is well-defined, repeatable work — where Anthropic's guidance places Sonnet; it costs about twice Haiku per token, a few cents per review | +| `@aplyca-adf:security-reviewer` | `model: sonnet` | Security review in the careful lane needs real reasoning about data flow; escalate to Opus for novel attack surfaces | +| `@aplyca-adf:test-runner` | `model: sonnet` | Test writing requires understanding the spec and matching patterns | +| `@aplyca-adf:architect` | `model: opus` | Architecture review weighs trade-offs across the system — judgment work; it runs rarely | +| `@aplyca-adf:debugger` | `model: sonnet` | Root cause analysis benefits from stronger reasoning | +| `@aplyca-adf:ux-reviewer` | `model: sonnet` | Review against the spec's stories, states, and accessibility — well-defined review work | +| `@aplyca-adf:spec-analyzer` | `model: opus` | Adversarial analysis of a plan is judgment work, and it runs once per full-lane folder, before the gate — where a missed problem is most expensive | + +**To override** for a specific project, edit the agent's `agent.md` frontmatter. Document your override and why. + +## Dynamic workflows + +The `/deep-*` workflows in `.claude/workflows/` fan out to many agents — one per review dimension, +spec lens, file, or spec — and then spend more agents verifying each finding. A `/aplyca-adf:deep-review` of a +moderate diff typically runs 6–7 reviewers plus one verifier per finding: several times the cost of +`/aplyca-adf:review`, for higher coverage and fewer false positives. Use them where that trade pays — high-stakes +changes, pre-gate analysis of risky specs, periodic sweeps — and the single-context skills +everywhere else. Workflow agents inherit the session model unless the script pins one. + +## Prompt caching strategy + +Anthropic's prompt caching can reduce input cost by ~90% on cache hits (with a 5-minute TTL). To exploit it, structure context so the **stable prefix** (rarely-changing) comes BEFORE the **variable suffix** (per-message specifics). + +### What's stable, what's variable + +| Stable (cache prefix — put first) | Variable (cache suffix — put last) | +|---|---| +| AGENTS.md, CLAUDE.md | Current conversation messages | +| Loaded engineering rules (`.claude/rules/`) | Per-task spec / test / doc references | +| Loaded skill (`.claude/skills//SKILL.md`) | Tool call results from this turn | +| Long-lived memory facts | Active file edits | +| Reference docs (architecture, security, glossary) | | + +### Practical rules to keep cache hit rates high + +- **Don't edit AGENTS.md / CLAUDE.md / engineering rules continuously.** Each edit busts the cache for every subsequent request. Batch edits into a single PR. +- **Avoid putting per-feature volatile content in CLAUDE.md.** That belongs in the feature's spec or in memory. CLAUDE.md should describe the project's stable identity. +- **Memory entries are great for project-specific gotchas** that change occasionally — they evolve without busting the CLAUDE.md cache. +- **Subagents have their own cache.** A subagent invocation doesn't bust the parent's cache. Use them for anything that loads a lot of one-off context. +- **Long conversations get compacted automatically.** Cleared tool outputs cost nothing on subsequent turns. You don't need to manage compaction by hand, but be aware that it can change cache prefix length. + +### Verifying cache behavior + +Anthropic API responses include `usage.cache_creation_input_tokens` and `usage.cache_read_input_tokens`. If you see lots of `cache_creation` and few `cache_read`, your prefix is unstable. Investigate what's varying. + +## Cost attribution patterns + +For consultancies and multi-project teams, attributing tokens to features / projects / clients matters for billing and budgeting. + +### Per-feature attribution + +Tag each AI session with the feature spec name. Manually: include "working on spec: newsletter-signup" in your first message. Programmatically: API requests can include a custom `metadata.user_id` field that you can set to the spec name; aggregate by metadata at month end. + +### Per-team / per-project attribution + +Use **separate API keys per team or per project**. Anthropic Console attributes spend by API key automatically. This requires no code changes. + +### Per-client attribution (consultancy pattern) + +Use an **AI gateway** (Helicone, LiteLLM) that routes by client and tags every request with a client ID. The gateway aggregates spend per client and produces invoice-ready reports. + +## Tool integrations + +| Tool | Best for | Setup effort | +|---|---|---| +| **[Anthropic Console](https://console.anthropic.com)** | Built-in dashboards by API key. Zero setup. | None | +| **[Helicone](https://helicone.ai)** | Open-source LLM gateway. Per-request cost tracking, dashboards, custom properties for tagging. Free tier suitable for small teams. | ~30 min — proxy your Anthropic base URL through Helicone | +| **[LiteLLM](https://litellm.ai)** | Multi-provider proxy with budget enforcement, alerts, and routing rules. Open-source. | ~1 hour — deploy as a service, point clients at it | +| **[Braintrust](https://braintrust.dev)** | Eval-focused but tracks per-run cost. Good if you're already running dynamic evals (see `evals/`). | ~1 hour | +| **Custom (Anthropic API direct)** | Full control. Read `usage` from response headers, log to your own DB. | Variable | + +For a typical consultancy or agency running AI-assisted work across several client projects, the recommended stack is: +- **Anthropic Console** for high-level monthly visibility (free, built-in) +- **Helicone or LiteLLM gateway** for per-client attribution and budget enforcement (when monthly spend is meaningful enough to justify the setup) + +## Budget enforcement + +| Level | What it does | When to use | +|---|---|---| +| **Soft alerts** (50%/80%/100% of monthly budget) | Email/Slack notifications when spend crosses thresholds | Always — easy and prevents surprise bills | +| **Hard limits at the gateway** | Refuses requests over the limit, returning 429 | When you've had a runaway-cost incident OR when client contracts cap spend | +| **Per-feature ceilings** | Manual approval if a single feature exceeds N tokens | Rarely — most features fit fine within reasonable limits; ceilings add friction | + +Don't enforce per-feature ceilings by default — most features don't need them, and the friction outweighs the savings. + +## Cost-conscious framework practices + +The framework's structural choices already help cost. To get the most savings: + +1. **Match the lane to the risk.** The full lane's plan-then-execute prevents expensive wrong paths where there's something to decide; for precise requests, the fast lane gets the same proof for a fraction of the calls. +2. **Use subagents for context-heavy work.** When you need to load a lot of one-off context (e.g., reading 20 files to answer one question), invoke a subagent with the right tools — it returns a summary, the parent's context stays clean, and your cache prefix stays stable. +3. **Keep sessions short.** One task per session, `/clear` between tasks, short tool output. Auto-compaction helps when a window fills, but a session that never grows that large is cheaper on every call. +4. **Use memory for repeated context.** Per-project gotchas, terminology, recurring patterns — these belong in memory (where they're loaded just-in-time) not in CLAUDE.md (where they bloat every request). +5. **Use the smallest model that works for the job.** Default to Sonnet, not Opus — chosen per session. Haiku belongs to subagents with small contexts. +6. **Run static evals in CI, not dynamic evals.** Static checks cost nothing. Dynamic evals cost real tokens — run them nightly or pre-release, not on every PR (see `evals/README.md`, if this project keeps evals). +7. **Cache aggressively.** Keep AGENTS.md / CLAUDE.md / rules stable. Batch edits. Don't put per-feature content in shared files. + +## Quick reference: when costs spike + +If your monthly bill jumps unexpectedly: + +1. Check Anthropic Console for the timeline of the spike. Which day? Which API key? +2. Look at your gateway dashboard (if you have one) for the requests in that window — what tasks were being run? What models? +3. Common causes: + - Someone defaulted their tool to Opus, or to a 1M-context model + - Long sessions: many tasks in one conversation, or verbose logs and browser loops filling the context + - Small changes routed through the full lane + - A skill was modified and the cache prefix got reordered (cache hit rate drops, every request becomes cache miss) + - Long-running agentic loops with no termination condition + - Manual experimentation in the AI tool that was forgotten +4. Fix at the source. Don't add hard limits as a band-aid for an upstream problem. + +## See also + +- [`AGENTS.md`](../AGENTS.md) — project-wide AI conventions +- [Anthropic pricing](https://platform.claude.com/docs/en/about-claude/pricing) — current rates +- [Helicone docs](https://docs.helicone.ai) — gateway setup +- [LiteLLM docs](https://docs.litellm.ai) — multi-provider proxy setup diff --git a/plugins/aplyca-adf/docs/MCP-INTEGRATION.md b/plugins/aplyca-adf/docs/MCP-INTEGRATION.md new file mode 100644 index 0000000..d08479a --- /dev/null +++ b/plugins/aplyca-adf/docs/MCP-INTEGRATION.md @@ -0,0 +1,203 @@ +# MCP integration + +How to expose this project's specs, ADRs, runbooks, and other knowledge artifacts as **Model Context Protocol (MCP) resources** so AI tools (Claude Code, Cursor, Antigravity, others) can query them via a uniform protocol instead of file reads. + +This is **optional**. The framework works without MCP. But for projects with a large `specs/` directory, many ADRs, or multiple runbooks, exposing them as queryable MCP resources turns "find all specs touched in the last week" or "give me the Security section of every spec in the marketing area" from a manual file-walk into a single AI-readable query. + +## What MCP is (one paragraph) + +The Model Context Protocol is an open standard from Anthropic that lets AI tools discover and call tools, read resources, and follow prompts from external servers via a uniform JSON-RPC API. AI tools like Claude Code can connect to multiple MCP servers and use their resources alongside the local file system. As of 2026 it has authentication semantics, schema validation, and is supported across major AI coding tools. See [modelcontextprotocol.io](https://modelcontextprotocol.io) for the current spec. + +## Why expose framework artifacts via MCP + +The framework's primary artifacts (specs, ADRs, runbooks, admin guides) are markdown. Without MCP, an AI tool reads them via the file system — fine for small projects, awkward at scale because: + +- Cross-spec queries ("which specs reference the Mailchimp integration?") require manual greps +- Section-level access ("show me the Security section of all UI specs") requires custom parsing +- The AI loads more context than necessary (full spec files vs. just the relevant section) +- No way to cleanly expose specs to multiple AI tools with the same interface + +MCP solves this by giving you a server that can: +- List specs/ADRs/runbooks as discoverable resources +- Return full content OR specific sections by URI +- Filter by metadata (feature-type, area, status, last-modified) +- Surface relationships (which specs reference which ADRs) + +## When to set up an MCP server for this framework + +**Yes:** +- You have ≥30 specs and section-level queries are getting tedious +- Multiple teams / multiple AI tools query the same spec corpus +- You want consistent access across Claude Code, Cursor, and Antigravity without each tool implementing its own parsing +- You need authenticated access (e.g., specs that include sensitive client info) + +**No / not yet:** +- <10 specs and grepping works fine +- Single team, single AI tool — file reads are simpler +- You're not willing to maintain a small TypeScript/Python service + +## What to expose + +For most projects adopting this framework: + +| Resource family | URI pattern (example) | Use | +|---|---|---| +| Specs | `mcp://specs/` | Full `spec.md` content (or a legacy single-file spec) | +| Spec sections | `mcp://specs//section/` | E.g. `mcp://specs/007-newsletter-signup/section/Security` | +| Spec metadata | `mcp://specs//meta` | Frontmatter only — feature-type, status, owners, approvals | +| Plan and tasks | `mcp://specs//plan`, `…/tasks` | The change surface, test strategy, and gate results | +| Spec list | `mcp://specs?filter=` | Filtered list — e.g. `?feature-type=ui&status=approved` | +| ADRs | `mcp://adrs/` | Full ADR content | +| Admin docs | `mcp://docs/admin/` | Pre-implementable user-facing docs | +| Runbooks | `mcp://docs/runbooks/` | Operational docs | + +Don't expose code as MCP resources — that's what file system access is for. + +## Reference implementation (TypeScript) + +This is a minimal starter — adopters extend it for their needs. Uses `@modelcontextprotocol/sdk`. + +```typescript +// tools/spec-mcp-server/server.ts +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + ListResourcesRequestSchema, + ReadResourceRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import { readdir, readFile } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { join, basename } from 'node:path'; +import matter from 'gray-matter'; + +const SPECS_DIR = join(process.cwd(), 'specs'); + +// Spec folders (specs/NNN-/spec.md, plan.md, tasks.md) and legacy single-file specs (specs/.md). +async function listSpecs() { + const entries = await readdir(SPECS_DIR, { withFileTypes: true }); + return entries + .filter(entry => !entry.name.startsWith('_') && entry.name !== 'README.md') + .flatMap(entry => { + if (entry.isDirectory() && existsSync(join(SPECS_DIR, entry.name, 'spec.md'))) { + return [{ name: entry.name, folder: join(SPECS_DIR, entry.name) }]; + } + if (entry.isFile() && entry.name.endsWith('.md')) { + return [{ name: basename(entry.name, '.md'), folder: null }]; + } + return []; + }); +} + +function specFile(name, part = 'spec') { + const folder = join(SPECS_DIR, name); + return existsSync(folder) ? join(folder, `${part}.md`) : join(SPECS_DIR, `${name}.md`); +} + +// The text from a "## " line up to the next "## " heading. +function extractSection(content, heading) { + const lines = content.split('\n'); + const start = lines.findIndex(line => line.toLowerCase().startsWith(`## ${heading.toLowerCase()}`)); + if (start === -1) return null; + const end = lines.findIndex((line, index) => index > start && line.startsWith('## ')); + return lines.slice(start, end === -1 ? undefined : end).join('\n'); +} + +const server = new Server( + { name: 'spec-mcp-server', version: '0.2.0' }, + { capabilities: { resources: {} } }, +); + +server.setRequestHandler(ListResourcesRequestSchema, async () => { + const specs = await listSpecs(); + return { + resources: specs.flatMap(({ name, folder }) => [ + { uri: `mcp://specs/${name}`, name, mimeType: 'text/markdown' }, + { uri: `mcp://specs/${name}/meta`, name: `${name} (frontmatter)`, mimeType: 'application/json' }, + ...(folder + ? [ + { uri: `mcp://specs/${name}/plan`, name: `${name} (plan)`, mimeType: 'text/markdown' }, + { uri: `mcp://specs/${name}/tasks`, name: `${name} (tasks)`, mimeType: 'text/markdown' }, + ] + : []), + ]), + }; +}); + +server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + const uri = request.params.uri; + // In mcp://specs//, "specs" parses as the URL's host; the path starts at the folder. + const [, name, part, ...rest] = new URL(uri).pathname.split('/'); + const respond = (mimeType, text) => ({ contents: [{ uri, mimeType, text }] }); + + if (part === 'plan' || part === 'tasks') { + return respond('text/markdown', await readFile(specFile(name, part), 'utf-8')); + } + + const { data: frontmatter, content } = matter(await readFile(specFile(name), 'utf-8')); + if (part === 'meta') return respond('application/json', JSON.stringify(frontmatter, null, 2)); + if (part === 'section') { + const heading = decodeURIComponent(rest.join('/')); + return respond('text/markdown', extractSection(content, heading) ?? `Section "${heading}" not found in ${name}`); + } + return respond('text/markdown', content); +}); + +await server.connect(new StdioServerTransport()); +``` + +This is ~90 lines and supports listing spec folders (and legacy single-file specs), reading a spec, its plan and tasks, its frontmatter as JSON, and individual sections. Extend with: ADRs, runbooks, admin docs, search/filter, authenticated access, write capability if your team wants AI-driven spec edits via MCP (most teams don't — keep MCP read-only). + +## Wiring it into AI tools + +### Claude Code + +Add to `.mcp.json` at the repository root — project scope, so the team shares it and it's on in this +project only: + +```json +{ + "mcpServers": { + "specs": { + "command": "node", + "args": ["./tools/spec-mcp-server/dist/server.js"] + } + } +} +``` + +In Claude Code, the resources appear in the resource picker and can be referenced via `mcp://specs/` URIs. Tool definitions are deferred — only the resource names consume context until you read one. + +### Cursor + +Cursor supports MCP servers via the Settings → MCP Servers UI. Add the same command/args. Cursor's discovery mechanism is similar — resources surface in the AI's context-picker. + +### Antigravity / Gemini + +Check `GEMINI.md` for current MCP support. As of writing, Antigravity supports MCP servers via its `.agent/` configuration. Same command/args pattern. + +## Authentication & secrets + +- **Local-only servers** (stdio transport, runs as you): no auth needed; the MCP server has whatever filesystem access you have. +- **Network-exposed servers** (HTTP transport): use the MCP spec's authentication mechanisms (OAuth, API keys). Don't expose specs over the network without auth — they often contain client / business-sensitive details. +- **Read-only by default.** Don't grant the MCP server write access to your specs directory unless you have a specific reason. Spec edits should go through the normal `/aplyca-adf:write-spec` workflow with human review. + +## Cost implications + +MCP resource definitions are **deferred by default** in Claude Code — only the resource names consume context until you actually read one. This means exposing a 200-spec corpus via MCP costs roughly the same as exposing a 5-spec corpus, until the AI actually reads one. Big efficiency win vs. dumping all specs into CLAUDE.md. + +See `COST-MODEL.md` for the broader cost discipline. + +## What this framework's MCP integration is NOT + +- Not a hard dependency. The framework works without MCP. +- Not a replacement for file system access. Code, tests, and most docs are still read directly. +- Not a runtime API for the AI to modify specs. Keep MCP read-only; spec changes go through `/aplyca-adf:write-spec` with human approval. +- Not a code module that ships with the skeleton. The reference snippet above is illustrative — adopters implement and extend. + +## See also + +- [modelcontextprotocol.io](https://modelcontextprotocol.io) — the spec +- [Anthropic MCP guidance](https://platform.claude.com/docs/en/agents-and-tools/mcp) — Claude Code-specific docs +- [`SPEC-MODEL.md`](SPEC-MODEL.md) — the spec structure that determines what's worth exposing +- [`COST-MODEL.md`](COST-MODEL.md) — why MCP's deferred-by-default loading helps cost +- [`MEMORY-STRATEGY.md`](MEMORY-STRATEGY.md) — how MCP resources fit alongside other persistence layers diff --git a/plugins/aplyca-adf/docs/MEMORY-STRATEGY.md b/plugins/aplyca-adf/docs/MEMORY-STRATEGY.md new file mode 100644 index 0000000..d5f3af9 --- /dev/null +++ b/plugins/aplyca-adf/docs/MEMORY-STRATEGY.md @@ -0,0 +1,192 @@ +# Memory strategy + +How to decide where a piece of project knowledge belongs — `AGENTS.md`, `CLAUDE.md`, engineering rules, persistent memory, spec folders, decision records, reference pages, or the tracker. Each layer has a different cadence, scope, and load behavior; using the wrong one wastes tokens, bloats context, or causes facts to go stale. + +## The persistence layers + +This framework has **nine** places where project knowledge can live. Each has a different role. + +| Layer | Where | Loaded when | Change cadence | Owned by | +|---|---|---|---|---| +| **Non-negotiables** | `docs/CONSTITUTION.md` | Read before every spec, plan, and review; overrides everything below | Rare (amendments) | Tech lead + approvers | +| **Project identity & rules** | `AGENTS.md` (+ nested `AGENTS.md` per module) | Every session, every AI tool (Claude Code via the `@AGENTS.md` import); nested files when working in their folder | Slow (months) | Tech lead | +| **Tool-specific config** | `CLAUDE.md`, `GEMINI.md`, `.claude/settings.json`, hooks | Every session, by the matching tool | Slow (months) | Tech lead | +| **Engineering rules** | `.claude/rules/*.md` | When Claude reads a file matching the rule's `paths:` | Slow (months) | Tech lead | +| **Per-feature knowledge** | `specs/NNN-/` (spec, plan, tasks), plus the user-facing docs they produce | On demand when working on that feature | Per feature and per change request | The team | +| **Decisions** | `docs/architecture/decisions/` (ADRs) and `docs/process/` (PDRs) | On demand when revisiting a decision | Per decision (rare) | Architect / tech lead / team | +| **How subsystems work** | `docs/reference/` | On demand — one page per subsystem | When the subsystem changes (same PR) | The team | +| **Persistent memory** | Tool-specific memory (Claude Code: `~/.claude/projects//memory/`) | Just-in-time, by the AI | Fast (per learning) | The AI + the user | +| **Requirements & conversation** | The tracker (linked, never copied) | When a task is read | Continuous | Requester + team | + +Plus operational layers (out of scope for this doc): +- Runbooks (`docs/runbooks/`) — operational knowledge for incident response +- API contracts (`openapi/`, type definitions in code) + +## The decision tree + +``` +Is this knowledge about... ? +├── ...a principle no change may violate? +│ └── docs/CONSTITUTION.md (amended via a PDR, never casually) +│ +├── ...the project's identity, stack, or workflow conventions? +│ └── AGENTS.md (universal; nested AGENTS.md for one module) or CLAUDE.md / GEMINI.md (tool-specific) +│ +├── ...how to write/review code in a specific path? +│ └── .claude/rules/.md (path-scoped) +│ +├── ...a specific feature's requirements, behavior, design, or delivery evidence? +│ └── specs/NNN-/ (spec.md, plan.md, tasks.md — change requests amend the same folder) +│ +├── ...a significant technical decision (framework choice, integration pattern)? +│ └── docs/architecture/decisions/NNNN-.md (ADR) +│ +├── ...a decision about how the team works (gates, workflow, hand-offs)? +│ └── docs/process/NNNN-<title>.md (PDR) +│ +├── ...how a subsystem works at the code level? +│ └── docs/reference/<subsystem>.md +│ +├── ...a learned fact, gotcha, or pattern that recurs across features? +│ └── Memory (persistent, just-in-time loaded) +│ +└── ...something the AI should remember about how YOU prefer to work? + └── Memory (user-type entry) +``` + +## When to use memory specifically + +Memory is the right choice when **all of these** are true: + +1. The fact is **specific enough** that it's not a project-wide convention (those go in AGENTS.md / rules). +2. The fact is **recurring** — likely to come up across many sessions, not just once. +3. The fact would otherwise be **re-derived from scratch** each session (wasted tokens) or **forgotten** between sessions. +4. The fact is **stable enough** to outlive a single feature (specs handle per-feature facts). + +### Good memory entries + +- "The Contentful environment for staging is named `staging-2024`, not `staging`. The default in the SDK is wrong." (gotcha that recurs across features) +- "When working with the Mailchimp v3 API, batch operations cap at 500 entries per request — chunk larger inputs." (domain-specific quirk) +- "User prefers `pnpm` over `npm`; defaults to TypeScript strict mode; uses Vitest, not Jest." (user-type, persists preferences) +- "The marketing team approves copy changes; the editorial team approves article content. Different Slack channels." (project-type, useful when drafting comms) +- "Past incident: Vercel KV had cross-region inconsistency that broke our rate-limiter. Mitigation: include the region in the key." (project-type, captures hard-won knowledge) +- "Senior dev pushed back on adding Redux — said 'we already have React Query + URL state, don't need a state library'." (feedback-type, preserves a validated judgment) + +### Bad memory entries + +- "The `validateEmail` function is at `lib/newsletter/validate-email.ts`." (read the code; this rots immediately) +- "Specs go in the `specs/` directory." (project structure — belongs in AGENTS.md) +- "We decided PRs open as drafts." (a process decision — belongs in a PDR, reflected in AGENTS.md) +- "Tests go before implementation." (workflow rule — belongs in AGENTS.md / rules) +- "The newsletter signup spec has 5 ACs." (per-feature — belongs in the spec, will go stale as the spec evolves) +- "Today is 2026-04-28." (factual but ephemeral — re-derive each session) + +## Memory entry types + +This framework's memory system has four types. Choose the one that fits: + +| Type | What it captures | +|---|---| +| **`user`** | Who the user is, role, preferences, knowledge gaps | +| **`feedback`** | Guidance the user has given about how to work — both corrections AND validated successes | +| **`project`** | Ongoing work, decisions, gotchas, ambient knowledge specific to this project | +| **`reference`** | Pointers to where information lives in external systems (Linear projects, Slack channels, Grafana dashboards) | + +A memory file always has frontmatter: + +```yaml +--- +name: Short, specific name +description: One-line description (used to decide relevance later) +type: user | feedback | project | reference +--- + +The body of the memory. +``` + +For `feedback` and `project` types: lead with the rule/fact, then a `**Why:**` line and a `**How to apply:**` line. Knowing *why* lets future-you (or the AI) judge edge cases. + +## Memory hygiene + +Memory accumulates. Without hygiene, you end up with contradictory entries, stale references to renamed files, and a pile the AI ignores because it can't tell what's current. + +### Practices + +- **Update wrong memories rather than appending.** If a memory is now wrong (the rate limit changed, the API was deprecated), edit the existing memory file. Don't add a new one with contradicting information. +- **Remove memories that turned out to be wrong.** Better to forget than to mislead. +- **Quarterly review.** Walk through the memory directory; archive or delete entries that are no longer load-bearing. +- **Don't write memories about things the code shows.** Function locations, class names, file paths — these belong in `git grep`, not memory. Memories that quote code rot immediately. +- **Don't write memories for things you can derive in 10 seconds.** "The dev port is 3000" — if the AI can find that in `package.json`, don't memorize it. + +### Verifying before recommending from memory + +A memory that names a specific function, file, or flag is a claim that it existed *when the memory was written*. Before recommending action based on a memory: + +- If the memory names a file path, verify the file still exists. +- If the memory names a function, grep for it. +- If the user is about to act on the recommendation (not just asking about history), verify first. + +"The memory says X exists" is not the same as "X exists now." + +## What goes where: examples + +| Knowledge | Layer | Why | +|---|---|---| +| "This project uses Next.js 15 + Contentful + Vercel" | AGENTS.md | Project identity, stable, every session needs it | +| "All TypeScript files use strict mode; no `any`" | `.claude/rules/code-quality.md` | Engineering standard, path-scoped | +| "The newsletter signup form rate-limits to 10/IP/min" | `specs/007-newsletter-signup/spec.md` | Feature requirement, per spec folder | +| "We chose Mailchimp over SendGrid in Q4 2025 because of legacy list compatibility" | ADR | Significant decision, captures rationale for future readers | +| "Contentful's `staging-2024` env name is non-default; default in SDK is wrong" | Memory (project) | Recurring gotcha, not a per-feature concern, easy to forget | +| "User prefers terse PR descriptions; doesn't want emoji in commits" | Memory (user) | User-specific preference | +| "Use `pnpm` not `npm`" | Memory (user) OR AGENTS.md | If team-wide, AGENTS.md; if user-specific, memory | +| "Past incident: Vercel KV cross-region inconsistency; include region in key" | Memory (project) | Hard-won knowledge, recurs across rate-limit work | +| "Pipeline bugs are tracked in Linear project INGEST" | Memory (reference) | External-system pointer | + +## For consultancy / multi-project teams + +Teams that work on many projects (consultancies, agencies) have a unique consideration: **per-client knowledge** that doesn't belong in any specific client's repo but recurs across the consultancy's work. + +Recommended split: + +- **Per-client repo `CLAUDE.md`** — that client's stack, conventions, critical rules. Goes in their repo. +- **Per-client memory entries** (project-type) — gotchas specific to that client's setup. Stored in your local memory, scoped to their repo's project slug. +- **Cross-client memory entries** (reference or project) — recurring patterns (e.g., "Contentful gotchas that affect every Contentful project we build"). Stored at user-level memory, available across all projects. + +Example: +- `Our consultancy uses Next.js + Contentful + Vercel as the default stack` → cross-client (user-level memory) +- `Client X's Mailchimp instance has list ID 12345 (production) and 67890 (staging)` → per-client (project memory) +- `For Client Y, the editorial team prefers WhatsApp over Slack for content reviews` → per-client (project memory) +- `Contentful's staging environment naming convention varies per client; always confirm before assuming` → cross-client (user memory) + +## Memory and prompt caching + +Memory entries are loaded just-in-time when relevant — so they don't bloat every request. This is good for cache efficiency. + +But: **AGENTS.md and CLAUDE.md are loaded every session and form the cache prefix.** If you put recurring-but-volatile knowledge there instead of memory, every edit busts the cache (see [`COST-MODEL.md`](COST-MODEL.md)). + +Rule of thumb: **if you'd edit a fact more than once a quarter, it doesn't belong in AGENTS.md / CLAUDE.md / rules. It belongs in memory or a spec.** + +## When the AI asks "should I save this to memory?" + +The AI may proactively offer to save things. Heuristic for accepting: + +- ✓ "Save: this team prefers X over Y for [reason]" — capture validated preference +- ✓ "Save: gotcha — when calling API Z, watch for [edge case]" — recurring gotcha +- ✓ "Save: Linear project INGEST tracks pipeline bugs" — external-system reference +- ✘ "Save: today we worked on the newsletter spec" — ephemeral, doesn't help future sessions +- ✘ "Save: the validateEmail function is at lib/newsletter/" — code-derivable, will rot +- ✘ "Save: this PR is about adding topic selection" — captured in commit history already + +When in doubt: don't save. A small, sharp memory beats a bloated one the AI can't navigate. + +## See also + +- [`AGENTS.md`](../AGENTS.md) — project identity layer +- [`CLAUDE.md`](../CLAUDE.md) — Claude Code-specific tool config +- [`docs/SPEC-MODEL.md`](SPEC-MODEL.md) — per-feature knowledge layer +- [`docs/architecture/decisions/`](architecture/decisions/) — ADR layer +- [`docs/COST-MODEL.md`](COST-MODEL.md) — cache implications of where knowledge lives +- Tool-specific memory documentation: + - Claude Code: [memory tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool) + - Cursor: project rules + custom rules + - Antigravity: see `GEMINI.md` diff --git a/plugins/aplyca-adf/docs/SPEC-MODEL.md b/plugins/aplyca-adf/docs/SPEC-MODEL.md new file mode 100644 index 0000000..38fe822 --- /dev/null +++ b/plugins/aplyca-adf/docs/SPEC-MODEL.md @@ -0,0 +1,215 @@ +# Spec model + +This project uses a **multi-perspective spec model**: every feature's `spec.md` captures input from +all relevant roles in one document, with required sections enforced before the spec can go to +planning. The spec lives in a **spec folder** (`specs/NNN-<slug>/`) next to the `plan.md` that says +how it will be built and the `tasks.md` that breaks the plan into commits — see `specs/README.md` for +the process. + +| File | Answers | Written by | +|---|---|---| +| `spec.md` | WHAT and WHY — every role's requirements | `/aplyca-adf:write-spec` | +| `plan.md` | HOW — architecture, change surface, test strategy, documentation plan, assumptions | `/aplyca-adf:write-plan` | +| `tasks.md` | In what order, one commit at a time — each task names its test | `/aplyca-adf:write-plan` | + +## Why this model + +Most spec templates capture only what the business wants. Real features need input from multiple +roles — security, accessibility, design, deployment, documentation — and skipping any of them creates +the "we forgot about X" problem after launch: + +- The form that shipped without bot protection because security wasn't asked. +- The page that's invisible to screen readers because accessibility wasn't asked. +- The deploy that broke because nobody documented the new environment variable. +- The feature nobody knows how to use because the docs were "later". + +This model forces the conversation across all perspectives **before code is written**, while keeping +the spec readable and its diff useful. + +## Structure of spec.md + +Six parts. Sections are **required**, **conditional** (required when a frontmatter flag says so), or +**optional** (filled only when relevant). + +### Part 1 — Intent (required) + +| Section | Status | Owned by | +|---|---|---| +| Business | Required | Client / PM | +| Functional (user stories, numbered ACs, edge cases) | Required | Senior dev / tech lead | +| Out of scope | Required | Senior dev / tech lead | + +### Part 2 — User experience + +| Section | Status | Owned by | +|---|---|---| +| Design | Optional | Designer | +| Accessibility | Required if UI | A11y lead / designer | + +### Part 3 — Non-functional requirements + +| Section | Status | Owned by | +|---|---|---| +| Security | Required | Security lead / architect | +| Privacy | Required if `personal-data: yes` | Privacy / legal / tech lead | +| Performance | Optional | Tech lead | +| SEO | Optional | SEO / marketing | +| Analytics | Optional | Analytics lead / marketing | +| Localization | Optional | Localization lead / tech lead | + +### Part 4 — Constraints + +| Section | Status | Owned by | +|---|---|---| +| Constraints & prior decisions (business rules, compliance, ADRs and PDRs to respect, reasoned constraints on HOW) | Optional | Tech lead | + +The design itself — architecture, integrations, diagrams, the files that change — is not part of the +spec. It lives in `plan.md`, and is approved together with the spec at the approval gate. + +### Part 5 — Validation & delivery + +| Section | Status | Owned by | +|---|---|---| +| Testing | Required | QA / tech lead | +| Documentation (pre- and post-implementable) | Required | Tech writer / dev | +| Observability | Optional | DevOps / SRE | +| Deployment | Optional | DevOps | + +### Part 6 — Meta + +| Section | Status | Owned by | +|---|---|---| +| Clarifications | Required | Whoever wrote the spec | +| References | Optional | Whoever wrote the spec | + +### Change requests + +When delivered work changes, the same `spec.md` gets a `CR N` section — intent plus a Delivered → +Change table — and new acceptance criteria tagged `(CR N)`. Retired criteria are struck through, not +deleted. The folder stays the single record of the feature, before and after. + +## Mandatory enforcement + +`/aplyca-adf:write-spec` **refuses to hand a spec to planning** while a required section is empty: + +**Always required:** Business · Functional (at least one numbered AC) · Out of scope · Security · +Testing · Documentation (both subsections) · Clarifications (may be an empty list, but it exists). + +**Conditionally required:** +- **Accessibility** — when `feature-type` is `ui` or `mixed`. For `api`, `infra`, or `content`, mark `> Not applicable: [reason]`. +- **Privacy** — when `personal-data: yes`. Collecting an email address counts. + +**Approval** happens later, at the gate in `/aplyca-adf:write-plan`, once the change surface is known: the +developer signs off on scope, change surface, and assumptions together, and `status` becomes +`approved` with an `approvals:` line. + +## How to "fill" a section + +1. **Concrete content** — requirements, mockup links, environment variables. +2. **Standard rules apply** — `> Standard project [area] applies (see .claude/rules/[file].md). No additional requirements.` Common for Security and Accessibility on routine features. +3. **Not applicable** — `> Not applicable: [one-line reason]`, only when the section truly doesn't apply. + +All three count as filled. An empty section, or one holding only template text, blocks planning. + +## How the AI uses each section + +| Skill / agent | Reads | +|---|---| +| `/aplyca-adf:write-spec` | All sections — orchestrates filling them and enforces the required ones | +| `/aplyca-adf:write-plan` | All sections — turns them into the change surface, a test for every testable requirement, and doc tasks | +| `@aplyca-adf:spec-analyzer` | The whole folder — every AC → task → test, constitution conflicts, change-surface gaps, unstated assumptions | +| `/aplyca-adf:write-tests` | **Functional** ACs and edge cases, **Testing**, and testable criteria from **Security**, **Accessibility**, **Performance**, **Privacy**, **Analytics**, **Localization** | +| `/aplyca-adf:write-docs` | **Documentation → Pre-implementable**, via the plan's documentation plan; ACs and planned tests are the source of truth for every claim | +| `/aplyca-adf:implement` | All sections, through `tasks.md` — every task's code satisfies the requirements its ACs and sections carry | +| `/aplyca-adf:review` | All sections — checks each requirement was actually met, inside the approved change surface | + +## The Documentation section: pre-implementable vs post-implementable + +**Pre-implementable docs** (written via `/aplyca-adf:write-docs` before code): +- Admin and operator guides +- API contracts (OpenAPI, GraphQL schemas, public type signatures) +- End-user help and copy defaults (often seeded into a CMS) +- Public READMEs and SDK documentation + +**Post-implementable docs** (backfilled after code — they need real output): +- Runbooks with real metrics, dashboards, and logs +- Tutorials with real screenshots +- Troubleshooting guides built from real failure modes + +Both subsections must be filled or marked `Not applicable: [reason]`. + +## Which sections get filled for common feature types + +### Newsletter signup form (UI + form + third-party integration) + +Filled: Business, Functional, Out of scope, Design, Accessibility, Security, Privacy, Constraints & +prior decisions (subscribers live only in the email provider), Testing, Documentation, Deployment, +Clarifications, References. Not filled: Performance (default applies), SEO (form not SEO-relevant), +Analytics (separate task), Localization (English only this iteration), Observability (default +applies). + +### Health-check API endpoint (no UI, no personal data) + +Filled: Business, Functional, Out of scope, Security ("Standard applies"), Accessibility ("Not +applicable: no UI"), Testing, Documentation, Observability, Clarifications. + +### CMS content-model change (field rename, no UI change) + +Filled: Business, Functional, Out of scope, Accessibility ("Not applicable: no UI change"), Security +("Standard applies"), Testing, Documentation, Deployment (schema change ordered before the code +deploy), Clarifications. + +### Analytics tracking on existing pages (integration only) + +Filled: Business, Functional, Out of scope, Security ("Standard applies"), Accessibility ("Not +applicable: no new UI"), Privacy (tracking implications), Analytics, Testing, Documentation, +Deployment (env vars), Clarifications. + +## Owners and handoffs + +The `owners:` map in the frontmatter records who is responsible for each filled section: +reviewers know who to ask, partial updates (only Security changed) go to the right person, and +clarification questions route themselves. Role labels by default (`client`, `senior-dev`, +`designer`, `tech-lead`, `qa`, `tech-writer`, `devops`, `security-lead`, `a11y-lead`, `privacy`); +replace them with names if your team prefers. + +## Frontmatter reference + +```yaml +--- +title: "" +area: "" # e.g. "marketing", "auth", "checkout" +status: draft # draft → in-review → approved → implemented +feature-type: ui | api | infra | content | mixed +personal-data: yes | no # collecting email/name/IP/etc. counts as yes +tracker: "" # link to the requirement — never a copy of it +approvals: [] # "YYYY-MM-DD · <who> · initial scope" / "… · CR 1" +pull-requests: [] # "<url> · initial delivery" / "<url> · CR 1" +owners: + business: client + functional: senior-dev + # one entry per filled section +references: + figma: "" + adrs: [] + pdrs: [] + related-specs: [] +--- +``` + +## When the model gets in the way + +1. **Ceremony on changes with nothing to decide.** A typo, a version bump, or a precise adjustment + the requester already decided needs no spec folder — it takes the fast or careful lane + (`specs/README.md` § Lanes), with a light `CR N` entry when it changes recorded behavior. The test is "is there + anything to decide, and how risky is the area?", not "is it big?". +2. **Speculative filling.** Writing Performance and Deployment "just in case" is worse than leaving + them out. An absent optional section says "this didn't apply"; a guessed one says "we didn't + really think about it" while looking like we did. + +## Related + +- `specs/README.md` — the process: the lanes, the flow, the approval gate, change requests +- `specs/_templates/` — the files to copy (`spec.md`, `plan.md`, `tasks.md`) +- `docs/CONSTITUTION.md` — the gate every spec and plan is checked against +- `.claude/skills/write-spec/`, `write-plan/`, `implement/` — the playbooks diff --git a/plugins/aplyca-adf/hooks/session-context.sh b/plugins/aplyca-adf/hooks/session-context.sh index 96f1620..882e94d 100755 --- a/plugins/aplyca-adf/hooks/session-context.sh +++ b/plugins/aplyca-adf/hooks/session-context.sh @@ -112,4 +112,10 @@ if [ -x "$root/scripts/agent/worktree-new.sh" ]; then fi fi +# A packaged project reads the framework's reference docs from the aplyca-adf plugin, which links in +# the project's files only point at on GitHub (decision 0019). +if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -d "${CLAUDE_PLUGIN_ROOT}/docs" ] && [ ! -f "$root/docs/SPEC-MODEL.md" ]; then + echo "- The framework's reference docs — SPEC-MODEL.md, COST-MODEL.md, MEMORY-STRATEGY.md, MCP-INTEGRATION.md — are in ${CLAUDE_PLUGIN_ROOT}/docs/: read them there, not on GitHub." +fi + exit 0 diff --git a/plugins/aplyca-adf/skills/adopt/SKILL.md b/plugins/aplyca-adf/skills/adopt/SKILL.md index 4f1d441..c630faf 100644 --- a/plugins/aplyca-adf/skills/adopt/SKILL.md +++ b/plugins/aplyca-adf/skills/adopt/SKILL.md @@ -107,10 +107,11 @@ Present the table before going further. Wrong facts here poison every file downs ## Step 3 — Copy the skeleton and the chosen modules - **Ask how to install** (decisions 0016, 0018), with your recommendation: - - **Packaged** — the default. The skills, agents, workflows, and hook scripts come from the - `aplyca-adf` plugin, pinned to a release tag, and the repository commits only its own layer and - its modules: about 40 fewer files. People type `/aplyca-adf:triage`. Claude Code's cloud sessions - don't load it, and CI installs it first. It pins a release tag, v1.0.0 or later + - **Packaged** — the default. The skills, agents, workflows, hook scripts, and the framework's + reference docs come from the `aplyca-adf` plugin, pinned to a release tag, and the repository + commits only its own layer and its modules: about 50 fewer files. People type + `/aplyca-adf:triage`. Claude Code's cloud sessions don't load it, and CI installs it first. It + pins a release tag, v1.0.0 or later (`git ls-remote --tags https://github.com/aplyca/AgenticDevelopmentFramework 'v*'`); with none yet, say so and install committed. - **Committed** — recommend it instead when the team also uses another AI tool for the framework's @@ -119,6 +120,13 @@ Present the table before going further. Wrong facts here poison every file downs For packaged, follow `docs/SETUP.md` § Packaged install alongside the steps below: what to leave out, the settings, the names in `CLAUDE.md` and `DEV-SETUP.md`, the stamp, and the checks. + **The reference docs** (decision 0019) — `COST-MODEL.md`, `MCP-INTEGRATION.md`, + `MEMORY-STRATEGY.md`, `SPEC-MODEL.md` — stay out of `docs/` when the release carries them in + `<framework-root>/plugins/aplyca-adf/docs/`; an older release has none there, so copy them as + usual. Once the files are in, run + `python3 <framework-root>/scripts/link-reference-docs.py <repo> --packaged <tag>`: it points the + files that name a reference doc at the pinned release, and lists any other file that still names + one — one the team wrote follows by hand. - Copy `skeleton/` into the repo **without overwriting existing files**. For collisions (`README.md`, `CONTRIBUTING.md`, `.claude/settings.json` are common), merge: keep the project's content, add the skeleton's missing sections. @@ -159,7 +167,9 @@ Present the table before going further. Wrong facts here poison every file downs areas, as path globs), `ENV_TEMPLATE` if not auto-detected. - **`.claude/settings.json`** — extend `permissions.allow` with the repo's routine read-only commands; keep the `ask` rules for outward actions; for GitLab, add the `glab` equivalents of the `gh` rules. - Packaged: no `hooks` block, since the plugin wires them. + Packaged: no `hooks` block, since the plugin wires them, and + `Read(~/.claude/plugins/cache/aplyca/aplyca-adf/**)` in `permissions.allow`, so the plugin's skills + and agents open its reference docs without asking. - **`.claude/rules/*`** — `<!-- CUSTOMIZE -->` sections and `paths:` frontmatter to the real structure; delete rules that can't apply. - **`CONTRIBUTING.md`** — keep the branching model that matches (A or B), the status vocabulary, @@ -205,6 +215,7 @@ Run these checks and report each as PASS / GAP with one line of evidence: - [ ] `.claude/settings.json` is valid JSON (`python3 -m json.tool .claude/settings.json`) and every hook entry uses the nested `hooks` array - [ ] `.claude/settings.json` turns the plugin on for the project (`enabledPlugins` and the `aplyca` marketplace) — unless the team chose the local-only fallback - [ ] Packaged: in a new session, `/aplyca-adf:triage` is offered; run the hook samples below against the plugin's scripts with `CLAUDE_PROJECT_DIR` set (`docs/SETUP.md` § Packaged install) +- [ ] Packaged, on a release that carries the reference docs: none of them is in `docs/`, `link-reference-docs.py` run again rewrites nothing, and `permissions.allow` has the plugin's read rule - [ ] Hook scripts are executable and behave: pipe a sample event to each — e.g. `printf '{"cwd":".","tool_input":{"command":"git push origin main"}}' | .claude/hooks/guard-git.sh` exits 2; a `git status` event exits 0 - [ ] `CLAUDE.md` imports `AGENTS.md` (`@AGENTS.md`) — ask the user to start a new session and confirm with `/memory` that both load - [ ] No `[bracketed placeholders]` remain in `AGENTS.md`, `CONSTITUTION.md`, `CONTRIBUTING.md`; every unknown is a `TODO(team)` question diff --git a/plugins/aplyca-adf/skills/cost-report/SKILL.md b/plugins/aplyca-adf/skills/cost-report/SKILL.md index 438854d..2934ad9 100644 --- a/plugins/aplyca-adf/skills/cost-report/SKILL.md +++ b/plugins/aplyca-adf/skills/cost-report/SKILL.md @@ -25,7 +25,8 @@ stays local: the script reads `~/.claude/projects/` and prints a report; nothing 2. **Present the result:** totals, the most expensive sessions, the size bands, the model line, and the flags. -3. **Explain the drivers** with `docs/COST-MODEL.md` (in the project, or the framework's skeleton): +3. **Explain the drivers** with the cost model — `docs/COST-MODEL.md` in a committed project, the + plugin's `${CLAUDE_PLUGIN_ROOT}/docs/COST-MODEL.md` in a packaged one: - **Calls × context** — cost grows with the number of steps and with how much each step re-reads. - **The model** — the `on sonnet` column re-prices an Opus or Fable session's tokens at Sonnet's prices. Where the session's work had a clear spec and a way to check it — the fast and careful diff --git a/plugins/aplyca-adf/skills/handoff/SKILL.md b/plugins/aplyca-adf/skills/handoff/SKILL.md index 8e42afd..6941556 100644 --- a/plugins/aplyca-adf/skills/handoff/SKILL.md +++ b/plugins/aplyca-adf/skills/handoff/SKILL.md @@ -6,6 +6,8 @@ argument-hint: "[who or where it goes — a teammate, a fresh session, another m > **Step 0 — which copy.** This is the packaged copy ([decision 0016](https://github.com/aplyca/AgenticDevelopmentFramework/blob/main/docs/decisions/0016-packaged-install.md)). Unless this project's `CLAUDE.md` says "This project uses the packaged install", stop here: open `.claude/skills/handoff/SKILL.md` and follow that file instead — it's the version this project upgraded to. If it doesn't exist, the project doesn't use this skill: say so and stop. +> **The reference docs this file names are the plugin's copies,** in `${CLAUDE_PLUGIN_ROOT}/docs/` — outside this project, which keeps none in its own `docs/`. Read them at the full paths given. + # Handoff A handoff lets someone with no access to this conversation continue the work. Everything worth @@ -18,7 +20,7 @@ incomplete handoff strands whoever receives it: they either ask, or guess. 1. **Check that the work travels.** A handoff is for work that leaves this session: to a teammate, to another machine or tool, or to a fresh session after the approval gate (in the full lane, the spec folder already carries everything — point at it). Work that stays here needs no handoff: - continue, or `/compact` with what the next phase needs (`docs/COST-MODEL.md` § Between phases). + continue, or `/compact` with what the next phase needs (`${CLAUDE_PLUGIN_ROOT}/docs/COST-MODEL.md` § Between phases). A side task found mid-work isn't a handoff either: note it for the developer — or, with the parallel-agents module, `/dispatch` it to its own worktree. diff --git a/plugins/aplyca-adf/skills/orchestrate/SKILL.md b/plugins/aplyca-adf/skills/orchestrate/SKILL.md index c005f7c..cd6627f 100644 --- a/plugins/aplyca-adf/skills/orchestrate/SKILL.md +++ b/plugins/aplyca-adf/skills/orchestrate/SKILL.md @@ -6,6 +6,8 @@ argument-hint: "[review | investigate | pre-commit | custom <description>]" > **Step 0 — which copy.** This is the packaged copy ([decision 0016](https://github.com/aplyca/AgenticDevelopmentFramework/blob/main/docs/decisions/0016-packaged-install.md)). Unless this project's `CLAUDE.md` says "This project uses the packaged install", stop here: open `.claude/skills/orchestrate/SKILL.md` and follow that file instead — it's the version this project upgraded to. If it doesn't exist, the project doesn't use this skill: say so and stop. +> **The reference docs this file names are the plugin's copies,** in `${CLAUDE_PLUGIN_ROOT}/docs/` — outside this project, which keeps none in its own `docs/`. Read them at the full paths given. + # Orchestrate (Parallel Multi-Agent Coordination) Dispatch specialized agents in parallel for analytical tasks where independent perspectives add value. The skill plans which agents to run, runs them in parallel where dependencies allow, and synthesizes findings into a unified report. @@ -54,7 +56,7 @@ Dispatch specialized agents in parallel for analytical tasks where independent p 4. **Determine model tiering** — each agent has a default model alias in its `agent.md` frontmatter. Don't override unless you have a specific reason. The defaults already tier sensibly: - `@aplyca-adf:code-reviewer`, `@aplyca-adf:security-reviewer`, `@aplyca-adf:ux-reviewer`, `@aplyca-adf:architect` → `haiku` (well-bounded review) - `@aplyca-adf:spec-writer`, `@aplyca-adf:test-runner`, `@aplyca-adf:debugger`, `@aplyca-adf:spec-analyzer` → `sonnet` (reasoning-heavy) - See `docs/COST-MODEL.md` for the full per-agent recommendations and trade-offs. + See `${CLAUDE_PLUGIN_ROOT}/docs/COST-MODEL.md` for the full per-agent recommendations and trade-offs. 5. **Present the orchestration plan**: ``` @@ -114,7 +116,7 @@ Orchestration costs more than `/aplyca-adf:review` because each agent has its ow - `/aplyca-adf:review` (single context): ~5-10k input + ~2-5k output, mostly cached - `/aplyca-adf:orchestrate review` (3 parallel agents, Haiku-tier): ~15-25k input + ~5-10k output (each agent independently) -Roughly 2-3× the cost. Worth it for high-stakes diffs; overkill for trivial ones. See `docs/COST-MODEL.md` for the full cost model. +Roughly 2-3× the cost. Worth it for high-stakes diffs; overkill for trivial ones. See `${CLAUDE_PLUGIN_ROOT}/docs/COST-MODEL.md` for the full cost model. ## Verification diff --git a/plugins/aplyca-adf/skills/spec-workflow/SKILL.md b/plugins/aplyca-adf/skills/spec-workflow/SKILL.md index 3f92670..3736cd2 100644 --- a/plugins/aplyca-adf/skills/spec-workflow/SKILL.md +++ b/plugins/aplyca-adf/skills/spec-workflow/SKILL.md @@ -5,6 +5,8 @@ description: Reference for how work flows in this project — the fast, careful, > **Step 0 — which copy.** This is the packaged copy ([decision 0016](https://github.com/aplyca/AgenticDevelopmentFramework/blob/main/docs/decisions/0016-packaged-install.md)). Unless this project's `CLAUDE.md` says "This project uses the packaged install", stop here: open `.claude/skills/spec-workflow/SKILL.md` and follow that file instead — it's the version this project upgraded to. If it doesn't exist, the project doesn't use this skill: say so and stop. +> **The reference docs this file names are the plugin's copies,** in `${CLAUDE_PLUGIN_ROOT}/docs/` — outside this project, which keeps none in its own `docs/`. Read them at the full paths given. + # Development Workflows Every task starts with **triage** (`/aplyca-adf:triage`): read it in full, then decide the deliverable (an @@ -122,4 +124,4 @@ teammate, another machine, or a fresh session is `/aplyca-adf:handoff`: pointers The developer can ask for more care without changing the lane — questions before any code, `/aplyca-adf:evaluate` to compare designs, a higher effort level or `/model opus`, extra tests, -`@aplyca-adf:security-reviewer`, `/aplyca-adf:deep-review`. Each costs differently (`docs/COST-MODEL.md` § Effort). +`@aplyca-adf:security-reviewer`, `/aplyca-adf:deep-review`. Each costs differently (`${CLAUDE_PLUGIN_ROOT}/docs/COST-MODEL.md` § Effort). diff --git a/plugins/aplyca-adf/skills/upgrade/SKILL.md b/plugins/aplyca-adf/skills/upgrade/SKILL.md index d1a7835..7eb13d3 100644 --- a/plugins/aplyca-adf/skills/upgrade/SKILL.md +++ b/plugins/aplyca-adf/skills/upgrade/SKILL.md @@ -91,13 +91,15 @@ ask, and never discard it. - **Committed → packaged** — recommend it to a team that works in Claude Code only, since packaged is the default for new projects: remove the skills, agents, - workflows, and hook scripts the plugin carries — only those unchanged since OLD_SHA; one the team - edited stays, under a name of its own, or goes upstream — and the `hooks` block. Add the pinned - marketplace and `aplyca-adf`, the names note in `CLAUDE.md`, the full names in `DEV-SETUP.md`'s key - commands, and `install: packaged` in the stamp. + workflows, hook scripts, and reference docs the plugin carries — only those unchanged since OLD_SHA; + one the team edited stays, under a name of its own, or goes upstream — and the `hooks` block. Add + the pinned marketplace and `aplyca-adf`, the names note in `CLAUDE.md`, the full names in + `DEV-SETUP.md`'s key commands, and `install: packaged` in the stamp, then add the plugin's read + rule and run `link-reference-docs.py --packaged`, both in Step 5. - **Packaged → committed**, when the team adds another AI tool or needs Claude Code's cloud sessions: - copy the machinery and the `hooks` block back, and remove `install: packaged`, the names note, and - the `aplyca-adf:` prefix in `DEV-SETUP.md`. + copy the machinery, the reference docs, and the `hooks` block back, remove `install: packaged`, the + names note, the plugin's read rule, and the `aplyca-adf:` prefix in `DEV-SETUP.md`, and run + `python3 <framework-root>/scripts/link-reference-docs.py <repo> --committed`. Keep `aplyca-adf` turned on and pinned, for `/aplyca-adf:upgrade`. - **Record the switch** — it changes how the team works — as a process decision in the same pull request: the next `docs/process/NNNN-<slug>.md` from `docs/process/0000-pdr-template.md`, with its @@ -126,12 +128,13 @@ teammates still on the old name have no `/upgrade`. `git -C <framework-root> diff --name-status OLD_SHA NEW_SHA -- skeleton/ modules/<each installed module>/files/` gives the changed set (module paths map into the repo by dropping `modules/<name>/files/`). In a packaged project, leave out what the plugin carries — `.claude/skills/` (module skills stay), -`.claude/agents/`, `.claude/workflows/`, and `.claude/hooks/` except `config.sh` — and never add a -`hooks` block to the settings. Classify per the taxonomy in `docs/UPGRADING.md`: +`.claude/agents/`, `.claude/workflows/`, `.claude/hooks/` except `config.sh`, and, when the new +release carries them in `<framework-root>/plugins/aplyca-adf/docs/`, the reference docs in `docs/` +(decision 0019) — and never add a `hooks` block to the settings. Classify per the taxonomy in `docs/UPGRADING.md`: | Bucket | Typical contents | Action | |---|---|---| -| **Safe to overwrite** | `.claude/skills/*`, `.claude/agents/*`, `.claude/workflows/*`, hook scripts and helpers (everything in `.claude/hooks/` but `config.sh`), universal rules, framework reference docs, `specs/_templates/*` (if unmodified), `docs/process/0000-pdr-template.md`, module scripts | Copy verbatim from the new version | +| **Safe to overwrite** | `.claude/skills/*`, `.claude/agents/*`, `.claude/workflows/*`, hook scripts and helpers (everything in `.claude/hooks/` but `config.sh`), universal rules, the framework reference docs (`docs/COST-MODEL.md`, `MCP-INTEGRATION.md`, `MEMORY-STRATEGY.md`, `SPEC-MODEL.md`), `specs/_templates/*` (if unmodified), `docs/process/0000-pdr-template.md`, module scripts | Copy verbatim from the new version | | **Merge required** | `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `CONTRIBUTING.md`, `.claude/settings.json`, `.claude/hooks/config.sh`, customizable rules, `.claudeignore`, `docs/CONSTITUTION.md`, `specs/README.md`, `docs/process/README.md`, `docs/reference/README.md`, `docs/TRACKER-INTEGRATION.md`, `docs/getting-started/DEV-SETUP.md`, module config (`worktree.conf`, the PR template, `branch-policy.yml`, `.githooks/pre-push`) | 3-way merge: reapply the project's customizations on top of the new template | | **Project-owned** | Spec folders and legacy specs, ADRs, PDRs, project docs, `docs/reference/*` pages, everything the team authored | Never touched | @@ -165,8 +168,17 @@ vs the OLD_SHA version) and confirm they will survive. Wait for approval. - The plugin setting, when the developer accepted it: merge both entries into `.claude/settings.json`. - Pin the new release: set the marketplace's `"ref"` in `.claude/settings.json` to `v<X.Y.Z>` (add it if the project has none). In a packaged project that one line upgrades the plugin's skills, agents, - workflows, and hooks; in a committed one it keeps the plugin's copies at the same release as the - committed files. + workflows, hooks, and reference docs; in a committed one it keeps the plugin's copies at the same + release as the committed files. +- Packaged, on a release that carries the reference docs: run + `python3 <framework-root>/scripts/link-reference-docs.py <repo> --packaged v<X.Y.Z>`. It points the + files that name a reference doc at the new release, and lists the other files that still name one + and the reference docs still in `docs/`. A copy unchanged since OLD_SHA goes; one the team edited + is the developer's call — under a name of its own, or the committed install. Add + `Read(~/.claude/plugins/cache/aplyca/aplyca-adf/**)` to `permissions.allow` in + `.claude/settings.json` if it isn't there: the plugin's skills and agents read the docs from its + folder, and Claude Code asks first without it. Add the last sentence of `docs/SETUP.md`'s names + note to the project's, if it lacks it. - Restamp: `Skeleton source:` → `<new version> · NEW_SHA (<date>) · modules: <list>` — the list includes the new ones; a packaged project keeps `· install: packaged`. diff --git a/plugins/aplyca-adf/skills/write-spec/SKILL.md b/plugins/aplyca-adf/skills/write-spec/SKILL.md index 14086a4..95a52f0 100644 --- a/plugins/aplyca-adf/skills/write-spec/SKILL.md +++ b/plugins/aplyca-adf/skills/write-spec/SKILL.md @@ -6,9 +6,11 @@ argument-hint: "[feature description, tracker link, or spec folder to amend]" > **Step 0 — which copy.** This is the packaged copy ([decision 0016](https://github.com/aplyca/AgenticDevelopmentFramework/blob/main/docs/decisions/0016-packaged-install.md)). Unless this project's `CLAUDE.md` says "This project uses the packaged install", stop here: open `.claude/skills/write-spec/SKILL.md` and follow that file instead — it's the version this project upgraded to. If it doesn't exist, the project doesn't use this skill: say so and stop. +> **The reference docs this file names are the plugin's copies,** in `${CLAUDE_PLUGIN_ROOT}/docs/` — outside this project, which keeps none in its own `docs/`. Read them at the full paths given. + # Write Spec -Write `specs/NNN-<slug>/spec.md` using the multi-perspective spec model (`docs/SPEC-MODEL.md`): the +Write `specs/NNN-<slug>/spec.md` using the multi-perspective spec model (`${CLAUDE_PLUGIN_ROOT}/docs/SPEC-MODEL.md`): the WHAT and WHY from every relevant role, in one document, with required sections enforced. The HOW comes next, in `plan.md`, from `/aplyca-adf:write-plan` — which also holds the approval gate. diff --git a/plugins/aplyca-adf/workflows/deep-spec-analysis.js b/plugins/aplyca-adf/workflows/deep-spec-analysis.js index 7cc5324..bbdfad9 100644 --- a/plugins/aplyca-adf/workflows/deep-spec-analysis.js +++ b/plugins/aplyca-adf/workflows/deep-spec-analysis.js @@ -11,6 +11,10 @@ export const meta = { const READ_ONLY = 'Do not modify, create, or delete any file. Read files and run read-only commands (git log, grep) only.' +// The spec model the lenses check against. The aplyca-adf plugin's copy carries the model's text here +// instead: a packaged project keeps no copy of it in docs/ (decision 0019). +const SPEC_MODEL = "The spec model (SPEC-MODEL.md), which these checks follow:\n\n# Spec model\n\nThis project uses a **multi-perspective spec model**: every feature's `spec.md` captures input from\nall relevant roles in one document, with required sections enforced before the spec can go to\nplanning. The spec lives in a **spec folder** (`specs/NNN-<slug>/`) next to the `plan.md` that says\nhow it will be built and the `tasks.md` that breaks the plan into commits — see `specs/README.md` for\nthe process.\n\n| File | Answers | Written by |\n|---|---|---|\n| `spec.md` | WHAT and WHY — every role's requirements | `/aplyca-adf:write-spec` |\n| `plan.md` | HOW — architecture, change surface, test strategy, documentation plan, assumptions | `/aplyca-adf:write-plan` |\n| `tasks.md` | In what order, one commit at a time — each task names its test | `/aplyca-adf:write-plan` |\n\n## Why this model\n\nMost spec templates capture only what the business wants. Real features need input from multiple\nroles — security, accessibility, design, deployment, documentation — and skipping any of them creates\nthe \"we forgot about X\" problem after launch:\n\n- The form that shipped without bot protection because security wasn't asked.\n- The page that's invisible to screen readers because accessibility wasn't asked.\n- The deploy that broke because nobody documented the new environment variable.\n- The feature nobody knows how to use because the docs were \"later\".\n\nThis model forces the conversation across all perspectives **before code is written**, while keeping\nthe spec readable and its diff useful.\n\n## Structure of spec.md\n\nSix parts. Sections are **required**, **conditional** (required when a frontmatter flag says so), or\n**optional** (filled only when relevant).\n\n### Part 1 — Intent (required)\n\n| Section | Status | Owned by |\n|---|---|---|\n| Business | Required | Client / PM |\n| Functional (user stories, numbered ACs, edge cases) | Required | Senior dev / tech lead |\n| Out of scope | Required | Senior dev / tech lead |\n\n### Part 2 — User experience\n\n| Section | Status | Owned by |\n|---|---|---|\n| Design | Optional | Designer |\n| Accessibility | Required if UI | A11y lead / designer |\n\n### Part 3 — Non-functional requirements\n\n| Section | Status | Owned by |\n|---|---|---|\n| Security | Required | Security lead / architect |\n| Privacy | Required if `personal-data: yes` | Privacy / legal / tech lead |\n| Performance | Optional | Tech lead |\n| SEO | Optional | SEO / marketing |\n| Analytics | Optional | Analytics lead / marketing |\n| Localization | Optional | Localization lead / tech lead |\n\n### Part 4 — Constraints\n\n| Section | Status | Owned by |\n|---|---|---|\n| Constraints & prior decisions (business rules, compliance, ADRs and PDRs to respect, reasoned constraints on HOW) | Optional | Tech lead |\n\nThe design itself — architecture, integrations, diagrams, the files that change — is not part of the\nspec. It lives in `plan.md`, and is approved together with the spec at the approval gate.\n\n### Part 5 — Validation & delivery\n\n| Section | Status | Owned by |\n|---|---|---|\n| Testing | Required | QA / tech lead |\n| Documentation (pre- and post-implementable) | Required | Tech writer / dev |\n| Observability | Optional | DevOps / SRE |\n| Deployment | Optional | DevOps |\n\n### Part 6 — Meta\n\n| Section | Status | Owned by |\n|---|---|---|\n| Clarifications | Required | Whoever wrote the spec |\n| References | Optional | Whoever wrote the spec |\n\n### Change requests\n\nWhen delivered work changes, the same `spec.md` gets a `CR N` section — intent plus a Delivered →\nChange table — and new acceptance criteria tagged `(CR N)`. Retired criteria are struck through, not\ndeleted. The folder stays the single record of the feature, before and after.\n\n## Mandatory enforcement\n\n`/aplyca-adf:write-spec` **refuses to hand a spec to planning** while a required section is empty:\n\n**Always required:** Business · Functional (at least one numbered AC) · Out of scope · Security ·\nTesting · Documentation (both subsections) · Clarifications (may be an empty list, but it exists).\n\n**Conditionally required:**\n- **Accessibility** — when `feature-type` is `ui` or `mixed`. For `api`, `infra`, or `content`, mark `> Not applicable: [reason]`.\n- **Privacy** — when `personal-data: yes`. Collecting an email address counts.\n\n**Approval** happens later, at the gate in `/aplyca-adf:write-plan`, once the change surface is known: the\ndeveloper signs off on scope, change surface, and assumptions together, and `status` becomes\n`approved` with an `approvals:` line.\n\n## How to \"fill\" a section\n\n1. **Concrete content** — requirements, mockup links, environment variables.\n2. **Standard rules apply** — `> Standard project [area] applies (see .claude/rules/[file].md). No additional requirements.` Common for Security and Accessibility on routine features.\n3. **Not applicable** — `> Not applicable: [one-line reason]`, only when the section truly doesn't apply.\n\nAll three count as filled. An empty section, or one holding only template text, blocks planning.\n\n## How the AI uses each section\n\n| Skill / agent | Reads |\n|---|---|\n| `/aplyca-adf:write-spec` | All sections — orchestrates filling them and enforces the required ones |\n| `/aplyca-adf:write-plan` | All sections — turns them into the change surface, a test for every testable requirement, and doc tasks |\n| `@aplyca-adf:spec-analyzer` | The whole folder — every AC → task → test, constitution conflicts, change-surface gaps, unstated assumptions |\n| `/aplyca-adf:write-tests` | **Functional** ACs and edge cases, **Testing**, and testable criteria from **Security**, **Accessibility**, **Performance**, **Privacy**, **Analytics**, **Localization** |\n| `/aplyca-adf:write-docs` | **Documentation → Pre-implementable**, via the plan's documentation plan; ACs and planned tests are the source of truth for every claim |\n| `/aplyca-adf:implement` | All sections, through `tasks.md` — every task's code satisfies the requirements its ACs and sections carry |\n| `/aplyca-adf:review` | All sections — checks each requirement was actually met, inside the approved change surface |\n\n## The Documentation section: pre-implementable vs post-implementable\n\n**Pre-implementable docs** (written via `/aplyca-adf:write-docs` before code):\n- Admin and operator guides\n- API contracts (OpenAPI, GraphQL schemas, public type signatures)\n- End-user help and copy defaults (often seeded into a CMS)\n- Public READMEs and SDK documentation\n\n**Post-implementable docs** (backfilled after code — they need real output):\n- Runbooks with real metrics, dashboards, and logs\n- Tutorials with real screenshots\n- Troubleshooting guides built from real failure modes\n\nBoth subsections must be filled or marked `Not applicable: [reason]`.\n\n## Which sections get filled for common feature types\n\n### Newsletter signup form (UI + form + third-party integration)\n\nFilled: Business, Functional, Out of scope, Design, Accessibility, Security, Privacy, Constraints &\nprior decisions (subscribers live only in the email provider), Testing, Documentation, Deployment,\nClarifications, References. Not filled: Performance (default applies), SEO (form not SEO-relevant),\nAnalytics (separate task), Localization (English only this iteration), Observability (default\napplies).\n\n### Health-check API endpoint (no UI, no personal data)\n\nFilled: Business, Functional, Out of scope, Security (\"Standard applies\"), Accessibility (\"Not\napplicable: no UI\"), Testing, Documentation, Observability, Clarifications.\n\n### CMS content-model change (field rename, no UI change)\n\nFilled: Business, Functional, Out of scope, Accessibility (\"Not applicable: no UI change\"), Security\n(\"Standard applies\"), Testing, Documentation, Deployment (schema change ordered before the code\ndeploy), Clarifications.\n\n### Analytics tracking on existing pages (integration only)\n\nFilled: Business, Functional, Out of scope, Security (\"Standard applies\"), Accessibility (\"Not\napplicable: no new UI\"), Privacy (tracking implications), Analytics, Testing, Documentation,\nDeployment (env vars), Clarifications.\n\n## Owners and handoffs\n\nThe `owners:` map in the frontmatter records who is responsible for each filled section:\nreviewers know who to ask, partial updates (only Security changed) go to the right person, and\nclarification questions route themselves. Role labels by default (`client`, `senior-dev`,\n`designer`, `tech-lead`, `qa`, `tech-writer`, `devops`, `security-lead`, `a11y-lead`, `privacy`);\nreplace them with names if your team prefers.\n\n## Frontmatter reference\n\n```yaml\n---\ntitle: \"\"\narea: \"\" # e.g. \"marketing\", \"auth\", \"checkout\"\nstatus: draft # draft → in-review → approved → implemented\nfeature-type: ui | api | infra | content | mixed\npersonal-data: yes | no # collecting email/name/IP/etc. counts as yes\ntracker: \"\" # link to the requirement — never a copy of it\napprovals: [] # \"YYYY-MM-DD · <who> · initial scope\" / \"… · CR 1\"\npull-requests: [] # \"<url> · initial delivery\" / \"<url> · CR 1\"\nowners:\n business: client\n functional: senior-dev\n # one entry per filled section\nreferences:\n figma: \"\"\n adrs: []\n pdrs: []\n related-specs: []\n---\n```\n\n## When the model gets in the way\n\n1. **Ceremony on changes with nothing to decide.** A typo, a version bump, or a precise adjustment\n the requester already decided needs no spec folder — it takes the fast or careful lane\n (`specs/README.md` § Lanes), with a light `CR N` entry when it changes recorded behavior. The test is \"is there\n anything to decide, and how risky is the area?\", not \"is it big?\".\n2. **Speculative filling.** Writing Performance and Deployment \"just in case\" is worse than leaving\n them out. An absent optional section says \"this didn't apply\"; a guessed one says \"we didn't\n really think about it\" while looking like we did.\n\n## Related\n\n- `specs/README.md` — the process: the lanes, the flow, the approval gate, change requests\n- `specs/_templates/` — the files to copy (`spec.md`, `plan.md`, `tasks.md`)\n- `docs/CONSTITUTION.md` — the gate every spec and plan is checked against\n- `.claude/skills/write-spec/`, `write-plan/`, `implement/` — the playbooks\n" + const LOCATE_SCHEMA = { type: 'object', required: ['specFolder', 'status', 'isChangeRequest', 'summary'], @@ -66,7 +70,7 @@ if (!located || !located.specFolder) { } const folder = located.specFolder -const context = `Spec folder: ${folder} (status: ${located.status}${located.isChangeRequest ? ', latest section is a change request — analyze the CR against what was delivered' : ''}). Read spec.md, plan.md and tasks.md in full, plus docs/CONSTITUTION.md, specs/README.md and docs/SPEC-MODEL.md. ${READ_ONLY} +const context = `Spec folder: ${folder} (status: ${located.status}${located.isChangeRequest ? ', latest section is a change request — analyze the CR against what was delivered' : ''}). Read spec.md, plan.md and tasks.md in full, plus docs/CONSTITUTION.md and specs/README.md. ${SPEC_MODEL} ${READ_ONLY} Report only gaps you can evidence. Severity: critical = blocks approval (constitution conflict, AC with no task or test, missing layer in the change surface, invented requirement); gap = should be fixed before approval; question = only a human can answer.` const LENSES = [ @@ -74,7 +78,7 @@ const LENSES = [ { key: 'change-surface', prompt: 'CHANGE SURFACE: search the code for the entities, routes, components, tables and functions the plan changes. Find callers, shared components, configuration, migrations, access policies and tests that plan.md does not list; list unlisted consumers of shared code; flag files named in tasks.md that are missing from the change surface table.' }, { key: 'constitution', prompt: 'CONSTITUTION AND DECISIONS: read every principle in docs/CONSTITUTION.md and every accepted ADR (docs/architecture/decisions/) and PDR (docs/process/) the plan touches against the spec folder. Authorization changes, append-only history, dependencies, silenced types, accessibility, secrets.' }, { key: 'consistency', prompt: 'CONSISTENCY AND ASSUMPTIONS: contradictions between spec, plan and tasks; requirements nothing backs (no tracker link, clarification, or stated requirement — plausible additions are the most dangerous); assumptions the plan relies on but does not list; open questions still open; for a change request, whether the Delivered → Change table matches what the spec records as delivered.' }, - { key: 'perspectives', prompt: 'MULTI-PERSPECTIVE COMPLETENESS: required sections filled for this feature-type and personal-data value (docs/SPEC-MODEL.md); "Not applicable" used with a real reason; no speculative optional sections; acceptance criteria numbered and independently testable; edge cases cover empty, error and boundary states.' }, + { key: 'perspectives', prompt: 'MULTI-PERSPECTIVE COMPLETENESS: required sections filled for this feature-type and personal-data value (the spec model); "Not applicable" used with a real reason; no speculative optional sections; acceptance criteria numbered and independently testable; edge cases cover empty, error and boundary states.' }, ] const analyzed = await pipeline( diff --git a/scripts/build-aplyca-adf.sh b/scripts/build-aplyca-adf.sh index 1c330bf..e21c650 100755 --- a/scripts/build-aplyca-adf.sh +++ b/scripts/build-aplyca-adf.sh @@ -2,7 +2,7 @@ # # Builds the packaged half of plugins/aplyca-adf (docs/decisions/0016-packaged-install.md) from # skeleton/.claude/: the core skills, the agents as flat files, the workflows, and the hook scripts -# with their hooks.json. Claude Code puts everything a plugin carries under the plugin's name, so the +# with their hooks.json — and, from skeleton/docs/, the framework's reference docs (decision 0019). Claude Code puts everything a plugin carries under the plugin's name, so the # copies name each other that way: `/triage` becomes `/aplyca-adf:triage`, `@code-reviewer` becomes # `@aplyca-adf:code-reviewer`. The hooks read the project's .claude/hooks/config.sh. The copies act only # in a packaged project: each skill and agent opens with a step that hands over to the committed copy @@ -37,6 +37,13 @@ command = re.compile(r"(?<![\w./@:-])/(" + "|".join(map(re.escape, skills + work agent = re.compile(r"(?<![\w./-])@(" + "|".join(map(re.escape, agents)) + r")(?![\w-])") +# The framework's reference docs (decision 0019): generic, never edited by a project, so a packaged +# project reads them from the plugin. Its skills and agents name the plugin's copy; Claude Code fills in +# ${CLAUDE_PLUGIN_ROOT} when it loads them. +REFERENCE_DOCS = ["COST-MODEL", "MCP-INTEGRATION", "MEMORY-STRATEGY", "SPEC-MODEL"] +reference = re.compile(r"(?<![\w/.-])docs/(" + "|".join(REFERENCE_DOCS) + r")\.md") + + def rename(text): text = command.sub(lambda m: f"/{PLUGIN}:{m.group(1)}", text) return agent.sub(lambda m: f"@{PLUGIN}:{m.group(1)}", text) @@ -47,7 +54,8 @@ def copy(source, target, executable=False, kind=None, name=None): with open(source, encoding="utf-8") as f: text = rename(f.read()) if kind: - text = hand_over(text, kind, name) + named = reference.sub(r"${CLAUDE_PLUGIN_ROOT}/docs/\1.md", text) + text = hand_over(named, kind, name, docs=named != text) with open(target, "w", encoding="utf-8") as f: f.write(text) os.chmod(target, 0o755 if executable else 0o644) @@ -64,10 +72,16 @@ HANDOVER = { } -def hand_over(text, kind, name): +# A model reading a list of project paths takes one outside the project for another project path +# unless it's told: an agent read docs/SPEC-MODEL.md in the project instead of the plugin's copy. +DOCS_NOTE = ("> **The reference docs this file names are the plugin's copies,** in `${CLAUDE_PLUGIN_ROOT}/docs/` — " + "outside this project, which keeps none in its own `docs/`. Read them at the full paths given.\n\n") + + +def hand_over(text, kind, name, docs=False): # After the frontmatter, so the name and description still come first. head, sep, body = text.partition("\n---\n") - return head + sep + "\n" + HANDOVER[kind].format(name=name) + body.lstrip("\n") + return head + sep + "\n" + HANDOVER[kind].format(name=name) + (DOCS_NOTE if docs else "") + body.lstrip("\n") listing = os.path.join(out, ".generated") @@ -89,8 +103,24 @@ for name in skills: generated.append(f"skills/{name}") for name in agents: copy(os.path.join(src, "agents", name, "agent.md"), os.path.join(out, "agents", name + ".md"), kind="agent", name=name) +for name in REFERENCE_DOCS: + copy(os.path.join(root, "skeleton", "docs", name + ".md"), os.path.join(out, "docs", name + ".md")) +# Claude Code doesn't fill in ${CLAUDE_PLUGIN_ROOT} in a workflow script, so a workflow that needs the +# spec model gets its text: the skeleton's one-line constant becomes the plugin's copy of the doc. +SPEC_MODEL_LINE = "const SPEC_MODEL = 'Read docs/SPEC-MODEL.md.'" for name in workflows: - copy(os.path.join(src, "workflows", name + ".js"), os.path.join(out, "workflows", name + ".js")) + target = os.path.join(out, "workflows", name + ".js") + copy(os.path.join(src, "workflows", name + ".js"), target) + with open(target, encoding="utf-8") as f: + text = f.read() + if SPEC_MODEL_LINE in text: + assert text.count(SPEC_MODEL_LINE) == 1, f"{name}.js names the spec model more than once" + with open(os.path.join(out, "docs", "SPEC-MODEL.md"), encoding="utf-8") as f: + model = "The spec model (SPEC-MODEL.md), which these checks follow:\n\n" + f.read() + text = text.replace(SPEC_MODEL_LINE, "const SPEC_MODEL = " + json.dumps(model, ensure_ascii=False)) + with open(target, "w", encoding="utf-8") as f: + f.write(text) + assert not reference.search(text), f"{name}.js names a reference doc the plugin can't reach from a workflow" LIB_SOURCE = '. "$(dirname "$0")/_lib.sh"' HOOKS_DIR_LINE = 'HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"' for file in sorted(os.listdir(os.path.join(src, "hooks"))): @@ -112,7 +142,7 @@ for file in sorted(os.listdir(os.path.join(src, "hooks"))): text = text.replace('"$HOOKS_DIR/', '"${CLAUDE_PLUGIN_ROOT}/hooks/').replace('"$HOOKS_DIR"', '"${CLAUDE_PLUGIN_ROOT}/hooks"') with open(target, "w", encoding="utf-8") as f: f.write(text) -generated += ["agents", "workflows", "hooks"] +generated += ["agents", "docs", "workflows", "hooks"] with open(os.path.join(src, "settings.json"), encoding="utf-8") as f: hooks = json.load(f)["hooks"] diff --git a/scripts/link-reference-docs.py b/scripts/link-reference-docs.py new file mode 100755 index 0000000..bea0c26 --- /dev/null +++ b/scripts/link-reference-docs.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Point a project's links to the framework's reference docs at the copy its install uses. + +The reference docs (decision 0019) are the framework's own: generic, and never edited by a project. A +packaged project reads them from the aplyca-adf plugin and commits none of them, so its files link +them at the release it pins, on GitHub. A committed project keeps them in docs/ and links them there. +/aplyca-adf:adopt and /aplyca-adf:upgrade run this from the framework at the release they install. + +Usage: + link-reference-docs.py <repo> --packaged vX.Y.Z link the release on GitHub (and move an older pin) + link-reference-docs.py <repo> --committed link docs/ in the project + +It rewrites the project's copies of the skeleton files that name a reference doc, then lists every +other file that still names one the other way, and the reference docs the project still has in docs/. +It deletes nothing. +""" +import argparse +import os +import re +import sys + +FRAMEWORK = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DOCS = sorted(f[:-3] for f in os.listdir(os.path.join(FRAMEWORK, "plugins", "aplyca-adf", "docs")) if f.endswith(".md")) +NAMES = "|".join(map(re.escape, DOCS)) +RELEASE = "https://github.com/aplyca/AgenticDevelopmentFramework/blob/{tag}/skeleton/docs/{name}.md" +TEXT_FILES = (".md", ".mdc") +SKIP_DIRS = {".git", "node_modules"} + +# Local: `docs/X.md`, [text](docs/X.md#anchor) from any folder, and a bare docs/X.md. +LOCAL_CODE = re.compile(r"`docs/(" + NAMES + r")\.md`") +LOCAL_LINK = re.compile(r"\[([^\]\n]*)\]\((?:\./|(?:\.\./)+)?docs/(" + NAMES + r")\.md(#[^)\s]*)?\)") +LOCAL_BARE = re.compile(r"(?<![\w/.-])docs/(" + NAMES + r")\.md") +# On GitHub, at any release: the same three shapes. +URL = r"https://github\.com/aplyca/AgenticDevelopmentFramework/blob/[^/\s)]+/skeleton/docs/(" + NAMES + r")\.md" +RELEASE_CODE = re.compile(r"\[`(" + NAMES + r")\.md`\]\(" + URL + r"\)", re.I) +RELEASE_LINK = re.compile(r"\[([^\]\n]*)\]\(" + URL + r"(#[^)\s]*)?\)", re.I) +RELEASE_BARE = re.compile(URL, re.I) + + +def to_release(text, tag): + url = lambda name: RELEASE.format(tag=tag, name=name) + text = RELEASE_BARE.sub(lambda m: url(m.group(1)), text) # an older pin moves to this one + text = LOCAL_CODE.sub(lambda m: f"[`{m.group(1)}.md`]({url(m.group(1))})", text) + text = LOCAL_LINK.sub( + lambda m: f"[{m.group(1).replace(f'docs/{m.group(2)}.md', f'{m.group(2)}.md')}]({url(m.group(2))}{m.group(3) or ''})", + text, + ) + return LOCAL_BARE.sub(lambda m: url(m.group(1)), text) + + +def to_local(text, folder): + path = lambda name: os.path.relpath(os.path.join("docs", name + ".md"), folder or ".") + text = RELEASE_CODE.sub(lambda m: f"`docs/{m.group(1)}.md`", text) + + def link(m): + label = re.sub(r"(?<![\w/.-])" + re.escape(m.group(2)) + r"\.md", f"docs/{m.group(2)}.md", m.group(1)) + return f"[{label}]({path(m.group(2))}{m.group(3) or ''})" + + text = RELEASE_LINK.sub(link, text) + return RELEASE_BARE.sub(lambda m: f"docs/{m.group(1)}.md", text) + + +def text_files(top, skip=()): + for directory, dirs, files in os.walk(top): + rel_dir = os.path.relpath(directory, top) + dirs[:] = sorted( + d for d in dirs + if d not in SKIP_DIRS and os.path.normpath(os.path.join(rel_dir, d)) not in skip + ) + for file in sorted(files): + if file.endswith(TEXT_FILES): + yield os.path.normpath(os.path.join(rel_dir, file)) + + +def skeleton_files(): + """The skeleton's own files that name a reference doc — not its .claude/, which the plugin carries.""" + skeleton = os.path.join(FRAMEWORK, "skeleton") + reference = {os.path.join("docs", name + ".md") for name in DOCS} + for rel in text_files(skeleton, skip={".claude"}): + if rel in reference: + continue + with open(os.path.join(skeleton, rel), encoding="utf-8") as f: + if LOCAL_BARE.search(f.read()): + yield rel + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + parser.add_argument("repo") + mode = parser.add_mutually_exclusive_group(required=True) + mode.add_argument("--packaged", metavar="TAG", help="the release the project pins, e.g. v1.2.0") + mode.add_argument("--committed", action="store_true") + args = parser.parse_args() + if args.packaged and not re.fullmatch(r"v\d+\.\d+\.\d+", args.packaged): + sys.exit(f"--packaged takes a release tag such as v1.2.0, not {args.packaged!r}") + + repo = os.path.abspath(args.repo) + rewritten = [] + for rel in skeleton_files(): + path = os.path.join(repo, rel) + if not os.path.isfile(path): + continue + with open(path, encoding="utf-8") as f: + before = f.read() + after = to_release(before, args.packaged) if args.packaged else to_local(before, os.path.dirname(rel)) + if after != before: + with open(path, "w", encoding="utf-8") as f: + f.write(after) + rewritten.append(rel) + print(f"Rewrote the reference-doc links in {len(rewritten)} file(s): {', '.join(rewritten) or 'none'}") + + stale = RELEASE_BARE if args.committed else LOCAL_BARE + mentions = [] + for rel in text_files(repo, skip={os.path.join(".claude", "worktrees")}): + if rel in rewritten: + continue + with open(os.path.join(repo, rel), encoding="utf-8", errors="replace") as f: + for number, line in enumerate(f, 1): + if stale.search(line) and not (args.packaged and rel.startswith("docs" + os.sep) and rel[5:-3] in DOCS): + mentions.append(f" {rel}:{number}: {line.strip()[:160]}") + if mentions: + print("Other files still naming a reference doc the " + ("packaged" if args.committed else "committed") + + " way — update them by hand if they should follow:") + print("\n".join(mentions)) + + present = [f"docs/{name}.md" for name in DOCS if os.path.isfile(os.path.join(repo, "docs", name + ".md"))] + if args.packaged and present: + print("Still in docs/ (a packaged project reads the plugin's copy): " + ", ".join(present)) + if args.committed and len(present) < len(DOCS): + print("Missing from docs/ (copy them from the skeleton): " + + ", ".join(f"docs/{name}.md" for name in DOCS if f"docs/{name}.md" not in present)) + + +if __name__ == "__main__": + main() diff --git a/skeleton/.claude/hooks/session-context.sh b/skeleton/.claude/hooks/session-context.sh index fab8535..d01f3eb 100755 --- a/skeleton/.claude/hooks/session-context.sh +++ b/skeleton/.claude/hooks/session-context.sh @@ -112,4 +112,10 @@ if [ -x "$root/scripts/agent/worktree-new.sh" ]; then fi fi +# A packaged project reads the framework's reference docs from the aplyca-adf plugin, which links in +# the project's files only point at on GitHub (decision 0019). +if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -d "${CLAUDE_PLUGIN_ROOT}/docs" ] && [ ! -f "$root/docs/SPEC-MODEL.md" ]; then + echo "- The framework's reference docs — SPEC-MODEL.md, COST-MODEL.md, MEMORY-STRATEGY.md, MCP-INTEGRATION.md — are in ${CLAUDE_PLUGIN_ROOT}/docs/: read them there, not on GitHub." +fi + exit 0 diff --git a/skeleton/.claude/workflows/deep-spec-analysis.js b/skeleton/.claude/workflows/deep-spec-analysis.js index 7cc5324..edf8651 100644 --- a/skeleton/.claude/workflows/deep-spec-analysis.js +++ b/skeleton/.claude/workflows/deep-spec-analysis.js @@ -11,6 +11,10 @@ export const meta = { const READ_ONLY = 'Do not modify, create, or delete any file. Read files and run read-only commands (git log, grep) only.' +// The spec model the lenses check against. The aplyca-adf plugin's copy carries the model's text here +// instead: a packaged project keeps no copy of it in docs/ (decision 0019). +const SPEC_MODEL = 'Read docs/SPEC-MODEL.md.' + const LOCATE_SCHEMA = { type: 'object', required: ['specFolder', 'status', 'isChangeRequest', 'summary'], @@ -66,7 +70,7 @@ if (!located || !located.specFolder) { } const folder = located.specFolder -const context = `Spec folder: ${folder} (status: ${located.status}${located.isChangeRequest ? ', latest section is a change request — analyze the CR against what was delivered' : ''}). Read spec.md, plan.md and tasks.md in full, plus docs/CONSTITUTION.md, specs/README.md and docs/SPEC-MODEL.md. ${READ_ONLY} +const context = `Spec folder: ${folder} (status: ${located.status}${located.isChangeRequest ? ', latest section is a change request — analyze the CR against what was delivered' : ''}). Read spec.md, plan.md and tasks.md in full, plus docs/CONSTITUTION.md and specs/README.md. ${SPEC_MODEL} ${READ_ONLY} Report only gaps you can evidence. Severity: critical = blocks approval (constitution conflict, AC with no task or test, missing layer in the change surface, invented requirement); gap = should be fixed before approval; question = only a human can answer.` const LENSES = [ @@ -74,7 +78,7 @@ const LENSES = [ { key: 'change-surface', prompt: 'CHANGE SURFACE: search the code for the entities, routes, components, tables and functions the plan changes. Find callers, shared components, configuration, migrations, access policies and tests that plan.md does not list; list unlisted consumers of shared code; flag files named in tasks.md that are missing from the change surface table.' }, { key: 'constitution', prompt: 'CONSTITUTION AND DECISIONS: read every principle in docs/CONSTITUTION.md and every accepted ADR (docs/architecture/decisions/) and PDR (docs/process/) the plan touches against the spec folder. Authorization changes, append-only history, dependencies, silenced types, accessibility, secrets.' }, { key: 'consistency', prompt: 'CONSISTENCY AND ASSUMPTIONS: contradictions between spec, plan and tasks; requirements nothing backs (no tracker link, clarification, or stated requirement — plausible additions are the most dangerous); assumptions the plan relies on but does not list; open questions still open; for a change request, whether the Delivered → Change table matches what the spec records as delivered.' }, - { key: 'perspectives', prompt: 'MULTI-PERSPECTIVE COMPLETENESS: required sections filled for this feature-type and personal-data value (docs/SPEC-MODEL.md); "Not applicable" used with a real reason; no speculative optional sections; acceptance criteria numbered and independently testable; edge cases cover empty, error and boundary states.' }, + { key: 'perspectives', prompt: 'MULTI-PERSPECTIVE COMPLETENESS: required sections filled for this feature-type and personal-data value (the spec model); "Not applicable" used with a real reason; no speculative optional sections; acceptance criteria numbered and independently testable; edge cases cover empty, error and boundary states.' }, ] const analyzed = await pipeline(