gspec 1.14.0 → 1.16.0

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 (65) hide show
  1. package/README.md +20 -9
  2. package/bin/gspec.js +218 -59
  3. package/commands/gspec.analyze.md +13 -8
  4. package/commands/gspec.audit.md +202 -0
  5. package/commands/gspec.implement.md +10 -7
  6. package/commands/gspec.migrate.md +29 -15
  7. package/commands/gspec.profile.md +55 -35
  8. package/commands/gspec.style.md +64 -12
  9. package/dist/antigravity/gspec-analyze/SKILL.md +14 -9
  10. package/dist/antigravity/gspec-architect/SKILL.md +1 -1
  11. package/dist/antigravity/gspec-audit/SKILL.md +206 -0
  12. package/dist/antigravity/gspec-feature/SKILL.md +1 -1
  13. package/dist/antigravity/gspec-implement/SKILL.md +11 -8
  14. package/dist/antigravity/gspec-migrate/SKILL.md +30 -16
  15. package/dist/antigravity/gspec-practices/SKILL.md +1 -1
  16. package/dist/antigravity/gspec-profile/SKILL.md +56 -36
  17. package/dist/antigravity/gspec-research/SKILL.md +1 -1
  18. package/dist/antigravity/gspec-stack/SKILL.md +1 -1
  19. package/dist/antigravity/gspec-style/SKILL.md +65 -13
  20. package/dist/claude/gspec-analyze/SKILL.md +14 -9
  21. package/dist/claude/gspec-architect/SKILL.md +1 -1
  22. package/dist/claude/gspec-audit/SKILL.md +207 -0
  23. package/dist/claude/gspec-feature/SKILL.md +1 -1
  24. package/dist/claude/gspec-implement/SKILL.md +11 -8
  25. package/dist/claude/gspec-migrate/SKILL.md +30 -16
  26. package/dist/claude/gspec-practices/SKILL.md +1 -1
  27. package/dist/claude/gspec-profile/SKILL.md +56 -36
  28. package/dist/claude/gspec-research/SKILL.md +1 -1
  29. package/dist/claude/gspec-stack/SKILL.md +1 -1
  30. package/dist/claude/gspec-style/SKILL.md +65 -13
  31. package/dist/codex/gspec-analyze/SKILL.md +14 -9
  32. package/dist/codex/gspec-architect/SKILL.md +1 -1
  33. package/dist/codex/gspec-audit/SKILL.md +206 -0
  34. package/dist/codex/gspec-feature/SKILL.md +1 -1
  35. package/dist/codex/gspec-implement/SKILL.md +11 -8
  36. package/dist/codex/gspec-migrate/SKILL.md +30 -16
  37. package/dist/codex/gspec-practices/SKILL.md +1 -1
  38. package/dist/codex/gspec-profile/SKILL.md +56 -36
  39. package/dist/codex/gspec-research/SKILL.md +1 -1
  40. package/dist/codex/gspec-stack/SKILL.md +1 -1
  41. package/dist/codex/gspec-style/SKILL.md +65 -13
  42. package/dist/cursor/gspec-analyze.mdc +14 -9
  43. package/dist/cursor/gspec-architect.mdc +1 -1
  44. package/dist/cursor/gspec-audit.mdc +205 -0
  45. package/dist/cursor/gspec-feature.mdc +1 -1
  46. package/dist/cursor/gspec-implement.mdc +11 -8
  47. package/dist/cursor/gspec-migrate.mdc +30 -16
  48. package/dist/cursor/gspec-practices.mdc +1 -1
  49. package/dist/cursor/gspec-profile.mdc +56 -36
  50. package/dist/cursor/gspec-research.mdc +1 -1
  51. package/dist/cursor/gspec-stack.mdc +1 -1
  52. package/dist/cursor/gspec-style.mdc +65 -13
  53. package/dist/opencode/gspec-analyze/SKILL.md +14 -9
  54. package/dist/opencode/gspec-architect/SKILL.md +1 -1
  55. package/dist/opencode/gspec-audit/SKILL.md +206 -0
  56. package/dist/opencode/gspec-feature/SKILL.md +1 -1
  57. package/dist/opencode/gspec-implement/SKILL.md +11 -8
  58. package/dist/opencode/gspec-migrate/SKILL.md +30 -16
  59. package/dist/opencode/gspec-practices/SKILL.md +1 -1
  60. package/dist/opencode/gspec-profile/SKILL.md +56 -36
  61. package/dist/opencode/gspec-research/SKILL.md +1 -1
  62. package/dist/opencode/gspec-stack/SKILL.md +1 -1
  63. package/dist/opencode/gspec-style/SKILL.md +65 -13
  64. package/package.json +1 -1
  65. package/templates/spec-sync.md +24 -2
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`, and `/gspec-analyze` — 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-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,8 +42,9 @@ flowchart LR
42
42
  Architect["4. Architect
43
43
  technical blueprint"]
44
44
 
45
- Analyze["5. Analyze
46
- reconcile specs"]
45
+ Analyze["5. Analyze & Audit
46
+ reconcile specs
47
+ check specs vs code"]
47
48
 
48
49
  Build["6. Build
49
50
  implement"]
@@ -76,7 +77,7 @@ flowchart LR
76
77
  | Command | Role | What it produces |
77
78
  |---|---|---|
78
79
  | `/gspec-profile` | Business Strategist | Product identity, audience, value proposition, positioning |
79
- | `/gspec-style` | UI/UX Designer | Visual design language, design tokens, component patterns |
80
+ | `/gspec-style` | UI/UX Designer | Visual design language, design tokens, component patterns. Produces either a renderable `style.html` design system or a `style.md` Markdown guide |
80
81
  | `/gspec-stack` | Software Architect | Technology stack, frameworks, infrastructure, architecture |
81
82
  | `/gspec-practices` | Engineering Lead | Development standards, code quality, testing, workflows |
82
83
 
@@ -104,13 +105,16 @@ Use `/gspec-feature` when you want detailed PRDs with prioritized capabilities a
104
105
 
105
106
  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.
106
107
 
107
- **5. Analyze** *(optional)* — Reconcile discrepancies across specs before building.
108
+ **5. Analyze & Audit** *(optional)* — Reconcile discrepancies before building, and keep specs honest as the codebase evolves.
108
109
 
109
110
  | Command | Role | What it does |
110
111
  |---|---|---|
111
- | `/gspec-analyze` | Specification Analyst | Cross-references all specs, identifies contradictions, and walks you through reconciling each one |
112
+ | `/gspec-analyze` | Specification Analyst | Cross-references all specs against **each other**, identifies contradictions, and walks you through reconciling each one |
113
+ | `/gspec-audit` | Specification Auditor | Cross-references specs against the **actual codebase**, finds drift (stack mismatches, stale data models, design tokens that don't match the stylesheet, capability checkboxes that lie), and walks you through updating specs to match reality |
112
114
 
113
- Use `/gspec-analyze` after `/gspec-architect` (or any time multiple specs exist) to catch conflicts before `/gspec-implement` sees them. For example, if the stack says PostgreSQL but the architecture references MongoDB, or a feature PRD defines a data model that contradicts the architecture, `/gspec-analyze` will surface the discrepancy and let you choose the resolution. Each conflict is presented one at a time with options — no new files are created, only existing specs are updated.
115
+ Use `/gspec-analyze` after `/gspec-architect` (or any time multiple specs exist) to catch spec-to-spec conflicts before `/gspec-implement` sees them. For example, if the stack says PostgreSQL but the architecture references MongoDB.
116
+
117
+ 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.
114
118
 
115
119
  **6. Build** — Implement with full context.
116
120
 
@@ -120,6 +124,8 @@ Use `/gspec-analyze` after `/gspec-architect` (or any time multiple specs exist)
120
124
 
121
125
  **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.
122
126
 
127
+ **Design-tool integration** — The style guide supports both Markdown (`style.md`) and a renderable HTML design system (`style.html`) that design-aware AI tools can open, render, and reason about directly. Drop mockups from external design tools (Figma, v0, Framer AI, etc.) into `gspec/design/` and `/gspec-implement` will use them as authoritative visual guidance when building UI.
128
+
123
129
  **Maintenance** — Keep specs up to date with the latest gspec format.
124
130
 
125
131
  | Command | Role | What it does |
@@ -184,18 +190,23 @@ All specifications live in a `gspec/` directory at your project root:
184
190
  project-root/
185
191
  └── gspec/
186
192
  ├── profile.md # Product identity and positioning
187
- ├── style.md # Visual design language
193
+ ├── style.html # Visual design language (HTML — renderable design system)
194
+ │ # or style.md if you prefer a Markdown style guide
188
195
  ├── stack.md # Technology stack and architecture
189
196
  ├── practices.md # Development standards
190
197
  ├── architecture.md # Technical architecture blueprint
191
198
  ├── research.md # Competitive analysis and feature gaps
199
+ ├── design/ # Optional — external mockups read during implementation
200
+ │ ├── dashboard.html
201
+ │ ├── checkout-flow.png
202
+ │ └── ...
192
203
  └── features/
193
204
  ├── user-authentication.md
194
205
  ├── dashboard-analytics.md
195
206
  └── ...
196
207
  ```
197
208
 
198
- These are standard Markdown files. They live in your repo, are version-controlled with your code, and are readable by both humans and AI tools.
209
+ Most specs are Markdown. The style guide can also be a self-contained HTML file (`style.html`) that renders the design system as live swatches, typography specimens, and styled component previews — ideal for design-aware AI tools. The optional `gspec/design/` folder holds mockups (HTML, SVG, PNG, JPG) exported from external design tools like Figma, v0, or Framer AI; `/gspec-implement` reads them to reason about layout and visual intent. All files live in your repo, are version-controlled with your code, and are readable by both humans and AI tools.
199
210
 
200
211
  ## Key Design Decisions
201
212
 
package/bin/gspec.js CHANGED
@@ -118,6 +118,17 @@ function promptConfirmNo(message) {
118
118
  });
119
119
  }
120
120
 
121
+ function promptConfirmYes(message) {
122
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
123
+ return new Promise((resolve) => {
124
+ rl.question(message, (answer) => {
125
+ rl.close();
126
+ const trimmed = answer.trim().toLowerCase();
127
+ resolve(trimmed === '' || trimmed.startsWith('y'));
128
+ });
129
+ });
130
+ }
131
+
121
132
  function formatStarterName(slug) {
122
133
  if (slug === '_none') return 'None';
123
134
  return slug
@@ -244,11 +255,13 @@ async function seedFromSavedSpecs(cwd) {
244
255
  const gspecDir = join(cwd, 'gspec');
245
256
  const filesToWrite = [];
246
257
 
258
+ // `dest` is null for types whose destination filename depends on the saved file's extension
259
+ // (currently `styles`, which may be .md or .html).
247
260
  const CATEGORY_ORDER = [
248
261
  { type: 'profiles', label: 'Select a profile', dest: 'profile.md', mode: 'single' },
249
262
  { type: 'practices', label: 'Select practices', dest: 'practices.md', mode: 'single' },
250
263
  { type: 'stacks', label: 'Select a stack', dest: 'stack.md', mode: 'single' },
251
- { type: 'styles', label: 'Select a style', dest: 'style.md', mode: 'single' },
264
+ { type: 'styles', label: 'Select a style', dest: null, mode: 'single' },
252
265
  { type: 'features', label: 'Select features (optional)', dest: null, mode: 'multi' },
253
266
  ];
254
267
 
@@ -264,19 +277,24 @@ async function seedFromSavedSpecs(cwd) {
264
277
  : await promptSelect(cat.label, [...specs, NONE_OPTION]);
265
278
 
266
279
  if (selected !== '_none') {
280
+ const savedFilename = await resolveSavedSpecFilename(cat.type, selected);
281
+ if (!savedFilename) continue;
282
+ const destFilename = cat.dest || destFilenameForRestoredSpec(cat.type, savedFilename);
267
283
  filesToWrite.push({
268
- src: join(gspecHome, cat.type, `${selected}.md`),
269
- dest: join(gspecDir, cat.dest),
270
- label: `gspec/${cat.dest}`,
284
+ src: join(gspecHome, cat.type, savedFilename),
285
+ dest: join(gspecDir, destFilename),
286
+ label: `gspec/${destFilename}`,
271
287
  });
272
288
  }
273
289
  } else {
274
290
  let selectedSlugs = await promptMultiSelect(cat.label, specs);
275
291
  for (const slug of selectedSlugs) {
292
+ const savedFilename = await resolveSavedSpecFilename(cat.type, slug);
293
+ if (!savedFilename) continue;
276
294
  filesToWrite.push({
277
- src: join(gspecHome, cat.type, `${slug}.md`),
278
- dest: join(gspecDir, 'features', `${slug}.md`),
279
- label: `gspec/features/${slug}.md`,
295
+ src: join(gspecHome, cat.type, savedFilename),
296
+ dest: join(gspecDir, 'features', savedFilename),
297
+ label: `gspec/features/${savedFilename}`,
280
298
  });
281
299
  }
282
300
  }
@@ -550,6 +568,11 @@ const MIGRATE_COMMANDS = {
550
568
  };
551
569
 
552
570
  function parseSpecVersion(content) {
571
+ // HTML spec files store the version as a first-line comment:
572
+ // <!-- spec-version: v1 -->
573
+ const htmlMatch = content.match(/^\s*<!--\s*spec-version:\s*([^\s-][^-]*?)\s*-->/);
574
+ if (htmlMatch) return htmlMatch[1].trim();
575
+
553
576
  const match = content.match(/^---\n([\s\S]*?)\n---/);
554
577
  if (!match) return null;
555
578
  const newMatch = match[1].match(/^spec-version:\s*(.+)$/m);
@@ -567,6 +590,11 @@ async function collectGspecFiles(gspecDir) {
567
590
  if (entry.endsWith('.md') && entry.toLowerCase() !== 'readme.md') {
568
591
  files.push({ path: join(gspecDir, entry), label: `gspec/${entry}` });
569
592
  }
593
+ // Pick up style.html (the HTML-format style guide) alongside Markdown specs.
594
+ // Other .html files under gspec/ are not gspec-owned and are skipped.
595
+ if (entry === 'style.html') {
596
+ files.push({ path: join(gspecDir, entry), label: `gspec/${entry}` });
597
+ }
570
598
  }
571
599
 
572
600
  for (const subdir of ['features', 'epics']) {
@@ -637,19 +665,82 @@ const GSPEC_TYPE_MAP = {
637
665
  'profile.md': 'profiles',
638
666
  'stack.md': 'stacks',
639
667
  'style.md': 'styles',
668
+ 'style.html': 'styles',
640
669
  'practices.md': 'practices',
641
670
  };
642
671
 
643
672
  // Reverse: restore type folder → gspec/ destination filename
673
+ // The `styles` entry is a function because the destination depends on the saved file's extension.
644
674
  const RESTORE_DEST_MAP = {
645
675
  profiles: 'profile.md',
646
676
  stacks: 'stack.md',
647
- styles: 'style.md',
677
+ styles: 'style.md', // default when the saved extension is .md
648
678
  practices: 'practices.md',
649
679
  features: null, // features keep their own filename
650
680
  };
651
681
 
682
+ // Given a save-type folder and a saved slug, resolve the actual filename in ~/.gspec/<type>/.
683
+ // Styles can be stored as .md or .html; all others are .md.
684
+ async function resolveSavedSpecFilename(type, slug) {
685
+ const dir = join(GSPEC_HOME, type);
686
+ const candidates = type === 'styles'
687
+ ? [`${slug}.md`, `${slug}.html`]
688
+ : [`${slug}.md`];
689
+ for (const candidate of candidates) {
690
+ try {
691
+ await stat(join(dir, candidate));
692
+ return candidate;
693
+ } catch (e) {
694
+ if (e.code !== 'ENOENT') throw e;
695
+ }
696
+ }
697
+ return null;
698
+ }
699
+
700
+ // Destination filename in a project's gspec/ directory for a restored saved spec.
701
+ // For styles, preserve the saved file's extension so a .html style guide restores as style.html.
702
+ function destFilenameForRestoredSpec(type, savedFilename) {
703
+ if (type === 'features') return savedFilename;
704
+ if (type === 'styles') {
705
+ return savedFilename.endsWith('.html') ? 'style.html' : 'style.md';
706
+ }
707
+ return RESTORE_DEST_MAP[type];
708
+ }
709
+
710
+ function isHtmlSpec(content) {
711
+ const head = content.slice(0, 500).trimStart().toLowerCase();
712
+ if (head.startsWith('<!doctype') || head.startsWith('<html')) return true;
713
+ // Leading HTML comments before <!DOCTYPE> (where we store HTML spec metadata)
714
+ if (head.startsWith('<!--')) {
715
+ // Peek further to see if a <!DOCTYPE> / <html> follows the comments
716
+ const scan = content.slice(0, 2000).toLowerCase();
717
+ return /<!doctype|<html/.test(scan);
718
+ }
719
+ return false;
720
+ }
721
+
722
+ function parseHtmlMetadata(content) {
723
+ // Consume consecutive `<!-- key: value -->` comments at the top of the file,
724
+ // stopping at the first non-comment, non-blank line.
725
+ const fields = {};
726
+ const lines = content.split('\n');
727
+ let bodyStart = 0;
728
+ for (let i = 0; i < lines.length; i++) {
729
+ const trimmed = lines[i].trim();
730
+ if (trimmed === '') {
731
+ bodyStart = i + 1;
732
+ continue;
733
+ }
734
+ const match = trimmed.match(/^<!--\s*([\w-]+):\s*(.+?)\s*-->$/);
735
+ if (!match) break;
736
+ fields[match[1]] = match[2];
737
+ bodyStart = i + 1;
738
+ }
739
+ return { fields, body: lines.slice(bodyStart).join('\n') };
740
+ }
741
+
652
742
  function parseFrontmatter(content) {
743
+ if (isHtmlSpec(content)) return parseHtmlMetadata(content);
653
744
  const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
654
745
  if (!match) return { fields: {}, body: content };
655
746
  const fields = {};
@@ -662,7 +753,34 @@ function parseFrontmatter(content) {
662
753
  return { fields, body: content.slice(match[0].length) };
663
754
  }
664
755
 
756
+ function setHtmlMetadataField(content, key, value) {
757
+ const lines = content.split('\n');
758
+ let lastCommentIndex = -1;
759
+ for (let i = 0; i < lines.length; i++) {
760
+ const trimmed = lines[i].trim();
761
+ if (trimmed === '') continue;
762
+ if (trimmed.startsWith('<!--') && trimmed.endsWith('-->')) {
763
+ lastCommentIndex = i;
764
+ const m = trimmed.match(/^<!--\s*([\w-]+):\s*(.+?)\s*-->$/);
765
+ if (m && m[1] === key) {
766
+ lines[i] = `<!-- ${key}: ${value} -->`;
767
+ return lines.join('\n');
768
+ }
769
+ continue;
770
+ }
771
+ break;
772
+ }
773
+ const newComment = `<!-- ${key}: ${value} -->`;
774
+ if (lastCommentIndex >= 0) {
775
+ lines.splice(lastCommentIndex + 1, 0, newComment);
776
+ } else {
777
+ lines.unshift(newComment);
778
+ }
779
+ return lines.join('\n');
780
+ }
781
+
665
782
  function setFrontmatterField(content, key, value) {
783
+ if (isHtmlSpec(content)) return setHtmlMetadataField(content, key, value);
666
784
  const match = content.match(/^(---\s*\n)([\s\S]*?)(\n---)/);
667
785
  if (!match) {
668
786
  // No frontmatter — create one
@@ -700,10 +818,11 @@ async function collectSavableFiles(cwd) {
700
818
  throw e;
701
819
  }
702
820
 
703
- // Top-level spec files
821
+ // Top-level spec files. Accept the Markdown specs plus the HTML style guide.
704
822
  const topEntries = await readdir(gspecDir);
705
823
  for (const entry of topEntries) {
706
- if (!entry.endsWith('.md') || entry.toLowerCase() === 'readme.md') continue;
824
+ if (entry.toLowerCase() === 'readme.md') continue;
825
+ if (!entry.endsWith('.md') && entry !== 'style.html') continue;
707
826
  const type = GSPEC_TYPE_MAP[entry];
708
827
  if (!type) continue;
709
828
  files.push({
@@ -758,20 +877,53 @@ async function saveSpec(cwd) {
758
877
  }
759
878
 
760
879
  const selected = files[num - 1];
880
+ // Preserve the source file's extension when saving (.md for most specs, .html for style.html).
881
+ const ext = selected.path.endsWith('.html') ? '.html' : '.md';
761
882
 
762
- // Prompt for name
763
- const name = await promptInput(chalk.bold('\n Save name (no spaces, e.g. my-saas-stack): '));
764
- if (!name) {
765
- console.error(chalk.red('\n Name is required.'));
766
- process.exit(1);
883
+ // Read source content and look for an existing name in frontmatter
884
+ let content = await readFile(selected.path, 'utf-8');
885
+ const { fields: sourceFields } = parseFrontmatter(content);
886
+ const existingName = sourceFields.name;
887
+
888
+ let name;
889
+ let overwriteConfirmed = false;
890
+
891
+ if (existingName) {
892
+ const existingPath = join(GSPEC_HOME, selected.type, `${existingName}${ext}`);
893
+ let savedExists = false;
894
+ try {
895
+ await stat(existingPath);
896
+ savedExists = true;
897
+ } catch (e) {
898
+ if (e.code !== 'ENOENT') throw e;
899
+ }
900
+
901
+ if (savedExists) {
902
+ const overwrite = await promptConfirmYes(
903
+ chalk.bold(`\n Overwrite existing ~/.gspec/${selected.type}/${existingName}${ext}? [Y/n]: `)
904
+ );
905
+ if (overwrite) {
906
+ name = existingName;
907
+ overwriteConfirmed = true;
908
+ }
909
+ } else {
910
+ name = existingName;
911
+ }
767
912
  }
768
- if (/\s/.test(name)) {
769
- console.error(chalk.red('\n Name cannot contain spaces. Use hyphens instead (e.g. my-saas-stack).'));
770
- process.exit(1);
913
+
914
+ if (!name) {
915
+ const answered = await promptInput(chalk.bold('\n Save name (no spaces, e.g. my-saas-stack): '));
916
+ if (!answered) {
917
+ console.error(chalk.red('\n Name is required.'));
918
+ process.exit(1);
919
+ }
920
+ if (/\s/.test(answered)) {
921
+ console.error(chalk.red('\n Name cannot contain spaces. Use hyphens instead (e.g. my-saas-stack).'));
922
+ process.exit(1);
923
+ }
924
+ name = answered;
771
925
  }
772
926
 
773
- // Read content and update frontmatter with name
774
- let content = await readFile(selected.path, 'utf-8');
775
927
  content = setFrontmatterField(content, 'name', name);
776
928
 
777
929
  // Ensure description exists
@@ -783,28 +935,36 @@ async function saveSpec(cwd) {
783
935
  }
784
936
  }
785
937
 
786
- // Write to ~/.gspec/{type}/{name}.md
938
+ // Write to ~/.gspec/{type}/{name}{ext}
787
939
  const destDir = join(GSPEC_HOME, selected.type);
788
- const destPath = join(destDir, `${name}.md`);
940
+ const destPath = join(destDir, `${name}${ext}`);
789
941
  await mkdir(destDir, { recursive: true });
790
942
 
791
- // Check if file already exists
792
- try {
793
- await stat(destPath);
794
- const overwrite = await promptConfirm(chalk.yellow(`\n ${selected.type}/${name} already exists. Overwrite? [y/N]: `));
795
- if (!overwrite) {
796
- console.log(chalk.dim('\n Save cancelled.\n'));
797
- return;
943
+ // Check for conflict unless overwrite was already confirmed above
944
+ if (!overwriteConfirmed) {
945
+ try {
946
+ await stat(destPath);
947
+ const overwrite = await promptConfirm(chalk.yellow(`\n ${selected.type}/${name}${ext} already exists. Overwrite? [y/N]: `));
948
+ if (!overwrite) {
949
+ console.log(chalk.dim('\n Save cancelled.\n'));
950
+ return;
951
+ }
952
+ } catch (e) {
953
+ if (e.code !== 'ENOENT') throw e;
798
954
  }
799
- } catch (e) {
800
- if (e.code !== 'ENOENT') throw e;
801
955
  }
802
956
 
803
957
  // Uncheck all implementation checkboxes so saved specs start fresh
804
958
  content = content.replace(/- \[x\]/g, '- [ ]');
805
959
 
806
960
  await writeFile(destPath, content, 'utf-8');
807
- console.log(chalk.green(`\n ✓ Saved to ~/.gspec/${selected.type}/${name}.md\n`));
961
+ console.log(chalk.green(`\n ✓ Saved to ~/.gspec/${selected.type}/${name}${ext}\n`));
962
+ }
963
+
964
+ function isSavedSpecFile(type, filename) {
965
+ if (filename.endsWith('.md')) return true;
966
+ if (type === 'styles' && filename.endsWith('.html')) return true;
967
+ return false;
808
968
  }
809
969
 
810
970
  async function listSavedTypes() {
@@ -816,8 +976,7 @@ async function listSavedTypes() {
816
976
  const info = await stat(join(GSPEC_HOME, entry));
817
977
  if (info.isDirectory()) {
818
978
  const files = await readdir(join(GSPEC_HOME, entry));
819
- const mdFiles = files.filter((f) => f.endsWith('.md'));
820
- if (mdFiles.length > 0) types.push(entry);
979
+ if (files.some((f) => isSavedSpecFile(entry, f))) types.push(entry);
821
980
  }
822
981
  } catch { /* skip */ }
823
982
  }
@@ -833,11 +992,11 @@ async function listSavedSpecs(type) {
833
992
  const entries = await readdir(dir);
834
993
  const specs = [];
835
994
  for (const entry of entries) {
836
- if (!entry.endsWith('.md')) continue;
995
+ if (!isSavedSpecFile(type, entry)) continue;
837
996
  const content = await readFile(join(dir, entry), 'utf-8');
838
997
  const { fields } = parseFrontmatter(content);
839
998
  specs.push({
840
- slug: entry.replace(/\.md$/, ''),
999
+ slug: entry.replace(/\.(md|html)$/, ''),
841
1000
  description: fields.description || '',
842
1001
  });
843
1002
  }
@@ -910,25 +1069,20 @@ async function restoreSpec(specPath, cwd) {
910
1069
  }
911
1070
 
912
1071
  async function restoreFile(type, name, cwd) {
913
- const srcPath = join(GSPEC_HOME, type, `${name}.md`);
914
-
915
- try {
916
- await stat(srcPath);
917
- } catch (e) {
918
- if (e.code === 'ENOENT') {
919
- console.error(chalk.red(`\n Not found: ~/.gspec/${type}/${name}.md\n`));
920
- process.exit(1);
921
- }
922
- throw e;
1072
+ const savedFilename = await resolveSavedSpecFilename(type, name);
1073
+ if (!savedFilename) {
1074
+ console.error(chalk.red(`\n Not found: ~/.gspec/${type}/${name}.md\n`));
1075
+ process.exit(1);
923
1076
  }
1077
+ const srcPath = join(GSPEC_HOME, type, savedFilename);
924
1078
 
925
1079
  const gspecDir = join(cwd, 'gspec');
926
1080
  let destPath;
927
1081
 
928
1082
  if (type === 'features') {
929
- destPath = join(gspecDir, 'features', `${name}.md`);
1083
+ destPath = join(gspecDir, 'features', savedFilename);
930
1084
  } else {
931
- const destFile = RESTORE_DEST_MAP[type];
1085
+ const destFile = destFilenameForRestoredSpec(type, savedFilename);
932
1086
  if (!destFile) {
933
1087
  console.error(chalk.red(`\n Unknown spec type: ${type}\n`));
934
1088
  process.exit(1);
@@ -1157,10 +1311,18 @@ async function restorePlaybook(name, cwd) {
1157
1311
  restorations.push({ type: 'features', slug: f });
1158
1312
  }
1159
1313
 
1314
+ // Resolve each restoration to its actual saved filename (styles may be .md or .html)
1315
+ for (const r of restorations) {
1316
+ r.savedFilename = await resolveSavedSpecFilename(r.type, r.slug);
1317
+ }
1318
+
1160
1319
  // Check for existing files
1161
1320
  const existing = [];
1162
1321
  for (const r of restorations) {
1163
- const destFile = r.type === 'features' ? join('features', `${r.slug}.md`) : RESTORE_DEST_MAP[r.type];
1322
+ if (!r.savedFilename) continue;
1323
+ const destFile = r.type === 'features'
1324
+ ? join('features', r.savedFilename)
1325
+ : destFilenameForRestoredSpec(r.type, r.savedFilename);
1164
1326
  const destPath = join(gspecDir, destFile);
1165
1327
  try {
1166
1328
  await stat(destPath);
@@ -1186,18 +1348,15 @@ async function restorePlaybook(name, cwd) {
1186
1348
  // Restore all specs
1187
1349
  const outdated = [];
1188
1350
  for (const r of restorations) {
1189
- const srcFile = join(GSPEC_HOME, r.type, `${r.slug}.md`);
1190
- try {
1191
- await stat(srcFile);
1192
- } catch (e) {
1193
- if (e.code === 'ENOENT') {
1194
- console.log(` ${chalk.yellow('!')} Skipped ${r.type}/${r.slug} — not found in ~/.gspec/`);
1195
- continue;
1196
- }
1197
- throw e;
1351
+ if (!r.savedFilename) {
1352
+ console.log(` ${chalk.yellow('!')} Skipped ${r.type}/${r.slug} — not found in ~/.gspec/`);
1353
+ continue;
1198
1354
  }
1355
+ const srcFile = join(GSPEC_HOME, r.type, r.savedFilename);
1199
1356
 
1200
- const destFile = r.type === 'features' ? join('features', `${r.slug}.md`) : RESTORE_DEST_MAP[r.type];
1357
+ const destFile = r.type === 'features'
1358
+ ? join('features', r.savedFilename)
1359
+ : destFilenameForRestoredSpec(r.type, r.savedFilename);
1201
1360
  const destPath = join(gspecDir, destFile);
1202
1361
  await mkdir(dirname(destPath), { recursive: true });
1203
1362
  const specContent = await readFile(srcFile, 'utf-8');
@@ -4,6 +4,8 @@ Your task is to read all existing gspec specification documents, identify discre
4
4
 
5
5
  This command is designed to be run **after** `gspec-architect` (or at any point when multiple specs exist) and **before** `gspec-implement`, to ensure the implementing agent receives a coherent, conflict-free set of instructions.
6
6
 
7
+ > **Analyze vs. audit.** `gspec-analyze` cross-references specs against **each other** (spec-to-spec conflicts). `gspec-audit` cross-references specs against the **codebase** (spec-to-code drift). If the user's intent is "do my docs still reflect what the code does?", route to `gspec-audit` instead.
8
+
7
9
  You should:
8
10
  - Read and deeply cross-reference all available gspec documents
9
11
  - Identify concrete discrepancies — not style differences or minor wording variations, but substantive contradictions where two specs disagree on a fact, technology, behavior, or requirement
@@ -23,11 +25,12 @@ Read **every** available gspec document in this order:
23
25
 
24
26
  1. `gspec/profile.md` — Product identity, scope, audience, and positioning
25
27
  2. `gspec/stack.md` — Technology choices, frameworks, infrastructure
26
- 3. `gspec/style.md` — Visual design language, tokens, component styling
27
- 4. `gspec/practices.md`Development standards, testing, conventions
28
- 5. `gspec/architecture.md` — Technical blueprint: project structure, data model, API design, environment
29
- 6. `gspec/research.md` — Competitive analysis and feature proposals
30
- 7. `gspec/features/*.md` — Individual feature requirements and dependencies
28
+ 3. `gspec/style.md` **or** `gspec/style.html` — Visual design language, tokens, component styling. Read whichever exists; read both if both are present. For an HTML style guide, the canonical token values are the CSS custom properties defined in the `<style>` block — inspect those when cross-referencing token-related claims
29
+ 4. `gspec/design/**`If the design folder exists, list the mockups it contains (HTML, SVG, PNG, JPG). You do not need to deeply parse images, but note which screens or flows have mockups so you can flag features that reference a screen lacking a mockup, or mockups that depict behavior contradicted by a feature PRD
30
+ 5. `gspec/practices.md` — Development standards, testing, conventions
31
+ 6. `gspec/architecture.md` — Technical blueprint: project structure, data model, API design, environment
32
+ 7. `gspec/research.md` — Competitive analysis and feature proposals
33
+ 8. `gspec/features/*.md` — Individual feature requirements and dependencies
31
34
 
32
35
  If fewer than two spec files exist, inform the user that there is nothing to cross-reference and stop.
33
36
 
@@ -53,8 +56,10 @@ Systematically compare specs against each other. Look for these categories of di
53
56
  - Authentication or authorization requirements differ between specs
54
57
 
55
58
  #### Design & Style Conflicts
56
- - A feature PRD references visual patterns or components that contradict `style.md`
57
- - Architecture's component structure doesn't align with the design system in `style.md`
59
+ - A feature PRD references visual patterns or components that contradict the style guide (`style.md` or `style.html`)
60
+ - Architecture's component structure doesn't align with the design system in the style guide
61
+ - A mockup in `gspec/design/` depicts a layout, color, or component treatment that contradicts the style guide's tokens or patterns
62
+ - A feature PRD describes a screen that has a mockup in `gspec/design/`, but the PRD and mockup disagree on behavior or composition
58
63
 
59
64
  #### Practice & Convention Conflicts
60
65
  - Architecture's file naming, testing approach, or code organization contradicts `practices.md`
@@ -123,7 +128,7 @@ When updating specs to resolve a discrepancy:
123
128
 
124
129
  - **Surgical updates only** — change the minimum text needed to resolve the conflict
125
130
  - **Preserve format and tone** — match the existing document's style, heading structure, and voice
126
- - **Preserve `spec-version` frontmatter** — do not alter or remove it
131
+ - **Preserve `spec-version` metadata** — do not alter or remove it. For Markdown files this is YAML frontmatter (`---\nspec-version: ...\n---`); for HTML style guides it is the first-line comment (`<!-- spec-version: ... -->`). Both must be left intact.
127
132
  - **Do not rewrite sections** — if a one-line change resolves the conflict, make a one-line change
128
133
  - **Do not add changelog annotations** — the git history captures what changed
129
134