loopflow 0.2.2__tar.gz → 0.3.0__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.
- {loopflow-0.2.2 → loopflow-0.3.0}/.gitignore +2 -2
- {loopflow-0.2.2 → loopflow-0.3.0}/PKG-INFO +29 -4
- {loopflow-0.2.2 → loopflow-0.3.0}/README.md +28 -3
- {loopflow-0.2.2 → loopflow-0.3.0}/pyproject.toml +11 -1
- loopflow-0.3.0/src/loopflow/LOOPFLOW_STYLE.md +130 -0
- loopflow-0.3.0/src/loopflow/__init__.py +1 -0
- {loopflow-0.2.2 → loopflow-0.3.0}/src/loopflow/cli/__init__.py +8 -3
- loopflow-0.3.0/src/loopflow/cli/maestro.py +81 -0
- {loopflow-0.2.2 → loopflow-0.3.0}/src/loopflow/cli/meta.py +68 -0
- {loopflow-0.2.2 → loopflow-0.3.0}/src/loopflow/cli/pr.py +92 -19
- {loopflow-0.2.2 → loopflow-0.3.0}/src/loopflow/cli/run.py +163 -43
- loopflow-0.3.0/src/loopflow/cli/status.py +55 -0
- loopflow-0.3.0/src/loopflow/cli/wt.py +457 -0
- {loopflow-0.2.2 → loopflow-0.3.0}/src/loopflow/config.py +2 -1
- loopflow-0.3.0/src/loopflow/config_template.yaml +36 -0
- {loopflow-0.2.2 → loopflow-0.3.0}/src/loopflow/context.py +36 -3
- {loopflow-0.2.2 → loopflow-0.3.0}/src/loopflow/files.py +37 -1
- loopflow-0.3.0/src/loopflow/git.py +594 -0
- {loopflow-0.2.2 → loopflow-0.3.0}/src/loopflow/launcher.py +99 -0
- loopflow-0.3.0/src/loopflow/maestro/__init__.py +16 -0
- loopflow-0.3.0/src/loopflow/maestro/adapters.py +38 -0
- loopflow-0.3.0/src/loopflow/maestro/client.py +75 -0
- loopflow-0.3.0/src/loopflow/maestro/daemon.py +19 -0
- loopflow-0.3.0/src/loopflow/maestro/notification.py +39 -0
- loopflow-0.3.0/src/loopflow/maestro/service.py +157 -0
- loopflow-0.3.0/src/loopflow/maestro/session.py +44 -0
- {loopflow-0.2.2 → loopflow-0.3.0}/src/loopflow/pipeline.py +8 -5
- loopflow-0.3.0/src/loopflow/prompts/design.md +22 -0
- loopflow-0.3.0/src/loopflow/prompts/implement.md +21 -0
- loopflow-0.3.0/src/loopflow/prompts/review.md +47 -0
- loopflow-0.2.2/src/loopflow/__init__.py +0 -1
- loopflow-0.2.2/src/loopflow/cli/wt.py +0 -143
- loopflow-0.2.2/src/loopflow/git.py +0 -334
- {loopflow-0.2.2 → loopflow-0.3.0}/src/loopflow/builtins/__init__.py +0 -0
- {loopflow-0.2.2 → loopflow-0.3.0}/src/loopflow/builtins/commit_message.txt +0 -0
- {loopflow-0.2.2 → loopflow-0.3.0}/src/loopflow/builtins/pr_message.txt +0 -0
- {loopflow-0.2.2 → loopflow-0.3.0}/src/loopflow/llm_http.py +0 -0
- {loopflow-0.2.2 → loopflow-0.3.0}/src/loopflow/tokens.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: loopflow
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Arrange LLMs to code in harmony
|
|
5
5
|
Author: Jack
|
|
6
6
|
License-Expression: MIT
|
|
@@ -29,7 +29,7 @@ Description-Content-Type: text/markdown
|
|
|
29
29
|
|
|
30
30
|
Run LLM coding tasks from reusable prompt files.
|
|
31
31
|
|
|
32
|
-
macOS only.
|
|
32
|
+
macOS only. Supports Claude Code and OpenAI Codex via configuration.
|
|
33
33
|
|
|
34
34
|
## Install
|
|
35
35
|
|
|
@@ -48,7 +48,7 @@ The workflow: create a worktree, run tasks there, merge when ready. You can have
|
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
50
|
lf wt create my-feature # branch + worktree, opens IDEs
|
|
51
|
-
cd .
|
|
51
|
+
cd ../loopflow.my-feature # worktrees are sibling directories
|
|
52
52
|
|
|
53
53
|
lf design # interactive: figure out what to build
|
|
54
54
|
lf ship # batch: implement, review, test, commit, open PR
|
|
@@ -103,15 +103,33 @@ lf ship # runs each task, auto-commits between steps
|
|
|
103
103
|
## Worktrees
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
|
-
lf wt create auth # .
|
|
106
|
+
lf wt create auth # ../loopflow.auth/, opens Warp + Cursor
|
|
107
107
|
lf wt list # show all worktrees
|
|
108
108
|
lf wt clean # remove merged branches
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
## Session Tracking
|
|
112
|
+
|
|
113
|
+
Track running tasks across multiple terminals with the maestro daemon:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
lf maestro start # start tracking daemon
|
|
117
|
+
lf status # show running sessions
|
|
118
|
+
|
|
119
|
+
# In another terminal
|
|
120
|
+
lf implement -p # batch task registers automatically
|
|
121
|
+
|
|
122
|
+
# Check from anywhere
|
|
123
|
+
lf status # see all running sessions
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
When maestro is running, tasks automatically register, and you'll get macOS notifications when batch tasks complete.
|
|
127
|
+
|
|
111
128
|
## Configuration
|
|
112
129
|
|
|
113
130
|
```yaml
|
|
114
131
|
# .lf/config.yaml
|
|
132
|
+
model: claude # Model to use (claude or codex)
|
|
115
133
|
push: true # auto-push after commits
|
|
116
134
|
pr: false # open PR after pipelines
|
|
117
135
|
|
|
@@ -128,6 +146,8 @@ ide:
|
|
|
128
146
|
| `-x, --context` | Add context files |
|
|
129
147
|
| `-w, --worktree` | Create worktree and run task there |
|
|
130
148
|
| `-c, --copy` | Copy prompt to clipboard, show token breakdown |
|
|
149
|
+
| `-m, --model` | Choose model (claude, codex) |
|
|
150
|
+
| `--parallel` | Run with multiple models in parallel |
|
|
131
151
|
|
|
132
152
|
## Commands
|
|
133
153
|
|
|
@@ -137,7 +157,12 @@ ide:
|
|
|
137
157
|
| `lf <pipeline>` | Run a pipeline |
|
|
138
158
|
| `lf : "prompt"` | Inline prompt |
|
|
139
159
|
| `lf wt create/list/clean` | Worktree management |
|
|
160
|
+
| `lf wt compare <a> <b>` | Compare two worktree implementations |
|
|
140
161
|
| `lf pr create` | Open GitHub PR |
|
|
141
162
|
| `lf pr land [-a]` | Squash-merge to main |
|
|
163
|
+
| `lf meta init` | Initialize repo with prompts and config |
|
|
142
164
|
| `lf meta install` | Install Claude Code |
|
|
143
165
|
| `lf meta doctor` | Check dependencies |
|
|
166
|
+
| `lf maestro start` | Start session tracking daemon |
|
|
167
|
+
| `lf maestro stop` | Stop session tracking daemon |
|
|
168
|
+
| `lf status` | Show running sessions |
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Run LLM coding tasks from reusable prompt files.
|
|
4
4
|
|
|
5
|
-
macOS only.
|
|
5
|
+
macOS only. Supports Claude Code and OpenAI Codex via configuration.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -21,7 +21,7 @@ The workflow: create a worktree, run tasks there, merge when ready. You can have
|
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
lf wt create my-feature # branch + worktree, opens IDEs
|
|
24
|
-
cd .
|
|
24
|
+
cd ../loopflow.my-feature # worktrees are sibling directories
|
|
25
25
|
|
|
26
26
|
lf design # interactive: figure out what to build
|
|
27
27
|
lf ship # batch: implement, review, test, commit, open PR
|
|
@@ -76,15 +76,33 @@ lf ship # runs each task, auto-commits between steps
|
|
|
76
76
|
## Worktrees
|
|
77
77
|
|
|
78
78
|
```bash
|
|
79
|
-
lf wt create auth # .
|
|
79
|
+
lf wt create auth # ../loopflow.auth/, opens Warp + Cursor
|
|
80
80
|
lf wt list # show all worktrees
|
|
81
81
|
lf wt clean # remove merged branches
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
## Session Tracking
|
|
85
|
+
|
|
86
|
+
Track running tasks across multiple terminals with the maestro daemon:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
lf maestro start # start tracking daemon
|
|
90
|
+
lf status # show running sessions
|
|
91
|
+
|
|
92
|
+
# In another terminal
|
|
93
|
+
lf implement -p # batch task registers automatically
|
|
94
|
+
|
|
95
|
+
# Check from anywhere
|
|
96
|
+
lf status # see all running sessions
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
When maestro is running, tasks automatically register, and you'll get macOS notifications when batch tasks complete.
|
|
100
|
+
|
|
84
101
|
## Configuration
|
|
85
102
|
|
|
86
103
|
```yaml
|
|
87
104
|
# .lf/config.yaml
|
|
105
|
+
model: claude # Model to use (claude or codex)
|
|
88
106
|
push: true # auto-push after commits
|
|
89
107
|
pr: false # open PR after pipelines
|
|
90
108
|
|
|
@@ -101,6 +119,8 @@ ide:
|
|
|
101
119
|
| `-x, --context` | Add context files |
|
|
102
120
|
| `-w, --worktree` | Create worktree and run task there |
|
|
103
121
|
| `-c, --copy` | Copy prompt to clipboard, show token breakdown |
|
|
122
|
+
| `-m, --model` | Choose model (claude, codex) |
|
|
123
|
+
| `--parallel` | Run with multiple models in parallel |
|
|
104
124
|
|
|
105
125
|
## Commands
|
|
106
126
|
|
|
@@ -110,7 +130,12 @@ ide:
|
|
|
110
130
|
| `lf <pipeline>` | Run a pipeline |
|
|
111
131
|
| `lf : "prompt"` | Inline prompt |
|
|
112
132
|
| `lf wt create/list/clean` | Worktree management |
|
|
133
|
+
| `lf wt compare <a> <b>` | Compare two worktree implementations |
|
|
113
134
|
| `lf pr create` | Open GitHub PR |
|
|
114
135
|
| `lf pr land [-a]` | Squash-merge to main |
|
|
136
|
+
| `lf meta init` | Initialize repo with prompts and config |
|
|
115
137
|
| `lf meta install` | Install Claude Code |
|
|
116
138
|
| `lf meta doctor` | Check dependencies |
|
|
139
|
+
| `lf maestro start` | Start session tracking daemon |
|
|
140
|
+
| `lf maestro stop` | Stop session tracking daemon |
|
|
141
|
+
| `lf status` | Show running sessions |
|
|
@@ -41,9 +41,19 @@ build-backend = "hatchling.build"
|
|
|
41
41
|
|
|
42
42
|
[tool.hatch.build.targets.wheel]
|
|
43
43
|
packages = ["src/loopflow"]
|
|
44
|
+
include = [
|
|
45
|
+
"src/loopflow/prompts/*.md",
|
|
46
|
+
"src/loopflow/LOOPFLOW_STYLE.md",
|
|
47
|
+
"src/loopflow/config_template.yaml",
|
|
48
|
+
]
|
|
44
49
|
|
|
45
50
|
[tool.hatch.build.targets.sdist]
|
|
46
|
-
include = [
|
|
51
|
+
include = [
|
|
52
|
+
"src/loopflow",
|
|
53
|
+
"src/loopflow/prompts/*.md",
|
|
54
|
+
"src/loopflow/LOOPFLOW_STYLE.md",
|
|
55
|
+
"src/loopflow/config_template.yaml",
|
|
56
|
+
]
|
|
47
57
|
|
|
48
58
|
[tool.hatch.version]
|
|
49
59
|
path = "src/loopflow/__init__.py"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Style Guide
|
|
2
|
+
|
|
3
|
+
This is the governing document for this codebase. Humans and LLMs alike are expected to follow it.
|
|
4
|
+
|
|
5
|
+
## Quick Reference
|
|
6
|
+
|
|
7
|
+
- Prefix private functions with `_`
|
|
8
|
+
- Return `None` for "not found"; raise exceptions for "shouldn't happen"
|
|
9
|
+
- No `Args:`/`Returns:` docstrings—if types are clear, skip the docstring
|
|
10
|
+
- Mock side effects, but don't test mock wiring
|
|
11
|
+
|
|
12
|
+
# Goals
|
|
13
|
+
|
|
14
|
+
## Clarity
|
|
15
|
+
|
|
16
|
+
Design around data structures and public APIs. Aim for a 1:1 mapping between real-world concepts and their representation in code.
|
|
17
|
+
|
|
18
|
+
Write code that demonstrates its own correctness. If a feature exists, write a test that proves it works. Assume you won't finish everything you start—make it easy to see what's done and what's broken.
|
|
19
|
+
|
|
20
|
+
## Simplicity
|
|
21
|
+
|
|
22
|
+
Every line of code must earn its place. Readable code is not terse code; don't sacrifice clarity for brevity. But recognize that lines can be net-negative:
|
|
23
|
+
|
|
24
|
+
* Unused code
|
|
25
|
+
* Comments that restate the obvious
|
|
26
|
+
* Checks for impossible conditions
|
|
27
|
+
|
|
28
|
+
Start with minimal data structures and APIs. If the core is right, trimming excess at the edges is straightforward.
|
|
29
|
+
|
|
30
|
+
# Code Organization
|
|
31
|
+
|
|
32
|
+
Consistency with existing code matters more than any specific rule.
|
|
33
|
+
|
|
34
|
+
Keep information in one place. Version numbers, configuration, documentation—each piece of information should have a single source of truth. Don't duplicate versions in multiple files. If something needs to appear in multiple places, generate it or reference the source.
|
|
35
|
+
|
|
36
|
+
Put imports at the top of the file, not inline.
|
|
37
|
+
|
|
38
|
+
Keep one implementation. Avoid `v2_`, `_old`, `_new`, `_backup` prefixes and suffixes—look up old versions in git. If you're tempted to keep both old and new code around, delete the old version and commit. You can always get it back from git if needed.
|
|
39
|
+
|
|
40
|
+
Don't maintain backwards compatibility unless explicitly required. If a config format or API changes, migrate everything to the new format—don't write code that handles both old and new. Backwards compatibility is for production databases and published APIs with external users, not internal config files.
|
|
41
|
+
|
|
42
|
+
## Naming
|
|
43
|
+
|
|
44
|
+
Use verb-first names for action functions: `find_user()`, `load_config()`, `create_session()`.
|
|
45
|
+
|
|
46
|
+
Prefix private functions with underscore: `_validate()`, `_parse_line()`.
|
|
47
|
+
|
|
48
|
+
Name things after what they are, not what they're for: `Document`, `Session`, `Target`—not `DocumentHelper`, `SessionManager`, `OutputHandler`.
|
|
49
|
+
|
|
50
|
+
## Error Handling
|
|
51
|
+
|
|
52
|
+
Errors are for users; exceptions are for programmers.
|
|
53
|
+
|
|
54
|
+
Return errors when the caller should handle them—invalid input, missing files, failed requests. Raise exceptions for bugs: violated invariants, impossible states, programming mistakes.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# Error: caller decides what to do
|
|
58
|
+
def find_config(path: Path) -> Optional[Config]:
|
|
59
|
+
if not path.exists():
|
|
60
|
+
return None
|
|
61
|
+
return load(path)
|
|
62
|
+
|
|
63
|
+
# Exception: this shouldn't happen
|
|
64
|
+
def get_target(name: str) -> Target:
|
|
65
|
+
if name not in TARGETS:
|
|
66
|
+
raise ValueError(f"Unknown target: {name}")
|
|
67
|
+
return TARGETS[name]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
When in doubt: if you'd write an `assert`, raise an exception instead—it's easier for callers to catch.
|
|
71
|
+
|
|
72
|
+
# Documentation
|
|
73
|
+
|
|
74
|
+
The best documentation is simple code. Descriptive names, type hints, and clear APIs often suffice.
|
|
75
|
+
|
|
76
|
+
The worst documentation is wrong documentation. If it can drift from the code, it will. Update docs when you change code—or delete them.
|
|
77
|
+
|
|
78
|
+
Put documentation next to code. A few paragraphs at the top of a key file beats a separate doc that nobody maintains.
|
|
79
|
+
|
|
80
|
+
Skip obvious docstrings. If the function name and types tell the whole story, don't repeat it in prose.
|
|
81
|
+
|
|
82
|
+
# Testing
|
|
83
|
+
|
|
84
|
+
Test user behavior, not implementation details. A good test proves that something users care about actually works. Most tests don't meet that bar. Delete them.
|
|
85
|
+
|
|
86
|
+
Aim for a mix:
|
|
87
|
+
- **Smoke tests**: Does the system run without crashing?
|
|
88
|
+
- **Edge case tests**: What happens at boundaries?
|
|
89
|
+
- **Value tests**: Does this feature do what users expect?
|
|
90
|
+
|
|
91
|
+
## When to Mock
|
|
92
|
+
|
|
93
|
+
Mock to isolate your code from things that shouldn't be part of unit tests:
|
|
94
|
+
- **External systems**: Network calls, databases, file systems (when testing logic, not I/O)
|
|
95
|
+
- **Side effects**: Sending emails, writing logs, spawning processes
|
|
96
|
+
- **Slow operations**: Anything that would make tests take seconds instead of milliseconds
|
|
97
|
+
|
|
98
|
+
Don't mock to verify internal wiring. If a test's assertions are just "did we call the mock with the right args?"—that's testing implementation, not behavior. The test will break when you refactor, even if the feature still works.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# Bad: testing that we called the mock correctly
|
|
102
|
+
def test_send_notification():
|
|
103
|
+
with patch("app.email.send") as mock_send:
|
|
104
|
+
notify_user(user)
|
|
105
|
+
mock_send.assert_called_once_with(user.email, ANY)
|
|
106
|
+
|
|
107
|
+
# Good: mock the side effect, test the behavior
|
|
108
|
+
def test_notify_user_returns_success():
|
|
109
|
+
with patch("app.email.send"): # prevent actual email
|
|
110
|
+
result = notify_user(user)
|
|
111
|
+
assert result.success
|
|
112
|
+
|
|
113
|
+
# Better: if possible, test without mocking
|
|
114
|
+
def test_notification_message_format():
|
|
115
|
+
msg = build_notification(user)
|
|
116
|
+
assert user.name in msg.body
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
If a test requires elaborate mock setup, it's usually a sign that either:
|
|
120
|
+
1. The code under test does too much (refactor it)
|
|
121
|
+
2. You're testing implementation rather than behavior (test something else)
|
|
122
|
+
3. This should be an integration test, not a unit test (move it)
|
|
123
|
+
|
|
124
|
+
# Git
|
|
125
|
+
|
|
126
|
+
Commit messages are documentation. Explain what changed and why, not line-by-line what you did.
|
|
127
|
+
|
|
128
|
+
Keep messages short—one sentence to one paragraph.
|
|
129
|
+
|
|
130
|
+
Do not add AI attribution footers like "Generated with Claude Code" or "Co-Authored-By: Claude" to commits. The git history should read the same whether written by a human or AI.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -15,21 +15,23 @@ app = typer.Typer(
|
|
|
15
15
|
|
|
16
16
|
# Import and register subcommands
|
|
17
17
|
from loopflow.cli import run as run_module
|
|
18
|
-
from loopflow.cli import wt, pr, meta
|
|
18
|
+
from loopflow.cli import wt, pr, meta, maestro, status
|
|
19
19
|
|
|
20
20
|
app.add_typer(wt.app, name="wt")
|
|
21
21
|
app.add_typer(pr.app, name="pr")
|
|
22
22
|
app.add_typer(meta.app, name="meta")
|
|
23
|
+
app.add_typer(maestro.app, name="maestro")
|
|
23
24
|
|
|
24
25
|
# Register top-level commands
|
|
25
|
-
app.command()(run_module.run)
|
|
26
|
+
app.command(context_settings={"allow_extra_args": True, "allow_interspersed_args": True})(run_module.run)
|
|
26
27
|
app.command()(run_module.inline)
|
|
27
28
|
app.command(name="pipeline")(run_module.pipeline)
|
|
29
|
+
app.command()(status.status)
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
def main():
|
|
31
33
|
"""Entry point that supports 'lf <task>' and 'lf <pipeline>' shorthand."""
|
|
32
|
-
known_commands = {"run", "pipeline", "inline", "wt", "pr", "meta", "--help", "-h"}
|
|
34
|
+
known_commands = {"run", "pipeline", "inline", "wt", "pr", "meta", "maestro", "status", "--help", "-h"}
|
|
33
35
|
|
|
34
36
|
try:
|
|
35
37
|
if len(sys.argv) > 1:
|
|
@@ -40,6 +42,9 @@ def main():
|
|
|
40
42
|
sys.argv.pop(1)
|
|
41
43
|
sys.argv.insert(1, "inline")
|
|
42
44
|
elif first_arg not in known_commands:
|
|
45
|
+
# Handle colon suffix: "lf implement: add logout" -> "lf implement add logout"
|
|
46
|
+
if first_arg.endswith(":"):
|
|
47
|
+
sys.argv[1] = first_arg[:-1]
|
|
43
48
|
name = sys.argv[1]
|
|
44
49
|
repo_root = find_worktree_root()
|
|
45
50
|
config = load_config(repo_root) if repo_root else None
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Maestro daemon management commands."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import signal
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="Maestro daemon management.")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_paths() -> tuple[Path, Path, Path]:
|
|
15
|
+
"""Get maestro file paths."""
|
|
16
|
+
lf_dir = Path.home() / ".lf"
|
|
17
|
+
return (
|
|
18
|
+
lf_dir / "maestro.sock",
|
|
19
|
+
lf_dir / "maestro.json",
|
|
20
|
+
lf_dir / "maestro.pid",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _is_running(pid_path: Path) -> bool:
|
|
25
|
+
"""Check if maestro is running."""
|
|
26
|
+
if not pid_path.exists():
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
pid = int(pid_path.read_text().strip())
|
|
31
|
+
os.kill(pid, 0) # Signal 0 checks if process exists
|
|
32
|
+
return True
|
|
33
|
+
except (OSError, ValueError):
|
|
34
|
+
# Process doesn't exist or invalid PID
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command()
|
|
39
|
+
def start():
|
|
40
|
+
"""Start the maestro daemon."""
|
|
41
|
+
socket_path, state_path, pid_path = _get_paths()
|
|
42
|
+
|
|
43
|
+
if _is_running(pid_path):
|
|
44
|
+
typer.echo("Maestro already running")
|
|
45
|
+
raise typer.Exit(0)
|
|
46
|
+
|
|
47
|
+
# Start daemon as background process
|
|
48
|
+
process = subprocess.Popen(
|
|
49
|
+
[sys.executable, "-m", "loopflow.maestro.daemon"],
|
|
50
|
+
stdout=subprocess.DEVNULL,
|
|
51
|
+
stderr=subprocess.DEVNULL,
|
|
52
|
+
start_new_session=True,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Save PID
|
|
56
|
+
pid_path.parent.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
pid_path.write_text(str(process.pid))
|
|
58
|
+
|
|
59
|
+
typer.echo(f"Maestro listening on {socket_path}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.command()
|
|
63
|
+
def stop():
|
|
64
|
+
"""Stop the maestro daemon."""
|
|
65
|
+
socket_path, state_path, pid_path = _get_paths()
|
|
66
|
+
|
|
67
|
+
if not _is_running(pid_path):
|
|
68
|
+
typer.echo("Maestro not running")
|
|
69
|
+
raise typer.Exit(0)
|
|
70
|
+
|
|
71
|
+
pid = int(pid_path.read_text().strip())
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
os.kill(pid, signal.SIGTERM)
|
|
75
|
+
pid_path.unlink()
|
|
76
|
+
if socket_path.exists():
|
|
77
|
+
socket_path.unlink()
|
|
78
|
+
typer.echo("Maestro stopped")
|
|
79
|
+
except OSError:
|
|
80
|
+
typer.echo("Failed to stop maestro", err=True)
|
|
81
|
+
raise typer.Exit(1)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import platform
|
|
4
4
|
import shutil
|
|
5
5
|
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
import typer
|
|
8
9
|
|
|
@@ -36,6 +37,73 @@ def _install_cask(name: str) -> bool:
|
|
|
36
37
|
return result.returncode == 0
|
|
37
38
|
|
|
38
39
|
|
|
40
|
+
@app.command()
|
|
41
|
+
def init(
|
|
42
|
+
prompts_only: bool = typer.Option(False, "--prompts", help="Only install prompts"),
|
|
43
|
+
style_only: bool = typer.Option(False, "--style", help="Only install style guide"),
|
|
44
|
+
):
|
|
45
|
+
"""Initialize a repository with loopflow prompts, style guide, and config."""
|
|
46
|
+
repo_root = find_worktree_root()
|
|
47
|
+
if not repo_root:
|
|
48
|
+
typer.echo("Error: Not in a git repository", err=True)
|
|
49
|
+
raise typer.Exit(1)
|
|
50
|
+
|
|
51
|
+
# Determine what to install
|
|
52
|
+
install_prompts = prompts_only or not style_only
|
|
53
|
+
install_style = style_only or not prompts_only
|
|
54
|
+
install_config = not prompts_only and not style_only
|
|
55
|
+
|
|
56
|
+
# Get bundled assets path
|
|
57
|
+
bundled_dir = Path(__file__).parent.parent
|
|
58
|
+
prompts_dir = bundled_dir / "prompts"
|
|
59
|
+
style_template = bundled_dir / "LOOPFLOW_STYLE.md"
|
|
60
|
+
config_template = bundled_dir / "config_template.yaml"
|
|
61
|
+
|
|
62
|
+
# Install prompts to .claude/commands/ (accessible via raw claude too)
|
|
63
|
+
if install_prompts:
|
|
64
|
+
commands_dir = repo_root / ".claude" / "commands"
|
|
65
|
+
commands_dir.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
|
|
67
|
+
for prompt_name in ["review.md", "implement.md", "design.md"]:
|
|
68
|
+
src = prompts_dir / prompt_name
|
|
69
|
+
dst = commands_dir / prompt_name
|
|
70
|
+
|
|
71
|
+
if dst.exists():
|
|
72
|
+
typer.echo(f"- .claude/commands/{prompt_name} (already exists)")
|
|
73
|
+
else:
|
|
74
|
+
shutil.copy(src, dst)
|
|
75
|
+
typer.echo(f"✓ Created .claude/commands/{prompt_name}")
|
|
76
|
+
|
|
77
|
+
# Install style guide to .lf/ (only included in lf sessions, not raw claude/codex)
|
|
78
|
+
if install_style:
|
|
79
|
+
lf_dir = repo_root / ".lf"
|
|
80
|
+
lf_dir.mkdir(exist_ok=True)
|
|
81
|
+
style_dst = lf_dir / "LOOPFLOW_STYLE.md"
|
|
82
|
+
if style_dst.exists():
|
|
83
|
+
typer.echo("- .lf/LOOPFLOW_STYLE.md (already exists)")
|
|
84
|
+
else:
|
|
85
|
+
shutil.copy(style_template, style_dst)
|
|
86
|
+
typer.echo("✓ Created .lf/LOOPFLOW_STYLE.md")
|
|
87
|
+
|
|
88
|
+
# Install config
|
|
89
|
+
if install_config:
|
|
90
|
+
config_dir = repo_root / ".lf"
|
|
91
|
+
config_dir.mkdir(exist_ok=True)
|
|
92
|
+
config_dst = config_dir / "config.yaml"
|
|
93
|
+
|
|
94
|
+
if config_dst.exists():
|
|
95
|
+
typer.echo("- .lf/config.yaml (already exists)")
|
|
96
|
+
else:
|
|
97
|
+
shutil.copy(config_template, config_dst)
|
|
98
|
+
typer.echo("✓ Created .lf/config.yaml")
|
|
99
|
+
|
|
100
|
+
if install_prompts and not prompts_only:
|
|
101
|
+
typer.echo("\nYou can now use:")
|
|
102
|
+
typer.echo(" lf review # or: claude /review")
|
|
103
|
+
typer.echo(" lf implement")
|
|
104
|
+
typer.echo(" lf design")
|
|
105
|
+
|
|
106
|
+
|
|
39
107
|
@app.command()
|
|
40
108
|
def version():
|
|
41
109
|
"""Show loopflow version."""
|