* 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
21 KiB
Tasks — planctl v2 task-state commands
Source PRD: prd.md
Source design: design.md
Relevant Files
cmd/planctl/tasks.go— New file:TaskRecordtype,stripCheckboxPrefix,parseTaskID,buildTaskRecords,buildTaskRecordsFromScan,findTaskByID,atomicRewriteTaskLine,evalPlanStatus.cmd/planctl/tasks_test.go— New file: unit tests for all functions intasks.go.cmd/planctl/main.go— AddrunNext,runList,runComplete,runStatushandlers; rewriteswitch subcommanddispatch; updateprintUsage.cmd/planctl/main_test.go— Add per-subcommand--helptests; updateTestRun_V2ReservedSubcommandsas stubs are replaced; delete it in task 6.4 once all stubs are replaced.cmd/planctl/emit.go— AppendemitNext,emitList,emitComplete,emitStatusformatter 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 tot.TempDir()before callingrunComplete— never mutate files insidetestdata/. - The dry-run SHA-256 invariant (M6) is verified by comparing
sha256.Sum256ontasks.mdbytes before and after therunComplete --dry-runcall. testdata/v2/requires adding"v2"to theclassesslice inTestGoldenFixtures(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.txtwith subcommand only (no explicit plan-dir), nocwd-rel.txt(fixture root is the default cwd). - Source-code traceability format:
// spec:planctl-task-cmds/<ref>where<ref>isR<n.m>,D§<n>, orT<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-cmdsRequirements: infra
- 0.1 Start jj change on main:
-
1.0 Add
tasks.go:TaskRecordtype 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.gowithpackage main; defineTaskRecordstruct embeddingTaskLine(fromindex.go) with fieldsText string,ID string,ReqTags []string,DesignTags []stringRequirements: 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+ "" +)andparseTaskID(text string) stringthat returns"T" + match[1]or""for tasks with no leadingN.Mnumeric 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— iterateidx.TaskLines; for each, getscan.Lines[tl.Line-1], callstripCheckboxPrefix+parseTaskID; collect same-lineTagRefs fromidx.TaskTagsbyref.Line == tl.LineintoReqTags/DesignTagsRequirements: R1.2, R2.4, R2.5 Design: D§2, D§3 - 1.5 Add
buildTaskRecordsFromScan(scan *ScanResult) []TaskRecord— lightweight variant forrunCompletepre-write phase: callextractTaskLines(scan)+extractTaskTags(scan)directly (no fullBuildIndex) then build records Requirements: R3.1 Design: D§3 - 1.6 Add
findResulttype with constantsfindOK,findBadFormat,findNotFound; addfindTaskByID(records []TaskRecord, ref string) (*TaskRecord, findResult, []string)—findBadFormatwhen ref doesn't start withTor suffix isn'tN.M;findNotFoundreturns sorted slice of available non-empty IDs;findOKreturns 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"→ validatelines[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
PlanStatusstring type with constantsStatusLintError,StatusNeedsCloseout,StatusDone,StatusNotStarted,StatusInProgress; addevalPlanStatus(diags []Diagnostic, tasks []TaskRecord, plan *Plan) PlanStatususing R4.5 precedence: (1) anySevError→lint_error; (2) all checked + missing closeout file(s) →needs_closeout; (3) all checked + closeout present →done; (4) zero checked →not_started; (5) else →in_progressRequirements: R4.5 Design: D§3 - 1.9 Create
cmd/planctl/tasks_test.gowith 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
- 1.1 Create
-
2.0 Implement
planctl nextsubcommand 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) intinmain.go: parse--format+--help; callresolvePlans; for each planloadPlan→BuildIndex→buildTaskRecords; exit 2 if any plan'stasks.mdis missing; return firstTaskRecordwhere!tl.Checkedin directory-sort order; emit viaemitNextRequirements: 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) intinemit.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)whenID=="", strip combined req+design tags into single Tags line); text not-found:No open tasks.\n; JSON found: object per R1.3 withparent="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 theswitch subcommandblock; replace the currentcase "list", "next", "complete", "new-plan":multi-case with separate cases:case "next":andcase "list", "complete", "new-plan":(retain the stub for the others); remove"next"fromTestRun_V2ReservedSubcommandsinmain_test.goRequirements: 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; generateexpected.golden+expected.exit=0via-update; add a sibling JSON varianttestdata/v2/next-open-tasks-json/withargs.txt=next\n--format=json\nFIXTURERequirements: 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=0Requirements: R1.4 Design: D§4 - 2.6 Create fixture
testdata/v2/next-missing-tasks/— plan dir with onlyprd.md, notasks.md;args.txt=next\nFIXTURE;expected.exit=2Requirements: R1.5 Design: D§2.1 - 2.7 Create fixture
testdata/v2/next-multi/—dev/plans/26170-alpha/(all tasks[x]) anddev/plans/26172-beta/(one[ ]task);args.txt=next(no explicit dir; Case C from fixture root); assertexpected.goldennames beta's open task Requirements: R1.7 Design: D§2.1 - 2.8 Add
TestSubcmd_NextHelptomain_test.go:run(["next","--help"], ...)→ exit 0, stdout contains"next", a usage line, and an exit-code table Requirements: R5.3 Design: D§1
- 2.1 Add
-
3.0 Implement
planctl listsubcommand 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) intinmain.go: parse--format,--all,--help; callresolvePlans; for each planloadPlan→BuildIndex→buildTaskRecords; exit 2 (naming the plan) if any plan is missingtasks.md; filter to unchecked only unless--allRequirements: R2.1, R2.2, R2.3, R2.7 Design: D§2.1 - 3.2 Add
emitListinemit.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 plansat 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)inmain.go; update the stub case tocase "complete", "new-plan":only; remove"list"fromTestRun_V2ReservedSubcommandsinmain_test.goRequirements: 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 andTotal: 2 open across 2 plansRequirements: R2.6 Design: D§4 - 3.7 Create fixture
testdata/v2/list-missing-tasks/— plan dir with onlyprd.md;args.txt=list\nFIXTURE;expected.exit=2Requirements: R2.7 Design: D§2.1 - 3.8 Create fixture
testdata/v2/list-multi-missing-tasks/— two plan dirs (Case C), one missingtasks.md;args.txt=list; assertexpected.exit=2and 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 withopen_count,total_count,tasksarray;expected.exit=0(satisfies M4 JSON coverage forlist) Requirements: R2.5 Design: D§4 - 3.10 Add
TestSubcmd_ListHelptomain_test.go:run(["list","--help"], ...)→ exit 0, stdout mentions"list",--all, exit-code table Requirements: R5.3 Design: D§1
- 3.1 Add
-
4.0 Implement
planctl completesubcommand 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) intinmain.go: parse--format,--dry-run,--help, and positional<task-ref>; callresolvePlans; iflen(plans) > 1exit 2 with count (D§5); callloadPlan; exit 2 iftasks.mdmissing; callbuildTaskRecordsFromScan; callfindTaskByID→ handlefindBadFormat(exit 1 + usage hint) andfindNotFound(exit 1 + list available IDs); if already[x]print message and exit 0; if--dry-runcallemitCompletewithout writing; else callatomicRewriteTaskLine→loadPlanreload →BuildIndex→lintPlan→emitCompleteRequirements: 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
emitCompleteinemit.go— text live:Completed T2.1 (line 42). N diagnostics.\nfollowed 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)inmain.go; update stub tocase "new-plan":only; remove"complete"fromTestRun_V2ReservedSubcommandsinmain_test.goRequirements: R5.5 Design: D§1 - 4.4 Create source files for
testdata/v2/complete-success/— plan with one open task (with valid T-ID); writeTestCompleteSuccessinmain_test.go: copy fixture tot.TempDir(), callrun(["complete","T<id>","<tmpdir>"], ...), assert exit 0,tasks.mdin 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]; writeTestCompleteAlreadyDone: 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 tasksT1.0andT1.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 onlyprd.md;args.txt=complete\nT1.0\nFIXTURE;expected.exit=2Requirements: 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 nocodex-sessions.md; writeTestCompleteTriggersLint: copy to tempdir, complete the last task, assert exit 1, output containsmissing-closeout-fileRequirements: R3.6 Design: D§2.2 - 4.10 Create source files for
testdata/v2/complete-dry-run/— plan with one open task; writeTestCompleteDryRun: copy to tempdir, callrun(["complete","--dry-run","T<id>","<tmpdir>"], ...), assert exit 0, stdout containsWould change line, andsha256.Sum256oftasks.mdis 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 forcomplete) Requirements: R3.9 Design: D§4 - 4.12 Add
TestSubcmd_CompleteHelptomain_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
- 4.1 Add
-
5.0 Implement
planctl statussubcommand 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) intinmain.go: parse--format,--strict,--help; callresolvePlans; for each plan:loadPlan→BuildIndex→lintPlan(p, lintFlags{strict: strict})→buildTaskRecords→evalPlanStatus; collect per-plan results and callemitStatusRequirements: R4.1, R4.2, R4.6, R4.7 Design: D§2.3 - 5.2 Add
emitStatusinemit.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;computeStatusExitreturns 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)inmain.go(previously fell through todefault:); noTestRun_V2ReservedSubcommandsupdate needed sincestatuswas 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 containsStatus: NOT STARTEDRequirements: R4.5 Design: D§4 - 5.5 Create fixture
testdata/v2/status-in-progress/— one[x]task, one[ ]task;expected.exit=1; golden containsStatus: IN PROGRESSRequirements: 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 containsStatus: LINT ERRORandLint: FAILRequirements: R4.2, R4.5 Design: D§4 - 5.7 Create fixture
testdata/v2/status-needs-closeout/— all tasks[x], lint clean, nocodex-sessions.md;expected.exit=1; golden containsStatus: NEEDS CLOSEOUTand 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.mdpresent;expected.exit=0; golden containsStatus: DONERequirements: R4.5 Design: D§4 - 5.9 Create fixture
testdata/v2/status-multi/— two plan dirs (Case C) with different states (not_startedandin_progress);args.txt=status; golden shows both plan blocks and aggregate Requirements: R4.6 Design: D§4 - 5.10 Create two fixtures for
--strictbehavior:testdata/v2/status-strict-warnings/(no--strict, warnings-only plan,expected.exit=0) andtestdata/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 forstatus) Requirements: R4.4 Design: D§4 - 5.12 Add
TestSubcmd_StatusHelptomain_test.go:run(["status","--help"], ...)→ exit 0, stdout mentions"status",--strict, and exit-code table Requirements: R5.3 Design: D§1
- 5.1 Add
-
6.0 CLI dispatch rewrite,
printUsageupdate, 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 finalcase "new-plan":stub entirely (it falls todefault:); verify theswitch subcommandblock now has onlylint,next,list,complete,status, anddefault:Requirements: R5.5 Design: D§1, D§8 - 6.2 Update
printUsageinmain.go: remove thev2-reserved subcommandsblock andnew-planentry; add aSubcommands:section listing all five subcommands (lint,next,list,complete,status) with one-line descriptions; note per-subcommand--helpfor full flag docs Requirements: R5.3, R5.5 Design: D§1 - 6.3 Update
TestRun_Helpinmain_test.go: assert stdout contains all five subcommand names and does not containnew-planorv2-reservedRequirements: R5.3 Design: D§1 - 6.4 Delete
TestRun_V2ReservedSubcommandsfrommain_test.go(all stubs replaced;new-plannow hits thedefault:unknown-subcommand case, already covered byTestRun_UnknownSubcommand) Requirements: R5.5 Design: D§8 - 6.5 Add
"v2"to theclassesslice inTestGoldenFixturesinmain_test.goso the harness sweeps alltestdata/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
- 6.1 In