loopengt 0.1.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.
Files changed (87) hide show
  1. loopengt/__init__.py +31 -0
  2. loopengt/adapters/__init__.py +1 -0
  3. loopengt/adapters/antigravity/__init__.py +1 -0
  4. loopengt/adapters/antigravity/adapter.py +55 -0
  5. loopengt/adapters/antigravity/commands.py +21 -0
  6. loopengt/adapters/base.py +51 -0
  7. loopengt/adapters/claude_code/__init__.py +1 -0
  8. loopengt/adapters/claude_code/adapter.py +55 -0
  9. loopengt/adapters/claude_code/commands.py +16 -0
  10. loopengt/adapters/codex/__init__.py +1 -0
  11. loopengt/adapters/codex/adapter.py +52 -0
  12. loopengt/adapters/codex/commands.py +16 -0
  13. loopengt/adapters/cursor/__init__.py +1 -0
  14. loopengt/adapters/cursor/adapter.py +56 -0
  15. loopengt/adapters/cursor/commands.py +29 -0
  16. loopengt/adapters/generic/__init__.py +1 -0
  17. loopengt/adapters/generic/terminal.py +82 -0
  18. loopengt/cli/__init__.py +1 -0
  19. loopengt/cli/commands/__init__.py +1 -0
  20. loopengt/cli/commands/design.py +171 -0
  21. loopengt/cli/commands/doctor.py +110 -0
  22. loopengt/cli/commands/eval.py +105 -0
  23. loopengt/cli/commands/init.py +131 -0
  24. loopengt/cli/commands/mcp_serve.py +57 -0
  25. loopengt/cli/commands/run.py +99 -0
  26. loopengt/cli/commands/template.py +145 -0
  27. loopengt/cli/commands/trace.py +114 -0
  28. loopengt/cli/formatters.py +125 -0
  29. loopengt/cli/main.py +66 -0
  30. loopengt/core/__init__.py +1 -0
  31. loopengt/core/evals/__init__.py +1 -0
  32. loopengt/core/evals/judges.py +216 -0
  33. loopengt/core/evals/metrics.py +119 -0
  34. loopengt/core/evals/regression.py +157 -0
  35. loopengt/core/memory/__init__.py +1 -0
  36. loopengt/core/memory/retrieval.py +124 -0
  37. loopengt/core/memory/store.py +184 -0
  38. loopengt/core/memory/summarizer.py +97 -0
  39. loopengt/core/models/__init__.py +43 -0
  40. loopengt/core/models/agent.py +126 -0
  41. loopengt/core/models/loop_spec.py +251 -0
  42. loopengt/core/models/policy.py +131 -0
  43. loopengt/core/models/state.py +271 -0
  44. loopengt/core/models/tool.py +105 -0
  45. loopengt/core/runtime/__init__.py +1 -0
  46. loopengt/core/runtime/checkpoint.py +152 -0
  47. loopengt/core/runtime/executor.py +463 -0
  48. loopengt/core/runtime/handoff.py +139 -0
  49. loopengt/core/runtime/scheduler.py +168 -0
  50. loopengt/core/tracing/__init__.py +1 -0
  51. loopengt/core/tracing/events.py +95 -0
  52. loopengt/core/tracing/exporters.py +158 -0
  53. loopengt/core/tracing/store.py +202 -0
  54. loopengt/mcp/__init__.py +1 -0
  55. loopengt/mcp/client/__init__.py +1 -0
  56. loopengt/mcp/client/manager.py +118 -0
  57. loopengt/mcp/client/tools.py +107 -0
  58. loopengt/mcp/server/__init__.py +1 -0
  59. loopengt/mcp/server/prompts.py +82 -0
  60. loopengt/mcp/server/resources.py +75 -0
  61. loopengt/mcp/server/server.py +50 -0
  62. loopengt/mcp/server/tools.py +214 -0
  63. loopengt/mcp/shared/__init__.py +1 -0
  64. loopengt/mcp/shared/schemas.py +91 -0
  65. loopengt/plugins/__init__.py +1 -0
  66. loopengt/plugins/base.py +90 -0
  67. loopengt/plugins/loader.py +130 -0
  68. loopengt/plugins/manifest.py +70 -0
  69. loopengt/plugins/registry.py +146 -0
  70. loopengt/prompts/LOOPENGT.md +60 -0
  71. loopengt/prompts/__init__.py +1 -0
  72. loopengt/storage/__init__.py +1 -0
  73. loopengt/storage/jsonl.py +84 -0
  74. loopengt/storage/sqlite.py +102 -0
  75. loopengt/templates/__init__.py +1 -0
  76. loopengt/templates/builtins/handoff_loop/LOOPENGS.md +10 -0
  77. loopengt/templates/builtins/planner_executor/LOOPENGS.md +29 -0
  78. loopengt/templates/builtins/research_architect/LOOPENGS.md +17 -0
  79. loopengt/templates/builtins/reviewer_retry/LOOPENGS.md +29 -0
  80. loopengt/templates/builtins/supervisor_workers/LOOPENGS.md +29 -0
  81. loopengt/templates/loader.py +38 -0
  82. loopengt/templates/registry.py +85 -0
  83. loopengt-0.1.0.dist-info/METADATA +275 -0
  84. loopengt-0.1.0.dist-info/RECORD +87 -0
  85. loopengt-0.1.0.dist-info/WHEEL +4 -0
  86. loopengt-0.1.0.dist-info/entry_points.txt +8 -0
  87. loopengt-0.1.0.dist-info/licenses/LICENSE +674 -0
loopengt/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ """Loop Engineering Agent — design, orchestrate, and evaluate agent loops."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = [
7
+ "__version__",
8
+ "LoopSpec",
9
+ "LoopState",
10
+ "AgentRole",
11
+ "ToolSpec",
12
+ "Policy",
13
+ ]
14
+
15
+ # Lazy imports to keep startup fast
16
+ def __getattr__(name: str) -> object:
17
+ """Lazy-load public API symbols."""
18
+ _imports: dict[str, tuple[str, str]] = {
19
+ "LoopSpec": ("loopengt.core.models.loop_spec", "LoopSpec"),
20
+ "LoopState": ("loopengt.core.models.state", "LoopState"),
21
+ "AgentRole": ("loopengt.core.models.agent", "AgentRole"),
22
+ "ToolSpec": ("loopengt.core.models.tool", "ToolSpec"),
23
+ "Policy": ("loopengt.core.models.policy", "Policy"),
24
+ }
25
+ if name in _imports:
26
+ module_path, attr_name = _imports[name]
27
+ import importlib
28
+
29
+ module = importlib.import_module(module_path)
30
+ return getattr(module, attr_name)
31
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1 @@
1
+ """IDE adapters for loop engineering."""
@@ -0,0 +1 @@
1
+ """Antigravity IDE adapter."""
@@ -0,0 +1,55 @@
1
+ """Antigravity IDE adapter — skeleton implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from loopengt.plugins.base import Plugin
8
+ from loopengt.plugins.manifest import PluginManifest
9
+
10
+
11
+ class AntigravityAdapter(Plugin):
12
+ """Adapter for Google Antigravity IDE.
13
+
14
+ Integrates loopengt with Antigravity's MCP tool system and
15
+ skill framework.
16
+
17
+ .. note:: Skeleton — requires Antigravity MCP integration.
18
+ """
19
+
20
+ @property
21
+ def manifest(self) -> PluginManifest:
22
+ return PluginManifest(
23
+ name="antigravity",
24
+ version="0.1.0",
25
+ description="Antigravity IDE adapter for loopengt",
26
+ capabilities=["adapter", "commands", "mcp"],
27
+ entry_points=["loopengt.adapters"],
28
+ )
29
+
30
+ def activate(self, context: dict[str, Any]) -> None:
31
+ self._active = True
32
+
33
+ def deactivate(self) -> None:
34
+ self._active = False
35
+
36
+ def capabilities(self) -> set[str]:
37
+ return {"commands", "mcp", "file_access", "terminal"}
38
+
39
+ def register_commands(self, registry: Any) -> None:
40
+ raise NotImplementedError("Antigravity command registration not yet implemented")
41
+
42
+ def on_loop_start(self, loop_spec: dict[str, Any]) -> None:
43
+ pass
44
+
45
+ def on_step_start(self, step: str, agent: str) -> None:
46
+ pass
47
+
48
+ def on_step_complete(self, step: str, result: Any) -> None:
49
+ pass
50
+
51
+ def on_loop_complete(self, result: Any) -> None:
52
+ pass
53
+
54
+ def on_error(self, error: Exception, context: dict[str, Any]) -> None:
55
+ pass
@@ -0,0 +1,21 @@
1
+ """Antigravity command definitions — placeholder."""
2
+
3
+ from __future__ import annotations
4
+
5
+ ANTIGRAVITY_COMMANDS = [
6
+ {
7
+ "id": "loopengt.design",
8
+ "title": "Design Loop",
9
+ "description": "Design a new agent loop from a goal",
10
+ },
11
+ {
12
+ "id": "loopengt.run",
13
+ "title": "Run Loop",
14
+ "description": "Execute a loop.yaml spec",
15
+ },
16
+ {
17
+ "id": "loopengt.trace",
18
+ "title": "View Trace",
19
+ "description": "Inspect execution trace",
20
+ },
21
+ ]
@@ -0,0 +1,51 @@
1
+ """Adapter protocol — the contract all IDE adapters must implement."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Protocol, runtime_checkable
6
+
7
+
8
+ @runtime_checkable
9
+ class Adapter(Protocol):
10
+ """Protocol for IDE/environment adapters.
11
+
12
+ Each adapter bridges loopengt's runtime with a specific IDE or
13
+ execution environment (terminal, Cursor, Claude Code, etc.).
14
+ """
15
+
16
+ def capabilities(self) -> set[str]:
17
+ """Return the set of capabilities this adapter provides.
18
+
19
+ Standard capabilities include:
20
+ - ``"commands"`` — can register user-facing commands
21
+ - ``"notifications"`` — can show notifications
22
+ - ``"progress"`` — can show progress indicators
23
+ - ``"file_access"`` — can read/write files
24
+ - ``"terminal"`` — has terminal access
25
+ - ``"mcp"`` — supports MCP protocol
26
+ """
27
+ ...
28
+
29
+ def register_commands(self, registry: Any) -> None:
30
+ """Register adapter-specific commands with the given registry."""
31
+ ...
32
+
33
+ def on_loop_start(self, loop_spec: dict[str, Any]) -> None:
34
+ """Called when a loop begins execution."""
35
+ ...
36
+
37
+ def on_step_start(self, step: str, agent: str) -> None:
38
+ """Called when a step begins execution."""
39
+ ...
40
+
41
+ def on_step_complete(self, step: str, result: Any) -> None:
42
+ """Called when a step completes."""
43
+ ...
44
+
45
+ def on_loop_complete(self, result: Any) -> None:
46
+ """Called when the loop finishes."""
47
+ ...
48
+
49
+ def on_error(self, error: Exception, context: dict[str, Any]) -> None:
50
+ """Called when an error occurs during execution."""
51
+ ...
@@ -0,0 +1 @@
1
+ """Claude Code adapter."""
@@ -0,0 +1,55 @@
1
+ """Claude Code adapter — skeleton implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from loopengt.plugins.base import Plugin
8
+ from loopengt.plugins.manifest import PluginManifest
9
+
10
+
11
+ class ClaudeCodeAdapter(Plugin):
12
+ """Adapter for Claude Code (Anthropic's CLI-based coding agent).
13
+
14
+ Integrates loopengt with Claude Code's MCP tool capabilities
15
+ and permission model.
16
+
17
+ .. note:: Skeleton — requires Claude Code's MCP integration.
18
+ """
19
+
20
+ @property
21
+ def manifest(self) -> PluginManifest:
22
+ return PluginManifest(
23
+ name="claude-code",
24
+ version="0.1.0",
25
+ description="Claude Code adapter for loopengt",
26
+ capabilities=["adapter", "commands", "mcp"],
27
+ entry_points=["loopengt.adapters"],
28
+ )
29
+
30
+ def activate(self, context: dict[str, Any]) -> None:
31
+ self._active = True
32
+
33
+ def deactivate(self) -> None:
34
+ self._active = False
35
+
36
+ def capabilities(self) -> set[str]:
37
+ return {"commands", "mcp", "file_access", "terminal"}
38
+
39
+ def register_commands(self, registry: Any) -> None:
40
+ raise NotImplementedError("Claude Code command registration not yet implemented")
41
+
42
+ def on_loop_start(self, loop_spec: dict[str, Any]) -> None:
43
+ pass
44
+
45
+ def on_step_start(self, step: str, agent: str) -> None:
46
+ pass
47
+
48
+ def on_step_complete(self, step: str, result: Any) -> None:
49
+ pass
50
+
51
+ def on_loop_complete(self, result: Any) -> None:
52
+ pass
53
+
54
+ def on_error(self, error: Exception, context: dict[str, Any]) -> None:
55
+ pass
@@ -0,0 +1,16 @@
1
+ """Claude Code command definitions — placeholder."""
2
+
3
+ from __future__ import annotations
4
+
5
+ CLAUDE_CODE_COMMANDS = [
6
+ {
7
+ "id": "loopengt.design",
8
+ "title": "Design Loop",
9
+ "description": "Design a new agent loop from a goal",
10
+ },
11
+ {
12
+ "id": "loopengt.run",
13
+ "title": "Run Loop",
14
+ "description": "Execute a loop.yaml spec",
15
+ },
16
+ ]
@@ -0,0 +1 @@
1
+ """Codex adapter."""
@@ -0,0 +1,52 @@
1
+ """Codex (OpenAI) adapter — skeleton implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from loopengt.plugins.base import Plugin
8
+ from loopengt.plugins.manifest import PluginManifest
9
+
10
+
11
+ class CodexAdapter(Plugin):
12
+ """Adapter for OpenAI Codex agent environment.
13
+
14
+ .. note:: Skeleton — requires Codex CLI integration.
15
+ """
16
+
17
+ @property
18
+ def manifest(self) -> PluginManifest:
19
+ return PluginManifest(
20
+ name="codex",
21
+ version="0.1.0",
22
+ description="Codex adapter for loopengt",
23
+ capabilities=["adapter", "commands"],
24
+ entry_points=["loopengt.adapters"],
25
+ )
26
+
27
+ def activate(self, context: dict[str, Any]) -> None:
28
+ self._active = True
29
+
30
+ def deactivate(self) -> None:
31
+ self._active = False
32
+
33
+ def capabilities(self) -> set[str]:
34
+ return {"commands", "file_access"}
35
+
36
+ def register_commands(self, registry: Any) -> None:
37
+ raise NotImplementedError("Codex command registration not yet implemented")
38
+
39
+ def on_loop_start(self, loop_spec: dict[str, Any]) -> None:
40
+ pass
41
+
42
+ def on_step_start(self, step: str, agent: str) -> None:
43
+ pass
44
+
45
+ def on_step_complete(self, step: str, result: Any) -> None:
46
+ pass
47
+
48
+ def on_loop_complete(self, result: Any) -> None:
49
+ pass
50
+
51
+ def on_error(self, error: Exception, context: dict[str, Any]) -> None:
52
+ pass
@@ -0,0 +1,16 @@
1
+ """Codex command definitions — placeholder."""
2
+
3
+ from __future__ import annotations
4
+
5
+ CODEX_COMMANDS = [
6
+ {
7
+ "id": "loopengt.design",
8
+ "title": "Design Loop",
9
+ "description": "Design a new agent loop",
10
+ },
11
+ {
12
+ "id": "loopengt.run",
13
+ "title": "Run Loop",
14
+ "description": "Execute a loop spec",
15
+ },
16
+ ]
@@ -0,0 +1 @@
1
+ """Cursor IDE adapter."""
@@ -0,0 +1,56 @@
1
+ """Cursor IDE adapter — skeleton implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from loopengt.plugins.base import Plugin
8
+ from loopengt.plugins.manifest import PluginManifest
9
+
10
+
11
+ class CursorAdapter(Plugin):
12
+ """Adapter for the Cursor IDE.
13
+
14
+ Registers loopengt commands accessible via Cursor's command palette
15
+ and integrates with Cursor's agent capabilities.
16
+
17
+ .. note:: This is a skeleton implementation. Full integration
18
+ requires the Cursor extension API.
19
+ """
20
+
21
+ @property
22
+ def manifest(self) -> PluginManifest:
23
+ return PluginManifest(
24
+ name="cursor",
25
+ version="0.1.0",
26
+ description="Cursor IDE adapter for loopengt",
27
+ capabilities=["adapter", "commands", "mcp"],
28
+ entry_points=["loopengt.adapters"],
29
+ )
30
+
31
+ def activate(self, context: dict[str, Any]) -> None:
32
+ self._active = True
33
+
34
+ def deactivate(self) -> None:
35
+ self._active = False
36
+
37
+ def capabilities(self) -> set[str]:
38
+ return {"commands", "mcp", "file_access"}
39
+
40
+ def register_commands(self, registry: Any) -> None:
41
+ raise NotImplementedError("Cursor command registration not yet implemented")
42
+
43
+ def on_loop_start(self, loop_spec: dict[str, Any]) -> None:
44
+ pass # Will integrate with Cursor notifications
45
+
46
+ def on_step_start(self, step: str, agent: str) -> None:
47
+ pass
48
+
49
+ def on_step_complete(self, step: str, result: Any) -> None:
50
+ pass
51
+
52
+ def on_loop_complete(self, result: Any) -> None:
53
+ pass
54
+
55
+ def on_error(self, error: Exception, context: dict[str, Any]) -> None:
56
+ pass
@@ -0,0 +1,29 @@
1
+ """Cursor-specific command definitions — placeholder."""
2
+
3
+ from __future__ import annotations
4
+
5
+ # Cursor commands will be registered via the Cursor extension API.
6
+ # This module defines the command metadata for Cursor integration.
7
+
8
+ CURSOR_COMMANDS = [
9
+ {
10
+ "id": "loopengt.design",
11
+ "title": "LOOPENGT: Design Loop",
12
+ "description": "Design a new agent loop from a goal",
13
+ },
14
+ {
15
+ "id": "loopengt.run",
16
+ "title": "LOOPENGT: Run Loop",
17
+ "description": "Execute a loop.yaml spec",
18
+ },
19
+ {
20
+ "id": "loopengt.trace",
21
+ "title": "LOOPENGT: View Trace",
22
+ "description": "Inspect execution trace of a run",
23
+ },
24
+ {
25
+ "id": "loopengt.template",
26
+ "title": "LOOPENGT: Use Template",
27
+ "description": "Create a loop from a template",
28
+ },
29
+ ]
@@ -0,0 +1 @@
1
+ """Generic (non-IDE-specific) adapters."""
@@ -0,0 +1,82 @@
1
+ """Terminal/CLI adapter — the default adapter for command-line usage."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from rich.console import Console
8
+
9
+ from loopengt.adapters.base import Adapter
10
+ from loopengt.plugins.base import Plugin
11
+ from loopengt.plugins.manifest import PluginManifest
12
+
13
+ console = Console()
14
+
15
+
16
+ class TerminalAdapter(Plugin):
17
+ """Full terminal/CLI adapter for loopengt.
18
+
19
+ This is the default adapter used when running ``loopengt`` from the
20
+ command line. It outputs step progress to the terminal using Rich.
21
+ """
22
+
23
+ @property
24
+ def manifest(self) -> PluginManifest:
25
+ return PluginManifest(
26
+ name="terminal",
27
+ version="0.1.0",
28
+ description="Terminal/CLI adapter for loopengt",
29
+ capabilities=["adapter", "commands", "notifications", "progress", "terminal"],
30
+ entry_points=["loopengt.adapters"],
31
+ )
32
+
33
+ def activate(self, context: dict[str, Any]) -> None:
34
+ """Activate the terminal adapter."""
35
+ self._active = True
36
+
37
+ def deactivate(self) -> None:
38
+ """Deactivate the terminal adapter."""
39
+ self._active = False
40
+
41
+ # ------------------------------------------------------------------
42
+ # Adapter protocol
43
+ # ------------------------------------------------------------------
44
+
45
+ def capabilities(self) -> set[str]:
46
+ return {"commands", "notifications", "progress", "terminal"}
47
+
48
+ def register_commands(self, registry: Any) -> None:
49
+ """Terminal adapter commands are registered via CLI; no-op here."""
50
+
51
+ def on_loop_start(self, loop_spec: dict[str, Any]) -> None:
52
+ name = loop_spec.get("name", "unknown")
53
+ console.print(f"\n[bold blue]▶ Loop starting:[/bold blue] {name}")
54
+ console.print(f" Pattern: {loop_spec.get('pattern', 'sequential')}")
55
+ agents = loop_spec.get("agents", [])
56
+ if agents:
57
+ names = [
58
+ a["name"] if isinstance(a, dict) else str(a) for a in agents
59
+ ]
60
+ console.print(f" Agents: {', '.join(names)}")
61
+ console.print()
62
+
63
+ def on_step_start(self, step: str, agent: str) -> None:
64
+ console.print(f" [yellow]● Step:[/yellow] {step} [dim]agent={agent}[/dim]")
65
+
66
+ def on_step_complete(self, step: str, result: Any) -> None:
67
+ status = "✓" if result else "●"
68
+ color = "green" if result else "dim"
69
+ console.print(f" [{color}]{status} Completed:[/{color}] {step}")
70
+
71
+ def on_loop_complete(self, result: Any) -> None:
72
+ console.print(f"\n[bold green]✓ Loop completed[/bold green]")
73
+ if isinstance(result, dict):
74
+ run_id = result.get("run_id", "")
75
+ if run_id:
76
+ console.print(f" Run ID: {run_id}")
77
+
78
+ def on_error(self, error: Exception, context: dict[str, Any]) -> None:
79
+ console.print(f"\n[bold red]✗ Error:[/bold red] {error}")
80
+ step = context.get("step")
81
+ if step:
82
+ console.print(f" Step: {step}")
@@ -0,0 +1 @@
1
+ """CLI package for loopengt."""
@@ -0,0 +1 @@
1
+ """CLI command subpackage."""
@@ -0,0 +1,171 @@
1
+ """``loopengt design`` — invoke LOOPENGT to generate a loop spec from a goal."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+ import yaml
9
+
10
+ from loopengt.cli.formatters import print_error, print_info, print_success
11
+
12
+
13
+ def design_cmd(
14
+ goal: str = typer.Argument(
15
+ ..., help="Natural-language goal for the loop to achieve."
16
+ ),
17
+ output: Path = typer.Option(
18
+ Path("loop.yaml"),
19
+ "--output",
20
+ "-o",
21
+ help="Output path for the generated loop.yaml.",
22
+ ),
23
+ template: str | None = typer.Option(
24
+ None,
25
+ "--template",
26
+ "-t",
27
+ help="Start from a named template instead of blank.",
28
+ ),
29
+ design_doc: Path = typer.Option(
30
+ Path("loopengt.md"),
31
+ "--design-doc",
32
+ "-d",
33
+ help="Output path for the design document.",
34
+ ),
35
+ ) -> None:
36
+ """Design a loop spec from a natural-language goal.
37
+
38
+ Invokes the LOOPENGT architect prompt (or an LLM if configured) to
39
+ produce a ``loop.yaml`` and ``LOOP_DESIGN.md``.
40
+ """
41
+ print_info(f"Designing loop for goal: {goal}")
42
+
43
+ # Try LLM-based design first
44
+ try:
45
+ from loopengt.core.designer import design_loop
46
+
47
+ spec_dict, design_markdown = design_loop(goal, template_name=template)
48
+ except ImportError:
49
+ # Fall back to template-based stub generation
50
+ spec_dict = _generate_stub_spec(goal, template)
51
+ design_markdown = _generate_stub_design(goal, spec_dict)
52
+
53
+ # Write loop.yaml
54
+ output.parent.mkdir(parents=True, exist_ok=True)
55
+ output.write_text(
56
+ yaml.dump(spec_dict, default_flow_style=False, sort_keys=False),
57
+ encoding="utf-8",
58
+ )
59
+ print_success(f"Loop spec written to {output}")
60
+
61
+ # Write design doc
62
+ design_doc.parent.mkdir(parents=True, exist_ok=True)
63
+ design_doc.write_text(design_markdown, encoding="utf-8")
64
+ print_success(f"Design document written to {design_doc}")
65
+
66
+ print_info("Next: loopengt run loop.yaml")
67
+
68
+
69
+ def _generate_stub_spec(goal: str, template: str | None) -> dict:
70
+ """Generate a minimal loop spec dict without an LLM."""
71
+ import re
72
+
73
+ # Derive a slug name from the goal
74
+ slug = re.sub(r"[^a-z0-9]+", "-", goal.lower()).strip("-")[:64]
75
+
76
+ spec: dict = {
77
+ "name": slug,
78
+ "version": "1.0",
79
+ "goal": goal,
80
+ "pattern": "sequential",
81
+ "agents": [
82
+ {
83
+ "name": "planner",
84
+ "description": f"Plans the approach for: {goal}",
85
+ "capabilities": ["planning"],
86
+ },
87
+ {
88
+ "name": "executor",
89
+ "description": "Executes the plan produced by the planner",
90
+ "capabilities": ["code_generation", "tool_use"],
91
+ },
92
+ ],
93
+ "steps": [
94
+ {
95
+ "name": "plan",
96
+ "description": "Analyse the goal and produce an action plan",
97
+ "agent": "planner",
98
+ },
99
+ {
100
+ "name": "execute",
101
+ "description": "Execute the plan",
102
+ "agent": "executor",
103
+ "dependencies": [{"step_name": "plan"}],
104
+ },
105
+ ],
106
+ "policy": {
107
+ "max_turns": 20,
108
+ "max_total_time_seconds": 600,
109
+ },
110
+ "stop_conditions": [
111
+ {"condition_type": "max_turns", "value": 20},
112
+ {"condition_type": "goal_met", "value": "true"},
113
+ ],
114
+ }
115
+
116
+ if template:
117
+ spec["metadata"] = {"based_on_template": template}
118
+
119
+ return spec
120
+
121
+
122
+ def _generate_stub_design(goal: str, spec: dict) -> str:
123
+ """Generate a stub loopengt.md without an LLM."""
124
+ agents_section = ""
125
+ for agent in spec.get("agents", []):
126
+ agents_section += (
127
+ f"### {agent['name']}\n"
128
+ f"- **Description**: {agent.get('description', 'N/A')}\n"
129
+ f"- **Capabilities**: {', '.join(agent.get('capabilities', []))}\n\n"
130
+ )
131
+
132
+ steps_section = ""
133
+ for i, step in enumerate(spec.get("steps", []), 1):
134
+ steps_section += (
135
+ f"{i}. **{step['name']}** — {step.get('description', 'N/A')}\n"
136
+ f" - Agent: `{step['agent']}`\n"
137
+ )
138
+ if step.get("dependencies"):
139
+ deps = ", ".join(d["step_name"] for d in step["dependencies"])
140
+ steps_section += f" - Depends on: {deps}\n"
141
+ steps_section += "\n"
142
+
143
+ return (
144
+ f"# Loop Design: {spec.get('name', 'untitled')}\n"
145
+ f"\n"
146
+ f"## Goal\n"
147
+ f"{goal}\n"
148
+ f"\n"
149
+ f"## Pattern\n"
150
+ f"`{spec.get('pattern', 'sequential')}`\n"
151
+ f"\n"
152
+ f"## Agents\n"
153
+ f"{agents_section}"
154
+ f"## Steps\n"
155
+ f"{steps_section}"
156
+ f"## Policy\n"
157
+ f"- Max turns: {spec.get('policy', {}).get('max_turns', 50)}\n"
158
+ f"- Max time: {spec.get('policy', {}).get('max_total_time_seconds', 3600)}s\n"
159
+ f"\n"
160
+ f"## Risks & Mitigations\n"
161
+ f"- *To be refined with LLM-assisted design.*\n"
162
+ f"\n"
163
+ f"## Execution Instructions\n"
164
+ f"If you are reading this as an Agentic IDE (Cursor, Claude Code, Antigravity, etc.), "
165
+ f"**you are the Orchestrator**.\n\n"
166
+ f"Please execute this loop by fulfilling the roles of each agent described above in sequence, "
167
+ f"writing code and performing tasks as specified in the Steps section.\n"
168
+ f"\n"
169
+ f"---\n"
170
+ f"*Generated by loopengt design (stub mode)*\n"
171
+ )