gooseloop 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.
- gooseloop/__init__.py +42 -0
- gooseloop/__main__.py +181 -0
- gooseloop/artifact.py +82 -0
- gooseloop/branch_policy.py +49 -0
- gooseloop/config.py +104 -0
- gooseloop/context_prepend.py +316 -0
- gooseloop/contrib/__init__.py +18 -0
- gooseloop/contrib/claude_handoff.py +74 -0
- gooseloop/contrib/customer_pipeline.py +98 -0
- gooseloop/engine.py +68 -0
- gooseloop/environment.py +31 -0
- gooseloop/extract.py +200 -0
- gooseloop/footer.py +75 -0
- gooseloop/goose.py +340 -0
- gooseloop/looper.py +581 -0
- gooseloop/phase.py +146 -0
- gooseloop/predicates.py +69 -0
- gooseloop/protocol.py +197 -0
- gooseloop/recipe_merge.py +152 -0
- gooseloop/session.py +41 -0
- gooseloop/text.py +60 -0
- gooseloop/toolkit.py +243 -0
- gooseloop-0.1.0.dist-info/METADATA +169 -0
- gooseloop-0.1.0.dist-info/RECORD +28 -0
- gooseloop-0.1.0.dist-info/WHEEL +5 -0
- gooseloop-0.1.0.dist-info/entry_points.txt +2 -0
- gooseloop-0.1.0.dist-info/licenses/LICENSE +21 -0
- gooseloop-0.1.0.dist-info/top_level.txt +1 -0
gooseloop/toolkit.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Engine toolkit: the helpers every real engine ended up writing.
|
|
2
|
+
|
|
3
|
+
Extracted from three independent copies (doc_drift, and the site-drift /
|
|
4
|
+
pain-harvest / site-pitch engines) once the duplication proved the need.
|
|
5
|
+
Everything here is stdlib-only and framework-agnostic: an engine can use any
|
|
6
|
+
of it without buying into anything else.
|
|
7
|
+
|
|
8
|
+
What lives here and why it earned the spot:
|
|
9
|
+
|
|
10
|
+
Source / parse_source(s) a doc, feed, or reference is a file path or a
|
|
11
|
+
live URL; every engine that reads inputs from
|
|
12
|
+
config re-invented this two-arm shape.
|
|
13
|
+
FetchResult / fetch_url hardened urllib fetch with HTML-to-text. The
|
|
14
|
+
named-field result exists because doc_drift
|
|
15
|
+
needed ETag/Last-Modified for revision probing
|
|
16
|
+
and had to fork the (text, error) tuple.
|
|
17
|
+
html_to_text minimal dependency-free HTML normalizer.
|
|
18
|
+
cap paste budget with a visible truncation marker.
|
|
19
|
+
The marker is the load-bearing part: a silent
|
|
20
|
+
cap reads as "pasted everything" when it didn't.
|
|
21
|
+
template_safe / ZWSP goose renders assembled recipes through
|
|
22
|
+
minijinja; any surviving delimiter in pasted
|
|
23
|
+
reference text aborts the run. One ZWSP trick,
|
|
24
|
+
one home (context_prepend shares this constant).
|
|
25
|
+
safe_filename/unique_slug model-emitted routing params become file paths
|
|
26
|
+
via BranchPolicy.output_path. The model is
|
|
27
|
+
untrusted input: slashes and leading dots are
|
|
28
|
+
stripped so a hallucinated slug can never
|
|
29
|
+
resolve outside the output dir.
|
|
30
|
+
load_state / save_state cross-run JSON state with corrupt-file recovery
|
|
31
|
+
and defaults backfill.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import json
|
|
37
|
+
import re
|
|
38
|
+
import urllib.error
|
|
39
|
+
import urllib.request
|
|
40
|
+
from dataclasses import dataclass
|
|
41
|
+
from email.utils import parsedate_to_datetime
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
from typing import Optional
|
|
44
|
+
|
|
45
|
+
DEFAULT_PASTE_CAP = 60_000
|
|
46
|
+
_HTTP_TIMEOUT = 15
|
|
47
|
+
_HTTP_MAX_BYTES = 5_000_000
|
|
48
|
+
_DEFAULT_USER_AGENT = "gooseloop/1.0 (+https://github.com/smattymatty/gooseloop)"
|
|
49
|
+
_URL_RE = re.compile(r"^https?://", re.IGNORECASE)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ---- sources: a file path or a live URL --------------------------
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class Source:
|
|
57
|
+
kind: str # "file" | "url"
|
|
58
|
+
value: str # absolute path for files; the URL string for urls
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def is_url(self) -> bool:
|
|
62
|
+
return self.kind == "url"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parse_source(raw: str, base: Path) -> Source:
|
|
66
|
+
raw = raw.strip()
|
|
67
|
+
if _URL_RE.match(raw):
|
|
68
|
+
return Source("url", raw)
|
|
69
|
+
path = Path(raw).expanduser()
|
|
70
|
+
resolved = path if path.is_absolute() else (base / path).resolve()
|
|
71
|
+
return Source("file", str(resolved))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def parse_sources(raw, base: Path) -> tuple[Source, ...]:
|
|
75
|
+
"""A single source, a list of them, or None. Returns a tuple of Sources."""
|
|
76
|
+
if raw is None:
|
|
77
|
+
return ()
|
|
78
|
+
items = raw if isinstance(raw, list) else [raw]
|
|
79
|
+
return tuple(parse_source(str(x), base) for x in items if str(x).strip())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---- http fetch + html->text (dependency-free) -------------------
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class FetchResult:
|
|
87
|
+
"""Outcome of one fetch_url call. Exactly one of text/error is set.
|
|
88
|
+
|
|
89
|
+
etag and last_modified_unix carry the response headers when the server
|
|
90
|
+
sent them; revision-probing engines build change tokens from these.
|
|
91
|
+
"""
|
|
92
|
+
text: Optional[str]
|
|
93
|
+
error: Optional[str]
|
|
94
|
+
etag: Optional[str] = None
|
|
95
|
+
last_modified_unix: Optional[int] = None
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def ok(self) -> bool:
|
|
99
|
+
return self.error is None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def fetch_url(
|
|
103
|
+
url: str,
|
|
104
|
+
*,
|
|
105
|
+
strip: bool = True,
|
|
106
|
+
timeout: int = _HTTP_TIMEOUT,
|
|
107
|
+
max_bytes: int = _HTTP_MAX_BYTES,
|
|
108
|
+
user_agent: str = _DEFAULT_USER_AGENT,
|
|
109
|
+
) -> FetchResult:
|
|
110
|
+
"""Fetch a URL. strip=True normalizes HTML to visible text (stable for
|
|
111
|
+
hashing and readable for the model); strip=False returns the body as-is
|
|
112
|
+
(for JSON / sitemap XML / href scraping). Any network or HTTP error comes
|
|
113
|
+
back as FetchResult(error=reason), never an exception."""
|
|
114
|
+
req = urllib.request.Request(url, headers={"User-Agent": user_agent})
|
|
115
|
+
try:
|
|
116
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
117
|
+
raw = resp.read(max_bytes)
|
|
118
|
+
ctype = resp.headers.get("Content-Type", "")
|
|
119
|
+
etag = resp.headers.get("ETag")
|
|
120
|
+
last_mod = resp.headers.get("Last-Modified")
|
|
121
|
+
except urllib.error.HTTPError as e:
|
|
122
|
+
return FetchResult(None, f"HTTP {e.code} fetching {url}")
|
|
123
|
+
except (urllib.error.URLError, OSError, ValueError) as e:
|
|
124
|
+
return FetchResult(None, f"could not fetch {url}: {e}")
|
|
125
|
+
|
|
126
|
+
text = raw.decode("utf-8", errors="replace")
|
|
127
|
+
if strip and ("html" in ctype.lower() or _looks_like_html(text)):
|
|
128
|
+
text = html_to_text(text)
|
|
129
|
+
return FetchResult(text, None, etag, _http_date_to_unix(last_mod))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def url_resolves(url: str) -> bool:
|
|
133
|
+
"""True if the URL fetches without a network/HTTP error."""
|
|
134
|
+
return fetch_url(url, strip=False).ok
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
_SCRIPT_STYLE_RE = re.compile(r"<(script|style)\b[^>]*>.*?</\1>", re.IGNORECASE | re.DOTALL)
|
|
138
|
+
_TAG_RE = re.compile(r"<[^>]+>")
|
|
139
|
+
_WS_RE = re.compile(r"[ \t\f\v]+")
|
|
140
|
+
_BLANKLINES_RE = re.compile(r"\n{3,}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _looks_like_html(text: str) -> bool:
|
|
144
|
+
head = text[:512].lower()
|
|
145
|
+
return "<html" in head or "<!doctype html" in head
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def html_to_text(html: str) -> str:
|
|
149
|
+
"""Minimal, dependency-free HTML->text: drop script/style, strip tags, tidy."""
|
|
150
|
+
import html as _html
|
|
151
|
+
text = _SCRIPT_STYLE_RE.sub(" ", html)
|
|
152
|
+
text = _TAG_RE.sub("\n", text)
|
|
153
|
+
text = _html.unescape(text)
|
|
154
|
+
text = _WS_RE.sub(" ", text)
|
|
155
|
+
text = "\n".join(line.strip() for line in text.splitlines())
|
|
156
|
+
return _BLANKLINES_RE.sub("\n\n", text).strip()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _http_date_to_unix(value: Optional[str]) -> Optional[int]:
|
|
160
|
+
if not value:
|
|
161
|
+
return None
|
|
162
|
+
try:
|
|
163
|
+
dt = parsedate_to_datetime(value)
|
|
164
|
+
except (TypeError, ValueError):
|
|
165
|
+
return None
|
|
166
|
+
return int(dt.timestamp()) if dt is not None else None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def cap(text: str, limit: int = DEFAULT_PASTE_CAP) -> str:
|
|
170
|
+
"""Bound pasted content. The truncation marker is deliberate: a capped
|
|
171
|
+
paste must say so, or the model (and the operator reading the session)
|
|
172
|
+
believes it saw everything."""
|
|
173
|
+
if len(text) > limit:
|
|
174
|
+
return f"{text[:limit]}\n\n(... truncated at {limit} chars ...)"
|
|
175
|
+
return text
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ---- minijinja delimiter neutralizer -----------------------------
|
|
179
|
+
|
|
180
|
+
# goose renders the whole assembled recipe through minijinja. Any delimiter
|
|
181
|
+
# that survives in inlined reference text is parsed as a tag and aborts the
|
|
182
|
+
# run. Splitting each delimiter with a zero-width space keeps the text
|
|
183
|
+
# visually identical while goose no longer recognises a tag.
|
|
184
|
+
ZWSP = ""
|
|
185
|
+
_TEMPLATE_DELIMS = ("{{", "}}", "{%", "%}", "{#", "#}")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def template_safe(text: str) -> str:
|
|
189
|
+
for d in _TEMPLATE_DELIMS:
|
|
190
|
+
while d in text:
|
|
191
|
+
text = text.replace(d, d[0] + ZWSP + d[1])
|
|
192
|
+
return text
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ---- filename + slug safety --------------------------------------
|
|
196
|
+
|
|
197
|
+
_SAFE_RE = re.compile(r"[^a-zA-Z0-9._-]+")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def safe_filename(text: str, fallback: str = "item") -> str:
|
|
201
|
+
"""Reduce untrusted text (a model-emitted routing slug, a URL fragment) to
|
|
202
|
+
a single safe path component. Separators collapse to '-'; leading dots are
|
|
203
|
+
stripped so '..' and dotfiles cannot come back out. Never returns empty:
|
|
204
|
+
a fully-consumed input yields `fallback`."""
|
|
205
|
+
cleaned = _SAFE_RE.sub("-", text).strip("-").lstrip(".-")
|
|
206
|
+
return cleaned or fallback
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def unique_slug(raw: str, seen: set[str]) -> str:
|
|
210
|
+
slug = safe_filename(raw)
|
|
211
|
+
if slug not in seen:
|
|
212
|
+
return slug
|
|
213
|
+
i = 2
|
|
214
|
+
while f"{slug}-{i}" in seen:
|
|
215
|
+
i += 1
|
|
216
|
+
return f"{slug}-{i}"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ---- json state io -----------------------------------------------
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def load_state(path: Path, defaults: dict) -> dict:
|
|
223
|
+
"""Read a JSON state file, falling back to a copy of `defaults` on a missing
|
|
224
|
+
or corrupt file. Missing keys in a present file are backfilled from defaults."""
|
|
225
|
+
if not path.exists():
|
|
226
|
+
return dict(defaults)
|
|
227
|
+
try:
|
|
228
|
+
with open(path, "rb") as f:
|
|
229
|
+
data = json.load(f)
|
|
230
|
+
except (json.JSONDecodeError, OSError):
|
|
231
|
+
return dict(defaults)
|
|
232
|
+
if not isinstance(data, dict):
|
|
233
|
+
return dict(defaults)
|
|
234
|
+
for k, v in defaults.items():
|
|
235
|
+
data.setdefault(k, v)
|
|
236
|
+
return data
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def save_state(path: Path, state: dict) -> None:
|
|
240
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
241
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
242
|
+
json.dump(state, f, indent=2, sort_keys=True)
|
|
243
|
+
f.write("\n")
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gooseloop
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Execution shell for goose-recipe pipelines
|
|
5
|
+
Author: Storm Developments
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Storm Developments
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Source, https://gitforge.ca/storm/gooseloop
|
|
29
|
+
Keywords: goose,llm,pipeline,agent,automation
|
|
30
|
+
Classifier: Development Status :: 3 - Alpha
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Programming Language :: Python :: 3
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
35
|
+
Requires-Python: >=3.11
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
License-File: LICENSE
|
|
38
|
+
Requires-Dist: PyYAML>=6.0
|
|
39
|
+
Provides-Extra: dev
|
|
40
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
41
|
+
Requires-Dist: minijinja>=2; extra == "dev"
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# gooseloop
|
|
45
|
+
|
|
46
|
+
An execution shell for [goose](https://block.github.io/goose/) recipe pipelines.
|
|
47
|
+
|
|
48
|
+
`gooseloop` runs an **Engine** against an **Environment**, driving a `Pipeline`
|
|
49
|
+
of `review -> body -> summary` through goose, with retry, rate-limit handling,
|
|
50
|
+
session bookkeeping, recipe overlay merging, and a typed session ledger.
|
|
51
|
+
|
|
52
|
+
It is generic. The framework knows nothing about your domain. Engines plug in.
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
pip install gooseloop
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or, from a checkout:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
pip install -e .
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Requires Python 3.11+, the `goose` CLI on `$PATH`, and a YAML recipe directory.
|
|
67
|
+
|
|
68
|
+
## The shape
|
|
69
|
+
|
|
70
|
+
Three primitives:
|
|
71
|
+
|
|
72
|
+
- **Engine** (verbs). Returns a `Pipeline(review, body, summary)`. Owns
|
|
73
|
+
branching, recipes, scoring, retry rules.
|
|
74
|
+
- **Environment** (nouns). One abstract method (`env_vars()`). Subclasses or
|
|
75
|
+
contrib mixins (`gooseloop.contrib.CustomerPipelineEnvironment`,
|
|
76
|
+
`gooseloop.contrib.ClaudeHandoffEnvironment`) add shape-specific contracts.
|
|
77
|
+
- **Pipeline** (the bookend). Review runs first and emits structured routing.
|
|
78
|
+
Body phases run in queue order (cadence phases + review-spawned branches).
|
|
79
|
+
Summary runs last and reads the final ledger.
|
|
80
|
+
|
|
81
|
+
The contract is documented in [`PROTOCOL.md`](PROTOCOL.md).
|
|
82
|
+
The design history lives in [`docs/adr/`](docs/adr/).
|
|
83
|
+
|
|
84
|
+
## Five minute tour
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from gooseloop import GooseLooper, Engine, Environment, Phase, Pipeline
|
|
88
|
+
|
|
89
|
+
class HelloEnvironment(Environment):
|
|
90
|
+
def env_vars(self) -> dict[str, str]:
|
|
91
|
+
return {"GREETING": "hello"}
|
|
92
|
+
|
|
93
|
+
class HelloEngine(Engine):
|
|
94
|
+
name = "hello-world"
|
|
95
|
+
|
|
96
|
+
def pipeline(self, ctx) -> Pipeline:
|
|
97
|
+
return Pipeline(
|
|
98
|
+
review=Phase(name="review", recipe_path="recipes/review.yaml"),
|
|
99
|
+
body=[Phase(name="greet", recipe_path="recipes/greet.yaml")],
|
|
100
|
+
summary=Phase(name="summary", recipe_path="recipes/summary.yaml"),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
GooseLooper(engine=HelloEngine(), environment=HelloEnvironment()).begin_loop()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The framework ships a working version of this engine under `engines/hello_world/`.
|
|
107
|
+
|
|
108
|
+
## Built-in engines
|
|
109
|
+
|
|
110
|
+
Three reference engines ship in `engines/`. They live alongside the framework,
|
|
111
|
+
not inside the installed wheel, so run them **from a checkout** (the repo root,
|
|
112
|
+
where `engines/` is importable) and select one with `-e <module>`:
|
|
113
|
+
|
|
114
|
+
| Engine | What it does | Run (from the repo root) |
|
|
115
|
+
|--------|--------------|--------------------------|
|
|
116
|
+
| `hello_world` | The minimal reference engine (the tour above). | `python3 -m gooseloop run -e engines.hello_world` |
|
|
117
|
+
| `git_recap` | Summarises your recent commits across configured repos into a changelog. Configure `[git_recap]` in `gooseloop.toml`. | `python3 -m gooseloop run -e engines.git_recap` |
|
|
118
|
+
| `doc_drift` | Finds derived docs/pages that fell behind their canonical source and drafts a patch to seal. Configure `[doc_drift]`, then `cp doc-map.example.toml doc-map.toml` and edit. | `python3 -m gooseloop run -e engines.doc_drift` |
|
|
119
|
+
|
|
120
|
+
Drop `-e` to run whatever `[gooseloop] engine_module` points at (`hello_world`
|
|
121
|
+
by default). The installed `gooseloop` console script is equivalent to
|
|
122
|
+
`python3 -m gooseloop`, but the built-in engines above still need a checkout on
|
|
123
|
+
the path. Add `--review-only` to any of them to stop after the review phase.
|
|
124
|
+
|
|
125
|
+
## Recipe overlay merge
|
|
126
|
+
|
|
127
|
+
Recipes compose docker-compose style. The looper resolves layers in this order:
|
|
128
|
+
|
|
129
|
+
1. Base recipe (declared in `gooseloop.toml`).
|
|
130
|
+
2. `<name>.local.yaml` (gitignored; per-machine).
|
|
131
|
+
3. `--review-overlay X.yaml` / `--summary-overlay X.yaml` (ad-hoc).
|
|
132
|
+
|
|
133
|
+
Inspect the merged result with `gooseloop recipe --resolve <name>`.
|
|
134
|
+
|
|
135
|
+
Merge rules table is in [PROTOCOL.md §6](PROTOCOL.md).
|
|
136
|
+
|
|
137
|
+
## Contrib mixins
|
|
138
|
+
|
|
139
|
+
Domain-shaped Environment ABCs ship under `gooseloop.contrib`:
|
|
140
|
+
|
|
141
|
+
- `CustomerPipelineEnvironment`: customer-acquisition pipelines
|
|
142
|
+
(`build_digest()`, `journal_text()`, `lifecycle_dirs()`, ...).
|
|
143
|
+
- `ClaudeHandoffEnvironment`: Claude design-handoff engines
|
|
144
|
+
(`handoff_dir()`, `target_repo()`, `dev_up_probe()`, ...).
|
|
145
|
+
|
|
146
|
+
An engine with no fit subclasses bare `Environment` and writes one method.
|
|
147
|
+
|
|
148
|
+
## CLI
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
gooseloop run # run the configured engine, one pass
|
|
152
|
+
gooseloop run --review-only # stop after review
|
|
153
|
+
gooseloop run --review-overlay x.yaml --summary-overlay y.yaml
|
|
154
|
+
gooseloop recipe --resolve review # print fully-merged recipe
|
|
155
|
+
gooseloop engines # list known engines from gooseloop.toml
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`gooseloop.toml` at the project root configures which engine, which recipes,
|
|
159
|
+
retry tuning, sessions dir.
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT. See [`LICENSE`](LICENSE).
|
|
164
|
+
|
|
165
|
+
## Provenance
|
|
166
|
+
|
|
167
|
+
`gooseloop` was extracted from Storm Developments' customer-acquisition pipeline
|
|
168
|
+
in June 2026. Storm's customer engine remains the reference consumer; the
|
|
169
|
+
framework is independent and OSS-first.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
gooseloop/__init__.py,sha256=u48uVVU0YS94w-GmQrQn07bRriHqXD55mapB4LhnhMQ,1395
|
|
2
|
+
gooseloop/__main__.py,sha256=TZx_k1Vf_Uaa1HzfHqmKNM8qdfZF49KKl-5gCzabUTU,7040
|
|
3
|
+
gooseloop/artifact.py,sha256=8xKGkpR_wpQh76oUMjsqd4ePIRd_3dTe8HXLfm88_BA,3241
|
|
4
|
+
gooseloop/branch_policy.py,sha256=NjIRYLNdEpWFicDP0LwgoYKXSrqZ8khQAxAc8xrMBUM,2058
|
|
5
|
+
gooseloop/config.py,sha256=9VT7-tq0T8dN8DWef-JvvO9laCF4_mIbdv6tZs3korI,3532
|
|
6
|
+
gooseloop/context_prepend.py,sha256=IG1lFFW6PffYbJpZRvttSpuku7EyqTw3I92dY1L9qio,12701
|
|
7
|
+
gooseloop/engine.py,sha256=SYTGZ92eL5bg9s8paqc1Ue4Bx-W7TusPc9zkFsl-9EU,2519
|
|
8
|
+
gooseloop/environment.py,sha256=hJ-UNaI1k9gEBAAMNM0mUx8wkhkHK1rmqaqxV2SYUho,1185
|
|
9
|
+
gooseloop/extract.py,sha256=PLDcxP5Mf4GKBq3ACkbyZTcLnZ2NjBOWkpT1KY-0NXs,7310
|
|
10
|
+
gooseloop/footer.py,sha256=aNqzlCdzeH1fKdaDyxTykIVioYS7Mpd4gIr13FZeHIY,2776
|
|
11
|
+
gooseloop/goose.py,sha256=yEjB7ZIpfTxY53dhQa3wyvdUF8nml_Mcl1UyMCZ41c0,12101
|
|
12
|
+
gooseloop/looper.py,sha256=LV8ywftiSzviJG4lYSsb9M5vi9YIeRS4uBlgUeohyrc,24432
|
|
13
|
+
gooseloop/phase.py,sha256=UVdR2UOxt6IYX7TzSE9iGAz2z9xh4sgC1r4hQi7BlJU,5895
|
|
14
|
+
gooseloop/predicates.py,sha256=t2WBlYJ5ok_ypdrf_AlaLDV4fBIhdN3D5ZpqkXEGnGw,2425
|
|
15
|
+
gooseloop/protocol.py,sha256=E7qCjoO2indv6MSua_MbEeCnXTxWvmZLAxXq0mSZCys,6995
|
|
16
|
+
gooseloop/recipe_merge.py,sha256=5cfcdIjJWpJTjsDzPs2GDgewnOU-198-MgExgas0c9o,4980
|
|
17
|
+
gooseloop/session.py,sha256=jWfXeJGiILMvs380Oz502PtMrxaWn7NesUxGEkZvQwU,1339
|
|
18
|
+
gooseloop/text.py,sha256=FcxGLSFJd5KlI1z99hLlDYhf4smSR_YJ1iE47ya2l4I,1633
|
|
19
|
+
gooseloop/toolkit.py,sha256=tY_m2sPxJUjjAtovZh9IOZgbDgUAu2RIdOVA5gblDE8,8909
|
|
20
|
+
gooseloop/contrib/__init__.py,sha256=KRoc5DkqtzNcu97BY4M3T6BcW_KL6CF02pL0rRATOi0,760
|
|
21
|
+
gooseloop/contrib/claude_handoff.py,sha256=xz9rDgXqcNnBya8g-5gdaH9ZrSlcX6CXbRv3n6H1pz4,2653
|
|
22
|
+
gooseloop/contrib/customer_pipeline.py,sha256=hzflDtdi2akHLba4745B3vE62nbSxiWGFOCgcWPQSLk,3670
|
|
23
|
+
gooseloop-0.1.0.dist-info/licenses/LICENSE,sha256=c17jwlcrQHxqB8tXRgcHk9ek4J9Dm3m99lcsA_QK8yU,1075
|
|
24
|
+
gooseloop-0.1.0.dist-info/METADATA,sha256=lbW3S3hDzbWsD1-jDQlt2rwUQzAa_6qc2lkiVcOO_WA,6715
|
|
25
|
+
gooseloop-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
26
|
+
gooseloop-0.1.0.dist-info/entry_points.txt,sha256=it6aXQnfzwZSrBfA1DuMuk60Qr5a--F4B3yPzIf3bpM,54
|
|
27
|
+
gooseloop-0.1.0.dist-info/top_level.txt,sha256=8RIsNdZaIfAQLi6nIlMWiYoKM4KDC74ZNIgCY94rGCA,10
|
|
28
|
+
gooseloop-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Storm Developments
|
|
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 @@
|
|
|
1
|
+
gooseloop
|