cap-pro 1.0.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 (275) hide show
  1. package/.claude-plugin/README.md +26 -0
  2. package/.claude-plugin/marketplace.json +24 -0
  3. package/.claude-plugin/plugin.json +24 -0
  4. package/LICENSE +21 -0
  5. package/README.ja-JP.md +834 -0
  6. package/README.ko-KR.md +823 -0
  7. package/README.md +806 -0
  8. package/README.pt-BR.md +452 -0
  9. package/README.zh-CN.md +800 -0
  10. package/agents/cap-architect.md +269 -0
  11. package/agents/cap-brainstormer.md +207 -0
  12. package/agents/cap-curator.md +276 -0
  13. package/agents/cap-debugger.md +365 -0
  14. package/agents/cap-designer.md +246 -0
  15. package/agents/cap-historian.md +464 -0
  16. package/agents/cap-migrator.md +291 -0
  17. package/agents/cap-prototyper.md +197 -0
  18. package/agents/cap-validator.md +308 -0
  19. package/bin/install.js +5433 -0
  20. package/cap/bin/cap-tools.cjs +853 -0
  21. package/cap/bin/lib/arc-scanner.cjs +344 -0
  22. package/cap/bin/lib/cap-affinity-engine.cjs +862 -0
  23. package/cap/bin/lib/cap-anchor.cjs +228 -0
  24. package/cap/bin/lib/cap-annotation-writer.cjs +340 -0
  25. package/cap/bin/lib/cap-checkpoint.cjs +434 -0
  26. package/cap/bin/lib/cap-cluster-detect.cjs +945 -0
  27. package/cap/bin/lib/cap-cluster-display.cjs +52 -0
  28. package/cap/bin/lib/cap-cluster-format.cjs +245 -0
  29. package/cap/bin/lib/cap-cluster-helpers.cjs +295 -0
  30. package/cap/bin/lib/cap-cluster-io.cjs +212 -0
  31. package/cap/bin/lib/cap-completeness.cjs +540 -0
  32. package/cap/bin/lib/cap-deps.cjs +583 -0
  33. package/cap/bin/lib/cap-design-families.cjs +332 -0
  34. package/cap/bin/lib/cap-design.cjs +966 -0
  35. package/cap/bin/lib/cap-divergence-detector.cjs +400 -0
  36. package/cap/bin/lib/cap-doctor.cjs +752 -0
  37. package/cap/bin/lib/cap-feature-map-internals.cjs +19 -0
  38. package/cap/bin/lib/cap-feature-map-migrate.cjs +335 -0
  39. package/cap/bin/lib/cap-feature-map-monorepo.cjs +885 -0
  40. package/cap/bin/lib/cap-feature-map-shard.cjs +315 -0
  41. package/cap/bin/lib/cap-feature-map.cjs +1943 -0
  42. package/cap/bin/lib/cap-fitness-score.cjs +1075 -0
  43. package/cap/bin/lib/cap-impact-analysis.cjs +652 -0
  44. package/cap/bin/lib/cap-learn-review.cjs +1072 -0
  45. package/cap/bin/lib/cap-learning-signals.cjs +627 -0
  46. package/cap/bin/lib/cap-loader.cjs +227 -0
  47. package/cap/bin/lib/cap-logger.cjs +57 -0
  48. package/cap/bin/lib/cap-memory-bridge.cjs +764 -0
  49. package/cap/bin/lib/cap-memory-confidence.cjs +452 -0
  50. package/cap/bin/lib/cap-memory-dir.cjs +987 -0
  51. package/cap/bin/lib/cap-memory-engine.cjs +698 -0
  52. package/cap/bin/lib/cap-memory-extends.cjs +398 -0
  53. package/cap/bin/lib/cap-memory-graph.cjs +790 -0
  54. package/cap/bin/lib/cap-memory-migrate.cjs +2015 -0
  55. package/cap/bin/lib/cap-memory-pin.cjs +183 -0
  56. package/cap/bin/lib/cap-memory-platform.cjs +490 -0
  57. package/cap/bin/lib/cap-memory-prune.cjs +707 -0
  58. package/cap/bin/lib/cap-memory-schema.cjs +812 -0
  59. package/cap/bin/lib/cap-migrate-tags.cjs +309 -0
  60. package/cap/bin/lib/cap-migrate.cjs +540 -0
  61. package/cap/bin/lib/cap-pattern-apply.cjs +1203 -0
  62. package/cap/bin/lib/cap-pattern-pipeline.cjs +1034 -0
  63. package/cap/bin/lib/cap-plugin-manifest.cjs +80 -0
  64. package/cap/bin/lib/cap-realtime-affinity.cjs +399 -0
  65. package/cap/bin/lib/cap-reconcile.cjs +570 -0
  66. package/cap/bin/lib/cap-research-gate.cjs +218 -0
  67. package/cap/bin/lib/cap-scope-filter.cjs +402 -0
  68. package/cap/bin/lib/cap-semantic-pipeline.cjs +1038 -0
  69. package/cap/bin/lib/cap-session-extract.cjs +987 -0
  70. package/cap/bin/lib/cap-session.cjs +445 -0
  71. package/cap/bin/lib/cap-snapshot-linkage.cjs +963 -0
  72. package/cap/bin/lib/cap-stack-docs.cjs +646 -0
  73. package/cap/bin/lib/cap-tag-observer.cjs +371 -0
  74. package/cap/bin/lib/cap-tag-scanner.cjs +1766 -0
  75. package/cap/bin/lib/cap-telemetry.cjs +466 -0
  76. package/cap/bin/lib/cap-test-audit.cjs +1438 -0
  77. package/cap/bin/lib/cap-thread-migrator.cjs +307 -0
  78. package/cap/bin/lib/cap-thread-synthesis.cjs +545 -0
  79. package/cap/bin/lib/cap-thread-tracker.cjs +519 -0
  80. package/cap/bin/lib/cap-trace.cjs +399 -0
  81. package/cap/bin/lib/cap-trust-mode.cjs +336 -0
  82. package/cap/bin/lib/cap-ui-design-editor.cjs +642 -0
  83. package/cap/bin/lib/cap-ui-mind-map.cjs +712 -0
  84. package/cap/bin/lib/cap-ui-thread-nav.cjs +693 -0
  85. package/cap/bin/lib/cap-ui.cjs +1245 -0
  86. package/cap/bin/lib/cap-upgrade.cjs +1028 -0
  87. package/cap/bin/lib/cli/arg-helpers.cjs +49 -0
  88. package/cap/bin/lib/cli/frontmatter-router.cjs +31 -0
  89. package/cap/bin/lib/cli/init-router.cjs +68 -0
  90. package/cap/bin/lib/cli/phase-router.cjs +102 -0
  91. package/cap/bin/lib/cli/state-router.cjs +61 -0
  92. package/cap/bin/lib/cli/template-router.cjs +37 -0
  93. package/cap/bin/lib/cli/uat-router.cjs +29 -0
  94. package/cap/bin/lib/cli/validation-router.cjs +26 -0
  95. package/cap/bin/lib/cli/verification-router.cjs +31 -0
  96. package/cap/bin/lib/cli/workstream-router.cjs +39 -0
  97. package/cap/bin/lib/commands.cjs +961 -0
  98. package/cap/bin/lib/config.cjs +467 -0
  99. package/cap/bin/lib/convention-reader.cjs +258 -0
  100. package/cap/bin/lib/core.cjs +1241 -0
  101. package/cap/bin/lib/feature-aggregator.cjs +423 -0
  102. package/cap/bin/lib/frontmatter.cjs +337 -0
  103. package/cap/bin/lib/init.cjs +1443 -0
  104. package/cap/bin/lib/manifest-generator.cjs +383 -0
  105. package/cap/bin/lib/milestone.cjs +253 -0
  106. package/cap/bin/lib/model-profiles.cjs +69 -0
  107. package/cap/bin/lib/monorepo-context.cjs +226 -0
  108. package/cap/bin/lib/monorepo-migrator.cjs +509 -0
  109. package/cap/bin/lib/phase.cjs +889 -0
  110. package/cap/bin/lib/profile-output.cjs +989 -0
  111. package/cap/bin/lib/profile-pipeline.cjs +540 -0
  112. package/cap/bin/lib/roadmap.cjs +330 -0
  113. package/cap/bin/lib/security.cjs +394 -0
  114. package/cap/bin/lib/session-manager.cjs +292 -0
  115. package/cap/bin/lib/skeleton-generator.cjs +179 -0
  116. package/cap/bin/lib/state.cjs +1032 -0
  117. package/cap/bin/lib/template.cjs +231 -0
  118. package/cap/bin/lib/test-detector.cjs +62 -0
  119. package/cap/bin/lib/uat.cjs +283 -0
  120. package/cap/bin/lib/verify.cjs +889 -0
  121. package/cap/bin/lib/workspace-detector.cjs +371 -0
  122. package/cap/bin/lib/workstream.cjs +492 -0
  123. package/cap/commands/gsd/workstreams.md +63 -0
  124. package/cap/references/arc-standard.md +315 -0
  125. package/cap/references/cap-agent-architecture.md +101 -0
  126. package/cap/references/cap-gitignore-template +9 -0
  127. package/cap/references/cap-zero-deps.md +158 -0
  128. package/cap/references/checkpoints.md +778 -0
  129. package/cap/references/continuation-format.md +249 -0
  130. package/cap/references/contract-test-templates.md +312 -0
  131. package/cap/references/feature-map-template.md +25 -0
  132. package/cap/references/git-integration.md +295 -0
  133. package/cap/references/git-planning-commit.md +38 -0
  134. package/cap/references/model-profiles.md +174 -0
  135. package/cap/references/phase-numbering.md +126 -0
  136. package/cap/references/planning-config.md +202 -0
  137. package/cap/references/property-test-templates.md +316 -0
  138. package/cap/references/security-test-templates.md +347 -0
  139. package/cap/references/session-template.json +8 -0
  140. package/cap/references/tdd.md +263 -0
  141. package/cap/references/user-profiling.md +681 -0
  142. package/cap/references/verification-patterns.md +612 -0
  143. package/cap/templates/UAT.md +265 -0
  144. package/cap/templates/claude-md.md +175 -0
  145. package/cap/templates/codebase/architecture.md +255 -0
  146. package/cap/templates/codebase/concerns.md +310 -0
  147. package/cap/templates/codebase/conventions.md +307 -0
  148. package/cap/templates/codebase/integrations.md +280 -0
  149. package/cap/templates/codebase/stack.md +186 -0
  150. package/cap/templates/codebase/structure.md +285 -0
  151. package/cap/templates/codebase/testing.md +480 -0
  152. package/cap/templates/config.json +44 -0
  153. package/cap/templates/context.md +352 -0
  154. package/cap/templates/continue-here.md +78 -0
  155. package/cap/templates/copilot-instructions.md +7 -0
  156. package/cap/templates/debug-subagent-prompt.md +91 -0
  157. package/cap/templates/discussion-log.md +63 -0
  158. package/cap/templates/milestone-archive.md +123 -0
  159. package/cap/templates/milestone.md +115 -0
  160. package/cap/templates/phase-prompt.md +610 -0
  161. package/cap/templates/planner-subagent-prompt.md +117 -0
  162. package/cap/templates/project.md +186 -0
  163. package/cap/templates/requirements.md +231 -0
  164. package/cap/templates/research-project/ARCHITECTURE.md +204 -0
  165. package/cap/templates/research-project/FEATURES.md +147 -0
  166. package/cap/templates/research-project/PITFALLS.md +200 -0
  167. package/cap/templates/research-project/STACK.md +120 -0
  168. package/cap/templates/research-project/SUMMARY.md +170 -0
  169. package/cap/templates/research.md +552 -0
  170. package/cap/templates/roadmap.md +202 -0
  171. package/cap/templates/state.md +176 -0
  172. package/cap/templates/summary.md +364 -0
  173. package/cap/templates/user-preferences.md +498 -0
  174. package/cap/templates/verification-report.md +322 -0
  175. package/cap/workflows/add-phase.md +112 -0
  176. package/cap/workflows/add-tests.md +351 -0
  177. package/cap/workflows/add-todo.md +158 -0
  178. package/cap/workflows/audit-milestone.md +340 -0
  179. package/cap/workflows/audit-uat.md +109 -0
  180. package/cap/workflows/autonomous.md +891 -0
  181. package/cap/workflows/check-todos.md +177 -0
  182. package/cap/workflows/cleanup.md +152 -0
  183. package/cap/workflows/complete-milestone.md +767 -0
  184. package/cap/workflows/diagnose-issues.md +231 -0
  185. package/cap/workflows/discovery-phase.md +289 -0
  186. package/cap/workflows/discuss-phase-assumptions.md +653 -0
  187. package/cap/workflows/discuss-phase.md +1049 -0
  188. package/cap/workflows/do.md +104 -0
  189. package/cap/workflows/execute-phase.md +846 -0
  190. package/cap/workflows/execute-plan.md +514 -0
  191. package/cap/workflows/fast.md +105 -0
  192. package/cap/workflows/forensics.md +265 -0
  193. package/cap/workflows/health.md +181 -0
  194. package/cap/workflows/help.md +660 -0
  195. package/cap/workflows/insert-phase.md +130 -0
  196. package/cap/workflows/list-phase-assumptions.md +178 -0
  197. package/cap/workflows/list-workspaces.md +56 -0
  198. package/cap/workflows/manager.md +362 -0
  199. package/cap/workflows/map-codebase.md +377 -0
  200. package/cap/workflows/milestone-summary.md +223 -0
  201. package/cap/workflows/new-milestone.md +486 -0
  202. package/cap/workflows/new-project.md +1250 -0
  203. package/cap/workflows/new-workspace.md +237 -0
  204. package/cap/workflows/next.md +97 -0
  205. package/cap/workflows/node-repair.md +92 -0
  206. package/cap/workflows/note.md +156 -0
  207. package/cap/workflows/pause-work.md +176 -0
  208. package/cap/workflows/plan-milestone-gaps.md +273 -0
  209. package/cap/workflows/plan-phase.md +857 -0
  210. package/cap/workflows/plant-seed.md +169 -0
  211. package/cap/workflows/pr-branch.md +129 -0
  212. package/cap/workflows/profile-user.md +449 -0
  213. package/cap/workflows/progress.md +507 -0
  214. package/cap/workflows/quick.md +757 -0
  215. package/cap/workflows/remove-phase.md +155 -0
  216. package/cap/workflows/remove-workspace.md +90 -0
  217. package/cap/workflows/research-phase.md +82 -0
  218. package/cap/workflows/resume-project.md +326 -0
  219. package/cap/workflows/review.md +228 -0
  220. package/cap/workflows/session-report.md +146 -0
  221. package/cap/workflows/settings.md +283 -0
  222. package/cap/workflows/ship.md +228 -0
  223. package/cap/workflows/stats.md +60 -0
  224. package/cap/workflows/transition.md +671 -0
  225. package/cap/workflows/ui-phase.md +298 -0
  226. package/cap/workflows/ui-review.md +161 -0
  227. package/cap/workflows/update.md +323 -0
  228. package/cap/workflows/validate-phase.md +170 -0
  229. package/cap/workflows/verify-phase.md +254 -0
  230. package/cap/workflows/verify-work.md +637 -0
  231. package/commands/cap/annotate.md +165 -0
  232. package/commands/cap/brainstorm.md +393 -0
  233. package/commands/cap/checkpoint.md +106 -0
  234. package/commands/cap/completeness.md +94 -0
  235. package/commands/cap/continue.md +72 -0
  236. package/commands/cap/debug.md +588 -0
  237. package/commands/cap/deps.md +169 -0
  238. package/commands/cap/design.md +479 -0
  239. package/commands/cap/init.md +354 -0
  240. package/commands/cap/iterate.md +249 -0
  241. package/commands/cap/learn.md +459 -0
  242. package/commands/cap/memory.md +275 -0
  243. package/commands/cap/migrate-feature-map.md +91 -0
  244. package/commands/cap/migrate-memory.md +108 -0
  245. package/commands/cap/migrate-tags.md +91 -0
  246. package/commands/cap/migrate.md +131 -0
  247. package/commands/cap/prototype.md +510 -0
  248. package/commands/cap/reconcile.md +121 -0
  249. package/commands/cap/review.md +360 -0
  250. package/commands/cap/save.md +72 -0
  251. package/commands/cap/scan.md +404 -0
  252. package/commands/cap/start.md +356 -0
  253. package/commands/cap/status.md +118 -0
  254. package/commands/cap/test-audit.md +262 -0
  255. package/commands/cap/test.md +394 -0
  256. package/commands/cap/trace.md +133 -0
  257. package/commands/cap/ui.md +167 -0
  258. package/hooks/dist/cap-check-update.js +115 -0
  259. package/hooks/dist/cap-context-monitor.js +185 -0
  260. package/hooks/dist/cap-learn-review-hook.js +114 -0
  261. package/hooks/dist/cap-learning-hook.js +192 -0
  262. package/hooks/dist/cap-memory.js +299 -0
  263. package/hooks/dist/cap-prompt-guard.js +97 -0
  264. package/hooks/dist/cap-statusline.js +157 -0
  265. package/hooks/dist/cap-tag-observer.js +115 -0
  266. package/hooks/dist/cap-version-check.js +112 -0
  267. package/hooks/dist/cap-workflow-guard.js +175 -0
  268. package/hooks/hooks.json +55 -0
  269. package/package.json +58 -0
  270. package/scripts/base64-scan.sh +262 -0
  271. package/scripts/build-hooks.js +93 -0
  272. package/scripts/cap-removal-checklist.md +202 -0
  273. package/scripts/prompt-injection-scan.sh +199 -0
  274. package/scripts/run-tests.cjs +181 -0
  275. package/scripts/secret-scan.sh +227 -0
@@ -0,0 +1,400 @@
1
+ // @cap-feature(feature:F-035) Detect In-Session Topic Divergence During Brainstorm — monitor message-by-message topic drift and suggest branch/stay/redirect actions
2
+ // @cap-decision Pure logic module with explicit I/O in finalizeSession() only — same pattern as cap-thread-tracker.cjs and cap-thread-synthesis.cjs.
3
+ // @cap-decision Keyword decay uses a sliding window approach rather than numeric weights — simpler to reason about, zero floating-point drift, and adequate for brainstorm-length sessions.
4
+ // @cap-constraint Zero external dependencies — uses only Node.js built-ins (crypto).
5
+
6
+ 'use strict';
7
+
8
+ const crypto = require('node:crypto');
9
+
10
+ const {
11
+ extractKeywords,
12
+ computeKeywordOverlap,
13
+ branchThread,
14
+ persistThread,
15
+ loadIndex,
16
+ saveIndex,
17
+ addToIndex,
18
+ } = require('./cap-thread-tracker.cjs');
19
+
20
+ // --- Constants ---
21
+
22
+ /** Default minimum overlap ratio below which divergence is detected. */
23
+ const DEFAULT_DIVERGENCE_THRESHOLD = 0.15;
24
+
25
+ /** Number of recent messages whose keywords remain at full weight in the running set. */
26
+ const KEYWORD_DECAY_WINDOW = 5;
27
+
28
+ /** Number of consecutive below-threshold messages that signal gradual drift. */
29
+ const GRADUAL_DRIFT_WINDOW = 3;
30
+
31
+ // --- Types ---
32
+
33
+ /**
34
+ * @typedef {Object} DivergenceSession
35
+ * @property {string} id - Session ID (e.g., "dsess-a1b2c3d4")
36
+ * @property {string[]} baselineKeywords - Keywords from the initial thread/prompt
37
+ * @property {string[]} runningKeywords - Current merged keyword set (decayed)
38
+ * @property {Array<{text: string, keywords: string[], timestamp: string}>} messages - All processed messages
39
+ * @property {Array<{threadId: string, divergencePoint: number}>} branches - Branches created during this session
40
+ * @property {number} threshold - Divergence threshold (overlap ratio)
41
+ * @property {number[]} recentOverlaps - Last N overlap ratios for gradual drift detection
42
+ */
43
+
44
+ /**
45
+ * @typedef {Object} DivergenceResult
46
+ * @property {boolean} diverged - Whether divergence was detected
47
+ * @property {number} overlapRatio - Overlap ratio between message keywords and running keywords
48
+ * @property {'sudden'|'gradual'|'none'} divergenceType - Type of divergence detected
49
+ */
50
+
51
+ /**
52
+ * @typedef {Object} ProcessResult
53
+ * @property {boolean} diverged - Whether divergence was detected
54
+ * @property {number} overlapRatio - Overlap ratio for this message
55
+ * @property {string[]} newKeywords - Keywords extracted from the message
56
+ * @property {string|null} suggestion - Brief inline suggestion string, or null if no divergence
57
+ */
58
+
59
+ /**
60
+ * @typedef {Object} ActionSuggestion
61
+ * @property {'branch'|'stay'|'redirect'} type - Suggested action
62
+ * @property {string} message - Conversational inline suggestion
63
+ */
64
+
65
+ /**
66
+ * @typedef {Object} BranchResult
67
+ * @property {Object} branchedThread - The newly created branch thread
68
+ * @property {DivergenceSession} updatedSession - Session updated with branch tracking
69
+ */
70
+
71
+ /**
72
+ * @typedef {Object} FinalizeResult
73
+ * @property {number} threadsSaved - Number of threads persisted
74
+ * @property {boolean} indexUpdated - Whether the thread index was updated
75
+ */
76
+
77
+ // --- Session ID Generation ---
78
+
79
+ /**
80
+ * Generate a unique divergence session ID.
81
+ * @returns {string} Session ID in format "dsess-{8 hex chars}"
82
+ */
83
+ function generateSessionId() {
84
+ return 'dsess-' + crypto.randomBytes(4).toString('hex');
85
+ }
86
+
87
+ // --- Session Creation ---
88
+
89
+ // @cap-todo(ac:F-035/AC-4) Initialize divergence tracking session with baseline keywords and running keyword set
90
+
91
+ /**
92
+ * Create a new divergence tracking session.
93
+ * Accepts either a thread object (with keywords) or a raw prompt string.
94
+ * @param {Object|string} threadOrPrompt - Thread object with .keywords, or a prompt string
95
+ * @param {Object} [options]
96
+ * @param {number} [options.threshold] - Override default divergence threshold
97
+ * @returns {DivergenceSession}
98
+ */
99
+ function createSession(threadOrPrompt, options = {}) {
100
+ const threshold = options.threshold != null ? options.threshold : DEFAULT_DIVERGENCE_THRESHOLD;
101
+
102
+ let baselineKeywords;
103
+ if (typeof threadOrPrompt === 'string') {
104
+ baselineKeywords = extractKeywords(threadOrPrompt);
105
+ } else if (threadOrPrompt && Array.isArray(threadOrPrompt.keywords)) {
106
+ baselineKeywords = [...threadOrPrompt.keywords];
107
+ } else {
108
+ baselineKeywords = [];
109
+ }
110
+
111
+ return {
112
+ id: generateSessionId(),
113
+ baselineKeywords,
114
+ runningKeywords: [...baselineKeywords],
115
+ messages: [],
116
+ branches: [],
117
+ threshold,
118
+ recentOverlaps: [],
119
+ };
120
+ }
121
+
122
+ // --- Keyword Decay ---
123
+
124
+ // @cap-todo(ac:F-035/AC-4) Maintain running keyword set with decay — keywords not seen in recent messages lose weight
125
+ // @cap-decision Decay uses set membership in a sliding window rather than numeric weights. Keywords that appear in any of the last N messages stay in the running set. This avoids floating-point accumulation and is deterministic.
126
+
127
+ /**
128
+ * Update the running keyword set by merging new keywords and decaying old ones.
129
+ * Keywords are kept if they appear in any message within the decay window.
130
+ * @param {Array<{keywords: string[]}>} messages - All session messages (used for decay window)
131
+ * @param {string[]} newKeywords - Keywords from the latest message
132
+ * @param {string[]} baselineKeywords - Original baseline keywords (always retained)
133
+ * @param {number} [decayWindow] - Number of recent messages to consider
134
+ * @returns {string[]} Updated running keyword set
135
+ */
136
+ function updateRunningKeywords(messages, newKeywords, baselineKeywords, decayWindow = KEYWORD_DECAY_WINDOW) {
137
+ // Baseline keywords are always retained
138
+ const retained = new Set(baselineKeywords);
139
+
140
+ // Keywords from recent messages within the decay window are retained
141
+ const windowStart = Math.max(0, messages.length - decayWindow);
142
+ for (let i = windowStart; i < messages.length; i++) {
143
+ for (const kw of messages[i].keywords) {
144
+ retained.add(kw);
145
+ }
146
+ }
147
+
148
+ // Add new keywords
149
+ for (const kw of newKeywords) {
150
+ retained.add(kw);
151
+ }
152
+
153
+ return [...retained].sort();
154
+ }
155
+
156
+ // --- Divergence Detection ---
157
+
158
+ // @cap-todo(ac:F-035/AC-1) Compare each new message against running keywords and detect when overlap drops below threshold
159
+
160
+ /**
161
+ * Core divergence detection: compare message keywords against the session's running keywords.
162
+ * Detects both sudden drops and gradual drift.
163
+ * @param {DivergenceSession} session - Current session state
164
+ * @param {string[]} messageKeywords - Keywords extracted from the new message
165
+ * @returns {DivergenceResult}
166
+ */
167
+ function detectDivergence(session, messageKeywords) {
168
+ if (messageKeywords.length === 0) {
169
+ return { diverged: false, overlapRatio: 0, divergenceType: 'none' };
170
+ }
171
+
172
+ if (session.runningKeywords.length === 0) {
173
+ return { diverged: false, overlapRatio: 0, divergenceType: 'none' };
174
+ }
175
+
176
+ const { overlapRatio } = computeKeywordOverlap(messageKeywords, session.runningKeywords);
177
+
178
+ // Check for sudden divergence: single message drops below threshold
179
+ const suddenDivergence = overlapRatio < session.threshold;
180
+
181
+ // Check for gradual drift: last N overlaps all below threshold
182
+ const recentWithCurrent = [...session.recentOverlaps, overlapRatio];
183
+ const windowForGradual = recentWithCurrent.slice(-GRADUAL_DRIFT_WINDOW);
184
+ const gradualDrift = windowForGradual.length >= GRADUAL_DRIFT_WINDOW &&
185
+ windowForGradual.every(r => r < session.threshold);
186
+
187
+ if (suddenDivergence && !gradualDrift) {
188
+ return { diverged: true, overlapRatio, divergenceType: 'sudden' };
189
+ }
190
+
191
+ if (gradualDrift) {
192
+ return { diverged: true, overlapRatio, divergenceType: 'gradual' };
193
+ }
194
+
195
+ return { diverged: false, overlapRatio, divergenceType: 'none' };
196
+ }
197
+
198
+ // --- Message Processing ---
199
+
200
+ // @cap-todo(ac:F-035/AC-1) Process each user message: extract keywords, detect divergence, update running set
201
+ // @cap-todo(ac:F-035/AC-4) Track topic evolution by updating running keyword set with each message
202
+
203
+ /**
204
+ * Process a new user message within a divergence tracking session.
205
+ * Extracts keywords, checks for divergence, updates running keywords.
206
+ * Returns divergence status and an optional inline suggestion.
207
+ * @param {DivergenceSession} session - Current session (mutated in place)
208
+ * @param {string} userMessage - The user's message text
209
+ * @returns {ProcessResult}
210
+ */
211
+ function processMessage(session, userMessage) {
212
+ const newKeywords = extractKeywords(userMessage);
213
+
214
+ // Detect divergence BEFORE updating running keywords (compare against current state)
215
+ const divergenceResult = detectDivergence(session, newKeywords);
216
+
217
+ // Record the message
218
+ session.messages.push({
219
+ text: userMessage,
220
+ keywords: newKeywords,
221
+ timestamp: new Date().toISOString(),
222
+ });
223
+
224
+ // Update running keywords with decay
225
+ session.runningKeywords = updateRunningKeywords(
226
+ session.messages,
227
+ newKeywords,
228
+ session.baselineKeywords
229
+ );
230
+
231
+ // Track recent overlaps for gradual drift detection
232
+ session.recentOverlaps.push(divergenceResult.overlapRatio);
233
+
234
+ // Generate suggestion if divergence detected
235
+ let suggestion = null;
236
+ if (divergenceResult.diverged) {
237
+ const actionSuggestion = suggestAction(session, divergenceResult);
238
+ suggestion = actionSuggestion.message;
239
+ }
240
+
241
+ return {
242
+ diverged: divergenceResult.diverged,
243
+ overlapRatio: divergenceResult.overlapRatio,
244
+ newKeywords,
245
+ suggestion,
246
+ };
247
+ }
248
+
249
+ // --- Action Suggestion ---
250
+
251
+ // @cap-todo(ac:F-035/AC-2) When divergence detected, suggest branch/stay/redirect as brief inline suggestion
252
+ // @cap-todo(ac:F-035/AC-7) Divergence detection shall not interrupt conversation flow — present as brief inline suggestion
253
+
254
+ /**
255
+ * Generate a brief, non-blocking action suggestion based on divergence.
256
+ * @param {DivergenceSession} session - Current session state
257
+ * @param {DivergenceResult} divergenceResult - Result from detectDivergence
258
+ * @returns {ActionSuggestion}
259
+ */
260
+ function suggestAction(session, divergenceResult) {
261
+ // @cap-decision Default suggestion is 'branch' for sudden shifts, 'redirect' for gradual drift. The brainstormer agent presents this inline and lets the user decide.
262
+
263
+ if (divergenceResult.divergenceType === 'gradual') {
264
+ return {
265
+ type: 'redirect',
266
+ message: '[Topic drift detected] The conversation has been gradually shifting. Would you like to: (1) branch into this new direction, (2) refocus on the original topic, or (3) continue as-is?',
267
+ };
268
+ }
269
+
270
+ // Sudden divergence — suggest branching
271
+ return {
272
+ type: 'branch',
273
+ message: '[New topic detected] This seems like a different direction. Would you like to: (1) branch into a new thread for this topic, (2) stay on the current topic, or (3) redirect the current thread?',
274
+ };
275
+ }
276
+
277
+ // --- Branch Execution ---
278
+
279
+ // @cap-todo(ac:F-035/AC-3) If branch chosen, call branchThread() with divergence point set to last message before topic shift
280
+ // @cap-todo(ac:F-035/AC-5) After branch creation, continue brainstorm on new branch while preserving parent thread state
281
+
282
+ /**
283
+ * Execute a branch based on detected divergence.
284
+ * Creates a new branch thread from the parent, sets the divergence point
285
+ * to the last message before the topic shift.
286
+ * @param {string} cwd - Project root path
287
+ * @param {DivergenceSession} session - Current session (mutated to track branch)
288
+ * @param {Object} parentThread - The parent thread object (from cap-thread-tracker)
289
+ * @param {string} newPrompt - The divergent message/prompt that triggered the branch
290
+ * @returns {BranchResult}
291
+ */
292
+ function executeBranch(cwd, session, parentThread, newPrompt) {
293
+ // Divergence point is the last message before the shift
294
+ const messageCount = session.messages.length;
295
+ const divergencePointIndex = Math.max(0, messageCount - 1);
296
+ const lastMessageBeforeShift = messageCount >= 2
297
+ ? session.messages[messageCount - 2].text
298
+ : parentThread.problemStatement || 'Session start';
299
+
300
+ const divergencePointDesc = `Message ${divergencePointIndex}: "${truncate(lastMessageBeforeShift, 80)}"`;
301
+
302
+ // Create the branch thread via thread-tracker
303
+ const branchedThread = branchThread(parentThread, {
304
+ problemStatement: newPrompt,
305
+ divergencePoint: divergencePointDesc,
306
+ });
307
+
308
+ // Track the branch in the session
309
+ session.branches.push({
310
+ threadId: branchedThread.id,
311
+ divergencePoint: divergencePointIndex,
312
+ });
313
+
314
+ // Persist the branch thread and update the index
315
+ persistThread(cwd, branchedThread);
316
+
317
+ return {
318
+ branchedThread,
319
+ updatedSession: session,
320
+ };
321
+ }
322
+
323
+ // --- Session Finalization ---
324
+
325
+ // @cap-todo(ac:F-035/AC-6) At brainstorm end, persist all threads (parent + branches) and update thread index with branch relationships
326
+
327
+ /**
328
+ * Finalize a divergence session: persist the parent thread and all branches,
329
+ * update the thread index with branch relationships.
330
+ * @param {string} cwd - Project root path
331
+ * @param {DivergenceSession} session - The completed session
332
+ * @param {Object} parentThread - The parent thread to persist
333
+ * @returns {FinalizeResult}
334
+ */
335
+ function finalizeSession(cwd, session, parentThread) {
336
+ let threadsSaved = 0;
337
+
338
+ // Persist the parent thread
339
+ persistThread(cwd, parentThread);
340
+ threadsSaved++;
341
+
342
+ // Load the current index to ensure branch relationships are recorded
343
+ const index = loadIndex(cwd);
344
+
345
+ // All branch threads were already persisted in executeBranch(),
346
+ // but we ensure the index reflects the complete branch tree
347
+ for (const branch of session.branches) {
348
+ // Verify branch is in the index (executeBranch already added it, but be defensive)
349
+ const inIndex = index.threads.some(t => t.id === branch.threadId);
350
+ if (!inIndex) {
351
+ // This shouldn't happen normally, but handle gracefully
352
+ // @cap-risk Branch thread missing from index after executeBranch — defensive re-add
353
+ threadsSaved++;
354
+ }
355
+ }
356
+
357
+ // Save the final index state
358
+ saveIndex(cwd, index);
359
+
360
+ return {
361
+ threadsSaved,
362
+ indexUpdated: true,
363
+ };
364
+ }
365
+
366
+ // --- Helpers ---
367
+
368
+ /**
369
+ * Truncate a string at a word boundary.
370
+ * @param {string} text
371
+ * @param {number} maxLen
372
+ * @returns {string}
373
+ */
374
+ function truncate(text, maxLen) {
375
+ if (!text || text.length <= maxLen) return text || '';
376
+ const trimmed = text.substring(0, maxLen).trim();
377
+ const lastSpace = trimmed.lastIndexOf(' ');
378
+ if (lastSpace > maxLen * 0.4) return trimmed.substring(0, lastSpace) + '...';
379
+ return trimmed + '...';
380
+ }
381
+
382
+ module.exports = {
383
+ // Core
384
+ createSession,
385
+ processMessage,
386
+ detectDivergence,
387
+ suggestAction,
388
+ executeBranch,
389
+ finalizeSession,
390
+ updateRunningKeywords,
391
+
392
+ // Constants (exposed for testing and configuration)
393
+ DEFAULT_DIVERGENCE_THRESHOLD,
394
+ KEYWORD_DECAY_WINDOW,
395
+ GRADUAL_DRIFT_WINDOW,
396
+
397
+ // Internal (for testing)
398
+ generateSessionId,
399
+ truncate,
400
+ };