@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.
Files changed (56) hide show
  1. package/README.md +3 -3
  2. package/dist/cli.js +4 -0
  3. package/dist/commands/readjudicate.js +28 -0
  4. package/dist/commands/retract.js +53 -0
  5. package/dist/commands/tombstone.js +28 -15
  6. package/dist/core/evolve/run-bench.js +7 -1
  7. package/dist/core/evolve/run-rows.js +14 -0
  8. package/dist/core/ledger.js +2 -0
  9. package/dist/core/promote.js +61 -46
  10. package/dist/core/readjudicate.js +247 -0
  11. package/dist/core/retract.js +166 -0
  12. package/dist/core/stats.js +7 -0
  13. package/dist/core/worktree.js +1 -1
  14. package/dist/genomes/toy-smoke/init.mjs +1 -1
  15. package/dist/test/bundle.test.js +1 -1
  16. package/dist/test/fixture-loop.test.js +1 -1
  17. package/dist/test/fixtures-self.js +1 -1
  18. package/dist/test/fixtures-wt.js +1 -1
  19. package/dist/test/friction.test.js +3 -3
  20. package/dist/test/graft.test.js +1 -1
  21. package/dist/test/historian-run-scenario.test.js +2 -2
  22. package/dist/test/promote.test.js +222 -5
  23. package/dist/test/readjudicate.test.js +212 -0
  24. package/dist/test/reflect.test.js +1 -1
  25. package/dist/test/repopath-seams.test.js +1 -0
  26. package/dist/test/seam-gates.test.js +6 -0
  27. package/dist/test/self-snapshot.test.js +2 -2
  28. package/dist/test/stats.test.js +17 -0
  29. package/docs/AGENTISM.md +58 -0
  30. package/docs/release/README.md +9 -0
  31. package/docs/release/TEMPLATE.md +10 -0
  32. package/docs/release/v0.2.7.md +7 -0
  33. package/docs/release/v0.2.8.md +8 -0
  34. package/graders/historian/judge-poststage.mjs +8 -3
  35. package/graders/pcb-agent/bench-approver.py +77 -0
  36. package/graders/pcb-agent/grader.mjs +130 -0
  37. package/graders/pcb-agent/mutate.sh +165 -0
  38. package/graders/pcb-agent/r20-bench.sh +27 -0
  39. package/graders/pcb-agent/reset-sandbox.sh +7 -0
  40. package/graders/pcb-agent/run-scenario-r20.sh +77 -0
  41. package/graders/pcb-agent/run-scenario.sh +94 -0
  42. package/graders/pcb-agent/seed/esp32s3-r12-BRIEF.txt +51 -0
  43. package/graders/pcb-agent/seed/spec.json +129 -0
  44. package/graders/pcb-agent/seed/tools/board_author_pcb.py +237 -0
  45. package/graders/pcb-agent/seed/tools/board_author_sch.py +118 -0
  46. package/graders/pcb-agent/seed/tools/board_finalize.sh +23 -0
  47. package/graders/pcb-agent/seed/tools/board_gate.py +69 -0
  48. package/graders/pcb-agent/seed/tools/board_gate_pcb.py +23 -0
  49. package/graders/pcb-agent/seed/tools/board_route.py +324 -0
  50. package/graders/pcb-agent/seed/tools/board_zone_pour.py +312 -0
  51. package/graders/pcb-agent/seed/tools/claims_lint.py +417 -0
  52. package/graders/pcb-agent/seed-r20-arena.sh +54 -0
  53. package/graders/pcb-agent/seed-sandbox.sh +13 -0
  54. package/package.json +1 -1
  55. package/plugin/abathur-command.md +3 -3
  56. 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
+ }