@fission-ai/openspec 1.5.0 → 1.6.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/dist/commands/change.d.ts +0 -1
- package/dist/commands/change.js +6 -40
- package/dist/commands/validate.d.ts +8 -0
- package/dist/commands/validate.js +16 -4
- package/dist/core/archive.js +15 -16
- package/dist/core/command-generation/adapters/claude.d.ts +1 -1
- package/dist/core/command-generation/adapters/claude.js +3 -1
- package/dist/core/command-generation/adapters/index.d.ts +2 -0
- package/dist/core/command-generation/adapters/index.js +2 -0
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
- package/dist/core/command-generation/adapters/oh-my-pi.js +44 -0
- package/dist/core/command-generation/adapters/trae.d.ts +13 -0
- package/dist/core/command-generation/adapters/trae.js +48 -0
- package/dist/core/command-generation/registry.js +4 -0
- package/dist/core/config.js +1 -0
- package/dist/core/init.js +3 -2
- package/dist/core/list.js +18 -9
- package/dist/core/openspec-root.js +18 -15
- package/dist/core/parsers/change-parser.js +2 -1
- package/dist/core/parsers/markdown-parser.d.ts +0 -3
- package/dist/core/parsers/markdown-parser.js +5 -59
- package/dist/core/parsers/requirement-blocks.d.ts +11 -0
- package/dist/core/parsers/requirement-blocks.js +42 -8
- package/dist/core/parsers/requirement-text.d.ts +55 -0
- package/dist/core/parsers/requirement-text.js +136 -0
- package/dist/core/profile-sync-drift.js +1 -0
- package/dist/core/profiles.d.ts +2 -2
- package/dist/core/profiles.js +2 -1
- package/dist/core/schemas/base.schema.js +7 -2
- package/dist/core/shared/allowed-tools.d.ts +12 -0
- package/dist/core/shared/allowed-tools.js +12 -0
- package/dist/core/shared/skill-generation.js +5 -1
- package/dist/core/shared/tool-detection.d.ts +2 -2
- package/dist/core/shared/tool-detection.js +2 -0
- package/dist/core/specs-apply.js +35 -1
- package/dist/core/store/operations.js +20 -0
- package/dist/core/templates/skill-templates.d.ts +1 -0
- package/dist/core/templates/skill-templates.js +1 -0
- package/dist/core/templates/workflows/update-change.d.ts +10 -0
- package/dist/core/templates/workflows/update-change.js +167 -0
- package/dist/core/update.js +4 -4
- package/dist/core/validation/validator.d.ts +7 -0
- package/dist/core/validation/validator.js +101 -38
- package/dist/core/view.js +1 -1
- package/dist/utils/task-progress.d.ts +10 -1
- package/dist/utils/task-progress.js +66 -2
- package/package.json +21 -18
package/README.md
CHANGED
|
@@ -85,6 +85,18 @@ AI: Archived to openspec/changes/archive/2025-01-23-add-dark-mode/
|
|
|
85
85
|
|
|
86
86
|
</details>
|
|
87
87
|
|
|
88
|
+
## Why teams adopt OpenSpec
|
|
89
|
+
|
|
90
|
+
Solo, OpenSpec keeps you and your AI honest on a single repo. On a team, the hard part moves: a feature spans the API server, the web app, and a shared library; requirements are owned by one team and consumed by others; planning starts before any code exists.
|
|
91
|
+
|
|
92
|
+
**[Stores](docs/stores-beta/user-guide.md)** are the answer — planning in a repo of its own. The same `openspec/` shape you already know (specs and changes), shared by `git push` like anything else. One source of truth your whole team and every coding agent can read, across every repo.
|
|
93
|
+
|
|
94
|
+
- **Cross-repo features** — one change, one plan, even when the code lands in three repos.
|
|
95
|
+
- **Shared requirements** — a platform team owns the specs; product teams reference them read-only, right where their coding agent can read them. No drifting wiki.
|
|
96
|
+
- **Plan before code** — capture the plan in the store now; the code repos catch up later.
|
|
97
|
+
|
|
98
|
+
> Stores are in **beta**. Start with the [Stores User Guide](docs/stores-beta/user-guide.md).
|
|
99
|
+
|
|
88
100
|
## Quick Start
|
|
89
101
|
|
|
90
102
|
**Requires Node.js 20.19.0 or higher.**
|
|
@@ -150,7 +162,7 @@ AI coding assistants are powerful but unpredictable when requirements live only
|
|
|
150
162
|
- **Agree before you build** — human and AI align on specs before code gets written
|
|
151
163
|
- **Stay organized** — each change gets its own folder with proposal, specs, design, and tasks
|
|
152
164
|
- **Work fluidly** — update any artifact anytime, no rigid phase gates
|
|
153
|
-
- **Use your tools** — works with
|
|
165
|
+
- **Use your tools** — works with 30+ AI assistants via slash commands
|
|
154
166
|
|
|
155
167
|
### How we compare
|
|
156
168
|
|
package/dist/commands/change.js
CHANGED
|
@@ -5,10 +5,9 @@ import { Validator } from '../core/validation/validator.js';
|
|
|
5
5
|
import { ChangeParser } from '../core/parsers/change-parser.js';
|
|
6
6
|
import { isInteractive } from '../utils/interactive.js';
|
|
7
7
|
import { getActiveChangeIds } from '../utils/item-discovery.js';
|
|
8
|
+
import { getTaskProgressForChange } from '../utils/task-progress.js';
|
|
8
9
|
// Constants for better maintainability
|
|
9
10
|
const ARCHIVE_DIR = 'archive';
|
|
10
|
-
const TASK_PATTERN = /^[-*]\s+\[[\sx]\]/i;
|
|
11
|
-
const COMPLETED_TASK_PATTERN = /^[-*]\s+\[x\]/i;
|
|
12
11
|
export class ChangeCommand {
|
|
13
12
|
converter;
|
|
14
13
|
rootPath;
|
|
@@ -94,23 +93,14 @@ export class ChangeCommand {
|
|
|
94
93
|
if (options?.json) {
|
|
95
94
|
const changeDetails = await Promise.all(changes.map(async (changeName) => {
|
|
96
95
|
const proposalPath = path.join(changesPath, changeName, 'proposal.md');
|
|
97
|
-
const tasksPath = path.join(changesPath, changeName, 'tasks.md');
|
|
98
96
|
try {
|
|
99
97
|
const content = await fs.readFile(proposalPath, 'utf-8');
|
|
100
98
|
const changeDir = path.join(changesPath, changeName);
|
|
101
99
|
const parser = new ChangeParser(content, changeDir);
|
|
102
100
|
const change = await parser.parseChangeWithDeltas(changeName);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
taskStatus = this.countTasks(tasksContent);
|
|
107
|
-
}
|
|
108
|
-
catch (error) {
|
|
109
|
-
// Tasks file may not exist, which is okay
|
|
110
|
-
if (process.env.DEBUG) {
|
|
111
|
-
console.error(`Failed to read tasks file at ${tasksPath}:`, error);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
101
|
+
// Resolve task progress through the shared tracked-tasks helper so
|
|
102
|
+
// this deprecated noun-form list cannot re-fork the resolution (#1202).
|
|
103
|
+
const taskStatus = await getTaskProgressForChange(changesPath, changeName, process.cwd());
|
|
114
104
|
return {
|
|
115
105
|
id: changeName,
|
|
116
106
|
title: this.extractTitle(content, changeName),
|
|
@@ -144,21 +134,11 @@ export class ChangeCommand {
|
|
|
144
134
|
// Long format: id: title and minimal counts
|
|
145
135
|
for (const changeName of sorted) {
|
|
146
136
|
const proposalPath = path.join(changesPath, changeName, 'proposal.md');
|
|
147
|
-
const tasksPath = path.join(changesPath, changeName, 'tasks.md');
|
|
148
137
|
try {
|
|
149
138
|
const content = await fs.readFile(proposalPath, 'utf-8');
|
|
150
139
|
const title = this.extractTitle(content, changeName);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const tasksContent = await fs.readFile(tasksPath, 'utf-8');
|
|
154
|
-
const { total, completed } = this.countTasks(tasksContent);
|
|
155
|
-
taskStatusText = ` [tasks ${completed}/${total}]`;
|
|
156
|
-
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
if (process.env.DEBUG) {
|
|
159
|
-
console.error(`Failed to read tasks file at ${tasksPath}:`, error);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
140
|
+
const { total, completed } = await getTaskProgressForChange(changesPath, changeName, process.cwd());
|
|
141
|
+
const taskStatusText = total > 0 ? ` [tasks ${completed}/${total}]` : '';
|
|
162
142
|
const changeDir = path.join(changesPath, changeName);
|
|
163
143
|
const parser = new ChangeParser(await fs.readFile(proposalPath, 'utf-8'), changeDir);
|
|
164
144
|
const change = await parser.parseChangeWithDeltas(changeName);
|
|
@@ -253,20 +233,6 @@ export class ChangeCommand {
|
|
|
253
233
|
const match = content.match(/^#\s+(?:Change:\s+)?(.+)$/im);
|
|
254
234
|
return match ? match[1].trim() : changeName;
|
|
255
235
|
}
|
|
256
|
-
countTasks(content) {
|
|
257
|
-
const lines = content.split('\n');
|
|
258
|
-
let total = 0;
|
|
259
|
-
let completed = 0;
|
|
260
|
-
for (const line of lines) {
|
|
261
|
-
if (line.match(TASK_PATTERN)) {
|
|
262
|
-
total++;
|
|
263
|
-
if (line.match(COMPLETED_TASK_PATTERN)) {
|
|
264
|
-
completed++;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return { total, completed };
|
|
269
|
-
}
|
|
270
236
|
printNextSteps() {
|
|
271
237
|
const bullets = [];
|
|
272
238
|
bullets.push('- Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements');
|
|
@@ -14,6 +14,14 @@ interface ExecuteOptions {
|
|
|
14
14
|
export declare class ValidateCommand {
|
|
15
15
|
execute(itemName: string | undefined, options?: ExecuteOptions): Promise<void>;
|
|
16
16
|
private normalizeType;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve change IDs by directory existence within the resolved root — the
|
|
19
|
+
* same rule `openspec status`/`instructions` use (`getAvailableChanges`) —
|
|
20
|
+
* rather than requiring `proposal.md`. This lets `validate` resolve a
|
|
21
|
+
* scaffolded or still-authoring change that the sibling commands already
|
|
22
|
+
* resolve (#1182). Sorted to preserve the prior `getActiveChangeIds` ordering.
|
|
23
|
+
*/
|
|
24
|
+
private listChangeIds;
|
|
17
25
|
private runInteractiveSelector;
|
|
18
26
|
private printNonInteractiveHint;
|
|
19
27
|
private validateDirectItem;
|
|
@@ -3,7 +3,8 @@ import path from 'path';
|
|
|
3
3
|
import { Validator } from '../core/validation/validator.js';
|
|
4
4
|
import { resolveRootForCommand, toRootOutput, withStoreFlag, isStoreSelectedRoot, } from '../core/root-selection.js';
|
|
5
5
|
import { isInteractive, resolveNoInteractive } from '../utils/interactive.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getSpecIds } from '../utils/item-discovery.js';
|
|
7
|
+
import { getAvailableChanges } from './workflow/shared.js';
|
|
7
8
|
import { nearestMatches } from '../utils/match.js';
|
|
8
9
|
export class ValidateCommand {
|
|
9
10
|
async execute(itemName, options = {}) {
|
|
@@ -42,6 +43,17 @@ export class ValidateCommand {
|
|
|
42
43
|
return v;
|
|
43
44
|
return undefined;
|
|
44
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve change IDs by directory existence within the resolved root — the
|
|
48
|
+
* same rule `openspec status`/`instructions` use (`getAvailableChanges`) —
|
|
49
|
+
* rather than requiring `proposal.md`. This lets `validate` resolve a
|
|
50
|
+
* scaffolded or still-authoring change that the sibling commands already
|
|
51
|
+
* resolve (#1182). Sorted to preserve the prior `getActiveChangeIds` ordering.
|
|
52
|
+
*/
|
|
53
|
+
async listChangeIds(root) {
|
|
54
|
+
const ids = await getAvailableChanges(root.path, root.changesDir);
|
|
55
|
+
return ids.sort();
|
|
56
|
+
}
|
|
45
57
|
async runInteractiveSelector(root, opts) {
|
|
46
58
|
const { select } = await import('@inquirer/prompts');
|
|
47
59
|
const choice = await select({
|
|
@@ -60,7 +72,7 @@ export class ValidateCommand {
|
|
|
60
72
|
if (choice === 'specs')
|
|
61
73
|
return this.runBulkValidation(root, { changes: false, specs: true }, opts);
|
|
62
74
|
// one
|
|
63
|
-
const [changes, specs] = await Promise.all([
|
|
75
|
+
const [changes, specs] = await Promise.all([this.listChangeIds(root), getSpecIds(root.path)]);
|
|
64
76
|
const items = [];
|
|
65
77
|
items.push(...changes.map(id => ({ name: `change/${id}`, value: { type: 'change', id } })));
|
|
66
78
|
items.push(...specs.map(id => ({ name: `spec/${id}`, value: { type: 'spec', id } })));
|
|
@@ -81,7 +93,7 @@ export class ValidateCommand {
|
|
|
81
93
|
console.error('Or run in an interactive terminal.');
|
|
82
94
|
}
|
|
83
95
|
async validateDirectItem(root, itemName, opts) {
|
|
84
|
-
const [changes, specs] = await Promise.all([
|
|
96
|
+
const [changes, specs] = await Promise.all([this.listChangeIds(root), getSpecIds(root.path)]);
|
|
85
97
|
const isChange = changes.includes(itemName);
|
|
86
98
|
const isSpec = specs.includes(itemName);
|
|
87
99
|
const type = opts.typeOverride ?? (isChange ? 'change' : isSpec ? 'spec' : undefined);
|
|
@@ -183,7 +195,7 @@ export class ValidateCommand {
|
|
|
183
195
|
async runBulkValidation(root, scope, opts) {
|
|
184
196
|
const spinner = !opts.json && !opts.noInteractive ? ora('Validating...').start() : undefined;
|
|
185
197
|
const [changeIds, specIds] = await Promise.all([
|
|
186
|
-
scope.changes ?
|
|
198
|
+
scope.changes ? this.listChangeIds(root) : Promise.resolve([]),
|
|
187
199
|
scope.specs ? getSpecIds(root.path) : Promise.resolve([]),
|
|
188
200
|
]);
|
|
189
201
|
const DEFAULT_CONCURRENCY = 6;
|
package/dist/core/archive.js
CHANGED
|
@@ -5,6 +5,12 @@ import { Validator } from './validation/validator.js';
|
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import { emitStoreRootBanner, isRootSelectionError, resolveOpenSpecRoot, toRootOutput, withStoreFlag, isStoreSelectedRoot, } from './root-selection.js';
|
|
7
7
|
import { findSpecUpdates, buildUpdatedSpec, writeUpdatedSpec, } from './specs-apply.js';
|
|
8
|
+
function isMissingPathError(error) {
|
|
9
|
+
return (typeof error === 'object' &&
|
|
10
|
+
error !== null &&
|
|
11
|
+
'code' in error &&
|
|
12
|
+
error.code === 'ENOENT');
|
|
13
|
+
}
|
|
8
14
|
async function listActiveChangeNames(changesDir) {
|
|
9
15
|
try {
|
|
10
16
|
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
|
@@ -13,7 +19,9 @@ async function listActiveChangeNames(changesDir) {
|
|
|
13
19
|
.map((entry) => entry.name)
|
|
14
20
|
.sort();
|
|
15
21
|
}
|
|
16
|
-
catch {
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (!isMissingPathError(error))
|
|
24
|
+
throw error;
|
|
17
25
|
return [];
|
|
18
26
|
}
|
|
19
27
|
}
|
|
@@ -136,13 +144,6 @@ export class ArchiveCommand {
|
|
|
136
144
|
const changesDir = root.changesDir;
|
|
137
145
|
const archiveDir = root.archiveDir;
|
|
138
146
|
const mainSpecsDir = root.specsDir;
|
|
139
|
-
// Check if changes directory exists
|
|
140
|
-
try {
|
|
141
|
-
await fs.access(changesDir);
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
throw new Error("No OpenSpec changes directory found. Run 'openspec init' first.");
|
|
145
|
-
}
|
|
146
147
|
// Get change name interactively if not provided
|
|
147
148
|
if (!changeName) {
|
|
148
149
|
if (json) {
|
|
@@ -237,6 +238,7 @@ export class ArchiveCommand {
|
|
|
237
238
|
}
|
|
238
239
|
console.log(chalk.red('\nValidation failed. Please fix the errors before archiving.'));
|
|
239
240
|
console.log(chalk.yellow('To skip validation (not recommended), use --no-validate flag.'));
|
|
241
|
+
process.exitCode = 1;
|
|
240
242
|
return null;
|
|
241
243
|
}
|
|
242
244
|
}
|
|
@@ -266,7 +268,7 @@ export class ArchiveCommand {
|
|
|
266
268
|
console.log(chalk.yellow(`Affected files: ${changeDir}`));
|
|
267
269
|
}
|
|
268
270
|
// Show progress and check for incomplete tasks
|
|
269
|
-
const progress = await getTaskProgressForChange(changesDir, changeName);
|
|
271
|
+
const progress = await getTaskProgressForChange(changesDir, changeName, path.resolve(changesDir, '..', '..'));
|
|
270
272
|
if (!json) {
|
|
271
273
|
const status = formatTaskStatus(progress);
|
|
272
274
|
console.log(`Task status: ${status}`);
|
|
@@ -342,6 +344,7 @@ export class ArchiveCommand {
|
|
|
342
344
|
}
|
|
343
345
|
console.log(String(err.message || err));
|
|
344
346
|
console.log('Aborted. No files were changed.');
|
|
347
|
+
process.exitCode = 1;
|
|
345
348
|
return null;
|
|
346
349
|
}
|
|
347
350
|
// Validate every rebuilt spec before writing any of them, so a
|
|
@@ -362,6 +365,7 @@ export class ArchiveCommand {
|
|
|
362
365
|
console.log(chalk.yellow(` ⚠ ${issue.message}`));
|
|
363
366
|
}
|
|
364
367
|
console.log('Aborted. No files were changed.');
|
|
368
|
+
process.exitCode = 1;
|
|
365
369
|
return null;
|
|
366
370
|
}
|
|
367
371
|
}
|
|
@@ -422,12 +426,7 @@ export class ArchiveCommand {
|
|
|
422
426
|
}
|
|
423
427
|
async selectChange(changesDir) {
|
|
424
428
|
const { select } = await import('@inquirer/prompts');
|
|
425
|
-
|
|
426
|
-
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
|
427
|
-
const changeDirs = entries
|
|
428
|
-
.filter(entry => entry.isDirectory() && entry.name !== 'archive')
|
|
429
|
-
.map(entry => entry.name)
|
|
430
|
-
.sort();
|
|
429
|
+
const changeDirs = await listActiveChangeNames(changesDir);
|
|
431
430
|
if (changeDirs.length === 0) {
|
|
432
431
|
console.log('No active changes found.');
|
|
433
432
|
return null;
|
|
@@ -437,7 +436,7 @@ export class ArchiveCommand {
|
|
|
437
436
|
try {
|
|
438
437
|
const progressList = [];
|
|
439
438
|
for (const id of changeDirs) {
|
|
440
|
-
const progress = await getTaskProgressForChange(changesDir, id);
|
|
439
|
+
const progress = await getTaskProgressForChange(changesDir, id, path.resolve(changesDir, '..', '..'));
|
|
441
440
|
const status = formatTaskStatus(progress);
|
|
442
441
|
progressList.push({ id, status });
|
|
443
442
|
}
|
|
@@ -7,7 +7,7 @@ import type { ToolCommandAdapter } from '../types.js';
|
|
|
7
7
|
/**
|
|
8
8
|
* Claude Code adapter for command generation.
|
|
9
9
|
* File path: .claude/commands/opsx/<id>.md
|
|
10
|
-
* Frontmatter: name, description, category, tags
|
|
10
|
+
* Frontmatter: name, description, allowed-tools, category, tags
|
|
11
11
|
*/
|
|
12
12
|
export declare const claudeAdapter: ToolCommandAdapter;
|
|
13
13
|
//# sourceMappingURL=claude.d.ts.map
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { escapeYamlValue } from '../yaml.js';
|
|
8
|
+
import { OPENSPEC_CLI_ALLOWED_TOOLS } from '../../shared/allowed-tools.js';
|
|
8
9
|
/**
|
|
9
10
|
* Formats a tags array as a YAML array with proper escaping.
|
|
10
11
|
*/
|
|
@@ -15,7 +16,7 @@ function formatTagsArray(tags) {
|
|
|
15
16
|
/**
|
|
16
17
|
* Claude Code adapter for command generation.
|
|
17
18
|
* File path: .claude/commands/opsx/<id>.md
|
|
18
|
-
* Frontmatter: name, description, category, tags
|
|
19
|
+
* Frontmatter: name, description, allowed-tools, category, tags
|
|
19
20
|
*/
|
|
20
21
|
export const claudeAdapter = {
|
|
21
22
|
toolId: 'claude',
|
|
@@ -26,6 +27,7 @@ export const claudeAdapter = {
|
|
|
26
27
|
return `---
|
|
27
28
|
name: ${escapeYamlValue(content.name)}
|
|
28
29
|
description: ${escapeYamlValue(content.description)}
|
|
30
|
+
allowed-tools: ${OPENSPEC_CLI_ALLOWED_TOOLS}
|
|
29
31
|
category: ${escapeYamlValue(content.category)}
|
|
30
32
|
tags: ${formatTagsArray(content.tags)}
|
|
31
33
|
---
|
|
@@ -22,11 +22,13 @@ export { iflowAdapter } from './iflow.js';
|
|
|
22
22
|
export { junieAdapter } from './junie.js';
|
|
23
23
|
export { kilocodeAdapter } from './kilocode.js';
|
|
24
24
|
export { kiroAdapter } from './kiro.js';
|
|
25
|
+
export { ohMyPiAdapter } from './oh-my-pi.js';
|
|
25
26
|
export { opencodeAdapter } from './opencode.js';
|
|
26
27
|
export { piAdapter } from './pi.js';
|
|
27
28
|
export { qoderAdapter } from './qoder.js';
|
|
28
29
|
export { lingmaAdapter } from './lingma.js';
|
|
29
30
|
export { qwenAdapter } from './qwen.js';
|
|
30
31
|
export { roocodeAdapter } from './roocode.js';
|
|
32
|
+
export { traeAdapter } from './trae.js';
|
|
31
33
|
export { windsurfAdapter } from './windsurf.js';
|
|
32
34
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -22,11 +22,13 @@ export { iflowAdapter } from './iflow.js';
|
|
|
22
22
|
export { junieAdapter } from './junie.js';
|
|
23
23
|
export { kilocodeAdapter } from './kilocode.js';
|
|
24
24
|
export { kiroAdapter } from './kiro.js';
|
|
25
|
+
export { ohMyPiAdapter } from './oh-my-pi.js';
|
|
25
26
|
export { opencodeAdapter } from './opencode.js';
|
|
26
27
|
export { piAdapter } from './pi.js';
|
|
27
28
|
export { qoderAdapter } from './qoder.js';
|
|
28
29
|
export { lingmaAdapter } from './lingma.js';
|
|
29
30
|
export { qwenAdapter } from './qwen.js';
|
|
30
31
|
export { roocodeAdapter } from './roocode.js';
|
|
32
|
+
export { traeAdapter } from './trae.js';
|
|
31
33
|
export { windsurfAdapter } from './windsurf.js';
|
|
32
34
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oh My Pi (OMP) Command Adapter
|
|
3
|
+
*
|
|
4
|
+
* Formats commands for Oh My Pi following its slash command specification.
|
|
5
|
+
* OMP loads slash commands from .omp/commands/*.md with YAML frontmatter.
|
|
6
|
+
* The filename (minus .md) becomes the slash command name.
|
|
7
|
+
*/
|
|
8
|
+
import type { ToolCommandAdapter } from '../types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Oh My Pi adapter for command generation.
|
|
11
|
+
* File path: .omp/commands/opsx-<id>.md
|
|
12
|
+
* Frontmatter: description
|
|
13
|
+
*
|
|
14
|
+
* OMP uses the filename (minus .md) as the slash command name, so
|
|
15
|
+
* opsx-propose.md → /opsx-propose. Command references in the body
|
|
16
|
+
* are transformed from /opsx: to /opsx- for consistency, and
|
|
17
|
+
* $@ is injected after **Input**: headings so user-supplied arguments
|
|
18
|
+
* (e.g. /opsx-propose my-feature) are visible to the agent.
|
|
19
|
+
*/
|
|
20
|
+
export declare const ohMyPiAdapter: ToolCommandAdapter;
|
|
21
|
+
//# sourceMappingURL=oh-my-pi.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oh My Pi (OMP) Command Adapter
|
|
3
|
+
*
|
|
4
|
+
* Formats commands for Oh My Pi following its slash command specification.
|
|
5
|
+
* OMP loads slash commands from .omp/commands/*.md with YAML frontmatter.
|
|
6
|
+
* The filename (minus .md) becomes the slash command name.
|
|
7
|
+
*/
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { transformToHyphenCommands } from '../../../utils/command-references.js';
|
|
10
|
+
import { escapeYamlValue } from '../yaml.js';
|
|
11
|
+
const OMP_INPUT_HEADING = /^\*\*Input\*\*:[^\n]*$/m;
|
|
12
|
+
function injectOmpArgs(body) {
|
|
13
|
+
if (body.includes('$@') || body.includes('$ARGUMENTS')) {
|
|
14
|
+
return body;
|
|
15
|
+
}
|
|
16
|
+
return body.replace(OMP_INPUT_HEADING, (heading) => `${heading}\n**Provided arguments**: $@`);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Oh My Pi adapter for command generation.
|
|
20
|
+
* File path: .omp/commands/opsx-<id>.md
|
|
21
|
+
* Frontmatter: description
|
|
22
|
+
*
|
|
23
|
+
* OMP uses the filename (minus .md) as the slash command name, so
|
|
24
|
+
* opsx-propose.md → /opsx-propose. Command references in the body
|
|
25
|
+
* are transformed from /opsx: to /opsx- for consistency, and
|
|
26
|
+
* $@ is injected after **Input**: headings so user-supplied arguments
|
|
27
|
+
* (e.g. /opsx-propose my-feature) are visible to the agent.
|
|
28
|
+
*/
|
|
29
|
+
export const ohMyPiAdapter = {
|
|
30
|
+
toolId: 'oh-my-pi',
|
|
31
|
+
getFilePath(commandId) {
|
|
32
|
+
return path.join('.omp', 'commands', `opsx-${commandId}.md`);
|
|
33
|
+
},
|
|
34
|
+
formatFile(content) {
|
|
35
|
+
const transformedBody = transformToHyphenCommands(content.body);
|
|
36
|
+
return `---
|
|
37
|
+
description: ${escapeYamlValue(content.description)}
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
${injectOmpArgs(transformedBody)}
|
|
41
|
+
`;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=oh-my-pi.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trae Command Adapter
|
|
3
|
+
*
|
|
4
|
+
* Formats commands for Trae IDE following its command specification.
|
|
5
|
+
*/
|
|
6
|
+
import type { ToolCommandAdapter } from '../types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Trae adapter for command generation.
|
|
9
|
+
* File path: .trae/commands/opsx-<id>.md
|
|
10
|
+
* Frontmatter: name, description
|
|
11
|
+
*/
|
|
12
|
+
export declare const traeAdapter: ToolCommandAdapter;
|
|
13
|
+
//# sourceMappingURL=trae.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trae Command Adapter
|
|
3
|
+
*
|
|
4
|
+
* Formats commands for Trae IDE following its command specification.
|
|
5
|
+
*/
|
|
6
|
+
import path from 'path';
|
|
7
|
+
/**
|
|
8
|
+
* Escapes a string value for safe YAML output.
|
|
9
|
+
* Quotes the string if it contains special YAML characters.
|
|
10
|
+
*/
|
|
11
|
+
function escapeYamlValue(value) {
|
|
12
|
+
if (value === '') {
|
|
13
|
+
return '""';
|
|
14
|
+
}
|
|
15
|
+
// Check if value needs quoting (contains special YAML characters or starts/ends with whitespace)
|
|
16
|
+
const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
|
|
17
|
+
if (needsQuoting) {
|
|
18
|
+
// Use double quotes and escape internal double quotes, backslashes, and newlines
|
|
19
|
+
const escaped = value
|
|
20
|
+
.replace(/\\/g, '\\\\')
|
|
21
|
+
.replace(/"/g, '\\"')
|
|
22
|
+
.replace(/\n/g, '\\n')
|
|
23
|
+
.replace(/\r/g, '\\r');
|
|
24
|
+
return `"${escaped}"`;
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Trae adapter for command generation.
|
|
30
|
+
* File path: .trae/commands/opsx-<id>.md
|
|
31
|
+
* Frontmatter: name, description
|
|
32
|
+
*/
|
|
33
|
+
export const traeAdapter = {
|
|
34
|
+
toolId: 'trae',
|
|
35
|
+
getFilePath(commandId) {
|
|
36
|
+
return path.join('.trae', 'commands', `opsx-${commandId}.md`);
|
|
37
|
+
},
|
|
38
|
+
formatFile(content) {
|
|
39
|
+
return `---
|
|
40
|
+
name: ${escapeYamlValue(content.name)}
|
|
41
|
+
description: ${escapeYamlValue(content.description)}
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
${content.body}
|
|
45
|
+
`;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=trae.js.map
|
|
@@ -23,12 +23,14 @@ import { iflowAdapter } from './adapters/iflow.js';
|
|
|
23
23
|
import { junieAdapter } from './adapters/junie.js';
|
|
24
24
|
import { kilocodeAdapter } from './adapters/kilocode.js';
|
|
25
25
|
import { kiroAdapter } from './adapters/kiro.js';
|
|
26
|
+
import { ohMyPiAdapter } from './adapters/oh-my-pi.js';
|
|
26
27
|
import { opencodeAdapter } from './adapters/opencode.js';
|
|
27
28
|
import { piAdapter } from './adapters/pi.js';
|
|
28
29
|
import { qoderAdapter } from './adapters/qoder.js';
|
|
29
30
|
import { lingmaAdapter } from './adapters/lingma.js';
|
|
30
31
|
import { qwenAdapter } from './adapters/qwen.js';
|
|
31
32
|
import { roocodeAdapter } from './adapters/roocode.js';
|
|
33
|
+
import { traeAdapter } from './adapters/trae.js';
|
|
32
34
|
import { windsurfAdapter } from './adapters/windsurf.js';
|
|
33
35
|
/**
|
|
34
36
|
* Registry for looking up tool command adapters.
|
|
@@ -56,12 +58,14 @@ export class CommandAdapterRegistry {
|
|
|
56
58
|
CommandAdapterRegistry.register(junieAdapter);
|
|
57
59
|
CommandAdapterRegistry.register(kilocodeAdapter);
|
|
58
60
|
CommandAdapterRegistry.register(kiroAdapter);
|
|
61
|
+
CommandAdapterRegistry.register(ohMyPiAdapter);
|
|
59
62
|
CommandAdapterRegistry.register(opencodeAdapter);
|
|
60
63
|
CommandAdapterRegistry.register(piAdapter);
|
|
61
64
|
CommandAdapterRegistry.register(qoderAdapter);
|
|
62
65
|
CommandAdapterRegistry.register(lingmaAdapter);
|
|
63
66
|
CommandAdapterRegistry.register(qwenAdapter);
|
|
64
67
|
CommandAdapterRegistry.register(roocodeAdapter);
|
|
68
|
+
CommandAdapterRegistry.register(traeAdapter);
|
|
65
69
|
CommandAdapterRegistry.register(windsurfAdapter);
|
|
66
70
|
}
|
|
67
71
|
/**
|
package/dist/core/config.js
CHANGED
|
@@ -27,6 +27,7 @@ export const AI_TOOLS = [
|
|
|
27
27
|
{ name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' },
|
|
28
28
|
{ name: 'Lingma', value: 'lingma', available: true, successLabel: 'Lingma', skillsDir: '.lingma' },
|
|
29
29
|
{ name: 'Mistral Vibe', value: 'vibe', available: true, successLabel: 'Mistral Vibe', skillsDir: '.vibe' },
|
|
30
|
+
{ name: 'Oh My Pi', value: 'oh-my-pi', available: true, successLabel: 'Oh My Pi', skillsDir: '.omp' },
|
|
30
31
|
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' },
|
|
31
32
|
{ name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' },
|
|
32
33
|
{ name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' },
|
package/dist/core/init.js
CHANGED
|
@@ -39,6 +39,7 @@ const WORKFLOW_TO_SKILL_DIR = {
|
|
|
39
39
|
'new': 'openspec-new-change',
|
|
40
40
|
'continue': 'openspec-continue-change',
|
|
41
41
|
'apply': 'openspec-apply-change',
|
|
42
|
+
'update': 'openspec-update-change',
|
|
42
43
|
'ff': 'openspec-ff-change',
|
|
43
44
|
'sync': 'openspec-sync-specs',
|
|
44
45
|
'archive': 'openspec-archive-change',
|
|
@@ -403,8 +404,8 @@ export class InitCommand {
|
|
|
403
404
|
const skillDir = path.join(skillsDir, dirName);
|
|
404
405
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
405
406
|
// Generate SKILL.md content with YAML frontmatter including generatedBy
|
|
406
|
-
// Use hyphen-based command references for tools where filename
|
|
407
|
-
const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined;
|
|
407
|
+
// Use hyphen-based command references for tools where filename === command name (oh-my-pi, opencode, pi)
|
|
408
|
+
const transformer = (tool.value === 'opencode' || tool.value === 'pi' || tool.value === 'oh-my-pi') ? transformToHyphenCommands : undefined;
|
|
408
409
|
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
409
410
|
// Write the skill file
|
|
410
411
|
await FileSystemUtils.writeFile(skillFile, skillContent);
|
package/dist/core/list.js
CHANGED
|
@@ -4,6 +4,22 @@ import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progre
|
|
|
4
4
|
import { readFileSync } from 'fs';
|
|
5
5
|
import { join } from 'path';
|
|
6
6
|
import { MarkdownParser } from './parsers/markdown-parser.js';
|
|
7
|
+
function isMissingPathError(error) {
|
|
8
|
+
return (typeof error === 'object' &&
|
|
9
|
+
error !== null &&
|
|
10
|
+
'code' in error &&
|
|
11
|
+
error.code === 'ENOENT');
|
|
12
|
+
}
|
|
13
|
+
async function readChangeDirectoryEntries(changesDir) {
|
|
14
|
+
try {
|
|
15
|
+
return await fs.readdir(changesDir, { withFileTypes: true });
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (isMissingPathError(error))
|
|
19
|
+
return [];
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
7
23
|
/**
|
|
8
24
|
* Get the most recent modification time of any file in a directory (recursive).
|
|
9
25
|
* Falls back to the directory's own mtime if no files are found.
|
|
@@ -64,15 +80,8 @@ export class ListCommand {
|
|
|
64
80
|
const { sort = 'recent', json = false, root } = options;
|
|
65
81
|
if (mode === 'changes') {
|
|
66
82
|
const changesDir = path.join(targetPath, 'openspec', 'changes');
|
|
67
|
-
// Check if changes directory exists
|
|
68
|
-
try {
|
|
69
|
-
await fs.access(changesDir);
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
throw new Error("No OpenSpec changes directory found. Run 'openspec init' first.");
|
|
73
|
-
}
|
|
74
83
|
// Get all directories in changes (excluding archive)
|
|
75
|
-
const entries = await
|
|
84
|
+
const entries = await readChangeDirectoryEntries(changesDir);
|
|
76
85
|
const changeDirs = entries
|
|
77
86
|
.filter(entry => entry.isDirectory() && entry.name !== 'archive')
|
|
78
87
|
.map(entry => entry.name);
|
|
@@ -88,7 +97,7 @@ export class ListCommand {
|
|
|
88
97
|
// Collect information about each change
|
|
89
98
|
const changes = [];
|
|
90
99
|
for (const changeDir of changeDirs) {
|
|
91
|
-
const progress = await getTaskProgressForChange(changesDir, changeDir);
|
|
100
|
+
const progress = await getTaskProgressForChange(changesDir, changeDir, targetPath);
|
|
92
101
|
const changePath = path.join(changesDir, changeDir);
|
|
93
102
|
const lastModified = await getLastModified(changePath);
|
|
94
103
|
changes.push({
|
|
@@ -11,8 +11,8 @@ export const OPENSPEC_CHANGES_DIR = 'openspec/changes';
|
|
|
11
11
|
export const OPENSPEC_ARCHIVE_DIR = 'openspec/changes/archive';
|
|
12
12
|
export const DEFAULT_OPENSPEC_SCHEMA = 'spec-driven';
|
|
13
13
|
export const DIRECTORY_ANCHOR_FILE_NAME = '.gitkeep';
|
|
14
|
-
// Git cannot track empty directories, so
|
|
15
|
-
//
|
|
14
|
+
// Git cannot track empty directories, so setup anchors otherwise-empty
|
|
15
|
+
// conventional store directories for teammates who clone the repo later.
|
|
16
16
|
export const ANCHORED_OPENSPEC_DIRS = [OPENSPEC_SPECS_DIR, OPENSPEC_ARCHIVE_DIR];
|
|
17
17
|
async function pathKind(targetPath) {
|
|
18
18
|
try {
|
|
@@ -51,6 +51,14 @@ function unresolvedInspection() {
|
|
|
51
51
|
function missingDirectoryDiagnostic(code, message, target) {
|
|
52
52
|
return makeStoreDiagnostic('error', code, message, { target });
|
|
53
53
|
}
|
|
54
|
+
async function inspectOptionalPlanningDirectory(inspection, storeRoot, key, relativePath, notDirectoryCode, target) {
|
|
55
|
+
const kind = await pathKind(path.join(storeRoot, relativePath));
|
|
56
|
+
inspection[key] = { present: kind === 'directory' };
|
|
57
|
+
if (kind === 'directory' || kind === 'missing')
|
|
58
|
+
return kind;
|
|
59
|
+
inspection.diagnostics.push(missingDirectoryDiagnostic(notDirectoryCode, `${relativePath}/ exists but is not a directory.`, target));
|
|
60
|
+
return kind;
|
|
61
|
+
}
|
|
54
62
|
export async function inspectOpenSpecRoot(storeRoot) {
|
|
55
63
|
const rootKind = await pathKind(storeRoot);
|
|
56
64
|
const inspection = unresolvedInspection();
|
|
@@ -90,23 +98,18 @@ export async function inspectOpenSpecRoot(storeRoot) {
|
|
|
90
98
|
inspection.diagnostics.push(missingDirectoryDiagnostic('openspec_config_missing', 'Missing openspec/config.yaml or openspec/config.yml.', 'openspec.config'));
|
|
91
99
|
}
|
|
92
100
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
inspection
|
|
100
|
-
if (kind === 'directory')
|
|
101
|
-
continue;
|
|
102
|
-
inspection.diagnostics.push(missingDirectoryDiagnostic(kind === 'missing' ? code : code.replace('_missing', '_not_directory'), kind === 'missing' ? message : `${relativePath}/ exists but is not a directory.`, target));
|
|
101
|
+
await inspectOptionalPlanningDirectory(inspection, storeRoot, 'specs', OPENSPEC_SPECS_DIR, 'openspec_specs_not_directory', 'openspec.specs');
|
|
102
|
+
const changesKind = await inspectOptionalPlanningDirectory(inspection, storeRoot, 'changes', OPENSPEC_CHANGES_DIR, 'openspec_changes_not_directory', 'openspec.changes');
|
|
103
|
+
if (changesKind === 'directory') {
|
|
104
|
+
await inspectOptionalPlanningDirectory(inspection, storeRoot, 'archive', OPENSPEC_ARCHIVE_DIR, 'openspec_archive_not_directory', 'openspec.archive');
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
inspection.archive = { present: false };
|
|
103
108
|
}
|
|
104
109
|
inspection.healthy =
|
|
105
110
|
inspection.present === true &&
|
|
106
111
|
inspection.config.present === true &&
|
|
107
|
-
inspection.
|
|
108
|
-
inspection.changes.present === true &&
|
|
109
|
-
inspection.archive.present === true;
|
|
112
|
+
inspection.diagnostics.length === 0;
|
|
110
113
|
return inspection;
|
|
111
114
|
}
|
|
112
115
|
async function ensureDirectory(storeRoot, relativePath, ledger) {
|