Skip to content

Commit a52a495

Browse files
authored
fix(#910): split evolution-analysis local triage from private-repo dispatch (#916)
* fix(#910): split evolution-analysis local triage from private-repo dispatch evolution-analysis conflated "no private-repo access" with "cannot do local triage": with no script: of its own, the job fell back to the generic evolution_access_gate.sh default, which skips the ENTIRE agent run (including the token-free Phase 0 local triage pass) whenever GitHub write access is unavailable. The job's prompt and SKILL.md Security section also unconditionally said "ABORT" without scoping it to the GitHub-dependent Phase 1 work, contradicting the Phase 0 carve-out documented earlier in the same skill. Add scripts/evolution_analysis_gate.sh: a dedicated wake-gate that always runs evolution_local_triage.py first (best-effort, never blocking), then delegates the wake/skip decision to the same write-access check every other evolution stage uses (reused via `source`, not duplicated). This guarantees analysis/YYYY-MM-DD.json exists every cycle regardless of GitHub access, while still gating the LLM agent (Phase 1) on write access. Wire it into cron/evolution/analysis.yaml via `script:`, and reword the prompt + SKILL.md Security section so the ABORT applies to Phase 1 only. Tests: new tests/scripts/test_evolution_analysis_gate.py covers push access, read-only access, unauthenticated, and no-gh/no-token scenarios, asserting local triage output is written and the wake decision is correct in every case (61 tests pass across the evolution gate/triage/ audit/skill-integrity suites; ruff check + format clean). * fix(#910): install YAML-declared scripts on reconcile, fail-closed gate fallback Adversarial review of the #910 fix (6fc794e) surfaced two gaps: - CRITICAL: register_evolution_cron.py only refreshed no_agent scripts on the reconcile path (already-registered jobs). An agent job's script: field changing name (evolution_access_gate.sh -> evolution_analysis_gate.sh, exactly what #910's fix does to evolution-analysis) updated the DB record via update_job() but never copied the new file into HERMES_HOME/scripts/, silently no-op'ing the whole fix for any already-registered install. Widened the existing no_agent-only refresh to cover every job with a YAML-declared script:, installed unconditionally before reconcile runs. - MINOR: evolution_analysis_gate.sh defaulted to {"wakeAgent": true} if its sibling evolution_access_gate.sh was missing (a degraded install) — fail-open, defeating the write-access gate's purpose. Now fails closed (does not wake) in that case; Phase 0's local-triage output still stands. - NIT: the sourced evolution_access_gate.sh exports $HERMES_HOME/.env into the caller's environment (set -a). Wrapped the source in a subshell so those exports stay contained — no execve/PATH lookup needed, same empty-PATH-safe property as plain `source`. Tests: added test_changed_script_reconciles_record_and_installs_file (register_evolution_cron.py) and test_missing_access_gate_fails_closed (evolution_analysis_gate.sh) to cover both fixes directly. 93 passed (test_evolution_analysis_gate, test_evolution_access_gate, test_evolution_local_triage, test_register_evolution_cron, test_evolution_analysis_audit, test_evolution_skill_integrity). ruff check + format --check clean on all touched files (pre-existing format drift in unrelated lines of register_evolution_cron.py / test_register_evolution_cron.py left untouched, out of scope). * docs(#910): document why the gate exits 0 unconditionally (not exit \$?) Independent review (agy/Gemini) of b83eba2 flagged the subshell wrap around `source evolution_access_gate.sh` followed by `exit 0` as potentially masking a failing sourced script. Verified against the scheduler contract (cron/scheduler.py:2969): `if _ran_ok and not _parse_wake_gate(...)` — a NONZERO/failed script run is treated as "gate didn't run" and wakes the agent unconditionally, ignoring any JSON printed. So propagating a hypothetical future nonzero exit from evolution_access_gate.sh (it currently always ends in a successful `echo`, so this is moot today) would flip a real failure from "don't wake" into "wake anyway" — the opposite of what this gate exists to prevent. `exit 0` is therefore the deliberate, correct choice, not an oversight; added a comment explaining this so a future reader doesn't "fix" it into an `exit $?` regression. No behavior change — comment only. Re-ran the 56-test suite (all pass).
1 parent 58f89ad commit a52a495

6 files changed

Lines changed: 397 additions & 22 deletions

File tree

cron/evolution/analysis.yaml

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,33 @@ schedule: "0 1,5,9,13,17,21 * * *" # Every 4h (was daily 21:00). Raises process
33
enabled: true
44
mode: PRIVATE
55

6+
# #910: analysis has a token-free LOCAL triage half (Phase 0, reads
7+
# issues/introspection/research sidecars, writes analysis/YYYY-MM-DD.json)
8+
# and a GitHub-dependent full pass (Phase 1). Without this dedicated gate,
9+
# the job fell back to the generic access-gate default, which skipped the
10+
# ENTIRE run — Phase 0 included — whenever GitHub write access was
11+
# unavailable. evolution_analysis_gate.sh always runs Phase 0's local
12+
# triage first (best-effort, never blocking), then gates only the LLM
13+
# agent wake decision on GitHub write access, same as every other stage.
14+
script: evolution_analysis_gate.sh
15+
616
prompt: |
717
You are Hermes Evolution Analyzer.
8-
18+
919
Your task: Analyze all open issues and PRs to prioritize implementation.
10-
20+
1121
Use the evolution-analysis skill for instructions.
12-
22+
1323
Output to: ~/.hermes/evolution/analysis/{current_date}.json
14-
15-
CRITICAL: Verify `gh auth status` works before proceeding — the gh CLI is
16-
the primary auth mechanism. GITHUB_TOKEN is set as fallback. If neither
17-
gh CLI auth nor GITHUB_TOKEN is available, ABORT immediately.
24+
25+
NOTE: The wake-gate script already ran Phase 0 (local, token-free triage)
26+
before you were woken, so analysis/{current_date}.json already exists with
27+
at least a local-triage baseline. You were only woken because GitHub write
28+
access is confirmed — proceed with Phase 1 (the full, GitHub-aware pass)
29+
per the evolution-analysis skill, refining/superseding the local-only
30+
baseline. If mid-run `gh auth status` / GITHUB_TOKEN turns out to be
31+
unavailable after all, ABORT Phase 1 only — the local-triage baseline
32+
already on disk still stands as this cycle's output.
1833
1934
skills:
2035
- evolution/analysis

scripts/evolution_analysis_gate.sh

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/bin/bash
2+
# Evolution-analysis wake-gate (#910).
3+
#
4+
# WHY: evolution-analysis has two independent halves — a LOCAL, file-based
5+
# triage pass (reads issues/introspection/research sidecars, writes
6+
# analysis/YYYY-MM-DD.json) that needs no GitHub token at all, and a
7+
# GitHub-dependent full pass (Phase 1: `gh issue list`, triage/reject/close)
8+
# that needs write access to the repo. Before this fix, evolution-analysis
9+
# was registered WITHOUT its own gate script, so it fell back to the generic
10+
# `evolution_access_gate.sh` default: a job-wide wake-gate that skips the
11+
# LLM agent entirely whenever GitHub write access is unavailable. That meant
12+
# the local-only triage pass never even got a chance to run — the pipeline
13+
# accumulated backlog but produced no analysis/*.json output on days the
14+
# private token was missing/scoped-down (the exact MERGED_ZERO symptom this
15+
# issue reports).
16+
#
17+
# HOW: this gate ALWAYS runs the local triage script first (unconditionally,
18+
# best-effort — a failure here must never block the wake-gate decision below),
19+
# so `analysis/YYYY-MM-DD.json` exists for today regardless of GitHub access.
20+
# It then delegates the wake decision to the same write-access check every
21+
# other evolution stage uses: wake the LLM agent (for the richer, GitHub-
22+
# dependent Phase 1 pass) only when write access is confirmed; otherwise the
23+
# local-only pass already written stands as this cycle's output.
24+
set +e
25+
26+
# Resolve our own directory using bash parameter expansion only — NOT
27+
# `dirname`/`cd`/`pwd` (external commands that need PATH resolution). This
28+
# script must locate its sibling scripts even when PATH is empty/restricted
29+
# (the same isolated-PATH convention `tests/scripts/test_evolution_access_gate.py`
30+
# uses), so no external binary can be on the critical path here.
31+
_src="${BASH_SOURCE[0]}"
32+
case "$_src" in
33+
*/*) SCRIPT_DIR="${_src%/*}" ;;
34+
*) SCRIPT_DIR="." ;;
35+
esac
36+
37+
# Phase 0 — local (file-based) triage. No GitHub API calls, no token
38+
# required. Runs unconditionally, before the write-access check, so it can
39+
# never be skipped by a missing/scoped-down token.
40+
if command -v python3 >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/evolution_local_triage.py" ]; then
41+
python3 "$SCRIPT_DIR/evolution_local_triage.py" 2>&1 || true
42+
else
43+
echo "evolution-analysis-gate: python3 or evolution_local_triage.py unavailable — skipping local triage" >&2
44+
fi
45+
46+
# Phase 1 gate — reuse the generic write-access wake-gate verbatim so the
47+
# decision logic (and its tests) live in exactly one place. `source` inside
48+
# a `( ... )` subshell (not `exec bash ...`, not a bare top-level `source`):
49+
# a subshell forks THIS already-running interpreter (no execve, no PATH
50+
# lookup needed — unlike spawning a nested `bash` binary, which would need
51+
# to resolve `bash` via PATH, possibly empty/restricted under a locked-down
52+
# cron PATH or this script's own test harness), while also containing the
53+
# access gate's `.env` exports (it sources `$HERMES_HOME/.env` with `set -a`)
54+
# to the subshell instead of leaking them into this script's own remaining
55+
# environment. The subshell's stdout still flows straight through to ours,
56+
# so its last line is still the `{"wakeAgent": ...}` JSON this script's
57+
# contract requires as ITS last line too.
58+
#
59+
# `exit 0` here (NOT `exit $?`) is deliberate, not exit-code masking: the
60+
# scheduler's wake-gate contract (cron/scheduler.py `_run_job_script` +
61+
# `_parse_wake_gate`) treats a NONZERO/failed script run as "gate could not
62+
# run" and wakes the agent UNCONDITIONALLY, ignoring any JSON it printed —
63+
# the opposite of fail-closed. Propagating a hypothetical future nonzero
64+
# exit from evolution_access_gate.sh (today it always ends in a successful
65+
# `echo`, so this is currently moot) would flip a real failure from "don't
66+
# wake" into "wake anyway", which is the one outcome this whole gate exists
67+
# to prevent. Exiting 0 unconditionally keeps the printed JSON — not the
68+
# process exit code — as the single source of truth for the wake decision.
69+
if [ -f "$SCRIPT_DIR/evolution_access_gate.sh" ]; then
70+
# shellcheck disable=SC1090
71+
( source "$SCRIPT_DIR/evolution_access_gate.sh" )
72+
exit 0
73+
fi
74+
75+
# Fallback: the generic write-access gate isn't installed alongside us — a
76+
# degraded install (register_evolution_cron.py's _install_access_gate runs
77+
# unconditionally on every registration, so this should always be present).
78+
# We cannot confirm write access without it, so fail CLOSED: do not wake the
79+
# LLM agent. Phase 0's local-triage output above already stands as this
80+
# cycle's result, and no tokens are spent on work that might not even be
81+
# pushable.
82+
echo "evolution-analysis-gate: evolution_access_gate.sh not found — cannot confirm write access, not waking" >&2
83+
echo '{"wakeAgent": false}'

scripts/register_evolution_cron.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -408,17 +408,22 @@ def main(argv: list[str]) -> int:
408408

409409
name = str(spec.get("name") or yaml_file.stem).strip()
410410

411-
# Refresh installed no_agent scripts on EVERY run — including for
412-
# already-registered jobs — mirroring the access gate above:
413-
# `hermes update` refreshes the repo checkout, but the scheduler
414-
# executes the copy in HERMES_HOME/scripts; without this refresh the
415-
# installed script stays frozen at whatever version existed when the
416-
# job was first registered.
417-
if (
418-
spec.get("no_agent")
419-
and str(spec.get("script") or "").strip()
420-
and not dry_run
421-
):
411+
# Refresh any YAML-declared script on EVERY run — no_agent AND
412+
# per-job agent-gate scripts (Hydra's evolution_hydra_gate.py,
413+
# evolution-analysis's evolution_analysis_gate.sh, etc.) alike —
414+
# including for already-registered jobs, mirroring the access gate
415+
# above: `hermes update` refreshes the repo checkout, but the
416+
# scheduler executes the copy in HERMES_HOME/scripts. Without this
417+
# refresh, two things go stale: (a) the installed script's CONTENT
418+
# stays frozen at whatever version existed when the job was first
419+
# registered, and (b) worse, on a script NAME change (e.g. #910's
420+
# evolution_access_gate.sh -> evolution_analysis_gate.sh) the
421+
# reconcile branch below only updates the job record's `script`
422+
# field — it does NOT install the file — so the job would end up
423+
# pointing at a script that was never copied into
424+
# HERMES_HOME/scripts/ at all. Installing here, unconditionally and
425+
# before that reconcile runs, covers both cases for every job kind.
426+
if str(spec.get("script") or "").strip() and not dry_run:
422427
_install_script(repo_root, str(spec["script"]).strip())
423428

424429
schedule = str(spec.get("schedule") or "").strip()

skills/evolution/evolution-analysis/SKILL.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,17 @@ Save to `~/.hermes/evolution/analysis/YYYY-MM-DD.json`:
441441
442442
## Security
443443
444-
Verify `gh auth status` works before proceeding — the gh CLI is the primary
445-
auth mechanism. If gh CLI auth is unavailable AND GITHUB_TOKEN is not set,
446-
**ABORT**. Do NOT export tokens into the environment — `gh` handles auth via
447-
its own stored credentials.
444+
**This ABORT applies to Phase 1 only — it never applies to Phase 0.** Phase 0
445+
(local triage, see above) reads only local sidecar files and makes no GitHub
446+
API calls, so it has nothing to abort: it always runs and always produces
447+
`analysis/YYYY-MM-DD.json`, private-tool availability notwithstanding (#910 —
448+
this stage previously conflated "no private-repo access" with "cannot do
449+
local triage", aborting the whole run and leaving no output at all on days
450+
the private token was unavailable).
451+
452+
Before starting Phase 1, verify `gh auth status` works — the gh CLI is the
453+
primary auth mechanism. If gh CLI auth is unavailable AND GITHUB_TOKEN is not
454+
set, **ABORT Phase 1 only**: skip the GitHub-dependent triage/reject/close
455+
work and leave Phase 0's local-triage output (already written, per above) as
456+
this cycle's result. Do NOT export tokens into the environment — `gh` handles
457+
auth via its own stored credentials.
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
"""Tests for scripts/evolution_analysis_gate.sh (#910).
2+
3+
evolution-analysis has two independent halves:
4+
5+
* Phase 0 — a LOCAL, file-based triage pass (``evolution_local_triage.py``)
6+
that reads issues/introspection/research sidecars and writes
7+
``analysis/YYYY-MM-DD.json``. It needs no GitHub token or ``gh`` at all.
8+
* Phase 1 — a GitHub-dependent full pass (``gh issue list``, triage/reject/
9+
close) that needs WRITE access to the repo.
10+
11+
Before this fix, evolution-analysis had no gate script of its own, so it
12+
inherited the generic ``evolution_access_gate.sh`` default: a job-wide
13+
wake-gate that skips the *entire* agent run — Phase 0 included — whenever
14+
GitHub write access is unavailable. That meant the token-free local triage
15+
pass never ran on days the private token was missing/scoped-down, even
16+
though it needs no GitHub access whatsoever.
17+
18+
This suite asserts the fix's two guarantees independently:
19+
20+
1. ``analysis/YYYY-MM-DD.json`` (local triage output) is written regardless
21+
of GitHub write-access outcome — with push access, without push access,
22+
and with no ``gh``/token at all.
23+
2. The wake-gate decision (last stdout line) still correctly reflects
24+
write access, exactly like ``evolution_access_gate.sh`` alone.
25+
"""
26+
27+
from __future__ import annotations
28+
29+
import json
30+
import os
31+
import shutil
32+
import stat
33+
import subprocess
34+
from pathlib import Path
35+
36+
REPO_ROOT = Path(__file__).resolve().parents[2]
37+
GATE = REPO_ROOT / "scripts" / "evolution_analysis_gate.sh"
38+
BASH = shutil.which("bash") or "/bin/bash"
39+
40+
41+
def _write_fake_gh(bin_dir: Path, *, perms: str | None, authed: bool = True) -> None:
42+
"""Install a fake `gh` that answers `api user` and `api repos/...`."""
43+
if perms is None:
44+
perms = "{}"
45+
user_branch = 'echo "tester"\n exit 0' if authed else "exit 1"
46+
script = f"""#!/bin/bash
47+
if [ "$1" = "api" ] && [ "$2" = "user" ]; then
48+
{user_branch}
49+
fi
50+
if [ "$1" = "api" ]; then
51+
case "$2" in
52+
repos/*) printf '%s' '{perms}'; exit 0 ;;
53+
esac
54+
fi
55+
exit 1
56+
"""
57+
gh = bin_dir / "gh"
58+
gh.write_text(script)
59+
gh.chmod(gh.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
60+
61+
62+
def _run_gate(tmp_path: Path, bin_dir: Path, gate: Path = GATE) -> tuple[str, Path]:
63+
"""Run the gate; return (last stdout line, evolution dir).
64+
65+
Unlike ``test_evolution_access_gate.py`` (which tests a script with no
66+
dependency beyond ``gh``/``curl``), this gate also runs
67+
``evolution_local_triage.py`` via ``python3`` — so PATH here PREPENDS
68+
``bin_dir`` (our fake ``gh``, or nothing) onto the real, inherited PATH
69+
rather than replacing it. That keeps the real ``python3`` (and whatever
70+
interpreter/shim mechanism it needs on the host) resolvable exactly as
71+
it would be in production, while still fully controlling which ``gh``
72+
the gate sees — the fake one in ``bin_dir`` always shadows any real one.
73+
74+
``gate`` defaults to the real ``scripts/evolution_analysis_gate.sh`` but
75+
can point at a copy in an isolated directory (e.g. to test behavior when
76+
a sibling script like ``evolution_access_gate.sh`` is deliberately absent).
77+
"""
78+
home = tmp_path / "hermes_home"
79+
evolution_dir = home / "evolution"
80+
evolution_dir.mkdir(parents=True, exist_ok=True)
81+
env = dict(os.environ)
82+
env["PATH"] = f"{bin_dir}{os.pathsep}{os.environ.get('PATH', '')}"
83+
env["HERMES_HOME"] = str(home)
84+
env["GITHUB_EVOLUTION_REPO"] = "Owner/repo"
85+
for k in ("GITHUB_TOKEN", "GITHUB_PRIVATE_TOKEN"):
86+
env.pop(k, None)
87+
proc = subprocess.run(
88+
[BASH, str(gate)],
89+
capture_output=True,
90+
text=True,
91+
env=env,
92+
)
93+
assert proc.returncode == 0, proc.stderr
94+
return proc.stdout.strip().splitlines()[-1], evolution_dir
95+
96+
97+
def _wakes(line: str) -> bool:
98+
return json.loads(line) == {"wakeAgent": True}
99+
100+
101+
def _analysis_json(evolution_dir: Path) -> dict:
102+
analysis_dir = evolution_dir / "analysis"
103+
files = list(analysis_dir.glob("*.json"))
104+
assert len(files) == 1, f"expected exactly one analysis file, found {files}"
105+
return json.loads(files[0].read_text(encoding="utf-8"))
106+
107+
108+
def test_push_access_wakes_and_writes_local_triage(tmp_path: Path) -> None:
109+
bin_dir = tmp_path / "bin"
110+
bin_dir.mkdir()
111+
_write_fake_gh(
112+
bin_dir, perms='{"admin":false,"maintain":false,"push":true,"pull":true}'
113+
)
114+
line, evo_dir = _run_gate(tmp_path, bin_dir)
115+
assert _wakes(line)
116+
result = _analysis_json(evo_dir)
117+
assert result["local_triage"] is True
118+
119+
120+
def test_read_only_account_does_not_wake_but_still_writes_local_triage(
121+
tmp_path: Path,
122+
) -> None:
123+
"""The exact bug this issue reports: reachable but push:false must still
124+
let Phase 0 (local triage) produce output — only the wake decision (Phase
125+
1, the LLM agent) should be gated on write access."""
126+
bin_dir = tmp_path / "bin"
127+
bin_dir.mkdir()
128+
_write_fake_gh(
129+
bin_dir, perms='{"admin":false,"maintain":false,"push":false,"pull":true}'
130+
)
131+
line, evo_dir = _run_gate(tmp_path, bin_dir)
132+
assert not _wakes(line)
133+
result = _analysis_json(evo_dir)
134+
assert result["local_triage"] is True
135+
136+
137+
def test_no_gh_no_token_does_not_wake_but_still_writes_local_triage(
138+
tmp_path: Path,
139+
) -> None:
140+
"""No gh on PATH and no env token: cannot confirm write access, so the
141+
agent must not wake — but local triage needs neither and must still run.
142+
This is the core regression this issue is about (#910)."""
143+
bin_dir = tmp_path / "bin"
144+
bin_dir.mkdir() # deliberately empty: no gh, no curl
145+
line, evo_dir = _run_gate(tmp_path, bin_dir)
146+
assert not _wakes(line)
147+
result = _analysis_json(evo_dir)
148+
assert result["local_triage"] is True
149+
150+
151+
def test_unauthenticated_does_not_wake_but_still_writes_local_triage(
152+
tmp_path: Path,
153+
) -> None:
154+
bin_dir = tmp_path / "bin"
155+
bin_dir.mkdir()
156+
_write_fake_gh(bin_dir, perms="{}", authed=False)
157+
line, evo_dir = _run_gate(tmp_path, bin_dir)
158+
assert not _wakes(line)
159+
result = _analysis_json(evo_dir)
160+
assert result["local_triage"] is True
161+
162+
163+
def test_missing_access_gate_fails_closed(tmp_path: Path) -> None:
164+
"""If evolution_access_gate.sh is somehow absent (a degraded install —
165+
register_evolution_cron.py's _install_access_gate runs unconditionally,
166+
so this should never normally happen), we cannot confirm write access.
167+
The gate must fail CLOSED (not wake) rather than default to waking the
168+
agent — waking unconditionally would defeat the entire point of the
169+
write-access gate."""
170+
bin_dir = tmp_path / "bin"
171+
bin_dir.mkdir()
172+
fake_scripts = tmp_path / "fake_scripts"
173+
fake_scripts.mkdir()
174+
shutil.copy(GATE, fake_scripts / GATE.name)
175+
shutil.copy(
176+
REPO_ROOT / "scripts" / "evolution_local_triage.py",
177+
fake_scripts / "evolution_local_triage.py",
178+
)
179+
# Deliberately do NOT copy evolution_access_gate.sh alongside it.
180+
line, evo_dir = _run_gate(tmp_path, bin_dir, gate=fake_scripts / GATE.name)
181+
assert not _wakes(line)
182+
result = _analysis_json(evo_dir)
183+
assert result["local_triage"] is True
184+
185+
186+
def test_local_triage_reflects_real_sidecars(tmp_path: Path) -> None:
187+
"""End-to-end: a filed issue in the issues/ sidecar surfaces in the
188+
gate-produced analysis JSON even when GitHub write access is denied."""
189+
bin_dir = tmp_path / "bin"
190+
bin_dir.mkdir()
191+
_write_fake_gh(bin_dir, perms='{"push":false}')
192+
home = tmp_path / "hermes_home"
193+
issues_dir = home / "evolution" / "issues"
194+
issues_dir.mkdir(parents=True)
195+
(issues_dir / "2026-07-08.json").write_text(
196+
json.dumps({
197+
"date": "2026-07-08",
198+
"proposals": [
199+
{
200+
"title": "Alpha",
201+
"decision": "filed",
202+
"issue": 100,
203+
"priority_score": 1.5,
204+
"impact": 0.8,
205+
"effort": 0.3,
206+
}
207+
],
208+
})
209+
)
210+
line, evo_dir = _run_gate(tmp_path, bin_dir)
211+
assert not _wakes(line)
212+
result = _analysis_json(evo_dir)
213+
nums = [s["issue_number"] for s in result["selected_for_implementation"]]
214+
assert 100 in nums

0 commit comments

Comments
 (0)