@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.
@@ -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