session-context-manager.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734
  1. #!/usr/bin/env npx ts-node
  2. /**
  3. * Session Context Manager
  4. *
  5. * Manages persistent session context files to solve context fragmentation
  6. * in multi-agent orchestration. All agents read/update a shared context.md
  7. * file to maintain state across delegations.
  8. *
  9. * Usage:
  10. * import { createSession, loadSession, updateSession } from './session-context-manager';
  11. *
  12. * Location: .tmp/sessions/{session-id}/context.md
  13. */
  14. const fs = require('fs');
  15. const path = require('path');
  16. // Find project root (look for .git or package.json)
  17. function findProjectRoot(): string {
  18. let dir = process.cwd();
  19. while (dir !== path.dirname(dir)) {
  20. if (fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'package.json'))) {
  21. return dir;
  22. }
  23. dir = path.dirname(dir);
  24. }
  25. return process.cwd();
  26. }
  27. const PROJECT_ROOT = findProjectRoot();
  28. const SESSIONS_DIR = path.join(PROJECT_ROOT, '.tmp', 'sessions');
  29. // Types
  30. interface SessionContext {
  31. sessionId: string;
  32. feature: string;
  33. created: string;
  34. status: 'in_progress' | 'completed' | 'blocked';
  35. request: string;
  36. contextFiles: string[];
  37. referenceFiles: string[];
  38. architecture?: {
  39. boundedContext?: string;
  40. module?: string;
  41. verticalSlice?: string;
  42. };
  43. stories?: string[];
  44. priorities?: {
  45. riceScore?: number;
  46. wsjfScore?: number;
  47. releaseSlice?: string;
  48. };
  49. contracts?: Array<{
  50. type: string;
  51. name: string;
  52. path?: string;
  53. status: string;
  54. }>;
  55. adrs?: Array<{
  56. id: string;
  57. path?: string;
  58. title?: string;
  59. }>;
  60. progress: {
  61. currentStage: string;
  62. completedStages: string[];
  63. outputs: Record<string, string[]>;
  64. };
  65. decisions: Array<{
  66. timestamp: string;
  67. decision: string;
  68. rationale: string;
  69. }>;
  70. files: string[];
  71. exitCriteria: string[];
  72. }
  73. interface SessionUpdate {
  74. status?: 'in_progress' | 'completed' | 'blocked';
  75. contextFiles?: string[];
  76. referenceFiles?: string[];
  77. architecture?: {
  78. boundedContext?: string;
  79. module?: string;
  80. verticalSlice?: string;
  81. };
  82. stories?: string[];
  83. priorities?: {
  84. riceScore?: number;
  85. wsjfScore?: number;
  86. releaseSlice?: string;
  87. };
  88. contracts?: Array<{
  89. type: string;
  90. name: string;
  91. path?: string;
  92. status: string;
  93. }>;
  94. adrs?: Array<{
  95. id: string;
  96. path?: string;
  97. title?: string;
  98. }>;
  99. }
  100. interface StageOutput {
  101. stage: string;
  102. outputs: string[];
  103. }
  104. interface Decision {
  105. decision: string;
  106. rationale: string;
  107. }
  108. // Ensure sessions directory exists
  109. function ensureSessionsDir(): void {
  110. if (!fs.existsSync(SESSIONS_DIR)) {
  111. fs.mkdirSync(SESSIONS_DIR, { recursive: true });
  112. }
  113. }
  114. // Get session directory path
  115. function getSessionDir(sessionId: string): string {
  116. return path.join(SESSIONS_DIR, sessionId);
  117. }
  118. // Get context file path
  119. function getContextPath(sessionId: string): string {
  120. return path.join(getSessionDir(sessionId), 'context.md');
  121. }
  122. // Generate context.md content from session data
  123. function generateContextMarkdown(session: SessionContext): string {
  124. const lines: string[] = [];
  125. // Header
  126. lines.push(`# Task Context: ${session.feature}`);
  127. lines.push('');
  128. lines.push(`Session ID: ${session.sessionId}`);
  129. lines.push(`Created: ${session.created}`);
  130. lines.push(`Status: ${session.status}`);
  131. lines.push('');
  132. // Current Request
  133. lines.push('## Current Request');
  134. lines.push(session.request);
  135. lines.push('');
  136. // Context Files
  137. if (session.contextFiles.length > 0) {
  138. lines.push('## Context Files to Load');
  139. session.contextFiles.forEach(file => lines.push(`- ${file}`));
  140. lines.push('');
  141. }
  142. // Reference Files
  143. if (session.referenceFiles.length > 0) {
  144. lines.push('## Reference Files');
  145. session.referenceFiles.forEach(file => lines.push(`- ${file}`));
  146. lines.push('');
  147. }
  148. // Architecture
  149. if (session.architecture) {
  150. lines.push('## Architecture');
  151. if (session.architecture.boundedContext) {
  152. lines.push(`- Bounded Context: ${session.architecture.boundedContext}`);
  153. }
  154. if (session.architecture.module) {
  155. lines.push(`- Module: ${session.architecture.module}`);
  156. }
  157. if (session.architecture.verticalSlice) {
  158. lines.push(`- Vertical Slice: ${session.architecture.verticalSlice}`);
  159. }
  160. lines.push('');
  161. }
  162. // Stories
  163. if (session.stories && session.stories.length > 0) {
  164. lines.push('## User Stories');
  165. session.stories.forEach(story => lines.push(`- ${story}`));
  166. lines.push('');
  167. }
  168. // Priorities
  169. if (session.priorities) {
  170. lines.push('## Priorities');
  171. if (session.priorities.riceScore) {
  172. lines.push(`- RICE Score: ${session.priorities.riceScore}`);
  173. }
  174. if (session.priorities.wsjfScore) {
  175. lines.push(`- WSJF Score: ${session.priorities.wsjfScore}`);
  176. }
  177. if (session.priorities.releaseSlice) {
  178. lines.push(`- Release Slice: ${session.priorities.releaseSlice}`);
  179. }
  180. lines.push('');
  181. }
  182. // Contracts
  183. if (session.contracts && session.contracts.length > 0) {
  184. lines.push('## Contracts');
  185. session.contracts.forEach(contract => {
  186. lines.push(`- ${contract.type}: ${contract.name} (${contract.status})`);
  187. if (contract.path) {
  188. lines.push(` Path: ${contract.path}`);
  189. }
  190. });
  191. lines.push('');
  192. }
  193. // ADRs
  194. if (session.adrs && session.adrs.length > 0) {
  195. lines.push('## Architectural Decision Records');
  196. session.adrs.forEach(adr => {
  197. lines.push(`- ${adr.id}: ${adr.title || 'N/A'}`);
  198. if (adr.path) {
  199. lines.push(` Path: ${adr.path}`);
  200. }
  201. });
  202. lines.push('');
  203. }
  204. // Progress
  205. lines.push('## Progress');
  206. lines.push(`Current Stage: ${session.progress.currentStage}`);
  207. if (session.progress.completedStages.length > 0) {
  208. lines.push('');
  209. lines.push('Completed Stages:');
  210. session.progress.completedStages.forEach(stage => lines.push(`- ${stage}`));
  211. }
  212. if (Object.keys(session.progress.outputs).length > 0) {
  213. lines.push('');
  214. lines.push('Stage Outputs:');
  215. Object.entries(session.progress.outputs).forEach(([stage, outputs]) => {
  216. lines.push(`- ${stage}:`);
  217. outputs.forEach(output => lines.push(` - ${output}`));
  218. });
  219. }
  220. lines.push('');
  221. // Decisions
  222. if (session.decisions.length > 0) {
  223. lines.push('## Key Decisions');
  224. session.decisions.forEach(decision => {
  225. lines.push(`- [${decision.timestamp}] ${decision.decision}`);
  226. lines.push(` Rationale: ${decision.rationale}`);
  227. });
  228. lines.push('');
  229. }
  230. // Files Created
  231. if (session.files.length > 0) {
  232. lines.push('## Files Created');
  233. session.files.forEach(file => lines.push(`- ${file}`));
  234. lines.push('');
  235. }
  236. // Exit Criteria
  237. if (session.exitCriteria.length > 0) {
  238. lines.push('## Exit Criteria');
  239. session.exitCriteria.forEach(criterion => {
  240. const checked = session.status === 'completed' ? 'x' : ' ';
  241. lines.push(`- [${checked}] ${criterion}`);
  242. });
  243. lines.push('');
  244. }
  245. return lines.join('\n');
  246. }
  247. // Parse context.md back to session data
  248. function parseContextMarkdown(content: string): SessionContext | null {
  249. const lines = content.split('\n');
  250. // Extract header info
  251. const sessionIdMatch = content.match(/Session ID: (.+)/);
  252. const createdMatch = content.match(/Created: (.+)/);
  253. const statusMatch = content.match(/Status: (in_progress|completed|blocked)/);
  254. const featureMatch = content.match(/# Task Context: (.+)/);
  255. if (!sessionIdMatch || !createdMatch || !statusMatch || !featureMatch) {
  256. return null;
  257. }
  258. const session: SessionContext = {
  259. sessionId: sessionIdMatch[1],
  260. feature: featureMatch[1],
  261. created: createdMatch[1],
  262. status: statusMatch[1] as 'in_progress' | 'completed' | 'blocked',
  263. request: '',
  264. contextFiles: [],
  265. referenceFiles: [],
  266. progress: {
  267. currentStage: '',
  268. completedStages: [],
  269. outputs: {}
  270. },
  271. decisions: [],
  272. files: [],
  273. exitCriteria: []
  274. };
  275. // Parse sections
  276. let currentSection = '';
  277. let requestLines: string[] = [];
  278. for (let i = 0; i < lines.length; i++) {
  279. const line = lines[i];
  280. if (line.startsWith('## ')) {
  281. currentSection = line.substring(3);
  282. continue;
  283. }
  284. switch (currentSection) {
  285. case 'Current Request':
  286. if (line.trim()) {
  287. requestLines.push(line);
  288. }
  289. break;
  290. case 'Context Files to Load':
  291. if (line.startsWith('- ')) {
  292. session.contextFiles.push(line.substring(2));
  293. }
  294. break;
  295. case 'Reference Files':
  296. if (line.startsWith('- ')) {
  297. session.referenceFiles.push(line.substring(2));
  298. }
  299. break;
  300. case 'Exit Criteria':
  301. if (line.startsWith('- [')) {
  302. const criterion = line.substring(6); // Remove "- [ ] " or "- [x] "
  303. session.exitCriteria.push(criterion);
  304. }
  305. break;
  306. case 'Files Created':
  307. if (line.startsWith('- ')) {
  308. session.files.push(line.substring(2));
  309. }
  310. break;
  311. case 'Progress':
  312. if (line.startsWith('Current Stage: ')) {
  313. session.progress.currentStage = line.substring(15);
  314. }
  315. break;
  316. }
  317. }
  318. session.request = requestLines.join('\n');
  319. return session;
  320. }
  321. /**
  322. * Create a new session with initial context
  323. */
  324. export function createSession(
  325. feature: string,
  326. request: string,
  327. options: {
  328. contextFiles?: string[];
  329. referenceFiles?: string[];
  330. exitCriteria?: string[];
  331. architecture?: {
  332. boundedContext?: string;
  333. module?: string;
  334. verticalSlice?: string;
  335. };
  336. stories?: string[];
  337. priorities?: {
  338. riceScore?: number;
  339. wsjfScore?: number;
  340. releaseSlice?: string;
  341. };
  342. contracts?: Array<{
  343. type: string;
  344. name: string;
  345. path?: string;
  346. status: string;
  347. }>;
  348. adrs?: Array<{
  349. id: string;
  350. path?: string;
  351. title?: string;
  352. }>;
  353. } = {}
  354. ): { success: boolean; sessionId?: string; error?: string } {
  355. try {
  356. ensureSessionsDir();
  357. // Generate session ID from feature and timestamp
  358. const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
  359. const sessionId = `${feature}-${timestamp}`;
  360. const sessionDir = getSessionDir(sessionId);
  361. // Create session directory
  362. if (fs.existsSync(sessionDir)) {
  363. return { success: false, error: `Session ${sessionId} already exists` };
  364. }
  365. fs.mkdirSync(sessionDir, { recursive: true });
  366. // Create initial session context
  367. const session: SessionContext = {
  368. sessionId,
  369. feature,
  370. created: new Date().toISOString(),
  371. status: 'in_progress',
  372. request,
  373. contextFiles: options.contextFiles || [],
  374. referenceFiles: options.referenceFiles || [],
  375. architecture: options.architecture,
  376. stories: options.stories,
  377. priorities: options.priorities,
  378. contracts: options.contracts,
  379. adrs: options.adrs,
  380. progress: {
  381. currentStage: 'Stage 0: Context Loading',
  382. completedStages: [],
  383. outputs: {}
  384. },
  385. decisions: [],
  386. files: [],
  387. exitCriteria: options.exitCriteria || []
  388. };
  389. // Write context.md
  390. const contextPath = getContextPath(sessionId);
  391. const markdown = generateContextMarkdown(session);
  392. fs.writeFileSync(contextPath, markdown, 'utf-8');
  393. return { success: true, sessionId };
  394. } catch (error) {
  395. return { success: false, error: (error as Error).message };
  396. }
  397. }
  398. /**
  399. * Load session context from context.md
  400. */
  401. export function loadSession(sessionId: string): { success: boolean; session?: SessionContext; error?: string } {
  402. try {
  403. const contextPath = getContextPath(sessionId);
  404. if (!fs.existsSync(contextPath)) {
  405. return { success: false, error: `Session ${sessionId} not found` };
  406. }
  407. const content = fs.readFileSync(contextPath, 'utf-8');
  408. const session = parseContextMarkdown(content);
  409. if (!session) {
  410. return { success: false, error: `Failed to parse context.md for session ${sessionId}` };
  411. }
  412. return { success: true, session };
  413. } catch (error) {
  414. return { success: false, error: (error as Error).message };
  415. }
  416. }
  417. /**
  418. * Update session context with new information
  419. */
  420. export function updateSession(
  421. sessionId: string,
  422. updates: SessionUpdate
  423. ): { success: boolean; error?: string } {
  424. try {
  425. const loadResult = loadSession(sessionId);
  426. if (!loadResult.success || !loadResult.session) {
  427. return { success: false, error: loadResult.error };
  428. }
  429. const session = loadResult.session;
  430. // Apply updates
  431. if (updates.status) {
  432. session.status = updates.status;
  433. }
  434. if (updates.contextFiles) {
  435. session.contextFiles = [...new Set([...session.contextFiles, ...updates.contextFiles])];
  436. }
  437. if (updates.referenceFiles) {
  438. session.referenceFiles = [...new Set([...session.referenceFiles, ...updates.referenceFiles])];
  439. }
  440. if (updates.architecture) {
  441. session.architecture = { ...session.architecture, ...updates.architecture };
  442. }
  443. if (updates.stories) {
  444. session.stories = [...new Set([...(session.stories || []), ...updates.stories])];
  445. }
  446. if (updates.priorities) {
  447. session.priorities = { ...session.priorities, ...updates.priorities };
  448. }
  449. if (updates.contracts) {
  450. session.contracts = [...(session.contracts || []), ...updates.contracts];
  451. }
  452. if (updates.adrs) {
  453. session.adrs = [...(session.adrs || []), ...updates.adrs];
  454. }
  455. // Write updated context.md
  456. const contextPath = getContextPath(sessionId);
  457. const markdown = generateContextMarkdown(session);
  458. fs.writeFileSync(contextPath, markdown, 'utf-8');
  459. return { success: true };
  460. } catch (error) {
  461. return { success: false, error: (error as Error).message };
  462. }
  463. }
  464. /**
  465. * Mark a stage as complete and record outputs
  466. */
  467. export function markStageComplete(
  468. sessionId: string,
  469. stage: string,
  470. outputs: string[]
  471. ): { success: boolean; error?: string } {
  472. try {
  473. const loadResult = loadSession(sessionId);
  474. if (!loadResult.success || !loadResult.session) {
  475. return { success: false, error: loadResult.error };
  476. }
  477. const session = loadResult.session;
  478. // Add to completed stages if not already there
  479. if (!session.progress.completedStages.includes(stage)) {
  480. session.progress.completedStages.push(stage);
  481. }
  482. // Record outputs
  483. session.progress.outputs[stage] = outputs;
  484. // Write updated context.md
  485. const contextPath = getContextPath(sessionId);
  486. const markdown = generateContextMarkdown(session);
  487. fs.writeFileSync(contextPath, markdown, 'utf-8');
  488. return { success: true };
  489. } catch (error) {
  490. return { success: false, error: (error as Error).message };
  491. }
  492. }
  493. /**
  494. * Add a decision to the session context
  495. */
  496. export function addDecision(
  497. sessionId: string,
  498. decision: Decision
  499. ): { success: boolean; error?: string } {
  500. try {
  501. const loadResult = loadSession(sessionId);
  502. if (!loadResult.success || !loadResult.session) {
  503. return { success: false, error: loadResult.error };
  504. }
  505. const session = loadResult.session;
  506. // Add decision with timestamp
  507. session.decisions.push({
  508. timestamp: new Date().toISOString(),
  509. decision: decision.decision,
  510. rationale: decision.rationale
  511. });
  512. // Write updated context.md
  513. const contextPath = getContextPath(sessionId);
  514. const markdown = generateContextMarkdown(session);
  515. fs.writeFileSync(contextPath, markdown, 'utf-8');
  516. return { success: true };
  517. } catch (error) {
  518. return { success: false, error: (error as Error).message };
  519. }
  520. }
  521. /**
  522. * Add a file to the session's created files list
  523. */
  524. export function addFile(
  525. sessionId: string,
  526. filePath: string
  527. ): { success: boolean; error?: string } {
  528. try {
  529. const loadResult = loadSession(sessionId);
  530. if (!loadResult.success || !loadResult.session) {
  531. return { success: false, error: loadResult.error };
  532. }
  533. const session = loadResult.session;
  534. // Add file if not already tracked
  535. if (!session.files.includes(filePath)) {
  536. session.files.push(filePath);
  537. }
  538. // Write updated context.md
  539. const contextPath = getContextPath(sessionId);
  540. const markdown = generateContextMarkdown(session);
  541. fs.writeFileSync(contextPath, markdown, 'utf-8');
  542. return { success: true };
  543. } catch (error) {
  544. return { success: false, error: (error as Error).message };
  545. }
  546. }
  547. /**
  548. * Get a summary of the current session state
  549. */
  550. export function getSessionSummary(sessionId: string): {
  551. success: boolean;
  552. summary?: {
  553. sessionId: string;
  554. feature: string;
  555. status: string;
  556. currentStage: string;
  557. completedStages: number;
  558. totalDecisions: number;
  559. filesCreated: number;
  560. exitCriteriaMet: number;
  561. exitCriteriaTotal: number;
  562. };
  563. error?: string;
  564. } {
  565. try {
  566. const loadResult = loadSession(sessionId);
  567. if (!loadResult.success || !loadResult.session) {
  568. return { success: false, error: loadResult.error };
  569. }
  570. const session = loadResult.session;
  571. return {
  572. success: true,
  573. summary: {
  574. sessionId: session.sessionId,
  575. feature: session.feature,
  576. status: session.status,
  577. currentStage: session.progress.currentStage,
  578. completedStages: session.progress.completedStages.length,
  579. totalDecisions: session.decisions.length,
  580. filesCreated: session.files.length,
  581. exitCriteriaMet: session.status === 'completed' ? session.exitCriteria.length : 0,
  582. exitCriteriaTotal: session.exitCriteria.length
  583. }
  584. };
  585. } catch (error) {
  586. return { success: false, error: (error as Error).message };
  587. }
  588. }
  589. // CLI interface
  590. if (require.main === module) {
  591. const args = process.argv.slice(2);
  592. const command = args[0];
  593. switch (command) {
  594. case 'create': {
  595. const feature = args[1];
  596. const request = args[2];
  597. if (!feature || !request) {
  598. console.error('Usage: session-context-manager.ts create <feature> <request>');
  599. process.exit(1);
  600. }
  601. const result = createSession(feature, request);
  602. if (result.success) {
  603. console.log(`✅ Session created: ${result.sessionId}`);
  604. console.log(` Location: .tmp/sessions/${result.sessionId}/context.md`);
  605. } else {
  606. console.error(`❌ Error: ${result.error}`);
  607. process.exit(1);
  608. }
  609. break;
  610. }
  611. case 'load': {
  612. const sessionId = args[1];
  613. if (!sessionId) {
  614. console.error('Usage: session-context-manager.ts load <sessionId>');
  615. process.exit(1);
  616. }
  617. const result = loadSession(sessionId);
  618. if (result.success && result.session) {
  619. console.log(JSON.stringify(result.session, null, 2));
  620. } else {
  621. console.error(`❌ Error: ${result.error}`);
  622. process.exit(1);
  623. }
  624. break;
  625. }
  626. case 'summary': {
  627. const sessionId = args[1];
  628. if (!sessionId) {
  629. console.error('Usage: session-context-manager.ts summary <sessionId>');
  630. process.exit(1);
  631. }
  632. const result = getSessionSummary(sessionId);
  633. if (result.success && result.summary) {
  634. console.log('Session Summary:');
  635. console.log(` Feature: ${result.summary.feature}`);
  636. console.log(` Status: ${result.summary.status}`);
  637. console.log(` Current Stage: ${result.summary.currentStage}`);
  638. console.log(` Completed Stages: ${result.summary.completedStages}`);
  639. console.log(` Decisions Made: ${result.summary.totalDecisions}`);
  640. console.log(` Files Created: ${result.summary.filesCreated}`);
  641. console.log(` Exit Criteria: ${result.summary.exitCriteriaMet}/${result.summary.exitCriteriaTotal}`);
  642. } else {
  643. console.error(`❌ Error: ${result.error}`);
  644. process.exit(1);
  645. }
  646. break;
  647. }
  648. default:
  649. console.log('Session Context Manager');
  650. console.log('');
  651. console.log('Commands:');
  652. console.log(' create <feature> <request> - Create new session');
  653. console.log(' load <sessionId> - Load session context');
  654. console.log(' summary <sessionId> - Show session summary');
  655. console.log('');
  656. console.log('Programmatic usage:');
  657. console.log(' import { createSession, loadSession, updateSession } from "./session-context-manager"');
  658. break;
  659. }
  660. }