gsd-pi 2.19.0 → 2.20.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 (249) hide show
  1. package/README.md +5 -1
  2. package/dist/cli.js +3 -3
  3. package/dist/onboarding.d.ts +3 -1
  4. package/dist/onboarding.js +77 -3
  5. package/dist/remote-questions-config.d.ts +1 -1
  6. package/dist/resources/extensions/google-search/index.ts +164 -47
  7. package/dist/resources/extensions/gsd/auto-prompts.ts +103 -24
  8. package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
  9. package/dist/resources/extensions/gsd/auto.ts +424 -30
  10. package/dist/resources/extensions/gsd/commands.ts +518 -36
  11. package/dist/resources/extensions/gsd/context-budget.ts +243 -0
  12. package/dist/resources/extensions/gsd/context-store.ts +195 -0
  13. package/dist/resources/extensions/gsd/dashboard-overlay.ts +41 -3
  14. package/dist/resources/extensions/gsd/db-writer.ts +341 -0
  15. package/dist/resources/extensions/gsd/debug-logger.ts +178 -0
  16. package/dist/resources/extensions/gsd/dispatch-guard.ts +0 -1
  17. package/dist/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  18. package/dist/resources/extensions/gsd/doctor-proactive.ts +286 -0
  19. package/dist/resources/extensions/gsd/doctor.ts +283 -2
  20. package/dist/resources/extensions/gsd/export.ts +81 -2
  21. package/dist/resources/extensions/gsd/files.ts +39 -9
  22. package/dist/resources/extensions/gsd/git-service.ts +6 -0
  23. package/dist/resources/extensions/gsd/gsd-db.ts +752 -0
  24. package/dist/resources/extensions/gsd/guided-flow.ts +26 -1
  25. package/dist/resources/extensions/gsd/history.ts +0 -1
  26. package/dist/resources/extensions/gsd/index.ts +277 -1
  27. package/dist/resources/extensions/gsd/md-importer.ts +526 -0
  28. package/dist/resources/extensions/gsd/metrics.ts +39 -3
  29. package/dist/resources/extensions/gsd/notifications.ts +0 -1
  30. package/dist/resources/extensions/gsd/post-unit-hooks.ts +70 -1
  31. package/dist/resources/extensions/gsd/preferences.ts +125 -150
  32. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -5
  33. package/dist/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  34. package/dist/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  35. package/dist/resources/extensions/gsd/prompts/quick-task.md +48 -0
  36. package/dist/resources/extensions/gsd/prompts/system.md +2 -1
  37. package/dist/resources/extensions/gsd/quick.ts +156 -0
  38. package/dist/resources/extensions/gsd/skill-discovery.ts +5 -3
  39. package/dist/resources/extensions/gsd/skill-health.ts +417 -0
  40. package/dist/resources/extensions/gsd/skill-telemetry.ts +127 -0
  41. package/dist/resources/extensions/gsd/state.ts +30 -0
  42. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  43. package/dist/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  44. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  45. package/dist/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  46. package/dist/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  47. package/dist/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  48. package/dist/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  49. package/dist/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  50. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  51. package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  52. package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  53. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  54. package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  55. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  56. package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  57. package/dist/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  58. package/dist/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  59. package/dist/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  60. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  61. package/dist/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  62. package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  63. package/dist/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  64. package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  65. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  66. package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  67. package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  68. package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  69. package/dist/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  70. package/dist/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  71. package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
  72. package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  73. package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  74. package/dist/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  75. package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  76. package/dist/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  77. package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  78. package/dist/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  79. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
  80. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  81. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
  82. package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  83. package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  84. package/dist/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  85. package/dist/resources/extensions/gsd/types.ts +29 -0
  86. package/dist/resources/extensions/gsd/undo.ts +0 -1
  87. package/dist/resources/extensions/gsd/unit-runtime.ts +5 -1
  88. package/dist/resources/extensions/gsd/visualizer-data.ts +352 -1
  89. package/dist/resources/extensions/gsd/visualizer-overlay.ts +166 -22
  90. package/dist/resources/extensions/gsd/visualizer-views.ts +464 -2
  91. package/dist/resources/extensions/gsd/worktree-command.ts +18 -0
  92. package/dist/resources/extensions/gsd/worktree-manager.ts +11 -4
  93. package/dist/resources/extensions/remote-questions/config.ts +4 -2
  94. package/dist/resources/extensions/remote-questions/discord-adapter.ts +2 -4
  95. package/dist/resources/extensions/remote-questions/format.ts +154 -8
  96. package/dist/resources/extensions/remote-questions/manager.ts +9 -7
  97. package/dist/resources/extensions/remote-questions/remote-command.ts +100 -4
  98. package/dist/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  99. package/dist/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  100. package/dist/resources/extensions/remote-questions/types.ts +2 -1
  101. package/dist/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  102. package/dist/resources/extensions/voice/index.ts +4 -3
  103. package/package.json +1 -1
  104. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/agent-session.js +12 -1
  106. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  108. package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
  109. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +6 -0
  111. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  112. package/packages/pi-coding-agent/dist/core/lsp/client.js +25 -0
  113. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +2 -0
  115. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/lsp/index.js +106 -3
  117. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/lsp/lsp.md +6 -0
  119. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +35 -0
  120. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/lsp/types.js +6 -0
  122. package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +3 -1
  124. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/lsp/utils.js +45 -0
  126. package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
  128. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/core/settings-manager.js +43 -11
  130. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/system-prompt.js +7 -1
  133. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
  135. package/packages/pi-coding-agent/dist/core/tools/edit.js +5 -0
  136. package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
  137. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
  138. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  139. package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
  140. package/packages/pi-coding-agent/dist/core/tools/write.js +5 -0
  141. package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
  142. package/packages/pi-coding-agent/src/core/agent-session.ts +13 -1
  143. package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
  144. package/packages/pi-coding-agent/src/core/lsp/client.ts +26 -0
  145. package/packages/pi-coding-agent/src/core/lsp/index.ts +157 -2
  146. package/packages/pi-coding-agent/src/core/lsp/lsp.md +6 -0
  147. package/packages/pi-coding-agent/src/core/lsp/types.ts +53 -0
  148. package/packages/pi-coding-agent/src/core/lsp/utils.ts +56 -0
  149. package/packages/pi-coding-agent/src/core/settings-manager.ts +41 -11
  150. package/packages/pi-coding-agent/src/core/system-prompt.ts +7 -1
  151. package/packages/pi-coding-agent/src/core/tools/edit.ts +3 -0
  152. package/packages/pi-coding-agent/src/core/tools/write.ts +3 -0
  153. package/src/resources/extensions/google-search/index.ts +164 -47
  154. package/src/resources/extensions/gsd/auto-prompts.ts +103 -24
  155. package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
  156. package/src/resources/extensions/gsd/auto.ts +424 -30
  157. package/src/resources/extensions/gsd/commands.ts +518 -36
  158. package/src/resources/extensions/gsd/context-budget.ts +243 -0
  159. package/src/resources/extensions/gsd/context-store.ts +195 -0
  160. package/src/resources/extensions/gsd/dashboard-overlay.ts +41 -3
  161. package/src/resources/extensions/gsd/db-writer.ts +341 -0
  162. package/src/resources/extensions/gsd/debug-logger.ts +178 -0
  163. package/src/resources/extensions/gsd/dispatch-guard.ts +0 -1
  164. package/src/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  165. package/src/resources/extensions/gsd/doctor-proactive.ts +286 -0
  166. package/src/resources/extensions/gsd/doctor.ts +283 -2
  167. package/src/resources/extensions/gsd/export.ts +81 -2
  168. package/src/resources/extensions/gsd/files.ts +39 -9
  169. package/src/resources/extensions/gsd/git-service.ts +6 -0
  170. package/src/resources/extensions/gsd/gsd-db.ts +752 -0
  171. package/src/resources/extensions/gsd/guided-flow.ts +26 -1
  172. package/src/resources/extensions/gsd/history.ts +0 -1
  173. package/src/resources/extensions/gsd/index.ts +277 -1
  174. package/src/resources/extensions/gsd/md-importer.ts +526 -0
  175. package/src/resources/extensions/gsd/metrics.ts +39 -3
  176. package/src/resources/extensions/gsd/notifications.ts +0 -1
  177. package/src/resources/extensions/gsd/post-unit-hooks.ts +70 -1
  178. package/src/resources/extensions/gsd/preferences.ts +125 -150
  179. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -5
  180. package/src/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  181. package/src/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  182. package/src/resources/extensions/gsd/prompts/quick-task.md +48 -0
  183. package/src/resources/extensions/gsd/prompts/system.md +2 -1
  184. package/src/resources/extensions/gsd/quick.ts +156 -0
  185. package/src/resources/extensions/gsd/skill-discovery.ts +5 -3
  186. package/src/resources/extensions/gsd/skill-health.ts +417 -0
  187. package/src/resources/extensions/gsd/skill-telemetry.ts +127 -0
  188. package/src/resources/extensions/gsd/state.ts +30 -0
  189. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  190. package/src/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  191. package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  192. package/src/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  193. package/src/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  194. package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  195. package/src/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  196. package/src/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  197. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  198. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  199. package/src/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  200. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  201. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  202. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  203. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  204. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  205. package/src/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  206. package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  207. package/src/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  208. package/src/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  209. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  210. package/src/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  211. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  212. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  213. package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  214. package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  215. package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  216. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  217. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  218. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
  219. package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  220. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  221. package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  222. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  223. package/src/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  224. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  225. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  226. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
  227. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  228. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
  229. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  230. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  231. package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  232. package/src/resources/extensions/gsd/types.ts +29 -0
  233. package/src/resources/extensions/gsd/undo.ts +0 -1
  234. package/src/resources/extensions/gsd/unit-runtime.ts +5 -1
  235. package/src/resources/extensions/gsd/visualizer-data.ts +352 -1
  236. package/src/resources/extensions/gsd/visualizer-overlay.ts +166 -22
  237. package/src/resources/extensions/gsd/visualizer-views.ts +464 -2
  238. package/src/resources/extensions/gsd/worktree-command.ts +18 -0
  239. package/src/resources/extensions/gsd/worktree-manager.ts +11 -4
  240. package/src/resources/extensions/remote-questions/config.ts +4 -2
  241. package/src/resources/extensions/remote-questions/discord-adapter.ts +2 -4
  242. package/src/resources/extensions/remote-questions/format.ts +154 -8
  243. package/src/resources/extensions/remote-questions/manager.ts +9 -7
  244. package/src/resources/extensions/remote-questions/remote-command.ts +100 -4
  245. package/src/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  246. package/src/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  247. package/src/resources/extensions/remote-questions/types.ts +2 -1
  248. package/src/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  249. package/src/resources/extensions/voice/index.ts +4 -3
@@ -1,7 +1,7 @@
1
1
  // Data loader for workflow visualizer overlay — aggregates state + metrics.
2
2
 
3
3
  import { deriveState } from './state.js';
4
- import { parseRoadmap, parsePlan, loadFile } from './files.js';
4
+ import { parseRoadmap, parsePlan, parseSummary, loadFile } from './files.js';
5
5
  import { findMilestoneIds } from './guided-flow.js';
6
6
  import { resolveMilestoneFile, resolveSliceFile } from './paths.js';
7
7
  import {
@@ -11,6 +11,7 @@ import {
11
11
  aggregateBySlice,
12
12
  aggregateByModel,
13
13
  loadLedgerFromDisk,
14
+ classifyUnitPhase,
14
15
  } from './metrics.js';
15
16
 
16
17
  import type { Phase } from './types.js';
@@ -49,6 +50,37 @@ export interface VisualizerTask {
49
50
  active: boolean;
50
51
  }
51
52
 
53
+ export interface CriticalPathInfo {
54
+ milestonePath: string[];
55
+ slicePath: string[];
56
+ milestoneSlack: Map<string, number>;
57
+ sliceSlack: Map<string, number>;
58
+ }
59
+
60
+ export interface AgentActivityInfo {
61
+ currentUnit: { type: string; id: string; startedAt: number } | null;
62
+ elapsed: number;
63
+ completedUnits: number;
64
+ totalSlices: number;
65
+ completionRate: number;
66
+ active: boolean;
67
+ sessionCost: number;
68
+ sessionTokens: number;
69
+ }
70
+
71
+ export interface ChangelogEntry {
72
+ milestoneId: string;
73
+ sliceId: string;
74
+ title: string;
75
+ oneLiner: string;
76
+ filesModified: { path: string; description: string }[];
77
+ completedAt: string;
78
+ }
79
+
80
+ export interface ChangelogInfo {
81
+ entries: ChangelogEntry[];
82
+ }
83
+
52
84
  export interface VisualizerData {
53
85
  milestones: VisualizerMilestone[];
54
86
  phase: Phase;
@@ -57,6 +89,308 @@ export interface VisualizerData {
57
89
  bySlice: SliceAggregate[];
58
90
  byModel: ModelAggregate[];
59
91
  units: UnitMetrics[];
92
+ criticalPath: CriticalPathInfo;
93
+ remainingSliceCount: number;
94
+ agentActivity: AgentActivityInfo | null;
95
+ changelog: ChangelogInfo;
96
+ }
97
+
98
+ // ─── Critical Path ────────────────────────────────────────────────────────────
99
+
100
+ export function computeCriticalPath(milestones: VisualizerMilestone[]): CriticalPathInfo {
101
+ const empty: CriticalPathInfo = {
102
+ milestonePath: [],
103
+ slicePath: [],
104
+ milestoneSlack: new Map(),
105
+ sliceSlack: new Map(),
106
+ };
107
+
108
+ if (milestones.length === 0) return empty;
109
+
110
+ // Milestone-level critical path (weight = number of incomplete slices)
111
+ const msMap = new Map(milestones.map(m => [m.id, m]));
112
+ const msIds = milestones.map(m => m.id);
113
+ const msAdj = new Map<string, string[]>();
114
+ const msWeight = new Map<string, number>();
115
+
116
+ for (const ms of milestones) {
117
+ msAdj.set(ms.id, []);
118
+ const incomplete = ms.slices.filter(s => !s.done).length;
119
+ msWeight.set(ms.id, ms.status === 'complete' ? 0 : Math.max(1, incomplete));
120
+ }
121
+
122
+ for (const ms of milestones) {
123
+ for (const dep of ms.dependsOn) {
124
+ if (msMap.has(dep)) {
125
+ const adj = msAdj.get(dep);
126
+ if (adj) adj.push(ms.id);
127
+ }
128
+ }
129
+ }
130
+
131
+ // Topological sort (Kahn's algorithm)
132
+ const inDegree = new Map<string, number>();
133
+ for (const id of msIds) inDegree.set(id, 0);
134
+ for (const ms of milestones) {
135
+ for (const dep of ms.dependsOn) {
136
+ if (msMap.has(dep)) inDegree.set(ms.id, (inDegree.get(ms.id) ?? 0) + 1);
137
+ }
138
+ }
139
+
140
+ const queue: string[] = [];
141
+ for (const [id, deg] of inDegree) {
142
+ if (deg === 0) queue.push(id);
143
+ }
144
+
145
+ const topoOrder: string[] = [];
146
+ while (queue.length > 0) {
147
+ const node = queue.shift()!;
148
+ topoOrder.push(node);
149
+ for (const next of (msAdj.get(node) ?? [])) {
150
+ const d = (inDegree.get(next) ?? 1) - 1;
151
+ inDegree.set(next, d);
152
+ if (d === 0) queue.push(next);
153
+ }
154
+ }
155
+
156
+ // Longest path from each root
157
+ const dist = new Map<string, number>();
158
+ const prev = new Map<string, string | null>();
159
+ for (const id of msIds) {
160
+ dist.set(id, 0);
161
+ prev.set(id, null);
162
+ }
163
+
164
+ for (const node of topoOrder) {
165
+ const w = msWeight.get(node) ?? 1;
166
+ const nodeDist = dist.get(node)! + w;
167
+ for (const next of (msAdj.get(node) ?? [])) {
168
+ if (nodeDist > dist.get(next)!) {
169
+ dist.set(next, nodeDist);
170
+ prev.set(next, node);
171
+ }
172
+ }
173
+ }
174
+
175
+ // Find the end of the critical path (node with max dist + own weight)
176
+ let maxDist = 0;
177
+ let endNode = msIds[0];
178
+ for (const id of msIds) {
179
+ const totalDist = dist.get(id)! + (msWeight.get(id) ?? 1);
180
+ if (totalDist > maxDist) {
181
+ maxDist = totalDist;
182
+ endNode = id;
183
+ }
184
+ }
185
+
186
+ // Trace back
187
+ const milestonePath: string[] = [];
188
+ let cur: string | null = endNode;
189
+ while (cur !== null) {
190
+ milestonePath.unshift(cur);
191
+ cur = prev.get(cur) ?? null;
192
+ }
193
+
194
+ // Compute milestone slack
195
+ const milestoneSlack = new Map<string, number>();
196
+ const criticalSet = new Set(milestonePath);
197
+ for (const id of msIds) {
198
+ if (criticalSet.has(id)) {
199
+ milestoneSlack.set(id, 0);
200
+ } else {
201
+ const nodeTotal = dist.get(id)! + (msWeight.get(id) ?? 1);
202
+ milestoneSlack.set(id, Math.max(0, maxDist - nodeTotal));
203
+ }
204
+ }
205
+
206
+ // Slice-level critical path within active milestone
207
+ const activeMs = milestones.find(m => m.status === 'active');
208
+ let slicePath: string[] = [];
209
+ const sliceSlack = new Map<string, number>();
210
+
211
+ if (activeMs && activeMs.slices.length > 0) {
212
+ const slMap = new Map(activeMs.slices.map(s => [s.id, s]));
213
+ const slAdj = new Map<string, string[]>();
214
+ for (const s of activeMs.slices) slAdj.set(s.id, []);
215
+ for (const s of activeMs.slices) {
216
+ for (const dep of s.depends) {
217
+ if (slMap.has(dep)) {
218
+ const adj = slAdj.get(dep);
219
+ if (adj) adj.push(s.id);
220
+ }
221
+ }
222
+ }
223
+
224
+ // Topo sort slices
225
+ const slIn = new Map<string, number>();
226
+ for (const s of activeMs.slices) slIn.set(s.id, 0);
227
+ for (const s of activeMs.slices) {
228
+ for (const dep of s.depends) {
229
+ if (slMap.has(dep)) slIn.set(s.id, (slIn.get(s.id) ?? 0) + 1);
230
+ }
231
+ }
232
+
233
+ const slQueue: string[] = [];
234
+ for (const [id, d] of slIn) {
235
+ if (d === 0) slQueue.push(id);
236
+ }
237
+
238
+ const slTopo: string[] = [];
239
+ while (slQueue.length > 0) {
240
+ const n = slQueue.shift()!;
241
+ slTopo.push(n);
242
+ for (const next of (slAdj.get(n) ?? [])) {
243
+ const d = (slIn.get(next) ?? 1) - 1;
244
+ slIn.set(next, d);
245
+ if (d === 0) slQueue.push(next);
246
+ }
247
+ }
248
+
249
+ const slDist = new Map<string, number>();
250
+ const slPrev = new Map<string, string | null>();
251
+ for (const s of activeMs.slices) {
252
+ const w = s.done ? 0 : 1;
253
+ slDist.set(s.id, 0);
254
+ slPrev.set(s.id, null);
255
+ }
256
+
257
+ for (const n of slTopo) {
258
+ const w = (slMap.get(n)?.done ? 0 : 1);
259
+ const nd = slDist.get(n)! + w;
260
+ for (const next of (slAdj.get(n) ?? [])) {
261
+ if (nd > slDist.get(next)!) {
262
+ slDist.set(next, nd);
263
+ slPrev.set(next, n);
264
+ }
265
+ }
266
+ }
267
+
268
+ let slMax = 0;
269
+ let slEnd = activeMs.slices[0].id;
270
+ for (const s of activeMs.slices) {
271
+ const totalDist = slDist.get(s.id)! + (s.done ? 0 : 1);
272
+ if (totalDist > slMax) {
273
+ slMax = totalDist;
274
+ slEnd = s.id;
275
+ }
276
+ }
277
+
278
+ let slCur: string | null = slEnd;
279
+ while (slCur !== null) {
280
+ slicePath.unshift(slCur);
281
+ slCur = slPrev.get(slCur) ?? null;
282
+ }
283
+
284
+ const slCritSet = new Set(slicePath);
285
+ for (const s of activeMs.slices) {
286
+ if (slCritSet.has(s.id)) {
287
+ sliceSlack.set(s.id, 0);
288
+ } else {
289
+ const nodeTotal = slDist.get(s.id)! + (s.done ? 0 : 1);
290
+ sliceSlack.set(s.id, Math.max(0, slMax - nodeTotal));
291
+ }
292
+ }
293
+ }
294
+
295
+ return { milestonePath, slicePath, milestoneSlack, sliceSlack };
296
+ }
297
+
298
+ // ─── Agent Activity ──────────────────────────────────────────────────────────
299
+
300
+ function loadAgentActivity(units: UnitMetrics[], milestones: VisualizerMilestone[]): AgentActivityInfo | null {
301
+ if (units.length === 0) return null;
302
+
303
+ // Find currently running unit (finishedAt === 0)
304
+ const running = units.find(u => u.finishedAt === 0);
305
+ const now = Date.now();
306
+
307
+ const completedUnits = units.filter(u => u.finishedAt > 0).length;
308
+ const totalSlices = milestones.reduce((sum, m) => sum + m.slices.length, 0);
309
+
310
+ // Completion rate from finished units
311
+ const finished = units.filter(u => u.finishedAt > 0);
312
+ let completionRate = 0;
313
+ if (finished.length >= 2) {
314
+ const earliest = Math.min(...finished.map(u => u.startedAt));
315
+ const latest = Math.max(...finished.map(u => u.finishedAt));
316
+ const totalHours = (latest - earliest) / 3_600_000;
317
+ completionRate = totalHours > 0 ? finished.length / totalHours : 0;
318
+ }
319
+
320
+ const sessionCost = units.reduce((sum, u) => sum + u.cost, 0);
321
+ const sessionTokens = units.reduce((sum, u) => sum + u.tokens.total, 0);
322
+
323
+ return {
324
+ currentUnit: running
325
+ ? { type: running.type, id: running.id, startedAt: running.startedAt }
326
+ : null,
327
+ elapsed: running ? now - running.startedAt : 0,
328
+ completedUnits,
329
+ totalSlices,
330
+ completionRate,
331
+ active: !!running,
332
+ sessionCost,
333
+ sessionTokens,
334
+ };
335
+ }
336
+
337
+ // ─── Changelog ───────────────────────────────────────────────────────────────
338
+
339
+ const changelogCache = new Map<string, { mtime: number; entry: ChangelogEntry }>();
340
+
341
+ async function loadChangelog(basePath: string, milestones: VisualizerMilestone[]): Promise<ChangelogInfo> {
342
+ const entries: ChangelogEntry[] = [];
343
+
344
+ for (const ms of milestones) {
345
+ for (const sl of ms.slices) {
346
+ if (!sl.done) continue;
347
+
348
+ const summaryFile = resolveSliceFile(basePath, ms.id, sl.id, 'SUMMARY');
349
+ if (!summaryFile) continue;
350
+
351
+ // Check cache by file path
352
+ const cacheKey = `${ms.id}/${sl.id}`;
353
+ const cached = changelogCache.get(cacheKey);
354
+
355
+ // Check mtime for cache invalidation
356
+ let mtime = 0;
357
+ try {
358
+ const { statSync } = await import('node:fs');
359
+ mtime = statSync(summaryFile).mtimeMs;
360
+ } catch {
361
+ continue;
362
+ }
363
+
364
+ if (cached && cached.mtime === mtime) {
365
+ entries.push(cached.entry);
366
+ continue;
367
+ }
368
+
369
+ const content = await loadFile(summaryFile);
370
+ if (!content) continue;
371
+
372
+ const summary = parseSummary(content);
373
+ const entry: ChangelogEntry = {
374
+ milestoneId: ms.id,
375
+ sliceId: sl.id,
376
+ title: sl.title,
377
+ oneLiner: summary.oneLiner,
378
+ filesModified: summary.filesModified.map(f => ({
379
+ path: f.path,
380
+ description: f.description,
381
+ })),
382
+ completedAt: summary.frontmatter.completed_at ?? '',
383
+ };
384
+
385
+ changelogCache.set(cacheKey, { mtime, entry });
386
+ entries.push(entry);
387
+ }
388
+ }
389
+
390
+ // Sort by completedAt descending
391
+ entries.sort((a, b) => (b.completedAt || '').localeCompare(a.completedAt || ''));
392
+
393
+ return { entries };
60
394
  }
61
395
 
62
396
  // ─── Loader ───────────────────────────────────────────────────────────────────
@@ -142,6 +476,19 @@ export async function loadVisualizerData(basePath: string): Promise<VisualizerDa
142
476
  byModel = aggregateByModel(units);
143
477
  }
144
478
 
479
+ // Compute new fields
480
+ const criticalPath = computeCriticalPath(milestones);
481
+
482
+ let remainingSliceCount = 0;
483
+ for (const ms of milestones) {
484
+ for (const sl of ms.slices) {
485
+ if (!sl.done) remainingSliceCount++;
486
+ }
487
+ }
488
+
489
+ const agentActivity = loadAgentActivity(units, milestones);
490
+ const changelog = await loadChangelog(basePath, milestones);
491
+
145
492
  return {
146
493
  milestones,
147
494
  phase: state.phase,
@@ -150,5 +497,9 @@ export async function loadVisualizerData(basePath: string): Promise<VisualizerDa
150
497
  bySlice,
151
498
  byModel,
152
499
  units,
500
+ criticalPath,
501
+ remainingSliceCount,
502
+ agentActivity,
503
+ changelog,
153
504
  };
154
505
  }
@@ -6,9 +6,23 @@ import {
6
6
  renderDepsView,
7
7
  renderMetricsView,
8
8
  renderTimelineView,
9
+ renderAgentView,
10
+ renderChangelogView,
11
+ renderExportView,
12
+ type ProgressFilter,
9
13
  } from "./visualizer-views.js";
14
+ import { writeExportFile } from "./export.js";
10
15
 
11
- const TAB_LABELS = ["1 Progress", "2 Deps", "3 Metrics", "4 Timeline"];
16
+ const TAB_COUNT = 7;
17
+ const TAB_LABELS = [
18
+ "1 Progress",
19
+ "2 Deps",
20
+ "3 Metrics",
21
+ "4 Timeline",
22
+ "5 Agent",
23
+ "6 Changes",
24
+ "7 Export",
25
+ ];
12
26
 
13
27
  export class GSDVisualizerOverlay {
14
28
  private tui: { requestRender: () => void };
@@ -16,7 +30,7 @@ export class GSDVisualizerOverlay {
16
30
  private onClose: () => void;
17
31
 
18
32
  activeTab = 0;
19
- scrollOffsets: number[] = [0, 0, 0, 0];
33
+ scrollOffsets: number[] = new Array(TAB_COUNT).fill(0);
20
34
  loading = true;
21
35
  disposed = false;
22
36
  cachedWidth?: number;
@@ -25,6 +39,15 @@ export class GSDVisualizerOverlay {
25
39
  data: VisualizerData | null = null;
26
40
  basePath: string;
27
41
 
42
+ // Filter state (Progress tab)
43
+ filterMode = false;
44
+ filterText = "";
45
+ filterField: "all" | "status" | "risk" | "keyword" = "all";
46
+
47
+ // Export state
48
+ lastExportPath?: string;
49
+ exportStatus?: string;
50
+
28
51
  constructor(
29
52
  tui: { requestRender: () => void },
30
53
  theme: Theme,
@@ -52,6 +75,37 @@ export class GSDVisualizerOverlay {
52
75
  }
53
76
 
54
77
  handleInput(data: string): void {
78
+ // Filter mode input routing
79
+ if (this.filterMode) {
80
+ if (matchesKey(data, Key.escape)) {
81
+ this.filterMode = false;
82
+ this.filterText = "";
83
+ this.invalidate();
84
+ this.tui.requestRender();
85
+ return;
86
+ }
87
+ if (matchesKey(data, Key.enter)) {
88
+ this.filterMode = false;
89
+ this.invalidate();
90
+ this.tui.requestRender();
91
+ return;
92
+ }
93
+ if (matchesKey(data, Key.backspace)) {
94
+ this.filterText = this.filterText.slice(0, -1);
95
+ this.invalidate();
96
+ this.tui.requestRender();
97
+ return;
98
+ }
99
+ // Append printable characters
100
+ if (data.length === 1 && data.charCodeAt(0) >= 32) {
101
+ this.filterText += data;
102
+ this.invalidate();
103
+ this.tui.requestRender();
104
+ return;
105
+ }
106
+ return;
107
+ }
108
+
55
109
  if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
56
110
  this.dispose();
57
111
  this.onClose();
@@ -59,19 +113,46 @@ export class GSDVisualizerOverlay {
59
113
  }
60
114
 
61
115
  if (matchesKey(data, Key.tab)) {
62
- this.activeTab = (this.activeTab + 1) % 4;
116
+ this.activeTab = (this.activeTab + 1) % TAB_COUNT;
63
117
  this.invalidate();
64
118
  this.tui.requestRender();
65
119
  return;
66
120
  }
67
121
 
68
- if (data === "1" || data === "2" || data === "3" || data === "4") {
122
+ if ("1234567".includes(data) && data.length === 1) {
69
123
  this.activeTab = parseInt(data, 10) - 1;
70
124
  this.invalidate();
71
125
  this.tui.requestRender();
72
126
  return;
73
127
  }
74
128
 
129
+ // "/" enters filter mode on Progress tab
130
+ if (data === "/" && this.activeTab === 0) {
131
+ this.filterMode = true;
132
+ this.filterText = "";
133
+ this.invalidate();
134
+ this.tui.requestRender();
135
+ return;
136
+ }
137
+
138
+ // "f" cycles filter field on Progress tab (when not in filter mode)
139
+ if (data === "f" && this.activeTab === 0) {
140
+ const fields: Array<"all" | "status" | "risk" | "keyword"> = ["all", "status", "risk", "keyword"];
141
+ const idx = fields.indexOf(this.filterField);
142
+ this.filterField = fields[(idx + 1) % fields.length];
143
+ this.invalidate();
144
+ this.tui.requestRender();
145
+ return;
146
+ }
147
+
148
+ // Export tab key handling
149
+ if (this.activeTab === 6 && this.data) {
150
+ if (data === "m" || data === "j" || data === "s") {
151
+ this.handleExportKey(data);
152
+ return;
153
+ }
154
+ }
155
+
75
156
  if (matchesKey(data, Key.down) || matchesKey(data, "j")) {
76
157
  this.scrollOffsets[this.activeTab]++;
77
158
  this.invalidate();
@@ -101,6 +182,62 @@ export class GSDVisualizerOverlay {
101
182
  }
102
183
  }
103
184
 
185
+ private handleExportKey(key: "m" | "j" | "s"): void {
186
+ if (!this.data) return;
187
+
188
+ const format = key === "m" ? "markdown" : key === "j" ? "json" : "snapshot";
189
+
190
+ if (format === "snapshot") {
191
+ // Capture current active tab's rendered lines as snapshot
192
+ const snapshotLines = this.renderTabContent(this.activeTab, 80);
193
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
194
+ const { writeFileSync, mkdirSync } = require("node:fs");
195
+ const { join } = require("node:path");
196
+ const { gsdRoot } = require("./paths.js");
197
+ const exportDir = gsdRoot(this.basePath);
198
+ mkdirSync(exportDir, { recursive: true });
199
+ const outPath = join(exportDir, `snapshot-${timestamp}.txt`);
200
+ writeFileSync(outPath, snapshotLines.join("\n") + "\n", "utf-8");
201
+ this.lastExportPath = outPath;
202
+ this.exportStatus = "Snapshot saved";
203
+ } else {
204
+ const result = writeExportFile(this.basePath, format, this.data);
205
+ if (result) {
206
+ this.lastExportPath = result;
207
+ this.exportStatus = `${format} export saved`;
208
+ }
209
+ }
210
+
211
+ this.invalidate();
212
+ this.tui.requestRender();
213
+ }
214
+
215
+ private renderTabContent(tab: number, width: number): string[] {
216
+ if (!this.data) return [];
217
+ const th = this.theme;
218
+ switch (tab) {
219
+ case 0: {
220
+ const filter: ProgressFilter | undefined =
221
+ this.filterText ? { text: this.filterText, field: this.filterField } : undefined;
222
+ return renderProgressView(this.data, th, width, filter);
223
+ }
224
+ case 1:
225
+ return renderDepsView(this.data, th, width);
226
+ case 2:
227
+ return renderMetricsView(this.data, th, width);
228
+ case 3:
229
+ return renderTimelineView(this.data, th, width);
230
+ case 4:
231
+ return renderAgentView(this.data, th, width);
232
+ case 5:
233
+ return renderChangelogView(this.data, th, width);
234
+ case 6:
235
+ return renderExportView(this.data, th, width, this.lastExportPath);
236
+ default:
237
+ return [];
238
+ }
239
+ }
240
+
104
241
  render(width: number): string[] {
105
242
  if (this.cachedLines && this.cachedWidth === width) {
106
243
  return this.cachedLines;
@@ -112,35 +249,42 @@ export class GSDVisualizerOverlay {
112
249
 
113
250
  // Tab bar
114
251
  const tabs = TAB_LABELS.map((label, i) => {
252
+ let displayLabel = label;
253
+ // Show filter indicator on Progress tab
254
+ if (i === 0 && this.filterText) {
255
+ displayLabel += " ✱";
256
+ }
115
257
  if (i === this.activeTab) {
116
- return th.fg("accent", `[${label}]`);
258
+ return th.fg("accent", `[${displayLabel}]`);
117
259
  }
118
- return th.fg("dim", `[${label}]`);
260
+ return th.fg("dim", `[${displayLabel}]`);
119
261
  });
120
- content.push(" " + tabs.join(" "));
262
+ content.push(" " + tabs.join(" "));
121
263
  content.push("");
122
264
 
265
+ // Filter bar (when in filter mode)
266
+ if (this.filterMode && this.activeTab === 0) {
267
+ content.push(
268
+ th.fg("accent", `Filter (${this.filterField}): ${this.filterText}█`),
269
+ );
270
+ content.push("");
271
+ }
272
+
123
273
  if (this.loading) {
124
274
  const loadingText = "Loading…";
125
275
  const vis = visibleWidth(loadingText);
126
276
  const leftPad = Math.max(0, Math.floor((innerWidth - vis) / 2));
127
277
  content.push(" ".repeat(leftPad) + loadingText);
128
278
  } else if (this.data) {
129
- let viewLines: string[] = [];
130
- switch (this.activeTab) {
131
- case 0:
132
- viewLines = renderProgressView(this.data, th, innerWidth);
133
- break;
134
- case 1:
135
- viewLines = renderDepsView(this.data, th, innerWidth);
136
- break;
137
- case 2:
138
- viewLines = renderMetricsView(this.data, th, innerWidth);
139
- break;
140
- case 3:
141
- viewLines = renderTimelineView(this.data, th, innerWidth);
142
- break;
279
+ const viewLines = this.renderTabContent(this.activeTab, innerWidth);
280
+
281
+ // Show export status message if present
282
+ if (this.exportStatus && this.activeTab === 6) {
283
+ content.push(th.fg("success", this.exportStatus));
284
+ content.push("");
285
+ this.exportStatus = undefined;
143
286
  }
287
+
144
288
  content.push(...viewLines);
145
289
  }
146
290
 
@@ -156,7 +300,7 @@ export class GSDVisualizerOverlay {
156
300
  const lines = this.wrapInBox(visibleContent, width);
157
301
 
158
302
  // Footer hint
159
- const hint = th.fg("dim", "Tab/1-4 switch · ↑↓ scroll · g/G top/end · esc close");
303
+ const hint = th.fg("dim", "Tab/1-7 switch · / filter · ↑↓ scroll · g/G top/end · esc close");
160
304
  const hintVis = visibleWidth(hint);
161
305
  const hintPad = Math.max(0, Math.floor((width - hintVis) / 2));
162
306
  lines.push(" ".repeat(hintPad) + hint);