@tachikomagundam/abathur 0.2.6 → 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/README.md +3 -3
- package/dist/cli.js +4 -0
- package/dist/commands/readjudicate.js +28 -0
- package/dist/commands/retract.js +53 -0
- package/dist/commands/tombstone.js +28 -15
- package/dist/core/evolve/run-bench.js +7 -1
- package/dist/core/evolve/run-rows.js +14 -0
- package/dist/core/ledger.js +2 -0
- package/dist/core/promote.js +61 -46
- package/dist/core/readjudicate.js +247 -0
- package/dist/core/retract.js +166 -0
- package/dist/core/stats.js +7 -0
- package/dist/core/worktree.js +1 -1
- package/dist/genomes/toy-smoke/init.mjs +1 -1
- package/dist/test/bundle.test.js +1 -1
- package/dist/test/fixture-loop.test.js +1 -1
- package/dist/test/fixtures-self.js +1 -1
- package/dist/test/fixtures-wt.js +1 -1
- package/dist/test/friction.test.js +3 -3
- package/dist/test/graft.test.js +1 -1
- package/dist/test/historian-run-scenario.test.js +2 -2
- package/dist/test/promote.test.js +222 -5
- package/dist/test/readjudicate.test.js +212 -0
- package/dist/test/reflect.test.js +1 -1
- package/dist/test/repopath-seams.test.js +1 -0
- package/dist/test/seam-gates.test.js +6 -0
- package/dist/test/self-snapshot.test.js +2 -2
- 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/historian/judge-poststage.mjs +8 -3
- 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-command.md +3 -3
- package/plugin/abathur.ts +6 -6
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Mutator wrapper for the historian live run (task 14). Invoked by the run-loop
|
|
3
|
+
# as `bash mutate.sh <brief-file> <worktree> <model>` with cwd = the throwaway
|
|
4
|
+
# launch worktree (reflect.ts contract). The REAL candidate source is a headless
|
|
5
|
+
# `opencode run` child: it reads the reflection brief and proposes exactly one
|
|
6
|
+
# mutation — a new RUN CARD under abathur-notes/ (never a sealed path;
|
|
7
|
+
# kernel.immutableGlobs is enforced independently by the driver's PathPolicy).
|
|
8
|
+
# Run cards are the delivery seam (S4): run-scenario.sh appends them VERBATIM to
|
|
9
|
+
# every scenario brief under a `## Run card` heading, so a candidate tree's
|
|
10
|
+
# mutation genuinely steers the benched agent and score deltas are attributable.
|
|
11
|
+
# The wrapper deterministically packages the model's run card as a unified CREATE
|
|
12
|
+
# diff (creation diffs have no context to drift) and appends a genome.jsonc
|
|
13
|
+
# create-diff carrying the resolved spec bytes (plan 184: every sealed tree
|
|
14
|
+
# must contain genome.jsonc so bundle export self-describes; plan 181 requires
|
|
15
|
+
# it for the lineage). On unparseable model output the wrapper degrades to a
|
|
16
|
+
# packaging-only candidate — honest, logged, never a fabricated mutation.
|
|
17
|
+
set -uo pipefail
|
|
18
|
+
brief_file="${1:?usage: mutate.sh <brief-file> <worktree> <model>}"
|
|
19
|
+
worktree="${2:?usage: mutate.sh <brief-file> <worktree> <model>}"
|
|
20
|
+
model="${3:?usage: mutate.sh <brief-file> <worktree> <model>}"
|
|
21
|
+
# the published engine does not export ABATHUR_GENOME_CANONICAL; derive from the
|
|
22
|
+
# launch worktree's own genome.jsonc (written by the engine each round). Env wins
|
|
23
|
+
# when present; missing-both still fail-closes on the [ -f "$canonical" ] guard.
|
|
24
|
+
canonical="${ABATHUR_GENOME_CANONICAL:-$worktree/genome.jsonc}"
|
|
25
|
+
[ -f "$brief_file" ] || { echo "mutate: missing brief $brief_file" >&2; exit 1; }
|
|
26
|
+
cp -f "$brief_file" "${ABATHUR_MUTATOR_RAW:-/tmp/abathur-mutate-raw.jsonl}.brief" 2>/dev/null || true
|
|
27
|
+
[ -f "$canonical" ] || { echo "mutate: missing canonical spec $canonical" >&2; exit 1; }
|
|
28
|
+
bin="${ABATHUR_OPENCODE_BIN:-opencode}"
|
|
29
|
+
|
|
30
|
+
ptfile="$(mktemp /tmp/abathur-mutate-prompt.XXXXXX)"
|
|
31
|
+
cat > "$ptfile" <<'PROMPT_EOF'
|
|
32
|
+
You are the genome mutator of the Abathur evolution harness, PCB-agent lane. Read the reflection brief below (evidence from the incumbent bench of the pcb-agent genome: the PCB team run log, FINDINGS.md, and the grader OUTCOME/EYES metrics). Propose exactly ONE improvement: create a NEW RUN CARD under abathur-notes/ (slug .md). The run card is appended VERBATIM to the team task brief at bench time — write direct, actionable engineering guidance the PCB orchestrator team can follow. TARGET THE EYES GAP: if the grader shows traps the team hit but never NAMED (trap_coverage low while outcome partial), the card must teach the DISCOVERY PROCEDURE (how to notice this class of defect yourself), not the pasted answer — an answer scores once and dies; a discovery method evolves the team. Prefer converting one observed failure into a check the team can run itself. Output ONLY a single JSON object, no prose, shape:
|
|
33
|
+
{"rationale": "<one sentence, <=400 chars>", "path": "abathur-notes/<slug>.md", "content": "<full markdown file contents>"}
|
|
34
|
+
Do not modify any other path. Scenarios/, README.md and genome.jsonc are immutable; board tooling lives in the arena, not the genome tree.
|
|
35
|
+
|
|
36
|
+
BRIEF:
|
|
37
|
+
PROMPT_EOF
|
|
38
|
+
cat "$brief_file" >> "$ptfile"
|
|
39
|
+
prompt="$(cat "$ptfile")"
|
|
40
|
+
rm -f "$ptfile"
|
|
41
|
+
|
|
42
|
+
raw="${ABATHUR_MUTATOR_RAW:-/tmp/abathur-mutate-raw.jsonl}"
|
|
43
|
+
"$bin" run --model "$model" --format json -- "$prompt" >"$raw" 2>/dev/null
|
|
44
|
+
rc=$?
|
|
45
|
+
echo "mutate: opencode rc=$rc raw=$raw" >&2
|
|
46
|
+
|
|
47
|
+
python3 - "$raw" "$canonical" "$rc" "$worktree" <<'PY'
|
|
48
|
+
import json, os, re, sys
|
|
49
|
+
|
|
50
|
+
raw, canonical_path, rc, worktree = sys.argv[1], sys.argv[2], int(sys.argv[3]), sys.argv[4]
|
|
51
|
+
|
|
52
|
+
def create_diff(path, text):
|
|
53
|
+
lines = text.split("\n")
|
|
54
|
+
if lines and lines[-1] == "":
|
|
55
|
+
lines.pop()
|
|
56
|
+
body = "".join("+" + ln + "\n" for ln in lines)
|
|
57
|
+
return f"--- /dev/null\n+++ b/{path}\n@@ -0,0 +1,{len(lines)} @@\n{body}"
|
|
58
|
+
|
|
59
|
+
def last_text_events(path):
|
|
60
|
+
texts = []
|
|
61
|
+
try:
|
|
62
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
63
|
+
for line in fh:
|
|
64
|
+
line = line.strip()
|
|
65
|
+
if not line:
|
|
66
|
+
continue
|
|
67
|
+
try:
|
|
68
|
+
ev = json.loads(line)
|
|
69
|
+
except ValueError:
|
|
70
|
+
continue
|
|
71
|
+
part = ev.get("part") or {}
|
|
72
|
+
if ev.get("type") == "text" and isinstance(part.get("text"), str):
|
|
73
|
+
texts.append(part["text"])
|
|
74
|
+
except FileNotFoundError:
|
|
75
|
+
pass
|
|
76
|
+
return texts
|
|
77
|
+
|
|
78
|
+
proposal = None
|
|
79
|
+
for text in reversed(last_text_events(raw)):
|
|
80
|
+
candidate = text.strip()
|
|
81
|
+
fence = re.search(r"```(?:json)?\s*(\{.*\})\s*```", candidate, re.S)
|
|
82
|
+
if fence:
|
|
83
|
+
candidate = fence.group(1)
|
|
84
|
+
end = candidate.rfind("}")
|
|
85
|
+
if end < 0:
|
|
86
|
+
continue
|
|
87
|
+
# prose preambles can contain set-notation braces like {症状/Symptoms, ...} —
|
|
88
|
+
# the first '{' is not necessarily JSON; try every open brace from the LAST
|
|
89
|
+
# backwards so the outermost-to-last-close object wins when one parses.
|
|
90
|
+
opens = [i for i, ch in enumerate(candidate) if ch == "{" and i < end]
|
|
91
|
+
doc = None
|
|
92
|
+
for start in reversed(opens):
|
|
93
|
+
try:
|
|
94
|
+
parsed = json.loads(candidate[start : end + 1])
|
|
95
|
+
except ValueError:
|
|
96
|
+
continue
|
|
97
|
+
if isinstance(parsed, dict):
|
|
98
|
+
doc = parsed
|
|
99
|
+
break
|
|
100
|
+
if doc is not None:
|
|
101
|
+
proposal = doc
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
spec_text = open(canonical_path, encoding="utf-8").read()
|
|
105
|
+
if not spec_text.endswith("\n"):
|
|
106
|
+
spec_text += "\n"
|
|
107
|
+
# genome.jsonc in the base tree was written by this wrapper's own prior round,
|
|
108
|
+
# so a replace-all modify hunk always anchors deterministically at line 1.
|
|
109
|
+
# A CREATE here would break apply the moment the file lands in the base (udiff
|
|
110
|
+
# refuses create-over-existing and discards the WHOLE candidate).
|
|
111
|
+
gen_path = os.path.join(worktree, "genome.jsonc")
|
|
112
|
+
try:
|
|
113
|
+
with open(gen_path, encoding="utf-8") as fh:
|
|
114
|
+
base_text = fh.read()
|
|
115
|
+
except FileNotFoundError:
|
|
116
|
+
packaging = create_diff("genome.jsonc", spec_text)
|
|
117
|
+
except OSError as err:
|
|
118
|
+
sys.stderr.write("mutate: genome.jsonc unreadable: %s\n" % err)
|
|
119
|
+
sys.exit(3)
|
|
120
|
+
else:
|
|
121
|
+
if base_text == spec_text:
|
|
122
|
+
packaging = None
|
|
123
|
+
else:
|
|
124
|
+
base_body = base_text.split("\n")
|
|
125
|
+
if base_body and base_body[-1] == "":
|
|
126
|
+
base_body.pop()
|
|
127
|
+
old_h = "".join("-" + ln + "\n" for ln in base_body)
|
|
128
|
+
new_h = "".join("+" + ln + "\n" for ln in spec_text.split("\n")[:-1])
|
|
129
|
+
packaging = (
|
|
130
|
+
"--- a/genome.jsonc\n+++ b/genome.jsonc\n"
|
|
131
|
+
"@@ -1,%d +1,%d @@\n%s%s" % (len(base_body), len(spec_text.split("\n")) - 1, old_h, new_h)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if isinstance(proposal, dict):
|
|
135
|
+
path = proposal.get("path")
|
|
136
|
+
content = proposal.get("content")
|
|
137
|
+
rationale = proposal.get("rationale")
|
|
138
|
+
if (
|
|
139
|
+
isinstance(path, str)
|
|
140
|
+
and re.fullmatch(r"abathur-notes/[a-z0-9][a-z0-9._-]{0,63}\.md", path)
|
|
141
|
+
and isinstance(content, str)
|
|
142
|
+
and 0 < len(content) < 200000
|
|
143
|
+
and isinstance(rationale, str)
|
|
144
|
+
and 0 < len(rationale) <= 4000
|
|
145
|
+
and "\x00" not in content
|
|
146
|
+
):
|
|
147
|
+
content = content.replace("\r", "")
|
|
148
|
+
if not content.endswith("\n"):
|
|
149
|
+
content += "\n"
|
|
150
|
+
diffs = [create_diff(path, content)] + ([packaging] if packaging else [])
|
|
151
|
+
candidate = {"id": "mutate-live", "rationale": rationale, "diffs": diffs}
|
|
152
|
+
print(json.dumps({"candidates": [candidate]}))
|
|
153
|
+
sys.exit(0)
|
|
154
|
+
|
|
155
|
+
if packaging:
|
|
156
|
+
print(json.dumps({
|
|
157
|
+
"candidates": [{
|
|
158
|
+
"id": "mutate-package-only",
|
|
159
|
+
"rationale": "packaging-only candidate: model output unparseable or path rejected (opencode rc=%d); lineage genome.jsonc added, no skill mutation proposed" % rc,
|
|
160
|
+
"diffs": [packaging],
|
|
161
|
+
}]
|
|
162
|
+
}))
|
|
163
|
+
else:
|
|
164
|
+
print(json.dumps({"candidates": []}))
|
|
165
|
+
PY
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# r20 production-topology bench sandbox: runs a command INSIDE bwrap where the
|
|
3
|
+
# arena is bind-mounted AT the control literal, with host-vendored dirs re-bound
|
|
4
|
+
# over the arena's excluded gaps (bind-mounts stack: later wins).
|
|
5
|
+
# Usage: r20-bench.sh <arena> -- <inner command...>
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
ARENA="${1:?arena}"; shift; [ "${1:-}" = "--" ] && shift
|
|
8
|
+
SRC=/home/lab/workspace/pcb-control
|
|
9
|
+
LIT=$SRC # the literal path the control plane believes itself to be
|
|
10
|
+
# v8 netns ABANDONED: lo cannot be raised in this unprivileged userns (EPERM).
|
|
11
|
+
# Isolation strategy instead: arena bind at the literal (fs) + PCB_PRODUCTION_SERVE_PORT
|
|
12
|
+
# exported to a FREE host port (net coexistence; data dirs are the arena's, so the
|
|
13
|
+
# bench serve cannot touch the real lane state; verified by G5 record tuple).
|
|
14
|
+
PORT=$(python3 -c "import socket;s=socket.socket();s.bind((\"127.0.0.1\",0));print(s.getsockname()[1]);s.close()")
|
|
15
|
+
ARGS=(--ro-bind / / --dev-bind /dev /dev --proc /proc --bind /tmp /tmp
|
|
16
|
+
--bind "$ARENA" "$LIT" --unshare-pid --setenv PCB_PRODUCTION_SERVE_PORT "$PORT")
|
|
17
|
+
# vendored opencode binary (excluded from arena tar; must appear AT the literal)
|
|
18
|
+
ARGS+=(--ro-bind "$SRC/profiles/production/bin" "$LIT/profiles/production/bin")
|
|
19
|
+
# every vendored node_modules under the control tree (typescript for the loader,
|
|
20
|
+
# production-home caches carrying OMO/ACP/HR packages) — ro at their literals
|
|
21
|
+
while IFS= read -r -d '' d; do
|
|
22
|
+
rel="${d#$SRC}"
|
|
23
|
+
[ -d "$ARENA$rel" ] && continue # arena copy exists: leave it (candidate-owned)
|
|
24
|
+
mkdir -p "$ARENA$(dirname "$rel")" 2>/dev/null || true
|
|
25
|
+
ARGS+=(--ro-bind "$d" "$LIT$rel")
|
|
26
|
+
done < <(find "$SRC" -maxdepth 6 -name node_modules -type d -print0)
|
|
27
|
+
exec bwrap "${ARGS[@]}" -- /bin/bash -c "$*"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Reset hook (pcb-agent): honest no-op. Freshness is guaranteed at run time —
|
|
3
|
+
# run-scenario.sh re-seeds the arena (rm -rf + copy pristine starter kit) at the
|
|
4
|
+
# top of EVERY rep, so back-to-back generations are state-equal without a
|
|
5
|
+
# separate wipe. Kept as an explicit file so the contract is visible, not empty.
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
echo "RESET-NOOP (per-rep reseed happens in run-scenario.sh)"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# r20 run-scenario v2 — PRODUCTION-TOPOLOGY bench (user ruling 2026-09-30:
|
|
3
|
+
# "我工作环境里是要用PCB-Orchestrator这个角色来执行的"). One rep = fresh arena
|
|
4
|
+
# (per-rep reseed is the L2 reset AND the pollution guard: smoke runs mutate the
|
|
5
|
+
# copied opencode.db/config and can drift G1), bwrap quarantine at the control
|
|
6
|
+
# literal, launcher-gated serve on a free port (netns impossible: lo EPERM),
|
|
7
|
+
# scenario driven as a REAL pcb-orchestrator session over REST, transcript +
|
|
8
|
+
# control-plane ledger harvested with literal->arena path mapping for the grader.
|
|
9
|
+
# Usage: run-scenario-r20.sh <unit-id> <scenario-file> <genome-repo> [arena-root]
|
|
10
|
+
set -uo pipefail
|
|
11
|
+
UNIT="$1"; SCENARIO="$2"; GENOME_REPO="$3"; ARENA="${4:-/tmp/r20-arena-$UNIT}"
|
|
12
|
+
GRAD=/home/lab/workspace/harness/Abathur/graders/pcb-agent
|
|
13
|
+
LIT=/home/lab/workspace/pcb-control
|
|
14
|
+
OUT="$GENOME_REPO/.omo/run-artifacts/$UNIT"; mkdir -p "$OUT"
|
|
15
|
+
BRIEF_FILE="$OUT/brief.md"; cp "$SCENARIO" "$BRIEF_FILE"
|
|
16
|
+
|
|
17
|
+
bash "$GRAD/seed-r20-arena.sh" "$ARENA" "$GENOME_REPO" > "$OUT/seed.log" 2>&1 || { echo "SEED_FAIL" > "$OUT/infra.fail"; exit 1; }
|
|
18
|
+
|
|
19
|
+
# candidate role files: seed copied $GENOME_REPO/agents/*.md over all three trees (v4 seam)
|
|
20
|
+
|
|
21
|
+
cat > "$OUT/inner.sh" <<'INNER'
|
|
22
|
+
set -x
|
|
23
|
+
L=/home/lab/workspace/pcb-control
|
|
24
|
+
OUTD=__OUT__
|
|
25
|
+
PORT=${PCB_PRODUCTION_SERVE_PORT}
|
|
26
|
+
cd $L/production-console
|
|
27
|
+
export HOME=$L/production-home
|
|
28
|
+
timeout ${PCB_REP_TIMEOUT:-1500} $L/tools/start_production_orchestrator_ui.sh --serve $PORT > $OUTD/serve.log 2>&1 &
|
|
29
|
+
SPID=$!
|
|
30
|
+
OK=0
|
|
31
|
+
for i in $(seq 1 150); do sleep 3
|
|
32
|
+
curl -s --max-time 2 http://127.0.0.1:$PORT/session >/dev/null 2>&1 && { OK=1; break; }
|
|
33
|
+
kill -0 $SPID 2>/dev/null || break
|
|
34
|
+
done
|
|
35
|
+
[ "$OK" = 1 ] || { echo "SERVE_UP=0" > $OUTD/infra.fail; kill $SPID 2>/dev/null; exit 1; }
|
|
36
|
+
$L/tools/start_production_orchestrator_ui.sh --probe $PORT 2>&1 | tail -1 > $OUTD/probe.txt
|
|
37
|
+
SID=$(curl -s --max-time 10 -X POST http://127.0.0.1:$PORT/session -H 'content-type: application/json' -d "{\"title\":\"bench-$UNIT\"}" | python3 -c "import json,sys;print(json.load(sys.stdin).get('id',''))")
|
|
38
|
+
# r21: named bench-approver arm (reactive H-gate stand-in, append-only audit log)
|
|
39
|
+
setsid nohup python3 /home/lab/workspace/harness/Abathur/graders/pcb-agent/bench-approver.py \
|
|
40
|
+
http://127.0.0.1:$PORT "$SID" $OUTD/approve-log.jsonl ${PCB_TURN_TIMEOUT:-1200} >/dev/null 2>&1 &
|
|
41
|
+
APPR=$!
|
|
42
|
+
python3 - "$OUTD" "$BRIEF_FILE" <<'PYMSG' > $OUTD/payload.json
|
|
43
|
+
import json, sys
|
|
44
|
+
outd, brief = sys.argv[1], sys.argv[2]
|
|
45
|
+
msg = open(brief).read()
|
|
46
|
+
json.dump({"agent": "pcb-orchestrator",
|
|
47
|
+
"model": {"providerID": "local-qwen", "modelID": "qwen3.8-flash-next"},
|
|
48
|
+
"parts": [{"type": "text", "text": msg}]}, open(outd + "/payload_body.json", "w"))
|
|
49
|
+
PYMSG
|
|
50
|
+
timeout ${PCB_TURN_TIMEOUT:-1200} curl -s --max-time ${PCB_TURN_TIMEOUT:-1200} -X POST "http://127.0.0.1:$PORT/session/$SID/message" -H 'content-type: application/json' -d @$OUTD/payload_body.json > $OUTD/transcript.json
|
|
51
|
+
echo "$SID" > $OUTD/session-id
|
|
52
|
+
kill $SPID $APPR 2>/dev/null
|
|
53
|
+
[ -s $OUTD/transcript.json ] || { echo "EMPTY_TRANSCRIPT" > $OUTD/infra.fail; exit 1; }
|
|
54
|
+
INNER
|
|
55
|
+
sed -i "s#__OUT__#/tmp/r20-out-$UNIT#g" "$OUT/inner.sh" # sandbox-writable; harvested after
|
|
56
|
+
mkdir -p /tmp/r20-out-$UNIT && rm -f /tmp/r20-out-$UNIT/*
|
|
57
|
+
|
|
58
|
+
# r20-bench.sh builds the full mount table (bin + every vendored node_modules
|
|
59
|
+
# literal-stack + free-port env). inner.sh reads $PCB_PRODUCTION_SERVE_PORT.
|
|
60
|
+
timeout $(( ${PCB_REP_TIMEOUT:-1500} + 900 )) bash "$GRAD/r20-bench.sh" "$ARENA" -- \
|
|
61
|
+
"BRIEF_FILE=$BRIEF_FILE bash $OUT/inner.sh" > "$OUT/inner.log" 2>&1
|
|
62
|
+
RC=$?
|
|
63
|
+
cp -f /tmp/r20-out-$UNIT/* "$OUT/" 2>/dev/null || true
|
|
64
|
+
# node_modules literal-stack binds (arena lacks them by design)
|
|
65
|
+
# harvest: map the control literal back to the arena for the grader
|
|
66
|
+
python3 - "$OUT" "$ARENA" "$LIT" <<'PYH' 2>/dev/null || true
|
|
67
|
+
import json, os, re, sys
|
|
68
|
+
out, arena, lit = sys.argv[1:4]
|
|
69
|
+
t = os.path.join(out, "transcript.json")
|
|
70
|
+
if os.path.exists(t):
|
|
71
|
+
s = open(t, errors="replace").read().replace(lit, arena)
|
|
72
|
+
open(t, "w").write(s)
|
|
73
|
+
PYH
|
|
74
|
+
cp -f "$ARENA"/state/capabilities/*.json "$OUT/" 2>/dev/null || true
|
|
75
|
+
find "$ARENA/state" -name 'denied-*' -newer "$OUT/seed.log" 2>/dev/null | head -20 > "$OUT/ledger-rows.txt"
|
|
76
|
+
echo "r20-rep rc=$RC unit=$UNIT arena=$ARENA out=$OUT"
|
|
77
|
+
exit $RC
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# run-scenario.sh (pcb-agent) — spawn the PCB TEAM headless on the scenario brief.
|
|
3
|
+
# The seat writes ZERO board artifacts here: this script only (1) seeds an isolated
|
|
4
|
+
# arena, (2) launches the internal-control orchestrator profile (isolated HOME/DB per
|
|
5
|
+
# L-isolation) with the brief, (3) records the transcript. All engineering is done by
|
|
6
|
+
# the team's own agents. Transcript = the evidence the grader reads.
|
|
7
|
+
#
|
|
8
|
+
# Contract mirrors historian: argv = <unitId> <scenario-file> [repoRoot]; last stdout
|
|
9
|
+
# line = {"tokensEst":N,"turns":M} for parseRunMeta; ABATHUR_TRANSCRIPT honored.
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
unit_id="${1:?usage: run-scenario.sh <unitId> <scenario-file> [repoRoot]}"
|
|
12
|
+
scenario_file="${2:?scenario file}"
|
|
13
|
+
repo_root="${3:-$(dirname "$(dirname "$scenario_file")")}"
|
|
14
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
15
|
+
CONTROL=/home/lab/workspace/pcb-control
|
|
16
|
+
ARENA_ROOT="${PCB_BENCH_ARENA:-/tmp/opencode/pcb-bench}"
|
|
17
|
+
ARENA="$ARENA_ROOT/$unit_id"
|
|
18
|
+
TRANSCRIPT="${ABATHUR_TRANSCRIPT:-$ARENA_ROOT/$unit_id.transcript.jsonl}"
|
|
19
|
+
mkdir -p "$ARENA_ROOT"
|
|
20
|
+
|
|
21
|
+
bash "$HERE/seed-sandbox.sh" "$ARENA" "$HERE/seed"
|
|
22
|
+
|
|
23
|
+
# Brief = the scenario file (whole thing; it is the task card, no solutions inside).
|
|
24
|
+
BRIEF="$(cat "$scenario_file")"
|
|
25
|
+
# run cards (S4 seam, historian precedent): candidate mutations ride to the team verbatim.
|
|
26
|
+
rc=""
|
|
27
|
+
if [ -d "$repo_root/abathur-notes" ]; then
|
|
28
|
+
for n in "$repo_root"/abathur-notes/*.md; do [ -e "$n" ] || continue
|
|
29
|
+
rc="$rc
|
|
30
|
+
## Run card
|
|
31
|
+
$(cat "$n")"; done
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
cd "$ARENA"
|
|
35
|
+
# per-unit isolated HOME/DB (never the human main DB; L-isolation law), seeded
|
|
36
|
+
# from the PROVISIONED internal home's provider config (local-qwen baseURL etc).
|
|
37
|
+
UHOME="$ARENA_ROOT/$unit_id.home"; rm -rf "$UHOME"; mkdir -p "$UHOME/.config" "$UHOME/.local/share/opencode"
|
|
38
|
+
cp -r /home/lab/workspace/pcb-control/internal-home/.config/opencode "$UHOME/.config/" 2>/dev/null || true
|
|
39
|
+
rm -rf "$UHOME/.config/opencode/node_modules" "$UHOME/.local/share/opencode/opencode.db" 2>/dev/null || true
|
|
40
|
+
DB="$UHOME/.local/share/opencode/opencode.db"
|
|
41
|
+
|
|
42
|
+
# THE TEAM IS THE SUBJECT: mount the PCB role tree into the arena cwd so
|
|
43
|
+
# `--agent pcb-orchestrator` resolves. Role provenance is the genome seam:
|
|
44
|
+
# if the candidate tree carries agents/*.md (a mutation to the team itself),
|
|
45
|
+
# those files WIN over the canonical internal-control copies.
|
|
46
|
+
mkdir -p "$ARENA/.opencode/agents"
|
|
47
|
+
cp /home/lab/workspace/pcb-control/internal-control/.opencode/agents/*.md "$ARENA/.opencode/agents/"
|
|
48
|
+
if [ -d "$repo_root/agents" ] && ls "$repo_root"/agents/*.md >/dev/null 2>&1; then
|
|
49
|
+
cp -f "$repo_root"/agents/*.md "$ARENA/.opencode/agents/"
|
|
50
|
+
echo "role-provenance: candidate-tree agents override canonical" >&2
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
RUNENV=(env -i PATH="$PATH" LANG=C.UTF-8 TERM=xterm-256color HOME="$UHOME"
|
|
54
|
+
XDG_DATA_HOME="$UHOME/.local/share" XDG_CACHE_HOME="$UHOME/.cache" XDG_STATE_HOME="$UHOME/.local/state"
|
|
55
|
+
OPENCODE_DB="$DB" KICAD_SHARE_DIR=/home/lab/.local/AppDir/share/kicad)
|
|
56
|
+
|
|
57
|
+
# FAIL-CLOSED SELF-PROOF GATE: the named agent must actually resolve BEFORE we
|
|
58
|
+
# spend a rep. "agent not found. Falling back to default" voids the rep — that
|
|
59
|
+
# is precisely the channel-dead noise theater the lane forbids.
|
|
60
|
+
"${RUNENV[@]}" opencode agent list >"$ARENA/.agent-list.txt" 2>&1
|
|
61
|
+
grep -q "pcb-orchestrator" "$ARENA/.agent-list.txt" || {
|
|
62
|
+
echo "RUNNER_GATE_FAIL: pcb-orchestrator not in resolved agent inventory" >&2
|
|
63
|
+
tail -5 "$ARENA/.agent-list.txt" >&2
|
|
64
|
+
exit 1
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
timeout "${PCB_RUN_TIMEOUT:-2400}" "${RUNENV[@]}" \
|
|
68
|
+
opencode run --agent pcb-orchestrator --format json \
|
|
69
|
+
--dir "$ARENA" -- "$BRIEF$rc" > "$TRANSCRIPT" 2>"$ARENA/.run-stderr" || true
|
|
70
|
+
|
|
71
|
+
if [ ! -s "$TRANSCRIPT" ]; then
|
|
72
|
+
echo "RUNNER_INFRA_FAIL: empty transcript (see $ARENA/.run-stderr)" >&2
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
# mechanical role attribution proof: the bench session in the isolated DB MUST
|
|
76
|
+
# be owned by pcb-orchestrator (transcript lines carry no agent field; the DB
|
|
77
|
+
# session table does). No proof = void rep, never a scored fallback run.
|
|
78
|
+
ROLE_OK="$(sqlite3 "$DB" "SELECT COUNT(*) FROM session WHERE agent='pcb-orchestrator';" 2>/dev/null || echo 0)"
|
|
79
|
+
[ "${ROLE_OK:-0}" -ge 1 ] || { echo "RUNNER_INFRA_FAIL: no pcb-orchestrator session in $DB (fallback-agent run would be noise theater)" >&2; exit 1; }
|
|
80
|
+
|
|
81
|
+
turns=$(grep -c '"type":"text"' "$TRANSCRIPT" 2>/dev/null || true); turns="${turns:-0}"
|
|
82
|
+
tokens=$(python3 - "$TRANSCRIPT" <<'EOF' 2>/dev/null | tail -1 || true
|
|
83
|
+
import json,sys
|
|
84
|
+
n=0
|
|
85
|
+
for l in open(sys.argv[1]):
|
|
86
|
+
try: d=json.loads(l)
|
|
87
|
+
except Exception: continue
|
|
88
|
+
p=d.get("part",{})
|
|
89
|
+
if p.get("type")=="text": n+=len(p.get("text",""))
|
|
90
|
+
print(n//4)
|
|
91
|
+
EOF
|
|
92
|
+
); tokens="${tokens:-0}"
|
|
93
|
+
echo "arena=$ARENA" > "$ARENA_ROOT/$unit_id.arena"
|
|
94
|
+
python3 -c "import json,re;t=int(re.sub(r'[^0-9]','',str('$tokens')) or 0);u=int(re.sub(r'[^0-9]','',str('$turns')) or 0);print(json.dumps({'tokensEst':t,'turns':u}))"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# MODULE BRIEF -- esp32s3-r12 (fixture-tier mini-board)
|
|
2
|
+
|
|
3
|
+
Mission dir: fixtures/board-lab/esp32s3-r12/ (root: /home/lab/workspace/pcb-control/fixtures)
|
|
4
|
+
Binding plan: 2ee2cfb309bea8552dd294db3eb087d74950f0e82e0e11524a142c9f8f217f4a / dispatch pcb-dispatch-r12a-plan1
|
|
5
|
+
|
|
6
|
+
## 0. Purpose
|
|
7
|
+
Six-module ESP32-S3 breakout: USB-C 5V in -> ME6211 3.3V LDO -> module power/decoupling, one status LED, one 1x4 debug header with UART. Fixtures only; never the real board.
|
|
8
|
+
|
|
9
|
+
## 1. Netlist (exactly 6 nets)
|
|
10
|
+
- +3V3 : U1.3V3, U2.VOUT, C1.1, C2.1, C3.1, D1.anode, H1.1 (L10: single merged name, no variants)
|
|
11
|
+
- +5V_IN: J1.VBUS, U2.VIN
|
|
12
|
+
- GND : J1.GND, U2.GND, C1.2, C2.2, C3.2, R1.2? NO -> R1.2 is +3V3 per LED string below.
|
|
13
|
+
Correction -- GND: J1.GND, U2.GND, C1.2, C2.2, C3.2, D1... see M5 for string orientation.
|
|
14
|
+
- GPIO2 : U1.GPIO2, R1.2
|
|
15
|
+
- GPIO4 : U1.GPIO4, H1.2 (label "TX" = board->host)
|
|
16
|
+
- GPIO5 : U1.GPIO5, H1.3 (label "RX" = host->board)
|
|
17
|
+
Final GND set: J1.GND, U2.GND, C1.2, C2.2, C3.2, H1.4.
|
|
18
|
+
All other U1 pins NC; flash/PSRAM GPIO26-32 MUST remain unconnected. U1.EN tied directly to +3V3 (hard-enable; accepted fixture deviation, flagged for DFM review).
|
|
19
|
+
|
|
20
|
+
## 2. Modules (6 max -- no extra refs permitted)
|
|
21
|
+
- M1 U1 ESP32-S3-WROOM-1 (module). Power pads 3V3/GND per library footprint; IO used: EN, GPIO2, GPIO4 (UART TX), GPIO5 (UART RX).
|
|
22
|
+
- M2 J1 USB-C receptacle, power-only. Connect VBUS(A4/A9/B4/B5 bussed)->+5V_IN and GND only. CC1/CC2 left NC -- fixture is bench/charger-fed; no PD negotiation. Deviation noted for reviewer.
|
|
23
|
+
- M3 U2 ME6211C33 LDO (SOT-23-3 class). Function pins VIN->+5V_IN, GND->GND, VOUT->+3V3. Pad-number resolution against the project library symbol is the schematic agent's job, verified by gate.
|
|
24
|
+
- M4 Decoupling: C1,C2 100nF 0402 across +3V3/GND placed at U1 3V3 pad(s); C3 10uF 0603 across +3V3/GND at U2 VOUT. ME6211 input cap omitted (fixture simplification, flagged).
|
|
25
|
+
- M5 Status: +3V3 -> D1 anode, D1 cathode -> R1 (1k) -> GPIO2 (sink-driven, active-low lit). GPIO2 is not an S3 strapping pin (straps are 0/3/45/46): safe.
|
|
26
|
+
- M6 H1 1x4 header, 2.54mm pitch: pin1 +3V3, pin2 GPIO4/TX, pin3 GPIO5/RX, pin4 GND.
|
|
27
|
+
|
|
28
|
+
## 3. Explicit rules -- binding on brief AND every downstream artifact
|
|
29
|
+
- L10 ONE merged power net named +3V3 for every 3.3V pad. Forbidden: +3V3A, 3V3_D, VDD_3V3, duplicate 3.3V names. Net-name scan of sch+pcb must find exactly "+3V3".
|
|
30
|
+
- L11 Every footprint pad pitch AND courtyard gap >= 0.25mm (class clearance 0.2mm + 0.05 margin). 0402 pads 1.0x0.5mm, rot-aware: clearance check applies after per-ref rotation. U1 side pads on >=1.5mm vertical grid rows.
|
|
31
|
+
- L16 Netlist uses KiCad schematic global-label grammar: every connection is a wire terminating in a label whose text equals the net name, placed at each pin. No hierarchical/local-only labels, no net-naming-by-wire.
|
|
32
|
+
- L1 pos.csv refs == bom.csv refs exactly (same set, same strings): {U1,U2,J1,H1,C1,C2,C3,D1,R1} -- 10 refs, no suffix drift, no extra/missing.
|
|
33
|
+
- Placement grid: 0.5mm snap for all component origins.
|
|
34
|
+
|
|
35
|
+
## 4. Artifact contract (mission dir)
|
|
36
|
+
- esp32s3-r12.kicad_sch -- 10 symbols, nets per section 1, grammar per L16.
|
|
37
|
+
- esp32s3-r12.kicad_pcb -- footprints from project lib; courtyard clearances per L11; board outline suggestion <= 55x35mm (target, non-binding).
|
|
38
|
+
- bom.csv / pos.csv -- per L1; pos.csv includes rotation column (feeds L11 rot-aware check).
|
|
39
|
+
- reports/ -- gate evidence JSON from tools/gate_runner.py.
|
|
40
|
+
|
|
41
|
+
## 5. Acceptance gates (deterministic, tools/gate_runner.py evidence only)
|
|
42
|
+
- g-net: parsed sch netlist == section 1 set exactly; net count == 6.
|
|
43
|
+
- g-l10: regex scan for 3.3V-name variants in sch+pcb yields zero hits outside "+3V3".
|
|
44
|
+
- g-l11: pad-pitch/courtyard matrix min gap >= 0.25mm; 0402 pad geom 1.0x0.5 present; U1 side-pad row pitch >= 1.5mm.
|
|
45
|
+
- g-l16: every net pin has wire+label; zero label-less pin wires.
|
|
46
|
+
- g-pos: set(bom.refs) == set(pos.refs), len == 10.
|
|
47
|
+
- g-grid: all placement origins on 0.5mm grid.
|
|
48
|
+
Fail on any gate -> revert to last checkpoint; 3 design-fails on same gate -> ESCALATE with options menu.
|
|
49
|
+
|
|
50
|
+
## 6. Out of scope
|
|
51
|
+
Antenna keepout tuning, ESD parts, USB data lines, battery/charge, strapping-button circuits, any board outside fixtures/.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.0",
|
|
3
|
+
"board": {
|
|
4
|
+
"name": "toy-m2",
|
|
5
|
+
"_provenance": "TOY-FIXTURE netlist from esp32s3-r12-BRIEF.txt + architect corrections (7 nets/9 refs). U1 pin-pad function mapping = FIXTURE CONVENTION (toy GPIOs on pads 4/5/9), NOT silicon pinout \u2014 HUMAN VERIFICATION REQUIRED before any real-order use. | C1/C2 0603 deviation per pad-corridor DRC class",
|
|
6
|
+
"project": "board-lab toy M2 full-cycle"
|
|
7
|
+
},
|
|
8
|
+
"components": [
|
|
9
|
+
{
|
|
10
|
+
"ref": "U1",
|
|
11
|
+
"mpn": "ESP32-S3-WROOM-1-N8R8",
|
|
12
|
+
"footprint": "RF_Module:ESP32-S3-WROOM-1",
|
|
13
|
+
"pins": {
|
|
14
|
+
"1": "GND",
|
|
15
|
+
"2": "+3V3",
|
|
16
|
+
"3": "+3V3",
|
|
17
|
+
"4": "GPIO4",
|
|
18
|
+
"5": "GPIO5",
|
|
19
|
+
"9": "GPIO2",
|
|
20
|
+
"21": "GND",
|
|
21
|
+
"6": "NC(pin unused)",
|
|
22
|
+
"7": "NC(pin unused)",
|
|
23
|
+
"8": "NC(pin unused)",
|
|
24
|
+
"10": "NC(pin unused)",
|
|
25
|
+
"11": "NC(pin unused)",
|
|
26
|
+
"12": "NC(pin unused)",
|
|
27
|
+
"13": "NC(pin unused)",
|
|
28
|
+
"14": "NC(pin unused)",
|
|
29
|
+
"15": "NC(pin unused)",
|
|
30
|
+
"16": "NC(pin unused)",
|
|
31
|
+
"17": "NC(pin unused)",
|
|
32
|
+
"18": "NC(pin unused)",
|
|
33
|
+
"19": "NC(pin unused)",
|
|
34
|
+
"20": "NC(pin unused)"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"ref": "U2",
|
|
39
|
+
"mpn": "ME6211C33",
|
|
40
|
+
"footprint": "Package_TO_SOT_SMD:SOT-23",
|
|
41
|
+
"pins": {
|
|
42
|
+
"1": "+5V_IN",
|
|
43
|
+
"2": "GND",
|
|
44
|
+
"3": "+3V3"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"ref": "J1",
|
|
49
|
+
"mpn": "USB4110",
|
|
50
|
+
"footprint": "Connector_USB:USB_C_Receptacle_GCT_USB4110",
|
|
51
|
+
"pins": {
|
|
52
|
+
"1": "GND",
|
|
53
|
+
"2": "NC(pin unused)",
|
|
54
|
+
"3": "NC(pin unused)",
|
|
55
|
+
"4": "+5V_IN",
|
|
56
|
+
"5": "NC(pin unused)",
|
|
57
|
+
"6": "+5V_IN",
|
|
58
|
+
"7": "NC(pin unused)",
|
|
59
|
+
"8": "GND",
|
|
60
|
+
"9": "GND",
|
|
61
|
+
"10": "NC(pin unused)",
|
|
62
|
+
"11": "NC(pin unused)",
|
|
63
|
+
"12": "NC(pin unused)",
|
|
64
|
+
"13": "NC(pin unused)",
|
|
65
|
+
"14": "NC(pin unused)",
|
|
66
|
+
"15": "NC(pin unused)",
|
|
67
|
+
"16": "GND"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"ref": "H1",
|
|
72
|
+
"mpn": "HDR1x4",
|
|
73
|
+
"footprint": "Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical",
|
|
74
|
+
"pins": {
|
|
75
|
+
"1": "+3V3",
|
|
76
|
+
"2": "GPIO4",
|
|
77
|
+
"3": "GPIO5",
|
|
78
|
+
"4": "GND"
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"ref": "C1",
|
|
83
|
+
"mpn": "100nF",
|
|
84
|
+
"footprint": "Capacitor_SMD:C_0603_1608Metric",
|
|
85
|
+
"pins": {
|
|
86
|
+
"1": "+3V3",
|
|
87
|
+
"2": "GND"
|
|
88
|
+
},
|
|
89
|
+
"_dev": "0402->0603: pad-pair corridor (0.36mm) admits no 0.2mm track w/ 0.2 clr; fixture deviation logged"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"ref": "C2",
|
|
93
|
+
"mpn": "100nF",
|
|
94
|
+
"footprint": "Capacitor_SMD:C_0603_1608Metric",
|
|
95
|
+
"pins": {
|
|
96
|
+
"1": "+3V3",
|
|
97
|
+
"2": "GND"
|
|
98
|
+
},
|
|
99
|
+
"_dev": "0402->0603: pad-pair corridor (0.36mm) admits no 0.2mm track w/ 0.2 clr; fixture deviation logged"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"ref": "C3",
|
|
103
|
+
"mpn": "10uF",
|
|
104
|
+
"footprint": "Capacitor_SMD:C_0603_1608Metric",
|
|
105
|
+
"pins": {
|
|
106
|
+
"1": "+3V3",
|
|
107
|
+
"2": "GND"
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"ref": "D1",
|
|
112
|
+
"mpn": "LED",
|
|
113
|
+
"footprint": "LED_SMD:LED_0603_1608Metric",
|
|
114
|
+
"pins": {
|
|
115
|
+
"1": "+3V3",
|
|
116
|
+
"2": "LED1"
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"ref": "R1",
|
|
121
|
+
"mpn": "1k",
|
|
122
|
+
"footprint": "Resistor_SMD:R_0603_1608Metric",
|
|
123
|
+
"pins": {
|
|
124
|
+
"1": "LED1",
|
|
125
|
+
"2": "GPIO2"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|