diff --git a/.changeset/remove-lazy-init.md b/.changeset/remove-lazy-init.md new file mode 100644 index 00000000..5baddf70 --- /dev/null +++ b/.changeset/remove-lazy-init.md @@ -0,0 +1,8 @@ +--- +"@perstack/core": patch +"@perstack/skill-manager": patch +"@perstack/runtime": patch +"@perstack/installer": patch +--- + +Remove dead `lazyInit` field from schemas, types, and all usages diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 9df7630e..86736217 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -34,7 +34,6 @@ jobs: e2e/perstack-cli/providers.test.ts e2e/perstack-cli/error-handling.test.ts e2e/perstack-cli/interactive.test.ts - e2e/perstack-cli/lazy-init.test.ts e2e/perstack-cli/bundled-base.test.ts e2e/perstack-cli/versioned-base.test.ts e2e/perstack-cli/runtime-version.test.ts diff --git a/apps/create-expert/perstack.toml b/apps/create-expert/perstack.toml index 58560edd..dbf8e205 100644 --- a/apps/create-expert/perstack.toml +++ b/apps/create-expert/perstack.toml @@ -59,7 +59,6 @@ pick = ["readTextFile", "writeTextFile", "listDirectory", "think", "attemptCompl # args = ["-y", "some-mcp-server"] # requiredEnv = ["API_KEY"] # rule = "Instructions for using this skill" -# lazyInit = false ``` ## Your Workflow @@ -115,5 +114,4 @@ description = "Test-run expert definitions to verify they work correctly" command = "npx" packageName = "@perstack/create-expert-skill" requiredEnv = ["PROVIDER_API_KEY"] -lazyInit = true rule = "After creating or modifying an expert in perstack.toml, use runExpert to test it with a simple query. Review the activities to verify correctness." diff --git a/benchmarks/mcp-startup/perstack.toml b/benchmarks/mcp-startup/perstack.toml index 4df0364e..22bd6035 100644 --- a/benchmarks/mcp-startup/perstack.toml +++ b/benchmarks/mcp-startup/perstack.toml @@ -20,4 +20,3 @@ description = "Browser automation capabilities using Firecrawl" command = "npx" packageName = "firecrawl-mcp" requiredEnv = ["FIRECRAWL_API_KEY"] -lazyInit = false diff --git a/benchmarks/production-perf/perstack.toml b/benchmarks/production-perf/perstack.toml index 75057604..374cf135 100644 --- a/benchmarks/production-perf/perstack.toml +++ b/benchmarks/production-perf/perstack.toml @@ -29,4 +29,3 @@ pick = ["attemptCompletion"] type = "mcpStdioSkill" command = "node" args = ["../../e2e/fixtures/minimal-mcp-server.mjs"] -lazyInit = false diff --git a/docs/references/perstack-toml.md b/docs/references/perstack-toml.md index 1b00b3fc..cd5ea529 100644 --- a/docs/references/perstack-toml.md +++ b/docs/references/perstack-toml.md @@ -252,7 +252,6 @@ requiredEnv = ["API_KEY"] | `omit` | string[] | No | Tools to exclude (blacklist) | | `requiredEnv` | string[] | No | Required environment variables | | `allowedDomains` | string[] | No | Allowed domain patterns for network access | -| `lazyInit` | boolean | No | Delay initialization until first use (default: `false`) | ### MCP SSE Skill diff --git a/e2e/experts/lazy-init.toml b/e2e/experts/lazy-init.toml deleted file mode 100644 index 0bb317a6..00000000 --- a/e2e/experts/lazy-init.toml +++ /dev/null @@ -1,54 +0,0 @@ -model = "claude-haiku-4-5" - -[provider] -providerName = "anthropic" - -envPath = [".env", ".env.local"] - -# Expert with all skills lazyInit=false -# All skills should be fully connected BEFORE startRun -[experts."e2e-lazy-init-all-false"] -version = "1.0.0" -description = "E2E test expert with all lazyInit = false" -instruction = """ -Call attemptCompletion with result "OK" -""" - -[experts."e2e-lazy-init-all-false".skills."@perstack/base"] -type = "mcpStdioSkill" -command = "npx" -packageName = "@perstack/base" -pick = ["attemptCompletion"] -lazyInit = false - -[experts."e2e-lazy-init-all-false".skills."attacker"] -type = "mcpStdioSkill" -description = "Minimal MCP server (no lazy init)" -command = "node" -args = ["e2e/fixtures/minimal-mcp-server.mjs"] -lazyInit = false - -# Expert with multiple skills: one lazyInit=false (required), one lazyInit=true -# Tests that: -# - @perstack/base (always lazyInit=false) blocks startRun -# - exa (lazyInit=true) may connect AFTER startRun -[experts."e2e-lazy-init-mixed"] -version = "1.0.0" -description = "E2E test expert with mixed lazyInit settings" -instruction = """ -Call attemptCompletion with result "OK" -""" - -[experts."e2e-lazy-init-mixed".skills."@perstack/base"] -type = "mcpStdioSkill" -command = "npx" -packageName = "@perstack/base" -pick = ["attemptCompletion"] -lazyInit = false - -[experts."e2e-lazy-init-mixed".skills."attacker"] -type = "mcpStdioSkill" -description = "Minimal MCP server (lazy init)" -command = "node" -args = ["e2e/fixtures/minimal-mcp-server.mjs"] -lazyInit = true diff --git a/e2e/experts/mixed-tools.toml b/e2e/experts/mixed-tools.toml index d8207b07..066b2939 100644 --- a/e2e/experts/mixed-tools.toml +++ b/e2e/experts/mixed-tools.toml @@ -22,7 +22,6 @@ description = "Web search" command = "npx" args = ["-y", "exa-mcp-server"] requiredEnv = ["EXA_API_KEY"] -lazyInit = false [experts."e2e-mixed-tools".skills."user-input"] type = "interactiveSkill" diff --git a/e2e/experts/skills.toml b/e2e/experts/skills.toml index e5f692ae..bccc59d3 100644 --- a/e2e/experts/skills.toml +++ b/e2e/experts/skills.toml @@ -49,7 +49,6 @@ description = "Web search" command = "npx" args = ["-y", "exa-mcp-server"] requiredEnv = ["EXA_API_KEY"] -lazyInit = false [experts."e2e-multi-skill".skills."@perstack/base"] type = "mcpStdioSkill" diff --git a/e2e/experts/special-tools.toml b/e2e/experts/special-tools.toml index 5dd91402..64b00aad 100644 --- a/e2e/experts/special-tools.toml +++ b/e2e/experts/special-tools.toml @@ -23,7 +23,6 @@ description = "Web search" command = "npx" args = ["-y", "exa-mcp-server"] requiredEnv = ["EXA_API_KEY"] -lazyInit = false [experts."e2e-special-tools".skills."@perstack/base"] type = "mcpStdioSkill" diff --git a/e2e/perstack-cli/lazy-init.test.ts b/e2e/perstack-cli/lazy-init.test.ts deleted file mode 100644 index 7f32ff93..00000000 --- a/e2e/perstack-cli/lazy-init.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Lazy Init E2E Tests - * - * Tests skill initialization behavior based on lazyInit setting: - * - lazyInit = false (default): All skills must be fully connected BEFORE startRun - * - lazyInit = true: startRun can occur BEFORE skill is fully connected - * - * TOML: e2e/experts/lazy-init.toml - */ -import { describe, expect, it } from "vitest" -import { runCli, withEventParsing } from "../lib/runner.js" - -const LAZY_INIT_CONFIG = "./e2e/experts/lazy-init.toml" -// LLM API calls require extended timeout -const LLM_TIMEOUT = 180000 - -type SkillConnectedEvent = { - type: "skillConnected" - skillName: string - timestamp: number -} - -type StartRunEvent = { - type: "startRun" - timestamp: number -} - -type AnyEvent = { type: string; timestamp?: number; skillName?: string } - -function getSkillConnectedEvents(events: AnyEvent[]): SkillConnectedEvent[] { - return events.filter((e) => e.type === "skillConnected") as SkillConnectedEvent[] -} - -function getStartRunEvent(events: AnyEvent[]): StartRunEvent | undefined { - return events.find((e) => e.type === "startRun") as StartRunEvent | undefined -} - -describe.concurrent("Lazy Init", () => { - /** - * Tests all skills with lazyInit=false: - * All MCP servers must be fully connected BEFORE startRun begins. - */ - it( - "all lazyInit=false: all skills should be connected before startRun", - async () => { - const cmdResult = await runCli( - ["run", "--config", LAZY_INIT_CONFIG, "e2e-lazy-init-all-false", "Complete the task"], - { timeout: LLM_TIMEOUT }, - ) - const result = withEventParsing(cmdResult) - expect(result.exitCode).toBe(0) - - const events = result.events as AnyEvent[] - const skillConnectedEvents = getSkillConnectedEvents(events) - const startRunEvent = getStartRunEvent(events) - - expect(startRunEvent).toBeDefined() - - // Find skill events - const baseSkillEvent = skillConnectedEvents.find((e) => e.skillName === "@perstack/base") - const attackerSkillEvent = skillConnectedEvents.find((e) => e.skillName === "attacker") - - expect(baseSkillEvent).toBeDefined() - expect(attackerSkillEvent).toBeDefined() - - // Both skills (lazyInit=false) should be connected BEFORE startRun - expect(baseSkillEvent!.timestamp).toBeLessThanOrEqual(startRunEvent!.timestamp) - expect(attackerSkillEvent!.timestamp).toBeLessThanOrEqual(startRunEvent!.timestamp) - - // Run should complete successfully - const completeRunEvent = events.find((e) => e.type === "completeRun") - expect(completeRunEvent).toBeDefined() - }, - LLM_TIMEOUT, - ) - - /** - * Tests mixed lazyInit settings: - * - @perstack/base is always lazyInit=false (enforced by runtime), so it blocks startRun - * - attacker (minimal-mcp-server) has lazyInit=true, runtime does NOT wait for it - * - * This verifies that: - * 1. lazyInit=false skills are fully connected before startRun - * 2. lazyInit=true skills do NOT block startRun (startRun occurs immediately after base connects) - * 3. The run still completes successfully - */ - it( - "mixed: lazyInit=false blocks startRun, lazyInit=true does not block", - async () => { - const cmdResult = await runCli( - ["run", "--config", LAZY_INIT_CONFIG, "e2e-lazy-init-mixed", "Complete the task"], - { timeout: LLM_TIMEOUT }, - ) - const result = withEventParsing(cmdResult) - expect(result.exitCode).toBe(0) - - const events = result.events as AnyEvent[] - const skillConnectedEvents = getSkillConnectedEvents(events) - const startRunEvent = getStartRunEvent(events) - - expect(startRunEvent).toBeDefined() - - // Find skill events - const baseSkillEvent = skillConnectedEvents.find((e) => e.skillName === "@perstack/base") - const attackerSkillEvent = skillConnectedEvents.find((e) => e.skillName === "attacker") - - expect(baseSkillEvent).toBeDefined() - expect(attackerSkillEvent).toBeDefined() - - // @perstack/base (lazyInit=false) should be connected BEFORE startRun - expect(baseSkillEvent!.timestamp).toBeLessThanOrEqual(startRunEvent!.timestamp) - - // Verify that startRun happens immediately after base connects (not waiting for attacker) - // The gap between base connected and startRun should be small (< 50ms) - // Using 50ms to account for event loop, GC pauses, and CI runner variability - const gapMs = startRunEvent!.timestamp - baseSkillEvent!.timestamp - expect(gapMs).toBeLessThan(50) - - // Run should complete successfully - const completeRunEvent = events.find((e) => e.type === "completeRun") - expect(completeRunEvent).toBeDefined() - }, - LLM_TIMEOUT, - ) -}) diff --git a/packages/core/src/schemas/expert.ts b/packages/core/src/schemas/expert.ts index 88f6809a..1b96cd2a 100644 --- a/packages/core/src/schemas/expert.ts +++ b/packages/core/src/schemas/expert.ts @@ -80,7 +80,6 @@ export const expertSchema = z.object({ pick: [], omit: [], requiredEnv: [], - lazyInit: false, } satisfies SkillWithoutName, }) .transform((skills) => { diff --git a/packages/core/src/schemas/perstack-toml.ts b/packages/core/src/schemas/perstack-toml.ts index 6b8ba2cd..0d3ff692 100644 --- a/packages/core/src/schemas/perstack-toml.ts +++ b/packages/core/src/schemas/perstack-toml.ts @@ -158,7 +158,6 @@ export type PerstackConfigSkill = args?: string[] requiredEnv?: string[] allowedDomains?: string[] - lazyInit?: boolean } | { type: "mcpSseSkill" @@ -264,7 +263,6 @@ export const perstackConfigSchema = z.object({ args: z.array(z.string()).optional(), requiredEnv: z.array(z.string()).optional(), allowedDomains: z.array(domainPatternSchema).optional(), - lazyInit: z.boolean().optional().default(false), }), z.object({ type: z.literal("mcpSseSkill"), diff --git a/packages/core/src/schemas/skill.test.ts b/packages/core/src/schemas/skill.test.ts index c291171d..3bc3abe9 100644 --- a/packages/core/src/schemas/skill.test.ts +++ b/packages/core/src/schemas/skill.test.ts @@ -13,7 +13,6 @@ describe("@perstack/core: mcpStdioSkillSchema", () => { expect(result.name).toBe("test-skill") expect(result.pick).toEqual([]) expect(result.omit).toEqual([]) - expect(result.lazyInit).toBe(false) }) it("applies default values", () => { @@ -26,7 +25,6 @@ describe("@perstack/core: mcpStdioSkillSchema", () => { expect(result.omit).toEqual([]) expect(result.args).toEqual([]) expect(result.requiredEnv).toEqual([]) - expect(result.lazyInit).toBe(false) }) }) diff --git a/packages/core/src/schemas/skill.ts b/packages/core/src/schemas/skill.ts index 371a5c8b..1a9ff3d8 100644 --- a/packages/core/src/schemas/skill.ts +++ b/packages/core/src/schemas/skill.ts @@ -70,8 +70,6 @@ export interface McpStdioSkill { args: string[] /** Environment variables required by this skill */ requiredEnv: string[] - /** Whether to delay initialization until first use */ - lazyInit: boolean } export const mcpStdioSkillSchema = z.object({ @@ -85,7 +83,6 @@ export const mcpStdioSkillSchema = z.object({ packageName: z.string().optional(), args: z.array(z.string()).optional().default([]), requiredEnv: z.array(z.string()).optional().default([]), - lazyInit: z.boolean().optional().default(false), }) mcpStdioSkillSchema satisfies z.ZodType diff --git a/packages/installer/src/expert-resolver.ts b/packages/installer/src/expert-resolver.ts index ea2c47c2..b51539d1 100644 --- a/packages/installer/src/expert-resolver.ts +++ b/packages/installer/src/expert-resolver.ts @@ -66,7 +66,6 @@ export function toRuntimeExpert(key: string, expert: PublishedExpertData): Exper command: skill.command, packageName: skill.packageName, requiredEnv: skill.requiredEnv ?? [], - lazyInit: false, }, ] case "mcpSseSkill": @@ -80,7 +79,6 @@ export function toRuntimeExpert(key: string, expert: PublishedExpertData): Exper pick: skill.pick ?? [], omit: skill.omit ?? [], endpoint: skill.endpoint, - lazyInit: false, }, ] case "interactiveSkill": diff --git a/packages/runtime/src/helpers/resolve-expert.test.ts b/packages/runtime/src/helpers/resolve-expert.test.ts index 7c1619c9..68551e46 100644 --- a/packages/runtime/src/helpers/resolve-expert.test.ts +++ b/packages/runtime/src/helpers/resolve-expert.test.ts @@ -65,7 +65,6 @@ function createTestExpert(overrides: Partial = {}): Expert { requiredEnv: [], pick: [], omit: [], - lazyInit: false, }, }, delegates: [], diff --git a/packages/runtime/src/helpers/resolve-expert.ts b/packages/runtime/src/helpers/resolve-expert.ts index b9560f82..e8f0eff6 100644 --- a/packages/runtime/src/helpers/resolve-expert.ts +++ b/packages/runtime/src/helpers/resolve-expert.ts @@ -90,7 +90,6 @@ function toRuntimeExpert( command: skill.command, packageName: skill.packageName, requiredEnv: skill.requiredEnv ?? [], - lazyInit: false, }, ] case "mcpSseSkill": @@ -104,7 +103,6 @@ function toRuntimeExpert( pick: skill.pick ?? [], omit: skill.omit ?? [], endpoint: skill.endpoint, - lazyInit: false, }, ] case "interactiveSkill": diff --git a/packages/runtime/src/messages/message.test.ts b/packages/runtime/src/messages/message.test.ts index 97042f9d..b0efdb55 100644 --- a/packages/runtime/src/messages/message.test.ts +++ b/packages/runtime/src/messages/message.test.ts @@ -536,7 +536,7 @@ describe("@perstack/messages: instruction-message", () => { requiredEnv: [], pick: [], omit: [], - lazyInit: false, + rule: "Always use this skill carefully.", }, }, @@ -565,7 +565,6 @@ describe("@perstack/messages: instruction-message", () => { requiredEnv: [], pick: [], omit: [], - lazyInit: false, }, }, delegates: [], diff --git a/packages/skill-manager/src/adapters/in-memory-base-adapter.test.ts b/packages/skill-manager/src/adapters/in-memory-base-adapter.test.ts index c5fcbcfd..4c5076e7 100644 --- a/packages/skill-manager/src/adapters/in-memory-base-adapter.test.ts +++ b/packages/skill-manager/src/adapters/in-memory-base-adapter.test.ts @@ -45,7 +45,6 @@ function createBaseSkill() { requiredEnv: [], pick: [], omit: [], - lazyInit: false, } } diff --git a/packages/skill-manager/src/adapters/lockfile-adapter.test.ts b/packages/skill-manager/src/adapters/lockfile-adapter.test.ts index e77434fc..ab0c74dc 100644 --- a/packages/skill-manager/src/adapters/lockfile-adapter.test.ts +++ b/packages/skill-manager/src/adapters/lockfile-adapter.test.ts @@ -15,7 +15,7 @@ function createMockSkill(overrides: Partial = {}): McpStdioSkill requiredEnv: [], pick: [], omit: [], - lazyInit: false, + ...overrides, } as McpStdioSkill } diff --git a/packages/skill-manager/src/adapters/lockfile-adapter.ts b/packages/skill-manager/src/adapters/lockfile-adapter.ts index c3d25de8..a016efbd 100644 --- a/packages/skill-manager/src/adapters/lockfile-adapter.ts +++ b/packages/skill-manager/src/adapters/lockfile-adapter.ts @@ -127,7 +127,6 @@ export class LockfileSkillAdapter extends SkillAdapter { command: overrideCommand, packageName: undefined, args: overrideArgs, - lazyInit: false, } as McpStdioSkill } return skill diff --git a/packages/skill-manager/src/adapters/mcp-adapter.test.ts b/packages/skill-manager/src/adapters/mcp-adapter.test.ts index ff1b6d65..68a7b556 100644 --- a/packages/skill-manager/src/adapters/mcp-adapter.test.ts +++ b/packages/skill-manager/src/adapters/mcp-adapter.test.ts @@ -35,7 +35,7 @@ function createStdioSkill(overrides: Partial = {}): McpStdioSkill requiredEnv: [], pick: [], omit: [], - lazyInit: false, + ...overrides, } as McpStdioSkill } diff --git a/packages/skill-manager/src/skill-manager.test.ts b/packages/skill-manager/src/skill-manager.test.ts index c5a55063..25c28fb6 100644 --- a/packages/skill-manager/src/skill-manager.test.ts +++ b/packages/skill-manager/src/skill-manager.test.ts @@ -119,7 +119,7 @@ function createBaseSkill(overrides: Partial = {}): McpStdioSkill requiredEnv: [], pick: [], omit: [], - lazyInit: false, + ...overrides, } as McpStdioSkill } @@ -197,7 +197,6 @@ describe("@perstack/skill-manager: SkillManager", () => { requiredEnv: [], pick: [], omit: [], - lazyInit: false, } as McpStdioSkill, }, }) @@ -292,7 +291,6 @@ describe("@perstack/skill-manager: SkillManager", () => { requiredEnv: [], pick: [], omit: [], - lazyInit: false, } as McpStdioSkill, }, }) @@ -412,7 +410,6 @@ describe("@perstack/skill-manager: SkillManager", () => { requiredEnv: [], pick: [], omit: [], - lazyInit: false, } as McpStdioSkill, }, }) @@ -517,7 +514,6 @@ describe("@perstack/skill-manager: SkillManager", () => { requiredEnv: [], pick: [], omit: [], - lazyInit: false, } as McpStdioSkill await manager.addSkill(skill) expect(manager.getAdapters().has("new-skill")).toBe(true) @@ -540,7 +536,6 @@ describe("@perstack/skill-manager: SkillManager", () => { requiredEnv: [], pick: [], omit: [], - lazyInit: false, } as McpStdioSkill await manager.addSkill(skill) expect(factory.createMcp).toHaveBeenCalledWith( @@ -683,7 +678,6 @@ describe("buildSkillFromInput", () => { packageName: "@my/pkg", args: [], requiredEnv: [], - lazyInit: false, }) }) @@ -711,7 +705,6 @@ describe("buildSkillFromInput", () => { packageName: "@my/pkg", args: ["--flag"], requiredEnv: ["API_KEY"], - lazyInit: false, }) }) diff --git a/packages/skill-manager/src/skill-manager.ts b/packages/skill-manager/src/skill-manager.ts index fdd5fc99..48c45245 100644 --- a/packages/skill-manager/src/skill-manager.ts +++ b/packages/skill-manager/src/skill-manager.ts @@ -57,7 +57,6 @@ function applyBaseSkillCommandOverride( command: overrideCommand, packageName: undefined, args: overrideArgs, - lazyInit: false, } as McpStdioSkill } return skill @@ -113,7 +112,6 @@ export function buildSkillFromInput(input: { packageName: input.packageName, args: input.args ?? [], requiredEnv: input.requiredEnv ?? [], - lazyInit: false, } as McpStdioSkill } diff --git a/packages/skill-manager/src/utils/base-skill-helpers.test.ts b/packages/skill-manager/src/utils/base-skill-helpers.test.ts index 78caf3c0..5c5552b5 100644 --- a/packages/skill-manager/src/utils/base-skill-helpers.test.ts +++ b/packages/skill-manager/src/utils/base-skill-helpers.test.ts @@ -11,7 +11,7 @@ const createMcpStdioSkill = (overrides: Partial = {}): McpStdioSk pick: [], omit: [], requiredEnv: [], - lazyInit: false, + ...overrides, })