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/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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gooseloop = gooseloop.__main__:main
@@ -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