@stylusnexus/work-plan 2026.7.3 → 2026.7.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.
package/README.md CHANGED
@@ -503,6 +503,7 @@ The bundled `notes/` folder stays empty until you run `/work-plan init-repo <key
503
503
  - **AI subcommands (`group`, `suggest-priorities`) send issue titles to Claude** via Claude Code's existing integration. Body content, code, and PR contents are NOT sent. If your repo is private and you're cautious about what reaches the model, skip these subcommands.
504
504
  - **`init-repo` writes to your config via `yq -i`.** Inputs are JSON-encoded before being passed to `yq`, so a maliciously crafted `--github=` value can't break out of the YAML edit.
505
505
  - **`install.sh` / `install.ps1` only touch user-owned dirs.** No `sudo`, no system-wide changes, no privilege escalation.
506
+ - **Least-privilege tool access.** The `work-plan` and `repo-activity-summary` skills declare `allowed-tools` frontmatter, so Claude Code grants them a scoped allowlist — `work-plan` gets `Bash(work-plan:*)`, `Bash(python3:*)`, and `Write` (the two-step AI subcommands' JSON cache); `repo-activity-summary` gets `Bash(gh:*)` — rather than unrestricted shell.
506
507
 
507
508
  For vulnerability reporting, threat model, and past advisories, see [SECURITY.md](./SECURITY.md).
508
509
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 2026.07.03+7d98e1e
1
+ 2026.07.10+92f8eef
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stylusnexus/work-plan",
3
- "version": "2026.7.3",
3
+ "version": "2026.7.10",
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"
@@ -2,6 +2,7 @@
2
2
  name: work-plan
3
3
  description: Use when starting or ending a work session across many GitHub issues, switching between parallel agent sessions on different workstreams, re-orienting on what to do next, sweeping for stale tracking state, or bootstrapping a new repo into a daily-planning system.
4
4
  argument-hint: "[brief|handoff|orient|reconcile|hygiene|--help]"
5
+ allowed-tools: Bash(work-plan:*), Bash(python3:*), Write
5
6
  ---
6
7
 
7
8
  # Work Plan
@@ -24,6 +24,20 @@ from lib.drift import detect_drift
24
24
  from lib.render import time_aware_framing, render_track_row, render_archived_reopen
25
25
 
26
26
 
27
+ def _numeric_refs(*ref_lists) -> list:
28
+ """Union of the given ref lists, keeping only int issue numbers, sorted.
29
+
30
+ `next_up` legitimately holds non-issue string tokens (epic/track names like
31
+ `golden-path-v2`) alongside issue numbers. Those aren't fetchable GitHub
32
+ issues, and `sorted()` can't order a set that mixes str and int (#417), so
33
+ string tokens are dropped from the fetch set.
34
+ """
35
+ refs: set = set()
36
+ for lst in ref_lists:
37
+ refs |= set(lst or [])
38
+ return sorted(n for n in refs if isinstance(n, int))
39
+
40
+
27
41
  def run(args: list[str]) -> int:
28
42
  flags, _ = parse_flags(args, {"--repo"})
29
43
  repo_arg = flags.get("--repo")
@@ -137,7 +151,8 @@ def _build_track_block(track, cfg, now: datetime) -> dict:
137
151
  stored_next_up = meta.get("next_up") or []
138
152
  # Fetch state for stored next_up issues even if they're not in github.issues,
139
153
  # so stale closed entries surface as a clear signal rather than vanishing.
140
- fetch_nums = sorted(set(issue_nums) | set(stored_next_up))
154
+ # Only numeric refs are fetchable; string tokens in next_up are dropped (#417).
155
+ fetch_nums = _numeric_refs(issue_nums, stored_next_up)
141
156
  issues = fetch_issues(repo, fetch_nums) if (repo and fetch_nums) else []
142
157
  issues_by_num = {i["number"]: i for i in issues}
143
158
 
@@ -0,0 +1,36 @@
1
+ """#417 — brief must not crash when next_up mixes issue numbers and string tokens.
2
+
3
+ `sorted(set(issue_nums) | set(stored_next_up))` raised
4
+ `TypeError: '<' not supported between instances of 'str' and 'int'` for any track
5
+ whose `next_up` held a non-issue token (e.g. an epic name like `golden-path-v2`)
6
+ next to issue numbers. `_numeric_refs` unions the ref lists, drops non-int tokens
7
+ (they aren't fetchable issues), and sorts what remains.
8
+ """
9
+ import sys
10
+ import unittest
11
+ from pathlib import Path
12
+
13
+ SKILL_ROOT = Path(__file__).resolve().parents[1]
14
+ sys.path.insert(0, str(SKILL_ROOT))
15
+
16
+ from commands.brief import _numeric_refs
17
+
18
+
19
+ class NumericRefsTest(unittest.TestCase):
20
+ def test_mixed_str_and_int_does_not_crash_and_drops_tokens(self):
21
+ # The exact shape that crashed live: a string epic token beside numbers.
22
+ result = _numeric_refs([6012, 6015], ["golden-path-v2", 5996, 5979])
23
+ self.assertEqual(result, [5979, 5996, 6012, 6015])
24
+
25
+ def test_dedupes_across_lists(self):
26
+ self.assertEqual(_numeric_refs([100, 200], [200, 300]), [100, 200, 300])
27
+
28
+ def test_handles_none_and_empty(self):
29
+ self.assertEqual(_numeric_refs(None, []), [])
30
+
31
+ def test_all_string_tokens_yield_empty(self):
32
+ self.assertEqual(_numeric_refs(["golden-path-v2", "epic-x"]), [])
33
+
34
+
35
+ if __name__ == "__main__":
36
+ unittest.main()