Compare commits

...
Author SHA1 Message Date
TabishB 1f9d39c327 chore: narrow invocation work into unify pipeline proposal 2026-02-21 17:13:06 -08:00
TabishB c9bc915f62 fix: unify tool command reference rendering across generation 2026-02-21 16:53:05 -08:00
e4c32dbe07 feat: add support for Pi (pi.dev) coding agent (#735)
* feat: add support for Pi (pi.dev) coding agent

Add Pi as a supported tool in OpenSpec with full adapter implementation.

Changes:
- Create pi.ts adapter for command generation
- Register adapter in registry and export from index
- Add Pi to AI_TOOLS config with .pi skills directory
- Add tests for piAdapter following existing patterns
- Update supported-tools.md documentation

Pi uses:
- Skills: .pi/skills/ (Agent Skills standard)
- Prompts: .pi/prompts/*.md (with description frontmatter)

Closes #732

* fix: add Pi to LEGACY_SLASH_COMMAND_PATHS for test compliance

* style: add trailing newline to pi.ts

* fix: correct legacy cleanup pattern for Pi (opsx-*.md not openspec-*.md)

* fix: add YAML escaping for Pi adapter to handle special characters in descriptions

- Add escapeYamlValue() function to properly escape YAML special characters
- Apply escaping to description field in frontmatter
- Add tests for YAML special character escaping (colons, quotes, newlines)

This follows the same pattern used by cursor, claude, and windsurf adapters.

* fix: remove Pi from LEGACY_SLASH_COMMAND_PATHS

Pi was never supported in pre-1.0 versions, so no legacy cleanup is needed.
Per reviewer feedback: this is only for tools from pre-1.0 OpenSpec.

* test: relax legacy-cleanup registry coverage invariant

---------

Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
Co-authored-by: TabishB <tabishbidiwale@gmail.com>
2026-02-21 15:34:40 -08:00
11 changed files with 5143 additions and 32 deletions
+2 -1
View File
@@ -31,6 +31,7 @@ For each tool you select, OpenSpec installs:
| Kilo Code | `.kilocode/skills/` | `.kilocode/workflows/` |
| Kiro | `.kiro/skills/` | `.kiro/prompts/` |
| OpenCode | `.opencode/skills/` | `.opencode/command/` |
| Pi | `.pi/skills/` | `.pi/prompts/` |
| Qoder | `.qoder/skills/` | `.qoder/commands/opsx/` |
| Qwen Code | `.qwen/skills/` | `.qwen/commands/` |
| RooCode | `.roo/skills/` | `.roo/commands/` |
@@ -56,7 +57,7 @@ openspec init --tools all
openspec init --tools none
```
**Available tool IDs:** `amazon-q`, `antigravity`, `auggie`, `claude`, `cline`, `codebuddy`, `codex`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `kilocode`, `kiro`, `opencode`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf`
**Available tool IDs:** `amazon-q`, `antigravity`, `auggie`, `claude`, `cline`, `codebuddy`, `codex`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `kilocode`, `kiro`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf`
## What Gets Installed
@@ -15,19 +15,21 @@ The design goal is to preserve current behavior while making extension points ex
- Define one canonical source for workflow content and metadata
- Make tool/agent-specific behavior explicit and centrally discoverable
- Keep command adapters as the formatting boundary for tool syntax differences
- Represent tool-specific command surfaces and terminology explicitly (not as scattered string rewrites)
- Consolidate artifact generation/write orchestration into one reusable engine
- Improve correctness with enforceable validation and parity tests
**Non-Goals:**
- Redesigning command semantics or workflow instruction content
- Changing user-facing CLI command names/flags in this proposal
- Guaranteeing fully accurate literal slash-command strings for every supported tool on day one
- Merging unrelated legacy cleanup behavior beyond artifact generation reuse
## Decisions
### 1. Canonical `WorkflowManifest`
**Decision**: Represent each workflow once in a manifest entry containing canonical skill and command definitions plus metadata defaults.
**Decision**: Represent each workflow once in a manifest entry containing canonical skill and command definitions plus metadata defaults. Canonical text uses semantic tokens for tool-specific references.
Suggested shape:
@@ -41,6 +43,12 @@ interface WorkflowManifestEntry {
tags: string[];
compatibility: string;
}
// Examples in canonical workflow text:
// - {{cmd.apply}}
// - {{cmd.continue.withArg}}
// - {{term.change}}
// - {{term.workflow}}
```
**Rationale**:
@@ -50,7 +58,7 @@ interface WorkflowManifestEntry {
### 2. `ToolProfileRegistry` for capability wiring
**Decision**: Add a tool profile layer that maps tool IDs to generation capabilities and behavior.
**Decision**: Add a tool profile layer that maps tool IDs to generation capabilities, command-surface rendering, and terminology.
Suggested shape:
@@ -59,6 +67,16 @@ interface ToolProfile {
toolId: string;
skillsDir?: string;
commandAdapterId?: string;
commandSurface: {
pattern: 'opsx-colon' | 'opsx-hyphen' | 'opsx-slash' | 'openspec-hyphen' | 'custom';
verified: boolean;
aliases?: string[];
};
terminology: {
change: string;
workflow: string;
command: string;
};
transforms: string[];
}
```
@@ -67,10 +85,12 @@ interface ToolProfile {
- Prevents capability drift between `AI_TOOLS`, adapter registry, and detection logic
- Allows intentional "skills-only" tools without implicit special casing
- Provides one place to answer "what does this tool support?"
- Makes command rendering decisions explicit and testable
- Supports future terminology tailoring without copy/paste template forks
### 3. First-class transform pipeline
**Decision**: Model transforms as ordered plugins with scope + phase + applicability.
**Decision**: Model transforms as ordered plugins with scope + phase + applicability. Include token rendering in the transform pipeline instead of hardcoding literal command strings in templates.
Suggested shape:
@@ -87,17 +107,32 @@ interface ArtifactTransform {
Execution order:
1. Render canonical content from manifest
2. Apply matching `preAdapter` transforms
3. For commands, run adapter formatting
4. Apply matching `postAdapter` transforms
5. Validate and write
2. Apply token-render transform (`{{cmd.*}}`, `{{term.*}}`) using tool profile
3. Apply matching `preAdapter` transforms
4. For commands, run adapter formatting
5. Apply matching `postAdapter` transforms
6. Validate and write
**Rationale**:
- Keeps adapters focused on tool formatting, not scattered behavioral rewrites
- Makes agent-specific modifications explicit and testable
- Replaces ad-hoc transform calls in `init`/`update`
- Enables neutral fallback rendering when a tool profile is not verified for literal command syntax
### 4. Shared `ArtifactSyncEngine`
### 4. Fallback policy for unverified command surfaces
**Decision**: When a tool profile has `commandSurface.verified === false`, command tokens SHALL render to neutral workflow guidance instead of literal slash-command strings.
Examples:
- Literal (verified): `Run {{cmd.apply}}`
- Neutral (unverified): `Run the Apply workflow` or `use the apply skill`
**Rationale**:
- Prevents confidently wrong guidance in generated artifacts
- Allows incremental tool-surface verification without blocking rollout
- Keeps templates stable while rendering policy evolves
### 5. Shared `ArtifactSyncEngine`
**Decision**: Introduce a single orchestration engine used by all generation entry points.
@@ -112,13 +147,15 @@ Responsibilities:
- Enables dry-run and future preview features without re-implementing logic
- Improves reliability of updates and legacy migrations
### 5. Validation + parity guardrails
### 6. Validation + parity guardrails
**Decision**: Add strict checks in tests (and optional runtime assertions in dev builds) for:
- Required skill metadata fields (`license`, `compatibility`, `metadata`) present for all manifest entries
- Projection consistency (skills, commands, detection names derived from manifest)
- Tool profile consistency (adapter existence, expected capabilities)
- Token coverage checks (no unresolved `{{...}}` tokens in rendered outputs)
- Tool command-surface verification matrix and fallback expectations
- Golden/parity output for key workflows/tools
**Rationale**:
@@ -143,7 +180,8 @@ Adding manifest/profile/transform registries increases conceptual surface area.
## Implementation Approach
1. Build manifest + profile + transform types and registries behind current public API
2. Rewire `getSkillTemplates`/`getCommandContents` to derive from manifest
3. Introduce `ArtifactSyncEngine` and switch `init` to use it with parity checks
4. Switch `update` and legacy upgrade flows to same engine
5. Remove duplicate/hardcoded lists after parity is green
2. Tokenize command/terminology references in workflow templates
3. Rewire `getSkillTemplates`/`getCommandContents` to derive from manifest
4. Introduce `ArtifactSyncEngine` and switch `init` to use it with parity checks
5. Switch `update` and legacy upgrade flows to same engine
6. Remove duplicate/hardcoded lists after parity is green
@@ -4,7 +4,8 @@ The recent split of `skill-templates.ts` into workflow modules improved readabil
- Workflow definitions are split from projection logic (`getSkillTemplates`, `getCommandTemplates`, `getCommandContents`)
- Tool capability and compatibility are spread across `AI_TOOLS`, `CommandAdapterRegistry`, and hardcoded lists like `SKILL_NAMES`
- Agent/tool-specific transformations (for example OpenCode command reference rewrites) are applied in different places (`init`, `update`, and adapter code)
- Agent/tool-specific transformations are applied in different places (`init`, `update`, and adapter code)
- Command and terminology references are currently hardcoded in workflow text, but tool invocation surfaces vary (`/opsx:apply`, `/opsx-apply`, `/opsx/apply`, and tool-specific naming)
- Artifact writing logic is duplicated across `init`, `update`, and legacy-upgrade flow
This fragmentation creates drift risk (missing exports, missing metadata parity, mismatched counts/support) and makes future workflow/tool additions slower and less predictable.
@@ -15,13 +16,16 @@ This fragmentation creates drift risk (missing exports, missing metadata parity,
- Introduce a `ToolProfileRegistry` to centralize tool capabilities (skills path, command adapter, transforms)
- Introduce a first-class transform pipeline with explicit phases (`preAdapter`, `postAdapter`) and scopes (`skill`, `command`, `both`)
- Introduce a shared `ArtifactSyncEngine` used by `init`, `update`, and legacy upgrade paths
- Add tokenized workflow text rendering so command references and tool terminology are resolved per tool profile at generation time
- Add explicit command-surface profiles per tool (pattern, namespace/path style, alias support, verification status)
- Add safe fallback behavior: when a tool command surface is not verified, render neutral workflow guidance (for example skill/workflow names) instead of potentially wrong literal command strings
- Add strict validation and test guardrails to preserve fidelity during migration and future changes
## Capabilities
### New Capabilities
- `template-artifact-pipeline`: Unified workflow manifest, tool profile registry, transform pipeline, and sync engine for skill/command generation
- `template-artifact-pipeline`: Unified workflow manifest, tool profile registry, token-aware transform pipeline, and sync engine for skill/command generation
### Modified Capabilities
@@ -41,7 +45,9 @@ This fragmentation creates drift risk (missing exports, missing metadata parity,
- **Testing additions**:
- Manifest completeness tests (workflows, required metadata, projection parity)
- Transform ordering and applicability tests
- Tool command-surface/terminology profile validation tests
- End-to-end parity tests for generated skill/command outputs across tools
- **User-facing behavior**:
- No new CLI surface area required
- Existing generated artifacts remain behaviorally equivalent unless explicitly changed in future deltas
- Generated text may become more tool-accurate for verified tool profiles
- Generated text may intentionally use neutral workflow wording for unverified tools to avoid incorrect slash-command guidance
@@ -10,14 +10,18 @@
- [ ] 2.1 Add `ToolProfile` types and `ToolProfileRegistry`
- [ ] 2.2 Map all currently supported tools to explicit profile entries
- [ ] 2.3 Wire profile lookups to command adapter resolution and skills path resolution
- [ ] 2.4 Replace hardcoded detection arrays (for example `SKILL_NAMES`) with manifest-derived values
- [ ] 2.4 Add per-tool `commandSurface` metadata (pattern, aliases, `verified` flag)
- [ ] 2.5 Add per-tool terminology metadata (for example change/workflow/command labels)
- [ ] 2.6 Replace hardcoded detection arrays (for example `SKILL_NAMES`) with manifest-derived values
## 3. Transform Pipeline
- [ ] 3.1 Introduce transform interfaces (`scope`, `phase`, `priority`, `applies`, `transform`)
- [ ] 3.2 Implement transform runner with deterministic ordering
- [ ] 3.3 Migrate OpenCode command reference rewrite to transform pipeline
- [ ] 3.4 Remove ad-hoc transform invocation from `init` and `update`
- [ ] 3.3 Add token renderer transform for command + terminology tokens (`{{cmd.*}}`, `{{term.*}}`)
- [ ] 3.4 Implement neutral fallback rendering for tools with unverified command surfaces
- [ ] 3.5 Migrate OpenCode command reference rewrite to transform pipeline
- [ ] 3.6 Remove ad-hoc transform invocation from `init` and `update`
## 4. Artifact Sync Engine
@@ -29,10 +33,12 @@
## 5. Validation and Tests
- [ ] 5.1 Add manifest completeness tests (metadata required fields, command IDs, dir names)
- [ ] 5.2 Add tool-profile consistency tests (skillsDir support and adapter/profile alignment)
- [ ] 5.3 Add transform applicability/order tests
- [ ] 5.4 Expand parity tests for representative workflow/tool matrix
- [ ] 5.5 Run full test suite and verify generated artifacts remain stable
- [ ] 5.2 Add tool-profile consistency tests (skillsDir support, adapter/profile alignment, command-surface metadata)
- [ ] 5.3 Add token rendering tests (all tokens resolved, per-tool rendering correctness)
- [ ] 5.4 Add fallback tests for unverified tool command surfaces
- [ ] 5.5 Add transform applicability/order tests
- [ ] 5.6 Expand parity tests for representative workflow/tool matrix
- [ ] 5.7 Run full test suite and verify generated artifacts remain stable
## 6. Cleanup and Documentation
+4967
View File
File diff suppressed because it is too large Load Diff
@@ -22,6 +22,7 @@ export { iflowAdapter } from './iflow.js';
export { kilocodeAdapter } from './kilocode.js';
export { kiroAdapter } from './kiro.js';
export { opencodeAdapter } from './opencode.js';
export { piAdapter } from './pi.js';
export { qoderAdapter } from './qoder.js';
export { qwenAdapter } from './qwen.js';
export { roocodeAdapter } from './roocode.js';
@@ -0,0 +1,46 @@
/**
* Pi Command Adapter
*
* Formats commands for Pi (pi.dev) following its prompt template specification.
* Pi prompt templates live in .pi/prompts/*.md with description frontmatter.
*/
import path from 'path';
import type { CommandContent, ToolCommandAdapter } from '../types.js';
/**
* Escapes a string value for safe YAML output.
* Quotes the string if it contains special YAML characters.
*/
function escapeYamlValue(value: string): string {
// Check if value needs quoting (contains special YAML characters or starts/ends with whitespace)
const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
if (needsQuoting) {
// Use double quotes and escape internal double quotes and backslashes
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
return `"${escaped}"`;
}
return value;
}
/**
* Pi adapter for prompt template generation.
* File path: .pi/prompts/opsx-<id>.md
* Frontmatter: description
*/
export const piAdapter: ToolCommandAdapter = {
toolId: 'pi',
getFilePath(commandId: string): string {
return path.join('.pi', 'prompts', `opsx-${commandId}.md`);
},
formatFile(content: CommandContent): string {
return `---
description: ${escapeYamlValue(content.description)}
---
${content.body}
`;
},
};
+2
View File
@@ -24,6 +24,7 @@ import { iflowAdapter } from './adapters/iflow.js';
import { kilocodeAdapter } from './adapters/kilocode.js';
import { kiroAdapter } from './adapters/kiro.js';
import { opencodeAdapter } from './adapters/opencode.js';
import { piAdapter } from './adapters/pi.js';
import { qoderAdapter } from './adapters/qoder.js';
import { qwenAdapter } from './adapters/qwen.js';
import { roocodeAdapter } from './adapters/roocode.js';
@@ -55,6 +56,7 @@ export class CommandAdapterRegistry {
CommandAdapterRegistry.register(kilocodeAdapter);
CommandAdapterRegistry.register(kiroAdapter);
CommandAdapterRegistry.register(opencodeAdapter);
CommandAdapterRegistry.register(piAdapter);
CommandAdapterRegistry.register(qoderAdapter);
CommandAdapterRegistry.register(qwenAdapter);
CommandAdapterRegistry.register(roocodeAdapter);
+1
View File
@@ -36,6 +36,7 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code', skillsDir: '.kilocode' },
{ name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' },
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' },
{ name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' },
{ name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' },
{ name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' },
{ name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode', skillsDir: '.roo' },
+44 -1
View File
@@ -18,6 +18,7 @@ import { githubCopilotAdapter } from '../../../src/core/command-generation/adapt
import { iflowAdapter } from '../../../src/core/command-generation/adapters/iflow.js';
import { kilocodeAdapter } from '../../../src/core/command-generation/adapters/kilocode.js';
import { opencodeAdapter } from '../../../src/core/command-generation/adapters/opencode.js';
import { piAdapter } from '../../../src/core/command-generation/adapters/pi.js';
import { qoderAdapter } from '../../../src/core/command-generation/adapters/qoder.js';
import { qwenAdapter } from '../../../src/core/command-generation/adapters/qwen.js';
import { roocodeAdapter } from '../../../src/core/command-generation/adapters/roocode.js';
@@ -523,6 +524,48 @@ describe('command-generation/adapters', () => {
});
});
describe('piAdapter', () => {
it('should have correct toolId', () => {
expect(piAdapter.toolId).toBe('pi');
});
it('should generate correct file path', () => {
const filePath = piAdapter.getFilePath('explore');
expect(filePath).toBe(path.join('.pi', 'prompts', 'opsx-explore.md'));
});
it('should generate correct file paths for different commands', () => {
expect(piAdapter.getFilePath('new')).toBe(path.join('.pi', 'prompts', 'opsx-new.md'));
expect(piAdapter.getFilePath('bulk-archive')).toBe(path.join('.pi', 'prompts', 'opsx-bulk-archive.md'));
});
it('should format file with description frontmatter', () => {
const output = piAdapter.formatFile(sampleContent);
expect(output).toContain('---\n');
expect(output).toContain('description: Enter explore mode for thinking');
expect(output).toContain('---\n\n');
expect(output).toContain('This is the command body.');
});
it('should escape YAML special characters in description', () => {
const contentWithSpecialChars: CommandContent = {
...sampleContent,
description: 'Fix: regression in "auth" feature',
};
const output = piAdapter.formatFile(contentWithSpecialChars);
expect(output).toContain('description: "Fix: regression in \\"auth\\" feature"');
});
it('should escape newlines in description', () => {
const contentWithNewline: CommandContent = {
...sampleContent,
description: 'Line 1\nLine 2',
};
const output = piAdapter.formatFile(contentWithNewline);
expect(output).toContain('description: "Line 1\\nLine 2"');
});
});
describe('roocodeAdapter', () => {
it('should have correct toolId', () => {
expect(roocodeAdapter.toolId).toBe('roocode');
@@ -566,7 +609,7 @@ describe('command-generation/adapters', () => {
amazonQAdapter, antigravityAdapter, auggieAdapter, clineAdapter,
codexAdapter, codebuddyAdapter, continueAdapter, costrictAdapter,
crushAdapter, factoryAdapter, geminiAdapter, githubCopilotAdapter,
iflowAdapter, kilocodeAdapter, opencodeAdapter, qoderAdapter,
iflowAdapter, kilocodeAdapter, opencodeAdapter, piAdapter, qoderAdapter,
qwenAdapter, roocodeAdapter
];
for (const adapter of adapters) {
+7 -7
View File
@@ -902,16 +902,16 @@ ${OPENSPEC_MARKERS.end}`);
});
});
it('should cover all tools from the CommandAdapterRegistry', () => {
const expectedTools = CommandAdapterRegistry.getAll().map(adapter => adapter.toolId);
it('should only include legacy tool IDs that are present in the CommandAdapterRegistry', () => {
const registeredTools = new Set(CommandAdapterRegistry.getAll().map(adapter => adapter.toolId));
// Verify all adapters have legacy paths
for (const tool of expectedTools) {
expect(LEGACY_SLASH_COMMAND_PATHS).toHaveProperty(tool);
// Verify all legacy map entries correspond to known adapters
for (const tool of Object.keys(LEGACY_SLASH_COMMAND_PATHS)) {
expect(registeredTools.has(tool)).toBe(true);
}
// Verify counts match
expect(expectedTools.length).toBe(Object.keys(LEGACY_SLASH_COMMAND_PATHS).length);
// Pi was never a pre-1.0 legacy tool
expect(LEGACY_SLASH_COMMAND_PATHS).not.toHaveProperty('pi');
});
});