@stylusnexus/work-plan 2026.6.9 → 2026.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +91 -13
  2. package/VERSION +1 -1
  3. package/bin/work-plan +23 -0
  4. package/package.json +2 -2
  5. package/skills/work-plan/SKILL.md +41 -8
  6. package/skills/work-plan/commands/auto_triage.py +243 -0
  7. package/skills/work-plan/commands/batch_slot.py +184 -0
  8. package/skills/work-plan/commands/brief.py +6 -6
  9. package/skills/work-plan/commands/canonicalize.py +71 -17
  10. package/skills/work-plan/commands/close.py +21 -6
  11. package/skills/work-plan/commands/coverage.py +100 -0
  12. package/skills/work-plan/commands/duplicates.py +21 -8
  13. package/skills/work-plan/commands/group.py +86 -10
  14. package/skills/work-plan/commands/handoff.py +17 -5
  15. package/skills/work-plan/commands/hygiene.py +29 -3
  16. package/skills/work-plan/commands/init.py +39 -7
  17. package/skills/work-plan/commands/init_repo.py +43 -1
  18. package/skills/work-plan/commands/list_cmd.py +34 -6
  19. package/skills/work-plan/commands/move.py +131 -0
  20. package/skills/work-plan/commands/new_track.py +100 -23
  21. package/skills/work-plan/commands/reconcile.py +175 -33
  22. package/skills/work-plan/commands/refresh_md.py +19 -6
  23. package/skills/work-plan/commands/set_field.py +17 -7
  24. package/skills/work-plan/commands/slot.py +20 -5
  25. package/skills/work-plan/commands/where_was_i.py +23 -5
  26. package/skills/work-plan/lib/config.py +6 -0
  27. package/skills/work-plan/lib/export_model.py +57 -2
  28. package/skills/work-plan/lib/github_state.py +54 -13
  29. package/skills/work-plan/lib/notes_readme.py +38 -0
  30. package/skills/work-plan/lib/prompts.py +34 -3
  31. package/skills/work-plan/lib/tracks.py +208 -18
  32. package/skills/work-plan/tests/test_auto_triage.py +351 -0
  33. package/skills/work-plan/tests/test_batch_slot.py +291 -0
  34. package/skills/work-plan/tests/test_close_tier.py +166 -0
  35. package/skills/work-plan/tests/test_config_shared.py +57 -0
  36. package/skills/work-plan/tests/test_coverage.py +192 -0
  37. package/skills/work-plan/tests/test_export.py +204 -1
  38. package/skills/work-plan/tests/test_export_command.py +2 -2
  39. package/skills/work-plan/tests/test_github_state.py +52 -14
  40. package/skills/work-plan/tests/test_group_apply.py +411 -0
  41. package/skills/work-plan/tests/test_init_repo.py +128 -0
  42. package/skills/work-plan/tests/test_init_shared.py +185 -0
  43. package/skills/work-plan/tests/test_list_sort.py +162 -0
  44. package/skills/work-plan/tests/test_move.py +240 -0
  45. package/skills/work-plan/tests/test_new_track.py +169 -4
  46. package/skills/work-plan/tests/test_notes_readme.py +78 -0
  47. package/skills/work-plan/tests/test_prompts.py +121 -0
  48. package/skills/work-plan/tests/test_reconcile_move.py +154 -0
  49. package/skills/work-plan/tests/test_reconcile_readonly.py +92 -0
  50. package/skills/work-plan/tests/test_track_resolution.py +295 -0
  51. package/skills/work-plan/tests/test_tracks.py +395 -1
  52. package/skills/work-plan/tests/test_where_was_i.py +135 -0
  53. package/skills/work-plan/work_plan.py +38 -18
package/README.md CHANGED
@@ -7,7 +7,23 @@
7
7
 
8
8
  Track-aware daily work planning for developers running parallel Claude Code / Codex sessions across many GitHub issues.
9
9
 
10
- `work-plan` is a CLI-backed agent skill (a pure-Python-stdlib CLI + `SKILL.md`). It treats your daily work as a set of *tracks* — each track is a markdown file with YAML frontmatter listing its priority, milestone, GitHub issue numbers, and current status. The skill derives state live from GitHub (`gh`), git, and the markdown body, so the markdown stays light (it references issues by ID rather than duplicating their state).
10
+ ## What this is
11
+
12
+ A daily work-planning system for developers running parallel AI sessions across many GitHub issues. It's made of three things: a pure-Python stdlib CLI, a set of YAML-frontmattered markdown files ("tracks"), and a `SKILL.md` that tells your AI how to use the CLI. Together they give you and your agent a shared, live picture of what's in flight — without asking you to maintain it manually.
13
+
14
+ **Installs as a plugin** for Claude Code and Codex (see [Quick install](#quick-install) for the exact commands), as an npm global for any editor or terminal (`npm install -g @stylusnexus/work-plan`), and as a VS Code extension (search "Work Plan", publisher `stylusnexus`).
15
+
16
+ The system derives state *live* from GitHub (`gh`), `git`, and your track files on every run — nothing is mirrored or cached. AI sessions get a paste-ready context block; you stay oriented even when switching between a dozen parallel workstreams.
17
+
18
+ **Why it exists:** born from the frustration of building detailed plans that die the moment you open a new agent session and start over. Inspired by [Andrej Karpathy's notes on vibe coding](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) and the specific pain of doing enthusiastic work on the wrong thing.
19
+
20
+ ## What this is not
21
+
22
+ - **Not a Jira / Linear / GitHub Projects replacement.** It doesn't manage sprints, roadmaps, or capacity planning — it helps *you and your AI agent* stay oriented on GitHub issues you already have.
23
+ - **Not a standalone issue tracker.** GitHub is canonical; `work-plan` just reads and references it.
24
+ - **Not zero-setup.** Requires Python 3.9+, the `gh` GitHub CLI (authenticated), and `yq` (the Go version, not the Python one).
25
+ - **Not a background service.** No daemon, no cache, no sync loop — `git pull` is the sync mechanism for shared tracks.
26
+ - **Not a replacement for reading your code.** It tells you *what* to work on and *where you left off* — not what the code does.
11
27
 
12
28
  ## Quick install
13
29
 
@@ -35,16 +51,24 @@ The five essentials you'll use 80% of the time are:
35
51
  | `/work-plan brief` | Morning. Multi-track snapshot — what's on your plate across every active track. Add `--repo=<key>` to scope to one project. |
36
52
  | `/work-plan handoff <track>` | End of a work block. Captures what you touched. Use `--auto-next` for an algorithmic priority-sorted `next_up` (no LLM), `--set-next 1,2,3` for explicit numbers, or pair with Claude in chat for a curated pick. |
37
53
  | `/work-plan orient <track>` | Switching context. ~15-line paste-block of priority / last session / next pick / git state — drop into a fresh Claude Code terminal. |
38
- | `/work-plan reconcile <track> \| --all \| --repo=<key> [--draft]` | Track frontmatter membership drifted from GitHub labels. Use on label-driven tracks only — for hand-curated tracks, use `refresh-md` instead. `--draft` previews proposed ADDs/FLAGs without prompting or writing. `--repo=<key>` scopes the sweep to one repo. |
39
- | `/work-plan hygiene [--repo=<key>]` | Weekly. Refresh status icons, reconcile labels, scan for duplicates. `--repo=<key>` scopes steps 1–2 to one repo (duplicates is global, so it's skipped in scoped mode). |
54
+ | `/work-plan reconcile <track> \| --all \| --repo=<key> [--draft] [--yes]` | Track frontmatter membership drifted from GitHub labels. Use on label-driven tracks only — for hand-curated tracks, use `refresh-md` instead. In an `--all`/`--repo` sweep it also moves issues relabeled from one track to another in the same repo. `--draft` previews proposed ADDs/MOVEs/FLAGs; `--yes` applies without prompting. `--repo=<key>` scopes the sweep to one repo. |
55
+ | `/work-plan hygiene [--repo=<key>]` | **Weekly all-in-one cleanup.** Runs three steps: ① `refresh-md --all` (pull live GitHub state into every active track's status table), ② `reconcile --all` (sync frontmatter membership against GitHub labels), `duplicates` (flag likely-duplicate issues). `--repo=<key>` scopes steps and ② to one repo; step is skipped in scoped mode. |
56
+
57
+ A dozen more subcommands cover slotting new issues into tracks, closing tracks (shipped/abandoned/parked), and one-time priority-label backfill. Three capabilities worth calling out explicitly:
58
+
59
+ **Shared tracks** — track files can live *inside your repo clone* (`.work-plan/<slug>.md`) so teammates see the same planning state via `git pull`. The default is private (`notes_root/<repo>/`); register a local clone with `init-repo --local=<path>` to opt into shared mode. Pass `--private` to any write command to keep a specific track local. See [Shared tracks](#shared-tracks).
60
+
61
+ **AI-powered clustering (`group`)** — hand a flat list of GitHub issues to your AI and get back thematic track files. Run `group --milestone=X` to fetch all issues in a milestone, get a clustering prompt, save the JSON answer, then `group --apply` creates the tracks. Pairs with `auto-triage` for ongoing maintenance: once tracks exist, `auto-triage` assigns newly-filed untracked issues back into them.
62
+
63
+ **Coverage + auto-triage** — `coverage --repo=<key>` reports how many open issues fall outside the track model (42% on a real production repo). `auto-triage --repo=<key>` then produces an AI prompt to assign those orphans to existing tracks. Run both periodically to keep the backlog visible.
40
64
 
41
- A dozen more subcommands cover slotting new issues into tracks, closing tracks (shipped/abandoned/parked), AI-clustering raw GitHub issues into thematic tracks, and one-time priority-label backfill.
65
+ **Cross-track dependencies** set `depends_on: [<track-slug>]` in a track's frontmatter to declare explicit dependencies between tracks. The VS Code viewer renders these as thick amber `==>` edges in the dependency graph, and the detail panel shows clickable dependency chips that navigate directly to the dependent track. Set via `/work-plan set <track> depends_on=slug1,slug2` or the "Edit Track Fields" right-click menu in VS Code. Complementary to the issue-derived "owns" edges already inferred from blockers.
42
66
 
43
67
  Beyond issue tracking, **`plan-status`** answers a different question — *which of your accumulated plan/spec docs actually shipped, half-shipped, or died*. It correlates each plan's declared file-manifest (`Create:`/`Modify:`/`Test:` paths) against git and the filesystem rather than trusting checkboxes (which are routinely left unchecked even for shipped work). Read-only by default; optionally stamp the verdict into each doc (`--stamp`), get an AI verdict on prose/ambiguous docs (`--llm`), and act on the results behind confirmation gates (`--archive` dead plans, `--issues` for partial ones). See [Plan & doc liveness](#plan--doc-liveness-plan-status).
44
68
 
45
69
  ## How it works
46
70
 
47
- The toolkit treats GitHub as the canonical source of issue state and never tries to mirror it. Track markdown files are lightweight references — they list issue numbers and a few pieces of derived metadata (priority, milestone, `next_up`, last session timestamp). The CLI re-derives everything else live from `gh`, `git`, and the markdown body.
71
+ The toolkit treats GitHub as the canonical source of issue state and never tries to mirror it. Track markdown files are lightweight references — they list issue numbers and a few pieces of derived metadata (priority, milestone, `next_up`, `depends_on`, last session timestamp). The CLI re-derives everything else live from `gh`, `git`, and the markdown body.
48
72
 
49
73
  ```mermaid
50
74
  flowchart TB
@@ -84,7 +108,49 @@ flowchart TB
84
108
  - Free-form via Claude in your agent session, which can review project memory and write a curated list back. The two `--*-next` flags are the no-LLM paths.
85
109
  - For tracks where you don't want to bother curating at all, set `next_up_auto: true` in the track's frontmatter — `brief` will then derive the list live each invocation, ignoring whatever's stored.
86
110
  - **Weekly** → `hygiene` runs `refresh-md --all` + `reconcile --all` + `duplicates` in sequence to keep status icons, GitHub labels, and dedup state honest.
87
- > **When does the body status table get refreshed?** `handoff` already rewrites the ✅/🔲 icons for its own track on every run (live `gh` fetch → `update_row_status`). `brief` reads GitHub state live and never relies on the body table, so it's always accurate. The only drift `refresh-md` exists to fix is *cross-track*: a track you haven't `handoff`'d recently whose icons fell behind because issues moved while you were heads-down on a sibling track. That's why `hygiene --all` sweeps it weekly.
111
+
112
+ > **When should I run `refresh-md`?** Any time you close or merge issues and want the track body to reflect the new state. `handoff` rewrites the status table for one track on every run, but `brief` reads GitHub live without writing anything back — so a track you haven't `handoff`'d recently stays stale on disk. `refresh-md <track>` (or **Refresh Track Body** in VS Code) fixes that on-demand; `hygiene` sweeps all tracks weekly.
113
+
114
+ > **GitHub access is read-only.** The toolkit never writes to GitHub. All issue data comes from read-only `gh` CLI calls (`gh issue list`, `gh issue view`). Every write (frontmatter, status table, session log) goes to your local markdown files only.
115
+
116
+ ## Shared tracks
117
+
118
+ By default track files live under `notes_root/<repo>/` — local only, never committed. **Shared tracks** live inside the repo clone itself (`.work-plan/<slug>.md`) and travel with the repo via `git pull`/`git push`. Teammates see the same planning state without a separate notes sync.
119
+
120
+ **Set up shared tracks for a repo:**
121
+
122
+ ```bash
123
+ # Register the local clone path in your config
124
+ /work-plan init-repo myproject --github=org/myproject --local=/path/to/clone
125
+ ```
126
+
127
+ Once `local:` is set and points to a valid git repo, all new tracks for that repo go into `.work-plan/` automatically.
128
+
129
+ **Syncing:**
130
+
131
+ ```bash
132
+ git pull # pull teammates' track changes
133
+ git add .work-plan/ && git commit && git push # share your own
134
+ ```
135
+
136
+ The CLI never auto-pushes. When you create or update a shared track, it prints a reminder:
137
+ ```
138
+ ↑ shared — commit + push to share with teammates.
139
+ ```
140
+
141
+ **Opt out per-command:** pass `--private` to route a specific track to `notes_root` instead:
142
+
143
+ ```bash
144
+ /work-plan group --milestone='v1.0' --private # keep clusters local
145
+ /work-plan new-track myproject exploration # --private for one-off tracks
146
+ ```
147
+
148
+ **Multi-repo disambiguation:** if the same track slug exists in two repos, qualify with `@<repo>` or `--repo=<key>`:
149
+
150
+ ```bash
151
+ /work-plan slot 4234 auth-flow@critforge
152
+ /work-plan close auth-flow --repo=critforge
153
+ ```
88
154
 
89
155
  ## Plan & doc liveness (`plan-status`)
90
156
 
@@ -215,7 +281,7 @@ To install for **both** Claude Code AND Codex, run the installer twice with diff
215
281
 
216
282
  ### VS Code extension
217
283
 
218
- The **Work Plan** extension is the visual face of the CLI — a sidebar tree (repos → tracks), a Mermaid dependency graph, the Untracked bucket, and full read/write (slot/close/edit/new-track/…) with a public-repo confirm modal.
284
+ The **Work Plan** extension is the visual face of the CLI — a sidebar tree (repos → tracks), a Mermaid dependency graph (with focus toggle and repo-scoped full map), the Untracked bucket, cross-track dependency chips, per-issue move buttons, and full read/write (slot/close/edit/move/new-track/…) with a public-repo confirm modal.
219
285
 
220
286
  ![Work Plan VS Code extension — sidebar and dependency graph](https://raw.githubusercontent.com/stylusnexus/work-plan-toolkit/main/vscode/media/screenshots/dependency-graph.png)
221
287
 
@@ -226,6 +292,16 @@ Install it from either registry:
226
292
 
227
293
  The extension **shells out to the `work-plan` CLI**, so install the CLI too (npm or any method above). If `work-plan` isn't on your editor's `PATH` — common when VS Code is launched from the Dock/Finder rather than a terminal — set **`workPlan.cliPath`** in Settings to an absolute launcher path (e.g. `/path/to/work-plan-toolkit/bin/work-plan`, or the npm global bin), then reload the window. Extensions auto-update from the registry.
228
294
 
295
+ Useful settings:
296
+
297
+ | Setting | Default | What it does |
298
+ |---|---|---|
299
+ | `workPlan.cliPath` | `"work-plan"` | Absolute path to the CLI, if it's not on the editor's PATH |
300
+ | `workPlan.autoRefreshInterval` | `0` (off) | Re-poll the CLI silently in the background (seconds). Set to 30, 60, 300, or 900 if teammates are pushing shared-track changes and you want the tree to stay current without manual refresh |
301
+ | `workPlan.expandReposByDefault` | `false` | Expand all repo groups on load (single-repo workspaces always expand) |
302
+
303
+ Shared tracks show a **`shared`** tag in the tree description so you can tell at a glance which tracks travel via `git push/pull` and which are local-only.
304
+
229
305
  ### Updating
230
306
 
231
307
  | Installed via | Update with |
@@ -304,9 +380,9 @@ work-plan-toolkit/
304
380
  │ ├── work-plan/
305
381
  │ │ ├── SKILL.md
306
382
  │ │ ├── work_plan.py # CLI entry
307
- │ │ ├── commands/ # 16 subcommand modules
383
+ │ │ ├── commands/ # 24 subcommand modules
308
384
  │ │ ├── lib/ # config, frontmatter, gh, git, prompts, …
309
- │ │ └── tests/ # 234 unittest cases
385
+ │ │ └── tests/ # 600+ unittest cases
310
386
  │ └── repo-activity-summary/
311
387
  │ └── SKILL.md # bundled companion skill
312
388
  ├── commands/
@@ -417,14 +493,16 @@ See `docs/usage-examples.md` for end-to-end scenarios (morning brief, mid-work h
417
493
  | `close <track> [--state=shipped\|parked\|abandoned] [--note=<text>]` | Mark track shipped, parked, or abandoned. Moves to `archive/<state>/` for shipped/abandoned. Pass `--state=` (and an optional `--note=`) to run without prompts. |
418
494
  | `refresh-md <track>` `\|` `--all` `\|` `--repo=<key>` | Update issue STATE (open/closed, status labels) inside the track body's status table. Does NOT change track membership — this is the right tool for "refresh the work I just completed." `--all` sweeps every active track; `--repo=<key>` scopes the sweep to one repo. |
419
495
  | `hygiene [--repo=<key>]` | Weekly all-in-one: `refresh-md` + `reconcile` + `duplicates`. With `--repo=<key>`, steps 1 and 2 scope to that repo and the global `duplicates` step is skipped. |
420
- | `list [--all]` | List active tracks (or all including parked/archived). |
496
+ | `list [--all] [--sort=recent\|priority]` | List active tracks (or all including parked/archived). `--sort=recent` orders by `last_touched` (most recent first); `--sort=priority` orders by `launch_priority` (P0→P3) with recency as tiebreaker. Default keeps discovery order. |
421
497
  | `init <path> [--priority=P0..P3] [--milestone=<m>]` | Add frontmatter to a brand-new track .md file (the file must already exist). Pass `--priority=`/`--milestone=` to skip the prompts. |
422
498
  | `init-repo <key> --github=<slug> [--local=<path>]` | Bootstrap a new repo: create `<notes_root>/<key>/archive/{shipped,abandoned}/` and add the repo block to your config. `--github` is required; `--local` is optional. |
423
499
  | `new-track <repo> <slug> [--priority=P0..P3] [--milestone=<m>]` | One-shot, non-interactive: create a new track file under `notes_root` for `<repo>` (a config key **or** an `org/repo` slug) with frontmatter. Unlike `init`, it makes the file for you — the headless creation path the VS Code viewer uses. |
424
500
  | `set-notes-root <path>` | Relocate where your private track notes live (updates `notes_root` in config). Does not move existing tracks — it warns if any would be orphaned. |
425
501
  | `suggest-priorities --repo=<key>` | Two-step AI label backfill: CLI fetches unlabeled issues, Claude proposes priorities, `--apply` writes labels via `gh`. |
426
- | `group [--milestone=X] [--label=Y]` | AI-cluster GitHub issues into thematic tracks (creates `<repo>/<slug>.md` per cluster). |
427
- | `reconcile <track>` `\|` `--all` `\|` `--repo=<key> [--draft]` | Update track MEMBERSHIP (the `github.issues` list in frontmatter) by syncing against a GitHub label. Read-only on GitHub. Default label is `track/<slug>`; override per-track via `github.labels: [...]` in frontmatter (OR semantics). `--draft` previews ADDs/FLAGs without prompting or writing. `--repo=<key>` scopes the sweep to one repo. NOT for hand-curated tracks (it'll propose dropping curated issues every run) — use `refresh-md` if you only want to update issue state. When >50% of frontmatter issues lack the label, reconcile prints a hint pointing to `refresh-md`. |
502
+ | `group [--milestone=X] [--label=Y] [--repo=Z] [--private] [--apply] [--limit=N]` | AI-cluster GitHub issues into thematic track files. Two-step: CLI prints prompt → you save JSON answer → `--apply` creates the tracks. `--private` routes to `notes_root` instead of `.work-plan/`. `--limit` controls how many issues are shown in the prompt (default 100). |
503
+ | `auto-triage [--repo=<key>] [--apply] [--limit=N]` | AI-assign untracked open issues to existing tracks. Two-step (same pattern as `group`). Run `coverage` first to measure the gap. `--limit` controls how many untracked issues are shown (default 100). |
504
+ | `coverage [--repo=<key>] [--list] [--limit=N]` | Report how many open issues are not in any track. `--list` prints titles. Read-only. |
505
+ | `reconcile <track>` `\|` `--all` `\|` `--repo=<key> [--draft] [--yes]` | Update track MEMBERSHIP (the `github.issues` list in frontmatter) by syncing against a GitHub label. Read-only on GitHub. Default label is `track/<slug>`; override per-track via `github.labels: [...]` in frontmatter (OR semantics). In an `--all`/`--repo` sweep it also detects **MOVEs** — an issue relabeled from one track to another in the same repo is moved (removed from the old track, added to the new); ambiguous targets stay as FLAGs. `--draft` previews ADDs/MOVEs/FLAGs without prompting or writing. `--yes` applies without prompting (non-interactive, e.g. the VS Code extension); PUBLIC-repo move destinations are skipped under `--yes`. `--repo=<key>` scopes the sweep to one repo. NOT for hand-curated tracks (it'll propose dropping curated issues every run) — use `refresh-md` if you only want to update issue state. When >50% of frontmatter issues lack the label, reconcile prints a hint pointing to `refresh-md`. |
428
506
  | `duplicates [--repo=<key>]` | Find likely-duplicate issues by title similarity (stdlib `difflib`). Prints `gh issue close` consolidation commands. |
429
507
  | `canonicalize <track>` | Add a canonical issue table to a track file (so `refresh-md` knows where to update). |
430
508
  | `plan-status [--repo=<key>] [--json] [--stamp [--draft]] [--llm [--apply]] [--archive \| --issues] [--draft]` | Reach a verdict on every plan/spec doc in a repo by correlating its declared file-manifest against git + the filesystem: ✅ shipped / 🟡 partial / 💀 dead / 👻 manifest-less / 🧳 foreign. Read-only by default. `--stamp` writes an idempotent status header into each doc (`--draft` previews); `--llm` runs a two-step AI verdict on prose/ambiguous docs; `--archive` moves dead plans to `archive/abandoned/` and `--issues` opens issues for partial plans (both gated, both honor `--draft`); `--json` for machine output. |
@@ -467,7 +545,7 @@ cd skills/work-plan
467
545
  python3 -m unittest discover tests
468
546
  ```
469
547
 
470
- 234 tests, no external dependencies (mocks `gh`/`git` calls).
548
+ 600+ tests, no external dependencies (mocks `gh`/`git` calls).
471
549
 
472
550
  ## License
473
551
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 2026.06.09+f3bc861
1
+ 2026.06.10+a6052bf
package/bin/work-plan CHANGED
@@ -24,6 +24,29 @@ while [ -L "$src" ]; do
24
24
  esac
25
25
  done
26
26
  root="$(cd -P "$(dirname "$src")/.." && pwd)"
27
+
28
+ # Runtime preflight: surface a clear, per-tool message if a required external
29
+ # tool is missing, instead of a Python traceback from deep in config-load (the
30
+ # classic "yq not found" confusion). yq/gh aren't needed for --version/--help,
31
+ # so don't block those.
32
+ _wp_have() { command -v "$1" >/dev/null 2>&1; }
33
+ _wp_missing=""
34
+ _wp_have python3 || _wp_missing="$_wp_missing python3"
35
+ case "${1:-}" in
36
+ --version|-v|--help|-h|"") ;;
37
+ *)
38
+ _wp_have yq || _wp_missing="$_wp_missing yq"
39
+ _wp_have gh || _wp_missing="$_wp_missing gh"
40
+ ;;
41
+ esac
42
+ if [ -n "$_wp_missing" ]; then
43
+ echo "work-plan: missing required tool(s) on PATH:$_wp_missing" >&2
44
+ case "$_wp_missing" in *python3*) echo " python3 — the CLI runtime (brew install python · https://python.org)" >&2 ;; esac
45
+ case "$_wp_missing" in *yq*) echo " yq — YAML config/frontmatter (brew install yq — the Go mikefarah/yq, NOT the python yq)" >&2 ;; esac
46
+ case "$_wp_missing" in *gh*) echo " gh — GitHub issue state (brew install gh, then: gh auth login · https://cli.github.com)" >&2 ;; esac
47
+ exit 1
48
+ fi
49
+
27
50
  for c in \
28
51
  "$root/skills/work-plan/work_plan.py" \
29
52
  "${CLAUDE_PLUGIN_ROOT:-}/skills/work-plan/work_plan.py" \
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stylusnexus/work-plan",
3
- "version": "2026.6.9",
4
- "description": "Track-aware daily work planning over GitHub issues the work-plan CLI. Pure Python stdlib; this package only ships + launches it.",
3
+ "version": "2026.6.10",
4
+ "description": "Track-aware daily work planning over GitHub issues. Shared tracks (git-synced .work-plan/ in each repo), AI clustering (group/auto-triage), VS Code viewer, Claude Code + Codex plugins. Pure Python stdlib.",
5
5
  "bin": {
6
6
  "work-plan": "bin/work-plan"
7
7
  },
@@ -15,15 +15,17 @@ Track-aware daily planner. Each "track" is a YAML-frontmattered markdown file th
15
15
  | `/work-plan brief` | Starting work or after a gap. Multi-track snapshot. |
16
16
  | `/work-plan handoff [track] [--auto-next \| --set-next 1,2,3]` | Wrapping up a work block. Captures touched + next + blockers; writes session log. Add `--auto-next` to suggest a priority-sorted next_up list from open issues (interactive: apply / edit / skip). Tracks with `next_up_auto: true` in frontmatter get the auto-derived list surfaced in `brief` automatically. |
17
17
  | `/work-plan orient [track]` (alias `where-was-i`) | Re-orienting. With a track: ~15-line track paste-block. Without: cwd snapshot (branch, recent commits, modified files) for non-track work. Add `--pick` for the interactive track picker. |
18
- | `/work-plan hygiene` | Weekly all-in-one cleanup: refresh-md --all + reconcile --all + duplicates. |
18
+ | `/work-plan hygiene [--repo=<key>]` | **Weekly all-in-one cleanup.** Three steps in sequence: ① `refresh-md --all` pull live GitHub state into every active track's status table (same as "Refresh Track Body" but for all tracks); ② `reconcile --all` sync track frontmatter membership against GitHub labels; ③ `duplicates` — flag likely-duplicate issues for consolidation. Run once a week to keep status icons, labels, and dedup state honest. `--repo=<key>` scopes steps ① and ② to one repo; step ③ is skipped in scoped mode (it needs a single explicit repo to be unambiguous). |
19
19
  | `/work-plan slot <issue-num> [track]` | A new GitHub issue should belong to a track. If the issue is already listed in another active track's frontmatter, you'll be prompted to move it (remove from source) instead of duplicating. |
20
20
  | `/work-plan close [track]` | Track is done (shipped) / paused (parked) / won't ship (abandoned). |
21
- | `/work-plan refresh-md <track> \| --all` | Status icons drifted from GitHub state. **You usually don't need to call this directly:** `handoff` already rewrites the body table for its own track on every run, and `brief` reads GitHub live. Reach for `refresh-md` (or `--all`) when a sibling track has drifted because you haven't `handoff`'d it lately. |
21
+ | `/work-plan refresh-md <track> \| --all \| --repo=<key>` | **Pull live GitHub state into a track's status table.** Run this after closing or merging issues it re-fetches each issue's open/closed state from GitHub and rewrites the status cells in the track body, which refreshes the dependency graph and `next_up` display. `--all` sweeps every active track; `--repo=<key>` scopes to one repo. In VS Code: right-click a track → **Refresh Track Body**. |
22
22
  | `/work-plan list [--all]` | List active tracks (or all including parked/archived). |
23
23
  | `/work-plan init <path>` | Add frontmatter to a new track .md file. |
24
24
  | `/work-plan init-repo <key> [--github=<slug>] [--local=<path>]` | Bootstrap a new repo: create `<notes_root>/<key>/archive/{shipped,abandoned}/` and add the repo block to your config. |
25
25
  | `/work-plan suggest-priorities --repo=<key>` | Two-step AI label backfill (one-time migration). |
26
- | `/work-plan group [--milestone=X] [--label=Y] [--repo=Z]` | Two-step AI clustering: turn a flat list of issues into thematic track files. |
26
+ | `/work-plan group [--milestone=X] [--label=Y] [--repo=Z]` | Two-step AI clustering: turn a flat list of issues into thematic track files. Powerful for a new milestone or repo re-org — fetches issues, prints a clustering prompt, you save the JSON answer, then `--apply` creates the track files. |
27
+ | `/work-plan auto-triage [--repo=<key>]` | Two-step AI assignment: assign untracked open issues to *existing* tracks. Use after `coverage` shows a gap. Prints a prompt listing untracked issues + active tracks; save AI's JSON answer; re-run with `--apply`. |
28
+ | `/work-plan coverage [--repo=<key>] [--list]` | Report how many open issues are not in any track (per repo). `--list` shows titles. Read-only. Run before `auto-triage` or `group` to measure the gap. |
27
29
  | `/work-plan reconcile <track> \| --all [--draft]` | Sync track frontmatter with GitHub labels (read-only on GitHub). Default label is `track/<slug>`; override per-track via `github.labels` in frontmatter. Add `--draft` to preview proposed ADDs/FLAGs without prompting or writing. |
28
30
  | `/work-plan duplicates [--min-similarity=0.7]` | Find likely-duplicate issues by title similarity (stdlib difflib). |
29
31
  | `/work-plan plan-status [--repo=<key>] [--stamp [--draft]] [--type=plan\|spec]` | **Doc/plan liveness.** "Which of my plan/spec docs actually shipped, half-shipped, or died?" Correlates each plan's declared file-manifest (Create/Modify/Test paths) against git + filesystem — not the unreliable checkboxes. Reports ✅ shipped / 🟡 partial / 💀 dead / 👻 manifest-less. Read-only by default; `--stamp` writes an idempotent status header into each doc (`--draft` previews, writes nothing). Natural-language triggers: "what's done vs unfinished in `<repo>`", "stamp the plan statuses", "which plans are stale/dead". |
@@ -60,15 +62,46 @@ After running `handoff`:
60
62
  5. Persist via `python3 <skill-path>/work_plan.py handoff <track> --set-next <comma-list>` (e.g. `--set-next 4167,4148,4149`).
61
63
  6. Show the user what was set so they can override.
62
64
 
63
- ## Two-step AI subcommands (`suggest-priorities`, `group`)
65
+ ## Two-step AI subcommands (`suggest-priorities`, `group`, `auto-triage`)
64
66
 
65
- Both are two-step:
67
+ All three follow the same pattern:
66
68
 
67
- 1. CLI fetches issues + writes prompt to terminal.
68
- 2. **You** read the issues, output the requested JSON, save via Write tool to `~/.claude/work-plan/cache/priorities.answers.json` or `~/.claude/work-plan/cache/groups.answers.json`.
69
+ 1. CLI fetches issues + writes prompt to terminal (saved to `~/.claude/work-plan/cache/`).
70
+ 2. **You** read the issues, output the requested JSON, save via Write tool to the path the CLI printed.
69
71
  3. Re-run with `--apply` to commit changes.
70
72
 
71
- Show the proposed labels/clusters BEFORE applying. The user may want to override.
73
+ Show the proposed labels/clusters/assignments BEFORE applying. The user may want to override.
74
+
75
+ **Which one to use:**
76
+ - `group` — issues need to be *clustered into new track files* (run once per milestone or after a re-org)
77
+ - `auto-triage` — untracked issues need to be *assigned to existing tracks* (run after `coverage` shows a gap)
78
+ - `suggest-priorities` — issues need `priority/PN` labels backfilled (one-time migration)
79
+
80
+ ## Two-tier track storage (shared vs private)
81
+
82
+ Track files live in one of two places:
83
+
84
+ | Tier | Path | Who sees it |
85
+ |---|---|---|
86
+ | **Shared** | `<local-clone>/.work-plan/<slug>.md` | Everyone with repo access (committed + pushed) |
87
+ | **Private** | `<notes_root>/<folder>/<slug>.md` | Local only (never committed) |
88
+
89
+ **Routing logic (automatic):** if a repo has a registered `local:` path that is a valid git repo, new tracks go into `.work-plan/` by default. Pass `--private` to any write command to route to `notes_root` instead.
90
+
91
+ **Setup shared tracks for a repo:**
92
+ ```
93
+ /work-plan init-repo myproject --github=org/myproject --local=/path/to/clone
94
+ ```
95
+
96
+ **Syncing shared tracks:** `git pull` pulls teammates' track changes; `git add .work-plan/ && git commit && git push` shares your own. The CLI never auto-pushes.
97
+
98
+ **Disambiguation when the same track slug exists in two repos:**
99
+ ```
100
+ /work-plan slot 4234 auth-flow@critforge # @repo qualifier
101
+ /work-plan close auth-flow --repo=critforge # --repo=<key> flag
102
+ ```
103
+
104
+ Both forms work on: `slot`, `close`, `handoff`, `canonicalize`, `refresh-md`, `reconcile`, `set`.
72
105
 
73
106
  ## Track ↔ GitHub label mapping
74
107
 
@@ -0,0 +1,243 @@
1
+ """auto-triage subcommand: AI-assign untracked issues to existing tracks.
2
+
3
+ Two-step (same pattern as `group`):
4
+ 1. Run without --apply: fetches untracked open issues, writes a batch file,
5
+ prints a prompt for the AI to assign each issue to an existing track.
6
+ 2. Run with --apply: reads the AI's JSON answers and slots each assignment
7
+ into the relevant track's frontmatter.
8
+
9
+ Use --repo=<key> to scope to one configured repo. When the config has a
10
+ single repo, --repo is inferred automatically.
11
+
12
+ Answers JSON format (written to cache/auto_triage.answers.json):
13
+ [
14
+ {"track": "auth-flow", "issues": [4501, 4502]},
15
+ {"track": "tabletop-sessions", "issues": [4503]}
16
+ ]
17
+ Issues omitted from every list are left untracked (no error).
18
+ """
19
+ import json
20
+ import subprocess
21
+ import sys
22
+ from datetime import datetime
23
+ from pathlib import Path
24
+
25
+ from lib.config import load_config, ConfigError
26
+ from lib.frontmatter import parse_file, write_file
27
+ from lib.scratch import cache_dir
28
+ from lib.tracks import discover_tracks
29
+ from lib.github_state import fetch_open_issues
30
+
31
+
32
+ def _batch_path() -> Path:
33
+ return cache_dir() / "auto_triage.json"
34
+
35
+
36
+ def _answers_path() -> Path:
37
+ return cache_dir() / "auto_triage.answers.json"
38
+
39
+
40
+ PROMPT_TEMPLATE = """\
41
+ You have a list of EXISTING tracks and a list of UNTRACKED open issues.
42
+ Assign each issue to the most appropriate existing track.
43
+
44
+ Return JSON — an array of assignment objects:
45
+ [
46
+ {"track": "<exact-track-slug>", "issues": [<issue-numbers>]},
47
+ ...
48
+ ]
49
+
50
+ Rules:
51
+ - Use ONLY the track slugs listed under "Existing tracks" below.
52
+ - An issue can appear in AT MOST ONE track assignment.
53
+ - Omit issues that genuinely don't fit any existing track (they stay untracked).
54
+ - Do NOT invent new tracks — that's /work-plan group's job.
55
+ - Do NOT include empty assignments (issues: []).
56
+
57
+ """
58
+
59
+
60
+ def run(args: list[str]) -> int:
61
+ apply_mode = "--apply" in args
62
+ repo_arg = next((a for a in args if a.startswith("--repo=")), None)
63
+
64
+ limit = 100
65
+ for a in args:
66
+ if a.startswith("--limit="):
67
+ try:
68
+ limit = int(a.split("=", 1)[1])
69
+ except ValueError:
70
+ print("ERROR: --limit must be an integer.")
71
+ return 2
72
+
73
+ try:
74
+ cfg = load_config()
75
+ except ConfigError as e:
76
+ print(f"ERROR: {e}")
77
+ return 1
78
+
79
+ if apply_mode:
80
+ return _apply(cfg)
81
+
82
+ # -----------------------------------------------------------------------
83
+ # Step 1: fetch untracked issues + print AI prompt
84
+ # -----------------------------------------------------------------------
85
+ repos_cfg = cfg.get("repos", {})
86
+ if repo_arg:
87
+ folder = repo_arg.split("=", 1)[1]
88
+ if folder not in repos_cfg:
89
+ print(f"ERROR: repo folder '{folder}' not in config.yml.")
90
+ return 1
91
+ elif len(repos_cfg) == 1:
92
+ folder = next(iter(repos_cfg))
93
+ else:
94
+ print("Multiple repos in config. Specify with --repo=<folder-name>.")
95
+ return 1
96
+
97
+ repo = repos_cfg[folder].get("github")
98
+ if not repo:
99
+ print(f"ERROR: repo entry '{folder}' has no 'github' key.")
100
+ return 1
101
+
102
+ tracks = discover_tracks(cfg)
103
+ active_tracks = [
104
+ t for t in tracks
105
+ if t.has_frontmatter and t.repo == repo
106
+ and t.meta.get("status") in ("active", "in-progress", "blocked")
107
+ ]
108
+ if not active_tracks:
109
+ print(f"No active tracks found for {repo}. Run /work-plan group first.")
110
+ return 0
111
+
112
+ # Build per-repo set of already-tracked issue numbers
113
+ tracked_nums: set = set()
114
+ for t in tracks:
115
+ if t.repo == repo and t.has_frontmatter:
116
+ tracked_nums.update(t.meta.get("github", {}).get("issues") or [])
117
+
118
+ print(f"Fetching open issues from {repo}...")
119
+ open_issues = fetch_open_issues(repo, limit=500)
120
+ untracked = [i for i in open_issues if i.get("number") not in tracked_nums]
121
+
122
+ if not untracked:
123
+ print(f"No untracked issues found for {repo} — full coverage!")
124
+ return 0
125
+
126
+ batch_path = _batch_path()
127
+ batch_path.write_text(json.dumps({
128
+ "repo": repo,
129
+ "folder": folder,
130
+ "untracked": untracked,
131
+ "tracks": [{"slug": t.meta.get("track", t.name), "name": t.name,
132
+ "milestone": t.meta.get("milestone_alignment"),
133
+ "priority": t.meta.get("launch_priority")}
134
+ for t in active_tracks],
135
+ }, indent=2))
136
+
137
+ print(f"Found {len(untracked)} untracked issues ({len(active_tracks)} active tracks).")
138
+ print()
139
+ print("=" * 60)
140
+ print(PROMPT_TEMPLATE)
141
+
142
+ print("Existing tracks:")
143
+ for t in active_tracks:
144
+ slug = t.meta.get("track", t.name)
145
+ milestone = t.meta.get("milestone_alignment", "—")
146
+ priority = t.meta.get("launch_priority", "—")
147
+ print(f" {slug} [{priority}, {milestone}]")
148
+
149
+ print()
150
+ print("Untracked issues to assign:")
151
+ shown = untracked[:limit]
152
+ for i in shown:
153
+ num = i.get("number", "?")
154
+ title = i.get("title", "")
155
+ milestone = i.get("milestone") or {}
156
+ m_title = milestone.get("title", "—") if isinstance(milestone, dict) else "—"
157
+ labels = [lb["name"] for lb in (i.get("labels") or [])]
158
+ print(f" #{num} [{m_title}] [{','.join(labels) or 'no-labels'}] {title}")
159
+ remainder = len(untracked) - len(shown)
160
+ if remainder > 0:
161
+ print(f" … and {remainder} more issues (use --limit=N to show more)")
162
+
163
+ print("=" * 60)
164
+ print()
165
+ print(f"After the agent returns assignment JSON, save it to:")
166
+ print(f" {_answers_path()}")
167
+ print("Then run:")
168
+ print(" python3 ~/.claude/skills/work-plan/work_plan.py auto-triage --apply")
169
+ return 0
170
+
171
+
172
+ def _apply(cfg: dict) -> int:
173
+ answers_path = _answers_path()
174
+ batch_path = _batch_path()
175
+ if not answers_path.exists():
176
+ print(f"ERROR: {answers_path} not found. Run without --apply first.")
177
+ return 1
178
+ if not batch_path.exists():
179
+ print(f"ERROR: {batch_path} not found.")
180
+ return 1
181
+
182
+ batch = json.loads(batch_path.read_text())
183
+ repo = batch["repo"]
184
+ folder = batch["folder"]
185
+ if folder not in cfg.get("repos", {}):
186
+ print(f"ERROR: batch folder '{folder}' not in config.yml repos.")
187
+ return 1
188
+
189
+ answers = json.loads(answers_path.read_text())
190
+
191
+ tracks = discover_tracks(cfg)
192
+ tracks_by_slug = {}
193
+ for t in tracks:
194
+ if t.repo == repo and t.has_frontmatter:
195
+ slug = t.meta.get("track", t.name)
196
+ tracks_by_slug[slug] = t
197
+ tracks_by_slug[t.name] = t # also index by name for resilience
198
+
199
+ untracked_nums = {i["number"] for i in batch.get("untracked", [])}
200
+
201
+ slotted = 0
202
+ skipped = 0
203
+ for assignment in answers:
204
+ slug = assignment.get("track", "").strip()
205
+ issue_nums = assignment.get("issues") or []
206
+ if not slug or not issue_nums:
207
+ continue
208
+
209
+ track = tracks_by_slug.get(slug)
210
+ if not track:
211
+ print(f" WARN: track '{slug}' not found — skipping {len(issue_nums)} issue(s).")
212
+ skipped += len(issue_nums)
213
+ continue
214
+
215
+ existing_meta, existing_body = parse_file(track.path)
216
+ if not existing_meta:
217
+ print(f" SKIP {slug}: file exists but has no frontmatter.")
218
+ skipped += len(issue_nums)
219
+ continue
220
+
221
+ existing_issues = list(existing_meta.get("github", {}).get("issues") or [])
222
+ existing_set = set(existing_issues)
223
+ new_nums = [n for n in issue_nums if n in untracked_nums and n not in existing_set]
224
+ already_there = [n for n in issue_nums if n in existing_set]
225
+
226
+ if already_there:
227
+ print(f" ℹ {slug}: #{','.join(str(n) for n in already_there)} already present.")
228
+ if not new_nums:
229
+ continue
230
+
231
+ merged = sorted(existing_set | set(new_nums))
232
+ existing_meta.setdefault("github", {})["issues"] = merged
233
+ existing_meta["last_touched"] = datetime.now().strftime("%Y-%m-%dT%H:%M")
234
+ write_file(track.path, existing_meta, existing_body)
235
+ print(f" ✓ {slug}: added #{','.join(str(n) for n in new_nums)} "
236
+ f"({len(merged)} issues total)")
237
+ slotted += len(new_nums)
238
+
239
+ print()
240
+ print(f"Done: {slotted} issue(s) assigned, {skipped} skipped.")
241
+ if slotted:
242
+ print("Next: run /work-plan brief to see the updated tracks.")
243
+ return 0