fix(opencode): tool-call telemetry captured NOTHING β 2,699/2,699 rows read text='unknown' - #298
Merged
Merged
Conversation
β¦s read text='unknown'
Root-caused two independent defects in scripts/collector/opencode/activity-plugin.js,
both verified against the installed `@opencode-ai/plugin` typings and reproduced
byte-for-byte in tests.
1. THE TOOL NAME WAS NEVER READ.
`tool.execute.after` did `input?.tool?.name || input?.name || "unknown"`.
The plugin contract (node_modules/@opencode-ai/plugin/dist/index.d.ts:249) is
`(input: { tool: string, sessionID, callID, args }, output: {title,output,metadata})`
β `input.tool` is a STRING, so `.name` is undefined on every call and the
literal fallback won 100% of the time. Fixed by reading the string form
(with a tolerated `{name}` object form for contract drift).
The `"unknown"` fallback is itself the hazard: it is a plausible tool name,
so a totally-broken capture was indistinguishable from real data. A failure
now writes the namespaced sentinel `__name_capture_failed__` plus
`name_captured:false` and `name_capture_shape:<typeof>` in the payload, and a
tool genuinely called "unknown" is still recorded as a real capture.
2. THE PAYLOAD WAS SHELL-MANGLED.
`emitEvent` ran execSync with the arguments joined into one shell string, all
values unquoted. Bash then BRACE-EXPANDED the unquoted JSON object into three
separate `b64:payload=` arguments and `emit` kept the last, which is exactly
the corruption seen in the live rows:
b64:payload=duration_ms:0
b64:payload=success:true
b64:payload=args_summary:{"name":"customize-opencode"} <- stored
Values containing spaces were also word-split (`b64:cwd=/tmp/dir with space`
became three argv entries). Fixed with `execFileSync(emit, argv)` β no shell,
no splitting, no quote removal.
Also corrected while in there, because both were fabricated measurements:
* `duration_ms` was `output.duration_ms` β a field that does not exist on this
hook β so it was always 0. Now measured across `tool.execute.before` β
`.after` via a bounded callID map, and ABSENT (not 0) when unmeasurable.
* `success: true` was hard-coded; the hook cannot observe a failure at all
(OpenCode throws past it). Replaced with `outcome: "completed"`.
* `session` was `currentSession`, set only by a `session.created` handler that
the plugin contract never calls. Now taken from `input.sessionID`.
* `args_summary` was a raw `.slice(0, 200)` of a JSON string, which produces
invalid JSON on truncation. Over-budget args now degrade to a structured
marker `{"_truncated":true,"keys":[...],"bytes":N}`.
GATE: scripts/collector/opencode/tests was NOT in run-tests.sh's target list β
166 tests that no gate has ever run, which is why this shipped. Added (this is
the only line of run-tests.sh touched: the pytest dir list, ~line 172).
READS MUST TOLERATE BOTH SHAPES. activity.events is append-only with a 180d TTL,
so the 2,699 malformed rows stay until they expire. No backfill and no
DELETE/ALTER was attempted. Any consumer of source='opencode' kind='tool-call'
must treat text='unknown' and a non-JSON payload as pre-fix garbage; the clean
discriminator is `payload.name_captured`, which only exists post-fix.
β¦sandbox
The previous commit was green on the dev host (`nix-shell`, 166/166) and RED on
the authoritative gate (`nix build .#checks.x86_64-linux.pytests`):
`collected=4539 passed=4524 skipped=1 failed=14 RESULT: FAIL`. I reported the
dev-host number as if it were the gate. It is not.
ROOT CAUSE. Both `write_mock_emit` (pre-existing) and `write_argv_recorder` (new)
write a stub script AT RUNTIME with `#!/usr/bin/env bash`.
* NixOS dev host: /usr/bin/env exists (symlink into coreutils) β stub execs.
* nix build sandbox: no /usr/bin/env β execve fails ENOENT.
nixpkgs' `patchShebangs src/scripts` (flake.nix) fixes shebangs of files IN THE
SOURCE TREE β which is why the real `scripts/collector/emit` works in the sandbox
β but it cannot touch a file a test creates while running. So the defect was
structurally unobservable in the tier I measured.
It was also mostly SILENT: `emitEvent` swallows every error by design, so node
still exits 0 when the stub cannot be exec'd; the tests failed later on an empty
list, pointing at the wrong thing.
FIX β deterministic, one place. New `tests/_mockbin.py` owns the shebang
(`/bin/sh`, POSIX bodies β the convention four other suites in this repo already
carry as hand-written comments) and refuses a body that supplies its own. Both
call sites go through it. Assertions are unchanged; the helper is fixed.
Plus, so the class cannot return:
* `test_the_mock_helper_can_actually_exec` β named tier precondition, runs in
both tiers, never skips.
* `test_no_runtime_written_shebang_uses_usr_bin_env` β structural scan of the
suite's sources, with a positive control proving the scan can flag an
offender. Its needles are assembled from char codes: the first version
matched its OWN source lines (the `pgrep -f` self-match).
* `drive_tool_call` now names the rival mechanism on an empty log instead of
letting `IndexError` misreport it.
TWO FURTHER DEFECTS THE SANDBOX FOUND, both real, neither fixed by weakening:
1. `_mockbin.py` was UNTRACKED, so the flake source omitted it and the sandbox
run collapsed to `collected=2 errors=2` β 168 tests silently not collected
while the per-directory floor (2850 global) was nowhere near tripping. Caught
by comparing COLLECTED COUNTS across runs, not by the exit code. Staged.
2. The corruption's SHAPE is shell-dependent. The pre-fix `execSync` bug has two
effects and only one is universal:
* word-splitting + quote removal β every POSIX shell, both tiers;
* brace expansion of the unquoted JSON object into three `b64:payload=`
args β bash only, and it is what produced the exact stored value
`args_summary:{"name":"β¦"}` in the 2,699 live rows (the workbench runs
bash). The sandbox's /bin/sh does not brace-expand.
The negative controls now assert the universal half unconditionally and gate
the bash-only signature on `_mockbin.shell_does_brace_expansion()`, a MEASURED
probe β with `test_brace_expansion_probe_agrees_with_the_shell_node_actually_uses`
pinning that the probe describes the same shell node's execSync runs. Both
shapes are asserted; neither branch is a skip.
TIERS, both reported:
dev host `nix-shell` opencode 170/170
SANDBOX `nix build .#checksβ¦pytests` opencode collected=170 passed=170
TOTAL collected=4543 passed=4542
skipped=1 failed=0 RESULT: PASS
ZacxDev
added a commit
that referenced
this pull request
Aug 3, 2026
β¦eploy the plugin declaratively (#302) * fix(opencode): a non-function export in #298 silently killed ALL activity telemetry opencode's plugin loader iterates EVERY named export of a plugin module and requires each to be a function. #298 added `export const _internals = {...}` alongside three exported helpers, so opencode rejected activity-plugin.js outright β "Plugin export is not a function" β and every hook stopped running the moment ship.sh deployed it. `emitEvent` swallows all errors by design, so the outage was completely silent. Measured on opencode 1.18.4 (2026-08-02) with probe plugins in the real ~/.config/opencode/plugin/, read off `opencode run --print-logs`: - object export + plugin factory β whole module rejected - plugin factory alone β loads clean - two function exports β BOTH invoked as plugin factories The last kind=tool-call row is 2026-08-03 02:32 UTC; #298 landed 21:47 local and ship.sh deployed it at 21:48. Two things this proves are NOT the cause, both of which looked plausible: the 1.18.9 β 1.18.4 downgrade (the probes discriminate at a fixed version), and the plugin directory (the loader glob is {plugin,plugins}/*.{ts,js} and 1.18.4 demonstrably reads BOTH β it logged a load error for each path). Fixes: - export only ActivityPlugin; helpers are module-private again. - deploy declaratively from nix/home.nix into the singular plugin/ dir, exactly like guard.js and env.js. This replaces deploy-plugin.sh, a hand-run script that had to be remembered per host and was not: it ran on the workbench on 2026-07-29 and NEVER on the laptop, which recorded zero kind=tool-call rows for the plugin's entire existence. - an activation step removes the plural-dir symlink the old script left on the workbench, which would otherwise load the plugin twice and double-emit. Tests: the three tests removed here exercised deploy-plugin.sh and stayed green through the whole outage β "the script makes a symlink" says nothing about whether opencode can load what it points at. Replaced with the loader contract (every named export is a function; exactly one export) and the deployment declaration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(opencode): the switch that deploys the plugin would have ABORTED on the workbench home.file now owns ~/.config/opencode/plugin/activity.js, but a hand-made symlink already sits at exactly that path on the workbench (created 2026-08-02 while diagnosing the outage). checkLinkTargets refuses to clobber a non-store file at a managed path and aborts the ENTIRE switch β and `force` does not displace it. So the activation step has to run entryBefore checkLinkTargets, not after writeBoundary. Widened to clean BOTH stale copies: the singular path for the reason above, and the plural plugins/ path because opencode globs {plugin,plugins}/*.{ts,js} and reads both, so leaving it would load the plugin twice and double-emit. Only ever removes a symlink pointing at the repo's activity-plugin.js; a store symlink, a real file, or a foreign plugin is left untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev
added a commit
that referenced
this pull request
Aug 3, 2026
β¦run (#306) `scripts/dl-router/tests` was absent from `scripts/run-tests.sh`'s target list since the suite was written: 989 tests, zero of them gated. Same declarations-vs-instances shape as #298 (166 opencode tests) and #276 (913 guard-core tests) β one missing line in a target list. Adding it surfaced two SANDBOX-ONLY portability defects, both of the kind that are structurally invisible on a dev host: * test_setup_script.py's write_pgrep() wrote a stub with `#!/usr/bin/env bash`. The nix build sandbox has no /usr/bin/env; every NixOS dev host does. Now goes through the helper #298 landed. * test_cli.py / test_server_wiring.py hard-coded port 8799 for their "sidecar is DOWN/unreachable" assertions. That is a claim about the whole machine, not the test β an orphaned python3 (pid 2994086, started Aug 1 13:40, ppid 1) was listening on it and the test reached a REAL dl-router, failing with `sidecar HTTP 409: not_owned_tab`. Ports now come from a `closed_port` fixture. Shared instead of re-derived (RULES.md "One rule, one place"): * `scripts/collector/opencode/tests/_mockbin.py` -> `scripts/testlib/ mockbin.py`, importable by any suite. * #298's runtime-shebang scan was scoped to ONE directory, which is exactly why it could not see the dl-router defect. The scanner moved to `scripts/testlib/shebang_scan.py` and the guard is now REPO-WIDE (`scripts/tests/test_runtime_shebangs.py`) with a pinned allowlist that fails BOTH ways β an unpinned offender and a pin that matches nothing. MIN_TESTS 2850 -> 5600. The old floor had drifted below HALF the real total, so a whole 989-test suite could have vanished underneath it. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev
added a commit
that referenced
this pull request
Aug 3, 2026
β¦laims it disproved (#308) Records the arc: an audit of the Claude/opencode setup + activity telemetry that turned into 12 merged PRs across devrc and one Flux commit on homelab-talos. Headline measurements: always-on session context 63,954 -> 43,420 B; opencode AGENTS.md 47,741 -> 39,280 B; ClickHouse store 112.4 GB -> 82.1 MB; MEMORY_LIMIT_EXCEEDED 113,378 -> 0; gated tests 4,373 -> 5,792. Four claims asserted during the session and later DISPROVED are recorded with the evidence, because each was believed and acted on for a while: 1. "ClickHouse is full because activity.events/payload is too big" β it is 27 MiB, 0.03% of the instance; the cause was un-TTL'd system.trace_log. 2. "opencode ignores its declared model, and nav has zero turns" β the config had landed 29 days AFTER the telemetry window started, and zero opencode turns had occurred since the deploy. 3. "the clickup skill does not ship to the workbench" β ship.sh:368 already rsyncs it; and moving it into the nix store would have turned a 0600 credential file into 0444 world-readable. 4. "A1 fixed insights.py" β it fixed the memory ceiling and unmasked a different fault (Code 209 socket timeout on the nebula path only). Also records the outage this session CAUSED: PR #298's `export const _internals` killed all opencode telemetry on both hosts for ~11 hours, because opencode's loader rejects a whole plugin module if any named export is not a function, and emitEvent swallows every error by design. It was found by luck, which is the justification for the telemetry deadman work now in flight. The through-line, and the reason this doc exists: every single defect found was SILENT. A guard that structurally could not fire, a plugin that did not load, 1,497 tests never in a runner's target list, insights.py exiting 0 on a healthy pipeline, harnesses that tested nothing, and a runner printing FAIL while exiting 0. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 3, 2026
ZacxDev
added a commit
that referenced
this pull request
Aug 3, 2026
β¦ver run (#309) `scripts/run-node-tests.sh` hard-coded its collection to one directory: FILES=(scripts/browser-bridge/tests/*.test.mjs) so every other `.test.mjs` suite in the repo was invisible to the only check that runs node. Measured at origin/main (13bc8bd): * scripts/dl-router/tests 508 tests, never gated * scripts/collector/browser-ext/tests 21 tests, never gated 529 tests, ungated since each suite was written. The gate reported `RESULT: PASS` with a 468-test total the whole time β a hard-coded list cannot know what it is not looking at. Fourth instance of the same shape (#276: 913 pytest tests behind one list entry, #298: 166, #306: 989). Adding two lines would leave the NEXT suite ungated identically, so collection is now DISCOVERY (bash globstar over scripts/**/*.test.mjs) plus a TWO-WAY PIN: * a discovered directory absent from SUITES -> FATAL (forces an accounting entry with a measured floor, rather than being swept in under the total) * a pinned suite discovery does not find -> FATAL (the suite vanished) Discovery alone would reintroduce the silent-collapse hole the old hard-coded glob at least did not have: an emptied dl-router/tests would just collect fewer files and still pass over a global floor. Each suite also runs in its OWN `node --test` invocation with its own TAP summary and its own floor. A single global floor of 970 is fully satisfied by browser-bridge (468) + dl-router (508) with browser-ext's 21 tests entirely gone; per-suite floors make that loud. A PORTABILITY DEFECT found while building this, worth recording because the first draft shipped it and the harness hid it: discovery used `find -printf '%h\n' 2>/dev/null`. `-printf` is a GNU extension, and THREE different `find`s are reachable from this repo β busybox under bash (~/.nix-profile/bin/find, rejects it), bfs 4.1.1 under the interactive zsh, GNU findutils in the nix sandbox. Paired with `2>/dev/null` the rejection became an EMPTY discovery list with no error β a false "no suites found". Collection is now a bash builtin (globstar), which depends on no external binary and behaves identically in every tier; `test_runner_does_not_use_find_printf` pins it. MEASURED, both tiers: nix build .#checks.x86_64-linux.nodetests 997 tests / 997 pass / 0 fail bash scripts/run-node-tests.sh (dev host) 997 tests / 997 pass / 0 fail 468 (baseline, browser-bridge) + 508 + 21 = 997. No unexplained delta. scripts/tests/test_run_node_tests_suites.py guards the pin, with its regression and invariant guards labelled honestly in the module docstring, mutation proofs that the guard goes red naming the offending directory, and a positive control on both parsers (the reassuring answer here is an empty set, which an unwired harness also produces). The 529 newly-gated tests are NOT regression coverage for this change β they are pre-existing tests whose value is that they now run at all. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev
added a commit
that referenced
this pull request
Aug 3, 2026
β¦onger be silent (#311) PR #298 killed all opencode telemetry on both hosts for ~11 hours and nothing reported it; `emitEvent` swallows every error by design, and "no rows" is exactly what a healthy-but-idle source looks like. It was found by luck. Two more of the same shape were already sitting in the data: the laptop had zero `kind=tool-call` rows for its entire existence, and `kind=session-create` has never emitted a row. New: `scripts/collector/deadman.py` β per (host, source) liveness over `activity.events`, surfaced as the `tlm` pill on the workbench i3 bar (a new `telemetry` source in `bar-status-poll`, signal 17, + a rising-edge dunst toast). One workbench runner covers BOTH hosts because it reads the shared table. What makes it not cry wolf, and not lie: - Silence is counted in ACTIVE time (5-min buckets in which ANY source on that host emitted), not wall time. Overnight/away time is not in the set, so an on-demand source is not punished for the operator being asleep. - The budget is MEASURED per pair: clamp(2 x p99 active-gap, 2h, 48h). Measured 2026-08-03: keys/i3/tmux land on the 2h floor, workbench/opencode 11.5h, workbench/tool 31.1h. Nothing is hand-tuned; the cap (48 active hours) sits above the largest gap observed anywhere in 14 days (224 buckets). - Expected-present is measured too: a pair is judged only if it cleared a baseline, so workbench/browser (0 rows, correctly absent) can never alarm. - "Cannot tell" is not "healthy": not-configured / unreachable / query-failed / no-data are each their own state, and `ok` is unreachable unless rows came back AND >=1 pair was measured. A persistent unknown shows a VISIBLE `tlm ?` pill (grace-gated 30 min) rather than the empty block every other source renders. Docs corrected against measurement, not prose: the activity SKILL.md claimed keylog/i3/browser were "GUI-only -> laptop only; the workbench is headless". The workbench emits i3 (41,001 rows) and keys (37,376), both fresh; only `browser` is genuinely laptop-only. Controls (a zero is never reported alone): positive 1 dead on live data with workbench/keys silenced 6h; 0 on the same data untouched, same code path negative dead endpoint -> `unreachable` + `tlm ?`, never green mutation 17/17 mutants killed, incl. a byte-identical no-op control that stayed green Gate: pytests 5850 collected / 5849 passed / 1 skipped / 0 failed in BOTH tiers (nix sandbox and dev host); nodetests 468/468. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
All 2,699
source='opencode' kind='tool-call'rows on the workbench carrytext='unknown', and a payload that is not valid JSON. OpenCode tool distribution has never been measurable, so we cannot say whether opencode has the same Bash-over-Grep bias Claude Code demonstrably has.Root cause β two independent defects
1. The tool name was never read.
tool.execute.afterdidinput?.tool?.name || input?.name || "unknown". The installed contract (~/.config/opencode/node_modules/@opencode-ai/plugin/dist/index.d.ts:249) is:input.toolis a string, so.nameisundefinedon every single call and the literal fallback won 100% of the time.2. The payload was shell-mangled.
emitEventranexecSyncwith the arguments joined into one shell string, values unquoted. Bash brace-expanded the unquoted JSON object into three separateb64:payload=arguments;emitkeeps the last. Reproduced byte-for-byte intest_harness_negative_control_sees_the_mangled_payload:Values with spaces were word-split too:
b64:cwd=/tmp/dir with spaceβ 3 argv entries.The fix
execFileSync(emit, argv)β no shell, no splitting, no quote removal.extractToolName()reads the string form (tolerating a{name}object form for contract drift).unknownwas a plausible tool name; failures now write the namespaced sentinel__name_capture_failed__plusname_captured:falseandname_capture_shape:<typeof>. A tool genuinely calledunknownstill recordsname_captured:true.duration_mswasoutput.duration_msβ a field that does not exist on this hook β so it was always 0. Now measured acrosstool.execute.beforeβ.aftervia a boundedcallIDmap, and absent (not 0) when unmeasurable.success:truewas hard-coded; the hook cannot observe a failure at all (OpenCode throws past it). Replaced withoutcome:"completed".sessioncame fromcurrentSession, set only by asession.createdhandler the contract never calls. Nowinput.sessionID.args_summarywas a raw.slice(0,200)of a JSON string β invalid JSON whenever it truncated. Now degrades to{"_truncated":true,"keys":[β¦],"bytes":N}.Why this shipped: the suite was never gated
scripts/collector/opencode/testswas not inrun-tests.sh's target list β 166 tests that no gate has ever run. Added. This is the only line ofrun-tests.shthis PR touches: the pytest dir list (~line 172).MIN_TESTSdeliberately untouched.The pre-existing
test_plugin.pycould not have caught either bug: it tests a copy ofemitEventre-typed inside the test string and builds emit's argv itself withsubprocess.run([...]), so neither the real function nor the real hook handler is ever executed. Both of its tool-call tests pass unchanged against the broken code.Harness validation + control pair
Negative controls (harness must go RED on known-bad code) β a vendored reproduction of the two pre-fix mechanisms:
test_harness_negative_control_sees_the_lost_tool_nametext == 'unknown'test_harness_negative_control_sees_the_mangled_payloadPositive control for the reassuring zero β
corrupted_arg_count()returns 0 for the fixed code:proj,with,space) + 3b64:payload=fragmentsβ Worth recording: my first positive control measured 0 and failed. It asserted on a spacey value inside
args_summary, butJSON.stringifywraps string values in", so the shell preserves those spaces. The splitting hits the fields the plugin does not quote (project,cwd). Without that control,test_no_argv_corruption's 0 would have been read as proof while the counter was blind to the case it exists for.β Correction β the first push was RED on the authoritative gate
The original version of this section reported
4568 collected / 4567 passed / 0 failedas "the full gate". That was the dev-host tier (nix-shellwith the flake's package set). It is not the gate. The authoritative gate isnix build .#checks.x86_64-linux.pytests, and on the first push it was RED.I reproduced it independently before fixing anything, and ran the discriminating control the coordinator also ran: the branch fails alone, so it is not a cross-PR interaction.
Why the dev host could not see it
Both
write_mock_emit(pre-existing) andwrite_argv_recorder(new) write a stub script at runtime with#!/usr/bin/env bash./usr/bin/envexists (lrwxrwxrwx /usr/bin/env -> /nix/store/β¦-coreutils-9.10/bin/env) β the stub execs, suite green./usr/bin/envβexecvefailsENOENT.nixpkgs'
patchShebangs src/scripts(flake.nix) rewrites shebangs of files in the source tree β which is why the realscripts/collector/emitworks in the sandbox β but it cannot touch a file a test creates while running. The defect was structurally unobservable in the tier I measured.It was also mostly silent:
emitEventswallows every error by design, so node still exits 0 when the stub cannot exec; tests then failed on an empty list (IndexError), pointing at the wrong thing.The fix β deterministic, one place, assertions untouched
New
tests/_mockbin.pyowns the shebang (/bin/sh, POSIX bodies β the convention four other suites in this repo already carry as hand-written comments) and raises if a call site supplies its own. Both call sites go through it. No assertion was weakened.Added so the class cannot return:
test_the_mock_helper_can_actually_execβ named tier precondition, runs in both tiers, never skips.test_no_runtime_written_shebang_uses_usr_bin_envβ structural scan of the suite's sources, with a positive control proving the scan can flag an offender. Its needles are assembled from char codes: the first version matched its own source lines (thepgrep -fself-match).drive_tool_callnow names the rival mechanism on an empty log instead of lettingIndexErrormisreport it.Two further defects the sandbox exposed β neither fixed by weakening
1.
_mockbin.pywas untracked, so the flake source omitted it (flakes only see tracked files) and the run collapsed toopencode collected=2 errors=2β 168 tests silently not collected, while the global floor (2850) was nowhere near tripping. Caught by comparing collected counts across runs, not by an exit code. Staged.2. The corruption's SHAPE is shell-dependent. The pre-fix
execSyncbug has two effects and only one is universal:b64:payload=args β bash only, and it is what produced the exact stored valueargs_summary:{"name":"β¦"}in the 2,699 live rows (the workbench runs bash). The sandbox's/bin/shdoes not brace-expand.The negative controls now assert the universal half unconditionally and gate the bash-only signature on
_mockbin.shell_does_brace_expansion()β a measured probe, withtest_brace_expansion_probe_agrees_with_the_shell_node_actually_usespinning that the probe describes the same shell node'sexecSyncactually runs. Both shapes are asserted; neither branch is a skip.Both tiers, both numbers
nix-shell β¦ python -m pytest scripts/collector/opencode/tests/nix build .#checks.x86_64-linux.pytestsTOTAL collected=4543 passed=4542 skipped=1 failed=0 Β· RESULT: PASS
Progression on the authoritative gate:
8306719403f65f(this branch)origin/mainbaseline in this tier = 4373 collected (the opencode suite was not in the target list at all).Red/green matrix β SANDBOX TIER
Base ref
origin/main= 73f5ec0. Method: at403f65f,git checkout origin/main -- scripts/collector/opencode/activity-plugin.js, runnix build .#checks.x86_64-linux.pytests, restore.scripts/collector/opencode/testsThe 11 red-at-base, in the authoritative tier:
Green at both are the 2 harness negative controls + 2 positive controls (they drive the vendored buggy code, so they must be green in both β correct by construction), and 4 tier/structural guards +
test_b64_values_are_real_base64, all labelled in-file as preconditions/invariant guards, not regression coverage.Crucially, the harness negative controls now pass in the tier that gates merges. On the first push they were 2 of the 14 failures β so the harness had never been validated where it counts, and every surviving green there proved nothing.
Control pairs re-confirmed in the sandbox tier
All are pinned equalities that passed inside
nix build:corrupted_arg_count(argv entries with no=)b64:payload=fragments from the buggy emitterJSONDecodeError(tier-independent)Adjacent finding (not fixed here β would re-block this PR)
scripts/dl-router/testsis also absent fromrun-tests.sh's target list, andtest_setup_script.py:134writes a#!/usr/bin/env bashstub and execs it β the same latent sandbox failure. Adding that suite is a separate change; flagging it rather than expanding this PR.π€ Generated with Claude Code
Existing rows
activity.eventsis append-only with a 180d TTL. No backfill, no DELETE, no ALTER was attempted. The 2,699 malformed rows stay until they expire. Readers must tolerate both shapes: treattext='unknown'with a non-JSONpayloadas pre-fix garbage. The clean discriminator ispayload.name_captured, which only exists post-fix.Not verified
The fixed plugin has not been exercised by a real OpenCode session β that needs a
home-manager switchplus an opencode run, and this task was explicitly told not to switch or restart anything. Every claim above rests on the real hook handlers being driven under node through the realemitinto the realcollector.parse_line, against the hook shape taken from the installed typings.