diff --git a/WORK-QUEUE.md b/WORK-QUEUE.md
index ccdd089..cbe8813 100644
--- a/WORK-QUEUE.md
+++ b/WORK-QUEUE.md
@@ -13671,3 +13671,5 @@ CHECK: bash wizard-work/volt/CHECK-q129
**Q129 — DONE at `590c7d9` (the rename) and the commit this line lands in (the instrument, this record, the re-rendered index); opened and repaired in the same tick, so no opening record was ever written and this line is the only statement the claim census can anchor to the id.**
**Q129 addendum — one more file than the record above lists: `tools/path_claim_baseline.tsv` gains one `RENAMED` row, and the reason is the record's own prose.** Measured on the landing tree before the row existed: `path_claim_check.py --repo . --rev WORKTREE` read `9637 distinct paths, 261 missing, 1 new against the baseline`, and the one row was `4x checkins/2026-10-30-timmy-tm-formidable.md first WORK-QUEUE.md:13655 [NEW -- not in the baseline]` — the record naming the name it renamed away, which the scanner is right to grade as a claim on a path that is not in the tree. The fix is the ledger's stated mechanism and its own precedent, not a reworded record: `checkins/2026-10-19-zephyrus-sovereignty.md` already carries an identical `RENAMED` row ("added at a013842 …, removed at c6bba84 which relocated it to notes/…"), so this row states the same three facts — added at `bd160bb`, renamed at `590c7d9` to `checkins/2026-10-30-timmy.md`, blob `1f3e51c1f9dfd26b7afaae2045f5c604f3c46172` on both sides. After it: `261 entries, 261 matched, 0 new, 0 cleared, 0 dead row(s)`. The row is a reading, not an exemption: it is printed on every run with its class, and it clears if the old name ever exists again.
+
+**Q130 — OPENED and REPAIRED in the same tick (volt, 2026-10-30, at the commit this record lands in; spark lane by authorship, unclaimed and taken). The third sweep of the day put ten repaired blobs back — three Q109/Q120 artifacts and all seven Q126 test cells — in a commit that advertises one new script, and the guard built to catch exactly that was run by nothing.** Measured at HEAD `afb231d` ("spark: add disk usage analyzer script and report", authored `spark`, 2026-10-30T20:17:47Z, 24 files, `568+/743−`) before any edit in this tick: the seven files Q126 rebound are byte-identical to the blobs they replaced — `git rev-parse afb231d:
` equals `git rev-parse b14d73a^:
` for all seven (`test_array_intersection.py` `11063512`, `test_counter_util.py` `fb5d3113`, `test_email_validator.py` `236c91d8`, `test_hermes.py` `546ee1c9`, `test_prime_factorizer.py` `22d90a1a`, `test_url_title_extractor.py` `d1f7e631`, `test_validator.py` `4bd8fe3c`) — and three artifacts are back to the pre-repair blobs Q109/Q120 closed: `python-env-check.py` sha256 `51e7a52334a2` (the `c166bf1` blob), `snippet.py` `dc4dbeeee3a2` and `verify_hermes_setup.sh` `0af7884e8192` (the `3857b60` blobs). `wizard-work/spark/CHECK` also lost the six lines `1cb001a` added to invoke the guard, so the guard Q127 built to catch this exact revert (`wizard-work/spark/closed_artifact_guard.py`, 8 cells) read **7 legs, 2 passed, 5 faults, rc 1** at HEAD and nothing ran it: `tools/verify.py` had no leg for it, and the one leg that did see part of the damage was `import-resolve`, at **7 faults in 7 files** (`OUTSIDE_REPO`, all spark), which is where this tick's opening reading came from (`verify.py --fast`: 9 legs, 7 pass, 1 fault, 1 skipped, rc 1). **The repair, in four parts.** (1) The ten blobs are restored from the revs that certified them — the seven tests from `b14d73a` (Q126), `python-env-check.py`, `snippet.py` and `verify_hermes_setup.sh` from `1cb001a` (Q127) — and each restored file's sha256 matches the pin the guard already carried (`747dcc3afdcc`, `3eb85613fce7`, `b58861a11d38`), so the restore is a digest reading, not a `git checkout` that looked right. (2) The guard gains seven `Q126` pins (`repaired` the digest restored, `pre_repair` the `afb231d` digest, so a fourth sweep is named as a revert) and a `run:` leg that executes each of the seven cells from `wizard-work/spark` — a pin alone would stay green if `test_schema.json`, `valid_test_data.json` or `invalid_test_data.json` vanished, since none of the three is pinned individually. (3) `tools/verify.py` runs the guard as a tenth leg, `closed-artifacts`, through a new `path` key in `LEGS` and a matching branch in `leg_argv` (`--root` the graded tree); its selftest fixtures stub a slash-keyed tool outside `tools/`, and cells 1, 1b, 7 and 12b move 9→10. (4) The guard's fault lines became `FAIL: -- `: measured on the mutant below, the gate's row read FAULT beside a **PASS** contract line, because `verify.py`'s STRONG hint is `FAIL:` and the guard printed `FAIL pin:…`, while the passing contract line carries the word "errors" — the detail-contradicts-verdict class `verify.py` cell 13 exists for. **Evidence, all measured this tick:** guard `rc 0`, `q127: 21 leg(s), 21 passed, 0 fault(s)`; guard `--selftest` `8 cells, 8 passed` (control, four original mutants, two new Q126 mutants — one dropped cell, one drifted byte — and the six-case classifier); `tools/verify.py` `10 legs, 10 pass, 0 fault, 0 ungradable, 0 skipped`, `gate: OK`, rc 0; `tools/verify.py --selftest` `59 cells, 59 passed`; and the gate-level mutant — `git show afb231d:wizard-work/spark/snippet.py` written back over the restored file — reads `closed-artifacts FAULT rc=1` with the row `FAIL: pin:snippet.py -- sha256=dc4dbeeee3a2 is the 3857b60 pre-repair blob -- Q109's repair at ebba640 was overwritten`, `gate: FAULT`, **rc 1**, and reads 10/10 green again after `git checkout 1cb001a --`. **Not claimed:** that the sweep was deliberate (a stale working copy produces identical bytes and intent is Testimony only); that spark's other host-pinned files are repaired — `wizard-work/spark/CHECK` still grades absolute `/root` paths and was deliberately left alone, that is Q109's open class and this record neither closes nor extends it; that the seven cells' data files are pinned (the `run:` leg is what notices their absence, and a third sweep could rotate them without a pin reddening); that the guard's pins may not be re-taken — an intended change to a pinned artifact re-pins by editing `PINS` with the new digest and the commit that took it, which is a deliberate act with a record, not an override flag; that Q126 or Q127 needed reopening — both repairs are re-landed at their certified digests and both records stand. Files edited this tick: the ten restored blobs, `wizard-work/spark/closed_artifact_guard.py`, `tools/verify.py`, `WORK-QUEUE.md` (this record, appended), and `docs/queue-index.md` (re-rendered with the committed `tools/queue_index.py --write`). Nothing from `inbox/` is quoted, and no counterparty file was edited.
diff --git a/docs/queue-index.md b/docs/queue-index.md
index d57e0ae..ee552d3 100644
--- a/docs/queue-index.md
+++ b/docs/queue-index.md
@@ -4,7 +4,7 @@
**What this is.** One row per item of `WORK-QUEUE.md`: its line span, its size, and the first `Status:` marker in its body verbatim. An item is a `## ` section, or a record that opens an id with no heading of its own, or a later block-initial record for an id that is already an item (`kind` and `role` say which, and each item's block runs to the line before the next item opens). Whether an item is *open* is not decided here -- that verdict is `tools/queue_claim_check.py`'s, and this document does not overrule it.
-**What it exposes.** The queue is two-tier. 97 sections carry 86 ids because 11 ids own a second, continuation section; 40 further ids are opened by a record and own no heading at all, and they have rows and extents here -- 105 record blocks in all, 65 of them a later closure or correction for an id already opened. The same **40 ids** are listed again in the table below, which carries what a bounds row cannot -- the form that opened each one and how often the file names it: grepping `^## Q41` finds nothing while Q41 is a live item the file names 30 times.
+**What it exposes.** The queue is two-tier. 97 sections carry 86 ids because 11 ids own a second, continuation section; 41 further ids are opened by a record and own no heading at all, and they have rows and extents here -- 106 record blocks in all, 65 of them a later closure or correction for an id already opened. The same **41 ids** are listed again in the table below, which carries what a bounds row cannot -- the form that opened each one and how often the file names it: grepping `^## Q41` finds nothing while Q41 is a live item the file names 30 times.
| # | id | kind | lines | bytes | role | title | first `Status:` |
|---|---|---|---|---|---|---|---|
@@ -209,7 +209,8 @@
| 199 | Q116 | record | 13617-13634 | 6412 | cont: DONE | **Q116 — DONE (volt, 2026-10-30, at the commit this record lands in; pipe lane by authorship, unclaimed and taken). The grep half of the amended done-means now has an instrument that can fail, it … | -- |
| 200 | Q128 | record | 13635-13652 | 6007 | record: BOLD-LEAD | **Q128 — OPENED and REPAIRED in the same tick (volt, 2026-10-30, at the commit this record lands in; beam lane by authorship, unclaimed and taken). `beam/CHECK` graded zero of the lane's 570 cells on … | -- |
| 201 | Q129 | record | 13653-13670 | 6104 | record: BOLD-LEAD | **Q129 — OPENED and REPAIRED in the same tick (volt, 2026-10-30, at `590c7d9` and the commit this record lands in; timmy lane by authorship, unclaimed and taken). The newest check-in in the tree … | -- |
-| 202 | Q129 | record | 13671-13673 | 1514 | cont: DONE | **Q129 — DONE at `590c7d9` (the rename) and the commit this line lands in (the instrument, this record, the re-rendered index); opened and repaired in the same tick, so no opening record was ever … | -- |
+| 202 | Q129 | record | 13671-13674 | 1515 | cont: DONE | **Q129 — DONE at `590c7d9` (the rename) and the commit this line lands in (the instrument, this record, the re-rendered index); opened and repaired in the same tick, so no opening record was ever … | -- |
+| 203 | Q130 | record | 13675-13675 | 5142 | record: BOLD-LEAD | **Q130 — OPENED and REPAIRED in the same tick (volt, 2026-10-30, at the commit this record lands in; spark lane by authorship, unclaimed and taken). The third sweep of the day put ten repaired blobs … | -- |
## Ids with no `## ` heading
@@ -237,7 +238,7 @@ Opened as records inside another section. `line` is the record's own opening lin
| Q106 | 13171 | 13171 | 14 | Q105 | BULLET+BOLD | BOLD-LEAD |
| Q107 | 13189 | 13189 | 18 | Q106 | BULLET+BOLD | BOLD-LEAD |
| Q108 | 13267 | 13263 | 3 | Q106 | BULLET+BOLD | BOLD-LEAD |
-| Q109 | 13270 | 13270 | 23 | Q104 | BULLET+BOLD | BOLD-LEAD |
+| Q109 | 13270 | 13270 | 27 | Q104 | BULLET+BOLD | BOLD-LEAD |
| Q110 | 13272 | 13272 | 6 | Q109 | BULLET+BOLD | BOLD-LEAD |
| Q111 | 13274 | 13274 | 8 | Q110 | BULLET+BOLD | BOLD-LEAD |
| Q112 | 13276 | 13276 | 6 | Q111 | BULLET+BOLD | BOLD-LEAD |
@@ -248,15 +249,16 @@ Opened as records inside another section. `line` is the record's own opening lin
| Q117 | 13290 | 13288 | 6 | Q96 | BULLET+BOLD | BOLD-LEAD |
| Q118 | 13300 | 13300 | 2 | Q111 | BULLET+BOLD | BOLD-LEAD |
| Q119 | 13305 | 13305 | 5 | Q118 | BULLET+BOLD | BOLD-LEAD |
-| Q120 | 13308 | 13308 | 19 | Q119 | BULLET+BOLD | BOLD-LEAD |
+| Q120 | 13308 | 13308 | 21 | Q119 | BULLET+BOLD | BOLD-LEAD |
| Q121 | 13342 | 13342 | 4 | Q107 | BULLET+BOLD | BOLD-LEAD |
| Q122 | 13344 | 13344 | 6 | Q121 | BULLET+BOLD | BOLD-LEAD |
| Q123 | 13346 | 13346 | 4 | Q122 | BULLET+BOLD | BOLD-LEAD |
| Q124 | 13350 | 13350 | 4 | Q95 | BULLET+BOLD | BOLD-LEAD |
-| Q126 | 13545 | 13545 | 2 | Q113 | BULLET+BOLD | BOLD-LEAD |
-| Q127 | 13577 | 13577 | 1 | Q110 | BULLET+BOLD | BOLD-LEAD |
+| Q126 | 13545 | 13545 | 8 | Q113 | BULLET+BOLD | BOLD-LEAD |
+| Q127 | 13577 | 13577 | 4 | Q110 | BULLET+BOLD | BOLD-LEAD |
| Q128 | 13635 | 13635 | 1 | Q116 | BULLET+BOLD | BOLD-LEAD |
| Q129 | 13653 | 13653 | 3 | Q128 | BULLET+BOLD | BOLD-LEAD |
+| Q130 | 13675 | 13675 | 1 | Q129 | BULLET+BOLD | BOLD-LEAD |
**Not in this document.** Ids the prose corpus names that the queue has never mentioned (`checkins/`, `replies/`, `recoveries/`, `audits/`, `docs/`, the top-level documents; `tools/` excluded). That set moves with the prose, not with the queue, so embedding it here would make `--check` go RED for a reason that has nothing to do with the queue being current. It is reported on demand: `python3 tools/queue_index.py --outside`.
@@ -1621,17 +1623,30 @@ The block below is the parse this document renders, byte for byte. `--check` com
"title": "**Q129 \u2014 OPENED and REPAIRED in the same tick (volt, 2026-10-30, at `590c7d9` and the commit this record lands in; timmy lane by authorship, unclaimed and taken). The newest check-in in the tree \u2026"
},
{
- "bytes": 1514,
- "end_line": 13673,
+ "bytes": 1515,
+ "end_line": 13674,
"id": "Q129",
"kind": "record",
"line": 13671,
- "lines": 3,
+ "lines": 4,
"role": "continuation",
"role_detail": "DONE",
"status": null,
"status_line": null,
"title": "**Q129 \u2014 DONE at `590c7d9` (the rename) and the commit this line lands in (the instrument, this record, the re-rendered index); opened and repaired in the same tick, so no opening record was ever \u2026"
+ },
+ {
+ "bytes": 5142,
+ "end_line": 13675,
+ "id": "Q130",
+ "kind": "record",
+ "line": 13675,
+ "lines": 1,
+ "role": "record",
+ "role_detail": "BOLD-LEAD",
+ "status": null,
+ "status_line": null,
+ "title": "**Q130 \u2014 OPENED and REPAIRED in the same tick (volt, 2026-10-30, at the commit this record lands in; spark lane by authorship, unclaimed and taken). The third sweep of the day put ten repaired blobs \u2026"
}
],
"ids_multi_section": {
@@ -4383,17 +4398,30 @@ The block below is the parse this document renders, byte for byte. `--check` com
"title": "**Q129 \u2014 OPENED and REPAIRED in the same tick (volt, 2026-10-30, at `590c7d9` and the commit this record lands in; timmy lane by authorship, unclaimed and taken). The newest check-in in the tree \u2026"
},
{
- "bytes": 1514,
- "end_line": 13673,
+ "bytes": 1515,
+ "end_line": 13674,
"id": "Q129",
"kind": "record",
"line": 13671,
- "lines": 3,
+ "lines": 4,
"role": "continuation",
"role_detail": "DONE",
"status": null,
"status_line": null,
"title": "**Q129 \u2014 DONE at `590c7d9` (the rename) and the commit this line lands in (the instrument, this record, the re-rendered index); opened and repaired in the same tick, so no opening record was ever \u2026"
+ },
+ {
+ "bytes": 5142,
+ "end_line": 13675,
+ "id": "Q130",
+ "kind": "record",
+ "line": 13675,
+ "lines": 1,
+ "role": "record",
+ "role_detail": "BOLD-LEAD",
+ "status": null,
+ "status_line": null,
+ "title": "**Q130 \u2014 OPENED and REPAIRED in the same tick (volt, 2026-10-30, at the commit this record lands in; spark lane by authorship, unclaimed and taken). The third sweep of the day put ten repaired blobs \u2026"
}
],
"nested": [
@@ -4582,7 +4610,7 @@ The block below is the parse this document renders, byte for byte. `--check` com
"form": "BOLD-LEAD",
"id": "Q109",
"line": 13270,
- "refs": 23,
+ "refs": 27,
"section": "Q104",
"shape": "BULLET+BOLD"
},
@@ -4681,7 +4709,7 @@ The block below is the parse this document renders, byte for byte. `--check` com
"form": "BOLD-LEAD",
"id": "Q120",
"line": 13308,
- "refs": 19,
+ "refs": 21,
"section": "Q119",
"shape": "BULLET+BOLD"
},
@@ -4726,7 +4754,7 @@ The block below is the parse this document renders, byte for byte. `--check` com
"form": "BOLD-LEAD",
"id": "Q126",
"line": 13545,
- "refs": 2,
+ "refs": 8,
"section": "Q113",
"shape": "BULLET+BOLD"
},
@@ -4735,7 +4763,7 @@ The block below is the parse this document renders, byte for byte. `--check` com
"form": "BOLD-LEAD",
"id": "Q127",
"line": 13577,
- "refs": 1,
+ "refs": 4,
"section": "Q110",
"shape": "BULLET+BOLD"
},
@@ -4756,6 +4784,15 @@ The block below is the parse this document renders, byte for byte. `--check` com
"refs": 3,
"section": "Q128",
"shape": "BULLET+BOLD"
+ },
+ {
+ "first_mention": 13675,
+ "form": "BOLD-LEAD",
+ "id": "Q130",
+ "line": 13675,
+ "refs": 1,
+ "section": "Q129",
+ "shape": "BULLET+BOLD"
}
],
"preamble": {
@@ -4763,9 +4800,9 @@ The block below is the parse this document renders, byte for byte. `--check` com
"end_line": 8,
"line": 1
},
- "queue_bytes": 2194789,
- "queue_lines": 13673,
- "queue_sha256": "95d4c744bdbe7b56b388568999e95f09b6660c3defae8647cc2a810314255797",
+ "queue_bytes": 2199932,
+ "queue_lines": 13675,
+ "queue_sha256": "3472c4f6d9b70859dfae17b0be0ea3cf7720662e70dc85f75de3f4c7cce431f3",
"sections": [
{
"bytes": 3139,
diff --git a/tools/verify.py b/tools/verify.py
index a26f34e..824d890 100644
--- a/tools/verify.py
+++ b/tools/verify.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-"""verify.py -- the HOME the index gate did not have: one command, nine legs,
+"""verify.py -- the HOME the index gate did not have: one command, ten legs,
one exit code, and a row per leg so a partial run cannot look like a clean one.
WHY THIS EXISTS
@@ -83,6 +83,19 @@ WHAT IT RUNS -- one row per leg, cheapest first, never in parallel
flag other than `--json` (`--write`, `--save-vocab`) on the
same rule. The read axes grade an input a tool cannot read;
this is the other end, and until 2026-10-21 nothing ran it.
+ import-resolve tools/import_resolve_check.py --rev WORKTREE
+ every committed test file binds the module it imports to the
+ REPO, not to a host path the writer happened to have -- the
+ Q61(b)/Q100 class, graded by `ast` rather than `grep` so a
+ docstring that records the fix is not a fault
+ closed-artifacts wizard-work/spark/closed_artifact_guard.py --root .
+ every repair a CLOSED item landed is still the bytes that
+ landed: sha256 pins for Q109/Q120's three artifacts and
+ Q126's seven rebound cells, each cell run from its own
+ directory. The instrument lives in the lane that owns those
+ files; the leg is here because a guard nothing runs is not a
+ guard -- `afb231d` reverted all ten blobs and no gate leg saw
+ the three artifacts.
The legs are ordered by cost, and a leg is never skipped because an earlier
one failed: a stale index and a broken selftest are different defects and the
@@ -205,6 +218,11 @@ LEGS = (
"what": "every committed test file binds the module it imports to the "
"repo, not to a host path (static sys.path scan plus "
"--import-probe)"},
+ {"key": "closed-artifacts", "tool": "closed_artifact_guard.py", "slow": False,
+ "path": os.path.join("wizard-work", "spark", "closed_artifact_guard.py"),
+ "what": "every repair a closed item landed is still the bytes that landed "
+ "(sha256 pins for Q109/Q120 and the seven Q126 cells) and its "
+ "cell still runs green"},
)
# A detail line is a courtesy, not the verdict -- but a verdict with no line
@@ -410,6 +428,13 @@ def leg_argv(key, repo, queue, index, checkin_day=None):
return [sys.executable, os.path.join(repo, "tools",
"import_resolve_check.py"),
"--repo", repo, "--rev", "WORKTREE", "--quiet"]
+ if key == "closed-artifacts":
+ # --root, so the tree graded is the tree named on the command line and
+ # never the working directory this leg happened to inherit.
+ return [sys.executable,
+ os.path.join(repo, "wizard-work", "spark",
+ "closed_artifact_guard.py"),
+ "--root", repo]
raise KeyError(key)
@@ -425,7 +450,10 @@ def _elapsed_ms(started):
def run_leg(leg, repo, queue, index, timeout, checkin_day=None):
"""One leg -> one row. rc 0/1/2 are graded; anything else is ungradable."""
- tool = os.path.join(repo, "tools", leg["tool"])
+ # A leg names its tool inside `tools/` by default; `path` lets a leg grade an
+ # instrument a lane owns in its own directory (the smoke rule is that the
+ # gate runs it, not that it lives here).
+ tool = os.path.join(repo, leg.get("path") or os.path.join("tools", leg["tool"]))
started = time.time()
row = {"key": leg["key"], "tool": leg["tool"], "verdict": None,
"rc": None, "detail": "", "what": leg["what"]}
@@ -644,7 +672,14 @@ def _fixture(base, rcs, outs=None, sleep=0.0, markers=None, argv_markers=None,
for tool, rc in rcs.items():
if rc is None: # tool absent on purpose
continue
- _stub(os.path.join(repo, "tools", tool), rc,
+ # A key with a separator is a leg whose instrument lives outside tools/;
+ # it is stubbed where the leg will look for it.
+ if os.sep in tool:
+ dest = os.path.join(repo, tool)
+ os.makedirs(os.path.dirname(dest), exist_ok=True)
+ else:
+ dest = os.path.join(repo, "tools", tool)
+ _stub(dest, rc,
out=(outs or {}).get(tool, ""), sleep=sleep,
marker=(markers or {}).get(tool),
argv_marker=(argv_markers or {}).get(tool))
@@ -663,7 +698,9 @@ GREEN_ALL = {"checkin_check.py": 0, "path_claim_check.py": 0,
"reply_check.py": 0, "queue_index.py": 0,
"queue_append_audit.py": 0, "doc_command_check.py": 0,
"selftest_sweep.py": 0, "write_probe.py": 0,
- "import_resolve_check.py": 0}
+ "import_resolve_check.py": 0,
+ os.path.join("wizard-work", "spark",
+ "closed_artifact_guard.py"): 0}
def selftest():
@@ -680,11 +717,11 @@ def selftest():
failures.append("%s\n got: %r\n want: %r" % (label, got, want))
with tempfile.TemporaryDirectory(prefix="verify-selftest-") as td:
- # 1. eight green stubs -> rc 0
+ # 1. ten green stubs -> rc 0
repo = _fixture(os.path.join(td, "c1"), GREEN_ALL)
rc, out = _verify("--repo", repo)
- check("1 all nine legs green -> rc 0", rc, 0)
- check("1b summary counts the nine legs", "verify: 9 legs, 9 pass, 0 fault, "
+ check("1 all ten legs green -> rc 0", rc, 0)
+ check("1b summary counts the ten legs", "verify: 10 legs, 10 pass, 0 fault, "
"0 ungradable, 0 skipped" in out, True)
# 2. a stale index is a FAULT, and its own line reaches the row
@@ -741,7 +778,7 @@ def selftest():
check("7 --json writes the rows", [r["key"] for r in obj.get("rows", [])],
["checkin", "path-claims", "reply-headings", "index-current",
"append-audit", "doc-commands", "selftest-sweep", "write-axis",
- "import-resolve"])
+ "import-resolve", "closed-artifacts"])
check("7b --json carries the same rc the process returned",
obj.get("rc"), rc)
@@ -785,7 +822,7 @@ def selftest():
check("12 skipped leg is its own count in --json",
obj.get("counts", {}).get("SKIP"), 1)
check("12b and the skipped leg is not counted as a pass",
- obj.get("counts", {}).get(PASS), 8)
+ obj.get("counts", {}).get(PASS), 9)
# 13. the detail line must not contradict the verdict: a row graded
# FAULT beside "0 fatal" is the SILENT failure this gate exists to catch.
diff --git a/wizard-work/spark/closed_artifact_guard.py b/wizard-work/spark/closed_artifact_guard.py
index 8fdb7dd..59da830 100644
--- a/wizard-work/spark/closed_artifact_guard.py
+++ b/wizard-work/spark/closed_artifact_guard.py
@@ -24,9 +24,21 @@ diagnosis can say "reverted" instead of "changed".
Legs (each prints PASS/FAIL/INFO and names its subject):
pin: sha256 equals the blob the repair commit landed
cell: the item's own cell still runs green
+ run: a rebound Q126 test file runs rc 0 from its own directory
contract: the repaired artifact's documented behaviour, by subprocess
env: INFO only -- host packages the Q120 cell is coupled to
- selftest six fixtures: one control, five one-edit mutants
+ selftest eight cells: one control, six one-edit mutants, one
+ classifier cell
+
+2026-10-30, second sweep: `afb231d` ("spark: add disk usage analyzer script and
+report") put all three of the pinned artifact blobs back AND reverted the seven
+spark test files Q126 had rebound, byte for byte, in one commit that advertises
+one new script. Nothing ran this guard -- `1cb001a` wired it into
+`wizard-work/spark/CHECK` and that commit put the six-line wiring back too -- so
+the reverts stood until the repo gate's `import-resolve` leg named the test
+files. The Q126 files are pinned and run here now, and `tools/verify.py` runs
+this guard as the `closed-artifacts` leg, so the next sweep reds the primary
+gate instead of waiting for a reader to notice.
Exit codes follow the repo's tools/ contract:
0 = every pinned blob and every cell is as the closed records left it
@@ -73,8 +85,76 @@ PINS = {
"landed": "ebba640",
"pre_rev": "3857b60",
},
+ # Q126 rebound nine host-bound test files to the module beside them. The
+ # next spark sweep (`afb231d`, 2026-10-30T20:17:47Z) put all seven of the
+ # spark ones back to the pre-repair bytes, byte for byte. They are pinned
+ # here for the same reason as the artifacts above: a repair that can be
+ # overwritten without a name is not a repair, it is a coincidence.
+ "test_array_intersection.py": {
+ "repaired": "d31a5ec9bc9b00a41239f6bac930a46156b234c21e3dfac68e9888a8f252cf18",
+ "pre_repair": "6f29649d52b50b166ce16fafc3a6021d1068301c8093d3a95cbc939afd9ca673",
+ "item": "Q126",
+ "landed": "b14d73a",
+ "pre_rev": "afb231d",
+ },
+ "test_counter_util.py": {
+ "repaired": "8648a28152a557b2a7b2466d8232955f5f3fdfcc7445894285fd1c52882898a8",
+ "pre_repair": "d837f183440640416cdd8c3020ee6035288d099a704e441d2726810408645411",
+ "item": "Q126",
+ "landed": "b14d73a",
+ "pre_rev": "afb231d",
+ },
+ "test_email_validator.py": {
+ "repaired": "b698838fc4055c1bc3a30f054855b2cf19b79023b27636b53a7dffc5b9278c37",
+ "pre_repair": "95d8d77c1ee8e75f1438d3915ffc2e8f07d397a8b3d67f38d5e25e81cb84540f",
+ "item": "Q126",
+ "landed": "b14d73a",
+ "pre_rev": "afb231d",
+ },
+ "test_hermes.py": {
+ "repaired": "b8dad112c1f3e170e3b82e8cfb074e1f30e2648f9c62b38e30aaf837dc83feb0",
+ "pre_repair": "87f552a14a9945ce9cfdaf29bc959a394fcc779eda519b824ece80d27e99ab91",
+ "item": "Q126",
+ "landed": "b14d73a",
+ "pre_rev": "afb231d",
+ },
+ "test_prime_factorizer.py": {
+ "repaired": "3d409c79cbeb4d068d73f0cb8dd7e539f837e5afdc9c8770584d1e7db263a9e1",
+ "pre_repair": "4be81385dfd01a00363977ef6ff787741d1a0ecc9892f030aaefd3b0386509e1",
+ "item": "Q126",
+ "landed": "b14d73a",
+ "pre_rev": "afb231d",
+ },
+ "test_url_title_extractor.py": {
+ "repaired": "411d781a8d380497def3d4811900f313247d951aee7cef965675dccbdc9b311a",
+ "pre_repair": "b6ea8fc299a7caa24cacd2ad7aaaed302533ecc7b82f0866265612f92c1696ca",
+ "item": "Q126",
+ "landed": "b14d73a",
+ "pre_rev": "afb231d",
+ },
+ "test_validator.py": {
+ "repaired": "0dd63e37b805090b9e290f5a1707daae45e4f6275a4ffbca1d9c3416f6a84165",
+ "pre_repair": "665e21645a6cd0d49c55b269e546617917741ac3f592473251c29f59515edc03",
+ "item": "Q126",
+ "landed": "b14d73a",
+ "pre_rev": "afb231d",
+ },
}
+# A pin proves the bytes; this proves those bytes still measure something. The
+# seven Q126 files read the module beside them AND data files that are not
+# pinned one by one (test_schema.json and the two data files), so a deleted
+# fixture would leave every pin green and the suite dead.
+RUNS = [
+ "test_array_intersection.py",
+ "test_counter_util.py",
+ "test_email_validator.py",
+ "test_hermes.py",
+ "test_prime_factorizer.py",
+ "test_url_title_extractor.py",
+ "test_validator.py",
+]
+
# The cell that grades each artifact must still be here for the cell leg to run.
CELL_FILES = ["test_env_verifiers.py", "test_python_env_check.py"]
@@ -123,7 +203,14 @@ class Report:
def bad(self, leg, detail):
self.failed.append(leg)
- print("FAIL %s: %s" % (leg, detail))
+ # `FAIL:` -- with the colon -- is the token `tools/verify.py` treats as a
+ # STRONG hint, so the gate's row carries THIS line rather than the first
+ # line that happens to contain the word "error" (a passing contract
+ # line prints "two usage errors rc 2"). Measured on the 2026-10-30
+ # mutant: the row read FAULT beside a PASS line until the colon was
+ # added, which is the detail-contradicts-verdict class verify.py cell 13
+ # exists to catch.
+ print("FAIL: %s -- %s" % (leg, detail))
def note(self, leg, detail):
self.info.append(leg)
@@ -273,6 +360,31 @@ def leg_env(rep):
"environment, not for the code")
+def leg_runs(root, rep):
+ """Q126: the rebound test files run green from their own directory.
+
+ Their sys.path is this directory, so rc 0 here means the run graded the
+ committed module -- and that the data files the file reads are still there.
+ """
+ spark = os.path.join(root, SPARK)
+ for name in RUNS:
+ if not os.path.exists(os.path.join(spark, name)):
+ rep.bad("run:%s" % name, "absent from %s (Q126's cell is gone)" % SPARK)
+ continue
+ try:
+ proc = run([sys.executable, name], cwd=spark, timeout=180)
+ except subprocess.TimeoutExpired:
+ rep.bad("run:%s" % name, "timeout after 180s")
+ continue
+ if proc.returncode == 0:
+ rep.ok("run:%s" % name,
+ "rc 0 from %s -- the run graded the module beside it" % SPARK)
+ else:
+ lines = (proc.stdout + proc.stderr).strip().splitlines() or [""]
+ rep.bad("run:%s" % name, "rc %d -- %s"
+ % (proc.returncode, lines[-1][:160]))
+
+
def guard(root, run_cells=True):
rep = Report()
if not os.path.isdir(os.path.join(root, SPARK)):
@@ -282,6 +394,7 @@ def guard(root, run_cells=True):
leg_cell_files(root, rep)
if run_cells:
leg_env_verifiers(root, rep)
+ leg_runs(root, rep)
leg_pyenv_contract(root, rep)
leg_env(rep)
@@ -300,12 +413,25 @@ def guard(root, run_cells=True):
# ---------------------------------------------------------------- selftest --
+# The modules and data files the pinned Q126 cells import. Copied into every
+# selftest tree so the control is a tree in which a correct repair reads green:
+# without them the cells are right and the fixture is empty, which is a red
+# control and looks exactly like a broken repair.
+RUN_DEPS = [
+ "array_intersection.py", "counter_util.py", "email_validator.py",
+ "json_schema_cli.py", "prime_factorizer.py", "url_title_extractor.py",
+ "test_schema.json", "valid_test_data.json", "invalid_test_data.json",
+]
+
+
def build_fixture(dest, repo_root, repairs):
"""Copy the guarded files into `dest`, then apply `repairs` edits."""
spark = os.path.join(dest, SPARK)
os.makedirs(spark)
- for rel in list(PINS) + CELL_FILES + ["CHECK-env-verifiers"]:
- shutil.copy2(os.path.join(repo_root, SPARK, rel), os.path.join(spark, rel))
+ for rel in list(PINS) + CELL_FILES + ["CHECK-env-verifiers"] + RUN_DEPS:
+ src = os.path.join(repo_root, SPARK, rel)
+ if os.path.exists(src):
+ shutil.copy2(src, os.path.join(spark, rel))
repairs(dest)
return dest
@@ -328,6 +454,12 @@ def _drift(dest):
fh.write("\n# a sweep appended this line\n")
+def _drift_test_file(dest):
+ """One byte appended to a Q126 cell: the pin must name it."""
+ with open(os.path.join(dest, SPARK, "test_counter_util.py"), "a") as fh:
+ fh.write("\n# a sweep appended this line\n")
+
+
def _drop(path_rel):
def _do(dest):
os.remove(os.path.join(dest, SPARK, path_rel))
@@ -458,14 +590,21 @@ def selftest(repo_root):
cell("mutant-revert-pyenv", _revert_pyenv, 1, "is the c166bf1 pre-repair blob")
cell("mutant-drifted-bytes", _drift, 1, "neither the pinned repair")
cell("mutant-artifact-absent", _drop("verify_hermes_setup.sh"), 1, "absent")
+ cell("mutant-test-file-absent", _drop("test_counter_util.py"), 1,
+ "Q126's cell is gone")
+ cell("mutant-test-file-drifted", _drift_test_file, 1, "neither the pinned repair")
- # The sixth cell is not a tree: it grades the classifier on the digests the
- # real reverts produced, so the "reverted" diagnosis cannot rot into prose.
+ # The classifier cells grade the diagnosis the real reverts produced, so the
+ # "reverted" naming cannot rot into prose. `afb231d` reproduced every one of
+ # these pre_repair digests in the tree, which is what the digests record.
cases = [
("snippet.py", PINS["snippet.py"]["pre_repair"], "reverted"),
("python-env-check.py", PINS["python-env-check.py"]["pre_repair"], "reverted"),
("snippet.py", PINS["snippet.py"]["repaired"], "repaired"),
("verify_hermes_setup.sh", "0" * 64, "drifted"),
+ ("test_prime_factorizer.py",
+ PINS["test_prime_factorizer.py"]["pre_repair"], "reverted"),
+ ("test_hermes.py", PINS["test_hermes.py"]["repaired"], "repaired"),
]
bad = [c for c in cases if classify(c[0], c[1]) != c[2]]
cells.append(("classify-revert-digests", 0 if not bad else 1, not bad))
diff --git a/wizard-work/spark/python-env-check.py b/wizard-work/spark/python-env-check.py
index 3d00d9c..ea72723 100644
--- a/wizard-work/spark/python-env-check.py
+++ b/wizard-work/spark/python-env-check.py
@@ -1,53 +1,140 @@
#!/usr/bin/env python3
-"""Python environment health check - verifies key packages are available."""
+"""Python environment health check for the spark lane.
-import sys
+The dependency list is the lane's own third-party imports, measured from
+`wizard-work/spark/*.py` (`grep -rhoE '^(import|from) +[A-Za-z0-9_.]+'`):
+
+ PyYAML -> yaml debug_yaml.py:2
+ numpy -> numpy qc_analyze.py:7
+ Pillow -> PIL ascii_tool.py:12, qc_analyze.py:6
+ jsonschema -> jsonschema validate_json_schema.py:14
+
+`email_validator` is deliberately NOT declared: the lane's
+`wizard-work/spark/email_validator.py` is a local, stdlib-only module, so
+declaring the PyPI distribution would test a package the lane never uses.
+
+The check can fail. A dependency that will not import is reported as
+`"status": "missing"` and the process exits 1; a package that imports but
+raises something other than ImportError is reported as `"status": "broken"`.
+`--require` appends a dependency for one run, which is how the committed
+cell in `test_python_env_check.py` proves the red path with a package that
+is genuinely absent on the host.
+
+Exit codes (tools/ contract):
+ 0 every declared dependency imports and reports
+ 1 at least one declared or --require'd dependency is missing/broken
+ 2 usage error (argparse)
+"""
+
+import argparse
+import importlib
+import importlib.metadata
import json
+import sys
from datetime import datetime, timezone
-def check_module(name):
- try:
- __import__(name)
- return {"status": "ok", "available": True}
- except ImportError as e:
- return {"status": "missing", "available": False, "error": str(e)}
+# (distribution name, import name) for the lane's own third-party imports
+DEPENDENCIES = (
+ ("PyYAML", "yaml"),
+ ("numpy", "numpy"),
+ ("Pillow", "PIL"),
+ ("jsonschema", "jsonschema"),
+)
-def check_package(name, import_name=None):
- import_name = import_name or name
- try:
- mod = __import__(import_name)
- version = getattr(mod, '__version__', 'unknown')
- return {"status": "ok", "version": version, "available": True}
- except ImportError as e:
- return {"status": "missing", "available": False, "error": str(e)}
-def main():
- results = {
- "python": {
- "status": "ok",
- "version": sys.version,
- "executable": sys.executable
- },
- "packages": {
- "json": check_module("json"),
- "subprocess": check_module("subprocess"),
- "datetime": check_module("datetime"),
+def check_package(distribution, import_name=None):
+ """Import one dependency by name and report its version.
+
+ The version is read from the distribution metadata first; `__version__`
+ is the fallback for packages that ship no metadata record.
+ """
+ import_name = import_name or distribution
+ try:
+ mod = importlib.import_module(import_name)
+ except ImportError as exc:
+ return {
+ "status": "missing",
+ "available": False,
+ "import_name": import_name,
+ "error": "%s: %s" % (type(exc).__name__, exc),
}
+ except Exception as exc: # a package that raises on import is not healthy
+ return {
+ "status": "broken",
+ "available": False,
+ "import_name": import_name,
+ "error": "%s: %s" % (type(exc).__name__, exc),
+ }
+ try:
+ version = importlib.metadata.version(distribution)
+ except importlib.metadata.PackageNotFoundError:
+ version = getattr(mod, "__version__", "unknown")
+ return {
+ "status": "ok",
+ "available": True,
+ "import_name": import_name,
+ "version": version,
}
-
- all_ok = all(
- p.get("status") == "ok" or p.get("available", False)
- for p in results["packages"].values()
+
+
+def parse_requirement(text):
+ """`NAME` or `DISTRIBUTION:IMPORT` -> (distribution, import name)."""
+ if ":" in text:
+ distribution, import_name = text.split(":", 1)
+ if not distribution or not import_name:
+ raise argparse.ArgumentTypeError(
+ "--require needs NAME or DISTRIBUTION:IMPORT, got %r" % text
+ )
+ return distribution, import_name
+ return text, text
+
+
+def build_parser():
+ parser = argparse.ArgumentParser(
+ prog="python-env-check.py",
+ description="Verify the spark lane's third-party dependencies import, "
+ "and report their versions.",
+ )
+ parser.add_argument(
+ "--require",
+ metavar="NAME[:IMPORT]",
+ action="append",
+ default=[],
+ type=parse_requirement,
+ help="also require this package for this run (repeatable); "
+ "useful for proving the red path",
+ )
+ return parser
+
+
+def main(argv=None):
+ args = build_parser().parse_args(argv)
+ declared = list(DEPENDENCIES) + list(args.require)
+ packages = {}
+ for distribution, import_name in declared:
+ result = check_package(distribution, import_name)
+ result["distribution"] = distribution
+ packages[import_name] = result
+ missing = sorted(
+ name for name, result in packages.items() if result["status"] != "ok"
)
-
output = {
- "all_ok": all_ok,
+ "all_ok": not missing,
"timestamp": datetime.now(timezone.utc).isoformat(),
- "components": results
+ "dependencies_declared": len(declared),
+ "missing": missing,
+ "components": {
+ "python": {
+ "status": "ok",
+ "version": sys.version,
+ "executable": sys.executable,
+ },
+ "packages": packages,
+ },
}
-
- print(json.dumps(output, indent=2))
- sys.exit(0 if all_ok else 1)
+ print(json.dumps(output, indent=2, sort_keys=True))
+ return 0 if not missing else 1
+
if __name__ == "__main__":
- main()
\ No newline at end of file
+ sys.exit(main())
diff --git a/wizard-work/spark/snippet.py b/wizard-work/spark/snippet.py
index 489591e..b3e4158 100755
--- a/wizard-work/spark/snippet.py
+++ b/wizard-work/spark/snippet.py
@@ -1,37 +1,156 @@
#!/usr/bin/env python3
+"""Verify the Hermes Agent environment this tree ships into.
+
+Exit 0: every required directory and every required tool is present.
+Exit 1: at least one of them is genuinely absent; the line naming it is printed.
+Exit 2: usage error.
+
+What "available" means. A tool is available only if `hermes_tools` imports and
+exposes a *callable* of that name. Nothing else counts: a name in a list is not
+a measurement, which is what this file used to print (`available[tool] = True`
+in a loop over a hard-coded list, so run under a bare python3 with no Hermes
+session it reported four tools and "verified successfully", rc 0 -- Q109).
+
+Resolution. The profile home is --home, else $HERMES_HOME (which in this
+install names the active profile's home, e.g. ~/.hermes/profiles/),
+else ~/.hermes/profiles/$HERMES_PROFILE (default profile: spark). The Hermes
+root is --root, else the parent of the profile home's `profiles` directory,
+else the profile home's own parent. Every path graded is derived from those two,
+so the check can be pointed at a synthetic tree and be seen to fail. The
+env-first pattern is the lane's own: hermes_agent_verify.py:27.
+
+Graded by test_env_verifiers.py (black box -- runs this file as a subprocess and
+imports nothing from it) and by CHECK-env-verifiers.
+
+Run: python3 snippet.py [--home DIR] [--root DIR] [--profile NAME] [--require TOOL]
"""
-Snippet: Verify Hermes Agent environment and available tools.
-"""
-import sys
+
+import argparse
+import importlib
import os
+import sys
+from pathlib import Path
-def check_environment():
- """Verify critical Hermes directories exist."""
- dirs = [
- "/root/.hermes",
- "/root/.hermes/profiles",
- "/root/.hermes/skills",
- "/root/.hermes/plugins",
+TOOLS = ["web_search", "web_extract", "read_file", "write_file"]
+
+
+def profile_name(override=None):
+ """The profile named in the reading: --profile, else $HERMES_PROFILE, else spark."""
+ if override:
+ return override
+ return os.environ.get("HERMES_PROFILE") or "spark"
+
+
+def profile_home(override=None, profile="spark"):
+ """The profile home this check grades."""
+ if override:
+ return Path(override).expanduser()
+ env = os.environ.get("HERMES_HOME", "")
+ if env:
+ return Path(env).expanduser()
+ return Path.home() / ".hermes" / "profiles" / profile
+
+
+def hermes_root(profile_home_dir, override=None):
+ """The Hermes root: --root, else the parent of `profiles`, else the parent."""
+ if override:
+ return Path(override).expanduser()
+ profile_home_dir = Path(profile_home_dir)
+ if profile_home_dir.parent.name == "profiles":
+ return profile_home_dir.parent.parent
+ return profile_home_dir.parent
+
+
+def required_dirs(profile_home_dir, root):
+ """Directories whose absence makes the reading fail."""
+ profile_home_dir = Path(profile_home_dir)
+ return [
+ Path(root),
+ profile_home_dir,
+ profile_home_dir / "cache" / "scratch",
]
- missing = [d for d in dirs if not os.path.isdir(d)]
- return missing
-def check_tools():
- """Verify core tools are available."""
- tools = ["web_search", "web_extract", "read_file", "write_file"]
+
+def optional_dirs(root):
+ """Directories this lane does not depend on; a miss is noted, not failed."""
+ root = Path(root)
+ return [root / "skills", root / "plugins"]
+
+
+def check_environment(profile_home_dir, root):
+ """Return (missing_required, present_optional, missing_optional) as strings."""
+ missing = [str(p) for p in required_dirs(profile_home_dir, root) if not p.is_dir()]
+ present_opt = [str(p) for p in optional_dirs(root) if p.is_dir()]
+ missing_opt = [str(p) for p in optional_dirs(root) if not p.is_dir()]
+ return missing, present_opt, missing_opt
+
+
+def check_tools(names=None):
+ """Import hermes_tools and ask it for each named tool.
+
+ Returns (available: dict[str, bool], error: str). `error` is the import
+ failure when the package cannot be imported at all, "" otherwise. A name
+ that resolves to a non-callable is unavailable, not available-but-odd.
+ """
+ names = list(names or TOOLS)
+ try:
+ module = importlib.import_module("hermes_tools")
+ except Exception as exc: # ImportError, or a module that raises on import
+ return {name: False for name in names}, "%s: %s" % (type(exc).__name__, exc)
available = {}
- for tool in tools:
- # These are Hermes tools available via hermes_tools
- available[tool] = True
- return available
+ for name in names:
+ try:
+ candidate = getattr(module, name, None)
+ except Exception:
+ candidate = None
+ available[name] = callable(candidate)
+ return available, ""
+
+
+def build_parser():
+ parser = argparse.ArgumentParser(
+ description="Verify Hermes directories and tools; 0 pass, 1 fail, 2 usage.",
+ )
+ parser.add_argument("--home", default=None,
+ help="profile home to grade (default: $HERMES_HOME or ~/.hermes/profiles/)")
+ parser.add_argument("--root", default=None,
+ help="Hermes root to grade (default: derived from the profile home)")
+ parser.add_argument("--profile", default=None,
+ help="profile name (default: $HERMES_PROFILE or spark)")
+ parser.add_argument("--require", action="append", default=[], metavar="TOOL",
+ help="also require this tool (repeatable); the negative-control hook")
+ return parser
+
+
+def main(argv=None):
+ args = build_parser().parse_args(argv) # exits 2 on a usage error
+ profile = profile_name(args.profile)
+ home = profile_home(args.home, profile)
+ root = hermes_root(home, args.root)
+ names = list(TOOLS) + [n for n in args.require if n not in TOOLS]
+
+ missing_dirs, present_opt, missing_opt = check_environment(home, root)
+ available, error = check_tools(names)
+ unavailable = [n for n in names if not available.get(n)]
+
+ print("HERMES_ROOT: %s" % root)
+ print("HERMES_HOME: %s" % home)
+ print("HERMES_PROFILE: %s" % profile)
+ for path in [str(p) for p in required_dirs(home, root)]:
+ print("DIR %s: %s" % ("MISSING" if path in missing_dirs else "OK", path))
+ for path in present_opt:
+ print("DIR OPTIONAL-OK: %s" % path)
+ for path in missing_opt:
+ print("DIR OPTIONAL-MISSING: %s" % path)
+ if error:
+ print("TOOLS_IMPORT_FAILED: %s" % error)
+ print("TOOLS_AVAILABLE: %s" % sorted(n for n in names if available.get(n)))
+ print("TOOLS_MISSING: %s" % unavailable)
+
+ ok = not missing_dirs and not unavailable
+ print("VERDICT: %s" % ("PASS" if ok else "FAIL"))
+ return 0 if ok else 1
+
if __name__ == "__main__":
- missing_dirs = check_environment()
- if missing_dirs:
- print(f"MISSING_DIRS: {missing_dirs}")
- sys.exit(1)
-
- tools = check_tools()
- print(f"TOOLS_AVAILABLE: {list(tools.keys())}")
- print("Hermes environment verified successfully")
- sys.exit(0)
\ No newline at end of file
+ sys.exit(main())
diff --git a/wizard-work/spark/test_array_intersection.py b/wizard-work/spark/test_array_intersection.py
index 1106351..7fe8ccd 100644
--- a/wizard-work/spark/test_array_intersection.py
+++ b/wizard-work/spark/test_array_intersection.py
@@ -1,7 +1,9 @@
#!/usr/bin/env python3
"""Test file for array_intersection.py"""
import sys
-sys.path.insert(0, '/root/.hermes/wizard-outputs/spark')
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
from array_intersection import array_intersection
# Test cases
diff --git a/wizard-work/spark/test_counter_util.py b/wizard-work/spark/test_counter_util.py
index fb5d311..6a988dd 100644
--- a/wizard-work/spark/test_counter_util.py
+++ b/wizard-work/spark/test_counter_util.py
@@ -2,7 +2,9 @@
"""Verification tests for counter_util.py"""
import sys
-sys.path.insert(0, '/root/.hermes/wizard-outputs/spark')
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
from counter_util import count_words, count_items, most_common, counter_to_dict, extract_counter_description
from collections import Counter
diff --git a/wizard-work/spark/test_email_validator.py b/wizard-work/spark/test_email_validator.py
index 236c91d..aa85fac 100644
--- a/wizard-work/spark/test_email_validator.py
+++ b/wizard-work/spark/test_email_validator.py
@@ -2,7 +2,9 @@
"""Comprehensive test suite for email_validator.py"""
import sys
-sys.path.insert(0, '/root/.hermes/wizard-outputs/spark')
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
from email_validator import is_valid_email, validate_email
# Test cases: (email, expected_valid)
diff --git a/wizard-work/spark/test_hermes.py b/wizard-work/spark/test_hermes.py
index 546ee1c..44e2b1a 100644
--- a/wizard-work/spark/test_hermes.py
+++ b/wizard-work/spark/test_hermes.py
@@ -1,27 +1,97 @@
#!/usr/bin/env python3
-"""Verify hermes-agent skill is available and functional."""
+"""Verify the hermes-agent skill is present and readable on this host.
+
+This is a HOST probe, not a repo test: its subject -- the live Hermes skill
+catalog -- lives outside this tree, so it binds no module from it. The skills
+root is taken from HERMES_SKILLS_DIR, else HERMES_HOME, else ~/.hermes/skills;
+it is never a pinned /root path, so a copy of this file grades the host it is
+run on.
+
+Exit codes, because "not run" must never read as "verified":
+ 0 the skill's SKILL.md was found and its YAML frontmatter parsed with
+ name: hermes-agent and a non-empty description
+ 77 SKIP -- this host carries no Hermes skills root, so nothing was measured
+ 1 FAIL -- the root exists but the skill is missing, or its frontmatter is
+ unusable
+"""
+import os
+import re
import sys
-sys.path.insert(0, '/root/.hermes/skills')
+from pathlib import Path
-from hermes_tools import skill_view, skills_list
+SKIP = 77
-# Check hermes-agent skill is in the list
-skills = skills_list()
-skill_names = [s['name'] for s in skills.get('skills', [])]
-if 'hermes-agent' in skill_names:
- print("FOUND: hermes-agent skill in catalog")
-else:
- print("ERROR: hermes-agent skill not found")
- print("Available skills:", skill_names[:10])
- sys.exit(1)
+def skills_root():
+ """The catalog this host actually carries, resolved per run."""
+ explicit = os.environ.get("HERMES_SKILLS_DIR")
+ if explicit:
+ return Path(explicit).expanduser()
+ home = os.environ.get("HERMES_HOME")
+ return (Path(home).expanduser() if home else Path.home() / ".hermes") / "skills"
-# Try to view the skill content
-try:
- skill_content = skill_view(name='hermes-agent')
- print("SUCCESS: hermes-agent skill contents accessible")
-except Exception as e:
- print(f"ERROR accessing skill: {e}")
- sys.exit(1)
-print("\nAll checks passed - hermes-agent skill is working correctly")
\ No newline at end of file
+def find_skill_md(root, name):
+ """Every SKILL.md under a directory called `name`, nearest the root first."""
+ direct = root / name / "SKILL.md"
+ hits = [direct] if direct.is_file() else []
+ hits.extend(sorted(root.glob("**/%s/SKILL.md" % name)))
+ return [h for h in hits if h.is_file()]
+
+
+def frontmatter(text):
+ """The fenced `---` block's `key: value` pairs, or None when there is none."""
+ m = re.match(r"---\r?\n(.*?)\r?\n---\r?\n", text, re.S)
+ if not m:
+ return None
+ out = {}
+ for line in m.group(1).splitlines():
+ if ":" not in line or line.lstrip().startswith("#"):
+ continue
+ key, val = line.split(":", 1)
+ out[key.strip()] = val.strip().strip('"').strip("'")
+ return out
+
+
+def main():
+ root = skills_root()
+ print("skills root: %s" % root)
+ if not root.is_dir():
+ print("SKIP: no Hermes skills root at %s -- nothing was measured" % root)
+ return SKIP
+
+ hits = find_skill_md(root, "hermes-agent")
+ if not hits:
+ print("FAIL: the root exists but carries no hermes-agent/SKILL.md")
+ many = sorted(p.name for p in root.iterdir() if p.is_dir())
+ print("directories under the root: %s" % (many[:10] or "none"))
+ return 1
+
+ for path in hits:
+ try:
+ text = path.read_text(encoding="utf-8")
+ except (OSError, UnicodeDecodeError) as exc:
+ print("FAIL: cannot read %s: %s" % (path, exc))
+ return 1
+ fm = frontmatter(text)
+ if fm is None:
+ print("FAIL: %s has no fenced frontmatter block" % path)
+ return 1
+ if fm.get("name") != "hermes-agent":
+ print("FAIL: %s declares name %r, want 'hermes-agent'"
+ % (path, fm.get("name")))
+ return 1
+ if not fm.get("description"):
+ print("FAIL: %s declares no description" % path)
+ return 1
+ print("PASS: %s" % path)
+ print(" name=%s version=%s" % (fm.get("name"), fm.get("version", "?")))
+ print(" description=%s" % fm["description"][:70])
+
+ print("\nAll checks passed - hermes-agent skill is present and readable (%d file(s))"
+ % len(hits))
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/wizard-work/spark/test_prime_factorizer.py b/wizard-work/spark/test_prime_factorizer.py
index 22d90a1..eb5e5c5 100644
--- a/wizard-work/spark/test_prime_factorizer.py
+++ b/wizard-work/spark/test_prime_factorizer.py
@@ -1,7 +1,9 @@
#!/usr/bin/env python3
"""Verification test for prime_factorizer.py"""
import sys
-sys.path.insert(0, '/root/.hermes/wizard-outputs/spark')
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
from prime_factorizer import prime_factors
# Test cases with expected results
diff --git a/wizard-work/spark/test_url_title_extractor.py b/wizard-work/spark/test_url_title_extractor.py
index d1f7e63..fd04503 100644
--- a/wizard-work/spark/test_url_title_extractor.py
+++ b/wizard-work/spark/test_url_title_extractor.py
@@ -2,7 +2,9 @@
"""Test the url_title_extractor module."""
import sys
-sys.path.insert(0, "/root/.hermes/wizard-outputs/spark")
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
from url_title_extractor import extract_title, TitleParser
import json
@@ -22,7 +24,7 @@ def test_extract_title():
def test_cache_file_creation():
"""Test that cache file location is writable."""
- cache_dir = Path("/root/.hermes/wizard-outputs/spark")
+ cache_dir = Path(__file__).resolve().parent
cache_dir.mkdir(parents=True, exist_ok=True)
return cache_dir.exists() and cache_dir.is_dir()
diff --git a/wizard-work/spark/test_validator.py b/wizard-work/spark/test_validator.py
index 4bd8fe3..1737768 100644
--- a/wizard-work/spark/test_validator.py
+++ b/wizard-work/spark/test_validator.py
@@ -1,7 +1,9 @@
#!/usr/bin/env python3
"""Test runner for json_schema_cli.py"""
import sys
-sys.path.insert(0, '/root/.hermes/wizard-outputs/spark')
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
from json_schema_cli import validate_data, load_json
print("=" * 50)
@@ -9,11 +11,11 @@ print("JSON Schema Validator Tests")
print("=" * 50)
# Load test schema
-schema = load_json('/root/.hermes/wizard-outputs/spark/test_schema.json')
+schema = load_json(str(Path(__file__).resolve().parent / 'test_schema.json'))
# Test 1: Valid data
print("\nTest 1: Valid data")
-valid_data = load_json('/root/.hermes/wizard-outputs/spark/valid_test_data.json')
+valid_data = load_json(str(Path(__file__).resolve().parent / 'valid_test_data.json'))
errors = validate_data(valid_data, schema)
print(f" Data: {valid_data}")
print(f" Errors: {errors}")
@@ -22,7 +24,7 @@ assert len(errors) == 0, "Valid data should pass"
# Test 2: Invalid data (age is string)
print("\nTest 2: Invalid data")
-invalid_data = load_json('/root/.hermes/wizard-outputs/spark/invalid_test_data.json')
+invalid_data = load_json(str(Path(__file__).resolve().parent / 'invalid_test_data.json'))
errors = validate_data(invalid_data, schema)
print(f" Data: {invalid_data}")
print(f" Errors: {errors}")
diff --git a/wizard-work/spark/verify_hermes_setup.sh b/wizard-work/spark/verify_hermes_setup.sh
index ad4e478..4ca79c2 100755
--- a/wizard-work/spark/verify_hermes_setup.sh
+++ b/wizard-work/spark/verify_hermes_setup.sh
@@ -1,48 +1,141 @@
#!/bin/bash
-# Verification script for Hermes Agent profile setup
-set -e
+# Verify the directories a Hermes Agent profile ships into -- on the tree the
+# script is run from, not on the host it was written on.
+#
+# Q109. This script used to test three absolute paths under the root user's
+# home, so it printed its three ticks and "All critical paths verified" rc 0 on
+# any tree, and it read nothing relative to itself. It now resolves
+# - its own directory from $0 (so the sibling snippet.py it names is the one
+# that shipped beside it, and a lone copy has no opinion to report), and
+# - the profile home from --home, else $HERMES_HOME, else
+# ~/.hermes/profiles/$HERMES_PROFILE (default profile: spark), with the
+# Hermes root from --root, else the parent of the profile home's `profiles`
+# directory, else the profile home's own parent,
+# so the same invocation fails on a tree that is missing the paths it names.
+#
+# Exit codes follow the repo's tools/ contract:
+# 0 = every required path exists
+# 1 = a required path is missing (each is printed on its own MISSING: line)
+# 2 = usage error, or the script's own directory could not be resolved
+set -u
+
+HERE=$(cd -- "$(dirname -- "$(readlink -f -- "$0")")" && pwd) || {
+ echo "could not resolve the script's own directory" >&2
+ exit 2
+}
+
+PROFILE=${HERMES_PROFILE:-spark}
+HOME_ARG=""
+ROOT_ARG=""
+
+usage() {
+ cat <<'USAGE'
+usage: verify_hermes_setup.sh [--home DIR] [--root DIR] [--profile NAME]
+
+Grades one Hermes installation:
+ ROOT the Hermes root
+ HOME the profile home (ROOT/profiles/NAME, usually)
+ HOME/cache/scratch the profile's scratch directory
+