context-index.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. #!/usr/bin/env npx ts-node
  2. /**
  3. * Lightweight Context Index
  4. *
  5. * Solves context fragmentation WITHOUT passing massive context to subagents.
  6. * Orchestrator maintains a lightweight index (just paths and metadata).
  7. * Each subagent gets ONLY the specific files they need for their job.
  8. *
  9. * Key Principle:
  10. * - Orchestrator: "Here's the ONE file you need: .tmp/architecture/auth-system/contexts.json"
  11. * - NOT: "Here's the entire session context with everything from all previous agents"
  12. *
  13. * Usage:
  14. * import { createContextIndex, addAgentOutput, getContextForAgent, getFullContext } from './context-index';
  15. *
  16. * Location: .tmp/context-index/{feature}.json
  17. */
  18. import * as fs from 'fs';
  19. import * as path from 'path';
  20. // Find project root
  21. function findProjectRoot(): string {
  22. let dir = process.cwd();
  23. while (dir !== path.dirname(dir)) {
  24. if (fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'package.json'))) {
  25. return dir;
  26. }
  27. dir = path.dirname(dir);
  28. }
  29. return process.cwd();
  30. }
  31. const PROJECT_ROOT = findProjectRoot();
  32. const INDEX_DIR = path.join(PROJECT_ROOT, '.tmp', 'context-index');
  33. // Types
  34. interface AgentMetadata {
  35. [key: string]: any;
  36. }
  37. interface AgentOutput {
  38. outputs: string[];
  39. metadata: AgentMetadata;
  40. timestamp: string;
  41. }
  42. interface ContextIndex {
  43. feature: string;
  44. created: string;
  45. updated: string;
  46. agents: Record<string, AgentOutput>;
  47. contextFiles: string[];
  48. referenceFiles: string[];
  49. }
  50. interface AgentContext {
  51. feature: string;
  52. agentType: string;
  53. contextFiles: string[];
  54. referenceFiles: string[];
  55. agentOutputs: string[];
  56. metadata: AgentMetadata;
  57. }
  58. // Ensure index directory exists
  59. function ensureIndexDir(): void {
  60. if (!fs.existsSync(INDEX_DIR)) {
  61. fs.mkdirSync(INDEX_DIR, { recursive: true });
  62. }
  63. }
  64. // Get index file path
  65. function getIndexPath(feature: string): string {
  66. return path.join(INDEX_DIR, `${feature}.json`);
  67. }
  68. /**
  69. * Create a new context index for a feature
  70. *
  71. * @param feature - Feature identifier (kebab-case)
  72. * @param options - Initial context files and reference files
  73. * @returns Success status and error if any
  74. */
  75. export function createContextIndex(
  76. feature: string,
  77. options: {
  78. contextFiles?: string[];
  79. referenceFiles?: string[];
  80. } = {}
  81. ): { success: boolean; error?: string } {
  82. try {
  83. ensureIndexDir();
  84. const indexPath = getIndexPath(feature);
  85. // Don't overwrite existing index
  86. if (fs.existsSync(indexPath)) {
  87. return { success: false, error: `Context index for ${feature} already exists` };
  88. }
  89. const index: ContextIndex = {
  90. feature,
  91. created: new Date().toISOString(),
  92. updated: new Date().toISOString(),
  93. agents: {},
  94. contextFiles: options.contextFiles || [],
  95. referenceFiles: options.referenceFiles || []
  96. };
  97. fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf-8');
  98. return { success: true };
  99. } catch (error) {
  100. return { success: false, error: (error as Error).message };
  101. }
  102. }
  103. /**
  104. * Load context index for a feature
  105. *
  106. * @param feature - Feature identifier
  107. * @returns Success status with index data or error
  108. */
  109. export function loadContextIndex(feature: string): {
  110. success: boolean;
  111. index?: ContextIndex;
  112. error?: string;
  113. } {
  114. try {
  115. const indexPath = getIndexPath(feature);
  116. if (!fs.existsSync(indexPath)) {
  117. return { success: false, error: `Context index for ${feature} not found` };
  118. }
  119. const content = fs.readFileSync(indexPath, 'utf-8');
  120. const index: ContextIndex = JSON.parse(content);
  121. return { success: true, index };
  122. } catch (error) {
  123. return { success: false, error: (error as Error).message };
  124. }
  125. }
  126. /**
  127. * Add agent output to the context index
  128. *
  129. * @param feature - Feature identifier
  130. * @param agent - Agent type (e.g., "ArchitectureAnalyzer", "StoryMapper")
  131. * @param outputPath - Path to agent's output file
  132. * @param metadata - Agent-specific metadata (e.g., boundedContext, verticalSlice)
  133. * @returns Success status and error if any
  134. */
  135. export function addAgentOutput(
  136. feature: string,
  137. agent: string,
  138. outputPath: string,
  139. metadata: AgentMetadata = {}
  140. ): { success: boolean; error?: string } {
  141. try {
  142. const loadResult = loadContextIndex(feature);
  143. if (!loadResult.success || !loadResult.index) {
  144. return { success: false, error: loadResult.error };
  145. }
  146. const index = loadResult.index;
  147. // Initialize agent entry if doesn't exist
  148. if (!index.agents[agent]) {
  149. index.agents[agent] = {
  150. outputs: [],
  151. metadata: {},
  152. timestamp: new Date().toISOString()
  153. };
  154. }
  155. // Add output path if not already tracked
  156. if (!index.agents[agent].outputs.includes(outputPath)) {
  157. index.agents[agent].outputs.push(outputPath);
  158. }
  159. // Merge metadata
  160. index.agents[agent].metadata = {
  161. ...index.agents[agent].metadata,
  162. ...metadata
  163. };
  164. // Update timestamp
  165. index.agents[agent].timestamp = new Date().toISOString();
  166. index.updated = new Date().toISOString();
  167. // Save updated index
  168. const indexPath = getIndexPath(feature);
  169. fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf-8');
  170. return { success: true };
  171. } catch (error) {
  172. return { success: false, error: (error as Error).message };
  173. }
  174. }
  175. /**
  176. * Get minimal context for a specific agent type
  177. * Returns ONLY the files needed for that agent's job
  178. *
  179. * @param feature - Feature identifier
  180. * @param agentType - Agent type requesting context
  181. * @returns Agent-specific context with minimal file list
  182. */
  183. export function getContextForAgent(
  184. feature: string,
  185. agentType: string
  186. ): {
  187. success: boolean;
  188. context?: AgentContext;
  189. error?: string;
  190. } {
  191. try {
  192. const loadResult = loadContextIndex(feature);
  193. if (!loadResult.success || !loadResult.index) {
  194. return { success: false, error: loadResult.error };
  195. }
  196. const index = loadResult.index;
  197. // Agent-specific context rules
  198. const agentContext: AgentContext = {
  199. feature,
  200. agentType,
  201. contextFiles: [],
  202. referenceFiles: [],
  203. agentOutputs: [],
  204. metadata: {}
  205. };
  206. switch (agentType) {
  207. case 'ArchitectureAnalyzer':
  208. // Needs: architecture patterns, reference files
  209. agentContext.contextFiles = index.contextFiles.filter(f =>
  210. f.includes('architecture') || f.includes('patterns')
  211. );
  212. agentContext.referenceFiles = index.referenceFiles;
  213. break;
  214. case 'StoryMapper':
  215. // Needs: ArchitectureAnalyzer output + story mapping context
  216. agentContext.contextFiles = index.contextFiles.filter(f =>
  217. f.includes('story') || f.includes('user')
  218. );
  219. if (index.agents['ArchitectureAnalyzer']) {
  220. agentContext.agentOutputs = index.agents['ArchitectureAnalyzer'].outputs;
  221. agentContext.metadata = index.agents['ArchitectureAnalyzer'].metadata;
  222. }
  223. break;
  224. case 'PrioritizationEngine':
  225. // Needs: StoryMapper output + prioritization context
  226. agentContext.contextFiles = index.contextFiles.filter(f =>
  227. f.includes('prioritization') || f.includes('scoring')
  228. );
  229. if (index.agents['StoryMapper']) {
  230. agentContext.agentOutputs = index.agents['StoryMapper'].outputs;
  231. agentContext.metadata = index.agents['StoryMapper'].metadata;
  232. }
  233. break;
  234. case 'ContractManager':
  235. // Needs: ArchitectureAnalyzer output + contract patterns
  236. agentContext.contextFiles = index.contextFiles.filter(f =>
  237. f.includes('contract') || f.includes('api')
  238. );
  239. if (index.agents['ArchitectureAnalyzer']) {
  240. agentContext.agentOutputs = index.agents['ArchitectureAnalyzer'].outputs;
  241. agentContext.metadata = index.agents['ArchitectureAnalyzer'].metadata;
  242. }
  243. break;
  244. case 'ADRManager':
  245. // Needs: ArchitectureAnalyzer output + ADR templates
  246. agentContext.contextFiles = index.contextFiles.filter(f =>
  247. f.includes('adr') || f.includes('decision')
  248. );
  249. if (index.agents['ArchitectureAnalyzer']) {
  250. agentContext.agentOutputs = index.agents['ArchitectureAnalyzer'].outputs;
  251. agentContext.metadata = index.agents['ArchitectureAnalyzer'].metadata;
  252. }
  253. break;
  254. case 'TaskManager':
  255. // Needs: ALL previous agent outputs + task management context
  256. agentContext.contextFiles = index.contextFiles.filter(f =>
  257. f.includes('task') || f.includes('standards')
  258. );
  259. // Collect outputs from all agents
  260. Object.keys(index.agents).forEach(agent => {
  261. agentContext.agentOutputs.push(...index.agents[agent].outputs);
  262. agentContext.metadata[agent] = index.agents[agent].metadata;
  263. });
  264. break;
  265. case 'CoderAgent':
  266. // Needs: TaskManager output (subtask JSON) + coding standards
  267. agentContext.contextFiles = index.contextFiles.filter(f =>
  268. f.includes('standards') || f.includes('code-quality') || f.includes('security')
  269. );
  270. if (index.agents['TaskManager']) {
  271. agentContext.agentOutputs = index.agents['TaskManager'].outputs;
  272. }
  273. break;
  274. default:
  275. // Unknown agent: give minimal context
  276. agentContext.contextFiles = index.contextFiles;
  277. break;
  278. }
  279. return { success: true, context: agentContext };
  280. } catch (error) {
  281. return { success: false, error: (error as Error).message };
  282. }
  283. }
  284. /**
  285. * Get full context index (for orchestrator only)
  286. *
  287. * @param feature - Feature identifier
  288. * @returns Complete context index with all agent outputs
  289. */
  290. export function getFullContext(feature: string): {
  291. success: boolean;
  292. index?: ContextIndex;
  293. error?: string;
  294. } {
  295. return loadContextIndex(feature);
  296. }
  297. /**
  298. * Add context files to the index
  299. *
  300. * @param feature - Feature identifier
  301. * @param contextFiles - Array of context file paths to add
  302. * @returns Success status and error if any
  303. */
  304. export function addContextFiles(
  305. feature: string,
  306. contextFiles: string[]
  307. ): { success: boolean; error?: string } {
  308. try {
  309. const loadResult = loadContextIndex(feature);
  310. if (!loadResult.success || !loadResult.index) {
  311. return { success: false, error: loadResult.error };
  312. }
  313. const index = loadResult.index;
  314. // Add new context files (deduplicate)
  315. index.contextFiles = [...new Set([...index.contextFiles, ...contextFiles])];
  316. index.updated = new Date().toISOString();
  317. // Save updated index
  318. const indexPath = getIndexPath(feature);
  319. fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf-8');
  320. return { success: true };
  321. } catch (error) {
  322. return { success: false, error: (error as Error).message };
  323. }
  324. }
  325. /**
  326. * Add reference files to the index
  327. *
  328. * @param feature - Feature identifier
  329. * @param referenceFiles - Array of reference file paths to add
  330. * @returns Success status and error if any
  331. */
  332. export function addReferenceFiles(
  333. feature: string,
  334. referenceFiles: string[]
  335. ): { success: boolean; error?: string } {
  336. try {
  337. const loadResult = loadContextIndex(feature);
  338. if (!loadResult.success || !loadResult.index) {
  339. return { success: false, error: loadResult.error };
  340. }
  341. const index = loadResult.index;
  342. // Add new reference files (deduplicate)
  343. index.referenceFiles = [...new Set([...index.referenceFiles, ...referenceFiles])];
  344. index.updated = new Date().toISOString();
  345. // Save updated index
  346. const indexPath = getIndexPath(feature);
  347. fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf-8');
  348. return { success: true };
  349. } catch (error) {
  350. return { success: false, error: (error as Error).message };
  351. }
  352. }
  353. // CLI interface
  354. if (require.main === module) {
  355. const args: string[] = process.argv.slice(2);
  356. const command: string = args[0];
  357. switch (command) {
  358. case 'create': {
  359. const feature = args[1];
  360. if (!feature) {
  361. console.error('Usage: context-index.ts create <feature>');
  362. process.exit(1);
  363. }
  364. const result = createContextIndex(feature);
  365. if (result.success) {
  366. console.log(`✅ Context index created for: ${feature}`);
  367. console.log(` Location: .tmp/context-index/${feature}.json`);
  368. } else {
  369. console.error(`❌ Error: ${result.error}`);
  370. process.exit(1);
  371. }
  372. break;
  373. }
  374. case 'add-output': {
  375. const feature = args[1];
  376. const agent = args[2];
  377. const outputPath = args[3];
  378. const metadataJson = args[4];
  379. if (!feature || !agent || !outputPath) {
  380. console.error('Usage: context-index.ts add-output <feature> <agent> <outputPath> [metadata-json]');
  381. process.exit(1);
  382. }
  383. const metadata = metadataJson ? JSON.parse(metadataJson) : {};
  384. const result = addAgentOutput(feature, agent, outputPath, metadata);
  385. if (result.success) {
  386. console.log(`✅ Added ${agent} output: ${outputPath}`);
  387. } else {
  388. console.error(`❌ Error: ${result.error}`);
  389. process.exit(1);
  390. }
  391. break;
  392. }
  393. case 'get-context': {
  394. const feature = args[1];
  395. const agentType = args[2];
  396. if (!feature || !agentType) {
  397. console.error('Usage: context-index.ts get-context <feature> <agentType>');
  398. process.exit(1);
  399. }
  400. const result = getContextForAgent(feature, agentType);
  401. if (result.success && result.context) {
  402. console.log(JSON.stringify(result.context, null, 2));
  403. } else {
  404. console.error(`❌ Error: ${result.error}`);
  405. process.exit(1);
  406. }
  407. break;
  408. }
  409. case 'show': {
  410. const feature = args[1];
  411. if (!feature) {
  412. console.error('Usage: context-index.ts show <feature>');
  413. process.exit(1);
  414. }
  415. const result = getFullContext(feature);
  416. if (result.success && result.index) {
  417. console.log(JSON.stringify(result.index, null, 2));
  418. } else {
  419. console.error(`❌ Error: ${result.error}`);
  420. process.exit(1);
  421. }
  422. break;
  423. }
  424. default:
  425. console.log('Lightweight Context Index');
  426. console.log('');
  427. console.log('Commands:');
  428. console.log(' create <feature> - Create new context index');
  429. console.log(' add-output <feature> <agent> <path> [meta] - Add agent output');
  430. console.log(' get-context <feature> <agentType> - Get minimal context for agent');
  431. console.log(' show <feature> - Show full context index');
  432. console.log('');
  433. console.log('Programmatic usage:');
  434. console.log(' import { createContextIndex, addAgentOutput, getContextForAgent } from "./context-index"');
  435. break;
  436. }
  437. }