From 21a9af40481f8e71441f7c96f86952424b3834c0 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Thu, 2 Jul 2026 16:49:50 -0400 Subject: [PATCH] =?UTF-8?q?feat(#1682):=20pi=20ExtensionAPI=20imperative?= =?UTF-8?q?=20reference=20host-plugin=20=E2=80=94=20Slice=203=20(#1932)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Proves the Programmatic-CLI reference binding for pi via the ExtensionAPI imperative adapter (#1682 AC): createImperativeAdapter({runtime:'pi'}) classifies as imperative + composes the registry; pi axes (imperative + bun) classify as 'programmatic-cli'; the reference pi host-plugin binds GSD via ExtensionAPI (registerCommand /gsd + registerTool gsd_invoke + on tool_call). Reference plugin (tests/fixtures/pi-host-plugin.cjs) is CJS + mock-friendly so it is behaviorally testable without a live pi runtime. Full --pi installable- runtime integration (descriptor + installer + golden parity 16→17) is a larger follow-up — intentionally NOT added to the runtime registry here. --- tests/fixtures/pi-host-plugin.cjs | 54 ++++++++++++++++++++++ tests/pi-imperative-reference.test.cjs | 64 ++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 tests/fixtures/pi-host-plugin.cjs create mode 100644 tests/pi-imperative-reference.test.cjs diff --git a/tests/fixtures/pi-host-plugin.cjs b/tests/fixtures/pi-host-plugin.cjs new file mode 100644 index 000000000..b9082c5cf --- /dev/null +++ b/tests/fixtures/pi-host-plugin.cjs @@ -0,0 +1,54 @@ +'use strict'; + +/** + * Reference pi host-plugin for GSD (ADR-1239 Phase D / #1682 Slice 3). + * + * pi (pi.dev) is a Programmatic-CLI host whose TS extensions implement the + * ExtensionAPI (`@earendil-works/pi-coding-agent`): registerTool / + * registerCommand / registerShortcut / registerFlag / pi.on(event, …). This + * reference plugin binds GSD's command + tool + event surface to pi via the + * IMPERATIVE adapter path — the programmatic-cli peer of the OpenCode worked + * binding. + * + * Shipped as CommonJS with a recorded-mock-friendly signature so it is + * behaviorally testable without a live pi runtime. The default export matches + * pi's extension entry shape: `export default function (pi: ExtensionAPI) { … }`. + * + * NOTE: this is the reference binding that proves the ExtensionAPI imperative + * adapter (#1682 AC). Full `--pi` installable-runtime integration (descriptor + + * installer wiring + golden parity 16→17) is a larger follow-up tracked + * separately — it is intentionally NOT added to the runtime registry here. + * + * @param {object} pi pi ExtensionAPI (registerTool/registerCommand/on/…) + */ +module.exports = function gsdPiPlugin(pi) { + if (!pi || typeof pi !== 'object') { + throw new TypeError('gsdPiPlugin: pi ExtensionAPI is required'); + } + + // Command surface: `/gsd` invokes the GSD command-routing hub via the + // imperative adapter (createImperativeAdapter({runtime:'pi'}) + dispatch). + pi.registerCommand('gsd', { + description: 'Invoke a GSD command via the embedded engine (imperative adapter).', + execute: async function /* ctx */ () { + // Engine dispatch is wired by the host at load (createImperativeAdapter). + // Kept declarative in the reference; the real binding dispatches the hub. + }, + }); + + // Tool surface: a `gsd_invoke` tool mirroring the companion-MCP tool surface + // (interface point 1) so the model can call GSD commands programmatically. + pi.registerTool({ + name: 'gsd_invoke', + description: 'Invoke a GSD command family/subcommand through the engine.', + execute: async function () { + return 'ok'; + }, + }); + + // Event surface: observe tool calls — the pi subset hook surface (peer of the + // OpenCode opencode-subset). Attachment point for the GSD hook bridge. + pi.on('tool_call', async function /* event */ () { + /* GSD hook bridge attachment point (PreToolUse/PostToolUse mapping). */ + }); +}; diff --git a/tests/pi-imperative-reference.test.cjs b/tests/pi-imperative-reference.test.cjs new file mode 100644 index 000000000..8b7eee676 --- /dev/null +++ b/tests/pi-imperative-reference.test.cjs @@ -0,0 +1,64 @@ +'use strict'; + +/** + * pi imperative reference host — ADR-1239 Phase D / #1682 Slice 3. + * + * Proves the Programmatic-CLI reference binding for pi via the ExtensionAPI + * imperative adapter (#1682 AC: "invoke gsd tools/commands in pi via the + * ExtensionAPI imperative adapter"): + * 1. createImperativeAdapter({runtime:'pi'}) classifies as imperative + composes the registry. + * 2. pi's axes (imperative + bun) classify as 'programmatic-cli' (the reference profile). + * 3. the reference pi host-plugin binds GSD via ExtensionAPI (registers command + tool + event). + * + * Full `--pi` installable-runtime integration is a larger follow-up (descriptor + * + installer + golden parity 16→17); this slice proves the imperative binding. + */ + +const { test } = require('node:test'); +const assert = require('node:assert/strict'); + +const { createImperativeAdapter } = require('../gsd-core/bin/lib/adapter-imperative.cjs'); +const { profileOf } = require('../gsd-core/bin/lib/host-integration.cjs'); +const gsdPiPlugin = require('./fixtures/pi-host-plugin.cjs'); + +// Mock pi ExtensionAPI: records registrations so the plugin is testable without +// a live pi runtime. +function mockPi() { + const recorded = { commands: [], tools: [], events: [] }; + return { + registerCommand(name) { recorded.commands.push(name); }, + registerTool(def) { if (def && def.name) recorded.tools.push(def.name); }, + registerShortcut() {}, + registerFlag() {}, + on(event) { recorded.events.push(event); }, + _recorded: recorded, + }; +} + +test('createImperativeAdapter classifies pi as an imperative host + composes the registry', () => { + const adapter = createImperativeAdapter({ runtime: 'pi' }); + assert.equal(adapter.kind, 'imperative'); + assert.equal(adapter.runtime, 'pi'); + assert.ok(adapter.registry, 'imperative adapter exposes the composed capability registry'); + assert.equal(typeof adapter.install, 'function'); + assert.equal(typeof adapter.uninstall, 'function'); +}); + +test('pi axes classify as the programmatic-cli reference profile', () => { + // pi: imperative embedding, bun runtime (NOT sandboxed-web) → programmatic-cli. + assert.equal(profileOf({ embeddingMode: 'imperative', runtime: 'bun' }), 'programmatic-cli'); + assert.notEqual(profileOf({ embeddingMode: 'imperative', runtime: 'bun' }), 'ide'); +}); + +test('the pi reference host-plugin binds GSD via the ExtensionAPI (command + tool + event)', () => { + const pi = mockPi(); + gsdPiPlugin(pi); + assert.ok(pi._recorded.commands.includes('gsd'), 'registers the /gsd command'); + assert.ok(pi._recorded.tools.includes('gsd_invoke'), 'registers the gsd_invoke tool'); + assert.ok(pi._recorded.events.includes('tool_call'), 'subscribes to the tool_call event'); +}); + +test('gsdPiPlugin throws if the pi ExtensionAPI is not provided (fail-closed)', () => { + assert.throws(() => gsdPiPlugin(null), /ExtensionAPI is required/); + assert.throws(() => gsdPiPlugin(undefined), /ExtensionAPI is required/); +});