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

11 KiB

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/ — PRD, design, task list, and codex-review session log.

Install

go install forgejo.zerova.net/sid/template-jj/cmd/planctl@latest

See docs/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:

{"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:

{"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:

- 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

# From inside a plan-dir:
planctl lint && codex exec ...

# Pointed explicitly:
planctl lint dev/plans/26172-planctl/ || exit 1

CI gate

# Exit 1 on any warning or error.
planctl lint --strict dev/plans/26172-planctl/

JSON for programmatic consumers

planctl lint --format=json dev/plans/26172-planctl/ \
  | jq 'select(.severity=="error")'

Multi-plan sweep from repo root

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