opencommand 0.1.1__tar.gz → 0.1.3__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.
- {opencommand-0.1.1 → opencommand-0.1.3}/PKG-INFO +16 -1
- {opencommand-0.1.1 → opencommand-0.1.3}/README.md +14 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/pyproject.toml +2 -1
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/cli.py +128 -12
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/core/config.py +23 -0
- opencommand-0.1.3/src/opencommand/modules/skills/desktop_linux.md +41 -0
- opencommand-0.1.3/src/opencommand/modules/skills/desktop_macos.md +42 -0
- opencommand-0.1.3/src/opencommand/modules/skills/desktop_windows.md +44 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/system/automatic.py +11 -7
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/system/cron.py +19 -0
- opencommand-0.1.3/src/opencommand/modules/tools/desktop.py +202 -0
- opencommand-0.1.3/src/opencommand/modules/tools/desktop_safety.py +162 -0
- opencommand-0.1.3/src/opencommand/modules/tools/system.py +285 -0
- opencommand-0.1.3/src/opencommand/modules/tools/yara_rules/default.yar +61 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/swarm/agents/commander.py +23 -4
- opencommand-0.1.1/src/opencommand/modules/tools/desktop.py +0 -127
- opencommand-0.1.1/src/opencommand/modules/tools/system.py +0 -163
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/__init__.py +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/core/agent.py +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/core/errors.py +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/core/logging.py +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/core/supervisor.py +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/core/tools.py +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/engine/__init__.py +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/engine/runner.py +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/knowledge.py +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/memory/MEMORY.md +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/skills/bash.md +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/skills/powershell.md +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/skills/python.md +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/skills/system.md +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/system/notify.py +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/system/platform.py +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/templates.py +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/tools/files.py +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/tools/instructions.py +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/tools/memory.py +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/tools/playwright.py +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/tools/research.py +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/tools/terminal.py +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/providers/provider.py +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/swarm/agents/worker.py +0 -0
- {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/tui/dashboard.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: opencommand
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Local-first swarm agent framework: a Commander plans and dispatches a swarm of Workers that research, code, test, and interactively drive the computer to complete long-horizon codebase goals.
|
|
5
5
|
Author: DSiENT
|
|
6
6
|
Author-email: DSiENT <w.caskey7@gmail.com>
|
|
@@ -16,6 +16,7 @@ Requires-Dist: llama-cpp-python>=0.3.33
|
|
|
16
16
|
Requires-Dist: huggingface-hub>=0.30.0
|
|
17
17
|
Requires-Dist: watchdog>=6.0.0
|
|
18
18
|
Requires-Dist: ntfy>=2.7.1
|
|
19
|
+
Requires-Dist: yara-python>=4.5.0
|
|
19
20
|
Requires-Python: >=3.14
|
|
20
21
|
Project-URL: Homepage, https://github.com/D5-Interactive/OpenCommand
|
|
21
22
|
Project-URL: Repository, https://github.com/D5-Interactive/OpenCommand
|
|
@@ -55,12 +56,26 @@ playwright install # optional, for the playwright tool
|
|
|
55
56
|
```bash
|
|
56
57
|
opencommand models list # list built-in embedded models
|
|
57
58
|
opencommand models pull all # download GGUF models into ./models
|
|
59
|
+
opencommand config # interactive menu: workers, batch size, GPU, etc.
|
|
58
60
|
opencommand run "Add unit tests for the engine module."
|
|
61
|
+
opencommand run --goal-file Goal.md # run from a Goal.md spec (intended state)
|
|
59
62
|
opencommand run "Build a Panda3D FPS" --pipeline advanced --verify "uv run pytest -q"
|
|
60
63
|
opencommand tui # live swarm dashboard
|
|
61
64
|
opencommand cron add healthcheck "every 1h" "echo ok"
|
|
62
65
|
```
|
|
63
66
|
|
|
67
|
+
### Goal.md spec
|
|
68
|
+
|
|
69
|
+
Instead of an inline goal, point `run` at a `Goal.md` file that defines the
|
|
70
|
+
intended end state / requirements for the session. Its contents become the goal
|
|
71
|
+
fed to the swarm (and the advanced pipeline's review gate checks against it):
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
opencommand run --goal-file Goal.md
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The file name is configurable via `opencommand config` (default `Goal.md`).
|
|
78
|
+
|
|
64
79
|
## Built-in models
|
|
65
80
|
|
|
66
81
|
| Role | Model |
|
|
@@ -32,12 +32,26 @@ playwright install # optional, for the playwright tool
|
|
|
32
32
|
```bash
|
|
33
33
|
opencommand models list # list built-in embedded models
|
|
34
34
|
opencommand models pull all # download GGUF models into ./models
|
|
35
|
+
opencommand config # interactive menu: workers, batch size, GPU, etc.
|
|
35
36
|
opencommand run "Add unit tests for the engine module."
|
|
37
|
+
opencommand run --goal-file Goal.md # run from a Goal.md spec (intended state)
|
|
36
38
|
opencommand run "Build a Panda3D FPS" --pipeline advanced --verify "uv run pytest -q"
|
|
37
39
|
opencommand tui # live swarm dashboard
|
|
38
40
|
opencommand cron add healthcheck "every 1h" "echo ok"
|
|
39
41
|
```
|
|
40
42
|
|
|
43
|
+
### Goal.md spec
|
|
44
|
+
|
|
45
|
+
Instead of an inline goal, point `run` at a `Goal.md` file that defines the
|
|
46
|
+
intended end state / requirements for the session. Its contents become the goal
|
|
47
|
+
fed to the swarm (and the advanced pipeline's review gate checks against it):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
opencommand run --goal-file Goal.md
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The file name is configurable via `opencommand config` (default `Goal.md`).
|
|
54
|
+
|
|
41
55
|
## Built-in models
|
|
42
56
|
|
|
43
57
|
| Role | Model |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "opencommand"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3"
|
|
4
4
|
description = "Local-first swarm agent framework: a Commander plans and dispatches a swarm of Workers that research, code, test, and interactively drive the computer to complete long-horizon codebase goals."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -20,6 +20,7 @@ dependencies = [
|
|
|
20
20
|
"huggingface_hub>=0.30.0",
|
|
21
21
|
"watchdog>=6.0.0",
|
|
22
22
|
"ntfy>=2.7.1",
|
|
23
|
+
"yara-python>=4.5.0",
|
|
23
24
|
]
|
|
24
25
|
|
|
25
26
|
[project.scripts]
|
|
@@ -47,7 +47,10 @@ def _build_tools(engine: Engine, cfg: OpenCommandConfig) -> ToolRegistry:
|
|
|
47
47
|
reg.register(PlaywrightTool())
|
|
48
48
|
reg.register(SystemTool())
|
|
49
49
|
vision_role = cfg.vision_model if cfg.vision else None
|
|
50
|
-
|
|
50
|
+
from .modules.tools.desktop_safety import DesktopPolicy
|
|
51
|
+
desktop_policy = DesktopPolicy.from_dict(cfg.desktop_policy)
|
|
52
|
+
reg.register(DesktopTool(engine.runner(vision_role) if vision_role else None,
|
|
53
|
+
policy=desktop_policy))
|
|
51
54
|
return reg
|
|
52
55
|
|
|
53
56
|
|
|
@@ -57,7 +60,7 @@ def _load_cfg(path: str | None) -> OpenCommandConfig:
|
|
|
57
60
|
|
|
58
61
|
|
|
59
62
|
@click.group()
|
|
60
|
-
@click.version_option(version="0.1.
|
|
63
|
+
@click.version_option(version="0.1.3")
|
|
61
64
|
@click.option("-v", "--verbose", is_flag=True, help="Enable verbose (DEBUG) logging.")
|
|
62
65
|
def cli(verbose: bool) -> None:
|
|
63
66
|
"""OpenCommand - all-in-one local model runner & command center."""
|
|
@@ -145,6 +148,61 @@ def _build_rebuild_command(cmake_args: str) -> tuple:
|
|
|
145
148
|
return base, False, dict(os.environ, CMAKE_ARGS=cmake_args)
|
|
146
149
|
|
|
147
150
|
|
|
151
|
+
@cli.command()
|
|
152
|
+
@click.argument("path", required=False)
|
|
153
|
+
def config(path: str | None) -> None:
|
|
154
|
+
"""Interactive menu to set advanced swarm settings (persisted to config.json)."""
|
|
155
|
+
from .modules.tools.desktop_safety import DesktopPolicy
|
|
156
|
+
|
|
157
|
+
cfg = _load_cfg(path)
|
|
158
|
+
click.echo(click.style("OpenCommand configuration", fg="cyan", bold=True))
|
|
159
|
+
click.echo(f"Workspace: {cfg.workspace}\n")
|
|
160
|
+
|
|
161
|
+
cfg.vision = click.confirm("Enable vision / desktop tool?", default=cfg.vision)
|
|
162
|
+
cfg.vision_model = click.prompt(
|
|
163
|
+
"Vision model role", type=click.Choice(["vision", "vision_small"]),
|
|
164
|
+
default=cfg.vision_model)
|
|
165
|
+
cfg.max_workers = click.prompt(
|
|
166
|
+
"Max workers (concurrent model pool size)", type=int, default=cfg.max_workers)
|
|
167
|
+
cfg.batch_size = click.prompt(
|
|
168
|
+
"Workers per swarm batch", type=int, default=cfg.batch_size)
|
|
169
|
+
cfg.max_retries = click.prompt(
|
|
170
|
+
"Max retries per worker", type=int, default=cfg.max_retries)
|
|
171
|
+
cfg.pipeline = click.prompt(
|
|
172
|
+
"Pipeline", type=click.Choice(["standard", "advanced"]), default=cfg.pipeline)
|
|
173
|
+
cfg.live_verify = click.confirm(
|
|
174
|
+
"Enable live-verify (interactive desktop check after tests pass)?",
|
|
175
|
+
default=cfg.live_verify)
|
|
176
|
+
cfg.max_iterations = click.prompt(
|
|
177
|
+
"Max automatic-mode iterations", type=int, default=cfg.max_iterations)
|
|
178
|
+
cfg.max_research_loops = click.prompt(
|
|
179
|
+
"Max research refinement loops / phase", type=int, default=cfg.max_research_loops)
|
|
180
|
+
cfg.max_test_loops = click.prompt(
|
|
181
|
+
"Max test+debug fix loops / phase", type=int, default=cfg.max_test_loops)
|
|
182
|
+
cfg.n_gpu_layers = click.prompt(
|
|
183
|
+
"GPU layers to offload (0=CPU, -1=all)", type=int, default=cfg.n_gpu_layers)
|
|
184
|
+
cfg.goal_file = click.prompt(
|
|
185
|
+
"Session goal spec file name", default=cfg.goal_file)
|
|
186
|
+
|
|
187
|
+
# --- Desktop takeover safety policy ------------------------------------
|
|
188
|
+
click.echo(click.style("\nDesktop takeover safety policy", fg="cyan", bold=True))
|
|
189
|
+
click.echo("Defines exactly what the desktop agent is allowed to do.")
|
|
190
|
+
pol = DesktopPolicy.from_dict(cfg.desktop_policy)
|
|
191
|
+
pol.allow_mouse = click.confirm(" Allow mouse control (click)?", default=pol.allow_mouse)
|
|
192
|
+
pol.allow_keyboard = click.confirm(" Allow keyboard control (key)?", default=pol.allow_keyboard)
|
|
193
|
+
pol.allow_type = click.confirm(" Allow typing text?", default=pol.allow_type)
|
|
194
|
+
pol.require_confirm = click.confirm(
|
|
195
|
+
" Require confirmation for risky-but-permitted actions?", default=pol.require_confirm)
|
|
196
|
+
pol.max_steps = click.prompt(
|
|
197
|
+
" Max desktop interaction steps per run", type=int, default=pol.max_steps)
|
|
198
|
+
pol.pause_seconds = click.prompt(
|
|
199
|
+
" Pause (seconds) between actions", type=float, default=pol.pause_seconds)
|
|
200
|
+
cfg.desktop_policy = pol.to_dict()
|
|
201
|
+
|
|
202
|
+
cfg.save()
|
|
203
|
+
click.echo(click.style(f"\nSaved config -> {cfg._path()}", fg="green"))
|
|
204
|
+
|
|
205
|
+
|
|
148
206
|
@cli.group()
|
|
149
207
|
def models() -> None:
|
|
150
208
|
"""Manage built-in embedded models."""
|
|
@@ -229,6 +287,33 @@ def cron_remove(name: str, path: str | None) -> None:
|
|
|
229
287
|
click.echo(f"Removed cron job '{name}'.")
|
|
230
288
|
|
|
231
289
|
|
|
290
|
+
@cron.command("edit")
|
|
291
|
+
@click.argument("name")
|
|
292
|
+
@click.option("--schedule", default=None, help="New schedule expression.")
|
|
293
|
+
@click.option("--command", default=None, help="New shell command.")
|
|
294
|
+
@click.option("--goal", default=None, help="New swarm goal (replaces command).")
|
|
295
|
+
@click.option("--path", default=None, help="Workspace directory.")
|
|
296
|
+
def cron_edit(name: str, schedule: str | None, command: str | None,
|
|
297
|
+
goal: str | None, path: str | None) -> None:
|
|
298
|
+
"""Edit an existing cron job NAME (change schedule/command/goal)."""
|
|
299
|
+
from .modules.system.cron import CronScheduler
|
|
300
|
+
|
|
301
|
+
if schedule is None and command is None and goal is None:
|
|
302
|
+
click.echo("Nothing to change: pass --schedule, --command, or --goal.")
|
|
303
|
+
return
|
|
304
|
+
cfg = _load_cfg(path)
|
|
305
|
+
cron_file = cfg.workspace / ".opencommand" / "cron.json"
|
|
306
|
+
sched = CronScheduler.load(cron_file)
|
|
307
|
+
try:
|
|
308
|
+
sched.edit(name, schedule=schedule, command=command, goal=goal)
|
|
309
|
+
except KeyError:
|
|
310
|
+
click.echo(f"No cron job named '{name}'.")
|
|
311
|
+
return
|
|
312
|
+
sched.save(cron_file)
|
|
313
|
+
click.echo(f"Updated cron job '{name}'.")
|
|
314
|
+
click.echo(f"Persisted to {cron_file}")
|
|
315
|
+
|
|
316
|
+
|
|
232
317
|
@cron.command("status")
|
|
233
318
|
@click.option("--path", default=None, help="Workspace directory.")
|
|
234
319
|
def cron_status(path: str | None) -> None:
|
|
@@ -274,11 +359,15 @@ def cron_start(path: str | None) -> None:
|
|
|
274
359
|
|
|
275
360
|
|
|
276
361
|
@cli.command()
|
|
277
|
-
@click.argument("goal")
|
|
362
|
+
@click.argument("goal", required=False)
|
|
278
363
|
@click.option("--path", default=None, help="Workspace directory.")
|
|
279
|
-
@click.option("--
|
|
364
|
+
@click.option("--goal-file", default=None,
|
|
365
|
+
help="Path to a Goal.md spec defining the intended state/goal. "
|
|
366
|
+
"If omitted, the configured goal_file in the workspace is used.")
|
|
367
|
+
@click.option("--max-iter", default=None, type=int, help="Automatic-mode iterations.")
|
|
280
368
|
@click.option("--verify", default=None, help="Shell command to verify success (e.g. 'pytest -q').")
|
|
281
369
|
@click.option("--pool", default=None, type=int, help="Worker model pool size (parallelism).")
|
|
370
|
+
@click.option("--batch-size", default=None, type=int, help="Workers spawned per swarm batch.")
|
|
282
371
|
@click.option("--no-vision", is_flag=True, help="Disable the desktop/vision tool.")
|
|
283
372
|
@click.option("--vision-model", default=None,
|
|
284
373
|
type=click.Choice(["vision", "vision_small"]),
|
|
@@ -290,10 +379,16 @@ def cron_start(path: str | None) -> None:
|
|
|
290
379
|
"(Phase 6: scaffold + research-context + commander review gate).")
|
|
291
380
|
@click.option("--live-verify", is_flag=True,
|
|
292
381
|
help="After tests pass, run the desktop tool to interactively verify UI goals.")
|
|
293
|
-
def run(goal: str, path: str | None,
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
382
|
+
def run(goal: str | None, path: str | None, goal_file: str | None, max_iter: int | None,
|
|
383
|
+
verify: str | None, pool: int | None, batch_size: int | None,
|
|
384
|
+
no_vision: bool, vision_model: str | None, pipeline: str | None,
|
|
385
|
+
live_verify: bool) -> None:
|
|
386
|
+
"""Run a GOAL autonomously using embedded models.
|
|
387
|
+
|
|
388
|
+
The GOAL may be passed inline, or defined in a Goal.md spec file (see
|
|
389
|
+
--goal-file) that describes the intended end state / requirements for the
|
|
390
|
+
session. When a goal file is used, its contents become the goal.
|
|
391
|
+
"""
|
|
297
392
|
import asyncio
|
|
298
393
|
|
|
299
394
|
from .modules.system.automatic import run_automatic
|
|
@@ -308,6 +403,23 @@ def run(goal: str, path: str | None, max_iter: int, verify: str | None,
|
|
|
308
403
|
cfg.pipeline = pipeline
|
|
309
404
|
if live_verify:
|
|
310
405
|
cfg.live_verify = True
|
|
406
|
+
if max_iter is not None:
|
|
407
|
+
cfg.max_iterations = max_iter
|
|
408
|
+
if batch_size is not None:
|
|
409
|
+
cfg.batch_size = batch_size
|
|
410
|
+
|
|
411
|
+
# Resolve the goal: inline arg wins; otherwise load from a Goal.md spec.
|
|
412
|
+
goal_path = Path(goal_file or cfg.goal_file)
|
|
413
|
+
if goal is None:
|
|
414
|
+
if not goal_path.is_absolute():
|
|
415
|
+
goal_path = cfg.workspace / goal_path
|
|
416
|
+
if not goal_path.exists():
|
|
417
|
+
raise click.UsageError(
|
|
418
|
+
f"No goal provided and no spec file found at {goal_path}. "
|
|
419
|
+
"Pass a GOAL argument or create a Goal.md spec.")
|
|
420
|
+
goal = goal_path.read_text(encoding="utf-8").strip()
|
|
421
|
+
click.echo(click.style(f"Loaded goal spec: {goal_path}", fg="green"))
|
|
422
|
+
|
|
311
423
|
engine = Engine(model_dir=cfg.workspace / "models",
|
|
312
424
|
pool_size=pool or cfg.max_workers)
|
|
313
425
|
sup = Supervisor(max_retries=cfg.max_retries)
|
|
@@ -320,8 +432,9 @@ def run(goal: str, path: str | None, max_iter: int, verify: str | None,
|
|
|
320
432
|
cron = CronScheduler.load(cron_file)
|
|
321
433
|
if cron._jobs:
|
|
322
434
|
cron.on_goal = lambda g: run_automatic(
|
|
323
|
-
commander, g, max_iterations=
|
|
324
|
-
pipeline=cfg.pipeline, live_verify=cfg.live_verify
|
|
435
|
+
commander, g, max_iterations=cfg.max_iterations, verify_cmd=verify,
|
|
436
|
+
pipeline=cfg.pipeline, live_verify=cfg.live_verify,
|
|
437
|
+
batch_size=cfg.batch_size)
|
|
325
438
|
click.echo(click.style(f"Loaded {len(cron._jobs)} cron job(s).", fg="green"))
|
|
326
439
|
|
|
327
440
|
async def _main() -> str:
|
|
@@ -329,8 +442,9 @@ def run(goal: str, path: str | None, max_iter: int, verify: str | None,
|
|
|
329
442
|
cron.start()
|
|
330
443
|
try:
|
|
331
444
|
return await run_automatic(
|
|
332
|
-
commander, goal, max_iterations=
|
|
333
|
-
pipeline=cfg.pipeline, live_verify=cfg.live_verify
|
|
445
|
+
commander, goal, max_iterations=cfg.max_iterations, verify_cmd=verify,
|
|
446
|
+
pipeline=cfg.pipeline, live_verify=cfg.live_verify,
|
|
447
|
+
batch_size=cfg.batch_size)
|
|
334
448
|
finally:
|
|
335
449
|
cron.stop()
|
|
336
450
|
sup.shutdown()
|
|
@@ -338,6 +452,8 @@ def run(goal: str, path: str | None, max_iter: int, verify: str | None,
|
|
|
338
452
|
click.echo(f"OpenCommand on {describe()}")
|
|
339
453
|
click.echo(click.style(f"Pipeline: {cfg.pipeline}"
|
|
340
454
|
+ (" + live-verify" if cfg.live_verify else ""), fg="cyan"))
|
|
455
|
+
click.echo(click.style(f"Batch size: {cfg.batch_size} Pool: {pool or cfg.max_workers}",
|
|
456
|
+
fg="cyan"))
|
|
341
457
|
click.echo(click.style("Loading embedded models...", fg="cyan"))
|
|
342
458
|
click.echo(click.style("Running automatic mode...", fg="cyan"))
|
|
343
459
|
report = asyncio.run(_main())
|
|
@@ -17,6 +17,15 @@ class OpenCommandConfig:
|
|
|
17
17
|
max_retries: int = 3
|
|
18
18
|
pipeline: str = "standard" # standard | advanced (Phase 6)
|
|
19
19
|
live_verify: bool = False # interactive desktop check after verify passes
|
|
20
|
+
# --- Advanced settings (config menu / Goal.md) -------------------------
|
|
21
|
+
max_iterations: int = 5 # outer automatic-mode iterations
|
|
22
|
+
max_research_loops: int = 3 # research refinement loops per phase
|
|
23
|
+
max_test_loops: int = 5 # test+debug fix loops per phase
|
|
24
|
+
batch_size: int = 4 # workers spawned per swarm batch
|
|
25
|
+
n_gpu_layers: int = 0 # 0 = CPU; -1 = all GPU layers
|
|
26
|
+
goal_file: str = "Goal.md" # session goal spec file name
|
|
27
|
+
# --- Desktop takeover safety policy (see modules/tools/desktop_safety.py) -
|
|
28
|
+
desktop_policy: dict = field(default_factory=dict) # empty -> safe defaults
|
|
20
29
|
|
|
21
30
|
def save(self) -> None:
|
|
22
31
|
self._path().parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -32,6 +41,13 @@ class OpenCommandConfig:
|
|
|
32
41
|
"max_retries": self.max_retries,
|
|
33
42
|
"pipeline": self.pipeline,
|
|
34
43
|
"live_verify": self.live_verify,
|
|
44
|
+
"max_iterations": self.max_iterations,
|
|
45
|
+
"max_research_loops": self.max_research_loops,
|
|
46
|
+
"max_test_loops": self.max_test_loops,
|
|
47
|
+
"batch_size": self.batch_size,
|
|
48
|
+
"n_gpu_layers": self.n_gpu_layers,
|
|
49
|
+
"goal_file": self.goal_file,
|
|
50
|
+
"desktop_policy": self.desktop_policy,
|
|
35
51
|
}, indent=2)
|
|
36
52
|
|
|
37
53
|
@classmethod
|
|
@@ -49,6 +65,13 @@ class OpenCommandConfig:
|
|
|
49
65
|
max_retries=d.get("max_retries", 3),
|
|
50
66
|
pipeline=d.get("pipeline", "standard"),
|
|
51
67
|
live_verify=d.get("live_verify", False),
|
|
68
|
+
max_iterations=d.get("max_iterations", 5),
|
|
69
|
+
max_research_loops=d.get("max_research_loops", 3),
|
|
70
|
+
max_test_loops=d.get("max_test_loops", 5),
|
|
71
|
+
batch_size=d.get("batch_size", 4),
|
|
72
|
+
n_gpu_layers=d.get("n_gpu_layers", 0),
|
|
73
|
+
goal_file=d.get("goal_file", "Goal.md"),
|
|
74
|
+
desktop_policy=d.get("desktop_policy", {}),
|
|
52
75
|
)
|
|
53
76
|
|
|
54
77
|
def _path(self) -> Path:
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: desktop-linux
|
|
3
|
+
category: desktop-safety
|
|
4
|
+
updated: 2026-07-12
|
|
5
|
+
tags: [desktop, linux, safety, pyautogui]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Desktop Agent — Safe Operation on Linux
|
|
9
|
+
|
|
10
|
+
Guidance for the OpenCommand desktop takeover agent when driving a **Linux**
|
|
11
|
+
host (X11/Wayland). The goal is to accomplish UI goals without destabilizing
|
|
12
|
+
the session.
|
|
13
|
+
|
|
14
|
+
## Hard rules (enforced by policy, do not bypass)
|
|
15
|
+
- Never trigger `Ctrl+Alt+Del`/`Ctrl+Alt+Backspace` (session kill),
|
|
16
|
+
`Ctrl+Alt+F1..F12` (VT switch), or `Super+L` (lock) unless the goal requires it.
|
|
17
|
+
- Never type destructive shell commands: `rm -rf`, `dd if=`, `mkfs`, `shutdown`,
|
|
18
|
+
`reboot`, `:(){:|:&};:` (fork bomb), or anything piped to `| sh` / `| bash`.
|
|
19
|
+
- Do not escalate to root (no `sudo`, no `pkexec`). If a polkit prompt appears,
|
|
20
|
+
STOP and report.
|
|
21
|
+
|
|
22
|
+
## pyautogui on Linux
|
|
23
|
+
- `pyautogui.FAILSAFE = True` is always on: move the mouse to the top-left
|
|
24
|
+
corner to abort. Respect that escape hatch.
|
|
25
|
+
- Screenshot support needs `scrot` (or `gnome-screenshot`); install it if
|
|
26
|
+
`locateOnScreen` is needed. Wayland may require `gnome-screenshot`/`grim`
|
|
27
|
+
instead of `scrot`.
|
|
28
|
+
- `type()` targets the focused window. Click into the field first and confirm
|
|
29
|
+
focus before typing.
|
|
30
|
+
- Prefer `click(x, y)` via image matching over blind key combos.
|
|
31
|
+
|
|
32
|
+
## Safe interaction patterns
|
|
33
|
+
1. Capture screen → locate the target control (image match or window title).
|
|
34
|
+
2. Click the control; `wait(1)` for the UI to settle.
|
|
35
|
+
3. For text entry, click the field, then `type()` a small specific string.
|
|
36
|
+
4. Verify with another capture before `done`.
|
|
37
|
+
|
|
38
|
+
## Verification before `done`
|
|
39
|
+
- Confirm the intended window/state is visible and no error dialog is open.
|
|
40
|
+
- If an unexpected confirmation/save dialog appeared, report it rather than
|
|
41
|
+
dismissing system dialogs blindly.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: desktop-macos
|
|
3
|
+
category: desktop-safety
|
|
4
|
+
updated: 2026-07-12
|
|
5
|
+
tags: [desktop, macos, safety, pyautogui]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Desktop Agent — Safe Operation on macOS
|
|
9
|
+
|
|
10
|
+
Guidance for the OpenCommand desktop takeover agent when driving a **macOS**
|
|
11
|
+
host. The goal is to accomplish UI goals without destabilizing the session.
|
|
12
|
+
|
|
13
|
+
## Hard rules (enforced by policy, do not bypass)
|
|
14
|
+
- Never trigger `Cmd+Q` (quit all / app), `Cmd+Shift+Q` (log out),
|
|
15
|
+
`Cmd+Ctrl+Power` (force restart), `Ctrl+Power` (dialog), or `Cmd+Option+Esc`
|
|
16
|
+
(force quit) unless the goal explicitly requires it.
|
|
17
|
+
- Never type destructive shell commands: `rm -rf`, `sudo`, `diskutil erase`,
|
|
18
|
+
`shutdown`, `reboot`, or anything piped to `| sh` / `| bash`.
|
|
19
|
+
- Do not approve a Touch ID / admin password prompt on the agent's behalf. If
|
|
20
|
+
one appears, STOP and report.
|
|
21
|
+
|
|
22
|
+
## pyautogui on macOS
|
|
23
|
+
- `pyautogui.FAILSAFE = True` is always on: move the mouse to the top-left
|
|
24
|
+
corner to abort. Respect that escape hatch.
|
|
25
|
+
- macOS key names differ: use `command` (not `cmd`), `option` (not `alt`),
|
|
26
|
+
`control`, `shift`, `return`, `escape`, `space`, `delete` (backspace),
|
|
27
|
+
`backspace` (forward delete). The policy normalizes these.
|
|
28
|
+
- `type()` targets the focused window. Click into the field first and confirm
|
|
29
|
+
focus before typing. Note `typewrite` is the underlying call; accented
|
|
30
|
+
characters may need explicit key names.
|
|
31
|
+
- Prefer `click(x, y)` via image matching over blind key combos.
|
|
32
|
+
|
|
33
|
+
## Safe interaction patterns
|
|
34
|
+
1. Capture screen → locate the target control (image match or window title).
|
|
35
|
+
2. Click the control; `wait(1)` for the UI to settle.
|
|
36
|
+
3. For text entry, click the field, then `type()` a small specific string.
|
|
37
|
+
4. Verify with another capture before `done`.
|
|
38
|
+
|
|
39
|
+
## Verification before `done`
|
|
40
|
+
- Confirm the intended window/state is visible and no error dialog is open.
|
|
41
|
+
- If an unexpected confirmation/save dialog appeared, report it rather than
|
|
42
|
+
dismissing system dialogs blindly.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: desktop-windows
|
|
3
|
+
category: desktop-safety
|
|
4
|
+
updated: 2026-07-12
|
|
5
|
+
tags: [desktop, windows, safety, pyautogui]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Desktop Agent — Safe Operation on Windows
|
|
9
|
+
|
|
10
|
+
Guidance for the OpenCommand desktop takeover agent when driving a **Windows**
|
|
11
|
+
host. The goal is to accomplish UI goals without ever destabilizing the system.
|
|
12
|
+
|
|
13
|
+
## Hard rules (enforced by policy, do not bypass)
|
|
14
|
+
- Never trigger `Win+L` (lock), `Win+R` (Run), `Win+X` (admin menu), `Win+I`
|
|
15
|
+
(Settings), `Alt+F4` on the desktop/shell, or `Ctrl+Alt+Del`.
|
|
16
|
+
- Never type destructive shell commands: `rmdir /s`, `del /q`, `format`,
|
|
17
|
+
`shutdown`, `deltree`, or anything piped to `iex`/`powershell -enc`.
|
|
18
|
+
- Never open an admin/UAC prompt on the agent's own behalf. If a UAC dialog
|
|
19
|
+
appears, STOP and report — do not click "Yes".
|
|
20
|
+
|
|
21
|
+
## pyautogui on Windows
|
|
22
|
+
- `pyautogui.FAILSAFE = True` is always on: a human can slam the mouse into the
|
|
23
|
+
top-left corner to abort the agent instantly. Respect that escape hatch.
|
|
24
|
+
- `pyautogui.PAUSE` is set (default 0.5s) so actions are paced and interruptible.
|
|
25
|
+
- Use `click(x, y)` with coordinates from `locateOnScreen`/`locateCenterOnScreen`
|
|
26
|
+
rather than blind `key` combos wherever possible.
|
|
27
|
+
- Prefer `pyautogui.hotkey('ctrl', 'c')` over `key('ctrl+c')` for combos; the
|
|
28
|
+
policy only permits safe combos.
|
|
29
|
+
- `type()` writes to whatever has focus — only type after you have clicked into
|
|
30
|
+
the correct field and confirmed focus.
|
|
31
|
+
|
|
32
|
+
## Safe interaction patterns
|
|
33
|
+
1. Capture screen → identify the target control by image match or by reading
|
|
34
|
+
the window.
|
|
35
|
+
2. Click the control (single left click). Wait for the UI to settle (use
|
|
36
|
+
`wait(1)`).
|
|
37
|
+
3. If text entry is needed, click the field first, then `type()` a small,
|
|
38
|
+
specific string. Avoid pasting long shell one-liners.
|
|
39
|
+
4. Verify the result with another screen capture before declaring done.
|
|
40
|
+
|
|
41
|
+
## Verification before `done`
|
|
42
|
+
- Confirm the intended window/state is visible and no error dialog is open.
|
|
43
|
+
- If a confirmation or save dialog appeared unexpectedly, report it; do not
|
|
44
|
+
dismiss system dialogs blindly.
|
|
@@ -28,12 +28,13 @@ log = get_logger("system.automatic")
|
|
|
28
28
|
|
|
29
29
|
async def run_automatic(commander: Commander, goal: str, max_iterations: int = 5,
|
|
30
30
|
verify_cmd: str | None = None, pipeline: str = "standard",
|
|
31
|
-
live_verify: bool = False) -> str:
|
|
31
|
+
live_verify: bool = False, batch_size: int = 4) -> str:
|
|
32
32
|
"""Iteratively execute the goal, auto-fixing failures, until 100% met.
|
|
33
33
|
|
|
34
34
|
``pipeline`` is "standard" or "advanced" (scaffold + research loop +
|
|
35
35
|
commander review gate + test/debug loop). ``live_verify`` runs the desktop
|
|
36
36
|
tool as a final interactive check when the goal is UI/UX facing.
|
|
37
|
+
``batch_size`` is the number of workers spawned per swarm batch.
|
|
37
38
|
|
|
38
39
|
The loop only returns success when BOTH the exit-code verify gate AND the
|
|
39
40
|
Commander review gate pass (and, if enabled, the live-verify check). If the
|
|
@@ -47,10 +48,11 @@ async def run_automatic(commander: Commander, goal: str, max_iterations: int = 5
|
|
|
47
48
|
tags=["rocket", "opencommand"])
|
|
48
49
|
last = ""
|
|
49
50
|
for i in range(max_iterations):
|
|
50
|
-
log.info("iteration=%d/%d goal=%r pipeline=%s", i + 1,
|
|
51
|
-
original_goal, pipeline)
|
|
51
|
+
log.info("iteration=%d/%d goal=%r pipeline=%s batch=%d", i + 1,
|
|
52
|
+
max_iterations, original_goal, pipeline, batch_size)
|
|
52
53
|
last = await _execute(commander, original_goal, pipeline,
|
|
53
|
-
verify_cmd=verify_cmd, live_verify=live_verify
|
|
54
|
+
verify_cmd=verify_cmd, live_verify=live_verify,
|
|
55
|
+
batch_size=batch_size)
|
|
54
56
|
# Exit-code gate (real verification, not string matching).
|
|
55
57
|
if verify_cmd:
|
|
56
58
|
rc, out = _verify(commander, verify_cmd)
|
|
@@ -98,11 +100,13 @@ async def run_automatic(commander: Commander, goal: str, max_iterations: int = 5
|
|
|
98
100
|
|
|
99
101
|
|
|
100
102
|
async def _execute(commander: Commander, goal: str, pipeline: str,
|
|
101
|
-
verify_cmd: str | None = None, live_verify: bool = False
|
|
103
|
+
verify_cmd: str | None = None, live_verify: bool = False,
|
|
104
|
+
batch_size: int = 4) -> str:
|
|
102
105
|
if pipeline == "advanced":
|
|
103
106
|
return await commander.execute_advanced(
|
|
104
|
-
goal, verify_cmd=verify_cmd, live_verify=live_verify
|
|
105
|
-
|
|
107
|
+
goal, verify_cmd=verify_cmd, live_verify=live_verify,
|
|
108
|
+
batch_size=batch_size)
|
|
109
|
+
return await commander.execute(goal, batch_size=batch_size)
|
|
106
110
|
|
|
107
111
|
|
|
108
112
|
def _fix_prompt(original_goal: str, last: str) -> str:
|
|
@@ -131,6 +131,25 @@ class CronScheduler:
|
|
|
131
131
|
job.goal = goal
|
|
132
132
|
return job
|
|
133
133
|
|
|
134
|
+
def edit(self, name: str, schedule: str | None = None,
|
|
135
|
+
command: str | None = None, goal: str | None = None) -> CronJob:
|
|
136
|
+
"""Edit an existing job's schedule/command/goal in place.
|
|
137
|
+
|
|
138
|
+
Returns the updated job, or raises KeyError if no such job exists.
|
|
139
|
+
At least one of schedule/command/goal must be provided.
|
|
140
|
+
"""
|
|
141
|
+
for job in self._jobs:
|
|
142
|
+
if job.name == name:
|
|
143
|
+
if schedule is not None:
|
|
144
|
+
job.schedule = schedule
|
|
145
|
+
job._parse()
|
|
146
|
+
if command is not None:
|
|
147
|
+
job.command = command
|
|
148
|
+
if goal is not None:
|
|
149
|
+
job.goal = goal
|
|
150
|
+
return job
|
|
151
|
+
raise KeyError(f"No cron job named {name!r}")
|
|
152
|
+
|
|
134
153
|
def save(self, path: Path) -> None:
|
|
135
154
|
"""Persist serializable jobs to <workspace>/.opencommand/cron.json."""
|
|
136
155
|
path.parent.mkdir(parents=True, exist_ok=True)
|