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.
Files changed (86) hide show
  1. workstream/ARCHITECTURE.md +89 -0
  2. workstream/__init__.py +8 -0
  3. workstream/cli.py +136 -0
  4. workstream/commands/__init__.py +0 -0
  5. workstream/commands/backfill.py +139 -0
  6. workstream/commands/block.py +93 -0
  7. workstream/commands/checkin.py +51 -0
  8. workstream/commands/cron.py +119 -0
  9. workstream/commands/focus_cmd.py +273 -0
  10. workstream/commands/idea.py +172 -0
  11. workstream/commands/index.py +89 -0
  12. workstream/commands/init.py +567 -0
  13. workstream/commands/inspect_cmd.py +354 -0
  14. workstream/commands/list_cmd.py +99 -0
  15. workstream/commands/nest.py +108 -0
  16. workstream/commands/new.py +95 -0
  17. workstream/commands/next_cmd.py +333 -0
  18. workstream/commands/report.py +190 -0
  19. workstream/commands/resume.py +145 -0
  20. workstream/commands/review.py +227 -0
  21. workstream/commands/serve.py +23 -0
  22. workstream/commands/setup.py +178 -0
  23. workstream/commands/show.py +123 -0
  24. workstream/commands/snooze.py +117 -0
  25. workstream/commands/stale.py +116 -0
  26. workstream/commands/sweep.py +1753 -0
  27. workstream/commands/tree.py +105 -0
  28. workstream/commands/update_status.py +117 -0
  29. workstream/config.py +322 -0
  30. workstream/extensions/__init__.py +0 -0
  31. workstream/extensions/workstream.ts +633 -0
  32. workstream/focus_artifact.py +157 -0
  33. workstream/git.py +194 -0
  34. workstream/harness.py +49 -0
  35. workstream/llm.py +78 -0
  36. workstream/markdown.py +501 -0
  37. workstream/models.py +274 -0
  38. workstream/plan_index.py +88 -0
  39. workstream/provisioning.py +196 -0
  40. workstream/repo_discovery.py +158 -0
  41. workstream/review_artifact.py +96 -0
  42. workstream/scripts/migrate_statuses.py +120 -0
  43. workstream/skills/__init__.py +0 -0
  44. workstream/skills/workstream_context/SKILL.md +75 -0
  45. workstream/skills/workstream_context/__init__.py +0 -0
  46. workstream/skills/workstream_focus/SKILL.md +141 -0
  47. workstream/skills/workstream_init/SKILL.md +86 -0
  48. workstream/skills/workstream_review/SKILL.md +224 -0
  49. workstream/skills/workstream_sweep/SKILL.md +178 -0
  50. workstream/sweep_state.py +93 -0
  51. workstream/templates/dashboard.html +382 -0
  52. workstream/templates/detail.html +360 -0
  53. workstream/templates/plan.html +210 -0
  54. workstream/test/__init__.py +0 -0
  55. workstream/test/conftest.py +221 -0
  56. workstream/test/fixtures/sample_sprint_note.md +10 -0
  57. workstream/test/fixtures/sample_workstream.md +41 -0
  58. workstream/test/test_backfill.py +180 -0
  59. workstream/test/test_batch_writeback.py +81 -0
  60. workstream/test/test_commands.py +938 -0
  61. workstream/test/test_config.py +54 -0
  62. workstream/test/test_focus_artifact.py +211 -0
  63. workstream/test/test_git.py +88 -0
  64. workstream/test/test_heuristics.py +136 -0
  65. workstream/test/test_hierarchy.py +231 -0
  66. workstream/test/test_init.py +452 -0
  67. workstream/test/test_inspect.py +143 -0
  68. workstream/test/test_llm.py +78 -0
  69. workstream/test/test_markdown.py +626 -0
  70. workstream/test/test_models.py +506 -0
  71. workstream/test/test_next.py +206 -0
  72. workstream/test/test_plan_index.py +83 -0
  73. workstream/test/test_provisioning.py +270 -0
  74. workstream/test/test_repo_discovery.py +181 -0
  75. workstream/test/test_resume.py +71 -0
  76. workstream/test/test_sweep.py +1196 -0
  77. workstream/test/test_sweep_state.py +86 -0
  78. workstream/test/test_thoughts.py +516 -0
  79. workstream/test/test_web.py +606 -0
  80. workstream/thoughts.py +505 -0
  81. workstream/web.py +444 -0
  82. workstream_cli-0.0.1.dist-info/LICENSE +21 -0
  83. workstream_cli-0.0.1.dist-info/METADATA +93 -0
  84. workstream_cli-0.0.1.dist-info/RECORD +86 -0
  85. workstream_cli-0.0.1.dist-info/WHEEL +4 -0
  86. 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