The #2787 change added client_adapter_pi_emits_string_enum_issue2787 directly below client_adapter_pi_wraps_result_and_honors_abort and, in the same hunk, removed that test's check that the generated abort handler kills the cli child (if (!child.killed) child.kill();). The generator still emits the line; restore the assertion byte-for-byte so the abort path keeps its coverage.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
augment() is awaited inline in 'tool.execute.after', so a hook-augment
child that never exits blocked every grep/glob/read tool result in the
host OpenCode session. The emitted helper now:
- kills the child (SIGKILL) and resolves '' after AUGMENT_TIMEOUT_MS
(10000ms, a backstop above hook-augment's own in-process deadline,
which is clamped to 10000ms, so the child's graceful self-exit and
its hook-augment-timeouts.log breadcrumb always fire first)
- listens for child.stdin 'error' and guards stdin.end(), because stream
errors fire on the stream, not the child, and previously escaped as
unhandled 'error' events (EPIPE with a >64KB buffered write, ENOENT
spawn), crashing the host under Bun 1.4.0
- destroys stdout in the timeout path to release the read end promptly
Fixes#2693
Signed-off-by: Lance Woodson <65194841+woodsonl@users.noreply.github.com>
DeusData asked (2026-09-19T20:25:34Z on PR #2089) to place the hooks in
the server entry rather than returning them from setup(), since the
server runtime reads default.server and dispatches the hooks it
returns, while the V2 config loader only reads id + setup()/effect()
and has no tool domain yet to register against.
Moved the existing hook body under server: async (ctx) => {...} and
added an empty setup() {}. Updated the emission test to check for both
and to assert the old async setup(ctx) { shape is gone.
Signed-off-by: Amir Fathi <amirfathi.me@gmail.com>
OpenCode's V2 loader validates only the module's default export,
expecting {id, setup()|effect()}. cbm_client_adapter_opencode still
emitted the V1 named export (export const CodebaseMemory = ...), so
every server start failed with SchemaError: Missing key at ["default"].
Wrap the existing hook logic in a default export with an id and a
setup(ctx) function; the returned hooks object is unchanged.
Fixes#2077
Signed-off-by: Amir Fathi <amirfathi.me@gmail.com>
- Fix client adapter passing output?.args instead of input?.args in tool.execute.after hook (closes#1737)
- Add tool_search and tool_search_regex permissions to OpenCode subagent profiles so deferred MCP tools can be discovered and unblocked
- Add regression tests for OpenCode adapter hook args and subagent profile permissions
Signed-off-by: Choti Wongbussakorn <126886556+Chewji9875@users.noreply.github.com>
Resolves#1806 where Pi extension crashed on startup with 'Cannot read
properties of undefined (reading properties)' error.
- Convert JSON schema to TypeBox format in Pi extension generation
- Add TypeBox import to generated extension
- Update test to expect TypeBox format instead of JSON
- Handle all schema types: string, integer, boolean, array, enums
- Preserve descriptions and optional field handling
The generated extension now properly works with Pi's tool registration
API that requires TypeBox Type.Object schemas.
Signed-off-by: Sakil Ansari <sakilansari@example.com>
Signed-off-by: Sakil Ansari <sakilansari@Sakils-MacBook-Pro.local>
Add omp (Oh My Pi) as a STABLE, detected client surface with full
four-layer integration: MCP server registration, durable instructions,
skill, and three direct-MCP tiered subagent profiles (Scout/Verify/
Auditor). This matches Claude Code's integration depth — omp subagents
have direct MCP access (confirmed via ablation testing), unlike
parent-handoff-only clients (Pochi, Cursor, Rovo, Augment).
Schema (agent_clients.h):
New CBM_AGENT_CLIENT_OMP enum value, positioned before COUNT.
Profile (agent_clients.c):
- stability: CBM_AGENT_STABLE (auto-detected, auto-installed)
- capabilities: MCP | INSTRUCTIONS | SKILL | AGENT
No HOOK — omp's context event is fire-once advisory only; the
recommended upstream path is skill + agents + MCP, not TS hooks.
- detection_command: "omp" (specific enough per existing clients)
- marker: ~/.omp/agent (omp-managed config root)
- resolve_path: ~/.omp/agent/mcp.json
- json_client: true (uses standard mcpServers JSON edit path)
- agent_json_canonical: added to type:"stdio" branch alongside
GitLab Duo and Visual Studio (omp's schema expects this field)
Dialect (agent_profiles.h/.c):
New CBM_GRAPH_DIALECT_OMP, direct-capable (not in handoff-only list).
- tool prefix: "mcp__codebase_memory_mcp_" (single underscore,
hyphens→underscores — verified at runtime via omp device inventory)
- render: YAML frontmatter with tools (read/grep/glob + tier-specific
MCP tool allowlist), read-summarize: false (exact source verification
for evidence tiers — omp's read returns structural summaries by
default), autoloadSkills: [codebase-memory] (subagents start blank)
Install/uninstall (cli.c):
- install_omp_durable_context: writes AGENTS.md + skill +
3 tiered agent profiles to ~/.omp/agent/
- uninstall_omp_durable_context: removes managed block + skill +
tiered profiles
- legacy_omp_verify_agent_content: migration source for prior installs
- Wired into install_agent_client_registry and
uninstall_agent_client_registry loops
Design decisions (documented for reviewers):
1. No HOOK capability: omp's tool_call event can only block/pass (no
augmentation channel like Claude's additionalContext). A context-
event nudge was tested but is advisory-only and excluded from the
upstream path to keep the PR minimal.
2. read-summarize: false: omp's read tool returns structural summaries
by default; Tier 2/3 require exact snippets for material claims.
3. autoloadSkills: omp subagents start with no conversation history;
without autoload the skill decision matrix is never loaded.
4. Direct-capable (not handoff): ablation testing confirmed omp
subagents can call mcp__* tools directly. write→xd:// dispatch
does NOT work in subagents ("xd:// is not mounted in this session").
Test coverage:
- test_agent_clients.c: omp in stable_ids (19 entries), capabilities
array, json_schemas (type:stdio assertion), foreign_entries
- test_agent_profiles.c: omp in direct_dialects array, direct-capable
assertion, dedicated test verifying direct profile has
mcp__codebase_memory_mcp_ prefix + read-summarize + autoloadSkills,
and handoff profile excludes MCP tools
- test_cli.c: required_agents updated to 44, all 43→44 / 37→38 string
contracts updated across README/npm/index.html/llms.txt/main.c
Build: scripts/build.sh passes (exit 0).
Tests: scripts/test.sh passes (exit 0, all suites green).
Lint: scripts/lint.sh has one pre-existing error in pass_lsp_cross.h
(parameter name mismatch imp_keys/imp_vals vs imp_names/imp_qns),
unrelated to this change.
Signed-off-by: DavidHLP <lysf15520112973@163.com>
Maintainer asked for in-tree coverage of {content, details} wrapping and
how tool errors are surfaced, plus a cheap abort-signal line. String-shape
only; no live Pi process.
Signed-off-by: Alex Musichen <alex.musichen@gmail.com>
Pi 0.84.2 calls execute(toolCallId, params, signal, onUpdate, ctx).
The generated (args, ctx) shape bound the call id as the MCP arguments.
Forward params and signal, pin the @earendil-works/pi-coding-agent
contract in the generated header, and lock the 5-arg form in tests.
Signed-off-by: Alex Musichen <alex.musichen@gmail.com>
The generated execute forwarded the raw MCP JSON directly, but pi's
ToolDefinition.execute must return { content: [{ type: 'text', text }],
details } — a result without a content array crashes the TUI's
getTextOutput on result.content.filter(...).
Request raw JSON from the CLI ('--json') so the bridge parses the MCP
result instead of the human-readable text, then wrap it: pass the content
array through, throw on transport errors, and stringify anything else.
Adds coverage asserting the corrected execute shape and the --json flag.
Signed-off-by: Alex Musichen <alex.musichen@gmail.com>
The generated Pi extension registered each MCP tool as { name, run }, but
Pi's ToolDefinition requires label, description, parameters, and execute.
Tools registered that way carried no parameter schema, so strict providers
such as xAI/Grok reject the request with a 422 'missing field parameters',
and the tools were uncallable because Pi invokes execute, never run.
Emit the full tool shape from the registry: label/description via new
accessors, the input_schema embedded directly as a JSON object literal, and
execute instead of run.
Signed-off-by: Alex Musichen <alex.musichen@gmail.com>
Two contributors independently sent the same fix for #1550 while ours was
already in CI (#1553 by @musichen, who also reported the bug, and #1564 by
@henri-edh). Rather than close both as duplicates, this takes the part of
theirs that ours was missing.
@henri-edh's comment names the actual mechanism: Pi loads an extension via
jiti.import(path, { default: true }) and rejects anything that is not a
function, so a named-only export hands the loader the module namespace object
and the install fails with "Extension does not export a valid factory
function". Ours said "Pi calls the default export" — true, but it does not tell
the next reader why a named export cannot simply be added alongside, which is
exactly the change someone would try. Their wording is now ours.
The assertion is tightened the same way, from `export default function` to
`export default function (pi)`, so a default export with the wrong shape cannot
satisfy it.
Co-Authored-By: musichen <musichen@users.noreply.github.com>
Co-Authored-By: henri-edh <henri-edh@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Two regressions we shipped ourselves, both with the same shape: a change that
was correct in the abstract broke a concrete thing users had working.
**`update --ui` became a hard error (#1544).** v0.10.2 removed the ui/standard
chooser and deleted the flags that drove it, so a command in scripts, aliases
and muscle memory started failing with "unknown update option" where v0.10.1
had printed the install-script handoff. Retiring a CHOICE is fine; retiring the
WORDS is a break users get nothing for. Both flags are accepted again, do
nothing, and print one note explaining that every build now includes the UI.
A genuinely unknown flag is still rejected — the point is compatibility, not
silence. Reported by @ehendrix23.
**The generated Pi extension could not load (#1550).** Since #1392 the
extension has been generated from the tool registry, emitting
`export function register(pi)`. Pi loads an extension by calling its DEFAULT
export as a factory, so the file failed to load — and a Pi extension that fails
to load takes every `pi` command with it, `pi doctor` included, not just cbm's
tools. It now default-exports the factory. Reported by @musichen.
The existing generator test passed throughout: it asserted WHICH tools the file
lists, never that the file Pi loads is loadable. The new test pins the export
shape, which is the property that actually matters to the consumer.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The generated OpenCode adapter covered only the grep/glob graph lookup, so
OpenCode sessions missed the read-coverage and lifecycle context other
clients get through their hook configuration. Extend the plugin to the
surfaces OpenCode documents:
- unwrap hook-augment's additionalContext envelope so plain text, not raw
JSON, lands in the tool output
- post-read index-coverage notes via tool.execute.after, mapping OpenCode's
filePath argument to the file_path key the default dialect reads
- session-start tier routing injected once per sessionID on the first tool
result, since OpenCode documents no context-output lifecycle hook
- post-compaction reinjection through the documented experimental
session.compacting surface (output.context)
Every failure path resolves to the empty string: the adapter can degrade to
a no-op but never blocks a tool call. Pin the new surfaces in
test_agent_clients (31/31 pass; the #616 payload guard stays green) and
record the wider surface in the README client matrix.
Signed-off-by: jc <jcvi007@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wires the registry-driven generators into install, plan and uninstall.
pi gets ~/.pi/agent/extensions/cbmem.ts, its only route to the graph -- pi has
no MCP client. OpenCode gets ~/.config/opencode/plugins/cbm-augment.ts, which
adds no tools (it already reaches all of them over MCP) and supplies only the
automatic graph lookup before a grep/glob that other clients get from their own
hook configuration. OpenCode has none; its plugin system is the only extension
point, verified against their plugin documentation.
Both go in as a MARKED BLOCK rather than a whole-file write. Those directories
are auto-loaded, so a user may legitimately keep their own module there, and an
install routine that clobbers it is destroying user content. Uninstall removes
only our block and leaves the rest of the file, matching every other uninstall
path here.
A generator returning NULL is a hard error rather than a skip. A silently absent
extension is precisely the failure that left the earlier proposal a no-op for six
weeks, so it must surface as an install error instead of a quiet success.
Note for reviewers: the generated body deliberately carries NO ownership
markers. cbm_text_upsert_managed_block adds them itself and rejects content that
already contains them -- an existing guard test (test_cli.c:6018) caught that
integration mistake before CI did, which is the test doing exactly its job.
Reimplements the ideas from two community PRs, both closed as superseded by this
work, with credit to their authors:
- #534 (@Tensorboyalive) proposed the pi extension. It embedded 380 lines of
TypeScript as C string literals and registered 7 of the 15 registry tools;
this generates from the registry so the surface cannot drift, and ships no .ts
in the repository. Their marker-delimited editing, their uninstall test
asserting the markers are GONE, and their defensive stdout parsing (take the
last line that parses, not the first) are all carried forward here.
- #616 (@ZeR020) proposed the OpenCode plugin. Its payload omitted
hook_event_name, which hook-augment requires, so it produced zero bytes; and
its path template rejected the double quote but not the backslash, so a
Windows home like C:\Users\urs\bin produced an invalid unicode escape and the
auto-loaded plugin failed to parse. Both are closed by construction here and
pinned by tests. Their hook-point research is what made the OpenCode side
possible at all.
Tests: install writes both extensions and the dry run does not; uninstall
removes our block while preserving a user's own content in the same file.
Co-Authored-By: Tensorboyalive <Tensorboyalive@users.noreply.github.com>
Co-Authored-By: ZeR020 <ZeR020@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
pi has no MCP client, and OpenCode has no declarative hook configuration --
verified against OpenCode's own plugin documentation, which states hooks are
available only through JavaScript/TypeScript plugin modules. For those two
clients a module is the only extension point.
We do not want to ship such a module as a repository asset. #534 proposed one
embedding 380 lines of TypeScript as C string literals and registering 7 of the
15 registry tools; every tool added afterwards would have been silently missing
for that client. Hand-maintained copies of the tool list have already produced
defects here (#1361, and the smoke-invariants count).
So the module is GENERATED from the live registry instead. cbm_client_adapter_pi
walks cbm_mcp_tool_count()/cbm_mcp_tool_name() and registers every tool, which
makes drift structurally impossible rather than merely discouraged: adding a
tool to TOOLS[] adds it to every generated adapter with no second edit. Nothing
in the repository is a .ts file.
Both emitters wrap their output in ownership markers so a caller can rewrite its
own block and leave a user-authored file alone.
Two defects from the prior proposals are closed by construction:
- Path escaping. #616's template rejected the double quote but not the
backslash, so a Windows home like C:\Users\urs\bin produced an invalid
unicode escape and the whole auto-loaded plugin failed to parse, which is
worse than an absent plugin. cbm_client_adapter_escape_js escapes backslash,
quote, newline and CR, and fails closed rather than emitting a truncated
literal; generation aborts if the path cannot be escaped.
- The silent no-op. #616's payload omitted hook_event_name, which hook-augment
requires and without which it accepts nothing, so the plugin emitted zero
bytes for six weeks. The generated payload carries it and a test pins it.
Recorded honestly: the OpenCode emitter hooks tool.execute.after, whose ability
to modify a tool's output is NOT part of OpenCode's documented plugin contract
(only tool.execute.before's argument mutation is). If they change it the
augmentation stops with no error. That risk is accepted deliberately and the
emitter says so in a comment, so a future reader does not have to rediscover it.
This commit adds the generator and its tests only; wiring into the install and
uninstall routines follows separately so the two are reviewable apart.
Tests: every registry tool appears in the pi module (revert-checked by
simulating the 7-of-15 subset, which fails); Windows/quote/newline escaping and
its truncation boundary; the OpenCode payload carries hook_event_name and
registers no tools; NULL/empty binary paths generate nothing.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>