opencommand 0.1.1__tar.gz → 0.1.2__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 (36) hide show
  1. {opencommand-0.1.1 → opencommand-0.1.2}/PKG-INFO +15 -1
  2. {opencommand-0.1.1 → opencommand-0.1.2}/README.md +14 -0
  3. {opencommand-0.1.1 → opencommand-0.1.2}/pyproject.toml +1 -1
  4. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/cli.py +80 -11
  5. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/core/config.py +19 -0
  6. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/modules/system/automatic.py +11 -7
  7. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/swarm/agents/commander.py +23 -4
  8. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/__init__.py +0 -0
  9. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/core/agent.py +0 -0
  10. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/core/errors.py +0 -0
  11. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/core/logging.py +0 -0
  12. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/core/supervisor.py +0 -0
  13. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/core/tools.py +0 -0
  14. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/engine/__init__.py +0 -0
  15. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/engine/runner.py +0 -0
  16. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/modules/knowledge.py +0 -0
  17. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/modules/memory/MEMORY.md +0 -0
  18. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/modules/skills/bash.md +0 -0
  19. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/modules/skills/powershell.md +0 -0
  20. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/modules/skills/python.md +0 -0
  21. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/modules/skills/system.md +0 -0
  22. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/modules/system/cron.py +0 -0
  23. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/modules/system/notify.py +0 -0
  24. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/modules/system/platform.py +0 -0
  25. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/modules/templates.py +0 -0
  26. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/modules/tools/desktop.py +0 -0
  27. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/modules/tools/files.py +0 -0
  28. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/modules/tools/instructions.py +0 -0
  29. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/modules/tools/memory.py +0 -0
  30. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/modules/tools/playwright.py +0 -0
  31. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/modules/tools/research.py +0 -0
  32. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/modules/tools/system.py +0 -0
  33. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/modules/tools/terminal.py +0 -0
  34. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/providers/provider.py +0 -0
  35. {opencommand-0.1.1 → opencommand-0.1.2}/src/opencommand/swarm/agents/worker.py +0 -0
  36. {opencommand-0.1.1 → opencommand-0.1.2}/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.2
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>
@@ -55,12 +55,26 @@ playwright install # optional, for the playwright tool
55
55
  ```bash
56
56
  opencommand models list # list built-in embedded models
57
57
  opencommand models pull all # download GGUF models into ./models
58
+ opencommand config # interactive menu: workers, batch size, GPU, etc.
58
59
  opencommand run "Add unit tests for the engine module."
60
+ opencommand run --goal-file Goal.md # run from a Goal.md spec (intended state)
59
61
  opencommand run "Build a Panda3D FPS" --pipeline advanced --verify "uv run pytest -q"
60
62
  opencommand tui # live swarm dashboard
61
63
  opencommand cron add healthcheck "every 1h" "echo ok"
62
64
  ```
63
65
 
66
+ ### Goal.md spec
67
+
68
+ Instead of an inline goal, point `run` at a `Goal.md` file that defines the
69
+ intended end state / requirements for the session. Its contents become the goal
70
+ fed to the swarm (and the advanced pipeline's review gate checks against it):
71
+
72
+ ```bash
73
+ opencommand run --goal-file Goal.md
74
+ ```
75
+
76
+ The file name is configurable via `opencommand config` (default `Goal.md`).
77
+
64
78
  ## Built-in models
65
79
 
66
80
  | 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.2"
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 = [
@@ -57,7 +57,7 @@ def _load_cfg(path: str | None) -> OpenCommandConfig:
57
57
 
58
58
 
59
59
  @click.group()
60
- @click.version_option(version="0.1.0")
60
+ @click.version_option(version="0.1.1")
61
61
  @click.option("-v", "--verbose", is_flag=True, help="Enable verbose (DEBUG) logging.")
62
62
  def cli(verbose: bool) -> None:
63
63
  """OpenCommand - all-in-one local model runner & command center."""
@@ -145,6 +145,44 @@ def _build_rebuild_command(cmake_args: str) -> tuple:
145
145
  return base, False, dict(os.environ, CMAKE_ARGS=cmake_args)
146
146
 
147
147
 
148
+ @cli.command()
149
+ @click.argument("path", required=False)
150
+ def config(path: str | None) -> None:
151
+ """Interactive menu to set advanced swarm settings (persisted to config.json)."""
152
+ cfg = _load_cfg(path)
153
+ click.echo(click.style("OpenCommand configuration", fg="cyan", bold=True))
154
+ click.echo(f"Workspace: {cfg.workspace}\n")
155
+
156
+ cfg.vision = click.confirm("Enable vision / desktop tool?", default=cfg.vision)
157
+ cfg.vision_model = click.prompt(
158
+ "Vision model role", type=click.Choice(["vision", "vision_small"]),
159
+ default=cfg.vision_model)
160
+ cfg.max_workers = click.prompt(
161
+ "Max workers (concurrent model pool size)", type=int, default=cfg.max_workers)
162
+ cfg.batch_size = click.prompt(
163
+ "Workers per swarm batch", type=int, default=cfg.batch_size)
164
+ cfg.max_retries = click.prompt(
165
+ "Max retries per worker", type=int, default=cfg.max_retries)
166
+ cfg.pipeline = click.prompt(
167
+ "Pipeline", type=click.Choice(["standard", "advanced"]), default=cfg.pipeline)
168
+ cfg.live_verify = click.confirm(
169
+ "Enable live-verify (interactive desktop check after tests pass)?",
170
+ default=cfg.live_verify)
171
+ cfg.max_iterations = click.prompt(
172
+ "Max automatic-mode iterations", type=int, default=cfg.max_iterations)
173
+ cfg.max_research_loops = click.prompt(
174
+ "Max research refinement loops / phase", type=int, default=cfg.max_research_loops)
175
+ cfg.max_test_loops = click.prompt(
176
+ "Max test+debug fix loops / phase", type=int, default=cfg.max_test_loops)
177
+ cfg.n_gpu_layers = click.prompt(
178
+ "GPU layers to offload (0=CPU, -1=all)", type=int, default=cfg.n_gpu_layers)
179
+ cfg.goal_file = click.prompt(
180
+ "Session goal spec file name", default=cfg.goal_file)
181
+
182
+ cfg.save()
183
+ click.echo(click.style(f"\nSaved config -> {cfg._path()}", fg="green"))
184
+
185
+
148
186
  @cli.group()
149
187
  def models() -> None:
150
188
  """Manage built-in embedded models."""
@@ -274,11 +312,15 @@ def cron_start(path: str | None) -> None:
274
312
 
275
313
 
276
314
  @cli.command()
277
- @click.argument("goal")
315
+ @click.argument("goal", required=False)
278
316
  @click.option("--path", default=None, help="Workspace directory.")
279
- @click.option("--max-iter", default=5, help="Automatic-mode iterations.")
317
+ @click.option("--goal-file", default=None,
318
+ help="Path to a Goal.md spec defining the intended state/goal. "
319
+ "If omitted, the configured goal_file in the workspace is used.")
320
+ @click.option("--max-iter", default=None, type=int, help="Automatic-mode iterations.")
280
321
  @click.option("--verify", default=None, help="Shell command to verify success (e.g. 'pytest -q').")
281
322
  @click.option("--pool", default=None, type=int, help="Worker model pool size (parallelism).")
323
+ @click.option("--batch-size", default=None, type=int, help="Workers spawned per swarm batch.")
282
324
  @click.option("--no-vision", is_flag=True, help="Disable the desktop/vision tool.")
283
325
  @click.option("--vision-model", default=None,
284
326
  type=click.Choice(["vision", "vision_small"]),
@@ -290,10 +332,16 @@ def cron_start(path: str | None) -> None:
290
332
  "(Phase 6: scaffold + research-context + commander review gate).")
291
333
  @click.option("--live-verify", is_flag=True,
292
334
  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."""
335
+ def run(goal: str | None, path: str | None, goal_file: str | None, max_iter: int | None,
336
+ verify: str | None, pool: int | None, batch_size: int | None,
337
+ no_vision: bool, vision_model: str | None, pipeline: str | None,
338
+ live_verify: bool) -> None:
339
+ """Run a GOAL autonomously using embedded models.
340
+
341
+ The GOAL may be passed inline, or defined in a Goal.md spec file (see
342
+ --goal-file) that describes the intended end state / requirements for the
343
+ session. When a goal file is used, its contents become the goal.
344
+ """
297
345
  import asyncio
298
346
 
299
347
  from .modules.system.automatic import run_automatic
@@ -308,6 +356,23 @@ def run(goal: str, path: str | None, max_iter: int, verify: str | None,
308
356
  cfg.pipeline = pipeline
309
357
  if live_verify:
310
358
  cfg.live_verify = True
359
+ if max_iter is not None:
360
+ cfg.max_iterations = max_iter
361
+ if batch_size is not None:
362
+ cfg.batch_size = batch_size
363
+
364
+ # Resolve the goal: inline arg wins; otherwise load from a Goal.md spec.
365
+ goal_path = Path(goal_file or cfg.goal_file)
366
+ if goal is None:
367
+ if not goal_path.is_absolute():
368
+ goal_path = cfg.workspace / goal_path
369
+ if not goal_path.exists():
370
+ raise click.UsageError(
371
+ f"No goal provided and no spec file found at {goal_path}. "
372
+ "Pass a GOAL argument or create a Goal.md spec.")
373
+ goal = goal_path.read_text(encoding="utf-8").strip()
374
+ click.echo(click.style(f"Loaded goal spec: {goal_path}", fg="green"))
375
+
311
376
  engine = Engine(model_dir=cfg.workspace / "models",
312
377
  pool_size=pool or cfg.max_workers)
313
378
  sup = Supervisor(max_retries=cfg.max_retries)
@@ -320,8 +385,9 @@ def run(goal: str, path: str | None, max_iter: int, verify: str | None,
320
385
  cron = CronScheduler.load(cron_file)
321
386
  if cron._jobs:
322
387
  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)
388
+ commander, g, max_iterations=cfg.max_iterations, verify_cmd=verify,
389
+ pipeline=cfg.pipeline, live_verify=cfg.live_verify,
390
+ batch_size=cfg.batch_size)
325
391
  click.echo(click.style(f"Loaded {len(cron._jobs)} cron job(s).", fg="green"))
326
392
 
327
393
  async def _main() -> str:
@@ -329,8 +395,9 @@ def run(goal: str, path: str | None, max_iter: int, verify: str | None,
329
395
  cron.start()
330
396
  try:
331
397
  return await run_automatic(
332
- commander, goal, max_iterations=max_iter, verify_cmd=verify,
333
- pipeline=cfg.pipeline, live_verify=cfg.live_verify)
398
+ commander, goal, max_iterations=cfg.max_iterations, verify_cmd=verify,
399
+ pipeline=cfg.pipeline, live_verify=cfg.live_verify,
400
+ batch_size=cfg.batch_size)
334
401
  finally:
335
402
  cron.stop()
336
403
  sup.shutdown()
@@ -338,6 +405,8 @@ def run(goal: str, path: str | None, max_iter: int, verify: str | None,
338
405
  click.echo(f"OpenCommand on {describe()}")
339
406
  click.echo(click.style(f"Pipeline: {cfg.pipeline}"
340
407
  + (" + live-verify" if cfg.live_verify else ""), fg="cyan"))
408
+ click.echo(click.style(f"Batch size: {cfg.batch_size} Pool: {pool or cfg.max_workers}",
409
+ fg="cyan"))
341
410
  click.echo(click.style("Loading embedded models...", fg="cyan"))
342
411
  click.echo(click.style("Running automatic mode...", fg="cyan"))
343
412
  report = asyncio.run(_main())
@@ -17,6 +17,13 @@ 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
20
27
 
21
28
  def save(self) -> None:
22
29
  self._path().parent.mkdir(parents=True, exist_ok=True)
@@ -32,6 +39,12 @@ class OpenCommandConfig:
32
39
  "max_retries": self.max_retries,
33
40
  "pipeline": self.pipeline,
34
41
  "live_verify": self.live_verify,
42
+ "max_iterations": self.max_iterations,
43
+ "max_research_loops": self.max_research_loops,
44
+ "max_test_loops": self.max_test_loops,
45
+ "batch_size": self.batch_size,
46
+ "n_gpu_layers": self.n_gpu_layers,
47
+ "goal_file": self.goal_file,
35
48
  }, indent=2)
36
49
 
37
50
  @classmethod
@@ -49,6 +62,12 @@ class OpenCommandConfig:
49
62
  max_retries=d.get("max_retries", 3),
50
63
  pipeline=d.get("pipeline", "standard"),
51
64
  live_verify=d.get("live_verify", False),
65
+ max_iterations=d.get("max_iterations", 5),
66
+ max_research_loops=d.get("max_research_loops", 3),
67
+ max_test_loops=d.get("max_test_loops", 5),
68
+ batch_size=d.get("batch_size", 4),
69
+ n_gpu_layers=d.get("n_gpu_layers", 0),
70
+ goal_file=d.get("goal_file", "Goal.md"),
52
71
  )
53
72
 
54
73
  def _path(self) -> Path:
@@ -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:
@@ -184,7 +184,7 @@ class Commander:
184
184
 
185
185
  # --- Standard pipeline (backward compatible) ---------------------------
186
186
 
187
- async def execute(self, goal: str) -> str:
187
+ async def execute(self, goal: str, batch_size: int = 4) -> str:
188
188
  """Standard pipeline: plan -> research -> workers (no scaffold/review)."""
189
189
  plan = await self.plan(goal)
190
190
  save_note(self.workspace, "goal_plan",
@@ -201,7 +201,7 @@ class Commander:
201
201
  worker_jobs = {f"worker-{i}": self._worker_job(i, t)
202
202
  for i, t in enumerate(phase.workers)}
203
203
  if worker_jobs:
204
- for k, v in (await self.supervisor.run_all(worker_jobs)).items():
204
+ for k, v in (await self._run_batched(worker_jobs, batch_size)).items():
205
205
  report.append(f" [{k}] {v[:300]}")
206
206
  save_note(self.workspace, "goal_report", "\n".join(report), tags=["report"])
207
207
  return "\n".join(report)
@@ -211,7 +211,8 @@ class Commander:
211
211
  async def execute_advanced(self, goal: str, max_research_loops: int = 3,
212
212
  max_test_loops: int = 5,
213
213
  verify_cmd: str | None = None,
214
- live_verify: bool = False) -> str:
214
+ live_verify: bool = False,
215
+ batch_size: int = 4) -> str:
215
216
  """Fully-featured autonomous loop for a GOAL.
216
217
 
217
218
  Per phase:
@@ -257,7 +258,7 @@ class Commander:
257
258
  worker_jobs = {f"worker-{i}": self._worker_job(i, t, context)
258
259
  for i, t in enumerate(phase.workers)}
259
260
  if worker_jobs:
260
- for k, v in (await self.supervisor.run_all(worker_jobs)).items():
261
+ for k, v in (await self._run_batched(worker_jobs, batch_size)).items():
261
262
  report.append(f" [{k}] {v[:300]}")
262
263
  # 4. TEST+DEBUG LOOP: reach a testable state, then fix until green.
263
264
  if verify_cmd:
@@ -469,6 +470,24 @@ class Commander:
469
470
 
470
471
  # --- Helpers -----------------------------------------------------------
471
472
 
473
+ async def _run_batched(self, jobs: dict[str, Callable[[], Awaitable[str]]],
474
+ batch_size: int = 4) -> dict[str, str]:
475
+ """Run jobs in bounded batches so at most ``batch_size`` run at once.
476
+
477
+ Workers share a fixed-size runner pool, so capping concurrency here keeps
478
+ the pool from thrashing and bounds RAM/CPU usage on smaller hosts. Each
479
+ batch is dispatched through the Supervisor (which isolates every job with
480
+ crash containment), so behavior is identical to ``run_all`` but bounded.
481
+ """
482
+ if batch_size <= 0:
483
+ batch_size = 1
484
+ results: dict[str, str] = {}
485
+ items = list(jobs.items())
486
+ for start in range(0, len(items), batch_size):
487
+ batch = dict(items[start:start + batch_size])
488
+ results.update(await self.supervisor.run_all(batch))
489
+ return results
490
+
472
491
  def _scaffold(self, phase: Phase, idx: int) -> str:
473
492
  """Lay the foundation for a phase so workers fill in structure, not fight it.
474
493