autoloop 0.1.0__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 (32) hide show
  1. autoloop-0.1.0/LICENSE +21 -0
  2. autoloop-0.1.0/PKG-INFO +123 -0
  3. autoloop-0.1.0/README.md +98 -0
  4. autoloop-0.1.0/pyproject.toml +51 -0
  5. autoloop-0.1.0/setup.cfg +4 -0
  6. autoloop-0.1.0/src/autoloop/__init__.py +1 -0
  7. autoloop-0.1.0/src/autoloop/__main__.py +3 -0
  8. autoloop-0.1.0/src/autoloop/loop_control.py +173 -0
  9. autoloop-0.1.0/src/autoloop/main.py +4747 -0
  10. autoloop-0.1.0/src/autoloop/skill/SKILL.md +66 -0
  11. autoloop-0.1.0/src/autoloop/templates/implement_criteria.md +8 -0
  12. autoloop-0.1.0/src/autoloop/templates/implement_producer.md +51 -0
  13. autoloop-0.1.0/src/autoloop/templates/implement_verifier.md +46 -0
  14. autoloop-0.1.0/src/autoloop/templates/plan_criteria.md +8 -0
  15. autoloop-0.1.0/src/autoloop/templates/plan_producer.md +92 -0
  16. autoloop-0.1.0/src/autoloop/templates/plan_verifier.md +59 -0
  17. autoloop-0.1.0/src/autoloop/templates/test_criteria.md +8 -0
  18. autoloop-0.1.0/src/autoloop/templates/test_producer.md +42 -0
  19. autoloop-0.1.0/src/autoloop/templates/test_verifier.md +44 -0
  20. autoloop-0.1.0/src/autoloop.egg-info/PKG-INFO +123 -0
  21. autoloop-0.1.0/src/autoloop.egg-info/SOURCES.txt +30 -0
  22. autoloop-0.1.0/src/autoloop.egg-info/dependency_links.txt +1 -0
  23. autoloop-0.1.0/src/autoloop.egg-info/entry_points.txt +2 -0
  24. autoloop-0.1.0/src/autoloop.egg-info/requires.txt +1 -0
  25. autoloop-0.1.0/src/autoloop.egg-info/top_level.txt +1 -0
  26. autoloop-0.1.0/tests/test_autoloop_git_tracking.py +87 -0
  27. autoloop-0.1.0/tests/test_autoloop_observability.py +3939 -0
  28. autoloop-0.1.0/tests/test_imports.py +8 -0
  29. autoloop-0.1.0/tests/test_loop_control.py +140 -0
  30. autoloop-0.1.0/tests/test_module_entrypoint.py +74 -0
  31. autoloop-0.1.0/tests/test_phase_local_behavior.py +465 -0
  32. autoloop-0.1.0/tests/test_resources.py +18 -0
autoloop-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marcelo Rauter
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: autoloop
3
+ Version: 0.1.0
4
+ Summary: Protocol-driven agent loops for shipping real code with less babysitting.
5
+ Author: Marcelo Rauter
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/mrauter1/autoloop
8
+ Project-URL: Repository, https://github.com/mrauter1/autoloop
9
+ Project-URL: Issues, https://github.com/mrauter1/autoloop/issues
10
+ Keywords: llm,agent,orchestration,cli,codex
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Environment :: Console
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Operating System :: OS Independent
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: PyYAML>=6.0
24
+ Dynamic: license-file
25
+
26
+ # Autoloop
27
+
28
+ Protocol-driven agent loops for shipping real code with less babysitting.
29
+
30
+ Autoloop is a stateful orchestration runtime for repository work. It runs plan, implement, and test producer/verifier pairs with durable state, resumable runs, phase scoping, provider-neutral sessions, and git-backed checkpoints.
31
+
32
+ ## Requirements
33
+
34
+ - Python 3.10+
35
+ - One supported provider CLI:
36
+ - `codex` for `provider.name: codex` (default)
37
+ - `claude` for `provider.name: claude`, with local `claude auth status` already working
38
+ - `git` is optional when running with `--no-git`
39
+
40
+ ## Install
41
+
42
+ Install from PyPI:
43
+
44
+ ```bash
45
+ pip install autoloop
46
+ autoloop --help
47
+ ```
48
+
49
+ Install from this repository:
50
+
51
+ ```bash
52
+ python -m pip install -e .
53
+ autoloop --help
54
+ python -m autoloop --help
55
+ ```
56
+
57
+ Or use the bundled installer:
58
+
59
+ ```bash
60
+ ./install_autoloop.sh
61
+ ```
62
+
63
+ The installer sets up Autoloop itself and the packaged skill, but it does not install provider CLIs for you. Install `codex` or `claude` separately based on the provider you plan to run.
64
+
65
+ ## Configuration
66
+
67
+ Autoloop reads configuration from these directories, in this order:
68
+
69
+ 1. `$XDG_CONFIG_HOME/autoloop/` or `~/.config/autoloop/`
70
+ 2. the selected workspace root passed to `--workspace`
71
+
72
+ Within each directory it looks for:
73
+
74
+ - `autoloop.yaml`
75
+ - `autoloop.config`
76
+ - legacy read-only fallback: `superloop.yaml`
77
+ - legacy read-only fallback: `superloop.config`
78
+
79
+ If more than one config file exists in the same directory, Autoloop fails fast instead of guessing.
80
+
81
+ Use the nested provider shape for new configs:
82
+
83
+ ```yaml
84
+ provider:
85
+ name: codex
86
+ codex:
87
+ model: gpt-5.4
88
+ model_effort: null
89
+ claude:
90
+ model: null
91
+ effort: null
92
+ permission_strategy: inherit
93
+ runtime:
94
+ pairs: plan,implement,test
95
+ max_iterations: 15
96
+ phase_mode: single
97
+ intent_mode: preserve
98
+ full_auto_answers: false
99
+ no_git: false
100
+ ```
101
+
102
+ Notes:
103
+
104
+ - Legacy flat Codex keys `provider.model` and `provider.model_effort` are still read for backward compatibility.
105
+ - `--model` and `--model-effort` are Codex-only CLI overrides.
106
+ - Claude defaults intentionally inherit the operator's existing Claude Code environment. Default Claude runs do not add strict mode, `--bare`, tool restriction flags, or `stream-json`.
107
+ - `runtime.full_auto_answers` now uses the active provider instead of being Codex-only.
108
+
109
+ ## Runtime State
110
+
111
+ Fresh runs write state under `.autoloop/`.
112
+
113
+ For the first Autoloop release, resume and task listing can still read legacy `.superloop/` workspaces when no `.autoloop/` state exists for the requested task or run.
114
+
115
+ Session files now store provider-neutral `provider` and `session_id` fields. Legacy Codex session files that only contain `thread_id` remain load-compatible.
116
+
117
+ ## Claude Validation
118
+
119
+ Maintainer live-validation steps for inherited-environment Claude repos and resume config-drift checks are recorded in [docs/claude_rollout_checklist.md](docs/claude_rollout_checklist.md).
120
+
121
+ ## Skill
122
+
123
+ The packaged Autoloop skill lives at `src/autoloop/skill/SKILL.md`.
@@ -0,0 +1,98 @@
1
+ # Autoloop
2
+
3
+ Protocol-driven agent loops for shipping real code with less babysitting.
4
+
5
+ Autoloop is a stateful orchestration runtime for repository work. It runs plan, implement, and test producer/verifier pairs with durable state, resumable runs, phase scoping, provider-neutral sessions, and git-backed checkpoints.
6
+
7
+ ## Requirements
8
+
9
+ - Python 3.10+
10
+ - One supported provider CLI:
11
+ - `codex` for `provider.name: codex` (default)
12
+ - `claude` for `provider.name: claude`, with local `claude auth status` already working
13
+ - `git` is optional when running with `--no-git`
14
+
15
+ ## Install
16
+
17
+ Install from PyPI:
18
+
19
+ ```bash
20
+ pip install autoloop
21
+ autoloop --help
22
+ ```
23
+
24
+ Install from this repository:
25
+
26
+ ```bash
27
+ python -m pip install -e .
28
+ autoloop --help
29
+ python -m autoloop --help
30
+ ```
31
+
32
+ Or use the bundled installer:
33
+
34
+ ```bash
35
+ ./install_autoloop.sh
36
+ ```
37
+
38
+ The installer sets up Autoloop itself and the packaged skill, but it does not install provider CLIs for you. Install `codex` or `claude` separately based on the provider you plan to run.
39
+
40
+ ## Configuration
41
+
42
+ Autoloop reads configuration from these directories, in this order:
43
+
44
+ 1. `$XDG_CONFIG_HOME/autoloop/` or `~/.config/autoloop/`
45
+ 2. the selected workspace root passed to `--workspace`
46
+
47
+ Within each directory it looks for:
48
+
49
+ - `autoloop.yaml`
50
+ - `autoloop.config`
51
+ - legacy read-only fallback: `superloop.yaml`
52
+ - legacy read-only fallback: `superloop.config`
53
+
54
+ If more than one config file exists in the same directory, Autoloop fails fast instead of guessing.
55
+
56
+ Use the nested provider shape for new configs:
57
+
58
+ ```yaml
59
+ provider:
60
+ name: codex
61
+ codex:
62
+ model: gpt-5.4
63
+ model_effort: null
64
+ claude:
65
+ model: null
66
+ effort: null
67
+ permission_strategy: inherit
68
+ runtime:
69
+ pairs: plan,implement,test
70
+ max_iterations: 15
71
+ phase_mode: single
72
+ intent_mode: preserve
73
+ full_auto_answers: false
74
+ no_git: false
75
+ ```
76
+
77
+ Notes:
78
+
79
+ - Legacy flat Codex keys `provider.model` and `provider.model_effort` are still read for backward compatibility.
80
+ - `--model` and `--model-effort` are Codex-only CLI overrides.
81
+ - Claude defaults intentionally inherit the operator's existing Claude Code environment. Default Claude runs do not add strict mode, `--bare`, tool restriction flags, or `stream-json`.
82
+ - `runtime.full_auto_answers` now uses the active provider instead of being Codex-only.
83
+
84
+ ## Runtime State
85
+
86
+ Fresh runs write state under `.autoloop/`.
87
+
88
+ For the first Autoloop release, resume and task listing can still read legacy `.superloop/` workspaces when no `.autoloop/` state exists for the requested task or run.
89
+
90
+ Session files now store provider-neutral `provider` and `session_id` fields. Legacy Codex session files that only contain `thread_id` remain load-compatible.
91
+
92
+ ## Claude Validation
93
+
94
+ Maintainer live-validation steps for inherited-environment Claude repos and resume config-drift checks are recorded in [docs/claude_rollout_checklist.md](docs/claude_rollout_checklist.md).
95
+
96
+ ## Skill
97
+
98
+ The packaged Autoloop skill lives at `src/autoloop/skill/SKILL.md`.
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.3"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "autoloop"
7
+ version = "0.1.0"
8
+ description = "Protocol-driven agent loops for shipping real code with less babysitting."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ { name = "Marcelo Rauter" }
15
+ ]
16
+ dependencies = [
17
+ "PyYAML>=6.0",
18
+ ]
19
+ keywords = ["llm", "agent", "orchestration", "cli", "codex"]
20
+ classifiers = [
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Environment :: Console",
28
+ "Intended Audience :: Developers",
29
+ "Operating System :: OS Independent",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/mrauter1/autoloop"
34
+ Repository = "https://github.com/mrauter1/autoloop"
35
+ Issues = "https://github.com/mrauter1/autoloop/issues"
36
+
37
+ [project.scripts]
38
+ autoloop = "autoloop.main:main"
39
+
40
+ [tool.setuptools]
41
+ package-dir = {"" = "src"}
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["src"]
45
+ include = ["autoloop*"]
46
+
47
+ [tool.setuptools.package-data]
48
+ autoloop = [
49
+ "templates/*.md",
50
+ "skill/SKILL.md",
51
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ """Autoloop package."""
@@ -0,0 +1,3 @@
1
+ from .main import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,173 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Literal
8
+
9
+ CONTROL_SCHEMA_ID = "docloop.loop_control/v1"
10
+
11
+ PROMISE_COMPLETE = "COMPLETE"
12
+ PROMISE_INCOMPLETE = "INCOMPLETE"
13
+ PROMISE_BLOCKED = "BLOCKED"
14
+ PROMISE_VALUES = {PROMISE_COMPLETE, PROMISE_INCOMPLETE, PROMISE_BLOCKED}
15
+
16
+ CANONICAL_BLOCK_RE = re.compile(
17
+ r"<loop-control>\s*(.*?)\s*</loop-control>",
18
+ re.DOTALL | re.IGNORECASE,
19
+ )
20
+ LEGACY_QUESTION_RE = re.compile(r"<question>(.*?)</question>", re.DOTALL | re.IGNORECASE)
21
+ PROMISE_LINE_RE = re.compile(
22
+ r"^\s*<promise>(COMPLETE|INCOMPLETE|BLOCKED)</promise>\s*$",
23
+ re.IGNORECASE,
24
+ )
25
+ UNCHECKED_BOX_RE = re.compile(r"^\s*-\s\[ \]", re.MULTILINE)
26
+
27
+
28
+ class LoopControlParseError(ValueError):
29
+ """Raised when loop-control output is malformed or conflicting."""
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class LoopQuestion:
34
+ text: str
35
+ best_supposition: str | None = None
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class LoopControl:
40
+ question: LoopQuestion | None
41
+ promise: str | None
42
+ source: Literal["canonical", "legacy", "none"]
43
+ raw_payload: str | None
44
+
45
+
46
+ def last_non_empty_line(text: str) -> str:
47
+ for line in reversed(text.splitlines()):
48
+ if line.strip():
49
+ return line.strip()
50
+ return ""
51
+
52
+
53
+ def parse_loop_control(stdout: str) -> LoopControl:
54
+ canonical_matches = list(CANONICAL_BLOCK_RE.finditer(stdout))
55
+ if len(canonical_matches) > 1:
56
+ raise LoopControlParseError("Multiple <loop-control> blocks found.")
57
+
58
+ if canonical_matches:
59
+ return _parse_canonical_control(stdout, canonical_matches[0])
60
+
61
+ return _parse_legacy_control(stdout)
62
+
63
+
64
+ def criteria_all_checked(criteria_file: Path) -> bool:
65
+ criteria_text = criteria_file.read_text(encoding="utf-8")
66
+ return UNCHECKED_BOX_RE.search(criteria_text) is None
67
+
68
+
69
+ def _parse_canonical_control(stdout: str, canonical_match: re.Match[str]) -> LoopControl:
70
+ prefix = stdout[: canonical_match.start()]
71
+ suffix = stdout[canonical_match.end() :]
72
+
73
+ if suffix.strip():
74
+ raise LoopControlParseError(
75
+ "Canonical <loop-control> output must be the last non-empty block in stdout."
76
+ )
77
+
78
+ if LEGACY_QUESTION_RE.search(prefix):
79
+ raise LoopControlParseError(
80
+ "Canonical <loop-control> output cannot be combined with legacy <question> tags."
81
+ )
82
+ legacy_promise_match = PROMISE_LINE_RE.fullmatch(last_non_empty_line(prefix))
83
+ if legacy_promise_match:
84
+ raise LoopControlParseError(
85
+ "Canonical <loop-control> output cannot be combined with legacy final-line <promise> tags."
86
+ )
87
+
88
+ raw_payload = canonical_match.group(1).strip()
89
+ try:
90
+ payload = json.loads(raw_payload)
91
+ except json.JSONDecodeError as exc:
92
+ raise LoopControlParseError(f"Invalid canonical loop-control JSON: {exc.msg}.") from exc
93
+
94
+ if not isinstance(payload, dict):
95
+ raise LoopControlParseError("Canonical loop-control payload must decode to a JSON object.")
96
+ _validate_canonical_schema(payload)
97
+
98
+ kind = payload.get("kind")
99
+ if kind == "question":
100
+ return _parse_canonical_question(payload, raw_payload)
101
+ if kind == "promise":
102
+ return _parse_canonical_promise(payload, raw_payload)
103
+ raise LoopControlParseError("Canonical loop-control kind must be 'question' or 'promise'.")
104
+
105
+
106
+ def _parse_canonical_question(payload: dict[str, object], raw_payload: str) -> LoopControl:
107
+ if "promise" in payload:
108
+ raise LoopControlParseError("Canonical question payload must not include a promise field.")
109
+
110
+ question = _require_non_empty_string(payload.get("question"), "Canonical question payload requires 'question'.")
111
+ best_supposition = payload.get("best_supposition")
112
+ if best_supposition is not None:
113
+ best_supposition = _require_non_empty_string(
114
+ best_supposition,
115
+ "Canonical question payload best_supposition must be a non-empty string when provided.",
116
+ )
117
+
118
+ return LoopControl(
119
+ question=LoopQuestion(text=question, best_supposition=best_supposition),
120
+ promise=None,
121
+ source="canonical",
122
+ raw_payload=raw_payload,
123
+ )
124
+
125
+
126
+ def _parse_canonical_promise(payload: dict[str, object], raw_payload: str) -> LoopControl:
127
+ if "question" in payload or "best_supposition" in payload:
128
+ raise LoopControlParseError(
129
+ "Canonical promise payload must not include question or best_supposition fields."
130
+ )
131
+
132
+ promise = _require_non_empty_string(payload.get("promise"), "Canonical promise payload requires 'promise'.")
133
+ promise = promise.upper()
134
+ if promise not in PROMISE_VALUES:
135
+ raise LoopControlParseError(
136
+ f"Canonical promise must be one of {PROMISE_COMPLETE}, {PROMISE_INCOMPLETE}, or {PROMISE_BLOCKED}, not {promise!r}."
137
+ )
138
+
139
+ return LoopControl(
140
+ question=None,
141
+ promise=promise,
142
+ source="canonical",
143
+ raw_payload=raw_payload,
144
+ )
145
+
146
+
147
+ def _validate_canonical_schema(payload: dict[str, object]) -> None:
148
+ if payload.get("schema") != CONTROL_SCHEMA_ID:
149
+ raise LoopControlParseError(
150
+ f"Canonical loop-control schema must be {CONTROL_SCHEMA_ID!r}."
151
+ )
152
+
153
+
154
+ def _parse_legacy_control(stdout: str) -> LoopControl:
155
+ question_match = LEGACY_QUESTION_RE.search(stdout)
156
+ promise_match = PROMISE_LINE_RE.fullmatch(last_non_empty_line(stdout))
157
+
158
+ question = None
159
+ if question_match:
160
+ question_text = question_match.group(1).strip()
161
+ question = LoopQuestion(text=question_text)
162
+
163
+ promise = promise_match.group(1).upper() if promise_match else None
164
+ if question or promise:
165
+ return LoopControl(question=question, promise=promise, source="legacy", raw_payload=None)
166
+
167
+ return LoopControl(question=None, promise=None, source="none", raw_payload=None)
168
+
169
+
170
+ def _require_non_empty_string(value: object, message: str) -> str:
171
+ if not isinstance(value, str) or not value.strip():
172
+ raise LoopControlParseError(message)
173
+ return value.strip()