@stylusnexus/work-plan 2026.6.13-2 → 2026.6.14
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 +7 -2
- package/VERSION +1 -1
- package/package.json +1 -1
- package/skills/work-plan/commands/brief.py +12 -0
- package/skills/work-plan/commands/close_issue.py +2 -2
- package/skills/work-plan/commands/export.py +18 -4
- package/skills/work-plan/commands/in_progress.py +110 -0
- package/skills/work-plan/commands/where_was_i.py +30 -2
- package/skills/work-plan/lib/export_model.py +36 -5
- package/skills/work-plan/lib/git_state.py +32 -0
- package/skills/work-plan/lib/github_state.py +71 -6
- package/skills/work-plan/lib/in_progress.py +23 -0
- package/skills/work-plan/lib/render.py +5 -0
- package/skills/work-plan/tests/test_close_issue.py +2 -2
- package/skills/work-plan/tests/test_export.py +139 -11
- package/skills/work-plan/tests/test_export_command.py +27 -0
- package/skills/work-plan/tests/test_git_state.py +38 -1
- package/skills/work-plan/tests/test_github_state.py +66 -0
- package/skills/work-plan/tests/test_in_progress.py +43 -0
- package/skills/work-plan/tests/test_in_progress_command.py +166 -0
- package/skills/work-plan/tests/test_list_open_issues.py +8 -3
- package/skills/work-plan/tests/test_register_in_progress.py +22 -0
- package/skills/work-plan/tests/test_render.py +48 -0
- package/skills/work-plan/tests/test_where_was_i.py +80 -0
- package/skills/work-plan/work_plan.py +6 -1
|
@@ -106,5 +106,53 @@ class RenderTrackRowTest(unittest.TestCase):
|
|
|
106
106
|
self.assertNotIn("(P3, open, ", row)
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
class RenderInProgressTest(unittest.TestCase):
|
|
110
|
+
def _block(self, next_up):
|
|
111
|
+
return {
|
|
112
|
+
"name": "alpha", "operational_status": "active", "launch_priority": "P2",
|
|
113
|
+
"milestone_alignment": "—", "last_touched_label": "1d ago",
|
|
114
|
+
"last_handoff_label": "1d ago", "next_up": next_up,
|
|
115
|
+
"next_up_stale_closed_count": 0, "track_slug": "alpha",
|
|
116
|
+
"active_branches": [], "new_issues": [], "blockers": [],
|
|
117
|
+
"drift_items": [], "closure_ready": False, "closure_signals_summary": None,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
def test_in_progress_item_marked(self):
|
|
121
|
+
row = render_track_row(self._block([
|
|
122
|
+
{"number": 271, "title": "x", "priority": "P1", "state": "open",
|
|
123
|
+
"milestone": None, "in_progress": True},
|
|
124
|
+
]))
|
|
125
|
+
self.assertIn("in-progress", row)
|
|
126
|
+
self.assertIn("#271", row)
|
|
127
|
+
|
|
128
|
+
def test_non_in_progress_item_not_marked(self):
|
|
129
|
+
row = render_track_row(self._block([
|
|
130
|
+
{"number": 9, "title": "y", "priority": "P2", "state": "open",
|
|
131
|
+
"milestone": None, "in_progress": False},
|
|
132
|
+
]))
|
|
133
|
+
self.assertNotIn("in-progress", row)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class RenderBlockedByTest(unittest.TestCase):
|
|
137
|
+
def _block(self, item):
|
|
138
|
+
return {"name": "alpha", "operational_status": "active", "launch_priority": "P2",
|
|
139
|
+
"milestone_alignment": "—", "last_touched_label": "1d", "last_handoff_label": "1d",
|
|
140
|
+
"next_up": [item], "next_up_stale_closed_count": 0, "track_slug": "alpha",
|
|
141
|
+
"active_branches": [], "new_issues": [], "blockers": [],
|
|
142
|
+
"drift_items": [], "closure_ready": False, "closure_signals_summary": None}
|
|
143
|
+
|
|
144
|
+
def _item(self, blocked):
|
|
145
|
+
return {"number": 5, "title": "x", "priority": "P1", "state": "open",
|
|
146
|
+
"milestone": None, "in_progress": False, "blocked_by_display": blocked}
|
|
147
|
+
|
|
148
|
+
def test_blocked_by_annotation_rendered(self):
|
|
149
|
+
row = render_track_row(self._block(self._item(["#9"])))
|
|
150
|
+
self.assertIn("blocked by #9", row)
|
|
151
|
+
|
|
152
|
+
def test_no_annotation_when_empty(self):
|
|
153
|
+
row = render_track_row(self._block(self._item([])))
|
|
154
|
+
self.assertNotIn("blocked by", row)
|
|
155
|
+
|
|
156
|
+
|
|
109
157
|
if __name__ == "__main__":
|
|
110
158
|
unittest.main()
|
|
@@ -513,5 +513,85 @@ class OrientRepoFlagTest(unittest.TestCase):
|
|
|
513
513
|
self.assertIn("ambiguous", out.lower())
|
|
514
514
|
|
|
515
515
|
|
|
516
|
+
class OrientInProgressTest(unittest.TestCase):
|
|
517
|
+
def test_next_pick_marked_in_progress(self):
|
|
518
|
+
from types import SimpleNamespace
|
|
519
|
+
track = SimpleNamespace(
|
|
520
|
+
name="alpha", repo="o/r", local_path=Path("/repo"), path=Path("/n/alpha.md"),
|
|
521
|
+
body="", meta={"track": "alpha", "launch_priority": "P1",
|
|
522
|
+
"milestone_alignment": "—",
|
|
523
|
+
"github": {"issues": [271]}, "next_up": [271]})
|
|
524
|
+
issue = {"number": 271, "title": "x", "state": "open", "labels": [], "milestone": None}
|
|
525
|
+
with mock.patch("commands.where_was_i.fetch_issues", return_value=[issue]), \
|
|
526
|
+
mock.patch("commands.where_was_i.hot_issue_numbers", return_value={271}), \
|
|
527
|
+
mock.patch("commands.where_was_i.current_branch", return_value=None), \
|
|
528
|
+
mock.patch("commands.where_was_i.find_new_issues_for_tracks", return_value={}):
|
|
529
|
+
out = io.StringIO()
|
|
530
|
+
with redirect_stdout(out):
|
|
531
|
+
where_was_i._orient_track(track)
|
|
532
|
+
self.assertIn("in-progress", out.getvalue())
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
class OrientBlockedByTest(unittest.TestCase):
|
|
536
|
+
def test_next_pick_shows_blocked_by(self):
|
|
537
|
+
from types import SimpleNamespace
|
|
538
|
+
track = SimpleNamespace(name="alpha", repo="o/r", local_path=None,
|
|
539
|
+
path=Path("/n/alpha.md"), body="",
|
|
540
|
+
meta={"track": "alpha", "launch_priority": "P1",
|
|
541
|
+
"milestone_alignment": "—",
|
|
542
|
+
"github": {"issues": [5]}, "next_up": [5], "blockers": []})
|
|
543
|
+
issue = {"number": 5, "title": "x", "state": "open", "labels": [],
|
|
544
|
+
"blocked_by": [{"number": 9, "repo": "o/r", "title": "dep"}], "blocking": []}
|
|
545
|
+
with mock.patch("commands.where_was_i.fetch_issues", return_value=[issue]), \
|
|
546
|
+
mock.patch("commands.where_was_i.hot_issue_numbers", return_value=set()), \
|
|
547
|
+
mock.patch("commands.where_was_i.current_branch", return_value=None), \
|
|
548
|
+
mock.patch("commands.where_was_i.find_new_issues_for_tracks", return_value={}):
|
|
549
|
+
out = io.StringIO()
|
|
550
|
+
with redirect_stdout(out):
|
|
551
|
+
from commands import where_was_i
|
|
552
|
+
where_was_i._orient_track(track)
|
|
553
|
+
self.assertIn("blocked by #9", out.getvalue())
|
|
554
|
+
|
|
555
|
+
def _orient_with_blocked_by(self, blocked_by, blockers):
|
|
556
|
+
from types import SimpleNamespace
|
|
557
|
+
track = SimpleNamespace(name="alpha", repo="o/r", local_path=None,
|
|
558
|
+
path=Path("/n/alpha.md"), body="",
|
|
559
|
+
meta={"track": "alpha", "launch_priority": "P1",
|
|
560
|
+
"milestone_alignment": "—",
|
|
561
|
+
"github": {"issues": [5]}, "next_up": [5],
|
|
562
|
+
"blockers": blockers})
|
|
563
|
+
issue = {"number": 5, "title": "x", "state": "open", "labels": [],
|
|
564
|
+
"blocked_by": blocked_by, "blocking": []}
|
|
565
|
+
with mock.patch("commands.where_was_i.fetch_issues", return_value=[issue]), \
|
|
566
|
+
mock.patch("commands.where_was_i.hot_issue_numbers", return_value=set()), \
|
|
567
|
+
mock.patch("commands.where_was_i.current_branch", return_value=None), \
|
|
568
|
+
mock.patch("commands.where_was_i.find_new_issues_for_tracks", return_value={}):
|
|
569
|
+
out = io.StringIO()
|
|
570
|
+
with redirect_stdout(out):
|
|
571
|
+
from commands import where_was_i
|
|
572
|
+
where_was_i._orient_track(track)
|
|
573
|
+
return out.getvalue()
|
|
574
|
+
|
|
575
|
+
def test_cross_repo_blocked_by_shows_qualified_ref(self):
|
|
576
|
+
# A cross-repo edge renders owner/repo#N, not a bare #N.
|
|
577
|
+
out = self._orient_with_blocked_by(
|
|
578
|
+
[{"number": 9, "repo": "other/repo", "title": "dep"}], [])
|
|
579
|
+
self.assertIn("blocked by other/repo#9", out)
|
|
580
|
+
|
|
581
|
+
def test_same_repo_edge_in_manual_blockers_is_suppressed(self):
|
|
582
|
+
# A same-repo edge whose number is a manual blocker is owned by the
|
|
583
|
+
# "Blocker:" line, so it is not re-annotated.
|
|
584
|
+
out = self._orient_with_blocked_by(
|
|
585
|
+
[{"number": 9, "repo": "o/r", "title": "dep"}], [9])
|
|
586
|
+
self.assertNotIn("blocked by", out)
|
|
587
|
+
|
|
588
|
+
def test_cross_repo_edge_not_suppressed_by_same_number_blocker(self):
|
|
589
|
+
# A manual blocker #9 means o/r#9; a cross-repo other/repo#9 is a
|
|
590
|
+
# different issue and must still be annotated.
|
|
591
|
+
out = self._orient_with_blocked_by(
|
|
592
|
+
[{"number": 9, "repo": "other/repo", "title": "dep"}], [9])
|
|
593
|
+
self.assertIn("blocked by other/repo#9", out)
|
|
594
|
+
|
|
595
|
+
|
|
516
596
|
if __name__ == "__main__":
|
|
517
597
|
unittest.main()
|
|
@@ -55,6 +55,7 @@ SUBCOMMANDS = {
|
|
|
55
55
|
"plan-ack": "commands.plan_ack",
|
|
56
56
|
"plan-baseline": "commands.plan_baseline",
|
|
57
57
|
"close-issue": "commands.close_issue",
|
|
58
|
+
"in-progress": "commands.in_progress",
|
|
58
59
|
"export": "commands.export",
|
|
59
60
|
"auth-status": "commands.auth_status",
|
|
60
61
|
"list-open-issues": "commands.list_open_issues",
|
|
@@ -190,9 +191,13 @@ DESCRIPTIONS = [
|
|
|
190
191
|
"When you want a tripwire on a plan you believe is done: stamp its baseline, and get alerted if it later regresses.",
|
|
191
192
|
"/work-plan plan-baseline --repo=myproject -- docs/superpowers/plans/2026-03-16-idea-mode-ui.md"),
|
|
192
193
|
("close-issue", "--repo=<key|slug> [--reason=completed|not_planned] [--comment=<text>] -- <number>",
|
|
193
|
-
"⚠️
|
|
194
|
+
"⚠️ A GitHub-mutating command (others: `in-progress`, `plan-status --issues`) — closes a GitHub issue via `gh issue close` (most of the toolkit is read-only on GitHub). PRs merged to `dev` don't auto-close issues (GitHub auto-closes only from the default branch), so done-but-OPEN issues pile up; this closes one. `--reason` maps to GitHub's completed/not-planned; `--comment` posts a closing note. `--repo` takes a config key or an org/repo slug. The VS Code viewer gates this behind a mandatory 'Close on GitHub?' modal on every close.",
|
|
194
195
|
"When an issue is actually done but stayed open because its PR merged to dev, not main — close it without leaving the editor.",
|
|
195
196
|
"/work-plan close-issue --repo=stylusnexus/work-plan-toolkit --reason=completed --comment='Closed via dev merge' -- 287"),
|
|
197
|
+
("in-progress", "<n> [--clear] [--repo=<key|slug>] [--confirm=<token>]",
|
|
198
|
+
"Mark a tracked GitHub issue as in-progress by adding the `work-plan:in-progress` label (or remove it with --clear). Repo-scoped: resolves <n> to the one tracked repo that lists it, or pass --repo to disambiguate. The label is auto-created. Writes into a PUBLIC repo only with a confirm token (prints {needs_confirm, token} otherwise — the VS Code viewer surfaces it as a modal). brief/orient/the viewer also derive in-progress for free from a hot feat/<n>- or fix/<n>- branch.",
|
|
199
|
+
"When you start actively working an issue that has no hot branch yet (a hot branch is detected automatically), or to clear the flag when you stop.",
|
|
200
|
+
"/work-plan in-progress 271"),
|
|
196
201
|
("set-notes-root", "<path>",
|
|
197
202
|
"Update notes_root in ~/.claude/work-plan/config.yml to an absolute path. Creates the target directory if absent. Prints a WARN if existing frontmatter'd tracks live at the old location (they won't be moved — manual migration required). Non-interactive: safe to call from a GUI or script.",
|
|
198
203
|
"VS Code viewer cold-start: user has picked a folder for their private track notes and the extension invokes this to persist the choice. Also useful on the CLI to relocate notes without hand-editing config.yml.",
|