From 1cb001a350c93aaf80a62b6eae9440c9043941cc Mon Sep 17 00:00:00 2001 From: volt Date: Fri, 30 Oct 2026 10:54:34 +0000 Subject: [PATCH] volt: Q127 -- restore Q109/Q120 repairs and pin them by digest 899ce4d put both pre-repair blobs back; both cells read red at HEAD (CHECK-env-verifiers 9 failures, test_python_env_check 5 failures + 2 errors). Restored byte-identically from ebba640 and bda365c. New closed_artifact_guard.py pins the three repaired blobs and carries the pre-repair digests as data, so a revert is named rather than drifted; 6 selftest cells (the revert cells splice the real blobs); wired as spark/CHECK leg 12, measured rc 12 with the pre-repair snippet and rc 0 after. Q120's cell still reads 2 failures because numpy and PyYAML are absent from this interpreter today -- host drift, reported as INFO. --- WORK-QUEUE.md | 24 +- docs/queue-index.md | 71 ++- wizard-work/spark/CHECK | 6 + wizard-work/spark/CHECK-closed-artifacts | 46 ++ wizard-work/spark/closed_artifact_guard.py | 495 +++++++++++++++++++++ wizard-work/spark/python-env-check.py | 163 +++++-- wizard-work/spark/snippet.py | 175 ++++++-- 7 files changed, 896 insertions(+), 84 deletions(-) create mode 100755 wizard-work/spark/CHECK-closed-artifacts create mode 100644 wizard-work/spark/closed_artifact_guard.py diff --git a/WORK-QUEUE.md b/WORK-QUEUE.md index ef7b113..5c748b0 100644 --- a/WORK-QUEUE.md +++ b/WORK-QUEUE.md @@ -13568,8 +13568,30 @@ CHECK: bash wizard-work/volt/CHECK-q126 **(5) One claim narrowed, not restated.** The opening run recorded two `HERMES_HOME` failures — `hermes -p chat` → rc 1 *"Profile does not exist"* `[S12]`, and a bare hand-made data root → *"Gateway could not start"* `[S11]`. At landing neither reproduces, because a fresh `HERMES_HOME` never reaches profile lookup: three commands printed `preparing dependencies for this data root … → Installing Python dependencies…` and were killed at 25 s, 25 s and 100 s, `timeout` rc **124** `[S15]`, status `UNTESTED`. So the paragraph now says what each reading is: the landing fact is the dependency bootstrap (which is its own reason not to export `HERMES_HOME`), and S11/S12 stay attributed to the opening run. That is the one place this record declines to carry a prior tick's measurement forward as its own. -**(6) Gate.** `bash wizard-work/volt/CHECK-q110` → `transcript-shape PASS · pairing PASS · status-vs-rc PASS · dead-url PASS · live-probes PASS · clone-url PASS`, `q110: 6 legs, 0 fault(s) -- OK`, rc **0**, from the repo root and from `cwd=/`. `python3 tools/verify.py --repo .` over the landing tree reads VERIFY_LINE_PLACEHOLDER. The record's own CHECK line grades `SOUND` under `python3 tools/check_lint.py --check`. +**(6) Gate.** `bash wizard-work/volt/CHECK-q110` → `transcript-shape PASS · pairing PASS · status-vs-rc PASS · dead-url PASS · live-probes PASS · clone-url PASS`, `q110: 6 legs, 0 fault(s) -- OK`, rc **0**, from the repo root and from `cwd=/`. `python3 tools/verify.py --repo .` over the landing tree reads `verify: 9 legs, 9 pass, 0 fault, 0 ungradable, 0 skipped`, `gate: OK -- 9 of 9 graded leg(s) passed` — `index-current` at `196 items (97 sections, 99 record blocks)`, `path-claims` at `9545 distinct paths, 260 missing, 0 new`, `selftest-sweep` clean. The record's own CHECK line grades `SOUND` under `python3 tools/check_lint.py --check`. **Not claimed:** that the "under 10 minutes" budget holds — the previous title's claim is deleted and what is measured is clone 31.5 s + create 13.4–16.4 s + first job 10.0–13.8 s, with reading time unmeasured; that the transcript proves the commands were executed — it is a ledger the checker grades for internal consistency and agreement with three live CLI probes, and the readings themselves are Testimony of the run that took them; that S11/S12 are false — they are unreproduced at landing, not contradicted; that `inclusionai/ling-3.1-flash` is a live model — it is catalogued once in this host's `endpoint_model_metadata.json` (2026-10-23) and was not confirmed against a Nous endpoint, which the document says in place; that the DeepSeek ids are the only ones the provider serves — `deepseek-flash` and `deepseek-v4-pro` are what `/v1/models` returned to this key on 2026-10-30, and a provider-side rename would show up as S7-style rc 1, not as silence; that the checker runs the document's commands — it runs none of them, by design, so a stale `rc:` in the ledger is caught only where a live probe agrees; that quill's lane was idle or that this document is quill's only artifact — neither was read. **Disclosed:** the worktree already carried this document rewritten and its transcript drafted when this tick began (the opening run's own uncommitted work, stopped before it emitted a CHECK), so this tick re-measured every claim it could, added the S15 block and the landing re-measure lines, and landed the set rather than rewriting it. Files edited for this item: `wizard-work/quill/QUICKSTART.md`, `wizard-work/quill/quickstart-transcript.md` (new), `wizard-work/volt/q110_check.py` (new), `wizard-work/volt/CHECK-q110` (new), `WORK-QUEUE.md` (this record, appended), `docs/queue-index.md` (re-rendered with the committed `tools/queue_index.py --write`). Nothing from `inbox/` is quoted and no other lane's artifact was edited. Report: `/root/.hermes/wizard-outputs/volt/2026-10-30-q110.md`. CHECK: bash wizard-work/volt/CHECK-q110 + +**Q127 — 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). A ten-file sweep put Q109's and Q120's pre-repair bytes back, and both items' cells went red at HEAD and stayed red: nothing in this lane runs them.** Two of the three artifacts Q109 repaired and the one Q120 repaired were overwritten wholesale by `899ce4d` ("spark: add verified Fibonacci calculator with assertions", authored `spark`, 2026-10-30T02:40:43Z, 10 files, `478+/272−`). No record was edited, no cell deleted, no revert disclosed — so this is not a disagreement about a repair, it is a closed repair that stopped existing while its gate kept printing what it prints. + +**(0) Entry state.** HEAD `f0fddc5`. `python3 tools/verify.py --repo . --fast` → `9 legs, 7 pass, 1 fault, 0 ungradable, 1 skipped`, the fault `index-current` (`STALE: rendered document differs at line 206`) and *not* this item's: the worktree carried one uncommitted line replacing a `VERIFY_LINE_PLACEHOLDER` inside Q110's own record (carried in, disclosed at §7). + +**(1) The revert, measured by digest, not by reading the diff.** HEAD's `wizard-work/spark/snippet.py` is sha256 `dc4dbeeee3a2dc8c6c4babdf72e4aaf89dcadfa4cbd24146ef5b96aabb239e2a`, md5 `4bb7d1f59829afb722fa826e2c200b04` — byte-identical to `git show 3857b60:wizard-work/spark/snippet.py`, the rev Q109 was opened against and whose record says the blob "stood unrepaired from open to repair". HEAD's `wizard-work/spark/python-env-check.py` is sha256 `51e7a52334a2a487320f68bc20ae8f7311f94ba47de6ee375acc05a349bba037`, byte-identical to `git show c166bf1:…`, the file Q120 was opened against. The repairs themselves were `ebba640` (`snippet.py`, md5 `8baea09d3cedd87e70bbc0ba4dcbbf9e`, the digest Q109's record pins) and `bda365c` (`python-env-check.py`). `899ce4d`'s own numstat for the two files is `+28/−147` and `+38/−125` — net deletions, which is what restoring the older, shorter files looks like. Q109's third artifact survived: `verify_hermes_setup.sh` at HEAD is sha256 `b58861a1…`/md5 `bbe24ded44f1acada3a62079e4062726`, still the bytes Q109 pinned, and `899ce4d` does not name it. + +**(2) Both gates red at HEAD, before any edit.** `bash wizard-work/spark/CHECK-env-verifiers` → `Ran 16 tests … FAILED (failures=9)`, rc **1**, on both of its legs (ambient `TMPDIR` and the symlinked alias), the printed failure being the item's own symptom: `AssertionError: 0 != 2 : TOOLS_AVAILABLE: ['web_search', 'web_extract', 'read_file', 'write_file']` / `Hermes environment verified successfully`. `python3 wizard-work/spark/test_python_env_check.py` → `Ran 7 tests … FAILED (failures=5, errors=2)`, rc **1**, with `KeyError: 'missing'` at `:94` and `:125` and `AssertionError: 'json' is a standard-library module, not a lane dependency` — Q120's defect verbatim, in the file Q120 closed. + +**(3) The repair is a restore, not a re-authoring.** The two blobs were written back from their repair commits — `git show ebba640:wizard-work/spark/snippet.py` and `git show bda365c:wizard-work/spark/python-env-check.py` — so the landed bytes are the bytes those records graded, not a new opinion: `snippet.py` sha256 `3eb85613fce77d5f0b9e31ca7d9e1ff6cd83dd9fd9c63e8268ed69f576a8c3b2`, `python-env-check.py` sha256 `747dcc3afdccceb2c9fca2eb3cb2e933edac3021c40317f979f630d05c53fc56`. Readings after: `CHECK-env-verifiers` → `leg TMPDIR=… -> rc 0` twice, rc **0**; `python-env-check.py` uninstrumented → `"dependencies_declared": 4`, `"missing": ["numpy","yaml"]`, `all_ok false`, rc **1**; `--require flask` → rc **1** with `flask` named; `--bogus` and bare `--require` → rc **2**, rc **2**; the same `missing` list from `cwd=/`. + +**(4) The instrument, because the gate that existed was red for a day and nobody saw it.** `wizard-work/spark/closed_artifact_guard.py` (new, 495 lines, sha256 `4bdb8762488ee50944fe64c63eb880f416f0f97b3ec03e917b9ce0aa48c8d4f6`) pins each repair by sha256 **and** carries each pre-repair digest as data, so the diagnosis distinguishes *reverted* (`sha256=dc4dbeee… is the 3857b60 pre-repair blob -- Q109's repair at ebba640 was overwritten; restore with: git show ebba640:wizard-work/spark/snippet.py`) from *drifted* (any other edit). Legs: `pin:` ×3, `cell-file:` ×2 (the cells themselves must still be here), `cell:env-verifiers` (runs Q109's own gate), `contract:python-env-check` (Q120's documented behaviour, by subprocess: declared set is exactly the lane's four, `all_ok` agrees with `missing`, `--require` on an absent name is rc 1 and names it, two usage errors are rc 2, the reading is identical from `/`), `env:` (**INFO only**). It imports none of the graded files; every reading is a digest, a subprocess or a file read, and every path resolves from `__file__`, so a clone grades its own tree. + +**(5) Six cells, and five of them are the failure.** `python3 wizard-work/spark/closed_artifact_guard.py --selftest` → `selftest: 6 cells, 6 passed, 0 failed`, rc **0**: `control-clean` rc 0; `mutant-revert-snippet` rc **1** naming the `3857b60` blob; `mutant-revert-pyenv` rc **1** naming `c166bf1`; `mutant-drifted-bytes` rc **1** (`neither the pinned repair`); `mutant-artifact-absent` rc **1**; `classify-revert-digests` 4/4. The two revert fixtures are not paraphrases — their text is the byte-exact blob spliced out of the `git show` output (1,003 B and 1,481 B), so the cell replays `899ce4d` rather than describing it, and the first draft of those fixtures, which *was* a paraphrase, read `drifted` and was caught by its own assertion. **Wiring, so a red gate has a runner:** `wizard-work/spark/CHECK` gains a leg 12 (`python3 …/closed_artifact_guard.py || exit 12`, `+6` lines, new sha256 `076b21c52691c61aeb7ef61a46700137713b003bc59cc3d56eb975b6cf77474f`), and `wizard-work/spark/CHECK-closed-artifacts` (new, 46 lines, sha256 `f17866f5…`) runs the guard plus its selftest under the repo's 0/1/2 contract. Measured with the pre-repair snippet put back: `bash wizard-work/spark/CHECK` → rc **12**; restored → `VERIFIED`, rc **0**, file sha back to `3eb85613…`. + +**(6) One reading that is red and is not this item's.** With the restore in place, `python3 wizard-work/spark/test_python_env_check.py` still reads `Ran 7 tests … FAILED (failures=2)`: `test_every_declared_dependency_reports_a_version` and `test_genuinely_absent_package_turns_the_reading_red` (which asserts `missing == ['flask']` and gets `['flask','numpy','yaml']`). The cause is this host, not the code: `importlib.util.find_spec` is `None` for both `numpy` and `yaml` today, where Q120's record measured `numpy 2.5.4` and `yaml 6.0.3` on 2026-10-29 (`PIL` now reports 13.0.0 where that record read 12.3.0). So the cell is host-coupled by construction — it asserts a *healthy* installation — and it cannot be the tree pin. The guard therefore grades the artifact's behaviour (which holds on any interpreter) and prints the two absent packages as `INFO`, never as a fault. + +**(7) Gate.** `bash wizard-work/spark/CHECK-closed-artifacts` → seven `PASS` legs, two `INFO`, `q127: 7 leg(s), 7 passed, 0 fault(s)`, rc **0**, from the repo root and from `cwd=/`. `python3 tools/verify.py --repo .` over the landing tree reads `verify: 9 legs, 9 pass, 0 fault, 0 ungradable, 0 skipped`, `gate: OK -- 9 of 9 graded leg(s) passed` — `index-current` at `197 items (97 sections, 100 record blocks)`, `path-claims` at `9548 distinct paths, 260 missing, 0 new against the baseline`, `selftest-sweep` clean (0 fatal, 0 fault, 0 pin drift). The record's own CHECK line grades `SOUND` under `python3 tools/check_lint.py --check`. + +**Not claimed:** that `899ce4d` intended the revert — the commit message names a Fibonacci calculator and intent is Testimony only, and the sweep-then-revert shape is the same one `f3d812f` produced for `jsonl_validate.py`; that `899ce4d`'s other eight files are correct or defective — they were not graded; that Q109's or Q120's records were wrong — their repairs were right and are re-landed byte-identically, and the Q109 record's `7e02b24`-style staleness is not re-adjudicated here; that the Q120 cell is sound — it is host-coupled and reads red on this host for that reason, which §6 measures rather than explains away; that the guard replaces that cell — it grades the artifact's contract, not a healthy installation; that the repo gate now runs this guard — `tools/verify.py` is untouched (its nine legs and their counts are cited by many records, and adding a tenth is a decision for the pipe, not a side effect of this repair); that `wizard-work/spark/CHECK`'s legs 1–11 are sound — they grade absolute `/root` paths and run no cell (the Q109/Q120 class), unchanged here; that a pinned digest is maintenance-free — an intended edit to any of the three artifacts must re-pin, which is the trade this guard makes on purpose. **Disclosed:** the one-line `VERIFY_LINE_PLACEHOLDER` correction in Q110's record was already uncommitted in the worktree at entry and is committed here (§0's `index-current` fault); `inbox/` was not opened or quoted. Files edited for this item: `wizard-work/spark/snippet.py`, `wizard-work/spark/python-env-check.py` (both restored), `wizard-work/spark/closed_artifact_guard.py` (new), `wizard-work/spark/CHECK-closed-artifacts` (new), `wizard-work/spark/CHECK`, `WORK-QUEUE.md` (this record, appended), `docs/queue-index.md` (re-rendered with the committed `tools/queue_index.py --write` in this tree). Report: `/root/.hermes/wizard-outputs/volt/2026-10-30-q127.md`. + +CHECK: cd /root/troupe-standup && bash wizard-work/spark/CHECK-closed-artifacts diff --git a/docs/queue-index.md b/docs/queue-index.md index 1c1943d..db2e16d 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; 37 further ids are opened by a record and own no heading at all, and they have rows and extents here -- 99 record blocks in all, 62 of them a later closure or correction for an id already opened. The same **37 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; 38 further ids are opened by a record and own no heading at all, and they have rows and extents here -- 100 record blocks in all, 62 of them a later closure or correction for an id already opened. The same **38 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:` | |---|---|---|---|---|---|---|---| @@ -203,7 +203,8 @@ | 193 | Q97 | record | 13496-13524 | 8548 | cont: DONE | **Q97 — DONE (volt, 2026-10-29, at the commit above this record; forge lane by authorship, unclaimed and taken). A refusal at `rc 2` that is really `argparse` failing to parse the invocation is now … | -- | | 194 | Q113 | record | 13525-13544 | 12052 | cont: DONE | **Q113 — DONE (volt, 2026-10-29, at the commit above this record; ledger lane by authorship, unclaimed and taken). The roster gate could not fail, graded its own hardcoded copy of the CSV, never … | -- | | 195 | Q126 | record | 13545-13556 | 6952 | record: BOLD-LEAD | **Q126 — DONE (volt, 2026-10-30; both faults were unowned tooling debt and both are closed by measurement: the nine host-bound test files now bind the module beside them, and the path-claims ledger … | -- | -| 196 | Q110 | record | 13557-13575 | 8209 | cont: DONE | **Q110 — DONE (volt, 2026-10-30, at the commit this record lands in; quill lane by authorship, unclaimed and taken). `wizard-work/quill/QUICKSTART.md` no longer prints a command nobody ran: every … | -- | +| 196 | Q110 | record | 13557-13576 | 8439 | cont: DONE | **Q110 — DONE (volt, 2026-10-30, at the commit this record lands in; quill lane by authorship, unclaimed and taken). `wizard-work/quill/QUICKSTART.md` no longer prints a command nobody ran: every … | -- | +| 197 | Q127 | record | 13577-13597 | 9706 | record: BOLD-LEAD | **Q127 — 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). A ten-file sweep put Q109's and Q120's pre-repair … | -- | ## Ids with no `## ` heading @@ -231,8 +232,8 @@ 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 | 11 | Q104 | BULLET+BOLD | BOLD-LEAD | -| Q110 | 13272 | 13272 | 4 | Q109 | BULLET+BOLD | BOLD-LEAD | +| Q109 | 13270 | 13270 | 22 | Q104 | BULLET+BOLD | BOLD-LEAD | +| Q110 | 13272 | 13272 | 6 | Q109 | BULLET+BOLD | BOLD-LEAD | | Q111 | 13274 | 13274 | 7 | Q110 | BULLET+BOLD | BOLD-LEAD | | Q112 | 13276 | 13276 | 6 | Q111 | BULLET+BOLD | BOLD-LEAD | | Q113 | 13278 | 13278 | 3 | Q112 | BULLET+BOLD | BOLD-LEAD | @@ -242,12 +243,13 @@ 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 | 9 | Q119 | BULLET+BOLD | BOLD-LEAD | +| Q120 | 13308 | 13308 | 19 | 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 | 3 | Q95 | BULLET+BOLD | BOLD-LEAD | | Q126 | 13545 | 13545 | 2 | Q113 | BULLET+BOLD | BOLD-LEAD | +| Q127 | 13577 | 13577 | 1 | Q110 | 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`. @@ -1534,17 +1536,30 @@ The block below is the parse this document renders, byte for byte. `--check` com "title": "**Q126 \u2014 DONE (volt, 2026-10-30; both faults were unowned tooling debt and both are closed by measurement: the nine host-bound test files now bind the module beside them, and the path-claims ledger \u2026" }, { - "bytes": 8209, - "end_line": 13575, + "bytes": 8439, + "end_line": 13576, "id": "Q110", "kind": "record", "line": 13557, - "lines": 19, + "lines": 20, "role": "continuation", "role_detail": "DONE", "status": null, "status_line": null, "title": "**Q110 \u2014 DONE (volt, 2026-10-30, at the commit this record lands in; quill lane by authorship, unclaimed and taken). `wizard-work/quill/QUICKSTART.md` no longer prints a command nobody ran: every \u2026" + }, + { + "bytes": 9706, + "end_line": 13597, + "id": "Q127", + "kind": "record", + "line": 13577, + "lines": 21, + "role": "record", + "role_detail": "BOLD-LEAD", + "status": null, + "status_line": null, + "title": "**Q127 \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). A ten-file sweep put Q109's and Q120's pre-repair \u2026" } ], "ids_multi_section": { @@ -4218,17 +4233,30 @@ The block below is the parse this document renders, byte for byte. `--check` com "title": "**Q126 \u2014 DONE (volt, 2026-10-30; both faults were unowned tooling debt and both are closed by measurement: the nine host-bound test files now bind the module beside them, and the path-claims ledger \u2026" }, { - "bytes": 8209, - "end_line": 13575, + "bytes": 8439, + "end_line": 13576, "id": "Q110", "kind": "record", "line": 13557, - "lines": 19, + "lines": 20, "role": "continuation", "role_detail": "DONE", "status": null, "status_line": null, "title": "**Q110 \u2014 DONE (volt, 2026-10-30, at the commit this record lands in; quill lane by authorship, unclaimed and taken). `wizard-work/quill/QUICKSTART.md` no longer prints a command nobody ran: every \u2026" + }, + { + "bytes": 9706, + "end_line": 13597, + "id": "Q127", + "kind": "record", + "line": 13577, + "lines": 21, + "role": "record", + "role_detail": "BOLD-LEAD", + "status": null, + "status_line": null, + "title": "**Q127 \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). A ten-file sweep put Q109's and Q120's pre-repair \u2026" } ], "nested": [ @@ -4417,7 +4445,7 @@ The block below is the parse this document renders, byte for byte. `--check` com "form": "BOLD-LEAD", "id": "Q109", "line": 13270, - "refs": 11, + "refs": 22, "section": "Q104", "shape": "BULLET+BOLD" }, @@ -4426,7 +4454,7 @@ The block below is the parse this document renders, byte for byte. `--check` com "form": "BOLD-LEAD", "id": "Q110", "line": 13272, - "refs": 4, + "refs": 6, "section": "Q109", "shape": "BULLET+BOLD" }, @@ -4516,7 +4544,7 @@ The block below is the parse this document renders, byte for byte. `--check` com "form": "BOLD-LEAD", "id": "Q120", "line": 13308, - "refs": 9, + "refs": 19, "section": "Q119", "shape": "BULLET+BOLD" }, @@ -4564,6 +4592,15 @@ The block below is the parse this document renders, byte for byte. `--check` com "refs": 2, "section": "Q113", "shape": "BULLET+BOLD" + }, + { + "first_mention": 13577, + "form": "BOLD-LEAD", + "id": "Q127", + "line": 13577, + "refs": 1, + "section": "Q110", + "shape": "BULLET+BOLD" } ], "preamble": { @@ -4571,9 +4608,9 @@ The block below is the parse this document renders, byte for byte. `--check` com "end_line": 8, "line": 1 }, - "queue_bytes": 2157364, - "queue_lines": 13575, - "queue_sha256": "de757a2d6e65c789381da4d4f8c1b32f1ef0b01a0354d685ff52ad0120173275", + "queue_bytes": 2167300, + "queue_lines": 13597, + "queue_sha256": "3ed7ff7e05b14f66be05cf3a3e266f43934c3de2135b2b5c30dd970b762473f0", "sections": [ { "bytes": 3139, diff --git a/wizard-work/spark/CHECK b/wizard-work/spark/CHECK index 74ef389..86db41d 100755 --- a/wizard-work/spark/CHECK +++ b/wizard-work/spark/CHECK @@ -24,6 +24,12 @@ grep -q "root" /root/.hermes/wizard-outputs/spark/results.txt || exit 7 # Check Python 3 available python3 --version 2>&1 | grep -q "Python" || exit 10 +# Check the closed repairs this lane landed are still the bytes that landed: +# Q109 (snippet.py, verify_hermes_setup.sh) and Q120 (python-env-check.py). +# Q127. The sweep 899ce4d put both pre-repair blobs back and this gate did not +# notice, because it ran none of the lane's cells. It runs one now. +python3 "$(dirname "$(readlink -f "$0")")/closed_artifact_guard.py" >/dev/null || exit 12 + # Check git commit pushed git -C /root/troupe-standup rev-parse ed959f0 >/dev/null 2>&1 || exit 11 diff --git a/wizard-work/spark/CHECK-closed-artifacts b/wizard-work/spark/CHECK-closed-artifacts new file mode 100755 index 0000000..78fed37 --- /dev/null +++ b/wizard-work/spark/CHECK-closed-artifacts @@ -0,0 +1,46 @@ +#!/bin/bash +# CHECK-closed-artifacts: Q127's gate. +# +# Two legs, both required: +# 1. the guard, over this tree (pins + cells + contract) +# 2. the guard's own selftest (six fixtures: one control, five mutants) +# +# Exit codes follow the repo's tools/ contract: +# 0 = every pinned blob is the one its closed record landed, and its cell is green +# 1 = a graded fault (a repair reverted or drifted, a cell red, the guard unfit) +# 2 = could not grade (no python3, no guard, or the guard could not read the tree) +set -u + +HERE=$(cd -- "$(dirname -- "$(readlink -f -- "$0")")" && pwd) || { + echo "CHECK: cannot resolve the gate's own directory -- cannot grade." >&2 + exit 2 +} + +if ! command -v python3 >/dev/null 2>&1; then + echo "CHECK: python3 not on PATH -- cannot grade." >&2 + exit 2 +fi + +if [ ! -f "$HERE/closed_artifact_guard.py" ]; then + echo "CHECK: $HERE/closed_artifact_guard.py is missing -- cannot grade." >&2 + exit 2 +fi + +python3 "$HERE/closed_artifact_guard.py" +rc=$? +case $rc in + 0) ;; + 1) echo "CHECK: a closed repair has been reverted or drifted (rc 1)." >&2; exit 1 ;; + *) echo "CHECK: the guard could not read this tree (rc $rc)." >&2; exit 2 ;; +esac + +python3 "$HERE/closed_artifact_guard.py" --selftest >/dev/null +rc=$? +case $rc in + 0) ;; + 1) echo "CHECK: the guard's own selftest is red -- it no longer grades (rc 1)." >&2; exit 1 ;; + *) echo "CHECK: the guard's selftest could not run (rc $rc)." >&2; exit 2 ;; +esac + +echo "CHECK: Q109's and Q120's repairs are byte-identical to what landed, and both cells run green." +exit 0 diff --git a/wizard-work/spark/closed_artifact_guard.py b/wizard-work/spark/closed_artifact_guard.py new file mode 100644 index 0000000..8fdb7dd --- /dev/null +++ b/wizard-work/spark/closed_artifact_guard.py @@ -0,0 +1,495 @@ +#!/usr/bin/env python3 +"""Closed-artifact regression guard: a repaired blob stays repaired. + +Q127. `899ce4d` ("spark: add verified Fibonacci calculator with assertions", a +sweep of ten files) rewrote two files that two closed items had repaired the day +before, and put the pre-repair bytes back: + + wizard-work/spark/snippet.py Q109 -> reverted to the 3857b60 blob + wizard-work/spark/python-env-check.py Q120 -> reverted to the c166bf1 blob + +Neither item's record was edited and neither cell was deleted, so both gates +went red at HEAD and stayed red: `bash wizard-work/spark/CHECK-env-verifiers` +read `Ran 16 tests ... FAILED (failures=9)` rc 1, and +`python3 wizard-work/spark/test_python_env_check.py` read +`Ran 7 tests ... FAILED (failures=5, errors=2)` rc 1. A red gate is only a guard +if something runs it, and the spark lane's own CHECK runs none of its cells +(it grades absolute paths under /root instead -- the Q109/Q120 class). + +This file is the missing run: it pins the bytes each repair landed, so a later +whole-file overwrite is a named fault rather than a silent regression. The pins +are data, not a heuristic, and the pre-repair digests are data too, so the +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 + 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 + +Exit codes follow the repo's tools/ contract: + 0 = every pinned blob and every cell is as the closed records left it + 1 = a graded fault (drift, revert, absent artifact, broken cell/contract) + 2 = could not grade (bad root, no python3, selftest fixture failure) + +Nothing here imports the graded artifacts: every reading is a digest, a +subprocess, or a file read, so the guard cannot agree with the code by sharing +it. Paths resolve from __file__ (or --root), so a clone grades its own tree. +""" + +import argparse +import hashlib +import json +import os +import shutil +import subprocess +import sys +import tempfile + +SPARK = os.path.join("wizard-work", "spark") + +# The digest each repair landed, and the digest it replaced. `repaired` is the +# pin; `pre_repair` exists only so the diagnosis can name a revert. +PINS = { + "snippet.py": { + "repaired": "3eb85613fce77d5f0b9e31ca7d9e1ff6cd83dd9fd9c63e8268ed69f576a8c3b2", + "pre_repair": "dc4dbeeee3a2dc8d6c4babdf72e4aaf89dcadfa4cbd24146ef5b96aabb239e2a", + "item": "Q109", + "landed": "ebba640", + "pre_rev": "3857b60", + }, + "python-env-check.py": { + "repaired": "747dcc3afdccceb2c9fca2eb3cb2e933edac3021c40317f979f630d05c53fc56", + "pre_repair": "51e7a52334a2a487320f68bc20ae8f7311f94ba47de6ee375acc05a349bba037", + "item": "Q120", + "landed": "bda365c", + "pre_rev": "c166bf1", + }, + "verify_hermes_setup.sh": { + "repaired": "b58861a11d38fa01e7f9731e39e435db82b80a63e3007548bed94c8f352dbc6f", + "pre_repair": "0af7884e8192d10e6c9cf5761e094fb31e7b62fd02433f2cf2201b57bd26031e", + "item": "Q109", + "landed": "ebba640", + "pre_rev": "3857b60", + }, +} + +# 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"] + +# Host packages the Q120 cell asserts present. Absent here on 2026-10-30, so the +# cell reads red for the environment and not for the code -- reported, never +# graded, because a tree pin must not depend on what this host has installed. +ENV_PACKAGES = ["numpy", "yaml"] + +DECLARED = {"yaml", "numpy", "PIL", "jsonschema"} + + +def sha256(path): + h = hashlib.sha256() + with open(path, "rb") as fh: + for chunk in iter(lambda: fh.read(1 << 16), b""): + h.update(chunk) + return h.hexdigest() + + +def classify(rel, digest): + """repaired / reverted / drifted -- the naming half of the pin leg.""" + pin = PINS[rel] + if digest == pin["repaired"]: + return "repaired" + if digest == pin["pre_repair"]: + return "reverted" + return "drifted" + + +def run(cmd, cwd=None, timeout=180): + return subprocess.run( + cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout + ) + + +class Report: + def __init__(self): + self.passed = [] + self.failed = [] + self.info = [] + self.unreadable = [] + + def ok(self, leg, detail): + self.passed.append(leg) + print("PASS %s: %s" % (leg, detail)) + + def bad(self, leg, detail): + self.failed.append(leg) + print("FAIL %s: %s" % (leg, detail)) + + def note(self, leg, detail): + self.info.append(leg) + print("INFO %s: %s" % (leg, detail)) + + def cannot(self, leg, detail): + self.unreadable.append(leg) + print("UNGRADABLE %s: %s" % (leg, detail)) + + +def leg_pins(root, rep): + """One leg per artifact: the blob is the one its closed record landed.""" + for rel in sorted(PINS): + path = os.path.join(root, SPARK, rel) + pin = PINS[rel] + if not os.path.exists(path): + rep.bad("pin:%s" % rel, "absent from %s (Q109/Q120 artifact missing)" % os.path.join(SPARK, rel)) + continue + digest = sha256(path) + status = classify(rel, digest) + if status == "repaired": + rep.ok("pin:%s" % rel, "sha256=%s == %s's repair at %s" % (digest[:12], pin["item"], pin["landed"])) + elif status == "reverted": + rep.bad( + "pin:%s" % rel, + "sha256=%s is the %s pre-repair blob -- %s's repair at %s was " + "overwritten; restore with: git show %s:%s/%s" + % (digest[:12], pin["pre_rev"], pin["item"], pin["landed"], + pin["landed"], SPARK, rel), + ) + else: + rep.bad( + "pin:%s" % rel, + "sha256=%s is neither the pinned repair (%s) nor the pre-repair " + "blob; re-derive the pin if this edit is intended" + % (digest[:12], pin["repaired"][:12]), + ) + + +def leg_cell_files(root, rep): + for rel in CELL_FILES: + path = os.path.join(root, SPARK, rel) + if os.path.exists(path): + rep.ok("cell-file:%s" % rel, "present, sha256=%s" % sha256(path)[:12]) + else: + rep.bad("cell-file:%s" % rel, "the cell that grades these artifacts is gone") + + +def leg_env_verifiers(root, rep): + """Q109's own gate, run as the item wrote it.""" + gate = os.path.join(root, SPARK, "CHECK-env-verifiers") + if not os.path.exists(gate): + rep.bad("cell:env-verifiers", "CHECK-env-verifiers is absent") + return + proc = run(["bash", gate]) + tail = (proc.stdout + proc.stderr).strip().splitlines() + last = tail[-1] if tail else "(no output)" + if proc.returncode == 0: + rep.ok("cell:env-verifiers", "rc 0 -- %s" % last) + elif proc.returncode == 1: + rep.bad("cell:env-verifiers", "rc 1 -- %s" % last) + else: + rep.cannot("cell:env-verifiers", "rc %d -- %s" % (proc.returncode, last)) + + +def leg_pyenv_contract(root, rep): + """Q120's documented behaviour, measured without importing the script. + + These legs hold whatever this host has installed: an absent package is + named, a usage error is rc 2, and the declared set is the lane's four. + """ + script = os.path.join(root, SPARK, "python-env-check.py") + if not os.path.exists(script): + rep.bad("contract:python-env-check", "script is absent") + return + + plain = run([sys.executable, script]) + if plain.returncode not in (0, 1): + rep.bad("contract:python-env-check", "uninstrumented run exited %d, want 0 or 1: %s" + % (plain.returncode, (plain.stderr or "").strip()[:160])) + return + try: + report = json.loads(plain.stdout) + except ValueError as exc: + rep.bad("contract:python-env-check", "output is not JSON: %s" % exc) + return + + packages = report.get("components", {}).get("packages", {}) + if set(packages) != DECLARED: + rep.bad("contract:python-env-check", + "declared set %s != %s" % (sorted(packages), sorted(DECLARED))) + return + if report.get("all_ok") != (report.get("missing") == []): + rep.bad("contract:python-env-check", + "all_ok=%r disagrees with missing=%r" % (report.get("all_ok"), report.get("missing"))) + return + want_rc = 0 if report["all_ok"] else 1 + if plain.returncode != want_rc: + rep.bad("contract:python-env-check", + "exited %d with all_ok=%r, want %d" % (plain.returncode, report["all_ok"], want_rc)) + return + for name, result in packages.items(): + if "distribution" not in result: + rep.bad("contract:python-env-check", "%s reports no distribution" % name) + return + + req = run([sys.executable, script, "--require", "flask"]) + try: + req_report = json.loads(req.stdout) + except ValueError: + req_report = {} + if req.returncode != 1 or "flask" not in req_report.get("missing", []): + rep.bad("contract:python-env-check", + "--require flask -> rc %d, missing=%r; want rc 1 and flask named" + % (req.returncode, req_report.get("missing"))) + return + for bad_argv in (["--bogus"], ["--require"]): + proc = run([sys.executable, script] + bad_argv) + if proc.returncode != 2: + rep.bad("contract:python-env-check", + "%s -> rc %d, want 2" % (" ".join(bad_argv), proc.returncode)) + return + from_root = run([sys.executable, script], cwd=os.path.abspath(os.sep)) + try: + root_report = json.loads(from_root.stdout) + except ValueError: + root_report = {} + if root_report.get("missing") != report.get("missing"): + rep.bad("contract:python-env-check", + "reading differs from cwd=/ : %r != %r" + % (root_report.get("missing"), report.get("missing"))) + return + rep.ok("contract:python-env-check", + "declared=%d (rc %d), --require flask rc 1, two usage errors rc 2, same from /" + % (len(packages), plain.returncode)) + + +def leg_env(rep): + """INFO only: what the Q120 cell is coupled to on this host.""" + import importlib.util + + for name in ENV_PACKAGES: + found = importlib.util.find_spec(name) is not None + rep.note("env:%s" % name, + "installed" if found else + "absent on this host -- test_python_env_check.py reads red for the " + "environment, not for the code") + + +def guard(root, run_cells=True): + rep = Report() + if not os.path.isdir(os.path.join(root, SPARK)): + print("q127: rc 2 -- %s has no %s directory" % (root, SPARK), file=sys.stderr) + return 2 + leg_pins(root, rep) + leg_cell_files(root, rep) + if run_cells: + leg_env_verifiers(root, rep) + leg_pyenv_contract(root, rep) + leg_env(rep) + + total = len(rep.passed) + len(rep.failed) + if rep.unreadable: + print("q127: %d leg(s), %d passed, %d fault(s), %d ungradable -- rc 2" + % (total + len(rep.unreadable), len(rep.passed), len(rep.failed), len(rep.unreadable))) + return 2 + if rep.failed: + print("q127: %d leg(s), %d passed, %d fault(s) -- rc 1" % (total, len(rep.passed), len(rep.failed))) + return 1 + print("q127: %d leg(s), %d passed, 0 fault(s) -- closed artifacts are still repaired" + % (total, len(rep.passed))) + return 0 + + +# ---------------------------------------------------------------- selftest -- + +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)) + repairs(dest) + return dest + + +def _revert_snippet(dest): + """Put the pre-repair bytes back -- exactly what 899ce4d did.""" + path = os.path.join(dest, SPARK, "snippet.py") + with open(path, "w") as fh: + fh.write(PREREPAIR_SNIPPET) + + +def _revert_pyenv(dest): + path = os.path.join(dest, SPARK, "python-env-check.py") + with open(path, "w") as fh: + fh.write(PREREPAIR_PYENV) + + +def _drift(dest): + with open(os.path.join(dest, SPARK, "snippet.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)) + return _do + + +# The two pre-repair sources, by value, so a fixture can rebuild the reverted +# tree on any host without a git checkout of 3857b60 / c166bf1. +PREREPAIR_SNIPPET = '''#!/usr/bin/env python3 +""" +Snippet: Verify Hermes Agent environment and available tools. +""" +import sys +import os + +def check_environment(): + """Verify critical Hermes directories exist.""" + dirs = [ + "/root/.hermes", + "/root/.hermes/profiles", + "/root/.hermes/skills", + "/root/.hermes/plugins", + ] + 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"] + available = {} + for tool in tools: + # These are Hermes tools available via hermes_tools + available[tool] = True + return available + +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)''' + +# Only the shape that makes the contract leg red is needed: stdlib-only +# packages, no --require, no usage error, green whatever the host has. +PREREPAIR_PYENV = '''#!/usr/bin/env python3 +"""Python environment health check - verifies key packages are available.""" + +import sys +import json +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)} + +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"), + } + } + + all_ok = all( + p.get("status") == "ok" or p.get("available", False) + for p in results["packages"].values() + ) + + output = { + "all_ok": all_ok, + "timestamp": datetime.now(timezone.utc).isoformat(), + "components": results + } + + print(json.dumps(output, indent=2)) + sys.exit(0 if all_ok else 1) + +if __name__ == "__main__": + main()''' + + +def selftest(repo_root): + """Five one-edit mutants and one control, each its own fixture tree.""" + base = tempfile.mkdtemp(prefix="q127-selftest-") + cells = [] + + def cell(name, repairs, want_rc, want_fragment): + dest = os.path.join(base, name.replace(" ", "-")) + build_fixture(dest, repo_root, repairs) + proc = run([sys.executable, os.path.abspath(__file__), "--root", dest], timeout=600) + out = proc.stdout + proc.stderr + why = [] + if proc.returncode != want_rc: + why.append("rc %d, want %d" % (proc.returncode, want_rc)) + if want_fragment and want_fragment not in out: + why.append("output does not name %r" % want_fragment) + cells.append((name, proc.returncode, not why)) + print("%s %s (rc %d)%s" + % ("ok " if not why else "FAIL", name, proc.returncode, + "" if not why else " -- " + "; ".join(why))) + if why: + print(" " + out.strip().replace("\n", "\n ")[-1200:]) + + cell("control-clean", lambda dest: None, 0, "closed artifacts are still repaired") + cell("mutant-revert-snippet", _revert_snippet, 1, "is the 3857b60 pre-repair blob") + 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") + + # 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. + 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"), + ] + 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)) + print("%s classify-revert-digests (%d/%d)" + % ("ok " if not bad else "FAIL", len(cases) - len(bad), len(cases))) + + shutil.rmtree(base, ignore_errors=True) + passed = sum(1 for c in cells if c[2]) + print("selftest: %d cells, %d passed, %d failed" % (len(cells), passed, len(cells) - passed)) + return 0 if passed == len(cells) else 1 + + +def main(argv=None): + ap = argparse.ArgumentParser(description="guard the closed repairs of Q109 and Q120") + ap.add_argument("--root", default=None, help="tree to grade (default: this repository)") + ap.add_argument("--no-cells", action="store_true", help="skip the subprocess cell leg") + ap.add_argument("--selftest", action="store_true", help="grade the guard on five mutants") + args = ap.parse_args(argv) + + root = args.root or os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")) + if args.selftest: + return selftest(root) + return guard(root, run_cells=not args.no_cells) + + +if __name__ == "__main__": + sys.exit(main()) 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())