quality-loop 0.2.0__tar.gz → 0.2.1__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.
- {quality_loop-0.2.0/quality_loop.egg-info → quality_loop-0.2.1}/PKG-INFO +7 -4
- {quality_loop-0.2.0 → quality_loop-0.2.1}/README.md +6 -3
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/__init__.py +1 -1
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/drive/terminal.py +15 -2
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/report.py +10 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1/quality_loop.egg-info}/PKG-INFO +7 -4
- {quality_loop-0.2.0 → quality_loop-0.2.1}/tests/test_drive.py +31 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/tests/test_loop.py +5 -3
- {quality_loop-0.2.0 → quality_loop-0.2.1}/tests/test_operator.py +2 -2
- {quality_loop-0.2.0 → quality_loop-0.2.1}/tests/test_scenario_report.py +18 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/LICENSE +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/pyproject.toml +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/OPERATOR.md +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/__main__.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/adapter.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/audit.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/cli.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/compare.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/config.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/drive/__init__.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/drive/_mac.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/drive/_windows.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/drive/keys.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/install.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/mcp.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/operator.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/pages.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/processes.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/progress.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/room.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/runner.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/scenario.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/screens/__init__.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/screens/_mac.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/screens/_windows.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/session.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/skill/SKILL.md +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/skill/references/filing.md +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/skill/references/setup.md +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/standin.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop/transcript.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop.egg-info/SOURCES.txt +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop.egg-info/dependency_links.txt +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop.egg-info/entry_points.txt +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop.egg-info/requires.txt +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/quality_loop.egg-info/top_level.txt +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/setup.cfg +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/tests/test_core.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/tests/test_mcp.py +0 -0
- {quality_loop-0.2.0 → quality_loop-0.2.1}/tests/test_screens.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quality-loop
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Measure what a product teaches - its skill, tools or manual - with isolated agent runs of real tasks, reported by an operator agent
|
|
5
5
|
Author: craftware
|
|
6
6
|
License-Expression: MIT
|
|
@@ -121,9 +121,12 @@ under a policy the person sets. See [Running the loop](https://github.com/crftwr
|
|
|
121
121
|
|
|
122
122
|
## Status
|
|
123
123
|
|
|
124
|
-
0.x, early. It has been run end to end on macOS against the two examples
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
0.x, early. It has been run end to end on macOS against the two examples,
|
|
125
|
+
Keyhac and XeFM (including XeFM's terminal UI in a live check), and on
|
|
126
|
+
Windows (11, ARM64) against the toy, Keyhac and XeFM, with real page and
|
|
127
|
+
terminal screens. Not yet run anywhere for real: synthetic keys and
|
|
128
|
+
screenshots of a desktop app (`drive.DesktopApp`); a terminal app is not
|
|
129
|
+
driven on Windows yet. The design discussion is
|
|
127
130
|
[crftwr/keyhac#205](https://github.com/crftwr/keyhac/issues/205) and this
|
|
128
131
|
repository's issues.
|
|
129
132
|
|
|
@@ -90,9 +90,12 @@ under a policy the person sets. See [Running the loop](https://github.com/crftwr
|
|
|
90
90
|
|
|
91
91
|
## Status
|
|
92
92
|
|
|
93
|
-
0.x, early. It has been run end to end on macOS against the two examples
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
0.x, early. It has been run end to end on macOS against the two examples,
|
|
94
|
+
Keyhac and XeFM (including XeFM's terminal UI in a live check), and on
|
|
95
|
+
Windows (11, ARM64) against the toy, Keyhac and XeFM, with real page and
|
|
96
|
+
terminal screens. Not yet run anywhere for real: synthetic keys and
|
|
97
|
+
screenshots of a desktop app (`drive.DesktopApp`); a terminal app is not
|
|
98
|
+
driven on Windows yet. The design discussion is
|
|
96
99
|
[crftwr/keyhac#205](https://github.com/crftwr/keyhac/issues/205) and this
|
|
97
100
|
repository's issues.
|
|
98
101
|
|
|
@@ -31,6 +31,14 @@ class TerminalError(Exception):
|
|
|
31
31
|
pass
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
#: Seconds after a lone Esc before the next key. A terminal app cannot tell
|
|
35
|
+
#: Esc followed quickly by a key from Alt+that key: it waits a moment (PuiKit
|
|
36
|
+
#: 0.1s, ncurses ESCDELAY) for the rest of a sequence. The first XeFM live
|
|
37
|
+
#: check sent Esc then Ctrl-T 20ms apart, and the command palette never
|
|
38
|
+
#: opened - it had arrived as Alt-Ctrl-T.
|
|
39
|
+
ESC_SETTLE = 0.3
|
|
40
|
+
|
|
41
|
+
|
|
34
42
|
def _pyte():
|
|
35
43
|
try:
|
|
36
44
|
import pyte
|
|
@@ -68,8 +76,11 @@ class PtyApp:
|
|
|
68
76
|
# The app's answers to queries (cursor position, device attributes)
|
|
69
77
|
# go back down the pty, or a curses app that asks waits forever.
|
|
70
78
|
self.screen.write_process_input = lambda data: self._write(data.encode("utf-8"))
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
# ESCDELAY: how long ncurses waits after an ESC to see whether a
|
|
80
|
+
# sequence follows - a second by default. Short, so a lone Esc is an
|
|
81
|
+
# Esc soon enough for ESC_SETTLE below to cover it.
|
|
82
|
+
environment = {"ESCDELAY": "25", **os.environ, **(env or {}),
|
|
83
|
+
"TERM": "xterm-256color", "LINES": str(rows), "COLUMNS": str(cols)}
|
|
73
84
|
self.process = subprocess.Popen(
|
|
74
85
|
argv, stdin=slave, stdout=slave, stderr=slave, cwd=cwd, env=environment,
|
|
75
86
|
start_new_session=True, close_fds=True)
|
|
@@ -108,6 +119,8 @@ class PtyApp:
|
|
|
108
119
|
self._write(item.text.encode("utf-8"))
|
|
109
120
|
else:
|
|
110
121
|
self._write(keynames.terminal_bytes(item))
|
|
122
|
+
if item.name == "Esc" and not item.mods:
|
|
123
|
+
time.sleep(ESC_SETTLE)
|
|
111
124
|
time.sleep(0.02)
|
|
112
125
|
self._sent = time.monotonic()
|
|
113
126
|
|
|
@@ -103,6 +103,10 @@ def parse(text: str, room: str = "") -> tuple[list[Problem], list[str]]:
|
|
|
103
103
|
problems: list[Problem] = []
|
|
104
104
|
current: Problem | None = None
|
|
105
105
|
detail: list[str] = []
|
|
106
|
+
#: The field the indented lines below it continue. The first Windows runs
|
|
107
|
+
#: had operators write `- **Evidence:**` with the evidence as a nested
|
|
108
|
+
#: list under it, and five findings came out "no evidence" that had it.
|
|
109
|
+
field_key: str | None = None
|
|
106
110
|
|
|
107
111
|
def close():
|
|
108
112
|
if current is not None:
|
|
@@ -115,6 +119,7 @@ def parse(text: str, room: str = "") -> tuple[list[Problem], list[str]]:
|
|
|
115
119
|
current = Problem(severity=heading["severity"].strip().lower(),
|
|
116
120
|
title=heading["title"], room=room)
|
|
117
121
|
detail = []
|
|
122
|
+
field_key = None
|
|
118
123
|
elif current is not None and (field := _FIELD.match(line)) \
|
|
119
124
|
and field["label"].strip().lower() in by_label:
|
|
120
125
|
value = field["value"].strip()
|
|
@@ -124,7 +129,12 @@ def parse(text: str, room: str = "") -> tuple[list[Problem], list[str]]:
|
|
|
124
129
|
if key == "kind":
|
|
125
130
|
value = value.lower()
|
|
126
131
|
setattr(current, key, value)
|
|
132
|
+
field_key = key
|
|
133
|
+
elif current is not None and field_key and line[:1].isspace() and line.strip():
|
|
134
|
+
before = getattr(current, field_key)
|
|
135
|
+
setattr(current, field_key, f"{before}\n{line.strip()}".strip())
|
|
127
136
|
elif current is not None:
|
|
137
|
+
field_key = None
|
|
128
138
|
detail.append(line)
|
|
129
139
|
close()
|
|
130
140
|
malformed = [f"{p.title}: {issue}" for p in problems for issue in p.problems()]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quality-loop
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Measure what a product teaches - its skill, tools or manual - with isolated agent runs of real tasks, reported by an operator agent
|
|
5
5
|
Author: craftware
|
|
6
6
|
License-Expression: MIT
|
|
@@ -121,9 +121,12 @@ under a policy the person sets. See [Running the loop](https://github.com/crftwr
|
|
|
121
121
|
|
|
122
122
|
## Status
|
|
123
123
|
|
|
124
|
-
0.x, early. It has been run end to end on macOS against the two examples
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
0.x, early. It has been run end to end on macOS against the two examples,
|
|
125
|
+
Keyhac and XeFM (including XeFM's terminal UI in a live check), and on
|
|
126
|
+
Windows (11, ARM64) against the toy, Keyhac and XeFM, with real page and
|
|
127
|
+
terminal screens. Not yet run anywhere for real: synthetic keys and
|
|
128
|
+
screenshots of a desktop app (`drive.DesktopApp`); a terminal app is not
|
|
129
|
+
driven on Windows yet. The design discussion is
|
|
127
130
|
[crftwr/keyhac#205](https://github.com/crftwr/keyhac/issues/205) and this
|
|
128
131
|
repository's issues.
|
|
129
132
|
|
|
@@ -190,3 +190,34 @@ def test_mac_key_codes():
|
|
|
190
190
|
assert _mac.keycode(keys.parse("Shift-G")) == (5, _mac.FLAGS["shift"])
|
|
191
191
|
assert _mac.keycode(keys.parse("G")) == (5, 0), "a letter names the key"
|
|
192
192
|
assert _mac.keycode(keys.parse("F1"))[0] == 122
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@posix_only
|
|
196
|
+
def test_a_lone_esc_is_not_glued_to_the_next_key(tmp_path):
|
|
197
|
+
"""Esc then Ctrl-T, sent 20ms apart, reached XeFM as Alt-Ctrl-T."""
|
|
198
|
+
pytest.importorskip("pyte")
|
|
199
|
+
from quality_loop.drive import terminal
|
|
200
|
+
script = tmp_path / "gaps.py"
|
|
201
|
+
script.write_text(textwrap.dedent('''
|
|
202
|
+
import os, sys, time, tty
|
|
203
|
+
tty.setraw(0)
|
|
204
|
+
last, out = time.monotonic(), []
|
|
205
|
+
while len(out) < 2:
|
|
206
|
+
data = os.read(0, 16)
|
|
207
|
+
now = time.monotonic()
|
|
208
|
+
out.append((data.hex(), round(now - last, 2)))
|
|
209
|
+
last = now
|
|
210
|
+
sys.stdout.write(repr(out) + "\\r\\n")
|
|
211
|
+
sys.stdout.flush()
|
|
212
|
+
time.sleep(5)
|
|
213
|
+
'''))
|
|
214
|
+
app = terminal.PtyApp([sys.executable, str(script)], rows=5, cols=60)
|
|
215
|
+
try:
|
|
216
|
+
app.wait_quiet(patience=5)
|
|
217
|
+
app.send(keys.sequence(["Esc", "Ctrl-T"]))
|
|
218
|
+
app.wait_quiet(patience=5)
|
|
219
|
+
seen = eval(app.text().strip().splitlines()[-1])
|
|
220
|
+
finally:
|
|
221
|
+
app.stop()
|
|
222
|
+
assert [hex_ for hex_, _gap in seen] == ["1b", "14"], "two reads, not one ESC+key burst"
|
|
223
|
+
assert seen[1][1] >= terminal.ESC_SETTLE - 0.05
|
|
@@ -401,12 +401,14 @@ def test_an_upgrade_leaves_a_hand_edited_skill_alone(tmp_path):
|
|
|
401
401
|
|
|
402
402
|
|
|
403
403
|
def test_the_command_found_is_relative_inside_the_checkout(tmp_path, monkeypatch):
|
|
404
|
-
|
|
404
|
+
# The venv's own layout: Scripts/quality-loop.exe on Windows.
|
|
405
|
+
bin_dir = tmp_path / ".venv" / ("Scripts" if os.name == "nt" else "bin")
|
|
405
406
|
bin_dir.mkdir(parents=True)
|
|
406
|
-
|
|
407
|
+
script = "quality-loop.exe" if os.name == "nt" else "quality-loop"
|
|
408
|
+
(bin_dir / script).write_text("")
|
|
407
409
|
monkeypatch.setattr(sys, "executable", str(bin_dir / "python"))
|
|
408
410
|
monkeypatch.setattr(install.shutil, "which", lambda _name: None)
|
|
409
|
-
assert install.command_for(tmp_path) ==
|
|
411
|
+
assert install.command_for(tmp_path) == (bin_dir / script).relative_to(tmp_path).as_posix()
|
|
410
412
|
assert install.commands_for(tmp_path) == {
|
|
411
413
|
"command": ".venv/bin/quality-loop",
|
|
412
414
|
"command_windows": ".venv/Scripts/quality-loop.exe"}
|
|
@@ -41,8 +41,8 @@ def test_the_operator_has_no_shell_and_only_quality_loop_s_server(op):
|
|
|
41
41
|
assert "Write" not in allow and "Edit" not in allow
|
|
42
42
|
assert "mcp__quality-loop" in allow
|
|
43
43
|
writes = [rule for rule in allow if rule.startswith(("Write(", "Edit("))]
|
|
44
|
-
assert writes == [f"Edit(
|
|
45
|
-
f"Edit(
|
|
44
|
+
assert writes == [f"Edit({operator.absolute(op.directory)}/**)",
|
|
45
|
+
f"Edit({operator.absolute(op.room / 'REPORT.md')})"]
|
|
46
46
|
assert all(rule.startswith("Edit(//") for rule in writes), "absolute: //"
|
|
47
47
|
assert _value(command, "--permission-mode") == "dontAsk"
|
|
48
48
|
|
|
@@ -128,6 +128,24 @@ def test_a_report_is_read_the_way_a_loop_needs_it():
|
|
|
128
128
|
assert any("no id" in m for m in malformed) and any("no evidence" in m for m in malformed)
|
|
129
129
|
|
|
130
130
|
|
|
131
|
+
def test_a_field_continued_as_a_nested_list_is_kept():
|
|
132
|
+
problems, malformed = report.parse(
|
|
133
|
+
"### [medium] Defaults pasted in\n"
|
|
134
|
+
"- **Id:** `extend-default`\n"
|
|
135
|
+
"- **Kind:** product\n"
|
|
136
|
+
"- **Evidence:**\n"
|
|
137
|
+
" - 66 new lines in a 14-line file\n"
|
|
138
|
+
" - config.md says to copy the defaults\n"
|
|
139
|
+
"- **Where it would be fixed:** config.md\n"
|
|
140
|
+
"\n"
|
|
141
|
+
" An indented paragraph after a blank line is free text.\n")
|
|
142
|
+
assert not malformed
|
|
143
|
+
assert problems[0].evidence == ("- 66 new lines in a 14-line file\n"
|
|
144
|
+
"- config.md says to copy the defaults")
|
|
145
|
+
assert problems[0].where == "config.md"
|
|
146
|
+
assert "indented paragraph" in problems[0].detail
|
|
147
|
+
|
|
148
|
+
|
|
131
149
|
def test_findings_concatenate_and_count_most_severe_first(tmp_path):
|
|
132
150
|
path = report.write_findings(tmp_path, [
|
|
133
151
|
("11_f1", "F1 off", REPORT),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|