@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.
- package/README.md +301 -132
- package/SKILL.md +72 -28
- package/bin/footprint.js +16 -0
- package/dist/src/adapters/claude.d.ts +2 -0
- package/dist/src/adapters/claude.d.ts.map +1 -0
- package/dist/src/adapters/claude.js +7 -0
- package/dist/src/adapters/claude.js.map +1 -0
- package/dist/src/adapters/codex.d.ts +2 -0
- package/dist/src/adapters/codex.d.ts.map +1 -0
- package/dist/src/adapters/codex.js +7 -0
- package/dist/src/adapters/codex.js.map +1 -0
- package/dist/src/adapters/gemini.d.ts +2 -0
- package/dist/src/adapters/gemini.d.ts.map +1 -0
- package/dist/src/adapters/gemini.js +7 -0
- package/dist/src/adapters/gemini.js.map +1 -0
- package/dist/src/adapters/index.d.ts +5 -0
- package/dist/src/adapters/index.d.ts.map +1 -0
- package/dist/src/adapters/index.js +12 -0
- package/dist/src/adapters/index.js.map +1 -0
- package/dist/src/adapters/structured-prefix.d.ts +10 -0
- package/dist/src/adapters/structured-prefix.d.ts.map +1 -0
- package/dist/src/adapters/structured-prefix.js +59 -0
- package/dist/src/adapters/structured-prefix.js.map +1 -0
- package/dist/src/adapters/types.d.ts +32 -0
- package/dist/src/adapters/types.d.ts.map +1 -0
- package/dist/src/adapters/types.js +2 -0
- package/dist/src/adapters/types.js.map +1 -0
- package/dist/src/cli/context-flow.d.ts +92 -0
- package/dist/src/cli/context-flow.d.ts.map +1 -0
- package/dist/src/cli/context-flow.js +724 -0
- package/dist/src/cli/context-flow.js.map +1 -0
- package/dist/src/cli/history-display.d.ts +27 -0
- package/dist/src/cli/history-display.d.ts.map +1 -0
- package/dist/src/cli/history-display.js +167 -0
- package/dist/src/cli/history-display.js.map +1 -0
- package/dist/src/cli/index.js +924 -0
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/cli/launch-spec.d.ts +31 -0
- package/dist/src/cli/launch-spec.d.ts.map +1 -0
- package/dist/src/cli/launch-spec.js +182 -0
- package/dist/src/cli/launch-spec.js.map +1 -0
- package/dist/src/cli/live-demo.d.ts +34 -0
- package/dist/src/cli/live-demo.d.ts.map +1 -0
- package/dist/src/cli/live-demo.js +254 -0
- package/dist/src/cli/live-demo.js.map +1 -0
- package/dist/src/cli/pty-transcript.d.ts +34 -0
- package/dist/src/cli/pty-transcript.d.ts.map +1 -0
- package/dist/src/cli/pty-transcript.js +174 -0
- package/dist/src/cli/pty-transcript.js.map +1 -0
- package/dist/src/cli/session-display.d.ts +74 -0
- package/dist/src/cli/session-display.d.ts.map +1 -0
- package/dist/src/cli/session-display.js +922 -0
- package/dist/src/cli/session-display.js.map +1 -0
- package/dist/src/cli/session-execution.d.ts +55 -0
- package/dist/src/cli/session-execution.d.ts.map +1 -0
- package/dist/src/cli/session-execution.js +817 -0
- package/dist/src/cli/session-execution.js.map +1 -0
- package/dist/src/cli/session-runtime.d.ts +5 -0
- package/dist/src/cli/session-runtime.d.ts.map +1 -0
- package/dist/src/cli/session-runtime.js +11 -0
- package/dist/src/cli/session-runtime.js.map +1 -0
- package/dist/src/cli/setup.d.ts.map +1 -1
- package/dist/src/cli/setup.js +2 -0
- package/dist/src/cli/setup.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +148 -7
- package/dist/src/index.js.map +1 -1
- package/dist/src/ingestion/deterministic.d.ts +3 -0
- package/dist/src/ingestion/deterministic.d.ts.map +1 -0
- package/dist/src/ingestion/deterministic.js +862 -0
- package/dist/src/ingestion/deterministic.js.map +1 -0
- package/dist/src/ingestion/index.d.ts +5 -0
- package/dist/src/ingestion/index.d.ts.map +1 -0
- package/dist/src/ingestion/index.js +27 -0
- package/dist/src/ingestion/index.js.map +1 -0
- package/dist/src/ingestion/semantic.d.ts +6 -0
- package/dist/src/ingestion/semantic.d.ts.map +1 -0
- package/dist/src/ingestion/semantic.js +627 -0
- package/dist/src/ingestion/semantic.js.map +1 -0
- package/dist/src/ingestion/types.d.ts +10 -0
- package/dist/src/ingestion/types.d.ts.map +1 -0
- package/dist/src/ingestion/types.js +2 -0
- package/dist/src/ingestion/types.js.map +1 -0
- package/dist/src/lib/context-memory.d.ts +140 -0
- package/dist/src/lib/context-memory.d.ts.map +1 -0
- package/dist/src/lib/context-memory.js +974 -0
- package/dist/src/lib/context-memory.js.map +1 -0
- package/dist/src/lib/history-handoff.d.ts +43 -0
- package/dist/src/lib/history-handoff.d.ts.map +1 -0
- package/dist/src/lib/history-handoff.js +179 -0
- package/dist/src/lib/history-handoff.js.map +1 -0
- package/dist/src/lib/observability.d.ts +3 -0
- package/dist/src/lib/observability.d.ts.map +1 -0
- package/dist/src/lib/observability.js +63 -0
- package/dist/src/lib/observability.js.map +1 -0
- package/dist/src/lib/session-artifacts.d.ts +51 -0
- package/dist/src/lib/session-artifacts.d.ts.map +1 -0
- package/dist/src/lib/session-artifacts.js +132 -0
- package/dist/src/lib/session-artifacts.js.map +1 -0
- package/dist/src/lib/session-filters.d.ts +11 -0
- package/dist/src/lib/session-filters.d.ts.map +1 -0
- package/dist/src/lib/session-filters.js +16 -0
- package/dist/src/lib/session-filters.js.map +1 -0
- package/dist/src/lib/session-history.d.ts +50 -0
- package/dist/src/lib/session-history.d.ts.map +1 -0
- package/dist/src/lib/session-history.js +73 -0
- package/dist/src/lib/session-history.js.map +1 -0
- package/dist/src/lib/session-trends.d.ts +129 -0
- package/dist/src/lib/session-trends.d.ts.map +1 -0
- package/dist/src/lib/session-trends.js +361 -0
- package/dist/src/lib/session-trends.js.map +1 -0
- package/dist/src/lib/storage/database.d.ts +212 -1
- package/dist/src/lib/storage/database.d.ts.map +1 -1
- package/dist/src/lib/storage/database.js +1694 -114
- package/dist/src/lib/storage/database.js.map +1 -1
- package/dist/src/lib/storage/export-sessions.d.ts +33 -0
- package/dist/src/lib/storage/export-sessions.d.ts.map +1 -0
- package/dist/src/lib/storage/export-sessions.js +525 -0
- package/dist/src/lib/storage/export-sessions.js.map +1 -0
- package/dist/src/lib/storage/index.d.ts +7 -6
- package/dist/src/lib/storage/index.d.ts.map +1 -1
- package/dist/src/lib/storage/index.js +6 -5
- package/dist/src/lib/storage/index.js.map +1 -1
- package/dist/src/lib/storage/schema.d.ts +6 -1
- package/dist/src/lib/storage/schema.d.ts.map +1 -1
- package/dist/src/lib/storage/schema.js +337 -2
- package/dist/src/lib/storage/schema.js.map +1 -1
- package/dist/src/lib/storage/types.d.ts +122 -0
- package/dist/src/lib/storage/types.d.ts.map +1 -1
- package/dist/src/prompts/skill-prompt.d.ts.map +1 -1
- package/dist/src/prompts/skill-prompt.js +13 -0
- package/dist/src/prompts/skill-prompt.js.map +1 -1
- package/dist/src/tools/confirm-context-link.d.ts +62 -0
- package/dist/src/tools/confirm-context-link.d.ts.map +1 -0
- package/dist/src/tools/confirm-context-link.js +36 -0
- package/dist/src/tools/confirm-context-link.js.map +1 -0
- package/dist/src/tools/context-schemas.d.ts +694 -0
- package/dist/src/tools/context-schemas.d.ts.map +1 -0
- package/dist/src/tools/context-schemas.js +171 -0
- package/dist/src/tools/context-schemas.js.map +1 -0
- package/dist/src/tools/export-sessions.d.ts +111 -0
- package/dist/src/tools/export-sessions.d.ts.map +1 -0
- package/dist/src/tools/export-sessions.js +136 -0
- package/dist/src/tools/export-sessions.js.map +1 -0
- package/dist/src/tools/get-context.d.ts +208 -0
- package/dist/src/tools/get-context.d.ts.map +1 -0
- package/dist/src/tools/get-context.js +27 -0
- package/dist/src/tools/get-context.js.map +1 -0
- package/dist/src/tools/get-history-handoff.d.ts +109 -0
- package/dist/src/tools/get-history-handoff.d.ts.map +1 -0
- package/dist/src/tools/get-history-handoff.js +85 -0
- package/dist/src/tools/get-history-handoff.js.map +1 -0
- package/dist/src/tools/get-history-trends.d.ts +155 -0
- package/dist/src/tools/get-history-trends.d.ts.map +1 -0
- package/dist/src/tools/get-history-trends.js +123 -0
- package/dist/src/tools/get-history-trends.js.map +1 -0
- package/dist/src/tools/get-session-artifacts.d.ts +151 -0
- package/dist/src/tools/get-session-artifacts.d.ts.map +1 -0
- package/dist/src/tools/get-session-artifacts.js +184 -0
- package/dist/src/tools/get-session-artifacts.js.map +1 -0
- package/dist/src/tools/get-session-decisions.d.ts +69 -0
- package/dist/src/tools/get-session-decisions.d.ts.map +1 -0
- package/dist/src/tools/get-session-decisions.js +99 -0
- package/dist/src/tools/get-session-decisions.js.map +1 -0
- package/dist/src/tools/get-session-messages.d.ts +55 -0
- package/dist/src/tools/get-session-messages.d.ts.map +1 -0
- package/dist/src/tools/get-session-messages.js +89 -0
- package/dist/src/tools/get-session-messages.js.map +1 -0
- package/dist/src/tools/get-session-narrative.d.ts +72 -0
- package/dist/src/tools/get-session-narrative.d.ts.map +1 -0
- package/dist/src/tools/get-session-narrative.js +106 -0
- package/dist/src/tools/get-session-narrative.js.map +1 -0
- package/dist/src/tools/get-session-timeline.d.ts +55 -0
- package/dist/src/tools/get-session-timeline.d.ts.map +1 -0
- package/dist/src/tools/get-session-timeline.js +93 -0
- package/dist/src/tools/get-session-timeline.js.map +1 -0
- package/dist/src/tools/get-session-trends.d.ts +108 -0
- package/dist/src/tools/get-session-trends.d.ts.map +1 -0
- package/dist/src/tools/get-session-trends.js +130 -0
- package/dist/src/tools/get-session-trends.js.map +1 -0
- package/dist/src/tools/get-session.d.ts +251 -0
- package/dist/src/tools/get-session.d.ts.map +1 -0
- package/dist/src/tools/get-session.js +290 -0
- package/dist/src/tools/get-session.js.map +1 -0
- package/dist/src/tools/index.d.ts +22 -0
- package/dist/src/tools/index.d.ts.map +1 -1
- package/dist/src/tools/index.js +22 -0
- package/dist/src/tools/index.js.map +1 -1
- package/dist/src/tools/list-contexts.d.ts +50 -0
- package/dist/src/tools/list-contexts.d.ts.map +1 -0
- package/dist/src/tools/list-contexts.js +28 -0
- package/dist/src/tools/list-contexts.js.map +1 -0
- package/dist/src/tools/list-sessions.d.ts +86 -0
- package/dist/src/tools/list-sessions.d.ts.map +1 -0
- package/dist/src/tools/list-sessions.js +97 -0
- package/dist/src/tools/list-sessions.js.map +1 -0
- package/dist/src/tools/merge-contexts.d.ts +58 -0
- package/dist/src/tools/merge-contexts.d.ts.map +1 -0
- package/dist/src/tools/merge-contexts.js +27 -0
- package/dist/src/tools/merge-contexts.js.map +1 -0
- package/dist/src/tools/move-session-context.d.ts +62 -0
- package/dist/src/tools/move-session-context.d.ts.map +1 -0
- package/dist/src/tools/move-session-context.js +33 -0
- package/dist/src/tools/move-session-context.js.map +1 -0
- package/dist/src/tools/reingest-session.d.ts +31 -0
- package/dist/src/tools/reingest-session.d.ts.map +1 -0
- package/dist/src/tools/reingest-session.js +43 -0
- package/dist/src/tools/reingest-session.js.map +1 -0
- package/dist/src/tools/reject-context-link.d.ts +58 -0
- package/dist/src/tools/reject-context-link.d.ts.map +1 -0
- package/dist/src/tools/reject-context-link.js +26 -0
- package/dist/src/tools/reject-context-link.js.map +1 -0
- package/dist/src/tools/resolve-context.d.ts +287 -0
- package/dist/src/tools/resolve-context.d.ts.map +1 -0
- package/dist/src/tools/resolve-context.js +35 -0
- package/dist/src/tools/resolve-context.js.map +1 -0
- package/dist/src/tools/search-history.d.ts +86 -0
- package/dist/src/tools/search-history.d.ts.map +1 -0
- package/dist/src/tools/search-history.js +103 -0
- package/dist/src/tools/search-history.js.map +1 -0
- package/dist/src/tools/session-ui-metadata.d.ts +15 -0
- package/dist/src/tools/session-ui-metadata.d.ts.map +1 -0
- package/dist/src/tools/session-ui-metadata.js +15 -0
- package/dist/src/tools/session-ui-metadata.js.map +1 -0
- package/dist/src/tools/set-active-context.d.ts +58 -0
- package/dist/src/tools/set-active-context.d.ts.map +1 -0
- package/dist/src/tools/set-active-context.js +26 -0
- package/dist/src/tools/set-active-context.js.map +1 -0
- package/dist/src/tools/split-context.d.ts +62 -0
- package/dist/src/tools/split-context.d.ts.map +1 -0
- package/dist/src/tools/split-context.js +36 -0
- package/dist/src/tools/split-context.js.map +1 -0
- package/dist/src/tools/verify-footprint.js +1 -1
- package/dist/src/tools/verify-footprint.js.map +1 -1
- package/dist/src/types.d.ts +2 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/ui/register.d.ts +6 -1
- package/dist/src/ui/register.d.ts.map +1 -1
- package/dist/src/ui/register.js +60 -16
- package/dist/src/ui/register.js.map +1 -1
- package/dist/ui/dashboard.html +239 -868
- package/dist/ui/detail.html +107 -248
- package/dist/ui/export.html +115 -298
- package/dist/ui/session-dashboard-live.html +264 -0
- package/dist/ui/session-dashboard.html +329 -0
- package/dist/ui/session-detail-live.html +336 -0
- package/dist/ui/session-detail.html +355 -0
- 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
|