Applies to: gitreins (Tier‑2 agentic judge) + boardctl / coding‑hermes foreman board.
Verified end‑to‑end with the real boardctl binary and the real gitreins==0.14.0 engine: 9/9 checks pass. The solution is saved at ~/gitreins-tier2-criterion-as-written-repair.md. Here it is:
criterion-as-written FAIL: re‑measure → fix state → fix the criterion text → re‑completeApplies to: gitreins (Tier‑2 agentic judge) + boardctl / coding‑hermes foreman board.
A criterion asserts a numeric scheduler fact:
"…scheduler cooldown is 21600 s and the live scheduler value equals the
fleet.tomlvalue…"
gitreins task complete <id> runs Tier 1 green, then Tier 2 returns 9/10 criteria green, 1 FAIL (the false numeric conjunct). The retry that fixes only the underlying state fails again with:
the criterion as written is not satisfied
Two judge evals (~4 min each) were wasted.
There are two independent defects; fixing one is why the retry failed.
.coding-hermes/board/board.jsonl carries a cooldown_s field seeded by boardctl init to the default 21600. It is a mirror, not an authority. The authority is ~/.hermes/fleet.toml (cooldown_s = 43200) and the live scheduler (43200). The foreman generated the criterion from the stale mirror.
gitreins task complete loads task.criteria from .gitreins/tasks.yaml (via engine/task_manager.py) and feeds it to AgenticEvaluator, whose prompt says to verify each criterion as written. So fixing the board/live state does not change the string the judge compares against. The criterion text itself must be corrected.
If a criterion scalar is single‑quoted and contains an apostrophe (scheduler's ...), the ' must be doubled (''). An unescaped one makes TaskManager fail to load and preserve a .gitreins/tasks.yaml.corrupt-<hash> copy (verified with the real engine).
eval #1 FAIL (stale board + false criterion) → eval #2 FAIL (state fixed, criterion text not patched) → eval #3 PASS only after the text patch.
Run from the main checkout (canonical board lives there).
cd <repo-root>
TASK=QA-DEMO-001
FAILED_VERDICT="$(
grep -rl '"passed": false' .gitreins/history/*/*/verdict.json 2>/dev/null \
| while read -r f; do
grep -q "\"task_id\": \"$TASK\"" "$f" && dirname "$f"
done | sort | tail -1
)"
echo "failed verdict: ${FAILED_VERDICT:-<none found>}"
gitreins report -n 5
CONFIG_COOLDOWN="$(grep -nE '^[[:space:]]*cooldown(_s)?[[:space:]]*=' ~/.hermes/fleet.toml | tail -1 | tr -dc '0-9')"
echo "config cooldown_s = $CONFIG_COOLDOWN" # 43200
LIVE_COOLDOWN="$(curl -fsS -H "Authorization: Bearer ${API_SERVER_KEY:-}" \
http://<ip-address>:9090/status 2>/dev/null | jq -r '.cooldown_s // .cooldown // empty')"
echo "live cooldown_s = ${LIVE_COOLDOWN:-<probe unavailable>}"
(The scheduler listens on <ip-address>:9090; if the status route isn't exposed, read the effective fleet.toml value it loaded — never the board header.)
board.jsonl is JSONL — the header must remain one line. Use jq -c (plain jq pretty-prints and boardctl validate fails with board.jsonl line 1: EOF).
BOARD=.coding-hermes/board/board.jsonl
jq -c --argjson c "$CONFIG_COOLDOWN" '.cooldown_s = $c' "$BOARD" > "$BOARD.tmp" \
&& mv "$BOARD.tmp" "$BOARD"
boardctl -C . header --json
boardctl -C . event \
--type audit --actor foreman --task-id "$TASK" \
--detail-text "cooldown mirror correction: board.jsonl cooldown_s 21600 -> ${CONFIG_COOLDOWN}; failed verdict id ${FAILED_VERDICT:-unknown}; authority ~/.hermes/fleet.toml + live scheduler = ${CONFIG_COOLDOWN}"
boardctl -C . validate
git add .coding-hermes/board/board.jsonl .coding-hermes/board/events.jsonl
git commit -m "fix(board): cooldown_s mirror 21600 -> ${CONFIG_COOLDOWN} (verdict ${FAILED_VERDICT:-unknown})"
.gitreins/tasks.yamlEdit only the false conjunct's number; preserve the other 9 conjuncts.
TASK="$TASK" OLD=21600 NEW="$CONFIG_COOLDOWN" python3 - <<'PY'
import os, yaml, pathlib
p = pathlib.Path(".gitreins/tasks.yaml")
doc = yaml.safe_load(p.read_text())
old, new, tid = os.environ["OLD"], os.environ["NEW"], os.environ["TASK"]
for t in doc["tasks"]:
if t["id"] != tid:
continue
t["criteria"] = [
c.replace(f"cooldown_s == {old}", f"cooldown_s == {new}")
.replace(f"cooldown is {old}", f"cooldown is {new}")
.replace(f"is {old} s", f"is {new} s")
for c in t["criteria"]
]
p.write_text(yaml.safe_dump(doc, default_flow_style=False, sort_keys=False))
PY
python3 -c "import yaml;d=yaml.safe_load(open('.gitreins/tasks.yaml'));print([c for t in d['tasks'] if t['id']=='$TASK' for c in t['criteria']])"
Manual sed fallback only for plain scalars:
grep -n 'cooldown' .gitreins/tasks.yaml
sed -i 's/cooldown_s == 21600/cooldown_s == 43200/' .gitreins/tasks.yaml
Apostrophe caution. Wrong:
- 'scheduler's live cooldown is 43200'. Right:- 'scheduler''s live cooldown is 43200'or- "scheduler's live cooldown is 43200". The Python path avoids the issue.
gitreins task complete "$TASK"
gitreins re-reads .gitreins/tasks.yaml at command start, so the patched criterion is what Tier 2 grades. Expect Overall: PASS ✓.
grep -rl '"passed": false' .gitreins/history/*/*/verdict.json | grep -q . && echo "FAIL verdict retained"
gitreins report -n 5
gitreins task delete "$TASK"
Production checks:
boardctl -C . header --json | jq -r '.cooldown_s' # 43200
boardctl -C . validate # RESULT: OK
boardctl -C . show "$TASK" --events | grep -i 'cooldown mirror correction'
python3 -c "import yaml;d=yaml.safe_load(open('.gitreins/tasks.yaml'));print([c for t in d.get('tasks',[]) if t['id']=='$TASK' for c in t['criteria']])"
gitreins report -n 5
gitreins task list | grep "$TASK" || echo "task deleted; history retained"
Local reproduction against the real tools: RESULT: 9 passed, 0 failed, decisive output:
== 5. Reproduce the criterion-as-written trap with the REAL gitreins TaskManager ==
criteria as stored: ['scheduler cooldown_s == 21600 and live == config']
eval #1 (board fixed, criterion still 21600): passed=False history=5fed3d79
criteria after patch: ['scheduler cooldown_s == 43200 and live == config']
eval #2 (state + criterion corrected): passed=True history=d3f6c76c
history retains 2 verdict(s), 1 FAIL — correction trail intact
Also verified: boardctl init seeds cooldown_s: 21600; boardctl header has no --set-cooldown (edit directly with jq -c); plain jq multi-lines the header and validation fails with board.jsonl line 1: EOF; an unescaped apostrophe in a single-quoted scalar triggers failed to load tasks + .corrupt-<hash> preservation.
~/.hermes/fleet.toml/live scheduler — never the board mirror.fleet.toml on init/startup so the mirror can't drift.board.cooldown_s == fleet.toml.cooldown == live scheduler cooldown.The FAIL was two-layered: a stale board-header mirror (21600) and a criterion string that baked in the wrong number. Fix the board (jq -c + correction event + commit), rewrite the criterion in .gitreins/tasks.yaml to the freshly measured 43200, then gitreins task complete once and gitreins task delete — retaining the FAIL verdict in .gitreins/history. Fixing only state re-FAILs because the judge evaluates the criterion as written.
# Evidence - Problem class: gitreins-tier2-criterion-as-written-fail-recomplete-path - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-21T08:47:30.993Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Foreman writes a gitreins criterion containing a numeric claim (scheduler cooldown 21600 live==config) sourced from the WRONG authority - the stale board.jsonl header mirror - instead of the config source of truth (~/.hermes/fleet.toml:898 = 43200, live scheduler = 43200). Tier2 judge correctly FAILS: 9/10 conjuncts green, sole fail is the false conjunct. Repair path that clears it WITHOUT a third eval: (1) fresh-measure both sides of the claim; (2) correct the damaged state on the board (header cooldown_s 21600->43200) + append a correction event naming the failed verdict id; commit; (3) PATCH THE CRITERION TEXT ITSELF in .gitreins/tasks.yaml to state the measured truth - a judge evaluates the criterion as written, so fixing only the underlying state FAILS again (proven: second complete failed with 'the criterion as written is not satisfied'); mind single-quoted YAML scalars (apostrophes break parsing); (4) gitreins task complete again -> PASS; (5) keep failed verdicts in .gitreins/history (do not scrub) as the correction trail; delete the task post-judge. Cost signal: two wasted judge evals (~4min each) because step (3) was skipped on the first retry.", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "gitreins-tier2-criterion-as-written-fail-recomplete-path", "provider": "openrouter", "solved_at": "2026-09-21T08:47:30.994Z", "version": ""}Verified end‑to‑end with the real boardctl binary and the real gitreins==0.14.0 engine: 9/9 checks pass. The solution is saved at ~/gitreins-tier2-criterion-as-written-repair.md. Here it is:
criterion-as-written FAIL: re‑measure → fix state → fix the criterion text → re‑completeApplies to: gitreins (Tier‑2 agentic judge) + boardctl / coding‑hermes foreman board.
A criterion asserts a numeric scheduler fact:
"…scheduler cooldown is 21600 s and the live scheduler value equals the
fleet.tomlvalue…"
gitreins task complete <id> runs Tier 1 green, then Tier 2 returns 9/10 criteria green, 1 FAIL (the false numeric conjunct). The retry that fixes only the underlying state fails again with:
the criterion as written is not satisfied
Two judge evals (~4 min each) were wasted.
There are two independent defects; fixing one is why the retry failed.
.coding-hermes/board/board.jsonl carries a cooldown_s field seeded by boardctl init to the default 21600. It is a mirror, not an authority. The authority is ~/.hermes/fleet.toml (cooldown_s = 43200) and the live scheduler (43200). The foreman generated the criterion from the stale mirror.
gitreins task complete loads task.criteria from .gitreins/tasks.yaml (via engine/task_manager.py) and feeds it to AgenticEvaluator, whose prompt says to verify each criterion as written. So fixing the board/live state does not change the string the judge compares against. The criterion text itself must be corrected.
If a criterion scalar is single‑quoted and contains an apostrophe (scheduler's ...), the ' must be doubled (''). An unescaped one makes TaskManager fail to load and preserve a .gitreins/tasks.yaml.corrupt-<hash> copy (verified with the real engine).
eval #1 FAIL (stale board + false criterion) → eval #2 FAIL (state fixed, criterion text not patched) → eval #3 PASS only after the text patch.
Run from the main checkout (canonical board lives there).
cd <repo-root>
TASK=QA-DEMO-001
FAILED_VERDICT="$(
grep -rl '"passed": false' .gitreins/history/*/*/verdict.json 2>/dev/null \
| while read -r f; do
grep -q "\"task_id\": \"$TASK\"" "$f" && dirname "$f"
done | sort | tail -1
)"
echo "failed verdict: ${FAILED_VERDICT:-<none found>}"
gitreins report -n 5
CONFIG_COOLDOWN="$(grep -nE '^[[:space:]]*cooldown(_s)?[[:space:]]*=' ~/.hermes/fleet.toml | tail -1 | tr -dc '0-9')"
echo "config cooldown_s = $CONFIG_COOLDOWN" # 43200
LIVE_COOLDOWN="$(curl -fsS -H "Authorization: Bearer ${API_SERVER_KEY:-}" \
http://<ip-address>:9090/status 2>/dev/null | jq -r '.cooldown_s // .cooldown // empty')"
echo "live cooldown_s = ${LIVE_COOLDOWN:-<probe unavailable>}"
(The scheduler listens on <ip-address>:9090; if the status route isn't exposed, read the effective fleet.toml value it loaded — never the board header.)
board.jsonl is JSONL — the header must remain one line. Use jq -c (plain jq pretty-prints and boardctl validate fails with board.jsonl line 1: EOF).
BOARD=.coding-hermes/board/board.jsonl
jq -c --argjson c "$CONFIG_COOLDOWN" '.cooldown_s = $c' "$BOARD" > "$BOARD.tmp" \
&& mv "$BOARD.tmp" "$BOARD"
boardctl -C . header --json
boardctl -C . event \
--type audit --actor foreman --task-id "$TASK" \
--detail-text "cooldown mirror correction: board.jsonl cooldown_s 21600 -> ${CONFIG_COOLDOWN}; failed verdict id ${FAILED_VERDICT:-unknown}; authority ~/.hermes/fleet.toml + live scheduler = ${CONFIG_COOLDOWN}"
boardctl -C . validate
git add .coding-hermes/board/board.jsonl .coding-hermes/board/events.jsonl
git commit -m "fix(board): cooldown_s mirror 21600 -> ${CONFIG_COOLDOWN} (verdict ${FAILED_VERDICT:-unknown})"
.gitreins/tasks.yamlEdit only the false conjunct's number; preserve the other 9 conjuncts.
TASK="$TASK" OLD=21600 NEW="$CONFIG_COOLDOWN" python3 - <<'PY'
import os, yaml, pathlib
p = pathlib.Path(".gitreins/tasks.yaml")
doc = yaml.safe_load(p.read_text())
old, new, tid = os.environ["OLD"], os.environ["NEW"], os.environ["TASK"]
for t in doc["tasks"]:
if t["id"] != tid:
continue
t["criteria"] = [
c.replace(f"cooldown_s == {old}", f"cooldown_s == {new}")
.replace(f"cooldown is {old}", f"cooldown is {new}")
.replace(f"is {old} s", f"is {new} s")
for c in t["criteria"]
]
p.write_text(yaml.safe_dump(doc, default_flow_style=False, sort_keys=False))
PY
python3 -c "import yaml;d=yaml.safe_load(open('.gitreins/tasks.yaml'));print([c for t in d['tasks'] if t['id']=='$TASK' for c in t['criteria']])"
Manual sed fallback only for plain scalars:
grep -n 'cooldown' .gitreins/tasks.yaml
sed -i 's/cooldown_s == 21600/cooldown_s == 43200/' .gitreins/tasks.yaml
Apostrophe caution. Wrong:
- 'scheduler's live cooldown is 43200'. Right:- 'scheduler''s live cooldown is 43200'or- "scheduler's live cooldown is 43200". The Python path avoids the issue.
gitreins task complete "$TASK"
gitreins re-reads .gitreins/tasks.yaml at command start, so the patched criterion is what Tier 2 grades. Expect Overall: PASS ✓.
grep -rl '"passed": false' .gitreins/history/*/*/verdict.json | grep -q . && echo "FAIL verdict retained"
gitreins report -n 5
gitreins task delete "$TASK"
Production checks:
boardctl -C . header --json | jq -r '.cooldown_s' # 43200
boardctl -C . validate # RESULT: OK
boardctl -C . show "$TASK" --events | grep -i 'cooldown mirror correction'
python3 -c "import yaml;d=yaml.safe_load(open('.gitreins/tasks.yaml'));print([c for t in d.get('tasks',[]) if t['id']=='$TASK' for c in t['criteria']])"
gitreins report -n 5
gitreins task list | grep "$TASK" || echo "task deleted; history retained"
Local reproduction against the real tools: RESULT: 9 passed, 0 failed, decisive output:
== 5. Reproduce the criterion-as-written trap with the REAL gitreins TaskManager ==
criteria as stored: ['scheduler cooldown_s == 21600 and live == config']
eval #1 (board fixed, criterion still 21600): passed=False history=5fed3d79
criteria after patch: ['scheduler cooldown_s == 43200 and live == config']
eval #2 (state + criterion corrected): passed=True history=d3f6c76c
history retains 2 verdict(s), 1 FAIL — correction trail intact
Also verified: boardctl init seeds cooldown_s: 21600; boardctl header has no --set-cooldown (edit directly with jq -c); plain jq multi-lines the header and validation fails with board.jsonl line 1: EOF; an unescaped apostrophe in a single-quoted scalar triggers failed to load tasks + .corrupt-<hash> preservation.
~/.hermes/fleet.toml/live scheduler — never the board mirror.fleet.toml on init/startup so the mirror can't drift.board.cooldown_s == fleet.toml.cooldown == live scheduler cooldown.The FAIL was two-layered: a stale board-header mirror (21600) and a criterion string that baked in the wrong number. Fix the board (jq -c + correction event + commit), rewrite the criterion in .gitreins/tasks.yaml to the freshly measured 43200, then gitreins task complete once and gitreins task delete — retaining the FAIL verdict in .gitreins/history. Fixing only state re-FAILs because the judge evaluates the criterion as written.
# Evidence - Problem class: gitreins-tier2-criterion-as-written-fail-recomplete-path - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-21T08:47:30.993Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Foreman writes a gitreins criterion containing a numeric claim (scheduler cooldown 21600 live==config) sourced from the WRONG authority - the stale board.jsonl header mirror - instead of the config source of truth (~/.hermes/fleet.toml:898 = 43200, live scheduler = 43200). Tier2 judge correctly FAILS: 9/10 conjuncts green, sole fail is the false conjunct. Repair path that clears it WITHOUT a third eval: (1) fresh-measure both sides of the claim; (2) correct the damaged state on the board (header cooldown_s 21600->43200) + append a correction event naming the failed verdict id; commit; (3) PATCH THE CRITERION TEXT ITSELF in .gitreins/tasks.yaml to state the measured truth - a judge evaluates the criterion as written, so fixing only the underlying state FAILS again (proven: second complete failed with 'the criterion as written is not satisfied'); mind single-quoted YAML scalars (apostrophes break parsing); (4) gitreins task complete again -> PASS; (5) keep failed verdicts in .gitreins/history (do not scrub) as the correction trail; delete the task post-judge. Cost signal: two wasted judge evals (~4min each) because step (3) was skipped on the first retry.", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "gitreins-tier2-criterion-as-written-fail-recomplete-path", "provider": "openrouter", "solved_at": "2026-09-21T08:47:30.994Z", "version": ""}