loopgate 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.
- loopgate-0.1.0/.githooks/_resolve +47 -0
- loopgate-0.1.0/.githooks/hoist +45 -0
- loopgate-0.1.0/.githooks/pre-commit +7 -0
- loopgate-0.1.0/.githooks/pre-push +10 -0
- loopgate-0.1.0/.githooks/prepare-commit-msg +7 -0
- loopgate-0.1.0/.gitignore +32 -0
- loopgate-0.1.0/LICENSE +21 -0
- loopgate-0.1.0/PKG-INFO +478 -0
- loopgate-0.1.0/README.md +443 -0
- loopgate-0.1.0/docs/PROJECT_STATUS.md +38 -0
- loopgate-0.1.0/docs/PROMPT.md +39 -0
- loopgate-0.1.0/docs/plan.md +68 -0
- loopgate-0.1.0/docs/specs/another_spec.md +56 -0
- loopgate-0.1.0/docs/specs/base.md +56 -0
- loopgate-0.1.0/harness/__init__.py +0 -0
- loopgate-0.1.0/harness/cli.py +532 -0
- loopgate-0.1.0/harness/config.py +203 -0
- loopgate-0.1.0/harness/gate.py +297 -0
- loopgate-0.1.0/harness/js-scaffold/PROMPT.md +8 -0
- loopgate-0.1.0/harness/js-scaffold/README.md +12 -0
- loopgate-0.1.0/harness/js-scaffold/index.html +47 -0
- loopgate-0.1.0/harness/js-scaffold/package-lock.json +176 -0
- loopgate-0.1.0/harness/js-scaffold/package.json +16 -0
- loopgate-0.1.0/harness/js-scaffold/quiz.js +21 -0
- loopgate-0.1.0/harness/js-scaffold/specs/quiz.md +6 -0
- loopgate-0.1.0/harness/js-scaffold/test.js +8 -0
- loopgate-0.1.0/harness/ralph.ps1 +84 -0
- loopgate-0.1.0/harness/ralph.sh +34 -0
- loopgate-0.1.0/harness/temp.pyproject.toml +407 -0
- loopgate-0.1.0/harness/tests/conftest.py +192 -0
- loopgate-0.1.0/harness/tests/test_cli.py +1399 -0
- loopgate-0.1.0/harness/tests/test_gate.py +915 -0
- loopgate-0.1.0/harness/tests/test_init_fixtures.py +270 -0
- loopgate-0.1.0/harness/tests/test_properties.py +148 -0
- loopgate-0.1.0/harness/tests/test_ralph.py +177 -0
- loopgate-0.1.0/harness/tests/test_ralph_ps1.py +150 -0
- loopgate-0.1.0/mutation/check_mutmut.py +163 -0
- loopgate-0.1.0/preferences/__init__.py +0 -0
- loopgate-0.1.0/preferences/preferences.py +306 -0
- loopgate-0.1.0/pyproject.toml +473 -0
- loopgate-0.1.0/tests/mutation/mutmut-cicd-stats.json +11 -0
- loopgate-0.1.0/tests/mutation/test_check_mutmut.py +135 -0
- loopgate-0.1.0/tests/preferences/test_preferences.py +498 -0
- loopgate-0.1.0/tests/preferences/test_preferences_properties.py +245 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Used by the git hooks so they can run checks.
|
|
2
|
+
# e.g.
|
|
3
|
+
# . "$(dirname "$0")/_resolve"
|
|
4
|
+
# exec "$HARNESS" preflight
|
|
5
|
+
#
|
|
6
|
+
# Loads the harness executable.
|
|
7
|
+
# The harness executable was created in cli.py at `def record_harness`` called with `harness install`.
|
|
8
|
+
#
|
|
9
|
+
# $HARNESS is the path recorded by `record_harness`.
|
|
10
|
+
#
|
|
11
|
+
# Then, git hook does not require `uv` or `pip` or any specific environment layout.
|
|
12
|
+
#
|
|
13
|
+
# if is-agent-in-loop, check harness executable found via harness-path
|
|
14
|
+
RALPH_LOOP="${RALPH_LOOP:-0}"
|
|
15
|
+
|
|
16
|
+
if [ "$RALPH_LOOP" = "0" ]; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
recorded="$(git rev-parse --git-common-dir)/harness-path"
|
|
21
|
+
HARNESS=""
|
|
22
|
+
|
|
23
|
+
if [ -r "$recorded" ]; then
|
|
24
|
+
HARNESS="$(cat "$recorded")"
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
if [ ! -x "$HARNESS" ]; then
|
|
28
|
+
missing_harness="$HARNESS"
|
|
29
|
+
HARNESS="$(command -v harness 2>/dev/null || true)"
|
|
30
|
+
if [ ! -x "$HARNESS" ]; then
|
|
31
|
+
printf "\nloopgate: hooks are not installed. Run 'harness install' in this repo.\n" >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
if [ -n "$missing_harness" ]; then
|
|
36
|
+
printf \
|
|
37
|
+
"\nloopgate: recorded harness executable %s is unavailable.\nRecording fallback %s in '.git/harness-path'.\n\n" \
|
|
38
|
+
"$missing_harness" "$HARNESS" >&2
|
|
39
|
+
else
|
|
40
|
+
printf \
|
|
41
|
+
"\nloopgate: no recorded harness executable was found.\nRecording fallback %s in '.git/harness-path'.\n\n" \
|
|
42
|
+
"$HARNESS" >&2
|
|
43
|
+
fi
|
|
44
|
+
printf '%s\n' "$HARNESS" > "$recorded"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
export PATH="$(dirname "$HARNESS")${PATH:+:$PATH}"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Install loopgate hooks into a repo's .githooks without clobbering existing hooks.
|
|
3
|
+
set -eu
|
|
4
|
+
|
|
5
|
+
source_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
6
|
+
target_dir=.githooks
|
|
7
|
+
if [ "$source_dir" = "$(CDPATH= cd -- "$target_dir" 2>/dev/null && pwd)" ]; then
|
|
8
|
+
exit 0
|
|
9
|
+
fi
|
|
10
|
+
cp "$source_dir/_resolve" "$target_dir/_resolve"
|
|
11
|
+
|
|
12
|
+
for name in pre-commit pre-push prepare-commit-msg; do
|
|
13
|
+
source=$source_dir/$name
|
|
14
|
+
hook=$target_dir/$name
|
|
15
|
+
legacy_hook=$target_dir/loopgate-$name
|
|
16
|
+
legacy_call="\"\$(dirname \"\$0\")/loopgate-$name\" \"\$@\" || exit # loopgate"
|
|
17
|
+
case $name in
|
|
18
|
+
pre-commit) command='exec "$HARNESS" preflight' ;;
|
|
19
|
+
pre-push) command='exec "$HARNESS" gate' ;;
|
|
20
|
+
prepare-commit-msg) command='exec "$HARNESS" prepare-commit-msg "$@"' ;;
|
|
21
|
+
esac
|
|
22
|
+
if [ -e "$hook" ] && grep -Fqx "$legacy_call" "$hook"; then
|
|
23
|
+
tmp=$(mktemp "$target_dir/.hoist.XXXXXX")
|
|
24
|
+
grep -Fvx "$legacy_call" "$hook" > "$tmp"
|
|
25
|
+
mv "$tmp" "$hook"
|
|
26
|
+
if [ "$(wc -l < "$hook")" -eq 1 ] && [ "$(head -n 1 "$hook")" = '#!/bin/sh' ]; then
|
|
27
|
+
cp "$source" "$hook"
|
|
28
|
+
fi
|
|
29
|
+
fi
|
|
30
|
+
rm -f "$legacy_hook"
|
|
31
|
+
if [ ! -e "$hook" ]; then
|
|
32
|
+
cp "$source" "$hook"
|
|
33
|
+
elif ! cmp -s "$source" "$hook" && ! grep -Fqx ') || exit # loopgate' "$hook"; then
|
|
34
|
+
case $(head -n 1 "$hook") in "#!"*sh*) ;; *) echo "hoist: $hook is not a shell hook" >&2; exit 1 ;; esac
|
|
35
|
+
tmp=$(mktemp "$target_dir/.hoist.XXXXXX")
|
|
36
|
+
{
|
|
37
|
+
IFS= read -r first
|
|
38
|
+
printf '%s\n%s\n%s\n%s\n%s\n' \
|
|
39
|
+
"$first" '(' ' . "$(dirname "$0")/_resolve"' " $command" ') || exit # loopgate'
|
|
40
|
+
cat
|
|
41
|
+
} < "$hook" > "$tmp"
|
|
42
|
+
mv "$tmp" "$hook"
|
|
43
|
+
fi
|
|
44
|
+
chmod +x "$hook"
|
|
45
|
+
done
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Ralph pre-push gate: lint, types, security (semgrep), tests, 100% coverage (blocking),
|
|
3
|
+
# plus a format report (informative, never blocks)
|
|
4
|
+
# on what is about to be pushed. The pre-commit hook is kept fast; this is the heavy
|
|
5
|
+
# pass, and the local backstop before code leaves the machine.
|
|
6
|
+
# Bypassing this hook is forbidden by AGENTS.md.
|
|
7
|
+
set -eu
|
|
8
|
+
|
|
9
|
+
. "$(dirname "$0")/_resolve"
|
|
10
|
+
exec "$HARNESS" gate
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
*.py[cod]
|
|
2
|
+
__pycache__/
|
|
3
|
+
.venv/
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
.ruff_cache/
|
|
6
|
+
.hypothesis/
|
|
7
|
+
.coverage
|
|
8
|
+
.DS_Store
|
|
9
|
+
.claude
|
|
10
|
+
.codex
|
|
11
|
+
.vscode/
|
|
12
|
+
docs/launch.md
|
|
13
|
+
test-results/
|
|
14
|
+
**/node_modules/
|
|
15
|
+
dist/
|
|
16
|
+
.tox/
|
|
17
|
+
|
|
18
|
+
.env
|
|
19
|
+
.env.*
|
|
20
|
+
|
|
21
|
+
# in-repo worker worktrees (isolated agent workspaces, never /tmp)
|
|
22
|
+
.worktrees/
|
|
23
|
+
|
|
24
|
+
# scratchpad: ship the dir + agent doc, ignore everything agents/runs write into it
|
|
25
|
+
scratchpad/*
|
|
26
|
+
!scratchpad/README.md
|
|
27
|
+
!scratchpad/runs/
|
|
28
|
+
scratchpad/runs/*
|
|
29
|
+
!scratchpad/runs/.gitkeep
|
|
30
|
+
|
|
31
|
+
# mutmut's mutated copies of the source. Ignoring them is also what keeps ruff and semgrep out.
|
|
32
|
+
mutants/
|
loopgate-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rxdt
|
|
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.
|
loopgate-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: loopgate
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Gated, harnessed, autonomy loop with quality checks.
|
|
5
|
+
Author: rxdt
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: agent,autonomy,claude,codex,harness,llm,loop,scaffold
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
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: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: complexipy
|
|
21
|
+
Requires-Dist: coverage
|
|
22
|
+
Requires-Dist: hypothesis
|
|
23
|
+
Requires-Dist: mutmut>=3.7.0
|
|
24
|
+
Requires-Dist: pip-audit
|
|
25
|
+
Requires-Dist: pylint
|
|
26
|
+
Requires-Dist: pyright
|
|
27
|
+
Requires-Dist: pytest
|
|
28
|
+
Requires-Dist: pytest-cov
|
|
29
|
+
Requires-Dist: pytest-xdist
|
|
30
|
+
Requires-Dist: ruff
|
|
31
|
+
Requires-Dist: semgrep
|
|
32
|
+
Requires-Dist: tomlkit
|
|
33
|
+
Requires-Dist: typer
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
<div align="center">
|
|
37
|
+
<img src=".assets/.banner.svg" alt="Blue infinity loop" width="360">
|
|
38
|
+
|
|
39
|
+
<h1>L∞pGate</h1>
|
|
40
|
+
<h4>Run coding agents strictly and only accept changes that pass your quality gates.</h4>
|
|
41
|
+
<p>A loop harness for Claude, Codex, Copilot, or any CLI agent. A loop runner hands each agent a prompt. Agents can edit. Gates decide what lands. You set the plan. Each agent iteration must update specs and commit through quality guardrails.</p>
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+
[](LICENSE)
|
|
45
|
+
[](https://makeapullrequest.com)
|
|
46
|
+
[](https://www.firsttimersonly.com/)
|
|
47
|
+

|
|
48
|
+

|
|
49
|
+

|
|
50
|
+
[](https://github.com/sebmestrallet/absurd-badges)
|
|
51
|
+
[](https://github.com/sebmestrallet/absurd-badges)
|
|
52
|
+

|
|
53
|
+
[](https://github.com/rxdt/loopgate_harness/actions/workflows/ci.yml)
|
|
54
|
+

|
|
55
|
+
[](https://github.com/rxdt/loopgate_harness/actions/workflows/mutation.yml)
|
|
56
|
+
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## TL;DR
|
|
62
|
+
|
|
63
|
+
1. `gh repo create <your-github-username>/<your-new-app-name> --template rxdt/loopgate_harness --private --clone && cd <your-new-app-name> && uv run harness install && source .venv/bin/activate && git add . && git commit --amend --no-edit`
|
|
64
|
+
2. `harness gate` and to loop `harness codex`
|
|
65
|
+
|
|
66
|
+
**Requirements**: `pip`, `uv`, or `poetry`; Python 3.10 or newer; Linux or macOS. Windows support is experimental.
|
|
67
|
+
|
|
68
|
+
## Index
|
|
69
|
+
|
|
70
|
+
- [Features](#features)
|
|
71
|
+
- [Default Tools](#default-tools)
|
|
72
|
+
- [Details](#details)
|
|
73
|
+
- [Start a project](#start-a-project)
|
|
74
|
+
- [A L∞Pgate Loop](#a-lpgate-loop)
|
|
75
|
+
- [Commands](#commands)
|
|
76
|
+
- [Directory Layout](#directory-layout)
|
|
77
|
+
- [Expanding your harness](#expanding-your-harness)
|
|
78
|
+
- [FAQ](#faq)
|
|
79
|
+
- [Coordination](#coordination)
|
|
80
|
+
- [Before infinity loops](#read-this-before-a-first-run)
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Features
|
|
85
|
+
|
|
86
|
+
Each run starts fresh, has clear limits, saves its logs, protects key files, and must pass checks you choose.
|
|
87
|
+
- **Quality-first**: Fight the AI slop with standards and style 💯
|
|
88
|
+
- **Gate-enforced work**: Agent changes land only if they pass the quality gates you set ✅
|
|
89
|
+
- **L∞P now. What boilerplate?** Start loopiing with quality checks from day one 🔄
|
|
90
|
+
- **Built-in stack**: lint, format checks, type-checks, dependency audit, property tests, mutation tests, 100% coverage, complexity analysis, and Semgrep ☑☑☑, git hooks, are the BASICS
|
|
91
|
+
- **Hooks ready to go**: pre-commit, pre-push, and commit-message hooks are already hooked up ➜]
|
|
92
|
+
- **Worker-agnostic**: Claude, Codex, Copilot, Agy, or any prompt-reading CLI
|
|
93
|
+
- **Repo-as-memory workflow**: specs/status/prompt are durable but code is king, leaving you free 😎
|
|
94
|
+
- **No-rot**: Fresh-context agent iterations to reduce context rot 🧠
|
|
95
|
+
- **Simple**: One-command setup gets you going 🆗
|
|
96
|
+
- **Installable project template**: `harness install` gets a repo ready! 🚀
|
|
97
|
+
- **Existing-repo setup**: `harness init` adds LoopGate without requiring the template
|
|
98
|
+
- **Progressive**: Preflight vs full gate split ᯓ➤
|
|
99
|
+
- **Forbidden-file containment**: Don't touch that!-configurable. Set forbidden files for agents ✋
|
|
100
|
+
- **No-waste**: Timeouts and time-limits for all looping agents ⏰
|
|
101
|
+
- **Diff size guardrails**: Agent's staged Lines Of Code at error 500 and prompt slop refactor 🤌
|
|
102
|
+
- **No empty work**: Agents blocked from empty commits ⬜
|
|
103
|
+
- **Agent containment prioritized**: Stop the madness
|
|
104
|
+
- **Industrial Security** Enabled with [Semgrep](https://semgrep.dev/) 🔓
|
|
105
|
+
- **Interactive-agent containment too!**: Run `harness configure-agents` so all Claude/Codex sessions are beholden to the repo checks 🔥
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Default Tool Dependencies
|
|
110
|
+
_Edit at will_
|
|
111
|
+
- [ruff](https://docs.astral.sh/ruff/) lints and formats Python code, fast
|
|
112
|
+
- [pylint](https://pypi.org/project/pylint/) catches code errors and style problems
|
|
113
|
+
- [pydoclint](https://pypi.org/project/pydoclint/0.9.1/) checks docstrings match function signatures
|
|
114
|
+
- [pyright](https://github.com/microsoft/pyright) enforces types before code ever runs
|
|
115
|
+
- [pytest](https://docs.pytest.org/en/stable/) runs the project's test suite (runs across multiple CPUs)
|
|
116
|
+
- [hypothesis](https://hypothesis.readthedocs.io/) generates test inputs to expose edge cases.
|
|
117
|
+
**_Tests the code_.** [Real Example](tests/preferences/test_properties.py)
|
|
118
|
+
- [mutmut](https://mutmut.readthedocs.io/) mutates your code to find weak tests.
|
|
119
|
+
**_Tests the tests_.** Easy to use script at [check_mutmut.py](https://github.com/rxdt/loopgate_harness/blob/main/mutation/check_mutmut.py).
|
|
120
|
+
- [complexipy](https://github.com/rohaquinlop/complexipy) flags functions that are too complex
|
|
121
|
+
- [semgrep](https://docs.semgrep.dev/semgrep-ci/sample-ci-configs) scans code for security flaws
|
|
122
|
+
- [pip-audit](https://github.com/pypa/pip-audit) scans Python environments for package vulnerabilities. Switch out with `["uv", "audit"]` for faster, less-mature [uv audit](https://astral.sh/blog/uv-audit) here: _[audit](pyproject.toml#146)_
|
|
123
|
+
- [preferences.py](preferences/preferences.py) A custom AST-parser to optionally expand. It catches e.g. a [style preference](https://google.github.io/styleguide/pyguide) that tools don't.
|
|
124
|
+
- Forbidden paths set in [[tool.harness]](pyproject.toml)
|
|
125
|
+
- Update `[tool.harness.gate]` or `[tool.harness.gate]` in [pyproject](pyproject.toml) to change what is checked before a commit or push.
|
|
126
|
+
- Failing checks block by default. If a fresh drop-in fails across the board, set [`behavior = "warn"`](pyproject.toml#L87) to report without blocking, then flip back to `"fail"`.
|
|
127
|
+
|
|
128
|
+
### The Gate: Tiered Checks
|
|
129
|
+
|
|
130
|
+
> [!NOTE]
|
|
131
|
+
> **A pre-commit or gate phase self-heals by un-staging forbidden files.**
|
|
132
|
+
|
|
133
|
+
⚡ `harness preflight` _(pre-commit)_
|
|
134
|
+
|
|
135
|
+
are the fast checks to run often. Lint + check format for everyone, _plus_ **containment** for the agents.
|
|
136
|
+
|
|
137
|
+
✅ `harness gate` _(pre-push)_ = _(pre-commit)_ checks **+**
|
|
138
|
+
|
|
139
|
+
adds type-checks, security audit, dependency audit, AST-scan, complexity analysis, full test coverage, prompt to run mutmut
|
|
140
|
+
|
|
141
|
+
Only humans can bypass triggered gates and commit, always. Only humans can use flag `--no-verify`.
|
|
142
|
+
|
|
143
|
+
> [!IMPORTANT]
|
|
144
|
+
> ### Network Access
|
|
145
|
+
|
|
146
|
+
Note that `semgrep --config auto` needs network for semgrep registry rules.
|
|
147
|
+
`pip-audit` and `uv audit` also need a network connection to scan the repo.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Details
|
|
152
|
+
|
|
153
|
+
[plan.md](docs/plan.md) is where you define what you want the end product to be. You must be _VERY_ clear on what the finished product should and should **not** contain. Do **not** let agents guess. Tell agents *exactly* what the finished "product" should be.
|
|
154
|
+
|
|
155
|
+
[`PROMPT`](docs/PROMPT.md) tells each agent to pick a spec.md and build. [`specs/`](docs/specs) say _what_ to build. The agent decides _what next_. You keep [`plan.md`](docs/plan.md) current, and specs get rewritten from it. The agent is told in `docs/PROMPT.md` to update the specs. Each iteration the agent updates its spec and [`PROJECT_STATUS`](docs/PROJECT_STATUS.md).
|
|
156
|
+
|
|
157
|
+
### In summary, your job, the bare minimum:
|
|
158
|
+
<mark>write something into the plan</mark>
|
|
159
|
+
|
|
160
|
+
## Start a project
|
|
161
|
+
## Start a project
|
|
162
|
+
|
|
163
|
+
1. `gh repo create <your-github-username>/<your-new-app-name> --template rxdt/loopgate_harness --private --clone` **or**
|
|
164
|
+
['Use This Template'](https://github.com/new?template_name=loopgate_harness&template_owner=rxdt)
|
|
165
|
+
2. Source your environment (if applicable)
|
|
166
|
+
3. From the root, install dependencies and run `harness install` to remove template-only files and set up the git hooks. For example: `uv sync && source .venv/bin/activate && harness install`.
|
|
167
|
+
4. `git commit` (the `install` command updates the repo)
|
|
168
|
+
5. Write your grand vision in [docs/plan.md](docs/plan.md)
|
|
169
|
+
6. Optionally add the first spec in `docs/specs/` (or leave it to the agents to draft the first specs based on your `plan.md`)
|
|
170
|
+
7. Product code goes in [`src/`](src/). _List your source code directories in [`pyproject.toml [tool.coverage.run] line 234`](pyproject.toml#toolcoveragerun)_
|
|
171
|
+
8. Run some loops!
|
|
172
|
+
|
|
173
|
+
`harness run <agent=claude|codex|agy|copilot> [iterations] [minutes]`
|
|
174
|
+
9. Not what you wanted? Refine [`docs/plan.md`](docs/plan.md) / [`docs/PROMPT.md`](docs/PROMPT.md) and re-run
|
|
175
|
+
10. Configurations for Ruff linting, type-checking Pyright, Complexipy, Pytest coverage, etcetera are set in [`pyproject.toml`](pyproject.toml).
|
|
176
|
+
11. Your coding quirks go in [`preferences/preferences.py`](preferences/preferences.py). Delete functions that don't serve you. Add your own.
|
|
177
|
+
|
|
178
|
+
> [!IMPORTANT]
|
|
179
|
+
> Default configuration is in [`pyproject.toml`](pyproject.toml). Update tool settings, add agent commands, change checks, or leave it as is.
|
|
180
|
+
|
|
181
|
+
### Works with `uv`, `poetry`, or `pip`
|
|
182
|
+
|
|
183
|
+
```sh
|
|
184
|
+
uv sync
|
|
185
|
+
source .venv/bin/activate
|
|
186
|
+
harness install
|
|
187
|
+
git add . && git commit
|
|
188
|
+
harness gate
|
|
189
|
+
harness run <agent>
|
|
190
|
+
|
|
191
|
+
poetry install
|
|
192
|
+
poetry run harness install
|
|
193
|
+
git add . && git commit
|
|
194
|
+
poetry run harness gate
|
|
195
|
+
poetry run harness run <agent>
|
|
196
|
+
|
|
197
|
+
python -m venv .venv
|
|
198
|
+
source .venv/bin/activate
|
|
199
|
+
python -m pip install -r requirements.txt -e .
|
|
200
|
+
harness install
|
|
201
|
+
git add . && git commit
|
|
202
|
+
harness gate
|
|
203
|
+
harness run <agent>
|
|
204
|
+
```
|
|
205
|
+
> [!TIP]
|
|
206
|
+
> If you don't like _ANYTHING_ in this framework, [update it](#expanding-your-harness). Or even better, [contribute](CONTRIBUTING.md).
|
|
207
|
+
>
|
|
208
|
+

|
|
209
|
+
|
|
210
|
+
## A L∞Pgate Loop / Graph / Universe
|
|
211
|
+
|
|
212
|
+
The repo is the only memory. Each iteration is a fresh-context agent, driven by our loop runner, [Ralph](#faq).
|
|
213
|
+
|
|
214
|
+
- `docs/specs/` say WHAT to build
|
|
215
|
+
- constant `docs/PROMPT.md` tells the agent: read `docs/specs/`, review `src/`, build the most important unfinished thing
|
|
216
|
+
- agent builds
|
|
217
|
+
- agent commits
|
|
218
|
+
- every git commit passes the fast preflight (lint, format, plus loop containment for the agent)
|
|
219
|
+
- every git push runs the full gate: lint, types, semgrep, tests, 100% coverage
|
|
220
|
+
- the loop stops at `max_iterations`, a nonzero worker exit, or a timeout
|
|
221
|
+
- Unspecified iterations/minutes → default to 2 iterations × 20 minutes each
|
|
222
|
+
- Each run streams agent 'thought' output live and is saved in a local scratchpad log
|
|
223
|
+
- **The harness is worker-agnostic.** Any agent CLI that reads a prompt from stdin and can edit/commit works.
|
|
224
|
+
|
|
225
|
+

|
|
226
|
+
|
|
227
|
+
## Commands
|
|
228
|
+
|
|
229
|
+
Tool commands are defined in `[tool.harness]` in [pyproject.toml](pyproject.toml).
|
|
230
|
+
|
|
231
|
+
```sh
|
|
232
|
+
harness install # install dependencies, remove template-only files, and set up git hooks
|
|
233
|
+
harness init # add LoopGate to an existing repository (configurations copied from your config files)
|
|
234
|
+
harness configure-agents # configures Claude and Codex with containment rules and environment variables
|
|
235
|
+
harness preflight # fast checks: preferences, ruff lint + format (plus loop containment)
|
|
236
|
+
harness gate # full pass: preferences, ruff, format, pyright, pylint, complexipy, semgrep, pip-audit, pytest @ 100% cov, hypothesis
|
|
237
|
+
harness info # show configured agents, checks, and protected paths
|
|
238
|
+
harness status # shows run log link, the newest json / latest run of N loops, 1 iteration
|
|
239
|
+
RALPH_LOOP=1 harness gate # explicitly run as if you are the agent in the loop
|
|
240
|
+
harness run <agent> [max_iterations] [max_minutes] [verbose] # claude/codex/agy/copilot, defaults: 2 20 True
|
|
241
|
+
|
|
242
|
+
# AGENT CALLS, examples
|
|
243
|
+
harness run claude 10 20
|
|
244
|
+
harness run codex 2 20
|
|
245
|
+
harness run agy 3 10
|
|
246
|
+
harness run copilot 2 20
|
|
247
|
+
```
|
|
248
|
+
#### To run LoopGate with any agent, the worker must be installed and authenticated separately.
|
|
249
|
+
|
|
250
|
+
### Add a mutation score badge
|
|
251
|
+
|
|
252
|
+
Run `uv run mutmut run && uv run mutmut export-cicd-stats`, then use [check_mutmut.py](mutation/check_mutmut.py) to write `mutation-score.json` for the Shields badge.
|
|
253
|
+
|
|
254
|
+
### Containment
|
|
255
|
+
|
|
256
|
+
`harness run` launches an autonomous LLM worker with the configured permissions, e.g.
|
|
257
|
+
`--permission-mode acceptEdits` or `--sandbox danger-full-access`.
|
|
258
|
+
|
|
259
|
+
`harness configure-agents` will set rules for Codex and Claude. It sets `RALPH_LOOP=1` in `~/.claude/settings.json` and `~/.codex/config.toml` (and saves backup files), plus rules that stop agents from changing those settings. **So interactive IDE AND terminal sessions face the same gates as loop workers.** You can read the exact rules at [config.py:23](harness/config.py#L23) (Codex) and [config.py:174](harness/config.py#L174) (Claude).
|
|
260
|
+
|
|
261
|
+
The gate bounds what any **commit** may touch, but the worker itself is **not** sandboxed to this repo unless you set that config. Consider the balance: without access it cannot do much. With machine access it can wreak havoc. Under a permissive mode it can run arbitrary shell. You are authorizing real changes. Choose the worker and permission mode deliberately.
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
<details>
|
|
265
|
+
<summary>
|
|
266
|
+
|
|
267
|
+
## Directory Layout
|
|
268
|
+
|
|
269
|
+
</summary>
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
harness/ the gate, loop runner, CLI (🤖 forbidden directory)
|
|
273
|
+
tests/ the harness's own tests
|
|
274
|
+
gate.py run the full local gate + honor preferences.py
|
|
275
|
+
cli.py command-line entry point
|
|
276
|
+
js-scaffold javascript example to build upon
|
|
277
|
+
preferences/ user-defined preferences not covered by tools (🤖 forbidden directory)
|
|
278
|
+
mutation/ get your mutation score and learn how to run mutmut (🤖 forbidden directory)
|
|
279
|
+
check_mutant.py
|
|
280
|
+
tests/
|
|
281
|
+
preferences/ (🤖 tests/preferences is forbidden directory)
|
|
282
|
+
.githooks/ pre-commit / pre-push gate hooks (🤖 forbidden directory)
|
|
283
|
+
pyproject.toml project + tooling config (🤖 forbidden file)
|
|
284
|
+
docs/ PROMPT, specs/, your plan (agent and human maintained)
|
|
285
|
+
scratchpad/ scratch dir agents can use for temp files (For the 🤖 to play)
|
|
286
|
+
src/ your product/source code (add to coverage source)
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Run logs
|
|
290
|
+
|
|
291
|
+
Every run is saved as a log file in `scratchpad/runs/`. `harness status` shows how many logs you have and the path to the newest one. Open that file to read what the agent thought and did. _(Metrics and audited logs coming soon.)_
|
|
292
|
+
|
|
293
|
+
[`pyproject.toml`](pyproject.toml) is the single source of harness configuration. Humans own it and [`preferences/`](preferences/); both are agent-protected.
|
|
294
|
+
|
|
295
|
+
If an agent edits a forbidden file, the file will be unstaged (not allowed to commit). A forbidden pattern by an agent (e.g. `# noqa` or `nosemgrep` will also prevent their commit and force them to fix it.)
|
|
296
|
+
|
|
297
|
+
</details>
|
|
298
|
+
|
|
299
|
+
<details>
|
|
300
|
+
<summary>
|
|
301
|
+
|
|
302
|
+
## Expanding your harness </summary>
|
|
303
|
+
|
|
304
|
+
- Edit rules at [pyproject.toml](pyproject.toml) for [ruff](https://docs.astral.sh/ruff/), [pylint](https://pypi.org/project/pylint/), [pydoclint](https://pypi.org/project/pydoclint/0.9.1/), [pyright](https://github.com/microsoft/pyright), [pytest](https://docs.pytest.org/en/stable/), [hypothesis](https://hypothesis.readthedocs.io/), [complexipy](https://github.com/rohaquinlop/complexipy), [mutmut](https://mutmut.readthedocs.io/)
|
|
305
|
+
- Add forbidden files, directories, or patterns in `[tool.harness]` at [pyproject.toml](pyproject.toml)
|
|
306
|
+
- Add [Hypothesis](https://hypothesis.readthedocs.io/) tests in any test directory, examples at [test_properties.py](tests/preferences/test_properties.py).
|
|
307
|
+
- Run [mutmut](https://mutmut.readthedocs.io/) by hand with `uv run mutmut run`, then `uv run mutmut browse`. A surviving mutant is source code in need of a better test. Easy to use script at [check_mutmut.py](https://github.com/rxdt/loopgate_harness/blob/main/mutation/check_mutmut.py).
|
|
308
|
+
- [semgrep](https://docs.semgrep.dev/semgrep-ci/sample-ci-configs) has no repo config here. It uses registry configs / Semgrep's built-in defaults which ignore tests. Feel free to add a configuration file for it or any tool.
|
|
309
|
+
- Update `[tool.harness.gate.checks]` in [pyproject.toml](pyproject.toml) to change the full gate.
|
|
310
|
+
- Run the same gate in CI by adding a step to your workflow (e.g. `.github/workflows/ci.yml`):
|
|
311
|
+
|
|
312
|
+
```yaml
|
|
313
|
+
- run: uv sync && uv run harness gate
|
|
314
|
+
```
|
|
315
|
+
- Add or remove coding preferences [preferences.py](preferences/preferences.py) that only agents **must** respect. Current "preferences":
|
|
316
|
+
|
|
317
|
+
```py
|
|
318
|
+
function_argument_assignment_has_star # agents use non-specific `def fun(*)`
|
|
319
|
+
named_with_underscore_and_not_in_class_or_dunder # agents love over-using underscore names `def _fun()`
|
|
320
|
+
hidden_signature_star_args # Complain when a function uses *args or **kwargs (it hides function signatures)
|
|
321
|
+
dynamic_star_call # Calls to def fun(*items) breaks when you can't tell how many arguments f is getting
|
|
322
|
+
pointless_class # ensure classes are added for good reasons (carry state, values, methods)
|
|
323
|
+
lazy_assert # enforce real assertions, stronger tests
|
|
324
|
+
objects_injected_into_runtime_memory # finds calls that manipulate global state (dangerous, tricky)
|
|
325
|
+
lambda_found # abolish lambdas, make agents keep their code simple
|
|
326
|
+
lazy_any_type_hints # abolish type `Any` used to bypass strict type-checking
|
|
327
|
+
chaotic_continue_statements # abolish unecessary nested continue statements, clean code
|
|
328
|
+
complex_comprehension # no needlessly dense list/set/dict comprehensions, prefer linear code
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
</details>
|
|
332
|
+
|
|
333
|
+
<details>
|
|
334
|
+
<summary>
|
|
335
|
+
|
|
336
|
+
### FAQ </summary>
|
|
337
|
+
|
|
338
|
+
- **Who is Ralph?**
|
|
339
|
+
|
|
340
|
+
Ralph is our name for LoopGate's loop runner — the small program that starts your coding agent, hands it the prompt, and starts a fresh agent when the last one finishes. The name comes from the "Ralph Wiggum" technique: run an agent in a simple loop, over and over, until the work is done. Anything starting with `RALPH_` (like `RALPH_LOOP=1`) is just a setting Ralph gives the agent that says "you are inside the loop, follow the loop rules."
|
|
341
|
+
|
|
342
|
+
- **`harness run <agent>` exits immediately / can't find the worker?**
|
|
343
|
+
|
|
344
|
+
LoopGate does not install or log in agent CLIs. Install and authenticate the worker you selected (`claude`, `codex`, `copilot`, or `agy`), confirm it is on your `PATH` (e.g. `which codex`), then retry. If `which` finds the binary but the run still fails, finish that tool's login/auth flow and retry `harness run`.
|
|
345
|
+
|
|
346
|
+
- **What is the difference between a gate and a sandbox?**
|
|
347
|
+
|
|
348
|
+
A **gate** is a workflow checkpoint that evaluates code and decides whether it is allowed to land in your commits. A **sandbox** is an isolated OS-level environment designed to prevent code from modifying your underlying machine. LoopGate uses gates to control your git history, but it does _not_ provide a secure OS sandbox.
|
|
349
|
+
|
|
350
|
+
- **What if I don't want to use those agents?**
|
|
351
|
+
|
|
352
|
+
Remove or add commands to enable using different agents. Let's say you're going _exclusive_ with Mistral and will _only_ use Mistral 3. Set this and only this in [`[tool.harness.agents]`](pyproject.toml#L91-L123)
|
|
353
|
+
```
|
|
354
|
+
vibe = ["vibe", "--auto-approve", "--output", "streaming"]
|
|
355
|
+
```
|
|
356
|
+
Then run it with `harness run vibe <loop-count> <max-minutes-per-loop`
|
|
357
|
+
|
|
358
|
+
- **What if I don't want to build an app in Python?**
|
|
359
|
+
|
|
360
|
+
You don’t have to. The loop runner, Ralph, and the CLI take a prompt, launch agents pointed at markdown files. LoopGate is language-agnostic at the agent-loop level, only this template repo is configured to be Python-specific at [pyproject.toml](pyproject.toml). You can add _your_ language and your _tool commands_ for your checks to run there.
|
|
361
|
+
|
|
362
|
+
- **Javascript?**
|
|
363
|
+
|
|
364
|
+
The included [`harness/js-scaffold`](harness/js-scaffold/package.json) is a simple JavaScript **example** to expand on. Go to [pyproject.toml line 75](pyproject.toml#L75). Update checks. Put `js` into list `[tool.harness].languages`. Remove `py` if unused.
|
|
365
|
+
|
|
366
|
+
```
|
|
367
|
+
npm run --prefix harness/js-scaffold gate
|
|
368
|
+
npm run --prefix harness/js-scaffold preflight
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
- **Why not just a shell loop?**
|
|
372
|
+
|
|
373
|
+
A shell loop only reruns an agent. LoopGate ensures fresh context, durable repo state, time and iteration limits, protected paths, and quality gates that stop bad changes _before_ they land. Also, we like to keep our shell loops dumb round these parts. And Let the configs do the lifting.
|
|
374
|
+
|
|
375
|
+
#### LoopGate Glossary
|
|
376
|
+
|
|
377
|
+
Short definitions of common LoopGate terms.
|
|
378
|
+
|
|
379
|
+
- harness: the LoopGate tool that runs agents, manages loops, and checks changes.
|
|
380
|
+
- loop: one cycle of work where the worker reads instructions, works on a spec, makes changes, runs checks, and records progress.
|
|
381
|
+
- worker: the coding agent that does the work, such as Claude, Codex, or Copilot.
|
|
382
|
+
- gate: the checks that decide whether changes can be accepted. LoopGate runs these checks locally and in CI. aka pre-push, what happens right before a diff is pushed to origin.
|
|
383
|
+
- preflight: the quick checks that run before the full gate to catch common problems early. aka 'pre-commit', what happens right before files are git committed.
|
|
384
|
+
- prompt: the instructions in `docs/PROMPT.md` that tell the worker what to do in each loop.
|
|
385
|
+
- spec: a file in `docs/specs/` that describes what needs to be built.
|
|
386
|
+
- Ralph: LoopGate's loop runner. It starts the worker, gives it the prompt, and runs the workflow for each iteration.
|
|
387
|
+
|
|
388
|
+
#### LoopGate Glossary
|
|
389
|
+
|
|
390
|
+
Short definitions of common LoopGate terms.
|
|
391
|
+
|
|
392
|
+
- harness: the LoopGate tool that runs agents, manages loops, and checks changes.
|
|
393
|
+
- loop: one cycle of work where the worker reads instructions, works on a spec, makes changes, runs checks, and records progress.
|
|
394
|
+
- worker: the coding agent that does the work, such as Claude, Codex, or Copilot.
|
|
395
|
+
- gate: the checks that decide whether changes can be accepted. LoopGate runs these checks locally and in CI.
|
|
396
|
+
- preflight: the quick checks that run before the full gate to catch common problems early.
|
|
397
|
+
- prompt: the instructions in `docs/PROMPT.md` that tell the worker what to do in each loop.
|
|
398
|
+
- spec: a file in `docs/specs/` that describes what needs to be built.
|
|
399
|
+
- Ralph: LoopGate's loop runner. It starts the worker, gives it the prompt, and runs the workflow for each iteration.
|
|
400
|
+
|
|
401
|
+
</details>
|
|
402
|
+
|
|
403
|
+
<details>
|
|
404
|
+
<summary>
|
|
405
|
+
|
|
406
|
+
## Coordination </summary>
|
|
407
|
+
|
|
408
|
+
- Use `git log --oneline <branch>..HEAD` to show what's unpushed.
|
|
409
|
+
- There is NO worktree/branch creation by design. You can create branches/trees and run a loop in each, then merge _(if you feel like managing that)_
|
|
410
|
+
- Agent duties can be contained to a part of the repo. e.g. Codex-1-frontend uses `docs/specs/frontend.md`, Claude-2-researcher `docs/specs/backend`...
|
|
411
|
+
|
|
412
|
+
### If you want to run a graph
|
|
413
|
+
|
|
414
|
+
**Recommendations for running several agents at once on one branch (no worktrees):**
|
|
415
|
+
|
|
416
|
+
- **You (human):** seed each spec once with this exact line near the top:
|
|
417
|
+
|
|
418
|
+
```
|
|
419
|
+
Spec claimed by agent: <unclaimed>
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
- **The agents:** paste this exact block into [PROMPT.md line 3](docs/PROMPT.md#L3):
|
|
423
|
+
|
|
424
|
+
```
|
|
425
|
+
Other agents are working this repo. Before touching code, pick a spec whose claim line is
|
|
426
|
+
<unclaimed>, replace it with your exact name `<your-agent-id>-<spec-you-worked>-<RALPH_ITERATION>/<MAX_ITERATIONS>`, e.g. `claude-0003-backend-3/3`, and commit that claim first. Own that spec's file and its tests. Set the line back to <unclaimed> on your last commit.
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
`RALPH_ITERATION` and `MAX_ITERATIONS` are numbers the loop hands each agent: which round it is on, and the total rounds allowed.
|
|
430
|
+
|
|
431
|
+
- What fails when agents do not claim specs/work: agents all pick the top-priority spec, duplicate work, and leave a half-staged git index.
|
|
432
|
+
- What fails with too little time i.e. MAX_MINUTES too low: a worker dies mid-`gate` before it can commit. Give each iteration enough minutes to finish (the gate itself takes a while). One successful iteration needs ~2-3 min of pure overhead aside from 'real' work.
|
|
433
|
+
- A worker killed too soon leaves its spec claim STUCK: spec stays locked to its name. No other agent will take it until a human resets the line to `<unclaimed>`.
|
|
434
|
+
- preflight on git commit: ~ a few seconds
|
|
435
|
+
- full gate on git push: ~20-48s
|
|
436
|
+
- push + cleanup: ~ few seconds -
|
|
437
|
+
- Do not rely on agent names for coordination: agents self-name inconsistently and can collide (e.g. two both call themselves the same thing). Names are for human blame/log-matching only; the claim line + committed code are what actually coordinate.
|
|
438
|
+
|
|
439
|
+
- Which doc does what:
|
|
440
|
+
- **specs** = the product work
|
|
441
|
+
- **`docs/PROMPT.md`** = how to operate headlessly
|
|
442
|
+
- **repo + green gate** = the source of truth
|
|
443
|
+
- `docs/PROJECT_STATUS.md` is a human-readable record, not authoritative
|
|
444
|
+
|
|
445
|
+
- No branch/worktree creation in this harness was intentional:
|
|
446
|
+
1. For simplicity and maintainability of the framework.
|
|
447
|
+
2. Because a fresh iteration can't see the unmerged work in another worktree, so agents miss context and scramble to merge while conflicts pile up.
|
|
448
|
+
3. Change this behavior as you like.
|
|
449
|
+
|
|
450
|
+
</details>
|
|
451
|
+
|
|
452
|
+
<details>
|
|
453
|
+
<summary>
|
|
454
|
+
|
|
455
|
+
## Read before Infinity Loops </summary>
|
|
456
|
+
## Read before Infinity Loops </summary>
|
|
457
|
+
|
|
458
|
+
1. **This harness does not sandbox agents.** It tries to harness bad code in loops via gates. Sandboxing agents will, e.g. prevent them from maintaining git, running Playwright, being seen as trustworthy by semgrep leading to cyclical failures, etc.
|
|
459
|
+
|
|
460
|
+
2. **The gate is a guardrail, not a jail.** Agents are crafty, like people. They will find a way to complete a task at all costs. **Trust nothing and no one.**
|
|
461
|
+
|
|
462
|
+
3. **Mind your usage limits.** `harness run` works agents to the cap set. You can easily burn through your tokens, context windows, and provider usage limits. **Workers continue running as long as there is work to do.**
|
|
463
|
+
|
|
464
|
+
4. **`docs/PROMPT.md` tells the worker to push or not**.
|
|
465
|
+
|
|
466
|
+
5. Protect `main` and run the loop on its own branch.
|
|
467
|
+
|
|
468
|
+
6. **100% coverage does not mean good tests.** That is quantity, not quality. Run `uv run mutmut run` to find covered lines that no assertion actually checks.
|
|
469
|
+
|
|
470
|
+
</details>
|
|
471
|
+
|
|
472
|
+

|
|
473
|
+
|
|
474
|
+
## License
|
|
475
|
+
|
|
476
|
+
[MIT LICENSE](LICENSE)
|
|
477
|
+
|
|
478
|
+
Want to help? [CONTRIBUTING.md](CONTRIBUTING.md).
|