The aps CLI has two layers of functionality:
- Authoring — scaffold new projects, lint specs (
init,update,migrate,lint) - Orchestration — drive specs through the work-item lifecycle from the command line (
next,start,complete,graph)
You can ignore orchestration entirely and edit markdown by hand — the CLI is
additive. But once a plan gets non-trivial, aps next/start/complete are
faster and harder to get wrong than manual status edits.
Native aps/aps.exe commands work in bash, Windows PowerShell 5.1, and PowerShell 7. Examples using shell-specific environment assignments or pipelines are labelled; use Windows guidance for quoting and exit handling.
aps init # Create APS structure in the current project
aps update [dir] # Reconcile generated templates + skill (add missing, refresh)
aps update --global # Upgrade the machine-wide CLI at ~/.aps (or $APS_HOME)
aps migrate [dir] # Move a project onto the global binary (remove vendored bloat)
aps lint [file|dir] # Validate APS documents
aps next [module] # Show the next ready work item
aps start <ID> # Mark a Ready work item as In Progress
aps complete <ID> # Mark an In Progress work item as Complete
aps graph [module] # Show work items + dependency arrows
aps rollup # Roll-up table for a federated (nested) parent
aps audit [module] # Audit plan state against reality
aps doctor # Diagnose migration state (global binary vs vendored)
aps --help # Top-level help
aps <cmd> --help # Per-command helpOrchestration commands accept --plans <dir> for a non-default plan root.
lint takes a positional file/directory; init uses --plans-dir. Check the
native command's --help instead of assuming every flag is global.
The orchestration commands (next, start, complete,
graph, audit) additionally accept --child <name> to scope a
federated nested plan to one child.
Project-scoped commands (lint, next, start, complete, graph,
audit) resolve their plan root automatically. When you don't pass --plans
(or a lint target), aps walks up from the current directory for the nearest
.aps/config.yml and uses its plans_dir — so a repo with
plans_dir: docs/plans/ just works from any subdirectory, no flag needed.
Resolution order: explicit --plans / target → APS_PLANS environment
variable (the MCP/manual override) → discovered plans_dir → plans/.
aps also compares the project's cli_version pin to the running binary and
prints a warning on a mismatch. Add --strict (or set APS_STRICT=1) to turn
that mismatch into a non-zero exit — useful in CI to enforce the pinned
toolchain:
aps lint --strict # fail CI if the toolchain version driftsaps lint # Lint plans/
aps lint plans/modules/auth.aps.md
aps lint . --json # Machine-readable outputErrors cause a non-zero exit code. Warnings are informational. Directory scans skip symbolic links found inside the scanned tree; explicit file targets can still be links. Findings stay grouped by file in discovery order, including cross-tree collision warnings.
| Code | Scope | Description |
|---|---|---|
| E001 | Module | Missing ## Purpose section |
| E002 | Module | Missing ## Work Items section |
| E003 | Module | Missing ID/Status metadata table |
| E004 | Index | Missing ## Modules section |
| E005 | Work Item | Missing required field (**Intent:**, **Expected Outcome:**, or **Validation:**) |
| E010 | Issues | Missing ## Issues section |
| E011 | Issues | Missing ## Questions section |
| R001 | Release | Release filename needs three numeric version components, with optional prerelease/build suffixes |
| R002 | Release | Missing one header table with Target and Status body rows in the first 20 lines |
| R003 | Release | Missing ## Release Theme section |
| R004 | Release | Missing ## What Ships section |
Release rules apply to files under plans/releases/ (the v<version>.md
narratives). README.md and the .release.template.md are not linted.
Files ending in .aps.md keep their normal APS validation rules.
| Code | Scope | Description |
|---|---|---|
| W001 | Work Item | ID does not match PREFIX-NNN pattern (e.g., AUTH-001) |
| W002 | Module | Conductor (Type: Conductor) references a work-item ID in ## Coordinated Modules / ## Cross-Module Work Items that resolves nowhere in the plan tree (likely a typo) |
| W003 | Work Item | Dependency references an ID not found anywhere in the plan tree (work items and decisions both resolve cross-file) |
| W004 | Module / Index | Section exists but is empty (## Purpose, ## In Scope, ## Overview, ## Problem & Success Criteria, ## Modules) |
| W005 | Module | Status is Ready but no work items are defined |
| W006 | Index | Module listed under a ### Conductor / Crosscutting index subsection but its file is not marked Type: Conductor |
| W010 | Issues | Issue entry missing Status, Discovered, or Severity field |
| W011 | Issues | Question entry missing Status, Discovered, or Priority field |
| W012 | Issues | Issue ID does not match ISS-NNN format or uses wrong casing |
| W013 | Issues | Question ID does not match Q-NNN format or uses wrong casing |
| W017 | Module | Active module (Ready / In Progress) has no **Last reviewed:** field, or it is older than APS_STALE_DAYS (60) |
| W018 | Work Item | Complete item has no **Validation:** field inside a still-active module — completion cannot be audited |
| W019 | Index | Link in ## Modules points to a non-existent file (warning so seed plans stay clean; aps audit gates it as A004) |
| W020 | Index | Work-item ID defined in more than one child tree of a federated (nested-plans) monorepo — collisions make <name>:<ID> cross-tree references ambiguous (warning; each child tree stays independently valid) |
| W021 | Index | Module ID defined in more than one child tree of a federated monorepo — a warning because IDs remain bare per tree, but orchestration resolves each status within its owning child |
| W022 | Module | A Packages: scope tag (metadata-table column or work-item field) that resolves to no workspace directory — checked as given plus packages/<entry> and apps/<entry>; silent unless the workspace has a packages/ or apps/ directory (tagged monorepo tier, see docs/monorepo.md) |
| W023 | Module | A Reasoning: level (metadata-table column or work-item field) outside the canonical low | medium | high | max vocabulary (case-insensitive) — the field is optional, only a present, malformed value warns; Model: is never vocabulary-checked |
Nested plans (monorepos). When
aps lintis pointed at a federated parentindex.aps.md(one with a## Child Planssection), it follows those links and validates every child plan tree as one plan. Cross-tree dependencies are written<child-name>:<ID>(e.g.core:AUTH-001) and resolve against the named child when it is in scope; a child linted on its own treats such a reference as an intentional external link and stays clean. See the nested-plans design.
Pass --json for machine-readable results — handy for CI summaries:
{
"files": [
{
"path": "plans/modules/auth.aps.md",
"type": "module",
"errors": [],
"warnings": [
{ "code": "W003", "message": "Dependency 'VAL-002' not found in this file", "line": 105 }
]
}
],
"summary": { "files": 2, "errors": 0, "warnings": 1 }
}These are the install-time commands:
aps initscaffolds a new project (TUI wizard or flags).aps update [dir]reconciles the generated footprint — adds any missing core templates, refreshes existing ones, and reconciles the skill when installed. It reports each file as added / updated / unchanged / skipped, and never touches your plan content. (aps setup upgraderefreshes only the files already present;updateis the one that also adds what's missing.) When the project'scli_versionpin differs from the running CLI, an interactive run asks whether to update the pin, keep the pin and install a matching CLI, or continue without changing the pin; non-interactive runs warn and continue.aps update --globalis a different job: it upgrades the machine-wide CLI binary at$APS_HOME/bin(default~/.aps/bin) from GitHub releases. It does not touch project files.aps migrate [dir]moves a project off the vendored bash CLI onto the global binary: it runs theaps doctordiagnosis, then (with--apply) backs up and removes vendored CLI bloat, rewrites stale hook paths, pinscli_version, and drops a stale direnvPATH_add bin. Dry-runs by default.
See installation.md for the full lifecycle.
The orchestration commands read and rewrite your .aps.md files in place.
Markdown stays the single source of truth — there's no separate database.
Draft ──→ Ready ──→ In Progress ──→ Complete
↑ │
└───────────┘ (reset only by manual edit)
Canonical status vocabulary: Draft, Ready, In Progress, Complete,
Blocked.
Accepted aliases (normalized internally, never rewritten in your files):
| Alias | Canonical | Notes |
|---|---|---|
Proposed |
Draft |
anvil-001 and other adopters |
Done |
Complete |
Terminal / compacted items |
Lint also treats Merged, Released, and Shipped as terminal completion
states (see work-item compaction). Orchestration maps Done to Complete for
dependency checks; Proposed maps to Draft (module not actionable until
Ready).
| Command | Transition enforced |
|---|---|
aps next |
None — read-only |
aps start |
Ready → In Progress (all dependencies must be Complete) |
aps complete |
In Progress → Complete |
aps graph |
None — read-only |
aps audit |
None — read-only (but executes Validation commands) |
Invalid transitions are rejected with a clear error. The CLI never silently forces a state change.
$ aps next
AUTH-003: Implement token refresh
Module: AUTH | Dependencies: AUTH-001, AUTH-002, CORE-001 | Status: Ready
Model: claude-opus-5 | Reasoning: high
File: plans/modules/auth.aps.md
$ aps next auth # Scope to one module
$ aps next --plans docs/plans
$ aps next --package core # Only items tagged with the core package
$ aps next --by-package # Whole ready queue, grouped by packagenext walks every work item across every module, picks the first whose status
is Ready and whose dependencies (work-item IDs and module IDs) all resolve
to Complete. Decision dependencies (D-NNN) are treated as resolved inline in
the plan text. Items in Complete, Draft (including Proposed), or
Blocked modules are skipped.
In a tagged monorepo (see docs/monorepo.md), --package <name> restricts resolution to items whose effective Packages: tags include
the name — the item's own field, else its module's metadata column. Matching
is case-insensitive and packages/core matches core. --by-package prints
every ready item grouped under its package headings, with untagged items in a
final (untagged) bucket so partial adoption stays visible. Untagged items
never match a --package filter.
When a work item or its module carries routing hints, next adds a
Model: … | Reasoning: … line so the executing harness can pick the model and
effort level. Both fields are optional but preferred: set - **Model:** /
- **Reasoning:** on the item, or Model / Reasoning columns in the module
metadata table as a module-wide default the items inherit. Reasoning takes
low | medium | high | max (W023 warns on anything else); Model is a
free-form identifier. Unset hints show as None; the line is omitted when
neither is set.
$ aps start AUTH-003
Marked AUTH-003 as In Progress
Suggested branch: work/auth-003
File: plans/modules/auth.aps.md
Context package: .aps/context/AUTH-003.mdstart validates that:
- The work item exists and is currently
Ready. - Every dependency is
Complete. - The owning module is
ReadyorIn Progress(you can't start items in a Draft module).
On success it:
- Rewrites the
- **Status:**line (creates one if absent) toIn Progress. - Suggests a branch name (
work/<id>) — branch creation is left to you per ORCH D-003. APS doesn't manage git. - Assembles a context package at
.aps/context/<ID>.mdcontaining the work item, module scope, decisions, dependency learnings, and related file paths. The directory is gitignored by default — regenerated on eachstart.
Running aps start on an item that's already In Progress is a no-op warning,
not an error. Running it on a Complete item is rejected.
$ aps complete AUTH-003
Marked AUTH-003 as Complete: 2026-05-12
File: plans/modules/auth.aps.md
$ aps complete AUTH-003 --learning "Token refresh needs retry on network errors"
Marked AUTH-003 as Complete: 2026-05-12
Learning recorded for AUTH-003complete requires the item to be In Progress. It stamps Status as
Complete: <UTC date>. With --learning "..." it inserts a - **Learning:**
line immediately after - **Validation:** (per ORCH D-002). Learnings travel
with the work item — they show up in dependency learnings for downstream items
when those items start.
$ aps graph auth
AUTH-001 [Complete] Create users
<- none
AUTH-002 [Complete] Verify credentials
<- AUTH-001[Complete]
AUTH-003 [Complete] Add token refresh
<- AUTH-001[Complete] AUTH-002[Complete] CORE-001[Complete]
AUTH-004 [Ready] Add session audit log
<- AUTH-003[Complete]Useful for spotting blocked chains or auditing what's in flight. The arrow
reads as "blocked by". An empty <- none means no upstream dependencies.
Plans drift: items get marked Complete without passing their own validation, Draft items quietly ship, review dates go stale. The audit formalizes the completion-audit pattern from anvil-001 (which found 8 overstated and 19 understated items across 191 by hand):
$ aps audit
APS Audit
Complete-item verification:
AUTH-001 PASS npm test -- auth.test.ts
AUTH-002 FAIL npm test -- session.test.ts
AUTH-003 PARTIAL Validation is not a runnable command
Findings:
A001 AUTH-002 overstated: Validation failed: npm test -- session.test.ts
A002 PAY-004 understated: Draft but files exist: src/pay/refund.ts
A003 UI-002 stale: module last reviewed 2026-01-10 (89 days ago, threshold 60)
A004 index broken-link: ./modules/ghost.aps.md (line 41)
Findings: 4 (23 items audited)| Code | Meaning |
|---|---|
| A001 | Overstated — Complete item whose Validation command fails |
| A002 | Understated — Draft item whose Files already exist with content |
| A003 | Stale — Ready item in a module with no recent **Last reviewed:** |
| A004 | Broken link — index ## Modules link points to a non-existent file |
Options: --json for machine-readable output, --no-run to skip executing
validation commands (verification reports PARTIAL; no A001 findings),
--stale-days N to tune the threshold (also via APS_STALE_DAYS). Exit
code is non-zero when there are findings, so it slots into CI as a deeper,
optional companion to aps lint.
Warning: by default the audit executes backtick
Validationcommands found in Complete work items, with full shell semantics (a timeout applies:APS_AUDIT_TIMEOUT, default 60s, and a notice is printed to stderr). The trust boundary is the plan file itself — anyone who can edit a Validation field controls what runs. Only run it on plans you trust, or pass--no-run. In CI, use--no-runfor pull-request-triggered jobs — running with execution enabled on PR-modified plans hands code execution to the PR author. Reserve execution for trusted branches.
When you point an orchestration command at a federated parent
index.aps.md (one with a ## Child Plans section), it treats the whole
nested tree as one queue — the same traversal aps lint uses. aps next,
start, complete, graph, and audit all see the parent plus every child
plan reachable through ## Child Plans:
aps next --plans packages # next ready item across all child plans
aps graph --plans packages # dependency graph spanning every tree
aps audit --plans packages --no-run # audit every child plan from the root-
Scope to one child with
--child <name>, where<name>is the path-derived child name (the directory above itsplans/, e.g.core):aps next --child core --plans packages aps audit --child api --plans packages --no-run
-
Cross-tree dependencies written as
<name>:<ID>(e.g.core:AUTH-001, per D-002) are resolved against the named child, gateaps next/start, and appear as prefixed edges inaps graph. -
Mutations target the owning child file.
aps start/completerewrite the work item in the child module file it actually lives in, never a parent or a same-ID sibling. Accepts a bare ID, a<name>:<ID>ref, or a bare ID plus--child. -
Ambiguous bare IDs are rejected. If the same ID is defined in more than one child tree (the W020 case),
start/completerefuse the bare ID and ask you to disambiguate with<name>:<ID>or--child <name>.
Keeping the parent roll-up current. A federation root carries a
## Roll-up table summarising each child (modules complete/total, next ready
item, overall status). The root index stays hand-authored, so the table does
not regenerate itself — aps rollup prints the current rows for you to paste:
$ aps rollup --plans packages
| Child | Modules (complete/total) | Next ready item | Status |
| ----- | ------------------------ | --------------- | ------ |
| core | 0/1 | AUTH-001 | Ready |
| api | 0/1 | — | Ready |The refresh ritual: at session end (or whenever a child's state changes), run
aps rollup and copy the table body into the parent's ## Roll-up section.
Keeping it a manual paste keeps the root a readable, reviewable artifact rather
than generated output.
See the nested-plans design and monorepo guide for the full convention.
aps doctor checks whether a project is cleanly on the global binary or still
carries a vendored bash CLI. It is read-only and prints one line per check:
$ aps doctor
aps doctor — migration diagnostics
[ok ] global binary: aps 0.6.0 at /home/you/.aps/bin/aps
[warn] cli_version: project pins 0.5.0 but this binary is 0.6.0 — install the pinned release or update the pin
[warn] vendored CLI: leftover vendored CLI under /repo: bin/aps, lib — run `aps migrate` to back up and remove
[ok ] global runtime: /home/you/.aps/lib is complete
[warn] direnv: /repo/.envrc still adds ./bin to PATH — drop it once you run on the global binary
Checks: global binary presence/version, cli_version match, leftover vendored
CLI trees (bin/aps, lib/, .aps/bin, .aps/lib — only when the bash CLI
marker is present, so an unrelated project lib/ is never flagged), an
incomplete global ~/.aps/lib/ runtime (a missing file such as audit.sh is a
problem → non-zero exit), and a stale direnv PATH_add bin entry. See
Migrating to the Global Binary
for the full walkthrough.
aps next # Discover what to work on
aps start AUTH-003 # Claim it
git switch -c work/auth-003 # Optional — follow the suggestion
# ...implement, test, commit...
aps complete AUTH-003 --learning "..."
aps next # LoopThat loop is the intended day-to-day flow for solo and AI-driven work. For team workflows where multiple people might race on the same plan, treat the markdown rewrites as commits — push and pull to coordinate.
For agents that speak MCP, an optional server in mcp/ exposes
the orchestration commands as a single aps tool over stdio. The agent sends
either a direct command ("next auth") or a natural-language request
("what's the next ready work item in the auth module?"); the server routes
it to an allowlisted CLI invocation and returns the result.
For native Windows installation, tests, and process environment use the MCP PowerShell setup. Unix shell setup:
cd mcp && pnpm install --frozen-lockfileRegister it with your MCP-capable tool, e.g. for Claude Code:
{
"mcpServers": {
"aps": {
"command": "node",
"args": ["/path/to/anvil-plan-spec/mcp/src/index.ts"],
"env": { "APS_PLANS": "/path/to/your/project/plans" }
}
}
}On Windows, set APS_BIN explicitly to the absolute aps.exe path; the default sibling bin/aps is a bash script. For example, these are harness configuration values (substitute your real paths, without extra embedded shell quotes):
{
"command": "C:/Program Files/nodejs/node.exe",
"args": ["C:/Work/APS Source/mcp/src/index.ts"],
"env": {
"APS_BIN": "C:/Users/You/.aps/bin/aps.exe",
"APS_PLANS": "C:/Work/My Project/plans"
}
}Environment variables: APS_BIN overrides the aps executable (defaults to
the sibling bin/aps, then $PATH); APS_PLANS sets the plan root passed
to every command. The server is optional — everything it does is also
available via the CLI or by editing markdown directly.
Use native aps.exe under Windows PowerShell 5.1 or PowerShell 7. Select it explicitly if a script or older install shadows the command. Quoted executable paths require PowerShell's call operator, and native failures need exit checks:
$Aps = (Get-Command aps.exe -ErrorAction Stop).Source
& $Aps lint 'C:\Work\My Project\plans'
if ($LASTEXITCODE -ne 0) { throw 'APS lint failed' }
& $Aps next --plans 'C:\Work\My Project\plans'
if ($LASTEXITCODE -ne 0) { Write-Warning 'Inspect output; the ready queue may be empty' }Do not use Bash's VAR=value prefix in PowerShell; use $env:VAR and restore its previous value after the operation. Do not use && for error handling in Windows PowerShell 5.1. ErrorActionPreference alone does not turn native nonzero exit codes into exceptions. For initial setup use the PowerShell walkthrough. The list below is a command reference, not a script to run against a real plan; start/complete require authorised work and actual validation evidence.
On Windows, use native aps.exe from PowerShell or Scoop for the user
commands below. Known limitation: executable audit validation currently
launches bash (cli/src/audit.rs), even in the native binary. Use
aps audit --no-run on a Windows system without bash and run validation
commands explicitly in PowerShell; do not install WSL merely for this check.
Native PowerShell audit execution needs a separate implementation fix.
aps init
aps setup
aps update
aps migrate
aps lint plans\
aps next
aps start AUTH-003
aps complete AUTH-003 --learning "validated on Windows"
aps graph auth
aps audit --no-run
aps export --json
aps doctorThe legacy PowerShell script remains available for lint/init/update fallback use cases:
.\bin\aps.ps1 lint plans\
.\bin\aps.ps1 lint plans\modules\auth.aps.md
.\bin\aps.ps1 lint plans\ --jsonNo user command requires WSL or Git Bash. Those shells remain optional for
agent and contributor automation. Executable audit validation still has the
bash dependency noted above; use aps audit --no-run and run validation
directly in PowerShell when bash is absent. See
installation.md for the recommended
PowerShell and Scoop install paths.
Add .github/workflows/lint-aps.yml to your project. The canonical workflow
lives at docs/ci-lint-example.yml; the snippet below
keeps it inline for convenience.
name: Lint APS Documents
on:
push:
paths: ["plans/**/*.aps.md", "plans/**/*.actions.md"]
pull_request:
paths: ["plans/**/*.aps.md", "plans/**/*.actions.md"]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install APS CLI
run: |
curl -fsSL https://raw.githubusercontent.com/EddaCraft/anvil-plan-spec/main/scaffold/install \
| bash -s -- --global
- name: Lint APS documents
run: aps lint plans/
- name: Upload results on failure
if: failure()
run: aps lint plans/ --json > aps-lint-results.json
- name: Upload artefact
if: failure()
uses: actions/upload-artifact@v4
with:
name: aps-lint-results
path: aps-lint-results.jsonUsing the installer keeps lib/ in sync as new validation rules ship. If you
need to vendor the CLI without network access at build time, mirror bin/aps
plus the entire lib/ directory into your repo and run ./bin/aps lint plans/.