| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734 |
- #!/usr/bin/env npx ts-node
- /**
- * Session Context Manager
- *
- * Manages persistent session context files to solve context fragmentation
- * in multi-agent orchestration. All agents read/update a shared context.md
- * file to maintain state across delegations.
- *
- * Usage:
- * import { createSession, loadSession, updateSession } from './session-context-manager';
- *
- * Location: .tmp/sessions/{session-id}/context.md
- */
- const fs = require('fs');
- const path = require('path');
- // Find project root (look for .git or package.json)
- function findProjectRoot(): string {
- let dir = process.cwd();
- while (dir !== path.dirname(dir)) {
- if (fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'package.json'))) {
- return dir;
- }
- dir = path.dirname(dir);
- }
- return process.cwd();
- }
- const PROJECT_ROOT = findProjectRoot();
- const SESSIONS_DIR = path.join(PROJECT_ROOT, '.tmp', 'sessions');
- // Types
- interface SessionContext {
- sessionId: string;
- feature: string;
- created: string;
- status: 'in_progress' | 'completed' | 'blocked';
- request: string;
- contextFiles: string[];
- referenceFiles: string[];
- architecture?: {
- boundedContext?: string;
- module?: string;
- verticalSlice?: string;
- };
- stories?: string[];
- priorities?: {
- riceScore?: number;
- wsjfScore?: number;
- releaseSlice?: string;
- };
- contracts?: Array<{
- type: string;
- name: string;
- path?: string;
- status: string;
- }>;
- adrs?: Array<{
- id: string;
- path?: string;
- title?: string;
- }>;
- progress: {
- currentStage: string;
- completedStages: string[];
- outputs: Record<string, string[]>;
- };
- decisions: Array<{
- timestamp: string;
- decision: string;
- rationale: string;
- }>;
- files: string[];
- exitCriteria: string[];
- }
- interface SessionUpdate {
- status?: 'in_progress' | 'completed' | 'blocked';
- contextFiles?: string[];
- referenceFiles?: string[];
- architecture?: {
- boundedContext?: string;
- module?: string;
- verticalSlice?: string;
- };
- stories?: string[];
- priorities?: {
- riceScore?: number;
- wsjfScore?: number;
- releaseSlice?: string;
- };
- contracts?: Array<{
- type: string;
- name: string;
- path?: string;
- status: string;
- }>;
- adrs?: Array<{
- id: string;
- path?: string;
- title?: string;
- }>;
- }
- interface StageOutput {
- stage: string;
- outputs: string[];
- }
- interface Decision {
- decision: string;
- rationale: string;
- }
- // Ensure sessions directory exists
- function ensureSessionsDir(): void {
- if (!fs.existsSync(SESSIONS_DIR)) {
- fs.mkdirSync(SESSIONS_DIR, { recursive: true });
- }
- }
- // Get session directory path
- function getSessionDir(sessionId: string): string {
- return path.join(SESSIONS_DIR, sessionId);
- }
- // Get context file path
- function getContextPath(sessionId: string): string {
- return path.join(getSessionDir(sessionId), 'context.md');
- }
- // Generate context.md content from session data
- function generateContextMarkdown(session: SessionContext): string {
- const lines: string[] = [];
- // Header
- lines.push(`# Task Context: ${session.feature}`);
- lines.push('');
- lines.push(`Session ID: ${session.sessionId}`);
- lines.push(`Created: ${session.created}`);
- lines.push(`Status: ${session.status}`);
- lines.push('');
- // Current Request
- lines.push('## Current Request');
- lines.push(session.request);
- lines.push('');
- // Context Files
- if (session.contextFiles.length > 0) {
- lines.push('## Context Files to Load');
- session.contextFiles.forEach(file => lines.push(`- ${file}`));
- lines.push('');
- }
- // Reference Files
- if (session.referenceFiles.length > 0) {
- lines.push('## Reference Files');
- session.referenceFiles.forEach(file => lines.push(`- ${file}`));
- lines.push('');
- }
- // Architecture
- if (session.architecture) {
- lines.push('## Architecture');
- if (session.architecture.boundedContext) {
- lines.push(`- Bounded Context: ${session.architecture.boundedContext}`);
- }
- if (session.architecture.module) {
- lines.push(`- Module: ${session.architecture.module}`);
- }
- if (session.architecture.verticalSlice) {
- lines.push(`- Vertical Slice: ${session.architecture.verticalSlice}`);
- }
- lines.push('');
- }
- // Stories
- if (session.stories && session.stories.length > 0) {
- lines.push('## User Stories');
- session.stories.forEach(story => lines.push(`- ${story}`));
- lines.push('');
- }
- // Priorities
- if (session.priorities) {
- lines.push('## Priorities');
- if (session.priorities.riceScore) {
- lines.push(`- RICE Score: ${session.priorities.riceScore}`);
- }
- if (session.priorities.wsjfScore) {
- lines.push(`- WSJF Score: ${session.priorities.wsjfScore}`);
- }
- if (session.priorities.releaseSlice) {
- lines.push(`- Release Slice: ${session.priorities.releaseSlice}`);
- }
- lines.push('');
- }
- // Contracts
- if (session.contracts && session.contracts.length > 0) {
- lines.push('## Contracts');
- session.contracts.forEach(contract => {
- lines.push(`- ${contract.type}: ${contract.name} (${contract.status})`);
- if (contract.path) {
- lines.push(` Path: ${contract.path}`);
- }
- });
- lines.push('');
- }
- // ADRs
- if (session.adrs && session.adrs.length > 0) {
- lines.push('## Architectural Decision Records');
- session.adrs.forEach(adr => {
- lines.push(`- ${adr.id}: ${adr.title || 'N/A'}`);
- if (adr.path) {
- lines.push(` Path: ${adr.path}`);
- }
- });
- lines.push('');
- }
- // Progress
- lines.push('## Progress');
- lines.push(`Current Stage: ${session.progress.currentStage}`);
- if (session.progress.completedStages.length > 0) {
- lines.push('');
- lines.push('Completed Stages:');
- session.progress.completedStages.forEach(stage => lines.push(`- ${stage}`));
- }
- if (Object.keys(session.progress.outputs).length > 0) {
- lines.push('');
- lines.push('Stage Outputs:');
- Object.entries(session.progress.outputs).forEach(([stage, outputs]) => {
- lines.push(`- ${stage}:`);
- outputs.forEach(output => lines.push(` - ${output}`));
- });
- }
- lines.push('');
- // Decisions
- if (session.decisions.length > 0) {
- lines.push('## Key Decisions');
- session.decisions.forEach(decision => {
- lines.push(`- [${decision.timestamp}] ${decision.decision}`);
- lines.push(` Rationale: ${decision.rationale}`);
- });
- lines.push('');
- }
- // Files Created
- if (session.files.length > 0) {
- lines.push('## Files Created');
- session.files.forEach(file => lines.push(`- ${file}`));
- lines.push('');
- }
- // Exit Criteria
- if (session.exitCriteria.length > 0) {
- lines.push('## Exit Criteria');
- session.exitCriteria.forEach(criterion => {
- const checked = session.status === 'completed' ? 'x' : ' ';
- lines.push(`- [${checked}] ${criterion}`);
- });
- lines.push('');
- }
- return lines.join('\n');
- }
- // Parse context.md back to session data
- function parseContextMarkdown(content: string): SessionContext | null {
- const lines = content.split('\n');
-
- // Extract header info
- const sessionIdMatch = content.match(/Session ID: (.+)/);
- const createdMatch = content.match(/Created: (.+)/);
- const statusMatch = content.match(/Status: (in_progress|completed|blocked)/);
- const featureMatch = content.match(/# Task Context: (.+)/);
- if (!sessionIdMatch || !createdMatch || !statusMatch || !featureMatch) {
- return null;
- }
- const session: SessionContext = {
- sessionId: sessionIdMatch[1],
- feature: featureMatch[1],
- created: createdMatch[1],
- status: statusMatch[1] as 'in_progress' | 'completed' | 'blocked',
- request: '',
- contextFiles: [],
- referenceFiles: [],
- progress: {
- currentStage: '',
- completedStages: [],
- outputs: {}
- },
- decisions: [],
- files: [],
- exitCriteria: []
- };
- // Parse sections
- let currentSection = '';
- let requestLines: string[] = [];
-
- for (let i = 0; i < lines.length; i++) {
- const line = lines[i];
-
- if (line.startsWith('## ')) {
- currentSection = line.substring(3);
- continue;
- }
- switch (currentSection) {
- case 'Current Request':
- if (line.trim()) {
- requestLines.push(line);
- }
- break;
- case 'Context Files to Load':
- if (line.startsWith('- ')) {
- session.contextFiles.push(line.substring(2));
- }
- break;
- case 'Reference Files':
- if (line.startsWith('- ')) {
- session.referenceFiles.push(line.substring(2));
- }
- break;
- case 'Exit Criteria':
- if (line.startsWith('- [')) {
- const criterion = line.substring(6); // Remove "- [ ] " or "- [x] "
- session.exitCriteria.push(criterion);
- }
- break;
- case 'Files Created':
- if (line.startsWith('- ')) {
- session.files.push(line.substring(2));
- }
- break;
- case 'Progress':
- if (line.startsWith('Current Stage: ')) {
- session.progress.currentStage = line.substring(15);
- }
- break;
- }
- }
- session.request = requestLines.join('\n');
- return session;
- }
- /**
- * Create a new session with initial context
- */
- export function createSession(
- feature: string,
- request: string,
- options: {
- contextFiles?: string[];
- referenceFiles?: string[];
- exitCriteria?: string[];
- architecture?: {
- boundedContext?: string;
- module?: string;
- verticalSlice?: string;
- };
- stories?: string[];
- priorities?: {
- riceScore?: number;
- wsjfScore?: number;
- releaseSlice?: string;
- };
- contracts?: Array<{
- type: string;
- name: string;
- path?: string;
- status: string;
- }>;
- adrs?: Array<{
- id: string;
- path?: string;
- title?: string;
- }>;
- } = {}
- ): { success: boolean; sessionId?: string; error?: string } {
- try {
- ensureSessionsDir();
- // Generate session ID from feature and timestamp
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
- const sessionId = `${feature}-${timestamp}`;
- const sessionDir = getSessionDir(sessionId);
- // Create session directory
- if (fs.existsSync(sessionDir)) {
- return { success: false, error: `Session ${sessionId} already exists` };
- }
- fs.mkdirSync(sessionDir, { recursive: true });
- // Create initial session context
- const session: SessionContext = {
- sessionId,
- feature,
- created: new Date().toISOString(),
- status: 'in_progress',
- request,
- contextFiles: options.contextFiles || [],
- referenceFiles: options.referenceFiles || [],
- architecture: options.architecture,
- stories: options.stories,
- priorities: options.priorities,
- contracts: options.contracts,
- adrs: options.adrs,
- progress: {
- currentStage: 'Stage 0: Context Loading',
- completedStages: [],
- outputs: {}
- },
- decisions: [],
- files: [],
- exitCriteria: options.exitCriteria || []
- };
- // Write context.md
- const contextPath = getContextPath(sessionId);
- const markdown = generateContextMarkdown(session);
- fs.writeFileSync(contextPath, markdown, 'utf-8');
- return { success: true, sessionId };
- } catch (error) {
- return { success: false, error: (error as Error).message };
- }
- }
- /**
- * Load session context from context.md
- */
- export function loadSession(sessionId: string): { success: boolean; session?: SessionContext; error?: string } {
- try {
- const contextPath = getContextPath(sessionId);
-
- if (!fs.existsSync(contextPath)) {
- return { success: false, error: `Session ${sessionId} not found` };
- }
- const content = fs.readFileSync(contextPath, 'utf-8');
- const session = parseContextMarkdown(content);
- if (!session) {
- return { success: false, error: `Failed to parse context.md for session ${sessionId}` };
- }
- return { success: true, session };
- } catch (error) {
- return { success: false, error: (error as Error).message };
- }
- }
- /**
- * Update session context with new information
- */
- export function updateSession(
- sessionId: string,
- updates: SessionUpdate
- ): { success: boolean; error?: string } {
- try {
- const loadResult = loadSession(sessionId);
- if (!loadResult.success || !loadResult.session) {
- return { success: false, error: loadResult.error };
- }
- const session = loadResult.session;
- // Apply updates
- if (updates.status) {
- session.status = updates.status;
- }
- if (updates.contextFiles) {
- session.contextFiles = [...new Set([...session.contextFiles, ...updates.contextFiles])];
- }
- if (updates.referenceFiles) {
- session.referenceFiles = [...new Set([...session.referenceFiles, ...updates.referenceFiles])];
- }
- if (updates.architecture) {
- session.architecture = { ...session.architecture, ...updates.architecture };
- }
- if (updates.stories) {
- session.stories = [...new Set([...(session.stories || []), ...updates.stories])];
- }
- if (updates.priorities) {
- session.priorities = { ...session.priorities, ...updates.priorities };
- }
- if (updates.contracts) {
- session.contracts = [...(session.contracts || []), ...updates.contracts];
- }
- if (updates.adrs) {
- session.adrs = [...(session.adrs || []), ...updates.adrs];
- }
- // Write updated context.md
- const contextPath = getContextPath(sessionId);
- const markdown = generateContextMarkdown(session);
- fs.writeFileSync(contextPath, markdown, 'utf-8');
- return { success: true };
- } catch (error) {
- return { success: false, error: (error as Error).message };
- }
- }
- /**
- * Mark a stage as complete and record outputs
- */
- export function markStageComplete(
- sessionId: string,
- stage: string,
- outputs: string[]
- ): { success: boolean; error?: string } {
- try {
- const loadResult = loadSession(sessionId);
- if (!loadResult.success || !loadResult.session) {
- return { success: false, error: loadResult.error };
- }
- const session = loadResult.session;
- // Add to completed stages if not already there
- if (!session.progress.completedStages.includes(stage)) {
- session.progress.completedStages.push(stage);
- }
- // Record outputs
- session.progress.outputs[stage] = outputs;
- // Write updated context.md
- const contextPath = getContextPath(sessionId);
- const markdown = generateContextMarkdown(session);
- fs.writeFileSync(contextPath, markdown, 'utf-8');
- return { success: true };
- } catch (error) {
- return { success: false, error: (error as Error).message };
- }
- }
- /**
- * Add a decision to the session context
- */
- export function addDecision(
- sessionId: string,
- decision: Decision
- ): { success: boolean; error?: string } {
- try {
- const loadResult = loadSession(sessionId);
- if (!loadResult.success || !loadResult.session) {
- return { success: false, error: loadResult.error };
- }
- const session = loadResult.session;
- // Add decision with timestamp
- session.decisions.push({
- timestamp: new Date().toISOString(),
- decision: decision.decision,
- rationale: decision.rationale
- });
- // Write updated context.md
- const contextPath = getContextPath(sessionId);
- const markdown = generateContextMarkdown(session);
- fs.writeFileSync(contextPath, markdown, 'utf-8');
- return { success: true };
- } catch (error) {
- return { success: false, error: (error as Error).message };
- }
- }
- /**
- * Add a file to the session's created files list
- */
- export function addFile(
- sessionId: string,
- filePath: string
- ): { success: boolean; error?: string } {
- try {
- const loadResult = loadSession(sessionId);
- if (!loadResult.success || !loadResult.session) {
- return { success: false, error: loadResult.error };
- }
- const session = loadResult.session;
- // Add file if not already tracked
- if (!session.files.includes(filePath)) {
- session.files.push(filePath);
- }
- // Write updated context.md
- const contextPath = getContextPath(sessionId);
- const markdown = generateContextMarkdown(session);
- fs.writeFileSync(contextPath, markdown, 'utf-8');
- return { success: true };
- } catch (error) {
- return { success: false, error: (error as Error).message };
- }
- }
- /**
- * Get a summary of the current session state
- */
- export function getSessionSummary(sessionId: string): {
- success: boolean;
- summary?: {
- sessionId: string;
- feature: string;
- status: string;
- currentStage: string;
- completedStages: number;
- totalDecisions: number;
- filesCreated: number;
- exitCriteriaMet: number;
- exitCriteriaTotal: number;
- };
- error?: string;
- } {
- try {
- const loadResult = loadSession(sessionId);
- if (!loadResult.success || !loadResult.session) {
- return { success: false, error: loadResult.error };
- }
- const session = loadResult.session;
- return {
- success: true,
- summary: {
- sessionId: session.sessionId,
- feature: session.feature,
- status: session.status,
- currentStage: session.progress.currentStage,
- completedStages: session.progress.completedStages.length,
- totalDecisions: session.decisions.length,
- filesCreated: session.files.length,
- exitCriteriaMet: session.status === 'completed' ? session.exitCriteria.length : 0,
- exitCriteriaTotal: session.exitCriteria.length
- }
- };
- } catch (error) {
- return { success: false, error: (error as Error).message };
- }
- }
- // CLI interface
- if (require.main === module) {
- const args = process.argv.slice(2);
- const command = args[0];
- switch (command) {
- case 'create': {
- const feature = args[1];
- const request = args[2];
- if (!feature || !request) {
- console.error('Usage: session-context-manager.ts create <feature> <request>');
- process.exit(1);
- }
- const result = createSession(feature, request);
- if (result.success) {
- console.log(`✅ Session created: ${result.sessionId}`);
- console.log(` Location: .tmp/sessions/${result.sessionId}/context.md`);
- } else {
- console.error(`❌ Error: ${result.error}`);
- process.exit(1);
- }
- break;
- }
- case 'load': {
- const sessionId = args[1];
- if (!sessionId) {
- console.error('Usage: session-context-manager.ts load <sessionId>');
- process.exit(1);
- }
- const result = loadSession(sessionId);
- if (result.success && result.session) {
- console.log(JSON.stringify(result.session, null, 2));
- } else {
- console.error(`❌ Error: ${result.error}`);
- process.exit(1);
- }
- break;
- }
- case 'summary': {
- const sessionId = args[1];
- if (!sessionId) {
- console.error('Usage: session-context-manager.ts summary <sessionId>');
- process.exit(1);
- }
- const result = getSessionSummary(sessionId);
- if (result.success && result.summary) {
- console.log('Session Summary:');
- console.log(` Feature: ${result.summary.feature}`);
- console.log(` Status: ${result.summary.status}`);
- console.log(` Current Stage: ${result.summary.currentStage}`);
- console.log(` Completed Stages: ${result.summary.completedStages}`);
- console.log(` Decisions Made: ${result.summary.totalDecisions}`);
- console.log(` Files Created: ${result.summary.filesCreated}`);
- console.log(` Exit Criteria: ${result.summary.exitCriteriaMet}/${result.summary.exitCriteriaTotal}`);
- } else {
- console.error(`❌ Error: ${result.error}`);
- process.exit(1);
- }
- break;
- }
- default:
- console.log('Session Context Manager');
- console.log('');
- console.log('Commands:');
- console.log(' create <feature> <request> - Create new session');
- console.log(' load <sessionId> - Load session context');
- console.log(' summary <sessionId> - Show session summary');
- console.log('');
- console.log('Programmatic usage:');
- console.log(' import { createSession, loadSession, updateSession } from "./session-context-manager"');
- break;
- }
- }
|