Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,53 @@ jobs:
- name: Run bin/work-plan launcher test (Linux/macOS)
if: runner.os != 'Windows'
run: python3 tests/test_bin_wrapper.py

# Installer smoke test on Linux (#5). The unittest matrix exercises the CLI but
# never runs install.sh or its --target override, and it downloads the yq binary
# directly rather than going through the README one-liners. This job mechanizes
# the installer-centric acceptance criteria from #5: install.sh --help, a real
# install into a scratch target, the --target override, and work_plan.py --help.
# What stays genuinely manual is the per-distro package-manager one-liners
# (apt/pacman/dnf) and non-Ubuntu distros — GitHub-hosted Ubuntu can't prove those.
smoke-install:
name: install.sh smoke (ubuntu)
needs: lint-py39
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install mikefarah/yq
run: |
sudo curl -sSL -o /usr/local/bin/yq \
"https://github.com/mikefarah/yq/releases/download/v4.53.2/yq_linux_amd64"
sudo chmod +x /usr/local/bin/yq
yq --version

- name: install.sh --help renders
run: |
out=$(./install.sh --help)
echo "$out"
echo "$out" | grep -q "Usage: ./install.sh"

- name: install.sh into a scratch target
run: |
target="${RUNNER_TEMP}/claude"
mkdir -p "${target}"
./install.sh --target="${target}"
test -f "${target}/skills/work-plan/work_plan.py"
test -f "${target}/skills/work-plan/.installed-from"
test -f "${target}/commands/work-plan.md"

- name: install.sh --target override (Codex layout)
run: |
target="${RUNNER_TEMP}/agents"
mkdir -p "${target}"
./install.sh --target="${target}"
test -f "${target}/skills/work-plan/work_plan.py"

- name: work_plan.py --help executes
run: python3 skills/work-plan/work_plan.py --help
59 changes: 56 additions & 3 deletions .work-plan/cli-viewer-cross.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,65 @@ github:
- 271
- 280
- 285
- 324
- 328
- 329
- 330
- 349
branches: []
depends_on: []
last_touched: 2026-06-13T09:00
last_handoff: 2026-06-13T09:00
next_up: []
last_touched: 2026-06-15T21:09
last_handoff: 2026-06-15T21:09
next_up:
- 330
- 329
- 328
blockers: []
tier: shared
---
# cli-viewer-cross

## Session log

### Session — 2026-06-15 21:09

- Touched: chore(cli-viewer-cross): slot #330 + #349 into the track (8738ff5)
- Touched: feat(vscode): graph zoom/pan/fit + SVG/PNG export (#216) (#353) (211d8aa)
- Touched: feat(vscode): graph zoom/pan/fit + SVG/PNG export (#216) (9b7dbc7)
- Touched: chore(npm): drop redundant postinstall script for npm 12 default-deny compatibility (#344) (8dec257)
- Touched: chore(npm): drop redundant postinstall script for npm 12 default-deny (6f74481)
- Touched: fix(vscode): real GitHub href on issue links — clicking scrolled instead of opening (#324 follow-up) (e06c4f1)
- Touched: Merge pull request #323 from stylusnexus/dev (6579bf7)
- Touched: Merge pull request #322 from stylusnexus/fix/271-hot-branch-perf (5e55e68)
- Touched: fix(git_state): batch hot-branch detection — was O(branches) git calls per track (#271) (79164e8)
- Touched: Merge pull request #319 from stylusnexus/dev (ef58902)
- Touched: feat: issue-level in-progress (#271) (#317) (89843cf)
- Touched: docs(vscode): correct v0.8.0 CLI requirement to 2026.06.14 (#271 review) (3857898)
- Touched: fix(in-progress): toggle reflects label state, not the union signal (#271 review) (dab0ee8)
- Touched: fix(export): key issues_by_track by (repo,name) so same-named cross-repo tracks don't bleed (#271 review) (11932ef)
- Touched: fix(in-progress): reject --repo that doesn't track the issue (#271 review) (d290042)
- Touched: docs: document in-progress command + correct GitHub-mutation inventory (#271) (e8e1673)
- Touched: chore(vscode): require CLI 2026.06.14 + bump extension to 0.8.0 (#271) (8f377e7)
- Touched: fix(vscode): extract + test webview message guard so dropped messages can't recur (#305, #285) (c5d58ec)
- Touched: feat(vscode): detail-webview in-progress toggle via postMessage (#271) (423cdf5)
- Touched: feat(work_plan): register in-progress subcommand (#271) (149da7e)
- Touched: feat(in-progress): repo-qualified command behind confirm gate (#271) (4ca9f48)
- Touched: feat(github_state): set_issue_in_progress label writer (#271) (bf34ca6)
- Touched: feat(vscode): in-progress badge on tracked issue rows (#271) (9fe9f85)
- Touched: feat(orient): mark in-progress on next pick + behind-it rows (#271) (117564a)
- Touched: test(list-open-issues): expect in_progress field on shared issue surface (#271) (10d0a95)
- Touched: feat(brief): mark in-progress issues in the next-up list (#271) (78f8712)
- Touched: feat(export): compute per-track branch heat for in_progress (#271) (c602ddd)
- Touched: feat(export_model): thread per-issue in_progress keyed by (repo,name) (#271) (f8262a5)
- Touched: feat(github_state): fetch labels in lean GQL set for in-progress signal (#271) (b6281ea)
- Touched: feat(in_progress): union-merge issue in-progress signal (#271) (1ab0c52)
- Touched: feat(git_state): map hot feat/fix branches to issue numbers (#271) (a37825a)
- Touched: feat: declared track↔plan link + viewer navigation (#285) (#302) (20e085d)
- Next: (open)

### Session — 2026-06-15 21:09

- Touched: (no git activity attributed; 5 open from GitHub)
- Next: #330 design: track cleanup/deletion flow + clear deletion confirmation in VS Code
- Next: #329 feat: Mark a track for cleanup/deletion — CLI + VS Code menu
- Next: #328 feat: Archive a track — CLI command + VS Code track menu
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ The five essentials you'll use 80% of the time are:

| Command | When |
|---|---|
| `/work-plan brief` | Morning. Multi-track snapshot — what's on your plate across every active track. Add `--repo=<key>` to scope to one project. |
| `/work-plan brief` | Morning. Multi-track snapshot — what's on your plate across every active track. Run from inside a configured repo's checkout and it **auto-scopes to that repo** (one-line banner; `--repo=all` shows everything). Add `--repo=<key>` to scope to a specific project. |
| `/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. |
| `/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. |
| `/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. |
| `/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. |
| `/work-plan hygiene [--repo=<key>]` | **Weekly all-in-one cleanup.** Runs four steps: ① `refresh-md --all` (pull live GitHub state into every active track's status table), ② `reconcile --all` (sync frontmatter membership against GitHub labels), ③ `dedupe-tiers` (report shared/private duplicate tracks, no deletes), ④ `duplicates` (flag likely-duplicate issues). `--repo=<key>` scopes steps ①–③ to one repo; step is skipped in scoped mode. |
| `/work-plan in-progress <n> [--clear]` | Starting or stopping active work on an issue. Adds (or removes with `--clear`) the `work-plan:in-progress` label on GitHub. Repo-resolved from the issue number, or pass `--repo=<key\|slug>` to disambiguate. `brief`/`orient`/the VS Code viewer also detect in-progress automatically from a hot `feat/<n>-`/`fix/<n>-` branch. |

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:
Expand Down Expand Up @@ -109,7 +109,7 @@ flowchart TB
- 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.
- 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.
- **Ranking presets** — when `next_up_auto: true` is on, the default ranking is `flow` (milestone → dependency → priority → recency). Override per-track with `set-next-up <track> --preset=<name>`, or set `next_up_default: <name>` in your config for a global fallback. Named presets: `flow` (the default), `priority-driven` (priority first, no milestone bias — good for backlogs with no milestones), `backlog` (oldest issues first — surfaces stalled work). Custom criterion order: `set-next-up <track> --order=aging,priority,dependency`. Clear a track's override with `--clear`. Toggle auto-derivation itself with `--auto=on|off` (no hand-editing frontmatter required).
- **Weekly** → `hygiene` runs `refresh-md --all` + `reconcile --all` + `duplicates` in sequence to keep status icons, GitHub labels, and dedup state honest.
- **Weekly** → `hygiene` runs `refresh-md --all` + `reconcile --all` + `dedupe-tiers` (report-only) + `duplicates` in sequence to keep status icons, GitHub labels, tier dedup, and issue-dedup state honest.

> **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 **Sync Issue States from GitHub** in VS Code) fixes that on-demand; `hygiene` sweeps all tracks weekly.

Expand Down Expand Up @@ -510,13 +510,15 @@ See `docs/usage-examples.md` for end-to-end scenarios (morning brief, mid-work h

| Subcommand | What it does |
|---|---|
| `brief [--repo=<key>]` | Multi-track snapshot of all active tracks across configured repos. `--repo=<key>` filters to one project (matches the folder name under `notes_root` or the `org/repo` GitHub slug; archived-reopen callouts are also scoped). |
| `brief [--repo=<key> \| --repo=all]` | Multi-track snapshot of all active tracks across configured repos. When `--repo` is omitted and you're inside a configured repo's checkout, `brief` **auto-scopes to that repo** (resolved by clone path, then git remote) and prints a one-line banner; `--repo=all` forces the full cross-repo view. `--repo=<key>` filters to one project explicitly (matches the folder name under `notes_root` or the `org/repo` GitHub slug). In all cases the archived-reopen callouts are scoped to the same repo. Disable cwd auto-scope with `brief_auto_scope: false` in `config.yml`. |
| `which-repo [--json]` | Resolve the current directory to one configured repo — by local clone path first, then the git `origin` remote. Prints the matched config key + GitHub slug, or reports no match. Read-only; it's the shared resolver behind `brief`'s cwd auto-scope and the VS Code viewer's repo auto-focus. |
| `handoff <track> [--auto-next \| --set-next 1,2,3]` | Wrap up a work block. Writes a `### Session — <ts>` entry. `--auto-next` suggests a priority-sorted top-3 from open issues (interactive: apply / edit / skip). `--set-next 1,2,3` is the explicit form — note it writes the session entry too; for a field-only `next_up` change with no session log, use `set next_up=…`. Without either flag, just captures the session summary and reads any pre-existing `next_up`. |
| `orient [track]` (alias: `where-was-i`) | Read-only paste block. With a track name: ~15-line track summary (priority, last session, next pick, git state). With no track: cwd snapshot (branch, recent commits, modified files) for non-track work. Add `--pick` for the interactive track picker. |
| `slot <issue-num> [track]` | A new GitHub issue should belong to a track — adds it to the track's `github.issues` list. Non-interactive flags: `--move`/`--no-move` (relocate the issue off its prior track, or leave it; default no-move), `--confirm=<token>` (public-repo gate, see below). |
| `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. |
| `refresh-md <track>` `\|` `--all` `\|` `--repo=<key>` | Sync issue STATE (open/closed, status labels) from GitHub into the track body's status table. Does NOT change track membership — this is the right tool for "refresh the work I just completed." For a **canonical** table it re-derives the whole block from live data, milestone-ordered (active milestone first; see `canonicalize`), so the table self-heals and stays grouped instead of decaying; narrative (non-canonical) tables are updated conservatively in place. If the live fetch comes back incomplete (GitHub timeout/permission error, or a frontmatter issue that no longer resolves), that track is **skipped and left untouched** rather than rewriting valid rows as `(not fetched)`, and the command exits nonzero so sweeps can flag the degraded run. `--all` sweeps every active track; `--repo=<key>` scopes the sweep to one repo. |
| `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. |
| `hygiene [--repo=<key>]` | Weekly all-in-one: `refresh-md` + `reconcile` + `dedupe-tiers` (report-only) + `duplicates`. With `--repo=<key>`, steps 1–3 scope to that repo and the global `duplicates` step is skipped. |
| `dedupe-tiers [--repo=<key>] [--apply]` | Remove private track copies that a shared twin in a repo's `.work-plan/` supersedes (#359). When a track is promoted to the shared tier, its private original under `notes_root` is sometimes left behind (bulk/manual promotion, or a failed unlink during `push-track`) — `discover_tracks` then warns `exists in both shared and private` on every run with no cleanup path. This removes the safe orphans and **refuses** any whose private copy references issue numbers the shared one lacks (no silent data loss; the invariant is `issue_refs(private) ⊆ issue_refs(shared)`). Covers active and archived tiers. Default is a **dry-run report**; `--apply` deletes (auto-committed to `notes_root`, so undoable via `notes-vcs undo`). `--repo=<key>` scopes to one repo. |
| `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. |
| `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. |
| `init-repo <key> --github=<slug> [--local=<path>] [--update [--clear-local]]` | Bootstrap a new repo: create `<notes_root>/<key>/archive/{shipped,abandoned}/` and add the repo block to your config. `--github` is required for an add; `--local` is optional. `--update` on an existing key changes its local/github; `--update --clear-local` forgets the saved local path (keeps github + other fields). `--clear-local` and `--local` are mutually exclusive. |
Expand Down
32 changes: 29 additions & 3 deletions skills/work-plan/commands/brief.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""brief subcommand — fully featured."""
import os
from datetime import datetime
from pathlib import Path

from lib.config import load_config, ConfigError
from lib.cwd_repo import resolve_repo_for_dir
from lib.tracks import (
discover_tracks, discover_archived_tracks, filter_tracks_by_repo,
priority_rank, recency_sort_key,
Expand All @@ -24,9 +26,9 @@

def run(args: list[str]) -> int:
flags, _ = parse_flags(args, {"--repo"})
repo_key = flags.get("--repo")
if repo_key is True:
print("usage: work_plan.py brief [--repo=<key>]")
repo_arg = flags.get("--repo")
if repo_arg is True:
print("usage: work_plan.py brief [--repo=<key> | --repo=all]")
return 2

try:
Expand All @@ -36,15 +38,39 @@ def run(args: list[str]) -> int:
return 1

tracks = discover_tracks(cfg)

# Resolve the effective scope key ONCE, then thread it through both the track
# filter and the archived-reopen pass so the two can never diverge (#358):
# explicit --repo=<key> → scope to it (unchanged behavior, no banner)
# explicit --repo=all → force the full view (no scope, no auto-detect)
# --repo omitted → auto-detect from cwd (unless opted out); banner on hit
auto_scoped = False
if isinstance(repo_arg, str):
repo_key = None if repo_arg.lower() == "all" else repo_arg
else:
repo_key = None
if cfg.get("brief_auto_scope", True):
match = resolve_repo_for_dir(cfg, os.getcwd())
# Only auto-scope when the detected repo actually has tracks — a
# convenience must never hide tracks you'd otherwise see.
if match and filter_tracks_by_repo(tracks, match["key"]):
repo_key = match["key"]
auto_scoped = True

if repo_key:
scoped = filter_tracks_by_repo(tracks, repo_key)
if not scoped:
# Reachable only via an explicit --repo (the auto path above already
# guaranteed a non-empty match).
print(f"No tracks found for repo '{repo_key}'.")
available = sorted((cfg.get("repos") or {}).keys())
if available:
print(f"Configured repo keys: {', '.join(available)}")
return 0
tracks = scoped
if auto_scoped:
print(f"Scoped to repo '{repo_key}' (cwd). Use --repo=all to see everything.")
print()
active = [t for t in tracks if t.has_frontmatter
and t.meta.get("status") in ("active", "in-progress", "blocked")]

Expand Down
Loading
Loading