workstreams-cli 0.5.0__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.
- workstreams/__init__.py +71 -0
- workstreams/cli.py +499 -0
- workstreams/config.py +227 -0
- workstreams/dashboard.py +341 -0
- workstreams/event_log.py +240 -0
- workstreams/manager.py +573 -0
- workstreams/models.py +121 -0
- workstreams/multiplexer/__init__.py +32 -0
- workstreams/multiplexer/base.py +44 -0
- workstreams/multiplexer/tmux.py +202 -0
- workstreams/multiplexer/tmux_compatible.py +22 -0
- workstreams/multiplexer/zellij.py +109 -0
- workstreams/notifier.py +147 -0
- workstreams/py.typed +0 -0
- workstreams/subagent_client.py +125 -0
- workstreams_cli-0.5.0.dist-info/METADATA +622 -0
- workstreams_cli-0.5.0.dist-info/RECORD +20 -0
- workstreams_cli-0.5.0.dist-info/WHEEL +5 -0
- workstreams_cli-0.5.0.dist-info/entry_points.txt +2 -0
- workstreams_cli-0.5.0.dist-info/top_level.txt +1 -0
workstreams/__init__.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Workstreams - visually dispatch coding-agent work to subagents in real
|
|
2
|
+
terminal windows and monitor it in one dashboard.
|
|
3
|
+
|
|
4
|
+
Agent-agnostic: works with Claude Code, Codex, OpenCode, Qwen Code,
|
|
5
|
+
MiMoCode, Hermes, Kilo Code, Cline, and any other coding agent you can
|
|
6
|
+
run from a shell.
|
|
7
|
+
|
|
8
|
+
Quick start:
|
|
9
|
+
pip install workstreams-cli
|
|
10
|
+
workstreams init --project myproj --workstreams 3
|
|
11
|
+
workstreams start --cmd "claude"
|
|
12
|
+
workstreams dispatch --workstream 1 --subagent claude-code --issue 42 --prompt "Fix auth"
|
|
13
|
+
workstreams monitor
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .models import (
|
|
17
|
+
WorkstreamConfig,
|
|
18
|
+
WorkstreamsConfig,
|
|
19
|
+
WorkstreamStatus,
|
|
20
|
+
SubagentEvent,
|
|
21
|
+
)
|
|
22
|
+
from .config import load_config, save_config, config_path_in, find_config_file
|
|
23
|
+
from .manager import WorkstreamsManager
|
|
24
|
+
from .event_log import EventLog, get_event_log, default_log_dir
|
|
25
|
+
from .notifier import Notifier
|
|
26
|
+
from .subagent_client import (
|
|
27
|
+
subagent_report,
|
|
28
|
+
subagent_started,
|
|
29
|
+
subagent_progress,
|
|
30
|
+
subagent_completed,
|
|
31
|
+
subagent_failed,
|
|
32
|
+
subagent_error,
|
|
33
|
+
subagent_done,
|
|
34
|
+
)
|
|
35
|
+
from .multiplexer import MultiplexerBase, TmuxMultiplexer, ZellijMultiplexer, get_multiplexer
|
|
36
|
+
|
|
37
|
+
__version__ = "0.5.0"
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
# models
|
|
41
|
+
"WorkstreamConfig",
|
|
42
|
+
"WorkstreamsConfig",
|
|
43
|
+
"WorkstreamStatus",
|
|
44
|
+
"SubagentEvent",
|
|
45
|
+
# config
|
|
46
|
+
"load_config",
|
|
47
|
+
"save_config",
|
|
48
|
+
"config_path_in",
|
|
49
|
+
"find_config_file",
|
|
50
|
+
# manager
|
|
51
|
+
"WorkstreamsManager",
|
|
52
|
+
# events
|
|
53
|
+
"EventLog",
|
|
54
|
+
"get_event_log",
|
|
55
|
+
"default_log_dir",
|
|
56
|
+
# notifications
|
|
57
|
+
"Notifier",
|
|
58
|
+
# subagent client
|
|
59
|
+
"subagent_report",
|
|
60
|
+
"subagent_started",
|
|
61
|
+
"subagent_progress",
|
|
62
|
+
"subagent_completed",
|
|
63
|
+
"subagent_failed",
|
|
64
|
+
"subagent_error",
|
|
65
|
+
"subagent_done",
|
|
66
|
+
# multiplexers
|
|
67
|
+
"MultiplexerBase",
|
|
68
|
+
"TmuxMultiplexer",
|
|
69
|
+
"ZellijMultiplexer",
|
|
70
|
+
"get_multiplexer",
|
|
71
|
+
]
|
workstreams/cli.py
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"""CLI entry point for workstreams.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
workstreams <command> [options]
|
|
5
|
+
|
|
6
|
+
Every command accepts --project to pick which project config to use.
|
|
7
|
+
Most commands also accept --json for machine-readable output (useful for
|
|
8
|
+
coding agents that want to parse status).
|
|
9
|
+
|
|
10
|
+
Exit codes:
|
|
11
|
+
0 success
|
|
12
|
+
1 general error
|
|
13
|
+
2 invalid arguments
|
|
14
|
+
3 git / gh error
|
|
15
|
+
4 multiplexer unavailable
|
|
16
|
+
5 workstream not found
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
from datetime import datetime, UTC, timedelta
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import List, Optional
|
|
28
|
+
|
|
29
|
+
from .config import load_config, save_config, find_config_file
|
|
30
|
+
from .manager import WorkstreamsManager
|
|
31
|
+
from .event_log import get_event_log
|
|
32
|
+
from .subagent_client import subagent_report
|
|
33
|
+
from . import __version__
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _load_manager(args) -> WorkstreamsManager:
|
|
37
|
+
"""Build a manager from parsed args."""
|
|
38
|
+
base_path = Path.cwd()
|
|
39
|
+
if getattr(args, "project", None):
|
|
40
|
+
# Look up .workstreams.yaml in cwd first, then fall back to cwd
|
|
41
|
+
config = load_config(base_path, project=args.project)
|
|
42
|
+
# If the user passed --project explicitly, trust it over the file's value
|
|
43
|
+
config.project = args.project
|
|
44
|
+
else:
|
|
45
|
+
config = load_config(base_path)
|
|
46
|
+
|
|
47
|
+
if getattr(args, "multiplexer", None):
|
|
48
|
+
config.multiplexer = args.multiplexer
|
|
49
|
+
if getattr(args, "layout", None):
|
|
50
|
+
config.layout = args.layout
|
|
51
|
+
if getattr(args, "base_branch", None):
|
|
52
|
+
config.base_branch = args.base_branch
|
|
53
|
+
|
|
54
|
+
manager = WorkstreamsManager(config)
|
|
55
|
+
|
|
56
|
+
# Auto-init if no workstreams defined and command needs them
|
|
57
|
+
if not config.workstreams and args.command in ("start", "dispatch", "work", "run", "logs", "sync", "assign"):
|
|
58
|
+
manager.config.workstreams = _default_workstreams(manager, getattr(args, "workstream", 1))
|
|
59
|
+
save_config(manager.config, manager.base_path)
|
|
60
|
+
return manager
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _default_workstreams(manager: WorkstreamsManager, first_id: int = 1):
|
|
64
|
+
from .models import WorkstreamConfig
|
|
65
|
+
return [
|
|
66
|
+
WorkstreamConfig(
|
|
67
|
+
id=first_id,
|
|
68
|
+
name=f"ws{first_id}",
|
|
69
|
+
path=f"worktrees/ws{first_id}",
|
|
70
|
+
branch=f"ws/{first_id}",
|
|
71
|
+
command="",
|
|
72
|
+
env={},
|
|
73
|
+
)
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# =============================================================================
|
|
78
|
+
# Subcommand builders
|
|
79
|
+
# =============================================================================
|
|
80
|
+
|
|
81
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
82
|
+
parser = argparse.ArgumentParser(
|
|
83
|
+
prog="workstreams",
|
|
84
|
+
description="Visually dispatch coding-agent work to subagents in real terminal windows and monitor it in one dashboard.",
|
|
85
|
+
epilog="See `workstreams <command> --help` for per-command options.",
|
|
86
|
+
)
|
|
87
|
+
parser.add_argument("--version", action="version", version=f"workstreams {__version__}")
|
|
88
|
+
|
|
89
|
+
sub = parser.add_subparsers(dest="command")
|
|
90
|
+
|
|
91
|
+
# ---- shared parent for most commands ----
|
|
92
|
+
def common_parent(name: str) -> argparse.ArgumentParser:
|
|
93
|
+
p = sub.add_parser(name, help=f"{name} - see --help")
|
|
94
|
+
p.add_argument("--project", help="Project name (default: from .workstreams.yaml or cwd)")
|
|
95
|
+
p.add_argument("--json", action="store_true", help="Emit JSON output where supported")
|
|
96
|
+
return p
|
|
97
|
+
|
|
98
|
+
# init
|
|
99
|
+
init = common_parent("init")
|
|
100
|
+
init.add_argument("--workstreams", type=int, default=4, help="Number of workstreams to create (default: 4)")
|
|
101
|
+
init.add_argument("--multiplexer", choices=["tmux", "zellij", "nami", "lmux", "wmux", "herdr"], default=None, help="Multiplexer type")
|
|
102
|
+
init.add_argument("--layout", choices=["even-horizontal", "even-vertical", "main-horizontal", "tiled"], default=None)
|
|
103
|
+
init.add_argument("--base-branch", default=None, help="Base branch (default: main)")
|
|
104
|
+
init.add_argument("--mode", choices=["worktree", "branch"], default=None, help="worktree (isolated) or branch (shared dir)")
|
|
105
|
+
init.add_argument("--agent", choices=["auto", "claude", "codex", "opencode", "qwen", "generic"], default=None)
|
|
106
|
+
init.add_argument("--force", action="store_true", help="Overwrite existing config")
|
|
107
|
+
init.add_argument("--no-worktrees", action="store_true", help="Skip creating git worktrees (config only)")
|
|
108
|
+
|
|
109
|
+
# start
|
|
110
|
+
start = common_parent("start")
|
|
111
|
+
start.add_argument("--workstream", type=int, help="Start a single workstream by ID")
|
|
112
|
+
start.add_argument("--cmd", help="Override the command sent to each pane")
|
|
113
|
+
start.add_argument("--multiplexer", choices=["tmux", "zellij", "nami", "lmux", "wmux", "herdr"], default=None)
|
|
114
|
+
start.add_argument("--layout", choices=["even-horizontal", "even-vertical", "main-horizontal", "tiled"], default=None)
|
|
115
|
+
|
|
116
|
+
# attach
|
|
117
|
+
attach = common_parent("attach")
|
|
118
|
+
attach.add_argument("--multiplexer", choices=["tmux", "zellij", "nami", "lmux", "wmux", "herdr"], default=None)
|
|
119
|
+
attach.add_argument("--session", help="Override session name")
|
|
120
|
+
|
|
121
|
+
# status
|
|
122
|
+
status = common_parent("status")
|
|
123
|
+
status.add_argument("--workstream", type=int, help="Show one workstream")
|
|
124
|
+
status.add_argument("--live", action="store_true", help="Watch mode (same as monitor)")
|
|
125
|
+
status.add_argument("--once", action="store_true", help="Single frame, exit")
|
|
126
|
+
|
|
127
|
+
# monitor
|
|
128
|
+
monitor = common_parent("monitor")
|
|
129
|
+
monitor.add_argument("--refresh", type=int, default=2, help="Seconds between frames")
|
|
130
|
+
monitor.add_argument("--once", action="store_true", help="Render one frame and exit")
|
|
131
|
+
|
|
132
|
+
# workstream add / remove / cleanup
|
|
133
|
+
ws_sub = sub.add_parser("workstream", help="Manage workstreams (add, remove, cleanup)")
|
|
134
|
+
ws_sub_sub = ws_sub.add_subparsers(dest="ws_command")
|
|
135
|
+
|
|
136
|
+
ws_add = ws_sub_sub.add_parser("add", help="Add a workstream")
|
|
137
|
+
ws_add.add_argument("--project", help="Project name")
|
|
138
|
+
ws_add.add_argument("--name", required=True)
|
|
139
|
+
ws_add.add_argument("--branch", required=True)
|
|
140
|
+
ws_add.add_argument("--path", help="Worktree path (default: worktrees/<name>)")
|
|
141
|
+
ws_add.add_argument("--command", default="", help="Default command for this pane")
|
|
142
|
+
ws_add.add_argument("--json", action="store_true")
|
|
143
|
+
|
|
144
|
+
ws_remove = ws_sub_sub.add_parser("remove", help="Remove a workstream")
|
|
145
|
+
ws_remove.add_argument("--project", help="Project name")
|
|
146
|
+
ws_remove.add_argument("--workstream", type=int, required=True)
|
|
147
|
+
ws_remove.add_argument("--force", action="store_true", help="Delete directory + worktree")
|
|
148
|
+
ws_remove.add_argument("--json", action="store_true")
|
|
149
|
+
|
|
150
|
+
ws_cleanup = ws_sub_sub.add_parser("cleanup", help="Clean up completed workstreams")
|
|
151
|
+
ws_cleanup.add_argument("--project", help="Project name")
|
|
152
|
+
ws_cleanup.add_argument("--workstream", type=int)
|
|
153
|
+
ws_cleanup.add_argument("--all", dest="all_done", action="store_true", help="Clean all workstreams")
|
|
154
|
+
ws_cleanup.add_argument("--force", action="store_true")
|
|
155
|
+
ws_cleanup.add_argument("--json", action="store_true")
|
|
156
|
+
|
|
157
|
+
# dispatch
|
|
158
|
+
dispatch = common_parent("dispatch")
|
|
159
|
+
dispatch.add_argument("--workstream", type=int, required=True)
|
|
160
|
+
dispatch.add_argument("--subagent", required=True, help="Subagent identifier (claude-code, codex, opencode, qwen-code, mimocode, hermes, kilo-code, cline, ... free-form)")
|
|
161
|
+
dispatch.add_argument("--issue", type=int, default=0)
|
|
162
|
+
dispatch.add_argument("--prompt", help="Prompt / task description sent to the pane")
|
|
163
|
+
dispatch.add_argument("--agent", help="Agent binary to invoke (e.g. 'claude', 'codex', 'opencode run'). If omitted, sends --prompt verbatim to the pane.")
|
|
164
|
+
dispatch.add_argument("--wait", action="store_true", help="Block until subagent reports done/failed")
|
|
165
|
+
dispatch.add_argument("--multiplexer", choices=["tmux", "zellij", "nami", "lmux", "wmux", "herdr"], default=None)
|
|
166
|
+
|
|
167
|
+
# work (run agent command directly)
|
|
168
|
+
work = common_parent("work")
|
|
169
|
+
work.add_argument("--workstream", type=int, required=True)
|
|
170
|
+
work.add_argument("--agent", required=True, help="Agent command (claude, codex, opencode, qwen, cline, ...)")
|
|
171
|
+
work.add_argument("--task", required=True, help="Task / prompt for the agent")
|
|
172
|
+
work.add_argument("--subagent", default="agent", help="Identifier used in event log")
|
|
173
|
+
work.add_argument("--issue", type=int, default=0)
|
|
174
|
+
work.add_argument("--wait", action="store_true", help="Block until terminal event")
|
|
175
|
+
work.add_argument("--multiplexer", choices=["tmux", "zellij", "nami", "lmux", "wmux", "herdr"], default=None)
|
|
176
|
+
|
|
177
|
+
# run
|
|
178
|
+
run = common_parent("run")
|
|
179
|
+
run.add_argument("--workstream", type=int, required=True)
|
|
180
|
+
run.add_argument("--cmd", required=True, dest="run_command")
|
|
181
|
+
|
|
182
|
+
# logs
|
|
183
|
+
logs = common_parent("logs")
|
|
184
|
+
logs.add_argument("--workstream", type=int, required=True)
|
|
185
|
+
logs.add_argument("--follow", action="store_true")
|
|
186
|
+
logs.add_argument("--lines", type=int, default=100)
|
|
187
|
+
|
|
188
|
+
# tail
|
|
189
|
+
tail = common_parent("tail")
|
|
190
|
+
tail.add_argument("--workstream", type=int)
|
|
191
|
+
tail.add_argument("--lines", type=int, default=20)
|
|
192
|
+
|
|
193
|
+
# events
|
|
194
|
+
events = common_parent("events")
|
|
195
|
+
events.add_argument("--workstream", type=int)
|
|
196
|
+
events.add_argument("--since", type=int, default=10, help="Minutes back (default: 10)")
|
|
197
|
+
events.add_argument("--type", choices=["started", "progress", "completed", "failed", "error", "done"], help="Filter")
|
|
198
|
+
events.add_argument("--subagent", help="Filter by subagent name")
|
|
199
|
+
events.add_argument("--limit", type=int, default=50)
|
|
200
|
+
events.add_argument("--clear", action="store_true", help="Clear the event log")
|
|
201
|
+
|
|
202
|
+
# event (emit an event from any process - the key agent-integration command)
|
|
203
|
+
event = sub.add_parser("event", help="Emit a subagent event (for subagents / scripts to report)")
|
|
204
|
+
event.add_argument("event_type", choices=["started", "progress", "completed", "failed", "error", "done"])
|
|
205
|
+
event.add_argument("--project", required=True)
|
|
206
|
+
event.add_argument("--workstream", type=int, default=0)
|
|
207
|
+
event.add_argument("--subagent", required=True, help="Subagent identifier (free-form)")
|
|
208
|
+
event.add_argument("--issue", type=int, default=0)
|
|
209
|
+
event.add_argument("--message", default="")
|
|
210
|
+
event.add_argument("--data", help="JSON object of extra data (e.g. '{\"files\": 3}')")
|
|
211
|
+
event.add_argument("--json", action="store_true")
|
|
212
|
+
|
|
213
|
+
# notify
|
|
214
|
+
notify = common_parent("notify")
|
|
215
|
+
notify.add_argument("--title", required=True)
|
|
216
|
+
notify.add_argument("--message", required=True)
|
|
217
|
+
notify.add_argument("--urgency", choices=["low", "normal", "critical"], default="normal")
|
|
218
|
+
|
|
219
|
+
# assign
|
|
220
|
+
assign = common_parent("assign")
|
|
221
|
+
assign.add_argument("--workstream", type=int, required=True)
|
|
222
|
+
assign.add_argument("--issue", type=int, action="append", required=True)
|
|
223
|
+
|
|
224
|
+
# sync
|
|
225
|
+
sync = common_parent("sync")
|
|
226
|
+
sync.add_argument("--workstream", type=int)
|
|
227
|
+
sync.add_argument("--rebase", action="store_true")
|
|
228
|
+
|
|
229
|
+
# pr
|
|
230
|
+
pr = common_parent("pr")
|
|
231
|
+
pr.add_argument("--workstream", type=int, required=True)
|
|
232
|
+
pr.add_argument("--title", required=True)
|
|
233
|
+
pr.add_argument("--body", default="")
|
|
234
|
+
pr.add_argument("--base", help="Target branch (default: config's base_branch)")
|
|
235
|
+
pr.add_argument("--draft", action="store_true")
|
|
236
|
+
|
|
237
|
+
# merge
|
|
238
|
+
merge = common_parent("merge")
|
|
239
|
+
merge.add_argument("--workstream", type=int, required=True)
|
|
240
|
+
merge.add_argument("--method", choices=["merge", "squash", "rebase"], default="squash")
|
|
241
|
+
merge.add_argument("--delete-branch", action="store_true")
|
|
242
|
+
merge.add_argument("--auto", action="store_true")
|
|
243
|
+
|
|
244
|
+
# status-json helper
|
|
245
|
+
_ = parser
|
|
246
|
+
return parser
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# =============================================================================
|
|
250
|
+
# Command handlers
|
|
251
|
+
# =============================================================================
|
|
252
|
+
|
|
253
|
+
def _cmd_init(args) -> int:
|
|
254
|
+
manager = _load_manager(args)
|
|
255
|
+
if args.mode:
|
|
256
|
+
manager.config.mode = args.mode
|
|
257
|
+
if args.agent:
|
|
258
|
+
manager.config.agent = args.agent
|
|
259
|
+
if args.no_worktrees:
|
|
260
|
+
# Skip git worktree creation; just write config
|
|
261
|
+
from .models import WorkstreamConfig
|
|
262
|
+
for i in range(1, args.workstreams + 1):
|
|
263
|
+
manager.config.workstreams.append(WorkstreamConfig(
|
|
264
|
+
id=i, name=f"ws{i}", path=f"worktrees/ws{i}",
|
|
265
|
+
branch=f"ws/{i}", command="", env={},
|
|
266
|
+
))
|
|
267
|
+
save_config(manager.config, manager.base_path)
|
|
268
|
+
print(f"Initialized config only (no worktrees) for '{manager.config.project}'")
|
|
269
|
+
return 0
|
|
270
|
+
ok = manager.init_project(force=args.force, num_workstreams=args.workstreams, agent=args.agent)
|
|
271
|
+
return 0 if ok else 1
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _cmd_start(args) -> int:
|
|
275
|
+
manager = _load_manager(args)
|
|
276
|
+
manager.start(workstream_id=args.workstream, command=args.cmd)
|
|
277
|
+
return 0
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _cmd_attach(args) -> int:
|
|
281
|
+
manager = _load_manager(args)
|
|
282
|
+
manager.attach(multiplexer=args.multiplexer, session=args.session)
|
|
283
|
+
return 0
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _cmd_status(args) -> int:
|
|
287
|
+
manager = _load_manager(args)
|
|
288
|
+
if args.live:
|
|
289
|
+
manager.monitor(refresh=2, once=False)
|
|
290
|
+
return 0
|
|
291
|
+
manager.status(workstream_id=args.workstream, live=False, as_json=args.json or args.once)
|
|
292
|
+
return 0
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _cmd_monitor(args) -> int:
|
|
296
|
+
manager = _load_manager(args)
|
|
297
|
+
manager.monitor(refresh=args.refresh, once=args.once)
|
|
298
|
+
return 0
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _cmd_workstream(args) -> int:
|
|
302
|
+
manager = _load_manager(args)
|
|
303
|
+
if args.ws_command == "add":
|
|
304
|
+
ws = manager.add_workstream(args.name, args.branch, args.path, args.command)
|
|
305
|
+
if args.json:
|
|
306
|
+
print(json.dumps(ws.to_dict(), indent=2))
|
|
307
|
+
return 0
|
|
308
|
+
if args.ws_command == "remove":
|
|
309
|
+
return manager.remove_workstream(args.workstream, force=args.force)
|
|
310
|
+
if args.ws_command == "cleanup":
|
|
311
|
+
manager.cleanup(workstream_id=args.workstream, force=args.force, all_done=args.all_done)
|
|
312
|
+
return 0
|
|
313
|
+
print("Usage: workstreams workstream {add|remove|cleanup} --help")
|
|
314
|
+
return 2
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _cmd_dispatch(args) -> int:
|
|
318
|
+
manager = _load_manager(args)
|
|
319
|
+
# Build the pane command
|
|
320
|
+
prompt = args.prompt or f"Work on issue #{args.issue} as {args.subagent}" if args.issue else f"as {args.subagent}"
|
|
321
|
+
if args.agent:
|
|
322
|
+
pane_cmd = f"{args.agent} {prompt}"
|
|
323
|
+
if args.issue:
|
|
324
|
+
pane_cmd = f"{args.agent} {prompt}"
|
|
325
|
+
else:
|
|
326
|
+
pane_cmd = prompt
|
|
327
|
+
|
|
328
|
+
# Re-use dispatch for logging/notify, then send the actual agent command
|
|
329
|
+
ws = manager.config.workstream(args.workstream)
|
|
330
|
+
if not ws:
|
|
331
|
+
print(f"Workstream {args.workstream} not found", file=sys.stderr)
|
|
332
|
+
return 5
|
|
333
|
+
|
|
334
|
+
from .subagent_client import subagent_started, subagent_report as _report
|
|
335
|
+
subagent_started(manager.config.project, args.workstream, args.subagent, args.issue, prompt)
|
|
336
|
+
manager._append_workstream_log(ws, f"DISPATCH [{args.subagent}] {pane_cmd}")
|
|
337
|
+
manager.notifier.send(f"Workstream {ws.name}", f"{args.subagent} started" + (f" on #{args.issue}" if args.issue else ""))
|
|
338
|
+
print(f"Dispatched {args.subagent} -> workstream {ws.name}" + (f" (issue #{args.issue})" if args.issue else ""))
|
|
339
|
+
|
|
340
|
+
mux = manager._get_multiplexer()
|
|
341
|
+
if mux is not None:
|
|
342
|
+
try:
|
|
343
|
+
mux.send_command(args.workstream, pane_cmd)
|
|
344
|
+
except Exception as e:
|
|
345
|
+
print(f" (warning) pane send failed: {e}", file=sys.stderr)
|
|
346
|
+
|
|
347
|
+
if args.wait:
|
|
348
|
+
return manager._wait_for_done(args.workstream, args.subagent)
|
|
349
|
+
return 0
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _cmd_work(args) -> int:
|
|
353
|
+
manager = _load_manager(args)
|
|
354
|
+
return manager.work(
|
|
355
|
+
args.workstream,
|
|
356
|
+
agent_cmd=args.agent,
|
|
357
|
+
task=args.task,
|
|
358
|
+
subagent=args.subagent,
|
|
359
|
+
issue=args.issue,
|
|
360
|
+
wait=args.wait,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _cmd_run(args) -> int:
|
|
365
|
+
manager = _load_manager(args)
|
|
366
|
+
ok = manager.run(args.workstream, args.run_command)
|
|
367
|
+
return 0 if ok else 1
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _cmd_logs(args) -> int:
|
|
371
|
+
manager = _load_manager(args)
|
|
372
|
+
manager.logs(args.workstream, follow=args.follow, lines=args.lines)
|
|
373
|
+
return 0
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _cmd_tail(args) -> int:
|
|
377
|
+
manager = _load_manager(args)
|
|
378
|
+
manager.tail_logs(workstream_id=args.workstream, follow=False)
|
|
379
|
+
return 0
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _cmd_events(args) -> int:
|
|
383
|
+
manager = _load_manager(args)
|
|
384
|
+
if args.clear:
|
|
385
|
+
manager.event_log.clear()
|
|
386
|
+
print("Event log cleared.")
|
|
387
|
+
return 0
|
|
388
|
+
events = manager.get_events(
|
|
389
|
+
workstream_id=args.workstream,
|
|
390
|
+
since_minutes=args.since,
|
|
391
|
+
event_type=args.type,
|
|
392
|
+
subagent=args.subagent,
|
|
393
|
+
limit=args.limit,
|
|
394
|
+
)
|
|
395
|
+
if args.json:
|
|
396
|
+
print(json.dumps(events, indent=2, default=str))
|
|
397
|
+
return 0
|
|
398
|
+
for e in events:
|
|
399
|
+
ts = e.get("timestamp", "")[11:19]
|
|
400
|
+
print(f"[{ts}] [{e['subagent']}] ws{e['workstream_id']} #{e['issue']} - {e['event_type']}: {e['message']}")
|
|
401
|
+
if not events:
|
|
402
|
+
print(f"(no events in last {args.since} min)")
|
|
403
|
+
return 0
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _cmd_event(args) -> int:
|
|
407
|
+
"""Emit a single event from the calling process (subagent -> main terminal)."""
|
|
408
|
+
data = {}
|
|
409
|
+
if args.data:
|
|
410
|
+
try:
|
|
411
|
+
data = json.loads(args.data)
|
|
412
|
+
except json.JSONDecodeError:
|
|
413
|
+
print(f"Invalid --data JSON: {args.data}", file=sys.stderr)
|
|
414
|
+
return 2
|
|
415
|
+
ok = subagent_report(
|
|
416
|
+
project=args.project,
|
|
417
|
+
workstream_id=args.workstream,
|
|
418
|
+
subagent=args.subagent,
|
|
419
|
+
issue=args.issue,
|
|
420
|
+
event_type=args.event_type,
|
|
421
|
+
message=args.message,
|
|
422
|
+
data=data,
|
|
423
|
+
)
|
|
424
|
+
if args.json:
|
|
425
|
+
print(json.dumps({"ok": ok, "event_type": args.event_type, "subagent": args.subagent}))
|
|
426
|
+
else:
|
|
427
|
+
if ok:
|
|
428
|
+
print(f"event [{args.event_type}] {args.subagent} ws{args.workstream} #{args.issue} -> {args.project}")
|
|
429
|
+
else:
|
|
430
|
+
print("failed to write event", file=sys.stderr)
|
|
431
|
+
return 0 if ok else 1
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _cmd_notify(args) -> int:
|
|
435
|
+
manager = _load_manager(args)
|
|
436
|
+
manager.notify(args.title, args.message, args.urgency)
|
|
437
|
+
return 0
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _cmd_assign(args) -> int:
|
|
441
|
+
manager = _load_manager(args)
|
|
442
|
+
manager.assign(args.workstream, args.issue)
|
|
443
|
+
return 0
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _cmd_sync(args) -> int:
|
|
447
|
+
manager = _load_manager(args)
|
|
448
|
+
return manager.sync(workstream_id=args.workstream, rebase=args.rebase)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _cmd_pr(args) -> int:
|
|
452
|
+
manager = _load_manager(args)
|
|
453
|
+
return manager.pr(args.workstream, args.title, args.body, args.base, args.draft)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _cmd_merge(args) -> int:
|
|
457
|
+
manager = _load_manager(args)
|
|
458
|
+
return manager.merge(args.workstream, args.method, args.delete_branch, args.auto)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
HANDLERS = {
|
|
462
|
+
"init": _cmd_init,
|
|
463
|
+
"start": _cmd_start,
|
|
464
|
+
"attach": _cmd_attach,
|
|
465
|
+
"status": _cmd_status,
|
|
466
|
+
"monitor": _cmd_monitor,
|
|
467
|
+
"workstream": _cmd_workstream,
|
|
468
|
+
"dispatch": _cmd_dispatch,
|
|
469
|
+
"work": _cmd_work,
|
|
470
|
+
"run": _cmd_run,
|
|
471
|
+
"logs": _cmd_logs,
|
|
472
|
+
"tail": _cmd_tail,
|
|
473
|
+
"events": _cmd_events,
|
|
474
|
+
"event": _cmd_event,
|
|
475
|
+
"notify": _cmd_notify,
|
|
476
|
+
"assign": _cmd_assign,
|
|
477
|
+
"sync": _cmd_sync,
|
|
478
|
+
"pr": _cmd_pr,
|
|
479
|
+
"merge": _cmd_merge,
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
484
|
+
parser = build_parser()
|
|
485
|
+
args = parser.parse_args(argv)
|
|
486
|
+
|
|
487
|
+
if not args.command:
|
|
488
|
+
parser.print_help()
|
|
489
|
+
return 2
|
|
490
|
+
|
|
491
|
+
handler = HANDLERS.get(args.command)
|
|
492
|
+
if handler is None:
|
|
493
|
+
parser.print_help()
|
|
494
|
+
return 2
|
|
495
|
+
return handler(args)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
if __name__ == "__main__":
|
|
499
|
+
sys.exit(main())
|