loopgate 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.
- harness/.githooks/_resolve +47 -0
- harness/.githooks/hoist +45 -0
- harness/.githooks/pre-commit +7 -0
- harness/.githooks/pre-push +10 -0
- harness/.githooks/prepare-commit-msg +7 -0
- harness/__init__.py +0 -0
- harness/cli.py +532 -0
- harness/config.py +203 -0
- harness/docs/PROJECT_STATUS.md +38 -0
- harness/docs/PROMPT.md +39 -0
- harness/docs/plan.md +68 -0
- harness/docs/specs/another_spec.md +56 -0
- harness/docs/specs/base.md +56 -0
- harness/gate.py +297 -0
- harness/js-scaffold/PROMPT.md +8 -0
- harness/js-scaffold/README.md +12 -0
- harness/js-scaffold/index.html +47 -0
- harness/js-scaffold/package-lock.json +176 -0
- harness/js-scaffold/package.json +16 -0
- harness/js-scaffold/quiz.js +21 -0
- harness/js-scaffold/specs/quiz.md +6 -0
- harness/js-scaffold/test.js +8 -0
- harness/ralph.ps1 +84 -0
- harness/ralph.sh +34 -0
- harness/temp.pyproject.toml +407 -0
- harness/tests/mutation/mutmut-cicd-stats.json +11 -0
- harness/tests/mutation/test_check_mutmut.py +135 -0
- harness/tests/preferences/test_preferences.py +498 -0
- harness/tests/preferences/test_preferences_properties.py +245 -0
- loopgate-0.1.0.dist-info/METADATA +478 -0
- loopgate-0.1.0.dist-info/RECORD +37 -0
- loopgate-0.1.0.dist-info/WHEEL +4 -0
- loopgate-0.1.0.dist-info/entry_points.txt +2 -0
- loopgate-0.1.0.dist-info/licenses/LICENSE +21 -0
- mutation/check_mutmut.py +163 -0
- preferences/__init__.py +0 -0
- preferences/preferences.py +306 -0
harness/gate.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""1) Preflight pre-commit checks basic quality plus agent containment. `def run_preflight`
|
|
2
|
+
|
|
3
|
+
2) Full gate on staged files.
|
|
4
|
+
`def run_gate` mirrors what will run on Github (CI runs this same `harness gate`).
|
|
5
|
+
|
|
6
|
+
All containment lists and check commands come from [tool.harness] in pyproject.toml, read once at
|
|
7
|
+
import into the constants below. A check is a (name, argv) pair; its `preflight`/`blocking` flags sort
|
|
8
|
+
it into the maps and sets this module runs on.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from functools import cache
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import tomlkit as tomllib
|
|
20
|
+
import typer
|
|
21
|
+
from rich.console import Console
|
|
22
|
+
|
|
23
|
+
from mutation.check_mutmut import MINIMUM_MUTATION_SCORE, analyze_mutmut_report_passed
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
from preferences.preferences import preferences_violations as prefs
|
|
27
|
+
except ImportError: # humans can delete preferences.py
|
|
28
|
+
prefs = None
|
|
29
|
+
|
|
30
|
+
console = Console(force_terminal=True, color_system=None if os.environ.get("RALPH_LOOP") else "auto")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Gate:
|
|
34
|
+
"""Contains Gate configuration values and methods."""
|
|
35
|
+
|
|
36
|
+
EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" # universal empty tree hash
|
|
37
|
+
|
|
38
|
+
def __init__(self, root: Path) -> None:
|
|
39
|
+
self.repo_root = root
|
|
40
|
+
pyproject = self.repo_root / "pyproject.toml"
|
|
41
|
+
toml = tomllib.loads(pyproject.read_text(encoding="utf-8")).unwrap() if pyproject.is_file() else {}
|
|
42
|
+
harness = toml.get("tool", {}).get("harness")
|
|
43
|
+
if not harness:
|
|
44
|
+
defaults = tomllib.loads(Path(__file__).with_name("temp.pyproject.toml").read_text(encoding="utf-8"))
|
|
45
|
+
harness = defaults["tool"]["harness"]
|
|
46
|
+
self.settings = harness.get("settings", {"behavior": "fail", "error_diff_lines": 500, "languages": ["py"]})
|
|
47
|
+
self.forbidden: dict[str, list[str]] = harness.get("FORBIDDEN", {})
|
|
48
|
+
self.agents: dict[str, list[str]] = harness.get("agents", {})
|
|
49
|
+
self.commit_checks: dict[str, list[str]] = harness.get("preflight", {})
|
|
50
|
+
self.gate_checks: dict[str, list[str]] = harness.get("gate", {}) | self.commit_checks
|
|
51
|
+
self.forbidden_files: tuple[str, ...] = tuple(self.forbidden.get("FILES", []))
|
|
52
|
+
self.forbidden_dirs: tuple[str, ...] = tuple(self.forbidden.get("DIRS", []))
|
|
53
|
+
self.forbidden_patterns: tuple[str, ...] = tuple(self.forbidden.get("PATTERNS", []))
|
|
54
|
+
|
|
55
|
+
def run_checks(self, checks: dict[str, list[str]]) -> dict[str, list[str]]:
|
|
56
|
+
"""Run each named command, streaming its output live under a phase header.
|
|
57
|
+
Reports what each command did and leaves the verdict to the caller.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
checks: Mapping of check name to the argv that runs it.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
{ "pass": [...], "warn": [...], "fail": [ problems ] } bucketing each check name by exit code.
|
|
64
|
+
If anything is in "fail", a commit is not allowed.
|
|
65
|
+
"""
|
|
66
|
+
clean_env = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")}
|
|
67
|
+
if not os.environ.get("RALPH_LOOP"):
|
|
68
|
+
clean_env.update({"FORCE_COLOR": "1", "CLICOLOR_FORCE": "1", "SEMGREP_FORCE_COLOR": "1"})
|
|
69
|
+
results: dict[str, list[str]] = {"pass": [], "fail": [], "warn": []}
|
|
70
|
+
for name, command in checks.items():
|
|
71
|
+
colorize(name, " ".join(command))
|
|
72
|
+
sys.stdout.flush()
|
|
73
|
+
with subprocess.Popen(command, cwd=self.repo_root, env=clean_env) as process:
|
|
74
|
+
exit_code = process.wait()
|
|
75
|
+
if exit_code == 0:
|
|
76
|
+
results["pass"].append(name)
|
|
77
|
+
elif "format" in name:
|
|
78
|
+
results["warn"].append(name)
|
|
79
|
+
else:
|
|
80
|
+
results[self.settings["behavior"]].append(name)
|
|
81
|
+
key = "fail" if os.environ.get("RALPH_LOOP") else "warn"
|
|
82
|
+
colorize("AGENT CHECKs", "running non-human agent checks")
|
|
83
|
+
self._run_non_human_checks(results, key)
|
|
84
|
+
mutmut_key = "pass" if analyze_mutmut_report_passed() >= MINIMUM_MUTATION_SCORE else key
|
|
85
|
+
results[mutmut_key].append("mutmut")
|
|
86
|
+
return results
|
|
87
|
+
|
|
88
|
+
def _run_non_human_checks(self, results: dict[str, list[str]], key: str):
|
|
89
|
+
"""Runs checks on non-humans only. Checks things that linters or other chekcs to do not check.
|
|
90
|
+
Unstages files that should never be touched.
|
|
91
|
+
|
|
92
|
+
Arguments:
|
|
93
|
+
results: The original bucketing of each check name into "pass"/"fail"/"warn" lists
|
|
94
|
+
key: `results` dictionary will use "fail"/"warn" if an agent is being checked
|
|
95
|
+
"""
|
|
96
|
+
ref = "HEAD" if run_git(["rev-parse", "--verify", "HEAD"], check=False).strip() else self.EMPTY_TREE
|
|
97
|
+
staged = run_git([
|
|
98
|
+
"diff",
|
|
99
|
+
"--cached",
|
|
100
|
+
"--name-only",
|
|
101
|
+
"--no-renames",
|
|
102
|
+
"--diff-filter=ACMRD",
|
|
103
|
+
]).splitlines()
|
|
104
|
+
if not staged:
|
|
105
|
+
colorize("EMPTY COMMIT", "nothing staged: do real work, do not commit empty")
|
|
106
|
+
return
|
|
107
|
+
forbidden_paths: list[str] = [
|
|
108
|
+
path
|
|
109
|
+
for path in staged
|
|
110
|
+
if path.casefold() in self.forbidden_files or path.casefold().startswith(self.forbidden_dirs)
|
|
111
|
+
]
|
|
112
|
+
if forbidden_paths:
|
|
113
|
+
out = "\n".join(forbidden_paths)
|
|
114
|
+
colorize("EJECTED", f"Would keep forbidden paths out of agent commit:[dim green]\n{out}[/]")
|
|
115
|
+
if key == "fail":
|
|
116
|
+
run_git(["reset", "-q", ref, "--", *forbidden_paths])
|
|
117
|
+
if forbidden_patterns := self._check_for_bad_patterns():
|
|
118
|
+
results[key].append("FORBIDDEN FOR AGENT:\n" + "\n".join(forbidden_patterns))
|
|
119
|
+
if ignored_preferences := self._check_for_preferences():
|
|
120
|
+
results[key].append(f"PREFERENCES IGNORED:\n{ignored_preferences}")
|
|
121
|
+
self._check_diff_size(ref, results, key)
|
|
122
|
+
|
|
123
|
+
def _check_for_bad_patterns(self) -> list[str]:
|
|
124
|
+
"""Check staged files for banned patterns (agent-in-loop containment).
|
|
125
|
+
Does not unstage anything. Later, if any problem lands in { "fail": ... } the commit is blocked.
|
|
126
|
+
|
|
127
|
+
Banned patterns are flagged only on ADDED diff lines (a '+' line, never a '+++' header).
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
The banned-pattern hits plus any preference violations found in the staged files.
|
|
131
|
+
"""
|
|
132
|
+
staged_lines = run_git([
|
|
133
|
+
"diff",
|
|
134
|
+
"--cached",
|
|
135
|
+
"--unified=0",
|
|
136
|
+
"--output-indicator-new=a",
|
|
137
|
+
":(exclude,icase,glob)**/*.toml",
|
|
138
|
+
":(exclude,icase,glob)**/*.md",
|
|
139
|
+
])
|
|
140
|
+
problems: set[str] = set()
|
|
141
|
+
current_file = ""
|
|
142
|
+
for line in staged_lines.splitlines():
|
|
143
|
+
if line.startswith("+++ b/"):
|
|
144
|
+
current_file = line.removeprefix("+++ b/")
|
|
145
|
+
elif line.startswith("a"):
|
|
146
|
+
problems.update(
|
|
147
|
+
f"{current_file}: '{pattern}'"
|
|
148
|
+
for pattern in self.forbidden_patterns
|
|
149
|
+
if pattern.casefold() in line.casefold()
|
|
150
|
+
)
|
|
151
|
+
colorize("BANNED PATTERNS FOR AGENT", f"check for banned patterns in staged files\nIssues:\n{problems}")
|
|
152
|
+
return list(problems)
|
|
153
|
+
|
|
154
|
+
def _check_diff_size(self, ref: str, results: dict[str, list[str]], key: str):
|
|
155
|
+
"""Report size of staged diff and block a bloated commit if past Lines Of Code (LOC) cap.
|
|
156
|
+
|
|
157
|
+
LOC = added + deleted. Count diff lines, staged and unstaged. An edit is
|
|
158
|
+
one deletion plus one addition. Lockfiles and binaries are excluded.
|
|
159
|
+
|
|
160
|
+
Arguments:
|
|
161
|
+
ref: Git SHA to run git diff on
|
|
162
|
+
results: The full-checks result bucketing each check name into "pass"/"fail"/"warn" lists.
|
|
163
|
+
key: Whether to "fail" this check because an agent is being checked (not a human)
|
|
164
|
+
"""
|
|
165
|
+
stats = run_git(["diff", ref, "--numstat", "--cached", "--find-renames"]).splitlines()
|
|
166
|
+
total = 0
|
|
167
|
+
for line in stats:
|
|
168
|
+
inserted, deleted, path = line.split("\t", 2)
|
|
169
|
+
if not (inserted == "-" or path.lower().endswith(".lock")): # binary or lockfile
|
|
170
|
+
total += int(inserted) + int(deleted)
|
|
171
|
+
warn_at_75: int = round(self.settings["error_diff_lines"] * 0.75)
|
|
172
|
+
msg = (
|
|
173
|
+
f"{total} lines of code modified (insertions + deletions in staged files). Agents get WARN at "
|
|
174
|
+
f"75% {warn_at_75}, ERROR at {self.settings['error_diff_lines']}."
|
|
175
|
+
)
|
|
176
|
+
do_better = (
|
|
177
|
+
"\nRefactor bloat, reduce mis-direction, re-use fixtures, cut duplication, slim down "
|
|
178
|
+
"code. More code does not mean good code."
|
|
179
|
+
)
|
|
180
|
+
if total > self.settings["error_diff_lines"]:
|
|
181
|
+
results[key].append(msg + do_better)
|
|
182
|
+
elif total > warn_at_75:
|
|
183
|
+
results["warn"].append(msg + do_better)
|
|
184
|
+
colorize("DIFF SIZE", msg)
|
|
185
|
+
|
|
186
|
+
def _check_for_preferences(self) -> str:
|
|
187
|
+
"""Checks user preferences honored. Currently only a preferences.py file exists. New languages should
|
|
188
|
+
add their own branch.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
The preferences violations and filepath found in staged files.
|
|
192
|
+
"""
|
|
193
|
+
problems: list[str] = []
|
|
194
|
+
if "py" in self.settings["languages"]:
|
|
195
|
+
staged = run_git([
|
|
196
|
+
"diff",
|
|
197
|
+
"--cached",
|
|
198
|
+
"--name-only",
|
|
199
|
+
"--diff-filter=d",
|
|
200
|
+
"--",
|
|
201
|
+
"*.py",
|
|
202
|
+
]).splitlines()
|
|
203
|
+
if prefs:
|
|
204
|
+
for path in staged:
|
|
205
|
+
messages: str = prefs(path, run_git(["show", f":{path}"]))
|
|
206
|
+
if messages:
|
|
207
|
+
problems.append(messages)
|
|
208
|
+
colorize("REPO PREFERENCES", f"checking repo preferences are respected by agents\nIssues:\n{problems}")
|
|
209
|
+
return "\n".join(problems)
|
|
210
|
+
|
|
211
|
+
def run_preflight(self) -> dict[str, list[str]]:
|
|
212
|
+
"""Pre-commit: checks plus an informational format report. For agents in the loop also
|
|
213
|
+
unstages forbidden filepaths and flags banned patterns and any human-preferences not honored.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
The commit-checks result with any containment problems in a "fail" list.
|
|
217
|
+
"""
|
|
218
|
+
return self.run_checks(self.commit_checks)
|
|
219
|
+
|
|
220
|
+
def run_gate(self) -> dict[str, list[str]]:
|
|
221
|
+
"""Pre-push / CI: lint, types, pylint, security, pytest/hypothesis (blocking), complexipy, plus an
|
|
222
|
+
informational format report.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
results: The full-checks result bucketing each check name into "pass"/"fail"/"warn" lists.
|
|
226
|
+
"""
|
|
227
|
+
return self.run_checks(self.gate_checks)
|
|
228
|
+
|
|
229
|
+
def prepare_commit_msg(self, argv: list[str]) -> int:
|
|
230
|
+
"""Logic for the git prepare-commit-msg hook applicable to agents in the loop.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
argv: arguments used to invoke `git commit`
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Status code integer 0 or 1 (git blocks commit on code 1)
|
|
237
|
+
"""
|
|
238
|
+
if not os.environ.get("RALPH_LOOP"):
|
|
239
|
+
return 0
|
|
240
|
+
commit_msg_file: str = argv[1] if len(argv) > 1 else ""
|
|
241
|
+
command = argv[2] if len(argv) > 2 else ""
|
|
242
|
+
msg = ""
|
|
243
|
+
if command in {"merge", "squash", "rebase", "reset", "clean", "filter-branch"}:
|
|
244
|
+
msg = f"You cannot use that git command `{command}`.\n"
|
|
245
|
+
ref = "HEAD" if run_git(["rev-parse", "--verify", "HEAD"], check=False).strip() else self.EMPTY_TREE
|
|
246
|
+
if not run_git(["diff-index", "--cached", "--name-only", ref]):
|
|
247
|
+
msg += "Empty commit detected. Stage real work, Don't use --allow-empty. Say if you're blocked\n"
|
|
248
|
+
if Path(commit_msg_file).exists():
|
|
249
|
+
content = Path(commit_msg_file).read_text(encoding="utf-8")
|
|
250
|
+
actual_text = "\n".join([line for line in content.splitlines() if not line.startswith("#")]).strip()
|
|
251
|
+
if not actual_text:
|
|
252
|
+
msg += "Commit message is blank. Provide an informative message with your agent ID.\n"
|
|
253
|
+
if msg:
|
|
254
|
+
colorize("prepare-commit-message", msg)
|
|
255
|
+
return 1 # Intercepts git
|
|
256
|
+
return 0
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def run_git(args: list[str], repo: Path | None = None, check: bool = True) -> str:
|
|
260
|
+
"""Run a git command in the repo and return its stdout.
|
|
261
|
+
|
|
262
|
+
Arguments:
|
|
263
|
+
args: Git subcommand and its arguments
|
|
264
|
+
repo: the repository directory to run the git command from. Defaults to REPO_ROOT.
|
|
265
|
+
check: If check is True and the exit code was non-zero, it raises a CalledProcessError.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
The command's raw stdout string (callers will .splitlines() as needed)
|
|
269
|
+
"""
|
|
270
|
+
target = gates().repo_root if repo is None else repo
|
|
271
|
+
command = ["git", "-C", str(target), *args]
|
|
272
|
+
git_env = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")}
|
|
273
|
+
result = subprocess.run(command, capture_output=True, text=True, check=check, env=git_env)
|
|
274
|
+
return result.stdout
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def colorize(name: str, command: str) -> None:
|
|
278
|
+
"""Rich consosle printing to signpost checks.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
name: Phase name shown in the rule header.
|
|
282
|
+
command: The command string printed beneath the header.
|
|
283
|
+
"""
|
|
284
|
+
if os.environ.get("RALPH_LOOP"): # loop agents get plain text (no ANSI)
|
|
285
|
+
typer.echo(f"PHASE: {name.upper()}\n{command}")
|
|
286
|
+
else:
|
|
287
|
+
console.rule(f"[bold cyan] PHASE: {name.upper()}[/]", style="blink cyan on grey15")
|
|
288
|
+
console.print(f"[dim italic]{command}[/dim italic]\n", justify="center")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@cache
|
|
292
|
+
def gates() -> Gate:
|
|
293
|
+
"""Returns the singleton object containing gate checks and configs."""
|
|
294
|
+
root = run_git(["rev-parse", "--show-toplevel"], repo=Path.cwd(), check=False).strip()
|
|
295
|
+
if not root:
|
|
296
|
+
typer.echo("Run this inside a git repository")
|
|
297
|
+
return Gate(Path(root).resolve())
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# JS Quiz Scaffold
|
|
2
|
+
|
|
3
|
+
A minimal vanilla JS quiz that checks answers and colors buttons.
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
- Do not change the correct answer without updating specs/quiz.md
|
|
7
|
+
- Tests must pass after every change: `npm test`
|
|
8
|
+
- Do not add frameworks or large dependencies
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# JS Scaffold Example
|
|
2
|
+
|
|
3
|
+
Shows how Ralph works with a frontend JS project.
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
- `PROMPT.md` — rules Ralph must follow
|
|
7
|
+
- `specs/` — behavior specs Ralph must not break
|
|
8
|
+
- `npm test` — fails if behavior is broken
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
npm install
|
|
12
|
+
npm test
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Quiz</title>
|
|
8
|
+
<script src="./quiz.js" type="module" defer></script>
|
|
9
|
+
<style>
|
|
10
|
+
body {
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
align-items: center;
|
|
15
|
+
margin: 0;
|
|
16
|
+
}
|
|
17
|
+
h1{
|
|
18
|
+
font-size: 3.4rem;
|
|
19
|
+
}
|
|
20
|
+
p{
|
|
21
|
+
font-size: 1.5rem;
|
|
22
|
+
text-align: center
|
|
23
|
+
}
|
|
24
|
+
button{
|
|
25
|
+
padding-inline: 1.5rem;
|
|
26
|
+
width: 100%;
|
|
27
|
+
font-size: 1rem;
|
|
28
|
+
padding-block: 0.5rem;
|
|
29
|
+
display: block;
|
|
30
|
+
margin-bottom: 1rem;
|
|
31
|
+
}
|
|
32
|
+
</style>
|
|
33
|
+
</head>
|
|
34
|
+
<body>
|
|
35
|
+
<main>
|
|
36
|
+
<header>
|
|
37
|
+
<h1> Quiz Test </h1>
|
|
38
|
+
</header>
|
|
39
|
+
<section>
|
|
40
|
+
<p>NaN === NaN</p>
|
|
41
|
+
<button id="btn-true" type="button">True</button>
|
|
42
|
+
<button id="btn-false" type="button">False</button>
|
|
43
|
+
</section>
|
|
44
|
+
</main>
|
|
45
|
+
</body>
|
|
46
|
+
|
|
47
|
+
</html>
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "js-scaffold",
|
|
3
|
+
"lockfileVersion": 3,
|
|
4
|
+
"requires": true,
|
|
5
|
+
"packages": {
|
|
6
|
+
"": {
|
|
7
|
+
"name": "js-scaffold",
|
|
8
|
+
"devDependencies": {
|
|
9
|
+
"@biomejs/biome": "latest"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"node_modules/@biomejs/biome": {
|
|
13
|
+
"version": "2.5.4",
|
|
14
|
+
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.4.tgz",
|
|
15
|
+
"integrity": "sha512-xy5FNE5kQJKyK5MR1gJy6ztXYx4WBAbYGlK04lMEgmyPRWKybY9NFwiG9yo0XdzOU8Xvhj41u034J1ywfoWfMw==",
|
|
16
|
+
"dev": true,
|
|
17
|
+
"license": "MIT OR Apache-2.0",
|
|
18
|
+
"bin": {
|
|
19
|
+
"biome": "bin/biome"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=14.21.3"
|
|
23
|
+
},
|
|
24
|
+
"funding": {
|
|
25
|
+
"type": "opencollective",
|
|
26
|
+
"url": "https://opencollective.com/biome"
|
|
27
|
+
},
|
|
28
|
+
"optionalDependencies": {
|
|
29
|
+
"@biomejs/cli-darwin-arm64": "2.5.4",
|
|
30
|
+
"@biomejs/cli-darwin-x64": "2.5.4",
|
|
31
|
+
"@biomejs/cli-linux-arm64": "2.5.4",
|
|
32
|
+
"@biomejs/cli-linux-arm64-musl": "2.5.4",
|
|
33
|
+
"@biomejs/cli-linux-x64": "2.5.4",
|
|
34
|
+
"@biomejs/cli-linux-x64-musl": "2.5.4",
|
|
35
|
+
"@biomejs/cli-win32-arm64": "2.5.4",
|
|
36
|
+
"@biomejs/cli-win32-x64": "2.5.4"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"node_modules/@biomejs/cli-darwin-arm64": {
|
|
40
|
+
"version": "2.5.4",
|
|
41
|
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.4.tgz",
|
|
42
|
+
"integrity": "sha512-4o3NFRobXHynkgcFVrlZsoDAFtF2ldlEGN8sORSws5ZQqyY4PXnPUIylu4ksfyHuwkfvDREuWh3JK+niRwGq3w==",
|
|
43
|
+
"cpu": [
|
|
44
|
+
"arm64"
|
|
45
|
+
],
|
|
46
|
+
"dev": true,
|
|
47
|
+
"license": "MIT OR Apache-2.0",
|
|
48
|
+
"optional": true,
|
|
49
|
+
"os": [
|
|
50
|
+
"darwin"
|
|
51
|
+
],
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=14.21.3"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"node_modules/@biomejs/cli-darwin-x64": {
|
|
57
|
+
"version": "2.5.4",
|
|
58
|
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.4.tgz",
|
|
59
|
+
"integrity": "sha512-D32P5HkU2Y6PySuC/WsVDTOgsDwVFmujzhhhOQjajtATpVWFDXuVd3oRbsWNSEA+aaFzyzZm22szsyydBYlSyQ==",
|
|
60
|
+
"cpu": [
|
|
61
|
+
"x64"
|
|
62
|
+
],
|
|
63
|
+
"dev": true,
|
|
64
|
+
"license": "MIT OR Apache-2.0",
|
|
65
|
+
"optional": true,
|
|
66
|
+
"os": [
|
|
67
|
+
"darwin"
|
|
68
|
+
],
|
|
69
|
+
"engines": {
|
|
70
|
+
"node": ">=14.21.3"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"node_modules/@biomejs/cli-linux-arm64": {
|
|
74
|
+
"version": "2.5.4",
|
|
75
|
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.4.tgz",
|
|
76
|
+
"integrity": "sha512-pSEfW7B8kTsXUjUxC1xVVK+y85Ht3C5XxZ9gclmC7/3Ku9Vqz8jmI7k0p/BNIjQ6t4sFERI2sFeH73ybiZl6YQ==",
|
|
77
|
+
"cpu": [
|
|
78
|
+
"arm64"
|
|
79
|
+
],
|
|
80
|
+
"dev": true,
|
|
81
|
+
"license": "MIT OR Apache-2.0",
|
|
82
|
+
"optional": true,
|
|
83
|
+
"os": [
|
|
84
|
+
"linux"
|
|
85
|
+
],
|
|
86
|
+
"engines": {
|
|
87
|
+
"node": ">=14.21.3"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
|
91
|
+
"version": "2.5.4",
|
|
92
|
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.4.tgz",
|
|
93
|
+
"integrity": "sha512-Rpm5/AT1m+DlJmUoYvS4/vXc+0tXJPJ2NQz25TGPyHVF5JrWy75PE0GH6kVxsKtQDuCH4OgzquZq0R4kj/wCVg==",
|
|
94
|
+
"cpu": [
|
|
95
|
+
"arm64"
|
|
96
|
+
],
|
|
97
|
+
"dev": true,
|
|
98
|
+
"license": "MIT OR Apache-2.0",
|
|
99
|
+
"optional": true,
|
|
100
|
+
"os": [
|
|
101
|
+
"linux"
|
|
102
|
+
],
|
|
103
|
+
"engines": {
|
|
104
|
+
"node": ">=14.21.3"
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"node_modules/@biomejs/cli-linux-x64": {
|
|
108
|
+
"version": "2.5.4",
|
|
109
|
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.4.tgz",
|
|
110
|
+
"integrity": "sha512-FNxojWJkL7EajAuzBgoLe0T2G0y112M4lBrDIFl/DomFTx8yqenYOIdsRLNXvOvBBofE8hJi85LjzLmBDpY7/Q==",
|
|
111
|
+
"cpu": [
|
|
112
|
+
"x64"
|
|
113
|
+
],
|
|
114
|
+
"dev": true,
|
|
115
|
+
"license": "MIT OR Apache-2.0",
|
|
116
|
+
"optional": true,
|
|
117
|
+
"os": [
|
|
118
|
+
"linux"
|
|
119
|
+
],
|
|
120
|
+
"engines": {
|
|
121
|
+
"node": ">=14.21.3"
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"node_modules/@biomejs/cli-linux-x64-musl": {
|
|
125
|
+
"version": "2.5.4",
|
|
126
|
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.4.tgz",
|
|
127
|
+
"integrity": "sha512-aby/PohmmgbShcHqFsZVzG8H6D98+P+A6xRWRrQcLW1pCjabcov5UUlke4UqNQBYTkDQav+jB4zyyDDeKB2GaA==",
|
|
128
|
+
"cpu": [
|
|
129
|
+
"x64"
|
|
130
|
+
],
|
|
131
|
+
"dev": true,
|
|
132
|
+
"license": "MIT OR Apache-2.0",
|
|
133
|
+
"optional": true,
|
|
134
|
+
"os": [
|
|
135
|
+
"linux"
|
|
136
|
+
],
|
|
137
|
+
"engines": {
|
|
138
|
+
"node": ">=14.21.3"
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"node_modules/@biomejs/cli-win32-arm64": {
|
|
142
|
+
"version": "2.5.4",
|
|
143
|
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.4.tgz",
|
|
144
|
+
"integrity": "sha512-emoXexPZIPAZkz2RKmA95WJUqK3I5MJNYtwEbL5ESciRzhmFMMyekDhNG8hpeOaK+ZGRDxAU4wvGuA5IHQ0h0w==",
|
|
145
|
+
"cpu": [
|
|
146
|
+
"arm64"
|
|
147
|
+
],
|
|
148
|
+
"dev": true,
|
|
149
|
+
"license": "MIT OR Apache-2.0",
|
|
150
|
+
"optional": true,
|
|
151
|
+
"os": [
|
|
152
|
+
"win32"
|
|
153
|
+
],
|
|
154
|
+
"engines": {
|
|
155
|
+
"node": ">=14.21.3"
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
"node_modules/@biomejs/cli-win32-x64": {
|
|
159
|
+
"version": "2.5.4",
|
|
160
|
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.4.tgz",
|
|
161
|
+
"integrity": "sha512-U1jaluLw1qQc2Tx7/CeSoL9N5XcqIH+GWjpUAy1ouB5nVjSCMNO+NNHdY3RAs8zxNurLWAdj6pehQdCA2zyU+Q==",
|
|
162
|
+
"cpu": [
|
|
163
|
+
"x64"
|
|
164
|
+
],
|
|
165
|
+
"dev": true,
|
|
166
|
+
"license": "MIT OR Apache-2.0",
|
|
167
|
+
"optional": true,
|
|
168
|
+
"os": [
|
|
169
|
+
"win32"
|
|
170
|
+
],
|
|
171
|
+
"engines": {
|
|
172
|
+
"node": ">=14.21.3"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "js-scaffold",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"test": "node --test",
|
|
6
|
+
"lint": "npx @biomejs/biome check .",
|
|
7
|
+
"format": "npx @biomejs/biome check --write .",
|
|
8
|
+
"security": "semgrep scan --error --config auto --config p/secrets --exclude-rule yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag ../..",
|
|
9
|
+
"coverage": "node --test --experimental-test-coverage",
|
|
10
|
+
"preflight": "npm run lint && npm run format",
|
|
11
|
+
"gate": "npm run coverage && npm run security"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@biomejs/biome": "latest"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function checkAnswer(answer) {
|
|
2
|
+
const correctAnswer = false;
|
|
3
|
+
return answer === correctAnswer;
|
|
4
|
+
}
|
|
5
|
+
function testQuiz() {
|
|
6
|
+
const trueBtn = document.getElementById("btn-true");
|
|
7
|
+
const falseBtn = document.getElementById("btn-false");
|
|
8
|
+
trueBtn.addEventListener("click", () => {
|
|
9
|
+
const isCorrect = checkAnswer(true);
|
|
10
|
+
trueBtn.style.background = isCorrect ? "#d4edda" : "#f8d7da";
|
|
11
|
+
falseBtn.style.background = "";
|
|
12
|
+
});
|
|
13
|
+
falseBtn.addEventListener("click", () => {
|
|
14
|
+
const isCorrect = checkAnswer(false);
|
|
15
|
+
falseBtn.style.background = isCorrect ? "#d4edda" : "#f8d7da";
|
|
16
|
+
trueBtn.style.background = "";
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
if (typeof document !== "undefined") {
|
|
20
|
+
testQuiz();
|
|
21
|
+
}
|
harness/ralph.ps1
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Ralph (Windows twin of ralph.sh). Hand docs/PROMPT.md to a fresh-context agent and loop.
|
|
2
|
+
# Keep Ralph Dumb: start the worker, give it the prompt, print a line, repeat. Nothing else.
|
|
3
|
+
# Windows has no POSIX `timeout`, so this uses Process.WaitForExit + taskkill /T.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# powershell.exe -File ralph.ps1 <max_iterations> <max_minutes_per_iteration> <agent command...>
|
|
7
|
+
|
|
8
|
+
$ErrorActionPreference = "Stop"
|
|
9
|
+
[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
10
|
+
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
11
|
+
|
|
12
|
+
# Mark loop commits so the gate (run by the git hooks) applies containment to the worker.
|
|
13
|
+
$env:RALPH_LOOP = "1"
|
|
14
|
+
|
|
15
|
+
function ConvertTo-WindowsArgument([string]$argument) {
|
|
16
|
+
$escaped = [regex]::Replace($argument, '(\\*)"', '$1$1\"')
|
|
17
|
+
return '"' + $escaped + [regex]::Match($argument, '(\\*)$').Groups[1].Value + '"'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function Write-RalphEvent([System.Collections.IDictionary]$payload) {
|
|
21
|
+
[Console]::Out.WriteLine(($payload | ConvertTo-Json -Compress))
|
|
22
|
+
[Console]::Out.Flush()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if ($args.Count -lt 3) {
|
|
26
|
+
[Console]::Error.WriteLine(
|
|
27
|
+
"Usage: ralph.ps1 <max_iterations> <max_minutes_per_iteration> <agent command...>"
|
|
28
|
+
)
|
|
29
|
+
exit 2
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
$maxIterations = [int]$args[0]
|
|
33
|
+
$maxMinutes = [double]$args[1]
|
|
34
|
+
$worker = @($args | Select-Object -Skip 2)
|
|
35
|
+
$timeoutMilliseconds = [int][Math]::Ceiling($maxMinutes * 60 * 1000)
|
|
36
|
+
|
|
37
|
+
$iteration = 1
|
|
38
|
+
while ($iteration -le $maxIterations) {
|
|
39
|
+
Write-RalphEvent ([ordered]@{
|
|
40
|
+
type = "ralph"
|
|
41
|
+
iteration = $iteration
|
|
42
|
+
max_iterations = $maxIterations
|
|
43
|
+
max_minutes = $maxMinutes
|
|
44
|
+
timestamp = (Get-Date).ToString("yyyy-MM-ddTHH:mm")
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
$startInfo = [System.Diagnostics.ProcessStartInfo]::new()
|
|
48
|
+
$startInfo.FileName = $worker[0]
|
|
49
|
+
if ($worker.Count -gt 1) {
|
|
50
|
+
$startInfo.Arguments = (($worker[1..($worker.Count - 1)] | ForEach-Object {
|
|
51
|
+
ConvertTo-WindowsArgument $_
|
|
52
|
+
}) -join ' ')
|
|
53
|
+
}
|
|
54
|
+
$startInfo.RedirectStandardInput = $true
|
|
55
|
+
$startInfo.UseShellExecute = $false
|
|
56
|
+
|
|
57
|
+
$process = [System.Diagnostics.Process]::Start($startInfo)
|
|
58
|
+
$prompt = "$($env:RALPH_PROMPT)`n`nRALPH_ITERATION=$iteration/$maxIterations`n"
|
|
59
|
+
$process.StandardInput.Write($prompt)
|
|
60
|
+
$process.StandardInput.Close()
|
|
61
|
+
|
|
62
|
+
if (-not $process.WaitForExit($timeoutMilliseconds)) {
|
|
63
|
+
taskkill.exe /F /T /PID $process.Id | Out-Null
|
|
64
|
+
$process.WaitForExit()
|
|
65
|
+
$process.Dispose()
|
|
66
|
+
exit 124
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
$process.WaitForExit()
|
|
70
|
+
$exitCode = $process.ExitCode
|
|
71
|
+
$process.Dispose()
|
|
72
|
+
if ($exitCode -ne 0) {
|
|
73
|
+
exit $exitCode
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
$iteration += 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Write-RalphEvent ([ordered]@{
|
|
80
|
+
type = "ralph"
|
|
81
|
+
completed = $iteration - 1
|
|
82
|
+
max_minutes = $maxMinutes
|
|
83
|
+
timestamp = (Get-Date).ToString("yyyy-MM-ddTHH:mm")
|
|
84
|
+
})
|