diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c1965c2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.github/workflows/*.lock.yml linguist-generated=true merge=ours \ No newline at end of file diff --git a/.github/mcp.json b/.github/mcp.json new file mode 100644 index 0000000..b953af2 --- /dev/null +++ b/.github/mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "github-agentic-workflows": { + "command": "gh", + "args": [ + "aw", + "mcp-server" + ] + } + } +} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9c6b2d9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +name: CI +on: + push: + pull_request: +permissions: + contents: read +jobs: + build-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Typecheck + run: npm run typecheck + + - name: Test + run: npm test + + integration-test: + runs-on: ubuntu-latest + needs: build-test + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Integration test + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + run: | + if [ -z "${COPILOT_GITHUB_TOKEN:-}" ]; then + echo "Skipping integration tests: COPILOT_GITHUB_TOKEN is not available." + else + npm run test:integration + fi diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 0000000..433acf7 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,26 @@ +name: "Copilot Setup Steps" + +# This workflow configures the environment for GitHub Copilot Agent with gh-aw MCP server +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called 'copilot-setup-steps' to be recognized by GitHub Copilot Agent + copilot-setup-steps: + runs-on: ubuntu-latest + + # Set minimal permissions for setup steps + # Copilot Agent receives its own token with appropriate permissions + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: Install gh-aw extension + uses: github/gh-aw-actions/setup-cli@v0.77.5 + with: + version: v0.77.5 diff --git a/.github/workflows/daily-rig-sampler.lock.yml b/.github/workflows/daily-rig-sampler.lock.yml new file mode 100644 index 0000000..0818d3e --- /dev/null +++ b/.github/workflows/daily-rig-sampler.lock.yml @@ -0,0 +1,1532 @@ +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"beabd252ffc57667a5e80e7290e9d09ee0849ba36d0e6ba341028bf9c75b466a","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.77.5","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.55"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.55"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.55"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.19"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4","digest":"sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.77.5). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Each day, pick one rig sample (cached round-robin) from src/samples/, run it with the rig skill, analyze harness performance, and apply one focused quick-win improvement to skills/rig/rig.ts in a new draft PR. +# +# Secrets used: +# - COPILOT_GITHUB_TOKEN +# - GH_AW_CI_TRIGGER_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@v0.77.5 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.25.55 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 +# - ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.55 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.55 +# - ghcr.io/github/gh-aw-mcpg:v0.3.19 +# - ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 +# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + +name: "Daily Rig Sampler" +on: + schedule: + - cron: "37 16 * * *" + # Friendly format: daily (scattered) + workflow_dispatch: + inputs: + aw_context: + default: "" + description: "Agent caller context (used internally by Agentic Workflows)." + required: false + type: string + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Daily Rig Sampler" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + outputs: + comment_id: "" + comment_repo: "" + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Daily Rig Sampler" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-rig-sampler.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AGENT_VERSION: "1.0.52" + GH_AW_INFO_CLI_VERSION: "v0.77.5" + GH_AW_INFO_WORKFLOW_NAME: "Daily Rig Sampler" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","github","node"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + .antigravity + .claude + .codex + .crush + .gemini + .opencode + .pi + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_FILE: "daily-rig-sampler.lock.yml" + GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_COMPILED_VERSION: "v0.77.5" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + # poutine:ignore untrusted_checkout_exec + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" + { + cat << 'GH_AW_PROMPT_a110ec9a16ecfb3b_EOF' + + GH_AW_PROMPT_a110ec9a16ecfb3b_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_a110ec9a16ecfb3b_EOF' + + Tools: create_pull_request, missing_tool, missing_data, noop + GH_AW_PROMPT_a110ec9a16ecfb3b_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" + cat << 'GH_AW_PROMPT_a110ec9a16ecfb3b_EOF' + + GH_AW_PROMPT_a110ec9a16ecfb3b_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_a110ec9a16ecfb3b_EOF' + + The following GitHub context information is available for this workflow: + {{#if github.actor}} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if github.repository}} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if github.workspace}} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} + - **issue-number**: #__GH_AW_EXPR_802A9F6A__ + {{/if}} + {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} + - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ + {{/if}} + {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} + - **pull-request-number**: #__GH_AW_EXPR_463A214A__ + {{/if}} + {{#if github.event.comment.id || github.aw.context.comment_id}} + - **comment-id**: __GH_AW_EXPR_FF1D34CE__ + {{/if}} + {{#if github.run_id}} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_a110ec9a16ecfb3b_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_a110ec9a16ecfb3b_EOF' + + {{#runtime-import .github/workflows/daily-rig-sampler.md}} + GH_AW_PROMPT_a110ec9a16ecfb3b_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ALLOWED_EXTENSIONS: '' + GH_AW_CACHE_DESCRIPTION: '' + GH_AW_CACHE_DIR: '/tmp/gh-aw/cache-memory/' + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_ALLOWED_EXTENSIONS: process.env.GH_AW_ALLOWED_EXTENSIONS, + GH_AW_CACHE_DESCRIPTION: process.env.GH_AW_CACHE_DESCRIPTION, + GH_AW_CACHE_DIR: process.env.GH_AW_CACHE_DIR, + GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, + GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, + GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, + GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: activation + include-hidden-files: true + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw-prompts/prompt-template.txt + /tmp/gh-aw/aw-prompts/prompt-import-tree.json + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + /tmp/gh-aw/.github/agents + /tmp/gh-aw/.github/skills + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + issues: read + pull-requests: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_WORKFLOW_ID_SANITIZED: dailyrigsampler + outputs: + agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Daily Rig Sampler" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-rig-sampler.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Set runtime paths + id: set-runtime-paths + run: | + { + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" + } >> "$GITHUB_OUTPUT" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Create gh-aw temp directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} + # Cache memory file share configuration from frontmatter processed below + - name: Create cache-memory directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_cache_memory_dir.sh" + - name: Restore cache-memory file share data + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} + path: /tmp/gh-aw/cache-memory + restore-keys: | + memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}- + - name: Setup cache-memory git repository + env: + GH_AW_CACHE_DIR: /tmp/gh-aw/cache-memory + GH_AW_MIN_INTEGRITY: none + run: bash "${RUNNER_TEMP}/gh-aw/actions/setup_cache_memory_git.sh" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.52 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.55 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" + - name: Restore inline sub-agents from activation artifact + env: + GH_AW_SUB_AGENT_DIR: ".github/agents" + GH_AW_SUB_AGENT_EXT: ".agent.md" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" + - name: Restore inline skills from activation artifact + env: + GH_AW_SKILL_DIR: ".github/skills" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.55 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.55 ghcr.io/github/gh-aw-firewall/squid:0.25.55 ghcr.io/github/gh-aw-mcpg:v0.3.19 ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + - name: Generate Safe Outputs Config + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_a411d4098841fcb1_EOF' + {"create_pull_request":{"allowed_files":["skills/rig/rig.ts"],"draft":true,"labels":["automation","ai-agent"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review","reviewers":["copilot"],"title_prefix":"[rig-sampler] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_a411d4098841fcb1_EOF + - name: Generate Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[rig-sampler] \". Labels [\"automation\" \"ai-agent\"] will be automatically added. PRs will be created as drafts. Reviewers [\"copilot\"] will be assigned." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "create_pull_request": { + "defaultMax": 1, + "fields": { + "base": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "draft": { + "type": "boolean" + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + }, + "report_incomplete": { + "defaultMax": 5, + "fields": { + "details": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 1024 + } + } + } + } + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + await main(); + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + run: | + set -eo pipefail + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="8080" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + case "${DOCKER_HOST:-}" in + unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; + /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; + * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; + esac + DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.19' + + mkdir -p /home/runner/.copilot + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_5b483aba2b2317da_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + { + "mcpServers": { + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_5b483aba2b2317da_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" + - name: Start CLI Proxy + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_SERVER_URL: ${{ github.server_url }} + CLI_PROXY_POLICY: '{"allow-only":{"repos":"all","min-integrity":"none"}}' + CLI_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.19' + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/start_cli_proxy.sh" + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 30 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.55/awf-config.schema.json","network":{"allowDomains":["*.githubusercontent.com","api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.npms.io","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","bun.sh","cdn.jsdelivr.net","codeload.github.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","deb.nodesource.com","deno.land","docs.github.com","esm.sh","get.pnpm.io","github-cloud.githubusercontent.com","github-cloud.s3.amazonaws.com","github.blog","github.com","github.githubassets.com","googleapis.deno.dev","googlechromelabs.github.io","host.docker.internal","json-schema.org","json.schemastore.org","jsr.io","keyserver.ubuntu.com","lfs.github.com","nodejs.org","npm.pkg.github.com","npmjs.com","npmjs.org","objects.githubusercontent.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","patch-diff.githubusercontent.com","ppa.launchpad.net","raw.githubusercontent.com","registry.bower.io","registry.npmjs.com","registry.npmjs.org","registry.yarnpkg.com","repo.yarnpkg.com","s.symcb.com","s.symcd.com","security.ubuntu.com","skimdb.npmjs.com","storage.googleapis.com","telemetry.enterprise.githubcopilot.com","telemetry.vercel.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com","www.npmjs.com","www.npmjs.org","yarnpkg.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.55"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull --difc-proxy-host host.docker.internal:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \ + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir /tmp/gh-aw/cache-memory/ --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.77.5 + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || github.token }} + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Stop CLI Proxy + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/stop_cli_proxy.sh" + - name: Detect agent errors + if: always() + id: detect-agent-errors + continue-on-error: true + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.jsdelivr.net,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,docs.github.com,esm.sh,get.pnpm.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,googleapis.deno.dev,googlechromelabs.github.io,host.docker.internal,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,lfs.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.yarnpkg.com,s.symcb.com,s.symcd.com,security.ubuntu.com,skimdb.npmjs.com,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + await main(); + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + - name: Commit cache-memory changes + if: always() + env: + GH_AW_CACHE_DIR: /tmp/gh-aw/cache-memory + run: bash "${RUNNER_TEMP}/gh-aw/actions/commit_cache_memory_git.sh" + - name: Upload cache-memory data as artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: always() + with: + name: cache-memory + include-hidden-files: true + path: /tmp/gh-aw/cache-memory + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/agent_usage.json + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt + /tmp/gh-aw/agent/ + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + /tmp/gh-aw/awf-config.json + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + - update_cache_memory + if: > + always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || + needs.activation.outputs.stale_lock_file_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + concurrency: + group: "gh-aw-conclusion-daily-rig-sampler" + cancel-in-progress: false + queue: max + outputs: + incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Daily Rig Sampler" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-rig-sampler.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Process no-op messages + id: noop + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Daily Rig Sampler" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/daily-rig-sampler.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Log detection run + id: detection_runs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Daily Rig Sampler" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/daily-rig-sampler.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + await main(); + - name: Record missing tool + id: missing_tool + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Daily Rig Sampler" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/daily-rig-sampler.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Record incomplete + id: report_incomplete + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Daily Rig Sampler" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/daily-rig-sampler.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + await main(); + - name: Handle agent failure + id: handle_agent_failure + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Daily Rig Sampler" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/daily-rig-sampler.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "daily-rig-sampler" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ENGINE_ID: "copilot" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} + GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} + GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} + GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" + GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} + GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" + GH_AW_TIMEOUT_MINUTES: "30" + GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" + GH_AW_CACHE_MEMORY_ENABLED: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + + detection: + needs: + - activation + - agent + if: > + always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_reason: ${{ steps.detection_conclusion.outputs.reason }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Daily Rig Sampler" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-rig-sampler.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # --- Threat Detection --- + - name: Clean stale firewall files from agent artifact + run: | + rm -rf /tmp/gh-aw/sandbox/firewall/logs + rm -rf /tmp/gh-aw/sandbox/firewall/audit + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.55 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 ghcr.io/github/gh-aw-firewall/squid:0.25.55 + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP Config for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + WORKFLOW_NAME: "Daily Rig Sampler" + WORKFLOW_DESCRIPTION: "Each day, pick one rig sample (cached round-robin) from src/samples/, run it with the rig skill, analyze harness performance, and apply one focused quick-win improvement to skills/rig/rig.ts in a new draft PR." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.52 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.55 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.55/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.55"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.77.5 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + with: + script: | + try { + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError && !detectionExecutionFailed) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/daily-rig-sampler" + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_ENGINE_VERSION: "1.0.52" + GH_AW_WORKFLOW_ID: "daily-rig-sampler" + GH_AW_WORKFLOW_NAME: "Daily Rig Sampler" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/daily-rig-sampler.md" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + created_pr_number: ${{ steps.process_safe_outputs.outputs.created_pr_number }} + created_pr_url: ${{ steps.process_safe_outputs.outputs.created_pr_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Daily Rig Sampler" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-rig-sampler.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Extract base branch from agent output + id: extract-base-branch + if: steps.download-agent-output.outcome == 'success' + shell: bash + run: | + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + BASE_BRANCH=$("$GH_AW_NODE" -e " + try { + const data = JSON.parse(require('fs').readFileSync('/tmp/gh-aw/agent_output.json', 'utf8')); + const item = (data.items || []).find(i => + (i.type === 'create_pull_request' || i.type === 'push_to_pull_request_branch') && + i.base_branch + ); + if (item) process.stdout.write(item.base_branch); + } catch(e) {} + " 2>/dev/null || true) + # Validate: only allow safe git branch name characters + if [[ "$BASE_BRANCH" =~ ^[a-zA-Z0-9/_.-]+$ ]] && [ ${#BASE_BRANCH} -le 255 ]; then + printf 'base-branch=%s\n' "$BASE_BRANCH" >> "$GITHUB_OUTPUT" + echo "Extracted base branch from safe output: $BASE_BRANCH" + fi + fi + - name: Checkout repository (trusted default branch for comment events) + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.repository.default_branch }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + persist-credentials: false + fetch-depth: 1 + - name: Checkout repository + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ steps.extract-base-branch.outputs.base-branch || github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + persist-credentials: false + fetch-depth: 1 + - name: Configure Git credentials + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.jsdelivr.net,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,docs.github.com,esm.sh,get.pnpm.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,googleapis.deno.dev,googlechromelabs.github.io,host.docker.internal,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,lfs.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.yarnpkg.com,s.symcb.com,s.symcd.com,security.ubuntu.com,skimdb.npmjs.com,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"allowed_files\":[\"skills/rig/rig.ts\"],\"draft\":true,\"labels\":[\"automation\",\"ai-agent\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"request_review\",\"reviewers\":[\"copilot\"],\"title_prefix\":\"[rig-sampler] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Outputs Items + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: safe-outputs-items + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json + if-no-files-found: ignore + + update_cache_memory: + needs: + - activation + - agent + - detection + if: always() && needs.detection.result == 'success' && needs.agent.result == 'success' + runs-on: ubuntu-slim + permissions: {} + env: + GH_AW_WORKFLOW_ID_SANITIZED: dailyrigsampler + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.5 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Daily Rig Sampler" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-rig-sampler.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download cache-memory artifact (default) + id: download_cache_default + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + continue-on-error: true + with: + name: cache-memory + path: /tmp/gh-aw/cache-memory + - name: Check if cache-memory folder has content (default) + id: check_cache_default + shell: bash + run: | + if [ -d "/tmp/gh-aw/cache-memory" ] && [ "$(ls -A /tmp/gh-aw/cache-memory 2>/dev/null)" ]; then + echo "has_content=true" >> "$GITHUB_OUTPUT" + else + echo "has_content=false" >> "$GITHUB_OUTPUT" + fi + - name: Save cache-memory to cache (default) + if: steps.check_cache_default.outputs.has_content == 'true' + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} + path: /tmp/gh-aw/cache-memory + diff --git a/.github/workflows/daily-rig-sampler.md b/.github/workflows/daily-rig-sampler.md new file mode 100644 index 0000000..5e19224 --- /dev/null +++ b/.github/workflows/daily-rig-sampler.md @@ -0,0 +1,97 @@ +--- +name: Daily Rig Sampler +description: > + Each day, pick one rig sample (cached round-robin) from src/samples/, + run it with the rig skill, analyze harness performance, and apply one + focused quick-win improvement to skills/rig/rig.ts in a new draft PR. +on: + schedule: daily + workflow_dispatch: +permissions: + contents: read + actions: read + issues: read + pull-requests: read +engine: copilot +strict: true +timeout-minutes: 30 +tools: + github: + mode: gh-proxy + toolsets: [default] + bash: ["*"] + edit: + cache-memory: true +network: + allowed: [defaults, github, node] +safe-outputs: + create-pull-request: + title-prefix: "[rig-sampler] " + labels: [automation, ai-agent] + draft: true + reviewers: [copilot] + allowed-files: + - "skills/rig/rig.ts" +--- + +## Task + +You are improving the rig TypeScript harness one focused change at a time. + +### Step 1 — Pick the next sample (cached round-robin) + +1. Run `ls src/samples/` to list all sample files sorted alphabetically. +2. Open the cache-memory file `/tmp/gh-aw/cache-memory/last-sample.json`. + - If it does not exist, start from the first file. + - Otherwise read the `lastFile` field and advance to the next file in the sorted list (wrapping around). +3. Write `{"lastFile": ""}` back to `/tmp/gh-aw/cache-memory/last-sample.json`. +4. Note the chosen sample file path, e.g. `src/samples/05-write-readme-intent.ts`. + +### Step 2 — Install dependencies and run the sample + +1. Run `npm install` in the repository root to ensure all dependencies are present. +2. Run the chosen sample through the rig skill launcher: + ``` + node skills/rig/rig.ts 2>&1 + ``` + Capture both stdout (the agent's structured JSON output) and stderr (JSONL event lines prefixed with `rig.copilot-ask`). +3. Record the full output for analysis. + +### Step 3 — Analyze harness performance + +With the captured run output, evaluate: + +- **Did it succeed?** Did the agent return valid JSON matching the declared output schema, or did it fail? +- **Repair turns**: Count how many times the harness had to retry due to invalid JSON or schema violations (`rig.copilot-ask` events with `turns > 1`). +- **Turn count and latency**: Note total turns from the JSONL events. +- **Schema fit**: Did the sample's output schema feel too loose (e.g. plain `s.string` where a structured type would help) or too strict (repair loops due to enum mismatches)? +- **Error messages**: Were any error messages unclear or unhelpful? +- **API ergonomics**: Was there anything awkward in how the sample had to express its intent — boilerplate that a helper could eliminate, or a missing convenience on `p.*` or `s.*`? + +### Step 4 — Read rig.ts + +Read `skills/rig/rig.ts` in full to understand the current implementation. + +### Step 5 — Identify one quick-win improvement + +Based on your analysis of the actual run, identify **exactly one** small, self-contained improvement to `skills/rig/rig.ts`. + +Good categories (pick the one most directly supported by the run evidence): +- A missing `s.*` schema helper that would simplify sample code or prevent a repair loop. +- A clearer error message surfaced during repair or schema validation. +- A JSDoc comment on a public export that is currently undocumented. +- A small type-safety improvement (stricter overload, narrower generic). +- A minor performance or usability tweak with no behaviour change. + +Do **not** change the public API in a breaking way. Keep the change small and reviewable. + +### Step 6 — Apply the improvement + +Edit `skills/rig/rig.ts` to implement the improvement. Use the `edit` tool. + +### Step 7 — Create a pull request + +Emit a `create-pull-request` output with: +- `title`: one-line description of the improvement (no prefix needed — it is added automatically). +- `body`: explain which sample was run, what the run revealed (repair turns, output quality, etc.), and why this change improves the harness. +- `branch`: `rig-sampler/` (e.g. `rig-sampler/05-write-readme-intent`). diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d4946b6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,107 @@ +name: Release + +on: + workflow_dispatch: + inputs: + release_type: + description: "Release type" + required: true + type: choice + options: + - patch + - minor + - major + +jobs: + release: + runs-on: ubuntu-latest + # Only run on the origin repo (not forks) + if: github.repository == 'pelikhan/rig' + permissions: + contents: write + + steps: + - name: Check actor permission + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: context.actor, + }); + const level = perm.permission; + if (level !== 'admin' && level !== 'maintain') { + core.setFailed(`Actor ${context.actor} has permission '${level}', which is insufficient. Admin or maintain required.`); + } + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Use Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 24 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Typecheck + run: npm run typecheck + + - name: Compute next version + id: semver + run: | + # Get the latest semver tag (vX.Y.Z), default to v0.0.0 if none exists + LAST_TAG=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-version:refname | head -n 1) + if [ -z "$LAST_TAG" ]; then + LAST_TAG="v0.0.0" + fi + echo "Last tag: $LAST_TAG" + + # Strip leading 'v' + VERSION="${LAST_TAG#v}" + MAJOR=$(echo "$VERSION" | cut -d. -f1) + MINOR=$(echo "$VERSION" | cut -d. -f2) + PATCH=$(echo "$VERSION" | cut -d. -f3) + + RELEASE_TYPE="${{ github.event.inputs.release_type }}" + case "$RELEASE_TYPE" in + major) + MAJOR=$((MAJOR + 1)) + MINOR=0 + PATCH=0 + ;; + minor) + MINOR=$((MINOR + 1)) + PATCH=0 + ;; + patch) + PATCH=$((PATCH + 1)) + ;; + esac + + NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}" + echo "New tag: $NEW_TAG" + echo "tag=$NEW_TAG" >> "$GITHUB_OUTPUT" + + - name: Create and push tag + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag "${{ steps.semver.outputs.tag }}" + git push origin "${{ steps.semver.outputs.tag }}" + + - name: Create GitHub Release + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + await github.rest.repos.createRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: '${{ steps.semver.outputs.tag }}', + name: '${{ steps.semver.outputs.tag }}', + generate_release_notes: true, + }); diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..dbd4bd7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "github.copilot.enable": { + "markdown": true + } +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..604dc23 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,61 @@ +# AGENTS.md + +## Project Overview + +Rig is a minimal TypeScript agent harness. The core runtime (`skills/rig/rig.ts`) provides declarative agent construction with typed input/output shapes, prompt intents, and a Copilot SDK runtime. + +## Architecture + +``` +skills/rig/rig.ts — Core runtime (agent, p, copilotEngine, schemas) +skills/rig/samples/ — 51 sample agents demonstrating patterns +src/engines/copilot.test.ts — Copilot engine unit tests (vitest) +src/rig.test.ts — Unit tests (vitest) +scripts/run-sample.test.ts — Sample runner with a stub Copilot SDK client (dry-run) +skills/rig/SKILL.md — Framework reference docs +``` + +All imports use the `"rig"` path alias (resolved via tsconfig paths + vitest alias). `copilotEngine` is exported directly from `rig` for client construction. + +## Commands + +| Task | Command | +|------|---------| +| Typecheck | `npm run typecheck` | +| Unit tests | `npm test` | +| Run samples (stub) | `npm run sample` | +| Run single sample (stub) | `RIG_SAMPLE=02 npm run sample` | +| Run a sample for real | `echo "" \| node skills/rig/rig.ts ` (`npm run sample:run`) | + +## Code Style + +- Keep the core (`skills/rig/rig.ts`) self-contained; `@github/copilot-sdk` is imported directly in `skills/rig/rig.ts` +- Minimal comments; code should be self-explanatory +- Use `node:` prefix for Node.js built-in imports +- Types are colocated with the module that defines them, not in separate `.d.ts` files +- Trailing underscore on object keys (`key_`) means optional field +- Do not add legacy compatibility bridges; update callers, samples, and docs to the current API + +## Testing + +- Framework: vitest +- Tests live in `src/rig.test.ts` (agent definition, invocation, validation, and prompt intent coverage) +- Stub the Copilot SDK client with `vi.mock("@github/copilot-sdk", ...)` +- All unit tests must pass before committing +- Samples run via a stub Copilot SDK client that synthesizes shape-conforming output from the prompt's `` block + +## Key Concepts + +- **Shape descriptors**: JS values used as type exemplars (e.g., `""` = string, `0` = number, `[""]` = string array). Promoted to schemas via `SchemaLike`. +- **Schema helpers (`s.*`)**: `s.string`, `s.number`, `s.boolean`, `s.unknown`, `s.array`, `s.object`, `s.record`, `s.enum`, `s.optional` +- **Prompt intents (`p.*`)**: `p.bash(cmd)`, `p.read(path)`, `p.write(path, content)` — declarative placeholders resolved into prompt instructions, not executed in-process +- **Prompts**: `p\`...\`` template tag composes instructions with inline `p.*` helpers +- **Runtime transport**: Copilot SDK sessions are created by the harness; use launcher `--server` to switch to stdio transport. +- **Repair**: built-in addon re-prompts on parse/validation failure up to `maxTurns`, and other addons can still steer retry prompts. + +## Sample guide + +- `20-issue-reproducer.ts` — chained diagnosis, fix planning, and review +- `36-subagent-delegation.ts` — focused-agent delegation +- `47-prompt-intents.ts` — prompt intent primitives +- `50-end-to-end-release-agent.ts` — end-to-end release workflow orchestration diff --git a/README.md b/README.md index 88d6667..6377eea 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,272 @@ # rig -Harness as a Skill + +`rig` is a minimal TypeScript agent harness skill designed to run inside sandboxed agentic workflows embedded in markdown. + +## Install + +```bash +npm install github:pelikhan/rig#v0.0.8 +``` + +Or install the skill for Copilot coding agent: + +```bash +gh skills clone pelikhan/rig +``` + +## Core API + +```ts +import { + agent, + defineTool, + p, + s, +} from "rig"; +import { addons, oncePerSession, repair, steering, timeout } from "rig/addons"; +``` + +- `agent(spec)` creates a typed agent function. +- `s.*` defines input/output schemas. Omit `input`/`output` when free-form strings are enough. +- `p.*` creates declarative prompt intents for prompt templates or inputs. +- `p()` and ``p`...` `` create a prompt builder with `var`, `write`, and `region` primitives for assembling prompts. +- ``p`...` `` also works in `instructions` to embed prompt intents directly: `` instructions: p`Review ${p.bash("git status")}` ``. +- `defineTool(name, config)` matches the Copilot SDK helper and accepts rig `s.*` schemas for `parameters`. +- `addons` accepts express-like `(context, next)` turn addons for steering, inline validation, and Copilot session access. +- `rig` starts with no default addons. +- `rig/addons` provides optional addon helpers: `oncePerSession`, `repair`, `steering`, `timeout`, and `addons.{oncePerSession,repair,steering,timeout}`. +- `p\`...\`` returns a prompt builder and renders intent values when coerced to string; prefer `${p.read(...)}` / `${p.bash(...)}` when the context source is already known. + +## Embedding in markdown + +Use `rig` code fences in markdown files to define runnable harness programs: + +````markdown +```rig +export default "Summarize this repository."; +``` +```` + +Extract the `rig` fence and run it with: + +```bash +awk '/^```rig$/{in_block=1;next}/^```$/{if(in_block){exit}}in_block' ./program.md | node skills/rig/rig.ts +``` + +## Quick start + +```ts +import { agent, p, s } from "rig"; + +// Agent role: extract package scripts and summarize what they do. +const extractScripts = agent({ + model: "nano", + instructions: p`Read ${p.read("package.json")} and summarize the package scripts. Use ${p.bash("find src -name '*.ts' -type f | sort")} only to call out source files that look relevant.`, + output: s.object({ + scriptsByName: s.record(s.string), + summary: s.string, + relatedFiles: s.array(s.string), + }), +}); + +export default extractScripts; +``` + +When the context already lives in the workspace, prefer intent templates like the example above over adding `input` fields just to shuttle shell output or file contents. Favor `p.read("path")` over `p.bash("cat path")`, and let the harness work from files instead of assembling large in-memory strings first. + +## Schemas + +```ts +s.string +s.string("description") +s.number +s.boolean +s.unknown +s.array(item, "description") +s.object(fields, "description") +s.record(value, "description") +s.enum(...values) +s.enum(values, "description") +s.optional(shape) +s.optional(shape, "description") +``` + +Use declarative `s.*` helpers for every schema node. +`s.*` values serialize directly as JSON Schema, so no separate conversion step is needed. +Rig renders these declarations as JSON Schema in prompt schema blocks. +Implicit object literals, trailing-underscore optional fields, and `{"*": ...}` record sugar are not supported. + +## Prompt intents + +Prompt intents for shell and file operations are optimized for sandboxed agentic workflows. They assume the harness is already running with the required constraints and protections, so the generated instructions tell the agent to execute the action directly instead of adding extra permission prompts. + +```ts +p.bash("git status --short") +p.bash("npm test") +p.read("README.md") +p.write("README.md", "# Updated\n") + +const reviewWorkspace = agent({ + instructions: p`Review ${p.read("README.md")} against ${p.bash("git status --short")}.`, +}); +``` + +```ts +const b = p(); +const repo = b.var("repo", "rig"); +b.write("Summarize repository ", repo, ".\n"); +b.write("Start by checking ", b.bash("git status --short"), ".\n"); +b.region("ts", "type Summary = { text: string };"); +const prompt = b.toString(); +``` + +## Tools + +Register custom tools directly on an agent using the same shape as `@github/copilot-sdk`: + +```ts +const lookupIssue = defineTool("lookup_issue", { + description: "Look up an issue by id.", + parameters: s.object({ + issue: s.string, + }), + handler: async ({ issue }) => `Issue ${issue}`, +}); + +const triage = agent({ + instructions: "Use lookup_issue before answering.", + tools: [lookupIssue], +}); +``` + +Rig defaults agent tools to `skipPermission: true`, and you can also place plain tool objects in `tools`; rig will convert `s.*` parameter schemas into JSON Schema before creating the Copilot session. + +## Evaluating agentic performance + +Use these samples to quickly gauge how well `rig` supports increasingly agentic workflows: + +- `skills/rig/samples/20-issue-reproducer.md` — chained diagnose/fix flow +- `skills/rig/samples/36-subagent-delegation.md` — delegation between focused agents +- `skills/rig/samples/47-prompt-intents.md` — prompt intents embedded directly in prompt templates +- `skills/rig/samples/50-end-to-end-release-agent.md` — multi-step release planning workflow +- `docs/rig-syntax-genaiscript-comparison.md` — comparison of rig syntax with 10 GitHub GenAIScript sample ports + +## Agent behavior + +- Default model: `gpt-4.1` +- Default max turns: `4` +- No addons are loaded by default (including repair/retry behavior) + +Per call, you can override `model`, `timeout`, `maxTurns`, and `signal`. + +## Addons + +Each agent call runs a per-turn addon chain: + +```ts +const steerFinalTurn = async (context, next) => { + await next(); + if (context.nextPrompt && context.turn === context.maxTurns - 1) { + context.nextPrompt = `${context.nextPrompt}\nYou are running out of turns. Return corrected JSON now.`; + } +}; + +const review = agent({ + maxTurns: 3, + addons: steerFinalTurn, +}); +``` + +`context` includes `prompt`, `response`, `turn`, `maxTurns`, `signal`, `output`, `nextPrompt`, `error`, and `completed`. + +For the common retry flow with last-turn steering or stable default timeouts, opt into addons: + +```ts +const review = agent({ + maxTurns: 3, + addons: [timeout({ timeout: 30_000 }), steering(), repair], +}); +``` + +For direct SDK access, use `oncePerSession(...)` to register with the session once: + +```ts +const review = agent({ + addons: oncePerSession((session) => { + session.on?.((event) => { + // custom event handling + }); + }), +}); +``` + +Per-turn addons still receive `context.session` directly, and you can also register addons after creating the agent: + +```ts +const timingAddon = async (context, next) => { + await next(); +}; + +const review = agent({}); +review.use(timingAddon); +``` + +## Copilot SDK runtime + +`rig` is specialized for Copilot SDK sessions inside sandboxed agentic workflows. + +By default it connects to an already-running Copilot server via HTTP (`COPILOT_SDK_URI`, then `localhost:7777`). +Pass `--server` to spawn the server over stdio when launching a program. +Run `node skills/rig/rig.ts --help` for CLI usage; the launcher also accepts common help aliases such as `-h`, `help`, `/help`, and `/?`. + +For runnable programs, you can pipe a rig program directly on stdin (assumes the Copilot server is already running): + +```bash +cat <<'RIG' | node skills/rig/rig.ts +import { p } from "rig"; +export default p`Summarize this repository and include highlights from ${p.read("README.md")}.`; +RIG +``` + +Inline stdin programs run a default-exported root program with no required external input and write the result to stdout. Export either an agent, a string, or a prompt builder. If `export default` is omitted, the harness defaults to the first `const/let/var name = agent(...)` assignment. +`import { agent, p, s } from "rig"` is optional in inline mode because the harness injects it when missing. + +Inline mode accepts root agents that either omit `input`, use `input: s.object({})`, or rely on the default `input: s.string` (which is invoked with `""`). + +Pass `--server` to start the Copilot server automatically as part of the run: + +```bash +cat ./program.ts | node skills/rig/rig.ts --server +``` + +Pass `--typecheck` to typecheck the rig program before execution: + +```bash +cat ./program.ts | node skills/rig/rig.ts --typecheck +``` + +To run a root program from a program file, export the root value as the default export and pass input on stdin: + +```bash +echo "Summarize this repository" | node skills/rig/rig.ts src/program.ts +``` + +Program-file mode also supports `--typecheck`: + +```bash +echo "Summarize this repository" | node skills/rig/rig.ts src/program.ts --typecheck +``` + +For program-file mode stdin coercion: +- if root input schema is `string`, stdin is passed as raw text +- if root input schema is an object containing `text`, stdin is passed as `{ text: "" }` +- otherwise stdin must be valid JSON for the declared input schema + +Copilot SDK lifecycle events and rig request events are logged to stderr as JSONL. + +## Local development + +```bash +npm test +npm run typecheck +``` diff --git a/docs/rig-samples-agent-pov-review.md b/docs/rig-samples-agent-pov-review.md new file mode 100644 index 0000000..c1550a6 --- /dev/null +++ b/docs/rig-samples-agent-pov-review.md @@ -0,0 +1,57 @@ +# Rig samples review from an agent POV + +This review critiques sample design, syntax, and intuitiveness across `src/samples/*` and `skills/rig/samples/*`. + +## What works well + +1. **Small core surface area** + The `agent + s + p` API is compact and repeatable, which makes generation predictable once the pattern is learned. + +2. **Strong output contracts** + Most samples use explicit `s.object(...)` outputs and enums, which is friendly to repair loops and machine consumption. + +3. **Real task coverage** + The catalog covers practical workflows (triage, CI diagnosis, release planning, delegation) instead of toy prompts. + +## Design and intuitiveness issues + +1. **Root-agent mental model is unclear in multi-agent examples** + Several orchestrated samples run multiple agents but export a subagent as default (`20-issue-reproducer.ts`, `36-subagent-delegation.ts`, `50-end-to-end-release-agent.ts`, `51-claude-design.ts`, `53-ralf-loop.ts`). + For an agent author, this makes "what is the runnable root?" ambiguous. + +2. **Sample style drifts from documented guidance** + Docs recommend `p.read(...)` when reading files, but samples still frequently use `p.bash("cat ...")` (`23-schema-inference.ts`, `41-parse-coverage.ts`, and others). + This weakens trust in the docs as the canonical source. + +3. **Verbose schema ceremony for simple tasks** + The explicit schema style is correct, but for quick one-shot tasks the amount of typing can feel heavier than direct SDK usage. + +4. **Inline skill sample trust is currently broken by a failing example** + `skills/rig/samples/52-claude-design.md` exports a root agent that requires input, but stdin inline mode expects no-input root agents. + A failing checked-in sample reduces confidence for both humans and agents. + +## Syntax critique + +- **Good:** declarative schemas and explicit enums reduce ambiguity. +- **Good:** `p\`\`` with inline intents is expressive for agent-authored prompts. +- **Rough edge:** syntax is concise at runtime but still repetitive in examples with many tiny helper agents. +- **Rough edge:** mixed formatting styles across samples (some compact, some highly expanded) make pattern extraction harder for generation agents. + +## Would an agent choose `rig` or Copilot SDK directly? + +### Agent would likely choose `rig` when + +- the task is schema-first and expects strict JSON output; +- shell/file context can be expressed with `p.read/p.bash` intents; +- the goal is to generate a runnable sample quickly with consistent scaffolding. + +### Agent would likely choose Copilot SDK directly when + +- orchestration needs custom session lifecycle control, transport, or event handling; +- the workflow is long-running and not naturally expressed as one root `agent(...)` contract; +- the task needs non-standard control flow where harness conventions become constraints. + +## Bottom line + +`rig` is more attractive than direct Copilot SDK for most sample-style, structured tasks because it compresses boilerplate into a predictable pattern. +However, sample inconsistencies (root export ambiguity, file-read style drift, and one failing markdown sample) currently make the experience feel less intuitive than it could be. diff --git a/docs/rig-syntax-copilot-pi-agent-comparison.md b/docs/rig-syntax-copilot-pi-agent-comparison.md new file mode 100644 index 0000000..5df8c5e --- /dev/null +++ b/docs/rig-syntax-copilot-pi-agent-comparison.md @@ -0,0 +1,57 @@ +# Rig syntax comparison: Copilot SDK APIs and pi-agent SDK + +This page compares how `rig` syntax maps to two other harness styles: + +- GitHub Copilot SDK APIs (API usage only) +- pi-agent SDK + +The focus is generation reliability: what an agent can produce quickly with low ambiguity. + +## 1) Syntax mapping overview + +| Rig concept | Copilot SDK APIs (only) | pi-agent SDK | +|---|---|---| +| `agent({ name, instructions, input, output })` | App-level session setup + prompt contract + response parsing/validation | Agent definition/config + prompt contract + response parsing/validation | +| `s.object(...)`, `s.enum(...)`, `s.array(...)` | Explicit JSON schema or prompt-constrained JSON validated in app code | Same pattern: schema-constrained JSON validated by the harness/app | +| `p.read(...)`, `p.bash(...)` | Tool/context calls orchestrated by the host app before/within turns | Tool/context calls via pi-agent tool integration/orchestration | +| `agents: { subagent }` | Multiple sessions/roles coordinated in app orchestration | Multi-agent graph/delegation orchestration | +| `maxTurns`, optional `rig/addons` repair addon | Explicit retry + repair loop in app logic | Retry/repair policies in agent workflow/harness | +| `permissions` | Host-side policy gates around shell/write operations | Host-side tool permission policies | + +## 2) Top 10 scenarios: Copilot SDK APIs (ranked easiest → hardest) + +| Rank | Scenario | Ease/confusion rationale | Rig → Copilot SDK mapping note | +|---:|---|---|---| +| 1 | Single-field summarizer | Minimal schema and deterministic shape keep ambiguity very low. | `output: { text }` maps to one session request and strict JSON parse. | +| 2 | Enum classifier | Closed label sets reduce drift and invalid output. | `s.enum(...)` maps to constrained JSON contract validated after response. | +| 3 | Structured extractor | Input text to typed fields is usually straightforward. | `input/output` schemas map to one-turn extraction with validation. | +| 4 | Diff summary | Slightly larger context but objective remains clear. | Diff as input + structured summary fields in response contract. | +| 5 | Test-log diagnosis | Interpretation complexity rises with noisy logs. | Log input + typed root-cause/next-step schema with validation. | +| 6 | PR triage recommendation | Requires prioritization judgment and policy interpretation. | One/two turns with constrained triage schema and confidence fields. | +| 7 | README draft generation | Creative synthesis adds style and completeness ambiguity. | Multi-section structured output with post-parse checks. | +| 8 | Release notes generation | Requires grouping/dedup across many commits. | Batched commit input + grouped typed output contract. | +| 9 | Schema-repairing extractor | Needs robust retry when output is invalid or partial. | `maxTurns` plus optional `rig/addons` repair addon maps to explicit app-level validation/repair loop. | +| 10 | Multi-agent orchestrator | Highest coordination overhead across roles and merges. | `agents` maps to multi-session orchestration and aggregation logic. | + +## 3) Top 10 scenarios: pi-agent SDK (ranked easiest → hardest) + +| Rank | Scenario | Ease/confusion rationale | Rig → pi-agent mapping note | +|---:|---|---|---| +| 1 | Issue classification | Tight label set and simple context produce stable output. | `s.enum(...)` maps to constrained classification response schema. | +| 2 | Package script extraction | Deterministic source file extraction is low ambiguity. | `${p.read("package.json")}` maps to file-read tool + typed extraction. | +| 3 | Git diff summary | Clear input artifact, moderate reasoning complexity. | `${p.bash("git diff -- .")}` maps to shell tool + structured summary. | +| 4 | PR triage | Policy interpretation exists but remains bounded by schema. | Enum-heavy triage schema maps well to classifier-style flows. | +| 5 | Release notes draft | Cross-item synthesis introduces grouping ambiguity. | Multi-input retrieval + typed release-note sections. | +| 6 | Security scan review | Severity calibration and false-positive handling increase confusion risk. | Findings ingestion + risk rubric in structured output schema. | +| 7 | CI log diagnosis | Noisy traces and multiple plausible root causes. | Tooled log capture + hypothesis/next-action typed fields. | +| 8 | Flaky test analysis | Nondeterminism and weak signals increase uncertainty. | Repeated evidence aggregation + hypothesis confidence fields. | +| 9 | Subagent delegation planner | Coordination and dependency ordering are hard to generate correctly. | `agents` maps to multi-agent planning/delegation graph. | +| 10 | End-to-end release agent | Long chained workflow with coupled decisions and tools. | Orchestrated multi-step workflow with strict stage outputs. | + +## 4) Practical takeaways for lower-confusion generation + +1. Start with schema-tight, single-turn tasks first. +2. Prefer enums/literals for decision fields. +3. Keep context acquisition explicit (`read`/`bash`/tool calls) before generation. +4. Add repair loops only when necessary; they increase implementation complexity. +5. Introduce subagents last, after single-agent schema contracts are stable. diff --git a/docs/rig-syntax-genaiscript-comparison.md b/docs/rig-syntax-genaiscript-comparison.md new file mode 100644 index 0000000..93bb49e --- /dev/null +++ b/docs/rig-syntax-genaiscript-comparison.md @@ -0,0 +1,86 @@ +# Rig syntax comparison: GenAIScript `.genai.js` samples + +This review compares current `rig` syntax with 10 representative `.genai.js` samples found on GitHub on 2026-05-31. +The set mixes Microsoft-owned and community repositories that use GenAIScript's `script(...)`, `$`` prompt, `def(...)`, `defSchema(...)`, `defAgent(...)`, and `env/workspace/github` conventions. + +## Syntax mapping + +| GenAIScript pattern | Rig pattern | Notes | +|---|---|---| +| `script({ ... })` | `agent({ ... })` | `rig` puts name, model, instructions, schemas, and subagents in one declaration. | +| `$` template prompt | `instructions: p\`...\`` or a plain string | Both are readable; `rig` keeps prompt text inside the agent spec. | +| `def("NAME", value)` | `${p.read(...)}`, `${p.bash(...)}`, or inline prompt text | `rig` favors explicit prompt intents over mutable prompt variables. | +| `defSchema(...)` with JSON Schema | `s.object(...)`, `s.array(...)`, `s.enum(...)`, `s.optional(...)` | `rig` uses one explicit JSON Schema-compatible declaration style across samples and runtime. | +| `defAgent(...)` | `agents: { helper }` | `rig` subagents are declared as normal agents and attached structurally. | +| `workspace.*`, `github.*`, `env.vars.*` | prompt intents plus caller-provided input | `rig` exposes less ambient runtime state and pushes more context into the prompt contract. | +| top-level `await` workflow code | one declarative agent spec | `rig` is smaller and easier to generate, but less imperative for long scripted workflows. | + +## Reviewed samples and rig ports + +| Sample | Source | Rig port | +|---|---|---| +| glossary generation | [`glossary.genai.js`](https://raw.githubusercontent.com/microsoft/generative-ai-with-javascript/a7abd828d0a7b5d56f6e5450e5b26b250c33a392/docs/scripts/glossary.genai.js) | [`skills/rig/samples/55-genaiscript-glossary-port.md`](../skills/rig/samples/55-genaiscript-glossary-port.md) | +| batch refactor with helper agents | [`refactor.genai.js`](https://raw.githubusercontent.com/sinedied/genaiscript-talk/882eb643d6fcb854d22b939a870b0d0dd53d36da/genaisrc/refactor.genai.js) | [`skills/rig/samples/56-genaiscript-refactor-batch-port.md`](../skills/rig/samples/56-genaiscript-refactor-batch-port.md) | +| GitHub issue review | [`issue-review.genai.js`](https://raw.githubusercontent.com/sinedied/genaiscript-talk/882eb643d6fcb854d22b939a870b0d0dd53d36da/genaisrc/issue-review.genai.js) | [`skills/rig/samples/57-genaiscript-issue-review-port.md`](../skills/rig/samples/57-genaiscript-issue-review-port.md) | +| multi-agent travel plan | [`travel.genai.js`](https://raw.githubusercontent.com/sinedied/genaiscript-talk/882eb643d6fcb854d22b939a870b0d0dd53d36da/genaisrc/travel.genai.js) | [`skills/rig/samples/58-genaiscript-travel-plan-port.md`](../skills/rig/samples/58-genaiscript-travel-plan-port.md) | +| file-backed city extraction | [`cityinfo.genai.js`](https://raw.githubusercontent.com/darbotlabs/genaid/46c02f2e23082cb9c244483c9f36dd39212101b2/packages/sample/genaid/cityinfo.genai.js) | [`skills/rig/samples/59-genaiscript-city-info-port.md`](../skills/rig/samples/59-genaiscript-city-info-port.md) | +| schema-only city generator | [`defschema.genai.js`](https://raw.githubusercontent.com/darbotlabs/genaid/46c02f2e23082cb9c244483c9f36dd39212101b2/packages/sample/genaid/defschema.genai.js) | [`skills/rig/samples/60-genaiscript-schema-cities-port.md`](../skills/rig/samples/60-genaiscript-schema-cities-port.md) | +| workspace file picker | [`list-files.genai.js`](https://raw.githubusercontent.com/darbotlabs/genaid/46c02f2e23082cb9c244483c9f36dd39212101b2/packages/sample/genaid/list-files.genai.js) | [`skills/rig/samples/61-genaiscript-list-files-port.md`](../skills/rig/samples/61-genaiscript-list-files-port.md) | +| TODO implementation helper | [`todo.genai.js`](https://raw.githubusercontent.com/darbotlabs/genaid/46c02f2e23082cb9c244483c9f36dd39212101b2/packages/sample/genaid/todo.genai.js) | [`skills/rig/samples/62-genaiscript-todo-port.md`](../skills/rig/samples/62-genaiscript-todo-port.md) | +| slide deck generator | [`slides.genai.js`](https://raw.githubusercontent.com/darbotlabs/genaid/46c02f2e23082cb9c244483c9f36dd39212101b2/packages/sample/genaid/slides.genai.js) | [`skills/rig/samples/63-genaiscript-slide-deck-port.md`](../skills/rig/samples/63-genaiscript-slide-deck-port.md) | +| code review persona | [`review-code.genai.js`](https://raw.githubusercontent.com/sinedied/grumpydev-mcp/7310acbda1b82a41e2f6b31bd064f612f169d6fc/genaisrc/review-code.genai.js) | [`skills/rig/samples/64-genaiscript-grumpy-review-port.md`](../skills/rig/samples/64-genaiscript-grumpy-review-port.md) | + +## JSON schema syntax review of the ports + +All 10 ports are now reviewed against the current rig schema style (explicit `s.*` declarations that serialize to JSON Schema): + +1. **55 glossary**: uses `s.object` with nested `s.array(s.object(...))` for strict term/definition pairs. +2. **56 refactor batch**: both root and helper agents use explicit object schemas for deterministic subagent handoff. +3. **57 issue review**: output shape is explicit (`summary` + `questions`) and consistent with prompt constraints. +4. **58 travel plan**: helper-agent and root-agent outputs are all explicitly typed with `s.object` + `s.array`. +5. **59 city info**: uses `s.array(s.object(...))` to enforce strongly typed tabular extraction. +6. **60 schema cities**: mirrors 59 and keeps the generated records schema-first. +7. **61 list files**: simple but explicit array-of-strings contract for stable tooling consumption. +8. **62 TODO**: nested object-array schema keeps file/plan entries constrained and machine-readable. +9. **63 slide deck**: deep nested schema (`slides[].title/bullets[]`) is explicit and unambiguous. +10. **64 grumpy review**: fixed shape (`findings[]`, `verdict`) keeps persona output structured. + +The reviewed set no longer relies on deprecated shorthand forms; it consistently uses canonical `s.*` schema declarations. + +## What GenAIScript makes easy + +1. Very fast top-level scripting with ambient helpers such as `workspace`, `github`, and `env`. +2. Natural imperative flows: find files, read them, call the model, then write output. +3. JSON Schema reuse through `defSchema(...)` without translating into another schema DSL. +4. Prompt assembly is concise when a script mostly glues inputs into one `$`` block. + +## What rig makes easier + +1. One canonical declaration shape: `agent({ name, model, instructions, output, agents })`. +2. Smaller syntax surface for generated code: `agent`, `s`, and `p` cover most examples. +3. Stronger schema readability for common cases because `s.object(...)` and `s.enum(...)` stay compact. +4. Cleaner separation between prompt contract and host-side side effects. +5. Better sample consistency because subagents use the same syntax as root agents. + +## Rig weaknesses exposed by the comparison + +1. File discovery is awkward compared with `workspace.findFiles(...)`; ports fall back to shell commands inside `p.bash(...)`. +2. There is no first-class equivalent to ambient `env.vars` sample parameters in inline markdown examples, so some ports must inline example values. +3. `rig` lacks a direct artifact-oriented pattern like GenAIScript's read/process/write script flow, which makes output-file generation examples less natural. +4. Raw JSON Schema copy/paste from external examples still needs translation into `s.*` helpers. +5. Long imperative workflows are harder to express directly because `rig` intentionally centers one agent spec over step-by-step runtime code. + +## GenAIScript weaknesses exposed by the comparison + +1. The ambient runtime (`env`, `workspace`, `github`, globals) is powerful but broad, which makes generation drift more likely. +2. Sample syntax mixes metadata, prompt text, file IO, agent definitions, and side effects at top level. +3. Reusing helper agents and schemas adds extra concepts (`def`, `defAgent`, `defSchema`) that beginners must learn quickly. +4. Output contracts rely more often on prompt wording and JSON Schema snippets than on one obvious canonical declaration shape. + +## Follow-ups to improve rig + +1. Add a first-class `p.glob(...)` or `p.findFiles(...)` helper so workspace enumeration does not require shell `find` commands. +2. Add a lightweight sample metadata helper for common patterns such as file-backed context, canned inputs, and generated artifact descriptions. +3. Document a canonical "generate artifact" pattern that combines `p.read(...)`, strict output schemas, and `p.write(...)` instructions. +4. Add one official multi-agent orchestration sample that mirrors GenAIScript's `defAgent(...)` style more directly. +5. Document JSON Schema to `s.*` translation rules for users porting prompts from GenAIScript or similar tools. diff --git a/docs/rig-syntax-review.md b/docs/rig-syntax-review.md new file mode 100644 index 0000000..0b2969a --- /dev/null +++ b/docs/rig-syntax-review.md @@ -0,0 +1,40 @@ +# Rig syntax review + +This review covers the current rig syntax as expressed in: + +- `src/rig.ts` +- `skills/rig/SKILL.md` +- all sample programs in `src/samples/` + +## Current state + +Rig now enforces a single declarative schema style: + +- use `s.object(...)` for object shapes +- use `s.array(...)` for arrays +- use `s.record(...)` for records +- use `s.optional(...)` for optional fields +- use `s.enum(...)` explicitly for union values + +Implicit schema syntax has been removed from the runtime and sample corpus. +Shorthand object literals, trailing-underscore optional fields, and `{"*": ...}` record sugar are no longer accepted. + +## What works well + +1. **One canonical schema dialect.** The runtime, samples, and skill documentation all reinforce the same `s.*` style. +2. **Small syntax surface.** `agent`, `s`, `p`, and call-time overrides remain enough to express the full harness. +3. **Consistent runtime contract.** Prompt rendering still emits ``, ``, ``, and ``, with optional `` and `` blocks. +4. **Better failure mode for invalid declarations.** Runtime validation now rejects non-declarative schemas with a clear error. + +## Remaining guidance + +1. Keep new samples in the explicit declarative style. +2. Prefer `s.optional(...)` over any naming convention for optional fields. +3. Treat `skills/rig/SKILL.md` and the early samples as the canonical starting points for generated code. +4. Continue keeping the runnable sample corpus green so checked-in examples stay trustworthy. + +## Validation + +- `npm test` +- `npm run typecheck` +- `npm run sample` diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a9a3f5f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1501 @@ +{ + "name": "rig", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rig", + "devDependencies": { + "@github/copilot-sdk": "^1.0.0-beta.9", + "@types/node": "^25.9.1", + "typescript": "^5.8.0", + "vitest": "^4.1.7", + "zx": "^8.8.5" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@github/copilot": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.55.tgz", + "integrity": "sha512-wqzI0L7krORW6jDAQPx7VnInka5BYN5yVgu+dpUK4w8xP5RgnOBa6kRoXpydj/9O1ufs0k6RKRtQjsVLp52TRw==", + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "detect-libc": "^2.1.2" + }, + "bin": { + "copilot": "npm-loader.js" + }, + "optionalDependencies": { + "@github/copilot-darwin-arm64": "1.0.55", + "@github/copilot-darwin-x64": "1.0.55", + "@github/copilot-linux-arm64": "1.0.55", + "@github/copilot-linux-x64": "1.0.55", + "@github/copilot-linuxmusl-arm64": "1.0.55", + "@github/copilot-linuxmusl-x64": "1.0.55", + "@github/copilot-win32-arm64": "1.0.55", + "@github/copilot-win32-x64": "1.0.55" + } + }, + "node_modules/@github/copilot-darwin-arm64": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.55.tgz", + "integrity": "sha512-v59pOpA7YO8j/lpDU/1E8l1Ag0hd26hIiEzTNbzqKd7tJpvhN0XTDWDCink50wXL656XIXt8lD8i8sGeD6yPfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ], + "bin": { + "copilot-darwin-arm64": "copilot" + } + }, + "node_modules/@github/copilot-darwin-x64": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.55.tgz", + "integrity": "sha512-XrJ9ent/9ogLk8yNp3TMsNVW0qTRDlkw/b34VnTgbAkJCaI3UVqaqpFn60Laa6J5mOPW0/JeKIkkva+7IJdqpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ], + "bin": { + "copilot-darwin-x64": "copilot" + } + }, + "node_modules/@github/copilot-linux-arm64": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.55.tgz", + "integrity": "sha512-5Q46Q72/l/U8KQRcBwYjzFPNXBCPG177FTmjEVOAH0qk7w58fMUDBEpnf9n1IpxYJDWQJ5BFGtLdfYgVVtkevw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "copilot-linux-arm64": "copilot" + } + }, + "node_modules/@github/copilot-linux-x64": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.55.tgz", + "integrity": "sha512-KWmMCDmKJivvOyDAAe5K8r7uSlVq8aZCh20VfrVXsc4bckO6KjXY/TOagrdBNqkk5rh8v63ghBbxFdWIOvEJRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "copilot-linux-x64": "copilot" + } + }, + "node_modules/@github/copilot-linuxmusl-arm64": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.55.tgz", + "integrity": "sha512-Jb5ug9Ic1pzxB2ZT1xoR8b3Ea1xnvCa4h8cBque51+TevXe6QF98vAfSUIwLe4xu+K6JKhiKEA0SD3w29Z74eA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "copilot-linuxmusl-arm64": "copilot" + } + }, + "node_modules/@github/copilot-linuxmusl-x64": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.55.tgz", + "integrity": "sha512-qMGIjHxKmW9q26EpoaNKWpmEVGyL/IM8ThVkh7yolDzv9lECFudPzT5yLX7f+VIiF6qWQlrQyzmamp7/fNQ2Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "copilot-linuxmusl-x64": "copilot" + } + }, + "node_modules/@github/copilot-sdk": { + "version": "1.0.0-beta.9", + "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.0-beta.9.tgz", + "integrity": "sha512-D4yiGL4/faFCjL7bozhX7bgxt/x1wp2LZ2p9Tw+xrA5hbcLh5Be5kPen+bFA8NbVfgt1G2djDYFZlrZjXXmcBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@github/copilot": "^1.0.55-5", + "vscode-jsonrpc": "^8.2.1", + "zod": "^4.3.6" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@github/copilot-win32-arm64": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.55.tgz", + "integrity": "sha512-TO4EJ8it6Qki7wMKYHqGUEDYmB0EAToy+pE5++OpydB6FijyQ31+/XwjvdnEFkuB4ZgPqu/6Y8hxMKucl2+FYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ], + "bin": { + "copilot-win32-arm64": "copilot.exe" + } + }, + "node_modules/@github/copilot-win32-x64": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.55.tgz", + "integrity": "sha512-TBMiSZMz8Dhx79JeSEM+7ONGxR5NmxfiDUdySo6thVbRmjS9D8msyAP8ucTsbLBJcTFeb7vsaeObD/ujYQgDtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ], + "bin": { + "copilot-win32-x64": "copilot.exe" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", + "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", + "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", + "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", + "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", + "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", + "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", + "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", + "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", + "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", + "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", + "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", + "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", + "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", + "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", + "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz", + "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz", + "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz", + "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz", + "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.7", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz", + "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.7", + "@vitest/utils": "4.1.7", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz", + "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz", + "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.7", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "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" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", + "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.132.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.2", + "@rolldown/binding-darwin-arm64": "1.0.2", + "@rolldown/binding-darwin-x64": "1.0.2", + "@rolldown/binding-freebsd-x64": "1.0.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", + "@rolldown/binding-linux-arm64-gnu": "1.0.2", + "@rolldown/binding-linux-arm64-musl": "1.0.2", + "@rolldown/binding-linux-ppc64-gnu": "1.0.2", + "@rolldown/binding-linux-s390x-gnu": "1.0.2", + "@rolldown/binding-linux-x64-gnu": "1.0.2", + "@rolldown/binding-linux-x64-musl": "1.0.2", + "@rolldown/binding-openharmony-arm64": "1.0.2", + "@rolldown/binding-wasm32-wasi": "1.0.2", + "@rolldown/binding-win32-arm64-msvc": "1.0.2", + "@rolldown/binding-win32-x64-msvc": "1.0.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.2.tgz", + "integrity": "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", + "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.2", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "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" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz", + "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.7", + "@vitest/mocker": "4.1.7", + "@vitest/pretty-format": "4.1.7", + "@vitest/runner": "4.1.7", + "@vitest/snapshot": "4.1.7", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "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" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "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.7", + "@vitest/browser-preview": "4.1.7", + "@vitest/browser-webdriverio": "4.1.7", + "@vitest/coverage-istanbul": "4.1.7", + "@vitest/coverage-v8": "4.1.7", + "@vitest/ui": "4.1.7", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz", + "integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zx": { + "version": "8.8.5", + "resolved": "https://registry.npmjs.org/zx/-/zx-8.8.5.tgz", + "integrity": "sha512-SNgDF5L0gfN7FwVOdEFguY3orU5AkfFZm9B5YSHog/UDHv+lvmd82ZAsOenOkQixigwH2+yyH198AwNdKhj+RA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "zx": "build/cli.js" + }, + "engines": { + "node": ">= 12.17.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3be2c41 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "rig", + "private": true, + "type": "module", + "exports": { + ".": "./skills/rig/rig.ts", + "./addons": "./skills/rig/addons.ts" + }, + "scripts": { + "test": "vitest run src/", + "test:integration": "vitest run scripts/haiku.integration.test.ts", + "sample": "vitest run scripts/run-sample.test.ts", + "sample:run": "node skills/rig/rig.ts", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@github/copilot-sdk": "^1.0.0-beta.9", + "@types/node": "^25.9.1", + "typescript": "^5.8.0", + "vitest": "^4.1.7", + "zx": "^8.8.5" + } +} diff --git a/scripts/haiku.integration.test.ts b/scripts/haiku.integration.test.ts new file mode 100644 index 0000000..868a67e --- /dev/null +++ b/scripts/haiku.integration.test.ts @@ -0,0 +1,128 @@ +import { spawn } from "node:child_process"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const token = process.env["COPILOT_GITHUB_TOKEN"]; +const itWithToken = token ? it : it.skip; +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const haikuSamplePath = resolve(repoRoot, "src/samples/01-single-agent-haiku.ts"); +const sonnetSamplePath = resolve(repoRoot, "src/samples/56-single-agent-sonnet.ts"); +const complexSamplePath = resolve(repoRoot, "src/samples/57-complex-integration-sonnet.ts"); +const launcherPath = resolve(repoRoot, "skills/rig/rig.ts"); +const INTEGRATION_TIMEOUT_MS = 120_000; + +async function runIntegrationSample(samplePath: string, input: string): Promise { + return new Promise((resolve, reject) => { + const child = spawn( + process.execPath, + [launcherPath, samplePath, "--server"], + { + cwd: repoRoot, + env: { ...process.env, COPILOT_GITHUB_TOKEN: token }, + }, + ); + + let stdout = ""; + let stderr = ""; + const timer = setTimeout(() => { + child.kill("SIGTERM"); + reject(new Error("Timed out waiting for haiku integration run.")); + }, INTEGRATION_TIMEOUT_MS); + + child.stdout.on("data", (chunk) => { + stdout += chunk.toString(); + }); + + child.stderr.on("data", (chunk) => { + stderr += chunk.toString(); + }); + + child.on("error", (error) => { + clearTimeout(timer); + reject(error); + }); + + child.on("close", (code) => { + clearTimeout(timer); + if (code !== 0) { + reject(new Error(`Haiku integration run failed with exit code ${code}.\n${stderr}`)); + return; + } + resolve(stdout); + }); + + child.stdin.end(input); + }); +} + +describe("rig runtime integration", () => { + itWithToken( + "runs a single-agent haiku sample with the real runtime", + async () => { + const stdout = await runIntegrationSample(haikuSamplePath, "autumn rain on city windows"); + const result = JSON.parse(stdout) as { haiku: string }; + expect(typeof result.haiku).toBe("string"); + const lines = result.haiku + .split(/\r?\n/g) + .map((line) => line.trim()) + .filter(Boolean); + expect(lines).toHaveLength(3); + }, + INTEGRATION_TIMEOUT_MS, + ); + + itWithToken( + "runs a single-agent sonnet sample with the real runtime", + async () => { + const stdout = await runIntegrationSample(sonnetSamplePath, "midnight train through fog"); + const result = JSON.parse(stdout) as { haiku: string }; + expect(typeof result.haiku).toBe("string"); + const lines = result.haiku + .split(/\r?\n/g) + .map((line) => line.trim()) + .filter(Boolean); + expect(lines).toHaveLength(3); + }, + INTEGRATION_TIMEOUT_MS, + ); + + itWithToken( + "runs a complex sonnet sample with tools, addons, intents, and subagent wiring", + async () => { + const stdout = await runIntegrationSample( + complexSamplePath, + JSON.stringify({ + topic: "ship a stable release process", + audience: "maintainers", + }), + ); + const result = JSON.parse(stdout) as { + headline: string; + checklist: string[]; + riskLevel: "low" | "medium" | "high"; + nextActions: Array<{ owner: string; action: string }>; + contextDigest: { + repository: string; + usedFeatures: string[]; + toolHint: string; + }; + }; + + expect(typeof result.headline).toBe("string"); + expect(result.headline.length).toBeGreaterThan(0); + expect(Array.isArray(result.checklist)).toBe(true); + expect(result.checklist.length).toBeGreaterThan(0); + expect(["low", "medium", "high"]).toContain(result.riskLevel); + expect(Array.isArray(result.nextActions)).toBe(true); + expect(result.nextActions.length).toBeGreaterThan(0); + expect(typeof result.contextDigest.repository).toBe("string"); + expect(result.contextDigest.repository.length).toBeGreaterThan(0); + expect(Array.isArray(result.contextDigest.usedFeatures)).toBe(true); + expect(result.contextDigest.usedFeatures.length).toBeGreaterThanOrEqual(5); + expect(typeof result.contextDigest.toolHint).toBe("string"); + expect(result.contextDigest.toolHint.length).toBeGreaterThan(0); + }, + INTEGRATION_TIMEOUT_MS, + ); +}); diff --git a/scripts/run-sample.test.ts b/scripts/run-sample.test.ts new file mode 100644 index 0000000..a5334f1 --- /dev/null +++ b/scripts/run-sample.test.ts @@ -0,0 +1,306 @@ +/** + * Run a rig sample with a stub Copilot SDK client that returns shape-conforming output. + * Usage: npx vitest run scripts/run-sample.test.ts -- --sample 02 + * or: npm run sample -- 02 + */ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { readFileSync, readdirSync } from "fs"; +import { mkdtemp, rm, writeFile } from "fs/promises"; +import { tmpdir } from "os"; +import { resolve } from "path"; +import { Readable, Writable } from "stream"; +import { execFile } from "child_process"; +import { promisify } from "util"; +import { runLauncherCli } from "rig"; + +const execFileAsync = promisify(execFile); + +const mocks = vi.hoisted(() => { + const approveAll = vi.fn(); + let sendAndWaitImpl: (request: { prompt: string }) => unknown | Promise = async () => ({ text: "stub response" }); + const disconnectSession = vi.fn(async () => {}); + const stopClient = vi.fn(async () => []); + const createSession = vi.fn(async () => ({ + sendAndWait: async (request: { prompt: string }) => { + const response = await sendAndWaitImpl(request); + return JSON.stringify(response); + }, + disconnect: disconnectSession, + })); + const forUri = vi.fn(() => ({ kind: "uri", url: "localhost:7777" })); + const forStdio = vi.fn(() => ({ kind: "stdio" })); + const CopilotClient = function () { + return { createSession, stop: stopClient }; + }; + const setSendAndWaitImpl = (impl: (request: { prompt: string }) => unknown | Promise) => { + sendAndWaitImpl = impl; + }; + return { approveAll, createSession, disconnectSession, stopClient, forUri, forStdio, CopilotClient, setSendAndWaitImpl }; +}); + +vi.mock("@github/copilot-sdk", () => ({ + approveAll: mocks.approveAll, + CopilotClient: mocks.CopilotClient, + RuntimeConnection: { forUri: mocks.forUri, forStdio: mocks.forStdio }, +})); + +function generateOutput(prompt: string): unknown { + const match = prompt.match(/([\s\S]*?)<\/output_schema>/); + if (!match) return { text: "stub response" }; + const schema = match[1].trim(); + try { + return parseJsonSchema(JSON.parse(schema)); + } catch (error) { + if (!(error instanceof SyntaxError)) throw error; + // Backward compatibility with historical schema text format. + } + return parseTypeText(schema); +} + +const ADDITIONAL_PROPERTIES_STUB_KEY_PREFIX = "example"; + +function nextAdditionalPropertiesStubKey(result: Record): string { + let key = ADDITIONAL_PROPERTIES_STUB_KEY_PREFIX; + let suffix = 2; + while (result[key] !== undefined) { + key = `${ADDITIONAL_PROPERTIES_STUB_KEY_PREFIX}${suffix}`; + suffix += 1; + } + return key; +} + +function parseJsonSchema(schema: unknown): unknown { + if (!schema || typeof schema !== "object" || Array.isArray(schema)) return "stub"; + const node = schema as Record; + if (Array.isArray(node["enum"]) && node["enum"].length > 0) return node["enum"][0]; + if (Array.isArray(node["oneOf"]) && node["oneOf"].length > 0) return parseJsonSchema(node["oneOf"][0]); + if (Array.isArray(node["anyOf"]) && node["anyOf"].length > 0) return parseJsonSchema(node["anyOf"][0]); + + switch (node["type"]) { + case "string": + return "stub"; + case "number": + case "integer": + return 0; + case "boolean": + return true; + case "array": + return []; + case "object": { + const properties = node["properties"] && typeof node["properties"] === "object" && !Array.isArray(node["properties"]) + ? node["properties"] as Record + : {}; + const result: Record = {}; + for (const [key, value] of Object.entries(properties)) { + result[key] = parseJsonSchema(value); + } + if (node["additionalProperties"]) { + const fallbackKey = nextAdditionalPropertiesStubKey(result); + result[fallbackKey] = parseJsonSchema(node["additionalProperties"]); + } + return result; + } + default: + if (node["properties"] && typeof node["properties"] === "object" && !Array.isArray(node["properties"])) { + const result: Record = {}; + for (const [key, value] of Object.entries(node["properties"] as Record)) { + result[key] = parseJsonSchema(value); + } + return result; + } + return "stub"; + } +} + +function parseTypeText(text: string): unknown { + text = text.trim(); + + // Union/enum: "value1" | "value2" | ... + if (text.includes("|") && !text.startsWith("{")) { + const parts = text.split("|").map((s) => s.trim()); + // Pick first non-null + for (const p of parts) { + if (p === "null") continue; + if (p.startsWith('"') && p.endsWith('"')) return p.slice(1, -1); + if (p === "string") return "stub"; + if (p === "number") return 0; + if (p === "boolean") return true; + return p; + } + return null; + } + + // Array: type[] + if (text.endsWith("[]")) return []; + + // Primitives + if (text === "string") return "stub"; + if (text === "number") return 0; + if (text === "boolean") return true; + if (text === "unknown") return null; + + // Object: { ... } + if (text.startsWith("{")) { + const inner = text.slice(1, text.lastIndexOf("}")).trim(); + if (!inner) return {}; + const result: Record = {}; + const fields = splitFields(inner); + for (const field of fields) { + // [key: string]: type (wildcard/record) + const wildcardMatch = field.match(/^\[key:\s*string\]\s*:\s*([\s\S]+?);?\s*$/); + if (wildcardMatch) { + result["example"] = parseTypeText(wildcardMatch[1].replace(/;\s*$/, "").trim()); + continue; + } + // key?: type; or key: type; (type may be multi-line for nested objects) + const fieldMatch = field.match(/^(\w+)(\?)?\s*:\s*([\s\S]+?);?\s*$/); + if (fieldMatch) { + const [, key, , type] = fieldMatch; + result[key] = parseTypeText(type.replace(/;\s*$/, "").trim()); + } + } + if (Object.keys(result).length === 0) return { text: "stub" }; + return result; + } + + // Quoted literal + if (text.startsWith('"') && text.endsWith('"')) return text.slice(1, -1); + // Numeric literal + if (/^\d+$/.test(text)) return Number(text); + // Boolean literal + if (text === "true") return true; + if (text === "false") return false; + + return "stub"; +} + +function splitFields(inner: string): string[] { + const fields: string[] = []; + let depth = 0; + let current = ""; + for (const line of inner.split("\n")) { + const trimmed = line.trim(); + if (!trimmed) continue; + current += (current ? "\n" : "") + trimmed; + depth += (trimmed.match(/\{/g) || []).length; + depth -= (trimmed.match(/\}/g) || []).length; + // A field is complete when we're back to depth 0 and line ends with ; + if (depth <= 0 && (trimmed.endsWith(";") || trimmed.endsWith("}"))) { + fields.push(current); + current = ""; + depth = 0; + } + } + if (current) fields.push(current); + return fields; +} + +// Determine which samples to run +const sampleDir = resolve(__dirname, "../src/samples"); +const allFiles = readdirSync(sampleDir) + .filter((f) => f.endsWith(".ts")) + .sort(); +const markdownDir = resolve(__dirname, "../skills/rig/samples"); +const allMarkdownFiles = readdirSync(markdownDir) + .filter((f) => f.endsWith(".md")) + .sort(); + +const filter = process.env["RIG_SAMPLE"]; +const targets = filter + ? allFiles.filter((f) => f.includes(filter)) + : allFiles; +const markdownTargets = filter + ? allMarkdownFiles.filter((f) => f.includes(filter)) + : allMarkdownFiles; + +function extractRigCode(markdown: string): string { + const match = markdown.match(/```rig\n([\s\S]*?)\n```/); + if (!match) throw new Error("Expected sample markdown to contain a ```rig code fence."); + return match[1]; +} + +function withTypecheckModel(code: string): string { + return code.replace(/model:\s*"[^"]+"/g, 'model: "typecheck"'); +} + +beforeEach(() => { + mocks.createSession.mockClear(); + mocks.setSendAndWaitImpl(async ({ prompt }) => generateOutput(prompt)); +}); + +describe("samples", () => { + for (const file of targets) { + it(file, async () => { + // Dynamically import the sample — vitest resolves "rig" via alias + // Samples use top-level await so the import itself runs them + const start = performance.now(); + try { + await import(`../src/samples/${file}`); + } catch (e: any) { + // Timeout/abort errors are expected for samples that test those features + if (e?.message?.includes("Timed out") || e?.name === "AbortError") { + // expected + } else { + throw e; + } + } + const elapsed = performance.now() - start; + expect(elapsed).toBeLessThan(5000); + }); + } +}); + +describe("skill markdown samples", () => { + for (const file of markdownTargets) { + it(file, async () => { + const code = extractRigCode(readFileSync(resolve(markdownDir, file), "utf8")); + const runnableCode = withTypecheckModel(code); + expect(code.split("\n").length).toBeLessThanOrEqual(30); + expect(code).toContain("export default"); + expect(code).toContain("// Agent role:"); + expect(code).not.toContain("console.log"); + expect((code.match(/^import .* from "rig";$/gm) ?? [])).toHaveLength(1); + expect(code).not.toMatch(/^await\s+\w+\(/m); + + const stdin = Readable.from([runnableCode]); + const output: string[] = []; + const stdout = new Writable({ + write(chunk, _encoding, callback) { + output.push(chunk.toString()); + callback(); + }, + }); + + const start = performance.now(); + await runLauncherCli([], {}, { stdin, stdout }); + const elapsed = performance.now() - start; + expect(elapsed).toBeLessThan(5000); + expect(output.join("")).not.toBe(""); + }); + } +}); + +describe("skill markdown samples typecheck", () => { + it("typechecks extracted rig programs with npx tsc", async () => { + const typecheckDir = await mkdtemp(resolve(tmpdir(), "rig-sample-typecheck-")); + try { + for (const file of markdownTargets) { + const markdown = readFileSync(resolve(markdownDir, file), "utf8"); + const code = withTypecheckModel(extractRigCode(markdown)); + const tsFile = resolve(typecheckDir, file.replace(/\.md$/, ".ts")); + await writeFile(tsFile, `${code}\n`, "utf8"); + } + + await execFileAsync( + "npx", + ["--yes", "--package", "typescript@5.9.3", "--", "tsc", "--noEmit", "--pretty", "false"], + { + cwd: resolve(__dirname, ".."), + env: { ...process.env, npm_config_ignore_scripts: "true" }, + }, + ); + } finally { + await rm(typecheckDir, { recursive: true, force: true }); + } + }); +}); diff --git a/skills/rig/SKILL.md b/skills/rig/SKILL.md new file mode 100644 index 0000000..7be8b3e --- /dev/null +++ b/skills/rig/SKILL.md @@ -0,0 +1,376 @@ +# rig + +Minimal TypeScript agent harness skill for structured agent calls inside sandboxed agentic workflows, intended for embedding in markdown with `rig` code fences. + +## Install + +Install the latest GitHub release directly: + +```bash +npm install github:pelikhan/rig#v0.0.8 +``` + +Or clone the skill for Copilot coding agent: + +```bash +gh skills clone pelikhan/rig +``` + +## Preferred imports + +```ts +import { agent, p, s } from "rig"; +``` + +## Recommended default pattern + +Prefer this shape when generating a new rig program: + +```ts +import { agent, p, s } from "rig"; + +// Agent role: review the diff and return only the declared output. +const reviewDiff = agent({ + model: "mini", + instructions: "Review the diff and return only the declared output.", + output: s.object({ + summary: s.string, + risk: s.enum("low", "medium", "high"), + findings: s.array(s.object({ + file: s.string, + message: s.string, + line: s.optional(s.number), + })), + }), +}); + +export default reviewDiff; +``` + +## Fast generation checklist + +Use this checklist before finalizing generated code: + +1. Use a single `import { ... } from "rig"` statement. +2. Use `agent({ ... })`; include `name` when it helps differentiate agents, keep `instructions` and `output` explicit, and include `input` when the scenario needs it. +3. Define input/output with `s.object(...)` and explicit `s.*` helpers. +4. Keep output schema strict (enums/literals for constrained values). +5. Add a `// Agent role: ...` comment above each agent declaration. +6. Set `model` explicitly to `"large"`, `"mini"`, or `"nano"`. +7. Prefer `${p.read(...)}` / `${p.bash(...)}` inside `p\`\`` templates when the context source is already known; add input fields only for true caller-provided data. +8. Put stable defaults in spec; register addons in spec or with `agent.use(...)`. +9. Add `agents` only when required by the scenario. +10. Avoid `console.log(...)` in snippets. +11. For inline markdown skill mode, export exactly one default root agent with no input and do not call it directly. +12. Assume Node.js 24 runtime for operational examples and generated snippets. +13. For bash-like operations from TypeScript, prefer `google/zx` (`import { $ } from "zx"`). +14. Prefer Node.js native APIs (for example built-in `fetch` and native glob support) over extra helper dependencies. + +## Canonical construction order + +Use this order to reduce syntax drift: + +1. Core agent shape: `agent({ name, instructions, input, output })`. +2. Explicit typed schemas with `s.object(...)` and `s.*`. +3. Shell/file context with `p\`\`` and `${p.*}` before adding extra input plumbing. +4. Advanced spec fields (`agents`) when scenario needs them. +5. Invocation overrides (`model`, `timeout`, `maxTurns`, `signal`) at call time. + +## `agent(spec)` + +Declare a structured agent. + +### Spec fields + +| Field | Purpose | +|-------|---------| +| `name` | Agent name used in the prompt | +| `instructions` | Prompt instructions as a plain string or a ``p`...` `` prompt builder | +| `input` | Input schema | +| `output` | Output schema | +| `model` | Default model name; examples should use `"large"`, `"mini"`, or `"nano"` | +| `maxTurns` | Retry budget for invalid JSON or invalid output | +| `addons` | Per-turn addons for steering, validation, and retry customization | +| `agents` | Optional named subagents exposed to the harness | + +Use `agent({ name, ... })` as the only agent declaration form. `name` is optional; when omitted rig normalizes it to `"agent"`. + +## Agent behavior defaults + +| Setting | Default | +|---------|---------| +| Model | `gpt-4.1` | +| Max turns | `4` | +| Addons | none | + +Override per call with `model`, `maxTurns`, `timeout`, and `signal`. Put stable defaults in the agent spec; use call-time options for per-run changes. + +## Schemas + +Use `s.*` helpers for input and output schemas. +These `s.*` declarations must stay JSON Schema-compatible and serialize directly as JSON Schema because rig renders prompt schema blocks as JSON Schema. + +```ts +input: s.object({ + title: s.string, + severity: s.enum("low", "medium", "high"), +}) +``` + +Use explicit schemas in docs and generated samples. + +## `s` schema helpers + +```ts +s.string +s.string("description") +s.number +s.boolean +s.unknown +s.array(item) +s.array(item, "description") +s.object(fields) +s.object(fields, "description") +s.record(value) +s.record(value, "description") +s.enum(...values) +s.enum(values, "description") +s.optional(shape) +s.optional(shape, "description") +``` + +Common examples: + +```ts +s.enum("bug", "feature", "question") +s.optional(s.number) +s.record(s.string) +``` + +## Tools + +Register custom tools with `defineTool` using the same shape as `@github/copilot-sdk`. Use `s.*` schemas for `parameters`. Rig defaults tools to `skipPermission: true`. + +```ts +import { agent, defineTool, s } from "rig"; + +const lookupIssue = defineTool("lookup_issue", { + description: "Look up an issue by id.", + parameters: s.object({ + issue: s.string, + }), + handler: async ({ issue }) => `Issue ${issue}`, +}); + +// Agent role: triage an issue using the lookup tool. +const triage = agent({ + model: "mini", + instructions: "Use lookup_issue before answering.", + tools: [lookupIssue], +}); +``` + +## Prompt helpers + +`p` is both the prompt template tag and the prompt-intent helper namespace. +These helpers are declarative placeholders, not direct shell execution in the core harness. +Prefer template expressions when the context source is already known. +Prefer `p.read("path")` over `p.bash("cat path")`, and keep large context in files instead of building in-memory strings just to feed an agent. +Rig assumes the surrounding workflow already provides the sandbox and protections it needs, so prompt intents for shell/file actions should execute directly without extra permission prompts. + +```ts +p.bash("git diff -- .") +p.bash("npm test") +p.read("README.md") +p.write("README.md", "# Hello\n") +``` + +Use `p.*` helpers: + +- in input values +- inside `p\`\`` instruction templates, preferably as the default pattern + +```ts +const prompt = p`Review the repository status using ${p.bash("git status --short")}.`; +``` + +Only introduce `input` fields for data the caller truly supplies at runtime. Do not require inputs just to thread known file or shell context into the prompt. + +## `p` as a prompt builder for instructions + +``p`...` `` can also be used in `instructions` when you want to embed prompt intents or inline context directly in the agent instructions. + +```ts +import { agent, p, s } from "rig"; + +const reviewAgent = agent({ + instructions: p`Review the repository using ${p.bash("git status --short")} and summarize changes.`, + output: s.object({ summary: s.string }), +}); +``` + +- ``p`...` `` accepts `${p.bash(...)}`, `${p.read(...)}`, and `${p.write(...)}` expressions. +- Nested `PromptBuilder` values used as interpolations are inlined as plain text. +- The rendered `PromptBuilder` replaces the instructions string when the agent prompt is assembled. + +## Call-time options + +Pass overrides when calling an agent: + +```ts +const controller = new AbortController(); + +const result = await myAgent(input, { + model: "mini", + timeout: 30_000, + maxTurns: 2, + signal: controller.signal, +}); +``` + +Use call-time options for per-run changes. Use addons for stable defaults (for example `timeout({ timeout: ... })`). + +## Subagents + +Expose subagents with `agents`: + +```ts +// Agent role: extract the most important changes from the diff. +const summarizeDiff = agent({ + model: "mini", +}); + +// Agent role: review the diff using the provided subagent when helpful. +const reviewer = agent({ + model: "mini", + output: s.object({ + summary: s.string, + issues: s.array(s.string), + }), + agents: { summarizeDiff }, + instructions: "Review the diff. You may use the provided subagent conceptually.", +}); +``` + +When delegating task resolution, keep each subagent narrow and explicit (for example: `analyzeTask`, `draftRigProgram`, `verifySchema`) and make the root agent instructions require combining their outputs into one final response. + +## Task harness pattern for rig markdown + +When the task asks for a runnable markdown example, require exactly one fenced ````rig` block that is valid inline harness input: + +- include `import { ... } from "rig"` (or rely on inline injection intentionally) +- define one default-exported no-input root agent +- avoid calling the root agent directly in the snippet +- keep the block aligned with this skill's construction order and checklist + +## Repair and retries + +Rig starts with no addons by default. Opt into retry behavior with `repair` from `rig/addons`. + +```ts +import { repair } from "rig/addons"; + +// Agent role: repair invalid output and return a stable summary. +const summarize = agent({ + model: "mini", + maxTurns: 3, + addons: repair, +}); +``` + +Use addons to steer retry prompts when needed (for example `steering()` from `rig/addons`). +Use `oncePerSession()` from `rig/addons` when you need to register with the Copilot session once per call instead of checking `context.turn === 1`. + +The addon `context` object contains: `prompt`, `response`, `turn`, `maxTurns`, `signal`, `output`, `nextPrompt`, `error`, `completed`, and `session` (direct Copilot SDK session access). + +## Running programs + +Treat fenced `rig` code blocks in markdown as runnable rig programs. +Run them by extracting the fence content and piping it into `node skills/rig/rig.ts`. +Inline programs run a default-exported root program with no required external input and write stdout. Export either an agent, a string, or a prompt builder value. If `export default` is omitted, the harness defaults to the first `const/let/var name = agent(...)` assignment: + +```bash +cat <<'RIG' | node skills/rig/rig.ts +// Agent role: summarize this repository in one sentence. +export default "Summarize this repository in one sentence."; +RIG +``` + +`import { agent, p, s } from "rig"` is optional in inline mode; the harness injects it if omitted. + +Inline mode accepts root agents that either omit `input`, use `input: s.object({})`, or rely on the default `input: s.string` (which is invoked with `""`). + +The harness also supports program-file mode. Export the root program as the default export and pass input on stdin: + +```bash +echo "Review this diff" | node skills/rig/rig.ts src/program.ts +``` + +Pass `--server` to have the harness start the Copilot server automatically before running: + +```bash +echo "Review this diff" | node skills/rig/rig.ts src/program.ts --server +``` + +Pass `--typecheck` to typecheck the rig program before execution: + +```bash +cat <<'RIG' | node skills/rig/rig.ts --typecheck +import { p } from "rig"; +export default p`Summarize this repository and include highlights from ${p.read("README.md")}.`; +RIG +``` + +Program-file mode also supports `--typecheck`: + +```bash +echo "Review this diff" | node skills/rig/rig.ts src/program.ts --typecheck +``` + +For program-file mode stdin coercion: +- if root input schema is `string`, stdin is passed as raw text +- if root input schema is an object containing `text`, stdin is passed as `{ text: "" }` +- otherwise stdin must be valid JSON for the declared input schema + +## Copilot SDK runtime + +`rig` is specialized for Copilot SDK sessions and no longer exposes a custom engine mount API. +By default it connects over HTTP using `COPILOT_SDK_URI`, then `localhost:7777`. +Use `--server` at launch time when you want the harness to start the Copilot server via stdio. + +## Patterns to prefer + +- Prefer `s.object(...)` for important examples. Omit schemas entirely when the default free-form string is enough. +- Keep outputs small, typed, and explicit. +- Use `s.enum(...)` when exact values matter. +- Prefer `p.*` inside `p\`\`` templates; fall back to inputs only for real caller-provided data. +- Prefer `p.read(...)` for existing files instead of shelling out through `cat`. +- Assume Node.js 24 runtime for operational code. +- Prefer `google/zx` for shell-style automation in TypeScript examples. +- Prefer Node.js native APIs (including built-in `fetch` and native glob support) before adding dependencies. +- Put durable defaults in the agent spec; register addons in spec or with `agent.use(...)`. +- Use `steering()` from `rig/addons` when you want the builtin last-retry warning addon; it is opt-in. +- Introduce `agents` only when the scenario needs them. + +## Patterns to avoid + +- When a free-form string is enough, omit `input`/`output` and use the default `s.string` schemas. +- Do not wrap a single string field in an input object just to carry text. +- Do not import prompt helpers from anywhere except `rig`. +- Do not require `input` fields just to pass `p.read(...)` / `p.bash(...)` context into instructions. +- Do not leave outputs as unstructured prose when a schema would help. +- Do not invent alternate schema syntaxes when explicit `s.*` is available. +- Do not replace file reads with `cat`-style shell commands or large in-memory strings when a file path already exists. +- Do not add third-party fetch or glob helpers when Node.js 24 native APIs already cover the requirement. +- Do not put call-time overrides (`model`, `timeout`, `maxTurns`, `signal`) into unrelated config objects. + +## API direction + +Use only the current API: + +- `agent({ name, ... })` +- `p.*` and `p\`...\`` from `rig` +- `s.*` for explicit schema helpers +- `oncePerSession` / `repair` / `steering` / `timeout` from `rig/addons` for optional addons + +Do not add deprecated hooks or compatibility layers. diff --git a/skills/rig/addons.ts b/skills/rig/addons.ts new file mode 100644 index 0000000..cdda1a4 --- /dev/null +++ b/skills/rig/addons.ts @@ -0,0 +1,88 @@ +import { analyzeResponse, defaultRepairPrompt } from "./rig.ts"; +import type { AgentAddon, AgentAddonContext, CopilotSession } from "./rig.ts"; + +const DEFAULT_STEERING_WARNING = "You are running out of turns. This is your final attempt before reaching the turn limit. Please correct your output now."; + +export type SteeringOptions = { + message?: string; +}; + +export type TimeoutOptions = { + timeout: number; +}; + +export type SessionRegistration = ( + session: CopilotSession, + context: AgentAddonContext, +) => void | Promise; + +export function steering(options: SteeringOptions = {}): AgentAddon { + const message = options.message ?? DEFAULT_STEERING_WARNING; + return async (context, next) => { + await next(); + if (context.nextPrompt && context.turn + 1 === context.maxTurns) { + context.nextPrompt = `${context.nextPrompt}\n${message}`; + } + }; +} + +export const repair: AgentAddon = async (context, next) => { + await next(); + if (context.completed || context.error !== undefined || context.nextPrompt !== undefined) { + return; + } + if (context.response === undefined) { + return; + } + const analysis = analyzeResponse(context.response, context.outputSchema, context.spec.name, context.turn); + if (analysis.ok) { + context.completed = true; + context.output = analysis.output; + return; + } + if (context.turn >= context.maxTurns) { + context.error = analysis.error; + return; + } + context.nextPrompt = defaultRepairPrompt(context.spec, analysis.error); +}; + +export function timeout(options: TimeoutOptions): AgentAddon { + return async (context, next) => { + context.signal = timeoutSignal(context.signal, options.timeout); + await next(); + }; +} + +export function oncePerSession(register: SessionRegistration): AgentAddon { + const seen = new WeakSet(); + return async (context, next) => { + if (!seen.has(context.session)) { + await register(context.session, context); + seen.add(context.session); + } + await next(); + }; +} + +function timeoutSignal(parent?: AbortSignal, timeoutMs?: number): AbortSignal | undefined { + if (!timeoutMs) { + return parent; + } + const controller = new AbortController(); + const onAbort = () => controller.abort(parent?.reason); + parent?.addEventListener("abort", onAbort, { once: true }); + const timer = setTimeout( + () => controller.abort(new Error(`Timed out after ${timeoutMs}ms`)), + timeoutMs, + ); + controller.signal.addEventListener("abort", () => clearTimeout(timer), { once: true }); + return controller.signal; +} + +export const addons = { + oncePerSession, + timeout, + repair, + steering, +}; diff --git a/skills/rig/rig.ts b/skills/rig/rig.ts new file mode 100644 index 0000000..38ccda8 --- /dev/null +++ b/skills/rig/rig.ts @@ -0,0 +1,1586 @@ +import { basename, dirname, isAbsolute, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { access, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { execFile } from "node:child_process"; +import { AsyncLocalStorage } from "node:async_hooks"; +import { promisify } from "node:util"; +import { CopilotClient, RuntimeConnection, approveAll, defineTool as sdkDefineTool } from "@github/copilot-sdk"; +import type { + CopilotClientOptions, + SystemMessageConfig, + Tool as CopilotTool, + ToolHandler, + ZodSchema, +} from "@github/copilot-sdk"; + +export type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; +export type ValidationResult = { ok: true } | { ok: false; error: string }; + +export type StringSchema = { type: "string"; description?: string }; +export type NumberSchema = { type: "number"; description?: string }; +export type BooleanSchema = { type: "boolean"; description?: string }; +export type UnknownSchema = { description?: string }; +export type ArraySchema = { type: "array"; items: Item; description?: string }; +export type ObjectSchema = Record> = { + type: "object"; + properties: Fields; + description?: string; +}; +export type RecordSchema = { type: "object"; additionalProperties: Value; description?: string }; +export type EnumSchema = { enum: Values; description?: string }; +const OPTIONAL_SYMBOL: unique symbol = Symbol("rig.optional"); +type OptionalMarker = { readonly [OPTIONAL_SYMBOL]: true }; +type UnwrapOptional = Omit; +export type OptionalSchema = Inner & OptionalMarker; + +export type Schema = + | StringSchema + | NumberSchema + | BooleanSchema + | UnknownSchema + | ArraySchema + | ObjectSchema + | RecordSchema + | EnumSchema + | OptionalSchema; + +type SchemaHelperFactory = T & ((description?: string) => T); + +const SCHEMA_SYMBOL: unique symbol = Symbol("rig.schema"); + +function markAsSchema(obj: T): T { + Object.defineProperty(obj, SCHEMA_SYMBOL, { value: true, enumerable: false, writable: false, configurable: false }); + Object.defineProperty(obj, "toJSON", { + value: () => serializeSchema(obj as unknown as Schema), + enumerable: false, + writable: false, + configurable: false, + }); + return obj; +} + +function cloneSchema(schema: Inner, description?: string): Inner { + const cloned = { ...(schema as Record) } as Inner; + if (description !== undefined) { + Object.assign(cloned as object, { description }); + } + return markAsSchema(cloned as unknown as object) as Inner; +} + +function markAsOptional(schema: Inner): OptionalSchema { + Object.defineProperty(schema, OPTIONAL_SYMBOL, { value: true, enumerable: false, writable: false, configurable: false }); + return schema as OptionalSchema; +} + +function isOptionalSchema(schema: Schema): schema is OptionalSchema { + return OPTIONAL_SYMBOL in schema; +} + +function createTypedPrimitiveSchema(type: T["type"]): SchemaHelperFactory { + const base = markAsSchema({ type } as T); + const factory = Object.assign( + markAsSchema(((description?: string) => (description === undefined ? base : markAsSchema({ type, description } as T))) as SchemaHelperFactory), + base, + ); + return factory; +} + +function createUnknownSchema(): SchemaHelperFactory { + const base: UnknownSchema = markAsSchema({}); + const factory = Object.assign( + markAsSchema(((description?: string) => (description === undefined ? base : markAsSchema({ description }))) as SchemaHelperFactory), + base, + ); + return factory; +} + +type EnumSchemaFactory = { + (...values: Values): EnumSchema; + (values: Values, description: string): EnumSchema; +}; + +const createEnumSchema: EnumSchemaFactory = (...args: unknown[]) => { + const valuesOrTuple = args as readonly Json[]; + if ( + valuesOrTuple.length === 2 + && Array.isArray(valuesOrTuple[0]) + && typeof valuesOrTuple[1] === "string" + ) { + const enumValues = valuesOrTuple[0] as readonly Json[]; + const description = valuesOrTuple[1] as string; + return markAsSchema({ enum: enumValues, description }); + } + const enumValues = valuesOrTuple as readonly Json[]; + return markAsSchema({ enum: enumValues }); +}; + +export type Simplify = { [K in keyof T]: T[K] } & {}; + +export type AgentInputValue = + T extends readonly (infer Item)[] ? PromptIntent | PromptBuilder | AgentInputValue[] : + T extends object ? PromptIntent | PromptBuilder | { [K in keyof T]: AgentInputValue } : + T | PromptIntent | PromptBuilder; + +export type InferSchema = + T extends OptionalMarker ? InferSchema> | undefined : + T extends { type: "string" } ? string : + T extends { type: "number" } ? number : + T extends { type: "boolean" } ? boolean : + T extends { enum: infer Values extends readonly unknown[] } ? Values[number] : + T extends { type: "array"; items: infer Item } ? InferSchema[] : + T extends { type: "object"; properties: infer Fields extends Record } ? Simplify< + & { [K in keyof Fields as Fields[K] extends OptionalMarker ? never : K]: InferSchema } + & { [K in keyof Fields as Fields[K] extends OptionalMarker ? K : never]?: InferSchema> } + > : + T extends { type: "object"; additionalProperties: infer Value } ? Record> : + unknown; + +export const s = { + string: createTypedPrimitiveSchema("string"), + number: createTypedPrimitiveSchema("number"), + boolean: createTypedPrimitiveSchema("boolean"), + unknown: createUnknownSchema(), + array(items: Item, description?: string): ArraySchema { + return description === undefined ? markAsSchema({ type: "array", items }) : markAsSchema({ type: "array", items, description }); + }, + object>(properties: Fields, description?: string): ObjectSchema { + return description === undefined ? markAsSchema({ type: "object", properties }) : markAsSchema({ type: "object", properties, description }); + }, + record(additionalProperties: Value, description?: string): RecordSchema { + return description === undefined ? markAsSchema({ type: "object", additionalProperties }) : markAsSchema({ type: "object", additionalProperties, description }); + }, + enum: createEnumSchema, + optional(schema: Inner, description?: string): OptionalSchema { + return markAsOptional(cloneSchema(schema, description)); + }, + toJsonSchema, +}; + +export type JsonSchemaObject = { [key: string]: unknown }; + +export function toJsonSchema(schema: Schema): JsonSchemaObject { + return serializeSchema(schema); +} + +function serializeSchema(schema: Schema): JsonSchemaObject { + const { description } = schema; + const withDescription = (obj: JsonSchemaObject): JsonSchemaObject => + description === undefined ? obj : { ...obj, description }; + if ("enum" in schema) { + return withDescription({ enum: schema.enum }); + } + if ("items" in schema) { + return withDescription({ type: "array", items: serializeSchema(schema.items) }); + } + if ("additionalProperties" in schema) { + return withDescription({ type: "object", additionalProperties: serializeSchema(schema.additionalProperties) }); + } + if ("properties" in schema) { + const properties: Record = {}; + const required: string[] = []; + for (const [key, field] of Object.entries(schema.properties) as [string, Schema][]) { + properties[key] = serializeSchema(field); + if (!isOptionalSchema(field)) { + required.push(key); + } + } + const obj: JsonSchemaObject = { type: "object", properties }; + if (required.length > 0) { + obj["required"] = required; + } + return withDescription(obj); + } + if ("type" in schema) { + return withDescription({ type: schema.type }); + } + return withDescription({}); +} + +const defaultStringSchema = s.string; +const defaultName = "agent"; + +export type CopilotEngineOptions = Omit & { + connection?: CopilotClientOptions["connection"]; + server?: boolean; +}; + +function resolveDefaultCopilotUri(): string { + return process.env["COPILOT_SDK_URI"] ?? "localhost:7777"; +} + +export function copilotEngine(options: CopilotEngineOptions = {}): CopilotClient { + const { server, connection, ...clientOptions } = options; + return new CopilotClient({ + ...clientOptions, + connection: connection ?? (server ? RuntimeConnection.forStdio() : RuntimeConnection.forUri(resolveDefaultCopilotUri())), + }); +} + +function jsonl(value: unknown): string { + try { + return JSON.stringify(value, (_, v) => { + if (typeof v === "bigint") { + return v.toString(); + } + if (v instanceof Error) { + return { name: v.name, message: v.message, stack: v.stack }; + } + return v; + }); + } catch (error) { + const reason = error instanceof Error ? error.message : String(error); + return JSON.stringify(rigEvent("logger.error", { error: reason })); + } +} + +function rigEvent(type: string, data?: unknown): { type: string; data?: unknown } { + return { type: `rig.${type}`, data }; +} + +function writeEvent(event: unknown): void { + process.stderr.write(`${jsonl(event)}\n`); +} + +export type CopilotSession = Awaited>; +export type AgentAddonContext = { + spec: NormalizedAgentSpec; + session: CopilotSession; + input: unknown; + outputSchema: Schema; + signal: AbortSignal | undefined; + turn: number; + maxTurns: number; + prompt: string; + response?: string; + completed: boolean; + output?: unknown; + nextPrompt?: string; + error?: unknown; +}; +export type AgentAddon = ( + context: AgentAddonContext, + next: () => Promise, +) => void | Promise; +export type Tool = CopilotTool; +export type ToolParameters = Schema | ZodSchema | Record; +export type ToolConfig = { + description?: string; + parameters?: ToolParameters; + handler?: ToolHandler; + overridesBuiltInTool?: boolean; + skipPermission?: boolean; +}; + +export function defineTool(name: string, config: ToolConfig): Tool { + return sdkDefineTool(name, { + ...normalizeToolConfig(config), + parameters: normalizeToolParameters(config.parameters), + }); +} + +export type AgentSpec = { + name?: string; + instructions?: string | PromptBuilder; + input?: Input; + output?: Output; + model?: string; + maxTurns?: number; + addons?: AgentAddon | AgentAddon[]; + agents?: Record>; + systemMessage?: SystemMessageConfig; + tools?: Tool[]; +}; +/** Internal normalized variant with a guaranteed resolved name. */ +type NormalizedAgentSpec = AgentSpec & { name: string }; + +export type CallOptions = { + signal?: AbortSignal; + timeout?: number; + model?: string; + maxTurns?: number; +}; + +export type LaunchOptions = { + cwd?: string; + startServer?: boolean; + typecheck?: boolean; +}; + +export type LauncherIo = { + stdin: NodeJS.ReadableStream; + stdout: NodeJS.WritableStream; +}; + +export type AgentFn = ((input: AgentInputValue, options?: CallOptions) => Promise) & { + agentName: string; + inputSchema: Schema; + outputSchema: Schema; + inputShape: Schema; + outputShape: Schema; + spec: NormalizedAgentSpec; + _namespace: string; + use: (addons: AgentAddon | AgentAddon[]) => AgentFn; +}; + +export type PromptIntentOptions = { + cwd?: string; + env?: Record; + timeout?: number; + purpose?: string; + signal?: AbortSignal; +}; + +export type PromptIntent = { + __rig: "prompt"; + id: string; + mode: "prompt.text" | "prompt.read" | "prompt.write"; + command?: string; + path?: string; + contents?: string; + options?: Omit; +}; + +let nextPromptIntentId = 1; + +type PromptHelpers = { + (): PromptBuilder; + (strings: TemplateStringsArray, ...values: unknown[]): PromptBuilder; + bash(command: string, options?: PromptIntentOptions): PromptIntent; + read(path: string, options?: PromptIntentOptions): PromptIntent; + write(path: string, contents: string, options?: PromptIntentOptions): PromptIntent; + var(name: string, value: T): PromptVariable; + region(language: string, body: unknown): string; +}; + +export type PromptVariable = { + __rig: "prompt.var"; + name: string; + value: T; +}; + +function isTemplateStringsArray(value: unknown): value is TemplateStringsArray { + return Array.isArray(value) && Array.isArray((value as { raw?: unknown })?.raw); +} + +function isPromptVariable(value: unknown): value is PromptVariable { + return !!value && typeof value === "object" && (value as { __rig?: string }).__rig === "prompt.var"; +} + +function createPromptVariable(name: string, value: T): PromptVariable { + return { __rig: "prompt.var", name, value }; +} + +function renderPromptPart(value: unknown): string { + if (isPromptIntent(value)) { + return renderPromptIntentValue(value); + } + if (value instanceof PromptBuilder) { + return value.toString(); + } + if (isPromptVariable(value)) { + return renderPromptPart(value.value); + } + if (value === null || value === undefined) { + return ""; + } + if (typeof value === "string") { + return value; + } + if (typeof value === "object") { + return json(value); + } + return String(value); +} + +function renderCodeRegion(language: string, body: unknown): string { + const content = renderPromptPart(body); + const normalized = content.endsWith("\n") ? content : `${content}\n`; + return `\`\`\`${language}\n${normalized}\`\`\`\n`; +} + +function normalizePromptTemplateText(text: string): string { + if (!text.includes("\n")) { + return text; + } + const lines = text.split("\n"); + while (lines.length > 0 && lines[0]?.trim() === "") { + lines.shift(); + } + while (lines.length > 0 && lines[lines.length - 1]?.trim() === "") { + lines.pop(); + } + if (lines.length === 0) { + return ""; + } + const indents = lines + .filter((line) => line.trim() !== "") + .map((line) => line.match(/^[\t ]*/)?.[0].length ?? 0); + const minIndent = indents.length > 0 ? Math.min(...indents) : 0; + if (minIndent <= 0) { + return lines.join("\n"); + } + return lines.map((line) => line.slice(minIndent)).join("\n"); +} + +function promptTemplateDelimiter(strings: TemplateStringsArray): string { + let delimiter = "\u0000"; + while (strings.some((part) => part.includes(delimiter))) { + delimiter += "\u0000"; + } + return delimiter; +} + +function promptFactory(): PromptBuilder; +function promptFactory(strings: TemplateStringsArray, ...values: unknown[]): PromptBuilder; +function promptFactory(...args: unknown[]): PromptBuilder { + if (args.length === 0) { + return new PromptBuilder(); + } + if (!isTemplateStringsArray(args[0])) { + const receivedType = args[0] === null ? "null" : typeof args[0]; + throw new TypeError(`p() expects either no arguments (for builder) or tagged template syntax like p\`...\` (received ${args.length} arg(s), first arg type: ${receivedType})`); + } + const strings = args[0]; + const values = args.slice(1); + const builder = new PromptBuilder(); + const delimiter = promptTemplateDelimiter(strings); + const normalizedStrings = normalizePromptTemplateText(strings.join(delimiter)).split(delimiter); + for (let index = 0; index < normalizedStrings.length; index += 1) { + builder.write(normalizedStrings[index] ?? ""); + if (index < values.length) { + builder.write(values[index]); + } + } + return builder; +} + +export const p: PromptHelpers = Object.assign( + promptFactory, + { + bash(command: string, options?: PromptIntentOptions): PromptIntent { + return createPromptIntent("prompt.text", withOptions({ command }, options)); + }, + read(path: string, options?: PromptIntentOptions): PromptIntent { + return createPromptIntent("prompt.read", withOptions({ path }, options)); + }, + write(path: string, contents: string, options?: PromptIntentOptions): PromptIntent { + return createPromptIntent("prompt.write", withOptions({ path, contents }, options)); + }, + var(name: string, value: T): PromptVariable { + return createPromptVariable(name, value); + }, + region(language: string, body: unknown): string { + return renderCodeRegion(language, body); + }, + }, +); + +export class PromptBuilder { + readonly vars = new Map(); + private readonly chunks: string[] = []; + + bash(command: string, options?: PromptIntentOptions): PromptIntent { + return p.bash(command, options); + } + + read(path: string, options?: PromptIntentOptions): PromptIntent { + return p.read(path, options); + } + + file(path: string, contents: string, options?: PromptIntentOptions): PromptIntent { + return p.write(path, contents, options); + } + + var(name: string, value: T): PromptVariable { + const variable = createPromptVariable(name, value); + this.vars.set(name, variable); + return variable; + } + + get(name: string): T | undefined { + return this.vars.get(name)?.value as T | undefined; + } + + write(...values: unknown[]): this { + this.chunks.push(values.map(renderPromptPart).join("")); + return this; + } + + line(...values: unknown[]): this { + return this.write(...values, "\n"); + } + + region(language: string, body: unknown): this { + this.chunks.push(renderCodeRegion(language, body)); + return this; + } + + toString(): string { + return this.chunks.join(""); + } +} + +export class AgentError extends Error { + readonly kind: "parse" | "validation"; + readonly agent: string; + readonly turn: number; + readonly response: string; + readonly schema: Schema; + readonly schemaText: string; + + constructor(options: { + kind: "parse" | "validation"; + agent: string; + turn: number; + response: string; + schema: Schema; + message: string; + }) { + super(options.message); + this.name = "AgentError"; + this.kind = options.kind; + this.agent = options.agent; + this.turn = options.turn; + this.response = options.response; + this.schema = options.schema; + this.schemaText = renderSchema(options.schema); + } +} + +let currentCopilotOptions: CopilotEngineOptions | undefined; +type CopilotRunContext = { + client: CopilotClient; +}; +type CopilotSessionHandle = { + session: CopilotSession; + close(): Promise; +}; +const copilotRunStorage = new AsyncLocalStorage(); + +/** + * Mounts an engine and executes a rig program file. + * Relative paths are resolved from `options.cwd` (or process cwd). + */ +export async function launchRigProgram(programPath: string, options: LaunchOptions = {}): Promise { + const cwd = options.cwd ?? process.cwd(); + const resolvedPath = isAbsolute(programPath) ? programPath : resolve(cwd, programPath); + + configureCopilot(resolveCopilotOptions(cwd, options)); + await import(pathToFileURL(resolvedPath).href); +} + +async function readStdin(stream: NodeJS.ReadableStream): Promise { + const chunks: string[] = []; + for await (const chunk of stream) { + chunks.push(typeof chunk === "string" ? chunk : chunk.toString()); + } + return chunks.join(""); +} + +function resolveCopilotOptions(cwd: string, options: LaunchOptions): { workingDirectory: string } | { workingDirectory: string; server: true } { + return options.startServer ? { workingDirectory: cwd, server: true } : { workingDirectory: cwd }; +} + +function asRootAgent(value: unknown): AgentFn | undefined { + if (typeof value !== "function") { + return undefined; + } + const candidate = value as Partial; + if (!candidate.inputSchema || !candidate.outputSchema) { + return undefined; + } + return value as AgentFn; +} + +/** + * Normalizes supported launcher root exports to an agent function. + * Strings and prompt builders are wrapped in a default agent. + */ +function asRootProgram(value: unknown, name: string): AgentFn | undefined { + const rootAgent = asRootAgent(value); + if (rootAgent) { + return rootAgent; + } + if (typeof value === "string" || value instanceof PromptBuilder) { + return agent({ name, instructions: value }) as AgentFn; + } + return undefined; +} + +function noInputInvocation(agentFn: AgentFn): unknown | undefined { + const schema = agentFn.inputSchema; + if ("type" in schema && schema.type === "string") { + return ""; + } + if (!("properties" in schema)) { + return undefined; + } + const keys = Object.keys(schema.properties); + if (keys.length === 0) { + return {}; + } + if ( + keys.length === 1 + && "text" in schema.properties + && "type" in (schema.properties.text as Schema) + && (schema.properties.text as StringSchema).type === "string" + ) { + return { text: "" }; + } + return undefined; +} + +function withInjectedRigImport(programCode: string): string { + if (/\bfrom\s*["']rig["']/.test(programCode)) { + return programCode; + } + return `import { agent, p, s } from "rig";\n\n${programCode}`; +} + +function withInjectedDefaultRootAgent(programCode: string): string { + if (/\bexport\s+default\b/.test(programCode)) { + return programCode; + } + const firstAgentAssignment = programCode.match( + /^\s*(?:const|let|var)\s+([$_\p{ID_Start}][$_\p{ID_Continue}]*)\s*=\s*agent\s*\(/mu, + ); + if (!firstAgentAssignment) { + return programCode; + } + return `${programCode}\n\nexport default ${firstAgentAssignment[1]};\n`; +} + +function coerceStdinInput(agentFn: AgentFn, text: string): unknown { + const schema = agentFn.inputSchema; + if ("type" in schema && schema.type === "string") { + return text; + } + if ("properties" in schema && "text" in schema.properties) { + return { text }; + } + try { + return JSON.parse(text); + } catch { + throw new Error("Expected stdin to contain JSON for the root agent input schema."); + } +} + +function renderStdout(value: unknown): string { + if (typeof value === "string") { + return value; + } + if ( + value + && typeof value === "object" + && "text" in value + && typeof (value as { text: unknown }).text === "string" + ) { + return (value as { text: string }).text; + } + return JSON.stringify(value); +} + +async function typecheckProgram(programPath: string, cwd: string): Promise { + const execFileAsync = promisify(execFile); + const skillTsconfigPath = resolve(dirname(fileURLToPath(import.meta.url)), "tsconfig.json"); + const candidateTsconfigPaths = [resolve(cwd, "tsconfig.json"), skillTsconfigPath]; + let baseTsconfigPath: string | undefined; + for (const tsconfigPath of candidateTsconfigPaths) { + try { + await access(tsconfigPath); + baseTsconfigPath = tsconfigPath; + break; + } catch { + // Try the next candidate. + } + } + if (!baseTsconfigPath) { + throw new Error( + `Typecheck mode requires tsconfig.json at one of: ${candidateTsconfigPaths.join(", ")}`, + ); + } + const tempRoot = resolve(cwd, ".tmp"); + await mkdir(tempRoot, { recursive: true }); + const tempDir = await mkdtemp(resolve(tempRoot, "rig-typecheck-")); + const projectPath = resolve(tempDir, "tsconfig.typecheck.json"); + try { + await writeFile(projectPath, JSON.stringify({ + extends: baseTsconfigPath, + include: [programPath], + }), "utf8"); + await execFileAsync( + "npx", + ["--yes", "--package", "typescript@5.9.3", "--", "tsc", "--project", projectPath, "--pretty", "false"], + { + cwd, + env: { ...process.env, npm_config_ignore_scripts: "true" }, + }, + ); + } catch (error) { + const execError = error as NodeJS.ErrnoException & { stdout?: string; stderr?: string }; + if (execError.code === "ENOENT") { + throw new Error("Typecheck mode requires `npx tsc` to be available in PATH."); + } + const diagnostics = [execError.stdout, execError.stderr] + .filter((entry) => typeof entry === "string" && entry.trim()) + .join("\n") + .trim(); + const detail = diagnostics ? `\n${diagnostics}` : ""; + throw new Error(`Typecheck failed for ${programPath}.${detail}`); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } +} + +async function runRootAgentFromStdin( + programPath: string, + options: LaunchOptions = {}, + io: LauncherIo, + scriptName: string, +): Promise { + const prompt = await readStdin(io.stdin); + if (!prompt.trim()) { + throw new Error(`Usage: ${scriptName} `); + } + + const cwd = options.cwd ?? process.cwd(); + const resolvedPath = isAbsolute(programPath) ? programPath : resolve(cwd, programPath); + if (options.typecheck) { + await typecheckProgram(resolvedPath, cwd); + } + + configureCopilot(resolveCopilotOptions(cwd, options)); + const mod = await import(pathToFileURL(resolvedPath).href); + const rootAgent = asRootProgram(mod.default, "launcher-root"); + if (!rootAgent) { + throw new Error("Expected program to export a root value (agent, string, or prompt builder) as default export."); + } + + const result = await rootAgent(coerceStdinInput(rootAgent, prompt)); + io.stdout.write(renderStdout(result)); +} + +async function runProgramCodeFromStdin( + options: LaunchOptions = {}, + io: LauncherIo, + scriptName: string, +): Promise { + const programCode = await readStdin(io.stdin); + if (!programCode.trim()) { + throw new Error(`Usage: ${scriptName} [--server] [--typecheck]`); + } + + const cwd = options.cwd ?? process.cwd(); + const tempRoot = resolve(cwd, ".tmp"); + await mkdir(tempRoot, { recursive: true }); + const tempDir = await mkdtemp(resolve(tempRoot, "rig-stdin-")); + const tempProgramPath = resolve(tempDir, "program.ts"); + const transformedProgramCode = withInjectedDefaultRootAgent(withInjectedRigImport(programCode)); + await writeFile(tempProgramPath, transformedProgramCode, "utf8"); + try { + if (options.typecheck) { + await typecheckProgram(tempProgramPath, cwd); + } + configureCopilot(resolveCopilotOptions(cwd, options)); + const mod = await import(pathToFileURL(tempProgramPath).href); + const rootAgent = asRootProgram(mod.default, "launcher-inline-root"); + if (!rootAgent) { + throw new Error("Expected program to export a root value (agent, string, or prompt builder) as default export."); + } + const input = noInputInvocation(rootAgent); + if (input === undefined) { + throw new Error("Expected stdin program root agent to have no input (omit input or use input: s.object({}))."); + } + const result = await rootAgent(input); + io.stdout.write(renderStdout(result)); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } +} + +const launcherHelpArgs = new Set(["--help", "-h", "help", "/help", "/?"]); + +function isLauncherHelpArg(arg: string): boolean { + return launcherHelpArgs.has(arg.toLowerCase()); +} + +function renderLauncherUsage(scriptName: string): string { + return [ + `Usage: ${scriptName} [] [--server] [--typecheck]`, + "", + "Modes:", + " Read a rig program from stdin and run its default root export.", + " Read stdin input and run the program file root export.", + "", + "Help aliases:", + " --help, -h, help, /help, /?", + "", + "Examples:", + ` cat ./program.ts | ${scriptName}`, + ` cat ./program.ts | ${scriptName} --typecheck`, + ` echo "Summarize this repository" | ${scriptName} src/program.ts`, + ].join("\n"); +} + +export async function runLauncherCli( + argv: string[] = process.argv.slice(2), + options: LaunchOptions = {}, + io: LauncherIo = process, +): Promise { + const scriptName = process.argv[1] ? basename(process.argv[1]) : "launcher"; + if (argv.some(isLauncherHelpArg)) { + io.stdout.write(`${renderLauncherUsage(scriptName)}\n`); + return; + } + const positionalArgs = argv.filter((arg) => !arg.startsWith("--")); + const flags = argv.filter((arg) => arg.startsWith("--")); + const serverFlag = flags.includes("--server"); + const typecheckFlag = flags.includes("--typecheck"); + const unknownFlags = flags.filter((f) => f !== "--server" && f !== "--typecheck"); + if (positionalArgs.length > 1 || unknownFlags.length > 0) { + throw new Error(`Usage: ${scriptName} [--server] [--typecheck]`); + } + const mergedOptions: LaunchOptions = { + ...options, + ...(serverFlag ? { startServer: true } : {}), + ...(typecheckFlag ? { typecheck: true } : {}), + }; + if (positionalArgs.length === 1) { + await runRootAgentFromStdin(positionalArgs[0]!, mergedOptions, io, scriptName); + return; + } + await runProgramCodeFromStdin(mergedOptions, io, scriptName); +} + +export function agent< + const Input extends Schema = StringSchema, + const Output extends Schema = StringSchema +>(spec: AgentSpec): AgentFn, InferSchema>; +export function agent(spec: AgentSpec): AgentFn { + const normalizedSpec = normalizeSpec(spec); + const inputSchema = normalizedSpec.input ?? defaultStringSchema; + const outputSchema = normalizedSpec.output ?? defaultStringSchema; + + const invoke = async (input: unknown, options: CallOptions = {}) => { + const runtime = resolveCallRuntime(normalizedSpec, options); + const normalizedInput = normalizeInput(input, inputSchema); + let prompt = renderPrompt(normalizedSpec, normalizedInput); + let lastResponse = ""; + const copilot = await createCopilotSession(runtime.model, runtime.systemMessage, runtime.tools); + let failure: unknown; + + try { + for (let turn = 1; turn <= runtime.maxTurns; turn += 1) { + throwIfAborted(runtime.signal); + const context: AgentAddonContext = { + spec: normalizedSpec, + session: copilot.session, + input: normalizedInput, + outputSchema, + signal: runtime.signal, + turn, + maxTurns: runtime.maxTurns, + prompt, + completed: false, + }; + + await runAgentAddons(runtime.addons, context, async () => { + lastResponse = await sendCopilotPrompt(copilot.session, context.prompt, context.signal); + context.response = lastResponse; + }); + + if (context.error !== undefined) { + throw context.error; + } + if (context.completed) { + return context.output; + } + if (context.nextPrompt !== undefined) { + prompt = context.nextPrompt; + continue; + } + if (context.response !== undefined) { + const analysis = analyzeResponse(context.response, context.outputSchema, context.spec.name, context.turn); + if (analysis.ok) { + return analysis.output; + } + throw analysis.error; + } + throw new Error( + `Agent ${normalizedSpec.name}: addons must set context.output with context.completed=true or context.nextPrompt for turn ${turn}.`, + ); + } + } catch (error) { + failure = error; + throw error; + } finally { + try { + await copilot.close(); + } catch (cleanupError) { + if (failure === undefined) { + throw cleanupError; + } + } + } + + throw new Error(`Agent ${normalizedSpec.name} failed after ${runtime.maxTurns} turns. Last response:\n${lastResponse}`); + }; + + const fn = (async (input: unknown, options: CallOptions = {}) => { + const existingContext = copilotRunStorage.getStore(); + if (existingContext) { + return invoke(input, options); + } + + const client = copilotEngine(currentCopilotOptions); + return copilotRunStorage.run({ client }, async () => { + let failure: unknown; + try { + return await invoke(input, options); + } catch (error) { + failure = error; + throw error; + } finally { + try { + await stopCopilotClient(client); + } catch (cleanupError) { + if (failure === undefined) { + throw cleanupError; + } + } + } + }); + }) as AgentFn; + + fn.agentName = normalizedSpec.name; + fn.inputSchema = inputSchema; + fn.outputSchema = outputSchema; + fn.inputShape = inputSchema; + fn.outputShape = outputSchema; + fn.spec = normalizedSpec; + fn._namespace = normalizedSpec.name; + fn.use = (addons) => { + normalizedSpec.addons = [ + ...normalizeAddons(normalizedSpec.addons), + ...normalizeAddons(addons), + ]; + return fn; + }; + return fn; +} + +export type AgentFactory = typeof agent; + +function validate(value: unknown, schema: Schema): ValidationResult { + return validateSchema(value, schema, "$", false); +} + +function normalizeSpec(specOrName: AgentSpec): NormalizedAgentSpec { + const agentName = specOrName.name ?? defaultName; + const spec: NormalizedAgentSpec = { + name: agentName, + }; + if (specOrName.instructions !== undefined) spec.instructions = specOrName.instructions; + if (specOrName.input !== undefined) { + assertValidSchema(specOrName.input, agentName, "input"); + spec.input = specOrName.input; + } + if (specOrName.output !== undefined) { + assertValidSchema(specOrName.output, agentName, "output"); + spec.output = specOrName.output; + } + if (specOrName.model !== undefined) spec.model = specOrName.model; + if (specOrName.maxTurns !== undefined) spec.maxTurns = specOrName.maxTurns; + if (specOrName.addons !== undefined) spec.addons = specOrName.addons; + if (specOrName.agents !== undefined) spec.agents = specOrName.agents; + if (specOrName.systemMessage !== undefined) spec.systemMessage = specOrName.systemMessage; + if (specOrName.tools !== undefined) spec.tools = normalizeTools(specOrName.tools, agentName); + return spec; +} + +function normalizeToolParameters(parameters: ToolParameters | undefined): ToolParameters | undefined { + return parameters !== undefined && isSchema(parameters) ? toJsonSchema(parameters) : parameters; +} + +function normalizeToolConfig(tool: T): T & { skipPermission: boolean } { + return { + ...tool, + skipPermission: tool.skipPermission ?? true, + }; +} + +function normalizeTools(tools: Tool[], agentName: string): Tool[] { + return tools.map((tool, index) => { + if (!tool || typeof tool !== "object") { + throw new Error(`Invalid tool for agent "${agentName}" at tools[${index}]. Expected a tool definition object.`); + } + if (typeof tool.name !== "string" || tool.name.length === 0) { + throw new Error(`Invalid tool for agent "${agentName}" at tools[${index}]. Expected a non-empty tool name.`); + } + return { + ...normalizeToolConfig(tool), + parameters: normalizeToolParameters(tool.parameters), + }; + }); +} + +function normalizeInput(input: unknown, schema: Schema): unknown { + if (input !== undefined) { + return input; + } + if ("type" in schema && schema.type === "string") { + return ""; + } + if ("properties" in schema) { + return {}; + } + return input ?? null; +} + +function renderPrompt(spec: NormalizedAgentSpec, input: unknown): string { + const value = inlinePromptIntents(input); + const instructions = renderInstructions(spec.instructions); + const sections = [ + tag("instructions", instructions.trim()), + tag("output_schema", renderSchema(spec.output ?? defaultStringSchema)), + tag("input", json(value)), + ]; + + if (spec.agents && Object.keys(spec.agents).length > 0) { + sections.push(tag( + "subagents", + json(Object.entries(spec.agents).map(([name, subagent]) => ({ + name, + instructions: subagent.spec.instructions === undefined ? null : renderInstructions(subagent.spec.instructions), + model: subagent.spec.model ?? null, + input: renderSchema(subagent.inputSchema), + output: renderSchema(subagent.outputSchema), + }))), + )); + } + + sections.push(tag("rules", [ + "Return exactly one JSON value.", + "Do not wrap the JSON in Markdown.", + "Match the output schema exactly.", + ].join("\n"))); + + return sections.join("\n\n"); +} + +function renderInstructions(instructions?: AgentSpec["instructions"]): string { + if (instructions === undefined) { + return "Return only valid JSON matching the output schema."; + } + if (typeof instructions === "string") { + return instructions; + } + return instructions.toString(); +} + +export function defaultRepairPrompt(spec: AgentSpec, error: AgentError): string { + const agentName = spec.name ?? defaultName; + return [ + ``, + tag("instructions", "Your previous response was invalid. Return only corrected JSON."), + tag("error", error.message), + tag("output_schema", error.schemaText), + tag("previous_response", error.response), + "", + ].join("\n\n"); +} + +function parseJson(text: string): { ok: true; value: unknown } | { ok: false; error: string } { + try { + return { ok: true, value: JSON.parse(text) }; + } catch {} + + const fenced = extractFencedJson(text); + if (fenced !== undefined) { + try { + return { ok: true, value: JSON.parse(fenced) }; + } catch (error) { + return { ok: false, error: error instanceof Error ? error.message : String(error) }; + } + } + + const objectText = extractBalancedJsonObject(text); + if (objectText === undefined) { + return { ok: false, error: "No JSON value found." }; + } + + try { + return { ok: true, value: JSON.parse(objectText) }; + } catch (error) { + return { ok: false, error: error instanceof Error ? error.message : String(error) }; + } +} + +function extractFencedJson(text: string): string | undefined { + const fenceStart = text.indexOf("```"); + if (fenceStart === -1) { + return undefined; + } + + let cursor = fenceStart + 3; + while (cursor < text.length && /\s/.test(text[cursor]!)) { + cursor += 1; + } + + const labelStart = cursor; + while (cursor < text.length && /[a-z0-9_-]/i.test(text[cursor]!)) { + cursor += 1; + } + + const label = text.slice(labelStart, cursor); + if (label && label.toLowerCase() !== "json") { + return undefined; + } + + while (cursor < text.length && /\s/.test(text[cursor]!)) { + cursor += 1; + } + + const fenceEnd = text.indexOf("```", cursor); + if (fenceEnd === -1) { + return undefined; + } + + return text.slice(cursor, fenceEnd); +} + +function extractBalancedJsonObject(text: string): string | undefined { + const objectStart = text.indexOf("{"); + if (objectStart === -1) { + return undefined; + } + + let depth = 0; + let inString = false; + let escaping = false; + for (let index = objectStart; index < text.length; index += 1) { + const char = text[index]!; + if (inString) { + if (escaping) { + escaping = false; + } else if (char === "\\") { + escaping = true; + } else if (char === "\"") { + inString = false; + } + continue; + } + + if (char === "\"") { + inString = true; + continue; + } + + if (char === "{") { + depth += 1; + } else if (char === "}") { + depth -= 1; + if (depth === 0) { + return text.slice(objectStart, index + 1); + } + } + } + + return undefined; +} + +function validateSchema(value: unknown, schema: Schema, path: string, optional: boolean): ValidationResult { + if ((optional || isOptionalSchema(schema)) && value === undefined) { + return { ok: true }; + } + if ("enum" in schema) { + return schema.enum.some((item: Json) => deepEqual(item, value)) + ? ok() + : bad(path, schema.enum.map((item: Json) => JSON.stringify(item)).join(" | "), value); + } + if ("items" in schema) { + if (!Array.isArray(value)) { + return bad(path, "array", value); + } + for (let index = 0; index < value.length; index += 1) { + const result = validateSchema(value[index], schema.items, `${path}[${index}]`, false); + if (!result.ok) { + return result; + } + } + return ok(); + } + if ("properties" in schema) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return bad(path, "object", value); + } + for (const [key, fieldSchema] of Object.entries(schema.properties) as [string, Schema][]) { + const result = validateSchema( + (value as Record)[key], + fieldSchema, + `${path}.${key}`, + false, + ); + if (!result.ok) { + return result; + } + } + return ok(); + } + if ("additionalProperties" in schema) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return bad(path, "object", value); + } + for (const [key, item] of Object.entries(value as object)) { + const result = validateSchema(item, schema.additionalProperties, `${path}.${key}`, false); + if (!result.ok) { + return result; + } + } + return ok(); + } + if ("type" in schema) { + if (schema.type === "string") return typeof value === "string" ? ok() : bad(path, "string", value); + if (schema.type === "number") return typeof value === "number" ? ok() : bad(path, "number", value); + if (schema.type === "boolean") return typeof value === "boolean" ? ok() : bad(path, "boolean", value); + } + return ok(); +} + +function renderSchema(schema: Schema): string { + return json(schema); +} + +function inlinePromptIntents(value: T): T { + const seen = new WeakSet(); + + const walk = (current: unknown): unknown => { + if (current instanceof PromptBuilder) { + return current.toString(); + } + if (isPromptIntent(current)) { + return renderPromptIntentValue(current); + } + if (!current || typeof current !== "object") { + return current; + } + if (seen.has(current)) { + throw new Error("Cannot serialize circular input while preparing prompt."); + } + seen.add(current); + if (Array.isArray(current)) { + return current.map(walk); + } + return Object.fromEntries(Object.entries(current).map(([key, item]) => [key, walk(item)])); + }; + + return walk(value) as T; +} + +export type ResponseAnalysisResult = { ok: true; output: unknown } | { ok: false; error: AgentError }; + +export function analyzeResponse(response: string, outputSchema: Schema, agentName: string, turn: number): ResponseAnalysisResult { + const parsed = parseJson(response); + if (!parsed.ok) { + return { + ok: false, + error: new AgentError({ + kind: "parse", + agent: agentName, + turn, + response, + schema: outputSchema, + message: `Agent ${agentName} returned invalid JSON: ${parsed.error}`, + }), + }; + } + + const result = validate(parsed.value, outputSchema); + if (!result.ok) { + return { + ok: false, + error: new AgentError({ + kind: "validation", + agent: agentName, + turn, + response, + schema: outputSchema, + message: `Agent ${agentName} output validation failed: ${result.error}`, + }), + }; + } + + return { ok: true, output: parsed.value }; +} + +function renderPromptIntentInstruction(intent: PromptIntent): string { + const options = formatPromptIntentOptions(intent.options); + switch (intent.mode) { + case "prompt.text": + return `Run bash command and return stdout as text: ${intent.command}${promptExecutionContext()}${options}`; + case "prompt.read": + return `Read file and return its contents as text: ${JSON.stringify(requiredPath(intent))}${promptExecutionContext()}${options}`; + case "prompt.write": + return `Write file at path ${JSON.stringify(requiredPath(intent))} with contents:\n${intent.contents ?? ""}${promptExecutionContext()}${options}`; + default: + throw new Error(`Unsupported prompt intent mode: ${(intent as { mode?: string }).mode ?? "unknown"}`); + } +} + +function promptExecutionContext(): string { + return "\nRig runs inside a sandboxed agentic workflow. Execute this action directly without asking for extra permission or confirmation."; +} + +function formatPromptIntentOptions(options: PromptIntent["options"]): string { + return options ? `\nOptions: ${json(options)}` : ""; +} + +function requiredPath(intent: PromptIntent): string { + if (!intent.path) { + throw new Error(`Prompt intent mode ${intent.mode} requires a path.`); + } + return intent.path; +} + +function createPromptIntent( + mode: PromptIntent["mode"], + args: Omit, "__rig" | "id" | "mode">, +): PromptIntent { + return { __rig: "prompt", id: `prompt_intent_${nextPromptIntentId++}`, mode, ...args }; +} + +function stripSignal(options: PromptIntentOptions): Omit { + const { signal: _signal, ...rest } = options; + return rest; +} + +function withOptions, "__rig" | "id" | "mode">>( + value: T, + options?: PromptIntentOptions, +): T | (T & { options: Omit }) { + return options ? { ...value, options: stripSignal(options) } : value; +} + +function configureCopilot(options: CopilotEngineOptions): void { + currentCopilotOptions = options; +} + +function asError(error: unknown): Error { + return error instanceof Error ? error : new Error(String(error)); +} + +function throwCleanupErrors(errors: Error[], message: string): void { + if (errors.length === 1) { + throw errors[0]!; + } + if (errors.length > 1) { + throw new AggregateError(errors, message); + } +} + +async function stopCopilotClient(client: CopilotClient): Promise { + const errors: Error[] = []; + + try { + const stopErrors = await client.stop(); + if (Array.isArray(stopErrors)) { + errors.push(...stopErrors.map(asError)); + } + } catch (error) { + errors.push(asError(error)); + } + + throwCleanupErrors(errors, "Failed to stop Copilot client"); +} + +async function createCopilotSession( + model: string, + systemMessage?: SystemMessageConfig, + tools?: Tool[], +): Promise { + const runContext = copilotRunStorage.getStore(); + const client = runContext?.client; + if (!client) { + throw new Error("No Copilot client found in execution context. Invoke agents through the exported agent function."); + } + const config = { + model, + streaming: false, + onPermissionRequest: approveAll, + ...(systemMessage !== undefined && { systemMessage }), + ...(tools !== undefined && { tools }), + }; + const session = await client.createSession(config); + session.on?.((event: unknown) => { + writeEvent(event); + }); + + return { + session, + async close() { + const errors: Error[] = []; + + if (session.disconnect) { + try { + await session.disconnect(); + } catch (error) { + errors.push(asError(error)); + } + } + + throwCleanupErrors(errors, "Failed to close Copilot session"); + }, + }; +} + +async function sendCopilotPrompt(session: CopilotSession, prompt: string, signal?: AbortSignal): Promise { + const request = signal ? { prompt, signal } : { prompt }; + writeEvent(rigEvent("copilot-ask", { prompt })); + const response = await session.sendAndWait(request); + if (!response) { + return ""; + } + if (typeof response === "string") { + return response; + } + const value = response as any; + return value?.data?.content ?? value?.data?.text ?? value?.text ?? value?.content ?? JSON.stringify(response); +} + +function resolveCallRuntime(spec: NormalizedAgentSpec, options: CallOptions): { + model: string; + maxTurns: number; + signal: AbortSignal | undefined; + addons: AgentAddon[]; + systemMessage: SystemMessageConfig | undefined; + tools: Tool[] | undefined; +} { + return { + model: options.model ?? spec.model ?? "gpt-4.1", + maxTurns: options.maxTurns ?? spec.maxTurns ?? 4, + signal: timeoutSignal(options.signal, options.timeout), + addons: normalizeAddons(spec.addons), + systemMessage: spec.systemMessage, + tools: spec.tools, + }; +} + +function normalizeAddons(addons?: AgentAddon | AgentAddon[]): AgentAddon[] { + if (!addons) { + return []; + } + const items = Array.isArray(addons) ? [...addons] : [addons]; + for (const addon of items) { + if (typeof addon !== "function") { + throw new Error("Agent addon entries must be functions."); + } + } + return items; +} + +async function runAgentAddons( + addons: AgentAddon[], + context: AgentAddonContext, + terminal: () => Promise, +): Promise { + let index = -1; + const dispatch = async (current: number): Promise => { + if (current <= index) { + throw new Error(`Agent ${context.spec.name} addon at index ${current} called next() multiple times.`); + } + index = current; + const addon = addons[current]; + if (addon === undefined) { + await terminal(); + return; + } + await addon(context, () => dispatch(current + 1)); + }; + await dispatch(0); +} + +function isSchema(value: unknown): value is Schema { + return !!value && (typeof value === "object" || typeof value === "function") && SCHEMA_SYMBOL in value; +} + +function assertValidSchema(schema: Schema, agentName: string, slot: "input" | "output", path: string = slot): void { + if (!isSchema(schema)) { + throw new Error(`Invalid ${slot} schema for agent "${agentName}" at ${path}. Use declarative s.* schema helpers.`); + } + if ("items" in schema) { + assertValidSchema(schema.items, agentName, slot, `${path}[]`); + return; + } + if ("additionalProperties" in schema) { + assertValidSchema(schema.additionalProperties, agentName, slot, `${path}.*`); + return; + } + if ("properties" in schema) { + for (const [key, value] of Object.entries(schema.properties) as [string, Schema][]) { + assertValidSchema(value, agentName, slot, `${path}.${key}`); + } + } +} + +function isPromptIntent(value: unknown): value is PromptIntent { + return !!value + && typeof value === "object" + && (value as { __rig?: unknown }).__rig === "prompt" + && typeof (value as { mode?: unknown }).mode === "string"; +} + +function renderPromptIntentValue(intent: PromptIntent): string { + return renderPromptIntentInstruction(intent); +} + +function ok(): ValidationResult { + return { ok: true }; +} + +function bad(path: string, expected: string, actual: unknown): ValidationResult { + const actualType = actual === null ? "null" : Array.isArray(actual) ? "array" : typeof actual; + return { ok: false, error: `${path}: expected ${expected}, got ${actualType}` }; +} + +function tag(name: string, value: string): string { + return `<${name}>\n${value}\n`; +} + +function json(value: unknown): string { + return JSON.stringify(value, null, 2); +} + +function deepEqual(left: unknown, right: unknown): boolean { + return JSON.stringify(left) === JSON.stringify(right); +} + +function escapeAttribute(value: string): string { + return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">"); +} + +function throwIfAborted(signal?: AbortSignal): void { + if (signal?.aborted) { + throw signal.reason ?? new DOMException("Aborted", "AbortError"); + } +} + +function timeoutSignal(parent?: AbortSignal, timeout?: number): AbortSignal | undefined { + if (!timeout) { + return parent; + } + const controller = new AbortController(); + const onAbort = () => controller.abort(parent?.reason); + parent?.addEventListener("abort", onAbort, { once: true }); + const timer = setTimeout(() => controller.abort(new Error(`Timed out after ${timeout}ms`)), timeout); + controller.signal.addEventListener("abort", () => clearTimeout(timer), { once: true }); + return controller.signal; +} + +const isMain = process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url; +if (isMain) { + runLauncherCli().catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`${message}\n`); + process.exitCode = 1; + }); +} diff --git a/skills/rig/samples/02-review-git-diff.md b/skills/rig/samples/02-review-git-diff.md new file mode 100644 index 0000000..55ff518 --- /dev/null +++ b/skills/rig/samples/02-review-git-diff.md @@ -0,0 +1,9 @@ +# 02 - Review Git Diff + +```rig +import { p } from "rig"; + +// Agent role: review the repository diff and return a structured summary. + +export default p`Review ${p.bash("git diff --stat")} and ${p.bash("git status --short")} and return a concise summary with key findings.`; +``` diff --git a/skills/rig/samples/03-diagnose-test-failure.md b/skills/rig/samples/03-diagnose-test-failure.md new file mode 100644 index 0000000..f14e3b8 --- /dev/null +++ b/skills/rig/samples/03-diagnose-test-failure.md @@ -0,0 +1,26 @@ +# 03 - Diagnose Test Failure + +```rig +import { agent, p, s } from "rig"; +// Agent role: review input.diff for correctness and regression risks. Return only the declared output shape. +const reviewer = agent({ + model: "mini", + output: s.object({ + summary: s.string, + risk: s.enum("low", "medium", "high"), + findings: s.array(s.object({ + severity: s.enum("info", "warning", "error"), + message: s.string, + file: s.optional(s.string), + line: s.optional(s.number) + })), + tests: s.array(s.string) + }), + instructions: ` + Review input.diff for correctness and regression risks. + Return only the declared output shape. + `, +}); + +export default reviewer; +``` diff --git a/skills/rig/samples/04-generate-readme.md b/skills/rig/samples/04-generate-readme.md new file mode 100644 index 0000000..6293704 --- /dev/null +++ b/skills/rig/samples/04-generate-readme.md @@ -0,0 +1,27 @@ +# 04 - Generate Readme + +```rig +import { agent, p, s } from "rig"; +const ShResult = s.object({ + ok: s.boolean, + stdout: s.string, + stderr: s.string, + exitCode: s.number +}); +// Agent role: diagnose the failing test result. Do not edit files. +const diagnose = agent({ + model: "mini", + output: s.object({ + rootCause: s.string, + confidence: s.number, + relevantFiles: s.array(s.string), + nextSteps: s.array(s.string) + }), + instructions: ` + Diagnose the failing test result. + Do not edit files. + `, +}); + +export default diagnose; +``` diff --git a/skills/rig/samples/05-write-readme-intent.md b/skills/rig/samples/05-write-readme-intent.md new file mode 100644 index 0000000..f013425 --- /dev/null +++ b/skills/rig/samples/05-write-readme-intent.md @@ -0,0 +1,19 @@ +# 05 - Write Readme Intent + +```rig +import { agent, p, s } from "rig"; +// Agent role: generate a concise README for the package. Include install, usage, and API sections. +const readmeWriter = agent({ + model: "mini", + output: s.object({ + path: s.enum("README.md"), + contents: s.string + }), + instructions: ` + Generate a concise README for the package. + Include install, usage, and API sections. + `, +}); + +export default readmeWriter; +``` diff --git a/skills/rig/samples/06-list-source-files.md b/skills/rig/samples/06-list-source-files.md new file mode 100644 index 0000000..ef01ec1 --- /dev/null +++ b/skills/rig/samples/06-list-source-files.md @@ -0,0 +1,18 @@ +# 06 - List Source Files + +```rig +import { agent, p, s } from "rig"; +// Agent role: confirm whether the write intent succeeded. +const writer = agent({ + model: "mini", + output: s.object({ + written: s.boolean, + summary: s.string + }), + instructions: ` + Confirm whether the write intent succeeded. + `, +}); + +export default writer; +``` diff --git a/skills/rig/samples/07-summarize-many-files.md b/skills/rig/samples/07-summarize-many-files.md new file mode 100644 index 0000000..56c3388 --- /dev/null +++ b/skills/rig/samples/07-summarize-many-files.md @@ -0,0 +1,17 @@ +# 07 - Summarize Many Files + +```rig +import { agent, p, s } from "rig"; + +// Agent role: summarize the repository file list in one sentence. + +const summarizeFiles = agent({ + model: "mini", + instructions: "Summarize the repository file list in one sentence.", + output: s.object({ + summary: s.string, + }), +}); + +export default summarizeFiles; +``` diff --git a/skills/rig/samples/08-extract-package-scripts.md b/skills/rig/samples/08-extract-package-scripts.md new file mode 100644 index 0000000..aed5f5e --- /dev/null +++ b/skills/rig/samples/08-extract-package-scripts.md @@ -0,0 +1,12 @@ +# 08 - Extract Package Scripts + +```rig +import { agent, p, s } from "rig"; +// Agent role: extract package scripts and summarize what they do. +const extractScripts = agent({ + model: "mini", + instructions: p`Read ${p.read("package.json")} and summarize the package scripts. Use ${p.bash("find src -name '*.ts' -type f | sort")} only to call out source files that look relevant.`, + output: s.object({ scriptsByName: s.record(s.string), summary: s.string, relatedFiles: s.array(s.string) }), +}); +export default extractScripts; +``` diff --git a/skills/rig/samples/09-classify-issue.md b/skills/rig/samples/09-classify-issue.md new file mode 100644 index 0000000..b49e349 --- /dev/null +++ b/skills/rig/samples/09-classify-issue.md @@ -0,0 +1,18 @@ +# 09 - Classify Issue + +```rig +import { agent, s } from "rig"; + +// Agent role: classify the issue. + +const classifyIssue = agent({ + model: "mini", + instructions: "Classify the issue.", + output: s.object({ + label: s.enum("bug", "feature", "question", "docs"), + confidence: s.enum("low", "medium", "high"), + }), +}); + +export default classifyIssue; +``` diff --git a/skills/rig/samples/10-triage-pr.md b/skills/rig/samples/10-triage-pr.md new file mode 100644 index 0000000..77c90cc --- /dev/null +++ b/skills/rig/samples/10-triage-pr.md @@ -0,0 +1,18 @@ +# 10 - Triage Pr + +```rig +import { agent, s } from "rig"; +// Agent role: classify the GitHub issue and suggest labels. +const classifyIssue = agent({ + model: "mini", + output: s.object({ + kind: s.enum("bug", "feature", "question", "chore"), + priority: s.enum("p0", "p1", "p2", "p3"), + rationale: s.string, + labels: s.array(s.string) + }), + instructions: `Classify the GitHub issue and suggest labels.`, +}); + +export default classifyIssue; +``` diff --git a/skills/rig/samples/11-release-notes.md b/skills/rig/samples/11-release-notes.md new file mode 100644 index 0000000..bd1f6ae --- /dev/null +++ b/skills/rig/samples/11-release-notes.md @@ -0,0 +1,18 @@ +# 11 - Release Notes + +```rig +import { agent, p, s } from "rig"; +// Agent role: triage the pull request and recommend reviewers. +const triage = agent({ + model: "mini", + output: s.object({ + area: s.enum("runtime", "docs", "tests", "ci", "unknown"), + risk: s.enum("low", "medium", "high"), + reviewers: s.array(s.string), + reason: s.string + }), + instructions: `Triage the pull request and recommend reviewers.`, +}); + +export default triage; +``` diff --git a/skills/rig/samples/12-security-scan-review.md b/skills/rig/samples/12-security-scan-review.md new file mode 100644 index 0000000..09673ff --- /dev/null +++ b/skills/rig/samples/12-security-scan-review.md @@ -0,0 +1,18 @@ +# 12 - Security Scan Review + +```rig +import { agent, p, s } from "rig"; +// Agent role: write release notes from commits. Omit empty sections as empty arrays. +const releaseNotes = agent({ + model: "mini", + output: s.object({ + version: s.optional(s.string), + highlights: s.array(s.string), + breaking: s.array(s.string), + fixes: s.array(s.string) + }), + instructions: `Write release notes from commits. Omit empty sections as empty arrays.`, +}); + +export default releaseNotes; +``` diff --git a/skills/rig/samples/13-test-plan.md b/skills/rig/samples/13-test-plan.md new file mode 100644 index 0000000..656a512 --- /dev/null +++ b/skills/rig/samples/13-test-plan.md @@ -0,0 +1,20 @@ +# 13 - Test Plan + +```rig +import { agent, p, s } from "rig"; +// Agent role: review dependency security posture from the provided outputs. +const securityReview = agent({ + model: "mini", + output: s.object({ + status: s.enum("clean", "needs-action", "unknown"), + findings: s.array(s.object({ + package: s.string, + severity: s.string, + action: s.string + })) + }), + instructions: `Review dependency security posture from the provided outputs.`, +}); + +export default securityReview; +``` diff --git a/skills/rig/samples/14-changelog-categorizer.md b/skills/rig/samples/14-changelog-categorizer.md new file mode 100644 index 0000000..bf2bbed --- /dev/null +++ b/skills/rig/samples/14-changelog-categorizer.md @@ -0,0 +1,17 @@ +# 14 - Changelog Categorizer + +```rig +import { agent, p, s } from "rig"; +// Agent role: create a focused validation plan for the current changes. +const planner = agent({ + model: "mini", + output: s.object({ + commands: s.array(s.string), + manualChecks: s.array(s.string), + rationale: s.string + }), + instructions: `Create a focused validation plan for the current changes.`, +}); + +export default planner; +``` diff --git a/skills/rig/samples/15-api-diff-summary.md b/skills/rig/samples/15-api-diff-summary.md new file mode 100644 index 0000000..28648eb --- /dev/null +++ b/skills/rig/samples/15-api-diff-summary.md @@ -0,0 +1,16 @@ +# 15 - Api Diff Summary + +```rig +import { agent, s } from "rig"; +// Agent role: convert the change description to Keep a Changelog style. +const categorize = agent({ + model: "mini", + output: s.object({ + category: s.enum("added", "changed", "deprecated", "removed", "fixed", "security"), + entry: s.string + }), + instructions: `Convert the change description to Keep a Changelog style.`, +}); + +export default categorize; +``` diff --git a/skills/rig/samples/16-docs-gap-analysis.md b/skills/rig/samples/16-docs-gap-analysis.md new file mode 100644 index 0000000..c33bb16 --- /dev/null +++ b/skills/rig/samples/16-docs-gap-analysis.md @@ -0,0 +1,17 @@ +# 16 - Docs Gap Analysis + +```rig +import { agent, p, s } from "rig"; +// Agent role: compare public API declarations and identify breaking changes. +const apiDiff = agent({ + model: "mini", + output: s.object({ + breaking: s.boolean, + summary: s.string, + changes: s.array(s.string) + }), + instructions: `Compare public API declarations and identify breaking changes.`, +}); + +export default apiDiff; +``` diff --git a/skills/rig/samples/17-refactor-plan.md b/skills/rig/samples/17-refactor-plan.md new file mode 100644 index 0000000..3a096b4 --- /dev/null +++ b/skills/rig/samples/17-refactor-plan.md @@ -0,0 +1,17 @@ +# 17 - Refactor Plan + +```rig +import { agent, p, s } from "rig"; +// Agent role: find documentation gaps against the source API. +const docsGap = agent({ + model: "mini", + output: s.object({ + missing: s.array(s.string), + stale: s.array(s.string), + quickFixes: s.array(s.string) + }), + instructions: `Find documentation gaps against the source API.`, +}); + +export default docsGap; +``` diff --git a/skills/rig/samples/18-patch-writer-output.md b/skills/rig/samples/18-patch-writer-output.md new file mode 100644 index 0000000..f7e57e5 --- /dev/null +++ b/skills/rig/samples/18-patch-writer-output.md @@ -0,0 +1,17 @@ +# 18 - Patch Writer Output + +```rig +import { agent, p, s } from "rig"; +// Agent role: plan a minimal, low-risk refactor. Do not edit files. +const refactorPlan = agent({ + model: "mini", + output: s.object({ + steps: s.array(s.string), + files: s.array(s.string), + risks: s.array(s.string) + }), + instructions: `Plan a minimal, low-risk refactor. Do not edit files.`, +}); + +export default refactorPlan; +``` diff --git a/skills/rig/samples/19-fix-then-review.md b/skills/rig/samples/19-fix-then-review.md new file mode 100644 index 0000000..2d7606f --- /dev/null +++ b/skills/rig/samples/19-fix-then-review.md @@ -0,0 +1,17 @@ +# 19 - Fix Then Review + +```rig +import { agent, p, s } from "rig"; +// Agent role: return a complete replacement for the target file. +const patcher = agent({ + model: "mini", + output: s.object({ + path: s.string, + contents: s.string, + summary: s.string + }), + instructions: `Return a complete replacement for the target file.`, +}); + +export default patcher; +``` diff --git a/skills/rig/samples/20-issue-reproducer.md b/skills/rig/samples/20-issue-reproducer.md new file mode 100644 index 0000000..9f8ff6d --- /dev/null +++ b/skills/rig/samples/20-issue-reproducer.md @@ -0,0 +1,18 @@ +# 20 - Issue Reproducer + +```rig +import { agent, p, s } from "rig"; +const Diagnosis = s.object({ rootCause: s.string, confidence: s.number }); +// Agent role: diagnose the failing test output. +const diagnose = agent({ model: "mini", output: Diagnosis, instructions: "Diagnose the failing test output." }); +// Agent role: write the smallest safe patch for the diagnosis. +const fix = agent({ model: "mini", input: s.object({ diagnosis: Diagnosis }), instructions: "Write the smallest safe patch." }); +// Agent role: reproduce and fix the bug using the provided specialists when helpful. +const issueReproducer = agent({ + model: "mini", + instructions: p`Reproduce the failing test from ${p.bash("npm test")} and use the specialists when helpful.`, + output: s.object({ diagnosis: Diagnosis, fixSummary: s.string, approved: s.boolean }), + agents: { diagnose, fix }, +}); +export default issueReproducer; +``` diff --git a/skills/rig/samples/21-ci-log-diagnosis.md b/skills/rig/samples/21-ci-log-diagnosis.md new file mode 100644 index 0000000..6db9cdd --- /dev/null +++ b/skills/rig/samples/21-ci-log-diagnosis.md @@ -0,0 +1,18 @@ +# 21 - Ci Log Diagnosis + +```rig +import { agent, s } from "rig"; +// Agent role: extract a clear reproduction from the issue. +const reproducer = agent({ + model: "mini", + output: s.object({ + steps: s.array(s.string), + expected: s.string, + actual: s.string, + missingInfo: s.array(s.string) + }), + instructions: `Extract a clear reproduction from the issue.`, +}); + +export default reproducer; +``` diff --git a/skills/rig/samples/22-config-normalizer.md b/skills/rig/samples/22-config-normalizer.md new file mode 100644 index 0000000..28ef882 --- /dev/null +++ b/skills/rig/samples/22-config-normalizer.md @@ -0,0 +1,17 @@ +# 22 - Config Normalizer + +```rig +import { agent, p, s } from "rig"; +// Agent role: diagnose the CI log. Prefer the first real failure over cascading errors. +const ciDiagnosis = agent({ + model: "mini", + output: s.object({ + failure: s.string, + likelyCause: s.string, + commandsToTry: s.array(s.string) + }), + instructions: `Diagnose the CI log. Prefer the first real failure over cascading errors.`, +}); + +export default ciDiagnosis; +``` diff --git a/skills/rig/samples/23-schema-inference.md b/skills/rig/samples/23-schema-inference.md new file mode 100644 index 0000000..32b8639 --- /dev/null +++ b/skills/rig/samples/23-schema-inference.md @@ -0,0 +1,16 @@ +# 23 - Schema Inference + +```rig +import { agent, p, s } from "rig"; +// Agent role: normalize the config into a JSON-compatible object. +const normalize = agent({ + model: "mini", + output: s.object({ + normalized: s.unknown, + warnings: s.array(s.string) + }), + instructions: `Normalize the config into a JSON-compatible object.`, +}); + +export default normalize; +``` diff --git a/skills/rig/samples/24-error-message-improver.md b/skills/rig/samples/24-error-message-improver.md new file mode 100644 index 0000000..8da215f --- /dev/null +++ b/skills/rig/samples/24-error-message-improver.md @@ -0,0 +1,20 @@ +# 24 - Error Message Improver + +```rig +import { agent, p, s } from "rig"; +// Agent role: infer a practical runtime-visible schema from the samples. +const inferShape = agent({ + model: "mini", + output: s.object({ + fields: s.array(s.object({ + name: s.string, + type: s.string, + optional: s.boolean + })), + example: s.unknown + }), + instructions: `Infer a practical runtime-visible schema from the samples.`, +}); + +export default inferShape; +``` diff --git a/skills/rig/samples/25-migration-guide.md b/skills/rig/samples/25-migration-guide.md new file mode 100644 index 0000000..6c24740 --- /dev/null +++ b/skills/rig/samples/25-migration-guide.md @@ -0,0 +1,16 @@ +# 25 - Migration Guide + +```rig +import { agent, s } from "rig"; +// Agent role: rewrite the error to be actionable and precise. +const improve = agent({ + model: "mini", + output: s.object({ + message: s.string, + explanation: s.string + }), + instructions: `Rewrite the error to be actionable and precise.`, +}); + +export default improve; +``` diff --git a/skills/rig/samples/26-design-review.md b/skills/rig/samples/26-design-review.md new file mode 100644 index 0000000..2aaa593 --- /dev/null +++ b/skills/rig/samples/26-design-review.md @@ -0,0 +1,20 @@ +# 26 - Design Review + +```rig +import { agent, s } from "rig"; +// Agent role: write a concise migration guide. +const migration = agent({ + model: "mini", + output: s.object({ + title: s.string, + steps: s.array(s.string), + examples: s.array(s.object({ + before: s.string, + after: s.string + })) + }), + instructions: `Write a concise migration guide.`, +}); + +export default migration; +``` diff --git a/skills/rig/samples/27-dependency-upgrade-plan.md b/skills/rig/samples/27-dependency-upgrade-plan.md new file mode 100644 index 0000000..1f9d713 --- /dev/null +++ b/skills/rig/samples/27-dependency-upgrade-plan.md @@ -0,0 +1,18 @@ +# 27 - Dependency Upgrade Plan + +```rig +import { agent, s } from "rig"; +// Agent role: review the design proposal for simplicity and maintainability. +const designReview = agent({ + model: "mini", + output: s.object({ + decision: s.enum("approve", "revise", "reject"), + strengths: s.array(s.string), + concerns: s.array(s.string), + requiredChanges: s.array(s.string) + }), + instructions: `Review the design proposal for simplicity and maintainability.`, +}); + +export default designReview; +``` diff --git a/skills/rig/samples/28-license-check.md b/skills/rig/samples/28-license-check.md new file mode 100644 index 0000000..0e5158b --- /dev/null +++ b/skills/rig/samples/28-license-check.md @@ -0,0 +1,21 @@ +# 28 - License Check + +```rig +import { agent, p, s } from "rig"; +// Agent role: plan safe dependency upgrades. +const upgradePlan = agent({ + model: "mini", + output: s.object({ + upgrades: s.array(s.object({ + package: s.string, + from: s.string, + to: s.string, + risk: s.string + })), + order: s.array(s.string) + }), + instructions: `Plan safe dependency upgrades.`, +}); + +export default upgradePlan; +``` diff --git a/skills/rig/samples/29-bug-report-draft.md b/skills/rig/samples/29-bug-report-draft.md new file mode 100644 index 0000000..53e088a --- /dev/null +++ b/skills/rig/samples/29-bug-report-draft.md @@ -0,0 +1,21 @@ +# 29 - Bug Report Draft + +```rig +import { agent, p, s } from "rig"; +// Agent role: flag unknown or concerning dependency licenses. +const licenseCheck = agent({ + model: "mini", + output: s.object({ + compliant: s.boolean, + unknown: s.array(s.string), + concerning: s.array(s.object({ + package: s.string, + license: s.string, + reason: s.string + })) + }), + instructions: `Flag unknown or concerning dependency licenses.`, +}); + +export default licenseCheck; +``` diff --git a/skills/rig/samples/30-github-action-review.md b/skills/rig/samples/30-github-action-review.md new file mode 100644 index 0000000..98ec462 --- /dev/null +++ b/skills/rig/samples/30-github-action-review.md @@ -0,0 +1,17 @@ +# 30 - Github Action Review + +```rig +import { agent, p, s } from "rig"; +// Agent role: draft a GitHub bug report from the failure details. +const bugReport = agent({ + model: "mini", + output: s.object({ + title: s.string, + body: s.string, + labels: s.array(s.string) + }), + instructions: `Draft a GitHub bug report from the failure details.`, +}); + +export default bugReport; +``` diff --git a/skills/rig/samples/31-monorepo-package-map.md b/skills/rig/samples/31-monorepo-package-map.md new file mode 100644 index 0000000..bcfcb5f --- /dev/null +++ b/skills/rig/samples/31-monorepo-package-map.md @@ -0,0 +1,17 @@ +# 31 - Monorepo Package Map + +```rig +import { agent, p, s } from "rig"; +// Agent role: review the workflow for reliability, caching, and least privilege. +const actionReview = agent({ + model: "mini", + output: s.object({ + summary: s.string, + problems: s.array(s.string), + improvements: s.array(s.string) + }), + instructions: `Review the workflow for reliability, caching, and least privilege.`, +}); + +export default actionReview; +``` diff --git a/skills/rig/samples/32-command-planner.md b/skills/rig/samples/32-command-planner.md new file mode 100644 index 0000000..0c752ab --- /dev/null +++ b/skills/rig/samples/32-command-planner.md @@ -0,0 +1,24 @@ +# 32 - Command Planner + +```rig +import { agent, p, s } from "rig"; +// Agent role: build a package map for a JavaScript monorepo. +const packageMap = agent({ + model: "mini", + output: s.object({ + packages: s.array(s.object({ + name: s.string, + path: s.string, + private: s.boolean + })), + relationships: s.array(s.object({ + from: s.string, + to: s.string, + kind: s.string + })) + }), + instructions: `Build a package map for a JavaScript monorepo.`, +}); + +export default packageMap; +``` diff --git a/skills/rig/samples/33-readonly-investigator.md b/skills/rig/samples/33-readonly-investigator.md new file mode 100644 index 0000000..80b988d --- /dev/null +++ b/skills/rig/samples/33-readonly-investigator.md @@ -0,0 +1,19 @@ +# 33 - Readonly Investigator + +```rig +import { agent, s } from "rig"; +// Agent role: plan shell commands for the goal. Prefer readonly commands. +const commandPlanner = agent({ + model: "mini", + output: s.object({ + commands: s.array(s.object({ + command: s.string, + purpose: s.string, + readonly: s.boolean + })) + }), + instructions: `Plan shell commands for the goal. Prefer readonly commands.`, +}); + +export default commandPlanner; +``` diff --git a/skills/rig/samples/34-intent-options.md b/skills/rig/samples/34-intent-options.md new file mode 100644 index 0000000..c1bc7d7 --- /dev/null +++ b/skills/rig/samples/34-intent-options.md @@ -0,0 +1,16 @@ +# 34 - Intent Options + +```rig +import { agent, p, s } from "rig"; +// Agent role: investigate the project using only readonly evidence. +const investigator = agent({ + model: "mini", + output: s.object({ + observations: s.array(s.string), + likelyEntryPoints: s.array(s.string) + }), + instructions: `Investigate the project using only readonly evidence.`, +}); + +export default investigator; +``` diff --git a/skills/rig/samples/35-call-options.md b/skills/rig/samples/35-call-options.md new file mode 100644 index 0000000..782c90d --- /dev/null +++ b/skills/rig/samples/35-call-options.md @@ -0,0 +1,16 @@ +# 35 - Call Options + +```rig +import { agent, p, s } from "rig"; +// Agent role: parse environment outputs. +const envReader = agent({ + model: "mini", + output: s.object({ + nodeMajor: s.number, + files: s.array(s.string) + }), + instructions: `Parse environment outputs.`, +}); + +export default envReader; +``` diff --git a/skills/rig/samples/36-subagent-delegation.md b/skills/rig/samples/36-subagent-delegation.md new file mode 100644 index 0000000..c868549 --- /dev/null +++ b/skills/rig/samples/36-subagent-delegation.md @@ -0,0 +1,19 @@ +# 36 - Subagent Delegation + +```rig +import { agent, s } from "rig"; +// Agent role: extract the most important implementation details from the topic. +const researcher = agent({ + model: "mini", + output: s.object({ summary: s.string, risks: s.array(s.string) }), + instructions: "Extract the most important implementation details from the topic.", +}); +// Agent role: plan the next steps and use the researcher when helpful. +const planner = agent({ + model: "mini", + instructions: "Plan the next steps for explaining runtime-visible schemas in one paragraph. Use the researcher when helpful.", + output: s.object({ decision: s.string, nextSteps: s.array(s.string) }), + agents: { researcher }, +}); +export default planner; +``` diff --git a/skills/rig/samples/37-output-with-nullable.md b/skills/rig/samples/37-output-with-nullable.md new file mode 100644 index 0000000..9e340d8 --- /dev/null +++ b/skills/rig/samples/37-output-with-nullable.md @@ -0,0 +1,26 @@ +# 37 - Output With Nullable + +```rig +import { agent, s } from "rig"; +// Agent role: summarize the diff. +const summarizeDiff = agent({ + model: "mini", + output: s.object({ + summary: s.string, + files: s.array(s.string) + }), + instructions: `Summarize the diff.`, +}); +// Agent role: review the diff. You may use the provided subagent conceptually. +const reviewer = agent({ + model: "mini", + output: s.object({ + summary: s.string, + issues: s.array(s.string) + }), + agents: { summarizeDiff }, + instructions: `Review the diff. You may use the provided subagent conceptually.`, +}); + +export default reviewer; +``` diff --git a/skills/rig/samples/38-exact-literal-output.md b/skills/rig/samples/38-exact-literal-output.md new file mode 100644 index 0000000..31ad917 --- /dev/null +++ b/skills/rig/samples/38-exact-literal-output.md @@ -0,0 +1,16 @@ +# 38 - Exact Literal Output + +```rig +import { agent, s } from "rig"; +// Agent role: extract event metadata. Use undefined when deletedAt is absent. +const parseEvent = agent({ + model: "mini", + output: s.object({ + title: s.string, + deletedAt: s.optional(s.string) + }), + instructions: `Extract event metadata. Use undefined when deletedAt is absent.`, +}); + +export default parseEvent; +``` diff --git a/skills/rig/samples/39-unknown-raw-output.md b/skills/rig/samples/39-unknown-raw-output.md new file mode 100644 index 0000000..b44e4ad --- /dev/null +++ b/skills/rig/samples/39-unknown-raw-output.md @@ -0,0 +1,17 @@ +# 39 - Unknown Raw Output + +```rig +import { agent, s } from "rig"; +// Agent role: convert the finding into a typed review record. +const reviewRecord = agent({ + model: "mini", + output: s.object({ + kind: s.enum("review-finding"), + finding: s.string, + severity: s.enum("info", "warning", "error") + }), + instructions: `Convert the finding into a typed review record.`, +}); + +export default reviewRecord; +``` diff --git a/skills/rig/samples/40-record-output.md b/skills/rig/samples/40-record-output.md new file mode 100644 index 0000000..f3ef379 --- /dev/null +++ b/skills/rig/samples/40-record-output.md @@ -0,0 +1,16 @@ +# 40 - Record Output + +```rig +import { agent, p, s } from "rig"; +// Agent role: extract any JSON object from input.text into raw. +const extractJson = agent({ + model: "mini", + output: s.object({ + raw: s.unknown, + summary: s.string + }), + instructions: `Extract any JSON object from input.text into raw.`, +}); + +export default extractJson; +``` diff --git a/skills/rig/samples/41-parse-coverage.md b/skills/rig/samples/41-parse-coverage.md new file mode 100644 index 0000000..defc58b --- /dev/null +++ b/skills/rig/samples/41-parse-coverage.md @@ -0,0 +1,19 @@ +# 41 - Parse Coverage + +```rig +import { agent, p, s } from "rig"; +// Agent role: parse coverage by file path. +const coverage = agent({ + model: "mini", + output: s.object({ + files: s.record(s.object({ + lines: s.number, + branches: s.number, + notes: s.optional(s.string) + })) + }), + instructions: `Parse coverage by file path.`, +}); + +export default coverage; +``` diff --git a/skills/rig/samples/42-json-repair.md b/skills/rig/samples/42-json-repair.md new file mode 100644 index 0000000..414ea1a --- /dev/null +++ b/skills/rig/samples/42-json-repair.md @@ -0,0 +1,18 @@ +# 42 - Json Repair + +```rig +import { agent, s } from "rig"; + +// Agent role: summarize the diff. + +const summarize = agent({ + model: "mini", + instructions: "Summarize the diff.", + output: s.object({ + summary: s.string, + }), + maxTurns: 2, +}); + +export default summarize; +``` diff --git a/skills/rig/samples/43-snapshot-test-updater.md b/skills/rig/samples/43-snapshot-test-updater.md new file mode 100644 index 0000000..ce7e1f1 --- /dev/null +++ b/skills/rig/samples/43-snapshot-test-updater.md @@ -0,0 +1,16 @@ +# 43 - Snapshot Test Updater + +```rig +import { agent, s } from "rig"; +// Agent role: repair input.text into a JSON-compatible value. +const repair = agent({ + model: "mini", + output: s.object({ + repaired: s.unknown, + changes: s.array(s.string) + }), + instructions: `Repair input.text into a JSON-compatible value.`, +}); + +export default repair; +``` diff --git a/skills/rig/samples/44-flaky-test-analysis.md b/skills/rig/samples/44-flaky-test-analysis.md new file mode 100644 index 0000000..e4e4e1e --- /dev/null +++ b/skills/rig/samples/44-flaky-test-analysis.md @@ -0,0 +1,17 @@ +# 44 - Flaky Test Analysis + +```rig +import { agent, p, s } from "rig"; +// Agent role: decide whether snapshot updates are legitimate. +const snapshotReview = agent({ + model: "mini", + output: s.object({ + safeToUpdate: s.boolean, + reason: s.string, + command: s.optional(s.string) + }), + instructions: `Decide whether snapshot updates are legitimate.`, +}); + +export default snapshotReview; +``` diff --git a/skills/rig/samples/45-code-owner-suggestion.md b/skills/rig/samples/45-code-owner-suggestion.md new file mode 100644 index 0000000..779b790 --- /dev/null +++ b/skills/rig/samples/45-code-owner-suggestion.md @@ -0,0 +1,17 @@ +# 45 - Code Owner Suggestion + +```rig +import { agent, p, s } from "rig"; +// Agent role: analyze whether the test failure appears flaky. +const flaky = agent({ + model: "mini", + output: s.object({ + likelyFlaky: s.boolean, + signals: s.array(s.string), + stabilizationIdeas: s.array(s.string) + }), + instructions: `Analyze whether the test failure appears flaky.`, +}); + +export default flaky; +``` diff --git a/skills/rig/samples/46-prompt-intent-inspection.md b/skills/rig/samples/46-prompt-intent-inspection.md new file mode 100644 index 0000000..7217d60 --- /dev/null +++ b/skills/rig/samples/46-prompt-intent-inspection.md @@ -0,0 +1,16 @@ +# 46 - Prompt Intent Inspection + +```rig +import { agent, p, s } from "rig"; +// Agent role: suggest owners for changed files. +const owners = agent({ + model: "mini", + output: s.object({ + owners: s.array(s.string), + unmatchedFiles: s.array(s.string) + }), + instructions: `Suggest owners for changed files.`, +}); + +export default owners; +``` diff --git a/skills/rig/samples/47-prompt-intents.md b/skills/rig/samples/47-prompt-intents.md new file mode 100644 index 0000000..db2932d --- /dev/null +++ b/skills/rig/samples/47-prompt-intents.md @@ -0,0 +1,18 @@ +# 47 - Prompt Intents + +```rig +import { agent, p, s } from "rig"; + +// Agent role: summarize the current git workspace changes. + +const promptIntents = agent({ + model: "mini", + instructions: "Summarize the current git workspace changes.", + output: s.object({ + summary: s.string, + changedFiles: s.array(s.string), + }), +}); + +export default promptIntents; +``` diff --git a/skills/rig/samples/48-custom-engine.md b/skills/rig/samples/48-custom-engine.md new file mode 100644 index 0000000..7b31065 --- /dev/null +++ b/skills/rig/samples/48-custom-engine.md @@ -0,0 +1,14 @@ +# 48 - Copilot Runtime + +Launch this sample with `node skills/rig/rig.ts --server` when you want stdio mode. + +```rig +import { agent, s } from "rig"; +// Agent role: explain when to launch rig with --server. +const review = agent({ + model: "mini", + instructions: "Explain when to launch rig with --server instead of connecting to an HTTP Copilot server.", + output: s.object({ summary: s.string, recommendedMode: s.enum("http", "server") }), +}); +export default review; +``` diff --git a/skills/rig/samples/49-timeout-signal-helper.md b/skills/rig/samples/49-timeout-signal-helper.md new file mode 100644 index 0000000..8c5c553 --- /dev/null +++ b/skills/rig/samples/49-timeout-signal-helper.md @@ -0,0 +1,13 @@ +# 49 - Timeout Signal Helper + +```rig +import { agent, s } from "rig"; +import { timeout } from "rig/addons"; +// Agent role: return a short response before the timeout expires. +const worker = agent({ + model: "mini", + instructions: "Return a short response before the timeout expires.", + addons: timeout({ timeout: 5_000 }), +}); +export default worker; +``` diff --git a/skills/rig/samples/50-end-to-end-release-agent.md b/skills/rig/samples/50-end-to-end-release-agent.md new file mode 100644 index 0000000..fce1866 --- /dev/null +++ b/skills/rig/samples/50-end-to-end-release-agent.md @@ -0,0 +1,30 @@ +# 50 - End To End Release Agent + +```rig +import { agent, p, s } from "rig"; +// Agent role: summarize the release candidate changes. +const analyzeChanges = agent({ model: "mini", + input: s.object({ diff: s.string, commits: s.string }), + output: s.object({ summary: s.string, highlights: s.array(s.string) }), + instructions: "Summarize the release candidate changes.", +}); +// Agent role: choose the safest semantic version bump. +const chooseVersion = agent({ model: "mini", + input: s.object({ summary: s.string, highlights: s.array(s.string) }), + output: s.object({ bump: s.enum("patch", "minor", "major"), rationale: s.string }), + instructions: "Choose the safest semantic version bump.", +}); +// Agent role: draft the release note from the chosen version bump. +const draftRelease = agent({ model: "mini", + input: s.object({ bump: s.enum("patch", "minor", "major"), rationale: s.string, summary: s.string }), + output: s.object({ title: s.string, checklist: s.array(s.string), risks: s.array(s.string) }), + instructions: "Draft the release note from the chosen version bump.", +}); +// Agent role: plan the next release using the provided specialists. +const releaseAgent = agent({ model: "mini", + instructions: p`Plan the next release using ${p.bash("git diff --stat -- .")} and ${p.bash("git log --oneline -20")}.`, + output: s.object({ title: s.string, bump: s.enum("patch", "minor", "major"), checklist: s.array(s.string), risks: s.array(s.string) }), + agents: { analyzeChanges, chooseVersion, draftRelease }, +}); +export default releaseAgent; +``` diff --git a/skills/rig/samples/51-subagent-task-harness.md b/skills/rig/samples/51-subagent-task-harness.md new file mode 100644 index 0000000..25ce224 --- /dev/null +++ b/skills/rig/samples/51-subagent-task-harness.md @@ -0,0 +1,25 @@ +# 51 - Subagent Task Harness for Rig Markdown + +```rig +import { agent, p, s } from "rig"; +// Agent role: draft a runnable rig markdown snippet for the requested task. +const draftRigMarkdown = agent({ + model: "mini", + output: s.object({ markdown: s.string }), + instructions: "Return exactly one markdown response with one ```rig fenced block.", +}); +// Agent role: validate generated rig code by running TypeScript in no-emit mode. +const typecheckRigProgram = agent({ + model: "typecheck", + output: s.object({ ok: s.boolean, diagnostics: s.string }), + instructions: p`Run ${p.bash("npx tsc --noEmit --pretty false")} and summarize whether typecheck passed.`, +}); +// Agent role: solve the task by delegating to subagents and returning markdown. +const solveTask = agent({ + model: "large", + output: s.object({ markdown: s.string }), + agents: { draftRigMarkdown, typecheckRigProgram }, + instructions: "Use the drafting subagent, validate with typecheck, then return one runnable rig markdown snippet.", +}); +export default solveTask; +``` diff --git a/skills/rig/samples/52-claude-design.md b/skills/rig/samples/52-claude-design.md new file mode 100644 index 0000000..0fd4fa3 --- /dev/null +++ b/skills/rig/samples/52-claude-design.md @@ -0,0 +1,29 @@ +# 52 - Claude Design (Critique-Revise) + +```rig +import { agent, s } from "rig"; +// Agent role: coordinate writer/critic/reviser to produce one final response. +const writer = agent({ + model: "mini", + output: s.object({ draft: s.string }), + instructions: "Write a helpful, clear response to the request.", +}); +const critic = agent({ + model: "mini", + input: s.object({ request: s.string, draft: s.string }), + output: s.object({ issues: s.array(s.string), score: s.number, acceptable: s.boolean }), + instructions: "Evaluate the draft against helpfulness, harmlessness, and honesty principles.", +}); +const reviser = agent({ + model: "mini", + input: s.object({ request: s.string, draft: s.string, issues: s.array(s.string) }), + output: s.object({ response: s.string }), + instructions: "Revise the draft to address all issues identified by the critic.", +}); +const root = agent({ model: "mini", + instructions: "Use writer, critic, and reviser to produce one final response.", + output: s.object({ response: s.string }), + agents: { writer, critic, reviser }, +}); +export default root; +``` diff --git a/skills/rig/samples/53-ralf-loop.md b/skills/rig/samples/53-ralf-loop.md new file mode 100644 index 0000000..646fa0b --- /dev/null +++ b/skills/rig/samples/53-ralf-loop.md @@ -0,0 +1,26 @@ +# 53 - RALF Loop (Run, Analyze, Loop, Fix) + +```rig +import { agent, p, s } from "rig"; +// Agent role: diagnose failing tests and decide if the loop is done. +const diagnose = agent({ + model: "mini", + input: s.object({ ok: s.boolean, stdout: s.string, exitCode: s.number }), + output: s.object({ done: s.boolean, rootCause: s.string }), + instructions: "Diagnose test failures. Set done to true if all tests passed.", +}); +// Agent role: apply the smallest safe fix for the root cause. +const fix = agent({ + model: "mini", + output: s.object({ summary: s.string, changed: s.boolean }), + instructions: "Apply the smallest safe fix for the root cause.", +}); +// Agent role: run a RALF loop iterating diagnose-fix cycles until tests pass. +const ralfLoop = agent({ + model: "large", + output: s.object({ iterations: s.number, fixed: s.boolean }), + agents: { diagnose, fix }, + instructions: p`Run ${p.bash("npm test")} then loop: diagnose failures, fix, repeat up to 3 times.`, +}); +export default ralfLoop; +``` diff --git a/skills/rig/samples/54-large-scale-summarization.md b/skills/rig/samples/54-large-scale-summarization.md new file mode 100644 index 0000000..0d6e895 --- /dev/null +++ b/skills/rig/samples/54-large-scale-summarization.md @@ -0,0 +1,17 @@ +# 54 - Large-Scale Summarization at Minimum Cost + +```rig +import { agent, p, s } from "rig"; +// Agent role: summarize one evidence shard cheaply with a small model. +const summarizeShard = agent({ model: "mini", input: s.object({ scenario: s.string, shardLabel: s.string, evidence: s.string }), output: s.object({ shardLabel: s.string, summary: s.string, facts: s.array(s.string) }), instructions: "Summarize only high-signal facts." }); +// Agent role: merge shard summaries into one concise final summary. +const reduceScenario = agent({ model: "large", input: s.object({ scenario: s.string, shardSummaries: s.array(s.object({ shardLabel: s.string, summary: s.string, facts: s.array(s.string) })) }), output: s.object({ scenario: s.string, summary: s.string, savings: s.string }), instructions: "Deduplicate facts and keep the final summary short." }); +// Agent role: orchestrate deterministic search + parallel shard summarization for nine large-scale scenarios. +const summarizeAtScale = agent({ + model: "large", + output: s.object({ scenarios: s.array(s.object({ id: s.string, summary: s.string })), costNotes: s.array(s.string) }), + agents: { summarizeShard, reduceScenario }, + instructions: p`Cover: git diff, 24h repo changes, 24h exported/doc updates, semantic search (query->rg shards->shard summaries->final), plus 24h CI failures, auth/permission changes, dependency risk, API impact, and monorepo owner impact. Use ${p.bash("rg -n \"export|auth|permission|schema\" src docs || true")} and ${p.bash("git log --since='24 hours ago' --name-status --pretty=format:'%h %s'")} first, then parallelize shard work with Promises and use large-model reduction only once per scenario.`, +}); +export default summarizeAtScale; +``` diff --git a/skills/rig/samples/55-file-change-lint-middleware.md b/skills/rig/samples/55-file-change-lint-middleware.md new file mode 100644 index 0000000..4950c05 --- /dev/null +++ b/skills/rig/samples/55-file-change-lint-middleware.md @@ -0,0 +1,27 @@ +# 55 - File Change Lint Middleware + +```rig +import { agent, s, type AgentAddon } from "rig"; +import { $ } from "zx"; + +const fingerprint = async () => { + const { exitCode, stdout } = await $`git status --porcelain`.nothrow(); + return exitCode === 0 ? stdout.trim() : ""; +}; +const lintOnFileChange = (runLint: () => Promise): AgentAddon => async (_context, next) => { + const before = await fingerprint(); + await next(); + const after = await fingerprint(); + if (before !== after) await runLint(); +}; + +// Agent role: update files and run linting after file changes. +const fileChangeMiddleware = agent({ + model: "mini", + instructions: "Update files when needed, then summarize the change.", + output: s.object({ changed: s.boolean, summary: s.string }), + addons: lintOnFileChange(() => $`npm run typecheck`), +}); + +export default fileChangeMiddleware; +``` diff --git a/skills/rig/samples/55-genaiscript-glossary-port.md b/skills/rig/samples/55-genaiscript-glossary-port.md new file mode 100644 index 0000000..267c3b1 --- /dev/null +++ b/skills/rig/samples/55-genaiscript-glossary-port.md @@ -0,0 +1,12 @@ +# 55 - GenAIScript Glossary Port + +```rig +import { agent, p, s } from "rig"; +// Agent role: extract advanced rig glossary terms from docs. +const glossaryPort = agent({ + model: "mini", + instructions: p`Read ${p.read("README.md")} and ${p.read("skills/rig/SKILL.md")}. Return only advanced rig terms with short definitions and skip duplicates.`, + output: s.object({ terms: s.array(s.object({ term: s.string, definition: s.string })) }), +}); +export default glossaryPort; +``` diff --git a/skills/rig/samples/56-genaiscript-refactor-batch-port.md b/skills/rig/samples/56-genaiscript-refactor-batch-port.md new file mode 100644 index 0000000..60b9101 --- /dev/null +++ b/skills/rig/samples/56-genaiscript-refactor-batch-port.md @@ -0,0 +1,14 @@ +# 56 - GenAIScript Refactor Batch Port + +```rig +import { agent, p, s } from "rig"; +// Agent role: suggest metadata refactors for the first three sample files. +const refactorOne = agent({ model: "mini", instructions: "Suggest one minimal metadata cleanup for a rig sample.", output: s.object({ file: s.string, change: s.string }) }); +const refactorBatch = agent({ + model: "mini", + instructions: p`Inspect ${p.bash("find skills/rig/samples -maxdepth 1 -name '*.md' | sort | head -n 3")}. Use refactorOne when helpful and return only the smallest safe cleanup suggestions.`, + output: s.object({ suggestions: s.array(s.object({ file: s.string, change: s.string })) }), + agents: { refactorOne }, +}); +export default refactorBatch; +``` diff --git a/skills/rig/samples/57-genaiscript-issue-review-port.md b/skills/rig/samples/57-genaiscript-issue-review-port.md new file mode 100644 index 0000000..fd4f16b --- /dev/null +++ b/skills/rig/samples/57-genaiscript-issue-review-port.md @@ -0,0 +1,12 @@ +# 57 - GenAIScript Issue Review Port + +```rig +import { agent, s } from "rig"; +// Agent role: review a GitHub issue report and return concise feedback. +const issueReviewPort = agent({ + model: "mini", + instructions: "Review this issue draft: \"The CLI crashes sometimes when reading stdin programs on Windows. I expected it to work.\" Ask only for missing reproduction details and missing expected behavior.", + output: s.object({ summary: s.string, questions: s.array(s.string) }), +}); +export default issueReviewPort; +``` diff --git a/skills/rig/samples/58-genaiscript-travel-plan-port.md b/skills/rig/samples/58-genaiscript-travel-plan-port.md new file mode 100644 index 0000000..52156f7 --- /dev/null +++ b/skills/rig/samples/58-genaiscript-travel-plan-port.md @@ -0,0 +1,15 @@ +# 58 - GenAIScript Travel Plan Port + +```rig +import { agent, s } from "rig"; +// Agent role: merge local advice and language tips into one travel plan. +const localGuide = agent({ model: "mini", instructions: "Suggest authentic places and activities for a 3-day trip to Egypt.", output: s.object({ ideas: s.array(s.string) }) }); +const languageTips = agent({ model: "mini", instructions: "Suggest communication tips for a 3-day trip to Egypt.", output: s.object({ tips: s.array(s.string) }) }); +const travelPlanPort = agent({ + model: "mini", + instructions: "Plan a 3-day trip to Egypt. Use localGuide and languageTips when helpful, then return one integrated itinerary.", + output: s.object({ itinerary: s.array(s.string), tips: s.array(s.string) }), + agents: { localGuide, languageTips }, +}); +export default travelPlanPort; +``` diff --git a/skills/rig/samples/59-genaiscript-city-info-port.md b/skills/rig/samples/59-genaiscript-city-info-port.md new file mode 100644 index 0000000..3012fa0 --- /dev/null +++ b/skills/rig/samples/59-genaiscript-city-info-port.md @@ -0,0 +1,12 @@ +# 59 - GenAIScript City Info Port + +```rig +import { agent, s } from "rig"; +// Agent role: extract structured city facts from a short dataset. +const cityInfoPort = agent({ + model: "mini", + instructions: "From this dataset, return a JSON array of city facts only: Cairo | population 10.1M | https://en.wikipedia.org/wiki/Cairo ; Alexandria | population 5.6M | https://en.wikipedia.org/wiki/Alexandria ; Giza | population 4.8M | https://en.wikipedia.org/wiki/Giza.", + output: s.array(s.object({ name: s.string, population: s.number, url: s.string })), +}); +export default cityInfoPort; +``` diff --git a/skills/rig/samples/60-genaiscript-schema-cities-port.md b/skills/rig/samples/60-genaiscript-schema-cities-port.md new file mode 100644 index 0000000..b49dbcc --- /dev/null +++ b/skills/rig/samples/60-genaiscript-schema-cities-port.md @@ -0,0 +1,12 @@ +# 60 - GenAIScript Schema Cities Port + +```rig +import { agent, s } from "rig"; +// Agent role: invent five city records that match a strict schema. +const schemaCitiesPort = agent({ + model: "mini", + instructions: "Give 5 cities with their populations and Wikipedia URLs. Return only valid output.", + output: s.array(s.object({ name: s.string, population: s.number, url: s.string })), +}); +export default schemaCitiesPort; +``` diff --git a/skills/rig/samples/61-genaiscript-list-files-port.md b/skills/rig/samples/61-genaiscript-list-files-port.md new file mode 100644 index 0000000..a697efa --- /dev/null +++ b/skills/rig/samples/61-genaiscript-list-files-port.md @@ -0,0 +1,12 @@ +# 61 - GenAIScript List Files Port + +```rig +import { agent, p, s } from "rig"; +// Agent role: pick the most interesting rig sample files. +const listFilesPort = agent({ + model: "nano", + instructions: p`Review ${p.bash("find skills/rig/samples -maxdepth 1 -name '*.md' | sort")}. Select the 3 most interesting sample files.`, + output: s.object({ files: s.array(s.string) }), +}); +export default listFilesPort; +``` diff --git a/skills/rig/samples/62-genaiscript-todo-port.md b/skills/rig/samples/62-genaiscript-todo-port.md new file mode 100644 index 0000000..ca773bb --- /dev/null +++ b/skills/rig/samples/62-genaiscript-todo-port.md @@ -0,0 +1,12 @@ +# 62 - GenAIScript TODO Port + +```rig +import { agent, p, s } from "rig"; +// Agent role: propose minimal TODO implementations from the current workspace. +const todoPort = agent({ + model: "mini", + instructions: p`Check ${p.bash("rg -n 'TODO' src skills scripts . || true")}. For up to one actionable TODO, return the target file and the minimal implementation plan.`, + output: s.object({ todos: s.array(s.object({ file: s.string, plan: s.string })) }), +}); +export default todoPort; +``` diff --git a/skills/rig/samples/63-genaiscript-slide-deck-port.md b/skills/rig/samples/63-genaiscript-slide-deck-port.md new file mode 100644 index 0000000..5642d1d --- /dev/null +++ b/skills/rig/samples/63-genaiscript-slide-deck-port.md @@ -0,0 +1,12 @@ +# 63 - GenAIScript Slide Deck Port + +```rig +import { agent, p, s } from "rig"; +// Agent role: turn the README into a short slide deck outline. +const slideDeckPort = agent({ + model: "mini", + instructions: p`Read ${p.read("README.md")} and draft a short markdown slide deck outline with terse titles and short bullets.`, + output: s.object({ slides: s.array(s.object({ title: s.string, bullets: s.array(s.string) })) }), +}); +export default slideDeckPort; +``` diff --git a/skills/rig/samples/64-genaiscript-grumpy-review-port.md b/skills/rig/samples/64-genaiscript-grumpy-review-port.md new file mode 100644 index 0000000..4eb3668 --- /dev/null +++ b/skills/rig/samples/64-genaiscript-grumpy-review-port.md @@ -0,0 +1,12 @@ +# 64 - GenAIScript Grumpy Review Port + +```rig +import { agent, p, s } from "rig"; +// Agent role: review one rig sample in a terse critical voice. +const grumpyReviewPort = agent({ + model: "mini", + instructions: p`Review ${p.read("skills/rig/samples/36-subagent-delegation.md")} in a terse senior-engineer voice. Be specific, but keep each point short.`, + output: s.object({ findings: s.array(s.string), verdict: s.string }), +}); +export default grumpyReviewPort; +``` diff --git a/skills/rig/tsconfig.json b/skills/rig/tsconfig.json new file mode 100644 index 0000000..1dd454a --- /dev/null +++ b/skills/rig/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "allowImportingTsExtensions": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["./*.ts"] +} diff --git a/src/addons.ts b/src/addons.ts new file mode 100644 index 0000000..f2f15af --- /dev/null +++ b/src/addons.ts @@ -0,0 +1,2 @@ +export { addons, oncePerSession, repair, steering, timeout } from "../skills/rig/addons.ts"; +export type { SessionRegistration, SteeringOptions, TimeoutOptions } from "../skills/rig/addons.ts"; diff --git a/src/engines/copilot.test.ts b/src/engines/copilot.test.ts new file mode 100644 index 0000000..ff0485d --- /dev/null +++ b/src/engines/copilot.test.ts @@ -0,0 +1,91 @@ +import { beforeEach, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => { + const approveAll = vi.fn(); + const createSession = vi.fn(); + const forUri = vi.fn(() => ({ kind: "uri", url: "localhost:7777" })); + const forStdio = vi.fn(() => ({ kind: "stdio" })); + const copilotClientCtor = vi.fn(); + const CopilotClient = function (this: unknown, options: unknown) { + copilotClientCtor(options); + return { createSession }; + }; + return { approveAll, createSession, forUri, forStdio, copilotClientCtor, CopilotClient }; +}); + +vi.mock("@github/copilot-sdk", () => ({ + approveAll: mocks.approveAll, + CopilotClient: mocks.CopilotClient, + RuntimeConnection: { forUri: mocks.forUri, forStdio: mocks.forStdio }, +})); + +import { copilotEngine } from "rig"; + +beforeEach(() => { + mocks.createSession.mockReset(); + mocks.forUri.mockClear(); + mocks.forUri.mockImplementation(() => ({ kind: "uri", url: "localhost:7777" })); + mocks.forStdio.mockClear(); + mocks.forStdio.mockImplementation(() => ({ kind: "stdio" })); + mocks.copilotClientCtor.mockClear(); + delete process.env["COPILOT_SDK_URI"]; + vi.restoreAllMocks(); +}); + +it("uses a URI (HTTP) connection by default", async () => { + copilotEngine().createSession({ model: "gpt-5", streaming: false } as any); + + expect(mocks.forUri).toHaveBeenCalledWith("localhost:7777"); + expect(mocks.copilotClientCtor).toHaveBeenCalledWith({ connection: { kind: "uri", url: "localhost:7777" } }); + expect(mocks.createSession).toHaveBeenCalledWith({ model: "gpt-5", streaming: false }); +}); + +it("uses COPILOT_SDK_URI when set", async () => { + process.env["COPILOT_SDK_URI"] = "http://127.0.0.1:4141"; + mocks.forUri.mockImplementation(((url: string) => ({ kind: "uri", url })) as any); + + copilotEngine().createSession({ model: "gpt-5", streaming: false } as any); + + expect(mocks.forUri).toHaveBeenCalledWith("http://127.0.0.1:4141"); + expect(mocks.copilotClientCtor).toHaveBeenCalledWith({ connection: { kind: "uri", url: "http://127.0.0.1:4141" } }); +}); + +it("preserves explicit client options", async () => { + const connection = { kind: "uri", url: "127.0.0.1:8765" } as const; + + copilotEngine({ connection, workingDirectory: "/tmp/rig" }); + + expect(mocks.forUri).not.toHaveBeenCalled(); + expect(mocks.copilotClientCtor).toHaveBeenCalledWith({ + connection, + workingDirectory: "/tmp/rig", + }); +}); + +it("subscribes to all Copilot SDK events and logs JSONL to stderr", async () => { + const on = vi.fn((handler: (event: unknown) => void) => { + handler({ type: "session.idle", data: { done: true } }); + return () => {}; + }); + mocks.createSession.mockResolvedValue({ on, sendAndWait: vi.fn() }); + + await copilotEngine().createSession({ model: "gpt-4.1", streaming: false } as any); + + expect(mocks.copilotClientCtor).toHaveBeenCalledTimes(1); + expect(mocks.createSession).toHaveBeenCalledTimes(1); +}); + +it("creates and subscribes only once per engine session", async () => { + const client = copilotEngine(); + await client.createSession({ model: "gpt-4.1", streaming: false } as any); + await client.createSession({ model: "gpt-4.1", streaming: false } as any); + + expect(mocks.createSession).toHaveBeenCalledTimes(2); +}); + +it("uses a stdio connection when server option is true", async () => { + copilotEngine({ server: true }).createSession({ model: "gpt-4.1", streaming: false } as any); + expect(mocks.forStdio).toHaveBeenCalledOnce(); + expect(mocks.forUri).not.toHaveBeenCalled(); + expect(mocks.copilotClientCtor).toHaveBeenCalledWith({ connection: { kind: "stdio" } }); +}); diff --git a/src/launcher-default-engine.test.ts b/src/launcher-default-engine.test.ts new file mode 100644 index 0000000..0dcfb64 --- /dev/null +++ b/src/launcher-default-engine.test.ts @@ -0,0 +1,72 @@ +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => { + const approveAll = vi.fn(); + const createSession = vi.fn(); + const stopClient = vi.fn(async () => []); + const copilotClientCtor = vi.fn(); + const defaultForUri = () => ({ kind: "uri", url: "localhost:0" }); + const forUri = vi.fn(defaultForUri); + const CopilotClient = function (this: unknown, options: unknown) { + copilotClientCtor(options); + return { createSession, stop: stopClient }; + }; + return { approveAll, createSession, stopClient, copilotClientCtor, defaultForUri, forUri, CopilotClient }; +}); + +vi.mock("@github/copilot-sdk", () => ({ + approveAll: mocks.approveAll, + CopilotClient: mocks.CopilotClient, + RuntimeConnection: { forUri: mocks.forUri, forStdio: vi.fn() }, +})); + +import { agent, launchRigProgram, s } from "rig"; + +it("uses the launcher cwd when mounting the default copilot engine", async () => { + const sendAndWait = vi.fn().mockResolvedValue(JSON.stringify("default-mounted")); + mocks.createSession.mockResolvedValue({ sendAndWait }); + + const cwd = "/tmp/workspace/pelikhan/rig/src"; + const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.fixture.ts"); + + await launchRigProgram(fixturePath, { cwd }); + + const call = agent({ + name: "launcher-default-engine-test", + input: s.object({}), + }); + const result = await call({}); + expect(result).toBe("default-mounted"); + expect(mocks.copilotClientCtor).toHaveBeenCalledWith(expect.objectContaining({ workingDirectory: cwd })); +}); + +it("uses COPILOT_SDK_URI when mounting the default copilot engine", async () => { + const sendAndWait = vi.fn().mockResolvedValue(JSON.stringify("env-mounted")); + mocks.createSession.mockResolvedValue({ sendAndWait }); + process.env["COPILOT_SDK_URI"] = "http://127.0.0.1:4141"; + mocks.forUri.mockImplementation(((url: string) => ({ kind: "uri", url })) as any); + + const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.fixture.ts"); + + try { + await launchRigProgram(fixturePath); + + const call = agent({ + name: "launcher-default-engine-uri-test", + input: s.object({}), + }); + const result = await call({}); + expect(result).toBe("env-mounted"); + expect(mocks.forUri).toHaveBeenCalledWith("http://127.0.0.1:4141"); + expect(mocks.copilotClientCtor).toHaveBeenCalledWith( + expect.objectContaining({ + connection: { kind: "uri", url: "http://127.0.0.1:4141" }, + }), + ); + } finally { + delete process.env["COPILOT_SDK_URI"]; + mocks.forUri.mockImplementation(mocks.defaultForUri); + } +}); diff --git a/src/launcher.fixture.ts b/src/launcher.fixture.ts new file mode 100644 index 0000000..b7eb193 --- /dev/null +++ b/src/launcher.fixture.ts @@ -0,0 +1,2 @@ +(globalThis as { __launcherLoaded?: number }).__launcherLoaded = + ((globalThis as { __launcherLoaded?: number }).__launcherLoaded ?? 0) + 1; diff --git a/src/launcher.stdin-default-prompt.fixture.ts b/src/launcher.stdin-default-prompt.fixture.ts new file mode 100644 index 0000000..d1de4c7 --- /dev/null +++ b/src/launcher.stdin-default-prompt.fixture.ts @@ -0,0 +1,3 @@ +import { p } from "rig"; + +export default p`Review ${p.read("README.md")} and summarize key points.`; diff --git a/src/launcher.stdin-default-string.fixture.ts b/src/launcher.stdin-default-string.fixture.ts new file mode 100644 index 0000000..ff1cde8 --- /dev/null +++ b/src/launcher.stdin-default-string.fixture.ts @@ -0,0 +1 @@ +export default "Summarize the provided input."; diff --git a/src/launcher.stdin-json.fixture.ts b/src/launcher.stdin-json.fixture.ts new file mode 100644 index 0000000..ff0a6a6 --- /dev/null +++ b/src/launcher.stdin-json.fixture.ts @@ -0,0 +1,9 @@ +import { agent, s } from "rig"; + +const root = agent({ + name: "launcher-stdin-json-root", + input: s.object({ message: s.string }), + output: s.object({ ok: s.boolean }), +}); + +export default root; diff --git a/src/launcher.stdin-named-root.fixture.ts b/src/launcher.stdin-named-root.fixture.ts new file mode 100644 index 0000000..dbcda3a --- /dev/null +++ b/src/launcher.stdin-named-root.fixture.ts @@ -0,0 +1,5 @@ +import { agent } from "rig"; + +export const root = agent({ + name: "launcher-stdin-named-root", +}); diff --git a/src/launcher.stdin-string.fixture.ts b/src/launcher.stdin-string.fixture.ts new file mode 100644 index 0000000..d0a09ed --- /dev/null +++ b/src/launcher.stdin-string.fixture.ts @@ -0,0 +1,7 @@ +import { agent } from "rig"; + +const root = agent({ + name: "launcher-stdin-string-root", +}); + +export default root; diff --git a/src/launcher.stdin.fixture.ts b/src/launcher.stdin.fixture.ts new file mode 100644 index 0000000..f54178c --- /dev/null +++ b/src/launcher.stdin.fixture.ts @@ -0,0 +1,7 @@ +import { agent } from "rig"; + +const root = agent({ + name: "launcher-stdin-root", +}); + +export default root; diff --git a/src/launcher.test.ts b/src/launcher.test.ts new file mode 100644 index 0000000..fc38d1f --- /dev/null +++ b/src/launcher.test.ts @@ -0,0 +1,477 @@ +import { beforeEach, expect, it, vi } from "vitest"; +import { resolve, dirname } from "node:path"; +import { Readable, Writable } from "node:stream"; +import { fileURLToPath } from "node:url"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; + +const mocks = vi.hoisted(() => { + const approveAll = vi.fn(); + let sendAndWaitImpl: () => unknown | Promise = async () => JSON.stringify("done"); + const disconnectSession = vi.fn(async () => {}); + const stopClient = vi.fn(async () => []); + const createSession = vi.fn(async () => ({ + sendAndWait: async () => { + const response = await sendAndWaitImpl(); + return typeof response === "string" ? response : JSON.stringify(response); + }, + disconnect: disconnectSession, + })); + const forUri = vi.fn(() => ({ kind: "uri", url: "localhost:7777" })); + const forStdio = vi.fn(() => ({ kind: "stdio" })); + const copilotClientCtor = vi.fn(); + const CopilotClient = function (this: unknown, options: unknown) { + copilotClientCtor(options); + return { createSession, stop: stopClient }; + }; + const setSendAndWaitImpl = (impl: () => unknown | Promise) => { + sendAndWaitImpl = impl; + }; + return { approveAll, createSession, disconnectSession, stopClient, forUri, forStdio, copilotClientCtor, CopilotClient, setSendAndWaitImpl }; +}); + +vi.mock("@github/copilot-sdk", () => ({ + approveAll: mocks.approveAll, + CopilotClient: mocks.CopilotClient, + RuntimeConnection: { forUri: mocks.forUri, forStdio: mocks.forStdio }, +})); + +import { agent, s } from "rig"; +import { launchRigProgram, runLauncherCli } from "rig"; + +beforeEach(() => { + mocks.createSession.mockClear(); + mocks.forUri.mockClear(); + mocks.forStdio.mockClear(); + mocks.copilotClientCtor.mockClear(); + mocks.disconnectSession.mockClear(); + mocks.stopClient.mockClear(); + mocks.setSendAndWaitImpl(async () => JSON.stringify("done")); +}); + +async function runCliAndCaptureStdout(argv: string[], stdinChunks: string[] = [""]): Promise { + const stdin = Readable.from(stdinChunks); + const output: string[] = []; + const stdout = new Writable({ + write(chunk, _encoding, callback) { + output.push(chunk.toString()); + callback(); + }, + }); + await runLauncherCli(argv, {}, { stdin, stdout }); + return output.join(""); +} + +it("loads a rig program and mounts a copilot client", async () => { + const __dirname = dirname(fileURLToPath(import.meta.url)); + const globalState = globalThis as { __launcherLoaded?: number }; + const before = globalState.__launcherLoaded ?? 0; + const fixturePath = resolve(__dirname, "./launcher.fixture.ts"); + + mocks.setSendAndWaitImpl(async () => JSON.stringify("mounted")); + await launchRigProgram(fixturePath); + + expect(globalState.__launcherLoaded).toBe(before + 1); + + const call = agent({ + name: "launcher-test", + input: s.object({}), + }); + const result = await call({}); + expect(result).toBe("mounted"); +}); + +it("uses stdin mode by default and writes the final answer to stdout", async () => { + const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.stdin.fixture.ts"); + const stdin = Readable.from(["Review this patch"]); + const output: string[] = []; + const stdout = new Writable({ + write(chunk, _encoding, callback) { + output.push(chunk.toString()); + callback(); + }, + }); + + await runLauncherCli([fixturePath], {}, { stdin, stdout }); + + expect(output.join("")).toBe("done"); +}); + +it("supports stdin mode for string input/output root agents", async () => { + const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.stdin-string.fixture.ts"); + const stdin = Readable.from(["Review this patch"]); + const output: string[] = []; + const stdout = new Writable({ + write(chunk, _encoding, callback) { + output.push(chunk.toString()); + callback(); + }, + }); + + mocks.setSendAndWaitImpl(async () => JSON.stringify("done")); + await runLauncherCli([fixturePath], {}, { stdin, stdout }); + + expect(output.join("")).toBe("done"); +}); + +it("supports stdin mode when root default export is a string", async () => { + const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.stdin-default-string.fixture.ts"); + const stdin = Readable.from(["Review this patch"]); + const output: string[] = []; + const stdout = new Writable({ + write(chunk, _encoding, callback) { + output.push(chunk.toString()); + callback(); + }, + }); + + mocks.setSendAndWaitImpl(async () => JSON.stringify("done")); + await runLauncherCli([fixturePath], {}, { stdin, stdout }); + + expect(output.join("")).toBe("done"); +}); + +it("supports stdin mode when root default export is a prompt builder", async () => { + const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.stdin-default-prompt.fixture.ts"); + const stdin = Readable.from(["Review this patch"]); + const output: string[] = []; + const stdout = new Writable({ + write(chunk, _encoding, callback) { + output.push(chunk.toString()); + callback(); + }, + }); + + mocks.setSendAndWaitImpl(async () => JSON.stringify("done")); + await runLauncherCli([fixturePath], {}, { stdin, stdout }); + + expect(output.join("")).toBe("done"); +}); + +it("supports stdin mode for JSON input and JSON stdout output", async () => { + const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.stdin-json.fixture.ts"); + const stdin = Readable.from(["{\"message\":\"hello\"}"]); + const output: string[] = []; + const stdout = new Writable({ + write(chunk, _encoding, callback) { + output.push(chunk.toString()); + callback(); + }, + }); + + mocks.setSendAndWaitImpl(async () => ({ ok: true })); + await runLauncherCli([fixturePath], {}, { stdin, stdout }); + + expect(output.join("")).toBe("{\"ok\":true}"); +}); + +it("rejects stdin mode when root agent expects JSON input but stdin is not JSON", async () => { + const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.stdin-json.fixture.ts"); + const stdin = Readable.from(["not-json"]); + const stdout = new Writable({ + write(_chunk, _encoding, callback) { + callback(); + }, + }); + + await expect( + runLauncherCli([fixturePath], {}, { stdin, stdout }), + ).rejects.toThrow("Expected stdin to contain JSON for the root agent input schema."); +}); + +it("requires stdin-mode root agent to be a default export", async () => { + const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.stdin-named-root.fixture.ts"); + const stdin = Readable.from(["Review this patch"]); + const stdout = new Writable({ + write(_chunk, _encoding, callback) { + callback(); + }, + }); + + await expect( + runLauncherCli([fixturePath], {}, { stdin, stdout }), + ).rejects.toThrow("Expected program to export a root value (agent, string, or prompt builder) as default export."); +}); + +it("rejects stdin mode when prompt is empty", async () => { + const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.stdin.fixture.ts"); + const stdin = Readable.from([" \n\t"]); + const stdout = new Writable({ + write(_chunk, _encoding, callback) { + callback(); + }, + }); + await expect( + runLauncherCli([fixturePath], {}, { stdin, stdout }), + ).rejects.toThrow(//); +}); + +it("rejects unknown cli arguments", async () => { + const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.fixture.ts"); + + await expect(runLauncherCli([fixturePath, "--file"])).rejects.toThrow( + //, + ); +}); + +it("prints launcher help for common help invocations", async () => { + for (const argv of [["--help"], ["-h"], ["help"], ["/help"], ["/?"]]) { + const output = await runCliAndCaptureStdout(argv); + expect(output).toContain("Usage:"); + expect(output).toContain("[]"); + } + expect(mocks.createSession).not.toHaveBeenCalled(); +}); + +it("accepts --server flag without rejecting", async () => { + const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.stdin.fixture.ts"); + const stdin = Readable.from(["Review this patch"]); + const output: string[] = []; + const stdout = new Writable({ + write(chunk, _encoding, callback) { + output.push(chunk.toString()); + callback(); + }, + }); + + await runLauncherCli([fixturePath, "--server"], {}, { stdin, stdout }); + + expect(output.join("")).toBe("done"); + expect(mocks.forStdio).toHaveBeenCalled(); +}); + +it("accepts --typecheck flag without rejecting", async () => { + const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.stdin.fixture.ts"); + const stdin = Readable.from(["Review this patch"]); + const output: string[] = []; + const stdout = new Writable({ + write(chunk, _encoding, callback) { + output.push(chunk.toString()); + callback(); + }, + }); + + await runLauncherCli([fixturePath, "--typecheck"], {}, { stdin, stdout }); + + expect(output.join("")).toBe("done"); +}); + +it("falls back to the skill tsconfig when cwd tsconfig is missing", async () => { + const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.stdin.fixture.ts"); + const skillDirCwd = resolve(dirname(fileURLToPath(import.meta.url)), "../skills/rig"); + const stdin = Readable.from(["Review this patch"]); + const output: string[] = []; + const stdout = new Writable({ + write(chunk, _encoding, callback) { + output.push(chunk.toString()); + callback(); + }, + }); + + await runLauncherCli([fixturePath, "--typecheck"], { cwd: skillDirCwd }, { stdin, stdout }); + + expect(output.join("")).toBe("done"); +}); + +it("rejects --typecheck when inline program fails typecheck", async () => { + const stdin = Readable.from([` +const root = agent({ + name: "launcher-stdin-program", + instructions: 42, +}); +export default root; +`]); + const stdout = new Writable({ + write(_chunk, _encoding, callback) { + callback(); + }, + }); + + await expect(runLauncherCli(["--typecheck"], {}, { stdin, stdout })).rejects.toThrow( + /Typecheck failed/, + ); +}); + +it("rejects --typecheck when program file fails typecheck before execution", async () => { + const tempDir = await mkdtemp(resolve(tmpdir(), "rig-launcher-test-")); + const fixturePath = resolve(tempDir, "typecheck-fail.ts"); + await writeFile( + fixturePath, + ` +import { agent, s } from "rig"; +const shouldBeString: string = 42; +void shouldBeString; +const root = agent({ + name: "launcher-typecheck-fail", +}); +export default root; +`, + "utf8", + ); + try { + const stdin = Readable.from(["Review this patch"]); + const stdout = new Writable({ + write(_chunk, _encoding, callback) { + callback(); + }, + }); + + await expect(runLauncherCli([fixturePath, "--typecheck"], {}, { stdin, stdout })).rejects.toThrow( + /Typecheck failed/, + ); + expect(mocks.createSession).not.toHaveBeenCalled(); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } +}); + +it("runs an inlined stdin program by invoking the default no-input root agent", async () => { + const stdin = Readable.from([` +const root = agent({ + name: "launcher-stdin-program", + instructions: "Write a short note.", +}); +export default root; +`]); + const output: string[] = []; + const stdout = new Writable({ + write(chunk, _encoding, callback) { + output.push(chunk.toString()); + callback(); + }, + }); + + await runLauncherCli([], {}, { stdin, stdout }); + + expect(output.join("")).toBe("done"); +}); + +it("runs an inlined stdin program by defaulting to the first agent assignment", async () => { + const stdin = Readable.from([` +const reviewer = agent({ + name: "launcher-stdin-program", + instructions: "Write a short note.", +}); +`]); + const output: string[] = []; + const stdout = new Writable({ + write(chunk, _encoding, callback) { + output.push(chunk.toString()); + callback(); + }, + }); + + await runLauncherCli([], {}, { stdin, stdout }); + + expect(output.join("")).toBe("done"); +}); + +it("runs an inlined stdin program when default export is a string", async () => { + const stdin = Readable.from([` +export default "Write a short note."; +`]); + const output: string[] = []; + const stdout = new Writable({ + write(chunk, _encoding, callback) { + output.push(chunk.toString()); + callback(); + }, + }); + + await runLauncherCli([], {}, { stdin, stdout }); + + expect(output.join("")).toBe("done"); +}); + +it("runs an inlined stdin program when default export is a prompt builder", async () => { + const stdin = Readable.from([` +export default p\`Write a short note about \${p.read("README.md")}.\`; +`]); + const output: string[] = []; + const stdout = new Writable({ + write(chunk, _encoding, callback) { + output.push(chunk.toString()); + callback(); + }, + }); + + await runLauncherCli([], {}, { stdin, stdout }); + + expect(output.join("")).toBe("done"); +}); + +it("rejects an inlined stdin program when the root agent requires input", async () => { + const stdin = Readable.from([` +const root = agent({ + name: "launcher-stdin-program-with-input", + input: s.object({ message: s.string }), +}); +export default root; +`]); + const stdout = new Writable({ + write(_chunk, _encoding, callback) { + callback(); + }, + }); + + await expect(runLauncherCli([], {}, { stdin, stdout })).rejects.toThrow( + "Expected stdin program root agent to have no input (omit input or use input: s.object({})).", + ); +}); + +it("uses the first agent assignment as inline stdin root when no default export exists", async () => { + const stdin = Readable.from([` +const first = agent({ + name: "launcher-stdin-program-with-input", + input: s.object({ message: s.string }), +}); +const second = agent({ + name: "launcher-stdin-program-no-input", +}); +`]); + const stdout = new Writable({ + write(_chunk, _encoding, callback) { + callback(); + }, + }); + + await expect(runLauncherCli([], {}, { stdin, stdout })).rejects.toThrow( + "Expected stdin program root agent to have no input (omit input or use input: s.object({})).", + ); +}); + +it("uses the first no-input agent assignment as inline stdin root when no default export exists", async () => { + const stdin = Readable.from([` +const first = agent({ + name: "launcher-stdin-program-no-input", +}); +const second = agent({ + name: "launcher-stdin-program-with-input", + input: s.object({ message: s.string }), +}); +`]); + const output: string[] = []; + const stdout = new Writable({ + write(chunk, _encoding, callback) { + output.push(chunk.toString()); + callback(); + }, + }); + + await runLauncherCli([], {}, { stdin, stdout }); + + expect(output.join("")).toBe("done"); +}); + +it("requires a non-empty stdin program when no program path is provided", async () => { + const stdin = Readable.from([" \n\t"]); + const stdout = new Writable({ + write(_chunk, _encoding, callback) { + callback(); + }, + }); + + await expect(runLauncherCli([], {}, { stdin, stdout })).rejects.toThrow( + //, + ); +}); diff --git a/src/rig.test.ts b/src/rig.test.ts new file mode 100644 index 0000000..1adacf4 --- /dev/null +++ b/src/rig.test.ts @@ -0,0 +1,1028 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => { + let sendAndWaitImpl: (request: { prompt: string; signal?: AbortSignal }) => unknown | Promise = async () => JSON.stringify("default"); + let onImpl: ((handler: (event: unknown) => void) => void) | undefined; + const approveAll = vi.fn(); + const disconnectSession = vi.fn(async () => {}); + const stopClient = vi.fn(async () => []); + + const createSession = vi.fn(async () => ({ + on: onImpl ? ((handler: (event: unknown) => void) => { + onImpl?.(handler); + return () => {}; + }) : undefined, + sendAndWait: async (request: { prompt: string; signal?: AbortSignal }) => { + const response = await sendAndWaitImpl(request); + return typeof response === "string" ? response : JSON.stringify(response); + }, + disconnect: disconnectSession, + })); + const forUri = vi.fn(() => ({ kind: "uri", url: "localhost:7777" })); + const forStdio = vi.fn(() => ({ kind: "stdio" })); + const sdkDefineTool = vi.fn((name: string, config: Record) => ({ name, ...config })); + const copilotClientCtor = vi.fn(); + const CopilotClient = function (this: unknown, options: unknown) { + copilotClientCtor(options); + return { createSession, stop: stopClient }; + }; + const setSendAndWaitImpl = (impl: (request: { prompt: string; signal?: AbortSignal }) => unknown | Promise) => { + sendAndWaitImpl = impl; + }; + const setOnImpl = (impl?: (handler: (event: unknown) => void) => void) => { + onImpl = impl; + }; + return { + approveAll, + createSession, + disconnectSession, + stopClient, + forUri, + forStdio, + sdkDefineTool, + copilotClientCtor, + CopilotClient, + setSendAndWaitImpl, + setOnImpl, + }; +}); + +vi.mock("@github/copilot-sdk", () => ({ + approveAll: mocks.approveAll, + CopilotClient: mocks.CopilotClient, + RuntimeConnection: { forUri: mocks.forUri, forStdio: mocks.forStdio }, + defineTool: mocks.sdkDefineTool, +})); + +import { AgentError, PromptBuilder, agent, defineTool, p, s, toJsonSchema } from "rig"; +import { oncePerSession, repair, steering, timeout } from "rig/addons"; + +beforeEach(() => { + mocks.createSession.mockClear(); + mocks.approveAll.mockClear(); + mocks.forUri.mockClear(); + mocks.forStdio.mockClear(); + mocks.sdkDefineTool.mockClear(); + mocks.copilotClientCtor.mockClear(); + mocks.disconnectSession.mockClear(); + mocks.stopClient.mockClear(); + mocks.setOnImpl(undefined); + mocks.setSendAndWaitImpl(async () => JSON.stringify("default")); + vi.restoreAllMocks(); +}); + +describe("agent", () => { + it("creates an agent from a structured spec", () => { + const classify = agent({ + name: "classify", + instructions: "Classify the issue.", + input: s.object({ + title: s.string, + body: s.string, + }), + output: s.object({ + label: s.enum("bug", "feature", "question", "docs"), + confidence: s.enum("low", "medium", "high"), + }), + }); + + expect(classify.agentName).toBe("classify"); + expect(classify.inputSchema).toEqual(s.object({ title: s.string, body: s.string })); + expect(classify.outputSchema).toEqual(s.object({ + label: s.enum("bug", "feature", "question", "docs"), + confidence: s.enum("low", "medium", "high"), + })); + }); + + it("preserves type inference for schema helpers", async () => { + mocks.setSendAndWaitImpl(async () => ({ + summary: "Looks good", + risk: "low", + findings: [{ file: "src/index.ts", message: "Check edge case" }], + })); + + const review = agent({ + name: "review", + input: s.object({ diff: s.string }), + output: s.object({ + summary: s.string, + risk: s.enum("low", "medium", "high"), + findings: s.array(s.object({ + file: s.string, + line: s.optional(s.number), + message: s.string, + })), + }), + }); + + type Review = Awaited>; + const risk: Review["risk"] = "low"; + const line: Review["findings"][number]["line"] = undefined; + + const result = await review({ diff: "..." }); + expect(risk).toBe("low"); + expect(line).toBeUndefined(); + expect(result.risk).toBe("low"); + expect(result.findings[0]?.line).toBeUndefined(); + }); + + it("defaults omitted agent names", async () => { + mocks.setSendAndWaitImpl(async () => JSON.stringify("ok")); + const unnamed = agent({}); + + expect(unnamed.agentName).toBe("agent"); + await expect(unnamed("hello")).resolves.toBe("ok"); + }); + + it("rejects implicit schema syntax at runtime", () => { + expect(() => agent({ + name: "implicit-top-level", + input: { text: "go" } as any, + })).toThrow(/Use declarative s\.\* schema helpers/); + + expect(() => agent({ + name: "implicit-nested", + input: s.object({ text: "go" as any }), + })).toThrow(/input\.text/); + }); + + it("does not expose deprecated hook APIs in core", () => { + expect((agent as { on?: unknown }).on).toBeUndefined(); + expect((agent as { use?: unknown }).use).toBeUndefined(); + }); + + it("does not expose lifecycle subscription APIs on agents", () => { + const myAgent = agent({ name: "test-agent" }) as { subscribe?: unknown }; + expect(myAgent.subscribe).toBeUndefined(); + }); + + it("defaults omitted input and output schemas to string", () => { + const textAgent = agent({ name: "text-agent" }); + expect(textAgent.inputSchema).toEqual(s.string); + expect(textAgent.outputSchema).toEqual(s.string); + }); + + it("uses empty strings for omitted default string inputs", async () => { + const prompts: string[] = []; + mocks.setSendAndWaitImpl(async ({ prompt }) => { + prompts.push(prompt); + return JSON.stringify("ok"); + }); + + const textAgent = agent({ name: "text-agent" }); + await expect((textAgent as (input?: string) => Promise)()).resolves.toBe("ok"); + expect(prompts[0]).toContain("\n{\n \"type\": \"string\"\n}\n"); + expect(prompts[0]).toContain("\n\"\"\n"); + expect(prompts[0]).toContain("Return exactly one JSON value."); + }); +}); + +describe("agent invocation", () => { + it("calls the copilot sdk and returns validated data", async () => { + mocks.setSendAndWaitImpl(async () => ({ text: "hello world" })); + const greet = agent({ + name: "greeter", + input: s.object({ text: s.string }), + output: s.object({ text: s.string }), + }); + + await expect(greet({ text: "Hi" })).resolves.toEqual({ text: "hello world" }); + expect(mocks.disconnectSession).toHaveBeenCalledTimes(1); + expect(mocks.stopClient).toHaveBeenCalledTimes(1); + }); + + it("closes the session and client when a call fails", async () => { + mocks.setSendAndWaitImpl(async () => { + throw new Error("boom"); + }); + const greet = agent({ + name: "greeter", + input: s.object({ text: s.string }), + output: s.object({ text: s.string }), + }); + + await expect(greet({ text: "Hi" })).rejects.toThrow("boom"); + expect(mocks.disconnectSession).toHaveBeenCalledTimes(1); + expect(mocks.stopClient).toHaveBeenCalledTimes(1); + }); + + it("reuses one client for nested agent invocations while creating model-specific sessions", async () => { + const child = agent({ + name: "child", + model: "o3-mini", + input: s.object({ text: s.string }), + output: s.object({ text: s.string }), + }); + const parent = agent({ + name: "parent", + model: "gpt-4.1", + input: s.object({ text: s.string }), + output: s.object({ text: s.string }), + }); + + mocks.setSendAndWaitImpl(async ({ prompt }) => { + if (prompt.includes('"text": "parent"')) { + await child({ text: "child" }); + return { text: "parent-ok" }; + } + return { text: "child-ok" }; + }); + + await expect(parent({ text: "parent" })).resolves.toEqual({ text: "parent-ok" }); + expect(mocks.copilotClientCtor).toHaveBeenCalledTimes(1); + expect(mocks.createSession).toHaveBeenCalledTimes(2); + expect(mocks.createSession.mock.calls).toEqual([ + [{ model: "gpt-4.1", streaming: false, onPermissionRequest: mocks.approveAll }], + [{ model: "o3-mini", streaming: false, onPermissionRequest: mocks.approveAll }], + ]); + expect(mocks.disconnectSession).toHaveBeenCalledTimes(2); + expect(mocks.stopClient).toHaveBeenCalledTimes(1); + }); + + it("logs raw Copilot SDK events and rig ask events as JSONL", async () => { + const stderr = vi.spyOn(process.stderr, "write").mockImplementation(() => true as any); + mocks.setOnImpl((handler) => { + handler({ type: "session.idle", data: { done: true } }); + }); + mocks.setSendAndWaitImpl(async () => ({ text: "hello world" })); + + const greet = agent({ + name: "greeter", + input: s.object({ text: s.string }), + output: s.object({ text: s.string }), + }); + + await expect(greet({ text: "Hi" })).resolves.toEqual({ text: "hello world" }); + + const logs = stderr.mock.calls.map(([chunk]) => JSON.parse(String(chunk).trim())); + expect(logs).toHaveLength(2); + expect(logs[0]).toEqual({ type: "session.idle", data: { done: true } }); + expect(logs[1]).toMatchObject({ + type: "rig.copilot-ask", + data: { prompt: expect.stringContaining("Hi") }, + }); + }); + + it("exposes the Copilot session through an addon", async () => { + const addon = vi.fn(async (context, next) => { + await next(); + expect(context.session).toMatchObject({ + sendAndWait: expect.any(Function), + disconnect: expect.any(Function), + }); + }); + mocks.setSendAndWaitImpl(async () => ({ text: "hello world" })); + + const greet = agent({ + name: "greeter", + input: s.object({ text: s.string }), + output: s.object({ text: s.string }), + addons: addon, + }); + + await expect(greet({ text: "Hi" })).resolves.toEqual({ text: "hello world" }); + expect(addon).toHaveBeenCalledTimes(1); + }); + + it("applies default addons from agent spec", async () => { + const addon = vi.fn(async (_context, next) => { + await next(); + }); + mocks.setSendAndWaitImpl(async () => ({ text: "hello world" })); + + const greet = agent({ + name: "greeter", + input: s.object({ text: s.string }), + output: s.object({ text: s.string }), + addons: addon, + }); + + await expect(greet({ text: "Hi" })).resolves.toEqual({ text: "hello world" }); + expect(addon).toHaveBeenCalledTimes(1); + }); + + it("supports express-like addon registration with use()", async () => { + const order: number[] = []; + const first = vi.fn(async (_context, next) => { + order.push(1); + await next(); + }); + const second = vi.fn(async (_context, next) => { + order.push(2); + await next(); + }); + mocks.setSendAndWaitImpl(async () => ({ text: "hello world" })); + + const greet = agent({ + name: "greeter", + input: s.object({ text: s.string }), + output: s.object({ text: s.string }), + }); + + expect(greet.use(first).use(second)).toBe(greet); + await expect(greet({ text: "Hi" })).resolves.toEqual({ text: "hello world" }); + expect(order).toEqual([1, 2]); + expect(first).toHaveBeenCalledTimes(1); + expect(second).toHaveBeenCalledTimes(1); + }); + + it("validates addons passed to use()", () => { + const greet = agent({ + name: "greeter", + input: s.object({ text: s.string }), + output: s.object({ text: s.string }), + }); + + expect(() => greet.use([null as unknown as any] as any)).toThrow( + "Agent addon entries must be functions.", + ); + }); + + it("disconnects the session when an addon throws", async () => { + const addon = vi.fn(() => { + throw new Error("hook failed"); + }); + const greet = agent({ + name: "greeter", + input: s.object({ text: s.string }), + output: s.object({ text: s.string }), + addons: addon, + }); + + await expect(greet({ text: "Hi" })).rejects.toThrow("hook failed"); + expect(mocks.disconnectSession).toHaveBeenCalledTimes(1); + }); + + it("starts with no repair addon by default", async () => { + mocks.setSendAndWaitImpl(async () => "not json"); + + const strict = agent({ + name: "strict", + maxTurns: 2, + }); + + await expect(strict("go")).rejects.toBeInstanceOf(AgentError); + await expect(strict("go")).rejects.toMatchObject({ kind: "parse", turn: 1 }); + }); + + it("retries invalid JSON with the repair addon", async () => { + const prompts: string[] = []; + let calls = 0; + + mocks.setSendAndWaitImpl(async ({ prompt }) => { + prompts.push(prompt); + calls += 1; + return calls === 1 ? "not json" : JSON.stringify("repaired"); + }); + + const repairable = agent({ + name: "repairable", + addons: repair, + maxTurns: 2, + }); + + await expect(repairable("go")).resolves.toBe("repaired"); + expect(prompts).toHaveLength(2); + expect(prompts[1]).toContain(" { + mocks.setSendAndWaitImpl(async () => "```json\n\"hello\"\n```"); + + const reviewer = agent({ name: "reviewer" }); + + await expect(reviewer("go")).resolves.toBe("hello"); + }); + + it("parses a JSON object embedded in surrounding text", async () => { + mocks.setSendAndWaitImpl(async () => 'Here you go:\n{"text":"hello"}\nThanks!'); + + const reviewer = agent({ + name: "reviewer", + output: s.object({ text: s.string }), + }); + + await expect(reviewer("go")).resolves.toEqual({ text: "hello" }); + }); + + it("retries validation failures with addon-customized repair prompts", async () => { + const prompts: string[] = []; + let calls = 0; + + mocks.setSendAndWaitImpl(async ({ prompt }) => { + prompts.push(prompt); + calls += 1; + return calls === 1 ? { wrong: true } : JSON.stringify("fixed"); + }); + + const repairable = agent({ + name: "repairable", + addons: [ + async (context, next) => { + await next(); + if (context.nextPrompt) { + context.nextPrompt = `please fix: ${context.nextPrompt}`; + } + }, + repair, + ], + maxTurns: 2, + }); + + await expect(repairable("go")).resolves.toBe("fixed"); + expect(prompts[1]).toContain("please fix"); + }); + + it("throws AgentError after the final invalid turn", async () => { + mocks.setSendAndWaitImpl(async () => "not json"); + + const strict = agent({ + name: "strict", + maxTurns: 1, + }); + + await expect(strict("go")).rejects.toBeInstanceOf(AgentError); + await expect(strict("go")).rejects.toMatchObject({ kind: "parse" }); + }); + + it("supports per-call model overrides", async () => { + mocks.setSendAndWaitImpl(async () => JSON.stringify("ok")); + + const call = agent({ name: "model-test", model: "gpt-4.1" }); + await call("x", { model: "o3-mini" }); + + expect(mocks.createSession).toHaveBeenCalledWith({ model: "o3-mini", streaming: false, onPermissionRequest: mocks.approveAll }); + }); + + it("passes systemMessage to the session when specified", async () => { + mocks.setSendAndWaitImpl(async () => JSON.stringify("ok")); + + const systemMessage = { content: "You are a helpful assistant." }; + const call = agent({ name: "sys-msg-test", systemMessage }); + await call("x"); + + expect(mocks.createSession).toHaveBeenCalledWith({ + model: "gpt-4.1", + streaming: false, + onPermissionRequest: mocks.approveAll, + systemMessage, + }); + }); + + it("does not pass systemMessage when not specified", async () => { + mocks.setSendAndWaitImpl(async () => JSON.stringify("ok")); + + const call = agent({ name: "no-sys-msg-test" }); + await call("x"); + + expect(mocks.createSession).toHaveBeenCalledWith({ model: "gpt-4.1", streaming: false, onPermissionRequest: mocks.approveAll }); + }); + + it("defines tools with rig schemas using the Copilot SDK helper shape", () => { + const handler = vi.fn(async ({ issue }: { issue: string }) => `Issue ${issue}`); + const lookupIssue = defineTool("lookup_issue", { + description: "Look up an issue by id.", + parameters: s.object({ issue: s.string }), + handler, + }); + + expect(mocks.sdkDefineTool).toHaveBeenCalledWith("lookup_issue", { + description: "Look up an issue by id.", + parameters: toJsonSchema(s.object({ issue: s.string })), + handler, + skipPermission: true, + }); + expect(lookupIssue).toMatchObject({ + name: "lookup_issue", + description: "Look up an issue by id.", + parameters: toJsonSchema(s.object({ issue: s.string })), + handler, + skipPermission: true, + }); + }); + + it("preserves explicit tool permission overrides", () => { + defineTool("lookup_issue", { + skipPermission: false, + }); + + expect(mocks.sdkDefineTool).toHaveBeenCalledWith("lookup_issue", { + skipPermission: false, + }); + }); + + it("passes tools to the session and normalizes rig schemas", async () => { + mocks.setSendAndWaitImpl(async () => JSON.stringify("ok")); + + const call = agent({ + name: "tool-test", + tools: [{ + name: "lookup_issue", + description: "Look up an issue by id.", + parameters: s.object({ issue: s.string }), + handler: async ({ issue }: { issue: string }) => `Issue ${issue}`, + }], + }); + await call("x"); + + expect(mocks.createSession).toHaveBeenCalledWith({ + model: "gpt-4.1", + onPermissionRequest: mocks.approveAll, + streaming: false, + tools: [expect.objectContaining({ + name: "lookup_issue", + description: "Look up an issue by id.", + parameters: toJsonSchema(s.object({ issue: s.string })), + handler: expect.any(Function), + skipPermission: true, + })], + }); + }); + + it("supports timeout and abort signals", async () => { + mocks.setSendAndWaitImpl(async ({ signal }) => { + await new Promise((_, reject) => { + signal?.addEventListener("abort", () => reject(signal.reason), { once: true }); + setTimeout(() => reject(new Error("should have aborted")), 5000); + }); + return ""; + }); + + const slow = agent({ name: "timeout-test" }); + await expect(slow("go", { timeout: 50 })).rejects.toThrow(/Timed out/); + }); + + it("supports timeout as an addon", async () => { + mocks.setSendAndWaitImpl(async ({ signal }) => { + await new Promise((_, reject) => { + signal?.addEventListener("abort", () => reject(signal.reason), { once: true }); + setTimeout(() => reject(new Error("should have aborted")), 5000); + }); + return ""; + }); + + const slow = agent({ name: "timeout-test", addons: timeout({ timeout: 50 }) }); + await expect(slow("go")).rejects.toThrow(/Timed out/); + }); + + it("inlines prompt intents and omits top-level prompt metadata", async () => { + const prompts: string[] = []; + + mocks.setSendAndWaitImpl(async ({ prompt }) => { + prompts.push(prompt); + return { text: "ok" }; + }); + + const inspect = agent({ + name: "inspect", + input: s.object({ status: s.string, diff: s.string }), + output: s.object({ text: s.string }), + }); + + await inspect({ + status: p.bash("git status --short"), + diff: p.bash("git diff --stat", { cwd: "/tmp/workspace" }), + }); + + expect(prompts[0]).not.toContain(""); + expect(prompts[0]).not.toContain(""); + expect(prompts[0]).not.toContain(''); + expect(prompts[0]).toContain("Run bash command and return stdout as text: git status --short"); + expect(prompts[0]).toContain("Run bash command and return stdout as text: git diff --stat"); + expect(prompts[0]).toContain("Rig runs inside a sandboxed agentic workflow."); + expect(prompts[0]).toContain("without asking for extra permission or confirmation."); + expect(prompts[0]).toContain("Options:"); + expect(prompts[0]).toContain("/tmp/workspace"); + }); + + it("supports prompt helpers inside instruction templates", async () => { + const prompts: string[] = []; + + mocks.setSendAndWaitImpl(async ({ prompt }) => { + prompts.push(prompt); + return { text: "ok" }; + }); + + const inspect = agent({ + name: "inspect", + instructions: p`Review the repo using ${p.bash("git status --short", { cwd: "/tmp/workspace" })} before answering.`, + output: s.object({ text: s.string }), + }); + + await inspect("go"); + + expect(prompts[0]).toContain("Review the repo using Run bash command and return stdout as text: git status --short"); + expect(prompts[0]).toContain("Rig runs inside a sandboxed agentic workflow."); + expect(prompts[0]).toContain("without asking for extra permission or confirmation."); + expect(prompts[0]).toContain("Options:"); + expect(prompts[0]).toContain("/tmp/workspace"); + expect(prompts[0]).toContain("before answering."); + }); + + it("supports addons that steer retries near max turns", async () => { + const prompts: string[] = []; + let calls = 0; + + mocks.setSendAndWaitImpl(async ({ prompt }) => { + prompts.push(prompt); + calls += 1; + if (calls === 1) { + return "not json"; + } + return prompt.includes("running out of turns") + ? JSON.stringify("recovered") + : "still not json"; + }); + + const steerable = agent({ + name: "steerable", + maxTurns: 2, + addons: [ + async (context, next) => { + await next(); + if (context.nextPrompt && context.turn === context.maxTurns - 1) { + context.nextPrompt = `${context.nextPrompt}\nAdd a short correction because you are running out of turns.`; + } + }, + repair, + ], + }); + + await expect(steerable("go")).resolves.toBe("recovered"); + expect(prompts).toHaveLength(2); + expect(prompts[1]).toContain("running out of turns"); + }); + + it("exports a steering addon that warns near max turns", async () => { + const prompts: string[] = []; + let calls = 0; + + mocks.setSendAndWaitImpl(async ({ prompt }) => { + prompts.push(prompt); + calls += 1; + if (calls === 1) { + return "not json"; + } + return prompt.includes("final attempt before reaching the turn limit") + ? JSON.stringify("recovered") + : "still not json"; + }); + + const steerable = agent({ + name: "steerable", + maxTurns: 2, + addons: [steering(), repair], + }); + + await expect(steerable("go")).resolves.toBe("recovered"); + expect(prompts).toHaveLength(2); + expect(prompts[1]).toContain("final attempt before reaching the turn limit"); + }); + + it("supports addons that validate snippets inline", async () => { + let calls = 0; + mocks.setSendAndWaitImpl(async () => { + calls += 1; + return calls === 1 + ? JSON.stringify({ code: "const x = 1;" }) + : JSON.stringify({ code: "```ts\nconst x = 1;\n```" }); + }); + + const snippetGuard = agent({ + name: "snippet-guard", + maxTurns: 2, + output: s.object({ code: s.string }), + addons: [ + async (context, next) => { + await next(); + if (!context.nextPrompt && context.output && typeof context.output === "object") { + const code = (context.output as { code?: unknown }).code; + if (typeof code === "string" && !code.includes("```")) { + context.completed = false; + context.output = undefined; + context.nextPrompt = "Return the same payload but wrap code in a fenced markdown block."; + } + } + }, + repair, + ], + }); + + await expect(snippetGuard("go")).resolves.toEqual({ code: "```ts\nconst x = 1;\n```" }); + }); + + it("rejects non-function addon entries", async () => { + mocks.setSendAndWaitImpl(async () => JSON.stringify("ok")); + const guarded = agent({ name: "guarded", addons: [null as unknown as any] as any }); + await expect(guarded("go")).rejects.toThrow( + "Agent addon entries must be functions.", + ); + }); + + it("registers with the Copilot session once per call", async () => { + let turns = 0; + const register = vi.fn(); + mocks.setSendAndWaitImpl(async () => { + turns += 1; + return turns === 1 ? "not json" : JSON.stringify("hello world"); + }); + + const review = agent({ + name: "review", + maxTurns: 2, + addons: [ + oncePerSession(async (session, context) => { + register(session, context.turn); + }), + repair, + ], + }); + + await expect(review("go")).resolves.toBe("hello world"); + expect(register).toHaveBeenCalledTimes(1); + expect(register).toHaveBeenCalledWith( + expect.objectContaining({ + sendAndWait: expect.any(Function), + disconnect: expect.any(Function), + }), + 1, + ); + }); + + it("renders schema descriptions for discovery", async () => { + const prompts: string[] = []; + + mocks.setSendAndWaitImpl(async ({ prompt }) => { + prompts.push(prompt); + return { text: "ok" }; + }); + + const describeSchema = agent({ + name: "describe-schema", + output: s.object({ + text: s.string("Final answer text"), + }, "Response payload"), + }); + + await describeSchema("go"); + + expect(prompts[0]).toContain("\"text\": {\n \"type\": \"string\",\n \"description\": \"Final answer text\"\n }"); + expect(prompts[0]).toContain("\"description\": \"Response payload\""); + }); + + it("renders subagent metadata for delegated task-solving prompts", async () => { + const prompts: string[] = []; + + mocks.setSendAndWaitImpl(async ({ prompt }) => { + prompts.push(prompt); + return { markdown: "```rig\nexport default agent({ name: \"root\" });\n```" }; + }); + + const draftRigMarkdown = agent({ + name: "draft-rig-markdown", + model: "mini", + instructions: "Generate a markdown response containing one ```rig block that solves the task.", + input: s.object({ task: s.string }), + output: s.object({ markdown: s.string }), + }); + + const orchestrator = agent({ + name: "orchestrator", + model: "large", + instructions: "Use the delegated subagents to solve the task and return the markdown program.", + input: s.object({ task: s.string }), + output: s.object({ markdown: s.string }), + agents: { draftRigMarkdown }, + }); + + await orchestrator({ task: "Create a rig markdown program that reviews a pull request diff." }); + + expect(prompts[0]).toContain(""); + expect(prompts[0]).toContain('"name": "draftRigMarkdown"'); + expect(prompts[0]).toContain('"instructions": "Generate a markdown response containing one ```rig block that solves the task."'); + expect(prompts[0]).toContain('"model": "mini"'); + expect(prompts[0]).toContain('"input": "{'); + expect(prompts[0]).toContain('"output": "{'); + }); +}); + +describe("prompt intents", () => { + it("exports prompt helpers from p and hides internal helpers", async () => { + const compat = await import("rig"); + expect(compat.p.read("README.md").mode).toBe("prompt.read"); + expect(compat.p.bash("git status --short").mode).toBe("prompt.text"); + expect(typeof compat.p).toBe("function"); + expect((compat as Record)["sh"]).toBeUndefined(); + expect((compat as Record)["validate"]).toBeUndefined(); + expect((compat as Record)["collectIntents"]).toBeUndefined(); + }); + + it("creates prompt intents via p helpers", () => { + const diff = p.bash("git diff"); + const testOutput = p.bash("npm test", { cwd: "/tmp/workspace" }); + const readme = p.read("README.md"); + + expect(diff.mode).toBe("prompt.text"); + expect(testOutput.mode).toBe("prompt.text"); + expect(testOutput.options).toEqual({ cwd: "/tmp/workspace" }); + expect(readme.mode).toBe("prompt.read"); + }); + + it("strips AbortSignal from intent options", () => { + const controller = new AbortController(); + const intent = p.bash("echo hi", { cwd: "/tmp", signal: controller.signal }); + + expect(intent.options).toEqual({ cwd: "/tmp" }); + }); +}); + +describe("prompt builder", () => { + it("exposes prompt helpers on p", () => { + expect(p.read("README.md").mode).toBe("prompt.read"); + expect(p.bash("git status --short").mode).toBe("prompt.text"); + expect(p.write("README.md", "# Updated\n").mode).toBe("prompt.write"); + }); + + it("returns a prompt builder from tagged template syntax", () => { + const builder = p`Repository: ${p.var("repo", "rig")}\nStatus: ${p.bash("git status --short")}`; + + expect(builder).toBeInstanceOf(PromptBuilder); + expect(String(builder)).toContain("Repository: rig"); + expect(String(builder)).toContain("Run bash command and return stdout as text: git status --short"); + }); + + it("normalizes indentation for multiline tagged template syntax", () => { + const builder = p` + Generate a patch. + Use ${p.read("README.md")} as context. + Return only valid JSON. + `; + + const rendered = String(builder); + expect(rendered).toContain("Generate a patch."); + expect(rendered).toContain("Use Read file and return its contents as text: \"README.md\""); + expect(rendered).toContain("sandboxed agentic workflow"); + expect(rendered).toContain("as context."); + expect(rendered).toContain("Return only valid JSON."); + expect(rendered.startsWith("Generate a patch.\nUse ")).toBe(true); + expect(rendered.startsWith("\n")).toBe(false); + expect(rendered.endsWith("\n")).toBe(false); + expect(rendered.split("\n").every((line) => !line.startsWith(" "))).toBe(true); + }); + + it("builds prompt text with variables and intents", () => { + const builder = p(); + const repo = builder.var("repo", "rig"); + builder.write("Repository: ", repo, "\n"); + builder.write("Status: ", builder.bash("git status --short")); + + expect(builder.get("repo")).toBe("rig"); + expect(String(builder)).toContain("Repository: rig"); + expect(String(builder)).toContain("Run bash command and return stdout as text: git status --short"); + }); + + it("creates code regions", () => { + const builder = p(); + builder.region("ts", "const done = true;"); + + expect(String(builder)).toBe("```ts\nconst done = true;\n```\n"); + expect(p.region("json", "{\n \"ok\": true\n}")).toContain("```json"); + }); +}); + +describe("p template literal for instructions", () => { + it("returns a PromptBuilder from tagged template syntax", () => { + const builder = p`Review the diff.`; + + expect(builder).toBeInstanceOf(PromptBuilder); + expect(String(builder)).toBe("Review the diff."); + }); + + it("inlines prompt intents as expressions", () => { + const builder = p`Review the repo using ${p.bash("git status --short")} before answering.`; + + expect(builder).toBeInstanceOf(PromptBuilder); + expect(String(builder)).toContain("Review the repo using Run bash command and return stdout as text: git status --short"); + expect(String(builder)).toContain("before answering."); + }); + + it("inlines nested PromptBuilder as an expression", () => { + const inner = p`World`; + const outer = p`Hello ${inner}`; + + expect(String(outer)).toBe("Hello World"); + }); + + it("can be used as instructions in an agent spec", async () => { + const prompts: string[] = []; + + mocks.setSendAndWaitImpl(async ({ prompt }) => { + prompts.push(prompt); + return { text: "ok" }; + }); + + const reviewAgent = agent({ + name: "review", + instructions: p`Use ${p.bash("git diff --stat")} to review changes.`, + output: s.object({ text: s.string }), + }); + + await reviewAgent("go"); + + expect(prompts[0]).toContain("Use Run bash command and return stdout as text: git diff --stat"); + expect(prompts[0]).toContain("to review changes."); + expect(prompts[0]).toContain("Rig runs inside a sandboxed agentic workflow."); + }); +}); + +describe("toJsonSchema", () => { + it("serializes s.* helpers as native JSON Schema without conversion", () => { + expect(JSON.parse(JSON.stringify(s.string))).toEqual({ type: "string" }); + expect(JSON.parse(JSON.stringify(s.object({ + name: s.string, + age: s.optional(s.number), + })))).toEqual({ + type: "object", + properties: { name: { type: "string" }, age: { type: "number" } }, + required: ["name"], + }); + }); + + it("converts primitive schemas", () => { + expect(toJsonSchema(s.string)).toEqual({ type: "string" }); + expect(toJsonSchema(s.number)).toEqual({ type: "number" }); + expect(toJsonSchema(s.boolean)).toEqual({ type: "boolean" }); + expect(toJsonSchema(s.unknown)).toEqual({}); + }); + + it("includes description when present", () => { + expect(toJsonSchema(s.string("A text value"))).toEqual({ type: "string", description: "A text value" }); + expect(toJsonSchema(s.number("A numeric value"))).toEqual({ type: "number", description: "A numeric value" }); + }); + + it("converts enum schemas", () => { + expect(toJsonSchema(s.enum("a", "b", "c"))).toEqual({ enum: ["a", "b", "c"] }); + expect(toJsonSchema(s.enum(["x", "y"], "A choice"))).toEqual({ enum: ["x", "y"], description: "A choice" }); + }); + + it("converts array schemas", () => { + expect(toJsonSchema(s.array(s.string))).toEqual({ type: "array", items: { type: "string" } }); + expect(toJsonSchema(s.array(s.number, "A list"))).toEqual({ type: "array", items: { type: "number" }, description: "A list" }); + }); + + it("converts record schemas", () => { + expect(toJsonSchema(s.record(s.string))).toEqual({ type: "object", additionalProperties: { type: "string" } }); + }); + + it("converts object schemas with required fields", () => { + expect(toJsonSchema(s.object({ name: s.string, age: s.number }))).toEqual({ + type: "object", + properties: { name: { type: "string" }, age: { type: "number" } }, + required: ["name", "age"], + }); + }); + + it("omits required for all-optional object fields", () => { + expect(toJsonSchema(s.object({ name: s.optional(s.string) }))).toEqual({ + type: "object", + properties: { name: { type: "string" } }, + }); + }); + + it("separates required and optional object fields", () => { + expect(toJsonSchema(s.object({ name: s.string, age: s.optional(s.number) }))).toEqual({ + type: "object", + properties: { name: { type: "string" }, age: { type: "number" } }, + required: ["name"], + }); + }); + + it("converts optional schemas to their inner schema", () => { + expect(toJsonSchema(s.optional(s.string))).toEqual({ type: "string" }); + expect(toJsonSchema(s.optional(s.string, "maybe text"))).toEqual({ type: "string", description: "maybe text" }); + }); + + it("converts nested schemas", () => { + expect(toJsonSchema(s.object({ + items: s.array(s.object({ id: s.number, label: s.string })), + }))).toEqual({ + type: "object", + properties: { + items: { + type: "array", + items: { + type: "object", + properties: { id: { type: "number" }, label: { type: "string" } }, + required: ["id", "label"], + }, + }, + }, + required: ["items"], + }); + }); + + it("is accessible as s.toJsonSchema", () => { + expect(s.toJsonSchema(s.string)).toEqual({ type: "string" }); + }); +}); diff --git a/src/samples/01-single-agent-haiku.ts b/src/samples/01-single-agent-haiku.ts new file mode 100644 index 0000000..de637ff --- /dev/null +++ b/src/samples/01-single-agent-haiku.ts @@ -0,0 +1,16 @@ +import { agent, s } from "rig"; +// Minimal single-agent sample used by integration tests to verify real runtime execution with Copilot auth. +// Agent role: write a single haiku about the user's topic. +const haiku = agent({ + name: "single-agent-haiku", + model: "claude-haiku-4.5", + output: s.object({ + haiku: s.string, + }), + instructions: ` + Write one haiku about the user's topic. + Return exactly three short lines in the haiku field. + `, +}); + +export default haiku; diff --git a/src/samples/02-review-git-diff.ts b/src/samples/02-review-git-diff.ts new file mode 100644 index 0000000..9fd13aa --- /dev/null +++ b/src/samples/02-review-git-diff.ts @@ -0,0 +1,27 @@ +import { agent, p, s } from "rig"; + +// Agent role: review the repository diff and return a structured summary. + +const reviewDiff = agent({ + model: "mini", + instructions: "Review the repository diff and return a structured summary.", + input: s.object({ + diff: s.string, + status: s.string, + }), + output: s.object({ + summary: s.string, + findings: s.array(s.object({ + file: s.string, + line: s.optional(s.number), + message: s.string, + })), + }), +}); + +await reviewDiff({ + diff: p.bash("git diff --stat"), + status: p.bash("git status --short"), +}); + +export default reviewDiff; diff --git a/src/samples/03-diagnose-test-failure.ts b/src/samples/03-diagnose-test-failure.ts new file mode 100644 index 0000000..07dff9c --- /dev/null +++ b/src/samples/03-diagnose-test-failure.ts @@ -0,0 +1,30 @@ +import { agent, p, s } from "rig"; +// Agent role: review input.diff for correctness and regression risks. Return only the declared output shape. +const reviewer = agent({ + model: "mini", + input: s.object({ + diff: s.string, + status: s.optional(s.string) + }), + output: s.object({ + summary: s.string, + risk: s.enum("low", "medium", "high"), + findings: s.array(s.object({ + severity: s.enum("info", "warning", "error"), + message: s.string, + file: s.optional(s.string), + line: s.optional(s.number) + })), + tests: s.array(s.string) + }), + instructions: ` + Review input.diff for correctness and regression risks. + Return only the declared output shape. + `, +}); +await reviewer({ + diff: p.bash("git diff -- ."), + status: p.bash("git status --short"), +}); + +export default reviewer; diff --git a/src/samples/04-generate-readme.ts b/src/samples/04-generate-readme.ts new file mode 100644 index 0000000..48fa436 --- /dev/null +++ b/src/samples/04-generate-readme.ts @@ -0,0 +1,23 @@ +import { agent, p, s } from "rig"; +// Agent role: diagnose the failing test result. Do not edit files. +const diagnose = agent({ + model: "mini", + input: s.object({ + test: s.string + }), + output: s.object({ + rootCause: s.string, + confidence: s.number, + relevantFiles: s.array(s.string), + nextSteps: s.array(s.string) + }), + instructions: ` + Diagnose the failing test result. + Do not edit files. + `, +}); +await diagnose({ + test: p.bash("npm test"), +}); + +export default diagnose; diff --git a/src/samples/05-write-readme-intent.ts b/src/samples/05-write-readme-intent.ts new file mode 100644 index 0000000..a3f241a --- /dev/null +++ b/src/samples/05-write-readme-intent.ts @@ -0,0 +1,23 @@ +import { agent, p, s } from "rig"; +// Agent role: generate a concise README for the package. Include install, usage, and API sections. +const readmeWriter = agent({ + model: "mini", + input: s.object({ + packageJson: s.string, + files: s.string + }), + output: s.object({ + path: s.enum("README.md"), + contents: s.string + }), + instructions: ` + Generate a concise README for the package. + Include install, usage, and API sections. + `, +}); +await readmeWriter({ + packageJson: p.read("package.json"), + files: p.bash("find . -maxdepth 2 -type f | sort"), +}); + +export default readmeWriter; diff --git a/src/samples/06-list-source-files.ts b/src/samples/06-list-source-files.ts new file mode 100644 index 0000000..b7259a5 --- /dev/null +++ b/src/samples/06-list-source-files.ts @@ -0,0 +1,27 @@ +import { agent, p, s } from "rig"; +// Agent role: confirm whether the write intent succeeded. +const writer = agent({ + model: "mini", + input: s.object({ + write: s.object({ + ok: s.boolean, + stdout: s.string, + stderr: s.string, + exitCode: s.number + }) + }), + output: s.object({ + written: s.boolean, + summary: s.string + }), + instructions: ` + Confirm whether the write intent succeeded. + `, +}); +await writer({ + write: p.write("README.md", "# Project\n\nGenerated README.\n", { + purpose: "create project README", + }), +}); + +export default writer; diff --git a/src/samples/07-summarize-many-files.ts b/src/samples/07-summarize-many-files.ts new file mode 100644 index 0000000..6cf2cec --- /dev/null +++ b/src/samples/07-summarize-many-files.ts @@ -0,0 +1,20 @@ +import { agent, p, s } from "rig"; + +// Agent role: summarize the repository file list in one sentence. + +const summarizeFiles = agent({ + model: "mini", + instructions: "Summarize the repository file list in one sentence.", + input: s.object({ + files: s.string, + }), + output: s.object({ + summary: s.string, + }), +}); + +await summarizeFiles({ + files: p.bash("find src -name '*.ts' -type f | sort"), +}); + +export default summarizeFiles; diff --git a/src/samples/08-extract-package-scripts.ts b/src/samples/08-extract-package-scripts.ts new file mode 100644 index 0000000..aee2195 --- /dev/null +++ b/src/samples/08-extract-package-scripts.ts @@ -0,0 +1,14 @@ +import { agent, p, s } from "rig"; + +// Agent role: extract package scripts and summarize what they do. +const extractScripts = agent({ + model: "mini", + instructions: p`Read ${p.read("package.json")} and summarize the package scripts. Use ${p.bash("find src -name '*.ts' -type f | sort")} only to call out source files that look relevant.`, + output: s.object({ + scriptsByName: s.record(s.string), + summary: s.string, + relatedFiles: s.array(s.string), + }), +}); + +export default extractScripts; diff --git a/src/samples/09-classify-issue.ts b/src/samples/09-classify-issue.ts new file mode 100644 index 0000000..0a4a96e --- /dev/null +++ b/src/samples/09-classify-issue.ts @@ -0,0 +1,23 @@ +import { agent, s } from "rig"; + +// Agent role: classify the issue. + +const classifyIssue = agent({ + model: "mini", + instructions: "Classify the issue.", + input: s.object({ + title: s.string, + body: s.string, + }), + output: s.object({ + label: s.enum("bug", "feature", "question", "docs"), + confidence: s.enum("low", "medium", "high"), + }), +}); + +await classifyIssue({ + title: "Crash on start", + body: "segfault", +}); + +export default classifyIssue; diff --git a/src/samples/10-triage-pr.ts b/src/samples/10-triage-pr.ts new file mode 100644 index 0000000..4da41b0 --- /dev/null +++ b/src/samples/10-triage-pr.ts @@ -0,0 +1,22 @@ +import { agent, s } from "rig"; +// Agent role: classify the GitHub issue and suggest labels. +const classifyIssue = agent({ + model: "mini", + input: s.object({ + title: s.string, + body: s.string + }), + output: s.object({ + kind: s.enum("bug", "feature", "question", "chore"), + priority: s.enum("p0", "p1", "p2", "p3"), + rationale: s.string, + labels: s.array(s.string) + }), + instructions: `Classify the GitHub issue and suggest labels.`, +}); +await classifyIssue({ + title: "CLI exits zero after failed upload", + body: "The command prints an error but exits with code 0.", +}); + +export default classifyIssue; diff --git a/src/samples/11-release-notes.ts b/src/samples/11-release-notes.ts new file mode 100644 index 0000000..7359d92 --- /dev/null +++ b/src/samples/11-release-notes.ts @@ -0,0 +1,22 @@ +import { agent, p, s } from "rig"; +// Agent role: triage the pull request and recommend reviewers. +const triage = agent({ + model: "mini", + input: s.object({ + diff: s.string, + files: s.string + }), + output: s.object({ + area: s.enum("runtime", "docs", "tests", "ci", "unknown"), + risk: s.enum("low", "medium", "high"), + reviewers: s.array(s.string), + reason: s.string + }), + instructions: `Triage the pull request and recommend reviewers.`, +}); +await triage({ + diff: p.bash("git diff origin/main...HEAD"), + files: p.bash("git diff --name-only origin/main...HEAD"), +}); + +export default triage; diff --git a/src/samples/12-security-scan-review.ts b/src/samples/12-security-scan-review.ts new file mode 100644 index 0000000..90335c3 --- /dev/null +++ b/src/samples/12-security-scan-review.ts @@ -0,0 +1,15 @@ +import { agent, p, s } from "rig"; +// Agent role: write release notes from commits. Omit empty sections as empty arrays. +const releaseNotes = agent({ + model: "mini", + output: s.object({ + version: s.optional(s.string), + highlights: s.array(s.string), + breaking: s.array(s.string), + fixes: s.array(s.string) + }), + instructions: `Write release notes from commits. Omit empty sections as empty arrays.`, +}); +await releaseNotes(p.bash("git log --oneline --decorate -50")); + +export default releaseNotes; diff --git a/src/samples/13-test-plan.ts b/src/samples/13-test-plan.ts new file mode 100644 index 0000000..882d742 --- /dev/null +++ b/src/samples/13-test-plan.ts @@ -0,0 +1,24 @@ +import { agent, p, s } from "rig"; +// Agent role: review dependency security posture from the provided outputs. +const securityReview = agent({ + model: "mini", + input: s.object({ + dependencies: s.string, + audit: s.string + }), + output: s.object({ + status: s.enum("clean", "needs-action", "unknown"), + findings: s.array(s.object({ + package: s.string, + severity: s.string, + action: s.string + })) + }), + instructions: `Review dependency security posture from the provided outputs.`, +}); +await securityReview({ + dependencies: p.bash("npm ls --depth=0"), + audit: p.bash("npm audit --json", { purpose: "security audit" }), +}); + +export default securityReview; diff --git a/src/samples/14-changelog-categorizer.ts b/src/samples/14-changelog-categorizer.ts new file mode 100644 index 0000000..5f041be --- /dev/null +++ b/src/samples/14-changelog-categorizer.ts @@ -0,0 +1,21 @@ +import { agent, p, s } from "rig"; +// Agent role: create a focused validation plan for the current changes. +const planner = agent({ + model: "mini", + input: s.object({ + diff: s.string, + packageJson: s.string + }), + output: s.object({ + commands: s.array(s.string), + manualChecks: s.array(s.string), + rationale: s.string + }), + instructions: `Create a focused validation plan for the current changes.`, +}); +await planner({ + diff: p.bash("git diff -- ."), + packageJson: p.read("package.json"), +}); + +export default planner; diff --git a/src/samples/15-api-diff-summary.ts b/src/samples/15-api-diff-summary.ts new file mode 100644 index 0000000..2ca9ff1 --- /dev/null +++ b/src/samples/15-api-diff-summary.ts @@ -0,0 +1,12 @@ +import { agent, s } from "rig"; +// Agent role: convert the change description to Keep a Changelog style. +const categorize = agent({ + model: "mini", + output: s.object({ + category: s.enum("added", "changed", "deprecated", "removed", "fixed", "security"), + entry: s.string + }), + instructions: `Convert the change description to Keep a Changelog style.`, +}); + +export default categorize; diff --git a/src/samples/16-docs-gap-analysis.ts b/src/samples/16-docs-gap-analysis.ts new file mode 100644 index 0000000..06675e9 --- /dev/null +++ b/src/samples/16-docs-gap-analysis.ts @@ -0,0 +1,21 @@ +import { agent, p, s } from "rig"; +// Agent role: compare public API declarations and identify breaking changes. +const apiDiff = agent({ + model: "mini", + input: s.object({ + before: s.string, + after: s.string + }), + output: s.object({ + breaking: s.boolean, + summary: s.string, + changes: s.array(s.string) + }), + instructions: `Compare public API declarations and identify breaking changes.`, +}); +await apiDiff({ + before: p.bash("git show origin/main:dist/index.d.ts"), + after: p.read("dist/index.d.ts"), +}); + +export default apiDiff; diff --git a/src/samples/17-refactor-plan.ts b/src/samples/17-refactor-plan.ts new file mode 100644 index 0000000..b484eb6 --- /dev/null +++ b/src/samples/17-refactor-plan.ts @@ -0,0 +1,21 @@ +import { agent, p, s } from "rig"; +// Agent role: find documentation gaps against the source API. +const docsGap = agent({ + model: "mini", + input: s.object({ + source: s.string, + docs: s.string + }), + output: s.object({ + missing: s.array(s.string), + stale: s.array(s.string), + quickFixes: s.array(s.string) + }), + instructions: `Find documentation gaps against the source API.`, +}); +await docsGap({ + source: p.bash("grep -R \"export \" -n src || true"), + docs: p`${p.read("README.md")}\n${p.read("docs/*.md")}`, +}); + +export default docsGap; diff --git a/src/samples/18-patch-writer-output.ts b/src/samples/18-patch-writer-output.ts new file mode 100644 index 0000000..c4b6c6e --- /dev/null +++ b/src/samples/18-patch-writer-output.ts @@ -0,0 +1,21 @@ +import { agent, p, s } from "rig"; +// Agent role: plan a minimal, low-risk refactor. Do not edit files. +const refactorPlan = agent({ + model: "mini", + input: s.object({ + files: s.string, + target: s.string + }), + output: s.object({ + steps: s.array(s.string), + files: s.array(s.string), + risks: s.array(s.string) + }), + instructions: `Plan a minimal, low-risk refactor. Do not edit files.`, +}); +await refactorPlan({ + files: p.bash("find src -type f | sort"), + target: "Split validation helpers out of the main runtime file.", +}); + +export default refactorPlan; diff --git a/src/samples/19-fix-then-review.ts b/src/samples/19-fix-then-review.ts new file mode 100644 index 0000000..d3912a9 --- /dev/null +++ b/src/samples/19-fix-then-review.ts @@ -0,0 +1,23 @@ +import { agent, p, s } from "rig"; +// Agent role: return a complete replacement for the target file. +const patcher = agent({ + model: "mini", + input: s.object({ + diagnosis: s.string, + file: s.string, + contents: s.string + }), + output: s.object({ + path: s.string, + contents: s.string, + summary: s.string + }), + instructions: `Return a complete replacement for the target file.`, +}); +await patcher({ + diagnosis: "The parser accepts trailing prose after JSON.", + file: "src/index.ts", + contents: p.read("src/index.ts"), +}); + +export default patcher; diff --git a/src/samples/20-issue-reproducer.ts b/src/samples/20-issue-reproducer.ts new file mode 100644 index 0000000..eaf713f --- /dev/null +++ b/src/samples/20-issue-reproducer.ts @@ -0,0 +1,55 @@ +import { agent, p, s } from "rig"; +const Diagnosis = s.object({ + rootCause: s.string, + confidence: s.number +}); +// Agent role: diagnose the test failure. +const diagnose = agent({ + model: "mini", + input: s.object({ + test: s.string + }), + output: Diagnosis, + instructions: `Diagnose the test failure.`, +}); +// Agent role: make the smallest safe patch using engine capabilities. +const fix = agent({ + model: "mini", + input: s.object({ + diagnosis: Diagnosis + }), + output: s.object({ + changed: s.boolean, + summary: s.string + }), + instructions: `Make the smallest safe patch using engine capabilities.`, +}); +// Agent role: review the patch against the diagnosis. +const review = agent({ + model: "mini", + input: s.object({ + diff: s.string, + diagnosis: Diagnosis + }), + output: s.object({ + approved: s.boolean, + issues: s.array(s.string) + }), + instructions: `Review the patch against the diagnosis.`, +}); +const d = await diagnose({ test: p.bash("npm test") }); +await fix({ diagnosis: d }); +await review({ diff: p.bash("git diff -- ."), diagnosis: d }); + +// Agent role: orchestrate diagnose/fix/review as the runnable root for this workflow. +const issueReproducer = agent({ + model: "mini", + instructions: "Use the provided subagents to diagnose, patch, and review a failing test case.", + output: s.object({ + approved: s.boolean, + issues: s.array(s.string), + }), + agents: { diagnose, fix, review } +}); + +export default issueReproducer; diff --git a/src/samples/21-ci-log-diagnosis.ts b/src/samples/21-ci-log-diagnosis.ts new file mode 100644 index 0000000..eabf465 --- /dev/null +++ b/src/samples/21-ci-log-diagnosis.ts @@ -0,0 +1,22 @@ +import { agent, s } from "rig"; +// Agent role: extract a clear reproduction from the issue. +const reproducer = agent({ + model: "mini", + input: s.object({ + issueTitle: s.string, + issueBody: s.string + }), + output: s.object({ + steps: s.array(s.string), + expected: s.string, + actual: s.string, + missingInfo: s.array(s.string) + }), + instructions: `Extract a clear reproduction from the issue.`, +}); +await reproducer({ + issueTitle: "Install fails on Windows", + issueBody: "npm install errors with EPERM when postinstall runs.", +}); + +export default reproducer; diff --git a/src/samples/22-config-normalizer.ts b/src/samples/22-config-normalizer.ts new file mode 100644 index 0000000..3278e82 --- /dev/null +++ b/src/samples/22-config-normalizer.ts @@ -0,0 +1,14 @@ +import { agent, p, s } from "rig"; +// Agent role: diagnose the CI log. Prefer the first real failure over cascading errors. +const ciDiagnosis = agent({ + model: "mini", + output: s.object({ + failure: s.string, + likelyCause: s.string, + commandsToTry: s.array(s.string) + }), + instructions: `Diagnose the CI log. Prefer the first real failure over cascading errors.`, +}); +await ciDiagnosis(p.read("ci.log")); + +export default ciDiagnosis; diff --git a/src/samples/23-schema-inference.ts b/src/samples/23-schema-inference.ts new file mode 100644 index 0000000..d931d23 --- /dev/null +++ b/src/samples/23-schema-inference.ts @@ -0,0 +1,13 @@ +import { agent, p, s } from "rig"; +// Agent role: normalize the config into a JSON-compatible object. +const normalize = agent({ + model: "mini", + output: s.object({ + normalized: s.unknown, + warnings: s.array(s.string) + }), + instructions: `Normalize the config into a JSON-compatible object.`, +}); +await normalize(p.read("config.json")); + +export default normalize; diff --git a/src/samples/24-error-message-improver.ts b/src/samples/24-error-message-improver.ts new file mode 100644 index 0000000..f034476 --- /dev/null +++ b/src/samples/24-error-message-improver.ts @@ -0,0 +1,17 @@ +import { agent, p, s } from "rig"; +// Agent role: infer a practical runtime-visible schema from the samples. +const inferShape = agent({ + model: "mini", + output: s.object({ + fields: s.array(s.object({ + name: s.string, + type: s.string, + optional: s.boolean + })), + example: s.unknown + }), + instructions: `Infer a practical runtime-visible schema from the samples.`, +}); +await inferShape(p.bash("head -100 data/events.ndjson")); + +export default inferShape; diff --git a/src/samples/25-migration-guide.ts b/src/samples/25-migration-guide.ts new file mode 100644 index 0000000..3aca7cb --- /dev/null +++ b/src/samples/25-migration-guide.ts @@ -0,0 +1,20 @@ +import { agent, s } from "rig"; +// Agent role: rewrite the error to be actionable and precise. +const improve = agent({ + model: "mini", + input: s.object({ + message: s.string, + context: s.optional(s.string) + }), + output: s.object({ + message: s.string, + explanation: s.string + }), + instructions: `Rewrite the error to be actionable and precise.`, +}); +await improve({ + message: "bad output", + context: "Validation failed for optional underscore field.", +}); + +export default improve; diff --git a/src/samples/26-design-review.ts b/src/samples/26-design-review.ts new file mode 100644 index 0000000..4b4757e --- /dev/null +++ b/src/samples/26-design-review.ts @@ -0,0 +1,26 @@ +import { agent, s } from "rig"; +// Agent role: write a concise migration guide. +const migration = agent({ + model: "mini", + input: s.object({ + fromVersion: s.string, + toVersion: s.string, + changes: s.array(s.string) + }), + output: s.object({ + title: s.string, + steps: s.array(s.string), + examples: s.array(s.object({ + before: s.string, + after: s.string + })) + }), + instructions: `Write a concise migration guide.`, +}); +await migration({ + fromVersion: "0.1", + toVersion: "0.2", + changes: ["Agents now always receive input objects."], +}); + +export default migration; diff --git a/src/samples/27-dependency-upgrade-plan.ts b/src/samples/27-dependency-upgrade-plan.ts new file mode 100644 index 0000000..060caab --- /dev/null +++ b/src/samples/27-dependency-upgrade-plan.ts @@ -0,0 +1,15 @@ +import { agent, s } from "rig"; +// Agent role: review the design proposal for simplicity and maintainability. +const designReview = agent({ + model: "mini", + output: s.object({ + decision: s.enum("approve", "revise", "reject"), + strengths: s.array(s.string), + concerns: s.array(s.string), + requiredChanges: s.array(s.string) + }), + instructions: `Review the design proposal for simplicity and maintainability.`, +}); +await designReview("Add a direct p.run helper for local execution."); + +export default designReview; diff --git a/src/samples/28-license-check.ts b/src/samples/28-license-check.ts new file mode 100644 index 0000000..68e6c79 --- /dev/null +++ b/src/samples/28-license-check.ts @@ -0,0 +1,25 @@ +import { agent, p, s } from "rig"; +// Agent role: plan safe dependency upgrades. +const upgradePlan = agent({ + model: "mini", + input: s.object({ + packageJson: s.string, + outdated: s.string + }), + output: s.object({ + upgrades: s.array(s.object({ + package: s.string, + from: s.string, + to: s.string, + risk: s.string + })), + order: s.array(s.string) + }), + instructions: `Plan safe dependency upgrades.`, +}); +await upgradePlan({ + packageJson: p.read("package.json"), + outdated: p.bash("npm outdated || true"), +}); + +export default upgradePlan; diff --git a/src/samples/29-bug-report-draft.ts b/src/samples/29-bug-report-draft.ts new file mode 100644 index 0000000..c461389 --- /dev/null +++ b/src/samples/29-bug-report-draft.ts @@ -0,0 +1,18 @@ +import { agent, p, s } from "rig"; +// Agent role: flag unknown or concerning dependency licenses. +const licenseCheck = agent({ + model: "mini", + output: s.object({ + compliant: s.boolean, + unknown: s.array(s.string), + concerning: s.array(s.object({ + package: s.string, + license: s.string, + reason: s.string + })) + }), + instructions: `Flag unknown or concerning dependency licenses.`, +}); +await licenseCheck(p.bash("npm ls --json --all", { purpose: "collect dependency tree" })); + +export default licenseCheck; diff --git a/src/samples/30-github-action-review.ts b/src/samples/30-github-action-review.ts new file mode 100644 index 0000000..ae73d1f --- /dev/null +++ b/src/samples/30-github-action-review.ts @@ -0,0 +1,21 @@ +import { agent, p, s } from "rig"; +// Agent role: draft a GitHub bug report from the failure details. +const bugReport = agent({ + model: "mini", + input: s.object({ + failure: s.string, + environment: s.string + }), + output: s.object({ + title: s.string, + body: s.string, + labels: s.array(s.string) + }), + instructions: `Draft a GitHub bug report from the failure details.`, +}); +await bugReport({ + failure: p.bash("npm test 2>&1 || true"), + environment: p.bash("node --version && npm --version && uname -a"), +}); + +export default bugReport; diff --git a/src/samples/31-monorepo-package-map.ts b/src/samples/31-monorepo-package-map.ts new file mode 100644 index 0000000..67ebc31 --- /dev/null +++ b/src/samples/31-monorepo-package-map.ts @@ -0,0 +1,14 @@ +import { agent, p, s } from "rig"; +// Agent role: review the workflow for reliability, caching, and least privilege. +const actionReview = agent({ + model: "mini", + output: s.object({ + summary: s.string, + problems: s.array(s.string), + improvements: s.array(s.string) + }), + instructions: `Review the workflow for reliability, caching, and least privilege.`, +}); +await actionReview(p.read(".github/workflows/*.{yml,yaml}")); + +export default actionReview; diff --git a/src/samples/32-command-planner.ts b/src/samples/32-command-planner.ts new file mode 100644 index 0000000..64baf96 --- /dev/null +++ b/src/samples/32-command-planner.ts @@ -0,0 +1,21 @@ +import { agent, p, s } from "rig"; +// Agent role: build a package map for a JavaScript monorepo. +const packageMap = agent({ + model: "mini", + output: s.object({ + packages: s.array(s.object({ + name: s.string, + path: s.string, + private: s.boolean + })), + relationships: s.array(s.object({ + from: s.string, + to: s.string, + kind: s.string + })) + }), + instructions: `Build a package map for a JavaScript monorepo.`, +}); +await packageMap(p.read("**/package.json")); + +export default packageMap; diff --git a/src/samples/33-readonly-investigator.ts b/src/samples/33-readonly-investigator.ts new file mode 100644 index 0000000..1cfdfa1 --- /dev/null +++ b/src/samples/33-readonly-investigator.ts @@ -0,0 +1,16 @@ +import { agent, s } from "rig"; +// Agent role: plan shell commands for the goal. Prefer readonly commands. +const commandPlanner = agent({ + model: "mini", + output: s.object({ + commands: s.array(s.object({ + command: s.string, + purpose: s.string, + readonly: s.boolean + })) + }), + instructions: `Plan shell commands for the goal. Prefer readonly commands.`, +}); +await commandPlanner("Understand why TypeScript declarations changed."); + +export default commandPlanner; diff --git a/src/samples/34-intent-options.ts b/src/samples/34-intent-options.ts new file mode 100644 index 0000000..8e1a644 --- /dev/null +++ b/src/samples/34-intent-options.ts @@ -0,0 +1,22 @@ +import { agent, p, s } from "rig"; +// Agent role: investigate the project using only readonly evidence. +const investigator = agent({ + model: "mini", + input: s.object({ + tree: s.string, + packageJson: s.string, + tests: s.string + }), + output: s.object({ + observations: s.array(s.string), + likelyEntryPoints: s.array(s.string) + }), + instructions: `Investigate the project using only readonly evidence.`, +}); +await investigator({ + tree: p.bash("find . -maxdepth 3 -type f | sort"), + packageJson: p.read("package.json"), + tests: p.bash("find . -name '*test*' -o -name '*spec*'"), +}); + +export default investigator; diff --git a/src/samples/35-call-options.ts b/src/samples/35-call-options.ts new file mode 100644 index 0000000..e078a5f --- /dev/null +++ b/src/samples/35-call-options.ts @@ -0,0 +1,27 @@ +import { agent, p, s } from "rig"; +// Agent role: parse environment outputs. +const envReader = agent({ + model: "mini", + input: s.object({ + nodeVersion: s.string, + cwdFiles: s.string + }), + output: s.object({ + nodeMajor: s.number, + files: s.array(s.string) + }), + instructions: `Parse environment outputs.`, +}); +await envReader({ + nodeVersion: p.bash("node --version", { + cwd: ".", + timeout: 10000, + purpose: "check Node version", + }), + cwdFiles: p.bash("ls -la", { + env: { FORCE_COLOR: "0" }, + purpose: "list current directory", + }), +}); + +export default envReader; diff --git a/src/samples/36-subagent-delegation.ts b/src/samples/36-subagent-delegation.ts new file mode 100644 index 0000000..dfdae30 --- /dev/null +++ b/src/samples/36-subagent-delegation.ts @@ -0,0 +1,49 @@ +import { agent, s } from "rig"; + +// Agent role: extract the most important implementation details from the topic. + +const researcher = agent({ + model: "mini", + instructions: "Extract the most important implementation details from the topic.", + input: s.object({ + topic: s.string, + }), + output: s.object({ + summary: s.string, + risks: s.array(s.string), + }), +}); + +// Agent role: turn the research summary into concrete next steps for the caller. + +const planner = agent({ + model: "mini", + instructions: "Turn the research summary into concrete next steps for the caller.", + input: s.object({ + summary: s.string, + risks: s.array(s.string), + }), + output: s.object({ + decision: s.string, + nextSteps: s.array(s.string), + }), +}); + +const research = await researcher({ + topic: "Explain runtime-visible schemas in one paragraph.", +}); + +await planner(research); + +// Agent role: orchestrate research and planning as the runnable root for delegation. +const delegateTask = agent({ + model: "mini", + instructions: "Use the provided subagents to research a topic and produce practical next steps.", + output: s.object({ + decision: s.string, + nextSteps: s.array(s.string), + }), + agents: { researcher, planner }, +}); + +export default delegateTask; diff --git a/src/samples/37-output-with-nullable.ts b/src/samples/37-output-with-nullable.ts new file mode 100644 index 0000000..62d6594 --- /dev/null +++ b/src/samples/37-output-with-nullable.ts @@ -0,0 +1,22 @@ +import { agent, s } from "rig"; +// Agent role: summarize the diff. +const summarizeDiff = agent({ + model: "mini", + output: s.object({ + summary: s.string, + files: s.array(s.string) + }), + instructions: `Summarize the diff.`, +}); +// Agent role: review the diff. You may use the provided subagent conceptually. +const reviewer = agent({ + model: "mini", + output: s.object({ + summary: s.string, + issues: s.array(s.string) + }), + agents: { summarizeDiff }, + instructions: `Review the diff. You may use the provided subagent conceptually.`, +}); + +export default reviewer; diff --git a/src/samples/38-exact-literal-output.ts b/src/samples/38-exact-literal-output.ts new file mode 100644 index 0000000..ad76dc9 --- /dev/null +++ b/src/samples/38-exact-literal-output.ts @@ -0,0 +1,12 @@ +import { agent, s } from "rig"; +// Agent role: extract event metadata. Use undefined when deletedAt is absent. +const parseEvent = agent({ + model: "mini", + output: s.object({ + title: s.string, + deletedAt: s.optional(s.string) + }), + instructions: `Extract event metadata. Use undefined when deletedAt is absent.`, +}); + +export default parseEvent; diff --git a/src/samples/39-unknown-raw-output.ts b/src/samples/39-unknown-raw-output.ts new file mode 100644 index 0000000..4331eff --- /dev/null +++ b/src/samples/39-unknown-raw-output.ts @@ -0,0 +1,13 @@ +import { agent, s } from "rig"; +// Agent role: convert the finding into a typed review record. +const reviewRecord = agent({ + model: "mini", + output: s.object({ + kind: s.enum("review-finding"), + finding: s.string, + severity: s.enum("info", "warning", "error") + }), + instructions: `Convert the finding into a typed review record.`, +}); + +export default reviewRecord; diff --git a/src/samples/40-record-output.ts b/src/samples/40-record-output.ts new file mode 100644 index 0000000..3949eb9 --- /dev/null +++ b/src/samples/40-record-output.ts @@ -0,0 +1,13 @@ +import { agent, p, s } from "rig"; +// Agent role: extract any JSON object from input.text into raw. +const extractJson = agent({ + model: "mini", + output: s.object({ + raw: s.unknown, + summary: s.string + }), + instructions: `Extract any JSON object from input.text into raw.`, +}); +await extractJson(p.bash("node ./scripts/print-config.js")); + +export default extractJson; diff --git a/src/samples/41-parse-coverage.ts b/src/samples/41-parse-coverage.ts new file mode 100644 index 0000000..bfa9a92 --- /dev/null +++ b/src/samples/41-parse-coverage.ts @@ -0,0 +1,16 @@ +import { agent, p, s } from "rig"; +// Agent role: parse coverage by file path. +const coverage = agent({ + model: "mini", + output: s.object({ + files: s.record(s.object({ + lines: s.number, + branches: s.number, + notes: s.optional(s.string) + })) + }), + instructions: `Parse coverage by file path.`, +}); +await coverage(p.read("coverage/coverage-summary.json")); + +export default coverage; diff --git a/src/samples/42-json-repair.ts b/src/samples/42-json-repair.ts new file mode 100644 index 0000000..e56e000 --- /dev/null +++ b/src/samples/42-json-repair.ts @@ -0,0 +1,17 @@ +import { agent, s } from "rig"; + +// Agent role: summarize the diff. + +const summarize = agent({ + model: "mini", + instructions: "Summarize the diff.", + input: s.object({ + diff: s.string, + }), + output: s.object({ + summary: s.string, + }), + maxTurns: 2, +}); + +export default summarize; diff --git a/src/samples/43-snapshot-test-updater.ts b/src/samples/43-snapshot-test-updater.ts new file mode 100644 index 0000000..52bb333 --- /dev/null +++ b/src/samples/43-snapshot-test-updater.ts @@ -0,0 +1,13 @@ +import { agent, s } from "rig"; +// Agent role: repair input.text into a JSON-compatible value. +const repair = agent({ + model: "mini", + output: s.object({ + repaired: s.unknown, + changes: s.array(s.string) + }), + instructions: `Repair input.text into a JSON-compatible value.`, +}); +await repair("{name: 'rig', trailing: true,}"); + +export default repair; diff --git a/src/samples/44-flaky-test-analysis.ts b/src/samples/44-flaky-test-analysis.ts new file mode 100644 index 0000000..c5955c6 --- /dev/null +++ b/src/samples/44-flaky-test-analysis.ts @@ -0,0 +1,21 @@ +import { agent, p, s } from "rig"; +// Agent role: decide whether snapshot updates are legitimate. +const snapshotReview = agent({ + model: "mini", + input: s.object({ + testResult: s.string, + diff: s.string + }), + output: s.object({ + safeToUpdate: s.boolean, + reason: s.string, + command: s.optional(s.string) + }), + instructions: `Decide whether snapshot updates are legitimate.`, +}); +await snapshotReview({ + testResult: p.bash("npm test -- --runInBand"), + diff: p.bash("git diff -- '*snap*'"), +}); + +export default snapshotReview; diff --git a/src/samples/45-code-owner-suggestion.ts b/src/samples/45-code-owner-suggestion.ts new file mode 100644 index 0000000..24d89a9 --- /dev/null +++ b/src/samples/45-code-owner-suggestion.ts @@ -0,0 +1,14 @@ +import { agent, p, s } from "rig"; +// Agent role: analyze whether the test failure appears flaky. +const flaky = agent({ + model: "mini", + output: s.object({ + likelyFlaky: s.boolean, + signals: s.array(s.string), + stabilizationIdeas: s.array(s.string) + }), + instructions: `Analyze whether the test failure appears flaky.`, +}); +await flaky(p.read("test-runs/*.log")); + +export default flaky; diff --git a/src/samples/46-prompt-intent-inspection.ts b/src/samples/46-prompt-intent-inspection.ts new file mode 100644 index 0000000..f94c6a0 --- /dev/null +++ b/src/samples/46-prompt-intent-inspection.ts @@ -0,0 +1,20 @@ +import { agent, p, s } from "rig"; +// Agent role: suggest owners for changed files. +const owners = agent({ + model: "mini", + input: s.object({ + codeowners: s.string, + changedFiles: s.string + }), + output: s.object({ + owners: s.array(s.string), + unmatchedFiles: s.array(s.string) + }), + instructions: `Suggest owners for changed files.`, +}); +await owners({ + codeowners: p.read(".github/CODEOWNERS"), + changedFiles: p.bash("git diff --name-only origin/main...HEAD"), +}); + +export default owners; diff --git a/src/samples/47-prompt-intents.ts b/src/samples/47-prompt-intents.ts new file mode 100644 index 0000000..204e203 --- /dev/null +++ b/src/samples/47-prompt-intents.ts @@ -0,0 +1,23 @@ +import { agent, p, s } from "rig"; + +// Agent role: summarize the current git workspace changes. + +const promptIntents = agent({ + model: "mini", + instructions: "Summarize the current git workspace changes.", + input: s.object({ + diff: s.string, + status: s.string, + }), + output: s.object({ + summary: s.string, + changedFiles: s.array(s.string), + }), +}); + +await promptIntents({ + diff: p.bash("git diff -- ."), + status: p.bash("git status --short"), +}); + +export default promptIntents; diff --git a/src/samples/48-custom-engine.ts b/src/samples/48-custom-engine.ts new file mode 100644 index 0000000..e41c58f --- /dev/null +++ b/src/samples/48-custom-engine.ts @@ -0,0 +1,15 @@ +import { agent, s } from "rig"; + +// Agent role: review the provided input and return the declared output. + +const review = agent({ + model: "mini", + output: s.object({ + summary: s.string, + risk: s.enum("low", "medium", "high"), + }), +}); + +await review("..."); + +export default review; diff --git a/src/samples/49-timeout-signal-helper.ts b/src/samples/49-timeout-signal-helper.ts new file mode 100644 index 0000000..8aa8498 --- /dev/null +++ b/src/samples/49-timeout-signal-helper.ts @@ -0,0 +1,12 @@ +import { agent } from "rig"; +import { timeout } from "rig/addons"; + +// Agent role: return a short response in output.text. + +const worker = agent({ + model: "mini", + instructions: `Return a short response in output.text.`, + addons: timeout({ timeout: 5_000 }), +}); + +export default worker; diff --git a/src/samples/50-end-to-end-release-agent.ts b/src/samples/50-end-to-end-release-agent.ts new file mode 100644 index 0000000..874ce09 --- /dev/null +++ b/src/samples/50-end-to-end-release-agent.ts @@ -0,0 +1,75 @@ +import { agent, p, s } from "rig"; + +// Agent role: summarize the release candidate changes from the diff and recent commits. + +const analyzeChanges = agent({ + model: "mini", + instructions: "Summarize the release candidate changes from the diff and recent commits.", + input: s.object({ + diff: s.string, + commits: s.string, + }), + output: s.object({ + summary: s.string, + highlights: s.array(s.string), + }), +}); + +// Agent role: choose the safest semantic version bump for the summarized changes. + +const chooseVersion = agent({ + model: "mini", + instructions: "Choose the safest semantic version bump for the summarized changes.", + input: s.object({ + summary: s.string, + highlights: s.array(s.string), + }), + output: s.object({ + bump: s.enum("patch", "minor", "major"), + rationale: s.string, + }), +}); + +// Agent role: draft the release title, checklist, and risks for the chosen version bump. + +const draftRelease = agent({ + model: "mini", + instructions: "Draft the release title, checklist, and risks for the chosen version bump.", + input: s.object({ + bump: s.enum("patch", "minor", "major"), + rationale: s.string, + summary: s.string, + highlights: s.array(s.string), + }), + output: s.object({ + title: s.string, + checklist: s.array(s.string), + risks: s.array(s.string), + }), +}); + +const analysis = await analyzeChanges({ + diff: p.bash("git diff --stat -- ."), + commits: p.bash("git log --oneline -20"), +}); + +const version = await chooseVersion(analysis); + +await draftRelease({ + ...analysis, + ...version, +}); + +// Agent role: orchestrate release analysis, versioning, and release draft planning. +const releaseCoordinator = agent({ + model: "mini", + instructions: "Use the provided subagents to produce a complete release draft from repo signals.", + output: s.object({ + title: s.string, + checklist: s.array(s.string), + risks: s.array(s.string), + }), + agents: { analyzeChanges, chooseVersion, draftRelease }, +}); + +export default releaseCoordinator; diff --git a/src/samples/51-claude-design.ts b/src/samples/51-claude-design.ts new file mode 100644 index 0000000..70fe408 --- /dev/null +++ b/src/samples/51-claude-design.ts @@ -0,0 +1,47 @@ +import { agent, s } from "rig"; + +// Agent role: write an initial response to the user request. +const writer = agent({ + model: "mini", + output: s.object({ draft: s.string }), + instructions: "Write a helpful, clear response to the request.", +}); + +// Agent role: critique the draft against helpfulness, harmlessness, and honesty principles. +const critic = agent({ + model: "mini", + input: s.object({ request: s.string, draft: s.string }), + output: s.object({ + issues: s.array(s.string), + score: s.number, + acceptable: s.boolean, + }), + instructions: "Evaluate the draft against helpfulness, harmlessness, and honesty principles.", +}); + +// Agent role: revise the draft to address all issues identified by the critic. +const reviser = agent({ + model: "mini", + input: s.object({ + request: s.string, + draft: s.string, + issues: s.array(s.string), + }), + output: s.object({ response: s.string }), + instructions: "Revise the draft to address all issues identified by the critic.", +}); + +const request = "Explain how to safely handle and dispose of old batteries."; +const { draft } = await writer(request); +const critique = await critic({ request, draft }); +await reviser({ request, draft, issues: critique.issues }); + +// Agent role: orchestrate writer/critic/reviser as the runnable root for this loop. +const claudeDesignLoop = agent({ + model: "mini", + instructions: "Use the provided subagents to draft, critique, and revise one final response.", + output: s.object({ response: s.string }), + agents: { writer, critic, reviser }, +}); + +export default claudeDesignLoop; diff --git a/src/samples/53-ralf-loop.ts b/src/samples/53-ralf-loop.ts new file mode 100644 index 0000000..211920b --- /dev/null +++ b/src/samples/53-ralf-loop.ts @@ -0,0 +1,47 @@ +import { agent, p, s } from "rig"; + +// Agent role: diagnose the root cause of test failures and decide if all tests pass. +const diagnose = agent({ + model: "mini", + input: s.object({ + test: s.string, + }), + output: s.object({ + done: s.boolean, + rootCause: s.string, + }), + instructions: "Diagnose the root cause of test failures. Set done to true if all tests pass.", +}); + +// Agent role: apply the smallest safe fix to address the diagnosed root cause. +const fix = agent({ + model: "mini", + input: s.object({ + rootCause: s.string, + }), + output: s.object({ + summary: s.string, + changed: s.boolean, + }), + instructions: "Apply the smallest safe fix to address the diagnosed root cause.", +}); + +const MAX_ITERATIONS = 3; +for (let i = 0; i < MAX_ITERATIONS; i++) { + const d = await diagnose({ test: p.bash("npm test") }); + if (d.done) break; + await fix({ rootCause: d.rootCause }); +} + +// Agent role: orchestrate diagnose/fix iterations as the runnable root for this loop. +const ralfLoop = agent({ + model: "mini", + instructions: "Use the provided subagents to iterate diagnose/fix until done.", + output: s.object({ + done: s.boolean, + rootCause: s.string, + }), + agents: { diagnose, fix }, +}); + +export default ralfLoop; diff --git a/src/samples/54-large-scale-summarization-rigs.ts b/src/samples/54-large-scale-summarization-rigs.ts new file mode 100644 index 0000000..d43cb2a --- /dev/null +++ b/src/samples/54-large-scale-summarization-rigs.ts @@ -0,0 +1,126 @@ +import { agent, p, s } from "rig"; + +const LARGE = "large"; +const MINI = "mini"; +const NANO = "nano"; + +// Agent role: summarize a bounded evidence shard using a small, low-cost model. +const summarizeShard = agent({ + model: MINI, + input: s.object({ + scenario: s.string, + shardLabel: s.string, + evidence: s.string, + }), + output: s.object({ + shardLabel: s.string, + summary: s.string, + facts: s.array(s.string), + }), + instructions: "Summarize only essential facts. Keep output concise and deduplicated.", +}); + +// Agent role: combine shard summaries into one final scenario summary. +const reduceScenario = agent({ + model: LARGE, + input: s.object({ + scenario: s.string, + shardSummaries: s.array(s.object({ + shardLabel: s.string, + summary: s.string, + facts: s.array(s.string), + })), + }), + output: s.object({ + scenario: s.string, + summary: s.string, + keyFindings: s.array(s.string), + estimatedTokenSavings: s.string, + }), + instructions: "Merge shard summaries into a single concise answer with no repeated facts.", +}); + +// Agent role: generate deterministic search patterns before any model-heavy summarization. +const planDeterministicSearch = agent({ + model: NANO, + input: s.object({ + query: s.string, + }), + output: s.object({ + grepPatterns: s.array(s.string), + includeGlobs: s.array(s.string), + }), + instructions: "Create high precision grep/rg patterns to minimize scanned text.", +}); + +// Agent role: orchestrate large-scale summarization scenarios with map/reduce and parallel fan-out. +const summarizeAtScale = agent({ + model: LARGE, + input: s.object({ + text: s.string, + }), + output: s.object({ + scenarios: s.array(s.object({ + id: s.string, + summary: s.string, + keyFindings: s.array(s.string), + estimatedTokenSavings: s.string, + })), + searchPlan: s.object({ + grepPatterns: s.array(s.string), + includeGlobs: s.array(s.string), + }), + }), + agents: { summarizeShard, reduceScenario, planDeterministicSearch }, + instructions: p`Build cost-efficient summaries for these large-scale scenarios: +- summarize a git diff +- summarize 24h of changes in a repo +- summarize exported changes in 24h of commits to update docs +- efficient semantic search of a codebase (query -> grep shards -> shard summaries -> final summary) +- summarize 24h of CI failures grouped by root cause +- summarize 24h of security-relevant auth and permission changes +- summarize 24h of dependency and lockfile changes by risk +- summarize 24h of API surface changes by consumer impact +- summarize 24h of cross-package monorepo changes by owner area + +Use deterministic evidence collection first, then parallel Promise fan-out with small models, and finish with one large-model reducer only when needed. +Use evidence such as ${p.bash("git diff -- .")} and ${p.bash("git log --since='24 hours ago' --name-status --pretty=format:'%h %s'")} and ${p.bash("git log --since='24 hours ago' -- src '*.md' '*.ts' '*.js' --name-only --pretty=format:'%h %s' || true")}.`, +}); + +const searchPlanPromise = planDeterministicSearch({ + query: "Where did auth, exports, and schema behavior change in the last 24h?", +}); + +const [diffShard, commitsShard, docsShard, searchPlan] = await Promise.all([ + summarizeShard({ + scenario: "git-diff-summary", + shardLabel: "diff", + evidence: p.bash("git diff -- ."), + }), + summarizeShard({ + scenario: "repo-24h-summary", + shardLabel: "commits-24h", + evidence: p.bash("git log --since='24 hours ago' --name-status --pretty=format:'%h %s'"), + }), + summarizeShard({ + scenario: "docs-exported-changes-24h", + shardLabel: "exports-and-docs", + evidence: p.bash("git log --since='24 hours ago' -- 'src/**' 'docs/**' --name-only --pretty=format:'%h %s' || true"), + }), + searchPlanPromise, +]); + +const semanticSearchShard = await summarizeShard({ + scenario: "semantic-search-pipeline", + shardLabel: "search-plan", + evidence: JSON.stringify(searchPlan), +}); + +await reduceScenario({ + scenario: "core-scenarios", + shardSummaries: [diffShard, commitsShard, docsShard, semanticSearchShard], +}); + +await summarizeAtScale({ text: p`` }); + +export default summarizeAtScale; diff --git a/src/samples/55-file-change-lint-middleware.ts b/src/samples/55-file-change-lint-middleware.ts new file mode 100644 index 0000000..ba54da9 --- /dev/null +++ b/src/samples/55-file-change-lint-middleware.ts @@ -0,0 +1,34 @@ +import { agent, s } from "rig"; +import type { AgentAddon } from "rig"; +import { $ } from "zx"; + +async function workspaceFingerprint(): Promise { + const { exitCode, stdout } = await $`git status --porcelain`.nothrow(); + return exitCode === 0 ? stdout.trim() : ""; +} + +function lintOnFileChange(runLint: () => Promise): AgentAddon { + return async (_context, next) => { + const before = await workspaceFingerprint(); + await next(); + const after = await workspaceFingerprint(); + if (before !== after) { + await runLint(); + } + }; +} + +// Agent role: apply workspace changes and trigger linting when files changed. +const fileChangeMiddleware = agent({ + model: "mini", + instructions: "Update files when needed, then summarize the change.", + output: s.object({ + changed: s.boolean, + summary: s.string, + }), + addons: lintOnFileChange(() => $`npm run typecheck`), +}); + +await fileChangeMiddleware("Inspect the workspace and apply a small fix if needed."); + +export default fileChangeMiddleware; diff --git a/src/samples/56-single-agent-sonnet.ts b/src/samples/56-single-agent-sonnet.ts new file mode 100644 index 0000000..8475848 --- /dev/null +++ b/src/samples/56-single-agent-sonnet.ts @@ -0,0 +1,17 @@ +import { agent, s } from "rig"; + +// Minimal single-agent sample used by integration tests to verify sonnet runtime execution. +// Agent role: write a single haiku about the user's topic. +const sonnetHaiku = agent({ + name: "single-agent-sonnet-haiku", + model: "claude-sonnet-4.5", + output: s.object({ + haiku: s.string, + }), + instructions: ` + Write one haiku about the user's topic. + Return exactly three short lines in the haiku field. + `, +}); + +export default sonnetHaiku; diff --git a/src/samples/57-complex-integration-sonnet.ts b/src/samples/57-complex-integration-sonnet.ts new file mode 100644 index 0000000..b047cf2 --- /dev/null +++ b/src/samples/57-complex-integration-sonnet.ts @@ -0,0 +1,65 @@ +import { agent, defineTool, p, s } from "rig"; +import { oncePerSession, steering, timeout } from "rig/addons"; + +const summarizeText = defineTool<{ text: string }>("summarize_text", { + description: "Create a concise summary from text.", + parameters: s.object({ + text: s.string, + }), + handler: async ({ text }) => { + const trimmed = text.trim(); + if (!trimmed) return "No content provided."; + return trimmed.split(/\s+/g).slice(0, 20).join(" "); + }, +}); + +const planner = agent({ + name: "complex-integration-planner", + model: "claude-haiku-4.5", + instructions: "Return 2-3 short plan steps for the provided topic.", + input: s.object({ + topic: s.string, + }), + output: s.object({ + steps: s.array(s.string), + }), +}); + +const complexIntegration = agent({ + name: "complex-integration-sonnet", + model: "claude-sonnet-4.5", + maxTurns: 4, + input: s.object({ + topic: s.string, + audience: s.optional(s.string), + }), + output: s.object({ + headline: s.string, + checklist: s.array(s.string), + riskLevel: s.enum("low", "medium", "high"), + nextActions: s.array(s.object({ + owner: s.string, + action: s.string, + })), + contextDigest: s.object({ + repository: s.string, + usedFeatures: s.array(s.string), + toolHint: s.string, + }), + }), + tools: [summarizeText], + agents: { planner }, + addons: [ + oncePerSession(async () => {}), + timeout({ timeout: 45_000 }), + steering(), + ], + instructions: p` + Build a compact execution brief for the user topic. + Use repository context from ${p.read("README.md")} and workspace state from ${p.bash("git status --short")}. + You may call planner for concise planning and summarize_text for text condensation. + Mention at least five rig features in contextDigest.usedFeatures. + `, +}); + +export default complexIntegration; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..acf139c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "allowImportingTsExtensions": true, + "skipLibCheck": true, + "noEmit": true, + "paths": { + "rig": ["./skills/rig/rig.ts"], + "rig/*": ["./src/*.ts"] + } + }, + "include": ["src", "scripts", "skills"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..553fe18 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; +import { resolve } from "path"; + +export default defineConfig({ + resolve: { + alias: [ + { find: /^rig$/, replacement: resolve(__dirname, "skills/rig/rig.ts") }, + { find: /^rig\/(.*)$/, replacement: resolve(__dirname, "src/$1") }, + ], + }, +});