workstream-cli 0.0.1__tar.gz
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_cli-0.0.1/LICENSE +21 -0
- workstream_cli-0.0.1/PKG-INFO +93 -0
- workstream_cli-0.0.1/README.md +61 -0
- workstream_cli-0.0.1/pyproject.toml +63 -0
- workstream_cli-0.0.1/src/workstream/ARCHITECTURE.md +89 -0
- workstream_cli-0.0.1/src/workstream/__init__.py +8 -0
- workstream_cli-0.0.1/src/workstream/cli.py +136 -0
- workstream_cli-0.0.1/src/workstream/commands/__init__.py +0 -0
- workstream_cli-0.0.1/src/workstream/commands/backfill.py +139 -0
- workstream_cli-0.0.1/src/workstream/commands/block.py +93 -0
- workstream_cli-0.0.1/src/workstream/commands/checkin.py +51 -0
- workstream_cli-0.0.1/src/workstream/commands/cron.py +119 -0
- workstream_cli-0.0.1/src/workstream/commands/focus_cmd.py +273 -0
- workstream_cli-0.0.1/src/workstream/commands/idea.py +172 -0
- workstream_cli-0.0.1/src/workstream/commands/index.py +89 -0
- workstream_cli-0.0.1/src/workstream/commands/init.py +567 -0
- workstream_cli-0.0.1/src/workstream/commands/inspect_cmd.py +354 -0
- workstream_cli-0.0.1/src/workstream/commands/list_cmd.py +99 -0
- workstream_cli-0.0.1/src/workstream/commands/nest.py +108 -0
- workstream_cli-0.0.1/src/workstream/commands/new.py +95 -0
- workstream_cli-0.0.1/src/workstream/commands/next_cmd.py +333 -0
- workstream_cli-0.0.1/src/workstream/commands/report.py +190 -0
- workstream_cli-0.0.1/src/workstream/commands/resume.py +145 -0
- workstream_cli-0.0.1/src/workstream/commands/review.py +227 -0
- workstream_cli-0.0.1/src/workstream/commands/serve.py +23 -0
- workstream_cli-0.0.1/src/workstream/commands/setup.py +178 -0
- workstream_cli-0.0.1/src/workstream/commands/show.py +123 -0
- workstream_cli-0.0.1/src/workstream/commands/snooze.py +117 -0
- workstream_cli-0.0.1/src/workstream/commands/stale.py +116 -0
- workstream_cli-0.0.1/src/workstream/commands/sweep.py +1753 -0
- workstream_cli-0.0.1/src/workstream/commands/tree.py +105 -0
- workstream_cli-0.0.1/src/workstream/commands/update_status.py +117 -0
- workstream_cli-0.0.1/src/workstream/config.py +322 -0
- workstream_cli-0.0.1/src/workstream/extensions/__init__.py +0 -0
- workstream_cli-0.0.1/src/workstream/extensions/workstream.ts +633 -0
- workstream_cli-0.0.1/src/workstream/focus_artifact.py +157 -0
- workstream_cli-0.0.1/src/workstream/git.py +194 -0
- workstream_cli-0.0.1/src/workstream/harness.py +49 -0
- workstream_cli-0.0.1/src/workstream/llm.py +78 -0
- workstream_cli-0.0.1/src/workstream/markdown.py +501 -0
- workstream_cli-0.0.1/src/workstream/models.py +274 -0
- workstream_cli-0.0.1/src/workstream/plan_index.py +88 -0
- workstream_cli-0.0.1/src/workstream/provisioning.py +196 -0
- workstream_cli-0.0.1/src/workstream/repo_discovery.py +158 -0
- workstream_cli-0.0.1/src/workstream/review_artifact.py +96 -0
- workstream_cli-0.0.1/src/workstream/scripts/migrate_statuses.py +120 -0
- workstream_cli-0.0.1/src/workstream/skills/__init__.py +0 -0
- workstream_cli-0.0.1/src/workstream/skills/workstream_context/SKILL.md +75 -0
- workstream_cli-0.0.1/src/workstream/skills/workstream_context/__init__.py +0 -0
- workstream_cli-0.0.1/src/workstream/skills/workstream_focus/SKILL.md +141 -0
- workstream_cli-0.0.1/src/workstream/skills/workstream_init/SKILL.md +86 -0
- workstream_cli-0.0.1/src/workstream/skills/workstream_review/SKILL.md +224 -0
- workstream_cli-0.0.1/src/workstream/skills/workstream_sweep/SKILL.md +178 -0
- workstream_cli-0.0.1/src/workstream/sweep_state.py +93 -0
- workstream_cli-0.0.1/src/workstream/templates/dashboard.html +382 -0
- workstream_cli-0.0.1/src/workstream/templates/detail.html +360 -0
- workstream_cli-0.0.1/src/workstream/templates/plan.html +210 -0
- workstream_cli-0.0.1/src/workstream/test/__init__.py +0 -0
- workstream_cli-0.0.1/src/workstream/test/conftest.py +221 -0
- workstream_cli-0.0.1/src/workstream/test/fixtures/sample_sprint_note.md +10 -0
- workstream_cli-0.0.1/src/workstream/test/fixtures/sample_workstream.md +41 -0
- workstream_cli-0.0.1/src/workstream/test/test_backfill.py +180 -0
- workstream_cli-0.0.1/src/workstream/test/test_batch_writeback.py +81 -0
- workstream_cli-0.0.1/src/workstream/test/test_commands.py +938 -0
- workstream_cli-0.0.1/src/workstream/test/test_config.py +54 -0
- workstream_cli-0.0.1/src/workstream/test/test_focus_artifact.py +211 -0
- workstream_cli-0.0.1/src/workstream/test/test_git.py +88 -0
- workstream_cli-0.0.1/src/workstream/test/test_heuristics.py +136 -0
- workstream_cli-0.0.1/src/workstream/test/test_hierarchy.py +231 -0
- workstream_cli-0.0.1/src/workstream/test/test_init.py +452 -0
- workstream_cli-0.0.1/src/workstream/test/test_inspect.py +143 -0
- workstream_cli-0.0.1/src/workstream/test/test_llm.py +78 -0
- workstream_cli-0.0.1/src/workstream/test/test_markdown.py +626 -0
- workstream_cli-0.0.1/src/workstream/test/test_models.py +506 -0
- workstream_cli-0.0.1/src/workstream/test/test_next.py +206 -0
- workstream_cli-0.0.1/src/workstream/test/test_plan_index.py +83 -0
- workstream_cli-0.0.1/src/workstream/test/test_provisioning.py +270 -0
- workstream_cli-0.0.1/src/workstream/test/test_repo_discovery.py +181 -0
- workstream_cli-0.0.1/src/workstream/test/test_resume.py +71 -0
- workstream_cli-0.0.1/src/workstream/test/test_sweep.py +1196 -0
- workstream_cli-0.0.1/src/workstream/test/test_sweep_state.py +86 -0
- workstream_cli-0.0.1/src/workstream/test/test_thoughts.py +516 -0
- workstream_cli-0.0.1/src/workstream/test/test_web.py +606 -0
- workstream_cli-0.0.1/src/workstream/thoughts.py +505 -0
- workstream_cli-0.0.1/src/workstream/web.py +444 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mahmoud Hashemi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: workstream-cli
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Attention management for makers. Track trains of thought across projects.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: workstream,productivity,cli,attention-management,task-management
|
|
7
|
+
Author: Mahmoud Hashemi
|
|
8
|
+
Author-email: mahmoud@hatnote.com
|
|
9
|
+
Requires-Python: >=3.10,<4.0
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Topic :: Office/Business :: Scheduling
|
|
22
|
+
Classifier: Topic :: Software Development
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Dist: ashes (>=24.0.0)
|
|
25
|
+
Requires-Dist: clastic (>=24.0.0)
|
|
26
|
+
Requires-Dist: face (>=20.0.0)
|
|
27
|
+
Requires-Dist: pyyaml (>=6.0,<7.0)
|
|
28
|
+
Project-URL: Homepage, https://github.com/mahmoud/workstream
|
|
29
|
+
Project-URL: Repository, https://github.com/mahmoud/workstream
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# workstream
|
|
33
|
+
|
|
34
|
+
Attention management for makers. Track trains of thought across projects.
|
|
35
|
+
|
|
36
|
+
`ws` is a CLI tool for managing workstreams -- named trains of thought that
|
|
37
|
+
span code repos, non-code activities, and time. Obsidian-native markdown files
|
|
38
|
+
with YAML frontmatter, designed to be legible to both humans and LLMs.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pipx install workstream-cli # recommended
|
|
44
|
+
# or
|
|
45
|
+
pip install workstream-cli
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For development:
|
|
49
|
+
```bash
|
|
50
|
+
pip install -e .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
ws init # first-time setup
|
|
57
|
+
ws new "API Redesign" # create a workstream
|
|
58
|
+
ws list # see all workstreams
|
|
59
|
+
ws next --tag code --size day # what should I work on?
|
|
60
|
+
ws stale # what's gone cold?
|
|
61
|
+
ws sweep # scan repos for plan/branch updates
|
|
62
|
+
ws report --since 7d # summarize recent work
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Development
|
|
66
|
+
|
|
67
|
+
### Running Tests
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pytest # quick, no coverage
|
|
71
|
+
pytest --cov=workstream # with coverage report
|
|
72
|
+
tox # full matrix + combined coverage report
|
|
73
|
+
tox -e py311 # single Python version
|
|
74
|
+
tox -e py311 -- -k test_models # single env, filtered tests
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Coverage Report
|
|
78
|
+
|
|
79
|
+
After running `tox` (which ends with `coverage-report`):
|
|
80
|
+
- Terminal summary prints automatically
|
|
81
|
+
- HTML report: open `htmlcov/index.html`
|
|
82
|
+
|
|
83
|
+
For a quick local coverage check without tox:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pytest --cov=workstream --cov-report=term-missing --cov-report=html
|
|
87
|
+
open htmlcov/index.html
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
|
93
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# workstream
|
|
2
|
+
|
|
3
|
+
Attention management for makers. Track trains of thought across projects.
|
|
4
|
+
|
|
5
|
+
`ws` is a CLI tool for managing workstreams -- named trains of thought that
|
|
6
|
+
span code repos, non-code activities, and time. Obsidian-native markdown files
|
|
7
|
+
with YAML frontmatter, designed to be legible to both humans and LLMs.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pipx install workstream-cli # recommended
|
|
13
|
+
# or
|
|
14
|
+
pip install workstream-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
For development:
|
|
18
|
+
```bash
|
|
19
|
+
pip install -e .
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
ws init # first-time setup
|
|
26
|
+
ws new "API Redesign" # create a workstream
|
|
27
|
+
ws list # see all workstreams
|
|
28
|
+
ws next --tag code --size day # what should I work on?
|
|
29
|
+
ws stale # what's gone cold?
|
|
30
|
+
ws sweep # scan repos for plan/branch updates
|
|
31
|
+
ws report --since 7d # summarize recent work
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Development
|
|
35
|
+
|
|
36
|
+
### Running Tests
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pytest # quick, no coverage
|
|
40
|
+
pytest --cov=workstream # with coverage report
|
|
41
|
+
tox # full matrix + combined coverage report
|
|
42
|
+
tox -e py311 # single Python version
|
|
43
|
+
tox -e py311 -- -k test_models # single env, filtered tests
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Coverage Report
|
|
47
|
+
|
|
48
|
+
After running `tox` (which ends with `coverage-report`):
|
|
49
|
+
- Terminal summary prints automatically
|
|
50
|
+
- HTML report: open `htmlcov/index.html`
|
|
51
|
+
|
|
52
|
+
For a quick local coverage check without tox:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pytest --cov=workstream --cov-report=term-missing --cov-report=html
|
|
56
|
+
open htmlcov/index.html
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "workstream-cli"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
readme = "README.md"
|
|
5
|
+
description = "Attention management for makers. Track trains of thought across projects."
|
|
6
|
+
authors = ["Mahmoud Hashemi <mahmoud@hatnote.com>"]
|
|
7
|
+
license = "MIT"
|
|
8
|
+
packages = [{include = "workstream", from = "src"}]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"Environment :: Console",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Topic :: Office/Business :: Scheduling",
|
|
21
|
+
"Topic :: Software Development",
|
|
22
|
+
"Topic :: Utilities",
|
|
23
|
+
]
|
|
24
|
+
keywords = ["workstream", "productivity", "cli", "attention-management", "task-management"]
|
|
25
|
+
|
|
26
|
+
[tool.poetry.scripts]
|
|
27
|
+
ws = "workstream.cli:main"
|
|
28
|
+
|
|
29
|
+
[tool.poetry.dependencies]
|
|
30
|
+
python = "^3.10"
|
|
31
|
+
face = ">=20.0.0"
|
|
32
|
+
pyyaml = "^6.0"
|
|
33
|
+
ashes = ">=24.0.0"
|
|
34
|
+
clastic = ">=24.0.0"
|
|
35
|
+
|
|
36
|
+
[tool.poetry.group.dev.dependencies]
|
|
37
|
+
pytest = "^7.4"
|
|
38
|
+
pytest-cov = ">=4.1"
|
|
39
|
+
coverage = {version = ">=7.0", extras = ["toml"]}
|
|
40
|
+
tox = ">=4.0"
|
|
41
|
+
|
|
42
|
+
[tool.poetry.urls]
|
|
43
|
+
Homepage = "https://github.com/mahmoud/workstream"
|
|
44
|
+
Repository = "https://github.com/mahmoud/workstream"
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["src/workstream/test"]
|
|
48
|
+
|
|
49
|
+
[build-system]
|
|
50
|
+
requires = ["poetry-core"]
|
|
51
|
+
build-backend = "poetry.core.masonry.api"
|
|
52
|
+
|
|
53
|
+
[tool.coverage.run]
|
|
54
|
+
source = ["workstream"]
|
|
55
|
+
branch = true
|
|
56
|
+
omit = ["src/workstream/test/*"]
|
|
57
|
+
|
|
58
|
+
[tool.coverage.report]
|
|
59
|
+
show_missing = true
|
|
60
|
+
skip_empty = true
|
|
61
|
+
|
|
62
|
+
[tool.coverage.html]
|
|
63
|
+
directory = "htmlcov"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# ws Architecture
|
|
2
|
+
|
|
3
|
+
ws is a workstream manager — it tracks trains of thought spanning projects and time.
|
|
4
|
+
Data lives as markdown files in `_Workstreams/`, each with YAML frontmatter and a markdown body.
|
|
5
|
+
|
|
6
|
+
## Execution Modes
|
|
7
|
+
|
|
8
|
+
ws operates in four distinct modes depending on who initiates the call and how results are consumed.
|
|
9
|
+
|
|
10
|
+
| Mode | Who drives | LLM involved | ws lifetime | Examples |
|
|
11
|
+
|------------|------------------|--------------|--------------------|----------------------------------------------------|
|
|
12
|
+
| **direct** | User via CLI | No | Runs and exits | `ws list`, `ws stale`, `ws next`, `ws report`, `ws show` |
|
|
13
|
+
| **handoff**| User via CLI | Yes (omp) | Execs into omp | `ws review`, `ws sweep --review-plans`, `ws init` bootstrap |
|
|
14
|
+
| **batch** | User via CLI | Yes (loop) | Stays alive | `ws sweep --discover`, `ws sweep --review-plans --batch`, summary backfill |
|
|
15
|
+
| **tool** | Agent in omp | Already running | Runs and exits | Agent runs `ws stale`, `ws show`, etc. as tool calls |
|
|
16
|
+
|
|
17
|
+
### Direct
|
|
18
|
+
|
|
19
|
+
Deterministic CLI commands. Pure Python, no LLM. Read workstream files, compute, print.
|
|
20
|
+
|
|
21
|
+
### Handoff
|
|
22
|
+
|
|
23
|
+
ws gathers context, writes a manifest to a temp file, then **replaces itself** via `os.execv` into an omp/claude harness process. The harness receives the manifest as `--append-system-prompt` and a skill directive. After exec, ws is gone — the harness owns the session.
|
|
24
|
+
|
|
25
|
+
### Batch
|
|
26
|
+
|
|
27
|
+
ws orchestrates repeated LLM calls in a loop. It stays alive as the driver: sends prompts via `llm.py`, parses structured responses, writes results back to workstream files. Used for bulk operations where interactive review is unnecessary.
|
|
28
|
+
|
|
29
|
+
### Tool
|
|
30
|
+
|
|
31
|
+
The user is already inside an omp session. The agent calls ws CLI commands as tools. ws is a passive data source — it reads, computes, prints, and exits. No handoff occurs.
|
|
32
|
+
|
|
33
|
+
## Recursion Guard
|
|
34
|
+
|
|
35
|
+
When a handoff command is invoked from **within** an existing omp session (tool mode), ws must not exec into a second harness. Detection: check for `$OMPCODE=1` or `$CLAUDECODE=1` environment variables. When detected, print the context manifest to stdout instead of calling `os.execv`. See `commands/sweep.py` for the handoff path.
|
|
36
|
+
|
|
37
|
+
## Data Model
|
|
38
|
+
|
|
39
|
+
All types live in `models.py`. No I/O — pure dataclasses and helpers.
|
|
40
|
+
|
|
41
|
+
| Type | Description |
|
|
42
|
+
|-----------------|----------------------------------------------------------|
|
|
43
|
+
| `Workstream` | Top-level entity: id, title, status, size, tags, repos, timestamps, thread, plans, branches, next_actions, log, thoughts, ideas, decisions, summary |
|
|
44
|
+
| `ThreadEntry` | Timestamped narrative entry in a workstream's thread |
|
|
45
|
+
| `ThoughtEntry` | Captured thought with timestamp and content |
|
|
46
|
+
| `IdeaEntry` | Idea with timestamp and content |
|
|
47
|
+
| `PlanRef` | Reference to a plan file: path, id, optional workstream |
|
|
48
|
+
| `BranchRef` | Reference to a git branch: name, repo, optional dates |
|
|
49
|
+
| `LogEntry` | Timestamped structured log entry (action + detail) |
|
|
50
|
+
|
|
51
|
+
Serialization to/from markdown+YAML frontmatter is handled by `markdown.py`.
|
|
52
|
+
|
|
53
|
+
## Key Modules
|
|
54
|
+
|
|
55
|
+
| Module | Responsibility |
|
|
56
|
+
|---------------------|-----------------------------------------------------------------------|
|
|
57
|
+
| `cli.py` | Entry point, face `Command` tree, middleware for config loading |
|
|
58
|
+
| `config.py` | Config discovery (`$WS_DIR`, walk-up, global pointer), LLM detection |
|
|
59
|
+
| `models.py` | Dataclasses for all entities. Pure data + helpers, no I/O |
|
|
60
|
+
| `markdown.py` | YAML frontmatter round-trip, section-level read/write |
|
|
61
|
+
| `llm.py` | LLM shell-out abstraction — detect and invoke CLI-based LLM agents |
|
|
62
|
+
| `git.py` | Thin subprocess wrappers for branch and commit queries |
|
|
63
|
+
| `repo_discovery.py` | Scan directories for git repos, build activity index |
|
|
64
|
+
| `sweep_state.py` | Persistent state for sweep deduplication across runs |
|
|
65
|
+
| `thoughts.py` | Thought/snooze computation helpers |
|
|
66
|
+
| `web.py` | Live web dashboard |
|
|
67
|
+
|
|
68
|
+
### Commands (`commands/`)
|
|
69
|
+
|
|
70
|
+
| Command | Mode | What it does |
|
|
71
|
+
|---------------|----------|-----------------------------------------------------------|
|
|
72
|
+
| `init` | handoff | First-time setup, install harness skills, optional bootstrap |
|
|
73
|
+
| `list_cmd` | direct | Tabular listing of workstreams |
|
|
74
|
+
| `show` | direct | Display a single workstream by id prefix |
|
|
75
|
+
| `stale` | direct | Find workstreams that need attention |
|
|
76
|
+
| `next_cmd` | direct | Prioritized active workstreams by staleness |
|
|
77
|
+
| `report` | direct | Activity report across workstreams |
|
|
78
|
+
| `sweep` | all four | Scan repos, discover/review plans, batch or interactive |
|
|
79
|
+
| `backfill` | batch | Add IDs and infer associations for plan files |
|
|
80
|
+
| `checkin` | direct | Record a check-in on a workstream |
|
|
81
|
+
| `new` | direct | Create a new workstream |
|
|
82
|
+
| `idea` | direct | Idea capture, listing, promotion |
|
|
83
|
+
| `snooze` | direct | Snooze/wake workstreams |
|
|
84
|
+
| `nest` | direct | Workstream hierarchy (nest/unnest) |
|
|
85
|
+
| `tree` | direct | ASCII tree of workstream hierarchy |
|
|
86
|
+
| `index` | direct | Generate index.md for the workstreams directory |
|
|
87
|
+
| `serve` | direct | Live web dashboard server |
|
|
88
|
+
| `setup` | direct | Validate installation health, optionally fix |
|
|
89
|
+
| `cron` | direct | Set up periodic sweep in user crontab |
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""ws CLI entry point and shared helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from face import Command, CommandLineError, UsageError, face_middleware
|
|
9
|
+
|
|
10
|
+
from workstream.config import Config, apply_timezone, load_config
|
|
11
|
+
from workstream.markdown import load_workstream
|
|
12
|
+
from workstream.models import Workstream, slugify
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Files that live in the workstream directory but aren't workstream files.
|
|
16
|
+
_SKIP_FILES = frozenset(('index.md', 'inbox.md', 'dashboard.html'))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_all_workstreams(ws_dir: Path) -> list[Workstream]:
|
|
20
|
+
"""Load every workstream from *ws_dir*, skipping non-workstream files."""
|
|
21
|
+
result: list[Workstream] = []
|
|
22
|
+
for f in sorted(ws_dir.glob('*.md')):
|
|
23
|
+
if f.name in _SKIP_FILES:
|
|
24
|
+
continue
|
|
25
|
+
try:
|
|
26
|
+
result.append(load_workstream(f))
|
|
27
|
+
except Exception:
|
|
28
|
+
continue
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def resolve_workstream(config: Config, id_prefix: str) -> Workstream:
|
|
33
|
+
"""Find the unique workstream matching *id_prefix*.
|
|
34
|
+
|
|
35
|
+
Matches against full ID, hex suffix, any prefix/suffix of the ID,
|
|
36
|
+
or title slug prefix. Raises ``UsageError`` on zero or multiple matches.
|
|
37
|
+
"""
|
|
38
|
+
ws_dir = config.workstreams_path
|
|
39
|
+
matches: list[Workstream] = []
|
|
40
|
+
for ws in load_all_workstreams(ws_dir):
|
|
41
|
+
# Match: full ID prefix, full ID suffix, hex-suffix prefix, or slug prefix
|
|
42
|
+
hex_suffix = ws.id.split('-', 3)[-1] # e.g. 'a1b2c3d4e5'
|
|
43
|
+
slug = slugify(ws.title)
|
|
44
|
+
if (ws.id.startswith(id_prefix)
|
|
45
|
+
or ws.id.endswith(id_prefix)
|
|
46
|
+
or hex_suffix.startswith(id_prefix)
|
|
47
|
+
or slug.startswith(id_prefix)):
|
|
48
|
+
matches.append(ws)
|
|
49
|
+
|
|
50
|
+
if len(matches) == 0:
|
|
51
|
+
raise UsageError(f'No workstream matching "{id_prefix}"')
|
|
52
|
+
if len(matches) > 1:
|
|
53
|
+
names = ', '.join(f'{w.id} ({w.title})' for w in matches)
|
|
54
|
+
raise UsageError(f'Multiple matches for "{id_prefix}": {names}')
|
|
55
|
+
return matches[0]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# Middleware: resolve config via --dir flag / env var / auto-discovery
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
@face_middleware(provides=['config'], optional=True)
|
|
63
|
+
def mw_load_config(next_, dir):
|
|
64
|
+
"""Resolve workstreams dir from --dir flag, $WS_DIR, or auto-discovery."""
|
|
65
|
+
if dir:
|
|
66
|
+
ws_dir = Path(dir).expanduser().resolve()
|
|
67
|
+
# Accept both notes root (parent dir) and direct _Workstreams path
|
|
68
|
+
candidate = ws_dir / '_Workstreams'
|
|
69
|
+
if candidate.is_dir():
|
|
70
|
+
ws_dir = candidate
|
|
71
|
+
elif not ws_dir.is_dir():
|
|
72
|
+
# CommandLineError from middleware doesn't get printed by face's
|
|
73
|
+
# run loop; print explicitly so the user sees the message.
|
|
74
|
+
print(f'error: --dir: directory does not exist: {ws_dir}', file=sys.stderr)
|
|
75
|
+
raise SystemExit(1)
|
|
76
|
+
config = load_config(ws_dir_override=ws_dir)
|
|
77
|
+
else:
|
|
78
|
+
config = load_config()
|
|
79
|
+
# Guard: if we resolved a workstreams dir but it has no config.yaml,
|
|
80
|
+
# the pointer is stale or WS_DIR is pointing at the wrong directory.
|
|
81
|
+
if config.workstreams_dir:
|
|
82
|
+
ws_path = Path(config.workstreams_dir)
|
|
83
|
+
if ws_path.is_dir() and not (ws_path / 'config.yaml').exists():
|
|
84
|
+
raise UsageError(
|
|
85
|
+
f'Directory {str(ws_path)!r} has no workstream config (config.yaml).\n'
|
|
86
|
+
f"Is $WS_DIR set correctly? Run 'ws init' to initialize."
|
|
87
|
+
)
|
|
88
|
+
apply_timezone(config)
|
|
89
|
+
return next_(config=config)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def build_command() -> Command:
|
|
93
|
+
"""Construct the top-level ``ws`` command with all subcommands."""
|
|
94
|
+
cmd = Command(None, name='ws', doc='Workstream manager \u2014 track trains of thought across projects.')
|
|
95
|
+
cmd.add('--dir', parse_as=str, missing=None,
|
|
96
|
+
doc='Workstreams directory (or parent); overrides $WS_DIR and auto-discovery')
|
|
97
|
+
cmd.add(mw_load_config)
|
|
98
|
+
|
|
99
|
+
from workstream.commands import init, new, list_cmd, show, report, index, next_cmd, stale, sweep, serve, backfill, cron, nest, tree, snooze, idea, checkin, setup, inspect_cmd, review, block, focus_cmd, resume, update_status
|
|
100
|
+
|
|
101
|
+
cmd.add(init.get_command())
|
|
102
|
+
cmd.add(new.get_command())
|
|
103
|
+
cmd.add(list_cmd.get_command())
|
|
104
|
+
cmd.add(show.get_command())
|
|
105
|
+
cmd.add(report.get_command())
|
|
106
|
+
cmd.add(index.get_command())
|
|
107
|
+
cmd.add(next_cmd.get_command())
|
|
108
|
+
cmd.add(stale.get_command())
|
|
109
|
+
cmd.add(sweep.get_command())
|
|
110
|
+
cmd.add(serve.get_command())
|
|
111
|
+
cmd.add(backfill.get_command())
|
|
112
|
+
cmd.add(cron.get_command())
|
|
113
|
+
cmd.add(nest.get_nest_command())
|
|
114
|
+
cmd.add(nest.get_unnest_command())
|
|
115
|
+
cmd.add(tree.get_command())
|
|
116
|
+
cmd.add(snooze.get_command())
|
|
117
|
+
cmd.add(snooze.get_wake_command())
|
|
118
|
+
cmd.add(idea.get_idea_command())
|
|
119
|
+
cmd.add(idea.get_ideas_command())
|
|
120
|
+
cmd.add(idea.get_promote_command())
|
|
121
|
+
cmd.add(checkin.get_command())
|
|
122
|
+
cmd.add(inspect_cmd.get_command())
|
|
123
|
+
cmd.add(setup.get_command())
|
|
124
|
+
cmd.add(review.get_command())
|
|
125
|
+
cmd.add(block.get_command())
|
|
126
|
+
cmd.add(block.get_unblock_command())
|
|
127
|
+
cmd.add(focus_cmd.get_command())
|
|
128
|
+
cmd.add(resume.get_command())
|
|
129
|
+
cmd.add(update_status.get_command())
|
|
130
|
+
return cmd
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def main() -> None:
|
|
134
|
+
"""CLI entry point."""
|
|
135
|
+
cmd = build_command()
|
|
136
|
+
cmd.run()
|
|
File without changes
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""ws backfill — add IDs and infer workstream associations for .plans/ files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from face import Command
|
|
8
|
+
|
|
9
|
+
from workstream.config import Config
|
|
10
|
+
from workstream.cli import load_all_workstreams
|
|
11
|
+
from workstream.markdown import parse_frontmatter, write_frontmatter
|
|
12
|
+
from workstream.models import ACTIVE_STATUSES, generate_id, slugify
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _load_active_workstreams(config):
|
|
16
|
+
"""Load active workstreams from the configured directory."""
|
|
17
|
+
return [ws for ws in load_all_workstreams(config.workstreams_path)
|
|
18
|
+
if ws.status in ACTIVE_STATUSES]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _slug_overlap(plan_slug: str, ws_slug: str) -> bool:
|
|
22
|
+
"""Check if plan slug shares meaningful tokens with workstream slug."""
|
|
23
|
+
plan_parts = set(plan_slug.split('-'))
|
|
24
|
+
ws_parts = set(ws_slug.split('-'))
|
|
25
|
+
# Remove trivially short tokens (single chars, empty)
|
|
26
|
+
plan_parts = {p for p in plan_parts if len(p) > 1}
|
|
27
|
+
ws_parts = {p for p in ws_parts if len(p) > 1}
|
|
28
|
+
if not plan_parts or not ws_parts:
|
|
29
|
+
return False
|
|
30
|
+
return bool(plan_parts & ws_parts)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _infer_workstream(repo_name: str, plan_slug: str, workstreams):
|
|
34
|
+
"""Try to infer the workstream for a plan.
|
|
35
|
+
|
|
36
|
+
Returns the workstream ID if exactly one active workstream matches
|
|
37
|
+
by repo AND has title/slug overlap. Returns None otherwise.
|
|
38
|
+
"""
|
|
39
|
+
candidates = []
|
|
40
|
+
for ws in workstreams:
|
|
41
|
+
if repo_name not in ws.repos:
|
|
42
|
+
continue
|
|
43
|
+
ws_slug = slugify(ws.title)
|
|
44
|
+
if _slug_overlap(plan_slug, ws_slug):
|
|
45
|
+
candidates.append(ws)
|
|
46
|
+
if len(candidates) == 1:
|
|
47
|
+
return candidates[0].id
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _backfill_handler(config: Config, dry_run: bool = False) -> None:
|
|
52
|
+
workstreams = _load_active_workstreams(config)
|
|
53
|
+
|
|
54
|
+
total_plans = 0
|
|
55
|
+
ids_added = 0
|
|
56
|
+
ws_associated = 0
|
|
57
|
+
|
|
58
|
+
for repo_cfg in config.repos:
|
|
59
|
+
repo_path = Path(repo_cfg.path).expanduser()
|
|
60
|
+
plans_dir = config.get_plans_dir(repo_cfg.name, repo_path)
|
|
61
|
+
if not plans_dir.is_dir():
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
for plan_file in sorted(plans_dir.glob('*.md')):
|
|
65
|
+
total_plans += 1
|
|
66
|
+
try:
|
|
67
|
+
text = plan_file.read_text(encoding='utf-8')
|
|
68
|
+
meta, body = parse_frontmatter(text)
|
|
69
|
+
except Exception as exc:
|
|
70
|
+
print(f' warning: skipping {repo_cfg.name}/{plan_file.name}: {exc}')
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
changed = False
|
|
74
|
+
plan_id = meta.get('id', '')
|
|
75
|
+
title = meta.get('title', plan_file.stem)
|
|
76
|
+
plan_slug = slugify(title)
|
|
77
|
+
|
|
78
|
+
# Step b/c: ensure plan has an ID and filename matches
|
|
79
|
+
if not plan_id:
|
|
80
|
+
plan_id = generate_id()
|
|
81
|
+
meta['id'] = plan_id
|
|
82
|
+
changed = True
|
|
83
|
+
ids_added += 1
|
|
84
|
+
if dry_run:
|
|
85
|
+
print(f' would add id {plan_id} to {repo_cfg.name}/{plan_file.name}')
|
|
86
|
+
|
|
87
|
+
# Determine canonical filename
|
|
88
|
+
canonical_name = f'{plan_id}-{plan_slug}.md'
|
|
89
|
+
needs_rename = plan_file.name != canonical_name
|
|
90
|
+
|
|
91
|
+
# Step d: infer workstream if empty
|
|
92
|
+
ws_field = meta.get('workstream', '')
|
|
93
|
+
if not ws_field:
|
|
94
|
+
inferred = _infer_workstream(repo_cfg.name, plan_slug, workstreams)
|
|
95
|
+
if inferred:
|
|
96
|
+
meta['workstream'] = inferred
|
|
97
|
+
changed = True
|
|
98
|
+
ws_associated += 1
|
|
99
|
+
if dry_run:
|
|
100
|
+
print(f' would set workstream={inferred} on {repo_cfg.name}/{plan_file.name}')
|
|
101
|
+
|
|
102
|
+
if not changed and not needs_rename:
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
if dry_run:
|
|
106
|
+
if needs_rename:
|
|
107
|
+
print(f' would rename {repo_cfg.name}/{plan_file.name} -> {canonical_name}')
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
# Write updated frontmatter
|
|
111
|
+
new_text = write_frontmatter(meta, body)
|
|
112
|
+
|
|
113
|
+
if needs_rename:
|
|
114
|
+
new_path = plans_dir / canonical_name
|
|
115
|
+
if new_path.exists() and new_path != plan_file:
|
|
116
|
+
print(f' warning: cannot rename {plan_file.name} -> {canonical_name}: target exists')
|
|
117
|
+
# Still write updated metadata to the original file
|
|
118
|
+
plan_file.write_text(new_text, encoding='utf-8')
|
|
119
|
+
else:
|
|
120
|
+
plan_file.write_text(new_text, encoding='utf-8')
|
|
121
|
+
if new_path != plan_file:
|
|
122
|
+
plan_file.rename(new_path)
|
|
123
|
+
print(f' renamed {repo_cfg.name}/{plan_file.name} -> {canonical_name}')
|
|
124
|
+
else:
|
|
125
|
+
plan_file.write_text(new_text, encoding='utf-8')
|
|
126
|
+
|
|
127
|
+
label = 'Would process' if dry_run else 'Processed'
|
|
128
|
+
print(f'Backfill {"(dry run) " if dry_run else ""}complete: '
|
|
129
|
+
f'{total_plans} plans {label.lower()}, '
|
|
130
|
+
f'{ids_added} IDs added, '
|
|
131
|
+
f'{ws_associated} workstream associations made.')
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_command() -> Command:
|
|
135
|
+
cmd = Command(_backfill_handler, name='backfill',
|
|
136
|
+
doc='Add IDs and infer workstream associations for .plans/ files.')
|
|
137
|
+
cmd.add('--dry-run', parse_as=True,
|
|
138
|
+
doc='Show what would be done without modifying files.')
|
|
139
|
+
return cmd
|