template-jj/docs/planctl.md
Sid 7fa52772b3 docs: add reference docs for both binaries + consolidated build guide
* docs/building.md — how to build jj-commitd and planctl from source,
  requirements, `go install` recipes, reproducible-build flags
  (`-trimpath -ldflags="-buildid="` per planctl PRD R7.3), test +
  benchmark commands, cross-compile, CI matrix link.
* docs/planctl.md — full command reference beyond --help: plan-dir
  discovery cases A-D, all 9 classification codes with severity and
  meaning, tag-grammar spec (including paren-aware annotations + the
  tag-unclosed recovery semantics), the five EARS regexes and the
  bold-prefix exemption, JSON schema, worked examples for pre-commit
  / CI / multi-plan / JSON consumers.
* docs/jj-commitd.md — architecture diagram, socket event protocol
  (session-start, post-edit, session-end, shutdown, status) with
  field tables, all env vars (REPO_ROOT, JJ_HOOK_DEBOUNCE_SEC,
  JJ_HOOK_STALE_MIN, JJ_HOOK_AUTO_SQUASH), baked-in timing constants,
  socket / log paths, fallback mode, troubleshooting.
* README.md — expanded the jj-commitd bullets and linked to each
  reference from a new "Documentation" section; kept top-level
  summaries terse.
2026-04-21 21:38:01 -06:00

228 lines
11 KiB
Markdown

# planctl — command reference
Lint tool for spec-driven plan directories. Read-only by design. v1 ships the `lint` subcommand only; task-state commands (`list`, `next`, `complete`, `new-plan`) are reserved for v2 and currently exit 2.
**Spec:** [`dev/plans/26172-planctl/`](../dev/plans/26172-planctl/) — PRD, design, task list, and codex-review session log.
## Install
```bash
go install forgejo.zerova.net/sid/template-jj/cmd/planctl@latest
```
See [`docs/building.md`](building.md) for building from source.
## Synopsis
```
planctl lint [flags] [plan-dir]
planctl --help
planctl --version
```
## Flags
| Flag | Default | Effect |
|-----------------------------|----------|------------------------------------------------------------------------------------------|
| `--format={text,json}` | `text` | Output format. `json` emits one JSONL object per diagnostic plus a final summary. |
| `--strict` | off | Treat warnings as errors for the exit code (exit 1 on any diagnostic). |
| `--no-ears` | off | Suppress `ears-violation` warnings entirely. |
| `--color={auto,always,never}` | `auto` | Accepted for forward-compatibility; no-op in v1 (TTY detection deferred). |
| `--help`, `-h` | — | Show usage and exit. |
| `--version` | — | Print version and exit. |
Both `--flag=value` and `--flag value` forms are accepted for the two value-taking flags. Unknown flags, unknown subcommands, and invalid flag values exit 2 without attempting any lint work.
## Exit codes
| Code | Meaning |
|------|------------------------------------------------------------------------------------------|
| `0` | No errors (warnings alone are informational without `--strict`). |
| `1` | Any error-severity diagnostic, or any warning under `--strict`. |
| `2` | Tool error, bad invocation, unknown flag/subcommand, or missing explicit path. |
## Plan-directory discovery
When invoked without a positional argument, `planctl` walks upward from the current directory until one of four cases matches (see the `resolvePlans` implementation in `cmd/planctl/main.go`):
- **Case A — explicit path.** `planctl lint <path>` lints exactly that directory. Non-existent path or non-directory → exit 2.
- **Case B — CWD inside a plan dir.** If `cwd` is at or below a directory whose basename matches `^\d{5}-[a-z][a-z0-9-]+$` AND whose parent is `plans/` AND whose grandparent is `dev/`, lint that plan dir.
- **Case C — CWD at or above a repo root.** If an ancestor of `cwd` has `dev/plans/` as an immediate subpath, enumerate every direct child of `dev/plans/` matching `<YYWWD>-<slug>` (excluding `archive/` and anything under it), sort lexicographically, and lint all of them. Multi-plan output per R5.9.
- **Case D — nothing found.** Walk reached filesystem root without a match → exit 2 with the 3-option disambiguation message.
Case B wins over Case C at the same ancestor: deep within a plan-dir's subtree, `planctl lint` targets that plan, not the whole repo's `dev/plans/`.
## Output format
### Text (default, single plan)
Compact — designed to be quoted into an LLM agent's context (PRD G3).
```
tasks.md:42: [error] orphan-requirement: cites R3.7, not declared in prd.md
prd.md:128: [warning] ears-violation: R4.3: body "..." does not match an EARS keyword; consider starting with "WHEN <trigger>, THE SYSTEM SHALL ..."
```
Clean plan: one summary line.
```
26172-planctl: clean (79 tasks, 45 requirements, 21 design sections)
```
No header, no aggregate summary, no inter-plan blank line — PRD R5.10.
### Text (multi-plan — Case C)
```
=== 26167-group-backend ===
tasks.md:90: [warning] ears-violation: ...
=== 26172-planctl ===
26172-planctl: clean (79 tasks, 45 requirements, 21 design sections)
=== 26175-other ===
tasks.md:42: [error] orphan-requirement: ...
3 plans linted, 1 errors, 1 warnings
```
Labels in the aggregate summary are always plural per PRD R5.9 (`1 errors` even when the count is 1 — literal wording).
### JSON (`--format=json`)
JSONL — one object per diagnostic, terminating summary object:
```json
{"plan_dir":"26167-group-backend","path":"tasks.md","line":90,"severity":"warning","code":"ears-violation","message":"..."}
{"plan_dir":"26175-other","path":"tasks.md","line":42,"severity":"error","code":"orphan-requirement","message":"cites R3.7, not declared in prd.md"}
{"summary":{"plans":3,"errors":1,"warnings":1}}
```
Field schema per diagnostic:
| Field | Type | Description |
|------------|--------|-------------------------------------------------------------------|
| `plan_dir` | string | Plan-directory basename (e.g. `26172-planctl`). |
| `path` | string | File relative to the plan dir (e.g. `tasks.md`); `""` for directory-level diagnostics. |
| `line` | int | 1-indexed source line. `0` for directory-level diagnostics. |
| `severity` | string | `"error"` or `"warning"`. |
| `code` | string | Classification code (see below). |
| `message` | string | Human-readable. No trailing period, no newlines. |
Summary object:
```json
{"summary":{"plans":N,"errors":E,"warnings":W}}
```
The same format is used for single- and multi-plan runs — consumers can disambiguate via `plan_dir`.
## Classification codes
Codes are part of the **public grep-stable contract** (design §4). Renaming a code is a breaking change; adding a new code is additive.
| Code | Severity | Meaning |
|---------------------------|----------|------------------------------------------------------------------------------------------|
| `tag-syntax` | error | A tag body failed the `R<n.m>[a]` / `D§<n>[.<m>…]` grammar from PRD R1.1. |
| `tag-unclosed` | error | `_Requirements:` or `_Design:` opener has no matching closing `_` on the same line. |
| `orphan-requirement` | error | `tasks.md` cites an R-id not declared in `prd.md`. |
| `orphan-design` | error | `tasks.md` cites a D§-id not declared in `design.md`. |
| `uncovered-requirement` | error | A PRD R-id is never cited by any `tasks.md` tag. |
| `uncovered-design` | error | A design D§-id is never cited by any `tasks.md` tag. |
| `missing-prd` | error | Plan directory has no `prd.md`. Fatal — R2/R4 checks skip for this plan. |
| `missing-closeout-file` | error | Fully-closed-out `tasks.md` (all `[x]`, at least one task) lacks `codex-sessions.md` or `handoff*.md`. |
| `ears-violation` | warning | PRD acceptance criterion doesn't begin with an EARS keyword. Suppress with `--no-ears`. |
## Tag grammar (PRD R1.1)
A traceability tag is the token `_<Tag>: <ref-list>_` where:
- `<Tag>` is one of `Requirements`, `Design`.
- `<ref-list>` is one or more `<ref>` values separated by `, ` (comma-space).
- `<ref>` is `<id>` optionally followed by `( <free-text> )` annotation.
- For `Requirements`: `<id>` matches `R\d+(\.\d+)+[a-z]?` — e.g. `R1.2`, `R2.3a`, `R10.15`. The literal `infra` is also accepted as a sentinel (R1.7) and is exempt from `orphan-requirement` (R2.9).
- For `Design`: `<id>` matches `D§\d+(\.\d+)*` — e.g. `D§3`, `D§3.4`.
- Both underscores must balance; paren nesting is respected so annotation bodies may contain `_` or `,`.
### Inline-code and fence exclusion (R1.5)
Tag-like tokens inside fenced code blocks, indented code blocks, HTML blocks, and inline `` `` `` spans are ignored. This is implemented via a goldmark AST walk that populates per-line `InCode[]` (block-level) and per-column `LineMask[]` (inline-code span) arrays in the scanner; the indexer consults both before running the tag state machine.
### Multi-tags per line
`_Requirements: R1.2_ _Design: D§3.4_` on one line is fine. See also the tag-unclosed recovery below.
### Tag-unclosed recovery
Under `_Requirements: R3 _Design: D§2_` the state machine emits one `tag-unclosed` diagnostic for the unclosed `_Requirements:` token AND a well-formed `D§2` extraction. The recovery rule: when the forward scan hits another opener (`_Requirements:` / `_Design:`) before finding a closer, the current tag is "unclosed" and scanning resumes at the inner opener. See design §3.3.
## EARS patterns
`planctl` checks PRD acceptance-criterion bodies (text after `R<n.m> ` on a bullet line) against five regexes — all require an explicit `SHALL` or `THEN` continuation:
```
^THE SYSTEM SHALL\b
^WHEN .+?, THE SYSTEM SHALL\b
^WHILE .+?, THE SYSTEM SHALL\b
^WHERE .+?, THE SYSTEM SHALL\b
^IF .+?, THEN (THE SYSTEM SHALL\b|.+\b)
```
### Bold-prefix exemption
A body whose opening `**` is matched by a closing `**` on the same line is exempted from the EARS check. Use this narrow escape hatch for definitional bullets:
```markdown
- R3.3 **Definition — "task".** For R3 file-presence checks, ...
```
An unmatched opening `**` does NOT trigger the exemption — a malformed body that happens to start with `**foo` is still checked.
### Suppressing EARS
`planctl lint --no-ears <plan-dir>` skips the check entirely (PRD R4.4). `--strict` still promotes any remaining warnings to errors.
## Examples
### Pre-commit / pre-codex sanity check
```bash
# From inside a plan-dir:
planctl lint && codex exec ...
# Pointed explicitly:
planctl lint dev/plans/26172-planctl/ || exit 1
```
### CI gate
```bash
# Exit 1 on any warning or error.
planctl lint --strict dev/plans/26172-planctl/
```
### JSON for programmatic consumers
```bash
planctl lint --format=json dev/plans/26172-planctl/ \
| jq 'select(.severity=="error")'
```
### Multi-plan sweep from repo root
```bash
cd ~/myrepo
planctl lint
# ... lints every dev/plans/<YYWWD>-<slug>/ child in lex order, skipping archive/
```
## Performance
PRD success metric M3 targets sub-100ms lint time on a 5-file, ~100 KB plan. The in-repo perf fixture (`cmd/planctl/testdata/perf/big-plan/`) has 36 R-ids, 18 D§ sections, 72 tasks in ~90 KB and runs at ~1 ms on a 2023-era M-series machine. `Benchmark_Lint_BigPlan` measures this in CI; `TestLint_BigPlan_SoftCeiling` emits a `WARNING` to stderr if the 100 ms ceiling is breached (no test failure — CI runner variance is expected).
## See also
- [PRD](../dev/plans/26172-planctl/prd.md) — normative requirements.
- [Design](../dev/plans/26172-planctl/design.md) — architectural decisions, rule internals.
- [`docs/building.md`](building.md) — build from source.
- [`docs/jj-commitd.md`](jj-commitd.md) — the other binary in this repo.