gsd-pi 2.26.0 → 2.27.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 (171) hide show
  1. package/README.md +43 -6
  2. package/dist/cli.js +4 -2
  3. package/dist/headless.d.ts +3 -0
  4. package/dist/headless.js +136 -8
  5. package/dist/help-text.js +3 -0
  6. package/dist/loader.js +33 -4
  7. package/dist/resources/extensions/bg-shell/index.ts +19 -2
  8. package/dist/resources/extensions/bg-shell/process-manager.ts +45 -0
  9. package/dist/resources/extensions/bg-shell/types.ts +21 -1
  10. package/dist/resources/extensions/gsd/auto/session.ts +224 -0
  11. package/dist/resources/extensions/gsd/auto-budget.ts +32 -0
  12. package/dist/resources/extensions/gsd/auto-dashboard.ts +63 -10
  13. package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
  14. package/dist/resources/extensions/gsd/auto-dispatch.ts +23 -10
  15. package/dist/resources/extensions/gsd/auto-model-selection.ts +179 -0
  16. package/dist/resources/extensions/gsd/auto-observability.ts +74 -0
  17. package/dist/resources/extensions/gsd/auto-prompts.ts +0 -1
  18. package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
  19. package/dist/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
  20. package/dist/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
  21. package/dist/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
  22. package/dist/resources/extensions/gsd/auto.ts +977 -1551
  23. package/dist/resources/extensions/gsd/commands.ts +3 -3
  24. package/dist/resources/extensions/gsd/dashboard-overlay.ts +47 -72
  25. package/dist/resources/extensions/gsd/doctor-proactive.ts +9 -4
  26. package/dist/resources/extensions/gsd/export-html.ts +1001 -0
  27. package/dist/resources/extensions/gsd/export.ts +49 -1
  28. package/dist/resources/extensions/gsd/git-service.ts +6 -0
  29. package/dist/resources/extensions/gsd/gitignore.ts +4 -1
  30. package/dist/resources/extensions/gsd/guided-flow.ts +24 -5
  31. package/dist/resources/extensions/gsd/index.ts +54 -1
  32. package/dist/resources/extensions/gsd/native-git-bridge.ts +30 -2
  33. package/dist/resources/extensions/gsd/observability-validator.ts +21 -0
  34. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
  35. package/dist/resources/extensions/gsd/preferences.ts +62 -1
  36. package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -3
  37. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  38. package/dist/resources/extensions/gsd/reports.ts +510 -0
  39. package/dist/resources/extensions/gsd/roadmap-slices.ts +1 -1
  40. package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
  41. package/dist/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
  42. package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
  43. package/dist/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
  44. package/dist/resources/extensions/gsd/state.ts +30 -0
  45. package/dist/resources/extensions/gsd/templates/task-summary.md +9 -0
  46. package/dist/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
  47. package/dist/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
  48. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
  49. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
  50. package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
  51. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
  52. package/dist/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
  53. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
  54. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
  55. package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
  56. package/dist/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
  57. package/dist/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
  58. package/dist/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
  59. package/dist/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
  60. package/dist/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
  61. package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
  62. package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
  63. package/dist/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
  64. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
  65. package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
  66. package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
  67. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
  68. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
  69. package/dist/resources/extensions/gsd/tests/worktree.test.ts +3 -1
  70. package/dist/resources/extensions/gsd/types.ts +38 -0
  71. package/dist/resources/extensions/gsd/verification-evidence.ts +183 -0
  72. package/dist/resources/extensions/gsd/verification-gate.ts +567 -0
  73. package/dist/resources/extensions/gsd/visualizer-data.ts +25 -3
  74. package/dist/resources/extensions/gsd/visualizer-overlay.ts +31 -21
  75. package/dist/resources/extensions/gsd/visualizer-views.ts +15 -66
  76. package/dist/resources/extensions/search-the-web/tool-search.ts +26 -0
  77. package/dist/resources/extensions/shared/format-utils.ts +85 -0
  78. package/dist/resources/extensions/shared/tests/format-utils.test.ts +153 -0
  79. package/dist/resources/extensions/subagent/index.ts +46 -1
  80. package/dist/resources/extensions/subagent/isolation.ts +9 -6
  81. package/package.json +1 -1
  82. package/packages/pi-ai/dist/providers/openai-completions.js +7 -4
  83. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  84. package/packages/pi-ai/src/providers/openai-completions.ts +7 -4
  85. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/core/lsp/client.js +7 -0
  87. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  88. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/lsp/config.js +9 -2
  90. package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
  91. package/packages/pi-coding-agent/src/core/lsp/client.ts +8 -0
  92. package/packages/pi-coding-agent/src/core/lsp/config.ts +9 -2
  93. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  94. package/packages/pi-tui/dist/components/editor.js +1 -1
  95. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  96. package/packages/pi-tui/src/components/editor.ts +3 -1
  97. package/scripts/link-workspace-packages.cjs +22 -6
  98. package/src/resources/extensions/bg-shell/index.ts +19 -2
  99. package/src/resources/extensions/bg-shell/process-manager.ts +45 -0
  100. package/src/resources/extensions/bg-shell/types.ts +21 -1
  101. package/src/resources/extensions/gsd/auto/session.ts +224 -0
  102. package/src/resources/extensions/gsd/auto-budget.ts +32 -0
  103. package/src/resources/extensions/gsd/auto-dashboard.ts +63 -10
  104. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
  105. package/src/resources/extensions/gsd/auto-dispatch.ts +23 -10
  106. package/src/resources/extensions/gsd/auto-model-selection.ts +179 -0
  107. package/src/resources/extensions/gsd/auto-observability.ts +74 -0
  108. package/src/resources/extensions/gsd/auto-prompts.ts +0 -1
  109. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
  110. package/src/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
  111. package/src/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
  112. package/src/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
  113. package/src/resources/extensions/gsd/auto.ts +977 -1551
  114. package/src/resources/extensions/gsd/commands.ts +3 -3
  115. package/src/resources/extensions/gsd/dashboard-overlay.ts +47 -72
  116. package/src/resources/extensions/gsd/doctor-proactive.ts +9 -4
  117. package/src/resources/extensions/gsd/export-html.ts +1001 -0
  118. package/src/resources/extensions/gsd/export.ts +49 -1
  119. package/src/resources/extensions/gsd/git-service.ts +6 -0
  120. package/src/resources/extensions/gsd/gitignore.ts +4 -1
  121. package/src/resources/extensions/gsd/guided-flow.ts +24 -5
  122. package/src/resources/extensions/gsd/index.ts +54 -1
  123. package/src/resources/extensions/gsd/native-git-bridge.ts +30 -2
  124. package/src/resources/extensions/gsd/observability-validator.ts +21 -0
  125. package/src/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
  126. package/src/resources/extensions/gsd/preferences.ts +62 -1
  127. package/src/resources/extensions/gsd/prompts/execute-task.md +4 -3
  128. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  129. package/src/resources/extensions/gsd/reports.ts +510 -0
  130. package/src/resources/extensions/gsd/roadmap-slices.ts +1 -1
  131. package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
  132. package/src/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
  133. package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
  134. package/src/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
  135. package/src/resources/extensions/gsd/state.ts +30 -0
  136. package/src/resources/extensions/gsd/templates/task-summary.md +9 -0
  137. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
  138. package/src/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
  139. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
  140. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
  141. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
  142. package/src/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
  143. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
  144. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
  145. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
  146. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
  147. package/src/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
  148. package/src/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
  149. package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
  150. package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
  151. package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
  152. package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
  153. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
  154. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
  155. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
  156. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
  157. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
  158. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
  159. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
  160. package/src/resources/extensions/gsd/tests/worktree.test.ts +3 -1
  161. package/src/resources/extensions/gsd/types.ts +38 -0
  162. package/src/resources/extensions/gsd/verification-evidence.ts +183 -0
  163. package/src/resources/extensions/gsd/verification-gate.ts +567 -0
  164. package/src/resources/extensions/gsd/visualizer-data.ts +25 -3
  165. package/src/resources/extensions/gsd/visualizer-overlay.ts +31 -21
  166. package/src/resources/extensions/gsd/visualizer-views.ts +15 -66
  167. package/src/resources/extensions/search-the-web/tool-search.ts +26 -0
  168. package/src/resources/extensions/shared/format-utils.ts +85 -0
  169. package/src/resources/extensions/shared/tests/format-utils.test.ts +153 -0
  170. package/src/resources/extensions/subagent/index.ts +46 -1
  171. package/src/resources/extensions/subagent/isolation.ts +9 -6
@@ -75,7 +75,11 @@ const KNOWN_PREFERENCE_KEYS = new Set<string>([
75
75
  "token_profile",
76
76
  "phases",
77
77
  "auto_visualize",
78
+ "auto_report",
78
79
  "parallel",
80
+ "verification_commands",
81
+ "verification_auto_fix",
82
+ "verification_max_retries",
79
83
  ]);
80
84
 
81
85
  export interface GSDSkillRule {
@@ -172,7 +176,12 @@ export interface GSDPreferences {
172
176
  token_profile?: TokenProfile;
173
177
  phases?: PhaseSkipPreferences;
174
178
  auto_visualize?: boolean;
179
+ /** Generate HTML report snapshot after each milestone completion. Default: true. Set false to disable. */
180
+ auto_report?: boolean;
175
181
  parallel?: import("./types.js").ParallelConfig;
182
+ verification_commands?: string[];
183
+ verification_auto_fix?: boolean;
184
+ verification_max_retries?: number;
176
185
  }
177
186
 
178
187
  export interface LoadedGSDPreferences {
@@ -327,7 +336,7 @@ function resolveSkillReference(ref: string, cwd: string): SkillResolution {
327
336
  try {
328
337
  const entries = readdirSync(dir, { withFileTypes: true });
329
338
  for (const entry of entries) {
330
- if (!entry.isDirectory()) continue;
339
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
331
340
  if (entry.name === expanded) {
332
341
  const skillFile = join(dir, entry.name, "SKILL.md");
333
342
  if (existsSync(skillFile)) {
@@ -586,6 +595,18 @@ export function getNextFallbackModel(
586
595
  }
587
596
  }
588
597
 
598
+ /**
599
+ * Detect whether an error message indicates a transient network error
600
+ * (worth retrying the same model) vs a permanent provider error
601
+ * (auth failure, quota exceeded, etc. — should fall back immediately).
602
+ */
603
+ export function isTransientNetworkError(errorMsg: string): boolean {
604
+ if (!errorMsg) return false;
605
+ const hasNetworkSignal = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|fetch failed|connection.*reset|dns/i.test(errorMsg);
606
+ const hasPermanentSignal = /auth|unauthorized|forbidden|invalid.*key|quota|billing/i.test(errorMsg);
607
+ return hasNetworkSignal && !hasPermanentSignal;
608
+ }
609
+
589
610
  export function resolveModelWithFallbacksForUnit(unitType: string): ResolvedModelConfig | undefined {
590
611
  const prefs = loadEffectiveGSDPreferences();
591
612
  if (!prefs?.preferences.models) return undefined;
@@ -773,6 +794,9 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
773
794
  parallel: (base.parallel || override.parallel)
774
795
  ? { ...(base.parallel ?? {}), ...(override.parallel ?? {}) } as import("./types.js").ParallelConfig
775
796
  : undefined,
797
+ verification_commands: mergeStringLists(base.verification_commands, override.verification_commands),
798
+ verification_auto_fix: override.verification_auto_fix ?? base.verification_auto_fix,
799
+ verification_max_retries: override.verification_max_retries ?? base.verification_max_retries,
776
800
  };
777
801
  }
778
802
 
@@ -1205,6 +1229,39 @@ export function validatePreferences(preferences: GSDPreferences): {
1205
1229
  }
1206
1230
  }
1207
1231
 
1232
+ // ─── Verification Preferences ───────────────────────────────────────────
1233
+ if (preferences.verification_commands !== undefined) {
1234
+ if (Array.isArray(preferences.verification_commands)) {
1235
+ const allStrings = preferences.verification_commands.every(
1236
+ (item: unknown) => typeof item === "string",
1237
+ );
1238
+ if (allStrings) {
1239
+ validated.verification_commands = preferences.verification_commands;
1240
+ } else {
1241
+ errors.push("verification_commands must be an array of strings");
1242
+ }
1243
+ } else {
1244
+ errors.push("verification_commands must be an array of strings");
1245
+ }
1246
+ }
1247
+
1248
+ if (preferences.verification_auto_fix !== undefined) {
1249
+ if (typeof preferences.verification_auto_fix === "boolean") {
1250
+ validated.verification_auto_fix = preferences.verification_auto_fix;
1251
+ } else {
1252
+ errors.push("verification_auto_fix must be a boolean");
1253
+ }
1254
+ }
1255
+
1256
+ if (preferences.verification_max_retries !== undefined) {
1257
+ const raw = preferences.verification_max_retries;
1258
+ if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) {
1259
+ validated.verification_max_retries = Math.floor(raw);
1260
+ } else {
1261
+ errors.push("verification_max_retries must be a non-negative number");
1262
+ }
1263
+ }
1264
+
1208
1265
  // ─── Git Preferences ───────────────────────────────────────────────────
1209
1266
  if (preferences.git && typeof preferences.git === "object") {
1210
1267
  const git: Record<string, unknown> = {};
@@ -1272,6 +1329,10 @@ export function validatePreferences(preferences: GSDPreferences): {
1272
1329
  if (typeof g.commit_docs === "boolean") git.commit_docs = g.commit_docs;
1273
1330
  else errors.push("git.commit_docs must be a boolean");
1274
1331
  }
1332
+ if (g.manage_gitignore !== undefined) {
1333
+ if (typeof g.manage_gitignore === "boolean") git.manage_gitignore = g.manage_gitignore;
1334
+ else errors.push("git.manage_gitignore must be a boolean");
1335
+ }
1275
1336
  if (g.worktree_post_create !== undefined) {
1276
1337
  if (typeof g.worktree_post_create === "string" && g.worktree_post_create.trim()) {
1277
1338
  git.worktree_post_create = g.worktree_post_create.trim();
@@ -38,15 +38,16 @@ Then:
38
38
  - Preferred: use the `bg_shell` tool if available — it manages process lifecycle correctly without stream-inheritance issues
39
39
  6. Verify must-haves are met by running concrete checks (tests, commands, observable behaviors)
40
40
  7. Run the slice-level verification checks defined in the slice plan's Verification section. Track which pass. On the final task of the slice, all must pass before marking done. On intermediate tasks, partial passes are expected — note which ones pass in the summary.
41
- 8. If the task touches UI, browser flows, DOM behavior, or user-visible web state:
41
+ 8. After the verification gate runs (you'll see gate results in stderr/notify output), populate the `## Verification Evidence` table in your task summary with the check results. Use the `formatEvidenceTable` format: one row per check with command, exit code, verdict (✅ pass / ❌ fail), and duration. If no verification commands were discovered, note that in the section.
42
+ 9. If the task touches UI, browser flows, DOM behavior, or user-visible web state:
42
43
  - exercise the real flow in the browser
43
44
  - prefer `browser_batch` when the next few actions are obvious and sequential
44
45
  - prefer `browser_assert` for explicit pass/fail verification of the intended outcome
45
46
  - use `browser_diff` when an action's effect is ambiguous
46
47
  - use console/network/dialog diagnostics when validating async, stateful, or failure-prone UI
47
48
  - record verification in terms of explicit checks passed/failed, not only prose interpretation
48
- 9. If the task plan includes an Observability Impact section, verify those signals directly. Skip this step if the task plan omits the section.
49
- 10. **If execution is running long or verification fails:**
49
+ 10. If the task plan includes an Observability Impact section, verify those signals directly. Skip this step if the task plan omits the section.
50
+ 11. **If execution is running long or verification fails:**
50
51
 
51
52
  **Context budget:** You have approximately **{{verificationBudget}}** reserved for verification context. If you've used most of your context and haven't finished all steps, stop implementing and prioritize writing the task summary with clear notes on what's done and what remains. A partial summary that enables clean resumption is more valuable than one more half-finished step with no documentation. Never sacrifice summary quality for one more implementation step.
52
53
 
@@ -154,7 +154,7 @@ Templates showing the expected format for each artifact type are in:
154
154
 
155
155
  **External facts:** Use `search-the-web` + `fetch_page`, or `search_and_read` for one-call extraction. Use `freshness` for recency. Never state current facts from training data without verification.
156
156
 
157
- **Background processes:** Use `bg_shell` with `start` + `wait_for_ready` for servers, watchers, and daemons. Never use `bash` with `&` or `nohup` to background a process — the `bash` tool waits for stdout to close, so backgrounded children that inherit the file descriptors cause it to hang indefinitely. Never poll with `sleep`/retry loops — `wait_for_ready` exists for this. For status checks, use `digest` (~30 tokens), not `output` (~2000 tokens). Use `highlights` (~100 tokens) when you need significant lines only. Use `output` only when actively debugging.
157
+ **Background processes:** Use `bg_shell` with `start` + `wait_for_ready` for servers, watchers, and daemons. Never use `bash` with `&` or `nohup` to background a process — the `bash` tool waits for stdout to close, so backgrounded children that inherit the file descriptors cause it to hang indefinitely. Never poll with `sleep`/retry loops — `wait_for_ready` exists for this. For status checks, use `digest` (~30 tokens), not `output` (~2000 tokens). Use `highlights` (~100 tokens) when you need significant lines only. Use `output` only when actively debugging. Background processes are session-scoped by default; set `persist_across_sessions:true` only when you intentionally need them to survive a fresh session.
158
158
 
159
159
  **One-shot commands:** Use `async_bash` for builds, tests, and installs. The result is pushed to you when the command exits — no polling needed. Use `await_job` to block on a specific job.
160
160
 
@@ -0,0 +1,510 @@
1
+ /**
2
+ * GSD Reports Registry
3
+ *
4
+ * Manages .gsd/reports/ — the persistent progression log of HTML snapshots.
5
+ *
6
+ * Layout:
7
+ * .gsd/reports/
8
+ * reports.json lightweight metadata index (never re-parses HTML)
9
+ * index.html auto-regenerated on every new snapshot
10
+ * M001-20260101T120000.html per-milestone snapshot
11
+ * final-20260201T090000.html full-project final snapshot
12
+ *
13
+ * Auto-triggered: after each milestone completion (when auto_report: true).
14
+ * Manual: /gsd export --html
15
+ */
16
+
17
+ import { writeFileSync, readFileSync, mkdirSync, existsSync } from 'node:fs';
18
+ import { join, basename } from 'node:path';
19
+ import { gsdRoot } from './paths.js';
20
+ import { formatCost, formatTokenCount } from './metrics.js';
21
+ import { formatDuration } from './history.js';
22
+
23
+ // ─── Types ────────────────────────────────────────────────────────────────────
24
+
25
+ export interface ReportEntry {
26
+ /** Filename relative to the reports/ dir, e.g. "M001-20260101T120000.html" */
27
+ filename: string;
28
+ /** ISO timestamp when this report was generated */
29
+ generatedAt: string;
30
+ /** Milestone ID this snapshot covers, or "final" for a full-project snapshot */
31
+ milestoneId: string | 'final';
32
+ /** Milestone title at snapshot time */
33
+ milestoneTitle: string;
34
+ /** Human-readable label shown in the index */
35
+ label: string;
36
+ /** Snapshot kind */
37
+ kind: 'milestone' | 'manual' | 'final';
38
+ // Metrics at snapshot time — for the index progression view
39
+ totalCost: number;
40
+ totalTokens: number;
41
+ totalDuration: number;
42
+ doneSlices: number;
43
+ totalSlices: number;
44
+ doneMilestones: number;
45
+ totalMilestones: number;
46
+ phase: string;
47
+ }
48
+
49
+ export interface ReportsIndex {
50
+ version: 1;
51
+ projectName: string;
52
+ projectPath: string;
53
+ gsdVersion: string;
54
+ entries: ReportEntry[];
55
+ }
56
+
57
+ // ─── Paths ────────────────────────────────────────────────────────────────────
58
+
59
+ export function reportsDir(basePath: string): string {
60
+ return join(gsdRoot(basePath), 'reports');
61
+ }
62
+
63
+ function reportsIndexPath(basePath: string): string {
64
+ return join(reportsDir(basePath), 'reports.json');
65
+ }
66
+
67
+ function reportsHtmlIndexPath(basePath: string): string {
68
+ return join(reportsDir(basePath), 'index.html');
69
+ }
70
+
71
+ // ─── Registry ─────────────────────────────────────────────────────────────────
72
+
73
+ export function loadReportsIndex(basePath: string): ReportsIndex | null {
74
+ const p = reportsIndexPath(basePath);
75
+ if (!existsSync(p)) return null;
76
+ try {
77
+ return JSON.parse(readFileSync(p, 'utf-8')) as ReportsIndex;
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
82
+
83
+ function saveReportsIndex(basePath: string, index: ReportsIndex): void {
84
+ const dir = reportsDir(basePath);
85
+ mkdirSync(dir, { recursive: true });
86
+ writeFileSync(reportsIndexPath(basePath), JSON.stringify(index, null, 2) + '\n', 'utf-8');
87
+ }
88
+
89
+ // ─── Write a report snapshot ──────────────────────────────────────────────────
90
+
91
+ export interface WriteReportSnapshotArgs {
92
+ basePath: string;
93
+ html: string;
94
+ milestoneId: string | 'final';
95
+ milestoneTitle: string;
96
+ kind: 'milestone' | 'manual' | 'final';
97
+ projectName: string;
98
+ projectPath: string;
99
+ gsdVersion: string;
100
+ // metrics
101
+ totalCost: number;
102
+ totalTokens: number;
103
+ totalDuration: number;
104
+ doneSlices: number;
105
+ totalSlices: number;
106
+ doneMilestones: number;
107
+ totalMilestones: number;
108
+ phase: string;
109
+ }
110
+
111
+ /**
112
+ * Write a report snapshot to .gsd/reports/, update reports.json, regenerate index.html.
113
+ * Returns the path of the written report file.
114
+ */
115
+ export function writeReportSnapshot(args: WriteReportSnapshotArgs): string {
116
+ const dir = reportsDir(args.basePath);
117
+ mkdirSync(dir, { recursive: true });
118
+
119
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
120
+ const prefix = args.milestoneId === 'final' ? 'final' : args.milestoneId;
121
+ const filename = `${prefix}-${timestamp}.html`;
122
+ const filePath = join(dir, filename);
123
+
124
+ writeFileSync(filePath, args.html, 'utf-8');
125
+
126
+ // Load or init registry
127
+ const existing = loadReportsIndex(args.basePath);
128
+ const index: ReportsIndex = existing ?? {
129
+ version: 1,
130
+ projectName: args.projectName,
131
+ projectPath: args.projectPath,
132
+ gsdVersion: args.gsdVersion,
133
+ entries: [],
134
+ };
135
+
136
+ // Keep metadata fresh
137
+ index.projectName = args.projectName;
138
+ index.projectPath = args.projectPath;
139
+ index.gsdVersion = args.gsdVersion;
140
+
141
+ const label = args.milestoneId === 'final'
142
+ ? 'Final Report'
143
+ : `${args.milestoneId}: ${args.milestoneTitle}`;
144
+
145
+ const entry: ReportEntry = {
146
+ filename,
147
+ generatedAt: new Date().toISOString(),
148
+ milestoneId: args.milestoneId,
149
+ milestoneTitle: args.milestoneTitle,
150
+ label,
151
+ kind: args.kind,
152
+ totalCost: args.totalCost,
153
+ totalTokens: args.totalTokens,
154
+ totalDuration: args.totalDuration,
155
+ doneSlices: args.doneSlices,
156
+ totalSlices: args.totalSlices,
157
+ doneMilestones: args.doneMilestones,
158
+ totalMilestones: args.totalMilestones,
159
+ phase: args.phase,
160
+ };
161
+
162
+ index.entries.push(entry);
163
+ saveReportsIndex(args.basePath, index);
164
+ regenerateHtmlIndex(args.basePath, index);
165
+
166
+ return filePath;
167
+ }
168
+
169
+ // ─── HTML Index Generator ─────────────────────────────────────────────────────
170
+
171
+ export function regenerateHtmlIndex(basePath: string, index: ReportsIndex): void {
172
+ const html = buildIndexHtml(index);
173
+ writeFileSync(reportsHtmlIndexPath(basePath), html, 'utf-8');
174
+ }
175
+
176
+ function buildIndexHtml(index: ReportsIndex): string {
177
+ const { projectName, projectPath, gsdVersion, entries } = index;
178
+ const generated = new Date().toISOString();
179
+
180
+ // Sort oldest → newest for the progression timeline
181
+ const sorted = [...entries].sort(
182
+ (a, b) => new Date(a.generatedAt).getTime() - new Date(b.generatedAt).getTime()
183
+ );
184
+
185
+ const latestEntry = sorted[sorted.length - 1];
186
+ const overallPct = latestEntry
187
+ ? (latestEntry.totalSlices > 0
188
+ ? Math.round((latestEntry.doneSlices / latestEntry.totalSlices) * 100)
189
+ : 0)
190
+ : 0;
191
+
192
+ // TOC: group by milestone
193
+ const milestoneGroups = new Map<string, ReportEntry[]>();
194
+ for (const e of sorted) {
195
+ const key = e.milestoneId;
196
+ const arr = milestoneGroups.get(key) ?? [];
197
+ arr.push(e);
198
+ milestoneGroups.set(key, arr);
199
+ }
200
+
201
+ const tocHtml = [...milestoneGroups.entries()].map(([mid, group]) => {
202
+ const links = group.map(e =>
203
+ `<li><a href="${esc(e.filename)}">${formatDateShort(e.generatedAt)}</a> <span class="toc-kind toc-${e.kind}">${e.kind}</span></li>`
204
+ ).join('');
205
+ return `
206
+ <div class="toc-group">
207
+ <div class="toc-group-label">${esc(mid === 'final' ? 'Final' : mid)}</div>
208
+ <ul>${links}</ul>
209
+ </div>`;
210
+ }).join('');
211
+
212
+ // Progression cards
213
+ const cardHtml = sorted.map((e, i) => {
214
+ const pct = e.totalSlices > 0 ? Math.round((e.doneSlices / e.totalSlices) * 100) : 0;
215
+ const isLatest = i === sorted.length - 1;
216
+
217
+ // Delta vs previous
218
+ let deltaHtml = '';
219
+ if (i > 0) {
220
+ const prev = sorted[i - 1];
221
+ const dCost = e.totalCost - prev.totalCost;
222
+ const dSlices = e.doneSlices - prev.doneSlices;
223
+ const dMillestones = e.doneMilestones - prev.doneMilestones;
224
+ const parts: string[] = [];
225
+ if (dCost > 0) parts.push(`+${formatCost(dCost)}`);
226
+ if (dSlices > 0) parts.push(`+${dSlices} slice${dSlices !== 1 ? 's' : ''}`);
227
+ if (dMillestones > 0) parts.push(`+${dMillestones} milestone${dMillestones !== 1 ? 's' : ''}`);
228
+ if (parts.length > 0) {
229
+ deltaHtml = `<div class="card-delta">${parts.map(p => `<span>${esc(p)}</span>`).join('')}</div>`;
230
+ }
231
+ }
232
+
233
+ return `
234
+ <a class="report-card${isLatest ? ' card-latest' : ''}" href="${esc(e.filename)}">
235
+ <div class="card-top">
236
+ <span class="card-label">${esc(e.label)}</span>
237
+ <span class="card-kind card-kind-${e.kind}">${e.kind}</span>
238
+ </div>
239
+ <div class="card-date">${formatDateShort(e.generatedAt)}</div>
240
+ <div class="card-progress">
241
+ <div class="card-bar-track">
242
+ <div class="card-bar-fill" style="width:${pct}%"></div>
243
+ </div>
244
+ <span class="card-pct">${pct}%</span>
245
+ </div>
246
+ <div class="card-stats">
247
+ <span>${esc(formatCost(e.totalCost))}</span>
248
+ <span>${esc(formatTokenCount(e.totalTokens))}</span>
249
+ <span>${esc(formatDuration(e.totalDuration))}</span>
250
+ <span>${e.doneSlices}/${e.totalSlices} slices</span>
251
+ </div>
252
+ ${deltaHtml}
253
+ ${isLatest ? '<div class="card-latest-badge">Latest</div>' : ''}
254
+ </a>`;
255
+ }).join('');
256
+
257
+ // Cost progression mini-chart (inline SVG sparkline)
258
+ const sparklineSvg = sorted.length > 1 ? buildCostSparkline(sorted) : '';
259
+
260
+ // Summary of latest state
261
+ const summaryHtml = latestEntry ? `
262
+ <div class="idx-summary">
263
+ <div class="idx-stat"><span class="idx-val">${formatCost(latestEntry.totalCost)}</span><span class="idx-lbl">Total Cost</span></div>
264
+ <div class="idx-stat"><span class="idx-val">${formatTokenCount(latestEntry.totalTokens)}</span><span class="idx-lbl">Total Tokens</span></div>
265
+ <div class="idx-stat"><span class="idx-val">${formatDuration(latestEntry.totalDuration)}</span><span class="idx-lbl">Duration</span></div>
266
+ <div class="idx-stat"><span class="idx-val">${latestEntry.doneSlices}/${latestEntry.totalSlices}</span><span class="idx-lbl">Slices</span></div>
267
+ <div class="idx-stat"><span class="idx-val">${latestEntry.doneMilestones}/${latestEntry.totalMilestones}</span><span class="idx-lbl">Milestones</span></div>
268
+ <div class="idx-stat"><span class="idx-val">${entries.length}</span><span class="idx-lbl">Reports</span></div>
269
+ </div>
270
+ <div class="idx-progress">
271
+ <div class="idx-bar-track"><div class="idx-bar-fill" style="width:${overallPct}%"></div></div>
272
+ <span class="idx-pct">${overallPct}% complete</span>
273
+ </div>` : '<p class="empty">No reports generated yet.</p>';
274
+
275
+ return `<!DOCTYPE html>
276
+ <html lang="en">
277
+ <head>
278
+ <meta charset="UTF-8">
279
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
280
+ <title>GSD Reports — ${esc(projectName)}</title>
281
+ <style>${INDEX_CSS}</style>
282
+ </head>
283
+ <body>
284
+ <header>
285
+ <div class="hdr-inner">
286
+ <div class="branding">
287
+ <span class="logo">GSD</span>
288
+ <span class="ver">v${esc(gsdVersion)}</span>
289
+ </div>
290
+ <div class="hdr-meta">
291
+ <h1>${esc(projectName)} <span class="hdr-subtitle">Reports</span></h1>
292
+ <span class="hdr-path">${esc(projectPath)}</span>
293
+ </div>
294
+ <div class="hdr-right">
295
+ <span class="gen-lbl">Updated</span>
296
+ <span class="gen">${formatDateShort(generated)}</span>
297
+ </div>
298
+ </div>
299
+ </header>
300
+
301
+ <div class="layout">
302
+ <!-- Sidebar TOC -->
303
+ <aside class="sidebar">
304
+ <div class="sidebar-title">Reports</div>
305
+ ${sorted.length > 0 ? tocHtml : '<p class="empty">No reports yet.</p>'}
306
+ </aside>
307
+
308
+ <!-- Main content -->
309
+ <main>
310
+ <section class="idx-overview">
311
+ <h2>Project Overview</h2>
312
+ ${summaryHtml}
313
+ ${sparklineSvg ? `<div class="sparkline-wrap"><h3>Cost Progression</h3>${sparklineSvg}</div>` : ''}
314
+ </section>
315
+
316
+ <section class="idx-cards">
317
+ <h2>Progression <span class="sec-count">${entries.length}</span></h2>
318
+ ${sorted.length > 0
319
+ ? `<div class="cards-grid">${cardHtml}</div>`
320
+ : '<p class="empty">No reports generated yet. Run <code>/gsd export --html</code> or enable <code>auto_report: true</code>.</p>'}
321
+ </section>
322
+ </main>
323
+ </div>
324
+
325
+ <footer>
326
+ <div class="ftr-inner">
327
+ <span class="ftr-brand">GSD v${esc(gsdVersion)}</span>
328
+ <span class="ftr-sep">—</span>
329
+ <span>${esc(projectName)}</span>
330
+ <span class="ftr-sep">—</span>
331
+ <span>${esc(projectPath)}</span>
332
+ <span class="ftr-sep">—</span>
333
+ <span>Updated ${formatDateShort(generated)}</span>
334
+ </div>
335
+ </footer>
336
+ </body>
337
+ </html>`;
338
+ }
339
+
340
+ // ─── Cost sparkline (inline SVG) ──────────────────────────────────────────────
341
+
342
+ function buildCostSparkline(entries: ReportEntry[]): string {
343
+ const costs = entries.map(e => e.totalCost);
344
+ const maxCost = Math.max(...costs, 0.001);
345
+ const W = 600, H = 60, PAD = 12;
346
+ const xStep = entries.length > 1 ? (W - PAD * 2) / (entries.length - 1) : W - PAD * 2;
347
+
348
+ const points = costs.map((c, i) => {
349
+ const x = PAD + i * xStep;
350
+ const y = PAD + (1 - c / maxCost) * (H - PAD * 2);
351
+ return `${x.toFixed(1)},${y.toFixed(1)}`;
352
+ }).join(' ');
353
+
354
+ const dots = costs.map((c, i) => {
355
+ const x = PAD + i * xStep;
356
+ const y = PAD + (1 - c / maxCost) * (H - PAD * 2);
357
+ return `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="3" class="spark-dot">
358
+ <title>${esc(entries[i].label)} — ${formatCost(c)}</title>
359
+ </circle>`;
360
+ }).join('');
361
+
362
+ // Labels at start and end
363
+ const startLabel = formatCost(costs[0]);
364
+ const endLabel = formatCost(costs[costs.length - 1]);
365
+
366
+ return `
367
+ <div class="sparkline">
368
+ <svg viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" class="spark-svg">
369
+ <polyline points="${esc(points)}" class="spark-line" fill="none"/>
370
+ ${dots}
371
+ <text x="${PAD}" y="${H - 2}" class="spark-lbl">${esc(startLabel)}</text>
372
+ <text x="${W - PAD}" y="${H - 2}" text-anchor="end" class="spark-lbl">${esc(endLabel)}</text>
373
+ </svg>
374
+ <div class="spark-axis">
375
+ ${entries.map((e, i) => {
376
+ const x = (PAD + i * xStep) / W * 100;
377
+ return `<span class="spark-tick" style="left:${x.toFixed(1)}%" title="${esc(e.generatedAt)}">${esc(e.milestoneId === 'final' ? 'final' : e.milestoneId)}</span>`;
378
+ }).join('')}
379
+ </div>
380
+ </div>`;
381
+ }
382
+
383
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
384
+
385
+ function formatDateShort(iso: string): string {
386
+ try {
387
+ const d = new Date(iso);
388
+ return d.toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' });
389
+ } catch { return iso; }
390
+ }
391
+
392
+ function esc(s: string | number | undefined | null): string {
393
+ if (s == null) return '';
394
+ return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
395
+ }
396
+
397
+ // ─── Index CSS ────────────────────────────────────────────────────────────────
398
+
399
+ const INDEX_CSS = `
400
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
401
+ :root{
402
+ --bg-0:#0f1115;--bg-1:#16181d;--bg-2:#1e2028;--bg-3:#272a33;
403
+ --border-1:#2b2e38;--border-2:#3b3f4c;
404
+ --text-0:#ededef;--text-1:#a1a1aa;--text-2:#71717a;
405
+ --accent:#5e6ad2;--accent-subtle:rgba(94,106,210,.12);
406
+ --font:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
407
+ --mono:'JetBrains Mono','Fira Code',ui-monospace,monospace;
408
+ }
409
+ html{font-size:13px}
410
+ body{background:var(--bg-0);color:var(--text-0);font-family:var(--font);line-height:1.6;-webkit-font-smoothing:antialiased}
411
+ a{color:var(--accent);text-decoration:none}
412
+ a:hover{text-decoration:underline}
413
+ h2{font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text-1);margin-bottom:16px;padding-bottom:8px;border-bottom:1px solid var(--border-1)}
414
+ h3{font-size:13px;font-weight:600;color:var(--text-1);margin:16px 0 8px}
415
+ code{font-family:var(--mono);font-size:12px;background:var(--bg-3);padding:1px 5px;border-radius:3px}
416
+ .empty{color:var(--text-2);font-size:13px;padding:8px 0}
417
+ .count{font-size:11px;font-weight:500;color:var(--text-2);background:var(--bg-3);border-radius:3px;padding:1px 6px}
418
+
419
+ /* Header */
420
+ header{background:var(--bg-1);border-bottom:1px solid var(--border-1);padding:12px 32px;position:sticky;top:0;z-index:100}
421
+ .hdr-inner{display:flex;align-items:center;gap:16px;max-width:1280px;margin:0 auto}
422
+ .branding{display:flex;align-items:baseline;gap:6px;flex-shrink:0}
423
+ .logo{font-size:18px;font-weight:800;letter-spacing:-.5px;color:var(--text-0)}
424
+ .ver{font-size:10px;color:var(--text-2);font-family:var(--mono)}
425
+ .hdr-meta{flex:1;min-width:0}
426
+ .hdr-meta h1{font-size:15px;font-weight:600}
427
+ .hdr-subtitle{color:var(--text-2);font-weight:400;font-size:13px;margin-left:4px}
428
+ .hdr-path{font-size:11px;color:var(--text-2);font-family:var(--mono);display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
429
+ .hdr-right{text-align:right;flex-shrink:0}
430
+ .gen-lbl{font-size:10px;color:var(--text-2);text-transform:uppercase;letter-spacing:.5px;display:block}
431
+ .gen{font-size:11px;color:var(--text-1)}
432
+
433
+ /* Layout */
434
+ .layout{display:grid;grid-template-columns:200px 1fr;gap:0;max-width:1280px;margin:0 auto;min-height:calc(100vh - 120px)}
435
+
436
+ /* Sidebar */
437
+ .sidebar{background:var(--bg-1);border-right:1px solid var(--border-1);padding:20px 14px;position:sticky;top:52px;height:calc(100vh - 52px);overflow-y:auto}
438
+ .sidebar-title{font-size:10px;font-weight:600;color:var(--text-2);text-transform:uppercase;letter-spacing:.5px;margin-bottom:12px}
439
+ .toc-group{margin-bottom:14px}
440
+ .toc-group-label{font-size:11px;font-weight:600;color:var(--text-1);margin-bottom:3px;font-family:var(--mono)}
441
+ .toc-group ul{list-style:none;display:flex;flex-direction:column;gap:1px}
442
+ .toc-group li{display:flex;align-items:center;gap:6px}
443
+ .toc-group a{font-size:11px;color:var(--text-2);padding:2px 4px;border-radius:3px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
444
+ .toc-group a:hover{background:var(--bg-2);color:var(--text-0);text-decoration:none}
445
+ .toc-kind{font-size:9px;color:var(--text-2);font-family:var(--mono);flex-shrink:0}
446
+
447
+ /* Main */
448
+ main{padding:28px;display:flex;flex-direction:column;gap:40px}
449
+
450
+ /* Overview */
451
+ .idx-summary{display:flex;flex-wrap:wrap;gap:1px;background:var(--border-1);border:1px solid var(--border-1);border-radius:4px;overflow:hidden;margin-bottom:16px}
452
+ .idx-stat{background:var(--bg-1);padding:10px 16px;display:flex;flex-direction:column;gap:2px;min-width:100px;flex:1}
453
+ .idx-val{font-size:18px;font-weight:600;color:var(--text-0);font-variant-numeric:tabular-nums}
454
+ .idx-lbl{font-size:10px;color:var(--text-2);text-transform:uppercase;letter-spacing:.4px}
455
+ .idx-progress{display:flex;align-items:center;gap:10px;margin-top:10px}
456
+ .idx-bar-track{flex:1;height:4px;background:var(--bg-3);border-radius:2px;overflow:hidden}
457
+ .idx-bar-fill{height:100%;background:var(--accent);border-radius:2px}
458
+ .idx-pct{font-size:12px;font-weight:600;color:var(--text-1);min-width:40px;text-align:right}
459
+
460
+ /* Sparkline */
461
+ .sparkline-wrap{margin-top:20px}
462
+ .sparkline{position:relative}
463
+ .spark-svg{display:block;background:var(--bg-1);border:1px solid var(--border-1);border-radius:4px;overflow:visible;max-width:100%}
464
+ .spark-line{stroke:var(--accent);stroke-width:1.5;fill:none}
465
+ .spark-dot{fill:var(--accent);stroke:var(--bg-1);stroke-width:2;cursor:pointer}
466
+ .spark-dot:hover{r:4;fill:var(--text-0)}
467
+ .spark-lbl{font-size:10px;fill:var(--text-2);font-family:var(--mono)}
468
+ .spark-axis{display:flex;position:relative;height:18px;margin-top:2px}
469
+ .spark-tick{position:absolute;transform:translateX(-50%);font-size:9px;color:var(--text-2);font-family:var(--mono);white-space:nowrap}
470
+
471
+ /* Report cards */
472
+ .cards-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:10px}
473
+ .report-card{
474
+ display:flex;flex-direction:column;gap:6px;
475
+ background:var(--bg-1);border:1px solid var(--border-1);border-radius:4px;
476
+ padding:14px;text-decoration:none;color:var(--text-0);
477
+ transition:border-color .12s;
478
+ }
479
+ .report-card:hover{border-color:var(--accent);text-decoration:none}
480
+ .card-latest{border-color:var(--accent)}
481
+ .card-top{display:flex;align-items:center;gap:8px}
482
+ .card-label{flex:1;font-weight:500;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
483
+ .card-kind{font-size:10px;color:var(--text-2);font-family:var(--mono);flex-shrink:0}
484
+ .card-date{font-size:11px;color:var(--text-2)}
485
+ .card-progress{display:flex;align-items:center;gap:6px}
486
+ .card-bar-track{flex:1;height:3px;background:var(--bg-3);border-radius:2px;overflow:hidden}
487
+ .card-bar-fill{height:100%;background:var(--accent);border-radius:2px}
488
+ .card-pct{font-size:11px;color:var(--text-2);min-width:30px;text-align:right}
489
+ .card-stats{display:flex;gap:8px;flex-wrap:wrap}
490
+ .card-stats span{font-size:11px;color:var(--text-2);font-variant-numeric:tabular-nums}
491
+ .card-delta{display:flex;gap:4px;flex-wrap:wrap}
492
+ .card-delta span{font-size:10px;color:var(--text-1);font-family:var(--mono)}
493
+ .card-latest-badge{display:none}
494
+
495
+ /* Footer */
496
+ footer{border-top:1px solid var(--border-1);padding:16px 32px}
497
+ .ftr-inner{display:flex;align-items:center;gap:6px;justify-content:center;font-size:11px;color:var(--text-2)}
498
+ .ftr-sep{color:var(--border-2)}
499
+
500
+ @media(max-width:768px){
501
+ .layout{grid-template-columns:1fr}
502
+ .sidebar{position:static;height:auto;border-right:none;border-bottom:1px solid var(--border-1)}
503
+ }
504
+ @media print{
505
+ .sidebar{display:none}
506
+ header{position:static}
507
+ body{background:#fff;color:#1a1a1a}
508
+ :root{--bg-0:#fff;--bg-1:#fafafa;--bg-2:#f5f5f5;--bg-3:#ebebeb;--border-1:#e5e5e5;--border-2:#d4d4d4;--text-0:#1a1a1a;--text-1:#525252;--text-2:#a3a3a3;--accent:#4f46e5}
509
+ }
510
+ `;
@@ -41,7 +41,7 @@ export function expandDependencies(deps: string[]): string[] {
41
41
  }
42
42
 
43
43
  function extractSlicesSection(content: string): string {
44
- const headingMatch = /^## Slices\s*$/m.exec(content);
44
+ const headingMatch = /^## Slices\b.*$/m.exec(content);
45
45
  if (!headingMatch || headingMatch.index == null) return "";
46
46
 
47
47
  const start = headingMatch.index + headingMatch[0].length;