From 6ddeb8445455185dc35535b2eb623ddad6d6f262 Mon Sep 17 00:00:00 2001 From: "extro.rook.host" Date: Wed, 7 Oct 2026 16:25:22 -0600 Subject: [PATCH] Recognise Pi coding-agent environments and preserve human gates --- CHANGELOG.md | 15 ++++ COMMANDS.md | 12 +-- README.md | 8 +- docs/index.html | 2 +- docs/start/index.html | 10 +-- skills/vit/SKILL.md | 2 + src/cmd/init.js | 2 +- src/cmd/learn.js | 2 +- src/cmd/remix.js | 2 +- src/cmd/ship.js | 4 +- src/cmd/skim.js | 2 +- src/lib/agent.js | 2 + test/agent.test.js | 67 +++++++++++------ test/canonical-beacon-cli-260727.test.js | 2 +- test/helpers.js | 2 +- test/pi-agent-cli.test.js | 96 ++++++++++++++++++++++++ 16 files changed, 185 insertions(+), 45 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 test/pi-agent-cli.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c74c49f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# changelog + +## [0.6.6] - 2026-10-07 + +### Fixed + +- Security fix: under Pi, `adopt` and `vet --dangerous-accept` now refuse agent execution. Regular `vet` uses the same review gate as other recognised agents. + +### Added + +- Pi can now run `init`, `skim`, `ship`, `remix`, and `learn` using its normal environment markers. + +### Known limitations + +- Pi-specific skill installation, doctor integration, and the Pi launcher for `vet --sandbox` remain unavailable. diff --git a/COMMANDS.md b/COMMANDS.md index 7d3af90..2677cea 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -5,7 +5,7 @@ full command reference for the vit CLI. for concepts and vocabulary, see [VOCAB. vit is a human+agent collaboration tool. commands are labeled by who runs them: - **you** (terminal) — commands you run directly -- **your agent** (inside Claude Code / Codex / Gemini CLI) — commands your coding agent runs +- **your agent** (inside Claude Code / Codex / Gemini CLI / opencode / Pi) — commands your coding agent runs --- @@ -46,7 +46,7 @@ checks Node.js version, git availability, login status, and (if in a repo) `.vit ### init -**your agent runs this** (inside Claude Code / Codex / Gemini CLI) +**your agent runs this** (inside Claude Code / Codex / Gemini CLI / opencode / Pi) initialize `.vit/` directory and derive the project beacon from git remotes. @@ -140,7 +140,7 @@ replays recent network activity and shows who's shipping capabilities and skills ### skim -**your agent runs this** (inside Claude Code / Codex / Gemini CLI) +**your agent runs this** (inside Claude Code / Codex / Gemini CLI / opencode / Pi) read capabilities and skills from followed accounts, filtered to your project. @@ -196,7 +196,7 @@ works for both capabilities and skills. no beacon required for skills. ### remix -**your agent runs this** (inside Claude Code / Codex / Gemini CLI) +**your agent runs this** (inside Claude Code / Codex / Gemini CLI / opencode / Pi) derive a vetted capability into the local codebase and generate an implementation plan. @@ -208,7 +208,7 @@ requires a vetted capability. creates a local remix with a structured plan, trac ### learn -**your agent runs this** (inside Claude Code / Codex / Gemini CLI) +**your agent runs this** (inside Claude Code / Codex / Gemini CLI / opencode / Pi) install a vetted skill for agent use. @@ -225,7 +225,7 @@ installs to `.claude/skills/{name}/` (project scope) or `~/.claude/skills/{name} ### ship -**your agent runs this** (inside Claude Code / Codex / Gemini CLI) +**your agent runs this** (inside Claude Code / Codex / Gemini CLI / opencode / Pi) publish a new capability or skill to the network. diff --git a/README.md b/README.md index 97810ea..84445f1 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ npm install -g vit vit login your-handle.bsky.social ``` -then open your coding agent (Claude Code, Codex CLI, or Gemini CLI) — it already knows how to use vit because installing vit auto-installs the agent skill. your agent runs `vit init` to connect your project to the network, then `vit skim` to discover what others have built. +then open your coding agent (Claude Code, Codex CLI, Gemini CLI, opencode, or Pi). vit auto-installs its skill for Claude Code, Codex CLI, and Gemini CLI. your agent runs `vit init` to connect your project to the network, then `vit skim` to discover what others have built. **[full getting started guide →](https://v-it.org/start/)** @@ -65,7 +65,11 @@ make install ## works with -vit is a human+agent collaboration tool. it works with [Claude Code](https://claude.ai/code), [Codex CLI](https://github.com/openai/codex), and [Gemini CLI](https://github.com/google-gemini/gemini-cli). some commands are for you (login, vet), others are for your agent (skim, remix, ship). +vit is a human+agent collaboration tool. it works with [Claude Code](https://claude.ai/code), [Codex CLI](https://github.com/openai/codex), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [opencode](https://opencode.ai), and [Pi](https://github.com/badlogic/pi-mono). some commands are for you (login, vet), others are for your agent (skim, remix, ship). + +Pi is recognised through `AI_AGENT=pi` or `PI_CODING_AGENT=true`. `init`, `skim`, `ship`, `remix`, and `learn` accept Pi. Pi uses the existing skill destinations; installation into Pi's own skill directory, `doctor` integration, and a Pi launcher for `vet --sandbox` are not yet available. Human-only commands keep the same gates. + +See [the changelog](CHANGELOG.md) for release notes. ## reference diff --git a/docs/index.html b/docs/index.html index 9e6473f..07015f7 100644 --- a/docs/index.html +++ b/docs/index.html @@ -313,7 +313,7 @@

a capability is a structured change instruction — what to do, why, and how to integrate it. not a diff. a social post that humans and agents can both read and remix.

a skill is a reusable agent ability that works across projects. publish one and any agent on the network can learn it. think npm for AI agent skills.

capabilities change codebases. skills teach agents. both flow through the same trust network.

-

works with Claude Code, Codex CLI, and Gemini CLI.

+

works with Claude Code, Codex CLI, Gemini CLI, opencode, and Pi.

a codebase is not a distribution artifact. a codebase is a living organism that can adapt to each install, and it deserves a living ecosystem.

the future is not “one repo, one roadmap.” the future is many codebases, each living, all sharing capabilities with each other through a social network that rewards provenance and trust.

diff --git a/docs/start/index.html b/docs/start/index.html index a0b5526..3bd10d6 100644 --- a/docs/start/index.html +++ b/docs/start/index.html @@ -293,7 +293,7 @@
  • Node.js 20+
  • git — only needed if you plan to ship caps (skills work without it)
  • a Bluesky account — vit publishes to your identity on the AT Protocol
  • -
  • a coding agent — Claude Code, Codex CLI, or Gemini CLI
  • +
  • a coding agent — Claude Code, Codex CLI, Gemini CLI, opencode, or Pi

  • @@ -301,7 +301,7 @@

    1. install

    you run this (terminal)
    npm install -g vit
    -

    installing vit auto-installs the agent skill so your coding agent knows how to use vit. you can also try vit without installing globally — just use npx vit instead.

    +

    installing vit auto-installs the agent skill for Claude Code, Codex CLI, and Gemini CLI. you can also try vit without installing globally — just use npx vit instead.

    2. log in

    you run this (terminal) @@ -326,15 +326,15 @@

    4. open your agent

    -

    open your coding agent (Claude Code, Codex, Gemini CLI). the vit skill was installed automatically, so your agent already knows how to use every vit command.

    +

    open your coding agent (Claude Code, Codex, Gemini CLI, opencode, or Pi). Pi can run the agent commands, but Pi-specific skill installation, doctor integration, and sandbox launching are not yet available.

    5. initialize your project

    -your agent runs this (inside Claude Code / Codex / Gemini CLI) +your agent runs this (inside Claude Code / Codex / Gemini CLI / opencode / Pi)
    vit init

    this creates a .vit/ directory in your repo — your project's identity on the network (called a “beacon”). other builders can now discover your project.

    6. skim the network

    -your agent runs this (inside Claude Code / Codex / Gemini CLI) +your agent runs this (inside Claude Code / Codex / Gemini CLI / opencode / Pi)
    vit skim

    your agent browses capabilities from people you follow, filtered to your project. this is how you discover what others have built that might be useful.

    diff --git a/skills/vit/SKILL.md b/skills/vit/SKILL.md index ef4a31c..904c4fa 100644 --- a/skills/vit/SKILL.md +++ b/skills/vit/SKILL.md @@ -13,6 +13,8 @@ description: >- vit is a Node CLI for social software capabilities. Agents use it to initialize projects, follow accounts, skim caps from followed accounts, and ship new caps. Some commands (login, adopt, vet) require human interaction - the agent should tell the user to run those in their terminal. +Pi is recognised through `AI_AGENT=pi` or `PI_CODING_AGENT=true`. The agent commands work under Pi, with the existing trust gates. Pi-specific skill installation, doctor integration, and the Pi sandbox launcher are not yet available. + ## 2. Prerequisites Dependency chain: `login → init → follow → skim/ship`. diff --git a/src/cmd/init.js b/src/cmd/init.js index ff4e9e4..b2d78cf 100644 --- a/src/cmd/init.js +++ b/src/cmd/init.js @@ -40,7 +40,7 @@ export default function register(program) { jsonError('agent required', 'run vit init from a coding agent'); return; } - console.error(`${name} init should be run by a coding agent (e.g. claude code, codex, gemini cli, opencode).`); + console.error(`${name} init should be run by a coding agent (e.g. claude code, codex, gemini cli, opencode, pi).`); console.error(`open your agent and ask it to run '${name} init' for you.`); process.exitCode = 1; return; diff --git a/src/cmd/learn.js b/src/cmd/learn.js index a875b74..fcf53cf 100644 --- a/src/cmd/learn.js +++ b/src/cmd/learn.js @@ -247,7 +247,7 @@ Examples: jsonError('agent required', 'run vit learn from a coding agent'); return; } - console.error(`${name} learn should be run by a coding agent (e.g. claude code, codex, gemini cli, opencode).`); + console.error(`${name} learn should be run by a coding agent (e.g. claude code, codex, gemini cli, opencode, pi).`); console.error(`open your agent and ask it to run '${name} learn' for you.`); process.exitCode = 1; return; diff --git a/src/cmd/remix.js b/src/cmd/remix.js index 10ccf59..b835ac7 100644 --- a/src/cmd/remix.js +++ b/src/cmd/remix.js @@ -31,7 +31,7 @@ export default function register(program) { jsonError('agent required', 'run vit remix from a coding agent'); return; } - console.error(`${name} remix should be run by a coding agent (e.g. claude code, codex, gemini cli, opencode).`); + console.error(`${name} remix should be run by a coding agent (e.g. claude code, codex, gemini cli, opencode, pi).`); console.error(`open your agent and ask it to run '${name} remix' for you.`); process.exitCode = 1; return; diff --git a/src/cmd/ship.js b/src/cmd/ship.js index 40643ff..7afc356 100644 --- a/src/cmd/ship.js +++ b/src/cmd/ship.js @@ -139,7 +139,7 @@ async function shipSkill(opts) { jsonError('agent required', 'run vit ship --skill from a coding agent'); return; } - console.error(`${name} ship --skill should be run by a coding agent (e.g. claude code, codex, gemini cli, opencode).`); + console.error(`${name} ship --skill should be run by a coding agent (e.g. claude code, codex, gemini cli, opencode, pi).`); console.error(`open your agent and ask it to run '${name} ship --skill' for you.`); process.exitCode = 1; return; @@ -348,7 +348,7 @@ export async function shipCap(opts) { jsonError('agent required', 'run vit ship from a coding agent'); return; } - console.error(`${name} ship should be run by a coding agent (e.g. claude code, codex, gemini cli, opencode).`); + console.error(`${name} ship should be run by a coding agent (e.g. claude code, codex, gemini cli, opencode, pi).`); console.error(`open your agent and ask it to run '${name} ship' for you.`); console.error(`refer to the using-vit skill (skills/vit/SKILL.md) for a shipping guide.`); process.exitCode = 1; diff --git a/src/cmd/skim.js b/src/cmd/skim.js index c613f24..09e2833 100644 --- a/src/cmd/skim.js +++ b/src/cmd/skim.js @@ -30,7 +30,7 @@ export default function register(program) { try { const gate = requireAgent(); if (!gate.ok) { - console.error(`${name} skim should be run by a coding agent (e.g. claude code, codex, gemini cli, opencode).`); + console.error(`${name} skim should be run by a coding agent (e.g. claude code, codex, gemini cli, opencode, pi).`); console.error(`open your agent and ask it to run '${name} skim' for you.`); process.exitCode = 1; return; diff --git a/src/lib/agent.js b/src/lib/agent.js index a71edb4..0afff89 100644 --- a/src/lib/agent.js +++ b/src/lib/agent.js @@ -12,6 +12,8 @@ export function detectCodingAgent() { for (const [envVar, name] of Object.entries(CODING_AGENTS)) { if (process.env[envVar] === '1') return { name, envVar }; } + if (process.env.AI_AGENT === 'pi') return { name: 'pi', envVar: 'AI_AGENT' }; + if (process.env.PI_CODING_AGENT === 'true') return { name: 'pi', envVar: 'PI_CODING_AGENT' }; return null; } diff --git a/test/agent.test.js b/test/agent.test.js index 3fdc0fc..b803d80 100644 --- a/test/agent.test.js +++ b/test/agent.test.js @@ -5,34 +5,19 @@ import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; import { detectCodingAgent, requireAgent, requireNotAgent } from '../src/lib/agent.js'; describe('agent', () => { - let originalClaudeCode; - let originalGeminiCli; - let originalCodexCi; - let originalOpencode; + const agentEnvVars = ['CLAUDECODE', 'GEMINI_CLI', 'CODEX_CI', 'OPENCODE', 'AI_AGENT', 'PI_CODING_AGENT']; + let originalEnv; beforeEach(() => { - originalClaudeCode = process.env.CLAUDECODE; - originalGeminiCli = process.env.GEMINI_CLI; - originalCodexCi = process.env.CODEX_CI; - originalOpencode = process.env.OPENCODE; - delete process.env.CLAUDECODE; - delete process.env.GEMINI_CLI; - delete process.env.CODEX_CI; - delete process.env.OPENCODE; + originalEnv = Object.fromEntries(agentEnvVars.map(key => [key, process.env[key]])); + for (const key of agentEnvVars) delete process.env[key]; }); afterEach(() => { - if (originalClaudeCode === undefined) delete process.env.CLAUDECODE; - else process.env.CLAUDECODE = originalClaudeCode; - - if (originalGeminiCli === undefined) delete process.env.GEMINI_CLI; - else process.env.GEMINI_CLI = originalGeminiCli; - - if (originalCodexCi === undefined) delete process.env.CODEX_CI; - else process.env.CODEX_CI = originalCodexCi; - - if (originalOpencode === undefined) delete process.env.OPENCODE; - else process.env.OPENCODE = originalOpencode; + for (const key of agentEnvVars) { + if (originalEnv[key] === undefined) delete process.env[key]; + else process.env[key] = originalEnv[key]; + } }); describe('detectCodingAgent', () => { @@ -76,6 +61,42 @@ describe('agent', () => { }); }); + describe('Pi markers', () => { + for (const [envVar, value] of [['AI_AGENT', 'pi'], ['PI_CODING_AGENT', 'true']]) { + test(`${envVar}=${value} permits agent work and refuses human-only work`, () => { + process.env[envVar] = value; + expect(detectCodingAgent()).toEqual({ name: 'pi', envVar }); + expect(requireAgent()).toEqual({ ok: true, name: 'pi', envVar }); + expect(requireNotAgent()).toEqual({ ok: false, name: 'pi', envVar }); + }); + } + + test('recognises both Pi markers together', () => { + process.env.AI_AGENT = 'pi'; + process.env.PI_CODING_AGENT = 'true'; + expect(detectCodingAgent()).toEqual({ name: 'pi', envVar: 'AI_AGENT' }); + }); + + for (const [envVar, values] of [ + ['AI_AGENT', ['', 'other-agent', 'Pi', 'true', '1']], + ['PI_CODING_AGENT', ['', 'false', '0', '1', 'TRUE']], + ]) { + for (const value of values) { + test(`does not recognise ${envVar}=${JSON.stringify(value)}`, () => { + process.env[envVar] = value; + expect(detectCodingAgent()).toBe(null); + }); + } + } + + test('preserves existing agent precedence in inherited Pi environments', () => { + process.env.CODEX_CI = '1'; + process.env.AI_AGENT = 'pi'; + process.env.PI_CODING_AGENT = 'true'; + expect(detectCodingAgent()).toEqual({ name: 'codex', envVar: 'CODEX_CI' }); + }); + }); + describe('requireAgent', () => { test('returns ok true when agent detected', () => { process.env.CLAUDECODE = '1'; diff --git a/test/canonical-beacon-cli-260727.test.js b/test/canonical-beacon-cli-260727.test.js index 87e91a5..2760a0c 100644 --- a/test/canonical-beacon-cli-260727.test.js +++ b/test/canonical-beacon-cli-260727.test.js @@ -7,7 +7,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; const vitBin = join(import.meta.dir, '..', 'bin', 'vit.js'); -const cleanAgentEnv = { CLAUDECODE: '', GEMINI_CLI: '', CODEX_CI: '', OPENCODE: '' }; +const cleanAgentEnv = { CLAUDECODE: '', GEMINI_CLI: '', CODEX_CI: '', OPENCODE: '', AI_AGENT: '', PI_CODING_AGENT: '' }; async function runVit(args, cwd, env = {}) { const proc = Bun.spawn(['bun', vitBin, ...args], { diff --git a/test/helpers.js b/test/helpers.js index 0410fe4..431ac08 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -5,7 +5,7 @@ import { join } from 'node:path'; import { spawnSync } from 'node:child_process'; const vitBin = join(import.meta.dir, '..', 'bin', 'vit.js'); -const nonAgentEnv = { CLAUDECODE: '', GEMINI_CLI: '', CODEX_CI: '', OPENCODE: '' }; +const nonAgentEnv = { CLAUDECODE: '', GEMINI_CLI: '', CODEX_CI: '', OPENCODE: '', AI_AGENT: '', PI_CODING_AGENT: '' }; export function run(args, cwd, env, input) { const result = spawnSync(`bun ${vitBin} ${args}`, { diff --git a/test/pi-agent-cli.test.js b/test/pi-agent-cli.test.js new file mode 100644 index 0000000..79e99cf --- /dev/null +++ b/test/pi-agent-cli.test.js @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 sol pbc + +import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { run } from './helpers.js'; + +for (const [label, marker] of [ + ['AI_AGENT', { AI_AGENT: 'pi' }], + ['PI_CODING_AGENT', { PI_CODING_AGENT: 'true' }], + ['both markers', { AI_AGENT: 'pi', PI_CODING_AGENT: 'true' }], +]) { + describe(`Pi CLI boundary via ${label}`, () => { + let dir; + let env; + + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), '.test-pi-cli-')); + const home = join(dir, 'home'); + mkdirSync(home); + env = { HOME: home, XDG_CONFIG_HOME: join(home, '.config'), ...marker }; + }); + + afterEach(() => { + rmSync(dir, { recursive: true, force: true }); + }); + + test('init writes the project beacon', () => { + const result = run('init --beacon https://github.com/example/project', dir, env); + expect(result.exitCode).toBe(0); + expect(JSON.parse(readFileSync(join(dir, '.vit', 'config.json'), 'utf-8')).beacon) + .toBe('vit:github.com/example/project'); + }); + + for (const command of [ + 'skim', + 'ship --title test --description test --ref pi-agent-test', + 'remix pi-agent-test', + ]) { + test(`${command.split(' ')[0]} reaches ordinary identity validation`, () => { + const result = run(command, dir, env, 'test capability body'); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('no DID configured'); + }); + } + + test('learn preserves vetting and then reaches identity validation', () => { + const untrusted = run('learn skill-pi-test', dir, env); + expect(untrusted.exitCode).toBe(1); + expect(untrusted.stderr).toContain('not yet vetted'); + mkdirSync(join(dir, '.vit')); + writeFileSync(join(dir, '.vit', 'trusted.jsonl'), JSON.stringify({ ref: 'skill-pi-test' }) + '\n'); + const trusted = run('learn skill-pi-test', dir, env); + expect(trusted.exitCode).toBe(1); + expect(trusted.stderr).toContain('no DID configured'); + }); + + test('adopt refuses before cloning', () => { + const result = run('adopt invalid-beacon', dir, env); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('must be run by a human'); + expect(existsSync(join(dir, 'project'))).toBe(false); + }); + + test('regular vet refuses agent review', () => { + const result = run('vet pi-agent-test', dir, env); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('agents should not vet directly'); + }); + + for (const format of ['', ' --json']) { + test(`dangerous-accept refuses even with confirm${format}`, () => { + mkdirSync(join(dir, '.vit')); + const result = run(`vet --dangerous-accept --confirm${format}`, dir, env); + expect(result.exitCode).toBe(1); + if (format) expect(JSON.parse(result.stdout).error).toBe('dangerous-accept is human-only'); + else expect(result.stderr).toContain('human-only'); + expect(existsSync(join(dir, '.vit', 'dangerous-accept'))).toBe(false); + }); + } + + test('vet keeps its explicit isolated-review override', () => { + const result = run('vet pi-agent-test --trust --confirm', dir, env); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('no DID configured'); + }); + + test('sandbox auto-detection reports the deferred Pi mapping', () => { + const result = run('vet pi-agent-test --sandbox', dir, env); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain("detected agent 'pi' has no sandbox mapping"); + }); + }); +} -- 2.51.2