* 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.
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
cwdis at or below a directory whose basename matches^\d{5}-[a-z][a-z0-9-]+$AND whose parent isplans/AND whose grandparent isdev/, lint that plan dir. - Case C — CWD at or above a repo root. If an ancestor of
cwdhasdev/plans/as an immediate subpath, enumerate every direct child ofdev/plans/matching<YYWWD>-<slug>(excludingarchive/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 ofRequirements,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>matchesR\d+(\.\d+)+[a-z]?— e.g.R1.2,R2.3a,R10.15. The literalinfrais also accepted as a sentinel (R1.7) and is exempt fromorphan-requirement(R2.9). - For
Design:<id>matchesD§\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
- PRD — normative requirements.
- Design — architectural decisions, rule internals.
docs/building.md— build from source.docs/jj-commitd.md— the other binary in this repo.