@tachikomagundam/abathur 0.2.7 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/evolve/run-bench.js +7 -1
- package/dist/core/stats.js +7 -0
- package/dist/test/stats.test.js +17 -0
- package/docs/AGENTISM.md +58 -0
- package/docs/release/README.md +9 -0
- package/docs/release/TEMPLATE.md +10 -0
- package/docs/release/v0.2.7.md +7 -0
- package/docs/release/v0.2.8.md +8 -0
- package/graders/pcb-agent/bench-approver.py +77 -0
- package/graders/pcb-agent/grader.mjs +130 -0
- package/graders/pcb-agent/mutate.sh +165 -0
- package/graders/pcb-agent/r20-bench.sh +27 -0
- package/graders/pcb-agent/reset-sandbox.sh +7 -0
- package/graders/pcb-agent/run-scenario-r20.sh +77 -0
- package/graders/pcb-agent/run-scenario.sh +94 -0
- package/graders/pcb-agent/seed/esp32s3-r12-BRIEF.txt +51 -0
- package/graders/pcb-agent/seed/spec.json +129 -0
- package/graders/pcb-agent/seed/tools/board_author_pcb.py +237 -0
- package/graders/pcb-agent/seed/tools/board_author_sch.py +118 -0
- package/graders/pcb-agent/seed/tools/board_finalize.sh +23 -0
- package/graders/pcb-agent/seed/tools/board_gate.py +69 -0
- package/graders/pcb-agent/seed/tools/board_gate_pcb.py +23 -0
- package/graders/pcb-agent/seed/tools/board_route.py +324 -0
- package/graders/pcb-agent/seed/tools/board_zone_pour.py +312 -0
- package/graders/pcb-agent/seed/tools/claims_lint.py +417 -0
- package/graders/pcb-agent/seed-r20-arena.sh +54 -0
- package/graders/pcb-agent/seed-sandbox.sh +13 -0
- package/package.json +1 -1
- package/plugin/abathur.ts +2 -2
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# ============================================================================
|
|
3
|
+
# claims_lint.py — RM-26 (round-4) + RM-37 v2 (round-6): mechanical claim
|
|
4
|
+
# verification for reports/attestations/receipts.
|
|
5
|
+
# ----------------------------------------------------------------------------
|
|
6
|
+
# v1 (RM-26): EVERY absolute-path claim that appears with a 64-hex token on the
|
|
7
|
+
# same line (+/-60 chars) must resolve to a file whose RAW sha256 equals the
|
|
8
|
+
# claim; bare path tokens inside backticks on evidence/ledger/receipt context
|
|
9
|
+
# lines must at least exist.
|
|
10
|
+
#
|
|
11
|
+
# v2 (RM-37, RC-2 / incident #4 root cause): cited HISTORY (session/message
|
|
12
|
+
# rows and dispatch/ledger/receipt ids) bypassed v1 because v1 only verified
|
|
13
|
+
# FILE PATHS. An attestation could cite a fabricated ses_/msg_/receipt id and
|
|
14
|
+
# v1 stayed green. In --claims mode v2 additionally requires that every such
|
|
15
|
+
# identifier RESOLVE against live sources:
|
|
16
|
+
# * ses_<id> -> a real row in the `session` table of the authoritative
|
|
17
|
+
# opencode DB opened READ-ONLY (sqlite URI mode=ro).
|
|
18
|
+
# * msg_<id> -> a real row in the `message` table of that DB.
|
|
19
|
+
# * pcb-dispatch-<...>, denied-<...> -> a file whose name matches the id
|
|
20
|
+
# under an allowlisted dispatch/ledger dir.
|
|
21
|
+
# * round-approval-<n>-<UTC> -> a receipt file of that exact name
|
|
22
|
+
# under an allowlisted capabilities dir.
|
|
23
|
+
# Any unresolved identifier -> exit 1 listing every miss. v1 path+sha behaviour
|
|
24
|
+
# is fully preserved and runs in BOTH modes; --report keeps the historical
|
|
25
|
+
# (path/sha-only) surface with no DB dependency (backward compatible, <2s).
|
|
26
|
+
#
|
|
27
|
+
# Usage:
|
|
28
|
+
# python3 tools/claims_lint.py --report FILE [--base DIR] # v1 surface
|
|
29
|
+
# python3 tools/claims_lint.py --claims FILE [--base DIR] [--db PATH] # v1 + ids
|
|
30
|
+
# --db authoritative opencode DB for session/message resolution. Default:
|
|
31
|
+
# $OPENCODE_DB if it exists, else $XDG_DATA_HOME|~/.local/share /opencode/
|
|
32
|
+
# opencode.db. Always opened `mode=ro`. Unreadable/missing DB is a
|
|
33
|
+
# fail-closed ERROR (every ses_/msg_ claim then counts as unresolved).
|
|
34
|
+
#
|
|
35
|
+
# v3 counts extension (round-10, R9-07: orchestrator prose child-count drift —
|
|
36
|
+
# receipts/ledgers are authoritative over prose):
|
|
37
|
+
# python3 tools/claims_lint.py --counts FILE --dir D [--dir D2 ...]
|
|
38
|
+
# [--since YYYY-MM-DDTHH:MM:SSZ]
|
|
39
|
+
# For every line matching a NUMERIC English count claim of the form
|
|
40
|
+
# "<N> children", "<N> dispatched children", "<N> receipts" or
|
|
41
|
+
# "<N> ledger lines" (case-insensitive), N is compared against a
|
|
42
|
+
# mechanically derived count over the explicit --dir set:
|
|
43
|
+
# children / dispatched children -> dispatch RECEIPT files (*.json that do
|
|
44
|
+
# NOT start with "denied-") carrying a non-empty "child_session_id";
|
|
45
|
+
# receipts -> the same receipt files, counted;
|
|
46
|
+
# ledger lines -> ledger files (name starts with
|
|
47
|
+
# "denied-" and ends with ".json").
|
|
48
|
+
# --since (UTC ISO-8601) filters files by modification time >= since
|
|
49
|
+
# (fail-closed: a malformed --since is a hard error). Any mismatch is a
|
|
50
|
+
# lint ERROR listing claimed vs actual per line; exit 1 iff any mismatch.
|
|
51
|
+
# v1/v2 behavior is UNTOUCHED and --claims stays the default full surface;
|
|
52
|
+
# --counts is an independent mode (no path/DB coupling, and the count modes
|
|
53
|
+
# never run the DB resolver).
|
|
54
|
+
# ============================================================================
|
|
55
|
+
import argparse
|
|
56
|
+
import hashlib
|
|
57
|
+
import json
|
|
58
|
+
import os
|
|
59
|
+
import re
|
|
60
|
+
import sqlite3
|
|
61
|
+
import sys
|
|
62
|
+
|
|
63
|
+
# ---- v1 patterns -----------------------------------------------------------
|
|
64
|
+
# ABS_PATH_RE: RM-37 adds the (?!\w) end-guard. Without it the shorter extension
|
|
65
|
+
# alternatives (sh, json) shadow their longer siblings (sha256, jsonl) and truncate
|
|
66
|
+
# a real "...sha256" token to "...sh", producing a false PATH MISSING. The detection
|
|
67
|
+
# semantics (abs path paired with a nearby 64-hex) are otherwise unchanged from RM-26.
|
|
68
|
+
ABS_PATH_RE = re.compile(r"(?<![\w./+-])/[\w][\w./+-]*\.(?:sha256|jsonl|json|patch|diff|yaml|yml|log|txt|md|csv|py|sh|ts)(?!\w)")
|
|
69
|
+
HEX64_RE = re.compile(r"\b[0-9a-f]{64}\b")
|
|
70
|
+
BARE_TICK_RE = re.compile(r"`([\w][\w./+-]*\.(?:json|jsonl|md|txt|py|sh|ts|log|sha256|csv|yaml|yml))`")
|
|
71
|
+
CONTEXT_RE = re.compile(r"evidence|ledger|receipt", re.IGNORECASE)
|
|
72
|
+
HEX_WINDOW = 60
|
|
73
|
+
|
|
74
|
+
# ---- v2 identifier classes (RM-37 step 7) ---------------------------------
|
|
75
|
+
# Each entry: (class-name, regex). Regexes are unanchored; a single line may
|
|
76
|
+
# carry many. The trailing-char classes stop at punctuation/quotes/backticks.
|
|
77
|
+
SESSION_ID_RE = re.compile(r"\bses_[A-Za-z0-9]{20,}")
|
|
78
|
+
MSG_ID_RE = re.compile(r"\bmsg_[A-Za-z0-9]{20,}")
|
|
79
|
+
DISPATCH_ID_RE = re.compile(r"\bpcb-dispatch-[a-z0-9][a-z0-9-]*")
|
|
80
|
+
DENIED_ID_RE = re.compile(r"\bdenied-[a-z0-9][a-z0-9-]*")
|
|
81
|
+
RECEIPT_ID_RE = re.compile(r"\bround-approval-\d+-\d{8}T\d{6}Z")
|
|
82
|
+
|
|
83
|
+
WORKSPACE_ROOT = "/home/lab/workspace"
|
|
84
|
+
# Allowlisted dirs for dispatch/ledger/receipt id resolution (RM-37 step 7).
|
|
85
|
+
DISPATCH_DIRS = [
|
|
86
|
+
os.path.join(WORKSPACE_ROOT, "pcb-control", "dispatches"),
|
|
87
|
+
os.path.join(WORKSPACE_ROOT, "pcb-control", "state", "dispatches"),
|
|
88
|
+
os.path.join(WORKSPACE_ROOT, "pcb-control", "state", "discovery-findings"),
|
|
89
|
+
]
|
|
90
|
+
RECEIPT_DIRS = [
|
|
91
|
+
os.path.join(WORKSPACE_ROOT, "pcb-control", "state", "capabilities"),
|
|
92
|
+
os.path.join(WORKSPACE_ROOT, "harness", "PCB-Agent", "reports", "recovery", "dispatches"),
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def default_db_path():
|
|
97
|
+
env = os.environ.get("OPENCODE_DB")
|
|
98
|
+
if env and os.path.isfile(env):
|
|
99
|
+
return env
|
|
100
|
+
xdg = os.environ.get("XDG_DATA_HOME") or os.path.join(os.path.expanduser("~"), ".local", "share")
|
|
101
|
+
return os.path.join(xdg, "opencode", "opencode.db")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def sha256_file(path):
|
|
105
|
+
h = hashlib.sha256()
|
|
106
|
+
with open(path, "rb") as f:
|
|
107
|
+
for chunk in iter(lambda: f.read(1 << 20), b""):
|
|
108
|
+
h.update(chunk)
|
|
109
|
+
return h.hexdigest()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class IdResolver:
|
|
113
|
+
"""Resolve v2 identifiers against live sources (DB read-only + allowlist)."""
|
|
114
|
+
|
|
115
|
+
def __init__(self, db_path, unverifiable_path=None):
|
|
116
|
+
self.db_path = db_path
|
|
117
|
+
self.unverifiable = {"session": set(), "message": set()}
|
|
118
|
+
self.unverifiable_reason = ""
|
|
119
|
+
if unverifiable_path and os.path.isfile(unverifiable_path):
|
|
120
|
+
with open(unverifiable_path, "r", encoding="utf-8") as f:
|
|
121
|
+
reg = json.load(f)
|
|
122
|
+
self.unverifiable_reason = str(reg.get("reason", ""))
|
|
123
|
+
for kind in ("session", "message"):
|
|
124
|
+
self.unverifiable[kind] = set(reg.get("ids", {}).get(kind, []))
|
|
125
|
+
self._conn = None
|
|
126
|
+
self._db_error = None
|
|
127
|
+
self._dir_index = None
|
|
128
|
+
self._seen = {}
|
|
129
|
+
|
|
130
|
+
# -- DB (mode=ro) ---------------------------------------------------------
|
|
131
|
+
def _open(self):
|
|
132
|
+
if self._conn is not None or self._db_error is not None:
|
|
133
|
+
return self._conn
|
|
134
|
+
try:
|
|
135
|
+
if not os.path.isfile(self.db_path):
|
|
136
|
+
raise OSError("db file not found")
|
|
137
|
+
uri = "file:%s?mode=ro" % self.db_path
|
|
138
|
+
self._conn = sqlite3.connect(uri, uri=True, timeout=10)
|
|
139
|
+
# guard: read-only connection must not be able to write
|
|
140
|
+
self._conn.execute("SELECT 1 FROM sqlite_master LIMIT 1")
|
|
141
|
+
except Exception as e: # fail-closed: a DB error makes ses_/msg_ unresolved
|
|
142
|
+
self._db_error = str(e)
|
|
143
|
+
self._conn = None
|
|
144
|
+
return self._conn
|
|
145
|
+
|
|
146
|
+
def _row_exists(self, table, ident):
|
|
147
|
+
conn = self._open()
|
|
148
|
+
if conn is None:
|
|
149
|
+
return False
|
|
150
|
+
try:
|
|
151
|
+
cur = conn.execute("SELECT 1 FROM %s WHERE id = ? LIMIT 1" % table, (ident,))
|
|
152
|
+
return cur.fetchone() is not None
|
|
153
|
+
except sqlite3.Error:
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
# -- directory allowlist --------------------------------------------------
|
|
157
|
+
def _index(self):
|
|
158
|
+
if self._dir_index is not None:
|
|
159
|
+
return self._dir_index
|
|
160
|
+
entries = {}
|
|
161
|
+
for d in DISPATCH_DIRS + RECEIPT_DIRS:
|
|
162
|
+
try:
|
|
163
|
+
if os.path.isdir(d):
|
|
164
|
+
entries[d] = os.listdir(d)
|
|
165
|
+
except OSError:
|
|
166
|
+
pass
|
|
167
|
+
self._dir_index = entries
|
|
168
|
+
return entries
|
|
169
|
+
|
|
170
|
+
def _file_id_resolves(self, token, dirs):
|
|
171
|
+
idx = self._index()
|
|
172
|
+
for d in dirs:
|
|
173
|
+
for name in idx.get(d, []):
|
|
174
|
+
# exact id, id.<ext>, or an id embedded in a longer ledger name
|
|
175
|
+
if name == token or name.startswith(token + ".") or token in name:
|
|
176
|
+
return True
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
def unverifiable_hit(self, kind, token):
|
|
180
|
+
return kind in self.unverifiable and token in self.unverifiable[kind]
|
|
181
|
+
|
|
182
|
+
def resolve(self, kind, token):
|
|
183
|
+
key = (kind, token)
|
|
184
|
+
if key in self._seen:
|
|
185
|
+
return self._seen[key]
|
|
186
|
+
if kind == "session":
|
|
187
|
+
ok = self._row_exists("session", token)
|
|
188
|
+
elif kind == "message":
|
|
189
|
+
ok = self._row_exists("message", token)
|
|
190
|
+
elif kind == "dispatch":
|
|
191
|
+
ok = self._file_id_resolves(token, DISPATCH_DIRS)
|
|
192
|
+
elif kind == "denied":
|
|
193
|
+
# denied-* are ledger files: match under dispatch/ledger dirs too
|
|
194
|
+
ok = self._file_id_resolves(token, DISPATCH_DIRS + RECEIPT_DIRS)
|
|
195
|
+
elif kind == "receipt":
|
|
196
|
+
ok = self._file_id_resolves(token, RECEIPT_DIRS)
|
|
197
|
+
else:
|
|
198
|
+
ok = False
|
|
199
|
+
self._seen[key] = ok
|
|
200
|
+
return ok
|
|
201
|
+
|
|
202
|
+
def close(self):
|
|
203
|
+
if self._conn is not None:
|
|
204
|
+
try:
|
|
205
|
+
self._conn.close()
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
IDENT_CLASSES = [
|
|
211
|
+
("session", SESSION_ID_RE),
|
|
212
|
+
("message", MSG_ID_RE),
|
|
213
|
+
("dispatch", DISPATCH_ID_RE),
|
|
214
|
+
("denied", DENIED_ID_RE),
|
|
215
|
+
("receipt", RECEIPT_ID_RE),
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def lint(report_path, base_dir, check_ids=False, db_path=None, unverifiable_path=None):
|
|
220
|
+
failures = 0
|
|
221
|
+
claims = 0
|
|
222
|
+
id_claims = 0
|
|
223
|
+
try:
|
|
224
|
+
with open(report_path, encoding="utf-8", errors="replace") as f:
|
|
225
|
+
text = f.read()
|
|
226
|
+
lines = text.splitlines()
|
|
227
|
+
except OSError as e:
|
|
228
|
+
print("FAIL: report unreadable: %s (%s)" % (report_path, e))
|
|
229
|
+
return 1, 0
|
|
230
|
+
report_dir = os.path.dirname(os.path.abspath(report_path))
|
|
231
|
+
|
|
232
|
+
resolver = IdResolver(db_path, unverifiable_path) if check_ids else None
|
|
233
|
+
db_unavailable_noted = False
|
|
234
|
+
try:
|
|
235
|
+
for no, line in enumerate(lines, 1):
|
|
236
|
+
hexes = list(HEX64_RE.finditer(line))
|
|
237
|
+
for m in ABS_PATH_RE.finditer(line):
|
|
238
|
+
path_tok = m.group(0)
|
|
239
|
+
hm = None
|
|
240
|
+
for hx in hexes:
|
|
241
|
+
if hx.start() - m.end() <= HEX_WINDOW and m.start() - hx.end() <= HEX_WINDOW:
|
|
242
|
+
hm = hx
|
|
243
|
+
break
|
|
244
|
+
if not hm:
|
|
245
|
+
continue
|
|
246
|
+
claims += 1
|
|
247
|
+
want = hm.group(0)
|
|
248
|
+
if not os.path.isfile(path_tok):
|
|
249
|
+
print("FAIL line %d: PATH MISSING: %s (claimed sha %s...)" % (no, path_tok, want[:16]))
|
|
250
|
+
failures += 1
|
|
251
|
+
continue
|
|
252
|
+
try:
|
|
253
|
+
have = sha256_file(path_tok)
|
|
254
|
+
except OSError as e:
|
|
255
|
+
print("FAIL line %d: PATH UNREADABLE: %s (%s)" % (no, path_tok, e))
|
|
256
|
+
failures += 1
|
|
257
|
+
continue
|
|
258
|
+
if have != want:
|
|
259
|
+
print("FAIL line %d: SHA256 MISMATCH: %s: claimed %s... disk %s..." % (no, path_tok, want[:16], have[:16]))
|
|
260
|
+
failures += 1
|
|
261
|
+
else:
|
|
262
|
+
print("OK line %d: %s == %s..." % (no, path_tok, want[:16]))
|
|
263
|
+
if CONTEXT_RE.search(line):
|
|
264
|
+
for m in BARE_TICK_RE.finditer(line):
|
|
265
|
+
rel = m.group(1)
|
|
266
|
+
cands = [os.path.join(report_dir, rel), os.path.join(base_dir, rel), rel]
|
|
267
|
+
if any(os.path.isfile(c) for c in cands):
|
|
268
|
+
continue
|
|
269
|
+
print("FAIL line %d: ARTIFACT MISSING (evidence/ledger/receipt context): %s" % (no, rel))
|
|
270
|
+
failures += 1
|
|
271
|
+
|
|
272
|
+
# ---- v2 identifier resolution (only in --claims mode) ----
|
|
273
|
+
if check_ids:
|
|
274
|
+
for kind, rx in IDENT_CLASSES:
|
|
275
|
+
for im in rx.finditer(line):
|
|
276
|
+
token = im.group(0)
|
|
277
|
+
id_claims += 1
|
|
278
|
+
if resolver.unverifiable_hit(kind, token):
|
|
279
|
+
print("NOTE line %d: %s ID %s is ATTESTED-PURGED (declared-unverifiable registry: %s)"
|
|
280
|
+
% (no, kind, token, resolver.unverifiable_reason))
|
|
281
|
+
continue
|
|
282
|
+
if resolver.resolve(kind, token):
|
|
283
|
+
continue
|
|
284
|
+
if kind in ("session", "message") and not db_unavailable_noted and resolver._db_error:
|
|
285
|
+
print("FAIL: session/message DB unreadable for identifier resolution: %s (%s)"
|
|
286
|
+
% (resolver.db_path, resolver._db_error))
|
|
287
|
+
db_unavailable_noted = True
|
|
288
|
+
print("FAIL line %d: UNRESOLVED %s ID: %s (must resolve against the read-only DB / allowlisted dirs)"
|
|
289
|
+
% (no, kind, token))
|
|
290
|
+
failures += 1
|
|
291
|
+
finally:
|
|
292
|
+
if resolver is not None:
|
|
293
|
+
resolver.close()
|
|
294
|
+
|
|
295
|
+
summary = "claims_lint: %d lines; path+hash claims=%d; failures=%d" % (len(lines), claims, failures)
|
|
296
|
+
if check_ids:
|
|
297
|
+
summary += "; id-claims=%d" % id_claims
|
|
298
|
+
print(summary)
|
|
299
|
+
return failures, claims
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# ---- v3 count-claim mode (R9-07) --------------------------------------------
|
|
303
|
+
COUNT_CLAIM_RE = re.compile(
|
|
304
|
+
r"\b(\d+)\s+(dispatched children|children|receipts|ledger[ _-]lines)\b",
|
|
305
|
+
re.IGNORECASE)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _parse_since(s):
|
|
309
|
+
# strict ISO-8601 UTC instant; anything else -> fail closed (None)
|
|
310
|
+
m = re.match(r"^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z)$", s or "")
|
|
311
|
+
if not m:
|
|
312
|
+
return None
|
|
313
|
+
import calendar
|
|
314
|
+
try:
|
|
315
|
+
y, mo, d, h, mi, se = (int(g) for g in m.groups()[:6])
|
|
316
|
+
return calendar.timegm((y, mo, d, h, mi, se, 0, 0, 0))
|
|
317
|
+
except (ValueError, OverflowError):
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def scan_counts(dirs, since_iso=None):
|
|
322
|
+
"""Mechanically derive (receipts, children, ledger_lines) over --dir set."""
|
|
323
|
+
since = None
|
|
324
|
+
if since_iso is not None:
|
|
325
|
+
since = _parse_since(since_iso)
|
|
326
|
+
if since is None:
|
|
327
|
+
raise ValueError("malformed --since (want YYYY-MM-DDTHH:MM:SSZ UTC): %r" % since_iso)
|
|
328
|
+
receipts = 0
|
|
329
|
+
children = 0
|
|
330
|
+
ledger_lines = 0
|
|
331
|
+
for d in dirs:
|
|
332
|
+
if not os.path.isdir(d):
|
|
333
|
+
continue
|
|
334
|
+
for name in sorted(os.listdir(d)):
|
|
335
|
+
p = os.path.join(d, name)
|
|
336
|
+
if not os.path.isfile(p) or not name.endswith(".json"):
|
|
337
|
+
continue
|
|
338
|
+
if since is not None and os.path.getmtime(p) < since:
|
|
339
|
+
continue
|
|
340
|
+
if name.startswith("denied-"):
|
|
341
|
+
ledger_lines += 1
|
|
342
|
+
continue
|
|
343
|
+
receipts += 1
|
|
344
|
+
try:
|
|
345
|
+
with open(p, encoding="utf-8", errors="replace") as f:
|
|
346
|
+
rec = json.load(f)
|
|
347
|
+
cid = rec.get("child_session_id") if isinstance(rec, dict) else None
|
|
348
|
+
if isinstance(cid, str) and cid:
|
|
349
|
+
children += 1
|
|
350
|
+
except (OSError, ValueError):
|
|
351
|
+
pass
|
|
352
|
+
return receipts, children, ledger_lines
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def counts_lint(counts_file, dirs, since_iso=None):
|
|
356
|
+
if not dirs:
|
|
357
|
+
print("counts_lint: --counts requires at least one --dir (fail-closed)")
|
|
358
|
+
return 1
|
|
359
|
+
try:
|
|
360
|
+
actual = scan_counts(dirs, since_iso)
|
|
361
|
+
except ValueError as e:
|
|
362
|
+
print("counts_lint: %s" % e)
|
|
363
|
+
return 1
|
|
364
|
+
receipts, children, ledger_lines = actual
|
|
365
|
+
try:
|
|
366
|
+
with open(counts_file, encoding="utf-8", errors="replace") as f:
|
|
367
|
+
lines = f.read().splitlines()
|
|
368
|
+
except OSError as e:
|
|
369
|
+
print("counts_lint: counts file unreadable: %s (%s)" % (counts_file, e))
|
|
370
|
+
return 1
|
|
371
|
+
failures = 0
|
|
372
|
+
claims = 0
|
|
373
|
+
for no, line in enumerate(lines, 1):
|
|
374
|
+
for m in COUNT_CLAIM_RE.finditer(line):
|
|
375
|
+
claims += 1
|
|
376
|
+
n = int(m.group(1))
|
|
377
|
+
noun = m.group(2).lower().replace("_", " ").replace("-", " ")
|
|
378
|
+
if "child" in noun:
|
|
379
|
+
got, label = children, "children"
|
|
380
|
+
elif "receipt" in noun:
|
|
381
|
+
got, label = receipts, "receipts"
|
|
382
|
+
else:
|
|
383
|
+
got, label = ledger_lines, "ledger lines"
|
|
384
|
+
if n == got:
|
|
385
|
+
print("OK line %d: claimed %d %s == actual %d (dirs=%d)" % (no, n, label, got, len(dirs)))
|
|
386
|
+
else:
|
|
387
|
+
print("FAIL line %d: COUNT MISMATCH: claimed %d %s, mechanically derived actual %d over --dir set"
|
|
388
|
+
% (no, n, label, got))
|
|
389
|
+
failures += 1
|
|
390
|
+
print("counts_lint: %d lines; count-claims=%d; receipts=%d; children=%d; ledger_lines=%d; failures=%d"
|
|
391
|
+
% (len(lines), claims, receipts, children, ledger_lines, failures))
|
|
392
|
+
return 1 if failures else 0
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def main():
|
|
396
|
+
ap = argparse.ArgumentParser(description="RM-26/RM-37 mechanical claim verifier (paths, hashes, and cited session/dispatch/receipt ids) + round-10 R9-07 count-claim verification")
|
|
397
|
+
mode = ap.add_mutually_exclusive_group(required=True)
|
|
398
|
+
mode.add_argument("--report", metavar="FILE", help="v1 surface: path+sha + evidence-context existence checks")
|
|
399
|
+
mode.add_argument("--claims", metavar="FILE", help="v2 surface: --report checks PLUS identifier resolution (DB mode=ro + allowlist)")
|
|
400
|
+
ap.add_argument("--unverifiable", metavar="FILE", help="explicit declared-unverifiable registry (purge victims: resolve + annotate, never silently)")
|
|
401
|
+
mode.add_argument("--counts", metavar="FILE", help="count-claim surface (R9-07): verify '<N> children|dispatched children|receipts|ledger lines' lines against a mechanical count over the --dir set")
|
|
402
|
+
ap.add_argument("--base", default=".", help="base dir for relative backtick paths")
|
|
403
|
+
ap.add_argument("--db", default=None, help="authoritative opencode DB for session/message rows (read-only)")
|
|
404
|
+
ap.add_argument("--dir", action="append", default=None, metavar="DIR", help="count mode: directory scanned (repeatable; e.g. dispatch receipts dir, ledger dir)")
|
|
405
|
+
ap.add_argument("--since", default=None, metavar="UTC", help="count mode: only count files modified at/after this UTC instant (ISO-8601)")
|
|
406
|
+
a = ap.parse_args()
|
|
407
|
+
if a.counts:
|
|
408
|
+
sys.exit(counts_lint(a.counts, a.dir or [], a.since))
|
|
409
|
+
target = a.claims if a.claims else a.report
|
|
410
|
+
check_ids = bool(a.claims)
|
|
411
|
+
db_path = a.db or default_db_path()
|
|
412
|
+
failures, _claims = lint(target, os.path.abspath(a.base), check_ids=check_ids, db_path=db_path, unverifiable_path=getattr(a, "unverifiable", None))
|
|
413
|
+
sys.exit(1 if failures else 0)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
if __name__ == "__main__":
|
|
417
|
+
main()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# r20 production-topology bench arena: FULL launcher + plugin + gates running
|
|
3
|
+
# against a candidate tree. Method: copy the control tree minus bulk, then
|
|
4
|
+
# rewrite ONLY the launcher's CONTROL_ROOT literal to the arena (sed-verified
|
|
5
|
+
# single-line patch; every gate expression stays byte-identical, they just
|
|
6
|
+
# evaluate on the candidate tree — this is the honest quarantine).
|
|
7
|
+
# Usage: seed-r20-arena.sh <arena-root> <candidate-worktree-or-empty>
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
ARENA="${1:?arena root}"; CAND="${2:-}"
|
|
10
|
+
SRC=/home/lab/workspace/pcb-control
|
|
11
|
+
rm -rf "$ARENA"; mkdir -p "$ARENA"
|
|
12
|
+
# bulk exclusions: reports/evidence are outputs, checkpoints/dispatches are state,
|
|
13
|
+
# node_modules + hermetic bin get symlinked (110MB, content-identical).
|
|
14
|
+
tar -C "$SRC" \
|
|
15
|
+
--exclude='./reports' --exclude='./evidence-links' --exclude='./state' \
|
|
16
|
+
--exclude='./dispatches' --exclude='./fixtures' --exclude='./node_modules' \
|
|
17
|
+
--exclude='*/node_modules' --exclude='./profiles/production/bin' \
|
|
18
|
+
--exclude='./.git' -cf - . | tar -C "$ARENA" -xf -
|
|
19
|
+
mkdir -p "$ARENA/state/capabilities" "$ARENA/reports/recovery" "$ARENA/fixtures" "$ARENA/dispatches"
|
|
20
|
+
# bin + node_modules: NO copies, NO links — r20-bench.sh stacks host ro-binds at
|
|
21
|
+
# their literals inside the sandbox. Remove any stale self-looping links from
|
|
22
|
+
# earlier seed versions so the mount points and the find-scan stay honest.
|
|
23
|
+
rm -f "$ARENA/profiles/production/bin"
|
|
24
|
+
find "$ARENA" -maxdepth 6 -type l -name node_modules -delete
|
|
25
|
+
find "$ARENA" -maxdepth 6 -xtype l -delete # dangling links only
|
|
26
|
+
# bwrap model: ARENA is bind-mounted AT $SRC inside the sandbox; every literal
|
|
27
|
+
# path (launcher CONTROL_ROOT, plugin profileOf, overlay plugin dirs) holds with
|
|
28
|
+
# zero edits. production-home is copied wholesale (config-home input; per v3
|
|
29
|
+
# lesson a degraded copy fails G1 — so copy everything and re-link vendored
|
|
30
|
+
# node_modules below); writes still land only under $SRC (bind-mounted arena)
|
|
31
|
+
# and /tmp.
|
|
32
|
+
# overlay keeps ABSOLUTE src paths: inside bwrap those literals ARE the arena (bind-mounted). No rewrite.
|
|
33
|
+
# candidate overlay: genome delivery = role files (+ plugin if a candidate ships one)
|
|
34
|
+
if [ -n "$CAND" ]; then
|
|
35
|
+
[ -d "$CAND/agents" ] && for t in internal-control production-console user-console; do
|
|
36
|
+
mkdir -p "$ARENA/$t/.opencode/agents"; cp -f "$CAND"/agents/*.md "$ARENA/$t/.opencode/agents/" 2>/dev/null || true; done
|
|
37
|
+
[ -f "$CAND/plugins/pcb-control-plane.ts" ] && cp -f "$CAND/plugins/pcb-control-plane.ts" "$ARENA/profiles/production/plugins/pcb-control-plane.ts"
|
|
38
|
+
[ -f "$CAND/launch-pins.json" ] && cp -f "$CAND/launch-pins.json" "$ARENA/profiles/production/launch-pins.json"
|
|
39
|
+
fi
|
|
40
|
+
# the single-line root rewrite is GONE (bwrap keeps literals; see header note)
|
|
41
|
+
# after plugin swap, re-pin plugin_sha256 + drop integrity-window notes that bind arena to src
|
|
42
|
+
python3 - "$ARENA" <<'PY'
|
|
43
|
+
import sys, os, json, hashlib
|
|
44
|
+
arena = sys.argv[1]
|
|
45
|
+
pins_path = os.path.join(arena, "profiles/production/launch-pins.json")
|
|
46
|
+
plg = os.path.join(arena, "profiles/production/plugins/pcb-control-plane.ts")
|
|
47
|
+
d = json.load(open(pins_path))
|
|
48
|
+
d["plugin_sha256"] = hashlib.sha256(open(plg,"rb").read()).hexdigest()
|
|
49
|
+
json.dump(d, open(pins_path,"w"), indent=2); open(pins_path,"a").write("\n")
|
|
50
|
+
PY
|
|
51
|
+
L="$ARENA/tools/start_production_orchestrator_ui.sh"
|
|
52
|
+
grep -c 'CONTROL_ROOT="/home/lab/workspace/pcb-control"' "$L" | grep -qx 1 || { echo "SEED_FAIL: CONTROL_ROOT literal changed shape"; exit 1; }
|
|
53
|
+
bash -n "$L"
|
|
54
|
+
echo "ARENA_SEEDED $ARENA plugin=$(sha256sum "$ARENA/profiles/production/plugins/pcb-control-plane.ts" | cut -c1-12)"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# seed-sandbox.sh — PCB bench per-unit arena seeding (evolution harness, NOT board work).
|
|
3
|
+
# Creates a fresh isolated arena and copies the STARTER tool kit + inputs only.
|
|
4
|
+
# The team's own improvements must land in tools-v2/ inside the arena; the seeded
|
|
5
|
+
# tools/ stay pristine so the grader can diff what the team actually changed.
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
ARENA="${1:?usage: seed-sandbox.sh <arena-dir> <seed-src-dir>}"
|
|
8
|
+
SEED="${2:?seed source dir (graders/pcb-agent/seed)}"
|
|
9
|
+
rm -rf "$ARENA"; mkdir -p "$ARENA/tools" "$ARENA/fab-out"
|
|
10
|
+
cp "$SEED"/tools/*.py "$SEED"/tools/*.sh "$ARENA/tools/"
|
|
11
|
+
cp "$SEED"/spec.json "$SEED"/esp32s3-r12-BRIEF.txt "$ARENA/"
|
|
12
|
+
chmod +w "$ARENA/tools/"* 2>/dev/null || true
|
|
13
|
+
echo "SEEDED arena=$ARENA tools=$(ls "$ARENA/tools" | wc -l) inputs=$(ls "$ARENA"/*.json "$ARENA"/*.txt | wc -l)"
|
package/package.json
CHANGED
package/plugin/abathur.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// abathur-opencode-plugin v0.2.
|
|
1
|
+
// abathur-opencode-plugin v0.2.8
|
|
2
2
|
// Official opencode plugin adapter for the abathur evolution harness.
|
|
3
3
|
// Registers ONE agent tool, `abathur`, that shells out to the abathur CLI —
|
|
4
4
|
// argv-only (node:child_process execFile, never a shell), top-level commands
|
|
@@ -29,7 +29,7 @@ const TIMEOUT_MS = 120_000;
|
|
|
29
29
|
const MAX_OUTPUT_BYTES = 64 * 1024;
|
|
30
30
|
|
|
31
31
|
/** The seven tool-reachable top-level commands plus --help. Anything else is refused locally.
|
|
32
|
-
* promote/tombstone/retract are human gates and deliberately terminal-only (0.2.1/0.2.
|
|
32
|
+
* promote/tombstone/retract are human gates and deliberately terminal-only (0.2.1/0.2.8): not reachable here. */
|
|
33
33
|
const ALLOWED_COMMANDS: readonly string[] = [
|
|
34
34
|
"genome",
|
|
35
35
|
"run",
|