Add Veto harness support (#675)

Adds Veto detection, provider transforms, installation paths, documentation, and regression coverage.\n\nAI-assisted maintainer repair, review, and validation by Codex under maintainer direction.
This commit is contained in:
oleg koval
2026-08-31 19:52:46 -04:00
committed by GitHub
parent 405998ad51
commit f3df3ffe40
9 changed files with 142 additions and 13 deletions
+3 -2
View File
@@ -103,7 +103,7 @@ From the root of your project, run:
npx impeccable install
```
This shows the harness folders it detected (for example `~/.claude`, `~/.codex`, `~/.grok`, `~/.hermes`, or project-local `.cursor`), lets you keep the detected set or customize providers, then asks whether to install into the current project or globally. Use `--providers=claude,codex,cursor,grok` and `--scope=project|global` to skip those choices in scripts. On Claude Code, Cursor, Codex, GitHub Copilot, and Grok Build, it also installs the provider-native hook manifest for the current project. Works with Cursor, Claude Code, Gemini CLI, Codex CLI, Grok Build, and every other supported tool. Reload your harness afterward.
This shows the harness folders or installed CLIs it detected (for example `~/.claude`, `~/.codex`, `~/.grok`, `~/.hermes`, `~/.veto`, or project-local `.cursor`), lets you keep the detected set or customize providers, then asks whether to install into the current project or globally. Use `--providers=claude,codex,cursor,grok,hermes,veto` and `--scope=project|global` to skip those choices in scripts. On Claude Code, Cursor, Codex, GitHub Copilot, and Grok Build, it also installs the provider-native hook manifest for the current project. Veto receives the packaged skill under `~/.veto/skills/` and does not run native Impeccable edit hooks. Works with Cursor, Claude Code, Gemini CLI, Codex CLI, Grok Build, Hermes Agent, Veto, and every other supported tool. Reload your harness afterward.
To refresh an existing install, run:
@@ -126,7 +126,7 @@ git add .gitmodules .impeccable .claude .cursor
git commit -m "Add Impeccable skills"
```
Use the providers your project needs, for example `claude`, `cursor`, `gemini`, `codex`, `github`, `grok`, `hermes`, `opencode`, `pi`, `qoder`, `trae`, `trae-cn`, `rovo-dev`, or `vibe`. The command links individual skill folders from `.impeccable/dist/universal/` and leaves existing real skill directories untouched unless you pass `--force`.
Use the providers your project needs, for example `claude`, `cursor`, `gemini`, `codex`, `github`, `grok`, `hermes`, `opencode`, `pi`, `qoder`, `trae`, `trae-cn`, `rovo-dev`, `vibe`, or `veto`. The command links individual skill folders from `.impeccable/dist/universal/` and leaves existing real skill directories untouched unless you pass `--force`.
To update later:
@@ -449,6 +449,7 @@ Full detector docs: [impeccable.style/docs/detector](https://impeccable.style/do
- [Rovo Dev](https://www.atlassian.com/software/rovo)
- [Qoder](https://qoder.com)
- [Mistral Vibe](https://docs.mistral.ai/vibe/code/overview)
- [Veto](https://github.com/oleg-koval/veto)
- [Google Antigravity](https://antigravity.google)
## Community & Ecosystem
+1 -1
View File
@@ -81,7 +81,7 @@ impeccable detect [options] [file-or-dir-or-url...]
## Part of Impeccable
This CLI is part of [Impeccable](https://impeccable.style), a cross-provider design skill pack for AI-powered development tools. The full suite includes 23 commands for Claude, Cursor, GitHub Copilot, Gemini, Codex, Hermes Agent, and more.
This CLI is part of [Impeccable](https://impeccable.style), a cross-provider design skill pack for AI-powered development tools. The full suite includes 23 commands for Claude, Cursor, GitHub Copilot, Gemini, Codex, Hermes Agent, Veto, and more.
## License
+39 -9
View File
@@ -9,8 +9,8 @@
*/
import { execSync } from 'node:child_process';
import { existsSync, readFileSync, readdirSync, statSync, lstatSync, unlinkSync, mkdirSync, mkdtempSync, writeFileSync, rmSync, rmdirSync, renameSync, createWriteStream, realpathSync, symlinkSync, readlinkSync, cpSync } from 'node:fs';
import { join, resolve, dirname, relative, isAbsolute, sep } from 'node:path';
import { existsSync, readFileSync, readdirSync, statSync, accessSync, constants, lstatSync, unlinkSync, mkdirSync, mkdtempSync, writeFileSync, rmSync, rmdirSync, renameSync, createWriteStream, realpathSync, symlinkSync, readlinkSync, cpSync } from 'node:fs';
import { join, resolve, dirname, relative, isAbsolute, sep, delimiter } from 'node:path';
import { createInterface, emitKeypressEvents } from 'node:readline';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
@@ -24,7 +24,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
const API_BASE = 'https://impeccable.style';
// Provider folder names in project roots
const PROVIDER_DIRS = ['.claude', '.cursor', '.gemini', '.agents', '.agent', '.github', '.grok', '.hermes', '.kiro', '.opencode', '.pi', '.qoder', '.trae', '.trae-cn', '.rovodev', '.vibe'];
const PROVIDER_DIRS = ['.claude', '.cursor', '.gemini', '.agents', '.agent', '.github', '.grok', '.hermes', '.kiro', '.opencode', '.pi', '.qoder', '.trae', '.trae-cn', '.rovodev', '.vibe', '.veto'];
const PROVIDER_ALIASES = {
agent: '.agent',
agents: '.agents',
@@ -49,6 +49,7 @@ const PROVIDER_ALIASES = {
trae: '.trae',
'trae-cn': '.trae-cn',
vibe: '.vibe',
veto: '.veto',
};
const PROVIDER_DISPLAY = {
@@ -68,8 +69,9 @@ const PROVIDER_DISPLAY = {
'.trae': { name: 'Trae', input: 'trae' },
'.trae-cn': { name: 'Trae CN', input: 'trae-cn' },
'.vibe': { name: 'Mistral Vibe', input: 'vibe' },
'.veto': { name: 'Veto', input: 'veto' },
};
const PROVIDER_INPUT_ORDER = ['antigravity', 'claude', 'codex', 'cursor', 'gemini', 'github', 'grok', 'hermes', 'kiro', 'opencode', 'pi', 'qoder', 'trae', 'trae-cn', 'rovo-dev', 'vibe'];
const PROVIDER_INPUT_ORDER = ['antigravity', 'claude', 'codex', 'cursor', 'gemini', 'github', 'grok', 'hermes', 'kiro', 'opencode', 'pi', 'qoder', 'trae', 'trae-cn', 'rovo-dev', 'vibe', 'veto'];
// OpenCode reads global skills from its config directory, not ~/.opencode:
// $OPENCODE_CONFIG_DIR, else $XDG_CONFIG_HOME/opencode, else
@@ -164,6 +166,10 @@ const GLOBAL_HARNESS_HINTS = [
{ home: '.qoder', provider: '.qoder' },
{ home: '.rovodev', provider: '.rovodev' },
{ home: '.vibe', provider: '.vibe' },
// Veto is a CLI harness whose managed skill directory is ~/.veto/skills.
// Require its managed state directory as well as the executable so an
// unrelated `veto` binary on PATH does not change project install defaults.
{ command: 'veto', provider: '.veto' },
];
// Last-resort default when nothing is detected: Claude Code + the universal
@@ -666,7 +672,7 @@ async function copyOrExtractLocalBundle(sourceValue) {
*/
function normalizeForHash(content) {
return content
.replace(/\.(claude|cursor|agents|agent|github|gemini|codex|grok|hermes|kiro|opencode|pi|qoder|trae|trae-cn|rovodev|vibe)\/skills\//g, '.PROVIDER/skills/');
.replace(/\.(claude|cursor|agents|agent|github|gemini|codex|grok|hermes|kiro|opencode|pi|qoder|trae|trae-cn|rovodev|vibe|veto)\/skills\//g, '.PROVIDER/skills/');
}
function hashSkillFile(filePath) {
@@ -932,6 +938,25 @@ function userSkillProbePaths(home, harnessDir, provider) {
]);
}
function commandOnPath(command) {
const candidates = process.platform === 'win32'
? [`${command}.exe`, `${command}.cmd`, `${command}.bat`]
: [command];
for (const directory of String(process.env.PATH || '').split(delimiter)) {
if (!directory) continue;
for (const candidate of candidates) {
const path = resolve(directory, candidate);
try {
if (statSync(path).isFile()) {
accessSync(path, constants.X_OK);
return path;
}
} catch {}
}
}
return null;
}
function collectInstallDetections(root, home = homedir()) {
const detections = [];
for (const provider of PROVIDER_DIRS) {
@@ -952,9 +977,14 @@ function collectInstallDetections(root, home = homedir()) {
const { provider } = hint;
// A hint is either a fixed dir under home or a resolver for harnesses
// whose location depends on the environment (OpenCode's config dir).
const foundPath = hint.resolve ? hint.resolve(home) : join(home, hint.home);
if (!existsSync(foundPath)) continue;
const skillProbePaths = hint.resolve
const foundPath = hint.command
? commandOnPath(hint.command)
: hint.resolve ? hint.resolve(home) : join(home, hint.home);
if (!foundPath || (!hint.command && !existsSync(foundPath))) continue;
if (hint.command && !existsSync(join(home, '.veto'))) continue;
const skillProbePaths = hint.command
? [userProviderSkillsDir(home, provider)]
: hint.resolve
? uniquePaths([userProviderSkillsDir(home, provider), join(foundPath, 'skills')])
: userSkillProbePaths(home, hint.home, provider);
detections.push({
@@ -965,7 +995,7 @@ function collectInstallDetections(root, home = homedir()) {
installPath: userProviderSkillsDir(home, provider),
skillProbePaths,
hasRealSkills: skillProbePaths.some(hasRealSkillEntries),
reason: 'user harness folder',
reason: hint.command ? 'CLI on PATH' : 'user harness folder',
});
}
return detections;
+1
View File
@@ -18,6 +18,7 @@ export const FILE_DOWNLOAD_PROVIDER_CONFIG_DIRS = Object.freeze({
pi: '.pi',
qoder: '.qoder',
vibe: '.vibe',
veto: '.veto',
});
export const FILE_DOWNLOAD_PROVIDERS = Object.freeze(
+2
View File
@@ -10,6 +10,7 @@
* - Codex: dist/codex/ only (OpenAI-metadata bundle; not synced to repo root)
* - Agents: .agents/skills/ (Codex repo/user installs)
* - GitHub: .github/skills/ (GitHub Copilot)
* - Veto: .veto/skills/ (Veto model-routing harness)
*
* Also assembles a universal ZIP containing all providers,
* and builds Tailwind CSS for production deployment.
@@ -550,6 +551,7 @@ This folder contains skills for all supported tools:
.trae/ -> Trae International
.rovodev/ -> Rovo Dev
.vibe/ -> Mistral Vibe
.veto/ -> Veto model-routing harness
.qoder/ -> Qoder
To install, copy the relevant folder(s) into your project root.
+1
View File
@@ -19,5 +19,6 @@ export const transformVibe = createTransformer(PROVIDERS.vibe);
export const transformGrok = createTransformer(PROVIDERS.grok);
export const transformAntigravity = createTransformer(PROVIDERS.antigravity);
export const transformHermes = createTransformer(PROVIDERS.hermes);
export const transformVeto = createTransformer(PROVIDERS.veto);
export { createTransformer, PROVIDERS };
+7
View File
@@ -138,6 +138,13 @@ export const PROVIDERS = {
displayName: 'Mistral Vibe',
frontmatterFields: ['user-invocable', 'license', 'compatibility', 'metadata', 'allowed-tools'],
},
veto: {
provider: 'veto',
providerTags: ['veto'],
configDir: '.veto',
displayName: 'Veto',
frontmatterFields: ['license', 'compatibility', 'metadata'],
},
grok: {
provider: 'grok',
providerTags: ['grok'],
+7
View File
@@ -526,6 +526,12 @@ export const PROVIDER_PLACEHOLDERS = {
ask_instruction: 'Ask the user directly to clarify what you cannot infer.',
command_prefix: '/'
},
veto: {
model: 'the selected model',
config_file: '~/.veto/config.json',
ask_instruction: 'Ask the user directly to clarify what you cannot infer.',
command_prefix: '/',
},
'vibe': {
model: 'Mistral',
config_file: 'AGENTS.md',
@@ -574,6 +580,7 @@ export const PROVIDER_BLOCK_TAGS = new Set([
'trae',
'trae-cn',
'vibe',
'veto',
]);
/**
+81 -1
View File
@@ -11,10 +11,11 @@
*/
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { execSync, execFileSync } from 'child_process';
import { mkdtempSync, existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, rmSync, lstatSync, realpathSync, readlinkSync, symlinkSync, statSync } from 'fs';
import { mkdtempSync, existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, chmodSync, rmSync, lstatSync, realpathSync, readlinkSync, symlinkSync, statSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import {
collectInstallDetections,
copyProviderAgents,
copyProviderHooks,
copyProviderSkills,
@@ -910,6 +911,85 @@ describe('skills install/update: local universal bundle e2e', () => {
rmSync(home, { recursive: true, force: true });
});
test('detects an installed Veto CLI and targets its managed skill directory', () => {
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-veto-detect-'));
const home = mkdtempSync(join(tmpdir(), 'imp-home-veto-detect-'));
const bin = join(tmp, 'bin');
mkdirSync(bin, { recursive: true });
mkdirSync(join(home, '.veto'), { recursive: true });
writeFileSync(join(bin, 'veto'), '#!/bin/sh\n');
const previousPath = process.env.PATH;
process.env.PATH = bin;
try {
expect(collectInstallDetections(tmp, home).some((detection) => detection.provider === '.veto')).toBe(false);
chmodSync(join(bin, 'veto'), 0o755);
const detections = collectInstallDetections(tmp, home);
const veto = detections.find((detection) => detection.provider === '.veto');
expect(veto).toEqual(expect.objectContaining({
provider: '.veto',
scope: 'user',
foundPath: join(bin, 'veto'),
installPath: join(home, '.veto', 'skills'),
reason: 'CLI on PATH',
}));
} finally {
process.env.PATH = previousPath;
rmSync(tmp, { recursive: true, force: true });
rmSync(home, { recursive: true, force: true });
}
});
test('does not detect an extensionless Veto file on Windows', () => {
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-veto-win-detect-'));
const home = mkdtempSync(join(tmpdir(), 'imp-home-veto-win-detect-'));
const bin = join(tmp, 'bin');
mkdirSync(bin, { recursive: true });
mkdirSync(join(home, '.veto'), { recursive: true });
writeFileSync(join(bin, 'veto'), '#!/bin/sh\n');
const previousPath = process.env.PATH;
const originalPlatform = process.platform;
process.env.PATH = bin;
try {
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
expect(collectInstallDetections(tmp, home).some((detection) => detection.provider === '.veto')).toBe(false);
const windowsCommand = join(bin, 'veto.cmd');
writeFileSync(windowsCommand, '@echo off\r\n');
chmodSync(windowsCommand, 0o755);
const detections = collectInstallDetections(tmp, home);
expect(detections.find((detection) => detection.provider === '.veto')).toEqual(expect.objectContaining({
foundPath: windowsCommand,
reason: 'CLI on PATH',
}));
} finally {
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
process.env.PATH = previousPath;
rmSync(tmp, { recursive: true, force: true });
rmSync(home, { recursive: true, force: true });
}
});
test('installs Veto skills into its global managed directory', () => {
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-veto-install-'));
const home = mkdtempSync(join(tmpdir(), 'imp-home-veto-install-'));
execSync('git init', { cwd: tmp });
const bundleRoot = createFakeUniversalBundle(tmp, ['.veto']);
const output = run('install -y --providers=veto --scope=global --no-hooks', {
cwd: tmp,
env: { ...process.env, HOME: home, IMPECCABLE_BUNDLE_PATH: bundleRoot },
});
expect(output).toContain('Installed impeccable into: .veto (global)');
expect(existsSync(join(home, '.veto', 'skills', 'impeccable', 'SKILL.md'))).toBe(true);
rmSync(tmp, { recursive: true, force: true });
rmSync(home, { recursive: true, force: true });
}, 15000);
test('installs provider-specific skills into a fresh project', () => {
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-local-install-'));
execSync('git init', { cwd: tmp });