@stylusnexus/work-plan 2026.6.15-1 → 2026.6.15-2
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.
- package/README.md +2 -2
- package/VERSION +1 -1
- package/package.json +1 -1
- package/skills/work-plan/commands/set_next_up.py +64 -8
- package/skills/work-plan/lib/export_model.py +5 -5
- package/skills/work-plan/tests/test_export.py +25 -0
- package/skills/work-plan/tests/test_set_next_up.py +95 -0
- package/skills/work-plan/work_plan.py +4 -4
package/README.md
CHANGED
|
@@ -108,7 +108,7 @@ flowchart TB
|
|
|
108
108
|
- `handoff <track> --set-next 4167,4148` — explicit numbers when you know exactly which issues are next.
|
|
109
109
|
- 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.
|
|
110
110
|
- 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.
|
|
111
|
-
- **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`.
|
|
111
|
+
- **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).
|
|
112
112
|
- **Weekly** → `hygiene` runs `refresh-md --all` + `reconcile --all` + `duplicates` in sequence to keep status icons, GitHub labels, and dedup state honest.
|
|
113
113
|
|
|
114
114
|
> **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.
|
|
@@ -523,7 +523,7 @@ See `docs/usage-examples.md` for end-to-end scenarios (morning brief, mid-work h
|
|
|
523
523
|
| `remove-repo <key>` | Unregister a repo: delete its block from your config. **Config-only** — the notes folder, any tracks, and the local clone are left untouched (a notes folder or tracks that referenced it are now orphaned and can be removed by hand). Completes the add/update/remove trio with `init-repo`. |
|
|
524
524
|
| `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. |
|
|
525
525
|
| `rename-track <old-slug \| old@repo> <new-slug> [--repo=<key>] [--fix-refs] [--commit]` | Rename an active track's slug: moves its `.md` file and updates the frontmatter `track` field + `last_touched`. Validates `<new-slug>` like `new-track` and rejects a name already taken in the same repo/tier. For shared tracks, `--commit` stages + commits the move (otherwise it prints a "commit to share" hint). `--fix-refs` rewrites sibling tracks' `depends_on` that reference the old slug; without it they're just warned about. Archived tracks are immutable. Public-repo gated. |
|
|
526
|
-
| `set-next-up <track> (--preset=<name> \| --order=a,b,c \| --clear) [--repo=<key>] [--confirm=<token>]` | Configure the ranking preset
|
|
526
|
+
| `set-next-up <track> (--preset=<name> \| --order=a,b,c \| --clear \| --auto=on\|off) [--repo=<key>] [--confirm=<token>]` | Configure the ranking preset and/or auto-derivation flag for a track's next_up list. `--preset` sets a named preset: `flow` (default — milestone → dependency → priority → recency), `priority-driven` (priority first, ignores milestone bias, for backlogs with no milestones), `backlog` (oldest issues first — surfaces stalled work), or `custom` (requires `--order`). `--order=a,b,c` sets a custom comma-separated criterion list (`milestone`, `dependency`, `priority`, `recency`, `aging`). `--clear` reverts to the global `next_up_default` config or the default `flow`. `--auto=on` activates `next_up_auto` so brief/orient/export auto-derive the next-up list live from the ranking preset (ignoring the curated list); `--auto=off` clears it to revert to the curated list. `--auto` can be used standalone or combined with `--preset`/`--order`/`--clear`. Writes `next_up_order` and/or `next_up_auto` into the track's frontmatter; does NOT touch the `next_up` issue-list. Global default: add `next_up_default: <preset>` to `~/.claude/work-plan/config.yml`. Public-repo gated. |
|
|
527
527
|
| `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. |
|
|
528
528
|
| `notes-vcs <init\|enable\|disable\|status\|undo> [<sha>] [--no-enable] [--json]` | Opt-in **local** version control for the private `notes_root` tier — history/undo for tracks on your machine, never pushed. `init` git-inits `notes_root` as a personal repo (baseline commit of existing tracks) and turns on auto-commit (`--no-enable` skips that). For safety it **refuses** a `notes_root` that already has a git remote or is a repo work-plan didn't create. When on, every track-mutating command (`slot`/`group`/`handoff`/`close`/`set`/…) commits **only the files it changed** (pre-existing uncommitted edits are left alone) as an undoable commit. The shared tier is unaffected — it's versioned by its own repo. `status` shows whether `notes_root` is a repo, whether auto-commit is on, and the last commit (`--json` for the machine shape the VS Code viewer polls). `undo [<sha>]` reverts a commit (default HEAD) — reverses the last edit. |
|
|
529
529
|
| `push-track <track\|track@repo> [--repo=<key>] [--no-push] [--confirm=<token>]` | **Promote a private track to the shared tier and publish it** (#306). Moves the track's `.md` from `notes_root` into the repo's `.work-plan/` (on its `plan_branch`, via a worktree), removes the private copy so it isn't duplicated, commits to the plan branch, and pushes — unless `--no-push`. Tier is derived from location, so this is a file move, not a frontmatter edit. Requires a local clone + a configured `plan_branch` (else hints `plan-branch init`). Pushing to a **public** repo makes the track world-visible → confirm-token gated. |
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2026.06.15+
|
|
1
|
+
2026.06.15+c80ece1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stylusnexus/work-plan",
|
|
3
|
-
"version": "2026.6.15-
|
|
3
|
+
"version": "2026.6.15-2",
|
|
4
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"
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
"""set-next-up subcommand — guarded edit of a track's next_up ranking preset.
|
|
2
2
|
|
|
3
3
|
Usage:
|
|
4
|
-
work_plan.py set-next-up <track> [--repo=<key>]
|
|
4
|
+
work_plan.py set-next-up <track> [--repo=<key>]
|
|
5
|
+
(--preset=<name> | --order=a,b,c | --clear | --auto=on|off)
|
|
6
|
+
[--confirm=<token>]
|
|
5
7
|
|
|
6
|
-
Writes `next_up_order` into the track's frontmatter.
|
|
7
|
-
`next_up` issue-list key.
|
|
8
|
+
Writes `next_up_order` and/or `next_up_auto` into the track's frontmatter.
|
|
9
|
+
Does NOT touch the `next_up` issue-list key.
|
|
10
|
+
|
|
11
|
+
--preset=<name> Set one of the named ranking presets (flow, priority-driven,
|
|
12
|
+
backlog) or 'custom' (which requires --order).
|
|
13
|
+
--order=a,b,c Set a custom comma-separated criterion list.
|
|
14
|
+
--clear Remove the next_up_order key (reverts to global/default).
|
|
15
|
+
--auto=on|off Toggle the next_up_auto flag. When on, brief/orient/export
|
|
16
|
+
auto-derive the next-up list via the ranking preset (#326).
|
|
17
|
+
Can be used standalone or combined with --preset/--order/--clear.
|
|
8
18
|
|
|
9
19
|
Public-repo gated: without --confirm it prints {needs_confirm, reason, token}
|
|
10
20
|
and makes no change. The VS Code extension surfaces that as a modal then
|
|
@@ -22,12 +32,12 @@ from lib.next_up import CRITERIA, PRESETS
|
|
|
22
32
|
|
|
23
33
|
def run(args: list[str]) -> int:
|
|
24
34
|
flags, positional = parse_flags(
|
|
25
|
-
args, {"--confirm", "--repo", "--clear", "--preset", "--order"}
|
|
35
|
+
args, {"--confirm", "--repo", "--clear", "--preset", "--order", "--auto"}
|
|
26
36
|
)
|
|
27
37
|
if not positional:
|
|
28
38
|
print(
|
|
29
39
|
"usage: work_plan.py set-next-up <track> "
|
|
30
|
-
"(--preset=<name> | --order=a,b,c | --clear) "
|
|
40
|
+
"(--preset=<name> | --order=a,b,c | --clear | --auto=on|off) "
|
|
31
41
|
"[--repo=<key>] [--confirm=<token>]"
|
|
32
42
|
)
|
|
33
43
|
return 2
|
|
@@ -41,11 +51,27 @@ def run(args: list[str]) -> int:
|
|
|
41
51
|
clear = bool(flags.get("--clear"))
|
|
42
52
|
preset_flag = flags.get("--preset") if flags.get("--preset") is not True else None
|
|
43
53
|
order_flag = flags.get("--order") if flags.get("--order") is not True else None
|
|
54
|
+
auto_raw = flags.get("--auto") if flags.get("--auto") is not True else None
|
|
55
|
+
|
|
56
|
+
# Parse and validate --auto value
|
|
57
|
+
auto_value = None # None means not specified
|
|
58
|
+
if auto_raw is not None:
|
|
59
|
+
auto_lower = auto_raw.lower() if isinstance(auto_raw, str) else ""
|
|
60
|
+
if auto_lower == "on":
|
|
61
|
+
auto_value = True
|
|
62
|
+
elif auto_lower == "off":
|
|
63
|
+
auto_value = False
|
|
64
|
+
else:
|
|
65
|
+
print(
|
|
66
|
+
f"ERROR: --auto must be 'on' or 'off', got {auto_raw!r}",
|
|
67
|
+
file=sys.stderr,
|
|
68
|
+
)
|
|
69
|
+
return 2
|
|
44
70
|
|
|
45
|
-
# Must have at least one of --preset, --order, or --
|
|
46
|
-
if not clear and preset_flag is None and order_flag is None:
|
|
71
|
+
# Must have at least one of --preset, --order, --clear, or --auto
|
|
72
|
+
if not clear and preset_flag is None and order_flag is None and auto_value is None:
|
|
47
73
|
print(
|
|
48
|
-
"ERROR: specify --preset=<name>, --order=a,b,c, or --
|
|
74
|
+
"ERROR: specify --preset=<name>, --order=a,b,c, --clear, or --auto=on|off",
|
|
49
75
|
file=sys.stderr,
|
|
50
76
|
)
|
|
51
77
|
return 2
|
|
@@ -123,8 +149,19 @@ def run(args: list[str]) -> int:
|
|
|
123
149
|
|
|
124
150
|
if clear:
|
|
125
151
|
track.meta.pop("next_up_order", None)
|
|
152
|
+
if auto_value is not None:
|
|
153
|
+
_apply_auto(track, auto_value)
|
|
126
154
|
write_file(track.path, track.meta, track.body)
|
|
127
155
|
print(f"✓ cleared next_up_order on {track.name}")
|
|
156
|
+
if auto_value is not None:
|
|
157
|
+
_print_auto_result(track, auto_value)
|
|
158
|
+
return 0
|
|
159
|
+
|
|
160
|
+
# If --auto is the only flag (no preset/order/clear), write just the auto flag.
|
|
161
|
+
if preset_flag is None and order_list is None and auto_value is not None:
|
|
162
|
+
_apply_auto(track, auto_value)
|
|
163
|
+
write_file(track.path, track.meta, track.body)
|
|
164
|
+
_print_auto_result(track, auto_value)
|
|
128
165
|
return 0
|
|
129
166
|
|
|
130
167
|
# Build the next_up_order mapping
|
|
@@ -145,10 +182,29 @@ def run(args: list[str]) -> int:
|
|
|
145
182
|
)
|
|
146
183
|
|
|
147
184
|
track.meta["next_up_order"] = nuo
|
|
185
|
+
if auto_value is not None:
|
|
186
|
+
_apply_auto(track, auto_value)
|
|
148
187
|
write_file(track.path, track.meta, track.body)
|
|
149
188
|
|
|
150
189
|
if preset_flag and preset_flag != "custom":
|
|
151
190
|
print(f"✓ set next_up_order preset={preset_flag!r} on {track.name}")
|
|
152
191
|
elif order_list is not None:
|
|
153
192
|
print(f"✓ set next_up_order custom order={order_list!r} on {track.name}")
|
|
193
|
+
if auto_value is not None:
|
|
194
|
+
_print_auto_result(track, auto_value)
|
|
154
195
|
return 0
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _apply_auto(track: "SimpleNamespace", auto_value: bool) -> None:
|
|
199
|
+
"""Mutate track.meta to set or remove next_up_auto."""
|
|
200
|
+
if auto_value:
|
|
201
|
+
track.meta["next_up_auto"] = True
|
|
202
|
+
else:
|
|
203
|
+
track.meta.pop("next_up_auto", None)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _print_auto_result(track: "SimpleNamespace", auto_value: bool) -> None:
|
|
207
|
+
if auto_value:
|
|
208
|
+
print(f"✓ set next_up_auto=true on {track.name}")
|
|
209
|
+
else:
|
|
210
|
+
print(f"✓ cleared next_up_auto on {track.name}")
|
|
@@ -124,10 +124,8 @@ def build_export(tracks, issues_by_track, visibility, now: str,
|
|
|
124
124
|
in_progress_nums=in_progress_set,
|
|
125
125
|
order=next_up_order,
|
|
126
126
|
)
|
|
127
|
-
next_up_is_auto = True
|
|
128
127
|
else:
|
|
129
128
|
next_up = [n for n in (t.meta.get("next_up") or []) if n not in closed_nums]
|
|
130
|
-
next_up_is_auto = False
|
|
131
129
|
out["tracks"].append({
|
|
132
130
|
"name": t.name,
|
|
133
131
|
"repo": t.repo,
|
|
@@ -155,9 +153,11 @@ def build_export(tracks, issues_by_track, visibility, now: str,
|
|
|
155
153
|
"plan": plan_by_track.get(t.name),
|
|
156
154
|
# Effective next_up ranking preset for this track (#326 Phase 2).
|
|
157
155
|
"next_up_preset": next_up_preset_name,
|
|
158
|
-
# True when
|
|
159
|
-
#
|
|
160
|
-
|
|
156
|
+
# True when the track has `next_up_auto: true` set in its frontmatter,
|
|
157
|
+
# meaning the next-up list is auto-derived from the ranking preset (#326).
|
|
158
|
+
# Reflects the SETTING, not whether derivation actually ran (so the
|
|
159
|
+
# viewer toggle shows On even when a track has zero open issues).
|
|
160
|
+
"next_up_auto": bool(t.meta.get("next_up_auto")),
|
|
161
161
|
})
|
|
162
162
|
out["untracked"] = [
|
|
163
163
|
{"repo": repo, "issues": [normalize_issue(r) for r in rows]}
|
|
@@ -86,6 +86,31 @@ class BuildExportNextUpAutoTest(unittest.TestCase):
|
|
|
86
86
|
self.assertEqual(tr["next_up"], [7]) # the curated list, unchanged
|
|
87
87
|
self.assertFalse(tr["next_up_auto"])
|
|
88
88
|
|
|
89
|
+
def test_auto_on_with_zero_open_issues_still_exports_flag_true(self):
|
|
90
|
+
"""next_up_auto: true + zero fetched issues → next_up_auto=True in export (the
|
|
91
|
+
SETTING, not whether auto-derivation actually ran). Viewer toggle must show
|
|
92
|
+
On even when there are no issues to rank.
|
|
93
|
+
|
|
94
|
+
When no issues are fetched, the auto-derivation branch does not run (there's
|
|
95
|
+
nothing to rank); the export still emits next_up_auto=True so the viewer
|
|
96
|
+
knows the flag is on. The next_up list itself falls through to the curated
|
|
97
|
+
list (which may be non-empty — that's acceptable and a separate concern)."""
|
|
98
|
+
# Use a track with no stored next_up to isolate the flag assertion cleanly.
|
|
99
|
+
meta = {"status": "active", "launch_priority": "P2", "milestone_alignment": "v1",
|
|
100
|
+
"blockers": [], "next_up": [], "depends_on": [],
|
|
101
|
+
"next_up_auto": True,
|
|
102
|
+
"github": {"repo": "o/r", "issues": []}}
|
|
103
|
+
from types import SimpleNamespace
|
|
104
|
+
from pathlib import Path
|
|
105
|
+
t = SimpleNamespace(name="t", repo="o/r", tier="private",
|
|
106
|
+
path=Path("/tmp/notes/t.md"), folder="myrepo", meta=meta)
|
|
107
|
+
out = build_export([t],
|
|
108
|
+
{("o/r", "t"): []}, # zero issues fetched
|
|
109
|
+
{"o/r": "PRIVATE"}, now="t")
|
|
110
|
+
tr = out["tracks"][0]
|
|
111
|
+
self.assertTrue(tr["next_up_auto"]) # flag reflects setting, not derivation
|
|
112
|
+
self.assertEqual(tr["next_up"], []) # no issues → empty list
|
|
113
|
+
|
|
89
114
|
|
|
90
115
|
class BuildExportNextUpFilterTest(unittest.TestCase):
|
|
91
116
|
"""next_up entries whose issue is closed in the fetched payload are filtered out."""
|
|
@@ -197,5 +197,100 @@ class SetNextUpTest(unittest.TestCase):
|
|
|
197
197
|
self.assertIn("--order is ignored", err.getvalue())
|
|
198
198
|
|
|
199
199
|
|
|
200
|
+
class SetNextUpAutoFlagTest(unittest.TestCase):
|
|
201
|
+
"""Tests for --auto=on|off flag on set-next-up."""
|
|
202
|
+
|
|
203
|
+
def _drive_with_stderr(self, args, vis="PRIVATE", cfg=None, track=None):
|
|
204
|
+
"""Like _drive but also captures stderr."""
|
|
205
|
+
base_cfg = {"notes_root": "/tmp"}
|
|
206
|
+
if cfg is not None:
|
|
207
|
+
base_cfg.update(cfg)
|
|
208
|
+
t = track if track is not None else _t()
|
|
209
|
+
with patch("commands.set_next_up.load_config", return_value=base_cfg), \
|
|
210
|
+
patch("commands.set_next_up.discover_tracks", return_value=[t]), \
|
|
211
|
+
patch("lib.write_guard.repo_visibility", return_value=vis), \
|
|
212
|
+
patch("commands.set_next_up.write_file") as mw:
|
|
213
|
+
out, err = io.StringIO(), io.StringIO()
|
|
214
|
+
with redirect_stdout(out), redirect_stderr(err):
|
|
215
|
+
rc = set_next_up.run(args)
|
|
216
|
+
return rc, mw, out.getvalue(), err.getvalue()
|
|
217
|
+
|
|
218
|
+
def test_auto_on_writes_next_up_auto_true(self):
|
|
219
|
+
"""--auto=on sets next_up_auto: True in track meta."""
|
|
220
|
+
rc, mw, out, err = self._drive_with_stderr(["ph", "--auto=on"])
|
|
221
|
+
self.assertEqual(rc, 0)
|
|
222
|
+
mw.assert_called_once()
|
|
223
|
+
meta = mw.call_args[0][1]
|
|
224
|
+
self.assertTrue(meta.get("next_up_auto"))
|
|
225
|
+
|
|
226
|
+
def test_auto_off_removes_next_up_auto(self):
|
|
227
|
+
"""--auto=off removes next_up_auto from track meta."""
|
|
228
|
+
t = _t(meta={"status": "active", "next_up_auto": True})
|
|
229
|
+
rc, mw, out, err = self._drive_with_stderr(["ph", "--auto=off"], track=t)
|
|
230
|
+
self.assertEqual(rc, 0)
|
|
231
|
+
mw.assert_called_once()
|
|
232
|
+
meta = mw.call_args[0][1]
|
|
233
|
+
self.assertNotIn("next_up_auto", meta)
|
|
234
|
+
|
|
235
|
+
def test_auto_off_on_track_without_key_still_succeeds(self):
|
|
236
|
+
"""--auto=off on a track with no next_up_auto key still writes ok (no KeyError)."""
|
|
237
|
+
rc, mw, out, err = self._drive_with_stderr(["ph", "--auto=off"])
|
|
238
|
+
self.assertEqual(rc, 0)
|
|
239
|
+
mw.assert_called_once()
|
|
240
|
+
meta = mw.call_args[0][1]
|
|
241
|
+
self.assertNotIn("next_up_auto", meta)
|
|
242
|
+
|
|
243
|
+
def test_auto_bogus_returns_rc2_no_write(self):
|
|
244
|
+
"""--auto=bogus → rc=2 and no write."""
|
|
245
|
+
rc, mw, out, err = self._drive_with_stderr(["ph", "--auto=bogus"])
|
|
246
|
+
self.assertEqual(rc, 2)
|
|
247
|
+
mw.assert_not_called()
|
|
248
|
+
|
|
249
|
+
def test_auto_standalone_private_writes(self):
|
|
250
|
+
"""--auto=on alone (no --preset/--order/--clear) is accepted on private repo."""
|
|
251
|
+
rc, mw, out, err = self._drive_with_stderr(["ph", "--auto=on"])
|
|
252
|
+
self.assertEqual(rc, 0)
|
|
253
|
+
mw.assert_called_once()
|
|
254
|
+
|
|
255
|
+
def test_auto_standalone_public_needs_confirm(self):
|
|
256
|
+
"""--auto=on alone on a PUBLIC repo → needs_confirm, no write."""
|
|
257
|
+
rc, mw, out, err = self._drive_with_stderr(["ph", "--auto=on"], vis="PUBLIC")
|
|
258
|
+
self.assertEqual(rc, 0)
|
|
259
|
+
mw.assert_not_called()
|
|
260
|
+
self.assertIn("needs_confirm", out)
|
|
261
|
+
|
|
262
|
+
def test_auto_standalone_public_with_valid_confirm_writes(self):
|
|
263
|
+
"""--auto=on alone on PUBLIC repo with valid --confirm token proceeds to write."""
|
|
264
|
+
tok = make_token("o/r", "ph")
|
|
265
|
+
rc, mw, out, err = self._drive_with_stderr(
|
|
266
|
+
["ph", "--auto=on", f"--confirm={tok}"], vis="PUBLIC"
|
|
267
|
+
)
|
|
268
|
+
self.assertEqual(rc, 0)
|
|
269
|
+
mw.assert_called_once()
|
|
270
|
+
meta = mw.call_args[0][1]
|
|
271
|
+
self.assertTrue(meta.get("next_up_auto"))
|
|
272
|
+
|
|
273
|
+
def test_auto_on_combined_with_preset_writes_both(self):
|
|
274
|
+
"""--auto=on --preset=backlog sets BOTH next_up_auto AND next_up_order in one write."""
|
|
275
|
+
rc, mw, out, err = self._drive_with_stderr(["ph", "--auto=on", "--preset=backlog"])
|
|
276
|
+
self.assertEqual(rc, 0)
|
|
277
|
+
mw.assert_called_once()
|
|
278
|
+
meta = mw.call_args[0][1]
|
|
279
|
+
self.assertTrue(meta.get("next_up_auto"))
|
|
280
|
+
self.assertEqual(meta.get("next_up_order"), {"preset": "backlog"})
|
|
281
|
+
|
|
282
|
+
def test_auto_on_prints_success_message(self):
|
|
283
|
+
"""--auto=on prints a clear success line."""
|
|
284
|
+
rc, mw, out, err = self._drive_with_stderr(["ph", "--auto=on"])
|
|
285
|
+
self.assertIn("next_up_auto", out)
|
|
286
|
+
self.assertIn("true", out.lower())
|
|
287
|
+
|
|
288
|
+
def test_auto_off_prints_success_message(self):
|
|
289
|
+
"""--auto=off prints a clear success line."""
|
|
290
|
+
t = _t(meta={"status": "active", "next_up_auto": True})
|
|
291
|
+
rc, mw, out, err = self._drive_with_stderr(["ph", "--auto=off"], track=t)
|
|
292
|
+
self.assertIn("next_up_auto", out)
|
|
293
|
+
|
|
294
|
+
|
|
200
295
|
if __name__ == "__main__":
|
|
201
296
|
unittest.main()
|
|
@@ -167,10 +167,10 @@ DESCRIPTIONS = [
|
|
|
167
167
|
"Guarded edit of a track's frontmatter fields (status, launch_priority, milestone_alignment, blockers, next_up). Validates field names + status values; blockers/next_up take comma-separated issue numbers. Setting `next_up` here writes ONLY the frontmatter field — for next_up plus a session-log entry (and a body refresh), use `handoff --set-next` instead. Writes into a PUBLIC repo only with a confirm token: without one it prints {needs_confirm, reason, token} and makes no change (the VS Code viewer surfaces that as a modal, then re-invokes with --confirm=<token>).",
|
|
168
168
|
"Programmatic/GUI edits that have no dedicated verb — e.g. the VS Code extension changing a status or blockers list. On the terminal you'll usually use the named verbs instead.",
|
|
169
169
|
"/work-plan set ux-redesign status=parked"),
|
|
170
|
-
("set-next-up", "<track | track@repo> (--preset=<name> | --order=a,b,c | --clear) [--repo=<key>] [--confirm=<token>]",
|
|
171
|
-
"Configure the ranking preset for a track's
|
|
172
|
-
"When you want a track to use a different ranking order than the default (flow). Use priority-driven for pure backlog work with no milestones, backlog to surface oldest stalled issues first.",
|
|
173
|
-
"/work-plan set-next-up my-track --preset=priority-driven"),
|
|
170
|
+
("set-next-up", "<track | track@repo> (--preset=<name> | --order=a,b,c | --clear | --auto=on|off) [--repo=<key>] [--confirm=<token>]",
|
|
171
|
+
"Configure the ranking preset and/or auto-derivation flag for a track's next_up list. --preset sets one of the named presets (flow, priority-driven, backlog) or 'custom' (which requires --order). --order=a,b,c sets a custom comma-separated criterion list (milestone, dependency, priority, recency, aging). --clear reverts to the global or default preset. --auto=on activates auto-derivation (brief/orient/export derive next-up live via the ranking preset instead of the curated list); --auto=off reverts to the curated list. --auto can be used standalone or combined with --preset/--order/--clear. Writes next_up_order and/or next_up_auto into the track's frontmatter (does NOT touch the next_up issue list). Public-repo gated: without --confirm it prints {needs_confirm, reason, token} and makes no change.",
|
|
172
|
+
"When you want a track to use a different ranking order than the default (flow), or to activate auto-derivation so the viewer always shows a freshly-ranked list. Use priority-driven for pure backlog work with no milestones, backlog to surface oldest stalled issues first.",
|
|
173
|
+
"/work-plan set-next-up my-track --preset=priority-driven --auto=on"),
|
|
174
174
|
("new-track", "<repo> <slug> [--priority=P0..P3] [--milestone=<m>] [--private] [--confirm=<token>]",
|
|
175
175
|
"Create a brand-new track file under notes_root in one headless call. <repo> is either a configured key (e.g. 'myproject') or a bare org/repo slug (e.g. 'your-org/myproject'). Writes frontmatter with status=active and optional priority/milestone. Gates on public repos — prints {needs_confirm, token} and exits cleanly; re-run with --confirm=<token> to proceed.",
|
|
176
176
|
"When a new feature branch or initiative starts and you want the track file created immediately — especially from a non-terminal caller like the VS Code extension that can't interactively run init.",
|