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.
Files changed (35) hide show
  1. package/README.md +33 -6
  2. package/bin/emitters.js +104 -0
  3. package/bin/gspec.js +214 -33
  4. package/commands/gspec.analyze.md +9 -0
  5. package/commands/gspec.audit.md +84 -9
  6. package/commands/gspec.feature.md +10 -0
  7. package/commands/gspec.implement.md +19 -8
  8. package/commands/gspec.tasks.md +150 -0
  9. package/dist/antigravity/gspec-analyze/SKILL.md +9 -0
  10. package/dist/antigravity/gspec-audit/SKILL.md +85 -10
  11. package/dist/antigravity/gspec-feature/SKILL.md +10 -0
  12. package/dist/antigravity/gspec-implement/SKILL.md +19 -8
  13. package/dist/antigravity/gspec-tasks/SKILL.md +154 -0
  14. package/dist/claude/gspec-analyze/SKILL.md +9 -0
  15. package/dist/claude/gspec-audit/SKILL.md +85 -10
  16. package/dist/claude/gspec-feature/SKILL.md +10 -0
  17. package/dist/claude/gspec-implement/SKILL.md +19 -8
  18. package/dist/claude/gspec-tasks/SKILL.md +155 -0
  19. package/dist/codex/gspec-analyze/SKILL.md +9 -0
  20. package/dist/codex/gspec-audit/SKILL.md +85 -10
  21. package/dist/codex/gspec-feature/SKILL.md +10 -0
  22. package/dist/codex/gspec-implement/SKILL.md +19 -8
  23. package/dist/codex/gspec-tasks/SKILL.md +154 -0
  24. package/dist/cursor/gspec-analyze.mdc +9 -0
  25. package/dist/cursor/gspec-audit.mdc +85 -10
  26. package/dist/cursor/gspec-feature.mdc +10 -0
  27. package/dist/cursor/gspec-implement.mdc +19 -8
  28. package/dist/cursor/gspec-tasks.mdc +153 -0
  29. package/dist/opencode/gspec-analyze/SKILL.md +9 -0
  30. package/dist/opencode/gspec-audit/SKILL.md +85 -10
  31. package/dist/opencode/gspec-feature/SKILL.md +10 -0
  32. package/dist/opencode/gspec-implement/SKILL.md +19 -8
  33. package/dist/opencode/gspec-tasks/SKILL.md +154 -0
  34. package/package.json +1 -1
  35. 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
- Analyze["5. Analyze & Audit
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["6. 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. Analyze & Audit** *(optional)* — Reconcile discrepancies before building, and keep specs honest as the codebase evolves.
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
- **6. Build** — Implement with full context.
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:
@@ -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
- const TARGETS = {
32
- claude: {
33
- sourceDir: join(DIST_DIR, 'claude'),
34
- installDir: '.claude/skills',
35
- label: 'Claude Code',
36
- layout: 'directory',
37
- },
38
- cursor: {
39
- sourceDir: join(DIST_DIR, 'cursor'),
40
- installDir: '.cursor/commands',
41
- label: 'Cursor',
42
- layout: 'flat',
43
- },
44
- antigravity: {
45
- sourceDir: join(DIST_DIR, 'antigravity'),
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)