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.
Files changed (43) hide show
  1. {opencommand-0.1.1 → opencommand-0.1.3}/PKG-INFO +16 -1
  2. {opencommand-0.1.1 → opencommand-0.1.3}/README.md +14 -0
  3. {opencommand-0.1.1 → opencommand-0.1.3}/pyproject.toml +2 -1
  4. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/cli.py +128 -12
  5. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/core/config.py +23 -0
  6. opencommand-0.1.3/src/opencommand/modules/skills/desktop_linux.md +41 -0
  7. opencommand-0.1.3/src/opencommand/modules/skills/desktop_macos.md +42 -0
  8. opencommand-0.1.3/src/opencommand/modules/skills/desktop_windows.md +44 -0
  9. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/system/automatic.py +11 -7
  10. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/system/cron.py +19 -0
  11. opencommand-0.1.3/src/opencommand/modules/tools/desktop.py +202 -0
  12. opencommand-0.1.3/src/opencommand/modules/tools/desktop_safety.py +162 -0
  13. opencommand-0.1.3/src/opencommand/modules/tools/system.py +285 -0
  14. opencommand-0.1.3/src/opencommand/modules/tools/yara_rules/default.yar +61 -0
  15. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/swarm/agents/commander.py +23 -4
  16. opencommand-0.1.1/src/opencommand/modules/tools/desktop.py +0 -127
  17. opencommand-0.1.1/src/opencommand/modules/tools/system.py +0 -163
  18. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/__init__.py +0 -0
  19. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/core/agent.py +0 -0
  20. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/core/errors.py +0 -0
  21. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/core/logging.py +0 -0
  22. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/core/supervisor.py +0 -0
  23. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/core/tools.py +0 -0
  24. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/engine/__init__.py +0 -0
  25. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/engine/runner.py +0 -0
  26. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/knowledge.py +0 -0
  27. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/memory/MEMORY.md +0 -0
  28. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/skills/bash.md +0 -0
  29. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/skills/powershell.md +0 -0
  30. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/skills/python.md +0 -0
  31. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/skills/system.md +0 -0
  32. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/system/notify.py +0 -0
  33. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/system/platform.py +0 -0
  34. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/templates.py +0 -0
  35. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/tools/files.py +0 -0
  36. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/tools/instructions.py +0 -0
  37. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/tools/memory.py +0 -0
  38. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/tools/playwright.py +0 -0
  39. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/tools/research.py +0 -0
  40. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/modules/tools/terminal.py +0 -0
  41. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/providers/provider.py +0 -0
  42. {opencommand-0.1.1 → opencommand-0.1.3}/src/opencommand/swarm/agents/worker.py +0 -0
  43. {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.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.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
- reg.register(DesktopTool(engine.runner(vision_role) if vision_role else None))
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.0")
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("--max-iter", default=5, help="Automatic-mode iterations.")
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, max_iter: int, verify: str | None,
294
- pool: int | None, no_vision: bool, vision_model: str | None,
295
- pipeline: str | None, live_verify: bool) -> None:
296
- """Run a GOAL autonomously using embedded models."""
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=max_iter, verify_cmd=verify,
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=max_iter, verify_cmd=verify,
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, max_iterations,
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) -> str:
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
- return await commander.execute(goal)
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)