Skip to content

feat(feedback): implement feedback-o-tron invoke dispatch (FFI-backed)#193

Merged
hyperpolymath merged 1 commit into
mainfrom
claude/feedback-otron
Jun 4, 2026
Merged

feat(feedback): implement feedback-o-tron invoke dispatch (FFI-backed)#193
hyperpolymath merged 1 commit into
mainfrom
claude/feedback-otron

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

What

Builds feedback-o-tron for real (the follow-up you green-lit), replacing the "Grade D Alpha" stub in feedback_ffi.zig's boj_cartridge_invoke with a live, state-machine-backed implementation, and switching the e2e cycle to the cartridge's actual declared tools.

Key insight

The stub already dispatched the correct cartridge.json tool names (feedback_register_channel/start_collecting/submit/get_stats) — it just returned {"status":"stub"}. The old e2e cycle invoked tools that never existed (open_channel/summary/export/status/list_channels), which is why it got {}. So the fix is: make the FFI real, and point the test at the real tools.

Changes

  • feedback_ffi.zigboj_cartridge_invoke now backs the 4 manifest tools with the existing fb_* channel state machine:
    • feedback_register_channelfb_register{"slot":N,"channel":C,"state":1}
    • feedback_start_collectingfb_start_collecting{"slot":N,"collecting":true,"state":2}
    • feedback_submitfb_submit{"recorded":true,"slot":N,"feedback_count":K}
    • feedback_get_statsfb_count/fb_positive_count/… → {"total_feedback":…,"positive":…,…}
    • channel/slot/sentiment are read from the JSON args via a small, std.json-free byte scanner (jsonInt) — sufficient for the flat arg objects and version-stable.
  • feedback_ffi.zig test — the in-file invoke test now drives the real register → start → submit ×2 → get_stats cycle and asserts the real shapes (so zig build test covers it in CI).
  • tests/e2e_full.sh Step 6 — rewritten to exercise the real tools via the "arguments" object (the field BojRest forwards to the FFI); asserts slot / collecting:true / recorded:true / total_feedback. Removes the FEEDBACK_OTRON skip from fix(rest): restore tiered /menu + make /cartridges & feedback-o-tron E2E truthful #192.
  • No Elixir change — the router already forwards arguments and returns the FFI's parsed JSON as the body. The cross-language flow (Zig JSON → Invoker parse → Jason re-encode → test) lands the keys the assertions read.

Verification — please note

e2e_full.sh passes bash -n + shellcheck locally. The Zig could not be compiled locally: this sandbox's network is GitHub-only — ziglang.org returns 403 and Zig isn't on GitHub releases, and apt is blocked. So the FFI is verified by CI (abi-drift.yml + e2e.yml both run zig build) and the added Zig unit test. I'll watch this PR and fix any compile error promptly. (If you'd rather I verify locally, allowing ziglang.org in the environment's network policy would let me install Zig 0.15.1.)

The ABI is unchanged (only the dispatch body), so abi-drift should stay green.

🤖 Draft via Claude Code.


Generated by Claude Code

…cked)

Replaces the "Grade D Alpha" stub in feedback_ffi.zig's boj_cartridge_invoke
with a real implementation wired to the existing fb_* channel state machine,
and switches the e2e cycle to the cartridge's actual declared tools.

- boj_cartridge_invoke now backs the four cartridge.json tools with live
  state: feedback_register_channel -> fb_register (returns the slot),
  feedback_start_collecting -> fb_start_collecting, feedback_submit ->
  fb_submit (returns recorded + running count), feedback_get_stats ->
  fb_count/positive/negative/neutral + fb_state. Arguments (channel, slot,
  sentiment) are read from the JSON args via a small std.json-free byte
  scanner (jsonInt) — sufficient for the flat arg objects and version-stable.
- Updated the in-file Zig unit test to drive the real register -> start ->
  submit x2 -> get_stats cycle and assert the real result shapes.
- tests/e2e_full.sh Step 6: the previous cycle invoked non-existent tools
  (open_channel/summary/export/status/list_channels) the FFI never declared.
  Rewritten to exercise the real manifest tools via the "arguments" object
  (the field BojRest forwards to the FFI), asserting slot / collecting /
  recorded / total_feedback. Removes the FEEDBACK_OTRON skip guard.

No Elixir change: the router already forwards "arguments" and returns the
FFI's parsed JSON as the response body.

Verification: e2e_full.sh passes bash -n + shellcheck here. The Zig could
NOT be compiled locally (this sandbox's network is GitHub-only; ziglang.org
returns 403 and Zig is not on GitHub releases), so the FFI is verified by
CI's `zig build` (abi-drift.yml + e2e.yml) and the added Zig unit test. I'll
watch the PR and fix any compile error.

https://claude.ai/code/session_019tMcRS1Dm1nWjjYP4WvbJa
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

🏁 path-claims bench

Commit d6e4678

Numbers
path-claims bench  (node v22.22.3)

  scenario                                              iters       ms        ns/op          ops/s
  --------------------------------------------------------------------------------------------------------------
  register: 10 active claims, 3 new paths               50000 iters    138 ms      2.77 µs/op    360.4k ops/s
  register: 100 active claims, 3 new paths              20000 iters    249 ms     12.49 µs/op     80.1k ops/s
  register: 1000 active claims, 3 new paths              5000 iters    769 ms    153.95 µs/op      6.5k ops/s
  register: 100 active claims, 20 new paths              5000 iters    276 ms     55.33 µs/op     18.1k ops/s

  pathsOverlap: deep diverge at segment 4             1000000 iters    122 ms     122.8 ns/op     8.14M ops/s
  pathsOverlap: short prefix match                    1000000 iters    105 ms     105.9 ns/op     9.45M ops/s

  refresh (existing claim)                             100000 iters      8 ms      81.3 ns/op    12.30M ops/s
  list (100 active claims)                              50000 iters    226 ms      4.54 µs/op    220.3k ops/s

  (Bench numbers depend on host; use deltas across commits, not absolute values.)

Host-dependent — compare deltas across commits, not absolute values.

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 279 issues detected

Severity Count
🔴 Critical 15
🟠 High 131
🟡 Medium 133

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Stale AI session file -- delete",
    "type": "stale",
    "file": "GEMINI.md",
    "action": "delete",
    "rule_module": "root_hygiene",
    "severity": "medium"
  },
  {
    "reason": "Action    if: always()\n        uses: actions/upload-artifact@ea165 needs attention",
    "type": "unpinned_action",
    "file": "e2e.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action perpolymath/standards/.github/workflows/governance-reusable.yml@main\n needs attention",
    "type": "unpinned_action",
    "file": "governance.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in abi-drift.yml",
    "type": "missing_timeout_minutes",
    "file": "abi-drift.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in codeql.yml",
    "type": "missing_timeout_minutes",
    "file": "codeql.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in container-publish.yml",
    "type": "missing_timeout_minutes",
    "file": "container-publish.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

@hyperpolymath
hyperpolymath marked this pull request as ready for review June 4, 2026 12:40
@hyperpolymath
hyperpolymath merged commit 3f49db3 into main Jun 4, 2026
29 of 30 checks passed
@hyperpolymath
hyperpolymath deleted the claude/feedback-otron branch June 4, 2026 12:40
hyperpolymath added a commit that referenced this pull request Jun 5, 2026
…e2e (#194)

## Why
#193 merged with E2E red — feedback invokes returned `{}`. I installed
Zig 0.15.2 (it's on **PyPI** — the sandbox blocks `ziglang.org` but not
PyPI) and traced it to root cause, locally, end-to-end.

**The `{}` was never my FFI code** (it compiles, 15/15 unit tests pass,
and the real `boj-invoke` CLI returns correct JSON). It was two infra
faults in one e2e step, plus an architectural fact:

1. **`boj-invoke` was never built** — the "Build FFI libraries" step ran
`zig build`, but the CLI the Elixir Invoker shells out to is only
produced by `zig build invoke`. No CLI → `:cli_missing` → `{}` for
*every* FFI tool invoke.
2. **Cartridge `.so`s were never built either** — after `cd ffi/zig`,
the `for cart in cartridges/*/ffi` loop globbed from the wrong
directory, matched nothing, and `|| true` hid it.
3. **The invoker is fork-per-request** (ADR-0005): each invoke is a
fresh process, so the cartridge's in-memory channel state does **not**
persist between HTTP calls. A multi-step `register → submit → stats`
cycle cannot span calls (proved: two separate submits both
`recorded:false`).

## What
- **`feedback_ffi.zig`** — `feedback_submit` is now
**self-provisioning**: when the slot isn't an active collecting channel
(the stateless case) it registers + starts collecting before recording,
so a lone submit returns `recorded:true` in one call. Within one process
(a pooled invoker, or the unit test) an already-collecting slot is
reused and counts still accumulate. Added a cold-start unit test →
**15/15 pass**.
- **`tests/e2e_full.sh`** Step 6 — assert only what a stateless invoker
can truthfully deliver: register returns a slot, each submit records,
`get_stats` returns the stats shape. Cross-call accumulation (the full
state machine) is left to the pooled-invoker follow-up and is covered by
the in-process Zig unit test.
- **`.github/workflows/e2e.yml`** — build `boj-invoke`; contain the `cd`
in a subshell so the cartridge loop globs from the repo root; tidy the
loop to an explicit `if` (silences SC2015).

## Verification (local, Zig 0.15.2 — CI's exact version)
```
zig build               # exit 0, libfeedback_mcp.so
zig test feedback_ffi   # All 15 tests passed
# real boj-invoke CLI (what the Invoker calls):
register → {"slot":0,"channel":2,"state":1}
submit   → {"recorded":true,"slot":0,"sentiment":1,"feedback_count":1}
stats    → {"slot":0,"total_feedback":0,...}   # truthfully per-call
```
The full Elixir server e2e needs `hex.pm` (blocked in this sandbox), but
`boj-invoke` is exactly what the Invoker shells out to, so the feedback
invoke path is fully covered. ABI is unchanged (dispatch body only) →
`abi-drift` stays green.

🤖 Draft via Claude Code.

---
_Generated by [Claude
Code](https://claude.ai/code/session_019tMcRS1Dm1nWjjYP4WvbJa)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants