From abd15ad53cc7519447fbc5542b34f5510b5aec7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elberte=20Pl=C3=ADnio?= Date: Wed, 22 Jul 2026 18:26:47 -0300 Subject: [PATCH 1/2] refactor: extract pi-kit to standalone repository Refs #54 --- AGENTS.md | 10 - bun.lock | 600 +------------- .../2026-07-21-claude-lanes-deployment.md | 2 + ...1-cross-harness-multimodel-lanes-design.md | 2 + .../2026-07-21-pi-kit-multimodel-lanes-mcp.md | 2 + packages/pi-kit/README.md | 105 --- packages/pi-kit/extensions/btw.ts | 167 ---- packages/pi-kit/extensions/compact-tools.ts | 200 ----- packages/pi-kit/extensions/delegation.ts | 71 -- packages/pi-kit/extensions/ember-footer.ts | 305 ------- packages/pi-kit/extensions/gates.ts | 169 ---- packages/pi-kit/extensions/grill-review.ts | 196 ----- packages/pi-kit/extensions/journal.ts | 121 --- packages/pi-kit/extensions/lanes.ts | 529 ------------ packages/pi-kit/extensions/table.ts | 43 - packages/pi-kit/extensions/vault.ts | 51 -- packages/pi-kit/mcp/create-server.ts | 207 ----- packages/pi-kit/mcp/server.ts | 63 -- packages/pi-kit/package.json | 43 - packages/pi-kit/src/adapters/claude-code.ts | 545 ------------- packages/pi-kit/src/adapters/pi.ts | 166 ---- packages/pi-kit/src/child-env.ts | 45 -- packages/pi-kit/src/delegation-core.ts | 33 - packages/pi-kit/src/execution-adapter.ts | 30 - packages/pi-kit/src/journal-core.ts | 154 ---- packages/pi-kit/src/lane-coordinator.ts | 297 ------- packages/pi-kit/src/lanes-tui.ts | 378 --------- packages/pi-kit/src/runner.ts | 752 ------------------ packages/pi-kit/src/schema.ts | 85 -- packages/pi-kit/src/table.ts | 117 --- packages/pi-kit/src/transcript.ts | 168 ---- packages/pi-kit/test/btw-extension.test.ts | 142 ---- packages/pi-kit/test/child-env.test.ts | 58 -- .../pi-kit/test/claude-code-adapter.test.ts | 318 -------- .../pi-kit/test/delegation-extension.test.ts | 107 --- packages/pi-kit/test/delegation.test.ts | 44 - packages/pi-kit/test/ember-footer.test.ts | 209 ----- .../pi-kit/test/execution-adapters.test.ts | 138 ---- .../test/fixtures/claude-lane-stream.jsonl | 17 - .../pi-kit/test/fixtures/lane-stream.jsonl | 8 - .../pi-kit/test/fixtures/mcp-lane-process.cjs | 12 - .../fixtures/normalized-lane-stream.jsonl | 8 - packages/pi-kit/test/gates.test.ts | 122 --- packages/pi-kit/test/grill-review.test.ts | 222 ------ packages/pi-kit/test/journal.test.ts | 146 ---- packages/pi-kit/test/lane-coordinator.test.ts | 480 ----------- packages/pi-kit/test/lanes-extension.test.ts | 445 ----------- packages/pi-kit/test/lanes-tui.test.ts | 70 -- packages/pi-kit/test/mcp-server.test.ts | 452 ----------- packages/pi-kit/test/runner.test.ts | 751 ----------------- packages/pi-kit/test/table.test.ts | 123 --- packages/pi-kit/test/transcript.test.ts | 75 -- packages/pi-kit/test/vault.test.ts | 63 -- packages/pi-kit/themes/pickforge-ember.json | 71 -- 54 files changed, 15 insertions(+), 9722 deletions(-) rename docs/superpowers/{plans => archive}/2026-07-21-claude-lanes-deployment.md (99%) rename docs/superpowers/{specs => archive}/2026-07-21-cross-harness-multimodel-lanes-design.md (99%) rename docs/superpowers/{plans => archive}/2026-07-21-pi-kit-multimodel-lanes-mcp.md (99%) delete mode 100644 packages/pi-kit/README.md delete mode 100644 packages/pi-kit/extensions/btw.ts delete mode 100644 packages/pi-kit/extensions/compact-tools.ts delete mode 100644 packages/pi-kit/extensions/delegation.ts delete mode 100644 packages/pi-kit/extensions/ember-footer.ts delete mode 100644 packages/pi-kit/extensions/gates.ts delete mode 100644 packages/pi-kit/extensions/grill-review.ts delete mode 100644 packages/pi-kit/extensions/journal.ts delete mode 100644 packages/pi-kit/extensions/lanes.ts delete mode 100644 packages/pi-kit/extensions/table.ts delete mode 100644 packages/pi-kit/extensions/vault.ts delete mode 100644 packages/pi-kit/mcp/create-server.ts delete mode 100755 packages/pi-kit/mcp/server.ts delete mode 100644 packages/pi-kit/package.json delete mode 100644 packages/pi-kit/src/adapters/claude-code.ts delete mode 100644 packages/pi-kit/src/adapters/pi.ts delete mode 100644 packages/pi-kit/src/child-env.ts delete mode 100644 packages/pi-kit/src/delegation-core.ts delete mode 100644 packages/pi-kit/src/execution-adapter.ts delete mode 100644 packages/pi-kit/src/journal-core.ts delete mode 100644 packages/pi-kit/src/lane-coordinator.ts delete mode 100644 packages/pi-kit/src/lanes-tui.ts delete mode 100644 packages/pi-kit/src/runner.ts delete mode 100644 packages/pi-kit/src/schema.ts delete mode 100644 packages/pi-kit/src/table.ts delete mode 100644 packages/pi-kit/src/transcript.ts delete mode 100644 packages/pi-kit/test/btw-extension.test.ts delete mode 100644 packages/pi-kit/test/child-env.test.ts delete mode 100644 packages/pi-kit/test/claude-code-adapter.test.ts delete mode 100644 packages/pi-kit/test/delegation-extension.test.ts delete mode 100644 packages/pi-kit/test/delegation.test.ts delete mode 100644 packages/pi-kit/test/ember-footer.test.ts delete mode 100644 packages/pi-kit/test/execution-adapters.test.ts delete mode 100644 packages/pi-kit/test/fixtures/claude-lane-stream.jsonl delete mode 100644 packages/pi-kit/test/fixtures/lane-stream.jsonl delete mode 100644 packages/pi-kit/test/fixtures/mcp-lane-process.cjs delete mode 100644 packages/pi-kit/test/fixtures/normalized-lane-stream.jsonl delete mode 100644 packages/pi-kit/test/gates.test.ts delete mode 100644 packages/pi-kit/test/grill-review.test.ts delete mode 100644 packages/pi-kit/test/journal.test.ts delete mode 100644 packages/pi-kit/test/lane-coordinator.test.ts delete mode 100644 packages/pi-kit/test/lanes-extension.test.ts delete mode 100644 packages/pi-kit/test/lanes-tui.test.ts delete mode 100644 packages/pi-kit/test/mcp-server.test.ts delete mode 100644 packages/pi-kit/test/runner.test.ts delete mode 100644 packages/pi-kit/test/table.test.ts delete mode 100644 packages/pi-kit/test/transcript.test.ts delete mode 100644 packages/pi-kit/test/vault.test.ts delete mode 100644 packages/pi-kit/themes/pickforge-ember.json diff --git a/AGENTS.md b/AGENTS.md index de558b0..1fc3ee3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,13 +10,3 @@ For substantial work, read `../AGENTS.md` (workspace root) and use the `plan-issue` workflow — GitHub Issues are the canonical plan/progress tracker. -## pi-kit conventions - -- Pi extension handlers must never let an exception escape into the parent - session: wrap handler bodies, return `isError` tool results, and treat UI - calls as fallible. -- The lane runner owns child processes end to end: spawn detached (own - process group), kill by group with SIGTERM→SIGKILL escalation, reap on - parent exit, cap stdout buffering. -- Gate matching is token-based, never substring regex: strip env assignments - and git global options before comparing subcommands. diff --git a/bun.lock b/bun.lock index 57c99e9..8196bb9 100644 --- a/bun.lock +++ b/bun.lock @@ -15,18 +15,18 @@ }, "packages/auth": { "name": "@pickforge/auth", - "version": "0.9.0", + "version": "0.12.0", "dependencies": { "@supabase/supabase-js": "^2.110.0", }, }, "packages/billing": { "name": "@pickforge/billing", - "version": "0.9.0", + "version": "0.12.0", }, "packages/brand": { "name": "@pickforge/brand", - "version": "0.9.0", + "version": "0.12.0", "dependencies": { "@fontsource/geist-mono": "^5.2.8", "@fontsource/geist-sans": "^5.2.5", @@ -34,111 +34,35 @@ }, "packages/edge-shared": { "name": "@pickforge/edge-shared", - "version": "0.9.0", + "version": "0.12.0", }, "packages/flags": { "name": "@pickforge/flags", - "version": "0.9.0", - }, - "packages/pi-kit": { - "name": "@pickforge/pi-kit", - "version": "0.1.0", - "dependencies": { - "@modelcontextprotocol/sdk": "1.29.0", - "zod": "^4.4.3", - }, - "devDependencies": { - "@earendil-works/pi-ai": "0.81.0", - "@earendil-works/pi-coding-agent": "0.81.0", - "@earendil-works/pi-tui": "0.81.0", - "typebox": "1.1.38", - "typescript": "^5.6.0", - "vitest": "^2.0.0", - }, + "version": "0.12.0", }, "packages/sync": { "name": "@pickforge/sync", - "version": "0.9.0", + "version": "0.12.0", }, "packages/tauri-release": { "name": "@pickforge/tauri-release", - "version": "0.9.0", + "version": "0.12.0", "bin": { "pickforge-tauri-release": "dist/cli.js", }, }, }, "packages": { - "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.91.1", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw=="], - - "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], - - "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], - - "@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="], - - "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], - - "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1048.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.11", "@aws-sdk/credential-provider-node": "^3.972.42", "@aws-sdk/eventstream-handler-node": "^3.972.16", "@aws-sdk/middleware-eventstream": "^3.972.12", "@aws-sdk/middleware-websocket": "^3.972.19", "@aws-sdk/token-providers": "3.1048.0", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ=="], - - "@aws-sdk/core": ["@aws-sdk/core@3.975.3", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.36", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.29.4", "@smithy/signature-v4": "^5.6.5", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7ur3kCKuvPLqlsZ2XlvnNBVQ7KkpSu6Y6dOTwSPHLrFpTEfZM8isLBJc4cgv96WB7GifeVM436mpycwxBd2vEA=="], - - "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.59", "", { "dependencies": { "@aws-sdk/core": "^3.975.3", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Ny5e4Mfh3QPmiAc0AiUe+cbTXDlxkU3Rc+EpWOfyWeWEy6yp7Fa1KmfNeCc+1a8by9zQ9gtohmiQUkMPScF3ng=="], - - "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.61", "", { "dependencies": { "@aws-sdk/core": "^3.975.3", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/fetch-http-handler": "^5.6.6", "@smithy/node-http-handler": "^4.9.6", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-8jAjgStl5Ytq4+HF3X/9f+EmRinaRbGRRtQGktlPfBRVx73H+R1y48vIeXerQtYGFaUqkEp3fT6jP854rVO2yQ=="], - - "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.4", "", { "dependencies": { "@aws-sdk/core": "^3.975.3", "@aws-sdk/credential-provider-env": "^3.972.59", "@aws-sdk/credential-provider-http": "^3.972.61", "@aws-sdk/credential-provider-login": "^3.972.66", "@aws-sdk/credential-provider-process": "^3.972.59", "@aws-sdk/credential-provider-sso": "^3.973.3", "@aws-sdk/credential-provider-web-identity": "^3.972.65", "@aws-sdk/nested-clients": "^3.997.33", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/credential-provider-imds": "^4.4.9", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-e6ZvVsj90aRALf1kHP+J4iqC1496ZpVgqI/+u0LJ5HL7q7ATauGy4gdDvRCP13L1pN/fMiZLah162PGIYkbUVQ=="], - - "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.66", "", { "dependencies": { "@aws-sdk/core": "^3.975.3", "@aws-sdk/nested-clients": "^3.997.33", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-g2fsqm87r/nKthLZ0VkkDBElkGg0PvSa8d97HQ6EilMbJTZ6hxa8FxkSZyJfgPfFdZn0TTmkOffQmTSUcAHIng=="], - - "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.70", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.59", "@aws-sdk/credential-provider-http": "^3.972.61", "@aws-sdk/credential-provider-ini": "^3.973.4", "@aws-sdk/credential-provider-process": "^3.972.59", "@aws-sdk/credential-provider-sso": "^3.973.3", "@aws-sdk/credential-provider-web-identity": "^3.972.65", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/credential-provider-imds": "^4.4.9", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3xzvkGdykBunxqh8WudmUpSyLWvIhfI6aBQo1b5rb3mDO5mNLadK+0hiI0qBQBMVynJbfLO+Ajy9dztMwy9O8w=="], - - "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.59", "", { "dependencies": { "@aws-sdk/core": "^3.975.3", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-DlZF2/MhLlatDdlrIy3CUCpfdbLrKx+3SMjVo+WyHnPpwzkc/M3vwAHw4OVJf7DMvO+4vfRqSCMc/E9I1auN0g=="], - - "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.3", "", { "dependencies": { "@aws-sdk/core": "^3.975.3", "@aws-sdk/nested-clients": "^3.997.33", "@aws-sdk/token-providers": "3.1088.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-hmdDHoy2G5Es2e8IgelNMYUuSQI6uCIAKZMJ2u2PdKDhxvbk1uWD/g4+R7R5c/tJfKEB1+KjjWiaoCr/S+ZTiQ=="], - - "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.65", "", { "dependencies": { "@aws-sdk/core": "^3.975.3", "@aws-sdk/nested-clients": "^3.997.33", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-gHQb/Kt0chjk/JQDa/GJDqmAvEuVn8n7z10wK2h0LFM9TUDRkohgOO4aEF+s2sBLM0br7Cl5W6P7phgjrrJvLQ=="], - - "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.29", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-t3tKQRTVXsI2QNPE3CaNjHl0wRO9Xi3acZkAyti2RQsiFmZ9Gi0kArX2ighlRJ1BtDVuul413gThAgzyTfgmWA=="], - - "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.24", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-oykin4mDWxNOuYQ7SF1cHzgYeuFEkF4cdRwgvjFFbIklkx09qIFBiOgsORafG9sXZFO3TayMmQuAQYgADXhI8w=="], - - "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.41", "", { "dependencies": { "@aws-sdk/core": "^3.975.3", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/fetch-http-handler": "^5.6.6", "@smithy/signature-v4": "^5.6.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-LSbGvvYmjc4Br9BPYI2dTLnIclmrSiQbahkP4D6nRGVEv4qsCZ8csVuKBPVEEFCVD+EEngGh8ROls6XpumtwMg=="], - - "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.33", "", { "dependencies": { "@aws-sdk/core": "^3.975.3", "@aws-sdk/signature-v4-multi-region": "^3.996.41", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/fetch-http-handler": "^5.6.6", "@smithy/node-http-handler": "^4.9.6", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dVZOroI/r3/ENvqNGgjMPul+jjlz9GddfVusgTXlVjfZj5isibOxecLkGQbRPp8XOuX+RAfjXLFgPkD1JS5xrw=="], - - "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.41", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng=="], - - "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1048.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA=="], - - "@aws-sdk/types": ["@aws-sdk/types@3.974.2", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA=="], - - "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.8", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g=="], - - "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.36", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA=="], - - "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], - "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], - "@earendil-works/pi-agent-core": ["@earendil-works/pi-agent-core@0.81.1", "", { "dependencies": { "@earendil-works/pi-ai": "^0.81.1", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" } }, "sha512-yqbh68CyhqxMov/jUogFJfMqlu2Gd37GAki+tr59YCmAPHfomiCA5ESzusXtpGzABeiZFC/OrRdQ4GwCCOMIHA=="], - - "@earendil-works/pi-ai": ["@earendil-works/pi-ai@0.81.0", "", { "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", "@google/genai": "1.52.0", "@mistralai/mistralai": "2.2.6", "@opentelemetry/api": "1.9.0", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "openai": "6.26.0", "partial-json": "0.1.7", "typebox": "1.1.38" }, "bin": { "pi-ai": "dist/cli.js" } }, "sha512-n3lDV1Px/2BOp86rUJkoHcXuQ6uf7711VEtSjE5gTwrnPeMAEjzV3LnbQ3wTF9bUxl253Igi3U35U1CMF5POng=="], - - "@earendil-works/pi-coding-agent": ["@earendil-works/pi-coding-agent@0.81.0", "", { "dependencies": { "@earendil-works/pi-agent-core": "^0.81.0", "@earendil-works/pi-ai": "^0.81.0", "@earendil-works/pi-tui": "^0.81.0", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", "diff": "8.0.4", "glob": "13.0.6", "highlight.js": "10.7.3", "hosted-git-info": "9.0.3", "ignore": "7.0.5", "jiti": "2.7.0", "minimatch": "10.2.5", "proper-lockfile": "4.1.2", "semver": "7.8.0", "typebox": "1.1.38", "undici": "8.5.0", "yaml": "2.9.0" }, "optionalDependencies": { "@mariozechner/clipboard": "0.3.9" }, "bin": { "pi": "dist/cli.js" } }, "sha512-2p0Dnx+3fkPLga8M82eg14ZYNLcFLhqxxKyVVfqUSIio9Xx4p7UjvJtopx/6PTeJKmdJl1/xOm/c02AFcJ+l/g=="], - - "@earendil-works/pi-tui": ["@earendil-works/pi-tui@0.81.0", "", { "dependencies": { "get-east-asian-width": "1.6.0", "marked": "18.0.5" } }, "sha512-vdrrV//CldG3Dt20vj+iCRcHOlPwxlOBJwwLg1KE/92IO+TlEwbf4uwKXgFZHGVh9lEJXtYsKzIRb/nHw5YuAA=="], - "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], @@ -201,10 +125,6 @@ "@fontsource/geist-sans": ["@fontsource/geist-sans@5.2.5", "", {}, "sha512-anllOHyJbElRs9fV15TeDRqAeb1IKm4bSknPl6ZMoyPTx1BBy7logudcUwpNjmQLkzn4Q0JGQLRCUKJYoyST6A=="], - "@google/genai": ["@google/genai@1.52.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q=="], - - "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], @@ -213,38 +133,10 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@mariozechner/clipboard": ["@mariozechner/clipboard@0.3.9", "", { "optionalDependencies": { "@mariozechner/clipboard-darwin-arm64": "0.3.9", "@mariozechner/clipboard-darwin-universal": "0.3.9", "@mariozechner/clipboard-darwin-x64": "0.3.9", "@mariozechner/clipboard-linux-arm64-gnu": "0.3.9", "@mariozechner/clipboard-linux-arm64-musl": "0.3.9", "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.9", "@mariozechner/clipboard-linux-x64-gnu": "0.3.9", "@mariozechner/clipboard-linux-x64-musl": "0.3.9", "@mariozechner/clipboard-win32-arm64-msvc": "0.3.9", "@mariozechner/clipboard-win32-x64-msvc": "0.3.9" } }, "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA=="], - - "@mariozechner/clipboard-darwin-arm64": ["@mariozechner/clipboard-darwin-arm64@0.3.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ=="], - - "@mariozechner/clipboard-darwin-universal": ["@mariozechner/clipboard-darwin-universal@0.3.9", "", { "os": "darwin" }, "sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ=="], - - "@mariozechner/clipboard-darwin-x64": ["@mariozechner/clipboard-darwin-x64@0.3.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg=="], - - "@mariozechner/clipboard-linux-arm64-gnu": ["@mariozechner/clipboard-linux-arm64-gnu@0.3.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw=="], - - "@mariozechner/clipboard-linux-arm64-musl": ["@mariozechner/clipboard-linux-arm64-musl@0.3.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ=="], - - "@mariozechner/clipboard-linux-riscv64-gnu": ["@mariozechner/clipboard-linux-riscv64-gnu@0.3.9", "", { "os": "linux", "cpu": "none" }, "sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw=="], - - "@mariozechner/clipboard-linux-x64-gnu": ["@mariozechner/clipboard-linux-x64-gnu@0.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw=="], - - "@mariozechner/clipboard-linux-x64-musl": ["@mariozechner/clipboard-linux-x64-musl@0.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ=="], - - "@mariozechner/clipboard-win32-arm64-msvc": ["@mariozechner/clipboard-win32-arm64-msvc@0.3.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ=="], - - "@mariozechner/clipboard-win32-x64-msvc": ["@mariozechner/clipboard-win32-x64-msvc@0.3.9", "", { "os": "win32", "cpu": "x64" }, "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA=="], - - "@mistralai/mistralai": ["@mistralai/mistralai@2.2.6", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.40.0", "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ=="], - - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], - "@oxc-project/types": ["@oxc-project/types@0.138.0", "", {}, "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA=="], "@pickforge/auth": ["@pickforge/auth@workspace:packages/auth"], @@ -257,30 +149,10 @@ "@pickforge/flags": ["@pickforge/flags@workspace:packages/flags"], - "@pickforge/pi-kit": ["@pickforge/pi-kit@workspace:packages/pi-kit"], - "@pickforge/sync": ["@pickforge/sync@workspace:packages/sync"], "@pickforge/tauri-release": ["@pickforge/tauri-release@workspace:packages/tauri-release"], - "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], - - "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], - - "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], - - "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="], - - "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="], - - "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], - - "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], - - "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], - - "@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.4", "", { "os": "android", "cpu": "arm64" }, "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ=="], @@ -363,26 +235,6 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="], - "@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="], - - "@smithy/core": ["@smithy/core@3.29.5", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-i0dk2t5B+CwV/dcJdUHILYkOQF5lof8f44dFCfDWToGCxjT9YQ+CgHqTAvJxzc3+zqQwm2QtVoJ5IqiNar/CnQ=="], - - "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.10", "", { "dependencies": { "@smithy/core": "^3.29.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-MJenAe4OKRZUo1LdYYFDCsSHxaHvInIU/z52GsheO9vl1/VSySVCr0zkyKD6TFiGkSUaWGxvKZ/70OvgUZR5HQ=="], - - "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.7", "", { "dependencies": { "@smithy/core": "^3.29.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-3zpg8yqqyXzoK2TsRDdkqVOj2RDBFfLXwCczOZ5c7TWB4eiaebfSCsbMjDPYB3PJ9ihV62QaeadZ+wLadZtNGA=="], - - "@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - - "@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA=="], - - "@smithy/signature-v4": ["@smithy/signature-v4@5.6.6", "", { "dependencies": { "@smithy/core": "^3.29.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-efP6DN3UTFrzIsGO42/xcabv8jU7+9nwEdphFUH7yL0k010ERyAWaO41KFQIDLcFZLZ8xzIQr4wplFxNzslSGQ=="], - - "@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], - - "@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], - - "@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@supabase/auth-js": ["@supabase/auth-js@2.110.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-Mi288WCTp6wxMFCOu/UgzgHEXODjdl2uVTLqK11eanzGZaldU3RyP8Am+ZbNuVzFP+5+iOvppxzv7N5Ym84xTg=="], @@ -411,8 +263,6 @@ "@types/node": ["@types/node@24.13.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA=="], - "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], - "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.10", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.10", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.10", "vitest": "4.1.10" }, "optionalPeers": ["@vitest/browser"] }, "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g=="], "@vitest/expect": ["@vitest/expect@4.1.10", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA=="], @@ -429,53 +279,21 @@ "@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="], - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - - "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - - "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.4", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA=="], - "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - - "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], - - "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], - - "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], - - "brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="], - - "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], - "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - - "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], - - "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - - "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], @@ -485,146 +303,32 @@ "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], - - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - "es-module-lexer": ["es-module-lexer@2.3.0", "", {}, "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw=="], - "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], - "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - - "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], - - "eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], - "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], - "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - - "express-rate-limit": ["express-rate-limit@8.6.0", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA=="], - - "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-uri": ["fast-uri@3.1.4", "", {}, "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw=="], - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], - - "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - "fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="], - "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], - - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "gaxios": ["gaxios@7.2.0", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg=="], - - "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], - - "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], - - "google-auth-library": ["google-auth-library@10.9.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg=="], - - "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], - - "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], - - "hono": ["hono@4.12.31", "", {}, "sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg=="], - - "hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="], - "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], - "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - - "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], - - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - "iceberg-js": ["iceberg-js@0.8.1", "", {}, "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA=="], - "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], - - "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], - - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], @@ -633,24 +337,10 @@ "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "jose": ["jose@6.2.4", "", {}, "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA=="], - "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], - "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], - - "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], - - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], - - "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], - - "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], - "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], @@ -681,34 +371,12 @@ "load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="], - "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], - - "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], - - "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="], "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], - "marked": ["marked@18.0.5", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - - "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - - "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - - "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], - "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -717,116 +385,42 @@ "nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="], - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - - "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], - - "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "obug": ["obug@2.1.3", "", {}, "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg=="], - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "openai": ["openai@6.26.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA=="], - - "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], - - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], - - "path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], - "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], - "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], "postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="], "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], - "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], - - "protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="], - - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - - "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], - - "range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="], - - "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], - "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], - "rolldown": ["rolldown@1.1.4", "", { "dependencies": { "@oxc-project/types": "=0.138.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.4", "@rolldown/binding-darwin-arm64": "1.1.4", "@rolldown/binding-darwin-x64": "1.1.4", "@rolldown/binding-freebsd-x64": "1.1.4", "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", "@rolldown/binding-linux-arm64-gnu": "1.1.4", "@rolldown/binding-linux-arm64-musl": "1.1.4", "@rolldown/binding-linux-ppc64-gnu": "1.1.4", "@rolldown/binding-linux-s390x-gnu": "1.1.4", "@rolldown/binding-linux-x64-gnu": "1.1.4", "@rolldown/binding-linux-x64-musl": "1.1.4", "@rolldown/binding-openharmony-arm64": "1.1.4", "@rolldown/binding-wasm32-wasi": "1.1.4", "@rolldown/binding-win32-arm64-msvc": "1.1.4", "@rolldown/binding-win32-x64-msvc": "1.1.4" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA=="], "rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="], - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], - - "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - - "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], - - "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], @@ -843,206 +437,30 @@ "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], - "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], - "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], - "tinyspy": ["tinyspy@3.0.2", "", {}, "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q=="], - - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], - "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], - "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], - "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], - - "typebox": ["typebox@1.1.38", "", {}, "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], - "undici": ["undici@8.5.0", "", {}, "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg=="], - "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "vite": ["vite@8.1.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.16", "rolldown": "~1.1.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA=="], - "vite-node": ["vite-node@2.1.9", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.3.7", "es-module-lexer": "^1.5.4", "pathe": "^1.1.2", "vite": "^5.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA=="], - "vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/browser-webdriverio": "4.1.10", "@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-v8": "4.1.10", "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="], - "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="], - "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], - "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - - "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], - - "@aws-sdk/credential-provider-http/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.7", "", { "dependencies": { "@smithy/core": "^3.29.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-wCU8HCLjAtAVqxxe0j2xff9LcEPw3yjBbg5IdQDIYFnxnPxbxcSLc7rgex7kqm9L/WYOnJEgaWQlfDkZleozMA=="], - - "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1088.0", "", { "dependencies": { "@aws-sdk/core": "^3.975.3", "@aws-sdk/nested-clients": "^3.997.33", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4ObatWt2qpJg5FBk4LOOKrTQYzaqeewAtdO3r9ZO8lH9YqLtpTzLyIdy0mJ+nVdfYOnqISkKNfmzP22bNDhwyw=="], - - "@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.7", "", { "dependencies": { "@smithy/core": "^3.29.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-wCU8HCLjAtAVqxxe0j2xff9LcEPw3yjBbg5IdQDIYFnxnPxbxcSLc7rgex7kqm9L/WYOnJEgaWQlfDkZleozMA=="], - - "@earendil-works/pi-agent-core/@earendil-works/pi-ai": ["@earendil-works/pi-ai@0.81.1", "", { "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", "@google/genai": "1.52.0", "@mistralai/mistralai": "2.2.6", "@opentelemetry/api": "1.9.0", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "openai": "6.26.0", "partial-json": "0.1.7", "typebox": "1.1.38" }, "bin": { "pi-ai": "dist/cli.js" } }, "sha512-hzHE7Z8l5mgJk+ke67Lge0rwS2+wbKJrFKl9o5M1R1rh33+cCT7D1AHz1OAtX5wFs90E1/BTGhyJRTUHaMxGvQ=="], - - "@pickforge/pi-kit/vitest": ["vitest@2.1.9", "", { "dependencies": { "@vitest/expect": "2.1.9", "@vitest/mocker": "2.1.9", "@vitest/pretty-format": "^2.1.9", "@vitest/runner": "2.1.9", "@vitest/snapshot": "2.1.9", "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "debug": "^4.3.7", "expect-type": "^1.1.0", "magic-string": "^0.30.12", "pathe": "^1.1.2", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.1", "tinypool": "^1.0.1", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", "vite-node": "2.1.9", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", "@vitest/browser": "2.1.9", "@vitest/ui": "2.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q=="], - - "@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - - "body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - - "make-dir/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - - "p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], - - "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - - "vite-node/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], - - "vite-node/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], - - "vite-node/vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], - "vitest/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], - - "@pickforge/pi-kit/vitest/@vitest/expect": ["@vitest/expect@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw=="], - - "@pickforge/pi-kit/vitest/@vitest/mocker": ["@vitest/mocker@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.12" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg=="], - - "@pickforge/pi-kit/vitest/@vitest/pretty-format": ["@vitest/pretty-format@2.1.9", "", { "dependencies": { "tinyrainbow": "^1.2.0" } }, "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ=="], - - "@pickforge/pi-kit/vitest/@vitest/runner": ["@vitest/runner@2.1.9", "", { "dependencies": { "@vitest/utils": "2.1.9", "pathe": "^1.1.2" } }, "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g=="], - - "@pickforge/pi-kit/vitest/@vitest/snapshot": ["@vitest/snapshot@2.1.9", "", { "dependencies": { "@vitest/pretty-format": "2.1.9", "magic-string": "^0.30.12", "pathe": "^1.1.2" } }, "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ=="], - - "@pickforge/pi-kit/vitest/@vitest/spy": ["@vitest/spy@2.1.9", "", { "dependencies": { "tinyspy": "^3.0.2" } }, "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ=="], - - "@pickforge/pi-kit/vitest/@vitest/utils": ["@vitest/utils@2.1.9", "", { "dependencies": { "@vitest/pretty-format": "2.1.9", "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ=="], - - "@pickforge/pi-kit/vitest/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], - - "@pickforge/pi-kit/vitest/std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], - - "@pickforge/pi-kit/vitest/tinyrainbow": ["tinyrainbow@1.2.0", "", {}, "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ=="], - - "@pickforge/pi-kit/vitest/vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], - - "vite-node/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], - - "@pickforge/pi-kit/vitest/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], - - "vite-node/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], - - "vite-node/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], - - "vite-node/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], - - "vite-node/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], - - "vite-node/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], - - "vite-node/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], - - "vite-node/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], - - "vite-node/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], - - "vite-node/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], - - "vite-node/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], - - "vite-node/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], - - "vite-node/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], - - "vite-node/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], - - "vite-node/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], - - "vite-node/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], - - "vite-node/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], - - "vite-node/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], - - "vite-node/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], - - "vite-node/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], - - "vite-node/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], - - "vite-node/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], - - "vite-node/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], - - "vite-node/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], - - "@pickforge/pi-kit/vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], } } diff --git a/docs/superpowers/plans/2026-07-21-claude-lanes-deployment.md b/docs/superpowers/archive/2026-07-21-claude-lanes-deployment.md similarity index 99% rename from docs/superpowers/plans/2026-07-21-claude-lanes-deployment.md rename to docs/superpowers/archive/2026-07-21-claude-lanes-deployment.md index dc67421..ead32f3 100644 --- a/docs/superpowers/plans/2026-07-21-claude-lanes-deployment.md +++ b/docs/superpowers/archive/2026-07-21-claude-lanes-deployment.md @@ -1,3 +1,5 @@ +pi-kit extracted to ElbertePlinio/pi-kit per issue #54. + # Managed Claude Lanes Deployment Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. diff --git a/docs/superpowers/specs/2026-07-21-cross-harness-multimodel-lanes-design.md b/docs/superpowers/archive/2026-07-21-cross-harness-multimodel-lanes-design.md similarity index 99% rename from docs/superpowers/specs/2026-07-21-cross-harness-multimodel-lanes-design.md rename to docs/superpowers/archive/2026-07-21-cross-harness-multimodel-lanes-design.md index 51c34b2..90c8427 100644 --- a/docs/superpowers/specs/2026-07-21-cross-harness-multimodel-lanes-design.md +++ b/docs/superpowers/archive/2026-07-21-cross-harness-multimodel-lanes-design.md @@ -1,3 +1,5 @@ +pi-kit extracted to ElbertePlinio/pi-kit per issue #54. + # Cross-Harness Multi-Model Lanes Design **Status:** Approved through Plannotator feedback on 2026-07-21 diff --git a/docs/superpowers/plans/2026-07-21-pi-kit-multimodel-lanes-mcp.md b/docs/superpowers/archive/2026-07-21-pi-kit-multimodel-lanes-mcp.md similarity index 99% rename from docs/superpowers/plans/2026-07-21-pi-kit-multimodel-lanes-mcp.md rename to docs/superpowers/archive/2026-07-21-pi-kit-multimodel-lanes-mcp.md index c471a66..f185416 100644 --- a/docs/superpowers/plans/2026-07-21-pi-kit-multimodel-lanes-mcp.md +++ b/docs/superpowers/archive/2026-07-21-pi-kit-multimodel-lanes-mcp.md @@ -1,3 +1,5 @@ +pi-kit extracted to ElbertePlinio/pi-kit per issue #54. + # pi-kit Multi-Model Lanes MCP Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. diff --git a/packages/pi-kit/README.md b/packages/pi-kit/README.md deleted file mode 100644 index 8a96d52..0000000 --- a/packages/pi-kit/README.md +++ /dev/null @@ -1,105 +0,0 @@ -# pi-kit lanes - -pi-kit runs asynchronous model lanes behind a shared `LaneCoordinator`. The coordinator owns one active run, validation, status, wait detachment, abandonment, shutdown, and JSON-safe snapshots. `LaneRunner` owns adapter selection, detached child process groups, concurrency, normalized events, journaling, transcript capture, SIGTERM-to-SIGKILL escalation, and reaping. Pi keeps presentation, widgets, nudges, and commands in `extensions/lanes.ts`. - -## Origins, routes, and policy - -Callers select a logical model, effort, and task; route and runtime model IDs come only from [`src/table.ts`](src/table.ts). Every lane requires an explicit full model selector and effort. `max` is invalid and `xhigh` is the ceiling. Grok is pinned to `high`; Opus is pinned to `xhigh`. - -Pi-origin lanes accept every table model. Fable, Sonnet, and Opus run through the genuine Claude Code executable. Sol, `xai/grok-4.5` at `high`, and GLM run through Pi directly; Grok does not use the Grok CLI. MCP-origin lanes run Sol, Grok, and GLM through Pi and reject Anthropic selectors because Claude Code must use its native Claude-to-Claude workflow. - -Pi callers may omit `mode`, which defaults to `workspace-write`; MCP callers must explicitly choose `read-only` or `workspace-write`. Read-only Pi workers receive only `read,grep,find,ls`; read-only Claude workers receive only `Read,Glob,Grep`. Workspace-write workers receive the normal tools of their harness: Pi keeps its normal tool set, while Claude receives `Read,Glob,Grep,Edit,Write,Bash`. Existing parent-session plan, local, and ship gates are separate and unchanged. - -Routes fail rather than substitute when a selector, effort, origin, executable, version, or authentication path is unavailable. Claude executable preflight requires a real executable file, rejects wrappers containing permission-bypass flags, and requires Claude Code 2.1.216 or newer. It does not read or copy OAuth credentials; authentication remains the genuine CLI's responsibility. - -## Child boundary - -Children receive a scrubbed environment containing only `PATH`, `HOME`, `USER`, `LOGNAME`, `SHELL`, `TMPDIR`, `TMP`, `TEMP`, locale variables (`LANG`, `LANGUAGE`, and supported `LC_*` keys), terminal variables (`TERM`, `COLORTERM`, `TERM_PROGRAM`, `TERM_PROGRAM_VERSION`), and supported `XDG_*` paths. Pi children additionally receive `PIKIT_CHILD=1`. Provider keys, cloud credentials, cookies, and agent sockets are not inherited. - -Lane IDs must match `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`. Transcript paths are resolved beneath the configured run directory and rejected if they escape it. Commands use fixed argument arrays rather than shell interpolation, and task text is passed as a positional literal. - -## Lane tools - -Pi and the session-local stdio MCP server expose exactly four operations: - -- `lanes_spawn` starts one run non-blockingly and rejects another spawn while it remains active. -- `lanes_status` returns the current run without blocking; an optional lane ID selects one lane. -- `lanes_wait` waits for settlement; cancelling the wait detaches the caller without cancelling lanes. -- `lanes_abandon` stops one named lane or all lanes with an optional reason. - -Pi and MCP handlers contain exceptions and return tool errors instead of throwing into the parent session. Error text is whitespace-normalized, bounded, and text-only. Status, wait, or abandon before spawn is an error. Lanes survive turn cancellation, but session shutdown abandons active work, sends SIGTERM, escalates to SIGKILL after five seconds, and waits for child `close`. - -## MCP wire DTO - -Every successful MCP call returns concise text in `content` and this JSON-safe shape in `structuredContent`; optional fields are omitted when absent: - -```ts -interface LaneSnapshotDto { - lane: string; - model: string; - effort: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; - mode: "read-only" | "workspace-write"; - state: "queued" | "running" | "done" | "failed" | "abandoned"; - currentTool?: string; - lastStatus?: string; - tokensIn: number; - tokensOut: number; - cost: number; - context: number; - answer?: string; - durationMs?: number; - abandonReason?: string; -} - -interface RunSnapshotDto { - run: string; - state: "active" | "ended"; - ok?: boolean; - totals: { - cost: number; - tokensIn: number; - tokensOut: number; - }; - lanes: LaneSnapshotDto[]; -} -``` - -The DTO deliberately excludes task text, cwd, rationale, PID, origin, and timestamps. Every numeric field is normalized with `Number.isFinite(value) ? value : 0`. Failures return bounded text with `isError: true` and no `structuredContent`. - -## Stdio invocation - -From the repository root: - -```sh -bun packages/pi-kit/mcp/server.ts -``` - -From `packages/pi-kit`: - -```sh -bun run mcp -``` - -Stdout is reserved for protocol traffic; diagnostics use stderr. Closing stdin triggers coordinated shutdown and must not leave child process groups running. - -## Journals, transcripts, and stream limits - -Run journals are append-only JSONL at `~/.pickforge/pi-kit/runs/.jsonl`; canonical per-lane transcripts are at `~/.pickforge/pi-kit/raw//.jsonl`. Set `PIKIT_DATA_DIR` to move both roots. Journal and transcript writes are best-effort and never determine lane settlement or crash the parent. - -Canonical transcript events are `task`, `thinking_delta`, `text_delta`, `tool_start`, `tool_end`, cumulative `usage`, and `assistant_end`. `LaneTranscript` also reads archived Pi JSONL. Canonical transcript capture is capped at 4 MiB per lane. Child stdout is parsed as a stream; a single record over 4 MiB fails the lane, while cumulative newline-terminated records do not. Stored final answers are capped at 4,000 characters; rendered text/thinking entries at 20,000 characters and tool inputs/results at 4,000 characters. Parser and spawn failures become failed lanes; missing routes fail before child spawn. - -## Validation - -These checks are synthetic and do not invoke a model: - -```sh -bun test packages/pi-kit/test -bun run typecheck -bun packages/pi-kit/mcp/server.ts (); - let shuttingDown = false; - - pi.registerMessageRenderer("pi-btw", (message, { expanded }, theme) => { - const data = message.details; - const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t)); - - if (!data) { - box.addChild(new Text(theme.fg("error", "▸ btw response unavailable"), 0, 0)); - return box; - } - - const ok = data.state === "done"; - const label = theme.fg(ok ? "customMessageLabel" : "error", ok ? "▸ btw" : "▸ btw failed"); - const meta = theme.fg("dim", `${data.model} · ${data.effort}`); - box.addChild(new Text(`${label} ${meta}`, 0, 0)); - box.addChild(new Text(`${theme.fg("accent", "Q")} ${theme.fg("text", data.prompt)}`, 0, 0)); - - const answer = data.answer.trim() || "No answer returned."; - if (expanded) { - box.addChild(new Text(theme.fg("accent", "A"), 0, 0)); - box.addChild( - new Markdown(answer, 0, 0, getMarkdownTheme(), { - color: (t) => theme.fg("customMessageText", t), - }), - ); - } else { - const oneLine = answer.replace(/\s*\n+\s*/g, " ↵ ").trim(); - const preview = - oneLine.length > COLLAPSED_PREVIEW_CHARS - ? `${oneLine.slice(0, COLLAPSED_PREVIEW_CHARS - 1)}…` - : oneLine; - box.addChild(new Text(`${theme.fg("accent", "A")} ${theme.fg("customMessageText", preview)}`, 0, 0)); - box.addChild(new Text(theme.fg("dim", "ctrl+o expand · full answer in transcript"), 0, 0)); - } - - return box; - }); - - pi.on("session_shutdown", async () => { - shuttingDown = true; - await Promise.allSettled([...active].map((coordinator) => coordinator.shutdown("session ended"))); - }); - - pi.registerCommand("btw", { - description: "Ask a secondary Pi process a side question without queueing or interrupting the parent session", - handler: async (args, ctx) => { - const prompt = args.trim(); - if (!prompt) { - notify(ctx, "Usage: /btw ", "warning"); - return; - } - - try { - const selector = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : ""; - const model = findModel(selector); - if (!model) { - notify(ctx, `The current model cannot run /btw in the managed background pool: ${selector || "none"}`, "error"); - return; - } - - const effort = pi.getThinkingLevel() as Effort; - const spec: LaneSpec = { - lane: "btw", - task: [ - "Answer this side question independently and concisely.", - "Do not modify files, continue the parent task, or ask the parent agent to act.", - "", - prompt, - ].join("\n"), - model: model.selector, - effort, - mode: "read-only", - cwd: ctx.cwd, - rationale: "Independent /btw side question", - }; - const violation = validateLaneSpec(spec); - if (violation) { - notify(ctx, `Cannot start /btw: ${violation}`, "error"); - return; - } - - const coordinator = new LaneCoordinator({ origin: "pi" }); - active.add(coordinator); - notify(ctx, "btw running in a secondary process", "info"); - - void coordinator - .spawn([spec]) - .then(() => coordinator.wait()) - .then((snapshot) => { - const lane = snapshot.lanes[0]; - const done = lane?.state === "done"; - if (shuttingDown) return; - const answer = lane?.answer?.trim() || lane?.abandonReason || "No answer returned."; - pi.sendMessage( - { - customType: "pi-btw", - content: answer, - display: true, - details: { - prompt, - answer, - state: done ? "done" : "failed", - model: model.selector, - effort, - }, - }, - { deliverAs: "nextTurn" }, - ); - const toast = done - ? `btw done — ${answer.slice(0, MAX_NOTIFICATION_CHARS)}${answer.length > MAX_NOTIFICATION_CHARS ? "…" : ""}` - : `btw failed — ${answer.slice(0, MAX_NOTIFICATION_CHARS)}`; - notify(ctx, toast, done ? "info" : "error"); - }) - .catch((error: unknown) => { - if (shuttingDown) return; - const answer = error instanceof Error ? error.message : String(error); - pi.sendMessage( - { - customType: "pi-btw", - content: answer, - display: true, - details: { - prompt, - answer, - state: "failed", - model: model.selector, - effort, - }, - }, - { deliverAs: "nextTurn" }, - ); - notify(ctx, `btw failed — ${answer.slice(0, MAX_NOTIFICATION_CHARS)}`, "error"); - }) - .finally(() => active.delete(coordinator)); - } catch (error) { - notify(ctx, `Cannot start /btw: ${error instanceof Error ? error.message : String(error)}`, "error"); - } - }, - }); -} diff --git a/packages/pi-kit/extensions/compact-tools.ts b/packages/pi-kit/extensions/compact-tools.ts deleted file mode 100644 index c3d7907..0000000 --- a/packages/pi-kit/extensions/compact-tools.ts +++ /dev/null @@ -1,200 +0,0 @@ -/** - * pi-kit compact tool rendering — Pickforge-style, space-efficient tool rows. - * - * Spreads the built-in tool definitions (behavior, descriptions, and prompt - * metadata untouched) and overrides only renderCall/renderResult. Collapsed - * rows are a few lines; ctrl+o still expands the full output. - */ -import type { - BashToolDetails, - EditToolDetails, - ExtensionAPI, - ReadToolDetails, -} from "@earendil-works/pi-coding-agent"; -import { - createBashToolDefinition, - createEditToolDefinition, - createReadToolDefinition, - createWriteToolDefinition, -} from "@earendil-works/pi-coding-agent"; -import { Text, truncateToWidth } from "@earendil-works/pi-tui"; - -const COLLAPSED_RESULT_LINES = 3; -const EXPANDED_RESULT_LINES = 40; -const TITLE_WIDTH = 96; -const LINE_WIDTH = 180; - -function firstLine(text: string): string { - const idx = text.indexOf("\n"); - return idx === -1 ? text : text.slice(0, idx); -} - -/** Logical line count: a single trailing newline does not add a line. */ -function countLines(text: string): number { - if (text.length === 0) return 0; - const body = text.endsWith("\n") ? text.slice(0, -1) : text; - return body.split("\n").length; -} - -function clip(text: string, width: number): string { - return truncateToWidth(text, width, "…"); -} - -function shortPath(path: string): string { - const home = process.env.HOME; - return home && path.startsWith(home) ? `~${path.slice(home.length)}` : path; -} - -export default function compactTools(pi: ExtensionAPI) { - const cwd = process.cwd(); - - // --- bash: `▸ cmd` + compact tail of output --- - const bash = createBashToolDefinition(cwd); - pi.registerTool({ - ...bash, - renderCall(args, theme) { - const cmd = clip(args.command.replaceAll(/\s+/g, " ").trim(), TITLE_WIDTH); - return new Text(`${theme.fg("accent", "▸")} ${theme.fg("text", cmd)}`, 0, 0); - }, - renderResult(result, { expanded, isPartial }, theme, context) { - const content = result.content[0]; - const output = content?.type === "text" ? content.text : ""; - const lines = output.split("\n").filter((line) => line.trim().length > 0); - if (isPartial) { - const tail = lines.at(-1); - return new Text(theme.fg("dim", tail ? `⋯ ${clip(tail, TITLE_WIDTH)}` : "⋯ running"), 0, 0); - } - - const details = result.details as BashToolDetails | undefined; - const failed = context.isError; - const badge = failed ? theme.fg("error", "✖") : theme.fg("success", "✔"); - let text = `${badge} ${theme.fg("muted", `${lines.length} line${lines.length === 1 ? "" : "s"}`)}`; - if (details?.truncation?.truncated) text += theme.fg("warning", " · truncated"); - - const shown = expanded ? EXPANDED_RESULT_LINES : COLLAPSED_RESULT_LINES; - const view = expanded ? lines.slice(0, shown) : lines.slice(-shown); - for (const line of view) { - text += `\n${theme.fg(failed ? "toolOutput" : "dim", clip(line, LINE_WIDTH))}`; - } - if (lines.length > shown) { - text += `\n${theme.fg("muted", `… ${lines.length - shown} more (ctrl+o)`)}`; - } - return new Text(text, 0, 0); - }, - }); - - // --- read: one line, path + size --- - const read = createReadToolDefinition(cwd); - pi.registerTool({ - ...read, - renderCall(args, theme) { - const range = - args.offset || args.limit - ? theme.fg("dim", ` ${args.offset ?? 1}..${args.offset && args.limit ? args.offset + args.limit : (args.limit ?? "")}`) - : ""; - return new Text( - `${theme.fg("accent", "▸")} ${theme.fg("toolTitle", "read")} ${theme.fg("text", shortPath(args.path))}${range}`, - 0, - 0, - ); - }, - renderResult(result, { expanded, isPartial }, theme, context) { - if (isPartial) return new Text(theme.fg("dim", "⋯ reading"), 0, 0); - const content = result.content[0]; - if (context.isError) { - const message = content?.type === "text" ? firstLine(content.text) : "failed"; - return new Text(theme.fg("error", `✖ ${clip(message, TITLE_WIDTH)}`), 0, 0); - } - if (content?.type === "image") return new Text(theme.fg("success", "✔ image"), 0, 0); - if (content?.type !== "text") return new Text(theme.fg("error", "✖ no content"), 0, 0); - - const details = result.details as ReadToolDetails | undefined; - const lineCount = countLines(content.text); - let text = theme.fg("success", "✔ ") + theme.fg("muted", `${lineCount} lines`); - if (details?.truncation?.truncated) { - text += theme.fg("warning", ` of ${details.truncation.totalLines}`); - } - if (expanded) { - for (const line of content.text.split("\n").slice(0, EXPANDED_RESULT_LINES)) { - text += `\n${theme.fg("dim", clip(line, LINE_WIDTH))}`; - } - if (lineCount > EXPANDED_RESULT_LINES) { - text += `\n${theme.fg("muted", `… ${lineCount - EXPANDED_RESULT_LINES} more`)}`; - } - } - return new Text(text, 0, 0); - }, - }); - - // --- edit: path + diff stat, expanded shows the diff --- - const edit = createEditToolDefinition(cwd); - pi.registerTool({ - ...edit, - renderCall(args, theme) { - return new Text( - `${theme.fg("accent", "▸")} ${theme.fg("toolTitle", "edit")} ${theme.fg("text", shortPath(args.path))}`, - 0, - 0, - ); - }, - renderResult(result, { expanded, isPartial }, theme, context) { - if (isPartial) return new Text(theme.fg("dim", "⋯ editing"), 0, 0); - const content = result.content[0]; - if (context.isError) { - const message = content?.type === "text" ? firstLine(content.text) : "failed"; - return new Text(theme.fg("error", `✖ ${clip(message, TITLE_WIDTH)}`), 0, 0); - } - const details = result.details as EditToolDetails | undefined; - if (!details?.diff) return new Text(theme.fg("success", "✔ applied"), 0, 0); - - const diffLines = details.diff.split("\n"); - let additions = 0; - let removals = 0; - for (const line of diffLines) { - if (line.startsWith("+") && !line.startsWith("+++")) additions++; - if (line.startsWith("-") && !line.startsWith("---")) removals++; - } - let text = - theme.fg("success", `✔ +${additions}`) + - theme.fg("dim", " / ") + - theme.fg("error", `-${removals}`); - if (expanded) { - for (const line of diffLines.slice(0, EXPANDED_RESULT_LINES)) { - const color = line.startsWith("+") - ? "toolDiffAdded" - : line.startsWith("-") - ? "toolDiffRemoved" - : "toolDiffContext"; - text += `\n${theme.fg(color, clip(line, LINE_WIDTH))}`; - } - if (diffLines.length > EXPANDED_RESULT_LINES) { - text += `\n${theme.fg("muted", `… ${diffLines.length - EXPANDED_RESULT_LINES} more`)}`; - } - } - return new Text(text, 0, 0); - }, - }); - - // --- write: path + line count --- - const write = createWriteToolDefinition(cwd); - pi.registerTool({ - ...write, - renderCall(args, theme) { - const lineCount = countLines(args.content); - return new Text( - `${theme.fg("accent", "▸")} ${theme.fg("toolTitle", "write")} ${theme.fg("text", shortPath(args.path))} ${theme.fg("dim", `(${lineCount} lines)`)}`, - 0, - 0, - ); - }, - renderResult(result, { isPartial }, theme, context) { - if (isPartial) return new Text(theme.fg("dim", "⋯ writing"), 0, 0); - if (context.isError) { - const content = result.content[0]; - const message = content?.type === "text" ? firstLine(content.text) : "failed"; - return new Text(theme.fg("error", `✖ ${clip(message, TITLE_WIDTH)}`), 0, 0); - } - return new Text(theme.fg("success", "✔ written"), 0, 0); - }, - }); -} diff --git a/packages/pi-kit/extensions/delegation.ts b/packages/pi-kit/extensions/delegation.ts deleted file mode 100644 index 1e1bfcd..0000000 --- a/packages/pi-kit/extensions/delegation.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * pi-kit delegation observability — measures how much tool work the - * orchestrator does directly vs. delegates to lanes. - * - * Shows `delegation N% · X lanes / Y direct` in the footer once the session - * has meaningful tool activity, and journals the final counts at shutdown. - */ -import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; -import { appendEvent } from "../src/journal-core.ts"; -import { emptyCounts, recordTool, summarize, type DelegationCounts } from "../src/delegation-core.ts"; - -const MIN_TOOLS_FOR_STATUS = 5; - -export default function delegation(pi: ExtensionAPI) { - let counts: DelegationCounts = emptyCounts(); - let sessionId: string | undefined; - - function refresh(ctx: ExtensionContext): void { - if (!ctx.hasUI) return; - try { - if (counts.direct + counts.lanes >= MIN_TOOLS_FOR_STATUS) { - ctx.ui.setStatus("pi-delegation", summarize(counts)); - } - } catch { - // Observability must never break the session. - } - } - - pi.on("session_start", (_event, ctx) => { - counts = emptyCounts(); - try { - sessionId = ctx.sessionManager.getSessionId(); - } catch { - sessionId = undefined; - } - }); - - pi.on("tool_execution_start", (event, ctx) => { - const args = event.args as { lanes?: unknown[] } | undefined; - const laneCount = Array.isArray(args?.lanes) ? args.lanes.length : 1; - counts = recordTool(counts, event.toolName, laneCount); - refresh(ctx); - }); - - pi.on("session_shutdown", () => { - if (counts.direct + counts.lanes === 0 || !sessionId) return; - // Dedicated "delegation" lane: never clobbers journal.ts's "main" lane status, - // and lane_created makes the status resolvable by reduceRun on its own. - const run = `sess-${sessionId}`; - const t = new Date().toISOString(); - try { - appendEvent({ - v: 1, - t, - run, - type: "lane_created", - lane: "delegation", - spec: { - lane: "delegation", - task: "delegation-ratio observability", - model: "none/none", - effort: "off", - rationale: "session accounting", - }, - }); - appendEvent({ v: 1, t, run, type: "lane_status", lane: "delegation", text: summarize(counts) }); - } catch { - // Journal is best effort. - } - }); -} diff --git a/packages/pi-kit/extensions/ember-footer.ts b/packages/pi-kit/extensions/ember-footer.ts deleted file mode 100644 index c20791c..0000000 --- a/packages/pi-kit/extensions/ember-footer.ts +++ /dev/null @@ -1,305 +0,0 @@ -/** - * pi-kit ember footer — Pickforge-style replacement for the built-in footer - * and pi-bar. - * - * Line 1: model · thinking · context pressure · git branch - * Line 2 (only when present): extension statuses, deduplicated — status text - * is shown as published, never prefixed with its key (no more `mode:mode:`). - * - * Toggle with /footer. Requires pi-bar to be uninstalled or hidden, since - * both replace the footer. - */ -import { - type AssistantMessage, - calculateCost, - type Usage, -} from "@earendil-works/pi-ai"; -import type { - ExtensionAPI, - ExtensionContext, -} from "@earendil-works/pi-coding-agent"; -import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui"; - -/** Status keys whose text is already surfaced elsewhere in the footer. */ -const HIDDEN_STATUS_KEYS = new Set(["pi-bar"]); - -function fmtTokens(count: number): string { - if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`; - if (count >= 1_000) return `${(count / 1_000).toFixed(0)}k`; - return `${count}`; -} - -function fmtDuration(elapsedMs: number): string { - const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1_000)); - const hours = Math.floor(totalSeconds / 3_600); - const minutes = Math.floor((totalSeconds % 3_600) / 60); - const seconds = totalSeconds % 60; - const clock = `${minutes.toString().padStart(2, "0")}:${ - seconds.toString().padStart(2, "0") - }`; - return hours > 0 ? `${hours}:${clock}` : clock; -} - -function pricedCost(ctx: ExtensionContext, message: AssistantMessage): number { - if (message.usage.cost.total > 0) return message.usage.cost.total; - try { - const provider = message.provider; - const modelId = message.model; - if (!provider || !modelId || message.usage.totalTokens <= 0) return 0; - const model = ctx.modelRegistry.find(provider, modelId); - if ( - !model || - !Object.values(model.cost).some((rate) => - typeof rate === "number" && rate > 0 - ) - ) return 0; - const estimatedUsage: Usage = { - ...message.usage, - cost: { ...message.usage.cost }, - }; - return calculateCost(model, estimatedUsage).total; - } catch { - return 0; - } -} - -export default function emberFooter(pi: ExtensionAPI) { - let enabled = true; - - // Cost accumulation is O(session length); cache it and refresh only when the - // entry count changes instead of walking the session every render frame. - let cachedCost = 0; - let cachedEntryCount = -1; - let sessionStartedAt = Date.now(); - let workingStartedAt: number | null = null; - let workedElapsedMs: number | null = null; - let clockTimer: ReturnType | undefined; - let requestRender: (() => void) | undefined; - - function sessionCost(ctx: ExtensionContext): number { - try { - const entries = ctx.sessionManager.getEntries(); - if (entries.length !== cachedEntryCount) { - cachedEntryCount = entries.length; - cachedCost = 0; - for (const entry of entries) { - if (entry.type === "message" && entry.message.role === "assistant") { - cachedCost += pricedCost(ctx, entry.message as AssistantMessage); - } else if ( - entry.type === "message" && entry.message.role === "toolResult" && - entry.message.usage - ) { - cachedCost += entry.message.usage.cost.total; - } else if ( - (entry.type === "branch_summary" || entry.type === "compaction") && - entry.usage - ) { - cachedCost += entry.usage.cost.total; - } - } - } - } catch { - // Footer must never break the session. - } - return cachedCost; - } - - function stopClock(): void { - if (clockTimer) clearInterval(clockTimer); - clockTimer = undefined; - } - - function startClock(): void { - stopClock(); - clockTimer = setInterval(() => requestRender?.(), 1_000); - clockTimer.unref?.(); - } - - function install(ctx: ExtensionContext): void { - if (!ctx.hasUI) return; - try { - ctx.ui.setFooter((tui, theme, footerData) => { - try { - const render = () => { - try { - tui.requestRender(); - } catch { - // Rendering callbacks must never break the session. - } - }; - requestRender = render; - const unsubscribe = footerData.onBranchChange(render); - const sep = theme.fg("dim", " · "); - - return { - dispose() { - try { - unsubscribe(); - } catch { - // Footer cleanup is best-effort. - } - if (requestRender === render) requestRender = undefined; - }, - invalidate() {}, - render(width: number): string[] { - try { - const cost = sessionCost(ctx); - let contextTokens: number | null = null; - let contextWindow = 0; - try { - const usage = ctx.getContextUsage(); - contextTokens = usage?.tokens ?? null; - contextWindow = usage?.contextWindow ?? 0; - } catch { - // Footer must never break the session. - } - - const model = ctx.model?.id ?? "no-model"; - const thinking = pi.getThinkingLevel(); - const pressurePct = contextTokens !== null && contextWindow > 0 - ? (contextTokens / contextWindow) * 100 - : null; - const pressureColor = pressurePct === null - ? "muted" - : pressurePct < 50 - ? "success" - : pressurePct < 80 - ? "warning" - : "error"; - const pressure = pressurePct === null - ? "—" - : `${pressurePct.toFixed(0)}% of ${fmtTokens(contextWindow)}`; - const branch = footerData.getGitBranch(); - const now = Date.now(); - let costText = `$${cost.toFixed(2)}`; - try { - if (ctx.model && ctx.modelRegistry.isUsingOAuth(ctx.model)) { - costText += " est"; - } - } catch { - // Footer must never break the session. - } - - const left = [ - theme.fg("accent", model), - theme.fg("muted", `think ${thinking}`), - theme.fg(pressureColor, pressure), - theme.fg( - "muted", - `session ${fmtDuration(now - sessionStartedAt)}`, - ), - ...(workingStartedAt !== null - ? [ - theme.fg( - "warning", - `working ${fmtDuration(now - workingStartedAt)}`, - ), - ] - : workedElapsedMs !== null - ? [ - theme.fg( - "muted", - `worked ${fmtDuration(workedElapsedMs)}`, - ), - ] - : []), - ].join(sep); - const right = [ - ...(branch ? [theme.fg("muted", ` ${branch}`)] : []), - theme.fg("dim", costText), - ].join(sep); - - const pad = " ".repeat( - Math.max(1, width - visibleWidth(left) - visibleWidth(right)), - ); - const lines = [truncateToWidth(left + pad + right, width)]; - - // Line 2: extension statuses, text only, deduplicated. - const seen = new Set(); - const statuses: string[] = []; - for (const [key, text] of footerData.getExtensionStatuses()) { - if (HIDDEN_STATUS_KEYS.has(key)) continue; - const clean = text.trim(); - if (!clean || seen.has(clean)) continue; - seen.add(clean); - statuses.push(clean); - } - if (statuses.length > 0) { - lines.push(truncateToWidth(statuses.join(sep), width)); - } - return lines; - } catch { - return []; - } - }, - }; - } catch { - return { - dispose() {}, - invalidate() {}, - render: () => [], - }; - } - }); - } catch { - // Footer installation must never break the session. - } - } - - pi.on("session_start", async (_event, ctx) => { - sessionStartedAt = Date.now(); - workingStartedAt = null; - workedElapsedMs = null; - cachedEntryCount = -1; - stopClock(); - try { - await ctx.modelRegistry.refresh(); - } catch { - // Existing registry data is still safe to render. - } - if (ctx.hasUI && enabled) startClock(); - if (enabled) install(ctx); - }); - - pi.on("agent_start", () => { - if (workingStartedAt === null) { - workedElapsedMs = null; - workingStartedAt = Date.now(); - } - requestRender?.(); - }); - - pi.on("agent_settled", () => { - if (workingStartedAt !== null) { - workedElapsedMs = Date.now() - workingStartedAt; - workingStartedAt = null; - } - requestRender?.(); - }); - - pi.on("session_shutdown", () => { - stopClock(); - workingStartedAt = null; - workedElapsedMs = null; - }); - - pi.registerCommand("footer", { - description: "Toggle the Pickforge ember footer", - handler: async (_args, ctx) => { - try { - enabled = !enabled; - if (enabled) { - startClock(); - install(ctx); - ctx.ui.notify("ember footer on", "info"); - } else { - stopClock(); - ctx.ui.setFooter(undefined); - ctx.ui.notify("built-in footer restored", "info"); - } - } catch { - // Footer commands must never break the session. - } - }, - }); -} diff --git a/packages/pi-kit/extensions/gates.ts b/packages/pi-kit/extensions/gates.ts deleted file mode 100644 index 455f70c..0000000 --- a/packages/pi-kit/extensions/gates.ts +++ /dev/null @@ -1,169 +0,0 @@ -/** - * pi-kit delivery-mode gate — blocks bash commands per the active delivery mode. - * - * Modes: - * - "plan-only": blocks any git/gh command that commits or ships (commit, push, merge, pr create/merge). - * - "local" : allows local commits, blocks anything that reaches a remote (push, pr merge). - * - "ship" : no delivery-mode blocking. - * All modes always block catastrophic `rm -rf` targeting `/`, `~`, `$HOME`, or `*`. - */ -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; -import { isToolCallEventType } from "@earendil-works/pi-coding-agent"; - -export type DeliveryMode = "plan-only" | "local" | "ship"; - -export interface GateDecision { - block: boolean; - reason?: string; -} - -const GIT_BLOCKED: Record> = { - "plan-only": { push: true, merge: true, commit: true }, - local: { push: true }, - ship: {}, -}; - -const GH_PR_BLOCKED: Record> = { - "plan-only": { create: true, merge: true }, - local: { merge: true }, - ship: {}, -}; - -/** git global options that consume a following argument. */ -const GIT_OPT_WITH_ARG: Record = { - "-C": true, - "-c": true, - "--git-dir": true, - "--work-tree": true, - "--namespace": true, - "--exec-path": true, -}; - -/** Bare tokens that make a recursive rm catastrophic — never a subpath like /tmp/foo or ./node_modules. */ -const DANGEROUS_RM_TARGETS: Record = { - "/": true, - "/*": true, - "~": true, - "~/": true, - "~/*": true, - $HOME: true, - "$HOME/": true, - "$HOME/*": true, - "*": true, -}; - -/** Resolve the effective subcommand after git's global options. */ -function gitSubcommand(tokens: string[], gitIndex: number): string | undefined { - let i = gitIndex + 1; - while (i < tokens.length) { - const t = tokens[i]!; - if (GIT_OPT_WITH_ARG[t]) { - i += 2; - continue; - } - if (t.startsWith("-")) { - i += 1; - continue; - } - return t; - } - return undefined; -} - -/** Split a shell command into chained sub-commands and tokenize, dropping leading env assignments. */ -function subCommands(command: string): string[][] { - const out: string[][] = []; - for (const raw of command.split(/&&|\|\||[;|]/)) { - const tokens = raw.trim().split(/\s+/).filter(Boolean); - let start = 0; - while (start < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[start]!)) start++; - if (start < tokens.length) out.push(tokens.slice(start)); - } - return out; -} - -function isCatastrophicRm(tokens: string[]): boolean { - const rmIndex = tokens.findIndex((t) => t === "rm" || /(?:^|\/)rm$/.test(t)); - if (rmIndex === -1) return false; - - let hasRecursive = false; - let hasDangerousTarget = false; - for (const arg of tokens.slice(rmIndex + 1)) { - const stripped = arg.replace(/^["']|["']$/g, ""); - if (/^--recursive$/i.test(stripped)) hasRecursive = true; - else if (/^-[a-z]+$/i.test(stripped)) { - if (/r/i.test(stripped)) hasRecursive = true; - } else if (DANGEROUS_RM_TARGETS[stripped]) { - hasDangerousTarget = true; - } - } - return hasRecursive && hasDangerousTarget; -} - -/** Pure gate decision — kept separate from extension wiring so it's directly testable. */ -export function decideGate(mode: DeliveryMode, command: string): GateDecision { - for (const tokens of subCommands(command)) { - if (isCatastrophicRm(tokens)) { - return { block: true, reason: "catastrophic recursive rm blocked in all modes" }; - } - - for (let i = 0; i < tokens.length; i++) { - const t = tokens[i]!; - if (t === "git" || /(?:^|\/)git$/.test(t)) { - const sub = gitSubcommand(tokens, i); - if (sub && GIT_BLOCKED[mode][sub]) { - return { block: true, reason: `delivery mode ${mode} blocks git ${sub}; /mode local or ship to allow` }; - } - } else if (t === "gh" || /(?:^|\/)gh$/.test(t)) { - const rest = tokens.slice(i + 1).filter((tok) => !tok.startsWith("-")); - if (rest[0] === "pr" && rest[1] && GH_PR_BLOCKED[mode][rest[1]]) { - return { block: true, reason: `delivery mode ${mode} blocks gh pr ${rest[1]}; /mode local or ship to allow` }; - } - } - } - } - return { block: false }; -} - -export default function (pi: ExtensionAPI) { - let mode: DeliveryMode = "local"; - - pi.on("session_start", async (_event, ctx) => { - if (ctx.hasUI) ctx.ui.setStatus("mode", `mode: ${mode}`); - }); - - pi.registerCommand("mode", { - description: "Show or set the delivery-mode gate (plan-only | local | ship)", - handler: async (args, ctx) => { - const arg = args.trim(); - if (!arg) { - ctx.ui.notify(`mode: ${mode}`, "info"); - return; - } - if (arg !== "plan-only" && arg !== "local" && arg !== "ship") { - ctx.ui.notify("usage: /mode ", "error"); - return; - } - mode = arg; - ctx.ui.notify(`mode: ${mode}`, "info"); - ctx.ui.setStatus("mode", `mode: ${mode}`); - try { - pi.appendEntry("pikit-mode", { mode }); - } catch { - // best effort — session persistence is not load-bearing for the gate itself - } - }, - }); - - pi.on("tool_call", async (event) => { - if (!isToolCallEventType("bash", event)) return undefined; - const command = event.input.command; - if (typeof command !== "string") return undefined; - - const decision = decideGate(mode, command); - if (decision.block) { - return { block: true, reason: decision.reason }; - } - return undefined; - }); -} diff --git a/packages/pi-kit/extensions/grill-review.ts b/packages/pi-kit/extensions/grill-review.ts deleted file mode 100644 index 3926075..0000000 --- a/packages/pi-kit/extensions/grill-review.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { randomUUID } from "node:crypto"; - -import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent"; -import { Type } from "typebox"; - -const PLANNOTATOR_REQUEST_CHANNEL = "plannotator:request"; - -type AnnotationResult = { - feedback?: string; - approved?: boolean; - exit?: boolean; -}; - -type PlannotatorResponse = - | { status: "handled"; result: AnnotationResult } - | { status: "unavailable"; error?: string } - | { status: "error"; error: string }; - -export type GrillReviewOutcome = - | { status: "feedback"; feedback: string } - | { status: "approved" } - | { status: "closed" } - | { status: "error"; error: string }; - -type NotificationContext = Pick; - -function safeNotify(ctx: NotificationContext, message: string, level: "info" | "warning" | "error"): void { - try { - ctx.ui.notify(message, level); - } catch { - // Review UX must never break the parent session. - } -} - -function fallbackPlannotatorUrl(): string { - return `http://localhost:${process.env.PLANNOTATOR_PORT || "19432"}/`; -} - -async function notifyPlannotatorUrl( - pi: ExtensionAPI, - ctx: NotificationContext, - signal?: AbortSignal, -): Promise { - let url = fallbackPlannotatorUrl(); - try { - const result = await pi.exec("plannotator-url", [], { timeout: 1_500, signal }); - const candidate = result.stdout.trim(); - if (result.code === 0 && /^https?:\/\/\S+\/$/.test(candidate)) { - url = candidate; - } - } catch { - // The loopback URL remains usable when the managed helper is unavailable. - } - if (signal?.aborted) { - throw new Error("Grill review was cancelled before opening."); - } - safeNotify(ctx, `Open Plannotator: ${url}`, "info"); -} - -function hasPlannotator(pi: ExtensionAPI): boolean { - try { - return pi.getCommands().some((command) => - command.name === "plannotator-last" || command.name.startsWith("plannotator-last:"), - ); - } catch { - return false; - } -} - -export function openGrillReview(pi: ExtensionAPI, signal?: AbortSignal): Promise { - if (!hasPlannotator(pi)) { - return Promise.resolve({ - status: "error", - error: "Plannotator is not loaded. Enable npm:@plannotator/pi-extension and reload Pi.", - }); - } - - if (signal?.aborted) { - return Promise.resolve({ status: "error", error: "Grill review was cancelled before opening." }); - } - - return new Promise((resolve) => { - let settled = false; - - // Plannotator owns the browser session and exposes no cancellation handle. - // Once opened, its human decision remains authoritative even if Pi is interrupted. - const finish = (outcome: GrillReviewOutcome) => { - if (settled) return; - settled = true; - resolve(outcome); - }; - - try { - pi.events.emit(PLANNOTATOR_REQUEST_CHANNEL, { - requestId: randomUUID(), - action: "annotate-last", - payload: { - filePath: "assistant-message", - mode: "annotate-last", - gate: true, - }, - respond: (response: PlannotatorResponse) => { - if (response.status === "error" || response.status === "unavailable") { - finish({ status: "error", error: response.error ?? "Plannotator review is unavailable." }); - return; - } - - const feedback = response.result.feedback?.trim(); - if (feedback) { - finish({ status: "feedback", feedback }); - } else if (response.result.approved) { - finish({ status: "approved" }); - } else { - finish({ status: "closed" }); - } - }, - }); - } catch (error) { - finish({ status: "error", error: error instanceof Error ? error.message : String(error) }); - } - }); -} - -function outcomeText(outcome: GrillReviewOutcome): string { - switch (outcome.status) { - case "feedback": - return `Grill review feedback:\n\n${outcome.feedback}`; - case "approved": - return "Grill review approved. Continue with the current direction."; - case "closed": - return "Grill review closed without feedback."; - case "error": - return outcome.error; - } -} - -export default function grillReview(pi: ExtensionAPI): void { - pi.registerTool({ - name: "grill_review", - label: "Grill review", - description: "Open Plannotator on the latest assistant question batch and return the human's annotations as structured feedback.", - promptSnippet: "Open Plannotator to review the latest question batch", - promptGuidelines: [ - "Use grill_review after presenting a compact numbered batch of related decisions or clarification questions when inline annotation would make answering easier; do not use it for a single simple question or facts the agent can discover itself.", - ], - parameters: Type.Object({}), - async execute(_toolCallId, _params, signal, _onUpdate, ctx) { - if (!hasPlannotator(pi)) { - throw new Error("Plannotator is not loaded. Enable npm:@plannotator/pi-extension and reload Pi."); - } - if (signal?.aborted) { - throw new Error("Grill review was cancelled before opening."); - } - - await notifyPlannotatorUrl(pi, ctx, signal); - const outcome = await openGrillReview(pi, signal); - if (outcome.status === "error") { - throw new Error(outcome.error); - } - return { - content: [{ type: "text" as const, text: outcomeText(outcome) }], - details: outcome, - }; - }, - }); - - pi.registerCommand("grill-review", { - description: "Review the latest assistant question batch in Plannotator", - handler: async (_args, ctx) => { - try { - await ctx.waitForIdle(); - if (!hasPlannotator(pi)) { - safeNotify(ctx, "Plannotator is not loaded. Enable npm:@plannotator/pi-extension and reload Pi.", "error"); - return; - } - await notifyPlannotatorUrl(pi, ctx); - const outcome = await openGrillReview(pi); - if (outcome.status === "feedback") { - pi.sendUserMessage(`Grill review feedback:\n\n${outcome.feedback}`, { deliverAs: "followUp" }); - return; - } - if (outcome.status === "approved") { - safeNotify(ctx, "Grill review approved.", "info"); - return; - } - if (outcome.status === "closed") { - safeNotify(ctx, "Grill review closed without feedback.", "info"); - return; - } - safeNotify(ctx, outcome.error, "error"); - } catch (error) { - safeNotify(ctx, `Grill review failed: ${error instanceof Error ? error.message : String(error)}`, "error"); - } - }, - }); -} diff --git a/packages/pi-kit/extensions/journal.ts b/packages/pi-kit/extensions/journal.ts deleted file mode 100644 index 5eec8e5..0000000 --- a/packages/pi-kit/extensions/journal.ts +++ /dev/null @@ -1,121 +0,0 @@ -import type { - ExtensionAPI, - ExtensionContext, -} from "@earendil-works/pi-coding-agent"; - -import type { Effort, KitEvent } from "../src/schema.ts"; -import { appendEvent, newRunId } from "../src/journal-core.ts"; - -type JournalEvent = KitEvent extends infer Event - ? Event extends KitEvent - ? Omit - : never - : never; - -export default function journal(pi: ExtensionAPI) { - let runId: string | undefined; - let startedAt = 0; - let mainCreated = false; - let ended = false; - let warned = false; - - function warn(ctx: ExtensionContext): void { - if (warned) return; - warned = true; - try { - ctx.ui.setStatus("pi-kit-journal", "journal unavailable"); - } catch { - // The journal must never affect the parent session. - } - } - - function record(ctx: ExtensionContext, event: JournalEvent): void { - if (!runId) return; - try { - appendEvent({ ...event, v: 1, t: new Date().toISOString(), run: runId } as KitEvent); - } catch { - warn(ctx); - } - } - - function ensureMain(ctx: ExtensionContext): void { - if (mainCreated || !runId) return; - const model = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "unknown/unknown"; - record(ctx, { - type: "lane_created", - lane: "main", - spec: { - lane: "main", - task: "parent session", - model, - effort: pi.getThinkingLevel() as Effort, - rationale: "parent session", - }, - }); - mainCreated = true; - } - - pi.on("session_start", (_event, ctx) => { - warned = false; - runId = undefined; - try { - const sessionId = ctx.sessionManager.getSessionId(); - runId = sessionId ? `sess-${sessionId}` : newRunId(); - startedAt = Date.now(); - mainCreated = false; - ended = false; - record(ctx, { type: "run_created", lanes: 1, origin: "session" }); - } catch { - runId = undefined; - warn(ctx); - } - }); - - pi.on("turn_end", (event, ctx) => { - try { - ensureMain(ctx); - const text = event.message.role === "assistant" - ? event.message.content - .filter((part) => part.type === "text") - .map((part) => part.text) - .join(" ") - .replaceAll(/\s+/g, " ") - .trim() - .slice(0, 120) - : ""; - if (text) record(ctx, { type: "lane_status", lane: "main", text }); - } catch { - warn(ctx); - } - }); - - pi.on("tool_execution_start", (event, ctx) => { - try { - ensureMain(ctx); - const args = event.args as Record | undefined; - const detail = typeof args?.command === "string" - ? args.command.slice(0, 60) - : typeof args?.path === "string" - ? args.path - : JSON.stringify(args ?? {}) ?? ""; - const summary = detail.replaceAll(/\s+/g, " ").trim().slice(0, 120); - record(ctx, { type: "lane_tool", lane: "main", tool: event.toolName, summary }); - } catch { - warn(ctx); - } - }); - - pi.on("session_shutdown", (_event, ctx) => { - if (ended) return; - try { - record(ctx, { - type: "run_end", - ok: true, - durationMs: startedAt ? Date.now() - startedAt : 0, - }); - ended = true; - } catch { - warn(ctx); - } - }); -} diff --git a/packages/pi-kit/extensions/lanes.ts b/packages/pi-kit/extensions/lanes.ts deleted file mode 100644 index fd41a7f..0000000 --- a/packages/pi-kit/extensions/lanes.ts +++ /dev/null @@ -1,529 +0,0 @@ -import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; -import { Type } from "typebox"; -import { - LaneCoordinator, - type LaneSnapshotDto, - type RunSnapshotDto, -} from "../src/lane-coordinator.ts"; -import { LanesTuiComponent } from "../src/lanes-tui.ts"; -import { LaneRunner } from "../src/runner.ts"; -import type { LaneSpec } from "../src/schema.ts"; -import { MODEL_TABLE, validateLaneSpec } from "../src/table.ts"; - -export interface LanesCoordinatorPort { - spawn(specs: LaneSpec[]): Promise; - status(lane?: string): RunSnapshotDto; - wait(signal?: AbortSignal): Promise; - abandon(input: { lane?: string; reason?: string }): RunSnapshotDto; - shutdown(reason: string): Promise; -} - -export interface LanesExtensionOptions { - createCoordinator?: () => LanesCoordinatorPort; -} - -interface LastRun { - id: string; - snapshot: RunSnapshotDto; - ctx: ExtensionContext; - hidden: boolean; - /** True once the coordinated run has settled and the summary is available. */ - ended: boolean; - timer?: NodeJS.Timeout; -} - -interface SpawnLaneInput { - lane?: string; - task: string; - model: string; - effort: string; - cwd?: string; - rationale?: string; -} - -type WidgetContext = Pick; - -function renderWidget(ctx: WidgetContext, run: LastRun): void { - if (!ctx.hasUI || run.hidden) return; - try { - renderWidgetUnsafe(ctx, run); - } catch { - // Widget rendering must never break the coordinator or the session. - } -} - -const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; -let spinnerTick = 0; - -function shortModel(selector: string): string { - return selector.slice(selector.indexOf("/") + 1); -} - -function fmtTokens(count: number): string { - if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`; - if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k`; - return `${count}`; -} - -function fmtDuration(ms: number): string { - const s = Math.floor(ms / 1000); - if (s < 60) return `${s}s`; - return `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`; -} - -function renderWidgetUnsafe(ctx: WidgetContext, run: LastRun): void { - const theme = ctx.ui.theme; - const lanes = run.snapshot.lanes; - spinnerTick = (spinnerTick + 1) % SPINNER.length; - - const stateGlyph = (state: LaneSnapshotDto["state"]): string => { - switch (state) { - case "queued": - return theme.fg("dim", "◌"); - case "running": - return theme.fg("accent", SPINNER[spinnerTick]!); - case "done": - return theme.fg("success", "✔"); - case "failed": - return theme.fg("error", "✖"); - case "abandoned": - return theme.fg("warning", "⊘"); - } - }; - - const done = lanes.filter((l) => l.state === "done").length; - const running = lanes.filter((l) => l.state === "running").length; - const failed = lanes.filter((l) => l.state === "failed" || l.state === "abandoned").length; - const settled = done + failed; - - const progress = lanes - .map((l) => - l.state === "done" - ? theme.fg("success", "■") - : l.state === "running" - ? theme.fg("accent", "▣") - : l.state === "queued" - ? theme.fg("dim", "□") - : theme.fg("error", "■"), - ) - .join(""); - - const dot = theme.fg("dim", " · "); - const header = [ - theme.fg("accent", "▸ lanes"), - theme.fg("muted", run.id), - `${progress} ${theme.fg("text", `${settled}/${lanes.length}`)}`, - theme.fg("warning", `$${run.snapshot.totals.cost.toFixed(4)}`), - theme.fg("muted", `↑${fmtTokens(run.snapshot.totals.tokensIn)} ↓${fmtTokens(run.snapshot.totals.tokensOut)}`), - theme.fg("dim", fmtDuration(run.snapshot.durationMs)), - ].join(dot); - - const nameWidth = Math.min(24, Math.max(...lanes.map((l) => l.lane.length), 4)); - const lines = [ - header, - ...lanes.map((lane) => { - const name = - lane.state === "running" - ? theme.fg("text", lane.lane.padEnd(nameWidth)) - : theme.fg("muted", lane.lane.padEnd(nameWidth)); - const model = theme.fg("dim", `${shortModel(lane.model)}:${lane.effort}`); - const tokens = theme.fg("muted", `↑${fmtTokens(lane.tokensIn)} ↓${fmtTokens(lane.tokensOut)}`); - const cost = theme.fg("warning", `$${lane.cost.toFixed(4)}`); - const parts = [`${stateGlyph(lane.state)} ${name}`, model, tokens, cost]; - if (lane.durationMs !== undefined) parts.push(theme.fg("dim", fmtDuration(lane.durationMs))); - if (lane.state === "failed" || lane.state === "abandoned") { - parts.push(theme.fg("error", (lane.abandonReason ?? "failed").slice(0, 40))); - } else { - const activity = lane.currentTool ?? lane.lastStatus ?? ""; - if (activity) parts.push(theme.fg("dim", activity.slice(0, 44))); - } - return ` ${parts.join(dot)}`; - }), - ]; - ctx.ui.setWidget("pi-lanes", lines); - ctx.ui.setStatus( - "pi-lanes", - running > 0 - ? theme.fg("accent", `▸ ${running} lane${running === 1 ? "" : "s"} running · $${run.snapshot.totals.cost.toFixed(4)}`) - : failed > 0 - ? theme.fg("error", `▸ lanes: ${done}✔ ${failed}✖`) - : theme.fg("success", `▸ lanes: ${done}/${lanes.length} ✔`), - ); -} - -function errorMessage(error: unknown): string { - const message = error instanceof Error ? error.message : String(error); - return message.replace(/\s+/g, " ").trim().slice(0, 240) || "unknown error"; -} - -function toolFailure(name: string, error: unknown) { - return { - content: [{ type: "text" as const, text: `${name} failed: ${errorMessage(error)}` }], - details: undefined, - isError: true, - }; -} - -function safeNotify(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error"): void { - try { - ctx.ui.notify(message, level); - } catch { - // Notifications are best-effort. - } -} - -function clearWidget(ctx: ExtensionContext): void { - try { - ctx.ui.setWidget("pi-lanes", undefined); - } catch { - // UI cleanup is best-effort. - } - try { - ctx.ui.setStatus("pi-lanes", undefined); - } catch { - // UI cleanup is best-effort. - } -} - -export default function lanesExtension(pi: ExtensionAPI, options: LanesExtensionOptions = {}): void { - let lastRun: LastRun | undefined; - const coordinator = - options.createCoordinator?.() ?? - new LaneCoordinator({ - origin: "pi", - createRunner: (runnerOptions) => - new LaneRunner({ - ...runnerOptions, - onUpdate() { - try { - if (lastRun?.id === runnerOptions.runId) renderLiveWidget(lastRun.ctx, lastRun); - } catch { - // Runner updates must never escape into the coordinated lifecycle. - } - }, - }), - }); - - function updateRun(run: LastRun, snapshot: RunSnapshotDto): void { - if (snapshot.run !== run.id) return; - run.snapshot = snapshot; - run.ended = snapshot.state === "ended"; - } - - function refreshRun(run: LastRun): void { - updateRun(run, coordinator.status()); - } - - function renderLiveWidget(ctx: ExtensionContext, run: LastRun): void { - try { - refreshRun(run); - } catch { - // A stale snapshot is sufficient for best-effort UI rendering. - } - renderWidget(ctx, run); - } - - async function monitorSettlement(run: LastRun, ctx: ExtensionContext): Promise { - try { - updateRun(run, await coordinator.wait()); - } catch { - return; - } finally { - clearInterval(run.timer); - // Settling a background run must update only its UI state. Starting or - // queueing a parent turn here would prevent the user from continuing. - if (lastRun === run) renderWidget(ctx, run); - } - } - - // Lanes survive ESC/turn cancellation, but not the session itself: on - // shutdown (/new, reload, exit) active children are stopped and reaped. - pi.on("session_shutdown", async () => { - try { - await coordinator.shutdown("session ended"); - if (lastRun) { - try { - refreshRun(lastRun); - } catch { - // Shutdown status refresh is best-effort. - } - renderWidget(lastRun.ctx, lastRun); - } - } catch { - // Session shutdown must never escape into the parent session. - } finally { - if (lastRun) clearInterval(lastRun.timer); - } - }); - - pi.registerTool({ - name: "lanes_spawn", - label: "Spawn lanes", - description: `Spawn independent Pi lanes that run concurrently in the background (non-blocking: this tool returns immediately; lanes survive turn cancellation). Never wait on an active run: finish the parent turn so the user can keep messaging, inspect progress with lanes_status only when needed, and collect results after the run settles with lanes_wait or lanes_status. Stop lanes only with lanes_abandon. You MUST state an explicit model and effort for every lane. Any effort off|minimal|low|medium|high|xhigh is allowed per lane (the value in parentheses is only that model's starting prior). Models: ${MODEL_TABLE.map((row) => `${row.selector} (prior ${row.prior})`).join(", ")}.`, - parameters: Type.Object({ - lanes: Type.Array( - Type.Object({ - lane: Type.Optional(Type.String()), - task: Type.String(), - model: Type.String(), - effort: Type.String(), - cwd: Type.Optional(Type.String()), - rationale: Type.Optional(Type.String()), - }), - { minItems: 1 }, - ), - }), - async execute(_toolCallId, params, _signal, _onUpdate, ctx) { - try { - const specs: LaneSpec[] = params.lanes.map((lane: SpawnLaneInput, index: number) => ({ - lane: lane.lane?.trim() || `lane-${index + 1}`, - task: lane.task, - model: lane.model, - effort: lane.effort as LaneSpec["effort"], - ...(lane.cwd ? { cwd: lane.cwd } : {}), - ...(lane.rationale ? { rationale: lane.rationale } : {}), - })); - const violations = specs.flatMap((spec) => { - const reason = validateLaneSpec(spec, { origin: "pi" }); - return reason ? [`${spec.lane}: ${reason}`] : []; - }); - if (violations.length > 0) { - return { - content: [{ type: "text" as const, text: `Invalid lane specs:\n${violations.join("\n")}` }], - details: { violations }, - isError: true, - }; - } - - if (lastRun && !lastRun.ended) { - return { - content: [ - { - type: "text" as const, - text: `Run ${lastRun.id} is still active. Use lanes_status to inspect it, lanes_wait to collect it, or lanes_abandon to stop it before spawning a new run.`, - }, - ], - details: { activeRun: lastRun.id }, - isError: true, - }; - } - - // Deliberately NOT wired to the tool-call abort signal: lanes survive - // ESC / turn cancellation. Stopping is explicit via lanes_abandon. - const snapshot = await coordinator.spawn(specs); - const view: LastRun = { - id: snapshot.run, - snapshot, - ctx, - hidden: false, - ended: snapshot.state === "ended", - }; - if (ctx.hasUI && !view.ended) { - view.timer = setInterval(() => renderLiveWidget(ctx, view), 1_000); - view.timer.unref(); - } - lastRun = view; - renderWidget(ctx, view); - void monitorSettlement(view, ctx); - - const text = [ - `Run ${view.id} spawned with ${specs.length} lane${specs.length === 1 ? "" : "s"} (non-blocking):`, - ...specs.map((spec) => ` ${spec.lane}: ${spec.model}:${spec.effort}`), - "Lanes run in the background and survive turn cancellation.", - "Finish the parent turn now so the user can keep messaging while lanes run.", - "Use lanes_status only when progress is needed; collect results after completion with lanes_wait or lanes_status; use lanes_abandon to stop lanes.", - ].join("\n"); - return { content: [{ type: "text" as const, text }], details: { run: view.id, lanes: specs.length } }; - } catch (error) { - return toolFailure("lanes_spawn", error); - } - }, - }); - - function summarizeLane(lane: LaneSnapshotDto, verbose: boolean): string { - const answerLimit = verbose ? 4_000 : 400; - const answer = lane.answer?.replace(/\s+/g, " ").trim().slice(0, answerLimit) ?? ""; - const failure = lane.state === "failed" || lane.state === "abandoned" ? ` · ${lane.abandonReason ?? "failed"}` : ""; - const activity = - lane.state === "running" ? ` · ${(lane.currentTool ?? lane.lastStatus ?? "working").slice(0, verbose ? 200 : 60)}` : ""; - const duration = lane.durationMs !== undefined ? ` · ${fmtDuration(lane.durationMs)}` : ""; - return `${lane.lane}: ${lane.state} · ${lane.model}:${lane.effort} · tok ${lane.tokensIn}/${lane.tokensOut} · $${lane.cost.toFixed(4)}${duration}${failure}${activity}${answer ? ` · ${answer}` : ""}`; - } - - function summarizeRun(snapshot: RunSnapshotDto, laneFilter?: string): { text: string; lanes: LaneSnapshotDto[] } { - if (laneFilter) { - const lane = snapshot.lanes[0]!; - return { - text: `run ${snapshot.run} (${snapshot.state})\n${summarizeLane(lane, true)}`, - lanes: [lane], - }; - } - const header = `run ${snapshot.run} (${snapshot.state}) · ${fmtDuration(snapshot.durationMs)} · $${snapshot.totals.cost.toFixed(4)} · tok ${snapshot.totals.tokensIn}/${snapshot.totals.tokensOut}`; - return { text: [header, ...snapshot.lanes.map((lane) => summarizeLane(lane, snapshot.state === "ended"))].join("\n"), lanes: snapshot.lanes }; - } - - pi.registerTool({ - name: "lanes_status", - label: "Lane status", - description: - "Report live status of the current lane run without blocking: per-lane state, current activity, tokens, cost, and (for settled lanes) answers. Pass `lane` for a detailed view of one lane — use this when the user asks about a specific lane. Do not call in a polling loop.", - parameters: Type.Object({ - lane: Type.Optional(Type.String()), - }), - async execute(_toolCallId, params) { - try { - if (!lastRun) { - return { content: [{ type: "text" as const, text: "No lane run has been spawned this session." }], details: undefined }; - } - if (params.lane && !lastRun.snapshot.lanes.some((lane) => lane.lane === params.lane)) { - const known = lastRun.snapshot.lanes.map((lane) => lane.lane).join(", "); - return { - content: [{ type: "text" as const, text: `Unknown lane "${params.lane}". Lanes in run ${lastRun.id}: ${known}` }], - details: undefined, - isError: true, - }; - } - const snapshot = coordinator.status(params.lane); - if (!params.lane) updateRun(lastRun, snapshot); - else lastRun.ended = snapshot.state === "ended"; - const { text } = summarizeRun(snapshot, params.lane); - return { content: [{ type: "text" as const, text }], details: undefined }; - } catch (error) { - return toolFailure("lanes_status", error); - } - }, - }); - - pi.registerTool({ - name: "lanes_wait", - label: "Collect lane results", - description: - "Return final results only after the current lane run has settled. This tool never blocks an active parent session: while lanes are running it returns immediately so the model can finish the turn and the user can keep messaging.", - parameters: Type.Object({}), - async execute() { - try { - if (!lastRun) { - return { content: [{ type: "text" as const, text: "No lane run has been spawned this session." }], details: undefined }; - } - const run = lastRun; - // Always refresh via synchronous status(): non-blocking for the parent turn, - // and does not race monitorSettlement for settled runs. - const snapshot = coordinator.status(); - updateRun(run, snapshot); - if (!run.ended) { - return { - content: [ - { - type: "text" as const, - text: `Run ${run.id} keeps running in the background. Finish the parent turn now; use lanes_status later if the user asks for progress.`, - }, - ], - details: undefined, - }; - } - const { text, lanes } = summarizeRun(snapshot); - const ok = lanes.every((lane) => lane.state === "done"); - return { content: [{ type: "text" as const, text }], details: lanes, isError: !ok }; - } catch (error) { - return toolFailure("lanes_wait", error); - } - }, - }); - - pi.registerTool({ - name: "lanes_abandon", - label: "Abandon lanes", - description: - "Explicitly stop lanes of the current run. Pass `lane` to stop one lane, omit it to stop all. This is the only way to stop lanes — they are not cancelled by ESC or turn cancellation.", - parameters: Type.Object({ - lane: Type.Optional(Type.String()), - reason: Type.Optional(Type.String()), - }), - async execute(_toolCallId, params, _signal, _onUpdate, ctx) { - try { - if (!lastRun) { - return { content: [{ type: "text" as const, text: "No lane run has been spawned this session." }], details: undefined }; - } - if (params.lane && !lastRun.snapshot.lanes.some((lane) => lane.lane === params.lane)) { - const known = lastRun.snapshot.lanes.map((lane) => lane.lane).join(", "); - return { - content: [{ type: "text" as const, text: `Unknown lane "${params.lane}". Lanes: ${known}` }], - details: undefined, - isError: true, - }; - } - const reason = params.reason?.trim() || "abandoned by model"; - updateRun(lastRun, coordinator.abandon({ ...(params.lane ? { lane: params.lane } : {}), reason })); - renderWidget(ctx, lastRun); - const { text } = summarizeRun(lastRun.snapshot); - return { content: [{ type: "text" as const, text }], details: undefined }; - } catch (error) { - return toolFailure("lanes_abandon", error); - } - }, - }); - - pi.registerCommand("lanes", { - description: "Show, hide, summarize, abandon lanes, or open the full-screen TUI", - handler: async (args, ctx) => { - try { - const [command = "show", lane] = (args.trim() || "show").split(/\s+/, 2); - if (command === "tui") { - if (!ctx.hasUI) { - safeNotify(ctx, "lanes tui requires interactive mode", "warning"); - return; - } - await ctx.ui.custom( - (tui, theme, _keybindings, done) => new LanesTuiComponent(tui, theme, () => done(undefined), lastRun?.id), - { - overlay: true, - overlayOptions: { width: "100%", maxHeight: "100%", anchor: "top-left", row: 0, col: 0 }, - }, - ); - return; - } - if (command === "hide") { - if (lastRun) lastRun.hidden = true; - clearWidget(ctx); - return; - } - if (!lastRun) { - safeNotify(ctx, "No lane run is available", "info"); - return; - } - if (command === "abandon") { - if (!lane) { - safeNotify(ctx, "Usage: /lanes abandon ", "warning"); - return; - } - if (lane !== "all" && !lastRun.snapshot.lanes.some((item) => item.lane === lane)) return; - updateRun( - lastRun, - coordinator.abandon(lane === "all" ? { reason: "user" } : { lane, reason: "user" }), - ); - renderWidget(ctx, lastRun); - return; - } - if (command === "last") { - refreshRun(lastRun); - const lanes = lastRun.snapshot.lanes; - const running = lanes.filter((item) => item.state === "running").length; - safeNotify( - ctx, - `run ${lastRun.id} · ${running} running · ${lanes.length} lanes · $${lastRun.snapshot.totals.cost.toFixed(4)} · ${lastRun.snapshot.totals.tokensIn}/${lastRun.snapshot.totals.tokensOut} tokens`, - "info", - ); - return; - } - if (command === "show") { - lastRun.hidden = false; - renderLiveWidget(ctx, lastRun); - return; - } - safeNotify(ctx, "Usage: /lanes [show|hide|last|tui|abandon ]", "warning"); - } catch (error) { - safeNotify(ctx, `lanes command failed: ${errorMessage(error)}`, "error"); - } - }, - }); -} diff --git a/packages/pi-kit/extensions/table.ts b/packages/pi-kit/extensions/table.ts deleted file mode 100644 index 2f6b10d..0000000 --- a/packages/pi-kit/extensions/table.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; - -import { BANNED, EFFORT_PINS } from "../src/table.ts"; - -export default function tablePolicy(pi: ExtensionAPI) { - pi.on("model_select", async (event, ctx) => { - try { - const selected = `${event.model.provider}/${event.model.id}`; - if (BANNED.some((pattern) => pattern.test(selected))) { - const previous = event.previousModel; - if (previous && !BANNED.some((pattern) => pattern.test(`${previous.provider}/${previous.id}`))) { - await pi.setModel(previous); - ctx.ui.notify( - `pi-kit: ${selected} is banned; reverted to ${previous.provider}/${previous.id}`, - "warning", - ); - } else { - ctx.ui.notify( - `pi-kit: ${selected} is banned (Haiku/Luna/Terra). Pick another model.`, - "error", - ); - } - return; - } - - const pin = EFFORT_PINS.find(({ match }) => match.test(selected)); - if (pin && pi.getThinkingLevel() !== pin.level) { - pi.setThinkingLevel(pin.level); - ctx.ui.notify(`pi-kit: ${selected} pinned to ${pin.level} effort`, "info"); - } - } catch { - // Policy enforcement must never break the session; the shared AGENTS rules still apply. - } - }); - - pi.on("session_start", (_event, ctx) => { - if (!ctx.model) return; - const selected = `${ctx.model.provider}/${ctx.model.id}`; - if (BANNED.some((pattern) => pattern.test(selected))) { - ctx.ui.notify(`pi-kit: session started on banned model ${selected} — switch models`, "error"); - } - }); -} diff --git a/packages/pi-kit/extensions/vault.ts b/packages/pi-kit/extensions/vault.ts deleted file mode 100644 index ca7a27f..0000000 --- a/packages/pi-kit/extensions/vault.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * pi-kit /promote — append a pending claim to the curated-memory review queue. - * Never overwrites; appends only, creating the file (with header) on first use. - */ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { dirname } from "node:path"; -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; - -const REVIEW_QUEUE_HEADER = "# Review queue\n"; -const REVIEW_QUEUE_FILENAME = "REVIEW_QUEUE.md"; -const VAULT_DIR_ENV = "PIKIT_VAULT_DIR"; -const DEFAULT_VAULT_DIR = "/home/dev/AgentMemory"; - -/** Pure append — creates the file with a header when missing, otherwise appends. Returns the written path. */ -export function appendPromotion(dir: string, text: string, source: string): string { - const path = `${dir}/${REVIEW_QUEUE_FILENAME}`; - mkdirSync(dirname(path), { recursive: true }); - - const block = `\n## ${new Date().toISOString()} pending\n- claim: ${text}\n- source: ${source}\n- status: pending review\n`; - if (!existsSync(path)) { - writeFileSync(path, REVIEW_QUEUE_HEADER + block, "utf8"); - return path; - } - const existing = readFileSync(path, "utf8"); - writeFileSync(path, existing + block, "utf8"); - return path; -} - -export default function (pi: ExtensionAPI) { - pi.registerCommand("promote", { - description: "Promote a claim to the curated-memory review queue", - handler: async (args, ctx) => { - const text = args.trim(); - if (!text) { - ctx.ui.notify("usage: /promote ", "error"); - return; - } - - try { - const dir = process.env[VAULT_DIR_ENV] ?? DEFAULT_VAULT_DIR; - const sessionId = ctx.sessionManager.getSessionFile(); - const source = sessionId ? `pi session ${sessionId}` : `pi session (cwd: ${ctx.cwd})`; - - const path = appendPromotion(dir, text, source); - ctx.ui.notify(`promoted to ${path}`, "info"); - } catch (error) { - ctx.ui.notify(`promote failed: ${error instanceof Error ? error.message : String(error)}`, "error"); - } - }, - }); -} diff --git a/packages/pi-kit/mcp/create-server.ts b/packages/pi-kit/mcp/create-server.ts deleted file mode 100644 index 3a56c76..0000000 --- a/packages/pi-kit/mcp/create-server.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { z } from "zod/v4"; -import { - LaneCoordinator, - type RunSnapshotDto, -} from "../src/lane-coordinator.ts"; -import type { LaneSpec } from "../src/schema.ts"; - -export interface LaneMcpCoordinator { - spawn(specs: LaneSpec[]): Promise; - status(lane?: string): RunSnapshotDto; - wait(signal?: AbortSignal): Promise; - abandon(input: { lane?: string; reason?: string }): RunSnapshotDto; - shutdown(reason: string): Promise; -} - -export interface CreateLaneMcpServerOptions { - coordinator?: LaneMcpCoordinator; -} - -const effortSchema = z.enum(["off", "minimal", "low", "medium", "high", "xhigh"]); -const modeSchema = z.enum(["read-only", "workspace-write"]); -const stateSchema = z.enum(["queued", "running", "done", "failed", "abandoned"]); -const runStateSchema = z.enum(["active", "ended"]); - -const laneSnapshotSchema = z.object({ - lane: z.string(), - model: z.string(), - effort: effortSchema, - mode: modeSchema, - state: stateSchema, - currentTool: z.string().optional(), - lastStatus: z.string().optional(), - tokensIn: z.number(), - tokensOut: z.number(), - cost: z.number(), - context: z.number(), - answer: z.string().optional(), - durationMs: z.number().optional(), - abandonReason: z.string().optional(), -}); - -const runSnapshotSchema = z.object({ - run: z.string(), - state: runStateSchema, - ok: z.boolean().optional(), - durationMs: z.number(), - totals: z.object({ - cost: z.number(), - tokensIn: z.number(), - tokensOut: z.number(), - }), - lanes: z.array(laneSnapshotSchema), -}); - -type WireRunSnapshot = z.infer; - -const spawnLaneSchema = z.object({ - lane: z.string().trim().min(1).optional(), - task: z.string().min(1), - model: z.string().min(1), - effort: effortSchema, - mode: modeSchema, - cwd: z.string().optional(), - rationale: z.string().optional(), -}); - -function finite(value: number): number { - return Number.isFinite(value) ? value : 0; -} - -function wireSnapshot(snapshot: RunSnapshotDto): WireRunSnapshot { - return { - run: snapshot.run, - state: snapshot.state, - ...(snapshot.ok !== undefined ? { ok: snapshot.ok } : {}), - durationMs: finite(snapshot.durationMs), - totals: { - cost: finite(snapshot.totals.cost), - tokensIn: finite(snapshot.totals.tokensIn), - tokensOut: finite(snapshot.totals.tokensOut), - }, - lanes: snapshot.lanes.map((lane) => ({ - lane: lane.lane, - model: lane.model, - effort: lane.effort, - mode: lane.mode, - state: lane.state, - ...(lane.currentTool !== undefined ? { currentTool: lane.currentTool } : {}), - ...(lane.lastStatus !== undefined ? { lastStatus: lane.lastStatus } : {}), - tokensIn: finite(lane.tokensIn), - tokensOut: finite(lane.tokensOut), - cost: finite(lane.cost), - context: finite(lane.context), - ...(lane.answer !== undefined ? { answer: lane.answer } : {}), - ...(lane.durationMs !== undefined ? { durationMs: finite(lane.durationMs) } : {}), - ...(lane.abandonReason !== undefined ? { abandonReason: lane.abandonReason } : {}), - })), - }; -} - -function errorMessage(error: unknown): string { - const message = error instanceof Error ? error.message : String(error); - return message.replace(/\s+/g, " ").trim().slice(0, 240) || "unknown error"; -} - -function failure(tool: string, error: unknown) { - return { - content: [{ type: "text" as const, text: `${tool} failed: ${errorMessage(error)}` }], - isError: true, - }; -} - -function success(snapshot: RunSnapshotDto) { - const dto = wireSnapshot(snapshot); - const text = `run ${dto.run} (${dto.state}) · ${dto.lanes.length} lane${dto.lanes.length === 1 ? "" : "s"} · $${dto.totals.cost.toFixed(4)}`; - return { - content: [{ type: "text" as const, text }], - structuredContent: dto, - }; -} - -export function createLaneMcpServer(options: CreateLaneMcpServerOptions = {}) { - const coordinator = options.coordinator ?? new LaneCoordinator({ origin: "mcp" }); - const server = new McpServer({ name: "pickforge-lanes", version: "0.1.0" }); - - server.registerTool( - "lanes_spawn", - { - description: "Spawn one cross-provider lane run without blocking.", - inputSchema: z.object({ lanes: z.array(spawnLaneSchema).min(1) }), - outputSchema: runSnapshotSchema, - }, - async ({ lanes }) => { - try { - const specs: LaneSpec[] = lanes.map((lane, index) => ({ - lane: lane.lane || `lane-${index + 1}`, - task: lane.task, - model: lane.model, - effort: lane.effort, - mode: lane.mode, - ...(lane.cwd !== undefined ? { cwd: lane.cwd } : {}), - ...(lane.rationale !== undefined ? { rationale: lane.rationale } : {}), - })); - return success(await coordinator.spawn(specs)); - } catch (error) { - return failure("lanes_spawn", error); - } - }, - ); - - server.registerTool( - "lanes_status", - { - description: "Return the current run or one named lane without blocking.", - inputSchema: z.object({ lane: z.string().min(1).optional() }), - outputSchema: runSnapshotSchema, - }, - async ({ lane }) => { - try { - return success(coordinator.status(lane)); - } catch (error) { - return failure("lanes_status", error); - } - }, - ); - - server.registerTool( - "lanes_wait", - { - description: "Wait for the current run; cancellation detaches only this wait.", - inputSchema: z.object({}), - outputSchema: runSnapshotSchema, - }, - async (_input, extra) => { - try { - return success(await coordinator.wait(extra.signal)); - } catch (error) { - return failure("lanes_wait", error); - } - }, - ); - - server.registerTool( - "lanes_abandon", - { - description: "Abandon one named lane or every active lane in the current run.", - inputSchema: z.object({ - lane: z.string().min(1).optional(), - reason: z.string().optional(), - }), - outputSchema: runSnapshotSchema, - }, - async ({ lane, reason }) => { - try { - return success(coordinator.abandon({ - ...(lane !== undefined ? { lane } : {}), - ...(reason !== undefined ? { reason } : {}), - })); - } catch (error) { - return failure("lanes_abandon", error); - } - }, - ); - - return { server, coordinator }; -} diff --git a/packages/pi-kit/mcp/server.ts b/packages/pi-kit/mcp/server.ts deleted file mode 100755 index 6195544..0000000 --- a/packages/pi-kit/mcp/server.ts +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env bun -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createLaneMcpServer } from "./create-server.ts"; - -function boundedError(error: unknown): string { - const message = error instanceof Error ? error.message : String(error); - return message.replace(/\s+/g, " ").trim().slice(0, 500) || "unknown error"; -} - -async function main(): Promise { - const { server, coordinator } = createLaneMcpServer(); - const transport = new StdioServerTransport(); - let shutdownPromise: Promise | undefined; - let reported = false; - - const shutdown = (reason: string): Promise => { - if (!shutdownPromise) { - shutdownPromise = (async () => { - let firstError: unknown; - try { - await coordinator.shutdown(reason); - } catch (error) { - firstError = error; - } - try { - await server.close(); - } catch (error) { - firstError ??= error; - } - if (firstError !== undefined) throw firstError; - })(); - } - return shutdownPromise; - }; - - const requestShutdown = (reason: string): void => { - void shutdown(reason).catch((error) => { - if (!reported) { - reported = true; - console.error(`pickforge-lanes-mcp: ${boundedError(error)}`); - } - process.exitCode = 1; - }); - }; - - process.once("SIGINT", () => requestShutdown("received SIGINT")); - process.once("SIGTERM", () => requestShutdown("received SIGTERM")); - process.stdin.once("end", () => requestShutdown("stdin ended")); - process.stdin.once("close", () => requestShutdown("stdin closed")); - server.server.onclose = () => requestShutdown("transport closed"); - - try { - await server.connect(transport); - } catch (error) { - await shutdown("transport startup failed"); - throw error; - } -} - -void main().catch((error) => { - console.error(`pickforge-lanes-mcp: ${boundedError(error)}`); - process.exitCode = 1; -}); diff --git a/packages/pi-kit/package.json b/packages/pi-kit/package.json deleted file mode 100644 index 1183bec..0000000 --- a/packages/pi-kit/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@pickforge/pi-kit", - "version": "0.1.0", - "description": "Personal Pi sovereignty kit: lane runner + TUI, model policy, event journal, delivery gates, vault promotion.", - "type": "module", - "private": true, - "bin": { - "pickforge-lanes-mcp": "./mcp/server.ts" - }, - "pi": { - "extensions": [ - "extensions/table.ts", - "extensions/journal.ts", - "extensions/lanes.ts", - "extensions/gates.ts", - "extensions/delegation.ts", - "extensions/grill-review.ts", - "extensions/vault.ts", - "extensions/compact-tools.ts", - "extensions/ember-footer.ts" - ], - "themes": [ - "themes" - ] - }, - "scripts": { - "test": "vitest run", - "typecheck": "tsc --noEmit", - "mcp": "bun mcp/server.ts" - }, - "devDependencies": { - "@earendil-works/pi-coding-agent": "0.81.0", - "typescript": "^5.6.0", - "vitest": "^2.0.0", - "typebox": "1.1.38", - "@earendil-works/pi-ai": "0.81.0", - "@earendil-works/pi-tui": "0.81.0" - }, - "dependencies": { - "@modelcontextprotocol/sdk": "1.29.0", - "zod": "^4.4.3" - } -} \ No newline at end of file diff --git a/packages/pi-kit/src/adapters/claude-code.ts b/packages/pi-kit/src/adapters/claude-code.ts deleted file mode 100644 index d61e0a8..0000000 --- a/packages/pi-kit/src/adapters/claude-code.ts +++ /dev/null @@ -1,545 +0,0 @@ -import { spawn } from "node:child_process"; -import { - accessSync, - closeSync, - constants, - openSync, - readSync, - realpathSync, - statSync, -} from "node:fs"; -import { delimiter, isAbsolute, join, resolve } from "node:path"; -import { createChildEnvironment } from "../child-env.ts"; -import type { - LaneEventParser, - LaneExecutionAdapter, - LaneProcessPlan, - NormalizedLaneEvent, -} from "../execution-adapter.ts"; -import type { LaneSpec } from "../schema.ts"; -import { findModel } from "../table.ts"; - -export const CLAUDE_CODE_MIN_VERSION = "2.1.216"; - -const WRAPPER_PREFIX_BYTES = 64 * 1024; -const VERSION_OUTPUT_BYTES = 16 * 1024; -const VERSION_TIMEOUT_MS = 5_000; -const DIAGNOSTIC_LIMIT = 240; - -const READ_ONLY_TOOLS = ["Read", "Glob", "Grep"] as const; -const WORKSPACE_TOOLS = ["Read", "Glob", "Grep", "Edit", "Write", "Bash"] as const; - -interface JsonRecord { - [key: string]: unknown; -} - -interface UsageSnapshot { - input: number; - output: number; - cacheRead: number; - cacheCreation: number; - context: number; -} - -interface PendingTool { - id: unknown; - name: string; - input: unknown; - partialJson: string; -} - -export interface ClaudeCodeAdapterOptions { - binary?: string; - env?: NodeJS.ProcessEnv; -} - -function isRecord(value: unknown): value is JsonRecord { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function finiteNumber(record: JsonRecord, ...keys: string[]): number { - for (const key of keys) { - const value = record[key]; - if (typeof value === "number" && Number.isFinite(value) && value >= 0) return value; - } - return 0; -} - -function readUsage(value: unknown): UsageSnapshot | undefined { - if (!isRecord(value)) return undefined; - const input = finiteNumber(value, "input_tokens", "inputTokens", "input"); - const output = finiteNumber(value, "output_tokens", "outputTokens", "output"); - const cacheRead = finiteNumber(value, "cache_read_input_tokens", "cacheReadInputTokens", "cacheRead"); - const cacheCreation = finiteNumber( - value, - "cache_creation_input_tokens", - "cacheCreationInputTokens", - "cacheCreation", - ); - if (input === 0 && output === 0 && cacheRead === 0 && cacheCreation === 0) return undefined; - const reportedContext = finiteNumber(value, "context_tokens", "contextTokens", "context"); - return { - input, - output, - cacheRead, - cacheCreation, - context: reportedContext || input + output + cacheRead + cacheCreation, - }; -} - -function compareVersions(left: string, right: string): number { - const leftParts = left.split(".").map(Number); - const rightParts = right.split(".").map(Number); - for (let index = 0; index < 3; index++) { - const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0); - if (difference !== 0) return difference; - } - return 0; -} - -function parseVersion(output: string): string | undefined { - return output.match(/\b(\d+\.\d+\.\d+)\b/)?.[1]; -} - -function executableCandidates(binary: string, env: NodeJS.ProcessEnv): string[] { - if (isAbsolute(binary) || binary.includes("/") || binary.includes("\\")) { - return [isAbsolute(binary) ? binary : resolve(binary)]; - } - return (env.PATH ?? "") - .split(delimiter) - .filter(Boolean) - .map((directory) => join(directory, binary)); -} - -function wrapperHasPermissionBypass(path: string): boolean { - let descriptor: number | undefined; - try { - descriptor = openSync(path, "r"); - const bytes = Buffer.allocUnsafe(WRAPPER_PREFIX_BYTES); - const count = readSync(descriptor, bytes, 0, bytes.length, 0); - const prefix = bytes.subarray(0, count).toString("utf8"); - if (!prefix.startsWith("#!")) return false; - return ( - prefix.includes("--dangerously-skip-permissions") || - /--permission-mode(?:=|\s+)bypassPermissions/.test(prefix) - ); - } catch { - return true; - } finally { - if (descriptor !== undefined) closeSync(descriptor); - } -} - -function versionOf(command: string, env: NodeJS.ProcessEnv): Promise { - return new Promise((resolveVersion) => { - const child = spawn(command, ["--version"], { - env, - stdio: ["ignore", "pipe", "pipe"], - detached: true, - }); - let output = ""; - let outputBytes = 0; - let invalid = false; - let settled = false; - - const settle = (version: string | undefined) => { - if (settled) return; - settled = true; - resolveVersion(version); - }; - const kill = () => { - invalid = true; - try { - if (child.pid) process.kill(-child.pid, "SIGKILL"); - else child.kill("SIGKILL"); - } catch { - try { - child.kill("SIGKILL"); - } catch {} - } - }; - const capture = (chunk: Buffer) => { - outputBytes += chunk.length; - if (outputBytes > VERSION_OUTPUT_BYTES) { - kill(); - return; - } - output += chunk.toString("utf8"); - }; - - child.stdout.on("data", capture); - child.stderr.on("data", capture); - const timeout = setTimeout(kill, VERSION_TIMEOUT_MS); - timeout.unref(); - const reapOnExit = () => kill(); - process.once("exit", reapOnExit); - child.once("error", () => { - invalid = true; - }); - child.once("close", (code) => { - clearTimeout(timeout); - process.removeListener("exit", reapOnExit); - settle(!invalid && code === 0 ? parseVersion(output) : undefined); - }); - }); -} - -function boundedDiagnostic(error: unknown): string { - const detail = error instanceof Error ? error.message : String(error); - return `Malformed Claude stream JSON: ${detail}`.replace(/[\r\n\t]+/g, " ").slice(0, DIAGNOSTIC_LIMIT); -} - -function isErrorResult(record: JsonRecord): boolean { - return record.is_error === true || (typeof record.subtype === "string" && /^error(?:_|$)/.test(record.subtype)); -} - -function boundedResultError(record: JsonRecord): string { - const subtype = typeof record.subtype === "string" ? record.subtype : "error"; - const detail = typeof record.result === "string" && record.result.trim() ? `: ${record.result}` : ""; - return `Claude Code ${subtype}${detail}`.replace(/[\r\n\t]+/g, " ").slice(0, DIAGNOSTIC_LIMIT); -} - -function contentText(value: unknown): string { - if (typeof value === "string") return value; - if (!Array.isArray(value)) return ""; - let text = ""; - for (const part of value) { - if (!isRecord(part)) continue; - if (typeof part.text === "string") text += part.text; - else if (typeof part.content === "string") text += part.content; - } - return text; -} - -class ClaudeCodeStreamParser implements LaneEventParser { - private readonly task: string; - private readonly startedTools = new Set(); - private readonly toolNames = new Map(); - private readonly pendingTools = new Map(); - private taskEmitted = false; - private ended = false; - private resultReceived = false; - private currentText = ""; - private currentThinking = ""; - private lastAssistantText = ""; - private usage: UsageSnapshot = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, context: 0 }; - private lastEmittedUsage = ""; - - constructor(spec: LaneSpec) { - this.task = spec.task; - } - - feedLine(line: string): NormalizedLaneEvent[] { - if (this.ended) return []; - const events = this.emitTask(); - if (!line.trim()) return events; - let parsed: unknown; - try { - parsed = JSON.parse(line); - } catch (error) { - events.push({ - v: 1, - type: "tool_end", - tool: "claude-code-parser", - text: boundedDiagnostic(error), - isError: true, - }); - return events; - } - if (isRecord(parsed)) events.push(...this.handleRecord(parsed)); - return events; - } - - end(): NormalizedLaneEvent[] { - if (this.ended) return []; - this.ended = true; - const events = this.emitTask(); - if (!this.resultReceived && this.lastAssistantText) { - events.push({ v: 1, type: "assistant_end", text: this.lastAssistantText }); - } - return events; - } - - private emitTask(): NormalizedLaneEvent[] { - if (this.taskEmitted) return []; - this.taskEmitted = true; - return [{ v: 1, type: "task", text: this.task }]; - } - - private handleRecord(record: JsonRecord): NormalizedLaneEvent[] { - if (record.type === "stream_event" && isRecord(record.event)) return this.handleStreamEvent(record.event); - if (record.type === "assistant" && isRecord(record.message)) return this.handleAssistant(record.message); - if (record.type === "user" && isRecord(record.message)) return this.handleUser(record.message); - if (record.type === "result") return this.handleResult(record); - return []; - } - - private handleStreamEvent(event: JsonRecord): NormalizedLaneEvent[] { - if (event.type === "message_start") { - this.currentText = ""; - this.currentThinking = ""; - return []; - } - if (event.type === "content_block_start" && isRecord(event.content_block)) { - const block = event.content_block; - if (block.type === "tool_use" && typeof block.name === "string" && typeof event.index === "number") { - this.pendingTools.set(event.index, { - id: block.id, - name: block.name, - input: block.input, - partialJson: "", - }); - } - return []; - } - if (event.type === "content_block_stop" && typeof event.index === "number") { - const pending = this.pendingTools.get(event.index); - if (!pending) return []; - this.pendingTools.delete(event.index); - let input = pending.input; - if (pending.partialJson) { - try { - input = JSON.parse(pending.partialJson); - } catch { - input = pending.input; - } - } - return this.startTool(pending.id, pending.name, input); - } - if (event.type !== "content_block_delta" || !isRecord(event.delta)) return []; - const delta = event.delta; - if ( - delta.type === "input_json_delta" && - typeof delta.partial_json === "string" && - typeof event.index === "number" - ) { - const pending = this.pendingTools.get(event.index); - if (pending) pending.partialJson += delta.partial_json; - return []; - } - if (delta.type === "text_delta" && typeof delta.text === "string") { - this.currentText += delta.text; - this.lastAssistantText = this.currentText; - return [{ v: 1, type: "text_delta", delta: delta.text }]; - } - if (delta.type === "thinking_delta" && typeof delta.thinking === "string") { - this.currentThinking += delta.thinking; - return [{ v: 1, type: "thinking_delta", delta: delta.thinking }]; - } - return []; - } - - private handleAssistant(message: JsonRecord): NormalizedLaneEvent[] { - const events: NormalizedLaneEvent[] = []; - if (Array.isArray(message.content)) { - for (const part of message.content) { - if (!isRecord(part)) continue; - if (part.type === "thinking" && typeof part.thinking === "string") { - const delta = this.missingSuffix(part.thinking, this.currentThinking); - if (delta) events.push({ v: 1, type: "thinking_delta", delta }); - this.currentThinking = part.thinking; - } else if (part.type === "text" && typeof part.text === "string") { - const delta = this.missingSuffix(part.text, this.currentText); - if (delta) events.push({ v: 1, type: "text_delta", delta }); - this.currentText = part.text; - this.lastAssistantText = part.text; - } else if (part.type === "tool_use" && typeof part.name === "string") { - events.push(...this.startTool(part.id, part.name, part.input)); - } - } - } - const snapshot = readUsage(message.usage); - if (snapshot) { - this.usage = { - input: this.usage.input + snapshot.input, - output: this.usage.output + snapshot.output, - cacheRead: this.usage.cacheRead + snapshot.cacheRead, - cacheCreation: this.usage.cacheCreation + snapshot.cacheCreation, - context: this.usage.context + snapshot.context, - }; - events.push(...this.emitUsage(this.usage)); - } - return events; - } - - private handleUser(message: JsonRecord): NormalizedLaneEvent[] { - if (!Array.isArray(message.content)) return []; - const events: NormalizedLaneEvent[] = []; - for (const part of message.content) { - if (!isRecord(part) || part.type !== "tool_result") continue; - const id = typeof part.tool_use_id === "string" ? part.tool_use_id : ""; - events.push({ - v: 1, - type: "tool_end", - tool: this.toolNames.get(id) ?? (id || "unknown"), - text: contentText(part.content), - isError: part.is_error === true, - }); - } - return events; - } - - private handleResult(record: JsonRecord): NormalizedLaneEvent[] { - if (this.resultReceived) return []; - this.resultReceived = true; - const events: NormalizedLaneEvent[] = []; - const snapshot = readUsage(record.usage); - if (snapshot) { - this.usage = snapshot; - events.push(...this.emitUsage(snapshot)); - } - if (isErrorResult(record)) { - const text = boundedResultError(record); - this.lastAssistantText = text; - events.push({ v: 1, type: "assistant_end", text, isError: true }); - return events; - } - const text = typeof record.result === "string" ? record.result : this.lastAssistantText; - this.lastAssistantText = text; - events.push({ v: 1, type: "assistant_end", text }); - return events; - } - - private startTool(idValue: unknown, name: string, input: unknown): NormalizedLaneEvent[] { - const id = typeof idValue === "string" ? idValue : `${name}:${this.startedTools.size}`; - this.toolNames.set(id, name); - if (this.startedTools.has(id)) return []; - this.startedTools.add(id); - return [{ v: 1, type: "tool_start", tool: name, input: input ?? {} }]; - } - - private emitUsage(snapshot: UsageSnapshot): NormalizedLaneEvent[] { - const key = `${snapshot.input}:${snapshot.output}:${snapshot.cacheRead}:${snapshot.context}`; - if (key === this.lastEmittedUsage) return []; - this.lastEmittedUsage = key; - return [ - { - v: 1, - type: "usage", - input: snapshot.input, - output: snapshot.output, - cacheRead: snapshot.cacheRead, - context: snapshot.context, - }, - ]; - } - - private missingSuffix(complete: string, partial: string): string { - if (!partial) return complete; - return complete.startsWith(partial) ? complete.slice(partial.length) : ""; - } -} - -export class ClaudeCodeExecutionAdapter implements LaneExecutionAdapter { - readonly route = "claude-code" as const; - private readonly binary: string; - private readonly env: NodeJS.ProcessEnv; - private command?: string; - private candidates?: string[]; - private preparation?: Promise; - - constructor(options: ClaudeCodeAdapterOptions = {}) { - this.binary = options.binary ?? "claude"; - this.env = createChildEnvironment(options.env ?? process.env); - } - - preflight(spec: LaneSpec): void { - const row = findModel(spec.model); - if (!row || row.route !== this.route) throw new Error(`Model ${spec.model} is not routed through Claude Code`); - if (!row.allowedEfforts.includes(spec.effort)) { - throw new Error(`Claude Code model ${spec.model} has unsupported effort ${spec.effort}`); - } - if (this.command || this.candidates) return; - - const candidates: string[] = []; - const seen = new Set(); - for (const candidate of executableCandidates(this.binary, this.env)) { - try { - accessSync(candidate, constants.X_OK); - const resolved = realpathSync(candidate); - if (seen.has(resolved)) continue; - seen.add(resolved); - if (!statSync(resolved).isFile()) continue; - accessSync(resolved, constants.X_OK); - if (!wrapperHasPermissionBypass(resolved)) candidates.push(resolved); - } catch { - // Try the next PATH candidate without exposing wrapper contents or environment values. - } - } - if (candidates.length === 0) throw new Error("No safe Claude Code executable was found on PATH"); - this.candidates = candidates; - } - - async prepare(spec: LaneSpec): Promise { - this.preflight(spec); - if (this.command) return; - this.preparation ??= this.prepareExecutable(); - await this.preparation; - } - - private async prepareExecutable(): Promise { - let unsupportedVersion: string | undefined; - for (const candidate of this.candidates ?? []) { - const version = await versionOf(candidate, this.env); - if (!version) continue; - if (compareVersions(version, CLAUDE_CODE_MIN_VERSION) < 0) { - unsupportedVersion = version; - continue; - } - this.command = candidate; - return; - } - if (unsupportedVersion) { - throw new Error( - `Claude Code ${unsupportedVersion} is unsupported; this adapter requires ${CLAUDE_CODE_MIN_VERSION} or newer`, - ); - } - throw new Error("No supported Claude Code executable was found on PATH"); - } - - build(spec: LaneSpec): LaneProcessPlan { - this.preflight(spec); - if (!this.command) throw new Error("Claude Code adapter preparation has not completed"); - const row = findModel(spec.model)!; - const mode = spec.mode ?? "workspace-write"; - const args = [ - "-p", - "--safe-mode", - "--disable-slash-commands", - "--no-chrome", - "--no-session-persistence", - "--permission-mode", - "dontAsk", - "--output-format", - "stream-json", - "--verbose", - "--include-partial-messages", - "--model", - row.runtimeModel, - "--effort", - spec.effort, - ]; - if (mode === "read-only") { - args.push("--tools", READ_ONLY_TOOLS.join(","), "--allowedTools", ...READ_ONLY_TOOLS); - } else if (mode === "workspace-write") { - args.push("--tools", WORKSPACE_TOOLS.join(","), "--allowedTools", ...WORKSPACE_TOOLS); - } else { - throw new Error(`Unsupported Claude Code lane mode: ${String(mode)}`); - } - args.push("--", spec.task); - return { - command: this.command!, - args, - cwd: spec.cwd ?? process.cwd(), - env: this.env, - }; - } - - createParser(spec: LaneSpec): LaneEventParser { - return new ClaudeCodeStreamParser(spec); - } -} - -export function createClaudeCodeAdapter(options: ClaudeCodeAdapterOptions = {}): ClaudeCodeExecutionAdapter { - return new ClaudeCodeExecutionAdapter(options); -} diff --git a/packages/pi-kit/src/adapters/pi.ts b/packages/pi-kit/src/adapters/pi.ts deleted file mode 100644 index b076763..0000000 --- a/packages/pi-kit/src/adapters/pi.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { createChildEnvironment } from "../child-env.ts"; -import type { - LaneEventParser, - LaneExecutionAdapter, - LaneProcessPlan, - NormalizedLaneEvent, -} from "../execution-adapter.ts"; -import type { LaneSpec } from "../schema.ts"; -import { findModel } from "../table.ts"; - -const READ_ONLY_TOOLS = "read,grep,find,ls"; - -const PI_PROVIDERS: Record = { - "openai-codex/gpt-5.6-sol": "openai-codex", - "xai/grok-4.5": "xai", - "ollama/glm-5.2:cloud": "ollama", -}; - -type JsonRecord = Record; - -function isRecord(value: unknown): value is JsonRecord { - return typeof value === "object" && value !== null; -} - -function numberFrom(record: JsonRecord, ...keys: string[]): number { - for (const key of keys) { - const value = record[key]; - if (typeof value === "number" && Number.isFinite(value)) return value; - } - return 0; -} - -function contentText(content: unknown): string { - if (!Array.isArray(content)) return ""; - let text = ""; - for (const part of content) { - if (isRecord(part) && part.type === "text" && typeof part.text === "string") text += part.text; - } - return text; -} - -function assistantText(message: unknown): string | undefined { - if (!isRecord(message) || message.role !== "assistant") return undefined; - return contentText(message.content) || undefined; -} - -class PiEventParser implements LaneEventParser { - private taskEmitted = false; - private input = 0; - private output = 0; - private cacheRead = 0; - - constructor(private readonly spec: LaneSpec) {} - - feedLine(line: string): NormalizedLaneEvent[] { - const events = this.start(); - if (!line.trim()) return events; - - let event: unknown; - try { - event = JSON.parse(line); - } catch { - return events; - } - if (!isRecord(event) || typeof event.type !== "string") return events; - - if (event.type === "message_update" && isRecord(event.assistantMessageEvent)) { - const update = event.assistantMessageEvent; - if (update.type === "text_delta" && typeof update.delta === "string") { - events.push({ v: 1, type: "text_delta", delta: update.delta }); - } else if (update.type === "thinking_delta" && typeof update.delta === "string") { - events.push({ v: 1, type: "thinking_delta", delta: update.delta }); - } - } else if (event.type === "tool_execution_start" && typeof event.toolName === "string") { - events.push({ v: 1, type: "tool_start", tool: event.toolName, input: event.args }); - } else if (event.type === "tool_execution_end") { - const tool = typeof event.toolName === "string" ? event.toolName : "tool"; - const text = isRecord(event.result) ? contentText(event.result.content) : ""; - events.push({ v: 1, type: "tool_end", tool, text, isError: event.isError === true }); - } else if (event.type === "message_end" && isRecord(event.message) && event.message.role === "assistant") { - if (isRecord(event.message.usage)) { - const usage = event.message.usage; - this.input += numberFrom(usage, "input", "inputTokens", "input_tokens"); - this.output += numberFrom(usage, "output", "outputTokens", "output_tokens"); - this.cacheRead += numberFrom(usage, "cacheRead", "cacheReadTokens", "cache_read"); - const reportedContext = numberFrom(usage, "totalTokens", "total_tokens", "total", "contextTokens"); - events.push({ - v: 1, - type: "usage", - input: this.input, - output: this.output, - cacheRead: this.cacheRead, - context: reportedContext || this.input + this.output, - }); - } - } else if (event.type === "agent_end" && Array.isArray(event.messages)) { - for (let index = event.messages.length - 1; index >= 0; index--) { - const text = assistantText(event.messages[index]); - if (text !== undefined) { - events.push({ v: 1, type: "assistant_end", text }); - break; - } - } - } - - return events; - } - - end(): NormalizedLaneEvent[] { - return this.start(); - } - - private start(): NormalizedLaneEvent[] { - if (this.taskEmitted) return []; - this.taskEmitted = true; - return [{ v: 1, type: "task", text: this.spec.task }]; - } -} - -export class PiExecutionAdapter implements LaneExecutionAdapter { - readonly route = "pi" as const; - - constructor( - private readonly piBinary = "pi", - private readonly env: NodeJS.ProcessEnv = process.env, - ) {} - - preflight(spec: LaneSpec): void { - const row = findModel(spec.model); - if (!row || row.route !== this.route || !PI_PROVIDERS[spec.model]) { - throw new Error(`model ${spec.model} is not available through the Pi execution route`); - } - if (spec.mode !== undefined && spec.mode !== "read-only" && spec.mode !== "workspace-write") { - throw new Error(`unsupported Pi lane mode: ${String(spec.mode)}`); - } - } - - build(spec: LaneSpec): LaneProcessPlan { - this.preflight(spec); - const row = findModel(spec.model)!; - const mode = spec.mode ?? "workspace-write"; - const args = ["--mode", "json", "--no-extensions", "--no-session", "-p"]; - if (mode === "read-only") args.push("--tools", READ_ONLY_TOOLS); - args.push( - "--provider", - PI_PROVIDERS[spec.model]!, - "--model", - row.runtimeModel, - "--thinking", - spec.effort, - `Task:\n${spec.task}`, - ); - - return { - command: this.piBinary, - args, - cwd: spec.cwd ?? process.cwd(), - env: { ...createChildEnvironment(this.env), PIKIT_CHILD: "1" }, - }; - } - - createParser(spec: LaneSpec): LaneEventParser { - this.preflight(spec); - return new PiEventParser(spec); - } -} diff --git a/packages/pi-kit/src/child-env.ts b/packages/pi-kit/src/child-env.ts deleted file mode 100644 index b28bd29..0000000 --- a/packages/pi-kit/src/child-env.ts +++ /dev/null @@ -1,45 +0,0 @@ -const CHILD_ENV_KEYS = [ - "PATH", - "HOME", - "USER", - "LOGNAME", - "SHELL", - "TMPDIR", - "TMP", - "TEMP", - "LANG", - "LANGUAGE", - "LC_ALL", - "LC_CTYPE", - "LC_COLLATE", - "LC_MESSAGES", - "LC_MONETARY", - "LC_NUMERIC", - "LC_TIME", - "LC_ADDRESS", - "LC_IDENTIFICATION", - "LC_MEASUREMENT", - "LC_NAME", - "LC_PAPER", - "LC_TELEPHONE", - "TERM", - "COLORTERM", - "TERM_PROGRAM", - "TERM_PROGRAM_VERSION", - "XDG_CONFIG_HOME", - "XDG_CACHE_HOME", - "XDG_DATA_HOME", - "XDG_STATE_HOME", - "XDG_RUNTIME_DIR", - "XDG_CONFIG_DIRS", - "XDG_DATA_DIRS", -] as const; - -export function createChildEnvironment(source: NodeJS.ProcessEnv): NodeJS.ProcessEnv { - const environment: NodeJS.ProcessEnv = {}; - for (const key of CHILD_ENV_KEYS) { - const value = source[key]; - if (value !== undefined) environment[key] = value; - } - return environment; -} diff --git a/packages/pi-kit/src/delegation-core.ts b/packages/pi-kit/src/delegation-core.ts deleted file mode 100644 index b01d48b..0000000 --- a/packages/pi-kit/src/delegation-core.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** Pure delegation-ratio accounting, kept separate from extension wiring for testability. */ - -export interface DelegationCounts { - /** Orchestrator tool executions, excluding lane dispatches. */ - direct: number; - /** lanes_spawn calls. */ - dispatches: number; - /** Total lanes spawned across all dispatches. */ - lanes: number; -} - -export function emptyCounts(): DelegationCounts { - return { direct: 0, dispatches: 0, lanes: 0 }; -} - -export function recordTool(counts: DelegationCounts, toolName: string, laneCount: number): DelegationCounts { - if (toolName === "lanes_spawn") { - return { ...counts, dispatches: counts.dispatches + 1, lanes: counts.lanes + Math.max(1, laneCount) }; - } - return { ...counts, direct: counts.direct + 1 }; -} - -/** Share of tool work delegated to lanes, 0..1. */ -export function delegationRatio(counts: DelegationCounts): number { - const total = counts.direct + counts.lanes; - if (total === 0) return 0; - return counts.lanes / total; -} - -export function summarize(counts: DelegationCounts): string { - const pct = Math.round(delegationRatio(counts) * 100); - return `delegation ${pct}% · ${counts.lanes} lane${counts.lanes === 1 ? "" : "s"} / ${counts.direct} direct`; -} diff --git a/packages/pi-kit/src/execution-adapter.ts b/packages/pi-kit/src/execution-adapter.ts deleted file mode 100644 index 75be1f6..0000000 --- a/packages/pi-kit/src/execution-adapter.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { ExecutionRoute, LaneSpec } from "./schema.ts"; - -export type NormalizedLaneEvent = - | { v: 1; type: "task"; text: string } - | { v: 1; type: "text_delta"; delta: string } - | { v: 1; type: "thinking_delta"; delta: string } - | { v: 1; type: "tool_start"; tool: string; input: unknown } - | { v: 1; type: "tool_end"; tool: string; text: string; isError: boolean } - | { v: 1; type: "usage"; input: number; output: number; cacheRead: number; context: number } - | { v: 1; type: "assistant_end"; text: string; isError?: boolean }; - -export interface LaneProcessPlan { - command: string; - args: string[]; - cwd: string; - env: NodeJS.ProcessEnv; -} - -export interface LaneEventParser { - feedLine(line: string): NormalizedLaneEvent[]; - end(): NormalizedLaneEvent[]; -} - -export interface LaneExecutionAdapter { - route: ExecutionRoute; - preflight(spec: LaneSpec): void; - prepare?(spec: LaneSpec): Promise; - build(spec: LaneSpec): LaneProcessPlan; - createParser(spec: LaneSpec): LaneEventParser; -} diff --git a/packages/pi-kit/src/journal-core.ts b/packages/pi-kit/src/journal-core.ts deleted file mode 100644 index 46bf664..0000000 --- a/packages/pi-kit/src/journal-core.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { appendFileSync, mkdirSync, readdirSync, readFileSync } from "node:fs"; -import { homedir } from "node:os"; -import { join } from "node:path"; -import { randomBytes } from "node:crypto"; - -import { - DATA_DIR_ENV, - type KitEvent, - type LaneProjection, - type RunProjection, -} from "./schema.ts"; - -export type ReadRunResult = KitEvent[] & { readonly corruptLines: number }; - -export function journalDir(): string { - const dataDir = process.env[DATA_DIR_ENV] ?? join(homedir(), ".pickforge", "pi-kit"); - const runsDir = join(dataDir, "runs"); - mkdirSync(runsDir, { recursive: true }); - return runsDir; -} - -/** Directory holding raw per-lane JSONL transcripts for a run. */ -export function rawRunDir(runId: string): string { - const dataDir = process.env[DATA_DIR_ENV] ?? join(homedir(), ".pickforge", "pi-kit"); - return join(dataDir, "raw", runId); -} - -/** All journaled run ids, newest first. */ -export function listRuns(): string[] { - return readdirSync(journalDir()) - .filter((name) => name.endsWith(".jsonl")) - .map((name) => name.slice(0, -".jsonl".length)) - .sort() - .reverse(); -} - -export function appendEvent(ev: KitEvent): void { - const event = { - ...ev, - v: ev.v ?? 1, - t: ev.t ?? new Date().toISOString(), - } as KitEvent; - appendFileSync(join(journalDir(), `${event.run}.jsonl`), `${JSON.stringify(event)}\n`, "utf8"); -} - -export function readRun(runId: string): ReadRunResult { - let contents: string; - try { - contents = readFileSync(join(journalDir(), `${runId}.jsonl`), "utf8"); - } catch { - contents = ""; - } - - const events: KitEvent[] = []; - let corruptLines = 0; - for (const line of contents.split("\n")) { - if (!line.trim()) continue; - try { - events.push(JSON.parse(line) as KitEvent); - } catch { - corruptLines += 1; - } - } - Object.defineProperty(events, "corruptLines", { value: corruptLines, enumerable: false }); - return events as ReadRunResult; -} - -export function reduceRun(events: KitEvent[]): RunProjection { - const projection: RunProjection = { - run: events[0]?.run ?? "", - origin: "", - createdAt: "", - ended: false, - lanes: new Map(), - totalCost: 0, - totalTokensIn: 0, - totalTokensOut: 0, - }; - - for (const event of events) { - projection.run ||= event.run; - if (event.type === "run_created") { - projection.origin = event.origin; - projection.createdAt = event.t; - continue; - } - if (event.type === "run_end") { - projection.ended = true; - projection.ok = event.ok; - projection.durationMs = event.durationMs; - continue; - } - if (event.type === "lane_created") { - const lane = event.lane ?? event.spec.lane; - projection.lanes.set(lane, { - spec: event.spec, - state: "queued", - tokensIn: 0, - tokensOut: 0, - cost: 0, - context: 0, - }); - continue; - } - - if (!event.lane) continue; - const lane = projection.lanes.get(event.lane); - if (!lane) continue; - - switch (event.type) { - case "lane_start": - lane.state = "running"; - lane.pid = event.pid; - lane.startedAtMs ??= Date.parse(event.t); - break; - case "lane_tool": - lane.currentTool = event.tool; - break; - case "lane_status": - lane.lastStatus = event.text; - break; - case "lane_usage": - lane.tokensIn = event.input; - lane.tokensOut = event.output; - lane.cost = event.cost; - lane.context = event.context; - break; - case "lane_end": - lane.state = event.ok ? "done" : "failed"; - lane.answer = event.answer; - lane.durationMs = event.durationMs; - break; - case "lane_abandoned": - lane.state = "abandoned"; - lane.abandonReason = event.reason; - if (lane.startedAtMs !== undefined) { - lane.durationMs = Math.max(0, Date.parse(event.t) - lane.startedAtMs); - } - break; - } - } - - for (const lane of projection.lanes.values()) { - projection.totalCost += lane.cost; - projection.totalTokensIn += lane.tokensIn; - projection.totalTokensOut += lane.tokensOut; - } - return projection; -} - -export function newRunId(): string { - const compactTimestamp = new Date().toISOString().slice(0, 19).replaceAll("-", "").replaceAll(":", ""); - return `run-${compactTimestamp}-${randomBytes(2).toString("hex")}`; -} diff --git a/packages/pi-kit/src/lane-coordinator.ts b/packages/pi-kit/src/lane-coordinator.ts deleted file mode 100644 index 1725d48..0000000 --- a/packages/pi-kit/src/lane-coordinator.ts +++ /dev/null @@ -1,297 +0,0 @@ -import { appendEvent, newRunId as createDefaultRunId, rawRunDir } from "./journal-core.ts"; -import { LaneRunner, type RunnerOptions } from "./runner.ts"; -import type { - KitEvent, - Effort, - LaneMode, - LaneOrigin, - LaneProjection, - LaneSpec, - LaneState, - RunProjection, -} from "./schema.ts"; -import { normalizeLaneSpec, validateLaneSpec } from "./table.ts"; - -export interface LaneRunnerPort { - projection(): RunProjection; - dispatch(specs: LaneSpec[]): Promise; - abandon(lane: string, reason: string): void; - abandonAll(reason: string): void; - shutdown(reason: string): Promise; -} - -export interface LaneCoordinatorJournal { - append(event: KitEvent): void; - rawDir(runId: string): string; -} - -export interface LaneCoordinatorOptions { - origin: LaneOrigin; - createRunner?: (options: RunnerOptions) => LaneRunnerPort; - journal?: LaneCoordinatorJournal; - clock?: () => Date; - newRunId?: () => string; -} - -export interface LaneSnapshotDto { - lane: string; - model: string; - effort: Effort; - mode: LaneMode; - state: LaneState; - currentTool?: string; - lastStatus?: string; - tokensIn: number; - tokensOut: number; - cost: number; - context: number; - answer?: string; - durationMs?: number; - abandonReason?: string; -} - -export interface RunSnapshotDto { - run: string; - state: "active" | "ended"; - ok?: boolean; - durationMs: number; - totals: { - cost: number; - tokensIn: number; - tokensOut: number; - }; - lanes: LaneSnapshotDto[]; -} - -interface CoordinatedRun { - id: string; - createdAt: string; - startedAt: number; - runner: LaneRunnerPort; - ended: boolean; - ok?: boolean; - durationMs?: number; - settled: Promise; -} - -const DEFAULT_JOURNAL: LaneCoordinatorJournal = { - append: appendEvent, - rawDir: rawRunDir, -}; - -function finite(value: number): number { - return Number.isFinite(value) ? value : 0; -} - -function snapshotLane(lane: LaneProjection, nowMs: number): LaneSnapshotDto { - const durationMs = lane.durationMs ?? - (lane.state === "running" && lane.startedAtMs !== undefined - ? Math.max(0, nowMs - lane.startedAtMs) - : undefined); - return { - lane: lane.spec.lane, - model: lane.spec.model, - effort: lane.spec.effort, - mode: lane.spec.mode ?? "workspace-write", - state: lane.state, - ...(lane.currentTool !== undefined ? { currentTool: lane.currentTool } : {}), - ...(lane.lastStatus !== undefined ? { lastStatus: lane.lastStatus } : {}), - tokensIn: finite(lane.tokensIn), - tokensOut: finite(lane.tokensOut), - cost: finite(lane.cost), - context: finite(lane.context), - ...(lane.answer !== undefined ? { answer: lane.answer } : {}), - ...(durationMs !== undefined ? { durationMs: finite(durationMs) } : {}), - ...(lane.abandonReason !== undefined ? { abandonReason: lane.abandonReason } : {}), - }; -} - -export class LaneCoordinator { - readonly origin: LaneOrigin; - private readonly createRunner: (options: RunnerOptions) => LaneRunnerPort; - private readonly journal: LaneCoordinatorJournal; - private readonly clock: () => Date; - private readonly createRunId: () => string; - private current?: CoordinatedRun; - private closed = false; - private shutdownPromise?: Promise; - - constructor(options: LaneCoordinatorOptions) { - this.origin = options.origin; - this.createRunner = options.createRunner ?? ((runnerOptions) => new LaneRunner(runnerOptions)); - this.journal = options.journal ?? DEFAULT_JOURNAL; - this.clock = options.clock ?? (() => new Date()); - this.createRunId = options.newRunId ?? createDefaultRunId; - } - - async spawn(specs: LaneSpec[]): Promise { - if (this.closed) throw new Error("Lane coordinator is shut down"); - if (this.current && !this.current.ended) { - throw new Error(`Run ${this.current.id} is still active`); - } - const normalized = this.validateAndNormalize(specs); - const id = this.createRunId(); - const created = this.clock(); - const createdAt = created.toISOString(); - const runner = this.createRunner({ - runId: id, - origin: this.origin, - append: (event) => this.journal.append(event), - rawDir: this.journal.rawDir(id), - }); - const run: CoordinatedRun = { - id, - createdAt, - startedAt: created.getTime(), - runner, - ended: false, - settled: Promise.resolve(), - }; - this.current = run; - this.appendLifecycleEvent({ - v: 1, - t: createdAt, - run: id, - type: "run_created", - lanes: normalized.length, - origin: this.origin, - }); - - const dispatch = runner.dispatch(normalized); - run.settled = dispatch.then( - (lanes) => { - this.finish(run, lanes.every((lane) => lane.state === "done")); - }, - async () => { - await runner.shutdown("lane run failed"); - this.finish(run, false); - }, - ); - return this.snapshot(run); - } - - status(lane?: string): RunSnapshotDto { - const run = this.requireRun(); - return this.snapshot(run, lane); - } - - async wait(signal?: AbortSignal): Promise { - const run = this.requireRun(); - if (!run.ended) { - if (!signal) { - await run.settled; - } else if (!signal.aborted) { - const detached = Promise.withResolvers(); - const onAbort = () => detached.resolve(); - signal.addEventListener("abort", onAbort, { once: true }); - try { - await Promise.race([run.settled, detached.promise]); - } finally { - signal.removeEventListener("abort", onAbort); - } - } - } - return this.snapshot(run); - } - - abandon(input: { lane?: string; reason?: string }): RunSnapshotDto { - const run = this.requireRun(); - const reason = input.reason?.trim() || "abandoned"; - if (input.lane !== undefined) { - if (!run.runner.projection().lanes.has(input.lane)) { - throw new Error(`Unknown lane "${input.lane}"`); - } - run.runner.abandon(input.lane, reason); - } else { - run.runner.abandonAll(reason); - } - return this.snapshot(run); - } - - shutdown(reason: string): Promise { - if (!this.shutdownPromise) { - this.closed = true; - this.shutdownPromise = this.finishShutdown(reason); - } - return this.shutdownPromise; - } - - private validateAndNormalize(specs: LaneSpec[]): LaneSpec[] { - if (specs.length === 0) throw new Error("At least one lane is required"); - const violations: string[] = []; - const seen = new Set(); - const normalized: LaneSpec[] = []; - specs.forEach((spec, index) => { - const label = spec.lane || `lane-${index + 1}`; - const reason = validateLaneSpec(spec, { origin: this.origin }); - if (reason) violations.push(`${label}: ${reason}`); - if (spec.lane) { - if (seen.has(spec.lane)) violations.push(`${spec.lane}: duplicate lane id`); - seen.add(spec.lane); - } - if (!reason) normalized.push(normalizeLaneSpec(spec, { origin: this.origin })); - }); - if (violations.length > 0) throw new Error(`Invalid lane specs:\n${violations.join("\n")}`); - return normalized; - } - - private requireRun(): CoordinatedRun { - if (!this.current) throw new Error("No lane run is available"); - return this.current; - } - - private snapshot(run: CoordinatedRun, laneName?: string): RunSnapshotDto { - const projection = run.runner.projection(); - let lanes: LaneProjection[]; - if (laneName === undefined) { - lanes = [...projection.lanes.values()]; - } else { - const lane = projection.lanes.get(laneName); - if (!lane) throw new Error(`Unknown lane "${laneName}"`); - lanes = [lane]; - } - const nowMs = this.clock().getTime(); - return { - run: run.id, - state: run.ended ? "ended" : "active", - ...(run.ended ? { ok: run.ok ?? false } : {}), - durationMs: run.durationMs ?? Math.max(0, nowMs - run.startedAt), - totals: { - cost: finite(projection.totalCost), - tokensIn: finite(projection.totalTokensIn), - tokensOut: finite(projection.totalTokensOut), - }, - lanes: lanes.map((lane) => snapshotLane(lane, nowMs)), - }; - } - - private appendLifecycleEvent(event: KitEvent): void { - try { - this.journal.append(event); - } catch {} - } - - private finish(run: CoordinatedRun, ok: boolean): void { - if (run.ended) return; - run.ended = true; - run.ok = ok; - const endedAt = this.clock(); - const elapsed = endedAt.getTime() - run.startedAt; - run.durationMs = Number.isFinite(elapsed) ? Math.max(0, elapsed) : 0; - this.appendLifecycleEvent({ - v: 1, - t: endedAt.toISOString(), - run: run.id, - type: "run_end", - ok, - durationMs: run.durationMs, - }); - } - - private async finishShutdown(reason: string): Promise { - const run = this.current; - if (!run || run.ended) return; - await run.runner.shutdown(reason); - await run.settled; - } -} diff --git a/packages/pi-kit/src/lanes-tui.ts b/packages/pi-kit/src/lanes-tui.ts deleted file mode 100644 index 7ffed34..0000000 --- a/packages/pi-kit/src/lanes-tui.ts +++ /dev/null @@ -1,378 +0,0 @@ -/** - * Full-screen lanes TUI overlay: browse live and archived lane runs, - * inspect each lane's session (thinking, text, tool calls, results). - * Shown via ctx.ui.custom() on top of the active Pi session. - */ -import { closeSync, openSync, readSync, statSync } from "node:fs"; -import { join } from "node:path"; -import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component, type TUI } from "@earendil-works/pi-tui"; -import { listRuns, rawRunDir, readRun, reduceRun } from "./journal-core.ts"; -import type { LaneProjection, RunProjection } from "./schema.ts"; -import { LaneTranscript } from "./transcript.ts"; - -interface Theme { - fg(color: string, text: string): string; -} - -const RAIL_WIDTH = 30; -const POLL_MS = 500; - -function fmtDuration(ms: number): string { - const seconds = Math.floor((Number.isFinite(ms) ? Math.max(0, ms) : 0) / 1_000); - if (seconds < 60) return `${seconds}s`; - return `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, "0")}s`; -} - -function laneDuration(lane: LaneProjection, nowMs = Date.now()): number | undefined { - return lane.durationMs ?? - (lane.state === "running" && lane.startedAtMs !== undefined ? Math.max(0, nowMs - lane.startedAtMs) : undefined); -} - -interface LaneFeed { - transcript: LaneTranscript; - bytesRead: number; -} - -export class RunView { - readonly runId: string; - projection: RunProjection; - private readonly feeds = new Map(); - - constructor(runId: string) { - this.runId = runId; - this.projection = reduceRun(readRun(runId)); - } - - refresh(): void { - this.projection = reduceRun(readRun(this.runId)); - for (const lane of this.projection.lanes.keys()) this.pollLane(lane); - } - - transcript(lane: string): LaneTranscript { - this.pollLane(lane); - return this.feeds.get(lane)!.transcript; - } - - private pollLane(lane: string): void { - let feed = this.feeds.get(lane); - if (!feed) { - feed = { transcript: new LaneTranscript(), bytesRead: 0 }; - this.feeds.set(lane, feed); - } - const path = join(rawRunDir(this.runId), `${lane}.jsonl`); - try { - const size = statSync(path).size; - if (size <= feed.bytesRead) return; - // Track offsets in bytes (never UTF-16 code units) and feed only - // complete lines, so multibyte characters and half-flushed tail lines - // are never split or silently dropped. - const fd = openSync(path, "r"); - try { - const buf = Buffer.alloc(size - feed.bytesRead); - const read = readSync(fd, buf, 0, buf.length, feed.bytesRead); - const end = buf.subarray(0, read).lastIndexOf(0x0a) + 1; - if (end === 0) return; - feed.transcript.feedChunk(buf.subarray(0, end).toString("utf8")); - feed.bytesRead += end; - } finally { - closeSync(fd); - } - } catch { - // No transcript captured for this lane (pre-tap run or lane not started). - } - } -} - -function stateColor(state: LaneProjection["state"]): string { - switch (state) { - case "running": - return "accent"; - case "done": - return "success"; - case "failed": - case "abandoned": - return "error"; - default: - return "dim"; - } -} - -function stateGlyph(state: LaneProjection["state"]): string { - switch (state) { - case "running": - return "▶"; - case "done": - return "✔"; - case "failed": - return "✖"; - case "abandoned": - return "⊘"; - default: - return "◌"; - } -} - -export class LanesTuiComponent implements Component { - private runs: string[] = []; - private runIndex = 0; - private laneIndex = 0; - private pane: "runs" | "lanes" = "lanes"; - private scroll = 0; - private follow = true; - private showThinking = true; - private view?: RunView; - private timer?: NodeJS.Timeout; - private lineCache?: { key: string; lines: string[] }; - - private readonly tui: TUI; - private readonly theme: Theme; - private readonly done: () => void; - - constructor(tui: TUI, theme: Theme, done: () => void, initialRun?: string) { - this.tui = tui; - this.theme = theme; - this.done = done; - this.reloadRuns(initialRun); - this.timer = setInterval(() => { - this.view?.refresh(); - this.tui.requestRender(); - }, POLL_MS); - this.timer.unref(); - } - - dispose(): void { - clearInterval(this.timer); - } - - invalidate(): void { - this.lineCache = undefined; - } - - private reloadRuns(preferred?: string): void { - this.runs = listRuns(); - const index = preferred ? this.runs.indexOf(preferred) : 0; - this.runIndex = index >= 0 ? index : 0; - this.selectRun(); - } - - private selectRun(): void { - const runId = this.runs[this.runIndex]; - this.view = runId ? new RunView(runId) : undefined; - this.view?.refresh(); - this.laneIndex = 0; - this.scroll = 0; - this.follow = true; - this.lineCache = undefined; - } - - private lanes(): LaneProjection[] { - return this.view ? [...this.view.projection.lanes.values()] : []; - } - - private selectedLane(): LaneProjection | undefined { - return this.lanes()[this.laneIndex]; - } - - handleInput(data: string): void { - if (matchesKey(data, "q") || matchesKey(data, "escape")) { - this.done(); - return; - } - if (matchesKey(data, "tab")) { - this.pane = this.pane === "runs" ? "lanes" : "runs"; - return; - } - if (matchesKey(data, "up") || matchesKey(data, "k")) { - if (this.pane === "runs") { - this.runIndex = Math.max(0, this.runIndex - 1); - this.selectRun(); - } else { - this.laneIndex = Math.max(0, this.laneIndex - 1); - this.resetScroll(); - } - return; - } - if (matchesKey(data, "down") || matchesKey(data, "j")) { - if (this.pane === "runs") { - this.runIndex = Math.min(Math.max(0, this.runs.length - 1), this.runIndex + 1); - this.selectRun(); - } else { - this.laneIndex = Math.min(Math.max(0, this.lanes().length - 1), this.laneIndex + 1); - this.resetScroll(); - } - return; - } - if (matchesKey(data, "pageUp")) { - this.follow = false; - this.scroll = Math.max(0, this.scroll - this.viewHeight()); - return; - } - if (matchesKey(data, "pageDown")) { - this.scroll += this.viewHeight(); - return; - } - if (matchesKey(data, "t")) { - this.showThinking = !this.showThinking; - this.lineCache = undefined; - return; - } - if (matchesKey(data, "f")) { - this.follow = !this.follow; - return; - } - if (matchesKey(data, "r")) { - this.reloadRuns(this.runs[this.runIndex]); - } - } - - private resetScroll(): void { - this.scroll = 0; - this.follow = true; - this.lineCache = undefined; - } - - private viewHeight(): number { - return Math.max(4, this.tui.terminal.rows - 3); - } - - private transcriptLines(width: number): string[] { - const lane = this.selectedLane(); - if (!lane || !this.view) return [this.theme.fg("dim", "no lane selected")]; - const transcript = this.view.transcript(lane.spec.lane); - const key = `${this.view.runId}:${lane.spec.lane}:${transcript.version}:${width}:${this.showThinking}`; - if (this.lineCache?.key === key) return this.lineCache.lines; - - const t = this.theme; - const out: string[] = []; - const wrap = (text: string, color?: string) => { - for (const raw of text.split("\n")) { - const wrapped = wrapTextWithAnsi(raw, width); - for (const line of wrapped.length > 0 ? wrapped : [""]) { - out.push(color ? t.fg(color, line) : line); - } - } - }; - - if (transcript.entries.length === 0) { - out.push(t.fg("dim", "no transcript captured for this lane (pre-transcript run?)")); - out.push(""); - if (lane.answer) { - out.push(t.fg("accent", "── final answer (journal) ──")); - wrap(lane.answer); - } - } - for (const entry of transcript.entries) { - switch (entry.kind) { - case "task": - out.push(t.fg("accent", "── task ──")); - wrap(entry.text, "muted"); - break; - case "thinking": - if (!this.showThinking) break; - out.push(t.fg("dim", "── thinking ──")); - wrap(entry.text, "dim"); - break; - case "text": - out.push(""); - wrap(entry.text); - break; - case "tool": - out.push(t.fg("warning", `⚙ ${entry.title ?? "tool"}`)); - wrap(entry.text, "muted"); - break; - case "tool_result": { - const isError = (entry.title ?? "").endsWith("(error)"); - out.push(t.fg(isError ? "error" : "dim", `→ ${entry.title ?? "result"}`)); - wrap(entry.text.length > 1_500 ? `${entry.text.slice(0, 1_500)}…` : entry.text, "dim"); - break; - } - case "info": - wrap(entry.text, "dim"); - break; - } - } - this.lineCache = { key, lines: out }; - return out; - } - - private railLines(height: number): string[] { - const t = this.theme; - const width = RAIL_WIDTH - 2; - const lines: string[] = []; - const runsFocused = this.pane === "runs"; - - lines.push(t.fg(runsFocused ? "accent" : "muted", runsFocused ? "▸ RUNS (archived)" : " RUNS (archived)")); - const maxRuns = Math.max(3, Math.floor((height - 4) / 3)); - const runStart = Math.max(0, Math.min(this.runIndex - Math.floor(maxRuns / 2), this.runs.length - maxRuns)); - for (let i = runStart; i < Math.min(this.runs.length, runStart + maxRuns); i++) { - const selected = i === this.runIndex; - const label = truncateToWidth(this.runs[i]!.replace(/^run-/, ""), width - 2); - lines.push(selected ? t.fg(runsFocused ? "accent" : "text", `› ${label}`) : t.fg("dim", ` ${label}`)); - } - - lines.push(""); - const lanesFocused = this.pane === "lanes"; - lines.push(t.fg(lanesFocused ? "accent" : "muted", lanesFocused ? "▸ LANES" : " LANES")); - this.lanes().forEach((lane, i) => { - const selected = i === this.laneIndex; - const glyph = t.fg(stateColor(lane.state), stateGlyph(lane.state)); - const name = truncateToWidth(lane.spec.lane, width - 4); - lines.push(`${selected ? t.fg("accent", "›") : " "} ${glyph} ${selected ? t.fg("text", name) : t.fg("muted", name)}`); - const duration = laneDuration(lane); - const meta = truncateToWidth( - `${lane.spec.model.slice(lane.spec.model.indexOf("/") + 1)}:${lane.spec.effort} $${lane.cost.toFixed(3)}${duration === undefined ? "" : ` ${fmtDuration(duration)}`}`, - width - 4, - ); - lines.push(` ${t.fg("dim", meta)}`); - }); - return lines.slice(0, height); - } - - render(width: number): string[] { - const height = Math.max(6, this.tui.terminal.rows); - const bodyHeight = height - 2; - const t = this.theme; - const view = this.view; - const lanes = this.lanes(); - const running = lanes.filter((l) => l.state === "running").length; - - const runDuration = view - ? view.projection.durationMs ?? Math.max(0, Date.now() - Date.parse(view.projection.createdAt)) - : 0; - const headerText = view - ? `lanes tui · ${view.runId} · ${fmtDuration(runDuration)} · ${lanes.length} lanes${running ? ` · ${running} running` : ""} · $${view.projection.totalCost.toFixed(4)}` - : "lanes tui · no runs recorded yet"; - const header = truncateToWidth(t.fg("accent", headerText), width); - - const railWidth = Math.min(RAIL_WIDTH, Math.max(16, Math.floor(width / 3))); - const mainWidth = Math.max(10, width - railWidth - 1); - const rail = this.railLines(bodyHeight); - const all = this.transcriptLines(mainWidth); - const viewH = bodyHeight; - const maxScroll = Math.max(0, all.length - viewH); - if (this.follow) this.scroll = maxScroll; - this.scroll = Math.min(this.scroll, maxScroll); - const visible = all.slice(this.scroll, this.scroll + viewH); - - const sep = t.fg("dim", "│"); - const lines: string[] = [header]; - for (let i = 0; i < bodyHeight; i++) { - const left = rail[i] ?? ""; - const leftPad = Math.max(0, railWidth - visibleWidth(left)); - const right = visible[i] ?? ""; - const row = `${truncateToWidth(left, railWidth)}${" ".repeat(leftPad)}${sep}${truncateToWidth(right, mainWidth)}`; - lines.push(truncateToWidth(row, width)); - } - - const pos = maxScroll > 0 ? ` · ${Math.min(100, Math.round((this.scroll / maxScroll) * 100))}%` : ""; - const footer = truncateToWidth( - t.fg( - "dim", - `tab panes · ↑↓ select · pgup/pgdn scroll${pos} · t thinking(${this.showThinking ? "on" : "off"}) · f follow(${this.follow ? "on" : "off"}) · r reload · q close`, - ), - width, - ); - lines.push(footer); - return lines; - } -} diff --git a/packages/pi-kit/src/runner.ts b/packages/pi-kit/src/runner.ts deleted file mode 100644 index 03f1e42..0000000 --- a/packages/pi-kit/src/runner.ts +++ /dev/null @@ -1,752 +0,0 @@ -import { spawn, type ChildProcessByStdio } from "node:child_process"; -import { createWriteStream, mkdirSync, type WriteStream } from "node:fs"; -import { isAbsolute, relative, resolve, sep } from "node:path"; -import type { Readable } from "node:stream"; -import { StringDecoder } from "node:string_decoder"; -import { createClaudeCodeAdapter } from "./adapters/claude-code.ts"; -import { PiExecutionAdapter } from "./adapters/pi.ts"; -import type { - LaneEventParser, - LaneExecutionAdapter, - LaneProcessPlan, - NormalizedLaneEvent, -} from "./execution-adapter.ts"; -import type { KitEvent, LaneOrigin, LaneProjection, LaneSpec, RunProjection } from "./schema.ts"; -import { estimateCost, findModel, normalizeLaneSpec, validateLaneSpec } from "./table.ts"; - -export interface RunnerOptions { - runId: string; - append: (ev: KitEvent) => void; - maxConcurrent?: number; - maxAttempts?: number; - piBinary?: string; - adapters?: readonly LaneExecutionAdapter[]; - origin?: LaneOrigin; - onUpdate?: () => void; - /** Directory for canonical per-lane JSONL transcripts (`/.jsonl`). */ - rawDir?: string; -} - -type TerminalOutcome = - | { type: "end"; ok: boolean; answer: string; stopReason?: string } - | { type: "abandoned" }; - -interface LaneRuntime { - child: ChildProcessByStdio; - parser: LaneEventParser; - raw?: WriteStream; - terminal?: TerminalOutcome; - closed: boolean; - terminating: boolean; - parserEnded: boolean; - startedAt: number; - stdoutBuffer: string; - stdoutOverflow: boolean; - stderrTail: string; - answer: string; - statusTail: string; - statusTimer?: NodeJS.Timeout; - lastStatusAt: number; - tokensIn: number; - tokensOut: number; - cacheRead: number; - context: number; - rawBytes: number; - rawCapped: boolean; - taskWritten: boolean; -} - -interface PreparedLane { - spec: LaneSpec; - adapter: LaneExecutionAdapter; -} - -interface LaneAttemptResult { - outcome: TerminalOutcome; - tokensIn: number; - tokensOut: number; - cost: number; - context: number; -} - -interface AttemptUsage { - tokensIn: number; - tokensOut: number; - cost: number; -} - -type JsonRecord = Record; - -const MAX_STREAM_BYTES = 4 * 1024 * 1024; - -/** Live runners with children, reaped synchronously if the parent dies. */ -const ACTIVE_RUNNERS = new Set(); -let exitHookInstalled = false; - -function installExitHook(): void { - if (exitHookInstalled) return; - exitHookInstalled = true; - process.once("exit", () => { - for (const runner of ACTIVE_RUNNERS) runner.reapAll(); - }); -} - -/** SIGTERM/SIGKILL the child's whole process group, falling back to the direct child. */ -function killTree(child: ChildProcessByStdio, signal: NodeJS.Signals): void { - if (child.exitCode !== null || child.signalCode !== null) return; - try { - if (child.pid) process.kill(-child.pid, signal); - else child.kill(signal); - } catch { - try { - child.kill(signal); - } catch { - // Process already gone. - } - } -} - -function isRecord(value: unknown): value is JsonRecord { - return typeof value === "object" && value !== null; -} - -function toolSummary(tool: string, args: unknown): string { - let summary: string | undefined; - if (isRecord(args)) { - if (tool.toLowerCase() === "bash" && typeof args.command === "string") summary = args.command; - for (const key of ["path", "file_path", "filePath"]) { - if (summary === undefined && typeof args[key] === "string") summary = args[key]; - } - } - if (summary === undefined) { - try { - summary = JSON.stringify(args) ?? ""; - } catch { - summary = String(args); - } - } - return summary.replace(/\s+/g, " ").trim().slice(0, 80); -} - -function validUsage(event: Extract): boolean { - return [event.input, event.output, event.cacheRead, event.context].every( - (value) => typeof value === "number" && Number.isFinite(value) && value >= 0, - ); -} - -function isAssistantError(event: Extract): boolean { - return event.isError === true; -} - -export class LaneRunner { - private readonly append: (ev: KitEvent) => void; - private readonly maxConcurrent: number; - private readonly maxAttempts: number; - private readonly onUpdate?: () => void; - private readonly adapters: readonly LaneExecutionAdapter[]; - private readonly origin: LaneOrigin; - private readonly rawDir?: string; - private readonly runId: string; - private readonly runtimes = new Map(); - private readonly live: RunProjection; - private dispatching = false; - private shuttingDown = false; - private dispatchPromise?: Promise; - private shutdownPromise?: Promise; - - constructor(opts: RunnerOptions) { - this.runId = opts.runId; - this.append = opts.append; - this.maxConcurrent = Math.max(1, Math.floor(opts.maxConcurrent ?? 4)); - this.maxAttempts = Math.max(1, Math.floor(opts.maxAttempts ?? 2)); - this.adapters = opts.adapters ?? [new PiExecutionAdapter(opts.piBinary), createClaudeCodeAdapter()]; - this.origin = opts.origin ?? "pi"; - if (opts.rawDir) this.rawDir = opts.rawDir; - this.onUpdate = opts.onUpdate; - this.live = { - run: opts.runId, - origin: this.origin, - createdAt: new Date().toISOString(), - ended: false, - lanes: new Map(), - totalCost: 0, - totalTokensIn: 0, - totalTokensOut: 0, - }; - } - - projection(): RunProjection { - return this.live; - } - - dispatch(specs: LaneSpec[]): Promise { - if (this.shuttingDown) return Promise.reject(new Error("LaneRunner is shutting down")); - if (this.dispatching) return Promise.reject(new Error("LaneRunner.dispatch may only be called once")); - const promise = this.dispatchRun(specs); - this.dispatchPromise = promise; - return promise; - } - - private async dispatchRun(specs: LaneSpec[]): Promise { - const violations: string[] = []; - const seen = new Set(); - const prepared: PreparedLane[] = []; - specs.forEach((input, index) => { - const label = input.lane || `lane-${index + 1}`; - const reason = validateLaneSpec(input, { origin: this.origin }); - if (reason) violations.push(`${label}: ${reason}`); - if (input.lane) { - if (seen.has(input.lane)) violations.push(`${input.lane}: duplicate lane id`); - seen.add(input.lane); - } - if (reason) return; - - try { - const spec = normalizeLaneSpec(input, { origin: this.origin }); - const row = findModel(spec.model)!; - const matches = this.adapters.filter((adapter) => adapter.route === row.route); - if (matches.length !== 1) { - violations.push( - `${label}: expected exactly one execution adapter for route ${row.route}; found ${matches.length}`, - ); - return; - } - const adapter = matches[0]!; - adapter.preflight(spec); - prepared.push({ spec, adapter }); - } catch (error) { - violations.push(`${label}: ${error instanceof Error ? error.message : String(error)}`); - } - }); - if (violations.length > 0) throw new Error(`Invalid lane specs:\n${violations.join("\n")}`); - - this.dispatching = true; - installExitHook(); - ACTIVE_RUNNERS.add(this); - for (const { spec } of prepared) { - this.record({ v: 1, t: new Date().toISOString(), run: this.runId, lane: spec.lane, type: "lane_created", spec }); - } - - let next = 0; - const workers = Array.from({ length: Math.min(this.maxConcurrent, prepared.length) }, async () => { - while (next < prepared.length) { - const lane = prepared[next++]; - if (lane && this.live.lanes.get(lane.spec.lane)?.state === "queued") await this.runLane(lane); - } - }); - try { - await Promise.all(workers); - } finally { - ACTIVE_RUNNERS.delete(this); - } - - this.live.ended = true; - this.live.ok = [...this.live.lanes.values()].every((lane) => lane.state === "done"); - this.notify(); - return [...this.live.lanes.values()]; - } - - abandon(lane: string, reason: string): void { - const projection = this.live.lanes.get(lane); - if (!projection || ["done", "failed", "abandoned"].includes(projection.state)) return; - - const runtime = this.runtimes.get(lane); - if (runtime?.terminal) return; - this.record({ v: 1, t: new Date().toISOString(), run: this.runId, lane, type: "lane_abandoned", reason }); - if (runtime) this.requestTerminal(runtime, { type: "abandoned" }); - } - - /** Abandon every non-terminal lane (abort signal, shutdown). */ - abandonAll(reason: string): void { - for (const [lane, projection] of this.live.lanes) { - if (!["done", "failed", "abandoned"].includes(projection.state)) this.abandon(lane, reason); - } - } - - /** Abandon active lanes and wait until dispatch has observed every child close. */ - shutdown(reason: string): Promise { - if (!this.shutdownPromise) { - this.shuttingDown = true; - this.shutdownPromise = this.finishShutdown(reason); - } - return this.shutdownPromise; - } - - /** Synchronous last-resort kill of every live child. Called from the process exit hook. */ - reapAll(): void { - this.shuttingDown = true; - for (const runtime of this.runtimes.values()) killTree(runtime.child, "SIGKILL"); - } - - private async finishShutdown(reason: string): Promise { - this.abandonAll(reason); - try { - await this.dispatchPromise; - } catch { - // Dispatch validation/preflight errors own no live children. - } finally { - ACTIVE_RUNNERS.delete(this); - } - } - - /** SIGTERM the child's process group now, escalate to SIGKILL after 5s. */ - private terminate(runtime: LaneRuntime): void { - if (runtime.terminating || runtime.closed) return; - runtime.terminating = true; - killTree(runtime.child, "SIGTERM"); - const killTimer = setTimeout(() => killTree(runtime.child, "SIGKILL"), 5_000); - killTimer.unref(); - runtime.child.once("close", () => clearTimeout(killTimer)); - } - - private requestTerminal(runtime: LaneRuntime, outcome: TerminalOutcome): void { - if (runtime.terminal) return; - runtime.terminal = outcome; - clearTimeout(runtime.statusTimer); - this.terminate(runtime); - } - - private record(event: KitEvent): void { - if (event.type === "lane_created") { - this.live.lanes.set(event.spec.lane, { - spec: event.spec, - state: "queued", - tokensIn: 0, - tokensOut: 0, - cost: 0, - context: 0, - }); - } else if (event.lane) { - const lane = this.live.lanes.get(event.lane); - if (lane) { - switch (event.type) { - case "lane_start": - lane.state = "running"; - lane.pid = event.pid; - lane.startedAtMs ??= Date.parse(event.t); - break; - case "lane_tool": - lane.currentTool = `${event.tool}: ${event.summary}`; - break; - case "lane_status": - lane.lastStatus = event.text; - break; - case "lane_usage": - this.live.totalTokensIn += event.input - lane.tokensIn; - this.live.totalTokensOut += event.output - lane.tokensOut; - this.live.totalCost += event.cost - lane.cost; - lane.tokensIn = event.input; - lane.tokensOut = event.output; - lane.cost = event.cost; - lane.context = event.context; - break; - case "lane_end": - lane.state = event.ok ? "done" : "failed"; - lane.answer = event.answer; - lane.durationMs = event.durationMs; - break; - case "lane_abandoned": - lane.state = "abandoned"; - lane.abandonReason = event.reason; - if (lane.startedAtMs !== undefined) { - lane.durationMs = Math.max(0, Date.parse(event.t) - lane.startedAtMs); - } - break; - } - } - } - try { - this.append(event); - } catch {} - this.notify(); - } - - private notify(): void { - try { - this.onUpdate?.(); - } catch {} - } - - private transcriptPath(lane: string): { directory: string; path: string } { - const directory = resolve(this.rawDir!); - const path = resolve(directory, `${lane}.jsonl`); - const childPath = relative(directory, path); - if (childPath === ".." || childPath.startsWith(`..${sep}`) || isAbsolute(childPath)) { - throw new Error(`Transcript path for lane ${lane} escapes rawDir`); - } - return { directory, path }; - } - - private async runLane(prepared: PreparedLane): Promise { - const startedAt = Date.now(); - let usage: AttemptUsage = { tokensIn: 0, tokensOut: 0, cost: 0 }; - - for (let attempt = 1; attempt <= this.maxAttempts; attempt++) { - const result = await this.runLaneAttempt(prepared, usage); - usage = { - tokensIn: usage.tokensIn + result.tokensIn, - tokensOut: usage.tokensOut + result.tokensOut, - cost: usage.cost + result.cost, - }; - if (result.outcome.type === "abandoned") return; - if (result.outcome.ok || attempt === this.maxAttempts || this.shuttingDown) { - this.record({ - v: 1, - t: new Date().toISOString(), - run: this.runId, - lane: prepared.spec.lane, - type: "lane_end", - ok: result.outcome.ok, - ...(result.outcome.stopReason ? { stopReason: result.outcome.stopReason } : {}), - answer: result.outcome.answer.slice(0, 4_000), - durationMs: Date.now() - startedAt, - }); - return; - } - const projection = this.live.lanes.get(prepared.spec.lane); - if (projection) projection.currentTool = undefined; - this.record({ - v: 1, - t: new Date().toISOString(), - run: this.runId, - lane: prepared.spec.lane, - type: "lane_status", - text: `attempt ${attempt} failed${result.outcome.stopReason ? ` (${result.outcome.stopReason})` : ""}; retrying`, - }); - } - } - - private async runLaneAttempt({ spec, adapter }: PreparedLane, prior: AttemptUsage): Promise { - try { - await adapter.prepare?.(spec); - } catch (error) { - if (this.live.lanes.get(spec.lane)?.state !== "abandoned") { - const message = error instanceof Error ? error.message : String(error); - return { - outcome: { type: "end", ok: false, stopReason: `prepare:${message}`, answer: message }, - tokensIn: 0, - tokensOut: 0, - cost: 0, - context: 0, - }; - } - return { - outcome: { type: "abandoned" }, - tokensIn: 0, - tokensOut: 0, - cost: 0, - context: 0, - }; - } - if (this.live.lanes.get(spec.lane)?.state === "abandoned") { - return { outcome: { type: "abandoned" }, tokensIn: 0, tokensOut: 0, cost: 0, context: 0 }; - } - - let plan: LaneProcessPlan; - let parser: LaneEventParser; - try { - plan = adapter.build(spec); - parser = adapter.createParser(spec); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return { - outcome: { type: "end", ok: false, stopReason: `setup:${message}`, answer: message }, - tokensIn: 0, - tokensOut: 0, - cost: 0, - context: 0, - }; - } - - const { promise, resolve } = Promise.withResolvers(); - let child: ChildProcessByStdio; - try { - child = spawn(plan.command, plan.args, { - cwd: plan.cwd, - env: plan.env, - stdio: ["ignore", "pipe", "pipe"], - detached: true, - }); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - resolve({ - outcome: { type: "end", ok: false, stopReason: `spawn:${message}`, answer: message }, - tokensIn: 0, - tokensOut: 0, - cost: 0, - context: 0, - }); - return promise; - } - - let raw: WriteStream | undefined; - if (this.rawDir) { - try { - const transcript = this.transcriptPath(spec.lane); - mkdirSync(transcript.directory, { recursive: true }); - raw = createWriteStream(transcript.path, { flags: "a" }); - raw.on("error", () => { - // Transcript capture is best-effort; the lane must still run and settle. - }); - } catch { - // Transcript capture is best-effort; the lane must still run. - } - } - - const runtime: LaneRuntime = { - child, - parser, - ...(raw ? { raw } : {}), - closed: false, - terminating: false, - parserEnded: false, - startedAt: Date.now(), - stdoutBuffer: "", - stdoutOverflow: false, - stderrTail: "", - answer: "", - statusTail: "", - lastStatusAt: 0, - tokensIn: 0, - tokensOut: 0, - cacheRead: 0, - context: 0, - rawBytes: 0, - rawCapped: false, - taskWritten: false, - }; - this.runtimes.set(spec.lane, runtime); - this.writeCanonical(runtime, { v: 1, type: "task", text: spec.task }); - - const emitStatus = () => { - runtime.statusTimer = undefined; - if (runtime.terminal || !runtime.statusTail) return; - runtime.lastStatusAt = Date.now(); - this.record({ - v: 1, - t: new Date().toISOString(), - run: this.runId, - lane: spec.lane, - type: "lane_status", - text: runtime.statusTail.slice(-120), - }); - }; - - const handleEvent = (event: NormalizedLaneEvent, allowSuccess = true) => { - if (!isRecord(event) || event.v !== 1 || typeof event.type !== "string") return; - if (event.type === "task") { - if (!runtime.taskWritten) this.writeCanonical(runtime, { v: 1, type: "task", text: spec.task }); - return; - } - if (event.type === "usage") { - if (!validUsage(event)) return; - if ( - event.input === runtime.tokensIn && - event.output === runtime.tokensOut && - event.cacheRead === runtime.cacheRead && - event.context === runtime.context - ) { - return; - } - } - this.writeCanonical(runtime, event); - - switch (event.type) { - case "thinking_delta": - break; - case "text_delta": { - if (runtime.answer.length < 4_000) runtime.answer += event.delta.slice(0, 4_000 - runtime.answer.length); - runtime.statusTail = `${runtime.statusTail}${event.delta}`.replace(/\s+/g, " ").trim().slice(-120); - const elapsed = Date.now() - runtime.lastStatusAt; - if (elapsed >= 1_000) emitStatus(); - else if (!runtime.statusTimer) { - runtime.statusTimer = setTimeout(emitStatus, 1_000 - elapsed); - runtime.statusTimer.unref(); - } - break; - } - case "tool_start": - this.record({ - v: 1, - t: new Date().toISOString(), - run: this.runId, - lane: spec.lane, - type: "lane_tool", - tool: event.tool, - summary: toolSummary(event.tool, event.input), - }); - break; - case "tool_end": - break; - case "usage": - runtime.tokensIn = event.input; - runtime.tokensOut = event.output; - runtime.cacheRead = event.cacheRead; - runtime.context = event.context; - this.record({ - v: 1, - t: new Date().toISOString(), - run: this.runId, - lane: spec.lane, - type: "lane_usage", - input: prior.tokensIn + event.input, - output: prior.tokensOut + event.output, - cacheRead: event.cacheRead, - cost: prior.cost + estimateCost(spec.model, event.input, event.output), - context: event.context, - }); - break; - case "assistant_end": - runtime.answer = event.text.slice(0, 4_000); - if (isAssistantError(event)) { - this.requestTerminal(runtime, { - type: "end", - ok: false, - answer: runtime.answer, - stopReason: "assistant-error", - }); - } else if (allowSuccess) { - this.requestTerminal(runtime, { type: "end", ok: true, answer: runtime.answer }); - } - break; - } - }; - - const feedParser = (line: string) => { - try { - for (const event of parser.feedLine(line)) handleEvent(event); - } catch (error) { - this.requestTerminal(runtime, { - type: "end", - ok: false, - answer: runtime.stderrTail, - stopReason: `parser:${error instanceof Error ? error.message : String(error)}`, - }); - } - }; - - child.once("spawn", () => { - if (!runtime.closed && !runtime.terminal) { - this.record({ - v: 1, - t: new Date().toISOString(), - run: this.runId, - lane: spec.lane, - type: "lane_start", - pid: child.pid!, - }); - } - }); - const stdoutDecoder = new StringDecoder("utf8"); - const failStdoutOverflow = () => { - runtime.stdoutOverflow = true; - runtime.stdoutBuffer = ""; - this.requestTerminal(runtime, { - type: "end", - ok: false, - answer: runtime.answer, - stopReason: "stdout-overflow", - }); - }; - child.stdout.on("data", (chunk: Buffer) => { - if (runtime.stdoutOverflow) return; - runtime.stdoutBuffer += stdoutDecoder.write(chunk); - let newline = runtime.stdoutBuffer.indexOf("\n"); - while (newline >= 0) { - const line = runtime.stdoutBuffer.slice(0, newline); - runtime.stdoutBuffer = runtime.stdoutBuffer.slice(newline + 1); - if (Buffer.byteLength(line) > MAX_STREAM_BYTES) { - failStdoutOverflow(); - return; - } - if (line.trim()) feedParser(line); - newline = runtime.stdoutBuffer.indexOf("\n"); - } - if (Buffer.byteLength(runtime.stdoutBuffer) > MAX_STREAM_BYTES) failStdoutOverflow(); - }); - child.stderr.setEncoding("utf8"); - child.stderr.on("data", (chunk: string) => { - runtime.stderrTail = `${runtime.stderrTail}${chunk}`.slice(-500); - }); - child.once("error", (error) => { - this.requestTerminal(runtime, { - type: "end", - ok: false, - answer: runtime.stderrTail || error.message, - stopReason: `spawn:${error.message}`, - }); - }); - child.once("close", (code) => { - runtime.closed = true; - clearTimeout(runtime.statusTimer); - if (!runtime.stdoutOverflow) { - runtime.stdoutBuffer += stdoutDecoder.end(); - if (runtime.stdoutBuffer) feedParser(runtime.stdoutBuffer); - } - runtime.stdoutBuffer = ""; - - if (!runtime.parserEnded) { - runtime.parserEnded = true; - try { - for (const event of parser.end()) handleEvent(event, code === 0); - } catch (error) { - if (!runtime.terminal) { - runtime.terminal = { - type: "end", - ok: false, - answer: runtime.stderrTail, - stopReason: `parser:${error instanceof Error ? error.message : String(error)}`, - }; - } - } - } - if (!runtime.terminal) { - runtime.terminal = { - type: "end", - ok: false, - answer: runtime.stderrTail || runtime.answer, - stopReason: `exit:${code ?? "signal"}`, - }; - } - - const outcome = runtime.terminal; - this.runtimes.delete(spec.lane); - - let finished = false; - const finish = () => { - if (finished) return; - finished = true; - resolve({ - outcome, - tokensIn: runtime.tokensIn, - tokensOut: runtime.tokensOut, - cost: estimateCost(spec.model, runtime.tokensIn, runtime.tokensOut), - context: runtime.context, - }); - }; - if (runtime.raw && !runtime.raw.destroyed) { - runtime.raw.once("error", finish); - runtime.raw.end(finish); - } else { - finish(); - } - }); - return promise; - } - - private writeCanonical(runtime: LaneRuntime, event: NormalizedLaneEvent): void { - if (event.type === "task") { - if (runtime.taskWritten) return; - runtime.taskWritten = true; - } - if (!runtime.raw?.writable || runtime.rawCapped) return; - const line = `${JSON.stringify(event)}\n`; - const bytes = Buffer.byteLength(line); - if (runtime.rawBytes + bytes > MAX_STREAM_BYTES) { - runtime.rawCapped = true; - return; - } - runtime.rawBytes += bytes; - runtime.raw.write(line); - } -} diff --git a/packages/pi-kit/src/schema.ts b/packages/pi-kit/src/schema.ts deleted file mode 100644 index b20969f..0000000 --- a/packages/pi-kit/src/schema.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * pi-kit event schema v1 — FROZEN CONTRACT. - * All extensions and the runner build against these types. - * The journal is append-only JSONL; one event per line; replay must be deterministic. - */ - -export type Effort = "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; - -export type LaneMode = "read-only" | "workspace-write"; -export type LaneOrigin = "pi" | "mcp"; -export type ExecutionRoute = "pi" | "claude-code"; - -export type LaneState = "queued" | "running" | "done" | "failed" | "abandoned"; - -/** A single dispatched subagent lane. Model and effort MUST be explicit. */ -export interface LaneSpec { - /** short unique id within the run, e.g. "lane-1" or a caller-chosen name */ - lane: string; - /** self-contained task prompt */ - task: string; - /** full selector "provider/model-id", e.g. "openai-codex/gpt-5.6-sol" */ - model: string; - effort: Effort; - /** tool and workspace access policy; Pi-origin calls default to workspace-write */ - mode?: LaneMode; - /** working directory; defaults to the parent session cwd */ - cwd?: string; - /** why this model/effort was chosen (journaled for audit) */ - rationale?: string; -} - -export type KitEvent = { v: 1; t: string; run: string; lane?: string } & ( - | { type: "run_created"; lanes: number; origin: string } - | { type: "lane_created"; spec: LaneSpec } - | { type: "lane_start"; pid: number } - | { type: "lane_tool"; tool: string; summary: string } - | { type: "lane_status"; text: string } - | { - type: "lane_usage"; - input: number; - output: number; - cacheRead?: number; - /** cumulative estimated USD for this lane */ - cost: number; - /** cumulative context tokens (last known) */ - context: number; - } - | { type: "lane_end"; ok: boolean; stopReason?: string; answer: string; durationMs: number } - | { type: "lane_abandoned"; reason: string } - | { type: "run_end"; ok: boolean; durationMs: number } -); - -/** Reduced projection of a run, rebuilt purely from journal events. */ -export interface LaneProjection { - spec: LaneSpec; - state: LaneState; - pid?: number; - currentTool?: string; - lastStatus?: string; - tokensIn: number; - tokensOut: number; - cost: number; - context: number; - answer?: string; - durationMs?: number; - /** In-memory/reduced start time used for live elapsed rendering; never exposed in tool DTOs. */ - startedAtMs?: number; - abandonReason?: string; -} - -export interface RunProjection { - run: string; - origin: string; - createdAt: string; - ended: boolean; - ok?: boolean; - lanes: Map; - totalCost: number; - totalTokensIn: number; - totalTokensOut: number; - durationMs?: number; -} - -export const DATA_DIR_ENV = "PIKIT_DATA_DIR"; -export const DEFAULT_DATA_DIR = "~/.pickforge/pi-kit"; diff --git a/packages/pi-kit/src/table.ts b/packages/pi-kit/src/table.ts deleted file mode 100644 index 63fe743..0000000 --- a/packages/pi-kit/src/table.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * pi-kit model table — POLICY AS CODE. FROZEN CONTRACT. - * Mirrors the managed pool in ~/.pi/agent/AGENTS.md. Costs are rough - * routing estimates in USD per million tokens, not billing truth. - */ -import type { - Effort, - ExecutionRoute, - LaneMode, - LaneOrigin, - LaneSpec, -} from "./schema.ts"; - -export interface ModelRow { - selector: string; - name: string; - prior: Effort; - route: ExecutionRoute; - runtimeModel: string; - allowedEfforts: Effort[]; - origins: LaneOrigin[]; - /** estimated $/Mtok input, output — routing heuristics only */ - inPerM: number; - outPerM: number; - vision: boolean; -} - -const ALL_EFFORTS: Effort[] = ["off", "minimal", "low", "medium", "high", "xhigh"]; -const ALL_ORIGINS: LaneOrigin[] = ["pi", "mcp"]; - -export const MODEL_TABLE: ModelRow[] = [ - { selector: "openai-codex/gpt-5.6-sol", name: "GPT-5.6 Sol", prior: "medium", route: "pi", runtimeModel: "gpt-5.6-sol", allowedEfforts: [...ALL_EFFORTS], origins: [...ALL_ORIGINS], inPerM: 1.75, outPerM: 14, vision: true }, - { selector: "anthropic/claude-fable-5", name: "Fable 5", prior: "high", route: "claude-code", runtimeModel: "fable", allowedEfforts: ["low", "medium", "high"], origins: ["pi"], inPerM: 3, outPerM: 15, vision: true }, - { selector: "anthropic/claude-opus-4-8", name: "Opus 4.8", prior: "xhigh", route: "claude-code", runtimeModel: "opus", allowedEfforts: ["xhigh"], origins: ["pi"], inPerM: 10, outPerM: 40, vision: true }, - { selector: "anthropic/claude-sonnet-5", name: "Sonnet 5", prior: "medium", route: "claude-code", runtimeModel: "sonnet", allowedEfforts: ["low", "medium", "high", "xhigh"], origins: ["pi"], inPerM: 3, outPerM: 15, vision: true }, - { selector: "xai/grok-4.5", name: "Grok 4.5", prior: "high", route: "pi", runtimeModel: "grok-4.5", allowedEfforts: ["high"], origins: [...ALL_ORIGINS], inPerM: 2, outPerM: 6, vision: true }, - { selector: "ollama/glm-5.2:cloud", name: "GLM-5.2", prior: "medium", route: "pi", runtimeModel: "glm-5.2:cloud", allowedEfforts: [...ALL_EFFORTS], origins: [...ALL_ORIGINS], inPerM: 0.6, outPerM: 2.2, vision: false }, -]; - -export const EFFORTS: Effort[] = [...ALL_EFFORTS]; -export const LANE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/; - -/** Models that must never run, on any route. */ -export const BANNED = [/haiku/i, /luna/i, /gpt-5\.6-terra/i]; - -/** Per-model pinned efforts (exact value required). */ -export const EFFORT_PINS: Array<{ match: RegExp; level: Effort }> = [ - { match: /opus/i, level: "xhigh" }, - { match: /grok/i, level: "high" }, -]; - -export function findModel(selector: string): ModelRow | undefined { - return MODEL_TABLE.find((m) => m.selector === selector); -} - -export interface LanePolicyContext { - origin: LaneOrigin; -} - -export type NormalizedLaneSpec = LaneSpec & { mode: LaneMode }; - -/** Apply origin-specific compatibility defaults after validation. */ -export function normalizeLaneSpec( - spec: LaneSpec, - { origin }: LanePolicyContext = { origin: "pi" }, -): NormalizedLaneSpec { - const mode = spec.mode ?? (origin === "pi" ? "workspace-write" : undefined); - if (!mode) throw new Error("mode is required for MCP-origin lanes"); - return { ...spec, mode }; -} - -/** Validate a lane spec against policy. Returns null when valid, else a reason. */ -export function validateLaneSpec( - spec: Partial, - { origin }: LanePolicyContext = { origin: "pi" }, -): string | null { - if (!spec.lane || !LANE_ID_PATTERN.test(spec.lane)) { - return "lane id must match ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$"; - } - if (!spec.task || !spec.task.trim()) return "task is required and must be self-contained"; - if (!spec.model) return "model is required: state the full selector from the model table"; - const model = spec.model; - if (BANNED.some((re) => re.test(model))) { - return `model ${model} is banned (Haiku/Luna/Terra are never allowed)`; - } - if (!spec.effort) return "effort is required: a default lane is not a model selection"; - const effort = spec.effort; - if (!EFFORTS.includes(effort)) { - return `effort "${effort}" is invalid; xhigh is the absolute ceiling (ultra/max do not exist)`; - } - const row = findModel(model); - if (!row) { - return `model ${model} is not in the current table; use one of: ${MODEL_TABLE.map((m) => m.selector).join(", ")}`; - } - if (!row.origins.includes(origin)) { - return `model ${model} is unavailable from ${origin}; use the native Claude workflow for Claude-to-Claude delegation`; - } - if (spec.mode !== undefined && spec.mode !== "read-only" && spec.mode !== "workspace-write") { - return `mode "${spec.mode}" is invalid; use read-only or workspace-write`; - } - if (origin === "mcp" && !spec.mode) return "mode is required for MCP-origin lanes"; - const pin = EFFORT_PINS.find((p) => p.match.test(model)); - if (pin && effort !== pin.level) { - return `model ${model} is pinned to effort ${pin.level}`; - } - if (!row.allowedEfforts.includes(effort)) { - return `model ${model} has unsupported effort ${effort}; use one of: ${row.allowedEfforts.join(", ")}`; - } - return null; -} - -/** Estimated USD for a usage delta on a model. */ -export function estimateCost(selector: string, tokensIn: number, tokensOut: number): number { - const row = findModel(selector); - if (!row) return 0; - return (tokensIn * row.inPerM + tokensOut * row.outPerM) / 1_000_000; -} diff --git a/packages/pi-kit/src/transcript.ts b/packages/pi-kit/src/transcript.ts deleted file mode 100644 index 7012f69..0000000 --- a/packages/pi-kit/src/transcript.ts +++ /dev/null @@ -1,168 +0,0 @@ -/** - * Lane transcript model: reduces a raw per-lane Pi JSONL stream (pi --mode json) - * into an ordered list of display entries (thinking, text, tool calls, results). - * Used by the lanes TUI for both live tailing and archived replay. - */ - -export type EntryKind = "task" | "thinking" | "text" | "tool" | "tool_result" | "info"; - -export interface TranscriptEntry { - kind: EntryKind; - /** Short label, e.g. tool name. */ - title?: string; - text: string; -} - -type JsonRecord = Record; - -function isRecord(value: unknown): value is JsonRecord { - return typeof value === "object" && value !== null; -} - -function firstText(content: unknown): string { - if (!Array.isArray(content)) return ""; - let text = ""; - for (const part of content) { - if (isRecord(part) && part.type === "text" && typeof part.text === "string") text += part.text; - } - return text; -} - -function inputText(input: unknown): string { - try { - return JSON.stringify(input) ?? ""; - } catch { - return String(input); - } -} - -const MAX_ENTRY_TEXT = 20_000; -const MAX_RESULT_TEXT = 4_000; - -export class LaneTranscript { - readonly entries: TranscriptEntry[] = []; - /** Bumped on every visible change, so views can cheaply detect updates. */ - version = 0; - private currentKey = ""; - - /** Feed one raw JSONL line from the lane's pi process. */ - feed(line: string): void { - if (!line.trim()) return; - let event: unknown; - try { - event = JSON.parse(line); - } catch { - return; - } - if (!isRecord(event) || typeof event.type !== "string") return; - if (event.v === 1 && this.handleNormalized(event)) return; - this.handlePi(event); - } - - feedChunk(chunk: string): void { - for (const line of chunk.split("\n")) this.feed(line); - } - - private push(kind: EntryKind, text: string, title?: string): void { - this.entries.push({ kind, text, ...(title !== undefined ? { title } : {}) }); - this.version++; - } - - private appendDelta(kind: "thinking" | "text", key: string, delta: string): void { - const last = this.entries[this.entries.length - 1]; - if (this.currentKey === key && last && last.kind === kind) { - if (last.text.length < MAX_ENTRY_TEXT) last.text += delta.slice(0, MAX_ENTRY_TEXT - last.text.length); - } else { - this.currentKey = key; - this.entries.push({ kind, text: delta.slice(0, MAX_ENTRY_TEXT) }); - } - this.version++; - } - - private handleNormalized(event: JsonRecord): boolean { - switch (event.type) { - case "task": - if (typeof event.text !== "string") return true; - if (this.entries.length === 0) this.push("task", event.text.slice(0, MAX_ENTRY_TEXT)); - return true; - case "thinking_delta": - if (typeof event.delta === "string") this.appendDelta("thinking", "normalized-thinking", event.delta); - return true; - case "text_delta": - if (typeof event.delta === "string") this.appendDelta("text", "normalized-text", event.delta); - return true; - case "tool_start": - if (typeof event.tool !== "string") return true; - this.currentKey = ""; - this.push("tool", inputText(event.input).slice(0, MAX_RESULT_TEXT), event.tool); - return true; - case "tool_end": - if (typeof event.tool !== "string" || typeof event.text !== "string") return true; - this.currentKey = ""; - this.push( - "tool_result", - event.text.slice(0, MAX_RESULT_TEXT), - event.isError === true ? `${event.tool} (error)` : event.tool, - ); - return true; - case "usage": - return true; - case "assistant_end": { - if (typeof event.text !== "string" || !event.text) return true; - const rendered = this.entries - .filter((entry) => entry.kind === "text") - .map((entry) => entry.text) - .join(""); - if (!rendered) this.push("text", event.text.slice(0, MAX_ENTRY_TEXT)); - else if (event.text.startsWith(rendered) && event.text.length > rendered.length) { - this.appendDelta("text", "normalized-text", event.text.slice(rendered.length)); - } - this.currentKey = ""; - return true; - } - default: - return false; - } - } - - private handlePi(event: JsonRecord): void { - if (event.type === "message_start" && isRecord(event.message) && event.message.role === "user") { - if (this.entries.length === 0) this.push("task", firstText(event.message.content).slice(0, MAX_ENTRY_TEXT)); - return; - } - - if (event.type === "message_update" && isRecord(event.assistantMessageEvent)) { - const update = event.assistantMessageEvent; - const index = typeof update.contentIndex === "number" ? update.contentIndex : 0; - if (update.type === "thinking_delta" && typeof update.delta === "string") { - this.appendDelta("thinking", `thinking:${index}`, update.delta); - } else if (update.type === "text_delta" && typeof update.delta === "string") { - this.appendDelta("text", `text:${index}`, update.delta); - } else if (update.type === "toolcall_end" && isRecord(update.toolCall)) { - const call = update.toolCall; - const name = typeof call.name === "string" ? call.name : "tool"; - let args = ""; - try { - args = JSON.stringify(call.arguments) ?? ""; - } catch { - args = String(call.arguments); - } - this.currentKey = ""; - this.push("tool", args.slice(0, MAX_RESULT_TEXT), name); - } - return; - } - - if (event.type === "tool_execution_end") { - const name = typeof event.toolName === "string" ? event.toolName : "tool"; - const text = isRecord(event.result) ? firstText(event.result.content) : ""; - this.currentKey = ""; - this.push("tool_result", text.slice(0, MAX_RESULT_TEXT), event.isError === true ? `${name} (error)` : name); - return; - } - - if (event.type === "agent_end") { - this.currentKey = ""; - } - } -} diff --git a/packages/pi-kit/test/btw-extension.test.ts b/packages/pi-kit/test/btw-extension.test.ts deleted file mode 100644 index 43b3829..0000000 --- a/packages/pi-kit/test/btw-extension.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; - -import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"; -import btwExtension from "../extensions/btw.ts"; -import { listRuns, readRun } from "../src/journal-core.ts"; - -const streamFixture = fileURLToPath(new URL("./fixtures/lane-stream.jsonl", import.meta.url)); -const originalDataDir = process.env.PIKIT_DATA_DIR; -const originalPath = process.env.PATH; - -type CommandHandler = (args: string, ctx: ExtensionCommandContext) => Promise; -type ShutdownHandler = () => Promise | void; - -function makeHarness() { - let handler: CommandHandler | undefined; - let shutdown: ShutdownHandler | undefined; - const sent: unknown[] = []; - const messages: Array<{ message: Record; options?: Record }> = []; - const notifications: Array<{ message: string; level: string }> = []; - const pi = { - registerCommand: (_name: string, command: { handler: CommandHandler }) => { - handler = command.handler; - }, - registerMessageRenderer: () => {}, - getThinkingLevel: () => "medium", - on: (name: string, listener: ShutdownHandler) => { - if (name === "session_shutdown") shutdown = listener; - }, - sendMessage: (message: Record, options?: Record) => messages.push({ message, options }), - sendUserMessage: (content: unknown) => sent.push(content), - } as unknown as ExtensionAPI; - const ctx = { - cwd: dataDir, - hasUI: false, - isIdle: () => false, - model: { provider: "openai-codex", id: "gpt-5.6-sol" }, - ui: { - notify: (message: string, level: string) => notifications.push({ message, level }), - }, - } as unknown as ExtensionCommandContext; - btwExtension(pi); - return { - invoke: (args: string) => { - if (!handler) throw new Error("btw command not registered"); - return handler(args, ctx); - }, - messages, - notifications, - sent, - shutdown: () => { - if (!shutdown) throw new Error("session_shutdown handler not registered"); - return shutdown(); - }, - }; -} - -let dataDir: string; - -beforeEach(async () => { - dataDir = await mkdtemp(join(tmpdir(), "pi-kit-btw-")); - process.env.PIKIT_DATA_DIR = dataDir; - const binary = join(dataDir, "pi"); - await writeFile( - binary, - `#!/usr/bin/env node\nconst { readFileSync } = require("node:fs");\nconst lines = readFileSync(${JSON.stringify(streamFixture)}, "utf8").trimEnd().split("\\n");\nsetTimeout(() => { for (const line of lines) console.log(line); }, 300);\n`, - "utf8", - ); - await chmod(binary, 0o755); - process.env.PATH = `${dataDir}:${originalPath}`; -}); - -afterEach(async () => { - if (originalDataDir === undefined) delete process.env.PIKIT_DATA_DIR; - else process.env.PIKIT_DATA_DIR = originalDataDir; - process.env.PATH = originalPath; - await rm(dataDir, { recursive: true, force: true }); -}); - -describe("btw extension", () => { - it("runs in a secondary background process without queueing a parent message", async () => { - const harness = makeHarness(); - const started = Date.now(); - - await harness.invoke("what is the answer?"); - - expect(Date.now() - started).toBeLessThan(250); - expect(harness.sent).toEqual([]); - expect(harness.messages).toEqual([]); - - const deadline = Date.now() + 5_000; - while (harness.messages.length === 0 && Date.now() < deadline) { - await new Promise((resolve) => setTimeout(resolve, 25)); - } - expect(harness.sent).toEqual([]); - expect(harness.messages).toHaveLength(1); - expect(harness.notifications).toContainEqual({ message: "btw done — Hello from lane.", level: "info" }); - expect(harness.messages[0]).toMatchObject({ - message: { - customType: "pi-btw", - content: "Hello from lane.", - display: true, - details: { - prompt: "what is the answer?", - answer: "Hello from lane.", - state: "done", - }, - }, - options: { deliverAs: "nextTurn" }, - }); - const run = listRuns()[0]!; - expect(readRun(run)).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: "lane_created", - spec: expect.objectContaining({ lane: "btw", mode: "read-only" }), - }), - ]), - ); - }); - - it("awaits background cleanup on session shutdown without delivering a stale answer", async () => { - const harness = makeHarness(); - await harness.invoke("what is the answer?"); - - await harness.shutdown(); - await new Promise((resolve) => setTimeout(resolve, 350)); - - expect(harness.messages).toEqual([]); - }); - - it("rejects an empty side question without starting work", async () => { - const harness = makeHarness(); - await harness.invoke(" "); - expect(harness.sent).toEqual([]); - expect(harness.messages).toEqual([]); - expect(harness.notifications).toContainEqual({ message: "Usage: /btw ", level: "warning" }); - }); -}); diff --git a/packages/pi-kit/test/child-env.test.ts b/packages/pi-kit/test/child-env.test.ts deleted file mode 100644 index 9d616f2..0000000 --- a/packages/pi-kit/test/child-env.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { createChildEnvironment } from "../src/child-env.ts"; - -describe("createChildEnvironment", () => { - it("keeps only the minimal runtime allowlist", () => { - const environment = createChildEnvironment({ - PATH: "/usr/bin", - HOME: "/home/lane", - USER: "lane", - LOGNAME: "lane", - SHELL: "/bin/zsh", - TMPDIR: "/tmp/lane", - LANG: "en_US.UTF-8", - LANGUAGE: "en_US", - LC_ALL: "en_US.UTF-8", - LC_CTYPE: "UTF-8", - TERM: "xterm-256color", - COLORTERM: "truecolor", - TERM_PROGRAM: "pi", - TERM_PROGRAM_VERSION: "1.0", - XDG_CONFIG_HOME: "/home/lane/.config", - XDG_CACHE_HOME: "/home/lane/.cache", - XDG_DATA_HOME: "/home/lane/.local/share", - XDG_STATE_HOME: "/home/lane/.local/state", - XDG_RUNTIME_DIR: "/tmp/lane-runtime", - OPENAI_API_KEY: "sentinel-key", - ANTHROPIC_AUTH_TOKEN: "sentinel-auth", - COOKIE: "sentinel-cookie", - AWS_ACCESS_KEY_ID: "sentinel-aws", - GOOGLE_APPLICATION_CREDENTIALS: "/sentinel/gcp.json", - AZURE_CLIENT_SECRET: "sentinel-azure", - SSH_AUTH_SOCK: "/sentinel/agent.sock", - }); - - expect(environment).toEqual({ - PATH: "/usr/bin", - HOME: "/home/lane", - USER: "lane", - LOGNAME: "lane", - SHELL: "/bin/zsh", - TMPDIR: "/tmp/lane", - LANG: "en_US.UTF-8", - LANGUAGE: "en_US", - LC_ALL: "en_US.UTF-8", - LC_CTYPE: "UTF-8", - TERM: "xterm-256color", - COLORTERM: "truecolor", - TERM_PROGRAM: "pi", - TERM_PROGRAM_VERSION: "1.0", - XDG_CONFIG_HOME: "/home/lane/.config", - XDG_CACHE_HOME: "/home/lane/.cache", - XDG_DATA_HOME: "/home/lane/.local/share", - XDG_STATE_HOME: "/home/lane/.local/state", - XDG_RUNTIME_DIR: "/tmp/lane-runtime", - }); - expect(Object.values(environment).some((value) => value?.startsWith("sentinel"))).toBe(false); - }); -}); diff --git a/packages/pi-kit/test/claude-code-adapter.test.ts b/packages/pi-kit/test/claude-code-adapter.test.ts deleted file mode 100644 index d881941..0000000 --- a/packages/pi-kit/test/claude-code-adapter.test.ts +++ /dev/null @@ -1,318 +0,0 @@ -import { access, chmod, mkdtemp, mkdir, readFile, realpath, rm, symlink, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { - CLAUDE_CODE_MIN_VERSION, - createClaudeCodeAdapter, -} from "../src/adapters/claude-code.ts"; -import type { LaneSpec } from "../src/schema.ts"; - -const fixture = fileURLToPath(new URL("./fixtures/claude-lane-stream.jsonl", import.meta.url)); -let sandbox: string; - -const baseSpec: LaneSpec = { - lane: "claude-worker", - task: "Inspect the workspace and report.", - model: "anthropic/claude-fable-5", - effort: "high", - mode: "read-only", -}; - -async function fakeClaude(directory: string, version = "2.1.216", body = ""): Promise { - await mkdir(directory, { recursive: true }); - const path = join(directory, "claude"); - await writeFile( - path, - `#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo \"${version} (Claude Code)\"; exit 0; fi\n${body}\n`, - "utf8", - ); - await chmod(path, 0o755); - return path; -} - -beforeEach(async () => { - sandbox = await mkdtemp(join(tmpdir(), "pi-kit-claude-adapter-")); -}); - -afterEach(async () => { - await rm(sandbox, { recursive: true, force: true }); -}); - -describe("Claude Code command policy", () => { - it("builds the complete read-only argv with the task after --", async () => { - const binary = await fakeClaude(join(sandbox, "bin")); - const adapter = createClaudeCodeAdapter({ binary }); - - await adapter.prepare(baseSpec); - const plan = adapter.build(baseSpec); - - expect(plan.command).toBe(await realpath(binary)); - expect(plan.args).toEqual([ - "-p", - "--safe-mode", - "--disable-slash-commands", - "--no-chrome", - "--no-session-persistence", - "--permission-mode", - "dontAsk", - "--output-format", - "stream-json", - "--verbose", - "--include-partial-messages", - "--model", - "fable", - "--effort", - "high", - "--tools", - "Read,Glob,Grep", - "--allowedTools", - "Read", - "Glob", - "Grep", - "--", - baseSpec.task, - ]); - expect(plan.cwd).toBe(process.cwd()); - }); - - it("builds unrestricted workspace-write tools without bespoke command policy", async () => { - const binary = await fakeClaude(join(sandbox, "bin")); - const adapter = createClaudeCodeAdapter({ binary }); - const task = "--dangerously-skip-permissions $(touch should-not-run)"; - - await adapter.prepare(baseSpec); - const plan = adapter.build({ ...baseSpec, task, mode: "workspace-write" }); - - expect(plan.args).toEqual([ - "-p", - "--safe-mode", - "--disable-slash-commands", - "--no-chrome", - "--no-session-persistence", - "--permission-mode", - "dontAsk", - "--output-format", - "stream-json", - "--verbose", - "--include-partial-messages", - "--model", - "fable", - "--effort", - "high", - "--tools", - "Read,Glob,Grep,Edit,Write,Bash", - "--allowedTools", - "Read", - "Glob", - "Grep", - "Edit", - "Write", - "Bash", - "--", - task, - ]); - expect(plan.args.slice(0, -1)).not.toContain("--dangerously-skip-permissions"); - expect(plan.args).not.toContain("--bare"); - expect(plan.args).not.toContain("bypassPermissions"); - expect(plan.args).not.toContain("max"); - expect(plan.args.at(-1)).toBe(task); - }); - - it("defaults existing Pi-origin specs to workspace-write", async () => { - const binary = await fakeClaude(join(sandbox, "bin")); - const adapter = createClaudeCodeAdapter({ binary }); - await adapter.prepare(baseSpec); - const plan = adapter.build({ ...baseSpec, mode: undefined }); - - expect(plan.args).toContain("Read,Glob,Grep,Edit,Write,Bash"); - }); - - it("does not inherit sensitive environment variables", async () => { - const binary = await fakeClaude(join(sandbox, "bin")); - const adapter = createClaudeCodeAdapter({ - binary, - env: { - PATH: "/usr/bin", - HOME: "/home/lane", - TERM: "xterm-256color", - ANTHROPIC_AUTH_TOKEN: "sentinel-auth", - COOKIE: "sentinel-cookie", - AWS_SESSION_TOKEN: "sentinel-aws", - GOOGLE_APPLICATION_CREDENTIALS: "/sentinel/gcp.json", - AZURE_CLIENT_SECRET: "sentinel-azure", - SSH_AUTH_SOCK: "/sentinel/agent.sock", - }, - }); - await adapter.prepare(baseSpec); - const plan = adapter.build(baseSpec); - - expect(plan.env).toEqual({ PATH: "/usr/bin", HOME: "/home/lane", TERM: "xterm-256color" }); - expect(Object.values(plan.env)).not.toContain("sentinel-auth"); - }); -}); - -describe("Claude Code executable preflight", () => { - it("resolves PATH candidates, skips unsafe wrappers, and returns a real executable", async () => { - await fakeClaude( - join(sandbox, "unsafe"), - "2.1.216", - "exec /usr/bin/false --permission-mode bypassPermissions \"$@\"", - ); - const safe = await fakeClaude(join(sandbox, "safe")); - const linkedDirectory = join(sandbox, "linked"); - await mkdir(linkedDirectory); - await symlink(safe, join(linkedDirectory, "claude")); - const adapter = createClaudeCodeAdapter({ - env: { ...process.env, PATH: [join(sandbox, "unsafe"), linkedDirectory].join(":") }, - }); - - await adapter.prepare(baseSpec); - expect(adapter.build(baseSpec).command).toBe(await realpath(safe)); - }); - - it("rejects dangerous wrappers and non-executable candidates", async () => { - await fakeClaude(join(sandbox, "unsafe"), "2.1.216", "exec claude --dangerously-skip-permissions \"$@\""); - const nonExecutable = join(sandbox, "plain", "claude"); - await mkdir(join(sandbox, "plain")); - await writeFile(nonExecutable, "#!/bin/sh\necho '2.1.216 (Claude Code)'\n", "utf8"); - const adapter = createClaudeCodeAdapter({ - env: { ...process.env, PATH: [join(sandbox, "unsafe"), join(sandbox, "plain")].join(":") }, - }); - - expect(() => adapter.preflight(baseSpec)).toThrow("No safe Claude Code executable"); - }); - - it.each([ - ["2.1.215", false], - [CLAUDE_CODE_MIN_VERSION, true], - ["2.1.217", true], - ])("handles Claude Code version %s", async (version, supported) => { - const binary = await fakeClaude(join(sandbox, version), version); - const adapter = createClaudeCodeAdapter({ binary }); - - if (supported) await expect(adapter.prepare(baseSpec)).resolves.toBeUndefined(); - else await expect(adapter.prepare(baseSpec)).rejects.toThrow(`requires ${CLAUDE_CODE_MIN_VERSION} or newer`); - }); - - it("runs version preparation asynchronously without invoking the provider", async () => { - const marker = join(sandbox, "version-started"); - const release = join(sandbox, "release-version"); - const provider = join(sandbox, "provider-invoked"); - const binary = join(sandbox, "claude"); - await writeFile( - binary, - `#!/usr/bin/env node\nconst { existsSync, writeFileSync } = require("node:fs");\nif (process.argv[2] === "--version") {\n writeFileSync(${JSON.stringify(marker)}, "started");\n const timer = setInterval(() => {\n if (existsSync(${JSON.stringify(release)})) {\n clearInterval(timer);\n console.log("2.1.216 (Claude Code)");\n process.exit(0);\n }\n }, 10);\n} else {\n writeFileSync(${JSON.stringify(provider)}, "invoked");\n}\n`, - "utf8", - ); - await chmod(binary, 0o755); - const adapter = createClaudeCodeAdapter({ binary }); - - let settled = false; - const preparation = adapter.prepare(baseSpec).finally(() => { - settled = true; - }); - for (let attempt = 0; attempt < 100; attempt++) { - try { - await access(marker); - break; - } catch { - await new Promise((resolve) => setTimeout(resolve, 10)); - } - } - await expect(access(marker)).resolves.toBeUndefined(); - expect(settled).toBe(false); - await expect(access(provider)).rejects.toMatchObject({ code: "ENOENT" }); - - await writeFile(release, "release", "utf8"); - await preparation; - expect(adapter.build(baseSpec).command).toBe(await realpath(binary)); - await expect(access(provider)).rejects.toMatchObject({ code: "ENOENT" }); - }); - - it("rejects prohibited efforts before command construction", async () => { - const binary = await fakeClaude(join(sandbox, "bin")); - const adapter = createClaudeCodeAdapter({ binary }); - - expect(() => adapter.build({ ...baseSpec, effort: "max" as LaneSpec["effort"] })).toThrow("unsupported effort"); - }); -}); - -describe("Claude Code stream parser", () => { - it("normalizes partial text, thinking, tool lifecycle, cumulative usage, and final result", async () => { - const lines = (await readFile(fixture, "utf8")).trimEnd().split("\n"); - const parser = createClaudeCodeAdapter({ binary: "/unused" }).createParser(baseSpec); - const events = lines.flatMap((line) => parser.feedLine(line)); - - expect(events[0]).toEqual({ v: 1, type: "task", text: baseSpec.task }); - expect(events).toContainEqual({ v: 1, type: "thinking_delta", delta: "Inspecting fixture." }); - expect(events.filter((event) => event.type === "text_delta")).toEqual([ - { v: 1, type: "text_delta", delta: "Adapter " }, - { v: 1, type: "text_delta", delta: "complete." }, - ]); - expect(events).toContainEqual({ - v: 1, - type: "tool_start", - tool: "Read", - input: { file_path: "README.md" }, - }); - expect(events).toContainEqual({ - v: 1, - type: "tool_end", - tool: "Read", - text: "fixture contents", - isError: false, - }); - expect(events.filter((event) => event.type === "usage")).toEqual([ - { v: 1, type: "usage", input: 70, output: 10, cacheRead: 20, context: 105 }, - { v: 1, type: "usage", input: 120, output: 40, cacheRead: 50, context: 230 }, - ]); - expect(events.at(-1)).toEqual({ v: 1, type: "assistant_end", text: "Adapter complete." }); - expect(parser.end()).toEqual([]); - }); - - it.each([ - { case: "is_error", subtype: "failure", is_error: true, result: `auth failed ${"x".repeat(500)}` }, - { case: "error subtype", subtype: "error_max_turns", is_error: false, result: "turn limit reached" }, - ])("normalizes a zero-exit $case result as a bounded failed assistant end", (result) => { - const parser = createClaudeCodeAdapter({ binary: "/unused" }).createParser(baseSpec); - - const events = parser.feedLine(JSON.stringify({ type: "result", ...result })); - const end = events.at(-1); - - expect(end).toMatchObject({ v: 1, type: "assistant_end", isError: true }); - expect(end && "text" in end ? end.text.length : 0).toBeLessThanOrEqual(240); - expect(parser.end()).toEqual([]); - }); - - it("turns malformed JSON into a bounded diagnostic event without throwing", () => { - const parser = createClaudeCodeAdapter({ binary: "/unused" }).createParser(baseSpec); - - const events = parser.feedLine(`{"type":"assistant","private":"${"x".repeat(2_000)}`); - const diagnostic = events.find((event) => event.type === "tool_end"); - - expect(diagnostic).toMatchObject({ v: 1, type: "tool_end", tool: "claude-code-parser", isError: true }); - expect(diagnostic && "text" in diagnostic ? diagnostic.text.length : 0).toBeLessThanOrEqual(240); - }); - - it("ignores unknown events and ends partial output once", () => { - const parser = createClaudeCodeAdapter({ binary: "/unused" }).createParser(baseSpec); - - expect(parser.feedLine('{"type":"future_event","payload":true}')).toEqual([ - { v: 1, type: "task", text: baseSpec.task }, - ]); - expect( - parser.feedLine( - '{"type":"stream_event","event":{"type":"content_block_delta","delta":{"type":"text_delta","text":"partial"}}}', - ), - ).toEqual([{ v: 1, type: "text_delta", delta: "partial" }]); - expect(parser.end()).toEqual([{ v: 1, type: "assistant_end", text: "partial" }]); - expect(parser.end()).toEqual([]); - }); - - it("emits only the task when an empty stream ends", () => { - const parser = createClaudeCodeAdapter({ binary: "/unused" }).createParser(baseSpec); - expect(parser.end()).toEqual([{ v: 1, type: "task", text: baseSpec.task }]); - }); -}); diff --git a/packages/pi-kit/test/delegation-extension.test.ts b/packages/pi-kit/test/delegation-extension.test.ts deleted file mode 100644 index 7401191..0000000 --- a/packages/pi-kit/test/delegation-extension.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -import { afterEach, beforeEach, describe, expect, it } from "vitest"; - -import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; -import delegation from "../extensions/delegation.ts"; -import { readRun } from "../src/journal-core.ts"; - -type Handler = (event: unknown, ctx: ExtensionContext) => void; - -function makeHarness(opts: { sessionId?: string; hasUI?: boolean } = {}) { - const handlers = new Map(); - const statuses: Array = []; - const ctx = { - hasUI: opts.hasUI ?? true, - ui: { - setStatus: (_key: string, text: string | undefined) => { - statuses.push(text); - }, - }, - sessionManager: { - getSessionId: () => opts.sessionId, - }, - } as unknown as ExtensionContext; - const pi = { - on: (name: string, handler: Handler) => { - handlers.set(name, handler); - }, - } as unknown as ExtensionAPI; - delegation(pi); - const emit = (name: string, event: unknown = {}) => handlers.get(name)?.(event, ctx); - return { emit, statuses }; -} - -function toolEvent(toolName: string, args: unknown = {}) { - return { toolName, args }; -} - -describe("delegation extension wiring", () => { - let dataDir: string; - - beforeEach(() => { - dataDir = mkdtempSync(join(tmpdir(), "pi-kit-delegation-")); - process.env.PIKIT_DATA_DIR = dataDir; - }); - - afterEach(() => { - delete process.env.PIKIT_DATA_DIR; - rmSync(dataDir, { recursive: true, force: true }); - }); - - it("shows no status until the activity threshold is reached", () => { - const { emit, statuses } = makeHarness({ sessionId: "abc" }); - emit("session_start"); - for (let i = 0; i < 4; i++) emit("tool_execution_start", toolEvent("bash")); - expect(statuses).toEqual([]); - emit("tool_execution_start", toolEvent("bash")); - expect(statuses).toEqual(["delegation 0% · 0 lanes / 5 direct"]); - }); - - it("counts lanes from lanes_spawn args in the status", () => { - const { emit, statuses } = makeHarness({ sessionId: "abc" }); - emit("session_start"); - for (let i = 0; i < 3; i++) emit("tool_execution_start", toolEvent("bash")); - emit("tool_execution_start", toolEvent("lanes_spawn", { lanes: [{}, {}, {}] })); - expect(statuses.at(-1)).toBe("delegation 50% · 3 lanes / 3 direct"); - }); - - it("journals the summary under a dedicated delegation lane at shutdown", () => { - const { emit } = makeHarness({ sessionId: "abc" }); - emit("session_start"); - emit("tool_execution_start", toolEvent("bash")); - emit("tool_execution_start", toolEvent("lanes_spawn", { lanes: [{}] })); - emit("session_shutdown"); - - const events = readRun("sess-abc"); - expect(events).toHaveLength(2); - expect(events[0]).toMatchObject({ type: "lane_created", lane: "delegation" }); - expect(events[1]).toMatchObject({ - type: "lane_status", - lane: "delegation", - text: "delegation 50% · 1 lane / 1 direct", - }); - }); - - it("writes nothing at shutdown without activity or session id", () => { - const idle = makeHarness({ sessionId: "idle" }); - idle.emit("session_start"); - idle.emit("session_shutdown"); - expect(readRun("sess-idle")).toEqual([]); - - const anonymous = makeHarness({ sessionId: undefined }); - anonymous.emit("session_start"); - anonymous.emit("tool_execution_start", toolEvent("bash")); - anonymous.emit("session_shutdown"); - expect(readRun("sess-undefined")).toEqual([]); - }); - - it("suppresses the status without a UI", () => { - const { emit, statuses } = makeHarness({ sessionId: "abc", hasUI: false }); - emit("session_start"); - for (let i = 0; i < 6; i++) emit("tool_execution_start", toolEvent("bash")); - expect(statuses).toEqual([]); - }); -}); diff --git a/packages/pi-kit/test/delegation.test.ts b/packages/pi-kit/test/delegation.test.ts deleted file mode 100644 index 8aeb5d5..0000000 --- a/packages/pi-kit/test/delegation.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { delegationRatio, emptyCounts, recordTool, summarize } from "../src/delegation-core.ts"; - -describe("delegation accounting", () => { - it("starts at zero and reports 0% without activity", () => { - const counts = emptyCounts(); - expect(delegationRatio(counts)).toBe(0); - expect(summarize(counts)).toBe("delegation 0% · 0 lanes / 0 direct"); - }); - - it("counts direct tool calls", () => { - let counts = emptyCounts(); - counts = recordTool(counts, "bash", 1); - counts = recordTool(counts, "read", 1); - expect(counts).toEqual({ direct: 2, dispatches: 0, lanes: 0 }); - expect(delegationRatio(counts)).toBe(0); - }); - - it("counts lanes per dispatch, not dispatches", () => { - let counts = emptyCounts(); - counts = recordTool(counts, "lanes_spawn", 3); - counts = recordTool(counts, "lanes_spawn", 1); - expect(counts).toEqual({ direct: 0, dispatches: 2, lanes: 4 }); - expect(delegationRatio(counts)).toBe(1); - }); - - it("treats a malformed lane count as one lane", () => { - const counts = recordTool(emptyCounts(), "lanes_spawn", 0); - expect(counts.lanes).toBe(1); - }); - - it("computes a mixed ratio and summary", () => { - let counts = emptyCounts(); - for (let i = 0; i < 6; i++) counts = recordTool(counts, "bash", 1); - counts = recordTool(counts, "lanes_spawn", 2); - expect(delegationRatio(counts)).toBe(0.25); - expect(summarize(counts)).toBe("delegation 25% · 2 lanes / 6 direct"); - }); - - it("pluralizes a single lane correctly", () => { - const counts = recordTool(emptyCounts(), "lanes_spawn", 1); - expect(summarize(counts)).toBe("delegation 100% · 1 lane / 0 direct"); - }); -}); diff --git a/packages/pi-kit/test/ember-footer.test.ts b/packages/pi-kit/test/ember-footer.test.ts deleted file mode 100644 index d4baf0b..0000000 --- a/packages/pi-kit/test/ember-footer.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -import type { - ExtensionAPI, - ExtensionContext, -} from "@earendil-works/pi-coding-agent"; -import emberFooter from "../extensions/ember-footer.ts"; - -type Handler = (event: unknown, ctx: ExtensionContext) => void | Promise; -type FooterFactory = ( - tui: { requestRender(): void }, - theme: Theme, - data: FooterData, -) => FooterComponent; -type FooterComponent = { render(width: number): string[]; dispose?(): void }; -type Theme = { fg(_color: string, text: string): string }; -type FooterData = { - onBranchChange(callback: () => void): () => void; - getGitBranch(): string | null; - getExtensionStatuses(): ReadonlyMap; -}; - -function usage(input: number, output: number, total = 0) { - return { - input, - output, - cacheRead: 0, - cacheWrite: 0, - reasoning: 0, - totalTokens: input + output, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total }, - }; -} - -function makeHarness( - opts: { - stalePricing?: boolean; - throwRender?: boolean; - throwTheme?: boolean; - throwFooterData?: boolean; - } = {}, -) { - const handlers = new Map(); - let footerFactory: FooterFactory | undefined; - let renders = 0; - const entries = [ - { - type: "message", - message: { - role: "assistant", - provider: "openai-codex", - model: "gpt-5.6-sol", - usage: usage(1_000, 500), - }, - }, - ]; - const pricedModel = { - provider: "openai-codex", - id: "gpt-5.6-sol", - cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 }, - }; - const staleModel = { - ...pricedModel, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - }; - const model = opts.stalePricing ? staleModel : pricedModel; - let registeredModel = model; - const ctx = { - hasUI: true, - model, - modelRegistry: { - find: (provider: string, id: string) => - provider === model.provider && id === model.id - ? registeredModel - : undefined, - refresh: async () => { - await Promise.resolve(); - registeredModel = pricedModel; - }, - isUsingOAuth: () => true, - }, - getContextUsage: () => ({ tokens: 10_000, contextWindow: 272_000 }), - sessionManager: { - getEntries: () => entries, - getBranch: () => entries, - getLeafId: () => "leaf", - }, - ui: { - setFooter: (factory: FooterFactory | undefined) => { - footerFactory = factory; - }, - notify: () => {}, - }, - } as unknown as ExtensionContext; - const pi = { - getThinkingLevel: () => "high", - on: (name: string, handler: Handler) => handlers.set(name, handler), - registerCommand: () => {}, - } as unknown as ExtensionAPI; - emberFooter(pi); - - const emit = async (name: string, event: unknown = {}) => { - await handlers.get(name)?.(event, ctx); - }; - const render = () => { - if (!footerFactory) throw new Error("footer not installed"); - const component = footerFactory( - { - requestRender: () => { - if (opts.throwRender) throw new Error("render unavailable"); - renders++; - }, - }, - { - fg: (_color, text) => { - if (opts.throwTheme) throw new Error("theme unavailable"); - return text; - }, - }, - { - onBranchChange: () => { - if (opts.throwFooterData) throw new Error("footer data unavailable"); - return () => {}; - }, - getGitBranch: () => null, - getExtensionStatuses: () => { - if (opts.throwFooterData) throw new Error("footer data unavailable"); - return new Map(); - }, - }, - ); - return component.render(200).join("\n"); - }; - - return { emit, render, getRenderRequests: () => renders }; -} - -describe("ember footer", () => { - beforeEach(() => { - vi.useFakeTimers(); - vi.setSystemTime(new Date("2026-07-21T12:00:00Z")); - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - it("recomputes missing persisted cost from the message model pricing", async () => { - const { emit, render } = makeHarness(); - await emit("session_start"); - - expect(render()).toContain("$0.02 est"); - }); - - it("refreshes stale model pricing when extensions reload", async () => { - const { emit, render } = makeHarness({ stalePricing: true }); - await emit("session_start"); - - expect(render()).toContain("$0.02 est"); - }); - - it("contains deferred footer factory and render failures", async () => { - const themeHarness = makeHarness({ throwTheme: true }); - await themeHarness.emit("session_start"); - expect(() => themeHarness.render()).not.toThrow(); - - const dataHarness = makeHarness({ throwFooterData: true }); - await dataHarness.emit("session_start"); - expect(() => dataHarness.render()).not.toThrow(); - }); - - it("contains timer and agent render callback failures", async () => { - const { emit, render } = makeHarness({ throwRender: true }); - await emit("session_start"); - render(); - - await expect(emit("agent_start")).resolves.toBeUndefined(); - expect(() => vi.advanceTimersByTime(1_000)).not.toThrow(); - await expect(emit("agent_settled")).resolves.toBeUndefined(); - }); - - it("times the whole agent run and retains the final worked duration", async () => { - const { emit, render, getRenderRequests } = makeHarness(); - await emit("session_start"); - expect(render()).toContain("session 00:00"); - expect(render()).not.toContain("working"); - expect(render()).not.toContain("worked"); - - emit("agent_start"); - vi.advanceTimersByTime(30_000); - emit("message_end", { message: { role: "assistant" } }); - vi.advanceTimersByTime(35_000); - expect(render()).toContain("session 01:05"); - expect(render()).toContain("working 01:05"); - expect(getRenderRequests()).toBeGreaterThan(0); - - emit("agent_settled"); - expect(render()).toContain("worked 01:05"); - expect(render()).not.toContain("working"); - vi.advanceTimersByTime(10_000); - expect(render()).toContain("worked 01:05"); - - emit("agent_start"); - expect(render()).toContain("working 00:00"); - expect(render()).not.toContain("worked"); - emit("session_shutdown"); - expect(vi.getTimerCount()).toBe(0); - }); -}); diff --git a/packages/pi-kit/test/execution-adapters.test.ts b/packages/pi-kit/test/execution-adapters.test.ts deleted file mode 100644 index 7163864..0000000 --- a/packages/pi-kit/test/execution-adapters.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { readFile } from "node:fs/promises"; -import { fileURLToPath } from "node:url"; -import { describe, expect, it } from "vitest"; -import { PiExecutionAdapter } from "../src/adapters/pi.ts"; -import type { LaneSpec } from "../src/schema.ts"; - -const fixturePath = fileURLToPath(new URL("./fixtures/lane-stream.jsonl", import.meta.url)); - -const baseSpec: LaneSpec = { - lane: "worker", - task: "Run the fixture task", - model: "openai-codex/gpt-5.6-sol", - effort: "medium", - mode: "read-only", - cwd: "/workspace with spaces", -}; - -describe("PiExecutionAdapter", () => { - it("builds the existing JSON invocation with an injectable binary and read-only tools", () => { - const adapter = new PiExecutionAdapter("/opt/pi test/bin/pi"); - - expect(adapter.build(baseSpec)).toMatchObject({ - command: "/opt/pi test/bin/pi", - cwd: "/workspace with spaces", - args: [ - "--mode", - "json", - "--no-extensions", - "--no-session", - "-p", - "--tools", - "read,grep,find,ls", - "--provider", - "openai-codex", - "--model", - "gpt-5.6-sol", - "--thinking", - "medium", - "Task:\nRun the fixture task", - ], - env: expect.objectContaining({ PIKIT_CHILD: "1" }), - }); - }); - - it.each(["@prompt.md", "-", "--dangerously-skip-permissions"])( - "prefixes positional task %s with a non-option literal", - (task) => { - const plan = new PiExecutionAdapter().build({ ...baseSpec, task }); - - expect(plan.args.at(-1)).toBe(`Task:\n${task}`); - expect(plan.args.at(-1)?.startsWith("Task:\n")).toBe(true); - }, - ); - - it("does not inherit sensitive environment variables", () => { - const plan = new PiExecutionAdapter("pi", { - PATH: "/usr/bin", - HOME: "/home/lane", - LANG: "en_US.UTF-8", - OPENAI_API_KEY: "sentinel-secret", - AWS_SECRET_ACCESS_KEY: "sentinel-aws", - GOOGLE_APPLICATION_CREDENTIALS: "/sentinel/gcp.json", - AZURE_CLIENT_SECRET: "sentinel-azure", - SSH_AUTH_SOCK: "/sentinel/agent.sock", - }).build(baseSpec); - - expect(plan.env).toEqual({ - PATH: "/usr/bin", - HOME: "/home/lane", - LANG: "en_US.UTF-8", - PIKIT_CHILD: "1", - }); - expect(Object.values(plan.env)).not.toContain("sentinel-secret"); - }); - - it("uses table runtime mappings for every Pi-routed model", () => { - const adapter = new PiExecutionAdapter(); - const cases = [ - ["openai-codex/gpt-5.6-sol", "medium", "openai-codex", "gpt-5.6-sol"], - ["xai/grok-4.5", "high", "xai", "grok-4.5"], - ["ollama/glm-5.2:cloud", "medium", "ollama", "glm-5.2:cloud"], - ] as const; - - for (const [selector, effort, provider, model] of cases) { - const args = adapter.build({ ...baseSpec, model: selector, effort }).args; - expect(args.slice(args.indexOf("--provider"), args.indexOf("--thinking"))).toEqual([ - "--provider", - provider, - "--model", - model, - ]); - } - }); - - it("uses normal harness tools for workspace-write mode", () => { - const plan = new PiExecutionAdapter().build({ ...baseSpec, mode: "workspace-write" }); - - expect(plan.args).toContain("--no-extensions"); - expect(plan.args).not.toContain("-e"); - expect(plan.args).not.toContain("--extension"); - expect(plan.args).not.toContain("--tools"); - }); - - it("normalizes Pi JSONL one line at a time", async () => { - const fixture = await readFile(fixturePath, "utf8"); - const parser = new PiExecutionAdapter().createParser(baseSpec); - const events = fixture.trimEnd().split("\n").flatMap((line) => parser.feedLine(line)); - events.push(...parser.feedLine("not json"), ...parser.end()); - - expect(events).toEqual([ - { v: 1, type: "task", text: "Run the fixture task" }, - { v: 1, type: "tool_start", tool: "bash", input: { command: "printf 'fixture tool output'" } }, - { v: 1, type: "text_delta", delta: "Hello from " }, - { v: 1, type: "text_delta", delta: "lane." }, - { v: 1, type: "usage", input: 1200, output: 300, cacheRead: 40, context: 1550 }, - { v: 1, type: "assistant_end", text: "Hello from lane." }, - ]); - }); - - it("normalizes thinking and tool results", () => { - const parser = new PiExecutionAdapter().createParser(baseSpec); - const lines = [ - JSON.stringify({ type: "message_update", assistantMessageEvent: { type: "thinking_delta", delta: "consider" } }), - JSON.stringify({ - type: "tool_execution_end", - toolName: "read", - result: { content: [{ type: "text", text: "first" }, { type: "text", text: " second" }] }, - isError: true, - }), - ]; - - expect(lines.flatMap((line) => parser.feedLine(line))).toEqual([ - { v: 1, type: "task", text: "Run the fixture task" }, - { v: 1, type: "thinking_delta", delta: "consider" }, - { v: 1, type: "tool_end", tool: "read", text: "first second", isError: true }, - ]); - }); -}); diff --git a/packages/pi-kit/test/fixtures/claude-lane-stream.jsonl b/packages/pi-kit/test/fixtures/claude-lane-stream.jsonl deleted file mode 100644 index b79c5ad..0000000 --- a/packages/pi-kit/test/fixtures/claude-lane-stream.jsonl +++ /dev/null @@ -1,17 +0,0 @@ -{"type":"system","subtype":"init","version":"2.1.216","tools":["Read","Glob","Grep"]} -{"type":"stream_event","event":{"type":"message_start","message":{"role":"assistant","content":[],"usage":{"input_tokens":0,"output_tokens":0}}}} -{"type":"stream_event","event":{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}} -{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"Inspecting fixture."}}} -{"type":"stream_event","event":{"type":"content_block_stop","index":0}} -{"type":"stream_event","event":{"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_fixture","name":"Read","input":{}}}} -{"type":"stream_event","event":{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"file_path\":\"README.md\"}"}}} -{"type":"stream_event","event":{"type":"content_block_stop","index":1}} -{"type":"assistant","message":{"role":"assistant","content":[{"type":"thinking","thinking":"Inspecting fixture."},{"type":"tool_use","id":"toolu_fixture","name":"Read","input":{"file_path":"README.md"}}],"usage":{"input_tokens":70,"output_tokens":10,"cache_creation_input_tokens":5,"cache_read_input_tokens":20}}} -{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_fixture","content":"fixture contents","is_error":false}]}} -{"type":"stream_event","event":{"type":"message_start","message":{"role":"assistant","content":[],"usage":{"input_tokens":0,"output_tokens":0}}}} -{"type":"stream_event","event":{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}} -{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Adapter "}}} -{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"complete."}}} -{"type":"stream_event","event":{"type":"content_block_stop","index":0}} -{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Adapter complete."}],"usage":{"input_tokens":50,"output_tokens":30,"cache_creation_input_tokens":15,"cache_read_input_tokens":30}}} -{"type":"result","subtype":"success","is_error":false,"result":"Adapter complete.","usage":{"input_tokens":120,"output_tokens":40,"cache_creation_input_tokens":20,"cache_read_input_tokens":50}} diff --git a/packages/pi-kit/test/fixtures/lane-stream.jsonl b/packages/pi-kit/test/fixtures/lane-stream.jsonl deleted file mode 100644 index 88ea009..0000000 --- a/packages/pi-kit/test/fixtures/lane-stream.jsonl +++ /dev/null @@ -1,8 +0,0 @@ -{"type":"session","version":3,"id":"fixture-session","timestamp":"2026-07-16T00:00:00.000Z","cwd":"/tmp"} -{"type":"agent_start"} -{"type":"tool_execution_start","toolCallId":"tool-1","toolName":"bash","args":{"command":"printf 'fixture tool output'"}} -{"type":"message_start","message":{"role":"assistant","content":[]}} -{"type":"message_update","message":{"role":"assistant","content":[{"type":"text","text":"Hello from "}]},"assistantMessageEvent":{"type":"text_delta","delta":"Hello from "}} -{"type":"message_update","message":{"role":"assistant","content":[{"type":"text","text":"Hello from lane."}]},"assistantMessageEvent":{"type":"text_delta","delta":"lane."}} -{"type":"message_end","message":{"role":"assistant","content":[{"type":"text","text":"Hello from lane."}],"usage":{"input":1200,"output":300,"cacheRead":40,"cacheWrite":10,"totalTokens":1550}}} -{"type":"agent_end","messages":[{"role":"assistant","content":[{"type":"text","text":"Hello from lane."}],"usage":{"input":1200,"output":300,"cacheRead":40,"cacheWrite":10,"totalTokens":1550}}]} diff --git a/packages/pi-kit/test/fixtures/mcp-lane-process.cjs b/packages/pi-kit/test/fixtures/mcp-lane-process.cjs deleted file mode 100644 index 46eee62..0000000 --- a/packages/pi-kit/test/fixtures/mcp-lane-process.cjs +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env node -const { spawn } = require("node:child_process"); -const { writeFileSync } = require("node:fs"); - -const processGroupFile = process.env.MCP_FIXTURE_PROCESS_GROUP_FILE; -const grandchildFile = process.env.MCP_FIXTURE_GRANDCHILD_FILE; -if (!processGroupFile || !grandchildFile) process.exit(64); - -const grandchild = spawn("sleep", ["30"], { stdio: "ignore" }); -writeFileSync(processGroupFile, String(process.pid)); -writeFileSync(grandchildFile, String(grandchild.pid)); -setInterval(() => {}, 1_000); diff --git a/packages/pi-kit/test/fixtures/normalized-lane-stream.jsonl b/packages/pi-kit/test/fixtures/normalized-lane-stream.jsonl deleted file mode 100644 index 45558cd..0000000 --- a/packages/pi-kit/test/fixtures/normalized-lane-stream.jsonl +++ /dev/null @@ -1,8 +0,0 @@ -{"v":1,"type":"task","text":"Run the normalized fixture task"} -{"v":1,"type":"thinking_delta","delta":"Inspecting fixture."} -{"v":1,"type":"tool_start","tool":"read","input":{"path":"README.md"}} -{"v":1,"type":"tool_end","tool":"read","text":"fixture contents","isError":false} -{"v":1,"type":"text_delta","delta":"Normalized "} -{"v":1,"type":"text_delta","delta":"answer."} -{"v":1,"type":"usage","input":120,"output":40,"cacheRead":50,"context":230} -{"v":1,"type":"assistant_end","text":"Normalized answer."} diff --git a/packages/pi-kit/test/gates.test.ts b/packages/pi-kit/test/gates.test.ts deleted file mode 100644 index 07799ef..0000000 --- a/packages/pi-kit/test/gates.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { decideGate } from "../extensions/gates.ts"; - -describe("decideGate — plan-only", () => { - it("blocks git push", () => { - expect(decideGate("plan-only", "git push origin main").block).toBe(true); - }); - - it("blocks git push with a leading env var assignment", () => { - expect(decideGate("plan-only", "VAR=1 git push origin main").block).toBe(true); - }); - - it("blocks git commit inside an && chain", () => { - expect(decideGate("plan-only", "cd x && git commit -m hi").block).toBe(true); - }); - - it("blocks gh pr create", () => { - expect(decideGate("plan-only", "gh pr create --title foo").block).toBe(true); - }); - - it("allows plain builds", () => { - expect(decideGate("plan-only", "npm run build").block).toBe(false); - }); - - it("allows git status", () => { - expect(decideGate("plan-only", "git status").block).toBe(false); - }); - - it("allows git diff", () => { - expect(decideGate("plan-only", "git diff HEAD~1").block).toBe(false); - }); - - it("includes a reason naming the verb and how to unblock", () => { - const decision = decideGate("plan-only", "git push"); - expect(decision.reason).toContain("git push"); - expect(decision.reason).toContain("/mode local or ship"); - }); -}); - -describe("decideGate — local", () => { - it("blocks git push", () => { - expect(decideGate("local", "git push origin main").block).toBe(true); - }); - - it("blocks gh pr merge", () => { - expect(decideGate("local", "gh pr merge 42").block).toBe(true); - }); - - it("allows git commit", () => { - expect(decideGate("local", "git commit -m hi").block).toBe(false); - }); -}); - -describe("decideGate — ship", () => { - it("allows git push", () => { - expect(decideGate("ship", "git push origin main").block).toBe(false); - }); - - it("allows gh pr merge", () => { - expect(decideGate("ship", "gh pr merge 42").block).toBe(false); - }); -}); - -describe("decideGate — catastrophic rm -rf, all modes", () => { - for (const mode of ["plan-only", "local", "ship"] as const) { - it(`blocks rm -rf / in ${mode} mode`, () => { - expect(decideGate(mode, "rm -rf /").block).toBe(true); - }); - - it(`allows rm -rf ./node_modules in ${mode} mode`, () => { - expect(decideGate(mode, "rm -rf ./node_modules").block).toBe(false); - }); - - it(`allows rm -rf /tmp/foo in ${mode} mode`, () => { - expect(decideGate(mode, "rm -rf /tmp/foo").block).toBe(false); - }); - } -}); - -describe("decideGate — review regressions", () => { - it("blocks git -C push in plan-only", () => { - expect(decideGate("plan-only", "git -C /some/repo push origin main").block).toBe(true); - }); - - it("blocks git --git-dir=.git push in local", () => { - expect(decideGate("local", "git --git-dir=.git push").block).toBe(true); - }); - - it("blocks git -c user.name=x commit in plan-only", () => { - expect(decideGate("plan-only", "git -c user.name=x commit -m hi").block).toBe(true); - }); - - it("allows git merge-base in plan-only", () => { - expect(decideGate("plan-only", "git merge-base main HEAD").block).toBe(false); - }); - - it("allows git push-to-checkout style subcommands only when not blocked verbs", () => { - expect(decideGate("local", "git push-to-checkout").block).toBe(false); - }); - - it("blocks absolute-path git binary push in local", () => { - expect(decideGate("local", "/usr/bin/git push").block).toBe(true); - }); - - for (const mode of ["plan-only", "local", "ship"] as const) { - it(`blocks rm -r / (no force) in ${mode}`, () => { - expect(decideGate(mode, "rm -r /").block).toBe(true); - }); - - it(`blocks rm -rf /* in ${mode}`, () => { - expect(decideGate(mode, "rm -rf /*").block).toBe(true); - }); - - it(`blocks rm --recursive $HOME/* in ${mode}`, () => { - expect(decideGate(mode, "rm --recursive $HOME/*").block).toBe(true); - }); - } - - it("still allows plain recursive rm on a project path chained after build", () => { - expect(decideGate("plan-only", "bun run build && rm -rf dist && mkdir dist").block).toBe(false); - }); -}); diff --git a/packages/pi-kit/test/grill-review.test.ts b/packages/pi-kit/test/grill-review.test.ts deleted file mode 100644 index 98911ea..0000000 --- a/packages/pi-kit/test/grill-review.test.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent"; -import grillReview from "../extensions/grill-review.ts"; - -type RegisteredTool = { - name: string; - promptSnippet?: string; - promptGuidelines?: string[]; - execute: ( - toolCallId: string, - params: Record, - signal: AbortSignal | undefined, - onUpdate: undefined, - ctx: ExtensionContext, - ) => Promise<{ content: Array<{ type: "text"; text: string }>; details?: unknown; isError?: boolean }>; -}; - -type RegisteredCommand = { - handler: (args: string, ctx: ExtensionCommandContext) => Promise; -}; - -type PlannotatorRequest = { - action: string; - payload: { gate?: boolean }; - respond: (response: unknown) => void; -}; - -function makeHarness(options: { - plannotatorLoaded?: boolean; - waitForIdle?: Promise; - helper?: { code: number; stdout: string } | "reject" | "pending"; -} = {}) { - let tool: RegisteredTool | undefined; - let command: RegisteredCommand | undefined; - let request: PlannotatorRequest | undefined; - const sent: Array<{ message: string; options?: { deliverAs?: string } }> = []; - const notifications: Array<{ message: string; level: string }> = []; - - const pi = { - registerTool: (definition: RegisteredTool) => { - tool = definition; - }, - registerCommand: (_name: string, definition: RegisteredCommand) => { - command = definition; - }, - getCommands: () => options.plannotatorLoaded === false - ? [{ name: "grill-review" }] - : [{ name: "grill-review" }, { name: "plannotator-last" }], - events: { - emit: (_channel: string, value: PlannotatorRequest) => { - request = value; - }, - }, - sendUserMessage: (message: string, sendOptions?: { deliverAs?: string }) => { - sent.push({ message, options: sendOptions }); - }, - exec: async (_command: string, _args: string[], execOptions?: { signal?: AbortSignal }) => { - if (options.helper === "reject") throw new Error("helper unavailable"); - if (options.helper === "pending") { - return new Promise((_, reject) => { - execOptions?.signal?.addEventListener("abort", () => reject(new Error("aborted")), { once: true }); - }); - } - return options.helper ?? { code: 0, stdout: "https://dev.tailnet.ts.net:19432/\n", stderr: "" }; - }, - } as unknown as ExtensionAPI; - - grillReview(pi); - - const ctx = { - waitForIdle: () => options.waitForIdle ?? Promise.resolve(), - ui: { - notify: (message: string, level: string) => { - notifications.push({ message, level }); - }, - }, - } as unknown as ExtensionCommandContext; - - return { - get tool() { - return tool!; - }, - get command() { - return command!; - }, - get request() { - return request!; - }, - ctx, - sent, - notifications, - }; -} - -function flushAsync(): Promise { - return new Promise((resolve) => setTimeout(resolve, 0)); -} - -describe("grill review extension", () => { - it("registers a model-visible tool and manual command", () => { - const harness = makeHarness(); - - expect(harness.tool.name).toBe("grill_review"); - expect(harness.tool.promptSnippet).toContain("question batch"); - expect(harness.tool.promptGuidelines?.join(" ")).toContain("Use grill_review"); - expect(harness.command).toBeDefined(); - }); - - it("returns feedback from Plannotator to the calling model", async () => { - const harness = makeHarness(); - const resultPromise = harness.tool.execute("tool-1", {}, undefined, undefined, harness.ctx); - await flushAsync(); - - expect(harness.notifications[0]).toEqual({ - message: "Open Plannotator: https://dev.tailnet.ts.net:19432/", - level: "info", - }); - expect(harness.request.action).toBe("annotate-last"); - expect(harness.request.payload.gate).toBe(true); - harness.request.respond({ status: "handled", result: { feedback: "Prefer option B" } }); - - const result = await resultPromise; - expect(result.isError).not.toBe(true); - expect(result.content[0]?.text).toContain("Prefer option B"); - }); - - it("fails clearly without advertising a URL when Plannotator is not loaded", async () => { - const harness = makeHarness({ plannotatorLoaded: false }); - - await expect(harness.tool.execute("tool-1", {}, undefined, undefined, harness.ctx)) - .rejects.toThrow("Plannotator is not loaded"); - expect(harness.notifications).toEqual([]); - }); - - it("throws when Plannotator reports a startup error", async () => { - const harness = makeHarness(); - const resultPromise = harness.tool.execute("tool-1", {}, undefined, undefined, harness.ctx); - await flushAsync(); - - harness.request.respond({ status: "error", error: "server failed" }); - await expect(resultPromise).rejects.toThrow("server failed"); - }); - - it("waits for an active turn to settle before reviewing and queues feedback safely", async () => { - let releaseIdle!: () => void; - const waitForIdle = new Promise((resolve) => { - releaseIdle = resolve; - }); - const harness = makeHarness({ waitForIdle }); - const commandPromise = harness.command.handler("", harness.ctx); - await flushAsync(); - - expect(harness.request).toBeUndefined(); - expect(harness.notifications).toEqual([]); - releaseIdle(); - await flushAsync(); - expect(harness.notifications[0]?.message).toContain("https://dev.tailnet.ts.net:19432/"); - harness.request.respond({ status: "handled", result: { feedback: "Split question 2" } }); - await commandPromise; - - expect(harness.sent).toEqual([{ - message: "Grill review feedback:\n\nSplit question 2", - options: { deliverAs: "followUp" }, - }]); - }); - - it("does not advertise a URL when cancelled while resolving it", async () => { - const harness = makeHarness({ helper: "pending" }); - const controller = new AbortController(); - const resultPromise = harness.tool.execute("tool-1", {}, controller.signal, undefined, harness.ctx); - - controller.abort(); - await expect(resultPromise).rejects.toThrow("cancelled before opening"); - expect(harness.notifications).toEqual([]); - expect(harness.request).toBeUndefined(); - }); - - it("keeps an opened review authoritative after the tool signal aborts", async () => { - const harness = makeHarness(); - const controller = new AbortController(); - let settled = false; - const resultPromise = harness.tool.execute("tool-1", {}, controller.signal, undefined, harness.ctx) - .then((result) => { - settled = true; - return result; - }); - await flushAsync(); - - controller.abort(); - await flushAsync(); - expect(settled).toBe(false); - - harness.request.respond({ status: "handled", result: { feedback: "Keep question 3" } }); - const result = await resultPromise; - expect(result.content[0]?.text).toContain("Keep question 3"); - }); - - it("does not start another turn when the batch is approved", async () => { - const harness = makeHarness(); - const commandPromise = harness.command.handler("", harness.ctx); - await flushAsync(); - harness.request.respond({ status: "handled", result: { feedback: "", approved: true } }); - await commandPromise; - - expect(harness.sent).toEqual([]); - expect(harness.notifications.at(-1)).toEqual({ message: "Grill review approved.", level: "info" }); - }); - - it("falls back to the loopback URL when plannotator-url fails", async () => { - const harness = makeHarness({ helper: "reject" }); - const commandPromise = harness.command.handler("", harness.ctx); - await flushAsync(); - - expect(harness.notifications[0]).toEqual({ - message: `Open Plannotator: http://localhost:${process.env.PLANNOTATOR_PORT || "19432"}/`, - level: "info", - }); - harness.request.respond({ status: "handled", result: { approved: true } }); - await commandPromise; - }); -}); diff --git a/packages/pi-kit/test/journal.test.ts b/packages/pi-kit/test/journal.test.ts deleted file mode 100644 index 47b2c3d..0000000 --- a/packages/pi-kit/test/journal.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { appendFileSync, mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -import { afterEach, beforeEach, describe, expect, it } from "vitest"; - -import { - appendEvent, - journalDir, - listRuns, - readRun, - reduceRun, -} from "../src/journal-core.ts"; -import type { KitEvent } from "../src/schema.ts"; - -function event( - run: string, - lane: string, - value: Type, -): Extract { - const base = { v: 1 as const, t: "2026-07-16T12:00:00.000Z", run, lane }; - let result: KitEvent; - switch (value) { - case "lane_created": result = { - ...base, - type: value, - spec: { - lane, - task: `work in ${lane}`, - model: "openai-codex/gpt-5.6-sol", - effort: "medium", - }, - }; break; - case "lane_start": result = { ...base, type: value, pid: 42 }; break; - case "lane_tool": result = { ...base, type: value, tool: "read", summary: "src/index.ts" }; break; - case "lane_usage": result = { ...base, type: value, input: 10, output: 5, cost: 0.1, context: 15 }; break; - case "lane_end": result = { ...base, type: value, ok: true, answer: "done", durationMs: 20 }; break; - case "lane_abandoned": result = { ...base, type: value, reason: "cancelled" }; break; - default: throw new Error(`unsupported event ${value}`); - } - return result as Extract; -} - -describe("journal core", () => { - let dataDir: string; - - beforeEach(() => { - dataDir = mkdtempSync(join(tmpdir(), "pi-kit-journal-")); - process.env.PIKIT_DATA_DIR = dataDir; - }); - - afterEach(() => { - delete process.env.PIKIT_DATA_DIR; - rmSync(dataDir, { recursive: true, force: true }); - }); - - it("appends and reads a realistic event sequence", () => { - const run = "run-sequence"; - const events: KitEvent[] = [ - { v: 1, t: "2026-07-16T12:00:00.000Z", run, type: "run_created", lanes: 1, origin: "test" }, - event(run, "lane-1", "lane_created"), - event(run, "lane-1", "lane_start"), - event(run, "lane-1", "lane_tool"), - event(run, "lane-1", "lane_usage"), - { ...event(run, "lane-1", "lane_usage"), input: 25, output: 9, cost: 0.25, context: 34 }, - event(run, "lane-1", "lane_end"), - ]; - - for (const item of events) appendEvent(item); - - expect(readRun(run)).toEqual(events); - }); - - it("lists only journal runs in newest-first order", () => { - appendEvent({ - v: 1, - t: "2026-07-16T12:00:00.000Z", - run: "run-20260716", - type: "run_created", - lanes: 0, - origin: "test", - }); - appendEvent({ - v: 1, - t: "2026-07-17T12:00:00.000Z", - run: "run-20260717", - type: "run_created", - lanes: 0, - origin: "test", - }); - appendFileSync(join(journalDir(), "notes.txt"), "not a run", "utf8"); - - expect(listRuns()).toEqual(["run-20260717", "run-20260716"]); - }); - - it("uses cumulative lane usage and reduces terminal states and totals", () => { - const run = "run-projection"; - const events: KitEvent[] = [ - { v: 1, t: "2026-07-16T12:00:00.000Z", run, type: "run_created", lanes: 3, origin: "test" }, - event(run, "done", "lane_created"), - event(run, "done", "lane_start"), - event(run, "done", "lane_usage"), - { ...event(run, "done", "lane_usage"), input: 30, output: 12, cost: 0.3, context: 42 }, - event(run, "done", "lane_end"), - event(run, "failed", "lane_created"), - { ...event(run, "failed", "lane_usage"), input: 7, output: 2, cost: 0.07, context: 9 }, - { ...event(run, "failed", "lane_end"), ok: false }, - event(run, "abandoned", "lane_created"), - { ...event(run, "abandoned", "lane_usage"), input: 3, output: 1, cost: 0.03, context: 4 }, - event(run, "abandoned", "lane_abandoned"), - { v: 1, t: "2026-07-16T12:01:00.000Z", run, type: "run_end", ok: false, durationMs: 60_000 }, - ]; - - const projection = reduceRun(events); - - expect(projection.lanes.get("done")).toMatchObject({ state: "done", tokensIn: 30, tokensOut: 12, cost: 0.3 }); - expect(projection.lanes.get("failed")?.state).toBe("failed"); - expect(projection.lanes.get("abandoned")?.state).toBe("abandoned"); - expect(projection).toMatchObject({ - ended: true, - ok: false, - totalTokensIn: 40, - totalTokensOut: 15, - totalCost: 0.4, - }); - expect(reduceRun(events)).toEqual(projection); - }); - - it("skips and counts corrupt lines", () => { - const run = "run-corrupt"; - const valid: KitEvent = { - v: 1, - t: "2026-07-16T12:00:00.000Z", - run, - type: "run_created", - lanes: 0, - origin: "test", - }; - appendEvent(valid); - appendFileSync(join(journalDir(), `${run}.jsonl`), "not-json\n", "utf8"); - - const read = readRun(run); - expect(read).toEqual([valid]); - expect(read.corruptLines).toBe(1); - }); -}); diff --git a/packages/pi-kit/test/lane-coordinator.test.ts b/packages/pi-kit/test/lane-coordinator.test.ts deleted file mode 100644 index 0bec062..0000000 --- a/packages/pi-kit/test/lane-coordinator.test.ts +++ /dev/null @@ -1,480 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - LaneCoordinator, - type LaneCoordinatorJournal, - type LaneRunnerPort, -} from "../src/lane-coordinator.ts"; -import type { LaneExecutionAdapter, NormalizedLaneEvent } from "../src/execution-adapter.ts"; -import { LaneRunner, type RunnerOptions } from "../src/runner.ts"; -import type { KitEvent, LaneProjection, LaneSpec, RunProjection } from "../src/schema.ts"; - -const solSpec: LaneSpec = { - lane: "worker", - task: "Run the fixture task", - model: "openai-codex/gpt-5.6-sol", - effort: "medium", -}; - -class FakeRunner implements LaneRunnerPort { - readonly dispatched = Promise.withResolvers(); - readonly finished = Promise.withResolvers(); - readonly abandoned: Array<{ lane?: string; reason: string }> = []; - readonly view: RunProjection; - shutdownCalls = 0; - private dispatchPromise?: Promise; - - constructor(readonly options: RunnerOptions) { - this.view = { - run: options.runId, - origin: options.origin ?? "pi", - createdAt: "runner-clock-must-not-leak", - ended: false, - lanes: new Map(), - totalCost: 0, - totalTokensIn: 0, - totalTokensOut: 0, - }; - } - - projection(): RunProjection { - return this.view; - } - - dispatch(specs: LaneSpec[]): Promise { - for (const spec of specs) { - this.view.lanes.set(spec.lane, { - spec, - state: "running", - pid: 123, - startedAtMs: Date.parse("2026-07-21T12:00:00.000Z"), - tokensIn: 10, - tokensOut: 2, - cost: 0.25, - context: 12, - }); - } - this.dispatched.resolve(); - this.dispatchPromise = this.finished.promise.then((lanes) => { - this.view.ended = true; - this.view.ok = lanes.every((lane) => lane.state === "done"); - return lanes; - }); - return this.dispatchPromise; - } - - settle(state: LaneProjection["state"] = "done"): void { - for (const lane of this.view.lanes.values()) lane.state = state; - this.finished.resolve([...this.view.lanes.values()]); - } - - fail(error: Error): void { - this.finished.reject(error); - } - - abandon(lane: string, reason: string): void { - this.abandoned.push({ lane, reason }); - const projection = this.view.lanes.get(lane); - if (projection) { - projection.state = "abandoned"; - projection.abandonReason = reason; - } - } - - abandonAll(reason: string): void { - this.abandoned.push({ reason }); - for (const lane of this.view.lanes.values()) { - if (lane.state === "queued" || lane.state === "running") { - lane.state = "abandoned"; - lane.abandonReason = reason; - } - } - } - - async shutdown(reason: string): Promise { - this.shutdownCalls++; - this.abandonAll(reason); - await this.dispatchPromise?.catch(() => {}); - } -} - -function captureUnhandledRejections(): { errors: unknown[]; stop: () => void } { - const errors: unknown[] = []; - const onUnhandledRejection = (error: unknown) => errors.push(error); - process.on("unhandledRejection", onUnhandledRejection); - return { - errors, - stop: () => process.off("unhandledRejection", onUnhandledRejection), - }; -} - -function harness(origin: "pi" | "mcp" = "pi") { - const events: KitEvent[] = []; - const runners: FakeRunner[] = []; - const journal: LaneCoordinatorJournal = { - append: (event) => events.push(event), - rawDir: (runId) => `/raw/${runId}`, - }; - let nowMs = Date.parse("2026-07-21T12:00:00.000Z"); - const coordinator = new LaneCoordinator({ - origin, - journal, - clock: () => new Date(nowMs), - newRunId: () => `run-${runners.length + 1}`, - createRunner: (options) => { - const runner = new FakeRunner(options); - runners.push(runner); - return runner; - }, - }); - return { - coordinator, - events, - runners, - advance(ms: number) { - nowMs += ms; - }, - }; -} - -describe("LaneCoordinator", () => { - it("spawns nonblocking, defaults Pi mode, journals through injected seams, and permits only one active run", async () => { - const { coordinator, events, runners, advance } = harness(); - - const first = await coordinator.spawn([solSpec]); - - expect(first).toMatchObject({ - run: "run-1", - state: "active", - durationMs: 0, - totals: { cost: 0, tokensIn: 0, tokensOut: 0 }, - lanes: [{ lane: "worker", model: solSpec.model, effort: "medium", mode: "workspace-write", state: "running" }], - }); - expect(runners[0]?.options).toMatchObject({ runId: "run-1", origin: "pi", rawDir: "/raw/run-1" }); - expect(events[0]).toEqual({ - v: 1, - t: "2026-07-21T12:00:00.000Z", - run: "run-1", - type: "run_created", - lanes: 1, - origin: "pi", - }); - await expect(coordinator.spawn([{ ...solSpec, lane: "second" }])).rejects.toThrow("still active"); - - advance(250); - expect(coordinator.status()).toMatchObject({ - durationMs: 250, - lanes: [{ lane: "worker", durationMs: 250 }], - }); - runners[0]!.settle(); - await coordinator.wait(); - expect(events.at(-1)).toMatchObject({ type: "run_end", ok: true, durationMs: 250 }); - - await coordinator.spawn([{ ...solSpec, lane: "second" }]); - expect(runners).toHaveLength(2); - runners[1]!.settle(); - await coordinator.wait(); - }); - - it("keeps spawn active when the run_created journal append throws", async () => { - const events: KitEvent[] = []; - const runners: FakeRunner[] = []; - const unhandled = captureUnhandledRejections(); - const coordinator = new LaneCoordinator({ - origin: "pi", - journal: { - append(event) { - if (event.type === "run_created") throw new Error("run_created journal unavailable"); - events.push(event); - }, - rawDir: () => "/raw/run-created-failure", - }, - newRunId: () => "run-created-failure", - createRunner: (options) => { - const runner = new FakeRunner(options); - runners.push(runner); - return runner; - }, - }); - - try { - await expect(coordinator.spawn([solSpec])).resolves.toMatchObject({ state: "active" }); - expect(runners).toHaveLength(1); - runners[0]!.settle(); - await expect(coordinator.wait()).resolves.toMatchObject({ state: "ended", ok: true }); - await new Promise((resolve) => setImmediate(resolve)); - expect(unhandled.errors).toEqual([]); - expect(events.at(-1)).toMatchObject({ type: "run_end", ok: true }); - } finally { - unhandled.stop(); - } - }); - - it("keeps settlement successful when the run_end journal append throws", async () => { - const runners: FakeRunner[] = []; - const unhandled = captureUnhandledRejections(); - const coordinator = new LaneCoordinator({ - origin: "pi", - journal: { - append(event) { - if (event.type === "run_end") throw new Error("run_end journal unavailable"); - }, - rawDir: () => "/raw/run-end-failure", - }, - newRunId: () => "run-end-failure", - createRunner: (options) => { - const runner = new FakeRunner(options); - runners.push(runner); - return runner; - }, - }); - - try { - await coordinator.spawn([solSpec]); - runners[0]!.settle(); - await expect(coordinator.wait()).resolves.toMatchObject({ state: "ended", ok: true }); - expect(coordinator.status()).toMatchObject({ state: "ended", ok: true }); - await new Promise((resolve) => setImmediate(resolve)); - expect(unhandled.errors).toEqual([]); - } finally { - unhandled.stop(); - } - }); - - it("keeps shutdown successful when its run_end journal append throws", async () => { - const runners: FakeRunner[] = []; - const unhandled = captureUnhandledRejections(); - const coordinator = new LaneCoordinator({ - origin: "pi", - journal: { - append(event) { - if (event.type === "run_end") throw new Error("shutdown journal unavailable"); - }, - rawDir: () => "/raw/shutdown-failure", - }, - newRunId: () => "shutdown-failure", - createRunner: (options) => { - const runner = new FakeRunner(options); - runners.push(runner); - return runner; - }, - }); - - try { - await coordinator.spawn([solSpec]); - const shutdown = coordinator.shutdown("session ended"); - runners[0]!.settle("abandoned"); - await expect(shutdown).resolves.toBeUndefined(); - expect(coordinator.status()).toMatchObject({ state: "ended", ok: false }); - await new Promise((resolve) => setImmediate(resolve)); - expect(unhandled.errors).toEqual([]); - } finally { - unhandled.stop(); - } - }); - - it("returns while runner preparation is pending and keeps status and wait responsive", async () => { - const preparation = Promise.withResolvers(); - const events: KitEvent[] = []; - const adapter: LaneExecutionAdapter = { - route: "pi", - preflight() {}, - async prepare() { - await preparation.promise; - }, - build(spec) { - return { - command: process.execPath, - args: ["-e", 'console.log(JSON.stringify({v:1,type:"assistant_end",text:"prepared"}))'], - cwd: spec.cwd ?? process.cwd(), - env: process.env, - }; - }, - createParser() { - return { - feedLine(line) { - return [JSON.parse(line) as NormalizedLaneEvent]; - }, - end() { - return []; - }, - }; - }, - }; - const coordinator = new LaneCoordinator({ - origin: "pi", - journal: { append: (event) => events.push(event), rawDir: () => "/unused" }, - newRunId: () => "run-preparing", - createRunner: (options) => new LaneRunner({ - runId: options.runId, - append: options.append, - origin: options.origin, - adapters: [adapter], - }), - }); - - const spawned = await Promise.race([ - coordinator.spawn([solSpec]), - new Promise((_, reject) => setTimeout(() => reject(new Error("spawn blocked on preparation")), 250)), - ]); - - expect(spawned).toMatchObject({ state: "active", lanes: [{ state: "queued" }] }); - expect(coordinator.status()).toMatchObject({ state: "active", lanes: [{ state: "queued" }] }); - const controller = new AbortController(); - const detachedWait = coordinator.wait(controller.signal); - controller.abort(); - await expect(detachedWait).resolves.toMatchObject({ state: "active", lanes: [{ state: "queued" }] }); - expect(events.some((event) => event.type === "lane_start")).toBe(false); - - preparation.resolve(); - await expect(coordinator.wait()).resolves.toMatchObject({ - state: "ended", - ok: true, - lanes: [{ state: "done", answer: "prepared" }], - }); - expect(events.some((event) => event.type === "lane_start")).toBe(true); - }); - - it("requires explicit MCP mode and rejects MCP Anthropic lanes through existing policy", async () => { - const { coordinator, runners } = harness("mcp"); - - await expect(coordinator.spawn([solSpec])).rejects.toThrow("mode is required for MCP-origin lanes"); - await expect( - coordinator.spawn([ - { - lane: "anthropic", - task: "Do not run", - model: "anthropic/claude-fable-5", - effort: "high", - mode: "read-only", - }, - ]), - ).rejects.toThrow("unavailable from mcp"); - expect(runners).toHaveLength(0); - }); - - it("returns exact whole-run and filtered JSON-safe DTOs without sensitive process or prompt fields", async () => { - const { coordinator, runners } = harness(); - await coordinator.spawn([ - { ...solSpec, cwd: "/sensitive/worktree", rationale: "private routing reason" }, - { ...solSpec, lane: "other" }, - ]); - const projection = runners[0]!.view; - projection.totalCost = Number.POSITIVE_INFINITY; - projection.totalTokensIn = Number.NaN; - projection.totalTokensOut = Number.NEGATIVE_INFINITY; - const worker = projection.lanes.get("worker")!; - worker.tokensIn = Number.NaN; - worker.tokensOut = Number.POSITIVE_INFINITY; - worker.cost = Number.NEGATIVE_INFINITY; - worker.context = Number.NaN; - worker.durationMs = Number.POSITIVE_INFINITY; - - const all = coordinator.status(); - const filtered = coordinator.status("worker"); - - expect(all.lanes.map((lane) => lane.lane)).toEqual(["worker", "other"]); - expect(filtered).toEqual({ - run: "run-1", - state: "active", - durationMs: 0, - totals: { cost: 0, tokensIn: 0, tokensOut: 0 }, - lanes: [{ - lane: "worker", - model: solSpec.model, - effort: "medium", - mode: "workspace-write", - state: "running", - tokensIn: 0, - tokensOut: 0, - cost: 0, - context: 0, - durationMs: 0, - }], - }); - const json = JSON.stringify(filtered); - expect(json).not.toContain("Run the fixture task"); - expect(json).not.toContain("/sensitive/worktree"); - expect(json).not.toContain("private routing reason"); - for (const field of ["task", "cwd", "rationale", "pid", "origin", "createdAt"]) { - expect(filtered).not.toHaveProperty(field); - expect(filtered.lanes[0]).not.toHaveProperty(field); - } - expect(JSON.parse(json)).toEqual(filtered); - await expect(Promise.resolve().then(() => coordinator.status("missing"))).rejects.toThrow('Unknown lane "missing"'); - - runners[0]!.settle(); - await coordinator.wait(); - }); - - it("waits for settlement but detaches an aborted wait without abandoning lanes", async () => { - const { coordinator, runners } = harness(); - await coordinator.spawn([solSpec]); - const controller = new AbortController(); - - const detached = coordinator.wait(controller.signal); - controller.abort(); - - await expect(detached).resolves.toMatchObject({ state: "active", lanes: [{ state: "running" }] }); - expect(runners[0]!.abandoned).toEqual([]); - - const settled = coordinator.wait(); - runners[0]!.settle(); - await expect(settled).resolves.toMatchObject({ state: "ended", ok: true, lanes: [{ state: "done" }] }); - }); - - it("abandons a named lane or the whole run and rejects operations without a run", async () => { - const empty = harness().coordinator; - expect(() => empty.status()).toThrow("No lane run"); - await expect(empty.wait()).rejects.toThrow("No lane run"); - expect(() => empty.abandon({})).toThrow("No lane run"); - - const { coordinator, runners } = harness(); - await coordinator.spawn([solSpec, { ...solSpec, lane: "other" }]); - - expect(coordinator.abandon({ lane: "worker", reason: "named stop" })).toMatchObject({ - lanes: [{ lane: "worker", state: "abandoned" }, { lane: "other", state: "running" }], - }); - expect(() => coordinator.abandon({ lane: "missing" })).toThrow('Unknown lane "missing"'); - expect(coordinator.abandon({ reason: "all stop" }).lanes).toEqual( - expect.arrayContaining([expect.objectContaining({ state: "abandoned", abandonReason: expect.any(String) })]), - ); - expect(runners[0]!.abandoned).toEqual([ - { lane: "worker", reason: "named stop" }, - { reason: "all stop" }, - ]); - - runners[0]!.settle("abandoned"); - await coordinator.wait(); - }); - - it("settles and journals a failed run when dispatch rejects", async () => { - const { coordinator, events, runners } = harness(); - await coordinator.spawn([solSpec]); - - runners[0]!.fail(new Error("child setup failed")); - - await expect(coordinator.wait()).resolves.toMatchObject({ state: "ended", ok: false }); - expect(runners[0]!.shutdownCalls).toBe(1); - expect(events.at(-1)).toMatchObject({ type: "run_end", ok: false }); - }); - - it("shutdown abandons and awaits the active runner close", async () => { - const { coordinator, runners } = harness(); - await coordinator.spawn([solSpec]); - - let stopped = false; - const shutdown = coordinator.shutdown("session ended").then(() => { - stopped = true; - }); - await Promise.resolve(); - - expect(stopped).toBe(false); - expect(runners[0]!.shutdownCalls).toBe(1); - expect(runners[0]!.abandoned).toEqual([{ reason: "session ended" }]); - - runners[0]!.settle("abandoned"); - await shutdown; - expect(stopped).toBe(true); - expect(coordinator.status()).toMatchObject({ state: "ended", ok: false }); - }); -}); diff --git a/packages/pi-kit/test/lanes-extension.test.ts b/packages/pi-kit/test/lanes-extension.test.ts deleted file mode 100644 index ec92773..0000000 --- a/packages/pi-kit/test/lanes-extension.test.ts +++ /dev/null @@ -1,445 +0,0 @@ -import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; - -import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; -import lanesExtension, { type LanesCoordinatorPort } from "../extensions/lanes.ts"; -import { readRun } from "../src/journal-core.ts"; -import type { RunSnapshotDto } from "../src/lane-coordinator.ts"; -import type { LaneSpec } from "../src/schema.ts"; -import { MODEL_TABLE } from "../src/table.ts"; - -const streamFixture = fileURLToPath(new URL("./fixtures/lane-stream.jsonl", import.meta.url)); -const originalDataDir = process.env.PIKIT_DATA_DIR; -const originalPath = process.env.PATH; - -interface ToolResult { - content: Array<{ type: string; text: string }>; - details?: unknown; - isError?: boolean; -} - -type ToolExecute = ( - toolCallId: string, - params: Record, - signal: AbortSignal | undefined, - onUpdate: undefined, - ctx: ExtensionContext, -) => Promise; - -type CommandHandler = (args: string, ctx: ExtensionContext) => Promise; -type ShutdownHandler = () => Promise | void; - -function makeHarness( - opts: { - idle?: boolean; - hasUI?: boolean; - ui?: ExtensionContext["ui"]; - createCoordinator?: () => LanesCoordinatorPort; - } = {}, -) { - const tools = new Map(); - const commands = new Map(); - const handlers = new Map(); - const sent: Array<{ content: string; options?: unknown }> = []; - const pi = { - registerTool: (tool: { name: string; description: string; execute: ToolExecute }) => { - tools.set(tool.name, tool); - }, - registerCommand: (name: string, command: { handler: CommandHandler }) => commands.set(name, command.handler), - on: (name: string, handler: ShutdownHandler) => handlers.set(name, handler), - sendUserMessage: (content: string, options?: unknown) => { - sent.push({ content, options }); - }, - } as unknown as ExtensionAPI; - const ctx = { - hasUI: opts.hasUI ?? false, - isIdle: () => opts.idle ?? true, - ui: opts.ui ?? {}, - } as unknown as ExtensionContext; - lanesExtension(pi, { createCoordinator: opts.createCoordinator }); - const call = (name: string, params: Record = {}, signal?: AbortSignal) => { - const tool = tools.get(name); - if (!tool) throw new Error(`tool ${name} not registered`); - return tool.execute("tc", params, signal, undefined, ctx); - }; - return { call, commands, ctx, handlers, sent, tools }; -} - -class FaultCoordinator implements LanesCoordinatorPort { - snapshot?: RunSnapshotDto; - fail?: "spawn" | "status" | "wait" | "abandon" | "shutdown"; - shutdownImpl: (reason: string) => Promise = async () => {}; - - async spawn(specs: LaneSpec[]): Promise { - if (this.fail === "spawn") throw new Error("injected spawn failure"); - this.snapshot = { - run: "run-injected", - state: "active", - durationMs: 65_000, - totals: { cost: 0, tokensIn: 0, tokensOut: 0 }, - lanes: specs.map((lane) => ({ - lane: lane.lane, - model: lane.model, - effort: lane.effort, - mode: lane.mode ?? "workspace-write", - state: "running", - durationMs: 5_000, - tokensIn: 0, - tokensOut: 0, - cost: 0, - context: 0, - })), - }; - return this.snapshot; - } - - status(lane?: string): RunSnapshotDto { - if (this.fail === "status") throw new Error("injected status failure"); - const snapshot = this.requireSnapshot(); - if (lane === undefined) return snapshot; - const found = snapshot.lanes.find((item) => item.lane === lane); - if (!found) throw new Error(`Unknown lane "${lane}"`); - return { ...snapshot, lanes: [found] }; - } - - async wait(_signal?: AbortSignal): Promise { - if (this.fail === "wait") throw new Error("injected wait failure"); - return new Promise(() => {}); - } - - abandon(input: { lane?: string; reason?: string }): RunSnapshotDto { - if (this.fail === "abandon") throw new Error("injected abandon failure"); - const snapshot = this.requireSnapshot(); - for (const lane of snapshot.lanes) { - if (input.lane === undefined || input.lane === lane.lane) { - lane.state = "abandoned"; - lane.abandonReason = input.reason ?? "abandoned"; - } - } - return snapshot; - } - - shutdown(reason: string): Promise { - if (this.fail === "shutdown") return Promise.reject(new Error("injected shutdown failure")); - return this.shutdownImpl(reason); - } - - private requireSnapshot(): RunSnapshotDto { - if (!this.snapshot) throw new Error("No lane run is available"); - return this.snapshot; - } -} - -let dataDir: string; - -async function fixtureBinary(name: string, source: string): Promise { - const path = join(dataDir, name); - await writeFile(path, source, "utf8"); - await chmod(path, 0o755); -} - -/** Fake `pi` on PATH that replays the fixture stream after a short delay. */ -async function installSlowPi(delayMs: number): Promise { - await fixtureBinary( - "pi", - `#!/usr/bin/env node\nconst { readFileSync } = require("node:fs");\nconst lines = readFileSync(${JSON.stringify(streamFixture)}, "utf8").trimEnd().split("\\n");\nsetTimeout(() => { for (const line of lines) console.log(line); }, ${delayMs});\n`, - ); - process.env.PATH = `${dataDir}:${originalPath}`; -} - -const spec = { lane: "worker", task: "fixture task", model: "openai-codex/gpt-5.6-sol", effort: "medium" }; - -async function waitFor(check: () => boolean, timeoutMs = 5_000): Promise { - const deadline = Date.now() + timeoutMs; - while (!check()) { - if (Date.now() > deadline) throw new Error("waitFor timeout"); - await new Promise((resolve) => setTimeout(resolve, 25)); - } -} - - -/** Poll until the coordinated run has settled, then return lanes_wait results. */ -async function collectWhenSettled( - call: (name: string, params?: Record, signal?: AbortSignal) => Promise, - timeoutMs = 5_000, -): Promise { - let lastStatus = ""; - const deadline = Date.now() + timeoutMs; - while (Date.now() <= deadline) { - const status = await call("lanes_status", {}); - lastStatus = status.content[0]?.text ?? ""; - if (!status.isError && lastStatus.includes("(ended)")) { - return call("lanes_wait", {}); - } - await new Promise((resolve) => setTimeout(resolve, 25)); - } - throw new Error(`collectWhenSettled timeout: ${lastStatus || "no status"}`); -} - -beforeEach(async () => { - dataDir = await mkdtemp(join(tmpdir(), "pi-kit-lanes-ext-")); - process.env.PIKIT_DATA_DIR = dataDir; -}); - -afterEach(async () => { - if (originalDataDir === undefined) delete process.env.PIKIT_DATA_DIR; - else process.env.PIKIT_DATA_DIR = originalDataDir; - process.env.PATH = originalPath; - await rm(dataDir, { recursive: true, force: true }); -}); - -describe("async lanes extension", () => { - it("keeps the four public tool names and descriptions frozen", () => { - const { tools } = makeHarness(); - expect([...tools.keys()]).toEqual(["lanes_spawn", "lanes_status", "lanes_wait", "lanes_abandon"]); - expect(tools.get("lanes_spawn")!.description).toBe( - `Spawn independent Pi lanes that run concurrently in the background (non-blocking: this tool returns immediately; lanes survive turn cancellation). Never wait on an active run: finish the parent turn so the user can keep messaging, inspect progress with lanes_status only when needed, and collect results after the run settles with lanes_wait or lanes_status. Stop lanes only with lanes_abandon. You MUST state an explicit model and effort for every lane. Any effort off|minimal|low|medium|high|xhigh is allowed per lane (the value in parentheses is only that model's starting prior). Models: ${MODEL_TABLE.map((row) => `${row.selector} (prior ${row.prior})`).join(", ")}.`, - ); - expect(tools.get("lanes_status")!.description).toBe( - "Report live status of the current lane run without blocking: per-lane state, current activity, tokens, cost, and (for settled lanes) answers. Pass `lane` for a detailed view of one lane — use this when the user asks about a specific lane. Do not call in a polling loop.", - ); - expect(tools.get("lanes_wait")!.description).toBe( - "Return final results only after the current lane run has settled. This tool never blocks an active parent session: while lanes are running it returns immediately so the model can finish the turn and the user can keep messaging.", - ); - expect(tools.get("lanes_abandon")!.description).toBe( - "Explicitly stop lanes of the current run. Pass `lane` to stop one lane, omit it to stop all. This is the only way to stop lanes — they are not cancelled by ESC or turn cancellation.", - ); - }); - - it("lanes_spawn returns immediately while lanes keep running", async () => { - await installSlowPi(300); - const { call } = makeHarness(); - const started = Date.now(); - const result = await call("lanes_spawn", { lanes: [spec] }); - expect(Date.now() - started).toBeLessThan(250); - expect(result.isError).toBeFalsy(); - const run = (result.details as { run: string }).run; - expect(result.content[0]!.text).toBe( - [ - `Run ${run} spawned with 1 lane (non-blocking):`, - " worker: openai-codex/gpt-5.6-sol:medium", - "Lanes run in the background and survive turn cancellation.", - "Finish the parent turn now so the user can keep messaging while lanes run.", - "Use lanes_status only when progress is needed; collect results after completion with lanes_wait or lanes_status; use lanes_abandon to stop lanes.", - ].join("\n"), - ); - expect(readRun(run)).toEqual( - expect.arrayContaining([ - expect.objectContaining({ type: "run_created", origin: "pi" }), - expect.objectContaining({ type: "lane_created", spec: expect.objectContaining({ mode: "workspace-write" }) }), - ]), - ); - - const status = await call("lanes_status", {}); - expect(status.content[0]!.text).toMatch(/worker: (queued|running)/); - - const final = await collectWhenSettled(call); - expect(final.isError).toBeFalsy(); - expect(final.content[0]!.text).toContain("worker: done"); - expect(final.content[0]!.text).toContain("Hello from lane."); - }); - - it("renders live whole-run and per-lane durations in the widget and status summary", async () => { - const coordinator = new FaultCoordinator(); - const widgets: string[][] = []; - const ui = { - theme: { fg: (_color: string, text: string) => text }, - setWidget: (_id: string, lines?: string[]) => { if (lines) widgets.push(lines); }, - setStatus: () => {}, - } as unknown as ExtensionContext["ui"]; - const { call, handlers } = makeHarness({ hasUI: true, ui, createCoordinator: () => coordinator }); - - await call("lanes_spawn", { lanes: [spec] }); - const rendered = widgets.at(-1)!.join("\n"); - expect(rendered).toContain("1m05s"); - expect(rendered).toContain("5s"); - - const status = await call("lanes_status", {}); - expect(status.content[0]!.text).toContain("(active) · 1m05s"); - expect(status.content[0]!.text).toContain("$0.0000 · 5s"); - await handlers.get("session_shutdown")!(); - }); - - it("lanes_status with a lane filter returns the detailed single-lane view", async () => { - await installSlowPi(10); - const { call } = makeHarness(); - await call("lanes_spawn", { lanes: [spec] }); - await collectWhenSettled(call); - - const detail = await call("lanes_status", { lane: "worker" }); - expect(detail.content[0]!.text).toContain("worker: done"); - - const unknown = await call("lanes_status", { lane: "nope" }); - expect(unknown.isError).toBe(true); - expect(unknown.content[0]!.text).toContain('Unknown lane "nope"'); - expect(unknown.content[0]!.text).toContain("worker"); - }); - - it("rejects a second spawn while a run is active, allows it after settlement", async () => { - await installSlowPi(300); - const { call } = makeHarness(); - await call("lanes_spawn", { lanes: [spec] }); - const second = await call("lanes_spawn", { lanes: [spec] }); - expect(second.isError).toBe(true); - expect(second.content[0]!.text).toContain("still active"); - - await collectWhenSettled(call); - const third = await call("lanes_spawn", { lanes: [{ ...spec, lane: "again" }] }); - expect(third.isError).toBeFalsy(); - await collectWhenSettled(call); - }); - - it("lanes_wait never blocks the parent session while lanes are active", async () => { - await installSlowPi(400); - const { call } = makeHarness(); - await call("lanes_spawn", { lanes: [spec] }); - - const started = Date.now(); - const active = await call("lanes_wait", {}); - expect(Date.now() - started).toBeLessThan(250); - expect(active.content[0]!.text).toContain("keeps running in the background"); - - const final = await collectWhenSettled(call); - expect(final.content[0]!.text).toContain("worker: done"); - }); - - it("lanes_abandon stops a named lane and reports unknown lanes", async () => { - await installSlowPi(2_000); - const { call } = makeHarness(); - await call("lanes_spawn", { lanes: [spec] }); - - const unknown = await call("lanes_abandon", { lane: "nope" }); - expect(unknown.isError).toBe(true); - - const result = await call("lanes_abandon", { lane: "worker", reason: "test stop" }); - expect(result.content[0]!.text).toContain("worker: abandoned"); - const final = await collectWhenSettled(call); - expect(final.isError).toBe(true); - expect(final.content[0]!.text).toContain("test stop"); - }); - - it("does not start a parent turn when a background run settles", async () => { - await installSlowPi(50); - const { call, sent } = makeHarness({ idle: true }); - await call("lanes_spawn", { lanes: [spec] }); - await collectWhenSettled(call); - expect(sent).toEqual([]); - }); -}); - -describe("failure containment", () => { - it.each(["spawn", "status", "abandon"] as const)( - "returns a concise isError tool result when coordinator %s throws", - async (operation) => { - const coordinator = new FaultCoordinator(); - coordinator.fail = operation; - const { call } = makeHarness({ createCoordinator: () => coordinator }); - if (operation !== "spawn") { - coordinator.fail = undefined; - await call("lanes_spawn", { lanes: [spec] }); - coordinator.fail = operation; - } - - const params = operation === "abandon" ? { lane: "worker" } : operation === "spawn" ? { lanes: [spec] } : {}; - const result = await call(`lanes_${operation}`, params); - - expect(result.isError).toBe(true); - expect(result.content).toEqual([ - { type: "text", text: `lanes_${operation} failed: injected ${operation} failure` }, - ]); - }, - ); - - it("contains a coordinator status failure while collecting settled results", async () => { - const coordinator = new FaultCoordinator(); - const { call } = makeHarness({ createCoordinator: () => coordinator }); - await call("lanes_spawn", { lanes: [spec] }); - coordinator.snapshot!.state = "ended"; - await call("lanes_status", {}); - coordinator.fail = "status"; - - const result = await call("lanes_wait", {}); - - expect(result.isError).toBe(true); - expect(result.content).toEqual([{ type: "text", text: "lanes_wait failed: injected status failure" }]); - }); - - it("contains throwing UI calls across spawn and every command path", async () => { - const coordinator = new FaultCoordinator(); - const throwingUi = { - theme: { fg: () => { throw new Error("theme failure"); } }, - setWidget: () => { throw new Error("widget failure"); }, - setStatus: () => { throw new Error("status failure"); }, - notify: () => { throw new Error("notify failure"); }, - custom: async () => { throw new Error("tui failure"); }, - } as unknown as ExtensionContext["ui"]; - const { call, commands, ctx } = makeHarness({ - hasUI: true, - ui: throwingUi, - createCoordinator: () => coordinator, - }); - - const spawned = await call("lanes_spawn", { lanes: [spec] }); - expect(spawned.isError).toBeFalsy(); - const command = commands.get("lanes")!; - for (const args of ["show", "hide", "last", "tui", "abandon worker", "unknown"]) { - await expect(command(args, ctx)).resolves.toBeUndefined(); - } - }); - - it("contains coordinator failures from commands", async () => { - const coordinator = new FaultCoordinator(); - const notifications: string[] = []; - const ui = { - theme: { fg: (_color: string, text: string) => text }, - setWidget: () => {}, - setStatus: () => {}, - notify: (message: string) => notifications.push(message), - } as unknown as ExtensionContext["ui"]; - const { call, commands, ctx } = makeHarness({ hasUI: true, ui, createCoordinator: () => coordinator }); - await call("lanes_spawn", { lanes: [spec] }); - coordinator.fail = "abandon"; - - await expect(commands.get("lanes")!("abandon worker", ctx)).resolves.toBeUndefined(); - expect(notifications).toContain("lanes command failed: injected abandon failure"); - }); -}); - -describe("session shutdown", () => { - it("abandons an active run without starting a parent turn on session_shutdown", async () => { - await installSlowPi(2_000); - const { call, handlers, sent } = makeHarness({ idle: true }); - await call("lanes_spawn", { lanes: [spec] }); - - await handlers.get("session_shutdown")!(); - - const status = await call("lanes_status", {}); - expect(status.content[0]!.text).toContain("worker: abandoned"); - await new Promise((resolve) => setTimeout(resolve, 200)); - expect(sent).toEqual([]); - }); - - it("awaits async coordinator shutdown and contains rejection", async () => { - const coordinator = new FaultCoordinator(); - const released = Promise.withResolvers(); - coordinator.shutdownImpl = async () => released.promise; - const { handlers } = makeHarness({ createCoordinator: () => coordinator }); - - let completed = false; - const shutdown = Promise.resolve(handlers.get("session_shutdown")!()).then(() => { - completed = true; - }); - await Promise.resolve(); - expect(completed).toBe(false); - released.resolve(); - await expect(shutdown).resolves.toBeUndefined(); - - const rejecting = new FaultCoordinator(); - rejecting.fail = "shutdown"; - const rejectedHarness = makeHarness({ createCoordinator: () => rejecting }); - await expect(rejectedHarness.handlers.get("session_shutdown")!()).resolves.toBeUndefined(); - }); -}); diff --git a/packages/pi-kit/test/lanes-tui.test.ts b/packages/pi-kit/test/lanes-tui.test.ts deleted file mode 100644 index 71a118d..0000000 --- a/packages/pi-kit/test/lanes-tui.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { appendFileSync, mkdirSync, rmSync } from "node:fs"; -import { mkdtempSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { appendEvent, rawRunDir } from "../src/journal-core.ts"; -import { RunView } from "../src/lanes-tui.ts"; - -const originalDataDir = process.env.PIKIT_DATA_DIR; -let dataDir: string; - -const spec = { lane: "worker", task: "t", model: "openai-codex/gpt-5.6-sol", effort: "medium" as const }; - -function textEvent(text: string): string { - return JSON.stringify({ - type: "message_update", - assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta: text }, - }); -} - -beforeEach(() => { - dataDir = mkdtempSync(join(tmpdir(), "pi-kit-tui-")); - process.env.PIKIT_DATA_DIR = dataDir; -}); - -afterEach(() => { - if (originalDataDir === undefined) delete process.env.PIKIT_DATA_DIR; - else process.env.PIKIT_DATA_DIR = originalDataDir; - rmSync(dataDir, { recursive: true, force: true }); -}); - -describe("RunView incremental transcript polling", () => { - it("tails multibyte content across polls without dropping events", () => { - const runId = "run-tui-test"; - appendEvent({ v: 1, t: new Date().toISOString(), run: runId, type: "run_created", lanes: 1, origin: "test" }); - appendEvent({ v: 1, t: new Date().toISOString(), run: runId, lane: "worker", type: "lane_created", spec }); - const dir = rawRunDir(runId); - mkdirSync(dir, { recursive: true }); - const file = join(dir, "worker.jsonl"); - - // First chunk: multibyte chars make byte length > UTF-16 length. - appendFileSync(file, `${textEvent("héllo — ✔ mündo")}\n`); - const view = new RunView(runId); - const first = view.transcript("worker"); - expect(first.entries.some((entry) => entry.text.includes("mündo"))).toBe(true); - - // Second chunk after the multibyte offset: must not be skipped or split. - appendFileSync(file, `${textEvent("segunda línea — completa")}\n`); - const second = view.transcript("worker"); - expect(second.entries.some((entry) => entry.text.includes("completa"))).toBe(true); - }); - - it("holds a partial trailing line until its newline arrives", () => { - const runId = "run-tui-partial"; - appendEvent({ v: 1, t: new Date().toISOString(), run: runId, type: "run_created", lanes: 1, origin: "test" }); - appendEvent({ v: 1, t: new Date().toISOString(), run: runId, lane: "worker", type: "lane_created", spec }); - const dir = rawRunDir(runId); - mkdirSync(dir, { recursive: true }); - const file = join(dir, "worker.jsonl"); - - const full = `${textEvent("only after newline")}\n`; - appendFileSync(file, full.slice(0, 25)); // half-flushed line - const view = new RunView(runId); - expect(view.transcript("worker").entries.length).toBe(0); - - appendFileSync(file, full.slice(25)); // rest arrives - const done = view.transcript("worker"); - expect(done.entries.some((entry) => entry.text.includes("only after newline"))).toBe(true); - }); -}); diff --git a/packages/pi-kit/test/mcp-server.test.ts b/packages/pi-kit/test/mcp-server.test.ts deleted file mode 100644 index b574349..0000000 --- a/packages/pi-kit/test/mcp-server.test.ts +++ /dev/null @@ -1,452 +0,0 @@ -import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; -import { access, chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; -import { afterEach, describe, expect, it } from "vitest"; -import { - createLaneMcpServer, - type LaneMcpCoordinator, -} from "../mcp/create-server.ts"; -import { - LaneCoordinator, - type LaneCoordinatorJournal, - type LaneRunnerPort, - type RunSnapshotDto, -} from "../src/lane-coordinator.ts"; -import type { RunnerOptions } from "../src/runner.ts"; -import type { LaneProjection, LaneSpec, RunProjection } from "../src/schema.ts"; - -const solSpec: LaneSpec = { - lane: "worker", - task: "Run the fixture task", - model: "openai-codex/gpt-5.6-sol", - effort: "medium", - mode: "read-only", -}; - -class ProtocolRunner implements LaneRunnerPort { - readonly finished = Promise.withResolvers(); - readonly view: RunProjection; - readonly abandoned: Array<{ lane?: string; reason: string }> = []; - - constructor(options: RunnerOptions) { - this.view = { - run: options.runId, - origin: options.origin ?? "mcp", - createdAt: "runner timestamp", - ended: false, - lanes: new Map(), - totalCost: 0, - totalTokensIn: 0, - totalTokensOut: 0, - }; - } - - projection(): RunProjection { - return this.view; - } - - dispatch(specs: LaneSpec[]): Promise { - for (const spec of specs) { - this.view.lanes.set(spec.lane, { - spec, - state: "running", - pid: 123, - tokensIn: 10, - tokensOut: 2, - cost: 0.25, - context: 12, - }); - } - return this.finished.promise; - } - - abandon(lane: string, reason: string): void { - this.abandoned.push({ lane, reason }); - const projection = this.view.lanes.get(lane); - if (projection) { - projection.state = "abandoned"; - projection.abandonReason = reason; - } - } - - abandonAll(reason: string): void { - this.abandoned.push({ reason }); - for (const projection of this.view.lanes.values()) { - projection.state = "abandoned"; - projection.abandonReason = reason; - } - } - - shutdown(reason: string): Promise { - this.abandonAll(reason); - return this.finished.promise.then(() => undefined); - } - - settle(state: LaneProjection["state"] = "done"): void { - for (const projection of this.view.lanes.values()) projection.state = state; - this.view.ended = true; - this.finished.resolve([...this.view.lanes.values()]); - } -} - -function realCoordinatorHarness() { - const runners: ProtocolRunner[] = []; - const journal: LaneCoordinatorJournal = { append() {}, rawDir: (run) => `/raw/${run}` }; - const coordinator = new LaneCoordinator({ - origin: "mcp", - journal, - newRunId: () => "run-mcp", - clock: () => new Date("2026-07-21T12:00:00.000Z"), - createRunner(options) { - const runner = new ProtocolRunner(options); - runners.push(runner); - return runner; - }, - }); - return { coordinator, runners }; -} - -const connected: Array<{ client: Client; server: ReturnType["server"] }> = []; - -async function protocolHarness(coordinator: LaneMcpCoordinator) { - const created = createLaneMcpServer({ coordinator }); - const client = new Client({ name: "pi-kit-test", version: "1.0.0" }); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); - await created.server.connect(serverTransport); - await client.connect(clientTransport); - connected.push({ client, server: created.server }); - return { ...created, client }; -} - -function textOf(result: unknown): string { - if (typeof result !== "object" || result === null || !("content" in result) || !Array.isArray(result.content)) return ""; - const first: unknown = result.content[0]; - return typeof first === "object" && first !== null && "type" in first && first.type === "text" && "text" in first && typeof first.text === "string" - ? first.text - : ""; -} - -async function waitUntil(predicate: () => boolean | Promise, message: string, timeoutMs = 2_000): Promise { - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - if (await predicate()) return; - await new Promise((resolve) => setTimeout(resolve, 10)); - } - throw new Error(message); -} - -async function waitForFile(path: string): Promise { - await waitUntil(async () => { - try { - await access(path); - return true; - } catch { - return false; - } - }, `Timed out waiting for ${path}`, 5_000); -} - -async function waitForProcessExit(child: ChildProcessWithoutNullStreams, timeoutMs: number): Promise { - if (child.exitCode !== null) return child.exitCode; - return await new Promise((resolve, reject) => { - const timer = setTimeout(() => reject(new Error(`Process ${child.pid} did not exit within ${timeoutMs}ms`)), timeoutMs); - child.once("close", (code) => { - clearTimeout(timer); - resolve(code); - }); - }); -} - -afterEach(async () => { - await Promise.allSettled(connected.splice(0).map(async ({ client, server }) => { - await client.close(); - await server.close(); - })); -}); - -describe("lane MCP protocol", () => { - it("registers only the four lane tools and returns text-only no-run errors", async () => { - const { coordinator } = realCoordinatorHarness(); - const { client } = await protocolHarness(coordinator); - - expect((await client.listTools()).tools.map((tool) => tool.name)).toEqual([ - "lanes_spawn", - "lanes_status", - "lanes_wait", - "lanes_abandon", - ]); - - for (const [name, args] of [ - ["lanes_status", {}], - ["lanes_wait", {}], - ["lanes_abandon", {}], - ] as const) { - const result = await client.callTool({ name, arguments: args }); - expect(result).toMatchObject({ isError: true }); - expect(textOf(result)).toContain("No lane run"); - expect("structuredContent" in result).toBe(false); - } - }); - - it("spawns nonblocking, rejects Anthropic and duplicate runs, filters status, and abandons", async () => { - const { coordinator, runners } = realCoordinatorHarness(); - const { client } = await protocolHarness(coordinator); - - const anthropic = await client.callTool({ - name: "lanes_spawn", - arguments: { - lanes: [{ ...solSpec, lane: "claude", model: "anthropic/claude-fable-5", effort: "high" }], - }, - }); - expect(anthropic).toMatchObject({ isError: true }); - expect(textOf(anthropic)).toContain("native Claude workflow"); - expect(runners).toHaveLength(0); - - const spawned = await client.callTool({ - name: "lanes_spawn", - arguments: { - lanes: [ - { ...solSpec, cwd: "/sensitive/worktree", rationale: "private routing reason" }, - { ...solSpec, lane: "other" }, - ], - }, - }); - expect(spawned.isError).not.toBe(true); - expect(textOf(spawned)).toContain("run-mcp"); - expect(spawned.structuredContent).toMatchObject({ - run: "run-mcp", - state: "active", - totals: { cost: 0, tokensIn: 0, tokensOut: 0 }, - lanes: [ - { lane: "worker", model: solSpec.model, effort: "medium", mode: "read-only", state: "running" }, - { lane: "other", model: solSpec.model, effort: "medium", mode: "read-only", state: "running" }, - ], - }); - const spawnedJson = JSON.stringify(spawned.structuredContent); - expect(spawnedJson).not.toContain("Run the fixture task"); - expect(spawnedJson).not.toContain("/sensitive/worktree"); - expect(spawnedJson).not.toContain("private routing reason"); - for (const field of ["task", "cwd", "rationale", "pid", "origin", "createdAt"]) { - expect(spawned.structuredContent).not.toHaveProperty(field); - expect((spawned.structuredContent as { lanes: unknown[] }).lanes[0]).not.toHaveProperty(field); - } - - const duplicate = await client.callTool({ name: "lanes_spawn", arguments: { lanes: [solSpec] } }); - expect(duplicate).toMatchObject({ isError: true }); - expect(textOf(duplicate)).toContain("still active"); - expect("structuredContent" in duplicate).toBe(false); - - const filtered = await client.callTool({ name: "lanes_status", arguments: { lane: "worker" } }); - expect(filtered.structuredContent).toMatchObject({ lanes: [{ lane: "worker", state: "running" }] }); - expect((filtered.structuredContent as { lanes: unknown[] }).lanes).toHaveLength(1); - - const abandoned = await client.callTool({ - name: "lanes_abandon", - arguments: { lane: "worker", reason: "protocol stop" }, - }); - expect((abandoned.structuredContent as { lanes: unknown[] }).lanes[0]).toMatchObject({ - lane: "worker", - state: "abandoned", - abandonReason: "protocol stop", - }); - expect(runners[0]!.abandoned).toEqual([{ lane: "worker", reason: "protocol stop" }]); - - runners[0]!.settle("abandoned"); - await coordinator.wait(); - }); - - it("passes the request signal to wait so cancellation detaches without abandonment", async () => { - let waitSignal: AbortSignal | undefined; - let abandonCalls = 0; - const snapshot = finiteSnapshot(); - const coordinator: LaneMcpCoordinator = { - async spawn() { return snapshot; }, - status() { return snapshot; }, - wait(signal) { - waitSignal = signal; - return new Promise((resolve) => signal?.addEventListener("abort", () => resolve(snapshot), { once: true })); - }, - abandon() { abandonCalls++; return snapshot; }, - async shutdown() {}, - }; - const { client } = await protocolHarness(coordinator); - const controller = new AbortController(); - - const waiting = client.callTool({ name: "lanes_wait", arguments: {} }, undefined, { signal: controller.signal }); - await waitUntil(() => waitSignal !== undefined, "wait handler did not receive a signal"); - controller.abort(); - - await expect(waiting).rejects.toThrow(); - await waitUntil(() => waitSignal?.aborted === true, "wait handler signal was not aborted"); - expect(abandonCalls).toBe(0); - expect((await client.callTool({ name: "lanes_status", arguments: {} })).structuredContent).toMatchObject({ state: "active" }); - }); - - it("converts every coordinator handler exception to a bounded text-only tool error", async () => { - const coordinator: LaneMcpCoordinator = { - async spawn() { throw new Error("spawn boom"); }, - status() { throw new Error("status boom"); }, - async wait() { throw new Error("wait boom"); }, - abandon() { throw new Error("abandon boom"); }, - async shutdown() {}, - }; - const { client } = await protocolHarness(coordinator); - const calls = [ - ["lanes_spawn", { lanes: [solSpec] }, "spawn boom"], - ["lanes_status", {}, "status boom"], - ["lanes_wait", {}, "wait boom"], - ["lanes_abandon", {}, "abandon boom"], - ] as const; - - for (const [name, args, message] of calls) { - const result = await client.callTool({ name, arguments: args }); - expect(result).toMatchObject({ isError: true }); - expect(textOf(result)).toContain(message); - expect(textOf(result).length).toBeLessThanOrEqual(280); - expect("structuredContent" in result).toBe(false); - } - }); - - it("normalizes every non-finite numeric DTO field before schema validation", async () => { - const snapshot = finiteSnapshot(); - snapshot.totals.cost = Number.POSITIVE_INFINITY; - snapshot.totals.tokensIn = Number.NaN; - snapshot.totals.tokensOut = Number.NEGATIVE_INFINITY; - Object.assign(snapshot.lanes[0]!, { - tokensIn: Number.NaN, - tokensOut: Number.POSITIVE_INFINITY, - cost: Number.NEGATIVE_INFINITY, - context: Number.NaN, - durationMs: Number.POSITIVE_INFINITY, - }); - const coordinator: LaneMcpCoordinator = { - async spawn() { return snapshot; }, - status() { return snapshot; }, - async wait() { return snapshot; }, - abandon() { return snapshot; }, - async shutdown() {}, - }; - const { client } = await protocolHarness(coordinator); - - const result = await client.callTool({ name: "lanes_status", arguments: {} }); - - expect(result.isError).not.toBe(true); - expect(result.structuredContent).toMatchObject({ - totals: { cost: 0, tokensIn: 0, tokensOut: 0 }, - lanes: [{ tokensIn: 0, tokensOut: 0, cost: 0, context: 0, durationMs: 0 }], - }); - expect(JSON.parse(JSON.stringify(result.structuredContent))).toEqual(result.structuredContent); - }); -}); - -function finiteSnapshot(): RunSnapshotDto { - return { - run: "run-test", - state: "active", - durationMs: 1_000, - totals: { cost: 0.25, tokensIn: 10, tokensOut: 2 }, - lanes: [{ - lane: solSpec.lane, - model: solSpec.model, - effort: solSpec.effort, - mode: "read-only", - state: "running", - tokensIn: 10, - tokensOut: 2, - cost: 0.25, - context: 12, - durationMs: 50, - }], - }; -} - -describe("lane MCP stdio process", () => { - it("treats EOF as shutdown, exits cleanly, and leaves no lane process group", async () => { - const root = await mkdtemp(join(tmpdir(), "pi-kit-mcp-process-")); - const binDir = join(root, "bin"); - const piBinary = join(binDir, "pi"); - const processGroupFile = join(root, "lane-process-group.pid"); - const grandchildFile = join(root, "lane-grandchild.pid"); - const fixture = fileURLToPath(new URL("./fixtures/mcp-lane-process.cjs", import.meta.url)); - const serverPath = fileURLToPath(new URL("../mcp/server.ts", import.meta.url)); - await mkdir(binDir); - await writeFile( - piBinary, - `#!/usr/bin/env node\nprocess.env.MCP_FIXTURE_PROCESS_GROUP_FILE = ${JSON.stringify(processGroupFile)};\nprocess.env.MCP_FIXTURE_GRANDCHILD_FILE = ${JSON.stringify(grandchildFile)};\nrequire(${JSON.stringify(fixture)});\n`, - ); - await chmod(piBinary, 0o755); - - const child = spawn("bun", [serverPath], { - cwd: fileURLToPath(new URL("..", import.meta.url)), - env: { - ...process.env, - PATH: `${binDir}:${process.env.PATH ?? ""}`, - PIKIT_DATA_DIR: join(root, "data"), - MCP_FIXTURE_PROCESS_GROUP_FILE: processGroupFile, - MCP_FIXTURE_GRANDCHILD_FILE: grandchildFile, - }, - stdio: ["pipe", "pipe", "pipe"], - }); - let stdout = ""; - let stderr = ""; - let processGroup: number | undefined; - let grandchild: number | undefined; - child.stdout.setEncoding("utf8").on("data", (chunk: string) => { stdout += chunk; }); - child.stderr.setEncoding("utf8").on("data", (chunk: string) => { stderr += chunk; }); - - try { - child.stdin.write(`${JSON.stringify({ - jsonrpc: "2.0", - id: 1, - method: "initialize", - params: { - protocolVersion: "2025-06-18", - capabilities: {}, - clientInfo: { name: "process-test", version: "1.0.0" }, - }, - })}\n`); - await waitUntil( - () => stdout.split("\n").some((line) => line.includes('"id":1')), - "MCP initialize did not complete", - 5_000, - ); - child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })}\n`); - child.stdin.write(`${JSON.stringify({ - jsonrpc: "2.0", - id: 2, - method: "tools/call", - params: { name: "lanes_spawn", arguments: { lanes: [solSpec] } }, - })}\n`); - await waitForFile(processGroupFile); - await waitForFile(grandchildFile); - const spawnedProcessGroup = Number((await readFile(processGroupFile, "utf8")).trim()); - const spawnedGrandchild = Number((await readFile(grandchildFile, "utf8")).trim()); - processGroup = spawnedProcessGroup; - grandchild = spawnedGrandchild; - - child.stdin.end(); - expect(await waitForProcessExit(child, 4_000)).toBe(0); - await waitUntil(() => { - try { process.kill(-spawnedProcessGroup, 0); return false; } catch { return true; } - }, `Process group ${spawnedProcessGroup} remained alive`, 4_000); - await waitUntil(() => { - try { process.kill(spawnedGrandchild, 0); return false; } catch { return true; } - }, `Grandchild ${spawnedGrandchild} remained alive`, 4_000); - expect(stderr).toBe(""); - expect(stdout.trim().split("\n").every((line) => { - try { JSON.parse(line); return true; } catch { return false; } - })).toBe(true); - } finally { - if (processGroup !== undefined) { - try { process.kill(-processGroup, "SIGKILL"); } catch {} - } - if (child.exitCode === null) child.kill("SIGKILL"); - await waitForProcessExit(child, 1_000).catch(() => undefined); - await rm(root, { recursive: true, force: true }); - } - }, 15_000); -}); diff --git a/packages/pi-kit/test/runner.test.ts b/packages/pi-kit/test/runner.test.ts deleted file mode 100644 index 0588c3f..0000000 --- a/packages/pi-kit/test/runner.test.ts +++ /dev/null @@ -1,751 +0,0 @@ -import { access, chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import type { - LaneEventParser, - LaneExecutionAdapter, - NormalizedLaneEvent, -} from "../src/execution-adapter.ts"; -import { LaneRunner } from "../src/runner.ts"; -import type { ExecutionRoute, KitEvent, LaneSpec } from "../src/schema.ts"; -import { estimateCost } from "../src/table.ts"; - -const streamFixture = fileURLToPath(new URL("./fixtures/lane-stream.jsonl", import.meta.url)); -const normalizedFixture = fileURLToPath(new URL("./fixtures/normalized-lane-stream.jsonl", import.meta.url)); -const originalDataDir = process.env.PIKIT_DATA_DIR; -let dataDir: string; - -const validSpec: LaneSpec = { - lane: "worker", - task: "Run the fixture task", - model: "openai-codex/gpt-5.6-sol", - effort: "medium", -}; - -async function executable(name: string, source: string): Promise { - const path = join(dataDir, name); - await writeFile(path, source, "utf8"); - await chmod(path, 0o755); - return path; -} - -async function waitForFile(path: string): Promise { - for (let attempt = 0; attempt < 100; attempt++) { - try { - await access(path); - return; - } catch { - await new Promise((resolve) => setTimeout(resolve, 10)); - } - } - throw new Error(`Timed out waiting for ${path}`); -} - -async function waitForDead(pid: number): Promise { - for (let attempt = 0; attempt < 100; attempt++) { - try { - process.kill(pid, 0); - await new Promise((resolve) => setTimeout(resolve, 10)); - } catch { - return; - } - } - throw new Error(`Process ${pid} remained alive`); -} - -async function waitForGroupDead(processGroup: number): Promise { - for (let attempt = 0; attempt < 100; attempt++) { - try { - process.kill(-processGroup, 0); - await new Promise((resolve) => setTimeout(resolve, 10)); - } catch { - return; - } - } - throw new Error(`Process group ${processGroup} remained alive`); -} - -function normalizedAdapter( - route: ExecutionRoute, - binary: string, - onEvent?: (event: NormalizedLaneEvent) => void, - onEnd?: () => NormalizedLaneEvent[], -): LaneExecutionAdapter { - return { - route, - preflight() {}, - build(spec) { - return { - command: binary, - args: [], - cwd: spec.cwd ?? process.cwd(), - env: { ...process.env, PIKIT_CHILD: "1" }, - }; - }, - createParser() { - let ended = false; - return { - feedLine(line): NormalizedLaneEvent[] { - if (!line.trim()) return []; - const event = JSON.parse(line) as NormalizedLaneEvent; - onEvent?.(event); - return [event]; - }, - end(): NormalizedLaneEvent[] { - if (ended) throw new Error("parser.end called more than once"); - ended = true; - return onEnd?.() ?? []; - }, - } satisfies LaneEventParser; - }, - }; -} - -beforeEach(async () => { - dataDir = await mkdtemp(join(tmpdir(), "pi-kit-runner-")); - process.env.PIKIT_DATA_DIR = dataDir; -}); - -afterEach(async () => { - if (originalDataDir === undefined) delete process.env.PIKIT_DATA_DIR; - else process.env.PIKIT_DATA_DIR = originalDataDir; - await rm(dataDir, { recursive: true, force: true }); -}); - -describe("LaneRunner", () => { - it("preserves the piBinary override while writing a canonical normalized transcript", async () => { - const piBinary = await executable( - "fixture-pi", - `#!/usr/bin/env node\nconst { readFileSync } = require("node:fs");\nconst lines = readFileSync(${JSON.stringify(streamFixture)}, "utf8").trimEnd().split("\\n");\n(async () => { for (const line of lines) { console.log(line); await new Promise((resolve) => setTimeout(resolve, 10)); } })();\n`, - ); - const events: KitEvent[] = []; - const rawDir = join(dataDir, "raw"); - const runner = new LaneRunner({ - runId: "run-success", - append: (event) => events.push(event), - piBinary, - rawDir, - }); - - const [lane] = await runner.dispatch([validSpec]); - - expect(events.map((event) => event.type)).toEqual( - expect.arrayContaining(["lane_start", "lane_tool", "lane_usage", "lane_end"]), - ); - expect(events.find((event) => event.type === "lane_created")).toMatchObject({ - spec: { mode: "workspace-write" }, - }); - expect(events.find((event) => event.type === "lane_tool")).toMatchObject({ - tool: "bash", - summary: "printf 'fixture tool output'", - }); - expect(events.find((event) => event.type === "lane_usage")).toMatchObject({ - input: 1200, - output: 300, - cacheRead: 40, - context: 1550, - cost: estimateCost(validSpec.model, 1200, 300), - }); - expect(lane).toMatchObject({ - state: "done", - tokensIn: 1200, - tokensOut: 300, - context: 1550, - cost: estimateCost(validSpec.model, 1200, 300), - answer: "Hello from lane.", - }); - const transcript = (await readFile(join(rawDir, "worker.jsonl"), "utf8")) - .trimEnd() - .split("\n") - .map((line) => JSON.parse(line)); - expect(transcript[0]).toEqual({ v: 1, type: "task", text: validSpec.task }); - expect(transcript.at(-1)).toEqual({ v: 1, type: "assistant_end", text: "Hello from lane." }); - expect(transcript.some((event) => event.type === "session")).toBe(false); - }); - - it("resolves the execution adapter from model-table route metadata", async () => { - const binary = await executable( - "normalized-claude", - `#!/usr/bin/env node\nconst { readFileSync } = require("node:fs");\nprocess.stdout.write(readFileSync(${JSON.stringify(normalizedFixture)}));\n`, - ); - const events: KitEvent[] = []; - const spec: LaneSpec = { - lane: "claude-worker", - task: "Run the normalized fixture task", - model: "anthropic/claude-fable-5", - effort: "high", - mode: "read-only", - }; - const runner = new LaneRunner({ - runId: "run-route", - append: (event) => events.push(event), - adapters: [normalizedAdapter("claude-code", binary)], - }); - - const [lane] = await runner.dispatch([spec]); - - expect(lane).toMatchObject({ - state: "done", - answer: "Normalized answer.", - tokensIn: 120, - tokensOut: 40, - context: 230, - }); - expect(events.find((event) => event.type === "lane_tool")).toMatchObject({ tool: "read", summary: "README.md" }); - }); - - it("rejects traversal and separator lane ids before spawning or resolving transcripts", async () => { - const marker = join(dataDir, "spawned"); - const binary = await executable("unsafe-lane", `#!/usr/bin/env bash\ntouch ${JSON.stringify(marker)}\n`); - const rawDir = join(dataDir, "raw"); - const runner = new LaneRunner({ - runId: "unsafe-lane", - append() {}, - rawDir, - adapters: [normalizedAdapter("pi", binary)], - }); - - await expect( - runner.dispatch([ - { ...validSpec, lane: "../escape" }, - { ...validSpec, lane: "nested/lane" }, - { ...validSpec, lane: "nested\\lane" }, - ]), - ).rejects.toThrow("lane id"); - await expect(access(marker)).rejects.toMatchObject({ code: "ENOENT" }); - await expect(access(join(dataDir, "escape.jsonl"))).rejects.toMatchObject({ code: "ENOENT" }); - }); - - it("records preparation failures without spawning the provider process", async () => { - const marker = join(dataDir, "provider-spawned"); - const binary = await executable("must-not-run", `#!/usr/bin/env bash\ntouch ${JSON.stringify(marker)}\n`); - const events: KitEvent[] = []; - const adapter: LaneExecutionAdapter = { - route: "pi", - preflight() {}, - async prepare() { - await Promise.resolve(); - throw new Error("version check failed"); - }, - build(spec) { - return { command: binary, args: [], cwd: spec.cwd ?? process.cwd(), env: process.env }; - }, - createParser() { - return { feedLine: () => [], end: () => [] }; - }, - }; - const runner = new LaneRunner({ - runId: "preparation-failure", - append: (event) => events.push(event), - adapters: [adapter], - }); - - const [lane] = await runner.dispatch([validSpec]); - - expect(lane).toMatchObject({ state: "failed", answer: "version check failed" }); - expect(events.find((event) => event.type === "lane_end")).toMatchObject({ - ok: false, - stopReason: "prepare:version check failed", - }); - expect(events.some((event) => event.type === "lane_start")).toBe(false); - await expect(access(marker)).rejects.toMatchObject({ code: "ENOENT" }); - }); - - it("reports spawn failures even when transcript capture also fails", async () => { - const rawDir = join(dataDir, "unwritable-raw"); - await mkdir(join(rawDir, `${validSpec.lane}.jsonl`), { recursive: true }); - const missingBinary = join(dataDir, "does-not-exist"); - const runner = new LaneRunner({ - runId: "spawn-failure", - append() {}, - rawDir, - adapters: [normalizedAdapter("pi", missingBinary)], - }); - - const [lane] = await runner.dispatch([validSpec]); - - expect(lane).toMatchObject({ - state: "failed", - answer: expect.stringContaining("ENOENT"), - }); - }); - - it("keeps exact-one routing for every managed provider family", async () => { - const binary = await executable( - "route-fixture", - `#!/usr/bin/env node\nconsole.log(JSON.stringify({v:1,type:"assistant_end",text:"routed"}));\n`, - ); - const seen = { pi: [] as string[], claude: [] as string[] }; - const routedAdapter = (route: ExecutionRoute, models: string[]): LaneExecutionAdapter => ({ - ...normalizedAdapter(route, binary), - preflight(spec) { - models.push(`${spec.model}:${spec.effort}`); - }, - }); - const runner = new LaneRunner({ - runId: "all-routes", - append() {}, - maxConcurrent: 6, - adapters: [routedAdapter("pi", seen.pi), routedAdapter("claude-code", seen.claude)], - }); - const specs: LaneSpec[] = [ - { ...validSpec, lane: "sol" }, - { ...validSpec, lane: "grok", model: "xai/grok-4.5", effort: "high" }, - { ...validSpec, lane: "glm", model: "ollama/glm-5.2:cloud" }, - { ...validSpec, lane: "fable", model: "anthropic/claude-fable-5", effort: "high" }, - { ...validSpec, lane: "opus", model: "anthropic/claude-opus-4-8", effort: "xhigh" }, - { ...validSpec, lane: "sonnet", model: "anthropic/claude-sonnet-5" }, - ]; - - const lanes = await runner.dispatch(specs); - - expect(lanes.every((lane) => lane.state === "done")).toBe(true); - expect(seen.pi).toEqual([ - "openai-codex/gpt-5.6-sol:medium", - "xai/grok-4.5:high", - "ollama/glm-5.2:cloud:medium", - ]); - expect(seen.claude).toEqual([ - "anthropic/claude-fable-5:high", - "anthropic/claude-opus-4-8:xhigh", - "anthropic/claude-sonnet-5:medium", - ]); - }); - - it("rejects zero or multiple matching adapters before spawning", async () => { - const marker = join(dataDir, "spawned"); - const binary = await executable("must-not-run", `#!/usr/bin/env bash\ntouch ${JSON.stringify(marker)}\n`); - const adapter = normalizedAdapter("pi", binary); - - await expect( - new LaneRunner({ runId: "none", append() {}, adapters: [] }).dispatch([validSpec]), - ).rejects.toThrow("exactly one execution adapter"); - await expect( - new LaneRunner({ runId: "many", append() {}, adapters: [adapter, adapter] }).dispatch([validSpec]), - ).rejects.toThrow("exactly one execution adapter"); - await expect(access(marker)).rejects.toMatchObject({ code: "ENOENT" }); - }); - - it("validates specs for their origin and normalizes Pi-origin mode", async () => { - const marker = join(dataDir, "spawned"); - const binary = await executable("origin-fixture", `#!/usr/bin/env bash\ntouch ${JSON.stringify(marker)}\n`); - const events: KitEvent[] = []; - const runner = new LaneRunner({ - runId: "origin", - origin: "mcp", - append: (event) => events.push(event), - adapters: [normalizedAdapter("pi", binary)], - }); - - await expect(runner.dispatch([validSpec])).rejects.toThrow("mode is required for MCP-origin lanes"); - expect(events).toEqual([]); - await expect(access(marker)).rejects.toMatchObject({ code: "ENOENT" }); - }); - - it("replaces cumulative usage snapshots and ignores malformed or duplicate reports", async () => { - const binary = await executable( - "usage-fixture", - `#!/usr/bin/env node\nfor (const event of [ - {v:1,type:"task",text:"Run the fixture task"}, - {v:1,type:"usage",input:70,output:10,cacheRead:20,context:100}, - {v:1,type:"usage",input:70,output:10,cacheRead:20,context:100}, - {v:1,type:"usage",input:"bad",output:999,cacheRead:0,context:999}, - {v:1,type:"usage",input:120,output:40,cacheRead:50,context:230}, - {v:1,type:"assistant_end",text:"done"} - ]) console.log(JSON.stringify(event));\nsetInterval(() => {}, 1000);\n`, - ); - const events: KitEvent[] = []; - const runner = new LaneRunner({ - runId: "usage", - append: (event) => events.push(event), - adapters: [normalizedAdapter("pi", binary)], - }); - - const [lane] = await runner.dispatch([validSpec]); - - expect(events.filter((event) => event.type === "lane_usage")).toHaveLength(2); - expect(lane).toMatchObject({ tokensIn: 120, tokensOut: 40, context: 230 }); - }); - - it("does not settle success until close and flushes parser end once", async () => { - const binary = await executable( - "slow-success", - `#!/usr/bin/env node\nprocess.on("SIGTERM", () => setTimeout(() => process.exit(0), 120));\nconsole.log(JSON.stringify({v:1,type:"task",text:"Run the fixture task"}));\nconsole.log(JSON.stringify({v:1,type:"assistant_end",text:"done"}));\nsetInterval(() => {}, 1000);\n`, - ); - const assistantSeen = Promise.withResolvers(); - let endCalls = 0; - const runner = new LaneRunner({ - runId: "slow-success", - append() {}, - adapters: [ - normalizedAdapter( - "pi", - binary, - (event) => { - if (event.type === "assistant_end") assistantSeen.resolve(); - }, - () => { - endCalls++; - return []; - }, - ), - ], - }); - - let settled = false; - const dispatch = runner.dispatch([validSpec]).finally(() => { - settled = true; - }); - await assistantSeen.promise; - await new Promise((resolve) => setTimeout(resolve, 30)); - expect(settled).toBe(false); - await dispatch; - expect(endCalls).toBe(1); - }); - - it("retries a failed lane once immediately and preserves cumulative usage", async () => { - const attemptsFile = join(dataDir, "retry-attempts"); - const binary = await executable( - "retry-once", - `#!/usr/bin/env node\nconst { existsSync, writeFileSync } = require("node:fs");\nconst first = !existsSync(${JSON.stringify(attemptsFile)});\nwriteFileSync(${JSON.stringify(attemptsFile)}, first ? "1" : "2");\nconsole.log(JSON.stringify({v:1,type:"usage",input:first?10:20,output:first?2:3,cacheRead:0,context:first?12:23}));\nif (first) { console.error("temporary failure"); process.exit(7); }\nconsole.log(JSON.stringify({v:1,type:"assistant_end",text:"recovered"}));\nsetInterval(() => {}, 1000);\n`, - ); - const events: KitEvent[] = []; - const runner = new LaneRunner({ - runId: "retry-once", - append: (event) => events.push(event), - adapters: [normalizedAdapter("pi", binary)], - }); - - const [lane] = await runner.dispatch([validSpec]); - - expect(await readFile(attemptsFile, "utf8")).toBe("2"); - expect(events.filter((event) => event.type === "lane_start")).toHaveLength(2); - expect(events.filter((event) => event.type === "lane_end")).toHaveLength(1); - expect(events).toContainEqual(expect.objectContaining({ - type: "lane_status", - text: expect.stringContaining("retrying"), - })); - expect(lane).toMatchObject({ - state: "done", - answer: "recovered", - tokensIn: 30, - tokensOut: 5, - cost: estimateCost(validSpec.model, 10, 2) + estimateCost(validSpec.model, 20, 3), - }); - }); - - it("retries a failed lane before a still-running sibling settles", async () => { - const attemptsFile = join(dataDir, "concurrent-retry-attempts"); - const binary = await executable( - "concurrent-retry", - `#!/usr/bin/env node\nconst { existsSync, writeFileSync } = require("node:fs");\nif (process.env.LANE === "sibling") { setTimeout(() => { console.log(JSON.stringify({v:1,type:"assistant_end",text:"sibling done"})); }, 400); setInterval(() => {}, 1000); } else { const first = !existsSync(${JSON.stringify(attemptsFile)}); writeFileSync(${JSON.stringify(attemptsFile)}, first ? "1" : "2"); if (first) process.exit(7); console.log(JSON.stringify({v:1,type:"assistant_end",text:"worker recovered"})); setInterval(() => {}, 1000); }\n`, - ); - const adapter = normalizedAdapter("pi", binary); - const build = adapter.build.bind(adapter); - adapter.build = (spec) => { - const plan = build(spec); - return { ...plan, env: { ...plan.env, LANE: spec.lane } }; - }; - const events: KitEvent[] = []; - const runner = new LaneRunner({ - runId: "concurrent-retry", - append: (event) => events.push(event), - adapters: [adapter], - }); - - await runner.dispatch([validSpec, { ...validSpec, lane: "sibling" }]); - - const workerStarts = events - .map((event, index) => ({ event, index })) - .filter(({ event }) => event.type === "lane_start" && event.lane === "worker"); - const siblingEnd = events.findIndex((event) => event.type === "lane_end" && event.lane === "sibling"); - expect(workerStarts).toHaveLength(2); - expect(workerStarts[1]!.index).toBeLessThan(siblingEnd); - }); - - it("does not settle failure until close", async () => { - const binary = await executable( - "slow-failure", - "#!/usr/bin/env node\nconsole.error('fixture failed');\nsetTimeout(() => process.exit(7), 120);\n", - ); - const started = Promise.withResolvers(); - const runner = new LaneRunner({ - runId: "slow-failure", - append(event) { - if (event.type === "lane_start") started.resolve(); - }, - adapters: [normalizedAdapter("pi", binary)], - }); - - let settled = false; - const dispatch = runner.dispatch([validSpec]).finally(() => { - settled = true; - }); - await started.promise; - await new Promise((resolve) => setTimeout(resolve, 30)); - expect(settled).toBe(false); - const [lane] = await dispatch; - expect(lane).toMatchObject({ state: "failed", answer: expect.stringContaining("fixture failed") }); - }); - - it("does not settle abandonment until close", async () => { - const binary = await executable( - "slow-abandon", - `#!/usr/bin/env node\nprocess.on("SIGTERM", () => setTimeout(() => process.exit(0), 120));\nconsole.log(JSON.stringify({v:1,type:"task",text:"Run the fixture task"}));\nsetInterval(() => {}, 1000);\n`, - ); - const ready = Promise.withResolvers(); - const events: KitEvent[] = []; - const runner = new LaneRunner({ - runId: "slow-abandon", - append: (event) => events.push(event), - adapters: [ - normalizedAdapter("pi", binary, (event) => { - if (event.type === "task") ready.resolve(); - }), - ], - }); - - let settled = false; - const dispatch = runner.dispatch([validSpec]).finally(() => { - settled = true; - }); - await ready.promise; - runner.abandon(validSpec.lane, "test stop"); - await new Promise((resolve) => setTimeout(resolve, 30)); - expect(settled).toBe(false); - const [lane] = await dispatch; - expect(lane).toMatchObject({ state: "abandoned", abandonReason: "test stop", durationMs: expect.any(Number) }); - expect(events.filter((event) => event.type === "lane_start")).toHaveLength(1); - }); - - it("does not let a late spawn event overwrite immediate abandonment", async () => { - const binary = await executable( - "immediate-abandon", - "#!/usr/bin/env node\nsetTimeout(() => {}, 2000);\n", - ); - const events: KitEvent[] = []; - const runner = new LaneRunner({ - runId: "immediate-abandon", - append: (event) => events.push(event), - adapters: [normalizedAdapter("pi", binary)], - }); - - const dispatch = runner.dispatch([validSpec]); - runner.abandon(validSpec.lane, "immediate stop"); - const [lane] = await dispatch; - - expect(lane).toMatchObject({ state: "abandoned", abandonReason: "immediate stop" }); - const abandonedIndex = events.findIndex((event) => event.type === "lane_abandoned"); - expect(events.slice(abandonedIndex + 1).some((event) => event.type === "lane_start")).toBe(false); - }); - - it("caps an unterminated stdout record and canonical transcript at 4 MiB", async () => { - const binary = await executable( - "overflow", - `#!/usr/bin/env node\nprocess.stdout.write("x".repeat(4 * 1024 * 1024 + 1));\nsetInterval(() => {}, 1000);\n`, - ); - const rawDir = join(dataDir, "overflow-raw"); - const runner = new LaneRunner({ - runId: "overflow", - append() {}, - rawDir, - adapters: [normalizedAdapter("pi", binary)], - }); - - const [lane] = await runner.dispatch([validSpec]); - - expect(lane).toMatchObject({ state: "failed" }); - expect((await stat(join(rawDir, "worker.jsonl"))).size).toBeLessThanOrEqual(4 * 1024 * 1024); - }); - - it("streams more than 4 MiB of newline-terminated records without failing the lane", async () => { - const binary = await executable( - "newline-overflow", - `#!/usr/bin/env node\nconst records = [\n JSON.stringify({v:1,type:"thinking_delta",delta:"x".repeat(1000)}),\n "malformed-" + "x".repeat(1000),\n JSON.stringify({v:1,type:"ignored",text:"x".repeat(1000)}),\n];\nfor (let index = 0; index < 5000; index++) process.stdout.write(records[index % records.length] + "\\n");\nconsole.log(JSON.stringify({v:1,type:"assistant_end",text:"completed after large streamed output"}));\nsetInterval(() => {}, 1000);\n`, - ); - const counts = { valid: 0, malformed: 0, ignored: 0 }; - const adapter: LaneExecutionAdapter = { - route: "pi", - preflight() {}, - build(spec) { - return { command: binary, args: [], cwd: spec.cwd ?? process.cwd(), env: process.env }; - }, - createParser() { - return { - feedLine(line) { - if (line.startsWith("malformed-")) { - counts.malformed++; - return []; - } - const parsed = JSON.parse(line) as { type?: string }; - if (parsed.type === "ignored") { - counts.ignored++; - return []; - } - counts.valid++; - return [parsed as NormalizedLaneEvent]; - }, - end() { - return []; - }, - }; - }, - }; - const events: KitEvent[] = []; - const runner = new LaneRunner({ - runId: "newline-overflow", - append: (event) => events.push(event), - adapters: [adapter], - }); - - const [lane] = await runner.dispatch([validSpec]); - - expect(lane).toMatchObject({ state: "done", answer: "completed after large streamed output" }); - expect(events.find((event) => event.type === "lane_end")).toMatchObject({ ok: true }); - expect(counts.valid).toBeGreaterThan(0); - expect(counts.malformed).toBeGreaterThan(0); - expect(counts.ignored).toBeGreaterThan(0); - }); - - it("updates projection and settles after journal appends fail during child callbacks", async () => { - const binary = await executable( - "append-failure", - `#!/usr/bin/env node\nfor (const event of [\n {v:1,type:"usage",input:12,output:3,cacheRead:1,context:16},\n {v:1,type:"assistant_end",text:"completed despite journal failure"},\n]) console.log(JSON.stringify(event));\nsetInterval(() => {}, 1000);\n`, - ); - let laneStarted = false; - const attempted: KitEvent["type"][] = []; - const runner = new LaneRunner({ - runId: "append-failure", - append(event) { - attempted.push(event.type); - if (laneStarted) throw new Error("journal unavailable"); - if (event.type === "lane_start") laneStarted = true; - }, - adapters: [normalizedAdapter("pi", binary)], - }); - - const [lane] = await runner.dispatch([validSpec]); - - expect(lane).toMatchObject({ - state: "done", - answer: "completed despite journal failure", - tokensIn: 12, - tokensOut: 3, - context: 16, - }); - expect(attempted).toContain("lane_usage"); - expect(attempted).toContain("lane_end"); - }); - - it("fails an error assistant end even when the child exits zero", async () => { - const binary = await executable("assistant-error", "#!/usr/bin/env node\n"); - const events: KitEvent[] = []; - const runner = new LaneRunner({ - runId: "assistant-error", - append: (event) => events.push(event), - adapters: [ - normalizedAdapter("pi", binary, undefined, () => [ - { v: 1, type: "assistant_end", text: "Claude failed", isError: true }, - ]), - ], - }); - - const [lane] = await runner.dispatch([validSpec]); - - expect(lane).toMatchObject({ state: "failed", answer: "Claude failed" }); - expect(events.find((event) => event.type === "lane_end")).toMatchObject({ - ok: false, - stopReason: "assistant-error", - }); - }); - - it("shutdown cannot orphan prior or current process groups across sequential runs", async () => { - const firstGrandchildFile = join(dataDir, "first-grandchild.pid"); - const secondGrandchildFile = join(dataDir, "second-grandchild.pid"); - const firstBinary = await executable( - "first-process-tree", - `#!/usr/bin/env node\nconst { spawn } = require("node:child_process");\nconst { writeFileSync } = require("node:fs");\nconst child = spawn("sleep", ["30"]);\nwriteFileSync(${JSON.stringify(firstGrandchildFile)}, String(child.pid));\nconsole.log(JSON.stringify({v:1,type:"assistant_end",text:"first done"}));\nsetInterval(() => {}, 1000);\n`, - ); - const firstEvents: KitEvent[] = []; - const firstRunner = new LaneRunner({ - runId: "first-process-tree", - append: (event) => firstEvents.push(event), - adapters: [normalizedAdapter("pi", firstBinary)], - }); - - const [firstLane] = await firstRunner.dispatch([validSpec]); - const firstProcessGroup = (firstEvents.find((event) => event.type === "lane_start") as Extract).pid; - const firstGrandchild = Number((await readFile(firstGrandchildFile, "utf8")).trim()); - expect(firstLane).toMatchObject({ state: "done" }); - await waitForDead(firstGrandchild); - await waitForGroupDead(firstProcessGroup); - - const secondBinary = await executable( - "second-process-tree", - `#!/usr/bin/env node\nconst { spawn } = require("node:child_process");\nconst { writeFileSync } = require("node:fs");\nconst child = spawn("sleep", ["30"]);\nwriteFileSync(${JSON.stringify(secondGrandchildFile)}, String(child.pid));\nprocess.on("SIGTERM", () => setTimeout(() => process.exit(0), 120));\nsetInterval(() => {}, 1000);\n`, - ); - const secondEvents: KitEvent[] = []; - const secondRunner = new LaneRunner({ - runId: "second-process-tree", - append: (event) => secondEvents.push(event), - adapters: [normalizedAdapter("pi", secondBinary)], - }); - const secondDispatch = secondRunner.dispatch([{ ...validSpec, lane: "second" }]); - await waitForFile(secondGrandchildFile); - const secondProcessGroup = (secondEvents.find((event) => event.type === "lane_start") as Extract).pid; - const secondGrandchild = Number((await readFile(secondGrandchildFile, "utf8")).trim()); - - let stopped = false; - const shutdown = secondRunner.shutdown("session ended").then(() => { - stopped = true; - }); - await new Promise((resolve) => setTimeout(resolve, 30)); - expect(stopped).toBe(false); - - const [secondLane] = await secondDispatch; - await shutdown; - expect(secondLane).toMatchObject({ state: "abandoned", abandonReason: "session ended" }); - await waitForDead(secondGrandchild); - await waitForGroupDead(secondProcessGroup); - }); - - it("reapAll kills the child process group used by the exit hook", async () => { - const grandchildPidFile = join(dataDir, "grandchild.pid"); - const binary = await executable( - "process-tree", - `#!/usr/bin/env bash\nsleep 30 &\necho $! > ${JSON.stringify(grandchildPidFile)}\nwait\n`, - ); - const runner = new LaneRunner({ - runId: "process-tree", - append() {}, - adapters: [normalizedAdapter("pi", binary)], - }); - - const dispatch = runner.dispatch([validSpec]); - await waitForFile(grandchildPidFile); - const grandchildPid = Number((await readFile(grandchildPidFile, "utf8")).trim()); - runner.reapAll(); - await dispatch; - - await waitForDead(grandchildPid); - }); - - it("rejects every invalid spec before spawning", async () => { - const marker = join(dataDir, "spawned"); - const piBinary = await executable("must-not-run-invalid", `#!/usr/bin/env bash\ntouch ${JSON.stringify(marker)}\n`); - const events: KitEvent[] = []; - const runner = new LaneRunner({ runId: "run-invalid", append: (event) => events.push(event), piBinary }); - const invalid = [ - { ...validSpec, lane: "missing-model", model: "" }, - { ...validSpec, lane: "missing-task", task: "" }, - ] as LaneSpec[]; - - await expect(runner.dispatch(invalid)).rejects.toThrow( - "Invalid lane specs:\nmissing-model: model is required: state the full selector from the model table\nmissing-task: task is required and must be self-contained", - ); - expect(events).toEqual([]); - await expect(access(marker)).rejects.toMatchObject({ code: "ENOENT" }); - }); -}); diff --git a/packages/pi-kit/test/table.test.ts b/packages/pi-kit/test/table.test.ts deleted file mode 100644 index a43e237..0000000 --- a/packages/pi-kit/test/table.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import type { LaneSpec } from "../src/schema.ts"; -import { - estimateCost, - findModel, - MODEL_TABLE, - normalizeLaneSpec, - validateLaneSpec, -} from "../src/table.ts"; - -const valid: LaneSpec = { - lane: "lane-1", - task: "Implement the requested slice", - model: "openai-codex/gpt-5.6-sol", - effort: "medium", -}; - -const validAnthropic: LaneSpec = { - ...valid, - model: "anthropic/claude-fable-5", - effort: "high", -}; - -describe("validateLaneSpec", () => { - it("accepts a valid explicit lane", () => { - expect(validateLaneSpec(valid)).toBeNull(); - expect(validateLaneSpec({ ...valid, lane: `a${"x".repeat(63)}` })).toBeNull(); - expect(validateLaneSpec({ ...valid, mode: "workspace-write" }, { origin: "mcp" })).toBeNull(); - }); - - it.each(["../escape", "nested/lane", "nested\\lane", ".hidden", `a${"x".repeat(64)}`])( - "rejects unsafe or oversized lane id %s", - (lane) => { - expect(validateLaneSpec({ ...valid, lane })).toContain("lane id"); - }, - ); - - it("rejects routes unavailable to the calling origin", () => { - expect(validateLaneSpec(validAnthropic, { origin: "mcp" })).toContain("native Claude workflow"); - expect(validateLaneSpec(validAnthropic, { origin: "pi" })).toBeNull(); - }); - - it("requires MCP callers to choose a mode", () => { - expect(validateLaneSpec({ ...valid, mode: undefined }, { origin: "mcp" })).toContain("mode is required"); - expect(validateLaneSpec({ ...valid, mode: "read-only" }, { origin: "mcp" })).toBeNull(); - }); - - it("rejects invalid modes", () => { - expect(validateLaneSpec({ ...valid, mode: "unsafe" as LaneSpec["mode"] })).toContain("mode"); - }); - - it("defaults existing Pi calls to workspace-write", () => { - expect(normalizeLaneSpec(valid, { origin: "pi" }).mode).toBe("workspace-write"); - expect(normalizeLaneSpec({ ...valid, mode: "read-only" }, { origin: "pi" }).mode).toBe("read-only"); - }); - - it("requires an explicit model and effort", () => { - expect(validateLaneSpec({ ...valid, model: undefined })).toContain("state the full selector"); - expect(validateLaneSpec({ ...valid, effort: undefined })).toContain("not a model selection"); - }); - - it.each(["anthropic/claude-haiku", "openai/gpt-5.6-luna", "openai/gpt-5.6-terra"])( - "rejects banned selector %s", - (model) => { - expect(validateLaneSpec({ ...valid, model })).toContain("banned"); - }, - ); - - it.each(["ultra", "max"])("rejects prohibited effort %s", (effort) => { - expect(validateLaneSpec({ ...valid, effort: effort as LaneSpec["effort"] })).toContain("ceiling"); - }); - - it("enforces route-specific efforts and hard pins", () => { - expect(validateLaneSpec({ ...validAnthropic, effort: "minimal" }, { origin: "pi" })).toContain( - "unsupported effort", - ); - - const opus = "anthropic/claude-opus-4-8"; - expect(validateLaneSpec({ ...valid, model: opus, effort: "high" })).toContain("pinned to effort xhigh"); - expect(validateLaneSpec({ ...valid, model: opus, effort: "xhigh" })).toBeNull(); - - const grok = "xai/grok-4.5"; - expect(validateLaneSpec({ ...valid, model: grok, effort: "medium" })).toContain("pinned to effort high"); - expect(validateLaneSpec({ ...valid, model: grok, effort: "high" })).toBeNull(); - }); - - it("lists the table for an unknown selector", () => { - const error = validateLaneSpec({ ...valid, model: "unknown/model" }); - expect(error).toContain("not in the current table"); - for (const row of MODEL_TABLE) expect(error).toContain(row.selector); - }); -}); - -describe("model execution metadata", () => { - it("routes Fable through Claude Code for Pi callers", () => { - expect(findModel("anthropic/claude-fable-5")).toMatchObject({ - route: "claude-code", - runtimeModel: "fable", - allowedEfforts: ["low", "medium", "high"], - origins: ["pi"], - }); - }); - - it("routes pinned Grok through Pi for both origins", () => { - expect(findModel("xai/grok-4.5")).toMatchObject({ - route: "pi", - runtimeModel: "grok-4.5", - allowedEfforts: ["high"], - origins: ["pi", "mcp"], - }); - }); -}); - -describe("estimateCost", () => { - it("applies per-million input and output prices", () => { - expect(estimateCost("openai-codex/gpt-5.6-sol", 1_000_000, 500_000)).toBe(8.75); - }); - - it("uses Grok routing prices", () => { - expect(estimateCost("xai/grok-4.5", 1_000_000, 500_000)).toBe(5); - }); -}); diff --git a/packages/pi-kit/test/transcript.test.ts b/packages/pi-kit/test/transcript.test.ts deleted file mode 100644 index b72f525..0000000 --- a/packages/pi-kit/test/transcript.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { readFile } from "node:fs/promises"; -import { fileURLToPath } from "node:url"; -import { describe, expect, it } from "vitest"; -import { LaneTranscript } from "../src/transcript.ts"; - -const normalizedFixture = fileURLToPath(new URL("./fixtures/normalized-lane-stream.jsonl", import.meta.url)); -const piFixture = fileURLToPath(new URL("./fixtures/lane-stream.jsonl", import.meta.url)); -const line = (obj: unknown) => JSON.stringify(obj); - -describe("LaneTranscript", () => { - it("renders canonical normalized JSONL", async () => { - const transcript = new LaneTranscript(); - - transcript.feedChunk(await readFile(normalizedFixture, "utf8")); - - expect(transcript.entries).toEqual([ - { kind: "task", text: "Run the normalized fixture task" }, - { kind: "thinking", text: "Inspecting fixture." }, - { kind: "tool", title: "read", text: '{"path":"README.md"}' }, - { kind: "tool_result", title: "read", text: "fixture contents" }, - { kind: "text", text: "Normalized answer." }, - ]); - }); - - it("keeps archived Pi JSONL transcripts readable", async () => { - const transcript = new LaneTranscript(); - - transcript.feedChunk(await readFile(piFixture, "utf8")); - - expect(transcript.entries).toEqual([{ kind: "text", text: "Hello from lane." }]); - }); - - it("reduces a raw pi json stream into ordered entries", () => { - const t = new LaneTranscript(); - t.feed(line({ type: "message_start", message: { role: "user", content: [{ type: "text", text: "do the task" }] } })); - t.feed(line({ type: "message_update", assistantMessageEvent: { type: "thinking_delta", contentIndex: 0, delta: "hmm " } })); - t.feed(line({ type: "message_update", assistantMessageEvent: { type: "thinking_delta", contentIndex: 0, delta: "ok" } })); - t.feed(line({ type: "message_update", assistantMessageEvent: { type: "text_delta", contentIndex: 1, delta: "Answer: " } })); - t.feed(line({ type: "message_update", assistantMessageEvent: { type: "text_delta", contentIndex: 1, delta: "42" } })); - t.feed( - line({ - type: "message_update", - assistantMessageEvent: { - type: "toolcall_end", - contentIndex: 2, - toolCall: { name: "bash", arguments: { command: "echo hi" } }, - }, - }), - ); - t.feed( - line({ - type: "tool_execution_end", - toolName: "bash", - isError: false, - result: { content: [{ type: "text", text: "hi\n" }] }, - }), - ); - - expect(t.entries.map((entry) => entry.kind)).toEqual(["task", "thinking", "text", "tool", "tool_result"]); - expect(t.entries[0]!.text).toBe("do the task"); - expect(t.entries[1]!.text).toBe("hmm ok"); - expect(t.entries[2]!.text).toBe("Answer: 42"); - expect(t.entries[3]!.title).toBe("bash"); - expect(t.entries[4]!.text).toBe("hi\n"); - }); - - it("ignores malformed lines and keeps version monotonic", () => { - const t = new LaneTranscript(); - t.feedChunk("not json\n{}\n"); - expect(t.entries).toEqual([]); - const before = t.version; - t.feed(line({ type: "message_update", assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta: "x" } })); - expect(t.version).toBeGreaterThan(before); - }); -}); diff --git a/packages/pi-kit/test/vault.test.ts b/packages/pi-kit/test/vault.test.ts deleted file mode 100644 index 6d5ab51..0000000 --- a/packages/pi-kit/test/vault.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { appendPromotion } from "../extensions/vault.ts"; - -describe("appendPromotion", () => { - let dir: string; - - beforeEach(() => { - dir = mkdtempSync(join(tmpdir(), "pikit-vault-")); - }); - - afterEach(() => { - rmSync(dir, { recursive: true, force: true }); - }); - - it("creates the file with a header when missing", () => { - const path = appendPromotion(dir, "first claim", "pi session /tmp/x"); - const content = readFileSync(path, "utf8"); - expect(content.startsWith("# Review queue\n")).toBe(true); - expect(content).toContain("- claim: first claim"); - expect(content).toContain("- source: pi session /tmp/x"); - expect(content).toContain("- status: pending review"); - }); - - it("appends without clobbering existing content", () => { - const path1 = appendPromotion(dir, "first claim", "src-1"); - const path2 = appendPromotion(dir, "second claim", "src-2"); - expect(path1).toBe(path2); - - const content = readFileSync(path2, "utf8"); - expect(content).toContain("- claim: first claim"); - expect(content).toContain("- claim: second claim"); - }); - - it("accumulates multiple entries across repeated calls", () => { - appendPromotion(dir, "claim a", "src-a"); - appendPromotion(dir, "claim b", "src-b"); - appendPromotion(dir, "claim c", "src-c"); - - const content = readFileSync(join(dir, "REVIEW_QUEUE.md"), "utf8"); - const claimCount = (content.match(/- claim:/g) ?? []).length; - expect(claimCount).toBe(3); - expect(content).toContain("- claim: claim a"); - expect(content).toContain("- claim: claim b"); - expect(content).toContain("- claim: claim c"); - }); - - it("preserves manually added content in the file", () => { - appendPromotion(dir, "first claim", "src-1"); - const path = join(dir, "REVIEW_QUEUE.md"); - const before = readFileSync(path, "utf8"); - // simulate a human edit between promotions - const withNote = `${before}\n\n`; - writeFileSync(path, withNote, "utf8"); - - appendPromotion(dir, "second claim", "src-2"); - const after = readFileSync(path, "utf8"); - expect(after).toContain(""); - expect(after).toContain("- claim: second claim"); - }); -}); diff --git a/packages/pi-kit/themes/pickforge-ember.json b/packages/pi-kit/themes/pickforge-ember.json deleted file mode 100644 index 3132c35..0000000 --- a/packages/pi-kit/themes/pickforge-ember.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json", - "name": "pickforge-ember", - "vars": { - "ember": "#FF7A1A", - "emberSoft": "#FF9A4A", - "emberDeep": "#CC5E0C", - "surface": "#0A0A0B", - "surface1": "#0F0F11", - "surface2": "#141417", - "textMain": "#F2F2F3", - "textMuted": "#6E6E75", - "good": "#34D399", - "bad": "#FB7185", - "warn": "#FBBF24" - }, - "colors": { - "accent": "ember", - "border": "#26262B", - "borderAccent": "ember", - "borderMuted": "#1C1C20", - "success": "good", - "error": "bad", - "warning": "warn", - "muted": "textMuted", - "dim": "#4A4A52", - "text": "textMain", - "thinkingText": "textMuted", - "selectedBg": "surface2", - "userMessageBg": "surface1", - "userMessageText": "textMain", - "customMessageBg": "surface1", - "customMessageText": "textMain", - "customMessageLabel": "ember", - "toolPendingBg": "surface1", - "toolSuccessBg": "surface1", - "toolErrorBg": "#1A0F11", - "toolTitle": "ember", - "toolOutput": "textMuted", - "mdHeading": "emberSoft", - "mdLink": "ember", - "mdLinkUrl": "textMuted", - "mdCode": "emberSoft", - "mdCodeBlock": "textMain", - "mdCodeBlockBorder": "#26262B", - "mdQuote": "textMuted", - "mdQuoteBorder": "emberDeep", - "mdHr": "#26262B", - "mdListBullet": "ember", - "toolDiffAdded": "good", - "toolDiffRemoved": "bad", - "toolDiffContext": "textMuted", - "syntaxComment": "textMuted", - "syntaxKeyword": "ember", - "syntaxFunction": "emberSoft", - "syntaxVariable": "textMain", - "syntaxString": "good", - "syntaxNumber": "warn", - "syntaxType": "emberSoft", - "syntaxOperator": "textMuted", - "syntaxPunctuation": "textMuted", - "thinkingOff": "textMuted", - "thinkingMinimal": "textMuted", - "thinkingLow": "emberSoft", - "thinkingMedium": "ember", - "thinkingHigh": "emberDeep", - "thinkingXhigh": "bad", - "thinkingMax": "bad", - "bashMode": "warn" - } -} From b085d053a7152523601a5b7c91b407aff100b1e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elberte=20Pl=C3=ADnio?= Date: Wed, 22 Jul 2026 18:40:20 -0300 Subject: [PATCH 2/2] chore: rebaseline coverage thresholds after pi-kit extraction Maintainer-approved rebaseline to the remaining packages' actual coverage; pi-kit's tests now enforce their own thresholds in its standalone repository. Refs #54 --- vitest.config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vitest.config.ts b/vitest.config.ts index 7b0fa3d..4c3e55d 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -9,10 +9,10 @@ export default defineConfig({ provider: "v8", reporter: ["text", "html", "lcov"], thresholds: { - branches: 66, - functions: 84, + branches: 64, + functions: 83, lines: 72, - statements: 73, + statements: 72, }, }, include: ["packages/**/*.test.ts"],