From a2b4fbd89feab128c598f1d5d93f5234fe5496b8 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Wed, 22 Apr 2026 15:36:00 -0400 Subject: [PATCH 1/5] chore: declare chrome devtools mcp in apm manifest --- AGENTS.md | 34 ---------------------------------- agents/ai.just | 13 ++++++------- apm.lock.yaml | 16 +++++++++++++++- apm.yml | 11 +++++++++++ opencode.json | 16 ++++++++++++++++ packages/AGENTS.md | 34 ++++++++++++++++++++++++++++++++++ 6 files changed, 82 insertions(+), 42 deletions(-) create mode 100644 opencode.json diff --git a/AGENTS.md b/AGENTS.md index 77cae799..beac2b6b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,40 +58,6 @@ When adding a new user-facing feature or shortcut, consider adding a tip so user - The Architecture section in `README.md` documents communication patterns, server internals, client state, and build pipeline. **Read it before declaring done** on any structural change, and update every part that no longer matches (table, diagram, prose, footnotes). -## Files matching `{packages/client/src,packages/server/src,packages/common/src}/**` - - -## oRPC Streaming Procedures - -Three invariants an agent editing any single file would otherwise miss. They are independent rules (different layers, different enforcement mechanisms) that share a trigger: **touching any streaming procedure**. - -### 1. Route client calls through the `stream` namespace - -Every async-iterator RPC the client consumes goes through `packages/client/src/rpc/rpc.ts`'s `stream` object, not `client.*` directly. The wrapper bakes in `STREAM_RETRY` context so `ClientRetryPlugin` can transparently re-subscribe on WebSocket reconnect. - -**When adding a new streaming procedure** (to `packages/common/src/contract.ts` + `packages/server/src/router.ts`), also add a corresponding entry to the `stream` object. Consumers MUST use `stream.xxx(...)` — calling `client.xxx(...)` directly silently loses reconnect handling. - -`stream.attach` takes an `onRetry` callback because imperative consumers (xterm.js `Terminal.tsx`, `TerminalPreview.tsx`) must clear their buffer before the retried iterator delivers its fresh snapshot — otherwise scrollback double-paints. - -### 2. Server handlers yield snapshot-then-deltas - -Every server-side streaming handler in `packages/server/src/router.ts` MUST yield a full state snapshot as its first item, then stream deltas. This is the invariant that makes `ClientRetryPlugin`'s transparent re-subscribe work: on reconnect, the plugin re-invokes the source, and the new iterator's first yield is a fresh snapshot that replaces stale client state. - -Two acceptable shapes: - -- **Implicit**: each yield is already a full replacement (e.g. `onMetadataChange` yields a current `TerminalMetadata`; `preferences.get` yields a current `Preferences`; `activity.get` yields a current `ActivityFeed`; `session.get` yields the current `SavedSession | null`; `terminal.list` yields a current `TerminalInfo[]`). Client reducers can just use the latest value. -- **Explicit discriminated union**: when clients accumulate deltas into a derived structure, yield `{ kind: "snapshot", ... } | { kind: "delta", ... }`. Client reducers replace on snapshot, append on delta. Without the discriminator, reconnect replays the history into an already-populated accumulator and duplicates state. - -If a new handler yields deltas only (no initial snapshot), reconnects will silently lose state with no error. - -### 3. Parameterize plugin contexts immediately - -When installing an oRPC client plugin that extends `ClientContext` (e.g. `ClientRetryPlugin`), parameterize both `RPCLink` AND `ContractRouterClient` at the same time. The current code uses `ClientRetryPluginContext`. - -Without this, per-call `{ context: ... }` options fall through to the default `Record` context type and TypeScript cannot catch typos — a misspelled field silently does nothing at runtime. This is a latent failure mode: tests still pass, the bug only surfaces when the context field you wanted to set is silently absent. - -The rule extends to future plugins: any plugin that exposes a context interface must be threaded through both type parameters the moment it's installed. - --- *This file was generated by APM CLI. Do not edit manually.* *To regenerate: `specify apm compile`* diff --git a/agents/ai.just b/agents/ai.just index 7cb3128d..ca406733 100644 --- a/agents/ai.just +++ b/agents/ai.just @@ -109,18 +109,17 @@ agent *args: apm just prepare {{ env('AI_AGENT', 'claude --dangerously-skip-permissions') }} {{ args }} -# Launch chrome-devtools-mcp server over stdio — wired up via the root -# `.mcp.json`, which shells out via `nix develop --command just -# ai::mcp-chrome-devtools` (Claude Code starts outside the nix devshell). +# Launch chrome-devtools-mcp server over stdio. Today Claude Code still +# needs the root `.mcp.json` entry; the top-level `apm.yml` mirrors the +# same server for runtimes APM can configure natively. +# Both paths shell out via `nix develop .#e2e --command just +# ai::mcp-chrome-devtools` so agent runtimes that start outside the nix +# devshell still get Playwright's Chrome-for-Testing. # Chrome binary resolved from Playwright's nix-provided Chrome-for-Testing # across platforms: `chrome-linux64/chrome` on Linux and # `chrome-mac-*/Google Chrome for Testing.app/.../Google Chrome for Testing` # on macOS. `find` picks either layout; a miss fails loud downstream # (empty --executable-path → MCP server errors out visibly). -# -# TODO: fold .mcp.json's entry into `dependencies.mcp` above once -# microsoft/apm#655 (Claude Code MCP adapter) merges and lands in juspay's -# fork — `.mcp.json` then becomes a generated artifact of `just ai::apm`. mcp-chrome-devtools: bash -c 'shopt -s nullglob; \ for c in "$PLAYWRIGHT_BROWSERS_PATH"/chromium-*/chrome-linux64/chrome \ diff --git a/apm.lock.yaml b/apm.lock.yaml index c4180d29..51d5dbf9 100644 --- a/apm.lock.yaml +++ b/apm.lock.yaml @@ -1,5 +1,5 @@ lockfile_version: '1' -generated_at: '2026-04-22T18:22:05.656399+00:00' +generated_at: '2026-04-22T19:34:51.132897+00:00' apm_version: 0.9.1 dependencies: - repo_url: _local/agents @@ -134,3 +134,17 @@ dependencies: .opencode/agents/hickey.md: sha256:edde9b0d3b27b947b7372f662b2e8652d136e0bb9af7924892b00450a1e586bc .opencode/agents/lowy.md: sha256:a1c8e5ba115439bcfa0d7e331f96e71b2d690ff2e5fb9b824bb5f588eb43ac75 content_hash: sha256:f309878ce79ee166620ef89812d093707f07b79cbebaa74725dc6f32f3cac8d3 +mcp_servers: +- chrome-devtools +mcp_configs: + chrome-devtools: + name: chrome-devtools + transport: stdio + args: + - develop + - .#e2e + - --command + - just + - ai::mcp-chrome-devtools + registry: false + command: nix diff --git a/apm.yml b/apm.yml index ac01bd5b..8d1c8e7d 100644 --- a/apm.yml +++ b/apm.yml @@ -13,3 +13,14 @@ dependencies: - juspay/skills/skills/nix-justfile - juspay/skills/skills/nix-typescript - anthropics/skills/skills/frontend-design + mcp: + - name: chrome-devtools + registry: false + transport: stdio + command: nix + args: + - develop + - .#e2e + - --command + - just + - ai::mcp-chrome-devtools diff --git a/opencode.json b/opencode.json new file mode 100644 index 00000000..a040b7f3 --- /dev/null +++ b/opencode.json @@ -0,0 +1,16 @@ +{ + "mcp": { + "chrome-devtools": { + "type": "local", + "enabled": true, + "command": [ + "nix", + "develop", + ".#e2e", + "--command", + "just", + "ai::mcp-chrome-devtools" + ] + } + } +} \ No newline at end of file diff --git a/packages/AGENTS.md b/packages/AGENTS.md index 48a28aa6..93498b86 100644 --- a/packages/AGENTS.md +++ b/packages/AGENTS.md @@ -90,6 +90,40 @@ Bad: `` inside a component Good: `export const FooIcon: Component<{ class?: string }> = ...` in Icons.tsx, then `` at the call site _Rationale_: Inline SVGs are invisible to search, duplicate across components, and bypass the existing icon registry convention. Centralizing icons in one file makes them discoverable, deduplicated, and consistent in sizing/color defaults. +## Files matching `{packages/client/src,packages/server/src,packages/common/src}/**` + + +## oRPC Streaming Procedures + +Three invariants an agent editing any single file would otherwise miss. They are independent rules (different layers, different enforcement mechanisms) that share a trigger: **touching any streaming procedure**. + +### 1. Route client calls through the `stream` namespace + +Every async-iterator RPC the client consumes goes through `packages/client/src/rpc/rpc.ts`'s `stream` object, not `client.*` directly. The wrapper bakes in `STREAM_RETRY` context so `ClientRetryPlugin` can transparently re-subscribe on WebSocket reconnect. + +**When adding a new streaming procedure** (to `packages/common/src/contract.ts` + `packages/server/src/router.ts`), also add a corresponding entry to the `stream` object. Consumers MUST use `stream.xxx(...)` — calling `client.xxx(...)` directly silently loses reconnect handling. + +`stream.attach` takes an `onRetry` callback because imperative consumers (xterm.js `Terminal.tsx`, `TerminalPreview.tsx`) must clear their buffer before the retried iterator delivers its fresh snapshot — otherwise scrollback double-paints. + +### 2. Server handlers yield snapshot-then-deltas + +Every server-side streaming handler in `packages/server/src/router.ts` MUST yield a full state snapshot as its first item, then stream deltas. This is the invariant that makes `ClientRetryPlugin`'s transparent re-subscribe work: on reconnect, the plugin re-invokes the source, and the new iterator's first yield is a fresh snapshot that replaces stale client state. + +Two acceptable shapes: + +- **Implicit**: each yield is already a full replacement (e.g. `onMetadataChange` yields a current `TerminalMetadata`; `preferences.get` yields a current `Preferences`; `activity.get` yields a current `ActivityFeed`; `session.get` yields the current `SavedSession | null`; `terminal.list` yields a current `TerminalInfo[]`). Client reducers can just use the latest value. +- **Explicit discriminated union**: when clients accumulate deltas into a derived structure, yield `{ kind: "snapshot", ... } | { kind: "delta", ... }`. Client reducers replace on snapshot, append on delta. Without the discriminator, reconnect replays the history into an already-populated accumulator and duplicates state. + +If a new handler yields deltas only (no initial snapshot), reconnects will silently lose state with no error. + +### 3. Parameterize plugin contexts immediately + +When installing an oRPC client plugin that extends `ClientContext` (e.g. `ClientRetryPlugin`), parameterize both `RPCLink` AND `ContractRouterClient` at the same time. The current code uses `ClientRetryPluginContext`. + +Without this, per-call `{ context: ... }` options fall through to the default `Record` context type and TypeScript cannot catch typos — a misspelled field silently does nothing at runtime. This is a latent failure mode: tests still pass, the bug only surfaces when the context field you wanted to set is silently absent. + +The rule extends to future plugins: any plugin that exposes a context interface must be threaded through both type parameters the moment it's installed. + --- *This file was generated by APM CLI. Do not edit manually.* *To regenerate: `specify apm compile`* From 62e99b5dfb4b0ab806762d6de9f99617a07d87b8 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Wed, 22 Apr 2026 15:39:17 -0400 Subject: [PATCH 2/5] fix: restore passing apm-sync outputs --- AGENTS.md | 34 ++++++++++++++++++++++++++++++++++ opencode.json | 16 ---------------- packages/AGENTS.md | 34 ---------------------------------- 3 files changed, 34 insertions(+), 50 deletions(-) delete mode 100644 opencode.json diff --git a/AGENTS.md b/AGENTS.md index beac2b6b..77cae799 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,6 +58,40 @@ When adding a new user-facing feature or shortcut, consider adding a tip so user - The Architecture section in `README.md` documents communication patterns, server internals, client state, and build pipeline. **Read it before declaring done** on any structural change, and update every part that no longer matches (table, diagram, prose, footnotes). +## Files matching `{packages/client/src,packages/server/src,packages/common/src}/**` + + +## oRPC Streaming Procedures + +Three invariants an agent editing any single file would otherwise miss. They are independent rules (different layers, different enforcement mechanisms) that share a trigger: **touching any streaming procedure**. + +### 1. Route client calls through the `stream` namespace + +Every async-iterator RPC the client consumes goes through `packages/client/src/rpc/rpc.ts`'s `stream` object, not `client.*` directly. The wrapper bakes in `STREAM_RETRY` context so `ClientRetryPlugin` can transparently re-subscribe on WebSocket reconnect. + +**When adding a new streaming procedure** (to `packages/common/src/contract.ts` + `packages/server/src/router.ts`), also add a corresponding entry to the `stream` object. Consumers MUST use `stream.xxx(...)` — calling `client.xxx(...)` directly silently loses reconnect handling. + +`stream.attach` takes an `onRetry` callback because imperative consumers (xterm.js `Terminal.tsx`, `TerminalPreview.tsx`) must clear their buffer before the retried iterator delivers its fresh snapshot — otherwise scrollback double-paints. + +### 2. Server handlers yield snapshot-then-deltas + +Every server-side streaming handler in `packages/server/src/router.ts` MUST yield a full state snapshot as its first item, then stream deltas. This is the invariant that makes `ClientRetryPlugin`'s transparent re-subscribe work: on reconnect, the plugin re-invokes the source, and the new iterator's first yield is a fresh snapshot that replaces stale client state. + +Two acceptable shapes: + +- **Implicit**: each yield is already a full replacement (e.g. `onMetadataChange` yields a current `TerminalMetadata`; `preferences.get` yields a current `Preferences`; `activity.get` yields a current `ActivityFeed`; `session.get` yields the current `SavedSession | null`; `terminal.list` yields a current `TerminalInfo[]`). Client reducers can just use the latest value. +- **Explicit discriminated union**: when clients accumulate deltas into a derived structure, yield `{ kind: "snapshot", ... } | { kind: "delta", ... }`. Client reducers replace on snapshot, append on delta. Without the discriminator, reconnect replays the history into an already-populated accumulator and duplicates state. + +If a new handler yields deltas only (no initial snapshot), reconnects will silently lose state with no error. + +### 3. Parameterize plugin contexts immediately + +When installing an oRPC client plugin that extends `ClientContext` (e.g. `ClientRetryPlugin`), parameterize both `RPCLink` AND `ContractRouterClient` at the same time. The current code uses `ClientRetryPluginContext`. + +Without this, per-call `{ context: ... }` options fall through to the default `Record` context type and TypeScript cannot catch typos — a misspelled field silently does nothing at runtime. This is a latent failure mode: tests still pass, the bug only surfaces when the context field you wanted to set is silently absent. + +The rule extends to future plugins: any plugin that exposes a context interface must be threaded through both type parameters the moment it's installed. + --- *This file was generated by APM CLI. Do not edit manually.* *To regenerate: `specify apm compile`* diff --git a/opencode.json b/opencode.json deleted file mode 100644 index a040b7f3..00000000 --- a/opencode.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "mcp": { - "chrome-devtools": { - "type": "local", - "enabled": true, - "command": [ - "nix", - "develop", - ".#e2e", - "--command", - "just", - "ai::mcp-chrome-devtools" - ] - } - } -} \ No newline at end of file diff --git a/packages/AGENTS.md b/packages/AGENTS.md index 93498b86..48a28aa6 100644 --- a/packages/AGENTS.md +++ b/packages/AGENTS.md @@ -90,40 +90,6 @@ Bad: `` inside a component Good: `export const FooIcon: Component<{ class?: string }> = ...` in Icons.tsx, then `` at the call site _Rationale_: Inline SVGs are invisible to search, duplicate across components, and bypass the existing icon registry convention. Centralizing icons in one file makes them discoverable, deduplicated, and consistent in sizing/color defaults. -## Files matching `{packages/client/src,packages/server/src,packages/common/src}/**` - - -## oRPC Streaming Procedures - -Three invariants an agent editing any single file would otherwise miss. They are independent rules (different layers, different enforcement mechanisms) that share a trigger: **touching any streaming procedure**. - -### 1. Route client calls through the `stream` namespace - -Every async-iterator RPC the client consumes goes through `packages/client/src/rpc/rpc.ts`'s `stream` object, not `client.*` directly. The wrapper bakes in `STREAM_RETRY` context so `ClientRetryPlugin` can transparently re-subscribe on WebSocket reconnect. - -**When adding a new streaming procedure** (to `packages/common/src/contract.ts` + `packages/server/src/router.ts`), also add a corresponding entry to the `stream` object. Consumers MUST use `stream.xxx(...)` — calling `client.xxx(...)` directly silently loses reconnect handling. - -`stream.attach` takes an `onRetry` callback because imperative consumers (xterm.js `Terminal.tsx`, `TerminalPreview.tsx`) must clear their buffer before the retried iterator delivers its fresh snapshot — otherwise scrollback double-paints. - -### 2. Server handlers yield snapshot-then-deltas - -Every server-side streaming handler in `packages/server/src/router.ts` MUST yield a full state snapshot as its first item, then stream deltas. This is the invariant that makes `ClientRetryPlugin`'s transparent re-subscribe work: on reconnect, the plugin re-invokes the source, and the new iterator's first yield is a fresh snapshot that replaces stale client state. - -Two acceptable shapes: - -- **Implicit**: each yield is already a full replacement (e.g. `onMetadataChange` yields a current `TerminalMetadata`; `preferences.get` yields a current `Preferences`; `activity.get` yields a current `ActivityFeed`; `session.get` yields the current `SavedSession | null`; `terminal.list` yields a current `TerminalInfo[]`). Client reducers can just use the latest value. -- **Explicit discriminated union**: when clients accumulate deltas into a derived structure, yield `{ kind: "snapshot", ... } | { kind: "delta", ... }`. Client reducers replace on snapshot, append on delta. Without the discriminator, reconnect replays the history into an already-populated accumulator and duplicates state. - -If a new handler yields deltas only (no initial snapshot), reconnects will silently lose state with no error. - -### 3. Parameterize plugin contexts immediately - -When installing an oRPC client plugin that extends `ClientContext` (e.g. `ClientRetryPlugin`), parameterize both `RPCLink` AND `ContractRouterClient` at the same time. The current code uses `ClientRetryPluginContext`. - -Without this, per-call `{ context: ... }` options fall through to the default `Record` context type and TypeScript cannot catch typos — a misspelled field silently does nothing at runtime. This is a latent failure mode: tests still pass, the bug only surfaces when the context field you wanted to set is silently absent. - -The rule extends to future plugins: any plugin that exposes a context interface must be threaded through both type parameters the moment it's installed. - --- *This file was generated by APM CLI. Do not edit manually.* *To regenerate: `specify apm compile`* From ee374f5abaff9b93931be0ee65e9f505dfc77feb Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Wed, 22 Apr 2026 15:46:49 -0400 Subject: [PATCH 3/5] fix: verify opencode mcp config in apm-sync --- agents/ai.just | 7 +++++++ opencode.json | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 opencode.json diff --git a/agents/ai.just b/agents/ai.just index ca406733..be32d9b3 100644 --- a/agents/ai.just +++ b/agents/ai.just @@ -98,6 +98,13 @@ apm-sync: apm-audit echo "ERROR: .opencode/ out of sync with sources — run: just ai::apm" >&2 exit 1 fi + if [[ -e opencode.json || -e "$scratch/opencode.json" ]]; then + if [[ ! -e opencode.json || ! -e "$scratch/opencode.json" ]] || \ + ! diff <(jq -S . opencode.json) <(jq -S . "$scratch/opencode.json"); then + echo "ERROR: opencode.json out of sync with sources — run: just ai::apm" >&2 + exit 1 + fi + fi if ! diff AGENTS.md "$scratch/AGENTS.md"; then echo "ERROR: AGENTS.md out of sync with sources — run: just ai::apm" >&2 exit 1 diff --git a/opencode.json b/opencode.json new file mode 100644 index 00000000..0fdc0c36 --- /dev/null +++ b/opencode.json @@ -0,0 +1,16 @@ +{ + "mcp": { + "chrome-devtools": { + "type": "local", + "enabled": true, + "command": [ + "nix", + "develop", + ".#e2e", + "--command", + "just", + "ai::mcp-chrome-devtools" + ] + } + } +} From 5f95ced20f25034cd93c1712b919ceba06d8184e Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Wed, 22 Apr 2026 15:56:34 -0400 Subject: [PATCH 4/5] fix: add codex mcp fallback --- .codex/config.toml | 8 ++++++++ agents/ai.just | 30 ++++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 .codex/config.toml diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 00000000..e45e0889 --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,8 @@ +# Project-local Codex MCP config. Codex CLI loads this only when the +# repository is marked trusted in the user's global Codex config. +# +# TODO: replace this manual file with APM-managed `.codex/config.toml` +# once microsoft/apm#803 lands. +[mcp_servers.chrome-devtools] +command = "nix" +args = ["develop", ".#e2e", "--command", "just", "ai::mcp-chrome-devtools"] diff --git a/agents/ai.just b/agents/ai.just index be32d9b3..5bd40419 100644 --- a/agents/ai.just +++ b/agents/ai.just @@ -86,7 +86,12 @@ apm-sync: apm-audit echo "ERROR: .claude/ out of sync with sources — run: just ai::apm" >&2 exit 1 fi - if ! diff -r .codex "$scratch/.codex"; then + # `.codex/config.toml` is currently a checked-in manual fallback for + # project-local Codex MCP until microsoft/apm#803 lands; current APM + # does not emit it in the scratch tree yet. + if ! diff -r \ + -x config.toml \ + .codex "$scratch/.codex"; then echo "ERROR: .codex/ out of sync with sources — run: just ai::apm" >&2 exit 1 fi @@ -94,7 +99,12 @@ apm-sync: apm-audit echo "ERROR: .agents/ out of sync with sources — run: just ai::apm" >&2 exit 1 fi - if ! diff -r .opencode "$scratch/.opencode"; then + if ! diff -r \ + -x .gitignore \ + -x node_modules \ + -x package-lock.json \ + -x package.json \ + .opencode "$scratch/.opencode"; then echo "ERROR: .opencode/ out of sync with sources — run: just ai::apm" >&2 exit 1 fi @@ -116,10 +126,18 @@ agent *args: apm just prepare {{ env('AI_AGENT', 'claude --dangerously-skip-permissions') }} {{ args }} -# Launch chrome-devtools-mcp server over stdio. Today Claude Code still -# needs the root `.mcp.json` entry; the top-level `apm.yml` mirrors the -# same server for runtimes APM can configure natively. -# Both paths shell out via `nix develop .#e2e --command just +# Launch chrome-devtools-mcp server over stdio. +# +# Today the checked-in project-local MCP entrypoints are: +# - Claude Code: `.mcp.json` +# - Codex CLI: `.codex/config.toml` (only for trusted projects) +# +# The top-level `apm.yml` mirrors the same server for runtimes APM can +# configure natively today. Codex project-scoped MCP is tracked upstream +# in microsoft/apm#803; once that lands, `.codex/config.toml` should +# become APM-managed like the other generated runtime config. +# +# All paths shell out via `nix develop .#e2e --command just # ai::mcp-chrome-devtools` so agent runtimes that start outside the nix # devshell still get Playwright's Chrome-for-Testing. # Chrome binary resolved from Playwright's nix-provided Chrome-for-Testing From 79d31381a78c4206441825eb250090bfc18b79cd Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Wed, 22 Apr 2026 15:58:02 -0400 Subject: [PATCH 5/5] fix: mark codex config as manual --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 49945b26..948960db 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,4 +5,5 @@ CLAUDE.md linguist-generated .claude/launch.json -linguist-generated .agents/** linguist-generated .codex/** linguist-generated +.codex/config.toml -linguist-generated .opencode/** linguist-generated