@stackmemoryai/stackmemory 0.5.1 → 0.5.3
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/dist/cli/commands/config.js +81 -0
- package/dist/cli/commands/config.js.map +2 -2
- package/dist/cli/commands/decision.js +262 -0
- package/dist/cli/commands/decision.js.map +7 -0
- package/dist/cli/commands/handoff.js +87 -24
- package/dist/cli/commands/handoff.js.map +3 -3
- package/dist/cli/commands/service.js +684 -0
- package/dist/cli/commands/service.js.map +7 -0
- package/dist/cli/commands/sweep.js +311 -0
- package/dist/cli/commands/sweep.js.map +7 -0
- package/dist/cli/index.js +98 -4
- package/dist/cli/index.js.map +2 -2
- package/dist/core/config/storage-config.js +111 -0
- package/dist/core/config/storage-config.js.map +7 -0
- package/dist/core/session/enhanced-handoff.js +654 -0
- package/dist/core/session/enhanced-handoff.js.map +7 -0
- package/dist/daemon/session-daemon.js +308 -0
- package/dist/daemon/session-daemon.js.map +7 -0
- package/dist/skills/repo-ingestion-skill.js +54 -10
- package/dist/skills/repo-ingestion-skill.js.map +2 -2
- package/package.json +4 -1
- package/scripts/archive/check-all-duplicates.ts +2 -2
- package/scripts/archive/merge-linear-duplicates.ts +6 -4
- package/scripts/install-claude-hooks-auto.js +72 -15
- package/scripts/measure-handoff-impact.mjs +395 -0
- package/scripts/measure-handoff-impact.ts +450 -0
- package/templates/claude-hooks/on-startup.js +200 -19
- package/templates/services/com.stackmemory.guardian.plist +59 -0
- package/templates/services/stackmemory-guardian.service +41 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
statSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
mkdirSync
|
|
9
|
+
} from "fs";
|
|
10
|
+
import { join, basename } from "path";
|
|
11
|
+
import { homedir, tmpdir } from "os";
|
|
12
|
+
import { globSync } from "glob";
|
|
13
|
+
let countTokens;
|
|
14
|
+
try {
|
|
15
|
+
const tokenizer = await import("@anthropic-ai/tokenizer");
|
|
16
|
+
countTokens = tokenizer.countTokens;
|
|
17
|
+
} catch {
|
|
18
|
+
countTokens = (text) => Math.ceil(text.length / 3.5);
|
|
19
|
+
}
|
|
20
|
+
function loadSessionDecisions(projectRoot) {
|
|
21
|
+
const storePath = join(projectRoot, ".stackmemory", "session-decisions.json");
|
|
22
|
+
if (existsSync(storePath)) {
|
|
23
|
+
try {
|
|
24
|
+
const store = JSON.parse(readFileSync(storePath, "utf-8"));
|
|
25
|
+
return store.decisions || [];
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
function loadReviewFeedback(projectRoot) {
|
|
33
|
+
const storePath = join(projectRoot, ".stackmemory", "review-feedback.json");
|
|
34
|
+
if (existsSync(storePath)) {
|
|
35
|
+
try {
|
|
36
|
+
const store = JSON.parse(
|
|
37
|
+
readFileSync(storePath, "utf-8")
|
|
38
|
+
);
|
|
39
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
|
|
40
|
+
return store.feedbacks.filter(
|
|
41
|
+
(f) => new Date(f.timestamp).getTime() > cutoff
|
|
42
|
+
);
|
|
43
|
+
} catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
function saveReviewFeedback(projectRoot, feedbacks) {
|
|
50
|
+
const dir = join(projectRoot, ".stackmemory");
|
|
51
|
+
if (!existsSync(dir)) {
|
|
52
|
+
mkdirSync(dir, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
const storePath = join(dir, "review-feedback.json");
|
|
55
|
+
let existing = [];
|
|
56
|
+
if (existsSync(storePath)) {
|
|
57
|
+
try {
|
|
58
|
+
const store2 = JSON.parse(
|
|
59
|
+
readFileSync(storePath, "utf-8")
|
|
60
|
+
);
|
|
61
|
+
existing = store2.feedbacks || [];
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const seen = /* @__PURE__ */ new Set();
|
|
66
|
+
const merged = [];
|
|
67
|
+
for (const f of [...feedbacks, ...existing]) {
|
|
68
|
+
const key = `${f.source}:${f.keyPoints[0] || ""}`;
|
|
69
|
+
if (!seen.has(key)) {
|
|
70
|
+
seen.add(key);
|
|
71
|
+
merged.push(f);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const store = {
|
|
75
|
+
feedbacks: merged.slice(0, 20),
|
|
76
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
77
|
+
};
|
|
78
|
+
writeFileSync(storePath, JSON.stringify(store, null, 2));
|
|
79
|
+
}
|
|
80
|
+
function findAgentOutputDirs(projectRoot) {
|
|
81
|
+
const dirs = [];
|
|
82
|
+
const tmpBase = process.env["TMPDIR"] || tmpdir() || "/tmp";
|
|
83
|
+
const projectPathEncoded = projectRoot.replace(/\//g, "-").replace(/^-/, "");
|
|
84
|
+
const pattern1 = join(tmpBase, "claude", `*${projectPathEncoded}*`, "tasks");
|
|
85
|
+
try {
|
|
86
|
+
const matches = globSync(pattern1);
|
|
87
|
+
dirs.push(...matches);
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
if (tmpBase !== "/private/tmp") {
|
|
91
|
+
const pattern2 = join(
|
|
92
|
+
"/private/tmp",
|
|
93
|
+
"claude",
|
|
94
|
+
`*${projectPathEncoded}*`,
|
|
95
|
+
"tasks"
|
|
96
|
+
);
|
|
97
|
+
try {
|
|
98
|
+
const matches = globSync(pattern2);
|
|
99
|
+
dirs.push(...matches);
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const homeClaudeDir = join(homedir(), ".claude", "projects");
|
|
104
|
+
if (existsSync(homeClaudeDir)) {
|
|
105
|
+
try {
|
|
106
|
+
const projectDirs = readdirSync(homeClaudeDir);
|
|
107
|
+
for (const d of projectDirs) {
|
|
108
|
+
const tasksDir = join(homeClaudeDir, d, "tasks");
|
|
109
|
+
if (existsSync(tasksDir)) {
|
|
110
|
+
dirs.push(tasksDir);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return [...new Set(dirs)];
|
|
117
|
+
}
|
|
118
|
+
class EnhancedHandoffGenerator {
|
|
119
|
+
projectRoot;
|
|
120
|
+
claudeProjectsDir;
|
|
121
|
+
constructor(projectRoot) {
|
|
122
|
+
this.projectRoot = projectRoot;
|
|
123
|
+
this.claudeProjectsDir = join(homedir(), ".claude", "projects");
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Generate a high-efficacy handoff
|
|
127
|
+
*/
|
|
128
|
+
async generate() {
|
|
129
|
+
const handoff = {
|
|
130
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
131
|
+
project: basename(this.projectRoot),
|
|
132
|
+
branch: this.getCurrentBranch(),
|
|
133
|
+
activeWork: await this.extractActiveWork(),
|
|
134
|
+
decisions: await this.extractDecisions(),
|
|
135
|
+
architecture: await this.extractArchitecture(),
|
|
136
|
+
blockers: await this.extractBlockers(),
|
|
137
|
+
reviewFeedback: await this.extractReviewFeedback(),
|
|
138
|
+
nextActions: await this.extractNextActions(),
|
|
139
|
+
codePatterns: await this.extractCodePatterns(),
|
|
140
|
+
estimatedTokens: 0
|
|
141
|
+
};
|
|
142
|
+
const markdown = this.toMarkdown(handoff);
|
|
143
|
+
handoff.estimatedTokens = countTokens(markdown);
|
|
144
|
+
return handoff;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Extract what we're currently building from git and recent files
|
|
148
|
+
*/
|
|
149
|
+
async extractActiveWork() {
|
|
150
|
+
const recentCommits = this.getRecentCommits(5);
|
|
151
|
+
const recentFiles = this.getRecentlyModifiedFiles(10);
|
|
152
|
+
let description = "Unknown - check git log for context";
|
|
153
|
+
let status = "in_progress";
|
|
154
|
+
if (recentCommits.length > 0) {
|
|
155
|
+
const lastCommit = recentCommits[0];
|
|
156
|
+
if (lastCommit.includes("feat:") || lastCommit.includes("implement")) {
|
|
157
|
+
description = lastCommit.replace(/^[a-f0-9]+\s+/, "");
|
|
158
|
+
} else if (lastCommit.includes("fix:")) {
|
|
159
|
+
description = "Bug fix: " + lastCommit.replace(/^[a-f0-9]+\s+/, "");
|
|
160
|
+
} else if (lastCommit.includes("chore:") || lastCommit.includes("refactor:")) {
|
|
161
|
+
description = lastCommit.replace(/^[a-f0-9]+\s+/, "");
|
|
162
|
+
} else {
|
|
163
|
+
description = lastCommit.replace(/^[a-f0-9]+\s+/, "");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const gitStatus = this.getGitStatus();
|
|
167
|
+
if (gitStatus.includes("conflict")) {
|
|
168
|
+
status = "blocked";
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
description,
|
|
172
|
+
status,
|
|
173
|
+
keyFiles: recentFiles.slice(0, 5),
|
|
174
|
+
progress: recentCommits.length > 0 ? `${recentCommits.length} commits in current session` : void 0
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Extract decisions from session store, git commits, and decision logs
|
|
179
|
+
*/
|
|
180
|
+
async extractDecisions() {
|
|
181
|
+
const decisions = [];
|
|
182
|
+
const sessionDecisions = loadSessionDecisions(this.projectRoot);
|
|
183
|
+
for (const d of sessionDecisions) {
|
|
184
|
+
decisions.push({
|
|
185
|
+
what: d.what,
|
|
186
|
+
why: d.why,
|
|
187
|
+
alternatives: d.alternatives
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const commits = this.getRecentCommits(20);
|
|
191
|
+
for (const commit of commits) {
|
|
192
|
+
if (commit.toLowerCase().includes("use ") || commit.toLowerCase().includes("switch to ") || commit.toLowerCase().includes("default to ") || commit.toLowerCase().includes("make ") && commit.toLowerCase().includes("optional")) {
|
|
193
|
+
const commitText = commit.replace(/^[a-f0-9]+\s+/, "");
|
|
194
|
+
if (!decisions.some((d) => d.what.includes(commitText.slice(0, 30)))) {
|
|
195
|
+
decisions.push({
|
|
196
|
+
what: commitText,
|
|
197
|
+
why: "See commit for details"
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const decisionsFile = join(
|
|
203
|
+
this.projectRoot,
|
|
204
|
+
".stackmemory",
|
|
205
|
+
"decisions.md"
|
|
206
|
+
);
|
|
207
|
+
if (existsSync(decisionsFile)) {
|
|
208
|
+
const content = readFileSync(decisionsFile, "utf-8");
|
|
209
|
+
const parsed = this.parseDecisionsFile(content);
|
|
210
|
+
decisions.push(...parsed);
|
|
211
|
+
}
|
|
212
|
+
return decisions.slice(0, 10);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Parse a decisions.md file
|
|
216
|
+
*/
|
|
217
|
+
parseDecisionsFile(content) {
|
|
218
|
+
const decisions = [];
|
|
219
|
+
const lines = content.split("\n");
|
|
220
|
+
let currentDecision = null;
|
|
221
|
+
for (const line of lines) {
|
|
222
|
+
if (line.startsWith("## ") || line.startsWith("### ")) {
|
|
223
|
+
if (currentDecision) {
|
|
224
|
+
decisions.push(currentDecision);
|
|
225
|
+
}
|
|
226
|
+
currentDecision = { what: line.replace(/^#+\s+/, ""), why: "" };
|
|
227
|
+
} else if (currentDecision && line.toLowerCase().includes("rationale:")) {
|
|
228
|
+
currentDecision.why = line.replace(/rationale:\s*/i, "").trim();
|
|
229
|
+
} else if (currentDecision && line.toLowerCase().includes("why:")) {
|
|
230
|
+
currentDecision.why = line.replace(/why:\s*/i, "").trim();
|
|
231
|
+
} else if (currentDecision && line.toLowerCase().includes("alternatives:")) {
|
|
232
|
+
currentDecision.alternatives = [];
|
|
233
|
+
} else if (currentDecision?.alternatives && line.trim().startsWith("-")) {
|
|
234
|
+
currentDecision.alternatives.push(line.replace(/^\s*-\s*/, "").trim());
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (currentDecision) {
|
|
238
|
+
decisions.push(currentDecision);
|
|
239
|
+
}
|
|
240
|
+
return decisions;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Extract architecture context from key files
|
|
244
|
+
*/
|
|
245
|
+
async extractArchitecture() {
|
|
246
|
+
const keyComponents = [];
|
|
247
|
+
const patterns = [];
|
|
248
|
+
const recentFiles = this.getRecentlyModifiedFiles(20);
|
|
249
|
+
const codeFiles = recentFiles.filter(
|
|
250
|
+
(f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".tsx")
|
|
251
|
+
);
|
|
252
|
+
for (const file of codeFiles.slice(0, 8)) {
|
|
253
|
+
const purpose = this.inferFilePurpose(file);
|
|
254
|
+
if (purpose) {
|
|
255
|
+
keyComponents.push({ file, purpose });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (codeFiles.some((f) => f.includes("/daemon/"))) {
|
|
259
|
+
patterns.push("Daemon/background process pattern");
|
|
260
|
+
}
|
|
261
|
+
if (codeFiles.some((f) => f.includes("/cli/"))) {
|
|
262
|
+
patterns.push("CLI command pattern");
|
|
263
|
+
}
|
|
264
|
+
if (codeFiles.some((f) => f.includes(".test.") || f.includes("__tests__"))) {
|
|
265
|
+
patterns.push("Test files present");
|
|
266
|
+
}
|
|
267
|
+
if (codeFiles.some((f) => f.includes("/core/"))) {
|
|
268
|
+
patterns.push("Core/domain separation");
|
|
269
|
+
}
|
|
270
|
+
return { keyComponents, patterns };
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Infer purpose from file name and path
|
|
274
|
+
*/
|
|
275
|
+
inferFilePurpose(filePath) {
|
|
276
|
+
const name = basename(filePath).replace(/\.(ts|js|tsx)$/, "");
|
|
277
|
+
const path = filePath.toLowerCase();
|
|
278
|
+
if (path.includes("daemon")) return "Background daemon/service";
|
|
279
|
+
if (path.includes("cli/command")) return "CLI command handler";
|
|
280
|
+
if (path.includes("config")) return "Configuration management";
|
|
281
|
+
if (path.includes("storage")) return "Data storage layer";
|
|
282
|
+
if (path.includes("handoff")) return "Session handoff logic";
|
|
283
|
+
if (path.includes("service")) return "Service orchestration";
|
|
284
|
+
if (path.includes("manager")) return "Resource/state management";
|
|
285
|
+
if (path.includes("handler")) return "Event/request handler";
|
|
286
|
+
if (path.includes("util") || path.includes("helper"))
|
|
287
|
+
return "Utility functions";
|
|
288
|
+
if (path.includes("types") || path.includes("interface"))
|
|
289
|
+
return "Type definitions";
|
|
290
|
+
if (path.includes("test")) return null;
|
|
291
|
+
if (name.includes("-")) {
|
|
292
|
+
return name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Extract blockers from git status and recent errors
|
|
298
|
+
*/
|
|
299
|
+
async extractBlockers() {
|
|
300
|
+
const blockers = [];
|
|
301
|
+
const gitStatus = this.getGitStatus();
|
|
302
|
+
if (gitStatus.includes("UU ") || gitStatus.includes("both modified")) {
|
|
303
|
+
blockers.push({
|
|
304
|
+
issue: "Merge conflict detected",
|
|
305
|
+
attempted: ["Check git status for affected files"],
|
|
306
|
+
status: "open"
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const testResult = execSync("npm test 2>&1 || true", {
|
|
311
|
+
encoding: "utf-8",
|
|
312
|
+
cwd: this.projectRoot,
|
|
313
|
+
timeout: 3e4
|
|
314
|
+
});
|
|
315
|
+
if (testResult.includes("FAIL") || testResult.includes("failed")) {
|
|
316
|
+
const failCount = (testResult.match(/(\d+) failed/i) || ["", "?"])[1];
|
|
317
|
+
blockers.push({
|
|
318
|
+
issue: `Test failures: ${failCount} tests failing`,
|
|
319
|
+
attempted: ["Run npm test for details"],
|
|
320
|
+
status: "open"
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const lintResult = execSync("npm run lint 2>&1 || true", {
|
|
327
|
+
encoding: "utf-8",
|
|
328
|
+
cwd: this.projectRoot,
|
|
329
|
+
timeout: 3e4
|
|
330
|
+
});
|
|
331
|
+
if (lintResult.includes("error") && !lintResult.includes("0 errors")) {
|
|
332
|
+
blockers.push({
|
|
333
|
+
issue: "Lint errors present",
|
|
334
|
+
attempted: ["Run npm run lint for details"],
|
|
335
|
+
status: "open"
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
} catch {
|
|
339
|
+
}
|
|
340
|
+
return blockers;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Extract review feedback from agent output files and persisted storage
|
|
344
|
+
*/
|
|
345
|
+
async extractReviewFeedback() {
|
|
346
|
+
const feedback = [];
|
|
347
|
+
const newFeedbacks = [];
|
|
348
|
+
const outputDirs = findAgentOutputDirs(this.projectRoot);
|
|
349
|
+
for (const tmpDir of outputDirs) {
|
|
350
|
+
if (!existsSync(tmpDir)) continue;
|
|
351
|
+
try {
|
|
352
|
+
const files = readdirSync(tmpDir).filter((f) => f.endsWith(".output"));
|
|
353
|
+
const recentFiles = files.map((f) => ({
|
|
354
|
+
name: f,
|
|
355
|
+
path: join(tmpDir, f),
|
|
356
|
+
stat: statSync(join(tmpDir, f))
|
|
357
|
+
})).filter((f) => Date.now() - f.stat.mtimeMs < 36e5).sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs).slice(0, 3);
|
|
358
|
+
for (const file of recentFiles) {
|
|
359
|
+
const content = readFileSync(file.path, "utf-8");
|
|
360
|
+
const extracted = this.extractKeyPointsFromReview(content);
|
|
361
|
+
if (extracted.keyPoints.length > 0) {
|
|
362
|
+
feedback.push(extracted);
|
|
363
|
+
newFeedbacks.push({
|
|
364
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
365
|
+
source: extracted.source,
|
|
366
|
+
keyPoints: extracted.keyPoints,
|
|
367
|
+
actionItems: extracted.actionItems,
|
|
368
|
+
sourceFile: file.name
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
} catch {
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (newFeedbacks.length > 0) {
|
|
376
|
+
saveReviewFeedback(this.projectRoot, newFeedbacks);
|
|
377
|
+
}
|
|
378
|
+
if (feedback.length === 0) {
|
|
379
|
+
const stored = loadReviewFeedback(this.projectRoot);
|
|
380
|
+
for (const s of stored.slice(0, 3)) {
|
|
381
|
+
feedback.push({
|
|
382
|
+
source: s.source,
|
|
383
|
+
keyPoints: s.keyPoints,
|
|
384
|
+
actionItems: s.actionItems
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return feedback.length > 0 ? feedback : void 0;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Extract key points from a review output
|
|
392
|
+
*/
|
|
393
|
+
extractKeyPointsFromReview(content) {
|
|
394
|
+
const keyPoints = [];
|
|
395
|
+
const actionItems = [];
|
|
396
|
+
let source = "Agent Review";
|
|
397
|
+
if (content.includes("Product Manager") || content.includes("product-manager")) {
|
|
398
|
+
source = "Product Manager";
|
|
399
|
+
} else if (content.includes("Staff Architect") || content.includes("staff-architect")) {
|
|
400
|
+
source = "Staff Architect";
|
|
401
|
+
}
|
|
402
|
+
const lines = content.split("\n");
|
|
403
|
+
let inRecommendations = false;
|
|
404
|
+
let inActionItems = false;
|
|
405
|
+
for (const line of lines) {
|
|
406
|
+
const trimmed = line.trim();
|
|
407
|
+
if (trimmed.toLowerCase().includes("recommendation") || trimmed.toLowerCase().includes("key finding")) {
|
|
408
|
+
inRecommendations = true;
|
|
409
|
+
inActionItems = false;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (trimmed.toLowerCase().includes("action") || trimmed.toLowerCase().includes("next step") || trimmed.toLowerCase().includes("priority")) {
|
|
413
|
+
inActionItems = true;
|
|
414
|
+
inRecommendations = false;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (trimmed.startsWith("- ") || trimmed.startsWith("* ") || /^\d+\.\s/.test(trimmed)) {
|
|
418
|
+
const point = trimmed.replace(/^[-*]\s+/, "").replace(/^\d+\.\s+/, "");
|
|
419
|
+
if (point.length > 10 && point.length < 200) {
|
|
420
|
+
if (inActionItems) {
|
|
421
|
+
actionItems.push(point);
|
|
422
|
+
} else if (inRecommendations) {
|
|
423
|
+
keyPoints.push(point);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
source,
|
|
430
|
+
keyPoints: keyPoints.slice(0, 5),
|
|
431
|
+
actionItems: actionItems.slice(0, 5)
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Extract next actions from todo state and git
|
|
436
|
+
*/
|
|
437
|
+
async extractNextActions() {
|
|
438
|
+
const actions = [];
|
|
439
|
+
const gitStatus = this.getGitStatus();
|
|
440
|
+
if (gitStatus.trim()) {
|
|
441
|
+
actions.push("Commit pending changes");
|
|
442
|
+
}
|
|
443
|
+
const recentFiles = this.getRecentlyModifiedFiles(5);
|
|
444
|
+
for (const file of recentFiles) {
|
|
445
|
+
try {
|
|
446
|
+
const fullPath = join(this.projectRoot, file);
|
|
447
|
+
if (existsSync(fullPath)) {
|
|
448
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
449
|
+
const todos = content.match(/\/\/\s*TODO:?\s*.+/gi) || [];
|
|
450
|
+
for (const todo of todos.slice(0, 2)) {
|
|
451
|
+
actions.push(todo.replace(/\/\/\s*TODO:?\s*/i, "TODO: "));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
} catch {
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const tasksFile = join(this.projectRoot, ".stackmemory", "tasks.json");
|
|
458
|
+
if (existsSync(tasksFile)) {
|
|
459
|
+
try {
|
|
460
|
+
const tasks = JSON.parse(readFileSync(tasksFile, "utf-8"));
|
|
461
|
+
const pending = tasks.filter(
|
|
462
|
+
(t) => t.status === "pending" || t.status === "in_progress"
|
|
463
|
+
);
|
|
464
|
+
for (const task of pending.slice(0, 3)) {
|
|
465
|
+
actions.push(task.title || task.description);
|
|
466
|
+
}
|
|
467
|
+
} catch {
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return actions.slice(0, 8);
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Extract established code patterns
|
|
474
|
+
*/
|
|
475
|
+
async extractCodePatterns() {
|
|
476
|
+
const patterns = [];
|
|
477
|
+
const eslintConfig = join(this.projectRoot, "eslint.config.js");
|
|
478
|
+
if (existsSync(eslintConfig)) {
|
|
479
|
+
const content = readFileSync(eslintConfig, "utf-8");
|
|
480
|
+
if (content.includes("argsIgnorePattern")) {
|
|
481
|
+
patterns.push("Underscore prefix for unused vars (_var)");
|
|
482
|
+
}
|
|
483
|
+
if (content.includes("ignores") && content.includes("test")) {
|
|
484
|
+
patterns.push("Test files excluded from lint");
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const tsconfig = join(this.projectRoot, "tsconfig.json");
|
|
488
|
+
if (existsSync(tsconfig)) {
|
|
489
|
+
const content = readFileSync(tsconfig, "utf-8");
|
|
490
|
+
if (content.includes('"strict": true')) {
|
|
491
|
+
patterns.push("TypeScript strict mode enabled");
|
|
492
|
+
}
|
|
493
|
+
if (content.includes("ES2022") || content.includes("ESNext")) {
|
|
494
|
+
patterns.push("ESM module system");
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return patterns;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Get recent git commits
|
|
501
|
+
*/
|
|
502
|
+
getRecentCommits(count) {
|
|
503
|
+
try {
|
|
504
|
+
const result = execSync(`git log --oneline -${count}`, {
|
|
505
|
+
encoding: "utf-8",
|
|
506
|
+
cwd: this.projectRoot
|
|
507
|
+
});
|
|
508
|
+
return result.trim().split("\n").filter(Boolean);
|
|
509
|
+
} catch {
|
|
510
|
+
return [];
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Get current git branch
|
|
515
|
+
*/
|
|
516
|
+
getCurrentBranch() {
|
|
517
|
+
try {
|
|
518
|
+
return execSync("git rev-parse --abbrev-ref HEAD", {
|
|
519
|
+
encoding: "utf-8",
|
|
520
|
+
cwd: this.projectRoot
|
|
521
|
+
}).trim();
|
|
522
|
+
} catch {
|
|
523
|
+
return "unknown";
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Get git status
|
|
528
|
+
*/
|
|
529
|
+
getGitStatus() {
|
|
530
|
+
try {
|
|
531
|
+
return execSync("git status --short", {
|
|
532
|
+
encoding: "utf-8",
|
|
533
|
+
cwd: this.projectRoot
|
|
534
|
+
});
|
|
535
|
+
} catch {
|
|
536
|
+
return "";
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Get recently modified files
|
|
541
|
+
*/
|
|
542
|
+
getRecentlyModifiedFiles(count) {
|
|
543
|
+
try {
|
|
544
|
+
const result = execSync(
|
|
545
|
+
`git diff --name-only HEAD~10 HEAD 2>/dev/null || git diff --name-only`,
|
|
546
|
+
{
|
|
547
|
+
encoding: "utf-8",
|
|
548
|
+
cwd: this.projectRoot
|
|
549
|
+
}
|
|
550
|
+
);
|
|
551
|
+
return result.trim().split("\n").filter(Boolean).slice(0, count);
|
|
552
|
+
} catch {
|
|
553
|
+
return [];
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Convert handoff to markdown
|
|
558
|
+
*/
|
|
559
|
+
toMarkdown(handoff) {
|
|
560
|
+
const lines = [];
|
|
561
|
+
lines.push(`# Session Handoff - ${handoff.timestamp.split("T")[0]}`);
|
|
562
|
+
lines.push("");
|
|
563
|
+
lines.push(`**Project**: ${handoff.project}`);
|
|
564
|
+
lines.push(`**Branch**: ${handoff.branch}`);
|
|
565
|
+
lines.push("");
|
|
566
|
+
lines.push("## Active Work");
|
|
567
|
+
lines.push(`- **Building**: ${handoff.activeWork.description}`);
|
|
568
|
+
lines.push(`- **Status**: ${handoff.activeWork.status}`);
|
|
569
|
+
if (handoff.activeWork.keyFiles.length > 0) {
|
|
570
|
+
lines.push(`- **Key files**: ${handoff.activeWork.keyFiles.join(", ")}`);
|
|
571
|
+
}
|
|
572
|
+
if (handoff.activeWork.progress) {
|
|
573
|
+
lines.push(`- **Progress**: ${handoff.activeWork.progress}`);
|
|
574
|
+
}
|
|
575
|
+
lines.push("");
|
|
576
|
+
if (handoff.decisions.length > 0) {
|
|
577
|
+
lines.push("## Key Decisions");
|
|
578
|
+
for (const d of handoff.decisions) {
|
|
579
|
+
lines.push(`1. **${d.what}**`);
|
|
580
|
+
if (d.why) {
|
|
581
|
+
lines.push(` - Rationale: ${d.why}`);
|
|
582
|
+
}
|
|
583
|
+
if (d.alternatives && d.alternatives.length > 0) {
|
|
584
|
+
lines.push(
|
|
585
|
+
` - Alternatives considered: ${d.alternatives.join(", ")}`
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
lines.push("");
|
|
590
|
+
}
|
|
591
|
+
if (handoff.architecture.keyComponents.length > 0) {
|
|
592
|
+
lines.push("## Architecture Context");
|
|
593
|
+
for (const c of handoff.architecture.keyComponents) {
|
|
594
|
+
lines.push(`- \`${c.file}\`: ${c.purpose}`);
|
|
595
|
+
}
|
|
596
|
+
if (handoff.architecture.patterns.length > 0) {
|
|
597
|
+
lines.push("");
|
|
598
|
+
lines.push("**Patterns**: " + handoff.architecture.patterns.join(", "));
|
|
599
|
+
}
|
|
600
|
+
lines.push("");
|
|
601
|
+
}
|
|
602
|
+
if (handoff.blockers.length > 0) {
|
|
603
|
+
lines.push("## Blockers");
|
|
604
|
+
for (const b of handoff.blockers) {
|
|
605
|
+
lines.push(`- **${b.issue}** [${b.status}]`);
|
|
606
|
+
if (b.attempted.length > 0) {
|
|
607
|
+
lines.push(` - Tried: ${b.attempted.join(", ")}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
lines.push("");
|
|
611
|
+
}
|
|
612
|
+
if (handoff.reviewFeedback && handoff.reviewFeedback.length > 0) {
|
|
613
|
+
lines.push("## Review Feedback");
|
|
614
|
+
for (const r of handoff.reviewFeedback) {
|
|
615
|
+
lines.push(`### ${r.source}`);
|
|
616
|
+
if (r.keyPoints.length > 0) {
|
|
617
|
+
lines.push("**Key Points**:");
|
|
618
|
+
for (const p of r.keyPoints) {
|
|
619
|
+
lines.push(`- ${p}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (r.actionItems.length > 0) {
|
|
623
|
+
lines.push("**Action Items**:");
|
|
624
|
+
for (const a of r.actionItems) {
|
|
625
|
+
lines.push(`- ${a}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
lines.push("");
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (handoff.nextActions.length > 0) {
|
|
632
|
+
lines.push("## Next Actions");
|
|
633
|
+
for (const a of handoff.nextActions) {
|
|
634
|
+
lines.push(`1. ${a}`);
|
|
635
|
+
}
|
|
636
|
+
lines.push("");
|
|
637
|
+
}
|
|
638
|
+
if (handoff.codePatterns && handoff.codePatterns.length > 0) {
|
|
639
|
+
lines.push("## Established Patterns");
|
|
640
|
+
for (const p of handoff.codePatterns) {
|
|
641
|
+
lines.push(`- ${p}`);
|
|
642
|
+
}
|
|
643
|
+
lines.push("");
|
|
644
|
+
}
|
|
645
|
+
lines.push("---");
|
|
646
|
+
lines.push(`*Estimated tokens: ~${handoff.estimatedTokens}*`);
|
|
647
|
+
lines.push(`*Generated at ${handoff.timestamp}*`);
|
|
648
|
+
return lines.join("\n");
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
export {
|
|
652
|
+
EnhancedHandoffGenerator
|
|
653
|
+
};
|
|
654
|
+
//# sourceMappingURL=enhanced-handoff.js.map
|