@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.
|
|
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
|
+
"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
|
-
|
|
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()
|