diff --git a/campaign/board.md b/campaign/board.md index 0041e38..8119cd3 100644 --- a/campaign/board.md +++ b/campaign/board.md @@ -58,7 +58,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | P2-B3 ProcessResearch (behavioral, RNG) | phase2 | mapped | high | 85% | 2026-09-08 | LIVE, PARTIAL PASS: 15 calls compared, 13 zero-divergence; RNG post-state matched 14/15 incl. every roll (validates MT19937 + draw mapping + odds together). 2 divergences are the declared SetResearched boundary. ORACLE FAILS by exactly one item across 40,300: an unposted EVENT_RESEARCH_OVERBUDGET - compare was blind because the event list was never a declared region. fpu_cw=0x127f => 53-bit double, x87 question SETTLED. No Zuul in the save: double roll still disassembly-only | | RNG signatures (Ghidra) | meta | verified | high | 100% | 2026-09-08 | Seed/Twist/NextFloat/NextInt verified; draw = y/(2^32-1); NextInt [0,n] inclusive; lazy twist; left@+0x9c4. RUNTIME CONFIRMED: fpu_cw=0x127f (53-bit double, round-nearest) - our next_float model is right, float_from_pc24 is an unused contingency | | engine: game/effects | engine | verified | high | 100% | 2026-09-08 | merged: TechId enum (196 slots @10000+i), 44 ids with typed strategic effects, species flag bits, ApplyTechEffect; 254 checks | -| VM140 exclusivity (lab rule) | meta | verified | high | 100% | 2026-09-08 | one agent at a time. Holder: B4 (window open). QUEUE: empty. Windows Update now DISABLED/paused on the VM (was rebooting mid-run). Non-holders build /srv/re-lab/build/sots-engine-, stage dist-, deploy C:\SOTS\shimdist- | +| VM140 exclusivity (lab rule) | meta | verified | high | 100% | 2026-09-08 | one agent at a time. Holder: R-recapture (window open 2026-09-08). QUEUE: empty. Windows Update now DISABLED/paused on the VM (was rebooting mid-run). Non-holders build /srv/re-lab/build/sots-engine-, stage dist-, deploy C:\SOTS\shimdist- | | Zuul double-roll (behavioural) | verify | backlog | — | 0% | 2026-09-08 | CONFIRMED NEEDED: ref-turn2 has only species 0 and 2, so the double roll is verified by disassembly + host tests only. Needs one compare from a species-5 save; the check is just that `left` drops by 2 not 1 | | budget tail coverage (expenses/aid/debt) | verify | backlog | — | 0% | 2026-09-08 | 8 ComputeBudget slots were always 0 in ref-turn2 (no sliders, no aid, no debt, no handicap). Need a save with expense sliders, a debtor and a research-aid treaty to exercise ExpenseTotal + the aid/bonus tail | | hook GetDifficultyMods | meta | backlog | — | 0% | 2026-09-08 | B1 derived the two difficulty rows from trace values (AI maintenance divisor 3, research x1.5) instead of snapshotting them; hook it properly so they stop being constants | @@ -66,5 +66,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | P2-B4 colony + movement (behavioral) | phase2 | verified | high | 100% | 2026-09-08 | LIVE: 36 calls compared, 0 divergences, tracecmp exit 0. Scout's headline: RNG left-delta 0 and mt hash identical on ALL 28 systems (only ProcessRebellion draws; none fired) - fpu_cw 0x127f confirmed. One real fleet move reproduced bit-for-bit; PlanFleetMovement's schedule matched. 3 prototypes + 6 helpers VERIFIED and written back to Ghidra; 22 formula corrections. THREE hook bugs found by reading the trace, not the verdict (stale args from describe_args-before-regions; StrategyServer has TWO bases 4 bytes apart; off_Fleets was a Ghidra-base number used as raw) - each would have given a clean compare that checked nothing. COVERAGE IS THIN: only 3 owned systems, 1 moving fleet, gate traffic all-zero; bats2, plague, rebellion, slaves, terraform, jumps, arrivals untested. No replace mode (input boundary). VM released: main menu, hooks=trace, build b4-fix2-20260908T0615Z | | harness gap: undeclared side-effect lists | verify | verified | high | 100% | 2026-09-08 | FIXED STRUCTURALLY (engine 3f0721f+): compile-time-required Coverage on every descriptor (a hook without one does not compile); Guard regions that catch AND localise undeclared writes (names player+0x2b0, not 'the hash moved'); replace mode now emits records; tracecmp prints coverage on every report + --strict-coverage. A hook claiming 'complete' while a guard caught an undeclared write now counts as a DIVERGENCE. Audit found 23 undeclared side effects: docs/harness-audit.md | | RollResearchEvent draw (behavioural) | verify | backlog | — | 0% | 2026-09-08 | ServerPlayer::RollResearchEvent draws one NextFloat when the completing def is the current target and +0x3b4 is set. Region declared, ours reproduces it, but the branch never fired on ref-turn2 - needs a workload that sets research_roll_pending | -| golden-trace recapture (post-guards) | verify | backlog | — | 0% | 2026-09-08 | region sets changed, so goldens must be recaptured: ProcessResearch (expect NEW divergences - the event defect becoming visible), ComputeBudget (verdict should stay 0), and first captures for OnTechResearched / ServerSystem::ProcessTurn / MoveFleet | +| golden-trace recapture (post-guards) | verify | in-progress | — | 0% | 2026-09-08 | region sets changed, so goldens must be recaptured: ProcessResearch (expect NEW divergences - the event defect becoming visible), ComputeBudget (verdict should stay 0), and first captures for OnTechResearched / ServerSystem::ProcessTurn / MoveFleet | | B1 replace double-run | verify | backlog | — | 0% | 2026-09-08 | ComputeBudget replace mode runs the original a second time to harvest budget slots; ComputeOutput repairs ships in orbit as a side effect, so this is a real per-turn double effect on objects no region covers. Needs a design fix (harvest without re-running, or declare+revert) | +| ReVa MCP link drop (workaround) | meta | verified | high | 100% | 2026-09-08 | The ReVa MCP client link dropped mid-session while the CT111 server stayed healthy (systemd active, :8080 listening, valid key -> 200). `tools/reva_call.py ''` calls the same server over plain HTTP (initialize -> notifications/initialized -> tools/call; replies are SSE with a leading `id:` line, initialize is plain JSON). Key is NEVER stored in the repo: $REVA_KEY, else ~/.claude.json, else ssh to the CT properties file. Use this whenever mcp__plugin_ReVa_ReVa__* is unavailable | +| event posting API | subsystem | in-progress | — | 0% | 2026-09-08 | Closes B3's oracle gap (unposted EVENT_RESEARCH_OVERBUDGET, the ONE item wrong across 40,300) and B2's known gap (ours posts no events, so both clean compares bound economy fields only). Needs: the player event list container, the post/append entry point, the event record layout | diff --git a/tools/reva_call.py b/tools/reva_call.py new file mode 100755 index 0000000..466864d --- /dev/null +++ b/tools/reva_call.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Call a ReVa (Ghidra) MCP tool over plain HTTP. + +Fallback for when the ReVa MCP client link drops but the server on CT111 is fine. +Usage: + tools/reva_call.py '' + tools/reva_call.py --list + +The API key is never stored here. It is read from, in order: + $REVA_KEY, then ~/.claude.json, then CT111's reva-lab.properties over ssh. +""" +import json +import os +import re +import subprocess +import sys +import urllib.request + +URL = os.environ.get("REVA_URL", "http://192.168.10.138:8080/mcp/message") + + +def api_key(): + if os.environ.get("REVA_KEY"): + return os.environ["REVA_KEY"] + cfg = os.path.expanduser("~/.claude.json") + if os.path.exists(cfg): + m = re.search(r'"X-API-Key"\s*:\s*"([0-9a-f]{16,})"', open(cfg, encoding="utf-8").read()) + if m: + return m.group(1) + out = subprocess.run( + ["ssh", "spicy", "pct exec 111 -- grep '^reva.server.options.api.key=' /opt/reva-src/reva-lab.properties"], + capture_output=True, text=True, timeout=30) + m = re.search(r"api\.key=(\S+)", out.stdout) + if m: + return m.group(1) + sys.exit("no ReVa API key found (set $REVA_KEY)") + + +def rpc(key, method, params, session=None, notify=False): + msg = {"jsonrpc": "2.0", "method": method, "params": params} + if not notify: + msg["id"] = 1 + req = urllib.request.Request(URL, data=json.dumps(msg).encode(), headers={ + "X-API-Key": key, + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + **({"Mcp-Session-Id": session} if session else {}), + }) + with urllib.request.urlopen(req, timeout=300) as r: + sid = r.headers.get("Mcp-Session-Id") or session + raw = r.read().decode("utf-8", "replace").strip() + if not raw: + return {}, sid + # tools/* replies come back as SSE (id:/event:/data: lines); initialize is plain JSON. + if not raw.startswith("{"): + raw = "".join(ln[5:].strip() for ln in raw.splitlines() if ln.startswith("data:")) + return json.loads(raw), sid + + +def main(): + if len(sys.argv) < 2: + sys.exit(__doc__) + key = api_key() + _, sid = rpc(key, "initialize", { + "protocolVersion": "2024-11-05", "capabilities": {}, + "clientInfo": {"name": "reva_call", "version": "1"}}) + rpc(key, "notifications/initialized", {}, sid, notify=True) + if sys.argv[1] == "--list": + res, _ = rpc(key, "tools/list", {}, sid) + for t in res.get("result", {}).get("tools", []): + print(f"{t['name']}: {t.get('description','')[:110]}") + return + args = json.loads(sys.argv[2]) if len(sys.argv) > 2 else {} + res, _ = rpc(key, "tools/call", {"name": sys.argv[1], "arguments": args}, sid) + if "error" in res: + print(json.dumps(res["error"], indent=2)) + sys.exit(1) + for c in res.get("result", {}).get("content", []): + print(c.get("text", json.dumps(c))) + + +if __name__ == "__main__": + main()