@fission-ai/openspec 1.2.0 → 1.3.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 +8 -13
- package/dist/commands/workflow/instructions.js +18 -72
- package/dist/commands/workflow/shared.d.ts +6 -1
- package/dist/commands/workflow/shared.js +21 -16
- package/dist/commands/workflow/status.js +21 -4
- package/dist/commands/workflow/templates.js +5 -4
- package/dist/core/artifact-graph/index.d.ts +1 -0
- package/dist/core/artifact-graph/index.js +1 -0
- package/dist/core/artifact-graph/instruction-loader.js +6 -4
- package/dist/core/artifact-graph/outputs.d.ts +14 -0
- package/dist/core/artifact-graph/outputs.js +39 -0
- package/dist/core/artifact-graph/state.js +2 -25
- package/dist/core/available-tools.d.ts +3 -2
- package/dist/core/available-tools.js +15 -2
- package/dist/core/command-generation/adapters/bob.d.ts +14 -0
- package/dist/core/command-generation/adapters/bob.js +45 -0
- package/dist/core/command-generation/adapters/index.d.ts +3 -0
- package/dist/core/command-generation/adapters/index.js +3 -0
- package/dist/core/command-generation/adapters/junie.d.ts +13 -0
- package/dist/core/command-generation/adapters/junie.js +26 -0
- package/dist/core/command-generation/adapters/lingma.d.ts +13 -0
- package/dist/core/command-generation/adapters/lingma.js +30 -0
- package/dist/core/command-generation/adapters/opencode.d.ts +1 -1
- package/dist/core/command-generation/adapters/opencode.js +2 -2
- package/dist/core/command-generation/adapters/pi.d.ts +4 -0
- package/dist/core/command-generation/adapters/pi.js +15 -1
- package/dist/core/command-generation/registry.js +6 -0
- package/dist/core/completions/installers/powershell-installer.d.ts +14 -0
- package/dist/core/completions/installers/powershell-installer.js +69 -9
- package/dist/core/config.d.ts +1 -0
- package/dist/core/config.js +5 -1
- package/dist/core/init.js +6 -10
- package/dist/core/legacy-cleanup.d.ts +1 -1
- package/dist/core/legacy-cleanup.js +22 -10
- package/dist/core/parsers/change-parser.js +7 -3
- package/dist/core/parsers/markdown-parser.d.ts +4 -0
- package/dist/core/parsers/markdown-parser.js +41 -1
- package/dist/core/parsers/spec-structure.d.ts +9 -0
- package/dist/core/parsers/spec-structure.js +88 -0
- package/dist/core/specs-apply.js +8 -0
- package/dist/core/templates/workflows/apply-change.js +4 -4
- package/dist/core/templates/workflows/bulk-archive-change.js +2 -2
- package/dist/core/templates/workflows/explore.js +25 -25
- package/dist/core/templates/workflows/onboard.js +24 -24
- package/dist/core/templates/workflows/verify-change.js +6 -6
- package/dist/core/update.js +2 -2
- package/dist/core/validation/validator.js +9 -0
- package/dist/telemetry/config.d.ts +7 -1
- package/dist/telemetry/config.js +87 -19
- package/dist/telemetry/index.js +19 -0
- package/dist/utils/file-system.d.ts +5 -0
- package/dist/utils/file-system.js +21 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +8 -72
|
@@ -1,14 +1,51 @@
|
|
|
1
1
|
export class MarkdownParser {
|
|
2
2
|
lines;
|
|
3
|
+
codeFenceLineMask;
|
|
3
4
|
currentLine;
|
|
4
5
|
constructor(content) {
|
|
5
6
|
const normalized = MarkdownParser.normalizeContent(content);
|
|
6
7
|
this.lines = normalized.split('\n');
|
|
8
|
+
this.codeFenceLineMask = MarkdownParser.buildCodeFenceMask(this.lines);
|
|
7
9
|
this.currentLine = 0;
|
|
8
10
|
}
|
|
9
11
|
static normalizeContent(content) {
|
|
10
12
|
return content.replace(/\r\n?/g, '\n');
|
|
11
13
|
}
|
|
14
|
+
static buildCodeFenceMask(lines) {
|
|
15
|
+
const mask = new Array(lines.length).fill(false);
|
|
16
|
+
let activeFence = null;
|
|
17
|
+
for (let i = 0; i < lines.length; i++) {
|
|
18
|
+
const fence = MarkdownParser.getFenceMarker(lines[i]);
|
|
19
|
+
if (!activeFence) {
|
|
20
|
+
if (fence) {
|
|
21
|
+
activeFence = fence;
|
|
22
|
+
mask[i] = true;
|
|
23
|
+
}
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
mask[i] = true;
|
|
27
|
+
if (MarkdownParser.isClosingFence(lines[i], activeFence)) {
|
|
28
|
+
activeFence = null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return mask;
|
|
32
|
+
}
|
|
33
|
+
static getFenceMarker(line) {
|
|
34
|
+
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})/);
|
|
35
|
+
if (!fenceMatch) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
marker: fenceMatch[1][0],
|
|
40
|
+
length: fenceMatch[1].length,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
static isClosingFence(line, activeFence) {
|
|
44
|
+
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})\s*$/);
|
|
45
|
+
return Boolean(fenceMatch &&
|
|
46
|
+
fenceMatch[1][0] === activeFence.marker &&
|
|
47
|
+
fenceMatch[1].length >= activeFence.length);
|
|
48
|
+
}
|
|
12
49
|
parseSpec(name) {
|
|
13
50
|
const sections = this.parseSections();
|
|
14
51
|
const purpose = this.findSection(sections, 'Purpose')?.content || '';
|
|
@@ -57,6 +94,9 @@ export class MarkdownParser {
|
|
|
57
94
|
const stack = [];
|
|
58
95
|
for (let i = 0; i < this.lines.length; i++) {
|
|
59
96
|
const line = this.lines[i];
|
|
97
|
+
if (this.codeFenceLineMask[i]) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
60
100
|
const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
61
101
|
if (headerMatch) {
|
|
62
102
|
const level = headerMatch[1].length;
|
|
@@ -86,7 +126,7 @@ export class MarkdownParser {
|
|
|
86
126
|
const contentLines = [];
|
|
87
127
|
for (let i = startLine; i < this.lines.length; i++) {
|
|
88
128
|
const line = this.lines[i];
|
|
89
|
-
const headerMatch = line.match(/^(#{1,6})\s+/);
|
|
129
|
+
const headerMatch = this.codeFenceLineMask[i] ? null : line.match(/^(#{1,6})\s+/);
|
|
90
130
|
if (headerMatch && headerMatch[1].length <= currentLevel) {
|
|
91
131
|
break;
|
|
92
132
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface MainSpecStructureIssue {
|
|
2
|
+
kind: 'delta-header' | 'requirement-outside-requirements';
|
|
3
|
+
line: number;
|
|
4
|
+
header: string;
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function findMainSpecStructureIssues(content: string): MainSpecStructureIssue[];
|
|
8
|
+
export declare function stripFencedCodeBlocksPreservingLines(content: string): string;
|
|
9
|
+
//# sourceMappingURL=spec-structure.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const REQUIREMENTS_SECTION_HEADER = /^##\s+Requirements\s*$/i;
|
|
2
|
+
const TOP_LEVEL_SECTION_HEADER = /^##\s+/;
|
|
3
|
+
const DELTA_HEADER = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\s*$/i;
|
|
4
|
+
const REQUIREMENT_HEADER = /^###\s+Requirement:\s*(.+)\s*$/;
|
|
5
|
+
export function findMainSpecStructureIssues(content) {
|
|
6
|
+
const normalized = content.replace(/\r\n?/g, '\n');
|
|
7
|
+
const stripped = stripFencedCodeBlocksPreservingLines(normalized);
|
|
8
|
+
const lines = stripped.split('\n');
|
|
9
|
+
const issues = [];
|
|
10
|
+
const requirementsHeaderIndex = lines.findIndex(line => REQUIREMENTS_SECTION_HEADER.test(line));
|
|
11
|
+
let requirementsEndIndex = lines.length;
|
|
12
|
+
if (requirementsHeaderIndex !== -1) {
|
|
13
|
+
for (let i = requirementsHeaderIndex + 1; i < lines.length; i++) {
|
|
14
|
+
if (TOP_LEVEL_SECTION_HEADER.test(lines[i])) {
|
|
15
|
+
requirementsEndIndex = i;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
for (let i = 0; i < lines.length; i++) {
|
|
21
|
+
const line = lines[i];
|
|
22
|
+
const trimmed = line.trim();
|
|
23
|
+
if (!trimmed) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (DELTA_HEADER.test(line)) {
|
|
27
|
+
issues.push({
|
|
28
|
+
kind: 'delta-header',
|
|
29
|
+
line: i + 1,
|
|
30
|
+
header: trimmed,
|
|
31
|
+
message: `Main spec contains delta header "${trimmed}". ` +
|
|
32
|
+
'Delta headers are only valid inside openspec/changes/<name>/specs/<capability>/spec.md ' +
|
|
33
|
+
'and truncate the parsed ## Requirements section.',
|
|
34
|
+
});
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const requirementMatch = line.match(REQUIREMENT_HEADER);
|
|
38
|
+
if (!requirementMatch) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const insideRequirements = requirementsHeaderIndex !== -1 &&
|
|
42
|
+
i > requirementsHeaderIndex &&
|
|
43
|
+
i < requirementsEndIndex;
|
|
44
|
+
if (!insideRequirements) {
|
|
45
|
+
issues.push({
|
|
46
|
+
kind: 'requirement-outside-requirements',
|
|
47
|
+
line: i + 1,
|
|
48
|
+
header: trimmed,
|
|
49
|
+
message: `Requirement header "${trimmed}" appears outside the main ## Requirements section. ` +
|
|
50
|
+
'Main specs only parse requirements inside that section, so this requirement is currently invisible to validate, list, and archive.',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return issues;
|
|
55
|
+
}
|
|
56
|
+
export function stripFencedCodeBlocksPreservingLines(content) {
|
|
57
|
+
const lines = content.split('\n');
|
|
58
|
+
const output = [];
|
|
59
|
+
let activeFence = null;
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})(.*)$/);
|
|
62
|
+
if (!activeFence) {
|
|
63
|
+
if (fenceMatch) {
|
|
64
|
+
activeFence = {
|
|
65
|
+
marker: fenceMatch[1][0],
|
|
66
|
+
length: fenceMatch[1].length,
|
|
67
|
+
};
|
|
68
|
+
output.push('');
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
output.push(line);
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
output.push('');
|
|
76
|
+
if (isClosingFence(line, activeFence)) {
|
|
77
|
+
activeFence = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return output.join('\n');
|
|
81
|
+
}
|
|
82
|
+
function isClosingFence(line, activeFence) {
|
|
83
|
+
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})\s*$/);
|
|
84
|
+
return Boolean(fenceMatch &&
|
|
85
|
+
fenceMatch[1][0] === activeFence.marker &&
|
|
86
|
+
fenceMatch[1].length >= activeFence.length);
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=spec-structure.js.map
|
package/dist/core/specs-apply.js
CHANGED
|
@@ -8,6 +8,7 @@ import { promises as fs } from 'fs';
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
import { extractRequirementsSection, parseDeltaSpec, normalizeRequirementName, } from './parsers/requirement-blocks.js';
|
|
11
|
+
import { findMainSpecStructureIssues } from './parsers/spec-structure.js';
|
|
11
12
|
import { Validator } from './validation/validator.js';
|
|
12
13
|
// -----------------------------------------------------------------------------
|
|
13
14
|
// Public API
|
|
@@ -153,6 +154,13 @@ export async function buildUpdatedSpec(update, changeName) {
|
|
|
153
154
|
isNewSpec = true;
|
|
154
155
|
targetContent = buildSpecSkeleton(specName, changeName);
|
|
155
156
|
}
|
|
157
|
+
const structureIssues = findMainSpecStructureIssues(targetContent);
|
|
158
|
+
if (structureIssues.length > 0) {
|
|
159
|
+
const details = structureIssues
|
|
160
|
+
.map(issue => `line ${issue.line}: ${issue.message}`)
|
|
161
|
+
.join('\n');
|
|
162
|
+
throw new Error(`${specName}: target spec is structurally invalid and cannot be updated until fixed:\n${details}`);
|
|
163
|
+
}
|
|
156
164
|
// Extract requirements section and build name->block map
|
|
157
165
|
const parts = extractRequirementsSection(targetContent);
|
|
158
166
|
const nameToBlock = new Map();
|
|
@@ -32,7 +32,7 @@ export function getApplyChangeSkillTemplate() {
|
|
|
32
32
|
\`\`\`
|
|
33
33
|
|
|
34
34
|
This returns:
|
|
35
|
-
-
|
|
35
|
+
- \`contextFiles\`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
|
|
36
36
|
- Progress (total, complete, remaining)
|
|
37
37
|
- Task list with status
|
|
38
38
|
- Dynamic instruction based on current state
|
|
@@ -44,7 +44,7 @@ export function getApplyChangeSkillTemplate() {
|
|
|
44
44
|
|
|
45
45
|
4. **Read context files**
|
|
46
46
|
|
|
47
|
-
Read
|
|
47
|
+
Read every file path listed under \`contextFiles\` from the apply instructions output.
|
|
48
48
|
The files depend on the schema being used:
|
|
49
49
|
- **spec-driven**: proposal, specs, design, tasks
|
|
50
50
|
- Other schemas: follow the contextFiles from CLI output
|
|
@@ -188,7 +188,7 @@ export function getOpsxApplyCommandTemplate() {
|
|
|
188
188
|
\`\`\`
|
|
189
189
|
|
|
190
190
|
This returns:
|
|
191
|
-
-
|
|
191
|
+
- \`contextFiles\`: artifact ID -> array of concrete file paths (varies by schema)
|
|
192
192
|
- Progress (total, complete, remaining)
|
|
193
193
|
- Task list with status
|
|
194
194
|
- Dynamic instruction based on current state
|
|
@@ -200,7 +200,7 @@ export function getOpsxApplyCommandTemplate() {
|
|
|
200
200
|
|
|
201
201
|
4. **Read context files**
|
|
202
202
|
|
|
203
|
-
Read
|
|
203
|
+
Read every file path listed under \`contextFiles\` from the apply instructions output.
|
|
204
204
|
The files depend on the schema being used:
|
|
205
205
|
- **spec-driven**: proposal, specs, design, tasks
|
|
206
206
|
- Other schemas: follow the contextFiles from CLI output
|
|
@@ -77,7 +77,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|
|
77
77
|
Display a table summarizing all changes:
|
|
78
78
|
|
|
79
79
|
\`\`\`
|
|
80
|
-
| Change
|
|
80
|
+
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
|
|
81
81
|
|---------------------|-----------|-------|---------|-----------|--------|
|
|
82
82
|
| schema-management | Done | 5/5 | 2 delta | None | Ready |
|
|
83
83
|
| project-config | Done | 3/3 | 1 delta | None | Ready |
|
|
@@ -323,7 +323,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|
|
323
323
|
Display a table summarizing all changes:
|
|
324
324
|
|
|
325
325
|
\`\`\`
|
|
326
|
-
| Change
|
|
326
|
+
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
|
|
327
327
|
|---------------------|-----------|-------|---------|-----------|--------|
|
|
328
328
|
| schema-management | Done | 5/5 | 2 delta | None | Ready |
|
|
329
329
|
| project-config | Done | 3/3 | 1 delta | None | Ready |
|
|
@@ -49,10 +49,10 @@ Depending on what the user brings, you might:
|
|
|
49
49
|
│ Use ASCII diagrams liberally │
|
|
50
50
|
├─────────────────────────────────────────┤
|
|
51
51
|
│ │
|
|
52
|
-
│
|
|
53
|
-
│
|
|
54
|
-
│
|
|
55
|
-
│
|
|
52
|
+
│ ┌────────┐ ┌────────┐ │
|
|
53
|
+
│ │ State │────────▶│ State │ │
|
|
54
|
+
│ │ A │ │ B │ │
|
|
55
|
+
│ └────────┘ └────────┘ │
|
|
56
56
|
│ │
|
|
57
57
|
│ System diagrams, state machines, │
|
|
58
58
|
│ data flows, architecture sketches, │
|
|
@@ -107,14 +107,14 @@ If the user mentions a change or you detect one is relevant:
|
|
|
107
107
|
|
|
108
108
|
3. **Offer to capture when decisions are made**
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
| Insight Type | Where to Capture |
|
|
111
|
+
|----------------------------|--------------------------------|
|
|
112
|
+
| New requirement discovered | \`specs/<capability>/spec.md\` |
|
|
113
|
+
| Requirement changed | \`specs/<capability>/spec.md\` |
|
|
114
|
+
| Design decision made | \`design.md\` |
|
|
115
|
+
| Scope changed | \`proposal.md\` |
|
|
116
|
+
| New work identified | \`tasks.md\` |
|
|
117
|
+
| Assumption invalidated | Relevant artifact |
|
|
118
118
|
|
|
119
119
|
Example offers:
|
|
120
120
|
- "That's a design decision. Capture it in design.md?"
|
|
@@ -220,7 +220,7 @@ User: A CLI tool that tracks local dev environments
|
|
|
220
220
|
You: That changes everything.
|
|
221
221
|
|
|
222
222
|
┌─────────────────────────────────────────────────┐
|
|
223
|
-
│
|
|
223
|
+
│ CLI TOOL DATA STORAGE │
|
|
224
224
|
└─────────────────────────────────────────────────┘
|
|
225
225
|
|
|
226
226
|
Key constraints:
|
|
@@ -344,10 +344,10 @@ Depending on what the user brings, you might:
|
|
|
344
344
|
│ Use ASCII diagrams liberally │
|
|
345
345
|
├─────────────────────────────────────────┤
|
|
346
346
|
│ │
|
|
347
|
-
│
|
|
348
|
-
│
|
|
349
|
-
│
|
|
350
|
-
│
|
|
347
|
+
│ ┌────────┐ ┌────────┐ │
|
|
348
|
+
│ │ State │────────▶│ State │ │
|
|
349
|
+
│ │ A │ │ B │ │
|
|
350
|
+
│ └────────┘ └────────┘ │
|
|
351
351
|
│ │
|
|
352
352
|
│ System diagrams, state machines, │
|
|
353
353
|
│ data flows, architecture sketches, │
|
|
@@ -404,14 +404,14 @@ If the user mentions a change or you detect one is relevant:
|
|
|
404
404
|
|
|
405
405
|
3. **Offer to capture when decisions are made**
|
|
406
406
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
407
|
+
| Insight Type | Where to Capture |
|
|
408
|
+
|----------------------------|--------------------------------|
|
|
409
|
+
| New requirement discovered | \`specs/<capability>/spec.md\` |
|
|
410
|
+
| Requirement changed | \`specs/<capability>/spec.md\` |
|
|
411
|
+
| Design decision made | \`design.md\` |
|
|
412
|
+
| Scope changed | \`proposal.md\` |
|
|
413
|
+
| New work identified | \`tasks.md\` |
|
|
414
|
+
| Assumption invalidated | Relevant artifact |
|
|
415
415
|
|
|
416
416
|
Example offers:
|
|
417
417
|
- "That's a design decision. Capture it in design.md?"
|
|
@@ -468,21 +468,21 @@ This same rhythm works for any size change—a small fix or a major feature.
|
|
|
468
468
|
|
|
469
469
|
**Core workflow:**
|
|
470
470
|
|
|
471
|
-
| Command
|
|
472
|
-
|
|
473
|
-
| \`/opsx:propose\` | Create a change and generate all artifacts |
|
|
474
|
-
| \`/opsx:explore\` | Think through problems before/during work
|
|
475
|
-
| \`/opsx:apply\`
|
|
476
|
-
| \`/opsx:archive\` | Archive a completed change
|
|
471
|
+
| Command | What it does |
|
|
472
|
+
|-------------------|--------------------------------------------|
|
|
473
|
+
| \`/opsx:propose\` | Create a change and generate all artifacts |
|
|
474
|
+
| \`/opsx:explore\` | Think through problems before/during work |
|
|
475
|
+
| \`/opsx:apply\` | Implement tasks from a change |
|
|
476
|
+
| \`/opsx:archive\` | Archive a completed change |
|
|
477
477
|
|
|
478
478
|
**Additional commands:**
|
|
479
479
|
|
|
480
|
-
| Command
|
|
481
|
-
|
|
482
|
-
| \`/opsx:new\`
|
|
483
|
-
| \`/opsx:continue\` | Continue working on an existing change
|
|
484
|
-
| \`/opsx:ff\`
|
|
485
|
-
| \`/opsx:verify\`
|
|
480
|
+
| Command | What it does |
|
|
481
|
+
|--------------------|----------------------------------------------------------|
|
|
482
|
+
| \`/opsx:new\` | Start a new change, step through artifacts one at a time |
|
|
483
|
+
| \`/opsx:continue\` | Continue working on an existing change |
|
|
484
|
+
| \`/opsx:ff\` | Fast-forward: create all artifacts at once |
|
|
485
|
+
| \`/opsx:verify\` | Verify implementation matches artifacts |
|
|
486
486
|
|
|
487
487
|
---
|
|
488
488
|
|
|
@@ -520,21 +520,21 @@ If the user says they just want to see the commands or skip the tutorial:
|
|
|
520
520
|
|
|
521
521
|
**Core workflow:**
|
|
522
522
|
|
|
523
|
-
| Command
|
|
524
|
-
|
|
525
|
-
| \`/opsx:propose <name>\` | Create a change and generate all artifacts |
|
|
526
|
-
| \`/opsx:explore\`
|
|
527
|
-
| \`/opsx:apply <name>\`
|
|
528
|
-
| \`/opsx:archive <name>\` | Archive when done
|
|
523
|
+
| Command | What it does |
|
|
524
|
+
|--------------------------|--------------------------------------------|
|
|
525
|
+
| \`/opsx:propose <name>\` | Create a change and generate all artifacts |
|
|
526
|
+
| \`/opsx:explore\` | Think through problems (no code changes) |
|
|
527
|
+
| \`/opsx:apply <name>\` | Implement tasks |
|
|
528
|
+
| \`/opsx:archive <name>\` | Archive when done |
|
|
529
529
|
|
|
530
530
|
**Additional commands:**
|
|
531
531
|
|
|
532
|
-
| Command
|
|
533
|
-
|
|
534
|
-
| \`/opsx:new <name>\`
|
|
535
|
-
| \`/opsx:continue <name>\` | Continue an existing change
|
|
536
|
-
| \`/opsx:ff <name>\`
|
|
537
|
-
| \`/opsx:verify <name>\`
|
|
532
|
+
| Command | What it does |
|
|
533
|
+
|---------------------------|-------------------------------------|
|
|
534
|
+
| \`/opsx:new <name>\` | Start a new change, step by step |
|
|
535
|
+
| \`/opsx:continue <name>\` | Continue an existing change |
|
|
536
|
+
| \`/opsx:ff <name>\` | Fast-forward: all artifacts at once |
|
|
537
|
+
| \`/opsx:verify <name>\` | Verify implementation |
|
|
538
538
|
|
|
539
539
|
Try \`/opsx:propose\` to start your first change.
|
|
540
540
|
\`\`\`
|
|
@@ -32,7 +32,7 @@ export function getVerifyChangeSkillTemplate() {
|
|
|
32
32
|
openspec instructions apply --change "<name>" --json
|
|
33
33
|
\`\`\`
|
|
34
34
|
|
|
35
|
-
This returns the change directory and
|
|
35
|
+
This returns the change directory and \`contextFiles\` (artifact ID -> array of concrete file paths). Read all available artifacts from \`contextFiles\`.
|
|
36
36
|
|
|
37
37
|
4. **Initialize verification report structure**
|
|
38
38
|
|
|
@@ -46,7 +46,7 @@ export function getVerifyChangeSkillTemplate() {
|
|
|
46
46
|
5. **Verify Completeness**
|
|
47
47
|
|
|
48
48
|
**Task Completion**:
|
|
49
|
-
- If tasks
|
|
49
|
+
- If \`contextFiles.tasks\` exists, read every file path in it
|
|
50
50
|
- Parse checkboxes: \`- [ ]\` (incomplete) vs \`- [x]\` (complete)
|
|
51
51
|
- Count complete vs total tasks
|
|
52
52
|
- If incomplete tasks exist:
|
|
@@ -85,7 +85,7 @@ export function getVerifyChangeSkillTemplate() {
|
|
|
85
85
|
7. **Verify Coherence**
|
|
86
86
|
|
|
87
87
|
**Design Adherence**:
|
|
88
|
-
- If design
|
|
88
|
+
- If \`contextFiles.design\` exists:
|
|
89
89
|
- Extract key decisions (look for sections like "Decision:", "Approach:", "Architecture:")
|
|
90
90
|
- Verify implementation follows those decisions
|
|
91
91
|
- If contradiction detected:
|
|
@@ -200,7 +200,7 @@ export function getOpsxVerifyCommandTemplate() {
|
|
|
200
200
|
openspec instructions apply --change "<name>" --json
|
|
201
201
|
\`\`\`
|
|
202
202
|
|
|
203
|
-
This returns the change directory and
|
|
203
|
+
This returns the change directory and \`contextFiles\` (artifact ID -> array of concrete file paths). Read all available artifacts from \`contextFiles\`.
|
|
204
204
|
|
|
205
205
|
4. **Initialize verification report structure**
|
|
206
206
|
|
|
@@ -214,7 +214,7 @@ export function getOpsxVerifyCommandTemplate() {
|
|
|
214
214
|
5. **Verify Completeness**
|
|
215
215
|
|
|
216
216
|
**Task Completion**:
|
|
217
|
-
- If tasks
|
|
217
|
+
- If \`contextFiles.tasks\` exists, read every file path in it
|
|
218
218
|
- Parse checkboxes: \`- [ ]\` (incomplete) vs \`- [x]\` (complete)
|
|
219
219
|
- Count complete vs total tasks
|
|
220
220
|
- If incomplete tasks exist:
|
|
@@ -253,7 +253,7 @@ export function getOpsxVerifyCommandTemplate() {
|
|
|
253
253
|
7. **Verify Coherence**
|
|
254
254
|
|
|
255
255
|
**Design Adherence**:
|
|
256
|
-
- If design
|
|
256
|
+
- If \`contextFiles.design\` exists:
|
|
257
257
|
- Extract key decisions (look for sections like "Decision:", "Approach:", "Architecture:")
|
|
258
258
|
- Verify implementation follows those decisions
|
|
259
259
|
- If contradiction detected:
|
package/dist/core/update.js
CHANGED
|
@@ -129,7 +129,7 @@ export class UpdateCommand {
|
|
|
129
129
|
const skillDir = path.join(skillsDir, dirName);
|
|
130
130
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
131
131
|
// Use hyphen-based command references for OpenCode
|
|
132
|
-
const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
|
|
132
|
+
const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined;
|
|
133
133
|
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
134
134
|
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
135
135
|
}
|
|
@@ -504,7 +504,7 @@ export class UpdateCommand {
|
|
|
504
504
|
const skillDir = path.join(skillsDir, dirName);
|
|
505
505
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
506
506
|
// Use hyphen-based command references for OpenCode
|
|
507
|
-
const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
|
|
507
|
+
const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined;
|
|
508
508
|
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
509
509
|
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
510
510
|
}
|
|
@@ -5,6 +5,7 @@ import { MarkdownParser } from '../parsers/markdown-parser.js';
|
|
|
5
5
|
import { ChangeParser } from '../parsers/change-parser.js';
|
|
6
6
|
import { MIN_PURPOSE_LENGTH, MAX_REQUIREMENT_TEXT_LENGTH, VALIDATION_MESSAGES } from './constants.js';
|
|
7
7
|
import { parseDeltaSpec, normalizeRequirementName } from '../parsers/requirement-blocks.js';
|
|
8
|
+
import { findMainSpecStructureIssues } from '../parsers/spec-structure.js';
|
|
8
9
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
9
10
|
export class Validator {
|
|
10
11
|
strictMode;
|
|
@@ -270,6 +271,14 @@ export class Validator {
|
|
|
270
271
|
}
|
|
271
272
|
applySpecRules(spec, content) {
|
|
272
273
|
const issues = [];
|
|
274
|
+
for (const structuralIssue of findMainSpecStructureIssues(content)) {
|
|
275
|
+
issues.push({
|
|
276
|
+
level: 'ERROR',
|
|
277
|
+
path: 'file',
|
|
278
|
+
line: structuralIssue.line,
|
|
279
|
+
message: structuralIssue.message,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
273
282
|
if (spec.overview.length < MIN_PURPOSE_LENGTH) {
|
|
274
283
|
issues.push({
|
|
275
284
|
level: 'WARNING',
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export declare const CONFIG_DIR_NAME = "openspec";
|
|
2
|
+
export declare const CONFIG_FILE_NAME = "config.json";
|
|
1
3
|
export interface TelemetryConfig {
|
|
2
4
|
anonymousId?: string;
|
|
3
5
|
noticeSeen?: boolean;
|
|
@@ -8,7 +10,11 @@ export interface GlobalConfig {
|
|
|
8
10
|
}
|
|
9
11
|
/**
|
|
10
12
|
* Get the path to the global config file.
|
|
11
|
-
*
|
|
13
|
+
* Follows XDG Base Directory Specification and platform conventions.
|
|
14
|
+
*
|
|
15
|
+
* - All platforms: $XDG_CONFIG_HOME/openspec/ if XDG_CONFIG_HOME is set
|
|
16
|
+
* - Unix/macOS fallback: ~/.config/openspec/
|
|
17
|
+
* - Windows fallback: %APPDATA%/openspec/
|
|
12
18
|
*/
|
|
13
19
|
export declare function getConfigPath(): string;
|
|
14
20
|
/**
|
package/dist/telemetry/config.js
CHANGED
|
@@ -1,17 +1,96 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Global configuration for telemetry state.
|
|
3
|
-
* Stores anonymous ID and notice-seen flag in
|
|
3
|
+
* Stores anonymous ID and notice-seen flag in the platform-appropriate config directory.
|
|
4
4
|
*/
|
|
5
5
|
import { promises as fs } from 'fs';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import os from 'os';
|
|
8
|
+
import { GLOBAL_CONFIG_DIR_NAME, GLOBAL_CONFIG_FILE_NAME, getGlobalConfigDir, } from '../core/global-config.js';
|
|
9
|
+
// Constants
|
|
10
|
+
export const CONFIG_DIR_NAME = GLOBAL_CONFIG_DIR_NAME;
|
|
11
|
+
export const CONFIG_FILE_NAME = GLOBAL_CONFIG_FILE_NAME;
|
|
12
|
+
function getConfigDir() {
|
|
13
|
+
return getGlobalConfigDir();
|
|
14
|
+
}
|
|
15
|
+
function getLegacyConfigPath() {
|
|
16
|
+
return path.join(os.homedir(), '.config', CONFIG_DIR_NAME, CONFIG_FILE_NAME);
|
|
17
|
+
}
|
|
18
|
+
async function readConfigFile(configPath) {
|
|
19
|
+
try {
|
|
20
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
21
|
+
return { status: 'ok', config: JSON.parse(content) };
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.code === 'ENOENT') {
|
|
25
|
+
return { status: 'missing' };
|
|
26
|
+
}
|
|
27
|
+
// If parse fails or another read error occurs, ignore the file.
|
|
28
|
+
return { status: 'invalid', config: {} };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function writeConfigFile(configPath, config) {
|
|
32
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
33
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
34
|
+
}
|
|
35
|
+
function hasMissingTelemetryFields(config) {
|
|
36
|
+
const telemetry = config.telemetry;
|
|
37
|
+
return (!telemetry ||
|
|
38
|
+
telemetry.anonymousId === undefined ||
|
|
39
|
+
telemetry.noticeSeen === undefined);
|
|
40
|
+
}
|
|
41
|
+
function mergeLegacyTelemetry(config, legacyConfig) {
|
|
42
|
+
const legacyTelemetry = legacyConfig.telemetry;
|
|
43
|
+
if (!legacyTelemetry) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const currentTelemetry = config.telemetry ?? {};
|
|
47
|
+
const shouldMigrate = (currentTelemetry.anonymousId === undefined && legacyTelemetry.anonymousId !== undefined) ||
|
|
48
|
+
(currentTelemetry.noticeSeen === undefined && legacyTelemetry.noticeSeen !== undefined);
|
|
49
|
+
if (!shouldMigrate) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
...config,
|
|
54
|
+
telemetry: {
|
|
55
|
+
...legacyTelemetry,
|
|
56
|
+
...currentTelemetry,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async function migrateLegacyTelemetryConfig(configPath, config, persist) {
|
|
61
|
+
const legacyConfigPath = getLegacyConfigPath();
|
|
62
|
+
if (path.resolve(configPath) === path.resolve(legacyConfigPath) || !hasMissingTelemetryFields(config)) {
|
|
63
|
+
return config;
|
|
64
|
+
}
|
|
65
|
+
const legacyRead = await readConfigFile(legacyConfigPath);
|
|
66
|
+
if (legacyRead.status !== 'ok') {
|
|
67
|
+
return config;
|
|
68
|
+
}
|
|
69
|
+
const migrated = mergeLegacyTelemetry(config, legacyRead.config);
|
|
70
|
+
if (!migrated) {
|
|
71
|
+
return config;
|
|
72
|
+
}
|
|
73
|
+
if (persist) {
|
|
74
|
+
try {
|
|
75
|
+
await writeConfigFile(configPath, migrated);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Preserve telemetry for this run even if the one-time migration cannot be persisted.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return migrated;
|
|
82
|
+
}
|
|
8
83
|
/**
|
|
9
84
|
* Get the path to the global config file.
|
|
10
|
-
*
|
|
85
|
+
* Follows XDG Base Directory Specification and platform conventions.
|
|
86
|
+
*
|
|
87
|
+
* - All platforms: $XDG_CONFIG_HOME/openspec/ if XDG_CONFIG_HOME is set
|
|
88
|
+
* - Unix/macOS fallback: ~/.config/openspec/
|
|
89
|
+
* - Windows fallback: %APPDATA%/openspec/
|
|
11
90
|
*/
|
|
12
91
|
export function getConfigPath() {
|
|
13
|
-
const configDir =
|
|
14
|
-
return path.join(configDir,
|
|
92
|
+
const configDir = getConfigDir();
|
|
93
|
+
return path.join(configDir, CONFIG_FILE_NAME);
|
|
15
94
|
}
|
|
16
95
|
/**
|
|
17
96
|
* Read the global config file.
|
|
@@ -19,17 +98,9 @@ export function getConfigPath() {
|
|
|
19
98
|
*/
|
|
20
99
|
export async function readConfig() {
|
|
21
100
|
const configPath = getConfigPath();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
catch (error) {
|
|
27
|
-
if (error.code === 'ENOENT') {
|
|
28
|
-
return {};
|
|
29
|
-
}
|
|
30
|
-
// If parse fails or other error, return empty config
|
|
31
|
-
return {};
|
|
32
|
-
}
|
|
101
|
+
const read = await readConfigFile(configPath);
|
|
102
|
+
const config = read.status === 'ok' ? read.config : {};
|
|
103
|
+
return migrateLegacyTelemetryConfig(configPath, config, read.status !== 'invalid');
|
|
33
104
|
}
|
|
34
105
|
/**
|
|
35
106
|
* Write to the global config file.
|
|
@@ -37,9 +108,6 @@ export async function readConfig() {
|
|
|
37
108
|
*/
|
|
38
109
|
export async function writeConfig(updates) {
|
|
39
110
|
const configPath = getConfigPath();
|
|
40
|
-
const configDir = path.dirname(configPath);
|
|
41
|
-
// Ensure directory exists
|
|
42
|
-
await fs.mkdir(configDir, { recursive: true });
|
|
43
111
|
// Read existing config and merge
|
|
44
112
|
const existing = await readConfig();
|
|
45
113
|
const merged = { ...existing, ...updates };
|
|
@@ -47,7 +115,7 @@ export async function writeConfig(updates) {
|
|
|
47
115
|
if (updates.telemetry && existing.telemetry) {
|
|
48
116
|
merged.telemetry = { ...existing.telemetry, ...updates.telemetry };
|
|
49
117
|
}
|
|
50
|
-
await
|
|
118
|
+
await writeConfigFile(configPath, merged);
|
|
51
119
|
}
|
|
52
120
|
/**
|
|
53
121
|
* Get the telemetry config section.
|