gspec 1.16.0 → 1.17.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 +33 -6
- package/bin/emitters.js +104 -0
- package/bin/gspec.js +214 -33
- package/commands/gspec.analyze.md +9 -0
- package/commands/gspec.audit.md +84 -9
- package/commands/gspec.feature.md +10 -0
- package/commands/gspec.implement.md +19 -8
- package/commands/gspec.tasks.md +150 -0
- package/dist/antigravity/gspec-analyze/SKILL.md +9 -0
- package/dist/antigravity/gspec-audit/SKILL.md +85 -10
- package/dist/antigravity/gspec-feature/SKILL.md +10 -0
- package/dist/antigravity/gspec-implement/SKILL.md +19 -8
- package/dist/antigravity/gspec-tasks/SKILL.md +154 -0
- package/dist/claude/gspec-analyze/SKILL.md +9 -0
- package/dist/claude/gspec-audit/SKILL.md +85 -10
- package/dist/claude/gspec-feature/SKILL.md +10 -0
- package/dist/claude/gspec-implement/SKILL.md +19 -8
- package/dist/claude/gspec-tasks/SKILL.md +155 -0
- package/dist/codex/gspec-analyze/SKILL.md +9 -0
- package/dist/codex/gspec-audit/SKILL.md +85 -10
- package/dist/codex/gspec-feature/SKILL.md +10 -0
- package/dist/codex/gspec-implement/SKILL.md +19 -8
- package/dist/codex/gspec-tasks/SKILL.md +154 -0
- package/dist/cursor/gspec-analyze.mdc +9 -0
- package/dist/cursor/gspec-audit.mdc +85 -10
- package/dist/cursor/gspec-feature.mdc +10 -0
- package/dist/cursor/gspec-implement.mdc +19 -8
- package/dist/cursor/gspec-tasks.mdc +153 -0
- package/dist/opencode/gspec-analyze/SKILL.md +9 -0
- package/dist/opencode/gspec-audit/SKILL.md +85 -10
- package/dist/opencode/gspec-feature/SKILL.md +10 -0
- package/dist/opencode/gspec-implement/SKILL.md +19 -8
- package/dist/opencode/gspec-tasks/SKILL.md +154 -0
- package/package.json +1 -1
- package/templates/spec-sync.md +2 -1
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ These documents become the shared context for all subsequent AI interactions. Wh
|
|
|
25
25
|
|
|
26
26
|
The only commands you *need* are the four fundamentals and `/gspec-implement`. Everything else exists to help when your project calls for it.
|
|
27
27
|
|
|
28
|
-
The fundamentals give your AI tool enough context to build well — it knows what the product is, how it should look, what technologies to use, and what engineering standards to follow. From there, `/gspec-implement` can take a plain-language description and start building. The remaining commands — `/gspec-research`, `/gspec-feature`, `/gspec-architect`, `/gspec-analyze`, and `/gspec-audit` — add structure and rigor when the scope or complexity warrants it.
|
|
28
|
+
The fundamentals give your AI tool enough context to build well — it knows what the product is, how it should look, what technologies to use, and what engineering standards to follow. From there, `/gspec-implement` can take a plain-language description and start building. The remaining commands — `/gspec-research`, `/gspec-feature`, `/gspec-architect`, `/gspec-tasks`, `/gspec-analyze`, and `/gspec-audit` — add structure and rigor when the scope or complexity warrants it.
|
|
29
29
|
|
|
30
30
|
```mermaid
|
|
31
31
|
flowchart LR
|
|
@@ -42,11 +42,14 @@ flowchart LR
|
|
|
42
42
|
Architect["4. Architect
|
|
43
43
|
technical blueprint"]
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
Plan["5. Plan
|
|
46
|
+
ordered tasks"]
|
|
47
|
+
|
|
48
|
+
Analyze["6. Analyze & Audit
|
|
46
49
|
reconcile specs
|
|
47
50
|
check specs vs code"]
|
|
48
51
|
|
|
49
|
-
Build["
|
|
52
|
+
Build["7. Build
|
|
50
53
|
implement"]
|
|
51
54
|
|
|
52
55
|
Define --> Research
|
|
@@ -55,9 +58,12 @@ flowchart LR
|
|
|
55
58
|
Research --> Specify
|
|
56
59
|
Research --> Build
|
|
57
60
|
Specify --> Architect
|
|
61
|
+
Specify --> Plan
|
|
58
62
|
Specify --> Build
|
|
63
|
+
Architect --> Plan
|
|
59
64
|
Architect --> Analyze
|
|
60
65
|
Architect --> Build
|
|
66
|
+
Plan --> Build
|
|
61
67
|
Analyze --> Build
|
|
62
68
|
Build --> Define
|
|
63
69
|
|
|
@@ -65,6 +71,7 @@ flowchart LR
|
|
|
65
71
|
style Research fill:#a855f7,color:#fff,stroke:none
|
|
66
72
|
style Specify fill:#f59e0b,color:#fff,stroke:none
|
|
67
73
|
style Architect fill:#f59e0b,color:#fff,stroke:none
|
|
74
|
+
style Plan fill:#f59e0b,color:#fff,stroke:none
|
|
68
75
|
style Analyze fill:#f59e0b,color:#fff,stroke:none
|
|
69
76
|
style Build fill:#22c55e,color:#fff,stroke:none
|
|
70
77
|
```
|
|
@@ -105,7 +112,15 @@ Use `/gspec-feature` when you want detailed PRDs with prioritized capabilities a
|
|
|
105
112
|
|
|
106
113
|
Use `/gspec-architect` when your feature involves significant technical complexity — new data models, service boundaries, auth flows, or integration points that benefit from upfront design. It also **identifies technical gaps and ambiguities** in your specs and proposes solutions, so that `/gspec-implement` can focus on building rather than making architectural decisions. For straightforward features, `/gspec-implement` can make sound architectural decisions on its own using your `stack` and `practices` specs.
|
|
107
114
|
|
|
108
|
-
**5.
|
|
115
|
+
**5. Plan** *(optional)* — Decompose a feature PRD into ordered work.
|
|
116
|
+
|
|
117
|
+
| Command | Role | What it produces |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `/gspec-tasks` | Engineering Lead | A sibling `gspec/features/<feature>.tasks.md` file with stable task IDs, explicit `deps:` lines, and `[P]` markers for parallel-safe work |
|
|
120
|
+
|
|
121
|
+
Use `/gspec-tasks` after `/gspec-feature` (and after `/gspec-architect` when it exists) for any feature large enough that build order matters or that has work which could legitimately run in parallel. The output is what `/gspec-implement` consumes — when a tasks file exists for an in-scope feature, implement plans phases from it, respecting deps and surfacing `[P]`-marked tasks for parallel execution. Trivial features can skip this step and go straight to `/gspec-implement`, which falls back to PRD-checkbox-driven planning.
|
|
122
|
+
|
|
123
|
+
**6. Analyze & Audit** *(optional)* — Reconcile discrepancies before building, and keep specs honest as the codebase evolves.
|
|
109
124
|
|
|
110
125
|
| Command | Role | What it does |
|
|
111
126
|
|---|---|---|
|
|
@@ -116,11 +131,11 @@ Use `/gspec-analyze` after `/gspec-architect` (or any time multiple specs exist)
|
|
|
116
131
|
|
|
117
132
|
Use `/gspec-audit` periodically — before a major release, after a long sprint, or any time you suspect docs have drifted from code. Audit reads package manifests, configs, source files, and test output, then asks you per-finding whether to update the spec to match the code, keep the spec and fix the code separately, or defer. Each finding is presented one at a time with the spec quote and the code evidence side by side. Audit never modifies code.
|
|
118
133
|
|
|
119
|
-
**
|
|
134
|
+
**7. Build** — Implement with full context.
|
|
120
135
|
|
|
121
136
|
| Command | Role | What it does |
|
|
122
137
|
|---|---|---|
|
|
123
|
-
| `/gspec-implement` | Senior Engineer | Reads all specs, plans the build order, and implements |
|
|
138
|
+
| `/gspec-implement` | Senior Engineer | Reads all specs (including any `*.tasks.md` files), plans the build order, and implements |
|
|
124
139
|
|
|
125
140
|
**Spec Sync** — gspec includes always-on spec sync that automatically keeps your specification documents in sync as the code evolves. This is installed alongside the skills and requires no manual intervention — when code changes affect spec-documented behavior, the sync rules prompt your AI tool to update the relevant gspec files.
|
|
126
141
|
|
|
@@ -182,6 +197,18 @@ Saved specs are organized by type in `~/.gspec/` (profiles, stacks, styles, prac
|
|
|
182
197
|
gspec restore playbook/my-starter
|
|
183
198
|
```
|
|
184
199
|
|
|
200
|
+
## Extensions
|
|
201
|
+
|
|
202
|
+
Author your own skills and have them auto-installed alongside the built-in `gspec-*` commands in every project. Extensions live in `~/.gspec/extensions/` as Markdown files with `name` and `description` frontmatter and the same shape as anything in `commands/`.
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
gspec extension save ./my-deploy.md # Install a local skill file as a user extension
|
|
206
|
+
gspec extension list # See what's installed
|
|
207
|
+
gspec extension remove my-deploy # Delete from ~/.gspec/extensions/
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
When you next run `npx gspec` in a project, the installer copies the built-in skills first, then emits each valid extension into the same per-platform install directory using the same formatting. Extension names that collide with built-in `gspec-*` skills are rejected with an error; malformed or duplicate extensions are skipped with a warning.
|
|
211
|
+
|
|
185
212
|
## Output Structure
|
|
186
213
|
|
|
187
214
|
All specifications live in a `gspec/` directory at your project root:
|
package/bin/emitters.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Placeholder pattern used in generic command files
|
|
5
|
+
export const PLACEHOLDER_RE = /<<<\w+>>>/g;
|
|
6
|
+
|
|
7
|
+
export function buildFrontmatter(fields) {
|
|
8
|
+
const lines = ['---'];
|
|
9
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
10
|
+
lines.push(`${key}: ${value}`);
|
|
11
|
+
}
|
|
12
|
+
lines.push('---');
|
|
13
|
+
return lines.join('\n');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Platform target definitions: how to emit a skill file for each AI tool.
|
|
17
|
+
// Used by both `scripts/build.js` (writing to dist/) and `bin/gspec.js`
|
|
18
|
+
// (writing user-installed extensions directly to a project's install dir).
|
|
19
|
+
export const TARGETS = {
|
|
20
|
+
claude: {
|
|
21
|
+
label: 'Claude Code',
|
|
22
|
+
distSubdir: 'claude',
|
|
23
|
+
installDir: '.claude/skills',
|
|
24
|
+
layout: 'directory',
|
|
25
|
+
// .claude/skills/<name>/SKILL.md
|
|
26
|
+
async emit(outDir, content, meta) {
|
|
27
|
+
const frontmatter = buildFrontmatter({
|
|
28
|
+
name: meta.name,
|
|
29
|
+
description: meta.description,
|
|
30
|
+
});
|
|
31
|
+
const body = content.replace(PLACEHOLDER_RE, '$ARGUMENTS');
|
|
32
|
+
const skillDir = join(outDir, meta.name);
|
|
33
|
+
await mkdir(skillDir, { recursive: true });
|
|
34
|
+
await writeFile(join(skillDir, 'SKILL.md'), frontmatter + '\n\n' + body, 'utf-8');
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
cursor: {
|
|
38
|
+
label: 'Cursor',
|
|
39
|
+
distSubdir: 'cursor',
|
|
40
|
+
installDir: '.cursor/commands',
|
|
41
|
+
layout: 'flat',
|
|
42
|
+
// .cursor/commands/<name>.mdc (flat file)
|
|
43
|
+
async emit(outDir, content, meta) {
|
|
44
|
+
const frontmatter = buildFrontmatter({
|
|
45
|
+
description: meta.description,
|
|
46
|
+
});
|
|
47
|
+
// Cursor has no $ARGUMENTS convention; strip the placeholder lines
|
|
48
|
+
const body = content.replace(/^.*<<<\w+>>>.*$\n?/gm, '');
|
|
49
|
+
await mkdir(outDir, { recursive: true });
|
|
50
|
+
await writeFile(join(outDir, `${meta.name}.mdc`), frontmatter + '\n\n' + body, 'utf-8');
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
antigravity: {
|
|
54
|
+
label: 'Antigravity',
|
|
55
|
+
distSubdir: 'antigravity',
|
|
56
|
+
installDir: '.agent/skills',
|
|
57
|
+
layout: 'directory',
|
|
58
|
+
// .agent/skills/<name>/SKILL.md
|
|
59
|
+
async emit(outDir, content, meta) {
|
|
60
|
+
const frontmatter = buildFrontmatter({
|
|
61
|
+
name: meta.name,
|
|
62
|
+
description: meta.description,
|
|
63
|
+
});
|
|
64
|
+
const body = content.replace(/^.*<<<\w+>>>.*$\n?/gm, '');
|
|
65
|
+
const skillDir = join(outDir, meta.name);
|
|
66
|
+
await mkdir(skillDir, { recursive: true });
|
|
67
|
+
await writeFile(join(skillDir, 'SKILL.md'), frontmatter + '\n\n' + body, 'utf-8');
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
codex: {
|
|
71
|
+
label: 'Codex',
|
|
72
|
+
distSubdir: 'codex',
|
|
73
|
+
installDir: '.agents/skills',
|
|
74
|
+
layout: 'directory',
|
|
75
|
+
// .agents/skills/<name>/SKILL.md
|
|
76
|
+
async emit(outDir, content, meta) {
|
|
77
|
+
const frontmatter = buildFrontmatter({
|
|
78
|
+
name: meta.name,
|
|
79
|
+
description: meta.description,
|
|
80
|
+
});
|
|
81
|
+
const body = content.replace(/^.*<<<\w+>>>.*$\n?/gm, '');
|
|
82
|
+
const skillDir = join(outDir, meta.name);
|
|
83
|
+
await mkdir(skillDir, { recursive: true });
|
|
84
|
+
await writeFile(join(skillDir, 'SKILL.md'), frontmatter + '\n\n' + body, 'utf-8');
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
opencode: {
|
|
88
|
+
label: 'Open Code',
|
|
89
|
+
distSubdir: 'opencode',
|
|
90
|
+
installDir: '.opencode/skills',
|
|
91
|
+
layout: 'directory',
|
|
92
|
+
// .opencode/skills/<name>/SKILL.md
|
|
93
|
+
async emit(outDir, content, meta) {
|
|
94
|
+
const frontmatter = buildFrontmatter({
|
|
95
|
+
name: meta.name,
|
|
96
|
+
description: meta.description,
|
|
97
|
+
});
|
|
98
|
+
const body = content.replace(/^.*<<<\w+>>>.*$\n?/gm, '');
|
|
99
|
+
const skillDir = join(outDir, meta.name);
|
|
100
|
+
await mkdir(skillDir, { recursive: true });
|
|
101
|
+
await writeFile(join(skillDir, 'SKILL.md'), frontmatter + '\n\n' + body, 'utf-8');
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
package/bin/gspec.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { program } from 'commander';
|
|
4
|
-
import { readdir, readFile, writeFile, mkdir, stat } from 'node:fs/promises';
|
|
4
|
+
import { readdir, readFile, writeFile, mkdir, stat, unlink } from 'node:fs/promises';
|
|
5
5
|
import { join, dirname, basename } from 'node:path';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { createInterface } from 'node:readline';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
|
+
import { TARGETS as EMITTER_TARGETS } from './emitters.js';
|
|
10
11
|
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const DIST_DIR = join(__dirname, '..', 'dist');
|
|
@@ -28,38 +29,21 @@ const BANNER = `
|
|
|
28
29
|
${chalk.white('═════════════════════════════baller.software═══')}
|
|
29
30
|
`;
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
installDir: '.agent/skills',
|
|
47
|
-
label: 'Antigravity',
|
|
48
|
-
layout: 'directory',
|
|
49
|
-
},
|
|
50
|
-
codex: {
|
|
51
|
-
sourceDir: join(DIST_DIR, 'codex'),
|
|
52
|
-
installDir: '.agents/skills',
|
|
53
|
-
label: 'Codex',
|
|
54
|
-
layout: 'directory',
|
|
55
|
-
},
|
|
56
|
-
opencode: {
|
|
57
|
-
sourceDir: join(DIST_DIR, 'opencode'),
|
|
58
|
-
installDir: '.opencode/skills',
|
|
59
|
-
label: 'Open Code',
|
|
60
|
-
layout: 'directory',
|
|
61
|
-
},
|
|
62
|
-
};
|
|
32
|
+
// Derive install-side TARGETS from the shared emitter config so we have one source of truth.
|
|
33
|
+
// `sourceDir` is computed from the shared `distSubdir`; `emit` is reused for installing user extensions.
|
|
34
|
+
const TARGETS = Object.fromEntries(
|
|
35
|
+
Object.entries(EMITTER_TARGETS).map(([key, t]) => [key, {
|
|
36
|
+
...t,
|
|
37
|
+
sourceDir: join(DIST_DIR, t.distSubdir),
|
|
38
|
+
}]),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Names emitted by core gspec; user extensions cannot collide with these.
|
|
42
|
+
const BUILTIN_SKILL_NAMES = new Set([
|
|
43
|
+
'gspec-profile', 'gspec-feature', 'gspec-tasks', 'gspec-style',
|
|
44
|
+
'gspec-stack', 'gspec-practices', 'gspec-architect', 'gspec-analyze',
|
|
45
|
+
'gspec-audit', 'gspec-research', 'gspec-implement', 'gspec-migrate',
|
|
46
|
+
]);
|
|
63
47
|
|
|
64
48
|
const TARGET_CHOICES = [
|
|
65
49
|
{ key: '1', name: 'claude', label: 'Claude Code' },
|
|
@@ -1397,6 +1381,8 @@ program
|
|
|
1397
1381
|
|
|
1398
1382
|
await install(targetName, process.cwd());
|
|
1399
1383
|
|
|
1384
|
+
await installExtensions(targetName, process.cwd());
|
|
1385
|
+
|
|
1400
1386
|
await seedFromSavedSpecs(process.cwd());
|
|
1401
1387
|
|
|
1402
1388
|
await installSpecSync(targetName, process.cwd());
|
|
@@ -1424,6 +1410,176 @@ program
|
|
|
1424
1410
|
}
|
|
1425
1411
|
});
|
|
1426
1412
|
|
|
1413
|
+
// --- Extensions ---
|
|
1414
|
+
|
|
1415
|
+
const EXTENSIONS_DIR = join(GSPEC_HOME, 'extensions');
|
|
1416
|
+
const EXTENSION_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
1417
|
+
|
|
1418
|
+
async function loadExtensions() {
|
|
1419
|
+
let entries;
|
|
1420
|
+
try {
|
|
1421
|
+
entries = await readdir(EXTENSIONS_DIR);
|
|
1422
|
+
} catch (e) {
|
|
1423
|
+
if (e.code === 'ENOENT') return [];
|
|
1424
|
+
throw e;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
const files = entries.filter((f) => f.endsWith('.md'));
|
|
1428
|
+
const loaded = [];
|
|
1429
|
+
for (const file of files) {
|
|
1430
|
+
const path = join(EXTENSIONS_DIR, file);
|
|
1431
|
+
const content = await readFile(path, 'utf-8');
|
|
1432
|
+
const { fields, body } = parseFrontmatter(content);
|
|
1433
|
+
loaded.push({ file, path, fields, body, content });
|
|
1434
|
+
}
|
|
1435
|
+
return loaded;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function validateExtension(ext) {
|
|
1439
|
+
const errors = [];
|
|
1440
|
+
if (!ext.fields.name) errors.push("missing 'name' frontmatter");
|
|
1441
|
+
if (!ext.fields.description) errors.push("missing 'description' frontmatter");
|
|
1442
|
+
if (ext.fields.name && !EXTENSION_NAME_RE.test(ext.fields.name)) {
|
|
1443
|
+
errors.push(`invalid name "${ext.fields.name}" (must match /^[a-z0-9][a-z0-9-]*$/)`);
|
|
1444
|
+
}
|
|
1445
|
+
if (ext.fields.name && BUILTIN_SKILL_NAMES.has(ext.fields.name)) {
|
|
1446
|
+
errors.push(`name "${ext.fields.name}" collides with a built-in gspec skill`);
|
|
1447
|
+
}
|
|
1448
|
+
return errors;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
async function installExtensions(targetName, cwd) {
|
|
1452
|
+
const extensions = await loadExtensions();
|
|
1453
|
+
if (extensions.length === 0) return;
|
|
1454
|
+
|
|
1455
|
+
const target = TARGETS[targetName];
|
|
1456
|
+
const valid = [];
|
|
1457
|
+
for (const ext of extensions) {
|
|
1458
|
+
const errors = validateExtension(ext);
|
|
1459
|
+
if (errors.length > 0) {
|
|
1460
|
+
console.warn(chalk.yellow(` ! Skipping extension ${ext.file}: ${errors.join('; ')}`));
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
valid.push(ext);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// Resolve duplicates by name (last write wins, with a warning)
|
|
1467
|
+
const byName = new Map();
|
|
1468
|
+
for (const ext of valid) {
|
|
1469
|
+
if (byName.has(ext.fields.name)) {
|
|
1470
|
+
console.warn(chalk.yellow(
|
|
1471
|
+
` ! Extension name "${ext.fields.name}" defined in two files; ${ext.file} overrides ${byName.get(ext.fields.name).file}`
|
|
1472
|
+
));
|
|
1473
|
+
}
|
|
1474
|
+
byName.set(ext.fields.name, ext);
|
|
1475
|
+
}
|
|
1476
|
+
const finalSet = Array.from(byName.values());
|
|
1477
|
+
if (finalSet.length === 0) return;
|
|
1478
|
+
|
|
1479
|
+
console.log(chalk.bold(`\nInstalling ${finalSet.length} user extension${finalSet.length === 1 ? '' : 's'} from ~/.gspec/extensions/...\n`));
|
|
1480
|
+
const installPath = join(cwd, target.installDir);
|
|
1481
|
+
for (const ext of finalSet) {
|
|
1482
|
+
const meta = { name: ext.fields.name, description: ext.fields.description };
|
|
1483
|
+
await target.emit(installPath, ext.body, meta);
|
|
1484
|
+
console.log(` ${chalk.green('+')} ${ext.fields.name} ${chalk.dim('(extension)')}`);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
async function extensionList() {
|
|
1489
|
+
console.log(BANNER);
|
|
1490
|
+
const extensions = await loadExtensions();
|
|
1491
|
+
if (extensions.length === 0) {
|
|
1492
|
+
console.log(chalk.dim('\n No extensions installed in ~/.gspec/extensions/.\n'));
|
|
1493
|
+
console.log(chalk.dim(' Use "gspec extension save <path>" to install one.\n'));
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
console.log(chalk.bold(`\n ${extensions.length} extension${extensions.length === 1 ? '' : 's'} in ~/.gspec/extensions/:\n`));
|
|
1498
|
+
for (const ext of extensions) {
|
|
1499
|
+
const errors = validateExtension(ext);
|
|
1500
|
+
const name = ext.fields.name || chalk.dim('(no name)');
|
|
1501
|
+
const desc = ext.fields.description ? chalk.dim(` — ${ext.fields.description}`) : '';
|
|
1502
|
+
if (errors.length > 0) {
|
|
1503
|
+
console.log(` ${chalk.yellow('!')} ${ext.file} → ${name}${desc}`);
|
|
1504
|
+
console.log(` ${chalk.yellow(errors.join('; '))}`);
|
|
1505
|
+
} else {
|
|
1506
|
+
console.log(` ${chalk.green('•')} ${name}${desc}`);
|
|
1507
|
+
console.log(` ${chalk.dim(ext.file)}`);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
console.log();
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
async function extensionSave(srcPath) {
|
|
1514
|
+
console.log(BANNER);
|
|
1515
|
+
|
|
1516
|
+
if (!srcPath) {
|
|
1517
|
+
console.error(chalk.red('\n Usage: gspec extension save <path-to-extension.md>\n'));
|
|
1518
|
+
process.exit(1);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
let content;
|
|
1522
|
+
try {
|
|
1523
|
+
content = await readFile(srcPath, 'utf-8');
|
|
1524
|
+
} catch (e) {
|
|
1525
|
+
if (e.code === 'ENOENT') {
|
|
1526
|
+
console.error(chalk.red(`\n File not found: ${srcPath}\n`));
|
|
1527
|
+
process.exit(1);
|
|
1528
|
+
}
|
|
1529
|
+
throw e;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
const { fields } = parseFrontmatter(content);
|
|
1533
|
+
const ext = { file: basename(srcPath), fields };
|
|
1534
|
+
const errors = validateExtension(ext);
|
|
1535
|
+
if (errors.length > 0) {
|
|
1536
|
+
console.error(chalk.red(`\n Cannot save extension: ${errors.join('; ')}\n`));
|
|
1537
|
+
process.exit(1);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
await mkdir(EXTENSIONS_DIR, { recursive: true });
|
|
1541
|
+
const destPath = join(EXTENSIONS_DIR, `${fields.name}.md`);
|
|
1542
|
+
|
|
1543
|
+
try {
|
|
1544
|
+
await stat(destPath);
|
|
1545
|
+
const overwrite = await promptConfirm(chalk.yellow(`\n Extension "${fields.name}" already exists. Overwrite? [y/N]: `));
|
|
1546
|
+
if (!overwrite) {
|
|
1547
|
+
console.log(chalk.dim('\n Cancelled.\n'));
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
} catch (e) {
|
|
1551
|
+
if (e.code !== 'ENOENT') throw e;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
await writeFile(destPath, content, 'utf-8');
|
|
1555
|
+
console.log(chalk.green(`\n ✓ Saved extension to ~/.gspec/extensions/${fields.name}.md\n`));
|
|
1556
|
+
console.log(chalk.dim(` It will be installed alongside core skills the next time you run "gspec" in a project.\n`));
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
async function extensionRemove(name) {
|
|
1560
|
+
console.log(BANNER);
|
|
1561
|
+
|
|
1562
|
+
if (!name) {
|
|
1563
|
+
console.error(chalk.red('\n Usage: gspec extension remove <name>\n'));
|
|
1564
|
+
process.exit(1);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
const path = join(EXTENSIONS_DIR, `${name}.md`);
|
|
1568
|
+
try {
|
|
1569
|
+
await stat(path);
|
|
1570
|
+
} catch (e) {
|
|
1571
|
+
if (e.code === 'ENOENT') {
|
|
1572
|
+
console.error(chalk.red(`\n Extension not found: ~/.gspec/extensions/${name}.md\n`));
|
|
1573
|
+
process.exit(1);
|
|
1574
|
+
}
|
|
1575
|
+
throw e;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
await unlink(path);
|
|
1579
|
+
console.log(chalk.green(`\n ✓ Removed ~/.gspec/extensions/${name}.md\n`));
|
|
1580
|
+
console.log(chalk.dim(` Already-installed copies in projects (.claude/skills/, .cursor/commands/, etc.) are left in place — delete them manually if desired.\n`));
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1427
1583
|
program
|
|
1428
1584
|
.command('save')
|
|
1429
1585
|
.description('Save a gspec spec to ~/.gspec for reuse across projects')
|
|
@@ -1446,4 +1602,29 @@ program
|
|
|
1446
1602
|
await createPlaybook();
|
|
1447
1603
|
});
|
|
1448
1604
|
|
|
1605
|
+
const extensionCmd = program
|
|
1606
|
+
.command('extension')
|
|
1607
|
+
.description('Manage user-authored gspec extension skills in ~/.gspec/extensions/');
|
|
1608
|
+
|
|
1609
|
+
extensionCmd
|
|
1610
|
+
.command('list')
|
|
1611
|
+
.description('List installed extensions')
|
|
1612
|
+
.action(async () => {
|
|
1613
|
+
await extensionList();
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
extensionCmd
|
|
1617
|
+
.command('save <path>')
|
|
1618
|
+
.description('Save a local .md skill file as a user extension in ~/.gspec/extensions/')
|
|
1619
|
+
.action(async (path) => {
|
|
1620
|
+
await extensionSave(path);
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
extensionCmd
|
|
1624
|
+
.command('remove <name>')
|
|
1625
|
+
.description('Remove a user extension from ~/.gspec/extensions/ (does not uninstall already-emitted copies)')
|
|
1626
|
+
.action(async (name) => {
|
|
1627
|
+
await extensionRemove(name);
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1449
1630
|
program.parse();
|
|
@@ -31,6 +31,7 @@ Read **every** available gspec document in this order:
|
|
|
31
31
|
6. `gspec/architecture.md` — Technical blueprint: project structure, data model, API design, environment
|
|
32
32
|
7. `gspec/research.md` — Competitive analysis and feature proposals
|
|
33
33
|
8. `gspec/features/*.md` — Individual feature requirements and dependencies
|
|
34
|
+
9. `gspec/features/*.tasks.md` — For any feature that has a tasks file, read it alongside the PRD. Tasks files declare a build order and parallelism strategy that must stay consistent with the PRD's capabilities
|
|
34
35
|
|
|
35
36
|
If fewer than two spec files exist, inform the user that there is nothing to cross-reference and stop.
|
|
36
37
|
|
|
@@ -75,6 +76,14 @@ Systematically compare specs against each other. Look for these categories of di
|
|
|
75
76
|
- Acceptance criteria in a feature PRD contradict architectural decisions
|
|
76
77
|
- Edge cases handled differently across specs
|
|
77
78
|
|
|
79
|
+
#### Tasks ↔ PRD Conflicts
|
|
80
|
+
For any feature that has a `gspec/features/<feature>.tasks.md` file, validate the tasks file against its PRD:
|
|
81
|
+
- A task's `covers:` line quotes capability text that does not exist in the PRD (orphan task)
|
|
82
|
+
- A PRD capability is not `covers:`-referenced by any task in the tasks file (orphan capability — every unchecked capability must be covered by at least one task)
|
|
83
|
+
- A task's checkbox is `- [x]` but its covered capability is still `- [ ]` in the PRD, or vice versa (state inconsistency)
|
|
84
|
+
- A task's `deps:` references a task ID that does not exist in the file
|
|
85
|
+
- The tasks file's `feature:` frontmatter slug does not match its filename's feature slug
|
|
86
|
+
|
|
78
87
|
**Do NOT flag:**
|
|
79
88
|
- Minor wording or style differences that don't change meaning
|
|
80
89
|
- Missing information (gaps are for `gspec-architect` to handle)
|