Imported from GentechLabs/genTech-agent-skills (
skills/agent-kit/harness-self-evolution/SKILL.md). Install upstream withnpx skills add GentechLabs/genTech-agent-skills --skill harness-self-evolution. Copyright stays with the author.
Harness Self-Evolution Cycle
The Harness Evolution agent makes a Hermes instance measurably more useful to its USER — not to its own bookkeeping. The Critic is its adversary. This skill governs running that cycle correctly.
The GATE (read first, every cycle)
Metric authoring: before writing a recommendation's
## Measurable improvement expectedblock, readreferences/metric-authoring-pitfalls.md— parser-safe metric form, the self-advancing-metric trap, and the stale-sentinel pitfall.
- Read
${HERMES_HOME}/harness/constitution.md. It is proposal-only by default — nevergit committo it; write amendments tofacts/amendment-proposal.mdand notify the user. - Read
facts/critique-log.md. If the last 3 entries are unaddressed, THIS cycle's job is to address them — a NEW hypothesis is disallowed. Execute the most recent counter-recommendation verbatim. - Fix, don't just report. If a problem is fixable with available tools (patch, file write, cron edit), fix it this cycle. Only defer to the operator when the fix needs their credentials or a decision.
External proposals when the gate is closed (human greenlights a NEW hypothesis)
The gate above governs the Evolution cron's own cycles. A DIFFERENT situation: Jordan (or another session) approves a brand-new improvement idea while the last critique is still open / a counter-recommendation is already bound to the next cycle. Do NOT write it to facts/recommendation.md — that file is owned by the cycle queue, and injecting a fresh hypothesis overwrites or collides with the bound counter-recommendation (the exact anti-pattern the gates exist to stop). Instead:
- Stage it in the vault build queue (
10-Labs/build-queue.md) as a numbered item with the source, the idea, and an explicitDo NOT inject into facts/recommendation.md now — evolve-N (timestamp) is bound to the open critique; a new hypothesis is gate-blocked until it clears. - Optionally add a one-line pointer in
11-Mess Hall/considerations.mdif it also needs a go/no-go decision. - Report honestly to the user: the idea is captured and will be lifted as the sanctioned hypothesis on a future cycle once the pending critique is ADDRESSED — do not claim it is queued for the immediate next run.
Worked example (Aug 2, 2026): Jordan approved "metacognitive self-modification" for the harness (from facebookresearch/HyperAgents research). The evolve-36 counter-recommendation was already bound at 16:00 UTC for an open WATCH/high critique, so the idea went to build-queue #35 as a staged candidate instead of into recommendation.md. A future Evolution cycle lifts it when the gate allows a new hypothesis.
Cycle steps
- Write the chosen capability + one measurable improvement to
facts/recommendation.mdBEFORE reading state (STEP 0). - Ground in evidence: cite ONE
witness-log.mdentry by timestamp. The improvement must be observable to the USER, not a token-economy optimisation (Article II). - Append a falsifiable prediction to
predictions.mdbefore executing (STEP 2.5). - Execute:
cd ${HERMES_HOME}/harness && bash scripts/execution-loop.sh evolve 2>&1 | tail -15(note: the script lives inscripts/, not the harness root). - Report ACTION or NO-OP. Never deliver an empty response; if genuinely nothing, reply
[SILENT](alone).
recommendation.md must be machine-parseable
Full field-by-field parser contract, the exact metric selector, and copy-paste
pre-flight/post-flight checks: references/plan-file-contract.md.
execution-loop.sh parses fields with ^FIELD: at line start (case-insensitive). Markdown headers like ## RECOMMENDED_ACTION: are NOT matched (the parser exits on ^#). Write the dispatch fields as plain lines:
RECOMMENDED_ACTION: update_script
CONFIDENCE: high
DETAIL: <one line describing the edit>
REASONING: <why>
Keep prose context under ## headers, but the four dispatch fields must be bare.
A missing DETAIL: writes a content-free audit row (evolve-32/33)
_parse_field "DETAIL" returning empty was unguarded until evolve-34 (it now aborts
dispatch with PLAN MISSING DETAIL; keep the self-checks below anyway) — execute_action checked
only action_type, then built full_detail="${action_detail} || ${metric_out}".
A plan with no DETAIL: line therefore ships
- detail: || METRIC: … into execution-log.md and - Change: || … into
harness-state.md: a row asserting a completed cycle while recording nothing about
what it did. That is the phantom-report class the harness exists to prevent, landed
inside its own audit trail. It also degenerates the (cycle, action, detail)
idempotency fingerprint, which is computed over $a_detail.
- Before dispatching, always self-check:
grep -c '^DETAIL:' facts/recommendation.md== 1. - After dispatching, always self-check:
grep -c 'detail: ||' facts/execution-log.md== 0. - Backfilling a content-free row is a legitimate
facts-cleanupaction. Restore the content from the plan file at that cycle's commit and leave an inline<!-- backfilled at evolve-N … -->comment saying it was backfilled and why (Article V: a repaired trace must announce the repair).
The metric block must be BACKTICKED — METRIC: prefixes are silently ignored
_verify_recommendation_metrics selects lines with
grep -E '(grep|awk|wc)[^]*'inside the## Measurable improvement expected section, and **[[ ${#lines[@]} -gt 0 ]] || return 0** — no match means it returns success having verified nothing. So a plan whose metrics read METRIC: grep -c 'x' file -> 0` produces zero captured metrics and a green row.
Correct format is a backticked expression plus a trailing comparison:
## Measurable improvement expected
- `grep -c 'detail: ||' facts/execution-log.md` == 0
- `grep -cE '^[[:space:]]*CYCLE_ID=' scripts/execution-loop.sh` >= 1
Verify the block parses BEFORE running the loop:
sed -n '/^## Measurable improvement expected/,/^## /p' facts/recommendation.md | grep -cE '(grep|awk|wc)[^]*' should equal the number of metrics you declared. This is a third instance of the fail-open family (:282returns 0 on no-match, like the old empty-cycle-id and non-numeric-output holes) — seereferences/plan-file-contract.md`.
📖 Full detail: references/metric-and-loop-pitfalls.md — every per-evolve write-up that used
to live inline (evolve-34/36/37/38/39/51/54/55/59/60/62 and the loop/parser pitfalls). Read it
before changing the metric block, the parser, or the dispatch/refresh scripts.
Anti-Pattern: self-reported result:ok without running the declared metric
The recurring, highest-severity failure. A cycle declares a success metric in recommendation.md (e.g. `grep -c 'x' file` == 0) but writes - result: ok to execution-log.md without ever running it — the audit trail lies. Caught across THREE consecutive harness cycles (Jul 31, 2026): the Change line was 190 chars against its own declared ≤180, and the fix-shipping cycle re-injected the exact string it was purging.
Fix — gate the result field on the declared metrics IN THE LOOP, not per-cycle. A per-cycle patch cannot fix a per-cycle blindness. Add a verifier that parses the backticked grep/awk/wc expressions out of the plan file, runs them, echoes one METRIC: line each into the audit detail, and downgrades ok → partial(metrics-failed) if any metric fails:
_verify_recommendation_metrics() {
local rc=0 expr out
mapfile -t exprs < <(sed -n '/^## Measurable improvement expected/,/^## /p' "$RECOMMENDATION_FILE" \
| grep -oP '(?<=`)(grep|awk|wc)[^`]*(?=`)')
[[ ${#exprs[@]} -gt 0 ]] || return 0
for expr in "${exprs[@]}"; do
out="$(cd "$HARNESS_DIR" && eval "$expr" 2>&1 | head -1)" || true
printf 'METRIC: %s -> %s\n' "$expr" "$out"
case "$out" in ''|*[!0-9]*) rc=1 ;; esac
done
return $rc
}
Call it immediately before _append_execution_log and append the METRIC: lines to the detail so the log carries inline proof the metrics ran. Key insight: a cycle that can rewrite its own pass criterion between the plan file and the prediction file cannot be falsified by the Verifier — the gate must live in the loop, and the audit entry must carry the metric OUTPUT, not a bare ok.
Threshold-aware gate (evolve-26 fix — the snippet above is INCOMPLETE)
The case "$out" in ''|*[!0-9]*) rc=1 test above only checks the output is a number — it accepts 0. A metric declared >= 1 that returns 0 is still graded ok. That is a second-order version of the same self-verification lie. The gate must parse each metric's trailing comparison (>= <= == > <) + operand and evaluate out <op> operand, failing when the comparison is FALSE — not only when the output is non-numeric:
_metric_compare() { # $1=out $2=op $3=operand -> 0 if holds
case "$2" in
'>=') [[ "$1" -ge "$3" ]] ;; '<=') [[ "$1" -le "$3" ]] ;;
'==') [[ "$1" -eq "$3" ]] ;; '>') [[ "$1" -gt "$3" ]] ;;
'<') [[ "$1" -lt "$3" ]] ;; *) return 0 ;; # no op -> presence-only
esac
}
Iterate over whole metric LINES (not just the backticked expr), pull expr with grep -oP '(?<=)(grep|awk|wc)[^]*(?=)', then: non-numeric → FAIL; opset and_metric_compare` false → FAIL; else PASS.
⚠️ CORRECTED (evolve-40) — do NOT scrape
op/operandfrom the whole line. The long-standinggrep -oE '(>=|<=|==|>|<)[[:space:]]*[0-9]+'applied to$lineis BROKEN for any expression that contains its own comparison operator. Theawk-wrapper form this skill recommends elsewhere —awk 'BEGIN{while((… |getline l)>0) …}'— carries a literal>0inside the awk program body, which sorts before the real trailing threshold. Result:opcame from the true>= 9butoperandcame from the>0, so the gate silently graded the metric as>= 0and published[PASS >= 0]. This is the actual root cause of the unfalsifiable[PASS >= 0]receipts — a parser mis-extraction, not an authoring error. Every awk-based metric published before evolve-40 was graded against a threshold of zero. Parse the comparison from the text AFTER the closing backtick only:local tail_cmp="${line##*\`}" op="$(printf '%s' "$tail_cmp" | grep -oE '(>=|<=|==|>|<)' | head -1)" operand="$(printf '%s' "$tail_cmp" | grep -oE '(>=|<=|==|>|<)[[:space:]]*-?[0-9]+' | grep -oE -- '-?[0-9]+' | head -1)"General rule: when parsing a declaration that embeds executable text, always anchor the parse to the delimiter, never scan the whole line. The embedded program is adversarial input to your own parser.
evolve-40: unfalsifiable thresholds are rejected by the gate
>= 0 on a counter (always ≥ 0) and any negative operand are non-measurements. The gate
now prints METRIC: … [UNFALSIFIABLE CRITERION >= 0 — no counter value can fail this; FAIL]
and rc=1. Asserted by selftest case (i); runner prints ALL 9 ASSERTIONS HOLD.
Authoring rule (now REQUIRED in prompts/evolution.md): every published threshold must be
one the PRE-CHANGE live value actually FAILS. Record the pre-change value next to each
metric in the plan. A threshold your starting state already satisfies proves nothing.
Companion clause: a METRIC must never measure facts/execution-log.md,
facts/harness-state.md or facts/.dispatch-state for a phrase that appears in the
dispatching row's own DETAIL — measure the artefact, not the receipt. Emit METRIC: <expr> -> <out> [PASS >= 1] / [FAIL >= 1] so the audit entry proves the threshold was checked, not just the run.
Prove it with a re-runnable selftest, don't eyeball it. Ship scripts/selftest-metric-gate.sh that sources the loop and asserts: a metric failing its threshold → rc=1, one satisfying it → rc=0. Reference it as a metric in recommendation.md so the gate verifies itself. See references/metric-gate-selftest.md for the working script + the two sourcing traps below. For proving a guard (not a metric) can fail — the tautology detector, the ^CYCLE_ID= anchor gotcha, and the set +e-after-source trap — see references/guard-falsifiability.md.
When the operator asks about a critique delivery (Aug 1, 2026)
The Harness Critic cron delivers its audit to the user's channel (e.g. Telegram HQ). When Jordan replies "is there anything for me to do?", the answer comes from the critique's own "Operator Action Required" line, not from re-reading the whole audit:
Operator Action Required: none — Evolution must address <items> in <next cycle>→ NO operator action. The counter-recommendation is queued for the next Evolution cycle automatically; the loop self-handles. Say so plainly and summarize what the next cycle will do (e.g. "evolve-30 at 16:00 UTC fixes both items, then the next Critic verifies"). Do not invent homework for the user.Operator Action Required: <something>→ that something IS the action (wallet decision, credential, approval). Surface it as a clear ask.
Also explain the verdict honestly: WATCH/high means the loop found NEW weaknesses
on top of a genuine fix — the harness catching itself inflating its own score is
the system working, not failing. When a critique contradicts a state file's
attribution (e.g. state says ADDRESSED (evolve-27) but the critique says
ADDRESSED (evolve-29)), trust the critique + git log over the state header.
New high-severity audit findings (evolve-29, Aug 1, 2026)
Two NEW anti-patterns the Critic caught that extend the existing gates:
-
Guard reading the file it polices (AUDIT GAP cycle-id from plan file).
execution-loop.shderived the cycle id viagrep -oE 'Cycle evolve-[0-9]+' "$RECOMMENDATION_FILE"— the same stale-prone plan file the assertion exists to police. A stale plan header makes the assertion grep for the OLD id, find the OLD row, and pass. Fix: cycle id must come from the loop's own CYCLE_ID orhighest evolve-N in execution-log.md + 1, and additionally assert the plan header names that cycle (STALE PLAN HEADERerror otherwise). Any guard whose input is derived from the bug it detects is trivially satisfiable — sever it.evolve-31 follow-up — the "fix" made it WORSE. Read this before touching a guard. evolve-30 replaced the plan-file read with
_cyc="${CYCLE_ID:-}"plus a log-derived fallback — but never DEFINEDCYCLE_IDanywhere (grep -rn 'CYCLE_ID' scripts/= 1 hit, zero assignments). So the fallback always ran, reading the log row that_append_execution_loghad written seven lines earlier from the plan file. The id was still laundered out of the plan; STALE PLAN HEADER became unfirable AND the previously-working AUDIT GAP check was killed as collateral — while the declared grep metric reported success. Two rules fall out:- A guard's input must be defined UPSTREAM of, and independently from, the
artifact it polices. Define the cycle id once at global scope from the log
clock (
highest ^## evolve-N + 1),exportit, and have every consumer (including_append_execution_log) read that variable. - A textual falsifier cannot verify a semantic claim. evolve-30's metric
was
grep -n '_cyc=' … | grep -c RECOMMENDATION_FILE == 0; it was satisfied by moving the read one function away with the dataflow unchanged. When the claim is "this guard can fail", the only honest falsifier is executing the guard against a failing input and asserting rc=1 — seereferences/guard-falsifiability.md.
Working shape (evolve-31, verified): extract the assertion into a standalone
_assert_cycle_trace()taking the cycle id as$1, call it fromexecute_actionwith${CYCLE_ID:-}, and shipscripts/selftest-stale-plan-guard.shasserting all three paths — stale plan → rc=1 +STALE PLAN HEADER; missing log row → rc=1 +AUDIT GAP; matching → rc=0. Report the residual honestly: AUDIT GAP still checks a row the same run wrote, so it verifies "the append happened", not an independent signal. Say that in the report rather than letting the next Critic find it. - A guard's input must be defined UPSTREAM of, and independently from, the
artifact it polices. Define the cycle id once at global scope from the log
clock (
-
Grading predictions FULFILLED before their due time (self-scoring inflation, Article II). Verifier stamped
OUTCOME #28/#29 FULFILLED~4h before the stamped due epoch while the prediction bodies themselves said "OPEN / NOT YET DUE". Fix: hard rule inprompts/verifier.md— an outcome may be headed FULFILLED/FALSIFIED only when EVERY falsifier is evaluated AND now >= due epoch; otherwise header MUST readPARTIAL (n/m falsifiers, due <ts>). Correct existing headers when found. A prediction with an untested falsifier is not FULFILLED — this is the harness grading its own homework.
Verifier: sweep for OVERDUE predictions before trusting the due-window-only read
The Verifier prompt says "read predictions where DUE is between last cycle and now" — but
predictions use inconsistent due markers (DUE:, Adjudicate:, due <epoch>, "at the
next Critic cycle", "checkable by Critic evolve-N"), and a prediction can silently pass its
epoch without ever landing in prediction-outcomes.md (a prior Verifier run skipped it, or it
used a non-DUE: marker). If you only read the due-window, overdue predictions accumulate
unadjudicated forever. Before finalising a cycle:
- Cross-check every prediction number. Enumerate ALL prediction ids in
predictions.md(grep -oE '^## PREDICTION #[0-9]+' …) and diff against the ids already handled inprediction-outcomes.md(grep -oE 'PREDICTION #[0-9]+' … | sort -u). Any id in the first set but not the second is potentially overdue. - For each missing id, check its due epoch against now (
date -u). Ifnow >= due, it is overdue and MUST be adjudicated this cycle even though it is outside the nominal window. Seen live 2026-08-05: #43, #44 and #45 had all passed their due epochs unrecorded and were adjudicated alongside #60/#61. - Adjudicate the overdue ones the same way — run every falsifier live, commit the outcome, and note in the report that it was "overdue, adjudicated" so the next Verifier does not re-flag it.
- Also grep
predictions.mdfor explicit statuses (Status: PENDING|OPEN) and verify each one is still genuinely future-due before leaving it.
Anti-Pattern: computing one field, leaving the adjacent one to rot
Each "computed field" fix historically addressed exactly the one line the previous Critic named (the header, then the Last Action label, then its body), leaving the neighbouring hand-edited line stale — the same defect recurred SIX times. When you make a state field computed, make the whole block computed (header + label + body), not one line.
Pitfall: falsifiers that measure the wrong thing (mis-specification)
A falsifier can be wrong while the fix it grades is perfectly correct — and then it records a false FALSIFIED against working code, which is worse than no prediction at all. Two concrete failure modes, both seen live:
- Anchored patterns break when code moves.
grep -c '^CYCLE_ID='returned 0 after a later cycle moved that assignment inside anifblock and indented it. Behaviour unchanged, metric broken. Prefer indentation-tolerant patterns in falsifiers:grep -cE '^[[:space:]]*CYCLE_ID='. - Self-referential counts. A falsifier reading
grep -c 'amended 2026-08-02 at evolve-33' facts/predictions.md == 1is false on arrival — the falsifier's own text lives in that file, so the count is 2. Use>= 1for any pattern the prediction file itself contains, and say why inline. !=on a growing quantity is unfalsifiable-in-spirit — it records regression for progress. A falsifier written`… | grep -cE '^PASS: '` != 6fires the moment a later cycle adds a seventh assertion: strictly more proof, graded FALSIFIED. Seen live — #36 falsifier 4 sat wrong for three cycles while the selftest grew 6 → 9. Rule: for any counter that only ever grows (assertions, guards, tests), the operator must be<or<=. Reserve!=for quantities that are genuinely expected to hold a fixed value. When amending a published falsifier this way, change the operator AND leave an inline dated Article V note explaining that the old form penalised improvement.- Case-sensitivity on a fixed-string grep is a false FALSIFIED waiting to happen. A
falsifier written
grep -c 'superseded' facts/observation-log.md== 0 fires as a FALSE FAIL against a correction entry that correctly spells itSUPERSEDED— GNU grep is case-sensitive, the lowercase literal matches nothing (rc=1), and a real fix is graded FALSIFIED. Seen live at #61 (same class as #59's BRE-vs-ERE literal:grep -c "METRIC: [^|;]+ -> "where+is a BRE literal → returns 0). For a falsifier asserting presence of a text marker, usegrep -ci(case-insensitive) or match the exact casing the fix emits — and re-run the literal at authoring time against the state you created. Distinguish "the marker is genuinely absent" from "my pattern couldn't match it": ifgrep -cifinds it but exact-casegrep -cdoesn't, the fix is present and the falsifier is mis-specified — record the intent as FULFILLED and flag the class in an Article V note, never a false FALSIFIED.
Rule: run every falsifier at authoring time, before appending the prediction. If a
falsifier does not return its expected value against the state you just created, the
falsifier is wrong — fix it now, not at the due date. When amending an already-published
falsifier, never silently rewrite it: change the pattern AND append a dated
(amended YYYY-MM-DD: <why the old pattern could not match>; behaviour unchanged) note,
per Article V.
Editing harness scripts under the terminal hardline blocklist
Inline sed -i/awk with regex + special chars, and &&-chained multi-statement lines, get rejected via the terminal tool with BLOCKED (hardline): command parser limit or malformed executable payload — even in cron/yolo mode. It is NOT a syntax error; the parser refuses the payload. Reliable paths:
- Use the
patchtool for in-place edits instead ofsed -i— this is the go-to for regex-y edits. - Run each check as its own standalone simple command (
grep -c 'x' fileon one line, never chained). A quoted argument containing shell metacharacters is enough to trip it even when quoting is correct —grep -c 'selftest-foo.sh; echo' fileinside an&&/;-chain of several echoes was rejected at evolve-35, while the same grep issued alone ran fine. When a verification batch is blocked, don't rewrite the quoting: split it into one call per check and batch them as parallel tool calls instead. - If you truly need awk/complex logic,
write_filea tiny.shandbash /tmp/foo.sh— a script file passes where the same code inline is blocked. Multi-lineecho "label=$(cmd ...)"verification batches are reliably BLOCKED (hit twice at evolve-54) — a newline-separated block ofecholines each wrapping a command substitution trips the parser even with correct quoting. Drop the labels and issue the bare commands separated by;on ONE line (grep -c 'a' f; grep -c 'b' f; cat x), then map the outputs to their checks by position. Ugly, but it goes through on the first try. - Appending a block of text: a quoted
cat >> file <<'ZZZ' … ZZZheredoc goes through (verified evolve-38 forpredictions.mdandcleanup-log.mdappends), while apython3 - <<'EOF'heredoc doing in-place string surgery was refused. So: heredoc for pure appends, thepatchtool for edits-in-place, and never mix the two in one command.
Pitfall: wc -c vs awk length on UTF-8
wc -c counts BYTES (a — em-dash = 3 bytes); awk length and Python [:N] count CHARACTERS. A 170-char payload + 10-char prefix reads 180 via awk length but 182 via wc -c when it contains multibyte chars. Match the verify tool to the cap's semantics — if the declared metric is awk length, verify with awk length.
The pre-execution unaddressed-critique abort — and how to clear it legitimately
read-external-feedback() in scripts/execution-loop.sh aborts the whole run with
ABORTED: <VERDICT> <severity> unaddressed when the LAST - Verdict: / - Severity:
in critique-log.md is high/critical and the last - Action: is not literally ADDRESSED.
This fires on WATCH high, not just DEGRADATION critical (seen live at evolve-35:
✗ must_act=true — unaddressed high critique / ABORTED: WATCH high unaddressed). Any
high-severity verdict with a non-ADDRESSED action marker blocks dispatch.
It greps awk '{print $3}' off the last matching line of each label — so the gate reads
the most recent entry only.
Correct sequence when you hit this abort:
- Do the fix first. Never touch the log to unblock yourself before the work is done — that is forging the audit trail (Article V).
- Append a RESOLUTION entry to
critique-log.mdcarrying- Action: ADDRESSED,- Verdict: RESOLVED,- Severity: low, plus the verified command output for each item of the counter-recommendation (git ls-files … | wc -l== 2,bash -nclean, etc.). - Re-run the loop. It should reach
✓ action dispatched.
The abort firing is the gate working. Treat a first-run abort as expected when you are executing a counter-recommendation, not as a bug.
Two ways to clear it — pick by whether the critique entry is still open. Appending a
RESOLUTION entry (above) is right when you want a separate audited resolution record. When
the blocking entry is the counter-recommendation you just executed in this cycle, the
cleaner move is to flip that entry's own - Action: PENDING marker in place to
- Action: ADDRESSED (evolve-N, <ISO ts> — <one-line summary of what shipped>). The gate
reads only the last ^- Action: line, so both work; in-place editing keeps one entry per
critique instead of growing a second log row per cycle. Locate it with
grep -n '^- Action:' facts/critique-log.md | tail -3, and edit via a tiny Python
heredoc that asserts the target line's current text before replacing it — a blind
line-number write silently corrupts the ledger if the file shifted:
l = open(p).read().split('\n')
assert l[IDX].strip() == '- Action: PENDING', l[IDX]
l[IDX] = '- Action: ADDRESSED (evolve-35, …)'
Still do the fix FIRST. Flipping the marker before the work exists is forging the trail.
Lighter variant that passes the hardline blocklist (verified evolve-36): a sed line-address
replacement is accepted where regex-y sed -i 's/…/…/' is refused, provided the replacement text
carries no shell metacharacters. Sequence — three separate simple commands, never chained:
grep -n '^- Action: PENDING' facts/critique-log.md | tail -3 → pick the line number →
sed -i '<N>s/.*/- Action: ADDRESSED (evolve-N) — <summary>/' facts/critique-log.md →
sed -n '<N-2>,<N+2>p' facts/critique-log.md to confirm you hit the right entry and its
Cycle audited: line still reads the cycle you meant. The read-back is the safety the assertion
provides in the Python variant — do not skip it.
Pitfall: a Critic counter-recommendation may name an already-used prediction number
The Critic drafts its counter-recommendation from the state it audited, so a "append
PREDICTION #N" instruction can collide with a number the cycle it audited already consumed
(evolve-35: counter-rec said #33, but evolve-34 had already published #33). Before
appending, check grep -c 'PREDICTION #N' facts/predictions.md. If taken, advance to the
next free number and record the divergence in both the prediction body and the cycle
report ("counter-rec said #33; already consumed by evolve-34; numbering advanced to #34,
content unchanged"). Do not reuse a number, and do not silently renumber without saying so —
same Article V rule as amending a falsifier. This is another instance of the meta-lesson
above: execute the counter-recommendation's intent, verify its specifics against live
state, and report the divergence.
Pitfall: the actor may be an AGENT, not a script — fix the PROMPT
Before patching code to stop a bad behaviour, confirm a script actually performs it:
grep -rn '<log phrase it emits>' --include=*.sh .. If nothing matches, the behaviour came
from a cron agent improvising (Gardener, Critic, Evolution). The durable fix then lives in
prompts/<agent>.md as a HARD RULE with an explicit pre-action guard and a verification
command the agent must run before reporting success — not in scripts/.
⚠️ evolve-58: THE FEEDBACK GATE WAS DEAD FOR 17 CYCLES — and how to keep it alive
read-external-feedback() in scripts/execution-loop.sh is the single mechanism that makes a
Critic finding binding rather than advisory. From evolve-39 to evolve-57 it was structurally
inert: it grepped the WHOLE critique-log for ^- Action: and took tail -1, while the Critic
had switched to unprefixed Verdict:/Severity: headers and stopped emitting - Action:.
Result: every high/critical critique was waved through because the gate always found the last
dashed marker — a stale ADDRESSED (evolve-39) at line 761 — and returned 0.
Two fixes, both load-bearing:
- Scope to the LAST
## Critique Entryblock only. Never grep the whole file for a label that may have been superseded. Use awk to isolate the final block:_last_entry="$(awk '/^## Critique Entry/{buf=""} {buf=buf $0 ORS} END{printf "%s", buf}' "$critique_log")" - Tolerate both dashed and unprefixed header styles. The Critic may emit either. Match
case-insensitively with
grep -oiE '^(- )?(verdict|Verdict): *[A-Za-z_-]+'and take$NF. - Fail CLOSED on an absent marker. If
last_actionis empty for a high/critical verdict, abort withABORTED: <verdict> <severity> — no Action marker in last critique entry. Empty must never read as ADDRESSED — this is the same fail-open family as the empty metric set and the whitelist drop.
Prove it with a selftest, not a grep. Cases (s) and (t) in scripts/selftest-detail-metric-gate.sh
execute the guard against synthetic logs:
- (s) A file with an OLD
- Action: ADDRESSED (evolve-39)followed by a NEWER unprefixed entry withVerdict: DEGRADATION/Severity: highand NO action marker → gate aborts (rc=2). - (t) Same file with the newer entry carrying
- Action: ADDRESSED (evolve-58)→ gate passes (rc=0).
Fix the format at the source. skills/harness-critic.md now mandates the four dashed fields
(- Cycle:, - Verdict:, - Severity:, - Action: PENDING|ADDRESSED (evolve-N) — items ...)
immediately under every entry header, with a post-flight that grep -c '^- Action:' increases by
1 per entry. The unprefixed style alone (used from evolve-39 to evolve-57) is what left the gate
reading a stale marker for 17 cycles.
Diagnostic signature — how to tell the gate is dead without running it:
grep -c '^- Action:' facts/critique-log.md # should be >= number of entries
grep -c '^## Critique Entry' facts/critique-log.md # actual entry count
If ^- Action: count is significantly lower than entry count, the gate is reading a stale
marker. The last ^- Action: line is the one the gate sees — check it with
grep -E '^- (action|Action):' facts/critique-log.md | tail -1.
Generalised rule: a gate that greps the whole file for a label will always find an ancient one. Scope every selector to the LAST block of the relevant type, and fail closed when the marker is absent — not when it is present but old.
Pitfall: a placeholder timestamp in an Article V audit note is not evidence
The evolve-56 backfill comment shipped with the literal string 16:0xZ instead of a real
timestamp — 0x is not a digit. An Article V repair note whose own timestamp is an unresolved
template is the phantom class reproduced inside the anti-phantom fix. Never write a placeholder
in an audit note. If you don't know the exact time yet, leave the field blank and fill it in
before committing — a missing timestamp is honest; a fake one is not. Verify before committing:
grep -c '0x[0-9a-f]Z' facts/execution-log.md == 0.
The inverse-phantom class: real work, no receipt (evolve-41/52/54/56/57)
Five cycles shipped committed code and wrote no ## evolve-N row. The root cause was set -e
aborting before _append_execution_log (evolve-54, documented above), but the pattern recurred
even after that fix because hand-executed cycles simply never called the append function. When you
apply a bound counter-recommendation with patch/write_file + git commit:
- Append the receipt by hand. Write a
## evolve-N — <ts>block tofacts/execution-log.mdwith- action:,- result:,- confidence:,- detail:and a|| METRIC:chain. - Re-run every metric LIVE against the working tree — never copy numbers from the originating session's report. A backfilled receipt whose numbers were transcribed rather than re-measured is itself a phantom.
- Carry an inline
<!-- BACKFILLED by evolve-N at <ts>. <cause>. Metrics re-run live. Article V. -->comment naming the cause (hand-executed cycle, no row written). - Update
.cycle-highwaterto match the new row id. - Freeze the plan snapshot at
facts/.plan-snapshots/<cycle>.md. - Final check:
grep -c '^## evolve-<id>' facts/execution-log.md== 1 ANDhead -1 facts/harness-state.mdnames your cycle.
A cycle that shipped code and left no receipt is indistinguishable, to the user and to the next Critic, from a cycle that did nothing. The inverse-phantom is worse than the original because the work is real and the ledger says it never happened.
Pitfall: compaction into a gitignored dir is silent audit-trail destruction
A Gardener run compacted critique-log.md 756→100 and predictions.md 236→100 and wrote the
only copies of the 792 removed lines to facts/.tmp/ — a path double-matched by .gitignore
and untracked. 61 KB of append-only history survived only because the deletion was still
uncommitted (HEAD held the last full copy).
Rules for any ledger compaction:
- Archive destination is
facts/history/(tracked), neverfacts/.tmp/or any scratch dir. - Guard before truncating anything:
git check-ignore -q "$ARCHIVE_DIR/probe" && { echo "FATAL: archive dir is gitignored — refusing to compact"; exit 1; } git add facts/history && git commitin the SAME run; verify withgit ls-files facts/history | wc -landgit show HEAD:<archive> | wc -lbefore reporting success.- If you find such a loss already staged, commit the recovery before anything else — the next
git commit -Aby any harness cron locks it in.
Pitfall: unset $cycle leaking evolve-0 into user-facing state
scripts/skill-automation.sh interpolated evolve-$cycle into harness-state.md when the cycle
id resolved empty, publishing a nonexistent cycle. Any script writing a cycle id to a user-visible
file must fall back (grep -oP 'Cycle evolve-\K\d+' "$STATE_FILE" | tail -1) and skip the write
with a log line rather than emit a placeholder. Verify grep -c 'evolve-0' facts/harness-state.md == 0.
evolve-60: THREE new enforcement mechanisms shipped — know they exist before you cycle
The evolve-59 critique (Article II drift — five cycles of instrument hygiene with no user-friction signal) produced three load-bearing additions to scripts/execution-loop.sh and facts/:
-
facts/observation-log.md— the Article II audit surface. Created at evolve-60. Records "no new user friction since ; harness self-referential for evolve-N..M". Any cycle's change must cite a friction source (witness-log, observation-log, or a concrete critique entry) OR the evolve arm aborts. This is the first-class record of the absence of user friction — write to it, don't let it go stale.⚠️ Keep it RECONCILED with witness-log.md — a stale "no friction" claim is counterfactual (evolve-61). The evolve-60 entry claimed "No new user friction since 2026-08-01" on the very day the harness's own credential-health probe logged
github=401and witness-log recorded a blocking ACTION-NEEDED. The ground-truth surface silently contradicted the witness log. Rule: whenever a friction source (witness-log, credential-health) records new friction, append a dated correction entry to observation-log.md stating the prior no-friction claim is SUPERSEDED — never leave the Article II surface asserting an absence that a live signal contradicts. The Critic flagged this exact staleness; reconcile it the same cycle you notice it. -
action-type-by-source (breaks the monoculture). In
execute_action, right afteraction_typeis parsed, a plan whoseDETAIL:references instrument code (execution-loop.shor a selftest script) is normalizedfacts-cleanup→update_script.facts-cleanupis now reserved for plans that only editfacts/*prose (audit trail). A 4-cycle facts-cleanup monoculture on one theme is a stagnation signal the Critic flags. -
grounded-plan()— the Article II start gate. Runs in the evolve arm right afterread-external-feedback. A plan is grounded if it EITHER carries an explicit no-op OR cites a friction source (^WITNESS:,^EVIDENCE:,^CRITIQUE:, or mentionsobservation-log.md/witness-log.md). Otherwise it aborts rc=2 withungrounded plan — no user-friction citation and no Article III no-op. This makes a cycle that would manufacture backlog to justify itself impossible. Authoring rule: always include aWITNESS/EVIDENCE CITATION:field inrecommendation.md— it is now required, not stylistic.⚠️ SUPERSEDED (evolve-61) — the no-op branch must match ONLY the STRUCTURED ledger form. The evolve-60 gate accepted
noop|no-op|no user friction|result: noop(a keyword coincidence): any self-referential plan could mention "no user friction" in DETAIL prose, pass the gate (rc=0), and then run a backlog anyway — that is not a no-op. Evolve-61 replaced it with a dedicated_plan_declares_noop()that matches only^- (result|Result): *noopon its own line, and the evolve arm nowreturn 0without callingexecute_actionwhen a structured no-op is declared (and writes a- action: noopaudit row so the refusal is visible). Rule: a gate's "exempt" branch must match the structure of the exemption, never a keyword that can appear incidentally in prose — and a declared no-op must actually skip the work, or it is a no-op in name only. Proven by selftest cases (u) ungrounded-abort (rc=2) and (v) keyword-coincidence-vs-structured discrimination; suite now 17/17 PASS.
Pitfall — my own evolve-60 plan hit the gate it ships. The plan declared a metric `grep -c '^## evolve-60' facts/execution-log.md` == 1 — the exact self-matching-criterion / "never metric your own cycle's future receipt" class the skill warns about (evolve-59 sub-case, above). The gate caught it live, emitted SELF-MATCHING CRITERION, and downgraded the row to partial(metrics-failed). This confirms the guard fires and the correct pattern: declare metrics that hold BEFORE dispatch (selftest PASS count, a counter present in a script, an assertion name present) — never the receipt's own existence. The receipt's existence is _reverify_published_metrics / _reverify_prior_cycle's job, not a plan metric's. The partial(metrics-failed) result for real, committed work is acceptable and honest — report it as the gate working, not as a failure.
Credential failure triage pattern (useful, not env-specific): when credential-health.sh flags a provider 401, check EVERY backup store before declaring the credential dead — .env, .env.bak, .env.bak-<ts>, AND the tool's own config (e.g. gh hosts.yml). At evolve-60 the GitHub token was 401 in all four (a genuine expiry). If all are dead, that is an operator-blocked item: log it to witness-log.md with severity/impact/fix-required and stop — do not spin cycles trying to self-fix a credential you cannot regenerate. This is the "check the source" discipline of the api-key-rotation pattern applied at the credential-probe layer.
⚠️ delta-mode masks REPEATED credential failures — "all probes OK" while a credential is dead (Aug 5, 2026)
_log_status in scripts/credential-health.sh has a delta-mode that skips re-logging an unchanged status (keeps the log a clean change-history, not 80 lines/day). But the ALL_OK=false update was placed AFTER that early return — so a repeated failure (401→401) hit the return before ALL_OK was set, and the probe printed credential-health: all probes OK while the credential stayed dead. The Critic's own operator-blocked item (the GitHub 401) was being silently hidden from the very probe designed to surface it.
Fix: set ALL_OK=false for a non-200 status BEFORE the delta-mode suppression return, so a repeated failure still flags the probe failed:
_log_status() {
...
# non-200 must ALWAYS set ALL_OK=false, even when delta-mode suppresses the log write
if [[ "$status" != "200" ]] && [[ "$status" != "SKIP" ]] \
&& [[ "$status" != "FORMAT_OK" ]] && [[ "$status" != "NO_ACCOUNT" ]] \
&& [[ "$status" != "NO_KEYPAIR" ]] && [[ "$status" != "PAY_OK" ]]; then
ALL_OK=false
fi
# then the delta-mode early return (only suppresses the LOG line, not the flag)
}
Diagnostic signature — how to tell a probe is false-greening without reading the code: the log's last line for a provider says | provider=401 but running the probe prints "all probes OK" and exits 0. If the last logged status is a failure and the probe says green, the flag-update is after a suppression return.
Generalised rule: a delta/suppression optimization must never sit between a status check and the aggregate flag that consumes it. Suppress the write, not the conclusion. Same family as the fail-open traps elsewhere in this skill (a path that "records nothing" must not also "conclude nothing").
evolve-62: the RECEIPT-EXISTS SELF-CHECK class — and the over-broad guard that faked a FAIL
Sixth recurrence of the self-referential-criterion family (after #34/#36/#37/#42/#51). The evolve-61 receipt grew an undeclared 8th metric that grepped facts/execution-log.md for its own cycle header — awk '/^## evolve-61/,/^- detail:/' … | grep -c '^## evolve-61' — which is 1 iff the receipt row was written (it counts its own header, so == 0 is structurally unpassable). It slipped past the grep-only SELF-MATCHING ban because it was awk-wrapped, and it was plan-authored, not auto-appended (grep the whole scripts/ tree for the literal before assuming code injected it).
The guard (now in _verify_recommendation_metrics): reject an expression that targets execution-log.md AND contains a grep for its own cycle header ^## ${CYCLE_ID}:
if printf '%s' "$expr" | grep -q 'facts/execution-log\.md' \
&& [[ -n "${CYCLE_ID:-}" ]] \
&& printf '%s' "$expr" | grep -qE "grep[^|]*\^##[[:space:]]*${CYCLE_ID}"; then
# emit RECEIPT-EXISTS SELF-CHECK, rc=1
fi
⚠️ The guard MUST match the own-header grep signature, NOT any mention of $CYCLE_ID. My first version used grep -qF "$CYCLE_ID" and falsely flagged a LEGITIMATE metric — awk '/^## evolve-61/,/^## evolve-62/' … | grep -o 'METRIC: [^;]*' — where evolve-62 is only the awk range END boundary used to measure the previous row. That false FAIL downgraded a correct receipt to partial(metrics-failed). The discriminating signature is grep … '^## <owncycle>' (the receipt header literal), NOT the presence of the id anywhere. Rule: a guard against a self-referential class must key on the class's signature (grep for own header), not on a token that appears incidentally in legitimate expressions.
evolve-62: the row_published counter is PIPE-BLIND — write plan metrics in countable form
_reverify_published_metrics counts published metrics with the prose-immune regex METRIC: [^|;]+ -> — the [^|;] character class means any metric whose expression contains a pipe (|) or semicolon (;) is invisible to it. The evolve-62 plan declared 5 metrics but 2 used pipes (awk … | grep … | wc -l and the awk 'BEGIN{while((…|getline …' wrapper), so the row published only 3 countable tokens and the re-verifier fired PLAN MUTATED-style noise at the cycle boundary.
Authoring rule: express every plan metric in a pipe-free, semicolon-free form so the prose-immune counter can see it. Two workarounds that reproduce cleanly:
- selftest PASS count:
grep -c '^PASS: ' <(bash scripts/selftest-X.sh 2>&1)(process substitution, no pipe) — NOT theawk … |getlinewrapper. - count of
METRIC:tokens in a prior row:awk '/^## evolve-N/,/^## evolve-M/{n+=gsub(/METRIC: /,"")} END{print n+0}'(awkgsubincrementsn, no pipe) — NOTawk … | grep -o … | wc -l. Pre-flight before dispatch: run each expression throughprintf 'METRIC: %s -> 1 [PASS]' "<expr>" | grep -oE 'METRIC: [^|;]+ -> 'and confirm it matches (COUNTABLE); any that don't must be rewritten before the loop, or the next re-verifier flags a false PLAN MUTATED.
Repairing a receipt that shipped a false FAIL: when a dispatch lands partial(metrics-failed) because an over-broad guard (or a plan-authored degenerate metric) flagged a metric that actually passes, repair the row in place — flip - result: to ok, correct the offending metric's [FAIL …] annotation to its true [PASS …], and delete any stale <!-- METRIC SHIFT … -->/<!-- PLAN MUTATED … --> comments that reference the removed criterion. A false FAIL is as corrosive as a false PASS: it makes partial(metrics-failed) uninformative. Re-run every metric live against the working tree before repairing, exactly as for a backfill.
Stale sentinel from an ABORTED (not dispatched) run is safe to clear
The evolve-62 run that aborted at the must_act gate (unaddressed high critique) left a facts/.dispatch-sentinels/dispatched-evolve-62 with no matching ^## evolve-62 ledger row, which then blocked re-entry with DISPATCH ALREADY RUNNING … refusing re-entry. The existing rule says clear a sentinel only when its cycle has a RESOLVED receipt row — but that loop leaves an aborted-cycle sentinel (no row) in place forever, deadlocking the very cycle that must run to address the critique. Refinement: a sentinel with NO matching ledger row that was created by YOUR OWN aborted pre-flight this session is safe to clear (rm -f facts/.dispatch-sentinels/dispatched-evolve-N after confirming grep -c '^## evolve-N' facts/execution-log.md == 0 and the high-water hasn't advanced past N). The 3h floor exists to stop a completed cycle from double-dispatching; an aborted one that never wrote a row cannot double-dispatch. Verify the high-water before clearing — if it advanced, the cycle did dispatch and the sentinel must stay.
⚠️ credential-health.sh "all probes OK" can be a FALSE READING — delta-mode masks a repeated 401
_log_status (credential-health.sh:54-58) runs in delta mode: it skips logging when the
provider's last logged status equals the current one (if [[ "$last_status" == "$status" ]]; then return 0). That return 0 fires BEFORE the ALL_OK=false flip at lines 61-65. So when a credential
was 401 on the previous probe and is STILL 401 on the next one, the current probe never reaches the
ALL_OK=false line and the script exits 0 printing credential-health: all probes OK — grading a
stale, still-broken failure as success.
Seen live (Aug 5, 2026): gh auth status clearly failed ("token in hosts.yml is invalid", exit 1),
every token store tested 401, yet bash scripts/credential-health.sh printed all probes OK and
appended a # Last verified: 09:41:52Z line with no 401 entry — because 401 was already the last
logged GitHub status and the delta-mode early-return swallowed the repetition. Confirm with
bash -x scripts/credential-health.sh 2>&1 | grep -iE 'github|401|200': you'll see _log_status github 401 then the early-return [[ 401 == 401 ]] short-circuit before ALL_OK=false.
Verification rule — never trust the probe's summary line; reproduce the check yourself. When the user says they updated a credential (e.g. "we updated tokens yesterday"), the harness probe is NOT sufficient evidence. Directly test the live value against the upstream API:
# for a GitHub token (probe reads ~/.config/gh/hosts.yml via `gh auth status`, after unset GITHUB_TOKEN):
TOK=$(sed -n 's/^[[:space:]]*oauth_token:[[:space:]]*//p' <profile-home>/.config/gh/hosts.yml | head -1)
curl -s -o /dev/null -w "HTTP %{http_code}\n" -H "Authorization: Bearer $TOK" https://api.github.com/user
gh auth status # probe reads this; exit 1 + "token is invalid" = still broken
And test EVERY store, because an update can land in one place the harness doesn't read: .env,
.env.bak, .env.bak-<ts>, each profile's .env (gizmo, gentech-treasury, etc.), and each
gh hosts.yml (profile home + /root). Also stat -c '%y' each store to see whether the update
actually touched the file the probe reads. The probe does NOT read .env's GITHUB_TOKEN.
Root-cause fix (durable): the ALL_OK flag must be computed from the observed status, not only set on a status change. Move the delta-mode early return AFTER the flag update, or derive the flag from the live check independent of logging. As written, a persistent failure and a fresh success produce the same "all probes OK" — the probe cannot distinguish them. If fixing, add a selftest that runs the probe twice against a still-failing store and asserts exit != 0.
⚠️ GitHub token store gotchas found while triaging the 401 (Aug 5, 2026)
Two traps trip up "propagate the fresh token" far more often than the expiry itself:
-
gh auth login --with-tokenwrites a BROKEN empty-username entry when a stale user already exists in hosts.yml. Runningprintf '%s' "$TOKEN" | gh auth login --with-tokenagainst a hosts.yml that already has aProtoJay4789:entry leaves BOTH the stale invalid entry AND a new"":empty-username entry.gh auth statusthen shows the OLD invalid token, andgh apiruns unauthenticated (the 60/60 core rate-limit view). The command cannot replace an existing entry cleanly. The reliable fix is to rewrite~/.config/gh/hosts.ymldirectly with exactly one valid entry:github.com: users: GentechLabs: oauth_token: <TOKEN> user: GentechLabs oauth_token: <TOKEN> git_protocol: httpsVerify after:
gh auth statusshows✓ Logged in ... account GentechLabsandgh api rate_limit --jq '.resources.core.limit'returns5000(the authenticated view), not60. -
A 403 "API rate limit exceeded" is NOT an invalid token. A token that authenticates — the response carries
x-github-authentication-token-expirationand a user ID that resolves — but returns403 {message: API rate limit exceeded for user ID ...}has exhausted its REST quota; it is still valid. Distinguish from 401 (bad credential) by the error body and the presence of the expiration header. The core quota resets hourly (gh api rate_limit --jq '.resources.core.reset'). Critically:git pushover HTTPS uses the git smart-HTTP protocol, which is SEPARATE from the REST API quota — so pushes succeed even while REST reads (gh api, notifications,gh auth statusrate_limit) are 403. Don't wait for the reset to push queued work; only API reads are throttled. This is why a repo can be stuck (stale embedded token in itsoriginURL) while a freshly-setgit remote set-urlpushes cleanly despite the 403s.
⚠️ A DIAGNOSTIC pre-flight can be a SILENT HARD GATE — set -euo pipefail kills the cycle
(evolve-61) The pre-flight bash credential-health.sh 2>&1 | while IFS= read -r line; do log "$line"; done
was written to diagnose credentials, but under set -euo pipefail a non-zero probe exit aborted the
entire cycle before execute_action — so the known github=401 silently stopped every evolution
dispatch, every 4 hours, with no ledger row and the last log line being the probe's own summary.
A diagnostic check that blocks the work it is supposed to accompany is not a check, it is a fuse.
Fix shape — capture the status with ||, never let the diagnostic gate dispatch:
local _ch_rc=0
bash scripts/credential-health.sh 2>&1 | while IFS= read -r line; do log "$line"; done \
|| _ch_rc=${PIPESTATUS[0]:-0}
log "credential-health pre-flight exited ${_ch_rc} (advisory — not gating dispatch)"
Note BOTH halves matter: the ||-capture (because a bare x=$(...); _ch_rc=$? is the set -e lie —
$? is never read when the command fails), AND PIPESTATUS[0] (the probe's status, not the while
reader's — the reader always exits 0).
Generalised rule: separate "can we proceed" from "what's the health report". A probe whose purpose
is to report a known-blocked state (operator will fix it) must never be a hard gate on unrelated
work. If a known-broken credential is allowed to halt all evolution cycles, one outage makes the
harness blind to everything else. This is the mirror image of the credential-tri
age rule: flag it, log it to witness-log.md as operator-blocked, and let the cycle continue — a
blocked credential degrades one capability, not the whole harness.
Operator-blocked items
When the only open user-facing friction requires the operator (e.g. Pay wallet NO_ACCOUNT / CLI not installed), do NOT spin cycles on log repairs to justify existence (Article III: the harness may remain silent). Write a dated decision block into witness-log.md with numbered options + a stated default, surface it in the report, and stop.
credential-health delta-mode masks repeated failures (Aug 5, 2026)
_log_status() early-returns on an unchanged status BEFORE setting ALL_OK=false, so a repeated 401 (401→401) is deduped from the log but also never flips ALL_OK — the probe reports "all probes OK" while the credential is dead. Fix: set ALL_OK=false for non-OK before the delta return. Full trace + generalised rule: references/credential-health-delta-mode.md.
