workstream-cli 0.0.1__py3-none-any.whl
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.
- workstream/ARCHITECTURE.md +89 -0
- workstream/__init__.py +8 -0
- workstream/cli.py +136 -0
- workstream/commands/__init__.py +0 -0
- workstream/commands/backfill.py +139 -0
- workstream/commands/block.py +93 -0
- workstream/commands/checkin.py +51 -0
- workstream/commands/cron.py +119 -0
- workstream/commands/focus_cmd.py +273 -0
- workstream/commands/idea.py +172 -0
- workstream/commands/index.py +89 -0
- workstream/commands/init.py +567 -0
- workstream/commands/inspect_cmd.py +354 -0
- workstream/commands/list_cmd.py +99 -0
- workstream/commands/nest.py +108 -0
- workstream/commands/new.py +95 -0
- workstream/commands/next_cmd.py +333 -0
- workstream/commands/report.py +190 -0
- workstream/commands/resume.py +145 -0
- workstream/commands/review.py +227 -0
- workstream/commands/serve.py +23 -0
- workstream/commands/setup.py +178 -0
- workstream/commands/show.py +123 -0
- workstream/commands/snooze.py +117 -0
- workstream/commands/stale.py +116 -0
- workstream/commands/sweep.py +1753 -0
- workstream/commands/tree.py +105 -0
- workstream/commands/update_status.py +117 -0
- workstream/config.py +322 -0
- workstream/extensions/__init__.py +0 -0
- workstream/extensions/workstream.ts +633 -0
- workstream/focus_artifact.py +157 -0
- workstream/git.py +194 -0
- workstream/harness.py +49 -0
- workstream/llm.py +78 -0
- workstream/markdown.py +501 -0
- workstream/models.py +274 -0
- workstream/plan_index.py +88 -0
- workstream/provisioning.py +196 -0
- workstream/repo_discovery.py +158 -0
- workstream/review_artifact.py +96 -0
- workstream/scripts/migrate_statuses.py +120 -0
- workstream/skills/__init__.py +0 -0
- workstream/skills/workstream_context/SKILL.md +75 -0
- workstream/skills/workstream_context/__init__.py +0 -0
- workstream/skills/workstream_focus/SKILL.md +141 -0
- workstream/skills/workstream_init/SKILL.md +86 -0
- workstream/skills/workstream_review/SKILL.md +224 -0
- workstream/skills/workstream_sweep/SKILL.md +178 -0
- workstream/sweep_state.py +93 -0
- workstream/templates/dashboard.html +382 -0
- workstream/templates/detail.html +360 -0
- workstream/templates/plan.html +210 -0
- workstream/test/__init__.py +0 -0
- workstream/test/conftest.py +221 -0
- workstream/test/fixtures/sample_sprint_note.md +10 -0
- workstream/test/fixtures/sample_workstream.md +41 -0
- workstream/test/test_backfill.py +180 -0
- workstream/test/test_batch_writeback.py +81 -0
- workstream/test/test_commands.py +938 -0
- workstream/test/test_config.py +54 -0
- workstream/test/test_focus_artifact.py +211 -0
- workstream/test/test_git.py +88 -0
- workstream/test/test_heuristics.py +136 -0
- workstream/test/test_hierarchy.py +231 -0
- workstream/test/test_init.py +452 -0
- workstream/test/test_inspect.py +143 -0
- workstream/test/test_llm.py +78 -0
- workstream/test/test_markdown.py +626 -0
- workstream/test/test_models.py +506 -0
- workstream/test/test_next.py +206 -0
- workstream/test/test_plan_index.py +83 -0
- workstream/test/test_provisioning.py +270 -0
- workstream/test/test_repo_discovery.py +181 -0
- workstream/test/test_resume.py +71 -0
- workstream/test/test_sweep.py +1196 -0
- workstream/test/test_sweep_state.py +86 -0
- workstream/test/test_thoughts.py +516 -0
- workstream/test/test_web.py +606 -0
- workstream/thoughts.py +505 -0
- workstream/web.py +444 -0
- workstream_cli-0.0.1.dist-info/LICENSE +21 -0
- workstream_cli-0.0.1.dist-info/METADATA +93 -0
- workstream_cli-0.0.1.dist-info/RECORD +86 -0
- workstream_cli-0.0.1.dist-info/WHEEL +4 -0
- workstream_cli-0.0.1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""ws stale — find workstreams that need attention."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import date
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from face import Command
|
|
8
|
+
|
|
9
|
+
from workstream.config import Config
|
|
10
|
+
from workstream.cli import load_all_workstreams
|
|
11
|
+
from workstream.models import Workstream
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_stale_workstreams(
|
|
15
|
+
workstreams_dir: Path,
|
|
16
|
+
days: int = 7,
|
|
17
|
+
) -> tuple[list[Workstream], list[Workstream], list[Workstream], list[Workstream], list[Workstream]]:
|
|
18
|
+
"""Return (snoozed_stale, active_idle, snoozed_expired, needs_direction, blocked) workstreams."""
|
|
19
|
+
workstreams = load_all_workstreams(workstreams_dir)
|
|
20
|
+
|
|
21
|
+
snoozed_stale = [
|
|
22
|
+
w for w in workstreams
|
|
23
|
+
if w.status == 'snoozed' and w.days_idle() > days
|
|
24
|
+
]
|
|
25
|
+
active_idle = [
|
|
26
|
+
w for w in workstreams
|
|
27
|
+
if w.status == 'active' and w.days_idle() > days
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# Sort both by days idle descending
|
|
31
|
+
snoozed_stale.sort(key=lambda w: w.days_idle(), reverse=True)
|
|
32
|
+
active_idle.sort(key=lambda w: w.days_idle(), reverse=True)
|
|
33
|
+
|
|
34
|
+
today_str = date.today().isoformat()
|
|
35
|
+
snoozed_expired = [
|
|
36
|
+
w for w in workstreams
|
|
37
|
+
if w.snooze_until and w.snooze_until <= today_str
|
|
38
|
+
]
|
|
39
|
+
snoozed_expired.sort(key=lambda w: w.snooze_until)
|
|
40
|
+
|
|
41
|
+
needs_direction = [
|
|
42
|
+
w for w in workstreams
|
|
43
|
+
if w.needs_direction()
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
blocked = [
|
|
47
|
+
w for w in workstreams
|
|
48
|
+
if w.status == 'blocked'
|
|
49
|
+
]
|
|
50
|
+
blocked.sort(key=lambda w: w.days_idle(), reverse=True)
|
|
51
|
+
|
|
52
|
+
return snoozed_stale, active_idle, snoozed_expired, needs_direction, blocked
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _stale_handler(config: Config, days: int = 7) -> None:
|
|
56
|
+
snoozed_stale, active_idle, snoozed_expired, needs_direction, blocked = get_stale_workstreams(
|
|
57
|
+
config.workstreams_path, days=days,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if not snoozed_stale and not active_idle and not snoozed_expired and not needs_direction and not blocked:
|
|
61
|
+
print('Nothing stale. Nice work.')
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
print(f'Stale workstreams (no activity in {days}+ days):')
|
|
65
|
+
print()
|
|
66
|
+
|
|
67
|
+
if snoozed_stale:
|
|
68
|
+
print(' SNOOZED (check if still blocked):')
|
|
69
|
+
for ws in snoozed_stale:
|
|
70
|
+
idle = ws.days_idle()
|
|
71
|
+
print(f' - {ws.title} (snoozed {idle} days ago)')
|
|
72
|
+
if ws.snooze_reason:
|
|
73
|
+
print(f' Reason: "{ws.snooze_reason}"')
|
|
74
|
+
print()
|
|
75
|
+
|
|
76
|
+
if active_idle:
|
|
77
|
+
print(' IDLE (active but no recent work):')
|
|
78
|
+
for ws in active_idle:
|
|
79
|
+
idle = ws.days_idle()
|
|
80
|
+
last = ws.computed_last_activity
|
|
81
|
+
print(f' - {ws.title} ({idle} days) -- last: {last}')
|
|
82
|
+
print()
|
|
83
|
+
|
|
84
|
+
if snoozed_expired:
|
|
85
|
+
today_str = date.today().isoformat()
|
|
86
|
+
print(' SNOOZED (snooze expired, needs attention):')
|
|
87
|
+
for ws in snoozed_expired:
|
|
88
|
+
print(f' - {ws.title} (snoozed until {ws.snooze_until}, {ws.snooze_count} times)')
|
|
89
|
+
expired_thoughts = [
|
|
90
|
+
t for t in ws.thoughts
|
|
91
|
+
if t.snooze_until and t.snooze_until <= today_str
|
|
92
|
+
]
|
|
93
|
+
if expired_thoughts:
|
|
94
|
+
print(' Thoughts:')
|
|
95
|
+
for t in expired_thoughts:
|
|
96
|
+
print(f' - [{t.date}] {t.text} (snoozed until {t.snooze_until})')
|
|
97
|
+
|
|
98
|
+
if needs_direction:
|
|
99
|
+
print(' NEEDS DIRECTION (active but no clear next step):')
|
|
100
|
+
for ws in needs_direction:
|
|
101
|
+
print(f' - {ws.title} (no next actions, no active plans)')
|
|
102
|
+
print()
|
|
103
|
+
|
|
104
|
+
if blocked:
|
|
105
|
+
print(' BLOCKED (waiting on external dependency):')
|
|
106
|
+
for ws in blocked:
|
|
107
|
+
idle = ws.days_idle()
|
|
108
|
+
print(f' - {ws.title} ({idle} days idle)')
|
|
109
|
+
if ws.blocked_notes:
|
|
110
|
+
print(f' Blocked: "{ws.blocked_notes}"')
|
|
111
|
+
print()
|
|
112
|
+
|
|
113
|
+
def get_command() -> Command:
|
|
114
|
+
cmd = Command(_stale_handler, name='stale', doc='Find stale workstreams needing attention.')
|
|
115
|
+
cmd.add('--days', parse_as=int, missing=7, doc='Idle threshold in days')
|
|
116
|
+
return cmd
|