From 97e9ed13bd84161bbdeb1667ff8730c7568b7be3 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 25 Feb 2026 12:44:06 +0000 Subject: [PATCH 1/6] feat: deprecate apps/create-expert, move definition to definitions/ with CD - Move expert definition to definitions/create-expert/perstack.toml with npm-based skill reference instead of local path - Add getPerstackConfigOrDefault() to @perstack/perstack-toml for graceful fallback when no perstack.toml exists - Update CLI start/run commands to use default config when expertKey is provided, enabling `perstack start create-expert` without local toml - Remove apps/create-expert package and clean up Dockerfile, knip.json, .gitignore references - Add .github/workflows/expert-cd.yml for publishing expert definitions to Perstack API on push to main - Update all docs to use `npx perstack start create-expert` instead of `npx create-expert` Co-Authored-By: Claude Opus 4.6 --- .changeset/deprecate-create-expert.md | 12 + .github/workflows/expert-cd.yml | 57 +++++ .gitignore | 2 +- README.md | 6 +- apps/create-expert/CHANGELOG.md | 240 ------------------ apps/create-expert/bin/cli.ts | 99 -------- apps/create-expert/package.json | 40 --- apps/create-expert/tsconfig.build.json | 11 - apps/create-expert/tsconfig.json | 9 - apps/create-expert/tsdown.config.ts | 9 - apps/perstack/bin/cli.ts | 13 +- bun.lock | 29 +-- .../create-expert/perstack.toml | 5 +- docker/Dockerfile | 1 - docs/README.md | 2 +- docs/getting-started/walkthrough.md | 4 +- docs/guides/rapid-prototyping.md | 18 +- docs/index.md | 2 +- knip.json | 4 - packages/perstack-toml/src/config.ts | 8 + packages/perstack-toml/src/index.ts | 7 +- 21 files changed, 117 insertions(+), 461 deletions(-) create mode 100644 .changeset/deprecate-create-expert.md create mode 100644 .github/workflows/expert-cd.yml delete mode 100644 apps/create-expert/CHANGELOG.md delete mode 100644 apps/create-expert/bin/cli.ts delete mode 100644 apps/create-expert/package.json delete mode 100644 apps/create-expert/tsconfig.build.json delete mode 100644 apps/create-expert/tsconfig.json delete mode 100644 apps/create-expert/tsdown.config.ts rename {apps => definitions}/create-expert/perstack.toml (99%) diff --git a/.changeset/deprecate-create-expert.md b/.changeset/deprecate-create-expert.md new file mode 100644 index 00000000..aa4ccd19 --- /dev/null +++ b/.changeset/deprecate-create-expert.md @@ -0,0 +1,12 @@ +--- +"@perstack/perstack-toml": patch +"perstack": patch +--- + +feat: support running published experts without local perstack.toml + +`perstack start ` and `perstack run` no longer require a local `perstack.toml` when an expert key is provided. The CLI falls back to an empty default config, allowing the runtime to resolve the expert from the Perstack API. + +Adds `getPerstackConfigOrDefault()` export to `@perstack/perstack-toml`. + +Removes the `apps/create-expert` CLI package — the expert definition is now published via the Perstack API and run with `perstack start create-expert`. diff --git a/.github/workflows/expert-cd.yml b/.github/workflows/expert-cd.yml new file mode 100644 index 00000000..095d8de3 --- /dev/null +++ b/.github/workflows/expert-cd.yml @@ -0,0 +1,57 @@ +name: Expert CD + +on: + push: + branches: [main] + paths: + - 'definitions/**/perstack.toml' + +jobs: + publish: + name: Publish Expert Definitions + runs-on: ubuntu-24.04 + strategy: + matrix: + definition: + - name: create-expert + path: definitions/create-expert + draft-id: ${{ vars.EXPERT_DRAFT_ID_CREATE_EXPERT }} + steps: + - uses: actions/checkout@v6 + + - uses: oven-sh/setup-bun@v2 + + - run: bun install --frozen-lockfile + + - run: bun run build + + - name: Extract version + id: version + run: | + VERSION=$(bun -e " + const TOML = require('smol-toml'); + const fs = require('fs'); + const config = TOML.parse(fs.readFileSync('${{ matrix.definition.path }}/perstack.toml', 'utf-8')); + const experts = config.experts || {}; + const coordinator = Object.entries(experts).find(([k]) => !k.startsWith('@')); + console.log(coordinator?.[1]?.version || '1.0.0'); + ") + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Push to draft + id: push + run: | + OUTPUT=$(npx perstack expert push ${{ matrix.definition.draft-id }} \ + --config ${{ matrix.definition.path }}/perstack.toml \ + --api-key ${{ secrets.PERSTACK_API_KEY }}) + REF_ID=$(echo "$OUTPUT" | grep "Ref ID:" | awk '{print $3}') + echo "ref-id=$REF_ID" >> "$GITHUB_OUTPUT" + + - name: Assign version + run: | + npx perstack expert version \ + ${{ matrix.definition.draft-id }} \ + ${{ steps.push.outputs.ref-id }} \ + ${{ steps.version.outputs.version }} \ + --tag latest \ + --api-key ${{ secrets.PERSTACK_API_KEY }} diff --git a/.gitignore b/.gitignore index e083eaf6..d17c14dc 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,7 @@ perstack.lock perstack.toml !examples/**/perstack.toml !benchmarks/**/perstack.toml -!apps/create-expert/perstack.toml +!definitions/**/perstack.toml !docker/example/perstack.toml perstack/ !apps/perstack diff --git a/README.md b/README.md index cd0023cb..69994e55 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,10 @@ Perstack takes a third approach: purpose-specific micro-agents on value-tier mod Every agent runs inside its own Docker container with an isolated filesystem and network. The host system is never exposed. This makes Perstack safe to run in production — agents can execute shell commands and write files without risk to the host. -`create-expert` generates agent definitions in a `perstack.toml` file. `perstack` executes them on an event-sourced runtime inside a sandboxed container. +Agent definitions live in a `perstack.toml` file. `perstack` executes them on an event-sourced runtime inside a sandboxed container. ```bash -npx create-expert "Form a team named ai-gaming to build a Bun-based CLI indie game playable on Bash for AI." +npx perstack start create-expert "Form a team named ai-gaming to build a Bun-based CLI indie game playable on Bash for AI." docker run --rm -e ANTHROPIC_API_KEY \ -v ./perstack.toml:/workspace/perstack.toml:ro \ @@ -36,7 +36,7 @@ A game built with these commands: [demo-dungeon-crawler](https://github.com/pers ## How it works -`create-expert` generates a `perstack.toml` that defines a team of micro-agents. Each agent has a single responsibility and its own context window. Complex tasks are broken down and delegated to specialists. +`create-expert` generates a `perstack.toml` that defines a team of micro-agents. Run it with `npx perstack start create-expert`. Each agent has a single responsibility and its own context window. Complex tasks are broken down and delegated to specialists. ```toml [experts."ai-gaming"] diff --git a/apps/create-expert/CHANGELOG.md b/apps/create-expert/CHANGELOG.md deleted file mode 100644 index 2dd088ee..00000000 --- a/apps/create-expert/CHANGELOG.md +++ /dev/null @@ -1,240 +0,0 @@ -# create-expert - -## 0.0.46 - -### Patch Changes - -- [#621](https://github.com/perstack-ai/perstack/pull/621) [`5cbcda9`](https://github.com/perstack-ai/perstack/commit/5cbcda96a28840c0602c127f1c351995d4862a2b) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - Enable automatic prompt caching for Anthropic via request-level cache_control - -## 0.0.45 - -### Patch Changes - -- [#619](https://github.com/perstack-ai/perstack/pull/619) [`307f1db`](https://github.com/perstack-ai/perstack/commit/307f1dbebe4a7b2ed2d737e523791c5993467579) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - Fix: republish with correct exports (publishConfig.exports applied via pnpm publish) - -## 0.0.44 - -### Patch Changes - -- [#616](https://github.com/perstack-ai/perstack/pull/616) [`023c89c`](https://github.com/perstack-ai/perstack/commit/023c89cc1684c99a9cfc7a3ea548c3120d291da0) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - Fix: restore tsdown bundling and resolve workspace:\* in published packages - -## 0.0.43 - -### Patch Changes - -- [#614](https://github.com/perstack-ai/perstack/pull/614) [`33bd794`](https://github.com/perstack-ai/perstack/commit/33bd7945f81a74ab5ec979878b1421a2fde8f16e) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - Fix getExpertScope to strip @version/@tag suffix from coordinator keys, preventing false "out-of-scope delegate" validation errors for versioned expert keys - -## 0.0.42 - -### Patch Changes - -- [#612](https://github.com/perstack-ai/perstack/pull/612) [`c1f50db`](https://github.com/perstack-ai/perstack/commit/c1f50db2d550f9db603993fbf98afae67afa2707) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - feat: add MCP registry search tools and skill-finder expert - - Add `searchMcpRegistry` and `getMcpServerDetail` tools to `@perstack/create-expert-skill` that search the official MCP registry for MCP servers matching expert skill requirements. Add `@create-expert/skill-finder` expert that uses these tools to investigate registry entries and produce skill-report.md with ready-to-use TOML configuration snippets. Update coordinator, planner, and definition-writer instructions to integrate skill findings into generated expert definitions. - -## 0.0.41 - -### Patch Changes - -- [#605](https://github.com/perstack-ai/perstack/pull/605) [`46e511e`](https://github.com/perstack-ai/perstack/commit/46e511efee90837ad3e089454b254c843f1642a7) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - Strengthen definition-writer instruction to explicitly preserve existing expert definitions in perstack.toml - -## 0.0.40 - -### Patch Changes - -- [#595](https://github.com/perstack-ai/perstack/pull/595) [`47feda4`](https://github.com/perstack-ai/perstack/commit/47feda4bcfb5b6bb2808e328518fe9eed124bce4) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - Fix per-run stepNumber semantics and increase create-expert E2E timeout - -## 0.0.39 - -### Patch Changes - -- [#579](https://github.com/perstack-ai/perstack/pull/579) [`869ca03`](https://github.com/perstack-ai/perstack/commit/869ca036b7977f1db399418a70141a1f5d2b4cb8) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - fix: improve runtime robustness - -## 0.0.38 - -### Patch Changes - -- [#571](https://github.com/perstack-ai/perstack/pull/571) [`d1f4747`](https://github.com/perstack-ai/perstack/commit/d1f4747f277bada802ca2db0b8e0a59400f624d6) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - Fix .mjs output extension caused by tsdown 0.20 defaulting fixedExtension to true for node platform - -## 0.0.37 - -### Patch Changes - -- [#561](https://github.com/perstack-ai/perstack/pull/561) [`bcb7893`](https://github.com/perstack-ai/perstack/commit/bcb7893755551f4383c68d4a9581128c1aefa5bc) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - Add createExpert tool and replace runExpert workflow with in-process delegation - -## 0.0.36 - -### Patch Changes - -- [#559](https://github.com/perstack-ai/perstack/pull/559) [`350b30e`](https://github.com/perstack-ai/perstack/commit/350b30e04d4bc11b15e23cfda8c73f222243038b) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - Add --headless mode to create-expert for JSON output without TUI - -## 0.0.35 - -### Patch Changes - -- Updated dependencies [[`d6c6ab8`](https://github.com/perstack-ai/perstack/commit/d6c6ab847c1d864eda93904ba620aabb6fdef9d3)]: - - @perstack/runtime@0.0.106 - -## 0.0.34 - -### Patch Changes - -- Updated dependencies [[`bc33ba9`](https://github.com/perstack-ai/perstack/commit/bc33ba956f745450e7b25823e2bb34ce7d7a2aa7)]: - - @perstack/runtime@0.0.105 - -## 0.0.33 - -### Patch Changes - -- Updated dependencies [[`6711300`](https://github.com/perstack-ai/perstack/commit/6711300869fe4e0ed004fe37fd3f9a8c01f15d2b)]: - - @perstack/core@0.0.49 - - @perstack/perstack-toml@0.0.5 - - @perstack/runtime@0.0.104 - -## 0.0.32 - -### Patch Changes - -- Updated dependencies []: - - @perstack/runtime@0.0.103 - -## 0.0.31 - -### Patch Changes - -- Updated dependencies [[`982ea74`](https://github.com/perstack-ai/perstack/commit/982ea74ef76ef9231ce6bbaabfcc8459eb3568e9), [`2ef7374`](https://github.com/perstack-ai/perstack/commit/2ef73743a091c81a0e6e68b0944eb659f30f460d)]: - - @perstack/runtime@0.0.102 - - @perstack/core@0.0.48 - - @perstack/perstack-toml@0.0.4 - -## 0.0.30 - -### Patch Changes - -- Updated dependencies [[`1ffecd1`](https://github.com/perstack-ai/perstack/commit/1ffecd1ae33e43ba7379f55554b70672717d1ba4)]: - - @perstack/runtime@0.0.101 - -## 0.0.29 - -### Patch Changes - -- Updated dependencies [[`f470cb8`](https://github.com/perstack-ai/perstack/commit/f470cb87233b232479342d05dee94f52accf859d)]: - - @perstack/core@0.0.47 - - @perstack/runtime@0.0.100 - - @perstack/perstack-toml@0.0.3 - -## 0.0.28 - -### Patch Changes - -- Updated dependencies [[`35525ea`](https://github.com/perstack-ai/perstack/commit/35525ea7ae637973ab77a427bd2092cfffd6bd77)]: - - @perstack/runtime@0.0.99 - -## 0.0.27 - -### Patch Changes - -- Updated dependencies [[`9737c0c`](https://github.com/perstack-ai/perstack/commit/9737c0c58dc21a34364e509214e63cd177a2bfa5)]: - - @perstack/runtime@0.0.98 - -## 0.0.26 - -### Patch Changes - -- [#520](https://github.com/perstack-ai/perstack/pull/520) [`1937c56`](https://github.com/perstack-ai/perstack/commit/1937c561a34be538012c872baec0e6564abf8382) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - refactor: remove phantom dependencies and clean up knip config - -- [#527](https://github.com/perstack-ai/perstack/pull/527) [`6d40f55`](https://github.com/perstack-ai/perstack/commit/6d40f5565a29f211984ef6a2b7eeb81384782ed7) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - fix: add PerstackError class for user-facing error handling - - Add `PerstackError` to distinguish user-facing errors from internal bugs. User-facing errors (missing config, invalid input, missing env vars) now show a clean message and exit(1), while unexpected errors crash with a stack trace for debugging. - -- Updated dependencies [[`6d40f55`](https://github.com/perstack-ai/perstack/commit/6d40f5565a29f211984ef6a2b7eeb81384782ed7), [`211e47f`](https://github.com/perstack-ai/perstack/commit/211e47f59518307a34d64987aa67ef7835b71c47)]: - - @perstack/core@0.0.46 - - @perstack/perstack-toml@0.0.2 - - @perstack/runtime@0.0.97 - -## 0.0.25 - -### Patch Changes - -- [#517](https://github.com/perstack-ai/perstack/pull/517) [`4eae2f6`](https://github.com/perstack-ai/perstack/commit/4eae2f67e783c36db210669267090ed970c1b94e) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - refactor: move config resolution from library packages to application layer - -## 0.0.24 - -### Patch Changes - -- [#512](https://github.com/perstack-ai/perstack/pull/512) [`d7af461`](https://github.com/perstack-ai/perstack/commit/d7af461ebf4a395c77dbe7b3ca5c336eba0e56f5) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - Make query argument optional and add --continue/--continue-job support - -## 0.0.23 - -### Patch Changes - -- Updated dependencies [[`8df1739`](https://github.com/perstack-ai/perstack/commit/8df173914605f085ced9e12774f34133b1471f83)]: - - @perstack/runtime@0.0.96 - -## 0.0.22 - -### Patch Changes - -- [#501](https://github.com/perstack-ai/perstack/pull/501) [`56d34ef`](https://github.com/perstack-ai/perstack/commit/56d34eff53796a209684dcb0753b433113c04e39) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - refactor: extract CLI business logic into @perstack/log and @perstack/installer packages, thin Commander interface - -## 0.0.21 - -### Patch Changes - -- [#495](https://github.com/perstack-ai/perstack/pull/495) [`70f276c`](https://github.com/perstack-ai/perstack/commit/70f276c1ad284a4752bf3cefa1c8a342e287cf3e) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - fix: externalize ink, react, dotenv to eliminate CJS from ESM bundles - -- [#495](https://github.com/perstack-ai/perstack/pull/495) [`70f276c`](https://github.com/perstack-ai/perstack/commit/70f276c1ad284a4752bf3cefa1c8a342e287cf3e) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - chore: update all dependencies to latest and clean up peerDependencies - -## 0.0.20 - -### Patch Changes - -- [#493](https://github.com/perstack-ai/perstack/pull/493) [`0f7082b`](https://github.com/perstack-ai/perstack/commit/0f7082b812ade28e1e27aa4b10a25c0ac28bed11) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - fix: externalize ink, react, dotenv to eliminate CJS from ESM bundles - -## 0.0.19 - -### Patch Changes - -- [#491](https://github.com/perstack-ai/perstack/pull/491) [`00f9df4`](https://github.com/perstack-ai/perstack/commit/00f9df4ab6c5019a1268d677e31f74c27a7a8d64) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - fix: bundle @perstack/tui into create-expert dist instead of externalizing - -## 0.0.18 - -### Patch Changes - -- [#488](https://github.com/perstack-ai/perstack/pull/488) [`eaa4616`](https://github.com/perstack-ai/perstack/commit/eaa46162459b41ae363ea3473d7c9f11d3dec38c) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - refactor: extract TUI code into @perstack/tui-components and @perstack/tui packages - -## 0.0.17 - -### Patch Changes - -- [#486](https://github.com/perstack-ai/perstack/pull/486) [`73e1d3f`](https://github.com/perstack-ai/perstack/commit/73e1d3fb4d58aa0fb5f4dc640e5db1406a55bf01) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - Fix create-expert-skill reference to use packageName instead of relative path - -## 0.0.16 - -### Patch Changes - -- Updated dependencies [[`e665790`](https://github.com/perstack-ai/perstack/commit/e665790cd6bd1ef890b6938c9f0c9a178e929403)]: - - perstack@0.0.83 - -## 0.0.15 - -### Patch Changes - -- Updated dependencies [[`2f5c2de`](https://github.com/perstack-ai/perstack/commit/2f5c2deee6eb9f6c9efe6da2749d11e055885778)]: - - perstack@0.0.82 - -## 0.0.14 - -### Patch Changes - -- Updated dependencies [[`062c725`](https://github.com/perstack-ai/perstack/commit/062c7253beabab5960ec795e354d8046c11f783b)]: - - perstack@0.0.81 - -## 0.0.13 - -### Patch Changes - -- [#467](https://github.com/perstack-ai/perstack/pull/467) [`5d51959`](https://github.com/perstack-ai/perstack/commit/5d51959c00dbe2c1b4e04fc45599f434c1393c0f) Thanks [@FL4TLiN3](https://github.com/FL4TLiN3)! - Add create-expert CLI and create-expert-skill MCP server for creating and test-running expert definitions - -- Updated dependencies [[`5d51959`](https://github.com/perstack-ai/perstack/commit/5d51959c00dbe2c1b4e04fc45599f434c1393c0f)]: - - perstack@0.0.80 diff --git a/apps/create-expert/bin/cli.ts b/apps/create-expert/bin/cli.ts deleted file mode 100644 index bbd243a3..00000000 --- a/apps/create-expert/bin/cli.ts +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env bun - -import { readFileSync } from "node:fs" -import { fileURLToPath } from "node:url" -import { PerstackError } from "@perstack/core" -import { findLockfile, loadLockfile, parsePerstackConfig } from "@perstack/perstack-toml" -import { runHandler, startHandler } from "@perstack/tui" -import { PROVIDER_ENV_MAP } from "@perstack/tui/provider-config" -import { Command } from "commander" -import packageJson from "../package.json" with { type: "json" } - -const tomlPath = new URL("../perstack.toml", import.meta.url) - -new Command() - .name(packageJson.name) - .description(packageJson.description) - .version(packageJson.version) - .argument("[query]", "Description of the expert to create or modify") - .option("--headless", "Run in headless mode with JSON output (no TUI)") - .option( - "--filter ", - "Filter events by type (comma-separated, e.g., completeRun,stopRunByError)", - ) - .option("--provider ", "Provider to use") - .option("--model ", "Model to use") - .option( - "--reasoning-budget ", - "Reasoning budget for native LLM reasoning (minimal, low, medium, high, or token count)", - ) - .option("--max-retries ", "Maximum number of generation retries, default is 5") - .option( - "--timeout ", - "Timeout for each generation in milliseconds, default is 300000 (5 minutes)", - ) - .option("--job-id ", "Job ID for identifying the job") - .option( - "--env-path ", - "Path to the environment file (can be specified multiple times), default is .env and .env.local", - (value: string, previous: string[]) => previous.concat(value), - [] as string[], - ) - .option("--verbose", "Enable verbose logging") - .option("--continue", "Continue the most recent job with new query") - .option("--continue-job ", "Continue the specified job with new query") - .option( - "--resume-from ", - "Resume from a specific checkpoint (requires --continue or --continue-job)", - ) - .option("-i, --interactive-tool-call-result", "Query is interactive tool call result") - .action(async (query: string | undefined, options: Record) => { - const config = parsePerstackConfig(readFileSync(tomlPath, "utf-8")) - - // Resolve create-expert-skill server path to absolute so it works regardless of CWD. - // tomlPath resolves to dist/perstack.toml from compiled CLI (dist/bin/cli.js). - // ../../ goes: dist/ → create-expert/ → apps/, then into create-expert-skill/. - const skillServerPath = fileURLToPath( - new URL("../../create-expert-skill/dist/bin/server.js", tomlPath), - ) - const skillFinderSkill = - config.experts?.["@create-expert/skill-finder"]?.skills?.["@perstack/create-expert-skill"] - if (skillFinderSkill && skillFinderSkill.type === "mcpStdioSkill") { - skillFinderSkill.args = [skillServerPath] - } - - const lockfilePath = findLockfile() - const lockfile = lockfilePath ? (loadLockfile(lockfilePath) ?? undefined) : undefined - const additionalEnv = (env: Record) => { - const provider = config.provider?.providerName ?? "anthropic" - const envKey = PROVIDER_ENV_MAP[provider] - const value = envKey ? env[envKey] : undefined - return value ? { PROVIDER_API_KEY: value } : ({} as Record) - } - - if (options.headless) { - if (!query) { - console.error("Error: query argument is required in headless mode") - process.exit(1) - } - await runHandler("create-expert", query, options, { - perstackConfig: config, - lockfile, - additionalEnv, - }) - } else { - await startHandler("create-expert", query, options, { - perstackConfig: config, - lockfile, - additionalEnv, - }) - } - }) - .parseAsync() - .catch((error) => { - if (error instanceof PerstackError) { - console.error(error.message) - process.exit(1) - } - throw error - }) diff --git a/apps/create-expert/package.json b/apps/create-expert/package.json deleted file mode 100644 index fc849442..00000000 --- a/apps/create-expert/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "create-expert", - "version": "0.0.46", - "description": "Create and modify Perstack expert definitions", - "author": "Wintermute Technologies, Inc.", - "license": "Apache-2.0", - "type": "module", - "bin": { - "create-expert": "bin/cli.ts" - }, - "publishConfig": { - "access": "public", - "bin": { - "create-expert": "dist/bin/cli.js" - } - }, - "files": [ - "dist" - ], - "scripts": { - "clean": "rm -rf dist", - "build": "bun run clean && tsdown --config ./tsdown.config.ts && cp perstack.toml dist/", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "commander": "^14.0.3" - }, - "devDependencies": { - "@perstack/core": "workspace:*", - "@perstack/perstack-toml": "workspace:*", - "@perstack/runtime": "workspace:*", - "@perstack/tui": "workspace:*", - "@tsconfig/node22": "^22.0.5", - "@types/node": "^25.3.0", - "typescript": "^5.9.3" - }, - "engines": { - "bun": ">=1.2.0" - } -} diff --git a/apps/create-expert/tsconfig.build.json b/apps/create-expert/tsconfig.build.json deleted file mode 100644 index 5a739bc8..00000000 --- a/apps/create-expert/tsconfig.build.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "@tsconfig/node22/tsconfig.json", - "compilerOptions": { - "outDir": "dist", - "sourceMap": true, - "rootDir": ".", - "jsx": "react-jsx" - }, - "include": ["bin"], - "exclude": ["**/*.test.ts"] -} diff --git a/apps/create-expert/tsconfig.json b/apps/create-expert/tsconfig.json deleted file mode 100644 index facf6c32..00000000 --- a/apps/create-expert/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "@tsconfig/node22/tsconfig.json", - "compilerOptions": { - "resolveJsonModule": true, - "jsx": "react-jsx" - }, - "include": ["**/*.ts"], - "exclude": ["node_modules", "dist", "**/*.test.ts", "tsdown.config.ts"] -} diff --git a/apps/create-expert/tsdown.config.ts b/apps/create-expert/tsdown.config.ts deleted file mode 100644 index 52dc0521..00000000 --- a/apps/create-expert/tsdown.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from "tsdown" -import { baseConfig } from "../../tsdown.config.ts" - -export default defineConfig({ - ...baseConfig, - dts: false, - entry: { "bin/cli": "bin/cli.ts" }, - external: ["react-devtools-core"], -}) diff --git a/apps/perstack/bin/cli.ts b/apps/perstack/bin/cli.ts index a6bc30ab..ebf68665 100755 --- a/apps/perstack/bin/cli.ts +++ b/apps/perstack/bin/cli.ts @@ -7,6 +7,7 @@ import { findConfigPath, findLockfile, getPerstackConfig, + getPerstackConfigOrDefault, loadLockfile, } from "@perstack/perstack-toml" import { @@ -32,6 +33,13 @@ async function resolveConfigAndLockfile(configOption?: string) { return { perstackConfig, lockfile } } +async function resolveConfigAndLockfileOrDefault(configOption?: string) { + const perstackConfig = await getPerstackConfigOrDefault(configOption) + const lockfilePath = findLockfile(configOption) + const lockfile = lockfilePath ? (loadLockfile(lockfilePath) ?? undefined) : undefined + return { perstackConfig, lockfile } +} + const program = new Command() .name(packageJson.name) .description(packageJson.description) @@ -70,7 +78,8 @@ program ) .option("-i, --interactive-tool-call-result", "Query is interactive tool call result") .action(async (expertKey, query, options) => { - const { perstackConfig, lockfile } = await resolveConfigAndLockfile(options.config) + const resolve = expertKey ? resolveConfigAndLockfileOrDefault : resolveConfigAndLockfile + const { perstackConfig, lockfile } = await resolve(options.config) await startHandler(expertKey, query, options, { perstackConfig, lockfile }) }) @@ -111,7 +120,7 @@ program "Filter events by type (comma-separated, e.g., completeRun,stopRunByError)", ) .action(async (expertKey, query, options) => { - const { perstackConfig, lockfile } = await resolveConfigAndLockfile(options.config) + const { perstackConfig, lockfile } = await resolveConfigAndLockfileOrDefault(options.config) await runHandler(expertKey, query, options, { perstackConfig, lockfile }) }) diff --git a/bun.lock b/bun.lock index 8c22183c..9796aab9 100644 --- a/bun.lock +++ b/bun.lock @@ -31,25 +31,6 @@ "typescript": "^5.9.3", }, }, - "apps/create-expert": { - "name": "create-expert", - "version": "0.0.46", - "bin": { - "create-expert": "bin/cli.ts", - }, - "dependencies": { - "commander": "^14.0.3", - }, - "devDependencies": { - "@perstack/core": "workspace:*", - "@perstack/perstack-toml": "workspace:*", - "@perstack/runtime": "workspace:*", - "@perstack/tui": "workspace:*", - "@tsconfig/node22": "^22.0.5", - "@types/node": "^25.3.0", - "typescript": "^5.9.3", - }, - }, "apps/create-expert-skill": { "name": "@perstack/create-expert-skill", "version": "0.0.5", @@ -68,7 +49,7 @@ }, "apps/perstack": { "name": "perstack", - "version": "0.0.98", + "version": "0.0.100", "dependencies": { "commander": "^14.0.3", }, @@ -112,7 +93,7 @@ }, "packages/installer": { "name": "@perstack/installer", - "version": "0.0.22", + "version": "0.0.23", "dependencies": { "@perstack/api-client": "^0.0.57", "@perstack/core": "workspace:*", @@ -308,7 +289,7 @@ }, "packages/runtime": { "name": "@perstack/runtime", - "version": "0.0.120", + "version": "0.0.121", "dependencies": { "@ai-sdk/amazon-bedrock": "^4.0.60", "@ai-sdk/anthropic": "^3.0.44", @@ -363,7 +344,7 @@ }, "packages/studio": { "name": "@perstack/studio", - "version": "0.0.1", + "version": "0.0.2", "dependencies": { "@perstack/api-client": "^0.0.57", "@perstack/core": "workspace:*", @@ -827,8 +808,6 @@ "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], - "create-expert": ["create-expert@workspace:apps/create-expert"], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], diff --git a/apps/create-expert/perstack.toml b/definitions/create-expert/perstack.toml similarity index 99% rename from apps/create-expert/perstack.toml rename to definitions/create-expert/perstack.toml index fd8ed00e..004ad319 100644 --- a/apps/create-expert/perstack.toml +++ b/definitions/create-expert/perstack.toml @@ -186,11 +186,10 @@ command = "npx" packageName = "@perstack/base" pick = ["readTextFile", "writeTextFile", "exec", "todo", "attemptCompletion"] -# When published to npm, use: command = "npx", packageName = "@perstack/create-expert-skill" [experts."@create-expert/skill-finder".skills."@perstack/create-expert-skill"] type = "mcpStdioSkill" -command = "bun" -args = ["./apps/create-expert-skill/dist/bin/server.js"] +command = "npx" +packageName = "@perstack/create-expert-skill" pick = ["searchMcpRegistry", "getMcpServerDetail"] [experts."@create-expert/definition-writer"] diff --git a/docker/Dockerfile b/docker/Dockerfile index 80b07eb3..f734ba95 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,7 +11,6 @@ COPY package.json bun.lock ./ # Apps COPY apps/perstack/package.json apps/perstack/package.json COPY apps/base/package.json apps/base/package.json -COPY apps/create-expert/package.json apps/create-expert/package.json COPY apps/create-expert-skill/package.json apps/create-expert-skill/package.json # Packages diff --git a/docs/README.md b/docs/README.md index 16e9b671..f54cf98b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -38,7 +38,7 @@ delegates = ["pro-trainer"] Or generate one interactively: ```bash -npx create-expert "Create a fitness assistant that delegates to a pro trainer" +npx perstack start create-expert "Create a fitness assistant that delegates to a pro trainer" ``` **2. Execute** — Run from the CLI with real-time feedback: diff --git a/docs/getting-started/walkthrough.md b/docs/getting-started/walkthrough.md index 915fe446..7242246d 100644 --- a/docs/getting-started/walkthrough.md +++ b/docs/getting-started/walkthrough.md @@ -20,7 +20,7 @@ export ANTHROPIC_API_KEY=sk-ant-... Generate an Expert definition interactively: ```bash -npx create-expert "Create a fitness assistant that delegates to a pro trainer" +npx perstack start create-expert "Create a fitness assistant that delegates to a pro trainer" ``` `create-expert` does more than scaffold a file — it: @@ -48,7 +48,7 @@ description = "Suggests scientifically-backed training menus" instruction = "Provide split routines and HIIT plans tailored to user history." ``` -You can also write `perstack.toml` manually — `create-expert` is a convenient starting point, not a requirement. +You can also write `perstack.toml` manually — `perstack start create-expert` is a convenient starting point, not a requirement. ## Run Your Expert diff --git a/docs/guides/rapid-prototyping.md b/docs/guides/rapid-prototyping.md index fa4f1c96..655903c7 100644 --- a/docs/guides/rapid-prototyping.md +++ b/docs/guides/rapid-prototyping.md @@ -1,6 +1,6 @@ --- title: "Prototyping for Agent-First Apps" -description: "Start with the agent, not the app. Use create-expert to go from idea to working agent in seconds, then expand to tools and applications." +description: "Start with the agent, not the app. Use perstack start create-expert to go from idea to working agent in seconds, then expand to tools and applications." tags: ["beginner", "quick-start", "create-expert"] sidebar: order: 1 @@ -46,10 +46,10 @@ npx perstack start reviewer "Review this login handler" ## From idea to agent in one command -Writing TOML by hand works, but there's a faster way. [`create-expert`](https://www.npmjs.com/package/create-expert) is a CLI that generates Expert definitions from natural language descriptions — it's itself an Expert that builds other Experts. +Writing TOML by hand works, but there's a faster way. `create-expert` is a published Expert that generates Expert definitions from natural language descriptions — it's itself an Expert that builds other Experts. ```bash -npx create-expert "A code review assistant that checks for security vulnerabilities, suggests fixes, and explains the reasoning behind each finding" +npx perstack start create-expert "A code review assistant that checks for security vulnerabilities, suggests fixes, and explains the reasoning behind each finding" ``` `create-expert` takes your description, generates a `perstack.toml`, test-runs the Expert against sample inputs, and iterates on the definition until behavior stabilizes. You get a working Expert — no code, no setup. @@ -57,7 +57,7 @@ npx create-expert "A code review assistant that checks for security vulnerabilit The description doesn't need to be precise. Start vague: ```bash -npx create-expert "Something that helps with onboarding new team members" +npx perstack start create-expert "Something that helps with onboarding new team members" ``` `create-expert` will interpret your intent, make decisions about scope and behavior, and produce a testable Expert. You can always refine from there. @@ -67,15 +67,15 @@ npx create-expert "Something that helps with onboarding new team members" `create-expert` reads the existing `perstack.toml` in your current directory. Run it again with a refinement instruction, and it modifies the definition in place: ```bash -npx create-expert "Make it more concise. It's too verbose when explaining findings" +npx perstack start create-expert "Make it more concise. It's too verbose when explaining findings" ``` ```bash -npx create-expert "Add a severity rating to each finding: critical, warning, or info" +npx perstack start create-expert "Add a severity rating to each finding: critical, warning, or info" ``` ```bash -npx create-expert "Run 10 tests with different code samples and show me the results" +npx perstack start create-expert "Run 10 tests with different code samples and show me the results" ``` Each iteration refines the definition. The Expert gets better, and you never open an editor. @@ -87,13 +87,13 @@ Prototyping isn't just about getting the agent to run — it's about finding whe Write a test case that your agent should catch. For the code reviewer, create a file with a deliberate vulnerability: ```bash -npx create-expert "Read the file test/vulnerable.py and review it. It contains a SQL injection — make sure the reviewer catches it and suggests a parameterized query fix" +npx perstack start create-expert "Read the file test/vulnerable.py and review it. It contains a SQL injection — make sure the reviewer catches it and suggests a parameterized query fix" ``` If the reviewer misses it, you've found a gap in the instruction. Refine and test again: ```bash -npx create-expert "The reviewer missed the SQL injection in the raw query on line 12. Update the instruction to pay closer attention to string concatenation in SQL statements" +npx perstack start create-expert "The reviewer missed the SQL injection in the raw query on line 12. Update the instruction to pay closer attention to string concatenation in SQL statements" ``` This is the feedback loop that matters: **write a scenario the agent should handle, test it, fix the instruction when it fails, repeat.** By the time you build the app around it, you already know what the agent can and can't do. diff --git a/docs/index.md b/docs/index.md index 9dfdc55b..7962daf1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,7 +13,7 @@ Perstack is an open-source runtime for executing AI agents defined as Experts. D ```bash # Generate an Expert interactively -npx create-expert "Create a code-reviewer that delegates to a style-checker" +npx perstack start create-expert "Create a code-reviewer that delegates to a style-checker" # Run it npx perstack start code-reviewer "Review PR #42 for security issues" diff --git a/knip.json b/knip.json index ed5dd3d5..58dc693d 100644 --- a/knip.json +++ b/knip.json @@ -16,10 +16,6 @@ "apps/perstack": { "entry": ["bin/cli.ts"] }, - "apps/create-expert": { - "entry": ["bin/cli.ts"], - "ignoreDependencies": ["@perstack/runtime"] - }, "apps/create-expert-skill": { "entry": ["bin/server.ts", "src/index.ts"] }, diff --git a/packages/perstack-toml/src/config.ts b/packages/perstack-toml/src/config.ts index 50fa2782..099f3aa4 100644 --- a/packages/perstack-toml/src/config.ts +++ b/packages/perstack-toml/src/config.ts @@ -19,6 +19,14 @@ export async function getPerstackConfig(configPath?: string): Promise { + const configString = await findPerstackConfigString(configPath) + if (configString === null) { + return {} as PerstackConfig + } + return parsePerstackConfig(configString) +} + export function parsePerstackConfig(config: string): PerstackConfig { const toml = TOML.parse(config ?? "") return parseWithFriendlyError(perstackConfigSchema, toml, "perstack.toml") diff --git a/packages/perstack-toml/src/index.ts b/packages/perstack-toml/src/index.ts index da6a7fb8..f91df4db 100644 --- a/packages/perstack-toml/src/index.ts +++ b/packages/perstack-toml/src/index.ts @@ -1,2 +1,7 @@ -export { findConfigPath, getPerstackConfig, parsePerstackConfig } from "./config.js" +export { + findConfigPath, + getPerstackConfig, + getPerstackConfigOrDefault, + parsePerstackConfig, +} from "./config.js" export { findLockfile, generateLockfileToml, loadLockfile } from "./lockfile.js" From ccb61a7b3fc85b5dd865e25fd25ab037372024d9 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 25 Feb 2026 12:46:01 +0000 Subject: [PATCH 2/6] fix: add perstack to knip ignoreBinaries for expert-cd workflow Co-Authored-By: Claude Opus 4.6 --- knip.json | 1 + 1 file changed, 1 insertion(+) diff --git a/knip.json b/knip.json index 58dc693d..340fc097 100644 --- a/knip.json +++ b/knip.json @@ -1,5 +1,6 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", + "ignoreBinaries": ["perstack"], "ignoreDependencies": [], "ignoreExportsUsedInFile": true, "ignore": [ From 71de82ed4ca5e0063a890bb4f1bb59a60e50460e Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 25 Feb 2026 12:52:04 +0000 Subject: [PATCH 3/6] feat: add check-version and publish-if-private jobs to expert CD - check-version: extracts version from perstack.toml and checks if it already exists via `perstack expert versions`, skipping publish if so - publish: only runs when check-version reports a new version - publish-if-private: runs after publish to ensure the scope is public via `perstack expert publish` Co-Authored-By: Claude Opus 4.6 --- .github/workflows/expert-cd.yml | 78 +++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/.github/workflows/expert-cd.yml b/.github/workflows/expert-cd.yml index 095d8de3..222ded0d 100644 --- a/.github/workflows/expert-cd.yml +++ b/.github/workflows/expert-cd.yml @@ -7,8 +7,8 @@ on: - 'definitions/**/perstack.toml' jobs: - publish: - name: Publish Expert Definitions + check-version: + name: Check Version runs-on: ubuntu-24.04 strategy: matrix: @@ -16,6 +16,9 @@ jobs: - name: create-expert path: definitions/create-expert draft-id: ${{ vars.EXPERT_DRAFT_ID_CREATE_EXPERT }} + outputs: + version: ${{ steps.version.outputs.version }} + should-publish: ${{ steps.check.outputs.should-publish }} steps: - uses: actions/checkout@v6 @@ -25,7 +28,7 @@ jobs: - run: bun run build - - name: Extract version + - name: Extract version from perstack.toml id: version run: | VERSION=$(bun -e " @@ -37,6 +40,41 @@ jobs: console.log(coordinator?.[1]?.version || '1.0.0'); ") echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Extracted version: $VERSION" + + - name: Check if version already exists + id: check + run: | + OUTPUT=$(npx perstack expert versions ${{ matrix.definition.name }} \ + --api-key ${{ secrets.PERSTACK_API_KEY }} 2>&1) || true + echo "$OUTPUT" + if echo "$OUTPUT" | grep -q "^ ${{ steps.version.outputs.version }}"; then + echo "Version ${{ steps.version.outputs.version }} already exists — skipping publish" + echo "should-publish=false" >> "$GITHUB_OUTPUT" + else + echo "Version ${{ steps.version.outputs.version }} not found — will publish" + echo "should-publish=true" >> "$GITHUB_OUTPUT" + fi + + publish: + name: Publish Expert Definitions + needs: check-version + if: needs.check-version.outputs.should-publish == 'true' + runs-on: ubuntu-24.04 + strategy: + matrix: + definition: + - name: create-expert + path: definitions/create-expert + draft-id: ${{ vars.EXPERT_DRAFT_ID_CREATE_EXPERT }} + steps: + - uses: actions/checkout@v6 + + - uses: oven-sh/setup-bun@v2 + + - run: bun install --frozen-lockfile + + - run: bun run build - name: Push to draft id: push @@ -44,6 +82,7 @@ jobs: OUTPUT=$(npx perstack expert push ${{ matrix.definition.draft-id }} \ --config ${{ matrix.definition.path }}/perstack.toml \ --api-key ${{ secrets.PERSTACK_API_KEY }}) + echo "$OUTPUT" REF_ID=$(echo "$OUTPUT" | grep "Ref ID:" | awk '{print $3}') echo "ref-id=$REF_ID" >> "$GITHUB_OUTPUT" @@ -52,6 +91,37 @@ jobs: npx perstack expert version \ ${{ matrix.definition.draft-id }} \ ${{ steps.push.outputs.ref-id }} \ - ${{ steps.version.outputs.version }} \ + ${{ needs.check-version.outputs.version }} \ --tag latest \ --api-key ${{ secrets.PERSTACK_API_KEY }} + + publish-if-private: + name: Publish Scope if Private + needs: publish + runs-on: ubuntu-24.04 + strategy: + matrix: + definition: + - name: create-expert + steps: + - uses: actions/checkout@v6 + + - uses: oven-sh/setup-bun@v2 + + - run: bun install --frozen-lockfile + + - run: bun run build + + - name: Publish scope if not already public + run: | + OUTPUT=$(npx perstack expert versions ${{ matrix.definition.name }} \ + --api-key ${{ secrets.PERSTACK_API_KEY }} 2>&1) || true + if echo "$OUTPUT" | grep -q "No published versions found\.\|Failed to list versions"; then + echo "Scope appears private or has no versions — attempting publish" + npx perstack expert publish ${{ matrix.definition.name }} \ + --api-key ${{ secrets.PERSTACK_API_KEY }} || true + else + echo "Scope already has published versions — ensuring public" + npx perstack expert publish ${{ matrix.definition.name }} \ + --api-key ${{ secrets.PERSTACK_API_KEY }} || true + fi From d2be79d8b47f2ef1764169b902583136302aa664 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 25 Feb 2026 12:55:49 +0000 Subject: [PATCH 4/6] fix: run publish-if-private before version assign in expert CD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scope must be public before assigning a version, otherwise the version remains private. Reorder: check-version → publish-if-private → publish (push + version). Co-Authored-By: Claude Opus 4.6 --- .github/workflows/expert-cd.yml | 56 ++++++++++++++------------------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/.github/workflows/expert-cd.yml b/.github/workflows/expert-cd.yml index 222ded0d..6e87988d 100644 --- a/.github/workflows/expert-cd.yml +++ b/.github/workflows/expert-cd.yml @@ -56,9 +56,32 @@ jobs: echo "should-publish=true" >> "$GITHUB_OUTPUT" fi + publish-if-private: + name: Publish Scope if Private + needs: check-version + if: needs.check-version.outputs.should-publish == 'true' + runs-on: ubuntu-24.04 + strategy: + matrix: + definition: + - name: create-expert + steps: + - uses: actions/checkout@v6 + + - uses: oven-sh/setup-bun@v2 + + - run: bun install --frozen-lockfile + + - run: bun run build + + - name: Publish scope if not already public + run: | + npx perstack expert publish ${{ matrix.definition.name }} \ + --api-key ${{ secrets.PERSTACK_API_KEY }} || true + publish: name: Publish Expert Definitions - needs: check-version + needs: [check-version, publish-if-private] if: needs.check-version.outputs.should-publish == 'true' runs-on: ubuntu-24.04 strategy: @@ -94,34 +117,3 @@ jobs: ${{ needs.check-version.outputs.version }} \ --tag latest \ --api-key ${{ secrets.PERSTACK_API_KEY }} - - publish-if-private: - name: Publish Scope if Private - needs: publish - runs-on: ubuntu-24.04 - strategy: - matrix: - definition: - - name: create-expert - steps: - - uses: actions/checkout@v6 - - - uses: oven-sh/setup-bun@v2 - - - run: bun install --frozen-lockfile - - - run: bun run build - - - name: Publish scope if not already public - run: | - OUTPUT=$(npx perstack expert versions ${{ matrix.definition.name }} \ - --api-key ${{ secrets.PERSTACK_API_KEY }} 2>&1) || true - if echo "$OUTPUT" | grep -q "No published versions found\.\|Failed to list versions"; then - echo "Scope appears private or has no versions — attempting publish" - npx perstack expert publish ${{ matrix.definition.name }} \ - --api-key ${{ secrets.PERSTACK_API_KEY }} || true - else - echo "Scope already has published versions — ensuring public" - npx perstack expert publish ${{ matrix.definition.name }} \ - --api-key ${{ secrets.PERSTACK_API_KEY }} || true - fi From f2e2b9f646b9da82a4c9e7b2d8b2adf344d07fbf Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 25 Feb 2026 12:57:13 +0000 Subject: [PATCH 5/6] refactor: consolidate expert CD into single job with step-level conditions Co-Authored-By: Claude Opus 4.6 --- .github/workflows/expert-cd.yml | 54 +++++---------------------------- 1 file changed, 8 insertions(+), 46 deletions(-) diff --git a/.github/workflows/expert-cd.yml b/.github/workflows/expert-cd.yml index 6e87988d..07866c61 100644 --- a/.github/workflows/expert-cd.yml +++ b/.github/workflows/expert-cd.yml @@ -7,8 +7,8 @@ on: - 'definitions/**/perstack.toml' jobs: - check-version: - name: Check Version + publish: + name: Publish Expert Definitions runs-on: ubuntu-24.04 strategy: matrix: @@ -16,9 +16,6 @@ jobs: - name: create-expert path: definitions/create-expert draft-id: ${{ vars.EXPERT_DRAFT_ID_CREATE_EXPERT }} - outputs: - version: ${{ steps.version.outputs.version }} - should-publish: ${{ steps.check.outputs.should-publish }} steps: - uses: actions/checkout@v6 @@ -49,57 +46,21 @@ jobs: --api-key ${{ secrets.PERSTACK_API_KEY }} 2>&1) || true echo "$OUTPUT" if echo "$OUTPUT" | grep -q "^ ${{ steps.version.outputs.version }}"; then - echo "Version ${{ steps.version.outputs.version }} already exists — skipping publish" + echo "Version ${{ steps.version.outputs.version }} already exists — skipping" echo "should-publish=false" >> "$GITHUB_OUTPUT" else echo "Version ${{ steps.version.outputs.version }} not found — will publish" echo "should-publish=true" >> "$GITHUB_OUTPUT" fi - publish-if-private: - name: Publish Scope if Private - needs: check-version - if: needs.check-version.outputs.should-publish == 'true' - runs-on: ubuntu-24.04 - strategy: - matrix: - definition: - - name: create-expert - steps: - - uses: actions/checkout@v6 - - - uses: oven-sh/setup-bun@v2 - - - run: bun install --frozen-lockfile - - - run: bun run build - - - name: Publish scope if not already public + - name: Publish scope if private + if: steps.check.outputs.should-publish == 'true' run: | npx perstack expert publish ${{ matrix.definition.name }} \ --api-key ${{ secrets.PERSTACK_API_KEY }} || true - publish: - name: Publish Expert Definitions - needs: [check-version, publish-if-private] - if: needs.check-version.outputs.should-publish == 'true' - runs-on: ubuntu-24.04 - strategy: - matrix: - definition: - - name: create-expert - path: definitions/create-expert - draft-id: ${{ vars.EXPERT_DRAFT_ID_CREATE_EXPERT }} - steps: - - uses: actions/checkout@v6 - - - uses: oven-sh/setup-bun@v2 - - - run: bun install --frozen-lockfile - - - run: bun run build - - name: Push to draft + if: steps.check.outputs.should-publish == 'true' id: push run: | OUTPUT=$(npx perstack expert push ${{ matrix.definition.draft-id }} \ @@ -110,10 +71,11 @@ jobs: echo "ref-id=$REF_ID" >> "$GITHUB_OUTPUT" - name: Assign version + if: steps.check.outputs.should-publish == 'true' run: | npx perstack expert version \ ${{ matrix.definition.draft-id }} \ ${{ steps.push.outputs.ref-id }} \ - ${{ needs.check-version.outputs.version }} \ + ${{ steps.version.outputs.version }} \ --tag latest \ --api-key ${{ secrets.PERSTACK_API_KEY }} From 73627925b103d4a36a14688cd8bf8402fed5b4b8 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 25 Feb 2026 13:15:06 +0000 Subject: [PATCH 6/6] feat: allow any HTTPS URL for --config remote config Remove GitHub-only host whitelist and redirect restriction from remote config fetch. Any HTTPS URL is now accepted. HTTPS enforcement remains. Update CLI reference docs to document URL support for --config. Co-Authored-By: Claude Opus 4.6 --- docs/references/cli.md | 6 ++-- packages/perstack-toml/src/config.test.ts | 43 +++++++++++------------ packages/perstack-toml/src/config.ts | 7 +--- 3 files changed, 24 insertions(+), 32 deletions(-) diff --git a/docs/references/cli.md b/docs/references/cli.md index a5082826..ef95ef84 100644 --- a/docs/references/cli.md +++ b/docs/references/cli.md @@ -88,7 +88,7 @@ Providers: `anthropic`, `google`, `openai`, `deepseek`, `ollama`, `azure-openai` | Option | Default | Description | | ---------------------- | ---------------------- | ------------------------ | -| `--config ` | Auto-discover from cwd | Path to `perstack.toml` | +| `--config ` | Auto-discover from cwd | Path or HTTPS URL to `perstack.toml` | | `--env-path ` | `.env`, `.env.local` | Environment file paths | #### Job and Run Management @@ -203,7 +203,7 @@ By default, Perstack initializes MCP skills at runtime to discover their tool de | Option | Default | Description | | ---------------------- | ---------------------- | ----------------------- | -| `--config ` | Auto-discover from cwd | Path to `perstack.toml` | +| `--config ` | Auto-discover from cwd | Path or HTTPS URL to `perstack.toml` | | `--env-path ` | `.env`, `.env.local` | Environment file paths | The lockfile is optional. If not present, skills are initialized at runtime. @@ -279,7 +279,7 @@ perstack expert push [options] | Option | Description | | ----------------- | ----------------------- | -| `--config ` | Path to `perstack.toml` | +| `--config ` | Path or HTTPS URL to `perstack.toml` | Reads experts from `perstack.toml` and creates a new draft ref. diff --git a/packages/perstack-toml/src/config.test.ts b/packages/perstack-toml/src/config.test.ts index 03facb98..fbc653f3 100644 --- a/packages/perstack-toml/src/config.test.ts +++ b/packages/perstack-toml/src/config.test.ts @@ -136,7 +136,7 @@ instruction = "Test" }) describe("remote config", () => { - it("should fetch config from raw.githubusercontent.com with redirect disabled", async () => { + it("should fetch config from any HTTPS URL", async () => { const mockToml = ` [experts."test-expert"] instruction = "Test instruction" @@ -146,13 +146,8 @@ instruction = "Test instruction" text: () => Promise.resolve(mockToml), }) globalThis.fetch = mockFetch - const config = await getPerstackConfig( - "https://raw.githubusercontent.com/owner/repo/main/perstack.toml", - ) - expect(mockFetch).toHaveBeenCalledWith( - "https://raw.githubusercontent.com/owner/repo/main/perstack.toml", - { redirect: "error" }, - ) + const config = await getPerstackConfig("https://example.com/perstack.toml") + expect(mockFetch).toHaveBeenCalledWith("https://example.com/perstack.toml") expect(config.experts?.["test-expert"]).toBeDefined() expect(config.experts?.["test-expert"]?.instruction).toBe("Test instruction") }) @@ -163,12 +158,6 @@ instruction = "Test instruction" ) }) - it("rejects disallowed hosts", async () => { - await expect(getPerstackConfig("https://example.com/config.toml")).rejects.toThrow( - "Remote config only allowed from:", - ) - }) - it("should throw error when fetch fails", async () => { const mockFetch = mock().mockResolvedValue({ ok: false, @@ -176,23 +165,31 @@ instruction = "Test instruction" statusText: "Not Found", }) globalThis.fetch = mockFetch - await expect( - getPerstackConfig("https://raw.githubusercontent.com/owner/repo/main/perstack.toml"), - ).rejects.toThrow("Failed to fetch remote config: 404 Not Found") + await expect(getPerstackConfig("https://example.com/perstack.toml")).rejects.toThrow( + "Failed to fetch remote config: 404 Not Found", + ) }) it("should throw friendly error for network failures", async () => { const mockFetch = mock().mockRejectedValue(new Error("Network error")) globalThis.fetch = mockFetch - await expect( - getPerstackConfig("https://raw.githubusercontent.com/owner/repo/main/perstack.toml"), - ).rejects.toThrow("Failed to fetch remote config: Network error") + await expect(getPerstackConfig("https://example.com/perstack.toml")).rejects.toThrow( + "Failed to fetch remote config: Network error", + ) }) it("should handle uppercase URL schemes (case-insensitive)", async () => { - await expect(getPerstackConfig("HTTPS://example.com/perstack.toml")).rejects.toThrow( - "Remote config only allowed from: raw.githubusercontent.com", - ) + const mockToml = ` +[experts."test-expert"] +instruction = "Test" +` + const mockFetch = mock().mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockToml), + }) + globalThis.fetch = mockFetch + const config = await getPerstackConfig("HTTPS://example.com/perstack.toml") + expect(config.experts?.["test-expert"]).toBeDefined() }) it("should reject uppercase HTTP URLs", async () => { diff --git a/packages/perstack-toml/src/config.ts b/packages/perstack-toml/src/config.ts index 099f3aa4..7610e7d8 100644 --- a/packages/perstack-toml/src/config.ts +++ b/packages/perstack-toml/src/config.ts @@ -9,8 +9,6 @@ import { import TOML from "smol-toml" import { isRemoteUrl } from "./utils.js" -const ALLOWED_CONFIG_HOSTS = ["raw.githubusercontent.com"] - export async function getPerstackConfig(configPath?: string): Promise { const configString = await findPerstackConfigString(configPath) if (configString === null) { @@ -42,11 +40,8 @@ async function fetchRemoteConfig(url: string): Promise { if (parsed.protocol !== "https:") { throw new PerstackError("Remote config requires HTTPS") } - if (!ALLOWED_CONFIG_HOSTS.includes(parsed.hostname)) { - throw new PerstackError(`Remote config only allowed from: ${ALLOWED_CONFIG_HOSTS.join(", ")}`) - } try { - const response = await fetch(url, { redirect: "error" }) + const response = await fetch(url) if (!response.ok) { throw new Error(`${response.status} ${response.statusText}`) }