template-jj/dev/plans/26174-planctl-task-cmds/tasks.md
sid 19154d8968 dev: backfill task-cmds plan dir + session-inventory note in skill docs
* Brings dev/plans/26174-planctl-task-cmds/{prd,design,tasks,codex-sessions}.md onto main (was on a sibling commit during the merge)
* Adds a session-inventory consumption note to all four planning skill docs
2026-04-26 09:52:49 -06:00

21 KiB

Tasks — planctl v2 task-state commands

Source PRD: prd.md Source design: design.md

Relevant Files

  • cmd/planctl/tasks.go — New file: TaskRecord type, stripCheckboxPrefix, parseTaskID, buildTaskRecords, buildTaskRecordsFromScan, findTaskByID, atomicRewriteTaskLine, evalPlanStatus.
  • cmd/planctl/tasks_test.go — New file: unit tests for all functions in tasks.go.
  • cmd/planctl/main.go — Add runNext, runList, runComplete, runStatus handlers; rewrite switch subcommand dispatch; update printUsage.
  • cmd/planctl/main_test.go — Add per-subcommand --help tests; update TestRun_V2ReservedSubcommands as stubs are replaced; delete it in task 6.4 once all stubs are replaced.
  • cmd/planctl/emit.go — Append emitNext, emitList, emitComplete, emitStatus formatter functions.
  • cmd/planctl/emit_test.go — Add unit tests for the four new emit functions.
  • cmd/planctl/testdata/v2/ — New golden-file fixture tree: ~27 fixture dirs (flat layout; subcmd name is part of fixture dir name, e.g. next-open-tasks/).

Notes

  • Run tests with go test ./cmd/planctl/....
  • Regenerate golden files with go test ./cmd/planctl/... -update.
  • Mutation tests (complete-success, complete-triggers-lint, complete-dry-run) must copy the fixture directory to t.TempDir() before calling runComplete — never mutate files inside testdata/.
  • The dry-run SHA-256 invariant (M6) is verified by comparing sha256.Sum256 on tasks.md bytes before and after the runComplete --dry-run call.
  • testdata/v2/ requires adding "v2" to the classes slice in TestGoldenFixtures (task 6.0).
  • Fixture layout: flat — fixtures live at testdata/v2/<subcmd>-<case>/ (e.g. testdata/v2/next-open-tasks/), not the two-level nested layout shown in design §7.1. The flat layout works with the existing harness without changes; the nested layout would require harness modifications. The design §7.1 table describes intent; the flat naming is the implementation decision.
  • Multi-plan fixtures follow the same layout as testdata/multi/case-c/: dev/plans/<plan1>/ + dev/plans/<plan2>/ inside the fixture root, args.txt with subcommand only (no explicit plan-dir), no cwd-rel.txt (fixture root is the default cwd).
  • Source-code traceability format: // spec:planctl-task-cmds/<ref> where <ref> is R<n.m>, D§<n>, or T<n.m>.
  • Follow the repo's VCS convention: jj commands per AGENTS.md.

Instructions for Completing Tasks

As you complete each task, flip [ ] to [x] in this file. Update after each sub-task, not just per parent.

Tasks

  • 0.0 Create feature branch Requirements: infra

    • 0.1 Start jj change on main: jj new main -m "feat: planctl-task-cmds" Requirements: infra
    • 0.2 Create bookmark: jj bookmark create feature/26174-planctl-task-cmds Requirements: infra
  • 1.0 Add tasks.go: TaskRecord type and core helpers Requirements: R1.2, R1.3, R2.4, R2.5, R3.1, R3.4 Design: D§2, D§3

    • 1.1 Create cmd/planctl/tasks.go with package main; define TaskRecord struct embedding TaskLine (from index.go) with fields Text string, ID string, ReqTags []string, DesignTags []string Requirements: R1.2, R1.3, R2.4, R2.5 Design: D§3
    • 1.2 Add stripCheckboxPrefix(line string) string — strip ^\s*- \[[ x]\] from a raw checkbox line, returning the task description text Requirements: R1.2 Design: D§3
    • 1.3 Add var taskIDPattern = regexp.MustCompile( + "" + ^(\d+(?:.\d+)+)\s + "" + ) and parseTaskID(text string) string that returns "T" + match[1] or "" for tasks with no leading N.M numeric prefix; emit (no-id) in output when ID is empty Requirements: R1.3, R3.1, R3.4 Design: D§3, D§6
    • 1.4 Add buildTaskRecords(idx Index, scan *ScanResult) []TaskRecord — iterate idx.TaskLines; for each, get scan.Lines[tl.Line-1], call stripCheckboxPrefix + parseTaskID; collect same-line TagRefs from idx.TaskTags by ref.Line == tl.Line into ReqTags/DesignTags Requirements: R1.2, R2.4, R2.5 Design: D§2, D§3
    • 1.5 Add buildTaskRecordsFromScan(scan *ScanResult) []TaskRecord — lightweight variant for runComplete pre-write phase: call extractTaskLines(scan) + extractTaskTags(scan) directly (no full BuildIndex) then build records Requirements: R3.1 Design: D§3
    • 1.6 Add findResult type with constants findOK, findBadFormat, findNotFound; add findTaskByID(records []TaskRecord, ref string) (*TaskRecord, findResult, []string)findBadFormat when ref doesn't start with T or suffix isn't N.M; findNotFound returns sorted slice of available non-empty IDs; findOK returns the matched record Requirements: R3.1, R3.4 Design: D§3
    • 1.7 Add atomicRewriteTaskLine(path string, lineNum int) (oldText, newText string, err error) — 10-step per D§3.5: os.ReadFile → split on "\n" → validate lines[lineNum-1] contains "- [ ]" → replace only that line → os.Stat(path) for mode → os.CreateTemp(filepath.Dir(path), ".planctl-*.tmp") → write modified bytes → os.Chmod(tmp, mode)os.Rename(tmp, path) → return old/new line text; preserve trailing newline Requirements: R3.2, R3.7 Design: D§3, D§4
    • 1.8 Add PlanStatus string type with constants StatusLintError, StatusNeedsCloseout, StatusDone, StatusNotStarted, StatusInProgress; add evalPlanStatus(diags []Diagnostic, tasks []TaskRecord, plan *Plan) PlanStatus using R4.5 precedence: (1) any SevErrorlint_error; (2) all checked + missing closeout file(s) → needs_closeout; (3) all checked + closeout present → done; (4) zero checked → not_started; (5) else → in_progress Requirements: R4.5 Design: D§3
    • 1.9 Create cmd/planctl/tasks_test.go with table-driven tests: TestParseTaskID (valid "2.0 Create...""T2.0", "10.3 ...""T10.3", bare "2 Create""", no prefix → "", "T2.1" raw T-input → treat as no-match since text wouldn't start with T), TestFindTaskByID (found, not found lists available IDs, bad format), TestEvalPlanStatus (all five branches), TestBuildTaskRecords (tag association by line, no-tag task, text stripping) Requirements: R1.2, R1.3, R3.1, R3.4, R4.5 Design: D§3
  • 2.0 Implement planctl next subcommand Requirements: R1.1, R1.2, R1.3, R1.4, R1.5, R1.6, R1.7 Design: D§2.1, D§4

    • 2.1 Add runNext(args []string, stdout, stderr io.Writer) int in main.go: parse --format + --help; call resolvePlans; for each plan loadPlanBuildIndexbuildTaskRecords; exit 2 if any plan's tasks.md is missing; return first TaskRecord where !tl.Checked in directory-sort order; emit via emitNext Requirements: R1.1, R1.2, R1.5, R1.6, R1.7 Design: D§2.1
    • 2.2 Add emitNext(w io.Writer, record *TaskRecord, format string) int in emit.go: text found: T<id> <text>\n Tags: _Requirements: R1.1, R1.2_ _Design: D§3_\n File: <path>:<line> (omit Tags row when no tags; emit (no-id) when ID=="", strip combined req+design tags into single Tags line); text not-found: No open tasks.\n; JSON found: object per R1.3 with parent = "T" + leading integer of task_id; JSON not-found: {"open":false} Requirements: R1.3, R1.4 Design: D§4
    • 2.3 Wire case "next": return runNext(rest, stdout, stderr) in the switch subcommand block; replace the current case "list", "next", "complete", "new-plan": multi-case with separate cases: case "next": and case "list", "complete", "new-plan": (retain the stub for the others); remove "next" from TestRun_V2ReservedSubcommands in main_test.go Requirements: R5.5 Design: D§1
    • 2.4 Create fixture testdata/v2/next-open-tasks/ — plan with one [x] task and one [ ] task; args.txt = next\nFIXTURE; generate expected.golden + expected.exit = 0 via -update; add a sibling JSON variant testdata/v2/next-open-tasks-json/ with args.txt = next\n--format=json\nFIXTURE Requirements: R1.2, R1.3 Design: D§4
    • 2.5 Create fixture testdata/v2/next-no-open-tasks/ — all tasks [x]; args.txt = next\nFIXTURE; expected.golden = No open tasks.\n; expected.exit = 0 Requirements: R1.4 Design: D§4
    • 2.6 Create fixture testdata/v2/next-missing-tasks/ — plan dir with only prd.md, no tasks.md; args.txt = next\nFIXTURE; expected.exit = 2 Requirements: R1.5 Design: D§2.1
    • 2.7 Create fixture testdata/v2/next-multi/dev/plans/26170-alpha/ (all tasks [x]) and dev/plans/26172-beta/ (one [ ] task); args.txt = next (no explicit dir; Case C from fixture root); assert expected.golden names beta's open task Requirements: R1.7 Design: D§2.1
    • 2.8 Add TestSubcmd_NextHelp to main_test.go: run(["next","--help"], ...) → exit 0, stdout contains "next", a usage line, and an exit-code table Requirements: R5.3 Design: D§1
  • 3.0 Implement planctl list subcommand Requirements: R2.1, R2.2, R2.3, R2.4, R2.5, R2.6, R2.7 Design: D§2.1, D§4

    • 3.1 Add runList(args []string, stdout, stderr io.Writer) int in main.go: parse --format, --all, --help; call resolvePlans; for each plan loadPlanBuildIndexbuildTaskRecords; exit 2 (naming the plan) if any plan is missing tasks.md; filter to unchecked only unless --all Requirements: R2.1, R2.2, R2.3, R2.7 Design: D§2.1
    • 3.2 Add emitList in emit.go — text single-plan: one row per task [ ] T2.1 <text padded> _Requirements:_; text multi-plan: === <plan-dir> === header before each group, Total: N open across M plans at end; JSON single-plan: object per R2.5 {"plan","open_count","total_count","tasks":[...]} each task includes "checked"; JSON multi-plan: object per R2.6 {"plans":[...],"total_open","total_tasks"} Requirements: R2.4, R2.5, R2.6 Design: D§4
    • 3.3 Wire case "list": return runList(rest, stdout, stderr) in main.go; update the stub case to case "complete", "new-plan": only; remove "list" from TestRun_V2ReservedSubcommands in main_test.go Requirements: R5.5 Design: D§1
    • 3.4 Create fixture testdata/v2/list-open-only/ — three tasks (one [x], two [ ]); args.txt = list\nFIXTURE; expected.exit = 0; golden shows only the two open tasks Requirements: R2.2 Design: D§4
    • 3.5 Create fixture testdata/v2/list-all/ — same plan structure; args.txt = list\n--all\nFIXTURE; golden shows all three tasks with [ ]/[x] markers Requirements: R2.3 Design: D§4
    • 3.6 Create fixture testdata/v2/list-multi/ — two plan dirs (Case C); one open task each; args.txt = list; golden shows === === headers and Total: 2 open across 2 plans Requirements: R2.6 Design: D§4
    • 3.7 Create fixture testdata/v2/list-missing-tasks/ — plan dir with only prd.md; args.txt = list\nFIXTURE; expected.exit = 2 Requirements: R2.7 Design: D§2.1
    • 3.8 Create fixture testdata/v2/list-multi-missing-tasks/ — two plan dirs (Case C), one missing tasks.md; args.txt = list; assert expected.exit = 2 and golden or stderr names the missing plan Requirements: R2.7 Design: D§2.1
    • 3.9 Create fixture testdata/v2/list-open-only-json/ — same plan as 3.4; args.txt = list\n--format=json\nFIXTURE; golden is JSON object per R2.5 with open_count, total_count, tasks array; expected.exit = 0 (satisfies M4 JSON coverage for list) Requirements: R2.5 Design: D§4
    • 3.10 Add TestSubcmd_ListHelp to main_test.go: run(["list","--help"], ...) → exit 0, stdout mentions "list", --all, exit-code table Requirements: R5.3 Design: D§1
  • 4.0 Implement planctl complete subcommand Requirements: R3.1, R3.2, R3.3, R3.4, R3.5, R3.6, R3.7, R3.8, R3.9 Design: D§2.2, D§3, D§4

    • 4.1 Add runComplete(args []string, stdout, stderr io.Writer) int in main.go: parse --format, --dry-run, --help, and positional <task-ref>; call resolvePlans; if len(plans) > 1 exit 2 with count (D§5); call loadPlan; exit 2 if tasks.md missing; call buildTaskRecordsFromScan; call findTaskByID → handle findBadFormat (exit 1 + usage hint) and findNotFound (exit 1 + list available IDs); if already [x] print message and exit 0; if --dry-run call emitComplete without writing; else call atomicRewriteTaskLineloadPlan reload → BuildIndexlintPlanemitComplete Requirements: R3.1, R3.2, R3.3, R3.4, R3.5, R3.6, R3.7, R3.8 Design: D§2.2, D§3
    • 4.2 Add emitComplete in emit.go — text live: Completed T2.1 (line 42). N diagnostics.\n followed by diagnostic lines if any; text dry-run: Would change line 42: "- [ ] ..." → "- [x] ..."\n; JSON per R3.9: {"dry_run":bool,"task_id":"T2.1","line":42,"old_text":"...","new_text":"...","diagnostics":[...]} Requirements: R3.8, R3.9 Design: D§4
    • 4.3 Wire case "complete": return runComplete(rest, stdout, stderr) in main.go; update stub to case "new-plan": only; remove "complete" from TestRun_V2ReservedSubcommands in main_test.go Requirements: R5.5 Design: D§1
    • 4.4 Create source files for testdata/v2/complete-success/ — plan with one open task (with valid T-ID); write TestCompleteSuccess in main_test.go: copy fixture to t.TempDir(), call run(["complete","T<id>","<tmpdir>"], ...), assert exit 0, tasks.md in tmpdir now contains [x], lint clean Requirements: R3.2, R3.6 Design: D§4
    • 4.5 Create source files for testdata/v2/complete-already-done/ — one task already [x]; write TestCompleteAlreadyDone: copy to tempdir, assert exit 0, file unchanged (SHA-256 identical) Requirements: R3.3 Design: D§4
    • 4.6 Create fixture testdata/v2/complete-not-found/ — plan with tasks T1.0 and T1.1; args.txt = complete\nT9.9\nFIXTURE; expected.exit = 1; golden (or stderr-captured) lists available IDs Requirements: R3.4 Design: D§4
    • 4.7 Create fixture testdata/v2/complete-missing-tasks/ — plan dir with only prd.md; args.txt = complete\nT1.0\nFIXTURE; expected.exit = 2 Requirements: R3.5 Design: D§2.2
    • 4.8 Create fixture testdata/v2/complete-multi-reject/ — two plan dirs (Case C) with no explicit plan-dir arg; args.txt = complete\nT1.0; expected.exit = 2; golden/stderr contains the count of plans found Requirements: R5.2 Design: D§2.2, D§5
    • 4.9 Create source files for testdata/v2/complete-triggers-lint/ — plan with one final open task and no codex-sessions.md; write TestCompleteTriggersLint: copy to tempdir, complete the last task, assert exit 1, output contains missing-closeout-file Requirements: R3.6 Design: D§2.2
    • 4.10 Create source files for testdata/v2/complete-dry-run/ — plan with one open task; write TestCompleteDryRun: copy to tempdir, call run(["complete","--dry-run","T<id>","<tmpdir>"], ...), assert exit 0, stdout contains Would change line, and sha256.Sum256 of tasks.md is identical before and after (verifies M6) Requirements: R3.8 Design: D§4
    • 4.11 Create fixture testdata/v2/complete-dry-run-json/ — same plan as 4.10; args.txt = complete\n--dry-run\n--format=json\nT<id>\nFIXTURE; golden is JSON object per R3.9 with "dry_run":true, task_id, line, old_text, new_text, diagnostics:[]; expected.exit = 0 (satisfies M4 JSON coverage for complete) Requirements: R3.9 Design: D§4
    • 4.12 Add TestSubcmd_CompleteHelp to main_test.go: run(["complete","--help"], ...) → exit 0, stdout mentions "complete", --dry-run, <task-ref>, snapshotting responsibility note, and exit-code table Requirements: R5.3 Design: D§1
  • 5.0 Implement planctl status subcommand Requirements: R4.1, R4.2, R4.3, R4.4, R4.5, R4.6, R4.7 Design: D§2.3, D§3, D§4

    • 5.1 Add runStatus(args []string, stdout, stderr io.Writer) int in main.go: parse --format, --strict, --help; call resolvePlans; for each plan: loadPlanBuildIndexlintPlan(p, lintFlags{strict: strict})buildTaskRecordsevalPlanStatus; collect per-plan results and call emitStatus Requirements: R4.1, R4.2, R4.6, R4.7 Design: D§2.3
    • 5.2 Add emitStatus in emit.go — text single-plan: four-line block per R4.3 (Plan:, Tasks:, Lint:, Close:, blank line, Status: <LABEL>); text multi-plan: one block per plan + aggregate row; JSON single-plan: object per R4.4; JSON multi-plan: array + totals per R4.6; computeStatusExit returns the exit code per R4.5 precedence (1 for lint_error/needs_closeout/not_started/in_progress, 0 for done) Requirements: R4.3, R4.4, R4.5, R4.6 Design: D§4
    • 5.3 Wire case "status": return runStatus(rest, stdout, stderr) in main.go (previously fell through to default:); no TestRun_V2ReservedSubcommands update needed since status was never in that list Requirements: R5.5 Design: D§1, D§8
    • 5.4 Create fixture testdata/v2/status-not-started/ — plan with two [ ] tasks; args.txt = status\nFIXTURE; expected.exit = 1; golden contains Status: NOT STARTED Requirements: R4.5 Design: D§4
    • 5.5 Create fixture testdata/v2/status-in-progress/ — one [x] task, one [ ] task; expected.exit = 1; golden contains Status: IN PROGRESS Requirements: R4.5 Design: D§4
    • 5.6 Create fixture testdata/v2/status-lint-error/ — plan with an orphan-requirement (tasks.md cites an R-id not in prd.md); expected.exit = 1; golden contains Status: LINT ERROR and Lint: FAIL Requirements: R4.2, R4.5 Design: D§4
    • 5.7 Create fixture testdata/v2/status-needs-closeout/ — all tasks [x], lint clean, no codex-sessions.md; expected.exit = 1; golden contains Status: NEEDS CLOSEOUT and names the missing file Requirements: R4.2, R4.5 Design: D§4
    • 5.8 Create fixture testdata/v2/status-done/ — all tasks [x], lint clean, codex-sessions.md present; expected.exit = 0; golden contains Status: DONE Requirements: R4.5 Design: D§4
    • 5.9 Create fixture testdata/v2/status-multi/ — two plan dirs (Case C) with different states (not_started and in_progress); args.txt = status; golden shows both plan blocks and aggregate Requirements: R4.6 Design: D§4
    • 5.10 Create two fixtures for --strict behavior: testdata/v2/status-strict-warnings/ (no --strict, warnings-only plan, expected.exit = 0) and testdata/v2/status-strict-warnings-strict/ (args.txt = status\n--strict\nFIXTURE, expected.exit = 1) Requirements: R5.6 Design: D§1
    • 5.11 Create fixture testdata/v2/status-done-json/ — same plan as 5.8 (all tasks done, lint clean, closeout present); args.txt = status\n--format=json\nFIXTURE; golden is JSON object per R4.4 with "status":"done", tasks.open:0; expected.exit = 0 (satisfies M4 JSON coverage for status) Requirements: R4.4 Design: D§4
    • 5.12 Add TestSubcmd_StatusHelp to main_test.go: run(["status","--help"], ...) → exit 0, stdout mentions "status", --strict, and exit-code table Requirements: R5.3 Design: D§1
  • 6.0 CLI dispatch rewrite, printUsage update, and integration tests Requirements: R5.1, R5.2, R5.3, R5.4, R5.5, R5.6 Design: D§1, D§7, D§8

    • 6.1 In main.go: remove the final case "new-plan": stub entirely (it falls to default:); verify the switch subcommand block now has only lint, next, list, complete, status, and default: Requirements: R5.5 Design: D§1, D§8
    • 6.2 Update printUsage in main.go: remove the v2-reserved subcommands block and new-plan entry; add a Subcommands: section listing all five subcommands (lint, next, list, complete, status) with one-line descriptions; note per-subcommand --help for full flag docs Requirements: R5.3, R5.5 Design: D§1
    • 6.3 Update TestRun_Help in main_test.go: assert stdout contains all five subcommand names and does not contain new-plan or v2-reserved Requirements: R5.3 Design: D§1
    • 6.4 Delete TestRun_V2ReservedSubcommands from main_test.go (all stubs replaced; new-plan now hits the default: unknown-subcommand case, already covered by TestRun_UnknownSubcommand) Requirements: R5.5 Design: D§8
    • 6.5 Add "v2" to the classes slice in TestGoldenFixtures in main_test.go so the harness sweeps all testdata/v2/<fixture>/ directories Requirements: R5.1 Design: D§7
    • 6.6 Add emit unit tests to emit_test.go: TestEmitNext (text + JSON, found + not-found), TestEmitList (text default + --all, multi-plan text + JSON), TestEmitComplete (text + JSON, dry-run true/false, with diagnostics), TestEmitStatus (text + JSON for each of the five status values) Requirements: R1.3, R1.4, R2.4, R2.5, R2.6, R3.8, R3.9, R4.3, R4.4 Design: D§4, D§7
    • 6.7 Run go test ./cmd/planctl/... -count=1 — all v1 golden fixtures plus new v2 fixtures green; verify M5 (no regressions) and Q1 (≥ 80% line coverage: go test ./cmd/planctl/... -coverprofile=cover.out && go tool cover -func=cover.out) Requirements: R5.1, R5.2 Design: D§7