@pcircle/footprint 1.5.0 → 1.7.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 +301 -132
  2. package/SKILL.md +72 -28
  3. package/bin/footprint.js +16 -0
  4. package/dist/src/adapters/claude.d.ts +2 -0
  5. package/dist/src/adapters/claude.d.ts.map +1 -0
  6. package/dist/src/adapters/claude.js +7 -0
  7. package/dist/src/adapters/claude.js.map +1 -0
  8. package/dist/src/adapters/codex.d.ts +2 -0
  9. package/dist/src/adapters/codex.d.ts.map +1 -0
  10. package/dist/src/adapters/codex.js +7 -0
  11. package/dist/src/adapters/codex.js.map +1 -0
  12. package/dist/src/adapters/gemini.d.ts +2 -0
  13. package/dist/src/adapters/gemini.d.ts.map +1 -0
  14. package/dist/src/adapters/gemini.js +7 -0
  15. package/dist/src/adapters/gemini.js.map +1 -0
  16. package/dist/src/adapters/index.d.ts +5 -0
  17. package/dist/src/adapters/index.d.ts.map +1 -0
  18. package/dist/src/adapters/index.js +12 -0
  19. package/dist/src/adapters/index.js.map +1 -0
  20. package/dist/src/adapters/structured-prefix.d.ts +10 -0
  21. package/dist/src/adapters/structured-prefix.d.ts.map +1 -0
  22. package/dist/src/adapters/structured-prefix.js +59 -0
  23. package/dist/src/adapters/structured-prefix.js.map +1 -0
  24. package/dist/src/adapters/types.d.ts +32 -0
  25. package/dist/src/adapters/types.d.ts.map +1 -0
  26. package/dist/src/adapters/types.js +2 -0
  27. package/dist/src/adapters/types.js.map +1 -0
  28. package/dist/src/cli/context-flow.d.ts +92 -0
  29. package/dist/src/cli/context-flow.d.ts.map +1 -0
  30. package/dist/src/cli/context-flow.js +724 -0
  31. package/dist/src/cli/context-flow.js.map +1 -0
  32. package/dist/src/cli/history-display.d.ts +27 -0
  33. package/dist/src/cli/history-display.d.ts.map +1 -0
  34. package/dist/src/cli/history-display.js +167 -0
  35. package/dist/src/cli/history-display.js.map +1 -0
  36. package/dist/src/cli/index.js +924 -0
  37. package/dist/src/cli/index.js.map +1 -1
  38. package/dist/src/cli/launch-spec.d.ts +31 -0
  39. package/dist/src/cli/launch-spec.d.ts.map +1 -0
  40. package/dist/src/cli/launch-spec.js +182 -0
  41. package/dist/src/cli/launch-spec.js.map +1 -0
  42. package/dist/src/cli/live-demo.d.ts +34 -0
  43. package/dist/src/cli/live-demo.d.ts.map +1 -0
  44. package/dist/src/cli/live-demo.js +254 -0
  45. package/dist/src/cli/live-demo.js.map +1 -0
  46. package/dist/src/cli/pty-transcript.d.ts +34 -0
  47. package/dist/src/cli/pty-transcript.d.ts.map +1 -0
  48. package/dist/src/cli/pty-transcript.js +174 -0
  49. package/dist/src/cli/pty-transcript.js.map +1 -0
  50. package/dist/src/cli/session-display.d.ts +74 -0
  51. package/dist/src/cli/session-display.d.ts.map +1 -0
  52. package/dist/src/cli/session-display.js +922 -0
  53. package/dist/src/cli/session-display.js.map +1 -0
  54. package/dist/src/cli/session-execution.d.ts +55 -0
  55. package/dist/src/cli/session-execution.d.ts.map +1 -0
  56. package/dist/src/cli/session-execution.js +817 -0
  57. package/dist/src/cli/session-execution.js.map +1 -0
  58. package/dist/src/cli/session-runtime.d.ts +5 -0
  59. package/dist/src/cli/session-runtime.d.ts.map +1 -0
  60. package/dist/src/cli/session-runtime.js +11 -0
  61. package/dist/src/cli/session-runtime.js.map +1 -0
  62. package/dist/src/cli/setup.d.ts.map +1 -1
  63. package/dist/src/cli/setup.js +2 -0
  64. package/dist/src/cli/setup.js.map +1 -1
  65. package/dist/src/index.d.ts +4 -0
  66. package/dist/src/index.d.ts.map +1 -1
  67. package/dist/src/index.js +148 -7
  68. package/dist/src/index.js.map +1 -1
  69. package/dist/src/ingestion/deterministic.d.ts +3 -0
  70. package/dist/src/ingestion/deterministic.d.ts.map +1 -0
  71. package/dist/src/ingestion/deterministic.js +862 -0
  72. package/dist/src/ingestion/deterministic.js.map +1 -0
  73. package/dist/src/ingestion/index.d.ts +5 -0
  74. package/dist/src/ingestion/index.d.ts.map +1 -0
  75. package/dist/src/ingestion/index.js +27 -0
  76. package/dist/src/ingestion/index.js.map +1 -0
  77. package/dist/src/ingestion/semantic.d.ts +6 -0
  78. package/dist/src/ingestion/semantic.d.ts.map +1 -0
  79. package/dist/src/ingestion/semantic.js +627 -0
  80. package/dist/src/ingestion/semantic.js.map +1 -0
  81. package/dist/src/ingestion/types.d.ts +10 -0
  82. package/dist/src/ingestion/types.d.ts.map +1 -0
  83. package/dist/src/ingestion/types.js +2 -0
  84. package/dist/src/ingestion/types.js.map +1 -0
  85. package/dist/src/lib/context-memory.d.ts +140 -0
  86. package/dist/src/lib/context-memory.d.ts.map +1 -0
  87. package/dist/src/lib/context-memory.js +974 -0
  88. package/dist/src/lib/context-memory.js.map +1 -0
  89. package/dist/src/lib/history-handoff.d.ts +43 -0
  90. package/dist/src/lib/history-handoff.d.ts.map +1 -0
  91. package/dist/src/lib/history-handoff.js +179 -0
  92. package/dist/src/lib/history-handoff.js.map +1 -0
  93. package/dist/src/lib/observability.d.ts +3 -0
  94. package/dist/src/lib/observability.d.ts.map +1 -0
  95. package/dist/src/lib/observability.js +63 -0
  96. package/dist/src/lib/observability.js.map +1 -0
  97. package/dist/src/lib/session-artifacts.d.ts +51 -0
  98. package/dist/src/lib/session-artifacts.d.ts.map +1 -0
  99. package/dist/src/lib/session-artifacts.js +132 -0
  100. package/dist/src/lib/session-artifacts.js.map +1 -0
  101. package/dist/src/lib/session-filters.d.ts +11 -0
  102. package/dist/src/lib/session-filters.d.ts.map +1 -0
  103. package/dist/src/lib/session-filters.js +16 -0
  104. package/dist/src/lib/session-filters.js.map +1 -0
  105. package/dist/src/lib/session-history.d.ts +50 -0
  106. package/dist/src/lib/session-history.d.ts.map +1 -0
  107. package/dist/src/lib/session-history.js +73 -0
  108. package/dist/src/lib/session-history.js.map +1 -0
  109. package/dist/src/lib/session-trends.d.ts +129 -0
  110. package/dist/src/lib/session-trends.d.ts.map +1 -0
  111. package/dist/src/lib/session-trends.js +361 -0
  112. package/dist/src/lib/session-trends.js.map +1 -0
  113. package/dist/src/lib/storage/database.d.ts +212 -1
  114. package/dist/src/lib/storage/database.d.ts.map +1 -1
  115. package/dist/src/lib/storage/database.js +1694 -114
  116. package/dist/src/lib/storage/database.js.map +1 -1
  117. package/dist/src/lib/storage/export-sessions.d.ts +33 -0
  118. package/dist/src/lib/storage/export-sessions.d.ts.map +1 -0
  119. package/dist/src/lib/storage/export-sessions.js +525 -0
  120. package/dist/src/lib/storage/export-sessions.js.map +1 -0
  121. package/dist/src/lib/storage/index.d.ts +7 -6
  122. package/dist/src/lib/storage/index.d.ts.map +1 -1
  123. package/dist/src/lib/storage/index.js +6 -5
  124. package/dist/src/lib/storage/index.js.map +1 -1
  125. package/dist/src/lib/storage/schema.d.ts +6 -1
  126. package/dist/src/lib/storage/schema.d.ts.map +1 -1
  127. package/dist/src/lib/storage/schema.js +337 -2
  128. package/dist/src/lib/storage/schema.js.map +1 -1
  129. package/dist/src/lib/storage/types.d.ts +122 -0
  130. package/dist/src/lib/storage/types.d.ts.map +1 -1
  131. package/dist/src/prompts/skill-prompt.d.ts.map +1 -1
  132. package/dist/src/prompts/skill-prompt.js +13 -0
  133. package/dist/src/prompts/skill-prompt.js.map +1 -1
  134. package/dist/src/tools/confirm-context-link.d.ts +62 -0
  135. package/dist/src/tools/confirm-context-link.d.ts.map +1 -0
  136. package/dist/src/tools/confirm-context-link.js +36 -0
  137. package/dist/src/tools/confirm-context-link.js.map +1 -0
  138. package/dist/src/tools/context-schemas.d.ts +694 -0
  139. package/dist/src/tools/context-schemas.d.ts.map +1 -0
  140. package/dist/src/tools/context-schemas.js +171 -0
  141. package/dist/src/tools/context-schemas.js.map +1 -0
  142. package/dist/src/tools/export-sessions.d.ts +111 -0
  143. package/dist/src/tools/export-sessions.d.ts.map +1 -0
  144. package/dist/src/tools/export-sessions.js +136 -0
  145. package/dist/src/tools/export-sessions.js.map +1 -0
  146. package/dist/src/tools/get-context.d.ts +208 -0
  147. package/dist/src/tools/get-context.d.ts.map +1 -0
  148. package/dist/src/tools/get-context.js +27 -0
  149. package/dist/src/tools/get-context.js.map +1 -0
  150. package/dist/src/tools/get-history-handoff.d.ts +109 -0
  151. package/dist/src/tools/get-history-handoff.d.ts.map +1 -0
  152. package/dist/src/tools/get-history-handoff.js +85 -0
  153. package/dist/src/tools/get-history-handoff.js.map +1 -0
  154. package/dist/src/tools/get-history-trends.d.ts +155 -0
  155. package/dist/src/tools/get-history-trends.d.ts.map +1 -0
  156. package/dist/src/tools/get-history-trends.js +123 -0
  157. package/dist/src/tools/get-history-trends.js.map +1 -0
  158. package/dist/src/tools/get-session-artifacts.d.ts +151 -0
  159. package/dist/src/tools/get-session-artifacts.d.ts.map +1 -0
  160. package/dist/src/tools/get-session-artifacts.js +184 -0
  161. package/dist/src/tools/get-session-artifacts.js.map +1 -0
  162. package/dist/src/tools/get-session-decisions.d.ts +69 -0
  163. package/dist/src/tools/get-session-decisions.d.ts.map +1 -0
  164. package/dist/src/tools/get-session-decisions.js +99 -0
  165. package/dist/src/tools/get-session-decisions.js.map +1 -0
  166. package/dist/src/tools/get-session-messages.d.ts +55 -0
  167. package/dist/src/tools/get-session-messages.d.ts.map +1 -0
  168. package/dist/src/tools/get-session-messages.js +89 -0
  169. package/dist/src/tools/get-session-messages.js.map +1 -0
  170. package/dist/src/tools/get-session-narrative.d.ts +72 -0
  171. package/dist/src/tools/get-session-narrative.d.ts.map +1 -0
  172. package/dist/src/tools/get-session-narrative.js +106 -0
  173. package/dist/src/tools/get-session-narrative.js.map +1 -0
  174. package/dist/src/tools/get-session-timeline.d.ts +55 -0
  175. package/dist/src/tools/get-session-timeline.d.ts.map +1 -0
  176. package/dist/src/tools/get-session-timeline.js +93 -0
  177. package/dist/src/tools/get-session-timeline.js.map +1 -0
  178. package/dist/src/tools/get-session-trends.d.ts +108 -0
  179. package/dist/src/tools/get-session-trends.d.ts.map +1 -0
  180. package/dist/src/tools/get-session-trends.js +130 -0
  181. package/dist/src/tools/get-session-trends.js.map +1 -0
  182. package/dist/src/tools/get-session.d.ts +251 -0
  183. package/dist/src/tools/get-session.d.ts.map +1 -0
  184. package/dist/src/tools/get-session.js +290 -0
  185. package/dist/src/tools/get-session.js.map +1 -0
  186. package/dist/src/tools/index.d.ts +22 -0
  187. package/dist/src/tools/index.d.ts.map +1 -1
  188. package/dist/src/tools/index.js +22 -0
  189. package/dist/src/tools/index.js.map +1 -1
  190. package/dist/src/tools/list-contexts.d.ts +50 -0
  191. package/dist/src/tools/list-contexts.d.ts.map +1 -0
  192. package/dist/src/tools/list-contexts.js +28 -0
  193. package/dist/src/tools/list-contexts.js.map +1 -0
  194. package/dist/src/tools/list-sessions.d.ts +86 -0
  195. package/dist/src/tools/list-sessions.d.ts.map +1 -0
  196. package/dist/src/tools/list-sessions.js +97 -0
  197. package/dist/src/tools/list-sessions.js.map +1 -0
  198. package/dist/src/tools/merge-contexts.d.ts +58 -0
  199. package/dist/src/tools/merge-contexts.d.ts.map +1 -0
  200. package/dist/src/tools/merge-contexts.js +27 -0
  201. package/dist/src/tools/merge-contexts.js.map +1 -0
  202. package/dist/src/tools/move-session-context.d.ts +62 -0
  203. package/dist/src/tools/move-session-context.d.ts.map +1 -0
  204. package/dist/src/tools/move-session-context.js +33 -0
  205. package/dist/src/tools/move-session-context.js.map +1 -0
  206. package/dist/src/tools/reingest-session.d.ts +31 -0
  207. package/dist/src/tools/reingest-session.d.ts.map +1 -0
  208. package/dist/src/tools/reingest-session.js +43 -0
  209. package/dist/src/tools/reingest-session.js.map +1 -0
  210. package/dist/src/tools/reject-context-link.d.ts +58 -0
  211. package/dist/src/tools/reject-context-link.d.ts.map +1 -0
  212. package/dist/src/tools/reject-context-link.js +26 -0
  213. package/dist/src/tools/reject-context-link.js.map +1 -0
  214. package/dist/src/tools/resolve-context.d.ts +287 -0
  215. package/dist/src/tools/resolve-context.d.ts.map +1 -0
  216. package/dist/src/tools/resolve-context.js +35 -0
  217. package/dist/src/tools/resolve-context.js.map +1 -0
  218. package/dist/src/tools/search-history.d.ts +86 -0
  219. package/dist/src/tools/search-history.d.ts.map +1 -0
  220. package/dist/src/tools/search-history.js +103 -0
  221. package/dist/src/tools/search-history.js.map +1 -0
  222. package/dist/src/tools/session-ui-metadata.d.ts +15 -0
  223. package/dist/src/tools/session-ui-metadata.d.ts.map +1 -0
  224. package/dist/src/tools/session-ui-metadata.js +15 -0
  225. package/dist/src/tools/session-ui-metadata.js.map +1 -0
  226. package/dist/src/tools/set-active-context.d.ts +58 -0
  227. package/dist/src/tools/set-active-context.d.ts.map +1 -0
  228. package/dist/src/tools/set-active-context.js +26 -0
  229. package/dist/src/tools/set-active-context.js.map +1 -0
  230. package/dist/src/tools/split-context.d.ts +62 -0
  231. package/dist/src/tools/split-context.d.ts.map +1 -0
  232. package/dist/src/tools/split-context.js +36 -0
  233. package/dist/src/tools/split-context.js.map +1 -0
  234. package/dist/src/tools/verify-footprint.js +1 -1
  235. package/dist/src/tools/verify-footprint.js.map +1 -1
  236. package/dist/src/types.d.ts +2 -0
  237. package/dist/src/types.d.ts.map +1 -1
  238. package/dist/src/ui/register.d.ts +6 -1
  239. package/dist/src/ui/register.d.ts.map +1 -1
  240. package/dist/src/ui/register.js +60 -16
  241. package/dist/src/ui/register.js.map +1 -1
  242. package/dist/ui/dashboard.html +239 -868
  243. package/dist/ui/detail.html +107 -248
  244. package/dist/ui/export.html +115 -298
  245. package/dist/ui/session-dashboard-live.html +264 -0
  246. package/dist/ui/session-dashboard.html +329 -0
  247. package/dist/ui/session-detail-live.html +336 -0
  248. package/dist/ui/session-detail.html +355 -0
  249. package/package.json +34 -9
@@ -0,0 +1,974 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { buildHistoryHandoffReport, } from "./history-handoff.js";
5
+ import { buildHistoryTrendReport, } from "./session-trends.js";
6
+ import { getSessionLabel, truncateSummary } from "./session-history.js";
7
+ import { parseArtifactMetadata } from "./session-artifacts.js";
8
+ const CONTEXT_STOPWORDS = new Set([
9
+ "a",
10
+ "an",
11
+ "and",
12
+ "are",
13
+ "as",
14
+ "at",
15
+ "be",
16
+ "because",
17
+ "browser",
18
+ "build",
19
+ "change",
20
+ "changes",
21
+ "check",
22
+ "context",
23
+ "continue",
24
+ "coverage",
25
+ "decision",
26
+ "feature",
27
+ "fix",
28
+ "for",
29
+ "from",
30
+ "handoff",
31
+ "implement",
32
+ "in",
33
+ "into",
34
+ "is",
35
+ "it",
36
+ "its",
37
+ "latest",
38
+ "memory",
39
+ "new",
40
+ "of",
41
+ "on",
42
+ "or",
43
+ "project",
44
+ "review",
45
+ "scope",
46
+ "session",
47
+ "ship",
48
+ "should",
49
+ "status",
50
+ "story",
51
+ "task",
52
+ "tests",
53
+ "that",
54
+ "the",
55
+ "this",
56
+ "to",
57
+ "update",
58
+ "use",
59
+ "we",
60
+ "with",
61
+ "work",
62
+ ]);
63
+ function hashText(value) {
64
+ return createHash("sha1").update(value).digest("hex").slice(0, 12);
65
+ }
66
+ function normalizeWorkspaceKey(value) {
67
+ const resolved = path.resolve(value.trim());
68
+ try {
69
+ return fs.realpathSync.native(resolved);
70
+ }
71
+ catch {
72
+ return resolved;
73
+ }
74
+ }
75
+ function normalizeWorkspaceFromSession(session) {
76
+ return normalizeWorkspaceKey(session.projectRoot?.trim() || session.cwd.trim());
77
+ }
78
+ function tokenize(value) {
79
+ if (!value) {
80
+ return new Set();
81
+ }
82
+ return new Set(value
83
+ .toLowerCase()
84
+ .split(/[^a-z0-9@/_-]+/i)
85
+ .map((token) => token.trim())
86
+ .filter((token) => token.length >= 3 &&
87
+ !CONTEXT_STOPWORDS.has(token) &&
88
+ !/^\d+$/.test(token)));
89
+ }
90
+ function countOverlap(left, right) {
91
+ let overlap = 0;
92
+ for (const value of left) {
93
+ if (right.has(value)) {
94
+ overlap += 1;
95
+ }
96
+ }
97
+ return overlap;
98
+ }
99
+ function incrementCounts(map, values) {
100
+ for (const value of values) {
101
+ map.set(value, (map.get(value) ?? 0) + 1);
102
+ }
103
+ }
104
+ function parseWorkspacePath(value) {
105
+ return normalizeWorkspaceKey(value);
106
+ }
107
+ function isSameWorkspace(target, candidate) {
108
+ return (target === candidate ||
109
+ target.startsWith(`${candidate}${path.sep}`) ||
110
+ candidate.startsWith(`${target}${path.sep}`));
111
+ }
112
+ function collectSessionSignals(db, sessions) {
113
+ const attemptsBySession = new Map();
114
+ for (const attempt of db.querySessionTrendAttempts({
115
+ sessionIds: sessions.map((session) => session.id),
116
+ })) {
117
+ const group = attemptsBySession.get(attempt.sessionId) ?? [];
118
+ group.push({
119
+ issueKey: attempt.issueKey,
120
+ issueLabel: attempt.issueLabel,
121
+ issueFamilyKey: attempt.issueFamilyKey,
122
+ issueFamilyLabel: attempt.issueFamilyLabel,
123
+ });
124
+ attemptsBySession.set(attempt.sessionId, group);
125
+ }
126
+ return sessions.map((session) => {
127
+ const attempts = attemptsBySession.get(session.id) ?? [];
128
+ const artifactLabels = [];
129
+ const artifactFamilyLabels = [];
130
+ const issueKeys = new Set(attempts.map((attempt) => attempt.issueKey));
131
+ const issueFamilies = new Set(attempts
132
+ .map((attempt) => attempt.issueFamilyKey)
133
+ .filter((value) => Boolean(value)));
134
+ for (const artifact of db.getSessionArtifacts(session.id)) {
135
+ const metadata = parseArtifactMetadata(artifact.metadata);
136
+ if (metadata.issueKey) {
137
+ issueKeys.add(metadata.issueKey);
138
+ artifactLabels.push(metadata.issueLabel ?? metadata.issueKey);
139
+ }
140
+ if (metadata.issueFamilyKey) {
141
+ issueFamilies.add(metadata.issueFamilyKey);
142
+ artifactFamilyLabels.push(metadata.issueFamilyLabel ?? metadata.issueFamilyKey);
143
+ }
144
+ }
145
+ return {
146
+ session,
147
+ workspaceKey: normalizeWorkspaceFromSession(session),
148
+ titleTokens: tokenize(session.title ?? ""),
149
+ issueKeys,
150
+ issueFamilies,
151
+ issueLabels: [
152
+ ...attempts
153
+ .map((attempt) => attempt.issueLabel)
154
+ .filter((value) => value.trim().length > 0),
155
+ ...artifactLabels,
156
+ ],
157
+ issueFamilyLabels: [
158
+ ...attempts
159
+ .map((attempt) => attempt.issueFamilyLabel)
160
+ .filter((value) => Boolean(value?.trim())),
161
+ ...artifactFamilyLabels,
162
+ ],
163
+ };
164
+ });
165
+ }
166
+ function aggregateSignals(signals) {
167
+ const issueLabelCounts = new Map();
168
+ const issueFamilyLabelCounts = new Map();
169
+ const issueKeys = new Set();
170
+ const issueFamilies = new Set();
171
+ const sessions = signals
172
+ .map((signal) => signal.session)
173
+ .sort((left, right) => Date.parse(left.startedAt) - Date.parse(right.startedAt) ||
174
+ left.id.localeCompare(right.id));
175
+ for (const signal of signals) {
176
+ for (const issueKey of signal.issueKeys) {
177
+ issueKeys.add(issueKey);
178
+ }
179
+ for (const issueFamily of signal.issueFamilies) {
180
+ issueFamilies.add(issueFamily);
181
+ }
182
+ incrementCounts(issueLabelCounts, signal.issueLabels);
183
+ incrementCounts(issueFamilyLabelCounts, signal.issueFamilyLabels);
184
+ }
185
+ const first = signals[0];
186
+ const last = signals.at(-1) ?? first;
187
+ return {
188
+ workspaceKey: first?.workspaceKey ?? "",
189
+ issueKeys,
190
+ issueFamilies,
191
+ anchorTokens: new Set(first?.titleTokens ?? []),
192
+ latestTokens: new Set(last?.titleTokens ?? []),
193
+ latestSession: sessions.at(-1) ?? null,
194
+ issueLabelCounts,
195
+ issueFamilyLabelCounts,
196
+ sessions,
197
+ };
198
+ }
199
+ function confidenceFromScore(score) {
200
+ if (score >= 10) {
201
+ return "high";
202
+ }
203
+ if (score >= 6) {
204
+ return "medium";
205
+ }
206
+ return "low";
207
+ }
208
+ function hasSemanticLinkReason(reasons) {
209
+ return reasons.some((reason) => reason === "shared issue keys" ||
210
+ reason === "shared failure families" ||
211
+ reason === "shared goal wording");
212
+ }
213
+ function scoreSignalAgainstAggregate(options) {
214
+ let score = 0;
215
+ const reasons = [];
216
+ if (isSameWorkspace(options.signal.workspaceKey, options.aggregate.workspaceKey)) {
217
+ score += 4;
218
+ reasons.push("shared workspace");
219
+ }
220
+ else {
221
+ return { score: 0, reasons: [] };
222
+ }
223
+ const issueOverlap = countOverlap(options.signal.issueKeys, options.aggregate.issueKeys);
224
+ if (issueOverlap > 0) {
225
+ score += 8 + Math.min(issueOverlap, 2);
226
+ reasons.push("shared issue keys");
227
+ }
228
+ const familyOverlap = countOverlap(options.signal.issueFamilies, options.aggregate.issueFamilies);
229
+ if (familyOverlap > 0) {
230
+ score += 5 + Math.min(familyOverlap, 2);
231
+ reasons.push("shared failure families");
232
+ }
233
+ const titleOverlap = Math.max(countOverlap(options.signal.titleTokens, options.aggregate.anchorTokens), countOverlap(options.signal.titleTokens, options.aggregate.latestTokens));
234
+ if (titleOverlap >= 2) {
235
+ score += 3;
236
+ reasons.push("shared goal wording");
237
+ }
238
+ else if (titleOverlap === 1) {
239
+ score += 1;
240
+ }
241
+ if (options.aggregate.latestSession) {
242
+ const latestTs = Date.parse(options.aggregate.latestSession.endedAt ??
243
+ options.aggregate.latestSession.startedAt);
244
+ const currentTs = Date.parse(options.signal.session.startedAt);
245
+ const gapHours = Math.max(currentTs - latestTs, 0) / (1000 * 60 * 60);
246
+ if (gapHours <= 72) {
247
+ score += 2;
248
+ reasons.push("recent continuity");
249
+ }
250
+ else if (gapHours <= 24 * 14) {
251
+ score += 1;
252
+ }
253
+ if (options.aggregate.latestSession.host === options.signal.session.host) {
254
+ score += 1;
255
+ }
256
+ }
257
+ if (options.preferred) {
258
+ score += 4;
259
+ reasons.push("preferred workspace context");
260
+ }
261
+ return { score, reasons };
262
+ }
263
+ function buildAutoThreads(db, sessions) {
264
+ const signals = collectSessionSignals(db, sessions).sort((left, right) => Date.parse(left.session.startedAt) -
265
+ Date.parse(right.session.startedAt) ||
266
+ left.session.id.localeCompare(right.session.id));
267
+ const threadsByWorkspace = new Map();
268
+ for (const signal of signals) {
269
+ const workspaceThreads = threadsByWorkspace.get(signal.workspaceKey) ?? [];
270
+ let bestThread = null;
271
+ let bestScore = -1;
272
+ let bestReasons = [];
273
+ for (const thread of workspaceThreads) {
274
+ const aggregate = aggregateSignals(thread.signals);
275
+ const candidate = scoreSignalAgainstAggregate({
276
+ signal,
277
+ aggregate,
278
+ });
279
+ if (candidate.score > bestScore) {
280
+ bestScore = candidate.score;
281
+ bestThread = thread;
282
+ bestReasons = candidate.reasons;
283
+ }
284
+ }
285
+ if (!bestThread || bestScore < 5 || !hasSemanticLinkReason(bestReasons)) {
286
+ workspaceThreads.push({
287
+ signals: [signal],
288
+ reasons: new Set(signal.issueKeys.size > 0 || signal.issueFamilies.size > 0
289
+ ? ["shared workspace signals"]
290
+ : ["isolated workspace activity"]),
291
+ scores: [],
292
+ });
293
+ threadsByWorkspace.set(signal.workspaceKey, workspaceThreads);
294
+ continue;
295
+ }
296
+ bestThread.signals.push(signal);
297
+ bestReasons.forEach((reason) => bestThread?.reasons.add(reason));
298
+ bestThread.scores.push(bestScore);
299
+ }
300
+ return Array.from(threadsByWorkspace.values())
301
+ .flat()
302
+ .map((thread) => {
303
+ const aggregate = aggregateSignals(thread.signals);
304
+ const average = thread.scores.length > 0
305
+ ? thread.scores.reduce((total, value) => total + value, 0) /
306
+ thread.scores.length
307
+ : 6;
308
+ return {
309
+ workspaceKey: aggregate.workspaceKey,
310
+ sessions: aggregate.sessions,
311
+ signals: new Set(thread.reasons),
312
+ confidence: confidenceFromScore(average),
313
+ confidenceScore: Number(average.toFixed(2)),
314
+ };
315
+ })
316
+ .sort((left, right) => Date.parse(right.sessions.at(-1)?.startedAt ?? "1970-01-01T00:00:00.000Z") -
317
+ Date.parse(left.sessions.at(-1)?.startedAt ?? "1970-01-01T00:00:00.000Z") || right.workspaceKey.localeCompare(left.workspaceKey));
318
+ }
319
+ function buildContextListItem(context, sessions, aggregate) {
320
+ const latestSession = aggregate.latestSession ?? sessions.at(-1);
321
+ if (!latestSession) {
322
+ throw new Error(`Context ${context.id} has no linked sessions`);
323
+ }
324
+ return {
325
+ id: context.id,
326
+ label: context.label,
327
+ workspaceKey: aggregate.workspaceKey,
328
+ latestSessionId: latestSession.id,
329
+ latestSessionLabel: getSessionLabel(latestSession),
330
+ latestStartedAt: latestSession.startedAt,
331
+ latestEndedAt: latestSession.endedAt,
332
+ sessionCount: sessions.length,
333
+ hosts: [...new Set(sessions.map((session) => session.host))],
334
+ statuses: [...new Set(sessions.map((session) => session.status))],
335
+ confidence: "high",
336
+ confidenceScore: 100,
337
+ signals: [
338
+ aggregate.issueKeys.size > 0
339
+ ? "confirmed issue continuity"
340
+ : "confirmed context",
341
+ aggregate.issueFamilies.size > 0
342
+ ? "failure-family continuity"
343
+ : "workspace continuity",
344
+ ],
345
+ };
346
+ }
347
+ function sortDecisionChronologically(decisions) {
348
+ return decisions
349
+ .slice()
350
+ .sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt) ||
351
+ left.decisionId.localeCompare(right.decisionId));
352
+ }
353
+ function resolveCurrentDecision(decisions) {
354
+ const chronological = sortDecisionChronologically(decisions);
355
+ const accepted = chronological.filter((decision) => decision.status === "accepted");
356
+ if (accepted.length > 0) {
357
+ return accepted.at(-1) ?? null;
358
+ }
359
+ const open = chronological.filter((decision) => decision.status !== "rejected");
360
+ return open.at(-1) ?? null;
361
+ }
362
+ function resolveDecisionTopics(decisions) {
363
+ const topics = [];
364
+ for (const decision of sortDecisionChronologically(decisions)) {
365
+ const tokens = tokenize([decision.title, decision.summary].join(" "));
366
+ let bestTopic = null;
367
+ let bestOverlap = 0;
368
+ for (const topic of topics) {
369
+ const overlap = Math.max(countOverlap(tokens, topic.representativeTokens), countOverlap(tokens, topic.sharedTokens));
370
+ if (overlap > bestOverlap) {
371
+ bestOverlap = overlap;
372
+ bestTopic = topic;
373
+ }
374
+ }
375
+ if (!bestTopic || bestOverlap < 2) {
376
+ topics.push({
377
+ topic: truncateSummary(decision.title, 80),
378
+ representativeTokens: new Set(tokens),
379
+ sharedTokens: new Set(tokens),
380
+ decisions: [decision],
381
+ });
382
+ continue;
383
+ }
384
+ bestTopic.decisions.push(decision);
385
+ bestTopic.representativeTokens = new Set(tokens);
386
+ bestTopic.sharedTokens = new Set([...bestTopic.sharedTokens].filter((token) => tokens.has(token)));
387
+ bestTopic.topic = truncateSummary(decision.title, 80);
388
+ }
389
+ return topics;
390
+ }
391
+ function buildDecisionLineage(decisions) {
392
+ const activeDecisions = [];
393
+ const supersededDecisions = [];
394
+ const changeLog = [];
395
+ for (const topic of resolveDecisionTopics(decisions)) {
396
+ const current = resolveCurrentDecision(topic.decisions);
397
+ if (!current) {
398
+ continue;
399
+ }
400
+ activeDecisions.push({
401
+ ...current,
402
+ topic: topic.topic,
403
+ });
404
+ const superseded = topic.decisions
405
+ .filter((decision) => decision.decisionId !== current.decisionId)
406
+ .map((decision) => ({
407
+ ...decision,
408
+ topic: topic.topic,
409
+ supersededByDecisionId: current.decisionId,
410
+ supersededByTitle: current.title,
411
+ }));
412
+ supersededDecisions.push(...superseded);
413
+ if (superseded.length > 0) {
414
+ const latestSuperseded = sortDecisionChronologically(superseded).at(-1);
415
+ changeLog.push({
416
+ kind: "decision-updated",
417
+ summary: `Decision updated in ${topic.topic}: ${truncateSummary(latestSuperseded.title, 72)} -> ${truncateSummary(current.title, 72)}`,
418
+ sessionId: current.sessionId,
419
+ sessionLabel: current.sessionLabel,
420
+ createdAt: current.createdAt,
421
+ });
422
+ }
423
+ }
424
+ return {
425
+ activeDecisions: sortDecisionChronologically(activeDecisions).reverse(),
426
+ supersededDecisions: sortDecisionChronologically(supersededDecisions).reverse(),
427
+ changeLog: changeLog.sort((left, right) => Date.parse(right.createdAt ?? "1970-01-01T00:00:00.000Z") -
428
+ Date.parse(left.createdAt ?? "1970-01-01T00:00:00.000Z") ||
429
+ right.summary.localeCompare(left.summary)),
430
+ };
431
+ }
432
+ function buildContextMarkdown(report) {
433
+ return [
434
+ `# Context Briefing: ${report.context.label}`,
435
+ "",
436
+ `- Context ID: ${report.context.id}`,
437
+ `- Workspace: ${report.context.workspaceKey}`,
438
+ `- Sessions: ${report.context.sessionCount}`,
439
+ `- Latest Session: ${report.context.latestSessionLabel}`,
440
+ "",
441
+ "## Current Truth",
442
+ "",
443
+ report.currentTruth.summary,
444
+ "",
445
+ "## Active Blockers",
446
+ "",
447
+ ...(report.currentTruth.activeBlockers.length > 0
448
+ ? report.currentTruth.activeBlockers.map((item) => `- ${item}`)
449
+ : ["_No active blockers detected in this context._"]),
450
+ "",
451
+ "## Open Questions",
452
+ "",
453
+ ...(report.currentTruth.openQuestions.length > 0
454
+ ? report.currentTruth.openQuestions.map((item) => `- ${item}`)
455
+ : ["_No open questions detected in this context._"]),
456
+ "",
457
+ "## Active Decisions",
458
+ "",
459
+ ...(report.activeDecisions.length > 0
460
+ ? report.activeDecisions.map((decision) => `- [${decision.status}] ${decision.title} (${decision.sessionLabel})`)
461
+ : ["_No active decisions extracted yet._"]),
462
+ "",
463
+ "## Superseded Decisions",
464
+ "",
465
+ ...(report.supersededDecisions.length > 0
466
+ ? report.supersededDecisions.map((decision) => `- [${decision.status}] ${decision.title} -> ${decision.supersededByTitle ?? "replaced later"}`)
467
+ : ["_No superseded decisions detected._"]),
468
+ "",
469
+ "## Recent Sessions",
470
+ "",
471
+ ...report.sessions.map((session) => `- ${session.label} (${session.host}, ${session.status}, ${session.startedAt})`),
472
+ "",
473
+ ].join("\n");
474
+ }
475
+ function buildCurrentTruthSummary(options) {
476
+ const activeDecisionSummary = options.activeDecisions.length > 0
477
+ ? options.activeDecisions
478
+ .slice(0, 3)
479
+ .map((decision) => decision.title)
480
+ .join(" | ")
481
+ : "No active decisions extracted yet.";
482
+ const narrative = options.latestHandoff ?? options.latestSummaryNarrative ?? null;
483
+ const narrativeSummary = narrative
484
+ ? truncateSummary(narrative.replace(/\s+/g, " "), 280)
485
+ : `Latest known direction comes from ${options.context.latestSessionLabel}.`;
486
+ const blockerSummary = options.trendReport.summary.activeBlockers > 0
487
+ ? `${options.trendReport.summary.activeBlockers} active blocker(s) remain in this context.`
488
+ : "No active blockers are currently detected in this context.";
489
+ return [
490
+ narrativeSummary,
491
+ blockerSummary,
492
+ `Active decisions: ${activeDecisionSummary}`,
493
+ ].join(" ");
494
+ }
495
+ function buildContextReportFromSessions(db, context, sessions) {
496
+ const sessionSummaries = sessions
497
+ .slice()
498
+ .sort((left, right) => Date.parse(right.startedAt) - Date.parse(left.startedAt) ||
499
+ right.id.localeCompare(left.id))
500
+ .map((session) => ({
501
+ id: session.id,
502
+ label: getSessionLabel(session),
503
+ host: session.host,
504
+ status: session.status,
505
+ startedAt: session.startedAt,
506
+ endedAt: session.endedAt,
507
+ }));
508
+ const details = sessions
509
+ .map((session) => db.getSessionDetail(session.id))
510
+ .filter((detail) => Boolean(detail));
511
+ const latestWithNarratives = details
512
+ .slice()
513
+ .sort((left, right) => Date.parse(right.session.startedAt) -
514
+ Date.parse(left.session.startedAt) ||
515
+ right.session.id.localeCompare(left.session.id))
516
+ .find((detail) => detail.narratives.length > 0) ?? null;
517
+ const latestSummaryNarrative = latestWithNarratives?.narratives.find((narrative) => narrative.kind === "project-summary")?.content ?? null;
518
+ const latestHandoff = latestWithNarratives?.narratives.find((narrative) => narrative.kind === "handoff")?.content ?? null;
519
+ const decisions = details.flatMap((detail) => detail.decisions.map((decision) => ({
520
+ decisionId: decision.id,
521
+ topic: truncateSummary(decision.title, 80),
522
+ sessionId: detail.session.id,
523
+ sessionLabel: getSessionLabel(detail.session),
524
+ title: decision.title,
525
+ summary: decision.summary,
526
+ rationale: decision.rationale,
527
+ status: decision.status,
528
+ createdAt: decision.createdAt,
529
+ })));
530
+ const decisionLineage = buildDecisionLineage(decisions);
531
+ const sessionIds = sessions.map((session) => session.id);
532
+ const trendReport = buildHistoryTrendReport(db, { sessionIds });
533
+ const handoff = buildHistoryHandoffReport(db, { sessionIds });
534
+ const currentTruth = {
535
+ summary: buildCurrentTruthSummary({
536
+ context,
537
+ latestSummaryNarrative,
538
+ latestHandoff,
539
+ activeDecisions: decisionLineage.activeDecisions,
540
+ trendReport,
541
+ }),
542
+ latestSessionId: context.latestSessionId,
543
+ latestSessionLabel: context.latestSessionLabel,
544
+ latestSummaryNarrative,
545
+ latestHandoff,
546
+ activeBlockers: handoff.blockers,
547
+ openQuestions: handoff.followUps,
548
+ };
549
+ const report = {
550
+ context,
551
+ currentTruth,
552
+ activeDecisions: decisionLineage.activeDecisions,
553
+ supersededDecisions: decisionLineage.supersededDecisions,
554
+ changeLog: [
555
+ {
556
+ kind: "context-refreshed",
557
+ summary: `Latest known context state comes from ${context.latestSessionLabel}.`,
558
+ sessionId: context.latestSessionId,
559
+ sessionLabel: context.latestSessionLabel,
560
+ createdAt: context.latestStartedAt,
561
+ },
562
+ ...decisionLineage.changeLog,
563
+ ],
564
+ sessions: sessionSummaries,
565
+ trends: trendReport.trends,
566
+ handoff: {
567
+ summary: handoff.summary,
568
+ followUps: handoff.followUps,
569
+ blockers: handoff.blockers,
570
+ },
571
+ markdown: "",
572
+ };
573
+ report.markdown = buildContextMarkdown(report);
574
+ return report;
575
+ }
576
+ function buildCanonicalContextStates(db) {
577
+ return db
578
+ .listContexts()
579
+ .map((context) => {
580
+ const sessions = db.listSessionsForContext(context.id);
581
+ if (sessions.length === 0) {
582
+ return null;
583
+ }
584
+ const aggregate = aggregateSignals(collectSessionSignals(db, sessions));
585
+ return {
586
+ record: context,
587
+ item: buildContextListItem(context, sessions, aggregate),
588
+ sessions,
589
+ aggregate,
590
+ };
591
+ })
592
+ .filter((value) => Boolean(value));
593
+ }
594
+ function buildSyntheticSignal(input) {
595
+ const now = new Date().toISOString();
596
+ const session = {
597
+ id: "synthetic",
598
+ host: input.host ?? "claude",
599
+ projectRoot: input.cwd,
600
+ cwd: input.cwd,
601
+ title: input.title ?? null,
602
+ status: "running",
603
+ startedAt: now,
604
+ endedAt: null,
605
+ metadata: null,
606
+ createdAt: now,
607
+ updatedAt: now,
608
+ };
609
+ return {
610
+ session,
611
+ workspaceKey: parseWorkspacePath(input.cwd),
612
+ titleTokens: tokenize(input.title ?? ""),
613
+ issueKeys: new Set(),
614
+ issueFamilies: new Set(),
615
+ issueLabels: [],
616
+ issueFamilyLabels: [],
617
+ };
618
+ }
619
+ function buildContextCandidate(context, sessions, score, reasons, preferred) {
620
+ return {
621
+ kind: "existing-context",
622
+ contextId: context.id,
623
+ label: context.label,
624
+ workspaceKey: context.workspaceKey,
625
+ confidence: confidenceFromScore(score),
626
+ confidenceScore: Number(score.toFixed(2)),
627
+ reasons,
628
+ sessionIds: sessions.map((session) => session.id),
629
+ latestSessionId: context.latestSessionId,
630
+ latestSessionLabel: context.latestSessionLabel,
631
+ preferred,
632
+ confirmationRequired: true,
633
+ };
634
+ }
635
+ function buildNewContextCandidate(thread) {
636
+ const latestSession = thread.sessions.at(-1) ?? null;
637
+ const label = latestSession?.title ??
638
+ latestSession?.id ??
639
+ `Context ${hashText(thread.workspaceKey)}`;
640
+ return {
641
+ kind: "new-context",
642
+ contextId: null,
643
+ label: truncateSummary(label, 120),
644
+ workspaceKey: thread.workspaceKey,
645
+ confidence: thread.confidence,
646
+ confidenceScore: thread.confidenceScore,
647
+ reasons: [...thread.signals],
648
+ sessionIds: thread.sessions.map((session) => session.id),
649
+ latestSessionId: latestSession?.id ?? null,
650
+ latestSessionLabel: latestSession ? getSessionLabel(latestSession) : null,
651
+ preferred: false,
652
+ confirmationRequired: true,
653
+ };
654
+ }
655
+ export function listContexts(db) {
656
+ const contexts = buildCanonicalContextStates(db)
657
+ .map((state) => state.item)
658
+ .sort((left, right) => Date.parse(right.latestStartedAt) - Date.parse(left.latestStartedAt) ||
659
+ right.id.localeCompare(left.id));
660
+ return {
661
+ contexts,
662
+ total: contexts.length,
663
+ };
664
+ }
665
+ export function getContextReport(db, contextId) {
666
+ const state = buildCanonicalContextStates(db).find((candidate) => candidate.item.id === contextId);
667
+ if (!state) {
668
+ throw new Error(`Context not found: ${contextId}`);
669
+ }
670
+ return buildContextReportFromSessions(db, state.item, state.sessions);
671
+ }
672
+ export function resolveContext(db, options) {
673
+ const canonicalStates = buildCanonicalContextStates(db);
674
+ const byId = new Map(canonicalStates.map((state) => [state.item.id, state]));
675
+ if (options.sessionId) {
676
+ const linked = db.findContextLinkForSession(options.sessionId);
677
+ if (linked) {
678
+ const linkedContext = byId.get(linked.contextId);
679
+ if (!linkedContext) {
680
+ throw new Error(`Linked context not found: ${linked.contextId}`);
681
+ }
682
+ return {
683
+ mode: "linked",
684
+ sessionId: options.sessionId,
685
+ cwd: linkedContext.item.workspaceKey,
686
+ confirmationRequired: false,
687
+ recommendedAction: "use-linked",
688
+ linkedContextId: linkedContext.item.id,
689
+ currentContext: linkedContext.item,
690
+ briefing: buildContextReportFromSessions(db, linkedContext.item, linkedContext.sessions),
691
+ candidates: [],
692
+ };
693
+ }
694
+ }
695
+ const baseSignal = options.sessionId
696
+ ? (() => {
697
+ const session = db.findSessionById(options.sessionId);
698
+ if (!session) {
699
+ throw new Error(`Session not found: ${options.sessionId}`);
700
+ }
701
+ return collectSessionSignals(db, [session])[0];
702
+ })()
703
+ : options.cwd
704
+ ? buildSyntheticSignal({
705
+ cwd: options.cwd,
706
+ title: options.title,
707
+ host: options.host,
708
+ })
709
+ : null;
710
+ if (!baseSignal) {
711
+ throw new Error("resolve-context requires sessionId or cwd");
712
+ }
713
+ const workspaceKey = baseSignal.workspaceKey;
714
+ const preferred = db.getWorkspacePreferredContext(workspaceKey);
715
+ const rejectedContextIds = options.sessionId
716
+ ? new Set(db
717
+ .listContextRejectionsForSession(options.sessionId)
718
+ .map((record) => record.contextId))
719
+ : new Set();
720
+ const candidates = canonicalStates
721
+ .filter((state) => isSameWorkspace(workspaceKey, state.aggregate.workspaceKey))
722
+ .filter((state) => !rejectedContextIds.has(state.item.id))
723
+ .map((state) => {
724
+ const score = scoreSignalAgainstAggregate({
725
+ signal: baseSignal,
726
+ aggregate: state.aggregate,
727
+ preferred: preferred?.contextId === state.item.id,
728
+ });
729
+ return {
730
+ state,
731
+ ...score,
732
+ };
733
+ })
734
+ .filter((candidate) => candidate.score >= 4 &&
735
+ (!options.sessionId || hasSemanticLinkReason(candidate.reasons)))
736
+ .sort((left, right) => right.score - left.score ||
737
+ Date.parse(right.state.item.latestStartedAt) -
738
+ Date.parse(left.state.item.latestStartedAt) ||
739
+ right.state.item.id.localeCompare(left.state.item.id))
740
+ .slice(0, 3)
741
+ .map((candidate) => buildContextCandidate(candidate.state.item, candidate.state.sessions, candidate.score, candidate.reasons, preferred?.contextId === candidate.state.item.id));
742
+ if (!options.sessionId && preferred) {
743
+ const preferredContext = byId.get(preferred.contextId);
744
+ const preferredCandidate = candidates.find((candidate) => candidate.contextId === preferred.contextId) ?? null;
745
+ const canAutoUsePreferred = preferredContext !== undefined &&
746
+ preferredCandidate !== null &&
747
+ hasSemanticLinkReason(preferredCandidate.reasons) &&
748
+ !candidates.some((candidate) => candidate.contextId !== preferred.contextId &&
749
+ candidate.confidenceScore >= preferredCandidate.confidenceScore &&
750
+ hasSemanticLinkReason(candidate.reasons));
751
+ if (preferredContext && canAutoUsePreferred) {
752
+ return {
753
+ mode: "preferred",
754
+ sessionId: null,
755
+ cwd: workspaceKey,
756
+ confirmationRequired: false,
757
+ recommendedAction: "use-preferred",
758
+ linkedContextId: preferredContext.item.id,
759
+ currentContext: preferredContext.item,
760
+ briefing: buildContextReportFromSessions(db, preferredContext.item, preferredContext.sessions),
761
+ candidates,
762
+ };
763
+ }
764
+ }
765
+ const autoThreadCandidate = options.sessionId
766
+ ? (() => {
767
+ const thread = buildAutoThreads(db, db.listUnlinkedSessions({ workspaceKey })).find((candidate) => candidate.sessions.some((session) => session.id === options.sessionId));
768
+ return thread ? buildNewContextCandidate(thread) : null;
769
+ })()
770
+ : (() => {
771
+ const thread = buildAutoThreads(db, db.listUnlinkedSessions({ workspaceKey }))[0];
772
+ return thread && isSameWorkspace(workspaceKey, thread.workspaceKey)
773
+ ? buildNewContextCandidate(thread)
774
+ : null;
775
+ })();
776
+ const allCandidates = autoThreadCandidate
777
+ ? [...candidates, autoThreadCandidate]
778
+ : candidates;
779
+ if (allCandidates.length === 0) {
780
+ return {
781
+ mode: "none",
782
+ sessionId: options.sessionId ?? null,
783
+ cwd: workspaceKey,
784
+ confirmationRequired: true,
785
+ recommendedAction: "create-new-context",
786
+ linkedContextId: null,
787
+ currentContext: null,
788
+ briefing: null,
789
+ candidates: [],
790
+ };
791
+ }
792
+ const top = allCandidates[0];
793
+ const briefing = top.kind === "existing-context" && top.contextId
794
+ ? (() => {
795
+ const state = byId.get(top.contextId);
796
+ return state
797
+ ? buildContextReportFromSessions(db, state.item, state.sessions)
798
+ : null;
799
+ })()
800
+ : null;
801
+ return {
802
+ mode: "suggested",
803
+ sessionId: options.sessionId ?? null,
804
+ cwd: workspaceKey,
805
+ confirmationRequired: true,
806
+ recommendedAction: top.kind === "existing-context" &&
807
+ top.confidence === "high" &&
808
+ hasSemanticLinkReason(top.reasons)
809
+ ? "confirm-existing"
810
+ : top.kind === "new-context"
811
+ ? "create-new-context"
812
+ : "choose-candidate",
813
+ linkedContextId: null,
814
+ currentContext: null,
815
+ briefing,
816
+ candidates: allCandidates,
817
+ };
818
+ }
819
+ export function confirmContextLink(db, options) {
820
+ const normalizedSessionIds = [
821
+ ...new Set(options.sessionIds.map((id) => id.trim()).filter(Boolean)),
822
+ ];
823
+ if (normalizedSessionIds.length === 0) {
824
+ throw new Error("At least one sessionId is required");
825
+ }
826
+ const firstSession = db.findSessionById(normalizedSessionIds[0]);
827
+ if (!firstSession) {
828
+ throw new Error(`Session not found: ${normalizedSessionIds[0]}`);
829
+ }
830
+ const context = options.contextId !== undefined
831
+ ? db.resolveContextById(options.contextId)
832
+ : db.createContext({
833
+ label: options.label?.trim() ||
834
+ firstSession.title ||
835
+ getSessionLabel(firstSession),
836
+ workspaceKey: normalizeWorkspaceFromSession(firstSession),
837
+ metadata: JSON.stringify({
838
+ createdFrom: "confirm-context-link",
839
+ }),
840
+ });
841
+ if (!context) {
842
+ throw new Error(`Context not found: ${options.contextId}`);
843
+ }
844
+ for (const sessionId of normalizedSessionIds) {
845
+ db.assignSessionToContext({
846
+ sessionId,
847
+ contextId: context.id,
848
+ linkSource: options.linkSource ?? "confirmed",
849
+ });
850
+ }
851
+ if (options.setPreferred) {
852
+ db.setWorkspacePreferredContext(context.workspaceKey, context.id);
853
+ }
854
+ return {
855
+ action: "confirmed",
856
+ context: getContextReport(db, context.id).context,
857
+ affectedSessionIds: normalizedSessionIds,
858
+ contextId: context.id,
859
+ mergedFromContextId: null,
860
+ };
861
+ }
862
+ export function rejectContextLink(db, sessionId, contextId) {
863
+ const rejection = db.rejectContextForSession(sessionId, contextId);
864
+ return {
865
+ action: "rejected",
866
+ context: null,
867
+ affectedSessionIds: [rejection.sessionId],
868
+ contextId: rejection.contextId,
869
+ mergedFromContextId: null,
870
+ };
871
+ }
872
+ export function moveSessionContext(db, options) {
873
+ const session = db.findSessionById(options.sessionId);
874
+ if (!session) {
875
+ throw new Error(`Session not found: ${options.sessionId}`);
876
+ }
877
+ const context = options.contextId !== undefined
878
+ ? db.resolveContextById(options.contextId)
879
+ : db.createContext({
880
+ label: options.label?.trim() || session.title || getSessionLabel(session),
881
+ workspaceKey: normalizeWorkspaceFromSession(session),
882
+ metadata: JSON.stringify({
883
+ createdFrom: "move-session-context",
884
+ }),
885
+ });
886
+ if (!context) {
887
+ throw new Error(`Context not found: ${options.contextId}`);
888
+ }
889
+ db.assignSessionToContext({
890
+ sessionId: session.id,
891
+ contextId: context.id,
892
+ linkSource: "moved",
893
+ });
894
+ if (options.setPreferred) {
895
+ db.setWorkspacePreferredContext(context.workspaceKey, context.id);
896
+ }
897
+ return {
898
+ action: "moved",
899
+ context: getContextReport(db, context.id).context,
900
+ affectedSessionIds: [session.id],
901
+ contextId: context.id,
902
+ mergedFromContextId: null,
903
+ };
904
+ }
905
+ export function mergeContexts(db, sourceContextId, targetContextId) {
906
+ db.mergeContexts(sourceContextId, targetContextId);
907
+ const target = db.resolveContextById(targetContextId);
908
+ if (!target) {
909
+ throw new Error(`Context not found: ${targetContextId}`);
910
+ }
911
+ return {
912
+ action: "merged",
913
+ context: getContextReport(db, target.id).context,
914
+ affectedSessionIds: db
915
+ .listSessionsForContext(target.id)
916
+ .map((session) => session.id),
917
+ contextId: target.id,
918
+ mergedFromContextId: sourceContextId,
919
+ };
920
+ }
921
+ export function splitContext(db, options) {
922
+ const source = db.resolveContextById(options.contextId);
923
+ if (!source) {
924
+ throw new Error(`Context not found: ${options.contextId}`);
925
+ }
926
+ const sessions = options.sessionIds
927
+ .map((sessionId) => db.findSessionById(sessionId))
928
+ .filter((session) => Boolean(session));
929
+ if (sessions.length === 0) {
930
+ throw new Error("At least one valid session is required for split");
931
+ }
932
+ const next = db.createContext({
933
+ label: options.label?.trim() ||
934
+ sessions.at(-1)?.title ||
935
+ `${source.label} split`,
936
+ workspaceKey: normalizeWorkspaceFromSession(sessions[0]),
937
+ metadata: JSON.stringify({
938
+ createdFrom: "split-context",
939
+ sourceContextId: source.id,
940
+ }),
941
+ });
942
+ for (const session of sessions) {
943
+ db.assignSessionToContext({
944
+ sessionId: session.id,
945
+ contextId: next.id,
946
+ linkSource: "split",
947
+ });
948
+ }
949
+ if (options.setPreferred) {
950
+ db.setWorkspacePreferredContext(next.workspaceKey, next.id);
951
+ }
952
+ return {
953
+ action: "split",
954
+ context: getContextReport(db, next.id).context,
955
+ affectedSessionIds: sessions.map((session) => session.id),
956
+ contextId: next.id,
957
+ mergedFromContextId: source.id,
958
+ };
959
+ }
960
+ export function setActiveContext(db, contextId, cwd) {
961
+ const context = db.resolveContextById(contextId);
962
+ if (!context) {
963
+ throw new Error(`Context not found: ${contextId}`);
964
+ }
965
+ db.setWorkspacePreferredContext(cwd ? parseWorkspacePath(cwd) : normalizeWorkspaceKey(context.workspaceKey), context.id);
966
+ return {
967
+ action: "preferred",
968
+ context: getContextReport(db, context.id).context,
969
+ affectedSessionIds: [],
970
+ contextId: context.id,
971
+ mergedFromContextId: null,
972
+ };
973
+ }
974
+ //# sourceMappingURL=context-memory.js.map