volt: Q127 -- restore Q109/Q120 repairs and pin them by digest
899ce4dput 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 fromebba640andbda365c. 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.
This commit is contained in:
1 parent
c0d7fc7769
commit
1cb001a350
7 files changed
+896
-84
No files matched your search
+23
-1
@@ -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 <name> 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:<file>` ×3, `cell-file:<name>` ×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:<pkg>` (**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
|
||||
+54
-17
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Executable
+46
@@ -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
|
||||
@@ -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:<path> sha256 equals the blob the repair commit landed
|
||||
cell:<name> the item's own cell still runs green
|
||||
contract:<name> the repaired artifact's documented behaviour, by subprocess
|
||||
env:<name> 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())
|
||||
@@ -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()
|
||||
sys.exit(main())
|
||||
+147
-28
@@ -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/<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 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/<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__":
|
||||
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)
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user