@compilr-dev/sdk 0.1.27 → 0.2.0

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.
Files changed (40) hide show
  1. package/dist/index.d.ts +6 -2
  2. package/dist/index.js +27 -1
  3. package/dist/meta-tools/registry.js +4 -2
  4. package/dist/team/activity.d.ts +21 -0
  5. package/dist/team/activity.js +34 -0
  6. package/dist/team/agent-selection.d.ts +53 -0
  7. package/dist/team/agent-selection.js +88 -0
  8. package/dist/team/artifacts.d.ts +175 -0
  9. package/dist/team/artifacts.js +279 -0
  10. package/dist/team/collision-utils.d.ts +16 -0
  11. package/dist/team/collision-utils.js +28 -0
  12. package/dist/team/context-resolver.d.ts +97 -0
  13. package/dist/team/context-resolver.js +322 -0
  14. package/dist/team/custom-agents.d.ts +68 -0
  15. package/dist/team/custom-agents.js +150 -0
  16. package/dist/team/delegation-tracker.d.ts +147 -0
  17. package/dist/team/delegation-tracker.js +215 -0
  18. package/dist/team/index.d.ts +34 -0
  19. package/dist/team/index.js +30 -0
  20. package/dist/team/interfaces.d.ts +36 -0
  21. package/dist/team/interfaces.js +7 -0
  22. package/dist/team/mention-parser.d.ts +64 -0
  23. package/dist/team/mention-parser.js +138 -0
  24. package/dist/team/shared-context.d.ts +293 -0
  25. package/dist/team/shared-context.js +673 -0
  26. package/dist/team/skill-requirements.d.ts +66 -0
  27. package/dist/team/skill-requirements.js +178 -0
  28. package/dist/team/task-assignment.d.ts +69 -0
  29. package/dist/team/task-assignment.js +123 -0
  30. package/dist/team/task-suggestion.d.ts +31 -0
  31. package/dist/team/task-suggestion.js +84 -0
  32. package/dist/team/team-agent.d.ts +201 -0
  33. package/dist/team/team-agent.js +492 -0
  34. package/dist/team/team.d.ts +297 -0
  35. package/dist/team/team.js +615 -0
  36. package/dist/team/tool-config.d.ts +110 -0
  37. package/dist/team/tool-config.js +739 -0
  38. package/dist/team/types.d.ts +211 -0
  39. package/dist/team/types.js +638 -0
  40. package/package.json +1 -1
@@ -0,0 +1,673 @@
1
+ /**
2
+ * SharedContextManager - Shared context across team agents
3
+ *
4
+ * Provides a lightweight, token-budgeted context layer that's injected
5
+ * into all agent system prompts. Enables cross-agent knowledge sharing.
6
+ *
7
+ * Token Budget: ~4000 tokens (2% of 200K context)
8
+ */
9
+ // =============================================================================
10
+ // Constants
11
+ // =============================================================================
12
+ const DEFAULT_TOKEN_BUDGET = 4000;
13
+ const MAX_DECISIONS = 10;
14
+ const MAX_DECISION_LENGTH = 100;
15
+ const MAX_MEMORY_SUMMARY_LENGTH = 2000;
16
+ const MAX_ACTIVITY_ITEMS = 5;
17
+ /** Simple token estimation (chars/4) — consumers can provide better estimators */
18
+ function estimateTokens(text) {
19
+ return Math.ceil(text.length / 4);
20
+ }
21
+ // =============================================================================
22
+ // SharedContextManager
23
+ // =============================================================================
24
+ /**
25
+ * Manages shared context across all team agents
26
+ */
27
+ export class SharedContextManager {
28
+ project;
29
+ team;
30
+ teamRoster = []; // Full roster for team awareness
31
+ recentActivity = []; // Activity feed for team awareness
32
+ decisions = [];
33
+ artifactIndex = [];
34
+ tokenBudget;
35
+ updatedAt = new Date();
36
+ constructor(options) {
37
+ this.project = {
38
+ id: options?.project?.id ?? null,
39
+ name: options?.project?.name ?? 'Unknown Project',
40
+ path: options?.project?.path ?? '.',
41
+ memorySummary: options?.project?.memorySummary ?? '',
42
+ };
43
+ this.team = {
44
+ agents: ['default'],
45
+ activeAgent: 'default',
46
+ };
47
+ this.tokenBudget = {
48
+ max: options?.maxTokens ?? DEFAULT_TOKEN_BUDGET,
49
+ current: 0,
50
+ };
51
+ this.updateTokenCount();
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // Project Management
55
+ // ---------------------------------------------------------------------------
56
+ /**
57
+ * Update project information
58
+ */
59
+ setProject(project) {
60
+ if (project.id !== undefined)
61
+ this.project.id = project.id;
62
+ if (project.name !== undefined)
63
+ this.project.name = project.name;
64
+ if (project.path !== undefined)
65
+ this.project.path = project.path;
66
+ if (project.memorySummary !== undefined) {
67
+ // Truncate if too long
68
+ this.project.memorySummary = project.memorySummary.slice(0, MAX_MEMORY_SUMMARY_LENGTH);
69
+ }
70
+ this.touch();
71
+ }
72
+ /**
73
+ * Get project information
74
+ */
75
+ getProject() {
76
+ return { ...this.project };
77
+ }
78
+ // ---------------------------------------------------------------------------
79
+ // Team Management
80
+ // ---------------------------------------------------------------------------
81
+ /**
82
+ * Update team roster
83
+ */
84
+ setTeam(agents, activeAgent) {
85
+ this.team.agents = [...agents];
86
+ if (activeAgent && agents.includes(activeAgent)) {
87
+ this.team.activeAgent = activeAgent;
88
+ }
89
+ this.touch();
90
+ }
91
+ /**
92
+ * Set active agent
93
+ */
94
+ setActiveAgent(agentId) {
95
+ if (this.team.agents.includes(agentId)) {
96
+ this.team.activeAgent = agentId;
97
+ this.touch();
98
+ }
99
+ }
100
+ /**
101
+ * Add an agent to the team
102
+ */
103
+ addAgent(agentId) {
104
+ if (!this.team.agents.includes(agentId)) {
105
+ this.team.agents.push(agentId);
106
+ this.touch();
107
+ }
108
+ }
109
+ /**
110
+ * Remove an agent from the team
111
+ */
112
+ removeAgent(agentId) {
113
+ const index = this.team.agents.indexOf(agentId);
114
+ if (index !== -1) {
115
+ this.team.agents.splice(index, 1);
116
+ // If active agent was removed, switch to default
117
+ if (this.team.activeAgent === agentId) {
118
+ this.team.activeAgent = 'default';
119
+ }
120
+ this.touch();
121
+ }
122
+ }
123
+ /**
124
+ * Get team roster
125
+ */
126
+ getTeam() {
127
+ return { ...this.team, agents: [...this.team.agents] };
128
+ }
129
+ // ---------------------------------------------------------------------------
130
+ // Team Roster (for Team Awareness)
131
+ // ---------------------------------------------------------------------------
132
+ /**
133
+ * Update the full team roster with agent details
134
+ * Called when team composition changes
135
+ */
136
+ updateTeamRoster(entries) {
137
+ this.teamRoster = entries.map((entry) => ({ ...entry }));
138
+ this.touch();
139
+ }
140
+ /**
141
+ * Set active agent in the roster
142
+ * Called on agent switch
143
+ */
144
+ setRosterActiveAgent(agentId) {
145
+ for (const entry of this.teamRoster) {
146
+ entry.isActive = entry.id === agentId;
147
+ }
148
+ // Also update the simple team info
149
+ if (this.team.agents.includes(agentId)) {
150
+ this.team.activeAgent = agentId;
151
+ }
152
+ this.touch();
153
+ }
154
+ /**
155
+ * Get the full team roster
156
+ */
157
+ getTeamRoster() {
158
+ return this.teamRoster.map((entry) => ({ ...entry }));
159
+ }
160
+ /**
161
+ * Check if team roster is available (more than just default agent)
162
+ */
163
+ hasTeamRoster() {
164
+ return this.teamRoster.length > 1;
165
+ }
166
+ /**
167
+ * Update task counts for an agent
168
+ * Called by REPL when todo list or work items change
169
+ */
170
+ updateAgentTaskCounts(agentId, todoCount, workItemCount) {
171
+ const entry = this.teamRoster.find((e) => e.id === agentId);
172
+ if (entry) {
173
+ entry.todoCount = todoCount;
174
+ entry.workItemCount = workItemCount;
175
+ this.touch();
176
+ }
177
+ }
178
+ /**
179
+ * Update task counts for all agents
180
+ * Called with a map of agentId -> { todoCount, workItemCount }
181
+ */
182
+ updateAllAgentTaskCounts(counts) {
183
+ for (const entry of this.teamRoster) {
184
+ if (entry.id in counts) {
185
+ const agentCounts = counts[entry.id];
186
+ entry.todoCount = agentCounts.todoCount;
187
+ entry.workItemCount = agentCounts.workItemCount;
188
+ }
189
+ else {
190
+ entry.todoCount = 0;
191
+ entry.workItemCount = 0;
192
+ }
193
+ }
194
+ this.touch();
195
+ }
196
+ // ---------------------------------------------------------------------------
197
+ // Activity Feed (for Team Awareness)
198
+ // ---------------------------------------------------------------------------
199
+ /**
200
+ * Record a team activity
201
+ * Called when significant actions occur (artifact created, task completed, etc.)
202
+ */
203
+ recordActivity(activity) {
204
+ const newActivity = {
205
+ ...activity,
206
+ timestamp: new Date(),
207
+ };
208
+ this.recentActivity.unshift(newActivity);
209
+ // Keep only MAX_ACTIVITY_ITEMS
210
+ if (this.recentActivity.length > MAX_ACTIVITY_ITEMS) {
211
+ this.recentActivity = this.recentActivity.slice(0, MAX_ACTIVITY_ITEMS);
212
+ }
213
+ this.touch();
214
+ }
215
+ /**
216
+ * Get recent activity
217
+ */
218
+ getRecentActivity() {
219
+ return this.recentActivity.map((a) => ({ ...a }));
220
+ }
221
+ /**
222
+ * Check if there is recent activity to display
223
+ */
224
+ hasRecentActivity() {
225
+ return this.recentActivity.length > 0;
226
+ }
227
+ /**
228
+ * Clear activity feed
229
+ */
230
+ clearActivity() {
231
+ this.recentActivity = [];
232
+ this.touch();
233
+ }
234
+ // ---------------------------------------------------------------------------
235
+ // Decision Management
236
+ // ---------------------------------------------------------------------------
237
+ /**
238
+ * Add a decision to the shared context
239
+ */
240
+ addDecision(decision) {
241
+ const id = `dec-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
242
+ // Truncate summary if too long
243
+ const summary = decision.summary.slice(0, MAX_DECISION_LENGTH);
244
+ const newDecision = {
245
+ id,
246
+ agent: decision.agent,
247
+ summary,
248
+ reasoning: decision.reasoning,
249
+ timestamp: new Date(),
250
+ };
251
+ this.decisions.unshift(newDecision);
252
+ // Keep only MAX_DECISIONS
253
+ if (this.decisions.length > MAX_DECISIONS) {
254
+ this.decisions = this.decisions.slice(0, MAX_DECISIONS);
255
+ }
256
+ this.touch();
257
+ return id;
258
+ }
259
+ /**
260
+ * Update/supersede a decision
261
+ */
262
+ supersededDecision(oldId, newDecision) {
263
+ // Mark old decision as superseded
264
+ const old = this.decisions.find((d) => d.id === oldId);
265
+ const newId = this.addDecision(newDecision);
266
+ if (old) {
267
+ old.supersededBy = newId;
268
+ }
269
+ return newId;
270
+ }
271
+ /**
272
+ * Get all decisions (active only by default)
273
+ */
274
+ getDecisions(includeSuperseded = false) {
275
+ if (includeSuperseded) {
276
+ return [...this.decisions];
277
+ }
278
+ return this.decisions.filter((d) => !d.supersededBy);
279
+ }
280
+ /**
281
+ * Clear all decisions
282
+ */
283
+ clearDecisions() {
284
+ this.decisions = [];
285
+ this.touch();
286
+ }
287
+ // ---------------------------------------------------------------------------
288
+ // Artifact Index Management
289
+ // ---------------------------------------------------------------------------
290
+ /**
291
+ * Update the artifact index (called by ArtifactStore)
292
+ */
293
+ setArtifactIndex(index) {
294
+ this.artifactIndex = [...index];
295
+ this.touch();
296
+ }
297
+ /**
298
+ * Add an artifact to the index
299
+ */
300
+ addArtifactToIndex(artifact) {
301
+ // Remove existing with same ID
302
+ this.artifactIndex = this.artifactIndex.filter((a) => a.id !== artifact.id);
303
+ this.artifactIndex.unshift(artifact);
304
+ this.touch();
305
+ }
306
+ /**
307
+ * Remove an artifact from the index
308
+ */
309
+ removeArtifactFromIndex(artifactId) {
310
+ this.artifactIndex = this.artifactIndex.filter((a) => a.id !== artifactId);
311
+ this.touch();
312
+ }
313
+ /**
314
+ * Get artifact index
315
+ */
316
+ getArtifactIndex() {
317
+ return [...this.artifactIndex];
318
+ }
319
+ // ---------------------------------------------------------------------------
320
+ // Token Budget Management
321
+ // ---------------------------------------------------------------------------
322
+ /**
323
+ * Update token count based on current content
324
+ */
325
+ updateTokenCount() {
326
+ let tokens = 0;
327
+ // Project info
328
+ tokens += estimateTokens(this.project.name);
329
+ tokens += estimateTokens(this.project.path);
330
+ tokens += estimateTokens(this.project.memorySummary);
331
+ // Team info
332
+ tokens += estimateTokens(this.team.agents.join(', '));
333
+ // Decisions
334
+ for (const decision of this.decisions) {
335
+ tokens += estimateTokens(decision.summary);
336
+ if (decision.reasoning) {
337
+ tokens += estimateTokens(decision.reasoning);
338
+ }
339
+ }
340
+ // Artifact index
341
+ for (const artifact of this.artifactIndex) {
342
+ tokens += estimateTokens(artifact.name);
343
+ tokens += estimateTokens(artifact.summary);
344
+ }
345
+ this.tokenBudget.current = tokens;
346
+ }
347
+ /**
348
+ * Check if adding content would exceed budget
349
+ */
350
+ wouldExceedBudget(additionalTokens) {
351
+ return this.tokenBudget.current + additionalTokens > this.tokenBudget.max;
352
+ }
353
+ /**
354
+ * Get token budget status
355
+ */
356
+ getTokenBudget() {
357
+ return { ...this.tokenBudget };
358
+ }
359
+ /**
360
+ * Get utilization percentage
361
+ */
362
+ getUtilization() {
363
+ return Math.round((this.tokenBudget.current / this.tokenBudget.max) * 100);
364
+ }
365
+ // ---------------------------------------------------------------------------
366
+ // Context Formatting
367
+ // ---------------------------------------------------------------------------
368
+ /**
369
+ * Format shared context for injection into system prompt.
370
+ * @param options.excludeRoster - If true, omit the team roster (it will be injected via anchor instead)
371
+ */
372
+ format(options) {
373
+ const lines = [];
374
+ lines.push('## SHARED TEAM CONTEXT');
375
+ lines.push('');
376
+ // Project info
377
+ lines.push(`**Project:** ${this.project.name}`);
378
+ lines.push(`**Path:** ${this.project.path}`);
379
+ if (this.project.memorySummary) {
380
+ lines.push(`**Summary:** ${this.project.memorySummary}`);
381
+ }
382
+ lines.push('');
383
+ // Team roster — skip if excluded (will be injected via anchor for live updates)
384
+ if (!options?.excludeRoster) {
385
+ if (this.hasTeamRoster()) {
386
+ lines.push(this.formatTeamRoster());
387
+ lines.push('');
388
+ }
389
+ else {
390
+ // Fallback to simple team info
391
+ lines.push(`**Team:** ${this.team.agents.map((a) => `$${a}`).join(', ')}`);
392
+ lines.push(`**Active:** $${this.team.activeAgent}`);
393
+ lines.push('');
394
+ // Team collaboration instructions (only if multiple agents)
395
+ if (this.team.agents.length > 1) {
396
+ lines.push('**Team Collaboration:**');
397
+ lines.push('- You are part of a multi-agent team. Each agent has a specialized role.');
398
+ lines.push('- If a question or task seems better suited for another team member, suggest asking them.');
399
+ lines.push('- Example: "This is more of an architecture question - you might want to ask $arch about this."');
400
+ lines.push('- Reference artifacts created by other agents when relevant using $mention syntax.');
401
+ lines.push('');
402
+ }
403
+ }
404
+ }
405
+ // Recent team activity (if any)
406
+ if (this.hasRecentActivity()) {
407
+ lines.push(this.formatRecentActivity());
408
+ lines.push('');
409
+ }
410
+ // Decisions
411
+ const activeDecisions = this.getDecisions(false);
412
+ if (activeDecisions.length > 0) {
413
+ lines.push('**Recent Decisions:**');
414
+ for (const decision of activeDecisions.slice(0, 5)) {
415
+ lines.push(`- [$${decision.agent}] ${decision.summary}`);
416
+ }
417
+ lines.push('');
418
+ }
419
+ // Artifacts
420
+ if (this.artifactIndex.length > 0) {
421
+ lines.push('**Available Artifacts:**');
422
+ for (const artifact of this.artifactIndex.slice(0, 10)) {
423
+ lines.push(`- "${artifact.name}" by $${artifact.agent} (${artifact.type})`);
424
+ }
425
+ lines.push('');
426
+ }
427
+ return lines.join('\n');
428
+ }
429
+ /**
430
+ * Format team roster as a table for team awareness
431
+ */
432
+ formatTeamRoster() {
433
+ const lines = [];
434
+ // Find the active agent for personalized guidance
435
+ const activeEntry = this.teamRoster.find((e) => e.isActive);
436
+ // Check if any agent has task counts to show
437
+ const showTaskCounts = this.teamRoster.some((e) => (e.todoCount ?? 0) > 0 || (e.workItemCount ?? 0) > 0);
438
+ lines.push('### Your Team');
439
+ lines.push('');
440
+ if (showTaskCounts) {
441
+ lines.push('| Agent | Role | Tasks | Expertise |');
442
+ lines.push('|-------|------|-------|-----------|');
443
+ }
444
+ else {
445
+ lines.push('| Agent | Role | Expertise |');
446
+ lines.push('|-------|------|-----------|');
447
+ }
448
+ for (const entry of this.teamRoster) {
449
+ const marker = entry.isActive ? '→ ' : ' ';
450
+ const youMarker = entry.isActive ? ' ← YOU' : '';
451
+ // Show first 3 expertise keywords to keep it concise
452
+ const expertise = entry.expertise.slice(0, 3).join(', ');
453
+ if (showTaskCounts) {
454
+ // Format task counts
455
+ const todoCount = entry.todoCount ?? 0;
456
+ const workItemCount = entry.workItemCount ?? 0;
457
+ const taskParts = [];
458
+ if (todoCount > 0)
459
+ taskParts.push(`${String(todoCount)} todo${todoCount > 1 ? 's' : ''}`);
460
+ if (workItemCount > 0)
461
+ taskParts.push(`${String(workItemCount)} item${workItemCount > 1 ? 's' : ''}`);
462
+ const taskStr = taskParts.length > 0 ? taskParts.join(', ') : '-';
463
+ lines.push(`| ${marker}$${entry.id} ${entry.mascot} | ${entry.displayName} | ${taskStr} | ${expertise}${youMarker} |`);
464
+ }
465
+ else {
466
+ lines.push(`| ${marker}$${entry.id} ${entry.mascot} | ${entry.displayName} | ${expertise}${youMarker} |`);
467
+ }
468
+ }
469
+ lines.push('');
470
+ // Handoff guidance - dynamic based on active agent and team
471
+ if (activeEntry) {
472
+ const yourExpertise = activeEntry.expertise.slice(0, 3).join(', ');
473
+ lines.push(`**Your expertise:** ${yourExpertise}`);
474
+ lines.push('');
475
+ }
476
+ lines.push('**Collaboration Guidelines:**');
477
+ lines.push('- If a task falls outside your expertise, suggest the appropriate specialist');
478
+ lines.push('- Example: "This would be better handled by $arch for architecture decisions"');
479
+ lines.push("- Do NOT attempt tasks far outside your expertise - it's better to suggest a handoff");
480
+ // Add specific handoff suggestions based on teammates
481
+ const handoffSuggestions = this.buildHandoffSuggestions(activeEntry?.id);
482
+ if (handoffSuggestions.length > 0) {
483
+ lines.push('');
484
+ lines.push('**When to involve teammates:**');
485
+ for (const suggestion of handoffSuggestions) {
486
+ lines.push(`- ${suggestion}`);
487
+ }
488
+ }
489
+ return lines.join('\n');
490
+ }
491
+ /**
492
+ * Build handoff suggestions based on team composition
493
+ */
494
+ buildHandoffSuggestions(activeAgentId) {
495
+ const suggestions = [];
496
+ for (const entry of this.teamRoster) {
497
+ // Skip the active agent
498
+ if (entry.id === activeAgentId)
499
+ continue;
500
+ // Build suggestion based on role
501
+ const role = entry.role;
502
+ switch (role) {
503
+ case 'arch':
504
+ suggestions.push(`Architecture/design questions → "Let me get $${entry.id}'s input on the design"`);
505
+ break;
506
+ case 'pm':
507
+ suggestions.push(`Planning/prioritization → "$${entry.id} can help with task breakdown"`);
508
+ break;
509
+ case 'qa':
510
+ suggestions.push(`Testing/quality concerns → "$${entry.id} should review the test strategy"`);
511
+ break;
512
+ case 'dev':
513
+ suggestions.push(`Implementation details → "$${entry.id} can handle the coding"`);
514
+ break;
515
+ case 'ops':
516
+ suggestions.push(`Deployment/infrastructure → "$${entry.id} can advise on CI/CD"`);
517
+ break;
518
+ case 'docs':
519
+ suggestions.push(`Documentation needs → "$${entry.id} can write the user docs"`);
520
+ break;
521
+ case 'ba':
522
+ suggestions.push(`Requirements clarity → "$${entry.id} can help clarify the business need"`);
523
+ break;
524
+ default:
525
+ // For default or custom agents, use their display name
526
+ if (entry.expertise.length > 0) {
527
+ suggestions.push(`${entry.expertise[0]} → "$${entry.id} specializes in this"`);
528
+ }
529
+ }
530
+ }
531
+ // Limit to 5 suggestions to keep it concise
532
+ return suggestions.slice(0, 5);
533
+ }
534
+ /**
535
+ * Format recent activity feed for team awareness
536
+ */
537
+ formatRecentActivity() {
538
+ const lines = [];
539
+ lines.push('### Recent Team Activity');
540
+ lines.push('');
541
+ for (const activity of this.recentActivity) {
542
+ const ago = this.formatTimeAgo(activity.timestamp);
543
+ lines.push(`- [${ago}] $${activity.agentId} ${activity.summary}`);
544
+ }
545
+ return lines.join('\n');
546
+ }
547
+ /**
548
+ * Format a timestamp as "Xm ago", "Xh ago", etc.
549
+ */
550
+ formatTimeAgo(timestamp) {
551
+ const now = Date.now();
552
+ const diff = now - timestamp.getTime();
553
+ const minutes = Math.floor(diff / 60000);
554
+ const hours = Math.floor(diff / 3600000);
555
+ if (minutes < 1) {
556
+ return 'just now';
557
+ }
558
+ else if (minutes < 60) {
559
+ return `${String(minutes)}m ago`;
560
+ }
561
+ else if (hours < 24) {
562
+ return `${String(hours)}h ago`;
563
+ }
564
+ else {
565
+ const days = Math.floor(hours / 24);
566
+ return `${String(days)}d ago`;
567
+ }
568
+ }
569
+ /**
570
+ * Format a compact version (for token-constrained situations)
571
+ */
572
+ formatCompact() {
573
+ const parts = [];
574
+ parts.push(`Project: ${this.project.name}`);
575
+ parts.push(`Team: ${this.team.agents.map((a) => `$${a}`).join(', ')}`);
576
+ const activeDecisions = this.getDecisions(false);
577
+ if (activeDecisions.length > 0) {
578
+ parts.push(`Decisions: ${String(activeDecisions.length)}`);
579
+ }
580
+ if (this.artifactIndex.length > 0) {
581
+ parts.push(`Artifacts: ${this.artifactIndex.map((a) => `"${a.name}"`).join(', ')}`);
582
+ }
583
+ return parts.join(' | ');
584
+ }
585
+ // ---------------------------------------------------------------------------
586
+ // Persistence
587
+ // ---------------------------------------------------------------------------
588
+ /**
589
+ * Mark as updated
590
+ */
591
+ touch() {
592
+ this.updatedAt = new Date();
593
+ this.updateTokenCount();
594
+ }
595
+ /**
596
+ * Serialize for persistence
597
+ */
598
+ serialize() {
599
+ return {
600
+ version: 1,
601
+ project: {
602
+ id: this.project.id,
603
+ name: this.project.name,
604
+ path: this.project.path,
605
+ memorySummary: this.project.memorySummary,
606
+ },
607
+ decisions: this.decisions.map((d) => ({
608
+ id: d.id,
609
+ agent: d.agent,
610
+ summary: d.summary,
611
+ reasoning: d.reasoning,
612
+ timestamp: d.timestamp.toISOString(),
613
+ supersededBy: d.supersededBy,
614
+ })),
615
+ artifactIndex: this.artifactIndex.map((a) => ({
616
+ id: a.id,
617
+ name: a.name,
618
+ agent: a.agent,
619
+ type: a.type,
620
+ summary: a.summary,
621
+ updatedAt: a.updatedAt.toISOString(),
622
+ })),
623
+ updatedAt: this.updatedAt.toISOString(),
624
+ };
625
+ }
626
+ /**
627
+ * Restore from serialized data
628
+ */
629
+ static fromSerialized(data) {
630
+ const manager = new SharedContextManager({
631
+ project: {
632
+ id: data.project.id,
633
+ name: data.project.name,
634
+ path: data.project.path,
635
+ memorySummary: data.project.memorySummary,
636
+ },
637
+ });
638
+ // Restore decisions
639
+ manager.decisions = data.decisions.map((d) => ({
640
+ id: d.id,
641
+ agent: d.agent,
642
+ summary: d.summary,
643
+ reasoning: d.reasoning,
644
+ timestamp: new Date(d.timestamp),
645
+ supersededBy: d.supersededBy,
646
+ }));
647
+ // Restore artifact index
648
+ manager.artifactIndex = data.artifactIndex.map((a) => ({
649
+ id: a.id,
650
+ name: a.name,
651
+ agent: a.agent,
652
+ type: a.type,
653
+ summary: a.summary,
654
+ updatedAt: new Date(a.updatedAt),
655
+ }));
656
+ manager.updatedAt = new Date(data.updatedAt);
657
+ manager.updateTokenCount();
658
+ return manager;
659
+ }
660
+ /**
661
+ * Get the full context object
662
+ */
663
+ getContext() {
664
+ return {
665
+ project: this.getProject(),
666
+ team: this.getTeam(),
667
+ decisions: this.getDecisions(),
668
+ artifactIndex: this.getArtifactIndex(),
669
+ tokenBudget: this.getTokenBudget(),
670
+ updatedAt: this.updatedAt,
671
+ };
672
+ }
673
+ }