-
Notifications
You must be signed in to change notification settings - Fork 4
feat(plugins): self-healing lifecycle for built-in plugins #86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
b5f006a
feat(plugins): self-healing lifecycle for built-in plugins
claudiusthebot 7ab0b42
fix(ci+playwright): Windows npx + narrow .skip CI grep
claudiusthebot 396d19c
fix(windows): shell:true for .cmd/.bat spawns
claudiusthebot 1dffe06
fix(plugins): address Copilot review comments on PR #86
claudiusthebot f2b1fd1
fix(plugins): address second round of Copilot review
claudiusthebot 9d0b86a
fix(plugins): Copilot review round 3 — classification + encapsulation
claudiusthebot f258d56
fix(playwright): Copilot review round 4
claudiusthebot 291c342
fix(playwright): read pinned @playwright/mcp version from package.json
claudiusthebot 55ddb7a
fix(plugins/mempalace): bump MEMPALACE_TARGET and MEMPALACE_FLOOR 3.3…
claudiusthebot dc4294c
fix(docker): copy scripts/ before npm ci to satisfy postinstall hook
claudiusthebot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| name: Plugin Smoke | ||
|
|
||
| # Functional smoke tests for the internal self-healing plugins. | ||
| # Each plugin gets an isolated job so a Docker/Playwright failure doesn't | ||
| # hide a mempalace regression (and vice versa). Triggers: PRs touching | ||
| # plugin code/config/workflow/scripts, pushes to main with the same paths, | ||
| # nightly cron to catch upstream breakage, and manual dispatch. | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - "src/plugins/**" | ||
| - "src/util/config.ts" | ||
| - ".github/workflows/plugin-smoke.yml" | ||
| - "scripts/smoke-*.mjs" | ||
| - "scripts/lib/**" | ||
| - "package.json" | ||
| - "package-lock.json" | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - "src/plugins/**" | ||
| - "src/util/config.ts" | ||
| - ".github/workflows/plugin-smoke.yml" | ||
| - "scripts/smoke-*.mjs" | ||
| - "scripts/lib/**" | ||
| - "package.json" | ||
| - "package-lock.json" | ||
| schedule: | ||
| # 05:30 UTC — catches upstream PyPI / ghcr / playwright CDN breakage | ||
| # before the first people of the day (Paweł, Dylan) see it in Talon. | ||
| - cron: "30 5 * * *" | ||
| workflow_dispatch: | ||
|
|
||
| concurrency: | ||
| group: plugin-smoke-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| # ── MemPalace: Linux/macOS/Windows × Python 3.11/3.12 ────────────── | ||
| mempalace: | ||
| name: mempalace · ${{ matrix.os }} · py${{ matrix.python }} | ||
| runs-on: ${{ matrix.os }} | ||
| timeout-minutes: 20 | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| os: [ubuntu-latest, macos-latest, windows-latest] | ||
| python: ["3.11", "3.12"] | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: 22 | ||
| - uses: actions/setup-python@v6 | ||
| with: | ||
| python-version: ${{ matrix.python }} | ||
| - name: Pinned target | ||
| shell: bash | ||
| run: grep MEMPALACE_TARGET src/plugins/mempalace/heal.ts | ||
| - name: npm ci | ||
| run: npm ci | ||
| - name: Create venv + install pinned mempalace | ||
| shell: bash | ||
| run: node scripts/smoke-mempalace.mjs install | ||
| - name: MCP responsiveness | ||
| shell: bash | ||
| run: node scripts/smoke-mempalace.mjs smoke | ||
|
|
||
| # ── GitHub MCP: Ubuntu only (Docker) ─────────────────────────────── | ||
| github: | ||
| name: github · ubuntu-latest | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: 22 | ||
| - name: Pinned image | ||
| shell: bash | ||
| run: grep GITHUB_MCP_IMAGE src/plugins/github/heal.ts | ||
| - name: npm ci | ||
| run: npm ci | ||
| - name: Pull pinned image | ||
| run: node scripts/smoke-github.mjs pull | ||
| - name: MCP responsiveness | ||
| run: node scripts/smoke-github.mjs smoke | ||
|
|
||
| # ── Playwright MCP: Linux/macOS/Windows × chromium ────────────────── | ||
| playwright: | ||
| name: playwright · ${{ matrix.os }} · ${{ matrix.browser }} | ||
| runs-on: ${{ matrix.os }} | ||
| timeout-minutes: 25 | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| os: [ubuntu-latest, macos-latest, windows-latest] | ||
| browser: [chromium] | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: 22 | ||
| - name: Pinned @playwright/mcp | ||
| shell: bash | ||
| run: grep PLAYWRIGHT_MCP_VERSION src/plugins/playwright/heal.ts | ||
| - name: npm ci | ||
| run: npm ci | ||
| - name: Verify version + install browser | ||
| shell: bash | ||
| env: | ||
| PW_SMOKE_BROWSER: ${{ matrix.browser }} | ||
| run: node scripts/smoke-playwright.mjs install | ||
| - name: MCP responsiveness | ||
| shell: bash | ||
| env: | ||
| PW_SMOKE_BROWSER: ${{ matrix.browser }} | ||
| run: node scripts/smoke-playwright.mjs smoke | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| /** | ||
| * Minimal MCP stdio client shared by the plugin smoke scripts. | ||
| * | ||
| * Implements just enough JSON-RPC to exercise the three things we need for | ||
| * smoke testing any MCP server: | ||
| * - initialize + notifications/initialized handshake | ||
| * - tools/list to assert the expected toolset is present | ||
| * - tools/call for a representative side-effect-free invocation | ||
| * | ||
| * Not for production use. Real Talon talks to MCP via the Claude Agent SDK. | ||
| */ | ||
|
|
||
| export class StdioMcpClient { | ||
| constructor(child, { name = "mcp", onStderrLine } = {}) { | ||
| this.name = name; | ||
| this.child = child; | ||
| this.buffer = ""; | ||
| this.pending = new Map(); | ||
| this.nextId = 1; | ||
| child.stdout.setEncoding("utf-8"); | ||
| child.stdout.on("data", (chunk) => this.#onData(chunk)); | ||
| child.stderr.setEncoding("utf-8"); | ||
| child.stderr.on("data", (chunk) => { | ||
| if (onStderrLine) { | ||
| for (const line of String(chunk).split(/\r?\n/)) { | ||
| if (line.trim()) onStderrLine(line); | ||
| } | ||
| } else { | ||
| process.stderr.write(`[${name}-stderr] ${chunk}`); | ||
| } | ||
| }); | ||
| child.on("exit", (code, signal) => { | ||
| const err = new Error(`${name} exited code=${code} signal=${signal}`); | ||
| for (const { reject } of this.pending.values()) reject(err); | ||
| this.pending.clear(); | ||
| }); | ||
| } | ||
|
|
||
| #onData(chunk) { | ||
| this.buffer += chunk; | ||
| let nl; | ||
| while ((nl = this.buffer.indexOf("\n")) !== -1) { | ||
| const line = this.buffer.slice(0, nl).trim(); | ||
| this.buffer = this.buffer.slice(nl + 1); | ||
| if (!line) continue; | ||
| let msg; | ||
| try { | ||
| msg = JSON.parse(line); | ||
| } catch { | ||
| // Some servers (github-mcp) print non-JSON banner lines — ignore. | ||
| continue; | ||
| } | ||
| if (msg.id !== undefined && this.pending.has(msg.id)) { | ||
| const { resolve, reject } = this.pending.get(msg.id); | ||
| this.pending.delete(msg.id); | ||
| if (msg.error) reject(new Error(JSON.stringify(msg.error))); | ||
| else resolve(msg.result); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| request(method, params, timeoutMs = 30_000) { | ||
| const id = this.nextId++; | ||
| const payload = { jsonrpc: "2.0", id, method, params }; | ||
| return new Promise((resolve, reject) => { | ||
| const timer = setTimeout(() => { | ||
| this.pending.delete(id); | ||
| reject(new Error(`${method} timed out after ${timeoutMs}ms`)); | ||
| }, timeoutMs); | ||
| this.pending.set(id, { | ||
| resolve: (value) => { | ||
| clearTimeout(timer); | ||
| resolve(value); | ||
| }, | ||
| reject: (err) => { | ||
| clearTimeout(timer); | ||
| reject(err); | ||
| }, | ||
| }); | ||
| this.child.stdin.write(`${JSON.stringify(payload)}\n`); | ||
| }); | ||
| } | ||
|
|
||
| notify(method, params) { | ||
| this.child.stdin.write( | ||
| `${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`, | ||
| ); | ||
| } | ||
|
|
||
| async handshake(timeoutMs = 30_000) { | ||
| const init = await this.request( | ||
| "initialize", | ||
| { | ||
| protocolVersion: "2024-11-05", | ||
| capabilities: {}, | ||
| clientInfo: { name: "talon-smoke", version: "0.0.0" }, | ||
| }, | ||
| timeoutMs, | ||
| ); | ||
| if (!init?.protocolVersion) { | ||
| throw new Error(`bad initialize response: ${JSON.stringify(init)}`); | ||
| } | ||
| this.notify("notifications/initialized", {}); | ||
| return init.protocolVersion; | ||
| } | ||
|
|
||
| async listTools(timeoutMs = 15_000) { | ||
| const result = await this.request("tools/list", {}, timeoutMs); | ||
| if (!Array.isArray(result?.tools)) { | ||
| throw new Error(`bad tools/list response: ${JSON.stringify(result)}`); | ||
| } | ||
| return result.tools.map((t) => t.name); | ||
| } | ||
|
|
||
| async callTool(name, args = {}, timeoutMs = 60_000) { | ||
| const result = await this.request( | ||
| "tools/call", | ||
| { name, arguments: args }, | ||
| timeoutMs, | ||
| ); | ||
| return result; | ||
| } | ||
|
|
||
| async close() { | ||
| try { | ||
| this.child.stdin.end(); | ||
| } catch { | ||
| /* already closed */ | ||
| } | ||
| await new Promise((resolve) => { | ||
| if (this.child.exitCode !== null) return resolve(); | ||
| this.child.on("exit", () => resolve()); | ||
| }); | ||
| } | ||
| } | ||
|
|
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.