Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions artifacts/issue-3803-ooo-bridge-test-report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
{
"schemaVersion": 1,
"kind": "api-package-test-report",
"issue": 3803,
"baseline": {
"gjcBase": "9477947f8b89b74bf3efcc4c1bf7c4591a0558ec",
"ouroborosRelease": "v0.50.7",
"ouroborosCommit": "cb658aa819bfabafecbbe91bc36327f10691171b",
"ouroborosWheelSha256": "df42f4ef10e032f2edc3249534bf91e8612dee789dfc3517895a9eb2df7f82c4",
"standaloneBridgeCommit": "4311fefd49e9c6781c4d1111b8dd3f758e7d8974",
"standaloneBridgeSha256": "2b0e1e25ac145331f112da629076875542db6f6e63c3c17adcd6770a4dcaf7bd"
},
"verification": [
{
"command": "bun test packages/coding-agent/test/ooo-bridge-extension-contract.test.ts packages/coding-agent/test/ooo-bridge-runner-redteam.test.ts packages/coding-agent/test/ooo-bridge-installed-flow.test.ts packages/coding-agent/test/extensions-discovery.test.ts packages/coding-agent/test/extensions-runner.test.ts",
"status": "passed",
"result": "100 pass, 0 fail, 295 expect() calls"
},
{
"command": "bun --cwd=packages/coding-agent run check",
"status": "passed",
"result": "Biome checked 2514 files; TypeScript noEmit completed successfully"
},
{
"command": "bun run check:public-sync",
"status": "passed",
"result": "Public docs/site/version surfaces are in sync"
},
{
"command": "bun run generate-docs-index",
"status": "passed",
"result": "Embedded docs index regenerated with 120 documents"
},
{
"command": "git diff --check",
"status": "passed",
"result": "No whitespace errors"
}
],
"adversarialCases": [
{
"id": "queued-explicit-generation-fence",
"status": "passed",
"evidence": "Deferred MCP operations hold queued explicit interview starts while actual AgentSession session_switch and InputController /clear resets advance the lifecycle generation; predecessor entries settle handled without issuing another MCP call."
},
{
"id": "session-switch-disposal",
"status": "passed",
"evidence": "The installed extension registers session_switch disposal. A real AgentSession new-session transition with the same ExtensionRunner clears the old Ouroboros session before InputController submits ordinary successor-session input."
},
{
"id": "clear-control-disposal",
"status": "passed",
"evidence": "InputController executes the real /clear path through AgentSession.clearContext after the bridge resets, and subsequent ordinary input is not sent to the prior Ouroboros session_id."
},
{
"id": "startup-overlap-serialization",
"status": "passed",
"evidence": "Two non-awaited InputController submissions during interview startup remain claimed; the second waits for the first session_id and cannot fall through to the model."
},
{
"id": "continuation-overlap-serialization",
"status": "passed",
"evidence": "Two concurrent continuation answers issue one MCP call at a time. The second starts only after the first settles and uses the latest correlated session state."
},
{
"id": "late-mcp-settlement-fence",
"status": "passed",
"evidence": "Runner timeout aborts the handler signal, disconnects the MCP transport, and a late question cannot recreate interview state or capture the next ordinary prompt."
},
{
"id": "dead-transport-release",
"status": "passed",
"evidence": "An MCP tool failure clears the interview session and cached connection; ordinary input passes through and a later explicit interview opens a fresh connection."
},
{
"id": "built-in-control-bypass",
"status": "passed",
"evidence": "Non-session slash controls plus bare dot and c bypass active capture; session-changing controls reset state before their built-in action."
},
{
"id": "compiled-one-file-install",
"status": "passed",
"evidence": "A real Bun compiled loader loads the isolated standalone extension without peer node_modules and registers both input and session_switch handlers."
},
{
"id": "visible-correlated-flow",
"status": "passed",
"evidence": "The installed example renders the first question, sends the next answer with the same session_id, renders completion, disconnects, and returns ordinary prompts to GJC."
},
{
"id": "compatible-cli-override",
"status": "passed",
"evidence": "OUROBOROS_CLI selects the executable for both MCP serving and non-interview dispatch."
},
{
"id": "unsupported-dispatch",
"status": "passed",
"evidence": "Exit code 78 remains pass-through for non-interview exact-prefix dispatch."
}
]
}
61 changes: 51 additions & 10 deletions docs/ooo-bridge-extension-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,71 @@ The extension runner already treats `InputEventResult.handled === true` as termi

## Dispatch and result semantics

`createOuroborosOooBridge()` is a small specialization of `createExactPrefixCommandBridge()`:
`createOuroborosOooBridge()` has two bounded paths:

- command: `ouroboros`
- arguments: `dispatch`, then the full submitted input text
- recursion guard variable: the Ouroboros bridge recursion-depth environment variable
- `ooo interview [topic]` starts `ouroboros_interview` through a lazily connected `ouroboros mcp serve --runtime gjc` stdio server.
- While that interview is active, subsequent ordinary interactive input is claimed as an answer with the same `session_id`. A completed result clears the correlation and closes the MCP connection.
- Other exact-prefix `ooo ...` commands run `ouroboros dispatch --runtime gjc <full-input>` through `createExactPrefixCommandBridge()`.
- `OUROBOROS_CLI` overrides the executable for both paths; otherwise the command is `ouroboros`.

- continue/pass-through exit code: `78`
Successful handled text is returned as `{ handled: true, text }`. The interactive input controller renders that text as a visible custom message before clearing the composer, so the first interview question, continuation questions, completion result, and successful non-interview command output reach the user.

Exit-code mapping:
Command-dispatch exit mapping remains:

| Dispatch result | GJC input result |
| --- | --- |
| `0` | `{ handled: true }`; do not send input to the model. |
| `0` | `{ handled: true, text? }`; render non-empty stdout (or stderr when stdout is empty) and do not send the input to the model. |
| `78` | `{}`; continue/pass-through so GJC processes the input normally. |
| any other non-zero | Surface an extension error notification using stderr, then stdout, then a generic exit-code message, and return `{ handled: true }`; the failed `ooo` command is terminal and is not sent to the model. |

## Recursion guard
MCP interview errors are notified and handled. A non-terminal response must contain a valid `interview_*` session ID in MCP `_meta` (with the visible `Session ...` text accepted as a compatibility fallback); otherwise the bridge fails closed instead of accepting an uncorrelated answer.

Runner timeout aborts the handler context signal. The bridge passes that signal to MCP connection/tool calls and generation-fences every post-await state mutation, so a late settlement cannot recreate correlation after the runner has fallen through. Any MCP connection or tool failure clears the interview session and cached transport before notifying; a later ordinary prompt therefore passes through, while a new explicit `ooo interview` reconnects cleanly.

Slash-prefixed UI commands bypass interview capture. The bare continue controls `.` and `c` also remain GJC controls; other ordinary text remains a valid interview answer.

Before dispatch, the helper increments the Ouroboros bridge recursion-depth environment variable and restores its previous value after dispatch finishes. A current numeric depth of `0` or `1` is dispatchable, which preserves concurrent independent interactive inputs while marking child dispatcher processes with depth `1`. A current numeric depth greater than `1`, or any non-empty non-numeric value, returns `{}` without dispatching.
The installed example also registers `session_switch` disposal because GJC reuses one `ExtensionRunner` across `/new`, `/drop`, resume, and fork transitions. Session-changing input controls reset immediately, including `/clear`, and the lifecycle hook covers identity changes initiated outside the input path. Interview startup and continuation calls share one FIFO operation chain: a second submission during startup is claimed and waits for the session ID, while overlapping answers issue one MCP call at a time against the latest settled state. Every queue entry is bound to the lifecycle generation at submission, so resets consume predecessor-generation entries—including explicit `ooo interview` starts—without calling MCP in the successor session.

## Recursion guard

This means the bridge allows exactly one inherited bridge-marked dispatcher level and blocks recursive re-entry from deeper bridge-marked children. The guard also passes through `event.source === "extension"` to avoid extension-originated messages re-entering the bridge.
Before command dispatch, the exact-prefix helper increments the Ouroboros bridge recursion-depth environment variable and restores its previous value after dispatch finishes. A current numeric depth of `0` or `1` is dispatchable. A current numeric depth greater than `1`, or any non-empty non-numeric value, returns `{}` without dispatching. The guard also passes through `event.source === "extension"` to avoid extension-originated messages re-entering the bridge.

## Installation and discovery

### Pinned Ouroboros baseline

This path is verified against [Q00/ouroboros `v0.50.7`](https://github.com/Q00/ouroboros/releases/tag/v0.50.7). Install its MCP profile at the exact version, then configure GJC:

```bash
uv tool install 'ouroboros-ai[mcp]==0.50.7'
ouroboros setup --runtime gjc
```

`pipx install 'ouroboros-ai[mcp]==0.50.7'` is equivalent. Do not pipe a mutable branch installer into a shell. Pin source audits to commit `cb658aa819bfabafecbbe91bc36327f10691171b`. The release asset `ouroboros_ai-0.50.7-py3-none-any.whl` has SHA-256 `df42f4ef10e032f2edc3249534bf91e8612dee789dfc3517895a9eb2df7f82c4`; compare a downloaded asset with that digest before installation.

### Verified GJC bridge installation

Ouroboros setup installs its own managed GJC bridge. Replace it with the standalone GJC bridge from immutable commit `4311fefd49e9c6781c4d1111b8dd3f758e7d8974`, whose example file has SHA-256 `2b0e1e25ac145331f112da629076875542db6f6e63c3c17adcd6770a4dcaf7bd`:

```bash
curl -fL https://raw.githubusercontent.com/Yeachan-Heo/gajae-code/4311fefd49e9c6781c4d1111b8dd3f758e7d8974/packages/coding-agent/examples/extensions/ooo-bridge.ts -o /tmp/gjc-ooo-bridge.ts
shasum -a 256 /tmp/gjc-ooo-bridge.ts
mkdir -p "${HOME}/${GJC_CONFIG_DIR:-.gjc}/agent/extensions/ouroboros-ooo-bridge" && cp /tmp/gjc-ooo-bridge.ts "${HOME}/${GJC_CONFIG_DIR:-.gjc}/agent/extensions/ouroboros-ooo-bridge/index.ts"
```

The `shasum` output must match the published example digest before the copy. The example has no runtime imports: it obtains the bundled bridge helper from the injected extension API, so the copied file works in compiled GJC binaries without extension-local `node_modules`. For project-only installation, copy the same verified file to `.gjc/extensions/ouroboros-ooo-bridge/index.ts`. Start a new GJC session after installation, then run:

```text
ooo interview "I want to build a task management CLI"
```

Set `OUROBOROS_CLI=/absolute/path/to/ouroboros` when the executable is outside `PATH`.

### Native interview versus external Ouroboros interview

- `/skill:deep-interview` is GJC's bundled native interview workflow. It includes Ouroboros-inspired behavior but does not invoke the external CLI.
- `ooo interview` is the external integration. It calls Ouroboros's MCP interview tool, renders each question in GJC, correlates ordinary answers by Ouroboros session ID, and stops claiming input when the interview completes.

The canonical install location is the agent extensions directory discovered by the native GJC provider:

- user-level: `$HOME/${GJC_CONFIG_DIR:-.gjc}/agent/extensions`
Expand Down
4 changes: 4 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- Interactive turns now announce their state as an OSC 777 sequence (`notify;Terax;gjc;working|attention|finished`), so a hosting terminal can follow the agent without polling. Terminals that do not parse it discard it like any unknown OSC, and print/RPC mode stdout is untouched.

### Added

- Added a verified, copy-installable `ooo` bridge example: `ooo interview` renders Ouroboros MCP questions in GJC, serializes startup and follow-up answers by session ID, cancellation-fences late settlement, disposes state on GJC session changes and `/clear`, drops queued predecessor-generation starts, releases dead transports and controls, honors `OUROBOROS_CLI`, and loads dependency-free in compiled binaries (#3803).

### Fixed

- Telegram daemon restart now revokes every persisted callback alias before polling. Reconnecting sessions must replay a pending ask to receive fresh, owner-bound aliases; old controls remain stale, and their keyboards are best-effort terminalized when the original Telegram message id is available. Shutdown now fences new session messages and drains every admitted handler before final callback persistence and ownership release, preventing a successful send racing shutdown from publishing alias state after a successor takes ownership (#3727).
Expand Down
42 changes: 38 additions & 4 deletions packages/coding-agent/examples/extensions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,39 @@ mkdir -p .gjc/extensions
cp packages/coding-agent/examples/extensions/pirate.ts .gjc/extensions/
```

### Enable the Ouroboros `ooo` bridge

Install the version-pinned Ouroboros `v0.50.7` MCP profile, then configure its GJC runtime:

```bash
uv tool install 'ouroboros-ai[mcp]==0.50.7'
ouroboros setup --runtime gjc
```

`pipx install 'ouroboros-ai[mcp]==0.50.7'` is the equivalent pipx installation. Do not pipe a mutable branch installer into a shell. Pin source audits to commit `cb658aa819bfabafecbbe91bc36327f10691171b`. The [v0.50.7 release](https://github.com/Q00/ouroboros/releases/tag/v0.50.7) publishes `ouroboros_ai-0.50.7-py3-none-any.whl` with SHA-256 `df42f4ef10e032f2edc3249534bf91e8612dee789dfc3517895a9eb2df7f82c4`; verify downloaded release assets before installing them.

Ouroboros setup installs its own managed bridge. Replace that file with this standalone GJC bridge, which preserves the interview session across serialized follow-up answers, disposes it on GJC session switches, and drops queued predecessor-session starts. Download the example from immutable GJC commit `4311fefd49e9c6781c4d1111b8dd3f758e7d8974` and verify it before installation:

```bash
curl -fL https://raw.githubusercontent.com/Yeachan-Heo/gajae-code/4311fefd49e9c6781c4d1111b8dd3f758e7d8974/packages/coding-agent/examples/extensions/ooo-bridge.ts -o /tmp/gjc-ooo-bridge.ts
shasum -a 256 /tmp/gjc-ooo-bridge.ts
mkdir -p "${HOME}/${GJC_CONFIG_DIR:-.gjc}/agent/extensions/ouroboros-ooo-bridge" && cp /tmp/gjc-ooo-bridge.ts "${HOME}/${GJC_CONFIG_DIR:-.gjc}/agent/extensions/ouroboros-ooo-bridge/index.ts"
```

The `shasum` output must match `2b0e1e25ac145331f112da629076875542db6f6e63c3c17adcd6770a4dcaf7bd` before the copy. The file has no runtime package imports and uses the host API injected by GJC, so compiled binaries do not require a peer `node_modules` directory beside the installation.

For a project-only installation, copy the same verified file to `.gjc/extensions/ouroboros-ooo-bridge/index.ts`. Start a new GJC session after installation, then enter:

```text
ooo interview "I want to build a task management CLI"
```

The first question is rendered in GJC. While that interview remains active, ordinary interactive input is sent as the answer with the same Ouroboros session ID; completion clears the correlation and returns subsequent ordinary prompts to GJC. Other `ooo ...` commands continue through `ouroboros dispatch --runtime gjc`, including exit-code `78` pass-through.

Set `OUROBOROS_CLI=/absolute/path/to/ouroboros` when the executable is outside `PATH`. Missing executable, MCP startup, and dispatch failures produce an error notification for the claimed input without preventing GJC startup or ordinary prompts.

This external path is separate from GJC's native `/skill:deep-interview`: the native skill runs GJC's bundled interview workflow, while `ooo interview` delegates to the installed Ouroboros MCP interview tool.

## Examples

### Custom Tools & API
Expand All @@ -39,10 +72,11 @@ cp packages/coding-agent/examples/extensions/pirate.ts .gjc/extensions/

### External Dependencies

| Extension | Description |
| ----------------- | ------------------------------------------------------------------------- |
| `chalk-logger.ts` | Uses chalk from parent node_modules (demonstrates jiti module resolution) |
| `with-deps/` | Extension with its own package.json and dependencies |
| Extension | Description |
| ----------------- | ---------------------------------------------------------------------------- |
| `chalk-logger.ts` | Uses chalk from parent node_modules (demonstrates jiti module resolution) |
| `ooo-bridge.ts` | Opt-in `ooo ...` input bridge to the installed Ouroboros CLI and MCP runtime |
| `with-deps/` | Extension with its own package.json and dependencies |

## Writing Extensions

Expand Down
15 changes: 15 additions & 0 deletions packages/coding-agent/examples/extensions/ooo-bridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
interface OooBridgeExtensionAPI {
pi: unknown;
on(event: "input" | "session_switch", handler: (event: unknown, context: unknown) => unknown): void;
}

interface OooBridgeHost {
createOuroborosOooBridge(): ((event: unknown, context: unknown) => unknown) & { reset(): Promise<void> };
}

export default function (pi: OooBridgeExtensionAPI) {
const host = pi.pi as OooBridgeHost;
const bridge = host.createOuroborosOooBridge();
pi.on("input", bridge);
pi.on("session_switch", () => bridge.reset());
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export {
loadExtensionFromFactory,
loadExtensions,
} from "./loader";
export * from "./ouroboros-ooo-bridge";
export * from "./prefix-command-bridge";
export * from "./runner";
// Type guards
Expand Down
Loading
Loading