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,158 @@
|
|
|
1
|
+
"""Scan directories for git repos and build a markdown index of activity."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import date
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from workstream.git import branch_ahead_count, list_branches, recent_commits
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def discover_repos(repo_dirs: list[str]) -> list[Path]:
|
|
12
|
+
"""Find git repositories in the given directories.
|
|
13
|
+
|
|
14
|
+
For each directory, checks if the directory itself is a repo and scans
|
|
15
|
+
its immediate subdirectories for ``.git/`` directories. Silently skips
|
|
16
|
+
dirs that don't exist.
|
|
17
|
+
"""
|
|
18
|
+
repos: set[Path] = set()
|
|
19
|
+
for raw in repo_dirs:
|
|
20
|
+
parent = Path(raw).expanduser().resolve()
|
|
21
|
+
if not parent.is_dir():
|
|
22
|
+
continue
|
|
23
|
+
# Check the directory itself
|
|
24
|
+
if (parent / ".git").is_dir():
|
|
25
|
+
repos.add(parent)
|
|
26
|
+
# Check immediate children
|
|
27
|
+
try:
|
|
28
|
+
for child in parent.iterdir():
|
|
29
|
+
if child.is_dir() and (child / ".git").is_dir():
|
|
30
|
+
repos.add(child)
|
|
31
|
+
except PermissionError:
|
|
32
|
+
continue
|
|
33
|
+
return sorted(repos)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def find_duplicate_repo_names(repo_dirs: list[str]) -> dict[str, list[Path]]:
|
|
37
|
+
"""Find repo names that appear in multiple parent directories.
|
|
38
|
+
|
|
39
|
+
Returns {name: [path1, path2, ...]} for names with >1 occurrence.
|
|
40
|
+
"""
|
|
41
|
+
from collections import defaultdict
|
|
42
|
+
by_name: defaultdict[str, list[Path]] = defaultdict(list)
|
|
43
|
+
for repo_path in discover_repos(repo_dirs):
|
|
44
|
+
by_name[repo_path.name].append(repo_path)
|
|
45
|
+
return {name: paths for name, paths in by_name.items() if len(paths) > 1}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _default_branch(branches: list[str]) -> str | None:
|
|
49
|
+
"""Pick the default branch: main > master > first available."""
|
|
50
|
+
if not branches:
|
|
51
|
+
return None
|
|
52
|
+
if "main" in branches:
|
|
53
|
+
return "main"
|
|
54
|
+
if "master" in branches:
|
|
55
|
+
return "master"
|
|
56
|
+
return branches[0]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _repo_summary(repo: Path, cutoff: str) -> dict | None:
|
|
60
|
+
"""Gather activity data for a single repo. Returns None on failure."""
|
|
61
|
+
try:
|
|
62
|
+
branches = list_branches(repo)
|
|
63
|
+
except Exception:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
default = _default_branch(branches)
|
|
67
|
+
if default is None:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
default_commits = recent_commits(repo, cutoff, default)
|
|
72
|
+
except Exception:
|
|
73
|
+
default_commits = []
|
|
74
|
+
|
|
75
|
+
active_branches: list[dict] = []
|
|
76
|
+
for branch in branches:
|
|
77
|
+
if branch == default:
|
|
78
|
+
continue
|
|
79
|
+
try:
|
|
80
|
+
commits = recent_commits(repo, cutoff, branch)
|
|
81
|
+
except Exception:
|
|
82
|
+
continue
|
|
83
|
+
if not commits:
|
|
84
|
+
continue
|
|
85
|
+
try:
|
|
86
|
+
ahead = branch_ahead_count(repo, branch)
|
|
87
|
+
except Exception:
|
|
88
|
+
ahead = 0
|
|
89
|
+
latest = commits[0]
|
|
90
|
+
active_branches.append({
|
|
91
|
+
"name": branch,
|
|
92
|
+
"ahead": ahead,
|
|
93
|
+
"date": latest["date"],
|
|
94
|
+
"subject": latest["subject"],
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
last_default_date = default_commits[0]["date"] if default_commits else None
|
|
98
|
+
is_active = bool(default_commits or active_branches)
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
"name": repo.name,
|
|
102
|
+
"path": str(repo),
|
|
103
|
+
"default_branch": default,
|
|
104
|
+
"last_default_date": last_default_date,
|
|
105
|
+
"active_branches": active_branches,
|
|
106
|
+
"is_active": is_active,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def build_repo_index(repos: list[Path], cutoff_date: date) -> str:
|
|
111
|
+
"""Build a markdown summary of repo activity since *cutoff_date*.
|
|
112
|
+
|
|
113
|
+
Repos are split into active (any commits since cutoff) and stale.
|
|
114
|
+
Git errors on individual repos are caught and the repo is skipped.
|
|
115
|
+
"""
|
|
116
|
+
if not repos:
|
|
117
|
+
return ""
|
|
118
|
+
|
|
119
|
+
cutoff = cutoff_date.isoformat()
|
|
120
|
+
summaries: list[dict] = []
|
|
121
|
+
for repo in repos:
|
|
122
|
+
info = _repo_summary(repo, cutoff)
|
|
123
|
+
if info is not None:
|
|
124
|
+
summaries.append(info)
|
|
125
|
+
|
|
126
|
+
active = [s for s in summaries if s["is_active"]]
|
|
127
|
+
stale = [s for s in summaries if not s["is_active"]]
|
|
128
|
+
|
|
129
|
+
# Derive dir list from common parents for the header
|
|
130
|
+
parents = sorted({str(Path(s["path"]).parent) for s in summaries})
|
|
131
|
+
dir_list = ", ".join(parents) if parents else "unknown"
|
|
132
|
+
|
|
133
|
+
lines: list[str] = [
|
|
134
|
+
f"### Repo index ({len(summaries)} repos from {dir_list})",
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
if active:
|
|
138
|
+
lines.append("Active repos (commits within lookback period):")
|
|
139
|
+
lines.append("")
|
|
140
|
+
for info in active:
|
|
141
|
+
lines.append(f"#### {info['name']} ({info['path']})")
|
|
142
|
+
if info["last_default_date"]:
|
|
143
|
+
lines.append(f"Last default-branch commit: {info['last_default_date']}")
|
|
144
|
+
if info["active_branches"]:
|
|
145
|
+
lines.append("Branches with recent activity:")
|
|
146
|
+
for b in info["active_branches"]:
|
|
147
|
+
lines.append(
|
|
148
|
+
f"- {b['name']} (+{b['ahead']} ahead)"
|
|
149
|
+
f' — {b["date"]}: "{b["subject"]}"'
|
|
150
|
+
)
|
|
151
|
+
lines.append("")
|
|
152
|
+
|
|
153
|
+
if stale:
|
|
154
|
+
lines.append("Stale repos (no commits within lookback period):")
|
|
155
|
+
lines.append(f"- {', '.join(s['name'] for s in stale)}")
|
|
156
|
+
lines.append("")
|
|
157
|
+
|
|
158
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Read and write review artifacts at _Workstreams/reviews/YYYY-MM-DD.md.
|
|
2
|
+
|
|
3
|
+
Review artifacts record the output of each review session: focus items,
|
|
4
|
+
decisions made, and freeform notes. The frontmatter contains structured
|
|
5
|
+
data (date, type, focus list); the body is freeform markdown.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from datetime import date
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
from workstream.markdown import parse_frontmatter, write_frontmatter
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ── Data types ──────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ReviewArtifact:
|
|
21
|
+
"""A single review artifact parsed from disk."""
|
|
22
|
+
|
|
23
|
+
__slots__ = ('date', 'type', 'focus', 'body', 'source_path')
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
review_date: str = '',
|
|
29
|
+
review_type: str = 'review',
|
|
30
|
+
focus: list[str] | None = None,
|
|
31
|
+
body: str = '',
|
|
32
|
+
source_path: Path | None = None,
|
|
33
|
+
):
|
|
34
|
+
self.date = review_date or date.today().isoformat()
|
|
35
|
+
self.type = review_type
|
|
36
|
+
self.focus = focus or []
|
|
37
|
+
self.body = body
|
|
38
|
+
self.source_path = source_path
|
|
39
|
+
|
|
40
|
+
def frontmatter_dict(self) -> dict:
|
|
41
|
+
d: dict = {
|
|
42
|
+
'date': self.date,
|
|
43
|
+
'type': self.type,
|
|
44
|
+
}
|
|
45
|
+
if self.focus:
|
|
46
|
+
d['focus'] = self.focus
|
|
47
|
+
return d
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ── I/O ──────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def reviews_dir(ws_dir: Path) -> Path:
|
|
54
|
+
"""Return the reviews directory, creating it if needed."""
|
|
55
|
+
d = ws_dir / 'reviews'
|
|
56
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
return d
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def load_review(path: Path) -> ReviewArtifact:
|
|
61
|
+
"""Load a review artifact from a markdown file."""
|
|
62
|
+
text = path.read_text(encoding='utf-8')
|
|
63
|
+
meta, body = parse_frontmatter(text)
|
|
64
|
+
return ReviewArtifact(
|
|
65
|
+
review_date=str(meta.get('date', '')),
|
|
66
|
+
review_type=meta.get('type', 'review'),
|
|
67
|
+
focus=meta.get('focus', []),
|
|
68
|
+
body=body.strip(),
|
|
69
|
+
source_path=path,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def save_review(artifact: ReviewArtifact, ws_dir: Path) -> Path:
|
|
74
|
+
"""Write a review artifact to _Workstreams/reviews/YYYY-MM-DD.md.
|
|
75
|
+
|
|
76
|
+
Returns the path written.
|
|
77
|
+
"""
|
|
78
|
+
rdir = reviews_dir(ws_dir)
|
|
79
|
+
path = rdir / f'{artifact.date}.md'
|
|
80
|
+
meta = artifact.frontmatter_dict()
|
|
81
|
+
text = write_frontmatter(meta, artifact.body)
|
|
82
|
+
text = text.rstrip('\n') + '\n'
|
|
83
|
+
path.write_text(text, encoding='utf-8')
|
|
84
|
+
artifact.source_path = path
|
|
85
|
+
return path
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def load_latest_review(ws_dir: Path) -> ReviewArtifact | None:
|
|
89
|
+
"""Load the most recent review artifact, or None if none exist."""
|
|
90
|
+
rdir = ws_dir / 'reviews'
|
|
91
|
+
if not rdir.is_dir():
|
|
92
|
+
return None
|
|
93
|
+
files = sorted(rdir.glob('*.md'), reverse=True)
|
|
94
|
+
if not files:
|
|
95
|
+
return None
|
|
96
|
+
return load_review(files[0])
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""One-time migration: tabled→snoozed, idea→Ideas entries, frontmatter cleanup, seed log.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python3 src/workstream/scripts/migrate_statuses.py [--dry-run]
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from datetime import date
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
# Ensure src/ is importable
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
16
|
+
|
|
17
|
+
from workstream.config import load_config
|
|
18
|
+
from workstream.markdown import (
|
|
19
|
+
append_idea,
|
|
20
|
+
append_log_entry,
|
|
21
|
+
load_workstream,
|
|
22
|
+
parse_frontmatter,
|
|
23
|
+
write_frontmatter,
|
|
24
|
+
)
|
|
25
|
+
from workstream.models import IdeaEntry, LogEntry
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def migrate(dry_run: bool = False) -> None:
|
|
29
|
+
config = load_config()
|
|
30
|
+
ws_dir = config.workstreams_path
|
|
31
|
+
|
|
32
|
+
if not ws_dir.exists():
|
|
33
|
+
print(f'Workstreams directory not found: {ws_dir}')
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
today = date.today().isoformat()
|
|
37
|
+
files = sorted(ws_dir.glob('*.md'))
|
|
38
|
+
skip_files = {'index.md', 'inbox.md', 'dashboard.html'}
|
|
39
|
+
|
|
40
|
+
migrated = 0
|
|
41
|
+
ideas_migrated = 0
|
|
42
|
+
logs_seeded = 0
|
|
43
|
+
|
|
44
|
+
for f in files:
|
|
45
|
+
if f.name in skip_files:
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
text = f.read_text(encoding='utf-8')
|
|
49
|
+
meta, body = parse_frontmatter(text)
|
|
50
|
+
if not meta:
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
changed = False
|
|
54
|
+
status = meta.get('status', '')
|
|
55
|
+
|
|
56
|
+
# 1. tabled → snoozed
|
|
57
|
+
if status == 'tabled':
|
|
58
|
+
if dry_run:
|
|
59
|
+
print(f' [DRY] Would change {f.name}: tabled → snoozed')
|
|
60
|
+
meta['status'] = 'snoozed'
|
|
61
|
+
tabled_note = meta.pop('tabled_note', '')
|
|
62
|
+
if tabled_note:
|
|
63
|
+
meta['snooze_reason'] = tabled_note
|
|
64
|
+
changed = True
|
|
65
|
+
migrated += 1
|
|
66
|
+
|
|
67
|
+
# 2. idea workstreams → for now just change to active
|
|
68
|
+
# (In full deployment, these would become Ideas entries on parents)
|
|
69
|
+
elif status == 'idea':
|
|
70
|
+
if dry_run:
|
|
71
|
+
print(f' [DRY] Would change {f.name}: idea → active')
|
|
72
|
+
meta['status'] = 'active'
|
|
73
|
+
changed = True
|
|
74
|
+
migrated += 1
|
|
75
|
+
|
|
76
|
+
# 3. Frontmatter cleanup: remove tabled_note if present
|
|
77
|
+
if 'tabled_note' in meta:
|
|
78
|
+
old_note = meta.pop('tabled_note')
|
|
79
|
+
if old_note and 'snooze_reason' not in meta:
|
|
80
|
+
meta['snooze_reason'] = old_note
|
|
81
|
+
changed = True
|
|
82
|
+
|
|
83
|
+
# 4. Seed initial log entry if no ## Log section exists
|
|
84
|
+
from workstream.markdown import parse_log as _parse_log
|
|
85
|
+
if not _parse_log(body):
|
|
86
|
+
created_date = str(meta.get('created', today))
|
|
87
|
+
log_detail = f"({meta.get('status', 'active')}, {meta.get('size', 'day')})"
|
|
88
|
+
entry = LogEntry(date=created_date, event='created', detail=log_detail)
|
|
89
|
+
body = append_log_entry(body, entry)
|
|
90
|
+
|
|
91
|
+
# If currently snoozed (either from migration or already), add snoozed log
|
|
92
|
+
if meta.get('status') == 'snoozed':
|
|
93
|
+
reason = meta.get('snooze_reason', '')
|
|
94
|
+
until = meta.get('snooze_until', '')
|
|
95
|
+
if until:
|
|
96
|
+
detail = f'until {until}: {reason}' if reason else f'until {until}'
|
|
97
|
+
else:
|
|
98
|
+
detail = f'indefinitely: {reason}' if reason else 'indefinitely'
|
|
99
|
+
body = append_log_entry(body, LogEntry(date=today, event='snoozed', detail=detail))
|
|
100
|
+
|
|
101
|
+
changed = True
|
|
102
|
+
logs_seeded += 1
|
|
103
|
+
if dry_run:
|
|
104
|
+
print(f' [DRY] Would seed log entry for {f.name}')
|
|
105
|
+
|
|
106
|
+
if changed and not dry_run:
|
|
107
|
+
out = write_frontmatter(meta, body)
|
|
108
|
+
out = out.rstrip('\n') + '\n'
|
|
109
|
+
f.write_text(out, encoding='utf-8')
|
|
110
|
+
print(f' Migrated: {f.name}')
|
|
111
|
+
|
|
112
|
+
print(f'\nMigration {"(dry run) " if dry_run else ""}complete:')
|
|
113
|
+
print(f' {migrated} status changes')
|
|
114
|
+
print(f' {ideas_migrated} ideas migrated')
|
|
115
|
+
print(f' {logs_seeded} log entries seeded')
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == '__main__':
|
|
119
|
+
dry = '--dry-run' in sys.argv
|
|
120
|
+
migrate(dry_run=dry)
|
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workstream-context
|
|
3
|
+
description: General workstream context — command reference, file structure, offline instructions
|
|
4
|
+
author: Mahmoud Hashemi
|
|
5
|
+
version: 2026-04-02.1
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Workstream Context
|
|
9
|
+
|
|
10
|
+
Workstream is an attention management system for trains of thought that span projects and time. It is not project management. A workstream is a persistent train of thought — a problem, initiative, or area of concern that you return to across sessions, days, and weeks.
|
|
11
|
+
|
|
12
|
+
## File Structure
|
|
13
|
+
|
|
14
|
+
All workstream data lives under a `_Workstreams/` directory:
|
|
15
|
+
|
|
16
|
+
| Path | Purpose |
|
|
17
|
+
|------|---------|
|
|
18
|
+
| `_Workstreams/*.md` | One markdown file per workstream |
|
|
19
|
+
| `_Workstreams/reviews/` | Review artifacts, named `YYYY-MM-DD.md` |
|
|
20
|
+
| `_Workstreams/focus/` | Focus artifacts (weekly priorities) |
|
|
21
|
+
| `_Workstreams/inbox.md` | Unmatched thoughts not yet assigned to a workstream |
|
|
22
|
+
| `config.yaml` | Configuration in `~/.config/workstream/` or `_Workstreams/config.yaml` |
|
|
23
|
+
|
|
24
|
+
## Workstream File Frontmatter
|
|
25
|
+
|
|
26
|
+
Each workstream markdown file has YAML frontmatter with these fields:
|
|
27
|
+
|
|
28
|
+
| Field | Description |
|
|
29
|
+
|-------|-------------|
|
|
30
|
+
| `id` | Unique identifier (slug) |
|
|
31
|
+
| `title` | Human-readable title |
|
|
32
|
+
| `status` | One of: `active`, `snoozed`, `blocked`, `completed`, `dropped` |
|
|
33
|
+
| `size` | Effort estimate: `hour`, `day`, `week`, or `open` |
|
|
34
|
+
| `tags` | List of categorization tags |
|
|
35
|
+
| `repos` | Associated repository paths |
|
|
36
|
+
| `parent` | Parent workstream id (for nesting) |
|
|
37
|
+
| `summary` | Brief description of current state |
|
|
38
|
+
|
|
39
|
+
## Command Reference
|
|
40
|
+
|
|
41
|
+
| Command | Description |
|
|
42
|
+
|---------|-------------|
|
|
43
|
+
| `ws list [--active\|--all\|--search\|--paths]` | List workstreams |
|
|
44
|
+
| `ws show <id>` | Detail view of a single workstream |
|
|
45
|
+
| `ws next [--quick\|--tag\|--size\|--focus]` | Suggest what to work on next |
|
|
46
|
+
| `ws new <title>` | Create a new workstream |
|
|
47
|
+
| `ws stale [--days N]` | Find neglected workstreams |
|
|
48
|
+
| `ws snooze <id> [reason]` | Defer a workstream |
|
|
49
|
+
| `ws wake <id>` | Resume a snoozed workstream |
|
|
50
|
+
| `ws block <id> <reason>` | Mark a workstream as blocked on an external dependency |
|
|
51
|
+
| `ws unblock <id>` | Clear a blocked status |
|
|
52
|
+
| `ws update-status <id> <status> [reason]` | Transition workstream status |
|
|
53
|
+
| `ws idea <parent-id> <text>` | Add an idea to a workstream |
|
|
54
|
+
| `ws ideas` | List ideas across workstreams |
|
|
55
|
+
| `ws promote <parent> <idx>` | Promote an idea to a workstream |
|
|
56
|
+
| `ws checkin` | Daily check-in across active workstreams |
|
|
57
|
+
| `ws sweep [--discover]` | Scan repos for plan/branch updates |
|
|
58
|
+
| `ws review` | Structured multi-workstream review session |
|
|
59
|
+
| `ws focus` | Set weekly priorities |
|
|
60
|
+
| `ws report [--since Nd]` | Activity summary over a time range |
|
|
61
|
+
| `ws index` | Regenerate the workstream index |
|
|
62
|
+
| `ws tree` | Display workstream hierarchy |
|
|
63
|
+
| `ws nest <child> <parent>` | Place a workstream under a parent |
|
|
64
|
+
| `ws unnest <id>` | Remove a workstream from its parent |
|
|
65
|
+
| `ws resume <id>` | Context resumption briefing for a workstream |
|
|
66
|
+
|
|
67
|
+
## If ws Is Not Installed
|
|
68
|
+
|
|
69
|
+
You can work with workstreams directly through the filesystem:
|
|
70
|
+
|
|
71
|
+
- Workstream files live in `_Workstreams/` as plain markdown.
|
|
72
|
+
- Status is stored in YAML frontmatter at the top of each file.
|
|
73
|
+
- The `## Next` section contains pending actions for the workstream.
|
|
74
|
+
- The `## Thread` section contains dated journal entries tracking progress.
|
|
75
|
+
- To update a workstream, edit the file directly: change frontmatter fields, add entries to the thread, or update the next-actions list.
|
|
File without changes
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workstream-focus
|
|
3
|
+
description: Prospective focus session — set priorities and identify blocked/agent-actionable items
|
|
4
|
+
author: Mahmoud Hashemi
|
|
5
|
+
version: 2026-04-02.1
|
|
6
|
+
date: 2026-04-01
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Workstream Focus
|
|
10
|
+
|
|
11
|
+
You are a prospective planning partner helping set focus and priorities for workstreams. Unlike review (retrospective cleanup), focus is forward-looking: what matters this week, what's blocked, what can be delegated to an agent.
|
|
12
|
+
|
|
13
|
+
## Context
|
|
14
|
+
|
|
15
|
+
The user manages multiple workstreams (trains of thought spanning projects and time) tracked as markdown files in a `_Workstreams/` directory. Focus sessions produce focus docs that feed `ws next` with structured priorities. Focus docs live at `_Workstreams/focus/YYYY-MM-DD.md`.
|
|
16
|
+
|
|
17
|
+
**Review vs Focus:** Review is retrospective — what happened, what to snooze/drop/wake. Focus is prospective — what matters now, what's blocked, what can be delegated. Review looks back to clean up. Focus looks forward to commit.
|
|
18
|
+
|
|
19
|
+
## Iteration vs New Mode
|
|
20
|
+
|
|
21
|
+
A focus session operates in one of two modes, determined by whether a current focus doc exists.
|
|
22
|
+
|
|
23
|
+
**Iteration mode** (existing focus doc this week): Shorter session. The existing focus doc is loaded as context. Surface what shifted since it was written. Ask what changed — new blockers, resolved items, priority shifts. Quick update, not a full walkthrough.
|
|
24
|
+
|
|
25
|
+
**New mode** (no focus doc this week, or `--new` flag): Full walkthrough from scratch. If a previous focus doc exists (from last week), it's offered for continuity context — check which priorities got attention, which carried over, which evaporated.
|
|
26
|
+
|
|
27
|
+
## Focus Session Protocol
|
|
28
|
+
|
|
29
|
+
When the focus manifest is present, follow this protocol step by step.
|
|
30
|
+
|
|
31
|
+
### 1. Orient
|
|
32
|
+
|
|
33
|
+
Surface current state from the manifest:
|
|
34
|
+
|
|
35
|
+
- Active count, blocked count, stale count, expiring snoozes
|
|
36
|
+
- If iterating: load existing focus doc, note what's changed since it was written
|
|
37
|
+
- If previous focus exists (from last week): check which priorities got attention and which didn't
|
|
38
|
+
|
|
39
|
+
> "You have N active workstreams, N blocked, N stale. Your last focus set priorities X, Y, Z — here's what moved."
|
|
40
|
+
|
|
41
|
+
### 2. What's top of mind?
|
|
42
|
+
|
|
43
|
+
Open-ended intake. Let the user lead:
|
|
44
|
+
|
|
45
|
+
- "What's most important this week?"
|
|
46
|
+
- "Any deadlines or external pressures?"
|
|
47
|
+
- "Anything keeping you up at night?"
|
|
48
|
+
|
|
49
|
+
Don't rush this. The answer often reveals priorities the workstream list doesn't capture.
|
|
50
|
+
|
|
51
|
+
### 3. Map to workstreams
|
|
52
|
+
|
|
53
|
+
Connect stated priorities to the workstream graph:
|
|
54
|
+
|
|
55
|
+
- Associate each stated priority with an existing workstream
|
|
56
|
+
- If something doesn't match an existing workstream, offer to create one with `ws new`
|
|
57
|
+
- Rank by impact: what has the highest positive impact if done? What has the highest negative impact if dropped?
|
|
58
|
+
- Aim for 2–3 priorities. More than 5 is not focus — it's a to-do list.
|
|
59
|
+
- Workstreams with no next actions and all plans terminal (implemented/obsolete) may be completion candidates — suggest `ws update-status <id> completed` after capturing follow-up ideas.
|
|
60
|
+
|
|
61
|
+
### 4. Identify blocked
|
|
62
|
+
|
|
63
|
+
Surface workstreams that are stuck:
|
|
64
|
+
|
|
65
|
+
- Which workstreams are waiting on something external?
|
|
66
|
+
- What specifically is blocking them?
|
|
67
|
+
- Offer to set blocked status: `ws block <id> <reason>`
|
|
68
|
+
- Distinguish clearly: is this truly blocked (external dependency, waiting on another person, missing access) or just deprioritized? Deprioritized items are not blocked — they're choices.
|
|
69
|
+
|
|
70
|
+
### 5. Identify agent-actionable
|
|
71
|
+
|
|
72
|
+
Surface workstreams an agent could advance without the user:
|
|
73
|
+
|
|
74
|
+
- No plans → obvious first action is a `/plan` session
|
|
75
|
+
- Clear `next_actions` with no ambiguity → could potentially delegate
|
|
76
|
+
- Research tasks, boilerplate, mechanical refactors → good candidates
|
|
77
|
+
- Infer from workstream state, but confirm with the user — only they know the full context
|
|
78
|
+
|
|
79
|
+
### 6. Capture
|
|
80
|
+
|
|
81
|
+
Write or update the focus doc at `_Workstreams/focus/YYYY-MM-DD.md`.
|
|
82
|
+
|
|
83
|
+
Frontmatter structure:
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
---
|
|
87
|
+
date: YYYY-MM-DD
|
|
88
|
+
type: focus
|
|
89
|
+
expires: YYYY-MM-DD # end of calendar week
|
|
90
|
+
supersedes: YYYY-MM-DD # optional, date of previous focus doc
|
|
91
|
+
priorities:
|
|
92
|
+
- id: <ws-id>
|
|
93
|
+
reason: "Why this matters"
|
|
94
|
+
- id: <ws-id>
|
|
95
|
+
reason: "Why this matters"
|
|
96
|
+
blocked:
|
|
97
|
+
- id: <ws-id>
|
|
98
|
+
on: "What's blocking it"
|
|
99
|
+
agent_actionable:
|
|
100
|
+
- id: <ws-id>
|
|
101
|
+
action: "What an agent could do"
|
|
102
|
+
context: "External context (deadlines, events, etc.)"
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Focus
|
|
106
|
+
|
|
107
|
+
<Summary of decisions and rationale from the conversation>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
If iterating on an existing doc, preserve decisions that haven't changed. Update what shifted. Note in the body what changed and why.
|
|
111
|
+
|
|
112
|
+
## Available Tools
|
|
113
|
+
|
|
114
|
+
- `ws list` — see all workstreams (use --active, --all, --tag, --search, --paths flags)
|
|
115
|
+
- `ws list --search <substring>` — filter workstreams by title/repo/tag substring (implies --all)
|
|
116
|
+
- `ws list --paths` — include workstream file path and repo directory paths in output
|
|
117
|
+
- `ws show <id>` — deep-dive into a specific workstream
|
|
118
|
+
- `ws next [--tag X] [--size X]` — suggest what to work on next
|
|
119
|
+
- `ws stale [--days N]` — find neglected workstreams
|
|
120
|
+
- `ws new <title>` — create a new workstream (defaults to active)
|
|
121
|
+
- `ws snooze <id> [reason]` — set a workstream aside with optional reason
|
|
122
|
+
- `ws wake <id>` — bring a snoozed workstream back to active
|
|
123
|
+
- `ws block <id> <reason>` — mark a workstream as blocked with reason
|
|
124
|
+
- `ws unblock <id>` — remove blocked status from a workstream
|
|
125
|
+
- `ws focus` — start or iterate on a focus session
|
|
126
|
+
- `ws idea <parent-id> <text>` — capture an idea on a parent workstream
|
|
127
|
+
- `ws checkin` — quick daily check-in
|
|
128
|
+
- `ws sweep` — scan repos for plan/branch updates
|
|
129
|
+
- `ws report [--since Nd]` — summarize recent activity
|
|
130
|
+
- `ws tree` — show workstream hierarchy as an ASCII tree
|
|
131
|
+
- `ws update-status <id> <status> [reason]` — transition to any status (completing, dropping, reopening)
|
|
132
|
+
|
|
133
|
+
## Principles
|
|
134
|
+
|
|
135
|
+
- This is about choosing where to spend time, not completing everything.
|
|
136
|
+
- Fewer priorities = better. 2–3 is ideal. More than 5 is not focus.
|
|
137
|
+
- Be direct about what's being neglected. Choosing means not-choosing.
|
|
138
|
+
- Respect the user's autonomy — suggest, don't dictate.
|
|
139
|
+
- Blocked means truly stuck on an external dependency, not just deprioritized.
|
|
140
|
+
- Agent-actionable is contextual — infer from workstream state, confirm with the user.
|
|
141
|
+
- A focus doc that doesn't change behavior is theater. Push for concrete commitments.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workstream-init
|
|
3
|
+
description: Interactive workstream discovery from sprint notes and repos — cold-start bootstrapping
|
|
4
|
+
author: Mahmoud Hashemi
|
|
5
|
+
version: 2026-04-02.1
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Workstream Init
|
|
9
|
+
|
|
10
|
+
You are helping the user discover and create workstreams from their existing sprint notes.
|
|
11
|
+
This is a bootstrapping conversation — the user may have zero or few workstreams and wants to
|
|
12
|
+
populate their workstream tree from recent notes.
|
|
13
|
+
|
|
14
|
+
## Context
|
|
15
|
+
|
|
16
|
+
The user's system prompt contains a **Bootstrap Context** section with:
|
|
17
|
+
- Today's date and the lookback period
|
|
18
|
+
- A list of recent sprint files (with dates and sizes)
|
|
19
|
+
- A **repo index** showing git repos, branches, and recent commits (if repo dirs are configured)
|
|
20
|
+
- The current workstream tree (may be empty)
|
|
21
|
+
|
|
22
|
+
The sprint files are in the notes directory and you can read them with your file tools.
|
|
23
|
+
|
|
24
|
+
### Repo Context
|
|
25
|
+
|
|
26
|
+
If the manifest includes a repo index, use it as a primary signal for workstream discovery:
|
|
27
|
+
- **Active repos are strong signals for top-level workstreams.** A repo with recent commits likely represents ongoing work.
|
|
28
|
+
- **Branches represent sub-workstreams or tactical work.** A feature branch like `feature/onboarding-v2` may be a sub-workstream under the repo's parent workstream.
|
|
29
|
+
- **Some repos may be worktrees, forks, or draft clones of the same project** — ask before creating duplicate workstreams.
|
|
30
|
+
- **Not all workstreams have repos.** Non-code workstreams (fundraising, hiring, community) come from note themes only.
|
|
31
|
+
- **Cross-reference notes with repos.** A theme from sprint notes + an active repo = high-confidence workstream.
|
|
32
|
+
- **Stale repos** (no recent commits) may still be relevant if they appear in notes. Ask the user.
|
|
33
|
+
|
|
34
|
+
## Process
|
|
35
|
+
|
|
36
|
+
### Pass 1: Interactive Theme Discovery
|
|
37
|
+
|
|
38
|
+
1. **Read the newest 2-3 sprint files** to get a sense of what the user has been working on
|
|
39
|
+
2. **Identify recurring themes**: projects, products, investigations, ideas, stalled work. Cross-reference with the repo index if available — active repos are strong theme signals.
|
|
40
|
+
3. **Propose workstreams** in batches of 5-7 using the `ask` tool:
|
|
41
|
+
- The ask tool has a short input timeout — large lists will auto-select before the user finishes reading.
|
|
42
|
+
- Group themes by area (e.g., "Product workstreams", then "FOSS/side projects", then "Business/ops").
|
|
43
|
+
- Each batch: multi-select list. Include a note in the question: "You can add any workstreams not listed here right after selection."
|
|
44
|
+
4. After the selection batches, ask: "Any workstreams I missed? Describe them and I'll create them." (free-text input, not multi-select)
|
|
45
|
+
5. **Ask the user how they want to configure workstreams:**
|
|
46
|
+
- "Want to go through each workstream one at a time, or configure them all in one batch?"
|
|
47
|
+
- **One at a time (default):** For each approved theme, ask individually:
|
|
48
|
+
- Priority: **active** (working on it now), **snooze for next week** (not now, revisit soon), **snooze for next month** (revisit later)
|
|
49
|
+
- Size estimate: hour, day, week, open
|
|
50
|
+
- Tags from: #code, #business, #fundraising, #foss, #community
|
|
51
|
+
- Parent workstream (if nesting makes sense): select from existing or "top-level"
|
|
52
|
+
- **Batch:** Present a single combined question listing all workstreams with status options. Faster, but the user sees everything at once.
|
|
53
|
+
6. **Create each workstream** — flags go OUTSIDE the quotes:
|
|
54
|
+
- `ws new "FinFam Product" --size week --tag code --tag business`
|
|
55
|
+
- The title is ONLY the name in quotes. Do NOT include --flags inside the quotes.
|
|
56
|
+
- For "snooze for next week" items: also run `ws snooze <id> next-week` after creating
|
|
57
|
+
- For "snooze for next month" items: also run `ws snooze <id> next-month` after creating
|
|
58
|
+
7. **Nest where appropriate**: `ws nest <child-id> <parent-id>`
|
|
59
|
+
8. **Continue reading** older files if the user wants to go deeper. Propose new workstreams as themes emerge from older notes.
|
|
60
|
+
|
|
61
|
+
### Pass 2: Detailed Thought Extraction
|
|
62
|
+
|
|
63
|
+
9. Once the user is satisfied with the workstream tree, run `ws sweep --discover` to do detailed thought-by-thought extraction. **Use a 300-second timeout** for this command — it processes files one at a time and saves progress after each, so it's safe to retry if interrupted. Run with `--interactive` for month-by-month review.
|
|
64
|
+
10. After sweep completes, check if `_Workstreams/inbox.md` has unmatched thoughts.
|
|
65
|
+
11. For inbox items: read the inbox, and for each thought ask whether it fits one of the new workstreams or should stay in inbox for later triage.
|
|
66
|
+
|
|
67
|
+
### Wrap-up
|
|
68
|
+
|
|
69
|
+
12. Run `ws tree` to show the final hierarchy
|
|
70
|
+
13. Summarize what was created: N workstreams, their statuses, and the nesting structure
|
|
71
|
+
14. Ask: "Want to continue exploring, or are we done?"
|
|
72
|
+
15. If done, tell the user:
|
|
73
|
+
- The **workstream-review** skill is available for ongoing management
|
|
74
|
+
- They can run `ws sweep --discover` anytime to process new sprint notes
|
|
75
|
+
- Use `ws list`, `ws next`, `ws stale` for day-to-day prioritization
|
|
76
|
+
- Exit this session with Ctrl+C or /exit
|
|
77
|
+
|
|
78
|
+
## Guidelines
|
|
79
|
+
|
|
80
|
+
- **Ask before creating** — don't batch-create without confirmation
|
|
81
|
+
- **Prefer broader workstreams** over many narrow ones (5-15 is typical for 60 days)
|
|
82
|
+
- **Ideas are cheap** — encourage capturing them with `ws idea <parent-id> <text>` on relevant workstreams. If a standalone workstream isn't active now, snooze it rather than skipping it.
|
|
83
|
+
- **Keep `ask` batches small** (5-7 options max) — the tool has a short input timeout and large lists get auto-selected before the user can respond
|
|
84
|
+
- **Work newest-first** — recent work is freshest in the user's mind
|
|
85
|
+
- **Don't read every file** — 2-3 files at a time, propose themes, then go deeper if the user wants
|
|
86
|
+
- **Respect the user's time** — if they want to stop early, that's fine. Progress is saved.
|