* lint.go: Severity / Code (full set) / Diagnostic types migrated here from their temporary home in index.go. Code constants form the grep-stable public contract per design §4. * checkTagSyntax (passthrough over idx.MalformedTags). * checkCrossRef with four diagnostic classes (orphan-requirement, orphan-design, uncovered-requirement, uncovered-design), R2.8 hasDesign gate, R2.9 infra-sentinel exemption, and sorted-id emission for stable uncovered-* order. * checkCloseoutFiles gated on R3.4 fully-closed-out definition (at-least-one + all-checked); emits Line=0 diagnostics with empty Path for the emitter to substitute plan-dir. * newMissingPRDResult: R3.1/R3.2 fatal short-circuit. * checkEars with the five design §3.4 regex patterns, narrow `^\*\*[^*]+\*\*` bold-prefix exemption, and --no-ears R4.4 skip. * sortDiagnostics uses sort.SliceStable with (Path, Line, Code, Message) tie-breakers so output is deterministic run-to-run per PRD §6 even when two diagnostics share the first three keys (missing-closeout-file Line=0 pair). * Position extended with a Text field populated at extraction time so checkEars keeps its literal `(idx, skip)` signature without passing a second *ScanResult. * lint_test.go: one subtest per checker plus sort stability regression. Parent task 4.0 from dev/plans/26172-planctl/tasks.md. Codex code-review session: 019db2b8-21c8-7d61-85d6-1da5c18d8033 (2 rounds). |
||
|---|---|---|
| .. | ||
| notes | ||
| plans | ||
| research | ||
| tools | ||
| QUICKSTART.md | ||
| README.md | ||
dev/
Working artifacts for the spec-driven workflow used in this repository. Tracked in version control (not excluded) so the history of how a feature was planned, designed, and executed travels with the code.
Upstream boundary (forks only): if this repo is a fork with an upstream you shouldn't leak dev/ content to, see "Upstream boundary" below. For greenfield projects, ignore that section.
Layout
dev/
├── QUICKSTART.md # one-screen cheat sheet
├── README.md # this file
├── plans/
│ ├── <YYWWD>-<feature-slug>/ # per-feature spec + review trail
│ │ ├── prd.md # Product Requirements (EARS acceptance criteria)
│ │ ├── design.md # Technical blueprint (optional; skip for trivial features)
│ │ ├── tasks.md # Task list with _Requirements:_ / _Design:_ tags
│ │ ├── codex-sessions.md # Append-only log of codex session UUIDs per stage
│ │ ├── copilot-sessions.md # Same, for GitHub Copilot CLI (if used)
│ │ ├── gemini-sessions.md # Same, for Google Gemini CLI (if used)
│ │ └── kiro-sessions.md # Same, for Kiro CLI (if used)
│ └── archive/ # Completed/abandoned plans (keeps active list scannable)
├── notes/ # Freeform working notes (ad-hoc)
├── research/ # Research artifacts (protocol traces, reference code digs, etc.)
└── tools/ # Local developer utilities (hooks, scripts)
├── install-hooks.sh # Symlinks hook scripts into .git/hooks/
└── pre-push-guard.sh # Optional: refuses pushes touching dev/ to a configured upstream
Plan directory convention
Plan directories carry a chronological YYWWD- prefix so they sort by creation date and short slugs don't collide over time:
YY= two-digit ISO year,WW= two-digit ISO week,D= ISO day-of-week (1=Mon …7=Sun). Compute withdate +%y%V%uat the moment the directory is created. Don't backdate.<slug>= kebab-case, 2–4 words, describing the feature. No spaces, no uppercase.
Example: on 2026-04-19 (date +%y%V%u = 26167), a plan for pane focus sync lives at dev/plans/26167-pane-focus-sync/.
Feature branches mirror the plan directory: feature/<YYWWD>-<feature-slug>. That keeps jj bookmark list / git branch -a in the same chronological order as ls dev/plans/.
Ad-hoc plan directories that predate the spec-driven skills (monolithic plan.md) are fine — they just use the same dir prefix without the prd.md / design.md / tasks.md shape.
Skill workflow
The spec-driven skills chain cleanly:
/create-prd— asks clarifying questions, draftsprd.md, runs a codex peer-review loop./create-design— producesdesign.md(skip for trivial features)./generate-tasks— two-phase task decomposition intotasks.md./process-task-list— executes sub-tasks one at a time with user approval, runs codex code review at parent-task boundaries, commits via jj.
Fresh session per stage. Each review stage (PRD, design, tasks, each parent-task code review, pre-PR) starts its own clean codex session. Session UUIDs are logged in codex-sessions.md (and copilot-sessions.md / gemini-sessions.md / kiro-sessions.md if other reviewers are used), but they're not resumed across stages — this keeps each review's context uncluttered. See the codex skill's "Cross-stage continuation" section for the rationale.
Traceability tags
| Location | Format | Example |
|---|---|---|
| PRD acceptance criteria | R<parent>.<child> |
R2.1 |
| Design section | D§<number> |
D§4.2 |
| Task | T<parent>.<child> |
T3.1 |
| Task list (markdown) | _Requirements: R1.1, R1.2_ / _Design: D§2_ |
appended to task line |
| Source code comment | // spec:<feature-slug>/<ref> |
// spec:user-auth/R2.1+D§6 |
Searching:
rg 'spec:user-auth' # all code tied to one feature
rg 'spec:.*/R2\.1' # all code tied to a specific requirement across features
rg 'spec:.*/D§4' # all code tied to a specific design section
Note: the in-code spec: tag uses the bare slug, not the full <YYWWD>-<feature-slug> directory prefix. Collisions inside a single codebase are unlikely, and the shorter form is friendlier in code.
Scope discipline
PRD and design are authoritative. No features, APIs, or schema changes that aren't traceable to the PRD or design. If an emerging need appears mid-execution, log it as a new sub-task with its own _Requirements:_ tag rather than silently expanding scope. Useful-but-unrelated fixes become new sub-tasks; don't bundle them into an existing one.
Commit convention
- This template uses jj (colocated with git). Commits are authored via
jj describe -m "..." -m "..." .... - Conventional commit prefixes:
feat:,fix:,refactor:,test:,docs:,chore:,dev:(fordev/-only changes). - Commit messages reference the parent task and PRD:
Task <N.0> from dev/plans/<YYWWD>-<feature-slug>/prd.md. - Never
--no-verify. - No
Co-Authored-Bylines.
Multi-agent workflow
Running multiple agents (Claude Code sessions, other LLM CLIs, or your own terminal) against the same repo in parallel is supported, but requires discipline — the working tree is shared, and one agent's jj new <somewhere-else> flips it underneath another agent mid-edit. Two layers of separation keep this sane.
Separation by commit class
Two disjoint commit classes, never overlapping:
- Plans commits — description starts with
dev:; changes confined todev/plans/**,dev/notes/**,dev/research/**. A plans commit NEVER contains<SOURCE_ROOT>/**edits. Planners typically work on a single long-lived plans commit or a short stack (jj new <plans-parent> -m "dev: <what I'm drafting>"). - Feature branches — one per PRD, bookmark named
feature/<YYWWD>-<feature-slug>, rooted onmainor a shareddev:infrastructure commit. Contains<SOURCE_ROOT>/**edits plus the specificdev/plans/<YYWWD>-<feature-slug>/dir the branch implements (moved in viajj squash). Implementers work here.
Plans commits and feature branches are siblings, not ancestor/descendant. The feature branch owns its own plan dir via jj squash; plans for other features stay on the plans commit so the planner can keep extending.
Primary isolation: jj workspaces
jj workspace is the proper tool for concurrent parallel agents. Each workspace has its own working directory sharing the same .jj/ object store; each has an independent @ pointer, so one agent's branch switch never flips another's tree.
Create a workspace per parallel agent:
# From the main clone:
jj workspace add --name planner ../<PROJECT_NAME>-planner
jj workspace add --name impl ../<PROJECT_NAME>-impl
# Each agent's Claude Code (or Codex/Copilot/etc.) session runs in its own directory:
cd ../<PROJECT_NAME>-planner # planner operates here
cd ../<PROJECT_NAME>-impl # implementer operates here
jj workspace list enumerates; jj workspace forget <name> removes. Workspaces share commit history, so the planner's commits are immediately visible to the implementer without copy steps.
Use workspaces whenever two agent sessions will run concurrently. The rest of this section (env var conventions, pre-flight) is for the single-workspace degraded mode.
Env var conventions (secondary — same-workspace)
When multiple agents must share one working directory, use env vars to self-identify and trigger pre-flight mismatch checks:
AGENT_ROLE=planner # or impl, reviewer, …
AGENT_BOOKMARK=feature/<YYWWD>-<feature-slug>
An agent's pre-flight compares @'s bookmark against $AGENT_BOOKMARK; mismatch triggers a prompt before any write. The spec-driven skills (create-prd, create-design, generate-tasks, process-task-list) respect this convention when set.
This is a convention, not a jj feature. jj itself reads JJ_USER / JJ_EMAIL / JJ_OP_USERNAME but has no "my bookmark" concept — the env var is a tripwire for the agent's own pre-flight logic. For real isolation, use workspaces; the env var is the fallback.
Pre-flight check (every agent, every session start)
If not using a dedicated workspace, run before any file write:
jj workspace list # confirm expected workspace
jj status
jj log -r '@|@-' --no-graph -T 'change_id.short() ++ " " ++ bookmarks ++ " [" ++ description.first_line() ++ "]\n"'
# If AGENT_BOOKMARK is set, verify @'s bookmark matches:
[ -n "$AGENT_BOOKMARK" ] && jj log -r @ -T bookmarks | grep -q "$AGENT_BOOKMARK" || echo "WARNING: working-tree bookmark does not match \$AGENT_BOOKMARK"
Interpret the output:
- Drafting agent (planner): if
@'s description doesn't start withdev:, you're on a feature branch — writes todev/plans/**land in the wrong commit.jj new <plans-commit>orjj edit <plans-commit>first; or ask the user which drafting context to use. - Implementing agent: if
@'s description starts withdev:, you're on a plans commit — code edits land in the wrong commit.jj new feature/<YYWWD>-<feature-slug>first. - Both: re-run
jj statusperiodically. The working tree can flip underneath you if another agent runsjj new/jj edit/jj bookmarkin parallel. A write failure that looks strange ("file doesn't exist?") is usually a silent branch switch — re-check before assuming the file system broke.
Handoff: planner → implementer
When the planner finishes drafting a feature's PRD/design/tasks and the implementer should start:
jj squash --from <plans-commit> --into feature/<YYWWD>-<feature-slug> dev/plans/<YYWWD>-<feature-slug>
Only the specific plan dir moves; the rest of the plans commit stays intact. The implementer, operating on feature/<YYWWD>-<feature-slug>, now sees that plan dir in its commit's history.
Handoff: implementer → planner (rare)
If the implementer discovers the plan needs a correction:
# On feature/<YYWWD>-<feature-slug>, after editing the plan dir in-place:
jj squash --from @ --into <plans-commit> dev/plans/<YYWWD>-<feature-slug>
Keeps the corrected plan on the shared plans commit so other feature branches eventually rebase onto the fix.
Shared-infrastructure edits (README.md, skills, tools)
Files shared across every branch — dev/README.md, dev/QUICKSTART.md, .claude/skills/**, dev/tools/** — live on a designated infrastructure commit (often the initial dev: add spec-driven workflow infrastructure commit or its descendant). Edits to these files should land on that commit specifically (jj edit <infra-commit>), not in a feature branch or plans commit, so every consumer rebases onto the update cleanly. An infrastructure commit edit rewrites descendants; jj auto-rebases them on your next operation.
Stranded work recovery
If a file write lands in the wrong commit (common when an agent missed a branch switch), recover with jj squash --from <wrong-commit> --into <right-commit> <path>. The path-restricted squash moves only the mis-landed files; the rest of both commits is untouched.
Coordination points
- Before starting a long session (drafting a PRD, processing a task list), the agent announces its working commit in a one-line message so the user and other agents see what's active.
- Before a branch switch (
jj new,jj edit,jj bookmark --move), the agent signals the user — the switch affects every agent's working tree. - Prefer in-place edits on the current commit over
jj newwhen possible — fewer working-tree flips for other agents to notice.
Upstream boundary (forks only)
If this repository is a fork and dev/ artifacts should never land in a PR against the upstream, set the environment variable UPSTREAM_BLOCKED_PATTERN to a substring that matches the upstream's remote URL (e.g. upstream-org/upstream-repo). Then install the pre-push guard:
./dev/tools/install-hooks.sh
The guard at .git/hooks/pre-push (symlinked from dev/tools/pre-push-guard.sh) checks the push target's remote URL against UPSTREAM_BLOCKED_PATTERN and refuses pushes whose diff touches any dev/ path. Pushes to other remotes (your personal fork, your team's private mirror, etc.) are unaffected.
For greenfield projects with no upstream to protect against, leave UPSTREAM_BLOCKED_PATTERN unset — the guard becomes a no-op and you can skip installing it.