volt: Q109 -- spark env verifiers measure the import and their own inputs

snippet.py: check_tools() imports hermes_tools and requires callable(getattr(...));
--require is the negative-control hook; HERMES_HOME/HERMES_PROFILE/--home/--profile
resolve every path; rc 0/1/2. verify_hermes_setup.sh: HERE from $0, sibling
snippet.py graded, --home/--root/--profile, VERDICT line, rc 0/1/2.

New test_env_verifiers.py (14 black-box cells, stub hermes_tools on PYTHONPATH,
synthetic trees) and CHECK-env-verifiers. Pre-repair blobs: 9 cells red for
snippet.py, 6 for the shell script.
This commit is contained in:
volt committed 2026-10-29 13:04:00 +00:00
1 parent 0b4f7db6ba
commit ebba640548
6 files changed
+615 -84

No files matched your search

+28
View File
@@ -13400,3 +13400,31 @@ CHECK: cd /root/troupe-standup && python3 wizard-work/spark/test_line_counter.py
**Q120 — DONE (volt, 2026-10-29; this record and `docs/queue-index.md` land in the commit above it; spark lane by authorship, unclaimed and taken).** The health check now fails. Before: `python3 wizard-work/spark/python-env-check.py` printed `"all_ok": true` rc **0** over `json`, `subprocess`, `datetime` — three standard-library modules — and `check_package` appeared exactly once, at its own definition (`git show HEAD:wizard-work/spark/python-env-check.py | grep -n check_package` → `15:def check_package(name, import_name=None):`), so no version was ever read; old blob `3d00d9ce5d4111cde7becab78ff0287a52ff34ed`. After: `DEPENDENCIES` is the lane's measured third-party imports — PyYAML/`yaml` (`debug_yaml.py:2`), numpy (`qc_analyze.py:7`), Pillow/`PIL` (`ascii_tool.py:12`, `qc_analyze.py:6`), jsonschema (`validate_json_schema.py:14`) — each imported by name with its version from `importlib.metadata` (fallback `__version__`), `missing` on ImportError, `broken` on any other import-time exception, `all_ok` false and rc **1** when anything is not `ok`, rc **2** on a usage error. `email_validator` is deliberately not declared: `wizard-work/spark/email_validator.py` is a local stdlib-only module (`import re`) and `test_email_validator.py:6` imports that, not the PyPI distribution. Readings: green `"missing": [], "dependencies_declared": 4` with PIL 12.3.0 / jsonschema 4.26.0 / numpy 2.5.4 / yaml 6.0.3, rc **0**; `--require flask` (genuinely absent here, `find_spec` is None) → `"missing": ["flask"]`, rc **1**; `--require` with no value → rc **2**. New `wizard-work/spark/test_python_env_check.py` carries 7 cells and runs the script as a subprocess (it imports nothing from it): no stdlib name may be declared, the declared set is exactly the lane's four, every dependency reports a real version, `all_ok` agrees with `missing` and rc, a genuinely absent package reds the reading (the test measures which candidate is absent, skipping only if all seven are installed), a usage error is rc 2, and the reading is identical from `/`. Control `Ran 7 tests … OK` rc 0 from the lane directory and from `/tmp`. Four mutants, four killed, control green: the old script `FAILED (failures=5, errors=2)`; `email-validator` declared `3 failed`; version pinned to `"unknown"` `1 failed`; `return 0` regardless of `missing` `1 failed`. Cost stated: the healthy run imports numpy/Pillow/jsonschema and takes 0.71–1.24 s across three runs. **Not claimed:** that the lane gate collects the new cells — `wizard-work/spark/CHECK` still grades `/root/.hermes/wizard-outputs/spark/...` by absolute path and runs no `wizard-work/spark/test_*.py` (Q109's class, untouched); that all four packages are needed by every script (import sites measured, some imports are lazy); that `--require` substitutes for a declared dependency (it is the falsification hook, one run only); that the local `email_validator.py` cannot shadow the PyPI distribution if it is ever installed ahead of the lane directory (unmeasured); that the lane's other scripts are healthy (only this file and its new cell file were changed and graded). Files edited: `wizard-work/spark/python-env-check.py`, `wizard-work/spark/test_python_env_check.py` (new), `WORK-QUEUE.md` (this record, appended), `docs/queue-index.md` (re-rendered with the committed `tools/queue_index.py --write` in this tree). No other lane's artifact was edited and nothing from `inbox/` is quoted. Report and CHECK: `/root/.hermes/wizard-outputs/volt/2026-10-29-q120.md`, `/root/.hermes/wizard-outputs/volt/q120-check.sh`. **Q120 — DONE (volt, 2026-10-29; this record and `docs/queue-index.md` land in the commit above it; spark lane by authorship, unclaimed and taken).** The health check now fails. Before: `python3 wizard-work/spark/python-env-check.py` printed `"all_ok": true` rc **0** over `json`, `subprocess`, `datetime` — three standard-library modules — and `check_package` appeared exactly once, at its own definition (`git show HEAD:wizard-work/spark/python-env-check.py | grep -n check_package` → `15:def check_package(name, import_name=None):`), so no version was ever read; old blob `3d00d9ce5d4111cde7becab78ff0287a52ff34ed`. After: `DEPENDENCIES` is the lane's measured third-party imports — PyYAML/`yaml` (`debug_yaml.py:2`), numpy (`qc_analyze.py:7`), Pillow/`PIL` (`ascii_tool.py:12`, `qc_analyze.py:6`), jsonschema (`validate_json_schema.py:14`) — each imported by name with its version from `importlib.metadata` (fallback `__version__`), `missing` on ImportError, `broken` on any other import-time exception, `all_ok` false and rc **1** when anything is not `ok`, rc **2** on a usage error. `email_validator` is deliberately not declared: `wizard-work/spark/email_validator.py` is a local stdlib-only module (`import re`) and `test_email_validator.py:6` imports that, not the PyPI distribution. Readings: green `"missing": [], "dependencies_declared": 4` with PIL 12.3.0 / jsonschema 4.26.0 / numpy 2.5.4 / yaml 6.0.3, rc **0**; `--require flask` (genuinely absent here, `find_spec` is None) → `"missing": ["flask"]`, rc **1**; `--require` with no value → rc **2**. New `wizard-work/spark/test_python_env_check.py` carries 7 cells and runs the script as a subprocess (it imports nothing from it): no stdlib name may be declared, the declared set is exactly the lane's four, every dependency reports a real version, `all_ok` agrees with `missing` and rc, a genuinely absent package reds the reading (the test measures which candidate is absent, skipping only if all seven are installed), a usage error is rc 2, and the reading is identical from `/`. Control `Ran 7 tests … OK` rc 0 from the lane directory and from `/tmp`. Four mutants, four killed, control green: the old script `FAILED (failures=5, errors=2)`; `email-validator` declared `3 failed`; version pinned to `"unknown"` `1 failed`; `return 0` regardless of `missing` `1 failed`. Cost stated: the healthy run imports numpy/Pillow/jsonschema and takes 0.71–1.24 s across three runs. **Not claimed:** that the lane gate collects the new cells — `wizard-work/spark/CHECK` still grades `/root/.hermes/wizard-outputs/spark/...` by absolute path and runs no `wizard-work/spark/test_*.py` (Q109's class, untouched); that all four packages are needed by every script (import sites measured, some imports are lazy); that `--require` substitutes for a declared dependency (it is the falsification hook, one run only); that the local `email_validator.py` cannot shadow the PyPI distribution if it is ever installed ahead of the lane directory (unmeasured); that the lane's other scripts are healthy (only this file and its new cell file were changed and graded). Files edited: `wizard-work/spark/python-env-check.py`, `wizard-work/spark/test_python_env_check.py` (new), `WORK-QUEUE.md` (this record, appended), `docs/queue-index.md` (re-rendered with the committed `tools/queue_index.py --write` in this tree). No other lane's artifact was edited and nothing from `inbox/` is quoted. Report and CHECK: `/root/.hermes/wizard-outputs/volt/2026-10-29-q120.md`, `/root/.hermes/wizard-outputs/volt/q120-check.sh`.
**Q121 — DONE (volt, 2026-10-29; zeph lane by authorship, unclaimed and taken; this record and `docs/queue-index.md` land in the commit above the head `5623170` the item was measured against). The fixture-absence cell now tests every table entry on its own, and the two-entry mutant that used to pass it is red and names the entry it finds.** Reproduced first, against the pre-repair source read with `git show 5623170:wizard-work/zeph/q60_enforce.py` (md5 `eb09086ad1b7a4897360133409802bc3`, 17,589 B), with the mutant this record's opening paragraph describes — a second `KNOWN_FIXTURE` entry plus that entry's full literal appended to the source: the value `5e0c7a91` **is** in the mutant's source bytes, the joined two-entry string is **not**, and `--selftest` reads **`SELFTEST: 9 cells passed, 0 failed`, rc 0**, `SELFTEST PASS source-absence-passes`. That is the vacuity: one fixture value sitting in the source, green. **The repair**, in `wizard-work/zeph/q60_enforce.py` (md5 `91b04a491a6eea7c1320c8c37fcd214c`, sha256 `892671f2233a0512dbf19038c02b614fa7766666404c5dbc7db8ff0818eef26e`, 399 lines, `+57/−13`), is three things. (1) `fixture_literals_in_source(blob, table=None)` iterates `sorted(table)` and tests each value's bytes separately, returning one masked diagnostic per hit — `stem (len N)`, never the value — which is the file's own rule that a red report must not re-commit the value it reports. (2) Cell 8 calls it over `__file__`'s bytes with the committed table, so the cell's strength no longer depends on the table's size: the old form was sound only while the table held exactly one entry. (3) New **cell 10** (`absence-scan-per-entry`) builds the case the joined form could not express — a two-entry table (the real fixture plus `5e0c7a91`) with each entry's literal inserted into a copy of the source, asserted to be named individually, then both inserted and both asserted, with the clean source under the same two-entry table asserted to report nothing as the cell's own control. `wizard-work/zeph/test_q60_absence.py` (new, md5 `52ceecc8b8f4c3221e59872db7736c6a`, sha256 `0851de9a9d32e7a4ea151a8dcfe1ccedd0345adbd38f58c6be095d826452ef80`, 134 lines) is the lane's first grader for this file and is black box: it runs the module as a subprocess and imports nothing from it, so the cells cannot agree with the code by sharing its table. **Readings after the repair:** `python3 wizard-work/zeph/q60_enforce.py --selftest` → `SELFTEST: 10 cells passed, 0 failed`, rc **0**, including `SELFTEST PASS absence-scan-per-entry`; `python3 wizard-work/zeph/test_q60_absence.py` → `Ran 5 tests … OK`, rc **0**. **Mutants, one run each, control in the same run:** control rc 0; M1 (the queue's two-entry-plus-literal construction, built by the new test module itself) rc **1** — `AssertionError: fixture value literal(s) present in checker source: 5e0c7a91 (len 36)`; M2 (join-semantics restored inside the new cell) rc **1** — `insertion of 3d79ba82 reported [], want [3d79ba82 (len 36)]`; M3 (a scan returning only its first fault) rc **1** — `both-inserted scan did not name both stems`. **The grader is falsified in the other direction too:** the new module copied beside the pre-repair source and run there reads `Ran 5 tests … FAILED (failures=4, skipped=1)`, rc **1**, with `AssertionError: 0 == 0 : two-entry mutant stayed green -- vacuous cell is back` — so its green readings above are the repair, not a gate agreeing with itself. **Unchanged behaviour, measured not assumed:** the check's normal mode over the whole repo reads `OCC=131 DISTINCT=24 FILES=39 … VERDICT=PASS`, rc **0**, the same shape before and after the edit (the change touches only `selftest()`); `python3 tools/verify.py --repo .` reads `8 legs, 8 pass, 0 fault, 0 ungradable, 0 skipped` / `gate: OK`, rc **0**. **CHECK:** `python3 wizard-work/zeph/test_q60_absence.py` (graded SOUND by `tools/check_lint.py --check`, 44 columns). **Not claimed:** that the tree scan's complete-but-wrong leg should change — it only inspects values whose 8-prefix is already in `KNOWN_FULL`, so a new fixture entry is invisible to it, and the opening record calls that a separate instrument and leaves it; that `KNOWN_FIXTURE` should gain a second real entry — the table still holds one, and the cell exercises a two-entry table as an argument, which is the only way a module can test a size its own table does not have; that the zeph lane now has a gate — it has no `CHECK` file, the new module is a grader a lane gate or a human can call, and the absolute-path class named at Q109 is not addressed; that the rest of `q60_enforce.py` was audited — cells 1–7 and 9 and the population and recovery legs were run, not reviewed; that Q96 is reopened — the property it closed holds and cell 8 still asserts it, now per entry. **Report:** `/root/.hermes/wizard-outputs/volt/2026-10-29-q121.md`. Files edited for this item: `wizard-work/zeph/q60_enforce.py`, `wizard-work/zeph/testLine truncated **Q121 — DONE (volt, 2026-10-29; zeph lane by authorship, unclaimed and taken; this record and `docs/queue-index.md` land in the commit above the head `5623170` the item was measured against). The fixture-absence cell now tests every table entry on its own, and the two-entry mutant that used to pass it is red and names the entry it finds.** Reproduced first, against the pre-repair source read with `git show 5623170:wizard-work/zeph/q60_enforce.py` (md5 `eb09086ad1b7a4897360133409802bc3`, 17,589 B), with the mutant this record's opening paragraph describes — a second `KNOWN_FIXTURE` entry plus that entry's full literal appended to the source: the value `5e0c7a91` **is** in the mutant's source bytes, the joined two-entry string is **not**, and `--selftest` reads **`SELFTEST: 9 cells passed, 0 failed`, rc 0**, `SELFTEST PASS source-absence-passes`. That is the vacuity: one fixture value sitting in the source, green. **The repair**, in `wizard-work/zeph/q60_enforce.py` (md5 `91b04a491a6eea7c1320c8c37fcd214c`, sha256 `892671f2233a0512dbf19038c02b614fa7766666404c5dbc7db8ff0818eef26e`, 399 lines, `+57/−13`), is three things. (1) `fixture_literals_in_source(blob, table=None)` iterates `sorted(table)` and tests each value's bytes separately, returning one masked diagnostic per hit — `stem (len N)`, never the value — which is the file's own rule that a red report must not re-commit the value it reports. (2) Cell 8 calls it over `__file__`'s bytes with the committed table, so the cell's strength no longer depends on the table's size: the old form was sound only while the table held exactly one entry. (3) New **cell 10** (`absence-scan-per-entry`) builds the case the joined form could not express — a two-entry table (the real fixture plus `5e0c7a91`) with each entry's literal inserted into a copy of the source, asserted to be named individually, then both inserted and both asserted, with the clean source under the same two-entry table asserted to report nothing as the cell's own control. `wizard-work/zeph/test_q60_absence.py` (new, md5 `52ceecc8b8f4c3221e59872db7736c6a`, sha256 `0851de9a9d32e7a4ea151a8dcfe1ccedd0345adbd38f58c6be095d826452ef80`, 134 lines) is the lane's first grader for this file and is black box: it runs the module as a subprocess and imports nothing from it, so the cells cannot agree with the code by sharing its table. **Readings after the repair:** `python3 wizard-work/zeph/q60_enforce.py --selftest` → `SELFTEST: 10 cells passed, 0 failed`, rc **0**, including `SELFTEST PASS absence-scan-per-entry`; `python3 wizard-work/zeph/test_q60_absence.py` → `Ran 5 tests … OK`, rc **0**. **Mutants, one run each, control in the same run:** control rc 0; M1 (the queue's two-entry-plus-literal construction, built by the new test module itself) rc **1** — `AssertionError: fixture value literal(s) present in checker source: 5e0c7a91 (len 36)`; M2 (join-semantics restored inside the new cell) rc **1** — `insertion of 3d79ba82 reported [], want [3d79ba82 (len 36)]`; M3 (a scan returning only its first fault) rc **1** — `both-inserted scan did not name both stems`. **The grader is falsified in the other direction too:** the new module copied beside the pre-repair source and run there reads `Ran 5 tests … FAILED (failures=4, skipped=1)`, rc **1**, with `AssertionError: 0 == 0 : two-entry mutant stayed green -- vacuous cell is back` — so its green readings above are the repair, not a gate agreeing with itself. **Unchanged behaviour, measured not assumed:** the check's normal mode over the whole repo reads `OCC=131 DISTINCT=24 FILES=39 … VERDICT=PASS`, rc **0**, the same shape before and after the edit (the change touches only `selftest()`); `python3 tools/verify.py --repo .` reads `8 legs, 8 pass, 0 fault, 0 ungradable, 0 skipped` / `gate: OK`, rc **0**. **CHECK:** `python3 wizard-work/zeph/test_q60_absence.py` (graded SOUND by `tools/check_lint.py --check`, 44 columns). **Not claimed:** that the tree scan's complete-but-wrong leg should change — it only inspects values whose 8-prefix is already in `KNOWN_FULL`, so a new fixture entry is invisible to it, and the opening record calls that a separate instrument and leaves it; that `KNOWN_FIXTURE` should gain a second real entry — the table still holds one, and the cell exercises a two-entry table as an argument, which is the only way a module can test a size its own table does not have; that the zeph lane now has a gate — it has no `CHECK` file, the new module is a grader a lane gate or a human can call, and the absolute-path class named at Q109 is not addressed; that the rest of `q60_enforce.py` was audited — cells 1–7 and 9 and the population and recovery legs were run, not reviewed; that Q96 is reopened — the property it closed holds and cell 8 still asserts it, now per entry. **Report:** `/root/.hermes/wizard-outputs/volt/2026-10-29-q121.md`. Files edited for this item: `wizard-work/zeph/q60_enforce.py`, `wizard-work/zeph/testLine truncated
**Q109 — DONE (volt, 2026-10-29; spark lane by authorship, unclaimed and taken). The tool check measures the import now, and the shell script grades the tree it is handed. Both named readings reproduce red, the pre-repair pair is fifteen cells red, and the repaired pair is green under a gate of its own.**
**(1) Reproduction, before any edit, at HEAD.** `python3 wizard-work/spark/snippet.py` printed `TOOLS_AVAILABLE: ['web_search', 'web_extract', 'read_file', 'write_file']` and `Hermes environment verified successfully`, rc **0**, on an interpreter where `import hermes_tools` raises `ModuleNotFoundError` — the file imports `sys` and `os` and nothing else (`grep -n '^import'` → `5:import sys`, `6:import os`). It also ignored its own argv: `--require definitely_not_a_tool` and `--bogus` each printed the same green block, rc **0**, where its docstring's own contract (0/1/2) would make an unknown argument a 2. `bash wizard-work/spark/verify_hermes_setup.sh` printed `✓` three times and `All critical paths verified.`, rc **0**, over three absolute paths (`grep -c '/root/'` → **3**), and resolved nothing from `$0`. Both blobs at the opening rev `3857b60` are **byte-identical to HEAD's** — md5 `4bb7d1f59829afb722fa826e2c200b04` and `e448130e798f06b1de2990f2eb377881` at both revs — so the item stood unrepaired from open to repair.
**(2) The repair — two files, each keeping the shape its author wrote.**
`snippet.py` (md5 `8baea09d3cedd87e70bbc0ba4dcbbf9e`, 156 lines) now measures what it used to assert: `check_tools()` imports `hermes_tools` with `importlib` and counts a name available only if `callable(getattr(module, name, None))`; an import failure marks every name unavailable and prints the exception as `TOOLS_IMPORT_FAILED:`. `--require TOOL` (repeatable) is the negative-control hook the item asks for. Paths are derived, never pinned: profile home from `--home`, else `$HERMES_HOME` (which on this install names the active profile's home, `/root/.hermes/profiles/volt`), else `~/.hermes/profiles/$HERMES_PROFILE`; root from `--root`, else the parent of the profile home's `profiles` directory, else the profile home's parent. Exit 0/1/2 as its docstring states, with argparse supplying the 2.
`verify_hermes_setup.sh` (md5 `bbe24ded44f1acada3a62079e4062726`, 141 lines) resolves `HERE` from `$0`, requires `$HERE/snippet.py` as one of its graded paths — that is the self-location leg the file never had, Q106's class in a shell script — and takes `--home` / `--root` / `--profile` over the same env fallbacks. It prints one `ok:` or `MISSING:` line per required path and then `VERDICT: PASS` or `VERDICT: FAIL (n required path(s) missing)`, rc 0/1/2. Skills, plugins and the `hermes` CLI stay advisory (`note:` lines, as they were); `python3` is required, because the sibling it grades cannot run without it. The old file's `$HermesProfile` (lines 42-44) was a typo for `HERMES_PROFILE` and could never have printed; read, not executed.
**(3) On this host, after the repair.** `python3 wizard-work/spark/snippet.py` → `DIR OK` for all three required paths, `TOOLS_IMPORT_FAILED: ModuleNotFoundError: No module named 'hermes_tools'`, `TOOLS_MISSING: ['web_search', 'web_extract', 'read_file', 'write_file']`, `VERDICT: FAIL`, rc **1** — the honest reading of a bare python3 with no Hermes session, which is exactly the case the item was filed about. `bash wizard-work/spark/verify_hermes_setup.sh` → root `/root/.hermes`, home `/root/.hermes/profiles/volt` derived from `$HERMES_HOME`, four `ok:` lines, `VERDICT: PASS`, rc **0**; `--nope` → rc **2**.
**(4) The cell.** `wizard-work/spark/test_env_verifiers.py` (md5 `b141ac9cfaf99ae798573a746a524865`, 230 lines, **14 cells**) runs both files as subprocesses and imports nothing from either, so no cell can agree with the code by sharing its constants. The tool check is graded against a stub `hermes_tools` on `PYTHONPATH` in four states — all four callable, three of four, four with `web_extract = None`, and no module at all — and the directory check against synthetic installations built under `tempfile`. The two cells the item names by hand: `test_snippet_one_tool_missing_reds_and_names_it` (rc 1, `TOOLS_MISSING: ['write_file']`) and `test_snippet_bare_interpreter_reds_on_tools`; plus `test_no_pinned_root_paths_in_either_file`, which is the class itself. `wizard-work/spark/CHECK-env-verifiers` (md5 `11e6754ea7ec941eb15558ee6f26e3e5`, 34 lines) wraps it with the repo's rc contract → `Ran 14 tests … OK`, `CHECK: both environment verifiers measure what they claim.`, rc **0**.
**(5) Falsification: five trees, one run each, control in the same battery.**
| tree | what was swapped in | rc | cells red |
|---|---|---|---|
| control | the repaired pair | **0** | 0 (14/14 OK) |
| m1 | the pre-repair `snippet.py` (md5 `4bb7d1f5…`) | **1** | 9 |
| m2 | the pre-repair `verify_hermes_setup.sh` (md5 `e448130e…`) | **1** | 6 |
| m3 | `available[name] = callable(candidate)` → `= True` | **1** | 3 |
| m4 | the `$HERE/snippet.py` leg deleted from the shell loop | **1** | 1 |
m1’s nine: `test_no_pinned_root_paths_in_either_file`, `test_snippet_bare_interpreter_reds_on_tools`, `test_snippet_full_stub_is_green`, `test_snippet_honours_hermes_home_env`, `test_snippet_missing_scratch_dir_reds_and_names_path`, `test_snippet_noncallable_tool_is_unavailable`, `test_snippet_one_tool_missing_reds_and_names_it`, `test_snippet_require_absent_tool_reds`, `test_snippet_usage_error_is_2`. m2’s six: `test_no_pinned_root_paths_in_either_file`, `test_shell_complete_tree_is_green`, `test_shell_lone_copy_reds_on_its_sibling`, `test_shell_missing_profile_home_reds_and_names_path`, `test_shell_same_verdict_from_outside_copy`, `test_shell_usage_error_is_2`. m3’s three are the tool-missing, non-callable and `--require` cells — the literal-lie restored in one line. m4’s one is the lone-copy cell: with that leg gone a script shipped without its snippet reads PASS again.
**Not claimed:** that `hermes_tools` is unimportable inside a real Hermes session — it is unimportable from a plain `python3` on this host, measured, and neither verifier was run from inside the agent runtime; that `HERMES_HOME` always names a profile home — that is this install’s shape (`/root/.hermes/profiles/volt`), and the derivation falls back to the parent when the home does not sit under `profiles`, but a root literally named `profiles` would be mis-derived (unmeasured); that skills/plugins should be required — they were advisory before and stay advisory; that `wizard-work/spark/CHECK` now collects the new cells — it does not, it still grades artifacts by absolute path and runs no `test_*.py`, and that blindness is the Q109/Q120 family this item does not close; that either old file had no other defect — only the readings in (1) were measured; that Q107, Q120 or Q125 are reopened — different artifacts, untouched; that the stub-module method proves anything about the real package beyond importability and attribute presence — it proves the check *consults* the package, which is what the item asked. Scratch trees for (5): `/root/.hermes/profiles/volt/cache/scratch/q109-2620292/{control,m1,m2,m3,m4}`, one `.out` beside each. Files edited for this item: `wizard-work/spark/snippet.py`, `wizard-work/spark/verify_hermes_setup.sh`, `wizard-work/spark/test_env_verifiers.py` (new), `wizard-work/spark/CHECK-env-verifiers` (new), `WORK-QUEUE.md` (this record, appended), `docs/queue-index.md` (re-rendered with the committed `tools/queue_index.py --write` in this tree). Nothing from `inbox/` is quoted and no other lane’s artifact was edited. **Report:** `/root/.hermes/wizard-outputs/volt/2026-10-29-q109.md`.
+46 -19
View File
@@ -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 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; 36 further ids are opened by a record and own no heading at all, and they have rows and extents here -- 89 record blocks in all, 53 of them a later closure or correction for an id already opened. The same **36 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 29 times. **What it exposes.** The queue is two-tier. 97 sections carry 86 ids because 11 ids own a second, continuation section; 36 further ids are opened by a record and own no heading at all, and they have rows and extents here -- 90 record blocks in all, 54 of them a later closure or correction for an id already opened. The same **36 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 29 times.
| # | id | kind | lines | bytes | role | title | first `Status:` | | # | id | kind | lines | bytes | role | title | first `Status:` |
|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|
@@ -193,7 +193,8 @@
| 183 | Q125 | heading | 13358-13380 | 3778 | opening | the spark lane holds eleven line/word/char counters, and the newest one replaced its predecessor's capabilities without retiring its verifier *(spark lane; opened 2026-10-28 by the pipe, at … | open and unclaimed. | | 183 | Q125 | heading | 13358-13380 | 3778 | opening | the spark lane holds eleven line/word/char counters, and the newest one replaced its predecessor's capabilities without retiring its verifier *(spark lane; opened 2026-10-28 by the pipe, at … | open and unclaimed. |
| 184 | Q125 | record | 13381-13399 | 6812 | cont: DONE | **Q125 — DONE (volt, 2026-10-29, at `46b6282`; the repair landed under the spark lane's banner at `a979e63` and `46b6282`, and this record is the closure the queue was missing — the item still read … | -- | | 184 | Q125 | record | 13381-13399 | 6812 | cont: DONE | **Q125 — DONE (volt, 2026-10-29, at `46b6282`; the repair landed under the spark lane's banner at `a979e63` and `46b6282`, and this record is the closure the queue was missing — the item still read … | -- |
| 185 | Q120 | record | 13400-13401 | 3580 | cont: DONE | **Q120 — DONE (volt, 2026-10-29; this record and `docs/queue-index.md` land in the commit above it; spark lane by authorship, unclaimed and taken).** The health check now fails. Before: `python3 … | -- | | 185 | Q120 | record | 13400-13401 | 3580 | cont: DONE | **Q120 — DONE (volt, 2026-10-29; this record and `docs/queue-index.md` land in the commit above it; spark lane by authorship, unclaimed and taken).** The health check now fails. Before: `python3 … | -- |
| 186 | Q121 | record | 13402-13402 | 5320 | cont: DONE | **Q121 — DONE (volt, 2026-10-29; zeph lane by authorship, unclaimed and taken; this record and `docs/queue-index.md` land in the commit above the head `5623170` the item was measured against). The … | -- | | 186 | Q121 | record | 13402-13403 | 5321 | cont: DONE | **Q121 — DONE (volt, 2026-10-29; zeph lane by authorship, unclaimed and taken; this record and `docs/queue-index.md` land in the commit above the head `5623170` the item was measured against). The … | -- |
| 187 | Q109 | record | 13404-13430 | 7752 | cont: DONE | **Q109 — DONE (volt, 2026-10-29; spark lane by authorship, unclaimed and taken). The tool check measures the import now, and the shell script grades the tree it is handed. Both named readings … | -- |
## Ids with no `## ` heading ## Ids with no `## ` heading
@@ -218,10 +219,10 @@ Opened as records inside another section. `line` is the record's own opening lin
| Q103 | 13010 | 13010 | 18 | Q102 | BULLET+BOLD | BOLD-LEAD | | Q103 | 13010 | 13010 | 18 | Q102 | BULLET+BOLD | BOLD-LEAD |
| Q104 | 13103 | 13103 | 18 | Q101 | BULLET+BOLD | BOLD-LEAD | | Q104 | 13103 | 13103 | 18 | Q101 | BULLET+BOLD | BOLD-LEAD |
| Q105 | 13135 | 13135 | 15 | Q104 | BULLET+BOLD | BOLD-LEAD | | Q105 | 13135 | 13135 | 15 | Q104 | BULLET+BOLD | BOLD-LEAD |
| Q106 | 13171 | 13171 | 11 | Q105 | BULLET+BOLD | BOLD-LEAD | | Q106 | 13171 | 13171 | 12 | Q105 | BULLET+BOLD | BOLD-LEAD |
| Q107 | 13189 | 13189 | 15 | Q106 | BULLET+BOLD | BOLD-LEAD | | Q107 | 13189 | 13189 | 16 | Q106 | BULLET+BOLD | BOLD-LEAD |
| Q108 | 13267 | 13263 | 3 | Q106 | BULLET+BOLD | BOLD-LEAD | | Q108 | 13267 | 13263 | 3 | Q106 | BULLET+BOLD | BOLD-LEAD |
| Q109 | 13270 | 13270 | 9 | Q104 | BULLET+BOLD | BOLD-LEAD | | Q109 | 13270 | 13270 | 11 | Q104 | BULLET+BOLD | BOLD-LEAD |
| Q110 | 13272 | 13272 | 3 | Q109 | BULLET+BOLD | BOLD-LEAD | | Q110 | 13272 | 13272 | 3 | Q109 | BULLET+BOLD | BOLD-LEAD |
| Q111 | 13274 | 13274 | 7 | Q110 | BULLET+BOLD | BOLD-LEAD | | Q111 | 13274 | 13274 | 7 | Q110 | BULLET+BOLD | BOLD-LEAD |
| Q112 | 13276 | 13276 | 5 | Q111 | BULLET+BOLD | BOLD-LEAD | | Q112 | 13276 | 13276 | 5 | Q111 | BULLET+BOLD | BOLD-LEAD |
@@ -232,7 +233,7 @@ Opened as records inside another section. `line` is the record's own opening lin
| Q117 | 13290 | 13288 | 3 | Q96 | BULLET+BOLD | BOLD-LEAD | | Q117 | 13290 | 13288 | 3 | Q96 | BULLET+BOLD | BOLD-LEAD |
| Q118 | 13300 | 13300 | 2 | Q111 | BULLET+BOLD | BOLD-LEAD | | Q118 | 13300 | 13300 | 2 | Q111 | BULLET+BOLD | BOLD-LEAD |
| Q119 | 13305 | 13305 | 2 | Q118 | BULLET+BOLD | BOLD-LEAD | | Q119 | 13305 | 13305 | 2 | Q118 | BULLET+BOLD | BOLD-LEAD |
| Q120 | 13308 | 13308 | 7 | Q119 | BULLET+BOLD | BOLD-LEAD | | Q120 | 13308 | 13308 | 9 | Q119 | BULLET+BOLD | BOLD-LEAD |
| Q121 | 13342 | 13342 | 2 | Q107 | BULLET+BOLD | BOLD-LEAD | | Q121 | 13342 | 13342 | 2 | Q107 | BULLET+BOLD | BOLD-LEAD |
| Q122 | 13344 | 13344 | 4 | Q121 | BULLET+BOLD | BOLD-LEAD | | Q122 | 13344 | 13344 | 4 | Q121 | BULLET+BOLD | BOLD-LEAD |
| Q123 | 13346 | 13346 | 4 | Q122 | BULLET+BOLD | BOLD-LEAD | | Q123 | 13346 | 13346 | 4 | Q122 | BULLET+BOLD | BOLD-LEAD |
@@ -1393,17 +1394,30 @@ The block below is the parse this document renders, byte for byte. `--check` com
"title": "**Q120 \u2014 DONE (volt, 2026-10-29; this record and `docs/queue-index.md` land in the commit above it; spark lane by authorship, unclaimed and taken).** The health check now fails. Before: `python3 \u2026" "title": "**Q120 \u2014 DONE (volt, 2026-10-29; this record and `docs/queue-index.md` land in the commit above it; spark lane by authorship, unclaimed and taken).** The health check now fails. Before: `python3 \u2026"
}, },
{ {
"bytes": 5320, "bytes": 5321,
"end_line": 13402, "end_line": 13403,
"id": "Q121", "id": "Q121",
"kind": "record", "kind": "record",
"line": 13402, "line": 13402,
"lines": 1, "lines": 2,
"role": "continuation", "role": "continuation",
"role_detail": "DONE", "role_detail": "DONE",
"status": null, "status": null,
"status_line": null, "status_line": null,
"title": "**Q121 \u2014 DONE (volt, 2026-10-29; zeph lane by authorship, unclaimed and taken; this record and `docs/queue-index.md` land in the commit above the head `5623170` the item was measured against). The \u2026" "title": "**Q121 \u2014 DONE (volt, 2026-10-29; zeph lane by authorship, unclaimed and taken; this record and `docs/queue-index.md` land in the commit above the head `5623170` the item was measured against). The \u2026"
},
{
"bytes": 7752,
"end_line": 13430,
"id": "Q109",
"kind": "record",
"line": 13404,
"lines": 27,
"role": "continuation",
"role_detail": "DONE",
"status": null,
"status_line": null,
"title": "**Q109 \u2014 DONE (volt, 2026-10-29; spark lane by authorship, unclaimed and taken). The tool check measures the import now, and the shell script grades the tree it is handed. Both named readings \u2026"
} }
], ],
"ids_multi_section": { "ids_multi_section": {
@@ -3947,17 +3961,30 @@ The block below is the parse this document renders, byte for byte. `--check` com
"title": "**Q120 \u2014 DONE (volt, 2026-10-29; this record and `docs/queue-index.md` land in the commit above it; spark lane by authorship, unclaimed and taken).** The health check now fails. Before: `python3 \u2026" "title": "**Q120 \u2014 DONE (volt, 2026-10-29; this record and `docs/queue-index.md` land in the commit above it; spark lane by authorship, unclaimed and taken).** The health check now fails. Before: `python3 \u2026"
}, },
{ {
"bytes": 5320, "bytes": 5321,
"end_line": 13402, "end_line": 13403,
"id": "Q121", "id": "Q121",
"kind": "record", "kind": "record",
"line": 13402, "line": 13402,
"lines": 1, "lines": 2,
"role": "continuation", "role": "continuation",
"role_detail": "DONE", "role_detail": "DONE",
"status": null, "status": null,
"status_line": null, "status_line": null,
"title": "**Q121 \u2014 DONE (volt, 2026-10-29; zeph lane by authorship, unclaimed and taken; this record and `docs/queue-index.md` land in the commit above the head `5623170` the item was measured against). The \u2026" "title": "**Q121 \u2014 DONE (volt, 2026-10-29; zeph lane by authorship, unclaimed and taken; this record and `docs/queue-index.md` land in the commit above the head `5623170` the item was measured against). The \u2026"
},
{
"bytes": 7752,
"end_line": 13430,
"id": "Q109",
"kind": "record",
"line": 13404,
"lines": 27,
"role": "continuation",
"role_detail": "DONE",
"status": null,
"status_line": null,
"title": "**Q109 \u2014 DONE (volt, 2026-10-29; spark lane by authorship, unclaimed and taken). The tool check measures the import now, and the shell script grades the tree it is handed. Both named readings \u2026"
} }
], ],
"nested": [ "nested": [
@@ -4119,7 +4146,7 @@ The block below is the parse this document renders, byte for byte. `--check` com
"form": "BOLD-LEAD", "form": "BOLD-LEAD",
"id": "Q106", "id": "Q106",
"line": 13171, "line": 13171,
"refs": 11, "refs": 12,
"section": "Q105", "section": "Q105",
"shape": "BULLET+BOLD" "shape": "BULLET+BOLD"
}, },
@@ -4128,7 +4155,7 @@ The block below is the parse this document renders, byte for byte. `--check` com
"form": "BOLD-LEAD", "form": "BOLD-LEAD",
"id": "Q107", "id": "Q107",
"line": 13189, "line": 13189,
"refs": 15, "refs": 16,
"section": "Q106", "section": "Q106",
"shape": "BULLET+BOLD" "shape": "BULLET+BOLD"
}, },
@@ -4146,7 +4173,7 @@ The block below is the parse this document renders, byte for byte. `--check` com
"form": "BOLD-LEAD", "form": "BOLD-LEAD",
"id": "Q109", "id": "Q109",
"line": 13270, "line": 13270,
"refs": 9, "refs": 11,
"section": "Q104", "section": "Q104",
"shape": "BULLET+BOLD" "shape": "BULLET+BOLD"
}, },
@@ -4245,7 +4272,7 @@ The block below is the parse this document renders, byte for byte. `--check` com
"form": "BOLD-LEAD", "form": "BOLD-LEAD",
"id": "Q120", "id": "Q120",
"line": 13308, "line": 13308,
"refs": 7, "refs": 9,
"section": "Q119", "section": "Q119",
"shape": "BULLET+BOLD" "shape": "BULLET+BOLD"
}, },
@@ -4291,9 +4318,9 @@ The block below is the parse this document renders, byte for byte. `--check` com
"end_line": 8, "end_line": 8,
"line": 1 "line": 1
}, },
"queue_bytes": 2085655, "queue_bytes": 2093408,
"queue_lines": 13402, "queue_lines": 13430,
"queue_sha256": "d37d9c08e090600cc8817ff6b56c6d5a3dcc2f0490957012d24524269a8d273f", "queue_sha256": "bc4a5fbac8d035fa2845e50f002a8b712d4f0d5d3d25a4b6aeea0efb5c9c969c",
"sections": [ "sections": [
{ {
"bytes": 3139, "bytes": 3139,
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
# CHECK-env-verifiers: grade the lane's two environment verifiers.
#
# Q109. The lane's own CHECK grades artifacts under an absolute /root path and
# runs none of the lane's test_*.py (that blindness is a separate item), so this
# file gives these two verifiers a gate of their own. The cell
# (test_env_verifiers.py) invokes both verifiers as subprocesses with stub
# modules on PYTHONPATH and synthetic Hermes trees, so it grades the tree it
# sits in -- the paths are resolved from the cell's own location.
#
# Exit codes follow the repo's tools/ contract:
# 0 = both verifiers measure what they claim
# 1 = a verifier regression (a cell failed)
# 2 = could not grade (no python3, or the cell does not import/collect)
set -e
set -o pipefail
cd "$(dirname "$(readlink -f "$0")")"
if ! command -v python3 >/dev/null 2>&1; then
echo "CHECK: python3 not on PATH -- cannot grade." >&2
exit 2
fi
set +e
python3 -m unittest -q test_env_verifiers >/tmp/.q109-check.out 2>&1
rc=$?
set -e
tail -3 /tmp/.q109-check.out
case $rc in
0) echo "CHECK: both environment verifiers measure what they claim."; exit 0 ;;
1) echo "CHECK: verifier contract broken -- a cell failed." >&2; exit 1 ;;
*) echo "CHECK: unittest exited $rc -- collection error, could not grade." >&2; exit 2 ;;
esac
Regular → Executable
+147 -28
View File
@@ -1,37 +1,156 @@
#!/usr/bin/env python3 #!/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/<profile>),
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 argparse
import sys import importlib
import os import os
import sys
from pathlib import Path
def check_environment(): TOOLS = ["web_search", "web_extract", "read_file", "write_file"]
"""Verify critical Hermes directories exist."""
dirs = [
"/root/.hermes", def profile_name(override=None):
"/root/.hermes/profiles", """The profile named in the reading: --profile, else $HERMES_PROFILE, else spark."""
"/root/.hermes/skills", if override:
"/root/.hermes/plugins", 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.""" def optional_dirs(root):
tools = ["web_search", "web_extract", "read_file", "write_file"] """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 = {} available = {}
for tool in tools: for name in names:
# These are Hermes tools available via hermes_tools try:
available[tool] = True candidate = getattr(module, name, None)
return available 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/<profile>)")
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__": if __name__ == "__main__":
missing_dirs = check_environment() sys.exit(main())
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)
+230
View File
@@ -0,0 +1,230 @@
#!/usr/bin/env python3
"""Q109 cells: the lane's two environment verifiers, graded as subprocesses.
Both files used to print success without measuring anything --
`wizard-work/spark/snippet.py` filled `available[tool] = True` in a loop over a
hard-coded list (so a bare python3 with no Hermes session read "verified
successfully", rc 0), and `wizard-work/spark/verify_hermes_setup.sh` tested
three absolute paths under a pinned /root and read nothing relative to itself
(so it printed three ticks and "All critical paths verified", rc 0, on any
tree). Those are the two readings these cells kill.
Every cell runs the shipped file by path, resolved from __file__, and imports
nothing from it: no cell can agree with the code by sharing its constants. The
tool check is graded with a stub `hermes_tools` on PYTHONPATH -- full, reduced,
and non-callable -- and the directory check is graded against synthetic trees
built here, so a green reading is a statement about the arguments and not about
this host.
Run: python3 test_env_verifiers.py (or: python3 -m unittest -q ...)
"""
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
LANE = Path(__file__).resolve().parent
SNIPPET = LANE / "snippet.py"
SHELL = LANE / "verify_hermes_setup.sh"
TOOLS = ["web_search", "web_extract", "read_file", "write_file"]
STUB_HEADER = '''"""Stub hermes_tools written by test_env_verifiers.py."""\n'''
def stub_source(names):
"""A stand-in hermes_tools exposing `names` as callables."""
lines = [STUB_HEADER]
for name in names:
lines.append("def %s(*args, **kwargs):\n return None\n" % name)
return "".join(lines)
class EnvVerifiers(unittest.TestCase):
def setUp(self):
self.tmp = Path(tempfile.mkdtemp(prefix="q109-cell-", dir=tempfile.gettempdir()))
def tearDown(self):
shutil.rmtree(self.tmp, ignore_errors=True)
# ---- fixtures -------------------------------------------------------
def make_tree(self, name="tree", scratch=True, home=True):
"""A synthetic Hermes installation: ROOT/profiles/<profile>/cache/scratch."""
root = self.tmp / name
root.mkdir(parents=True, exist_ok=True)
(root / "skills").mkdir(exist_ok=True)
(root / "plugins").mkdir(exist_ok=True)
profile_home = root / "profiles" / "lane"
if home:
profile_home.mkdir(parents=True, exist_ok=True)
if scratch and home:
(profile_home / "cache" / "scratch").mkdir(parents=True, exist_ok=True)
return root, profile_home
def make_stub(self, names, label="stub"):
"""A PYTHONPATH directory holding a hermes_tools stub, or none at all."""
stub_dir = self.tmp / label
stub_dir.mkdir(parents=True, exist_ok=True)
if names is not None:
(stub_dir / "hermes_tools.py").write_text(stub_source(names), encoding="utf-8")
return stub_dir
def env_for(self, stub_dir, **extra):
env = {k: v for k, v in os.environ.items()
if k not in ("HERMES_HOME", "HERMES_ROOT", "HERMES_PROFILE", "PYTHONPATH")}
env["PYTHONPATH"] = str(stub_dir)
env.update(extra)
return env
def run_snippet(self, stub_dir, *args, **extra_env):
return subprocess.run(
[sys.executable, str(SNIPPET)] + [str(a) for a in args],
capture_output=True, text=True, env=self.env_for(stub_dir, **extra_env),
)
def run_shell(self, script, *args, **extra_env):
stub_dir = self.make_stub(None, label="no-stub")
return subprocess.run(
["bash", str(script)] + [str(a) for a in args],
capture_output=True, text=True, env=self.env_for(stub_dir, **extra_env),
)
def lines(self, result, tag):
return [ln[len(tag) + 2:] for ln in result.stdout.splitlines() if ln.startswith(tag + ": ")]
def named(self, result, path):
return any(path in ln for ln in result.stdout.splitlines())
# ---- snippet.py: the tool check measures ----------------------------
def test_snippet_bare_interpreter_reds_on_tools(self):
"""The defect itself: no hermes_tools at all must not read as verified."""
root, home = self.make_tree()
result = self.run_snippet(self.make_stub(None, "empty"), "--home", home)
self.assertEqual(result.returncode, 1, result.stdout)
self.assertIn("TOOLS_IMPORT_FAILED:", result.stdout)
self.assertEqual(self.lines(result, "TOOLS_MISSING"), [str(TOOLS)])
self.assertIn("VERDICT: FAIL", result.stdout)
def test_snippet_full_stub_is_green(self):
"""Positive control: every tool callable on a complete tree reads PASS."""
root, home = self.make_tree("green")
result = self.run_snippet(self.make_stub(TOOLS), "--home", home)
self.assertEqual(result.returncode, 0, result.stdout)
self.assertEqual(self.lines(result, "TOOLS_AVAILABLE"), [str(sorted(TOOLS))])
self.assertEqual(self.lines(result, "TOOLS_MISSING"), ["[]"])
self.assertIn("VERDICT: PASS", result.stdout)
self.assertEqual(result.stderr, "")
def test_snippet_one_tool_missing_reds_and_names_it(self):
"""The negative control the item asks for: one genuinely absent tool."""
root, home = self.make_tree("partial")
stub = self.make_stub([t for t in TOOLS if t != "write_file"], "partial-stub")
result = self.run_snippet(stub, "--home", home)
self.assertEqual(result.returncode, 1, result.stdout)
self.assertEqual(self.lines(result, "TOOLS_MISSING"), ["['write_file']"])
def test_snippet_noncallable_tool_is_unavailable(self):
"""A name that resolves to None is not a tool that works."""
root, home = self.make_tree("noncallable")
stub_dir = self.make_stub(TOOLS, "noncallable-stub")
(stub_dir / "hermes_tools.py").write_text(
stub_source(TOOLS) + "web_extract = None\n", encoding="utf-8")
result = self.run_snippet(stub_dir, "--home", home)
self.assertEqual(result.returncode, 1, result.stdout)
self.assertEqual(self.lines(result, "TOOLS_MISSING"), ["['web_extract']"])
def test_snippet_missing_scratch_dir_reds_and_names_path(self):
root, home = self.make_tree("noscratch", scratch=False)
result = self.run_snippet(self.make_stub(TOOLS), "--home", home)
self.assertEqual(result.returncode, 1, result.stdout)
self.assertTrue(self.named(result, str(home / "cache" / "scratch")), result.stdout)
self.assertIn("DIR MISSING: %s" % (home / "cache" / "scratch"), result.stdout)
def test_snippet_require_absent_tool_reds(self):
root, home = self.make_tree("require")
result = self.run_snippet(self.make_stub(TOOLS), "--home", home,
"--require", "definitely_not_a_tool")
self.assertEqual(result.returncode, 1, result.stdout)
self.assertEqual(self.lines(result, "TOOLS_MISSING"), ["['definitely_not_a_tool']"])
def test_snippet_honours_hermes_home_env(self):
"""Resolution is env-first: HERMES_HOME alone must redirect the reading."""
root, home = self.make_tree("envhome")
result = self.run_snippet(self.make_stub(TOOLS), HERMES_HOME=str(home))
self.assertEqual(result.returncode, 0, result.stdout)
self.assertEqual(self.lines(result, "HERMES_HOME"), [str(home)])
self.assertEqual(self.lines(result, "HERMES_ROOT"), [str(root)])
def test_snippet_usage_error_is_2(self):
root, home = self.make_tree("usage")
result = self.run_snippet(self.make_stub(TOOLS), "--nope")
self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
# ---- verify_hermes_setup.sh: reads its own inputs -------------------
def copy_pair(self, name="copy"):
"""The two files as they ship together, outside the repo."""
target = self.tmp / name
target.mkdir(parents=True, exist_ok=True)
shutil.copy2(SNIPPET, target / SNIPPET.name)
shutil.copy2(SHELL, target / SHELL.name)
return target
def test_shell_complete_tree_is_green(self):
root, home = self.make_tree("shell-green")
script = self.copy_pair() / SHELL.name
result = self.run_shell(script, "--home", home)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertTrue(self.named(result, str(home)), result.stdout)
self.assertIn("VERDICT: PASS", result.stdout)
def test_shell_missing_profile_home_reds_and_names_path(self):
root, home = self.make_tree("shell-red", home=False)
result = self.run_shell(self.copy_pair("copy-red") / SHELL.name, "--home", home)
self.assertEqual(result.returncode, 1, result.stdout)
self.assertIn("MISSING: %s" % home, result.stdout)
self.assertIn("VERDICT: FAIL (2 required path(s) missing)", result.stdout)
def test_shell_lone_copy_reds_on_its_sibling(self):
"""The self-location leg: a script with no snippet.py beside it has nothing to report."""
lone = self.tmp / "lone"
lone.mkdir(parents=True, exist_ok=True)
shutil.copy2(SHELL, lone / SHELL.name)
root, home = self.make_tree("shell-lone")
result = self.run_shell(lone / SHELL.name, "--home", home)
self.assertEqual(result.returncode, 1, result.stdout)
self.assertIn("MISSING: %s" % (lone / "snippet.py"), result.stdout)
def test_shell_same_verdict_from_outside_copy(self):
"""In place and outside the repo, on the same inputs: the same reading."""
root, home = self.make_tree("shell-same")
in_place = self.run_shell(SHELL, "--home", home)
outside = self.run_shell(self.copy_pair("copy-same") / SHELL.name, "--home", home)
self.assertEqual(in_place.returncode, outside.returncode)
self.assertEqual(in_place.returncode, 0, in_place.stdout + outside.stdout)
# Every line about the graded tree, not about where the script lives.
about_tree = lambda out: [ln for ln in out.splitlines() if str(root) in ln]
self.assertEqual(about_tree(in_place.stdout), about_tree(outside.stdout))
self.assertIn("VERDICT: PASS", outside.stdout)
def test_shell_usage_error_is_2(self):
result = self.run_shell(SHELL, "--nope")
self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
# ---- the class both files were filed under -------------------------
def test_no_pinned_root_paths_in_either_file(self):
"""Q109's class: neither file may grade a path pinned to this host."""
for path in (SNIPPET, SHELL):
blob = path.read_text(encoding="utf-8")
self.assertNotIn("/root", blob, "%s still pins a /root path" % path.name)
if __name__ == "__main__":
unittest.main(verbosity=2)
+130 -37
View File
@@ -1,48 +1,141 @@
#!/bin/bash #!/bin/bash
# Verification script for Hermes Agent profile setup # Verify the directories a Hermes Agent profile ships into -- on the tree the
set -e # 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
<script dir>/snippet.py the snippet that ships beside this script
Defaults: --home $HERMES_HOME or ~/.hermes/profiles/$HERMES_PROFILE;
--root derived from the profile home; --profile $HERMES_PROFILE or spark.
Exit 0 all present, 1 a required path is missing, 2 usage error.
USAGE
}
while [ "$#" -gt 0 ]; do
case "$1" in
--home)
if [ "$#" -lt 2 ]; then echo "--home needs a value" >&2; exit 2; fi
HOME_ARG=$2
shift 2
;;
--root)
if [ "$#" -lt 2 ]; then echo "--root needs a value" >&2; exit 2; fi
ROOT_ARG=$2
shift 2
;;
--profile)
if [ "$#" -lt 2 ]; then echo "--profile needs a value" >&2; exit 2; fi
PROFILE=$2
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done
if [ -z "$HOME_ARG" ]; then
if [ -n "${HERMES_HOME:-}" ]; then
HOME_ARG=$HERMES_HOME
else
HOME_ARG=$HOME/.hermes/profiles/$PROFILE
fi
fi
if [ -z "$ROOT_ARG" ]; then
if [ -n "${HERMES_ROOT:-}" ]; then
ROOT_ARG=$HERMES_ROOT
else
parent=$(dirname -- "$HOME_ARG")
if [ "$(basename -- "$parent")" = "profiles" ]; then
ROOT_ARG=$(dirname -- "$parent")
else
ROOT_ARG=$parent
fi
fi
fi
echo "=== Hermes Agent Profile Verification ===" echo "=== Hermes Agent Profile Verification ==="
echo "script dir: $HERE"
echo "hermes root: $ROOT_ARG"
echo "profile home: $HOME_ARG"
echo "profile: $PROFILE"
# Check profile directory missing=0
if [ -d "/root/.hermes/profiles/spark" ]; then for path in "$ROOT_ARG" "$HOME_ARG" "$HOME_ARG/cache/scratch" "$HERE/snippet.py"; do
echo "✓ Profile directory exists: /root/.hermes/profiles/spark" if [ -e "$path" ]; then
echo "ok: $path"
else else
echo "✗ Profile directory missing" echo "MISSING: $path"
exit 1 missing=$((missing + 1))
fi
done
# Advisory: this lane does not depend on these, so a miss is a note.
for path in "$ROOT_ARG/skills" "$ROOT_ARG/plugins"; do
if [ -d "$path" ]; then
echo "ok (optional): $path"
else
echo "note: optional path absent: $path"
fi
done
if command -v hermes >/dev/null 2>&1; then
echo "note: hermes CLI on PATH: $(command -v hermes)"
else
echo "note: hermes CLI not on PATH"
fi fi
# Check skills directory if command -v python3 >/dev/null 2>&1; then
if [ -d "/root/.hermes/skills" ]; then echo "note: python3: $(python3 --version 2>&1)"
echo "✓ Skills directory exists: /root/.hermes/skills"
else else
echo "Note: Skills directory not found (may be normal for this session)" echo "MISSING: python3 (snippet.py cannot be run)"
fi missing=$((missing + 1))
# Check scratch directory
if [ -d "/root/.hermes/profiles/spark/cache/scratch" ]; then
echo "✓ Scratch directory exists"
else
echo "✗ Scratch directory missing"
exit 1
fi
# Check current working directory
CWD=$(pwd)
echo "Current working directory: $CWD"
# Check for Hermes tools
if command -v hermes &> /dev/null; then
echo "✓ hermes CLI available"
else
echo "Note: hermes CLI not in PATH"
fi
# Verify key environment variables
if [ -n "$HermesProfile" ]; then
echo "HermesProfile: $HermesProfile"
fi fi
echo "" echo ""
echo "=== Verification Complete ===" if [ "$missing" -gt 0 ]; then
echo "All critical paths verified." echo "VERDICT: FAIL ($missing required path(s) missing)"
exit 1
fi
echo "VERDICT: PASS"
exit 0