Skip to content

Commit 367d095

Browse files
feat(handoff): add compact format for reduced token usage
- Default to compact format (~50% smaller) - Add --verbose flag for full output - Truncate decisions/files for minimal context
1 parent 71453cf commit 367d095

File tree

2 files changed

+102
-4
lines changed

2 files changed

+102
-4
lines changed

src/cli/commands/handoff.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import { LinearTaskManager } from '../../features/tasks/linear-task-manager.js';
2121
import { logger } from '../../core/monitoring/logger.js';
2222
import { EnhancedHandoffGenerator } from '../../core/session/enhanced-handoff.js';
2323

24+
// Simple token estimation (avg 3.5 chars per token for English)
25+
const countTokens = (text: string): number => Math.ceil(text.length / 3.5);
26+
2427
// Handoff versioning - keep last N handoffs
2528
const MAX_HANDOFF_VERSIONS = 10;
2629

@@ -92,6 +95,7 @@ export function createCaptureCommand(): Command {
9295
.option('--no-commit', 'Skip git commit')
9396
.option('--copy', 'Copy the handoff prompt to clipboard')
9497
.option('--basic', 'Use basic handoff format instead of enhanced')
98+
.option('--verbose', 'Use verbose handoff format (default is compact)')
9599
.action(async (options) => {
96100
try {
97101
const projectRoot = process.cwd();
@@ -311,8 +315,15 @@ Generated by stackmemory capture at ${timestamp}
311315
// Use high-efficacy enhanced handoff generator (default)
312316
const enhancedGenerator = new EnhancedHandoffGenerator(projectRoot);
313317
const enhancedHandoff = await enhancedGenerator.generate();
314-
handoffPrompt = enhancedGenerator.toMarkdown(enhancedHandoff);
315-
console.log(`Estimated tokens: ~${enhancedHandoff.estimatedTokens}`);
318+
// Use compact format by default, verbose if requested
319+
handoffPrompt = options.verbose
320+
? enhancedGenerator.toMarkdown(enhancedHandoff)
321+
: enhancedGenerator.toCompact(enhancedHandoff);
322+
// Recalculate tokens for actual output format
323+
const actualTokens = countTokens(handoffPrompt);
324+
console.log(
325+
`Estimated tokens: ~${actualTokens}${options.verbose ? ' (verbose)' : ' (compact)'}`
326+
);
316327
}
317328

318329
// 7. Save handoff prompt (both latest and versioned)

src/core/session/enhanced-handoff.ts

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
writeFileSync,
1414
mkdirSync,
1515
} from 'fs';
16-
import { join, basename } from 'path';
16+
import { basename, join } from 'path';
1717
import { homedir, tmpdir } from 'os';
1818
import { globSync } from 'glob';
1919

@@ -825,7 +825,7 @@ export class EnhancedHandoffGenerator {
825825
}
826826

827827
/**
828-
* Convert handoff to markdown
828+
* Convert handoff to markdown (verbose format)
829829
*/
830830
toMarkdown(handoff: EnhancedHandoff): string {
831831
const lines: string[] = [];
@@ -935,4 +935,91 @@ export class EnhancedHandoffGenerator {
935935

936936
return lines.join('\n');
937937
}
938+
939+
/**
940+
* Convert handoff to compact format (~50% smaller)
941+
* Optimized for minimal context window usage
942+
*/
943+
toCompact(handoff: EnhancedHandoff): string {
944+
const lines: string[] = [];
945+
946+
// Header: single line
947+
lines.push(`# Handoff: ${handoff.project}@${handoff.branch}`);
948+
949+
// Active Work: condensed
950+
const status =
951+
handoff.activeWork.status === 'in_progress'
952+
? 'WIP'
953+
: handoff.activeWork.status;
954+
lines.push(`## Work: ${handoff.activeWork.description} [${status}]`);
955+
if (handoff.activeWork.keyFiles.length > 0) {
956+
// Use basenames only, limit to 5
957+
const files = handoff.activeWork.keyFiles
958+
.slice(0, 5)
959+
.map((f) => basename(f))
960+
.join(', ');
961+
const progress = handoff.activeWork.progress
962+
? ` (${handoff.activeWork.progress.replace(' in current session', '')})`
963+
: '';
964+
lines.push(`Files: ${files}${progress}`);
965+
}
966+
967+
// Decisions: single line each with arrow notation
968+
if (handoff.decisions.length > 0) {
969+
lines.push('');
970+
lines.push('## Decisions');
971+
for (const d of handoff.decisions.slice(0, 7)) {
972+
// Truncate long decisions
973+
const what = d.what.length > 40 ? d.what.slice(0, 37) + '...' : d.what;
974+
const why = d.why ? ` → ${d.why.slice(0, 50)}` : '';
975+
lines.push(`- ${what}${why}`);
976+
}
977+
}
978+
979+
// Blockers: terse format
980+
if (handoff.blockers.length > 0) {
981+
lines.push('');
982+
lines.push('## Blockers');
983+
for (const b of handoff.blockers) {
984+
const status = b.status === 'open' ? '!' : '✓';
985+
const tried = b.attempted.length > 0 ? ` → ${b.attempted[0]}` : '';
986+
lines.push(`${status} ${b.issue}${tried}`);
987+
}
988+
}
989+
990+
// Review Feedback: only if present, condensed
991+
if (handoff.reviewFeedback && handoff.reviewFeedback.length > 0) {
992+
lines.push('');
993+
lines.push('## Feedback');
994+
for (const r of handoff.reviewFeedback.slice(0, 2)) {
995+
lines.push(`[${r.source}]`);
996+
for (const p of r.keyPoints.slice(0, 3)) {
997+
lines.push(`- ${p.slice(0, 60)}`);
998+
}
999+
for (const a of r.actionItems.slice(0, 2)) {
1000+
lines.push(`→ ${a.slice(0, 60)}`);
1001+
}
1002+
}
1003+
}
1004+
1005+
// Next Actions: only top 3
1006+
if (handoff.nextActions.length > 0) {
1007+
lines.push('');
1008+
lines.push('## Next');
1009+
for (const a of handoff.nextActions.slice(0, 3)) {
1010+
lines.push(`- ${a.slice(0, 60)}`);
1011+
}
1012+
}
1013+
1014+
// Skip: Architecture, Patterns (can be inferred from codebase)
1015+
1016+
// Compact footer
1017+
lines.push('');
1018+
lines.push(`---`);
1019+
lines.push(
1020+
`~${handoff.estimatedTokens} tokens | ${handoff.timestamp.split('T')[0]}`
1021+
);
1022+
1023+
return lines.join('\n');
1024+
}
9381025
}

0 commit comments

Comments
 (0)